@astrale-os/kernel-server 0.5.0-beta.73 → 0.5.0-beta.75

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.
Files changed (84) hide show
  1. package/dist/index.d.ts +1 -6
  2. package/dist/index.js +0 -5
  3. package/dist/invocation/index.d.ts +1 -1
  4. package/dist/invocation/index.js +1 -1
  5. package/dist/invocation/release-route.d.ts +3 -7
  6. package/dist/invocation/release-route.js +4 -27
  7. package/dist/invocation/types.d.ts +2 -14
  8. package/dist/issuer/routes.js +28 -47
  9. package/dist/release/delivery/handler.d.ts +1 -5
  10. package/dist/release/delivery/handler.js +8 -29
  11. package/dist/release/delivery/index.d.ts +0 -1
  12. package/dist/release/delivery/index.js +1 -1
  13. package/dist/release/delivery/types.d.ts +3 -13
  14. package/dist/release/served.d.ts +6 -15
  15. package/dist/release/served.js +3 -7
  16. package/dist/release/snapshot.d.ts +2 -6
  17. package/dist/release/snapshot.js +2 -7
  18. package/dist/release/types.d.ts +1 -11
  19. package/dist/routes/address.d.ts +2 -0
  20. package/dist/routes/address.js +6 -0
  21. package/dist/routes/binary.d.ts +9 -0
  22. package/dist/routes/binary.js +110 -0
  23. package/dist/routes/bundle.d.ts +5 -0
  24. package/dist/routes/bundle.js +50 -0
  25. package/dist/routes/csrf.d.ts +16 -0
  26. package/dist/routes/csrf.js +35 -0
  27. package/dist/routes/document.d.ts +6 -0
  28. package/dist/routes/document.js +57 -0
  29. package/dist/routes/index.d.ts +3 -1
  30. package/dist/routes/index.js +2 -1
  31. package/dist/routes/model.d.ts +15 -0
  32. package/dist/routes/native.d.ts +17 -0
  33. package/dist/routes/native.js +126 -0
  34. package/dist/routes/preflight.d.ts +4 -0
  35. package/dist/routes/preflight.js +55 -0
  36. package/dist/routes/response.d.ts +14 -0
  37. package/dist/routes/response.js +33 -0
  38. package/dist/routes/routes.d.ts +5 -24
  39. package/dist/routes/routes.js +19 -625
  40. package/dist/routes/snapshot.d.ts +13 -0
  41. package/dist/routes/snapshot.js +39 -0
  42. package/dist/routes/stream.d.ts +6 -0
  43. package/dist/routes/stream.js +122 -0
  44. package/dist/server.js +12 -30
  45. package/dist/transport/http/binary.d.ts +6 -0
  46. package/dist/transport/http/binary.js +35 -0
  47. package/dist/transport/http/byte-stream.d.ts +4 -0
  48. package/dist/transport/http/byte-stream.js +123 -0
  49. package/dist/transport/http/control.d.ts +12 -0
  50. package/dist/transport/http/control.js +24 -0
  51. package/dist/transport/http/handler.d.ts +1 -1
  52. package/dist/transport/http/handler.js +4 -2
  53. package/dist/transport/http/index.d.ts +3 -1
  54. package/dist/transport/http/index.js +2 -1
  55. package/dist/transport/http/model.d.ts +20 -0
  56. package/dist/transport/http/model.js +1 -0
  57. package/dist/transport/http/render.d.ts +3 -36
  58. package/dist/transport/http/render.js +16 -412
  59. package/dist/transport/http/response.d.ts +14 -0
  60. package/dist/transport/http/response.js +77 -0
  61. package/dist/transport/http/stream.d.ts +5 -0
  62. package/dist/transport/http/stream.js +134 -0
  63. package/dist/transport/index.d.ts +2 -1
  64. package/dist/transport/index.js +2 -1
  65. package/dist/transport/result.d.ts +13 -0
  66. package/dist/transport/result.js +35 -0
  67. package/dist/transport/websocket/session.d.ts +1 -1
  68. package/dist/transport/websocket/session.js +3 -3
  69. package/dist/types.d.ts +13 -16
  70. package/package.json +5 -13
  71. package/dist/invocation/legacy/index.d.ts +0 -20
  72. package/dist/invocation/legacy/index.js +0 -20
  73. package/dist/invocation/legacy/publication-route.d.ts +0 -16
  74. package/dist/invocation/legacy/publication-route.js +0 -39
  75. package/dist/release/legacy/delivery.d.ts +0 -18
  76. package/dist/release/legacy/delivery.js +0 -2
  77. package/dist/release/legacy/index.d.ts +0 -39
  78. package/dist/release/legacy/index.js +0 -38
  79. package/dist/release/legacy/snapshot.d.ts +0 -26
  80. package/dist/release/legacy/snapshot.js +0 -55
  81. package/dist/release/legacy/table.d.ts +0 -5
  82. package/dist/release/legacy/table.js +0 -16
  83. package/dist/release/legacy/types.d.ts +0 -31
  84. /package/dist/{release/legacy/types.js → routes/model.js} +0 -0
