@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.
- package/dist/src/app-url.js +1 -1
- package/dist/src/catalog.js +1 -1
- package/dist/src/cli.js +3 -3
- 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/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 +2 -2
- package/dist/src/init.d.ts +1 -1
- package/dist/src/init.js +3 -0
- package/dist/src/local-branches.d.ts +37 -0
- package/dist/src/local-branches.js +193 -0
- package/dist/src/pglite-backing.js +8 -5
- package/dist/src/prerequisites.js +1 -1
- package/dist/src/process-groups.js +1 -1
- package/dist/src/redirect-proxy.js +2 -2
- 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.js +15 -15
- 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/package.json +6 -5
- package/src/app-url.ts +1 -1
- package/src/catalog.ts +1 -1
- package/src/cli.ts +3 -3
- package/src/console-apart.ts +7 -1
- package/src/covers.ts +4 -2
- 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 +2 -2
- package/src/init.ts +3 -0
- package/src/local-branches.ts +173 -0
- package/src/pglite-backing.ts +8 -5
- package/src/prerequisites.ts +1 -1
- package/src/process-groups.ts +1 -1
- package/src/redirect-proxy.ts +2 -2
- package/src/root.ts +27 -2
- package/src/run-task.ts +1 -1
- package/src/runtime.ts +15 -15
- 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
package/dist/src/root.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
|
// A TWIN'S ROOT (docs/concepts/the-model.md): the vendor's real account
|
|
10
2
|
// behind a twin on a shared world. `volter twin <vendor> root <url> --deploy <policy>` records it,
|
|
11
3
|
// key-free, in `.volter/world.json` (committed) and, at boot, in the twin's state dir (core's
|
|
@@ -22,6 +14,7 @@ import { authStrategyFor, getActiveWorldStore, getPack, sealingKey, buildRemoteE
|
|
|
22
14
|
import { loadWorldConfig, writeWorldConfig } from "./configs.js";
|
|
23
15
|
import { changesetReadiness, collectObservations, foldObservations, loadChecks, openRootCredential, performEntries, rootCustody, protocolStanding } from '@volter/world-core';
|
|
24
16
|
import { findWorldChangeset, listWorldChangesets } from "./changeset.js";
|
|
17
|
+
import { importModule } from "./import-module.js";
|
|
25
18
|
import { statusWorld } from "./runtime.js";
|
|
26
19
|
import { findInstalledPackage, twinPackageName } from "./catalog.js";
|
|
27
20
|
// ── the user's sealing key ──────────────────────────────────────────────────────────────────
|
|
@@ -125,7 +118,7 @@ async function packModule(vendor, from) {
|
|
|
125
118
|
if (registered)
|
|
126
119
|
return { pack: registered };
|
|
127
120
|
const entry = packEntryPath(vendor, from);
|
|
128
|
-
return entry ?
|
|
121
|
+
return entry ? await importModule(entry) : null;
|
|
129
122
|
}
|
|
130
123
|
/** The pack's module itself, installed beside the World or in this checkout: its every export (a
|
|
131
124
|
* mirror's shell and client), where the registry holds only the descriptor. */
|
|
@@ -144,7 +137,26 @@ export async function packMirrorExports(vendor, from) {
|
|
|
144
137
|
return null;
|
|
145
138
|
const dir = dirname(entry);
|
|
146
139
|
const file = existsSync(dir) ? readdirSync(dir).find((f) => /-mirror-ui\.(ts|js)$/.test(f) && !f.includes('.test.')) : undefined;
|
|
147
|
-
return (
|
|
140
|
+
return importModule(file ? join(dir, file) : entry);
|
|
141
|
+
}
|
|
142
|
+
export async function packMirror(vendor, from) {
|
|
143
|
+
const installed = findInstalledPackage(from, twinPackageName(vendor));
|
|
144
|
+
const dir = installed ?? resolve(import.meta.dir, '..', '..', 'twin', vendor);
|
|
145
|
+
// the pack's mirror module, as the hosted build reads it (its index need not re-export every builder)
|
|
146
|
+
const src = join(dir, 'src');
|
|
147
|
+
const file = existsSync(src) ? readdirSync(src).find((f) => f.endsWith('-mirror-ui.ts')) : undefined;
|
|
148
|
+
const entry = file ? join(src, file) : installed ? packEntry(installed) : join(src, 'index.ts');
|
|
149
|
+
if (!existsSync(entry))
|
|
150
|
+
return null;
|
|
151
|
+
const mod = await importModule(entry);
|
|
152
|
+
const pick = (re) => { const k = Object.keys(mod).find((key) => re.test(key) && typeof mod[key] === 'function'); return k ? mod[k] : undefined; };
|
|
153
|
+
const html = pick(/^\w+MirrorHtml$/);
|
|
154
|
+
const client = pick(/^build\w+MirrorClient$/);
|
|
155
|
+
const styles = pick(/^\w+MirrorStyles$/);
|
|
156
|
+
const signIn = pick(/^\w+MirrorSignIn$/);
|
|
157
|
+
if (!html || !client)
|
|
158
|
+
return null;
|
|
159
|
+
return { html: html, client: client, ...(styles ? { styles: styles } : {}), ...(signIn ? { signIn: signIn } : {}) };
|
|
148
160
|
}
|
|
149
161
|
export async function adaptersFor(vendor, from) {
|
|
150
162
|
const registered = stateSystemFor(vendor);
|
package/dist/src/run-task.js
CHANGED
|
@@ -7,7 +7,7 @@ import { siblingScript } from "./sibling.js";
|
|
|
7
7
|
* The task process is outside the caller's POSIX process group and owns boot through teardown. */
|
|
8
8
|
export function superviseWorldRun(configId, command, options) {
|
|
9
9
|
return new Promise((done, reject) => {
|
|
10
|
-
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'run-task-worker')], {
|
|
10
|
+
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'run-task-worker')], { windowsHide: true,
|
|
11
11
|
cwd: process.cwd(), env: process.env, detached: process.platform !== 'win32',
|
|
12
12
|
stdio: ['inherit', 'inherit', 'inherit', 'ipc'], serialization: 'json',
|
|
13
13
|
});
|
package/dist/src/runtime.js
CHANGED
|
@@ -65,7 +65,7 @@ function spawnRecorded(serviceId, command, args, cwd, env, log) {
|
|
|
65
65
|
recorderEnv.VOLTER_WORLD_SERVICE_NODE_OPTIONS = recorderEnv.NODE_OPTIONS;
|
|
66
66
|
delete recorderEnv.NODE_OPTIONS;
|
|
67
67
|
}
|
|
68
|
-
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'service-recorder'), command, ...args], {
|
|
68
|
+
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'service-recorder'), command, ...args], { windowsHide: true,
|
|
69
69
|
cwd,
|
|
70
70
|
env: recorderEnv,
|
|
71
71
|
detached: true,
|
|
@@ -614,7 +614,7 @@ function processStarts(pids) {
|
|
|
614
614
|
}
|
|
615
615
|
return starts;
|
|
616
616
|
}
|
|
617
|
-
const result = spawnSync('ps', ['-o', 'pid=,lstart=', '-p', pids.join(',')], { encoding: 'utf8', timeout: 1000, killSignal: 'SIGKILL', env: { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } });
|
|
617
|
+
const result = spawnSync('ps', ['-o', 'pid=,lstart=', '-p', pids.join(',')], { windowsHide: true, encoding: 'utf8', timeout: 1000, killSignal: 'SIGKILL', env: { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } });
|
|
618
618
|
for (const line of (result.stdout ?? '').split('\n')) {
|
|
619
619
|
const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(line);
|
|
620
620
|
if (match)
|
|
@@ -768,7 +768,7 @@ async function runExternalCommand(command, cwd, serviceId, phase, env = process.
|
|
|
768
768
|
if (!commandExists(bin))
|
|
769
769
|
throw new Error(`External service "${serviceId}": \`${bin}\` not found on PATH (the ${phase} command's tool is not installed)`);
|
|
770
770
|
abort?.throwIfAborted();
|
|
771
|
-
const child = spawn(bin, args, { cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
771
|
+
const child = spawn(bin, args, { windowsHide: true, cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
772
772
|
const lifetime = commandLifetime(child, abort, true);
|
|
773
773
|
let stdout = '';
|
|
774
774
|
let stderr = '';
|
|
@@ -815,7 +815,7 @@ async function runExternalUp(command, cwd, serviceId, env, logPath, abort) {
|
|
|
815
815
|
const chunks = [];
|
|
816
816
|
try {
|
|
817
817
|
abort?.throwIfAborted();
|
|
818
|
-
const child = spawn(bin, args, { cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
818
|
+
const child = spawn(bin, args, { windowsHide: true, cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
819
819
|
const lifetime = commandLifetime(child, abort, true);
|
|
820
820
|
let failure;
|
|
821
821
|
child.on('error', error => { failure = error; });
|
|
@@ -2237,10 +2237,10 @@ export function ownCommand(command) {
|
|
|
2237
2237
|
return [process.execPath, siblingScript(import.meta.url, stem), ...args];
|
|
2238
2238
|
}
|
|
2239
2239
|
function onPath(command) {
|
|
2240
|
-
return spawnSync('which', [command], { stdio: 'ignore', env: process.env }).status === 0;
|
|
2240
|
+
return spawnSync('which', [command], { windowsHide: true, stdio: 'ignore', env: process.env }).status === 0;
|
|
2241
2241
|
}
|
|
2242
2242
|
function resolveWithPublicDns(hostname) {
|
|
2243
|
-
const result = spawnSync('dig', ['+short', '@1.1.1.1', hostname, 'A'], {
|
|
2243
|
+
const result = spawnSync('dig', ['+short', '@1.1.1.1', hostname, 'A'], { windowsHide: true,
|
|
2244
2244
|
encoding: 'utf8',
|
|
2245
2245
|
});
|
|
2246
2246
|
if (result.status !== 0)
|
|
@@ -2276,7 +2276,7 @@ async function verifyPublicUrl(publicUrl, path, timeoutMs) {
|
|
|
2276
2276
|
|| (isIP(normalizedHostname) === 4 && normalizedHostname.split('.')[0] === '127');
|
|
2277
2277
|
if (local) {
|
|
2278
2278
|
const target = publicHealthUrl(publicUrl, path);
|
|
2279
|
-
const result = spawnSync('curl', ['--noproxy', '*', '-sS', '-o', '-', '-w', '\n%{http_code}', '--max-time', String(Math.max(1, Math.ceil(timeoutMs / 1000))), target], { encoding: 'utf8' });
|
|
2279
|
+
const result = spawnSync('curl', ['--noproxy', '*', '-sS', '-o', '-', '-w', '\n%{http_code}', '--max-time', String(Math.max(1, Math.ceil(timeoutMs / 1000))), target], { windowsHide: true, encoding: 'utf8' });
|
|
2280
2280
|
const separator = result.stdout.lastIndexOf('\n');
|
|
2281
2281
|
const body = separator >= 0 ? result.stdout.slice(0, separator).trim() : result.stdout.trim();
|
|
2282
2282
|
const status = Number(separator >= 0 ? result.stdout.slice(separator + 1).trim() : '0');
|
|
@@ -2307,7 +2307,7 @@ async function verifyPublicUrl(publicUrl, path, timeoutMs) {
|
|
|
2307
2307
|
'--resolve',
|
|
2308
2308
|
`${hostname}:443:${resolvedIp}`,
|
|
2309
2309
|
target,
|
|
2310
|
-
], { encoding: 'utf8' });
|
|
2310
|
+
], { windowsHide: true, encoding: 'utf8' });
|
|
2311
2311
|
if (result.status === 0) {
|
|
2312
2312
|
const output = result.stdout;
|
|
2313
2313
|
const separator = output.lastIndexOf('\n');
|
|
@@ -2370,7 +2370,7 @@ async function shareWorldReserved(name, options) {
|
|
|
2370
2370
|
// Each spawned tunnel owns one log generation. Truncating before spawn prevents a repeated
|
|
2371
2371
|
// share from accepting the previous process's URL handshake as the new process's endpoint.
|
|
2372
2372
|
const out = openPrivateLog(log, 'w');
|
|
2373
|
-
const child = spawn(tunnel.command, tunnel.args, {
|
|
2373
|
+
const child = spawn(tunnel.command, tunnel.args, { windowsHide: true,
|
|
2374
2374
|
cwd: root,
|
|
2375
2375
|
env: process.env,
|
|
2376
2376
|
detached: true,
|
|
@@ -2540,7 +2540,7 @@ async function probeExternalOnce(ready, cwd, env, logPath) {
|
|
|
2540
2540
|
return { ok: false, message: `readiness command \`${ready.command}\` not found on PATH` };
|
|
2541
2541
|
}
|
|
2542
2542
|
const [probeBin, ...probeArgs] = ownCommand([ready.command, ...(ready.args ?? [])]);
|
|
2543
|
-
const result = spawnSync(probeBin, probeArgs, {
|
|
2543
|
+
const result = spawnSync(probeBin, probeArgs, { windowsHide: true,
|
|
2544
2544
|
cwd, encoding: 'utf8', env, timeout: DOCTOR_EXTERNAL_PROBE_TIMEOUT_MS, maxBuffer: 64 * 1024 * 1024,
|
|
2545
2545
|
});
|
|
2546
2546
|
if (result.status === 0)
|
|
@@ -2914,7 +2914,7 @@ export function runWithWorldEnv(name, command, root = process.cwd(), options = {
|
|
|
2914
2914
|
throw new Error('Missing command after --');
|
|
2915
2915
|
const attached = worldAttachedCommandEnv(name, root, options.verbose);
|
|
2916
2916
|
return new Promise((resolveExit) => {
|
|
2917
|
-
const child = spawn(command[0], command.slice(1), {
|
|
2917
|
+
const child = spawn(command[0], command.slice(1), { windowsHide: true,
|
|
2918
2918
|
cwd: resolve(options.cwd ?? root),
|
|
2919
2919
|
env: attached.env,
|
|
2920
2920
|
stdio: 'inherit',
|
|
@@ -2998,7 +2998,7 @@ export async function attachWorld(name, command, root = process.cwd(), options =
|
|
|
2998
2998
|
let retired = true;
|
|
2999
2999
|
try {
|
|
3000
3000
|
return await new Promise((done, reject) => {
|
|
3001
|
-
const child = spawn(command[0], command.slice(1), { cwd: resolve(options.cwd ?? root), env: attached.env, stdio: 'inherit', detached: commandProcessGroup });
|
|
3001
|
+
const child = spawn(command[0], command.slice(1), { windowsHide: true, cwd: resolve(options.cwd ?? root), env: attached.env, stdio: 'inherit', detached: commandProcessGroup });
|
|
3002
3002
|
const lifetime = commandLifetime(child);
|
|
3003
3003
|
let failure;
|
|
3004
3004
|
record.consumerPid = child.pid;
|
|
@@ -3099,7 +3099,7 @@ async function runWithWorldEnvLogged(name, command, root, options = {}) {
|
|
|
3099
3099
|
let spawnError;
|
|
3100
3100
|
let child;
|
|
3101
3101
|
try {
|
|
3102
|
-
child = spawn(command[0], command.slice(1), {
|
|
3102
|
+
child = spawn(command[0], command.slice(1), { windowsHide: true,
|
|
3103
3103
|
cwd: resolve(options.cwd ?? root),
|
|
3104
3104
|
env: attached.env,
|
|
3105
3105
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
@@ -3206,7 +3206,7 @@ export async function shellWorld(name, root = process.cwd()) {
|
|
|
3206
3206
|
process.stderr.write(`world '${name}' active in a subshell — vendor calls hit the twins (env-only; install openssl for ambient https redirect). type 'exit' to leave.\n`);
|
|
3207
3207
|
}
|
|
3208
3208
|
try {
|
|
3209
|
-
const result = spawnSync(shell, ['-i'], { cwd, env, stdio: 'inherit' });
|
|
3209
|
+
const result = spawnSync(shell, ['-i'], { windowsHide: true, cwd, env, stdio: 'inherit' });
|
|
3210
3210
|
return result.status ?? 0;
|
|
3211
3211
|
}
|
|
3212
3212
|
finally {
|
|
@@ -3314,7 +3314,7 @@ function startProxyDaemonOwned(name, root, caCertPath, envFile, boot, caller = p
|
|
|
3314
3314
|
mkdirSync(dirname(proxyLogPath(root, name)), { recursive: true, mode: 0o700 });
|
|
3315
3315
|
const proxyOut = openPrivateLog(proxyLogPath(root, name), 'a');
|
|
3316
3316
|
try {
|
|
3317
|
-
child = spawn(process.execPath, [proxyDaemonEntry(), name, '--root', root, ...(envFile ? [`--env-file=${envFile}`] : [])], {
|
|
3317
|
+
child = spawn(process.execPath, [proxyDaemonEntry(), name, '--root', root, ...(envFile ? [`--env-file=${envFile}`] : [])], { windowsHide: true,
|
|
3318
3318
|
cwd: root, env: process.env, detached: true, stdio: ['ignore', proxyOut, proxyOut],
|
|
3319
3319
|
});
|
|
3320
3320
|
}
|
package/dist/src/schema.d.ts
CHANGED
|
@@ -190,6 +190,11 @@ export type WorldServiceConfig = {
|
|
|
190
190
|
* to loopback services directly while presenting a disguised world to the app/user.
|
|
191
191
|
*/
|
|
192
192
|
controlPlane?: boolean;
|
|
193
|
+
/** The account a vendor's own screens (its mirror) open signed in as: its handle, username or email, which the pack
|
|
194
|
+
* resolves against the accounts this World holds (`<vendor>MirrorSignIn`). Nothing secret is written here. */
|
|
195
|
+
signIn?: {
|
|
196
|
+
as: string;
|
|
197
|
+
};
|
|
193
198
|
injectEnv?: string;
|
|
194
199
|
/**
|
|
195
200
|
* Additional env vars exported into the generated world env after this service starts.
|
package/dist/src/schema.js
CHANGED
|
@@ -358,7 +358,7 @@ export function assertWorldSelection(value, path) {
|
|
|
358
358
|
function serviceV2ToLegacy(value, path) {
|
|
359
359
|
if (!record(value))
|
|
360
360
|
throw new Error(`World service must be an object in ${path}`);
|
|
361
|
-
assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root'], `Service ${String(value.id ?? '')}`, path, true);
|
|
361
|
+
assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root', 'signIn'], `Service ${String(value.id ?? '')}`, path, true);
|
|
362
362
|
const source = value.source === undefined ? {} : value.source;
|
|
363
363
|
const execution = value.execution === undefined ? {} : value.execution;
|
|
364
364
|
const endpoint = value.endpoint === undefined ? {} : value.endpoint;
|
|
@@ -379,6 +379,13 @@ function serviceV2ToLegacy(value, path) {
|
|
|
379
379
|
throw new Error(`World service must define string id in ${path}`);
|
|
380
380
|
if (value.description !== undefined && typeof value.description !== 'string')
|
|
381
381
|
throw new Error(`Service "${value.id}" description must be a string in ${path}`);
|
|
382
|
+
if (value.signIn !== undefined) {
|
|
383
|
+
if (!record(value.signIn))
|
|
384
|
+
throw new Error(`Service "${value.id}" signIn must be an object in ${path}`);
|
|
385
|
+
assertKeys(value.signIn, ['as'], `Service "${value.id}" signIn`, path);
|
|
386
|
+
if (typeof value.signIn.as !== 'string' || value.signIn.as.trim() === '')
|
|
387
|
+
throw new Error(`Service "${value.id}" signIn.as must name an account (a handle, username or email) in ${path}`);
|
|
388
|
+
}
|
|
382
389
|
if (value.description !== undefined && value['//'] !== undefined)
|
|
383
390
|
throw new Error(`Service "${value.id}" must not set both description and // in ${path}`);
|
|
384
391
|
if (execution.cwd !== undefined && typeof execution.cwd !== 'string')
|
|
@@ -437,6 +444,7 @@ function serviceV2ToLegacy(value, path) {
|
|
|
437
444
|
...(bindings.injectEnvTemplates === undefined ? {} : { injectEnvTemplates: bindings.injectEnvTemplates }),
|
|
438
445
|
...(bindings.cliRedirect === undefined ? {} : { cliRedirect: bindings.cliRedirect }),
|
|
439
446
|
...(value.root === undefined ? {} : { root: value.root }),
|
|
447
|
+
...(value.signIn === undefined ? {} : { signIn: { as: value.signIn.as } }),
|
|
440
448
|
...(external === undefined ? {} : { external }),
|
|
441
449
|
};
|
|
442
450
|
if (type !== 'external' && bindings.discover !== undefined)
|
|
@@ -573,6 +581,7 @@ function serviceToV2(service) {
|
|
|
573
581
|
...(Object.keys(endpoint).length ? { endpoint } : {}),
|
|
574
582
|
...(Object.keys(bindings).length ? { bindings } : {}),
|
|
575
583
|
...(service.root === undefined ? {} : { root: service.root }),
|
|
584
|
+
...(service.signIn === undefined ? {} : { signIn: service.signIn }),
|
|
576
585
|
};
|
|
577
586
|
}
|
|
578
587
|
/** Serialize normalized intent as the committed format-2 manifest. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type HistoryReference } from '@volter/world-core';
|
|
2
2
|
import { type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core';
|
|
3
|
+
import { type TrustedIssuer } from '@volter/world-access';
|
|
3
4
|
import { type Changeset, type Receipt } from '@volter/world-core';
|
|
4
5
|
import { rootForControlRoot } from './root.js';
|
|
5
6
|
import type { WorldInstance } from './schema.js';
|
|
@@ -29,6 +30,11 @@ export type ServedWorld = {
|
|
|
29
30
|
console: string | null;
|
|
30
31
|
stop: () => Promise<void>;
|
|
31
32
|
};
|
|
33
|
+
/** Where a World keeps its browser sessions (WorldDoors): a host that rotates the tokens without the doors at hand
|
|
34
|
+
* clears it itself. */
|
|
35
|
+
export declare function sessionsFile(worldRoot: string): string;
|
|
36
|
+
/** Where a World keeps its named keys (their hashes, never a key): `.volter/keys.json`. */
|
|
37
|
+
export declare function keysFile(worldRoot: string): string;
|
|
32
38
|
/** The served name: `bare.name` (`acme/team`), else `<id>/<id>`. */
|
|
33
39
|
export declare function servedName(worldRoot: string, configRef: string): string;
|
|
34
40
|
/** A world MOUNTED for serving: its doors, its tokens, and a boot that writes the serve record once the
|
|
@@ -53,7 +59,11 @@ export type MountedWorld = {
|
|
|
53
59
|
stop: () => Promise<void>;
|
|
54
60
|
};
|
|
55
61
|
export declare function mountWorld(name: string, opts?: {
|
|
56
|
-
|
|
62
|
+
passIssuers?: TrustedIssuer[];
|
|
63
|
+
root?: string; /** the host's branches of this World, by its served name */
|
|
64
|
+
branches?: (served: string) => BranchDoors | undefined; /** the origin a browser reaches this World at, when the host gives each World one */
|
|
65
|
+
browserOrigin?: (served: string) => string | null; /** served on this machine's loopback: its own pages need no token (DoorHost.localTrust) */
|
|
66
|
+
localTrust?: boolean;
|
|
57
67
|
}): Promise<MountedWorld>;
|
|
58
68
|
/** Boot the world (state kept) and serve it on its own port. Announces and returns after boot; `stop` downs the world. */
|
|
59
69
|
export declare function serveWorld(name: string, opts?: {
|
|
@@ -99,7 +109,78 @@ export type DoorHost = {
|
|
|
99
109
|
/** Where this host reads a twin's scenario (the handlers document a generative twin serves from,
|
|
100
110
|
* re-read on every request), or null when the twin takes none here. */
|
|
101
111
|
scenarioPath?: (vendor: string) => string | null;
|
|
112
|
+
/** A twin's mirror as its pack builds it: `index.html` (the shell) or `assets/<file>`, or null when
|
|
113
|
+
* the twin has none here. Every host answers the same mount (`/<served>/<vendor>/mirror/`); only
|
|
114
|
+
* where the files come from differs (built at serve under Bun, prebuilt under Node, static assets
|
|
115
|
+
* on Cloudflare). */
|
|
116
|
+
mirror?: (vendor: string, file: string) => Promise<Response | null>;
|
|
117
|
+
/** Whether the twin has a mirror on this host, without building it. */
|
|
118
|
+
hasMirror?: (vendor: string) => boolean | Promise<boolean>;
|
|
119
|
+
/** The account and token a twin's screens open signed in as, resolved by its pack from the twin's state at `root`
|
|
120
|
+
* for the account the World's config names (`signIn.as`); null when the pack cannot, or has no such account. */
|
|
121
|
+
mirrorSignIn?: (vendor: string, as: string, ctx: {
|
|
122
|
+
root: string;
|
|
123
|
+
twin: (path: string, init?: RequestInit) => Promise<Response>;
|
|
124
|
+
}) => Promise<{
|
|
125
|
+
account: string;
|
|
126
|
+
token: string;
|
|
127
|
+
} | null>;
|
|
128
|
+
/** Where this World's clock lives in its store (the file every twin reads `worldNow()` from). */
|
|
129
|
+
clockPath?: () => string;
|
|
130
|
+
/** The origin a browser reaches this World at (`http://<world>--<org>.localhost:<port>`), when the
|
|
131
|
+
* host gives each World one: its session lives only there (docs/contributing/architecture.md,
|
|
132
|
+
* "Viewing a World"). Null, or absent, on a host whose Worlds share one origin. */
|
|
133
|
+
browserOrigin?: () => string | null;
|
|
134
|
+
/** The origin another World this host serves (a branch of this one) is reached at, where it has one of its own. */
|
|
135
|
+
originOf?: (served: string) => string | null;
|
|
136
|
+
/** This World is served on this machine's loopback, for the person at it: a page of this World, reached by a
|
|
137
|
+
* loopback name, is given the World's session without a token (`localSession`). A remote host leaves it absent,
|
|
138
|
+
* and its pages sign in with a token. */
|
|
139
|
+
localTrust?: () => boolean;
|
|
140
|
+
/** The platforms whose passes open this World (docs/contributing/architecture.md, "The hosted product"): a person a
|
|
141
|
+
* trusted platform signed a pass for is given a session of the pass's scope. Absent, or empty, no pass opens it. */
|
|
142
|
+
passIssuers?: () => TrustedIssuer[];
|
|
143
|
+
/** This World's branches, where the host can make them (a branch is another World, so making one
|
|
144
|
+
* is the host's act); absent, the branches doors answer 404. */
|
|
145
|
+
branches?: BranchDoors;
|
|
146
|
+
};
|
|
147
|
+
/** A branch as its parent lists it. */
|
|
148
|
+
export type BranchRow = {
|
|
149
|
+
name: string;
|
|
150
|
+
from: string;
|
|
151
|
+
at: {
|
|
152
|
+
instant?: string;
|
|
153
|
+
live?: boolean;
|
|
154
|
+
label?: string;
|
|
155
|
+
} | null;
|
|
156
|
+
createdAt: string;
|
|
157
|
+
expiresAt: string | null;
|
|
102
158
|
};
|
|
159
|
+
/** What a host does for a World's branches doors. `create` makes a World cloned from this one at
|
|
160
|
+
* `at` (its history cut at that instant), removed when `ttlSeconds` runs out; `origin` is where the
|
|
161
|
+
* request reached this World (a host whose branches clone over its public URL uses it). */
|
|
162
|
+
export type BranchDoors = {
|
|
163
|
+
list: () => Promise<BranchRow[]>;
|
|
164
|
+
/** `parentKey`: the key the branch reaches this World with (never this World's own token), made for whoever asked */
|
|
165
|
+
create: (at: {
|
|
166
|
+
instant?: string;
|
|
167
|
+
live?: boolean;
|
|
168
|
+
label?: string;
|
|
169
|
+
}, ttlSeconds: number | null, origin: string, parentKey?: string) => Promise<{
|
|
170
|
+
name: string;
|
|
171
|
+
token: string;
|
|
172
|
+
readToken: string;
|
|
173
|
+
expiresAt: string | null;
|
|
174
|
+
}>;
|
|
175
|
+
remove: (name: string) => Promise<boolean>;
|
|
176
|
+
};
|
|
177
|
+
/** A World's browser-origin label, `<world>--<org>`, or null when its names cannot make one (a DNS
|
|
178
|
+
* label: lowercase letters, digits and single hyphens, 63 at most). A host maps a label back by the
|
|
179
|
+
* Worlds it serves, never by parsing it. */
|
|
180
|
+
export declare function worldOriginLabel(served: string): string | null;
|
|
181
|
+
/** The header the doors set on a read-scope request to a twin: the twin refuses the caller's writes
|
|
182
|
+
* and still makes the vendor's own moves. It only restricts, so a twin trusts it without a token. */
|
|
183
|
+
export declare const READ_ONLY_HEADER = "x-volter-read-only";
|
|
103
184
|
export declare class WorldDoors {
|
|
104
185
|
readonly name: string;
|
|
105
186
|
readonly worldRoot: string;
|
|
@@ -112,6 +193,46 @@ export declare class WorldDoors {
|
|
|
112
193
|
constructor(name: string, worldRoot: string, configRef: string, served: string, host: DoorHost, token: string, readToken: string);
|
|
113
194
|
/** New tokens; a request presenting the old ones is refused from the next call on. */
|
|
114
195
|
retoken(token: string, readToken: string): void;
|
|
196
|
+
/** BROWSER SESSIONS: the session cookie carries an opaque id, never a token, so a leaked cookie is one session and
|
|
197
|
+
* not the World's write token; rotating the tokens ends every session. Kept in the World's store, so a restart
|
|
198
|
+
* keeps them; each lasts 30 days from its opening. */
|
|
199
|
+
private sessionsPath;
|
|
200
|
+
private held;
|
|
201
|
+
private sessions;
|
|
202
|
+
private keepSessions;
|
|
203
|
+
/** A new session of `scope`: its id, the cookie's value. A token's lasts 30 days; a person's from a pass, `who`,
|
|
204
|
+
* lasts as long as the pass's platform says (twelve hours), so a person removed there loses it within the day. */
|
|
205
|
+
private openSession;
|
|
206
|
+
/** NAMED KEYS: what an app or a script holds (docs/contributing/architecture.md, "The hosted product"), each named
|
|
207
|
+
* when made, shown once, kept only as its hash, and revoked alone; the World's two tokens stay its first keys. */
|
|
208
|
+
private heldKeys;
|
|
209
|
+
private keys;
|
|
210
|
+
private keepKeys;
|
|
211
|
+
/** When a key was last used, to the minute (a key in constant use is not a write per request). */
|
|
212
|
+
private touchKey;
|
|
213
|
+
/** `GET|POST /-/<world>/keys`, `DELETE /-/<world>/keys/<id>`, `DELETE /-/<world>/keys?person=<sub>`: named keys are
|
|
214
|
+
* managed with write access by the World's token or a person's session, never by a key (a key cannot make a key
|
|
215
|
+
* that outlives it). Each records who made it and whom it is for; the token may give one an end and a person, and
|
|
216
|
+
* revoke every key made by or for a person (their leaving the org). */
|
|
217
|
+
/** The person a key is for: whom it was made for, else the person whose session made it. */
|
|
218
|
+
private keyPerson;
|
|
219
|
+
/** Revoke the keys `pick` names, and what they reach: the branches whose parent key goes, and the branches (with their
|
|
220
|
+
* keys) a revoked key made. Answers the revoked keys' ids. */
|
|
221
|
+
private revokeKeys;
|
|
222
|
+
private keysDoor;
|
|
223
|
+
/** Passes this World has already spent: each opens one session. */
|
|
224
|
+
private readonly spent;
|
|
225
|
+
/** A PASS opens a session: a person a trusted platform signed a pass for, into this World, at its own origin, asked
|
|
226
|
+
* from its own page (the console reads the pass from its address's fragment and posts it here). */
|
|
227
|
+
/** The org its host records as this World's owner (`<state>/owner`, written at its making or its claim), or null. */
|
|
228
|
+
private recordedOwner;
|
|
229
|
+
private passSession;
|
|
230
|
+
/** A named key that still opens this World: not revoked, not expired. */
|
|
231
|
+
private liveKey;
|
|
232
|
+
/** The named key a request presents, with the end of its life. */
|
|
233
|
+
private presentedKey;
|
|
234
|
+
private endSession;
|
|
235
|
+
private endSessions;
|
|
115
236
|
/** The world's twins, by vendor, once booted. */
|
|
116
237
|
twins(): string[];
|
|
117
238
|
private up;
|
|
@@ -119,8 +240,29 @@ export declare class WorldDoors {
|
|
|
119
240
|
* the injector's `x-twins-key` (a zero-edit app keeps its SDK's own Authorization), `Bearer <token>`,
|
|
120
241
|
* GitHub's `token <token>`, or the password half of `Basic` (Jira's email:token). */
|
|
121
242
|
private scope;
|
|
122
|
-
/** The
|
|
123
|
-
|
|
243
|
+
/** The request's scope, whether it came by a token the caller presented or by a browser's session
|
|
244
|
+
* (the cookie `POST …/session` set), and the token that opened it. Every form presented is a
|
|
245
|
+
* candidate: a mirror page sends the VENDOR's bearer (the token its sign-in holds) beside the World's
|
|
246
|
+
* session cookie, and the cookie is what opens the World. With a browser origin of its own
|
|
247
|
+
* (`browserOrigin`), a World honours its session only on that origin: a session cookie on any other
|
|
248
|
+
* (a host's shared origin, path-addressed) opens nothing. */
|
|
249
|
+
/** What this World issued, for world-access to decide a grant against: its two tokens, its named keys, its sessions. */
|
|
250
|
+
private issued;
|
|
251
|
+
private credential;
|
|
252
|
+
/** The session cookie's name where `url` reached this World, or null where a session opens nothing:
|
|
253
|
+
* on the World's own origin a host-only cookie (`__Host-` over https); without an origin of its own,
|
|
254
|
+
* the path-scoped cookie a shared origin has always carried. */
|
|
255
|
+
private sessionCookieName;
|
|
256
|
+
/** The session cookie for `url`, set to `value`: host-only over the whole origin on the World's own
|
|
257
|
+
* origin (a `__Host-` cookie must be), scoped to the World's paths on a shared one. Null where a
|
|
258
|
+
* session opens nothing (another World's origin). */
|
|
259
|
+
private sessionCookie;
|
|
260
|
+
/** A browser's session opens the World only to the World's own pages: a request the session cookie carries must
|
|
261
|
+
* say `Sec-Fetch-Site: same-origin` (a browser sets it; a page cannot), or be a read the person navigated to
|
|
262
|
+
* (`none`: a typed or bookmarked address). Another page of the same site (a sibling World's origin shares its
|
|
263
|
+
* suffix) is refused its reads too, so a twin that echoes the caller's Origin with credentials cannot hand one
|
|
264
|
+
* World's data to another World's page; a request with no header fails closed. Null when it may pass. */
|
|
265
|
+
private crossOriginSession;
|
|
124
266
|
/** Open one connection on a byte-stream door for the holder of the World's (write) token; null when
|
|
125
267
|
* the token is not the World's or the World has no such stream. */
|
|
126
268
|
openStream(token: string, id: string, sink: TwinStreamSink, peer: string): TwinStreamConnection | null;
|
|
@@ -129,14 +271,24 @@ export declare class WorldDoors {
|
|
|
129
271
|
private controlRoot;
|
|
130
272
|
private stateOf;
|
|
131
273
|
handle(request: Request): Promise<Response>;
|
|
274
|
+
/** THE MIRROR MOUNT: `/<served>/<vendor>/mirror/` and `…/mirror/assets/<file>`, keyless and GET only —
|
|
275
|
+
* the pack's own shell and client. The shell's <base> is the twin's place in this World, so the
|
|
276
|
+
* mirror reads and writes the World's wire with the browser's session (`POST …/session`); any
|
|
277
|
+
* other path under mirror/ is one of the mirror's own routes (a reloaded deep link) and gets the
|
|
278
|
+
* shell. A link is not a twin and has no mirror. */
|
|
279
|
+
private mirror;
|
|
280
|
+
/** A vendor's screens' sign-in (`signIn.as`), once per account every twelve hours while this World is served (rotating the
|
|
281
|
+
* tokens forgets them): the pack finds the account and mints its credential through the twin's own doors; a failure
|
|
282
|
+
* is not kept, so the next page asks again. An account or credential that is not plain printable text is refused. */
|
|
283
|
+
private readonly signIns;
|
|
284
|
+
private readonly signInAt;
|
|
285
|
+
private signedIn;
|
|
286
|
+
/** Whether a twin enforces a read-scope request itself (its manifest names `requestScopes: ['read']`):
|
|
287
|
+
* only then may the read token reach it with anything but GET or HEAD. Asked once per twin. */
|
|
288
|
+
private readonly readEnforced;
|
|
289
|
+
private enforcesRead;
|
|
132
290
|
/** The vendor wire: `/<org>/<world>/<vendor>/<rest>` → the twin's own URL. */
|
|
133
291
|
private wire;
|
|
134
|
-
/** Whether the twin's pack ships a mirror (its vendor's UI over this World's state). */
|
|
135
|
-
private hasMirror;
|
|
136
|
-
/** THE MIRROR MOUNT, as the hosted World serves it (apps/cloud supervisor.ts): `/<org>/<world>/<vendor>/mirror/`
|
|
137
|
-
* and `…/mirror/assets/*`, keyless, GET only — the pack's own shell and bundle. The shell's <base> is the
|
|
138
|
-
* twin's place under this World, so its reads go to the keyed wire with the browser's World session. */
|
|
139
|
-
private mirror;
|
|
140
292
|
private linksPath;
|
|
141
293
|
/** The World's links: name → the vendor origin it forwards to. */
|
|
142
294
|
links(): Record<string, {
|
|
@@ -151,6 +303,41 @@ export declare class WorldDoors {
|
|
|
151
303
|
private observedAt;
|
|
152
304
|
/** The world's own doors under `/-/<org>/<world>/`. */
|
|
153
305
|
private door;
|
|
306
|
+
/** A browser session for this World: the presented token as an HttpOnly cookie scoped to the World's
|
|
307
|
+
* paths, so a page served under it (a twin's mirror) reads the wire as the token's holder, in the
|
|
308
|
+
* token's scope. */
|
|
309
|
+
/** THE LOCAL SESSION: a World served on this machine's loopback never asks the person at it for a token. Any
|
|
310
|
+
* website can send requests to 127.0.0.1, so what stands in for the token is proof the request is this World's own
|
|
311
|
+
* page, as Vite and Jupyter check their dev servers: the Host is a loopback name (a rebound DNS name is not, so DNS
|
|
312
|
+
* rebinding fails), the browser's Origin is exactly this origin (another site's page cannot claim it, so CSRF
|
|
313
|
+
* fails), and the fetch says it is same-origin. A process on this machine can claim all three: a local World
|
|
314
|
+
* trusts this machine's user, as a local dev server does. The session is the write token's. A World with no origin
|
|
315
|
+
* of its own (a shared origin, where every World's pages are one origin) is not trusted so: its page asks for a
|
|
316
|
+
* token. */
|
|
317
|
+
private localPage;
|
|
318
|
+
private localSession;
|
|
319
|
+
private session;
|
|
320
|
+
/** THE CLOCK: the frozen instant every twin stamps from, kept in the World's store. It moves only
|
|
321
|
+
* forward once the World has entries (a twin's catch-up stamps each move at its due time, so a
|
|
322
|
+
* clock set back would put new entries before old ones); the past is reached by branching. */
|
|
323
|
+
private clockDoor;
|
|
324
|
+
/** THE BRANCHES DOORS: list (read), make one as of an instant, remove one (write). The host makes
|
|
325
|
+
* and removes them; a host that makes none answers 404, and a view offers no "as of". */
|
|
326
|
+
private branchesDoor;
|
|
327
|
+
/** The newest entry's instant across the World's twins (ms), or null when it has none. */
|
|
328
|
+
private newestEntry;
|
|
329
|
+
/** THE MAP: every twin and what it holds, by resource type, and whether it has a mirror. */
|
|
330
|
+
private map;
|
|
331
|
+
/** THE TIMELINE: the entries of every twin's log merged, newest first, by each entry's instant, then
|
|
332
|
+
* twin, then position. The World keeps no order across twins finer than its clock, and this claims
|
|
333
|
+
* none. `before` is the cursor the previous page answered; `twin` and `trace` narrow it. */
|
|
334
|
+
private timeline;
|
|
335
|
+
/** Each twin's timeline rows, kept until its logs change: a poll of an unchanged World reads no log
|
|
336
|
+
* (a twin is read again only when the size or time of its parent log, branch log or branch record
|
|
337
|
+
* moved), and the merged order is sorted again only when a twin was. */
|
|
338
|
+
private readonly timelineByTwin;
|
|
339
|
+
private timelineMerged;
|
|
340
|
+
private timelineRows;
|
|
154
341
|
/** PUSH: see `landChangeset` — the door hands the body to it. */
|
|
155
342
|
private push;
|
|
156
343
|
}
|