@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.
- package/dist/host-routing.cjs +106 -0
- package/dist/host-routing.d.cts +7 -0
- package/dist/inject.cjs +5 -8
- package/dist/src/board-keys.d.ts +6 -0
- package/dist/src/board-keys.js +4 -0
- package/dist/src/browser-cookie-jar.d.ts +9 -0
- package/dist/src/browser-cookie-jar.js +104 -0
- package/dist/src/browser-state.d.ts +52 -0
- package/dist/src/browser-state.js +105 -0
- package/dist/src/browser.d.ts +12 -0
- package/dist/src/browser.js +197 -0
- package/dist/src/derived-core.d.ts +3 -7
- package/dist/src/derived.d.ts +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/pack-fetch.d.ts +0 -3
- package/dist/src/pack-fetch.js +5 -26
- package/dist/src/public-suffixes.json +1 -0
- package/dist/src/runtime.d.ts +1 -1
- package/dist/src/runtime.js +1 -1
- package/dist/src/twin-fetch.d.ts +1 -1
- package/dist/src/twin-fetch.js +23 -4
- package/dist/vendor-hosts.cjs +13 -105
- package/dist/vendor-hosts.d.cts +3 -0
- package/host-routing.cjs +106 -0
- package/host-routing.d.cts +7 -0
- package/inject.cjs +5 -8
- package/package.json +11 -1
- package/src/board-keys.ts +4 -0
- package/src/browser-cookie-jar.ts +77 -0
- package/src/browser-session.ts +1 -1
- package/src/browser-state.ts +89 -0
- package/src/browser.ts +164 -0
- package/src/derived-core.ts +4 -5
- package/src/derived.ts +1 -1
- package/src/index.ts +2 -0
- package/src/pack-fetch.ts +6 -24
- package/src/public-suffixes.json +1 -0
- package/src/runtime.ts +2 -1
- package/src/twin-fetch.ts +9 -4
- package/vendor-hosts.cjs +13 -105
- package/vendor-hosts.d.cts +3 -0
- package/dist/src/browser-session-permit.d.ts +0 -13
- package/dist/src/browser-session-permit.js +0 -51
- 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
|
+
}
|
package/src/derived-core.ts
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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 '../
|
|
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;
|
|
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 =
|
|
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, {
|
|
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 }),
|
|
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 });
|