@volter/world-runtime 2.0.1 → 2.0.3
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 +26 -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 +22 -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/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/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
|
@@ -981,7 +981,7 @@ async function main() {
|
|
|
981
981
|
// VOLTER_WORLD names the World to the injector: a request addressed to its paths carries its key
|
|
982
982
|
const commandEnv = { ...process.env, ...env, ...bridged.env, VOLTER_WORLD: resolved.ref };
|
|
983
983
|
commandEnv.VOLTER_TWIN_INJECT_QUIET = head.includes('--verbose') ? '0' : commandEnv.VOLTER_TWIN_INJECT_QUIET ?? '1';
|
|
984
|
-
const child = spawn(command[0], command.slice(1), { env: commandEnv, stdio: 'inherit' });
|
|
984
|
+
const child = spawn(command[0], command.slice(1), { windowsHide: true, env: commandEnv, stdio: 'inherit' });
|
|
985
985
|
const code = await new Promise((resolveExit) => { child.on('exit', (status) => resolveExit(status ?? 1)); child.on('error', () => resolveExit(127)); });
|
|
986
986
|
// a client that wrote and exited at once still has bytes on their way through a bridge
|
|
987
987
|
await drainStreams();
|
|
@@ -1007,7 +1007,7 @@ async function main() {
|
|
|
1007
1007
|
const compose = splitDockerComposeArgs(command);
|
|
1008
1008
|
if (compose) {
|
|
1009
1009
|
// the consumer's own service names, from its own files, through its own docker
|
|
1010
|
-
const listed = spawnSync(compose.head[0], [...compose.head.slice(1), ...compose.composeFlags, 'config', '--services'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1010
|
+
const listed = spawnSync(compose.head[0], [...compose.head.slice(1), ...compose.composeFlags, 'config', '--services'], { windowsHide: true, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1011
1011
|
if (listed.status !== 0)
|
|
1012
1012
|
throw new Error(`volter-world attach --via reflect: docker compose config --services failed:\n${listed.stderr.toString().trim()}`);
|
|
1013
1013
|
const services = listed.stdout.toString().split('\n').map((s) => s.trim()).filter(Boolean);
|
|
@@ -1016,7 +1016,7 @@ async function main() {
|
|
|
1016
1016
|
toRun = dockerComposeWithOverride(command, overridePath);
|
|
1017
1017
|
process.stderr.write(`volter-world attach --via reflect: ${services.length} service(s) attached — dns ${manifest.resolverIp}:53, front ${manifest.targetIp}:443, CA ${ATTACHED_CA_PATH}\n`);
|
|
1018
1018
|
}
|
|
1019
|
-
const result = spawnSync(toRun[0], toRun.slice(1), { env, stdio: 'inherit' });
|
|
1019
|
+
const result = spawnSync(toRun[0], toRun.slice(1), { windowsHide: true, env, stdio: 'inherit' });
|
|
1020
1020
|
process.exit(result.status ?? 1);
|
|
1021
1021
|
}
|
|
1022
1022
|
if (via !== 'env') {
|
|
@@ -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
|
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";
|
package/dist/src/infra-cli.js
CHANGED
|
@@ -23,7 +23,7 @@ if (!existsSync(definition)) {
|
|
|
23
23
|
process.exit(2);
|
|
24
24
|
}
|
|
25
25
|
const base = ['compose', '-f', definition];
|
|
26
|
-
const run = (args) => spawnSync('docker', [...base, ...args], {
|
|
26
|
+
const run = (args) => spawnSync('docker', [...base, ...args], { windowsHide: true,
|
|
27
27
|
encoding: 'utf8',
|
|
28
28
|
env: { ...process.env, VOLTER_WORLD_DATA: worldData },
|
|
29
29
|
timeout: 120_000,
|
|
@@ -57,7 +57,7 @@ function selectBacking() {
|
|
|
57
57
|
process.stderr.write(`managed infrastructure: VOLTER_WORLD_INFRA_BACKING must be docker or pglite (got ${JSON.stringify(forced)})\n`);
|
|
58
58
|
process.exit(2);
|
|
59
59
|
}
|
|
60
|
-
const probe = spawnSync('docker', ['info', '--format', '{{.ServerVersion}}'], { encoding: 'utf8', timeout: 10_000 });
|
|
60
|
+
const probe = spawnSync('docker', ['info', '--format', '{{.ServerVersion}}'], { windowsHide: true, encoding: 'utf8', timeout: 10_000 });
|
|
61
61
|
if (probe.status === 0)
|
|
62
62
|
return 'docker';
|
|
63
63
|
// Absence of a container runtime (no binary, no daemon) is a capability
|
package/dist/src/init.d.ts
CHANGED
|
@@ -214,7 +214,7 @@ export declare function renderEnvFile(plan: InitPlan): string;
|
|
|
214
214
|
/** Write the plan's two artifacts. Byte-identical for the same repo + catalog: sorted keys, no
|
|
215
215
|
* timestamps, no absolute paths inside the files. */
|
|
216
216
|
/** What `.volter/.gitignore` says: the story is committed, the running state never is. */
|
|
217
|
-
export declare const STATE_GITIGNORE = "# Volter: the world's running state and live env \u2014 never committed. world.json, handlers/ and seeds/ are the story.\nworlds/\ncurrent\n*.env\ncredentials/\ntoken\n";
|
|
217
|
+
export declare const STATE_GITIGNORE = "# Volter: the world's running state and live env \u2014 never committed. world.json, handlers/ and seeds/ are the story.\nworlds/\ncurrent\n*.env\ncredentials/\ntoken\ntoken.read\nsessions.json\nkeys.json\n";
|
|
218
218
|
export declare function writeWorldInit(plan: InitPlan, options?: {
|
|
219
219
|
force?: boolean;
|
|
220
220
|
}): void;
|
package/dist/src/init.js
CHANGED
|
@@ -632,6 +632,29 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
632
632
|
const mintedEnv = [];
|
|
633
633
|
const infraSignals = new Map();
|
|
634
634
|
const infraEntries = [];
|
|
635
|
+
// SMTP's host and port under the names this app declares: apps name them their own way (twenty's EMAIL_SMTP_HOST/
|
|
636
|
+
// EMAIL_SMTP_PORT beside nodemailer's SMTP_HOST), and a raw-TCP twin is reached only through the env the app reads,
|
|
637
|
+
// so every declared `…SMTP_HOST` / `…SMTP_PORT` points at the smtp twin (twenty lane: its mail went to the log). A World
|
|
638
|
+
// regenerated with --force keeps its saved smtp service as it was: remove it from the config to pick these names up
|
|
639
|
+
const smtp = services.find((service) => service.id === 'smtp' && service.type === 'twin');
|
|
640
|
+
if (smtp) {
|
|
641
|
+
const templates = { ...(smtp.injectEnvTemplates ?? {}) };
|
|
642
|
+
// not the app's outbound mail: a server it listens on (INBOUND_) or a local catcher it runs (MAILHOG_, MAILPIT_)
|
|
643
|
+
const notOutbound = /^(?:INBOUND|INCOMING|MAILHOG|MAILPIT|MAILCATCHER|MAILDEV)_/;
|
|
644
|
+
for (const { name } of repoEnvEntries(repo)) {
|
|
645
|
+
if (name in templates || notOutbound.test(name))
|
|
646
|
+
continue;
|
|
647
|
+
if (/^(?:[A-Z0-9]+_)*SMTP_HOST$/.test(name))
|
|
648
|
+
templates[name] = '${host}';
|
|
649
|
+
else if (/^(?:[A-Z0-9]+_)*SMTP_PORT$/.test(name))
|
|
650
|
+
templates[name] = '${port}';
|
|
651
|
+
// the twin speaks plain SMTP on its port: a secure/TLS flag beside an injected host would make the app open
|
|
652
|
+
// implicit TLS to it and fail, so the flag says plain
|
|
653
|
+
else if (/^(?:[A-Z0-9]+_)*SMTP_(?:SECURE|TLS|SSL)$/.test(name))
|
|
654
|
+
templates[name] = 'false';
|
|
655
|
+
}
|
|
656
|
+
smtp.injectEnvTemplates = templates;
|
|
657
|
+
}
|
|
635
658
|
// Every name a twin service above injects at boot (its endpoint env and templates).
|
|
636
659
|
const worldInjected = new Set([
|
|
637
660
|
...(existing?.services ?? []),
|
|
@@ -963,6 +986,9 @@ current
|
|
|
963
986
|
*.env
|
|
964
987
|
credentials/
|
|
965
988
|
token
|
|
989
|
+
token.read
|
|
990
|
+
sessions.json
|
|
991
|
+
keys.json
|
|
966
992
|
`;
|
|
967
993
|
export function writeWorldInit(plan, options = {}) {
|
|
968
994
|
// A world already here is refused unless forced; a `.volter/` holding only running state
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type BranchDoors, type BranchRow, type MountedWorld } from './served-world.js';
|
|
2
|
+
export type LocalBranchesOptions = {
|
|
3
|
+
/** where branches live: `<dir>/<org>/<world>/` */
|
|
4
|
+
dir: string;
|
|
5
|
+
/** the URL a branch reaches its parent at (the host's own listener, on loopback) */
|
|
6
|
+
origin: () => string;
|
|
7
|
+
/** the Worlds the host serves, by served name; branches join it while they live */
|
|
8
|
+
worlds: Map<string, MountedWorld>;
|
|
9
|
+
/** mount a World at a root, with its branches doors wired */
|
|
10
|
+
mount: (root: string) => Promise<MountedWorld>;
|
|
11
|
+
announce?: (line: string) => void;
|
|
12
|
+
};
|
|
13
|
+
export declare class LocalBranches {
|
|
14
|
+
private readonly o;
|
|
15
|
+
private sweeper;
|
|
16
|
+
constructor(o: LocalBranchesOptions);
|
|
17
|
+
private record;
|
|
18
|
+
/** The branches of `parent` this host holds. */
|
|
19
|
+
list(parent: string): BranchRow[];
|
|
20
|
+
/** Branches being made, by parent: a parent is not removed mid-clone. */
|
|
21
|
+
private readonly making;
|
|
22
|
+
hasBranches(parent: string): boolean;
|
|
23
|
+
/** A World's branches doors, answered by this host. */
|
|
24
|
+
doorsFor(parent: string): BranchDoors;
|
|
25
|
+
private create;
|
|
26
|
+
private make;
|
|
27
|
+
/** The parent's frozen clock, when it has one. */
|
|
28
|
+
private frozenClock;
|
|
29
|
+
/** Stop a branch and remove its World (a branch's own branches first). */
|
|
30
|
+
remove(name: string): Promise<void>;
|
|
31
|
+
/** Remove the branches whose time ran out, now and every minute while the host serves. */
|
|
32
|
+
resume(): Promise<void>;
|
|
33
|
+
/** One sweep at a time: a slow one never overlaps the next. */
|
|
34
|
+
private sweeping;
|
|
35
|
+
private sweep;
|
|
36
|
+
stop(): void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// LOCAL BRANCHES: the branches a local host makes of the Worlds it serves (docs/contributing/architecture.md,
|
|
2
|
+
// "Viewing a World"). A branch is another World: the parent's config under a new name, cloned from the
|
|
3
|
+
// parent through its own doors (its history cut at an instant, when one is asked), its clock frozen at
|
|
4
|
+
// that instant so no twin's catch-up walks it forward, and removed when its time runs out. world-host
|
|
5
|
+
// and `volter world view` both make them with this; a hosted World's supervisor makes its own.
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { stateDirName, withStateRemoval } from '@volter/world-core';
|
|
9
|
+
import { clockFile } from "./runtime.js";
|
|
10
|
+
import { loadWorldConfig } from "./configs.js";
|
|
11
|
+
import { TOKEN_HEADER } from "./served-world.js";
|
|
12
|
+
const RECORD = 'branch.json';
|
|
13
|
+
const SWEEP_MS = 60_000;
|
|
14
|
+
/** A World's manifest under another name, in the manifest's own format: format 2 names a World in
|
|
15
|
+
* `metadata.id` and serves it bare at `serving.name`; format 1 in `id` and `bare.name`. */
|
|
16
|
+
function renamed(config, id, name) {
|
|
17
|
+
if (config.schemaVersion === 2) {
|
|
18
|
+
return { ...config, metadata: { ...config.metadata, id }, serving: { ...config.serving, mode: 'bare', name } };
|
|
19
|
+
}
|
|
20
|
+
return { ...config, id, bare: { ...config.bare, name } };
|
|
21
|
+
}
|
|
22
|
+
export class LocalBranches {
|
|
23
|
+
o;
|
|
24
|
+
sweeper = null;
|
|
25
|
+
constructor(o) {
|
|
26
|
+
this.o = o;
|
|
27
|
+
}
|
|
28
|
+
record(world) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(readFileSync(join(world.root, stateDirName(), RECORD), 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The branches of `parent` this host holds. */
|
|
37
|
+
list(parent) {
|
|
38
|
+
const rows = [];
|
|
39
|
+
for (const [name, world] of this.o.worlds) {
|
|
40
|
+
const r = this.record(world);
|
|
41
|
+
if (r?.from === parent)
|
|
42
|
+
rows.push({ name, from: r.from, at: r.at, createdAt: r.createdAt, expiresAt: r.expiresAt });
|
|
43
|
+
}
|
|
44
|
+
return rows.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
45
|
+
}
|
|
46
|
+
/** Branches being made, by parent: a parent is not removed mid-clone. */
|
|
47
|
+
making = new Map();
|
|
48
|
+
hasBranches(parent) { return (this.making.get(parent) ?? 0) > 0 || this.list(parent).length > 0; }
|
|
49
|
+
/** A World's branches doors, answered by this host. */
|
|
50
|
+
doorsFor(parent) {
|
|
51
|
+
return {
|
|
52
|
+
list: async () => this.list(parent),
|
|
53
|
+
create: (at, ttl, _origin, parentKey) => this.create(parent, at, ttl, parentKey),
|
|
54
|
+
remove: async (name) => { const w = this.o.worlds.get(name); if (!w || this.record(w)?.from !== parent)
|
|
55
|
+
return false; await this.remove(name); return true; },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async create(parent, at, ttlSeconds, parentKey) {
|
|
59
|
+
this.making.set(parent, (this.making.get(parent) ?? 0) + 1);
|
|
60
|
+
try {
|
|
61
|
+
return await this.make(parent, at, ttlSeconds, parentKey);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
const left = (this.making.get(parent) ?? 1) - 1;
|
|
65
|
+
if (left > 0)
|
|
66
|
+
this.making.set(parent, left);
|
|
67
|
+
else
|
|
68
|
+
this.making.delete(parent);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async make(parent, at, ttlSeconds, parentKey) {
|
|
72
|
+
const from = this.o.worlds.get(parent);
|
|
73
|
+
if (!from)
|
|
74
|
+
throw new Error(`no world ${parent} here`);
|
|
75
|
+
const [org, world] = parent.split('/');
|
|
76
|
+
const stamp = at.instant ? at.instant.replace(/[-:]/g, '').replace(/\.\d+Z$|Z$/, '').toLowerCase().slice(0, 13) : 'now';
|
|
77
|
+
// a labelled branch (a pull request's preview) is named for its label; the random tail keeps a replacement distinct
|
|
78
|
+
// a name already taken (a 4-hex tail's rare clash) is passed over for a fresh tail, never made over
|
|
79
|
+
const free = () => {
|
|
80
|
+
for (let tries = 0; tries < 8; tries++) {
|
|
81
|
+
const suffix = `${at.label ? `-${at.label}` : `-at-${stamp}`}-${Buffer.from(crypto.getRandomValues(new Uint8Array(2))).toString('hex')}`;
|
|
82
|
+
const id = `${world.slice(0, 64 - suffix.length)}${suffix}`;
|
|
83
|
+
const name = `${org}/${id}`;
|
|
84
|
+
const root = join(this.o.dir, org, id);
|
|
85
|
+
if (!this.o.worlds.has(name) && !existsSync(root))
|
|
86
|
+
return { id, name, root };
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
const picked = free();
|
|
91
|
+
if (!picked)
|
|
92
|
+
throw new Error(`no free name for a branch of ${parent}`);
|
|
93
|
+
const { id, name, root } = picked;
|
|
94
|
+
// the parent's config under the branch's own name: the same twins, served at the branch's place
|
|
95
|
+
mkdirSync(join(root, stateDirName()), { recursive: true });
|
|
96
|
+
writeFileSync(join(root, stateDirName(), 'world.json'), `${JSON.stringify(renamed(JSON.parse(readFileSync(join(from.root, stateDirName(), 'world.json'), 'utf8')), id, name), null, 2)}\n`);
|
|
97
|
+
let mounted;
|
|
98
|
+
try {
|
|
99
|
+
mounted = await this.o.mount(root);
|
|
100
|
+
await mounted.boot(this.o.origin());
|
|
101
|
+
this.o.worlds.set(name, mounted);
|
|
102
|
+
const origin = this.o.origin();
|
|
103
|
+
// each twin's history cut at the instant, by the parent's own history door
|
|
104
|
+
let views;
|
|
105
|
+
if (at.instant) {
|
|
106
|
+
const cut = await from.handle(new Request(`${origin}/-/${parent}/history?at=${encodeURIComponent(at.instant)}`, { headers: { [TOKEN_HEADER]: from.token } }));
|
|
107
|
+
if (!cut.ok)
|
|
108
|
+
throw new Error(`the history of ${parent} at ${at.instant}: ${await cut.text()}`);
|
|
109
|
+
views = (await cut.json()).views;
|
|
110
|
+
}
|
|
111
|
+
const cloned = await mounted.handle(new Request(`${origin}/-/${name}/origin`, { method: 'PUT', headers: { [TOKEN_HEADER]: mounted.token, 'content-type': 'application/json' }, body: JSON.stringify({ url: `${origin}/${parent}`, token: parentKey ?? from.token, ...(views ? { views } : {}) }) })); // the branch's own key to its parent (the parent's doors make it), never the parent's token
|
|
112
|
+
if (!cloned.ok)
|
|
113
|
+
throw new Error(`cloning ${parent}: ${await cloned.text()}`);
|
|
114
|
+
// the branch's clock stands at its instant, else at the parent's: no twin's catch-up walks it on
|
|
115
|
+
// (a parent whose clock was never set: the branch's stands at now, frozen there, as http-api says);
|
|
116
|
+
// a live branch (a pull request's preview) keeps the parent's time: real time, or its simulated clock
|
|
117
|
+
const clock = at.instant ?? this.frozenClock(from) ?? (at.live ? null : new Date().toISOString());
|
|
118
|
+
if (clock) {
|
|
119
|
+
const file = clockFile(root, id);
|
|
120
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
121
|
+
writeFileSync(file, `${clock}\n`);
|
|
122
|
+
}
|
|
123
|
+
const createdAt = new Date().toISOString();
|
|
124
|
+
const expiresAt = ttlSeconds === null ? null : new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
125
|
+
writeFileSync(join(root, stateDirName(), RECORD), `${JSON.stringify({ from: parent, at: at.instant ? { instant: at.instant } : null, createdAt, expiresAt }, null, 2)}\n`);
|
|
126
|
+
this.o.announce?.(`branch ${name} of ${parent}${at.instant ? ` as of ${at.instant}` : ''}${expiresAt ? `, until ${expiresAt}` : ''}`);
|
|
127
|
+
return { name, token: mounted.token, readToken: mounted.readToken, expiresAt };
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
this.o.worlds.delete(name);
|
|
131
|
+
try {
|
|
132
|
+
await mounted?.stop();
|
|
133
|
+
}
|
|
134
|
+
catch { /* reported below */ }
|
|
135
|
+
withStateRemoval(root, () => rmSync(root, { recursive: true, force: true }));
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** The parent's frozen clock, when it has one. */
|
|
140
|
+
frozenClock(world) {
|
|
141
|
+
try {
|
|
142
|
+
const held = readFileSync(clockFile(world.root, loadWorldConfig(join(world.root, stateDirName(), 'world.json'), world.root).config.id), 'utf8').trim();
|
|
143
|
+
return Number.isNaN(Date.parse(held)) ? null : held;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Stop a branch and remove its World (a branch's own branches first). */
|
|
150
|
+
async remove(name) {
|
|
151
|
+
const world = this.o.worlds.get(name);
|
|
152
|
+
if (!world)
|
|
153
|
+
return;
|
|
154
|
+
for (const child of this.list(name))
|
|
155
|
+
await this.remove(child.name);
|
|
156
|
+
await world.stop();
|
|
157
|
+
withStateRemoval(world.root, () => rmSync(world.root, { recursive: true, force: true }));
|
|
158
|
+
this.o.worlds.delete(name);
|
|
159
|
+
this.o.announce?.(`removed ${name}`);
|
|
160
|
+
}
|
|
161
|
+
/** Remove the branches whose time ran out, now and every minute while the host serves. */
|
|
162
|
+
async resume() {
|
|
163
|
+
await this.sweep();
|
|
164
|
+
this.sweeper = setInterval(() => { void this.sweep(); }, SWEEP_MS);
|
|
165
|
+
this.sweeper.unref?.();
|
|
166
|
+
}
|
|
167
|
+
/** One sweep at a time: a slow one never overlaps the next. */
|
|
168
|
+
sweeping = false;
|
|
169
|
+
async sweep() {
|
|
170
|
+
if (this.sweeping)
|
|
171
|
+
return;
|
|
172
|
+
this.sweeping = true;
|
|
173
|
+
try {
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
for (const [name, world] of [...this.o.worlds]) {
|
|
176
|
+
const r = this.record(world);
|
|
177
|
+
if (r?.expiresAt && Date.parse(r.expiresAt) <= now && this.o.worlds.has(name)) {
|
|
178
|
+
try {
|
|
179
|
+
await this.remove(name);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
this.o.announce?.(`branch ${name}: removal failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
this.sweeping = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
stop() { if (this.sweeper)
|
|
192
|
+
clearInterval(this.sweeper); this.sweeper = null; }
|
|
193
|
+
}
|
|
@@ -68,8 +68,10 @@ export function infraConnections(services, env) {
|
|
|
68
68
|
}
|
|
69
69
|
// this module's directory under Node as under Bun: `volter-world-infra` is a node-shebang bin, where
|
|
70
70
|
// `import.meta.dir` (Bun's) is undefined
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// lazy: a module that builds a URL from import.meta.url when it loads fails to load in workerd, which bundles this
|
|
72
|
+
// module into the hosted World (it never runs these services there)
|
|
73
|
+
const here = () => fileURLToPath(new URL('.', import.meta.url));
|
|
74
|
+
const hostScript = () => join(here(), 'pglite-host.mjs');
|
|
73
75
|
const READY_TIMEOUT_MS = 60_000;
|
|
74
76
|
const MONGODB_TWIN = '@volter/twin-mongodb';
|
|
75
77
|
/** The kinds this backing serves, and the name its files carry under the data dir. */
|
|
@@ -79,7 +81,7 @@ function pidPath(dataDir, kind) {
|
|
|
79
81
|
}
|
|
80
82
|
/** The MongoDB twin's cli: the package installed above the World's config (an app repo), else the one
|
|
81
83
|
* this runtime was installed with (its dependency, or the checkout's workspace link). */
|
|
82
|
-
export function mongodbTwinCli(worldConfig, froms = [...(worldConfig ? [dirname(worldConfig)] : []),
|
|
84
|
+
export function mongodbTwinCli(worldConfig, froms = [...(worldConfig ? [dirname(worldConfig)] : []), here()]) {
|
|
83
85
|
for (const from of froms) {
|
|
84
86
|
const dir = findInstalledPackage(from, MONGODB_TWIN);
|
|
85
87
|
const cli = dir === undefined ? undefined : packCli(dir);
|
|
@@ -92,7 +94,7 @@ export function mongodbTwinCli(worldConfig, froms = [...(worldConfig ? [dirname(
|
|
|
92
94
|
* process's own runtime; a checkout's is TypeScript whose kernel needs more than Node's type
|
|
93
95
|
* stripping, so it runs under Bun (this process's, or the one on PATH) — the repository's toolchain —
|
|
94
96
|
* and without Bun it is refused by name rather than started to fail. */
|
|
95
|
-
export function twinRunner(cli, bunOnPath = () => spawnSync('bun', ['--version'], { stdio: 'ignore', timeout: 10_000 }).status === 0, underBun = typeof globalThis.Bun !== 'undefined') {
|
|
97
|
+
export function twinRunner(cli, bunOnPath = () => spawnSync('bun', ['--version'], { stdio: 'ignore', timeout: 10_000, windowsHide: true }).status === 0, underBun = typeof globalThis.Bun !== 'undefined') {
|
|
96
98
|
if (!cli.endsWith('.ts') || underBun)
|
|
97
99
|
return process.execPath;
|
|
98
100
|
if (bunOnPath())
|
|
@@ -105,7 +107,7 @@ function hostArgv(service, serviceData) {
|
|
|
105
107
|
const cli = mongodbTwinCli(process.env.VOLTER_WORLD_CONFIG);
|
|
106
108
|
return [twinRunner(cli), cli, 'serve', '--port', String(service.hostPort), '--root', serviceData];
|
|
107
109
|
}
|
|
108
|
-
return ['node',
|
|
110
|
+
return ['node', hostScript(), '--port', String(service.hostPort), '--data', serviceData];
|
|
109
111
|
}
|
|
110
112
|
function alive(pid) {
|
|
111
113
|
try {
|
|
@@ -169,6 +171,7 @@ export async function pgliteUp(services, dataDir) {
|
|
|
169
171
|
// a real fd, not a pipe: the host must outlive this process untethered
|
|
170
172
|
const logFd = openSync(log, 'a');
|
|
171
173
|
const child = spawn(command, args, {
|
|
174
|
+
windowsHide: true,
|
|
172
175
|
detached: true,
|
|
173
176
|
stdio: ['ignore', logFd, logFd],
|
|
174
177
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
const realRunner = (cmd, args) => {
|
|
3
|
-
const result = spawnSync(cmd, args, { encoding: 'utf8' });
|
|
3
|
+
const result = spawnSync(cmd, args, { windowsHide: true, encoding: 'utf8' });
|
|
4
4
|
return {
|
|
5
5
|
status: result.status,
|
|
6
6
|
stdout: result.stdout || '',
|
|
@@ -22,7 +22,7 @@ export function survivingOwnedGroups(pids) {
|
|
|
22
22
|
});
|
|
23
23
|
if (!candidates.length || process.platform === 'win32')
|
|
24
24
|
return candidates;
|
|
25
|
-
const result = spawnSync('ps', ['-axo', 'pid=,pgid=,stat='], {
|
|
25
|
+
const result = spawnSync('ps', ['-axo', 'pid=,pgid=,stat='], { windowsHide: true,
|
|
26
26
|
encoding: 'utf8', timeout: 1000, killSignal: 'SIGKILL', maxBuffer: 4 * 1024 * 1024,
|
|
27
27
|
});
|
|
28
28
|
if (result.status !== 0 || result.error)
|
|
@@ -199,11 +199,11 @@ export function opensslAvailable() {
|
|
|
199
199
|
// this env var in real usage.
|
|
200
200
|
if (process.env.VOLTER_TEST_NO_OPENSSL === '1')
|
|
201
201
|
return false;
|
|
202
|
-
const r = spawnSync('openssl', ['version'], { stdio: 'ignore' });
|
|
202
|
+
const r = spawnSync('openssl', ['version'], { windowsHide: true, stdio: 'ignore' });
|
|
203
203
|
return r.status === 0;
|
|
204
204
|
}
|
|
205
205
|
function runOpenssl(args) {
|
|
206
|
-
const r = spawnSync('openssl', args, { encoding: 'utf8' });
|
|
206
|
+
const r = spawnSync('openssl', args, { windowsHide: true, encoding: 'utf8' });
|
|
207
207
|
if (r.status !== 0)
|
|
208
208
|
throw new Error(`openssl ${args[0]} failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
209
209
|
return r.stdout;
|
package/dist/src/root.d.ts
CHANGED
|
@@ -36,6 +36,29 @@ export declare function rootForControlRoot(controlRoot: string, service: string)
|
|
|
36
36
|
/** The pack's mirror module (`src/<vendor>-mirror-ui.ts`: its shell, client and stylesheet), read as the
|
|
37
37
|
* hosted mirror build reads it (apps/cloud/scripts/mirrors.ts); a pack's index need not re-export them. */
|
|
38
38
|
export declare function packMirrorExports(vendor: string, from: string): Promise<Record<string, unknown> | null>;
|
|
39
|
+
/** A pack's mirror as the pack builds it (`<vendor>MirrorHtml`, `build<Vendor>MirrorClient`,
|
|
40
|
+
* `<vendor>MirrorStyles`), found the way a World finds the pack; null when the pack has none. The
|
|
41
|
+
* client builds at serve under Bun and is served prebuilt under Node, by the pack's own builder. */
|
|
42
|
+
export type PackMirror = {
|
|
43
|
+
html: () => string;
|
|
44
|
+
client: () => Promise<string>;
|
|
45
|
+
styles?: () => Promise<string>; /** the account and a credential its screens sign in with (`<vendor>MirrorSignIn`): the account found in the twin's
|
|
46
|
+
* state at `root`, the credential minted for it through the twin's own doors (`twin`), never a stored one read back */
|
|
47
|
+
signIn?: (as: string, ctx: MirrorSignInContext) => Promise<{
|
|
48
|
+
account: string;
|
|
49
|
+
token: string;
|
|
50
|
+
} | null> | {
|
|
51
|
+
account: string;
|
|
52
|
+
token: string;
|
|
53
|
+
} | null;
|
|
54
|
+
};
|
|
55
|
+
/** What a pack's `<vendor>MirrorSignIn` is given: the twin's state root (to find the account) and the twin's own doors
|
|
56
|
+
* (to mint a credential for it, as the World's seed does through `/_twin/…`). */
|
|
57
|
+
export type MirrorSignInContext = {
|
|
58
|
+
root: string;
|
|
59
|
+
twin: (path: string, init?: RequestInit) => Promise<Response>;
|
|
60
|
+
};
|
|
61
|
+
export declare function packMirror(vendor: string, from: string): Promise<PackMirror | null>;
|
|
39
62
|
/** The real state system's adapters for a vendor: registered with the pack (protocol 2), else the
|
|
40
63
|
* pack's anchored exports (protocol 1: `perform<Name>Action`, `sync<Name>FromRemote`). */
|
|
41
64
|
export type IngestAdapter = (request: Request, ctx: {
|