@volter/world-runtime 2.0.1 → 2.0.2

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 (63) hide show
  1. package/dist/src/app-url.js +1 -1
  2. package/dist/src/catalog.js +1 -1
  3. package/dist/src/cli.js +3 -3
  4. package/dist/src/console-apart.d.ts +1 -0
  5. package/dist/src/console-apart.js +7 -0
  6. package/dist/src/covers.js +4 -2
  7. package/dist/src/host-worker.js +2 -9
  8. package/dist/src/host.js +2 -9
  9. package/dist/src/import-module.d.ts +1 -0
  10. package/dist/src/import-module.js +16 -0
  11. package/dist/src/index.d.ts +7 -2
  12. package/dist/src/index.js +4 -1
  13. package/dist/src/infra-cli.js +2 -2
  14. package/dist/src/init.d.ts +1 -1
  15. package/dist/src/init.js +3 -0
  16. package/dist/src/local-branches.d.ts +37 -0
  17. package/dist/src/local-branches.js +193 -0
  18. package/dist/src/pglite-backing.js +8 -5
  19. package/dist/src/prerequisites.js +1 -1
  20. package/dist/src/process-groups.js +1 -1
  21. package/dist/src/redirect-proxy.js +2 -2
  22. package/dist/src/root.d.ts +23 -0
  23. package/dist/src/root.js +22 -10
  24. package/dist/src/run-task.js +1 -1
  25. package/dist/src/runtime.js +15 -15
  26. package/dist/src/schema.d.ts +5 -0
  27. package/dist/src/schema.js +10 -1
  28. package/dist/src/served-world.d.ts +196 -9
  29. package/dist/src/served-world.js +847 -103
  30. package/dist/src/service-recorder.js +1 -1
  31. package/dist/src/storage-capacity.js +2 -2
  32. package/dist/src/up-task.js +1 -1
  33. package/dist/src/world-origins.d.ts +13 -0
  34. package/dist/src/world-origins.js +37 -0
  35. package/dist/src/world-view.d.ts +25 -0
  36. package/dist/src/world-view.js +110 -0
  37. package/package.json +6 -5
  38. package/src/app-url.ts +1 -1
  39. package/src/catalog.ts +1 -1
  40. package/src/cli.ts +3 -3
  41. package/src/console-apart.ts +7 -1
  42. package/src/covers.ts +4 -2
  43. package/src/host-worker.ts +2 -1
  44. package/src/host.ts +2 -1
  45. package/src/import-module.ts +9 -0
  46. package/src/index.ts +7 -2
  47. package/src/infra-cli.ts +2 -2
  48. package/src/init.ts +3 -0
  49. package/src/local-branches.ts +173 -0
  50. package/src/pglite-backing.ts +8 -5
  51. package/src/prerequisites.ts +1 -1
  52. package/src/process-groups.ts +1 -1
  53. package/src/redirect-proxy.ts +2 -2
  54. package/src/root.ts +27 -2
  55. package/src/run-task.ts +1 -1
  56. package/src/runtime.ts +15 -15
  57. package/src/schema.ts +11 -1
  58. package/src/served-world.ts +762 -85
  59. package/src/service-recorder.ts +1 -1
  60. package/src/storage-capacity.ts +2 -2
  61. package/src/up-task.ts +1 -1
  62. package/src/world-origins.ts +38 -0
  63. package/src/world-view.ts +100 -0
@@ -142,7 +142,7 @@ export function addAppHosts(name, hosts, options = {}) {
142
142
  }
143
143
  /** The listening TCP ports of a live pid, via lsof (macOS + Linux). */
