@volter/world-core 3.0.38 → 3.0.40

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 +110 -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 +110 -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,3 +1,4 @@
1
+ import { parseExactJson, compareExactNumbers, exactInteger, isExactIntegerValue } from "./exact-json.js";
1
2
  // THE DERIVED CORE — what a derived pack does for an operation it declares no handler for
2
3
  // (docs/contributing/architecture.md, "Protocol 3"). The generated surface says what the
3
4
  // vendor's operations are; the pack's manifest says the vendor facts no spec carries (how ids look,
@@ -14,20 +15,23 @@ import { GitRefs } from "./git/refs.js";
14
15
  import { worldNow } from "./world-clock.js";
15
16
  import { worldEnvValue } from "./world-env.js";
16
17
  import { openSessions, socketWrite } from "./sockets.js";
17
- import { MACHINE_POOL, poolFor } from "./machines.js";
18
- import { askApplication, deliverEvents, deliverToApplication } from "./events.js";
19
- import { handlerCrypto, hmac, lettersFrom, sha256, uuidFrom } from "./signing.js";
18
+ import { awaitWrite, awaitCondition, heldReadsWrite } from "./held-reads.js";
19
+ import { MACHINE_POOL, poolFor, guardedMachinePool } from "./machines.js";
20
+ import { askApplication, deliverEvents } from "./events.js";
21
+ import { handlerCrypto, hmac, ksuidFrom, lettersFrom, sha256, uuidFrom } from "./signing.js";
20
22
  import { hashFieldValue } from "./hash.js";
21
23
  import { readResourceBlob, removeResourceBlob, writeResourceBlob } from "./resource-blob.js";
22
24
  import { managedDatabase, unboundDatabase } from "./managed-database.js";
23
25
  import { execRedisRun } from "./redis/engine.js";
24
26
  import { sendMail } from "./smtp.js";
27
+ import { ownField, parseBracketForm } from "./form.js";
28
+ export { ownField, parseBracketForm } from "./form.js";
25
29
  import { multipartBoundary, multipartParts } from "./multipart.js";
26
- import { projectOwnerResources, resolveSubjectId, subjectAliases } from "./actions.js";
30
+ import { listActions, projectOwnerResources, resolveSubjectId, subjectAliases } from "./actions.js";
27
31
  import { vendorFetch } from "./vendor-call.js";
28
32
  import { packReferences } from "./references.js";
29
33
  import { twinPublicBase, withRequestScopes } from "./twin-fetch.js";
30
- import { inVendorMove, isReadOnlyRequest, runAsVendorMove } from "./request-scope.js";
34
+ import { inVendorMove, isReadOnlyRequest, runAsVendorMove, writesRefused } from "./request-scope.js";
31
35
  import { bodyTextOf, checkJsonBody, takeParsedBody } from "./derived.js";
32
36
  /** What a move to `to` stores in the field: a boolean field's value is a boolean, and a derived field's
33
37
  * value is written by the move's effects (a state name is not a timestamp), so it stores nothing itself. */
