@volter/world-core 3.0.37 → 3.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/app-route.cjs +26 -25
  2. package/app-route.d.cts +1 -0
  3. package/dist/app-route.cjs +26 -25
  4. package/dist/app-route.d.cts +1 -0
  5. package/dist/generated/pack-facts.json +409 -352
  6. package/dist/inject.cjs +474 -95
  7. package/dist/network-policy.cjs +108 -13
  8. package/dist/network-policy.d.cts +8 -3
  9. package/dist/src/actions.js +2 -1
  10. package/dist/src/anthropic-wire.d.ts +2 -0
  11. package/dist/src/anthropic-wire.js +5 -0
  12. package/dist/src/attestation.d.ts +6 -0
  13. package/dist/src/attestation.js +27 -0
  14. package/dist/src/bytes.d.ts +3 -0
  15. package/dist/src/bytes.js +17 -0
  16. package/dist/src/changeset.js +2 -1
  17. package/dist/src/clickhouse/sql.js +73 -14
  18. package/dist/src/derived-core.d.ts +210 -32
  19. package/dist/src/derived-core.js +510 -244
  20. package/dist/src/derived-real.d.ts +1 -1
  21. package/dist/src/derived-real.js +704 -93
  22. package/dist/src/derived.d.ts +18 -2
  23. package/dist/src/derived.js +21 -2
  24. package/dist/src/events.d.ts +28 -45
  25. package/dist/src/events.js +40 -58
  26. package/dist/src/exact-json.d.ts +10 -0
  27. package/dist/src/exact-json.js +98 -0
  28. package/dist/src/executor.d.ts +17 -0
  29. package/dist/src/executor.js +91 -30
  30. package/dist/src/file-response.js +1 -1
  31. package/dist/src/form.d.ts +8 -0
  32. package/dist/src/form.js +76 -0
  33. package/dist/src/git/format.d.ts +16 -0
  34. package/dist/src/git/format.js +110 -0
  35. package/dist/src/git/index.d.ts +1 -0
  36. package/dist/src/git/index.js +1 -0
  37. package/dist/src/git/lfs.d.ts +1 -1
  38. package/dist/src/git/lfs.js +4 -3
  39. package/dist/src/graphql-wire.d.ts +6 -0
  40. package/dist/src/graphql-wire.js +6 -3
  41. package/dist/src/grpc-wire.js +2 -1
  42. package/dist/src/head.d.ts +1 -0
  43. package/dist/src/head.js +15 -4
  44. package/dist/src/held-reads.d.ts +6 -0
  45. package/dist/src/held-reads.js +60 -0
  46. package/dist/src/history.js +5 -4
  47. package/dist/src/host-port.d.ts +5 -0
  48. package/dist/src/host-port.js +14 -0
  49. package/dist/src/index.d.ts +11 -3
  50. package/dist/src/index.js +9 -4
  51. package/dist/src/log.js +12 -7
  52. package/dist/src/machines.d.ts +81 -5
  53. package/dist/src/machines.js +125 -9
  54. package/dist/src/managed-database.d.ts +15 -11
  55. package/dist/src/managed-database.js +38 -37
  56. package/dist/src/multipart.d.ts +21 -3
  57. package/dist/src/multipart.js +112 -38
  58. package/dist/src/observe.d.ts +3 -0
  59. package/dist/src/observe.js +6 -2
  60. package/dist/src/openai-wire.d.ts +16 -0
  61. package/dist/src/openai-wire.js +43 -12
  62. package/dist/src/pack-fetch.d.ts +4 -6
  63. package/dist/src/pack-fetch.js +189 -57
  64. package/dist/src/packRegistry.d.ts +7 -1
  65. package/dist/src/packRegistry.js +20 -3
  66. package/dist/src/private-transport.d.ts +1 -0
  67. package/dist/src/private-transport.js +11 -0
  68. package/dist/src/protobuf.js +10 -4
  69. package/dist/src/rateBudget.d.ts +8 -1
  70. package/dist/src/rateBudget.js +1 -0
  71. package/dist/src/redis/engine.d.ts +14 -5
  72. package/dist/src/redis/engine.js +271 -72
  73. package/dist/src/redis/frames.d.ts +17 -0
  74. package/dist/src/redis/frames.js +123 -0
  75. package/dist/src/redis/index.d.ts +6 -0
  76. package/dist/src/redis/index.js +6 -0
  77. package/dist/src/redis/json.d.ts +2 -0
  78. package/dist/src/redis/json.js +14 -0
  79. package/dist/src/redis/lua-libs.d.ts +4 -0
  80. package/dist/src/redis/lua-libs.js +173 -0
  81. package/dist/src/redis/lua.d.ts +1 -0
  82. package/dist/src/redis/lua.js +36 -0
  83. package/dist/src/redis/member-storage.d.ts +5 -0
  84. package/dist/src/redis/member-storage.js +75 -0
  85. package/dist/src/redis/resp2.d.ts +4 -0
  86. package/dist/src/redis/resp2.js +29 -0
  87. package/dist/src/redis/stream.d.ts +14 -0
  88. package/dist/src/redis/stream.js +335 -0
  89. package/dist/src/remote-execute.d.ts +2 -2
  90. package/dist/src/request-body.d.ts +11 -0
  91. package/dist/src/request-body.js +11 -0
  92. package/dist/src/runtime.d.ts +6 -5
  93. package/dist/src/runtime.js +5 -5
  94. package/dist/src/s3/wire.d.ts +4 -4
  95. package/dist/src/s3/wire.js +5 -3
  96. package/dist/src/scenario.d.ts +22 -0
  97. package/dist/src/scenario.js +35 -31
  98. package/dist/src/serve-http.d.ts +2 -1
  99. package/dist/src/serve-http.js +10 -1
  100. package/dist/src/serve.js +7 -4
  101. package/dist/src/signing.d.ts +23 -5
  102. package/dist/src/signing.js +74 -11
  103. package/dist/src/sigv4.d.ts +3 -1
  104. package/dist/src/sigv4.js +4 -2
  105. package/dist/src/smtp.js +2 -1
  106. package/dist/src/sockets.js +8 -3
  107. package/dist/src/state-system.d.ts +3 -0
  108. package/dist/src/storage.js +5 -2
  109. package/dist/src/twin-fetch.d.ts +3 -1
  110. package/dist/src/twin-fetch.js +3 -2
  111. package/dist/src/vendor-call.d.ts +10 -3
  112. package/dist/src/vendor-call.js +58 -14
  113. package/dist/test-fixtures/attestation.SOURCE.md +1 -0
  114. package/dist/test-fixtures/attestation.json +50 -0
  115. package/generated/pack-facts.json +409 -352
  116. package/inject.cjs +474 -95
  117. package/network-policy.cjs +108 -13
  118. package/network-policy.d.cts +8 -3
  119. package/package.json +11 -4
  120. package/src/actions.ts +2 -1
  121. package/src/anthropic-wire.ts +6 -0
  122. package/src/attestation.ts +23 -0
  123. package/src/bytes.ts +21 -0
  124. package/src/changeset.ts +2 -1
  125. package/src/clickhouse/sql.ts +50 -16
  126. package/src/derived-core.ts +573 -249
  127. package/src/derived-real.ts +552 -42
  128. package/src/derived.ts +23 -4
  129. package/src/events.ts +54 -85
  130. package/src/exact-json.ts +71 -0
  131. package/src/executor.ts +86 -27
  132. package/src/file-response.ts +1 -1
  133. package/src/form.ts +66 -0
  134. package/src/git/format.ts +89 -0
  135. package/src/git/index.ts +1 -0
  136. package/src/git/lfs.ts +5 -4
  137. package/src/graphql-wire.ts +8 -3
  138. package/src/grpc-wire.ts +2 -1
  139. package/src/head.ts +15 -5
  140. package/src/held-reads.ts +54 -0
  141. package/src/history.ts +5 -4
  142. package/src/host-port.ts +9 -0
  143. package/src/index.ts +10 -4
  144. package/src/log.ts +11 -7
  145. package/src/machines.ts +135 -13
  146. package/src/managed-database.ts +48 -40
  147. package/src/multipart.ts +110 -35
  148. package/src/observe.ts +6 -3
  149. package/src/openai-wire.ts +45 -14
  150. package/src/pack-fetch.ts +173 -50
  151. package/src/packRegistry.ts +27 -4
  152. package/src/private-transport.ts +8 -0
  153. package/src/protobuf.ts +8 -4
  154. package/src/rateBudget.ts +6 -1
  155. package/src/redis/engine.ts +168 -63
  156. package/src/redis/frames.ts +53 -0
  157. package/src/redis/index.ts +6 -0
  158. package/src/redis/json.ts +13 -0
  159. package/src/redis/lua-libs.ts +62 -0
  160. package/src/redis/lua.ts +20 -0
  161. package/src/redis/member-storage.ts +58 -0
  162. package/src/redis/resp2.ts +25 -0
  163. package/src/redis/stream.ts +102 -0
  164. package/src/remote-execute.ts +2 -2
  165. package/src/request-body.ts +12 -0
  166. package/src/runtime.ts +6 -5
  167. package/src/s3/wire.ts +8 -5
  168. package/src/scenario.ts +34 -13
  169. package/src/serve-http.ts +11 -2
  170. package/src/serve.ts +4 -3
  171. package/src/signing.ts +71 -17
  172. package/src/sigv4.ts +5 -3
  173. package/src/smtp.ts +3 -2
  174. package/src/sockets.ts +8 -3
  175. package/src/state-system.ts +3 -0
  176. package/src/storage.ts +5 -2
  177. package/src/twin-fetch.ts +4 -3
  178. package/src/vendor-call.ts +59 -12
  179. package/test-fixtures/attestation.json +50 -0
