@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,1004 @@
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
+
8
+ import { applyTwinWrite, applyTwinWriteAtomic, twinResources, type TwinResource } from './serve.ts';
9
+ import { copyResource, ownFields, parentStamp, subjectHistory, treeChangesSince, treeStamp } from './log.ts';
10
+ import { getActiveWorldStore } from './world-store.ts';
11
+ import { worldNow } from './world-clock.ts';
12
+ import { createHash } from 'node:crypto';
13
+ import { hashFieldValue } from './hash.ts';
14
+ import { resolveSubjectId, subjectAliases } from './actions.ts';
15
+ import { packReferences } from './references.ts';
16
+ import type { SubjectFields } from './hash.ts';
17
+ import { twinPublicBase } from './twin-fetch.ts';
18
+ import type { DerivedCall, DerivedCoreOutcome, DerivedHandler, DerivedOperation } from './derived.ts';
19
+
20
+ // ── the manifest ─────────────────────────────────────────────────────────────────────────────
21
+
22
+ /** How a stored field gets its value when the server assigns it. `now` is the world clock in the
23
+ * manifest's time format; `id` the subject's id; a `value` is stored as given. */
24
+ export type FieldRule = { now: true } | { id: true } | { value: unknown } | { template: string };
25
+
26
+ /** A move of one state field, as data. `operation` is the operationId that causes it (an update
27
+ * that requests `to` when absent); `from` the values it leaves; `effects` the other fields it writes;
28
+ * `refusal` what the vendor answers when `from` does not hold. */
29
+ /** Who moves a state: a caller of the API (the default), the person on a vendor's hosted page or
30
+ * another outside party (`external`), the vendor on its own (`vendor`), or time (`time`). A move
31
+ * that is not the API's never answers an API call's legality. */
32
+ export type Actor = 'api' | 'external' | 'vendor' | 'time';
33
+
34
+ export type Transition = {
35
+ operation?: string;
36
+ actor?: Actor;
37
+ from: string[] | '*';
38
+ /** the value it moves to; absent, the value stays (a guard that moves nothing) */
39
+ to?: string;
40
+ effects?: Record<string, FieldRule>;
41
+ /** placeholders `{from}`, `{to}`, `{field}`, `{id}` */
42
+ refusal?: { status: number; code?: string; message: string };
43
+ /** a refusal of its own for a particular current value (an invoice already `paid`) */
44
+ refusals?: Record<string, { status: number; code?: string; message: string }>;
45
+ /** where the rule comes from: a docs URL, the SDK's types, or a recording */
46
+ source: string;
47
+ };
48
+
49
+ /** A state field's machine. Values compare as strings, so a boolean field reads `'true'`/`'false'`, and
50
+ * a field that is absent reads as `initial`. `derive` names states a stored value only implies (a
51
+ * timestamp that is 0 until something happens), tried in order. `vendorInitial` is where the vendor
52
+ * starts a subject when the twin starts it elsewhere. */
53
+ export type StateField = {
54
+ initial: string | boolean;
55
+ transitions: Transition[];
56
+ derive?: Array<{ state: string; when: { equals?: unknown; gt?: number; truthy?: boolean; absent?: boolean } }>;
57
+ vendorInitial?: string;
58
+ };
59
+
60
+ /** What a move to `to` stores in the field: a boolean field's value is a boolean, and a derived field's
61
+ * value is written by the move's effects (a state name is not a timestamp), so it stores nothing itself. */
62
+ export function storedState(decl: StateField, to: string | undefined): unknown {
63
+ if (to === undefined || decl.derive?.length) return undefined;
64
+ return typeof decl.initial === 'boolean' ? to === 'true' : to;
65
+ }
66
+
67
+ /** The state a stored value is in, by the field's machine. */
68
+ export function stateOf(decl: StateField, value: unknown): string {
69
+ for (const d of decl.derive ?? []) {
70
+ const w = d.when;
71
+ if (w.absent !== undefined && (value === undefined || value === null) === w.absent) return d.state;
72
+ if (w.equals !== undefined && value === w.equals) return d.state;
73
+ if (w.gt !== undefined && typeof value === 'number' && value > w.gt) return d.state;
74
+ if (w.truthy !== undefined && Boolean(value) === w.truthy) return d.state;
75
+ }
76
+ return value === undefined || value === null ? String(decl.initial) : String(value);
77
+ }
78
+
79
+ export type ResourceDecl = {
80
+ /** the tree's subject type, when it differs from the resource name */
81
+ storedAs?: string;
82
+ idPrefix: string;
83
+ /** fields the server assigns on create */
84
+ assigned?: Record<string, FieldRule>;
85
+ state?: Record<string, StateField>;
86
+ /** state candidates the manifest rules are not state */
87
+ notState?: string[];
88
+ /** id-holding fields a caller may ask to receive embedded, and the resource each holds */
89
+ embeds?: Record<string, string>;
90
+ /** this resource's deletion answer, when it is not the manifest's (`vector_store.deleted`) */
91
+ deleted?: unknown;
92
+ /** a child addressed under its parent: the path parameter naming the parent, the stored field that
93
+ * holds it, and the parent resource, whose not-found answers a missing parent */
94
+ /** `param` is the path parameter naming the parent, or `params` several with `value` the template
95
+ * that joins them into what `field` stores (`{owner}/{repo}`). `where` finds the parent by its stored
96
+ * fields when its stored id is not what the path names (a repository stored as `repo:7` with `owner`
97
+ * and `name`, addressed as `{owner}/{repo}`): each stored field against a template over the path. */
98
+ parent?: { param?: string; params?: string[]; value?: string; field: string; resource: string; allowDeleted?: boolean; where?: Record<string, string> };
99
+ /** a number the resource counts per parent (a repository's issue numbers, a thread's messages):
100
+ * the stored field, set on create to one past the highest among its siblings */
101
+ number?: { field: string };
102
+ /** how this resource renders a stored subject, when it differs from the pack's; it sees the subject's
103
+ * address, which a stored shape may not carry */
104
+ view?: (body: Record<string, unknown>, subject: { id: string; type: string }) => Record<string, unknown>;
105
+ /** an update replaces the fields it names (`replace`) or merges nested objects into them (`merge`, the default) */
106
+ update?: 'merge' | 'replace';
107
+ /** the stored subject id when it is not the path's last parameter: a template over the path
108
+ * parameters (`{vector_store_id}::{file_id}`) */
109
+ key?: string;
110
+ /** only subjects whose stored fields match are readable (a resource stored for every call but
111
+ * answered only when asked to be kept) */
112
+ readableWhen?: Record<string, unknown>;
113
+ /** this resource's not-found answer when it is not the manifest's: a message (`{id}`), or an
114
+ * error code (an RPC vendor's `channel_not_found`) */
115
+ notFound?: string | { code: string; message?: string };
116
+ /** query parameters of the list operation that filter on the same-named field */
117
+ filters?: string[];
118
+ order?: { field: string; direction: 'asc' | 'desc' };
119
+ };
120
+
121
+ export type ErrorSpec = { status: number; message: string; code?: string; param?: string; kind?: string };
122
+
123
+ /** A vendor screen a World serves (docs/contributing/architecture.md, "Screens"). A `flow` is a hosted
124
+ * page an application sends its user through, held to its documented round trip; a `workspace` is a
125
+ * screen people work in. `demand` says who reaches it; `controls` names what a person acts on, the
126
+ * vendor's way, each a (screen, control) cell. */
127
+ export type ScreenDecl = {
128
+ id: string;
129
+ kind: 'flow' | 'workspace';
130
+ host: string;
131
+ path: string;
132
+ demand: string;
133
+ status: 'done' | 'todo';
134
+ controls?: string[];
135
+ /** the vendor's documentation of the round trip or the screen */
136
+ source: string;
137
+ };
138
+
139
+ export type DerivedManifest = {
140
+ vendor: string;
141
+ /** the kernel service the pack writes */
142
+ service: string;
143
+ /** `json: 'always'` reads a body as JSON whatever its content type (GitHub does) */
144
+ body: { form?: { coerce: boolean }; json?: 'always' };
145
+ /** `acceptProvided`: a create that names its own `id` keeps it (a vendor that seeds by id). The template counts
146
+ * (`{prefix}_{n}`), or is `{uuid}` for a vendor whose ids are UUIDs: the next is derived from the resource and its
147
+ * count, so a World mints the same ids every run */
148
+ ids: { template: string; acceptProvided?: boolean };
149
+ /** operations the derived core must not claim though their resource is declared: served by
150
+ * the pack's existing code, or a gap, until someone models them */
151
+ unmodeled?: string[];
152
+ /** operations that only read though nothing in the spec says so (a POST that returns data):
153
+ * a read-only twin answers them */
154
+ reads?: string[];
155
+ time: 'unix' | 'iso';
156
+ /** the vendor's error body, with `{message}`, `{code}`, `{param}`, `{kind}` placeholders; a
157
+ * placeholder with no value is left out when `errorOmitsAbsent` */
158
+ error: unknown;
159
+ errorOmitsAbsent?: boolean;
160
+ /** the `{kind}` an error takes when it names none */
161
+ defaultKind?: string;
162
+ /** what a write to a read-only twin answers */
163
+ readOnly: ErrorSpec;
164
+ /** what a body labelled JSON that does not parse answers; absent, a 400 in the vendor's error body */
165
+ malformedBody?: ErrorSpec;
166
+ /** a request header that selects the vendor's API version, and the answer to a malformed one
167
+ * (`{value}` in its message is the header's value) */
168
+ version?: { header: string; pattern: string; error: ErrorSpec };
169
+ /** `withParam`: the error names the path parameter that held the unknown id */
170
+ notFound: { status: number; message: string; code?: string; kind?: string; withParam?: boolean };
171
+ /** the list envelope (placeholders `{data}`, `{has_more}`, `{url}`, `{first_id}`, `{last_id}`),
172
+ * its page size, its cursors, and where an unknown cursor starts: the first page or past the end */
173
+ list: {
174
+ style: 'envelope';
175
+ envelope: unknown;
176
+ limit: { param: string; default: number; max: number };
177
+ after?: string;
178
+ before?: string;
179
+ unknownCursor?: 'start' | 'end';
180
+ /** an opaque cursor over positions (`{next_cursor}` in the envelope; empty on the last page),
181
+ * in place of id cursors: `base64-offset` is base64 of `{"o":<offset>}` */
182
+ cursor?: { param: string; encoding: 'base64-offset' };
183
+ /** numbered pages (1-based `page`, the size in `limit.param`), with the vendor's `Link` header
184
+ * naming the first, previous, next and last pages */
185
+ page?: { param: string; link: boolean };
186
+ /** offset paging: `param` names how many items to skip; the page sits under the operation's envelope key beside the
187
+ * envelope, whose `{total_count}` is the count before paging (Tremendous's `{ orders: [...], total_count }`) */
188
+ offset?: { param: string };
189
+ /** page sizes by operationId, where a vendor's lists differ from `limit` (Tremendous: orders 10/500, invoices 10/10) */
190
+ limits?: Record<string, { default: number; max: number }>;
191
+ /** envelopes by operationId, where a vendor's lists differ from `envelope` (Tremendous gives `total_count` on orders,
192
+ * rewards and invoices, and none on campaigns, funding sources or webhooks) */
193
+ envelopes?: Record<string, unknown>;
194
+ };
195
+ /** an RPC vendor's success envelope, merged into every answer, with the resource under the
196
+ * operation's envelope key (`{ ok: true, channel: {...} }`) */
197
+ success?: Record<string, unknown>;
198
+ /** who is calling, from the request's credential (and the tree, where the vendor keeps tokens) */
199
+ identity?: (authorization: string | null, root?: string) => string;
200
+ deleted: unknown;
201
+ expandParam?: string;
202
+ /** server-sent events: `data` lines, optionally `event:` lines named by each event, and the
203
+ * sentinel a finished stream ends with */
204
+ sse?: { named: boolean; done?: string };
205
+ /** an operation whose stream is framed otherwise (named events, no sentinel) */
206
+ streamFor?: Record<string, { named: boolean; done?: string }>;
207
+ /** the vendor's idempotency header: a write repeated with the same key answers the stored
208
+ * answer; the same key with other parameters answers `conflict` */
209
+ /** `methods` defaults to POST; `onlySuccess` (replay only 2xx answers) to false; without
210
+ * `conflict`, a key replays its first answer whatever the parameters */
211
+ idempotency?: { header: string; storedAs: string; conflict?: ErrorSpec; methods?: string[]; onlySuccess?: boolean };
212
+ /** how the vendor reads its credential, and what it answers without one or with a key the twin
213
+ * reserves as invalid, or one of a shape the vendor never issues (`keyFormat`, a pattern every key it issues
214
+ * matches: Resend's begin `re_`); a request that presents no such header at all is a trusted in-process call */
215
+ auth?: { header: string; scheme: string; missing: ErrorSpec; invalidKeys: string[]; keyFormat?: string; invalid: ErrorSpec; gateWhenAbsent: boolean };
216
+ /** the twin door that makes the next request answer the vendor's rate-limit refusal */
217
+ /** headers every answer carries (a request id) */
218
+ answerHeaders?: Record<string, string>;
219
+ /** an origin the pack writes into URLs it mints, rewritten in every JSON answer to where this
220
+ * twin is reached for the request (twinPublicBase) */
221
+ origin?: { placeholder: string };
222
+ /** while a pack moves, how its existing code renders a stored subject (named by its stored
223
+ * type), so moved and unmoved operations answer the same shape */
224
+ view?: (storedType: string, body: Record<string, unknown>) => Record<string, unknown>;
225
+ resources: Record<string, ResourceDecl>;
226
+ /** the vendor's screens this pack serves or owes (demand decides which exist) */
227
+ screens?: ScreenDecl[];
228
+ /** called after every stored write with the rendered resource, its stored type and the kernel operation name */
229
+ onWrite?: (write: { operation: string; storedType: string; body: Record<string, unknown>; root?: string; occurredAt: string; request: Request }) => Promise<void>;
230
+ };
231
+
232
+ // ── request parsing ──────────────────────────────────────────────────────────────────────────
233
+
234
+ /** A form body's bracket notation (`a[b][0][c]=v`, `expand[]=x`) as nested objects and arrays. A form carries only
235
+ * text: a field the operation's spec types as a number or a boolean (`scalars`, by bracket path, written by
236
+ * world-tooling's formScalarsOf) is read as one when its text is that literal; every other field stays the text sent
237
+ * (`metadata[order]=007`, `name=2024`). */
238
+ export function parseBracketForm(text: string, scalars?: Readonly<Record<string, string | undefined>>): Record<string, unknown> {
239
+ const out: Record<string, unknown> = {};
240
+ for (const [rawKey, raw] of new URLSearchParams(text)) {
241
+ const parts = rawKey.replace(/\]/g, '').split('[');
242
+ const kind = scalars && scalarAt(scalars, parts);
243
+ const value: unknown = kind === 'boolean' ? (raw === 'true' ? true : raw === 'false' ? false : raw)
244
+ : kind === 'integer' ? (/^-?\d+$/.test(raw) ? Number(raw) : raw)
245
+ : kind === 'number' ? (/^-?\d+(\.\d+)?$/.test(raw) ? Number(raw) : raw)
246
+ : raw;
247
+ let node: any = out;
248
+ parts.forEach((key, i) => {
249
+ if (i === parts.length - 1) {
250
+ if (key === '') {
251
+ if (Array.isArray(node)) node.push(value);
252
+ } else node[key] = value;
253
+ return;
254
+ }
255
+ const next = parts[i + 1]!;
256
+ if (node[key] === undefined) node[key] = next === '' || /^\d+$/.test(next) ? [] : {};
257
+ node = node[key];
258
+ });
259
+ }
260
+ return out;
261
+ }
262
+
263
+ /** The type `scalars` gives a form key's parts: a segment is a property name, `[]` an array's item (an index or empty),
264
+ * `*` a map's key. */
265
+ function scalarAt(scalars: Readonly<Record<string, string | undefined>>, parts: string[]): string | undefined {
266
+ let paths = [''];
267
+ for (const [i, part] of parts.entries()) {
268
+ const segs = part === '' || /^\d+$/.test(part) ? ['[]', '*'] : [part, '*'];
269
+ paths = paths.flatMap((p) => segs.map((seg) => (i === 0 ? seg : `${p}.${seg}`)));
270
+ if (paths.length > 64) paths = paths.filter((p) => Object.keys(scalars).some((k) => k === p || k.startsWith(`${p}.`)));
271
+ }
272
+ for (const p of paths) if (scalars[p]) return scalars[p];
273
+ return undefined;
274
+ }
275
+
276
+ /** A request's parameters, its query's and its body's. With the manifest's `body.form.coerce`, a form's fields the
277
+ * operation's spec types as numbers or booleans are read as them (parseBracketForm); without an operation (a twin door)
278
+ * every form value is its text. */
279
+ export async function readParams(manifest: DerivedManifest, request: Request, operation?: DerivedOperation): Promise<Record<string, unknown>> {
280
+ const url = new URL(request.url);
281
+ const scalars = manifest.body.form?.coerce ? (operation?.scalars ?? {}) : undefined;
282
+ const query = parseBracketForm(url.search.replace(/^\?/, ''), scalars);
283
+ if (request.method === 'GET' || request.method === 'HEAD') return query;
284
+ const type = request.headers.get('content-type') ?? '';
285
+ if (type.includes('multipart/form-data')) {
286
+ // an upload: text fields as given, each file as its name, media type, size and content (its text), with its raw
287
+ // `bytes` beside them for a pack that must tell an image or audio file from anything else; the bytes are not
288
+ // enumerated, so a write's record of the request keeps the text alone
289
+ const form = await request.formData();
290
+ const out: Record<string, unknown> = { ...query };
291
+ for (const [key, value] of form.entries()) {
292
+ if (typeof value === 'string') {
293
+ out[key] = value;
294
+ continue;
295
+ }
296
+ const file = value as unknown as { name: string; type: string; size: number; arrayBuffer(): Promise<ArrayBuffer> };
297
+ const bytes = new Uint8Array(await file.arrayBuffer());
298
+ const parsed = { name: file.name, type: file.type, size: file.size, content: new TextDecoder().decode(bytes) };
299
+ out[key] = Object.defineProperty(parsed, 'bytes', { value: bytes, enumerable: false });
300
+ }
301
+ return out;
302
+ }
303
+ const text = await request.text();
304
+ if (!text) return query;
305
+ const asJson = type.includes('json') || manifest.body.json === 'always';
306
+ const body: unknown = asJson ? JSON.parse(text) : parseBracketForm(text, scalars);
307
+ // a body that is not an object (GitHub's set-labels takes a bare array) is the body, not fields
308
+ return body && typeof body === 'object' && !Array.isArray(body) ? { ...query, ...(body as Record<string, unknown>) } : { ...query };
309
+ }
310
+
311
+ // ── answers ──────────────────────────────────────────────────────────────────────────────────
312
+
313
+ const ABSENT = Symbol('absent');
314
+
315
+ function fill(template: unknown, values: Record<string, unknown>, omitAbsent = false): unknown {
316
+ if (typeof template === 'string') {
317
+ const whole = /^\{(\w+)\}$/.exec(template);
318
+ if (whole) return values[whole[1]!] ?? (omitAbsent ? ABSENT : null);
319
+ return template.replace(/\{(\w+)\}/g, (_, k: string) => String(values[k] ?? ''));
320
+ }
321
+ if (Array.isArray(template)) return template.map((t) => fill(t, values, omitAbsent)).filter((v) => v !== ABSENT);
322
+ if (template && typeof template === 'object') {
323
+ return Object.fromEntries(Object.entries(template).map(([k, v]) => [k, fill(v, values, omitAbsent)] as const).filter(([, v]) => v !== ABSENT));
324
+ }
325
+ return template;
326
+ }
327
+
328
+ export function vendorError(manifest: DerivedManifest, e: ErrorSpec): Response {
329
+ // the status as the answer's number, and as text for a vendor whose body spells it as a string (GitHub's "404")
330
+ 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) };
331
+ // a vendor that answers its errors as a document, not JSON (S3's XML `<Error><Code>…`): the template is that text,
332
+ // each value escaped into it
333
+ if (typeof manifest.error === 'string') {
334
+ const escape = (v: unknown): string => String(v ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
335
+ const body = manifest.error.replace(/\{(\w+)\}/g, (_, k: string) => escape(values[k as keyof typeof values]));
336
+ return new Response(body, { status: e.status, headers: { 'content-type': body.trimStart().startsWith('<') ? 'application/xml' : 'text/plain; charset=utf-8' } });
337
+ }
338
+ return Response.json(fill(manifest.error, values, manifest.errorOmitsAbsent ?? false), { status: e.status });
339
+ }
340
+
341
+ function notFound(manifest: DerivedManifest, object: string, id: string, param?: string): Response {
342
+ const nf = manifest.notFound;
343
+ if (!nf.withParam) param = undefined;
344
+ const own = manifest.resources[object]?.notFound;
345
+ const message = typeof own === 'string' ? own : (own?.message ?? nf.message);
346
+ const code = typeof own === 'object' ? own.code : nf.code;
347
+ return vendorError(manifest, { status: nf.status, message: String(fill(message, { object, id })), ...(code ? { code } : {}), ...(nf.kind ? { kind: nf.kind } : {}), ...(param ? { param } : {}) });
348
+ }
349
+
350
+ // ── the tree ─────────────────────────────────────────────────────────────────────────────────
351
+
352
+ const storedType = (m: DerivedManifest, resource: string): string => m.resources[resource]?.storedAs ?? resource;
353
+
354
+ // A service's resources by type, folded once per World state (the tree's stamp; any write moves it), not on every read:
355
+ // a read of one resource type projected the service's whole tree, so a pack whose tree also holds many subjects of
356
+ // other types (PlanetScale's SQL rows beside its management records) paid for all of them on each read. A read gets its
357
+ // own copy of each resource, as twinResources hands out, with the vendor's own id/type/updatedAt a spread would drop.
358
+ const typeIndexes = new WeakMap<object, Map<string, { stamp: string; byType: Map<string, Map<string, TwinResource>> }>>();
359
+ /** A service's resources of one type, each the reader's own copy: `twinResources` filtered by type, from an index
360
+ * that follows each write rather than projecting the whole tree per call. */
361
+ export function resourcesOfType(service: string, type: string, root?: string): TwinResource[] {
362
+ const store = getActiveWorldStore();
363
+ const indexes = typeIndexes.get(store) ?? typeIndexes.set(store, new Map()).get(store)!;
364
+ const key = `${service}\u0000${root ?? ''}`;
365
+ let held = indexes.get(key);
366
+ // a write moves the stamp; the index follows it by the subjects the write touched, and refolds only when
367
+ // the kernel cannot say which (a projection of the whole tree after every write grew with the World)
368
+ const delta = held ? treeChangesSince(service, root, held.stamp) : undefined;
369
+ if (held && delta) {
370
+ // the index keeps the tree's order: a subject updated where it stands stays there, one new to the tree (or made
371
+ // again after a delete) goes to the end in the order the tree holds it
372
+ for (const k of [...delta.removed, ...delta.appended]) held.byType.get(k.slice(0, k.indexOf(':')))?.delete(k);
373
+ const byKey = new Map(delta.changed.map((r) => [`${r.type}:${r.id}`, r]));
374
+ for (const [k, r] of byKey) if (!delta.appended.includes(k)) (held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)!).set(k, r);
375
+ for (const k of delta.appended) { const r = byKey.get(k)!; (held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)!).set(k, r); }
376
+ held.stamp = delta.stamp;
377
+ } else if (!held || held.stamp !== treeStamp(service, root)) {
378
+ // the stamp is read before the fold, so a write between them can only leave a newer fold under an older stamp
379
+ const stamp = treeStamp(service, root);
380
+ const byType = new Map<string, Map<string, TwinResource>>();
381
+ for (const r of twinResources(service, root)) (byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)!).set(`${r.type}:${r.id}`, r);
382
+ held = { stamp, byType };
383
+ indexes.set(key, held);
384
+ }
385
+ return [...(held.byType.get(type)?.values() ?? [])].map(copyResource);
386
+ }
387
+
388
+ function stored(m: DerivedManifest, resource: string, root?: string, opts: { withDeleted?: boolean } = {}): TwinResource[] {
389
+ const type = storedType(m, resource);
390
+ const when = Object.entries(m.resources[resource]?.readableWhen ?? {});
391
+ return resourcesOfType(m.service, type, root).filter((r) => (opts.withDeleted || r.deleted !== true) && when.every(([k, v]) => (r as Record<string, unknown>)[k] === v));
392
+ }
393
+
394
+ /** The subject an item operation names: the manifest's key over the path parameters, or the last one. */
395
+ function subjectOf(m: DerivedManifest, resource: string, call: DerivedCall): string | undefined {
396
+ const key = m.resources[resource]?.key;
397
+ return key ? String(fill(key, call.params)) : Object.values(call.params).at(-1);
398
+ }
399
+
400
+ /** A missing parent's not-found, when the resource lives under one. */
401
+ /** The parent value a call names: one path parameter, or several joined by the parent's template. */
402
+ function parentValue(parent: NonNullable<ResourceDecl['parent']>, params: Record<string, string>): string | undefined {
403
+ if (parent.params) return String(fill(parent.value ?? parent.params.map((p) => `{${p}}`).join('/'), params));
404
+ return parent.param !== undefined ? params[parent.param] : undefined;
405
+ }
406
+
407
+ function missingParent(m: DerivedManifest, resource: string, call: DerivedCall, root?: string): Response | undefined {
408
+ const parent = m.resources[resource]?.parent;
409
+ if (!parent) return undefined;
410
+ const id = parentValue(parent, call.params);
411
+ const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))] as const);
412
+ const found = (r: TwinResource): boolean => (where.length ? where.every(([field, v]) => String((r as Record<string, unknown>)[field]) === v) : r.id === id);
413
+ 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));
414
+ }
415
+
416
+ /** The vendor's view of a stored subject: its own fields, bookkeeping (`_`) left out. */
417
+ /** A stored subject as the vendor's object: its id first (the subject's, unless the pack kept an id of its own), then
418
+ * the fields the pack wrote, bookkeeping (`_`) and the kernel's updatedAt left out. */
419
+ export function render(r: TwinResource): Record<string, unknown> {
420
+ return Object.fromEntries(Object.entries({ id: r.id, ...ownFields(r) }).filter(([k]) => !k.startsWith('_') && k !== 'updatedAt'));
421
+ }
422
+
423
+ function view(m: DerivedManifest, resource: string, r: TwinResource): Record<string, unknown> {
424
+ const own = m.resources[resource]?.view;
425
+ if (own) return own(render(r), { id: r.id, type: r.type });
426
+ if (!m.view) return render(r);
427
+ // a pack's view sees the stored row whole, and bookkeeping (`_`) never reaches the wire whatever it returns
428
+ return Object.fromEntries(Object.entries(m.view(storedType(m, resource), r as Record<string, unknown>)).filter(([k]) => !k.startsWith('_')));
429
+ }
430
+
431
+ /** Where the core runs: the World root it reads and writes, and its clock (the World's by default;
432
+ * a caller that pins an instant passes its own). */
433
+ export type CoreScope = { root?: string; clock?: () => string };
434
+
435
+ function now(m: DerivedManifest, at: string): unknown {
436
+ return m.time === 'unix' ? Math.floor(Date.parse(at) / 1000) : at;
437
+ }
438
+
439
+ function applyRule(m: DerivedManifest, rule: FieldRule, id: string, values: Record<string, unknown>, at: string): unknown {
440
+ if ('now' in rule) return now(m, at);
441
+ if ('id' in rule) return id;
442
+ if ('value' in rule) return rule.value;
443
+ return fill(rule.template, { ...values, id });
444
+ }
445
+
446
+ const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
447
+
448
+ /** The next id the manifest's template gives this resource: one past the highest it already minted. */
449
+ function mintId(m: DerivedManifest, resource: string, root?: string): string {
450
+ const prefix = m.resources[resource]!.idPrefix;
451
+ if (m.ids.template === '{uuid}') return mintUuid(m, resource, root);
452
+ const [head, tail = ''] = m.ids.template.split('{n}');
453
+ const pattern = new RegExp(`^${escapeRe(String(fill(head!, { prefix })))}(\\d+)${escapeRe(String(fill(tail, { prefix })))}$`);
454
+ let max = 0;
455
+ // the one type, from the index that follows writes (a scan of the whole World per create grew with it)
456
+ for (const r of resourcesOfType(m.service, storedType(m, resource), root)) {
457
+ const hit = pattern.exec(r.id);
458
+ if (hit) max = Math.max(max, Number(hit[1]));
459
+ }
460
+ return String(fill(m.ids.template, { prefix, n: max + 1 }));
461
+ }
462
+
463
+ /** The next UUID for a resource: a version-4-shaped UUID derived from the resource and the count before it, past any
464
+ * the World already holds. */
465
+ function mintUuid(m: DerivedManifest, resource: string, root?: string): string {
466
+ const type = storedType(m, resource);
467
+ const held = new Set(resourcesOfType(m.service, type, root).map((r) => r.id));
468
+ for (let n = held.size + 1; ; n++) {
469
+ const h = createHash('sha256').update(`${m.service}:${resource}:${n}`).digest('hex');
470
+ 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)}`;
471
+ if (!held.has(id)) return id;
472
+ }
473
+ }
474
+
475
+ function expand(m: DerivedManifest, resource: string, body: Record<string, unknown>, paths: string[], root?: string): Record<string, unknown> {
476
+ const embeds = m.resources[resource]?.embeds ?? {};
477
+ const out = { ...body };
478
+ for (const path of paths) {
479
+ const [head, ...rest] = path.split('.');
480
+ const target = embeds[head!];
481
+ const id = out[head!];
482
+ if (!target || typeof id !== 'string') continue;
483
+ const hit = stored(m, target, root).find((r) => r.id === id);
484
+ if (hit) out[head!] = rest.length ? expand(m, target, view(m, target, hit), [rest.join('.')], root) : view(m, target, hit);
485
+ }
486
+ return out;
487
+ }
488
+
489
+ function expandPaths(m: DerivedManifest, params: Record<string, unknown>): string[] {
490
+ const raw = m.expandParam ? params[m.expandParam] : undefined;
491
+ return Array.isArray(raw) ? raw.map(String) : typeof raw === 'string' ? [raw] : [];
492
+ }
493
+
494
+ async function write(m: DerivedManifest, call: DerivedCall, resource: string, id: string, fields: Record<string, unknown>, operation: string, params: Record<string, unknown>, root: string | undefined, occurredAt: string): Promise<Record<string, unknown>> {
495
+ return (await writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt)).body;
496
+ }
497
+
498
+ async function writeDetailed(m: DerivedManifest, call: DerivedCall, resource: string, id: string, fields: Record<string, unknown>, operation: string, params: Record<string, unknown>, root: string | undefined, occurredAt: string): Promise<{ body: Record<string, unknown>; id: string; vendorData?: unknown }> {
499
+ const { resource: row, result } = await applyTwinWrite(
500
+ m.service,
501
+ // who made it: the login the pack's identity reads off the request, when it declares one, so a subject's
502
+ // history can say who did each thing
503
+ { 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) } : {}) } },
504
+ root,
505
+ );
506
+ const body = view(m, resource, row);
507
+ if (m.onWrite) await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request });
508
+ // the subject's id as it stands after the write: the vendor's, when a live head minted one
509
+ return { body, id: result.externalId ?? row.id, ...(result.vendorData !== undefined ? { vendorData: result.vendorData } : {}) };
510
+ }
511
+
512
+ /** An answer in the vendor's success envelope, under the operation's key, when the vendor has one. */
513
+ function envelope(m: DerivedManifest, op: DerivedOperation, body: Record<string, unknown>): Record<string, unknown> {
514
+ return m.success && op.answers?.key ? { ...m.success, [op.answers.key]: body } : body;
515
+ }
516
+
517
+ const encodeOffset = (o: number): string => btoa(JSON.stringify({ o }));
518
+ function decodeOffset(cursor: unknown): number {
519
+ if (typeof cursor !== 'string' || !cursor) return 0;
520
+ try {
521
+ const o = (JSON.parse(atob(cursor)) as { o?: unknown }).o;
522
+ return typeof o === 'number' && o >= 0 ? o : 0;
523
+ } catch {
524
+ return 0;
525
+ }
526
+ }
527
+
528
+ // ── state ────────────────────────────────────────────────────────────────────────────────────
529
+
530
+ /** What a coverage measurement sees of a pack's declared state logic: each move a request made and each refusal a
531
+ * guard gave, by the transition that decided it. Nothing is observed unless a measurement installs an observer
532
+ * (scripts/life-coverage.ts); serving never depends on it. */
533
+ export type TransitionObserver = (decl: StateField, transition: Transition, outcome: 'moved' | 'refused', from: string) => void;
534
+ let transitionObserver: TransitionObserver | undefined;
535
+ export function observeTransitions(observer: TransitionObserver | undefined): void {
536
+ transitionObserver = observer;
537
+ }
538
+
539
+ /** The transition a request asks for on one state field, or the vendor's refusal when none applies. */
540
+ /** What a declared machine says to one move: the transition that allows it, or the refusal it gives (and
541
+ * nothing when it declares neither). The derived core and `legal` ask it; so does an engine that is not
542
+ * HTTP-shaped (a line protocol's session), so one machine rules every wire. */
543
+ export function transitionFor(field: string, decl: StateField, operationId: string, current: unknown, requested: unknown, id?: string, actor: Actor = 'api'): { move?: Transition; refusal?: ErrorSpec } {
544
+ const now = stateOf(decl, current);
545
+ const candidates = decl.transitions.filter((t) => (t.actor ?? 'api') === actor && (t.operation ? t.operation === operationId : true) && (requested === undefined || (t.to ?? now) === String(requested)));
546
+ const move = candidates.find((t) => t.from === '*' || t.from.includes(now));
547
+ if (move) { transitionObserver?.(decl, move, 'moved', now); return { move }; }
548
+ const refused = candidates.find((t) => t.refusals?.[now]) ?? candidates.find((t) => t.refusal);
549
+ const r = refused?.refusals?.[now] ?? refused?.refusal;
550
+ if (refused && r) transitionObserver?.(decl, refused, 'refused', now);
551
+ if (r) return { refusal: { status: r.status, message: String(fill(r.message, { from: current, to: refused!.to ?? now, field, id: id ?? '' })), ...(r.code ? { code: r.code } : {}) } };
552
+ return {};
553
+ }
554
+
555
+ // ── the core ─────────────────────────────────────────────────────────────────────────────────
556
+
557
+ export type CoreOutcome = { served: Response } | { unmodeled: string };
558
+
559
+ // ALIAS-AWARE LOOKUP AT THE REQUEST BOUNDARY (architecture.md): after an adoption a caller may still name a subject by
560
+ // its local id; a declared reference in the call's params is resolved through the alias map once, here, where the
561
+ // params are parsed, so a handler's lookup (`ctx.row`) and the write both see the adopted id. The map is read only
562
+ // for a pack that declares references, and once per state of the parent side. An idempotency key's request hash is
563
+ // taken over the bytes as sent, so the same key sent once with the local id and once with the adopted one is two
564
+ // requests.
565
+ const aliasMemos = new WeakMap<object, Map<string, { stamp: string; aliases: Map<string, string> }>>();
566
+ function aliasesNow(service: string, root?: string): Map<string, string> {
567
+ const store = getActiveWorldStore();
568
+ const memos = aliasMemos.get(store) ?? aliasMemos.set(store, new Map()).get(store)!;
569
+ const key = `${service}\u0000${root ?? ''}`;
570
+ // aliases come from the parent side alone: a local write (the branch log) leaves them as they were
571
+ const stamp = parentStamp(service, root);
572
+ let held = memos.get(key);
573
+ if (!held || held.stamp !== stamp) { held = { stamp, aliases: subjectAliases(service, root) }; memos.set(key, held); }
574
+ return held.aliases;
575
+ }
576
+ /** The call with a path parameter naming the operation's subject, or its parent, by a local id moved to the adopted
577
+ * id (`GET /segments/<local>` after the segment was adopted). Only the operation's own type and its parent's are
578
+ * tried, so no other parameter is touched. A vendor naming its subject in the body (Slack's `ts`) declares it as a
579
+ * reference, which adoptedInCall resolves. */
580
+ function adoptedPath(m: DerivedManifest, call: DerivedCall, root?: string): DerivedCall {
581
+ const resource = call.operation.resource;
582
+ const decl = resource ? m.resources[resource] : undefined;
583
+ if (!decl || Object.keys(call.params).length === 0) return call;
584
+ const aliases = aliasesNow(m.service, root);
585
+ if (aliases.size === 0) return call;
586
+ const types = [storedType(m, resource!), ...(decl.parent ? [storedType(m, decl.parent.resource)] : [])];
587
+ let params = call.params;
588
+ for (const [name, value] of Object.entries(call.params)) {
589
+ const adopted = types.map((type) => aliases.get(`${type}:${value}`)).find((id) => id !== undefined);
590
+ if (adopted !== undefined && adopted !== value) params = { ...params, [name]: adopted };
591
+ }
592
+ return params === call.params ? call : { ...call, params };
593
+ }
594
+ /** A call's fields with each declared reference that names an adopted subject by its local id moved to the adopted
595
+ * id. A call (a handler's operation) need not say which subject type it writes, so every reference the pack declares
596
+ * is tried: a field is rewritten only when its value is a local id the alias map holds for the reference's target. */
597
+ function adoptedInCall(service: string, fields: Record<string, unknown>, root?: string): Record<string, unknown> {
598
+ const refs = packReferences(service);
599
+ if (refs.length === 0) return fields;
600
+ const aliases = aliasesNow(service, root);
601
+ if (aliases.size === 0) return fields;
602
+ let out = fields;
603
+ for (const ref of refs) {
604
+ const local = ref.key(out as SubjectFields);
605
+ const adopted = local === undefined ? undefined : aliases.get(`${ref.to}:${local}`);
606
+ if (adopted !== undefined && adopted !== local) out = { ...out, ...ref.adopt(out as SubjectFields, adopted) };
607
+ }
608
+ return out;
609
+ }
610
+ async function boundaryParams(m: DerivedManifest, call: DerivedCall, root?: string): Promise<Record<string, unknown>> {
611
+ return adoptedInCall(m.service, await readParams(m, call.request, call.operation), root);
612
+ }
613
+
614
+ /** Serve one operation from the manifest, or say why the core cannot. */
615
+ export async function serveCore(m: DerivedManifest, call: DerivedCall, scope: CoreScope = {}): Promise<CoreOutcome> {
616
+ const root = scope.root;
617
+ const at = (scope.clock ?? worldNow)();
618
+ const op: DerivedOperation = call.operation;
619
+ const resource = op.resource;
620
+ const decl = resource ? m.resources[resource] : undefined;
621
+ if (!resource || !decl) return { unmodeled: `no manifest entry for resource ${resource ?? '(none)'}` };
622
+ call = adoptedPath(m, call, root);
623
+ const params = await boundaryParams(m, call, root);
624
+ const idParam = subjectOf(m, resource, call);
625
+ const orphan = missingParent(m, resource, call, root);
626
+ if (orphan) return { served: orphan };
627
+ const parent = decl.parent;
628
+ const parentId = parent ? parentValue(parent, call.params) : undefined;
629
+ const underParent = (r: TwinResource): boolean => !parent || (r as Record<string, unknown>)[parent.field] === parentId;
630
+ const paths = expandPaths(m, params);
631
+ // the vendor's success status from its spec: 201 for most creates, 204 with no body where it answers nothing
632
+ const status = op.successStatus ?? 200;
633
+ const answer = (body: Record<string, unknown>) => ({
634
+ served: status === 204 ? new Response(null, { status }) : Response.json(envelope(m, op, paths.length ? expand(m, resource, body, paths, root) : body), { status }),
635
+ });
636
+ const control = new Set([m.expandParam, m.list.limit.param, m.list.after, m.list.before, m.list.offset?.param].filter(Boolean) as string[]);
637
+ const data = Object.fromEntries(Object.entries(params).filter(([k]) => !control.has(k) && k !== 'id'));
638
+
639
+ switch (op.class) {
640
+ case 'create': {
641
+ for (const field of Object.keys(decl.state ?? {})) if (field in data) return { unmodeled: `create sets state field ${field}` };
642
+ if (decl.key) return { unmodeled: 'a create under a composite key is a handler' };
643
+ const provided = m.ids.acceptProvided && typeof params.id === 'string' && params.id ? params.id : undefined;
644
+ const id = provided ?? mintId(m, resource, root);
645
+ const assigned = Object.fromEntries(Object.entries(decl.assigned ?? {}).map(([k, rule]) => [k, applyRule(m, rule, id, data, at)]));
646
+ const initial = Object.fromEntries(Object.entries(decl.state ?? {}).map(([k, s]) => [k, s.initial]));
647
+ const under = parent ? { [parent.field]: parentId } : {};
648
+ if (decl.number) {
649
+ const siblings = stored(m, resource, root, { withDeleted: true }).filter((r) => !parent || (r as Record<string, unknown>)[parent.field] === parentId);
650
+ (under as Record<string, unknown>)[decl.number.field] = 1 + siblings.reduce((n, r) => Math.max(n, Number((r as Record<string, unknown>)[decl.number!.field]) || 0), 0);
651
+ }
652
+ return answer(await write(m, call, resource, id, { ...assigned, ...initial, ...under, ...data }, `${storedType(m, resource)}.create`, params, root, at));
653
+ }
654
+ case 'retrieve': {
655
+ const hit = idParam ? stored(m, resource, root).find((r) => r.id === idParam && underParent(r)) : undefined;
656
+ return hit ? answer(view(m, resource, hit)) : { served: notFound(m, resource, String(idParam), Object.keys(call.params).at(-1)) };
657
+ }
658
+ case 'list': {
659
+ let rows = stored(m, resource, root).filter(underParent).map((r) => view(m, resource, r));
660
+ for (const f of decl.filters ?? []) if (params[f] !== undefined) rows = rows.filter((r) => String(r[f]) === String(params[f]));
661
+ const order = decl.order ?? { field: 'created', direction: 'desc' as const };
662
+ rows.sort((a, b) => {
663
+ // ties by mint order (`file-twin-10` after `file-twin-9`), not by the ids' lexical order
664
+ const d = Number(a[order.field] ?? 0) - Number(b[order.field] ?? 0) || String(a.id).localeCompare(String(b.id), undefined, { numeric: true });
665
+ return order.direction === 'desc' ? -d : d;
666
+ });
667
+ const size = m.list.limits?.[op.id] ?? m.list.limit;
668
+ const asked = params[m.list.limit.param];
669
+ const n = asked === undefined || asked === '' ? size.default : Number(asked);
670
+ const limit = Number.isFinite(n) ? Math.min(size.max, Math.max(1, Math.trunc(n))) : size.default;
671
+ const after = m.list.after ? params[m.list.after] : undefined;
672
+ const before = m.list.before ? params[m.list.before] : undefined;
673
+ let from = 0;
674
+ let to: number;
675
+ if (typeof before === 'string') {
676
+ const at = rows.findIndex((r) => r.id === before);
677
+ to = at === -1 ? (m.list.unknownCursor === 'end' ? 0 : rows.length) : at;
678
+ from = Math.max(0, to - limit);
679
+ } else {
680
+ if (typeof after === 'string') {
681
+ const at = rows.findIndex((r) => r.id === after);
682
+ from = at === -1 ? (m.list.unknownCursor === 'end' ? rows.length : 0) : at + 1;
683
+ }
684
+ to = from + limit;
685
+ }
686
+ 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));
687
+ const hasMore = typeof before === 'string' ? from > 0 : to < rows.length;
688
+ const url = op.path.replace(/\{[^}]+\}/g, (p) => call.params[p.slice(1, -1)] ?? p);
689
+ if (m.list.page) {
690
+ const n = Math.max(1, Math.floor(Number(params[m.list.page.param]) || 1));
691
+ const slice = rows.slice((n - 1) * limit, n * limit);
692
+ const last = Math.max(1, Math.ceil(rows.length / limit));
693
+ const headers = new Headers();
694
+ if (m.list.page.link && rows.length > limit) {
695
+ const at = (k: number): string => {
696
+ const u = new URL(call.request.url);
697
+ u.searchParams.set(m.list.page!.param, String(k));
698
+ return u.toString();
699
+ };
700
+ 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"`] : [])];
701
+ if (rels.length) headers.set('link', rels.join(', '));
702
+ }
703
+ return { served: Response.json(fill(m.list.envelope, { data: slice, next_cursor: null, key: op.answers?.key ?? 'data' }), { headers }) };
704
+ }
705
+ if (m.list.offset) {
706
+ const skip = Math.max(0, Math.trunc(Number(params[m.list.offset.param]) || 0));
707
+ const slice = rows.slice(skip, skip + limit);
708
+ const key = op.answers?.key ?? 'data';
709
+ return { served: Response.json({ ...(m.success ?? {}), [key]: slice, ...(fill(m.list.envelopes?.[op.id] ?? m.list.envelope, { data: slice, total_count: rows.length, key }) as object) }) };
710
+ }
711
+ if (m.list.cursor) {
712
+ const start = decodeOffset(params[m.list.cursor.param]);
713
+ const slice = rows.slice(start, start + limit);
714
+ const next = start + limit < rows.length ? encodeOffset(start + limit) : '';
715
+ return { served: Response.json({ ...(m.success ?? {}), ...(fill(m.list.envelope, { data: slice, next_cursor: next, key: op.answers?.key ?? 'data' }) as object), ...(op.answers?.key ? { [op.answers.key]: slice } : {}) }) };
716
+ }
717
+ 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 })) };
718
+ }
719
+ case 'update':
720
+ case 'action': {
721
+ const hit = idParam ? stored(m, resource, root).find((r) => r.id === idParam && underParent(r)) : undefined;
722
+ if (!hit) return { served: notFound(m, resource, String(idParam), Object.keys(call.params).at(-1)) };
723
+ const current = view(m, resource, hit);
724
+ const changes: Record<string, unknown> = { ...(op.class === 'update' ? data : {}) };
725
+ let moved = false;
726
+ for (const [field, sdecl] of Object.entries(decl.state ?? {})) {
727
+ const requested = op.class === 'update' ? data[field] : undefined;
728
+ if (op.class === 'update' && requested === undefined) continue;
729
+ const { move, refusal } = transitionFor(field, sdecl, op.id, current[field], requested, hit.id);
730
+ if (refusal) return { served: vendorError(m, refusal) };
731
+ if (!move) {
732
+ if (op.class === 'update') return { unmodeled: `no declared transition of ${field} to ${String(requested)} by ${op.id}` };
733
+ continue;
734
+ }
735
+ const stored = storedState(sdecl, move.to);
736
+ if (stored !== undefined) changes[field] = stored;
737
+ for (const [k, rule] of Object.entries(move.effects ?? {})) changes[k] = applyRule(m, rule, hit.id, { ...current, ...changes }, at);
738
+ moved = true;
739
+ }
740
+ if (op.class === 'action' && !moved) return { unmodeled: `action ${op.id} declares no transition` };
741
+ const merged: Record<string, unknown> = {};
742
+ for (const [k, v] of Object.entries(changes)) {
743
+ const prior = current[k];
744
+ merged[k] = decl.update !== 'replace' && v && typeof v === 'object' && !Array.isArray(v) && prior && typeof prior === 'object' && !Array.isArray(prior) ? Object.fromEntries(Object.entries({ ...(prior as object), ...(v as object) }).filter(([, value]) => value !== '')) : v;
745
+ }
746
+ return answer(await write(m, call, resource, hit.id, merged, `${storedType(m, resource)}.${op.class === 'update' ? 'update' : op.id}`, params, root, at));
747
+ }
748
+ case 'delete': {
749
+ const hit = idParam ? stored(m, resource, root).find((r) => r.id === idParam && underParent(r)) : undefined;
750
+ if (!hit) return { served: notFound(m, resource, String(idParam), Object.keys(call.params).at(-1)) };
751
+ await write(m, call, resource, hit.id, { deleted: true }, `${storedType(m, resource)}.delete`, params, root, at);
752
+ const shape = decl.deleted !== undefined ? decl.deleted : m.deleted;
753
+ return { served: status === 204 || shape === null ? new Response(null, { status: 204 }) : Response.json(fill(shape, { id: hit.id, object: current(hit) }), { status }) };
754
+ }
755
+ default:
756
+ return { unmodeled: `class ${op.class} is not served by the core` };
757
+ }
758
+ }
759
+
760
+ function current(r: TwinResource): unknown {
761
+ return render(r).object ?? r.type;
762
+ }
763
+
764
+ /** Server-sent events in the manifest's framing. The events are known when the answer starts: the
765
+ * twin decides deterministically, so the stream carries a decided answer, never a model's. */
766
+ export function sse(m: DerivedManifest, events: Array<{ event?: string; data: unknown }>, operationId?: string): Response {
767
+ const enc = new TextEncoder();
768
+ const framing = (operationId && m.streamFor?.[operationId]) || m.sse;
769
+ const frame = (e: { event?: string; data: unknown }): string => `${framing?.named && e.event ? `event: ${e.event}\n` : ''}data: ${JSON.stringify(e.data)}\n\n`;
770
+ const body = new ReadableStream<Uint8Array>({
771
+ start(controller) {
772
+ for (const e of events) controller.enqueue(enc.encode(frame(e)));
773
+ if (framing?.done) controller.enqueue(enc.encode(`data: ${framing.done}\n\n`));
774
+ controller.close();
775
+ },
776
+ });
777
+ return new Response(body, { status: 200, headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache' } });
778
+ }
779
+
780
+ // ── semantics ────────────────────────────────────────────────────────────────────────────────
781
+
782
+ /** What a semantics handler works with: the request's parameters, the tree through the pack's view,
783
+ * the write path (which runs the manifest's write hook), and the declared machine. A handler never
784
+ * imports the kernel; this is its whole interface. */
785
+ export type SemanticsContext = {
786
+ call: DerivedCall;
787
+ params: Record<string, unknown>;
788
+ /** the last path parameter: the subject an item operation names */
789
+ id: string | undefined;
790
+ /** the request body as the vendor reads it (an array, where the vendor takes one), and its text */
791
+ body: unknown;
792
+ text: string;
793
+ root: string | undefined;
794
+ occurredAt: string;
795
+ /** who is calling, when the manifest says how to tell */
796
+ actor: string | undefined;
797
+ now(): unknown;
798
+ get(resource: string, id: string): Record<string, unknown> | undefined;
799
+ rows(resource: string): Array<Record<string, unknown>>;
800
+ mint(resource: string): string;
801
+ write(resource: string, id: string, fields: Record<string, unknown>, operation: string): Promise<Record<string, unknown>>;
802
+ /** `write`, with what a live vendor answered when the head performed it */
803
+ writeDetailed(resource: string, id: string, fields: Record<string, unknown>, operation: string): Promise<{ body: Record<string, unknown>; id: string; vendorData?: unknown }>;
804
+ /** The id a subject has now, for an id a caller may still use from before the vendor minted its own
805
+ * (a branch naming a message by the ts it minted locally). */
806
+ resolve(resource: string, id: string): string;
807
+ /** Answer the vendor's success envelope around several fields (`{ ok: true, ts, channel, message }`). */
808
+ ok(fields: Record<string, unknown>): Response;
809
+ /** A stored row as it is kept, bookkeeping (`_`) fields and tombstones included. */
810
+ row(resource: string, id: string, opts?: { withDeleted?: boolean }): Record<string, unknown> | undefined;
811
+ rowsRaw(resource: string, opts?: { withDeleted?: boolean }): Array<Record<string, unknown>>;
812
+ /** Every stored subject of the pack, of every type, in the tree's order (a projection folding many types at once). */
813
+ tree(): Array<Record<string, unknown>>;
814
+ /** The writes that touched a subject, in order: its history. */
815
+ history(resource: string, id: string): Array<{ operation?: string; fields?: Record<string, unknown>; occurredAt?: string }>;
816
+ /** Write a bookkeeping subject (a `_`-prefixed type the vendor never serves), minting its id when none is given. */
817
+ record(type: string, fields: Record<string, unknown>, id?: string): Promise<string>;
818
+ /** Decide a write from the tree as it stands and make it, with nothing written between the read
819
+ * and the write (a minted number, a first poll that moves a subject). `decide` returns the write,
820
+ * or nothing to write; the answer is its `value`. */
821
+ atomically<T>(decide: (rows: (resource: string) => Array<Record<string, unknown>>) => { value: T; write?: { resource: string; id: string; fields: Record<string, unknown>; operation: string } }): Promise<T>;
822
+ /** Answer bytes or text as they are (a file's content), with the vendor's headers. */
823
+ raw(body: BodyInit | null, init?: { status?: number; headers?: Record<string, string> }): Response;
824
+ /** Whether the machine lets `operationId` move `field` from `current` (to `to`): nothing when it
825
+ * does, the vendor's refusal when it does not. A move the machine does not declare throws. */
826
+ legal(resource: string, field: string, operationId: string, current: unknown, to?: string, id?: string, actor?: Actor): ErrorSpec | undefined;
827
+ refuse(e: ErrorSpec): Response;
828
+ notFound(resource: string, id: string, param?: string): Response;
829
+ reply(body: unknown, status?: number): Response;
830
+ /** Answer in the vendor's success envelope under this operation's key (`{ ok: true, channel }`). */
831
+ wrap(body: Record<string, unknown>, extra?: Record<string, unknown>): Response;
832
+ /** Answer as a server-sent event stream, framed as the manifest says. */
833
+ sse(events: Array<{ event?: string; data: unknown }>): Response;
834
+ expand(resource: string, body: Record<string, unknown>): Record<string, unknown>;
835
+ /** This context at another moment of the World clock (ISO): a move time makes is written at the moment it fell due. */
836
+ at(when: string): Promise<SemanticsContext>;
837
+ /** The generic core's answer to this call, or undefined where the core does not serve it. */
838
+ core(): Promise<Response | undefined>;
839
+ /** A stored row's own fields, as the vendor's object carries them (an `id`, `type` or `updatedAt` of its own restored). */
840
+ own(row: Record<string, unknown>): Record<string, unknown>;
841
+ };
842
+ export type Semantics = (ctx: SemanticsContext) => Promise<Response>;
843
+
844
+ async function contextFor(m: DerivedManifest, call: DerivedCall, scope: CoreScope): Promise<SemanticsContext> {
845
+ const root = scope.root;
846
+ call = adoptedPath(m, call, root);
847
+ const at = (scope.clock ?? worldNow)();
848
+ const text = await call.request.clone().text().catch(() => '');
849
+ let body: unknown = undefined;
850
+ try { body = text ? JSON.parse(text) : undefined; } catch { body = text; }
851
+ // a handler reading the body itself sees the adopted id too (the boundary's one resolution, below)
852
+ if (body && typeof body === 'object' && !Array.isArray(body)) body = adoptedInCall(m.service, body as Record<string, unknown>, root);
853
+ const params = await boundaryParams(m, call, root);
854
+ const paths = expandPaths(m, params);
855
+ return {
856
+ call,
857
+ params,
858
+ id: Object.values(call.params).at(-1),
859
+ body,
860
+ text,
861
+ root,
862
+ occurredAt: at,
863
+ actor: m.identity ? m.identity(call.request.headers.get('authorization'), root) : undefined,
864
+ now: () => now(m, at),
865
+ get: (resource, id) => {
866
+ const hit = stored(m, resource, root).find((r) => r.id === id);
867
+ return hit ? view(m, resource, hit) : undefined;
868
+ },
869
+ rows: (resource) => stored(m, resource, root).map((r) => view(m, resource, r)),
870
+ mint: (resource) => mintId(m, resource, root),
871
+ write: (resource, id, fields, operation) => write(m, call, resource, id, fields, operation, params, root, at),
872
+ writeDetailed: (resource, id, fields, operation) => writeDetailed(m, call, resource, id, fields, operation, params, root, at),
873
+ resolve: (resource, id) => resolveSubjectId(m.service, storedType(m, resource), id, root),
874
+ ok: (fields) => Response.json({ ...(m.success ?? {}), ...fields }),
875
+ row: (resource, id, opts) => stored(m, resource, root, opts).find((r) => r.id === id) as Record<string, unknown> | undefined,
876
+ rowsRaw: (resource, opts) => stored(m, resource, root, opts) as Array<Record<string, unknown>>,
877
+ tree: () => twinResources(m.service, root) as Array<Record<string, unknown>>,
878
+ history: (resource, id) => subjectHistory(m.service, { type: storedType(m, resource), id }, root).map((e) => ({ ...(e.operation ? { operation: e.operation } : {}), ...(e.fields ? { fields: e.fields as Record<string, unknown> } : {}), ...(e.occurredAt ? { occurredAt: e.occurredAt } : {}) })),
879
+ record: async (type, fields, id) => {
880
+ if (!type.startsWith('_')) throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
881
+ const subject = id ?? `${type.slice(1)}_${twinResources(m.service, root).filter((r) => r.type === type).length + 1}`;
882
+ await applyTwinWrite(m.service, { operation: `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, occurredAt: at, actor: { kind: 'system' } }, root);
883
+ return subject;
884
+ },
885
+ raw: (body, init = {}) => new Response(body, { status: init.status ?? 200, headers: init.headers ?? {} }),
886
+ atomically: async (decide) => {
887
+ const { value } = await applyTwinWriteAtomic(
888
+ m.service,
889
+ (resources) => {
890
+ const rows = (resource: string): Array<Record<string, unknown>> => resources.filter((r) => r.type === storedType(m, resource) && r.deleted !== true) as Array<Record<string, unknown>>;
891
+ const d = decide(rows);
892
+ if (!d.write) return { kind: 'skip', value: d.value };
893
+ const w = d.write;
894
+ 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' } } };
895
+ },
896
+ root,
897
+ );
898
+ return value;
899
+ },
900
+ legal: (resource, field, operationId, current, to, id, actor) => {
901
+ const decl = m.resources[resource]?.state?.[field];
902
+ if (!decl) throw new Error(`semantics: ${resource}.${field} is not a declared state field`);
903
+ const { move, refusal } = transitionFor(field, decl, operationId, current, to, id ?? Object.values(call.params).at(-1), actor ?? 'api');
904
+ if (move) return undefined;
905
+ if (refusal) return refusal;
906
+ throw new Error(`semantics: ${operationId} moves ${resource}.${field} from ${String(current)}${to ? ` to ${to}` : ''}, which the machine does not declare`);
907
+ },
908
+ refuse: (e) => vendorError(m, e),
909
+ notFound: (resource, id, param) => notFound(m, resource, id, param),
910
+ reply: (body, status = 200) => Response.json(body, { status }),
911
+ wrap: (body, extra = {}) => Response.json({ ...envelope(m, call.operation, body), ...extra }),
912
+ sse: (events) => sse(m, events, call.operation.id),
913
+ expand: (resource, body) => (paths.length ? expand(m, resource, body, paths, root) : body),
914
+ at: (when) => contextFor(m, { ...call, request: new Request(call.request.url) }, { ...scope, clock: () => when }),
915
+ core: async () => { const out = await serveCore(m, call, { ...scope, clock: () => at }); return 'served' in out ? out.served : undefined; },
916
+ own: (row) => ownFields(row as unknown as TwinResource),
917
+ };
918
+ }
919
+
920
+ /** A pack's semantics handlers as the dispatch's handlers. */
921
+ export function bindSemantics(m: DerivedManifest, handlers: Record<string, Semantics>, scope: CoreScope = {}): Record<string, DerivedHandler> {
922
+ return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call: DerivedCall) => h(await contextFor(m, call, scope))]));
923
+ }
924
+
925
+ /** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
926
+ export function coreFor(m: DerivedManifest, scope: CoreScope = {}): { owns: (o: DerivedOperation) => boolean; serve: (call: DerivedCall) => Promise<DerivedCoreOutcome> } {
927
+ const crud = new Set(['create', 'retrieve', 'list', 'update', 'delete']);
928
+ const owns = (o: DerivedOperation): boolean => {
929
+ const decl = o.resource !== undefined ? m.resources[o.resource] : undefined;
930
+ if (!decl || m.unmodeled?.includes(o.id)) return false;
931
+ if (crud.has(o.class)) return true;
932
+ return o.class === 'action' && Object.values(decl.state ?? {}).some((f) => f.transitions.some((t) => t.operation === o.id));
933
+ };
934
+ return { owns, serve: (call) => serveCore(m, call, scope) };
935
+ }
936
+
937
+ // ── what every moved operation gets ──────────────────────────────────────────────────────────
938
+
939
+ const hex = (s: string): string => Array.from(new TextEncoder().encode(s), (b) => b.toString(16).padStart(2, '0')).join('');
940
+
941
+ /** Read-only refusal, API-version validation and idempotent replay, applied once around every
942
+ * operation a handler or the core serves. */
943
+ export function crossCutting(m: DerivedManifest, opts: CoreScope & { readOnly?: boolean } = {}): (call: DerivedCall, next: () => Promise<Response>) => Promise<Response> {
944
+ const finish = async (r: Response, request: Request): Promise<Response> => {
945
+ if (!m.origin || !(r.headers.get('content-type') ?? '').includes('json')) return withHeaders(r);
946
+ const text = (await r.text()).replaceAll(m.origin.placeholder, twinPublicBase(request));
947
+ return withHeaders(new Response(text, { status: r.status, statusText: r.statusText, headers: r.headers }));
948
+ };
949
+ const withHeaders = (r: Response): Response => {
950
+ if (!m.answerHeaders) return r;
951
+ const headers = new Headers(r.headers);
952
+ for (const [k, v] of Object.entries(m.answerHeaders)) if (!headers.has(k)) headers.set(k, v);
953
+ return new Response(r.body, { status: r.status, statusText: r.statusText, headers });
954
+ };
955
+ return async (call, next) => finish(await guarded(call, next), call.request);
956
+ async function guarded(call: DerivedCall, next: () => Promise<Response>): Promise<Response> {
957
+ const { request } = call;
958
+ if (m.auth) {
959
+ const raw = request.headers.get(m.auth.header);
960
+ if (raw !== null || m.auth.gateWhenAbsent) {
961
+ const scheme = `${m.auth.scheme.toLowerCase()} `;
962
+ const key = raw && raw.toLowerCase().startsWith(scheme) ? raw.slice(scheme.length).trim() : '';
963
+ if (!key) return vendorError(m, m.auth.missing);
964
+ if (m.auth.invalidKeys.includes(key) || (m.auth.keyFormat && !new RegExp(m.auth.keyFormat).test(key))) return vendorError(m, m.auth.invalid);
965
+ }
966
+ }
967
+ // a read-only twin refuses writes: what the operation does, not the HTTP verb it came by (an RPC
968
+ // wire POSTs its reads)
969
+ if (opts.readOnly && !['retrieve', 'list', 'computed'].includes(call.operation.class) && !m.reads?.includes(call.operation.id)) return vendorError(m, m.readOnly);
970
+ // a body labelled JSON that does not parse is the vendor's refusal, never a crash of the twin
971
+ if ((request.headers.get('content-type') ?? '').includes('json') || m.body.json === 'always') {
972
+ const text = request.method === 'GET' || request.method === 'HEAD' ? '' : await request.clone().text().catch(() => '');
973
+ if (text) { try { JSON.parse(text); } catch { return vendorError(m, m.malformedBody ?? { status: 400, message: 'The request body could not be parsed as JSON.' }); } }
974
+ }
975
+ if (m.version) {
976
+ const value = request.headers.get(m.version.header);
977
+ if (value && !new RegExp(m.version.pattern).test(value)) return vendorError(m, { ...m.version.error, message: String(fill(m.version.error.message, { value })) });
978
+ }
979
+ const key = m.idempotency ? request.headers.get(m.idempotency.header) : null;
980
+ if (!m.idempotency || !key || !(m.idempotency.methods ?? ['POST']).includes(request.method.toUpperCase())) return next();
981
+ const url = new URL(request.url);
982
+ const signature = hashFieldValue({ path: url.pathname + url.search, body: await request.clone().text() });
983
+ const id = `idem_${hex(key)}`;
984
+ const prior = twinResources(m.service, opts.root).find((r) => r.type === m.idempotency!.storedAs && r.id === id) as Record<string, unknown> | undefined;
985
+ if (prior) {
986
+ if (m.idempotency.conflict && prior.requestHash !== undefined && prior.requestHash !== signature) return vendorError(m, m.idempotency.conflict);
987
+ const answer = prior.response as { status: number; body?: unknown; text?: string; contentType?: string };
988
+ // a streamed answer replays as the stream it was; a record kept before text was kept replays its JSON
989
+ 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 });
990
+ }
991
+ const response = await next();
992
+ if (m.idempotency.onlySuccess && (response.status < 200 || response.status >= 300)) return response;
993
+ const contentType = response.headers.get('content-type') ?? 'application/json';
994
+ const text = await response.clone().text();
995
+ 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);
996
+ return response;
997
+ }
998
+ }
999
+
1000
+ /** A semantics context for a request no surface operation names: another wire's (GraphQL) resolvers
1001
+ * get the same interface as a handler, named by the operation id the wire gives. */
1002
+ export function semanticsContext(m: DerivedManifest, request: Request, operation: DerivedOperation, scope: CoreScope = {}): Promise<SemanticsContext> {
1003
+ return contextFor(m, { request, operation, params: {} }, scope);
1004
+ }