A small Express server that lets the Yuno Web SDK be loaded from a non-Yuno origin, so the white-label code paths — renamed globals, events, and DOM tokens — can be exercised end-to-end before shipping a partner-hosted integration.Documentation Index
Fetch the complete documentation index at: https://docs.y.uno/llms.txt
Use this file to discover all available pages before exploring further.
Why this server exists
The Yuno Web SDK historically ships with ayuno- prefixed surface: window.Yuno, the yuno-sdk-ready event, and yuno-* CSS classes and data attributes. The white-label release introduces a parallel surface:
| Legacy | White-label |
|---|---|
window.Yuno | window.SdkPayments |
yuno-sdk-ready event | sdk-payments-ready event |
yuno-* DOM tokens | sdk-payments-* DOM tokens |
*.y.uno. If it loads from a Yuno origin, branded leaks would slip through unnoticed. This server is the harness: it serves the SDK from localhost:9090 (or any non-Yuno hostname you point it at) and forwards every request to the real Yuno upstreams transparently.
How it works
- The browser only ever talks to the proxy origin. No
*.y.unorequest originates from the page itself — otherwise the SDK is not really being tested against a foreign host. - All upstream domains are reachable from the proxy. SDK assets, the card-form micro-app, the 3DS micro-app, the REST API, and the WebSocket service are all separate hostnames that must each be configurable.
- CORS is owned by the proxy, not the upstream. Upstream CORS would respond with
api.y.unoorigins; the proxy strips those and writes its own, echoing the caller.
Architecture
A single Node.js process built on three libraries:express— routing and middleware for HTTP.node-fetch— outbound HTTP to upstreams, for streaming bodies and header control.http-proxy— WebSocket upgrades, because Express middleware never seesUpgraderequests.
Request lifecycle
- CORS middleware runs first. It echoes the caller’s
Origin, handlesOPTIONSpreflight, and setsAccess-Control-Allow-Credentials: true. The proxy is the CORS authority. - Local routes match before anything else:
/,/static/*,/whitelabel-info. These never touch an upstream. - Backend pass-through matches
/v1/*and/v2/*(the Yuno REST surface used by the SDK) and forwards vianode-fetchtoBACKEND_URL. Hop-by-hop headers are stripped;access-control-*headers from the upstream are dropped so they cannot override the proxy’s own CORS. - SDK asset catch-all matches every remaining
GET/HEAD. A small dispatcher picks the right upstream based on path:- 3DS micro-app paths (
/challenge.html,/redirect.html,/session-id.html, and/assets/(challenge|redirect|session-id|validate-url)*) →SDK_3DS_UPSTREAM. - Card micro-app paths (
/v<semver>/pages/*,/v<semver>/assets/*) →SDK_CARD_UPSTREAM. - Everything else →
SDK_UPSTREAM.
- 3DS micro-app paths (
- WebSocket upgrades are handled on the underlying
http.Server, not Express./checkout-websocket-notification-ms/ws/*→BACKEND_WS_URL; everything else follows the same SDK / card / 3DS split as HTTP.
Version normalization
The main SDK bundle is versioned (/v1.7.4/main.js). Two complications:
- Partner pages may hardcode a version that the upstream no longer publishes.
- The “current” version drifts over time.
/v<x>/* SDK request to that version before sending it upstream. Card-app and 3DS paths are not rewritten — they have their own publish cadences.
SDK_MAIN_JS — pinning the SDK version
SDK_MAIN_JS decides which build of the SDK every browser loads. It does two related things:
- Template injection. The proxy serves
pages/index.htmlwith the literal__SDK_MAIN_JS__placeholder replaced by this path. A partner page can write<script src="__SDK_MAIN_JS__">and get e.g./v1.9.0/main.js. - Version normalization target. The proxy parses the
/v<x>/segment out ofSDK_MAIN_JSand rewrites any incoming/v<other>/main.js(and adjacent/v<other>/*.js,/v<other>/*.css) request to that same version. So ifSDK_MAIN_JS=/v1.9.0/main.js, a request for/v1.5.0/main.jsis silently fetched as/v1.9.0/main.jsfromSDK_UPSTREAM.
Resolution order at boot
SDK_MAIN_JS explicitly is the only way to skip the upstream fetch entirely — useful when versions.json is unreachable, you want a deterministic startup, or the upstream’s latest is not what you want to test against.
Format
<semver> matches [\d.]+(?:-[\w.]+)? — dot-separated numbers with an optional pre-release suffix.
| Value | When to use it |
|---|---|
/v1.9/main.js | Pin to a minor line (whatever the upstream publishes there) |
/v1.9.0/main.js | Pin to an exact patch — most common |
/v and contain /main.js for normalization to work. If the regex does not match, the path is used for template injection but version rewriting is disabled.
Common use cases
Test a specific SDK release end-to-end:v1.9.0, and any hardcoded /v<other>/*.js in partner pages is silently rewritten to v1.9.0 against SDK_UPSTREAM.
Quiet startup (no upstream versions.json call):
SDK_UPSTREAM is a local file server that does not expose versions.json.
Reproduce a customer bug on an older version:
v1.7.4 is still published on SDK_UPSTREAM, every browser that hits the proxy loads it — no rebuild required.
Gotchas
- Card and 3DS upstreams are not affected.
SDK_MAIN_JSonly pins the main SDK bundle’s version. The card micro-app (/v<x>/pages/*) and the 3DS micro-app keep their own version segments. - Setting it disables auto-detection. If a newer SDK version is published upstream, you keep loading the pinned version until you change the env var and restart.
- Restart required. The value is read once at boot. No hot reload.
- Mismatch with upstream is silent. Pinning to a version the upstream does not publish results in a
404for/v<your-pin>/main.js. Check the network tab to spot it. - No query strings or hash. Path only.
Building a server like this
1. Bootstrap the project
Use
node-fetch@2 (CommonJS) unless you have a build step — v3 is ESM-only.2. Define your upstream map
| Upstream var | Owns these paths |
|---|---|
SDK_UPSTREAM | /v<x>/main.js and everything not matched below |
SDK_CARD_UPSTREAM | /v<x>/pages/*, /v<x>/assets/* |
SDK_3DS_UPSTREAM | /challenge.html, /redirect.html, /session-id.html, /assets/(challenge|redirect|session-id|validate-url)* |
BACKEND_URL | /v1/*, /v2/* (REST API) |
BACKEND_WS_URL | /checkout-websocket-notification-ms/ws/{payment,enrollment} (WebSocket only) |
SDK_UPSTREAM and BACKEND_URL default to their parent when unset — keep this fallback behaviour, it makes single-env testing easier.
3. Set up CORS as the proxy’s responsibility
*) so credentialed requests work, and short-circuit preflights. When you later forward upstream responses, strip every access-control-* header from the upstream or it will override yours.
4. Filter hop-by-hop headers in both directions
Per RFC 7230 §6.1:content-length is the classic bug — node-fetch sets it, but if you also copy the original value the response truncates.
5. Forward the API surface
express.json() has already consumed the request body, so for non-GET methods you re-serialize req.body. Don’t try to req.pipe(upstream) after the body is consumed.
6. Catch-all proxy for SDK assets
7. Handle WebSocket upgrades on the http.Server
Express middleware does not seeUpgrade requests. Hook into the http server directly:
http-proxy negotiates ws:// vs wss:// from the target URL scheme.
8. Wire up environment variables
Usedotenv. Document per-environment values, not just defaults. For Yuno:
- SDK services follow
<service>[.<env>].y.uno—sdk-web.y.uno,sdk-web.staging.y.uno,sdk-web.dev.y.uno. - API surface uses
api[-<env>].y.uno—api.y.uno,api-staging.y.uno,api-dev.y.uno. Note the hyphen, not a dot. - The WebSocket service has no
api-prefix —<env>.y.unodirectly (y.uno,staging.y.uno,dev.y.uno).
.env.example so users don’t guess.
9. Ship a minimal landing page
Serveindex.html at / and a /whitelabel-info JSON endpoint that returns the resolved upstream config. The landing page calls the JSON endpoint and renders it — anyone debugging the proxy sees exactly what’s routed where without grepping logs.
Pitfalls and edge cases
- Route order matters. The SDK catch-all is registered last. Anything you
app.useafter it never runs. - CORS authority must be the proxy. Letting upstream
access-control-*headers through means credentialed cross-origin requests randomly fail when the upstream’s allow-list does not includelocalhost. - Hop-by-hop headers are bidirectional. Strip them on both legs.
content-lengthis the one that bites. - Body consumption is a one-shot. Once
express.json()parses a request, the underlying stream is drained. Re-serialize fromreq.bodyif you need to forward aPOST. - Streaming, not buffering, for assets.
upstream.body.pipe(res)keeps memory flat even for multi-MB bundles. - Version normalization is
SDK_UPSTREAM-only. Card and 3DS micro-apps have their own version segments — rewriting them breaks the upstream URL. - WebSocket auth is whatever headers
http-proxysees. Do not stripcookieorauthorizationfrom WS upgrades. .envis gitignored. Always keep.env.examplein sync — it is the spec for anyone cloning the repo.
Verification checklist
Once your proxy is running, verify in browser DevTools:- Network tab — no requests to any
*.y.unohost originate from the merchant page. All traffic is to the proxy origin. window.SdkPaymentsis defined;window.Yunois either undefined or a legacy alias.sdk-payments-readyevent fires ondocumentafter the SDK boots.- No
yuno-*DOM tokens in the rendered output, except for the trust-mark allow-list (yuno-badge,secure-by-yuno,yuno-typography-provider,yuno-c2p,yuno-redirect,yuno-debug,data-yuno-session-id).
Reference implementation
The full source lives in theyuno-sdk-web/white-label-proxy-server repository. About 290 lines in server.js — the whole thing fits in one file because there is no business logic, just routing and header hygiene.
See also
- White Label (Web SDK) — the SDK-side overrides this harness verifies.
- yuno-payments/yuno-sdk-web/tree/main/white-label-proxy-server — full source.
- RFC 7230 §6.1 — Connection-specific headers
- http-proxy WebSocket docs