@volter/world-core 3.0.38 → 3.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/app-route.cjs +26 -25
  2. package/app-route.d.cts +1 -0
  3. package/dist/app-route.cjs +26 -25
  4. package/dist/app-route.d.cts +1 -0
  5. package/dist/generated/pack-facts.json +409 -352
  6. package/dist/inject.cjs +474 -95
  7. package/dist/network-policy.cjs +110 -13
  8. package/dist/network-policy.d.cts +8 -3
  9. package/dist/src/actions.js +2 -1
  10. package/dist/src/anthropic-wire.d.ts +2 -0
  11. package/dist/src/anthropic-wire.js +5 -0
  12. package/dist/src/attestation.d.ts +6 -0
  13. package/dist/src/attestation.js +27 -0
  14. package/dist/src/bytes.d.ts +3 -0
  15. package/dist/src/bytes.js +17 -0
  16. package/dist/src/changeset.js +2 -1
  17. package/dist/src/clickhouse/sql.js +73 -14
  18. package/dist/src/derived-core.d.ts +210 -32
  19. package/dist/src/derived-core.js +510 -244
  20. package/dist/src/derived-real.d.ts +1 -1
  21. package/dist/src/derived-real.js +704 -93
  22. package/dist/src/derived.d.ts +18 -2
  23. package/dist/src/derived.js +21 -2
  24. package/dist/src/events.d.ts +28 -45
  25. package/dist/src/events.js +40 -58
  26. package/dist/src/exact-json.d.ts +10 -0
  27. package/dist/src/exact-json.js +98 -0
  28. package/dist/src/executor.d.ts +17 -0
  29. package/dist/src/executor.js +91 -30
  30. package/dist/src/file-response.js +1 -1
  31. package/dist/src/form.d.ts +8 -0
  32. package/dist/src/form.js +76 -0
  33. package/dist/src/git/format.d.ts +16 -0
  34. package/dist/src/git/format.js +110 -0
  35. package/dist/src/git/index.d.ts +1 -0
  36. package/dist/src/git/index.js +1 -0
  37. package/dist/src/git/lfs.d.ts +1 -1
  38. package/dist/src/git/lfs.js +4 -3
  39. package/dist/src/graphql-wire.d.ts +6 -0
  40. package/dist/src/graphql-wire.js +6 -3
  41. package/dist/src/grpc-wire.js +2 -1
  42. package/dist/src/head.d.ts +1 -0
  43. package/dist/src/head.js +15 -4
  44. package/dist/src/held-reads.d.ts +6 -0
  45. package/dist/src/held-reads.js +60 -0
  46. package/dist/src/history.js +5 -4
  47. package/dist/src/host-port.d.ts +5 -0
  48. package/dist/src/host-port.js +14 -0
  49. package/dist/src/index.d.ts +11 -3
  50. package/dist/src/index.js +9 -4
  51. package/dist/src/log.js +12 -7
  52. package/dist/src/machines.d.ts +81 -5
  53. package/dist/src/machines.js +125 -9
  54. package/dist/src/managed-database.d.ts +15 -11
  55. package/dist/src/managed-database.js +38 -37
  56. package/dist/src/multipart.d.ts +21 -3
  57. package/dist/src/multipart.js +112 -38
  58. package/dist/src/observe.d.ts +3 -0
  59. package/dist/src/observe.js +6 -2
  60. package/dist/src/openai-wire.d.ts +16 -0
  61. package/dist/src/openai-wire.js +43 -12
  62. package/dist/src/pack-fetch.d.ts +4 -6
  63. package/dist/src/pack-fetch.js +189 -57
  64. package/dist/src/packRegistry.d.ts +7 -1
  65. package/dist/src/packRegistry.js +20 -3
  66. package/dist/src/private-transport.d.ts +1 -0
  67. package/dist/src/private-transport.js +11 -0
  68. package/dist/src/protobuf.js +10 -4
  69. package/dist/src/rateBudget.d.ts +8 -1
  70. package/dist/src/rateBudget.js +1 -0
  71. package/dist/src/redis/engine.d.ts +14 -5
  72. package/dist/src/redis/engine.js +271 -72
  73. package/dist/src/redis/frames.d.ts +17 -0
  74. package/dist/src/redis/frames.js +123 -0
  75. package/dist/src/redis/index.d.ts +6 -0
  76. package/dist/src/redis/index.js +6 -0
  77. package/dist/src/redis/json.d.ts +2 -0
  78. package/dist/src/redis/json.js +14 -0
  79. package/dist/src/redis/lua-libs.d.ts +4 -0
  80. package/dist/src/redis/lua-libs.js +173 -0
  81. package/dist/src/redis/lua.d.ts +1 -0
  82. package/dist/src/redis/lua.js +36 -0
  83. package/dist/src/redis/member-storage.d.ts +5 -0
  84. package/dist/src/redis/member-storage.js +75 -0
  85. package/dist/src/redis/resp2.d.ts +4 -0
  86. package/dist/src/redis/resp2.js +29 -0
  87. package/dist/src/redis/stream.d.ts +14 -0
  88. package/dist/src/redis/stream.js +335 -0
  89. package/dist/src/remote-execute.d.ts +2 -2
  90. package/dist/src/request-body.d.ts +11 -0
  91. package/dist/src/request-body.js +11 -0
  92. package/dist/src/runtime.d.ts +6 -5
  93. package/dist/src/runtime.js +5 -5
  94. package/dist/src/s3/wire.d.ts +4 -4
  95. package/dist/src/s3/wire.js +5 -3
  96. package/dist/src/scenario.d.ts +22 -0
  97. package/dist/src/scenario.js +35 -31
  98. package/dist/src/serve-http.d.ts +2 -1
  99. package/dist/src/serve-http.js +10 -1
  100. package/dist/src/serve.js +7 -4
  101. package/dist/src/signing.d.ts +23 -5
  102. package/dist/src/signing.js +74 -11
  103. package/dist/src/sigv4.d.ts +3 -1
  104. package/dist/src/sigv4.js +4 -2
  105. package/dist/src/smtp.js +2 -1
  106. package/dist/src/sockets.js +8 -3
  107. package/dist/src/state-system.d.ts +3 -0
  108. package/dist/src/storage.js +5 -2
  109. package/dist/src/twin-fetch.d.ts +3 -1
  110. package/dist/src/twin-fetch.js +3 -2
  111. package/dist/src/vendor-call.d.ts +10 -3
  112. package/dist/src/vendor-call.js +58 -14
  113. package/dist/test-fixtures/attestation.SOURCE.md +1 -0
  114. package/dist/test-fixtures/attestation.json +50 -0
  115. package/generated/pack-facts.json +409 -352
  116. package/inject.cjs +474 -95
  117. package/network-policy.cjs +110 -13
  118. package/network-policy.d.cts +8 -3
  119. package/package.json +11 -4
  120. package/src/actions.ts +2 -1
  121. package/src/anthropic-wire.ts +6 -0
  122. package/src/attestation.ts +23 -0
  123. package/src/bytes.ts +21 -0
  124. package/src/changeset.ts +2 -1
  125. package/src/clickhouse/sql.ts +50 -16
  126. package/src/derived-core.ts +573 -249
  127. package/src/derived-real.ts +552 -42
  128. package/src/derived.ts +23 -4
  129. package/src/events.ts +54 -85
  130. package/src/exact-json.ts +71 -0
  131. package/src/executor.ts +86 -27
  132. package/src/file-response.ts +1 -1
  133. package/src/form.ts +66 -0
  134. package/src/git/format.ts +89 -0
  135. package/src/git/index.ts +1 -0
  136. package/src/git/lfs.ts +5 -4
  137. package/src/graphql-wire.ts +8 -3
  138. package/src/grpc-wire.ts +2 -1
  139. package/src/head.ts +15 -5
  140. package/src/held-reads.ts +54 -0
  141. package/src/history.ts +5 -4
  142. package/src/host-port.ts +9 -0
  143. package/src/index.ts +10 -4
  144. package/src/log.ts +11 -7
  145. package/src/machines.ts +135 -13
  146. package/src/managed-database.ts +48 -40
  147. package/src/multipart.ts +110 -35
  148. package/src/observe.ts +6 -3
  149. package/src/openai-wire.ts +45 -14
  150. package/src/pack-fetch.ts +173 -50
  151. package/src/packRegistry.ts +27 -4
  152. package/src/private-transport.ts +8 -0
  153. package/src/protobuf.ts +8 -4
  154. package/src/rateBudget.ts +6 -1
  155. package/src/redis/engine.ts +168 -63
  156. package/src/redis/frames.ts +53 -0
  157. package/src/redis/index.ts +6 -0
  158. package/src/redis/json.ts +13 -0
  159. package/src/redis/lua-libs.ts +62 -0
  160. package/src/redis/lua.ts +20 -0
  161. package/src/redis/member-storage.ts +58 -0
  162. package/src/redis/resp2.ts +25 -0
  163. package/src/redis/stream.ts +102 -0
  164. package/src/remote-execute.ts +2 -2
  165. package/src/request-body.ts +12 -0
  166. package/src/runtime.ts +6 -5
  167. package/src/s3/wire.ts +8 -5
  168. package/src/scenario.ts +34 -13
  169. package/src/serve-http.ts +11 -2
  170. package/src/serve.ts +4 -3
  171. package/src/signing.ts +71 -17
  172. package/src/sigv4.ts +5 -3
  173. package/src/smtp.ts +3 -2
  174. package/src/sockets.ts +8 -3
  175. package/src/state-system.ts +3 -0
  176. package/src/storage.ts +5 -2
  177. package/src/twin-fetch.ts +4 -3
  178. package/src/vendor-call.ts +59 -12
  179. package/test-fixtures/attestation.json +50 -0
