Souspli

Souspli / Docs / How

The sealed container

In the code: the cage (src/main/cage.ts, src/main/protocol.ts, src/preload/).

The container runs arbitrary untrusted HTML, CSS and JavaScript with no network access and no ambient authority. "No network" is enforced at four independent layers, so that any single layer failing is not a breach. Each is tagged Layer N in src/main/cage.ts.

Layer 1 — process configuration

sandbox: true, contextIsolation: true, nodeIntegration: false, webSecurity: true, experimentalFeatures: false, and a fresh, non-persistent session partition per letter (thing-<id>, no persist: prefix, so in memory only). No letter can read another's storage, and nothing a letter stores survives it being closed.

The OS-level Chromium sandbox is a backstop against a renderer memory-corruption exploit. It is always configured; whether the host can initialise it is a separate question the test banner reports honestly — see security.

Layer 2 — request interception

Every request whose scheme is not thing: is cancelled. All permission requests and permission checks are denied. window.open is denied. will-navigate and will-redirect are prevented, so a letter cannot leave its own page.

Layer 3 — egress that bypasses request interception

A dead proxy (127.0.0.1:1) swallows any HTTP(S) or SOCKS traffic that escapes Layer 2. WebRTC — which webRequest cannot see at all — is closed with setWebRTCIPHandlingPolicy('disable_non_proxied_udp') plus a process-wide command-line policy.

Layer 4 — Content-Security-Policy

Injected on every thing: response by the session and stamped on the bytes by the protocol handler:

default-src 'none'; script-src 'unsafe-inline' thing:; style-src 'unsafe-inline' thing:;
img-src thing: data: blob:; media-src thing: blob:; font-src thing: data:;
connect-src 'none'; frame-src 'none'; form-action 'none'; base-uri 'none'

connect-src 'none' kills fetch, XHR, WebSocket and EventSource at the CSP layer; Layer 2 kills them again at the network layer. Both, on purpose.

This policy is also what shapes what a type can do: images, audio, video and fonts from its own attachments work; frames, forms, plugins and embedded documents do not — which is why an enclosed PDF can travel in a letter but cannot be displayed by it.

How a letter is served

A letter is loaded as thing://<random-id>/index.html, handled by protocol.handle() on the container's own session.

The security gate is the admitted attachment table plus the handler — not the bridge. A program can construct an att/ URL by hand; it still only resolves names that were in the signed manifest. Integrity was checked once, at admission, not per request: re-hashing a 200 MB video on every range request buys nothing when the store is unreachable to the letter.

The header a letter cannot paint on

The trusted header is a separate native view, composited in its own region of the window. The letter renders into a different view entirely, so it cannot paint over, resize away or overlay the header — a spatial guarantee rather than a z-index.

This is tested two ways: a geometry assertion in the escape battery, and at the pixel level in test/shell/chrome.spec.ts — a letter that floods its viewport and draws a fake "✓ signed" badge leaves the real badge intact and its own colour entirely absent from a capture of the header.

What a letter is told about you

viewerInfo() returns a locale and a colour-scheme preference. No time zone, no screen dimensions, no identifier — nothing that fingerprints. When in doubt a field is left out.