@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.
Files changed (84) hide show
  1. package/dist/index.d.ts +1 -6
  2. package/dist/index.js +0 -5
  3. package/dist/invocation/index.d.ts +1 -1
  4. package/dist/invocation/index.js +1 -1
  5. package/dist/invocation/release-route.d.ts +3 -7
  6. package/dist/invocation/release-route.js +4 -27
  7. package/dist/invocation/types.d.ts +2 -14
  8. package/dist/issuer/routes.js +28 -47
  9. package/dist/release/delivery/handler.d.ts +1 -5
  10. package/dist/release/delivery/handler.js +8 -29
  11. package/dist/release/delivery/index.d.ts +0 -1
  12. package/dist/release/delivery/index.js +1 -1
  13. package/dist/release/delivery/types.d.ts +3 -13
  14. package/dist/release/served.d.ts +6 -15
  15. package/dist/release/served.js +3 -7
  16. package/dist/release/snapshot.d.ts +2 -6
  17. package/dist/release/snapshot.js +2 -7
  18. package/dist/release/types.d.ts +1 -11
  19. package/dist/routes/address.d.ts +2 -0
  20. package/dist/routes/address.js +6 -0
  21. package/dist/routes/binary.d.ts +9 -0
  22. package/dist/routes/binary.js +110 -0
  23. package/dist/routes/bundle.d.ts +5 -0
  24. package/dist/routes/bundle.js +50 -0
  25. package/dist/routes/csrf.d.ts +16 -0
  26. package/dist/routes/csrf.js +35 -0
  27. package/dist/routes/document.d.ts +6 -0
  28. package/dist/routes/document.js +57 -0
  29. package/dist/routes/index.d.ts +3 -1
  30. package/dist/routes/index.js +2 -1
  31. package/dist/routes/model.d.ts +15 -0
  32. package/dist/routes/native.d.ts +17 -0
  33. package/dist/routes/native.js +126 -0
  34. package/dist/routes/preflight.d.ts +4 -0
  35. package/dist/routes/preflight.js +55 -0
  36. package/dist/routes/response.d.ts +14 -0
  37. package/dist/routes/response.js +33 -0
  38. package/dist/routes/routes.d.ts +5 -24
  39. package/dist/routes/routes.js +19 -625
  40. package/dist/routes/snapshot.d.ts +13 -0
  41. package/dist/routes/snapshot.js +39 -0
  42. package/dist/routes/stream.d.ts +6 -0
  43. package/dist/routes/stream.js +122 -0
  44. package/dist/server.js +12 -30
  45. package/dist/transport/http/binary.d.ts +6 -0
  46. package/dist/transport/http/binary.js +35 -0
  47. package/dist/transport/http/byte-stream.d.ts +4 -0
  48. package/dist/transport/http/byte-stream.js +123 -0
  49. package/dist/transport/http/control.d.ts +12 -0
  50. package/dist/transport/http/control.js +24 -0
  51. package/dist/transport/http/handler.d.ts +1 -1
  52. package/dist/transport/http/handler.js +4 -2
  53. package/dist/transport/http/index.d.ts +3 -1
  54. package/dist/transport/http/index.js +2 -1
  55. package/dist/transport/http/model.d.ts +20 -0
  56. package/dist/transport/http/model.js +1 -0
  57. package/dist/transport/http/render.d.ts +3 -36
  58. package/dist/transport/http/render.js +16 -412
  59. package/dist/transport/http/response.d.ts +14 -0
  60. package/dist/transport/http/response.js +77 -0
  61. package/dist/transport/http/stream.d.ts +5 -0
  62. package/dist/transport/http/stream.js +134 -0
  63. package/dist/transport/index.d.ts +2 -1
  64. package/dist/transport/index.js +2 -1
  65. package/dist/transport/result.d.ts +13 -0
  66. package/dist/transport/result.js +35 -0
  67. package/dist/transport/websocket/session.d.ts +1 -1
  68. package/dist/transport/websocket/session.js +3 -3
  69. package/dist/types.d.ts +13 -16
  70. package/package.json +5 -13
  71. package/dist/invocation/legacy/index.d.ts +0 -20
  72. package/dist/invocation/legacy/index.js +0 -20
  73. package/dist/invocation/legacy/publication-route.d.ts +0 -16
  74. package/dist/invocation/legacy/publication-route.js +0 -39
  75. package/dist/release/legacy/delivery.d.ts +0 -18
  76. package/dist/release/legacy/delivery.js +0 -2
  77. package/dist/release/legacy/index.d.ts +0 -39
  78. package/dist/release/legacy/index.js +0 -38
  79. package/dist/release/legacy/snapshot.d.ts +0 -26
  80. package/dist/release/legacy/snapshot.js +0 -55
  81. package/dist/release/legacy/table.d.ts +0 -5
  82. package/dist/release/legacy/table.js +0 -16
  83. package/dist/release/legacy/types.d.ts +0 -31
  84. /package/dist/{release/legacy/types.js → routes/model.js} +0 -0
