@braidlabs/gateway 0.7.0 → 0.8.0

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 CHANGED
@@ -303,6 +303,34 @@ It only works end to end once the fragment can see the user: `forwardCredentials
303
303
  header via `additionalHeaders`. The gateway warns when a fragment opts in and neither is set — at startup for an inline registry, on the
304
304
  first redirect for a URL or loader registry.
305
305
 
306
+ ## Per-user preferences (opt-in)
307
+
308
+ `preferences` serves each signed-in user's own JSON entries at `/__braid/preferences/*`. It is what
309
+ `serverStore()` from `@braidlabs/console/workspace` talks to, so preferences, app settings and
310
+ saved layouts follow the user to any browser.
311
+
312
+ ```ts
313
+ import { filePreferenceBackend } from '@braidlabs/gateway/node';
314
+
315
+ createGateway({
316
+ registry,
317
+ principal: async (request) => {
318
+ const session = await sessions.read(request);
319
+ return session && { subject: session.userId, roles: session.roles };
320
+ },
321
+ preferences: { backend: filePreferenceBackend({ directory: '/var/lib/braid/preferences' }) },
322
+ });
323
+ ```
324
+
325
+ - Entries are keyed by the principal's `subject`, which must come from your session and be
326
+ globally unique (`${iss}|${sub}` across identity providers). No subject, 401.
327
+ - Same-origin only: a cross-site `Sec-Fetch-Site`, or a foreign `Origin` when that is absent, gets
328
+ 403, reads included.
329
+ - Bounded: 64 KiB a value, 1000 entries and 1 MiB a user, enforced by the backend in the same
330
+ write (413 past any of them). Every answer is `private, no-store`.
331
+ - Backends: `memoryPreferenceBackend()` for tests, `filePreferenceBackend()` for one Node process,
332
+ or your own `PreferenceBackend` over a database behind several instances. Full details: [gateway docs](../../docs/packages/gateway.md#per-user-preferences-opt-in).
333
+
306
334
  ## Discovery endpoint (optional)
307
335
 
308
336
  For shells that build their UI from the registry rather than hard-coding slot names — a launcher,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braidlabs/gateway",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "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.",
5
5
  "keywords": [
6
6
  "micro-frontends",
@@ -34,9 +34,12 @@ export function storagePartitionOf(fragment) {
34
34
  if (grant === 'partitioned')
35
35
  return fragment.id;
36
36
  if (grant && typeof grant === 'object') {
37
- const partition = grant.partition;
37
+ const { partition, databases } = grant;
38
38
  if (typeof partition === 'string' && partition !== '')
39
39
  return partition;
40
+ // as on the client: databases of its own imply a partition named for the fragment
41
+ if (databases === true && partition === undefined)
42
+ return fragment.id;
40
43
  }
41
44
  return null;
42
45
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cookie-scope.js","sourceRoot":"","sources":["../../../../libs/braid-gateway/src/cookie-scope.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cookie-scope.js","sourceRoot":"","sources":["../../../../libs/braid-gateway/src/cookie-scope.ts"],"names":[],"mappings":"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"}
package/src/gateway.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Principal, RegistryRefresh, RegistrySource, FragmentManifest } from './registry.js';
2
2
  import { DiscoveryOptions } from './discovery.js';
3
+ import { type PreferencesOptions } from './preferences.js';
3
4
  import { type BreakerOptions } from './breaker.js';
4
5
  import { type BaggageOptions } from './baggage.js';
5
6
  import { type TelemetryOptions } from './telemetry.js';
@@ -29,7 +30,13 @@ export interface GatewayOptions {
29
30
  * for document requests (GET, `Sec-Fetch-Dest: document` or an HTML `Accept`).
30
31
  */
31
32
  documents?: (request: Request, context: DocumentsContext) => Response | null | Promise<Response | null>;
