@astrale-os/kernel-server 0.5.0-beta.51 → 0.5.0-beta.53

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.
@@ -1,3 +1,4 @@
1
+ import { bundleUrl } from '@astrale-os/kernel-protocol/publication';
1
2
  import { verifyBundleBytes } from '@astrale-os/kernel-protocol/publication/bundle-evidence';
2
3
  import { accept as acceptPublication, encode as encodePublication, } from '@astrale-os/kernel-protocol/publication/publication';
3
4
  import { admitTable } from './table.js';
@@ -16,6 +17,14 @@ export function preparePublicationSnapshot(input) {
16
17
  throw new TypeError('Publication snapshot Domain identity differs from its Publication.');
17
18
  }
18
19
  const table = admitTable(admittedPublication, input.table, domain);
20
+ const addressed = new URL(bundleUrl(admittedPublication.identity.issuer, admittedPublication.schema.bundle.ref));
21
+ const addressedDirectory = addressed.pathname.slice(0, addressed.pathname.lastIndexOf('/') + 1);
22
+ const servingBundle = new URL(admittedPublication.schema.bundle.href);
23
+ if (servingBundle.origin === addressed.origin &&
24
+ servingBundle.pathname.startsWith(addressedDirectory) &&
25
+ servingBundle.href !== addressed.href) {
26
+ throw new TypeError('Publication bundle URL differs from its artifact digest.');
27
+ }
19
28
  const publicationBytes = encodePublication(admittedPublication);
20
29
  const snapshot = Object.freeze({ publication: admittedPublication });
