snapcap
Internals

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:

  1. Has the shape the bundle expects (e.g. a WebSocket constructor with onopen/onmessage/send/close, an IDBFactory with open(name, version) returning an IDBOpenDBRequest).
  2. 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 Node fetch.
  3. Persists where it belongs. Bundle writes to localStorage land 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:

OverrideBoundaryWhat the bundle reaches forWhat we substitute
CookieContainerShimhappy-dom outgoing fetchhappy-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
DocumentCookieShimbundle JS document.cookie reads/writesdocument.cookie getter/setterSame shared tough-cookie jar; HttpOnly cookies filtered out per W3C spec
WebSocketShimbundle WebSocket connectionsnew 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
LocalStorageShimbundle localStoragelocalStorage.getItem / setItem / removeItem / etc.StorageShim over DataStore with prefix local_
SessionStorageShimbundle sessionStoragesessionStorage.getItem / setItem / etc.StorageShim over DataStore with prefix session_
IndexedDbShimbundle indexedDBindexedDB.open(name, version) + transactions/object storesDataStore with structured key indexdb_<dbName>__<storeName>__<key>
Sandbox fetch + XMLHttpRequest shimsbundle's own outbound HTTPglobalThis.fetch(...) and new XMLHttpRequest() from inside the bundlesrc/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.shared via the bundle's UDS-prefixed localStorage writes.
  • gRPC-Web framing. The bundle's own gRPC clients do it. The SDK's hand-rolled transport/grpc-web.ts and transport/proto-encode.ts were retired in this refactor — every public method composes calls to the bundle's webpack-resolved methods through bundle/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 to globalThis. 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.

On this page