@volter/world-core 3.0.38 → 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.
Files changed (179) hide show
  1. package/app-route.cjs +26 -25
  2. package/app-route.d.cts +1 -0
  3. package/dist/app-route.cjs +26 -25
  4. package/dist/app-route.d.cts +1 -0
  5. package/dist/generated/pack-facts.json +409 -352
  6. package/dist/inject.cjs +474 -95
  7. package/dist/network-policy.cjs +108 -13
  8. package/dist/network-policy.d.cts +8 -3
  9. package/dist/src/actions.js +2 -1
  10. package/dist/src/anthropic-wire.d.ts +2 -0
  11. package/dist/src/anthropic-wire.js +5 -0
  12. package/dist/src/attestation.d.ts +6 -0
  13. package/dist/src/attestation.js +27 -0
  14. package/dist/src/bytes.d.ts +3 -0
  15. package/dist/src/bytes.js +17 -0
  16. package/dist/src/changeset.js +2 -1
  17. package/dist/src/clickhouse/sql.js +73 -14
  18. package/dist/src/derived-core.d.ts +210 -32
  19. package/dist/src/derived-core.js +510 -244
  20. package/dist/src/derived-real.d.ts +1 -1
  21. package/dist/src/derived-real.js +704 -93
  22. package/dist/src/derived.d.ts +18 -2
  23. package/dist/src/derived.js +21 -2
  24. package/dist/src/events.d.ts +28 -45
  25. package/dist/src/events.js +40 -58
  26. package/dist/src/exact-json.d.ts +10 -0
  27. package/dist/src/exact-json.js +98 -0
  28. package/dist/src/executor.d.ts +17 -0
  29. package/dist/src/executor.js +91 -30
  30. package/dist/src/file-response.js +1 -1
  31. package/dist/src/form.d.ts +8 -0
  32. package/dist/src/form.js +76 -0
  33. package/dist/src/git/format.d.ts +16 -0
  34. package/dist/src/git/format.js +110 -0
  35. package/dist/src/git/index.d.ts +1 -0
  36. package/dist/src/git/index.js +1 -0
  37. package/dist/src/git/lfs.d.ts +1 -1
  38. package/dist/src/git/lfs.js +4 -3
  39. package/dist/src/graphql-wire.d.ts +6 -0
  40. package/dist/src/graphql-wire.js +6 -3
  41. package/dist/src/grpc-wire.js +2 -1
  42. package/dist/src/head.d.ts +1 -0
  43. package/dist/src/head.js +15 -4
  44. package/dist/src/held-reads.d.ts +6 -0
  45. package/dist/src/held-reads.js +60 -0
  46. package/dist/src/history.js +5 -4
  47. package/dist/src/host-port.d.ts +5 -0
  48. package/dist/src/host-port.js +14 -0
  49. package/dist/src/index.d.ts +11 -3
  50. package/dist/src/index.js +9 -4
  51. package/dist/src/log.js +12 -7
  52. package/dist/src/machines.d.ts +81 -5
  53. package/dist/src/machines.js +125 -9
  54. package/dist/src/managed-database.d.ts +15 -11
  55. package/dist/src/managed-database.js +38 -37
  56. package/dist/src/multipart.d.ts +21 -3
  57. package/dist/src/multipart.js +112 -38
  58. package/dist/src/observe.d.ts +3 -0
  59. package/dist/src/observe.js +6 -2
  60. package/dist/src/openai-wire.d.ts +16 -0
  61. package/dist/src/openai-wire.js +43 -12
  62. package/dist/src/pack-fetch.d.ts +4 -6
  63. package/dist/src/pack-fetch.js +189 -57
  64. package/dist/src/packRegistry.d.ts +7 -1
  65. package/dist/src/packRegistry.js +20 -3
  66. package/dist/src/private-transport.d.ts +1 -0
  67. package/dist/src/private-transport.js +11 -0
  68. package/dist/src/protobuf.js +10 -4
  69. package/dist/src/rateBudget.d.ts +8 -1
  70. package/dist/src/rateBudget.js +1 -0
  71. package/dist/src/redis/engine.d.ts +14 -5
  72. package/dist/src/redis/engine.js +271 -72
  73. package/dist/src/redis/frames.d.ts +17 -0
  74. package/dist/src/redis/frames.js +123 -0
  75. package/dist/src/redis/index.d.ts +6 -0
  76. package/dist/src/redis/index.js +6 -0
  77. package/dist/src/redis/json.d.ts +2 -0
  78. package/dist/src/redis/json.js +14 -0
  79. package/dist/src/redis/lua-libs.d.ts +4 -0
  80. package/dist/src/redis/lua-libs.js +173 -0
  81. package/dist/src/redis/lua.d.ts +1 -0
  82. package/dist/src/redis/lua.js +36 -0
  83. package/dist/src/redis/member-storage.d.ts +5 -0
  84. package/dist/src/redis/member-storage.js +75 -0
  85. package/dist/src/redis/resp2.d.ts +4 -0
  86. package/dist/src/redis/resp2.js +29 -0
  87. package/dist/src/redis/stream.d.ts +14 -0
  88. package/dist/src/redis/stream.js +335 -0
  89. package/dist/src/remote-execute.d.ts +2 -2
  90. package/dist/src/request-body.d.ts +11 -0
  91. package/dist/src/request-body.js +11 -0
  92. package/dist/src/runtime.d.ts +6 -5
  93. package/dist/src/runtime.js +5 -5
  94. package/dist/src/s3/wire.d.ts +4 -4
  95. package/dist/src/s3/wire.js +5 -3
  96. package/dist/src/scenario.d.ts +22 -0
  97. package/dist/src/scenario.js +35 -31
  98. package/dist/src/serve-http.d.ts +2 -1
  99. package/dist/src/serve-http.js +10 -1
  100. package/dist/src/serve.js +7 -4
  101. package/dist/src/signing.d.ts +23 -5
  102. package/dist/src/signing.js +74 -11
  103. package/dist/src/sigv4.d.ts +3 -1
  104. package/dist/src/sigv4.js +4 -2
  105. package/dist/src/smtp.js +2 -1
  106. package/dist/src/sockets.js +8 -3
  107. package/dist/src/state-system.d.ts +3 -0
  108. package/dist/src/storage.js +5 -2
  109. package/dist/src/twin-fetch.d.ts +3 -1
  110. package/dist/src/twin-fetch.js +3 -2
  111. package/dist/src/vendor-call.d.ts +10 -3
  112. package/dist/src/vendor-call.js +58 -14
  113. package/dist/test-fixtures/attestation.SOURCE.md +1 -0
  114. package/dist/test-fixtures/attestation.json +50 -0
  115. package/generated/pack-facts.json +409 -352
  116. package/inject.cjs +474 -95
  117. package/network-policy.cjs +108 -13
  118. package/network-policy.d.cts +8 -3
  119. package/package.json +11 -4
  120. package/src/actions.ts +2 -1
  121. package/src/anthropic-wire.ts +6 -0
  122. package/src/attestation.ts +23 -0
  123. package/src/bytes.ts +21 -0
  124. package/src/changeset.ts +2 -1
  125. package/src/clickhouse/sql.ts +50 -16
  126. package/src/derived-core.ts +573 -249
  127. package/src/derived-real.ts +552 -42
  128. package/src/derived.ts +23 -4
  129. package/src/events.ts +54 -85
  130. package/src/exact-json.ts +71 -0
  131. package/src/executor.ts +86 -27
  132. package/src/file-response.ts +1 -1
  133. package/src/form.ts +66 -0
  134. package/src/git/format.ts +89 -0
  135. package/src/git/index.ts +1 -0
  136. package/src/git/lfs.ts +5 -4
  137. package/src/graphql-wire.ts +8 -3
  138. package/src/grpc-wire.ts +2 -1
  139. package/src/head.ts +15 -5
  140. package/src/held-reads.ts +54 -0
  141. package/src/history.ts +5 -4
  142. package/src/host-port.ts +9 -0
  143. package/src/index.ts +10 -4
  144. package/src/log.ts +11 -7
  145. package/src/machines.ts +135 -13
  146. package/src/managed-database.ts +48 -40
  147. package/src/multipart.ts +110 -35
  148. package/src/observe.ts +6 -3
  149. package/src/openai-wire.ts +45 -14
  150. package/src/pack-fetch.ts +173 -50
  151. package/src/packRegistry.ts +27 -4
  152. package/src/private-transport.ts +8 -0
  153. package/src/protobuf.ts +8 -4
  154. package/src/rateBudget.ts +6 -1
  155. package/src/redis/engine.ts +168 -63
  156. package/src/redis/frames.ts +53 -0
  157. package/src/redis/index.ts +6 -0
  158. package/src/redis/json.ts +13 -0
  159. package/src/redis/lua-libs.ts +62 -0
  160. package/src/redis/lua.ts +20 -0
  161. package/src/redis/member-storage.ts +58 -0
  162. package/src/redis/resp2.ts +25 -0
  163. package/src/redis/stream.ts +102 -0
  164. package/src/remote-execute.ts +2 -2
  165. package/src/request-body.ts +12 -0
  166. package/src/runtime.ts +6 -5
  167. package/src/s3/wire.ts +8 -5
  168. package/src/scenario.ts +34 -13
  169. package/src/serve-http.ts +11 -2
  170. package/src/serve.ts +4 -3
  171. package/src/signing.ts +71 -17
  172. package/src/sigv4.ts +5 -3
  173. package/src/smtp.ts +3 -2
  174. package/src/sockets.ts +8 -3
  175. package/src/state-system.ts +3 -0
  176. package/src/storage.ts +5 -2
  177. package/src/twin-fetch.ts +4 -3
  178. package/src/vendor-call.ts +59 -12
  179. package/test-fixtures/attestation.json +50 -0
