@volter/world-core 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.
Files changed (45) hide show
  1. package/dist/host-routing.cjs +106 -0
  2. package/dist/host-routing.d.cts +7 -0
  3. package/dist/inject.cjs +5 -8
  4. package/dist/src/board-keys.d.ts +6 -0
  5. package/dist/src/board-keys.js +4 -0
  6. package/dist/src/browser-cookie-jar.d.ts +9 -0
  7. package/dist/src/browser-cookie-jar.js +104 -0
  8. package/dist/src/browser-state.d.ts +52 -0
  9. package/dist/src/browser-state.js +105 -0
  10. package/dist/src/browser.d.ts +12 -0
  11. package/dist/src/browser.js +197 -0
  12. package/dist/src/derived-core.d.ts +3 -7
  13. package/dist/src/derived.d.ts +1 -0
  14. package/dist/src/index.d.ts +1 -0
  15. package/dist/src/index.js +1 -0
  16. package/dist/src/pack-fetch.d.ts +0 -3
  17. package/dist/src/pack-fetch.js +5 -26
  18. package/dist/src/public-suffixes.json +1 -0
  19. package/dist/src/runtime.d.ts +1 -1
  20. package/dist/src/runtime.js +1 -1
  21. package/dist/src/twin-fetch.d.ts +1 -1
  22. package/dist/src/twin-fetch.js +23 -4
  23. package/dist/vendor-hosts.cjs +13 -105
  24. package/dist/vendor-hosts.d.cts +3 -0
  25. package/host-routing.cjs +106 -0
  26. package/host-routing.d.cts +7 -0
  27. package/inject.cjs +5 -8
  28. package/package.json +11 -1
  29. package/src/board-keys.ts +4 -0
  30. package/src/browser-cookie-jar.ts +77 -0
  31. package/src/browser-session.ts +1 -1
  32. package/src/browser-state.ts +89 -0
  33. package/src/browser.ts +164 -0
  34. package/src/derived-core.ts +4 -5
  35. package/src/derived.ts +1 -1
  36. package/src/index.ts +2 -0
  37. package/src/pack-fetch.ts +6 -24
  38. package/src/public-suffixes.json +1 -0
  39. package/src/runtime.ts +2 -1
  40. package/src/twin-fetch.ts +9 -4
  41. package/vendor-hosts.cjs +13 -105
  42. package/vendor-hosts.d.cts +3 -0
  43. package/dist/src/browser-session-permit.d.ts +0 -13
  44. package/dist/src/browser-session-permit.js +0 -51
  45. package/src/browser-session-permit.ts +0 -43
