I/O overrides — the architectural principle
We override at I/O boundaries. We never reimplement Snap's protocols. This is the central design rule.
The single architectural principle that determines what code we write and what code we don't:
Override at I/O boundaries. Never reimplement Snap's protocols.
Snap's bundle is the driver. The SDK's job is to provide the I/O the bundle expects (cookies, storage, gRPC transport, WebSocket) — and then to get out of the way. Anything Snap's bundle already does well, we don't redo. Anything Snap's bundle does through a browser primitive, we substitute the browser primitive.
This page enumerates every override the SDK does today and explains why each one is necessary.
The pattern
For each I/O boundary the bundle reaches through, the SDK provides an implementation that:
- Has the shape the bundle expects (e.g. a
WebSocketconstructor withonopen/onmessage/send/close, anIDBFactorywithopen(name, version)returning anIDBOpenDBRequest). - Routes through SDK-controlled state instead of the browser shell. Cookies go through tough-cookie. Storage goes through DataStore. WebSocket goes through Node
ws. gRPC goes through Nodefetch. - Persists where it belongs. Bundle writes to
localStorageland in the consumer's DataStore. Bundle cookie writes land in the same jar the SDK uses for its own gRPC. Bundle storage and SDK storage are not separate worlds.
The bundle never knows the difference.
The enumerated list
All overrides today, with the boundary each one lives at:
| Override | Boundary | What the bundle reaches for | What we substitute |
|---|---|---|---|
CookieContainerShim | happy-dom outgoing fetch | happy-dom's per-Window CookieContainer.getCookies(url, includeHttpOnly) (called by FetchRequestHeaderUtility.getRequestHeaders) | Patched prototype that delegates to a shared tough-cookie CookieJar rooted at DataStore key cookie_jar |
DocumentCookieShim | bundle JS document.cookie reads/writes | document.cookie getter/setter | Same shared tough-cookie jar; HttpOnly cookies filtered out per W3C spec |
WebSocketShim | bundle WebSocket connections | new WebSocket(url, protocols) | Wraps Node ws; reads cookies synchronously from the shared jar for the upgrade GET; projects incoming binary into a sandbox-realm Uint8Array.buffer |
LocalStorageShim | bundle localStorage | localStorage.getItem / setItem / removeItem / etc. | StorageShim over DataStore with prefix local_ |
SessionStorageShim | bundle sessionStorage | sessionStorage.getItem / setItem / etc. | StorageShim over DataStore with prefix session_ |
IndexedDbShim | bundle indexedDB | indexedDB.open(name, version) + transactions/object stores | DataStore with structured key indexdb_<dbName>__<storeName>__<key> |
Sandbox fetch + XMLHttpRequest shims | bundle's own outbound HTTP | globalThis.fetch(...) and new XMLHttpRequest() from inside the bundle | src/shims/fetch.ts + src/shims/xml-http-request.ts route through the host's nativeFetch with the shared cookie jar, optional throttle gate, and structured LogEvent emission |
All six listed shims are part of the Sandbox SDK_SHIMS pipeline; fetch and XHR are installed alongside the rest by the Sandbox constructor. Together they cover every I/O boundary the bundle reaches through.
Why each one is necessary
CookieContainerShim — happy-dom keeps its cookies in an in-memory #cookies array on its own private BrowserContext. Anything the bundle writes from JS (or that arrives via Set-Cookie on a happy-dom-driven fetch) lives only inside that Window. Without this shim:
- Bundle
fetch()calls don't carry our SSO bearer or cookie state. - Bundle
document.cookie = "..."writes are invisible to the host-realm gRPC client. - The host-realm gRPC client's cookies are invisible to bundle code.
After the shim: all three paths see one unified jar, which is also persisted to cookie_jar in the DataStore.
DocumentCookieShim — same pipe, different access path. Without it, bundle JS that reads or writes document.cookie from inside the sandbox doesn't see the SDK's cookies.
WebSocketShim — happy-dom ships a WebSocket shim, but it doesn't ride our cookie/UA fingerprint and it doesn't synchronously project incoming bytes into the sandbox realm. The handshake to aws.duplex.snapchat.com requires parent-domain cookies on the upgrade GET — those live in our shared tough-cookie jar, and we must read them synchronously at construction time because the bundle creates the WS synchronously. Without this shim presence/typing don't connect; with it, the bundle just sees WebSocket as a global the moment it's needed.
LocalStorageShim / SessionStorageShim — Snap's bundle stores feature flags, analytics ids, the UDS-prefixed Fidelius wrapped identity (local_uds_uds.e2eeIdentityKey.shared), and a long tail of other state in localStorage / sessionStorage. Without DataStore-backed shims those writes go to happy-dom's in-memory defaults and vanish on process exit — every cold start the bundle re-mints, re-registers, re-fetches.
IndexedDbShim — Snap's bundle stores additional state in IndexedDB. Same reasoning — without persistence, every boot looks like a fresh device to the bundle.
Sandbox fetch + XHR shims — bundle code uses both fetch(...) (newer SPA paths) and new XMLHttpRequest() (older AtlasGw / friending paths). Both shims route through the host's nativeFetch, attach the shared cookie jar at request time, optionally pass through the per-Sandbox throttle gate, and emit structured LogEvents for the network observability channel. Without them, bundle code would either hit happy-dom's default fetch (which doesn't ride our cookie jar) or fail outright on XHR calls.
What we don't do
By the same principle, the SDK explicitly does NOT reimplement:
- Fidelius E2E encryption / decryption. The bundle does it. We provide DataStore-backed storage and let the WASM mint, register, encrypt, and decrypt — the wrapped identity lands at
local_uds_uds.e2eeIdentityKey.sharedvia the bundle's UDS-prefixedlocalStoragewrites. - gRPC-Web framing. The bundle's own gRPC clients do it. The SDK's hand-rolled
transport/grpc-web.tsandtransport/proto-encode.tswere retired in this refactor — every public method composes calls to the bundle's webpack-resolved methods throughbundle/register.ts. - Protobuf encoding for messages routed through the bundle. Codecs live in webpack modules; we resolve them by module id and call them in place.
- Kameleon attestation. The WASM does it. We boot the Module with the right decorations and call
AttestationSession.instance().finalize(username). - The webpack runtime. We patch one line so
__webpack_require__leaks toglobalThis. We don't reimplement webpack.
For each thing we don't do, there's something we do adjacent to it that satisfies the bundle's I/O contract — the cookie jar, the DataStore, the per-Sandbox throttle, the instantiateWasm callback that injects local WASM bytes.
The shim layer
Each browser-primitive override is a class extending abstract Shim, registered in SDK_SHIMS, iterated by the Sandbox constructor.
Persistence model
Every persistent piece of state the SDK and the Snap bundle care about lands in a single DataStore. This is the key map and the reasoning behind it.