The bundle layer
How the chat and accounts bundles are loaded inside the per-instance Sandbox, and how SDK code reaches into them through a typed registry.
The bundle/ layer is where Snap's actual JavaScript bundles meet the SDK. Two webpacked bundles run inside the per-instance Sandbox's vm.Context — the accounts bundle (login + kameleon attestation) and the chat bundle (AtlasGw, friending, search, presence, Fidelius E2E). The SDK calls into them through a typed registry of late-bound getters; consumer-shape work happens one layer up in api/*.ts.
For non-E2E paths (login, friends, search, conversation enumeration) the SDK reaches Snap's webpack methods directly through the registry — no session harness needed. For inbound message decrypt and outbound text/media/presence sends, the bundle's own En.createMessagingSession(...) call drives Snap's WASM-side state machines; the SDK lazily brings up that session on the first client.messaging.on(...) / sendText(...) / setTyping(...) / client.stories.post(...) call. The bring-up lives in src/bundle/chat/standalone/session/setup.ts:setupBundleSession and is wired from src/api/messaging/bringup.ts:bringUpSession (and src/api/stories.ts:#bringUpSession); the captured session is surfaced via SetupBundleSessionOpts.onSession so outbound paths can drive sendMessageWithContent / sendTypingNotification / enterConversation against it. See Fidelius for the identity side of the story.
The bundle/ directory
| File | Role |
|---|---|
bundle/download.ts | First-use fetch of vendor/snap-bundle/ — accounts JS + chat JS + WASMs. Cached on disk afterwards. |
bundle/accounts-loader.ts | Loads the accounts bundle into the sandbox and boots the kameleon WASM. Per-Sandbox singleton (cached on sandbox.kameleon). |
bundle/chat-loader.ts | Loads the chat bundle main + sibling chunks. Source-patches the webpack runtime to leak p to __snapcap_chat_p, and source-patches a handful of closure-private symbols (Fi, HY, JY, AtlasGw A, friending jz) onto globalThis.__SNAPCAP_* so the registry can address them by name. |
bundle/chat-wasm-boot.ts | Boots the chat-bundle Emscripten WASM (e4fa…wasm) directly from webpack module 86818. Pre-fetched bytes are fed through Emscripten's instantiateWasm hook so we never spawn a Web Worker. Reserved for the upcoming messaging features; not on the auth path. |
bundle/prime.ts | Resolves cyclic-dependency rewires after chat-bundle eval — primes module 10409 (HY/JY/JZ codecs) and module 94704 (Zustand M.getState). Both required before any register.ts getter works. |
bundle/presence-bridge.ts | Synthesizes the duplex-client shape (registerHandler / send / addStreamListener / …) the bundle's state.presence.initializePresenceServiceTs(bridge) expects. Lets Messaging.setTyping / setViewing prime the bundle's modern presence path so today's mobile recipients honour the indicator. |
bundle/register.ts | Tier-1 typed registry. One getter per bundle entity; each takes Sandbox as its first arg and returns the live class instance / module export. No consumer-shape adaptation. |
bundle/types.ts | Bundle-realm type declarations — protobuf shapes, manager interfaces, presence + messaging slice surfaces, the Wreq webpack-require type. |
The boundary is sharp: anything that reads source-patched globals or webpack modules lives in bundle/; anything that translates between consumer types (UUID strings, plain numbers) and bundle types (16-byte buffers, {highBits, lowBits} pairs) lives in api/*.ts and uses the helpers in api/_helpers.ts.
Bring-up order
api/auth.ts:bringUp(ctx) is the single entry point. Idempotent — a per-context _bundlesLoaded marker short-circuits subsequent calls.
1. getKameleon(sandbox, { page: "www_login" })
──────────────────────────────────────────
Loads the accounts bundle into the sandbox and boots the kameleon
WASM. Side-effect: `__SNAPCAP_LOGIN_CLIENT_IMPL` (WebLoginService
ctor) and the unaryFactory in module 98747 become reachable.
2. patchSandboxLocationToWeb(ctx)
──────────────────────────────
Replace `self.location` with a Proxy that reports
`pathname=/web`. The chat bundle's module 13094 reads
`self.location.pathname` at top-level eval and throws if it
doesn't start with "/web".
3. ensureChatBundle(sandbox)
─────────────────────────
Load the chat bundle source. Source-patches the webpack runtime,
patches two empty Node-stub modules (91903 / 36675) to delegate to
real `Buffer` and `fs`, exposes closure-private symbols on
`__SNAPCAP_*`. Calls `prime.ts` after eval to fix up the cyclic
rewires.After step 3 the registry getters in bundle/register.ts resolve. api/auth.ts then drives WebLoginService (cold path) or short-circuits through the bundle's own SSO initialize (warm path) and the bundle's Zustand auth slice ends up populated.
The registry pattern
bundle/register.ts exports two kinds of entities:
- WIRED — a constant maps to a confirmed
__SNAPCAP_*name or webpack module id. The getter resolves the live entity from the sandbox at call time. - TODO — constant is
undefined; the getter throws an explicit "not yet mapped" error. The comment above the constant points at the byte offset to investigate.
// Simplified shape — see bundle/register.ts for the real one.
const G_ATLAS_CLIENT = "__SNAPCAP_ATLAS";
export function atlasGw(sandbox: Sandbox): AtlasGwClient {
const inst = sandbox.getGlobal<AtlasGwClient>(G_ATLAS_CLIENT);
if (!inst) throw new Error("AtlasGw client not patched into globalThis");
return inst;
}The api-layer call site looks like:
// api/friends.ts
import { friendActionClient } from "../bundle/register.ts";
export async function add(ctx: ClientContext, userId: string, source: number) {
const params = makeFriendIdParams([userId], source);
await friendActionClient(ctx.sandbox).addFriends({ page: undefined, params });
}Source-patching __SNAPCAP_* names in bundle/chat-loader.ts is the architectural twin of the globalThis.__snapcap_p=p runtime leak documented in the webpack runtime patch — same trick, applied to closure-private class instances instead of the require function.
Why per-Sandbox
Both loaders cache their state on the Sandbox instance (sandbox.chatBundleLoaded, sandbox.kameleon, sandbox.chatWasmBoot), not on a process-wide singleton. Two SnapcapClient instances each construct their own Sandbox, each load their own copy of the bundle into their own vm.Context, and end up with independent webpack runtimes, Zustand stores, and Embind class registrations.
That's the substrate for multi-tenant in one process: no shared mutable bundle state, so two clients targeting two accounts don't collide on localStorage, the auth slice, or the cookie jar (each Sandbox builds a jar from its own DataStore).
Fidelius identity
Once the chat bundle is loaded, Fidelius identity bootstrap is automatic — Snap's bundle code drives e2ee_E2EEKeyManager.generateKeyInitializationRequest(...) and InitializeWebKey server-side as part of session bring-up, persists the wrapped form via the bundle's UDS storage wrapper (which lands at local_uds_uds.e2eeIdentityKey.shared in the DataStore), and unwraps it on warm boots. The SDK provides the storage delegates (DataStore-backed localStorage / sessionStorage / indexedDB) and stays out of the way.
The plaintext private key never crosses back to JS — only the wrapped form is observable. See Fidelius.
What's deferred
- Wire-tested media sends.
messaging.sendImage(...)/sendSnap(...)/stories.post(...)compile and bring up cleanly via the bundle session'sgetConversationManager()/getSnapManager()send entries (pn/E$/HMon chat module 56639), but the integration suite hasn't exercised them end-to-end yet. - Inbound presence delegate. Outbound
setTyping/setViewing/setReadwork via the convMgr + presence-slice dual path (seesrc/api/messaging/), but the bundle's inbound presence delegate hook still drops typing/viewing/read events from peers — themessaging.on("typing" | "viewing" | "read", ...)subscriptions exist but never fire. - Bundle-remap script. When Snap rebuilds, the source-patch sites and module ids drift. A planned
scripts/remap-bundle.tswill fingerprint module factory bodies so re-mapping the ~30__SNAPCAP_*constants drops from hours to minutes. See the TODO at the bottom of the SDK README.
Architecture
The mental model: layered architecture from consumer API down through SnapcapClient, MessagingSession, Sandbox + Shims, to Snap's bundle JS + WASM.
The sandbox isolation model
How vm.Context + happy-dom Window own-prop projection gives the bundle a globalThis without touching the host realm.