@volter/world-core 3.0.37 → 3.0.39
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/app-route.cjs +26 -25
- package/app-route.d.cts +1 -0
- package/dist/app-route.cjs +26 -25
- package/dist/app-route.d.cts +1 -0
- package/dist/generated/pack-facts.json +409 -352
- package/dist/inject.cjs +474 -95
- package/dist/network-policy.cjs +108 -13
- package/dist/network-policy.d.cts +8 -3
- package/dist/src/actions.js +2 -1
- package/dist/src/anthropic-wire.d.ts +2 -0
- package/dist/src/anthropic-wire.js +5 -0
- package/dist/src/attestation.d.ts +6 -0
- package/dist/src/attestation.js +27 -0
- package/dist/src/bytes.d.ts +3 -0
- package/dist/src/bytes.js +17 -0
- package/dist/src/changeset.js +2 -1
- package/dist/src/clickhouse/sql.js +73 -14
- package/dist/src/derived-core.d.ts +210 -32
- package/dist/src/derived-core.js +510 -244
- package/dist/src/derived-real.d.ts +1 -1
- package/dist/src/derived-real.js +704 -93
- package/dist/src/derived.d.ts +18 -2
- package/dist/src/derived.js +21 -2
- package/dist/src/events.d.ts +28 -45
- package/dist/src/events.js +40 -58
- package/dist/src/exact-json.d.ts +10 -0
- package/dist/src/exact-json.js +98 -0
- package/dist/src/executor.d.ts +17 -0
- package/dist/src/executor.js +91 -30
- package/dist/src/file-response.js +1 -1
- package/dist/src/form.d.ts +8 -0
- package/dist/src/form.js +76 -0
- package/dist/src/git/format.d.ts +16 -0
- package/dist/src/git/format.js +110 -0
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +1 -0
- package/dist/src/git/lfs.d.ts +1 -1
- package/dist/src/git/lfs.js +4 -3
- package/dist/src/graphql-wire.d.ts +6 -0
- package/dist/src/graphql-wire.js +6 -3
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/head.d.ts +1 -0
- package/dist/src/head.js +15 -4
- package/dist/src/held-reads.d.ts +6 -0
- package/dist/src/held-reads.js +60 -0
- package/dist/src/history.js +5 -4
- package/dist/src/host-port.d.ts +5 -0
- package/dist/src/host-port.js +14 -0
- package/dist/src/index.d.ts +11 -3
- package/dist/src/index.js +9 -4
- package/dist/src/log.js +12 -7
- package/dist/src/machines.d.ts +81 -5
- package/dist/src/machines.js +125 -9
- package/dist/src/managed-database.d.ts +15 -11
- package/dist/src/managed-database.js +38 -37
- package/dist/src/multipart.d.ts +21 -3
- package/dist/src/multipart.js +112 -38
- package/dist/src/observe.d.ts +3 -0
- package/dist/src/observe.js +6 -2
- package/dist/src/openai-wire.d.ts +16 -0
- package/dist/src/openai-wire.js +43 -12
- package/dist/src/pack-fetch.d.ts +4 -6
- package/dist/src/pack-fetch.js +189 -57
- package/dist/src/packRegistry.d.ts +7 -1
- package/dist/src/packRegistry.js +20 -3
- package/dist/src/private-transport.d.ts +1 -0
- package/dist/src/private-transport.js +11 -0
- package/dist/src/protobuf.js +10 -4
- package/dist/src/rateBudget.d.ts +8 -1
- package/dist/src/rateBudget.js +1 -0
- package/dist/src/redis/engine.d.ts +14 -5
- package/dist/src/redis/engine.js +271 -72
- package/dist/src/redis/frames.d.ts +17 -0
- package/dist/src/redis/frames.js +123 -0
- package/dist/src/redis/index.d.ts +6 -0
- package/dist/src/redis/index.js +6 -0
- package/dist/src/redis/json.d.ts +2 -0
- package/dist/src/redis/json.js +14 -0
- package/dist/src/redis/lua-libs.d.ts +4 -0
- package/dist/src/redis/lua-libs.js +173 -0
- package/dist/src/redis/lua.d.ts +1 -0
- package/dist/src/redis/lua.js +36 -0
- package/dist/src/redis/member-storage.d.ts +5 -0
- package/dist/src/redis/member-storage.js +75 -0
- package/dist/src/redis/resp2.d.ts +4 -0
- package/dist/src/redis/resp2.js +29 -0
- package/dist/src/redis/stream.d.ts +14 -0
- package/dist/src/redis/stream.js +335 -0
- package/dist/src/remote-execute.d.ts +2 -2
- package/dist/src/request-body.d.ts +11 -0
- package/dist/src/request-body.js +11 -0
- package/dist/src/runtime.d.ts +6 -5
- package/dist/src/runtime.js +5 -5
- package/dist/src/s3/wire.d.ts +4 -4
- package/dist/src/s3/wire.js +5 -3
- package/dist/src/scenario.d.ts +22 -0
- package/dist/src/scenario.js +35 -31
- package/dist/src/serve-http.d.ts +2 -1
- package/dist/src/serve-http.js +10 -1
- package/dist/src/serve.js +7 -4
- package/dist/src/signing.d.ts +23 -5
- package/dist/src/signing.js +74 -11
- package/dist/src/sigv4.d.ts +3 -1
- package/dist/src/sigv4.js +4 -2
- package/dist/src/smtp.js +2 -1
- package/dist/src/sockets.js +8 -3
- package/dist/src/state-system.d.ts +3 -0
- package/dist/src/storage.js +5 -2
- package/dist/src/twin-fetch.d.ts +3 -1
- package/dist/src/twin-fetch.js +3 -2
- package/dist/src/vendor-call.d.ts +10 -3
- package/dist/src/vendor-call.js +58 -14
- package/dist/test-fixtures/attestation.SOURCE.md +1 -0
- package/dist/test-fixtures/attestation.json +50 -0
- package/generated/pack-facts.json +409 -352
- package/inject.cjs +474 -95
- package/network-policy.cjs +108 -13
- package/network-policy.d.cts +8 -3
- package/package.json +11 -4
- package/src/actions.ts +2 -1
- package/src/anthropic-wire.ts +6 -0
- package/src/attestation.ts +23 -0
- package/src/bytes.ts +21 -0
- package/src/changeset.ts +2 -1
- package/src/clickhouse/sql.ts +50 -16
- package/src/derived-core.ts +573 -249
- package/src/derived-real.ts +552 -42
- package/src/derived.ts +23 -4
- package/src/events.ts +54 -85
- package/src/exact-json.ts +71 -0
- package/src/executor.ts +86 -27
- package/src/file-response.ts +1 -1
- package/src/form.ts +66 -0
- package/src/git/format.ts +89 -0
- package/src/git/index.ts +1 -0
- package/src/git/lfs.ts +5 -4
- package/src/graphql-wire.ts +8 -3
- package/src/grpc-wire.ts +2 -1
- package/src/head.ts +15 -5
- package/src/held-reads.ts +54 -0
- package/src/history.ts +5 -4
- package/src/host-port.ts +9 -0
- package/src/index.ts +10 -4
- package/src/log.ts +11 -7
- package/src/machines.ts +135 -13
- package/src/managed-database.ts +48 -40
- package/src/multipart.ts +110 -35
- package/src/observe.ts +6 -3
- package/src/openai-wire.ts +45 -14
- package/src/pack-fetch.ts +173 -50
- package/src/packRegistry.ts +27 -4
- package/src/private-transport.ts +8 -0
- package/src/protobuf.ts +8 -4
- package/src/rateBudget.ts +6 -1
- package/src/redis/engine.ts +168 -63
- package/src/redis/frames.ts +53 -0
- package/src/redis/index.ts +6 -0
- package/src/redis/json.ts +13 -0
- package/src/redis/lua-libs.ts +62 -0
- package/src/redis/lua.ts +20 -0
- package/src/redis/member-storage.ts +58 -0
- package/src/redis/resp2.ts +25 -0
- package/src/redis/stream.ts +102 -0
- package/src/remote-execute.ts +2 -2
- package/src/request-body.ts +12 -0
- package/src/runtime.ts +6 -5
- package/src/s3/wire.ts +8 -5
- package/src/scenario.ts +34 -13
- package/src/serve-http.ts +11 -2
- package/src/serve.ts +4 -3
- package/src/signing.ts +71 -17
- package/src/sigv4.ts +5 -3
- package/src/smtp.ts +3 -2
- package/src/sockets.ts +8 -3
- package/src/state-system.ts +3 -0
- package/src/storage.ts +5 -2
- package/src/twin-fetch.ts +4 -3
- package/src/vendor-call.ts +59 -12
- package/test-fixtures/attestation.json +50 -0
package/src/openai-wire.ts
CHANGED
|
@@ -14,7 +14,7 @@ const obj = (v: unknown): Row => (v && typeof v === 'object' && !Array.isArray(v
|
|
|
14
14
|
const arr = (v: unknown): Row[] => (Array.isArray(v) ? v as Row[] : []);
|
|
15
15
|
|
|
16
16
|
/** A turn: the text, or tool calls, the reasoning when there is some, and why it finished. */
|
|
17
|
-
export type WireTurn = { text?: string | null; toolCalls?: Array<{ name: string; arguments: Row }>; reasoning?: string | null; finishReason?: string };
|
|
17
|
+
export type WireTurn = { text?: string | null; toolCalls?: Array<{ name: string; arguments: Row }>; reasoning?: string | null; reasoningDetails?: Row[]; finishReason?: string };
|
|
18
18
|
|
|
19
19
|
/** Tokens, estimated at about four characters a token: a twin's count, not a tokenizer's. */
|
|
20
20
|
export const estimateTokens = (text: string): number => Math.max(1, Math.ceil(text.length / 4));
|
|
@@ -91,7 +91,7 @@ function chatParts(s: ChatShape): { text: string | null; finish: string; calls:
|
|
|
91
91
|
export function chatCompletion(s: ChatShape): Row {
|
|
92
92
|
const p = chatParts(s);
|
|
93
93
|
const field = s.reasoningField ?? 'reasoning';
|
|
94
|
-
const message = { role: 'assistant', content: p.text, refusal: null, ...(p.reasoning ? { [field]: p.reasoning } : {}), ...(p.calls.length ? { tool_calls: p.calls } : {}) };
|
|
94
|
+
const message = { role: 'assistant', content: p.text, refusal: null, ...(p.reasoning ? { [field]: p.reasoning } : {}), ...(s.turn.reasoningDetails ? { reasoning_details: s.turn.reasoningDetails } : {}), ...(p.calls.length ? { tool_calls: p.calls } : {}) };
|
|
95
95
|
return {
|
|
96
96
|
id: s.id, ...(s.extra ?? {}), model: s.model, object: 'chat.completion', created: s.created,
|
|
97
97
|
choices: [{ index: 0, message, logprobs: null, finish_reason: p.finish, ...(s.nativeFinish ? { native_finish_reason: p.finish } : {}) }], usage: p.usage,
|
|
@@ -101,21 +101,22 @@ export function chatCompletion(s: ChatShape): Row {
|
|
|
101
101
|
/** A chat completion streamed: its chunks (`object: chat.completion.chunk`) as Server-Sent Events, the text in two
|
|
102
102
|
* deltas, the last chunk carrying the finish reason and the usage, then `data: [DONE]`. `comment` is a keep-alive a
|
|
103
103
|
* vendor sends first (OpenRouter's `: OPENROUTER PROCESSING`). */
|
|
104
|
-
export function chatCompletionSSE(s: ChatShape & { comment?: string }): string {
|
|
104
|
+
export function chatCompletionSSE(s: ChatShape & { comment?: string; separateUsageChunk?: boolean }): string {
|
|
105
105
|
const p = chatParts(s);
|
|
106
106
|
const field = s.reasoningField ?? 'reasoning';
|
|
107
|
-
const chunk = (delta: Row, last: boolean): string => `data: ${JSON.stringify({
|
|
107
|
+
const chunk = (delta: Row, last: boolean, usage = last): string => `data: ${JSON.stringify({
|
|
108
108
|
id: s.id, ...((last ? s.extra : s.interimExtra ?? s.extra) ?? {}), model: s.model, object: 'chat.completion.chunk', created: s.created,
|
|
109
109
|
choices: [{ index: 0, delta, logprobs: null, finish_reason: last ? p.finish : null, ...(s.nativeFinish ? { native_finish_reason: last ? p.finish : null } : {}) }],
|
|
110
|
-
...(
|
|
110
|
+
...(usage ? { usage: p.usage } : {}),
|
|
111
111
|
})}\n\n`;
|
|
112
112
|
const half = p.text ? Math.ceil(p.text.length / 2) : 0;
|
|
113
113
|
return [
|
|
114
114
|
...(s.comment ? [`: ${s.comment}\n\n`] : []),
|
|
115
|
-
...(p.reasoning ? [chunk({ role: 'assistant', content: '', [field]: p.reasoning }, false)] : []),
|
|
115
|
+
...(p.reasoning || s.turn.reasoningDetails ? [chunk({ role: 'assistant', content: '', ...(p.reasoning ? { [field]: p.reasoning } : {}), ...(s.turn.reasoningDetails ? { reasoning_details: s.turn.reasoningDetails } : {}) }, false)] : []),
|
|
116
116
|
...(p.text ? [chunk({ role: 'assistant', content: p.text.slice(0, half) }, false), ...(p.text.length > half ? [chunk({ content: p.text.slice(half) }, false)] : [])] : []),
|
|
117
117
|
...(p.calls.length ? [chunk({ role: 'assistant', content: null, tool_calls: p.calls.map((c, i) => ({ index: i, ...c })) }, false)] : []),
|
|
118
|
-
chunk({}, true),
|
|
118
|
+
chunk(s.separateUsageChunk ? { role: 'assistant', content: '' } : {}, true, !s.separateUsageChunk),
|
|
119
|
+
...(s.separateUsageChunk ? [chunk({ role: 'assistant', content: '' }, true, true)] : []),
|
|
119
120
|
'data: [DONE]\n\n',
|
|
120
121
|
].join('');
|
|
121
122
|
}
|
|
@@ -229,12 +230,15 @@ export type ResponsesShape = {
|
|
|
229
230
|
/** ids for the output items: the message, the reasoning, each function call and its call id */
|
|
230
231
|
itemId: (kind: 'msg' | 'rs' | 'fc' | 'call', i: number) => string;
|
|
231
232
|
extra?: Row;
|
|
233
|
+
/** Vendor defaults; explicit request values win, including nullable gateway defaults. */
|
|
234
|
+
defaults?: Row;
|
|
232
235
|
};
|
|
233
236
|
|
|
234
237
|
/** A Responses object (`object: response`, `status: completed`): its output items (the reasoning when there is some,
|
|
235
238
|
* the message with its output text, or function calls) and its usage. */
|
|
236
239
|
export function responsesAnswer(s: ResponsesShape): Row {
|
|
237
240
|
const b = s.body;
|
|
241
|
+
const defaultOf = (field: string, fallback: unknown): unknown => s.defaults && Object.hasOwn(s.defaults, field) ? s.defaults[field] : fallback;
|
|
238
242
|
let text = s.turn.text ?? null;
|
|
239
243
|
const max = typeof b.max_output_tokens === 'number' ? b.max_output_tokens : undefined;
|
|
240
244
|
let incomplete: Row | null = null;
|
|
@@ -251,10 +255,10 @@ export function responsesAnswer(s: ResponsesShape): Row {
|
|
|
251
255
|
id: s.id, object: 'response', created_at: s.createdAt, status: incomplete ? 'incomplete' : 'completed', ...(s.extra ?? {}),
|
|
252
256
|
error: null, incomplete_details: incomplete, instructions: b.instructions ?? null, max_output_tokens: max ?? null, model: s.model, output,
|
|
253
257
|
parallel_tool_calls: b.parallel_tool_calls ?? true, previous_response_id: b.previous_response_id ?? null,
|
|
254
|
-
reasoning: { effort: obj(b.reasoning).effort ?? null, summary: obj(b.reasoning).summary ?? null }, store: b.store ?? true,
|
|
255
|
-
temperature: b.temperature ?? 1, text: b.text ?? { format: { type: 'text' } }, tool_choice: b.tool_choice ?? 'auto', tools: b.tools ?? [], top_p: b.top_p ?? 1,
|
|
258
|
+
reasoning: { effort: obj(b.reasoning).effort ?? null, summary: obj(b.reasoning).summary ?? null }, store: b.store ?? defaultOf('store', true),
|
|
259
|
+
temperature: b.temperature ?? defaultOf('temperature', 1), text: b.text ?? { format: { type: 'text' } }, tool_choice: b.tool_choice ?? 'auto', tools: b.tools ?? [], top_p: b.top_p ?? defaultOf('top_p', 1),
|
|
256
260
|
usage: { input_tokens: input, input_tokens_details: { cached_tokens: 0 }, output_tokens: out + reasoningTokens, output_tokens_details: { reasoning_tokens: reasoningTokens }, total_tokens: input + out + reasoningTokens },
|
|
257
|
-
metadata: b.metadata ?? {},
|
|
261
|
+
metadata: b.metadata ?? defaultOf('metadata', {}),
|
|
258
262
|
};
|
|
259
263
|
}
|
|
260
264
|
|
|
@@ -278,7 +282,14 @@ export function responsesSSE(s: ResponsesShape): string {
|
|
|
278
282
|
events.push({ type: 'response.function_call_arguments.delta', item_id: item.id, output_index: i, delta: item.arguments });
|
|
279
283
|
events.push({ type: 'response.function_call_arguments.done', item_id: item.id, output_index: i, arguments: item.arguments });
|
|
280
284
|
} else {
|
|
281
|
-
|
|
285
|
+
// announced empty, as a message item is: an SDK's accumulator appends each summary part the events below add
|
|
286
|
+
events.push({ type: 'response.output_item.added', output_index: i, item: { ...item, summary: [], status: 'in_progress' } });
|
|
287
|
+
for (const [n, summary] of arr(item.summary).entries()) {
|
|
288
|
+
events.push({ type: 'response.reasoning_summary_part.added', item_id: item.id, output_index: i, summary_index: n, part: { type: 'summary_text', text: '' } });
|
|
289
|
+
events.push({ type: 'response.reasoning_summary_text.delta', item_id: item.id, output_index: i, summary_index: n, delta: summary.text });
|
|
290
|
+
events.push({ type: 'response.reasoning_summary_text.done', item_id: item.id, output_index: i, summary_index: n, text: summary.text });
|
|
291
|
+
events.push({ type: 'response.reasoning_summary_part.done', item_id: item.id, output_index: i, summary_index: n, part: summary });
|
|
292
|
+
}
|
|
282
293
|
}
|
|
283
294
|
events.push({ type: 'response.output_item.done', output_index: i, item });
|
|
284
295
|
}
|
|
@@ -298,18 +309,19 @@ export const forbidsTools = (choice: unknown): boolean => choice === 'none' || o
|
|
|
298
309
|
|
|
299
310
|
/** The turn a scenario's decision scripts (`respond`: text, toolCalls, reasoning, finishReason), or undefined. */
|
|
300
311
|
export function scenarioTurn(decision: ScenarioDecision | undefined): WireTurn | undefined {
|
|
301
|
-
if (!decision || decision.kind !== 'handler') return undefined;
|
|
312
|
+
if (!decision || decision.kind !== 'handler' || decision.fault?.kind === 'generation') return undefined;
|
|
302
313
|
const r = obj(decision.respond);
|
|
303
314
|
return {
|
|
304
315
|
...(typeof r.text === 'string' ? { text: r.text } : {}),
|
|
305
316
|
...(r.toolCalls !== undefined ? { toolCalls: (Array.isArray(r.toolCalls) ? r.toolCalls : [r.toolCalls]) as WireTurn['toolCalls'] } : {}),
|
|
306
317
|
...(typeof r.reasoning === 'string' ? { reasoning: r.reasoning } : {}),
|
|
318
|
+
...(Array.isArray(r.reasoningDetails) ? { reasoningDetails: r.reasoningDetails as Row[] } : {}),
|
|
307
319
|
...(typeof r.finishReason === 'string' ? { finishReason: r.finishReason } : {}),
|
|
308
320
|
};
|
|
309
321
|
}
|
|
310
322
|
|
|
311
323
|
const nonEmpty = (v: unknown): v is string => typeof v === 'string' && v.length > 0;
|
|
312
|
-
const RESPOND_KEYS = new Set(['text', 'toolCalls', 'reasoning', 'finishReason']);
|
|
324
|
+
const RESPOND_KEYS = new Set(['text', 'toolCalls', 'reasoning', 'reasoningDetails', 'finishReason']);
|
|
313
325
|
|
|
314
326
|
/** A request as the scenario reads it: its model, its text, its last user text, the tools it offers and the tool
|
|
315
327
|
* results it ends with, from a chat completion's messages or a Responses input. */
|
|
@@ -347,10 +359,11 @@ export function scenario(vendor: string, operations: string[], faultBody: (statu
|
|
|
347
359
|
return null;
|
|
348
360
|
},
|
|
349
361
|
validateRespond: (respond) => {
|
|
350
|
-
if (typeof respond !== 'object' || respond === null || Array.isArray(respond)) return 'respond is an object { text?, toolCalls?, reasoning?, finishReason? }';
|
|
362
|
+
if (typeof respond !== 'object' || respond === null || Array.isArray(respond)) return 'respond is an object { text?, toolCalls?, reasoning?, reasoningDetails?, finishReason? }';
|
|
351
363
|
const r = respond as Row;
|
|
352
364
|
for (const k of Object.keys(r)) if (!RESPOND_KEYS.has(k)) return `respond: unknown key "${k}" (valid: ${[...RESPOND_KEYS].join(', ')})`;
|
|
353
365
|
if (r.text !== undefined && typeof r.text !== 'string') return 'respond.text is a string';
|
|
366
|
+
if (r.reasoningDetails !== undefined && (!Array.isArray(r.reasoningDetails) || r.reasoningDetails.some((d) => !d || typeof d !== 'object' || Array.isArray(d)))) return 'respond.reasoningDetails is an array of JSON objects';
|
|
354
367
|
if (r.reasoning !== undefined && typeof r.reasoning !== 'string') return 'respond.reasoning is a string';
|
|
355
368
|
for (const c of r.toolCalls === undefined ? [] : Array.isArray(r.toolCalls) ? r.toolCalls : [r.toolCalls]) {
|
|
356
369
|
const u = c as Row;
|
|
@@ -369,3 +382,21 @@ export function scenario(vendor: string, operations: string[], faultBody: (statu
|
|
|
369
382
|
};
|
|
370
383
|
return { adapter, operations, request };
|
|
371
384
|
}
|
|
385
|
+
|
|
386
|
+
/** An admitted generation that fails: partial JSON or actual Chat Completions error frames.
|
|
387
|
+
* The pack supplies its error envelope/type; the shared wire owns the SDK framing. */
|
|
388
|
+
export function chatCompletionFailure(s: { id: string; model: string; created: number; error: Row; text?: string; extra?: Row }, stream: boolean): Row | string {
|
|
389
|
+
const common = { id: s.id, model: s.model, created: s.created, ...s.extra };
|
|
390
|
+
if (!stream) return { ...common, object: 'chat.completion', choices: [{ index: 0, message: { role: 'assistant', content: s.text ?? '' }, finish_reason: 'error', error: s.error }] };
|
|
391
|
+
const frames: Row[] = [];
|
|
392
|
+
if (s.text) frames.push({ ...common, object: 'chat.completion.chunk', choices: [{ index: 0, delta: { role: 'assistant', content: s.text }, finish_reason: null }] });
|
|
393
|
+
frames.push({ ...common, object: 'chat.completion.chunk', choices: [{ index: 0, delta: { content: '' }, finish_reason: 'error' }], error: s.error });
|
|
394
|
+
return frames.map((f) => `data: ${JSON.stringify(f)}\n\n`).join('') + 'data: [DONE]\n\n';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Responses generation failures keep the response's canonical fields and native error code. */
|
|
398
|
+
export function responsesFailure(response: Row, stream: boolean, event = 'response.failed'): Row | string {
|
|
399
|
+
if (!stream) return response;
|
|
400
|
+
const data = event === 'response.failed' ? { type: event, response } : { type: event, error: response.error };
|
|
401
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
402
|
+
}
|
package/src/pack-fetch.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseExactJson } from './exact-json.ts';
|
|
2
|
+
import { readHostPort } from './host-port.ts';
|
|
1
3
|
// A Protocol 3 pack's whole HTTP surface, served by the kernel from the pack's declarations (docs/contributing/
|
|
2
4
|
// architecture.md, "The pack contract: the kernel's root"): the discovery door (`GET /twin`, the manifest's
|
|
3
5
|
// `discovery`), the vendor's own moves as time passes (`semantics/clock.ts`), the World's doors (the manifest's `doors`,
|
|
@@ -10,7 +12,7 @@ import { placedCookies, TWIN_PREFIX_HEADER } from './twin-fetch.ts';
|
|
|
10
12
|
import { PREFIX_PARAMS_HEADER } from './derived.ts';
|
|
11
13
|
import { withHeaders } from './with-headers.ts';
|
|
12
14
|
import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, coreFor, crossCutting, derivedRequestScopes, vendorError, type CoreScope, type DerivedManifest, type ErrorSpec, type Handler, type HandlerContext, type LanesDecl, type VendorManifest } from './derived-core.ts';
|
|
13
|
-
import { compileSurface, createDerivedFetch, matchOperation, type DerivedFetch, type DerivedOperation, type DerivedOwner, type DerivedSurface, type Workspace, type BoardFrame } from './derived.ts';
|
|
15
|
+
import { compileSurface, createDerivedFetch, matchOperation, pathParams, type DerivedFetch, type DerivedOperation, type DerivedOwner, type DerivedSurface, type Workspace, type BoardFrame } from './derived.ts';
|
|
14
16
|
import { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, runAsVendorMove } from './request-scope.ts';
|
|
15
17
|
import { parseScenarioDocument, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest, type PackScenarioAdapter } from './scenario.ts';
|
|
16
18
|
import { getActiveWorldStore } from './world-store.ts';
|
|
@@ -25,10 +27,13 @@ import { hostRules } from '../host-routing.cjs';
|
|
|
25
27
|
/** A descriptor's host rules, as data. */
|
|
26
28
|
type PackDescriptorHosts = ReadonlyArray<HostRule> | undefined;
|
|
27
29
|
import { socketUpgrade, type SocketEngine } from './sockets.ts';
|
|
28
|
-
import {
|
|
30
|
+
import type { WebSocketUpgrade, WebSocketPeer } from './serve-http.ts';
|
|
31
|
+
import { MACHINE_POOL, POOL_KINDS, resetFakePool } from './machines.ts';
|
|
29
32
|
import { applyTwinWrite } from './serve.ts';
|
|
30
33
|
import { graphqlEndpoint, type GraphqlPart } from './graphql-wire.ts';
|
|
31
|
-
import {
|
|
34
|
+
import { gzipSync } from 'node:zlib';
|
|
35
|
+
import { decompress } from './bytes.ts';
|
|
36
|
+
import { decodedBodyLimit, oversizedBody, RequestBodyTooLargeError } from './request-body.ts';
|
|
32
37
|
|
|
33
38
|
/** What a pack declares and writes, and nothing it assembles itself. */
|
|
34
39
|
export type PackParts = {
|
|
@@ -80,6 +85,7 @@ export type LanePart = {
|
|
|
80
85
|
serves?: (method: string, path: string) => boolean; boards?: boolean; draws?: (url: string) => boolean;
|
|
81
86
|
/** the hosts the lane's built content screens name: its vendor's router sends `<base>/@<host>/` there */
|
|
82
87
|
contentHosts?: ReadonlyArray<string>;
|
|
88
|
+
upgrade?: WebSocketUpgrade;
|
|
83
89
|
};
|
|
84
90
|
manifest: { doors?: ReadonlyArray<{ method: string; path: string }> };
|
|
85
91
|
};
|
|
@@ -112,6 +118,8 @@ function pathMatcher(pattern: string, prefix: boolean): (path: string) => Match
|
|
|
112
118
|
}
|
|
113
119
|
/** A declared host (`dash.cloudflare.com`, `<domain>.daily.co`) as a matcher of a request's host. */
|
|
114
120
|
function hostMatcher(pattern: string): (host: string) => Match | undefined {
|
|
121
|
+
const whole = /^(?:\{([^}]+)\}|<([^>]+)>)$/.exec(pattern);
|
|
122
|
+
if (whole) return (host) => host ? { params: { [whole[1] ?? whole[2]!]: host } } : undefined;
|
|
115
123
|
const names: string[] = [];
|
|
116
124
|
const re = new RegExp(`^${pattern.split('.').map((label) => {
|
|
117
125
|
const m = /^(?:\{([^}]+)\}|<([^>]+)>)$/.exec(label);
|
|
@@ -125,7 +133,7 @@ function hostMatcher(pattern: string): (host: string) => Match | undefined {
|
|
|
125
133
|
};
|
|
126
134
|
}
|
|
127
135
|
/** The host a request is for: the vendor host the World's injector names, else its own. */
|
|
128
|
-
const hostOf = (request: Request): string => (request.headers.get('x-volter-twin-original-host') ?? request.headers.get('host') ?? new URL(request.url).host)
|
|
136
|
+
const hostOf = (request: Request): string => readHostPort(request.headers.get('x-volter-twin-original-host') ?? request.headers.get('host') ?? new URL(request.url).host)?.host ?? '';
|
|
129
137
|
|
|
130
138
|
/** The answer for a path no operation, door or screen serves, and for an operation nothing models: the manifest's
|
|
131
139
|
* `gap`, its `{method}` and `{path}` filled, else the manifest's not-found refusal. */
|
|
@@ -137,25 +145,29 @@ function gapAnswer(m: DerivedManifest, request: Request, otherMethod = false): R
|
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
const METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
140
|
-
|
|
148
|
+
type ContentCoding = 'gzip' | 'zstd' | 'deflate' | 'br';
|
|
141
149
|
|
|
142
150
|
/** A request with its body decoded as its Content-Encoding says, when the manifest's `encodings` reads that coding;
|
|
143
151
|
* undefined when the bytes are not what the header names. */
|
|
144
|
-
async function decoded(m: DerivedManifest, request: Request): Promise<Request | undefined> {
|
|
152
|
+
async function decoded(m: DerivedManifest, request: Request): Promise<Request | Response | undefined> {
|
|
153
|
+
const codings = (request.headers.get('content-encoding') ?? '').split(',').map(c => c.trim().toLowerCase()).filter(Boolean);
|
|
154
|
+
if (codings.length > 1) return new Response('Unsupported Content-Encoding', { status: 415 });
|
|
145
155
|
if (request.method === 'GET' || request.method === 'HEAD' || request.body === null) return request;
|
|
146
156
|
// the body read once, here, and every part of the pack given a request made again from its bytes: a served request's
|
|
147
157
|
// body read through a clone and then read again (an `around` that checks a body before its handler does) tees a stream
|
|
148
158
|
// whose second branch a Bun server never finishes, and the request hung
|
|
149
159
|
const raw = new Uint8Array(await request.arrayBuffer());
|
|
150
|
-
const coding = (
|
|
160
|
+
const coding = (codings[0] ?? '') as ContentCoding;
|
|
151
161
|
if (!m.encodings?.request?.includes(coding)) return new Request(request.url, { method: request.method, headers: request.headers, body: raw });
|
|
152
162
|
let body: Uint8Array;
|
|
153
|
-
try { body =
|
|
163
|
+
try { body = await decompress(raw, coding, decodedBodyLimit(m.descriptor?.maxRequestBodySize)); }
|
|
164
|
+
catch (error) { return error instanceof RequestBodyTooLargeError ? oversizedBody() : undefined; }
|
|
154
165
|
const headers = new Headers(request.headers);
|
|
155
166
|
headers.delete('content-encoding');
|
|
156
167
|
headers.delete('content-length');
|
|
157
|
-
//
|
|
158
|
-
|
|
168
|
+
// A finite single-chunk stream holds the decoder's buffer; BufferSource extraction would copy the entire body.
|
|
169
|
+
const buffered = new ReadableStream<Uint8Array>({ start(controller) { controller.enqueue(body); controller.close(); } });
|
|
170
|
+
return new Request(request.url, { method: request.method, headers, body: buffered, duplex: 'half' } as RequestInit);
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
/** An answer in the coding the request accepts, when the manifest's `encodings` answers in it. */
|
|
@@ -171,6 +183,9 @@ async function encoded(m: DerivedManifest, request: Request, res: Response): Pro
|
|
|
171
183
|
}
|
|
172
184
|
|
|
173
185
|
/** A pack's HTTP surface, as the kernel serves it from the pack's parts. */
|
|
186
|
+
/** A door's path as it is served and listed: under `/_twin/`, whether the manifest writes the prefix or not. */
|
|
187
|
+
const doorPath = (path: string): string => (path.startsWith('/_twin/') ? path : `/_twin${path.startsWith('/') ? '' : '/'}${path}`);
|
|
188
|
+
|
|
174
189
|
export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}): DerivedFetch {
|
|
175
190
|
const m = parts.manifest;
|
|
176
191
|
const scope: CoreScope = {
|
|
@@ -195,7 +210,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
195
210
|
if (parts.gap) {
|
|
196
211
|
const url = new URL(request.url);
|
|
197
212
|
const matched = operation ? matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers) : undefined;
|
|
198
|
-
return parts.gap(await contextFor(m, { request, operation: operation ?? ({ id: 'unmatched', method: request.method, path: url.pathname, class: request.method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation), params: matched?.params ??
|
|
213
|
+
return parts.gap(await contextFor(m, { request, operation: operation ?? ({ id: 'unmatched', method: request.method, path: url.pathname, class: request.method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation), params: matched?.params ?? pathParams(routes, url.pathname) }, scope));
|
|
199
214
|
}
|
|
200
215
|
const url = new URL(request.url);
|
|
201
216
|
const other = !matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers)
|
|
@@ -224,7 +239,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
224
239
|
const raw = getActiveWorldStore().read(options.scenarioPath);
|
|
225
240
|
if (raw === null) throw new ScenarioError(`${m.vendor} scenario: no file at ${options.scenarioPath}`);
|
|
226
241
|
if (!engine || raw !== engineRaw) {
|
|
227
|
-
engine = new ScenarioEngine(parts.scenario.adapter, parseScenarioDocument(
|
|
242
|
+
engine = new ScenarioEngine(parts.scenario.adapter, parseScenarioDocument(parseExactJson(raw), parts.scenario.adapter));
|
|
228
243
|
engineRaw = raw;
|
|
229
244
|
}
|
|
230
245
|
return engine;
|
|
@@ -232,11 +247,13 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
232
247
|
/** The API, a turn the scenario decides handed its decision (ctx.scenario), or answered as the fault it names. */
|
|
233
248
|
const decided = async (arrived: Request): Promise<Response> => {
|
|
234
249
|
if (!parts.scenario) return api(arrived);
|
|
235
|
-
// the body is read once and the request made again from its bytes
|
|
236
|
-
//
|
|
237
|
-
//
|
|
250
|
+
// the body is read once and the request made again from its bytes, so the turn's context and the API each read their
|
|
251
|
+
// own. Merge served with --scenario once answered no turn in 8 s (t_c0279b69), and this change came with the cure;
|
|
252
|
+
// putting the earlier read back (the served request cloned for the context, then forwarded) does not reproduce the
|
|
253
|
+
// hang on its own (2026-10-01: merge's CLI with --scenario, and its life walked over HTTP: every turn answered), so
|
|
254
|
+
// what hung is UNEXPLAINED, and this is no proof of its cause
|
|
238
255
|
const bytes = arrived.method === 'GET' || arrived.method === 'HEAD' ? undefined : await arrived.arrayBuffer();
|
|
239
|
-
const request = new Request(arrived.url, { method: arrived.method, headers: arrived.headers, ...(bytes ? { body: bytes } : {}) });
|
|
256
|
+
const request = new Request(arrived.url, { method: arrived.method, headers: arrived.headers, signal: arrived.signal, ...(bytes ? { body: bytes } : {}) });
|
|
240
257
|
const url = new URL(request.url);
|
|
241
258
|
const matched = matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers);
|
|
242
259
|
if (!matched || !parts.scenario.operations.includes(matched.operation.id)) return api(request);
|
|
@@ -262,7 +279,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
262
279
|
const doors = (m.doors ?? []).map((d) => {
|
|
263
280
|
const answer = parts.doors?.[d.id];
|
|
264
281
|
if (!answer) throw new Error(`${m.vendor}: the manifest declares the door ${d.id}, and semantics/doors.ts exports no ${d.id}`);
|
|
265
|
-
return { decl: d, path: pathMatcher(
|
|
282
|
+
return { decl: d, path: pathMatcher(doorPath(d.path), false), answer };
|
|
266
283
|
});
|
|
267
284
|
const screens = (m.screens ?? []).filter((s) => s.status === 'done' && parts.screens?.[s.id]).map((s) => {
|
|
268
285
|
const hosts = [s.host, ...(s.hosts ?? [])].map(hostMatcher);
|
|
@@ -287,22 +304,49 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
287
304
|
// a context the kernel opens for what no surface operation names: a door, a screen, the clock, the board
|
|
288
305
|
const open = (request: Request, id: string, method: string, path: string, params: Record<string, string>): Promise<HandlerContext> =>
|
|
289
306
|
contextFor(m, { request, operation: { id, method, path, class: method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation, params }, scope);
|
|
307
|
+
const advanceClock = async (request: Request): Promise<void> => {
|
|
308
|
+
if (parts.clock) await runAsVendorMove(async () => parts.clock!(await open(new Request(request.url, m.tenant ? { headers: request.headers } : {}), 'clock', 'POST', '/_twin/clock', {})));
|
|
309
|
+
};
|
|
290
310
|
const refusedReadOnly = (): Response => vendorError(m, m.readOnly);
|
|
291
311
|
|
|
292
312
|
const prefix = m.pathPrefix ? new RegExp(`^(?:${m.pathPrefix})(?=/)`) : undefined;
|
|
313
|
+
const base = surface.basePath ?? '';
|
|
314
|
+
const baseAlias = m.basePathAlias && base ? new RegExp(`^(?:${m.basePathAlias})(?=/)`) : undefined;
|
|
293
315
|
const hostPath = m.pathFromHost ? new RegExp(m.pathFromHost, 'i') : undefined;
|
|
294
316
|
const hostParams = m.hostParams ? new RegExp(m.hostParams, 'i') : undefined;
|
|
295
|
-
|
|
317
|
+
// each operation's spanning parameters, keyed by the operation and the parameter as the route compiles them (a whole
|
|
318
|
+
// segment's label whose name the surface or the manifest declares spanning), so one operation's spanning name never
|
|
319
|
+
// keeps another's slashes, nor a label that only part of a segment holds
|
|
320
|
+
const spanningNames = new Set(surface.spanning ?? []);
|
|
321
|
+
const spanningOf = new Set(surface.operations.flatMap((op) => ((op.basePath ?? base) + op.path).split('/').flatMap((seg) => {
|
|
322
|
+
const label = /^\{([^}]+)\}$/.exec(seg)?.[1];
|
|
323
|
+
return label !== undefined && spanningNames.has(label) ? [`${op.id}\u0000${label}`] : [];
|
|
324
|
+
})));
|
|
325
|
+
/** 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
|
|
296
326
|
* path label (pathFromHost) put in front. */
|
|
327
|
+
const rewritten = (pathname: string, host: string): string => {
|
|
328
|
+
let path = pathname;
|
|
329
|
+
if (prefix) path = path.replace(prefix, '') || '/';
|
|
330
|
+
// a base the vendor serves as its own: the spec's base in its place (a path already under the spec's base stays)
|
|
331
|
+
if (baseAlias && !(path === base || path.startsWith(`${base}/`))) path = path.replace(baseAlias, base);
|
|
332
|
+
const label = hostPath?.exec(host)?.[1];
|
|
333
|
+
if (label) path = `/${label}${path === '/' ? '' : path}`;
|
|
334
|
+
return path;
|
|
335
|
+
};
|
|
336
|
+
/** The request as its vendor routes it: duplicate slashes collapsed, unless the rewritten path matches an operation
|
|
337
|
+
* through one of that operation's spanning parameters (the surface's and the manifest's), whose value keeps its
|
|
338
|
+
* slashes; then rewritten. */
|
|
297
339
|
const routed = (request: Request): Request => {
|
|
298
340
|
const url = new URL(request.url);
|
|
299
|
-
|
|
341
|
+
const host = hostOf(request);
|
|
342
|
+
const kept = rewritten(url.pathname, host);
|
|
343
|
+
const matched = matchOperation(routes, request.method, kept);
|
|
344
|
+
const spanning = matched !== undefined && Object.keys(matched.params).some((name) => spanningOf.has(`${matched.operation.id}\u0000${name}`));
|
|
345
|
+
const source = spanning ? url.pathname : url.pathname.replace(/\/{2,}/g, '/');
|
|
300
346
|
// a prefix's named groups (Jira's `/ex/jira/(?<cloudId>[^/]+)`) and the host's (E2B's envd at
|
|
301
347
|
// `49983-(?<sandboxID>…).e2b.app`) join the operation's parameters
|
|
302
|
-
const groups = { ...(prefix?.exec(
|
|
303
|
-
|
|
304
|
-
const label = hostPath?.exec(hostOf(request))?.[1];
|
|
305
|
-
if (label) path = `/${label}${path === '/' ? '' : path}`;
|
|
348
|
+
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]]; })) };
|
|
349
|
+
const path = spanning ? kept : rewritten(source, host);
|
|
306
350
|
if (path === url.pathname && !Object.keys(groups).length) return request;
|
|
307
351
|
// the path the client sent, which a signature over the request (SigV4's canonical URI) covers
|
|
308
352
|
const headers = new Headers(request.headers);
|
|
@@ -314,6 +358,8 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
314
358
|
};
|
|
315
359
|
const rootServes = (method: string, path: string): boolean => Boolean(matchOperation(routes, method, path));
|
|
316
360
|
const lane = m.lanes ? laneRouter(m.vendor, m.lanes, parts.lanes ?? {}, rootServes) : undefined;
|
|
361
|
+
// Inspect lane-owned World doors directly, without routing them back through the vendor gateway.
|
|
362
|
+
const laneDoor = (request: Request, path: string): Promise<Record<string, unknown>> => laneDoors(parts.lanes ?? {}, request, path);
|
|
317
363
|
// at a World's place, a page's asset or upload on another of the pack's content hosts is `<base>/@<host>/<path>`
|
|
318
364
|
// (architecture, "A page at a World's place"): answered by the built content screen that names that host exactly,
|
|
319
365
|
// routed from it, and by nothing else; a request naming no such host is general routing's
|
|
@@ -358,7 +404,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
358
404
|
};
|
|
359
405
|
/** A request as the kernel reads it: the headers only the kernel names taken off, its body decoded (undefined when it
|
|
360
406
|
* does not decode as its Content-Encoding says). */
|
|
361
|
-
const prepared = async (arrived: Request): Promise<Request | undefined> => {
|
|
407
|
+
const prepared = async (arrived: Request): Promise<Request | Response | undefined> => {
|
|
362
408
|
if (arrived.headers.has(SCENARIO_DECISION_HEADER) || arrived.headers.has(PREFIX_PARAMS_HEADER)) {
|
|
363
409
|
const headers = new Headers(arrived.headers);
|
|
364
410
|
headers.delete(SCENARIO_DECISION_HEADER);
|
|
@@ -374,7 +420,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
374
420
|
const address = arrived.headers.has(TWIN_PREFIX_HEADER) ? contentAddressOf(new URL(arrived.url).pathname) : undefined;
|
|
375
421
|
if (!address || !contentHosts.includes(address.host)) return undefined;
|
|
376
422
|
const readable = await prepared(arrived);
|
|
377
|
-
return readable ? atContentHost(readable) : undecodable(arrived);
|
|
423
|
+
return readable instanceof Response ? readable : readable ? atContentHost(readable) : undecodable(arrived);
|
|
378
424
|
};
|
|
379
425
|
const fetch = async (arrived: Request): Promise<Response> => {
|
|
380
426
|
// the discovery door is the pack's own, whichever lane would take its path
|
|
@@ -393,9 +439,12 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
393
439
|
// one a lane's names, the lane router sends it to that lane's own fetch, wrapped as any request to it is
|
|
394
440
|
const own = await answerContent(arrived);
|
|
395
441
|
if (own) return own;
|
|
396
|
-
|
|
442
|
+
// Root World doors belong to the pack at every routed host, including a lane's host.
|
|
443
|
+
const rootDoor = doors.some((d) => d.decl.method.toUpperCase() === arrived.method && d.path(asked));
|
|
444
|
+
const toLane = discovery || rootDoor ? undefined : lane?.(arrived);
|
|
397
445
|
if (toLane) return toLane(arrived);
|
|
398
446
|
const readable = await prepared(arrived);
|
|
447
|
+
if (readable instanceof Response) return readable;
|
|
399
448
|
if (!readable) return undecodable(arrived);
|
|
400
449
|
const incoming = routed(readable);
|
|
401
450
|
// a twin started read-only marks every request so, so every context it opens knows (isReadOnlyRequest)
|
|
@@ -405,31 +454,41 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
405
454
|
const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
|
|
406
455
|
if (request.method === 'GET' && path === '/twin' && m.discovery) {
|
|
407
456
|
const d = m.discovery;
|
|
457
|
+
const discoveryDoors = [...(m.doors ?? []), ...(m.machines ? [{ id: 'machine-pool', method: 'POST', path: '/_twin/machine-pool', note: 'Enroll the World execution pool: { kind: light | docker | none | fake, images?: { image: { context, command?, runtime? } } }.' }] : [])];
|
|
408
458
|
// the workspaces a person can be shown (Viewing a World): each built workspace screen's id and path
|
|
409
|
-
const
|
|
459
|
+
const laneDiscovery = await laneDoor(request, '/twin');
|
|
460
|
+
const shown = { ...(workspaces().length ? { screens: workspaces() } : {}), ...(Object.keys(laneDiscovery).length ? { lanes: laneDiscovery } : {}) };
|
|
461
|
+
const identity = d.identity ?? (m.auth?.held ? 'Authenticate as the vendor does, using a credential issued in this World; unknown credentials are refused.' : undefined);
|
|
410
462
|
// a model vendor's: its state, and what its scenario scripts, with the scenario's live counts
|
|
411
463
|
if (parts.scenario && d.behavior) {
|
|
412
|
-
|
|
464
|
+
const body = twinManifest({
|
|
413
465
|
vendor: m.vendor, twinOf: d.twinOf,
|
|
414
|
-
stateSentence: `Stateful: it stores ${d.stores}. Create state through the vendor's OWN API with the real SDK or plain fetch pointed here.`,
|
|
466
|
+
stateSentence: `Stateful: it stores ${d.stores}. Create state through the vendor's OWN API with the real SDK or plain fetch pointed here${discoveryDoors.length ? "; what the vendor does on its own pages (not its API) is a door of the World, listed under doors" : ''}.`,
|
|
415
467
|
behaviorSentence: d.behavior, exampleHandler: d.exampleHandler ?? null, ...(engineOf() ? { engine: engineOf() as never } : {}),
|
|
416
|
-
})
|
|
468
|
+
});
|
|
469
|
+
const declared = Object.fromEntries(discoveryDoors.map((door) => [`${door.method} ${doorPath(door.path)}`, door.note ?? 'a door of the World']));
|
|
470
|
+
return Response.json({ ...shown, ...body, program: { ...(body.program as Record<string, unknown>), ...(identity ? { identity } : {}) }, doors: { ...(body.doors as Record<string, string>), ...declared } });
|
|
417
471
|
}
|
|
418
|
-
return Response.json({ ...statefulTwinManifest({ vendor: m.vendor, twinOf: d.twinOf, stores: d.stores, ...(
|
|
472
|
+
return Response.json({ ...statefulTwinManifest({ vendor: m.vendor, twinOf: d.twinOf, stores: d.stores, ...(identity ? { identity } : {}), ...(d.behavior ? { behavior: d.behavior } : {}), ...(d.notes ? { notes: d.notes } : {}), ...(discoveryDoors.length ? { doors: discoveryDoors.map((door) => ({ ...door, path: doorPath(door.path) })) } : {}) }), ...shown });
|
|
419
473
|
}
|
|
420
474
|
// the scenario's handlers with their match counts, and the recent misses
|
|
421
|
-
if (request.method === 'GET' && path === '/twin/scenario'
|
|
475
|
+
if (request.method === 'GET' && path === '/twin/scenario') {
|
|
476
|
+
const lanes = await laneDoor(request, '/twin/scenario');
|
|
477
|
+
if (parts.scenario || Object.keys(lanes).length) return Response.json({ ...(engineOf()?.status() ?? { vendor: m.vendor, handlers: [], misses: 0, recentMisses: [] }), ...(Object.keys(lanes).length ? { lanes } : {}) });
|
|
478
|
+
}
|
|
422
479
|
// the vendor's own moves, not the caller's: its context carries none of the request's headers (a Stripe-Account
|
|
423
480
|
// header would make a payout time makes the caller's), but a tenant's, when the vendor keeps tenants apart
|
|
424
|
-
|
|
481
|
+
await advanceClock(request);
|
|
425
482
|
if (path.startsWith('/_twin/')) {
|
|
426
483
|
// the World's machine pool, enrolled by the kernel for a vendor that runs images (machines.ts)
|
|
427
484
|
if (m.machines && path === '/_twin/machine-pool' && request.method === 'POST') {
|
|
428
485
|
if (readOnly) return refusedReadOnly();
|
|
429
|
-
let
|
|
430
|
-
try {
|
|
486
|
+
let enrollment: Record<string, unknown> = {};
|
|
487
|
+
try { enrollment = parseExactJson(await request.clone().text()); } catch {}
|
|
488
|
+
const kind = enrollment.kind;
|
|
431
489
|
if (!POOL_KINDS.includes(kind as never)) return Response.json({ message: `kind must be one of ${POOL_KINDS.join(', ')}` }, { status: 400 });
|
|
432
|
-
await applyTwinWrite(m.service, { operation: 'machine_pool.enroll', subjectType: MACHINE_POOL, subjectId: 'pool', fields: { kind }, occurredAt: (await open(request, 'machine-pool', 'POST', '/_twin/machine-pool', {})).occurredAt, actor: { kind: 'system' } }, options.root);
|
|
490
|
+
await applyTwinWrite(m.service, { operation: 'machine_pool.enroll', subjectType: MACHINE_POOL, subjectId: 'pool', fields: { kind, ...(kind === 'fake' && Array.isArray(enrollment.httpReplies) ? { httpReplies: enrollment.httpReplies } : {}), ...(kind === 'light' ? { images: enrollment.images ?? {} } : {}) }, occurredAt: (await open(request, 'machine-pool', 'POST', '/_twin/machine-pool', {})).occurredAt, actor: { kind: 'system' } }, options.root);
|
|
491
|
+
if (kind === 'fake') resetFakePool(m.service, options.root);
|
|
433
492
|
return Response.json({ kind }, { status: 201 });
|
|
434
493
|
}
|
|
435
494
|
for (const d of doors) {
|
|
@@ -471,11 +530,27 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
471
530
|
if (readOnly && request.method !== 'GET' && request.method !== 'HEAD') return refusedReadOnly();
|
|
472
531
|
return byPath.s.answer(await open(request, byPath.s.decl.id, request.method, byPath.s.decl.path, { ...(byPath.onHost?.params ?? {}), ...byPath.hit!.params }));
|
|
473
532
|
}
|
|
533
|
+
for (const route of m.machineRoutes ?? []) {
|
|
534
|
+
const host = request.headers.get('x-volter-twin-original-host') ?? new URL(request.url).hostname;
|
|
535
|
+
const match = new RegExp(route.host).exec(host);
|
|
536
|
+
if (!match) continue;
|
|
537
|
+
const ctx = await open(request, 'machine.traffic', request.method, url.pathname, match.groups ?? {});
|
|
538
|
+
const field = (row: Record<string, unknown>, path: string): unknown => path.split('.').reduce<unknown>((value, key) => value && typeof value === 'object' ? (value as Record<string, unknown>)[key] : undefined, row);
|
|
539
|
+
const fill = (value: string, row: Record<string, unknown> = {}): string => value.replace(/\{([^}]+)\}/g, (_, key: string) => String(match.groups?.[key] ?? field(row, key) ?? ''));
|
|
540
|
+
const row = ctx.rowsRaw(route.resource).find(row => Object.entries(route.where).every(([key, value]) => String(field(row, key)) === fill(value)));
|
|
541
|
+
if (!row) return new Response('Guest is unavailable', { status: 503 });
|
|
542
|
+
try { return await ctx.machines.request(String(field(row, route.name)), Number(typeof route.port === 'string' ? fill(route.port, row) : route.port), request); }
|
|
543
|
+
catch { return new Response('Guest is unavailable', { status: 503 }); }
|
|
544
|
+
}
|
|
474
545
|
if (!parts.around) return encoded(m, request, await decided(request));
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
546
|
+
// The front reads a context before forwarding (possibly with new headers). Materialize a transport body before
|
|
547
|
+
// those independent reads, rather than chaining tees of the incoming stream through the front and the scenario.
|
|
548
|
+
const bytes = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.arrayBuffer();
|
|
549
|
+
const buffered = new Request(request.url, { method: request.method, headers: request.headers, signal: request.signal, ...(bytes ? { body: bytes } : {}) });
|
|
550
|
+
const matched = matchOperation(routes, buffered.method, url.pathname, url.searchParams, buffered.headers);
|
|
551
|
+
const operation = matched?.operation ?? ({ id: 'unmatched', method: buffered.method, path: url.pathname, class: buffered.method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation);
|
|
552
|
+
const ctx = await contextFor(m, { request: buffered.clone(), operation, params: matched?.params ?? pathParams(routes, url.pathname) }, scope);
|
|
553
|
+
return encoded(m, buffered, await parts.around(ctx, (rewritten) => decided(rewritten ?? buffered)));
|
|
479
554
|
};
|
|
480
555
|
// the request's id is the correlation of every entry it appends (runtime contract D3), minted here, always: it is the
|
|
481
556
|
// group a perform sends as one call (the real-system adapters, "Perform"). Under `auto` the group is performed once
|
|
@@ -513,13 +588,21 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
513
588
|
// the vendor's sockets: a request to a declared socket's path opens a session of its engine, over a context of the
|
|
514
589
|
// request that opened it (operation `socket:<id>`)
|
|
515
590
|
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}`);
|
|
516
|
-
const
|
|
591
|
+
const ownUpgrade = m.sockets?.length ? socketUpgrade(m.service, options.root, m.sockets, parts.sockets!,
|
|
517
592
|
(request, d) => open(request, `socket:${d.id}`, 'GET', d.path, {}), hostOf, (request) => new URL(routed(request).url).pathname,
|
|
518
593
|
// a turn a session speaks (a voice agent's reply) is the World's scenario's, as a turn over HTTP is
|
|
519
594
|
async (asked) => engineOf()?.serve(asked)) : undefined;
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
595
|
+
const upgrade = laneUpgrades(parts.lanes ?? {}, ownUpgrade);
|
|
596
|
+
// what serves each operation: the pack's own, and each lane's, each unit's under its own name
|
|
597
|
+
const ownersByUnit = (): Record<string, Record<string, DerivedOwner>> => ({
|
|
598
|
+
'': {
|
|
599
|
+
...((surface.format === 'command-reply' || surface.format === 'command-table' && m.descriptor?.transport === 'raw-tcp') ? Object.fromEntries((surface.commands ?? []).map((c) => [c.id, parts.around && !m.unmodeled?.includes(c.id) ? 'handler' : 'gap'])) : api.owners()),
|
|
600
|
+
...Object.fromEntries((parts.graphql?.operations ?? []).map((op) => [op.id, `${op.kind === 'query' ? 'Query' : 'Mutation'}.${op.field}` in (parts.graphql?.resolvers ?? {}) ? 'handler' : 'gap'] as const)),
|
|
601
|
+
},
|
|
602
|
+
...lanesOwners(parts.lanes ?? {}),
|
|
603
|
+
});
|
|
604
|
+
const owners = (): Record<string, DerivedOwner> => flatOwners(ownersByUnit());
|
|
605
|
+
return derivedRequestScopes(m, Object.assign(served, { owners, ownersByUnit, serves, advanceClock, workspaces, boards: Boolean(parts.board), maxRequestBodySize: m.descriptor?.maxRequestBodySize, draws: drawsUrl, contentHosts, ...(upgrade ? { upgrade } : {}) }), options.readOnly);
|
|
523
606
|
}
|
|
524
607
|
|
|
525
608
|
/** The vendor's gateway over its lanes: the lane a request goes to (a door to the lane that declares it, then the first
|
|
@@ -574,17 +657,52 @@ function laneRouter(vendor: string, decl: LanesDecl, lanes: Record<string, LaneP
|
|
|
574
657
|
|
|
575
658
|
/** A vendor whose every API is a lane (Cloudflare's API v4 and R2, AWS's services), served from its manifest: the
|
|
576
659
|
* discovery door, then its lanes as its gateway sends them; a request no lane takes is the vendor's not-found. */
|
|
577
|
-
/**
|
|
578
|
-
|
|
579
|
-
|
|
660
|
+
/** Each lane's answer at one of its World doors (`/twin`, `/twin/scenario`), by lane, read from the lane directly rather
|
|
661
|
+
* than routed back through the vendor's front: a vendor's discovery holds its lanes' (their doors, their scripts). */
|
|
662
|
+
async function laneDoors(lanes: Record<string, LanePart>, request: Request, path: string): Promise<Record<string, unknown>> {
|
|
663
|
+
const entries = await Promise.all(Object.entries(lanes).map(async ([name, part]) => {
|
|
664
|
+
const url = new URL(request.url); url.pathname = path; url.search = '';
|
|
665
|
+
const answer = await part.fetch(new Request(url, { headers: request.headers, signal: request.signal }));
|
|
666
|
+
return answer.ok ? [name, await answer.json()] as const : undefined;
|
|
667
|
+
}));
|
|
668
|
+
return Object.fromEntries(entries.filter((entry): entry is readonly [string, unknown] => entry !== undefined));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** What serves each of a vendor's lanes' operations (a handler, the core, or the gap), by lane: a lane's operation is the
|
|
672
|
+
* lane's own, so an id it shares with its root or another lane (Clerk's Backend and Frontend APIs both name
|
|
673
|
+
* `ListOrganizationMemberships`) keeps the lane's own owner, and a lane's gap stays a gap whoever else serves the id. */
|
|
674
|
+
const lanesOwners = (lanes: Record<string, LanePart>): Record<string, Record<string, DerivedOwner>> =>
|
|
675
|
+
Object.fromEntries(Object.entries(lanes).map(([name, l]) => [name, (l.fetch as { owners?: () => Record<string, DerivedOwner> }).owners?.() ?? {}]));
|
|
676
|
+
|
|
677
|
+
/** A vendor's owners as one map, for its breadth: its own API's operations by id, a lane's as `<lane>:<id>`. */
|
|
678
|
+
const flatOwners = (byUnit: Record<string, Record<string, DerivedOwner>>): Record<string, DerivedOwner> =>
|
|
679
|
+
Object.fromEntries(Object.entries(byUnit).flatMap(([unit, map]) => Object.entries(map).map(([id, owner]) => [unit ? `${unit}:${id}` : id, owner] as const)));
|
|
580
680
|
|
|
581
681
|
/** The workspaces a vendor's lanes draw, as one list. */
|
|
582
682
|
const lanesWorkspaces = (lanes: Record<string, LanePart>): Workspace[] =>
|
|
583
683
|
Object.values(lanes).flatMap((l) => (l.fetch as { workspaces?: () => Workspace[] }).workspaces?.() ?? []);
|
|
584
684
|
|
|
585
|
-
|
|
685
|
+
/** Keep each upgraded peer on the vendor or lane engine that accepted its opening request. */
|
|
686
|
+
function laneUpgrades(lanes: Record<string, LanePart>, own?: WebSocketUpgrade): WebSocketUpgrade | undefined {
|
|
687
|
+
const choices = [own, ...Object.values(lanes).map(lane => lane.fetch.upgrade)].filter((choice): choice is WebSocketUpgrade => choice !== undefined);
|
|
688
|
+
if (choices.length === 0) return undefined;
|
|
689
|
+
if (choices.length === 1) return choices[0];
|
|
690
|
+
const held = new WeakMap<WebSocketPeer, WebSocketUpgrade>();
|
|
691
|
+
const select = (request: Request): WebSocketUpgrade | undefined => choices.find(choice => choice.accepts(request));
|
|
692
|
+
return {
|
|
693
|
+
accepts: request => select(request) !== undefined,
|
|
694
|
+
protocol: request => select(request)?.protocol?.(request),
|
|
695
|
+
open(peer, request) { const selected = select(request); if (selected) { held.set(peer, selected); selected.open(peer, request); } },
|
|
696
|
+
message(peer, data) { return held.get(peer)?.message(peer, data); },
|
|
697
|
+
close(peer) { const selected = held.get(peer); held.delete(peer); selected?.close(peer); },
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function createVendorFetch(manifest: VendorManifest, lanes: Record<string, LanePart>): DerivedFetch {
|
|
702
|
+
const upgrade = laneUpgrades(lanes);
|
|
586
703
|
const lane = laneRouter(manifest.vendor, manifest.lanes, lanes);
|
|
587
704
|
return Object.assign(async (request: Request): Promise<Response> => {
|
|
705
|
+
for (const part of Object.values(lanes)) await (part.fetch as typeof part.fetch & { advanceClock?: (request: Request) => Promise<void> }).advanceClock?.(request);
|
|
588
706
|
const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
|
|
589
707
|
// the vendor's board is the frames of the lanes that lay one out, each drawn when any lane's screen takes its url
|
|
590
708
|
if (request.method === 'GET' && path === '/twin/board') {
|
|
@@ -602,9 +720,14 @@ export function createVendorFetch(manifest: VendorManifest, lanes: Record<string
|
|
|
602
720
|
if (request.method === 'GET' && path === '/twin' && manifest.discovery) {
|
|
603
721
|
// the vendor's screens are its lanes' (a dashboard drawn by one lane is the vendor's)
|
|
604
722
|
const screens = lanesWorkspaces(lanes);
|
|
605
|
-
|
|
723
|
+
const laneDiscovery = await laneDoors(lanes, request, '/twin');
|
|
724
|
+
return Response.json({ ...statefulTwinManifest({ vendor: manifest.vendor, ...manifest.discovery }), ...(screens.length ? { screens } : {}), ...(Object.keys(laneDiscovery).length ? { lanes: laneDiscovery } : {}) });
|
|
725
|
+
}
|
|
726
|
+
if (request.method === 'GET' && path === '/twin/scenario') {
|
|
727
|
+
const laneScenarios = await laneDoors(lanes, request, '/twin/scenario');
|
|
728
|
+
if (Object.keys(laneScenarios).length) return Response.json({ vendor: manifest.vendor, lanes: laneScenarios });
|
|
606
729
|
}
|
|
607
730
|
const to = lane(request);
|
|
608
731
|
return to ? to(request) : Response.json({ error: `${manifest.vendor}: no lane serves ${request.method} ${path}` }, { status: 404 });
|
|
609
|
-
}, { owners: () => lanesOwners(lanes), workspaces: () => lanesWorkspaces(lanes) });
|
|
732
|
+
}, { maxRequestBodySize: manifest.descriptor?.maxRequestBodySize, owners: () => flatOwners(lanesOwners(lanes)), ownersByUnit: () => lanesOwners(lanes), workspaces: () => lanesWorkspaces(lanes), ...(upgrade ? { upgrade } : {}) });
|
|
610
733
|
}
|