@volter/world-core 3.0.156 → 3.0.158

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 (74) hide show
  1. package/attach.cjs +2 -0
  2. package/capability-decision.cjs +6 -4
  3. package/dist/attach.cjs +2 -0
  4. package/dist/capability-decision.cjs +6 -4
  5. package/dist/generated/pack-facts.json +77 -8
  6. package/dist/network-policy.cjs +11 -1
  7. package/dist/network-policy.d.cts +1 -0
  8. package/dist/pack-facts.d.cts +3 -0
  9. package/dist/required-credential-transport.cjs +29 -0
  10. package/dist/required-credential-transport.d.cts +3 -0
  11. package/dist/src/blob-store.d.ts +16 -0
  12. package/dist/src/blob-store.js +76 -1
  13. package/dist/src/derived-core.d.ts +72 -6
  14. package/dist/src/derived-core.js +192 -51
  15. package/dist/src/derived-real.js +97 -15
  16. package/dist/src/derived.d.ts +5 -0
  17. package/dist/src/executor.d.ts +7 -0
  18. package/dist/src/executor.js +15 -1
  19. package/dist/src/git/lfs.d.ts +13 -3
  20. package/dist/src/git/lfs.js +41 -9
  21. package/dist/src/head.d.ts +2 -0
  22. package/dist/src/head.js +6 -0
  23. package/dist/src/index.d.ts +1 -0
  24. package/dist/src/index.js +1 -0
  25. package/dist/src/live/ingest.js +9 -13
  26. package/dist/src/live/rtmp.js +7 -5
  27. package/dist/src/live/segmenter.js +7 -1
  28. package/dist/src/machines.js +5 -1
  29. package/dist/src/managed-go-programs.d.ts +55 -0
  30. package/dist/src/managed-go-programs.js +133 -0
  31. package/dist/src/mongodb/stream.d.ts +23 -0
  32. package/dist/src/pack-fetch.d.ts +6 -1
  33. package/dist/src/pack-fetch.js +53 -7
  34. package/dist/src/pack-paths.d.ts +2 -1
  35. package/dist/src/pack-paths.js +6 -1
  36. package/dist/src/resource-blob.d.ts +4 -0
  37. package/dist/src/resource-blob.js +8 -1
  38. package/dist/src/runtime.d.ts +3 -3
  39. package/dist/src/runtime.js +2 -2
  40. package/dist/src/serve-http.js +53 -3
  41. package/dist/src/sockets.d.ts +4 -1
  42. package/dist/src/sockets.js +59 -6
  43. package/dist/src/vendor-call.js +58 -0
  44. package/dist/src/world-store.d.ts +15 -1
  45. package/dist/src/world-store.js +78 -2
  46. package/generated/pack-facts.json +77 -8
  47. package/network-policy.cjs +11 -1
  48. package/network-policy.d.cts +1 -0
  49. package/pack-facts.d.cts +3 -0
  50. package/package.json +8 -1
  51. package/required-credential-transport.cjs +29 -0
  52. package/required-credential-transport.d.cts +3 -0
  53. package/src/blob-store.ts +59 -1
  54. package/src/derived-core.ts +215 -51
  55. package/src/derived-real.ts +80 -14
  56. package/src/derived.ts +6 -1
  57. package/src/executor.ts +17 -3
  58. package/src/git/lfs.ts +40 -9
  59. package/src/head.ts +6 -0
  60. package/src/index.ts +1 -0
  61. package/src/live/ingest.ts +7 -3
  62. package/src/live/rtmp.ts +7 -5
  63. package/src/live/segmenter.ts +7 -1
  64. package/src/machines.ts +5 -1
  65. package/src/managed-go-programs.ts +146 -0
  66. package/src/mongodb/stream.ts +10 -0
  67. package/src/pack-fetch.ts +50 -9
  68. package/src/pack-paths.ts +5 -1
  69. package/src/resource-blob.ts +9 -1
  70. package/src/runtime.ts +4 -2
  71. package/src/serve-http.ts +43 -3
  72. package/src/sockets.ts +39 -8
  73. package/src/vendor-call.ts +54 -0
  74. package/src/world-store.ts +66 -2