@@ -1,640 +1,34 @@
1
- import * as codecs from '@astrale-os/kernel-protocol/codecs';
2
- import * as errors from '@astrale-os/kernel-protocol/errors';
3
- import * as protocol from '@astrale-os/kernel-protocol/invocation';
4
- import { bundleUrl, MEDIA_TYPE as RELEASE_MEDIA_TYPE, PATH as RELEASE_PATH, url as releaseUrl, } from '@astrale-os/kernel-protocol/release';
5
- import { mediaType as publicationMediaType, PATH as PUBLICATION_PATH, reference as publicationReference, url as publicationUrl, } from '@astrale-os/kernel-protocol/release/legacy';
6
- import * as routes from '@astrale-os/kernel-protocol/routes';
7
- import { admissionRefusal, classifyError, classifyRequestError, RequestBodyInvalidError, RequestBodyTooLargeError, retryAfterHeaders, } from '../errors/index.js';
8
- import { invocationContext } from '../invocation/index.js';
9
- import { handleDelivery } from '../release/delivery/index.js';
10
- import { pinned, readServed, servedDocument } from '../release/served.js';
11
- import { readBoundedBody } from '../transport/body.js';
12
- import {} from '../transport/router.js';
13
- import { interruptedStreamError } from '../transport/stream-error.js';
1
+ import { handleDelivery } from '../release/delivery/handler.js';
2
+ import { servedDocument } from '../release/served.js';
3
+ import { sameRoute } from './address.js';
4
+ import { bundleResponse } from './bundle.js';
5
+ import { mountDocuments } from './document.js';
6
+ import { nativeResponse } from './native.js';
7
+ import { releasePreflight } from './preflight.js';
8
+ import { servingSnapshot } from './snapshot.js';
9
+ /** Mount the advertised HTTP resources and native routes from one pinned serving snapshot. */
14
10
  export function mountRoutes(app, source, deliveryProvider, handler, active, nextInvocation, limits, cors, hostFor, reservations, externalIssuer, publicOrigin) {
15
11
  const externalOrigin = publicOrigin ?? (externalIssuer === undefined ? undefined : new URL(externalIssuer).origin);
16
12
  const served = (request) => servingSnapshot(source, limits, request, reservations, externalIssuer, externalOrigin);
17
- if (source.kind === 'release') {
18
- mountDocument(app, documentPath(RELEASE_PATH, releaseUrl, externalIssuer), 'Release', {
19
- cors,
20
- active,
21
- served,
22
- read: (snapshot) => snapshot.kind === 'release'
23
- ? {
24
- bytes: snapshot.releaseBytes,
25
- mediaType: RELEASE_MEDIA_TYPE,
26
- etag: snapshot.release.digest,
27
- }
28
- : undefined,
29
- });
30
- }
31
- // At a release-backed destination `domain.json` serves the frozen v3 projection.
32
- mountDocument(app, documentPath(PUBLICATION_PATH, publicationUrl, externalIssuer), 'Publication', {
33
- cors,
34
- active,
35
- served,
36
- read: (snapshot) => ({
37
- bytes: snapshot.publicationBytes,
38
- mediaType: publicationMediaType(snapshot.publication),
39
- etag: snapshot.publication.etag,
40
- }),
41
- });
13
+ mountDocuments(app, cors, active, served, externalIssuer);
14
+ const native = { handler, active, nextInvocation, limits, cors, hostFor, externalOrigin };
42
15
  app.all('*', async (context) => {
43
- const snapshot = served(context.req.raw);
16
+ const request = context.req.raw;
17
+ const snapshot = served(request);
44
18
  if (snapshot === undefined)
45
19
  return context.notFound();
46
- const request = context.req.raw;
47
20
  if (request.method === 'OPTIONS' && cors.enabled) {
48
- return publicationPreflight(cors, request, snapshot, externalOrigin);
21
+ return releasePreflight(cors, request, snapshot, externalOrigin);
49
22
  }
50
23
  const responseCors = cors.response(request);
51
24
  const corsFor = (exposed) => cors.response(request, exposed);
52
- const document = servedDocument(snapshot);
53
- const descriptor = document.schema.bundle;
54
- const addressed = bundleUrl(document.identity.issuer, descriptor.ref);
55
- const immutable = descriptor.href === addressed;
56
- if (sameRoute(request.url, descriptor.href, externalOrigin)) {
57
- // A release names its own document; a v3-input destination keeps its recorded bytes.
58
- const releaseBacked = snapshot.kind === 'release';
59
- if (!active.isAccepting) {
60
- return drained(releaseBacked
61
- ? 'Release artifact is unavailable while draining.'
62
- : 'Publication artifact is unavailable while draining.', corsFor);
63
- }
64
- if (request.method !== 'GET')
65
- return response({ error: 'Bundle retrieval requires GET.' }, 405, responseCors);
66
- const body = snapshot.bundle;
67
- if (body.byteLength !== descriptor.ref.size) {
68
- return response({
69
- error: releaseBacked
70
- ? 'Bundle differs from its release.'
71
- : 'Bundle differs from its Publication.',
72
- }, 500, responseCors);
73
- }
74
- const etag = quoteEtag(descriptor.ref.digest);
75
- const unchanged = request.headers.get('if-none-match') === etag;
76
- return new Response(unchanged ? null : toBody(body), {
77
- status: unchanged ? 304 : 200,
78
- headers: {
79
- ...responseCors,
80
- 'content-type': descriptor.ref.mediaType,
81
- etag,
82
- 'cache-control': immutable
83
- ? 'public, max-age=31536000, immutable, no-transform'
84
- : 'public, max-age=0, must-revalidate, no-transform',
85
- },
86
- });
87
- }
88
- if (request.method === 'GET' && immutable) {
89
- const pathname = new URL(addressed).pathname;
90
- const directory = pathname.slice(0, pathname.lastIndexOf('/') + 1);
91
- const requested = new URL(request.url);
92
- if (requested.pathname.startsWith(directory) &&
93
- /^[0-9a-f]{64}\.json$/u.test(requested.pathname.slice(directory.length))) {
94
- return new Response(null, {
95
- status: 404,
96
- headers: { ...responseCors, 'cache-control': 'no-store' },
97
- });
98
- }
99
- }
100
- const deliveryUrl = document.delivery?.href;
25
+ const bundle = bundleResponse(request, snapshot, active, responseCors, corsFor, externalOrigin);
26
+ if (bundle !== undefined)
27
+ return bundle;
28
+ const deliveryUrl = servedDocument(snapshot).delivery?.href;
101
29
  if (deliveryUrl !== undefined && sameRoute(request.url, deliveryUrl, externalOrigin)) {
102
30
  return handleDelivery(request, snapshot, deliveryProvider, active, limits, corsFor);
103
31
  }
104
- let routing;
105
- try {
106
- if (snapshot.kind === 'release') {
107
- const decoded = routes.decodeRouting(request, snapshot.references);
108
- if (decoded?.kind === 'stale')
109
- return stale(request, decoded.current, cors);
110
- routing = decoded;
111
- }
112
- else {
113
- const ifMatch = request.headers.get(routes.HTTP_ROUTE_DIGEST);
114
- if (ifMatch !== null &&
115
- /^"sha256:[0-9a-f]{64}"$/u.test(ifMatch) &&
116
- ifMatch !== quoteEtag(snapshot.publication.etag)) {
117
- return stale(request, snapshot.publication.etag, cors);
118
- }
119
- routing = routes.legacy.decodeRouting(request, publicationReference(snapshot.publication));
120
- }
121
- }
122
- catch (cause) {
123
- if (cause instanceof routes.RouteError) {
124
- return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route routing is invalid.' }, 400, corsFor);
125
- }
126
- throw cause;
127
- }
128
- const route = routes.match(snapshot.table, request);
129
- if (route === undefined)
130
- return context.notFound();
131
- const assigned = nextInvocation();
132
- const operation = active.begin(limits.invocationTimeoutMs, request.signal);
133
- if (operation === undefined)
134
- return nativeError(admissionRefusal(active.isAccepting), 503, corsFor);
135
- let handedOff = false;
136
- try {
137
- const body = await readBoundedBody(request, limits.maximumRequestBytes, operation.signal);
138
- const native = { url: request.url, method: request.method, headers: request.headers, body };
139
- const invocation = snapshot.kind === 'release'
140
- ? routes.decode(route, native, snapshot.domain, snapshot.references)
141
- : routes.legacy.decode(route, native, snapshot.domain, snapshot.table.publication);
142
- if (invocation.credentialIn === 'cookie' &&
143
- isStateChanging(request) &&
144
- isClearlyCrossSite(request, externalOrigin ?? new URL(request.url).origin)) {
145
- const refusal = {
146
- code: errors.CODES.ACCESS_DENIED,
147
- message: 'Cross-site cookie-authenticated request to a native route is refused.',
148
- };
149
- return nativeError(refusal, errors.httpStatusOf(refusal), corsFor);
150
- }
151
- const idempotencyHeader = request.headers.get(protocol.HTTP_IDEMPOTENCY_KEY);
152
- const idempotencyKey = idempotencyHeader === null
153
- ? undefined
154
- : protocol.acceptIdempotencyKey(idempotencyHeader, protocol.HTTP_IDEMPOTENCY_KEY);
155
- const result = await handler.dispatch(Object.freeze({
156
- call: invocation.call,
157
- ...(invocation.credential === undefined ? {} : { credential: invocation.credential }),
158
- ...(routing === undefined ? {} : { via: routing }),
159
- ...pinned(snapshot),
160
- context: invocationContext(assigned, operation, active, hostFor(request), body, idempotencyKey),
161
- }));
162
- if (result.kind === 'value') {
163
- return new Response(encodedBody(codecs.json.encode(result.value)), {
164
- headers: {
165
- ...responseCors,
166
- 'content-type': 'application/json',
167
- },
168
- });
169
- }
170
- if (result.kind === 'binary') {
171
- if (isStreamingBinary(result.value)) {
172
- // Hand the operation off only once the streaming response is built. A refused render
173
- // (reserved header or a body-forbidding status) throws before this point, so the
174
- // `finally` below still settles the operation instead of leaking its slot.
175
- const streaming = streamingBinaryResponse(result.value, operation, cors, request);
176
- handedOff = true;
177
- return streaming;
178
- }
179
- const applicationHeaders = applicationBinaryHeaders(result.value.headers);
180
- return new Response(toBody(result.value.body), {
181
- status: result.value.status ?? 200,
182
- headers: {
183
- ...cors.response(request, [
184
- protocol.http.BINARY_HEADERS_FIELD,
185
- ...Object.keys(applicationHeaders),
186
- ]),
187
- ...applicationHeaders,
188
- 'content-type': result.value.mediaType,
189
- [protocol.http.BINARY_HEADERS_FIELD]: protocol.http.encodeBinaryHeaderNames(applicationHeaders),
190
- },
191
- });
192
- }
193
- if (result.kind === 'stream') {
194
- handedOff = true;
195
- return await streamResponse(result.stream, operation, corsFor);
196
- }
197
- if (result.kind === 'miss') {
198
- return new Response(null, {
199
- status: routes.HTTP_ROUTE_MISS_STATUS,
200
- headers: {
201
- ...cors.response(request, [routes.HTTP_ROUTE_MISS]),
202
- ...routes.missHeaders(),
203
- },
204
- });
205
- }
206
- return nativeError({
207
- code: errors.CODES.UNSUPPORTED,
208
- message: 'Native routes do not forward Runtime redirects.',
209
- }, 502, corsFor);
210
- }
211
- catch (cause) {
212
- if (cause instanceof RequestBodyTooLargeError) {
213
- return nativeError({ code: errors.CODES.LIMIT_EXCEEDED, message: 'Request body is too large.' }, 413, corsFor);
214
- }
215
- if (cause instanceof RequestBodyInvalidError) {
216
- return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Request body is invalid.' }, 400, corsFor);
217
- }
218
- if (cause instanceof routes.RouteError) {
219
- return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route request is invalid.' }, 400, corsFor);
220
- }
221
- const payload = classifyRequestError(cause, operation);
222
- return nativeError(payload, errors.httpStatusOf(payload), corsFor);
223
- }
224
- finally {
225
- if (!handedOff)
226
- operation.settle();
227
- }
32
+ return (await nativeResponse(request, snapshot, native, responseCors)) ?? context.notFound();
228
33
  });
229
34
  }