@@ -44,7 +48,7 @@ export function stateOf(decl, value) {
44
48
  return d.state;
45
49
  if (w.equals !== undefined && value === w.equals)
46
50
  return d.state;
47
- if (w.gt !== undefined && typeof value === 'number' && value > w.gt)
51
+ if (w.gt !== undefined && (typeof value === 'number' || isExactIntegerValue(value)) && compareExactNumbers(value, w.gt) > 0)
48
52
  return d.state;
49
53
  if (w.truthy !== undefined && Boolean(value) === w.truthy)
50
54
  return d.state;
@@ -68,13 +72,13 @@ export function strictFields(m, operation, body) {
68
72
  const { type } = field;
69
73
  const fits = value === null
70
74
  || (type === 'boolean' && typeof value === 'boolean')
71
- || (type === 'integer' && Number.isInteger(value))
72
- || (type === 'number' && typeof value === 'number')
75
+ || (type === 'integer' && isExactIntegerValue(value))
76
+ || (type === 'number' && (typeof value === 'number' || isExactIntegerValue(value)))
73
77
  || (type === 'string' && typeof value === 'string')
74
78
  || !['boolean', 'integer', 'number', 'string'].includes(type);
75
79
  if (!fits)
76
80
  return refuse(strict.type, { name, type: type === 'integer' ? 'an integer' : `a ${type}` });
77
- if (typeof value === 'number' && ((field.minimum !== undefined && value < field.minimum) || (field.maximum !== undefined && value > field.maximum))) {
81
+ if ((typeof value === 'number' || isExactIntegerValue(value)) && ((field.minimum !== undefined && compareExactNumbers(value, field.minimum) < 0) || (field.maximum !== undefined && compareExactNumbers(value, field.maximum) > 0))) {
78
82
  return refuse(strict.range, { name, min: field.minimum ?? null, max: field.maximum ?? null });
79
83
  }
80
84
  fields[name] = value;
@@ -98,10 +102,10 @@ function validateBody(m, operation, body) {
98
102
  continue;
99
103
  }
100
104
  const types = field.type.split('|');
101
- const fits = types.some((type) => type === 'null' ? value === null : type === 'integer' ? Number.isInteger(value) : type === 'array' ? Array.isArray(value) : type === 'object' ? value !== null && typeof value === 'object' && !Array.isArray(value) : type === 'string' ? typeof value === 'string' : type === 'number' ? typeof value === 'number' : type === 'boolean' ? typeof value === 'boolean' : true);
105
+ const fits = types.some((type) => type === 'null' ? value === null : type === 'integer' ? isExactIntegerValue(value) : type === 'array' ? Array.isArray(value) : type === 'object' ? value !== null && typeof value === 'object' && !Array.isArray(value) : type === 'string' ? typeof value === 'string' : type === 'number' ? typeof value === 'number' || isExactIntegerValue(value) : type === 'boolean' ? typeof value === 'boolean' : true);
102
106
  if (!fits)
103
107
  return refuse(rule.type, { name: field.name, type: field.type, input: value });
104
- if (typeof value === 'number' && ((field.minimum !== undefined && value < field.minimum) || (field.maximum !== undefined && value > field.maximum)))
108
+ if ((typeof value === 'number' || isExactIntegerValue(value)) && ((field.minimum !== undefined && compareExactNumbers(value, field.minimum) < 0) || (field.maximum !== undefined && compareExactNumbers(value, field.maximum) > 0)))
105
109
  return refuse(rule.range, { name: field.name, min: field.minimum ?? null, max: field.maximum ?? null, input: value });
106
110
  }
107
111
  return undefined;
@@ -120,73 +124,6 @@ export function flagOf(m, value) {
120
124
  return false;
121
125
  return undefined;
122
126
  }
123
- /** A form body's bracket notation (`a[b][0][c]=v`, `expand[]=x`) as nested objects and arrays. A form carries only
124
- * text: a field the operation's spec types as a number or a boolean (`scalars`, by bracket path, written by
125
- * world-tooling's formScalarsOf) is read as one when its text is that literal; every other field stays the text sent
126
- * (`metadata[order]=007`, `name=2024`). */
127
- /** A field set as the object's own, whatever its name (`__proto__`, `constructor` and `prototype` are data a caller
128
- * may name: Stripe's metadata keys are the caller's), never through what the object inherits. */
129
- export function ownField(node, key, value) {
130
- Object.defineProperty(node, key, { value, writable: true, enumerable: true, configurable: true });
131
- }
132
- /** A field the object holds as its own, never one it inherits (`constructor` is every object's). */
133
- const ownValue = (node, key) => (Object.hasOwn(node, key) ? node[key] : undefined);
134
- export function parseBracketForm(text, scalars) {
135
- const out = {};
136
- for (const [rawKey, raw] of new URLSearchParams(text)) {
137
- const parts = rawKey.replace(/\]/g, '').split('[');
138
- const kind = scalars && scalarAt(scalars, parts);
139
- const value = kind === 'boolean' ? (raw === 'true' ? true : raw === 'false' ? false : raw)
140
- : kind === 'integer' ? (/^-?\d+$/.test(raw) ? Number(raw) : raw)
141
- : kind === 'number' ? (/^-?\d+(\.\d+)?$/.test(raw) ? Number(raw) : raw)
142
- : raw;
143
- // every field is the node's own, read and written as such: a key naming an object's machinery (`__proto__[x]`,
144
- // `constructor[prototype][x]`) is a field of that name, never a walk into what every object inherits
145
- let node = out;
146
- for (const [i, key] of parts.entries()) {
147
- // an array holds its items by index (`a[]`, `a[0]`): a named key under one (`a[]=1&a[length]=-1`) has nowhere to
148
- // go, as a key under text has none (`a=1&a[b]=2`): the first shape stands
149
- if (Array.isArray(node) && key !== '' && !/^\d+$/.test(key))
150
- break;
151
- if (i === parts.length - 1) {
152
- if (key === '') {
153
- if (Array.isArray(node))
154
- node.push(value);
155
- }
156
- else
157
- ownField(node, key, value);
158
- break;
159
- }
160
- const next = parts[i + 1];
161
- if (key === '')
162
- break;
163
- let child = ownValue(node, key);
164
- if (child === undefined) {
165
- child = next === '' || /^\d+$/.test(next) ? [] : {};
166
- ownField(node, key, child);
167
- }
168
- if (child === null || typeof child !== 'object')
169
- break;
170
- node = child;
171
- }
172
- }
173
- return out;
174
- }
175
- /** The type `scalars` gives a form key's parts: a segment is a property name, `[]` an array's item (an index or empty),
176
- * `*` a map's key. */
177
- function scalarAt(scalars, parts) {
178
- let paths = [''];
179
- for (const [i, part] of parts.entries()) {
180
- const segs = part === '' || /^\d+$/.test(part) ? ['[]', '*'] : [part, '*'];
181
- paths = paths.flatMap((p) => segs.map((seg) => (i === 0 ? seg : `${p}.${seg}`)));
182
- if (paths.length > 64)
183
- paths = paths.filter((p) => Object.keys(scalars).some((k) => k === p || k.startsWith(`${p}.`)));
184
- }
185
- for (const p of paths)
186
- if (scalars[p])
187
- return scalars[p];
188
- return undefined;
189
- }
190
127
  /** A request's parameters, its query's and its body's. With the manifest's `body.form.coerce`, a form's fields the
191
128
  * operation's spec types as numbers or booleans are read as them (parseBracketForm); without an operation (a twin door)
192
129
  * every form value is its text. */
@@ -194,7 +131,7 @@ export async function readParams(manifest, request, operation) {
194
131
  const url = new URL(request.url);
195
132
  const scalars = manifest.body.form?.coerce ? (operation?.scalars ?? {}) : undefined;
196
133
  const query = parseBracketForm(url.search.replace(/^\?/, ''), scalars);
197
- if (request.method === 'GET' || request.method === 'HEAD')
134
+ if (request.method === 'GET' || request.method === 'HEAD' || (operation && bytesPayload(operation)))
198
135
  return query;
199
136
  const type = request.headers.get('content-type') ?? '';
200
137
  if (type.includes('multipart/form-data')) {
@@ -219,7 +156,7 @@ export async function readParams(manifest, request, operation) {
219
156
  }
220
157
  // NDJSON (`application/x-ndjson`) is lines of JSON, not a JSON document: the handler reads its text
221
158
  const media = (type.split(';')[0] ?? '').trim().toLowerCase();
222
- const isJson = media.includes('json') && !media.includes('ndjson');
159
+ const isJson = media.includes('json') && !media.includes('ndjson') && !media.includes('connect+');
223
160
  // a form is a body labelled one (application/x-www-form-urlencoded); a body of any other type (an image, a video, an
224
161
  // octet-stream PUT to an upload URL, a text/plain SQL statement, an NDJSON batch) is no form: its parameters are the
225
162
  // query's, and the handler reads its text or bytes itself
@@ -235,7 +172,7 @@ export async function readParams(manifest, request, operation) {
235
172
  let body;
236
173
  if (asJson) {
237
174
  try {
238
- body = JSON.parse(text);
175
+ body = parseExactJson(text);
239
176
  }
240
177
  catch {
241
178
  if (isJson)
@@ -353,27 +290,102 @@ function parentValue(parent, params) {
353
290
  return String(fill(parent.value ?? parent.params.map((p) => `{${p}}`).join('/'), params));
354
291
  return parent.param !== undefined ? params[parent.param] : undefined;
355
292
  }
293
+ function parentField(row, path) {
294
+ if (row && typeof row === 'object' && Object.hasOwn(row, path))
295
+ return row[path];
296
+ let value = row;
297
+ for (const key of path.split('.'))
298
+ value = value && typeof value === 'object' ? value[key] : undefined;
299
+ return value;
300
+ }
356
301
  function missingParent(m, resource, call, root) {
357
302
  const parent = m.resources[resource]?.parent;
358
303
  if (!parent)
359
304
  return undefined;
360
305
  const id = parentValue(parent, call.params);
361
306
  const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))]);
362
- const found = (r) => (where.length ? where.every(([field, v]) => String(r[field]) === v) : r.id === id);
307
+ const found = (r) => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id);
363
308
  return id !== undefined && stored(m, parent.resource, root, { withDeleted: parent.allowDeleted === true }).some(found) ? undefined : notFound(m, parent.resource, String(id), parent.param ?? parent.params?.at(-1));
364
309
  }
365
310
  /** The vendor's view of a stored subject: its own fields, bookkeeping (`_`) left out. */
366
311
  /** A stored subject as the vendor's object: its id first (the subject's, unless the pack kept an id of its own), then
367
312
  * the fields the pack wrote, bookkeeping (`_`) and the kernel's updatedAt left out. */
368
- export function render(r) {
369
- return Object.fromEntries(Object.entries({ id: r.id, ...ownFields(r) }).filter(([k]) => !k.startsWith('_') && k !== 'updatedAt'));
313
+ export function render(r, vendorUnderscored = []) {
314
+ return renderOwn({ id: r.id, ...ownFields(r) }, vendorUnderscored);
315
+ }
316
+ /** Filter an own-field view through the same declared vendor fields as a stored row. */
317
+ function renderOwn(own, vendorUnderscored = []) {
318
+ return Object.fromEntries(Object.entries(own).filter(([k]) => !k.startsWith('_') || vendorUnderscored.includes(k)));
319
+ }
320
+ const viewOmissions = new WeakMap();
321
+ // Precompute once at manifest binding: omitted fields, whether any exist, and their transitive containers through embeds.
322
+ function prepareViewOmissions(m) {
323
+ const held = viewOmissions.get(m);
324
+ if (held)
325
+ return held;
326
+ const fields = new Map();
327
+ for (const [resource, decl] of Object.entries(m.resources)) {
328
+ if (decl.viewOmit?.length)
329
+ fields.set(resource, new Set(decl.viewOmit));
330
+ }
331
+ const affected = new Set(fields.keys());
332
+ const embeds = new Map();
333
+ if (fields.size) {
334
+ let changed = true;
335
+ while (changed) {
336
+ changed = false;
337
+ for (const [resource, decl] of Object.entries(m.resources)) {
338
+ if (!affected.has(resource) && Object.values(decl.embeds ?? {}).some((target) => affected.has(target))) {
339
+ affected.add(resource);
340
+ changed = true;
341
+ }
342
+ }
343
+ }
344
+ for (const resource of affected) {
345
+ embeds.set(resource, Object.entries(m.resources[resource]?.embeds ?? {}).filter(([, target]) => affected.has(target)));
346
+ }
347
+ }
348
+ const plan = { fields, affected, embeds };
349
+ viewOmissions.set(m, plan);
350
+ return plan;
351
+ }
352
+ /** The one response-omission mechanism, shared by stored, constructed and expanded resource views. */
353
+ function omitView(m, resource, body, seen) {
354
+ const plan = viewOmissions.get(m) ?? prepareViewOmissions(m);
355
+ if (!resource || !plan.affected.has(resource))
356
+ return body;
357
+ const held = seen?.get(resource)?.get(body);
358
+ if (held)
359
+ return held;
360
+ const out = { ...body };
361
+ seen ??= new Map();
362
+ let visited = seen.get(resource);
363
+ if (!visited) {
364
+ visited = new WeakMap();
365
+ seen.set(resource, visited);
366
+ }
367
+ visited.set(body, out);
368
+ for (const field of plan.fields.get(resource) ?? [])
369
+ delete out[field];
370
+ for (const [field, target] of plan.embeds.get(resource) ?? []) {
371
+ const present = out[field];
372
+ if (Array.isArray(present)) {
373
+ out[field] = present.map((child) => child && typeof child === 'object' && !Array.isArray(child) ? omitView(m, target, child, seen) : child);
374
+ }
375
+ else if (present && typeof present === 'object') {
376
+ out[field] = omitView(m, target, present, seen);
377
+ }
378
+ }
379
+ return out;
370
380
  }
371
381
  /** A stored subject as the vendor answers it: its own fields, its id under the name the vendor gives it. */
372
382
  function view(m, resource, r) {
373
383
  const idAs = m.resources[resource]?.idAs;
384
+ const keep = m.resources[resource]?.vendorUnderscored ?? [];
385
+ const shown = omitView(m, resource, render(r, keep));
374
386
  if (!idAs)
375
- return render(r);
376
- const { id, ...rest } = render(r);
387
+ return shown;
388
+ const { id, ...rest } = shown;
377
389
  return { [idAs]: id, ...rest };
378
390
  }
379
391
  function now(m, at) {
@@ -447,33 +459,58 @@ function worldSigningKey(service, root, at, label, alg) {
447
459
  keying.set(key, made);
448
460
  return made;
449
461
  }
450
- function mintId(m, resource, root, at) {
462
+ function mintId(m, resource, root, at, snapshot) {
451
463
  if (!m.resources[resource])
452
464
  throw new Error(`mint: ${resource} is not a resource ${m.vendor}'s manifest declares`);
453
465
  const prefix = m.resources[resource].idPrefix;
454
466
  const template = m.resources[resource].ids ?? m.ids.template;
455
467
  if (template === '{uuid}')
456
- return mintUuid(m, resource, root);
468
+ return mintUuid(m, resource, root, snapshot);
457
469
  const snowflake = /^\{snowflake:(\d+)\}$/.exec(template);
458
470
  if (snowflake)
459
- return mintSnowflake(m, BigInt(snowflake[1]), root, at);
460
- // `{letters:N}` and `{hex:N}`, alone or after the resource's prefix (`{prefix}{letters:16}`: Fly's `vol_…`)
461
- const letters = /^(\{prefix\})?\{letters:(\d+)\}$/.exec(template);
471
+ return mintSnowflake(m, BigInt(snowflake[1]), root, at, snapshot);
472
+ // `{letters:N}` and `{hex:N}`, alone, after the resource's prefix (`{prefix}{letters:16}`: Fly's `vol_…`), or after it
473
+ // and a separator (`{prefix}_{hex:48}`: OpenAI's `resp_67cc…`)
474
+ const letters = /^(?:\{prefix\}([_-]?))?\{letters:(\d+)\}$/.exec(template);
462
475
  if (letters)
463
- return mintLetters(m, resource, Number(letters[2]), root, letters[1] ? prefix : '');
464
- const hex = /^(\{prefix\})?\{hex:(\d+)\}$/.exec(template);
476
+ return mintLetters(m, resource, Number(letters[2]), root, letters[1] !== undefined ? `${prefix}${letters[1]}` : '', snapshot);
477
+ const hex = /^(?:\{prefix\}([_-]?))?\{hex:(\d+)\}$/.exec(template);
465
478
  if (hex)
466
- return mintHex(m, resource, Number(hex[2]), root, hex[1] ? prefix : '');
479
+ return mintHex(m, resource, Number(hex[2]), root, hex[1] !== undefined ? `${prefix}${hex[1]}` : '', snapshot);
480
+ const digits = /^\{digits:(\d+)\}$/.exec(template);
481
+ if (digits) {
482
+ const width = Number(digits[1]);
483
+ if (!Number.isInteger(width) || width < 1 || width > 77)
484
+ throw new Error('mint: decimal width must be between 1 and 77');
485
+ const held = new Set((snapshot ? snapshot.filter((r) => r.type === storedType(m, resource)) : resourcesOfType(m.service, storedType(m, resource), root)).map((r) => r.id));
486
+ const floor = 10n ** BigInt(width - 1);
487
+ for (let n = held.size + 1;; n++) {
488
+ const id = String(floor + BigInt('0x' + sha256(`${m.service}:${resource}:${n}`)) % (9n * floor));
489
+ if (!held.has(id))
490
+ return id;
491
+ if (n - held.size > 9 * Number(floor))
492
+ throw new Error('mint: decimal id space exhausted');
493
+ }
494
+ }
495
+ // `{ksuid}` alone, after the prefix, or after it and a separator (`{prefix}_{ksuid}`: Clerk's `user_2…`)
496
+ // `{ksuid:N}` gives the id N characters (a vendor's own length: OpenAI's `proj_` 24, `chatcmpl-` 29), its order kept
497
+ const ksuid = /^(?:\{prefix\}([_-]?))?\{ksuid(?::(\d+))?\}$/.exec(template);
498
+ // narrower than 11 characters a KSUID no longer holds its instant and mint count, so neither order nor uniqueness
499
+ // within a second: such an id is minted otherwise ({letters:N}, {hex:N})
500
+ if (ksuid && ksuid[2] && Number(ksuid[2]) < 11)
501
+ throw new Error(`${m.service}: ${resource}'s id template ${template} is narrower than a KSUID's 11 ordered characters`);
502
+ if (ksuid)
503
+ return mintKsuid(m, resource, at, root, ksuid[1] !== undefined ? `${prefix}${ksuid[1]}` : '', snapshot, ksuid[2] ? Number(ksuid[2]) : 27);
467
504
  const [head, tail = ''] = template.split('{n}');
468
505
  const pattern = new RegExp(`^${escapeRe(String(fill(head, { prefix })))}(\\d+)${escapeRe(String(fill(tail, { prefix })))}$`);
469
- let max = 0;
506
+ let max = 0n;
470
507
  // the one type, from the index that follows writes (a scan of the whole World per create grew with it)
471
- for (const r of resourcesOfType(m.service, storedType(m, resource), root)) {
508
+ for (const r of (snapshot ? snapshot.filter((r) => r.type === storedType(m, resource)) : resourcesOfType(m.service, storedType(m, resource), root))) {
472
509
  const hit = pattern.exec(r.id);
473
- if (hit)
474
- max = Math.max(max, Number(hit[1]));
510
+ if (hit && BigInt(hit[1]) > max)
511
+ max = BigInt(hit[1]);
475
512
  }
476
- return String(fill(template, { prefix, n: max + 1 }));
513
+ return String(fill(template, { prefix, n: String(max + 1n) }));
477
514
  }
478
515
  /** The next snowflake (Discord's, X's): the milliseconds since the vendor's epoch shifted left 22 bits, the low bits an
479
516
  * increment. One space over every snowflake resource of the vendor, as its one generator's: the instant's first, or one
@@ -482,14 +519,14 @@ function mintId(m, resource, root, at) {
482
519
  /** The last snowflake minted per vendor and World in this process: two mints before the first is written (a message
483
520
  * and its attachment) must not meet. */
484
521
  const lastSnowflake = new Map();
485
- function mintSnowflake(m, epoch, root, at) {
522
+ function mintSnowflake(m, epoch, root, at, snapshot) {
486
523
  const ms = BigInt(Date.parse(at ?? '') || 0);
487
524
  const base = ms > epoch ? (ms - epoch) << 22n : 0n;
488
525
  let max = -1n;
489
526
  for (const [name, decl] of Object.entries(m.resources)) {
490
527
  if (!/^\{snowflake:\d+\}$/.test(decl.ids ?? m.ids.template))
491
528
  continue;
492
- for (const r of resourcesOfType(m.service, storedType(m, name), root))
529
+ for (const r of (snapshot ? snapshot.filter((r) => r.type === storedType(m, name)) : resourcesOfType(m.service, storedType(m, name), root)))
493
530
  if (/^\d+$/.test(r.id)) {
494
531
  const v = BigInt(r.id);
495
532
  if (v > max)
@@ -503,10 +540,21 @@ function mintSnowflake(m, epoch, root, at) {
503
540
  lastSnowflake.set(key, id);
504
541
  return String(id);
505
542
  }
543
+ /** The next KSUID for a resource (Clerk's and Svix's ids, `ksuidFrom`): the World's instant, then the type's mint
544
+ * count, so a frozen clock still lists them as made; past any the World already holds. */
545
+ function mintKsuid(m, resource, at, root, prefix = '', snapshot, width = 27) {
546
+ const held = new Set((snapshot ? snapshot.filter((r) => r.type === storedType(m, resource)) : resourcesOfType(m.service, storedType(m, resource), root)).map((r) => r.id));
547
+ const seconds = Math.floor((Date.parse(at ?? '') || 0) / 1000);
548
+ for (let n = held.size + 1;; n++) {
549
+ const id = `${prefix}${ksuidFrom(seconds, n, `${m.service}:${resource}:${n}`, width)}`;
550
+ if (!held.has(id))
551
+ return id;
552
+ }
553
+ }
506
554
  /** The next id of `count` lowercase letters for a resource, derived from the resource and the count before it, past
507
555
  * any the World already holds. */
508
- function mintLetters(m, resource, count, root, prefix = '') {
509
- const held = new Set(resourcesOfType(m.service, storedType(m, resource), root).map((r) => r.id));
556
+ function mintLetters(m, resource, count, root, prefix = '', snapshot) {
557
+ const held = new Set((snapshot ? snapshot.filter((r) => r.type === storedType(m, resource)) : resourcesOfType(m.service, storedType(m, resource), root)).map((r) => r.id));
510
558
  for (let n = held.size + 1;; n++) {
511
559
  const id = `${prefix}${lettersFrom(`${m.service}:${resource}:${n}`, count)}`;
512
560
  if (!held.has(id))
@@ -515,8 +563,8 @@ function mintLetters(m, resource, count, root, prefix = '') {
515
563
  }
516
564
  /** The next hex id for a resource: N hex digits derived from the resource and the count before it, past any the World
517
565
  * already holds (a Cloudflare id, 32). */
518
- function mintHex(m, resource, count, root, prefix = '') {
519
- const held = new Set(resourcesOfType(m.service, storedType(m, resource), root).map((r) => r.id));
566
+ function mintHex(m, resource, count, root, prefix = '', snapshot) {
567
+ const held = new Set((snapshot ? snapshot.filter((r) => r.type === storedType(m, resource)) : resourcesOfType(m.service, storedType(m, resource), root)).map((r) => r.id));
520
568
  for (let n = held.size + 1;; n++) {
521
569
  const id = `${prefix}${sha256(`${m.service}:${resource}:${n}`).slice(0, count)}`;
522
570
  if (!held.has(id))
@@ -525,19 +573,35 @@ function mintHex(m, resource, count, root, prefix = '') {
525
573
  }
526
574
  /** The next UUID for a resource: a version-4-shaped UUID derived from the resource and the count before it, past any
527
575
  * the World already holds. */
528
- function mintUuid(m, resource, root) {
576
+ function mintUuid(m, resource, root, snapshot) {
529
577
  const type = storedType(m, resource);
530
- const held = new Set(resourcesOfType(m.service, type, root).map((r) => r.id));
578
+ const held = new Set((snapshot ? snapshot.filter((r) => r.type === type) : resourcesOfType(m.service, type, root)).map((r) => r.id));
531
579
  for (let n = held.size + 1;; n++) {
532
580
  const id = uuidFrom(`${m.service}:${resource}:${n}`);
533
581
  if (!held.has(id))
534
582
  return id;
535
583
  }
536
584
  }
537
- function expand(m, resource, body, paths, root) {
585
+ function expand(m, resource, body, paths, root, within = new Set()) {
586
+ body = omitView(m, resource, body);
538
587
  const embeds = m.resources[resource]?.embeds ?? {};
588
+ // the fields the vendor always embeds join the caller's paths; a resource already being embedded above is not embedded
589
+ // again inside itself (a cycle answers the id)
590
+ const always = within.has(resource) ? [] : (m.resources[resource]?.alwaysEmbedded ?? []);
591
+ const inner = new Set([...within, resource]);
539
592
  const out = { ...body };
540
- for (const path of paths) {
593
+ const idKey = m.resources[resource]?.idAs ?? 'id';
594
+ const ownId = typeof body[idKey] === 'string' ? body[idKey] : undefined;
595
+ for (const [field, c] of within.has(resource) ? [] : Object.entries(m.resources[resource]?.collections ?? {})) {
596
+ if (!ownId)
597
+ continue;
598
+ const childPaths = paths.filter((p) => p.startsWith(`${field}.data.`)).map((p) => p.slice(field.length + 6));
599
+ const data = stored(m, c.resource, root).filter((r) => r[c.by] === ownId)
600
+ .map((r) => expand(m, c.resource, view(m, c.resource, r), childPaths, root, inner));
601
+ const fill = (v) => v === '{count}' ? data.length : typeof v === 'string' ? v.replaceAll('{id}', ownId) : v;
602
+ out[field] = { ...Object.fromEntries(Object.entries(c.list ?? {}).map(([k, v]) => [k, fill(v)])), data };
603
+ }
604
+ for (const path of [...new Set([...always, ...paths])]) {
541
605
  const [head, ...rest] = path.split('.');
542
606
  const target = embeds[head];
543
607
  const id = out[head];
@@ -545,15 +609,14 @@ function expand(m, resource, body, paths, root) {
545
609
  continue;
546
610
  // an object already embedded (an earlier path's) is walked further, not fetched again
547
611
  if (id && typeof id === 'object' && !Array.isArray(id)) {
548
- if (rest.length)
549
- out[head] = expand(m, target, id, [rest.join('.')], root);
612
+ out[head] = expand(m, target, id, rest.length ? [rest.join('.')] : [], root, inner);
550
613
  continue;
551
614
  }
552
615
  if (typeof id !== 'string')
553
616
  continue;
554
617
  const hit = stored(m, target, root).find((r) => r.id === id);
555
618
  if (hit)
556
- out[head] = rest.length ? expand(m, target, view(m, target, hit), [rest.join('.')], root) : view(m, target, hit);
619
+ out[head] = expand(m, target, view(m, target, hit), rest.length ? [rest.join('.')] : [], root, inner);
557
620
  }
558
621
  return out;
559
622
  }
@@ -561,16 +624,17 @@ function expandPaths(m, params) {
561
624
  const raw = m.expandParam ? params[m.expandParam] : undefined;
562
625
  return Array.isArray(raw) ? raw.map(String) : typeof raw === 'string' ? [raw] : [];
563
626
  }
564
- async function write(m, call, resource, id, fields, operation, params, root, occurredAt) {
565
- return (await writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt)).body;
627
+ async function write(m, call, resource, id, fields, operation, params, root, occurredAt, checks) {
628
+ return (await writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt, checks)).body;
566
629
  }
567
630
  const sentOf = new WeakMap();
568
631
  /** The lane a vendor's router sent a request to (pack-fetch's laneRouter sets it). */
569
632
  export const LANE_HEADER = 'x-volter-lane';
570
633
  function sentRequest(m, call, body) {
571
634
  const url = new URL(call.request.url);
572
- const named = [...(m.headers ?? []), ...(m.version ? [m.version.header] : [])];
573
- const headers = Object.fromEntries(named.flatMap((h) => { const v = call.request.headers.get(h); return v === null ? [] : [[h.toLowerCase(), v]]; }));
635
+ const named = [...(m.headers ?? []), ...(m.version ? [m.version.header] : [])].map((h) => h.toLowerCase());
636
+ const recorded = (h) => named.some((n) => (n.endsWith('*') ? h.startsWith(n.slice(0, -1)) : h === n));
637
+ const headers = Object.fromEntries([...call.request.headers].filter(([h]) => recorded(h.toLowerCase())).map(([h, v]) => [h.toLowerCase(), v]));
574
638
  const lane = call.request.headers.get(LANE_HEADER) ?? undefined;
575
639
  const requestId = call.request.headers.get('x-twins-request-id') ?? undefined;
576
640
  const declared = new Set((call.operation.query ?? []).map((q) => q.name));
@@ -588,6 +652,9 @@ function sentRequest(m, call, body) {
588
652
  }
589
653
  /** What a write records of the call that made it: the surface operation and the request as it came, or which of the
590
654
  * World's own doors made it (a door or a screen, the gap, the clock), which a perform never sends. */
655
+ /** An operation whose body is its payload's bytes, one `blob` field: recorded as it came and never read as JSON
656
+ * ("What the derived core records with a write"). */
657
+ const bytesPayload = (op) => op.body?.length === 1 && op.body[0].type === 'blob';
591
658
  /** A body as it came: `{ $text }` when its bytes are UTF-8, else `{ $base64 }`. */
592
659
  async function rawBody(request) {
593
660
  const bytes = new Uint8Array(await request.clone().arrayBuffer());
@@ -624,13 +691,14 @@ function recordedInput(m, call, merged) {
624
691
  return { clock: true };
625
692
  if (id === 'unmatched')
626
693
  return { gap: true, ...asSent };
627
- if ((m.doors ?? []).some((d) => d.id === id) || (m.screens ?? []).some((sc) => sc.id === id) || id === 'machine-pool' || id.startsWith('socket:'))
694
+ if ((m.doors ?? []).some((d) => d.id === id) || (m.screens ?? []).some((sc) => sc.id === id) || (m.accountSetup ?? []).some((a) => a.operation === id) || id === 'machine-pool' || id.startsWith('socket:'))
628
695
  return { door: id, ...asSent };
629
696
  return { operationId: id, ...asSent };
630
697
  }
631
698
  /** The scope each call is served in (serveCore, contextFor), for the contexts a write opens for its hooks. */
632
699
  const scopeOfCall = new WeakMap();
633
- async function writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt) {
700
+ async function writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt, checks) {
701
+ const before = stateBeforeWrite(m, resource, id, root);
634
702
  const withFiles = async (input) => { const files = input.operationId ? await keptFiles(m, call, root) : undefined; return files ? { ...input, files } : input; };
635
703
  const { resource: row, result } = await applyTwinWrite(m.service,
636
704
  // who made it: the login the pack's identity reads off the request, when it declares one, so a subject's
@@ -639,6 +707,31 @@ async function writeDetailed(m, call, resource, id, fields, operation, params, r
639
707
  inVendorMove()
640
708
  ? { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { clock: true }, occurredAt, actor: { kind: 'system' } }
641
709
  : { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) } }, root);
710
+ observeStateWrite(m, result, before, root, checks);
711
+ return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt);
712
+ }
713
+ async function atomicWrite(m, call, resource, decide, operation, params, root, occurredAt, unchangedId, checks) {
714
+ const input = recordedInput(m, call, params);
715
+ const files = input.operationId ? await keptFiles(m, call, root) : undefined;
716
+ let before;
717
+ const committed = await applyTwinWriteAtomic(m.service, (snapshot) => {
718
+ const selected = decide(snapshot);
719
+ if (selected && transitionObserver)
720
+ before = snapshot.find((r) => r.type === storedType(m, resource) && r.id === selected.id);
721
+ return selected ? { kind: 'write', value: selected.id, write: {
722
+ operation, subjectType: storedType(m, resource), subjectId: selected.id, fields: selected.fields,
723
+ input: files ? { ...input, files } : input, occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) },
724
+ } } : { kind: 'skip', value: undefined };
725
+ }, root);
726
+ if (!committed.result || !committed.value) {
727
+ const row = unchangedId ? stored(m, resource, root).find((r) => r.id === unchangedId) : undefined;
728
+ return row ? view(m, resource, row) : undefined;
729
+ }
730
+ observeStateWrite(m, committed.result, before, root, checks);
731
+ const row = resourcesOfType(m.service, storedType(m, resource), root).find((r) => r.id === resolveSubjectId(m.service, storedType(m, resource), committed.value, root));
732
+ return (await finishWrite(m, call, resource, row, committed.result, operation, params, root, occurredAt)).body;
733
+ }
734
+ async function finishWrite(m, call, resource, row, result, operation, params, root, occurredAt) {
642
735
  await keepCounts(m, call, resource, row, params, root, occurredAt);
643
736
  const body = view(m, resource, row);
644
737
  // the write's own context for its hooks, in the scope the call was served in (a walk's application answers, a tenant,
@@ -649,10 +742,12 @@ async function writeDetailed(m, call, resource, id, fields, operation, params, r
649
742
  if (m.events) {
650
743
  const events = m.events;
651
744
  const write = { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request };
652
- await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values(await context(), write, type) : undefined, (answer) => deliveryOutcome(m, answer, root));
745
+ await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values(await context(), write, type) : undefined);
653
746
  }
654
747
  if (m.sockets?.length)
655
748
  socketWrite(m.service, root, { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request });
749
+ // a read held until the World writes (held-reads.ts) hears it
750
+ heldReadsWrite(m.service, root, storedType(m, resource));
656
751
  // the subject's id as it stands after the write: the vendor's, when a live head minted one
657
752
  return { body, id: result.externalId ?? row.id, ...(result.vendorData !== undefined ? { vendorData: result.vendorData } : {}) };
658
753
  }
@@ -664,15 +759,15 @@ async function keepCounts(m, call, resource, row, params, root, occurredAt) {
664
759
  if (storedType(m, c.of) !== storedType(m, resource) || row[c.by] === undefined || row[c.by] === null)
665
760
  continue;
666
761
  const pid = String(row[c.by]);
667
- const held = stored(m, parent, root).find((r) => r.id === pid);
762
+ const held = stored(m, parent, root).find((r) => String(c.parentKey ? ownFields(r)[c.parentKey] : r.id) === pid);
668
763
  if (!held)
669
764
  continue;
670
765
  const n = stored(m, c.of, root).filter((r) => {
671
766
  const x = r;
672
- return String(x[c.by]) === pid && (c.present === undefined || (x[c.present] !== undefined && x[c.present] !== null));
767
+ return String(x[c.by]) === pid && (c.present === undefined || (x[c.present] !== undefined && x[c.present] !== null)) && Object.entries(c.unless ?? {}).every(([key, value]) => ownFields(r)[key] !== value);
673
768
  }).length + (c.plus ?? 0);
674
769
  if (held[field] !== n)
675
- await writeDetailed(m, call, parent, pid, { [field]: n }, `${storedType(m, parent)}.counted`, params, root, occurredAt);
770
+ await writeDetailed(m, call, parent, String(held.id), { [field]: n }, `${storedType(m, parent)}.counted`, params, root, occurredAt);
676
771
  }
677
772
  }
678
773
  }
@@ -685,7 +780,7 @@ function decodeOffset(cursor) {
685
780
  if (typeof cursor !== 'string' || !cursor)
686
781
  return 0;
687
782
  try {
688
- const o = JSON.parse(atob(cursor)).o;
783
+ const o = parseExactJson(atob(cursor)).o;
689
784
  return typeof o === 'number' && o >= 0 ? o : 0;
690
785
  }
691
786
  catch {
@@ -696,61 +791,68 @@ let transitionObserver;
696
791
  export function observeTransitions(observer) {
697
792
  transitionObserver = observer;
698
793
  }
794
+ const stateCheckKey = (type, field) => JSON.stringify([type, field]);
795
+ const stateValue = (row, field) => field.split('.').reduce((value, part) => value && typeof value === 'object' ? value[part] : undefined, row);
796
+ function stateBeforeWrite(m, resource, id, root) {
797
+ if (!transitionObserver)
798
+ return undefined;
799
+ const type = storedType(m, resource);
800
+ const key = resolveSubjectId(m.service, type, id, root);
801
+ return resourcesOfType(m.service, type, root).find((row) => row.id === key);
802
+ }
803
+ /** Observe the committed entry, not a legality probe's display id. Only a move the machine checks
804
+ * for this entry's actual states, operation and actor is reported; no serving depends on measurement. */
805
+ function observeStateWrite(m, result, before, root, checks) {
806
+ if (!transitionObserver)
807
+ return;
808
+ const action = listActions(m.service, root).find((a) => a.id === result.actionId);
809
+ if (!action?.fields)
810
+ return;
811
+ const prior = before ? ownFields(before) : undefined;
812
+ const next = { ...prior, ...action.fields };
813
+ const decisions = new Map();
814
+ for (const [resource, decl] of Object.entries(m.resources)) {
815
+ if ((decl.storedAs ?? resource) !== action.subject.type)
816
+ continue;
817
+ for (const [field, machine] of Object.entries(decl.state ?? {})) {
818
+ if (!(field.split('.')[0] in action.fields))
819
+ continue;
820
+ const key = stateCheckKey(action.subject.type, field);
821
+ // Aliases of one stored field use the same consumed evidence on this one entry.
822
+ if (!decisions.has(key)) {
823
+ decisions.set(key, checks?.get(key) ?? []);
824
+ checks?.delete(key);
825
+ }
826
+ if (result.status === 'replayed')
827
+ continue;
828
+ const current = prior ? stateValue(prior, field) : undefined, target = stateOf(machine, stateValue(next, field));
829
+ const from = stateOf(machine, current);
830
+ const proof = decisions.get(key).find((c) => machine.transitions.includes(c.transition) && c.from === from && c.to === target);
831
+ // Initial-state and same-state moves still need an explicit check and a committed field write.
832
+ if ((!prior || from === target) && !proof)
833
+ continue;
834
+ const actor = proof?.actor ?? (action.actor?.kind === 'system' || action.input?.clock === true ? 'vendor' : 'api');
835
+ const operation = typeof action.input?.operationId === 'string' ? action.input.operationId : proof?.operation ?? (action.input?.clock === true ? 'clock' : action.operation ?? '');
836
+ transitionFor(field, machine, operation, current, target, action.subject.id, actor, { action, field, actor });
837
+ }
838
+ }
839
+ }
699
840
  /** The transition a request asks for on one state field, or the vendor's refusal when none applies. */
700
841
  /** What a declared machine says to one move: the transition that allows it, or the refusal it gives (and
701
842
  * nothing when it declares neither). The derived core and `legal` ask it; so does an engine that is not
702
843
  * HTTP-shaped (a line protocol's session), so one machine rules every wire. */
703
- /** What a delivery's answer writes on its endpoint (the endpoint kind's `outcome`, architecture "Events and webhooks"):
704
- * a success resets the failure count and writes its fields; a failure counts, and a run of them makes the declared
705
- * move, asked of the endpoint's machine as the vendor's (a move the machine does not allow is not made). Read fresh
706
- * and written in one move, since the answer arrives after the write's own and answers may arrive together. */
707
- async function deliveryOutcome(m, answer, root) {
708
- const o = answer.kind.outcome;
709
- const type = answer.kind.storedAs;
710
- const ok = answer.status >= 200 && answer.status < 300;
711
- // the read and the count are one move under the action lock, so two answers at once count twice
712
- await runAsVendorMove(() => applyTwinWriteAtomic(m.service, (resources) => {
713
- const row = resources.find((r) => r.type === type && String(r.id) === String(answer.row.id));
714
- if (!row || row.deleted === true)
715
- return { kind: 'skip', value: undefined };
716
- const fields = {};
717
- let operation = `${type}.delivered`;
718
- if (ok) {
719
- fields[o.failures] = 0;
720
- const seconds = Math.floor((Date.parse(answer.occurredAt) || 0) / 1000);
721
- for (const [k, v] of Object.entries(o.success ?? {}))
722
- fields[k] = v === '$time.iso' ? answer.occurredAt : v === '$time.s' ? seconds : v;
723
- }
724
- else {
725
- const failures = (Number(row[o.failures]) || 0) + 1;
726
- fields[o.failures] = failures;
727
- operation = `${type}.delivery-failed`;
728
- const exempt = o.unless !== undefined && row[o.unless.field] === o.unless.equals;
729
- if (o.move && o.after !== undefined && failures >= o.after && !exempt) {
730
- const decl = Object.values(m.resources).find((r) => (r.storedAs ?? '') === type) ?? m.resources[type];
731
- const machine = decl?.state?.[o.move.field];
732
- const { move } = machine ? transitionFor(o.move.field, machine, o.move.operation, row[o.move.field], o.move.to, String(row.id), 'vendor') : {};
733
- if (move) {
734
- fields[o.move.field] = o.move.to;
735
- operation = o.move.operation;
736
- }
737
- }
738
- }
739
- return { kind: 'write', value: undefined, write: { operation, subjectType: type, subjectId: String(row.id), fields, occurredAt: answer.occurredAt, actor: { kind: 'system' } } };
740
- }, root));
741
- }
742
- export function transitionFor(field, decl, operationId, current, requested, id, actor = 'api') {
844
+ export function transitionFor(field, decl, operationId, current, requested, id, actor = 'api', entry) {
743
845
  const now = stateOf(decl, current);
744
- const candidates = decl.transitions.filter((t) => (t.actor ?? 'api') === actor && (t.operation ? t.operation === operationId : true) && (requested === undefined || (t.to ?? now) === String(requested)));
745
- const move = candidates.find((t) => t.from === '*' || t.from.includes(now));
846
+ const candidates = decl.transitions.filter((t) => (t.actor ?? 'api') === actor && (t.operation ? t.operation === operationId : true) && (requested === undefined || (t.to ?? now) === String(requested) || (t.to === undefined && t.refusal !== undefined)));
847
+ const move = candidates.find((t) => !(t.to === undefined && t.refusal !== undefined) && (t.from === '*' || t.from.includes(now)));
746
848
  if (move) {
747
- transitionObserver?.(decl, move, 'moved', now);
849
+ transitionObserver?.(decl, move, 'moved', now, entry);
748
850
  return { move };
749
851
  }
750
- const refused = candidates.find((t) => t.refusals?.[now]) ?? candidates.find((t) => t.refusal);
852
+ const refused = candidates.find((t) => t.refusals?.[now]) ?? candidates.find((t) => t.refusal && (t.to !== undefined || t.from === '*' || t.from.includes(now)));
751
853
  const r = refused?.refusals?.[now] ?? refused?.refusal;
752
854
  if (refused && r)
753
- transitionObserver?.(decl, refused, 'refused', now);
855
+ transitionObserver?.(decl, refused, 'refused', now, entry);
754
856
  if (r)
755
857
  return { refusal: { status: r.status, message: String(fill(r.message, { from: current, to: refused.to ?? now, field, id: id ?? '' })), ...(r.code ? { code: r.code } : {}) } };
756
858
  return {};
@@ -891,47 +993,52 @@ function arrangeRows(m, resource, params, input) {
891
993
  indexed.sort((a, b) => {
892
994
  const x = a.row[order.field];
893
995
  const y = b.row[order.field];
894
- const byValue = !byText || typeof x === 'number' || typeof y === 'number' ? Number(x ?? 0) - Number(y ?? 0) : String(x ?? '').localeCompare(String(y ?? ''));
895
- // ties by mint order (`file-twin-10` after `file-twin-9`), not by the ids' lexical order
896
- const d = byValue || String(a.row.id).localeCompare(String(b.row.id), undefined, { numeric: true });
996
+ const byValue = !byText || typeof x === 'number' || typeof y === 'number' ? compareExactNumbers(x ?? 0, y ?? 0) : String(x ?? '').localeCompare(String(y ?? ''));
997
+ // ties (two made in one instant) by the order they were written: the rows come in the tree's order, which keeps each
998
+ // subject where it was first written, so this is the creation order whatever the id template (an ordered KSUID or
999
+ // count, or a hash-derived uuid, hex or letters id whose text says nothing of when it was made)
1000
+ const d = byValue || a.i - b.i;
897
1001
  return order.direction === 'desc' ? -d : d;
898
1002
  });
899
1003
  return indexed.map((x) => x.row);
900
1004
  }
901
1005
  /** A page of arranged rows, answered in the manifest's list envelope with its paging (cursors, pages, offsets). */
902
1006
  function pageRows(m, op, call, params, rows, resource, root, paths) {
903
- const size = m.list.limits?.[op.id] ?? m.list.limit;
904
- const asked = params[m.list.limit.param];
1007
+ const list = m.list;
1008
+ if (!list)
1009
+ return vendorError(m, m.gap ?? m.notFound);
1010
+ const size = list.limits?.[op.id] ?? list.limit;
1011
+ const asked = params[list.limit.param];
905
1012
  const n = asked === undefined || asked === '' ? size.default : Number(asked);
906
1013
  const limit = Number.isFinite(n) ? Math.min(size.max, Math.max(1, Math.trunc(n))) : size.default;
907
- const after = m.list.after ? params[m.list.after] : undefined;
908
- const before = m.list.before ? params[m.list.before] : undefined;
1014
+ const after = list.after ? params[list.after] : undefined;
1015
+ const before = list.before ? params[list.before] : undefined;
909
1016
  let from = 0;
910
1017
  let to;
911
1018
  if (typeof before === 'string') {
912
1019
  const at = rows.findIndex((r) => r.id === before);
913
- to = at === -1 ? (m.list.unknownCursor === 'end' ? 0 : rows.length) : at;
1020
+ to = at === -1 ? (list.unknownCursor === 'end' ? 0 : rows.length) : at;
914
1021
  from = Math.max(0, to - limit);
915
1022
  }
916
1023
  else {
917
1024
  if (typeof after === 'string') {
918
1025
  const at = rows.findIndex((r) => r.id === after);
919
- from = at === -1 ? (m.list.unknownCursor === 'end' ? rows.length : 0) : at + 1;
1026
+ from = at === -1 ? (list.unknownCursor === 'end' ? rows.length : 0) : at + 1;
920
1027
  }
921
1028
  to = from + limit;
922
1029
  }
923
- const page = rows.slice(from, to).map((r) => (paths.length ? expand(m, resource, r, paths.filter((p) => p.startsWith('data.')).map((p) => p.slice(5)), root) : r));
1030
+ const page = rows.slice(from, to).map((r) => expand(m, resource, r, paths.filter((p) => p.startsWith('data.')).map((p) => p.slice(5)), root));
924
1031
  const hasMore = typeof before === 'string' ? from > 0 : to < rows.length;
925
1032
  const url = op.path.replace(/\{[^}]+\}/g, (p) => call.params[p.slice(1, -1)] ?? p);
926
- if (m.list.page) {
927
- const n = Math.max(1, Math.floor(Number(params[m.list.page.param]) || 1));
928
- const slice = rows.slice((n - 1) * limit, n * limit);
1033
+ if (list.page) {
1034
+ const n = Math.max(1, Math.floor(Number(params[list.page.param]) || 1));
1035
+ const slice = rows.slice((n - 1) * limit, n * limit).map((r) => expand(m, resource, r, paths.filter((p) => p.startsWith('data.')).map((p) => p.slice(5)), root));
929
1036
  const last = Math.max(1, Math.ceil(rows.length / limit));
930
1037
  const headers = new Headers();
931
- if (m.list.page.link && rows.length > limit) {
1038
+ if (list.page.link && rows.length > limit) {
932
1039
  const at = (k) => {
933
1040
  const u = new URL(call.request.url);
934
- u.searchParams.set(m.list.page.param, String(k));
1041
+ u.searchParams.set(list.page.param, String(k));
935
1042
  return u.toString();
936
1043
  };
937
1044
  const rels = [...(n > 1 ? [`<${at(n - 1)}>; rel="prev"`] : []), ...(n < last ? [`<${at(n + 1)}>; rel="next"`, `<${at(last)}>; rel="last"`] : []), ...(n > 1 ? [`<${at(1)}>; rel="first"`] : [])];
@@ -939,31 +1046,63 @@ function pageRows(m, op, call, params, rows, resource, root, paths) {
939
1046
  headers.set('link', rels.join(', '));
940
1047
  }
941
1048
  // `{total_count}` the count before paging, `{max_page}` the number of pages (none for an empty list: Polar's pagination)
942
- return Response.json(fill(m.list.envelope, { data: slice, next_cursor: null, key: op.answers?.key ?? 'data', total_count: rows.length, max_page: Math.ceil(rows.length / limit) }), { headers });
1049
+ return Response.json(fill(list.envelope, { data: slice, next_cursor: null, key: op.answers?.key ?? 'data', total_count: rows.length, max_page: Math.ceil(rows.length / limit) }), { headers });
943
1050
  }
944
- if (m.list.offset) {
945
- const skip = Math.max(0, Math.trunc(Number(params[m.list.offset.param]) || 0));
1051
+ if (list.offset) {
1052
+ const skip = Math.max(0, Math.trunc(Number(params[list.offset.param]) || 0));
946
1053
  const slice = rows.slice(skip, skip + limit);
947
1054
  const key = op.answers?.key ?? 'data';
948
- return Response.json({ ...(m.success ?? {}), [key]: slice, ...fill(m.list.envelopes?.[op.id] ?? m.list.envelope, { data: slice, total_count: rows.length, key }) });
1055
+ return Response.json({ ...(m.success ?? {}), [key]: slice, ...fill(list.envelopes?.[op.id] ?? list.envelope, { data: slice, total_count: rows.length, key }) });
949
1056
  }
950
- if (m.list.cursor) {
951
- const start = decodeOffset(params[m.list.cursor.param]);
1057
+ if (list.cursor) {
1058
+ if (list.cursor.encoding === 'offset-template') {
1059
+ const cursor = list.cursor;
1060
+ const names = [];
1061
+ const pattern = '^' + cursor.template.split(/(\{offset\}|\{reverse\})/).map((part) => {
1062
+ if (part === '{offset}' || part === '{reverse}') {
1063
+ names.push(part.slice(1, -1));
1064
+ return '(\\d+)';
1065
+ }
1066
+ return escapeRe(part);
1067
+ }).join('') + '$';
1068
+ if (names.filter((name) => name === 'offset').length !== 1 || names.filter((name) => name === 'reverse').length !== 1)
1069
+ throw new Error('list: offset-template must name offset and reverse once');
1070
+ const value = params[cursor.param];
1071
+ const hit = typeof value === 'string' ? new RegExp(pattern).exec(value) : undefined;
1072
+ if (value !== undefined && !hit)
1073
+ return vendorError(m, { status: 400, message: 'Invalid cursor' });
1074
+ const decoded = Object.fromEntries(names.map((name, i) => [name, Number(hit?.[i + 1] ?? 0)]));
1075
+ if (!Number.isSafeInteger(decoded.offset) || ![0, 1].includes(decoded.reverse))
1076
+ return vendorError(m, { status: 400, message: 'Invalid cursor' });
1077
+ const start = decoded.reverse ? Math.max(0, decoded.offset - limit) : decoded.offset;
1078
+ const slice = rows.slice(start, start + limit);
1079
+ const link = (offset, reverse, rel, available) => {
1080
+ const marker = cursor.template.replace('{offset}', String(offset)).replace('{reverse}', String(reverse));
1081
+ const target = new URL(call.request.url);
1082
+ target.searchParams.set(cursor.param, marker);
1083
+ return `<${target.href}>; rel="${rel}"; ${cursor.link.results}="${available}"; ${cursor.link.cursor}="${marker}"`;
1084
+ };
1085
+ return Response.json(fill(list.envelope, { data: slice, key: op.answers?.key ?? 'data' }), { headers: {
1086
+ link: [link(start, 1, cursor.link.previous, start > 0), link(start + limit, 0, cursor.link.next, start + limit < rows.length)].join(', '),
1087
+ } });
1088
+ }
1089
+ const start = decodeOffset(params[list.cursor.param]);
952
1090
  const slice = rows.slice(start, start + limit);
953
1091
  const next = start + limit < rows.length ? encodeOffset(start + limit) : '';
954
- return Response.json({ ...(m.success ?? {}), ...fill(m.list.envelope, { data: slice, next_cursor: next, key: op.answers?.key ?? 'data' }), ...(op.answers?.key ? { [op.answers.key]: slice } : {}) });
1092
+ return Response.json({ ...(m.success ?? {}), ...fill(list.envelope, { data: slice, next_cursor: next, key: op.answers?.key ?? 'data' }), ...(op.answers?.key ? { [op.answers.key]: slice } : {}) });
955
1093
  }
956
- return Response.json(fill(m.list.envelope, { data: page, has_more: hasMore, url, first_id: page[0]?.id ?? null, last_id: page.at(-1)?.id ?? null }));
1094
+ return Response.json(fill(list.envelope, { data: page, has_more: hasMore, url, first_id: page[0]?.id ?? null, last_id: page.at(-1)?.id ?? null }));
957
1095
  }
958
1096
  /** Serve one operation from the manifest, or say why the core cannot. */
959
- /** A setting's fields as the vendor answers them: its own, without the kernel's bookkeeping or the parent it is kept
960
- * under (a setting answers its values, not an identity). */
961
- function settingFields(own, parentField) {
962
- return Object.fromEntries(Object.entries(own).filter(([k]) => !k.startsWith('_') && k !== 'deleted' && k !== parentField));
1097
+ /** A setting's fields as the vendor answers them: its own, without the kernel's bookkeeping (a `_` field the resource
1098
+ * does not declare the vendor's own, as view() and render() keep it) or the parent it is kept under (a setting answers
1099
+ * its values, not an identity). */
1100
+ function settingFields(own, parentField, vendorUnderscored = []) {
1101
+ return Object.fromEntries(Object.entries(own).filter(([k]) => (!k.startsWith('_') || vendorUnderscored.includes(k)) && k !== 'deleted' && k !== parentField));
963
1102
  }
964
1103
  function settingView(m, resource, hit) {
965
1104
  // ownFields: the kernel's id, type and updatedAt gone, a vendor's own of those names restored
966
- return settingFields(ownFields(hit), m.resources[resource]?.parent?.field);
1105
+ return settingFields(ownFields(hit), m.resources[resource]?.parent?.field, m.resources[resource]?.vendorUnderscored);
967
1106
  }
968
1107
  export async function serveCore(m, call, scope = {}) {
969
1108
  const root = scope.root;
@@ -994,9 +1133,9 @@ export async function serveCore(m, call, scope = {}) {
994
1133
  // the vendor's success status from its spec: 201 for most creates, 204 with no body where it answers nothing
995
1134
  const status = op.successStatus ?? 200;
996
1135
  const answer = (body) => ({
997
- served: status === 204 ? new Response(null, { status }) : Response.json(envelope(m, op, paths.length ? expand(m, resource, body, paths, root) : body), { status }),
1136
+ served: status === 204 ? new Response(null, { status }) : Response.json(envelope(m, op, expand(m, resource, body, paths, root)), { status }),
998
1137
  });
999
- const control = new Set([m.expandParam, m.list.limit.param, m.list.after, m.list.before, m.list.offset?.param, m.list.page?.param, m.list.cursor?.param].filter(Boolean));
1138
+ const control = new Set([m.expandParam, m.list?.limit.param, m.list?.after, m.list?.before, m.list?.offset?.param, m.list?.page?.param, m.list?.cursor?.param].filter(Boolean));
1000
1139
  const data = Object.fromEntries(Object.entries(params).filter(([k]) => !control.has(k) && k !== 'id'));
1001
1140
  // a strict vendor refuses a body it does not declare before the core writes it (body.strict, as ctx.fields)
1002
1141
  if (m.body.strict && (op.class === 'create' || op.class === 'update')) {
@@ -1019,7 +1158,8 @@ export async function serveCore(m, call, scope = {}) {
1019
1158
  const under = { ...(parent ? { [parent.field]: parentId } : {}), ...(tenantField ? { [tenantField]: tenant } : {}) };
1020
1159
  if (decl.number) {
1021
1160
  const siblings = stored(m, resource, root, { withDeleted: true }).filter(underParent);
1022
- under[decl.number.field] = 1 + siblings.reduce((n, r) => Math.max(n, Number(r[decl.number.field]) || 0), 0);
1161
+ const next = siblings.reduce((n, r) => { const value = String(r[decl.number.field] ?? '0'); const held = /^-?\d+$/.test(value) ? BigInt(value) : 0n; return held > n ? held : n; }, 0n) + 1n;
1162
+ under[decl.number.field] = exactInteger(String(next));
1023
1163
  }
1024
1164
  const made = { ...assigned, ...initial, ...under, ...data };
1025
1165
  const clash = conflictOf(m, resource, made, undefined, root);
@@ -1035,6 +1175,8 @@ export async function serveCore(m, call, scope = {}) {
1035
1175
  return hit ? answer(view(m, resource, hit)) : { served: notFound(m, resource, String(idParam), Object.keys(call.params).at(-1)) };
1036
1176
  }
1037
1177
  case 'list': {
1178
+ if (!m.list)
1179
+ return { unmodeled: 'no manifest list configuration' };
1038
1180
  const rows = stored(m, resource, root).filter(underParent).map((r) => view(m, resource, r));
1039
1181
  const arranged = arrangeRows(m, resource, params, rows);
1040
1182
  if (!Array.isArray(arranged))
@@ -1063,6 +1205,7 @@ export async function serveCore(m, call, scope = {}) {
1063
1205
  const current = view(m, resource, hit);
1064
1206
  const changes = { ...(op.class === 'update' ? data : {}) };
1065
1207
  let moved = false;
1208
+ const stateChecks = new Map();
1066
1209
  for (const [field, sdecl] of Object.entries(decl.state ?? {})) {
1067
1210
  const requested = op.class === 'update' ? data[field] : undefined;
1068
1211
  if (op.class === 'update' && requested === undefined)
@@ -1075,6 +1218,8 @@ export async function serveCore(m, call, scope = {}) {
1075
1218
  return { unmodeled: `no declared transition of ${field} to ${String(requested)} by ${op.id}` };
1076
1219
  continue;
1077
1220
  }
1221
+ if (transitionObserver)
1222
+ stateChecks.set(stateCheckKey(storedType(m, resource), field), [{ transition: move, operation: op.id, from: stateOf(sdecl, current[field]), to: move.to ?? stateOf(sdecl, current[field]), actor: 'api' }]);
1078
1223
  const stored = storedState(sdecl, move.to);
1079
1224
  if (stored !== undefined)
1080
1225
  changes[field] = stored;
@@ -1090,7 +1235,7 @@ export async function serveCore(m, call, scope = {}) {
1090
1235
  const clash = conflictOf(m, resource, { ...current, ...merged }, hit.id, root);
1091
1236
  if (clash)
1092
1237
  return { served: vendorError(m, clash) };
1093
- return answer(await write(m, call, resource, hit.id, merged, `${storedType(m, resource)}.${op.class === 'update' ? 'update' : op.id}`, params, root, at));
1238
+ return answer(await write(m, call, resource, hit.id, merged, `${storedType(m, resource)}.${op.class === 'update' ? 'update' : op.id}`, params, root, at, stateChecks));
1094
1239
  }
1095
1240
  case 'delete': {
1096
1241
  const hit = idParam ? stored(m, resource, root).find((r) => namedBy(decl, r, idParam) && underParent(r)) : undefined;
@@ -1132,20 +1277,26 @@ export async function contextFor(m, call, scope) {
1132
1277
  scopeOfCall.set(call, scope);
1133
1278
  const at = (scope.clock ?? worldNow)();
1134
1279
  // the guard's parse when it made one (taken once), else this context's own
1135
- const parsed = takeParsedBody(call.request);
1136
- const text = parsed ? '' : await bodyTextOf(call.request.clone()).catch(() => '');
1280
+ // a body that is its payload's bytes (S3's PutObject) is never read as JSON, whatever its label
1281
+ const payload = bytesPayload(call.operation);
1282
+ const parsed = payload ? undefined : takeParsedBody(call.request);
1283
+ const text = await bodyTextOf(call.request.clone()).catch(() => '');
1137
1284
  let body = parsed?.value;
1138
1285
  if (!parsed) {
1139
- try {
1140
- body = text ? JSON.parse(text) : undefined;
1141
- }
1142
- catch {
1143
- body = text;
1286
+ if (payload)
1287
+ body = text || undefined;
1288
+ else {
1289
+ try {
1290
+ body = text ? parseExactJson(text) : undefined;
1291
+ }
1292
+ catch {
1293
+ body = text;
1294
+ }
1144
1295
  }
1145
1296
  }
1146
1297
  // the request as it came, before the core's parameter merge and alias adoption: what a write records, so a perform
1147
1298
  // can send it again (the real-system adapters, "What the derived core records with a write")
1148
- const boundary = multipartBoundary(call.request.headers.get('content-type'));
1299
+ const boundary = payload ? null : multipartBoundary(call.request.headers.get('content-type'));
1149
1300
  const parts = boundary === null ? undefined : multipartParts(new Uint8Array(await call.request.clone().arrayBuffer()), boundary);
1150
1301
  // a body that is not JSON is recorded as it came, its type with it: text as text, bytes (a gzip the vendor's client
1151
1302
  // compressed itself, PostHog's `compression=gzip-js`) as base64
@@ -1157,8 +1308,21 @@ export async function contextFor(m, call, scope) {
1157
1308
  // every read of the body is of a copy, so the handler's own `call.request` and the core's answer (`core()`) each
1158
1309
  // still find it unread, whichever of them reads it first
1159
1310
  const pristine = call.request.clone();
1160
- const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root);
1311
+ const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root).catch((error) => {
1312
+ // An around context opens before the credential/JSON guard. Keep non-body parameters available to it without
1313
+ // turning the guard's declared malformed-body refusal into a context-construction exception.
1314
+ if (!(error instanceof SyntaxError) || error.message !== 'malformed JSON body')
1315
+ throw error;
1316
+ return boundaryParams(m, { ...call, request: new Request(call.request.url, { headers: call.request.headers }) }, root);
1317
+ });
1161
1318
  const paths = expandPaths(m, params);
1319
+ const stateChecks = new Map();
1320
+ const rawResources = new WeakMap();
1321
+ const rawRow = (resource, row) => {
1322
+ const name = m.resources[resource] ? resource : Object.keys(m.resources).find((name) => storedType(m, name) === row.type);
1323
+ rawResources.set(row, name ?? resource);
1324
+ return row;
1325
+ };
1162
1326
  return {
1163
1327
  call,
1164
1328
  params,
@@ -1168,7 +1332,6 @@ export async function contextFor(m, call, scope) {
1168
1332
  root,
1169
1333
  publicBase: twinPublicBase(call.request),
1170
1334
  scenario: scenarioDecisionOf(call.request),
1171
- deliver: (url, init) => deliverToApplication(url, init),
1172
1335
  ask: (url, init, within) => askApplication(url, init, within, scope.application),
1173
1336
  worldEnv: (name) => worldEnvValue(name),
1174
1337
  asVendor: (fn) => runAsVendorMove(fn),
@@ -1176,11 +1339,19 @@ export async function contextFor(m, call, scope) {
1176
1339
  if (!/^[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.-]+)*$/.test(name) || name.split('/').some((seg) => seg === '.' || seg === '..'))
1177
1340
  throw new Error(`semantics: ${name} is not a git repository name`);
1178
1341
  const plane = `${worldPaths(m.service, root).dir}/git`;
1179
- return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/objects`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
1342
+ return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
1180
1343
  },
1181
1344
  mail: (route, mail, headers) => sendMail(route, mail, headers),
1182
1345
  actor: m.identity ? m.identity(call.request.headers.get('authorization'), root) : undefined,
1183
1346
  now: () => now(m, at),
1347
+ render: (row, resource) => {
1348
+ const raw = rawResources.has(row);
1349
+ const name = resource ?? rawResources.get(row);
1350
+ const declared = name && (m.resources[name] ? name : Object.keys(m.resources).find((r) => storedType(m, r) === name));
1351
+ const keep = declared ? m.resources[declared]?.vendorUnderscored ?? [] : [];
1352
+ const shown = raw ? render(row, keep) : renderOwn(row, keep);
1353
+ return omitView(m, declared || undefined, shown);
1354
+ },
1184
1355
  get: (resource, id) => {
1185
1356
  const hit = stored(m, resource, root).find((r) => r.id === id);
1186
1357
  return hit ? view(m, resource, hit) : undefined;
@@ -1194,49 +1365,86 @@ export async function contextFor(m, call, scope) {
1194
1365
  rows: (resource) => stored(m, resource, root).map((r) => view(m, resource, r)),
1195
1366
  mint: (resource) => mintId(m, resource, root, at),
1196
1367
  sockets: (id) => openSessions(m.service, root, id),
1368
+ awaitWrite: (resources, ms) => (scope.readOnly === true || isReadOnlyRequest(call.request) ? Promise.resolve(false) : awaitWrite(m.service, root, resources.map((r) => storedType(m, r)), ms, call.request.signal)),
1369
+ awaitCondition: (resources, ms, pollMs, condition) => awaitCondition(m.service, root, resources.map((r) => storedType(m, r)), scope.readOnly === true || isReadOnlyRequest(call.request) ? 0 : ms, pollMs, condition, call.request.signal),
1197
1370
  issue: async (resource) => {
1198
- const id = mintId(m, resource, root, at);
1199
1371
  if (!resource.startsWith('_'))
1200
1372
  throw new Error(`semantics: ${resource} is not a bookkeeping type (an issued id's type starts with _)`);
1201
- if (scope.readOnly !== true && !isReadOnlyRequest(call.request)) {
1202
- await applyTwinWrite(m.service, { operation: `${resource.slice(1)}.issue`, subjectType: storedType(m, resource), subjectId: id, fields: { issuedAt: at }, occurredAt: at, actor: { kind: 'system' } }, root);
1203
- }
1204
- return id;
1373
+ if (scope.readOnly === true || isReadOnlyRequest(call.request))
1374
+ return mintId(m, resource, root, at);
1375
+ const { value } = await applyTwinWriteAtomic(m.service, (snapshot) => {
1376
+ const id = mintId(m, resource, root, at, snapshot);
1377
+ return { kind: 'write', value: id, write: { operation: `${resource.slice(1)}.issue`, subjectType: storedType(m, resource), subjectId: id, fields: { issuedAt: at }, occurredAt: at, actor: { kind: 'system' } } };
1378
+ }, root);
1379
+ return value;
1205
1380
  },
1206
1381
  occurredAt: at,
1207
1382
  secret: async (label) => hmac(await worldSeed(m.service, root, at), label, 'base64url'),
1208
- record: async (type, fields, id) => {
1383
+ record: async (type, fields, id, operation) => {
1209
1384
  if (!type.startsWith('_'))
1210
1385
  throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
1211
1386
  if (id !== undefined) {
1212
- await applyTwinWrite(m.service, { operation: `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, occurredAt: at, actor: { kind: 'system' } }, root);
1387
+ const before = operation ? stateBeforeWrite(m, type, id, root) : undefined;
1388
+ const { result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, occurredAt: at, actor: { kind: 'system' } }, root);
1389
+ if (operation)
1390
+ observeStateWrite(m, result, before, root, stateChecks);
1213
1391
  return id;
1214
1392
  }
1215
- const { value } = await applyTwinWriteAtomic(m.service, (resources) => {
1393
+ const { value, result } = await applyTwinWriteAtomic(m.service, (resources) => {
1216
1394
  // Allocate and append under the same state lock: concurrent sign-ins must never share an issue id.
1217
1395
  const subject = (() => {
1218
1396
  const prefix = `${type.slice(1)}_`;
1219
- let max = 0;
1397
+ let max = 0n;
1220
1398
  for (const r of resources) {
1221
1399
  if (r.type !== type)
1222
1400
  continue;
1223
- const n = r.id.startsWith(prefix) ? Number(r.id.slice(prefix.length)) : NaN;
1224
- if (Number.isInteger(n) && n > max)
1225
- max = n;
1401
+ const digits = r.id.startsWith(prefix) ? r.id.slice(prefix.length) : '';
1402
+ if (/^\d+$/.test(digits) && BigInt(digits) > max)
1403
+ max = BigInt(digits);
1226
1404
  }
1227
- return `${prefix}${max + 1}`;
1405
+ return `${prefix}${max + 1n}`;
1228
1406
  })();
1229
- return { kind: 'write', value: subject, write: { operation: `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, occurredAt: at, actor: { kind: 'system' } } };
1407
+ return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, occurredAt: at, actor: { kind: 'system' } } };
1230
1408
  }, root, () => resourcesOfType(m.service, type, root));
1409
+ if (operation && result)
1410
+ observeStateWrite(m, result, undefined, root, stateChecks);
1231
1411
  return value;
1232
1412
  },
1233
1413
  get machines() {
1234
1414
  const enrolled = resourcesOfType(m.service, MACHINE_POOL, root).find((r) => r.id === 'pool');
1235
- return poolFor(m.service, root, enrolled?.kind ?? 'none');
1415
+ return guardedMachinePool(poolFor(m.service, root, enrolled?.kind ?? 'none', enrolled?.httpReplies, { images: enrolled?.images }), m.service, ['retrieve', 'list'].includes(call.operation.class) || (m.reads?.includes(call.operation.id) ?? false));
1236
1416
  },
1237
1417
  signingKey: (label, alg) => worldSigningKey(m.service, root, at, label, alg ?? 'RS256'),
1238
1418
  heldSigningKey: (label, alg) => heldWorldSigningKey(m.service, root, label, alg ?? 'RS256'),
1239
- write: (resource, id, fields, operation) => write(m, call, resource, id, fields, operation, params, root, at),
1419
+ create: (resource, fields, operation) => atomicWrite(m, call, resource, (snapshot) => {
1420
+ const id = mintId(m, resource, root, at, snapshot);
1421
+ return { id, fields: typeof fields === 'function' ? fields(id) : fields };
1422
+ }, operation, params, root, at, undefined, stateChecks).then((row) => row),
1423
+ createUnique: async (resource, fields, operation) => {
1424
+ const rules = m.resources[resource]?.unique;
1425
+ if (!rules?.length)
1426
+ throw new Error('createUnique requires declared unique keys');
1427
+ let existing;
1428
+ const row = await atomicWrite(m, call, resource, (snapshot) => {
1429
+ const id = mintId(m, resource, root, at, snapshot);
1430
+ const selected = typeof fields === 'function' ? fields(id) : fields;
1431
+ const hit = snapshot.find((r) => r.type === storedType(m, resource) && r.deleted !== true && rules.some((rule) => rule.fields.every((f) => selected[f] !== undefined && view(m, resource, r)[f] === selected[f])));
1432
+ if (hit) {
1433
+ existing = view(m, resource, hit);
1434
+ return undefined;
1435
+ }
1436
+ return { id, fields: selected };
1437
+ }, operation, params, root, at, undefined, stateChecks);
1438
+ return existing ? { created: false, row: existing } : { created: true, row: row };
1439
+ },
1440
+ change: (resource, id, decide, operation) => atomicWrite(m, call, resource, (snapshot) => {
1441
+ const row = snapshot.find((r) => r.type === storedType(m, resource) && r.id === id && r.deleted !== true);
1442
+ if (!row)
1443
+ return undefined;
1444
+ const fields = decide(row);
1445
+ return fields === undefined ? undefined : { id, fields };
1446
+ }, operation, params, root, at, id, stateChecks),
1447
+ write: (resource, id, fields, operation) => write(m, call, resource, id, fields, operation, params, root, at, stateChecks),
1240
1448
  accumulate: async (resource, id, deltas, operation) => {
1241
1449
  if (!resource.startsWith('_') || !m.resources[resource])
1242
1450
  throw new Error('accumulate requires a declared bookkeeping resource');
@@ -1254,24 +1462,29 @@ export async function contextFor(m, call, scope) {
1254
1462
  return { kind: 'write', value: undefined, write: { operation, subjectType: storedType(m, resource), subjectId: id, fields, occurredAt: at, actor: { kind: 'agent' } } };
1255
1463
  }, root);
1256
1464
  },
1257
- writeDetailed: (resource, id, fields, operation) => writeDetailed(m, call, resource, id, fields, operation, params, root, at),
1465
+ writeDetailed: (resource, id, fields, operation) => writeDetailed(m, call, resource, id, fields, operation, params, root, at, stateChecks),
1258
1466
  resolve: (resource, id) => resolveSubjectId(m.service, storedType(m, resource), id, root),
1259
1467
  storedType: (resource) => storedType(m, resource),
1260
1468
  ok: (fields) => Response.json({ ...(m.success ?? {}), ...fields }),
1261
- row: (resource, id, opts) => stored(m, resource, root, opts).find((r) => r.id === id),
1262
- rowsRaw: (resource, opts) => stored(m, resource, root, opts),
1469
+ row: (resource, id, opts) => { const row = stored(m, resource, root, opts).find((r) => r.id === id); return row ? rawRow(resource, row) : undefined; },
1470
+ rowsRaw: (resource, opts) => stored(m, resource, root, opts).map((row) => rawRow(resource, row)),
1263
1471
  tree: () => twinResources(m.service, root),
1264
1472
  history: (resource, id) => subjectHistory(m.service, { type: storedType(m, resource), id }, root).map((e) => ({ ...(e.operation ? { operation: e.operation } : {}), ...(e.fields ? { fields: e.fields } : {}), ...(e.occurredAt ? { occurredAt: e.occurredAt } : {}) })),
1265
1473
  raw: (body, init = {}) => new Response(body, { status: init.status ?? 200, headers: init.headers ?? {} }),
1266
1474
  atomically: async (decide) => {
1267
- const { value } = await applyTwinWriteAtomic(m.service, (resources) => {
1475
+ let before;
1476
+ const { value, result } = await applyTwinWriteAtomic(m.service, (resources) => {
1268
1477
  const rows = (resource) => resources.filter((r) => r.type === storedType(m, resource) && r.deleted !== true);
1269
1478
  const d = decide(rows);
1270
1479
  if (!d.write)
1271
1480
  return { kind: 'skip', value: d.value };
1272
1481
  const w = d.write;
1482
+ if (transitionObserver)
1483
+ before = resources.find((r) => r.type === storedType(m, w.resource) && r.id === w.id);
1273
1484
  return { kind: 'write', value: d.value, write: { operation: w.operation, subjectType: storedType(m, w.resource), subjectId: w.id, fields: w.fields, occurredAt: at, actor: { kind: 'agent' } } };
1274
1485
  }, root);
1486
+ if (result)
1487
+ observeStateWrite(m, result, before, root, stateChecks);
1275
1488
  return value;
1276
1489
  },
1277
1490
  legal: (resource, field, operationId, current, to, id, actor) => {
@@ -1279,29 +1492,35 @@ export async function contextFor(m, call, scope) {
1279
1492
  if (!decl)
1280
1493
  throw new Error(`semantics: ${resource}.${field} is not a declared state field`);
1281
1494
  const { move, refusal } = transitionFor(field, decl, operationId, current, to, id ?? Object.values(call.params).at(-1), actor ?? 'api');
1282
- if (move)
1495
+ if (move) {
1496
+ if (transitionObserver) {
1497
+ const key = stateCheckKey(storedType(m, resource), field);
1498
+ stateChecks.set(key, [...(stateChecks.get(key) ?? []), { transition: move, operation: operationId, from: stateOf(decl, current), to: move.to ?? stateOf(decl, current), actor: actor ?? 'api' }]);
1499
+ }
1283
1500
  return undefined;
1501
+ }
1284
1502
  if (refusal)
1285
1503
  return refusal;
1286
1504
  throw new Error(`semantics: ${operationId} moves ${resource}.${field} from ${String(current)}${to ? ` to ${to}` : ''}, which the machine does not declare`);
1287
1505
  },
1288
1506
  refuse: (e) => vendorError(m, e),
1507
+ gap: () => vendorError(m, m.gap ?? m.notFound),
1289
1508
  notFound: (resource, id, param) => notFound(m, resource, id, param),
1290
1509
  validate: () => validateBody(m, call.operation, body),
1291
1510
  reply: (body, status = 200) => Response.json(body, { status }),
1292
1511
  wrap: (body, extra = {}) => Response.json({ ...envelope(m, call.operation, body), ...extra }),
1293
1512
  sse: (events) => sse(m, events, call.operation.id),
1294
- expand: (resource, body, given) => ((given ?? paths).length ? expand(m, resource, body, given ?? paths, root) : body),
1513
+ expand: (resource, body, given) => expand(m, resource, body, given ?? paths, root),
1295
1514
  at: (when) => contextFor(m, { ...call, request: new Request(call.request.url) }, { ...scope, clock: () => when }),
1296
1515
  crypto: handlerCrypto,
1297
1516
  flag: (value) => flagOf(m, value),
1298
1517
  fields: (body) => strictFields(m, call.operation, body),
1299
- vendorFetch: (url, init) => vendorFetch(url, init),
1518
+ vendorFetch: (url, init) => vendorFetch(url, init, scope.application),
1300
1519
  parts: async () => {
1301
1520
  const boundary = multipartBoundary(call.request.headers.get('content-type'));
1302
1521
  return boundary === null ? [] : multipartParts(new Uint8Array(await call.request.clone().arrayBuffer()), boundary);
1303
1522
  },
1304
- engine: scope.database === undefined ? unboundDatabase(m.service) : managedDatabase(scope.database, { readOnly: scope.readOnly === true || isReadOnlyRequest(call.request), clock: at }),
1523
+ engine: scope.database === undefined ? unboundDatabase(m.service) : managedDatabase(scope.database, { readOnly: scope.readOnly === true || isReadOnlyRequest(call.request) }),
1305
1524
  redis: (commands, options) => execRedisRun(commands, {
1306
1525
  root, occurredAt: at, service: m.service, readOnly: scope.readOnly === true || isReadOnlyRequest(call.request), dialect: options.dialect,
1307
1526
  ...(options.database === undefined ? {} : { database: options.database }),
@@ -1333,14 +1552,16 @@ export async function contextFor(m, call, scope) {
1333
1552
  }
1334
1553
  /** A pack's semantics handlers as the dispatch's handlers. */
1335
1554
  export function bindSemantics(m, handlers, scope = {}) {
1555
+ prepareViewOmissions(m);
1336
1556
  return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call) => h(await contextFor(m, call, scope))]));
1337
1557
  }
1338
1558
  /** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
1339
1559
  export function coreFor(m, scope = {}) {
1560
+ prepareViewOmissions(m);
1340
1561
  const crud = new Set(['create', 'retrieve', 'list', 'update', 'delete']);
1341
1562
  const owns = (o) => {
1342
1563
  const decl = o.resource !== undefined ? m.resources[o.resource] : undefined;
1343
- if (!decl || m.unmodeled?.includes(o.id))
1564
+ if (!decl || m.unmodeled?.includes(o.id) || (o.class === 'list' && !m.list))
1344
1565
  return false;
1345
1566
  if (crud.has(o.class))
1346
1567
  return true;
@@ -1363,7 +1584,7 @@ function scenarioDecisionOf(request) {
1363
1584
  if (!raw)
1364
1585
  return undefined;
1365
1586
  try {
1366
- return JSON.parse(raw);
1587
+ return parseExactJson(raw);
1367
1588
  }
1368
1589
  catch {
1369
1590
  return undefined;
@@ -1375,6 +1596,10 @@ export function authRefusal(m, request, root) {
1375
1596
  const url = new URL(request.url);
1376
1597
  if (m.auth.paths !== undefined && !new RegExp(m.auth.paths).test(url.pathname))
1377
1598
  return undefined;
1599
+ const wire = m.auth.wires?.find((w) => new RegExp(w.paths).test(url.pathname));
1600
+ const errorManifest = wire ? { ...m, error: wire.error } : m;
1601
+ const invalid = wire?.invalid ?? m.auth.invalid;
1602
+ const missing = wire?.missing ?? m.auth.missing;
1378
1603
  const sources = [{ header: m.auth.header, scheme: m.auth.scheme }, ...(m.auth.also ?? [])];
1379
1604
  const given = sources.map((s) => (s.query !== undefined ? url.searchParams.get(s.query) : s.header ? request.headers.get(s.header) : null)).some((v) => v !== null);
1380
1605
  if (!given && !m.auth.gateWhenAbsent)
@@ -1393,18 +1618,50 @@ export function authRefusal(m, request, root) {
1393
1618
  const key = sources.map(keyOf).find((k) => k !== '') ?? '';
1394
1619
  // no credential sent is `missing`; one sent in a form the vendor does not read is a key it does not know
1395
1620
  if (!key)
1396
- return vendorError(m, given ? m.auth.invalid : m.auth.missing);
1621
+ return vendorError(errorManifest, given ? invalid : missing);
1397
1622
  if (m.auth.invalidKeys.includes(key) || (m.auth.keyFormat && !new RegExp(m.auth.keyFormat).test(key)))
1398
- return vendorError(m, m.auth.invalid);
1623
+ return vendorError(errorManifest, invalid);
1399
1624
  const held = m.auth.held;
1400
1625
  if (held && !held.standing?.includes(key)) {
1401
1626
  const hash = sha256(key);
1402
1627
  const holds = resourcesOfType(m.service, held.storedAs, root).some((r) => r.deleted !== true && r[held.hashField] === hash && (held.unless === undefined || r[held.unless] !== true));
1403
1628
  if (!holds)
1404
- return vendorError(m, m.auth.invalid);
1629
+ return vendorError(errorManifest, invalid);
1405
1630
  }
1406
1631
  return undefined;
1407
1632
  }
1633
+ /** The number of answers a World has given that carried a filled answer header (`answerHeaders`'s `{hex:N}`), kept in
1634
+ * the World's own directory beside its rate-budget ledger (bookkeeping, never a log entry): a fresh World counts from 1,
1635
+ * so two Worlds made alike answer the same values in the same order, and a resumed World counts on, never answering a
1636
+ * value the count holds again. A World with no root (an in-memory walk) counts in this process. */
1637
+ const answeredInMemory = new Map();
1638
+ function nextAnswer(service, root) {
1639
+ const key = `${service}:${root ?? ''}`;
1640
+ if (root === undefined) {
1641
+ const n = (answeredInMemory.get(key) ?? 0) + 1;
1642
+ answeredInMemory.set(key, n);
1643
+ return n;
1644
+ }
1645
+ const store = getActiveWorldStore();
1646
+ const path = `${worldPaths(service, root).dir}/answer-count`;
1647
+ // a read-only request writes nothing, bookkeeping and its lock included, and reads the count without the lock: it
1648
+ // counts on in this process past the kept count, so after a restart, or from another process serving the same World,
1649
+ // the answers given to read-only requests since the last write may be given again
1650
+ if (writesRefused()) {
1651
+ const kept = Number(store.read(path) ?? '0') || 0;
1652
+ const extra = (answeredInMemory.get(key) ?? 0) + 1;
1653
+ answeredInMemory.set(key, extra);
1654
+ return kept + extra;
1655
+ }
1656
+ // read and written under the count's lock, as the rate-budget ledger is: two writes from processes serving one World never
1657
+ // take one number
1658
+ return store.withLock(`${path}.lock`, () => {
1659
+ const n = (Number(store.read(path) ?? '0') || 0) + (answeredInMemory.get(key) ?? 0) + 1;
1660
+ answeredInMemory.delete(key);
1661
+ store.writeAtomic(path, String(n));
1662
+ return n;
1663
+ });
1664
+ }
1408
1665
  export function crossCutting(m, opts = {}) {
1409
1666
  const finish = async (r, request) => {
1410
1667
  if (!m.origin || !(r.headers.get('content-type') ?? '').includes('json'))
@@ -1416,9 +1673,18 @@ export function crossCutting(m, opts = {}) {
1416
1673
  if (!m.answerHeaders)
1417
1674
  return r;
1418
1675
  const headers = new Headers(r.headers);
1676
+ const world = `${m.service}:${opts.root ?? ''}`;
1677
+ let seed;
1678
+ const filled = (v) => v.replace(/\{hex:(\d+)\}/g, (_, w) => {
1679
+ seed ??= `${world}:answer:${nextAnswer(m.service, opts.root)}`;
1680
+ let hex = '';
1681
+ for (let i = 0; hex.length < Number(w); i++)
1682
+ hex += sha256(`${seed}:${i}`);
1683
+ return hex.slice(0, Number(w));
1684
+ });
1419
1685
  for (const [k, v] of Object.entries(m.answerHeaders))
1420
1686
  if (!headers.has(k))
1421
- headers.set(k, v);
1687
+ headers.set(k, filled(v));
1422
1688
  return new Response(r.body, { status: r.status, statusText: r.statusText, headers });
1423
1689
  };
1424
1690
  return async (call, next) => finish(await guarded(call, next), call.request);
@@ -1438,7 +1704,7 @@ export function crossCutting(m, opts = {}) {
1438
1704
  // multipart or form: Cloudflare's asset and script uploads)
1439
1705
  const declared = call.operation.bodyEncoding;
1440
1706
  const labelled = request.headers.get('content-type') ?? '';
1441
- if ((labelled.includes('json') && !labelled.includes('ndjson')) || (m.body.json === 'always' && declared !== 'multipart' && declared !== 'form')) {
1707
+ if (!bytesPayload(call.operation) && ((labelled.includes('json') && !labelled.includes('ndjson') && !labelled.includes('connect+')) || (m.body.json === 'always' && declared !== 'multipart' && declared !== 'form'))) {
1442
1708
  const ok = request.method === 'GET' || request.method === 'HEAD' ? true : await checkJsonBody(request.clone()).catch(() => true);
1443
1709
  if (!ok)
1444
1710
  return vendorError(m, m.malformedBody ?? { status: 400, message: 'The request body could not be parsed as JSON.' });
@@ -1479,8 +1745,8 @@ export function crossCutting(m, opts = {}) {
1479
1745
  * coming (another wire's mutation, a door, a screen) — and answered with the vendor's own read-only
1480
1746
  * error; `GET /twin` advertises `requestScopes`. The pack's catch-up runs under `runAsVendorMove`
1481
1747
  * (request-scope.ts), so time's moves still land when a read-only request is what arrives. */
1482
- export function derivedRequestScopes(m, fetch) {
1483
- return withRequestScopes(fetch, { refuse: () => vendorError(m, m.readOnly) });
1748
+ export function derivedRequestScopes(m, fetch, readOnly = false) {
1749
+ return withRequestScopes(fetch, { readOnly, refuse: () => vendorError(m, m.readOnly) });
1484
1750
  }
1485
1751
  /** A semantics context for a request no surface operation names: another wire's (GraphQL) resolvers
1486
1752
  * get the same interface as a handler, named by the operation id the wire gives. */