@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
package/dist/server.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { accept as acceptIssuer } from '@astrale-os/kernel-core/auth/issuer';
|
|
1
2
|
import { Path } from '@astrale-os/kernel-core/path';
|
|
2
|
-
import { PATH as
|
|
3
|
+
import { PATH as RELEASE_PATH, url as releaseUrl } from '@astrale-os/kernel-protocol/release';
|
|
4
|
+
import { PATH as PUBLICATION_PATH, url as publicationUrl, } from '@astrale-os/kernel-protocol/release/legacy';
|
|
3
5
|
import { captureMethod } from './capture.js';
|
|
4
6
|
import { prepareCors } from './cors/index.js';
|
|
5
7
|
import { captureInvocationSource } from './invocation/index.js';
|
|
6
8
|
import { capture as captureExchangeProvider } from './issuer/exchange.js';
|
|
7
9
|
import { admitIssuer, issuerRouteReservations, mountIssuer } from './issuer/index.js';
|
|
8
10
|
import { ActiveOperations } from './lifecycle/index.js';
|
|
9
|
-
import {
|
|
11
|
+
import { currentServed, mountRoutes } from './routes/index.js';
|
|
10
12
|
import { createHttpHandler } from './transport/http/handler.js';
|
|
11
13
|
import { createServerRouter } from './transport/router.js';
|
|
12
14
|
import { handleWebSocketUpgrade } from './transport/websocket/upgrade.js';
|
|
@@ -29,6 +31,21 @@ function createPreparedServer(input, config) {
|
|
|
29
31
|
const externalIssuer = config.issuer?.issuer;
|
|
30
32
|
const externalOrigin = config.publicOrigin ??
|
|
31
33
|
(externalIssuer === undefined ? undefined : new URL(externalIssuer).origin);
|
|
34
|
+
const source = config.serving;
|
|
35
|
+
const serving = source === undefined
|
|
36
|
+
? undefined
|
|
37
|
+
: (request) => {
|
|
38
|
+
const origin = externalOrigin ?? new URL(request.url).origin;
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
kind: source.kind,
|
|
41
|
+
current: () => currentServed(source, config.limits, {
|
|
42
|
+
origin,
|
|
43
|
+
...(externalIssuer === undefined ? {} : { issuer: externalIssuer }),
|
|
44
|
+
reservations: config.routeReservations,
|
|
45
|
+
}),
|
|
46
|
+
deployment: () => acceptIssuer(origin),
|
|
47
|
+
});
|
|
48
|
+
};
|
|
32
49
|
const handleHttp = createHttpHandler({
|
|
33
50
|
handler,
|
|
34
51
|
active,
|
|
@@ -36,15 +53,7 @@ function createPreparedServer(input, config) {
|
|
|
36
53
|
...config.limits,
|
|
37
54
|
cors: config.cors,
|
|
38
55
|
websocket: config.websocket !== undefined,
|
|
39
|
-
...(
|
|
40
|
-
? {}
|
|
41
|
-
: {
|
|
42
|
-
currentPublication: (request) => currentPublicationSnapshot(config.publication, config.limits, {
|
|
43
|
-
origin: externalOrigin ?? new URL(request.url).origin,
|
|
44
|
-
...(externalIssuer === undefined ? {} : { issuer: externalIssuer }),
|
|
45
|
-
reservations: config.routeReservations,
|
|
46
|
-
})?.snapshot,
|
|
47
|
-
}),
|
|
56
|
+
...(serving === undefined ? {} : { serving }),
|
|
48
57
|
});
|
|
49
58
|
if (config.websocket !== undefined) {
|
|
50
59
|
app.get(config.path, (context) => handleWebSocketUpgrade({
|
|
@@ -55,15 +64,7 @@ function createPreparedServer(input, config) {
|
|
|
55
64
|
invocation: config.invocation,
|
|
56
65
|
...config.limits,
|
|
57
66
|
cors: config.cors,
|
|
58
|
-
...(
|
|
59
|
-
? {}
|
|
60
|
-
: {
|
|
61
|
-
currentPublication: () => currentPublicationSnapshot(config.publication, config.limits, {
|
|
62
|
-
origin: externalOrigin ?? new URL(context.req.raw.url).origin,
|
|
63
|
-
...(externalIssuer === undefined ? {} : { issuer: externalIssuer }),
|
|
64
|
-
reservations: config.routeReservations,
|
|
65
|
-
})?.snapshot,
|
|
66
|
-
}),
|
|
67
|
+
...(serving === undefined ? {} : { serving: serving(context.req.raw) }),
|
|
67
68
|
host: hosts.get(context.req.raw),
|
|
68
69
|
}));
|
|
69
70
|
}
|
|
@@ -83,8 +84,8 @@ function createPreparedServer(input, config) {
|
|
|
83
84
|
if (config.issuer !== undefined) {
|
|
84
85
|
mountIssuer(app, config.issuer, config.cors, active, config.limits.invocationTimeoutMs, config.tokenExchange);
|
|
85
86
|
}
|
|
86
|
-
if (config.
|
|
87
|
-
mountRoutes(app, config.
|
|
87
|
+
if (config.serving !== undefined) {
|
|
88
|
+
mountRoutes(app, config.serving, config.delivery, handler, active, config.invocation, config.limits, config.cors, (request) => hosts.get(request), config.routeReservations, externalIssuer, externalOrigin);
|
|
88
89
|
}
|
|
89
90
|
app.onError((_error, context) => new Response('{"error":"Internal server error."}', {
|
|
90
91
|
status: 500,
|
|
@@ -149,23 +150,31 @@ function admitOptions(config) {
|
|
|
149
150
|
probe: captureHealthProbe(config.health.probe),
|
|
150
151
|
});
|
|
151
152
|
const websocket = config.websocket === undefined ? undefined : captureWebSocket(config.websocket);
|
|
152
|
-
|
|
153
|
+
if (config.release !== undefined && config.publication !== undefined) {
|
|
154
|
+
throw new TypeError('Server options take at most one of release and publication.');
|
|
155
|
+
}
|
|
156
|
+
const serving = config.release !== undefined
|
|
157
|
+
? Object.freeze({ kind: 'release', source: captureReleaseSource(config.release) })
|
|
158
|
+
: config.publication !== undefined
|
|
159
|
+
? Object.freeze({
|
|
160
|
+
kind: 'publication',
|
|
161
|
+
source: capturePublicationSource(config.publication),
|
|
162
|
+
})
|
|
163
|
+
: undefined;
|
|
153
164
|
const delivery = config.delivery === undefined ? undefined : captureDeliveryProvider(config.delivery);
|
|
154
|
-
if (delivery !== undefined &&
|
|
155
|
-
throw new TypeError('
|
|
165
|
+
if (delivery !== undefined && serving === undefined) {
|
|
166
|
+
throw new TypeError('Delivery requires a release or Publication source.');
|
|
156
167
|
}
|
|
157
|
-
const
|
|
158
|
-
? undefined
|
|
159
|
-
: issuer === undefined
|
|
160
|
-
? protocolPublicationPath()
|
|
161
|
-
: protocolPublicationPath(issuer.issuer);
|
|
168
|
+
const documentPaths = serving === undefined ? [] : servedDocumentPaths(serving.kind, issuer?.issuer);
|
|
162
169
|
if (health?.path === path)
|
|
163
170
|
throw new TypeError('Health and invocation paths must be distinct.');
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
for (const { name, path: documentPath } of documentPaths) {
|
|
172
|
+
if (documentPath === path) {
|
|
173
|
+
throw new TypeError(`${name} and invocation paths must be distinct.`);
|
|
174
|
+
}
|
|
175
|
+
if (health !== undefined && documentPath === health.path) {
|
|
176
|
+
throw new TypeError(`Health and ${name} paths must be distinct.`);
|
|
177
|
+
}
|
|
169
178
|
}
|
|
170
179
|
if (issuer !== undefined) {
|
|
171
180
|
const issuerReservations = issuerRouteReservations(issuer, tokenExchange !== undefined);
|
|
@@ -190,13 +199,13 @@ function admitOptions(config) {
|
|
|
190
199
|
...(health === undefined ? {} : { health }),
|
|
191
200
|
...(issuer === undefined ? {} : { issuer }),
|
|
192
201
|
...(tokenExchange === undefined ? {} : { tokenExchange }),
|
|
193
|
-
...(
|
|
202
|
+
...(serving === undefined ? {} : { serving }),
|
|
194
203
|
...(delivery === undefined ? {} : { delivery }),
|
|
195
|
-
routeReservations: routeReservations(path, health?.path, issuer, websocket !== undefined, tokenExchange !== undefined,
|
|
204
|
+
routeReservations: routeReservations(path, health?.path, issuer, websocket !== undefined, tokenExchange !== undefined, documentPaths.map(({ path: documentPath }) => documentPath)),
|
|
196
205
|
...(websocket === undefined ? {} : { websocket }),
|
|
197
206
|
});
|
|
198
207
|
}
|
|
199
|
-
function routeReservations(invocationPath, healthPath, issuer, websocket, tokenExchange,
|
|
208
|
+
function routeReservations(invocationPath, healthPath, issuer, websocket, tokenExchange, documentPaths) {
|
|
200
209
|
const reservations = [
|
|
201
210
|
Object.freeze({
|
|
202
211
|
path: invocationPath,
|
|
@@ -209,15 +218,23 @@ function routeReservations(invocationPath, healthPath, issuer, websocket, tokenE
|
|
|
209
218
|
if (issuer !== undefined) {
|
|
210
219
|
reservations.push(...issuerRouteReservations(issuer, tokenExchange));
|
|
211
220
|
}
|
|
212
|
-
|
|
213
|
-
reservations.push(Object.freeze({ path:
|
|
221
|
+
for (const documentPath of documentPaths) {
|
|
222
|
+
reservations.push(Object.freeze({ path: documentPath, methods: Object.freeze(['GET']) }));
|
|
214
223
|
}
|
|
215
224
|
return Object.freeze(reservations);
|
|
216
225
|
}
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
function servedDocumentPaths(kind, issuer) {
|
|
227
|
+
const publication = {
|
|
228
|
+
name: 'Publication',
|
|
229
|
+
path: issuer === undefined ? PUBLICATION_PATH : new URL(publicationUrl(issuer)).pathname,
|
|
230
|
+
};
|
|
231
|
+
if (kind === 'publication')
|
|
232
|
+
return [publication];
|
|
233
|
+
const release = {
|
|
234
|
+
name: 'Release',
|
|
235
|
+
path: issuer === undefined ? RELEASE_PATH : new URL(releaseUrl(issuer)).pathname,
|
|
236
|
+
};
|
|
237
|
+
return [release, publication];
|
|
221
238
|
}
|
|
222
239
|
function captureHealthProbe(probe) {
|
|
223
240
|
const check = captureMethod(probe, 'check', 'Health probe is invalid.');
|
|
@@ -227,12 +244,16 @@ function captureWebSocket(adapter) {
|
|
|
227
244
|
const upgrade = captureMethod(adapter, 'upgrade', 'WebSocket adapter is invalid.');
|
|
228
245
|
return Object.freeze({ upgrade });
|
|
229
246
|
}
|
|
247
|
+
function captureReleaseSource(input) {
|
|
248
|
+
const current = captureMethod(input, 'current', 'Release source is invalid.');
|
|
249
|
+
return Object.freeze({ current });
|
|
250
|
+
}
|
|
230
251
|
function capturePublicationSource(input) {
|
|
231
252
|
const current = captureMethod(input, 'current', 'Publication source is invalid.');
|
|
232
253
|
return Object.freeze({ current });
|
|
233
254
|
}
|
|
234
255
|
function captureDeliveryProvider(input) {
|
|
235
|
-
const create = captureMethod(input, 'create', '
|
|
256
|
+
const create = captureMethod(input, 'create', 'Delivery provider is invalid.');
|
|
236
257
|
return Object.freeze({ create });
|
|
237
258
|
}
|
|
238
259
|
function admitLimits(limits) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import * as codecs from '@astrale-os/kernel-protocol/codecs';
|
|
2
2
|
export type LoadedCodec = codecs.Codec;
|
|
3
|
-
/**
|
|
4
|
-
export declare function loadCodecs():
|
|
3
|
+
/** The built-in Server representation set before pure Protocol binding selection. */
|
|
4
|
+
export declare function loadCodecs(): readonly [codecs.Codec];
|
package/dist/transport/codecs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as codecs from '@astrale-os/kernel-protocol/codecs';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
const SUPPORTED = Object.freeze([codecs.json]);
|
|
3
|
+
/** The built-in Server representation set before pure Protocol binding selection. */
|
|
4
|
+
export function loadCodecs() {
|
|
5
|
+
return SUPPORTED;
|
|
5
6
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
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 {
|
|
4
|
+
import type { RequestServing } from '../../release/served.js';
|
|
5
5
|
import { type ApplicationHost, type Handler } from '../../invocation/index.js';
|
|
6
6
|
export interface HttpHandlerConfig {
|
|
7
7
|
readonly handler: Handler;
|
|
@@ -17,6 +17,7 @@ export interface HttpHandlerConfig {
|
|
|
17
17
|
readonly binaryDownloadTimeoutMs?: number;
|
|
18
18
|
readonly cors: CorsPolicy;
|
|
19
19
|
readonly websocket: boolean;
|
|
20
|
-
|
|
20
|
+
/** Present exactly when the Server serves a release or a Publication. */
|
|
21
|
+
readonly serving?: (request: Request) => RequestServing;
|
|
21
22
|
}
|
|
22
23
|
export declare function createHttpHandler(config: HttpHandlerConfig): (request: Request, host?: ApplicationHost) => Promise<Response>;
|
|
@@ -2,12 +2,13 @@ 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
4
|
import { admissionRefusal, classifyError, classifyRequestError, RequestBodyInvalidError, RequestBodyTooLargeError, } from '../../errors/index.js';
|
|
5
|
-
import {
|
|
5
|
+
import { admitVia, invocationContext, } from '../../invocation/index.js';
|
|
6
|
+
import { pinned } from '../../release/served.js';
|
|
6
7
|
import { readBoundedBody } from '../body.js';
|
|
7
8
|
import { loadCodecs } from '../codecs.js';
|
|
8
9
|
import { resolveCredential } from '../credential.js';
|
|
9
10
|
import { binaryRequestBody } from './binary-request.js';
|
|
10
|
-
import { isStreamingBinary, renderHttpError, renderHttpResult, } from './render.js';
|
|
11
|
+
import { isStreamingBinary, renderDeploymentRetired, renderHttpError, renderHttpResult, } from './render.js';
|
|
11
12
|
export function createHttpHandler(config) {
|
|
12
13
|
return async (request, host) => {
|
|
13
14
|
const cors = (exposedHeaders) => config.cors.response(request, exposedHeaders);
|
|
@@ -55,15 +56,19 @@ export function createHttpHandler(config) {
|
|
|
55
56
|
let handedOff = false;
|
|
56
57
|
let binary;
|
|
57
58
|
try {
|
|
58
|
-
const supported =
|
|
59
|
+
const supported = loadCodecs();
|
|
59
60
|
const contentType = request.headers.get('content-type');
|
|
60
|
-
|
|
61
|
+
// No default representation: a body without a declared Astrale type is never decoded, so a
|
|
62
|
+
// browser cannot send an admissible invocation from another origin without a CORS preflight.
|
|
63
|
+
const incoming = binaryRequest
|
|
61
64
|
? codecs.json
|
|
62
65
|
: protocol.http.codecForContentType(contentType, supported);
|
|
63
66
|
if (incoming === undefined) {
|
|
64
67
|
return renderError(null, {
|
|
65
68
|
code: errors.CODES.UNSUPPORTED,
|
|
66
|
-
message:
|
|
69
|
+
message: contentType === null || contentType.trim() === ''
|
|
70
|
+
? 'Missing request Content-Type.'
|
|
71
|
+
: 'Unsupported request Content-Type.',
|
|
67
72
|
}, codecs.json, { status: 415, negotiated: false });
|
|
68
73
|
}
|
|
69
74
|
const outgoing = protocol.http.negotiateResponseCodec(request.headers.get('accept'), incoming, supported);
|
|
@@ -139,21 +144,19 @@ export function createHttpHandler(config) {
|
|
|
139
144
|
if (active.signal.aborted) {
|
|
140
145
|
return renderError(envelope.requestId, classifyRequestError(active.signal.reason, active), outgoing);
|
|
141
146
|
}
|
|
142
|
-
const
|
|
143
|
-
|
|
147
|
+
const serving = config.serving?.(request);
|
|
148
|
+
const served = serving?.current();
|
|
149
|
+
if (serving !== undefined && served === undefined) {
|
|
144
150
|
return renderError(envelope.requestId, {
|
|
145
151
|
code: errors.CODES.BACKEND_UNAVAILABLE,
|
|
146
|
-
message: 'Publication is unavailable
|
|
152
|
+
message: `${serving.kind === 'release' ? 'Release' : 'Publication'} is unavailable.`,
|
|
147
153
|
}, outgoing, { status: 503 });
|
|
148
154
|
}
|
|
149
155
|
if (envelope.routing?.kind === 'via') {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
message: 'Publication routing requires a Publication-backed destination.',
|
|
154
|
-
}, outgoing);
|
|
156
|
+
const decision = admitVia(served, envelope.routing);
|
|
157
|
+
if (decision.kind === 'retired') {
|
|
158
|
+
return renderDeploymentRetired(serving.deployment(), decision.release, cors);
|
|
155
159
|
}
|
|
156
|
-
const decision = publicationRouteDecision(publication.publication, envelope.routing.publication);
|
|
157
160
|
if (decision.kind === 'rejected') {
|
|
158
161
|
return renderError(envelope.requestId, decision.error, outgoing);
|
|
159
162
|
}
|
|
@@ -171,7 +174,7 @@ export function createHttpHandler(config) {
|
|
|
171
174
|
...(envelope.delegate === undefined ? {} : { delegate: envelope.delegate }),
|
|
172
175
|
...(envelope.routing?.kind === 'via' ? { via: envelope.routing } : {}),
|
|
173
176
|
...(envelope.schema === undefined ? {} : { schema: envelope.schema }),
|
|
174
|
-
...(
|
|
177
|
+
...(served === undefined ? {} : pinned(served)),
|
|
175
178
|
context: invocationContext(assigned, active, config.active, host, undefined, envelope.idempotencyKey, binary),
|
|
176
179
|
}));
|
|
177
180
|
if (binary !== undefined && result.kind !== 'redirect' && result.kind !== 'miss') {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
1
2
|
import type { BufferedBinaryResult, StreamingBinaryResult } from '@astrale-os/kernel-core/callable';
|
|
2
3
|
import type { Codec } from '@astrale-os/kernel-protocol/codecs';
|
|
4
|
+
import type { ReleaseDigest } from '@astrale-os/kernel-protocol/release';
|
|
3
5
|
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
4
6
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
5
7
|
import type { Handler } from '../../invocation/index.js';
|
|
@@ -26,6 +28,8 @@ export declare function renderRedirect(requestId: protocol.RequestId, invocation
|
|
|
26
28
|
}>['redirect']): protocol.RedirectResponse;
|
|
27
29
|
export declare function renderHttpError(requestId: protocol.RequestId, invocation: protocol.InvocationId, error: errors.ErrorPayload, codec: Codec, cors: CorsHeaders, options?: ErrorRenderOptions): Response;
|
|
28
30
|
export declare function renderHttpStale(requestId: protocol.RequestId, invocation: protocol.InvocationId, current: protocol.Stale['current'], codec: Codec, cors: CorsHeaders, maximumResponseBytes?: number): Response;
|
|
31
|
+
/** Retirement controls always use JSON, regardless of the negotiated request codec. */
|
|
32
|
+
export declare function renderDeploymentRetired(deployment: IssuerId, release: ReleaseDigest | undefined, cors: CorsHeaders): Response;
|
|
29
33
|
export declare function encodeEnvelope(codec: Codec, input: unknown): string | Uint8Array;
|
|
30
34
|
export declare function isStreamingBinary(input: Extract<HandlerResult, {
|
|
31
35
|
readonly kind: 'binary';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { json } from '@astrale-os/kernel-protocol/codecs';
|
|
1
2
|
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
2
3
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
4
|
+
import { deploymentRetiredBody, deploymentRetiredHeaders, HTTP_DEPLOYMENT_RETIRED, HTTP_DEPLOYMENT_RETIRED_STATUS, } from '@astrale-os/kernel-protocol/routes/deployment-retired';
|
|
3
5
|
import { HTTP_ROUTE_MISS, HTTP_ROUTE_STALE } from '@astrale-os/kernel-protocol/routes/routing';
|
|
4
6
|
import { classifyError, retryAfterHeaders } from '../../errors/index.js';
|
|
5
7
|
import { ResponseBodyTooLargeError } from '../../errors/index.js';
|
|
@@ -64,6 +66,13 @@ export function renderHttpError(requestId, invocation, error, codec, cors, optio
|
|
|
64
66
|
export function renderHttpStale(requestId, invocation, current, codec, cors, maximumResponseBytes) {
|
|
65
67
|
return envelopeResponse(codec, protocol.stale(requestId, invocation, current), cors(), 200, true, maximumResponseBytes);
|
|
66
68
|
}
|
|
69
|
+
/** Retirement controls always use JSON, regardless of the negotiated request codec. */
|
|
70
|
+
export function renderDeploymentRetired(deployment, release, cors) {
|
|
71
|
+
return new Response(toBody(json.encode(deploymentRetiredBody(deployment, release))), {
|
|
72
|
+
status: HTTP_DEPLOYMENT_RETIRED_STATUS,
|
|
73
|
+
headers: { ...cors([HTTP_DEPLOYMENT_RETIRED]), ...deploymentRetiredHeaders() },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
67
76
|
export function encodeEnvelope(codec, input) {
|
|
68
77
|
return codec.encode(input);
|
|
69
78
|
}
|
|
@@ -111,9 +120,8 @@ function streamingBinaryHttpResponse(binary, context) {
|
|
|
111
120
|
// delivered bytes against this protocol field instead.
|
|
112
121
|
headers.set(protocol.http.BINARY_LENGTH_FIELD, String(binary.length));
|
|
113
122
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
headers.set('cache-control', delivery === undefined ? 'no-store, no-transform' : 'private, no-store, no-transform');
|
|
123
|
+
if (delivery !== undefined)
|
|
124
|
+
headers.set('cache-control', 'private, no-store');
|
|
117
125
|
let terminal = false;
|
|
118
126
|
let delivered = 0;
|
|
119
127
|
let controller;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Codec } from '@astrale-os/kernel-protocol/codecs';
|
|
2
2
|
import type { NextInvocationId } from '../../invocation/index.js';
|
|
3
3
|
import type { ActiveOperations } from '../../lifecycle/index.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { RequestServing } from '../../release/served.js';
|
|
5
5
|
import type { WebSocketHandlers } from '../types.js';
|
|
6
6
|
import { type ApplicationHost, type Handler } from '../../invocation/index.js';
|
|
7
7
|
export interface WebSocketSessionConfig {
|
|
@@ -13,7 +13,8 @@ export interface WebSocketSessionConfig {
|
|
|
13
13
|
readonly maximumRequestBytes: number;
|
|
14
14
|
readonly maximumResponseBytes: number;
|
|
15
15
|
readonly invocationTimeoutMs: number;
|
|
16
|
-
|
|
16
|
+
/** Present exactly when the Server serves a release or a Publication; bound to the upgrade. */
|
|
17
|
+
readonly serving?: RequestServing;
|
|
17
18
|
readonly host?: ApplicationHost;
|
|
18
19
|
}
|
|
19
20
|
/** Build one socket-pinned transport session. */
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
2
2
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
3
|
+
import { deploymentRetiredBody } from '@astrale-os/kernel-protocol/routes/deployment-retired';
|
|
3
4
|
import { admissionRefusal, classifyError, classifyRequestError } from '../../errors/index.js';
|
|
4
5
|
import { ResponseBodyTooLargeError } from '../../errors/index.js';
|
|
5
|
-
import {
|
|
6
|
+
import { admitVia, invocationContext, } from '../../invocation/index.js';
|
|
7
|
+
import { pinned } from '../../release/served.js';
|
|
6
8
|
import { byteLength } from '../body.js';
|
|
7
9
|
import { resolveCredential } from '../credential.js';
|
|
8
10
|
import { isStreamingBinary, renderBinary, renderRedirect } from '../http/render.js';
|
|
@@ -100,25 +102,23 @@ export function createWebSocketSession(config) {
|
|
|
100
102
|
}, sendOutput);
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
+
const serving = config.serving;
|
|
106
|
+
const served = serving?.current();
|
|
107
|
+
if (serving !== undefined && served === undefined) {
|
|
105
108
|
await sendError(socket, admittedCodec, envelope.requestId, assigned, {
|
|
106
109
|
code: errors.CODES.BACKEND_UNAVAILABLE,
|
|
107
|
-
message: 'Publication is unavailable
|
|
110
|
+
message: `${serving.kind === 'release' ? 'Release' : 'Publication'} is unavailable.`,
|
|
108
111
|
}, sendOutput);
|
|
109
112
|
return;
|
|
110
113
|
}
|
|
111
114
|
if (envelope.routing?.kind === 'via') {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const decision = publicationRouteDecision(publication.publication, envelope.routing.publication);
|
|
120
|
-
if (decision.kind === 'rejected') {
|
|
121
|
-
await sendError(socket, admittedCodec, envelope.requestId, assigned, decision.error, sendOutput);
|
|
115
|
+
const decision = admitVia(served, envelope.routing);
|
|
116
|
+
if (decision.kind !== 'admitted') {
|
|
117
|
+
// A socket has no HTTP status: a retired deployment answers this request with the
|
|
118
|
+
// DEPLOYMENT_RETIRED error payload of the reserved control, correlated to its request.
|
|
119
|
+
await sendError(socket, admittedCodec, envelope.requestId, assigned, decision.kind === 'retired'
|
|
120
|
+
? deploymentRetiredBody(serving.deployment(), decision.release).error
|
|
121
|
+
: decision.error, sendOutput);
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
124
|
}
|
|
@@ -142,7 +142,7 @@ export function createWebSocketSession(config) {
|
|
|
142
142
|
...(envelope.delegate === undefined ? {} : { delegate: envelope.delegate }),
|
|
143
143
|
...(envelope.routing?.kind === 'via' ? { via: envelope.routing } : {}),
|
|
144
144
|
...(envelope.schema === undefined ? {} : { schema: envelope.schema }),
|
|
145
|
-
...(
|
|
145
|
+
...(served === undefined ? {} : pinned(served)),
|
|
146
146
|
context: invocationContext(assigned, operation, config.active, config.host, undefined, envelope.idempotencyKey),
|
|
147
147
|
}));
|
|
148
148
|
await sendResult(socket, admittedCodec, envelope.requestId, assigned, result, operation, sendOutput);
|
|
@@ -2,7 +2,7 @@ import type { CorsPolicy } from '../../cors/index.js';
|
|
|
2
2
|
import type { ApplicationHost, Handler } from '../../invocation/index.js';
|
|
3
3
|
import type { NextInvocationId } from '../../invocation/index.js';
|
|
4
4
|
import type { ActiveOperations } from '../../lifecycle/index.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type { RequestServing } from '../../release/served.js';
|
|
6
6
|
import type { WebSocketAdapter } from '../types.js';
|
|
7
7
|
export interface WebSocketHandlerConfig {
|
|
8
8
|
readonly request: Request;
|
|
@@ -14,7 +14,7 @@ export interface WebSocketHandlerConfig {
|
|
|
14
14
|
readonly maximumResponseBytes: number;
|
|
15
15
|
readonly invocationTimeoutMs: number;
|
|
16
16
|
readonly cors: CorsPolicy;
|
|
17
|
-
readonly
|
|
17
|
+
readonly serving?: RequestServing;
|
|
18
18
|
readonly host?: ApplicationHost;
|
|
19
19
|
}
|
|
20
20
|
export declare function handleWebSocketUpgrade(config: WebSocketHandlerConfig): Promise<Response>;
|
|
@@ -11,7 +11,7 @@ export async function handleWebSocketUpgrade(config) {
|
|
|
11
11
|
return failure(503, { ...config.cors.response(config.request, Object.keys(retry)), ...retry });
|
|
12
12
|
}
|
|
13
13
|
try {
|
|
14
|
-
const supported =
|
|
14
|
+
const supported = loadCodecs();
|
|
15
15
|
const selection = protocol.websocket.selectSubprotocol(config.request.headers.get('sec-websocket-protocol'), supported);
|
|
16
16
|
if (selection === undefined)
|
|
17
17
|
return failure(400, config.cors.response(config.request));
|
|
@@ -26,9 +26,7 @@ export async function handleWebSocketUpgrade(config) {
|
|
|
26
26
|
maximumRequestBytes: config.maximumRequestBytes,
|
|
27
27
|
maximumResponseBytes: config.maximumResponseBytes,
|
|
28
28
|
invocationTimeoutMs: config.invocationTimeoutMs,
|
|
29
|
-
...(config.
|
|
30
|
-
? {}
|
|
31
|
-
: { currentPublication: config.currentPublication }),
|
|
29
|
+
...(config.serving === undefined ? {} : { serving: config.serving }),
|
|
32
30
|
...(config.host === undefined ? {} : { host: config.host }),
|
|
33
31
|
}),
|
|
34
32
|
signal: operation.signal,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { CorsConfig } from './cors/index.js';
|
|
2
2
|
import type { Handler, InvocationSource } from './invocation/index.js';
|
|
3
3
|
import type { exchange, Issuer } from './issuer/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 { ReleaseSource } from './release/index.js';
|
|
6
|
+
import type { PublicationSource } from './release/legacy/index.js';
|
|
6
7
|
import type { Application, WebSocketAdapter } from './transport/index.js';
|
|
7
8
|
export interface ServerLimits {
|
|
8
9
|
readonly maximumRequestBytes: number;
|
|
@@ -12,6 +13,7 @@ export interface ServerLimits {
|
|
|
12
13
|
readonly maximumResponseBytes?: number;
|
|
13
14
|
/** Maximum Server-owned active operations across all mounted protocol surfaces. */
|
|
14
15
|
readonly maximumConcurrentOperations?: number;
|
|
16
|
+
/** Maximum bytes of each served discovery document (`release.json` and `domain.json`). */
|
|
15
17
|
readonly maximumPublicationBytes?: number;
|
|
16
18
|
readonly maximumBundleBytes?: number;
|
|
17
19
|
readonly maximumDeliveryBytes?: number;
|
|
@@ -51,8 +53,19 @@ export interface ServerOptions {
|
|
|
51
53
|
readonly publicOrigin?: string;
|
|
52
54
|
/** Present only for the configured self issuer; owns verification and signing. */
|
|
53
55
|
readonly tokenExchange?: exchange.Provider;
|
|
56
|
+
/**
|
|
57
|
+
* The release this deployment serves: `release.json`, its frozen v3 projection at `domain.json`,
|
|
58
|
+
* its bundle, Delivery v1 and v2, and its native routes; routed calls are admitted only for this
|
|
59
|
+
* exact release. At most one of `release` and `publication`; with neither, the Server serves no
|
|
60
|
+
* document and admits no routed call.
|
|
61
|
+
*/
|
|
62
|
+
readonly release?: ReleaseSource;
|
|
63
|
+
/** @deprecated Use `release`. Publication sources allow ETag and Schema drift when routing. */
|
|
54
64
|
readonly publication?: PublicationSource;
|
|
55
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Required exactly when the current release or Publication advertises delivery. With `release`
|
|
67
|
+
* it must answer Delivery v2 and v1; a v1-only provider fits only `publication`.
|
|
68
|
+
*/
|
|
56
69
|
readonly delivery?: DeliveryProvider;
|
|
57
70
|
readonly websocket?: WebSocketAdapter;
|
|
58
71
|
}
|
|
@@ -73,5 +86,7 @@ export type { CorsConfig } from './cors/index.js';
|
|
|
73
86
|
export type { InvocationSource } from './invocation/index.js';
|
|
74
87
|
export type { ApplicationHost, ExecutionControl, Handler, HandlerRequest, HandlerResult, HandlerStream, InvocationContext, RequestBackground, RequestEvidence, RequestSleep, } from './invocation/index.js';
|
|
75
88
|
export type { exchange, Issuer } from './issuer/index.js';
|
|
76
|
-
export type {
|
|
89
|
+
export type { Provider as DeliveryProvider } from './release/delivery/index.js';
|
|
90
|
+
export type { ReleaseSnapshot, ReleaseSnapshotInput, ReleaseSource } from './release/index.js';
|
|
91
|
+
export type { PublicationSnapshot, PublicationSnapshotInput, PublicationSource, } from './release/legacy/index.js';
|
|
77
92
|
export type { Application, WebSocketAdapter, WebSocketHandlers, WebSocketLike, WebSocketUpgradeOptions, } from './transport/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/kernel-server",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.70",
|
|
4
4
|
"description": "Portable HTTP and WebSocket boundary for Astrale invocation, publication, installation, and issuer protocols",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"import": "./dist/index.js"
|
|
21
21
|
},
|
|
22
22
|
"./publication/snapshot": {
|
|
23
|
-
"types": "./dist/
|
|
24
|
-
"import": "./dist/
|
|
23
|
+
"types": "./dist/release/legacy/snapshot.d.ts",
|
|
24
|
+
"import": "./dist/release/legacy/snapshot.js"
|
|
25
25
|
},
|
|
26
26
|
"./server": {
|
|
27
27
|
"types": "./dist/server.d.ts",
|
|
@@ -33,11 +33,10 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@astrale-os/kernel-core": "0.9.0-beta.
|
|
37
|
-
"@astrale-os/kernel-dsl": "0.2.0-beta.
|
|
38
|
-
"@astrale-os/kernel-protocol": "0.5.0-beta.
|
|
39
|
-
"hono": "^4.6.20"
|
|
40
|
-
"msgpackr": "^2.0.5"
|
|
36
|
+
"@astrale-os/kernel-core": "0.9.0-beta.62",
|
|
37
|
+
"@astrale-os/kernel-dsl": "0.2.0-beta.44",
|
|
38
|
+
"@astrale-os/kernel-protocol": "0.5.0-beta.68",
|
|
39
|
+
"hono": "^4.6.20"
|
|
41
40
|
},
|
|
42
41
|
"devDependencies": {
|
|
43
42
|
"@astrale/typescript-config": "npm:@jsr/astrale__typescript-config@~1.1.0",
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { ActiveOperations } from '../../lifecycle/index.js';
|
|
2
|
-
import type { PreparedPublicationSnapshot } from '../snapshot.js';
|
|
3
|
-
import type { Provider } from './types.js';
|
|
4
|
-
export interface Limits {
|
|
5
|
-
readonly maximumDeliveryBytes: number;
|
|
6
|
-
readonly invocationTimeoutMs: number;
|
|
7
|
-
}
|
|
8
|
-
/** Serve one delivery request against the exact Publication snapshot that selected its URL. */
|
|
9
|
-
export declare function handleDelivery(request: Request, snapshot: PreparedPublicationSnapshot, provider: Provider | undefined, active: ActiveOperations, limits: Limits, corsFor: (exposedHeaders?: readonly string[]) => Readonly<Record<string, string>>): Promise<Response>;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type * as publication from '@astrale-os/kernel-protocol/publication';
|
|
2
|
-
export interface Context {
|
|
3
|
-
/** Omitted for a public endpoint; otherwise the exact admitted retrieval credential. */
|
|
4
|
-
readonly token?: publication.delivery.Token;
|
|
5
|
-
readonly signal: AbortSignal;
|
|
6
|
-
}
|
|
7
|
-
/** Provider-owned retrieval, construction, and attestation signing. */
|
|
8
|
-
export interface Provider {
|
|
9
|
-
/** Throw DeliveryUnavailableError only for an expected dependency outage. */
|
|
10
|
-
create(request: publication.delivery.Request, context: Context): Promise<publication.delivery.Delivery>;
|
|
11
|
-
}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
export { preparePublicationSnapshot, readPreparedPublicationSnapshot, type PreparedPublicationSnapshot, } from './snapshot.js';
|
|
2
|
-
export type { PublicationSnapshot, PublicationSnapshotInput, PublicationSource } from './types.js';
|
|
3
|
-
export * as delivery from './delivery/index.js';
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { Domain } from '@astrale-os/kernel-dsl/v1';
|
|
2
|
-
import type * as publication from '@astrale-os/kernel-protocol/publication';
|
|
3
|
-
import type * as routes from '@astrale-os/kernel-protocol/routes';
|
|
4
|
-
export declare function admitTable(deployed: publication.Publication, input: routes.Table | undefined, domain: Domain): routes.Table;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|