@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,146 @@
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
+
5
+ export interface NativeGoKitReference {
6
+ package: { name: string; version: string };
7
+ target: 'wasm32-wali-go-single-thread';
8
+ releaseSha256: string;
9
+ producerSha256: string;
10
+ sourceSha256: string;
11
+ selection: { path: string; sha256: string };
12
+ recipe: { path: string; sha256: string };
13
+ installedRoot: string;
14
+ }
15
+ export interface NativeGoOperation {
16
+ sourceService: string;
17
+ role: 'server' | 'prerequisite' | 'health';
18
+ kit: string;
19
+ program: { name: string; version: string; manifestId: string; manifestSha256: string; path: string };
20
+ /** Resolved arguments require separate comparison with the full source. */
21
+ args: string[];
22
+ /** Names of existing World values, never caller secrets in this record. */
23
+ environmentBindings: Record<string, string>;
24
+ requires: { service: string; condition: 'service_started' | 'service_healthy' | 'service_completed_successfully' }[];
25
+ }
26
+ export interface NativeGoReference {
27
+ version: 2;
28
+ implementation: string;
29
+ release: string;
30
+ directory: string;
31
+ snapshotSha256: string;
32
+ declarationSha256: string;
33
+ image: string;
34
+ kits: Record<string, NativeGoKitReference>;
35
+ server: NativeGoOperation;
36
+ prerequisites: NativeGoOperation[];
37
+ health: NativeGoOperation;
38
+ }
39
+
40
+ const fail = (field: string): never => { throw new Error('Native Go operation metadata is invalid: ' + field); };
41
+ function object(value: unknown, fields: readonly string[], label: string): Record<string, unknown> {
42
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return fail(label);
43
+ const row = value as Record<string, unknown>;
44
+ if (Object.keys(row).length !== fields.length || fields.some(key => !Object.hasOwn(row, key))) return fail(label);
45
+ return row;
46
+ }
47
+ function text(value: unknown, label: string): string {
48
+ if (typeof value !== 'string' || !value || value.includes('\0')) return fail(label);
49
+ return value;
50
+ }
51
+ function digest(value: unknown, label: string): string {
52
+ const out = text(value, label);
53
+ if (!/^[a-f0-9]{64}$/u.test(out)) return fail(label);
54
+ return out;
55
+ }
56
+ function relativePath(value: unknown, label: string): string {
57
+ const out = text(value, label);
58
+ if (out.startsWith('/') || out.includes('\\') || posix.normalize(out) !== out
59
+ || out.split('/').some(part => !part || part === '.' || part === '..')) return fail(label);
60
+ return out;
61
+ }
62
+ function installedPath(value: unknown, label: string): string {
63
+ const out = text(value, label);
64
+ if (!out.startsWith('/opt/programs/') || out.includes('\\') || posix.normalize(out) !== out
65
+ || out.split('/').slice(1).some(part => !part || part === '.' || part === '..')) return fail(label);
66
+ return out;
67
+ }
68
+ function pin(value: unknown, label: string): { path: string; sha256: string } {
69
+ const row = object(value, ['path', 'sha256'], label);
70
+ return { path: relativePath(row.path, label + '.path'), sha256: digest(row.sha256, label + '.sha256') };
71
+ }
72
+ function kit(value: unknown, label: string): NativeGoKitReference {
73
+ const row = object(value, ['package', 'target', 'releaseSha256', 'producerSha256', 'sourceSha256', 'selection', 'recipe', 'installedRoot'], label);
74
+ const pack = object(row.package, ['name', 'version'], label + '.package');
75
+ if (row.target !== 'wasm32-wali-go-single-thread') return fail(label + '.target');
76
+ return { package: { name: text(pack.name, label + '.package.name'), version: text(pack.version, label + '.package.version') },
77
+ target: row.target, releaseSha256: digest(row.releaseSha256, label + '.releaseSha256'),
78
+ producerSha256: digest(row.producerSha256, label + '.producerSha256'), sourceSha256: digest(row.sourceSha256, label + '.sourceSha256'),
79
+ selection: pin(row.selection, label + '.selection'), recipe: pin(row.recipe, label + '.recipe'),
80
+ installedRoot: installedPath(row.installedRoot, label + '.installedRoot') };
81
+ }
82
+ function operation(value: unknown, role: NativeGoOperation['role'], kits: Record<string, NativeGoKitReference>, label: string): NativeGoOperation {
83
+ const row = object(value, ['sourceService', 'role', 'kit', 'program', 'args', 'environmentBindings', 'requires'], label);
84
+ const selected = text(row.kit, label + '.kit');
85
+ if (row.role !== role || !Object.hasOwn(kits, selected)) return fail(label + '.role/kit');
86
+ const provider = kits[selected]!;
87
+ const command = object(row.program, ['name', 'version', 'manifestId', 'manifestSha256', 'path'], label + '.program');
88
+ const path = installedPath(command.path, label + '.program.path'), version = text(command.version, label + '.program.version');
89
+ if (version !== provider.package.version || !path.startsWith(provider.installedRoot + '/')) return fail(label + '.program placement/version');
90
+ if (!Array.isArray(row.args)) return fail(label + '.args');
91
+ const args: string[] = [];
92
+ for (const arg of row.args) {
93
+ if (typeof arg !== 'string' || arg.includes('\0')) return fail(label + '.args.' + args.length);
94
+ args.push(arg);
95
+ }
96
+ if (!row.environmentBindings || typeof row.environmentBindings !== 'object' || Array.isArray(row.environmentBindings)) return fail(label + '.environmentBindings');
97
+ const bindings: Record<string, string> = Object.create(null);
98
+ for (const [name, value] of Object.entries(row.environmentBindings)) {
99
+ const source = text(value, label + '.environmentBindings.' + name);
100
+ if (![name, source].every(item => /^[A-Za-z_][A-Za-z0-9_]*$/u.test(item))) return fail(label + '.environmentBindings name');
101
+ bindings[name] = source;
102
+ }
103
+ if (!Array.isArray(row.requires)) return fail(label + '.requires');
104
+ const dependencies: NativeGoOperation['requires'] = [];
105
+ for (const value of row.requires) {
106
+ const edge = object(value, ['service', 'condition'], label + '.requires');
107
+ if (edge.condition !== 'service_started' && edge.condition !== 'service_healthy' && edge.condition !== 'service_completed_successfully')
108
+ return fail(label + '.requires.condition');
109
+ const service = text(edge.service, label + '.requires.service');
110
+ if (dependencies.some(prior => prior.service === service)) return fail(label + '.requires duplicate service');
111
+ dependencies.push({ service, condition: edge.condition });
112
+ }
113
+ return { sourceService: text(row.sourceService, label + '.sourceService'), role, kit: selected,
114
+ program: { name: text(command.name, label + '.program.name'), version, manifestId: text(command.manifestId, label + '.program.manifestId'),
115
+ manifestSha256: digest(command.manifestSha256, label + '.program.manifestSha256'), path },
116
+ args, environmentBindings: bindings, requires: dependencies };
117
+ }
118
+
119
+ /** All fields are declared data. The later owned consumer must authenticate
120
+ * the complete source, kit, original image and admitted installed manifests. */
121
+ export function nativeGoReference(value: unknown): NativeGoReference {
122
+ const row = object(value, ['version', 'implementation', 'release', 'directory', 'snapshotSha256', 'declarationSha256', 'image', 'kits', 'server', 'prerequisites', 'health'], 'reference');
123
+ if (row.version !== 2) return fail('version');
124
+ const image = text(row.image, 'image');
125
+ if (!/^[^\s@]+@sha256:[a-f0-9]{64}$/u.test(image)) return fail('image');
126
+ if (!row.kits || typeof row.kits !== 'object' || Array.isArray(row.kits) || !Object.keys(row.kits).length) return fail('kits');
127
+ const kits: Record<string, NativeGoKitReference> = Object.create(null);
128
+ for (const [name, value] of Object.entries(row.kits)) kits[text(name, 'kit name')] = kit(value, 'kits.' + name);
129
+ const server = operation(row.server, 'server', kits, 'server'), health = operation(row.health, 'health', kits, 'health');
130
+ const implementation = text(row.implementation, 'implementation'), release = text(row.release, 'release');
131
+ if (kits[server.kit]!.package.name !== implementation || kits[server.kit]!.package.version !== release
132
+ || health.sourceService !== server.sourceService) return fail('server/health implementation');
133
+ if (!Array.isArray(row.prerequisites)) return fail('prerequisites');
134
+ const prerequisites: NativeGoOperation[] = [];
135
+ for (const value of row.prerequisites)
136
+ prerequisites.push(operation(value, 'prerequisite', kits, 'prerequisites.' + prerequisites.length));
137
+ if (new Set(prerequisites.map(item => item.sourceService)).size !== prerequisites.length
138
+ || prerequisites.some(item => item.sourceService === server.sourceService)) return fail('prerequisite service');
139
+ const usedKits = new Set([server.kit, health.kit, ...prerequisites.map(item => item.kit)]);
140
+ if (Object.keys(kits).some(name => !usedKits.has(name))) return fail('unconsumed kit');
141
+ for (const item of prerequisites) if (!server.requires.some(edge => edge.service === item.sourceService && edge.condition === 'service_completed_successfully'))
142
+ return fail('server prerequisite completion edge');
143
+ return { version: 2, implementation, release, directory: relativePath(row.directory, 'directory'),
144
+ snapshotSha256: digest(row.snapshotSha256, 'snapshotSha256'), declarationSha256: digest(row.declarationSha256, 'declarationSha256'),
145
+ image, kits, server, prerequisites, health };
146
+ }
@@ -13,6 +13,16 @@ export type MongoDocument = Record<string, unknown>;
13
13
  /** BSON, as the pack supplies it (the `bson` library, installed by the pack's package.json). The kernel frames
14
14
  * documents and never reads inside one but for a command's `$db`. */
