@astrale-os/kernel-server 0.5.0-beta.69 → 0.5.0-beta.70

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 (62) hide show
  1. package/dist/cors/cors.js +9 -3
  2. package/dist/index.d.ts +8 -3
  3. package/dist/index.js +7 -2
  4. package/dist/invocation/index.d.ts +1 -1
  5. package/dist/invocation/index.js +1 -1
  6. package/dist/invocation/legacy/index.d.ts +20 -0
  7. package/dist/invocation/legacy/index.js +20 -0
  8. package/dist/invocation/{publication-route.d.ts → legacy/publication-route.d.ts} +6 -1
  9. package/dist/invocation/{publication-route.js → legacy/publication-route.js} +5 -1
  10. package/dist/invocation/release-route.d.ts +26 -0
  11. package/dist/invocation/release-route.js +55 -0
  12. package/dist/invocation/types.d.ts +16 -4
  13. package/dist/issuer/routes.js +13 -0
  14. package/dist/issuer/types.d.ts +5 -0
  15. package/dist/release/delivery/handler.d.ts +13 -0
  16. package/dist/{publication → release}/delivery/handler.js +33 -10
  17. package/dist/release/delivery/types.d.ts +21 -0
  18. package/dist/release/index.d.ts +3 -0
  19. package/dist/release/index.js +2 -0
  20. package/dist/release/legacy/delivery.d.ts +18 -0
  21. package/dist/release/legacy/delivery.js +2 -0
  22. package/dist/release/legacy/index.d.ts +39 -0
  23. package/dist/release/legacy/index.js +38 -0
  24. package/dist/{publication → release/legacy}/snapshot.d.ts +14 -4
  25. package/dist/{publication → release/legacy}/snapshot.js +11 -2
  26. package/dist/release/legacy/table.d.ts +5 -0
  27. package/dist/{publication → release/legacy}/table.js +2 -2
  28. package/dist/{publication → release/legacy}/types.d.ts +13 -4
  29. package/dist/release/served.d.ts +30 -0
  30. package/dist/release/served.js +14 -0
  31. package/dist/release/snapshot.d.ts +26 -0
  32. package/dist/release/snapshot.js +54 -0
  33. package/dist/release/table.d.ts +4 -0
  34. package/dist/release/table.js +11 -0
  35. package/dist/release/types.d.ts +33 -0
  36. package/dist/release/types.js +1 -0
  37. package/dist/routes/index.d.ts +1 -1
  38. package/dist/routes/index.js +1 -1
  39. package/dist/routes/routes.d.ts +9 -6
  40. package/dist/routes/routes.js +135 -76
  41. package/dist/server.js +66 -45
  42. package/dist/transport/codecs.d.ts +2 -2
  43. package/dist/transport/codecs.js +4 -3
  44. package/dist/transport/http/handler.d.ts +3 -2
  45. package/dist/transport/http/handler.js +18 -15
  46. package/dist/transport/http/render.d.ts +4 -0
  47. package/dist/transport/http/render.js +11 -3
  48. package/dist/transport/websocket/session.d.ts +3 -2
  49. package/dist/transport/websocket/session.js +15 -15
  50. package/dist/transport/websocket/upgrade.d.ts +2 -2
  51. package/dist/transport/websocket/upgrade.js +2 -4
  52. package/dist/types.d.ts +19 -4
  53. package/package.json +7 -8
  54. package/dist/publication/delivery/handler.d.ts +0 -9
  55. package/dist/publication/delivery/types.d.ts +0 -11
  56. package/dist/publication/index.d.ts +0 -3
  57. package/dist/publication/index.js +0 -2
  58. package/dist/publication/table.d.ts +0 -4
  59. /package/dist/{publication → release}/delivery/index.d.ts +0 -0
  60. /package/dist/{publication → release}/delivery/index.js +0 -0
  61. /package/dist/{publication → release}/delivery/types.js +0 -0
  62. /package/dist/{publication → release/legacy}/types.js +0 -0
@@ -0,0 +1,5 @@
1
+ import type { Domain } from '@astrale-os/kernel-dsl/v1';
2
+ import type * as publication from '@astrale-os/kernel-protocol/publication';
3
+ import { type Table } from '@astrale-os/kernel-protocol/routes/legacy';
4
+ /** @deprecated Use `admitTable` in `server/release/table.ts` for release-bound routes. */
5
+ export declare function admitTable(deployed: publication.Publication, input: Table | undefined, domain: Domain): Table;
@@ -1,6 +1,6 @@
1
1
  import { reference } from '@astrale-os/kernel-protocol/publication/reference';