@@ -0,0 +1,39 @@
1
+ import * as routes from '@astrale-os/kernel-protocol/routes';
2
+ import { readServed, servedDocument } from '../release/served.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 function currentServed(source, limits, serving) {
8
+ const prepared = readServed(source);
9
+ if (prepared === undefined ||
10
+ prepared.releaseBytes.byteLength > limits.maximumReleaseBytes ||
11
+ prepared.bundle.byteLength > limits.maximumBundleBytes ||
12
+ (serving?.issuer !== undefined &&
13
+ servedDocument(prepared).identity.issuer !== serving.issuer) ||
14
+ (serving !== undefined && conflictsWithServingRoutes(prepared, serving))) {
15
+ return undefined;
16
+ }
17
+ return prepared;
18
+ }
19
+ /** Admit the one request snapshot against its trusted origin and reserved routes. */
20
+ export function servingSnapshot(source, limits, request, reservations, externalIssuer, publicOrigin) {
21
+ return currentServed(source, limits, {
22
+ origin: publicOrigin ??
23
+ (externalIssuer === undefined ? new URL(request.url).origin : new URL(externalIssuer).origin),
24
+ ...(externalIssuer === undefined ? {} : { issuer: externalIssuer }),
25
+ reservations,
26
+ });
27
+ }
28
+ function conflictsWithServingRoutes(snapshot, serving) {
29
+ for (const reservation of serving.reservations) {
30
+ const href = `${serving.origin}${reservation.path}`;
31
+ for (const entry of snapshot.table.entries) {
32
+ if (reservation.methods.some((method) => entry.method === '*' || entry.method === method) &&
33
+ routes.templatesOverlap(entry.href, href)) {
34
+ return true;
35
+ }
36
+ }
37
+ }
38
+ return false;
39
+ }
@@ -0,0 +1,6 @@
1
+ import type { ActiveOperation } from '../lifecycle/index.js';
2
+ import type { CorsHeaders } from './model.js';
3
+ /** Render native item frames while retaining cancellation and producer cleanup. */
4
+ export declare function streamResponse(stream: AsyncIterable<unknown> & {
5
+ cancel(reason?: string): Promise<void>;
6
+ }, operation: ActiveOperation, cors: CorsHeaders): Promise<Response>;
@@ -0,0 +1,122 @@
1
+ import * as errors from '@astrale-os/kernel-protocol/errors';
2
+ import * as routes from '@astrale-os/kernel-protocol/routes';
3
+ import { classifyError, classifyRequestError } from '../errors/index.js';
4
+ import { interruptedStreamError } from '../transport/stream-error.js';
5
+ import { nativeError, response } from './response.js';
6
+ /** Render native item frames while retaining cancellation and producer cleanup. */
7
+ export async function streamResponse(stream, operation, cors) {
8
+ const iterator = stream[Symbol.asyncIterator]();
9
+ let rejectSetup;
10
+ const setupInterrupted = new Promise((_resolve, reject) => {
11
+ rejectSetup = reject;
12
+ });
13
+ let setupCleanup;
14
+ const cancelSetup = (reason) => {
15
+ rejectSetup(operation.signal.reason ?? new Error(reason));
16
+ setupCleanup ??= Promise.allSettled([
17
+ Promise.resolve().then(() => stream.cancel(reason)),
18
+ Promise.resolve().then(() => iterator.return?.()),
19
+ ]).then(() => operation.settle());
20
+ return setupCleanup;
21
+ };
22
+ const unregisterSetupCancellation = operation.onCancel(cancelSetup);
23
+ let first;
24
+ try {
25
+ first = await Promise.race([iterator.next(), setupInterrupted]);
26
+ }
27
+ catch (cause) {
28
+ unregisterSetupCancellation();
29
+ const cleanup = cancelSetup('native stream setup failed');
30
+ if (!operation.signal.aborted)
31
+ await cleanup;
32
+ const payload = classifyRequestError(cause, operation);
33
+ return nativeError(payload, errors.httpStatusOf(payload), cors);
34
+ }
35
+ unregisterSetupCancellation();
36
+ if (operation.signal.aborted) {
37
+ void cancelSetup('native stream setup cancelled');
38
+ const payload = classifyRequestError(operation.signal.reason, operation);
39
+ return response({ error: payload }, errors.httpStatusOf(payload), cors());
40
+ }
41
+ const encoder = new TextEncoder();
42
+ let pending = first;
43
+ let terminal = false;
44
+ let controller;
45
+ let stopping;
46
+ const encoded = (frame) => encoder.encode(routes.encodeStreamFrame(frame));
47
+ const stop = (reason, error) => {
48
+ stopping ??= (async () => {
49
+ if (terminal)
50
+ return;
51
+ terminal = true;
52
+ if (controller !== undefined) {
53
+ if (error !== undefined) {
54
+ try {
55
+ controller.enqueue(encoded({ kind: 'error', error }));
56
+ }
57
+ catch {
58
+ // The transport consumer may already have disconnected.
59
+ }
60
+ }
61
+ try {
62
+ controller.close();
63
+ }
64
+ catch {
65
+ // A cancelled response body is already terminal.
66
+ }
67
+ }
68
+ try {
69
+ await Promise.allSettled([
70
+ Promise.resolve().then(() => stream.cancel(reason)),
71
+ Promise.resolve().then(() => iterator.return?.()),
72
+ ]);
73
+ }
74
+ finally {
75
+ operation.settle();
76
+ }
77
+ })();
78
+ return stopping;
79
+ };
80
+ const body = new ReadableStream({
81
+ start(output) {
82
+ controller = output;
83
+ },
84
+ async pull(output) {
85
+ if (terminal)
86
+ return;
87
+ try {
88
+ const next = pending ?? (await iterator.next());
89
+ pending = undefined;
90
+ if (terminal)
91
+ return;
92
+ if (next.done) {
93
+ terminal = true;
94
+ output.enqueue(encoded({ kind: 'end' }));
95
+ output.close();
96
+ operation.settle();
97
+ }
98
+ else {
99
+ output.enqueue(encoded({ kind: 'item', value: next.value }));
100
+ }
101
+ }
102
+ catch (cause) {
103
+ await stop('native stream failed', operation.signal.aborted
104
+ ? interruptedStreamError(operation.timedOut)
105
+ : classifyError(cause));
106
+ }
107
+ },
108
+ async cancel(reason) {
109
+ const message = typeof reason === 'string' ? reason : 'native response cancelled';
110
+ operation.cancel(message);
111
+ await stop(message);
112
+ },
113
+ });
114
+ operation.onCancel((reason) => stop(reason, interruptedStreamError(operation.timedOut)));
115
+ return new Response(body, {
116
+ headers: {
117
+ ...cors(),
118
+ 'content-type': routes.STREAM_MEDIA_TYPE,
119
+ 'cache-control': 'no-cache',
120
+ },
121
+ });
122
+ }
package/dist/server.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { accept as acceptIssuer } from '@astrale-os/kernel-core/auth/issuer';
2
2
  import { Path } from '@astrale-os/kernel-core/path';