@@ -1,12 +1,16 @@
1
+ import { parseExactJson, exactInteger, compareExactNumbers } from "./exact-json.js";
1
2
  import { aliasesFrom, parentEntries, readParentTreeMap, readTree } from "./log.js";
2
3
  import { storedType } from "./derived-core.js";
3
4
  import { verifyEvent } from "./events.js";
4
5
  import { hashFieldValue } from "./hash.js";
5
- import { readResourceBlob } from "./resource-blob.js";
6
+ import { readResourceBlob, writeResourceBlob } from "./resource-blob.js";
6
7
  import { RefusedWriteError } from "./head.js";
7
8
  import { observeResources } from "./observe.js";
9
+ import { readRoot } from "./state-system.js";
8
10
  /** A JSON path's value: `a.b`, `a[0].b`, `$body.a` (the leading `$body.` read as the root). */
9
11
  function at(value, path) {
12
+ if (value && typeof value === 'object' && Object.hasOwn(value, path))
13
+ return value[path];
10
14
  let cur = value;
11
15
  for (const part of path.replace(/^\$body\.?/, '').split(/\.|\[(\d+)\]/).filter((p) => p !== undefined && p !== '')) {
12
16
  if (cur === null || typeof cur !== 'object')
@@ -16,20 +20,84 @@ function at(value, path) {
16
20
  return cur;
17
21
  }
18
22
  const object = (v) => (v !== null && typeof v === 'object' && !Array.isArray(v) ? v : undefined);
19
- const parse = (r) => { try {
20
- return r.body ? JSON.parse(r.body) : undefined;
23
+ const parse = (r) => {
24
+ if (r.body && r.body.trimStart().startsWith('<'))
25
+ return fromXml(r.body);
26
+ try {
27
+ return r.body ? parseExactJson(r.body) : undefined;
28
+ }
29
+ catch {
30
+ return r.body;
31
+ }
32
+ };
33
+ /** Link header relations and extension parameters, with commas and escapes inside quoted values preserved. */
34
+ function linkRelations(value) {
35
+ const links = [];
36
+ let rest = value.trim();
37
+ while (rest) {
38
+ const target = /^<([^<>]+)>/.exec(rest);
39
+ if (!target)
40
+ throw new Error('refresh continuation has a malformed Link header; nothing folded');
41
+ rest = rest.slice(target[0].length).trimStart();
42
+ const params = {};
43
+ while (rest.startsWith(';')) {
44
+ const param = /^;\s*([\w!#$%&'*+.^`|~-]+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s;,]+))\s*/.exec(rest);
45
+ if (!param)
46
+ throw new Error('refresh continuation has a malformed Link parameter; nothing folded');
47
+ const name = param[1].toLowerCase();
48
+ if (name in params)
49
+ throw new Error('refresh continuation repeats a Link parameter; nothing folded');
50
+ params[name] = param[2] !== undefined ? param[2].replace(/\\(.)/g, '$1') : param[3];
51
+ rest = rest.slice(param[0].length);
52
+ }
53
+ links.push({ url: target[1], params });
54
+ if (rest && !rest.startsWith(','))
55
+ throw new Error('refresh continuation has a malformed Link separator; nothing folded');
56
+ if (rest) {
57
+ rest = rest.slice(1).trimStart();
58
+ if (!rest)
59
+ throw new Error('refresh continuation ends with a Link separator; nothing folded');
60
+ }
61
+ }
62
+ return links;
63
+ }
64
+ /** An XML answer (S3's lists: ListObjectsV2, ListMultipartUploads, ListParts) read as JSON, so a refresh scope names its
65
+ * items as it does a JSON answer's (`ListBucketResult.Contents`): an element with children is an object of them, a
66
+ * child repeated a list of them, a leaf its text; attributes, declarations and comments are not read. */
67
+ function fromXml(text) {
68
+ const decode = (t) => t.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n))).replace(/&amp;/g, '&');
69
+ const stack = [{ name: '', obj: {}, text: '', children: false }];
70
+ const put = (parent, name, value) => {
71
+ const held = parent.obj[name];
72
+ parent.obj[name] = held === undefined ? value : Array.isArray(held) ? [...held, value] : [held, value];
73
+ parent.children = true;
74
+ };
75
+ for (const m of text.matchAll(/<\?[\s\S]*?\?>|<!--[\s\S]*?-->|<!\[CDATA\[([\s\S]*?)\]\]>|<(\/?)([A-Za-z_][\w:.-]*)[^>]*?(\/?)>|([^<]+)/g)) {
76
+ const top = stack.at(-1);
77
+ if (m[1] !== undefined)
78
+ top.text += m[1];
79
+ else if (m[5] !== undefined)
80
+ top.text += decode(m[5]);
81
+ else if (m[3] !== undefined && m[4] === '/')
82
+ put(top, m[3], '');
83
+ else if (m[3] !== undefined && m[2] !== '/')
84
+ stack.push({ name: m[3], obj: {}, text: '', children: false });
85
+ else if (m[3] !== undefined && stack.length > 1) {
86
+ const node = stack.pop();
87
+ put(stack.at(-1), node.name, node.children ? node.obj : node.text.trim());
88
+ }
89
+ }
90
+ return stack[0].obj;
21
91
  }
22
- catch {
23
- return r.body;
24
- } };
25
92
  /** The manifest's resource a surface schema answers (the operation's `answers.resource` is the spec's schema name). */
26
93
  function resourceOf(m, schema) {
27
94
  if (!schema)
28
95
  return undefined;
29
96
  if (m.resources[schema])
30
97
  return schema;
31
- const lower = schema.toLowerCase();
32
- return Object.keys(m.resources).find((r) => r.toLowerCase() === lower || storedType(m, r).toLowerCase() === lower);
98
+ const normalized = (name) => name.replace(/_/g, '').toLowerCase();
99
+ const lower = normalized(schema);
100
+ return Object.keys(m.resources).find((r) => normalized(r) === lower || normalized(storedType(m, r)) === lower);
33
101
  }
34
102
  /** The error template's fixed leaves (Slack's `ok: false`): an answer holding them all is the vendor's refusal, whatever
35
103
  * its status (a vendor that answers its errors 200). */
@@ -68,6 +136,12 @@ function refusalWords(m, body) {
68
136
  walk(m.error, '');
69
137
  return found ?? (typeof body === 'string' ? body : JSON.stringify(body ?? null)).slice(0, 300);
70
138
  }
139
+ /** Whether an answer is the vendor's refusal whatever its status: it holds every fixed leaf of the error template (Slack's
140
+ * `ok: false`), as a perform reads it ("Refresh": a 200 refusal is never folded as an empty page). */
141
+ function refusedIn(m, body) {
142
+ const shape = refusalShape(m);
143
+ return shape.length > 0 && shape.every(([p, v]) => at(body, p) === v);
144
+ }
71
145
  /** Every string in a body that names a subject the vendor knows by another id, as the vendor names it. */
72
146
  function resolvedDeep(value, resolve) {
73
147
  if (typeof value === 'string')
@@ -104,6 +178,36 @@ async function multipart(body, files, ctx) {
104
178
  }
105
179
  return { body: out, type: `multipart/form-data; boundary=${boundary}` };
106
180
  }
181
+ /** What every call of an operation carries beyond its parameters: its query discriminator (S3's `?uploads`, a bare key
182
+ * where its value is null) and its fixed headers (AWS JSON's `x-amz-target`), as the route that serves it reads them. */
183
+ function marked(op, path) {
184
+ const query = new URLSearchParams(path.includes('?') ? path.slice(path.indexOf('?') + 1) : '');
185
+ const marks = Object.entries(op.discriminator ?? {}).filter(([k]) => !query.has(k)).map(([k, v]) => (v === null ? encodeURIComponent(k) : `${encodeURIComponent(k)}=${encodeURIComponent(v)}`));
186
+ return marks.length ? `${path}${path.includes('?') ? '&' : '?'}${marks.join('&')}` : path;
187
+ }
188
+ /** Where a unit's call goes: a lane on a host of its own (`remoteHost`, the real-system adapters "Lanes") to that host,
189
+ * filled from the call's parameters, as an absolute URL the executor admits only on a host the descriptor declares; any
190
+ * other call to its path on the root's origin. A parameter is one host label, or the literal `values` maps it to. */
191
+ function sentTo(m, path, params) {
192
+ if (!m.remoteHost)
193
+ return path;
194
+ const { template, values } = m.remoteHost;
195
+ const host = template.replace(/\{([^}]+)\}/g, (_, name) => {
196
+ const raw = params[name] === undefined || params[name] === null ? '' : String(params[name]);
197
+ const mapped = values?.[name];
198
+ if (mapped) {
199
+ const literal = mapped[raw];
200
+ if (literal === undefined)
201
+ throw new Error(`${m.vendor}: the remote host has no value for ${name} ${JSON.stringify(raw)}`);
202
+ return literal;
203
+ }
204
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(raw))
205
+ throw new Error(`${m.vendor}: the remote host's ${name} is missing or is not a host label`);
206
+ return raw;
207
+ });
208
+ return `https://${host}${path}`;
209
+ }
210
+ const fixedHeaders = (op) => Object.fromEntries(Object.entries(op.headers ?? {}).filter((e) => typeof e[1] === 'string'));
107
211
  /** The body as the operation's encoding sends it. */