32
- /** 'development' enables verbose error bodies; defaults to 'development'. */
33
+ /**
34
+ * Defaults to 'production'.
35
+ *
36
+ * 'development' is for a gateway on your own machine: it **skips every `access` rule**, lists
37
+ * every fragment with its endpoint in discovery, sends verbose error bodies, and rewrites
38
+ * `Origin` to the endpoint's own. Never set it on a gateway other people can reach.
39
+ */
33
40
  mode?: 'production' | 'development';
34
41
  /**
35
42
  * Additional headers set on every request forwarded to a fragment endpoint.
@@ -92,6 +99,13 @@ export interface GatewayOptions {
92
99
  * See {@link DiscoveryOptions}.
93
100
  */
94
101
  discovery?: DiscoveryOptions;
102
+ /**
103
+ * Serves each signed-in user's preferences at `/__braid/preferences/*`, for the workspace's
104
+ * `serverStore` (`@braidlabs/console/workspace`): preferences, app settings and saved layouts. Entries are keyed by
105
+ * the `subject` your `principal` resolver returns; a request without one is refused with 401.
106
+ * Off unless configured. See {@link PreferencesOptions}.
107
+ */
108
+ preferences?: PreferencesOptions;
95
109
  /**
96
110
  * Resolves who is asking, for manifests that declare `access` rules.
97
111
  *
@@ -227,6 +241,17 @@ export interface GatewayOptions {
227
241
  * on an origin it does not own.
228
242
  */
229
243
  serviceWorker?: boolean | ServiceWorkerOptions;
244
+ /**
245
+ * Tell the browser which links it may fetch or render ahead of a click (Speculation Rules,
246
+ * Chromium), from the fragments whose manifest sets `speculate`.
247
+ *
248
+ * Every HTML document the gateway serves or passes through then carries
249
+ * `Speculation-Rules: "/__braid/speculation-rules.json"`. The header is the same for everyone,
250
+ * so a cached page stays shareable. The rules it points to are built per request from the
251
+ * registry: a fragment the caller may not fetch is left out, and when any fragment has access
252
+ * rules the response is private to the caller. Off by default.
253
+ */
254
+ speculationRules?: boolean;
230
255
  /**
231
256
  * The path this gateway is mounted under, for a host that shares its domain and does not own the
232
257
  * root (`/manage`). The reserved namespaces move under it — `/manage/__braid/frag/…` — and the
@@ -328,6 +353,13 @@ export interface BraidGateway {
328
353
  * document requests are passed through and fragments boot client-side instead.
329
354
  */
330
355
  handle(request: Request, next?: () => Promise<Response>): Promise<Response | null>;