3
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';
5
4
  import { captureMethod } from './capture.js';
6
5
  import { prepareCors } from './cors/index.js';
7
6
  import { captureInvocationSource } from './invocation/index.js';
@@ -150,20 +149,12 @@ function admitOptions(config) {
150
149
  probe: captureHealthProbe(config.health.probe),
151
150
  });
152
151
  const websocket = config.websocket === undefined ? undefined : captureWebSocket(config.websocket);
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;
152
+ const serving = config.release === undefined
153
+ ? undefined
154
+ : Object.freeze({ kind: 'release', source: captureReleaseSource(config.release) });
164
155
  const delivery = config.delivery === undefined ? undefined : captureDeliveryProvider(config.delivery);
165
156
  if (delivery !== undefined && serving === undefined) {
166
- throw new TypeError('Delivery requires a release or Publication source.');
157
+ throw new TypeError('Delivery requires a release source.');
167
158
  }
168
159
  const documentPaths = serving === undefined ? [] : servedDocumentPaths(serving.kind, issuer?.issuer);
169
160
  if (health?.path === path)
@@ -223,18 +214,13 @@ function routeReservations(invocationPath, healthPath, issuer, websocket, tokenE
223
214
  }
224
215
  return Object.freeze(reservations);
225
216
  }
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];
217
+ function servedDocumentPaths(_kind, issuer) {
218
+ return [
219
+ {
220
+ name: 'Release',
221
+ path: issuer === undefined ? RELEASE_PATH : new URL(releaseUrl(issuer)).pathname,
222
+ },
223
+ ];
238
224
  }
