@volter/world-runtime 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/LICENSE +202 -0
  2. package/dist/known-external-services.json +1108 -0
  3. package/dist/src/ancestry.d.ts +2 -0
  4. package/dist/src/ancestry.js +42 -0
  5. package/dist/src/app-url.d.ts +47 -0
  6. package/dist/src/app-url.js +239 -0
  7. package/dist/src/attach.d.ts +48 -0
  8. package/dist/src/attach.js +87 -0
  9. package/dist/src/branch.d.ts +20 -0
  10. package/dist/src/branch.js +65 -0
  11. package/dist/src/browser-proxy-cli.d.ts +2 -0
  12. package/dist/src/browser-proxy-cli.js +41 -0
  13. package/dist/src/ca-trust.d.ts +5 -0
  14. package/dist/src/ca-trust.js +64 -0
  15. package/dist/src/catalog.d.ts +31 -0
  16. package/dist/src/catalog.js +148 -0
  17. package/dist/src/changeset.d.ts +142 -0
  18. package/dist/src/changeset.js +570 -0
  19. package/dist/src/cli.d.ts +2 -0
  20. package/dist/src/cli.js +1262 -0
  21. package/dist/src/command-lifetime.d.ts +15 -0
  22. package/dist/src/command-lifetime.js +98 -0
  23. package/dist/src/configs.d.ts +18 -0
  24. package/dist/src/configs.js +119 -0
  25. package/dist/src/console-apart.d.ts +38 -0
  26. package/dist/src/console-apart.js +107 -0
  27. package/dist/src/consumers.d.ts +46 -0
  28. package/dist/src/consumers.js +200 -0
  29. package/dist/src/covers.d.ts +183 -0
  30. package/dist/src/covers.js +800 -0
  31. package/dist/src/fixture-env.d.ts +42 -0
  32. package/dist/src/fixture-env.js +221 -0
  33. package/dist/src/host-cli.d.ts +2 -0
  34. package/dist/src/host-cli.js +92 -0
  35. package/dist/src/host-fault-fixture.d.ts +32 -0
  36. package/dist/src/host-fault-fixture.js +100 -0
  37. package/dist/src/host-worker.d.ts +1 -0
  38. package/dist/src/host-worker.js +23 -0
  39. package/dist/src/host.d.ts +38 -0
  40. package/dist/src/host.js +135 -0
  41. package/dist/src/index.d.ts +48 -0
  42. package/dist/src/index.js +35 -0
  43. package/dist/src/infra-cli.d.ts +2 -0
  44. package/dist/src/infra-cli.js +136 -0
  45. package/dist/src/init.d.ts +227 -0
  46. package/dist/src/init.js +1117 -0
  47. package/dist/src/inject-map.d.ts +34 -0
  48. package/dist/src/inject-map.js +56 -0
  49. package/dist/src/lifecycle-record.d.ts +47 -0
  50. package/dist/src/lifecycle-record.js +196 -0
  51. package/dist/src/origin.d.ts +31 -0
  52. package/dist/src/origin.js +139 -0
  53. package/dist/src/pack-facts.d.ts +75 -0
  54. package/dist/src/pack-facts.js +98 -0
  55. package/dist/src/pglite-backing.d.ts +21 -0
  56. package/dist/src/pglite-backing.js +158 -0
  57. package/dist/src/pglite-host.mjs +147 -0
  58. package/dist/src/placeholder.d.ts +20 -0
  59. package/dist/src/placeholder.js +100 -0
  60. package/dist/src/prerequisites.d.ts +21 -0
  61. package/dist/src/prerequisites.js +49 -0
  62. package/dist/src/process-groups.d.ts +4 -0
  63. package/dist/src/process-groups.js +49 -0
  64. package/dist/src/project-inspect.d.ts +109 -0
  65. package/dist/src/project-inspect.js +827 -0
  66. package/dist/src/proxy-daemon.d.ts +2 -0
  67. package/dist/src/proxy-daemon.js +18 -0
  68. package/dist/src/redirect-proxy.d.ts +105 -0
  69. package/dist/src/redirect-proxy.js +665 -0
  70. package/dist/src/reflect.d.ts +74 -0
  71. package/dist/src/reflect.js +392 -0
  72. package/dist/src/resources.d.ts +26 -0
  73. package/dist/src/resources.js +22 -0
  74. package/dist/src/root.d.ts +114 -0
  75. package/dist/src/root.js +312 -0
  76. package/dist/src/run-task-worker.d.ts +1 -0
  77. package/dist/src/run-task-worker.js +38 -0
  78. package/dist/src/run-task.d.ts +18 -0
  79. package/dist/src/run-task.js +48 -0
  80. package/dist/src/runtime-test-support.d.ts +59 -0
  81. package/dist/src/runtime-test-support.js +205 -0
  82. package/dist/src/runtime.d.ts +256 -0
  83. package/dist/src/runtime.js +3502 -0
  84. package/dist/src/schema.d.ts +449 -0
  85. package/dist/src/schema.js +605 -0
  86. package/dist/src/serve.d.ts +30 -0
  87. package/dist/src/serve.js +82 -0
  88. package/dist/src/served-world.d.ts +194 -0
  89. package/dist/src/served-world.js +986 -0
  90. package/dist/src/service-exit.d.ts +46 -0
  91. package/dist/src/service-exit.js +195 -0
  92. package/dist/src/service-recorder.d.ts +1 -0
  93. package/dist/src/service-recorder.js +121 -0
  94. package/dist/src/sibling.d.ts +1 -0
  95. package/dist/src/sibling.js +9 -0
  96. package/dist/src/signals.d.ts +1 -0
  97. package/dist/src/signals.js +11 -0
  98. package/dist/src/storage-capacity.d.ts +8 -0
  99. package/dist/src/storage-capacity.js +61 -0
  100. package/dist/src/tail.d.ts +30 -0
  101. package/dist/src/tail.js +160 -0
  102. package/dist/src/tcp-port.d.ts +2 -0
  103. package/dist/src/tcp-port.js +36 -0
  104. package/dist/src/up-task-worker.d.ts +1 -0
  105. package/dist/src/up-task-worker.js +61 -0
  106. package/dist/src/up-task.d.ts +17 -0
  107. package/dist/src/up-task.js +49 -0
  108. package/dist/src/websocket-relay.d.ts +3 -0
  109. package/dist/src/websocket-relay.js +40 -0
  110. package/known-external-services.json +1108 -0
  111. package/package.json +83 -0
  112. package/src/ancestry.ts +36 -0
  113. package/src/app-url.ts +253 -0
  114. package/src/attach.ts +117 -0
  115. package/src/branch.ts +63 -0
  116. package/src/browser-proxy-cli.ts +44 -0
  117. package/src/ca-trust.ts +57 -0
  118. package/src/catalog.ts +156 -0
  119. package/src/changeset.ts +627 -0
  120. package/src/cli.ts +1111 -0
  121. package/src/command-lifetime.ts +79 -0
  122. package/src/configs.ts +110 -0
  123. package/src/console-apart.ts +90 -0
  124. package/src/consumers.ts +185 -0
  125. package/src/covers.ts +934 -0
  126. package/src/fixture-env.ts +230 -0
  127. package/src/host-cli.ts +90 -0
  128. package/src/host-worker.ts +23 -0
  129. package/src/host.ts +169 -0
  130. package/src/index.ts +171 -0
  131. package/src/infra-cli.ts +133 -0
  132. package/src/init.ts +1316 -0
  133. package/src/inject-map.ts +72 -0
  134. package/src/lifecycle-record.ts +168 -0
  135. package/src/origin.ts +134 -0
  136. package/src/pack-facts.ts +128 -0
  137. package/src/pglite-backing.ts +141 -0
  138. package/src/pglite-host.mjs +147 -0
  139. package/src/placeholder.ts +89 -0
  140. package/src/prerequisites.ts +66 -0
  141. package/src/process-groups.ts +33 -0
  142. package/src/project-inspect.ts +770 -0
  143. package/src/proxy-daemon.ts +21 -0
  144. package/src/redirect-proxy.ts +684 -0
  145. package/src/reflect.ts +440 -0
  146. package/src/resources.ts +22 -0
  147. package/src/root.ts +290 -0
  148. package/src/run-task-worker.ts +27 -0
  149. package/src/run-task.ts +44 -0
  150. package/src/runtime-test-support.ts +208 -0
  151. package/src/runtime.ts +3357 -0
  152. package/src/schema.ts +922 -0
  153. package/src/serve.ts +102 -0
  154. package/src/served-world.ts +812 -0
  155. package/src/service-exit.ts +175 -0
  156. package/src/service-recorder.ts +89 -0
  157. package/src/sibling.ts +10 -0
  158. package/src/signals.ts +10 -0
  159. package/src/storage-capacity.ts +60 -0
  160. package/src/tail.ts +205 -0
  161. package/src/tcp-port.ts +35 -0
  162. package/src/up-task-worker.ts +40 -0
  163. package/src/up-task.ts +45 -0
  164. package/src/websocket-relay.ts +32 -0
