@volter/world-runtime 3.0.37 → 3.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/dist/known-external-services.json +3 -10
  2. package/dist/src/actions-runner-cli.js +3 -3
  3. package/dist/src/actions-runner.js +2 -1
  4. package/dist/src/app-url.js +2 -1
  5. package/dist/src/backings.d.ts +10 -1
  6. package/dist/src/backings.js +66 -7
  7. package/dist/src/branch.js +85 -52
  8. package/dist/src/browser-routes.cjs +59 -0
  9. package/dist/src/ca-trust.js +4 -3
  10. package/dist/src/changeset.js +4 -3
  11. package/dist/src/cli-commands.js +25 -9
  12. package/dist/src/cli.js +2 -1
  13. package/dist/src/command-lifetime.d.mts +12 -0
  14. package/dist/src/command-lifetime.d.ts +1 -15
  15. package/dist/src/command-lifetime.js +1 -98
  16. package/dist/src/command-lifetime.mjs +90 -0
  17. package/dist/src/configs.js +1 -1
  18. package/dist/src/console-apart.js +3 -2
  19. package/dist/src/covers.d.ts +3 -2
  20. package/dist/src/covers.js +95 -18
  21. package/dist/src/error-message.d.mts +1 -0
  22. package/dist/src/error-message.d.ts +1 -0
  23. package/dist/src/error-message.js +2 -0
  24. package/dist/src/error-message.mjs +10 -0
  25. package/dist/src/guest-command.d.mts +4 -0
  26. package/dist/src/guest-command.mjs +26 -0
  27. package/dist/src/guest-control.d.mts +1 -0
  28. package/dist/src/guest-control.mjs +17 -0
  29. package/dist/src/guest-entry.mjs +146 -0
  30. package/dist/src/guest-processes.d.mts +9 -0
  31. package/dist/src/guest-processes.mjs +47 -0
  32. package/dist/src/guest-runner.d.ts +6 -0
  33. package/dist/src/guest-runner.js +567 -0
  34. package/dist/src/host.js +5 -4
  35. package/dist/src/index.d.ts +1 -0
  36. package/dist/src/index.js +1 -0
  37. package/dist/src/infra-cli.js +4 -0
  38. package/dist/src/infra.js +133 -23
  39. package/dist/src/init.js +60 -23
  40. package/dist/src/lifecycle-record.d.ts +3 -1
  41. package/dist/src/lifecycle-record.js +29 -6
  42. package/dist/src/livekit-backing.mjs +37 -0
  43. package/dist/src/local-branches.js +11 -3
  44. package/dist/src/mongodb-backing.d.ts +7 -0
  45. package/dist/src/mongodb-backing.js +58 -0
  46. package/dist/src/native-postgres-backing.d.ts +4 -0
  47. package/dist/src/native-postgres-backing.js +404 -0
  48. package/dist/src/origin.js +2 -1
  49. package/dist/src/owned-path.d.ts +4 -0
  50. package/dist/src/owned-path.js +37 -0
  51. package/dist/src/pack-facts.d.ts +1 -0
  52. package/dist/src/pglite-backing.d.ts +4 -13
  53. package/dist/src/pglite-backing.js +198 -106
  54. package/dist/src/pglite-host.mjs +47 -17
  55. package/dist/src/placeholder.d.ts +3 -2
  56. package/dist/src/placeholder.js +9 -4
  57. package/dist/src/process-groups.d.mts +2 -0
  58. package/dist/src/process-groups.d.ts +1 -4
  59. package/dist/src/process-groups.js +1 -49
  60. package/dist/src/process-groups.mjs +59 -0
  61. package/dist/src/process-receipt.d.mts +6 -0
  62. package/dist/src/process-receipt.d.ts +11 -0
  63. package/dist/src/process-receipt.js +3 -0
  64. package/dist/src/process-receipt.mjs +45 -0
  65. package/dist/src/process-signal.d.mts +9 -0
  66. package/dist/src/process-signal.mjs +92 -0
  67. package/dist/src/project-inspect.d.ts +1 -1
  68. package/dist/src/project-inspect.js +4 -2
  69. package/dist/src/proxy-daemon.js +2 -1
  70. package/dist/src/redirect-proxy.js +186 -19
  71. package/dist/src/redis-backing.js +99 -35
  72. package/dist/src/reflect.js +2 -1
  73. package/dist/src/root.js +7 -6
  74. package/dist/src/run-task-worker.js +2 -1
  75. package/dist/src/runtime.d.ts +24 -7
  76. package/dist/src/runtime.js +703 -286
  77. package/dist/src/schema.d.ts +12 -1
  78. package/dist/src/schema.js +20 -5
  79. package/dist/src/served-world.d.ts +1 -0
  80. package/dist/src/served-world.js +26 -14
  81. package/dist/src/service-recorder.js +8 -3
  82. package/dist/src/signals.d.mts +1 -0
  83. package/dist/src/signals.d.ts +1 -1
  84. package/dist/src/signals.js +1 -11
  85. package/dist/src/signals.mjs +10 -0
  86. package/dist/src/tail.d.ts +2 -0
  87. package/dist/src/tail.js +9 -2
  88. package/dist/src/tcp-port.js +27 -13
  89. package/dist/src/temporal-backing.mjs +28 -0
  90. package/dist/src/up-cli.js +8 -3
  91. package/dist/src/up-task-worker.js +2 -1
  92. package/dist/src/websocket-relay.d.ts +1 -1
  93. package/dist/src/websocket-relay.js +23 -10
  94. package/dist/src/world-view.js +2 -2
  95. package/known-external-services.json +3 -10
  96. package/package.json +12 -7
  97. package/src/actions-runner-cli.ts +3 -3
  98. package/src/actions-runner.ts +2 -1
  99. package/src/app-url.ts +2 -1
  100. package/src/backings.ts +65 -7
  101. package/src/branch.ts +73 -34
  102. package/src/browser-routes.cjs +59 -0
  103. package/src/ca-trust.ts +4 -3
  104. package/src/changeset.ts +4 -3
  105. package/src/cli-commands.ts +21 -9
  106. package/src/cli.ts +2 -1
  107. package/src/command-lifetime.d.mts +12 -0
  108. package/src/command-lifetime.mjs +90 -0
  109. package/src/command-lifetime.ts +1 -79
  110. package/src/configs.ts +1 -1
  111. package/src/console-apart.ts +2 -1
  112. package/src/covers.ts +97 -19
  113. package/src/error-message.d.mts +1 -0
  114. package/src/error-message.mjs +10 -0
  115. package/src/error-message.ts +2 -0
  116. package/src/guest-command.d.mts +4 -0
  117. package/src/guest-command.mjs +26 -0
  118. package/src/guest-control.d.mts +1 -0
  119. package/src/guest-control.mjs +17 -0
  120. package/src/guest-entry.mjs +146 -0
  121. package/src/guest-processes.d.mts +9 -0
  122. package/src/guest-processes.mjs +47 -0
  123. package/src/guest-runner.ts +414 -0
  124. package/src/host.ts +5 -4
  125. package/src/index.ts +1 -0
  126. package/src/infra-cli.ts +4 -0
  127. package/src/infra.ts +103 -22
  128. package/src/init.ts +55 -22
  129. package/src/lifecycle-record.ts +21 -6
  130. package/src/livekit-backing.mjs +37 -0
  131. package/src/local-branches.ts +6 -3
  132. package/src/mongodb-backing.ts +55 -0
  133. package/src/native-postgres-backing.ts +329 -0
  134. package/src/origin.ts +2 -1
  135. package/src/owned-path.ts +32 -0
  136. package/src/pack-facts.ts +1 -0
  137. package/src/pglite-backing.ts +155 -79
  138. package/src/pglite-host.mjs +47 -17
  139. package/src/placeholder.ts +10 -5
  140. package/src/process-groups.d.mts +2 -0
  141. package/src/process-groups.mjs +59 -0
  142. package/src/process-groups.ts +1 -33
  143. package/src/process-receipt.d.mts +6 -0
  144. package/src/process-receipt.mjs +45 -0
  145. package/src/process-receipt.ts +5 -0
  146. package/src/process-signal.d.mts +9 -0
  147. package/src/process-signal.mjs +92 -0
  148. package/src/project-inspect.ts +4 -2
  149. package/src/proxy-daemon.ts +2 -1
  150. package/src/redirect-proxy.ts +153 -18
  151. package/src/redis-backing.ts +74 -25
  152. package/src/reflect.ts +2 -1
  153. package/src/root.ts +7 -6
  154. package/src/run-task-worker.ts +2 -1
  155. package/src/runtime.ts +596 -265
  156. package/src/schema.ts +30 -6
  157. package/src/served-world.ts +21 -15
  158. package/src/service-recorder.ts +4 -2
  159. package/src/signals.d.mts +1 -0
  160. package/src/signals.mjs +10 -0
  161. package/src/signals.ts +1 -10
  162. package/src/tail.ts +10 -3
  163. package/src/tcp-port.ts +24 -13
  164. package/src/temporal-backing.mjs +28 -0
  165. package/src/up-cli.ts +6 -3
  166. package/src/up-task-worker.ts +2 -1
  167. package/src/websocket-relay.ts +18 -11
  168. package/src/world-view.ts +2 -2