@@ -0,0 +1,55 @@
1
+ export interface NativeGoKitReference {
2
+ package: {
3
+ name: string;
4
+ version: string;
5
+ };
6
+ target: 'wasm32-wali-go-single-thread';
7
+ releaseSha256: string;
8
+ producerSha256: string;
9
+ sourceSha256: string;
10
+ selection: {
11
+ path: string;
12
+ sha256: string;
13
+ };
14
+ recipe: {
15
+ path: string;
16
+ sha256: string;
17
+ };
18
+ installedRoot: string;
19
+ }
20
+ export interface NativeGoOperation {
21
+ sourceService: string;
22
+ role: 'server' | 'prerequisite' | 'health';
23
+ kit: string;
24
+ program: {
25
+ name: string;
26
+ version: string;
27
+ manifestId: string;
28
+ manifestSha256: string;
29
+ path: string;
30
+ };
31
+ /** Resolved arguments require separate comparison with the full source. */
32
+ args: string[];
33
+ /** Names of existing World values, never caller secrets in this record. */
34
+ environmentBindings: Record<string, string>;
35
+ requires: {
36
+ service: string;
37
+ condition: 'service_started' | 'service_healthy' | 'service_completed_successfully';
38
+ }[];
39
+ }
40
+ export interface NativeGoReference {
41
+ version: 2;
42
+ implementation: string;
43
+ release: string;
44
+ directory: string;
45
+ snapshotSha256: string;
46
+ declarationSha256: string;
47
+ image: string;
48
+ kits: Record<string, NativeGoKitReference>;
49
+ server: NativeGoOperation;
50
+ prerequisites: NativeGoOperation[];
51
+ health: NativeGoOperation;
52
+ }
53
+ /** All fields are declared data. The later owned consumer must authenticate
54
+ * the complete source, kit, original image and admitted installed manifests. */
55
+ export declare function nativeGoReference(value: unknown): NativeGoReference;
@@ -0,0 +1,133 @@
1
+ /** Tagged managed Go operation metadata. This structural reader grants no
2
+ * source/image correspondence, filesystem custody, catalog admission or exec. */
3
+ import { posix } from 'node:path';
4
+ const fail = (field) => { throw new Error('Native Go operation metadata is invalid: ' + field); };
5
+ function object(value, fields, label) {
6
+ if (!value || typeof value !== 'object' || Array.isArray(value))
7
+ return fail(label);
8
+ const row = value;
9
+ if (Object.keys(row).length !== fields.length || fields.some(key => !Object.hasOwn(row, key)))
10
+ return fail(label);
11
+ return row;
12
+ }
13
+ function text(value, label) {
14
+ if (typeof value !== 'string' || !value || value.includes('\0'))
15
+ return fail(label);
16
+ return value;
17
+ }
18
+ function digest(value, label) {
19
+ const out = text(value, label);
20
+ if (!/^[a-f0-9]{64}$/u.test(out))
21
+ return fail(label);
22
+ return out;
23
+ }
24
+ function relativePath(value, label) {
25
+ const out = text(value, label);
26
+ if (out.startsWith('/') || out.includes('\\') || posix.normalize(out) !== out
27
+ || out.split('/').some(part => !part || part === '.' || part === '..'))
28
+ return fail(label);
29
+ return out;
30
+ }
31
+ function installedPath(value, label) {
32
+ const out = text(value, label);
33
+ if (!out.startsWith('/opt/programs/') || out.includes('\\') || posix.normalize(out) !== out
34
+ || out.split('/').slice(1).some(part => !part || part === '.' || part === '..'))
35
+ return fail(label);
36
+ return out;
37
+ }
38
+ function pin(value, label) {
39
+ const row = object(value, ['path', 'sha256'], label);
40
+ return { path: relativePath(row.path, label + '.path'), sha256: digest(row.sha256, label + '.sha256') };
41
+ }
42
+ function kit(value, label) {
43
+ const row = object(value, ['package', 'target', 'releaseSha256', 'producerSha256', 'sourceSha256', 'selection', 'recipe', 'installedRoot'], label);
44
+ const pack = object(row.package, ['name', 'version'], label + '.package');
45
+ if (row.target !== 'wasm32-wali-go-single-thread')
46
+ return fail(label + '.target');
47
+ return { package: { name: text(pack.name, label + '.package.name'), version: text(pack.version, label + '.package.version') },
48
+ target: row.target, releaseSha256: digest(row.releaseSha256, label + '.releaseSha256'),
49
+ producerSha256: digest(row.producerSha256, label + '.producerSha256'), sourceSha256: digest(row.sourceSha256, label + '.sourceSha256'),
50
+ selection: pin(row.selection, label + '.selection'), recipe: pin(row.recipe, label + '.recipe'),
51
+ installedRoot: installedPath(row.installedRoot, label + '.installedRoot') };
52
+ }
53
+ function operation(value, role, kits, label) {
54
+ const row = object(value, ['sourceService', 'role', 'kit', 'program', 'args', 'environmentBindings', 'requires'], label);
55
+ const selected = text(row.kit, label + '.kit');
56
+ if (row.role !== role || !Object.hasOwn(kits, selected))
57
+ return fail(label + '.role/kit');
58
+ const provider = kits[selected];
59
+ const command = object(row.program, ['name', 'version', 'manifestId', 'manifestSha256', 'path'], label + '.program');
60
+ const path = installedPath(command.path, label + '.program.path'), version = text(command.version, label + '.program.version');
61
+ if (version !== provider.package.version || !path.startsWith(provider.installedRoot + '/'))
62
+ return fail(label + '.program placement/version');
63
+ if (!Array.isArray(row.args))
64
+ return fail(label + '.args');
65
+ const args = [];
66
+ for (const arg of row.args) {
67
+ if (typeof arg !== 'string' || arg.includes('\0'))
68
+ return fail(label + '.args.' + args.length);
69
+ args.push(arg);
70
+ }
71
+ if (!row.environmentBindings || typeof row.environmentBindings !== 'object' || Array.isArray(row.environmentBindings))
72
+ return fail(label + '.environmentBindings');
73
+ const bindings = Object.create(null);
74
+ for (const [name, value] of Object.entries(row.environmentBindings)) {
75
+ const source = text(value, label + '.environmentBindings.' + name);
76
+ if (![name, source].every(item => /^[A-Za-z_][A-Za-z0-9_]*$/u.test(item)))
77
+ return fail(label + '.environmentBindings name');
78
+ bindings[name] = source;
79
+ }
80
+ if (!Array.isArray(row.requires))
81
+ return fail(label + '.requires');
82
+ const dependencies = [];
83
+ for (const value of row.requires) {
84
+ const edge = object(value, ['service', 'condition'], label + '.requires');
85
+ if (edge.condition !== 'service_started' && edge.condition !== 'service_healthy' && edge.condition !== 'service_completed_successfully')
86
+ return fail(label + '.requires.condition');
87
+ const service = text(edge.service, label + '.requires.service');
88
+ if (dependencies.some(prior => prior.service === service))
89
+ return fail(label + '.requires duplicate service');
90
+ dependencies.push({ service, condition: edge.condition });
91
+ }
92
+ return { sourceService: text(row.sourceService, label + '.sourceService'), role, kit: selected,
93
+ program: { name: text(command.name, label + '.program.name'), version, manifestId: text(command.manifestId, label + '.program.manifestId'),
94
+ manifestSha256: digest(command.manifestSha256, label + '.program.manifestSha256'), path },
95
+ args, environmentBindings: bindings, requires: dependencies };
96
+ }
97
+ /** All fields are declared data. The later owned consumer must authenticate
98
+ * the complete source, kit, original image and admitted installed manifests. */
99
+ export function nativeGoReference(value) {
100
+ const row = object(value, ['version', 'implementation', 'release', 'directory', 'snapshotSha256', 'declarationSha256', 'image', 'kits', 'server', 'prerequisites', 'health'], 'reference');
101
+ if (row.version !== 2)
102
+ return fail('version');
103
+ const image = text(row.image, 'image');
104
+ if (!/^[^\s@]+@sha256:[a-f0-9]{64}$/u.test(image))
105
+ return fail('image');
106
+ if (!row.kits || typeof row.kits !== 'object' || Array.isArray(row.kits) || !Object.keys(row.kits).length)
107
+ return fail('kits');
108
+ const kits = Object.create(null);
109
+ for (const [name, value] of Object.entries(row.kits))
110
+ kits[text(name, 'kit name')] = kit(value, 'kits.' + name);
111
+ const server = operation(row.server, 'server', kits, 'server'), health = operation(row.health, 'health', kits, 'health');
112
+ const implementation = text(row.implementation, 'implementation'), release = text(row.release, 'release');
113
+ if (kits[server.kit].package.name !== implementation || kits[server.kit].package.version !== release
114
+ || health.sourceService !== server.sourceService)
115
+ return fail('server/health implementation');
116
+ if (!Array.isArray(row.prerequisites))
117
+ return fail('prerequisites');
118
+ const prerequisites = [];
119
+ for (const value of row.prerequisites)
120
+ prerequisites.push(operation(value, 'prerequisite', kits, 'prerequisites.' + prerequisites.length));
121
+ if (new Set(prerequisites.map(item => item.sourceService)).size !== prerequisites.length
122
+ || prerequisites.some(item => item.sourceService === server.sourceService))
123
+ return fail('prerequisite service');
124
+ const usedKits = new Set([server.kit, health.kit, ...prerequisites.map(item => item.kit)]);
125
+ if (Object.keys(kits).some(name => !usedKits.has(name)))
126
+ return fail('unconsumed kit');
127
+ for (const item of prerequisites)
128
+ if (!server.requires.some(edge => edge.service === item.sourceService && edge.condition === 'service_completed_successfully'))
129
+ return fail('server prerequisite completion edge');
130
+ return { version: 2, implementation, release, directory: relativePath(row.directory, 'directory'),
131
+ snapshotSha256: digest(row.snapshotSha256, 'snapshotSha256'), declarationSha256: digest(row.declarationSha256, 'declarationSha256'),
132
+ image, kits, server, prerequisites, health };
133
+ }
@@ -7,6 +7,29 @@ export type MongoCodec = {
7
7
  serialize(document: MongoDocument): Uint8Array;
8
8
  deserialize(bytes: Uint8Array): MongoDocument;
9
9
  };