15
15
  export type MongoCodec = { serialize(document: MongoDocument): Uint8Array; deserialize(bytes: Uint8Array): MongoDocument };
16
+ /** What a pack's stream reads of the `bson` library its generated fetch's `loadBson` resolves from the pack: the codec,
17
+ * extended JSON, and the number kinds a reply names. The kernel declares the shape and imports nothing of it. */
18
+ export type BsonLibrary = {
19
+ serialize(document: MongoDocument): Uint8Array;
20
+ deserialize(bytes: Uint8Array, options?: { promoteValues?: boolean; promoteLongs?: boolean; bsonRegExp?: boolean }): MongoDocument;
21
+ EJSON: { serialize(value: unknown, options?: { relaxed?: boolean }): MongoDocument; deserialize(value: unknown, options?: { relaxed?: boolean }): MongoDocument };
22
+ Double: new (value: number) => unknown;
23
+ Int32: new (value: number) => unknown;
24
+ Long: { fromNumber(value: number): unknown };
25
+ };
16
26
 
17
27
  /** What the sender's message said beside its command: whether it will take more than one reply to it (OP_MSG's exhaustAllowed). */
18
28
  export type MongoAsked = { exhaustAllowed: boolean };
package/src/pack-fetch.ts CHANGED
@@ -12,7 +12,7 @@ import { ORIGINAL_PATH_HEADER } from './sigv4.ts';
12
12
  import { placedCookies, TWIN_PREFIX_HEADER } from './twin-fetch.ts';
