@volter/world-core 3.0.38 → 3.0.39

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