@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.
@@ -69,7 +69,7 @@ export function siteOfHost(hostname, served, base = LOCAL_ORIGIN_BASE) {
69
69
  if (!label || !rest.endsWith(`.${label}`))
70
70
  continue;
71
71
  const site = rest.slice(0, -(label.length + 1));
72
- if (site.includes('.'))
72
+ if (site.includes('.') || site.includes('--'))
73
73
  return { world: name, site };
74
74
  }
75
75
  return undefined;
@@ -3,6 +3,8 @@
3
3
  // World"). `volter world view` is this; the runtime's `up` gains no step. A host that serves many
4
4
  // Worlds (world-host) answers the same doors; this serves one, and the branches made of it.
5
5
  import { existsSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { materializeBrowserFiles } from "./browsers.js";
7
+ import { statusWorld } from "./runtime.js";
6
8
  import { loadWorldConfig } from "./configs.js";
7
9
  import { join, resolve } from 'node:path';
8
10
  import { serveHttp, stateDirName } from '@volter/world-core/runtime';
@@ -90,6 +92,8 @@ export async function serveWorldView(name, opts = {}) {
90
92
  rmSync(current, { force: true });
91
93
  else
92
94
  writeFileSync(current, `${branch}\n`);
95
+ const selected = statusWorld(branch, root);
96
+ materializeBrowserFiles(selected.dirs.data, root, branch, selected.config);
93
97
  }
94
98
  finally {
95
99
  switching = false;
@@ -118,7 +122,7 @@ export async function serveWorldView(name, opts = {}) {
118
122
  // a site the World's twins answer, under the World's local origin (`<hostname>.<world>--<org>.localhost`)
119
123
  const site = origins && !proxied ? siteOfHost(at.hostname, worlds.keys()) : undefined;
120
124
  if (site)
121
- return worlds.get(site.world).site(request, site.site);
125
+ return !site.site.includes('.') && site.site.includes('--') ? worlds.get(site.world).handle(request) : worlds.get(site.world).site(request, site.site);
122
126
  // a World's content plane serves that World's twins' pages and nothing else of this view's (no console)
123
127
  const pagesFor = origins && !proxied ? pagesOfHost(at.hostname, worlds.keys()) : undefined;
124
128
  if (pagesFor)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "3.0.30",
3
+ "version": "3.0.32",
4
4
  "description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
5
5
  "keywords": [
6
6
  "twin",
@@ -68,14 +68,14 @@
68
68
  "node": ">=22.3"
69
69
  },
70
70
  "peerDependencies": {
71
- "@volter/world-core": "3.0.30",
72
- "@volter/world-console": "3.0.25"
71
+ "@volter/world-core": "3.0.32",
72
+ "@volter/world-console": "3.0.27"
73
73
  },
74
74
  "dependencies": {
75
75
  "@electric-sql/pglite": "0.5.8",
76
76
  "@electric-sql/pglite-pgvector": "0.0.9",
77
77
  "@volter/world-access": "3.0.1",
78
- "@volter/world-core": "3.0.30",
78
+ "@volter/world-core": "3.0.32",
79
79
  "pg-gateway": "0.3.0-beta.4",
80
80
  "smol-toml": "^1.8.0",
81
81
  "yaml": "2.9.0"
package/src/branch.ts 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.ts';
13
14
  import { downWorld, saveWorldInstance, statusWorld, upWorld } from './runtime.ts';
14
15
  import type { WorldInstance } from './schema.ts';
15
16
  import { parseInfraDefinition, pgliteDown, pgliteUp } from './pglite-backing.ts';
@@ -80,6 +81,8 @@ export async function branchWorld(base: string, name: string, options: BranchOpt
80
81
  }
81
82
  if (states.length) forked[service] = states.map(s => s.state);
82
83
  }
84
+ inheritWorldBrowsers(from.dirs.data, instance.dirs.data);
85
+ materializeBrowserFiles(instance.dirs.data, root, name, instance.config);
83
86
  await branchManagedDatabases(from, instance, databases);
84
87
  } catch (error) {
85
88
  await downWorld(name, root, { purge: true, expectedCreatedAt: instance.createdAt });
@@ -0,0 +1,146 @@
1
+ import { assertBrowserName, validateBrowserState, type BrowserState } from '@volter/world-core';
2
+ import { getActiveWorldStore, stateDirName, type WorldStore } from '@volter/world-core/runtime';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { dirname, join } from 'node:path';
5
+
6
+ export type WorldBrowser = { name: string; state: BrowserState; version: string };
7
+ /** Instance bookkeeping, alongside the clock; retained at down, discarded at reset. */
8
+ export const browsersDir = (data: string): string => join(dirname(data), 'browser-records');
9
+ const path = (data: string, name: string): string => { assertBrowserName(name); return join(browsersDir(data), `${name}.json`); };
10
+ const records = new WeakMap<WorldStore, Map<string, { version: string; text: string; record: WorldBrowser }>>();
11
+ export function worldBrowser(data: string, name: string): WorldBrowser | null {
12
+ const store = getActiveWorldStore();
13
+ const file = path(data, name);
14
+ const cache = records.get(store) ?? new Map<string, { version: string; text: string; record: WorldBrowser }>();
15
+ records.set(store, cache);
16
+ const text = store.read(file);
17
+ if (text === null) { cache.delete(file); return null; }
18
+ const held = cache.get(file);
19
+ if (held?.text === text) return held.record;
20
+ const record = JSON.parse(text) as WorldBrowser;
21
+ if (record.name !== name || typeof record.version !== 'string') throw new Error(`invalid browser record: ${name}`);
22
+ if (held?.version === record.version) { held.text = text; return held.record; }
23
+ validateBrowserState(record.state);
24
+ const freeze = (value: unknown): void => {
25
+ if (!value || typeof value !== 'object') return;
26
+ for (const item of Object.values(value)) freeze(item);
27
+ Object.freeze(value);
28
+ };
29
+ freeze(record);
30
+ cache.set(file, { version: record.version, text, record });
31
+ return record;
32
+ }
33
+ export function worldBrowsers(data: string): WorldBrowser[] {
34
+ return getActiveWorldStore().list(browsersDir(data)).filter(f => f.endsWith('.json')).sort().map(f => worldBrowser(data, f.slice(0, -5))!);
35
+ }
36
+ export function putWorldBrowser(data: string, name: string, state: BrowserState): WorldBrowser {
37
+ const record = { name, state: validateBrowserState(state), version: randomUUID() };
38
+ getActiveWorldStore().writeAtomic(path(data, name), `${JSON.stringify(record)}\n`, { secret: true });
39
+ retireBrowserLoads(data, name, record);
40
+ return record;
41
+ }
42
+ export function deleteWorldBrowser(data: string, name: string): void {
43
+ const store = getActiveWorldStore();
44
+ store.remove(path(data, name));
45
+ records.get(store)?.delete(path(data, name));
46
+ retireBrowserLoads(data, name);
47
+ }
48
+ export function inheritWorldBrowsers(fromData: string, toData: string): void {
49
+ const store = getActiveWorldStore();
50
+ for (const b of worldBrowsers(fromData)) store.writeAtomic(path(toData, b.name), `${JSON.stringify(b)}\n`, { secret: true });
51
+ }
52
+
53
+ /** Derived files under the World state directory, written only by its current branch. */
54
+ export function materializeBrowserFiles(data: string, root: string, name: string, main = name): void {
55
+ const store = getActiveWorldStore();
56
+ const stateDir = join(root, stateDirName());
57
+ const config = store.read(join(stateDir, 'world.json'));
58
+ if (config) main = (JSON.parse(config) as { id: string }).id;
59
+ if ((store.read(join(stateDir, 'current'))?.trim() || main) !== name) return;
60
+ const rows = worldBrowsers(data);
61
+ const dir = join(stateDir, 'browsers');
62
+ const index = join(dir, '.source.json');
63
+ if (!rows.length && store.read(index) === null) return;
64
+ if (rows.length) {
65
+ const ignore = join(stateDir, '.gitignore');
66
+ const held = store.read(ignore) ?? '';
67
+ if (!held.split('\n').includes('browsers/')) store.write(ignore, `${held.replace(/\n?$/, '\n')}browsers/\n`);
68
+ }
69
+ store.withLock(join(dir, '.lock'), () => {
70
+ let previous: { data: string; names: string[] } | null = null;
71
+ const text = store.read(index);
72
+ if (text) previous = JSON.parse(text) as { data: string; names: string[] };
73
+ for (const name of previous?.names ?? []) { assertBrowserName(name); if (!rows.some(b => b.name === name)) store.remove(join(dir, `${name}.json`)); }
74
+ for (const b of rows) store.writeAtomic(join(dir, `${b.name}.json`), `${JSON.stringify(b.state)}\n`, { secret: true });
75
+ if (rows.length) store.writeAtomic(index, `${JSON.stringify({ data, names: rows.map(b => b.name) })}\n`, { secret: true }); else store.remove(index);
76
+ });
77
+ }
78
+ export function clearBrowserFiles(data: string, root: string): void {
79
+ const store = getActiveWorldStore();
80
+ const dir = join(root, stateDirName(), 'browsers');
81
+ const index = join(dir, '.source.json');
82
+ if (store.read(index) === null) return;
83
+ store.withLock(join(dir, '.lock'), () => {
84
+ const text = store.read(index);
85
+ if (!text) return;
86
+ const held = JSON.parse(text) as { data: string; names: string[] };
87
+ if (held.data !== data) return;
88
+ for (const name of held.names) { assertBrowserName(name); store.remove(join(dir, `${name}.json`)); }
89
+ store.remove(index);
90
+ });
91
+ }
92
+
93
+ /** Private load-marker history; the cookie carries only an opaque id, never vendor state. */
94
+ export type BrowserLoad = { name: string; site: string; version: string; cookies: Array<{ name: string; path: string }> };
95
+ export const browserLoadId = (version: string, site: string): string => createHash('sha256').update(`${version}\0${site}`).digest('hex');
96
+ const loadPath = (data: string, id: string): string => join(dirname(data), 'browser-loads', `${id}.json`);
97
+ export function browserLoad(data: string, id: string | undefined): BrowserLoad | null {
98
+ if (!id || !/^[a-f0-9]{64}$/.test(id)) return null;
99
+ const text = getActiveWorldStore().read(loadPath(data, id));
100
+ return text === null ? null : JSON.parse(text) as BrowserLoad;
101
+ }
102
+ export function putBrowserLoad(data: string, id: string, load: BrowserLoad): void {
103
+ if (!/^[a-f0-9]{64}$/.test(id)) throw new Error('invalid browser load marker');
104
+ const held = browserLoad(data, id);
105
+ const cookies = new Map<string, { name: string; path: string }>();
106
+ if (held?.name === load.name && held.site === load.site && held.version === load.version) {
107
+ for (const cookie of held.cookies) cookies.set(JSON.stringify([cookie.name, cookie.path]), cookie);
108
+ }
109
+ for (const cookie of load.cookies) cookies.set(JSON.stringify([cookie.name, cookie.path]), cookie);
110
+ getActiveWorldStore().writeAtomic(loadPath(data, id), `${JSON.stringify({ ...load, cookies: [...cookies.values()] })}\n`, { secret: true });
111
+ }
112
+
113
+ /** Retire old ids; cookie paths survive loads and deletion until carried into the name's next version. */
114
+ function retireBrowserLoads(data: string, name: string, current?: WorldBrowser): void {
115
+ const store = getActiveWorldStore();
116
+ const dir = join(dirname(data), 'browser-loads');
117
+ const retiredFile = join(dir, `retired-${name}.json`);
118
+ const prior = new Map<string, BrowserLoad>();
119
+ const keep = (load: BrowserLoad): void => {
120
+ const cookies = new Map<string, { name: string; path: string }>();
121
+ for (const cookie of [...(prior.get(load.site)?.cookies ?? []), ...load.cookies]) {
122
+ cookies.set(JSON.stringify([cookie.name, cookie.path]), cookie);
123
+ }
124
+ prior.set(load.site, { ...load, cookies: [...cookies.values()] });
125
+ };
126
+ const retired = store.read(retiredFile);
127
+ if (retired !== null) for (const load of JSON.parse(retired) as BrowserLoad[]) keep(load);
128
+ const removed: string[] = [];
129
+ for (const file of store.list(dir)) {
130
+ if (!/^[a-f0-9]{64}\.json$/.test(file)) continue;
131
+ const load = browserLoad(data, file.slice(0, -5));
132
+ if (!load || load.name !== name || load.version === current?.version) continue;
133
+ keep(load);
134
+ removed.push(file);
135
+ }
136
+ // Preserve paths before removing the version records that supplied them.
137
+ if (current) {
138
+ for (const load of prior.values()) putBrowserLoad(data, browserLoadId(current.version, load.site), {
139
+ ...load, version: current.version,
140
+ });
141
+ store.remove(retiredFile);
142
+ } else if (prior.size) {
143
+ store.writeAtomic(retiredFile, `${JSON.stringify([...prior.values()])}\n`, { secret: true });
144
+ }
145
+ for (const file of removed) store.remove(join(dir, file));
146
+ }
package/src/index.ts CHANGED
@@ -177,3 +177,5 @@ export { CONSOLE_BASE, consoleRedirect, serveConsoleApart, serveConsoleFor, type
177
177
  export { assertWorldStateRemovable } from './ancestry.ts';
178
178
 
179
179
  export { withoutVaultSignIn } from './runtime.ts';
180
+
181
+ export { materializeBrowserFiles, browsersDir, worldBrowser, worldBrowsers, putWorldBrowser, deleteWorldBrowser, inheritWorldBrowsers, type WorldBrowser } from './browsers.ts';
package/src/init.ts CHANGED
@@ -1273,6 +1273,7 @@ token
1273
1273
  token.read
1274
1274
  sessions.json
1275
1275
  pages-sessions.json
1276
+ browsers/
1276
1277
  keys.json
1277
1278
  `;
1278
1279
 
@@ -1325,17 +1326,48 @@ export function seedCallsRunner(vendor: string): string {
1325
1326
  return `// ${vendor}'s default data, as its twin's pack declares it (./${vendor}.data.ts: calls to the vendor's own
1326
1327
  // operations), made against the twin in order. Ownership transferred on copy: both files are yours — edit or delete.
1327
1328
  import { seed as calls } from './${vendor}.data.ts';
1329
+ import * as defaults from './${vendor}.data.ts';
1330
+ import type { SeedBrowser } from '@volter/world-core/browser';
1328
1331
 
1329
1332
  export async function seed(): Promise<void> {
1330
1333
  // a twin with a root is the vendor's real account: its default data is not written (VOLTER_SEED_SKIP)
1331
1334
  if ((process.env.VOLTER_SEED_SKIP ?? '').split(',').includes('${vendor}')) return;
1332
1335
  const base = process.env.${stem}_TWIN_URL;
1333
1336
  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)');
1334
- for (const call of calls) {
1335
- const res = await fetch(\`\${base.replace(/\\/$/, '')}\${call.path}\`, {
1337
+ const clients = new Map<string, SeedBrowser>();
1338
+ const snapshots = new Map<string, Awaited<ReturnType<SeedBrowser['state']>>>();
1339
+ let browserHelper: typeof import('@volter/world-core/browser') | undefined;
1340
+ const browserCalls = ('browserSeed' in defaults ? defaults.browserSeed as typeof calls : undefined) ?? [];
1341
+ for (const call of [...calls, ...browserCalls]) {
1342
+ if (call.browser) {
1343
+ if (!snapshots.has(call.browser)) {
1344
+ const door = process.env.VOLTER_WORLD_BROWSER_DOOR;
1345
+ const token = process.env.VOLTER_WORLD_BROWSER_TOKEN;
1346
+ if (!door || !token) throw new Error('browser defaults run in a World seed with its browsers door and credential');
1347
+ const held = await fetch(\`\${door}/\${encodeURIComponent(call.browser)}\`, { headers: { 'x-volter-token': token } });
1348
+ if (!held.ok && held.status !== 404) throw new Error(\`browser \${call.browser}: \${held.status} \${await held.text()}\`);
1349
+ snapshots.set(call.browser, held.ok ? await held.json() as Awaited<ReturnType<SeedBrowser['state']>> : { cookies: [], origins: [] });
1350
+ }
1351
+ browserHelper ??= await import('@volter/world-core/browser').catch(error => {
1352
+ throw new Error('Install @volter/world-core 3.0.31 or later in this project to seed browsers.', { cause: error });
1353
+ });
1354
+ const initial = snapshots.get(call.browser)!;
1355
+ const hostname = new URL(call.path).hostname;
1356
+ if (initial.cookies.some(cookie => browserHelper!.cookieMatchesHost(cookie, hostname))
1357
+ || initial.origins.some(origin => new URL(origin.origin).hostname === hostname)) continue;
1358
+ }
1359
+ const headers = { ...(call.body !== undefined ? { 'content-type': 'application/json' } : {}), ...(call.headers ?? {}) };
1360
+ const form = /^application\\/x-www-form-urlencoded\\b/i.test(new Headers(headers).get('content-type') ?? '');
1361
+ const body = call.body === undefined ? undefined : form
1362
+ ? new URLSearchParams(Object.entries(call.body as Record<string, unknown>).map(([key, value]) => [key, String(value)]))
1363
+ : JSON.stringify(call.body);
1364
+ if (call.browser && !clients.has(call.browser)) {
1365
+ clients.set(call.browser, await browserHelper!.browser(call.browser));
1366
+ }
1367
+ const client = call.browser ? clients.get(call.browser)! : null;
1368
+ const res = await (client ? client.fetch : fetch)(client ? call.path : \`\${base.replace(/\\/$/, '')}\${call.path}\`, {
1336
1369
  method: call.method,
1337
- headers: { ...(call.body !== undefined ? { 'content-type': 'application/json' } : {}), ...(call.headers ?? {}) },
1338
- ...(call.body !== undefined ? { body: JSON.stringify(call.body) } : {}),
1370
+ headers, ...(body === undefined ? {} : { body }),
1339
1371
  });
1340
1372
  if (!res.ok) throw new Error(\`${vendor} seed: \${call.method} \${call.path}: \${res.status} \${await res.text()}\`);
1341
1373
  }
@@ -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.ts';
9
+ import { inheritWorldBrowsers, materializeBrowserFiles } from './browsers.ts';
10
+ import { clockFile, statusWorld } from './runtime.ts';
10
11
  import { loadWorldConfig } from './configs.ts';
11
12
  import { TOKEN_HEADER, type BranchDoors, type BranchRow, type MountedWorld } from './served-world.ts';
12
13
 
@@ -160,6 +161,9 @@ export class LocalBranches {
160
161
  // a live branch (a pull request's preview) keeps the parent's time: real time, or its simulated clock
161
162
  const clock = at.instant ?? this.frozenClock(from) ?? (at.live ? null : new Date().toISOString());
162
163
  if (clock) { const file = clockFile(root, id); mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, `${clock}\n`); }
164
+ const child = statusWorld(id, root);
165
+ inheritWorldBrowsers(statusWorld(world, from.root).dirs.data, child.dirs.data);
166
+ materializeBrowserFiles(child.dirs.data, root, id, child.config);
163
167
  const createdAt = new Date().toISOString();
164
168
  const expiresAt = ttlSeconds === null ? null : new Date(Date.now() + ttlSeconds * 1000).toISOString();
165
169
  // what it was made as (a pull request's preview is labelled and may be live)
package/src/pack-facts.ts CHANGED
@@ -8,6 +8,7 @@
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
10
 
11
+ import { createHostTable, type HostPredicate } from '@volter/world-core/host-routing';
11
12
  import factsDocument from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
12
13
 
13
14
  export type PackAdoption = { sdks?: string[]; pypi?: string[]; scopes?: string[]; envStems?: string[]; worldIds?: string[]; tools?: Array<{ package: string; usage: 'build' | 'deployment' }>; configFiles?: Array<{ file: string; usage: 'application' | 'build' | 'deployment'; when?: string }> };
@@ -146,3 +147,18 @@ export function overlayEndpointEnv(
146
147
  };
147
148
  }
148
149
  }
150
+
151
+ /** Facts supplied by resolved packs, compiled after the built-in facts in a routing table. */
152
+ export function overlaidPackVendors(): string[] { return Object.keys(overlaid); }
153
+
154
+ /** One routing table per immutable merged-facts version, built only for browser routing. */
155
+ const hostTables = new WeakMap<Record<string, PackFacts>, Record<string, HostPredicate>>();
156
+ export function packHostRules(): Record<string, HostPredicate> {
157
+ const facts = packFacts();
158
+ let rules = hostTables.get(facts);
159
+ if (!rules) {
160
+ rules = createHostTable(facts, overlaidPackVendors()).hosts;
161
+ hostTables.set(facts, rules);
162
+ }
163
+ return rules;
164
+ }
@@ -7,9 +7,11 @@
7
7
  // keep is a fork, taken before.
8
8
  import { materializeRoots } from './root.ts';
9
9
  import { loadWorldConfig } from './configs.ts';
10
+ import { WorldDoors, servedName } from './served-world.ts';
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.ts';
14
16
  import type { WorldInstance } from './schema.ts';
15
17
 
@@ -55,7 +57,18 @@ export async function seedWorld(name: string, options: SeedWorldOptions = {}): P
55
57
  const at = new Date().toISOString();
56
58
  for (const service of services) beginPlaceholderPull(service, join(instance.dirs.data, service), at);
57
59
  let exitCode = 1;
60
+ const served = servedName(root, instance.configPath);
61
+ const token = `tok_seed_${randomUUID()}`;
62
+ const doors = new WorldDoors(name, root, instance.configPath, served, {
63
+ ready: () => true, layout: () => instance, reload: () => statusWorld(name, root), twinFetch: () => null,
64
+ }, token, `tok_seed_read_${randomUUID()}`);
65
+ let doorServer: Awaited<ReturnType<typeof serveHttp>> | undefined;
58
66
  try {
67
+ doorServer = await serveHttp({ twinRequestJournal: false, maxRequestBodySize: 4 * 1024 * 1024, fetch: request => {
68
+ const path = new URL(request.url).pathname;
69
+ return path === `/-/${served}/browsers` || path.startsWith(`/-/${served}/browsers/`)
70
+ ? doors.handle(request) : Response.json({ error: 'the seed loader serves browser state only' }, { status: 404 });
71
+ } });
59
72
  // The runtime that is running runs the seed: Bun, or Node with its TypeScript type stripping
60
73
  // (`--experimental-strip-types`, accepted from Node 22.6 and on by default since 23.6). Never
61
74
  // `bun` by name: the published runtime supports Node alone, and the tab's engine has no Bun.
@@ -64,8 +77,9 @@ export async function seedWorld(name: string, options: SeedWorldOptions = {}): P
64
77
  if (major < 22 || (major === 22 && minor < 6)) 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
  } finally {
82
+ await doorServer?.stop();
69
83
  for (const service of services) endPlaceholderPull(service, join(instance.dirs.data, service));
70
84
  }
71
85
  const observed: Record<string, number> = {};
@@ -1,4 +1,4 @@
1
- import { BROWSER_SESSION_HEADER, serveHttp, type HttpServer } from '@volter/world-core/runtime';
1
+ import { serveHttp, type HttpServer } from '@volter/world-core/runtime';
2
2
  import { worldNetworkPolicyFromEnv, allowsWorldNetworkEgress } from '@volter/world-core/network-policy';
3
3
  import { websocketRelay } from './websocket-relay.ts';
4
4
  // The ambient TLS redirect proxy — the substance of "any CLI, zero config" (WORLD_ACTIVATE.md §"How
@@ -376,7 +376,7 @@ export async function startRedirectProxy(options: RedirectProxyOptions): Promise
376
376
  const origin = new URL(twin.origin);
377
377
 
378
378
  const headers = new Headers(req.headers);
379
- for (const h of [BROWSER_SESSION_HEADER, 'proxy-connection', 'accept-encoding', 'connection']) headers.delete(h);
379
+ for (const h of ['proxy-connection', 'accept-encoding', 'connection', 'x-volter-world-browser']) headers.delete(h);
380
380
  // the application is not a twin: it gets no World key, keeps its Host and hears it was reached over TLS
381
381
  if (twin.vendor === 'app') {
382
382
  headers.delete('x-twins-key');
@@ -509,9 +509,9 @@ export async function startRedirectProxy(options: RedirectProxyOptions): Promise
509
509
  outHeaders['x-volter-twin-original-host'] = headers.host ?? origin.host;
510
510
  outHeaders.host = origin.host;
511
511
  }
512
- delete outHeaders[BROWSER_SESSION_HEADER];
513
512
  delete outHeaders['proxy-connection'];
514
513
  delete outHeaders['accept-encoding'];
514
+ delete outHeaders['x-volter-world-browser'];
515
515
  upstream.write(`${method} ${path} ${version}\r\n`);
516
516
  for (const [name, value] of Object.entries(outHeaders)) upstream.write(`${name}: ${value}\r\n`);
517
517
  upstream.write('\r\n');
package/src/reflect.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { BROWSER_SESSION_HEADER } from '@volter/world-core/runtime';
2
1
  import { serveHttp, type HttpServer } from '@volter/world-core/runtime';
3
2
  import { websocketRelay } from './websocket-relay.ts';
4
3
  import { sessionTrustEnv } from './ca-trust.ts';
@@ -161,7 +160,7 @@ export async function startReflectFront(options: ReflectFrontOptions): Promise<R
161
160
  try { origin = new URL(target.origin); } catch { return new Response('bad twin origin', { status: 502 }); }
162
161
 
163
162
  const headers = new Headers(req.headers);
164
- headers.delete(BROWSER_SESSION_HEADER);
163
+ headers.delete('x-volter-world-browser');
165
164
  headers.delete('proxy-connection');
166
165
  headers.delete('accept-encoding');
167
166
  // the consumer spoke TLS to the front; a twin that answers with an absolute URL (Slack's Socket Mode link)
package/src/runtime.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { materializeBrowserFiles, clearBrowserFiles } from './browsers.ts';
1
2
  import { claimEnvOutput, publishEnvOutput } from './env-output.ts';
2
3
  import { offSignal } from './signals.ts';
3
4
  import { parallelStartup } from './startup.ts';
@@ -1741,7 +1742,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
1741
1742
  if (existsSync(logs)) { rmSync(join(logs, 'previous'), { recursive: true, force: true }); renameSync(logs, previousLogs); }
1742
1743
  try {
1743
1744
  if (options.keepState) {
1744
- const KEEP = ['data', 'marks', 'changesets', 'episodes'];
1745
+ const KEEP = ['data', 'marks', 'changesets', 'episodes', 'browser-records', 'browser-loads'];
1745
1746
  for (const entry of existsSync(instance) ? readdirSync(instance) : []) if (!KEEP.includes(entry)) rmSync(join(instance, entry), { recursive: true, force: true });
1746
1747
  } else removeWorldState(root, instance, () => rmSync(instance, { recursive: true, force: true }));
1747
1748
  } catch (error) {
@@ -1930,6 +1931,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
1930
1931
  // Config env and discovered credentials: a file this writes is owner-only; a file the caller already made keeps
1931
1932
  // the mode the caller chose (a shared env file may be read under another uid).
1932
1933
  publishEnvOutput(envOutputClaim, envFileContents(env));
1934
+ materializeBrowserFiles(data, root, name, loaded.config.id);
1933
1935
  writePidsFromInstance(worldInstance, Object.values(worldInstance.services).map((service) => service.pid).filter((pid) => pid > 0));
1934
1936
  saveWorldInstance(worldInstance);
1935
1937
  const servicePids = [...new Set(Object.values(services).map((service) => service.pid).filter((pid) => pid > 0))];
@@ -2286,7 +2288,10 @@ async function downWorldClaimed(
2286
2288
  })}\n`);
2287
2289
  }
2288
2290
  releaseWorldLifecycle(resolvedRoot, name);
2289
- return options.purge ? { ...result, purged: purgeInstanceDir(resolvedRoot, name) } : result;
2291
+ if (!options.purge) return result;
2292
+ const purged = purgeInstanceDir(resolvedRoot, name);
2293
+ if (purged && instance) clearBrowserFiles(instance.dirs.data, resolvedRoot);
2294
+ return { ...result, purged };
2290
2295
  }));
2291
2296
  }
2292
2297