@volter/world-runtime 2.0.0 → 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 (78) hide show
  1. package/dist/known-external-services.json +0 -8
  2. package/dist/src/app-url.js +1 -1
  3. package/dist/src/branch.js +79 -2
  4. package/dist/src/catalog.js +1 -1
  5. package/dist/src/cli.js +50 -22
  6. package/dist/src/console-apart.d.ts +1 -0
  7. package/dist/src/console-apart.js +7 -0
  8. package/dist/src/covers.js +4 -2
  9. package/dist/src/fixture-env.d.ts +3 -0
  10. package/dist/src/fixture-env.js +24 -0
  11. package/dist/src/host-worker.js +2 -9
  12. package/dist/src/host.js +2 -9
  13. package/dist/src/import-module.d.ts +1 -0
  14. package/dist/src/import-module.js +16 -0
  15. package/dist/src/index.d.ts +7 -2
  16. package/dist/src/index.js +4 -1
  17. package/dist/src/infra-cli.js +61 -12
  18. package/dist/src/init.d.ts +1 -1
  19. package/dist/src/init.js +34 -5
  20. package/dist/src/local-branches.d.ts +37 -0
  21. package/dist/src/local-branches.js +193 -0
  22. package/dist/src/pglite-backing.d.ts +8 -0
  23. package/dist/src/pglite-backing.js +121 -35
  24. package/dist/src/pglite-host.mjs +520 -14
  25. package/dist/src/prerequisites.js +1 -1
  26. package/dist/src/process-groups.js +1 -1
  27. package/dist/src/redirect-proxy.d.ts +1 -1
  28. package/dist/src/redirect-proxy.js +4 -4
  29. package/dist/src/redis-backing.d.ts +8 -0
  30. package/dist/src/redis-backing.js +120 -0
  31. package/dist/src/root.d.ts +23 -0
  32. package/dist/src/root.js +22 -10
  33. package/dist/src/run-task.js +1 -1
  34. package/dist/src/runtime.d.ts +1 -0
  35. package/dist/src/runtime.js +37 -20
  36. package/dist/src/schema.d.ts +5 -0
  37. package/dist/src/schema.js +10 -1
  38. package/dist/src/served-world.d.ts +196 -9
  39. package/dist/src/served-world.js +847 -103
  40. package/dist/src/service-recorder.js +1 -1
  41. package/dist/src/storage-capacity.js +2 -2
  42. package/dist/src/up-task.js +1 -1
  43. package/dist/src/world-origins.d.ts +13 -0
  44. package/dist/src/world-origins.js +37 -0
  45. package/dist/src/world-view.d.ts +25 -0
  46. package/dist/src/world-view.js +110 -0
  47. package/known-external-services.json +0 -8
  48. package/package.json +10 -4
  49. package/src/app-url.ts +1 -1
  50. package/src/branch.ts +66 -2
  51. package/src/catalog.ts +1 -1
  52. package/src/cli.ts +43 -21
  53. package/src/console-apart.ts +7 -1
  54. package/src/covers.ts +4 -2
  55. package/src/fixture-env.ts +25 -0
  56. package/src/host-worker.ts +2 -1
  57. package/src/host.ts +2 -1
  58. package/src/import-module.ts +9 -0
  59. package/src/index.ts +7 -2
  60. package/src/infra-cli.ts +56 -12
  61. package/src/init.ts +34 -5
  62. package/src/local-branches.ts +173 -0
  63. package/src/pglite-backing.ts +112 -36
  64. package/src/pglite-host.mjs +520 -14
  65. package/src/prerequisites.ts +1 -1
  66. package/src/process-groups.ts +1 -1
  67. package/src/redirect-proxy.ts +4 -4
  68. package/src/redis-backing.ts +107 -0
  69. package/src/root.ts +27 -2
  70. package/src/run-task.ts +1 -1
  71. package/src/runtime.ts +38 -20
  72. package/src/schema.ts +11 -1
  73. package/src/served-world.ts +762 -85
  74. package/src/service-recorder.ts +1 -1
  75. package/src/storage-capacity.ts +2 -2
  76. package/src/up-task.ts +1 -1
  77. package/src/world-origins.ts +38 -0
  78. package/src/world-view.ts +100 -0
