@volter/world-core 2.0.0

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 (180) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +29 -0
  3. package/app-route.cjs +154 -0
  4. package/app-route.d.cts +7 -0
  5. package/attach.cjs +80 -0
  6. package/dist/app-route.cjs +154 -0
  7. package/dist/app-route.d.cts +7 -0
  8. package/dist/attach.cjs +80 -0
  9. package/dist/generated/pack-facts.json +4306 -0
  10. package/dist/inject.cjs +1097 -0
  11. package/dist/network-policy.cjs +92 -0
  12. package/dist/network-policy.d.cts +10 -0
  13. package/dist/src/actions.d.ts +276 -0
  14. package/dist/src/actions.js +436 -0
  15. package/dist/src/ancestry.d.ts +22 -0
  16. package/dist/src/ancestry.js +238 -0
  17. package/dist/src/args.d.ts +3 -0
  18. package/dist/src/args.js +12 -0
  19. package/dist/src/blob-store.d.ts +55 -0
  20. package/dist/src/blob-store.js +186 -0
  21. package/dist/src/brand-tokens.d.ts +2 -0
  22. package/dist/src/brand-tokens.js +17 -0
  23. package/dist/src/changeset.d.ts +431 -0
  24. package/dist/src/changeset.js +0 -0
  25. package/dist/src/client-bundle.d.ts +1 -0
  26. package/dist/src/client-bundle.js +28 -0
  27. package/dist/src/credential.d.ts +38 -0
  28. package/dist/src/credential.js +114 -0
  29. package/dist/src/derived-core.d.ts +452 -0
  30. package/dist/src/derived-core.js +782 -0
  31. package/dist/src/derived.d.ts +84 -0
  32. package/dist/src/derived.js +122 -0
  33. package/dist/src/emit.d.ts +106 -0
  34. package/dist/src/emit.js +157 -0
  35. package/dist/src/executor.d.ts +120 -0
  36. package/dist/src/executor.js +387 -0
  37. package/dist/src/file-response.d.ts +3 -0
  38. package/dist/src/file-response.js +22 -0
  39. package/dist/src/fork.d.ts +26 -0
  40. package/dist/src/fork.js +68 -0
  41. package/dist/src/git/history.d.ts +36 -0
  42. package/dist/src/git/history.js +298 -0
  43. package/dist/src/git/index.d.ts +6 -0
  44. package/dist/src/git/index.js +6 -0
  45. package/dist/src/git/inflate.d.ts +11 -0
  46. package/dist/src/git/inflate.js +194 -0
  47. package/dist/src/git/objects.d.ts +64 -0
  48. package/dist/src/git/objects.js +161 -0
  49. package/dist/src/git/pack.d.ts +14 -0
  50. package/dist/src/git/pack.js +199 -0
  51. package/dist/src/git/refs.d.ts +19 -0
  52. package/dist/src/git/refs.js +35 -0
  53. package/dist/src/git/smart-http.d.ts +45 -0
  54. package/dist/src/git/smart-http.js +223 -0
  55. package/dist/src/hash.d.ts +38 -0
  56. package/dist/src/hash.js +48 -0
  57. package/dist/src/head.d.ts +140 -0
  58. package/dist/src/head.js +313 -0
  59. package/dist/src/history.d.ts +76 -0
  60. package/dist/src/history.js +322 -0
  61. package/dist/src/index.d.ts +73 -0
  62. package/dist/src/index.js +98 -0
  63. package/dist/src/lifecycle.d.ts +1 -0
  64. package/dist/src/lifecycle.js +8 -0
  65. package/dist/src/log.d.ts +254 -0
  66. package/dist/src/log.js +801 -0
  67. package/dist/src/mirror-shell.d.ts +2 -0
  68. package/dist/src/mirror-shell.js +13 -0
  69. package/dist/src/observe.d.ts +49 -0
  70. package/dist/src/observe.js +148 -0
  71. package/dist/src/pack-assets.d.ts +30 -0
  72. package/dist/src/pack-assets.js +88 -0
  73. package/dist/src/packRegistry.d.ts +374 -0
  74. package/dist/src/packRegistry.js +142 -0
  75. package/dist/src/placeholder-remote.d.ts +22 -0
  76. package/dist/src/placeholder-remote.js +86 -0
  77. package/dist/src/proxy.d.ts +25 -0
  78. package/dist/src/proxy.js +155 -0
  79. package/dist/src/rateBudget.d.ts +367 -0
  80. package/dist/src/rateBudget.js +925 -0
  81. package/dist/src/references.d.ts +18 -0
  82. package/dist/src/references.js +27 -0
  83. package/dist/src/remote-execute.d.ts +22 -0
  84. package/dist/src/remote-execute.js +1 -0
  85. package/dist/src/resource-blob.d.ts +10 -0
  86. package/dist/src/resource-blob.js +56 -0
  87. package/dist/src/scenario.d.ts +197 -0
  88. package/dist/src/scenario.js +425 -0
  89. package/dist/src/schemas.d.ts +78 -0
  90. package/dist/src/schemas.js +50 -0
  91. package/dist/src/serve-http.d.ts +48 -0
  92. package/dist/src/serve-http.js +340 -0
  93. package/dist/src/serve.d.ts +147 -0
  94. package/dist/src/serve.js +507 -0
  95. package/dist/src/shared-blob-index.d.ts +4 -0
  96. package/dist/src/shared-blob-index.js +126 -0
  97. package/dist/src/state-system.d.ts +70 -0
  98. package/dist/src/state-system.js +90 -0
  99. package/dist/src/storage.d.ts +101 -0
  100. package/dist/src/storage.js +337 -0
  101. package/dist/src/twin-fetch.d.ts +64 -0
  102. package/dist/src/twin-fetch.js +91 -0
  103. package/dist/src/types.d.ts +40 -0
  104. package/dist/src/types.js +1 -0
  105. package/dist/src/v1-removed.d.ts +159 -0
  106. package/dist/src/v1-removed.js +124 -0
  107. package/dist/src/volter-home.d.ts +5 -0
  108. package/dist/src/volter-home.js +10 -0
  109. package/dist/src/world-clock.d.ts +4 -0
  110. package/dist/src/world-clock.js +32 -0
  111. package/dist/src/world-env.d.ts +3 -0
  112. package/dist/src/world-env.js +22 -0
  113. package/dist/src/world-store-sql.d.ts +27 -0
  114. package/dist/src/world-store-sql.js +86 -0
  115. package/dist/src/world-store.d.ts +168 -0
  116. package/dist/src/world-store.js +475 -0
  117. package/dist/src/worldConfig.d.ts +9 -0
  118. package/dist/src/worldConfig.js +17 -0
  119. package/dist/stream-bridge.cjs +80 -0
  120. package/dist/vendor-hosts.cjs +200 -0
  121. package/generated/pack-facts.json +4306 -0
  122. package/inject.cjs +1097 -0
  123. package/network-policy.cjs +92 -0
  124. package/network-policy.d.cts +10 -0
  125. package/package.json +103 -0
  126. package/src/actions.ts +564 -0
  127. package/src/ancestry.ts +213 -0
  128. package/src/args.ts +14 -0
  129. package/src/blob-store.ts +185 -0
  130. package/src/brand-tokens.ts +17 -0
  131. package/src/changeset.ts +1032 -0
  132. package/src/client-bundle.ts +29 -0
  133. package/src/credential.ts +140 -0
  134. package/src/derived-core.ts +1004 -0
  135. package/src/derived.ts +176 -0
  136. package/src/emit.ts +242 -0
  137. package/src/executor.ts +431 -0
  138. package/src/file-response.ts +22 -0
  139. package/src/fork.ts +89 -0
  140. package/src/git/history.ts +177 -0
  141. package/src/git/index.ts +6 -0
  142. package/src/git/inflate.ts +125 -0
  143. package/src/git/objects.ts +110 -0
  144. package/src/git/pack.ts +105 -0
  145. package/src/git/refs.ts +25 -0
  146. package/src/git/smart-http.ts +149 -0
  147. package/src/hash.ts +66 -0
  148. package/src/head.ts +318 -0
  149. package/src/history.ts +246 -0
  150. package/src/index.ts +323 -0
  151. package/src/lifecycle.ts +8 -0
  152. package/src/log.ts +793 -0
  153. package/src/mirror-shell.ts +15 -0
  154. package/src/observe.ts +130 -0
  155. package/src/pack-assets.ts +81 -0
  156. package/src/packRegistry.ts +408 -0
  157. package/src/placeholder-remote.ts +81 -0
  158. package/src/proxy.ts +183 -0
  159. package/src/rateBudget.ts +1115 -0
  160. package/src/references.ts +46 -0
  161. package/src/remote-execute.ts +26 -0
  162. package/src/resource-blob.ts +57 -0
  163. package/src/scenario.ts +479 -0
  164. package/src/schemas.ts +56 -0
  165. package/src/serve-http.ts +299 -0
  166. package/src/serve.ts +618 -0
  167. package/src/shared-blob-index.ts +108 -0
  168. package/src/state-system.ts +115 -0
  169. package/src/storage.ts +407 -0
  170. package/src/twin-fetch.ts +147 -0
  171. package/src/types.ts +50 -0
  172. package/src/v1-removed.ts +172 -0
  173. package/src/volter-home.ts +11 -0
  174. package/src/world-clock.ts +33 -0
  175. package/src/world-env.ts +18 -0
  176. package/src/world-store-sql.ts +118 -0
  177. package/src/world-store.ts +572 -0
  178. package/src/worldConfig.ts +27 -0
  179. package/stream-bridge.cjs +80 -0
  180. package/vendor-hosts.cjs +200 -0
