@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.
Files changed (62) hide show
  1. package/dist/cors/cors.js +9 -3
  2. package/dist/index.d.ts +8 -3
  3. package/dist/index.js +7 -2
  4. package/dist/invocation/index.d.ts +1 -1
  5. package/dist/invocation/index.js +1 -1
  6. package/dist/invocation/legacy/index.d.ts +20 -0
  7. package/dist/invocation/legacy/index.js +20 -0
  8. package/dist/invocation/{publication-route.d.ts → legacy/publication-route.d.ts} +6 -1
  9. package/dist/invocation/{publication-route.js → legacy/publication-route.js} +5 -1
  10. package/dist/invocation/release-route.d.ts +26 -0
  11. package/dist/invocation/release-route.js +55 -0
  12. package/dist/invocation/types.d.ts +16 -4
  13. package/dist/issuer/routes.js +13 -0
  14. package/dist/issuer/types.d.ts +5 -0
  15. package/dist/release/delivery/handler.d.ts +13 -0
  16. package/dist/{publication → release}/delivery/handler.js +33 -10
  17. package/dist/release/delivery/types.d.ts +21 -0
  18. package/dist/release/index.d.ts +3 -0
  19. package/dist/release/index.js +2 -0
  20. package/dist/release/legacy/delivery.d.ts +18 -0
  21. package/dist/release/legacy/delivery.js +2 -0
  22. package/dist/release/legacy/index.d.ts +39 -0
  23. package/dist/release/legacy/index.js +38 -0
  24. package/dist/{publication → release/legacy}/snapshot.d.ts +14 -4
  25. package/dist/{publication → release/legacy}/snapshot.js +11 -2
  26. package/dist/release/legacy/table.d.ts +5 -0
  27. package/dist/{publication → release/legacy}/table.js +2 -2
  28. package/dist/{publication → release/legacy}/types.d.ts +13 -4
  29. package/dist/release/served.d.ts +30 -0
  30. package/dist/release/served.js +14 -0
  31. package/dist/release/snapshot.d.ts +26 -0
  32. package/dist/release/snapshot.js +54 -0
  33. package/dist/release/table.d.ts +4 -0
  34. package/dist/release/table.js +11 -0
  35. package/dist/release/types.d.ts +33 -0
  36. package/dist/release/types.js +1 -0
  37. package/dist/routes/index.d.ts +1 -1
  38. package/dist/routes/index.js +1 -1
  39. package/dist/routes/routes.d.ts +9 -6
  40. package/dist/routes/routes.js +135 -76
  41. package/dist/server.js +66 -45
  42. package/dist/transport/codecs.d.ts +2 -2
  43. package/dist/transport/codecs.js +4 -3
  44. package/dist/transport/http/handler.d.ts +3 -2
  45. package/dist/transport/http/handler.js +18 -15
  46. package/dist/transport/http/render.d.ts +4 -0
  47. package/dist/transport/http/render.js +11 -3
  48. package/dist/transport/websocket/session.d.ts +3 -2
  49. package/dist/transport/websocket/session.js +15 -15
  50. package/dist/transport/websocket/upgrade.d.ts +2 -2
  51. package/dist/transport/websocket/upgrade.js +2 -4
  52. package/dist/types.d.ts +19 -4
  53. package/package.json +7 -8
  54. package/dist/publication/delivery/handler.d.ts +0 -9
  55. package/dist/publication/delivery/types.d.ts +0 -11
  56. package/dist/publication/index.d.ts +0 -3
  57. package/dist/publication/index.js +0 -2
  58. package/dist/publication/table.d.ts +0 -4
  59. /package/dist/{publication → release}/delivery/index.d.ts +0 -0
  60. /package/dist/{publication → release}/delivery/index.js +0 -0
  61. /package/dist/{publication → release}/delivery/types.js +0 -0
  62. /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 PUBLICATION_PATH, url as publicationUrl, } from '@astrale-os/kernel-protocol/publication/publication';
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 { currentPublicationSnapshot, mountRoutes } from './routes/index.js';
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
- ...(config.publication === undefined
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
- ...(config.publication === undefined
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.publication !== undefined) {
87
- mountRoutes(app, config.publication, config.delivery, handler, active, config.invocation, config.limits, config.cors, (request) => hosts.get(request), config.routeReservations, externalIssuer, externalOrigin);
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
- const publication = config.publication === undefined ? undefined : capturePublicationSource(config.publication);
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 && publication === undefined) {
155
- throw new TypeError('Publication Delivery requires a Publication source.');
165
+ if (delivery !== undefined && serving === undefined) {
166
+ throw new TypeError('Delivery requires a release or Publication source.');
156
167
  }
157
- const publicationPath = publication === undefined
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
- if (publicationPath === path) {
165
- throw new TypeError('Publication and invocation paths must be distinct.');
166
- }
167
- if (health !== undefined && publicationPath === health.path) {
168
- throw new TypeError('Health and Publication paths must be distinct.');
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
- ...(publication === undefined ? {} : { publication }),
202
+ ...(serving === undefined ? {} : { serving }),
194
203
  ...(delivery === undefined ? {} : { delivery }),
195
- routeReservations: routeReservations(path, health?.path, issuer, websocket !== undefined, tokenExchange !== undefined, publicationPath),
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, publicationPath) {
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
- if (publicationPath !== undefined) {
213
- reservations.push(Object.freeze({ path: publicationPath, methods: Object.freeze(['GET']) }));
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 protocolPublicationPath(issuer) {
218
- if (issuer === undefined)
219
- return PUBLICATION_PATH;
220
- return new URL(publicationUrl(issuer)).pathname;
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', 'Publication Delivery provider is invalid.');
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
- /** Load the built-in Server representation set before pure Protocol binding selection. */
4
- export declare function loadCodecs(): Promise<readonly [codecs.Codec, codecs.Codec]>;
3
+ /** The built-in Server representation set before pure Protocol binding selection. */
4
+ export declare function loadCodecs(): readonly [codecs.Codec];
@@ -1,5 +1,6 @@
1
1
  import * as codecs from '@astrale-os/kernel-protocol/codecs';
2
- /** Load the built-in Server representation set before pure Protocol binding selection. */
3
- export async function loadCodecs() {
4
- return Object.freeze([codecs.json, await codecs.loadMsgpack()]);
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 { PublicationSnapshot } from '../../publication/index.js';
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
- readonly currentPublication?: (request: Request) => PublicationSnapshot | undefined;
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 { invocationContext, publicationRouteDecision, } from '../../invocation/index.js';
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 = await loadCodecs();
59
+ const supported = loadCodecs();
59
60
  const contentType = request.headers.get('content-type');
60
- const incoming = binaryRequest || contentType === null || contentType.trim() === ''
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: 'Unsupported request Content-Type.',
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 publication = config.currentPublication?.(request);
143
- if (config.currentPublication !== undefined && publication === undefined) {
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
- if (publication === undefined) {
151
- return renderError(envelope.requestId, {
152
- code: errors.CODES.INVALID_REQUEST,
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
- ...(publication === undefined ? {} : { publication }),
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
- // Clients that predate the protocol length count against Content-Length, which an intermediary
115
- // that re-encodes the stream would rewrite, so a compressible media type would fail delivery.
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 { PublicationSnapshot } from '../../publication/index.js';
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
- readonly currentPublication?: () => PublicationSnapshot | undefined;
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 { invocationContext, publicationRouteDecision, } from '../../invocation/index.js';
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 publication = config.currentPublication?.();
104
- if (config.currentPublication !== undefined && publication === undefined) {
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
- if (publication === undefined) {
113
- await sendError(socket, admittedCodec, envelope.requestId, assigned, {
114
- code: errors.CODES.INVALID_REQUEST,
115
- message: 'Publication routing requires a Publication-backed destination.',
116
- }, sendOutput);
117
- return;
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
- ...(publication === undefined ? {} : { publication }),
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 { PublicationSnapshot } from '../../publication/index.js';
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 currentPublication?: () => PublicationSnapshot | undefined;
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 = await loadCodecs();
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.currentPublication === undefined
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 './publication/delivery/index.js';
5
- import type { PublicationSource } from './publication/index.js';
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
- /** Required exactly when the current Publication advertises delivery. */
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 { PublicationSnapshot, PublicationSnapshotInput, PublicationSource, } from './publication/index.js';
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.69",
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/publication/snapshot.d.ts",
24
- "import": "./dist/publication/snapshot.js"
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.61",
37
- "@astrale-os/kernel-dsl": "0.2.0-beta.43",
38
- "@astrale-os/kernel-protocol": "0.5.0-beta.67",
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,2 +0,0 @@
1
- export { preparePublicationSnapshot, readPreparedPublicationSnapshot, } from './snapshot.js';
2
- 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