13
13
  import { PREFIX_PARAMS_HEADER } from './derived.ts';
14
14
  import { withHeaders } from './with-headers.ts';
15
- import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, scenarioDecisionHeader, storedType, coreFor, crossCutting, derivedRequestScopes, vendorError, type CoreScope, type DerivedManifest, type ErrorSpec, type Handler, type HandlerContext, type LanesDecl, type VendorManifest } from './derived-core.ts';
15
+ import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, scenarioDecisionHeader, storedType, coreFor, crossCutting, derivedRequestScopes, vendorError, type CoreScope, type DerivedManifest, type ErrorSpec, type Handler, type HandlerContext, type LanesDecl, type ScreenDecl, type VendorManifest } from './derived-core.ts';
16
16
  import { compileSurface, createDerivedFetch, matchOperation, pathParams, type DerivedFetch, type DerivedOperation, type DerivedOwner, type DerivedSurface, type Workspace, type BoardFrame } from './derived.ts';
17
17
  import { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, runAsVendorMove } from './request-scope.ts';
18
18
  import { parseScenarioDocument, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest, type PackScenarioAdapter } from './scenario.ts';
@@ -27,7 +27,7 @@ import { hostRules } from '../host-routing.cjs';
27
27
 
28
28
  /** A descriptor's host rules, as data. */
29
29
  type PackDescriptorHosts = ReadonlyArray<HostRule> | undefined;
30
- import { socketUpgrade, type SocketEngine } from './sockets.ts';
30
+ import { expireSocketWaits, socketUpgrade, type SocketEngine } from './sockets.ts';
31
31
  import { liveIngest } from './live/ingest.ts';
32
32
  import type { LiveEngine } from './live/types.ts';
33
33
  import { readResourceBlob, removeResourceBlob, writeResourceBlob } from './resource-blob.ts';
@@ -144,6 +144,29 @@ function hostMatcher(pattern: string): (host: string) => Match | undefined {
144
144
  /** The host a request is for: the vendor host the World's injector names, else its own. */
145
145
  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
146
 
147
+ /** Where a workspace asked `?show=<type>:<id>` opens the record (architecture, "Workspace"): the built workspace page
148
+ * that shows that type, its parameter the id; else the one that lists the type; else the workspace's home (its root
149
+ * page). Every other parameter a page's path names is the asking page's, and a page that cannot be filled is passed
150
+ * over. Undefined when nothing is asked; the path (with no `show`) otherwise. */
151
+ export function shownRecord(url: URL, workspaces: ReadonlyArray<ScreenDecl>, params: Record<string, string>): string | undefined {
152
+ const asked = /^([^:]+):(.+)$/.exec(url.searchParams.get('show') ?? '');
153
+ if (!asked) return undefined;
154
+ const [, type, id] = asked;
155
+ const fill = (path: string, extra: Record<string, string>): string | undefined => {
156
+ let unfilled = false;
157
+ const filled = path.replace(/\{([^}]+)\}|<([^>]+)>/g, (_, a?: string, b?: string) => {
158
+ const v = { ...params, ...extra }[(a ?? b)!];
159
+ if (v === undefined || v === '') { unfilled = true; return ''; }
160
+ return encodeURIComponent(v);
161
+ });
162
+ return unfilled ? undefined : filled;
163
+ };
164
+ const shows = workspaces.filter((s) => s.shows?.type === type).map((s) => fill(s.path, { [s.shows!.param]: id! })).find((p) => p !== undefined);
165
+ const lists = workspaces.filter((s) => s.lists?.includes(type!)).map((s) => fill(s.path, {})).find((p) => p !== undefined);
166
+ const home = workspaces.find((s) => s.path.replace(/\/+$/, '') === '') ? '/' : undefined;
167
+ return shows ?? lists ?? home ?? url.pathname;
168
+ }
169
+
147
170
  /** The answer for a path no operation, door or screen serves, and for an operation nothing models: the manifest's
148
171
  * `gap`, its `{method}` and `{path}` filled, else the manifest's not-found refusal. */
149
172
  function gapAnswer(m: DerivedManifest, request: Request, otherMethod = false): Response {
@@ -188,13 +211,26 @@ async function encoded(m: DerivedManifest, request: Request, res: Response): Pro
188
211
  headers.set('content-encoding', 'gzip');
189
212
  headers.delete('content-length');
190
213
  headers.append('vary', 'Accept-Encoding');
191
- return new Response(gzipSync(new Uint8Array(await res.arrayBuffer())), { status: res.status, statusText: res.statusText, headers });
214
+ const body = gzipSync(new Uint8Array(await res.arrayBuffer()));
215
+ // Hold the compressor's buffer with the same finite stream body grammar as decoded(), without copying it.
216
+ const buffered = new ReadableStream<Uint8Array>({ start(controller) { controller.enqueue(body); controller.close(); } });
217
+ return new Response(buffered, { status: res.status, statusText: res.statusText, headers });
192
218
  }
193
219
 
194
220
  /** A pack's HTTP surface, as the kernel serves it from the pack's parts. */
195
221
  /** A door's path as it is served and listed: under `/_twin/`, whether the manifest writes the prefix or not. */
196
222
  const doorPath = (path: string): string => (path.startsWith('/_twin/') ? path : `/_twin${path.startsWith('/') ? '' : '/'}${path}`);
197
223
 