package/src/machines.ts CHANGED
@@ -3,27 +3,80 @@
3
3
  // `POST /_twin/machine-pool`, recorded in the pack's own tree; a handler reaches it as `ctx.machines`. `docker` runs
4
4
  // each machine as a local container with its internal ports published on loopback; `none` runs nothing; `fake`
5
5
  // records what it was asked (capability verification). Nothing here runs at module scope, and `node:child_process`
6
- // is reached only when a docker pool is asked to act.
6
+ // is reached only when a compute pool is asked to act. The light pool delegates to the host's Substrate adapter.
7
+ import { refuseReadOnlyWrite } from './request-scope.ts';
7
8
  import { nodeBuiltin } from './serve-http.ts';
8
9
 
9
- export type MachineSpec = { name: string; image: string; env?: Record<string, string>; ports?: number[]; command?: string[] };
10
- export type MachineRun = { ports: Array<{ internal: number; host: number }> };
10
+ export type MachineSpec = { name: string; image: string; env?: Record<string, string>; ports?: number[]; command?: string[]; checks?: Array<{ port: number; type: 'http' | 'tcp'; path?: string }>; resetOnStop?: boolean; mounts?: Array<{ name: string; path: string }>; signal?: AbortSignal; startTimeoutMs?: number };
11
+ export type GuestImage = { context: string; dockerfile?: string; runtime?: 'node' | 'bun' | 'python' | 'deno'; command?: string[]; cwd?: string; env?: Record<string, string>; prepared?: boolean };
12
+ export type LightPoolOptions = { images?: Record<string, GuestImage> };
13
+ export type MachineRun = { ports: Array<{ internal: number; host: number; url?: string }> };
14
+ export type MachineStatus = { state: 'started' | 'paused' | 'stopped' | 'exited' | 'failed'; exitCode?: number; reason?: string };
15
+ export type MachineFileStat = { type: 'file' | 'directory'; size: number; mode: number; mtime: string };
16
+ export type MachineFiles = {
17
+ read(path: string): Promise<Uint8Array>;
18
+ write(path: string, bytes: Uint8Array): Promise<unknown>;
19
+ list(path: string): Promise<Array<{ name: string; type: 'file' | 'directory'; size: number }>>;
20
+ mkdir(path: string): Promise<unknown>;
21
+ remove(path: string): Promise<unknown>;
22
+ stat(path: string): Promise<MachineFileStat>;
23
+ };
11
24
  /** A process started in a machine: its pid, and what it writes and how it ends, as they happen. */