@@ -0,0 +1,782 @@
1
+ // THE DERIVED CORE — what a derived pack does for an operation it declares no handler for
2
+ // (docs/contributing/architecture.md, "Protocol 3"). The generated surface says what the
3
+ // vendor's operations are; the pack's manifest says the vendor facts no spec carries (how ids look,
4
+ // how lists page, how errors read, which fields are state and how they move). From those two this
5
+ // serves CRUD generically and applies declared transitions, over the kernel's tree and write path.
6
+ // Nothing here knows a vendor: every vendor difference is a manifest value.
7
+ import { applyTwinWrite, applyTwinWriteAtomic, twinResources } from "./serve.js";
8
+ import { copyResource, ownFields, parentStamp, subjectHistory, treeChangesSince, treeStamp } from "./log.js";
9
+ import { getActiveWorldStore } from "./world-store.js";
10
+ import { worldNow } from "./world-clock.js";
11
+ import { createHash } from 'node:crypto';
12
+ import { hashFieldValue } from "./hash.js";
13
+ import { resolveSubjectId, subjectAliases } from "./actions.js";
14
+ import { packReferences } from "./references.js";
15
+ import { twinPublicBase } from "./twin-fetch.js";
16
+ /** What a move to `to` stores in the field: a boolean field's value is a boolean, and a derived field's
17
+ * value is written by the move's effects (a state name is not a timestamp), so it stores nothing itself. */
18
+ export function storedState(decl, to) {
19
+ if (to === undefined || decl.derive?.length)
20
+ return undefined;
21
+ return typeof decl.initial === 'boolean' ? to === 'true' : to;
22
+ }
23
+ /** The state a stored value is in, by the field's machine. */
24
+ export function stateOf(decl, value) {
25
+ for (const d of decl.derive ?? []) {
26
+ const w = d.when;
27
+ if (w.absent !== undefined && (value === undefined || value === null) === w.absent)
28
+ return d.state;
29
+ if (w.equals !== undefined && value === w.equals)
30
+ return d.state;
31
+ if (w.gt !== undefined && typeof value === 'number' && value > w.gt)
32
+ return d.state;
33
+ if (w.truthy !== undefined && Boolean(value) === w.truthy)
34
+ return d.state;
35
+ }
36
+ return value === undefined || value === null ? String(decl.initial) : String(value);
37
+ }
38
+ // ── request parsing ──────────────────────────────────────────────────────────────────────────
39
+ /** A form body's bracket notation (`a[b][0][c]=v`, `expand[]=x`) as nested objects and arrays. A form carries only
40
+ * text: a field the operation's spec types as a number or a boolean (`scalars`, by bracket path, written by
41
+ * world-tooling's formScalarsOf) is read as one when its text is that literal; every other field stays the text sent
42
+ * (`metadata[order]=007`, `name=2024`). */
43
+ export function parseBracketForm(text, scalars) {
44
+ const out = {};
45
+ for (const [rawKey, raw] of new URLSearchParams(text)) {
46
+ const parts = rawKey.replace(/\]/g, '').split('[');
47
+ const kind = scalars && scalarAt(scalars, parts);
48
+ const value = kind === 'boolean' ? (raw === 'true' ? true : raw === 'false' ? false : raw)
49
+ : kind === 'integer' ? (/^-?\d+$/.test(raw) ? Number(raw) : raw)
50
+ : kind === 'number' ? (/^-?\d+(\.\d+)?$/.test(raw) ? Number(raw) : raw)
51
+ : raw;
52
+ let node = out;
53
+ parts.forEach((key, i) => {
54
+ if (i === parts.length - 1) {
55
+ if (key === '') {
56
+ if (Array.isArray(node))
57
+ node.push(value);
58
+ }
59
+ else
60
+ node[key] = value;
61
+ return;
62
+ }
63
+ const next = parts[i + 1];
64
+ if (node[key] === undefined)
65
+ node[key] = next === '' || /^\d+$/.test(next) ? [] : {};
66
+ node = node[key];
67
+ });
68
+ }
69
+ return out;
70
+ }
71
+ /** The type `scalars` gives a form key's parts: a segment is a property name, `[]` an array's item (an index or empty),
72
+ * `*` a map's key. */
73
+ function scalarAt(scalars, parts) {
74
+ let paths = [''];
75
+ for (const [i, part] of parts.entries()) {
76
+ const segs = part === '' || /^\d+$/.test(part) ? ['[]', '*'] : [part, '*'];
77
+ paths = paths.flatMap((p) => segs.map((seg) => (i === 0 ? seg : `${p}.${seg}`)));
78
+ if (paths.length > 64)
79
+ paths = paths.filter((p) => Object.keys(scalars).some((k) => k === p || k.startsWith(`${p}.`)));
80
+ }
81
+ for (const p of paths)
82
+ if (scalars[p])
83
+ return scalars[p];
84
+ return undefined;
85
+ }
86
+ /** A request's parameters, its query's and its body's. With the manifest's `body.form.coerce`, a form's fields the
87
+ * operation's spec types as numbers or booleans are read as them (parseBracketForm); without an operation (a twin door)
88
+ * every form value is its text. */
89
+ export async function readParams(manifest, request, operation) {
90
+ const url = new URL(request.url);
91
+ const scalars = manifest.body.form?.coerce ? (operation?.scalars ?? {}) : undefined;
92
+ const query = parseBracketForm(url.search.replace(/^\?/, ''), scalars);
93
+ if (request.method === 'GET' || request.method === 'HEAD')
94
+ return query;
95
+ const type = request.headers.get('content-type') ?? '';
96
+ if (type.includes('multipart/form-data')) {
97
+ // an upload: text fields as given, each file as its name, media type, size and content (its text), with its raw
98
+ // `bytes` beside them for a pack that must tell an image or audio file from anything else; the bytes are not
99
+ // enumerated, so a write's record of the request keeps the text alone
100
+ const form = await request.formData();
101
+ const out = { ...query };
102
+ for (const [key, value] of form.entries()) {
103
+ if (typeof value === 'string') {
104
+ out[key] = value;
105
+ continue;
106
+ }
107
+ const file = value;
108
+ const bytes = new Uint8Array(await file.arrayBuffer());
109
+ const parsed = { name: file.name, type: file.type, size: file.size, content: new TextDecoder().decode(bytes) };
110
+ out[key] = Object.defineProperty(parsed, 'bytes', { value: bytes, enumerable: false });
111
+ }
112
+ return out;
113
+ }
114
+ const text = await request.text();
115
+ if (!text)
116
+ return query;
117
+ const asJson = type.includes('json') || manifest.body.json === 'always';
118
+ const body = asJson ? JSON.parse(text) : parseBracketForm(text, scalars);
119
+ // a body that is not an object (GitHub's set-labels takes a bare array) is the body, not fields
120
+ return body && typeof body === 'object' && !Array.isArray(body) ? { ...query, ...body } : { ...query };
121
+ }
122
+ // ── answers ──────────────────────────────────────────────────────────────────────────────────
123
+ const ABSENT = Symbol('absent');
124
+ function fill(template, values, omitAbsent = false) {
125
+ if (typeof template === 'string') {
126
+ const whole = /^\{(\w+)\}$/.exec(template);
127
+ if (whole)
128
+ return values[whole[1]] ?? (omitAbsent ? ABSENT : null);
129
+ return template.replace(/\{(\w+)\}/g, (_, k) => String(values[k] ?? ''));
130
+ }
131
+ if (Array.isArray(template))
132
+ return template.map((t) => fill(t, values, omitAbsent)).filter((v) => v !== ABSENT);
133
+ if (template && typeof template === 'object') {
134
+ return Object.fromEntries(Object.entries(template).map(([k, v]) => [k, fill(v, values, omitAbsent)]).filter(([, v]) => v !== ABSENT));
135
+ }
136
+ return template;
137
+ }
138
+ export function vendorError(manifest, e) {
139
+ // the status as the answer's number, and as text for a vendor whose body spells it as a string (GitHub's "404")
140
+ const values = { message: e.message, code: e.code ?? null, param: e.param ?? null, kind: e.kind ?? manifest.defaultKind ?? null, status: e.status, statusText: String(e.status) };
141
+ // a vendor that answers its errors as a document, not JSON (S3's XML `<Error><Code>…`): the template is that text,
142
+ // each value escaped into it
143
+ if (typeof manifest.error === 'string') {
144
+ const escape = (v) => String(v ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
145
+ const body = manifest.error.replace(/\{(\w+)\}/g, (_, k) => escape(values[k]));
146
+ return new Response(body, { status: e.status, headers: { 'content-type': body.trimStart().startsWith('<') ? 'application/xml' : 'text/plain; charset=utf-8' } });
147
+ }
148
+ return Response.json(fill(manifest.error, values, manifest.errorOmitsAbsent ?? false), { status: e.status });
149
+ }
150
+ function notFound(manifest, object, id, param) {
151
+ const nf = manifest.notFound;
152
+ if (!nf.withParam)
153
+ param = undefined;
154
+ const own = manifest.resources[object]?.notFound;
155
+ const message = typeof own === 'string' ? own : (own?.message ?? nf.message);
156
+ const code = typeof own === 'object' ? own.code : nf.code;
157
+ return vendorError(manifest, { status: nf.status, message: String(fill(message, { object, id })), ...(code ? { code } : {}), ...(nf.kind ? { kind: nf.kind } : {}), ...(param ? { param } : {}) });
158
+ }
159
+ // ── the tree ─────────────────────────────────────────────────────────────────────────────────
160
+ const storedType = (m, resource) => m.resources[resource]?.storedAs ?? resource;
161
+ // A service's resources by type, folded once per World state (the tree's stamp; any write moves it), not on every read:
162
+ // a read of one resource type projected the service's whole tree, so a pack whose tree also holds many subjects of
163
+ // other types (PlanetScale's SQL rows beside its management records) paid for all of them on each read. A read gets its
164
+ // own copy of each resource, as twinResources hands out, with the vendor's own id/type/updatedAt a spread would drop.
165
+ const typeIndexes = new WeakMap();
166
+ /** A service's resources of one type, each the reader's own copy: `twinResources` filtered by type, from an index
167
+ * that follows each write rather than projecting the whole tree per call. */
168
+ export function resourcesOfType(service, type, root) {
169
+ const store = getActiveWorldStore();
170
+ const indexes = typeIndexes.get(store) ?? typeIndexes.set(store, new Map()).get(store);
171
+ const key = `${service}\u0000${root ?? ''}`;
172
+ let held = indexes.get(key);
173
+ // a write moves the stamp; the index follows it by the subjects the write touched, and refolds only when
174
+ // the kernel cannot say which (a projection of the whole tree after every write grew with the World)
175
+ const delta = held ? treeChangesSince(service, root, held.stamp) : undefined;
176
+ if (held && delta) {
177
+ // the index keeps the tree's order: a subject updated where it stands stays there, one new to the tree (or made
178
+ // again after a delete) goes to the end in the order the tree holds it
179
+ for (const k of [...delta.removed, ...delta.appended])
180
+ held.byType.get(k.slice(0, k.indexOf(':')))?.delete(k);
181
+ const byKey = new Map(delta.changed.map((r) => [`${r.type}:${r.id}`, r]));
182
+ for (const [k, r] of byKey)
183
+ if (!delta.appended.includes(k))
184
+ (held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)).set(k, r);
185
+ for (const k of delta.appended) {
186
+ const r = byKey.get(k);
187
+ (held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)).set(k, r);
188
+ }
189
+ held.stamp = delta.stamp;
190
+ }
191
+ else if (!held || held.stamp !== treeStamp(service, root)) {
192
+ // the stamp is read before the fold, so a write between them can only leave a newer fold under an older stamp
193
+ const stamp = treeStamp(service, root);
194
+ const byType = new Map();
195
+ for (const r of twinResources(service, root))
196
+ (byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)).set(`${r.type}:${r.id}`, r);
197
+ held = { stamp, byType };
198
+ indexes.set(key, held);
199
+ }
200
+ return [...(held.byType.get(type)?.values() ?? [])].map(copyResource);
201
+ }
202
+ function stored(m, resource, root, opts = {}) {
203
+ const type = storedType(m, resource);
204
+ const when = Object.entries(m.resources[resource]?.readableWhen ?? {});
205
+ return resourcesOfType(m.service, type, root).filter((r) => (opts.withDeleted || r.deleted !== true) && when.every(([k, v]) => r[k] === v));
206
+ }
207
+ /** The subject an item operation names: the manifest's key over the path parameters, or the last one. */
208
+ function subjectOf(m, resource, call) {
209
+ const key = m.resources[resource]?.key;
210
+ return key ? String(fill(key, call.params)) : Object.values(call.params).at(-1);
211
+ }
212
+ /** A missing parent's not-found, when the resource lives under one. */
213
+ /** The parent value a call names: one path parameter, or several joined by the parent's template. */
214
+ function parentValue(parent, params) {
215
+ if (parent.params)
216
+ return String(fill(parent.value ?? parent.params.map((p) => `{${p}}`).join('/'), params));
217
+ return parent.param !== undefined ? params[parent.param] : undefined;
218
+ }
219
+ function missingParent(m, resource, call, root) {
220
+ const parent = m.resources[resource]?.parent;
221
+ if (!parent)
222
+ return undefined;
223
+ const id = parentValue(parent, call.params);
224
+ const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))]);
225
+ const found = (r) => (where.length ? where.every(([field, v]) => String(r[field]) === v) : r.id === id);
226
+ return id !== undefined && stored(m, parent.resource, root, { withDeleted: parent.allowDeleted === true }).some(found) ? undefined : notFound(m, parent.resource, String(id), parent.param ?? parent.params?.at(-1));
227
+ }
228
+ /** The vendor's view of a stored subject: its own fields, bookkeeping (`_`) left out. */
229
+ /** A stored subject as the vendor's object: its id first (the subject's, unless the pack kept an id of its own), then
230
+ * the fields the pack wrote, bookkeeping (`_`) and the kernel's updatedAt left out. */
231
+ export function render(r) {
232
+ return Object.fromEntries(Object.entries({ id: r.id, ...ownFields(r) }).filter(([k]) => !k.startsWith('_') && k !== 'updatedAt'));
233
+ }
234
+ function view(m, resource, r) {
235
+ const own = m.resources[resource]?.view;
236
+ if (own)
237
+ return own(render(r), { id: r.id, type: r.type });
238
+ if (!m.view)
239
+ return render(r);
240
+ // a pack's view sees the stored row whole, and bookkeeping (`_`) never reaches the wire whatever it returns
241
+ return Object.fromEntries(Object.entries(m.view(storedType(m, resource), r)).filter(([k]) => !k.startsWith('_')));
242
+ }
243
+ function now(m, at) {
244
+ return m.time === 'unix' ? Math.floor(Date.parse(at) / 1000) : at;
245
+ }
246
+ function applyRule(m, rule, id, values, at) {
247
+ if ('now' in rule)
248
+ return now(m, at);
249
+ if ('id' in rule)
250
+ return id;
251
+ if ('value' in rule)
252
+ return rule.value;
253
+ return fill(rule.template, { ...values, id });
254
+ }
255
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
256
+ /** The next id the manifest's template gives this resource: one past the highest it already minted. */
257
+ function mintId(m, resource, root) {
258
+ const prefix = m.resources[resource].idPrefix;
259
+ if (m.ids.template === '{uuid}')
260
+ return mintUuid(m, resource, root);
261
+ const [head, tail = ''] = m.ids.template.split('{n}');
262
+ const pattern = new RegExp(`^${escapeRe(String(fill(head, { prefix })))}(\\d+)${escapeRe(String(fill(tail, { prefix })))}$`);
263
+ let max = 0;
264
+ // the one type, from the index that follows writes (a scan of the whole World per create grew with it)
265
+ for (const r of resourcesOfType(m.service, storedType(m, resource), root)) {
266
+ const hit = pattern.exec(r.id);
267
+ if (hit)
268
+ max = Math.max(max, Number(hit[1]));
269
+ }
270
+ return String(fill(m.ids.template, { prefix, n: max + 1 }));
271
+ }
272
+ /** The next UUID for a resource: a version-4-shaped UUID derived from the resource and the count before it, past any
273
+ * the World already holds. */
274
+ function mintUuid(m, resource, root) {
275
+ const type = storedType(m, resource);
276
+ const held = new Set(resourcesOfType(m.service, type, root).map((r) => r.id));
277
+ for (let n = held.size + 1;; n++) {
278
+ const h = createHash('sha256').update(`${m.service}:${resource}:${n}`).digest('hex');
279
+ const id = `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${'89ab'[parseInt(h[16], 16) % 4]}${h.slice(17, 20)}-${h.slice(20, 32)}`;
280
+ if (!held.has(id))
281
+ return id;
282
+ }
283
+ }
284
+ function expand(m, resource, body, paths, root) {
285
+ const embeds = m.resources[resource]?.embeds ?? {};
286
+ const out = { ...body };
287
+ for (const path of paths) {
288
+ const [head, ...rest] = path.split('.');
289
+ const target = embeds[head];
290
+ const id = out[head];
291
+ if (!target || typeof id !== 'string')
292
+ continue;
293
+ const hit = stored(m, target, root).find((r) => r.id === id);
294
+ if (hit)
295
+ out[head] = rest.length ? expand(m, target, view(m, target, hit), [rest.join('.')], root) : view(m, target, hit);
296
+ }
297
+ return out;
298
+ }
299
+ function expandPaths(m, params) {
300
+ const raw = m.expandParam ? params[m.expandParam] : undefined;
301
+ return Array.isArray(raw) ? raw.map(String) : typeof raw === 'string' ? [raw] : [];
302
+ }
303
+ async function write(m, call, resource, id, fields, operation, params, root, occurredAt) {
304
+ return (await writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt)).body;
305
+ }
306
+ async function writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt) {
307
+ const { resource: row, result } = await applyTwinWrite(m.service,
308
+ // who made it: the login the pack's identity reads off the request, when it declares one, so a subject's
309
+ // history can say who did each thing
310
+ { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { operationId: call.operation.id, params: call.params, body: params }, occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) } }, root);
311
+ const body = view(m, resource, row);
312
+ if (m.onWrite)
313
+ await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request });
314
+ // the subject's id as it stands after the write: the vendor's, when a live head minted one
315
+ return { body, id: result.externalId ?? row.id, ...(result.vendorData !== undefined ? { vendorData: result.vendorData } : {}) };
316
+ }
317
+ /** An answer in the vendor's success envelope, under the operation's key, when the vendor has one. */
318
+ function envelope(m, op, body) {
319
+ return m.success && op.answers?.key ? { ...m.success, [op.answers.key]: body } : body;
320
+ }
321
+ const encodeOffset = (o) => btoa(JSON.stringify({ o }));
322
+ function decodeOffset(cursor) {
323
+ if (typeof cursor !== 'string' || !cursor)
324
+ return 0;
325
+ try {
326
+ const o = JSON.parse(atob(cursor)).o;
327
+ return typeof o === 'number' && o >= 0 ? o : 0;
328
+ }
329
+ catch {
330
+ return 0;
331
+ }
332
+ }
333
+ let transitionObserver;
334
+ export function observeTransitions(observer) {
335
+ transitionObserver = observer;
336
+ }
337
+ /** The transition a request asks for on one state field, or the vendor's refusal when none applies. */
338
+ /** What a declared machine says to one move: the transition that allows it, or the refusal it gives (and
339
+ * nothing when it declares neither). The derived core and `legal` ask it; so does an engine that is not
340
+ * HTTP-shaped (a line protocol's session), so one machine rules every wire. */
341
+ export function transitionFor(field, decl, operationId, current, requested, id, actor = 'api') {
342
+ const now = stateOf(decl, current);
343
+ const candidates = decl.transitions.filter((t) => (t.actor ?? 'api') === actor && (t.operation ? t.operation === operationId : true) && (requested === undefined || (t.to ?? now) === String(requested)));
344
+ const move = candidates.find((t) => t.from === '*' || t.from.includes(now));
345
+ if (move) {
346
+ transitionObserver?.(decl, move, 'moved', now);
347
+ return { move };
348
+ }
349
+ const refused = candidates.find((t) => t.refusals?.[now]) ?? candidates.find((t) => t.refusal);
350
+ const r = refused?.refusals?.[now] ?? refused?.refusal;
351
+ if (refused && r)
352
+ transitionObserver?.(decl, refused, 'refused', now);
353
+ if (r)
354
+ return { refusal: { status: r.status, message: String(fill(r.message, { from: current, to: refused.to ?? now, field, id: id ?? '' })), ...(r.code ? { code: r.code } : {}) } };
355
+ return {};
356
+ }
357
+ // ALIAS-AWARE LOOKUP AT THE REQUEST BOUNDARY (architecture.md): after an adoption a caller may still name a subject by
358
+ // its local id; a declared reference in the call's params is resolved through the alias map once, here, where the
359
+ // params are parsed, so a handler's lookup (`ctx.row`) and the write both see the adopted id. The map is read only
360
+ // for a pack that declares references, and once per state of the parent side. An idempotency key's request hash is
361
+ // taken over the bytes as sent, so the same key sent once with the local id and once with the adopted one is two
362
+ // requests.
363
+ const aliasMemos = new WeakMap();
364
+ function aliasesNow(service, root) {
365
+ const store = getActiveWorldStore();
366
+ const memos = aliasMemos.get(store) ?? aliasMemos.set(store, new Map()).get(store);
367
+ const key = `${service}\u0000${root ?? ''}`;
368
+ // aliases come from the parent side alone: a local write (the branch log) leaves them as they were
369
+ const stamp = parentStamp(service, root);
370
+ let held = memos.get(key);
371
+ if (!held || held.stamp !== stamp) {
372
+ held = { stamp, aliases: subjectAliases(service, root) };
373
+ memos.set(key, held);
374
+ }
375
+ return held.aliases;
376
+ }
377
+ /** The call with a path parameter naming the operation's subject, or its parent, by a local id moved to the adopted
378
+ * id (`GET /segments/<local>` after the segment was adopted). Only the operation's own type and its parent's are
379
+ * tried, so no other parameter is touched. A vendor naming its subject in the body (Slack's `ts`) declares it as a
380
+ * reference, which adoptedInCall resolves. */
381
+ function adoptedPath(m, call, root) {
382
+ const resource = call.operation.resource;
383
+ const decl = resource ? m.resources[resource] : undefined;
384
+ if (!decl || Object.keys(call.params).length === 0)
385
+ return call;
386
+ const aliases = aliasesNow(m.service, root);
387
+ if (aliases.size === 0)
388
+ return call;
389
+ const types = [storedType(m, resource), ...(decl.parent ? [storedType(m, decl.parent.resource)] : [])];
390
+ let params = call.params;
391
+ for (const [name, value] of Object.entries(call.params)) {
392
+ const adopted = types.map((type) => aliases.get(`${type}:${value}`)).find((id) => id !== undefined);
393
+ if (adopted !== undefined && adopted !== value)
394
+ params = { ...params, [name]: adopted };
395
+ }
396
+ return params === call.params ? call : { ...call, params };
397
+ }
398
+ /** A call's fields with each declared reference that names an adopted subject by its local id moved to the adopted
399
+ * id. A call (a handler's operation) need not say which subject type it writes, so every reference the pack declares
400
+ * is tried: a field is rewritten only when its value is a local id the alias map holds for the reference's target. */
401
+ function adoptedInCall(service, fields, root) {
402
+ const refs = packReferences(service);
403
+ if (refs.length === 0)
404
+ return fields;
405
+ const aliases = aliasesNow(service, root);
406
+ if (aliases.size === 0)
407
+ return fields;
408
+ let out = fields;
409
+ for (const ref of refs) {
410
+ const local = ref.key(out);
411
+ const adopted = local === undefined ? undefined : aliases.get(`${ref.to}:${local}`);
412
+ if (adopted !== undefined && adopted !== local)
413
+ out = { ...out, ...ref.adopt(out, adopted) };
414
+ }
415
+ return out;
416
+ }
417
+ async function boundaryParams(m, call, root) {
418
+ return adoptedInCall(m.service, await readParams(m, call.request, call.operation), root);
419
+ }
420
+ /** Serve one operation from the manifest, or say why the core cannot. */
421
+ export async function serveCore(m, call, scope = {}) {
422
+ const root = scope.root;
423
+ const at = (scope.clock ?? worldNow)();
424
+ const op = call.operation;
425
+ const resource = op.resource;
426
+ const decl = resource ? m.resources[resource] : undefined;
427
+ if (!resource || !decl)
428
+ return { unmodeled: `no manifest entry for resource ${resource ?? '(none)'}` };
429
+ call = adoptedPath(m, call, root);
430
+ const params = await boundaryParams(m, call, root);
431
+ const idParam = subjectOf(m, resource, call);
432
+ const orphan = missingParent(m, resource, call, root);
433
+ if (orphan)
434
+ return { served: orphan };
435
+ const parent = decl.parent;
436
+ const parentId = parent ? parentValue(parent, call.params) : undefined;
437
+ const underParent = (r) => !parent || r[parent.field] === parentId;
438
+ const paths = expandPaths(m, params);
439
+ // the vendor's success status from its spec: 201 for most creates, 204 with no body where it answers nothing
440
+ const status = op.successStatus ?? 200;
441
+ const answer = (body) => ({
442
+ served: status === 204 ? new Response(null, { status }) : Response.json(envelope(m, op, paths.length ? expand(m, resource, body, paths, root) : body), { status }),
443
+ });
444
+ const control = new Set([m.expandParam, m.list.limit.param, m.list.after, m.list.before, m.list.offset?.param].filter(Boolean));
445
+ const data = Object.fromEntries(Object.entries(params).filter(([k]) => !control.has(k) && k !== 'id'));
446
+ switch (op.class) {
447
+ case 'create': {
448
+ for (const field of Object.keys(decl.state ?? {}))
449
+ if (field in data)
450
+ return { unmodeled: `create sets state field ${field}` };
451
+ if (decl.key)
452
+ return { unmodeled: 'a create under a composite key is a handler' };
453
+ const provided = m.ids.acceptProvided && typeof params.id === 'string' && params.id ? params.id : undefined;
454
+ const id = provided ?? mintId(m, resource, root);
455
+ const assigned = Object.fromEntries(Object.entries(decl.assigned ?? {}).map(([k, rule]) => [k, applyRule(m, rule, id, data, at)]));
456
+ const initial = Object.fromEntries(Object.entries(decl.state ?? {}).map(([k, s]) => [k, s.initial]));
457
+ const under = parent ? { [parent.field]: parentId } : {};
458
+ if (decl.number) {
459
+ const siblings = stored(m, resource, root, { withDeleted: true }).filter((r) => !parent || r[parent.field] === parentId);
460
+ under[decl.number.field] = 1 + siblings.reduce((n, r) => Math.max(n, Number(r[decl.number.field]) || 0), 0);
461
+ }
462
+ return answer(await write(m, call, resource, id, { ...assigned, ...initial, ...under, ...data }, `${storedType(m, resource)}.create`, params, root, at));
463
+ }
464
+ case 'retrieve': {
465
+ const hit = idParam ? stored(m, resource, root).find((r) => r.id === idParam && underParent(r)) : undefined;
466
+ return hit ? answer(view(m, resource, hit)) : { served: notFound(m, resource, String(idParam), Object.keys(call.params).at(-1)) };
467
+ }
468
+ case 'list': {
469
+ let rows = stored(m, resource, root).filter(underParent).map((r) => view(m, resource, r));
470
+ for (const f of decl.filters ?? [])
471
+ if (params[f] !== undefined)
472
+ rows = rows.filter((r) => String(r[f]) === String(params[f]));
473
+ const order = decl.order ?? { field: 'created', direction: 'desc' };
474
+ rows.sort((a, b) => {
475
+ // ties by mint order (`file-twin-10` after `file-twin-9`), not by the ids' lexical order
476
+ const d = Number(a[order.field] ?? 0) - Number(b[order.field] ?? 0) || String(a.id).localeCompare(String(b.id), undefined, { numeric: true });
477
+ return order.direction === 'desc' ? -d : d;
478
+ });
479
+ const size = m.list.limits?.[op.id] ?? m.list.limit;
480
+ const asked = params[m.list.limit.param];
481
+ const n = asked === undefined || asked === '' ? size.default : Number(asked);
482
+ const limit = Number.isFinite(n) ? Math.min(size.max, Math.max(1, Math.trunc(n))) : size.default;
483
+ const after = m.list.after ? params[m.list.after] : undefined;
484
+ const before = m.list.before ? params[m.list.before] : undefined;
485
+ let from = 0;
486
+ let to;
487
+ if (typeof before === 'string') {
488
+ const at = rows.findIndex((r) => r.id === before);
489
+ to = at === -1 ? (m.list.unknownCursor === 'end' ? 0 : rows.length) : at;
490
+ from = Math.max(0, to - limit);
491
+ }
492
+ else {
493
+ if (typeof after === 'string') {
494
+ const at = rows.findIndex((r) => r.id === after);
495
+ from = at === -1 ? (m.list.unknownCursor === 'end' ? rows.length : 0) : at + 1;
496
+ }
497
+ to = from + limit;
498
+ }
499
+ const page = rows.slice(from, to).map((r) => (paths.length ? expand(m, resource, r, paths.filter((p) => p.startsWith('data.')).map((p) => p.slice(5)), root) : r));
500
+ const hasMore = typeof before === 'string' ? from > 0 : to < rows.length;
501
+ const url = op.path.replace(/\{[^}]+\}/g, (p) => call.params[p.slice(1, -1)] ?? p);
502
+ if (m.list.page) {
503
+ const n = Math.max(1, Math.floor(Number(params[m.list.page.param]) || 1));
504
+ const slice = rows.slice((n - 1) * limit, n * limit);
505
+ const last = Math.max(1, Math.ceil(rows.length / limit));
506
+ const headers = new Headers();
507
+ if (m.list.page.link && rows.length > limit) {
508
+ const at = (k) => {
509
+ const u = new URL(call.request.url);
510
+ u.searchParams.set(m.list.page.param, String(k));
511
+ return u.toString();
512
+ };
513
+ const rels = [...(n > 1 ? [`<${at(n - 1)}>; rel="prev"`] : []), ...(n < last ? [`<${at(n + 1)}>; rel="next"`, `<${at(last)}>; rel="last"`] : []), ...(n > 1 ? [`<${at(1)}>; rel="first"`] : [])];
514
+ if (rels.length)
515
+ headers.set('link', rels.join(', '));
516
+ }
517
+ return { served: Response.json(fill(m.list.envelope, { data: slice, next_cursor: null, key: op.answers?.key ?? 'data' }), { headers }) };
518
+ }
519
+ if (m.list.offset) {
520
+ const skip = Math.max(0, Math.trunc(Number(params[m.list.offset.param]) || 0));
521
+ const slice = rows.slice(skip, skip + limit);
522
+ const key = op.answers?.key ?? 'data';
523
+ return { served: Response.json({ ...(m.success ?? {}), [key]: slice, ...fill(m.list.envelopes?.[op.id] ?? m.list.envelope, { data: slice, total_count: rows.length, key }) }) };
524
+ }
525
+ if (m.list.cursor) {
526
+ const start = decodeOffset(params[m.list.cursor.param]);
527
+ const slice = rows.slice(start, start + limit);
528
+ const next = start + limit < rows.length ? encodeOffset(start + limit) : '';
529
+ return { served: Response.json({ ...(m.success ?? {}), ...fill(m.list.envelope, { data: slice, next_cursor: next, key: op.answers?.key ?? 'data' }), ...(op.answers?.key ? { [op.answers.key]: slice } : {}) }) };
530
+ }
531
+ return { served: Response.json(fill(m.list.envelope, { data: page, has_more: hasMore, url, first_id: page[0]?.id ?? null, last_id: page.at(-1)?.id ?? null })) };
532
+ }
533
+ case 'update':
534
+ case 'action': {
535
+ const hit = idParam ? stored(m, resource, root).find((r) => r.id === idParam && underParent(r)) : undefined;
536
+ if (!hit)
537
+ return { served: notFound(m, resource, String(idParam), Object.keys(call.params).at(-1)) };
538
+ const current = view(m, resource, hit);
539
+ const changes = { ...(op.class === 'update' ? data : {}) };
540
+ let moved = false;
541
+ for (const [field, sdecl] of Object.entries(decl.state ?? {})) {
542
+ const requested = op.class === 'update' ? data[field] : undefined;
543
+ if (op.class === 'update' && requested === undefined)
544
+ continue;
545
+ const { move, refusal } = transitionFor(field, sdecl, op.id, current[field], requested, hit.id);
546
+ if (refusal)
547
+ return { served: vendorError(m, refusal) };
548
+ if (!move) {
549
+ if (op.class === 'update')
550
+ return { unmodeled: `no declared transition of ${field} to ${String(requested)} by ${op.id}` };
551
+ continue;
552
+ }
553
+ const stored = storedState(sdecl, move.to);
554
+ if (stored !== undefined)
555
+ changes[field] = stored;
556
+ for (const [k, rule] of Object.entries(move.effects ?? {}))
557
+ changes[k] = applyRule(m, rule, hit.id, { ...current, ...changes }, at);
558
+ moved = true;
559
+ }
560
+ if (op.class === 'action' && !moved)
561
+ return { unmodeled: `action ${op.id} declares no transition` };
562
+ const merged = {};
563
+ for (const [k, v] of Object.entries(changes)) {
564
+ const prior = current[k];
565
+ merged[k] = decl.update !== 'replace' && v && typeof v === 'object' && !Array.isArray(v) && prior && typeof prior === 'object' && !Array.isArray(prior) ? Object.fromEntries(Object.entries({ ...prior, ...v }).filter(([, value]) => value !== '')) : v;
566
+ }
567
+ return answer(await write(m, call, resource, hit.id, merged, `${storedType(m, resource)}.${op.class === 'update' ? 'update' : op.id}`, params, root, at));
568
+ }
569
+ case 'delete': {
570
+ const hit = idParam ? stored(m, resource, root).find((r) => r.id === idParam && underParent(r)) : undefined;
571
+ if (!hit)
572
+ return { served: notFound(m, resource, String(idParam), Object.keys(call.params).at(-1)) };
573
+ await write(m, call, resource, hit.id, { deleted: true }, `${storedType(m, resource)}.delete`, params, root, at);
574
+ const shape = decl.deleted !== undefined ? decl.deleted : m.deleted;
575
+ return { served: status === 204 || shape === null ? new Response(null, { status: 204 }) : Response.json(fill(shape, { id: hit.id, object: current(hit) }), { status }) };
576
+ }
577
+ default:
578
+ return { unmodeled: `class ${op.class} is not served by the core` };
579
+ }
580
+ }
581
+ function current(r) {
582
+ return render(r).object ?? r.type;
583
+ }
584
+ /** Server-sent events in the manifest's framing. The events are known when the answer starts: the
585
+ * twin decides deterministically, so the stream carries a decided answer, never a model's. */
586
+ export function sse(m, events, operationId) {
587
+ const enc = new TextEncoder();
588
+ const framing = (operationId && m.streamFor?.[operationId]) || m.sse;
589
+ const frame = (e) => `${framing?.named && e.event ? `event: ${e.event}\n` : ''}data: ${JSON.stringify(e.data)}\n\n`;
590
+ const body = new ReadableStream({
591
+ start(controller) {
592
+ for (const e of events)
593
+ controller.enqueue(enc.encode(frame(e)));
594
+ if (framing?.done)
595
+ controller.enqueue(enc.encode(`data: ${framing.done}\n\n`));
596
+ controller.close();
597
+ },
598
+ });
599
+ return new Response(body, { status: 200, headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache' } });
600
+ }
601
+ async function contextFor(m, call, scope) {
602
+ const root = scope.root;
603
+ call = adoptedPath(m, call, root);
604
+ const at = (scope.clock ?? worldNow)();
605
+ const text = await call.request.clone().text().catch(() => '');
606
+ let body = undefined;
607
+ try {
608
+ body = text ? JSON.parse(text) : undefined;
609
+ }
610
+ catch {
611
+ body = text;
612
+ }
613
+ // a handler reading the body itself sees the adopted id too (the boundary's one resolution, below)
614
+ if (body && typeof body === 'object' && !Array.isArray(body))
615
+ body = adoptedInCall(m.service, body, root);
616
+ const params = await boundaryParams(m, call, root);
617
+ const paths = expandPaths(m, params);
618
+ return {
619
+ call,
620
+ params,
621
+ id: Object.values(call.params).at(-1),
622
+ body,
623
+ text,
624
+ root,
625
+ occurredAt: at,
626
+ actor: m.identity ? m.identity(call.request.headers.get('authorization'), root) : undefined,
627
+ now: () => now(m, at),
628
+ get: (resource, id) => {
629
+ const hit = stored(m, resource, root).find((r) => r.id === id);
630
+ return hit ? view(m, resource, hit) : undefined;
631
+ },
632
+ rows: (resource) => stored(m, resource, root).map((r) => view(m, resource, r)),
633
+ mint: (resource) => mintId(m, resource, root),
634
+ write: (resource, id, fields, operation) => write(m, call, resource, id, fields, operation, params, root, at),
635
+ writeDetailed: (resource, id, fields, operation) => writeDetailed(m, call, resource, id, fields, operation, params, root, at),
636
+ resolve: (resource, id) => resolveSubjectId(m.service, storedType(m, resource), id, root),
637
+ ok: (fields) => Response.json({ ...(m.success ?? {}), ...fields }),
638
+ row: (resource, id, opts) => stored(m, resource, root, opts).find((r) => r.id === id),
639
+ rowsRaw: (resource, opts) => stored(m, resource, root, opts),
640
+ tree: () => twinResources(m.service, root),
641
+ history: (resource, id) => subjectHistory(m.service, { type: storedType(m, resource), id }, root).map((e) => ({ ...(e.operation ? { operation: e.operation } : {}), ...(e.fields ? { fields: e.fields } : {}), ...(e.occurredAt ? { occurredAt: e.occurredAt } : {}) })),
642
+ record: async (type, fields, id) => {
643
+ if (!type.startsWith('_'))
644
+ throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
645
+ const subject = id ?? `${type.slice(1)}_${twinResources(m.service, root).filter((r) => r.type === type).length + 1}`;
646
+ await applyTwinWrite(m.service, { operation: `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, occurredAt: at, actor: { kind: 'system' } }, root);
647
+ return subject;
648
+ },
649
+ raw: (body, init = {}) => new Response(body, { status: init.status ?? 200, headers: init.headers ?? {} }),
650
+ atomically: async (decide) => {
651
+ const { value } = await applyTwinWriteAtomic(m.service, (resources) => {
652
+ const rows = (resource) => resources.filter((r) => r.type === storedType(m, resource) && r.deleted !== true);
653
+ const d = decide(rows);
654
+ if (!d.write)
655
+ return { kind: 'skip', value: d.value };
656
+ const w = d.write;
657
+ return { kind: 'write', value: d.value, write: { operation: w.operation, subjectType: storedType(m, w.resource), subjectId: w.id, fields: w.fields, occurredAt: at, actor: { kind: 'agent' } } };
658
+ }, root);
659
+ return value;
660
+ },
661
+ legal: (resource, field, operationId, current, to, id, actor) => {
662
+ const decl = m.resources[resource]?.state?.[field];
663
+ if (!decl)
664
+ throw new Error(`semantics: ${resource}.${field} is not a declared state field`);
665
+ const { move, refusal } = transitionFor(field, decl, operationId, current, to, id ?? Object.values(call.params).at(-1), actor ?? 'api');
666
+ if (move)
667
+ return undefined;
668
+ if (refusal)
669
+ return refusal;
670
+ throw new Error(`semantics: ${operationId} moves ${resource}.${field} from ${String(current)}${to ? ` to ${to}` : ''}, which the machine does not declare`);
671
+ },
672
+ refuse: (e) => vendorError(m, e),
673
+ notFound: (resource, id, param) => notFound(m, resource, id, param),
674
+ reply: (body, status = 200) => Response.json(body, { status }),
675
+ wrap: (body, extra = {}) => Response.json({ ...envelope(m, call.operation, body), ...extra }),
676
+ sse: (events) => sse(m, events, call.operation.id),
677
+ expand: (resource, body) => (paths.length ? expand(m, resource, body, paths, root) : body),
678
+ at: (when) => contextFor(m, { ...call, request: new Request(call.request.url) }, { ...scope, clock: () => when }),
679
+ core: async () => { const out = await serveCore(m, call, { ...scope, clock: () => at }); return 'served' in out ? out.served : undefined; },
680
+ own: (row) => ownFields(row),
681
+ };
682
+ }
683
+ /** A pack's semantics handlers as the dispatch's handlers. */
684
+ export function bindSemantics(m, handlers, scope = {}) {
685
+ return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call) => h(await contextFor(m, call, scope))]));
686
+ }
687
+ /** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
688
+ export function coreFor(m, scope = {}) {
689
+ const crud = new Set(['create', 'retrieve', 'list', 'update', 'delete']);
690
+ const owns = (o) => {
691
+ const decl = o.resource !== undefined ? m.resources[o.resource] : undefined;
692
+ if (!decl || m.unmodeled?.includes(o.id))
693
+ return false;
694
+ if (crud.has(o.class))
695
+ return true;
696
+ return o.class === 'action' && Object.values(decl.state ?? {}).some((f) => f.transitions.some((t) => t.operation === o.id));
697
+ };
698
+ return { owns, serve: (call) => serveCore(m, call, scope) };
699
+ }
700
+ // ── what every moved operation gets ──────────────────────────────────────────────────────────
701
+ const hex = (s) => Array.from(new TextEncoder().encode(s), (b) => b.toString(16).padStart(2, '0')).join('');
702
+ /** Read-only refusal, API-version validation and idempotent replay, applied once around every
703
+ * operation a handler or the core serves. */
704
+ export function crossCutting(m, opts = {}) {
705
+ const finish = async (r, request) => {
706
+ if (!m.origin || !(r.headers.get('content-type') ?? '').includes('json'))
707
+ return withHeaders(r);
708
+ const text = (await r.text()).replaceAll(m.origin.placeholder, twinPublicBase(request));
709
+ return withHeaders(new Response(text, { status: r.status, statusText: r.statusText, headers: r.headers }));
710
+ };
711
+ const withHeaders = (r) => {
712
+ if (!m.answerHeaders)
713
+ return r;
714
+ const headers = new Headers(r.headers);
715
+ for (const [k, v] of Object.entries(m.answerHeaders))
716
+ if (!headers.has(k))
717
+ headers.set(k, v);
718
+ return new Response(r.body, { status: r.status, statusText: r.statusText, headers });
719
+ };
720
+ return async (call, next) => finish(await guarded(call, next), call.request);
721
+ async function guarded(call, next) {
722
+ const { request } = call;
723
+ if (m.auth) {
724
+ const raw = request.headers.get(m.auth.header);
725
+ if (raw !== null || m.auth.gateWhenAbsent) {
726
+ const scheme = `${m.auth.scheme.toLowerCase()} `;
727
+ const key = raw && raw.toLowerCase().startsWith(scheme) ? raw.slice(scheme.length).trim() : '';
728
+ if (!key)
729
+ return vendorError(m, m.auth.missing);
730
+ if (m.auth.invalidKeys.includes(key) || (m.auth.keyFormat && !new RegExp(m.auth.keyFormat).test(key)))
731
+ return vendorError(m, m.auth.invalid);
732
+ }
733
+ }
734
+ // a read-only twin refuses writes: what the operation does, not the HTTP verb it came by (an RPC
735
+ // wire POSTs its reads)
736
+ if (opts.readOnly && !['retrieve', 'list', 'computed'].includes(call.operation.class) && !m.reads?.includes(call.operation.id))
737
+ return vendorError(m, m.readOnly);
738
+ // a body labelled JSON that does not parse is the vendor's refusal, never a crash of the twin
739
+ if ((request.headers.get('content-type') ?? '').includes('json') || m.body.json === 'always') {
740
+ const text = request.method === 'GET' || request.method === 'HEAD' ? '' : await request.clone().text().catch(() => '');
741
+ if (text) {
742
+ try {
743
+ JSON.parse(text);
744
+ }
745
+ catch {
746
+ return vendorError(m, m.malformedBody ?? { status: 400, message: 'The request body could not be parsed as JSON.' });
747
+ }
748
+ }
749
+ }
750
+ if (m.version) {
751
+ const value = request.headers.get(m.version.header);
752
+ if (value && !new RegExp(m.version.pattern).test(value))
753
+ return vendorError(m, { ...m.version.error, message: String(fill(m.version.error.message, { value })) });
754
+ }
755
+ const key = m.idempotency ? request.headers.get(m.idempotency.header) : null;
756
+ if (!m.idempotency || !key || !(m.idempotency.methods ?? ['POST']).includes(request.method.toUpperCase()))
757
+ return next();
758
+ const url = new URL(request.url);
759
+ const signature = hashFieldValue({ path: url.pathname + url.search, body: await request.clone().text() });
760
+ const id = `idem_${hex(key)}`;
761
+ const prior = twinResources(m.service, opts.root).find((r) => r.type === m.idempotency.storedAs && r.id === id);
762
+ if (prior) {
763
+ if (m.idempotency.conflict && prior.requestHash !== undefined && prior.requestHash !== signature)
764
+ return vendorError(m, m.idempotency.conflict);
765
+ const answer = prior.response;
766
+ // a streamed answer replays as the stream it was; a record kept before text was kept replays its JSON
767
+ return answer.text !== undefined ? new Response(answer.text, { status: answer.status, headers: { 'content-type': answer.contentType ?? 'application/json' } }) : Response.json(answer.body, { status: answer.status });
768
+ }
769
+ const response = await next();
770
+ if (m.idempotency.onlySuccess && (response.status < 200 || response.status >= 300))
771
+ return response;
772
+ const contentType = response.headers.get('content-type') ?? 'application/json';
773
+ const text = await response.clone().text();
774
+ await applyTwinWrite(m.service, { operation: 'idempotency.record', subjectType: m.idempotency.storedAs, subjectId: id, fields: { response: { status: response.status, text, contentType }, requestHash: signature }, occurredAt: (opts.clock ?? worldNow)(), actor: { kind: 'system' } }, opts.root);
775
+ return response;
776
+ }
777
+ }
778
+ /** A semantics context for a request no surface operation names: another wire's (GraphQL) resolvers
779
+ * get the same interface as a handler, named by the operation id the wire gives. */
780
+ export function semanticsContext(m, request, operation, scope = {}) {
781
+ return contextFor(m, { request, operation, params: {} }, scope);
782
+ }