224
+ /** A request with its path's repeated slashes merged, as a front that merges them routes it (the manifest's
225
+ * `mergeSlashes`); one with none is itself. */
226
+ function slashesMerged(request: Request): Request {
227
+ const url = new URL(request.url);
228
+ const merged = url.pathname.replace(/\/{2,}/g, '/');
229
+ if (merged === url.pathname) return request;
230
+ url.pathname = merged;
231
+ return new Request(url, request);
232
+ }
233
+
198
234
  export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}): DerivedFetch {
199
235
  const m = parts.manifest;
200
236
  const scope: CoreScope = {
@@ -311,8 +347,8 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
311
347
  } catch { return false; }
312
348
  };
313
349
  // a context the kernel opens for what no surface operation names: a door, a screen, the clock, the board
314
- const open = (request: Request, id: string, method: string, path: string, params: Record<string, string>): Promise<HandlerContext> =>
315
- contextFor(m, { request, operation: { id, method, path, class: method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation, params }, scope);
350
+ const open = (request: Request, id: string, method: string, path: string, params: Record<string, string>, streamed?: true): Promise<HandlerContext> =>
351
+ contextFor(m, { request, operation: { id, method, path, class: method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation, params, ...(streamed ? { streamed } : {}) }, scope);
316
352
  const advanceClock = async (request: Request): Promise<void> => {
317
353
  if (parts.clock) await runAsVendorMove(async () => parts.clock!(await open(new Request(request.url, m.tenant ? { headers: request.headers } : {}), 'clock', 'POST', '/_twin/clock', {})));
318
354
  };
@@ -409,7 +445,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
409
445
  const asked = options.readOnly && !isReadOnlyRequest(there) ? withHeaders(there, new Headers([...there.headers, [READ_ONLY_REQUEST_HEADER, '1']])) : there;
410
446
  if (method === 'POST' && ((options.readOnly ?? false) || isReadOnlyRequest(asked))) return refusedReadOnly();
411
447
  if (parts.clock) await runAsVendorMove(async () => parts.clock!(await open(new Request(asked.url, m.tenant ? { headers: asked.headers } : {}), 'clock', 'POST', '/_twin/clock', {})));
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 }));
448
+ 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));
413
449
  };
414
450
  /** A request as the kernel reads it: the headers only the kernel names taken off, its body decoded (undefined when it
415
451
  * does not decode as its Content-Encoding says). */
@@ -431,7 +467,8 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
431
467
  const readable = await prepared(arrived);
432
468
  return readable instanceof Response ? readable : readable ? atContentHost(readable) : undecodable(arrived);
433
469
  };