12
25
  export type MachineProcess = { pid: number; events: AsyncIterable<{ stdout?: string } | { stderr?: string } | { exit: number }> };
13
26
  export type ExecSpec = { args: string[]; env?: Record<string, string>; cwd?: string };
27
+ export type MachineHttpReply = { port: number; method: string; path: string; requestText?: string; status: number; headers?: Record<string, string>; body?: string };
14
28
  export type MachinePool = {
15
- readonly kind: 'docker' | 'none' | 'fake';
29
+ readonly kind: 'docker' | 'none' | 'fake' | 'light';
30
+ /** Optional capability admission, before vendor resource creation; starts no process. */
31
+ admitRun?(spec: MachineSpec): Promise<void>;
16
32
  /** runs the image; a failure (no container runtime, an image that will not start) rejects with the runtime's message */
17
33
  run(spec: MachineSpec): Promise<MachineRun>;
18
34
  /** starts a process in a running machine; a machine the pool does not run rejects */
19
35
  exec(name: string, spec: ExecSpec): Promise<MachineProcess>;
36
+ request(name: string, port: number, request: Request): Promise<Response>;
37
+ /** Undefined means the metadata-only/fake backing has no process lifecycle to observe. */
38
+ status(name: string): Promise<MachineStatus | undefined>;
39
+ ports(name: string): Promise<number[]>;
40
+ files(name: string): Promise<MachineFiles>;
41
+ pause(name: string): Promise<void>;
42
+ resume(name: string): Promise<void>;
20
43
  stop(name: string): Promise<void>;
21
44
  remove(name: string): Promise<void>;
22
45
  /** the fake pool's record of what it was asked */
23
- readonly asked?: Array<{ act: 'run' | 'exec' | 'stop' | 'remove'; name: string; spec?: MachineSpec; exec?: ExecSpec }>;
46
+ readonly asked?: Array<{ act: 'run' | 'exec' | 'request' | 'pause' | 'resume' | 'stop' | 'remove'; name: string; spec?: MachineSpec; exec?: ExecSpec; port?: number; path?: string; requestText?: string }>;
24
47
  };
48
+ /** A declared compute capability gap, distinct from a failed admitted guest. */
49
+ export function isMachineCapabilityGap(error: unknown): boolean {
50
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === 'WORLD_MACHINE_CAPABILITY_GAP';
51
+ }
25
52
  export const MACHINE_POOL = '_machine_pool';
26
- export const POOL_KINDS = ['docker', 'none', 'fake'] as const;
53
+ export const POOL_KINDS = ['docker', 'none', 'fake', 'light'] as const;
54
+
55
+ /** The host owns provider bootstrapping. Browser/kernel imports stay inert, and a missing
56
+ * host provider refuses rather than falling back to metadata-only success. */
57
+ function lightPool(world: string, root: string | undefined, options: LightPoolOptions): MachinePool {
58
+ let ready: Promise<MachinePool> | undefined;
59
+ const load = (): Promise<MachinePool> => ready ??= (async () => {
60
+ const entry = typeof process === 'undefined' ? undefined : process.env.VOLTER_WORLD_GUEST_RUNNER;
61
+ if (!entry) throw new Error('light guest runner is unavailable: run this twin inside a World');
62
+ const provider = await import(entry);
63
+ return provider.createLightGuestPool({ world, root, ...options }) as MachinePool;
64
+ })();
65
+ return {
66
+ kind: 'light',
67
+ admitRun: async (spec) => { await (await load()).admitRun?.(spec); },
68
+ run: async (spec) => (await load()).run(spec),
69
+ exec: async (name, spec) => (await load()).exec(name, spec),
70
+ request: async (name, port, request) => (await load()).request(name, port, request),
71
+ status: async (name) => (await load()).status(name),
72
+ ports: async (name) => (await load()).ports(name),
73
+ files: async (name) => (await load()).files(name),
74
+ pause: async (name) => (await load()).pause(name),
75
+ resume: async (name) => (await load()).resume(name),
76
+ stop: async (name) => (await load()).stop(name),
77
+ remove: async (name) => (await load()).remove(name),
78
+ };
79
+ }
27
80
 
28
81
  /** A container's name for a machine: the World's prefix, the World's own mark (a machine id repeats across Worlds and
29
82
  * branches, which mint alike), then the vendor's machine name made safe for docker. */
@@ -41,6 +94,12 @@ function dockerPool(world: string): MachinePool {
41
94
  });
