@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,58 @@
1
+ /** PATH is the binary contract, as for the native PostgreSQL example and LiveKit. No acquisition at boot. */
2
+ export function mongodbArgv(service, data) {
3
+ return ['mongod', '--bind_ip', '127.0.0.1', '--port', String(service.hostPort), '--dbpath', data,
4
+ ...(service.replicaSet ? ['--replSet', service.replicaSet] : [])];
5
+ }
6
+ /** MongoDB's structured record comes from this child, not a listener already holding the port. */
7
+ export function mongodbListening(log, port) {
8
+ return log.split('\n').some((line) => {
9
+ try {
10
+ const record = JSON.parse(line);
11
+ return record.id === 23016 && record.msg === 'Waiting for connections' && record.attr?.port === port;
12
+ }
13
+ catch {
14
+ return false;
15
+ } // Non-JSON log lines are not the structured readiness record.
16
+ });
17
+ }
18
+ /** Ping the server; initialize only an explicitly declared one-member set and await its primary. */
19
+ export async function mongodbReady(service, launch, deadline, initialize = true) {
20
+ const { MongoClient } = await import('mongodb');
21
+ // Use the infrastructure lifecycle's remaining readiness budget rather than another fixed limit.
22
+ const remaining = deadline - Date.now();
23
+ if (remaining <= 0)
24
+ throw new Error('World-managed MongoDB readiness deadline elapsed');
25
+ const client = new MongoClient(`mongodb://127.0.0.1:${service.hostPort}`, {
26
+ directConnection: true, timeoutMS: remaining,
27
+ });
28
+ try {
29
+ await client.connect();
30
+ const admin = client.db('admin');
31
+ await admin.command({ ping: 1 });
32
+ if (!service.replicaSet)
33
+ return;
34
+ let config;
35
+ try {
36
+ config = (await admin.command({ replSetGetConfig: 1 })).config;
37
+ }
38
+ catch (error) {
39
+ // MongoDB's NotYetInitialized; any other refusal remains its own error.
40
+ if (error.code !== 94 || !initialize)
41
+ throw error;
42
+ config = { _id: service.replicaSet, members: [{ _id: 0, host: `127.0.0.1:${service.hostPort}` }] };
43
+ await admin.command({ replSetInitiate: config });
44
+ }
45
+ if (config._id !== service.replicaSet || config.members?.length !== 1 || config.members[0].host !== `127.0.0.1:${service.hostPort}`) {
46
+ throw new Error('World-managed MongoDB replica-set name or member address differs from the declared service; refusing reconfiguration');
47
+ }
48
+ while (!(await admin.command({ hello: 1 })).isWritablePrimary) {
49
+ launch.signal?.throwIfAborted();
50
+ if (Date.now() >= deadline)
51
+ throw new Error('World-managed MongoDB did not elect its single-node primary before the readiness deadline');
52
+ await new Promise((resolve) => setTimeout(resolve, 200));
53
+ }
54
+ }
55
+ finally {
56
+ await client.close();
57
+ }
58
+ }
@@ -0,0 +1,4 @@
1
+ import type { HostLaunch, InfraService } from './pglite-backing.js';
2
+ export declare function nativePostgresUp(services: InfraService[], root: string, definition: string, launch: HostLaunch): Promise<void>;
3
+ export declare function nativePostgresStatus(root: string): Promise<boolean>;
4
+ export declare function nativePostgresDown(root: string): Promise<void>;
@@ -0,0 +1,404 @@
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 } 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.js";
8
+ import { survivingOwnedGroups } from "./process-groups.js";
9
+ import { publishProcessReceipt, readProcessReceipt, readSpawnReceipt, spawnReceiptPath, verifySpawnIdentity, signalOwnedProcess } from "./process-receipt.js";
10
+ import { signalChild, childIdentity } from './process-signal.mjs';
11
+ import { errorMessage } from "./error-message.js";
12
+ const starts = new Map();
13
+ const wait = (ms) => new Promise(done => setTimeout(done, ms));
14
+ const recordPath = (root) => join(root, 'native-postgres.owner.json');
15
+ const startingPath = (root) => join(root, 'native-postgres.starting.json');
16
+ // Reuse pglite-backing.ts's 60-second managed readiness ceiling for the WHOLE
17
+ // startup, including initdb/psql/createdb. The conditioned native lifecycle walk
18
+ // measured native boot was 2487.76 ms (initdb 793.79 ms, psql 149.18 ms,
19
+ // createdb 81.13 ms) with empty PostgreSQL 17.9 state, Node 24.21, shared host,
20
+ // no app server, ae14857 plus this change; full command/fault shim source and
21
+ // output are in cookbook/calcom/output/native-lifetime.txt.
22
+ const READY_TIMEOUT_MS = 60_000;
23
+ // Retirement uses the existing command grace twice (grace, then confirmation),
24
+ // within the former native backing's 10-second shutdown budget. The same walk
25
+ // records cancellation, graceful shutdown and forced escalation; no new wall-clock limit.
26
+ function ownership(root) {
27
+ const records = [recordPath(root), startingPath(root)].map(file => readProcessReceipt(file, text => {
28
+ const row = JSON.parse(text);
29
+ if (!Number.isInteger(row?.pid) || row.pid < 0 || row.data !== resolve(root, 'native-postgres-data')
30
+ || !Number.isInteger(row.port) || row.port < 1 || row.port > 65535 || typeof row.binary !== 'string') {
31
+ throw new Error('Invalid native postgres ownership');
32
+ }
33
+ return row;
34
+ }));
35
+ const spawn = readSpawnReceipt(recordPath(root));
36
+ const values = records.flatMap(row => row.value ? [row.value] : []);
37
+ const errors = [...records, spawn].flatMap(row => row.error ? [row.error] : []);
38
+ if (values.length > 1 && values.some(row => JSON.stringify(row) !== JSON.stringify(values[0]))) {
39
+ errors.push(new Error('native postgres has conflicting ownership receipts; ownership uncertain'));
40
+ }
41
+ 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 };
42
+ }
43
+ function verifyOwner(held) {
44
+ const lines = readFileSync(join(held.data, 'postmaster.pid'), 'utf8').split('\n');
45
+ if (Number(lines[0]) !== held.pid || resolve(lines[1]) !== held.data || Number(lines[3]) !== held.port) {
46
+ throw new Error('native postgres ownership does not match the cluster; teardown refused');
47
+ }
48
+ }
49
+ const liveGroup = (pid) => pid > 0 && survivingOwnedGroups([pid]).length > 0;
50
+ function removeReceipts(root) {
51
+ // Called only after confirmed retirement. Do not parse a damaged receipt while clearing it.
52
+ const errors = [];
53
+ for (const file of [recordPath(root), startingPath(root)]) {
54
+ try {
55
+ rmSync(file, { force: true });
56
+ }
57
+ catch (error) {
58
+ errors.push(error);
59
+ }
60
+ }
61
+ // Keep the independent identity while ANY damaged readiness record still needs cleanup.
62
+ if (!errors.length) {
63
+ try {
64
+ rmSync(spawnReceiptPath(recordPath(root)), { force: true });
65
+ }
66
+ catch (error) {
67
+ errors.push(error);
68
+ }
69
+ }
70
+ if (errors.length)
71
+ throw new AggregateError(errors, 'native postgres retired but receipt cleanup failed');
72
+ }
73
+ async function retireIdentity(identity) {
74
+ const { pid } = identity;
75
+ for (const signal of ['SIGINT', 'SIGKILL']) {
76
+ if (!liveGroup(pid))
77
+ return;
78
+ await signalOwnedProcess(identity, signal, true);
79
+ const deadline = Date.now() + COMMAND_GRACE_MS;
80
+ while (liveGroup(pid) && Date.now() < deadline)
81
+ await wait(20);
82
+ }
83
+ if (liveGroup(pid))
84
+ throw new Error(`native postgres group ${pid} retirement unconfirmed; ownership retained`);
85
+ }
86
+ /** A retained child is ownership even before postmaster.pid or the ready receipt exists. */
87
+ async function retireStarted(started) {
88
+ const { child, owned } = started;
89
+ child.ref();
90
+ for (const signal of ['SIGINT', 'SIGKILL']) {
91
+ if (started.closed && !liveGroup(owned.pid))
92
+ return;
93
+ if (owned.pid > 0 && !signalChild(child, signal, true) && liveGroup(owned.pid)) {
94
+ await signalOwnedProcess(started.identity ?? await started.birth, signal, true);
95
+ }
96
+ const deadline = Date.now() + COMMAND_GRACE_MS;
97
+ while (Date.now() < deadline) {
98
+ if (started.closed && !liveGroup(owned.pid))
99
+ return;
100
+ await wait(20);
101
+ }
102
+ }
103
+ throw new Error(`native postgres child ${owned.pid} retirement unconfirmed; ownership retained`);
104
+ }
105
+ async function listening(port, deadline, signal) {
106
+ signal?.throwIfAborted();
107
+ const remaining = deadline - Date.now();
108
+ if (remaining <= 0)
109
+ throw new Error('native postgres readiness deadline elapsed');
110
+ return new Promise((done, fail) => {
111
+ const socket = connect({ host: '127.0.0.1', port, timeout: Math.min(1000, remaining) });
112
+ const settle = (value, error) => {
113
+ socket.destroy();
114
+ signal?.removeEventListener('abort', abort);
115
+ if (error)
116
+ fail(error);
117
+ else
118
+ done(value);
119
+ };
120
+ const abort = () => settle(undefined, signal.reason);
121
+ signal?.addEventListener('abort', abort, { once: true });
122
+ socket.once('connect', () => settle(true));
123
+ socket.once('error', () => settle(false));
124
+ socket.once('timeout', () => settle(false));
125
+ if (signal?.aborted)
126
+ abort();
127
+ });
128
+ }
129
+ /** Every helper joins exit and its private group, including spawn errors and abort. */
130
+ async function tool(command, args, env, deadline, root, signal) {
131
+ signal?.throwIfAborted();
132
+ const remaining = deadline - Date.now();
133
+ if (remaining <= 0)
134
+ throw new Error(`native postgres readiness deadline elapsed before ${command}`);
135
+ const child = spawn(command, args, { env, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
136
+ const lifetime = commandLifetime(child, signal);
137
+ let output = '';
138
+ let errorOutput = '';
139
+ let spawnError;
140
+ let timedOut = false;
141
+ let failure;
142
+ child.stdout?.on('data', chunk => { output += chunk; });
143
+ child.stderr?.on('data', chunk => { errorOutput += chunk; });
144
+ child.on('error', error => { spawnError = error; });
145
+ const timer = setTimeout(() => { timedOut = true; lifetime.cancel('SIGTERM'); }, remaining);
146
+ const receipt = join(root, `native-postgres-tool-${child.pid ?? 'unspawned'}.json`);
147
+ try {
148
+ try {
149
+ const birth = await lifetime.identity;
150
+ if (birth?.start) {
151
+ publishProcessReceipt(spawnReceiptPath(receipt), JSON.stringify(birth) + '\n');
152
+ publishProcessReceipt(receipt, JSON.stringify({ pid: child.pid, command }) + '\n');
153
+ }
154
+ else if (child.pid && liveGroup(child.pid))
155
+ throw new Error('native postgres helper birth capture failed; retiring held child');
156
+ }
157
+ catch (error) {
158
+ lifetime.cancel('SIGTERM');
159
+ try {
160
+ await lifetime.finish();
161
+ }
162
+ catch (cleanup) {
163
+ throw new AggregateError([error, cleanup], 'native postgres helper publication and retirement failed');
164
+ }
165
+ throw error;
166
+ }
167
+ const result = await lifetime.finish();
168
+ signal?.throwIfAborted();
169
+ if (timedOut)
170
+ throw new Error(`native postgres readiness deadline elapsed during ${command}`);
171
+ if (spawnError)
172
+ throw spawnError;
173
+ if (result.code !== 0 || lifetime.forwarded)
174
+ throw new Error(`${command} exited ${result.code}/${result.signal}: ${errorOutput.trim()}`);
175
+ return output;
176
+ }
177
+ catch (error) {
178
+ failure = error;
179
+ if (error instanceof CommandRetirementError) {
180
+ error.message += `; native postgres helper ${command}, pid ${child.pid}, receipt ${receipt}`;
181
+ }
182
+ throw error;
183
+ }
184
+ finally {
185
+ clearTimeout(timer);
186
+ // The lifetime has awaited closed output and vanished descendants. Unknown
187
+ // retirement retains this receipt and the World's rollback evidence.
188
+ if (!child.pid || !liveGroup(child.pid)) {
189
+ try {
190
+ rmSync(receipt, { force: true });
191
+ rmSync(spawnReceiptPath(receipt), { force: true });
192
+ }
193
+ catch (cleanup) {
194
+ if (failure !== undefined)
195
+ throw new AggregateError([failure, cleanup], 'native postgres helper failed and receipt cleanup failed');
196
+ throw cleanup;
197
+ }
198
+ }
199
+ }
200
+ }
201
+ // init emits literal environment values in its fixed compose shape. Unknown forms
202
+ // are refused rather than interpreted as shell, substitutions or another config.
203
+ function settings(definition) {
204
+ const values = {};
205
+ let postgres = false;
206
+ for (const line of definition.split('\n')) {
207
+ const service = line.match(/^ ([a-z][a-z0-9_-]*):$/);
208
+ if (service)
209
+ postgres = service[1] === 'postgres';
210
+ const field = postgres && line.match(/^ POSTGRES_(USER|DB): (.+)$/);
211
+ if (field) {
212
+ const value = field[2];
213
+ values[field[1]] = value.startsWith('"') ? JSON.parse(value) : value;
214
+ }
215
+ }
216
+ const user = values.USER || 'postgres';
217
+ const database = values.DB || user;
218
+ if ([user, database].some(value => typeof value !== 'string' || !value || /[\r\n\0$]/.test(value))) {
219
+ throw new Error('native postgres requires literal POSTGRES_USER and POSTGRES_DB');
220
+ }
221
+ return { user, database };
222
+ }
223
+ export async function nativePostgresUp(services, root, definition, launch) {
224
+ if (services.length !== 1 || services[0].kind !== 'postgres')
225
+ throw new Error('native postgres requires one declared postgres service');
226
+ const deadline = Date.now() + READY_TIMEOUT_MS;
227
+ const env = { ...(launch.env ?? process.env), LC_ALL: launch.env?.LC_ALL || 'C' };
228
+ const binaries = env.VOLTER_WORLD_POSTGRES_BIN;
229
+ if (!binaries)
230
+ throw new Error('native postgres requires VOLTER_WORLD_POSTGRES_BIN (an installed PostgreSQL tool directory)');
231
+ for (const name of ['initdb', 'postgres', 'createdb', 'psql']) {
232
+ if (!existsSync(join(binaries, name)))
233
+ throw new Error(`native postgres tool is missing: ${join(binaries, name)}`);
234
+ }
235
+ const previous = ownership(root);
236
+ if (previous.present || starts.has(root)) {
237
+ const pid = previous.identity?.pid ?? previous.held?.pid;
238
+ if (!previous.errors.length && !existsSync(startingPath(root)) && previous.held && pid && liveGroup(pid)) {
239
+ if (previous.identity)
240
+ await verifySpawnIdentity(previous.identity);
241
+ else
242
+ verifyOwner(previous.held);
243
+ throw new Error('native postgres is already running; stop this World before booting again');
244
+ }
245
+ await nativePostgresDown(root);
246
+ }
247
+ await retireTools(root);
248
+ const service = services[0];
249
+ if (await listening(service.hostPort, deadline, launch.signal))
250
+ throw new Error(`native postgres port ${service.hostPort} is already occupied`);
251
+ const data = resolve(root, 'native-postgres-data');
252
+ mkdirSync(data, { recursive: true, mode: 0o700 });
253
+ const { user, database } = settings(definition);
254
+ if (!existsSync(join(data, 'PG_VERSION'))) {
255
+ await tool(join(binaries, 'initdb'), ['-D', data, `--username=${user}`, '--auth=trust', '--no-locale', '--encoding=UTF8'], env, deadline, root, launch.signal);
256
+ }
257
+ launch.signal?.throwIfAborted();
258
+ const binary = join(binaries, 'postgres');
259
+ const log = join(root, 'native-postgres.log');
260
+ // Reserve durable startup evidence before spawning. A ready-receipt publication
261
+ // failure still leaves this receipt and the live handle for rollback.
262
+ const current = { pid: 0, data, port: service.hostPort, binary };
263
+ publishProcessReceipt(startingPath(root), JSON.stringify(current) + '\n');
264
+ let started;
265
+ try {
266
+ const fd = openSync(log, 'a');
267
+ let child;
268
+ try {
269
+ child = spawn(binary, ['-D', data, '-h', '127.0.0.1', '-p', String(service.hostPort), '-k', ''], { env, detached: true, stdio: ['ignore', fd, fd] });
270
+ current.pid = child.pid ?? 0;
271
+ started = { child, owned: current, closed: false };
272
+ const observed = started;
273
+ child.on('error', error => { observed.error = error; });
274
+ child.once('close', () => { observed.closed = true; });
275
+ starts.set(root, started);
276
+ started.birth = childIdentity(child);
277
+ }
278
+ finally {
279
+ closeSync(fd);
280
+ }
281
+ // The immutable spawn identity precedes either mutable ownership receipt.
282
+ started.identity = await started.birth;
283
+ if (child.pid && !started.identity?.start)
284
+ throw new Error('native postgres birth capture failed; retiring held child');
285
+ if (child.pid)
286
+ publishProcessReceipt(spawnReceiptPath(recordPath(root)), JSON.stringify(started.identity) + '\n');
287
+ publishProcessReceipt(startingPath(root), JSON.stringify(current) + '\n');
288
+ if (started.error)
289
+ throw started.error;
290
+ if (!child.pid)
291
+ throw new Error('native postgres spawn failed');
292
+ publishProcessReceipt(recordPath(root), JSON.stringify(current) + '\n');
293
+ for (;;) {
294
+ launch.signal?.throwIfAborted();
295
+ if (started.error)
296
+ throw started.error;
297
+ if (child.exitCode !== null || child.signalCode !== null || started.closed)
298
+ throw new Error(`native postgres exited before readiness; see ${log}`);
299
+ if (Date.now() >= deadline)
300
+ throw new Error(`native postgres did not become ready; see ${log}`);
301
+ if (existsSync(join(data, 'postmaster.pid'))) {
302
+ verifyOwner(current);
303
+ const lines = readFileSync(join(data, 'postmaster.pid'), 'utf8').split('\n');
304
+ if (lines[7]?.trim() === 'ready' && await listening(service.hostPort, deadline, launch.signal))
305
+ break;
306
+ }
307
+ await wait(100);
308
+ }
309
+ if (database !== 'postgres') {
310
+ const sql = `SELECT 1 FROM pg_database WHERE datname = '${database.replaceAll("'", "''")}'`;
311
+ 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);
312
+ if (!output.trim())
313
+ await tool(join(binaries, 'createdb'), ['-h', '127.0.0.1', '-p', String(service.hostPort), '-U', user, '--', database], env, deadline, root, launch.signal);
314
+ }
315
+ launch.signal?.throwIfAborted();
316
+ if (child.exitCode !== null || child.signalCode !== null)
317
+ throw new Error(`native postgres exited during readiness; see ${log}`);
318
+ rmSync(startingPath(root));
319
+ child.unref();
320
+ }
321
+ catch (error) {
322
+ // The original publication/readiness failure survives every cleanup failure.
323
+ const failures = [error];
324
+ try {
325
+ if (started)
326
+ await retireStarted(started);
327
+ removeReceipts(root);
328
+ starts.delete(root);
329
+ }
330
+ catch (cleanup) {
331
+ failures.push(cleanup);
332
+ }
333
+ if (failures.length > 1)
334
+ throw new AggregateError(failures, `native postgres startup and retirement failed; receipts retained`);
335
+ throw error;
336
+ }
337
+ }
338
+ export async function nativePostgresStatus(root) {
339
+ const state = ownership(root);
340
+ if (state.errors.length)
341
+ throw new AggregateError(state.errors, 'native postgres ownership uncertain; retry down');
342
+ if (!state.held || !liveGroup(state.held.pid))
343
+ return false;
344
+ if (state.identity)
345
+ await verifySpawnIdentity(state.identity);
346
+ verifyOwner(state.held);
347
+ return listening(state.held.port, Date.now() + READY_TIMEOUT_MS);
348
+ }
349
+ async function retireTools(root) {
350
+ if (!existsSync(root))
351
+ return;
352
+ const receipts = new Set(readdirSync(root).filter(name => /^native-postgres-tool-.*\.json(?:\.spawn\.json)?$/.test(name))
353
+ .map(name => name.endsWith('.spawn.json') ? name.slice(0, -'.spawn.json'.length) : name));
354
+ for (const file of receipts) {
355
+ const receipt = join(root, file);
356
+ const identity = readSpawnReceipt(receipt);
357
+ const recorded = readProcessReceipt(receipt, text => {
358
+ const row = JSON.parse(text);
359
+ if (!Number.isInteger(row.pid) || row.pid <= 0)
360
+ throw new Error('Invalid helper PID');
361
+ return row;
362
+ });
363
+ const pid = identity.value?.pid ?? recorded.value?.pid;
364
+ if (!pid)
365
+ throw new AggregateError([identity.error, recorded.error].filter(Boolean), `native postgres helper ownership uncertain; retained ${receipt}`);
366
+ if (liveGroup(pid)) {
367
+ if (!identity.value)
368
+ throw new Error(`native postgres helper ${pid} has no verified spawn identity; ownership retained`);
369
+ await verifySpawnIdentity(identity.value);
370
+ await retireIdentity(identity.value);
371
+ }
372
+ rmSync(receipt, { force: true });
373
+ rmSync(spawnReceiptPath(receipt), { force: true });
374
+ }
375
+ }
376
+ export async function nativePostgresDown(root) {
377
+ const started = starts.get(root);
378
+ const state = ownership(root);
379
+ if (state.errors.length)
380
+ process.stderr.write(`${errorMessage(new AggregateError(state.errors, 'native postgres ownership uncertain; retrying retirement through established identity'))}\n`);
381
+ if (started) {
382
+ await retireStarted(started);
383
+ }
384
+ else if (state.identity) {
385
+ if (liveGroup(state.identity.pid)) {
386
+ await verifySpawnIdentity(state.identity);
387
+ await retireIdentity(state.identity);
388
+ }
389
+ }
390
+ else if (state.held && state.held.pid > 0) {
391
+ if (liveGroup(state.held.pid)) {
392
+ throw new Error('native postgres spawn identity missing; retirement refused; receipts retained');
393
+ }
394
+ }
395
+ else if (state.present) {
396
+ throw new AggregateError(state.errors, 'native postgres ownership uncertain; no established process identity; receipts retained');
397
+ }
398
+ if (state.values.some(row => row.pid > 0 && liveGroup(row.pid))) {
399
+ throw new AggregateError(state.errors, 'native postgres receipt still names a live or uncertain group; ownership retained');
400
+ }
401
+ removeReceipts(root);
402
+ starts.delete(root);
403
+ await retireTools(root);
404
+ }
@@ -1,3 +1,4 @@
1
+ import { errorMessage } from "./error-message.js";
1
2
  import { captureHistory, historyDigest, publishOriginHistory, 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
@@ -146,7 +147,7 @@ export async function fetchFromOrigin(name, options) {
146
147
  writeWorldConfig(configPath, { ...config, remotes: { ...(config.remotes ?? {}), origin: named } });
147
148
  }
148
149
  catch (error) {
149
- console.error(`[origin] ${name}: the remote was not named in its config: ${error.message}`);
150
+ console.error(`[origin] ${name}: the remote was not named in its config: ${errorMessage(error)}`);
150
151
  }
151
152
  }
152
153
  return { world: name, origin, full: options.full ?? false, appended, behind, snapshots };
@@ -0,0 +1,4 @@
1
+ /** Check against a previously resolved owner, so re-reading a changed base cannot move the containment boundary. */
2
+ export declare function assertOwnedRealPath(realOwner: string, path: string): void;
3
+ /** A name-derived destination stays within its owner; inspect existing components before any write. */
4
+ export declare function ownedPath(base: string, name: string): string;
@@ -0,0 +1,37 @@
1
+ import { lstatSync, realpathSync } from 'node:fs';
2
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
3
+ function realBase(path) {
4
+ // Init may plan a base that has not been created. Resolve its nearest existing ancestor, preserving the suffix.
5
+ if (lstatSync(path, { throwIfNoEntry: false }))
6
+ return realpathSync(path);
7
+ return resolve(realBase(dirname(path)), basename(path));
8
+ }
9
+ /** Check against a previously resolved owner, so re-reading a changed base cannot move the containment boundary. */
10
+ export function assertOwnedRealPath(realOwner, path) {
11
+ const rel = relative(realOwner, realpathSync(path));
12
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
13
+ throw new Error('Invalid World-owned path');
14
+ }
15
+ /** A name-derived destination stays within its owner; inspect existing components before any write. */
16
+ export function ownedPath(base, name) {
17
+ const lexicalRoot = resolve(base), rel = relative(lexicalRoot, resolve(lexicalRoot, name));
18
+ const invalid = () => { throw new Error('Invalid World-owned path'); };
19
+ if (isAbsolute(name) || !rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
20
+ 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 = [];
23
+ let component = root;
24
+ for (const part of rel.split(sep)) {
25
+ component = resolve(component, part);
26
+ components.push(component);
27
+ }
28
+ for (const [index, component] of components.entries()) {
29
+ const stat = lstatSync(component, { throwIfNoEntry: false });
30
+ if (stat && (stat.isSymbolicLink() || (index < components.length - 1 && !stat.isDirectory())))
31
+ invalid();
32
+ if (stat)
33
+ assertOwnedRealPath(root, component);
34
+ }
35
+ // Preserve the operator's spelling: init plans later compare destinations relative to that same configured base.
36
+ return resolve(lexicalRoot, rel);
37
+ }
@@ -19,6 +19,7 @@ export type PackFacts = {
19
19
  serverBacking?: {
20
20
  command: string;
21
21
  };
22
+ maxRequestBodySize?: number;
22
23
  /** Existing descriptor transport, compiled into the same artifact as endpoint/adoption facts. */
23
24
  transport?: string;
24
25
  adoption?: PackAdoption;
@@ -1,24 +1,15 @@
1
1
  export interface InfraService {
2
2
  kind: string;
3
3
  hostPort: number;
4
+ replicaSet?: string;
4
5
  }
5
- /** Parse the services out of world.infrastructure.yml. This is NOT a YAML
6
- * parser: init's renderComposeFile emits a deterministic shape, and this
7
- * reads exactly that shape back (service key at 2-space indent = the kind;
8
- * the single loopback port mapping beneath it). Anything else is a malformed
9
- * definition and fails loudly. */
6
+ /** Read the generated definition's loopback port mappings. MongoDB may explicitly request
7
+ * one replica-set member through its Compose-compatible command; other mongod options are refused. */
10
8
  export declare function parseInfraDefinition(text: string): InfraService[];
11
9
  /** Endpoints of the declared infrastructure, published through external.discover. Only URLs
12
10
  * whose protocol and loopback port match a declared service belong to this lifecycle. */
13
11
  export declare function infraConnections(services: InfraService[], env: Record<string, string | undefined>): Record<string, string>;
14
- /** The MongoDB twin's cli: the package installed above the World's config (an app repo), else the one
15
- * this runtime was installed with (its dependency, or the checkout's workspace link). */
16
- export declare function mongodbTwinCli(worldConfig?: string, froms?: string[]): string;
17
- /** The runtime a twin's cli runs under. A published package's cli is JavaScript and runs under this
18
- * process's own runtime; a checkout's is TypeScript whose kernel needs more than Node's type
19
- * stripping, so it runs under Bun (this process's, or the one on PATH) — the repository's toolchain —
20
- * and without Bun it is refused by name rather than started to fail. */
21
- export declare function twinRunner(cli: string, bunOnPath?: () => boolean, underBun?: boolean): string;
12
+ export declare function containerlessDataDirectory(kind: string): string | undefined;
22
13
  /** How a backing starts its hosts: the environment they run in (the World's service environment, which
23
14
  * the runtime computes for the infrastructure service: admitted host env, the World's env, the service's own, less
24
15
  * the control plane's NODE_OPTIONS and proxies), and the boot's abort. The `volter-world-infra` process passes