@@ -0,0 +1,2 @@
1
+ export declare function assertWorldStateRemovable(root: string, path: string): void;
2
+ export declare function removeWorldState<T>(root: string, path: string, remove: () => T): T;
@@ -0,0 +1,42 @@
1
+ // Check discoverable legacy pointers before removing retained World storage. New pointers
2
+ // carry parent-side pins, including clones across checkouts, without a global World registry.
3
+ import { existsSync, readdirSync } from 'node:fs';
4
+ import { join, relative, resolve, sep } from 'node:path';
5
+ import { assertStateRemovable, readBranchMeta, stateDirName, withAncestryLock, withStateRemoval, getActiveWorldStore, worldPaths, worldStateRoot } from '@volter/world-core';
6
+ function checkWorldAncestry(root, path) {
7
+ const base = join(root, stateDirName(), 'worlds');
8
+ if (!existsSync(base))
9
+ return;
10
+ for (const world of readdirSync(base, { withFileTypes: true })) {
11
+ if (!world.isDirectory() || world.name.startsWith('.'))
12
+ continue;
13
+ const data = join(base, world.name, 'data');
14
+ if (!existsSync(data))
15
+ continue;
16
+ for (const service of readdirSync(data, { withFileTypes: true })) {
17
+ if (!service.isDirectory())
18
+ continue;
19
+ const control = join(data, service.name);
20
+ const states = worldStateRoot(control);
21
+ if (!existsSync(states))
22
+ continue;
23
+ for (const state of readdirSync(states, { withFileTypes: true })) {
24
+ if (!state.isDirectory())
25
+ continue;
26
+ const meta = readBranchMeta(state.name, control);
27
+ if (!meta?.parent || /^[a-z][a-z0-9+.-]*:\/\//i.test(meta.parent.at))
28
+ continue;
29
+ const parent = worldPaths(state.name, meta.parent.at).dir;
30
+ const under = (target) => { const rel = relative(getActiveWorldStore().canonicalPath?.(path) ?? resolve(path), getActiveWorldStore().canonicalPath?.(target) ?? resolve(target)); return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !rel.startsWith(sep)); };
31
+ if (under(parent) && !under(control))
32
+ throw new Error(`Retained branch parent: dependent World ${world.name} (${control}). Remove dependent branches first; down without purge can still stop compute.`);
33
+ }
34
+ }
35
+ }
36
+ }
37
+ export function assertWorldStateRemovable(root, path) {
38
+ withAncestryLock(() => { checkWorldAncestry(root, path); assertStateRemovable(path); });
39
+ }
40
+ export function removeWorldState(root, path, remove) {
41
+ return withStateRemoval(path, remove, () => checkWorldAncestry(root, path));
42
+ }
@@ -0,0 +1,47 @@
1
+ /** Who answered: an explicit `--set`, a `--detect` inspection, or the world's own `app` service. */
2
+ export type AppUrlSource = 'set' | 'detect' | 'service';
3
+ export type AppUrlRecord = {
4
+ world: string;
5
+ url: string;
6
+ via: AppUrlSource;
7
+ /** ISO timestamp of the registration (absent for the `app`-service fallback — nothing was recorded) */
8
+ recordedAt?: string;
9
+ /** how the URL was obtained, for the human reading `--json` output later */
10
+ detail?: string;
11
+ /** the application's own production hostnames (`--host`), routed to `url` inside the World as DNS routes them to
12
+ * its host in production: the injector, the socket backstop and the redirect proxy read them from this record */
13
+ hosts?: string[];
14
+ };
15
+ export declare function appUrlFile(root: string, name: string): string;
16
+ /** Record `url` as the app endpoint of the RUNNING/booted instance `name`. The instance must
17
+ * exist (a record for a world that was never upped would describe nothing); the app itself is
18
+ * deliberately NOT probed — the booter declaring "this is where I put the app" is the truth. */
19
+ export declare function setAppUrl(name: string, url: string, options?: {
20
+ root?: string;
21
+ via?: Exclude<AppUrlSource, 'service'>;
22
+ detail?: string;
23
+ }): AppUrlRecord;
24
+ /** Record `hosts` as the app's own production hostnames, beside its recorded URL (or its `app` service's). */
25
+ export declare function addAppHosts(name: string, hosts: string[], options?: {
26
+ root?: string;
27
+ }): AppUrlRecord;
28
+ /**
29
+ *`--detect <pid|port>`: resolve the app URL off what is ALREADY listening and record it.
30
+ * Disambiguation (both are just numbers): a live pid with listening ports wins — its single
31
+ * listening port is the app; several listening ports is a loud error (name the port instead).
32
+ * Otherwise the number is tried as a loopback port and must actually be listening. Detection
33
+ * that finds nothing refuses loudly rather than recording a URL nothing serves.
34
+ */
35
+ export declare function detectAppUrl(name: string, target: string, options?: {
36
+ root?: string;
37
+ }): Promise<AppUrlRecord>;
38
+ /**
39
+ * The recorded app URL of `name`, or the `app` service's assigned URL when nothing was recorded
40
+ * (the world-boots-the-app convention), or null. Null is the CALLER's loud-error cue — the CLI
41
+ * turns it into exit 1 with the registration recipe; a library consumer decides for itself.
42
+ */
43
+ export declare function readAppUrl(name: string, options?: {
44
+ root?: string;
45
+ }): AppUrlRecord | null;
46
+ /** The message the CLI prints when no app URL is known — the registration recipe, not a guess. */
47
+ export declare function appUrlUnsetMessage(name: string): string;
@@ -0,0 +1,239 @@
1
+ // `volter-world app-url` — the recorded, queryable APP endpoint of a world instance.
2
+ //
3
+ // Worlds own the lifecycle of SERVICES; the app under test is usually booted OUTSIDE the world by
4
+ // the caller (the attach pattern), so nothing in the instance knows where the app listens — every
5
+ // consumer (a browser step, a webhook target, a teammate's curl) re-derives it by folklore. The
6
+ // subject pilots hit exactly this: the world's URLs are one `volter-world url` away, the APP's URL
7
+ // is tribal knowledge. This verb makes the app endpoint a first-class instance record:
8
+ //
9
+ // • the BOOTER writes it once, as the LAST boot step — `--set <url>`, or `--detect <pid|port>`
10
+ // to read it off the already-listening process instead of typing it;
11
+ // • any consumer reads it back (`volter-world app-url <world> [--json]`);
12
+ // • an UNSET read fails LOUDLY with the registration recipe — never a guess, never a default.
13
+ //
14
+ // A world that boots the app itself declares an `app` service (`type: 'process'`) — that existing
15
+ // convention needs no registration: the read falls back to the `app` service's assigned URL.
16
+ //
17
+ // RUNTIME MINIMALISM HOLDS: this is a record, not orchestration. The runtime never boots, probes,
18
+ // supervises, or health-checks the app — `--detect` inspects the caller-named pid/port exactly
19
+ // once, at the caller's request, and stores what it saw. The record lives in the instance dir, so
20
+ // a re-`up` (which wipes the dir) or `down --purge` clears it: a recorded URL never outlives the
21
+ // instance it described.
22
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import { spawnSync } from 'node:child_process';
25
+ import net from 'node:net';
26
+ import { instanceDir, statusWorld } from "./runtime.js";
27
+ import { loadInject } from "./inject-map.js";
28
+ import { packFacts } from "./pack-facts.js";
29
+ export function appUrlFile(root, name) {
30
+ return join(instanceDir(root, name), 'app-url.json');
31
+ }
32
+ function assertHttpUrl(url) {
33
+ let parsed;
34
+ try {
35
+ parsed = new URL(url);
36
+ }
37
+ catch {
38
+ throw new Error(`volter-world app-url: "${url}" is not a URL (want e.g. http://127.0.0.1:3000)`);
39
+ }
40
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
41
+ throw new Error(`volter-world app-url: "${url}" must be http(s) — an app endpoint consumers can fetch`);
42
+ }
43
+ return parsed;
44
+ }
45
+ /** Record `url` as the app endpoint of the RUNNING/booted instance `name`. The instance must
46
+ * exist (a record for a world that was never upped would describe nothing); the app itself is
47
+ * deliberately NOT probed — the booter declaring "this is where I put the app" is the truth. */
48
+ export function setAppUrl(name, url, options = {}) {
49
+ const root = options.root ?? process.cwd();
50
+ const status = statusWorld(name, root); // throws "World instance not found" — the loud path
51
+ assertHttpUrl(url);
52
+ // a new URL keeps the hosts recorded for the app (they name the app, not where it listens)
53
+ const hosts = existsSync(appUrlFile(root, name)) ? JSON.parse(readFileSync(appUrlFile(root, name), 'utf8')).hosts : undefined;
54
+ const record = {
55
+ world: status.name,
56
+ url,
57
+ via: options.via ?? 'set',
58
+ recordedAt: new Date().toISOString(),
59
+ ...(options.detail === undefined ? {} : { detail: options.detail }),
60
+ ...(hosts?.length ? { hosts } : {}),
61
+ };
62
+ writeFileSync(appUrlFile(root, name), `${JSON.stringify(record, null, 2)}\n`);
63
+ return record;
64
+ }
65
+ /** A hostname the app may answer as: a DNS name (not an address, not loopback) that no twin serves, and not the
66
+ * host of this World's own origin or of a twin it runs (their requests carry the World key). */
67
+ function assertAppHost(host, world, root) {
68
+ const name = host.trim().toLowerCase().replace(/\.$/, '');
69
+ // a wildcard (`*.dub.link`) stands for every name below its parent, as a DNS wildcard record does; its parent has two
70
+ // labels at least (never a bare TLD). Whether the parent is a public suffix (co.uk, github.io) is the operator's word:
71
+ // a World keeps no public suffix list
72
+ const wildcard = name.startsWith('*.');
73
+ const bare = wildcard ? name.slice(2) : name;
74
+ if (!/^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/.test(bare) || net.isIP(bare) || bare === 'localhost' || bare.endsWith('.localhost')) {
75
+ throw new Error(`volter-world app-url: --host "${host}" is not a DNS hostname the app answers as (e.g. app.example.com, or *.example.com)`);
76
+ }
77
+ const inject = loadInject();
78
+ let vendor = Object.keys(inject.VENDOR_HOSTS).find((v) => inject.VENDOR_HOSTS[v](bare));
79
+ if (!vendor && wildcard) {
80
+ // a wildcard is refused when any name a vendor's rule can give its twin lies under it, or it lies under a vendor's
81
+ // host suffix: the rules' names, read from the descriptors (a host, a suffix, a pattern's literal tail) and from
82
+ // the hand table's predicates (their quoted names)
83
+ const names = vendorNames(inject.VENDOR_HOSTS);
84
+ const dotted = `.${bare}`;
85
+ vendor = names.find((n) => n.name === bare || n.name.endsWith(dotted) || (n.suffix && dotted.endsWith(n.name.startsWith('.') ? n.name : `.${n.name}`)))?.vendor;
86
+ }
87
+ if (vendor)
88
+ throw new Error(`volter-world app-url: --host ${name} is ${vendor}'s host, which its twin serves; an app cannot answer as it`);
89
+ const status = statusWorld(world, root);
90
+ const own = [status.env.VOLTER_WORLD ?? '', ...Object.values(status.services).map((service) => service.url ?? '')]
91
+ .flatMap((url) => { try {
92
+ return [new URL(url).hostname.toLowerCase()];
93
+ }
94
+ catch {
95
+ return [];
96
+ } });
97
+ if (own.some((h) => h === name || (wildcard && h.endsWith(`.${bare}`))))
98
+ throw new Error(`volter-world app-url: --host ${name} is this World's own origin or a twin's; an app cannot answer as it`);
99
+ return name;
100
+ }
101
+ /** Every hostname the vendor table can name, with its vendor: a descriptor rule's host, suffix, or a host pattern's
102
+ * literal tail (a suffix), and each quoted name in a hand-table predicate (a leading dot marks a suffix). */
103
+ function vendorNames(table) {
104
+ const out = [];
105
+ for (const [vendor, facts] of Object.entries(packFacts())) {
106
+ for (const rule of facts.hosts ?? []) {
107
+ if (rule.exclude)
108
+ continue;
109
+ if (rule.host)
110
+ out.push({ vendor: rule.key ?? vendor, name: rule.host.toLowerCase(), suffix: false });
111
+ if (rule.suffix)
112
+ out.push({ vendor: rule.key ?? vendor, name: rule.suffix.toLowerCase().replace(/^-/, '.'), suffix: true });
113
+ if (rule.hostPattern) {
114
+ // the literal tail, and each alternative of a trailing group joined to what precedes it
115
+ // (`\.(amazonaws\.com|api\.aws)$` names both)
116
+ const src = rule.hostPattern.replace(/\\\./g, '.').replace(/\$$/, '');
117
+ const group = /([a-z0-9.-]*)\((?:\?:)?([a-z0-9.|-]+)\)$/i.exec(src);
118
+ const tails = group ? group[2].split('|').map((alt) => `${group[1]}${alt}`) : [/[a-z0-9.-]+$/i.exec(src)?.[0] ?? ''];
119
+ for (const tail of tails)
120
+ if (tail.includes('.'))
121
+ out.push({ vendor: rule.key ?? vendor, name: tail.toLowerCase(), suffix: true });
122
+ }
123
+ }
124
+ }
125
+ for (const [vendor, predicate] of Object.entries(table)) {
126
+ for (const m of String(predicate).matchAll(/['"`](\.?[a-z0-9-]+(?:\.[a-z0-9-]+)+)['"`]/gi))
127
+ out.push({ vendor, name: m[1].toLowerCase(), suffix: m[1].startsWith('.') });
128
+ }
129
+ return out;
130
+ }
131
+ /** Record `hosts` as the app's own production hostnames, beside its recorded URL (or its `app` service's). */
132
+ export function addAppHosts(name, hosts, options = {}) {
133
+ const root = options.root ?? process.cwd();
134
+ const current = readAppUrl(name, { root });
135
+ if (!current)
136
+ throw new Error(appUrlUnsetMessage(name));
137
+ const all = [...new Set([...(current.hosts ?? []), ...hosts.map((host) => assertAppHost(host, name, root))])].sort();
138
+ // hosts beside the World's own `app` service keep no URL: they follow the service wherever a boot puts it
139
+ const record = current.via === 'service' ? { world: current.world, via: 'service', hosts: all } : { ...current, hosts: all };
140
+ writeFileSync(appUrlFile(root, name), `${JSON.stringify(record, null, 2)}\n`);
141
+ return current.via === 'service' ? { ...current, hosts: all } : record;
142
+ }
143
+ /** The listening TCP ports of a live pid, via lsof (macOS + Linux). */
144
+ function listeningTcpPorts(pid) {
145
+ const result = spawnSync('lsof', ['-a', '-p', String(pid), '-iTCP', '-sTCP:LISTEN', '-P', '-n', '-Fn'], { encoding: 'utf8' });
146
+ if (result.error) {
147
+ throw new Error(`volter-world app-url: \`lsof\` is unavailable (${result.error.message}) — pass the port, or --set the URL directly`);
148
+ }
149
+ const ports = new Set();
150
+ for (const line of (result.stdout ?? '').split('\n')) {
151
+ if (!line.startsWith('n'))
152
+ continue;
153
+ const port = Number(line.slice(line.lastIndexOf(':') + 1));
154
+ if (Number.isInteger(port) && port > 0 && port <= 65535)
155
+ ports.add(port);
156
+ }
157
+ return [...ports].sort((a, b) => a - b);
158
+ }
159
+ function pidIsLive(pid) {
160
+ try {
161
+ process.kill(pid, 0);
162
+ return true;
163
+ }
164
+ catch (error) {
165
+ return error.code === 'EPERM';
166
+ }
167
+ }
168
+ /** One TCP connection attempt to 127.0.0.1:<port> — is anything listening RIGHT NOW? */
169
+ function tcpListening(port, timeoutMs = 1500) {
170
+ return new Promise((resolvePromise) => {
171
+ const socket = net.connect({ port, host: '127.0.0.1' });
172
+ const done = (ok) => {
173
+ socket.destroy();
174
+ resolvePromise(ok);
175
+ };
176
+ socket.once('connect', () => done(true));
177
+ socket.once('error', () => done(false));
178
+ socket.setTimeout(timeoutMs, () => done(false));
179
+ });
180
+ }
181
+ /**
182
+ *`--detect <pid|port>`: resolve the app URL off what is ALREADY listening and record it.
183
+ * Disambiguation (both are just numbers): a live pid with listening ports wins — its single
184
+ * listening port is the app; several listening ports is a loud error (name the port instead).
185
+ * Otherwise the number is tried as a loopback port and must actually be listening. Detection
186
+ * that finds nothing refuses loudly rather than recording a URL nothing serves.
187
+ */
188
+ export async function detectAppUrl(name, target, options = {}) {
189
+ const numeric = Number(target);
190
+ if (!Number.isInteger(numeric) || numeric <= 0) {
191
+ throw new Error(`volter-world app-url: --detect wants a pid or a port, got "${target}"`);
192
+ }
193
+ if (pidIsLive(numeric)) {
194
+ const ports = listeningTcpPorts(numeric);
195
+ if (ports.length === 1) {
196
+ return setAppUrl(name, `http://127.0.0.1:${ports[0]}`, { ...options, via: 'detect', detail: `detected from pid ${numeric} (listening on ${ports[0]})` });
197
+ }
198
+ if (ports.length > 1) {
199
+ throw new Error(`volter-world app-url: pid ${numeric} is listening on ${ports.length} ports (${ports.join(', ')}) — `
200
+ + `ambiguous. Detect the app's port directly (--detect <port>) or record it explicitly (--set <url>).`);
201
+ }
202
+ // a live pid with NO listeners: fall through — the number was probably meant as a port
203
+ }
204
+ if (numeric <= 65535 && await tcpListening(numeric)) {
205
+ return setAppUrl(name, `http://127.0.0.1:${numeric}`, { ...options, via: 'detect', detail: `port ${numeric} is listening on 127.0.0.1` });
206
+ }
207
+ throw new Error(`volter-world app-url: nothing to detect from "${target}" — it is not a live pid with a listening TCP port, `
208
+ + `and nothing is listening on 127.0.0.1:${target}. Boot the app first, then register its URL (--set <url> once it listens).`);
209
+ }
210
+ /**
211
+ * The recorded app URL of `name`, or the `app` service's assigned URL when nothing was recorded
212
+ * (the world-boots-the-app convention), or null. Null is the CALLER's loud-error cue — the CLI
213
+ * turns it into exit 1 with the registration recipe; a library consumer decides for itself.
214
+ */
215
+ export function readAppUrl(name, options = {}) {
216
+ const root = options.root ?? process.cwd();
217
+ const status = statusWorld(name, root); // loud when the world does not exist at all
218
+ const file = appUrlFile(root, name);
219
+ const app = status.services['app'];
220
+ if (existsSync(file)) {
221
+ const record = JSON.parse(readFileSync(file, 'utf8'));
222
+ // hosts recorded beside the World's own `app` service: its URL is this boot's
223
+ if (!record.url && app?.url !== undefined)
224
+ return { world: status.name, url: app.url, via: 'service', detail: 'the world\'s own "app" service', ...(record.hosts ? { hosts: record.hosts } : {}) };
225
+ if (record.url)
226
+ return record;
227
+ }
228
+ if (app?.url !== undefined) {
229
+ return { world: status.name, url: app.url, via: 'service', detail: 'the world\'s own "app" service (nothing was recorded)' };
230
+ }
231
+ return null;
232
+ }
233
+ /** The message the CLI prints when no app URL is known — the registration recipe, not a guess. */
234
+ export function appUrlUnsetMessage(name) {
235
+ return (`World ${name} has no recorded app URL.\n`
236
+ + `If the app is booted OUTSIDE the world (the attach pattern), the booter registers it as the LAST boot step:\n`
237
+ + ` volter-world app-url ${name} --set http://127.0.0.1:<port> # or: --detect <pid|port>\n`
238
+ + `A world that boots the app itself can declare an "app" service (type "process") — its URL answers automatically.`);
239
+ }
@@ -0,0 +1,48 @@
1
+ export declare const WORLD_REF_FILE = ".volter-world";
2
+ export type WorldRefSource = 'explicit' | 'env' | 'file';
3
+ export interface ResolvedWorldRef {
4
+ ref: string;
5
+ source: WorldRefSource;
6
+ /** set when source === 'file' — the .volter-world file that supplied the ref */
7
+ file?: string;
8
+ }
9
+ /** A world reference is remote when it is a URL; every other ref is a local world name. */
10
+ export declare function isRemoteWorldRef(ref: string): boolean;
11
+ /** Find the nearest .volter-world file from `cwd` upward. First non-empty,
12
+ * non-comment line is the ref (à la .nvmrc). */
13
+ export declare function findWorldRefFile(cwd: string): {
14
+ file: string;
15
+ ref: string;
16
+ } | undefined;
17
+ /** Resolution order (docs/guides/route-a-cli-through-the-world.md): explicit argument → VOLTER_WORLD env var →
18
+ * nearest committed .volter-world file. No silent machine-default fallback:
19
+ * a missing ref is an explicit, actionable error. */
20
+ export declare function resolveWorldRef(explicit: string | undefined, options?: {
21
+ cwd?: string;
22
+ env?: Record<string, string | undefined>;
23
+ }): ResolvedWorldRef;
24
+ /** The manifest (docs/guides/route-a-cli-through-the-world.md): what a world publishes to its attachers. */
25
+ export interface WorldManifest {
26
+ name: string;
27
+ /** vendor host → twin endpoint, exactly the map the injector/proxy redirect */
28
+ vendors: Record<string, string>;
29
+ /** session CA (PEM) when the world has one, else null */
30
+ ca: string | null;
31
+ /** attachment traffic proxy when present, else null */
32
+ proxy: string | null;
33
+ /** suggested fake credentials — the world env minus runtime internals */
34
+ env: Record<string, string>;
35
+ /** a hosted world's byte-stream doors, by service id: the WebSocket that carries the protocol and the
36
+ * env its client reads, `${host}`/`${port}` being the attacher's loopback bridge
37
+ * (@volter/world-core/stream-bridge) */
38
+ streams?: Record<string, {
39
+ protocol: string;
40
+ url: string;
41
+ env: Record<string, string>;
42
+ }>;
43
+ }
44
+ /** Derive the manifest from a world's generated env map (the local rendering).
45
+ * Pure — the CA is inlined only when its file is readable. */
46
+ export declare function deriveWorldManifest(name: string, worldEnv: Record<string, string | undefined>): WorldManifest;
47
+ /** The manifest of a local world instance (name resolved against `root` like every other verb). */
48
+ export declare function worldManifest(name: string, root?: string): WorldManifest;
@@ -0,0 +1,87 @@
1
+ // The attach surface of the two-part model (docs/guides/route-a-cli-through-the-world.md): resolving a world
2
+ // reference and deriving the world manifest — the single document a world
3
+ // publishes and every attachment mechanism consumes. `world.env` /
4
+ // `instance.json` remain the local renderings; this module derives the manifest
5
+ // FROM them (it invents nothing). Attaching itself stays thin: `--via env`
6
+ // delegates to runWithWorldEnv, `--via direct` prints the manifest. Reflect and
7
+ // remote refs are proposals (docs/guides/route-a-cli-through-the-world.md) and are refused loudly, never
8
+ // silently degraded.
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { dirname, join, resolve } from 'node:path';
11
+ import { activeVendorMap } from "./redirect-proxy.js";
12
+ import { statusWorld } from "./runtime.js";
13
+ export const WORLD_REF_FILE = '.volter-world';
14
+ /** A world reference is remote when it is a URL; every other ref is a local world name. */
15
+ export function isRemoteWorldRef(ref) {
16
+ return /^https?:\/\//i.test(ref);
17
+ }
18
+ /** Find the nearest .volter-world file from `cwd` upward. First non-empty,
19
+ * non-comment line is the ref (à la .nvmrc). */
20
+ export function findWorldRefFile(cwd) {
21
+ let dir = resolve(cwd);
22
+ for (;;) {
23
+ const candidate = join(dir, WORLD_REF_FILE);
24
+ if (existsSync(candidate)) {
25
+ const line = readFileSync(candidate, 'utf8')
26
+ .split('\n')
27
+ .map((entry) => entry.trim())
28
+ .find((entry) => entry.length > 0 && !entry.startsWith('#'));
29
+ if (line)
30
+ return { file: candidate, ref: line };
31
+ }
32
+ const parent = dirname(dir);
33
+ if (parent === dir)
34
+ return undefined;
35
+ dir = parent;
36
+ }
37
+ }
38
+ /** Resolution order (docs/guides/route-a-cli-through-the-world.md): explicit argument → VOLTER_WORLD env var →
39
+ * nearest committed .volter-world file. No silent machine-default fallback:
40
+ * a missing ref is an explicit, actionable error. */
41
+ export function resolveWorldRef(explicit, options = {}) {
42
+ if (explicit !== undefined && explicit.length > 0)
43
+ return { ref: explicit, source: 'explicit' };
44
+ const env = options.env ?? process.env;
45
+ const fromEnv = env.VOLTER_WORLD;
46
+ if (fromEnv !== undefined && fromEnv.trim().length > 0)
47
+ return { ref: fromEnv.trim(), source: 'env' };
48
+ const fromFile = findWorldRefFile(options.cwd ?? process.cwd());
49
+ if (fromFile)
50
+ return { ref: fromFile.ref, source: 'file', file: fromFile.file };
51
+ throw new Error(`no world reference: pass one (volter-world attach <world> …), set VOLTER_WORLD, or commit a ${WORLD_REF_FILE} file`);
52
+ }
53
+ // Internal runtime vars are plumbing, not suggested credentials. CA-trust
54
+ // bundle vars are local renderings of `ca` (they hold machine-local paths), so
55
+ // a manifest never carries them — an attacher re-renders trust from `ca`.
56
+ const MANIFEST_ENV_EXCLUDED = /^(volter_world_|node_options$|twin_inject$|no_proxy$|https?_proxy$|node_extra_ca_certs$|ssl_cert_file$|(aws|curl|requests)_ca_bundle$)/i;
57
+ /** Derive the manifest from a world's generated env map (the local rendering).
58
+ * Pure — the CA is inlined only when its file is readable. */
59
+ export function deriveWorldManifest(name, worldEnv) {
60
+ const vendors = activeVendorMap(worldEnv);
61
+ const caPath = worldEnv.VOLTER_WORLD_CA;
62
+ let ca = null;
63
+ if (caPath && existsSync(caPath)) {
64
+ try {
65
+ ca = readFileSync(caPath, 'utf8');
66
+ }
67
+ catch {
68
+ ca = null;
69
+ }
70
+ }
71
+ const env = {};
72
+ for (const [key, value] of Object.entries(worldEnv)) {
73
+ if (value === undefined)
74
+ continue;
75
+ if (MANIFEST_ENV_EXCLUDED.test(key))
76
+ continue;
77
+ if (/_TWIN_URL$/.test(key))
78
+ continue; // rendered as `vendors`, not credentials
79
+ env[key] = value;
80
+ }
81
+ return { name, vendors, ca, proxy: worldEnv.VOLTER_WORLD_PROXY ?? null, env };
82
+ }
83
+ /** The manifest of a local world instance (name resolved against `root` like every other verb). */
84
+ export function worldManifest(name, root = process.cwd()) {
85
+ const status = statusWorld(name, root);
86
+ return deriveWorldManifest(name, status.env);
87
+ }
@@ -0,0 +1,20 @@
1
+ import type { WorldInstance } from './schema.js';
2
+ export type BranchOptions = {
3
+ root?: string;
4
+ envFile?: string;
5
+ now?: Date; /** branch from a point in the base's history: an instant, or a position per twin (contract "Just like Neon", 3) */
6
+ at?: {
7
+ instant?: string;
8
+ positions?: Record<string, number>;
9
+ views?: Record<string, string>;
10
+ };
11
+ };
12
+ /** A new world over the base's mirrors. */
13
+ export declare function branchWorld(base: string, name: string, options?: BranchOptions): Promise<{
14
+ instance: WorldInstance;
15
+ forked: Record<string, string[]>;
16
+ }>;
17
+ /** A stopped world back up, from its own record. */
18
+ export declare function checkoutWorld(name: string, options?: {
19
+ root?: string;
20
+ }): Promise<WorldInstance>;
@@ -0,0 +1,65 @@
1
+ // `volter-world branch` and `checkout` — a branch is a base position in the mirror plus its own
2
+ // log of changes, and a world is a branch with compute attached (docs/concepts/the-model.md).
3
+ // `branch <world> <name>` boots a new world from the base's config and forks every twin's mirror
4
+ // into it (core's `forkTwin`: a POINTER to the base at its current position — the base's whole
5
+ // history, its own entries included, is the branch's parent; nothing is copied). `checkout <name>`
6
+ // brings a stopped world back up from its own instance record, state intact.
7
+ import { existsSync, readdirSync } from 'node:fs';
8
+ import { dirname, join, resolve } from 'node:path';
9
+ import { captureHistory, historyAtInstant, forkTwin, stateGeneration, withAncestryLock, worldPaths, worldStateRoot } from '@volter/world-core';
10
+ import { downWorld, saveWorldInstance, statusWorld, upWorld } from "./runtime.js";
11
+ function stateServices(controlRoot) {
12
+ const stateRoot = worldStateRoot(controlRoot);
13
+ if (!existsSync(stateRoot))
14
+ return [];
15
+ return readdirSync(stateRoot, { withFileTypes: true }).filter((e) => e.isDirectory() && ['events.jsonl', 'actions.jsonl', 'branch.json'].some((f) => existsSync(join(stateRoot, e.name, f)))).map((e) => e.name).sort();
16
+ }
17
+ /** A new world over the base's mirrors. */
18
+ export async function branchWorld(base, name, options = {}) {
19
+ const root = resolve(options.root ?? process.cwd());
20
+ const from = statusWorld(base, root);
21
+ if (name === base)
22
+ throw new Error(`volter-world branch: the branch needs a name other than the base's ("${base}")`);
23
+ const envFile = options.envFile ?? join(dirname(from.envFile), `${name}.world.env`);
24
+ const at = (options.now ?? new Date()).toISOString();
25
+ // Capture source identity before awaiting child compute. A concurrent reset may win;
26
+ // in that case forking fails and the task-owned child is torn down, never an empty success.
27
+ const source = withAncestryLock(() => Object.keys(from.services).sort().map(service => {
28
+ const fromRoot = join(from.dirs.data, service);
29
+ return { service, fromRoot, states: stateServices(fromRoot).map(state => {
30
+ let view = options.at?.views?.[service] ?? options.at?.views?.[state] ?? captureHistory(state, fromRoot).view;
31
+ const timed = options.at?.instant ? historyAtInstant(state, options.at.instant, fromRoot, view) : undefined;
32
+ if (timed)
33
+ view = timed.view;
34
+ const position = options.at?.positions?.[service] ?? options.at?.positions?.[state] ?? timed?.position;
35
+ return { state, view, position, generation: stateGeneration(worldPaths(state, fromRoot).dir) };
36
+ }) };
37
+ }));
38
+ const instance = await upWorld(existsSync(from.configPath) ? from.configPath : from.config, { name, root, mode: from.mode, envFile });
39
+ const forked = {};
40
+ try {
41
+ if (from.origin) {
42
+ instance.origin = { ...from.origin };
43
+ saveWorldInstance(instance);
44
+ }
45
+ for (const { service, fromRoot, states } of source) {
46
+ const toRoot = join(instance.dirs.data, service);
47
+ for (const { state, generation, view, position: pos } of states) {
48
+ forkTwin({ service: state, toRoot, fromRoot, view, expectedGeneration: generation, occurredAt: at, ...(pos !== undefined ? { at: pos } : {}) });
49
+ }
50
+ if (states.length)
51
+ forked[service] = states.map(s => s.state);
52
+ }
53
+ }
54
+ catch (error) {
55
+ await downWorld(name, root, { purge: true, expectedCreatedAt: instance.createdAt });
56
+ throw error;
57
+ }
58
+ return { instance, forked };
59
+ }
60
+ /** A stopped world back up, from its own record. */
61
+ export async function checkoutWorld(name, options = {}) {
62
+ const root = resolve(options.root ?? process.cwd());
63
+ const was = statusWorld(name, root);
64
+ return upWorld(existsSync(was.configPath) ? was.configPath : was.config, { name, root, mode: was.mode, envFile: was.envFile, keepState: true });
65
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // @ts-nocheck
3
+ import { keepProcessAlive } from '@volter/world-core/lifecycle';
4
+ // Browser proxy launcher for fullstack app worlds. This is APP/WORLD orchestration glue
5
+ // (it wires specific app vendors + app URLs from env), so it lives in @volter/world-runtime,
6
+ // NOT in the vendor-agnostic kernel — control-plane must never name a vendor (A2). The kernel
7
+ // only exports the vendor-INDEPENDENT `createTwinProxy` utility; choosing which vendors a given
8
+ // app world proxies is the orchestrator's job.
9
+ import { createTwinProxy } from '@volter/world-core';
10
+ const port = Number(process.env.PORT || 0);
11
+ const target = process.env.TARGET_URL;
12
+ if (!target)
13
+ throw new Error('TARGET_URL is required');
14
+ const map = {};
15
+ if (process.env.CLERK_TWIN_URL) {
16
+ map.clerk = {
17
+ origin: process.env.CLERK_TWIN_URL,
18
+ apiPathPrefix: '/v1/',
19
+ loaderHost: 'https://api.clerk.com',
20
+ };
21
+ map.clerkLoader = {
22
+ origin: process.env.CLERK_TWIN_URL,
23
+ apiPathPrefix: '/npm/',
24
+ };
25
+ }
26
+ if (process.env.RUNHUMAN_API_URL || process.env.API_URL) {
27
+ map.runhumanApi = {
28
+ origin: process.env.RUNHUMAN_API_URL || process.env.API_URL,
29
+ apiPathPrefix: '/api',
30
+ };
31
+ }
32
+ const redirectOrigins = [
33
+ process.env.RUNHUMAN_WEB_URL,
34
+ process.env.RUNHUMAN_LEARN_URL,
35
+ process.env.RUNHUMAN_STATIC_URL,
36
+ process.env.RUNHUMAN_API_URL,
37
+ process.env.API_URL,
38
+ ].filter(Boolean);
39
+ const proxy = createTwinProxy({ target, map, redirectOrigins, port });
40
+ process.stdout.write(`[browser-proxy] http://127.0.0.1:${proxy.port} -> ${target} (${Object.keys(map).join(', ') || 'no browser vendors'})\n`);
41
+ await keepProcessAlive();
@@ -0,0 +1,5 @@
1
+ export declare class CaTrustError extends Error {
2
+ }
3
+ export type CaTrustInputs = NodeJS.ProcessEnv | NodeJS.ProcessEnv[];
4
+ /** Per-client trust, materialized only when a caller attaches. Never modify the signing CA. */
5
+ export declare function sessionTrustEnv(caCertPath: string, inputs?: CaTrustInputs): Record<string, string>;