42
95
  return {
43
96
  kind: 'docker',
97
+ async status(name) {
98
+ const state = JSON.parse(await docker(['inspect', '--format', '{{json .State}}', containerName(world, name)]));
99
+ return state.Running ? { state: 'started' } : { state: 'exited', exitCode: state.ExitCode, ...(state.Error ? { reason: state.Error } : {}) };
100
+ },
101
+ async ports() { throw new Error('port discovery requires the light guest backing'); },
102
+ async files() { throw new Error('file access requires the light guest backing'); },
44
103
  async run(spec) {
45
104
  const name = containerName(world, spec.name);
46
105
  await docker(['rm', '-f', name]).catch(() => undefined);
@@ -105,47 +164,110 @@ function dockerPool(world: string): MachinePool {
105
164
  const timeout = new Promise<number>((_, reject) => { timer = setTimeout(() => reject(new Error('the process did not start')), STEP_TIMEOUT_MS); });
106
165
  try { return { pid: await Promise.race([pidKnown, timeout]), events }; } finally { clearTimeout(timer); }
107
166
  },
167
+ async request(name, port, request) {
168
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('invalid machine port');
169
+ const line = (await docker(['port', containerName(world, name), String(port)])).split('\n')[0] ?? '';
170
+ const host = Number(line.split(':').at(-1));
171
+ if (!Number.isInteger(host) || host < 1 || host > 65535 || !line.startsWith('127.0.0.1:')) throw new Error('the machine port is not published on loopback');
172
+ const source = new URL(request.url);
173
+ const headers = new Headers(request.headers);
174
+ for (const key of ['host', 'connection', 'content-length', 'transfer-encoding']) headers.delete(key);
175
+ const body = ['GET', 'HEAD'].includes(request.method) ? undefined : await request.clone().arrayBuffer();
176
+ return fetch(`http://127.0.0.1:${host}${source.pathname}${source.search}`, { method: request.method, headers, body, redirect: 'manual', signal: request.signal });
177
+ },
178
+ async pause(name) { await docker(['pause', containerName(world, name)]); },
179
+ async resume(name) { await docker(['unpause', containerName(world, name)]); },
108
180
  async stop(name) { await docker(['stop', containerName(world, name)]).catch(() => undefined); },
109
181
  async remove(name) { await docker(['rm', '-f', containerName(world, name)]).catch(() => undefined); },
110
182
  };
111
183
  }
112
184
 
113
185
  /** The pool of a kind, for a World (`world`: the mark its containers carry). */
