@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.
@@ -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
+ }
@@ -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";
@@ -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
- for (const call of calls) {
1148
- const res = await fetch(\`\${base.replace(/\\/$/, '')}\${call.path}\`, {
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: { ...(call.body !== undefined ? { 'content-type': 'application/json' } : {}), ...(call.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 { clockFile } from "./runtime.js";
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)
@@ -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>;
@@ -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
+ }
@@ -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 { BROWSER_SESSION_HEADER, serveHttp } from '@volter/world-core/runtime';
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 [BROWSER_SESSION_HEADER, 'proxy-connection', 'accept-encoding', 'connection'])
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`);
@@ -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(BROWSER_SESSION_HEADER);
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)
@@ -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
- return options.purge ? { ...result, purged: purgeInstanceDir(resolvedRoot, name) } : result;
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; /** a parent World's session that asked
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. */