@astrale-os/kernel-server 0.5.0-beta.7 → 0.5.0-beta.71
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/capture.d.ts +0 -1
- package/dist/capture.js +0 -1
- package/dist/cors/cors.d.ts +0 -1
- package/dist/cors/cors.js +11 -4
- package/dist/cors/index.d.ts +0 -1
- package/dist/cors/index.js +0 -1
- package/dist/cors/types.d.ts +0 -1
- package/dist/cors/types.js +0 -1
- package/dist/errors/admission.d.ts +7 -0
- package/dist/errors/admission.js +22 -0
- package/dist/errors/availability.d.ts +0 -1
- package/dist/errors/availability.js +0 -1
- package/dist/errors/classify.d.ts +8 -1
- package/dist/errors/classify.js +15 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +2 -2
- package/dist/errors/request-body.d.ts +0 -1
- package/dist/errors/request-body.js +0 -1
- package/dist/errors/response-body.d.ts +0 -1
- package/dist/errors/response-body.js +0 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +7 -3
- package/dist/invocation/context.d.ts +3 -3
- package/dist/invocation/context.js +2 -2
- package/dist/invocation/identity.d.ts +1 -2
- package/dist/invocation/identity.js +1 -2
- package/dist/invocation/index.d.ts +1 -2
- package/dist/invocation/index.js +1 -2
- 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} +7 -5
- package/dist/invocation/{publication-route.js → legacy/publication-route.js} +7 -13
- package/dist/invocation/release-route.d.ts +26 -0
- package/dist/invocation/release-route.js +55 -0
- package/dist/invocation/types.d.ts +20 -7
- package/dist/invocation/types.js +0 -1
- package/dist/issuer/exchange.d.ts +0 -1
- package/dist/issuer/exchange.js +20 -10
- package/dist/issuer/index.d.ts +0 -1
- package/dist/issuer/index.js +0 -1
- package/dist/issuer/routes.d.ts +0 -1
- package/dist/issuer/routes.js +33 -5
- package/dist/issuer/types.d.ts +5 -1
- package/dist/issuer/types.js +0 -1
- package/dist/lifecycle/active.d.ts +4 -1
- package/dist/lifecycle/active.js +48 -11
- package/dist/lifecycle/index.d.ts +0 -1
- package/dist/lifecycle/index.js +0 -1
- package/dist/release/delivery/handler.d.ts +13 -0
- package/dist/{publication → release}/delivery/handler.js +42 -15
- package/dist/{publication → release}/delivery/index.d.ts +0 -1
- package/dist/{publication → release}/delivery/index.js +0 -1
- package/dist/release/delivery/types.d.ts +21 -0
- package/dist/release/delivery/types.js +1 -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/release/legacy/snapshot.d.ts +26 -0
- package/dist/release/legacy/snapshot.js +55 -0
- package/dist/release/legacy/table.d.ts +5 -0
- package/dist/{publication → release/legacy}/table.js +5 -6
- package/dist/release/legacy/types.d.ts +31 -0
- package/dist/release/legacy/types.js +1 -0
- 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 -2
- package/dist/routes/index.js +1 -2
- package/dist/routes/routes.d.ts +9 -7
- package/dist/routes/routes.js +314 -123
- package/dist/server.d.ts +0 -1
- package/dist/server.js +110 -48
- package/dist/transport/body.d.ts +0 -1
- package/dist/transport/body.js +0 -1
- package/dist/transport/codecs.d.ts +2 -3
- package/dist/transport/codecs.js +4 -4
- package/dist/transport/credential.d.ts +0 -1
- package/dist/transport/credential.js +0 -1
- package/dist/transport/http/binary-request.d.ts +11 -0
- package/dist/transport/http/binary-request.js +102 -0
- package/dist/transport/http/handler.d.ts +8 -3
- package/dist/transport/http/handler.js +95 -45
- package/dist/transport/http/index.d.ts +0 -1
- package/dist/transport/http/index.js +0 -1
- package/dist/transport/http/render.d.ts +9 -4
- package/dist/transport/http/render.js +150 -5
- package/dist/transport/index.d.ts +0 -1
- package/dist/transport/index.js +0 -1
- package/dist/transport/router.d.ts +0 -1
- package/dist/transport/router.js +0 -1
- package/dist/transport/stream-error.d.ts +0 -1
- package/dist/transport/stream-error.js +0 -1
- package/dist/transport/types.d.ts +1 -2
- package/dist/transport/types.js +0 -1
- package/dist/transport/websocket/index.d.ts +0 -1
- package/dist/transport/websocket/index.js +0 -1
- package/dist/transport/websocket/session.d.ts +3 -3
- package/dist/transport/websocket/session.js +30 -30
- package/dist/transport/websocket/upgrade.d.ts +2 -3
- package/dist/transport/websocket/upgrade.js +7 -7
- package/dist/types.d.ts +32 -6
- package/dist/types.js +0 -1
- package/package.json +12 -15
- package/dist/capture.d.ts.map +0 -1
- package/dist/capture.js.map +0 -1
- package/dist/cors/cors.d.ts.map +0 -1
- package/dist/cors/cors.js.map +0 -1
- package/dist/cors/index.d.ts.map +0 -1
- package/dist/cors/index.js.map +0 -1
- package/dist/cors/types.d.ts.map +0 -1
- package/dist/cors/types.js.map +0 -1
- package/dist/errors/availability.d.ts.map +0 -1
- package/dist/errors/availability.js.map +0 -1
- package/dist/errors/classify.d.ts.map +0 -1
- package/dist/errors/classify.js.map +0 -1
- package/dist/errors/index.d.ts.map +0 -1
- package/dist/errors/index.js.map +0 -1
- package/dist/errors/request-body.d.ts.map +0 -1
- package/dist/errors/request-body.js.map +0 -1
- package/dist/errors/response-body.d.ts.map +0 -1
- package/dist/errors/response-body.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/invocation/context.d.ts.map +0 -1
- package/dist/invocation/context.js.map +0 -1
- package/dist/invocation/identity.d.ts.map +0 -1
- package/dist/invocation/identity.js.map +0 -1
- package/dist/invocation/index.d.ts.map +0 -1
- package/dist/invocation/index.js.map +0 -1
- package/dist/invocation/publication-route.d.ts.map +0 -1
- package/dist/invocation/publication-route.js.map +0 -1
- package/dist/invocation/types.d.ts.map +0 -1
- package/dist/invocation/types.js.map +0 -1
- package/dist/issuer/exchange.d.ts.map +0 -1
- package/dist/issuer/exchange.js.map +0 -1
- package/dist/issuer/index.d.ts.map +0 -1
- package/dist/issuer/index.js.map +0 -1
- package/dist/issuer/routes.d.ts.map +0 -1
- package/dist/issuer/routes.js.map +0 -1
- package/dist/issuer/types.d.ts.map +0 -1
- package/dist/issuer/types.js.map +0 -1
- package/dist/lifecycle/active.d.ts.map +0 -1
- package/dist/lifecycle/active.js.map +0 -1
- package/dist/lifecycle/index.d.ts.map +0 -1
- package/dist/lifecycle/index.js.map +0 -1
- package/dist/publication/delivery/handler.d.ts +0 -10
- package/dist/publication/delivery/handler.d.ts.map +0 -1
- package/dist/publication/delivery/handler.js.map +0 -1
- package/dist/publication/delivery/index.d.ts.map +0 -1
- package/dist/publication/delivery/index.js.map +0 -1
- package/dist/publication/delivery/types.d.ts +0 -12
- package/dist/publication/delivery/types.d.ts.map +0 -1
- package/dist/publication/delivery/types.js +0 -2
- package/dist/publication/delivery/types.js.map +0 -1
- package/dist/publication/index.d.ts +0 -4
- package/dist/publication/index.d.ts.map +0 -1
- package/dist/publication/index.js +0 -3
- package/dist/publication/index.js.map +0 -1
- package/dist/publication/snapshot.d.ts +0 -16
- package/dist/publication/snapshot.d.ts.map +0 -1
- package/dist/publication/snapshot.js +0 -39
- package/dist/publication/snapshot.js.map +0 -1
- package/dist/publication/table.d.ts +0 -4
- package/dist/publication/table.d.ts.map +0 -1
- package/dist/publication/table.js.map +0 -1
- package/dist/publication/types.d.ts +0 -23
- package/dist/publication/types.d.ts.map +0 -1
- package/dist/publication/types.js +0 -2
- package/dist/publication/types.js.map +0 -1
- package/dist/publication/validator.d.ts +0 -4
- package/dist/publication/validator.d.ts.map +0 -1
- package/dist/publication/validator.js +0 -17
- package/dist/publication/validator.js.map +0 -1
- package/dist/routes/index.d.ts.map +0 -1
- package/dist/routes/index.js.map +0 -1
- package/dist/routes/routes.d.ts.map +0 -1
- package/dist/routes/routes.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/transport/body.d.ts.map +0 -1
- package/dist/transport/body.js.map +0 -1
- package/dist/transport/codecs.d.ts.map +0 -1
- package/dist/transport/codecs.js.map +0 -1
- package/dist/transport/credential.d.ts.map +0 -1
- package/dist/transport/credential.js.map +0 -1
- package/dist/transport/http/handler.d.ts.map +0 -1
- package/dist/transport/http/handler.js.map +0 -1
- package/dist/transport/http/index.d.ts.map +0 -1
- package/dist/transport/http/index.js.map +0 -1
- package/dist/transport/http/render.d.ts.map +0 -1
- package/dist/transport/http/render.js.map +0 -1
- package/dist/transport/index.d.ts.map +0 -1
- package/dist/transport/index.js.map +0 -1
- package/dist/transport/router.d.ts.map +0 -1
- package/dist/transport/router.js.map +0 -1
- package/dist/transport/stream-error.d.ts.map +0 -1
- package/dist/transport/stream-error.js.map +0 -1
- package/dist/transport/types.d.ts.map +0 -1
- package/dist/transport/types.js.map +0 -1
- package/dist/transport/websocket/index.d.ts.map +0 -1
- package/dist/transport/websocket/index.js.map +0 -1
- package/dist/transport/websocket/session.d.ts.map +0 -1
- package/dist/transport/websocket/session.js.map +0 -1
- package/dist/transport/websocket/upgrade.d.ts.map +0 -1
- package/dist/transport/websocket/upgrade.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/routes/routes.js
CHANGED
|
@@ -1,57 +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 { PATH as
|
|
5
|
-
import { reference as publicationReference } from '@astrale-os/kernel-protocol/
|
|
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
6
|
import * as routes from '@astrale-os/kernel-protocol/routes';
|
|
7
|
-
import { classifyError, RequestBodyInvalidError, RequestBodyTooLargeError, } from '../errors/index.js';
|
|
7
|
+
import { admissionRefusal, classifyError, classifyRequestError, RequestBodyInvalidError, RequestBodyTooLargeError, retryAfterHeaders, } from '../errors/index.js';
|
|
8
8
|
import { invocationContext } from '../invocation/index.js';
|
|
9
|
-
import { handleDelivery } from '../
|
|
10
|
-
import {
|
|
9
|
+
import { handleDelivery } from '../release/delivery/index.js';
|
|
10
|
+
import { pinned, readServed, servedDocument } from '../release/served.js';
|
|
11
11
|
import { readBoundedBody } from '../transport/body.js';
|
|
12
12
|
import {} from '../transport/router.js';
|
|
13
13
|
import { interruptedStreamError } from '../transport/stream-error.js';
|
|
14
|
-
export function mountRoutes(app, source, deliveryProvider, handler, active, nextInvocation, limits, cors, hostFor, reservations, externalIssuer) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const snapshot = servingSnapshot(source, limits, context.req.raw, reservations, externalIssuer);
|
|
30
|
-
if (snapshot === undefined)
|
|
31
|
-
return response({ error: 'Publication is not ready.' }, 503, responseCors);
|
|
32
|
-
const body = snapshot.publicationBytes;
|
|
33
|
-
if (context.req.raw.headers.get('if-none-match') === quoteEtag(snapshot.publication.etag)) {
|
|
34
|
-
return new Response(null, {
|
|
35
|
-
status: 304,
|
|
36
|
-
headers: {
|
|
37
|
-
...responseCors,
|
|
38
|
-
etag: quoteEtag(snapshot.publication.etag),
|
|
39
|
-
'cache-control': 'public, max-age=0, must-revalidate, no-transform',
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
return new Response(toBody(body), {
|
|
44
|
-
status: 200,
|
|
45
|
-
headers: {
|
|
46
|
-
...responseCors,
|
|
47
|
-
'content-type': 'application/json',
|
|
48
|
-
etag: quoteEtag(snapshot.publication.etag),
|
|
49
|
-
'cache-control': 'public, max-age=0, must-revalidate, no-transform',
|
|
50
|
-
},
|
|
14
|
+
export function mountRoutes(app, source, deliveryProvider, handler, active, nextInvocation, limits, cors, hostFor, reservations, externalIssuer, publicOrigin) {
|
|
15
|
+
const externalOrigin = publicOrigin ?? (externalIssuer === undefined ? undefined : new URL(externalIssuer).origin);
|
|
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,
|
|
51
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
|
+
}),
|
|
52
41
|
});
|
|
53
42
|
app.all('*', async (context) => {
|
|
54
|
-
const snapshot =
|
|
43
|
+
const snapshot = served(context.req.raw);
|
|
55
44
|
if (snapshot === undefined)
|
|
56
45
|
return context.notFound();
|
|
57
46
|
const request = context.req.raw;
|
|
@@ -59,48 +48,80 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
59
48
|
return publicationPreflight(cors, request, snapshot, externalOrigin);
|
|
60
49
|
}
|
|
61
50
|
const responseCors = cors.response(request);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
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
|
+
}
|
|
65
64
|
if (request.method !== 'GET')
|
|
66
65
|
return response({ error: 'Bundle retrieval requires GET.' }, 405, responseCors);
|
|
67
66
|
const body = snapshot.bundle;
|
|
68
|
-
if (body.byteLength !==
|
|
69
|
-
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);
|
|
70
73
|
}
|
|
71
|
-
|
|
72
|
-
|
|
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,
|
|
73
78
|
headers: {
|
|
74
79
|
...responseCors,
|
|
75
|
-
'content-type':
|
|
76
|
-
etag
|
|
77
|
-
'cache-control':
|
|
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',
|
|
78
85
|
},
|
|
79
86
|
});
|
|
80
87
|
}
|
|
81
|
-
|
|
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;
|
|
82
101
|
if (deliveryUrl !== undefined && sameRoute(request.url, deliveryUrl, externalOrigin)) {
|
|
83
|
-
return handleDelivery(request, snapshot, deliveryProvider, active, limits,
|
|
102
|
+
return handleDelivery(request, snapshot, deliveryProvider, active, limits, corsFor);
|
|
84
103
|
}
|
|
85
104
|
let routing;
|
|
86
105
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
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));
|
|
98
120
|
}
|
|
99
|
-
routing = routes.decodeRouting(request, publicationReference(snapshot.publication));
|
|
100
121
|
}
|
|
101
122
|
catch (cause) {
|
|
102
123
|
if (cause instanceof routes.RouteError) {
|
|
103
|
-
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route routing is invalid.' }, 400,
|
|
124
|
+
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route routing is invalid.' }, 400, corsFor);
|
|
104
125
|
}
|
|
105
126
|
throw cause;
|
|
106
127
|
}
|
|
@@ -110,14 +131,23 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
110
131
|
const assigned = nextInvocation();
|
|
111
132
|
const operation = active.begin(limits.invocationTimeoutMs, request.signal);
|
|
112
133
|
if (operation === undefined)
|
|
113
|
-
return nativeError(
|
|
114
|
-
code: errors.CODES.BACKEND_UNAVAILABLE,
|
|
115
|
-
message: 'Server is draining.',
|
|
116
|
-
}, 503, responseCors);
|
|
134
|
+
return nativeError(admissionRefusal(active.isAccepting), 503, corsFor);
|
|
117
135
|
let handedOff = false;
|
|
118
136
|
try {
|
|
119
137
|
const body = await readBoundedBody(request, limits.maximumRequestBytes, operation.signal);
|
|
120
|
-
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);
|
|
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
|
+
}
|
|
121
151
|
const idempotencyHeader = request.headers.get(protocol.HTTP_IDEMPOTENCY_KEY);
|
|
122
152
|
const idempotencyKey = idempotencyHeader === null
|
|
123
153
|
? undefined
|
|
@@ -126,7 +156,7 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
126
156
|
call: invocation.call,
|
|
127
157
|
...(invocation.credential === undefined ? {} : { credential: invocation.credential }),
|
|
128
158
|
...(routing === undefined ? {} : { via: routing }),
|
|
129
|
-
|
|
159
|
+
...pinned(snapshot),
|
|
130
160
|
context: invocationContext(assigned, operation, active, hostFor(request), body, idempotencyKey),
|
|
131
161
|
}));
|
|
132
162
|
if (result.kind === 'value') {
|
|
@@ -138,8 +168,17 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
138
168
|
});
|
|
139
169
|
}
|
|
140
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
|
+
}
|
|
141
179
|
const applicationHeaders = applicationBinaryHeaders(result.value.headers);
|
|
142
180
|
return new Response(toBody(result.value.body), {
|
|
181
|
+
status: result.value.status ?? 200,
|
|
143
182
|
headers: {
|
|
144
183
|
...cors.response(request, [
|
|
145
184
|
protocol.http.BINARY_HEADERS_FIELD,
|
|
@@ -153,7 +192,7 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
153
192
|
}
|
|
154
193
|
if (result.kind === 'stream') {
|
|
155
194
|
handedOff = true;
|
|
156
|
-
return await streamResponse(result.stream, operation,
|
|
195
|
+
return await streamResponse(result.stream, operation, corsFor);
|
|
157
196
|
}
|
|
158
197
|
if (result.kind === 'miss') {
|
|
159
198
|
return new Response(null, {
|
|
@@ -167,29 +206,20 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
167
206
|
return nativeError({
|
|
168
207
|
code: errors.CODES.UNSUPPORTED,
|
|
169
208
|
message: 'Native routes do not forward Runtime redirects.',
|
|
170
|
-
}, 502,
|
|
209
|
+
}, 502, corsFor);
|
|
171
210
|
}
|
|
172
211
|
catch (cause) {
|
|
173
212
|
if (cause instanceof RequestBodyTooLargeError) {
|
|
174
|
-
return nativeError({ code: errors.CODES.LIMIT_EXCEEDED, message: 'Request body is too large.' }, 413,
|
|
213
|
+
return nativeError({ code: errors.CODES.LIMIT_EXCEEDED, message: 'Request body is too large.' }, 413, corsFor);
|
|
175
214
|
}
|
|
176
215
|
if (cause instanceof RequestBodyInvalidError) {
|
|
177
|
-
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Request body is invalid.' }, 400,
|
|
216
|
+
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Request body is invalid.' }, 400, corsFor);
|
|
178
217
|
}
|
|
179
218
|
if (cause instanceof routes.RouteError) {
|
|
180
|
-
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route request is invalid.' }, 400,
|
|
181
|
-
}
|
|
182
|
-
if (operation.timedOut) {
|
|
183
|
-
return nativeError({
|
|
184
|
-
code: errors.CODES.DEADLINE_EXCEEDED,
|
|
185
|
-
message: 'Invocation deadline exceeded.',
|
|
186
|
-
}, 504, responseCors);
|
|
219
|
+
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route request is invalid.' }, 400, corsFor);
|
|
187
220
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
const payload = classifyError(cause);
|
|
192
|
-
return nativeError(payload, errors.httpStatusOf(payload), responseCors);
|
|
221
|
+
const payload = classifyRequestError(cause, operation);
|
|
222
|
+
return nativeError(payload, errors.httpStatusOf(payload), corsFor);
|
|
193
223
|
}
|
|
194
224
|
finally {
|
|
195
225
|
if (!handedOff)
|
|
@@ -197,11 +227,89 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
197
227
|
}
|
|
198
228
|
});
|
|
199
229
|
}
|
|
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
|
+
}
|
|
200
307
|
function publicationPreflight(cors, request, snapshot, externalOrigin) {
|
|
201
|
-
|
|
308
|
+
const document = servedDocument(snapshot);
|
|
309
|
+
if (sameRoute(request.url, document.schema.bundle.href, externalOrigin)) {
|
|
202
310
|
return cors.preflight(request, { methods: ['GET'], headers: ['if-none-match'] });
|
|
203
311
|
}
|
|
204
|
-
const deliveryUrl =
|
|
312
|
+
const deliveryUrl = document.delivery?.href;
|
|
205
313
|
if (deliveryUrl !== undefined && sameRoute(request.url, deliveryUrl, externalOrigin)) {
|
|
206
314
|
return cors.preflight(request, {
|
|
207
315
|
methods: ['POST'],
|
|
@@ -217,7 +325,7 @@ function publicationPreflight(cors, request, snapshot, externalOrigin) {
|
|
|
217
325
|
function nativeCorsRoute(entry, requestedMethod) {
|
|
218
326
|
const headers = new Set([
|
|
219
327
|
'accept',
|
|
220
|
-
routes.
|
|
328
|
+
routes.HTTP_ROUTE_DIGEST,
|
|
221
329
|
routes.HTTP_ROUTE_ISSUER,
|
|
222
330
|
protocol.HTTP_IDEMPOTENCY_KEY,
|
|
223
331
|
]);
|
|
@@ -246,23 +354,28 @@ function nativeCorsRoute(entry, requestedMethod) {
|
|
|
246
354
|
...(allowAnyHeaders ? { allowAnyHeaders: true } : {}),
|
|
247
355
|
});
|
|
248
356
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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);
|
|
254
363
|
if (prepared === undefined ||
|
|
364
|
+
(prepared.kind === 'release' &&
|
|
365
|
+
prepared.releaseBytes.byteLength > limits.maximumPublicationBytes) ||
|
|
255
366
|
prepared.publicationBytes.byteLength > limits.maximumPublicationBytes ||
|
|
256
367
|
prepared.bundle.byteLength > limits.maximumBundleBytes ||
|
|
257
|
-
(serving?.issuer !== undefined &&
|
|
368
|
+
(serving?.issuer !== undefined &&
|
|
369
|
+
servedDocument(prepared).identity.issuer !== serving.issuer) ||
|
|
258
370
|
(serving !== undefined && conflictsWithServingRoutes(prepared, serving))) {
|
|
259
371
|
return undefined;
|
|
260
372
|
}
|
|
261
373
|
return prepared;
|
|
262
374
|
}
|
|
263
|
-
function servingSnapshot(source, limits, request, reservations, externalIssuer) {
|
|
264
|
-
return
|
|
265
|
-
origin:
|
|
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),
|
|
266
379
|
...(externalIssuer === undefined ? {} : { issuer: externalIssuer }),
|
|
267
380
|
reservations,
|
|
268
381
|
});
|
|
@@ -279,6 +392,58 @@ function conflictsWithServingRoutes(snapshot, serving) {
|
|
|
279
392
|
}
|
|
280
393
|
return false;
|
|
281
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
|
+
}
|
|
282
447
|
async function streamResponse(stream, operation, cors) {
|
|
283
448
|
const iterator = stream[Symbol.asyncIterator]();
|
|
284
449
|
let rejectSetup;
|
|
@@ -304,14 +469,14 @@ async function streamResponse(stream, operation, cors) {
|
|
|
304
469
|
const cleanup = cancelSetup('native stream setup failed');
|
|
305
470
|
if (!operation.signal.aborted)
|
|
306
471
|
await cleanup;
|
|
307
|
-
const payload =
|
|
472
|
+
const payload = classifyRequestError(cause, operation);
|
|
308
473
|
return nativeError(payload, errors.httpStatusOf(payload), cors);
|
|
309
474
|
}
|
|
310
475
|
unregisterSetupCancellation();
|
|
311
476
|
if (operation.signal.aborted) {
|
|
312
477
|
void cancelSetup('native stream setup cancelled');
|
|
313
|
-
const payload =
|
|
314
|
-
return response({ error: payload }, errors.httpStatusOf(payload), cors);
|
|
478
|
+
const payload = classifyRequestError(operation.signal.reason, operation);
|
|
479
|
+
return response({ error: payload }, errors.httpStatusOf(payload), cors());
|
|
315
480
|
}
|
|
316
481
|
const encoder = new TextEncoder();
|
|
317
482
|
let pending = first;
|
|
@@ -389,32 +554,52 @@ async function streamResponse(stream, operation, cors) {
|
|
|
389
554
|
operation.onCancel((reason) => stop(reason, interruptedStreamError(operation.timedOut)));
|
|
390
555
|
return new Response(body, {
|
|
391
556
|
headers: {
|
|
392
|
-
...cors,
|
|
557
|
+
...cors(),
|
|
393
558
|
'content-type': routes.STREAM_MEDIA_TYPE,
|
|
394
559
|
'cache-control': 'no-cache',
|
|
395
560
|
},
|
|
396
561
|
});
|
|
397
562
|
}
|
|
398
|
-
function preStreamError(cause, operation) {
|
|
399
|
-
if (operation.timedOut) {
|
|
400
|
-
return {
|
|
401
|
-
code: errors.CODES.DEADLINE_EXCEEDED,
|
|
402
|
-
message: 'Invocation deadline exceeded before the native stream began.',
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
if (operation.signal.aborted) {
|
|
406
|
-
return {
|
|
407
|
-
code: errors.CODES.CANCELLED,
|
|
408
|
-
message: 'Invocation was cancelled before the native stream began.',
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
return classifyError(cause);
|
|
412
|
-
}
|
|
413
563
|
function sameRoute(left, right, externalOrigin) {
|
|
414
564
|
const a = new URL(left);
|
|
415
565
|
const b = new URL(right);
|
|
416
566
|
return ((externalOrigin ?? a.origin) === b.origin && a.pathname === b.pathname && a.search === b.search);
|
|
417
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
|
+
}
|
|
418
603
|
function applicationBinaryHeaders(input) {
|
|
419
604
|
const output = {};
|
|
420
605
|
for (const [name, value] of Object.entries(input ?? {})) {
|
|
@@ -436,9 +621,16 @@ function response(body, status, cors) {
|
|
|
436
621
|
headers: { ...cors, 'content-type': 'application/json' },
|
|
437
622
|
});
|
|
438
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. */
|
|
439
630
|
function nativeError(error, status, cors) {
|
|
440
631
|
const admitted = routes.decodeErrorResponse({ error });
|
|
441
|
-
|
|
632
|
+
const retry = retryAfterHeaders(error);
|
|
633
|
+
return response(admitted, status, { ...cors(Object.keys(retry)), ...retry });
|
|
442
634
|
}
|
|
443
635
|
function encodedBody(input) {
|
|
444
636
|
return typeof input === 'string' ? input : toBody(input);
|
|
@@ -446,4 +638,3 @@ function encodedBody(input) {
|
|
|
446
638
|
function toBody(input) {
|
|
447
639
|
return input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
|
|
448
640
|
}
|
|
449
|
-
//# sourceMappingURL=routes.js.map
|
package/dist/server.d.ts
CHANGED
|
@@ -2,4 +2,3 @@ import type { Server, ServerConfig, ServerOptions, PreparedServer } from './type
|
|
|
2
2
|
export declare function createServer(config: ServerConfig): Server;
|
|
3
3
|
/** Validate all static transport configuration before a Host starts Runtime recovery/bootstrap. */
|
|
4
4
|
export declare function prepareServer(options: ServerOptions): PreparedServer;
|
|
5
|
-
//# sourceMappingURL=server.d.ts.map
|