@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.
- package/dist/known-external-services.json +0 -8
- package/dist/src/app-url.js +1 -1
- package/dist/src/branch.js +79 -2
- package/dist/src/catalog.js +1 -1
- package/dist/src/cli.js +50 -22
- package/dist/src/console-apart.d.ts +1 -0
- package/dist/src/console-apart.js +7 -0
- package/dist/src/covers.js +4 -2
- package/dist/src/fixture-env.d.ts +3 -0
- package/dist/src/fixture-env.js +24 -0
- package/dist/src/host-worker.js +2 -9
- package/dist/src/host.js +2 -9
- package/dist/src/import-module.d.ts +1 -0
- package/dist/src/import-module.js +16 -0
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.js +4 -1
- package/dist/src/infra-cli.js +61 -12
- package/dist/src/init.d.ts +1 -1
- package/dist/src/init.js +34 -5
- package/dist/src/local-branches.d.ts +37 -0
- package/dist/src/local-branches.js +193 -0
- package/dist/src/pglite-backing.d.ts +8 -0
- package/dist/src/pglite-backing.js +121 -35
- package/dist/src/pglite-host.mjs +520 -14
- package/dist/src/prerequisites.js +1 -1
- package/dist/src/process-groups.js +1 -1
- package/dist/src/redirect-proxy.d.ts +1 -1
- package/dist/src/redirect-proxy.js +4 -4
- package/dist/src/redis-backing.d.ts +8 -0
- package/dist/src/redis-backing.js +120 -0
- package/dist/src/root.d.ts +23 -0
- package/dist/src/root.js +22 -10
- package/dist/src/run-task.js +1 -1
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.js +37 -20
- package/dist/src/schema.d.ts +5 -0
- package/dist/src/schema.js +10 -1
- package/dist/src/served-world.d.ts +196 -9
- package/dist/src/served-world.js +847 -103
- package/dist/src/service-recorder.js +1 -1
- package/dist/src/storage-capacity.js +2 -2
- package/dist/src/up-task.js +1 -1
- package/dist/src/world-origins.d.ts +13 -0
- package/dist/src/world-origins.js +37 -0
- package/dist/src/world-view.d.ts +25 -0
- package/dist/src/world-view.js +110 -0
- package/known-external-services.json +0 -8
- package/package.json +10 -4
- package/src/app-url.ts +1 -1
- package/src/branch.ts +66 -2
- package/src/catalog.ts +1 -1
- package/src/cli.ts +43 -21
- package/src/console-apart.ts +7 -1
- package/src/covers.ts +4 -2
- package/src/fixture-env.ts +25 -0
- package/src/host-worker.ts +2 -1
- package/src/host.ts +2 -1
- package/src/import-module.ts +9 -0
- package/src/index.ts +7 -2
- package/src/infra-cli.ts +56 -12
- package/src/init.ts +34 -5
- package/src/local-branches.ts +173 -0
- package/src/pglite-backing.ts +112 -36
- package/src/pglite-host.mjs +520 -14
- package/src/prerequisites.ts +1 -1
- package/src/process-groups.ts +1 -1
- package/src/redirect-proxy.ts +4 -4
- package/src/redis-backing.ts +107 -0
- package/src/root.ts +27 -2
- package/src/run-task.ts +1 -1
- package/src/runtime.ts +38 -20
- package/src/schema.ts +11 -1
- package/src/served-world.ts +762 -85
- package/src/service-recorder.ts +1 -1
- package/src/storage-capacity.ts +2 -2
- package/src/up-task.ts +1 -1
- package/src/world-origins.ts +38 -0
- 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"
|
package/dist/src/app-url.js
CHANGED
|
@@ -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
|
}
|
package/dist/src/branch.js
CHANGED
|
@@ -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).
|
|
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());
|
package/dist/src/catalog.js
CHANGED
|
@@ -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
|
|
573
|
-
//
|
|
574
|
-
//
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
622
|
+
console.error(`clock ${action}: ${JSON.stringify(value)} is not <N>(s|m|h|d)`);
|
|
602
623
|
process.exit(2);
|
|
603
624
|
}
|
|
604
|
-
|
|
605
|
-
|
|
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
|
|
609
|
-
|
|
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') {
|
|
@@ -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
|
package/dist/src/covers.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/src/fixture-env.js
CHANGED
|
@@ -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();
|
package/dist/src/host-worker.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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";
|