10
+ /** What a pack's stream reads of the `bson` library its generated fetch's `loadBson` resolves from the pack: the codec,
11
+ * extended JSON, and the number kinds a reply names. The kernel declares the shape and imports nothing of it. */
12
+ export type BsonLibrary = {
13
+ serialize(document: MongoDocument): Uint8Array;
14
+ deserialize(bytes: Uint8Array, options?: {
15
+ promoteValues?: boolean;
16
+ promoteLongs?: boolean;
17
+ bsonRegExp?: boolean;
18
+ }): MongoDocument;
19
+ EJSON: {
20
+ serialize(value: unknown, options?: {
21
+ relaxed?: boolean;
22
+ }): MongoDocument;
23
+ deserialize(value: unknown, options?: {
24
+ relaxed?: boolean;
25
+ }): MongoDocument;
26
+ };
27
+ Double: new (value: number) => unknown;
28
+ Int32: new (value: number) => unknown;
29
+ Long: {
30
+ fromNumber(value: number): unknown;
31
+ };
32
+ };
10
33
  /** What the sender's message said beside its command: whether it will take more than one reply to it (OP_MSG's exhaustAllowed). */
11
34
  export type MongoAsked = {
12
35
  exhaustAllowed: boolean;
@@ -1,4 +1,4 @@
1
- import { type DerivedManifest, type Handler, type HandlerContext, type VendorManifest } from './derived-core.js';
1
+ import { type DerivedManifest, type Handler, type HandlerContext, type ScreenDecl, type VendorManifest } from './derived-core.js';
2
2
  import { type DerivedFetch, type DerivedSurface, type BoardFrame } from './derived.js';
3
3
  import { type PackScenarioAdapter } from './scenario.js';
4
4
  import type { ApplicationStandIn } from './events.js';
@@ -86,5 +86,10 @@ export type PackFetchOptions = {
86
86
  /** the application a walk stands in for (a life's `application` answers): this fetch's `ctx.ask` is answered by it */
87
87
  application?: ApplicationStandIn;
88
88
  };
89
+ /** Where a workspace asked `?show=<type>:<id>` opens the record (architecture, "Workspace"): the built workspace page
90
+ * that shows that type, its parameter the id; else the one that lists the type; else the workspace's home (its root
91
+ * page). Every other parameter a page's path names is the asking page's, and a page that cannot be filled is passed
92
+ * over. Undefined when nothing is asked; the path (with no `show`) otherwise. */
93
+ export declare function shownRecord(url: URL, workspaces: ReadonlyArray<ScreenDecl>, params: Record<string, string>): string | undefined;
89
94
  export declare function createPackFetch(parts: PackParts, options?: PackFetchOptions): DerivedFetch;
90
95
  export declare function createVendorFetch(manifest: VendorManifest, lanes: Record<string, LanePart>): DerivedFetch;
@@ -23,7 +23,7 @@ import { withCors } from "./cors.js";
23
23
  import { contentAddressOf } from "./content-route.js";
24
24
  import { getPack } from "./packRegistry.js";
25
25
  import { hostRules } from '../host-routing.cjs';
26
- import { socketUpgrade } from "./sockets.js";
26
+ import { expireSocketWaits, socketUpgrade } from "./sockets.js";
27
27
  import { liveIngest } from "./live/ingest.js";
28
28
  import { readResourceBlob, removeResourceBlob, writeResourceBlob } from "./resource-blob.js";
29
29
  import { resourcesOfType } from "./derived-core.js";
@@ -71,6 +71,32 @@ function hostMatcher(pattern) {
71
71
  }
72
72
  /** The host a request is for: the vendor host the World's injector names, else its own. */
73
73
  const hostOf = (request) => readHostPort(request.headers.get('x-volter-twin-original-host') ?? request.headers.get('host') ?? new URL(request.url).host)?.host ?? '';
74
+ /** Where a workspace asked `?show=<type>:<id>` opens the record (architecture, "Workspace"): the built workspace page
75
+ * that shows that type, its parameter the id; else the one that lists the type; else the workspace's home (its root
76
+ * page). Every other parameter a page's path names is the asking page's, and a page that cannot be filled is passed
77
+ * over. Undefined when nothing is asked; the path (with no `show`) otherwise. */
78
+ export function shownRecord(url, workspaces, params) {
79
+ const asked = /^([^:]+):(.+)$/.exec(url.searchParams.get('show') ?? '');
80
+ if (!asked)
81
+ return undefined;
82
+ const [, type, id] = asked;
83
+ const fill = (path, extra) => {
84
+ let unfilled = false;
85
+ const filled = path.replace(/\{([^}]+)\}|<([^>]+)>/g, (_, a, b) => {
86
+ const v = { ...params, ...extra }[(a ?? b)];
87
+ if (v === undefined || v === '') {
88
+ unfilled = true;
89
+ return '';
90
+ }
91
+ return encodeURIComponent(v);
92
+ });
93
+ return unfilled ? undefined : filled;
94
+ };
95
+ const shows = workspaces.filter((s) => s.shows?.type === type).map((s) => fill(s.path, { [s.shows.param]: id })).find((p) => p !== undefined);
96
+ const lists = workspaces.filter((s) => s.lists?.includes(type)).map((s) => fill(s.path, {})).find((p) => p !== undefined);
97
+ const home = workspaces.find((s) => s.path.replace(/\/+$/, '') === '') ? '/' : undefined;
98
+ return shows ?? lists ?? home ?? url.pathname;
99
+ }
74
100
  /** The answer for a path no operation, door or screen serves, and for an operation nothing models: the manifest's
75
101
  * `gap`, its `{method}` and `{path}` filled, else the manifest's not-found refusal. */