@@ -1,23 +1,4 @@
1
- // THE scenario engine — System 2 of the twin programming model (one grammar, per-pack
2
- // vocabulary). See company-repo BRIEFS/TWIN-PROGRAMMING-MODEL.md (LOCKED, 2026-08-27).
3
- //
4
- // A HANDLER is an MSW-shaped data rule: { on, respond, once?, scope?, phase?, advancePhase? }.
5
- // Handlers are evaluated IN ORDER; the FIRST handler whose `on` conditions ALL hold fires.
6
- // No match → the caller serves its labeled deterministic stub and records the MISS (with the
7
- // request's extracted features — the authoring signal). The handler FILE in the world dir is
8
- // the only write surface; `engine.use(...)` exists for in-process tests only (LIFO over the
9
- // baseline, removable). There are NO runtime write doors — a running world is never mutated.
10
- //
11
- // The GRAMMAR (structure, ordering, once/scope/phase, strict validation, extractors,
12
- // placeholders, miss records) is this module's and identical for every vendor. The
13
- // VOCABULARY (which `on` keys exist and how each matches; what `respond` may contain; which
14
- // routes are stateful and therefore refuse success-shaped handlers) is the pack's, declared
15
- // through a PackScenarioAdapter. Determinism: the engine is a pure state machine — same
16
- // handler list + same request sequence → same decisions, byte for byte.
17
- //
18
- // STRICT EVERYWHERE (the gemini discipline): unknown top-level keys, unknown `on` keys,
19
- // unknown placeholder names, malformed extractors — all THROW with the valid vocabulary in
20
- // the message. A typo must fail loudly at load, never silently mis-match at serve.
1
+ import { exactInteger, compareExactNumbers, isExactIntegerValue } from "./exact-json.js";
21
2
  export class ScenarioError extends Error {
22
3
  }
23
4
  // `$comment` is allowed (and ignored) at document and handler level — JSON has no comments
@@ -112,22 +93,38 @@ function parseHandler(raw, index, adapter, extractors) {
112
93
  if (h.fault !== undefined) {
113
94
  const f = h.fault;
114
95
  if (typeof f !== "object" || f === null || Array.isArray(f))
115
- throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault is an object { kind: "slow" | "status" | "drop", ... }`);
116
- const kinds = ["slow", "status", "drop"];
96
+ throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault is an object { kind: "slow" | "status" | "drop" | "generation", ... }`);
97
+ const kinds = ["slow", "status", "drop", "generation"];
117
98
  if (!kinds.includes(f.kind))
118
99
  throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.kind is one of ${kinds.join(", ")}, got ${JSON.stringify(f.kind)}`);
119
- const keysFor = { slow: ["kind", "ms"], status: ["kind", "status", "retryAfterSeconds", "message"], drop: ["kind", "holdMs"] };
100
+ const keysFor = { slow: ["kind", "ms"], status: ["kind", "status", "retryAfterSeconds", "message", "metadata", "errorType"], generation: ["kind", "status", "message", "errorType", "metadata", "partialText", "event"], drop: ["kind", "holdMs"] };
120
101
  for (const key of Object.keys(f))
121
102
  if (!keysFor[f.kind].includes(key))
122
103
  throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.${key} is not a key of a ${String(f.kind)} fault (valid: ${keysFor[f.kind].join(", ")})`);
123
104
  if (f.kind === "slow" && !(Number.isInteger(f.ms) && f.ms >= 0 && f.ms <= 600_000))
