@volter/world-runtime 3.0.13 → 3.0.14

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,27 @@ export function localWorldOrigin(served, port, base = LOCAL_ORIGIN_BASE) {
10
10
  const label = worldOriginLabel(served);
11
11
  return label ? `http://${label}.${base}:${port}` : null;
12
12
  }
13
+ /** THE CONTENT PLANE's label beside a World's own: `<world>--<org>--pages`, where a browser sees the World's twins'
14
+ * pages apart from its doors and its session. A World of an org named `pages` (`<world>--pages`) is no content plane:
15
+ * its label names no World once the suffix is taken off, so it falls through to the World it is. */
16
+ export const PAGES_LABEL_SUFFIX = '--pages';
17
+ /** The content plane's origin for `served` on a host whose World origins are `<label>.<base>`, or null when its names
18
+ * make no label. */
19
+ export function pagesOriginOf(served, protocol, base, port) {
20
+ const label = worldOriginLabel(served);
21
+ return label && label.length + PAGES_LABEL_SUFFIX.length <= 63 ? `${protocol}//${label}${PAGES_LABEL_SUFFIX}.${base}${port ? `:${port}` : ''}` : null;
22
+ }
23
+ /** Which World's content plane a request's host names, or undefined for any other host. */
24
+ export function pagesOfHost(hostname, served, base = LOCAL_ORIGIN_BASE) {
25
+ const suffix = `${PAGES_LABEL_SUFFIX}.${base}`;
26
+ if (!hostname.endsWith(suffix))
27
+ return undefined;
28
+ const label = hostname.slice(0, -suffix.length);
29
+ for (const name of served)
30
+ if (worldOriginLabel(name) === label)
31
+ return name;
32
+ return undefined;
33
+ }
13
34
  /** Which World a request's host names: undefined for a host that is not a World's origin (the host's
14
35
  * own, path-addressed), null for a World-origin-shaped host naming no World served here. */