package/src/browser.ts ADDED
@@ -0,0 +1,164 @@
1
+ import { BrowserCookieJar } from './browser-cookie-jar.ts';
2
+ import { assertBrowserName, browserOrigin, validateBrowserState, type BrowserState } from './browser-state.ts';
3
+ import { nodeBuiltin } from './serve-http.ts';
4
+ import { worldNow } from './world-clock.ts';
5
+
6
+ export { cookieMatchesHost } from './browser-state.ts';
7
+
8
+ export type SeedBrowser = {
9
+ fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
10
+ storage(origin: string): { set(key: string, value: string): Promise<void>; remove(key: string): Promise<void> };
11
+ state(): Promise<BrowserState>;
12
+ };
13
+
14
+ /** Undo the request's path mount before its answer reaches a vendor-addressed client. */
15
+ function vendorResponse(response: Response, vendor: URL, twin: URL, mount: string): Response {
16
+ if (!mount) return response;
17
+ const pathAtVendor = (path: string): string => path === mount ? '/' : path.startsWith(`${mount}/`) ? path.slice(mount.length) : path;
18
+ const urlAtVendor = (value: string): string => {
19
+ if (!value.trim()) return value;
20
+ let at: URL;
21
+ try { at = new URL(value, twin); } catch { return value; }
22
+ if (at.origin !== twin.origin && at.origin !== vendor.origin) return value;
23
+ const path = pathAtVendor(at.pathname);
24
+ if (path === at.pathname) return value;
25
+ const tail = `${path}${at.search}${at.hash}`;
26
+ return /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(value.trim()) ? `${vendor.origin}${tail}` : tail;
27
+ };
28
+ const headers = new Headers(response.headers);
29
+ let changed = false;
30
+ const cookies = response.headers.getSetCookie();
31
+ const mapped = cookies.map(cookie => cookie.replace(/(;\s*path\s*=)([^;]*)/gi, (attribute, lead: string, path: string) => {
32
+ const at = pathAtVendor(path.trim());
33
+ return at === path.trim() ? attribute : `${lead}${at}`;
34
+ }));
35
+ if (mapped.some((cookie, i) => cookie !== cookies[i])) {
36
+ headers.delete('set-cookie');
37
+ for (const cookie of mapped) headers.append('set-cookie', cookie);
38
+ changed = true;
39
+ }
40
+ for (const name of ['location', 'content-location']) {
41
+ const value = headers.get(name);
42
+ if (value === null) continue;
43
+ const at = urlAtVendor(value);
44
+ if (at !== value) { headers.set(name, at); changed = true; }
45
+ }
46
+ const refresh = headers.get('refresh');
47
+ if (refresh !== null) {
48
+ const parts = /^(\s*\d+(?:\.\d+)?\s*[;,]\s*(?:url\s*=\s*)?)(?:"([^"]*)"|'([^']*)'|([^"']*?))(\s*)$/i.exec(refresh);
49
+ if (parts) {
50
+ const value = parts[2] ?? parts[3] ?? parts[4]!;
51
+ const at = urlAtVendor(value);
52
+ if (at !== value) {
53
+ const quote = parts[2] !== undefined ? '"' : parts[3] !== undefined ? "'" : '';
54
+ headers.set('refresh', `${parts[1]}${quote}${at}${quote}${parts[5]}`);
55
+ changed = true;
56
+ }
57
+ }
58
+ }
59
+ return changed ? new Response(response.body, { status: response.status, statusText: response.statusText, headers }) : response;
60
+ }
61
+
62
+ /** A seed's named browser. Every state write goes through the World's single HTTP loader. */
63
+ export async function browser(name: string): Promise<SeedBrowser> {
64
+ assertBrowserName(name);
65
+ const door = process.env.VOLTER_WORLD_BROWSER_DOOR;
66
+ const token = process.env.VOLTER_WORLD_BROWSER_TOKEN;
67
+ if (!process.env.VOLTER_WORLD || !door || !token) throw new Error('browser(name) runs in a World seed, with its browsers door and credential');
68
+ const require = nodeBuiltin<typeof import('node:module')>('module').createRequire(import.meta.url);
69
+ const routing = require('../vendor-hosts.cjs') as typeof import('../vendor-hosts.cjs');
70
+ const { packs } = require('../pack-facts.cjs').packFacts() as { packs: Record<string, { hostsClaimed?: { door: string } }> };
71
+ const resolve = async (url: URL): Promise<{ vendor: string; origin: string } | null> => {
72
+ const map = routing.twinOrigins(process.env);
73
+ const declared = routing.resolveTwinHost(url.hostname, map, url.pathname);
74
+ if (declared) return declared;
75
+ const claimed: Record<string, Set<string>> = {};
76
+ for (const [vendor, facts] of Object.entries(packs)) {
77
+ const origin = map[vendor] ?? process.env[`${routing.twinEnvStem(vendor)}_TWIN_URL`];
78
+ if (!origin || !facts.hostsClaimed) continue;
79
+ map[vendor] = origin;
80
+ const headers = new Headers({ 'x-volter-read-only': '1' });
81
+ if (process.env.VOLTER_TWINS_KEY) headers.set('x-twins-key', process.env.VOLTER_TWINS_KEY);
82
+ const listed = await fetch(`${origin}${facts.hostsClaimed.door}`, { headers })
83
+ .then(r => r.ok ? r.json() as Promise<{ hosts?: unknown }> : null).catch(() => null);
84
+ claimed[vendor] = new Set(Array.isArray(listed?.hosts) ? listed.hosts.map(h => String(h).toLowerCase()) : []);
85
+ }
86
+ return routing.resolveTwinHost(url.hostname, map, url.pathname, claimed);
87
+ };
88
+ const initial = await fetch(`${door}/${name}`, { headers: { 'x-volter-token': token } });
89
+ if (!initial.ok && initial.status !== 404) throw new Error(`browser ${name}: ${initial.status} ${await initial.text()}`);
90
+ const stored = initial.ok ? validateBrowserState(await initial.json()) : { cookies: [], origins: [] };
91
+ const jar = new BrowserCookieJar(stored.cookies);
92
+ const origins = structuredClone(stored.origins);
93
+ let queue: Promise<unknown> = Promise.resolve();
94
+ const serial = <T>(fn: () => Promise<T>): Promise<T> => { const next = queue.then(fn); queue = next.then(() => undefined, () => undefined); return next; };
95
+ const snapshot = (): BrowserState => {
96
+ const now = new Date(worldNow()).getTime();
97
+ const cookies = jar.snapshot(now);
98
+ return validateBrowserState({ cookies, origins: structuredClone(origins) });
99
+ };
100
+ const save = async (): Promise<void> => {
101
+ const response = await fetch(`${door}/${name}`, { method: 'PUT', headers: { 'content-type': 'application/json', 'x-volter-token': token }, body: JSON.stringify(snapshot()) });
102
+ if (!response.ok) throw new Error(`browser ${name}: ${response.status} ${await response.text()}`);
103
+ };
104
+ if (initial.status === 404) await save();
105
+ return {
106
+ state: () => serial(async () => snapshot()),
107
+ storage(origin) {
108
+ browserOrigin(origin);
109
+ const change = (key: string, value?: string): Promise<void> => serial(async () => {
110
+ if (typeof key !== 'string' || (value !== undefined && typeof value !== 'string')) throw new Error('browser storage: string keys and values');
111
+ const before = JSON.stringify(origins);
112
+ let entry = origins.find(o => o.origin === origin);
113
+ if (!entry && value !== undefined) { entry = { origin, localStorage: [] }; origins.push(entry); }
114
+ if (entry) {
115
+ entry.localStorage = entry.localStorage.filter(v => v.name !== key);
116
+ if (value !== undefined) entry.localStorage.push({ name: key, value });
117
+ }
118
+ if (JSON.stringify(origins) !== before) await save();
119
+ });
120
+ return { set: (key, value) => change(key, value), remove: key => change(key) };
121
+ },
122
+ fetch: (input, init) => serial(async () => {
123
+ const request = new Request(input, init);
124
+ let url = new URL(request.url);
125
+ let method = request.method;
126
+ let body: ArrayBuffer | undefined = request.body ? await request.arrayBuffer() : undefined;
127
+ const headers = new Headers(request.headers);
128
+ if (headers.has('cookie')) throw new Error('browser.fetch carries its own cookie jar');
129
+ for (let redirects = 0; ; redirects++) {
130
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) throw new Error('browser.fetch takes an http or https vendor URL');
131
+ const target = await resolve(url);
132
+ if (!target || target.vendor === 'app') throw new Error(`browser ${name}: no twin in this World serves ${url.hostname}${url.pathname}`);
133
+ const at = new URL(target.origin);
134
+ const mount = at.pathname.replace(/\/+$/, '');
135
+ at.pathname = `${mount}${url.pathname}`;
136
+ at.search = url.search;
137
+ const sent = new Headers(headers);
138
+ const cookies = jar.header(url, new Date(worldNow()).getTime());
139
+ if (cookies) sent.set('cookie', cookies);
140
+ sent.set('host', mount ? at.host : url.host);
141
+ sent.set('x-volter-twin-original-host', url.host);
142
+ sent.set('x-forwarded-host', url.host);
143
+ sent.set('x-forwarded-proto', url.protocol.slice(0, -1));
144
+ if (process.env.VOLTER_TWINS_KEY) sent.set('x-twins-key', process.env.VOLTER_TWINS_KEY);
145
+ const before = JSON.stringify(snapshot());
146
+ const response = vendorResponse(await fetch(at, { method, headers: sent, body, redirect: 'manual', signal: request.signal }), url, at, mount);
147
+ for (const cookie of response.headers.getSetCookie()) jar.set(cookie, url, new Date(worldNow()).getTime());
148
+ if (JSON.stringify(snapshot()) !== before) await save();
149
+ const location = response.headers.get('location');
150
+ if (!location || ![301, 302, 303, 307, 308].includes(response.status) || request.redirect === 'manual') return response;
151
+ if (request.redirect === 'error') throw new Error('browser.fetch refused a redirect');
152
+ if (redirects >= 20) throw new Error('browser.fetch exceeded the fetch redirect limit');
153
+ await response.body?.cancel();
154
+ const next = new URL(location, url);
155
+ if (next.origin !== url.origin) { headers.delete('authorization'); headers.delete('proxy-authorization'); }
156
+ if ((response.status === 303 && method !== 'HEAD') || ([301, 302].includes(response.status) && method === 'POST')) {
157
+ method = 'GET'; body = undefined;
158
+ for (const key of ['content-type', 'content-length', 'content-encoding', 'content-language', 'content-location']) headers.delete(key);
159
+ }
160
+ url = next;
161
+ }
162
+ }),
163
+ };
164
+ }
@@ -214,7 +214,7 @@ export type LaneRoute = {
214
214
  export type LanesDecl = { cors?: CorsDecl; routes: ReadonlyArray<LaneRoute>; default?: string };
215
215
  /** The manifest of a vendor whose every API is a lane (Cloudflare's API v4 and R2, AWS's services): no API of its own,
216
216
  * only what the vendor's front says — its discovery facts, its lanes and its descriptor. */
217
- export type VendorManifest = { vendor: string; discovery?: DerivedManifest['discovery']; lanes: LanesDecl; descriptor: Omit<PackDescriptor, 'vendor'>; ingest?: IngestDecl; rateBudget?: RateBudgetDeclaration; browserSession?: DerivedManifest['browserSession'];
217
+ export type VendorManifest = { vendor: string; discovery?: DerivedManifest['discovery']; lanes: LanesDecl; descriptor: Omit<PackDescriptor, 'vendor'>; ingest?: IngestDecl; rateBudget?: RateBudgetDeclaration;
218
218
  /** the vendor has no vendor-backed half (architecture, "The descriptor"): packOf copies it, and derives no state system */
219
219
  vendorBacked?: DerivedManifest['vendorBacked'] };
220
220
 
@@ -395,8 +395,6 @@ export type DerivedManifest = {
395
395
  resources: Record<string, ResourceDecl>;
396
396
  /** Read-only cross-pack credential subjects of the same vendor (architecture A3); no root is exposed to handlers. */
397
397
  ownerReads?: ReadonlyArray<{ owner: string; resource: string }>;
398
- /** The browser session of a World's person, issued with world-ui's kit. Declaring it lets the World offer View as. */
399
- browserSession?: { cookie: string; scope?: string };
400
398
  /** the vendor's screens this pack serves or owes (demand decides which exist) */
401
399
  screens?: ScreenDecl[];
402
400
  /** The discovery door's facts (`GET /twin`): what the twin is of, what it stores, and how it is authenticated. */
@@ -774,8 +772,9 @@ function view(m: DerivedManifest, resource: string, r: TwinResource): Record<str
774
772
  * a caller that pins an instant passes its own). */
775
773
  /** One call of a protocol-3 pack's default data (`src/semantics/seed.ts` exports `seed: SeedCall[]`): a request to the
776
774
  * vendor's own operation (or the pack's door), made in order against the twin by the runner `volter-world init` writes
777
- * beside the copied data. */
778
- export type SeedCall = { method: string; path: string; body?: unknown; headers?: Record<string, string> };
775
+ * beside the copied data. An ordinary path is relative to the twin base. A call naming a browser uses the
776
+ * vendor's absolute URL and is exported separately as `browserSeed`, after the ordinary `seed` calls. */
777
+ export type SeedCall = { browser?: string; method: string; path: string; body?: unknown; headers?: Record<string, string> };
779
778
 
780
779
  /** Where a call is answered: the World's root, its clock, and the managed Postgres the runtime binds to the pack
781
780
  * (`database`, the descriptor's `managedDatabase`), which a handler reaches as `ctx.engine` — READ ONLY in every
package/src/derived.ts CHANGED
@@ -208,7 +208,7 @@ export type DerivedFetch = ((request: Request) => Promise<Response>) & {
208
208
  };
209
209
 
210
210
  /** A workspace screen a pack draws, as `GET /twin` lists it: its id and path (Viewing a World). */
211
- export type Workspace = { id: string; path: string };
211
+ export type Workspace = { id: string; path: string; host?: string };
212
212
 
213
213
  /** A page of the World's board (architecture, "The board"): a page there is to see at the vendor, a record's own (an
214
214
  * account's profile, each of its posts, a site and each page it serves), as the vendor's pack lists them from its state
package/src/index.ts CHANGED
@@ -91,3 +91,5 @@ export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioErr
91
91
  export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioExtractorSpec, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord, ScenarioStatus } from './scenario.ts';
92
92
 
93
93
  export { browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.ts';
94
+
95
+ export { assertBrowserName, browserOrigin, validateBrowserState, cookieMatchesHost, type BrowserCookie, type BrowserState, type BrowserIndexedDB } from './browser-state.ts';
package/src/pack-fetch.ts CHANGED
@@ -5,8 +5,6 @@
5
5
  // `screens/<id>.tsx`), and the API (the derived dispatch over the generated surface, the handlers and the core). Every
6
6
  // one of them is answered over the contract's context, which the kernel opens: a pack opens none, reads the tree and
7
7
  // writes it only through one.
8
- import { startBrowserSession, endBrowserSession } from './browser-session.ts';
9
- import { BROWSER_SESSION_PATH, takeBrowserSessionPermit } from './browser-session-permit.ts';
10
8
  import { ORIGINAL_PATH_HEADER } from './sigv4.ts';
11
9
  import { placedCookies, TWIN_PREFIX_HEADER } from './twin-fetch.ts';
12
10
  import { PREFIX_PARAMS_HEADER } from './derived.ts';
@@ -22,7 +20,7 @@ import { withCors } from './cors.ts';
22
20
  import type { ApplicationStandIn } from './events.ts';
23
21
  import { contentAddressOf } from './content-route.ts';
24
22
  import { getPack, type HostRule } from './packRegistry.ts';
25
- import { hostRules } from '../vendor-hosts.cjs';
23
+ import { hostRules } from '../host-routing.cjs';
26
24
 
27
25
  /** A descriptor's host rules, as data. */
28
26
  type PackDescriptorHosts = ReadonlyArray<HostRule> | undefined;
@@ -79,11 +77,9 @@ export type PackScenario<Req = any> = { adapter: PackScenarioAdapter<Req>; opera
79
77
  /** A lane as its vendor's fetch is given it: its own pack fetch, and its manifest (the doors it declares). */
80
78
  export type LanePart = {
81
79
  fetch: ((request: Request) => Promise<Response>) & {
82
- serves?: (method: string, path: string) => boolean; boards?: boolean; drawsScreens?: boolean; draws?: (url: string) => boolean;
80
+ serves?: (method: string, path: string) => boolean; boards?: boolean; draws?: (url: string) => boolean;
83
81
  /** the hosts the lane's built content screens name: its vendor's router sends `<base>/@<host>/` there */
84
82
  contentHosts?: ReadonlyArray<string>;
85
- /** A vendor's browser session applies to its lanes without a lane importing its parent's manifest. */
86
- sessionDeclaration?: (session: NonNullable<DerivedManifest['browserSession']>) => void;
87
83
  };
88
84
  manifest: { doors?: ReadonlyArray<{ method: string; path: string }> };
89
85
  };
@@ -177,7 +173,6 @@ async function encoded(m: DerivedManifest, request: Request, res: Response): Pro
177
173
  /** A pack's HTTP surface, as the kernel serves it from the pack's parts. */
178
174
  export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}): DerivedFetch {
179
175
  const m = parts.manifest;
180
- let browserSession = m.browserSession;
181
176
  const scope: CoreScope = {
182
177
  ...(options.root !== undefined ? { root: options.root } : {}),
183
178
  ...(options.clock ? { clock: options.clock } : {}),
@@ -275,7 +270,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
275
270
  });
276
271
  // the workspaces a person can be shown (Viewing a World): each built workspace screen's id and path, the pack's own
277
272
  // and its lanes'
278
- const workspaces = (): Workspace[] => [...(m.screens ?? []).filter((s) => s.kind === 'workspace' && s.status === 'done' && parts.screens?.[s.id]).map((s) => ({ id: s.id, path: s.path })), ...lanesWorkspaces(parts.lanes ?? {})];
273
+ const workspaces = (): Workspace[] => [...(m.screens ?? []).filter((s) => s.kind === 'workspace' && s.status === 'done' && parts.screens?.[s.id]).map((s) => ({ id: s.id, path: s.path, host: s.host })), ...lanesWorkspaces(parts.lanes ?? {})];
279
274
  /** Whether a built screen a person views (a workspace or a flow, never content an API client reads) draws a url: its host
280
275
  * one the screen names, its path the screen's or under it (a root screen draws its root alone). */
281
276
  // by what the pack declares built (a screen drawn by another's handler, as X's profile is by its web pages, is built)
@@ -283,7 +278,6 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
283
278
  const hosts = [s.host, ...(s.hosts ?? [])].map(hostMatcher);
284
279
  return { root: s.path.replace(/\/+$/, '') === '', host: (h: string) => hosts.some((match) => match(h)), path: pathMatcher(s.path, true) };
285
280
  });