@@ -373,10 +373,6 @@
373
373
  "disposition": "demanded",
374
374
  "reason": "vendor google-play (Play Developer API), ×1 — ladder classification 2026-09-02"
375
375
  },
376
- "@googleapis/calendar": {
377
- "disposition": "demanded",
378
- "reason": "vendor google-calendar, ×1 (plus the GOOGLE_CALENDAR_API_KEY stem, ×1) — ladder classification 2026-09-02"
379
- },
380
376
  "@googleapis/sheets": {
381
377
  "disposition": "demanded",
382
378
  "reason": "vendor google-sheets, ×1 — ladder classification 2026-09-02"
@@ -863,10 +859,6 @@
863
859
  "disposition": "demanded",
864
860
  "reason": "vendor ghost (VITE_PUBLIC_GHOST_CONTENT_API_KEY — the browser-side Content API key), ×1 — ladder classification 2026-09-02"
865
861
  },
866
- "googlecalendar": {
867
- "disposition": "demanded",
868
- "reason": "vendor google-calendar (GOOGLE_CALENDAR_API_KEY; the @googleapis/calendar client is the same demand), ×1 — ladder classification 2026-09-02"
869
- },
870
862
  "googlecloud": {
871
863
  "disposition": "demanded",
872
864
  "reason": "vendor googlecloud (GOOGLE_CLOUD_API_KEY; the roster's one consumer spends it on the Web Fonts Developer API at www.googleapis.com/webfonts/v1, so the stem is a Google Cloud API key rather than a googleoauth credential), ×1 — ladder classification 2026-09-02"
@@ -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
  }
@@ -2,12 +2,17 @@
2
2
  // log of changes, and a world is a branch with compute attached (docs/concepts/the-model.md).
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
- // history, its own entries included, is the branch's parent; nothing is copied). `checkout <name>`
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
7
+ // base running at once share the database port its definition names; run one at a time (the SDK does). `checkout <name>`
6
8
  // brings a stopped world back up from its own instance record, state intact.
7
- import { existsSync, readdirSync } from 'node:fs';
9
+ import { cpSync, existsSync, readdirSync, readFileSync, rmSync } from 'node:fs';
8
10
  import { dirname, join, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
9
12
  import { captureHistory, historyAtInstant, forkTwin, stateGeneration, withAncestryLock, worldPaths, worldStateRoot } from '@volter/world-core';
10
13
  import { downWorld, saveWorldInstance, statusWorld, upWorld } from "./runtime.js";
14
+ import { parseInfraDefinition, pgliteDown, pgliteUp } from "./pglite-backing.js";
15
+ import { redisTwinCli, redisTwinDown, redisTwinUp } from "./redis-backing.js";
11
16
  function stateServices(controlRoot) {
12
17
  const stateRoot = worldStateRoot(controlRoot);
13
18
  if (!existsSync(stateRoot))
@@ -35,6 +40,12 @@ export async function branchWorld(base, name, options = {}) {
35
40
  return { state, view, position, generation: stateGeneration(worldPaths(state, fromRoot).dir) };
36
41
  }) };
37
42
  }));