108
212
  function encode(op, body) {
109
213
  if (body === undefined || op.bodyEncoding === 'none')
@@ -124,6 +228,68 @@ function unitOf(units, input) {
124
228
  ?? units.find((u) => u.surface.operations.some((o) => o.id === id))
125
229
  ?? (input.graphql ? units[0] : undefined);
126
230
  }
231
+ /** Read the schema-selected resource through response aliases and fragment selections. */
232
+ async function graphqlAnswers(m, surface, input, parsed) {
233
+ if (!surface.graphql)
234
+ throw new Error('a GraphQL perform needs the vendored schema on its derived surface');
235
+ const g = await import('graphql');
236
+ const schema = g.buildSchema(surface.graphql.sdl, { assumeValidSDL: true });
237
+ const sent = object(input.graphql);
238
+ const document = g.parse(String(sent.query));
239
+ const operation = g.getOperationAST(document, typeof sent.operationName === 'string' ? sent.operationName : undefined);
240
+ if (!operation || operation.operation !== 'mutation')
241
+ throw new Error('the recorded GraphQL write has no selected mutation');
242
+ const fragments = new Map(document.definitions.filter((d) => d.kind === 'FragmentDefinition').map((d) => [d.name.value, d]));
243
+ const fields = (selection, seen = new Set()) => selection.selections.flatMap((node) => {
244
+ if (node.kind === 'Field')
245
+ return [node];
246
+ if (node.kind === 'InlineFragment')
247
+ return fields(node.selectionSet, seen);
248
+ if (seen.has(node.name.value))
249
+ return [];
250
+ const fragment = fragments.get(node.name.value);
251
+ return fragment ? fields(fragment.selectionSet, new Set([...seen, node.name.value])) : [];
252
+ });
253
+ const decode = (value, selection) => {
254
+ if (Array.isArray(value))
255
+ return value.map((v) => decode(v, selection));
256
+ const row = object(value);
257
+ if (!row || !selection)
258
+ return value;
259
+ const decoded = {};
260
+ for (const field of fields(selection)) {
261
+ const key = field.alias?.value ?? field.name.value;
262
+ if (key in row)
263
+ decoded[field.name.value] = decode(row[key], field.selectionSet);
264
+ }
265
+ return decoded;
266
+ };
267
+ const answers = [];
268
+ for (const root of fields(operation.selectionSet)) {
269
+ const definition = schema.getMutationType()?.getFields()[root.name.value];
270
+ if (!definition)
271
+ continue;
272
+ const type = g.getNamedType(definition.type);
273
+ const value = object(at(parsed, `data.${root.alias?.value ?? root.name.value}`));
274
+ // Skipped selections (directives) have no answer and adopt nothing.
275
+ if (!value)
276
+ continue;
277
+ const resource = resourceOf(m, type.name);
278
+ if (resource) {
279
+ answers.push({ resource, value: object(decode(value, root.selectionSet)) });
280
+ continue;
281
+ }
282
+ if (g.isObjectType(type) && root.selectionSet)
283
+ for (const child of fields(root.selectionSet)) {
284
+ const field = type.getFields()[child.name.value];
285
+ const resource = field ? resourceOf(m, g.getNamedType(field.type).name) : undefined;
286
+ const selected = object(value[child.alias?.value ?? child.name.value]);
287
+ if (resource && selected)
288
+ answers.push({ resource, value: object(decode(selected, child.selectionSet)) });
289
+ }
290
+ }
291
+ return answers;
292
+ }
127
293
  /** The account a root is, before a perform sends a path naming the World's ("The account a root is"): the World's
128
294
  * account its own doors made, unadopted, adopts the one account the root's credential lists. Read once per deploy (an
129
295
  * adopted account is not asked again); a list the vendor refuses, or one naming several accounts, adopts none. */
@@ -141,7 +307,7 @@ async function adoptAccount(units, execute, ctx) {
141
307
  const op = scope && 'list' in scope ? u.surface.operations.find((o) => o.id === scope.list) : undefined;
142
308
  if (!local.length || !op)
143
309
  continue;
144
- const answer = await execute({ method: op.method.toUpperCase(), path: `${op.basePath ?? u.surface.basePath ?? ''}${op.path}`, ...(u.lane ? { lane: u.lane } : {}) });
310
+ const answer = await execute({ method: op.method.toUpperCase(), path: sentTo(m, marked(op, `${op.basePath ?? u.surface.basePath ?? ''}${op.path}`), {}), ...(u.lane ? { lane: u.lane } : {}), headers: fixedHeaders(op) });
145
311
  if (answer.status >= 400)
146
312
  continue;
147
313
  const items = itemsOf(op, parse(answer));
@@ -170,11 +336,16 @@ function performOf(units) {
170
336
  let path;
171
337
  let body;
172
338
  let op;
339
+ // the parameters a lane's own host is filled from, as the vendor names them
340
+ let hostParams = {};
173
341
  if (input.graphql) {
174
342
  const gql = m.graphql;
175
343
  method = 'POST';
176
344
  path = gql?.paths[0] ?? '/graphql';
177
- body = resolvedDeep(input.graphql, resolve);
345
+ const g = await import('graphql');
346
+ const recorded = object(input.graphql);
347
+ const query = g.print(g.visit(g.parse(String(recorded.query)), { StringValue(node) { const value = resolve(node.value); return value === node.value ? undefined : { ...node, value }; } }));
348
+ body = { ...recorded, query, variables: resolvedDeep(recorded.variables, resolve) };
178
349
  }
179
350
  else {
180
351
  op = surface.operations.find((o) => o.id === input.operationId);
@@ -205,7 +376,9 @@ function performOf(units) {
205
376
  const query = new URLSearchParams(Object.entries(queried).flatMap(([k, v]) => (Array.isArray(v) ? v : [v]).map((x) => [k, resolve(String(x))])));
206
377
  if ([...query].length)
207
378
  path += `?${query}`;
379
+ path = marked(op, path);
208
380
  body = sentBody === undefined ? undefined : resolvedDeep(sentBody, resolve);
381
+ hostParams = Object.fromEntries(Object.entries(params).map(([k, v]) => [k, resolve(String(v))]));
209
382
  }
210
383
  const files = Array.isArray(input.files) ? input.files : [];
211
384
  // a body recorded as it came (not JSON) goes as it came
@@ -213,26 +386,54 @@ function performOf(units) {
213
386
  const raw = asCame && typeof asCame.$text === 'string' ? { body: asCame.$text } : asCame && typeof asCame.$base64 === 'string' ? { body: new Uint8Array(Buffer.from(asCame.$base64, 'base64')) } : undefined;
214
387
  const sent = raw ? { ...raw, ...(typeof input.contentType === 'string' ? { type: input.contentType } : {}) }
215
388
  : files.length || op?.bodyEncoding === 'multipart' ? await multipart(body, files, ctx) : op ? encode(op, body) : { body: JSON.stringify(body), type: 'application/json' };
216
- const headers = { ...(object(input.headers) ?? {}), ...(sent.type ? { 'content-type': sent.type } : {}) };
389
+ const headers = { ...(op ? fixedHeaders(op) : {}), ...(object(input.headers) ?? {}), ...(sent.type ? { 'content-type': sent.type } : {}) };
217
390
  // a retried deploy makes nothing twice, and two Worlds deploying into one account never collide
218
391
  if (m.idempotency)
219
392
  headers[m.idempotency.header] = hashFieldValue({ credential: ctx.credential ?? '', entry: first.id });
220
- const answer = await execute({ method, path, headers, ...(sent.body !== undefined ? { body: sent.body } : {}), ...(unit.lane ? { lane: unit.lane } : {}) });
393
+ const answer = await execute({ method, path: sentTo(m, path, hostParams), ...(unit.lane ? { lane: unit.lane } : {}), headers, ...(sent.body !== undefined ? { body: sent.body } : {}) });
221
394
  const parsed = parse(answer);
222
- const shape = refusalShape(m);
223
- const refused = answer.status >= 400 || (shape.length > 0 && shape.every(([p, v]) => at(parsed, p) === v));
395
+ const gqlErrors = input.graphql && Array.isArray(at(parsed, 'errors')) ? at(parsed, 'errors') : [];
224
396
  if (answer.status === 408 || answer.status === 429 || answer.status >= 500)
225
397
  throw new Error(`${answer.status}: ${refusalWords(m, parsed)}${answer.headers['retry-after'] ? ` (retry after ${answer.headers['retry-after']}s)` : ''}`);
398
+ if (gqlErrors.length)
399
+ throw new RefusedWriteError('vendor', `400: ${gqlErrors.map((e) => String(e.message)).join('; ')}`, first.id);
400
+ const refused = answer.status >= 400 || refusedIn(m, parsed);
226
401
  if (refused)
227
402
  throw new RefusedWriteError('vendor', `${answer.status >= 400 ? answer.status : 400}: ${refusalWords(m, parsed)}`, first.id);
228
403
  // the subject the answer is: under the operation's key, inside the success envelope when the vendor has one
229
404
  const key = op?.answers?.key;
230
- const obj = object(key ? at(parsed, key) : parsed) ?? object(parsed) ?? {};
405
+ if (input.graphql) {
406
+ const answers = await graphqlAnswers(m, surface, input, parsed);
407
+ const remaining = [...group];
408
+ const adopted = [];
409
+ for (const answer of answers) {
410
+ const index = remaining.findIndex((entry) => entry.subject.type === storedType(m, answer.resource));
411
+ if (index < 0)
412
+ continue;
413
+ const entry = remaining.splice(index, 1)[0];
414
+ const decl = m.resources[answer.resource];
415
+ const data = answer.value ?? {};
416
+ const externalId = entry.fields?.deleted === true ? ctx.resolve(entry.subject.type, entry.subject.id) : storedIdOf(decl, data) ?? '';
417
+ adopted.push({ actionId: entry.id, externalId, data });
418
+ }
419
+ const primary = adopted.shift();
420
+ return primary ? { ...primary, ...(adopted.length ? { also: adopted } : {}) } : { externalId: '', actionId: group.at(-1).id };
421
+ }
422
+ // an XML answer is read through its document element (S3's `InitiateMultipartUploadResult`), as a JSON one under its key
423
+ const documentElement = () => {
424
+ const root = object(parsed);
425
+ const names = root ? Object.keys(root) : [];
426
+ return answer.body.trimStart().startsWith('<') && names.length === 1 ? object(root[names[0]]) : undefined;
427
+ };
428
+ const obj = object(key ? at(parsed, key) : parsed === undefined ? undefined : documentElement() ?? parsed) ?? object(parsed) ?? {};
231
429
  // the answered resource: the spec's schema name, else the envelope key the answer is under (Slack's `channel`,
232
430
  // `message`), when the manifest has a resource of that name
233
431
  const answerKey = op?.answers?.key;
234
432
  const resource = resourceOf(m, op?.answers?.resource) ?? (answerKey && m.resources[answerKey] ? answerKey : undefined);
235
- const decl = resource ? m.resources[resource] : undefined;
433
+ // an answer naming no resource of the manifest (an operation's own output structure: Secrets Manager's
434
+ // CreateSecretResponse, S3's CreateMultipartUploadOutput) is the group's subject's, its id read by that resource
435
+ const subjectDecl = () => Object.entries(m.resources).find(([r]) => storedType(m, r) === group.at(-1).subject.type)?.[1];
436
+ const decl = resource ? m.resources[resource] : subjectDecl();
236
437
  const idOf = (o, d) => { const v = o?.[d?.idAs ?? 'id']; return typeof v === 'string' ? v : undefined; };
237
438
  // the group's entry the answer is: of the answered resource's stored type (a cascade writes children first), else
238
439
  // the last; a delete answers its subject's own vendor id
@@ -247,9 +448,9 @@ function performOf(units) {
247
448
  // a keyed subject's id is its key filled from the answer (its envelope's fields, Slack's `channel` and `ts`, among them)
248
449
  const keyed = decl?.key ? decl : Object.entries(m.resources).find(([r]) => storedType(m, r) === primary.subject.type)?.[1];
249
450
  const externalId = deleted ? ctx.resolve(primary.subject.type, primary.subject.id)
250
- : keyed?.key ? storedIdOf(keyed, { ...(object(parsed) ?? {}), ...obj }) ?? '' : idOf(obj, decl) ?? '';
451
+ : keyed?.key ? storedIdOf(keyed, { ...(object(parsed) ?? {}), ...obj }) ?? '' : idOf(obj, decl) ?? (ctx.resolve(primary.subject.type, primary.subject.id) !== primary.subject.id ? ctx.resolve(primary.subject.type, primary.subject.id) : '');
251
452
  const also = [];
252
- for (const [companion, where] of Object.entries(decl?.companions ?? {})) {
453
+ for (const [companion, where] of Object.entries(decl?.companions ?? keyed?.companions ?? {})) {
253
454
  const id = at(parsed, where) ?? at(obj, where);
254
455
  const entry = group.find((e) => e !== primary && e.subject.type === storedType(m, companion));
255
456
  if (entry && typeof id === 'string')
@@ -296,7 +497,7 @@ function vendorFieldParams(m, resource, params, ctx) {
296
497
  const parent = decl.parent;
297
498
  const parentType = parent ? storedType(m, parent.resource) : undefined;
298
499
  const where = Object.entries(parent?.where ?? {}).map(([field, template]) => [field, template, fill(template)]);
299
- const localParent = where.length ? tree.find((r) => r.type === parentType && where.every(([f, , v]) => String(r[f]) === v)) : undefined;
500
+ const localParent = where.length ? tree.find((r) => r.type === parentType && where.every(([f, , v]) => String(at(r, f)) === v)) : undefined;
300
501
  const parentId = parent ? (where.length ? (localParent ? String(localParent.id) : undefined) : parent.params ? fill(parent.value ?? parent.params.map((p) => `{${p}}`).join('/')) : parent.param !== undefined ? params[parent.param] : undefined) : undefined;
301
502
  if (decl.number && !decl.key) {
302
503
  const type = storedType(m, resource);
@@ -311,16 +512,42 @@ function vendorFieldParams(m, resource, params, ctx) {
311
512
  if (parentCopy)
312
513
  for (const [field, template] of where) {
313
514
  const one = /^\{([^}]+)\}$/.exec(template);
314
- if (one && parentCopy[field] !== undefined)
315
- params[one[1]] = String(parentCopy[field]);
515
+ const value = at(parentCopy, field);
516
+ if (one && value !== undefined)
517
+ params[one[1]] = String(value);
316
518
  }
317
519
  }
318
520
  /** The items of a list's answer: under the operation's key, else the answer when it is a list. */
521
+ /** The items a scope names: a list, or one item where XML names a lone child (an S3 list of one object), or none. */
522
+ const listed = (v) => (Array.isArray(v) ? v : v === undefined || v === null || v === '' ? [] : [v]);
319
523
  function itemsOf(op, parsed) {
320
524
  const v = op.answers?.key ? at(parsed, op.answers.key) : parsed;
321
525
  const list = Array.isArray(v) ? v : Array.isArray(at(parsed, 'data')) ? at(parsed, 'data') : Array.isArray(at(parsed, 'results')) ? at(parsed, 'results') : [];
322
526
  return list.map(object).filter((x) => x !== undefined);
323
527
  }
528
+ /** A vendor response may hold download links in an object map or an array. Missing optional paths yield none. */
529
+ function valuesAt(value, keys) {
530
+ if (!keys.length)
531
+ return value === undefined || value === null ? [] : [value];
532
+ if (!value || typeof value !== 'object')
533
+ return [];
534
+ const [key, ...rest] = keys;
535
+ return key === '*' ? Object.values(value).flatMap((v) => valuesAt(v, rest)) : valuesAt(value[key], rest);
536
+ }
537
+ /** The field a refresh scope's answer names its subject by, where it differs from the resource's (`refresh.idAs`: the
538
+ * Hub's repo read carries the creation's ObjectId as `_id` and its own `namespace/name` as `id`). */
539
+ function scopeIdOf(scope, fields) {
540
+ const field = scope.idAs;
541
+ if (field?.includes('{'))
542
+ return field.replace(/\{([^}]+)\}/g, (_, name) => {
543
+ const value = at(fields, name);
544
+ if (value === undefined || value === null || value === '')
545
+ throw new Error(`refresh identity lacks ${name}; nothing folded`);
546
+ return String(value);
547
+ });
548
+ const value = field ? at(fields, field) : undefined;
549
+ return value === undefined || value === null || value === '' ? undefined : String(value);
550
+ }
324
551
  function refreshOf(units, service) {
325
552
  return async (execute, opts) => {
326
553
  const observed = [];
@@ -328,114 +555,489 @@ function refreshOf(units, service) {
328
555
  // what the root's credential may not read (a token scoped to some of the account): left as the World holds it, said
329
556
  const unread = [];
330
557
  let answered = 0;
331
- for (const unit of units) {
332
- const { manifest: m, surface } = unit;
558
+ for (const { manifest: m, surface, lane } of units) {
559
+ const onLane = lane ? { lane } : {};
333
560
  // parents before children, however deep
334
561
  const scoped = Object.entries(m.resources).filter(([, d]) => d.refresh && !('none' in d.refresh));
335
562
  const depth = (r, seen = new Set()) => { const p = m.resources[r]?.parent?.resource; return p && !seen.has(p) ? 1 + depth(p, seen.add(r)) : 0; };
336
563
  const order = scoped.sort(([a], [b]) => depth(a) - depth(b));
337
564
  for (const [resource, decl] of order) {
338
565
  const scope = decl.refresh;
566
+ if ('none' in scope)
567
+ continue;
568
+ if ('graphql' in scope && scope.graphql) {
569
+ await refreshGraphql(m, resource, decl, scope, execute, opts.scope, observed, complete);
570
+ answered += 1;
571
+ continue;
572
+ }
339
573
  const opId = 'list' in scope ? scope.list : 'get' in scope ? scope.get : '';
340
574
  const op = surface.operations.find((o) => o.id === opId);
341
575
  if (!op)
342
576
  throw new Error(`${m.vendor}: ${resource}'s refresh names ${opId}, no operation of the surface`);
343
577
  const type = storedType(m, resource);
344
- const parents = decl.parent ? [...new Set(observed.filter((o) => o.type === storedType(m, decl.parent.resource)).map((o) => o.id))] : [undefined];
345
- let whole = 'list' in scope && scope.complete !== false;
346
- for (const parent of parents) {
347
- const limit = m.list?.limit && (op.query ?? []).some((q) => q.name === m.list.limit.param) ? { [m.list.limit.param]: String(m.list.limit.max) } : {};
348
- let page = { ...limit };
349
- for (let taken = 0;;) {
350
- let unfilled = false;
351
- const path = `${op.basePath ?? surface.basePath ?? ''}${op.path}`.replace(/\{([^}]+)\}/g, (_, n) => { const v = n === decl.parent?.param && parent !== undefined ? String(parent) : ''; if (!v)
352
- unfilled = true; return encodeURIComponent(v); });
353
- // a parent the operation names in its query, not its path (Slack's conversations.history `channel`)
354
- const byQuery = decl.parent?.param && parent !== undefined && !op.path.includes(`{${decl.parent.param}}`) ? { [decl.parent.param]: String(parent) } : {};
355
- const q = new URLSearchParams({ ...page, ...byQuery });
356
- const answer = await execute({ method: op.method.toUpperCase(), path: [...q].length ? `${path}?${q}` : path, ...(unit.lane ? { lane: unit.lane } : {}) });
578
+ // the known, read one by one: a vendor that lists no whole type (Volter Identity's people and organizations) is
579
+ // read back subject by subject: each the vendor is known to hold (the parent tree: landed or observed, never a
580
+ // World-only subject, which a 404 would wrongly remove), by the vendor's id, its other path parameters filled
581
+ // from its own fields; a 404 is one the vendor no longer has, and only the subjects read are whole
582
+ if ('get' in scope && scope.known) {
583
+ const aliases = aliasesFrom(parentEntries(service, opts.root));
584
+ const held = [...readParentTreeMap(service, opts.root)].filter(([, r]) => r.type === type && r.fields.deleted !== true);
585
+ const read = [];
586
+ // a 403 leaves the type as the World holds it, never complete
587
+ let refused = false;
588
+ for (const [key, r] of held) {
589
+ const subject = { ...r.fields, id: r.id };
590
+ const vendorId = aliases.get(key) ?? String(r.id);
591
+ for (const [name, template] of Object.entries(scope.params ?? {}))
592
+ subject[name] = template.replace(/\{([^}]+)\}/g, (_, field) => {
593
+ if (field === 'id')
594
+ return vendorId;
595
+ if (subject[field] === undefined || subject[field] === null)
596
+ throw new Error(`${m.vendor}: ${resource}'s refresh parameter ${name} lacks ${field}; nothing folded`);
597
+ return String(subject[field]);
598
+ });
599
+ // each path parameter is the subject's own field of that name (the Hub's {namespace} and {repo}); one the subject
600
+ // holds no field for, or `id`, is the vendor's id for it
601
+ const path = `${op.basePath ?? surface.basePath ?? ''}${op.path}`.replace(/\{([^}]+)\}/g, (_, n) => encodeURIComponent(String(n === 'id' || subject[n] === undefined || subject[n] === null ? vendorId : subject[n])));
602
+ // a read naming its subject in the query (an RPC method: Telegram's getChat?chat_id=): the scope's `query`, each
603
+ // `{name}` filled as a path parameter is
604
+ const fillSubject = (t) => t.replace(/\{([^}]+)\}/g, (_, n) => String(n === 'id' || subject[n] === undefined || subject[n] === null ? vendorId : subject[n]));
605
+ const asked = Object.entries(scope.query ?? {}).map(([k, t]) => [k, fillSubject(t)]);
606
+ const target = marked(op, path);
607
+ const sent = asked.length ? `${target}${target.includes('?') ? '&' : '?'}${new URLSearchParams(asked)}` : target;
608
+ const answer = await execute({ method: op.method.toUpperCase(), path: sentTo(m, sent, subject), ...onLane, headers: fixedHeaders(op) });
357
609
  if (answer.status === 401)
358
610
  throw new Error(`401: the vendor refused the root's credential (${op.method.toUpperCase()} ${path}); nothing folded`);
359
611
  if (answer.status === 403) {
360
612
  unread.push({ type, path, status: 403 });
361
- whole = false;
613
+ refused = true;
362
614
  break;
363
615
  }
364
616
  if (answer.status === 429)
365
617
  throw new Error(`429: rate limited${answer.headers['retry-after'] ? `, retry after ${answer.headers['retry-after']}s` : ''}`);
366
- // one subject read by its parent and not there (an account with no workers.dev subdomain): nothing to observe. A
367
- // path missing a parameter (an empty segment) is the kernel's mistake, never the vendor's absence
368
- if ('get' in scope && answer.status === 404 && !unfilled) {
369
- answered += 1;
370
- break;
371
- }
372
- if (answer.status >= 400)
373
- throw new Error(`${answer.status}: ${refusalWords(m, parse(answer))}`);
618
+ read.push(String(subject.id));
374
619
  answered += 1;
375
- const parsed = parse(answer);
376
- const items = 'get' in scope ? [object(op.answers?.key ? at(parsed, op.answers.key) : parsed) ?? {}]
377
- : 'items' in scope && scope.items ? (at(parsed, scope.items) ?? []).map((x) => object(x)).filter((x) => x !== undefined) : itemsOf(op, parsed);
378
- for (const item of items) {
379
- const fields = { ...item, ...(decl.parent && parent !== undefined ? { [decl.parent.field]: parent } : {}) };
380
- const id = storedIdOf(decl, fields);
381
- if (id === undefined)
382
- continue;
383
- observed.push({ type, id, fields: fields });
384
- }
385
- taken += items.length;
386
- if ('get' in scope)
387
- break;
388
- if (taken >= 10_000) {
389
- whole = false;
390
- break;
391
- }
392
- // the next page by the manifest's list model. The list is whole only where its end shows: a cursor model
393
- // gives no next cursor (a short page may still name one), or a page is empty or shorter than the limit
394
- // asked; a list the twin cannot page on, or whose page size it does not know, is not known whole.
395
- if (m.list?.cursor) {
396
- const next = at(parsed, 'next_cursor') ?? at(parsed, 'response_metadata.next_cursor');
397
- if (!next)
398
- break;
399
- page = { ...page, [m.list.cursor.param]: String(next) };
620
+ if (answer.status === 404)
400
621
  continue;
622
+ if (answer.status >= 400 || refusedIn(m, parse(answer)))
623
+ throw new Error(`${answer.status}: ${refusalWords(m, parse(answer))}`);
624
+ const full = object(op.answers?.key ? at(parse(answer), op.answers.key) : parse(answer)) ?? {};
625
+ const local = m.worldFields?.[resource] ?? {};
626
+ const fields = Object.fromEntries(Object.entries(full).filter(([k]) => !(k in local)));
627
+ if (decl.parent) {
628
+ if (r.fields[decl.parent.field] !== undefined)
629
+ fields[decl.parent.field] = r.fields[decl.parent.field];
630
+ for (const [name, template] of Object.entries(decl.parent.keep ?? {})) {
631
+ if (!name.startsWith('_'))
632
+ throw new Error(`${m.vendor}: ${resource}'s parent.keep names a non-private field; nothing folded`);
633
+ fields[name] = fillSubject(template);
634
+ }
401
635
  }
402
- const size = Number(limit[m.list?.limit.param ?? ''] ?? 0);
403
- if (!items.length || (size && items.length < size))
404
- break;
405
- if (!size) {
406
- whole = false;
407
- break;
408
- }
409
- const last = items.at(-1)[decl.idAs ?? 'id'];
410
- if (m.list?.after)
411
- page = { ...page, [m.list.after]: String(last) };
412
- else if (m.list?.page)
413
- page = { ...page, [m.list.page.param]: String(Number(page[m.list.page.param] ?? 1) + 1) };
414
- else if (m.list?.offset)
415
- page = { ...page, [m.list.offset.param]: String(taken) };
416
- else {
417
- whole = false;
418
- break;
419
- }
636
+ // the subject asked for, where its answer names no id of its own (a singleton under its account)
637
+ const id = scopeIdOf(scope, fields) ?? storedIdOf(decl, fields) ?? String(r.id);
638
+ observed.push({ type, id, fields: fields });
420
639
  }
640
+ if (!refused)
641
+ complete.push({ type, ids: read });
642
+ continue;
421
643
  }
644
+ // each parent observed: the path parameters it fills and the value the child's parent field holds (its id, or for
645
+ // a parent the path names by several parameters, `parent.value` filled from them as the twin stores it)
646
+ const parents = decl.parent ? parentsOf(decl.parent, observed.filter((o) => o.type === storedType(m, decl.parent.resource))) : [undefined];
647
+ let whole = 'list' in scope && scope.complete !== false;
648
+ for (const under of parents)
649
+ for (const variant of scope.variants ?? [{ headers: {} }]) {
650
+ const parent = under?.value;
651
+ // the list's own headers, `{name}` its parent's parameter, and the variant's
652
+ const filled = (t) => t.replace(/\{([^}]+)\}/g, (_, n) => under?.params[n] ?? '');
653
+ const listHeaders = { ...Object.fromEntries(Object.entries(scope.headers ?? {}).map(([n, t]) => [n, filled(t)])), ...variant.headers };
654
+ // what a child keeps of its parent's parameters, private
655
+ const kept = Object.fromEntries(Object.entries(decl.parent?.keep ?? {}).map(([f, t]) => {
656
+ if (!f.startsWith('_'))
657
+ throw new Error(`${m.vendor}: ${resource}'s parent.keep names ${f}, which is not private`);
658
+ return [f, filled(t)];
659
+ }));
660
+ // a list asked by POST (an RPC list) carries its page in its body, as the operation's encoding sends it
661
+ const rpc = op.method.toLowerCase() !== 'get';
662
+ const unpaginated = 'list' in scope && scope.unpaginated === true;
663
+ const limit = !unpaginated && m.list?.limit && [...(op.query ?? []), ...(rpc ? op.body ?? [] : [])].some((q) => q.name === m.list.limit.param) ? { [m.list.limit.param]: String(m.list.limit.max) } : {};
664
+ let page = { ...scope.query, ...limit };
665
+ const seenLinks = new Set();
666
+ for (let taken = 0;;) {
667
+ let unfilled = false;
668
+ const path = `${op.basePath ?? surface.basePath ?? ''}${op.path}`.replace(/\{([^}]+)\}/g, (_, n) => { const v = under?.params[n] ?? ''; if (!v)
669
+ unfilled = true; return encodeURIComponent(v); });
670
+ // a parent the operation names in its query, not its path (Slack's conversations.history `channel`)
671
+ // and a parent named by several parameters, each the path does not take that the operation declares in its query
672
+ // (Slack's reactions.get: a message's `channel` and `timestamp`)
673
+ const byQuery = decl.parent?.param && parent !== undefined && !op.path.includes(`{${decl.parent.param}}`) ? { [decl.parent.param]: String(parent) }
674
+ : decl.parent?.params?.length && under ? Object.fromEntries(Object.entries(under.params).filter(([n]) => !op.path.includes(`{${n}}`) && (op.query ?? []).some((x) => x.name === n))) : {};
675
+ // a list asked by POST carries its parent in its body too, unless the operation reads it from its query
676
+ const parentInQuery = !rpc || (op.query ?? []).some((x) => x.name === decl.parent?.param);
677
+ const q = new URLSearchParams(rpc ? (parentInQuery ? byQuery : {}) : { ...page, ...byQuery });
678
+ // Fixed parameters and cursors take the operation's body types, not their manifest string encoding.
679
+ const typedPage = Object.fromEntries(Object.entries(page).map(([k, v]) => {
680
+ const type = (op.body ?? []).find((b) => b.name === k)?.type;
681
+ const value = type === 'integer' ? exactInteger(v) : type === 'number' ? Number(v)
682
+ : type === 'boolean' && (v === 'true' || v === 'false') ? v === 'true' : v;
683
+ return [k, value];
684
+ }));
685
+ const sent = rpc ? encode(op, { ...typedPage, ...(parentInQuery ? {} : byQuery) }) : undefined;
686
+ const answer = await execute({
687
+ method: op.method.toUpperCase(), path: sentTo(m, marked(op, [...q].length ? `${path}?${q}` : path), under?.params ?? {}), ...onLane,
688
+ headers: { ...fixedHeaders(op), ...listHeaders, ...(sent?.type ? { 'content-type': sent.type } : {}) }, ...(sent?.body !== undefined ? { body: sent.body } : {}),
689
+ });
690
+ if (answer.status === 401)
691
+ throw new Error(`401: the vendor refused the root's credential (${op.method.toUpperCase()} ${path}); nothing folded`);
692
+ if (answer.status === 403) {
693
+ unread.push({ type, path, status: 403 });
694
+ whole = false;
695
+ break;
696
+ }
697
+ if (answer.status === 429)
698
+ throw new Error(`429: rate limited${answer.headers['retry-after'] ? `, retry after ${answer.headers['retry-after']}s` : ''}`);
699
+ // one subject read by its parent and not there (an account with no workers.dev subdomain): nothing to observe. A
700
+ // path missing a parameter (an empty segment) is the kernel's mistake, never the vendor's absence
701
+ if ('get' in scope && answer.status === 404 && !unfilled) {
702
+ answered += 1;
703
+ break;
704
+ }
705
+ if (answer.status >= 400 || refusedIn(m, parse(answer)))
706
+ throw new Error(`${answer.status}: ${refusalWords(m, parse(answer))}`);
707
+ answered += 1;
708
+ const parsed = parse(answer);
709
+ const shape = 'list' in scope ? scope.shape : undefined;
710
+ const items = 'get' in scope ? [object(op.answers?.key ? at(parsed, op.answers.key) : parsed) ?? {}]
711
+ : 'items' in scope && scope.items ? listed(at(parsed, scope.items)).map((x) => object(x)).filter((x) => x !== undefined) : itemsOf(op, parsed);
712
+ const keyed = 'list' in scope && scope.keyed === true;
713
+ const map = keyed ? object(at(parsed, scope.items ?? '$body')) : undefined;
714
+ if (keyed && (!map || Object.values(map).some((value) => (shape?.value ? object(value) !== undefined : !object(value)))))
715
+ throw new Error(`${m.vendor}: ${resource}'s keyed refresh must return an object of ${shape?.value ? 'scalars' : 'resource objects'}; nothing folded`);
716
+ // the answer's items as subjects' fields (refresh.shape): a scalar item or map value named, a nested array spread
717
+ let entries = keyed
718
+ ? Object.entries(map).map(([key, value]) => (shape?.value
719
+ ? [shape.keyAs ? undefined : key, { [shape.value]: value, ...(shape.keyAs ? { [shape.keyAs]: key } : {}) }]
720
+ : [key, object(value)]))
721
+ : shape?.scalar && 'items' in scope && scope.items
722
+ ? listed(at(parsed, scope.items)).filter((x) => x !== null && typeof x !== 'object').map((x) => [undefined, { [shape.scalar]: x }])
723
+ : items.map((item) => [undefined, item]);
724
+ // paging reads the answer's own items, before any spread
725
+ const pageItems = entries;
726
+ if (shape?.spread) {
727
+ const { path: inner, as, carry } = shape.spread;
728
+ entries = entries.flatMap(([, item]) => {
729
+ const carried = Object.fromEntries(Object.entries(carry ?? {}).map(([f, p]) => [f, at(item, p)]));
730
+ return listed(at(item, inner)).flatMap((el) => {
731
+ const own = object(el) ?? (as && el !== null && el !== undefined ? { [as]: el } : undefined);
732
+ return own ? [[undefined, { ...carried, ...own }]] : [];
733
+ });
734
+ });
735
+ }
736
+ for (const [mapId, item] of entries) {
737
+ let full = item;
738
+ if ('list' in scope && scope.detail) {
739
+ const detail = surface.operations.find((o) => o.id === scope.detail);
740
+ if (!detail)
741
+ throw new Error(`${m.vendor}: ${resource}'s refresh detail names ${scope.detail}, no operation of the surface`);
742
+ const id = mapId ?? storedIdOf(decl, item);
743
+ const path = `${detail.basePath ?? surface.basePath ?? ''}${detail.path}`.replace(/\{([^}]+)\}/g, (_, n) => {
744
+ const value = under?.params[n] ?? item[n] ?? id;
745
+ if (value === undefined)
746
+ throw new Error(`${m.vendor}: ${resource}'s refresh detail has no value for ${n}`);
747
+ return encodeURIComponent(String(value));
748
+ });
749
+ // the subject a detail read names in its body or query (`detailWith`: AWS JSON's DescribeSecret, `{ SecretId }`)
750
+ const withValues = Object.fromEntries(Object.entries(scope.detailWith ?? {}).map(([k, t]) => [k, t.replace(/\{([^}]+)\}/g, (_, n) => String(n === 'id' ? id : item[n] ?? ''))]));
751
+ const detailRpc = detail.method.toLowerCase() !== 'get';
752
+ const detailQuery = { ...(scope.query ?? {}), ...(detailRpc ? {} : withValues) };
753
+ const detailSent = detailRpc && Object.keys(withValues).length ? encode(detail, withValues) : undefined;
754
+ const answer = await execute({ method: detail.method.toUpperCase(), path: sentTo(m, marked(detail, Object.keys(detailQuery).length ? `${path}?${new URLSearchParams(detailQuery)}` : path), { ...item, ...under?.params }), ...onLane, headers: { ...fixedHeaders(detail), ...(detailSent?.type ? { 'content-type': detailSent.type } : {}) }, ...(detailSent?.body !== undefined ? { body: detailSent.body } : {}) });
755
+ if (answer.status >= 400 || refusedIn(m, parse(answer)))
756
+ throw new Error(`${answer.status}: ${refusalWords(m, parse(answer)) || 'the vendor refused the refresh detail; nothing folded'}`);
757
+ const detailBody = parse(answer);
758
+ const detailKey = scope.detailMerge ?? detail.answers?.key;
759
+ const fragment = object(detailKey ? at(detailBody, detailKey) : detailBody);
760
+ if (scope.detailMerge && !fragment)
761
+ throw new Error(`${m.vendor}: ${resource}'s refresh detail fragment is not an object; nothing folded`);
762
+ const detailFields = fragment ?? {};
763
+ full = scope.detailMerge ? { ...item, ...detailFields } : detailFields;
764
+ }
765
+ // the vendor's shape alone: a World field the twin discloses (manifest.worldFields: Substrate's
766
+ // twin_local_ports) is the answering World's, never the vendor's, and is not folded in
767
+ const local = m.worldFields?.[resource] ?? {};
768
+ const vendorShape = Object.fromEntries(Object.entries(full).filter(([k]) => !(k in local)));
769
+ const fields = { ...vendorShape, ...kept, ...(decl.parent && parent !== undefined ? { [decl.parent.field]: parent } : {}) };
770
+ const id = mapId ?? scopeIdOf(scope, fields) ?? storedIdOf(decl, fields);
771
+ if (id === undefined)
772
+ continue;
773
+ observed.push({ type, id, fields: fields });
774
+ }
775
+ taken += pageItems.length;
776
+ if ('get' in scope || unpaginated)
777
+ break;
778
+ if (taken >= 10_000) {
779
+ whole = false;
780
+ break;
781
+ }
782
+ if ('nextLink' in scope && scope.nextLink) {
783
+ const nextLink = scope.nextLink;
784
+ const header = Object.entries(answer.headers).find(([name]) => name.toLowerCase() === nextLink.header.toLowerCase())?.[1];
785
+ const links = header ? linkRelations(header).filter((link) => link.params.rel?.split(/\s+/).includes(nextLink.rel)) : [];
786
+ if (links.length > 1)
787
+ throw new Error(`${m.vendor}: ambiguous refresh Link relation; nothing folded`);
788
+ const link = links[0];
789
+ if (!link || Object.entries(nextLink.when ?? {}).some(([name, value]) => link.params[name.toLowerCase()] !== value))
790
+ break;
791
+ let marker;
792
+ try {
793
+ marker = new URL(link.url).searchParams.get(nextLink.query);
794
+ }
795
+ catch {
796
+ throw new Error(`${m.vendor}: refresh Link continuation is not an absolute URL; nothing folded`);
797
+ }
798
+ if (!marker)
799
+ throw new Error(`${m.vendor}: refresh Link URL lacks its declared cursor; nothing folded`);
800
+ const param = scope.cursor ?? nextLink.query;
801
+ if (page[param] === marker || seenLinks.has(marker))
802
+ throw new Error(`${m.vendor}: ${resource}'s list repeated its continuation; nothing folded`);
803
+ seenLinks.add(marker);
804
+ page = { ...page, [param]: marker };
805
+ continue;
806
+ }
807
+ // the next page by the manifest's list model. The list is whole only where its end shows: a cursor model
808
+ // gives no next cursor (a short page may still name one), or a page is empty or shorter than the limit
809
+ // asked; a list the twin cannot page on, or whose page size it does not know, is not known whole.
810
+ // the cursor the scope names (AWS's NextToken), sent back as its parameter
811
+ if ('next' in scope && scope.next) {
812
+ // the vendor's own flag that no page follows ends it, whatever cursor the page names
813
+ if ('more' in scope && scope.more !== undefined && !['true', true].includes(at(parsed, scope.more)))
814
+ break;
815
+ const markers = typeof scope.next === 'string' ? { [scope.cursor ?? scope.next]: scope.next } : scope.next;
816
+ const next = Object.fromEntries(Object.entries(markers).flatMap(([param, where]) => {
817
+ const marker = at(parsed, where);
818
+ return marker === undefined || marker === null || marker === '' ? [] : [[param, String(marker)]];
819
+ }));
820
+ if (!Object.keys(next).length)
821
+ break;
822
+ // a page that names the cursor it was asked with would be read forever: nothing folds
823
+ if (Object.entries(next).every(([k, v]) => page[k] === v))
824
+ throw new Error(`${m.vendor}: ${resource}'s list repeated its continuation; nothing folded`);
825
+ page = { ...page, ...next };
826
+ continue;
827
+ }
828
+ if (m.list?.cursor) {
829
+ const next = at(parsed, 'next_cursor') ?? at(parsed, 'response_metadata.next_cursor');
830
+ if (!next)
831
+ break;
832
+ page = { ...page, [m.list.cursor.param]: String(next) };
833
+ continue;
834
+ }
835
+ const size = Number(limit[m.list?.limit.param ?? ''] ?? 0);
836
+ if (!pageItems.length || (size && pageItems.length < size))
837
+ break;
838
+ if (!size) {
839
+ whole = false;
840
+ break;
841
+ }
842
+ const last = pageItems.at(-1)[0] ?? pageItems.at(-1)[1][decl.idAs ?? 'id'];
843
+ if (m.list?.after)
844
+ page = { ...page, [m.list.after]: String(last) };
845
+ else if (m.list?.page)
846
+ page = { ...page, [m.list.page.param]: String(Number(page[m.list.page.param] ?? 1) + 1) };
847
+ else if (m.list?.offset)
848
+ page = { ...page, [m.list.offset.param]: String(taken) };
849
+ else {
850
+ whole = false;
851
+ break;
852
+ }
853
+ }
854
+ }
422
855
  // a type under a parent is whole only for the parents observed: what it held under another parent stands. A root
423
856
  // scoped to one resource (`RootConfig.scope`, GitHub's `repos/o/r`) lists only what is under it, so a type with no
424
857
  // parent is never the account's whole list there
425
858
  if (whole && !decl.parent && !opts.scope)
426
859
  complete.push(type);
427
860
  else if (whole && parents.length)
428
- complete.push({ type, field: decl.parent.field, within: parents.map(String) });
861
+ complete.push({ type, field: decl.parent.field, within: parents.map((p) => p.value) });
429
862
  }
430
863
  }
431
864
  // every read refused is a credential the vendor refuses, never an account that holds nothing
432
865
  if (unread.length && !answered)
433
866
  throw new Error(`403: the vendor refused the root's credential for every read (${unread.map((u) => u.path).join(', ')}); nothing folded`);
867
+ const downloads = [];
868
+ for (const { manifest: m, surface, lane } of units)
869
+ for (const [resource, decl] of Object.entries(m.resources)) {
870
+ const scope = decl.refresh;
871
+ if (!scope || 'none' in scope || !scope.blobs?.length)
872
+ continue;
873
+ for (const row of observed.filter((o) => o.type === storedType(m, resource)))
874
+ for (const blob of scope.blobs) {
875
+ // the bytes an operation of the unit answers for the subject (R2's GetObject: its list names no download link)
876
+ if ('operation' in blob) {
877
+ if (!blob.field.startsWith('_') || (blob.headersField !== undefined && !blob.headersField.startsWith('_')))
878
+ throw new Error(`${m.vendor}: ${resource}'s refresh blob fields must be private`);
879
+ const op = surface.operations.find((o) => o.id === blob.operation);
880
+ if (!op || op.method.toUpperCase() !== 'GET')
881
+ throw new Error(`${m.vendor}: ${resource}'s refresh blob names ${blob.operation}, no GET of the surface`);
882
+ const under = decl.parent ? parentsOf(decl.parent, observed.filter((o) => o.type === storedType(m, decl.parent.resource))).find((p) => p.value === row.fields[decl.parent.field]) : undefined;
883
+ const values = { ...row.fields, ...under?.params, id: row.id };
884
+ const path = `${op.basePath ?? surface.basePath ?? ''}${op.path}`.replace(/\{([^}]+)\}/g, (_, n) => {
885
+ if (values[n] === undefined || values[n] === null)
886
+ throw new Error(`${m.vendor}: ${resource}'s refresh blob has no value for ${n}`);
887
+ return encodeURIComponent(String(values[n]));
888
+ });
889
+ const answer = await execute({ method: 'GET', path: sentTo(m, marked(op, path), values), ...(lane ? { lane } : {}), headers: { ...fixedHeaders(op), ...blob.headers }, responseType: 'bytes' });
890
+ if (answer.status < 200 || answer.status >= 300 || !answer.bodyBytes)
891
+ throw new Error(`${m.vendor}: refresh blob ${blob.operation} answered ${answer.status}; nothing folded`);
892
+ const key = `refresh/${encodeURIComponent(row.type)}/${encodeURIComponent(row.id)}`;
893
+ // only the headers the scope names: the answer's own (its date, its length) would change every read
894
+ const keptHeaders = Object.fromEntries(Object.entries(answer.headers).filter(([h]) => (blob.keepHeaders ?? []).some((n) => (n.endsWith('*') ? h.toLowerCase().startsWith(n.slice(0, -1).toLowerCase()) : h.toLowerCase() === n.toLowerCase()))).map(([h, v]) => [h.toLowerCase(), v]));
895
+ row.fields = { ...row.fields, [blob.field]: key, ...(blob.headersField ? { [blob.headersField]: keptHeaders } : {}) };
896
+ downloads.push({ key, bytes: answer.bodyBytes });
897
+ continue;
898
+ }
899
+ for (const value of valuesAt(row.fields, blob.path.split('.'))) {
900
+ if (typeof value !== 'string')
901
+ throw new Error(`${m.vendor}: refresh blob ${blob.path} is not a URL`);
902
+ let url = new URL(value);
903
+ const boundUrl = readRoot(service, opts.root)?.url;
904
+ const bound = boundUrl ? new URL(boundUrl) : undefined;
905
+ const prefix = bound?.pathname.replace(/\/+$/, '') ?? '';
906
+ const primary = m.origin?.placeholder ? new URL(m.origin.placeholder).hostname : m.descriptor?.hosts?.find((h) => 'host' in h && !h.exclude);
907
+ const primaryHost = typeof primary === 'string' ? primary : primary && 'host' in primary ? primary.host : undefined;
908
+ let answer;
909
+ let pathname = '';
910
+ for (let hops = 0; hops <= 5; hops++) {
911
+ const fromRoot = !!bound && url.origin === bound.origin && (url.pathname === prefix || url.pathname.startsWith(prefix + '/'));
912
+ pathname = fromRoot ? url.pathname.slice(prefix.length) || '/' : url.pathname;
913
+ if (url.username || url.password || (!fromRoot && (url.protocol !== 'https:' || (url.port && url.port !== '443'))) || (!fromRoot && !m.descriptor?.hosts?.some((h) => 'host' in h && !h.exclude && h.host === url.hostname && (!h.pathPattern || new RegExp(h.pathPattern).test(url.pathname)))))
914
+ throw new Error(`${m.vendor}: refresh blob host is not declared: ${url.hostname}`);
915
+ answer = await execute({ method: 'GET', path: fromRoot || url.hostname === primaryHost ? pathname + url.search : url.href, headers: { host: url.host }, responseType: 'bytes' });
916
+ if (![301, 302, 303, 307, 308].includes(answer.status))
917
+ break;
918
+ if (hops === 5 || !answer.headers.location)
919
+ throw new Error(`${m.vendor}: invalid refresh blob redirect; nothing folded`);
920
+ url = new URL(answer.headers.location, url);
921
+ }
922
+ if (!answer || answer.status < 200 || answer.status >= 300 || !answer.bodyBytes)
923
+ throw new Error(`${m.vendor}: refresh blob ${blob.path} answered ${answer?.status}; nothing folded`);
924
+ const fill = (template, key = '') => template.replace(/\{pathname\}/g, decodeURIComponent(pathname)).replace(/\{key\}/g, key).replace(/\{header:([^}]+)\}/g, (_, name) => {
925
+ const value = answer.headers[name.toLowerCase()];
926
+ if (value === undefined)
927
+ throw new Error(`${m.vendor}: refresh blob header ${name} is missing; nothing folded`);
928
+ return value;
929
+ });
930
+ const key = fill(blob.key);
931
+ if (!key || key.startsWith('/') || key.includes('\0') || key.split(/[\\/]/).includes('..') || /\{[^}]+\}/.test(key))
932
+ throw new Error(`${m.vendor}: invalid refresh blob key`);
933
+ if (blob.resource) {
934
+ if (!blob.resource.startsWith('_') || !m.resources[blob.resource])
935
+ throw new Error(`${m.vendor}: refresh blob metadata requires a declared bookkeeping resource`);
936
+ observed.push({ type: storedType(m, blob.resource), id: key, fields: Object.fromEntries(Object.entries(blob.fields ?? {}).map(([name, template]) => [name, fill(template, key)])) });
937
+ }
938
+ downloads.push({ key, bytes: answer.bodyBytes });
939
+ }
940
+ }
941
+ }
942
+ for (const download of downloads)
943
+ await writeResourceBlob(service, download.key, download.bytes, opts.root);
434
944
  adopt(service, opts.root, units, observed);
435
945
  const report = observeResources(service, observed, { ...(opts.root !== undefined ? { root: opts.root } : {}), complete });
436
946
  return unread.length ? { ...report, unread } : report;
437
947
  };
438
948
  }
949
+ /** A declared GraphQL read over the same executor and atomic observation batch as REST. */
950
+ async function refreshGraphql(m, resource, decl, scope, execute, rootScope, observed, complete) {
951
+ const gql = scope.graphql;
952
+ const endpoint = m.graphql?.paths[0];
953
+ if (!endpoint)
954
+ throw new Error(`${m.vendor}: a GraphQL refresh needs a GraphQL endpoint`);
955
+ const type = storedType(m, resource);
956
+ const parents = decl.parent ? parentsOf(decl.parent, observed.filter((o) => o.type === storedType(m, decl.parent.resource))) : [undefined];
957
+ let whole = 'list' in scope && scope.complete !== false;
958
+ for (const under of parents) {
959
+ const substitute = (v) => typeof v === 'string' && /^\{[^}]+\}$/.test(v) ? under?.params[v.slice(1, -1)] ?? v
960
+ : Array.isArray(v) ? v.map(substitute) : object(v) ? Object.fromEntries(Object.entries(object(v)).map(([k, x]) => [k, substitute(x)])) : v;
961
+ const variables = substitute(gql.variables ?? {});
962
+ const cursors = new Set();
963
+ for (let taken = 0;;) {
964
+ const answer = await execute({ method: 'POST', path: endpoint, headers: { 'content-type': 'application/json' }, body: JSON.stringify({ query: gql.query, variables }) });
965
+ const parsed = parse(answer);
966
+ if (answer.status >= 400 || (Array.isArray(at(parsed, 'errors')) && at(parsed, 'errors').length))
967
+ throw new Error(`${answer.status}: the vendor refused the GraphQL refresh; nothing folded: ${JSON.stringify(parsed)}`);
968
+ const value = at(parsed, gql.items);
969
+ if (value === undefined)
970
+ throw new Error(`${m.vendor}: the GraphQL refresh returned no ${gql.items}; nothing folded`);
971
+ const items = listed(value).map(object).filter((x) => x !== undefined);
972
+ for (const item of items) {
973
+ const fields = { ...item, ...(decl.parent && under ? { [decl.parent.field]: under.value } : {}) };
974
+ const id = storedIdOf(decl, fields);
975
+ if (id !== undefined)
976
+ observed.push({ type, id, fields: fields });
977
+ }
978
+ taken += items.length;
979
+ if ('get' in scope || ('list' in scope && scope.unpaginated))
980
+ break;
981
+ if (!gql.pageInfo) {
982
+ whole = false;
983
+ break;
984
+ }
985
+ const info = object(at(parsed, gql.pageInfo));
986
+ if (!info || typeof info.hasNextPage !== 'boolean')
987
+ throw new Error(`${m.vendor}: missing GraphQL page information; nothing folded`);
988
+ if (!info.hasNextPage)
989
+ break;
990
+ if (taken >= 10_000) {
991
+ whole = false;
992
+ break;
993
+ }
994
+ const cursor = info.endCursor;
995
+ if (typeof cursor !== 'string' || !cursor || cursors.has(cursor))
996
+ throw new Error(`${m.vendor}: invalid GraphQL continuation; nothing folded`);
997
+ cursors.add(cursor);
998
+ variables[gql.cursor ?? 'after'] = cursor;
999
+ }
1000
+ }
1001
+ if (whole && !decl.parent && !rootScope)
1002
+ complete.push(type);
1003
+ else if (whole && parents.length && decl.parent)
1004
+ complete.push({ type, field: decl.parent.field, within: parents.map((p) => p.value) });
1005
+ }
1006
+ /** The parents a child type is listed under, from the parent type's observations: the path parameters each fills and
1007
+ * the value the child's `parent.field` holds. A single `param` is the parent's id. A parent the path names by several
1008
+ * parameters (`params`, GitHub's `{owner}/{repo}`) reads each from the parent's fields through its `where` templates
1009
+ * (`full_name: '{owner}/{repo}'`), and the child holds `value` filled from them (the parameters joined by `/` when it
1010
+ * names none), as the derived core stores it ("Refresh"). A parent whose fields fill not every parameter is skipped. */
1011
+ function parentsOf(parent, observations) {
1012
+ const out = new Map();
1013
+ for (const o of observations) {
1014
+ if (!parent.params?.length) {
1015
+ out.set(o.id, { params: parent.param ? { [parent.param]: o.id } : {}, value: o.id });
1016
+ continue;
1017
+ }
1018
+ const params = {};
1019
+ for (const [field, template] of Object.entries(parent.where ?? {})) {
1020
+ const names = [];
1021
+ // a template that is one parameter whole takes the field whole, its slashes included (an S3 key)
1022
+ const whole = /^\{([^}]+)\}$/.exec(template);
1023
+ if (whole) {
1024
+ const v = at(o.fields, field);
1025
+ if (v !== undefined && v !== null && v !== '')
1026
+ params[whole[1]] = String(v);
1027
+ continue;
1028
+ }
1029
+ const pattern = template.replace(/\{([^}]+)\}|[.*+?^$()|[\]\\]/g, (m, n) => (n ? (names.push(n), '([^/]+)') : `\\${m}`));
1030
+ const got = new RegExp(`^${pattern}$`).exec(String(at(o.fields, field) ?? ''));
1031
+ if (got)
1032
+ names.forEach((n, i) => { params[n] = got[i + 1]; });
1033
+ }
1034
+ if (!parent.params.every((n) => params[n] !== undefined))
1035
+ continue;
1036
+ const value = (parent.value ?? parent.params.map((n) => `{${n}}`).join('/')).replace(/\{([^}]+)\}/g, (_, n) => params[n] ?? '');
1037
+ out.set(value, { params, value });
1038
+ }
1039
+ return [...out.values()];
1040
+ }
439
1041
  /** A vendor's subject's stored id: its key template filled from its fields (its parent's among them), else its id
440
1042
  * field. Undefined when the subject names none. */