144
144
  function listeningTcpPorts(pid) {
145
- const result = spawnSync('lsof', ['-a', '-p', String(pid), '-iTCP', '-sTCP:LISTEN', '-P', '-n', '-Fn'], { encoding: 'utf8' });
145
+ const result = spawnSync('lsof', ['-a', '-p', String(pid), '-iTCP', '-sTCP:LISTEN', '-P', '-n', '-Fn'], { windowsHide: true, encoding: 'utf8' });
146
146
  if (result.error) {
147
147
  throw new Error(`volter-world app-url: \`lsof\` is unavailable (${result.error.message}) — pass the port, or --set the URL directly`);
148
148
  }
@@ -110,7 +110,7 @@ export function checkoutCatalog(root) {
110
110
  packDir: (vendor) => (isPackDir(join(packsDir, vendor)) ? join(packsDir, vendor) : undefined),
111
111
  version: (vendor) => packageVersion(join(packsDir, vendor)),
112
112
  sha: () => {
113
- const proc = spawnSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' });
113
+ const proc = spawnSync('git', ['-C', root, 'rev-parse', 'HEAD'], { windowsHide: true, encoding: 'utf8' });
114
114
  const sha = proc.status === 0 ? proc.stdout.trim() : '';
115
115
  return /^[0-9a-f]{40}$/.test(sha) ? sha : 'unknown';
116
116
  },
package/dist/src/cli.js CHANGED
@@ -981,7 +981,7 @@ async function main() {
981
981
  // VOLTER_WORLD names the World to the injector: a request addressed to its paths carries its key
982
982
  const commandEnv = { ...process.env, ...env, ...bridged.env, VOLTER_WORLD: resolved.ref };
983
983
  commandEnv.VOLTER_TWIN_INJECT_QUIET = head.includes('--verbose') ? '0' : commandEnv.VOLTER_TWIN_INJECT_QUIET ?? '1';
984
- const child = spawn(command[0], command.slice(1), { env: commandEnv, stdio: 'inherit' });
984
+ const child = spawn(command[0], command.slice(1), { windowsHide: true, env: commandEnv, stdio: 'inherit' });
985
985
  const code = await new Promise((resolveExit) => { child.on('exit', (status) => resolveExit(status ?? 1)); child.on('error', () => resolveExit(127)); });
986
986
  // a client that wrote and exited at once still has bytes on their way through a bridge
987
987
  await drainStreams();
@@ -1007,7 +1007,7 @@ async function main() {
1007
1007
  const compose = splitDockerComposeArgs(command);
1008
1008
  if (compose) {
1009
1009
  // the consumer's own service names, from its own files, through its own docker
1010
- const listed = spawnSync(compose.head[0], [...compose.head.slice(1), ...compose.composeFlags, 'config', '--services'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
1010
+ const listed = spawnSync(compose.head[0], [...compose.head.slice(1), ...compose.composeFlags, 'config', '--services'], { windowsHide: true, env, stdio: ['ignore', 'pipe', 'pipe'] });
1011
1011
  if (listed.status !== 0)
1012
1012
  throw new Error(`volter-world attach --via reflect: docker compose config --services failed:\n${listed.stderr.toString().trim()}`);
1013
1013
  const services = listed.stdout.toString().split('\n').map((s) => s.trim()).filter(Boolean);
@@ -1016,7 +1016,7 @@ async function main() {
1016
1016
  toRun = dockerComposeWithOverride(command, overridePath);
1017
1017
  process.stderr.write(`volter-world attach --via reflect: ${services.length} service(s) attached — dns ${manifest.resolverIp}:53, front ${manifest.targetIp}:443, CA ${ATTACHED_CA_PATH}\n`);
1018
1018
  }
1019
- const result = spawnSync(toRun[0], toRun.slice(1), { env, stdio: 'inherit' });
1019
+ const result = spawnSync(toRun[0], toRun.slice(1), { windowsHide: true, env, stdio: 'inherit' });
1020
1020
  process.exit(result.status ?? 1);
1021
1021
  }
1022
1022
  if (via !== 'env') {
@@ -4,6 +4,7 @@ export type ConsoleMount = {
4
4
  handle: (request: Request, base: string, opts?: {
5
5
  worlds?: string;
6
6
  names?: string[];
7
+ worldOrigin?: boolean;
7
8
  }) => Promise<Response | null>;
8
9
  };
9
10
  export declare const CONSOLE_BASE = "/-/console";
@@ -67,6 +67,9 @@ export async function serveConsoleFor(target, opts = {}) {
67
67
  throw error;
68
68
  }
69
69
  const hostname = opts.hostname ?? '127.0.0.1';
70
+ // the console's own origin, once it listens: a World that lets only its own origin frame its pages lets this
71
+ // console frame them too, as they are framed where it serves them itself
72
+ let consoleOrigin = '';
70
73
  const worldsServer = await serveHttp({
71
74
  hostname, port: opts.port !== undefined && opts.port !== 0 ? opts.port + 1 : 0, twinRequestJournal: false,
72
75
  async fetch(request) {
@@ -87,11 +90,15 @@ export async function serveConsoleFor(target, opts = {}) {
87
90
  const location = out.get('location');
88
91
  if (location?.startsWith(origin))
89
92
  out.set('location', `${url.origin}${location.slice(origin.length)}`);
93
+ const csp = out.get('content-security-policy');
94
+ if (csp && consoleOrigin)
95
+ out.set('content-security-policy', csp.replace(/frame-ancestors ([^;]*)/, (_all, allowed) => (/'none'/.test(allowed) ? `frame-ancestors ${allowed}` : `frame-ancestors ${allowed} ${consoleOrigin}`)));
90
96
  return new Response(answer.body, { status: answer.status, headers: out });
91
97
  },
92
98
  });
93
99
  const worlds = `http://${hostname}:${worldsServer.port}`;
94
100
  const consoleServer = await serveConsoleApart(mount, { upstream: origin, worlds, names, hostname, ...(opts.port !== undefined ? { port: opts.port } : {}) });
101
+ consoleOrigin = new URL(consoleServer.url).origin;
95
102
  return { url: consoleServer.url, worlds, servers: [consoleServer.server, worldsServer] };
96
103
  }
97
104
  /** The host's old console address (`/-/console/…`, a bookmark or a portal's link) sent on to the console's
@@ -450,8 +450,10 @@ function usesOf(signals) {
450
450
  export function detectRepoVendors(repoPath) {
451
451
  const repo = resolve(repoPath);
452
452
  // the platform's own packages — the twins an app installed, the command, the runtime — are
453
- // never a vendor signal: an app that installed @volter/twin-slack did not gain a Slack dependency
454
- const dependencies = projectDependencies(repo).filter((dep) => !dep.startsWith('@volter/') && !dep.startsWith('pypi:volter'));
453
+ // never a vendor signal: an app that installed @volter/twin-slack did not gain a Slack dependency.
454
+ // A Volter package a pack names as its vendor's SDK is one (`@volter/identity`, the Volter identity
455
+ // service's client, is the volteridentity twin's), as any vendor's SDK is.
456
+ const dependencies = projectDependencies(repo).filter((dep) => Object.hasOwn(SDK_TWINS, dep) || (!dep.startsWith('@volter/') && !dep.startsWith('pypi:volter')));
455
457
  const envNames = projectEnvNames(repo);
456
458
  const npmRegistries = projectNpmRegistries(repo);
457
459
  // vendor → detection sources
@@ -1,18 +1,11 @@
1
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
- });
6
- }
7
- return path;
8
- };
9
1
  // Worker entry for 'worker' isolation: one Worker thread per twin. It owns its event
10
2
  // loop + heap, so a CPU hog / leak / hard crash here is contained to this thread — the
11
3
  // host does NOT restart it: it gives up on the worker (fires a `gaveup` event → doctor
12
4
  // red) without disturbing sibling twins. Posts {type:'ready'} once listening.
13
5
  import { parentPort, workerData } from 'node:worker_threads';
6
+ import { importModule } from "./import-module.js";
14
7
  const spec = workerData;
15
- const mod = (await import(__rewriteRelativeImportExtension(spec.module)));
8
+ const mod = await importModule(spec.module);
16
9
  const factory = mod[spec.export];
17
10
  if (typeof factory !== 'function') {
18
11
  throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
package/dist/src/host.js CHANGED
@@ -1,11 +1,3 @@
1
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
- });
6
- }
7
- return path;
8
- };
9
1
  // Co-located twin host — run many twins in ONE process instead of one OS process
10
2
  // per twin. The twin core is a pure, transport-agnostic handler (see a pack's
11
3
  // `handle<Vendor>TwinRequest`), so nothing requires a process per twin; this host
@@ -29,9 +21,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
29
21
  // world config that drives it) stays vendor-agnostic — exactly like `bin` spawning.
30
22
  import { Worker } from 'node:worker_threads';
31
23
  import { siblingScript } from "./sibling.js";
24
+ import { importModule } from "./import-module.js";
32
25
  import { pathToFileURL } from 'node:url';
33
26
  async function loadFactory(spec) {
34
- const mod = (await import(__rewriteRelativeImportExtension(spec.module)));
27
+ const mod = await importModule(spec.module);
35
28
  const factory = mod[spec.export];
36
29
  if (typeof factory !== 'function') {
37
30
  throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
@@ -0,0 +1 @@
1
+ export declare function importModule<T = Record<string, unknown>>(specifier: string): Promise<T>;
@@ -0,0 +1,16 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // import() of what a World names a module by: a package name, a file URL, or a file path. Node's ESM loader takes an
10
+ // absolute path only as a file:// URL — on Windows `C:\…` reads as a URL with the scheme `c:` and is refused
11
+ // (ERR_UNSUPPORTED_ESM_URL_SCHEME); Bun takes either. A path becomes its file URL here; anything else is passed on.
12
+ import { isAbsolute } from 'node:path';
13
+ import { pathToFileURL } from 'node:url';
14
+ export function importModule(specifier) {
15
+ return import(__rewriteRelativeImportExtension(isAbsolute(specifier) ? pathToFileURL(specifier).href : specifier));
16
+ }
@@ -41,8 +41,13 @@ export { branchWorld, checkoutWorld } from './branch.js';
41
41
  export type { BranchOptions } from './branch.js';
42
42
  export { deployWorld, refreshTwin, adaptersFor, credentialPath, credentialPayloadFrom, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from './root.js';
43
43
  export type { DeployTwinOutcome, MaterializedRoot } from './root.js';
44
- export { landReceipts, mountWorld, readServeRecord, refreshSchedule, servedName, serveWorld, TOKEN_HEADER, WorldDoors } from './served-world.js';
45
- export type { DoorHost, MountedWorld, ServeRecord, WorldLayout } from './served-world.js';
44
+ export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServeRecord, refreshSchedule, servedName, serveWorld, sessionsFile, keysFile, TOKEN_HEADER, WorldDoors } from './served-world.js';
45
+ export type { BranchDoors, BranchRow, DoorHost, MountedWorld, ServeRecord, WorldLayout } from './served-world.js';
46
+ export { LocalBranches } from './local-branches.js';
47
+ export { serveWorldView, VIEW_CONSOLE_BASE } from './world-view.js';
48
+ export { LOCAL_ORIGIN_BASE, localWorldOrigin, pathOfWorld, worldDoorUnderConsole, worldOfHost } from './world-origins.js';
49
+ export type { ViewConsole, WorldView } from './world-view.js';
50
+ export type { LocalBranchesOptions } from './local-branches.js';
46
51
  export type { ServedWorld } from './served-world.js';
47
52
  export { CONSOLE_BASE, consoleRedirect, serveConsoleApart, serveConsoleFor, type ConsoleMount } from './console-apart.js';
48
53
  export { assertWorldStateRemovable } from './ancestry.js';
package/dist/src/index.js CHANGED
@@ -30,6 +30,9 @@ export { resetWorld, seedEntryFor, seedWorld } from "./placeholder.js";
30
30
  export { fetchFromOrigin, worldOrigin } from "./origin.js";
31
31
  export { branchWorld, checkoutWorld } from "./branch.js";
32
32
  export { deployWorld, refreshTwin, adaptersFor, credentialPath, credentialPayloadFrom, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from "./root.js";
33
- export { landReceipts, mountWorld, readServeRecord, refreshSchedule, servedName, serveWorld, TOKEN_HEADER, WorldDoors } from "./served-world.js";
33
+ export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServeRecord, refreshSchedule, servedName, serveWorld, sessionsFile, keysFile, TOKEN_HEADER, WorldDoors } from "./served-world.js";
34
+ export { LocalBranches } from "./local-branches.js";
35
+ export { serveWorldView, VIEW_CONSOLE_BASE } from "./world-view.js";
36
+ export { LOCAL_ORIGIN_BASE, localWorldOrigin, pathOfWorld, worldDoorUnderConsole, worldOfHost } from "./world-origins.js";
34
37
  export { CONSOLE_BASE, consoleRedirect, serveConsoleApart, serveConsoleFor } from "./console-apart.js";
35
38
  export { assertWorldStateRemovable } from "./ancestry.js";
@@ -23,7 +23,7 @@ if (!existsSync(definition)) {
23
23
  process.exit(2);
24
24
  }
25
25
  const base = ['compose', '-f', definition];
26
- const run = (args) => spawnSync('docker', [...base, ...args], {
26
+ const run = (args) => spawnSync('docker', [...base, ...args], { windowsHide: true,
27
27
  encoding: 'utf8',
28
28
  env: { ...process.env, VOLTER_WORLD_DATA: worldData },
29
29
  timeout: 120_000,
@@ -57,7 +57,7 @@ function selectBacking() {
57
57
  process.stderr.write(`managed infrastructure: VOLTER_WORLD_INFRA_BACKING must be docker or pglite (got ${JSON.stringify(forced)})\n`);
58
58
  process.exit(2);
59
59
  }
60
- const probe = spawnSync('docker', ['info', '--format', '{{.ServerVersion}}'], { encoding: 'utf8', timeout: 10_000 });
60
+ const probe = spawnSync('docker', ['info', '--format', '{{.ServerVersion}}'], { windowsHide: true, encoding: 'utf8', timeout: 10_000 });
61
61
  if (probe.status === 0)
62
62
  return 'docker';
63
63
  // Absence of a container runtime (no binary, no daemon) is a capability
@@ -214,7 +214,7 @@ export declare function renderEnvFile(plan: InitPlan): string;
214
214
  /** Write the plan's two artifacts. Byte-identical for the same repo + catalog: sorted keys, no
215
215
  * timestamps, no absolute paths inside the files. */
216
216
  /** What `.volter/.gitignore` says: the story is committed, the running state never is. */
217
- export declare const STATE_GITIGNORE = "# Volter: the world's running state and live env \u2014 never committed. world.json, handlers/ and seeds/ are the story.\nworlds/\ncurrent\n*.env\ncredentials/\ntoken\n";
217
+ export declare const STATE_GITIGNORE = "# Volter: the world's running state and live env \u2014 never committed. world.json, handlers/ and seeds/ are the story.\nworlds/\ncurrent\n*.env\ncredentials/\ntoken\ntoken.read\nsessions.json\nkeys.json\n";
218
218
  export declare function writeWorldInit(plan: InitPlan, options?: {
219
219
  force?: boolean;
220
220
  }): void;
package/dist/src/init.js CHANGED
@@ -963,6 +963,9 @@ current
963
963
  *.env
964
964
  credentials/
965
965
  token
966
+ token.read
967
+ sessions.json
968
+ keys.json
966
969
  `;
967
970
  export function writeWorldInit(plan, options = {}) {
968
971
  // A world already here is refused unless forced; a `.volter/` holding only running state
@@ -0,0 +1,37 @@
1
+ import { type BranchDoors, type BranchRow, type MountedWorld } from './served-world.js';
2
+ export type LocalBranchesOptions = {
3
+ /** where branches live: `<dir>/<org>/<world>/` */
4
+ dir: string;
5
+ /** the URL a branch reaches its parent at (the host's own listener, on loopback) */
6
+ origin: () => string;
7
+ /** the Worlds the host serves, by served name; branches join it while they live */
8
+ worlds: Map<string, MountedWorld>;
9
+ /** mount a World at a root, with its branches doors wired */
10
+ mount: (root: string) => Promise<MountedWorld>;
11
+ announce?: (line: string) => void;
12
+ };
13
+ export declare class LocalBranches {
14
+ private readonly o;
15
+ private sweeper;
16
+ constructor(o: LocalBranchesOptions);
17
+ private record;
18
+ /** The branches of `parent` this host holds. */
19
+ list(parent: string): BranchRow[];
20
+ /** Branches being made, by parent: a parent is not removed mid-clone. */
21
+ private readonly making;
22
+ hasBranches(parent: string): boolean;
23
+ /** A World's branches doors, answered by this host. */
24
+ doorsFor(parent: string): BranchDoors;
25
+ private create;
26
+ private make;
27
+ /** The parent's frozen clock, when it has one. */
28
+ private frozenClock;
29
+ /** Stop a branch and remove its World (a branch's own branches first). */
30
+ remove(name: string): Promise<void>;
31
+ /** Remove the branches whose time ran out, now and every minute while the host serves. */
32
+ resume(): Promise<void>;
33
+ /** One sweep at a time: a slow one never overlaps the next. */
34
+ private sweeping;
35
+ private sweep;
36
+ stop(): void;
37
+ }
@@ -0,0 +1,193 @@
1
+ // LOCAL BRANCHES: the branches a local host makes of the Worlds it serves (docs/contributing/architecture.md,
2
+ // "Viewing a World"). A branch is another World: the parent's config under a new name, cloned from the
3
+ // parent through its own doors (its history cut at an instant, when one is asked), its clock frozen at
4
+ // that instant so no twin's catch-up walks it forward, and removed when its time runs out. world-host
5
+ // and `volter world view` both make them with this; a hosted World's supervisor makes its own.
6
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { stateDirName, withStateRemoval } from '@volter/world-core';
9
+ import { clockFile } from "./runtime.js";
10
+ import { loadWorldConfig } from "./configs.js";
11
+ import { TOKEN_HEADER } from "./served-world.js";
12
+ const RECORD = 'branch.json';
13
+ const SWEEP_MS = 60_000;
14
+ /** A World's manifest under another name, in the manifest's own format: format 2 names a World in
15
+ * `metadata.id` and serves it bare at `serving.name`; format 1 in `id` and `bare.name`. */
16
+ function renamed(config, id, name) {
17
+ if (config.schemaVersion === 2) {
18
+ return { ...config, metadata: { ...config.metadata, id }, serving: { ...config.serving, mode: 'bare', name } };
19
+ }
20
+ return { ...config, id, bare: { ...config.bare, name } };
21
+ }
22
+ export class LocalBranches {
23
+ o;
24
+ sweeper = null;
25
+ constructor(o) {
26
+ this.o = o;
27
+ }
28
+ record(world) {
29
+ try {
30
+ return JSON.parse(readFileSync(join(world.root, stateDirName(), RECORD), 'utf8'));
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /** The branches of `parent` this host holds. */
37
+ list(parent) {
38
+ const rows = [];
39
+ for (const [name, world] of this.o.worlds) {
40
+ const r = this.record(world);
41
+ if (r?.from === parent)
42
+ rows.push({ name, from: r.from, at: r.at, createdAt: r.createdAt, expiresAt: r.expiresAt });
43
+ }
44
+ return rows.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
45
+ }
46
+ /** Branches being made, by parent: a parent is not removed mid-clone. */
47
+ making = new Map();
48
+ hasBranches(parent) { return (this.making.get(parent) ?? 0) > 0 || this.list(parent).length > 0; }
49
+ /** A World's branches doors, answered by this host. */
50
+ doorsFor(parent) {
51
+ return {
52
+ list: async () => this.list(parent),
53
+ create: (at, ttl, _origin, parentKey) => this.create(parent, at, ttl, parentKey),
54
+ remove: async (name) => { const w = this.o.worlds.get(name); if (!w || this.record(w)?.from !== parent)
55
+ return false; await this.remove(name); return true; },
56
+ };
57
+ }
58
+ async create(parent, at, ttlSeconds, parentKey) {
59
+ this.making.set(parent, (this.making.get(parent) ?? 0) + 1);
60
+ try {
61
+ return await this.make(parent, at, ttlSeconds, parentKey);
62
+ }
63
+ finally {
64
+ const left = (this.making.get(parent) ?? 1) - 1;
65
+ if (left > 0)
66
+ this.making.set(parent, left);
67
+ else
68
+ this.making.delete(parent);
69
+ }
70
+ }
71
+ async make(parent, at, ttlSeconds, parentKey) {
72
+ const from = this.o.worlds.get(parent);
73
+ if (!from)
74
+ throw new Error(`no world ${parent} here`);
75
+ const [org, world] = parent.split('/');
76
+ const stamp = at.instant ? at.instant.replace(/[-:]/g, '').replace(/\.\d+Z$|Z$/, '').toLowerCase().slice(0, 13) : 'now';
77
+ // a labelled branch (a pull request's preview) is named for its label; the random tail keeps a replacement distinct
78
+ // a name already taken (a 4-hex tail's rare clash) is passed over for a fresh tail, never made over
79
+ const free = () => {
80
+ for (let tries = 0; tries < 8; tries++) {
81
+ const suffix = `${at.label ? `-${at.label}` : `-at-${stamp}`}-${Buffer.from(crypto.getRandomValues(new Uint8Array(2))).toString('hex')}`;
82
+ const id = `${world.slice(0, 64 - suffix.length)}${suffix}`;
83
+ const name = `${org}/${id}`;
84
+ const root = join(this.o.dir, org, id);
85
+ if (!this.o.worlds.has(name) && !existsSync(root))
86
+ return { id, name, root };
87
+ }
88
+ return null;
89
+ };
90
+ const picked = free();
91
+ if (!picked)
92
+ throw new Error(`no free name for a branch of ${parent}`);
93
+ const { id, name, root } = picked;
94
+ // the parent's config under the branch's own name: the same twins, served at the branch's place
95
+ mkdirSync(join(root, stateDirName()), { recursive: true });
96
+ writeFileSync(join(root, stateDirName(), 'world.json'), `${JSON.stringify(renamed(JSON.parse(readFileSync(join(from.root, stateDirName(), 'world.json'), 'utf8')), id, name), null, 2)}\n`);
97
+ let mounted;
98
+ try {
99
+ mounted = await this.o.mount(root);
100
+ await mounted.boot(this.o.origin());
101
+ this.o.worlds.set(name, mounted);
102
+ const origin = this.o.origin();
103
+ // each twin's history cut at the instant, by the parent's own history door
104
+ let views;
105
+ if (at.instant) {
106
+ const cut = await from.handle(new Request(`${origin}/-/${parent}/history?at=${encodeURIComponent(at.instant)}`, { headers: { [TOKEN_HEADER]: from.token } }));
107
+ if (!cut.ok)
108
+ throw new Error(`the history of ${parent} at ${at.instant}: ${await cut.text()}`);
109
+ views = (await cut.json()).views;
110
+ }
111
+ const cloned = await mounted.handle(new Request(`${origin}/-/${name}/origin`, { method: 'PUT', headers: { [TOKEN_HEADER]: mounted.token, 'content-type': 'application/json' }, body: JSON.stringify({ url: `${origin}/${parent}`, token: parentKey ?? from.token, ...(views ? { views } : {}) }) })); // the branch's own key to its parent (the parent's doors make it), never the parent's token
112
+ if (!cloned.ok)
113
+ throw new Error(`cloning ${parent}: ${await cloned.text()}`);
114
+ // the branch's clock stands at its instant, else at the parent's: no twin's catch-up walks it on
115
+ // (a parent whose clock was never set: the branch's stands at now, frozen there, as http-api says);
116
+ // a live branch (a pull request's preview) keeps the parent's time: real time, or its simulated clock
117
+ const clock = at.instant ?? this.frozenClock(from) ?? (at.live ? null : new Date().toISOString());
118
+ if (clock) {
119
+ const file = clockFile(root, id);
120
+ mkdirSync(dirname(file), { recursive: true });
121
+ writeFileSync(file, `${clock}\n`);
122
+ }
123
+ const createdAt = new Date().toISOString();
124
+ const expiresAt = ttlSeconds === null ? null : new Date(Date.now() + ttlSeconds * 1000).toISOString();
125
+ writeFileSync(join(root, stateDirName(), RECORD), `${JSON.stringify({ from: parent, at: at.instant ? { instant: at.instant } : null, createdAt, expiresAt }, null, 2)}\n`);
126
+ this.o.announce?.(`branch ${name} of ${parent}${at.instant ? ` as of ${at.instant}` : ''}${expiresAt ? `, until ${expiresAt}` : ''}`);
127
+ return { name, token: mounted.token, readToken: mounted.readToken, expiresAt };
128
+ }
129
+ catch (error) {
130
+ this.o.worlds.delete(name);
131
+ try {
132
+ await mounted?.stop();
133
+ }
134
+ catch { /* reported below */ }
135
+ withStateRemoval(root, () => rmSync(root, { recursive: true, force: true }));
136
+ throw error;
137
+ }
138
+ }
139
+ /** The parent's frozen clock, when it has one. */
140
+ frozenClock(world) {
141
+ try {
142
+ const held = readFileSync(clockFile(world.root, loadWorldConfig(join(world.root, stateDirName(), 'world.json'), world.root).config.id), 'utf8').trim();
143
+ return Number.isNaN(Date.parse(held)) ? null : held;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ /** Stop a branch and remove its World (a branch's own branches first). */
150
+ async remove(name) {
151
+ const world = this.o.worlds.get(name);
152
+ if (!world)
153
+ return;
154
+ for (const child of this.list(name))
155
+ await this.remove(child.name);
156
+ await world.stop();
157
+ withStateRemoval(world.root, () => rmSync(world.root, { recursive: true, force: true }));
158
+ this.o.worlds.delete(name);
159
+ this.o.announce?.(`removed ${name}`);
160
+ }
161
+ /** Remove the branches whose time ran out, now and every minute while the host serves. */
162
+ async resume() {
163
+ await this.sweep();
164
+ this.sweeper = setInterval(() => { void this.sweep(); }, SWEEP_MS);
165
+ this.sweeper.unref?.();
166
+ }
167
+ /** One sweep at a time: a slow one never overlaps the next. */
168
+ sweeping = false;
169
+ async sweep() {
170
+ if (this.sweeping)
171
+ return;
172
+ this.sweeping = true;
173
+ try {
174
+ const now = Date.now();
175
+ for (const [name, world] of [...this.o.worlds]) {
176
+ const r = this.record(world);
177
+ if (r?.expiresAt && Date.parse(r.expiresAt) <= now && this.o.worlds.has(name)) {
178
+ try {
179
+ await this.remove(name);
180
+ }
181
+ catch (error) {
182
+ this.o.announce?.(`branch ${name}: removal failed: ${error instanceof Error ? error.message : String(error)}`);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ finally {
188
+ this.sweeping = false;
189
+ }
190
+ }
191
+ stop() { if (this.sweeper)
192
+ clearInterval(this.sweeper); this.sweeper = null; }
193
+ }
@@ -68,8 +68,10 @@ export function infraConnections(services, env) {
68
68
  }
69
69
  // this module's directory under Node as under Bun: `volter-world-infra` is a node-shebang bin, where
70
70
  // `import.meta.dir` (Bun's) is undefined
71
- const HERE = fileURLToPath(new URL('.', import.meta.url));
72
- const HOST = join(HERE, 'pglite-host.mjs');
71
+ // lazy: a module that builds a URL from import.meta.url when it loads fails to load in workerd, which bundles this
72
+ // module into the hosted World (it never runs these services there)
73
+ const here = () => fileURLToPath(new URL('.', import.meta.url));
74
+ const hostScript = () => join(here(), 'pglite-host.mjs');
73
75
  const READY_TIMEOUT_MS = 60_000;
74
76
  const MONGODB_TWIN = '@volter/twin-mongodb';
75
77
  /** The kinds this backing serves, and the name its files carry under the data dir. */
@@ -79,7 +81,7 @@ function pidPath(dataDir, kind) {
79
81
  }
80
82
  /** The MongoDB twin's cli: the package installed above the World's config (an app repo), else the one
81
83
  * this runtime was installed with (its dependency, or the checkout's workspace link). */
82
- export function mongodbTwinCli(worldConfig, froms = [...(worldConfig ? [dirname(worldConfig)] : []), HERE]) {
84
+ export function mongodbTwinCli(worldConfig, froms = [...(worldConfig ? [dirname(worldConfig)] : []), here()]) {
83
85
  for (const from of froms) {
84
86
  const dir = findInstalledPackage(from, MONGODB_TWIN);
85
87
  const cli = dir === undefined ? undefined : packCli(dir);
@@ -92,7 +94,7 @@ export function mongodbTwinCli(worldConfig, froms = [...(worldConfig ? [dirname(
92
94
  * process's own runtime; a checkout's is TypeScript whose kernel needs more than Node's type
93
95
  * stripping, so it runs under Bun (this process's, or the one on PATH) — the repository's toolchain —
94
96
  * and without Bun it is refused by name rather than started to fail. */
95
- export function twinRunner(cli, bunOnPath = () => spawnSync('bun', ['--version'], { stdio: 'ignore', timeout: 10_000 }).status === 0, underBun = typeof globalThis.Bun !== 'undefined') {
97
+ export function twinRunner(cli, bunOnPath = () => spawnSync('bun', ['--version'], { stdio: 'ignore', timeout: 10_000, windowsHide: true }).status === 0, underBun = typeof globalThis.Bun !== 'undefined') {
96
98
  if (!cli.endsWith('.ts') || underBun)
97
99
  return process.execPath;
98
100
  if (bunOnPath())
@@ -105,7 +107,7 @@ function hostArgv(service, serviceData) {
105
107
  const cli = mongodbTwinCli(process.env.VOLTER_WORLD_CONFIG);
106
108
  return [twinRunner(cli), cli, 'serve', '--port', String(service.hostPort), '--root', serviceData];
107
109
  }
108
- return ['node', HOST, '--port', String(service.hostPort), '--data', serviceData];
110
+ return ['node', hostScript(), '--port', String(service.hostPort), '--data', serviceData];
109
111
  }
110
112
  function alive(pid) {
111
113
  try {
@@ -169,6 +171,7 @@ export async function pgliteUp(services, dataDir) {
169
171
  // a real fd, not a pipe: the host must outlive this process untethered
170
172
  const logFd = openSync(log, 'a');
171
173
  const child = spawn(command, args, {
174
+ windowsHide: true,
172
175
  detached: true,
173
176
  stdio: ['ignore', logFd, logFd],
174
177
  });
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  const realRunner = (cmd, args) => {
3
- const result = spawnSync(cmd, args, { encoding: 'utf8' });
3
+ const result = spawnSync(cmd, args, { windowsHide: true, encoding: 'utf8' });
4
4
  return {
5
5
  status: result.status,
6
6
  stdout: result.stdout || '',
@@ -22,7 +22,7 @@ export function survivingOwnedGroups(pids) {
22
22
  });
23
23
  if (!candidates.length || process.platform === 'win32')
24
24
  return candidates;
25
- const result = spawnSync('ps', ['-axo', 'pid=,pgid=,stat='], {
25
+ const result = spawnSync('ps', ['-axo', 'pid=,pgid=,stat='], { windowsHide: true,
26
26
  encoding: 'utf8', timeout: 1000, killSignal: 'SIGKILL', maxBuffer: 4 * 1024 * 1024,
27
27
  });
28
28
  if (result.status !== 0 || result.error)
@@ -199,11 +199,11 @@ export function opensslAvailable() {
199
199
  // this env var in real usage.
200
200
  if (process.env.VOLTER_TEST_NO_OPENSSL === '1')
201
201
  return false;
202
- const r = spawnSync('openssl', ['version'], { stdio: 'ignore' });
202
+ const r = spawnSync('openssl', ['version'], { windowsHide: true, stdio: 'ignore' });
203
203
  return r.status === 0;
204
204
  }
205
205
  function runOpenssl(args) {
206
- const r = spawnSync('openssl', args, { encoding: 'utf8' });
206
+ const r = spawnSync('openssl', args, { windowsHide: true, encoding: 'utf8' });
207
207
  if (r.status !== 0)
208
208
  throw new Error(`openssl ${args[0]} failed: ${(r.stderr || r.stdout || '').trim()}`);
209
209
  return r.stdout;
@@ -36,6 +36,29 @@ export declare function rootForControlRoot(controlRoot: string, service: string)
36
36
  /** The pack's mirror module (`src/<vendor>-mirror-ui.ts`: its shell, client and stylesheet), read as the
37
37
  * hosted mirror build reads it (apps/cloud/scripts/mirrors.ts); a pack's index need not re-export them. */
38
38
  export declare function packMirrorExports(vendor: string, from: string): Promise<Record<string, unknown> | null>;
39
+ /** A pack's mirror as the pack builds it (`<vendor>MirrorHtml`, `build<Vendor>MirrorClient`,
40
+ * `<vendor>MirrorStyles`), found the way a World finds the pack; null when the pack has none. The
41
+ * client builds at serve under Bun and is served prebuilt under Node, by the pack's own builder. */
42
+ export type PackMirror = {
43
+ html: () => string;
44
+ client: () => Promise<string>;
45
+ styles?: () => Promise<string>; /** the account and a credential its screens sign in with (`<vendor>MirrorSignIn`): the account found in the twin's
46
+ * state at `root`, the credential minted for it through the twin's own doors (`twin`), never a stored one read back */
47
+ signIn?: (as: string, ctx: MirrorSignInContext) => Promise<{
48
+ account: string;
49
+ token: string;
50
+ } | null> | {
51
+ account: string;
52
+ token: string;
53
+ } | null;
54
+ };
55
+ /** What a pack's `<vendor>MirrorSignIn` is given: the twin's state root (to find the account) and the twin's own doors
56
+ * (to mint a credential for it, as the World's seed does through `/_twin/…`). */
57
+ export type MirrorSignInContext = {
58
+ root: string;
59
+ twin: (path: string, init?: RequestInit) => Promise<Response>;
60
+ };
61
+ export declare function packMirror(vendor: string, from: string): Promise<PackMirror | null>;
39
62
  /** The real state system's adapters for a vendor: registered with the pack (protocol 2), else the
40
63
  * pack's anchored exports (protocol 1: `perform<Name>Action`, `sync<Name>FromRemote`). */
41
64
  export type IngestAdapter = (request: Request, ctx: {