434
- const fetch = async (arrived: Request): Promise<Response> => {
470
+ const fetch = async (received: Request): Promise<Response> => {
471
+ const arrived = m.mergeSlashes ? slashesMerged(received) : received;
435
472
  redisServiceTimes.lap('1 request made by the stream');
436
473
  // the discovery door is the pack's own, whichever lane would take its path
437
474
  const asked = new URL(arrived.url).pathname.replace(/\/+$/, '') || '/';
@@ -499,6 +536,8 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
499
536
  // the vendor's own moves, not the caller's: its context carries none of the request's headers (a Stripe-Account
500
537
  // header would make a payout time makes the caller's), but a tenant's, when the vendor keeps tenants apart
501
538
  await advanceClock(request);
539
+ // a reply a session awaits that the World's clock has run out (sockets.ts)
540
+ if (m.sockets?.length) expireSocketWaits(m.service, options.root);
502
541
  if (path.startsWith('/_twin/')) {
503
542
  // the World's machine pool, enrolled by the kernel for a vendor that runs images (machines.ts)
504
543
  if (m.machines && path === '/_twin/machine-pool' && request.method === 'POST') {
@@ -547,8 +586,10 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
547
586
  ?? (apiPath ? undefined : hits.find((x) => x.onHost) ?? (addressed ? undefined : hits.find((x) => !rooted(x))
548
587
  ?? (placed ? hits.find((x) => x.s.decl.kind === 'workspace') : undefined)));
549
588
  if (byPath) {
589
+ 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;
590
+ if (asked !== undefined && asked !== path) return new Response(null, { status: 302, headers: { location: `${(request.headers.get(TWIN_PREFIX_HEADER) ?? '').replace(/\/+$/, '')}${asked}` } });
550
591
  if (readOnly && request.method !== 'GET' && request.method !== 'HEAD') return refusedReadOnly();
551
- return byPath.s.answer(await open(request, byPath.s.decl.id, request.method, byPath.s.decl.path, { ...(byPath.onHost?.params ?? {}), ...byPath.hit!.params }));
592
+ return byPath.s.answer(await open(request, byPath.s.decl.id, request.method, byPath.s.decl.path, { ...(byPath.onHost?.params ?? {}), ...byPath.hit!.params }, true));
552
593
  }
553
594
  for (const route of m.machineRoutes ?? []) {
554
595
  const host = request.headers.get('x-volter-twin-original-host') ?? new URL(request.url).hostname;
@@ -622,7 +663,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
622
663
  // request that opened it (operation `socket:<id>`)
623
664
  for (const d of m.sockets ?? []) if (!parts.sockets?.[d.id]) throw new Error(`${m.vendor}: the manifest declares the socket ${d.id}, and semantics/sockets.ts exports no ${d.id}`);
624
665
  const ownUpgrade = m.sockets?.length ? socketUpgrade(m.service, options.root, m.sockets, parts.sockets!,
625
- (request, d) => open(request, `socket:${d.id}`, 'GET', d.path, {}), hostOf, (request) => new URL(routed(request).url).pathname,
666
+ (request, d, params) => open(request, `socket:${d.id}`, 'GET', d.path, params), hostOf, (request) => new URL(routed(request).url).pathname,
626
667
  // a turn a session speaks (a voice agent's reply) is the World's scenario's, as a turn over HTTP is
627
668
  async (asked) => engineOf()?.serve(asked)) : undefined;
628
669
  const upgrade = laneUpgrades(parts.lanes ?? {}, ownUpgrade);
package/src/pack-paths.ts CHANGED
@@ -102,13 +102,17 @@ export function installedPackageDir(from: string, name: string): string | undefi
102
102
  }
103
103
  }
104
104
 
105
- /** A pack repository itself, or the P3 repository beside a platform checkout. Applications do not acquire a
105
+ /** A pack repository itself, or a platform checkout's P3 repository: the one `VOLTER_PACKS` names (a worktree's own
106
+ * pinned packs, as the repository's scripts read it), else the one beside it. Applications do not acquire a
106
107
  * sibling checkout merely because one happens to be beside them. */
107
108
  export function packRepository(from: string): string | undefined {
108
109
  let dir = resolve(from);
109
110
  for (;;) {
110
111
  if (packageName(dir) === 'twin-packs-p3') return dir;
111
112
  if (packageName(join(dir, 'packages', 'world-core')) === '@volter/world-core') {
113
+ const env = globalThis.process?.env?.VOLTER_PACKS;
114
+ const named = env ? resolve(env) : undefined;
115
+ if (named && packageName(named) === 'twin-packs-p3') return named;
112
116
  const sibling = join(dirname(dir), 'twin-packs-p3');
113
117
  if (packageName(sibling) === 'twin-packs-p3') return sibling;
114
118
  }
@@ -1,5 +1,5 @@
1
1
  import { isAbsolute, join, relative } from 'node:path';
2
- import { getActiveBlobStore, readBlobRange } from './blob-store.ts';
2
+ import { type BlobWritten, getActiveBlobStore, readBlobRange, streamChunks, writeBlobStream } from './blob-store.ts';
3
3
  import { readBranchMeta } from './log.ts';
4
4
  import { worldPaths } from './storage.ts';
5
5
 
@@ -19,6 +19,14 @@ export async function writeResourceBlob(service: string, key: string, bytes: Uin
19
19
  await getActiveBlobStore().put(resourceBlobFile(worldPaths(service, root).resources, key), bytes);
20
20
  }
21
21
 
22
+ /** Write a resource payload on this branch from a body as it arrives (a model's weights, an upload's bytes), kept only
23
+ * when `accept` holds for its size and SHA-256 (the blob seam's `writeBlobStream`). True when kept. */
24
+ export async function writeResourceBlobStream(service: string, key: string, body: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>, accept: (written: BlobWritten) => boolean, root?: string): Promise<boolean> {
25
+ assertResourceKey(key);
26
+ const chunks = body instanceof ReadableStream ? streamChunks(body) : body;
27
+ return writeBlobStream(resourceBlobFile(worldPaths(service, root).resources, key), chunks, accept);
28
+ }
29
+
22
30
  /** Remove this branch's copy of a resource payload; an ancestor's stays its own (the row that named it is what a
23
31
  * deletion ends). */
24
32
  export async function removeResourceBlob(service: string, key: string, root?: string): Promise<void> {
package/src/runtime.ts CHANGED
@@ -157,8 +157,10 @@ export {
157
157
  setActiveBlobStore,
158
158
  withBlobStore,
159
159
  readBlobRange,
160
+ streamChunks,
161
+ writeBlobStream,
160
162
  } from './blob-store.ts';
161
- export type { BlobStore } from './blob-store.ts';
163
+ export type { BlobStore, BlobWritten } from './blob-store.ts';
162
164
  export {
163
165
  applyTwinWrite,
164
166
  applyTwinWrites,
@@ -338,7 +340,7 @@ export { corsHeaders, withCors, type CorsDecl } from './cors.ts';
338
340
  export { setInProcessTwins, vendorFetch, VendorUnreachableError, type InProcessTwin } from './vendor-call.ts';
339
341
  export { closeManagedDatabases, databaseSession, EngineDatabaseError, EngineUnavailableError, managedDatabase, quoteIdent, useDatabaseDriver, type DatabaseDriver, type DatabaseSession } from './managed-database.ts';
340
342
  export type { Batch as EngineBatch, ManagedDatabase, Statement as EngineStatement, StatementResult as EngineStatementResult } from './managed-database.ts';
341
- export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobFile, resourceBlobKey, resourceBlobSize, resourceChain, writeResourceBlob } from './resource-blob.ts';
343
+ export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobFile, resourceBlobKey, resourceBlobSize, resourceChain, writeResourceBlob, writeResourceBlobStream } from './resource-blob.ts';
342
344
 
343
345
  export { assertStateRemovable, checkParent, stateGeneration, withAncestryLock, withHistoryLock, withHistoryLocks, withStateLock, withStateRemoval } from './ancestry.ts';
344
346
 
package/src/serve-http.ts CHANGED
@@ -105,6 +105,28 @@ export async function serveHttp(options: ServeHttpOptions): Promise<HttpServer>
105
105
  // the global `Bun.serve` — the request journal's wrap of it (serve.ts) applies to packs and to
106
106
  // this seam alike, so the seam never decorates twice on Bun
107
107
  const upgrade = options.upgrade;
108
+ // the sockets open now, by the seam's own count: Bun keeps a socket its server closed (a refused key's 1008, from
109
+ // `open` or later) in `pendingWebSockets` after its close handler ran, and `stop()`'s promise then never settles
110
+ // (measured on Bun 1.3.11, macOS arm64, alone: a Bun.serve whose open() calls ws.close(1008) leaves
111
+ // pendingWebSockets at 1 with its close handler called once, and an awaited stop(true) had not settled after 3 s;
112
+ // unawaited, the listener closed and the process used 4 ms of CPU over the next 2 s; a client-initiated close
113
+ // stops in 0 ms). Conform's life hung on it after OpenAI's keyless Realtime step.
114
+ // and the requests whose handlers have not answered: a stop that drops connections (closeActive) aborts each one's
115
+ // signal, and Bun's stop still waits for the handler to settle, which a handler that never answers never does
116
+ // (measured on Bun 1.3.11, macOS arm64, alone: a Bun.serve whose fetch never resolves, its request's signal aborted
117
+ // by stop(true), pendingRequests 1, the stop unsettled after 3 s). Conform's client check hung on a pack served with
118
+ // a scenario that answered nothing. Node's path below reports such a handler as cleanup and resolves; so does this one.
119
+ const live = new Set<unknown>();
120
+ const inflight = new Set<Request>();
121
+ const served = options.fetch;
122
+ const tracked = (request: Request, ...rest: unknown[]): Response | Promise<Response> => {
123
+ inflight.add(request);
124
+ let answer: Response | Promise<Response>;
125
+ try { answer = (served as (r: Request, ...x: unknown[]) => Response | Promise<Response>)(request, ...rest); } catch (error) { inflight.delete(request); throw error; }
126
+ if (!(answer instanceof Promise)) { inflight.delete(request); return answer; }
127
+ return answer.finally(() => { inflight.delete(request); });
128
+ };
129
+ options = { ...options, fetch: tracked };
108
130
  const server = bun.serve({ ...options, hostname, port: options.port ?? 0,
109
131
  ...(upgrade ? {
110
132
  fetch: (request: Request, server: { upgrade: (request: Request, options: { data: { request: Request } }) => boolean }) => {
@@ -114,14 +136,32 @@ export async function serveHttp(options: ServeHttpOptions): Promise<HttpServer>
114
136
  return options.fetch(request);
115
137
  },
116
138
  websocket: {
117
- open: (peer: WebSocketPeer & { data: { request: Request } }) => upgrade.open(peer, peer.data.request),
139
+ open: (peer: WebSocketPeer & { data: { request: Request } }) => { live.add(peer); return upgrade.open(peer, peer.data.request); },
118
140
  message: (peer: WebSocketPeer, data: string | Uint8Array) => upgrade.message(peer, data),
119
- close: upgrade.close,
141
+ close: (peer: WebSocketPeer, ...rest: unknown[]) => { live.delete(peer); return (upgrade.close as (p: WebSocketPeer, ...r: unknown[]) => unknown)(peer, ...rest); },
120
142
  },
121
143
  } : {}),
122
144
  });
123
145
  const port = server.port ?? 0;
124
- 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);
146
+ return keepHandler({ hostname, port, url: server.url instanceof URL ? server.url : new URL(`http://${urlHost}:${port}/`), stop: async (closeActive = true) => {
147
+ const stopping = Promise.resolve(server.stop(closeActive));
148
+ // stop closes the sockets still open (closeActive), each a close Bun may then keep counting, and drops the
149
+ // connections of handlers still running: once every socket the seam opened has closed and every handler left is
150
+ // one whose request was dropped, Bun's stop would never settle on them (above), and the seam has stopped
151
+ let settled = false;
152
+ stopping.then(() => { settled = true; }, () => { settled = true; });
153
+ while (!settled) {
154
+ const orphaned = [...inflight].every((request) => request.signal.aborted);
155
+ const counted = ((server as { pendingWebSockets?: number }).pendingWebSockets ?? 0) > 0;
156
+ if (live.size === 0 && orphaned && (counted || (closeActive && inflight.size > 0))) {
157
+ stopping.catch(() => {});
158
+ if (inflight.size) console.error(`serveHttp: forced stop left pending handler cleanup: ${inflight.size} request(s)`);
159
+ return;
160
+ }
161
+ await new Promise((resolve) => setTimeout(resolve, 10));
162
+ }
163
+ await stopping;
164
+ } }, options.fetch);
125
165
  }
126
166
  return keepHandler(await serveOnNode({ ...options, hostname, urlHost, fetch: decorate ? decorate(options) : options.fetch }), options.fetch);
127
167
  }
package/src/sockets.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  // upgrade through the serve seam, and offers every write to every open session of the vendor, after the write's
7
7
  // webhooks. A session's own counters (a sequence number, a heartbeat) are the connection's, never the World's state.
8
8
  // Nothing here runs at module scope beyond the empty registry.
9
+ import { worldNow } from './world-clock.ts';
9
10
  import type { EventWrite } from './events.ts';
10
11
  import type { ScenarioEngine } from './scenario.ts';
11
12
  import type { WebSocketPeer, WebSocketUpgrade } from './serve-http.ts';
@@ -48,7 +49,7 @@ export type SocketEngine<C> = {
48
49
  close?(session: SocketSession, ctx: C): void | Promise<void>;
49
50
  };
50
51
 
51
- type Open = { session: SocketSession; engine: SocketEngine<unknown>; context: () => Promise<unknown>; queue: Promise<void>; pending: Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }> };
52
+ type Open = { session: SocketSession; engine: SocketEngine<unknown>; context: () => Promise<unknown>; queue: Promise<void>; pending: Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout>; due: number }> };
52
53
  const sessions = new Map<string, Set<Open>>();