2
- import { acceptTable } from '@astrale-os/kernel-protocol/routes/admission';
3
- import { verifyTable } from '@astrale-os/kernel-protocol/routes/evidence';
2
+ import { acceptTable, verifyTable } from '@astrale-os/kernel-protocol/routes/legacy';
3
+ /** @deprecated Use `admitTable` in `server/release/table.ts` for release-bound routes. */
4
4
  export function admitTable(deployed, input, domain) {
5
5
  if (input === undefined) {
6
6
  if (deployed.routes.length !== 0) {
@@ -1,20 +1,29 @@
1
1
  import type { Domain } from '@astrale-os/kernel-dsl/v1';
2
2
  import type * as publication from '@astrale-os/kernel-protocol/publication';
3
- import type * as routes from '@astrale-os/kernel-protocol/routes';
3
+ import type { Table } from '@astrale-os/kernel-protocol/routes/legacy';
4
4
  declare const PUBLICATION_SNAPSHOT: unique symbol;
5
+ /** @deprecated Successor: `ReleaseSnapshotInput`, which serves a `DomainRelease`. */
5
6
  export interface PublicationSnapshotInput {
6
7
  readonly publication: publication.Publication;
7
8
  /** Exact artifact served at Publication.schema.bundle.href. */
8
9
  readonly bundle: Uint8Array;
9
- readonly table?: routes.Table;
10
+ readonly table?: Table;
10
11
  readonly domain: Domain;
11
12
  }
12
- /** Immutable admitted serving identity; bundle and compiled routes remain Server-private. */
13
+ /**
14
+ * Immutable admitted serving identity; bundle and compiled routes remain Server-private.
15
+ *
16
+ * @deprecated Successor: `ReleaseSnapshot`, which serves a `DomainRelease` and its v3 projection.
17
+ */
13
18
  export interface PublicationSnapshot {
14
19
  readonly publication: publication.Publication;
15
20
  readonly [PUBLICATION_SNAPSHOT]: true;
16
21
  }
17
- /** Atomically readable source swapped only after a complete snapshot is prepared. */
22
+ /**
23
+ * Atomically readable source swapped only after a complete snapshot is prepared.
24
+ *
25
+ * @deprecated Successor: `ReleaseSource`.
26
+ */
18
27
  export interface PublicationSource {
19
28
  /** Undefined until the first revision-coherent snapshot is ready. */
20
29
  current(): PublicationSnapshot | undefined;
@@ -0,0 +1,30 @@
1
+ import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
2
+ import type { DomainRelease } from '@astrale-os/kernel-protocol/release';
3
+ import type { Publication } from '@astrale-os/kernel-protocol/release/legacy';
4
+ import type { PreparedPublicationSnapshot } from './legacy/snapshot.js';
5
+ import type { PublicationSnapshot, PublicationSource } from './legacy/types.js';
6
+ import type { PreparedReleaseSnapshot } from './snapshot.js';
7
+ import type { ReleaseSnapshot, ReleaseSource } from './types.js';
8
+ export type ServingSource = {
9
+ readonly kind: 'release';
10
+ readonly source: ReleaseSource;
11
+ } | {
12
+ readonly kind: 'publication';
13
+ readonly source: PublicationSource;
14
+ };
15
+ export type Served = PreparedReleaseSnapshot | PreparedPublicationSnapshot;
16
+ export interface RequestServing {
17
+ readonly kind: ServingSource['kind'];
18
+ /** The snapshot the request pins; undefined while none is ready or it is withheld. */
19
+ current(): Served | undefined;
20
+ /** The deployment a DEPLOYMENT_RETIRED answer names: the Server's public serving origin. */
21
+ deployment(): IssuerId;
22
+ }
23
+ export declare function readServed(serving: ServingSource): Served | undefined;
24
+ /** A release and its v3 projection share bundles, Delivery endpoints and native routes. */
25
+ export declare function servedDocument(served: Served): DomainRelease | Publication;
26
+ export declare function pinned(served: Served): {
27
+ readonly release: ReleaseSnapshot;
28
+ } | {
29
+ readonly publication: PublicationSnapshot;
30
+ };
@@ -0,0 +1,14 @@
1
+ import { readPreparedPublicationSnapshot } from './legacy/snapshot.js';
2
+ import { readPreparedReleaseSnapshot } from './snapshot.js';
3
+ export function readServed(serving) {
4
+ return serving.kind === 'release'
5
+ ? readPreparedReleaseSnapshot(serving.source.current())
6
+ : readPreparedPublicationSnapshot(serving.source.current());
7
+ }
8
+ /** A release and its v3 projection share bundles, Delivery endpoints and native routes. */
9
+ export function servedDocument(served) {
10
+ return served.kind === 'release' ? served.release : served.publication;
11
+ }
12
+ export function pinned(served) {
13
+ return served.kind === 'release' ? { release: served.snapshot } : { publication: served.snapshot };
14
+ }
@@ -0,0 +1,26 @@
1
+ import type { Domain } from '@astrale-os/kernel-dsl/v1';
2
+ import type { ServedReferences, Table } from '@astrale-os/kernel-protocol/routes';
3
+ import { type DomainRelease } from '@astrale-os/kernel-protocol/release';
4
+ import { type Publication } from '@astrale-os/kernel-protocol/release/legacy';
5
+ import type { ReleaseSnapshot, ReleaseSnapshotInput } from './types.js';
6
+ export interface PreparedReleaseSnapshot {
7
+ readonly kind: 'release';
8
+ readonly snapshot: ReleaseSnapshot;
9
+ readonly release: DomainRelease;
10
+ readonly releaseBytes: Uint8Array;
11
+ /** @deprecated Use `release`. Frozen v3 projection served at `domain.json`. */
12
+ readonly publication: Publication;
13
+ readonly publicationBytes: Uint8Array;
14
+ /** The release and the projection reference native-route If-Match may name. */
15
+ readonly references: ServedReferences;
16
+ readonly bundle: Uint8Array;
17
+ readonly table: Table;
18
+ readonly domain: Domain;
19
+ }
20
+ /**
21
+ * Admit and detach one deployment's serving snapshot before it is served: the release, its exact
22
+ * bundle bytes, its resolved Domain and route Table, and its frozen v3 projection.
23
+ */
24
+ export declare function prepareReleaseSnapshot(input: ReleaseSnapshotInput): ReleaseSnapshot;
25
+ /** Internal proof that a ReleaseSource returned a Server-prepared snapshot without cloning it. */
26
+ export declare function readPreparedReleaseSnapshot(input: unknown): PreparedReleaseSnapshot | undefined;
@@ -0,0 +1,54 @@
1
+ import { accept as acceptRelease, bundleUrl, encode as encodeRelease, verifyBundleBytes, } from '@astrale-os/kernel-protocol/release';
2
+ import { encode as encodePublication, publication as project, reference as publicationReference, } from '@astrale-os/kernel-protocol/release/legacy';
3
+ import { admitTable } from './table.js';
4
+ const snapshots = new WeakMap();
5
+ /**
6
+ * Admit and detach one deployment's serving snapshot before it is served: the release, its exact
7
+ * bundle bytes, its resolved Domain and route Table, and its frozen v3 projection.
8
+ */
9
+ export function prepareReleaseSnapshot(input) {
10
+ if (input === null || typeof input !== 'object' || !(input.bundle instanceof Uint8Array)) {
11
+ throw new TypeError('Release snapshot input is invalid.');
12
+ }
13
+ const release = acceptRelease(input.release);
14
+ const bundleBytes = new Uint8Array(input.bundle);
15
+ verifyBundleBytes(release, bundleBytes);
16
+ const domain = input.domain;
17
+ if (domain.origin !== release.origin || domain.revision !== release.schema.revision) {
18
+ throw new TypeError('Release snapshot Domain identity differs from its release.');
19
+ }
20
+ const table = admitTable(release, input.table, domain);
21
+ const addressed = new URL(bundleUrl(release.identity.issuer, release.schema.bundle.ref));
22
+ const addressedDirectory = addressed.pathname.slice(0, addressed.pathname.lastIndexOf('/') + 1);
23
+ const servingBundle = new URL(release.schema.bundle.href);
24
+ if (servingBundle.origin === addressed.origin &&
25
+ servingBundle.pathname.startsWith(addressedDirectory) &&
26
+ servingBundle.href !== addressed.href) {
27
+ throw new TypeError('Release bundle URL differs from its artifact digest.');
28
+ }
29
+ const projection = project(release);
30
+ const snapshot = Object.freeze({ release, publication: projection });
31
+ const prepared = Object.freeze({
32
+ kind: 'release',
33
+ snapshot,
34
+ release,
35
+ releaseBytes: encodeRelease(release),
36
+ publication: projection,
37
+ publicationBytes: encodePublication(projection),
38
+ references: Object.freeze({
39
+ release: table.release,
40
+ projection: publicationReference(projection),
41
+ }),
42
+ bundle: bundleBytes,
43
+ table,
44
+ domain,
45
+ });
46
+ snapshots.set(snapshot, prepared);
47
+ return snapshot;
48
+ }
49
+ /** Internal proof that a ReleaseSource returned a Server-prepared snapshot without cloning it. */
50
+ export function readPreparedReleaseSnapshot(input) {
51
+ if (input === null || typeof input !== 'object')
52
+ return undefined;
53
+ return snapshots.get(input);
54
+ }
@@ -0,0 +1,4 @@
1
+ import type { Domain } from '@astrale-os/kernel-dsl/v1';
2
+ import type { DomainRelease } from '@astrale-os/kernel-protocol/release';
3
+ import { type Table } from '@astrale-os/kernel-protocol/routes';
4
+ export declare function admitTable(deployed: DomainRelease, input: Table | undefined, domain: Domain): Table;
@@ -0,0 +1,11 @@
1
+ import { reference } from '@astrale-os/kernel-protocol/release';
2
+ import { acceptTable, verifyTable } from '@astrale-os/kernel-protocol/routes';
3
+ export function admitTable(deployed, input, domain) {
4
+ if (input === undefined) {
5
+ if (deployed.routes.length !== 0) {
6
+ throw new TypeError('A release with native routes requires a Route Table.');
7
+ }
8
+ return verifyTable(Object.freeze({ release: reference(deployed), entries: Object.freeze([]) }), deployed, domain);
9
+ }
10
+ return verifyTable(acceptTable(input), deployed, domain);
11
+ }
@@ -0,0 +1,33 @@
1
+ import type { Domain } from '@astrale-os/kernel-dsl/v1';
2
+ import type * as release from '@astrale-os/kernel-protocol/release';
3
+ import type { Table } from '@astrale-os/kernel-protocol/routes';
4
+ declare const RELEASE_SNAPSHOT: unique symbol;
5
+ export interface ReleaseSnapshotInput {
6
+ readonly release: release.DomainRelease;
7
+ /** Exact artifact served at DomainRelease.schema.bundle.href. */
8
+ readonly bundle: Uint8Array;
9
+ /** Route Table bound to this release; required when it declares native routes. */
10
+ readonly table?: Table;
11
+ readonly domain: Domain;
12
+ }
13
+ /**
14
+ * Immutable admitted serving identity of one deployment: the release it serves at `release.json`
15
+ * and the frozen v3 projection it also serves at `domain.json`. Bundle and compiled routes remain
16
+ * Server-private.
17
+ */
18
+ export interface ReleaseSnapshot {
19
+ readonly release: release.DomainRelease;
20
+ /**
21
+ * The frozen v3 projection served at `domain.json` and named by legacy routing witnesses.
22
+ *
23
+ * @deprecated Use `release`.
24
+ */
25
+ readonly publication: release.legacy.Publication;
26
+ readonly [RELEASE_SNAPSHOT]: true;
27
+ }
28
+ /** Atomically readable source swapped only after a complete snapshot is prepared. */
29
+ export interface ReleaseSource {
30
+ /** Undefined until the first snapshot is ready. */
31
+ current(): ReleaseSnapshot | undefined;
32
+ }
33
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1 +1 @@
1
- export { currentPublicationSnapshot, mountRoutes, type RouteLimits, type RouteReservation, } from './routes.js';
1
+ export { currentServed, mountRoutes, type RouteLimits, type RouteReservation } from './routes.js';
@@ -1 +1 @@
1
- export { currentPublicationSnapshot, mountRoutes, } from './routes.js';
1
+ export { currentServed, mountRoutes } from './routes.js';
@@ -1,10 +1,9 @@
1
1
  import type { CorsPolicy } from '../cors/index.js';
2
2
  import type { NextInvocationId } from '../invocation/index.js';
3
3
  import type { ActiveOperations } from '../lifecycle/index.js';
4
- import type { Provider as DeliveryProvider } from '../publication/delivery/index.js';
5
- import type { PublicationSource } from '../publication/index.js';
4
+ import type { Provider as DeliveryProvider } from '../release/delivery/index.js';
5
+ import type { Served, ServingSource } from '../release/served.js';
6
6
  import { type ApplicationHost, type Handler } from '../invocation/index.js';
7
- import { type PreparedPublicationSnapshot } from '../publication/index.js';
8
7
  import { type ServerRouter } from '../transport/router.js';
9
8
  export interface RouteLimits {
10
9
  readonly maximumRequestBytes: number;
@@ -17,9 +16,13 @@ export interface RouteReservation {
17
16
  readonly path: string;
18
17
  readonly methods: readonly string[];
19
18
  }
20
- export declare function mountRoutes(app: ServerRouter, source: PublicationSource, deliveryProvider: DeliveryProvider | undefined, handler: Handler, active: ActiveOperations, nextInvocation: NextInvocationId, limits: RouteLimits, cors: CorsPolicy, hostFor: (request: Request) => ApplicationHost | undefined, reservations: readonly RouteReservation[], externalIssuer?: string, publicOrigin?: string): void;
21
- export declare function currentPublicationSnapshot(source: PublicationSource, limits: Pick<RouteLimits, 'maximumPublicationBytes' | 'maximumBundleBytes'>, serving?: {
19
+ export declare function mountRoutes(app: ServerRouter, source: ServingSource, deliveryProvider: DeliveryProvider | undefined, handler: Handler, active: ActiveOperations, nextInvocation: NextInvocationId, limits: RouteLimits, cors: CorsPolicy, hostFor: (request: Request) => ApplicationHost | undefined, reservations: readonly RouteReservation[], externalIssuer?: string, publicOrigin?: string): void;
20
+ /**
21
+ * Read the one snapshot a request pins, withheld while it exceeds a serving limit, names another
22
+ * serving issuer, or claims a route this Server reserves.
23
+ */
24
+ export declare function currentServed(source: ServingSource, limits: Pick<RouteLimits, 'maximumPublicationBytes' | 'maximumBundleBytes'>, serving?: {
22
25
  readonly origin: string;
23
26
  readonly issuer?: string;
24
27
  readonly reservations: readonly RouteReservation[];
25
- }): PreparedPublicationSnapshot | undefined;
28
+ }): Served | undefined;
@@ -1,59 +1,46 @@
1
1
  import * as codecs from '@astrale-os/kernel-protocol/codecs';
2
2
  import * as errors from '@astrale-os/kernel-protocol/errors';
3
3
  import * as protocol from '@astrale-os/kernel-protocol/invocation';
4
- import { bundleUrl } from '@astrale-os/kernel-protocol/publication';
5
- import { mediaType as publicationMediaType, PATH as PUBLICATION_PATH, url as publicationUrl, } from '@astrale-os/kernel-protocol/publication/publication';
6
- import { reference as publicationReference } from '@astrale-os/kernel-protocol/publication/reference';
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';
7
6
  import * as routes from '@astrale-os/kernel-protocol/routes';
8
7
  import { admissionRefusal, classifyError, classifyRequestError, RequestBodyInvalidError, RequestBodyTooLargeError, retryAfterHeaders, } from '../errors/index.js';
9
8
  import { invocationContext } from '../invocation/index.js';
10
- import { handleDelivery } from '../publication/delivery/index.js';
11
- import { readPreparedPublicationSnapshot, } from '../publication/index.js';
9
+ import { handleDelivery } from '../release/delivery/index.js';
10
+ import { pinned, readServed, servedDocument } from '../release/served.js';
12
11
  import { readBoundedBody } from '../transport/body.js';
13
12
  import {} from '../transport/router.js';
14
13
  import { interruptedStreamError } from '../transport/stream-error.js';
15
14
  export function mountRoutes(app, source, deliveryProvider, handler, active, nextInvocation, limits, cors, hostFor, reservations, externalIssuer, publicOrigin) {
16
- const publicationPath = externalIssuer === undefined
17
- ? PUBLICATION_PATH
18
- : new URL(publicationUrl(externalIssuer)).pathname;
19
15
  const externalOrigin = publicOrigin ?? (externalIssuer === undefined ? undefined : new URL(externalIssuer).origin);
20
- if (cors.enabled) {
21
- app.options(publicationPath, (context) => cors.preflight(context.req.raw, {
22
- methods: ['GET'],
23
- headers: ['if-none-match'],
24
- }));
25
- }
26
- app.get(publicationPath, (context) => {
27
- const responseCors = cors.response(context.req.raw, ['etag']);
28
- if (!active.isAccepting) {
29
- return drained('Publication is unavailable while draining.', (exposed) => cors.response(context.req.raw, exposed));
30
- }
31
- const snapshot = servingSnapshot(source, limits, context.req.raw, reservations, externalIssuer, externalOrigin);
32
- if (snapshot === undefined)
33
- return response({ error: 'Publication is not ready.' }, 503, responseCors);
34
- const body = snapshot.publicationBytes;
35
- if (context.req.raw.headers.get('if-none-match') === quoteEtag(snapshot.publication.etag)) {
36
- return new Response(null, {
37
- status: 304,
38
- headers: {
39
- ...responseCors,
40
- etag: quoteEtag(snapshot.publication.etag),
41
- 'cache-control': 'public, max-age=0, must-revalidate, no-transform',
42
- },
43
- });
44
- }
45
- return new Response(toBody(body), {
46
- status: 200,
47
- headers: {
48
- ...responseCors,
49
- 'content-type': publicationMediaType(snapshot.publication),
50
- etag: quoteEtag(snapshot.publication.etag),
51
- 'cache-control': 'public, max-age=0, must-revalidate, no-transform',
52
- },
16
+ 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,
53
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
+ }),
54
41
  });
55
42
  app.all('*', async (context) => {
56
- const snapshot = servingSnapshot(source, limits, context.req.raw, reservations, externalIssuer, externalOrigin);
43
+ const snapshot = served(context.req.raw);
57
44
  if (snapshot === undefined)
58
45
  return context.notFound();
59
46
  const request = context.req.raw;
@@ -62,17 +49,27 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
62
49
  }
63
50
  const responseCors = cors.response(request);
64
51
  const corsFor = (exposed) => cors.response(request, exposed);
65
- const descriptor = snapshot.publication.schema.bundle;
66
- const addressed = bundleUrl(snapshot.publication.identity.issuer, descriptor.ref);
52
+ const document = servedDocument(snapshot);
53
+ const descriptor = document.schema.bundle;
54
+ const addressed = bundleUrl(document.identity.issuer, descriptor.ref);
67
55
  const immutable = descriptor.href === addressed;
68
- if (sameRoute(request.url, snapshot.publication.schema.bundle.href, externalOrigin)) {
69
- if (!active.isAccepting)
70
- return drained('Publication artifact is unavailable while draining.', corsFor);
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
+ }
71
64
  if (request.method !== 'GET')
72
65
  return response({ error: 'Bundle retrieval requires GET.' }, 405, responseCors);
73
66
  const body = snapshot.bundle;
74
- if (body.byteLength !== snapshot.publication.schema.bundle.ref.size) {
75
- return response({ error: 'Bundle differs from its Publication.' }, 500, responseCors);
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);
76
73
  }
77
74
  const etag = quoteEtag(descriptor.ref.digest);
78
75
  const unchanged = request.headers.get('if-none-match') === etag;
@@ -80,7 +77,7 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
80
77
  status: unchanged ? 304 : 200,
81
78
  headers: {
82
79
  ...responseCors,
83
- 'content-type': snapshot.publication.schema.bundle.ref.mediaType,
80
+ 'content-type': descriptor.ref.mediaType,
84
81
  etag,
85
82
  'cache-control': immutable
86
83
  ? 'public, max-age=31536000, immutable, no-transform'
@@ -100,25 +97,27 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
100
97
  });
101
98
  }
102
99
  }
103
- const deliveryUrl = snapshot.publication.delivery?.href;
100
+ const deliveryUrl = document.delivery?.href;
104
101
  if (deliveryUrl !== undefined && sameRoute(request.url, deliveryUrl, externalOrigin)) {
105
102
  return handleDelivery(request, snapshot, deliveryProvider, active, limits, corsFor);
106
103
  }
107
104
  let routing;
108
105
  try {
109
- const ifMatch = request.headers.get(routes.HTTP_ROUTE_PUBLICATION);
110
- if (ifMatch !== null &&
111
- /^"sha256:[0-9a-f]{64}"$/u.test(ifMatch) &&
112
- ifMatch !== quoteEtag(snapshot.publication.etag)) {
113
- return new Response(null, {
114
- status: routes.HTTP_ROUTE_STALE_STATUS,
115
- headers: {
116
- ...cors.response(request, [routes.HTTP_ROUTE_STALE]),
117
- ...routes.staleHeaders(snapshot.publication.etag),
118
- },
119
- });
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
120
  }
121
- routing = routes.decodeRouting(request, publicationReference(snapshot.publication));
122
121
  }
123
122
  catch (cause) {
124
123
  if (cause instanceof routes.RouteError) {
@@ -136,7 +135,10 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
136
135
  let handedOff = false;
137
136
  try {
138
137
  const body = await readBoundedBody(request, limits.maximumRequestBytes, operation.signal);
139
- const invocation = routes.decode(route, { url: request.url, method: request.method, headers: request.headers, body }, snapshot.domain, snapshot.table.publication);
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);
140
142
  const idempotencyHeader = request.headers.get(protocol.HTTP_IDEMPOTENCY_KEY);
141
143
  const idempotencyKey = idempotencyHeader === null
142
144
  ? undefined
@@ -145,7 +147,7 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
145
147
  call: invocation.call,
146
148
  ...(invocation.credential === undefined ? {} : { credential: invocation.credential }),
147
149
  ...(routing === undefined ? {} : { via: routing }),
148
- publication: snapshot.snapshot,
150
+ ...pinned(snapshot),
149
151
  context: invocationContext(assigned, operation, active, hostFor(request), body, idempotencyKey),
150
152
  }));
151
153
  if (result.kind === 'value') {
@@ -280,10 +282,11 @@ function isStreamingBinary(input) {
280
282
  return !(input.body instanceof Uint8Array);
281
283
  }
282
284
  function publicationPreflight(cors, request, snapshot, externalOrigin) {
283
- if (sameRoute(request.url, snapshot.publication.schema.bundle.href, externalOrigin)) {
285
+ const document = servedDocument(snapshot);
286
+ if (sameRoute(request.url, document.schema.bundle.href, externalOrigin)) {
284
287
  return cors.preflight(request, { methods: ['GET'], headers: ['if-none-match'] });
285
288
  }
286
- const deliveryUrl = snapshot.publication.delivery?.href;
289
+ const deliveryUrl = document.delivery?.href;
287
290
  if (deliveryUrl !== undefined && sameRoute(request.url, deliveryUrl, externalOrigin)) {
288
291
  return cors.preflight(request, {
289
292
  methods: ['POST'],
@@ -299,7 +302,7 @@ function publicationPreflight(cors, request, snapshot, externalOrigin) {
299
302
  function nativeCorsRoute(entry, requestedMethod) {
300
303
  const headers = new Set([
301
304
  'accept',
302
- routes.HTTP_ROUTE_PUBLICATION,
305
+ routes.HTTP_ROUTE_DIGEST,
303
306
  routes.HTTP_ROUTE_ISSUER,
304
307
  protocol.HTTP_IDEMPOTENCY_KEY,
305
308
  ]);
@@ -328,22 +331,26 @@ function nativeCorsRoute(entry, requestedMethod) {
328
331
  ...(allowAnyHeaders ? { allowAnyHeaders: true } : {}),
329
332
  });
330
333
  }
331
- export function currentPublicationSnapshot(source, limits, serving) {
332
- const snapshot = source.current();
333
- if (snapshot === undefined)
334
- return undefined;
335
- const prepared = readPreparedPublicationSnapshot(snapshot);
334
+ /**
335
+ * Read the one snapshot a request pins, withheld while it exceeds a serving limit, names another
336
+ * serving issuer, or claims a route this Server reserves.
337
+ */
338
+ export function currentServed(source, limits, serving) {
339
+ const prepared = readServed(source);
336
340
  if (prepared === undefined ||
341
+ (prepared.kind === 'release' &&
342
+ prepared.releaseBytes.byteLength > limits.maximumPublicationBytes) ||
337
343
  prepared.publicationBytes.byteLength > limits.maximumPublicationBytes ||
338
344
  prepared.bundle.byteLength > limits.maximumBundleBytes ||
339
- (serving?.issuer !== undefined && prepared.publication.identity.issuer !== serving.issuer) ||
345
+ (serving?.issuer !== undefined &&
346
+ servedDocument(prepared).identity.issuer !== serving.issuer) ||
340
347
  (serving !== undefined && conflictsWithServingRoutes(prepared, serving))) {
341
348
  return undefined;
342
349
  }
343
350
  return prepared;
344
351
  }
345
352
  function servingSnapshot(source, limits, request, reservations, externalIssuer, publicOrigin) {
346
- return currentPublicationSnapshot(source, limits, {
353
+ return currentServed(source, limits, {
347
354
  origin: publicOrigin ??
348
355
  (externalIssuer === undefined ? new URL(request.url).origin : new URL(externalIssuer).origin),
349
356
  ...(externalIssuer === undefined ? {} : { issuer: externalIssuer }),
@@ -362,6 +369,58 @@ function conflictsWithServingRoutes(snapshot, serving) {
362
369
  }
363
370
  return false;
364
371
  }
372
+ function documentPath(path, url, externalIssuer) {
373
+ return externalIssuer === undefined ? path : new URL(url(externalIssuer)).pathname;
374
+ }
375
+ function mountDocument(app, path, name, serving) {
376
+ const { cors, active } = serving;
377
+ if (cors.enabled) {
378
+ app.options(path, (context) => cors.preflight(context.req.raw, {
379
+ methods: ['GET'],
380
+ headers: ['if-none-match'],
381
+ }));
382
+ }
383
+ app.get(path, (context) => {
384
+ const responseCors = cors.response(context.req.raw, ['etag']);
385
+ if (!active.isAccepting) {
386
+ return drained(`${name} is unavailable while draining.`, (exposed) => cors.response(context.req.raw, exposed));
387
+ }
388
+ const snapshot = serving.served(context.req.raw);
389
+ const document = snapshot === undefined ? undefined : serving.read(snapshot);
390
+ if (document === undefined)
391
+ return response({ error: `${name} is not ready.` }, 503, responseCors);
392
+ const etag = quoteEtag(document.etag);
393
+ if (context.req.raw.headers.get('if-none-match') === etag) {
394
+ return new Response(null, {
395
+ status: 304,
396
+ headers: {
397
+ ...responseCors,
398
+ etag,
399
+ 'cache-control': 'public, max-age=0, must-revalidate, no-transform',
400
+ },
401
+ });
402
+ }
403
+ return new Response(toBody(document.bytes), {
404
+ status: 200,
405
+ headers: {
406
+ ...responseCors,
407
+ 'content-type': document.mediaType,
408
+ etag,
409
+ 'cache-control': 'public, max-age=0, must-revalidate, no-transform',
410
+ },
411
+ });
412
+ });
413
+ }
414
+ /** Reserved native stale control naming the digest the destination serves (`StaleControl.current`). */
415
+ function stale(request, current, cors) {
416
+ return new Response(null, {
417
+ status: routes.HTTP_ROUTE_STALE_STATUS,
418
+ headers: {
419
+ ...cors.response(request, [routes.HTTP_ROUTE_STALE]),
420
+ ...routes.staleHeaders(current),
421
+ },
422
+ });
423
+ }
365
424
  async function streamResponse(stream, operation, cors) {
366
425
  const iterator = stream[Symbol.asyncIterator]();
367
426
  let rejectSetup;
@@ -504,7 +563,7 @@ function response(body, status, cors) {
504
563
  headers: { ...cors, 'content-type': 'application/json' },
505
564
  });
506
565
  }
507
- /** A Publication artifact refused while draining presents the lifecycle retry delay. */
566
+ /** A served document or artifact refused while draining presents the lifecycle retry delay. */
508
567
  function drained(message, cors) {
509
568
  const retry = retryAfterHeaders(admissionRefusal(false));
510
569
  return response({ error: message }, 503, { ...cors(Object.keys(retry)), ...retry });