43
+ // the base's managed databases (checked before the branch boots: its database would share the base's port)
44
+ const databases = managedDatabases(from);
45
+ if (databases.length && (options.at?.instant || options.at?.positions || options.at?.views)) {
46
+ 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`);
47
+ }
48
+ refuseLiveDatabases(from, databases);
38
49
  const instance = await upWorld(existsSync(from.configPath) ? from.configPath : from.config, { name, root, mode: from.mode, envFile });
39
50
  const forked = {};
40
51
  try {
@@ -50,6 +61,7 @@ export async function branchWorld(base, name, options = {}) {
50
61
  if (states.length)
51
62
  forked[service] = states.map(s => s.state);
52
63
  }
64
+ await branchManagedDatabases(from, instance, databases);
53
65
  }
54
66
  catch (error) {
55
67
  await downWorld(name, root, { purge: true, expectedCreatedAt: instance.createdAt });
@@ -57,6 +69,71 @@ export async function branchWorld(base, name, options = {}) {
57
69
  }
58
70
  return { instance, forked };
59
71
  }
72
+ /** The base's containerless managed databases: each PGlite `pglite-<kind>-data` and the MongoDB twin's
73
+ * `twin-mongodb-data` (pglite-backing.ts), and the redis twin's `redis-twin` tree (redis-backing.ts). A stopped World's
74
+ * hosts leave no pid file; a docker-backed World keeps its data under the container's volume, which a branch does not
75
+ * carry. */
76
+ function managedDatabases(from) {
77
+ const data = from.dirs.data;
78
+ if (!existsSync(data))
79
+ return [];
80
+ return readdirSync(data).filter((n) => /^pglite-[a-z0-9_-]+-data$/.test(n) || n === 'twin-mongodb-data' || n === 'redis-twin');
81
+ }
82
+ /** The pid files of the hosts serving one managed database directory. */
83
+ function hostPidFiles(data, db) {
84
+ if (db === 'redis-twin')
85
+ return readdirSync(data).filter((n) => /^redis-twin-\d+\.pid$/.test(n)).map((n) => join(data, n));
86
+ return [join(data, `${db.slice(0, -'-data'.length)}.pid`)];
87
+ }
88
+ /** A copy of a live database directory is not a consistent one, and the branch's database would answer on the base's
89
+ * port: the base must be stopped first (`volter world branch` stops it, as a branch replaces its base). */
90
+ function refuseLiveDatabases(from, dbs) {
91
+ for (const db of dbs) {
92
+ for (const pidFile of hostPidFiles(from.dirs.data, db)) {
93
+ const pid = existsSync(pidFile) ? Number(readFileSync(pidFile, 'utf8').trim()) : NaN;
94
+ let live = false;
95
+ if (Number.isInteger(pid) && pid > 0) {
96
+ try {
97
+ process.kill(pid, 0);
98
+ live = true;
99
+ }
100
+ catch {
101
+ live = false;
102
+ }
103
+ }
104
+ if (live)
105
+ throw new Error(`volter-world branch: ${from.name}'s managed ${db} is running (pid ${pid}); stop ${from.name} first so its database can be copied into the branch consistently`);
106
+ }
107
+ }
108
+ }
109
+ /** The branch's managed databases as the base holds them: the branch's own, just booted empty by `up`, are stopped,
110
+ * replaced by a copy of the base's data, and started again, so a branch of a migrated and seeded World serves that
111
+ * data, as its twins serve the base's history. */
112
+ async function branchManagedDatabases(from, instance, dbs) {
113
+ if (!dbs.length)
114
+ return;
115
+ const definition = join(dirname(instance.configPath), 'world.infrastructure.yml');
116
+ if (!existsSync(definition))
117
+ return;
118
+ const services = parseInfraDefinition(readFileSync(definition, 'utf8'));
119
+ const redis = services.filter((s) => s.kind === 'redis');
120
+ const hosted = services.filter((s) => s.kind !== 'redis');
121
+ const redisCli = dbs.includes('redis-twin') && redis.length ? redisTwinCli([dirname(fileURLToPath(import.meta.url)), dirname(instance.configPath)]) : undefined;
122
+ await pgliteDown(hosted, instance.dirs.data);
123
+ if (redisCli)
124
+ await redisTwinDown(redis, instance.dirs.data);
125
+ for (const db of dbs) {
126
+ if (db === 'redis-twin' && !redisCli)
127
+ continue;
128
+ const to = join(instance.dirs.data, db);
129
+ rmSync(to, { recursive: true, force: true });
130
+ cpSync(join(from.dirs.data, db), to, { recursive: true });
131
+ }
132
+ if (dbs.some((db) => db !== 'redis-twin'))
133
+ await pgliteUp(hosted, instance.dirs.data);
134
+ if (redisCli)
135
+ await redisTwinUp(redis, instance.dirs.data, redisCli);
136
+ }
60
137
  /** A stopped world back up, from its own record. */