441
1043
  function storedIdOf(decl, fields) {
@@ -503,7 +1105,7 @@ function adopt(service, root, units, observed) {
503
1105
  const parentType = decl?.parent && m ? storedType(m, decl.parent.resource) : undefined;
504
1106
  const resolved = (v) => aliases.get(`${parentType}:${String(v)}`) ?? String(v);
505
1107
  const siblings = pool.filter((p) => parentField === undefined || resolved(p.fields[parentField]) === String(o.fields[parentField]));
506
- const keys = decl?.alternateKeys ?? [];
1108
+ const keys = decl?.adoptBy ?? decl?.alternateKeys ?? [];
507
1109
  const match = keys.length ? siblings.filter((p) => keys.every((k) => p.fields[k] !== undefined && p.fields[k] === o.fields[k])) : siblings.length === 1 ? siblings : [];
508
1110
  if (match.length !== 1)
509
1111
  continue;
@@ -534,6 +1136,8 @@ function older(theirs, held) {
534
1136
  return false;
535
1137
  const a = Number(theirs);
536
1138
  const b = Number(held);
1139
+ if (/^-?\d+$/.test(String(theirs)) && /^-?\d+$/.test(String(held)))
1140
+ return compareExactNumbers(theirs, held) < 0;
537
1141
  if (String(theirs).trim() !== '' && String(held).trim() !== '' && Number.isFinite(a) && Number.isFinite(b))
538
1142
  return a < b;
539
1143
  const ta = Date.parse(String(theirs));
@@ -552,7 +1156,7 @@ function ingestOf(units, decl, service) {
552
1156
  return Response.json({ error: 'invalid signature' }, { status: 401 });
553
1157
  let event;
554
1158
  try {
555
- event = JSON.parse(body);
1159
+ event = parseExactJson(body);
556
1160
  }
557
1161
  catch {
558
1162
  return Response.json({ error: 'the event is not JSON' }, { status: 400 });
@@ -567,9 +1171,16 @@ function ingestOf(units, decl, service) {
567
1171
  const found = eventResource(m, decl, type);
568
1172
  if (!found)
569
1173
  continue;
570
- const item = object(at(event, decl.object));
571
1174
  const d = m.resources[found.resource];
572
- const id = item ? storedIdOf(d, item) : undefined;
1175
+ // the object where its type carries it, with its parent's value where the event names the parent beside it
1176
+ const carried = object(at(event, found.object ?? decl.object));
1177
+ const parent = found.parent && d.parent ? at(event, found.parent) : undefined;
1178
+ const withParent = carried && (typeof parent === 'string' || typeof parent === 'number') ? { ...carried, [d.parent.field]: String(parent) } : carried;
1179
+ // the subject's id where the type names it (kept out of the fields: the vendor's object carries no such field), and
1180
+ // what the type itself says of the subject
1181
+ const named = found.id && withParent ? at(withParent, found.id) : undefined;
1182
+ const item = withParent ? { ...(found.id && !found.id.includes('.') ? Object.fromEntries(Object.entries(withParent).filter(([k]) => k !== found.id)) : withParent), ...(found.fields ?? {}) } : undefined;
1183
+ const id = found.id ? (typeof named === 'string' || typeof named === 'number' ? String(named) : undefined) : item ? storedIdOf(d, item) : undefined;
573
1184
  if (!item || id === undefined)
574
1185
  break;
575
1186
  const stored = storedType(m, found.resource);