114
- export function machinePool(kind: MachinePool['kind'], world = 'world'): MachinePool {
186
+ export function machinePool(kind: MachinePool['kind'], world = 'world', httpReplies: readonly MachineHttpReply[] = [], light: LightPoolOptions = {}, root?: string): MachinePool {
187
+ if (kind === 'light') return lightPool(world, root, light);
115
188
  if (kind === 'docker') return dockerPool(world);
116
189
  if (kind === 'fake') {
117
190
  const asked: NonNullable<MachinePool['asked']> = [];
191
+ const running = new Map<string, { ports: number[]; paused: boolean }>();
118
192
  return {
119
193
  kind, asked,
120
- async run(spec) { asked.push({ act: 'run', name: spec.name, spec }); return { ports: (spec.ports ?? []).map((internal) => ({ internal, host: 40_000 + internal })) }; },
194
+ async status() { return undefined; },
195
+ async ports(name) { return running.get(name)?.ports ?? []; },
196
+ async files() { throw new Error('the fake pool has no filesystem'); },
197
+ async run(spec) { running.set(spec.name, { ports: spec.ports ?? [], paused: false }); asked.push({ act: 'run', name: spec.name, spec }); return { ports: (spec.ports ?? []).map((internal) => ({ internal, host: 40_000 + internal })) }; },
121
198
  async exec(name, spec) {
199
+ if (!running.has(name) || running.get(name)!.paused) throw new Error('the fake machine is not running');
122
200
  asked.push({ act: 'exec', name, exec: spec });
123
201
  // a fake process: its pid the count of processes asked, and it ends at once
124
202
  const pid = asked.filter((a) => a.act === 'exec').length + 100;
125
203
  return { pid, events: (async function* () { yield { exit: 0 }; })() };
126
204
  },
127
- async stop(name) { asked.push({ act: 'stop', name }); },
128
- async remove(name) { asked.push({ act: 'remove', name }); },
205
+ async request(name, port, request) {
206
+ const machine = running.get(name);
207
+ if (!machine || machine.paused || !machine.ports.includes(port)) throw new Error('the fake machine port is not running');
208
+ const url = new URL(request.url), path = url.pathname + url.search;
209
+ const requestText = ['GET', 'HEAD'].includes(request.method) ? '' : await request.clone().text();
210
+ asked.push({ act: 'request', name, port, path, requestText });
211
+ const reply = httpReplies.find((r) => r.port === port && r.method === request.method && r.path === path && (r.requestText === undefined || r.requestText === requestText));
212
+ if (!reply) throw new Error('no fake HTTP reply matches');
213
+ return new Response(reply.body ?? null, { status: reply.status, headers: reply.headers });
214
+ },
215
+ async pause(name) { if (!running.has(name)) throw new Error('no fake machine'); running.get(name)!.paused = true; asked.push({ act: 'pause', name }); },
216
+ async resume(name) { if (!running.has(name)) throw new Error('no fake machine'); running.get(name)!.paused = false; asked.push({ act: 'resume', name }); },
217
+ async stop(name) { running.delete(name); asked.push({ act: 'stop', name }); },
218
+ async remove(name) { running.delete(name); asked.push({ act: 'remove', name }); },
129
219
  };
130
220
  }
131
221
  return {
132
222
  kind: 'none', async run() { return { ports: [] }; },
223
+ async status() { return undefined; },
224
+ async ports() { throw new Error('no guest ports'); },
225
+ async files() { throw new Error('no guest files'); },
133
226
  async exec() { throw new Error('no machine runs in this World: its machine pool is none'); },
227
+ async request() { throw new Error('no machine runs in this World: its machine pool is none'); },
228
+ async pause() {},
229
+ async resume() {},
134
230
  async stop() { /* nothing runs */ }, async remove() { /* nothing runs */ },
135
231
  };
136
232
  }
137
233
 
138
234
  /** A vendor's pool in a World, one per World and kind for the life of the process (a fake pool keeps what it was asked). */
139
235
  const pools = new Map<string, MachinePool>();
140
- export function poolFor(service: string, root: string | undefined, kind: MachinePool['kind']): MachinePool {
141
- const key = `${service}\0${root ?? ''}\0${kind}`;
236
+ export function poolFor(service: string, root: string | undefined, kind: MachinePool['kind'], httpReplies: readonly MachineHttpReply[] = [], light: LightPoolOptions = {}): MachinePool {
237
+ const key = `${service}\0${root ?? ''}\0${kind}${kind === 'light' ? JSON.stringify(light) : ''}`;
142
238
  let pool = pools.get(key);
143
239
  if (!pool) {
144
240
  // the World's mark: a short digest of the vendor and the World's tree, so two Worlds' machines never share a name
145
241
  let h = 0;
146
242
  for (const ch of `${service}:${root ?? ''}`) h = (Math.imul(h, 31) + ch.charCodeAt(0)) >>> 0;
147
- pool = machinePool(kind, h.toString(16).padStart(8, '0'));
243
+ pool = machinePool(kind, h.toString(16).padStart(8, '0'), httpReplies, light, root);
148
244
  pools.set(key, pool);
149
245
  }
150
246
  return pool;
151
247
  }
248
+
249
+ /** Re-enrolling a verification pool starts a fresh fake execution record. */
250
+ export function resetFakePool(service: string, root: string | undefined): void { pools.delete(`${service}\0${root ?? ''}\0fake`); }
251
+
252
+ /** Every machine mutation shares the tree's refusal seam. HTTP/RPC reads must be explicitly
253
+ * classified by the pack; a computed operation may execute arbitrary guest code. */
254
+ export function guardedMachinePool(pool: MachinePool, service: string, safeRead: boolean): MachinePool {
255
+ const guard = (): void => refuseReadOnlyWrite(service);
256
+ return {
257
+ kind: pool.kind, asked: pool.asked,
258
+ ...(pool.admitRun ? { admitRun: (spec: MachineSpec) => { guard(); return pool.admitRun!(spec); } } : {}),
259
+ run: (spec) => { guard(); return pool.run(spec); },
260
+ exec: (name, spec) => { guard(); return pool.exec(name, spec); },
261
+ request: (name, port, request) => { if (!safeRead) guard(); return pool.request(name, port, request); },
262
+ status: (name) => pool.status(name),
263
+ ports: (name) => pool.ports(name),
264
+ files: async (name) => {
265
+ const files = await pool.files(name);
266
+ return { ...files, write: (path, bytes) => { guard(); return files.write(path, bytes); }, mkdir: (path) => { guard(); return files.mkdir(path); }, remove: (path) => { guard(); return files.remove(path); } };
267
+ },
268
+ pause: (name) => { guard(); return pool.pause(name); },
269
+ resume: (name) => { guard(); return pool.resume(name); },
270
+ stop: (name) => { guard(); return pool.stop(name); },
271
+ remove: (name) => { guard(); return pool.remove(name); },
272
+ };
273
+ }
@@ -52,47 +52,15 @@ export type Batch = { role?: string; settings?: Record<string, string>; readOnly
52
52
  export interface ManagedDatabase {
53
53
  /** Whether the World binds a database at all (a World with none refuses every batch). */
54
54
  readonly bound: boolean;
55
+ /** The URL the World binds it at, what an application connects to (a credential door's database URL); none when
56
+ * unbound. */
57
+ readonly url?: string;
55
58
  /** One transaction as one pipeline and one Sync; the answer is the batch's own statements', in order. */
56
59
  batch(batch: Batch): Promise<StatementResult[]>;
57
60
  /** A whole SQL script as one simple Query (a vendor's migrations, several statements to a file). */
58
61
  script(text: string): Promise<void>;
59
62
  }
60
63
 
61
- /** The transaction-local setting every batch a handler makes carries: the World clock at the call (ctx.occurredAt). */
62
- export const WORLD_CLOCK_SETTING = 'volter.world_clock';
63
-
64
- /** The World clock inside the database, for the schemas a vendor lays (its migrations stamp rows with Postgres's
65
- * `now()`, which reads the wall clock): `volter.now()`, the World clock the batch set or the wall clock where none is
66
- * set, and every column default in `schemas` that reads `now()` or `CURRENT_TIMESTAMP` (alone or inside an expression,
67
- * GoTrue's `timezone('utc', now())`) rewritten to read it. Run after the schemas are laid; running it again changes
68
- * nothing. A trigger or function that calls `now()` itself is the pack's to redefine. */
69
- export function worldClockSql(schemas: string[]): string {
70
- const list = schemas.map((s) => `'${s.replace(/'/g, "''")}'`).join(', ');
71
- return `CREATE SCHEMA IF NOT EXISTS volter;
72
- CREATE OR REPLACE FUNCTION volter.now() RETURNS timestamptz LANGUAGE sql STABLE AS $fn$
73
- SELECT coalesce(nullif(current_setting('${WORLD_CLOCK_SETTING}', true), '')::timestamptz, pg_catalog.now())
74
- $fn$;
75
- GRANT USAGE ON SCHEMA volter TO PUBLIC;
76
- GRANT EXECUTE ON FUNCTION volter.now() TO PUBLIC;
77
- DO $do$
78
- DECLARE d record; rewritten text;
79
- BEGIN
80
- FOR d IN
81
- SELECT n.nspname AS schema, c.relname AS tbl, a.attname AS col, pg_get_expr(ad.adbin, ad.adrelid) AS expr
82
- FROM pg_attrdef ad
83
- JOIN pg_class c ON c.oid = ad.adrelid
84
- JOIN pg_namespace n ON n.oid = c.relnamespace
85
- JOIN pg_attribute a ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
86
- WHERE n.nspname IN (${list}) AND c.relkind IN ('r', 'p')
87
- AND pg_get_expr(ad.adbin, ad.adrelid) ~* '(^|[^.[:alnum:]_])(now\\(\\)|current_timestamp)'
88
- LOOP
89
- rewritten := regexp_replace(d.expr, '(^|[^.[:alnum:]_])(now\\(\\)|current_timestamp)', '\\1volter.now()', 'gi');
90
- EXECUTE format('ALTER TABLE %I.%I ALTER COLUMN %I SET DEFAULT %s', d.schema, d.tbl, d.col, rewritten);
91
- END LOOP;
92
- END
93
- $do$;`;
94
- }
95
-
96
64
  /** Quote an identifier the way Postgres's quote_ident does for a name that needs it. */
97
65
  export function quoteIdent(name: string): string {
98
66
  return `"${name.replace(/"/g, '""')}"`;
@@ -252,14 +220,13 @@ function settingsStatement(settings: Record<string, string>): Statement | null {
252
220
  }
253
221
 
254
222
  /** The World's Postgres at `url` (the runtime's binding, never the environment); with `readOnly`, every transaction is
255
- * READ ONLY whatever the batch asks (a read-only twin or request: Postgres itself refuses a write, 25006); with `clock`,
256
- * every batch carries the World clock as `volter.world_clock`. */
257
- export function managedDatabase(url: string, options: { readOnly?: boolean; clock?: string } = {}): ManagedDatabase {
223
+ * READ ONLY whatever the batch asks (a read-only twin or request: Postgres itself refuses a write, 25006). */
224
+ export function managedDatabase(url: string, options: { readOnly?: boolean } = {}): ManagedDatabase {
258
225
  return {
259
226
  bound: true,
227
+ url,
260
228
  async batch(batch) {
261
- // the World clock at the call, which volter.now() and the defaults worldClockSql rewrote read
262
- const set = settingsStatement({ ...(options.clock !== undefined ? { [WORLD_CLOCK_SETTING]: options.clock } : {}), ...(batch.settings ?? {}) });
229
+ const set = settingsStatement(batch.settings ?? {});
263
230
  const head: Statement[] = [
264
231
  { text: batch.readOnly || options.readOnly ? 'BEGIN READ ONLY' : 'BEGIN' },
265
232
  ...(batch.role === undefined ? [] : [{ text: `SET LOCAL ROLE ${quoteIdent(batch.role)}` }]),
@@ -283,6 +250,47 @@ export function managedDatabase(url: string, options: { readOnly?: boolean; cloc
283
250
  };
284
251
  }
285
252
 
253
+ /** One connection of an application's own to the World's Postgres (a journey's SQL steps: twin-standard
254
+ * walk/journey-kit.ts), its statements sent as its client sends them: a text alone as one simple Query (a migration
255
+ * file, a `BEGIN`), a text with values as one extended-protocol statement, the values bind parameters. A transaction it
256
+ * opens stays open across its statements until it ends it, as on the application's own connection. Each statement's
257
+ * answer is its results in Postgres's text format, untyped. */
258
+ export type DatabaseSession = { query(text: string, values?: ReadonlyArray<string | null>): Promise<StatementResult[]>; end(): Promise<void> };
259
+
260
+ type PgResult = { command?: string | null; rowCount?: number | null; fields?: Array<{ name: string }>; rows?: Array<Array<string | null>> };
261
+
262
+ /** The World's Postgres at `url` as one connection held until `end()`. */
263
+ export async function databaseSession(url: string): Promise<DatabaseSession> {
264
+ const { Pool } = await loadDriver();
265
+ const pool = new Pool({ connectionString: url, max: 1, idleTimeoutMillis: 0, connectionTimeoutMillis: 10_000 });
266
+ pool.on('error', () => {});
267
+ let client: PgClient;
268
+ try { client = await pool.connect(); } catch (error) {
269
+ await pool.end().catch(() => {});
270
+ throw new EngineUnavailableError(`the World's Postgres is not answering: ${(error as Error).message}`);
271
+ }
272
+ // every value as the wire carries it: a row an array of texts, no type parsed
273
+ const raw = { getTypeParser: () => (value: string) => value };
274
+ return {
275
+ async query(text, values) {
276
+ try {
277
+ const answer = (await (client.query({ text, ...(values === undefined ? {} : { values }), rowMode: 'array', types: raw }) as Promise<PgResult | PgResult[]>));
278
+ return (Array.isArray(answer) ? answer : [answer]).map((r) => ({
279
+ fields: (r.fields ?? []).map((f) => f.name),
280
+ rows: r.rows ?? [],
281
+ command: [r.command ?? '', r.command === 'INSERT' ? '0' : '', r.rowCount ?? ''].filter((part) => part !== '').join(' '),
282
+ }));
283
+ } catch (error) {
284
+ throw asDatabaseError(error as Error);
285
+ }
286
+ },
287
+ async end() {
288
+ client.release();
289
+ await pool.end().catch(() => {});
290
+ },
291
+ };
292
+ }
293
+
286
294
  /** A handler's database when the World binds none: every use is the refusal naming what to add. */
287
295
  export function unboundDatabase(service: string): ManagedDatabase {
288
296
  const refuse = (): never => {
package/src/multipart.ts CHANGED
@@ -1,51 +1,126 @@
1
- // A multipart/form-data body's parts, read from its bytes (RFC 7578): the one reading every pack's uploads go through.
2
- // A runtime's own form parser is not used: Bun's drops a part whose field name is empty, which a browser's or
3
- // supabase-js's Blob upload sends. Dependency-free and side-effect free (a browser bundle imports it).
1
+ // The one MIME multipart reader for requests and published examples. Dependency-free, side-effect-free and
2
+ // byte-preserving: a browser bundle imports it, and empty field names and binary uploads must survive reading.
4
3
 
5
- /** One part: its field name (possibly empty), its filename when it is a file, its media type as sent, its bytes. */
6
4
  export type MultipartPart = { name: string; filename: string | null; type: string | null; body: Uint8Array };
5
+ export type MultipartFramedPart = MultipartPart & {
6
+ headers: Uint8Array; leading: Uint8Array; separator: Uint8Array; trailing: Uint8Array;
7
+ };
8
+ export type MultipartEnvelope = {
9
+ complete: boolean; preamble: Uint8Array; epilogue: Uint8Array;
10
+ delimiters: Uint8Array[]; parts: MultipartFramedPart[];
11
+ };
7
12
 
8
- /** The boundary a `multipart/form-data` content type names, or null (not multipart, or no boundary). */
13
+ /** The boundary named by any multipart media type. */
9
14
  export function multipartBoundary(contentType: string | null | undefined): string | null {
10
- if (!contentType || !/^multipart\/form-data/i.test(contentType)) return null;
11
- const hit = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(contentType);
15
+ if (!contentType || !/^multipart\//i.test(contentType)) return null;
16
+ const hit = /\bboundary\s*=\s*(?:"([^"]+)"|([^;\s]+))/i.exec(contentType);
12
17
  return hit ? (hit[1] ?? hit[2]!) : null;
13
18
  }
14
19
 
15
- function indexOf(haystack: Uint8Array, needle: Uint8Array, from: number): number {
16
- outer: for (let i = from; i <= haystack.length - needle.length; i += 1) {
17
- for (let j = 0; j < needle.length; j += 1) if (haystack[i + j] !== needle[j]) continue outer;
18
- return i;
20
+ type Line = { at: number; end: number; after: number };
21
+ function byteLines(bytes: Uint8Array): Line[] {
22
+ const lines: Line[] = [];
23
+ for (let at = 0; at < bytes.length;) {
24
+ let end = at;
25
+ while (end < bytes.length && bytes[end] !== 13 && bytes[end] !== 10) end++;
26
+ const after = end === bytes.length ? end : end + (bytes[end] === 13 && bytes[end + 1] === 10 ? 2 : 1);
27
+ lines.push({ at, end, after });
28
+ at = after;
19
29
  }
20
- return -1;
30
+ return lines;
21
31
  }
22
32
 
23
- /** The parts of a body delimited by `boundary`, in order; a malformed tail ends the reading. */
24
- export function multipartParts(bytes: Uint8Array, boundary: string): MultipartPart[] {
25
- const encoder = new TextEncoder();
33
+ /** Used both by the byte reader and by consumers guarding placeholder spans within a part. */
34
+ export function multipartDelimiterPattern(boundary: string): string {
35
+ const literal = boundary.replace(/[.*+?^$(){}|[\]\\]/g, '\\$&');
36
+ // Include malformed whitespace-before-close lines in the guard: they cannot be hidden in a value either.
37
+ return '(?:^|(?<=[\\r\\n]))--' + literal + '(?:--|[ \\t]+--)?[ \\t]*(?=\\r\\n|\\n|\\r|$)';
38
+ }
39
+
40
+ /** Infer only a boundary with a corresponding closing line; Content-Type remains authoritative when supplied. */
41
+ export function inferMultipartBoundary(bytes: Uint8Array): string | null {
42
+ const decoder = new TextDecoder();
43
+ const lines = byteLines(bytes).map(({ at, end }) => decoder.decode(bytes.subarray(at, end)));
44
+ for (const line of lines) {
45
+ if (!line.startsWith('--')) continue;
46
+ const boundary = line.slice(2).replace(/[ \t]+$/, '');
47
+ if (boundary && lines.some((closing) => closing.replace(/[ \t]+$/, '') === '--' + boundary + '--')) return boundary;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /** Read MIME fields once, unfolding continuations for interpretation while retaining raw bytes in the envelope. */
53
+ function mimeHeaders(raw: Uint8Array): Map<string, string> | undefined {
54
+ const fields = new Map<string, string>();
55
+ let previous: string | undefined;
56
+ for (const line of new TextDecoder().decode(raw).split(/\r\n|\n|\r/)) {
57
+ if (!line) continue;
58
+ if (/^[ \t]/.test(line)) {
59
+ if (!previous) return undefined;
60
+ fields.set(previous, fields.get(previous)! + ' ' + line.trim());
61
+ } else {
62
+ const field = /^([^\s:]+):[ \t]*(.*)$/.exec(line);
63
+ if (!field) return undefined;
64
+ previous = field[1]!.toLowerCase();
65
+ fields.set(previous, field[2]!.trim());
66
+ }
67
+ }
68
+ return fields;
69
+ }
70
+
71
+ /** Raw MIME framing and interpreted parts. A malformed tail ends reading and leaves `complete` false. */
72
+ export function multipartEnvelope(bytes: Uint8Array, boundary: string): MultipartEnvelope {
73
+ const empty = new Uint8Array();
74
+ const result: MultipartEnvelope = { complete: false, preamble: bytes.slice(), epilogue: empty, delimiters: [], parts: [] };
75
+ const lines = byteLines(bytes);
26
76
  const decoder = new TextDecoder();
27
- const delimiter = encoder.encode(`--${boundary}`);
28
- const blank = encoder.encode('\r\n\r\n');
29
- const parts: MultipartPart[] = [];
30
- let at = indexOf(bytes, delimiter, 0);
31
- while (at >= 0) {
32
- const start = at + delimiter.length;
33
- // `--` after the delimiter closes the body
34
- if (bytes[start] === 0x2d && bytes[start + 1] === 0x2d) break;
35
- const headerEnd = indexOf(bytes, blank, start);
36
- if (headerEnd < 0) break;
37
- const next = indexOf(bytes, delimiter, headerEnd + 4);
38
- if (next < 0) break;
39
- const head = decoder.decode(bytes.subarray(start, headerEnd));
40
- const disposition = /content-disposition:[^\r\n]*/i.exec(head)?.[0] ?? '';
41
- parts.push({
77
+ const prefix = '--' + boundary;
78
+ const hits: Array<Line & { closing: boolean; valid: boolean }> = [];
79
+ for (const line of lines) {
80
+ const text = decoder.decode(bytes.subarray(line.at, line.end));
81
+ if (!text.startsWith(prefix)) continue;
82
+ const suffix = text.slice(prefix.length);
83
+ const closing = /^--[ \t]*$/.test(suffix);
84
+ const valid = closing || /^[ \t]*$/.test(suffix);
85
+ if (!valid && !/^[ \t]+--[ \t]*$/.test(suffix)) continue;
86
+ hits.push({ ...line, closing, valid });
87
+ if (closing || !valid) break;
88
+ }
89
+ if (!hits.length) return result;
90
+ result.preamble = bytes.slice(0, hits[0]!.at);
91
+ result.delimiters = hits.map(({ at, end }) => bytes.slice(at, end));
92
+ for (let i = 0; i < hits.length - 1; i++) {
93
+ const current = hits[i]!;
94
+ const next = hits[i + 1]!;
95
+ if (!current.valid || current.closing || current.after === current.end || !next.valid) return result;
96
+ const headerLines = lines.filter((line) => line.at >= current.after && line.at < next.at);
97
+ const blank = headerLines.find((line) => line.at === line.end);
98
+ if (!blank) return result;
99
+ const previous = headerLines.find((line) => line.after === blank.at);
100
+ const headersEnd = previous?.end ?? blank.at;
101
+ const headers = bytes.slice(current.after, headersEnd);
102
+ const fields = mimeHeaders(headers);
103
+ if (!fields) return result;
104
+ const beforeNext = lines.find((line) => line.after === next.at);
105
+ // The line break immediately before a delimiter belongs to framing, never to the uploaded bytes.
106
+ const bodyEnd = Math.max(blank.after, beforeNext?.end ?? next.at);
107
+ const disposition = fields.get('content-disposition') ?? '';
108
+ result.parts.push({
42
109
  name: /\bname="([^"]*)"/i.exec(disposition)?.[1] ?? '',
43
110
  filename: /\bfilename="([^"]*)"/i.exec(disposition)?.[1] ?? null,
44
- type: /content-type:\s*([^\r\n]+)/i.exec(head)?.[1]?.trim() ?? null,
45
- // the CRLF before the next delimiter is the delimiter's, not the part's
46
- body: bytes.slice(headerEnd + 4, next - 2),
111
+ type: fields.get('content-type')?.trim() ?? null,
112
+ body: bytes.slice(blank.after, bodyEnd), headers,
113
+ leading: bytes.slice(current.end, current.after), separator: bytes.slice(headersEnd, blank.after),
114
+ trailing: bytes.slice(bodyEnd, next.at),
47
115
  });
48
- at = next;
49
116
  }
50
- return parts;
117
+ const last = hits.at(-1)!;
118
+ result.complete = last.valid && last.closing;
119
+ if (result.complete) result.epilogue = bytes.slice(last.end);
120
+ return result;
121
+ }
122
+
123
+ /** The request's parts, in order, using the same framing reader as wire comparison. */
124
+ export function multipartParts(bytes: Uint8Array, boundary: string): MultipartPart[] {
125
+ return multipartEnvelope(bytes, boundary).parts.map(({ name, filename, type, body }) => ({ name, filename, type, body }));
51
126
  }
package/src/observe.ts CHANGED
@@ -42,7 +42,7 @@ export async function collectObservations<T>(fn: () => Promise<T>): Promise<{ va
42
42
 
43
43
  /** A type a batch listed in full: all of it, or (a type under a parent) the subjects whose `field` names one of the
44
44
  * parents listed `within`. */
45
- export type Completed = string | { type: string; field: string; within: string[] };
45
+ export type Completed = string | { type: string; field: string; within: string[] } | { type: string; ids: string[] };
46
46
 
47
47
  /** Fold a collected batch: one `observeResources` per service, `complete` applied to each. */
48
48
  export function foldObservations(observations: Observation[], opts: { root?: string; at?: string; complete?: Completed[]; batch?: string } = {}): ObserveReport {
@@ -134,10 +134,13 @@ function foldResources(service: string, resources: ObservedResource[], opts: { r
134
134
  }
135
135
  for (const c of opts.complete ?? []) {
136
136
  const type = typeof c === 'string' ? c : c.type;
137
- const within = typeof c === 'string' ? undefined : new Set(c.within);
137
+ const within = typeof c === 'string' || !('within' in c) ? undefined : new Set(c.within);
138
+ // subjects read one by one (a refresh of the known): only those read are whole, an unseen one is gone
139
+ const ids = typeof c !== 'string' && 'ids' in c ? new Set(c.ids) : undefined;
138
140
  for (const s of tree.values()) {
139
141
  if (s.type !== type || seen.has(`${s.type}:${s.id}`) || s.fields.deleted === true) continue;
140
- if (within && typeof c !== 'string' && !within.has(String(s.fields[c.field]))) continue;
142
+ if (within && typeof c !== 'string' && 'field' in c && !within.has(String(s.fields[c.field]))) continue;
143
+ if (ids && !ids.has(s.id)) continue;
141
144
  if (append({ type: s.type, id: s.id }, { deleted: true })) report.removed += 1;
142
145
  }
143
146
  }