@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.
- package/dist/cors/cors.js +9 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +7 -2
- package/dist/invocation/index.d.ts +1 -1
- package/dist/invocation/index.js +1 -1
- package/dist/invocation/legacy/index.d.ts +20 -0
- package/dist/invocation/legacy/index.js +20 -0
- package/dist/invocation/{publication-route.d.ts → legacy/publication-route.d.ts} +6 -1
- package/dist/invocation/{publication-route.js → legacy/publication-route.js} +5 -1
- package/dist/invocation/release-route.d.ts +26 -0
- package/dist/invocation/release-route.js +55 -0
- package/dist/invocation/types.d.ts +16 -4
- package/dist/issuer/routes.js +13 -0
- package/dist/issuer/types.d.ts +5 -0
- package/dist/release/delivery/handler.d.ts +13 -0
- package/dist/{publication → release}/delivery/handler.js +33 -10
- package/dist/release/delivery/types.d.ts +21 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +2 -0
- package/dist/release/legacy/delivery.d.ts +18 -0
- package/dist/release/legacy/delivery.js +2 -0
- package/dist/release/legacy/index.d.ts +39 -0
- package/dist/release/legacy/index.js +38 -0
- package/dist/{publication → release/legacy}/snapshot.d.ts +14 -4
- package/dist/{publication → release/legacy}/snapshot.js +11 -2
- package/dist/release/legacy/table.d.ts +5 -0
- package/dist/{publication → release/legacy}/table.js +2 -2
- package/dist/{publication → release/legacy}/types.d.ts +13 -4
- package/dist/release/served.d.ts +30 -0
- package/dist/release/served.js +14 -0
- package/dist/release/snapshot.d.ts +26 -0
- package/dist/release/snapshot.js +54 -0
- package/dist/release/table.d.ts +4 -0
- package/dist/release/table.js +11 -0
- package/dist/release/types.d.ts +33 -0
- package/dist/release/types.js +1 -0
- package/dist/routes/index.d.ts +1 -1
- package/dist/routes/index.js +1 -1
- package/dist/routes/routes.d.ts +9 -6
- package/dist/routes/routes.js +135 -76
- package/dist/server.js +66 -45
- package/dist/transport/codecs.d.ts +2 -2
- package/dist/transport/codecs.js +4 -3
- package/dist/transport/http/handler.d.ts +3 -2
- package/dist/transport/http/handler.js +18 -15
- package/dist/transport/http/render.d.ts +4 -0
- package/dist/transport/http/render.js +11 -3
- package/dist/transport/websocket/session.d.ts +3 -2
- package/dist/transport/websocket/session.js +15 -15
- package/dist/transport/websocket/upgrade.d.ts +2 -2
- package/dist/transport/websocket/upgrade.js +2 -4
- package/dist/types.d.ts +19 -4
- package/package.json +7 -8
- package/dist/publication/delivery/handler.d.ts +0 -9
- package/dist/publication/delivery/types.d.ts +0 -11
- package/dist/publication/index.d.ts +0 -3
- package/dist/publication/index.js +0 -2
- package/dist/publication/table.d.ts +0 -4
- /package/dist/{publication → release}/delivery/index.d.ts +0 -0
- /package/dist/{publication → release}/delivery/index.js +0 -0
- /package/dist/{publication → release}/delivery/types.js +0 -0
- /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/
|
|
3
|
-
|
|
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
|
|
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?:
|
|
10
|
+
readonly table?: Table;
|
|
10
11
|
readonly domain: Domain;
|
|
11
12
|
}
|
|
12
|
-
/**
|
|
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
|
-
/**
|
|
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 {};
|
package/dist/routes/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { currentServed, mountRoutes, type RouteLimits, type RouteReservation } from './routes.js';
|
package/dist/routes/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { currentServed, mountRoutes } from './routes.js';
|
package/dist/routes/routes.d.ts
CHANGED
|
@@ -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 '../
|
|
5
|
-
import type {
|
|
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:
|
|
21
|
-
|
|
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
|
-
}):
|
|
28
|
+
}): Served | undefined;
|
package/dist/routes/routes.js
CHANGED
|
@@ -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/
|
|
5
|
-
import { mediaType as publicationMediaType, PATH as PUBLICATION_PATH, url as publicationUrl, } from '@astrale-os/kernel-protocol/
|
|
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 '../
|
|
11
|
-
import {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 =
|
|
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
|
|
66
|
-
const
|
|
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,
|
|
69
|
-
|
|
70
|
-
|
|
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 !==
|
|
75
|
-
return response({
|
|
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':
|
|
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 =
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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 &&
|
|
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
|
|
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
|
|
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 });
|