@volter/world-core 3.0.162 → 3.0.164
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/src/browser-session.d.ts +7 -1
- package/dist/src/browser-session.js +21 -10
- package/dist/src/derived-core.d.ts +36 -3
- package/dist/src/derived-core.js +121 -13
- package/dist/src/derived-real.js +15 -3
- package/dist/src/events.d.ts +6 -3
- package/dist/src/events.js +52 -7
- package/dist/src/file-response.d.ts +6 -2
- package/dist/src/file-response.js +29 -5
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/managed-database.d.ts +4 -2
- package/dist/src/managed-database.js +1 -0
- package/dist/src/media-header.d.ts +34 -0
- package/dist/src/media-header.js +129 -0
- package/dist/src/pack-fetch.d.ts +24 -3
- package/dist/src/pack-fetch.js +114 -94
- package/dist/src/packRegistry.d.ts +12 -0
- package/dist/src/request-body.d.ts +2 -3
- package/dist/src/request-body.js +2 -3
- package/dist/src/runtime.d.ts +2 -1
- package/dist/src/runtime.js +2 -1
- package/dist/src/serve-http.d.ts +1 -1
- package/dist/src/serve-http.js +93 -17
- package/package.json +1 -1
- package/src/browser-session.ts +17 -5
- package/src/derived-core.ts +135 -17
- package/src/derived-real.ts +10 -2
- package/src/events.ts +54 -9
- package/src/file-response.ts +28 -6
- package/src/index.ts +2 -1
- package/src/managed-database.ts +4 -3
- package/src/media-header.ts +108 -0
- package/src/pack-fetch.ts +107 -85
- package/src/packRegistry.ts +7 -1
- package/src/request-body.ts +2 -3
- package/src/runtime.ts +2 -1
- package/src/serve-http.ts +70 -16
package/src/events.ts
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
// Imports stay free of side effects: the delivery's transport (Node's http, the World's egress policy) is loaded when a
|
|
8
8
|
// delivery is made, never when a browser client imports the kernel.
|
|
9
9
|
import { createHash, createHmac } from 'node:crypto';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
10
11
|
import { applyTwinWrite, applyTwinWriteAtomic, twinResources } from './serve.ts';
|
|
11
12
|
import { equalSecrets as same, ksuidFrom } from './signing.ts';
|
|
12
13
|
import { deliveryTraceHeaders } from './trace-context.ts';
|
|
13
14
|
import { sendRecordedEvent } from './event-delivery.ts';
|
|
14
|
-
import { getActiveWorldStore, withWorldStore } from './world-store.ts';
|
|
15
|
+
import { getActiveWorldStore, withWorldStore, type WorldStore } from './world-store.ts';
|
|
15
16
|
import { worldPaths } from './storage.ts';
|
|
16
17
|
|
|
17
18
|
/** How the vendor signs a delivery. */
|
|
@@ -306,6 +307,26 @@ const worldTransport: EventTransport = async (url, body, headers) => {
|
|
|
306
307
|
try { return ((await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body })) as { status: number }).status; } catch { return 0; }
|
|
307
308
|
};
|
|
308
309
|
|
|
310
|
+
const pendingDeliveries = new WeakMap<WorldStore, Map<string, Set<Promise<void>>>>();
|
|
311
|
+
|
|
312
|
+
function forgetDeliveries(store: WorldStore, root: string, deliveries: Iterable<Promise<void>>): void {
|
|
313
|
+
const roots = pendingDeliveries.get(store), pending = roots?.get(root);
|
|
314
|
+
if (!pending) return;
|
|
315
|
+
for (const delivery of deliveries) pending.delete(delivery);
|
|
316
|
+
if (!pending.size) roots!.delete(root);
|
|
317
|
+
if (!roots!.size) pendingDeliveries.delete(store);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Await already-scheduled first dispatches and consume their retained persistence failures. */
|
|
321
|
+
export async function settleEventDeliveries(root: string): Promise<void> {
|
|
322
|
+
const store = getActiveWorldStore(), path = resolve(root);
|
|
323
|
+
const deliveries = [...(pendingDeliveries.get(store)?.get(path) ?? [])];
|
|
324
|
+
const outcomes = await Promise.allSettled(deliveries);
|
|
325
|
+
forgetDeliveries(store, path, deliveries);
|
|
326
|
+
const failures = outcomes.flatMap((outcome) => outcome.status === 'rejected' ? [outcome.reason] : []);
|
|
327
|
+
if (failures.length) throw new AggregateError(failures, 'Declared-event dispatch failed');
|
|
328
|
+
}
|
|
329
|
+
|
|
309
330
|
/** Who watches signed packets before dispatch (a walk reading each as its receiver would). This is not evidence of
|
|
310
331
|
* transport or acceptance. Set and restored by the walk, as its World store is; unset, nothing watches. */
|
|
311
332
|
export type DeliveryObserver = (delivery: EventDelivery & { service: string }) => void;
|
|
@@ -321,13 +342,14 @@ export function setDeliveryObserver(observer: DeliveryObserver | undefined): Del
|
|
|
321
342
|
* (`unreachable`), or it did not answer in time (`timeout`). */
|
|
322
343
|
export type ApplicationAnswer = { status: number; headers: Record<string, string>; body: string; missed?: 'timeout' | 'unreachable' };
|
|
323
344
|
|
|
324
|
-
/** Who answers
|
|
345
|
+
/** Who answers a vendor's requests to the application before the World's application route does, given to a
|
|
325
346
|
* pack's fetch (`PackFetchOptions.application`) and belonging to that fetch alone: an answer it gives is the
|
|
326
347
|
* application's; `'unreachable'` is a question nobody answers (a walk's life, a sealed World, takes no other); undefined
|
|
327
348
|
* is a question it does not take, which goes on as any other does: the application route, the World's egress rule, the
|
|
328
349
|
* network. A walk's stand-in answers every question (its life's answers, else unreachable); a hosted World's router
|
|
329
350
|
* answers only what it can answer truthfully (WorldDoors.askInWorld) and leaves the rest. `bodyError` models a
|
|
330
|
-
* disconnect after the response headers; a caller needing the complete body receives no complete answer
|
|
351
|
+
* disconnect after the response headers; a caller needing the complete body receives no complete answer, while an
|
|
352
|
+
* event dispatch acknowledges the response status without reading its body. */
|
|
331
353
|
export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{ status: number; headers?: Record<string, string>; body: string; bodyError?: string } | 'unreachable' | undefined>;
|
|
332
354
|
|
|
333
355
|
/** A message the vendor sends the application's server and decides by its answer (Stripe's real-time authorization
|
|
@@ -367,10 +389,15 @@ export async function deliverEvents(
|
|
|
367
389
|
data: (type: string) => Promise<Record<string, unknown>>,
|
|
368
390
|
transport: EventTransport = worldTransport,
|
|
369
391
|
values?: (type: string) => Promise<Record<string, unknown>>,
|
|
392
|
+
application?: ApplicationStandIn,
|
|
370
393
|
): Promise<EventDelivery[]> {
|
|
371
394
|
if (write.storedType === decl.store) return [];
|
|
395
|
+
const dispatch: EventTransport = application ? async (url, body, headers) => {
|
|
396
|
+
const answer = await application(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body });
|
|
397
|
+
return answer === undefined ? transport(url, body, headers) : answer === 'unreachable' ? 0 : answer.status;
|
|
398
|
+
} : transport;
|
|
372
399
|
const sent: EventDelivery[] = [];
|
|
373
|
-
for (const type of eventTypesOf(decl, write.operation)) sent.push(...(await deliverEventType(service, decl, write, data,
|
|
400
|
+
for (const type of eventTypesOf(decl, write.operation)) sent.push(...(await deliverEventType(service, decl, write, data, dispatch, values, type)));
|
|
374
401
|
return sent;
|
|
375
402
|
}
|
|
376
403
|
|
|
@@ -466,10 +493,28 @@ async function deliverEventType(
|
|
|
466
493
|
// active before it runs. Transport completion writes to the same retained state.
|
|
467
494
|
const store = getActiveWorldStore(), deliveryRoot = worldPaths(service, write.root).root;
|
|
468
495
|
for (const d of sent) deliveryObserver?.({ ...d, service });
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
496
|
+
const roots = pendingDeliveries.get(store) ?? new Map<string, Set<Promise<void>>>();
|
|
497
|
+
pendingDeliveries.set(store, roots);
|
|
498
|
+
const pending = roots.get(deliveryRoot) ?? new Set<Promise<void>>();
|
|
499
|
+
roots.set(deliveryRoot, pending);
|
|
500
|
+
for (const packet of recorded) {
|
|
501
|
+
let finish!: () => void;
|
|
502
|
+
let fail!: (reason: unknown) => void;
|
|
503
|
+
const dispatched = new Promise<void>((done, reject) => { finish = done; fail = reject; });
|
|
504
|
+
// Live writes return before dispatch; an explicit settlement reports persistence failures.
|
|
505
|
+
void dispatched.catch(() => {});
|
|
506
|
+
pending.add(dispatched);
|
|
507
|
+
setTimeout(async () => {
|
|
508
|
+
try {
|
|
509
|
+
await withWorldStore(store, () => sendRecordedEvent(service, packet.type, packet.id, deliveryRoot, transport, trace));
|
|
510
|
+
finish();
|
|
511
|
+
forgetDeliveries(store, deliveryRoot, [dispatched]);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
// Keep the rejection until explicit settlement consumes it. The queued/sending claim remains for
|
|
514
|
+
// native recovery; this must not fail the already-committed vendor write or cause a blind resend.
|
|
515
|
+
fail(error);
|
|
516
|
+
}
|
|
517
|
+
}, 0);
|
|
518
|
+
}
|
|
474
519
|
return sent;
|
|
475
520
|
}
|
package/src/file-response.ts
CHANGED
|
@@ -25,10 +25,16 @@ export function fileResponse(path: string, init?: ResponseInit): Response {
|
|
|
25
25
|
* Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: every satisfiable range is a
|
|
26
26
|
* 206 with its Content-Range, `bytes=0-` (the whole body) included; a range that does not parse, a first-byte-pos past
|
|
27
27
|
* its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed range that starts past
|
|
28
|
-
* the end (any range of an empty body) is a 416.
|
|
28
|
+
* the end (any range of an empty body) is a 416. Lazy sources supply inclusive ranges and stream in bounded reads.
|
|
29
|
+
* `nosniff` stops a browser second-guessing the content type the vendor gave.
|
|
29
30
|
*/
|
|
30
|
-
export function bytesResponse(
|
|
31
|
-
|
|
31
|
+
export function bytesResponse(
|
|
32
|
+
request: Request,
|
|
33
|
+
bytes: Uint8Array | { size: number; range(start: number, end: number): Promise<Uint8Array | null> },
|
|
34
|
+
contentType: string,
|
|
35
|
+
cacheControl = 'public, max-age=604800, immutable',
|
|
36
|
+
): Response {
|
|
37
|
+
const total = bytes instanceof Uint8Array ? bytes.length : bytes.size;
|
|
32
38
|
const base = { 'content-type': contentType, 'x-content-type-options': 'nosniff', 'accept-ranges': 'bytes', 'cache-control': cacheControl };
|
|
33
39
|
const range = /^bytes=(\d*)-(\d*)$/.exec((request.headers.get('range') ?? '').trim());
|
|
34
40
|
let start = 0;
|
|
@@ -44,9 +50,25 @@ export function bytesResponse(request: Request, bytes: Uint8Array, contentType:
|
|
|
44
50
|
// every satisfiable range is answered as one, the whole body's `bytes=0-` included: a media player (Safari's) reads a
|
|
45
51
|
// 206 with its Content-Range as the server taking ranges, and a 200 as one it cannot seek in
|
|
46
52
|
const partial = valid;
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
let body: BodyInit | null = null;
|
|
54
|
+
if (request.method !== 'HEAD') {
|
|
55
|
+
if (bytes instanceof Uint8Array) body = new Blob([bytes.slice(start, end + 1)]);
|
|
56
|
+
else {
|
|
57
|
+
let at = start;
|
|
58
|
+
body = new ReadableStream<Uint8Array>({
|
|
59
|
+
async pull(controller) {
|
|
60
|
+
if (at > end) { controller.close(); return; }
|
|
61
|
+
const last = Math.min(at + 8 * 1024 * 1024 - 1, end);
|
|
62
|
+
const chunk = await bytes.range(at, last);
|
|
63
|
+
if (!chunk) { controller.error(new Error('stored bytes are gone')); return; }
|
|
64
|
+
controller.enqueue(chunk);
|
|
65
|
+
at = last + 1;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return new Response(body, {
|
|
49
71
|
status: partial ? 206 : 200,
|
|
50
|
-
headers: { ...base, 'content-length': String(
|
|
72
|
+
headers: { ...base, 'content-length': String(end + 1 - start), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
|
|
51
73
|
});
|
|
52
74
|
}
|
package/src/index.ts
CHANGED
|
@@ -89,6 +89,7 @@ export * as protobuf from './protobuf.ts';
|
|
|
89
89
|
export * as grpc from './grpc-wire.ts';
|
|
90
90
|
export { acceptH2Connection, type H2Handler, type Http1Handler } from './h2.ts';
|
|
91
91
|
export { placeholderPng } from './placeholder-image.ts';
|
|
92
|
+
export { imageDimensions, readImageHeader, readMp4Header } from './media-header.ts';
|
|
92
93
|
export { presignSigV4, signSigV4, verifySigV4, type SigV4Verdict } from './sigv4.ts';
|
|
93
94
|
export * as redis from './redis/index.ts';
|
|
94
95
|
export * as mysql from './mysql/index.ts';
|
|
@@ -102,7 +103,7 @@ export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioE
|
|
|
102
103
|
|
|
103
104
|
export { verifyAttestationBundle } from './attestation.ts';
|
|
104
105
|
|
|
105
|
-
export { browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.ts';
|
|
106
|
+
export { cookieOf, signedIn, browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.ts';
|
|
106
107
|
|
|
107
108
|
export { readHostPort } from './host-port.ts';
|
|
108
109
|
export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost, type BrowserCookie, type BrowserState, type BrowserStorageState, type BrowserContext, type BrowserIndexedDB } from './browser-state.ts';
|
package/src/managed-database.ts
CHANGED
|
@@ -254,10 +254,10 @@ export function managedDatabase(url: string, options: { readOnly?: boolean } = {
|
|
|
254
254
|
* walk/journey-kit.ts), its statements sent as its client sends them: a text alone as one simple Query (a migration
|
|
255
255
|
* file, a `BEGIN`), a text with values as one extended-protocol statement, the values bind parameters. A transaction it
|
|
256
256
|
* opens stays open across its statements until it ends it, as on the application's own connection. Each statement's
|
|
257
|
-
* answer is its results in Postgres's text format,
|
|
258
|
-
export type DatabaseSession = { query(text: string, values?: ReadonlyArray<string | null>): Promise<StatementResult[]
|
|
257
|
+
* answer is its results in Postgres's text format, with native column type OIDs retained beside the values. */
|
|
258
|
+
export type DatabaseSession = { query(text: string, values?: ReadonlyArray<string | null>): Promise<Array<StatementResult & { fieldTypes: number[] }>>; end(): Promise<void> };
|
|
259
259
|
|
|
260
|
-
type PgResult = { command?: string | null; rowCount?: number | null; fields?: Array<{ name: string }>; rows?: Array<Array<string | null>> };
|
|
260
|
+
type PgResult = { command?: string | null; rowCount?: number | null; fields?: Array<{ name: string; dataTypeID: number }>; rows?: Array<Array<string | null>> };
|
|
261
261
|
|
|
262
262
|
/** The World's Postgres at `url` as one connection held until `end()`. */
|
|
263
263
|
export async function databaseSession(url: string): Promise<DatabaseSession> {
|
|
@@ -277,6 +277,7 @@ export async function databaseSession(url: string): Promise<DatabaseSession> {
|
|
|
277
277
|
const answer = (await (client.query({ text, ...(values === undefined ? {} : { values }), rowMode: 'array', types: raw }) as Promise<PgResult | PgResult[]>));
|
|
278
278
|
return (Array.isArray(answer) ? answer : [answer]).map((r) => ({
|
|
279
279
|
fields: (r.fields ?? []).map((f) => f.name),
|
|
280
|
+
fieldTypes: (r.fields ?? []).map((f) => f.dataTypeID),
|
|
280
281
|
rows: r.rows ?? [],
|
|
281
282
|
command: [r.command ?? '', r.command === 'INSERT' ? '0' : '', r.rowCount ?? ''].filter((part) => part !== '').join(' '),
|
|
282
283
|
}));
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// File-format headers only: no decoding, vendor limits, processing or rendition policy.
|
|
2
|
+
type ImageType = 'image/png' | 'image/gif' | 'image/jpeg' | 'image/webp';
|
|
3
|
+
type Dimensions = { width: number; height: number };
|
|
4
|
+
type ImageHeader = Dimensions & { jpeg?: { frameEnd: number; nonMarkerBytes: boolean; standaloneMarkers: boolean; fillBytes: boolean; shortSegments: boolean } };
|
|
5
|
+
|
|
6
|
+
const tag = (bytes: Uint8Array, at: number): string => String.fromCharCode(...bytes.subarray(at, at + 4));
|
|
7
|
+
|
|
8
|
+
/** Dimensions in a caller-declared image format, or undefined for an incomplete header. */
|
|
9
|
+
export function imageDimensions(bytes: Uint8Array, type: ImageType): ImageHeader | undefined {
|
|
10
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
11
|
+
if (type === 'image/png') return bytes.length >= 24 ? { width: view.getUint32(16), height: view.getUint32(20) } : undefined;
|
|
12
|
+
if (type === 'image/gif') return bytes.length >= 10 ? { width: view.getUint16(6, true), height: view.getUint16(8, true) } : undefined;
|
|
13
|
+
if (type === 'image/webp') {
|
|
14
|
+
if (bytes.length < 30) return undefined;
|
|
15
|
+
const chunk = tag(bytes, 12);
|
|
16
|
+
if (chunk === 'VP8 ') return { width: view.getUint16(26, true) & 0x3fff, height: view.getUint16(28, true) & 0x3fff };
|
|
17
|
+
if (chunk === 'VP8L') {
|
|
18
|
+
const bits = view.getUint32(21, true);
|
|
19
|
+
return { width: 1 + (bits & 0x3fff), height: 1 + ((bits >>> 14) & 0x3fff) };
|
|
20
|
+
}
|
|
21
|
+
if (chunk === 'VP8X') {
|
|
22
|
+
const u24 = (at: number): number => bytes[at]! | (bytes[at + 1]! << 8) | (bytes[at + 2]! << 16);
|
|
23
|
+
return { width: 1 + u24(24), height: 1 + u24(27) };
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
let at = 2;
|
|
28
|
+
const jpeg = { frameEnd: 0, nonMarkerBytes: false, standaloneMarkers: false, fillBytes: false, shortSegments: false };
|
|
29
|
+
while (at + 4 <= bytes.length) {
|
|
30
|
+
if (bytes[at] !== 0xff) { jpeg.nonMarkerBytes = true; at += 1; continue; }
|
|
31
|
+
const marker = bytes[at + 1]!;
|
|
32
|
+
if (marker === 0xff) { jpeg.fillBytes = true; at += 1; continue; }
|
|
33
|
+
if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) { jpeg.standaloneMarkers = true; at += 2; continue; }
|
|
34
|
+
const length = view.getUint16(at + 2);
|
|
35
|
+
if (length < 2) jpeg.shortSegments = true;
|
|
36
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
37
|
+
return at + 9 <= bytes.length ? { width: view.getUint16(at + 7), height: view.getUint16(at + 5), jpeg: { ...jpeg, frameEnd: at + 9 } } : undefined;
|
|
38
|
+
}
|
|
39
|
+
if (marker === 0xda) return undefined;
|
|
40
|
+
at += 2 + length;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** An image's signature and dimensions, without deciding whether a vendor accepts that format or size. */
|
|
46
|
+
export function readImageHeader(bytes: Uint8Array): (ImageHeader & { type: ImageType }) | undefined {
|
|
47
|
+
let type: ImageType;
|
|
48
|
+
if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) type = 'image/png';
|
|
49
|
+
else if (tag(bytes, 0).startsWith('GIF')) type = 'image/gif';
|
|
50
|
+
else if (bytes[0] === 0xff && bytes[1] === 0xd8) type = 'image/jpeg';
|
|
51
|
+
else if (tag(bytes, 0) === 'RIFF' && tag(bytes, 8) === 'WEBP') type = 'image/webp';
|
|
52
|
+
else return undefined;
|
|
53
|
+
const dimensions = imageDimensions(bytes, type);
|
|
54
|
+
return dimensions ? { type, ...dimensions } : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** ISO BMFF's bounded top-level boxes and movie headers, with raw duration and 16.16 track dimensions. */
|
|
58
|
+
export function readMp4Header(bytes: Uint8Array): {
|
|
59
|
+
fileType: boolean;
|
|
60
|
+
hasMediaData: boolean;
|
|
61
|
+
movies: Array<{ headers: Array<{ timescale: number; duration: number; payloadSize: number }>; tracks: Dimensions[] }>;
|
|
62
|
+
} {
|
|
63
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
64
|
+
function* boxes(from: number, to: number): Generator<{ type: string; start: number; end: number }> {
|
|
65
|
+
let at = from;
|
|
66
|
+
while (at + 8 <= to) {
|
|
67
|
+
let size = view.getUint32(at);
|
|
68
|
+
const type = tag(bytes, at + 4);
|
|
69
|
+
let header = 8;
|
|
70
|
+
if (size === 1) {
|
|
71
|
+
if (at + 16 > to) return;
|
|
72
|
+
size = Number(view.getBigUint64(at + 8));
|
|
73
|
+
header = 16;
|
|
74
|
+
}
|
|
75
|
+
if (size === 0) size = to - at;
|
|
76
|
+
if (!Number.isSafeInteger(size) || size < header || size > to - at) return;
|
|
77
|
+
yield { type, start: at + header, end: at + size };
|
|
78
|
+
at += size;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const result: ReturnType<typeof readMp4Header> = { fileType: bytes.length >= 8 && tag(bytes, 4) === 'ftyp', hasMediaData: false, movies: [] };
|
|
82
|
+
for (const box of boxes(0, bytes.length)) {
|
|
83
|
+
if (box.type === 'mdat' && box.end > box.start) result.hasMediaData = true;
|
|
84
|
+
if (box.type !== 'moov') continue;
|
|
85
|
+
const movie: (typeof result.movies)[number] = { headers: [], tracks: [] };
|
|
86
|
+
result.movies.push(movie);
|
|
87
|
+
for (const child of boxes(box.start, box.end)) {
|
|
88
|
+
const { type, start, end } = child;
|
|
89
|
+
if (type === 'mvhd' && start < end) {
|
|
90
|
+
const version = bytes[start];
|
|
91
|
+
const payloadSize = end - start;
|
|
92
|
+
if (version === 0 && payloadSize >= 20) movie.headers.push({ timescale: view.getUint32(start + 12), duration: view.getUint32(start + 16), payloadSize });
|
|
93
|
+
if (version === 1 && payloadSize >= 32) movie.headers.push({ timescale: view.getUint32(start + 20), duration: Number(view.getBigUint64(start + 24)), payloadSize });
|
|
94
|
+
}
|
|
95
|
+
if (type !== 'trak') continue;
|
|
96
|
+
for (const track of boxes(start, end)) {
|
|
97
|
+
if (track.type !== 'tkhd' || track.start >= track.end) continue;
|
|
98
|
+
const version = bytes[track.start];
|
|
99
|
+
const at = version === 1 ? track.start + 88 : version === 0 ? track.start + 76 : track.end;
|
|
100
|
+
if (at + 8 > track.end) continue;
|
|
101
|
+
const width = view.getUint32(at) / 65536;
|
|
102
|
+
const height = view.getUint32(at + 4) / 65536;
|
|
103
|
+
movie.tracks.push({ width, height });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
package/src/pack-fetch.ts
CHANGED
|
@@ -105,7 +105,7 @@ export type LanePart = {
|
|
|
105
105
|
* World's scenario file for a model vendor's turns (`scenarioPath`, the runtime's `--scenario`). */
|
|
106
106
|
export type PackFetchOptions = {
|
|
107
107
|
root?: string; readOnly?: boolean; clock?: () => string; database?: string; scenarioPath?: string;
|
|
108
|
-
/** the application a walk stands in for
|
|
108
|
+
/** the application a walk stands in for: this fetch's requests and declared events are answered by it */
|
|
109
109
|
application?: ApplicationStandIn;
|
|
110
110
|
};
|
|
111
111
|
|
|
@@ -143,7 +143,7 @@ function hostMatcher(pattern: string): (host: string) => Match | undefined {
|
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
145
|
/** The host a request is for: the vendor host the World's injector names, else its own. */
|
|
146
|
-
const hostOf = (request: Request): string => readHostPort(request.headers.get('x-volter-twin-original-host') ?? request.headers.get('host') ?? new URL(request.url).host)?.host ?? '';
|
|
146
|
+
export const hostOf = (request: Request): string => readHostPort(request.headers.get('x-volter-twin-original-host') ?? request.headers.get('host') ?? new URL(request.url).host)?.host ?? '';
|
|
147
147
|
|
|
148
148
|
/** Where a workspace asked `?show=<type>:<id>` opens the record (architecture, "Workspace"): the built workspace page
|
|
149
149
|
* that shows that type, its parameter the id; else the one that lists the type; else the workspace's home (its root
|
|
@@ -218,13 +218,26 @@ async function encoded(m: DerivedManifest, request: Request, res: Response): Pro
|
|
|
218
218
|
return new Response(buffered, { status: res.status, statusText: res.statusText, headers });
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
/** A pack's HTTP surface, as the kernel serves it from the pack's parts. */
|
|
222
221
|
/** A door's path as it is served and listed: under `/_twin/`, whether the manifest writes the prefix or not. */
|
|
223
222
|
const doorPath = (path: string): string => (path.startsWith('/_twin/') ? path : `/_twin${path.startsWith('/') ? '' : '/'}${path}`);
|
|
224
223
|
|
|
224
|
+
/** A declared World door, matched by the same normalized path for serving and response ownership. */
|
|
225
|
+
export function worldDoorMatcher<Door extends { method: string; path: string }>(doors: ReadonlyArray<Door>): (method: string, path: string) => { door: Door; params: Record<string, string> } | undefined {
|
|
226
|
+
const routes = doors.map((door) => ({ door, method: door.method.toUpperCase(), matches: pathMatcher(doorPath(door.path), false) }));
|
|
227
|
+
return (method, path) => {
|
|
228
|
+
const at = path.replace(/\/+$/, '') || '/';
|
|
229
|
+
for (const route of routes) {
|
|
230
|
+
if (route.method !== method.toUpperCase()) continue;
|
|
231
|
+
const hit = route.matches(at);
|
|
232
|
+
if (hit) return { door: route.door, params: hit.params };
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
225
238
|
/** A request with its path's repeated slashes merged, as a front that merges them routes it (the manifest's
|
|
226
239
|
* `mergeSlashes`); one with none is itself. */
|
|
227
|
-
function slashesMerged(request: Request): Request {
|
|
240
|
+
export function slashesMerged(request: Request): Request {
|
|
228
241
|
const url = new URL(request.url);
|
|
229
242
|
const merged = url.pathname.replace(/\/{2,}/g, '/');
|
|
230
243
|
if (merged === url.pathname) return request;
|
|
@@ -232,6 +245,58 @@ function slashesMerged(request: Request): Request {
|
|
|
232
245
|
return new Request(url, request);
|
|
233
246
|
}
|
|
234
247
|
|
|
248
|
+
/** The selected unit's request transformation, shared by serving and response judgment. */
|
|
249
|
+
export function unitRequestRouter(m: Pick<DerivedManifest, 'pathPrefix' | 'basePathAlias' | 'pathFromHost' | 'hostParams' | 'headerParams'>, surface: DerivedSurface, routes: ReturnType<typeof compileSurface>): (request: Request) => Request {
|
|
250
|
+
const prefix = m.pathPrefix ? new RegExp(`^(?:${m.pathPrefix})(?=/)`) : undefined;
|
|
251
|
+
const base = surface.basePath ?? '';
|
|
252
|
+
const baseAlias = m.basePathAlias && base ? new RegExp(`^(?:${m.basePathAlias})(?=/)`) : undefined;
|
|
253
|
+
const hostPath = m.pathFromHost ? new RegExp(m.pathFromHost, 'i') : undefined;
|
|
254
|
+
const hostParams = m.hostParams ? new RegExp(m.hostParams, 'i') : undefined;
|
|
255
|
+
// each operation's spanning parameters, keyed by the operation and the parameter as the route compiles them (a whole
|
|
256
|
+
// segment's label whose name the surface or the manifest declares spanning), so one operation's spanning name never
|
|
257
|
+
// keeps another's slashes, nor a label that only part of a segment holds
|
|
258
|
+
const spanningNames = new Set(surface.spanning ?? []);
|
|
259
|
+
const spanningOf = new Set(surface.operations.flatMap((op) => ((op.basePath ?? base) + op.path).split('/').flatMap((seg) => {
|
|
260
|
+
const label = /^\{([^}]+)\}$/.exec(seg)?.[1];
|
|
261
|
+
return label !== undefined && spanningNames.has(label) ? [`${op.id}\u0000${label}`] : [];
|
|
262
|
+
})));
|
|
263
|
+
/** A path as the vendor routes it: the manifest's path prefix taken off, the spec's base in an alias's place, a host's
|
|
264
|
+
* path label (pathFromHost) put in front. */
|
|
265
|
+
const rewritten = (pathname: string, host: string): string => {
|
|
266
|
+
let path = pathname;
|
|
267
|
+
if (prefix) path = path.replace(prefix, '') || '/';
|
|
268
|
+
// a base the vendor serves as its own: the spec's base in its place (a path already under the spec's base stays)
|
|
269
|
+
if (baseAlias && !(path === base || path.startsWith(`${base}/`))) path = path.replace(baseAlias, base);
|
|
270
|
+
const label = hostPath?.exec(host)?.[1];
|
|
271
|
+
if (label) path = `/${label}${path === '/' ? '' : path}`;
|
|
272
|
+
return path;
|
|
273
|
+
};
|
|
274
|
+
/** The request as its vendor routes it: duplicate slashes collapsed, unless the rewritten path matches an operation
|
|
275
|
+
* through one of that operation's spanning parameters (the surface's and the manifest's), whose value keeps its
|
|
276
|
+
* slashes; then rewritten. */
|
|
277
|
+
return (request: Request): Request => {
|
|
278
|
+
const url = new URL(request.url);
|
|
279
|
+
const host = hostOf(request);
|
|
280
|
+
const kept = rewritten(url.pathname, host);
|
|
281
|
+
const matched = matchOperation(routes, request.method, kept);
|
|
282
|
+
const spanning = matched !== undefined && Object.keys(matched.params).some((name) => spanningOf.has(`${matched.operation.id}\u0000${name}`));
|
|
283
|
+
const source = spanning ? url.pathname : url.pathname.replace(/\/{2,}/g, '/');
|
|
284
|
+
// a prefix's named groups (Jira's `/ex/jira/(?<cloudId>[^/]+)`) and the host's (E2B's envd at
|
|
285
|
+
// `49983-(?<sandboxID>…).e2b.app`) join the operation's parameters
|
|
286
|
+
const groups = { ...(prefix?.exec(source)?.groups ?? {}), ...(hostParams?.exec(host)?.groups ?? {}), ...Object.fromEntries(Object.entries(m.headerParams ?? {}).flatMap(([param, header]) => { const value = request.headers.get(header); return value === null ? [] : [[param, value]]; })) };
|
|
287
|
+
const path = spanning ? kept : rewritten(source, host);
|
|
288
|
+
if (path === url.pathname && !Object.keys(groups).length) return request;
|
|
289
|
+
// the path the client sent, which a signature over the request (SigV4's canonical URI) covers
|
|
290
|
+
const headers = new Headers(request.headers);
|
|
291
|
+
// the path as first sent: a front before this (an attach bridge) may have named the one its client signed
|
|
292
|
+
if (!headers.has(ORIGINAL_PATH_HEADER)) headers.set(ORIGINAL_PATH_HEADER, url.pathname);
|
|
293
|
+
if (Object.keys(groups).length) headers.set(PREFIX_PARAMS_HEADER, JSON.stringify(groups));
|
|
294
|
+
url.pathname = path;
|
|
295
|
+
return new Request(url, { method: request.method, headers, body: request.body, signal: request.signal, ...(request.body ? { duplex: 'half' } : {}) } as RequestInit);
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** A pack's HTTP surface, as the kernel serves it from the pack's parts. */
|
|
235
300
|
export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}): DerivedFetch {
|
|
236
301
|
const m = parts.manifest;
|
|
237
302
|
const scope: CoreScope = {
|
|
@@ -322,11 +387,8 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
322
387
|
if (decision.kind === 'fault') return Response.json(decision.result.body, { status: decision.result.status, headers: decision.result.headers ?? {} });
|
|
323
388
|
return api(withHeaders(request, new Headers([...request.headers, [SCENARIO_DECISION_HEADER, scenarioDecisionHeader(decision)]])));
|
|
324
389
|
};
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
if (!answer) throw new Error(`${m.vendor}: the manifest declares the door ${d.id}, and semantics/doors.ts exports no ${d.id}`);
|
|
328
|
-
return { decl: d, path: pathMatcher(doorPath(d.path), false), answer };
|
|
329
|
-
});
|
|
390
|
+
for (const d of m.doors ?? []) if (!parts.doors?.[d.id]) throw new Error(`${m.vendor}: the manifest declares the door ${d.id}, and semantics/doors.ts exports no ${d.id}`);
|
|
391
|
+
const door = worldDoorMatcher(m.doors ?? []);
|
|
330
392
|
const screens = (m.screens ?? []).filter((s) => s.status === 'done' && parts.screens?.[s.id]).map((s) => {
|
|
331
393
|
const hosts = [s.host, ...(s.hosts ?? [])].map(hostMatcher);
|
|
332
394
|
return { decl: s, host: (h: string) => hosts.map((match) => match(h)).find(Boolean), path: pathMatcher(s.path, true), answer: parts.screens![s.id]! };
|
|
@@ -355,53 +417,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
355
417
|
};
|
|
356
418
|
const refusedReadOnly = (): Response => vendorError(m, m.readOnly);
|
|
357
419
|
|
|
358
|
-
const
|
|
359
|
-
const base = surface.basePath ?? '';
|
|
360
|
-
const baseAlias = m.basePathAlias && base ? new RegExp(`^(?:${m.basePathAlias})(?=/)`) : undefined;
|
|
361
|
-
const hostPath = m.pathFromHost ? new RegExp(m.pathFromHost, 'i') : undefined;
|
|
362
|
-
const hostParams = m.hostParams ? new RegExp(m.hostParams, 'i') : undefined;
|
|
363
|
-
// each operation's spanning parameters, keyed by the operation and the parameter as the route compiles them (a whole
|
|
364
|
-
// segment's label whose name the surface or the manifest declares spanning), so one operation's spanning name never
|
|
365
|
-
// keeps another's slashes, nor a label that only part of a segment holds
|
|
366
|
-
const spanningNames = new Set(surface.spanning ?? []);
|
|
367
|
-
const spanningOf = new Set(surface.operations.flatMap((op) => ((op.basePath ?? base) + op.path).split('/').flatMap((seg) => {
|
|
368
|
-
const label = /^\{([^}]+)\}$/.exec(seg)?.[1];
|
|
369
|
-
return label !== undefined && spanningNames.has(label) ? [`${op.id}\u0000${label}`] : [];
|
|
370
|
-
})));
|
|
371
|
-
/** A path as the vendor routes it: the manifest's path prefix taken off, the spec's base in an alias's place, a host's
|
|
372
|
-
* path label (pathFromHost) put in front. */
|
|
373
|
-
const rewritten = (pathname: string, host: string): string => {
|
|
374
|
-
let path = pathname;
|
|
375
|
-
if (prefix) path = path.replace(prefix, '') || '/';
|
|
376
|
-
// a base the vendor serves as its own: the spec's base in its place (a path already under the spec's base stays)
|
|
377
|
-
if (baseAlias && !(path === base || path.startsWith(`${base}/`))) path = path.replace(baseAlias, base);
|
|
378
|
-
const label = hostPath?.exec(host)?.[1];
|
|
379
|
-
if (label) path = `/${label}${path === '/' ? '' : path}`;
|
|
380
|
-
return path;
|
|
381
|
-
};
|
|
382
|
-
/** The request as its vendor routes it: duplicate slashes collapsed, unless the rewritten path matches an operation
|
|
383
|
-
* through one of that operation's spanning parameters (the surface's and the manifest's), whose value keeps its
|
|
384
|
-
* slashes; then rewritten. */
|
|
385
|
-
const routed = (request: Request): Request => {
|
|
386
|
-
const url = new URL(request.url);
|
|
387
|
-
const host = hostOf(request);
|
|
388
|
-
const kept = rewritten(url.pathname, host);
|
|
389
|
-
const matched = matchOperation(routes, request.method, kept);
|
|
390
|
-
const spanning = matched !== undefined && Object.keys(matched.params).some((name) => spanningOf.has(`${matched.operation.id}\u0000${name}`));
|
|
391
|
-
const source = spanning ? url.pathname : url.pathname.replace(/\/{2,}/g, '/');
|
|
392
|
-
// a prefix's named groups (Jira's `/ex/jira/(?<cloudId>[^/]+)`) and the host's (E2B's envd at
|
|
393
|
-
// `49983-(?<sandboxID>…).e2b.app`) join the operation's parameters
|
|
394
|
-
const groups = { ...(prefix?.exec(source)?.groups ?? {}), ...(hostParams?.exec(host)?.groups ?? {}), ...Object.fromEntries(Object.entries(m.headerParams ?? {}).flatMap(([param, header]) => { const value = request.headers.get(header); return value === null ? [] : [[param, value]]; })) };
|
|
395
|
-
const path = spanning ? kept : rewritten(source, host);
|
|
396
|
-
if (path === url.pathname && !Object.keys(groups).length) return request;
|
|
397
|
-
// the path the client sent, which a signature over the request (SigV4's canonical URI) covers
|
|
398
|
-
const headers = new Headers(request.headers);
|
|
399
|
-
// the path as first sent: a front before this (an attach bridge) may have named the one its client signed
|
|
400
|
-
if (!headers.has(ORIGINAL_PATH_HEADER)) headers.set(ORIGINAL_PATH_HEADER, url.pathname);
|
|
401
|
-
if (Object.keys(groups).length) headers.set(PREFIX_PARAMS_HEADER, JSON.stringify(groups));
|
|
402
|
-
url.pathname = path;
|
|
403
|
-
return new Request(url, { method: request.method, headers, body: request.body, signal: request.signal, ...(request.body ? { duplex: 'half' } : {}) } as RequestInit);
|
|
404
|
-
};
|
|
420
|
+
const routed = unitRequestRouter(m, surface, routes);
|
|
405
421
|
const rootServes = (method: string, path: string): boolean => Boolean(matchOperation(routes, method, path));
|
|
406
422
|
const lane = m.lanes ? laneRouter(m.vendor, m.lanes, parts.lanes ?? {}, rootServes) : undefined;
|
|
407
423
|
// Inspect lane-owned World doors directly, without routing them back through the vendor gateway.
|
|
@@ -488,7 +504,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
488
504
|
const own = await answerContent(arrived);
|
|
489
505
|
if (own) return own;
|
|
490
506
|
// Root World doors belong to the pack at every routed host, including a lane's host.
|
|
491
|
-
const rootDoor =
|
|
507
|
+
const rootDoor = door(arrived.method, asked);
|
|
492
508
|
const toLane = discovery || rootDoor ? undefined : lane?.(arrived);
|
|
493
509
|
if (toLane) return toLane(arrived);
|
|
494
510
|
const readable = await prepared(arrived);
|
|
@@ -551,12 +567,11 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
551
567
|
if (kind === 'fake') resetFakePool(m.service, options.root);
|
|
552
568
|
return Response.json({ kind }, { status: 201 });
|
|
553
569
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const hit = d.path(path);
|
|
557
|
-
if (!hit) continue;
|
|
570
|
+
const hit = door(request.method, path);
|
|
571
|
+
if (hit) {
|
|
558
572
|
if (readOnly && request.method !== 'GET') return refusedReadOnly();
|
|
559
|
-
|
|
573
|
+
const d = hit.door;
|
|
574
|
+
return parts.doors![d.id]!(await open(request, d.id, d.method, d.path, hit.params));
|
|
560
575
|
}
|
|
561
576
|
return gapAnswer(m, request);
|
|
562
577
|
}
|
|
@@ -717,30 +732,16 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
717
732
|
return derivedRequestScopes(m, Object.assign(served, { owners, ownersByUnit, serves, advanceClock: async (request: Request) => runWithStorageTenant(m.service, await selectedAccount(request), () => advanceClock(request)), workspaces, boards: Boolean(parts.board), maxRequestBodySize: m.descriptor?.maxRequestBodySize, draws: drawsUrl, contentHosts, ...(upgrade ? { upgrade } : {}), ...(ingest ? { ingest } : {}) }), options.readOnly);
|
|
718
733
|
}
|
|
719
734
|
|
|
720
|
-
/** The
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const served = (name: string): ((request: Request) => Promise<Response>) => {
|
|
724
|
-
const part = lanes[name];
|
|
725
|
-
if (!part) throw new Error(`${vendor}: the manifest routes to the lane ${name}, and the vendor's fetch is given no ${name}`);
|
|
726
|
-
// the lane is named on the request, so a write it makes records which lane's API made it (LANE_HEADER)
|
|
727
|
-
const named = (request: Request): Promise<Response> => {
|
|
728
|
-
const headers = new Headers(request.headers);
|
|
729
|
-
headers.set(LANE_HEADER, name);
|
|
730
|
-
return part.fetch(withHeaders(request, headers));
|
|
731
|
-
};
|
|
732
|
-
return decl.cors ? withCors(decl.cors, named) : named;
|
|
733
|
-
};
|
|
734
|
-
const doors = Object.entries(lanes).flatMap(([name, part]) => (part.manifest.doors ?? []).map((d) => ({
|
|
735
|
-
name, method: d.method.toUpperCase(), matches: pathMatcher(d.path.startsWith('/_twin/') ? d.path : `/_twin/${d.path.replace(/^\//, '')}`, false),
|
|
736
|
-
})));
|
|
735
|
+
/** The lane selected by the vendor gateway, shared by serving and response judgment. */
|
|
736
|
+
export function laneSelector(decl: LanesDecl, lanes: Record<string, { manifest: LanePart['manifest']; fetch: Pick<LanePart['fetch'], 'serves' | 'contentHosts'> }>, rootServes: (method: string, path: string) => boolean = () => false): (request: Request) => string | undefined {
|
|
737
|
+
const door = worldDoorMatcher(Object.entries(lanes).flatMap(([name, part]) => (part.manifest.doors ?? []).map((d) => ({ ...d, name }))));
|
|
737
738
|
const laneRoutes = decl.routes.map((r) => {
|
|
738
739
|
const at = r.path?.replace(/\/+$/, '');
|
|
739
740
|
const host = r.host !== undefined ? new RegExp(r.host, 'i') : undefined;
|
|
740
741
|
const except = (r.exceptHosts ?? []).map((h) => h.toLowerCase());
|
|
741
742
|
const own = lanes[r.lane]?.fetch.serves;
|
|
742
743
|
return {
|
|
743
|
-
lane:
|
|
744
|
+
lane: r.lane,
|
|
744
745
|
holds: (request: Request, path: string, name: string): boolean =>
|
|
745
746
|
(at === undefined || path === at || path.startsWith(`${at}/`))
|
|
746
747
|
&& (host === undefined || host.test(name))
|
|
@@ -749,21 +750,42 @@ function laneRouter(vendor: string, decl: LanesDecl, lanes: Record<string, LaneP
|
|
|
749
750
|
&& !(r.unlessOnlyRoot && rootServes(request.method, path) && !(own?.(request.method, path) ?? false)),
|
|
750
751
|
};
|
|
751
752
|
});
|
|
752
|
-
const fallback = decl.default !== undefined ? served(decl.default) : undefined;
|
|
753
753
|
return (request) => {
|
|
754
754
|
// `<base>/@<host>/<path>` at a World's place, for a host a lane's built content screen names, goes to that lane, before
|
|
755
755
|
// any lane is picked by the request's host or path: its own fetch answers it by its content route, under the vendor's
|
|
756
756
|
// host rules (architecture, "A page at a World's place")
|
|
757
757
|
const address = request.headers.has(TWIN_PREFIX_HEADER) ? contentAddressOf(new URL(request.url).pathname) : undefined;
|
|
758
758
|
const content = address && Object.keys(lanes).find((l) => lanes[l]!.fetch.contentHosts?.includes(address.host));
|
|
759
|
-
if (content) return
|
|
759
|
+
if (content) return content;
|
|
760
760
|
const path = new URL(request.url).pathname.replace(/\/{2,}/g, '/');
|
|
761
761
|
if (path.startsWith('/_twin/')) {
|
|
762
|
-
const
|
|
763
|
-
if (
|
|
762
|
+
const hit = door(request.method, path);
|
|
763
|
+
if (hit) return hit.door.name;
|
|
764
764
|
}
|
|
765
765
|
const name = hostOf(request);
|
|
766
|
-
return laneRoutes.find((r) => r.holds(request, path, name))?.lane ??
|
|
766
|
+
return laneRoutes.find((r) => r.holds(request, path, name))?.lane ?? decl.default;
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** The vendor's gateway over its lanes: the lane a request goes to (a door to the lane that declares it, then the first
|
|
771
|
+
* route whose conditions all hold, then the default), behind the gateway's CORS; undefined when none takes it. */
|
|
772
|
+
function laneRouter(vendor: string, decl: LanesDecl, lanes: Record<string, LanePart>, rootServes: (method: string, path: string) => boolean = () => false): (request: Request) => ((request: Request) => Promise<Response>) | undefined {
|
|
773
|
+
const served = (name: string): ((request: Request) => Promise<Response>) => {
|
|
774
|
+
const part = lanes[name];
|
|
775
|
+
if (!part) throw new Error(`${vendor}: the manifest routes to the lane ${name}, and the vendor's fetch is given no ${name}`);
|
|
776
|
+
// the lane is named on the request, so a write it makes records which lane's API made it (LANE_HEADER)
|
|
777
|
+
const named = (request: Request): Promise<Response> => {
|
|
778
|
+
const headers = new Headers(request.headers);
|
|
779
|
+
headers.set(LANE_HEADER, name);
|
|
780
|
+
return part.fetch(withHeaders(request, headers));
|
|
781
|
+
};
|
|
782
|
+
return decl.cors ? withCors(decl.cors, named) : named;
|
|
783
|
+
};
|
|
784
|
+
const selected = laneSelector(decl, lanes, rootServes);
|
|
785
|
+
const handlers = new Map([...new Set([...decl.routes.map((route) => route.lane), ...(decl.default === undefined ? [] : [decl.default]), ...Object.keys(lanes)])].map((name) => [name, served(name)]));
|
|
786
|
+
return (request) => {
|
|
787
|
+
const name = selected(request);
|
|
788
|
+
return name === undefined ? undefined : handlers.get(name);
|
|
767
789
|
};
|
|
768
790
|
}
|
|
769
791
|
|
package/src/packRegistry.ts
CHANGED
|
@@ -221,7 +221,13 @@ export type TwinPack = {
|
|
|
221
221
|
* answer: `init` writes `$issue:<service>` for each, and `up`, once this twin is running, POSTs the body to the door
|
|
222
222
|
* and sets each name to its field (`file:<field>` materializes one file; `directory:<field>` a relative-file map). A twin that holds its keys (refusing any it
|
|
223
223
|
* did not issue) declares one, so a World's applications hold keys it issued rather than fixtures it refuses. */
|
|
224
|
-
credentialDoor?: {
|
|
224
|
+
credentialDoor?: {
|
|
225
|
+
path: string;
|
|
226
|
+
body: Record<string, unknown>;
|
|
227
|
+
fill: Record<string, string>;
|
|
228
|
+
account?: { field: string } & ({ tenancy?: never; source?: never } | { tenancy: string; source: string });
|
|
229
|
+
deployment?: { field: string; object: string; source: string; organizationOutputs?: string[] };
|
|
230
|
+
};
|
|
225
231
|
/** Another pack of this vendor issues the API's credentials; architecture, descriptor World wiring. */
|
|
226
232
|
credentialIssuer?: string;
|
|
227
233
|
prismaAdapter?: { adapter: string; export: string; urlEnv: string };
|
package/src/request-body.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The default is Bun's existing largest plain body (128 MiB), so both runtimes decode the same.
|
|
1
|
+
/** The shared HTTP transport and compressed-request output retain Bun's 128 MiB plain-body default.
|
|
3
2
|
* Source: bun-types/serve.d.ts, ServeOptions.maxRequestBodySize (@default 1024 * 1024 * 128),
|
|
4
3
|
* https://github.com/oven-sh/bun/blob/main/packages/bun-types/serve.d.ts .
|
|
5
|
-
* A pack's descriptor.maxRequestBodySize overrides
|
|
4
|
+
* A pack's descriptor.maxRequestBodySize overrides both the transport and decoded-output limits. */
|
|
6
5
|
export const DEFAULT_MAX_DECODED_REQUEST_BODY_SIZE = 128 * 1024 * 1024;
|
|
7
6
|
|
|
8
7
|
export class RequestBodyTooLargeError extends Error {
|