21
30
  const prepared = Object.freeze({
@@ -1,6 +1,7 @@
1
1
  import * as codecs from '@astrale-os/kernel-protocol/codecs';
2
2
  import * as errors from '@astrale-os/kernel-protocol/errors';
3
3
  import * as protocol from '@astrale-os/kernel-protocol/invocation';
4
+ import { bundleUrl } from '@astrale-os/kernel-protocol/publication';
4
5
  import { mediaType as publicationMediaType, PATH as PUBLICATION_PATH, url as publicationUrl, } from '@astrale-os/kernel-protocol/publication/publication';
5
6
  import { reference as publicationReference } from '@astrale-os/kernel-protocol/publication/reference';
6
7
  import * as routes from '@astrale-os/kernel-protocol/routes';
@@ -59,6 +60,9 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
59
60
  return publicationPreflight(cors, request, snapshot, externalOrigin);
60
61
  }
61
62
  const responseCors = cors.response(request);
63
+ const descriptor = snapshot.publication.schema.bundle;
64
+ const addressed = bundleUrl(snapshot.publication.identity.issuer, descriptor.ref);
65
+ const immutable = descriptor.href === addressed;
62
66
  if (sameRoute(request.url, snapshot.publication.schema.bundle.href, externalOrigin)) {
63
67
  if (!active.isAccepting)
64
68
  return response({ error: 'Publication artifact is unavailable while draining.' }, 503, responseCors);
@@ -68,16 +72,32 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
68
72
  if (body.byteLength !== snapshot.publication.schema.bundle.ref.size) {
69
73
  return response({ error: 'Bundle differs from its Publication.' }, 500, responseCors);
70
74
  }
71
- return new Response(toBody(body), {
72
- status: 200,
75
+ const etag = quoteEtag(descriptor.ref.digest);
76
+ const unchanged = request.headers.get('if-none-match') === etag;
77
+ return new Response(unchanged ? null : toBody(body), {
78
+ status: unchanged ? 304 : 200,
73
79
  headers: {
74
80
  ...responseCors,
75
81
  'content-type': snapshot.publication.schema.bundle.ref.mediaType,
76
- etag: quoteEtag(snapshot.publication.schema.bundle.ref.digest),
77
- 'cache-control': 'public, max-age=31536000, immutable, no-transform',
82
+ etag,
83
+ 'cache-control': immutable
84
+ ? 'public, max-age=31536000, immutable, no-transform'
85
+ : 'public, max-age=0, must-revalidate, no-transform',
78
86
  },
79
87
  });
80
88
  }
89
+ if (request.method === 'GET' && immutable) {
90
+ const pathname = new URL(addressed).pathname;
91
+ const directory = pathname.slice(0, pathname.lastIndexOf('/') + 1);
92
+ const requested = new URL(request.url);
93
+ if (requested.pathname.startsWith(directory) &&
94
+ /^[0-9a-f]{64}\.json$/u.test(requested.pathname.slice(directory.length))) {
95
+ return new Response(null, {
96
+ status: 404,
97
+ headers: { ...responseCors, 'cache-control': 'no-store' },
98
+ });
99
+ }
100
+ }
81
101
  const deliveryUrl = snapshot.publication.delivery?.href;
82
102
  if (deliveryUrl !== undefined && sameRoute(request.url, deliveryUrl, externalOrigin)) {
83
103
  return handleDelivery(request, snapshot, deliveryProvider, active, limits, cors.response(request));
@@ -206,12 +226,14 @@ function streamingBinaryResponse(binary, operation, cors, request) {
206
226
  const iterator = binary.body[Symbol.asyncIterator]();
207
227
  const applicationHeaders = applicationBinaryHeaders(binary.headers);
208
228
  let terminal = false;
229
+ let controller;
209
230
  let stopping;
210
- const stop = () => {
211
- stopping ??= Promise.resolve().then(async () => {
212
- if (terminal)
213
- return;
214
- terminal = true;
231
+ const stop = (reason = 'Binary response was interrupted.') => {
232
+ if (terminal)
233
+ return stopping ?? Promise.resolve();
234
+ terminal = true;
235
+ controller?.error(new Error(reason));
236
+ stopping = Promise.resolve().then(async () => {
215
237
  try {
216
238
  await iterator.return?.();
217
239
  }
@@ -222,6 +244,9 @@ function streamingBinaryResponse(binary, operation, cors, request) {
222
244
  return stopping;
223
245
  };
224
246
  const body = new ReadableStream({
247
+ start(output) {
248
+ controller = output;
249
+ },
225
250
  async pull(controller) {
226
251
  if (terminal)
227
252
  return;
@@ -238,6 +263,8 @@ function streamingBinaryResponse(binary, operation, cors, request) {
238
263
  controller.enqueue(next.value);
239
264
  }
240
265
  catch (cause) {
266
+ if (terminal)
267
+ return;
241
268
  terminal = true;
242
269
  controller.error(cause);
243
270
  operation.settle();
@@ -6,7 +6,7 @@ import { invocationContext, publicationRouteDecision, } from '../../invocation/i
6
6
  import { readBoundedBody } from '../body.js';
7
7
  import { loadCodecs } from '../codecs.js';
8
8
  import { resolveCredential } from '../credential.js';
9
- import { renderHttpError, renderHttpResult } from './render.js';
9
+ import { isStreamingBinary, renderHttpError, renderHttpResult, } from './render.js';
10
10
  export function createHttpHandler(config) {
11
11
  return async (request, host) => {
12
12
  const cors = (exposedHeaders) => config.cors.response(request, exposedHeaders);
@@ -146,7 +146,8 @@ export function createHttpHandler(config) {
146
146
  active,
147
147
  maximumResponseBytes: config.maximumResponseBytes,
148
148
  });
149
- handedOff = result.kind === 'stream';
149
+ handedOff =
150
+ result.kind === 'stream' || (result.kind === 'binary' && isStreamingBinary(result.value));
150
151
  return response;
151
152
  }
152
153
  catch (input) {
@@ -90,12 +90,14 @@ function streamingBinaryHttpResponse(binary, context) {
90
90
  headers: binary.headers,
91
91
  }), context.cors, true);
92
92
  let terminal = false;
93
+ let controller;
93
94
  let stopping;
94
- const stop = () => {
95
- stopping ??= Promise.resolve().then(async () => {
96
- if (terminal)
97
- return;
98
- terminal = true;
95
+ const stop = (reason = 'Binary response was interrupted.') => {
96
+ if (terminal)
97
+ return stopping ?? Promise.resolve();
98
+ terminal = true;
99
+ controller?.error(new Error(reason));
100
+ stopping = Promise.resolve().then(async () => {
99
101
  try {
100
102
  await iterator.return?.();
101
103
  }
@@ -106,6 +108,9 @@ function streamingBinaryHttpResponse(binary, context) {
106
108
  return stopping;
107
109
  };
108
110
  const body = new ReadableStream({
111
+ start(output) {
112
+ controller = output;
113
+ },
109
114
  async pull(controller) {
110
115
  if (terminal)
111
116
  return;
@@ -122,6 +127,8 @@ function streamingBinaryHttpResponse(binary, context) {
122
127
  controller.enqueue(next.value);
123
128
  }
124
129
  catch (cause) {
130
+ if (terminal)
131
+ return;
125
132
  terminal = true;
126
133
  controller.error(cause);
127
134
  context.active.settle();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-server",
3
- "version": "0.5.0-beta.51",
3
+ "version": "0.5.0-beta.53",
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": {
@@ -33,9 +33,9 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@astrale-os/kernel-core": "0.9.0-beta.45",
36
+ "@astrale-os/kernel-core": "0.9.0-beta.46",
37
37
  "@astrale-os/kernel-dsl": "0.2.0-beta.33",
38
- "@astrale-os/kernel-protocol": "0.5.0-beta.49",
38
+ "@astrale-os/kernel-protocol": "0.5.0-beta.51",
39
39
  "hono": "^4.6.20",
40
40
  "msgpackr": "^2.0.5"
41
41
  },