@volter/world-runtime 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 (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
@@ -19,11 +19,11 @@
19
19
  },
20
20
  "@daily-co/daily-js": {
21
21
  "disposition": "demanded",
22
- "reason": "vendor daily (daily.co), its call client (the call machine from c.daily.co, signalling on gs.daily.co, WebRTC media): the daily pack serves Daily's REST API and a room's page, not the call, so the call client is a coverage gap (§9 of twin-daily, 2026-09-28)"
22
+ "reason": "vendor daily (daily.co), its call client (the call machine from c.daily.co, signalling on gs.daily.co, WebRTC media): the Daily REST spec does not describe call-client media or signalling, so this SDK remains an uncovered wire"
23
23
  },
24
24
  "@daily-co/daily-react": {
25
25
  "disposition": "demanded",
26
- "reason": "vendor daily (daily.co), React hooks over its call client (@daily-co/daily-js): the daily pack serves Daily's REST API and a room's page, not the call, so the call client is a coverage gap (§9 of twin-daily, 2026-09-28)"
26
+ "reason": "vendor daily (daily.co), React hooks over its call client (@daily-co/daily-js): the Daily REST spec does not describe call-client media or signalling, so this SDK remains an uncovered wire"
27
27
  },
28
28
  "@azure/communication-email": {
29
29
  "disposition": "demanded",
@@ -193,10 +193,6 @@
193
193
  "disposition": "demanded",
194
194
  "reason": "vendor matrix, pack demanded by census 2026-08-31 (×1)"
195
195
  },
196
- "node-telegram-bot-api": {
197
- "disposition": "demanded",
198
- "reason": "vendor telegram, pack demanded by census 2026-08-31 (×2); already curated in covers.ts UNKNOWN_DEP_KNOWN_SERVICES"
199
- },
200
196
  "npm-registry@npm.jsr.io": {
201
197
  "disposition": "acknowledged",
202
198
  "reason": "acknowledged: registry destination — the npm-registry twin deliberately owns npmjs + GitHub only (census 2026-08-31, ×2)"
@@ -217,10 +213,6 @@
217
213
  "disposition": "demanded",
218
214
  "reason": "vendor shopify, well-known service client with no twin pack (curated in covers.ts UNKNOWN_DEP_KNOWN_SERVICES)"
219
215
  },
220
- "telegraf": {
221
- "disposition": "demanded",
222
- "reason": "vendor telegram, pack demanded by census 2026-08-31 (×1); already curated in covers.ts UNKNOWN_DEP_KNOWN_SERVICES"
223
- },
224
216
  "twenty-client-sdk": {
225
217
  "disposition": "demanded",
226
218
  "reason": "vendor twenty, pack demanded by census 2026-08-31 (×1)"
@@ -925,6 +917,7 @@
925
917
  }
926
918
  },
