@braidlabs/gateway 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +464 -0
- package/package.json +44 -0
- package/src/appd.d.ts +130 -0
- package/src/appd.js +93 -0
- package/src/appd.js.map +1 -0
- package/src/breaker.d.ts +51 -0
- package/src/breaker.js +96 -0
- package/src/breaker.js.map +1 -0
- package/src/discovery.d.ts +91 -0
- package/src/discovery.js +127 -0
- package/src/discovery.js.map +1 -0
- package/src/gateway.d.ts +241 -0
- package/src/gateway.js +735 -0
- package/src/gateway.js.map +1 -0
- package/src/index.d.ts +36 -0
- package/src/index.js +27 -0
- package/src/index.js.map +1 -0
- package/src/node.d.ts +35 -0
- package/src/node.js +252 -0
- package/src/node.js.map +1 -0
- package/src/protocol.d.ts +79 -0
- package/src/protocol.js +89 -0
- package/src/protocol.js.map +1 -0
- package/src/registry.d.ts +184 -0
- package/src/registry.js +187 -0
- package/src/registry.js.map +1 -0
- package/src/rewriter/html-rewrite-stream.d.ts +70 -0
- package/src/rewriter/html-rewrite-stream.js +483 -0
- package/src/rewriter/html-rewrite-stream.js.map +1 -0
- package/src/rewriter/transforms.d.ts +107 -0
- package/src/rewriter/transforms.js +312 -0
- package/src/rewriter/transforms.js.map +1 -0
- package/src/single-flight.d.ts +53 -0
- package/src/single-flight.js +97 -0
- package/src/single-flight.js.map +1 -0
- package/src/telemetry.d.ts +115 -0
- package/src/telemetry.js +177 -0
- package/src/telemetry.js.map +1 -0
package/src/gateway.d.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { Principal, RegistrySource } from './registry.js';
|
|
2
|
+
import { DiscoveryOptions } from './discovery.js';
|
|
3
|
+
import { type BreakerOptions } from './breaker.js';
|
|
4
|
+
import { type TelemetryOptions } from './telemetry.js';
|
|
5
|
+
/**
|
|
6
|
+
* Gateway core: fetch-native, platform-neutral origin-front middleware.
|
|
7
|
+
*
|
|
8
|
+
* Two responsibilities:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Namespace routing**: requests under `/__braid/frag/:id/*` address a fragment by id,
|
|
11
|
+
* exactly — realm stubs, assets, and data. No pattern matching, no request sniffing.
|
|
12
|
+
* 2. **Piercing**: for document requests whose page URL a fragment declares in its
|
|
13
|
+
* `pierce` patterns, the fragment's server-rendered HTML is interleaved into the shell's
|
|
14
|
+
* response stream, so fragments paint with the shell's first response.
|
|
15
|
+
*
|
|
16
|
+
* Everything else passes through to the shell untouched.
|
|
17
|
+
*/
|
|
18
|
+
export interface GatewayOptions {
|
|
19
|
+
/** The fragment registry: inline manifests, a JSON URL, or an async loader. */
|
|
20
|
+
registry: RegistrySource;
|
|
21
|
+
/** 'development' enables verbose error bodies; defaults to 'development'. */
|
|
22
|
+
mode?: 'production' | 'development';
|
|
23
|
+
/** Additional headers set on every request forwarded to a fragment endpoint. */
|
|
24
|
+
additionalHeaders?: Record<string, string>;
|
|
25
|
+
/**
|
|
26
|
+
* Publishes a paginated listing of the fragments this gateway serves, for shells that build
|
|
27
|
+
* their UI from the registry instead of hard-coding slot names.
|
|
28
|
+
*
|
|
29
|
+
* Off unless configured: a registry describes internal topology, so exposing it is a choice.
|
|
30
|
+
* See {@link DiscoveryOptions}.
|
|
31
|
+
*/
|
|
32
|
+
discovery?: DiscoveryOptions;
|
|
33
|
+
/**
|
|
34
|
+
* Resolves who is asking, for manifests that declare `access` rules.
|
|
35
|
+
*
|
|
36
|
+
* Wire it to whatever your app already uses for sessions. It is only called for fragments that
|
|
37
|
+
* actually restrict something, so fully public registries never pay for it — and a fragment
|
|
38
|
+
* with `access` rules but no resolver treats every caller as anonymous.
|
|
39
|
+
*/
|
|
40
|
+
principal?: (request: Request) => Principal | undefined | Promise<Principal | undefined>;
|
|
41
|
+
/**
|
|
42
|
+
* Whether to pass an incoming `x-forwarded-proto` / `x-forwarded-host` through to fragment
|
|
43
|
+
* endpoints instead of overwriting them from the request's own URL. Defaults to false.
|
|
44
|
+
*
|
|
45
|
+
* Leave it off unless a proxy you control is the only way requests reach this gateway.
|
|
46
|
+
* Otherwise a client can name any host it likes, and a fragment that builds absolute URLs
|
|
47
|
+
* from those headers (reset links, cache keys, redirects) will build them for the attacker's
|
|
48
|
+
* host.
|
|
49
|
+
*/
|
|
50
|
+
trustForwardedHeaders?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* What to do with `Cache-Control` on a page URL some fragment declares in `pierce`.
|
|
53
|
+
*
|
|
54
|
+
* - `'private'` (default) — keep it out of shared caches: `public` and `s-maxage` are dropped
|
|
55
|
+
* and `private` is added. The browser may still cache it.
|
|
56
|
+
* - `'preserve'` — leave the shell's own headers untouched.
|
|
57
|
+
*
|
|
58
|
+
* The default exists because such a URL has two representations (composed for a navigation,
|
|
59
|
+
* plain for a soft-navigation fetch) and `Vary: sec-fetch-dest` is not enough to protect them:
|
|
60
|
+
* most CDNs, Cloudflare among them, honor `Vary` only for `Accept-Encoding`. A shared cache
|
|
61
|
+
* that ignores it will store one representation and serve it as the other — the fragment
|
|
62
|
+
* silently vanishes from the page, or appears in a payload a router is trying to parse as JSON.
|
|
63
|
+
* Composed pages are also usually personalized, and caching one freezes the fragment's HTML at
|
|
64
|
+
* the shell's TTL, so a fragment deploy stays invisible until the page expires.
|
|
65
|
+
*
|
|
66
|
+
* Choose `'preserve'` only when the pages are genuinely anonymous **and** you have put
|
|
67
|
+
* `sec-fetch-dest` into the edge's cache key. See `docs/braid-cdn.md`.
|
|
68
|
+
*/
|
|
69
|
+
pierceCacheControl?: 'private' | 'preserve';
|
|
70
|
+
/**
|
|
71
|
+
* Called for every document request that reaches the shell, with which fragments composed into
|
|
72
|
+
* it. This is what makes traffic-informed impact analysis possible: "after this change, 43 of
|
|
73
|
+
* the 412 page URLs we actually serve stop composing billing".
|
|
74
|
+
*
|
|
75
|
+
* **On the request path, so it must be cheap.** It is called synchronously and never awaited;
|
|
76
|
+
* a sink that does real work should buffer and flush elsewhere. It is also called *outside* any
|
|
77
|
+
* try/catch — a throwing observer will fail the request, which is the honest behavior for a
|
|
78
|
+
* broken observer rather than one that silently records nothing.
|
|
79
|
+
*
|
|
80
|
+
* Off unless configured. Recording the paths a site serves is a data-retention decision, not a
|
|
81
|
+
* default: paths carry identifiers, and sometimes personal ones.
|
|
82
|
+
*/
|
|
83
|
+
observe?: (event: RoutingEvent) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Report what the gateway did, and optionally what the browser experienced — per fragment.
|
|
86
|
+
*
|
|
87
|
+
* Distinct from {@link GatewayOptions.observe}, which records *which* fragments composed where
|
|
88
|
+
* so a registry change can be analysed offline. This one records how they *behaved*: fetch
|
|
89
|
+
* outcomes and durations on the server, and web vitals in the browser attributed to the fragment
|
|
90
|
+
* whose subtree they happened in.
|
|
91
|
+
*
|
|
92
|
+
* Off unless configured. Unlike `observe`, the hook is wrapped — a throwing telemetry sink is
|
|
93
|
+
* reported once and then ignored, because a feature meant to be left on in production must not
|
|
94
|
+
* be able to take the site down with it.
|
|
95
|
+
*/
|
|
96
|
+
telemetry?: TelemetryOptions;
|
|
97
|
+
/**
|
|
98
|
+
* Stop paying a broken fragment's timeout on every request.
|
|
99
|
+
*
|
|
100
|
+
* A fragment endpoint that is down already costs its full `timeoutMs`; without a breaker it
|
|
101
|
+
* costs that *again* on the next request, and the one after. This converts a slow repeated
|
|
102
|
+
* failure into a fast one — the fallback the manifest already declares is served immediately
|
|
103
|
+
* instead of after the budget expires.
|
|
104
|
+
*
|
|
105
|
+
* `true` takes the defaults (3 consecutive failures, 10s cooldown). Off unless configured,
|
|
106
|
+
* because shedding load is a behaviour change and a deployment should opt into it knowingly.
|
|
107
|
+
*/
|
|
108
|
+
breaker?: boolean | Partial<BreakerOptions>;
|
|
109
|
+
/**
|
|
110
|
+
* Collapse concurrent identical fragment fetches into one.
|
|
111
|
+
*
|
|
112
|
+
* A CDN in front of this gateway caches the composed document and does nothing for the origin
|
|
113
|
+
* fetches a cache *miss* still costs. An unbound widget pierced into every page is fetched once
|
|
114
|
+
* per render; fifty concurrent renders are fifty identical fetches. This makes them one.
|
|
115
|
+
*
|
|
116
|
+
* Not a cache: nothing outlives the request that started it, so there is no TTL, no
|
|
117
|
+
* invalidation, and no dependence on which instance a request lands on — which is what makes it
|
|
118
|
+
* safe on ECS or any horizontally scaled deployment.
|
|
119
|
+
*
|
|
120
|
+
* Requests only share a fetch when their `cookie`, `authorization`, `user-agent`, and
|
|
121
|
+
* negotiation headers match exactly, so personalized fragments are never shared between users,
|
|
122
|
+
* and a fragment declaring `access` rules is never shared at all.
|
|
123
|
+
*
|
|
124
|
+
* **On by default**, because it changes how many times an identical fetch is made and nothing
|
|
125
|
+
* else: no response is stored, reused later, or shared across identities. A fragment whose
|
|
126
|
+
* endpoint varies on something the gateway cannot see sets `coalesce: false` on its manifest;
|
|
127
|
+
* set this to `false` to turn the whole mechanism off.
|
|
128
|
+
*/
|
|
129
|
+
coalesceFragmentFetches?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Serve Braid's service worker from `/__braid/sw.js`. Off by default.
|
|
132
|
+
*
|
|
133
|
+
* Worth doing here for a reason that is not convenience. **A worker's scope is capped by the path
|
|
134
|
+
* it is served from**, so a script at `/__braid/sw.js` defaults to controlling `/__braid/` — which
|
|
135
|
+
* would intercept fragment assets but not the shell's own, making it useless for the chunk-failure
|
|
136
|
+
* case that matters most. Widening it needs a `Service-Worker-Allowed` header on the script
|
|
137
|
+
* response, and the gateway is the one component already sitting in front of the origin that can
|
|
138
|
+
* send it without additional infrastructure configuration.
|
|
139
|
+
*
|
|
140
|
+
* The default scope is `/`. That sounds broad and costs nothing: scope precedence is
|
|
141
|
+
* longest-match, so a worker registered at `/legacy/` still controls clients under `/legacy/`
|
|
142
|
+
* whatever Braid claims — the root is a fallback, not an exclusion, and the handler ignores
|
|
143
|
+
* everything outside the namespace anyway. The override exists for a gateway mounted under a path
|
|
144
|
+
* on an origin it does not own.
|
|
145
|
+
*/
|
|
146
|
+
serviceWorker?: boolean | ServiceWorkerOptions;
|
|
147
|
+
}
|
|
148
|
+
export interface ServiceWorkerOptions {
|
|
149
|
+
/** The scope to claim, sent as `Service-Worker-Allowed`. Defaults to `/`. */
|
|
150
|
+
scope?: string;
|
|
151
|
+
/**
|
|
152
|
+
* The module the generated worker imports `setupBraidWorker` from.
|
|
153
|
+
*
|
|
154
|
+
* A URL your origin serves — the worker runs in its own realm and cannot resolve a bare specifier.
|
|
155
|
+
* Defaults to `/braid-sw.js`, which is where a build that copies `@braidlabs/sw` normally
|
|
156
|
+
* lands it.
|
|
157
|
+
*/
|
|
158
|
+
module?: string;
|
|
159
|
+
/** Fragment ids whose realm stubs the worker precaches at install. */
|
|
160
|
+
precache?: readonly string[];
|
|
161
|
+
/** Stamped into the worker so it can report disagreement with the page it is serving. */
|
|
162
|
+
buildId?: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* One document request, and what composed into it.
|
|
166
|
+
*
|
|
167
|
+
* Emitted for *every* document request the shell handles, not only pierce-matched ones. A path
|
|
168
|
+
* that composes nothing today is exactly the path that a widened pattern would start composing
|
|
169
|
+
* tomorrow, and analysis needs both sides to report a gain.
|
|
170
|
+
*/
|
|
171
|
+
export interface RoutingEvent {
|
|
172
|
+
/** Page pathname. Search is excluded — it never affects pierce matching. */
|
|
173
|
+
pathname: string;
|
|
174
|
+
/** Fragment ids that composed into this response, in registration order. */
|
|
175
|
+
fragmentIds: string[];
|
|
176
|
+
at: number;
|
|
177
|
+
}
|
|
178
|
+
export interface BraidGateway {
|
|
179
|
+
/**
|
|
180
|
+
* Handles a request if it belongs to Braid: a fragment-namespace request, or a document
|
|
181
|
+
* request that pierces one or more fragments. Returns null for everything else — the caller
|
|
182
|
+
* passes those through to the shell.
|
|
183
|
+
*
|
|
184
|
+
* @param next fetches the shell application's response. Required for piercing; without it,
|
|
185
|
+
* document requests are passed through and fragments boot client-side instead.
|
|
186
|
+
*/
|
|
187
|
+
handle(request: Request, next?: () => Promise<Response>): Promise<Response | null>;
|
|
188
|
+
/**
|
|
189
|
+
* Resolves a websocket upgrade addressed to a fragment.
|
|
190
|
+
*
|
|
191
|
+
* Dev servers push reloads over websockets, and live apps use them for real work. Both are
|
|
192
|
+
* addressed through the fragment namespace, so the gateway has to say where they go — the
|
|
193
|
+
* socket plumbing itself belongs to the platform binding.
|
|
194
|
+
*
|
|
195
|
+
* @returns the endpoint URL to dial, or null when the request is not a fragment upgrade, the
|
|
196
|
+
* fragment is unknown, or the caller may not load it.
|
|
197
|
+
*/
|
|
198
|
+
resolveUpgrade(request: Request): Promise<{
|
|
199
|
+
fragmentId: string;
|
|
200
|
+
target: URL;
|
|
201
|
+
} | null>;
|
|
202
|
+
}
|
|
203
|
+
export declare function createGateway(options: GatewayOptions): BraidGateway;
|
|
204
|
+
/**
|
|
205
|
+
* Wraps a gateway as web middleware: braid requests are handled — including document requests
|
|
206
|
+
* that pierce fragments into the shell — and everything else goes to the shell via `next()`.
|
|
207
|
+
*
|
|
208
|
+
* `next` is memoized, so the shell application runs at most once per request no matter how the
|
|
209
|
+
* gateway and the caller interleave.
|
|
210
|
+
*/
|
|
211
|
+
export declare function toWebMiddleware(gateway: BraidGateway): (request: Request, next: () => Promise<Response>) => Promise<Response>;
|
|
212
|
+
/**
|
|
213
|
+
* Wraps a fetch handler — the shell application as `(Request) => Response` — with the gateway.
|
|
214
|
+
*
|
|
215
|
+
* This is the binding for every web-standard runtime: Cloudflare Workers, Deno, Bun, and
|
|
216
|
+
* h3/Nitro (via `toWebHandler(app)`). It is also the only in-process way to pierce on those
|
|
217
|
+
* runtimes, because piercing needs to *read* the shell's response, which a middleware chain
|
|
218
|
+
* with no return value cannot give it.
|
|
219
|
+
*
|
|
220
|
+
* ```ts
|
|
221
|
+
* // h3 / Nitro
|
|
222
|
+
* import { toWebHandler } from 'h3';
|
|
223
|
+
* export default toFetchHandler(gateway, toWebHandler(app));
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export declare function toFetchHandler(gateway: BraidGateway, appHandler: (request: Request) => Response | Promise<Response>): (request: Request) => Promise<Response>;
|
|
227
|
+
/**
|
|
228
|
+
* Resolves a namespace-stripped request against a fragment's endpoint, **within the endpoint's
|
|
229
|
+
* own path**.
|
|
230
|
+
*
|
|
231
|
+
* `new URL('/admin', 'https://internal/apps/billing/')` yields `https://internal/admin`: an
|
|
232
|
+
* absolute path replaces the endpoint's path entirely. Left alone, that turns the gateway into
|
|
233
|
+
* a proxy for the endpoint host's whole origin rather than the subtree the manifest declared.
|
|
234
|
+
* So the endpoint's path is treated as a prefix.
|
|
235
|
+
*
|
|
236
|
+
* The containment check afterwards is belt-and-braces: dot segments, including percent-encoded
|
|
237
|
+
* ones (`%2e%2e`), are already normalized when the incoming request URL is parsed, which takes
|
|
238
|
+
* such a request out of the fragment namespace entirely before it ever reaches here. The check
|
|
239
|
+
* costs nothing and does not assume every runtime normalizes identically.
|
|
240
|
+
*/
|
|
241
|
+
export declare function resolveEndpointUrl(endpoint: string, strippedUrl: URL, fragmentId: string): URL;
|