356
+ /**
357
+ * Headers a binding adds to a page it passes to the app when `handle` returned null — the
358
+ * pointer to the speculation rules, when `speculationRules` is on — or null for none. Set on the
359
+ * way out rather than by reading the app's response, so a passed-through page streams exactly as
360
+ * it would without the gateway. Browsers ignore them on anything but HTML.
361
+ */
362
+ pageHeaders(request: Request): Record<string, string> | null;
331
363
  /**
332
364
  * Resolves a websocket upgrade addressed to a fragment.
333
365
  *
package/src/gateway.js CHANGED
@@ -1,6 +1,8 @@
1
- import { BRAID_ADAPTER_META, BRAID_ADAPTER_OPTIONS_META, BRAID_FRAGMENT_ID_HEADER, BRAID_MAX_HOPS, BRAID_PROTOCOL_META, BRAID_PROTOCOL_VERSION, BRAID_REDIRECT_LOCATION_HEADER, BRAID_RETURN_URL_HEADER, braidFragmentUrl, parseBraidPathname, BRAID_SERVICE_WORKER_PATH, BRAID_VITALS_BEACON_PATH, BRAID_VITALS_SCRIPT_PATH, BRAID_VIA_HEADER, } from './protocol.js';
1
+ import { BRAID_ADAPTER_META, BRAID_ADAPTER_OPTIONS_META, BRAID_FRAGMENT_ID_HEADER, BRAID_MAX_HOPS, BRAID_PROTOCOL_META, BRAID_PROTOCOL_VERSION, BRAID_REDIRECT_LOCATION_HEADER, BRAID_RETURN_URL_HEADER, braidFragmentUrl, parseBraidPathname, BRAID_SERVICE_WORKER_PATH, BRAID_SPECULATION_RULES_PATH, BRAID_VITALS_BEACON_PATH, BRAID_VITALS_SCRIPT_PATH, BRAID_VIA_HEADER, } from './protocol.js';
2
2
  import { canFetch, canList, hasAccessRules, isOriginSource, Registry, } from './registry.js';
3
3
  import { createDiscoveryHandler } from './discovery.js';
4
+ import { createPreferencesHandler } from './preferences.js';
5
+ import { speculationRulesFor } from './speculation.js';
4
6
  import { createBreaker } from './breaker.js';
5
7
  import { createSingleFlight, singleFlightKey } from './single-flight.js';
6
8
  import { scopeRequestCookies, scopeResponseCookies, scopeSetCookie, storagePartitionOf, withoutScopedCookies } from './cookie-scope.js';
@@ -33,7 +35,9 @@ export function createGateway(options) {
33
35
  throw new Error(`braid-gateway: registryRefreshMs must be a positive number of milliseconds, got ${options.registryRefreshMs}`);
34
36
  }
35
37
  }
36
- const mode = options.mode ?? 'development';
38
+ // Production unless asked: development skips access rules, so it must never be what a
39
+ // deployment gets for forgetting an option.
40
+ const mode = options.mode ?? 'production';
37
41
  const pierceCacheControl = options.pierceCacheControl ?? 'private';
38
42
  const realmPolicy = options.realmPolicy;
39
43
  if (realmPolicy !== undefined && realmPolicy !== 'report' && realmPolicy !== 'enforce') {
@@ -147,7 +151,9 @@ export function createGateway(options) {
147
151
  const unsupported = [
148
152
  serviceWorker && 'serviceWorker',
149
153
  options.discovery && 'discovery',
154
+ options.preferences && 'preferences',
150
155
  vitalsEnabled && 'telemetry.webVitals',
156
+ options.speculationRules && 'speculationRules',
151
157
  ].filter(Boolean);
152
158
  if (unsupported.length) {
153
159
  throw new Error(`braid-gateway: basePath "${basePath}" is not supported together with ${unsupported.join(', ')} yet — ` +
@@ -259,8 +265,10 @@ export function createGateway(options) {
259
265
  */
260
266
  const resolvePrincipal = async (request) => options.principal ? ((await options.principal(request)) ?? undefined) : undefined;
261
267
  const discovery = createDiscoveryHandler(registry, options.discovery, mode, resolvePrincipal);
