@braidlabs/gateway
v0.6.0
Published
Braid gateway — fetch-native origin-front middleware that routes fragment traffic by exact id under the reserved /__braid/frag/ namespace, loads fragment manifests, and passes everything else through to your existing app.
Maintainers
Readme
@braidlabs/gateway
Every
/__braid/…URL this package serves — what asks for it, when, and what comes back — is walked through in Braid, explained.
The Braid gateway: fetch-native, platform-neutral origin-front middleware. Routes fragment
traffic by exact id under the reserved /__braid/frag/:fragmentId/* namespace — no route
pattern sniffing, no header-trust fallback — and passes everything else through to your existing
app.
Braid's founding architecture lives in docs/braid-architecture.md.
Also see: failure modes · CDN configuration · using Braid without the gateway
Usage
import { createGateway } from '@braidlabs/gateway';
const gateway = createGateway({
registry: [
// adapter defaults to "compat" — zero fragment code required
{ id: 'legacy-billing', endpoint: 'https://billing.internal', pierce: ['/billing/*'] },
],
});The registry is data, not code: pass inline manifests, a URL to a JSON array of manifests, or an async loader (file/KV/database). Deploying a fragment never redeploys the gateway.
A fragment that ships a web component rather than a whole app declares the adapter, the module to load, and the element to mount:
{ id: 'rating', endpoint: 'https://widgets.example.com',
adapter: 'custom-element', entry: '/star-rating.js', element: 'star-rating',
events: ['rating:change'] }The registry as an FDC3 App Directory
createGateway({ registry, discovery: { appd: true } });Serves the same registry as an FDC3 2.2 App Directory: /__braid/registry/appd/v2/apps lists
{ applications, message }, and /appd/v2/apps/{appId} returns the bare Application record.
It is a projection, not a second directory — same manifests, same
access.list rules — so a caller can never see through AppD a resolver that discovery would have
hidden. findIntent becomes a registry query, and a user only sees resolvers they may use.
Manifests contribute intents through an fdc3 block and listing metadata through appd:
{ id: 'billing', endpoint: '…', pierce: ['/billing/*'],
fdc3: { listensFor: { ViewInvoice: { contexts: ['fdc3.instrument'] } },
raises: { ViewChart: ['fdc3.instrument'] } },
appd: { publisher: 'Payments', contactEmail: '[email protected]' } }Two mapping decisions worth knowing:
details.urlis a page, when there is one. AppD asks where a web app lives; a fragment lives inside a host page. If the fragment declarespierce, the first concrete pattern names a page it actually appears on and that is the URL — follow it and you see the app. Otherwise the mount is used, andhostManifests.braid.standalonePagesays which you got.- Braid launch detail rides in
hostManifests, which is what AppD reserves for exactly this. A fragment is mounted into a page, not opened as a window, so a Braid-aware agent readshostManifests.braidand mounts a<fragment-slot>; one that does not falls back to the URL.
An app the caller may not list 404s (with AppD's { code, message } error body) exactly as an
unregistered one does — distinguishing them would let an unauthorized caller enumerate the registry
one id at a time.
Records are validated in the test suite against FDC3 2.2's own App Directory schema. Relative icon
src values are resolved against the origin, since AppD requires absolute URLs, and a link that is
not http(s) is dropped rather than passed on. Optional members
Braid has no data for (lang, screenshots, localizedVersions, interop.appChannels) are not
produced. To serve the same directory from the governed registry instead of a gateway, see
createRegistryApi({ appd }) in @braidlabs/registry; to give fragments window.fdc3, see
@braidlabs/fdc3.
A relative entry is re-rooted into the fragment's namespace, so the module and its imports are
fetched through the gateway rather than from the host's root. Such a fragment serves no document,
and the gateway answers its document request with 204 rather than forwarding it.
Bindings
The core is handle(request, next) — fetch-native and platform-neutral. Piercing needs to
read the shell's response, so each binding differs in how it obtains it. All three below are
covered by integration tests against the real frameworks (bindings.spec.ts).
Express / Connect / Vite / plain http — mount it first; next() runs the rest of the app,
and the gateway reads what it writes:
import { toNodeMiddleware } from '@braidlabs/gateway/node';
app.use(toNodeMiddleware(gateway));Angular SSR (@angular/ssr/node) — mount toNodeMiddleware on Express before the Angular handler, and enable trustProxyHeaders on AngularNodeAppEngine so forwarded gateway headers are accepted:
const angularApp = new AngularNodeAppEngine({
allowedHosts: ['localhost', '127.0.0.1'],
trustProxyHeaders: true,
});
app.use(toNodeMiddleware(gateway));NestJS — the Express adapter is a Connect stack, so the same binding applies:
const app = await NestFactory.create(AppModule);
app.use(toNodeMiddleware(gateway));Nitro / h3 / Workers / Deno / Bun — wrap the app's fetch handler:
import { toFetchHandler } from '@braidlabs/gateway';
import { toWebHandler } from 'h3';
export default toFetchHandler(gateway, toWebHandler(app));For a built Nitro app, the gateway also composes in front of the generated Node listener, which needs no Nitro internals and matches the "origin-front middleware" model:
import { listener } from './.output/server/index.mjs';
const braid = toNodeMiddleware(gateway);
createServer((req, res) => braid(req, res, () => listener(req, res)));A fetch-style middleware form is available too:
toWebMiddleware(gateway)(request, () => shellResponse). In every binding the shell application
runs at most once per request.
What it serves
- Realm stubs (
/__braid/realm/:id/*): a minimal document carrying the composition protocol version, the manifest-declared adapter, and a<base>that keeps every relative subresource request inside the fragment's namespace. Version mismatches fail in the client as named errors — no title-check heuristics. - Fragment documents (
/__braid/doc/:id/*): the fragment's HTML prepared for the host page's DOM — exactly what piercing injects, for the client-boot path. Fetched at the page's route, unless the manifest declaresdocumentPath(/index.html) for an SPA on a static origin that 404s every route. A fragment's redirect here is reported as an error, or — for a fragment that opts in — followed as a page navigation; see Login redirects. - Fragment assets/data (
/__braid/frag/:id/*): forwarded to the endpoint with the prefix stripped, so endpoints see the same paths they serve standalone. Redirects pass through unfollowed; each fragment gets a manifest-declared timeout budget. - Pierced documents: see below.
- Unknown ids: 404, never the app shell.
Piercing
Add pierce patterns to a manifest and the gateway server-renders that fragment into the page:
{ id: 'legacy-billing', endpoint: 'https://billing.internal', pierce: ['/billing', '/billing/*'] }On a matching document request the gateway fetches the shell and every matching fragment
concurrently, then interleaves the fragments into the shell's response stream — a fragment
never serializes behind the shell. Each fragment lands in the <fragment-slot> that names it,
as a declarative shadow root, so the browser parses it into exactly the shape the client runtime
would have built; the slot then adopts it instead of fetching. Shells with no matching slot get
one created before </body>.
A shell can instead mark one <fragment-slot outlet>. It takes the page's routed (bound) fragment,
the first in registration order that no earlier named slot claimed, and is named after it. A shell
that composes many routes then needs one slot rather than one per fragment id. Unbound widgets
never fill an outlet. A named slot after the outlet is too late for a fragment the outlet already
took, so the gateway unnames it (data-braid-in-outlet) and warns, rather than let the client boot
a second copy. The outlet is filled on full page loads only.
Fragment HTML is transformed on the way through: the doctype is stripped, <html>/<head>/<body>
become braid-html/braid-head/braid-body (start and end tags), scripts are neutralized to
type="inert", and script preload links become rel="inert-*". Fragment scripts are therefore
never live in the host realm, not even between parsing and activation. The gateway does add
<link rel="prefetch"> hints for the fragment's own scripts (inside its shadow root, never inside
its document) so their downloads start before the client boots it; a prefetch fetches into the
HTTP cache and never executes anything.
If a fragment can't be server-rendered, the page still renders and the slot is left for the
client runtime to fill — a transient SSR failure self-heals rather than becoming a visible
error. Set fallback: 'error-html' on the manifest when a missing section is worse than a
visible failure.
The three namespaces
Each kind of thing the gateway serves has its own path. That is a caching decision: a URL whose
response depends on a request header needs that header in every cache key between here and the
browser, and most CDNs ignore Vary on anything but Accept-Encoding.
| Path | Serves | Cacheable |
| --- | --- | --- |
| /__braid/frag/:id/* | the fragment's own endpoint — assets, data, anything it serves | yes, on URL alone — this is nearly all the traffic |
| /__braid/realm/:id/* | the realm stub the fragment's hidden iframe boots from | yes, on URL alone (1h + stale-while-revalidate) |
| /__braid/doc/:id/* | the fragment's document, prepared for the host page's DOM | per the fragment's own cache headers |
No braid URL varies on a request header. Point a CDN at them and they cache correctly with no configuration at all.
Mounting under a path
A host that shares its domain and does not own the root — /manage/ behind a load balancer —
moves the namespaces under its mount. Set the same basePath on both sides:
createGateway({ registry, basePath: '/manage' }); // serves /manage/__braid/{frag,realm,doc}/…
initBraid({ basePath: '/manage' }); // host page: requests them thereThe gateway then leaves root /__braid/* alone. Page URLs are unaffected: pierce patterns still
match the full pathname (/manage/goals/*). serviceWorker, discovery, and
telemetry.webVitals do not support a basePath yet, and combining them with one throws.
The one thing that does vary
| Header | URL | Response changes to | Why |
| --- | --- | --- | --- |
| sec-fetch-dest: document | a page URL some fragment pierces | the shell with fragments composed into it | A page navigation gets a complete document with fragments already inside. The same URL fetched by a client-side router wants the SPA's own payload, and injecting a declarative shadow root into that would corrupt it. The browser sets this header; it must reach the origin. |
Both representations carry Vary: sec-fetch-dest — and, because most CDNs honor Vary only for
Accept-Encoding, the gateway also rewrites Cache-Control on these URLs to keep them out of
shared caches: public and s-maxage are dropped, private is added, and your own max-age /
no-store / stale-while-revalidate are untouched. Browser caching still works.
Set pierceCacheControl: 'preserve' to opt out — only if the pages are anonymous and you have
put sec-fetch-dest into the edge's cache key. See CDN configuration.
The gateway sends Vary: sec-fetch-dest on those page responses.
Why Vary at all. HTTP caches key on the URL. When one URL can return different bodies
depending on a request header, the cache must include that header in its key or it hands the
wrong body to the next caller. Vary is how the origin says which headers matter — but it is
advisory, and many CDNs honor it only for Accept-Encoding. Since only page URLs vary now,
and page URLs are usually personalized and uncacheable anyway, the practical advice is simply:
don't edge-cache pierced pages. See CDN setup.
Login redirects (opt-in)
A fragment that needs a login answers its document request with a redirect. A browser fetch cannot
act on that: followed, it dies as a CORS error; told not to follow, it hides where the redirect was
going. So by default the client reports a named fragment-fetch error, and the redirect goes nowhere.
A fragment can opt in to having the page follow it instead:
createGateway({
registry: [{ id: 'goals', endpoint: 'https://goals.internal/', redirect: 'navigate' }],
redirectOrigins: ['https://login.example.com', 'https://*.example.com'],
additionalHeaders: (request) => ({ 'x-user': signedIdentityFor(request) }), // see below
});For that fragment's /__braid/doc/ requests the gateway answers a redirect with a 409 carrying the
target in x-braid-redirect-location (never a 3xx, which the browser would hide again), and the
client navigates the whole page there — what the shell would have done had the redirect hit the
top-level request. Nothing else changes: other fragments, and the /__braid/frag/ namespace, keep
passing redirects through.
Where it may go. The target is chosen by the fragment's endpoint and becomes a top-level
navigation, so it is checked, and anything else is refused with a 502:
| Target | Result |
| --- | --- |
| A path on the host's own origin (including /__braid/…, for a login that is itself a fragment) | followed as a host path |
| A path inside the fragment's own endpoint (/apps/goals/login for https://goals.internal/apps/goals/) | mapped to the same path on the host (/login) |
| An origin in redirectOrigins — exact (https://login.example.com) or a subdomain wildcard (https://*.example.com, which does not match example.com itself) | followed as given |
| javascript:, data:, any other scheme, any other origin | refused |
Return URLs. A fragment builds ?next= from the URL it saw, which is its own and means nothing to
the user. A query parameter whose whole value is that URL (its path, on the fragment's origin or
the host's; the query is ignored) is pointed at the page the user was on, taken from the
x-braid-return-url header the client sends (the Referer header is the fallback, and only a page on
the gateway's origin is ever used). The header is read by the gateway and not forwarded to the
fragment, because a page URL can carry a token in its hash. Other parameters keep their exact bytes. Not
rewritten: a return URL buried inside another parameter's value (an OAuth state, say), and a
parameter equal to the fragment's bare root /, which is usually an OAuth redirect_uri.
Concurrent requests are not coalesced for a fragment that opts in: a redirect's cookies carry the login's state and nonce, and sharing one with another caller would share those too.
The loop. The gateway strips the caller's cookies by default, so a fragment that redirects
because it cannot see who the user is will redirect again after login sends them back — forever. The
client follows a given fragment's redirect at most once per 30 seconds (remembered in
sessionStorage; if that is unavailable it does not follow at all) and reports an error instead.
It only works end to end once the fragment can see the user: forwardCredentials, or a per-caller
header via additionalHeaders. The gateway warns when a fragment opts in and neither is set — at startup for an inline registry, on the
first redirect for a URL or loader registry.
Discovery endpoint (optional)
For shells that build their UI from the registry rather than hard-coding slot names — a launcher, an admin console, a directory of available apps — the gateway can publish a paginated listing. It is off by default, because a registry describes internal topology.
const gateway = createGateway({
registry,
discovery: {
path: '/__braid/registry', // default
pageSize: 100, // default; also the ceiling unless maxPageSize says otherwise
principal: (request) => sessionFrom(request), // → { roles, permissions }
},
});GET /__braid/registry?page=2&pageSize=50{
"items": [{ "id": "billing", "title": "Billing", "adapter": "compat", "mount": "/__braid/frag/billing/" }],
"page": 2, "pageSize": 50, "total": 137, "totalPages": 3, "hasMore": true,
"protocolVersion": "1"
}Defaults that protect you. Internal endpoint values are withheld unless you set
includeEndpoints. Listings are no-store and vary on cookie/authorization, so a shared
cache can never serve one caller's listing to another. Page size is capped however large a number
the caller asks for.
Development mode lists everything — every fragment, with endpoints, ignoring access rules —
and logs a warning at startup saying so. That is deliberate for local debugging and must not ship;
the response carries "unfiltered": true so a client can tell.
Access: who may list, who may load
Everything is public by default. A manifest with no access is listed for everyone and
loadable by everyone. Restrict only what needs restricting, and declare it at registration so a
fragment's own team owns its exposure rather than every host re-deciding it.
{
"id": "payroll",
"endpoint": "https://payroll.internal",
"access": {
"list": { "roles": ["finance", "admin"] }, // who sees it in the registry
"fetch": { "roles": ["finance"] } // who may actually load it
}
}The two rules are independent, which is the point:
| list | fetch | Behavior |
| --- | --- | --- |
| open | open | the default — a public fragment |
| open | restricted | shown in a launcher, refused on load; listings mark it "loadable": false so the UI can render that state |
| restricted | open | kept out of listings, still loadable by anyone with a deep link |
| restricted | restricted | invisible and unreachable — to that caller it does not exist |
Within a rule, roles are any-of (holding one of them is enough) and scopes are all-of (an operation requiring two scopes needs both). Declare either or both.
Wire up who is asking once, at the gateway:
createGateway({
registry,
principal: (request) => sessionFrom(request), // → { roles, scopes }
discovery: {},
});principal is only consulted for fragments that actually declare access, so a public registry
never pays for a session lookup on asset requests.
Enforcement. access.fetch applies to every namespace request — the realm stub, the
document, and every asset — and to piercing, where an unauthorized fragment is simply not
composed into the page (the slot is left empty rather than the page failing). A caller who may
list but not load gets 403; a caller who may not even list it gets 404, because confirming
existence would turn the namespace into an inventory of what they cannot reach.
Development mode bypasses access rules entirely, so local work needs no session wiring.
This is authorization for composition, not a substitute for the fragment's own. The fragment's endpoint should still authorize the requests it receives.
Telemetry (optional)
Off unless configured. One hook, two kinds of event — what the gateway did, and what the browser experienced — both attributed to the fragment responsible.
createGateway({
registry,
telemetry: {
on: (event) => otel.emit(event),
webVitals: true, // also collect LCP/CLS/INP/FCP/TTFB in the browser
sampleRate: 0.1, // of browser sessions, not of metrics
},
});| Event | When | Carries |
| --- | --- | --- |
| fragment-fetch | the gateway fetched a fragment endpoint | fragmentId, phase, outcome, status, durationMs |
| web-vital | a browser reported a vital | name, value, rating, fragmentId, pathname |
phase separates a pierce fetch, which delays the page, from a namespace fetch, which
delays only that fragment. outcome reports a 5xx as ok with its status: the distinction an
operator needs is did we reach it, and folding a reachable-but-broken endpoint in with an
unreachable one loses it.
Why per-fragment vitals are the point
Server-side timings per fragment are ordinary — a reverse proxy gives you those. Web vitals per
fragment are not, because in a composed page the page-level number is an average over apps owned
by different teams. "The page has a CLS of 0.31" starts an argument; "ninety percent of the layout
shift happened inside reviews" ends it.
Attribution works by climbing from the reported element to its enclosing <fragment-slot> —
through shadow roots, since a pierced fragment's DOM lives in a declarative shadow root where
closest() stops at the boundary. fragmentId: null is a real answer, not a gap: TTFB belongs to
the document, and an LCP element in the shell's own markup is genuinely the shell's.
webVitals is off by default because it serves a collector from /__braid/vitals.js and injects
it into every composed page, which is a deployment's decision rather than a library's. The
collector is ~2 kB, hand-written rather than pulling in web-vitals, and beacons to
/__braid/vitals on visibilitychange/pagehide.
That endpoint is reachable by anyone who can load the page, so nothing it receives is trusted:
metric names are checked against the five, values must be finite and positive, the pathname is
stripped of its query and length-capped, the batch is capped, and a fragmentId the registry does
not know is nulled rather than relayed. It always answers 204 — the browser is unloading and
nothing is listening, so a status code would only tell a prober what the endpoint accepts.
The hook is wrapped: a throwing sink is logged once and then muted, because a feature meant to be
left on in production must not be able to take the site down. (observe deliberately does not
do this — a broken analysis hook should fail loudly, since silently recording nothing corrupts the
decisions its data feeds.)
What the listing publishes
Each entry carries what a consumer needs to embed the fragment, not just name it: id, title,
adapter, mount, pierce, loadable, and — because they change the required markup — bound
and src. A widget embedded without its src renders an empty shell on every page, so a listing
that omitted it would force every consumer to guess. endpoint appears only when
includeEndpoints is on, or in development.
The registry console uses exactly this to generate copy-pasteable integration code per fragment.
Circuit breaker (optional)
createGateway({ registry, breaker: true }); // 3 failures, 10s cooldown
createGateway({ registry, breaker: { failureThreshold: 5, resetTimeoutMs: 30_000 } });The problem is compounding, not failure. A fragment endpoint that is down already costs its
full timeoutMs — but it costs that on every request, because nothing remembers the last one.
Four fragments at a 3s budget is a page held hostage to the worst of them, repeatedly, with the
shell's response waiting behind it. The fallback machinery already handles a fragment that fails;
what it cannot do is stop paying to rediscover that it fails.
So this is a latency control first, an availability control second. When the circuit is open the manifest's declared fallback is served immediately instead of after the budget expires — the rendered result is identical, it just arrives in ~0ms.
Per fragment id, never global: fragments are independently deployed, and one team's bad release
must not shed another team's traffic. A 5xx counts as a failure (reachable is not the same as
working). A shed request reports outcome: 'shed' rather than 'error', so an operator counting
failures does not count our own load-shedding as the endpoint failing again. Transitions arrive on
the telemetry hook as kind: 'breaker' — on the four state edges only, never per request.
Off unless configured, because shedding load is a behaviour change a deployment should opt into.
Concurrent-fetch coalescing (on by default)
Composing a page costs one origin fetch per fragment. A widget pierced into / and /* is fetched
once per render, so fifty concurrent renders are fifty identical fetches of the same header panel.
The gateway collapses those into one.
createGateway({ registry }); // on
createGateway({ registry, coalesceFragmentFetches: false }); // offNot a cache. It only notices that a fetch it is already making is the one another request wants; nothing outlives the request that started it. No TTL, no invalidation, no shared state, and no dependence on which instance a request lands on — which is what makes it safe by default on ECS or any horizontally scaled deployment.
The key is computed from the request that is actually sent, so it contains exactly what the
endpoint can see: user-agent, the negotiation headers, any header the gateway itself added via
additionalHeaders, and — when forwardCredentials is on — cookie and authorization. A
fragment declaring access rules is never shared at all.
Because credentials are not forwarded by default, two signed-in users produce a byte-identical
fragment request and do share a flight, which is correct: the endpoint cannot tell them apart.
Turn on forwardCredentials, or add a per-caller header, and they stop sharing again. Sharing a
render across identities the endpoint can distinguish is a data leak wearing a performance
feature's clothes, so anything that varies by caller has to be in the key.
For an endpoint that varies on something the gateway cannot see — a tenant or feature-flag header —
opt out on the manifest with coalesce: false.
How much it saves scales with endpoint latency × concurrency. Against a 120ms endpoint, ten parallel renders collapse to one fetch; against a fragment on localhost answering in 10ms the requests barely overlap and it saves nothing. Measure somewhere that resembles production.
Content Security Policy
The gateway injects inline markup into someone else's document: the <style> that makes slots lay
out as blocks, and — when webVitals is on — the collector <script>. Under a strict policy
(script-src 'nonce-…') the browser drops anything unstamped silently, which is the worst
failure shape available: the page renders, the slot layout rule is gone, and no server log mentions
it.
So the gateway reads the nonce from the shell's own Content-Security-Policy response header and
stamps what it injects. Nothing to configure — it works if your shell already sends a nonce.
It never mints one. A nonce the shell's policy does not list is not trusted, and a nonce reused
across responses is not a nonce. A shell with no policy, or one using hashes or 'unsafe-inline',
gets unstamped markup, which is correct for all three.
Security posture
The trusted tier is namespace isolation, not a security boundary: fragments are same-origin
with the host and share its cookies, storage, and DOM reachability. What the gateway does
guarantee is that no code a fragment's markup carries inline can execute outside the fragment's
realm or navigate the host page — with one opt-in exception, login redirects,
whose targets are restricted to the host's origin, the fragment's own endpoint, and
redirectOrigins — scripts are neutralized, inline on* handlers are stripped,
<meta http-equiv="refresh"> is defanged, and frames whose document the markup carries inline
lose it: iframe[srcdoc], and any iframe/embed/object URL other than http(s):, a path, or
about:blank (so javascript:, data:, blob:). srcdoc and javascript: run on parse with
the host's origin, no click needed; data: and blob: are refused alongside them as the same
markup-as-document shape. Each removal is named on the tag in data-braid-blocked. The rule is
deliberately blunt — it also removes a sandboxed srcdoc and a data: PDF or SVG in an
object — so serve such content from a URL instead. A frame that loads a document from the
fragment's own namespace (<iframe src="widget.html">) is left alone: it runs the fragment's
code with the host's origin, which its realm already does. The full list, the trade-offs and the
workarounds: What the gateway does to fragment markup.
Deliberately not neutralized, because they require a user to click rather than executing on
parse: javascript: URLs on links (a, area) and form actions. A trusted fragment can
navigate a page the user clicks through.
Defaults worth knowing: x-forwarded-proto/x-forwarded-host are overwritten from the real
request (opt into passthrough with trustForwardedHeaders only behind a proxy you control); an
endpoint's path is a boundary, so endpoint: 'https://internal/apps/billing/' cannot be used to
reach the rest of that origin; and the caller's Cookie and Authorization are not forwarded
to fragment endpoints.
That last one is the trust boundary between a shell and the fragments it composes. Those headers
authenticate the caller to the shell's origin, and a fragment endpoint is frequently a different
team and often a different company — forwarding them would let every fragment backend act as that
user against the shell. A fragment that needs to know who is asking should be given something
scoped to it: a per-request assertion via additionalHeaders, or a token exchanged for the
fragment's own audience. forwardCredentials: true restores the old behaviour, and is only
appropriate when every endpoint in the registry sits inside the same trust boundary as the shell.
When only some do, pass a function instead, forwardCredentials: (fragment) => …, and the gateway
decides per fragment. A function that throws forwards nothing, and websocket upgrades follow the
same decision. proxy-authorization is stripped with them, and credentialHeaders adds any
identity header an auth proxy in front of the shell injects (x-auth-request-access-token,
cf-access-jwt-assertion, …).
Not yet implemented from the architecture's security section: allow-listed manifest origins and signed manifests. Until those land, treat the registry as trusted configuration.
The rewriter is owned, not forked. rewriteHtmlStream is a small streaming HTML
rewriter with bounded memory and chunk-boundary safety; its conformance vectors
(html-rewrite-stream.spec.ts) are the oracle any second implementation — a native
HTMLRewriter path on workerd, say — must pass before it is allowed to serve traffic. Only the
owned path ships today, because HTMLRewriter does not exist in Node and an untested dual path
is how the upstream project got wasm/native drift.