53
54
  /** A World's next session number, per World (its service and root): a session's id is the World's, never the
54
55
  * process's, so a life replayed on a fresh World numbers its sessions as it did the first time. */
@@ -60,6 +61,15 @@ export function openSessions(service: string, root: string | undefined, socket:
60
61
  return [...(sessions.get(keyOf(service, root)) ?? [])].filter((o) => o.session.socket === socket).map((o) => o.session);
61
62
  }
62
63
 
64
+ /** What a session awaits that the World's clock has run out (a life's `wait` past a relay's timeout), rejected as the
65
+ * machine's clock would reject it; called as the vendor serves a request ("A request answered by a session"). */
66
+ export function expireSocketWaits(service: string, root: string | undefined): void {
67
+ const open = sessions.get(keyOf(service, root));
68
+ if (!open?.size) return;
69
+ const now = Date.parse(worldNow());
70
+ for (const o of open) for (const [k, p] of o.pending) if (p.due <= now) { clearTimeout(p.timer); o.pending.delete(k); p.reject(new Error('no reply in time')); }
71
+ }
72
+
63
73
  /** Offer a write to every open session of the vendor, each in its turn after what it is already doing. Queued, never
64
74
  * awaited: the write may be one a session's own frame made, which that session finishes before it is told. */
65
75
  export function socketWrite(service: string, root: string | undefined, write: EventWrite): void {
@@ -72,21 +82,42 @@ export function socketWrite(service: string, root: string | undefined, write: Ev
72
82
  }
73
83
  }