239
225
  function captureHealthProbe(probe) {
240
226
  const check = captureMethod(probe, 'check', 'Health probe is invalid.');
@@ -248,10 +234,6 @@ function captureReleaseSource(input) {
248
234
  const current = captureMethod(input, 'current', 'Release source is invalid.');
249
235
  return Object.freeze({ current });
250
236
  }
251
- function capturePublicationSource(input) {
252
- const current = captureMethod(input, 'current', 'Publication source is invalid.');
253
- return Object.freeze({ current });
254
- }
255
237
  function captureDeliveryProvider(input) {
256
238
  const create = captureMethod(input, 'create', 'Delivery provider is invalid.');
257
239
  return Object.freeze({ create });
@@ -281,7 +263,7 @@ function admitLimits(limits) {
281
263
  maximumBinaryRequestBytes: admitOptionalLimit(limits.maximumBinaryRequestBytes, limits.maximumRequestBytes, 'maximumBinaryRequestBytes'),
282
264
  maximumResponseBytes: admitOptionalLimit(limits.maximumResponseBytes, 16 * 1024 * 1024, 'maximumResponseBytes'),
283
265
  maximumConcurrentOperations: admitOptionalLimit(limits.maximumConcurrentOperations, 1_024, 'maximumConcurrentOperations'),
284
- maximumPublicationBytes: admitOptionalLimit(limits.maximumPublicationBytes, limits.maximumRequestBytes, 'maximumPublicationBytes'),
266
+ maximumReleaseBytes: admitOptionalLimit(limits.maximumReleaseBytes, limits.maximumRequestBytes, 'maximumReleaseBytes'),
285
267
  maximumBundleBytes: admitOptionalLimit(limits.maximumBundleBytes, 16 * 1024 * 1024, 'maximumBundleBytes'),
286
268
  maximumDeliveryBytes: admitOptionalLimit(limits.maximumDeliveryBytes, limits.maximumRequestBytes, 'maximumDeliveryBytes'),
287
269
  invocationTimeoutMs: limits.invocationTimeoutMs,
@@ -0,0 +1,6 @@
1
+ import * as protocol from '@astrale-os/kernel-protocol/invocation';
2
+ import type { CorsHeaders } from './model.js';
3
+ /** Present admitted buffered bytes under the response budget and binary manifest. */
4
+ export declare function binaryHttpResponse(binary: protocol.BinaryResponse, cors: CorsHeaders, maximumResponseBytes: number): Response;
5
+ /** Bind binary correlation and application headers to the HTTP response. */
6
+ export declare function binaryHttpHeaders(binary: Pick<protocol.BinaryResponse, 'headers' | 'invocation' | 'mediaType' | 'requestId'>, cors: CorsHeaders, streaming: boolean): Headers;
@@ -0,0 +1,35 @@
1
+ import * as protocol from '@astrale-os/kernel-protocol/invocation';
2
+ import { appendVary, requireResponseBound, toBody } from './response.js';
3
+ /** Present admitted buffered bytes under the response budget and binary manifest. */
4
+ export function binaryHttpResponse(binary, cors, maximumResponseBytes) {
5
+ requireResponseBound(binary.body, maximumResponseBytes);
6
+ const headers = binaryHttpHeaders(binary, cors, false);
7
+ return new Response(toBody(binary.body), { status: binary.status, headers });
8
+ }
9
+ /** Bind binary correlation and application headers to the HTTP response. */
10
+ export function binaryHttpHeaders(binary, cors, streaming) {
11
+ const applicationHeaders = Object.keys(binary.headers ?? {}).filter((name) => protocol.http.isApplicationBinaryHeader(name));
12
+ const headers = new Headers(cors([
13
+ protocol.http.REQUEST_ID_FIELD,
14
+ protocol.http.INVOCATION_FIELD,
15
+ protocol.http.BINARY_HEADERS_FIELD,
16
+ ...(streaming
17
+ ? [protocol.http.BINARY_DELIVERY_FIELD, protocol.http.BINARY_LENGTH_FIELD]
18
+ : []),
19
+ ...applicationHeaders,
20
+ ]));
21
+ for (const [name, value] of Object.entries(binary.headers ?? {})) {
22
+ if (protocol.http.isApplicationBinaryHeader(name))
23
+ headers.set(name, value);
24
+ }
25
+ headers.set('content-type', binary.mediaType);
26
+ headers.set(protocol.http.BINARY_HEADERS_FIELD, protocol.http.encodeBinaryHeaderNames(binary.headers));
27
+ headers.set(protocol.http.REQUEST_ID_FIELD, String(binary.requestId));
28
+ headers.set(protocol.http.INVOCATION_FIELD, protocol.http.encodeInvocationId(binary.invocation));
29
+ if (streaming) {
30
+ headers.set(protocol.http.BINARY_DELIVERY_FIELD, protocol.http.STREAMING_BINARY_DELIVERY);
31
+ }
32
+ headers.set('cache-control', 'no-store');
33
+ appendVary(headers, 'accept');
34
+ return headers;
35
+ }
@@ -0,0 +1,4 @@
1
+ import type { StreamingBinaryResult } from '@astrale-os/kernel-core/callable';
2
+ import type { RenderContext } from './model.js';
3
+ /** Deliver raw binary chunks with exact length, idle budget and producer cleanup. */
4
+ export declare function streamingBinaryHttpResponse(binary: StreamingBinaryResult, context: RenderContext): Response;
@@ -0,0 +1,123 @@
1
+ import * as protocol from '@astrale-os/kernel-protocol/invocation';
2
+ import { binaryHttpHeaders } from './binary.js';
3
+ /** Deliver raw binary chunks with exact length, idle budget and producer cleanup. */
4
+ export function streamingBinaryHttpResponse(binary, context) {
5
+ if (binary.length !== undefined && (!Number.isSafeInteger(binary.length) || binary.length < 0)) {
6
+ throw new TypeError('Streaming binary length must be a non-negative safe integer.');
7
+ }
8
+ const delivery = binary.delivery;
9
+ if (delivery !== undefined) {
10
+ if (!Number.isSafeInteger(delivery.maximumDurationMs) ||
11
+ delivery.maximumDurationMs <= 0 ||
12
+ delivery.maximumDurationMs > 2_147_483_647 ||
13
+ !Number.isSafeInteger(delivery.idleTimeoutMs) ||
14
+ delivery.idleTimeoutMs <= 0 ||
15
+ delivery.idleTimeoutMs > 2_147_483_647) {
16
+ throw new TypeError('Streaming binary delivery budget is invalid.');
17
+ }
18
+ context.active.extend(delivery.maximumDurationMs);
19
+ }
20
+ const iterator = binary.body[Symbol.asyncIterator]();
21
+ const headers = binaryHttpHeaders(Object.freeze({
22
+ requestId: context.requestId,
23
+ invocation: context.invocation,
24
+ mediaType: binary.mediaType,
25
+ headers: binary.headers,
26
+ }), context.cors, true);
27
+ if (binary.length !== undefined) {
28
+ headers.set('content-length', String(binary.length));
29
+ // Content-Length belongs to HTTP and an intermediary may rewrite it; the Client counts the
30
+ // delivered bytes against this protocol field instead.
31
+ headers.set(protocol.http.BINARY_LENGTH_FIELD, String(binary.length));
32
+ }
33
+ if (delivery !== undefined)
34
+ headers.set('cache-control', 'private, no-store');
35
+ let terminal = false;
36
+ let delivered = 0;
37
+ let controller;
38
+ let stopping;
39
+ let idleTimer;
40
+ const clearIdle = () => {
41
+ if (idleTimer !== undefined)
42
+ clearTimeout(idleTimer);
43
+ };
44
+ const resetIdle = () => {
45
+ if (delivery === undefined || terminal)
46
+ return;
47
+ clearIdle();
48
+ idleTimer = setTimeout(() => context.active.cancel('binary response idle timeout exceeded'), delivery.idleTimeoutMs);
49
+ };
50
+ const stop = (reason = 'Binary response was interrupted.') => {
51
+ if (terminal)
52
+ return stopping ?? Promise.resolve();
53
+ terminal = true;
54
+ clearIdle();
55
+ controller?.error(new Error(reason));
56
+ stopping = Promise.resolve().then(async () => {
57
+ try {
58
+ await iterator.return?.();
59
+ }
60
+ finally {
61
+ context.active.settle();
62
+ }
63
+ });
64
+ return stopping;
65
+ };
66
+ const fail = (cause) => {
67
+ if (terminal)
68
+ return;
69
+ terminal = true;
70
+ clearIdle();
71
+ controller?.error(cause);
72
+ stopping = Promise.resolve()
73
+ .then(() => iterator.return?.())
74
+ .catch(() => undefined)
75
+ .then(() => context.active.settle());
76
+ };
77
+ const body = new ReadableStream({
78
+ start(output) {
79
+ controller = output;
80
+ resetIdle();
81
+ },
82
+ async pull(controller) {
83
+ if (terminal)
84
+ return;
85
+ try {
86
+ const next = await iterator.next();
87
+ if (terminal)
88
+ return;
89
+ if (next.done) {
90
+ if (binary.length !== undefined && delivered !== binary.length) {
91
+ fail(new Error('OUTCOME_UNKNOWN: streaming binary ended before its declared length.'));
92
+ return;
93
+ }
94
+ terminal = true;
95
+ clearIdle();
96
+ controller.close();
97
+ context.active.settle();
98
+ return;
99
+ }
100
+ if (!(next.value instanceof Uint8Array)) {
101
+ throw new TypeError('Streaming binary yielded a non-byte chunk.');
102
+ }
103
+ delivered += next.value.byteLength;
104
+ if (binary.length !== undefined && delivered > binary.length) {
105
+ fail(new Error('OUTCOME_UNKNOWN: streaming binary exceeded its declared length.'));
106
+ return;
107
+ }
108
+ controller.enqueue(next.value);
109
+ resetIdle();
110
+ }
111
+ catch (cause) {
112
+ fail(cause);
113
+ }
114
+ },
115
+ async cancel(reason) {
116
+ const message = typeof reason === 'string' ? reason : 'binary response consumer cancelled';
117
+ context.active.cancel(message);
118
+ await stop();
119
+ },
120
+ });
121
+ context.active.onCancel(stop);
122
+ return new Response(body, { status: binary.status ?? 200, headers });
123
+ }
@@ -0,0 +1,12 @@
1
+ import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
2
+ import type { Codec } from '@astrale-os/kernel-protocol/codecs';
3
+ import type { ReleaseDigest } from '@astrale-os/kernel-protocol/release';
4
+ import * as errors from '@astrale-os/kernel-protocol/errors';
5
+ import * as protocol from '@astrale-os/kernel-protocol/invocation';
6
+ import type { CorsHeaders, ErrorRenderOptions } from './model.js';
7
+ /** Render a correlated error with its status, authentication challenge and retry delay. */
8
+ export declare function renderHttpError(requestId: protocol.RequestId, invocation: protocol.InvocationId, error: errors.ErrorPayload, codec: Codec, cors: CorsHeaders, options?: ErrorRenderOptions): Response;
9
+ /** Present stale routing as a bounded status-200 Invocation envelope. */
10
+ export declare function renderHttpStale(requestId: protocol.RequestId, invocation: protocol.InvocationId, current: protocol.Stale['current'], codec: Codec, cors: CorsHeaders, maximumResponseBytes?: number): Response;
11
+ /** Retirement controls always use JSON, regardless of the negotiated request codec. */
12
+ export declare function renderDeploymentRetired(deployment: IssuerId, release: ReleaseDigest | undefined, cors: CorsHeaders): Response;
@@ -0,0 +1,24 @@
1
+ import { json } from '@astrale-os/kernel-protocol/codecs';
2
+ import * as errors from '@astrale-os/kernel-protocol/errors';
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';
5
+ import { retryAfterHeaders } from '../../errors/index.js';
6
+ import { envelopeResponse, errorEnvelopeResponse, toBody } from './response.js';
7
+ /** Render a correlated error with its status, authentication challenge and retry delay. */
8
+ export function renderHttpError(requestId, invocation, error, codec, cors, options = {}) {
9
+ const status = options.status ?? errors.httpStatusOf(error);
10
+ const retry = retryAfterHeaders(error);
11
+ const extra = status === 401 ? { 'www-authenticate': 'Bearer realm="astrale-invocation"' } : retry;
12
+ return errorEnvelopeResponse(codec, { error, requestId, invocation }, { ...cors(Object.keys(retry)), ...extra }, status, options.negotiated ?? true, options.maximumResponseBytes);
13
+ }
14
+ /** Present stale routing as a bounded status-200 Invocation envelope. */
15
+ export function renderHttpStale(requestId, invocation, current, codec, cors, maximumResponseBytes) {
16
+ return envelopeResponse(codec, protocol.stale(requestId, invocation, current), cors(), 200, true, maximumResponseBytes);
17
+ }
18
+ /** Retirement controls always use JSON, regardless of the negotiated request codec. */
19
+ export function renderDeploymentRetired(deployment, release, cors) {
20
+ return new Response(toBody(json.encode(deploymentRetiredBody(deployment, release))), {
21
+ status: HTTP_DEPLOYMENT_RETIRED_STATUS,
22
+ headers: { ...cors([HTTP_DEPLOYMENT_RETIRED]), ...deploymentRetiredHeaders() },
23
+ });
24
+ }
@@ -17,7 +17,7 @@ export interface HttpHandlerConfig {
17
17
  readonly binaryDownloadTimeoutMs?: number;
18
18
  readonly cors: CorsPolicy;
19
19
  readonly websocket: boolean;
20
- /** Present exactly when the Server serves a release or a Publication. */
20
+ /** Present exactly when the Server serves a Release. */
21
21
  readonly serving?: (request: Request) => RequestServing;
22
22
  }
23
23
  export declare function createHttpHandler(config: HttpHandlerConfig): (request: Request, host?: ApplicationHost) => Promise<Response>;
@@ -7,8 +7,10 @@ import { pinned } from '../../release/served.js';
7
7
  import { readBoundedBody } from '../body.js';
8
8
  import { loadCodecs } from '../codecs.js';
9
9
  import { resolveCredential } from '../credential.js';
10
+ import { isStreamingBinary } from '../result.js';
10
11
  import { binaryRequestBody } from './binary-request.js';
11
- import { isStreamingBinary, renderDeploymentRetired, renderHttpError, renderHttpResult, } from './render.js';
12
+ import { renderDeploymentRetired, renderHttpError } from './control.js';
13
+ import { renderHttpResult } from './render.js';
12
14
  export function createHttpHandler(config) {
13
15
  return async (request, host) => {
14
16
  const cors = (exposedHeaders) => config.cors.response(request, exposedHeaders);
@@ -149,7 +151,7 @@ export function createHttpHandler(config) {
149
151
  if (serving !== undefined && served === undefined) {
150
152
  return renderError(envelope.requestId, {
151
153
  code: errors.CODES.BACKEND_UNAVAILABLE,
152
- message: `${serving.kind === 'release' ? 'Release' : 'Publication'} is unavailable.`,
154
+ message: 'Release is unavailable.',
153
155
  }, outgoing, { status: 503 });
154
156
  }
155
157
  if (envelope.routing?.kind === 'via') {
@@ -1,2 +1,4 @@
1
1
  export { createHttpHandler, type HttpHandlerConfig } from './handler.js';
2
- export { encodeEnvelope, renderBinary, renderHttpError, renderHttpResult, renderHttpStale, renderRedirect, type CorsHeaders, type ErrorRenderOptions, type RenderContext, } from './render.js';
2
+ export { renderHttpResult } from './render.js';
3
+ export { renderHttpError, renderHttpStale } from './control.js';
4
+ export type { CorsHeaders, ErrorRenderOptions, RenderContext } from './model.js';
@@ -1,2 +1,3 @@
1
1
  export { createHttpHandler } from './handler.js';
2
- export { encodeEnvelope, renderBinary, renderHttpError, renderHttpResult, renderHttpStale, renderRedirect, } from './render.js';
2
+ export { renderHttpResult } from './render.js';
3
+ export { renderHttpError, renderHttpStale } from './control.js';
@@ -0,0 +1,20 @@
1
+ import type { Codec } from '@astrale-os/kernel-protocol/codecs';
2
+ import type * as protocol from '@astrale-os/kernel-protocol/invocation';
3
+ import type { ActiveOperation } from '../../lifecycle/index.js';
4
+ /** Correlation, binding and active lifetime retained while presenting one result. */
5
+ export interface RenderContext {
6
+ readonly requestId: protocol.RequestId;
7
+ readonly codec: Codec;
8
+ readonly cors: CorsHeaders;
9
+ readonly invocation: protocol.InvocationId;
10
+ readonly active: ActiveOperation;
11
+ readonly maximumResponseBytes: number;
12
+ }
13
+ /** Derive response headers with the transport fields exposed to the browser. */
14
+ export type CorsHeaders = (exposedHeaders?: readonly string[]) => Readonly<Record<string, string>>;
15
+ /** Bound and negotiate an error using the admitted request presentation. */
16
+ export interface ErrorRenderOptions {
17
+ readonly status?: number;
18
+ readonly negotiated?: boolean;
19
+ readonly maximumResponseBytes?: number;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,37 +1,4 @@
1
- import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
2
- import type { BufferedBinaryResult, StreamingBinaryResult } from '@astrale-os/kernel-core/callable';
3
- import type { Codec } from '@astrale-os/kernel-protocol/codecs';
4
- import type { ReleaseDigest } from '@astrale-os/kernel-protocol/release';
5
- import * as errors from '@astrale-os/kernel-protocol/errors';
6
- import * as protocol from '@astrale-os/kernel-protocol/invocation';
7
- import type { Handler } from '../../invocation/index.js';
8
- import type { ActiveOperation } from '../../lifecycle/index.js';
9
- export interface RenderContext {
10
- readonly requestId: protocol.RequestId;
11
- readonly codec: Codec;
12
- readonly cors: CorsHeaders;
13
- readonly invocation: protocol.InvocationId;
14
- readonly active: ActiveOperation;
15
- readonly maximumResponseBytes: number;
16
- }
17
- export type CorsHeaders = (exposedHeaders?: readonly string[]) => Readonly<Record<string, string>>;
18
- export interface ErrorRenderOptions {
19
- readonly status?: number;
20
- readonly negotiated?: boolean;
21
- readonly maximumResponseBytes?: number;
22
- }
23
- type HandlerResult = Awaited<ReturnType<Handler['dispatch']>>;
1
+ import type { HandlerResult } from '../../invocation/index.js';
2
+ import type { RenderContext } from './model.js';
3
+ /** Render one admitted Handler result and retain only live producers in the active operation. */
24
4
  export declare function renderHttpResult(result: HandlerResult, context: RenderContext): Response;
25
- export declare function renderBinary(requestId: protocol.RequestId, invocation: protocol.InvocationId, binary: BufferedBinaryResult): protocol.BinaryResponse;
26
- export declare function renderRedirect(requestId: protocol.RequestId, invocation: protocol.InvocationId, redirect: Extract<HandlerResult, {
27
- readonly kind: 'redirect';
28
- }>['redirect']): protocol.RedirectResponse;
29
- export declare function renderHttpError(requestId: protocol.RequestId, invocation: protocol.InvocationId, error: errors.ErrorPayload, codec: Codec, cors: CorsHeaders, options?: ErrorRenderOptions): Response;
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;
33
- export declare function encodeEnvelope(codec: Codec, input: unknown): string | Uint8Array;
34
- export declare function isStreamingBinary(input: Extract<HandlerResult, {
35
- readonly kind: 'binary';
36
- }>['value']): input is StreamingBinaryResult;
37
- export {};