61
138
  export async function checkoutWorld(name, options = {}) {
62
139
  const root = resolve(options.root ?? process.cwd());
@@ -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
@@ -2,7 +2,7 @@
2
2
  import { sessionTrustEnv } from "./ca-trust.js";
3
3
  import { spawn, spawnSync } from 'node:child_process';
4
4
  import { createRequire } from 'node:module';
5
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
5
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { dirname, join, resolve } from 'node:path';
8
8
  import { activateScript, branchWorld, checkoutWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, resetWorld, seedWorld, worldOrigin, approveWorldChangeset, checkPrerequisites, coverWorld, createWorldChangeset, diffWorld, doctorWorld, downWorld, findWorldChangeset, formatCoverageReport, formatInitReport, formatPrerequisiteChecks, formatProjectInspection, initWorld, inspectProject, isRemoteWorldRef, listWorldChangesets, listWorlds, markWorld, migrateWorldConfig, readReflectRoutes, reflectRoutesPath, replayWorldChangeset, resolveWorldRef, attachWorld, retireWorldConsumers, runWorld, shareWorldServices, shellWorld, startReflectFront, startReflectResolver, statusWorld, statusWorldChangeset, tailWorldActions, unshareWorld, urlsWorld, urlWorld, verifyWorldChangeset, worldManifest, writeReflectRoutes, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, ATTACHED_CA_PATH } from "./index.js";
@@ -568,21 +568,44 @@ async function main() {
568
568
  return;
569
569
  }
570
570
  if (cmd === 'clock') {
571
- // THE operator door for world time (physics): show / set <iso> / advance <duration>.
572
- // The clock is a frozen instant every twin reads per request (kernel worldNow()); setting
573
- // or advancing takes effect live, no restarts. `advance` requires a set clock (advancing
574
- // wall-clock would silently freeze time as a side effect).
571
+ // THE operator door for world time (physics): show / set <iso> / advance <duration> / shift <duration> / clear.
572
+ // The clock every twin reads per request (kernel worldNow()), and every application process through the injector
573
+ // (inject.cjs, WORLD TIME); a change takes effect live, no restarts. Its two forms (world-clock.cjs): `set` freezes
574
+ // time at an instant (scripted time: a life, a seed); `shift` moves a World forward while its time keeps running (a
575
+ // World serving an application, whose time a frozen instant would stop); `advance` moves either form; `clear`
576
+ // returns the World to the machine's time. `advance` refuses a World on the machine's time, which it would freeze.
575
577
  // argv shape here: cmd='clock', subject=<world>, rest=[action, value?, flags...]
578
+ const clockForm = createRequire(import.meta.url)('@volter/world-core/world-clock');
576
579
  const name = subject;
577
580
  const [action, value] = rest;
578
- if (!name || !action || (action !== 'show' && action !== 'set' && action !== 'advance')) {
579
- console.error('usage: volter-world clock <world> show | set <iso-8601> | advance <N s|m|h|d> [--root <repo>]');
581
+ if (!name || !action || !['show', 'set', 'advance', 'shift', 'clear'].includes(action)) {
582
+ console.error('usage: volter-world clock <world> show | set <iso-8601> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear [--root <repo>]');
580
583
  process.exit(2);
581
584
  }
582
585
  const rootDir = resolve(optionValue(rest, '--root') ?? process.cwd());
583
586
  const file = clockFile(rootDir, name);
587
+ const current = existsSync(file) ? clockForm.parseClock(readFileSync(file, 'utf8')) : null;
588
+ const write = (clock) => {
589
+ mkdirSync(dirname(file), { recursive: true });
590
+ writeFileSync(file, `${clockForm.formatClock(clock)}\n`);
591
+ // the World's time now, bare, as scripts read it (`show` says which form)
592
+ console.log(new Date(clockForm.clockNowMs(clock, Date.now())).toISOString());
593
+ };
584
594
  if (action === 'show') {
585
- console.log(existsSync(file) ? `${readFileSync(file, 'utf8').trim()} (frozen)` : `${new Date().toISOString()} (wall clock — no world clock set)`);
595
+ if (!current)
596
+ console.log(`${new Date().toISOString()} (wall clock — no world clock set)`);
597
+ else if (current.kind === 'frozen')
598
+ console.log(`${new Date(current.at).toISOString()} (frozen)`);
599
+ else {
600
+ const ahead = current.at - current.since;
601
+ console.log(`${new Date(clockForm.clockNowMs(current, Date.now())).toISOString()} (running, ${ahead >= 0 ? '+' : '-'}${Math.abs(ahead) / 1000}s from the machine's time)`);
602
+ }
603
+ return;
604
+ }
605
+ if (action === 'clear') {
606
+ if (existsSync(file))
607
+ rmSync(file);
608
+ console.log(`${new Date().toISOString()} (wall clock — no world clock set)`);
586
609
  return;
587
610
  }
588
611
  if (action === 'set') {
@@ -591,25 +614,30 @@ async function main() {
591
614
  console.error(`clock set: ${JSON.stringify(value)} is not an ISO-8601 instant`);
592
615
  process.exit(2);
593
616
  }
594
- mkdirSync(dirname(file), { recursive: true });
595
- writeFileSync(file, `${new Date(parsed).toISOString()}\n`);
596
- console.log(new Date(parsed).toISOString());
617
+ write({ kind: 'frozen', at: parsed });
597
618
  return;
598
619
  }
599
620
  const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec((value ?? '').trim());
600
621
  if (!m) {
601
- console.error(`clock advance: ${JSON.stringify(value)} is not <N>(s|m|h|d)`);
622
+ console.error(`clock ${action}: ${JSON.stringify(value)} is not <N>(s|m|h|d)`);
602
623
  process.exit(2);
603
624
  }
604
- if (!existsSync(file)) {
605
- console.error('clock advance: no world clock is set (advance from wall-clock would freeze time as a side effect) — `clock set <iso>` first');
625
+ const by = Number(m[1]) * { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
626
+ if (action === 'advance') {
627
+ if (!current) {
628
+ console.error('clock advance: no world clock is set (advance from wall-clock would freeze time as a side effect) — `clock set <iso>` first, or `clock shift` to move a running World');
629
+ process.exit(2);
630
+ }
631
+ write({ ...current, at: current.at + by });
632
+ return;
633
+ }
634
+ // shift: a World on the machine's time, or already running, moves forward and keeps running
635
+ if (current?.kind === 'frozen') {
636
+ console.error('clock shift: this World\'s clock is frozen — `clock advance` moves it, or `clock clear` first');
606
637
  process.exit(2);
607
638
  }
608
- const base = Date.parse(readFileSync(file, 'utf8').trim());
609
- const unit = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
610
- const next = new Date(base + Number(m[1]) * unit).toISOString();
611
- writeFileSync(file, `${next}\n`);
612
- console.log(next);
639
+ const wall = Date.now();
640
+ write(current ? { ...current, at: current.at + by } : { kind: 'running', at: wall + by, since: wall });
613
641
  return;
614
642
  }
615
643
  if (cmd === 'urls') {
@@ -953,7 +981,7 @@ async function main() {
953
981
  // VOLTER_WORLD names the World to the injector: a request addressed to its paths carries its key
954
982
  const commandEnv = { ...process.env, ...env, ...bridged.env, VOLTER_WORLD: resolved.ref };
955
983
  commandEnv.VOLTER_TWIN_INJECT_QUIET = head.includes('--verbose') ? '0' : commandEnv.VOLTER_TWIN_INJECT_QUIET ?? '1';
956
- 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' });
957
985
  const code = await new Promise((resolveExit) => { child.on('exit', (status) => resolveExit(status ?? 1)); child.on('error', () => resolveExit(127)); });
958
986
  // a client that wrote and exited at once still has bytes on their way through a bridge
959
987
  await drainStreams();
@@ -979,7 +1007,7 @@ async function main() {
979
1007
  const compose = splitDockerComposeArgs(command);
980
1008
  if (compose) {
981
1009
  // the consumer's own service names, from its own files, through its own docker
982
- 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'] });
983
1011
  if (listed.status !== 0)
984
1012
  throw new Error(`volter-world attach --via reflect: docker compose config --services failed:\n${listed.stderr.toString().trim()}`);
985
1013
  const services = listed.stdout.toString().split('\n').map((s) => s.trim()).filter(Boolean);
@@ -988,7 +1016,7 @@ async function main() {
988
1016
  toRun = dockerComposeWithOverride(command, overridePath);
989
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`);
990
1018
  }
991
- 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' });
992
1020
  process.exit(result.status ?? 1);
993
1021
  }
994
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
@@ -39,4 +39,7 @@ export declare function isGoogleOAuthClientEnvName(name: string): boolean;
39
39
  * come from fakeGoogleServiceAccountJson instead), and NOT the OAuth-client names above —
40
40
  * those need the `{"web":{...}}` shape, not SA JSON. */
41
41
  export declare function isGoogleServiceAccountEnvName(name: string): boolean;
42
+ /** Whether `name` is a key an application parses (APP_KEY_SHAPES): a secret init fakes in its shape, whatever the
43
+ * example leaves it (LibreChat's CREDS_IV is empty in its example and not credential-shaped by name). */
44
+ export declare function isAppKeyName(name: string): boolean;
42
45
  export declare function fakeEnvValue(name: string): string;
@@ -137,6 +137,11 @@ export function isGoogleServiceAccountEnvName(name) {
137
137
  * world-runtime must not import a pack, and the tests pin every value against the injector's actual
138
138
  * VENDOR_HOSTS predicate so either side changing makes the contract fail loudly. */
139
139
  const ENDPOINT_SHAPES = [
140
+ // the vendor's own API URL, which the World routes to its twin: an app that reads a base-URL override (empty in its
141
+ // example, the vendor's URL as its default) gets the vendor's URL, never an unreachable fake
142
+ { match: /^DYNADOT_BASE_URL$/, value: 'https://api.dynadot.com/api3.json', source: 'packages/twin/dynadot/src/index.ts hosts (api.dynadot.com); dubinc/dub apps/web/lib/dynadot/client.ts default' },
143
+ // Vercel KV's REST URL (the Upstash Redis REST API under Vercel's names): an upstash host the twin serves
144
+ { match: /(^|_)KV_REST_API_URL$/, value: 'https://twin-fake.upstash.io', source: 'packages/world-core inject VENDOR_HOSTS.upstash; lukevella/rallly KV_REST_API_URL' },
140
145
  {
141
146
  match: /(^|_)UPSTASH_REDIS_REST_URL$/,
142
147
  value: 'https://twin-fake.upstash.io',
@@ -184,6 +189,14 @@ const CREDENTIAL_SHAPES = [
184
189
  // (`.clerk.accounts.dev`, packages/twin/clerk/src/index.ts hosts), so the boundary — the injector
185
190
  // server-side, the browser proxy browser-side — resolves it to the twin, and an app CSP that
186
191
  // allowlists that host stays legal (peak drive 2026-08-26).
192
+ // Stripe's keys carry their kind in their prefix, and applications check it before calling (Cal.com's Stripe app
193
+ // refuses a key that is not `sk_…`/`pk_…`); a webhook signing secret is `whsec_…`. Test mode, as a World is.
194
+ { match: /(^|_)STRIPE_[A-Z0-9_]*WEBHOOK_SECRET$/, value: () => 'whsec_twinfake0000000000000000000000000000', source: 'https://docs.stripe.com/webhooks#verify-events' },
195
+ { match: /(^|_)STRIPE_[A-Z0-9_]*(SECRET|PRIVATE|API)(_API)?_KEY$/, value: () => 'sk_test_twinfake000000000000000000000000000000', source: 'https://docs.stripe.com/keys (sk_test_)' },
196
+ { match: /(^|_)STRIPE_[A-Z0-9_]*(PUBLISHABLE|PUBLIC)(_API)?_KEY$/, value: () => 'pk_test_twinfake000000000000000000000000000000', source: 'https://docs.stripe.com/keys (pk_test_)' },
197
+ // tavily-auth.ts and firecrawl-auth.ts refuse a key without the vendor's prefix with the vendor's 401
198
+ { match: /(^|_)TAVILY_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'tvly-twinfake00000000000000000000', source: 'packages/twin/tavily/src/tavily-auth.ts' },
199
+ { match: /(^|_)FIRECRAWL_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'fc-twinfake00000000000000000000000', source: 'packages/twin/firecrawl/src/firecrawl-auth.ts FIRECRAWL_KEY_PREFIX' },
187
200
  { match: /(^|_)CLERK_[A-Z0-9_]*SECRET[A-Z0-9_]*$/, value: () => 'sk_test_twinfake000000000000000000000000000000', source: 'packages/twin/clerk/src/clerk-twin.ts' },
188
201
  { match: /(^|_)CLERK_[A-Z0-9_]*PUBLISHABLE[A-Z0-9_]*$/, value: () => `pk_test_${Buffer.from('twin.clerk.accounts.dev$').toString('base64')}`, source: 'packages/twin/clerk/src/index.ts' },
189
202
  ];
@@ -194,7 +207,18 @@ const APP_KEY_SHAPES = [
194
207
  // AES-256-GCM: the key base64-decodes to 32 bytes ("ENCRYPTION_KEY must be 32 bytes (base64-encoded)"); the 32 bytes
195
208
  // spell out that they are fake
196
209
  { match: /^ENCRYPTION_KEY$/, value: () => Buffer.from('twin-fake-encryption-key-32bytes').toString('base64'), source: "dubinc/dub apps/web/lib/encryption.ts" },
210
+ // AES-256-CBC as hex: the key is 32 bytes (64 hex characters) and the IV 16 (32); LibreChat refuses to boot on any
211
+ // other shape; the bytes spell out that they are fake
212
+ // AES-256 over the key's latin1 bytes: exactly 32 characters (Cal.com's symmetricEncrypt)
213
+ { match: /^CALENDSO_ENCRYPTION_KEY$/, value: () => 'twin-fake-calendso-key-32-chars!', source: 'calcom/cal.diy packages/lib/crypto.ts' },
214
+ { match: /^CREDS_KEY$/, value: () => Buffer.from('twin-fake-creds-key-of-32-bytes!').toString('hex'), source: 'LibreChat-AI/LibreChat api/server/utils/crypto.js' },
215
+ { match: /^CREDS_IV$/, value: () => Buffer.from('twin-fake-iv-16b').toString('hex'), source: 'LibreChat-AI/LibreChat api/server/utils/crypto.js' },
197
216
  ];
217
+ /** Whether `name` is a key an application parses (APP_KEY_SHAPES): a secret init fakes in its shape, whatever the
218
+ * example leaves it (LibreChat's CREDS_IV is empty in its example and not credential-shaped by name). */
219
+ export function isAppKeyName(name) {
220
+ return APP_KEY_SHAPES.some((shape) => shape.match.test(name));
221
+ }
198
222
  export function fakeEnvValue(name) {
199
223
  if (isGoogleOAuthClientEnvName(name))
200
224
  return fakeGoogleOAuthClientJson();
@@ -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";