74
84
 
85
+ /** A declared socket path's parameters for a request's path, or undefined when it does not match: a `{name}` segment
86
+ * matches one segment, as an operation's path parameter does (architecture, sockets). */
87
+ function socketParams(declared: string, path: string): Record<string, string> | undefined {
88
+ const names: string[] = [];
89
+ const pattern = (declared.replace(/\/+$/, '') || '/').split(/(\{[^}/]+\})/).map((part) => {
90
+ const name = /^\{([^}/]+)\}$/.exec(part)?.[1];
91
+ if (name === undefined) return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ names.push(name);
93
+ return '([^/]+)';
94
+ }).join('');
95
+ const hit = new RegExp(`^${pattern}$`).exec(path);
96
+ if (!hit) return undefined;
97
+ try { return Object.fromEntries(names.map((name, i) => [name, decodeURIComponent(hit[i + 1]!)])); } catch { return undefined; }
98
+ }
99
+
75
100
  /** The upgrade the serve seam takes: a request to a declared socket's path (and host) opens a session of its engine. */
76
101
  export function socketUpgrade<C>(
77
102
  service: string,
78
103
  root: string | undefined,
79
104
  decls: ReadonlyArray<SocketDecl>,
80
105
  engines: Record<string, SocketEngine<C>>,
81
- context: (request: Request, decl: SocketDecl) => Promise<C>,
106
+ context: (request: Request, decl: SocketDecl, params: Record<string, string>) => Promise<C>,
82
107
  hostOf: (request: Request) => string,
83
108
  pathOf: (request: Request) => string = (request) => new URL(request.url).pathname,
84
109
  decide: (asked: unknown) => Promise<ScenarioServed | undefined> = async () => undefined,
85
110
  ): WebSocketUpgrade {
86
- const declFor = (request: Request): SocketDecl | undefined => decls.find((d) => {
111
+ const matchFor = (request: Request): { decl: SocketDecl; params: Record<string, string> } | undefined => {
87
112
  const path = pathOf(request).replace(/\/+$/, '') || '/';
88
- return (d.path.replace(/\/+$/, '') || '/') === path && (!d.host || d.host.toLowerCase() === hostOf(request)) && engines[d.id] !== undefined;
89
- });
113
+ for (const d of decls) {
114
+ if ((d.host && d.host.toLowerCase() !== hostOf(request)) || engines[d.id] === undefined) continue;
115
+ const params = socketParams(d.path, path);
116
+ if (params) return { decl: d, params };
117
+ }
118
+ return undefined;
119
+ };
120
+ const declFor = (request: Request): SocketDecl | undefined => matchFor(request)?.decl;
90
121
  const byPeer = new WeakMap<WebSocketPeer, Open>();
91
122
  const key = keyOf(service, root);
92
123
  return {
@@ -99,7 +130,7 @@ export function socketUpgrade<C>(
99
130
  return declared ? offered.find((p) => declared.includes(p)) : offered[0];
100
131
  },
101
132
  open: (peer, request) => {
102
- const decl = declFor(request)!;
133
+ const { decl, params } = matchFor(request)!;
103
134
  const engine = engines[decl.id]! as SocketEngine<unknown>;
104
135
  const pending: Open['pending'] = new Map();
105
136
  const session: SocketSession = {
@@ -108,7 +139,7 @@ export function socketUpgrade<C>(
108
139
  close: (code, reason) => { peer.close(code, reason); },
109
140
  expect: (key, ms) => new Promise((resolve, reject) => {
110
141
  const timer = setTimeout(() => { pending.delete(key); reject(new Error('no reply in time')); }, ms);
111
- pending.set(key, { resolve, reject, timer });
142
+ pending.set(key, { resolve, reject, timer, due: Date.parse(worldNow()) + ms });
112
143
  }),
113
144
  fulfil: (key, value) => {
114
145
  const p = pending.get(key);
@@ -118,7 +149,7 @@ export function socketUpgrade<C>(
118
149
  },
119
150
  decide,
120
151
  };
121
- const o: Open = { session, engine, context: () => context(request.clone(), decl), queue: Promise.resolve(), pending };
152
+ const o: Open = { session, engine, context: () => context(request.clone(), decl, params), queue: Promise.resolve(), pending };
122
153
  byPeer.set(peer, o);
123
154
  (sessions.get(key) ?? sessions.set(key, new Set()).get(key)!).add(o);
124
155
  if (engine.open) o.queue = o.queue.then(async () => engine.open!(session, await o.context())).catch((error) => session.close(1011, String((error as Error).message ?? error).slice(0, 120)));
@@ -41,6 +41,50 @@ async function worldTwins(): Promise<Record<string, string>> {
41
41
  return { ...twinOrigins(env), ...twinOrigins(instanceEnv) };
42
42
  }
43
43
 
44
+ /** The hosts each claiming twin last listed at its `hostsClaimed` door, by the door's address, and when; read again
45
+ * after CLAIMS_TTL_MS or for a host it lacks, as the injector polls the doors every five seconds (inject.cjs, "HOSTS A TWIN CLAIMS WHILE IT
46
+ * RUNS"). */
47
+ const claimsRead = new Map<string, { hosts: Set<string>; at: number }>();
48
+ const CLAIMS_TTL_MS = 5_000;
49
+
50
+ /** The hosts a twin claims at its door: in process (a journey walk's companion) or at its address, within the injector's
51
+ * first-read deadline (inject.cjs CLAIMS_WAIT_MS, 1500 ms, measured beside it); a door that does not answer claims
52
+ * nothing. */
53
+ async function claimedHosts(at: string, ask: () => Promise<Response>, host: string): Promise<Set<string>> {
54
+ const held = claimsRead.get(at);
55
+ // a host the held list lacks is asked for again once, as the injector refreshes before it refuses: a domain claimed a
56
+ // moment ago is found
57
+ if (held && Date.now() - held.at < CLAIMS_TTL_MS && held.hosts.has(host)) return held.hosts;
58
+ let hosts = new Set<string>();
59
+ try {
60
+ const r = await ask();
61
+ const body = r.ok ? await r.json() as { hosts?: unknown[] } : null;
62
+ hosts = new Set((body?.hosts ?? []).map((h) => String(h).toLowerCase()));
63
+ } catch { /* an unanswered door claims nothing */ }
64
+ claimsRead.set(at, { hosts, at: Date.now() });
65
+ return hosts;
66
+ }
67
+
68
+ /** The twin that claims a host at its `hostsClaimed` door (an R2 bucket's custom domain, a Worker's), as the injector
69
+ * routes it: a twin this process answers first, the World's twins after. */
70
+ async function claimingTwin(target: URL, twins: Record<string, string>): Promise<{ vendor: string; local: boolean } | undefined> {
71
+ const { packFacts } = await import('../pack-facts.cjs') as { packFacts: () => { packs: Record<string, { hostsClaimed?: { door?: string } }> } };
72
+ const host = target.hostname.toLowerCase();
73
+ const key = process.env.VOLTER_TWINS_KEY;
74
+ const doors = Object.entries(packFacts().packs).flatMap(([vendor, facts]) => (facts.hostsClaimed?.door ? [[vendor, facts.hostsClaimed.door] as const] : []));
75
+ for (const [vendor, door] of doors) {
76
+ const twin = inProcessTwins[vendor];
77
+ if (twin && (await claimedHosts(`in-process:${vendor}${door}`, async () => twin(new Request(`http://${vendor}.twin${door}`)), host)).has(host)) return { vendor, local: true };
78
+ }
79
+ for (const [vendor, door] of doors) {
80
+ const origin = twins[vendor];
81
+ if (origin === undefined) continue;
82
+ const at = `${origin.replace(/\/$/, '')}${door}`;
83
+ if ((await claimedHosts(at, () => fetch(at, { headers: key ? { 'x-twins-key': key } : {}, signal: AbortSignal.timeout(1500) }), host)).has(host)) return { vendor, local: false };
84
+ }
85
+ return undefined;
86
+ }
87
+
44
88
  /** One URL answered only by a World-held application or vendor twin: the fetch's application stand-in first (its answer
45
89
  * is the application's; `'unreachable'` leaves no application to reach, the World's twins still reachable). */
46
90
  async function worldResponse(target: URL, init: RequestInit, standIn?: ApplicationStandIn): Promise<Response> {
@@ -67,8 +111,18 @@ async function worldResponse(target: URL, init: RequestInit, standIn?: Applicati
67
111
  if (key && !headers.has('x-twins-key')) headers.set('x-twins-key', key);
68
112
  return fetch(`${twins[vendor]!.replace(/\/$/, '')}${target.pathname}${target.search}`, { ...init, headers, redirect: 'manual' });
69
113
  }
114
+ // the application's own hosts before any twin's claimed ones: a call to the application asks no door
70
115
  const { appDestination, appFetch } = await import('../app-route.cjs') as { appDestination: (url: URL) => unknown; appFetch: (url: URL, init: RequestInit) => Promise<Response> };
71
116
  if (scripted !== 'unreachable' && appDestination(target)) return appFetch(target, { ...init, redirect: 'manual' });
117
+ const claimer = await claimingTwin(target, twins);
118
+ if (claimer !== undefined) {
119
+ const headers = new Headers(init.headers);
120
+ headers.set('x-volter-twin-original-host', target.host);
121
+ if (claimer.local) return inProcessTwins[claimer.vendor]!(new Request(target, { ...init, headers, redirect: 'manual' }));
122
+ const key = process.env.VOLTER_TWINS_KEY;
123
+ if (key && !headers.has('x-twins-key')) headers.set('x-twins-key', key);
124
+ return fetch(`${twins[claimer.vendor]!.replace(/\/$/, '')}${target.pathname}${target.search}`, { ...init, headers, redirect: 'manual' });
125
+ }
72
126
  throw new VendorUnreachableError(`${target.host}${target.pathname}: this World holds no application or vendor twin at this URL`);
73
127
  }
74
128