15
36
  export function worldOfHost(hostname, served, base = LOCAL_ORIGIN_BASE) {
@@ -8,7 +8,7 @@ import { join, resolve } from 'node:path';
8
8
  import { serveHttp, stateDirName } from '@volter/world-core/runtime';
9
9
  import { followParentPins, LocalBranches } from "./local-branches.js";
10
10
  import { mountWorld } from "./served-world.js";
11
- import { localWorldOrigin, pathOfWorld, siteOfHost, worldDoorUnderConsole, worldOfHost } from "./world-origins.js";
11
+ import { localWorldOrigin, pagesOfHost, pagesOriginOf, pathOfWorld, siteOfHost, worldDoorUnderConsole, worldOfHost } from "./world-origins.js";
12
12
  export const VIEW_CONSOLE_BASE = '/-/console';
13
13
  /** The branch Worlds already under `dir` (`<dir>/<org>/<world>/.volter/world.json`). */
14
14
  function branchRoots(dir) {
@@ -50,7 +50,9 @@ export async function serveWorldView(name, opts = {}) {
50
50
  const browserOrigin = origins ? (served) => (proxied && (base === null || served === base) ? proxied.origin : port ? localWorldOrigin(served, port) : null) : undefined;
51
51
  // sites are routed at each World's own *.localhost origin, never behind the proxy
52
52
  const sitesOrigin = origins && !proxied ? (served) => (port ? localWorldOrigin(served, port) : null) : undefined;
53
- const mountAt = (at) => mountWorld(loadWorldConfig(join(at, stateDirName(), 'world.json'), at).config.id, { root: at, branches: (served) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(sitesOrigin ? { sitesOrigin } : {}), ...(local ? { localTrust: true, ...(opts.localPerson ? { localPerson: opts.localPerson } : {}) } : {}) });
53
+ // the content plane beside each World's origin (`<world>--<org>--pages.localhost`): its twins' pages, apart from its doors
54
+ const pagesOrigin = origins && !proxied ? (served) => (port ? pagesOriginOf(served, 'http:', 'localhost', port) : null) : undefined;
55
+ const mountAt = (at) => mountWorld(loadWorldConfig(join(at, stateDirName(), 'world.json'), at).config.id, { root: at, branches: (served) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(sitesOrigin ? { sitesOrigin } : {}), ...(pagesOrigin ? { pagesOrigin } : {}), ...(local ? { localTrust: true, ...(opts.localPerson ? { localPerson: opts.localPerson } : {}) } : {}) });
54
56
  const branches = new LocalBranches({ dir: join(root, stateDirName(), 'branches'), origin: () => loopback, worlds, mount: mountAt, ...(opts.announce ? { announce: opts.announce } : {}) });
55
57
  // THE WORLD'S OWN BRANCHES, one served at a time: serving another stops this one's twins and starts that one's under the
56
58
  // same served name (its tokens and sessions are the World's, so the page stays open); `.volter/current` follows, as
@@ -92,7 +94,7 @@ export async function serveWorldView(name, opts = {}) {
92
94
  switching = false;
93
95
  }
94
96
  };
95
- const mountOpts = { root, branches: (served) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(sitesOrigin ? { sitesOrigin } : {}), ...(local ? { localTrust: true, ...(opts.localPerson ? { localPerson: opts.localPerson } : {}) } : {}), serveBranch };
97
+ const mountOpts = { root, branches: (served) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(sitesOrigin ? { sitesOrigin } : {}), ...(pagesOrigin ? { pagesOrigin } : {}), ...(local ? { localTrust: true, ...(opts.localPerson ? { localPerson: opts.localPerson } : {}) } : {}), serveBranch };
96
98
  let world = await mountWorld(name, mountOpts);
97
99
  base = world.served;
98
100
  worlds.set(world.served, world);
@@ -116,6 +118,10 @@ export async function serveWorldView(name, opts = {}) {
116
118
  const site = origins && !proxied ? siteOfHost(at.hostname, worlds.keys()) : undefined;
117
119
  if (site)
118
120
  return worlds.get(site.world).site(request, site.site);
121
+ // a World's content plane serves that World's twins' pages and nothing else of this view's (no console)
122
+ const pagesFor = origins && !proxied ? pagesOfHost(at.hostname, worlds.keys()) : undefined;
123
+ if (pagesFor)
124
+ return path === '/-/ping' ? Response.json({ ok: true, worlds: [pagesFor] }) : worlds.get(pagesFor).handle(request);
119
125
  const own = origins ? (proxied && at.host === proxied.host ? base : worldOfHost(at.hostname, worlds.keys())) : undefined;
120
126
  if (own === null)
121
127
  return Response.json({ error: `no world at ${at.host}` }, { status: 404 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "3.0.13",
3
+ "version": "3.0.14",
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.13",
72
- "@volter/world-console": "3.0.8"
71
+ "@volter/world-core": "3.0.14",
72
+ "@volter/world-console": "3.0.9"
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.13",
78
+ "@volter/world-core": "3.0.14",
79
79
  "pg-gateway": "0.3.0-beta.4",
80
80
  "smol-toml": "^1.8.0",
81
81
  "yaml": "2.9.0"
package/src/index.ts CHANGED
@@ -168,7 +168,7 @@ export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServe
168
168
  export type { BranchDoors, BranchRow, DoorHost, MountedWorld, ServeRecord, WorldLayout } from './served-world.ts';
169
169
  export { followParentPins, LocalBranches } from './local-branches.ts';
170
170
  export { serveWorldView, VIEW_CONSOLE_BASE } from './world-view.ts';
171
- export { LOCAL_ORIGIN_BASE, localWorldOrigin, pathOfWorld, siteOfHost, worldDoorUnderConsole, worldOfHost } from './world-origins.ts';
171
+ export { LOCAL_ORIGIN_BASE, localWorldOrigin, pagesOfHost, pagesOriginOf, pathOfWorld, siteOfHost, worldDoorUnderConsole, worldOfHost } from './world-origins.ts';
172
172
  export type { ViewConsole, WorldView } from './world-view.ts';
173
173
  export type { LocalBranchesOptions } from './local-branches.ts';
174
174
  export type { ServedWorld } from './served-world.ts';
package/src/init.ts CHANGED
@@ -1272,6 +1272,7 @@ credentials/
1272
1272
  token
1273
1273
  token.read
1274
1274
  sessions.json
1275
+ pages-sessions.json
1275
1276
  keys.json
1276
1277
  `;
1277
1278
 
@@ -93,13 +93,10 @@ export class LocalBranches {
93
93
  list: async () => this.list(parent),
94
94
  create: (at, ttl, _origin, parentKey, byKey) => this.create(parent, at, ttl, parentKey, byKey),
95
95
  remove: async (name) => { const w = this.o.worlds.get(name); if (!w || this.record(w)?.from !== parent) return false; await this.remove(name); return true; },
96
- // a branch of this parent only, served in this process beside it
97
- show: async (name, request, vendor, rest, prefix) => {
96
+ // a branch of this parent only, served in this process beside it: its own content plane's pass
97
+ pagesPass: async (name, scope, land, grant) => {
98
98
  const w = this.o.worlds.get(name);
99
- const r = w ? this.record(w) : null;
100
- if (!w || r?.from !== parent) return Response.json({ error: `no branch ${name} of ${parent}` }, { status: 404 });
101
- if (!r.at?.instant || r.at.label || r.at.live) return Response.json({ error: `${name} is not a copy as of an instant: its pages are shown at its own address` }, { status: 403 });
102
- return await w.shown(request, vendor, rest, prefix);
99
+ return w && this.record(w)?.from === parent ? w.pagesPass(scope, land, grant) : null;
103
100
  },
104
101
  };
105
102
  }
@@ -161,7 +158,7 @@ export class LocalBranches {
161
158
  if (clock) { const file = clockFile(root, id); mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, `${clock}\n`); }
162
159
  const createdAt = new Date().toISOString();
163
160
  const expiresAt = ttlSeconds === null ? null : new Date(Date.now() + ttlSeconds * 1000).toISOString();
164
- // what it was made as (a pull request's preview is labelled and may be live): what may be shown of it rests on that
161
+ // what it was made as (a pull request's preview is labelled and may be live)
165
162
  const made = { ...(at.instant ? { instant: at.instant } : {}), ...(at.label ? { label: at.label } : {}), ...(at.live ? { live: true } : {}) };
166
163
  writeFileSync(join(root, stateDirName(), RECORD), `${JSON.stringify({ from: parent, at: Object.keys(made).length ? made : null, createdAt, expiresAt } satisfies BranchRecord, null, 2)}\n`);
167
164
  this.o.announce?.(`branch ${name} of ${parent}${at.instant ? ` as of ${at.instant}` : ''}${expiresAt ? `, until ${expiresAt}` : ''}`);
@@ -159,11 +159,17 @@ function configRefFor(worldRoot: string, name: string): string {
159
159
  /** A world mounted by a host. `token`/`readToken` read the CURRENT tokens (rotate changes them);
160
160
  * `twins()` names the world's twins once booted; `rotate()` mints both tokens anew — the old die
161
161
  * with the call — and keeps them beside the world like `boot` does. */
162
- export type MountedWorld = { name: string; served: string; root: string; readonly token: string; readonly readToken: string; twins: () => string[]; rotate: () => { token: string; readToken: string }; handle: (request: Request) => Promise<Response>; /** a hostname this World's twins answer, as a browser reads it (siteOfHost) */ site: (request: Request, host: string) => Promise<Response>; /** WorldDoors.shown */ shown: (request: Request, vendor: string, rest: string, prefix: string) => Promise<Response>; boot: (url: string) => Promise<void>; stop: () => Promise<void> };
162
+ export type MountedWorld = { name: string; served: string; root: string; readonly token: string; readonly readToken: string; twins: () => string[]; rotate: () => { token: string; readToken: string }; handle: (request: Request) => Promise<Response>; /** a hostname this World's twins answer, as a browser reads it (siteOfHost) */ site: (request: Request, host: string) => Promise<Response>; /** WorldDoors.pagesPass */ pagesPass: (scope: 'read' | 'write', land: string, grant?: { who?: string; framer?: string; asker?: string }) => string | null; boot: (url: string) => Promise<void>; stop: () => Promise<void> };
163
163
  /** A browser session a World keeps: its scope, when it ends, and the person a pass named, when one did. */
164
+ /** What a content-plane pass, and the session it opens, carry: its scope; the control session that asked (the content
165
+ * session ends with it; none when the World's token asked); the person; the console that frames it; and whether it
166
+ * is this machine's own page (attributed to the machine). */
167
+ type PagesGrant = { scope: 'read' | 'write'; session?: string; who?: string; framer?: string; local?: true; /** a parent World's session that asked
168
+ * for a branch's page, as an opaque mark: the branch keeps one content session per asker and caps passes by it, but
169
+ * cannot tell when it ends, so such a session lasts an hour */ asker?: string };
164
170
  type HeldSession = { scope: 'read' | 'write'; until: number; /** opened by this machine's own page (localSession) */ local?: true; who?: string; /** the platform whose pass opened it */ issuer?: string; /** the person's subject there */ sub?: string; /** the read key a shared link opened it with: it lasts only while that key does */ key?: string };
165
171
 
166
- export async function mountWorld(name: string, opts: { /** the platforms whose passes open this World */ passIssuers?: TrustedIssuer[]; root?: string; /** the host's branches of this World, by its served name */ branches?: (served: string) => BranchDoors | undefined; /** the origin a browser reaches this World at, when the host gives each World one */ browserOrigin?: (served: string) => string | null; /** DoorHost.sitesOrigin */ sitesOrigin?: (served: string) => string | null; /** served on this machine's loopback: its own pages need no token (DoorHost.localTrust) */ localTrust?: boolean; /** the person signed in on this machine: its own page acts as them (DoorHost.localPerson) */ localPerson?: string; /** DoorHost.serveBranch */ serveBranch?: (branch: string) => Promise<void> } = {}): Promise<MountedWorld> {
172
+ export async function mountWorld(name: string, opts: { /** the platforms whose passes open this World */ passIssuers?: TrustedIssuer[]; root?: string; /** the host's branches of this World, by its served name */ branches?: (served: string) => BranchDoors | undefined; /** the origin a browser reaches this World at, when the host gives each World one */ browserOrigin?: (served: string) => string | null; /** DoorHost.sitesOrigin */ sitesOrigin?: (served: string) => string | null; /** DoorHost.pagesOrigin */ pagesOrigin?: (served: string) => string | null; /** served on this machine's loopback: its own pages need no token (DoorHost.localTrust) */ localTrust?: boolean; /** the person signed in on this machine: its own page acts as them (DoorHost.localPerson) */ localPerson?: string; /** DoorHost.serveBranch */ serveBranch?: (branch: string) => Promise<void> } = {}): Promise<MountedWorld> {
167
173
  const worldRoot = resolve(opts.root ?? process.cwd());
168
174
  const configRef = configRefFor(worldRoot, name);
169
175
  if (loadWorldConfig(configRef, worldRoot).config.resources) {
@@ -192,6 +198,7 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
192
198
  ...(opts.branches?.(served) ? { branches: opts.branches(served)! } : {}),
193
199
  ...(opts.browserOrigin ? { browserOrigin: () => opts.browserOrigin!(served), originOf: (other: string) => opts.browserOrigin!(other) } : {}),
194
200
  ...(opts.sitesOrigin ? { sitesOrigin: () => opts.sitesOrigin!(served) } : {}),
201
+ ...(opts.pagesOrigin ? { pagesOrigin: () => opts.pagesOrigin!(served) } : {}),
195
202
  ...(opts.localTrust ? { localTrust: () => true, ...(opts.localPerson ? { localPerson: () => opts.localPerson } : {}) } : {}),
196
203
  ...(opts.serveBranch ? { serveBranch: opts.serveBranch } : {}),
197
204
  ...(opts.passIssuers?.length ? { passIssuers: () => opts.passIssuers! } : {}),
@@ -215,7 +222,7 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
215
222
  rotate: () => { world.retoken(mintToken('tok_'), mintToken('tok_r_')); keep(); return { token: world.token, readToken: world.readToken }; },
216
223
  handle: (request) => world.handle(request),
217
224
  site: (request, host) => world.site(request, host),
218
- shown: (request, vendor, rest, prefix) => world.shown(request, vendor, rest, prefix),
225
+ pagesPass: (scope, land, grant) => world.pagesPass(scope, land, grant),
219
226
  async boot(url) {
220
227
  keep();
221
228
  const port = Number(new URL(url).port) || 0;
@@ -344,6 +351,10 @@ export type DoorHost = {
344
351
  * host gives each World one: its session lives only there (docs/contributing/architecture.md,
345
352
  * "Viewing a World"). Null, or absent, on a host whose Worlds share one origin. */
346
353
  browserOrigin?: () => string | null;
354
+ /** THE CONTENT PLANE: the origin this World's twins' pages are shown at for a browser (`<world>--<org>--pages`), apart
355
+ * from the origin of its doors and its session (docs/contributing/architecture.md, "Viewing a World"): a twin's page
356
+ * runs there with a session that uses the twins and opens no door. Null, or absent, where Worlds share one origin. */
357
+ pagesOrigin?: () => string | null;
347
358
  /** The origin under which this host routes the hostnames the World's twins answer, each at `<hostname>.<origin's host>`
348
359
  * (siteOfHost); none where it routes no sites (behind a proxy, on a hosted World). */
349
360
  sitesOrigin?: () => string | null;
@@ -377,9 +388,9 @@ export type BranchDoors = {
377
388
  create: (at: { instant?: string; live?: boolean; label?: string }, ttlSeconds: number | null, origin: string, parentKey?: string, /** the name of the key that made it: whoever holds a key gets a key of the branch (named for it),
378
389
  * never the branch's own token, so the branch's doors refuse it what a key is refused */ byKey?: string) => Promise<{ name: string; token: string; readToken: string; expiresAt: string | null }>;
379
390
  remove: (name: string) => Promise<boolean>;
380
- /** a page of a branch, read as its parent shows it (a vendor's screen at `prefix`, the parent's own path for it): where
381
- * the host holds the branch beside its parent; absent, a branch's pages are seen only at the branch's own address */
382
- show?: (name: string, request: Request, vendor: string, rest: string, prefix: string) => Promise<Response>;
391
+ /** the address that opens a branch's content plane with a one-time pass at `scope`, landing on `land` (a path of the
392
+ * branch): how its parent's console frames the branch's pages beside its own (the comparison) */
393
+ pagesPass?: (name: string, scope: 'read' | 'write', land: string, grant?: { who?: string; framer?: string; asker?: string }) => Promise<string | null>;
383
394
  };
384
395
 
385
396
  /** A World's browser-origin label, `<world>--<org>`, or null when its names cannot make one (a DNS
@@ -420,7 +431,133 @@ export class WorldDoors {
420
431
  constructor(readonly name: string, readonly worldRoot: string, readonly configRef: string, readonly served: string, private readonly host: DoorHost, token: string, readToken: string) { this.token = token; this.readToken = readToken; }
421
432
  /** New tokens; a request presenting the old ones is refused from the next call on. */
422
433
  // new tokens end everything the old ones reached: every session and every named key
423
- retoken(token: string, readToken: string): void { this.token = token; this.readToken = readToken; this.endSessions(); this.keepKeys([]); }
434
+ retoken(token: string, readToken: string): void { this.token = token; this.readToken = readToken; this.endSessions(); this.keepKeys([]); this.keepPagesSessions({}); this.pagePasses.clear(); }
435
+
436
+ /** THE CONTENT PLANE (DoorHost.pagesOrigin): a browser sees this World's twins' pages at an origin apart from its doors
437
+ * and its session, so a twin's page (another app's content, a pack's markup) runs with nothing that opens a door. A
438
+ * page there holds a session of its own, opened from a one-time pass the World's own origin gives its session or
439
+ * token: it uses the twins at its scope and nothing else. One per session that gave it a pass (every frame of a board
440
+ * shares it), kept in the World's store, ended with that session; framed only by the console that asked. */
441
+ private onPages(url: URL): boolean { const at = this.host.pagesOrigin?.() ?? null; return at !== null && url.host === new URL(at).host; }
442
+ private readonly pagePasses = new Map<string, PagesGrant & { until: number }>();
443
+ private pagesPath(): string { return join(this.worldRoot, stateDirName(), 'pages-sessions.json'); }
444
+ private pagesHeld: Record<string, PagesGrant & { until: number }> | null = null;
445
+ private pagesSessions(): Record<string, PagesGrant & { until: number }> {
446
+ if (this.pagesHeld === null) { try { this.pagesHeld = JSON.parse(getActiveWorldStore().read(this.pagesPath()) ?? '{}') as Record<string, PagesGrant & { until: number }>; } catch { this.pagesHeld = {}; } }
447
+ return this.pagesHeld;
448
+ }
449
+ private keepPagesSessions(all: Record<string, PagesGrant & { until: number }>): void {
450
+ const now = Date.now();
451
+ this.pagesHeld = Object.fromEntries(Object.entries(all).filter(([, x]) => x.until > now).sort(([, a], [, b]) => b.until - a.until).slice(0, 2000));
452
+ getActiveWorldStore().write(this.pagesPath(), `${JSON.stringify(this.pagesHeld)}\n`, { secret: true });
453
+ // never committed, as a World's other sessions are not: a `.volter/.gitignore` written before this file existed names it
454
+ if (!this.pagesIgnored) {
455
+ this.pagesIgnored = true;
456
+ const ignore = join(this.worldRoot, stateDirName(), '.gitignore');
457
+ const held = getActiveWorldStore().read(ignore);
458
+ if (held !== null && !held.split('\n').includes('pages-sessions.json')) getActiveWorldStore().write(ignore, `${held.replace(/\n?$/, '\n')}pages-sessions.json\n`);
459
+ }
460
+ }
461
+ private pagesIgnored = false;
462
+ /** The origin of the World this one is a branch of, whose console frames its pages beside its own: none for a World
463
+ * that is no branch (its record, written by the host that made it). */
464
+ private parentOrigin(): string | null {
465
+ try { const from = (JSON.parse(getActiveWorldStore().read(join(this.worldRoot, stateDirName(), 'branch.json')) ?? 'null') as { from?: string } | null)?.from; return from ? this.host.originOf?.(from) ?? null : null; } catch { return null; }
466
+ }
467
+ /** Whether what gave a content session its pass still stands: the control session that asked (ended, signed out, or
468
+ * its shared link revoked, the content session ends with it), or this World's token (rotated, every one ends). */
469
+ private pagesParentLive(grant: PagesGrant): boolean { return grant.session === undefined || this.issued().session(grant.session) !== null; }
470
+ /** The address that opens this World's content plane for a minute's one-time pass and lands on `land` (a path of this
471
+ * World); null where the World has no content plane, or the asker holds fifty passes not yet used. */
472
+ pagesPass(scope: 'read' | 'write', land: string, grant: Omit<PagesGrant, 'scope'> = {}): string | null {
473
+ const at = this.host.pagesOrigin?.() ?? null;
474
+ if (at === null) return null;
475
+ const now = Date.now();
476
+ for (const [k, v] of this.pagePasses) if (v.until <= now) this.pagePasses.delete(k);
477
+ const askerOf = (g: Omit<PagesGrant, 'scope'>): string => g.session ?? g.asker ?? g.framer ?? 'token';
478
+ const asker = askerOf(grant);
479
+ if ([...this.pagePasses.values()].filter((v) => askerOf(v) === asker).length >= 50 || this.pagePasses.size >= 5000) return null;
480
+ const pass = mintToken('pass_');
481
+ this.pagePasses.set(pass, { ...grant, scope, until: now + 60_000 });
482
+ return `${at}/-/${this.served}/pages/open#${new URLSearchParams({ pass, land }).toString()}`;
483
+ }
484
+ private pagesCookie(url: URL): string { return url.protocol === 'https:' ? '__Host-volter_pages' : 'volter_pages'; }
485
+ private pageSession(request: Request, url: URL): ({ id: string } & PagesGrant) | null {
486
+ const id = new RegExp(`(?:^|;\\s*)${this.pagesCookie(url)}=([^;]+)`).exec(request.headers.get('cookie') ?? '')?.[1];
487
+ const held = id && Object.hasOwn(this.pagesSessions(), id) ? this.pagesSessions()[id]! : undefined;
488
+ if (!id || !held || held.until <= Date.now() || !this.pagesParentLive(held)) return null;
489
+ const { until: _until, ...grant } = held;
490
+ return { id, ...grant };
491
+ }
492
+ /** What the content plane answers: its opening page and session, and the twins' pages; no door of the World. */
493
+ private async pagesPlane(request: Request, url: URL): Promise<Response> {
494
+ const doors = `/-/${this.served}/pages/`;
495
+ const door = url.pathname.startsWith(doors) ? url.pathname.slice(doors.length) : null;
496
+ const own = this.host.browserOrigin?.() ?? null;
497
+ if (door === 'open' && request.method === 'GET') {
498
+ // framed by this World's console, or its parent's (a comparison frames its branch's pages); the pages it opens are
499
+ // framed only by the console that asked
500
+ const consoles = [own, this.parentOrigin()].filter((x): x is string => Boolean(x)).join(' ') || "'none'";
501
+ return new Response(`<!doctype html><meta charset="utf-8"><title>${this.served}</title><body data-world="${this.served}"><script src="${doors}open.js"></script></body>`,
502
+ { headers: { 'content-type': 'text/html; charset=utf-8', 'content-security-policy': `default-src 'none'; script-src 'self'; connect-src 'self'; base-uri 'none'; frame-ancestors ${consoles}`, 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' } });
503
+ }
504
+ if (door === 'open.js' && request.method === 'GET') {
505
+ const script = `(() => {
506
+ const world = document.body.dataset.world;
507
+ const carried = new URLSearchParams(location.hash.slice(1));
508
+ history.replaceState(null, '', location.pathname);
509
+ const pass = carried.get('pass') || '';
510
+ const land = carried.get('land') || '';
511
+ // resolved as the browser will (encoded dots too), on this origin and under this World
512
+ let to = null;
513
+ try { const u = new URL(land, location.origin); if (land.startsWith('/') && !/[\\\\\\s]/.test(land) && u.origin === location.origin && u.pathname.startsWith('/' + world + '/')) to = u.pathname + u.search; } catch {}
514
+ if (!pass || !to) { document.body.textContent = 'Nothing to open.'; return; }
515
+ fetch('/-/' + world + '/pages/session', { method: 'POST', headers: { 'x-volter-pages-pass': pass } })
516
+ .then((r) => { if (r.ok) location.replace(to); else document.body.textContent = 'This World did not open.'; })
517
+ .catch(() => { document.body.textContent = 'This World did not answer.'; });
518
+ })();
519
+ `;
520
+ return new Response(script, { headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-store' } });
521
+ }
522
+ if (door === 'session' && request.method === 'POST') {
523
+ // only this plane's own opening page asks (a browser sets the header; a page elsewhere cannot)
524
+ if (request.headers.get('sec-fetch-site') !== 'same-origin') return Response.json({ error: 'the content plane opens its session from its own page' }, { status: 403 });
525
+ const pass = request.headers.get('x-volter-pages-pass') ?? '';
526
+ const given = this.pagePasses.get(pass);
527
+ this.pagePasses.delete(pass);
528
+ if (!given || given.until <= Date.now() || !this.pagesParentLive(given)) return Response.json({ error: 'that pass is used or expired: open the page again from the World' }, { status: 401 });
529
+ const { until: _until, ...grant } = given;
530
+ // one content session per asker at a scope and framer: a board's frames opening together share it
531
+ const same = (x: PagesGrant): boolean => x.scope === grant.scope && x.session === grant.session && x.asker === grant.asker && x.framer === grant.framer && x.who === grant.who;
532
+ const now = Date.now();
533
+ const found = Object.entries(this.pagesSessions()).find(([, x]) => x.until > now && same(x))?.[0];
534
+ const id = found ?? mintToken('pgs_');
535
+ // twelve hours from its last opening while the session that asked stands; an hour where that cannot be known
536
+ this.keepPagesSessions({ ...this.pagesSessions(), [id]: { ...grant, until: now + (grant.session !== undefined || grant.asker === undefined ? 12 : 1) * 3_600_000 } });
537
+ return Response.json({ world: this.served, scope: grant.scope }, { headers: { 'set-cookie': `${this.pagesCookie(url)}=${id}; Path=/; HttpOnly; Secure; SameSite=None; Partitioned` } });
538
+ }
539
+ if (url.pathname.startsWith(`/${this.served}/`) && !url.pathname.startsWith(`/${this.served}/.well-known/`)) {
540
+ const answer = await this.wire(request, url, url.pathname.slice(`/${this.served}/`.length));
541
+ if (answer.status === 101) return answer;
542
+ const out = new Headers(answer.headers);
543
+ // framed by the console whose session asked, and by the page itself
544
+ const framer = this.pageSession(request, url)?.framer ?? own;
545
+ out.append('content-security-policy', `frame-ancestors 'self'${framer ? ` ${framer}` : ''}`);
546
+ // a twin's own sign-in kept in the frame a console shows it in, which may be another site (each *.localhost is):
547
+ // its cookies are partitioned by that console, as this plane's own session is
548
+ const cookies = answer.headers.getSetCookie();
549
+ if (cookies.length) {
550
+ out.delete('set-cookie');
551
+ for (const c of cookies) out.append('set-cookie', `${c.replace(/;\s*(?:samesite=[^;]*|secure|partitioned)\s*(?=;|$)/gi, '')}; SameSite=None; Secure; Partitioned`);
552
+ }
553
+ return new Response(answer.body, { status: answer.status, statusText: answer.statusText, headers: out });
554
+ }
555
+ // a navigation elsewhere (a twin sending the person to its root, `/`) goes to the World's own origin, as it always did
556
+ if (own && request.method === 'GET' && ['document', 'iframe'].includes(request.headers.get('sec-fetch-dest') ?? '') && !url.pathname.startsWith('/-/')) {
557
+ return new Response(null, { status: 303, headers: { location: `${own}${url.pathname}${url.search}`, 'cache-control': 'no-store' } });
558
+ }
559
+ return Response.json({ error: `this origin shows ${this.served}'s pages only` }, { status: 404 });
560
+ }
424
561
 
425
562
  /** BROWSER SESSIONS: the session cookie carries an opaque id, never a token, so a leaked cookie is one session and
426
563
  * not the World's write token; rotating the tokens ends every session. Kept in the World's store, so a restart
@@ -613,6 +750,9 @@ export class WorldDoors {
613
750
  };
614
751
  }
615
752
  private credential(request: Request): { scope: 'write' | 'read' | null; via: 'token' | 'key' | 'session' | null; token: string | null; /** the session's id, when the session opened it */ session?: string; /** who the session is */ who?: string } {
753
+ // on the content plane only its own session counts: a page there presents nothing that opens a door
754
+ const at = new URL(request.url);
755
+ if (this.onPages(at)) { const held = this.pageSession(request, at); return held ? { scope: held.scope, via: 'session', token: null, session: `pages:${held.id}`, ...(held.who ? { who: held.who } : {}) } : { scope: null, via: null, token: null }; }
616
756
  const presented = presentedSecrets(request.headers, { token: TOKEN_HEADER, twinsKey: TWINS_KEY_HEADER });
617
757
  const name = this.sessionCookieName(new URL(request.url));
618
758
  // the name is one of two fixed literals (sessionCookieName); the separator browsers use is "; "
@@ -658,7 +798,7 @@ export class WorldDoors {
658
798
  const cred = this.credential(request);
659
799
  if (cred.scope === null) return null;
660
800
  // this machine's own page with no one signed in here is this machine; another session with no name is just that
661
- if (cred.via === 'session') return cred.who ? `person:${cred.who}` : cred.session && this.sessions()[cred.session]?.local ? 'machine' : 'person';
801
+ if (cred.via === 'session') return cred.who ? `person:${cred.who}` : cred.session && (cred.session.startsWith('pages:') ? this.pagesSessions()[cred.session.slice(6)]?.local : this.sessions()[cred.session]?.local) ? 'machine' : 'person';
662
802
  // a key acts as itself, for its person (docs/contributing/architecture.md, "Who acted is recorded")
663
803
  if (cred.via === 'key') { const held = this.keyOf(cred.token); const person = held ? this.keyName(held) : undefined; return held ? `key:${held.name}${person ? ` for:${person}` : ''}` : 'key'; }
664
804
  return cred.scope === 'read' ? 'token:read' : 'token';
@@ -739,25 +879,6 @@ export class WorldDoors {
739
879
  return out;
740
880
  }
741
881
 
742
- /** A vendor's page of this World as another World shows it at `prefix` (a parent showing its branch, the comparison's
743
- * "before"): read with this World's read token (a twin that enforces the read scope refuses any write), its links
744
- * minted under `prefix`.
745
- * The host calls it for the parent, which has checked its own caller; nothing of this World's session is involved. */
746
- async shown(request: Request, vendor: string, rest: string, prefix: string): Promise<Response> {
747
- if (request.method !== 'GET' && request.method !== 'HEAD') return Response.json({ error: "a copy's pages are read: GET and HEAD" }, { status: 405 });
748
- if (!this.host.ready()) return Response.json({ error: 'booting', world: this.served }, { status: 503, headers: { 'retry-after': '1' } });
749
- // a twin's pages only: a link forwards to the vendor itself, never something shown on another World's origin
750
- if (this.link(vendor) || !this.host.twinFetch(vendor)) return Response.json({ error: `no twin "${vendor}" in ${this.served}` }, { status: 404 });
751
- const url = new URL(request.url);
752
- // only what a browser's read of a page says, as a site's read (site()): never a caller's credential, cookie, or
753
- // the signed-address headers a session may not name
754
- const headers = new Headers();
755
- for (const name of ['accept', 'accept-language', 'if-none-match', 'if-modified-since', 'range', 'user-agent', 'sec-fetch-dest']) { const v = request.headers.get(name); if (v !== null) headers.set(name, v); }
756
- headers.set(TOKEN_HEADER, this.readToken);
757
- const at = new URL(`${url.origin}/${this.served}/${vendor}/${rest}${url.search}`);
758
- return await this.wire(new Request(at, { method: request.method, headers, redirect: 'manual' }), at, `${vendor}/${rest}`, prefix);
759
- }
760
-
761
882
  /** A hostname one of this World's twins answers (its hostsClaimed door lists it), read as a browser reads it there:
762
883
  * the request goes to that twin with the hostname as its original host, as the injector routes it inside the World.
763
884
  * Reads only: the site is the twin's state as the World holds it, never a way to write it. */
@@ -792,6 +913,7 @@ export class WorldDoors {
792
913
  const url = new URL(request.url);
793
914
  const prefix = `/${this.served}/`;
794
915
  if (url.pathname === '/-/ping') return Response.json({ ok: true, world: this.served, worlds: [this.served] });
916
+ if (this.onPages(url)) return await this.pagesPlane(request, url);
795
917
  if (url.pathname.startsWith(`/-/${this.served}/`)) return await this.door(request, url, url.pathname.slice(`/-/${this.served}/`.length));
796
918
  // the attach manifest (@volter/world-core/attach, VOLTER_WORLD=<this World's URL>): every twin at its
797
919
  // place under this World, and the token the injector carries to them — the presented one
@@ -865,19 +987,36 @@ export class WorldDoors {
865
987
  }
866
988
 
867
989
  /** The vendor wire: `/<org>/<world>/<vendor>/<rest>` → the twin's own URL. */
868
- private async wire(request: Request, url: URL, rest: string, /** the path its twin's pages are shown at, when not this World's own */ shownAt?: string): Promise<Response> {
990
+ private async wire(request: Request, url: URL, rest: string): Promise<Response> {
869
991
  const [vendor, ...tail] = rest.split('/');
870
992
  const { scope, via } = this.credential(request);
871
993
  const manifestAsk = request.method === 'GET' && tail.join('/').replace(/\/+$/, '') === 'twin';
994
+ const browserAt = this.host.browserOrigin?.() ?? null;
995
+ if (scope === null && this.onPages(url) && browserAt && request.method === 'GET' && ['document', 'iframe'].includes(request.headers.get('sec-fetch-dest') ?? '')) {
996
+ return new Response(null, { status: 303, headers: { location: `${browserAt}${url.pathname}${url.search}`, 'cache-control': 'no-store' } });
997
+ }
872
998
  if (scope === null && !manifestAsk) return Response.json({ error: 'token required' }, { status: 401 });
873
999
  const foreign = this.crossOriginSession(request, via);
874
1000
  if (foreign) return foreign;
875
1001
  const reads = request.method === 'GET' || request.method === 'HEAD';
1002
+ // THE TWO PLANES: a browser's session never sees a twin's page on the World's own origin. Its navigation there goes to
1003
+ // the content plane with a pass (a frame or a tab alike); anything else its pages would send is refused. A page on the
1004
+ // content plane that has no session yet is sent back here to get one
1005
+ const navigation = request.method === 'GET' && ['document', 'iframe'].includes(request.headers.get('sec-fetch-dest') ?? '');
1006
+ const pagesAt = this.host.pagesOrigin?.() ?? null;
1007
+ if (pagesAt !== null && !this.onPages(url) && via === 'session') {
1008
+ const asked = this.credential(request);
1009
+ const to = navigation ? this.pagesPass(scope!, `${url.pathname}${url.search}`, { ...(asked.session ? { session: asked.session } : {}), ...(asked.who ? { who: asked.who } : {}), ...(this.host.browserOrigin?.() ? { framer: this.host.browserOrigin!()! } : {}), ...(asked.session && this.sessions()[asked.session]?.local ? { local: true as const } : {}) }) : null;
1010
+ if (to) return new Response(null, { status: 303, headers: { location: to, 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' } });
1011
+ return Response.json({ error: `${this.served}'s twins' pages are shown at ${pagesAt}` }, { status: 403 });
1012
+ }
876
1013
  // A LINK: a path of this World that forwards to the vendor itself with the credential sealed beside
877
1014
  // the World (the World holds the key; its callers hold the World's token). Journaled as a twin's request is.
878
1015
  // a person's browser opening a page at the twin's place (a navigation, or the frame the console shows it in)
879
1016
  const page = request.method === 'GET' && ['document', 'iframe'].includes(request.headers.get('sec-fetch-dest') ?? '');
880
1017
  const link = this.link(vendor!);
1018
+ // a link spends the World's sealed vendor credential: never from the content plane, where any twin's markup runs
1019
+ if (link && this.onPages(url)) return Response.json({ error: `${vendor} is linked to the vendor itself: it is used through the World's own origin` }, { status: 404 });
881
1020
  if (link) {
882
1021
  if (scope === 'read' && !reads) return Response.json({ error: 'the read token opens only reads' }, { status: 403 });
883
1022
  return await this.forward(request, url, tail.join('/'), vendor!, link.origin);
@@ -902,10 +1041,10 @@ export class WorldDoors {
902
1041
  // that would write for its caller is refused too); at any other, its GET passes as it always has
903
1042
  if (enforced) headers.set(READ_ONLY_HEADER, '1');
904
1043
  // the browser's World session is the World's credential, never the twin's (a tunnel would hand it on)
905
- const cookies = (headers.get('cookie') ?? '').split(/;\s*/).filter((c) => c && !/^(?:__Host-)?volter_world=/.test(c));
1044
+ const cookies = (headers.get('cookie') ?? '').split(/;\s*/).filter((c) => c && !/^(?:__Host-)?volter_(?:world|pages)=/.test(c));
906
1045
  if (cookies.length) headers.set('cookie', cookies.join('; ')); else headers.delete('cookie');
907
1046
  // the twin is mounted under this World's path: live URLs it mints must carry it (twinPublicBase)
908
- headers.set(TWIN_PREFIX_HEADER, shownAt ?? `/${this.served}/${vendor}`);
1047
+ headers.set(TWIN_PREFIX_HEADER, `/${this.served}/${vendor}`);
909
1048
  headers.set('x-forwarded-host', url.host); headers.set('x-forwarded-proto', url.protocol.replace(':', ''));
910
1049
  // where this World shows the hostnames its twins answer, for a page that links one (twinSiteUrl): under its own
911
1050
  // origin on this machine (siteOfHost); a World without one shows none
@@ -931,9 +1070,9 @@ export class WorldDoors {
931
1070
  // above): caches keep them apart
932
1071
  out.append('vary', 'Sec-Fetch-Dest');
933
1072
  const setCookies = answer.headers.getSetCookie();
934
- if (setCookies.some((c) => /^\s*(?:__Host-)?volter_world=/i.test(c))) {
1073
+ if (setCookies.some((c) => /^\s*(?:__Host-)?volter_(?:world|pages)=/i.test(c))) {
935
1074
  out.delete('set-cookie');
936
- for (const c of setCookies) if (!/^\s*(?:__Host-)?volter_world=/i.test(c)) out.append('set-cookie', c);
1075
+ for (const c of setCookies) if (!/^\s*(?:__Host-)?volter_(?:world|pages)=/i.test(c)) out.append('set-cookie', c);
937
1076
  }
938
1077
  return new Response(answer.body, { status: answer.status, headers: out });
939
1078
  }
@@ -1024,36 +1163,6 @@ export class WorldDoors {
1024
1163
  // ending a browser's session needs no token: it only clears the caller's own cookie, which never
1025
1164
  // reaches these /-/ paths, so a page holding just the session could not otherwise sign out
1026
1165
  const sessionPath = path.replace(/\/+$/, '');
1027
- // THE HAND-OFF: a page of no UI that opens this World's session from the token in its fragment, then goes to the page
1028
- // of this World it names (`land`): how a page elsewhere links to this World's own pages (the console's comparison
1029
- // opening a vendor's page in a copy of the World, read only). The token never reaches the server in a URL; its script
1030
- // is a file of this origin, so the page keeps a strict policy. It is never framed: a session opened in another
1031
- // site's frame is not this World's own page's
1032
- if (sessionPath === 'session/hand-off' && request.method === 'GET') {
1033
- return new Response(`<!doctype html><meta charset="utf-8"><title>${this.served}</title><body data-world="${this.served}"><script src="/-/${this.served}/session/hand-off.js"></script></body>`,
1034
- { headers: { 'content-type': 'text/html; charset=utf-8', 'content-security-policy': "default-src 'none'; script-src 'self'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'", 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' } });
1035
- }
1036
- if (sessionPath === 'session/hand-off.js' && request.method === 'GET') {
1037
- const script = `(() => {
1038
- const world = document.body.dataset.world;
1039
- const carried = new URLSearchParams(location.hash.slice(1));
1040
- history.replaceState(null, '', location.pathname);
1041
- const token = carried.get('token') || '';
1042
- const land = carried.get('land') || '';
1043
- // resolved as the browser will (encoded dots too), on this origin and under this World
1044
- let to = null;
1045
- try { const u = new URL(land, location.origin); if (land.startsWith('/') && !/[\\\\\\s]/.test(land) && u.origin === location.origin && u.pathname.startsWith('/' + world + '/')) to = u.pathname + u.search; } catch {}
1046
- const open = () => fetch('/-/' + world + '/session', { method: 'POST', headers: { 'x-volter-token': token } });
1047
- if (!token || !to) { document.body.textContent = 'Nothing to open.'; return; }
1048
- // a session this browser already holds here is kept: a page elsewhere cannot replace, downgrade or fix it
1049
- fetch('/-/' + world + '/session')
1050
- .then((held) => (held.ok ? held : open()))
1051
- .then((r) => { if (r.ok) location.replace(to); else document.body.textContent = 'This World did not open.'; })
1052
- .catch(() => { document.body.textContent = 'This World did not answer.'; });
1053
- })();
1054
- `;
1055
- return new Response(script, { headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-store' } });
1056
- }
1057
1166
  // a person signing in from a page (the console's form, posted to this origin): the token in the
1058
1167
  // form body, the session set here, then on to a page of this World
1059
1168
  // a form or beacon from another site never opens or ends a session here (the console, on this host's
@@ -1113,29 +1222,6 @@ export class WorldDoors {
1113
1222
  if (kind === 'session' && !name && request.method === 'POST' && via === 'key' && scope !== 'read') return Response.json({ error: 'a key opens no browser session: open the World with its token, or from the platform' }, { status: 403 });
1114
1223
  if (kind === 'session' && !name && (request.method === 'POST' || request.method === 'DELETE')) return this.session(request, url, scope, via === 'key' ? this.presentedKey(request) : undefined);
1115
1224
  if (kind === 'keys') return this.keysDoor(request, name, scope, via, this.credential(request).session);
1116
- // A COPY'S PAGES SHOWN HERE: `copies/<branch>/<vendor>/…`, a vendor's page in a branch of this World, read on this
1117
- // World's origin under its caller's credential (the comparison frames its "before" beside "now" this way: the
1118
- // branch's own session opens only on the branch's own pages). Only a copy as of an instant, which only a writer of
1119
- // this World makes, so its pages run here with nothing a stranger wrote (a labelled or live branch, a pull request's
1120
- // preview written by the app it tests, is shown at its own address). Read only; no cookie the copy sets is kept
1121
- if (kind === 'copies' && name && verb) {
1122
- const show = this.host.branches?.show;
1123
- if (!show) return Response.json({ error: "this host shows a copy's pages only at the copy's own address" }, { status: 501 });
1124
- const [org] = this.served.split('/');
1125
- // the path after the vendor as it came, empty segments and a trailing slash kept (an object key may hold `//`)
1126
- const full = `${org}/${name}`;
1127
- // made by the World's token or a person's session: a key's copy is written by whoever holds the key, and its pages
1128
- // would run here under the viewer's session
1129
- const madeBy = this.keys().find((k) => k.branch === full && (!k.expiresAt || Date.parse(k.expiresAt) > Date.now()))?.createdBy?.via;
1130
- if (madeBy !== 'token' && madeBy !== 'session') return Response.json({ error: `${full} was not made by this World's token or a person here: its pages are shown at its own address` }, { status: 403 });
1131
- // the path after the vendor as it came (empty segments and a trailing slash kept: an object key may hold `//`)
1132
- const exact = /^copies\/[^/]+\/[^/]+(?:\/(.*))?$/.exec(path);
1133
- if (!exact) return Response.json({ error: 'copies/<branch>/<vendor>/<path>' }, { status: 404 });
1134
- const answer = await show(full, request, verb, exact[1] ?? '', `/-/${this.served}/copies/${name}/${verb}`);
1135
- if (answer.status < 200) return Response.json({ error: "a copy's pages are read, never upgraded" }, { status: 400 });
1136
- const out = new Headers(answer.headers); out.delete('set-cookie');
1137
- return new Response(answer.body, { status: answer.status, statusText: answer.statusText, headers: out });
1138
- }
1139
1225
  if (scope === 'read' && request.method !== 'GET') return Response.json({ error: 'the read token opens only reads' }, { status: 403 });
1140
1226
 
1141
1227
  if (kind === 'clock') return await this.clockDoor(request, name);
@@ -1588,6 +1674,20 @@ export class WorldDoors {
1588
1674
  try { named = listWorlds(this.worldRoot).map((w) => ({ name: w.name, running: w.running })); } catch { /* a World with no local branches */ }
1589
1675
  return Response.json({ world: this.served, branches: await branches.list(), checkedOut: this.name, named, canServe: this.host.serveBranch !== undefined });
1590
1676
  }
1677
+ // A BRANCH'S PAGES (`GET branches/<org>/<branch>/pages?land=<path>`): a navigation to the branch's own content plane
1678
+ // with a one-time read pass, for a person or the World's token: how the console frames a branch's page beside this
1679
+ // World's (its comparison); the branch's pages never run on this World's origin
1680
+ if (rest.length === 3 && rest[2] === 'pages' && request.method === 'GET') {
1681
+ if (via !== 'session' && via !== 'token') return Response.json({ error: "a branch's pages open for a person or the World's token" }, { status: 403 });
1682
+ const name = `${decodeURIComponent(rest[0]!)}/${decodeURIComponent(rest[1]!)}`;
1683
+ if (!(await branches.list()).some((b) => b.name === name)) return Response.json({ error: `no branch ${name} of ${this.served}` }, { status: 404 });
1684
+ const land = url.searchParams.get('land') ?? `/${name}/`;
1685
+ const asked = this.credential(request);
1686
+ const mark = asked.session ? createHash('sha256').update(`${this.served}\0${asked.session}`).digest('base64url').slice(0, 22) : 'token';
1687
+ const to = branches.pagesPass ? await branches.pagesPass(name, 'read', land, { asker: mark, ...(asked.who ? { who: asked.who } : {}), ...(this.host.browserOrigin?.() ? { framer: this.host.browserOrigin!()! } : {}) }) : null;
1688
+ if (!to) return Response.json({ error: `this host shows ${name}'s pages only at its own address` }, { status: 501 });
1689
+ return new Response(null, { status: 303, headers: { location: to, 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' } });
1690
+ }
1591
1691
  // SERVE ANOTHER NAMED BRANCH here (`POST branches/<branch>/serve`): only who may write the World switches what it serves
1592
1692
  if (rest.length === 2 && rest[1] === 'serve' && request.method === 'POST') {
1593
1693
  if (!this.host.serveBranch) return Response.json({ error: `this host serves ${this.served} on one branch` }, { status: 404 });
@@ -13,6 +13,25 @@ export function localWorldOrigin(served: string, port: number, base = LOCAL_ORIG
13
13
  return label ? `http://${label}.${base}:${port}` : null;
14
14
  }
15
15
 
16
+ /** THE CONTENT PLANE's label beside a World's own: `<world>--<org>--pages`, where a browser sees the World's twins'
17
+ * pages apart from its doors and its session. A World of an org named `pages` (`<world>--pages`) is no content plane:
18
+ * its label names no World once the suffix is taken off, so it falls through to the World it is. */
19
+ export const PAGES_LABEL_SUFFIX = '--pages';
20
+ /** The content plane's origin for `served` on a host whose World origins are `<label>.<base>`, or null when its names
21
+ * make no label. */
22
+ export function pagesOriginOf(served: string, protocol: string, base: string, port?: number | string): string | null {
23
+ const label = worldOriginLabel(served);
24
+ return label && label.length + PAGES_LABEL_SUFFIX.length <= 63 ? `${protocol}//${label}${PAGES_LABEL_SUFFIX}.${base}${port ? `:${port}` : ''}` : null;
25
+ }
26
+ /** Which World's content plane a request's host names, or undefined for any other host. */
27
+ export function pagesOfHost(hostname: string, served: Iterable<string>, base = LOCAL_ORIGIN_BASE): string | undefined {
28
+ const suffix = `${PAGES_LABEL_SUFFIX}.${base}`;
29
+ if (!hostname.endsWith(suffix)) return undefined;
30
+ const label = hostname.slice(0, -suffix.length);
31
+ for (const name of served) if (worldOriginLabel(name) === label) return name;
32
+ return undefined;
33
+ }
34
+
16
35
  /** Which World a request's host names: undefined for a host that is not a World's origin (the host's
17
36
  * own, path-addressed), null for a World-origin-shaped host naming no World served here. */
18
37
  export function worldOfHost(hostname: string, served: Iterable<string>, base = LOCAL_ORIGIN_BASE): string | null | undefined {