268
+ const preferences = createPreferencesHandler(options.preferences, resolvePrincipal);
262
269
  return {
263
270
  refreshRegistry,
271
+ pageHeaders,
264
272
  close() {
265
273
  closed = true;
266
274
  clearTimeout(refreshTimer);
@@ -275,6 +283,9 @@ export function createGateway(options) {
275
283
  if (discovery?.owns(requestUrl.pathname)) {
276
284
  return discovery.handle(request, requestUrl);
277
285
  }
286
+ if (preferences?.owns(requestUrl.pathname)) {
287
+ return preferences.handle(request, requestUrl);
288
+ }
278
289
  if (serviceWorker && requestUrl.pathname === BRAID_SERVICE_WORKER_PATH) {
279
290
  return serviceWorkerResponse(serviceWorker);
280
291
  }
@@ -291,36 +302,14 @@ export function createGateway(options) {
291
302
  if (vitalsEnabled && requestUrl.pathname === BRAID_VITALS_BEACON_PATH) {
292
303
  return handleVitalsBeacon(request);
293
304
  }
305
+ if (options.speculationRules && requestUrl.pathname === BRAID_SPECULATION_RULES_PATH) {
306
+ return speculationRulesResponse(request);
307
+ }
294
308
  const route = braidRoute(requestUrl.pathname);
295
309
  if (!route) {
296
- // HEAD gets the same answer without the body, so a monitor or link checker in front of a
297
- // gateway with no app behind it sees the page a browser would.
298
- if (options.documents && request.method === 'HEAD') {
299
- const asGet = new Request(request.url, { method: 'GET', headers: request.headers });
300
- const document = await options.documents(asGet, { manifests: await registry.current() });
301
- if (document) {
302
- const headers = new Headers(document.headers);
303
- headers.append('vary', 'sec-fetch-dest');
304
- applyPierceCacheControl(headers);
305
- await document.body?.cancel();
306
- return new Response(null, { status: document.status, headers });
307
- }
308
- }
309
- const document = options.documents && isDocumentRequest(request)
310
- ? await options.documents(request, { manifests: await registry.current() })
311
- : null;
312
- if (document) {
313
- // The defined document is the shell: pierced like an app's response when fragments match,
314
- // and served as it is when none do.
315
- const composed = await handleShellRequest(request, requestUrl, async () => document);
316
- if (composed)
317
- return composed;
318
- const served = new Response(document.body, document);
319
- served.headers.append('vary', 'sec-fetch-dest');
320
- applyPierceCacheControl(served.headers);
321
- return served;
322
- }
323
- return next ? handleShellRequest(request, requestUrl, next) : null;
310
+ // A page the gateway leaves to the app comes back null, untouched: the binding adds the
311
+ // page headers on its way through, without the gateway reading the app's response.
312
+ return withPageHeaders(request, await pageResponse(request, requestUrl, next));
324
313
  }
325
314
  /**
326
315
  * Braid requests are routed to the addressed fragment exactly: an unknown fragment id is a
@@ -694,6 +683,13 @@ export function createGateway(options) {
694
683
  }
695
684
  // the endpoint serves an embedded fragment, not a full document
696
685
  fragmentRequest.headers.set('sec-fetch-dest', 'empty');
686
+ // only codings every platform fetch decodes, so the coding of what comes back is known: a
687
+ // browser's `zstd` would come back decoded by some runtimes and not others (see `forwardable`)
688
+ if (fragmentFetch === globalThis.fetch)
689
+ fragmentRequest.headers.set('accept-encoding', [...FETCH_DECODED_CODINGS].join(', '));
690
+ // A browser (the gateway in a service worker) forbids the header and drops it without a word:
691
+ // the browser then picks the codings itself and decodes every one it picked.
692
+ const codingsPinned = fragmentRequest.headers.has('accept-encoding');
697
693
  fragmentRequest.headers.set('x-braid-fragment-mode', 'embedded');
698
694
  /**
699
695
  * In development, present the request as coming from the endpoint's own origin.
@@ -763,7 +759,9 @@ export function createGateway(options) {
763
759
  sent = true;
764
760
  return runFetch(fetchTelemetry, () => fragmentFetch(fragmentRequest, { redirect: 'manual', signal: timeoutSignal }));
765
761
  };
766
- const response = key ? await singleFlight.run(key, send) : await send();
762
+ const sentResponse = key ? await singleFlight.run(key, send) : await send();
763
+ // a function endpoint hands over its own bytes; only the platform fetch decodes them
764
+ const response = forwardable(sentResponse, fragmentFetch === globalThis.fetch, codingsPinned);
767
765
  // A 5xx is a reachable endpoint, but it is not a working one — a fragment returning 500 on
768
766
  // every request would otherwise hold the circuit closed forever and keep paying for it.
769
767
  if (response.status >= 500)
@@ -799,6 +797,83 @@ export function createGateway(options) {
799
797
  return { ok: false, error, timedOut: timeoutSignal.aborted };
800
798
  }
801
799
  }
800
+ /** A request outside the Braid namespace: a page, composed or passed to the shell. */
801
+ async function pageResponse(request, requestUrl, next) {
802
+ // HEAD gets the same answer without the body, so a monitor or link checker in front of a
803
+ // gateway with no app behind it sees the page a browser would.
804
+ if (options.documents && request.method === 'HEAD') {
805
+ const asGet = new Request(request.url, { method: 'GET', headers: request.headers });
806
+ const document = await options.documents(asGet, { manifests: await registry.current() });
807
+ if (document) {
808
+ const headers = new Headers(document.headers);
809
+ headers.append('vary', 'sec-fetch-dest');
810
+ applyPierceCacheControl(headers);
811
+ await document.body?.cancel();
812
+ return new Response(null, { status: document.status, headers });
813
+ }
814
+ }
815
+ const document = options.documents && isDocumentRequest(request)
816
+ ? await options.documents(request, { manifests: await registry.current() })
817
+ : null;
818
+ if (document) {
819
+ // The defined document is the shell: pierced like an app's response when fragments match,
820
+ // and served as it is when none do.
821
+ const composed = await handleShellRequest(request, requestUrl, async () => document);
822
+ if (composed)
823
+ return composed;
824
+ const served = new Response(document.body, document);
825
+ served.headers.append('vary', 'sec-fetch-dest');
826
+ applyPierceCacheControl(served.headers);
827
+ return served;
828
+ }
829
+ return next ? handleShellRequest(request, requestUrl, next) : null;
830
+ }
831
+ /**
832
+ * The headers a page carries because of this gateway, for a browser navigation: today, the
833
+ * pointer to the speculation rules.
834
+ *
835
+ * Every page, not only composed ones: the page that *links* to a fragment's route is the one the
836
+ * browser speculates from, and it is usually not that route. The value is the same for every
837
+ * caller, so it never makes a page private, and a browser ignores it on anything but HTML.
838
+ */
839
+ function pageHeaders(request) {
840
+ if (!options.speculationRules || !isNavigation(request))
841
+ return null;
842
+ return { 'speculation-rules': `"${BRAID_SPECULATION_RULES_PATH}"` };
843
+ }
844
+ /** A page the gateway produced, with {@link pageHeaders} on it. */
845
+ function withPageHeaders(request, page) {
846
+ const headers = page && !isOpaque(page) ? pageHeaders(request) : null;
847
+ return headers ? withHeaders(page, headers) : page;
848
+ }
849
+ /**
850
+ * The rules themselves: the pierce patterns of every fragment that asks for speculation, less
851
+ * those the caller may not list or fetch — as discovery and the namespace keep them out of sight.
852
+ *
853
+ * Private whenever the gateway can tell callers apart (a `principal` resolver) or any fragment
854
+ * has access rules: decided from configuration, so a shared cache never holds a list that a
855
+ * registry refresh has since made personal. Never stored in development, where nothing is filtered.
856
+ */
857
+ async function speculationRulesResponse(request) {
858
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
859
+ return new Response(null, { status: 405, headers: { allow: 'GET, HEAD' } });
860
+ }
861
+ const manifests = await registry.listFragments();
862
+ const personal = !isDevelopment && (options.principal !== undefined || manifests.some(hasAccessRules));
863
+ const principal = personal ? await resolvePrincipal(request) : undefined;
864
+ const visible = manifests.filter((manifest) => isDevelopment || !hasAccessRules(manifest) || (canList(manifest, principal) && canFetch(manifest, principal)));
865
+ return new Response(request.method === 'HEAD' ? null : JSON.stringify(speculationRulesFor(visible)), {
866
+ headers: {
867
+ 'content-type': 'application/speculationrules+json',
868
+ ...(isDevelopment
869
+ ? { 'cache-control': 'no-store' }
870
+ : personal
871
+ ? { 'cache-control': 'private, no-store', vary: 'cookie, authorization' }
872
+ : // short: the registry it comes from refreshes on its own schedule
873
+ { 'cache-control': 'public, max-age=60' }),
874
+ },
875
+ });
876
+ }
802
877
  /**
803
878
  * Handles a request bound for the shell application.
804
879
  *
@@ -842,6 +917,8 @@ export function createGateway(options) {
842
917
  }
843
918
  }
844
919
  const shell = await next();
920
+ if (isOpaque(shell))
921
+ return shell;
845
922
  const isNullBody = shell.status === 204 ||
846
923
  shell.status === 205 ||
847
924
  shell.status === 304 ||
@@ -887,6 +964,10 @@ export function createGateway(options) {
887
964
  cancelFragmentBodies();
888
965
  throw error;
889
966
  }
967
+ if (isOpaque(shellResponse)) {
968
+ cancelFragmentBodies();
969
+ return shellResponse;
970
+ }
890
971
  const isShellNullBody = shellResponse.status === 204 ||
891
972
  shellResponse.status === 205 ||
892
973
  shellResponse.status === 304 ||
@@ -1087,7 +1168,7 @@ export function createGateway(options) {
1087
1168
  export function toWebMiddleware(gateway) {
1088
1169
  return async (request, next) => {
1089
1170
  const shellOnce = once(next);
1090
- return (await gateway.handle(request, shellOnce)) ?? shellOnce();
1171
+ return (await gateway.handle(request, shellOnce)) ?? passThrough(gateway, request, await shellOnce());
1091
1172
  };
1092
1173
  }
1093
1174
  /**
@@ -1107,9 +1188,14 @@ export function toWebMiddleware(gateway) {
1107
1188
  export function toFetchHandler(gateway, appHandler) {
1108
1189
  return async (request) => {
1109
1190
  const shellOnce = once(() => Promise.resolve(appHandler(request)));
1110
- return (await gateway.handle(request, shellOnce)) ?? shellOnce();
1191
+ return (await gateway.handle(request, shellOnce)) ?? passThrough(gateway, request, await shellOnce());
1111
1192
  };
1112
1193
  }
1194
+ /** The app's own response to a request the gateway passed to it, with the gateway's page headers. */
1195
+ function passThrough(gateway, request, response) {
1196
+ const headers = isOpaque(response) ? null : gateway.pageHeaders(request);
1197
+ return headers ? withHeaders(response, headers) : response;
1198
+ }
1113
1199
  /** Memoizes an async thunk so the wrapped work happens at most once. */
1114
1200
  function once(fn) {
1115
1201
  let pending;
@@ -1238,11 +1324,98 @@ function escapeHtml(value) {
1238
1324
  function appendHop(request) {
1239
1325
  return [request.headers.get(BRAID_VIA_HEADER), 'braid'].filter(Boolean).join(', ');
1240
1326
  }
1327
+ /**
1328
+ * A response with nothing to compose or annotate: a browser's opaque redirect (status 0), which is
1329
+ * what a navigation's own `fetch` returns when the shell redirects, or a network error. Neither can
1330
+ * be copied into a new `Response`, and both are for the browser to act on as they are.
1331
+ */
1332
+ function isOpaque(response) {
1333
+ return response.status === 0;
1334
+ }
1335
+ /** A top-level browser navigation: the request a page's own headers are for. */
1336
+ function isNavigation(request) {
1337
+ return request.method === 'GET' && request.headers.get('sec-fetch-dest') === 'document';
1338
+ }
1339
+ /**
1340
+ * The response with these headers set. A response's headers are often immutable (one `fetch`
1341
+ * returned), so it is copied then — the body streams through the copy untouched.
1342
+ */
1343
+ function withHeaders(response, headers) {
1344
+ try {
1345
+ for (const [name, value] of Object.entries(headers))
1346
+ response.headers.set(name, value);
1347
+ return response;
1348
+ }
1349
+ catch {
1350
+ const copy = new Response(response.body, response);
1351
+ for (const [name, value] of Object.entries(headers))
1352
+ copy.headers.set(name, value);
1353
+ return copy;
1354
+ }
1355
+ }
1241
1356
  /** How many Braid gateways a request has already crossed, from its `x-braid-via` entries. */
1242
1357
  function hopCount(request) {
1243
1358
  const via = request.headers.get(BRAID_VIA_HEADER);
1244
1359
  return via ? via.split(',').filter((entry) => entry.trim()).length : 0;
1245
1360
  }
1361
+ /** The content codings every platform `fetch` decodes before handing over the body. */
1362
+ const FETCH_DECODED_CODINGS = new Set(['gzip', 'x-gzip', 'deflate', 'br']);
1363
+ /**
1364
+ * Headers that describe one connection, not the response (RFC 9110 §7.6.1). The gateway's
1365
+ * connection to the client is its own; forwarding the endpoint's would let the endpoint frame it —
1366
+ * a `transfer-encoding: identity` with the length removed leaves the body unframed on a keep-alive
1367
+ * connection, and a proxy in front may read the rest as its next response.
1368
+ */
1369
+ const HOP_BY_HOP_HEADERS = ['connection', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'te', 'trailer', 'upgrade', 'proxy-authenticate', 'proxy-authorization'];
1370
+ /**
1371
+ * An endpoint's response with headers that describe what the gateway will actually send.
1372
+ *
1373
+ * Hop-by-hop headers always go, with any the endpoint's `connection` names. And when the platform
1374
+ * `fetch` produced the response (`decodedByFetch`), the body was decoded but `content-encoding` and
1375
+ * `content-length` still describe the bytes on the wire. Forwarded as they are, the client gets a
1376
+ * body it cannot decode and — worse, on Node, which writes whatever bytes it is given — more bytes
1377
+ * than the declared length on a keep-alive connection. The surplus is read as the start of the
1378
+ * *next* response on that socket, so a different request fails. So for a coding `fetch` decodes,
1379
+ * both go and the server frames the body itself; any other coding is refused (below). An identity
1380
+ * response keeps its length. (The Node binding also refuses to write past a declared length,
1381
+ * whatever produced it.)
1382
+ *
1383
+ * Only what the platform `fetch` returned: a function endpoint hands over its own bytes and is
1384
+ * trusted to describe them — one that wraps `fetch` must drop these headers itself. On Workers,
1385
+ * which would re-encode a passed-through body, this costs the compression of that one hop.
1386
+ *
1387
+ * Where the codings could not be pinned (`codingsPinned` false: a browser, which forbids setting
1388
+ * `accept-encoding`), the browser asked for them and decodes all it asked for, so none is refused.
1389
+ */
1390
+ function forwardable(response, decodedByFetch, codingsPinned = true) {
1391
+ const encoding = response.headers.get('content-encoding');
1392
+ const connection = response.headers.get('connection');
1393
+ const reframe = decodedByFetch && encoding !== null;
1394
+ // A coding the gateway did not ask for (it asks for exactly FETCH_DECODED_CODINGS): whether this
1395
+ // runtime's fetch decoded it varies — Node's decodes zstd — so neither the length nor the coding
1396
+ // can be said to describe the body. Refused, the same on every runtime, rather than guessed at.
1397
+ const codings = encoding?.split(',').map((coding) => coding.trim().toLowerCase()).filter(Boolean) ?? [];
1398
+ if (reframe && codingsPinned && codings.some((coding) => coding !== 'identity' && !FETCH_DECODED_CODINGS.has(coding))) {
1399
+ void response.body?.cancel();
1400
+ return new Response(`braid-gateway: the endpoint answered in content-encoding "${encoding}", which was not requested`, {
1401
+ status: 502,
1402
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
1403
+ });
1404
+ }
1405
+ if (!reframe && !HOP_BY_HOP_HEADERS.some((name) => response.headers.has(name)))
1406
+ return response;
1407
+ const headers = new Headers(response.headers);
1408
+ for (const name of connection?.split(',') ?? [])
1409
+ if (name.trim())
1410
+ headers.delete(name.trim());
1411
+ for (const name of HOP_BY_HOP_HEADERS)
1412
+ headers.delete(name);
1413
+ if (reframe) {
1414
+ headers.delete('content-length');
1415
+ headers.delete('content-encoding');
1416
+ }
1417
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
1418
+ }
1246
1419
  /** Realm stub ETags by stub content, so a hot path does not hash the same few hundred bytes twice. */
1247
1420
  const stubEtags = new Map();
1248
1421
  /** A strong ETag over a realm stub's content. */