@volter/world-runtime 3.0.30 → 3.0.32
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/branch.js +3 -0
- package/dist/src/browsers.d.ts +29 -0
- package/dist/src/browsers.js +180 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/init.d.ts +1 -1
- package/dist/src/init.js +36 -4
- package/dist/src/local-branches.js +5 -1
- package/dist/src/pack-facts.d.ts +4 -0
- package/dist/src/pack-facts.js +14 -0
- package/dist/src/placeholder.js +16 -2
- package/dist/src/redirect-proxy.js +3 -3
- package/dist/src/reflect.js +1 -2
- package/dist/src/runtime.js +9 -2
- package/dist/src/served-world.d.ts +12 -9
- package/dist/src/served-world.js +406 -189
- package/dist/src/world-origins.js +1 -1
- package/dist/src/world-view.js +5 -1
- package/package.json +4 -4
- package/src/branch.ts +3 -0
- package/src/browsers.ts +146 -0
- package/src/index.ts +2 -0
- package/src/init.ts +36 -4
- package/src/local-branches.ts +5 -1
- package/src/pack-facts.ts +16 -0
- package/src/placeholder.ts +16 -2
- package/src/redirect-proxy.ts +3 -3
- package/src/reflect.ts +1 -2
- package/src/runtime.ts +7 -2
- package/src/served-world.ts +323 -166
- package/src/world-origins.ts +1 -1
- package/src/world-view.ts +5 -1
package/dist/src/branch.js
CHANGED
|
@@ -10,6 +10,7 @@ import { cpSync, existsSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
|
10
10
|
import { dirname, join, resolve } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { captureHistory, historyAtInstant, forkTwin, stateGeneration, withHistoryLocks, worldPaths, worldStateRoot } from '@volter/world-core/runtime';
|
|
13
|
+
import { inheritWorldBrowsers, materializeBrowserFiles } from "./browsers.js";
|
|
13
14
|
import { downWorld, saveWorldInstance, statusWorld, upWorld } from "./runtime.js";
|
|
14
15
|
import { parseInfraDefinition, pgliteDown, pgliteUp } from "./pglite-backing.js";
|
|
15
16
|
import { redisTwinCli, redisTwinDown, redisTwinUp } from "./redis-backing.js";
|
|
@@ -99,6 +100,8 @@ export async function branchWorld(base, name, options = {}) {
|
|
|
99
100
|
if (states.length)
|
|
100
101
|
forked[service] = states.map(s => s.state);
|
|
101
102
|
}
|
|
103
|
+
inheritWorldBrowsers(from.dirs.data, instance.dirs.data);
|
|
104
|
+
materializeBrowserFiles(instance.dirs.data, root, name, instance.config);
|
|
102
105
|
await branchManagedDatabases(from, instance, databases);
|
|
103
106
|
}
|
|
104
107
|
catch (error) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type BrowserState } from '@volter/world-core';
|
|
2
|
+
export type WorldBrowser = {
|
|
3
|
+
name: string;
|
|
4
|
+
state: BrowserState;
|
|
5
|
+
version: string;
|
|
6
|
+
};
|
|
7
|
+
/** Instance bookkeeping, alongside the clock; retained at down, discarded at reset. */
|
|
8
|
+
export declare const browsersDir: (data: string) => string;
|
|
9
|
+
export declare function worldBrowser(data: string, name: string): WorldBrowser | null;
|
|
10
|
+
export declare function worldBrowsers(data: string): WorldBrowser[];
|
|
11
|
+
export declare function putWorldBrowser(data: string, name: string, state: BrowserState): WorldBrowser;
|
|
12
|
+
export declare function deleteWorldBrowser(data: string, name: string): void;
|
|
13
|
+
export declare function inheritWorldBrowsers(fromData: string, toData: string): void;
|
|
14
|
+
/** Derived files under the World state directory, written only by its current branch. */
|
|
15
|
+
export declare function materializeBrowserFiles(data: string, root: string, name: string, main?: string): void;
|
|
16
|
+
export declare function clearBrowserFiles(data: string, root: string): void;
|
|
17
|
+
/** Private load-marker history; the cookie carries only an opaque id, never vendor state. */
|
|
18
|
+
export type BrowserLoad = {
|
|
19
|
+
name: string;
|
|
20
|
+
site: string;
|
|
21
|
+
version: string;
|
|
22
|
+
cookies: Array<{
|
|
23
|
+
name: string;
|
|
24
|
+
path: string;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
export declare const browserLoadId: (version: string, site: string) => string;
|
|
28
|
+
export declare function browserLoad(data: string, id: string | undefined): BrowserLoad | null;
|
|
29
|
+
export declare function putBrowserLoad(data: string, id: string, load: BrowserLoad): void;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { assertBrowserName, validateBrowserState } from '@volter/world-core';
|
|
2
|
+
import { getActiveWorldStore, stateDirName } from '@volter/world-core/runtime';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
/** Instance bookkeeping, alongside the clock; retained at down, discarded at reset. */
|
|
6
|
+
export const browsersDir = (data) => join(dirname(data), 'browser-records');
|
|
7
|
+
const path = (data, name) => { assertBrowserName(name); return join(browsersDir(data), `${name}.json`); };
|
|
8
|
+
const records = new WeakMap();
|
|
9
|
+
export function worldBrowser(data, name) {
|
|
10
|
+
const store = getActiveWorldStore();
|
|
11
|
+
const file = path(data, name);
|
|
12
|
+
const cache = records.get(store) ?? new Map();
|
|
13
|
+
records.set(store, cache);
|
|
14
|
+
const text = store.read(file);
|
|
15
|
+
if (text === null) {
|
|
16
|
+
cache.delete(file);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const held = cache.get(file);
|
|
20
|
+
if (held?.text === text)
|
|
21
|
+
return held.record;
|
|
22
|
+
const record = JSON.parse(text);
|
|
23
|
+
if (record.name !== name || typeof record.version !== 'string')
|
|
24
|
+
throw new Error(`invalid browser record: ${name}`);
|
|
25
|
+
if (held?.version === record.version) {
|
|
26
|
+
held.text = text;
|
|
27
|
+
return held.record;
|
|
28
|
+
}
|
|
29
|
+
validateBrowserState(record.state);
|
|
30
|
+
const freeze = (value) => {
|
|
31
|
+
if (!value || typeof value !== 'object')
|
|
32
|
+
return;
|
|
33
|
+
for (const item of Object.values(value))
|
|
34
|
+
freeze(item);
|
|
35
|
+
Object.freeze(value);
|
|
36
|
+
};
|
|
37
|
+
freeze(record);
|
|
38
|
+
cache.set(file, { version: record.version, text, record });
|
|
39
|
+
return record;
|
|
40
|
+
}
|
|
41
|
+
export function worldBrowsers(data) {
|
|
42
|
+
return getActiveWorldStore().list(browsersDir(data)).filter(f => f.endsWith('.json')).sort().map(f => worldBrowser(data, f.slice(0, -5)));
|
|
43
|
+
}
|
|
44
|
+
export function putWorldBrowser(data, name, state) {
|
|
45
|
+
const record = { name, state: validateBrowserState(state), version: randomUUID() };
|
|
46
|
+
getActiveWorldStore().writeAtomic(path(data, name), `${JSON.stringify(record)}\n`, { secret: true });
|
|
47
|
+
retireBrowserLoads(data, name, record);
|
|
48
|
+
return record;
|
|
49
|
+
}
|
|
50
|
+
export function deleteWorldBrowser(data, name) {
|
|
51
|
+
const store = getActiveWorldStore();
|
|
52
|
+
store.remove(path(data, name));
|
|
53
|
+
records.get(store)?.delete(path(data, name));
|
|
54
|
+
retireBrowserLoads(data, name);
|
|
55
|
+
}
|
|
56
|
+
export function inheritWorldBrowsers(fromData, toData) {
|
|
57
|
+
const store = getActiveWorldStore();
|
|
58
|
+
for (const b of worldBrowsers(fromData))
|
|
59
|
+
store.writeAtomic(path(toData, b.name), `${JSON.stringify(b)}\n`, { secret: true });
|
|
60
|
+
}
|
|
61
|
+
/** Derived files under the World state directory, written only by its current branch. */
|
|
62
|
+
export function materializeBrowserFiles(data, root, name, main = name) {
|
|
63
|
+
const store = getActiveWorldStore();
|
|
64
|
+
const stateDir = join(root, stateDirName());
|
|
65
|
+
const config = store.read(join(stateDir, 'world.json'));
|
|
66
|
+
if (config)
|
|
67
|
+
main = JSON.parse(config).id;
|
|
68
|
+
if ((store.read(join(stateDir, 'current'))?.trim() || main) !== name)
|
|
69
|
+
return;
|
|
70
|
+
const rows = worldBrowsers(data);
|
|
71
|
+
const dir = join(stateDir, 'browsers');
|
|
72
|
+
const index = join(dir, '.source.json');
|
|
73
|
+
if (!rows.length && store.read(index) === null)
|
|
74
|
+
return;
|
|
75
|
+
if (rows.length) {
|
|
76
|
+
const ignore = join(stateDir, '.gitignore');
|
|
77
|
+
const held = store.read(ignore) ?? '';
|
|
78
|
+
if (!held.split('\n').includes('browsers/'))
|
|
79
|
+
store.write(ignore, `${held.replace(/\n?$/, '\n')}browsers/\n`);
|
|
80
|
+
}
|
|
81
|
+
store.withLock(join(dir, '.lock'), () => {
|
|
82
|
+
let previous = null;
|
|
83
|
+
const text = store.read(index);
|
|
84
|
+
if (text)
|
|
85
|
+
previous = JSON.parse(text);
|
|
86
|
+
for (const name of previous?.names ?? []) {
|
|
87
|
+
assertBrowserName(name);
|
|
88
|
+
if (!rows.some(b => b.name === name))
|
|
89
|
+
store.remove(join(dir, `${name}.json`));
|
|
90
|
+
}
|
|
91
|
+
for (const b of rows)
|
|
92
|
+
store.writeAtomic(join(dir, `${b.name}.json`), `${JSON.stringify(b.state)}\n`, { secret: true });
|
|
93
|
+
if (rows.length)
|
|
94
|
+
store.writeAtomic(index, `${JSON.stringify({ data, names: rows.map(b => b.name) })}\n`, { secret: true });
|
|
95
|
+
else
|
|
96
|
+
store.remove(index);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export function clearBrowserFiles(data, root) {
|
|
100
|
+
const store = getActiveWorldStore();
|
|
101
|
+
const dir = join(root, stateDirName(), 'browsers');
|
|
102
|
+
const index = join(dir, '.source.json');
|
|
103
|
+
if (store.read(index) === null)
|
|
104
|
+
return;
|
|
105
|
+
store.withLock(join(dir, '.lock'), () => {
|
|
106
|
+
const text = store.read(index);
|
|
107
|
+
if (!text)
|
|
108
|
+
return;
|
|
109
|
+
const held = JSON.parse(text);
|
|
110
|
+
if (held.data !== data)
|
|
111
|
+
return;
|
|
112
|
+
for (const name of held.names) {
|
|
113
|
+
assertBrowserName(name);
|
|
114
|
+
store.remove(join(dir, `${name}.json`));
|
|
115
|
+
}
|
|
116
|
+
store.remove(index);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
export const browserLoadId = (version, site) => createHash('sha256').update(`${version}\0${site}`).digest('hex');
|
|
120
|
+
const loadPath = (data, id) => join(dirname(data), 'browser-loads', `${id}.json`);
|
|
121
|
+
export function browserLoad(data, id) {
|
|
122
|
+
if (!id || !/^[a-f0-9]{64}$/.test(id))
|
|
123
|
+
return null;
|
|
124
|
+
const text = getActiveWorldStore().read(loadPath(data, id));
|
|
125
|
+
return text === null ? null : JSON.parse(text);
|
|
126
|
+
}
|
|
127
|
+
export function putBrowserLoad(data, id, load) {
|
|
128
|
+
if (!/^[a-f0-9]{64}$/.test(id))
|
|
129
|
+
throw new Error('invalid browser load marker');
|
|
130
|
+
const held = browserLoad(data, id);
|
|
131
|
+
const cookies = new Map();
|
|
132
|
+
if (held?.name === load.name && held.site === load.site && held.version === load.version) {
|
|
133
|
+
for (const cookie of held.cookies)
|
|
134
|
+
cookies.set(JSON.stringify([cookie.name, cookie.path]), cookie);
|
|
135
|
+
}
|
|
136
|
+
for (const cookie of load.cookies)
|
|
137
|
+
cookies.set(JSON.stringify([cookie.name, cookie.path]), cookie);
|
|
138
|
+
getActiveWorldStore().writeAtomic(loadPath(data, id), `${JSON.stringify({ ...load, cookies: [...cookies.values()] })}\n`, { secret: true });
|
|
139
|
+
}
|
|
140
|
+
/** Retire old ids; cookie paths survive loads and deletion until carried into the name's next version. */
|
|
141
|
+
function retireBrowserLoads(data, name, current) {
|
|
142
|
+
const store = getActiveWorldStore();
|
|
143
|
+
const dir = join(dirname(data), 'browser-loads');
|
|
144
|
+
const retiredFile = join(dir, `retired-${name}.json`);
|
|
145
|
+
const prior = new Map();
|
|
146
|
+
const keep = (load) => {
|
|
147
|
+
const cookies = new Map();
|
|
148
|
+
for (const cookie of [...(prior.get(load.site)?.cookies ?? []), ...load.cookies]) {
|
|
149
|
+
cookies.set(JSON.stringify([cookie.name, cookie.path]), cookie);
|
|
150
|
+
}
|
|
151
|
+
prior.set(load.site, { ...load, cookies: [...cookies.values()] });
|
|
152
|
+
};
|
|
153
|
+
const retired = store.read(retiredFile);
|
|
154
|
+
if (retired !== null)
|
|
155
|
+
for (const load of JSON.parse(retired))
|
|
156
|
+
keep(load);
|
|
157
|
+
const removed = [];
|
|
158
|
+
for (const file of store.list(dir)) {
|
|
159
|
+
if (!/^[a-f0-9]{64}\.json$/.test(file))
|
|
160
|
+
continue;
|
|
161
|
+
const load = browserLoad(data, file.slice(0, -5));
|
|
162
|
+
if (!load || load.name !== name || load.version === current?.version)
|
|
163
|
+
continue;
|
|
164
|
+
keep(load);
|
|
165
|
+
removed.push(file);
|
|
166
|
+
}
|
|
167
|
+
// Preserve paths before removing the version records that supplied them.
|
|
168
|
+
if (current) {
|
|
169
|
+
for (const load of prior.values())
|
|
170
|
+
putBrowserLoad(data, browserLoadId(current.version, load.site), {
|
|
171
|
+
...load, version: current.version,
|
|
172
|
+
});
|
|
173
|
+
store.remove(retiredFile);
|
|
174
|
+
}
|
|
175
|
+
else if (prior.size) {
|
|
176
|
+
store.writeAtomic(retiredFile, `${JSON.stringify([...prior.values()])}\n`, { secret: true });
|
|
177
|
+
}
|
|
178
|
+
for (const file of removed)
|
|
179
|
+
store.remove(join(dir, file));
|
|
180
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -53,3 +53,4 @@ export type { ServedWorld } from './served-world.js';
|
|
|
53
53
|
export { CONSOLE_BASE, consoleRedirect, serveConsoleApart, serveConsoleFor, type ConsoleMount } from './console-apart.js';
|
|
54
54
|
export { assertWorldStateRemovable } from './ancestry.js';
|
|
55
55
|
export { withoutVaultSignIn } from './runtime.js';
|
|
56
|
+
export { materializeBrowserFiles, browsersDir, worldBrowser, worldBrowsers, putWorldBrowser, deleteWorldBrowser, inheritWorldBrowsers, type WorldBrowser } from './browsers.js';
|
package/dist/src/index.js
CHANGED
|
@@ -37,3 +37,4 @@ export { LOCAL_ORIGIN_BASE, localWorldOrigin, pagesOfHost, pagesOriginOf, pathOf
|
|
|
37
37
|
export { CONSOLE_BASE, consoleRedirect, serveConsoleApart, serveConsoleFor } from "./console-apart.js";
|
|
38
38
|
export { assertWorldStateRemovable } from "./ancestry.js";
|
|
39
39
|
export { withoutVaultSignIn } from "./runtime.js";
|
|
40
|
+
export { materializeBrowserFiles, browsersDir, worldBrowser, worldBrowsers, putWorldBrowser, deleteWorldBrowser, inheritWorldBrowsers } from "./browsers.js";
|
package/dist/src/init.d.ts
CHANGED
|
@@ -227,7 +227,7 @@ export declare function renderEnvFile(plan: InitPlan): string;
|
|
|
227
227
|
/** Write the plan's two artifacts. Byte-identical for the same repo + catalog: sorted keys, no
|
|
228
228
|
* timestamps, no absolute paths inside the files. */
|
|
229
229
|
/** What `.volter/.gitignore` says: the story is committed, the running state never is. */
|
|
230
|
-
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\npages-sessions.json\nkeys.json\n";
|
|
230
|
+
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\npages-sessions.json\nbrowsers/\nkeys.json\n";
|
|
231
231
|
export declare function writeWorldInit(plan: InitPlan, options?: {
|
|
232
232
|
force?: boolean;
|
|
233
233
|
}): void;
|
package/dist/src/init.js
CHANGED
|
@@ -1088,6 +1088,7 @@ token
|
|
|
1088
1088
|
token.read
|
|
1089
1089
|
sessions.json
|
|
1090
1090
|
pages-sessions.json
|
|
1091
|
+
browsers/
|
|
1091
1092
|
keys.json
|
|
1092
1093
|
`;
|
|
1093
1094
|
export function writeWorldInit(plan, options = {}) {
|
|
@@ -1138,17 +1139,48 @@ export function seedCallsRunner(vendor) {
|
|
|
1138
1139
|
return `// ${vendor}'s default data, as its twin's pack declares it (./${vendor}.data.ts: calls to the vendor's own
|
|
1139
1140
|
// operations), made against the twin in order. Ownership transferred on copy: both files are yours — edit or delete.
|
|
1140
1141
|
import { seed as calls } from './${vendor}.data.ts';
|
|
1142
|
+
import * as defaults from './${vendor}.data.ts';
|
|
1143
|
+
import type { SeedBrowser } from '@volter/world-core/browser';
|
|
1141
1144
|
|
|
1142
1145
|
export async function seed(): Promise<void> {
|
|
1143
1146
|
// a twin with a root is the vendor's real account: its default data is not written (VOLTER_SEED_SKIP)
|
|
1144
1147
|
if ((process.env.VOLTER_SEED_SKIP ?? '').split(',').includes('${vendor}')) return;
|
|
1145
1148
|
const base = process.env.${stem}_TWIN_URL;
|
|
1146
1149
|
if (!base) throw new Error('${stem}_TWIN_URL is not set — bring the world up and run the seed with its env (volter-world attach/run)');
|
|
1147
|
-
|
|
1148
|
-
|
|
1150
|
+
const clients = new Map<string, SeedBrowser>();
|
|
1151
|
+
const snapshots = new Map<string, Awaited<ReturnType<SeedBrowser['state']>>>();
|
|
1152
|
+
let browserHelper: typeof import('@volter/world-core/browser') | undefined;
|
|
1153
|
+
const browserCalls = ('browserSeed' in defaults ? defaults.browserSeed as typeof calls : undefined) ?? [];
|
|
1154
|
+
for (const call of [...calls, ...browserCalls]) {
|
|
1155
|
+
if (call.browser) {
|
|
1156
|
+
if (!snapshots.has(call.browser)) {
|
|
1157
|
+
const door = process.env.VOLTER_WORLD_BROWSER_DOOR;
|
|
1158
|
+
const token = process.env.VOLTER_WORLD_BROWSER_TOKEN;
|
|
1159
|
+
if (!door || !token) throw new Error('browser defaults run in a World seed with its browsers door and credential');
|
|
1160
|
+
const held = await fetch(\`\${door}/\${encodeURIComponent(call.browser)}\`, { headers: { 'x-volter-token': token } });
|
|
1161
|
+
if (!held.ok && held.status !== 404) throw new Error(\`browser \${call.browser}: \${held.status} \${await held.text()}\`);
|
|
1162
|
+
snapshots.set(call.browser, held.ok ? await held.json() as Awaited<ReturnType<SeedBrowser['state']>> : { cookies: [], origins: [] });
|
|
1163
|
+
}
|
|
1164
|
+
browserHelper ??= await import('@volter/world-core/browser').catch(error => {
|
|
1165
|
+
throw new Error('Install @volter/world-core 3.0.31 or later in this project to seed browsers.', { cause: error });
|
|
1166
|
+
});
|
|
1167
|
+
const initial = snapshots.get(call.browser)!;
|
|
1168
|
+
const hostname = new URL(call.path).hostname;
|
|
1169
|
+
if (initial.cookies.some(cookie => browserHelper!.cookieMatchesHost(cookie, hostname))
|
|
1170
|
+
|| initial.origins.some(origin => new URL(origin.origin).hostname === hostname)) continue;
|
|
1171
|
+
}
|
|
1172
|
+
const headers = { ...(call.body !== undefined ? { 'content-type': 'application/json' } : {}), ...(call.headers ?? {}) };
|
|
1173
|
+
const form = /^application\\/x-www-form-urlencoded\\b/i.test(new Headers(headers).get('content-type') ?? '');
|
|
1174
|
+
const body = call.body === undefined ? undefined : form
|
|
1175
|
+
? new URLSearchParams(Object.entries(call.body as Record<string, unknown>).map(([key, value]) => [key, String(value)]))
|
|
1176
|
+
: JSON.stringify(call.body);
|
|
1177
|
+
if (call.browser && !clients.has(call.browser)) {
|
|
1178
|
+
clients.set(call.browser, await browserHelper!.browser(call.browser));
|
|
1179
|
+
}
|
|
1180
|
+
const client = call.browser ? clients.get(call.browser)! : null;
|
|
1181
|
+
const res = await (client ? client.fetch : fetch)(client ? call.path : \`\${base.replace(/\\/$/, '')}\${call.path}\`, {
|
|
1149
1182
|
method: call.method,
|
|
1150
|
-
headers
|
|
1151
|
-
...(call.body !== undefined ? { body: JSON.stringify(call.body) } : {}),
|
|
1183
|
+
headers, ...(body === undefined ? {} : { body }),
|
|
1152
1184
|
});
|
|
1153
1185
|
if (!res.ok) throw new Error(\`${vendor} seed: \${call.method} \${call.path}: \${res.status} \${await res.text()}\`);
|
|
1154
1186
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
8
8
|
import { stateDirName, withStateRemoval } from '@volter/world-core/runtime';
|
|
9
|
-
import {
|
|
9
|
+
import { inheritWorldBrowsers, materializeBrowserFiles } from "./browsers.js";
|
|
10
|
+
import { clockFile, statusWorld } from "./runtime.js";
|
|
10
11
|
import { loadWorldConfig } from "./configs.js";
|
|
11
12
|
import { TOKEN_HEADER } from "./served-world.js";
|
|
12
13
|
const RECORD = 'branch.json';
|
|
@@ -167,6 +168,9 @@ export class LocalBranches {
|
|
|
167
168
|
mkdirSync(dirname(file), { recursive: true });
|
|
168
169
|
writeFileSync(file, `${clock}\n`);
|
|
169
170
|
}
|
|
171
|
+
const child = statusWorld(id, root);
|
|
172
|
+
inheritWorldBrowsers(statusWorld(world, from.root).dirs.data, child.dirs.data);
|
|
173
|
+
materializeBrowserFiles(child.dirs.data, root, id, child.config);
|
|
170
174
|
const createdAt = new Date().toISOString();
|
|
171
175
|
const expiresAt = ttlSeconds === null ? null : new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
172
176
|
// what it was made as (a pull request's preview is labelled and may be live)
|
package/dist/src/pack-facts.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HostPredicate } from '@volter/world-core/host-routing';
|
|
1
2
|
export type PackAdoption = {
|
|
2
3
|
sdks?: string[];
|
|
3
4
|
pypi?: string[];
|
|
@@ -108,3 +109,6 @@ export declare function overlayEndpointEnv(table: Record<string, {
|
|
|
108
109
|
injectEnvTemplates?: Record<string, string>;
|
|
109
110
|
note: string;
|
|
110
111
|
}>): void;
|
|
112
|
+
/** Facts supplied by resolved packs, compiled after the built-in facts in a routing table. */
|
|
113
|
+
export declare function overlaidPackVendors(): string[];
|
|
114
|
+
export declare function packHostRules(): Record<string, HostPredicate>;
|
package/dist/src/pack-facts.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// The overlay MUTATES the central tables at module init — every use-site keeps reading the
|
|
8
8
|
// object it always read — and a fact declared in BOTH places throws: dual declaration is the
|
|
9
9
|
// drift this migration exists to end, so it is loud everywhere, not just in a test.
|
|
10
|
+
import { createHostTable } from '@volter/world-core/host-routing';
|
|
10
11
|
import factsDocument from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
|
|
11
12
|
// world-runtime and core are sibling workspace packages; the artifact is committed,
|
|
12
13
|
// so a missing file is a broken checkout (or an unbuilt generator change), never a soft state.
|
|
@@ -106,3 +107,16 @@ export function overlayEndpointEnv(table) {
|
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
}
|
|
110
|
+
/** Facts supplied by resolved packs, compiled after the built-in facts in a routing table. */
|
|
111
|
+
export function overlaidPackVendors() { return Object.keys(overlaid); }
|
|
112
|
+
/** One routing table per immutable merged-facts version, built only for browser routing. */
|
|
113
|
+
const hostTables = new WeakMap();
|
|
114
|
+
export function packHostRules() {
|
|
115
|
+
const facts = packFacts();
|
|
116
|
+
let rules = hostTables.get(facts);
|
|
117
|
+
if (!rules) {
|
|
118
|
+
rules = createHostTable(facts, overlaidPackVendors()).hosts;
|
|
119
|
+
hostTables.set(facts, rules);
|
|
120
|
+
}
|
|
121
|
+
return rules;
|
|
122
|
+
}
|
package/dist/src/placeholder.js
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
// keep is a fork, taken before.
|
|
8
8
|
import { materializeRoots } from "./root.js";
|
|
9
9
|
import { loadWorldConfig } from "./configs.js";
|
|
10
|
+
import { WorldDoors, servedName } from "./served-world.js";
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
10
12
|
import { existsSync, readdirSync } from 'node:fs';
|
|
11
13
|
import { dirname, join, resolve } from 'node:path';
|
|
12
|
-
import { beginPlaceholderPull, endPlaceholderPull, listEvents, PLACEHOLDER_REMOTE, worldStateRoot } from '@volter/world-core/runtime';
|
|
14
|
+
import { serveHttp, beginPlaceholderPull, endPlaceholderPull, listEvents, PLACEHOLDER_REMOTE, worldStateRoot } from '@volter/world-core/runtime';
|
|
13
15
|
import { downWorld, runWithWorldEnv, saveWorldInstance, statusWorld, upWorld } from "./runtime.js";
|
|
14
16
|
/** The seed entry `init` emits beside the world config, unless the caller names one. */
|
|
15
17
|
export function seedEntryFor(instance, entry) {
|
|
@@ -54,7 +56,18 @@ export async function seedWorld(name, options = {}) {
|
|
|
54
56
|
for (const service of services)
|
|
55
57
|
beginPlaceholderPull(service, join(instance.dirs.data, service), at);
|
|
56
58
|
let exitCode = 1;
|
|
59
|
+
const served = servedName(root, instance.configPath);
|
|
60
|
+
const token = `tok_seed_${randomUUID()}`;
|
|
61
|
+
const doors = new WorldDoors(name, root, instance.configPath, served, {
|
|
62
|
+
ready: () => true, layout: () => instance, reload: () => statusWorld(name, root), twinFetch: () => null,
|
|
63
|
+
}, token, `tok_seed_read_${randomUUID()}`);
|
|
64
|
+
let doorServer;
|
|
57
65
|
try {
|
|
66
|
+
doorServer = await serveHttp({ twinRequestJournal: false, maxRequestBodySize: 4 * 1024 * 1024, fetch: request => {
|
|
67
|
+
const path = new URL(request.url).pathname;
|
|
68
|
+
return path === `/-/${served}/browsers` || path.startsWith(`/-/${served}/browsers/`)
|
|
69
|
+
? doors.handle(request) : Response.json({ error: 'the seed loader serves browser state only' }, { status: 404 });
|
|
70
|
+
} });
|
|
58
71
|
// The runtime that is running runs the seed: Bun, or Node with its TypeScript type stripping
|
|
59
72
|
// (`--experimental-strip-types`, accepted from Node 22.6 and on by default since 23.6). Never
|
|
60
73
|
// `bun` by name: the published runtime supports Node alone, and the tab's engine has no Bun.
|
|
@@ -64,9 +77,10 @@ export async function seedWorld(name, options = {}) {
|
|
|
64
77
|
throw new Error(`${entry} is TypeScript: seeding it under Node needs Node 22.6 or newer (type stripping), or Bun; this is Node ${process.versions.node}`);
|
|
65
78
|
}
|
|
66
79
|
const runner = typeof Bun !== 'undefined' ? ['bun', entry] : [process.execPath, '--experimental-strip-types', '--no-warnings', entry];
|
|
67
|
-
exitCode = await runWithWorldEnv(name, runner, root, { cwd: options.cwd ?? dirname(entry), env: { VOLTER_SEED_SKIP: [...rooted].sort().join(',') } });
|
|
80
|
+
exitCode = await runWithWorldEnv(name, runner, root, { cwd: options.cwd ?? dirname(entry), env: { VOLTER_SEED_SKIP: [...rooted].sort().join(','), VOLTER_WORLD_BROWSER_DOOR: `${doorServer.url.origin}/-/${served}/browsers`, VOLTER_WORLD_BROWSER_TOKEN: token } });
|
|
68
81
|
}
|
|
69
82
|
finally {
|
|
83
|
+
await doorServer?.stop();
|
|
70
84
|
for (const service of services)
|
|
71
85
|
endPlaceholderPull(service, join(instance.dirs.data, service));
|
|
72
86
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { serveHttp } from '@volter/world-core/runtime';
|
|
2
2
|
import { worldNetworkPolicyFromEnv, allowsWorldNetworkEgress } from '@volter/world-core/network-policy';
|
|
3
3
|
import { websocketRelay } from "./websocket-relay.js";
|
|
4
4
|
// The ambient TLS redirect proxy — the substance of "any CLI, zero config" (WORLD_ACTIVATE.md §"How
|
|
@@ -350,7 +350,7 @@ export async function startRedirectProxy(options) {
|
|
|
350
350
|
}
|
|
351
351
|
const origin = new URL(twin.origin);
|
|
352
352
|
const headers = new Headers(req.headers);
|
|
353
|
-
for (const h of [
|
|
353
|
+
for (const h of ['proxy-connection', 'accept-encoding', 'connection', 'x-volter-world-browser'])
|
|
354
354
|
headers.delete(h);
|
|
355
355
|
// the application is not a twin: it gets no World key, keeps its Host and hears it was reached over TLS
|
|
356
356
|
if (twin.vendor === 'app') {
|
|
@@ -479,9 +479,9 @@ export async function startRedirectProxy(options) {
|
|
|
479
479
|
outHeaders['x-volter-twin-original-host'] = headers.host ?? origin.host;
|
|
480
480
|
outHeaders.host = origin.host;
|
|
481
481
|
}
|
|
482
|
-
delete outHeaders[BROWSER_SESSION_HEADER];
|
|
483
482
|
delete outHeaders['proxy-connection'];
|
|
484
483
|
delete outHeaders['accept-encoding'];
|
|
484
|
+
delete outHeaders['x-volter-world-browser'];
|
|
485
485
|
upstream.write(`${method} ${path} ${version}\r\n`);
|
|
486
486
|
for (const [name, value] of Object.entries(outHeaders))
|
|
487
487
|
upstream.write(`${name}: ${value}\r\n`);
|
package/dist/src/reflect.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { BROWSER_SESSION_HEADER } from '@volter/world-core/runtime';
|
|
2
1
|
import { serveHttp } from '@volter/world-core/runtime';
|
|
3
2
|
import { websocketRelay } from "./websocket-relay.js";
|
|
4
3
|
import { sessionTrustEnv } from "./ca-trust.js";
|
|
@@ -140,7 +139,7 @@ export async function startReflectFront(options) {
|
|
|
140
139
|
return new Response('bad twin origin', { status: 502 });
|
|
141
140
|
}
|
|
142
141
|
const headers = new Headers(req.headers);
|
|
143
|
-
headers.delete(
|
|
142
|
+
headers.delete('x-volter-world-browser');
|
|
144
143
|
headers.delete('proxy-connection');
|
|
145
144
|
headers.delete('accept-encoding');
|
|
146
145
|
// the consumer spoke TLS to the front; a twin that answers with an absolute URL (Slack's Socket Mode link)
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { materializeBrowserFiles, clearBrowserFiles } from "./browsers.js";
|
|
1
2
|
import { claimEnvOutput, publishEnvOutput } from "./env-output.js";
|
|
2
3
|
import { offSignal } from "./signals.js";
|
|
3
4
|
import { parallelStartup } from "./startup.js";
|
|
@@ -1792,7 +1793,7 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
1792
1793
|
}
|
|
1793
1794
|
try {
|
|
1794
1795
|
if (options.keepState) {
|
|
1795
|
-
const KEEP = ['data', 'marks', 'changesets', 'episodes'];
|
|
1796
|
+
const KEEP = ['data', 'marks', 'changesets', 'episodes', 'browser-records', 'browser-loads'];
|
|
1796
1797
|
for (const entry of existsSync(instance) ? readdirSync(instance) : [])
|
|
1797
1798
|
if (!KEEP.includes(entry))
|
|
1798
1799
|
rmSync(join(instance, entry), { recursive: true, force: true });
|
|
@@ -2011,6 +2012,7 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
2011
2012
|
// Config env and discovered credentials: a file this writes is owner-only; a file the caller already made keeps
|
|
2012
2013
|
// the mode the caller chose (a shared env file may be read under another uid).
|
|
2013
2014
|
publishEnvOutput(envOutputClaim, envFileContents(env));
|
|
2015
|
+
materializeBrowserFiles(data, root, name, loaded.config.id);
|
|
2014
2016
|
writePidsFromInstance(worldInstance, Object.values(worldInstance.services).map((service) => service.pid).filter((pid) => pid > 0));
|
|
2015
2017
|
saveWorldInstance(worldInstance);
|
|
2016
2018
|
const servicePids = [...new Set(Object.values(services).map((service) => service.pid).filter((pid) => pid > 0))];
|
|
@@ -2389,7 +2391,12 @@ async function downWorldClaimed(name, root, options) {
|
|
|
2389
2391
|
})}\n`);
|
|
2390
2392
|
}
|
|
2391
2393
|
releaseWorldLifecycle(resolvedRoot, name);
|
|
2392
|
-
|
|
2394
|
+
if (!options.purge)
|
|
2395
|
+
return result;
|
|
2396
|
+
const purged = purgeInstanceDir(resolvedRoot, name);
|
|
2397
|
+
if (purged && instance)
|
|
2398
|
+
clearBrowserFiles(instance.dirs.data, resolvedRoot);
|
|
2399
|
+
return { ...result, purged };
|
|
2393
2400
|
}));
|
|
2394
2401
|
}
|
|
2395
2402
|
export function statusWorld(name, root = process.cwd(), options = {}) {
|
|
@@ -67,6 +67,10 @@ export type MountedWorld = {
|
|
|
67
67
|
boot: (url: string) => Promise<void>;
|
|
68
68
|
stop: () => Promise<void>;
|
|
69
69
|
};
|
|
70
|
+
/** A browser session a World keeps: its scope, when it ends, and the person a pass named, when one did. */
|
|
71
|
+
/** What a content-plane pass, and the session it opens, carry: its scope; the control session that asked (the content
|
|
72
|
+
* session ends with it; none when the World's token asked); the person; the console that frames it; and whether it
|
|
73
|
+
* is this machine's own page (attributed to the machine). */
|
|
70
74
|
type PagesGrant = {
|
|
71
75
|
scope: 'read' | 'write';
|
|
72
76
|
session?: string;
|
|
@@ -74,7 +78,8 @@ type PagesGrant = {
|
|
|
74
78
|
framer?: string;
|
|
75
79
|
local?: true; /** the hosted site it
|
|
76
80
|
* opens (a hostname this World's twins answer), not the content plane */
|
|
77
|
-
site?: string;
|
|
81
|
+
site?: string;
|
|
82
|
+
browser?: string; /** a parent World's session that asked
|
|
78
83
|
* for a branch's page, as an opaque mark: the branch keeps one content session per asker and caps passes by it, but
|
|
79
84
|
* cannot tell when it ends, so such a session lasts an hour */
|
|
80
85
|
asker?: string;
|
|
@@ -110,6 +115,7 @@ export type WorldLayout = {
|
|
|
110
115
|
dirs: {
|
|
111
116
|
data: string;
|
|
112
117
|
};
|
|
118
|
+
envFile?: string;
|
|
113
119
|
services: Record<string, {
|
|
114
120
|
url?: string;
|
|
115
121
|
protocol?: unknown;
|
|
@@ -262,13 +268,6 @@ export declare class WorldDoors {
|
|
|
262
268
|
private keepCookiesHere;
|
|
263
269
|
/** What the content plane answers: its opening page and session, and the twins' pages; no door of the World. */
|
|
264
270
|
private pagesPlane;
|
|
265
|
-
/** A landing resolved as a browser resolves it, within this vendor's place only. */
|
|
266
|
-
private vendorLanding;
|
|
267
|
-
/** The pack's declared browser session, never inferred from its vendor or a sign-in page. */
|
|
268
|
-
private browserSessionOf;
|
|
269
|
-
private vendorPeople;
|
|
270
|
-
/** The twin receives its own cookie and changes the session in its own process, over a host-only permit. */
|
|
271
|
-
private vendorSession;
|
|
272
271
|
/** BROWSER SESSIONS: the session cookie carries an opaque id, never a token, so a leaked cookie is one session and
|
|
273
272
|
* not the World's write token; rotating the tokens ends every session. Kept in the World's store, so a restart
|
|
274
273
|
* keeps them; each lasts 30 days from its opening. */
|
|
@@ -381,6 +380,10 @@ export declare class WorldDoors {
|
|
|
381
380
|
vendor: string;
|
|
382
381
|
hosts: string[];
|
|
383
382
|
}>>;
|
|
383
|
+
/** Browser sites include the pack's exact declared hosts, beside the dynamically claimed sites. */
|
|
384
|
+
private browserSites;
|
|
385
|
+
private browserRoutes;
|
|
386
|
+
private loadBrowser;
|
|
384
387
|
/** What a twin asks an application (`ctx.ask`) that this World can answer truthfully, where no process routes it (a
|
|
385
388
|
* hosted World's isolate): a GET or HEAD of a URL whose hostname one of this World's twins answers (its hostsClaimed
|
|
386
389
|
* door: a Cloudflare twin's Worker route or custom domain) is that twin's answer, read as a browser reads the site
|
|
@@ -395,7 +398,7 @@ export declare class WorldDoors {
|
|
|
395
398
|
/** A hostname one of this World's twins answers (its hostsClaimed door lists it), read as a browser reads it there:
|
|
396
399
|
* the request goes to that twin with the hostname as its original host, as the injector routes it inside the World.
|
|
397
400
|
* Reads only: the site is the twin's state as the World holds it, never a way to write it. */
|
|
398
|
-
site(request: Request, host: string): Promise<Response>;
|
|
401
|
+
site(request: Request, host: string, browser?: string, grant?: PagesGrant): Promise<Response>;
|
|
399
402
|
handle(request: Request): Promise<Response>;
|
|
400
403
|
/** A twin's workspaces as its discovery door lists them (`GET /twin`'s `screens`: each built workspace screen's id and
|
|
401
404
|
* path), which a person is shown at `/<served>/<vendor><path>`. Asked again after a minute, as `enforcesRead` is. */
|