124
105
  throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.ms is an integer 0..600000 (milliseconds)`);
125
- if (f.kind === "status" && !(Number.isInteger(f.status) && f.status >= 400 && f.status <= 599))
106
+ if ((f.kind === "status" || f.kind === "generation") && !(Number.isInteger(f.status) && f.status >= 400 && f.status <= 599))
126
107
  throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.status is an integer 400..599 — a fault is a refusal or a failure, never a success`);
127
108
  if (f.kind === "status" && f.retryAfterSeconds !== undefined && !(Number.isInteger(f.retryAfterSeconds) && f.retryAfterSeconds >= 0))
128
109
  throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.retryAfterSeconds is a non-negative integer`);
129
110
  if (f.kind === "status" && f.message !== undefined && typeof f.message !== "string")
130
111
  throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.message is a string`);
112
+ if ((f.kind === "status" || f.kind === "generation") && f.metadata !== undefined && (typeof f.metadata !== "object" || f.metadata === null || Array.isArray(f.metadata)))
113
+ throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.metadata is an object`);
114
+ if (f.kind === "status" && f.errorType !== undefined && typeof f.errorType !== "string")
115
+ throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.errorType is a string`);
116
+ if (f.kind === "generation") {
117
+ if (typeof f.message !== "string" || typeof f.errorType !== "string" || !f.errorType)
118
+ throw new ScenarioError(`${adapter.vendor} scenario: ${at}: generation fault requires message and nonempty errorType`);
119
+ for (const key of ["partialText", "event"])
120
+ if (f[key] !== undefined && typeof f[key] !== "string")
121
+ throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.${key} is a string`);
122
+ if (!adapter.validateGenerationFault)
123
+ throw new ScenarioError(`${adapter.vendor} scenario: ${at}: generation faults require a compute-only adapter`);
124
+ const refusal = adapter.validateGenerationFault(f, h);
125
+ if (refusal)
126
+ throw new ScenarioError(`${adapter.vendor} scenario: ${at}: ${refusal}`);
127
+ }
131
128
  if (f.kind === "drop" && f.holdMs !== undefined && !(Number.isInteger(f.holdMs) && f.holdMs >= 0))
132
129
  throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.holdMs is a non-negative integer`);
133
130
  if (f.kind !== "slow" && "respond" in h)
@@ -185,10 +182,11 @@ function applyTransform(value, transform, vendor, where) {
185
182
  if (!transform)
186
183
  return value;
187
184
  const [op, rawN] = transform.slice(1).split(":");
188
- if (typeof value !== "number")
185
+ if (typeof value !== "number" && !(typeof value === "string" && isExactIntegerValue(value)))
189
186
  throw new ScenarioError(`${vendor} scenario: ${where} applies |${op}:${rawN} to a non-numeric extractor value ${JSON.stringify(value)}`);
190
- const n = Number(rawN);
191
- return op === "min" ? Math.min(value, n) : Math.max(value, n);
187
+ const n = /^-?\d+$/.test(rawN) ? exactInteger(rawN) : Number(rawN);
188
+ const cmp = compareExactNumbers(value, n);
189
+ return op === "min" ? (cmp <= 0 ? value : n) : (cmp >= 0 ? value : n);
192
190
  }
193
191
  const MISS_KEEP = 20;
194
192
  /** The engine: pure state machine over registered handlers. One instance per twin server. */