230
- function streamingBinaryResponse(binary, operation, cors, request) {
231
- // Admit the reserved/invalid headers before acquiring the producer, so a refused render never
232
- // captures the stream it would then have to abandon.
233
- const applicationHeaders = applicationBinaryHeaders(binary.headers);
234
- const iterator = binary.body[Symbol.asyncIterator]();
235
- let terminal = false;
236
- let controller;
237
- let stopping;
238
- const stop = (reason = 'Binary response was interrupted.') => {
239
- if (terminal)
240
- return stopping ?? Promise.resolve();
241
- terminal = true;
242
- controller?.error(new Error(reason));
243
- stopping = Promise.resolve().then(async () => {
244
- try {
245
- await iterator.return?.();
246
- }
247
- finally {
248
- operation.settle();
249
- }
250
- });
251
- return stopping;
252
- };
253
- const body = new ReadableStream({
254
- start(output) {
255
- controller = output;
256
- },
257
- async pull(controller) {
258
- if (terminal)
259
- return;
260
- try {
261
- const next = await iterator.next();
262
- if (terminal)
263
- return;
264
- if (next.done) {
265
- terminal = true;
266
- controller.close();
267
- operation.settle();
268
- return;
269
- }
270
- controller.enqueue(next.value);
271
- }
272
- catch (cause) {
273
- if (terminal)
274
- return;
275
- terminal = true;
276
- controller.error(cause);
277
- operation.settle();
278
- }
279
- },
280
- async cancel(reason) {
281
- const message = typeof reason === 'string' ? reason : 'native binary response cancelled';
282
- operation.cancel(message);
283
- await stop();
284
- },
285
- });
286
- operation.onCancel(stop);
287
- try {
288
- return new Response(body, {
289
- status: binary.status ?? 200,
290
- headers: {
291
- ...cors.response(request, Object.keys(applicationHeaders)),
292
- ...applicationHeaders,
293
- 'content-type': binary.mediaType,
294
- },
295
- });
296
- }
297
- catch (cause) {
298
- // A body-forbidding status (e.g. 204) rejects the Response after the producer was wired:
299
- // close it and let the caller settle the operation rather than leaking its slot.
300
- void stop('Binary response was refused.');
301
- throw cause;
302
- }
303
- }
304
- function isStreamingBinary(input) {
305
- return !(input.body instanceof Uint8Array);
306
- }
307
- function publicationPreflight(cors, request, snapshot, externalOrigin) {
308
- const document = servedDocument(snapshot);
309
- if (sameRoute(request.url, document.schema.bundle.href, externalOrigin)) {
310
- return cors.preflight(request, { methods: ['GET'], headers: ['if-none-match'] });
311
- }
312
- const deliveryUrl = document.delivery?.href;
313
- if (deliveryUrl !== undefined && sameRoute(request.url, deliveryUrl, externalOrigin)) {
314
- return cors.preflight(request, {
315
- methods: ['POST'],
316
- headers: ['authorization', 'content-type'],
317
- });
318
- }
319
- const requestedMethod = request.headers.get('access-control-request-method');
320
- const route = requestedMethod === null
321
- ? undefined
322
- : routes.match(snapshot.table, { url: request.url, method: requestedMethod });
323
- return cors.preflight(request, route === undefined ? undefined : nativeCorsRoute(route, requestedMethod));
324
- }
325
- function nativeCorsRoute(entry, requestedMethod) {
326
- const headers = new Set([
327
- 'accept',
328
- routes.HTTP_ROUTE_DIGEST,
329
- routes.HTTP_ROUTE_ISSUER,
330
- protocol.HTTP_IDEMPOTENCY_KEY,
331
- ]);
332
- if (entry.body !== undefined)
333
- headers.add('content-type');
334
- let allowAnyHeaders = false;
335
- for (const pick of Object.values(entry.headers ?? {})) {
336
- if (pick === '*')
337
- allowAnyHeaders = true;
338
- else
339
- headers.add(pick.toLowerCase());
340
- }
341
- const credential = entry.credential;
342
- const sources = credential === undefined
343
- ? [{ in: 'header' }]
344
- : Array.isArray(credential)
345
- ? credential
346
- : [credential];
347
- for (const source of sources) {
348
- if (source.in === 'header')
349
- headers.add((source.name ?? 'authorization').toLowerCase());
350
- }
351
- return Object.freeze({
352
- methods: Object.freeze([entry.method === '*' ? requestedMethod.toUpperCase() : entry.method]),
353
- headers: Object.freeze([...headers]),
354
- ...(allowAnyHeaders ? { allowAnyHeaders: true } : {}),
355
- });
356
- }
357
- /**
358
- * Read the one snapshot a request pins, withheld while it exceeds a serving limit, names another
359
- * serving issuer, or claims a route this Server reserves.
360
- */
361
- export function currentServed(source, limits, serving) {
362
- const prepared = readServed(source);
363
- if (prepared === undefined ||
364
- (prepared.kind === 'release' &&
365
- prepared.releaseBytes.byteLength > limits.maximumPublicationBytes) ||
366
- prepared.publicationBytes.byteLength > limits.maximumPublicationBytes ||
367
- prepared.bundle.byteLength > limits.maximumBundleBytes ||
368
- (serving?.issuer !== undefined &&
369
- servedDocument(prepared).identity.issuer !== serving.issuer) ||
370
- (serving !== undefined && conflictsWithServingRoutes(prepared, serving))) {
371
- return undefined;
372
- }
373
- return prepared;
374
- }
375
- function servingSnapshot(source, limits, request, reservations, externalIssuer, publicOrigin) {
376
- return currentServed(source, limits, {
377
- origin: publicOrigin ??
378
- (externalIssuer === undefined ? new URL(request.url).origin : new URL(externalIssuer).origin),
379
- ...(externalIssuer === undefined ? {} : { issuer: externalIssuer }),
380
- reservations,
381
- });
382
- }
383
- function conflictsWithServingRoutes(snapshot, serving) {
384
- for (const reservation of serving.reservations) {
385
- const href = `${serving.origin}${reservation.path}`;
386
- for (const entry of snapshot.table.entries) {
387
- if (reservation.methods.some((method) => entry.method === '*' || entry.method === method) &&
388
- routes.templatesOverlap(entry.href, href)) {
389
- return true;
390
- }
391
- }
392
- }
393
- return false;
394
- }
395
- function documentPath(path, url, externalIssuer) {
396
- return externalIssuer === undefined ? path : new URL(url(externalIssuer)).pathname;
397
- }
398
- function mountDocument(app, path, name, serving) {
399
- const { cors, active } = serving;
400
- if (cors.enabled) {
401
- app.options(path, (context) => cors.preflight(context.req.raw, {
402
- methods: ['GET'],
403
- headers: ['if-none-match'],
404
- }));
405
- }
406
- app.get(path, (context) => {
407
- const responseCors = cors.response(context.req.raw, ['etag']);
408
- if (!active.isAccepting) {
409
- return drained(`${name} is unavailable while draining.`, (exposed) => cors.response(context.req.raw, exposed));
410
- }
411
- const snapshot = serving.served(context.req.raw);
412
- const document = snapshot === undefined ? undefined : serving.read(snapshot);
413
- if (document === undefined)
414
- return response({ error: `${name} is not ready.` }, 503, responseCors);
415
- const etag = quoteEtag(document.etag);
416
- if (context.req.raw.headers.get('if-none-match') === etag) {
417
- return new Response(null, {
418
- status: 304,
419
- headers: {
420
- ...responseCors,
421
- etag,
422
- 'cache-control': 'public, max-age=0, must-revalidate, no-transform',
423
- },
424
- });
425
- }
426
- return new Response(toBody(document.bytes), {
427
- status: 200,
428
- headers: {
429
- ...responseCors,
430
- 'content-type': document.mediaType,
431
- etag,
432
- 'cache-control': 'public, max-age=0, must-revalidate, no-transform',
433
- },
434
- });
435
- });
436
- }
437
- /** Reserved native stale control naming the digest the destination serves (`StaleControl.current`). */
438
- function stale(request, current, cors) {
439
- return new Response(null, {
440
- status: routes.HTTP_ROUTE_STALE_STATUS,
441
- headers: {
442
- ...cors.response(request, [routes.HTTP_ROUTE_STALE]),
443
- ...routes.staleHeaders(current),
444
- },
445
- });
446
- }
447
- async function streamResponse(stream, operation, cors) {
448
- const iterator = stream[Symbol.asyncIterator]();
449
- let rejectSetup;
450
- const setupInterrupted = new Promise((_resolve, reject) => {
451
- rejectSetup = reject;
452
- });
453
- let setupCleanup;
454
- const cancelSetup = (reason) => {
455
- rejectSetup(operation.signal.reason ?? new Error(reason));
456
- setupCleanup ??= Promise.allSettled([
457
- Promise.resolve().then(() => stream.cancel(reason)),
458
- Promise.resolve().then(() => iterator.return?.()),
459
- ]).then(() => operation.settle());
460
- return setupCleanup;
461
- };
462
- const unregisterSetupCancellation = operation.onCancel(cancelSetup);
463
- let first;
464
- try {
465
- first = await Promise.race([iterator.next(), setupInterrupted]);
466
- }
467
- catch (cause) {
468
- unregisterSetupCancellation();
469
- const cleanup = cancelSetup('native stream setup failed');
470
- if (!operation.signal.aborted)
471
- await cleanup;
472
- const payload = classifyRequestError(cause, operation);
473
- return nativeError(payload, errors.httpStatusOf(payload), cors);
474
- }
475
- unregisterSetupCancellation();
476
- if (operation.signal.aborted) {
477
- void cancelSetup('native stream setup cancelled');
478
- const payload = classifyRequestError(operation.signal.reason, operation);
479
- return response({ error: payload }, errors.httpStatusOf(payload), cors());
480
- }
481
- const encoder = new TextEncoder();
482
- let pending = first;
483
- let terminal = false;
484
- let controller;
485
- let stopping;
486
- const encoded = (frame) => encoder.encode(routes.encodeStreamFrame(frame));
487
- const stop = (reason, error) => {
488
- stopping ??= (async () => {
489
- if (terminal)
490
- return;
491
- terminal = true;
492
- if (controller !== undefined) {
493
- if (error !== undefined) {
494
- try {
495
- controller.enqueue(encoded({ kind: 'error', error }));
496
- }
497
- catch {
498
- // The transport consumer may already have disconnected.
499
- }
500
- }
501
- try {
502
- controller.close();
503
- }
504
- catch {
505
- // A cancelled response body is already terminal.
506
- }
507
- }
508
- try {
509
- await Promise.allSettled([
510
- Promise.resolve().then(() => stream.cancel(reason)),
511
- Promise.resolve().then(() => iterator.return?.()),
512
- ]);
513
- }
514
- finally {
515
- operation.settle();
516
- }
517
- })();
518
- return stopping;
519
- };
520
- const body = new ReadableStream({
521
- start(output) {
522
- controller = output;
523
- },
524
- async pull(output) {
525
- if (terminal)
526
- return;
527
- try {
528
- const next = pending ?? (await iterator.next());
529
- pending = undefined;
530
- if (terminal)
531
- return;
532
- if (next.done) {
533
- terminal = true;
534
- output.enqueue(encoded({ kind: 'end' }));
535
- output.close();
536
- operation.settle();
537
- }
538
- else {
539
- output.enqueue(encoded({ kind: 'item', value: next.value }));
540
- }
541
- }
542
- catch (cause) {
543
- await stop('native stream failed', operation.signal.aborted
544
- ? interruptedStreamError(operation.timedOut)
545
- : classifyError(cause));
546
- }
547
- },
548
- async cancel(reason) {
549
- const message = typeof reason === 'string' ? reason : 'native response cancelled';
550
- operation.cancel(message);
551
- await stop(message);
552
- },
553
- });
554
- operation.onCancel((reason) => stop(reason, interruptedStreamError(operation.timedOut)));
555
- return new Response(body, {
556
- headers: {
557
- ...cors(),
558
- 'content-type': routes.STREAM_MEDIA_TYPE,
559
- 'cache-control': 'no-cache',
560
- },
561
- });
562
- }
563
- function sameRoute(left, right, externalOrigin) {
564
- const a = new URL(left);
565
- const b = new URL(right);
566
- return ((externalOrigin ?? a.origin) === b.origin && a.pathname === b.pathname && a.search === b.search);
567
- }
568
- /**
569
- * A request that mutates state or navigates to an effectful target. Non-safe HTTP methods always
570
- * qualify; a GET or HEAD qualifies only as a top-level browser navigation (`Sec-Fetch-Mode:
571
- * navigate`), the one GET shape that carries SameSite=Lax cookies cross-site. Programmatic GETs
572
- * (`cors`/`no-cors`) are left untouched so read clients are never broken.
573
- */
574
- function isStateChanging(request) {
575
- switch (request.method.toUpperCase()) {
576
- case 'POST':
577
- case 'PUT':
578
- case 'PATCH':
579
- case 'DELETE':
580
- return true;
581
- case 'GET':
582
- case 'HEAD':
583
- return request.headers.get('sec-fetch-mode') === 'navigate';
584
- default:
585
- return false;
586
- }
587
- }
588
- /**
589
- * Positive evidence that a request originates from another site. `Sec-Fetch-Site` is set by the
590
- * browser and cannot be forged by page script, so when present it is authoritative: only
591
- * `cross-site` is refused, and `same-origin`/`same-site`/`none` are trusted (a shared-domain
592
- * subdomain deployment keeps working). When it is absent, a differing `Origin` is the next-best
593
- * signal. Absence of both is treated as not-cross-site, so same-origin browsers and non-browser
594
- * clients that omit these headers are never refused.
595
- */
596
- function isClearlyCrossSite(request, serviceOrigin) {
597
- const site = request.headers.get('sec-fetch-site');
598
- if (site !== null)
599
- return site === 'cross-site';
600
- const origin = request.headers.get('origin');
601
- return origin !== null && origin !== serviceOrigin;
602
- }
603
- function applicationBinaryHeaders(input) {
604
- const output = {};
605
- for (const [name, value] of Object.entries(input ?? {})) {
606
- if (name.toLowerCase() === routes.HTTP_ROUTE_STALE ||
607
- name.toLowerCase() === routes.HTTP_ROUTE_MISS) {
608
- throw new TypeError(`${name.toLowerCase()} is reserved for native routing control.`);
609
- }
610
- if (protocol.http.isApplicationBinaryHeader(name))
611
- output[name] = value;
612
- }
613
- return output;
614
- }
615
- function quoteEtag(digest) {
616
- return `"${digest}"`;
617
- }
618
- function response(body, status, cors) {
619
- return new Response(encodedBody(codecs.json.encode(body)), {
620
- status,
621
- headers: { ...cors, 'content-type': 'application/json' },
622
- });
623
- }
624
- /** A served document or artifact refused while draining presents the lifecycle retry delay. */
625
- function drained(message, cors) {
626
- const retry = retryAfterHeaders(admissionRefusal(false));
627
- return response({ error: message }, 503, { ...cors(Object.keys(retry)), ...retry });
628
- }
629
- /** Every native error, including a capacity refusal raised by dispatch, presents its retry delay. */
630
- function nativeError(error, status, cors) {
631
- const admitted = routes.decodeErrorResponse({ error });
632
- const retry = retryAfterHeaders(error);
633
- return response(admitted, status, { ...cors(Object.keys(retry)), ...retry });
634
- }
635
- function encodedBody(input) {
636
- return typeof input === 'string' ? input : toBody(input);
637
- }
638
- function toBody(input) {
639
- return input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
640
- }
@@ -0,0 +1,13 @@
1
+ import type { Served, ServingSource } from '../release/served.js';
2
+ import type { RouteLimits, RouteReservation } from './model.js';
3
+ /**
4
+ * Read the one snapshot a request pins, withheld while it exceeds a serving limit, names another
5
+ * serving issuer, or claims a route this Server reserves.
6
+ */
7
+ export declare function currentServed(source: ServingSource, limits: Pick<RouteLimits, 'maximumReleaseBytes' | 'maximumBundleBytes'>, serving?: {
8
+ readonly origin: string;
9
+ readonly issuer?: string;
10
+ readonly reservations: readonly RouteReservation[];
11
+ }): Served | undefined;
12
+ /** Admit the one request snapshot against its trusted origin and reserved routes. */
13
+ export declare function servingSnapshot(source: ServingSource, limits: Pick<RouteLimits, 'maximumReleaseBytes' | 'maximumBundleBytes'>, request: Request, reservations: readonly RouteReservation[], externalIssuer?: string, publicOrigin?: string): Served | undefined;