@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,340 @@
1
+ // The HTTP server seam of the kernel (docs/contributing/architecture.md#the-serve-seam): a fetch handler on
2
+ // a port, in the shape every `Bun.serve` call already has, so the serve path meets the runtime in
3
+ // ONE place. Bun-backed where Bun is the runtime; `node:http`-backed otherwise, the request rebuilt
4
+ // as a standard Request and the handler's Response written back. Async, because binding a port is
5
+ // asynchronous on Node — Bun's synchronous bind is the special case.
6
+ //
7
+ // Nothing here runs at module scope, and `node:http` is reached through `process.getBuiltinModule`
8
+ // rather than a static import: this module rides into mirror-UI client bundles through the kernel's
9
+ // entrypoint, where a static `node:` import is the 2026-09-06 class of break.
10
+ /** A decorator the request journal installs: every server made through the seam serves through it. */
11
+ let decorate;
12
+ export function setServeDecorator(fn) { decorate = fn; }
13
+ const bunRuntime = () => { const b = globalThis.Bun; return b && typeof b.serve === 'function' ? b : undefined; };
14
+ /** The path a World asks a service it booted, when that service's port answers, which boot it belongs to. */
15
+ export const WORLD_BOOT_PATH = '/__volter/world-boot';
16
+ export async function serveHttp(options) {
17
+ // Inside a World every server made through the seam answers which boot it belongs to, so the World can tell its own
18
+ // twin from a leftover World's that holds the same port. Outside a World nothing changes.
19
+ const worldBoot = globalThis.process?.env?.VOLTER_WORLD_BOOT_ID;
20
+ if (worldBoot) {
21
+ const served = options.fetch;
22
+ options = { ...options, fetch: (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? Response.json({ boot: worldBoot }) : served(request)) };
23
+ }
24
+ // loopback by default: an unnamed interface is never every interface
25
+ const hostname = options.hostname ?? '127.0.0.1';
26
+ const urlHost = options.hostname ?? '127.0.0.1';
27
+ const bun = bunRuntime();
28
+ if (bun) {
29
+ // the global `Bun.serve` — the request journal's wrap of it (serve.ts) applies to packs and to
30
+ // this seam alike, so the seam never decorates twice on Bun
31
+ const upgrade = options.upgrade;
32
+ const server = bun.serve({ ...options, hostname, port: options.port ?? 0,
33
+ ...(upgrade ? {
34
+ fetch: (request, server) => {
35
+ if (request.headers.get('upgrade')?.toLowerCase() === 'websocket' && upgrade.accepts(request) && server.upgrade(request, { data: { request } }))
36
+ return undefined;
37
+ return options.fetch(request);
38
+ },
39
+ websocket: {
40
+ open: (peer) => upgrade.open(peer, peer.data.request),
41
+ message: (peer, data) => upgrade.message(peer, data),
42
+ close: upgrade.close,
43
+ },
44
+ } : {}),
45
+ });
46
+ const port = server.port ?? 0;
47
+ return { hostname, port, url: server.url instanceof URL ? server.url : new URL(`http://${urlHost}:${port}/`), stop: async (closeActive = true) => { await server.stop(closeActive); } };
48
+ }
49
+ return serveOnNode({ ...options, hostname, urlHost, fetch: decorate ? decorate(options) : options.fetch });
50
+ }
51
+ /** A Node builtin without a static import — `process.getBuiltinModule` (Node 22.3+, Bun): what a
52
+ * module that also rides into a browser bundle reaches `node:crypto` with. A bare `require()`
53
+ * is Bun's alone; Node's ESM has none (found 2026-09-07 by the pages running under node). */
54
+ export function nodeBuiltin(name) { return builtin(name); }
55
+ function builtin(name) {
56
+ const get = process.getBuiltinModule;
57
+ if (typeof get !== 'function')
58
+ throw new Error(`serveHttp: this runtime is neither Bun nor a Node with process.getBuiltinModule (Node 22.3+)`);
59
+ return get(name);
60
+ }
61
+ async function serveOnNode(options) {
62
+ const http = builtin('node:http');
63
+ const { Readable } = builtin('node:stream');
64
+ if ('websocket' in options)
65
+ throw new Error('serveHttp: use the portable upgrade callbacks for WebSocket support on Node');
66
+ const protocol = options.tls ? 'https' : 'http';
67
+ const factory = (handler) => options.tls
68
+ ? builtin('node:https').createServer({ key: typeof options.tls.key === 'string' ? options.tls.key : Buffer.from(options.tls.key), cert: typeof options.tls.cert === 'string' ? options.tls.cert : Buffer.from(options.tls.cert) }, handler)
69
+ : http.createServer(handler);
70
+ let cleanupReports = 0;
71
+ const reportCleanup = (stage, error) => {
72
+ // A disconnected peer cannot receive an error response. Keep bounded
73
+ // ownership evidence without printing request URLs, headers or bodies.
74
+ cleanupReports++;
75
+ if (cleanupReports > 32) {
76
+ if (cleanupReports === 33)
77
+ try {
78
+ process.stderr.write('serveHttp further cleanup failures suppressed\n');
79
+ }
80
+ catch { /* best-effort */ }
81
+ return;
82
+ }
83
+ let detail = 'unknown failure';
84
+ try {
85
+ detail = error instanceof Error
86
+ ? `${error.name.slice(0, 64)}: ${error.message.slice(0, 512)}`
87
+ : String(error).slice(0, 512);
88
+ }
89
+ catch { /* hostile thrown values are still reported by stage */ }
90
+ try {
91
+ process.stderr.write(`serveHttp ${stage}: ${detail.slice(0, 512)}\n`);
92
+ }
93
+ catch { /* stderr is best-effort */ }
94
+ };
95
+ const active = new Set();
96
+ // Node's Request signal is dependent on the source signal. Neither a pending
97
+ // handler nor a streaming response necessarily keeps the Request alive, so
98
+ // retain it until the whole HTTP response path (including body cleanup) ends.
99
+ const activeRequests = new Set();
100
+ const server = factory((req, res) => {
101
+ const controller = new AbortController();
102
+ active.add(controller);
103
+ const abort = () => { if (!controller.signal.aborted)
104
+ controller.abort(); };
105
+ const onRequestClose = () => { if (!req.complete)
106
+ abort(); };
107
+ const onResponseClose = () => { if (!res.writableFinished)
108
+ abort(); };
109
+ const onSocketClose = () => { if (!res.writableFinished)
110
+ abort(); };
111
+ req.on('aborted', abort);
112
+ req.on('close', onRequestClose);
113
+ req.on('error', abort);
114
+ req.socket.on('close', onSocketClose);
115
+ req.socket.on('error', abort);
116
+ res.on('close', onResponseClose);
117
+ res.on('error', abort);
118
+ const disconnected = () => controller.signal.aborted || res.destroyed;
119
+ const cancelBody = async (body) => {
120
+ if (!body)
121
+ return;
122
+ try {
123
+ await body.cancel(controller.signal.reason);
124
+ }
125
+ catch (error) {
126
+ reportCleanup('late response body cancellation failed', error);
127
+ }
128
+ };
129
+ const waitForDrain = () => new Promise((resolve, reject) => {
130
+ if (disconnected()) {
131
+ reject(new Error('HTTP client disconnected'));
132
+ return;
133
+ }
134
+ const retire = () => {
135
+ res.off('drain', drained);
136
+ res.off('close', closed);
137
+ res.off('error', errored);
138
+ controller.signal.removeEventListener('abort', closed);
139
+ };
140
+ const drained = () => { retire(); resolve(); };
141
+ const closed = () => { retire(); reject(new Error('HTTP client disconnected')); };
142
+ const errored = (error) => { retire(); reject(error); };
143
+ res.once('drain', drained);
144
+ res.once('close', closed);
145
+ res.once('error', errored);
146
+ controller.signal.addEventListener('abort', closed, { once: true });
147
+ if (disconnected())
148
+ closed();
149
+ });
150
+ let request;
151
+ const handle = async () => {
152
+ const method = req.method ?? 'GET';
153
+ let response;
154
+ try {
155
+ const headers = new Headers();
156
+ for (const [k, v] of Object.entries(req.headers)) {
157
+ if (v === undefined)
158
+ continue;
159
+ if (Array.isArray(v))
160
+ for (const one of v)
161
+ headers.append(k, one);
162
+ else
163
+ headers.set(k, v);
164
+ }
165
+ const url = `${protocol}://${req.headers.host ?? `${options.hostname}:${server.address()?.port ?? 0}`}${req.url ?? '/'}`;
166
+ const withBody = method !== 'GET' && method !== 'HEAD';
167
+ const init = { method, headers, signal: controller.signal, ...(withBody ? { body: Readable.toWeb(req), duplex: 'half' } : {}) };
168
+ request = new Request(url, init);
169
+ activeRequests.add(request);
170
+ response = await options.fetch(request);
171
+ }
172
+ catch (error) {
173
+ if (disconnected())
174
+ return;
175
+ // Preserve the existing Bun-shaped pre-header error hook and default 500.
176
+ const err = error instanceof Error ? error : new Error(String(error));
177
+ try {
178
+ response = options.error ? await options.error(err) : new Response(`${err.stack ?? err.message}\n`, { status: 500, headers: { 'content-type': 'text/plain' } });
179
+ }
180
+ catch (again) {
181
+ response = new Response(`${again instanceof Error ? again.stack ?? again.message : String(again)}\n`, { status: 500, headers: { 'content-type': 'text/plain' } });
182
+ }
183
+ }
184
+ if (disconnected()) {
185
+ await cancelBody(response.body);
186
+ return;
187
+ }
188
+ let reader;
189
+ let cancelReader;
190
+ const cancelOwnedReader = () => {
191
+ if (!reader)
192
+ return Promise.resolve();
193
+ if (!cancelReader) {
194
+ cancelReader = Promise.resolve().then(() => reader.cancel(controller.signal.reason)).catch((error) => {
195
+ reportCleanup('response reader cancellation failed', error);
196
+ });
197
+ }
198
+ return cancelReader;
199
+ };
200
+ const onAbort = () => { void cancelOwnedReader(); };
201
+ try {
202
+ if (method === 'HEAD') {
203
+ // A HEAD body is never sent, but its producer is still ours to stop.
204
+ // Failure here is not a successful response completion.
205
+ try {
206
+ await response.body?.cancel(controller.signal.reason);
207
+ }
208
+ catch (error) {
209
+ reportCleanup('HEAD response body cancellation failed', error);
210
+ throw error;
211
+ }
212
+ if (!disconnected()) {
213
+ res.statusCode = response.status;
214
+ for (const [k, v] of response.headers) {
215
+ if (k !== 'set-cookie')
216
+ res.setHeader(k, v);
217
+ }
218
+ const cookies = response.headers.getSetCookie?.() ?? [];
219
+ if (cookies.length)
220
+ res.setHeader('set-cookie', cookies);
221
+ res.end();
222
+ }
223
+ return;
224
+ }
225
+ if (response.body)
226
+ reader = response.body.getReader();
227
+ controller.signal.addEventListener('abort', onAbort, { once: true });
228
+ if (disconnected()) {
229
+ await cancelOwnedReader();
230
+ return;
231
+ }
232
+ res.statusCode = response.status;
233
+ for (const [k, v] of response.headers) {
234
+ if (k !== 'set-cookie')
235
+ res.setHeader(k, v);
236
+ }
237
+ const cookies = response.headers.getSetCookie?.() ?? [];
238
+ if (cookies.length)
239
+ res.setHeader('set-cookie', cookies);
240
+ if (reader) {
241
+ while (true) {
242
+ if (disconnected())
243
+ throw new Error('HTTP client disconnected');
244
+ const next = await reader.read();
245
+ if (disconnected())
246
+ throw new Error('HTTP client disconnected');
247
+ if (next.done)
248
+ break;
249
+ if (!res.write(next.value))
250
+ await waitForDrain();
251
+ }
252
+ }
253
+ if (!disconnected())
254
+ res.end();
255
+ }
256
+ catch (error) {
257
+ // Once headers/body may have gone out, a second error response or clean EOF is false.
258
+ abort();
259
+ if (!res.destroyed)
260
+ res.destroy(error instanceof Error ? error : new Error(String(error)));
261
+ }
262
+ finally {
263
+ controller.signal.removeEventListener('abort', onAbort);
264
+ if (reader) {
265
+ if (controller.signal.aborted)
266
+ await cancelOwnedReader();
267
+ try {
268
+ reader.releaseLock();
269
+ }
270
+ catch (error) {
271
+ reportCleanup('response reader release failed', error);
272
+ }
273
+ }
274
+ }
275
+ };
276
+ const responseSettled = new Promise((resolve) => {
277
+ const settled = () => {
278
+ res.off('finish', settled);
279
+ res.off('close', settled);
280
+ resolve();
281
+ };
282
+ res.once('finish', settled);
283
+ res.once('close', settled);
284
+ if (res.writableFinished || res.destroyed)
285
+ settled();
286
+ });
287
+ void handle().finally(() => {
288
+ if (request)
289
+ activeRequests.delete(request);
290
+ }).catch((error) => {
291
+ abort();
292
+ if (!res.destroyed)
293
+ res.destroy(error instanceof Error ? error : new Error(String(error)));
294
+ }).then(async () => {
295
+ await responseSettled;
296
+ active.delete(controller);
297
+ req.off('aborted', abort);
298
+ req.off('close', onRequestClose);
299
+ req.off('error', abort);
300
+ req.socket.off('close', onSocketClose);
301
+ req.socket.off('error', abort);
302
+ res.off('close', onResponseClose);
303
+ res.off('error', abort);
304
+ });
305
+ });
306
+ let socketServer;
307
+ if (options.upgrade) {
308
+ const { WebSocketServer } = await import('ws');
309
+ socketServer = new WebSocketServer({ noServer: true });
310
+ const upgrade = options.upgrade;
311
+ server.on('upgrade', (req, socket, head) => {
312
+ const request = new Request(`${protocol}://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, { headers: Object.fromEntries(Object.entries(req.headers).filter((entry) => typeof entry[1] === 'string')) });
313
+ if (!upgrade.accepts(request)) {
314
+ socket.end('HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
315
+ return;
316
+ }
317
+ socketServer.handleUpgrade(req, socket, head, (peer) => {
318
+ peer.on('message', (data, binary) => { const bytes = Array.isArray(data) ? Buffer.concat(data) : data instanceof ArrayBuffer ? new Uint8Array(data) : data; Promise.resolve(upgrade.message(peer, binary ? new Uint8Array(bytes) : Buffer.from(bytes).toString())).catch(() => peer.close(1011, 'handler failed')); });
319
+ peer.on('close', () => upgrade.close(peer));
320
+ peer.on('error', () => peer.close());
321
+ upgrade.open(peer, request);
322
+ });
323
+ });
324
+ }
325
+ if (options.idleTimeout !== undefined)
326
+ server.keepAliveTimeout = options.idleTimeout * 1000;
327
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(options.port ?? 0, options.hostname, () => { server.off('error', reject); resolve(); }); });
328
+ const port = server.address().port;
329
+ return {
330
+ hostname: options.hostname, port, url: new URL(`${protocol}://${options.urlHost}:${port}/`),
331
+ stop: (closeActive = true) => new Promise((resolve) => { if (closeActive) {
332
+ for (const controller of active)
333
+ controller.abort();
334
+ for (const peer of socketServer?.clients ?? [])
335
+ peer.terminate();
336
+ } socketServer?.close(); server.close(() => { if (closeActive && active.size)
337
+ reportCleanup('forced stop left pending handler cleanup', `${active.size} request(s)`); resolve(); }); if (closeActive)
338
+ server.closeAllConnections(); }),
339
+ };
340
+ }
@@ -0,0 +1,147 @@
1
+ import type { SubjectFields } from './hash.js';
2
+ import type { ActionProjection, TwinActionPrecondition } from './actions.js';
3
+ export type TwinCredentialShape = {
4
+ /** header name (lowercased) or query-param name the credential arrived under. */
5
+ name: string;
6
+ /** where it sat on the request. */
7
+ in: 'header' | 'query';
8
+ /** the auth SCHEME when the value carried one (`Bearer`, `Basic`, `AWS4-HMAC-SHA256`). A scheme
9
+ * is not a secret, and it is what distinguishes "a bearer token arrived" from "a named vendor
10
+ * header arrived" — the exact confusion this journal was added to end. */
11
+ scheme?: string;
12
+ /** `sha256:<16 hex>` of the value (of the credential part when a scheme was present). Stable
13
+ * across processes and runs, so equal fingerprints mean equal values; non-reversible. */
14
+ fp?: string;
15
+ /** the name arrived with an EMPTY value — the "the SDK sent the header but no key" case. */
16
+ empty?: true;
17
+ };
18
+ export type TwinRequestJournalEntry = {
19
+ at?: string;
20
+ method: string;
21
+ path: string;
22
+ status: number;
23
+ /** the serve's wall time in milliseconds (layer 8: a world's response-time report) */
24
+ ms?: number;
25
+ /** credential-looking headers/query params that arrived, named + fingerprinted, never quoted. */
26
+ credentials?: TwinCredentialShape[];
27
+ };
28
+ /** The journal read back: every entry kept for a service, oldest first (an empty list when the journal is off or empty). */
29
+ export declare function readTwinRequestJournal(service: string, root?: string): TwinRequestJournalEntry[];
30
+ /** Non-reversible, stable fingerprint. 64 bits of sha256 — enough that two distinct credentials
31
+ * never collide in a journal, far too little to walk back to a real key. */
32
+ export declare function credentialFingerprint(value: string): string;
33
+ /**
34
+ * The credential SHAPE of one request: every credential-looking header and query param, named and
35
+ * fingerprinted, sorted for stable diffing. Values never leave this function.
36
+ */
37
+ export declare function twinRequestCredentials(headers: Headers | Record<string, string>, url?: string | URL): TwinCredentialShape[];
38
+ export declare function twinRequestJournalEnabled(env?: Record<string, string | undefined>): boolean;
39
+ /** Where a service's request journal lives: beside its `actions.jsonl`. */
40
+ export declare function twinRequestJournalPath(service: string, root?: string): string;
41
+ /**
42
+ * Append one served request to the journal — a no-op unless VOLTER_TWIN_REQUEST_JOURNAL=1, so
43
+ * the default path does zero I/O. Never throws: an observability append must not fail a serve.
44
+ */
45
+ export declare function journalTwinRequest(service: string, entry: TwinRequestJournalEntry, root?: string): void;
46
+ export declare const TWIN_JOURNAL_IDENTITY: unique symbol;
47
+ export type TwinJournalIdentity = {
48
+ service: string;
49
+ root?: string;
50
+ };
51
+ export declare function vendorFromStack(stack: string | undefined): string | undefined;
52
+ /**
53
+ * Wrap `Bun.serve` so EVERY twin's HTTP surface journals, with no per-pack line. Idempotent
54
+ * (a `Symbol.for` marker survives a second copy of this module) and a strict pass-through when
55
+ * VOLTER_TWIN_REQUEST_JOURNAL is not `1` — the default path adds one function call per request
56
+ * and does zero I/O. Called once at module load, below; exported so a test can assert it.
57
+ *
58
+ * A pack that knows its own identity may declare it by putting `{service, root}` on the serve
59
+ * options under `TWIN_JOURNAL_IDENTITY` (a symbol key Bun's option reader ignores). Nothing in
60
+ * the estate needs to: `createTwinServer` is the only caller, because it is the one server whose
61
+ * service is an argument rather than a fact about the pack.
62
+ */
63
+ export declare function installTwinRequestJournal(): boolean;
64
+ export type TwinResource = {
65
+ id: string;
66
+ type: string;
67
+ updatedAt: string;
68
+ } & Record<string, unknown>;
69
+ export declare function twinResources(service: string, root?: string): TwinResource[];
70
+ export type TwinWriteResult = {
71
+ status: 'performed' | 'replayed';
72
+ actionId: string; /** the id the vendor minted when the head performed the write */
73
+ externalId?: string; /** what the vendor answered when the head performed the write, as the adapter returned it */
74
+ vendorData?: unknown;
75
+ };
76
+ export type TwinWriteInput = {
77
+ operation: string;
78
+ provider?: string;
79
+ subjectType: string;
80
+ subjectId: string;
81
+ fields: SubjectFields;
82
+ input?: Record<string, unknown>;
83
+ projection?: ActionProjection;
84
+ occurredAt?: string;
85
+ actor?: {
86
+ kind: 'agent' | 'human' | 'bot' | 'system';
87
+ id?: string;
88
+ };
89
+ preconditions?: TwinActionPrecondition[];
90
+ correlationId?: string;
91
+ uniqueness?: string;
92
+ /** AT-MOST-ONCE, opt in. A local vendor write is an OCCURRENCE by default: two calls are two
93
+ * actions, even byte-identical in the same instant. Pass a caller-supplied key when a re-issue
94
+ * of the SAME request must collapse onto the first. Without a key the kernel cannot tell a retry
95
+ * from a repeat, and guessing from content is what silently dropped writes.
96
+ *
97
+ * SCOPE, stated plainly because an earlier version of this comment overclaimed: the key dedupes
98
+ * on (service, operation, subjectId, key). It therefore helps only when the CALLER supplies the
99
+ * subject id. A create whose id the twin mints gets a fresh id per call and is NOT deduped by
100
+ * this — which is the commonest shape a vendor's own idempotency header covers — so a pack
101
+ * modelling such a header for creates keeps doing it itself, by request signature and stored
102
+ * response (the payments pack in this catalog does). Reuse of a key with a DIFFERENT payload
103
+ * raises a conflict, which the generic twin server answers as a 400 (2026-09-04). */
104
+ idempotencyKey?: string;
105
+ /** The EXACT identity of an action being re-materialized, when the caller already has one —
106
+ * replay reproducing a source changeset's actions into a target world. Distinct from
107
+ * `idempotencyKey`, which is a REQUEST key the kernel derives an id from: this IS the id, so a
108
+ * replayed action keeps the identity it had in the source world. Implies at-most-once, because
109
+ * identity equality is what at-most-once means. Not for packs. */
110
+ actionId?: string;
111
+ };
112
+ export type AtomicTwinWriteDecision<T> = {
113
+ kind: 'skip';
114
+ value: T;
115
+ } | {
116
+ kind: 'write';
117
+ value: T;
118
+ write: TwinWriteInput;
119
+ };
120
+ /**
121
+ * Decide a state-dependent local write and append it under the same cross-process action lock.
122
+ * Use this when acceptance or the new fields depend on current projected state; a caller-side
123
+ * read followed by `applyTwinWrite` is not atomic across processes.
124
+ */
125
+ export declare function applyTwinWriteAtomic<T>(service: string, prepare: (resources: readonly TwinResource[]) => AtomicTwinWriteDecision<T>, root?: string): Promise<{
126
+ value: T;
127
+ result?: TwinWriteResult;
128
+ }>;
129
+ export declare function resolveTwinRead(service: string, pathname: string, opts?: {
130
+ root?: string;
131
+ }): {
132
+ status: number;
133
+ body: unknown;
134
+ };
135
+ export declare function applyTwinWrite(service: string, write: TwinWriteInput, root?: string): Promise<{
136
+ result: TwinWriteResult;
137
+ resource: TwinResource;
138
+ }>;
139
+ export declare function createTwinServer(options: {
140
+ service: string;
141
+ root?: string;
142
+ port?: number;
143
+ readOnly?: boolean;
144
+ }): Promise<{
145
+ port: number;
146
+ stop: () => void;
147
+ }>;