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