927
919
  "notExternal": {
920
+ "@chat-adapter/state-memory": "Chat SDK local in-memory state adapter, no external connection: @chat-adapter/state-memory 4.39.0 dist/index.js stores subscriptions in Set and locks, cache and queues in Map; https://chat-sdk.dev/adapters/official/memory",
928
921
  "@agentclientprotocol/sdk": "Agent Client Protocol library, not a vendor",
929
922
  "@ai-sdk/devtools": "local dev-tools UI for the AI SDK",
930
923
  "@ai-sdk/langchain": "LangChain interop adapter, not a service",
@@ -7,9 +7,9 @@ import { hasFlag, optionValue } from '@volter/world-core/args';
7
7
  import { mkdirSync } from 'node:fs';
8
8
  import { runQueued } from "./actions-runner.js";
9
9
  const args = process.argv.slice(2);
10
- const github = optionValue(args, '--github') ?? process.env.GITHUB_API_URL;
11
- const token = optionValue(args, '--token') ?? process.env.GITHUB_TOKEN;
12
- const work = optionValue(args, '--work') ?? '.volter/actions-runner';
10
+ const github = optionValue(args, '--github') || process.env.GITHUB_API_URL;
11
+ const token = optionValue(args, '--token') || process.env.GITHUB_TOKEN;
12
+ const work = optionValue(args, '--work', '.volter/actions-runner');
13
13
  if (!github || !token) {
14
14
  process.stderr.write('usage: volter-world-actions-runner --github <url> --token <token> [--work <dir>] [--once]\n');
15
15
  process.exit(2);
@@ -7,6 +7,7 @@
7
7
  //
8
8
  // What it does not do (named, so a workflow that needs them fails plainly): matrices beyond their first combination,
9
9
  // services and containers, composite and JavaScript actions from the marketplace, reusable workflows, concurrency groups.
10
+ import { errorMessage } from "./error-message.js";
10
11
  import { spawn } from 'node:child_process';
11
12
  import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
13
  import { join } from 'node:path';
@@ -256,7 +257,7 @@ export async function runOnce(opts, repo, run) {
256
257
  }
257
258
  }
258
259
  catch (error) {
259
- log(`run ${repo}#${id}: ${error.message}`);
260
+ log(`run ${repo}#${id}: ${errorMessage(error)}`);
260
261
  conclusion = 'failure';
261
262
  }
262
263
  finally {
@@ -19,6 +19,7 @@
19
19
  // once, at the caller's request, and stores what it saw. The record lives in the instance dir, so
20
20
  // a re-`up` (which wipes the dir) or `down --purge` clears it: a recorded URL never outlives the
21
21
  // instance it described.
22
+ import { errorMessage } from "./error-message.js";
22
23
  import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
23
24
  import { withFileLock } from '@volter/world-core/runtime';
24
25
  import { join } from 'node:path';
@@ -208,7 +209,7 @@ export function addAppHosts(name, hosts, options = {}) {
208
209
  function listeningTcpPorts(pid) {
209
210
  const result = spawnSync('lsof', ['-a', '-p', String(pid), '-iTCP', '-sTCP:LISTEN', '-P', '-n', '-Fn'], { windowsHide: true, encoding: 'utf8' });
210
211
  if (result.error) {
211
- throw new Error(`volter-world app-url: \`lsof\` is unavailable (${result.error.message}) — pass the port, or --set the URL directly`);
212
+ throw new Error(`volter-world app-url: \`lsof\` is unavailable (${errorMessage(result.error)}) — pass the port, or --set the URL directly`);
212
213
  }
213
214
  const ports = new Set();
214
215
  for (const line of (result.stdout ?? '').split('\n')) {
@@ -3,6 +3,7 @@ export type ServerBackingFacts = {
3
3
  serverBacking: {
4
4
  command: string;
5
5
  };
6
+ maxRequestBodySize?: number;
6
7
  adoption: {
7
8
  sdks: string[];
8
9
  pypi: string[];
@@ -16,8 +17,11 @@ export type ServerBackingFacts = {
16
17
  };
17
18
  };
18
19
  /**
20
+ * MongoDB is declared infrastructure, not an API service in this table: its native server uses
21
+ * mongodb-backing.ts through the same managed lifecycle as Postgres (pglite-backing.ts).
22
+ *
19
23
  * A vendor whose open-source server a World runs as its service instead of a twin (architecture, "A vendor's own
20
- * server as a backing"): the media plane no pack can hold as data. The service is the vendor's own binary on an 'auto'
24
+ * server as a backing"): the wire or media plane no pack can hold as data. The service is the vendor's own binary on an 'auto'
21
25
  * port, loopback only; the application's key and secret are the ones the server is started with, so its SDK signs
22
26
  * what the server verifies. What the server does not do on its own (LiveKit's egress, a separate service) the
23
27
  * service's note names.
@@ -27,3 +31,8 @@ export declare const VENDOR_SERVER_BACKINGS: Record<string, {
27
31
  env: Record<string, string>;
28
32
  facts: ServerBackingFacts;
29
33
  }>;
34
+ /** Declare a backing's application-owned env stems as service bindings; omitted stems use the vendor's defaults. */
35
+ export declare function serverBackingService(vendor: string, declaration?: {
36
+ envStems?: readonly string[];
37
+ service?: WorldServiceConfig;
38
+ }): WorldServiceConfig;
@@ -1,6 +1,10 @@
1
+ import { fileURLToPath } from 'node:url';
1
2
  /**
3
+ * MongoDB is declared infrastructure, not an API service in this table: its native server uses
4
+ * mongodb-backing.ts through the same managed lifecycle as Postgres (pglite-backing.ts).
5
+ *
2
6
  * A vendor whose open-source server a World runs as its service instead of a twin (architecture, "A vendor's own
3
- * server as a backing"): the media plane no pack can hold as data. The service is the vendor's own binary on an 'auto'
7
+ * server as a backing"): the wire or media plane no pack can hold as data. The service is the vendor's own binary on an 'auto'
4
8
  * port, loopback only; the application's key and secret are the ones the server is started with, so its SDK signs
5
9
  * what the server verifies. What the server does not do on its own (LiveKit's egress, a separate service) the
6
10
  * service's note names.
@@ -10,6 +14,31 @@ export const VENDOR_SERVER_BACKINGS = (() => {
10
14
  // livekit-server wants a secret of at least 32 characters
11
15
  const secret = 'twin-livekit-secret-for-the-worlds-own-server';
12
16
  return {
17
+ temporal: {
18
+ service: {
19
+ '//': 'Temporal CLI development server on PATH, loopback only, SQLite in World state. No cluster, Cloud, TLS, World clock or branching; reset clears workflow history. See cookbook/temporal-backing.',
20
+ id: 'temporal', type: 'process', command: 'node',
21
+ args: [fileURLToPath(new URL('./temporal-backing.mjs', import.meta.url))],
22
+ port: 'auto', portArg: false, controlPlane: true, branchState: 'unsupported',
23
+ // Existing service readiness budgets, no new timeout. Native dev-server checkout took 0.84 s
24
+ // on 6e0123e + backing error handling changes with Node 23.9.0; SDK consumer only in this World, host not isolated.
25
+ // Reproduce: cookbook/temporal-backing/README.md, timed checkout and reset commands.
26
+ ready: { command: 'node', args: [fileURLToPath(new URL('./temporal-backing.mjs', import.meta.url)), '--health'] },
27
+ injectEnvTemplates: {
28
+ TEMPORAL_ADDRESS: '${host}:${port}', TEMPORAL_NAMESPACE: 'default', TEMPORAL_TLS: 'false', TEMPORAL_API_KEY: '',
29
+ },
30
+ },
31
+ env: { TEMPORAL_NAMESPACE: 'default', TEMPORAL_TLS: 'false', TEMPORAL_API_KEY: '' },
32
+ facts: {
33
+ serverBacking: { command: 'temporal' },
34
+ adoption: { sdks: ['@temporalio/client', '@temporalio/worker', '@temporalio/workflow', 'nestjs-temporal-core'],
35
+ pypi: ['temporalio'], envStems: ['TEMPORAL'] },
36
+ hostsNone: 'The World runs the vendor development server at its declared loopback gRPC endpoint.',
37
+ endpointEnv: { name: 'TEMPORAL_ADDRESS', templates: {
38
+ TEMPORAL_ADDRESS: '${host}:${port}', TEMPORAL_NAMESPACE: 'default', TEMPORAL_TLS: 'false', TEMPORAL_API_KEY: '',
39
+ }, note: 'The local development server creates default; no TLS or Cloud API key.' },
40
+ },
41
+ },
13
42
  livekit: {
14
43
  service: {
15
44
  '//': 'LiveKit\'s own server (livekit-server, Apache-2.0; `brew install livekit` or LiveKit\'s install script puts it '
@@ -18,11 +47,11 @@ export const VENDOR_SERVER_BACKINGS = (() => {
18
47
  + `"webhook: {api_key: ${key}, urls: [<the app's webhook URL>]}" to args.`,
19
48
  id: 'livekit',
20
49
  type: 'twin',
21
- command: 'livekit-server',
22
- // media on the loopback address over UDP from LiveKit's default range; ICE over TCP off (its fixed 7881 would
23
- // collide with a second server)
24
- args: ['--bind', '127.0.0.1', '--node-ip', '127.0.0.1', '--keys', `${key}: ${secret}`, '--rtc.tcp_port', '0'],
50
+ command: 'node',
51
+ // The wrapper supplies a local TURN relay using UDP at the allocated HTTP port.
52
+ args: [fileURLToPath(new URL('./livekit-backing.mjs', import.meta.url)), '--keys', `${key}: ${secret}`],
25
53
  port: 'auto',
54
+ portArg: false,
26
55
  rootArg: false,
27
56
  controlPlane: true,
28
57
  ready: { httpUrl: '${url}/' },
@@ -34,12 +63,42 @@ export const VENDOR_SERVER_BACKINGS = (() => {
34
63
  },
35
64
  env: { LIVEKIT_API_KEY: key, LIVEKIT_API_SECRET: secret },
36
65
  facts: {
37
- // Current consumers: RH2's package.json and server SDK imports; LiveKit's server is the backing, not a pack.
66
+ // LiveKit's client and server SDKs use its own server as the backing.
38
67
  serverBacking: { command: 'livekit-server' },
39
- adoption: { sdks: ['livekit-client', 'livekit-server-sdk', '@livekit/rtc-node'], pypi: ['livekit', 'livekit-agents', 'livekit-api'], envStems: ['LIVEKIT'] },
68
+ adoption: { sdks: ['livekit-client', 'livekit-server-sdk', '@livekit/rtc-node', '@livekit/react-native',
69
+ '@livekit/react-native-expo-plugin', '@livekit/react-native-webrtc'],
70
+ pypi: ['livekit', 'livekit-agents', 'livekit-api'], envStems: ['LIVEKIT'] },
40
71
  hostsNone: 'The World runs the vendor server on its declared endpoint; SDKs read LIVEKIT_URL.',
41
72
  endpointEnv: { name: 'LIVEKIT_URL', templates: { LIVEKIT_URL: 'ws://${host}:${port}', LIVEKIT_API_KEY: key, LIVEKIT_API_SECRET: secret }, note: 'Credentials and URL are the same ones the World starts its LiveKit server with.' },
42
73
  },
43
74
  },
44
75
  };
45
76
  })();
77
+ /** Declare a backing's application-owned env stems as service bindings; omitted stems use the vendor's defaults. */
78
+ export function serverBackingService(vendor, declaration = {}) {
79
+ const backing = VENDOR_SERVER_BACKINGS[vendor];
80
+ // Native infrastructure supplies its already-declared lifecycle rather than an API service from the table.
81
+ const service = declaration.service ?? backing?.service;
82
+ if (!service)
83
+ throw new Error(`No server backing declared for ${vendor}`);
84
+ const base = backing?.facts.adoption.envStems[0] ?? vendor.toUpperCase();
85
+ const stems = declaration.envStems ?? backing?.facts.adoption.envStems ?? [base];
86
+ if (!stems.length || stems.some((stem) => !/^[A-Z][A-Z0-9_]*$/.test(stem)))
87
+ throw new Error('Server backing env stems must be nonempty uppercase env names');
88
+ const unique = [...new Set(stems)];
89
+ const bindingName = (name, stem) => {
90
+ if (!name.startsWith(`${base}_`))
91
+ throw new Error(`Server backing ${vendor} binding ${name} has no vendor stem`);
92
+ return `${stem}${name.slice(base.length)}`;
93
+ };
94
+ const injectEnvTemplates = Object.fromEntries(unique.flatMap((stem) => Object.entries(service.injectEnvTemplates ?? {}).map(([name, value]) => [bindingName(name, stem), value])));
95
+ const external = service.external === undefined ? undefined : {
96
+ ...service.external,
97
+ ...(service.external.discover === undefined ? {} : { discover: unique.flatMap((stem) => service.external.discover.map((mapping) => {
98
+ const as = bindingName(mapping.as, stem);
99
+ // The producer keeps its own keys; only the application binding gets the declared stem.
100
+ return { ...mapping, as };
101
+ })) }),
102
+ };
103
+ return { ...service, ...(external ? { external } : { injectEnvTemplates }) };
104
+ }
@@ -3,16 +3,18 @@
3
3
  // `branch <world> <name>` boots a new world from the base's config and forks every twin's mirror
4
4
  // into it (core's `forkTwin`: a POINTER to the base at its current position — the base's whole
5
5
  // history, its own entries included, is the branch's parent; nothing is copied). A managed database (containerless
6
- // Postgres) keeps no history, so it is the one thing copied: the stopped base's data, as of now. Two branches of one
6
+ // Postgres and MongoDB) keeps no history, so it is the one thing copied: the stopped base's data, as of now. Two branches of one
7
7
  // base running at once share the database port its definition names; run one at a time (the SDK does). `checkout <name>`
8
8
  // brings a stopped world back up from its own instance record, state intact.
9
+ import { runInfrastructure } from "./infra.js";
9
10
  import { cpSync, existsSync, readdirSync, readFileSync, rmSync } from 'node:fs';
10
11
  import { dirname, join, resolve } from 'node:path';
11
12
  import { fileURLToPath } from 'node:url';
12
13
  import { captureHistory, historyAtInstant, forkTwin, stateGeneration, withHistoryLocks, worldPaths, worldStateRoot } from '@volter/world-core/runtime';
13
14
  import { inheritWorldBrowsers, materializeBrowserFiles } from "./browsers.js";
14
- import { downWorld, saveWorldInstance, statusWorld, upWorld } from "./runtime.js";
15
- import { parseInfraDefinition, pgliteDown, pgliteUp } from "./pglite-backing.js";
15
+ import { externalServiceEnv, downWorld, recordedWorldNetwork, saveWorldInstance, readWorldBootRecord, upWorld } from "./runtime.js";
16
+ import { loadWorldConfig } from "./configs.js";
17
+ import { containerlessDataDirectory, parseInfraDefinition, pgliteDown, pgliteUp } from "./pglite-backing.js";
16
18
  import { redisTwinCli, redisTwinDown, redisTwinUp } from "./redis-backing.js";
17
19
  function stateServices(controlRoot) {
18
20
  const stateRoot = worldStateRoot(controlRoot);
@@ -23,7 +25,12 @@ function stateServices(controlRoot) {
23
25
  /** A new world over the base's mirrors. */
24
26
  export async function branchWorld(base, name, options = {}) {
25
27
  const root = resolve(options.root ?? process.cwd());
26
- const from = statusWorld(base, root);
28
+ const from = readWorldBootRecord(base, root);
29
+ const { config } = loadWorldConfig(existsSync(from.configPath) ? from.configPath : from.config, root);
30
+ const unsupported = [...new Set([...config.services, ...Object.values(from.services)]
31
+ .filter((service) => service.branchState === 'unsupported').map((service) => service.id))];
32
+ if (unsupported.length)
33
+ throw new Error(`volter-world branch: services do not support carrying state to a branch: ${unsupported.join(', ')}; retain the original World or reset it explicitly`);
27
34
  if (name === base)
28
35
  throw new Error(`volter-world branch: the branch needs a name other than the base's ("${base}")`);
29
36
  const envFile = options.envFile ?? join(dirname(from.envFile), `${name}.world.env`);
@@ -48,42 +55,9 @@ export async function branchWorld(base, name, options = {}) {
48
55
  throw new Error(`volter-world branch: ${base} has a managed database, which keeps no history to branch from at an instant or position; branch from its current state`);
49
56
  }
50
57
  refuseLiveDatabases(from, databases);
51
- // a branch that fails to boot (a twin whose mounted version breaks the config's pin) leaves no half-made branch behind:
52
- // its record without a fork would check out later as an empty World
53
- const existed = (() => { try {
54
- statusWorld(name, root);
55
- return true;
56
- }
57
- catch {
58
- return false;
59
- } })();
60
- const began = Date.now();
61
- let instance;
62
- try {
63
- instance = await upWorld(existsSync(from.configPath) ? from.configPath : from.config, { name, root, mode: from.mode, envFile, forceEnvOutput: options.forceEnvOutput });
64
- }
65
- catch (error) {
66
- // only a record made during this call is ours to remove, and it is removed by its own createdAt, as the fork's
67
- // cleanup below does: a concurrent `branch` of the same name that recorded first keeps its World
68
- let recorded = null;
69
- if (!existed) {
70
- try {
71
- const made = statusWorld(name, root).createdAt;
72
- if (Date.parse(made) >= began)
73
- recorded = made;
74
- }
75
- catch { /* the boot recorded nothing */ }
76
- }
77
- if (recorded) {
78
- try {
79
- await downWorld(name, root, { purge: true, expectedCreatedAt: recorded });
80
- }
81
- catch (cleanup) {
82
- throw new Error(`${error instanceof Error ? error.message : String(error)} (and the half-made branch ${name} could not be removed: ${cleanup instanceof Error ? cleanup.message : String(cleanup)})`);
83
- }
84
- }
85
- throw error;
86
- }
58
+ // up owns rollback. If boot throws, retain its diagnostics: a name/time lookup cannot prove
59
+ // that a partial record belongs to this invocation, especially under a frozen World clock.
60
+ const instance = await upWorld(existsSync(from.configPath) ? from.configPath : from.config, { name, root, mode: from.mode, owner: from.lifecycle?.owner ?? from.resources?.owner, networkCeiling: recordedWorldNetwork(from), envFile, forceEnvOutput: options.forceEnvOutput });
87
61
  const forked = {};
88
62
  try {
89
63
  // the branch's origin is its base's, or none: never one a stopped World of the same name left
@@ -105,23 +79,34 @@ export async function branchWorld(base, name, options = {}) {
105
79
  await branchManagedDatabases(from, instance, databases);
106
80
  }
107
81
  catch (error) {
108
- await downWorld(name, root, { purge: true, expectedCreatedAt: instance.createdAt });
82
+ try {
83
+ await downWorld(name, root, { purge: true, expectedInstance: instance.instance });
84
+ }
85
+ catch (cleanup) {
86
+ throw new AggregateError([error, cleanup], 'Copied database startup and branch cleanup failed');
87
+ }
109
88
  throw error;
110
89
  }
111
90
  return { instance, forked };
112
91
  }
113
- /** The base's containerless managed databases: each PGlite `pglite-<kind>-data` and the MongoDB twin's
114
- * `twin-mongodb-data` (pglite-backing.ts), and the redis twin's `redis-twin` tree (redis-backing.ts). A stopped World's
92
+ /** The base's containerless managed databases: each PGlite `pglite-<kind>-data`, MongoDB's
93
+ * `mongod-mongodb-data` (pglite-backing.ts), the redis twin's `redis-twin` tree (redis-backing.ts), and native PostgreSQL. A stopped World's
115
94
  * hosts leave no pid file; a docker-backed World keeps its data under the container's volume, which a branch does not
116
95
  * carry. */
117
96
  function managedDatabases(from) {
118
97
  const data = from.dirs.data;
119
98
  if (!existsSync(data))
120
99
  return [];
121
- return readdirSync(data).filter((n) => /^pglite-[a-z0-9_-]+-data$/.test(n) || n === 'twin-mongodb-data' || n === 'redis-twin');
100
+ const known = (name) => /^pglite-[a-z0-9_-]+-data$/.test(name) || name === 'mongod-mongodb-data' || name === 'native-postgres-data';
101
+ if (readdirSync(data).some(name => name.endsWith('-data') && !known(name) && !from.services[name])) {
102
+ throw new Error('volter-world branch: unrecognised storage; retain the original World');
103
+ }
104
+ return readdirSync(data).filter((n) => /^pglite-[a-z0-9_-]+-data$/.test(n) || n === 'mongod-mongodb-data' || n === 'redis-twin' || n === 'native-postgres-data');
122
105
  }
123
106
  /** The pid files of the hosts serving one managed database directory. */
124
107
  function hostPidFiles(data, db) {
108
+ if (db === 'native-postgres-data')
109
+ return [join(data, 'native-postgres.owner.json')];
125
110
  if (db === 'redis-twin')
126
111
  return readdirSync(data).filter((n) => /^redis-twin-\d+\.pid$/.test(n)).map((n) => join(data, n));
127
112
  return [join(data, `${db.slice(0, -'-data'.length)}.pid`)];
@@ -131,7 +116,8 @@ function hostPidFiles(data, db) {
131
116
  function refuseLiveDatabases(from, dbs) {
132
117
  for (const db of dbs) {
133
118
  for (const pidFile of hostPidFiles(from.dirs.data, db)) {
134
- const pid = existsSync(pidFile) ? Number(readFileSync(pidFile, 'utf8').trim()) : NaN;
119
+ const recorded = existsSync(pidFile) ? readFileSync(pidFile, 'utf8').trim() : '';
120
+ const pid = db === 'native-postgres-data' && recorded ? JSON.parse(recorded).pid : Number(recorded);
135
121
  let live = false;
136
122
  if (Number.isInteger(pid) && pid > 0) {
137
123
  try {
@@ -147,18 +133,65 @@ function refuseLiveDatabases(from, dbs) {
147
133
  }
148
134
  }
149
135
  }
136
+ /** Compare actual cold-storage formats, not the edited config's requested backing. */
137
+ function requireMatchingBackings(source, child) {
138
+ const kinds = {
139
+ postgres: ['native-postgres-data', 'pglite-postgres-data'],
140
+ mongodb: ['mongod-mongodb-data'],
141
+ };
142
+ for (const [kind, formats] of Object.entries(kinds)) {
143
+ const from = source.filter(directory => formats.includes(directory));
144
+ if (!from.length)
145
+ continue;
146
+ const to = child.filter(directory => formats.includes(directory));
147
+ if (from.length !== 1 || to.length !== 1 || from[0] !== to[0]) {
148
+ throw new Error(`volter-world branch: ${kind} source backing ${from.join(', ')} differs from child backing ${to.join(', ') || 'none'}; refusing to copy database bytes`);
149
+ }
150
+ }
151
+ for (const directory of source) {
152
+ if (!child.includes(directory))
153
+ throw new Error(`volter-world branch: source backing ${directory} differs from child backing ${child.join(', ') || 'none'}; refusing to copy database bytes`);
154
+ }
155
+ }
150
156
  /** The branch's managed databases as the base holds them: the branch's own, just booted empty by `up`, are stopped,
151
157
  * replaced by a copy of the base's data, and started again, so a branch of a migrated and seeded World serves that
152
158
  * data, as its twins serve the base's history. */
153
159
  async function branchManagedDatabases(from, instance, dbs) {
154
160
  if (!dbs.length)
155
161
  return;
162
+ requireMatchingBackings(dbs, managedDatabases(instance));
156
163
  const definition = join(dirname(instance.configPath), 'world.infrastructure.yml');
157
164
  if (!existsSync(definition))
158
- return;
165
+ throw new Error('volter-world branch: source has managed database bytes but child has no infrastructure definition; refusing to copy');
159
166
  const services = parseInfraDefinition(readFileSync(definition, 'utf8'));
160
167
  const redis = services.filter((s) => s.kind === 'redis');
161
- const hosted = services.filter((s) => s.kind !== 'redis');
168
+ const hosted = services.filter((service) => {
169
+ const directory = containerlessDataDirectory(service.kind);
170
+ if (directory === undefined || !dbs.includes(directory))
171
+ return false;
172
+ if (!existsSync(join(instance.dirs.data, directory))) {
173
+ throw new Error(`volter-world branch: ${service.kind}'s copied state belongs to a different backing than the branch booted; refusing to replace its database`);
174
+ }
175
+ return true;
176
+ });
177
+ const config = loadWorldConfig(instance.configPath, instance.root).config;
178
+ const infrastructure = config.services.find((service) => service.type === 'external' && (service.external?.up[0] === 'volter-world-infra' || service.id === 'infrastructure'));
179
+ if (!infrastructure)
180
+ throw new Error(`volter-world branch: ${instance.name}'s managed databases have no declared infrastructure service`);
181
+ const launch = { env: externalServiceEnv(infrastructure, instance.dirs, instance.env, config.stripEnv) };
182
+ if (dbs.includes('native-postgres-data')) {
183
+ const io = { out: (text) => process.stdout.write(text), err: (text) => process.stderr.write(text) };
184
+ if (await runInfrastructure('down', launch.env, io) !== 0)
185
+ throw new Error('managed native database branch could not stop its new backing');
186
+ for (const db of dbs) {
187
+ const to = join(instance.dirs.data, db);
188
+ rmSync(to, { recursive: true, force: true });
189
+ cpSync(join(from.dirs.data, db), to, { recursive: true });
190
+ }
191
+ if (await runInfrastructure('up', launch.env, io) !== 0)
192
+ throw new Error('managed native database branch could not resume its copied backing');
193
+ return;
194
+ }
162
195
  const redisCli = dbs.includes('redis-twin') && redis.length ? redisTwinCli([dirname(fileURLToPath(import.meta.url)), dirname(instance.configPath)]) : undefined;
163
196
  await pgliteDown(hosted, instance.dirs.data);
164
197
  if (redisCli)
@@ -170,14 +203,14 @@ async function branchManagedDatabases(from, instance, dbs) {
170
203
  rmSync(to, { recursive: true, force: true });
171
204
  cpSync(join(from.dirs.data, db), to, { recursive: true });
172
205
  }
173
- if (dbs.some((db) => db !== 'redis-twin'))
174
- await pgliteUp(hosted, instance.dirs.data);
206
+ if (hosted.length)
207
+ await pgliteUp(hosted, instance.dirs.data, launch);
175
208
  if (redisCli)
176
- await redisTwinUp(redis, instance.dirs.data, redisCli);
209
+ await redisTwinUp(redis, instance.dirs.data, redisCli, launch);
177
210
  }
178
211
  /** A stopped world back up, from its own record. */
179
212
  export async function checkoutWorld(name, options = {}) {
180
213
  const root = resolve(options.root ?? process.cwd());
181
- const was = statusWorld(name, root);
182
- return upWorld(existsSync(was.configPath) ? was.configPath : was.config, { name, root, mode: was.mode, envFile: was.envFile, keepState: true });
214
+ const was = readWorldBootRecord(name, root);
215
+ return upWorld(existsSync(was.configPath) ? was.configPath : was.config, { name, root, mode: was.mode, owner: was.lifecycle?.owner ?? was.resources?.owner, envFile: was.envFile, keepState: true });
183
216
  }
@@ -0,0 +1,59 @@
1
+ // Bind an existing browser page to a World-attached HTTP transport. A namespace gives the World its own page and cookie
2
+ // paths on that page's origin; vendor URLs remain the wire targets. Published external JavaScript is never rewritten.
3
+ exports.installWorldBrowserRoutes = async function (router, transport, { namespace, origin, onExchange, context = false } = {}) {
4
+ if (namespace && (!/^\/[A-Za-z0-9._~/-]+$/.test(namespace) || !origin)) throw new Error('A World page needs a path namespace and its browser origin');
5
+ const base = namespace?.replace(/\/$/, '');
6
+ const forward = async route => {
7
+ try {
8
+ const request = route.request();
9
+ if (context && namespace) {
10
+ let documentUrl;
11
+ try { documentUrl = request.frame().page().url(); } catch {}
12
+ const ownPath = `${origin}${base}/`;
13
+ if (!request.url().startsWith(ownPath) && !documentUrl?.startsWith(ownPath)) return route.fallback();
14
+ }
15
+ let url = new URL(request.url()), method = request.method(), response;
16
+ let prefix;
17
+ if (namespace && url.origin === origin && url.pathname.startsWith(base + '/')) {
18
+ const rest = url.pathname.slice(base.length + 1), at = rest.indexOf('/');
19
+ const host = at < 0 ? rest : rest.slice(0, at);
20
+ prefix = `${base}/${host}`;
21
+ url = new URL(`https://${host}${at < 0 ? '/' : rest.slice(at)}${url.search}`);
22
+ }
23
+ if (namespace && !prefix) prefix = `${base}/${url.host}`;
24
+ // Complete wire headers stay opaque: the abbreviated request header view omits authentication headers.
25
+ const headers = await request.allHeaders();
26
+ if (prefix) headers['x-forwarded-prefix'] = prefix;
27
+ const visited = new Set();
28
+ for (;;) {
29
+ if (visited.has(url.href)) throw new Error('World browser redirect loop');
30
+ visited.add(url.href);
31
+ response = await route.fetch({ url: transport + '/?url=' + encodeURIComponent(url.href), method, headers, maxRedirects: 0, timeout: 1000 });
32
+ const status = response.status(), location = response.headers().location;
33
+ onExchange?.({ method, path: url.pathname, status });
34
+ if (![301, 302, 303, 307, 308].includes(status) || !location) break;
35
+ const next = new URL(location, url);
36
+ // Chrome owns the application's HTTP loopback callback.
37
+ if (next.protocol !== 'https:') break;
38
+ if (next.origin !== url.origin) {
39
+ for (const name of Object.keys(headers)) if (/^(authorization|cookie|proxy-authorization|x-volter-twin-|x-forwarded-prefix)/i.test(name)) delete headers[name];
40
+ }
41
+ if (status === 303 || ([301, 302].includes(status) && method === 'POST')) method = 'GET';
42
+ url = next;
43
+ }
44
+ if (namespace && /text\/html/i.test(response.headers()['content-type'] ?? '')) {
45
+ // Input values are protocol data (OAuth redirect_uri, state and code). Keep them byte-for-byte. Links and inline
46
+ // scripts name browser destinations, mapped to this World's page; external script bytes remain as published.
47
+ const body = (await response.text()).split(/(<input\b[^>]*>)/gi).map(part => /^<input\b/i.test(part) ? part : part.replace(/https:\/\/([A-Za-z0-9.-]+(?::\d+)?)(?=[/\s"'&<]|$)/g, (_, host) => `${origin}${base}/${host}`)).join('');
48
+ const outHeaders = { ...response.headers() };
49
+ delete outHeaders['content-length'];
50
+ await route.fulfill({ response, headers: outHeaders, body });
51
+ } else await route.fulfill({ response });
52
+ } catch (error) {
53
+ onExchange?.({ error: error.message });
54
+ await route.abort('blockedbyclient').catch(() => {});
55
+ }
56
+ };
57
+ await router.route('https://**/*', forward);
58
+ return { message: 'World routing installed on auth tab', namespace: base ?? null, dispose: () => router.unroute('https://**/*', forward) };
59
+ };
@@ -1,3 +1,4 @@
1
+ import { errorMessage } from "./error-message.js";
1
2
  import { createHash, randomUUID, X509Certificate } from 'node:crypto';
2
3
  import { existsSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
3
4
  import { dirname, join } from 'node:path';
@@ -22,7 +23,7 @@ export function sessionTrustEnv(caCertPath, inputs = process.env) {
22
23
  return pem;
23
24
  }
24
25
  catch (error) {
25
- throw new CaTrustError(`Cannot read configured CA file ${path}: ${String(error)}`);
26
+ throw new CaTrustError(`Cannot read configured CA file ${path}: ${errorMessage(error)}`);
26
27
  }
27
28
  };
28
29
  const session = read(caCertPath);
@@ -47,9 +48,9 @@ export function sessionTrustEnv(caCertPath, inputs = process.env) {
47
48
  rmSync(temporary, { force: true });
48
49
  }
49
50
  catch (cleanupError) {
50
- throw new CaTrustError(`Cannot materialize session trust bundle: ${String(error)}; cleanup: ${String(cleanupError)}`);
51
+ throw new CaTrustError(`Cannot materialize session trust bundle: ${errorMessage(error)}; cleanup: ${errorMessage(cleanupError)}`);
51
52
  }
52
- throw new CaTrustError(`Cannot materialize session trust bundle: ${String(error)}`);
53
+ throw new CaTrustError(`Cannot materialize session trust bundle: ${errorMessage(error)}`);
53
54
  }
54
55
  return path;
55
56
  };
@@ -1,3 +1,4 @@
1
+ import { errorMessage } from "./error-message.js";
1
2
  import { worldChangesetRules } from "./configs.js";
2
3
  import { blobDigest, captureHistory, captureParentHistory, getActiveBlobStore, getActiveWorldStore, withHistoryLocks, worldPaths } from '@volter/world-core/runtime';
3
4
  // `volter-world mark` / `diff` / `changeset` — the WORLD-side verbs over the changeset
@@ -41,7 +42,7 @@ function requireWorld(name, root) {
41
42
  return statusWorld(name, root);
42
43
  }
43
44
  catch (error) {
44
- if (!/World instance not found/.test(String(error.message)))
45
+ if (!/World instance not found/.test(String(errorMessage(error))))
45
46
  throw error;
46
47
  const known = listWorlds(root).map((world) => world.name);
47
48
  throw new Error(`World "${name}" not found in ${root}${known.length ? ` (known worlds: ${known.join(', ')})` : ' (no worlds here)'} — start it with \`volter world up\` in the app's folder (the operator's own: \`volter-world up <config> --env-out <path> --name ${name}\`)`);
@@ -482,7 +483,7 @@ export async function pushWorldChangeset(name, options) {
482
483
  res = { ok: true, status: 200 };
483
484
  }
484
485
  catch (error) {
485
- body = { error: error instanceof Error ? error.message : String(error) };
486
+ body = { error: errorMessage(error) };
486
487
  res = { ok: false, status: 409 };
487
488
  }
488
489
  }
@@ -547,7 +548,7 @@ export async function pushWorldChangeset(name, options) {
547
548
  // the remote's record of who pushed it and who owns it, kept on this copy too (outside the hash)
548
549
  const changeset = { ...withApplication(located.changeset, application), ...(body.owner && !located.changeset.owner ? { owner: body.owner } : {}), ...(body.pushedBy ? { pushedBy: body.pushedBy } : {}) };
549
550
  writeChangesetFile(located.path, changeset);
550
- const unsent = namedFailure ? `records the landed entries name did not reach the origin (${namedFailure instanceof Error ? namedFailure.message : String(namedFailure)}); the next push from this branch that touches ${twins.map(([t]) => t).join(', ')} sends them` : undefined;
551
+ const unsent = namedFailure ? `records the landed entries name did not reach the origin (${errorMessage(namedFailure)}); the next push from this branch that touches ${twins.map(([t]) => t).join(', ')} sends them` : undefined;
551
552
  return { ...(unsent ? { unsent } : {}), application, changeset, world: located.world, path: located.path, remote: { url, namespace } };
552
553
  }
553
554
  /** `volter-world changeset rebase <name>`: the merge onto the authoring world's moved mirror. */