Render untrusted remote content at a real, isolated browser origin whose network layer is a service worker — while the credentials that fetch it stay on a different origin, behind a capability.
The content gets a genuine origin with a genuine URL, so storage, cookies, history, WebAssembly and streaming all behave natively. Nothing is faked, and nothing is re-implemented in JavaScript.
A https://app.example the app: holds the credential and the transport
B https://<id>.browse.example untrusted content: holds nothing
The hard part is not the network. It is the certificate.
Giving B a real origin over HTTPS needs a certificate for B's hostname, and
issuing one per site exhausts CA rate limits, so a single wildcard has to cover
every site. But a wildcard matches exactly one label, and only the left-most
one (RFC 6125 §6.4.3; CA/Browser Forum Baseline Requirements) — *.*.example is
not a certificate any CA issues or any browser accepts. So the target cannot be
encoded into the hostname: a clearnet subdomain has too many labels, and a long
identifier crowds the 63-character DNS label limit on its own.
So the target is not named at all. The origin is a short, stable hash of it:
id = base32(sha256(canonical))[:20]
B = https://<id>.<suffix>
One wildcard now covers every site. The hash is deterministic, so a site keeps
its cookies and storage across sessions. And because the frame knows only its own
id, it supplies a path and never a host — one browse origin cannot ask for
another's content.
cfg := realorigin.Config{
Addr: "127.0.0.1:7998",
Suffix: ".browse.example", // a different registrable domain from the app's
AppOrigin: "https://app.example",
}
go cfg.ListenAndServe(ctx) // serves the worker and the shell, nothing elseServe realorigin.ResponderJS() from the app origin, first-party, and give
it a transport:
realOrigin.configure({
suffix: '.browse.example',
fetch: function (target, req) {
// req: {url, method, headers, body(ArrayBuffer|null), path}
return myTransport(target, req); // → {status, headers, body}
},
});
var id = await realOrigin.register('https://example.com');
frame.src = 'https://' + id + '.browse.example/';That is the whole integration. The transport is the only part you write.
A transport that has to set up a route before it can fetch anything leaves the frame blank for a while, and a bare spinner wastes that time. Stream it:
realOrigin.progress('connecting through exit …', id); // just that frame
realOrigin.progress('route established'); // every loading frameLines reach the frame until its first response lands — which is exactly the interstitial's lifetime, since the document is replaced after that.
If the built-in shell is too plain for what you have to say, replace it:
cfg := realorigin.Config{ /* … */ Shell: myShell }Start from realorigin.BootstrapHTML(). A replacement must speak the same bridge
protocol, and gets the same __APP_ORIGIN__ / __SUFFIX__ substitutions.
{ type: 'realorigin-hello', shortid } → a private port, bound to one target
{ type: 'realorigin-fetch', req: { url, method, headers, body } }
→ { status, headers, body } | { error }
error becomes 502, sixty seconds of silence becomes 504, and content-length,
transfer-encoding and connection are stripped so the browser recomputes its
own framing.
go run ./cmd/realorigin-demo
Then open http://localhost:7999. The demo's transport is plain HTTP through its own process, which makes it a real-origin proxy that sidesteps CORS. Swap that one function for a mesh, an onion route, a peer-to-peer fetch or a decrypted archive and nothing else changes.
The demo fetches server-side, and that is not the interesting case. Its transport is an HTTP client in the demo's own process, chosen because it needs no infrastructure to try. A transport that runs in the visitor's tab — a wasm client, a WebRTC peer, a local decrypted archive — is what gives this its unusual property: the server then serves the shell and the worker and nothing else, and carries, sees and stores none of the traffic. Nothing scales with how much anyone browses. That is how skywire uses it, and the substrate is identical either way; only the transport moves.
Each of these was paid for once already.
- A wildcard spans one label. Everything about the naming follows from it, and it is why the origin is a hash.
- Wildcards need DNS-01. HTTP-01 cannot issue them, so a hosted deployment needs a DNS provider credential.
AandBmust be different names — locally too. Same host and port is one origin, and the browsed page's own scripts then read the app'slocalStorage, DOM and globals directly: whatever credential the app holds is one call away. Different origins, and the browser refuses all three, leavingpostMessageas the only channel and the app deciding what to answer. The certificate does not enforce this — it only makes an HTTPS origin possible. The origin enforces it.- Hosted, go further: separate registrable domains. Different hostnames make
the two cross-origin, which protects storage, DOM and globals. Only different
registrable domains make them cross-site, which additionally stops
Bfrom settingDomain=-scoped cookies the app will receive. - The responder must be first-party on
A. In a cross-origin helper iframe, Storage Partitioning lands it in a different partition from the app's own workers, where it cannot reach the client it exists to call. Bmust be framed byA. The shell reaches the app throughwindow.parentand refuses to run as a top-level document.- Navigations are not intercepted, deliberately: a worker that served the first page would have to be installed by a page it had not served yet. Every path but the worker serves the shell, and each navigation re-runs it.
- Mixed content decides whether you need a local certificate. All-HTTP on
*.localhostneeds none, because browsers treat it as a secure context; an HTTPS app forces HTTPS browse origins and a*.<suffix>SAN.
Extracted from the real-origin mesh browser in skywire's wasm hypervisor, where it was built to show mesh content without handing that content the visor's identity key. Nothing here is specific to that: the transport was always behind an interface, because the service worker runs on the untrusted origin and must not know what the credential is for.
MIT.
Made with goda:
go run github.com/loov/goda@latest graph github.com/0magnet/realorigin/... | dot -Tsvg -o docs/realorigin-goda-graph.svg
Made with gocloc (excludes vendor/, node_modules/, .git/):
gocloc --not-match-d='(vendor|node_modules|\.git)' .
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Go 8 80 211 741
HTML 2 24 35 221
JavaScript 2 21 71 165
Markdown 1 36 0 129
YAML 1 0 7 98
Makefile 1 10 0 22
-------------------------------------------------------------------------------
TOTAL 15 171 324 1376
-------------------------------------------------------------------------------