76
102
  function gapAnswer(m, request, otherMethod = false) {
@@ -120,11 +146,24 @@ async function encoded(m, request, res) {
120
146
  headers.set('content-encoding', 'gzip');
121
147
  headers.delete('content-length');
122
148
  headers.append('vary', 'Accept-Encoding');
123
- return new Response(gzipSync(new Uint8Array(await res.arrayBuffer())), { status: res.status, statusText: res.statusText, headers });
149
+ const body = gzipSync(new Uint8Array(await res.arrayBuffer()));
150
+ // Hold the compressor's buffer with the same finite stream body grammar as decoded(), without copying it.
151
+ const buffered = new ReadableStream({ start(controller) { controller.enqueue(body); controller.close(); } });
152
+ return new Response(buffered, { status: res.status, statusText: res.statusText, headers });
124
153
  }
125
154
  /** A pack's HTTP surface, as the kernel serves it from the pack's parts. */
126
155
  /** A door's path as it is served and listed: under `/_twin/`, whether the manifest writes the prefix or not. */
127
156
  const doorPath = (path) => (path.startsWith('/_twin/') ? path : `/_twin${path.startsWith('/') ? '' : '/'}${path}`);
157
+ /** A request with its path's repeated slashes merged, as a front that merges them routes it (the manifest's
158
+ * `mergeSlashes`); one with none is itself. */
159
+ function slashesMerged(request) {
160
+ const url = new URL(request.url);
161
+ const merged = url.pathname.replace(/\/{2,}/g, '/');
162
+ if (merged === url.pathname)
163
+ return request;
164
+ url.pathname = merged;
165
+ return new Request(url, request);
166
+ }
128
167
  export function createPackFetch(parts, options = {}) {
129
168
  const m = parts.manifest;
130
169
  const scope = {
@@ -258,7 +297,7 @@ export function createPackFetch(parts, options = {}) {
258
297
  }
259
298
  };
260
299
  // a context the kernel opens for what no surface operation names: a door, a screen, the clock, the board
261
- const open = (request, id, method, path, params) => contextFor(m, { request, operation: { id, method, path, class: method === 'GET' ? 'retrieve' : 'action' }, params }, scope);
300
+ const open = (request, id, method, path, params, streamed) => contextFor(m, { request, operation: { id, method, path, class: method === 'GET' ? 'retrieve' : 'action' }, params, ...(streamed ? { streamed } : {}) }, scope);
262
301
  const advanceClock = async (request) => {
263
302
  if (parts.clock)
264
303
  await runAsVendorMove(async () => parts.clock(await open(new Request(request.url, m.tenant ? { headers: request.headers } : {}), 'clock', 'POST', '/_twin/clock', {})));
@@ -370,7 +409,7 @@ export function createPackFetch(parts, options = {}) {
370
409
  return refusedReadOnly();
371
410
  if (parts.clock)
372
411
  await runAsVendorMove(async () => parts.clock(await open(new Request(asked.url, m.tenant ? { headers: asked.headers } : {}), 'clock', 'POST', '/_twin/clock', {})));
373
- return hit.s.answer(await open(asked, hit.s.decl.id, method, hit.s.decl.path, { ...(hit.s.host(host)?.params ?? {}), ...hit.hit.params }));
412
+ return hit.s.answer(await open(asked, hit.s.decl.id, method, hit.s.decl.path, { ...(hit.s.host(host)?.params ?? {}), ...hit.hit.params }, true));
374
413
  };
375
414
  /** A request as the kernel reads it: the headers only the kernel names taken off, its body decoded (undefined when it
376
415
  * does not decode as its Content-Encoding says). */
@@ -393,7 +432,8 @@ export function createPackFetch(parts, options = {}) {
393
432
  const readable = await prepared(arrived);
394
433
  return readable instanceof Response ? readable : readable ? atContentHost(readable) : undecodable(arrived);
395
434
  };
396
- const fetch = async (arrived) => {
435
+ const fetch = async (received) => {
436
+ const arrived = m.mergeSlashes ? slashesMerged(received) : received;
397
437
  redisServiceTimes.lap('1 request made by the stream');
398
438
  // the discovery door is the pack's own, whichever lane would take its path
399
439
  const asked = new URL(arrived.url).pathname.replace(/\/+$/, '') || '/';
@@ -469,6 +509,9 @@ export function createPackFetch(parts, options = {}) {
469
509
  // the vendor's own moves, not the caller's: its context carries none of the request's headers (a Stripe-Account
470
510
  // header would make a payout time makes the caller's), but a tenant's, when the vendor keeps tenants apart
471
511
  await advanceClock(request);
512
+ // a reply a session awaits that the World's clock has run out (sockets.ts)
513
+ if (m.sockets?.length)
514
+ expireSocketWaits(m.service, options.root);
472
515
  if (path.startsWith('/_twin/')) {
473
516
  // the World's machine pool, enrolled by the kernel for a vendor that runs images (machines.ts)
474
517
  if (m.machines && path === '/_twin/machine-pool' && request.method === 'POST') {
@@ -526,9 +569,12 @@ export function createPackFetch(parts, options = {}) {
526
569
  ?? (apiPath ? undefined : hits.find((x) => x.onHost) ?? (addressed ? undefined : hits.find((x) => !rooted(x))
527
570
  ?? (placed ? hits.find((x) => x.s.decl.kind === 'workspace') : undefined)));
528
571
  if (byPath) {
572
+ const asked = byPath.s.decl.kind === 'workspace' && request.method === 'GET' ? shownRecord(url, screens.filter((x) => x.decl.kind === 'workspace').map((x) => x.decl), { ...(byPath.onHost?.params ?? {}), ...byPath.hit.params }) : undefined;
573
+ if (asked !== undefined && asked !== path)
574
+ return new Response(null, { status: 302, headers: { location: `${(request.headers.get(TWIN_PREFIX_HEADER) ?? '').replace(/\/+$/, '')}${asked}` } });
529
575
  if (readOnly && request.method !== 'GET' && request.method !== 'HEAD')
530
576
  return refusedReadOnly();
531
- return byPath.s.answer(await open(request, byPath.s.decl.id, request.method, byPath.s.decl.path, { ...(byPath.onHost?.params ?? {}), ...byPath.hit.params }));
577
+ return byPath.s.answer(await open(request, byPath.s.decl.id, request.method, byPath.s.decl.path, { ...(byPath.onHost?.params ?? {}), ...byPath.hit.params }, true));
532
578
  }
533
579
  for (const route of m.machineRoutes ?? []) {
534
580
  const host = request.headers.get('x-volter-twin-original-host') ?? new URL(request.url).hostname;
@@ -619,7 +665,7 @@ export function createPackFetch(parts, options = {}) {
619
665
  for (const d of m.sockets ?? [])
620
666
  if (!parts.sockets?.[d.id])
621
667
  throw new Error(`${m.vendor}: the manifest declares the socket ${d.id}, and semantics/sockets.ts exports no ${d.id}`);
622
- const ownUpgrade = m.sockets?.length ? socketUpgrade(m.service, options.root, m.sockets, parts.sockets, (request, d) => open(request, `socket:${d.id}`, 'GET', d.path, {}), hostOf, (request) => new URL(routed(request).url).pathname,
668
+ const ownUpgrade = m.sockets?.length ? socketUpgrade(m.service, options.root, m.sockets, parts.sockets, (request, d, params) => open(request, `socket:${d.id}`, 'GET', d.path, params), hostOf, (request) => new URL(routed(request).url).pathname,
623
669
  // a turn a session speaks (a voice agent's reply) is the World's scenario's, as a turn over HTTP is
624
670
  async (asked) => engineOf()?.serve(asked)) : undefined;
625
671
  const upgrade = laneUpgrades(parts.lanes ?? {}, ownUpgrade);
@@ -8,7 +8,8 @@ export declare function installedVendorPacks(from: string, options?: {
8
8
  }): Map<string, string>;
9
9
  /** Node's nearest installed package, including linked packages and hoisted workspace dependencies. */
10
10
  export declare function installedPackageDir(from: string, name: string): string | undefined;
11
- /** A pack repository itself, or the P3 repository beside a platform checkout. Applications do not acquire a
11
+ /** A pack repository itself, or a platform checkout's P3 repository: the one `VOLTER_PACKS` names (a worktree's own
12
+ * pinned packs, as the repository's scripts read it), else the one beside it. Applications do not acquire a
12
13
  * sibling checkout merely because one happens to be beside them. */
13
14
  export declare function packRepository(from: string): string | undefined;
14
15
  /** Resolve one vendor's package directory for host-side reads (assets, infrastructure launchers). */
@@ -121,7 +121,8 @@ export function installedPackageDir(from, name) {
121
121
  dir = up;
122
122
  }
123
123
  }
124
- /** A pack repository itself, or the P3 repository beside a platform checkout. Applications do not acquire a
124
+ /** A pack repository itself, or a platform checkout's P3 repository: the one `VOLTER_PACKS` names (a worktree's own
125
+ * pinned packs, as the repository's scripts read it), else the one beside it. Applications do not acquire a
125
126
  * sibling checkout merely because one happens to be beside them. */
126
127
  export function packRepository(from) {
127
128
  let dir = resolve(from);
@@ -129,6 +130,10 @@ export function packRepository(from) {
129
130
  if (packageName(dir) === 'twin-packs-p3')
130
131
  return dir;
131
132
  if (packageName(join(dir, 'packages', 'world-core')) === '@volter/world-core') {
133
+ const env = globalThis.process?.env?.VOLTER_PACKS;
134
+ const named = env ? resolve(env) : undefined;
135
+ if (named && packageName(named) === 'twin-packs-p3')
136
+ return named;
132
137
  const sibling = join(dirname(dir), 'twin-packs-p3');
133
138
  if (packageName(sibling) === 'twin-packs-p3')
134
139
  return sibling;
@@ -1,7 +1,11 @@
1
+ import { type BlobWritten } from './blob-store.js';
1
2
  /** Read a resource payload at this branch, then its retained ancestors. */
2
3
  export declare function readResourceBlob(service: string, key: string, root?: string): Promise<Uint8Array | null>;
3
4
  /** Write a resource payload on this branch (a stored object's bytes), where a read finds it first. */
4
5
  export declare function writeResourceBlob(service: string, key: string, bytes: Uint8Array, root?: string): Promise<void>;
6
+ /** Write a resource payload on this branch from a body as it arrives (a model's weights, an upload's bytes), kept only
7
+ * when `accept` holds for its size and SHA-256 (the blob seam's `writeBlobStream`). True when kept. */
8
+ export declare function writeResourceBlobStream(service: string, key: string, body: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>, accept: (written: BlobWritten) => boolean, root?: string): Promise<boolean>;
5
9
  /** Remove this branch's copy of a resource payload; an ancestor's stays its own (the row that named it is what a
6
10
  * deletion ends). */
7
11
  export declare function removeResourceBlob(service: string, key: string, root?: string): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { isAbsolute, join, relative } from 'node:path';
2
- import { getActiveBlobStore, readBlobRange } from "./blob-store.js";
2
+ import { getActiveBlobStore, readBlobRange, streamChunks, writeBlobStream } from "./blob-store.js";
3
3
  import { readBranchMeta } from "./log.js";
4
4
  import { worldPaths } from "./storage.js";
5
5
  /** Read a resource payload at this branch, then its retained ancestors. */
@@ -17,6 +17,13 @@ export async function writeResourceBlob(service, key, bytes, root) {
17
17
  assertResourceKey(key);
18
18
  await getActiveBlobStore().put(resourceBlobFile(worldPaths(service, root).resources, key), bytes);
19
19
  }
20
+ /** Write a resource payload on this branch from a body as it arrives (a model's weights, an upload's bytes), kept only
21
+ * when `accept` holds for its size and SHA-256 (the blob seam's `writeBlobStream`). True when kept. */
22
+ export async function writeResourceBlobStream(service, key, body, accept, root) {
23
+ assertResourceKey(key);
24
+ const chunks = body instanceof ReadableStream ? streamChunks(body) : body;
25
+ return writeBlobStream(resourceBlobFile(worldPaths(service, root).resources, key), chunks, accept);
26
+ }
20
27
  /** Remove this branch's copy of a resource payload; an ancestor's stays its own (the row that named it is what a
21
28
  * deletion ends). */
22
29
  export async function removeResourceBlob(service, key, root) {
@@ -36,8 +36,8 @@ export type { WorldStore, WorldStat, HydrationSource, HydrationSink } from './wo
36
36
  export type { CheckLoader } from './head.js';
37
37
  export { SqlWorldStore } from './world-store-sql.js';
38
38
  export type { SqlExec } from './world-store-sql.js';
39
- export { FsBlobStore, MemoryBlobStore, blobDigest, getActiveBlobStore, setActiveBlobStore, withBlobStore, readBlobRange, } from './blob-store.js';
40
- export type { BlobStore } from './blob-store.js';
39
+ export { FsBlobStore, MemoryBlobStore, blobDigest, getActiveBlobStore, setActiveBlobStore, withBlobStore, readBlobRange, streamChunks, writeBlobStream, } from './blob-store.js';
40
+ export type { BlobStore, BlobWritten } from './blob-store.js';
41
41
  export { applyTwinWrite, applyTwinWrites, applyTwinWriteAtomic, createTwinServer, CALLER_HEADER, callerForHeader, callerFromHeader, journalTwinRequest, readTwinRequestJournalFrom, type TwinRequestJournalCursor, twinRequestJournalFailures, twinRequestCredentials, readTwinRequestJournal, recordedInput, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from './serve.js';
42
42
  export type { AtomicTwinWriteDecision, TwinRequestJournalEntry, TwinResource, TwinWriteInput, TwinWriteResult, } from './serve.js';
43
43
  export { createTwinProxy } from './proxy.js';
@@ -79,7 +79,7 @@ export { corsHeaders, withCors, type CorsDecl } from './cors.js';
79
79
  export { setInProcessTwins, vendorFetch, VendorUnreachableError, type InProcessTwin } from './vendor-call.js';
80
80
  export { closeManagedDatabases, databaseSession, EngineDatabaseError, EngineUnavailableError, managedDatabase, quoteIdent, useDatabaseDriver, type DatabaseDriver, type DatabaseSession } from './managed-database.js';
81
81
  export type { Batch as EngineBatch, ManagedDatabase, Statement as EngineStatement, StatementResult as EngineStatementResult } from './managed-database.js';
82
- export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobFile, resourceBlobKey, resourceBlobSize, resourceChain, writeResourceBlob } from './resource-blob.js';
82
+ export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobFile, resourceBlobKey, resourceBlobSize, resourceChain, writeResourceBlob, writeResourceBlobStream } from './resource-blob.js';
83
83
  export { assertStateRemovable, checkParent, stateGeneration, withAncestryLock, withHistoryLock, withHistoryLocks, withStateLock, withStateRemoval } from './ancestry.js';
84
84
  export { captureHistory, captureParentHistory, historyChanges, historyAtInstant, historyDigest, historyEntries, historyLength, historyPrefix, inheritedHistory, originHead, publishOriginHistory, readHistoryView } from './history.js';
85
85
  export type { HistoryView, HistoryLayout, HistoryReference, HistoryOrigin } from './history.js';
@@ -54,7 +54,7 @@ export { FsWorldStore, MemoryWorldStore, getActiveWorldStore, setActiveWorldStor
54
54
  export { SqlWorldStore } from "./world-store-sql.js";
55
55
  // The blob seam (runtime contract R11): byte storage behind byte-carrying handlers, so a
56
56
  // serverless namespace puts bytes in object storage while local worlds keep today's layout.
57
- export { FsBlobStore, MemoryBlobStore, blobDigest, getActiveBlobStore, setActiveBlobStore, withBlobStore, readBlobRange, } from "./blob-store.js";
57
+ export { FsBlobStore, MemoryBlobStore, blobDigest, getActiveBlobStore, setActiveBlobStore, withBlobStore, readBlobRange, streamChunks, writeBlobStream, } from "./blob-store.js";
58
58
  export { applyTwinWrite, applyTwinWrites, applyTwinWriteAtomic, createTwinServer, CALLER_HEADER, callerForHeader, callerFromHeader, journalTwinRequest, readTwinRequestJournalFrom, twinRequestJournalFailures, twinRequestCredentials, readTwinRequestJournal, recordedInput, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from "./serve.js";
59
59
  export { createTwinProxy } from "./proxy.js";
60
60
  export { forkTwin, isFork, readForkMeta, } from "./fork.js";
@@ -100,7 +100,7 @@ export { multipartBoundary, multipartParts, multipartEnvelope, inferMultipartBou
100
100
  export { corsHeaders, withCors } from "./cors.js";
101
101
  export { setInProcessTwins, vendorFetch, VendorUnreachableError } from "./vendor-call.js";
102
102
  export { closeManagedDatabases, databaseSession, EngineDatabaseError, EngineUnavailableError, managedDatabase, quoteIdent, useDatabaseDriver } from "./managed-database.js";
103
- export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobFile, resourceBlobKey, resourceBlobSize, resourceChain, writeResourceBlob } from "./resource-blob.js";
103
+ export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobFile, resourceBlobKey, resourceBlobSize, resourceChain, writeResourceBlob, writeResourceBlobStream } from "./resource-blob.js";
104
104
  export { assertStateRemovable, checkParent, stateGeneration, withAncestryLock, withHistoryLock, withHistoryLocks, withStateLock, withStateRemoval } from "./ancestry.js";
105
105
  export { captureHistory, captureParentHistory, historyChanges, historyAtInstant, historyDigest, historyEntries, historyLength, historyPrefix, inheritedHistory, originHead, publishOriginHistory, readHistoryView } from "./history.js";
106
106
  export { foldHistory } from "./log.js";
@@ -60,6 +60,37 @@ export async function serveHttp(options) {
60
60
  // the global `Bun.serve` — the request journal's wrap of it (serve.ts) applies to packs and to
61
61
  // this seam alike, so the seam never decorates twice on Bun
62
62
  const upgrade = options.upgrade;
63
+ // the sockets open now, by the seam's own count: Bun keeps a socket its server closed (a refused key's 1008, from
64
+ // `open` or later) in `pendingWebSockets` after its close handler ran, and `stop()`'s promise then never settles
65
+ // (measured on Bun 1.3.11, macOS arm64, alone: a Bun.serve whose open() calls ws.close(1008) leaves
66
+ // pendingWebSockets at 1 with its close handler called once, and an awaited stop(true) had not settled after 3 s;
67
+ // unawaited, the listener closed and the process used 4 ms of CPU over the next 2 s; a client-initiated close
68
+ // stops in 0 ms). Conform's life hung on it after OpenAI's keyless Realtime step.
69
+ // and the requests whose handlers have not answered: a stop that drops connections (closeActive) aborts each one's
70
+ // signal, and Bun's stop still waits for the handler to settle, which a handler that never answers never does
71
+ // (measured on Bun 1.3.11, macOS arm64, alone: a Bun.serve whose fetch never resolves, its request's signal aborted
72
+ // by stop(true), pendingRequests 1, the stop unsettled after 3 s). Conform's client check hung on a pack served with
73
+ // a scenario that answered nothing. Node's path below reports such a handler as cleanup and resolves; so does this one.
74
+ const live = new Set();
75
+ const inflight = new Set();
76
+ const served = options.fetch;
77
+ const tracked = (request, ...rest) => {
78
+ inflight.add(request);
79
+ let answer;
80
+ try {
81
+ answer = served(request, ...rest);
82
+ }
83
+ catch (error) {
84
+ inflight.delete(request);
85
+ throw error;
86
+ }
87
+ if (!(answer instanceof Promise)) {
88
+ inflight.delete(request);
89
+ return answer;
90
+ }
91
+ return answer.finally(() => { inflight.delete(request); });
92
+ };
93
+ options = { ...options, fetch: tracked };
63
94
  const server = bun.serve({ ...options, hostname, port: options.port ?? 0,
64
95
  ...(upgrade ? {
65
96
  fetch: (request, server) => {
@@ -70,14 +101,33 @@ export async function serveHttp(options) {
70
101
  return options.fetch(request);
71
102
  },
72
103
  websocket: {
73
- open: (peer) => upgrade.open(peer, peer.data.request),
104
+ open: (peer) => { live.add(peer); return upgrade.open(peer, peer.data.request); },
74
105
  message: (peer, data) => upgrade.message(peer, data),
75
- close: upgrade.close,
106
+ close: (peer, ...rest) => { live.delete(peer); return upgrade.close(peer, ...rest); },
76
107
  },
77
108
  } : {}),
78
109
  });
79
110
  const port = server.port ?? 0;
80
- return keepHandler({ hostname, port, url: server.url instanceof URL ? server.url : new URL(`http://${urlHost}:${port}/`), stop: async (closeActive = true) => { await server.stop(closeActive); } }, options.fetch);
111
+ return keepHandler({ hostname, port, url: server.url instanceof URL ? server.url : new URL(`http://${urlHost}:${port}/`), stop: async (closeActive = true) => {
112
+ const stopping = Promise.resolve(server.stop(closeActive));
113
+ // stop closes the sockets still open (closeActive), each a close Bun may then keep counting, and drops the
114
+ // connections of handlers still running: once every socket the seam opened has closed and every handler left is
115
+ // one whose request was dropped, Bun's stop would never settle on them (above), and the seam has stopped
116
+ let settled = false;
117
+ stopping.then(() => { settled = true; }, () => { settled = true; });
118
+ while (!settled) {
119
+ const orphaned = [...inflight].every((request) => request.signal.aborted);
120
+ const counted = (server.pendingWebSockets ?? 0) > 0;
121
+ if (live.size === 0 && orphaned && (counted || (closeActive && inflight.size > 0))) {
122
+ stopping.catch(() => { });
123
+ if (inflight.size)
124
+ console.error(`serveHttp: forced stop left pending handler cleanup: ${inflight.size} request(s)`);
125
+ return;
126
+ }
127
+ await new Promise((resolve) => setTimeout(resolve, 10));
128
+ }
129
+ await stopping;
130
+ } }, options.fetch);
81
131
  }
82
132
  return keepHandler(await serveOnNode({ ...options, hostname, urlHost, fetch: decorate ? decorate(options) : options.fetch }), options.fetch);
83
133
  }
@@ -44,8 +44,11 @@ export type SocketEngine<C> = {
44
44
  };
45
45
  /** The open sessions of one socket of the vendor in a World (a handler reaches them as `ctx.sockets(id)`). */
46
46
  export declare function openSessions(service: string, root: string | undefined, socket: string): SocketSession[];
47
+ /** What a session awaits that the World's clock has run out (a life's `wait` past a relay's timeout), rejected as the
48
+ * machine's clock would reject it; called as the vendor serves a request ("A request answered by a session"). */
49
+ export declare function expireSocketWaits(service: string, root: string | undefined): void;
47
50
  /** Offer a write to every open session of the vendor, each in its turn after what it is already doing. Queued, never
48
51
  * awaited: the write may be one a session's own frame made, which that session finishes before it is told. */
49
52
  export declare function socketWrite(service: string, root: string | undefined, write: EventWrite): void;
50
53
  /** The upgrade the serve seam takes: a request to a declared socket's path (and host) opens a session of its engine. */
51
- export declare function socketUpgrade<C>(service: string, root: string | undefined, decls: ReadonlyArray<SocketDecl>, engines: Record<string, SocketEngine<C>>, context: (request: Request, decl: SocketDecl) => Promise<C>, hostOf: (request: Request) => string, pathOf?: (request: Request) => string, decide?: (asked: unknown) => Promise<ScenarioServed | undefined>): WebSocketUpgrade;
54
+ export declare function socketUpgrade<C>(service: string, root: string | undefined, decls: ReadonlyArray<SocketDecl>, engines: Record<string, SocketEngine<C>>, context: (request: Request, decl: SocketDecl, params: Record<string, string>) => Promise<C>, hostOf: (request: Request) => string, pathOf?: (request: Request) => string, decide?: (asked: unknown) => Promise<ScenarioServed | undefined>): WebSocketUpgrade;