@@ -0,0 +1,55 @@
1
+ // MongoDB's own server behind the existing declared-infrastructure lifecycle. Process ownership,
2
+ // logs, stopped-state copying and teardown stay in pglite-backing.ts and branch.ts, as for Postgres.
3
+ import type { HostLaunch, InfraService } from './pglite-backing.ts';
4
+
5
+ /** PATH is the binary contract, as for the native PostgreSQL example and LiveKit. No acquisition at boot. */
6
+ export function mongodbArgv(service: InfraService, data: string): string[] {
7
+ return ['mongod', '--bind_ip', '127.0.0.1', '--port', String(service.hostPort), '--dbpath', data,
8
+ ...(service.replicaSet ? ['--replSet', service.replicaSet] : [])];
9
+ }
10
+
11
+ /** MongoDB's structured record comes from this child, not a listener already holding the port. */
12
+ export function mongodbListening(log: string, port: number): boolean {
13
+ return log.split('\n').some((line) => {
14
+ try {
15
+ const record = JSON.parse(line);
16
+ return record.id === 23016 && record.msg === 'Waiting for connections' && record.attr?.port === port;
17
+ } catch { return false; } // Non-JSON log lines are not the structured readiness record.
18
+ });
19
+ }
20
+
21
+ /** Ping the server; initialize only an explicitly declared one-member set and await its primary. */
22
+ export async function mongodbReady(service: InfraService, launch: HostLaunch, deadline: number, initialize = true): Promise<void> {
23
+ const { MongoClient } = await import('mongodb');
24
+ // Use the infrastructure lifecycle's remaining readiness budget rather than another fixed limit.
25
+ const remaining = deadline - Date.now();
26
+ if (remaining <= 0) throw new Error('World-managed MongoDB readiness deadline elapsed');
27
+ const client = new MongoClient(`mongodb://127.0.0.1:${service.hostPort}`, {
28
+ directConnection: true, timeoutMS: remaining,
29
+ });
30
+ try {
31
+ await client.connect();
32
+ const admin = client.db('admin');
33
+ await admin.command({ ping: 1 });
34
+ if (!service.replicaSet) return;
35
+ let config;
36
+ try {
37
+ config = (await admin.command({ replSetGetConfig: 1 })).config;
38
+ } catch (error) {
39
+ // MongoDB's NotYetInitialized; any other refusal remains its own error.
40
+ if ((error as { code?: number }).code !== 94 || !initialize) throw error;
41
+ config = { _id: service.replicaSet, members: [{ _id: 0, host: `127.0.0.1:${service.hostPort}` }] };
42
+ await admin.command({ replSetInitiate: config });
43
+ }
44
+ if (config._id !== service.replicaSet || config.members?.length !== 1 || config.members[0].host !== `127.0.0.1:${service.hostPort}`) {
45
+ throw new Error('World-managed MongoDB replica-set name or member address differs from the declared service; refusing reconfiguration');
46
+ }
47
+ while (!(await admin.command({ hello: 1 })).isWritablePrimary) {
48
+ launch.signal?.throwIfAborted();
49
+ if (Date.now() >= deadline) throw new Error('World-managed MongoDB did not elect its single-node primary before the readiness deadline');
50
+ await new Promise((resolve) => setTimeout(resolve, 200));
51
+ }
52
+ } finally {
53
+ await client.close();
54
+ }
55
+ }
@@ -0,0 +1,329 @@
1
+ // World-managed multi-session PostgreSQL. The declaration, data, process and teardown
2
+ // belong to the infrastructure service; a recipe never operates postgres itself.
3
+ import { spawn, type ChildProcess } from 'node:child_process';
4
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync } from 'node:fs';
5
+ import { connect } from 'node:net';
6
+ import { join, resolve } from 'node:path';
7
+ import { commandLifetime, COMMAND_GRACE_MS, CommandRetirementError } from './command-lifetime.ts';
8
+ import { survivingOwnedGroups } from './process-groups.ts';
9
+ import { publishProcessReceipt, readProcessReceipt, readSpawnReceipt, spawnReceiptPath, verifySpawnIdentity, signalOwnedProcess, type SpawnIdentity } from './process-receipt.ts';
10
+ import { signalChild, childIdentity } from './process-signal.mjs';
11
+ import { errorMessage } from './error-message.ts';
12
+ import type { HostLaunch, InfraService } from './pglite-backing.ts';
13
+
14
+ interface Ownership { pid: number; data: string; port: number; binary: string }
15
+ interface Started { child: ChildProcess; owned: Ownership; closed: boolean; identity?: SpawnIdentity; birth?: Promise<SpawnIdentity | undefined>; error?: Error }
16
+ const starts = new Map<string, Started>();
17
+ const wait = (ms: number) => new Promise<void>(done => setTimeout(done, ms));
18
+ const recordPath = (root: string) => join(root, 'native-postgres.owner.json');
19
+ const startingPath = (root: string) => join(root, 'native-postgres.starting.json');
20
+ // Reuse pglite-backing.ts's 60-second managed readiness ceiling for the WHOLE
21
+ // startup, including initdb/psql/createdb. The conditioned native lifecycle walk
22
+ // measured native boot was 2487.76 ms (initdb 793.79 ms, psql 149.18 ms,
23
+ // createdb 81.13 ms) with empty PostgreSQL 17.9 state, Node 24.21, shared host,
24
+ // no app server, ae14857 plus this change; full command/fault shim source and
25
+ // output are in cookbook/calcom/output/native-lifetime.txt.
26
+ const READY_TIMEOUT_MS = 60_000;
27
+ // Retirement uses the existing command grace twice (grace, then confirmation),
28
+ // within the former native backing's 10-second shutdown budget. The same walk
29
+ // records cancellation, graceful shutdown and forced escalation; no new wall-clock limit.
30
+ function ownership(root: string) {
31
+ const records = [recordPath(root), startingPath(root)].map(file => readProcessReceipt(file, text => {
32
+ const row = JSON.parse(text) as Ownership;
33
+ if (!Number.isInteger(row?.pid) || row.pid < 0 || row.data !== resolve(root, 'native-postgres-data')
34
+ || !Number.isInteger(row.port) || row.port < 1 || row.port > 65535 || typeof row.binary !== 'string') {
35
+ throw new Error('Invalid native postgres ownership');
36
+ }
37
+ return row;
38
+ }));
39
+ const spawn = readSpawnReceipt(recordPath(root));
40
+ const values = records.flatMap(row => row.value ? [row.value] : []);
41
+ const errors = [...records, spawn].flatMap(row => row.error ? [row.error] : []);
42
+ if (values.length > 1 && values.some(row => JSON.stringify(row) !== JSON.stringify(values[0]))) {
43
+ errors.push(new Error('native postgres has conflicting ownership receipts; ownership uncertain'));
44
+ }
45
+ return { present: records.some(row => row.present) || spawn.present, held: values.find(row => row.pid === spawn.value?.pid) ?? values[0], identity: spawn.value, values, errors };
46
+ }
47
+ function verifyOwner(held: Ownership): void {
48
+ const lines = readFileSync(join(held.data, 'postmaster.pid'), 'utf8').split('\n');
49
+ if (Number(lines[0]) !== held.pid || resolve(lines[1]!) !== held.data || Number(lines[3]) !== held.port) {
50
+ throw new Error('native postgres ownership does not match the cluster; teardown refused');
51
+ }
52
+ }
53
+ const liveGroup = (pid: number) => pid > 0 && survivingOwnedGroups([pid]).length > 0;
54
+ function removeReceipts(root: string): void {
55
+ // Called only after confirmed retirement. Do not parse a damaged receipt while clearing it.
56
+ const errors: unknown[] = [];
57
+ for (const file of [recordPath(root), startingPath(root)]) {
58
+ try { rmSync(file, { force: true }); } catch (error) { errors.push(error); }
59
+ }
60
+ // Keep the independent identity while ANY damaged readiness record still needs cleanup.
61
+ if (!errors.length) {
62
+ try { rmSync(spawnReceiptPath(recordPath(root)), { force: true }); } catch (error) { errors.push(error); }
63
+ }
64
+ if (errors.length) throw new AggregateError(errors, 'native postgres retired but receipt cleanup failed');
65
+ }
66
+ async function retireIdentity(identity: SpawnIdentity): Promise<void> {
67
+ const { pid } = identity;
68
+ for (const signal of ['SIGINT', 'SIGKILL'] as const) {
69
+ if (!liveGroup(pid)) return;
70
+ await signalOwnedProcess(identity, signal, true);
71
+ const deadline = Date.now() + COMMAND_GRACE_MS;
72
+ while (liveGroup(pid) && Date.now() < deadline) await wait(20);
73
+ }
74
+ if (liveGroup(pid)) throw new Error(`native postgres group ${pid} retirement unconfirmed; ownership retained`);
75
+ }
76
+ /** A retained child is ownership even before postmaster.pid or the ready receipt exists. */
77
+ async function retireStarted(started: Started): Promise<void> {
78
+ const { child, owned } = started;
79
+ child.ref();
80
+ for (const signal of ['SIGINT', 'SIGKILL'] as const) {
81
+ if (started.closed && !liveGroup(owned.pid)) return;
82
+ if (owned.pid > 0 && !signalChild(child, signal, true) && liveGroup(owned.pid)) {
83
+ await signalOwnedProcess(started.identity ?? await started.birth, signal, true);
84
+ }
85
+ const deadline = Date.now() + COMMAND_GRACE_MS;
86
+ while (Date.now() < deadline) {
87
+ if (started.closed && !liveGroup(owned.pid)) return;
88
+ await wait(20);
89
+ }
90
+ }
91
+ throw new Error(`native postgres child ${owned.pid} retirement unconfirmed; ownership retained`);
92
+ }
93
+ async function listening(port: number, deadline: number, signal?: AbortSignal): Promise<boolean> {
94
+ signal?.throwIfAborted();
95
+ const remaining = deadline - Date.now();
96
+ if (remaining <= 0) throw new Error('native postgres readiness deadline elapsed');
97
+ return new Promise((done, fail) => {
98
+ const socket = connect({ host: '127.0.0.1', port, timeout: Math.min(1000, remaining) });
99
+ const settle = (value?: boolean, error?: unknown) => {
100
+ socket.destroy(); signal?.removeEventListener('abort', abort);
101
+ if (error) fail(error); else done(value!);
102
+ };
103
+ const abort = () => settle(undefined, signal!.reason);
104
+ signal?.addEventListener('abort', abort, { once: true });
105
+ socket.once('connect', () => settle(true));
106
+ socket.once('error', () => settle(false));
107
+ socket.once('timeout', () => settle(false));
108
+ if (signal?.aborted) abort();
109
+ });
110
+ }
111
+ /** Every helper joins exit and its private group, including spawn errors and abort. */
112
+ async function tool(command: string, args: string[], env: NodeJS.ProcessEnv, deadline: number, root: string, signal?: AbortSignal): Promise<string> {
113
+ signal?.throwIfAborted();
114
+ const remaining = deadline - Date.now();
115
+ if (remaining <= 0) throw new Error(`native postgres readiness deadline elapsed before ${command}`);
116
+ const child = spawn(command, args, { env, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
117
+ const lifetime = commandLifetime(child, signal);
118
+ let output = ''; let errorOutput = ''; let spawnError: Error | undefined; let timedOut = false; let failure: unknown;
119
+ child.stdout?.on('data', chunk => { output += chunk; });
120
+ child.stderr?.on('data', chunk => { errorOutput += chunk; });
121
+ child.on('error', error => { spawnError = error; });
122
+ const timer = setTimeout(() => { timedOut = true; lifetime.cancel('SIGTERM'); }, remaining);
123
+ const receipt = join(root, `native-postgres-tool-${child.pid ?? 'unspawned'}.json`);
124
+ try {
125
+ try {
126
+ const birth = await lifetime.identity;
127
+ if (birth?.start) {
128
+ publishProcessReceipt(spawnReceiptPath(receipt), JSON.stringify(birth) + '\n');
129
+ publishProcessReceipt(receipt, JSON.stringify({ pid: child.pid, command }) + '\n');
130
+ } else if (child.pid && liveGroup(child.pid)) throw new Error('native postgres helper birth capture failed; retiring held child');
131
+ } catch (error) {
132
+ lifetime.cancel('SIGTERM');
133
+ try { await lifetime.finish(); }
134
+ catch (cleanup) { throw new AggregateError([error, cleanup], 'native postgres helper publication and retirement failed'); }
135
+ throw error;
136
+ }
137
+ const result = await lifetime.finish();
138
+ signal?.throwIfAborted();
139
+ if (timedOut) throw new Error(`native postgres readiness deadline elapsed during ${command}`);
140
+ if (spawnError) throw spawnError;
141
+ if (result.code !== 0 || lifetime.forwarded) throw new Error(`${command} exited ${result.code}/${result.signal}: ${errorOutput.trim()}`);
142
+ return output;
143
+ } catch (error) {
144
+ failure = error;
145
+ if (error instanceof CommandRetirementError) {
146
+ error.message += `; native postgres helper ${command}, pid ${child.pid}, receipt ${receipt}`;
147
+ }
148
+ throw error;
149
+ } finally {
150
+ clearTimeout(timer);
151
+ // The lifetime has awaited closed output and vanished descendants. Unknown
152
+ // retirement retains this receipt and the World's rollback evidence.
153
+ if (!child.pid || !liveGroup(child.pid)) {
154
+ try { rmSync(receipt, { force: true }); rmSync(spawnReceiptPath(receipt), { force: true }); }
155
+ catch (cleanup) {
156
+ if (failure !== undefined) throw new AggregateError([failure, cleanup], 'native postgres helper failed and receipt cleanup failed');
157
+ throw cleanup;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ // init emits literal environment values in its fixed compose shape. Unknown forms
163
+ // are refused rather than interpreted as shell, substitutions or another config.
164
+ function settings(definition: string): { user: string; database: string } {
165
+ const values: Record<string, string> = {};
166
+ let postgres = false;
167
+ for (const line of definition.split('\n')) {
168
+ const service = line.match(/^ ([a-z][a-z0-9_-]*):$/);
169
+ if (service) postgres = service[1] === 'postgres';
170
+ const field = postgres && line.match(/^ POSTGRES_(USER|DB): (.+)$/);
171
+ if (field) {
172
+ const value = field[2]!;
173
+ values[field[1]!] = value.startsWith('"') ? JSON.parse(value) : value;
174
+ }
175
+ }
176
+ const user = values.USER || 'postgres';
177
+ const database = values.DB || user;
178
+ if ([user, database].some(value => typeof value !== 'string' || !value || /[\r\n\0$]/.test(value))) {
179
+ throw new Error('native postgres requires literal POSTGRES_USER and POSTGRES_DB');
180
+ }
181
+ return { user, database };
182
+ }
183
+
184
+ export async function nativePostgresUp(services: InfraService[], root: string, definition: string, launch: HostLaunch): Promise<void> {
185
+ if (services.length !== 1 || services[0]!.kind !== 'postgres') throw new Error('native postgres requires one declared postgres service');
186
+ const deadline = Date.now() + READY_TIMEOUT_MS;
187
+ const env: NodeJS.ProcessEnv = { ...(launch.env ?? process.env), LC_ALL: launch.env?.LC_ALL || 'C' };
188
+ const binaries = env.VOLTER_WORLD_POSTGRES_BIN;
189
+ if (!binaries) throw new Error('native postgres requires VOLTER_WORLD_POSTGRES_BIN (an installed PostgreSQL tool directory)');
190
+ for (const name of ['initdb', 'postgres', 'createdb', 'psql']) {
191
+ if (!existsSync(join(binaries, name))) throw new Error(`native postgres tool is missing: ${join(binaries, name)}`);
192
+ }
193
+ const previous = ownership(root);
194
+ if (previous.present || starts.has(root)) {
195
+ const pid = previous.identity?.pid ?? previous.held?.pid;
196
+ if (!previous.errors.length && !existsSync(startingPath(root)) && previous.held && pid && liveGroup(pid)) {
197
+ if (previous.identity) await verifySpawnIdentity(previous.identity);
198
+ else verifyOwner(previous.held);
199
+ throw new Error('native postgres is already running; stop this World before booting again');
200
+ }
201
+ await nativePostgresDown(root);
202
+ }
203
+ await retireTools(root);
204
+ const service = services[0]!;
205
+ if (await listening(service.hostPort, deadline, launch.signal)) throw new Error(`native postgres port ${service.hostPort} is already occupied`);
206
+ const data = resolve(root, 'native-postgres-data');
207
+ mkdirSync(data, { recursive: true, mode: 0o700 });
208
+ const { user, database } = settings(definition);
209
+ if (!existsSync(join(data, 'PG_VERSION'))) {
210
+ await tool(join(binaries, 'initdb'), ['-D', data, `--username=${user}`, '--auth=trust', '--no-locale', '--encoding=UTF8'], env, deadline, root, launch.signal);
211
+ }
212
+ launch.signal?.throwIfAborted();
213
+ const binary = join(binaries, 'postgres');
214
+ const log = join(root, 'native-postgres.log');
215
+ // Reserve durable startup evidence before spawning. A ready-receipt publication
216
+ // failure still leaves this receipt and the live handle for rollback.
217
+ const current: Ownership = { pid: 0, data, port: service.hostPort, binary };
218
+ publishProcessReceipt(startingPath(root), JSON.stringify(current) + '\n');
219
+ let started: Started | undefined;
220
+ try {
221
+ const fd = openSync(log, 'a');
222
+ let child: ChildProcess;
223
+ try {
224
+ child = spawn(binary, ['-D', data, '-h', '127.0.0.1', '-p', String(service.hostPort), '-k', ''], { env, detached: true, stdio: ['ignore', fd, fd] });
225
+ current.pid = child.pid ?? 0;
226
+ started = { child, owned: current, closed: false };
227
+ const observed = started;
228
+ child.on('error', error => { observed.error = error; });
229
+ child.once('close', () => { observed.closed = true; });
230
+ starts.set(root, started);
231
+ started.birth = childIdentity(child);
232
+ } finally { closeSync(fd); }
233
+ // The immutable spawn identity precedes either mutable ownership receipt.
234
+ started.identity = await started.birth;
235
+ if (child.pid && !started.identity?.start) throw new Error('native postgres birth capture failed; retiring held child');
236
+ if (child.pid) publishProcessReceipt(spawnReceiptPath(recordPath(root)), JSON.stringify(started.identity) + '\n');
237
+ publishProcessReceipt(startingPath(root), JSON.stringify(current) + '\n');
238
+ if (started.error) throw started.error;
239
+ if (!child.pid) throw new Error('native postgres spawn failed');
240
+ publishProcessReceipt(recordPath(root), JSON.stringify(current) + '\n');
241
+ for (;;) {
242
+ launch.signal?.throwIfAborted();
243
+ if (started.error) throw started.error;
244
+ if (child.exitCode !== null || child.signalCode !== null || started.closed) throw new Error(`native postgres exited before readiness; see ${log}`);
245
+ if (Date.now() >= deadline) throw new Error(`native postgres did not become ready; see ${log}`);
246
+ if (existsSync(join(data, 'postmaster.pid'))) {
247
+ verifyOwner(current);
248
+ const lines = readFileSync(join(data, 'postmaster.pid'), 'utf8').split('\n');
249
+ if (lines[7]?.trim() === 'ready' && await listening(service.hostPort, deadline, launch.signal)) break;
250
+ }
251
+ await wait(100);
252
+ }
253
+ if (database !== 'postgres') {
254
+ const sql = `SELECT 1 FROM pg_database WHERE datname = '${database.replaceAll("'", "''")}'`;
255
+ const output = await tool(join(binaries, 'psql'), ['-h', '127.0.0.1', '-p', String(service.hostPort), '-U', user, '-d', 'postgres', '-tAc', sql], env, deadline, root, launch.signal);
256
+ if (!output.trim()) await tool(join(binaries, 'createdb'), ['-h', '127.0.0.1', '-p', String(service.hostPort), '-U', user, '--', database], env, deadline, root, launch.signal);
257
+ }
258
+ launch.signal?.throwIfAborted();
259
+ if (child.exitCode !== null || child.signalCode !== null) throw new Error(`native postgres exited during readiness; see ${log}`);
260
+ rmSync(startingPath(root));
261
+ child.unref();
262
+ } catch (error) {
263
+ // The original publication/readiness failure survives every cleanup failure.
264
+ const failures: unknown[] = [error];
265
+ try {
266
+ if (started) await retireStarted(started);
267
+ removeReceipts(root);
268
+ starts.delete(root);
269
+ } catch (cleanup) { failures.push(cleanup); }
270
+ if (failures.length > 1) throw new AggregateError(failures, `native postgres startup and retirement failed; receipts retained`);
271
+ throw error;
272
+ }
273
+ }
274
+ export async function nativePostgresStatus(root: string): Promise<boolean> {
275
+ const state = ownership(root);
276
+ if (state.errors.length) throw new AggregateError(state.errors, 'native postgres ownership uncertain; retry down');
277
+ if (!state.held || !liveGroup(state.held.pid)) return false;
278
+ if (state.identity) await verifySpawnIdentity(state.identity);
279
+ verifyOwner(state.held);
280
+ return listening(state.held.port, Date.now() + READY_TIMEOUT_MS);
281
+ }
282
+
283
+ async function retireTools(root: string): Promise<void> {
284
+ if (!existsSync(root)) return;
285
+ const receipts = new Set(readdirSync(root).filter(name => /^native-postgres-tool-.*\.json(?:\.spawn\.json)?$/.test(name))
286
+ .map(name => name.endsWith('.spawn.json') ? name.slice(0, -'.spawn.json'.length) : name));
287
+ for (const file of receipts) {
288
+ const receipt = join(root, file);
289
+ const identity = readSpawnReceipt(receipt);
290
+ const recorded = readProcessReceipt(receipt, text => {
291
+ const row = JSON.parse(text);
292
+ if (!Number.isInteger(row.pid) || row.pid <= 0) throw new Error('Invalid helper PID');
293
+ return row as { pid: number };
294
+ });
295
+ const pid = identity.value?.pid ?? recorded.value?.pid;
296
+ if (!pid) throw new AggregateError([identity.error, recorded.error].filter(Boolean), `native postgres helper ownership uncertain; retained ${receipt}`);
297
+ if (liveGroup(pid)) {
298
+ if (!identity.value) throw new Error(`native postgres helper ${pid} has no verified spawn identity; ownership retained`);
299
+ await verifySpawnIdentity(identity.value);
300
+ await retireIdentity(identity.value);
301
+ }
302
+ rmSync(receipt, { force: true }); rmSync(spawnReceiptPath(receipt), { force: true });
303
+ }
304
+ }
305
+ export async function nativePostgresDown(root: string): Promise<void> {
306
+ const started = starts.get(root);
307
+ const state = ownership(root);
308
+ if (state.errors.length) process.stderr.write(`${errorMessage(new AggregateError(state.errors, 'native postgres ownership uncertain; retrying retirement through established identity'))}\n`);
309
+ if (started) {
310
+ await retireStarted(started);
311
+ } else if (state.identity) {
312
+ if (liveGroup(state.identity.pid)) {
313
+ await verifySpawnIdentity(state.identity);
314
+ await retireIdentity(state.identity);
315
+ }
316
+ } else if (state.held && state.held.pid > 0) {
317
+ if (liveGroup(state.held.pid)) {
318
+ throw new Error('native postgres spawn identity missing; retirement refused; receipts retained');
319
+ }
320
+ } else if (state.present) {
321
+ throw new AggregateError(state.errors, 'native postgres ownership uncertain; no established process identity; receipts retained');
322
+ }
323
+ if (state.values.some(row => row.pid > 0 && liveGroup(row.pid))) {
324
+ throw new AggregateError(state.errors, 'native postgres receipt still names a live or uncertain group; ownership retained');
325
+ }
326
+ removeReceipts(root);
327
+ starts.delete(root);
328
+ await retireTools(root);
329
+ }
package/src/origin.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { errorMessage } from './error-message.ts';
1
2
  import { captureHistory, historyDigest, originHead, publishOriginHistory, type HistoryLayout, type HistoryReference, readHistoryView, historyLength, worldPaths } from '@volter/world-core/runtime';
2
3
  // `volter world clone` and `fetch` — a world's ORIGIN is another world, served (docs/concepts/the-model.md
3
4
  // #a-branch-is-a-position-not-a-copy: a clone is a branch whose parent is at a URL): its whole log per twin is this world's PARENT. `clone` records the origin
@@ -137,7 +138,7 @@ export async function fetchFromOrigin(name: string, options: FetchOptions): Prom
137
138
  const { path: configPath, config } = loadWorldConfig(instance.configPath, root);
138
139
  const named = isPathRemote(origin.url) ? origin.url : `${origin.url}/${origin.namespace}`;
139
140
  if (config.remotes?.origin !== named) writeWorldConfig(configPath, { ...config, remotes: { ...(config.remotes ?? {}), origin: named } });
140
- } catch (error) { console.error(`[origin] ${name}: the remote was not named in its config: ${(error as Error).message}`); }
141
+ } catch (error) { console.error(`[origin] ${name}: the remote was not named in its config: ${errorMessage(error)}`); }
141
142
  }
142
143
  return { world: name, origin, full: options.full ?? false, appended, behind, snapshots };
143
144
  }
@@ -0,0 +1,32 @@
1
+ import { lstatSync, realpathSync } from 'node:fs';
2
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
3
+
4
+ function realBase(path: string): string {
5
+ // Init may plan a base that has not been created. Resolve its nearest existing ancestor, preserving the suffix.
6
+ if (lstatSync(path, { throwIfNoEntry: false })) return realpathSync(path);
7
+ return resolve(realBase(dirname(path)), basename(path));
8
+ }
9
+
10
+ /** Check against a previously resolved owner, so re-reading a changed base cannot move the containment boundary. */
11
+ export function assertOwnedRealPath(realOwner: string, path: string): void {
12
+ const rel = relative(realOwner, realpathSync(path));
13
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error('Invalid World-owned path');
14
+ }
15
+
16
+ /** A name-derived destination stays within its owner; inspect existing components before any write. */
17
+ export function ownedPath(base: string, name: string): string {
18
+ const lexicalRoot = resolve(base), rel = relative(lexicalRoot, resolve(lexicalRoot, name));
19
+ const invalid = (): never => { throw new Error('Invalid World-owned path'); };
20
+ if (isAbsolute(name) || !rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) invalid();
21
+ // The operator chooses the base (including a symlink to another volume); only the name-derived join is guarded.
22
+ const root = realBase(lexicalRoot), components: string[] = [];
23
+ let component = root;
24
+ for (const part of rel.split(sep)) { component = resolve(component, part); components.push(component); }
25
+ for (const [index, component] of components.entries()) {
26
+ const stat = lstatSync(component, { throwIfNoEntry: false });
27
+ if (stat && (stat.isSymbolicLink() || (index < components.length - 1 && !stat.isDirectory()))) invalid();
28
+ if (stat) assertOwnedRealPath(root, component);
29
+ }
30
+ // Preserve the operator's spelling: init plans later compare destinations relative to that same configured base.
31
+ return resolve(lexicalRoot, rel);
32
+ }
package/src/pack-facts.ts CHANGED
@@ -14,6 +14,7 @@ import factsDocument from '@volter/world-core/generated/pack-facts.json' with {
14
14
  export type PackAdoption = { sdks?: string[]; pypi?: string[]; scopes?: string[]; envStems?: string[]; worldIds?: string[]; tools?: Array<{ package: string; usage: 'build' | 'deployment' }>; configFiles?: Array<{ file: string; usage: 'application' | 'build' | 'deployment'; when?: string }> };
15
15
  export type PackFacts = {
16
16
  serverBacking?: { command: string };
17
+ maxRequestBodySize?: number;
17
18
  /** Existing descriptor transport, compiled into the same artifact as endpoint/adoption facts. */
18
19
  transport?: string;
19
20
  adoption?: PackAdoption;