@@ -322,7 +320,7 @@ export class ScenarioEngine {
322
320
  throw new ScenarioError(`${this.adapter.vendor} scenario: placeholder "{{${name}}}": pattern did not match the request (the handler demanded a value the request never stated)`);
323
321
  const captured = m[1] ?? m[0];
324
322
  if (tp.as === "number") {
325
- const n = Number(captured);
323
+ const n = /^-?\d+$/.test(captured) ? exactInteger(captured) : Number(captured);
326
324
  if (Number.isNaN(n))
327
325
  throw new ScenarioError(`${this.adapter.vendor} scenario: placeholder "{{${name}}}": captured "${captured}" is not a number`);
328
326
  value = n;
@@ -375,6 +373,8 @@ function waitForScenarioFault(ms, signal) {
375
373
  }
376
374
  export async function scenarioFaultResult(fault, render, signal) {
377
375
  throwIfScenarioAborted(signal);
376
+ if (fault.kind === "generation")
377
+ return null; // compute handler renders the admitted generation failure
378
378
  if (fault.kind === "slow") {
379
379
  await waitForScenarioFault(fault.ms, signal);
380
380
  throwIfScenarioAborted(signal);
@@ -393,18 +393,22 @@ export async function scenarioFaultResult(fault, render, signal) {
393
393
  return { status: fault.status, headers, body: rendered?.body ?? { error: { message: fault.message ?? `twin fault: ${fault.status}`, type: "twin_fault" } } };
394
394
  }
395
395
  export function statefulTwinManifest(input) {
396
+ // the World's doors the pack declares (what the vendor's own pages do that its API does not: issuing an account's key)
397
+ const doors = input.doors ?? [];
396
398
  return {
397
399
  twin: true,
398
400
  vendor: input.vendor,
399
401
  twinOf: input.twinOf,
400
402
  program: {
401
- state: `Stateful: it stores ${input.stores}. Create state through the vendor's OWN API with the real SDK or plain fetch pointed here — there is no fixture language and no write door besides the vendor's.`,
403
+ state: doors.length
404
+ ? `Stateful: it stores ${input.stores}. Create state through the vendor's OWN API with the real SDK or plain fetch pointed here; what the vendor does on its own pages (not its API) is a door of the World, listed under doors. There is no fixture language.`
405
+ : `Stateful: it stores ${input.stores}. Create state through the vendor's OWN API with the real SDK or plain fetch pointed here — there is no fixture language and no write door besides the vendor's.`,
402
406
  identity: input.identity ?? 'Authenticate as the vendor does; the twin accepts any non-sentinel credential.',
403
407
  time: 'Writes are stamped from the WORLD CLOCK (volter-world clock <world> set/advance) — deterministic history is clock-set, seed, clock-advance.',
404
- behavior: 'This twin is state, not scripting — answers are functions of what you seeded. Judgment/fault scripting lives on the scripted vendor twins (their GET /twin explains).',
408
+ behavior: input.behavior ?? 'This twin is state, not scripting — answers are functions of what you seeded. Judgment/fault scripting lives on the scripted vendor twins (their GET /twin explains).',
405
409
  ...(input.notes ? { notes: input.notes } : {}),
406
410
  },
407
- doors: { manifest: 'GET /twin' },
411
+ doors: { manifest: 'GET /twin', ...Object.fromEntries(doors.map((d) => [`${d.method} ${d.path}`, d.note ?? 'a door of the World'])) },
408
412
  };
409
413
  }
410
414
  /** The GET /twin manifest — the discovery door's body. Education ships INSIDE the twin:
@@ -1,4 +1,5 @@
1
1
  import type { TwinStream } from './twin-fetch.js';
2
+ export declare function connectWebSocket(url: string, headers: Record<string, string>, protocols?: string[]): Promise<WebSocket>;
2
3
  export type HttpHandler = (request: Request) => Response | Promise<Response>;
3
4
  export type WebSocketPeer = {
4
5
  send: (data: string | Uint8Array) => unknown;
@@ -28,7 +29,7 @@ export type ServeHttpOptions = {
28
29
  fetch: HttpHandler;
29
30
  /** seconds a connection may idle (Bun honors it; Node's keep-alive timeout is set from it) */
30
31
  idleTimeout?: number;
31
- /** the largest request body accepted, in bytes (Bun's default is 128 MB; Node sets none) */
32
+ /** Explicit transport limit in bytes; absent, Bun retains its 128 MiB default. */
32
33
  maxRequestBodySize?: number;
33
34
  /** what a thrown handler answers with; absent, a 500 carrying the error (as Bun.serve) */
34
35
  error?: (error: Error) => Response | Promise<Response>;
@@ -7,6 +7,12 @@
7
7
  // Nothing here runs at module scope, and `node:http` is reached through `process.getBuiltinModule`
8
8
  // rather than a static import: this module rides into browser client bundles through the kernel's
9
9
  // entrypoint, where a static `node:` import is the 2026-09-06 class of break.
10
+ /** One socket library at the host seam, shared by inbound upgrades and authenticated outbound relays. */
11
+ const socketLibrary = () => import('ws');
12
+ export async function connectWebSocket(url, headers, protocols = []) {
13
+ const { WebSocket: Client } = await socketLibrary();
14
+ return new Client(url, protocols, { headers });
15
+ }
10
16
  /** A decorator the request journal installs: every server made through the seam serves through it. */
11
17
  let decorate;
12
18
  export function setServeDecorator(fn) { decorate = fn; }
@@ -18,6 +24,9 @@ export async function serveHttp(options) {
18
24
  // twin from a leftover World's that holds the same port. Outside a World nothing changes.
19
25
  // a pack's fetch carries the upgrade its sockets take (pack-fetch.ts), read before anything wraps the fetch: served on
20
26
  // its own or in a World, its sockets answer
27
+ const declaredCap = options.fetch.maxRequestBodySize;
28
+ if (options.maxRequestBodySize === undefined && declaredCap !== undefined)
29
+ options = { ...options, maxRequestBodySize: declaredCap };
21
30
  const carried = options.upgrade ?? options.fetch.upgrade;
22
31
  if (carried)
23
32
  options = { ...options, upgrade: carried };
@@ -327,7 +336,7 @@ async function serveOnNode(options) {
327
336
  });
328
337
  let socketServer;
329
338
  if (options.upgrade) {
330
- const { WebSocketServer } = await import('ws');
339
+ const { WebSocketServer } = await socketLibrary();
331
340
  const upgrade = options.upgrade;
332
341
  const requestOf = (req) => new Request(`${protocol}://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, { headers: Object.fromEntries(Object.entries(req.headers).filter((entry) => typeof entry[1] === 'string')) });
333
342
  // the subprotocol the socket selects, answered as Bun's branch answers it
package/dist/src/serve.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { parseExactJson } from "./exact-json.js";
1
2
  // Twin serve layer (the twins architecture notes, increment 1): answer vendor-shaped
2
3
  // READS from the local event-sourced state — the "local twin" read path. An app
3
4
  // or agent points at this instead of the real vendor; it responds from local
@@ -56,7 +57,7 @@ export function readTwinRequestJournal(service, root, options = {}) {
56
57
  if (!line)
57
58
  continue;
58
59
  try {
59
- rows.push(JSON.parse(line));
60
+ rows.push(parseExactJson(line));
60
61
  }
61
62
  catch { /* a torn line */ }
62
63
  }
@@ -107,7 +108,7 @@ export function readTwinRequestJournalFrom(service, root, cursor, options = {})
107
108
  if (!line)
108
109
  continue;
109
110
  try {
110
- entries.push(JSON.parse(line));
111
+ entries.push(parseExactJson(line));
111
112
  }
112
113
  catch { /* a torn line */ }
113
114
  }
@@ -363,8 +364,10 @@ function journalingFetch(config, callSiteVendor) {
363
364
  const response = (await identitySlot().run(slot, () => inner.call(this, request, server)));
364
365
  if (response instanceof Response)
365
366
  status = response.status;
366
- else if (response === undefined)
367
- return response; // upgraded (websocket) — nothing served
367
+ else if (response === undefined) {
368
+ status = 101;
369
+ return response;
370
+ } // accepted WebSocket upgrade
368
371
  return response;
369
372
  }
370
373
  finally {
@@ -7,14 +7,22 @@ export type Jwk = {
7
7
  kid: string;
8
8
  n: string;
9
9
  e: string;
10
+ } | {
11
+ kty: 'OKP';
12
+ use: 'sig';
13
+ alg: 'EdDSA';
14
+ crv: 'Ed25519';
15
+ kid: string;
16
+ x: string;
10
17
  };
11
18
  export type Jwks = {
12
19
  keys: Jwk[];
13
20
  };
14
- /** A signing key: an RSA private key as a PEM (its public half, given or derived, is what a JWKS serves and names by
15
- * `kid`), or a shared secret. Only deterministic algorithms: an ECDSA signature is random, so a World signs none. */
21
+ /** A signing key: an RSA or Ed25519 private key as a PEM (its public half, given or derived, is what a JWKS serves and
22
+ * names by `kid`), or a shared secret. Only deterministic algorithms: an ECDSA signature is random, so a World signs
23
+ * none; Ed25519's is deterministic (RFC 8032), Better Auth's default (`EdDSA`). */
16
24
  export type SigningKey = {
17
- alg: 'RS256' | 'RS384' | 'RS512';
25
+ alg: 'RS256' | 'RS384' | 'RS512' | 'EdDSA';
18
26
  privatePem: string;
19
27
  publicPem?: string;
20
28
  } | {
@@ -32,18 +40,19 @@ export type JwtVerdict = {
32
40
  /** A public key's id: a prefix and the first 16 hex of SHA-256 over its DER (what its JWKS and every token name). */
33
41
  export declare function keyId(publicPem: string, prefix?: string): string;
34
42
  /** A JWT: `iat` and `nbf` are `now` (seconds, the World's), `exp` `now` plus the lifetime (60 s when none is given),
35
- * unless the claims name their own. RS256 tokens carry the key's `kid`. */
43
+ * unless the claims name their own. A token of a key pair carries the key's `kid`. */
36
44
  export declare function jwtSign(claims: Row, key: SigningKey, opts: {
37
45
  now: number;
38
46
  expiresInSeconds?: number;
39
47
  kidPrefix?: string;
48
+ typ?: string;
40
49
  }): string;
41
50
  /** A JWT's header and payload, unverified. */
42
51
  export declare function jwtDecode(token: string): {
43
52
  header: Row;
44
53
  payload: Row;
45
54
  };
46
- /** A token checked: an RS256 one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
55
+ /** A token checked: an RS256 or EdDSA one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
47
56
  * then its `exp`/`nbf` at `now` (seconds). */
48
57
  export declare function jwtVerify(token: string, key: Jwks | {
49
58
  alg: 'HS256' | 'HS384' | 'HS512';
@@ -79,6 +88,9 @@ export declare const md5: (value: string | Uint8Array) => string;
79
88
  export declare function uuidFrom(seed: string): string;
80
89
  /** `count` lowercase letters derived from `seed` (a 20-letter project ref, a slug's suffix), the same every run. */
81
90
  export declare function lettersFrom(seed: string, count?: number): string;
91
+ /** `count` base62 characters (0-9A-Za-z) derived from `seed`, the same every run: the opaque tail of an id a vendor
92
+ * writes in base62 (OpenAI's `call_unLAR8MvFNptuiZK6K6HCy5k`, 24 after its prefix). */
93
+ export declare function base62From(seed: string, count?: number): string;
82
94
  /** Two strings compared in constant time. */
83
95
  export declare function equalSecrets(a: string, b: string): boolean;
84
96
  /** Bytes signed with a private key (RSA with SHA-256, or Ed25519), in the encoding asked: a certificate's signature,
@@ -123,6 +135,7 @@ export declare const handlerCrypto: {
123
135
  readonly digest: typeof digest;
124
136
  readonly uuidFrom: typeof uuidFrom;
125
137
  readonly lettersFrom: typeof lettersFrom;
138
+ readonly base62From: typeof base62From;
126
139
  readonly equalSecrets: typeof equalSecrets;
127
140
  readonly sealedBoxKeyPair: typeof sealedBoxKeyPair;
128
141
  readonly sealedBoxOpen: typeof sealedBoxOpen;
@@ -132,4 +145,9 @@ export declare const handlerCrypto: {
132
145
  readonly certificateOf: typeof certificateOf;
133
146
  };
134
147
  export type HandlerCrypto = typeof handlerCrypto;
148
+ /** A [KSUID](https://github.com/segmentio/ksuid) as 27 base62 characters (0-9A-Za-z, most significant first): `seconds`
149
+ * past KSUID's epoch (1400000000) in four bytes, then `n` in four, then twelve bytes derived from `seed`, so KSUIDs
150
+ * made in one second sort by `n`, at `width` characters (27 unless a vendor's id is longer or shorter). Deterministic:
151
+ * the same arguments give the same id. */
152
+ export declare function ksuidFrom(seconds: number, n: number, seed: string, width?: number): string;
135
153
  export {};
@@ -1,6 +1,7 @@
1
+ import { parseExactJson, compareExactNumbers } from "./exact-json.js";
1
2
  // SIGNING ON THE CONTEXT (docs/contributing/architecture.md, "What an author writes, and how": the tokens-and-keys row)
2
3
  // — one deterministic implementation of what vendors sign with, given to handlers as `ctx.crypto`, so no pack reaches
3
- // `node:crypto` itself: JWTs (RS256 with a pack's instance key, HS256 with a shared secret), their verification, a
4
+ // `node:crypto` itself: JWTs (RS256 or EdDSA with a pack's instance key, HS256 with a shared secret), their verification, a
4
5
  // JWKS for a public key, an HMAC and a SHA-256. Nothing here is random: a key is the pack's data (a PEM it declares),
5
6
  // and every value a signature carries comes from the call.
6
7
  // A namespace, never named imports: a browser client bundles the kernel, and the browser's `node:crypto` polyfill has no
@@ -8,7 +9,7 @@
8
9
  import * as nodeCrypto from 'node:crypto';
9
10
  import { sealedBoxKeyPair, sealedBoxOpen } from "./sealed-box.js";
10
11
  /** The algorithms `jwtSign` signs with. */
11
- export const SIGNING_ALGORITHMS = ['RS256', 'RS384', 'RS512', 'HS256', 'HS384', 'HS512'];
12
+ export const SIGNING_ALGORITHMS = ['RS256', 'RS384', 'RS512', 'EdDSA', 'HS256', 'HS384', 'HS512'];
12
13
  const digestOf = (alg) => `sha${alg.slice(2)}`;
13
14
  const base64url = (input) => Buffer.from(input).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
14
15
  const fromBase64url = (s) => Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
@@ -19,15 +20,24 @@ export function keyId(publicPem, prefix = 'ins_') {
19
20
  return `${prefix}${nodeCrypto.createHash('sha256').update(der).digest('hex').slice(0, 16)}`;
20
21
  }
21
22
  /** A JWT: `iat` and `nbf` are `now` (seconds, the World's), `exp` `now` plus the lifetime (60 s when none is given),
22
- * unless the claims name their own. RS256 tokens carry the key's `kid`. */
23
+ * unless the claims name their own. A token of a key pair carries the key's `kid`. */
23
24
  export function jwtSign(claims, key, opts) {
25
+ // a key of another kind than its algorithm's is refused by name (an RSA key under EdDSA would fail inside the signer)
26
+ if ('privatePem' in key) {
27
+ const kind = nodeCrypto.createPrivateKey(key.privatePem).asymmetricKeyType;
28
+ const wants = key.alg === 'EdDSA' ? 'ed25519' : 'rsa';
29
+ if (kind !== wants)
30
+ throw new Error(`jwtSign: ${key.alg} signs with an ${wants === 'rsa' ? 'RSA' : 'Ed25519'} key, and this key is ${kind ?? 'of no known type'}`);
31
+ }
24
32
  const header = 'privatePem' in key
25
- ? { alg: key.alg, typ: 'JWT', kid: keyId(key.publicPem ?? nodeCrypto.createPublicKey(key.privatePem).export({ type: 'spki', format: 'pem' }).toString(), opts.kidPrefix) }
26
- : { alg: key.alg, typ: 'JWT' };
33
+ ? { alg: key.alg, typ: opts.typ ?? 'JWT', kid: keyId(key.publicPem ?? nodeCrypto.createPublicKey(key.privatePem).export({ type: 'spki', format: 'pem' }).toString(), opts.kidPrefix) }
34
+ : { alg: key.alg, typ: opts.typ ?? 'JWT' };
27
35
  const payload = { iat: opts.now, exp: opts.now + (opts.expiresInSeconds ?? 60), nbf: opts.now, ...claims };
28
36
  const input = `${jsonPart(header)}.${jsonPart(payload)}`;
29
37
  if ('secret' in key)
30
38
  return `${input}.${base64url(nodeCrypto.createHmac(digestOf(key.alg), key.secret).update(input).digest())}`;
39
+ if (key.alg === 'EdDSA')
40
+ return `${input}.${base64url(nodeCrypto.sign(null, Buffer.from(input), key.privatePem))}`;
31
41
  const signer = nodeCrypto.createSign(`RSA-SHA${key.alg.slice(2)}`);
32
42
  signer.update(input);
33
43
  signer.end();
@@ -38,17 +48,17 @@ export function jwtDecode(token) {
38
48
  const parts = token.split('.');
39
49
  if (parts.length !== 3)
40
50
  throw new Error('malformed jwt');
41
- const part = (s) => JSON.parse(fromBase64url(s).toString('utf8'));
51
+ const part = (s) => parseExactJson(fromBase64url(s).toString('utf8'));
42
52
  return { header: part(parts[0]), payload: part(parts[1]) };
43
53
  }
44
54
  function timely(payload, now) {
45
- if (typeof payload.exp === 'number' && now >= payload.exp)
55
+ if ((typeof payload.exp === 'number' || typeof payload.exp === 'string' && /^-?\d+$/.test(payload.exp)) && compareExactNumbers(now, payload.exp) >= 0)
46
56
  return { valid: false, reason: 'expired' };
47
- if (typeof payload.nbf === 'number' && now < payload.nbf)
57
+ if ((typeof payload.nbf === 'number' || typeof payload.nbf === 'string' && /^-?\d+$/.test(payload.nbf)) && compareExactNumbers(now, payload.nbf) < 0)
48
58
  return { valid: false, reason: 'not_yet_valid' };
49
59
  return { valid: true, payload };
50
60
  }
51
- /** A token checked: an RS256 one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
61
+ /** A token checked: an RS256 or EdDSA one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
52
62
  * then its `exp`/`nbf` at `now` (seconds). */
53
63
  export function jwtVerify(token, key, opts) {
54
64
  let decoded;
@@ -68,12 +78,20 @@ export function jwtVerify(token, key, opts) {
68
78
  return { valid: false, reason: 'bad_signature' };
69
79
  return timely(decoded.payload, opts.now);
70
80
  }
71
- if (!['RS256', 'RS384', 'RS512'].includes(String(decoded.header.alg)))
81
+ if (!['RS256', 'RS384', 'RS512', 'EdDSA'].includes(String(decoded.header.alg)))
72
82
  return { valid: false, reason: 'unexpected_alg' };
73
83
  // a token naming a key the set does not hold matches none; one naming no key is tried against the set's only key
74
84
  const jwk = decoded.header.kid !== undefined ? key.keys.find((k) => k.kid === decoded.header.kid) : key.keys.length === 1 ? key.keys[0] : undefined;
75
85
  if (!jwk)
76
86
  return { valid: false, reason: 'no_matching_key' };
87
+ if ((jwk.kty === 'OKP') !== (decoded.header.alg === 'EdDSA'))
88
+ return { valid: false, reason: 'unexpected_alg' };
89
+ if (jwk.kty === 'OKP') {
90
+ const edKey = nodeCrypto.createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: jwk.x }, format: 'jwk' });
91
+ if (!nodeCrypto.verify(null, Buffer.from(`${h}.${p}`), edKey, fromBase64url(s)))
92
+ return { valid: false, reason: 'bad_signature' };
93
+ return timely(decoded.payload, opts.now);
94
+ }
77
95
  const verifier = nodeCrypto.createVerify(`RSA-SHA${String(decoded.header.alg).slice(2)}`);
78
96
  verifier.update(`${h}.${p}`);
79
97
  verifier.end();
@@ -85,6 +103,8 @@ export function jwtVerify(token, key, opts) {
85
103
  /** The JWKS that verifies every token a public key's pair signs. */
86
104
  export function jwks(publicPem, kidPrefix) {
87
105
  const jwk = nodeCrypto.createPublicKey(publicPem).export({ format: 'jwk' });
106
+ if (jwk.kty === 'OKP')
107
+ return { keys: [{ kty: 'OKP', use: 'sig', alg: 'EdDSA', crv: 'Ed25519', kid: keyId(publicPem, kidPrefix), x: String(jwk.x) }] };
88
108
  return { keys: [{ kty: 'RSA', use: 'sig', alg: 'RS256', kid: keyId(publicPem, kidPrefix), n: String(jwk.n), e: String(jwk.e) }] };
89
109
  }
90
110
  /** An HMAC of a value, hex or base64 (a webhook's signature, a signed id): SHA-256 unless the vendor names another
@@ -158,6 +178,23 @@ export function lettersFrom(seed, count = 20) {
158
178
  }
159
179
  return out;
160
180
  }
181
+ /** `count` base62 characters (0-9A-Za-z) derived from `seed`, the same every run: the opaque tail of an id a vendor
182
+ * writes in base62 (OpenAI's `call_unLAR8MvFNptuiZK6K6HCy5k`, 24 after its prefix). */
183
+ export function base62From(seed, count = 24) {
184
+ const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
185
+ let out = '';
186
+ for (let round = 0; out.length < count; round += 1) {
187
+ // each byte below 248 (4 x 62) maps evenly onto the alphabet; the rest are skipped
188
+ for (const byte of sha256(`${seed}:${round}`).match(/../g)) {
189
+ if (out.length === count)
190
+ break;
191
+ const b = Number.parseInt(byte, 16);
192
+ if (b < 248)
193
+ out += ALPHABET[b % 62];
194
+ }
195
+ }
196
+ return out;
197
+ }
161
198
  /** Two strings compared in constant time. */
162
199
  export function equalSecrets(a, b) {
163
200
  const left = Buffer.from(a, 'utf8');
@@ -219,4 +256,30 @@ export function certificateOf(cert) {
219
256
  }
220
257
  }
221
258
  /** What a handler signs and hashes with: `ctx.crypto`. */
222
- export const handlerCrypto = { jwtSign, jwtVerify, jwtDecode, jwks, keyId, hmac, sha256, md5, digest, uuidFrom, lettersFrom, equalSecrets, sealedBoxKeyPair, sealedBoxOpen, signWith, verifyWith, publicKeyOf, certificateOf };
259
+ export const handlerCrypto = { jwtSign, jwtVerify, jwtDecode, jwks, keyId, hmac, sha256, md5, digest, uuidFrom, lettersFrom, base62From, equalSecrets, sealedBoxKeyPair, sealedBoxOpen, signWith, verifyWith, publicKeyOf, certificateOf };
260
+ /** A [KSUID](https://github.com/segmentio/ksuid) as 27 base62 characters (0-9A-Za-z, most significant first): `seconds`
261
+ * past KSUID's epoch (1400000000) in four bytes, then `n` in four, then twelve bytes derived from `seed`, so KSUIDs
262
+ * made in one second sort by `n`, at `width` characters (27 unless a vendor's id is longer or shorter). Deterministic:
263
+ * the same arguments give the same id. */
264
+ export function ksuidFrom(seconds, n, seed, width = 27) {
265
+ const bytes = new Uint8Array(20);
266
+ const view = new DataView(bytes.buffer);
267
+ view.setUint32(0, Math.max(0, seconds - 1_400_000_000) >>> 0);
268
+ view.setUint32(4, n >>> 0);
269
+ const tail = sha256(seed);
270
+ for (let i = 0; i < 12; i++)
271
+ bytes[8 + i] = parseInt(tail.slice(i * 2, i * 2 + 2), 16);
272
+ const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
273
+ let value = 0n;
274
+ for (const b of bytes)
275
+ value = (value << 8n) | BigInt(b);
276
+ let out = '';
277
+ while (value > 0n) {
278
+ out = ALPHABET[Number(value % 62n)] + out;
279
+ value /= 62n;
280
+ }
281
+ const full = out.padStart(27, '0');
282
+ // another width than KSUID's 27 (a vendor's id of 24 or 29 characters): its first `width` characters, which keep the
283
+ // instant and the mint count (and so the order) from 11 on, or more base62 characters derived from the seed after them
284
+ return width <= 27 ? full.slice(0, width) : `${full}${base62From(`${seed}:ksuid`, width - 27)}`;
285
+ }
@@ -37,7 +37,8 @@ export declare function signSigV4(input: {
37
37
  /**
38
38
  * A presigned URL signed again with a key: its date, expiry, region, service and signed headers are the URL's own
39
39
  * (X-Amz-Date, X-Amz-Expires, X-Amz-Credential's scope, X-Amz-SignedHeaders, `host` when it names none), its key and
40
- * signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it.
40
+ * signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it. A
41
+ * signed header other than `host` (a Content-Type the URL binds) is signed with the value the request sends (`headers`).
41
42
  */
42
43
  export declare function presignSigV4(input: {
43
44
  method: string;
@@ -45,4 +46,5 @@ export declare function presignSigV4(input: {
45
46
  keyId: string;
46
47
  secret: string;
47
48
  host?: string;
49
+ headers?: Record<string, string>;
48
50
  }): string;
package/dist/src/sigv4.js CHANGED
@@ -136,7 +136,8 @@ export function signSigV4(input) {
136
136
  /**
137
137
  * A presigned URL signed again with a key: its date, expiry, region, service and signed headers are the URL's own
138
138
  * (X-Amz-Date, X-Amz-Expires, X-Amz-Credential's scope, X-Amz-SignedHeaders, `host` when it names none), its key and
139
- * signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it.
139
+ * signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it. A
140
+ * signed header other than `host` (a Content-Type the URL binds) is signed with the value the request sends (`headers`).
140
141
  */
141
142
  export function presignSigV4(input) {
142
143
  const url = new URL(input.url);
@@ -156,7 +157,8 @@ export function presignSigV4(input) {
156
157
  .map(([k, v]) => `${k}=${v}`)
157
158
  .join('&');
158
159
  const names = (q.get('X-Amz-SignedHeaders') ?? 'host').toLowerCase().split(';').filter(Boolean);
159
- const canonicalHeaders = names.map((n) => `${n}:${n === 'host' ? (input.host ?? url.host) : ''}\n`).join('');
160
+ const sent = new Headers(input.headers ?? {});
161
+ const canonicalHeaders = names.map((n) => `${n}:${(n === 'host' ? (input.host ?? url.host) : sent.get(n) ?? '').trim().replace(/\s+/g, ' ')}\n`).join('');
160
162
  const canonicalRequest = [input.method.toUpperCase(), canonicalUri, canonicalQuery, canonicalHeaders, names.join(';'), 'UNSIGNED-PAYLOAD'].join('\n');
161
163
  const stringToSign = ['AWS4-HMAC-SHA256', amzDate, `${dateStamp}/${region}/${service}/aws4_request`, sha256Hex(canonicalRequest)].join('\n');
162
164
  let key = hmac(`AWS4${input.secret}`, dateStamp);
package/dist/src/smtp.js CHANGED
@@ -40,7 +40,8 @@ export async function sendMail(route, mail, headers = {}) {
40
40
  import('node:net'),
41
41
  import('../network-policy.cjs'),
42
42
  ]);
43
- const refusal = worldEgressRefusal(`http://${route.host}:${route.port}/`);
43
+ // sending mail is a write: an external relay needs a `writes` grant
44
+ const refusal = worldEgressRefusal(`http://${route.host}:${route.port}/`, process.env, 'SMTP');
44
45
  if (refusal !== null)
45
46
  throw new Error(refusal);
46
47
  const socket = connect(route.port, route.host);
@@ -1,5 +1,7 @@
1
1
  const sessions = new Map();
2
- let nextSession = 1;
2
+ /** A World's next session number, per World (its service and root): a session's id is the World's, never the
3
+ * process's, so a life replayed on a fresh World numbers its sessions as it did the first time. */
4
+ const nextSession = new Map();
3
5
  const keyOf = (service, root) => `${service}\0${root ?? ''}`;
4
6
  /** The open sessions of one socket of the vendor in a World (a handler reaches them as `ctx.sockets(id)`). */
5
7
  export function openSessions(service, root, socket) {
@@ -40,7 +42,7 @@ export function socketUpgrade(service, root, decls, engines, context, hostOf, pa
40
42
  const engine = engines[decl.id];
41
43
  const pending = new Map();
42
44
  const session = {
43
- id: nextSession++, socket: decl.id, state: {},
45
+ id: (() => { const n = (nextSession.get(key) ?? 0) + 1; nextSession.set(key, n); return n; })(), socket: decl.id, state: {},
44
46
  send: (frame) => { peer.send(typeof frame === 'string' || frame instanceof Uint8Array ? frame : JSON.stringify(frame)); },
45
47
  close: (code, reason) => { peer.close(code, reason); },
46
48
  expect: (key, ms) => new Promise((resolve, reject) => {
@@ -83,8 +85,11 @@ export function socketUpgrade(service, root, decls, engines, context, hostOf, pa
83
85
  o.pending.delete(k);
84
86
  }
85
87
  // the end is written after what the session is still doing, over its own context
88
+ // the session is gone, so a failure has no frame to go in: it is reported, never swallowed
86
89
  if (o.engine.close)
87
- o.queue = o.queue.then(async () => o.engine.close(o.session, await o.context())).catch(() => { });
90
+ o.queue = o.queue.then(async () => o.engine.close(o.session, await o.context())).catch((error) => {
91
+ console.error(`${service}: the ${o.session.socket} socket's end was not written: ${String(error?.message ?? error)}`);
92
+ });
88
93
  },
89
94
  };
90
95
  }
@@ -38,6 +38,9 @@ export type StateSystemAdapters = {
38
38
  root?: string;
39
39
  secret?: string;
40
40
  }) => Response | Promise<Response>;
41
+ /** Why a recorded write is the World's own and sent nowhere, or undefined when perform sends it: a unit of the pack that
42
+ * declares `vendorBacked.none` beside lanes that are vendor-backed (PlanetScale's SQL wire beside its API lane). */
43
+ unsent?: (input: Record<string, unknown> | undefined) => string | undefined;
41
44
  };
42
45
  /** Register a twin's adapters (the mounting host does this from the pack's descriptor or its
43
46
  * anchored exports). Replaces any earlier registration for the service. */
@@ -1,3 +1,4 @@
1
+ import { parseExactJson } from "./exact-json.js";
1
2
  import { dirname, isAbsolute, join, resolve } from 'node:path';
2
3
  import { withStateLock, withStateRemoval } from "./ancestry.js";
3
4
  import { appendParentEntry, toEntry } from "./log.js";
@@ -121,7 +122,7 @@ function readJsonl(path) {
121
122
  if (!line.trim())
122
123
  continue;
123
124
  try {
124
- rows.push(JSON.parse(line));
125
+ rows.push(parseExactJson(line));
125
126
  }
126
127
  catch (error) {
127
128
  throw new Error(`${path}:${index + 1}: invalid JSONL row: ${error.message}`);
@@ -137,7 +138,7 @@ export function readJsonFile(path) {
137
138
  const raw = getActiveWorldStore().read(path);
138
139
  if (raw === null)
139
140
  throw new Error('no such file');
140
- return JSON.parse(raw);
141
+ return parseExactJson(raw);
141
142
  }
142
143
  catch (error) {
143
144
  throw new Error(`${path}: invalid JSON: ${error.message}`);
@@ -292,6 +293,8 @@ export function createEvent(input) {
292
293
  * one of them so scrubService never demands --force for an ordinary twin. */
293
294
  const KNOWN_SERVICE_ENTRIES = new Set([
294
295
  'ancestry.json', 'history-format.json', 'views', 'origin.jsonl', 'origin-head.json',
296
+ 'answer-count',
297
+ 'answer-count.lock',
295
298
  'events.jsonl',
296
299
  'events.jsonl.lock',
297
300
  'event-queue.jsonl',
@@ -50,12 +50,14 @@ export type TwinStream = (sink: TwinStreamSink, peer: string) => TwinStreamConne
50
50
  export declare const TWIN_REQUEST_SCOPES: readonly string[];
51
51
  /**
52
52
  * THE READ SCOPE for a pack that writes its own fetch (the derived packs, the byte-wire packs): a
53
- * request carrying the read-only marker runs with the kernel's write seam refusing its writes, and a
53
+ * request carrying the read-only marker, or an instance mounted read-only, runs with the kernel's
54
+ * write seam refusing its writes, and a
54
55
  * refused write answers `refuse` (the vendor's own read-only error; a generic 405 without one) —
55
56
  * whatever the handler made of the refusal. `GET /twin` advertises `requestScopes`. Everything else
56
57
  * passes through untouched; the wrapped fetch keeps its own properties (a derived fetch's `owners`).
57
58
  */
58
59
  export declare function withRequestScopes<F extends (request: Request) => Promise<Response>>(fetch: F, opts?: {
60
+ readOnly?: boolean;
59
61
  refuse?: (request: Request, error: ReadOnlyRequestError) => Response | Promise<Response>;
60
62
  }): F;
61
63
  export type TwinFetchHandlerResult = {
@@ -130,7 +130,8 @@ function readOnlyRefusal(error) {
130
130
  }
131
131
  /**
132
132
  * THE READ SCOPE for a pack that writes its own fetch (the derived packs, the byte-wire packs): a
133
- * request carrying the read-only marker runs with the kernel's write seam refusing its writes, and a
133
+ * request carrying the read-only marker, or an instance mounted read-only, runs with the kernel's
134
+ * write seam refusing its writes, and a
134
135
  * refused write answers `refuse` (the vendor's own read-only error; a generic 405 without one) —
135
136
  * whatever the handler made of the refusal. `GET /twin` advertises `requestScopes`. Everything else
136
137
  * passes through untouched; the wrapped fetch keeps its own properties (a derived fetch's `owners`).
@@ -149,7 +150,7 @@ export function withRequestScopes(fetch, opts = {}) {
149
150
  // the request's W3C traceparent follows every entry the fetch writes, as through the kernel's own adapters
150
151
  // (a custom fetch never entered it, so the timeline's trace filter missed its entries)
151
152
  return runWithRequestTrace(request, async () => {
152
- if (!isReadOnlyRequest(request))
153
+ if (!opts.readOnly && !isReadOnlyRequest(request))
153
154
  return fetch(request);
154
155
  const out = await runAsReadOnlyRequest(() => fetch(request));
155
156
  if (!out.refused)
@@ -1,6 +1,13 @@
1
+ import type { ApplicationStandIn } from './events.js';
1
2
  /** A call to a vendor the World runs no twin of. */
2
3
  export declare class VendorUnreachableError extends Error {
3
- constructor(message: string);
4
+ constructor(message: string, options?: ErrorOptions);
4
5
  }
5
- /** `url`, a vendor's own address, answered by the World's twin of that vendor. */
6
- export declare function vendorFetch(url: string | URL, init?: RequestInit): Promise<Response>;
6
+ /** A twin answered in this process: its fetch, as a World's server hands it a request. */
7
+ export type InProcessTwin = (request: Request) => Response | Promise<Response>;
8
+ /** The twins this process answers itself, by routing key (a key of VENDOR_HOSTS: a journey walk's companions, over the
9
+ * walk's own World), consulted before the instance's; returns those set before, which the caller puts back. */
10
+ export declare function setInProcessTwins(twins: Readonly<Record<string, InProcessTwin>>): Readonly<Record<string, InProcessTwin>>;
11
+ /** A vendor URL (or the World's application), including redirects, always answered inside the World; `standIn` is the
12
+ * calling fetch's application stand-in (PackFetchOptions.application), consulted at every hop. */
13
+ export declare function vendorFetch(url: string | URL, init?: RequestInit, standIn?: ApplicationStandIn): Promise<Response>;