286
- const drawsScreens = viewed.length > 0 || Object.values(parts.lanes ?? {}).some((l) => l.fetch.drawsScreens);
287
281
  const drawsUrl = (at: string): boolean => {
288
282
  try {
289
283
  const u = new URL(at); const path = u.pathname.replace(/\/+$/, '') || '/';
@@ -412,7 +406,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
412
406
  if (request.method === 'GET' && path === '/twin' && m.discovery) {
413
407
  const d = m.discovery;
414
408
  // the workspaces a person can be shown (Viewing a World): each built workspace screen's id and path
415
- const shown = { drawsScreens, ...(workspaces().length ? { screens: workspaces() } : {}), ...(browserSession ? { browserSession } : {}) };
409
+ const shown = workspaces().length ? { screens: workspaces() } : {};
416
410
  // a model vendor's: its state, and what its scenario scripts, with the scenario's live counts
417
411
  if (parts.scenario && d.behavior) {
418
412
  return Response.json({ ...shown, ...twinManifest({
@@ -428,17 +422,6 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
428
422
  // the vendor's own moves, not the caller's: its context carries none of the request's headers (a Stripe-Account
429
423
  // header would make a payout time makes the caller's), but a tenant's, when the vendor keeps tenants apart
430
424
  if (parts.clock) await runAsVendorMove(async () => parts.clock!(await open(new Request(request.url, m.tenant ? { headers: request.headers } : {}), 'clock', 'POST', '/_twin/clock', {})));
431
- if (path === BROWSER_SESSION_PATH && browserSession) {
432
- if (request.method !== 'POST' || readOnly) return Response.json({ error: 'a browser session needs a permitted write' }, { status: 403 });
433
- const choice = takeBrowserSessionPermit(request, m.service, options.root);
434
- if (!choice) return Response.json({ error: 'only the World can change this browser session' }, { status: 403 });
435
- const ctx = await open(request, 'browser-session', 'POST', BROWSER_SESSION_PATH, {});
436
- const person = choice.person === null ? null : ctx.rowsRaw('_person').find((p) => p.id === choice.person && typeof p.email === 'string');
437
- if (person === undefined) return Response.json({ error: 'that person is no longer in the World' }, { status: 404 });
438
- const cleared = await endBrowserSession(ctx, browserSession.cookie);
439
- const cookie = person ? await startBrowserSession(ctx, String(person.email), browserSession.cookie, browserSession.scope) : cleared;
440
- return new Response(null, { status: 303, headers: { location: choice.land, 'set-cookie': cookie } });
441
- }
442
425
  if (path.startsWith('/_twin/')) {
443
426
  // the World's machine pool, enrolled by the kernel for a vendor that runs images (machines.ts)
444
427
  if (m.machines && path === '/_twin/machine-pool' && request.method === 'POST') {
@@ -536,7 +519,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
536
519
  async (asked) => engineOf()?.serve(asked)) : undefined;
537
520
  // what serves each operation: the pack's own, and each lane's
538
521
  const owners = (): Record<string, DerivedOwner> => ({ ...api.owners(), ...lanesOwners(parts.lanes ?? {}) });
539
- return derivedRequestScopes(m, Object.assign(served, { sessionDeclaration: (session: NonNullable<DerivedManifest['browserSession']>) => { browserSession ??= session; }, owners, serves, workspaces, boards: Boolean(parts.board), drawsScreens, draws: drawsUrl, contentHosts, ...(upgrade ? { upgrade } : {}) }));
522
+ return derivedRequestScopes(m, Object.assign(served, { owners, serves, workspaces, boards: Boolean(parts.board), draws: drawsUrl, contentHosts, ...(upgrade ? { upgrade } : {}) }));
540
523
  }
541
524
 
542
525
  /** The vendor's gateway over its lanes: the lane a request goes to (a door to the lane that declares it, then the first
@@ -600,7 +583,6 @@ const lanesWorkspaces = (lanes: Record<string, LanePart>): Workspace[] =>
600
583
  Object.values(lanes).flatMap((l) => (l.fetch as { workspaces?: () => Workspace[] }).workspaces?.() ?? []);
601
584
 
602
585
  export function createVendorFetch(manifest: VendorManifest, lanes: Record<string, LanePart>): ((request: Request) => Promise<Response>) & { owners: () => Record<string, DerivedOwner>; workspaces: () => Workspace[] } {
603
- if (manifest.browserSession) for (const part of Object.values(lanes)) part.fetch.sessionDeclaration?.(manifest.browserSession);
604
586
  const lane = laneRouter(manifest.vendor, manifest.lanes, lanes);
605
587
  return Object.assign(async (request: Request): Promise<Response> => {
606
588
  const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
@@ -620,7 +602,7 @@ export function createVendorFetch(manifest: VendorManifest, lanes: Record<string
620
602
  if (request.method === 'GET' && path === '/twin' && manifest.discovery) {
621
603
  // the vendor's screens are its lanes' (a dashboard drawn by one lane is the vendor's)
622
604
  const screens = lanesWorkspaces(lanes);
623
- return Response.json({ ...statefulTwinManifest({ vendor: manifest.vendor, ...manifest.discovery }), drawsScreens: Object.values(lanes).some((l) => l.fetch.drawsScreens), ...(manifest.browserSession ? { browserSession: manifest.browserSession } : {}), ...(screens.length ? { screens } : {}) });
605
+ return Response.json({ ...statefulTwinManifest({ vendor: manifest.vendor, ...manifest.discovery }), ...(screens.length ? { screens } : {}) });
624
606
  }
625
607
  const to = lane(request);
626
608
  return to ? to(request) : Response.json({ error: `${manifest.vendor}: no lane serves ${request.method} ${path}` }, { status: 404 });