@volter/world-core 3.0.8 → 3.0.10

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.
@@ -979,9 +979,16 @@
979
979
  },
980
980
  {
981
981
  "host": "github.com",
982
- "pathPattern": "^/login|^/session|^/settings/|^/organizations/|^/apps/|^/[^/]+/[^/]+\\.git/"
982
+ "pathPattern": "^/login|^/session|^/settings/|^/organizations/|^/apps/|^/[^/]+/[^/]+\\.git/|^/[^/]+/[^/]+/settings/pages"
983
+ },
984
+ {
985
+ "suffix": ".github.io"
983
986
  }
984
987
  ],
988
+ "hostsClaimed": {
989
+ "door": "/_twin/hosts",
990
+ "note": "GitHub Pages sites: each owner's github.io and each custom domain, answered with the publishing branch's files."
991
+ },
985
992
  "archetype": "crud",
986
993
  "protocol": {
987
994
  "declared": "3",
@@ -113,11 +113,41 @@ export type DerivedFetch = ((request: Request) => Promise<Response>) & {
113
113
  upgrade?: WebSocketUpgrade;
114
114
  /** The workspace screens it draws, its lanes' too (`GET /twin`'s `screens`). */
115
115
  workspaces?(): Workspace[];
116
+ /** Whether it lays out frames of the World's board (`semantics/board.ts`). */
117
+ boards?: boolean;
118
+ /** Whether a built screen of it takes a URL: a board frame it draws. */
119
+ draws?(url: string): boolean;
116
120
  };
117
121
  /** A workspace screen a pack draws, as `GET /twin` lists it: its id and path (Viewing a World). */
118
122
  export type Workspace = {
119
123
  id: string;
120
124
  path: string;
121
125
  };
126
+ /** A frame of the World's board (architecture, "The board"): one thing of the vendor's where a person sees it, as the
127
+ * vendor's pack lays out its default board (`semantics/board.ts`), the editorial choice of which views of the vendor a
128
+ * person keeps open (a Worker's site, a repository's page, a Jira board). `GET /twin/board` lists them. */
129
+ export type BoardFrame = {
130
+ /** stable within the vendor, whatever the World's ids: what a saved board names the frame by */
131
+ id: string;
132
+ /** what it is, in the vendor's own word (a site, a repository, a board) */
133
+ kind: string;
134
+ title: string;
135
+ /** the vendor's container it is grouped under (an account, an organization, a project); `aka`, the ids it was known
136
+ * by before (an account a deploy adopted), so a board that included it under one still holds it */
137
+ section: {
138
+ id: string;
139
+ title: string;
140
+ aka?: string[];
141
+ };
142
+ /** where the vendor shows it: an absolute URL on the vendor's host, or on a hostname the vendor serves for its customer */
143
+ url: string;
144
+ /** the records whose changes are this frame's changes */
145
+ covers: Array<{
146
+ type: string;
147
+ id: string;
148
+ }>;
149
+ /** set by the kernel, never the pack: whether a built screen of the vendor's (any of its lanes') draws the url */
150
+ drawn?: boolean;
151
+ };
122
152
  export declare function createDerivedFetch(options: DerivedFetchOptions): DerivedFetch;
123
153
  export {};
@@ -28,7 +28,7 @@ export type { MultipartPart } from './multipart.js';
28
28
  export declare const bindSemantics: (m: Parameters<typeof bindAny>[0], handlers: Record<string, Handler>, scope?: CoreScope) => Record<string, DerivedHandler>;
29
29
  export { createPackFetch, createVendorFetch, type LanePart, type PackFetchOptions, type PackParts, type PackScenario } from './pack-fetch.js';
30
30
  export { createDerivedFetch } from './derived.js';
31
- export type { DerivedCall, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedSurface } from './derived.js';
31
+ export type { DerivedCall, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedSurface, BoardFrame } from './derived.js';
32
32
  export { coreFor, crossCutting, derivedRequestScopes, vendorError } from './derived-core.js';
33
33
  export { answerVendorErrors, RefusedWriteError } from './head.js';
34
34
  export { GraphqlError, type GraphqlPart, type GraphqlResolver } from './graphql-wire.js';
@@ -1,5 +1,5 @@
1
1
  import { type DerivedManifest, type Handler, type HandlerContext, type VendorManifest } from './derived-core.js';
2
- import { type DerivedFetch, type DerivedOwner, type DerivedSurface, type Workspace } from './derived.js';
2
+ import { type DerivedFetch, type DerivedOwner, type DerivedSurface, type Workspace, type BoardFrame } from './derived.js';
3
3
  import { type PackScenarioAdapter } from './scenario.js';
4
4
  import { type SocketEngine } from './sockets.js';
5
5
  import { type GraphqlPart } from './graphql-wire.js';
@@ -39,6 +39,8 @@ export type PackParts = {
39
39
  };
40
40
  /** `semantics/sockets.ts`: each socket's engine, by the id the manifest's `sockets` gives it (sockets.ts). */
41
41
  sockets?: Record<string, SocketEngine<HandlerContext>>;
42
+ /** `semantics/board.ts`: the vendor's frames on the World's board, read from its state (`GET /twin/board`). */
43
+ board?: (ctx: HandlerContext) => BoardFrame[] | Promise<BoardFrame[]>;
42
44
  };
43
45
  /** A pack's scenario (`semantics/scenario.ts` exports `scenario`): the adapter the kernel's engine reads the World's
44
46
  * scenario document with, the operations it decides a turn of, and the request an operation is to it (undefined when
@@ -52,6 +54,8 @@ export type PackScenario<Req = any> = {
52
54
  export type LanePart = {
53
55
  fetch: ((request: Request) => Promise<Response>) & {
54
56
  serves?: (method: string, path: string) => boolean;
57
+ boards?: boolean;
58
+ draws?: (url: string) => boolean;
55
59
  };
56
60
  manifest: {
57
61
  doors?: ReadonlyArray<{
@@ -211,7 +211,17 @@ export function createPackFetch(parts, options = {}) {
211
211
  // the workspaces a person can be shown (Viewing a World): each built workspace screen's id and path, the pack's own
212
212
  // and its lanes'
213
213
  const workspaces = () => [...(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 ?? {})];
214
- // a context the kernel opens for what no surface operation names: a door, a screen, the clock
214
+ /** Whether a built screen of this pack takes a url: its host one the screen names, its path under the screen's. */
215
+ const drawsUrl = (at) => {
216
+ try {
217
+ const u = new URL(at);
218
+ return screens.some((s) => Boolean(s.host(u.hostname.toLowerCase())) && Boolean(s.path(u.pathname.replace(/\/+$/, '') || '/')));
219
+ }
220
+ catch {
221
+ return false;
222
+ }
223
+ };
224
+ // a context the kernel opens for what no surface operation names: a door, a screen, the clock, the board
215
225
  const open = (request, id, method, path, params) => contextFor(m, { request, operation: { id, method, path, class: method === 'GET' ? 'retrieve' : 'action' }, params }, scope);
216
226
  const refusedReadOnly = () => vendorError(m, m.readOnly);
217
227
  const prefix = m.pathPrefix ? new RegExp(`^(?:${m.pathPrefix})(?=/)`) : undefined;
@@ -246,6 +256,15 @@ export function createPackFetch(parts, options = {}) {
246
256
  // the discovery door is the pack's own, whichever lane would take its path
247
257
  const asked = new URL(arrived.url).pathname.replace(/\/+$/, '') || '/';
248
258
  const discovery = arrived.method === 'GET' && m.discovery !== undefined && (asked === '/twin' || asked === '/twin/scenario');
259
+ // the board is read from the state as it stands, before anything else runs (the clock, a lane, the API): a read,
260
+ // whoever asks, each frame drawn when a built screen takes its url; a pack that lays out none has an empty board
261
+ if (arrived.method === 'GET' && asked === '/twin/board') {
262
+ if (!parts.board)
263
+ return Response.json({ frames: [] });
264
+ const request = withHeaders(arrived, new Headers([...arrived.headers, [READ_ONLY_REQUEST_HEADER, '1']]));
265
+ const frames = await parts.board(await open(request, 'board', 'GET', '/twin/board', {}));
266
+ return Response.json({ frames: frames.map((f) => ({ ...f, drawn: drawsUrl(f.url) })) });
267
+ }
249
268
  const toLane = discovery ? undefined : lane?.(arrived);
250
269
  if (toLane)
251
270
  return toLane(arrived);
@@ -331,10 +350,14 @@ export function createPackFetch(parts, options = {}) {
331
350
  // over R2's /{Bucket}/{Key}), the root workspace at the root alone
332
351
  const page = placed && ['document', 'iframe'].includes(request.headers.get('sec-fetch-dest') ?? '');
333
352
  const rooted = (x) => x.s.decl.path.replace(/\/+$/, '') === '';
353
+ // a request the World routed from a vendor's own hostname (its proxy, a site, a call inside the World) is answered by
354
+ // that host's screens alone: a page of github.com never answers at <owner>.github.io, nor Cloudflare's sign-in at a
355
+ // Worker's Custom Domain; a screen by its path alone is for the twin reached at its own address
356
+ const addressed = !placed && request.headers.has('x-volter-twin-original-host');
334
357
  const byPath = hits.find((x) => x.onHost && exact(x.s))
335
358
  ?? (page ? hits.find((x) => !rooted(x)) ?? (path === '/' ? hits.find((x) => x.s.decl.kind === 'workspace') : undefined) : undefined)
336
- ?? (apiPath ? undefined : hits.find((x) => x.onHost) ?? hits.find((x) => !rooted(x))
337
- ?? (placed ? hits.find((x) => x.s.decl.kind === 'workspace') : undefined));
359
+ ?? (apiPath ? undefined : hits.find((x) => x.onHost) ?? (addressed ? undefined : hits.find((x) => !rooted(x))
360
+ ?? (placed ? hits.find((x) => x.s.decl.kind === 'workspace') : undefined)));
338
361
  if (byPath) {
339
362
  if (readOnly && request.method !== 'GET' && request.method !== 'HEAD')
340
363
  return refusedReadOnly();
@@ -396,7 +419,7 @@ export function createPackFetch(parts, options = {}) {
396
419
  async (asked) => engineOf()?.serve(asked)) : undefined;
397
420
  // what serves each operation: the pack's own, and each lane's
398
421
  const owners = () => ({ ...api.owners(), ...lanesOwners(parts.lanes ?? {}) });
399
- return derivedRequestScopes(m, Object.assign(served, { owners, serves, workspaces, ...(upgrade ? { upgrade } : {}) }));
422
+ return derivedRequestScopes(m, Object.assign(served, { owners, serves, workspaces, boards: Boolean(parts.board), draws: drawsUrl, ...(upgrade ? { upgrade } : {}) }));
400
423
  }
401
424
  /** The vendor's gateway over its lanes: the lane a request goes to (a door to the lane that declares it, then the first
402
425
  * route whose conditions all hold, then the default), behind the gateway's CORS; undefined when none takes it. */
@@ -452,6 +475,22 @@ export function createVendorFetch(manifest, lanes) {
452
475
  const lane = laneRouter(manifest.vendor, manifest.lanes, lanes);
453
476
  return Object.assign(async (request) => {
454
477
  const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
478
+ // the vendor's board is the frames of the lanes that lay one out, each drawn when any lane's screen takes its url
479
+ if (request.method === 'GET' && path === '/twin/board') {
480
+ const frames = [];
481
+ for (const l of Object.values(lanes)) {
482
+ if (!l.fetch.boards)
483
+ continue;
484
+ try {
485
+ const answer = await l.fetch(new Request(request.url, { headers: request.headers }));
486
+ if (answer.ok)
487
+ frames.push(...((await answer.json()).frames ?? []));
488
+ }
489
+ catch { /* a lane whose board does not answer lays out nothing */ }
490
+ }
491
+ const draws = (url) => Object.values(lanes).some((l) => l.fetch.draws?.(url) ?? false);
492
+ return Response.json({ frames: frames.map((f) => ({ ...f, drawn: draws(f.url) })) });
493
+ }
455
494
  if (request.method === 'GET' && path === '/twin' && manifest.discovery) {
456
495
  // the vendor's screens are its lanes' (a dashboard drawn by one lane is the vendor's)
457
496
  const screens = lanesWorkspaces(lanes);
@@ -8,7 +8,7 @@ export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, ReadOnlyRequestError, runA
8
8
  export { compileSurface, createDerivedFetch, matchOperation } from './derived.js';
9
9
  export { currentTraceparent, deliveryTraceHeaders, newTraceparent, parseTraceparent, runWithRequestTrace, runWithTraceparent, TRACEPARENT_HEADER, traceparentForDelivery, validTraceparent } from './trace-context.js';
10
10
  export type { Traceparent } from './trace-context.js';
11
- export type { DerivedCall, DerivedCoreOutcome, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedOwner, DerivedSurface } from './derived.js';
11
+ export type { DerivedCall, DerivedCoreOutcome, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedOwner, DerivedSurface, BoardFrame } from './derived.js';
12
12
  export { authRefusal, bindSemantics, coreFor, crossCutting, flagOf, derivedRequestScopes, observeTransitions, resourcesOfType as twinResourcesOfType, semanticsContext, stateOf, transitionFor, parseBracketForm, readParams, render as renderDerived, serveCore, sse, vendorError } from './derived-core.js';
13
13
  export { deliverEvents, fillEnvelope, signEvent, signEventAll, verifyEvent } from './events.js';
14
14
  export { equalSecrets, hmac, jwks, jwtDecode, jwtSign, jwtVerify, keyId, lettersFrom, md5, sha256, SIGNING_ALGORITHMS, uuidFrom } from './signing.js';
@@ -979,9 +979,16 @@
979
979
  },
980
980
  {
981
981
  "host": "github.com",
982
- "pathPattern": "^/login|^/session|^/settings/|^/organizations/|^/apps/|^/[^/]+/[^/]+\\.git/"
982
+ "pathPattern": "^/login|^/session|^/settings/|^/organizations/|^/apps/|^/[^/]+/[^/]+\\.git/|^/[^/]+/[^/]+/settings/pages"
983
+ },
984
+ {
985
+ "suffix": ".github.io"
983
986
  }
984
987
  ],
988
+ "hostsClaimed": {
989
+ "door": "/_twin/hosts",
990
+ "note": "GitHub Pages sites: each owner's github.io and each custom domain, answered with the publishing branch's files."
991
+ },
985
992
  "archetype": "crud",
986
993
  "protocol": {
987
994
  "declared": "3",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-core",
3
- "version": "3.0.8",
3
+ "version": "3.0.10",
4
4
  "description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
5
5
  "keywords": [
6
6
  "twin",
package/src/derived.ts CHANGED
@@ -198,11 +198,35 @@ export type DerivedFetch = ((request: Request) => Promise<Response>) & {
198
198
  upgrade?: WebSocketUpgrade;
199
199
  /** The workspace screens it draws, its lanes' too (`GET /twin`'s `screens`). */
200
200
  workspaces?(): Workspace[];
201
+ /** Whether it lays out frames of the World's board (`semantics/board.ts`). */
202
+ boards?: boolean;
203
+ /** Whether a built screen of it takes a URL: a board frame it draws. */
204
+ draws?(url: string): boolean;
201
205
  };
202
206
 
203
207
  /** A workspace screen a pack draws, as `GET /twin` lists it: its id and path (Viewing a World). */
204
208
  export type Workspace = { id: string; path: string };
205
209
 
210
+ /** A frame of the World's board (architecture, "The board"): one thing of the vendor's where a person sees it, as the
211
+ * vendor's pack lays out its default board (`semantics/board.ts`), the editorial choice of which views of the vendor a
212
+ * person keeps open (a Worker's site, a repository's page, a Jira board). `GET /twin/board` lists them. */
213
+ export type BoardFrame = {
214
+ /** stable within the vendor, whatever the World's ids: what a saved board names the frame by */
215
+ id: string;
216
+ /** what it is, in the vendor's own word (a site, a repository, a board) */
217
+ kind: string;
218
+ title: string;
219
+ /** the vendor's container it is grouped under (an account, an organization, a project); `aka`, the ids it was known
220
+ * by before (an account a deploy adopted), so a board that included it under one still holds it */
221
+ section: { id: string; title: string; aka?: string[] };
222
+ /** where the vendor shows it: an absolute URL on the vendor's host, or on a hostname the vendor serves for its customer */
223
+ url: string;
224
+ /** the records whose changes are this frame's changes */
225
+ covers: Array<{ type: string; id: string }>;
226
+ /** set by the kernel, never the pack: whether a built screen of the vendor's (any of its lanes') draws the url */
227
+ drawn?: boolean;
228
+ };
229
+
206
230
  export function createDerivedFetch(options: DerivedFetchOptions): DerivedFetch {
207
231
  const handlers = options.handlers ?? {};
208
232
  const known = new Set(options.surface.operations.map((o) => o.id));
package/src/index.ts CHANGED
@@ -39,7 +39,7 @@ export const bindSemantics = (m: Parameters<typeof bindAny>[0], handlers: Record
39
39
  // ── the derived fetch ──────────────────────────────────────────────────────────────────────────
40
40
  export { createPackFetch, createVendorFetch, type LanePart, type PackFetchOptions, type PackParts, type PackScenario } from './pack-fetch.ts';
41
41
  export { createDerivedFetch } from './derived.ts';
42
- export type { DerivedCall, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedSurface } from './derived.ts';
42
+ export type { DerivedCall, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedSurface, BoardFrame } from './derived.ts';
43
43
  export { coreFor, crossCutting, derivedRequestScopes, vendorError } from './derived-core.ts';
44
44
  export { answerVendorErrors, RefusedWriteError } from './head.ts';
45
45
  // a GraphQL API's resolvers (`semantics/graphql.ts`), and the error one answers with the vendor's type
package/src/pack-fetch.ts CHANGED
@@ -10,7 +10,7 @@ import { TWIN_PREFIX_HEADER } from './twin-fetch.ts';
10
10
  import { PREFIX_PARAMS_HEADER } from './derived.ts';
11
11
  import { withHeaders } from './with-headers.ts';
12
12
  import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, coreFor, crossCutting, derivedRequestScopes, vendorError, type CoreScope, type DerivedManifest, type ErrorSpec, type Handler, type HandlerContext, type LanesDecl, type VendorManifest } from './derived-core.ts';
13
- import { compileSurface, createDerivedFetch, matchOperation, type DerivedFetch, type DerivedOperation, type DerivedOwner, type DerivedSurface, type Workspace } from './derived.ts';
13
+ import { compileSurface, createDerivedFetch, matchOperation, type DerivedFetch, type DerivedOperation, type DerivedOwner, type DerivedSurface, type Workspace, type BoardFrame } from './derived.ts';
14
14
  import { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, runAsVendorMove } from './request-scope.ts';
15
15
  import { parseScenarioDocument, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest, type PackScenarioAdapter } from './scenario.ts';
16
16
  import { getActiveWorldStore } from './world-store.ts';
@@ -57,6 +57,8 @@ export type PackParts = {
57
57
  graphql?: GraphqlPart<HandlerContext> & { sdl: string };
58
58
  /** `semantics/sockets.ts`: each socket's engine, by the id the manifest's `sockets` gives it (sockets.ts). */
59
59
  sockets?: Record<string, SocketEngine<HandlerContext>>;
60
+ /** `semantics/board.ts`: the vendor's frames on the World's board, read from its state (`GET /twin/board`). */
61
+ board?: (ctx: HandlerContext) => BoardFrame[] | Promise<BoardFrame[]>;
60
62
  };
61
63
 
62
64
  /** A pack's scenario (`semantics/scenario.ts` exports `scenario`): the adapter the kernel's engine reads the World's
@@ -66,7 +68,7 @@ export type PackParts = {
66
68
  export type PackScenario<Req = any> = { adapter: PackScenarioAdapter<Req>; operations: ReadonlyArray<string>; request: (ctx: HandlerContext) => Req | undefined };
67
69
 
68
70
  /** A lane as its vendor's fetch is given it: its own pack fetch, and its manifest (the doors it declares). */
69
- export type LanePart = { fetch: ((request: Request) => Promise<Response>) & { serves?: (method: string, path: string) => boolean }; manifest: { doors?: ReadonlyArray<{ method: string; path: string }> } };
71
+ export type LanePart = { fetch: ((request: Request) => Promise<Response>) & { serves?: (method: string, path: string) => boolean; boards?: boolean; draws?: (url: string) => boolean }; manifest: { doors?: ReadonlyArray<{ method: string; path: string }> } };
70
72
 
71
73
  /** Where a pack serves: its World's root, whether it was started read-only, the World instant when a caller pins it, the
72
74
  * World's managed Postgres the runtime binds a pack declaring `managedDatabase` to (a handler's `ctx.engine`), and the
@@ -245,7 +247,11 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
245
247
  // the workspaces a person can be shown (Viewing a World): each built workspace screen's id and path, the pack's own
246
248
  // and its lanes'
247
249
  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 ?? {})];
248
- // a context the kernel opens for what no surface operation names: a door, a screen, the clock
250
+ /** Whether a built screen of this pack takes a url: its host one the screen names, its path under the screen's. */
251
+ const drawsUrl = (at: string): boolean => {
252
+ try { const u = new URL(at); return screens.some((s) => Boolean(s.host(u.hostname.toLowerCase())) && Boolean(s.path(u.pathname.replace(/\/+$/, '') || '/'))); } catch { return false; }
253
+ };
254
+ // a context the kernel opens for what no surface operation names: a door, a screen, the clock, the board
249
255
  const open = (request: Request, id: string, method: string, path: string, params: Record<string, string>): Promise<HandlerContext> =>
250
256
  contextFor(m, { request, operation: { id, method, path, class: method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation, params }, scope);
251
257
  const refusedReadOnly = (): Response => vendorError(m, m.readOnly);
@@ -278,6 +284,14 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
278
284
  // the discovery door is the pack's own, whichever lane would take its path
279
285
  const asked = new URL(arrived.url).pathname.replace(/\/+$/, '') || '/';
280
286
  const discovery = arrived.method === 'GET' && m.discovery !== undefined && (asked === '/twin' || asked === '/twin/scenario');
287
+ // the board is read from the state as it stands, before anything else runs (the clock, a lane, the API): a read,
288
+ // whoever asks, each frame drawn when a built screen takes its url; a pack that lays out none has an empty board
289
+ if (arrived.method === 'GET' && asked === '/twin/board') {
290
+ if (!parts.board) return Response.json({ frames: [] });
291
+ const request = withHeaders(arrived, new Headers([...arrived.headers, [READ_ONLY_REQUEST_HEADER, '1']]));
292
+ const frames = await parts.board(await open(request, 'board', 'GET', '/twin/board', {}));
293
+ return Response.json({ frames: frames.map((f) => ({ ...f, drawn: drawsUrl(f.url) })) });
294
+ }
281
295
  const toLane = discovery ? undefined : lane?.(arrived);
282
296
  if (toLane) return toLane(arrived);
283
297
  // only the kernel names a scenario's decision, or a path prefix's parameters
@@ -349,10 +363,14 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
349
363
  // over R2's /{Bucket}/{Key}), the root workspace at the root alone
350
364
  const page = placed && ['document', 'iframe'].includes(request.headers.get('sec-fetch-dest') ?? '');
351
365
  const rooted = (x: (typeof hits)[number]): boolean => x.s.decl.path.replace(/\/+$/, '') === '';
366
+ // a request the World routed from a vendor's own hostname (its proxy, a site, a call inside the World) is answered by
367
+ // that host's screens alone: a page of github.com never answers at <owner>.github.io, nor Cloudflare's sign-in at a
368
+ // Worker's Custom Domain; a screen by its path alone is for the twin reached at its own address
369
+ const addressed = !placed && request.headers.has('x-volter-twin-original-host');
352
370
  const byPath = hits.find((x) => x.onHost && exact(x.s))
353
371
  ?? (page ? hits.find((x) => !rooted(x)) ?? (path === '/' ? hits.find((x) => x.s.decl.kind === 'workspace') : undefined) : undefined)
354
- ?? (apiPath ? undefined : hits.find((x) => x.onHost) ?? hits.find((x) => !rooted(x))
355
- ?? (placed ? hits.find((x) => x.s.decl.kind === 'workspace') : undefined));
372
+ ?? (apiPath ? undefined : hits.find((x) => x.onHost) ?? (addressed ? undefined : hits.find((x) => !rooted(x))
373
+ ?? (placed ? hits.find((x) => x.s.decl.kind === 'workspace') : undefined)));
356
374
  if (byPath) {
357
375
  if (readOnly && request.method !== 'GET' && request.method !== 'HEAD') return refusedReadOnly();
358
376
  return byPath.s.answer(await open(request, byPath.s.decl.id, request.method, byPath.s.decl.path, { ...(byPath.onHost?.params ?? {}), ...byPath.hit!.params }));
@@ -403,7 +421,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
403
421
  async (asked) => engineOf()?.serve(asked)) : undefined;
404
422
  // what serves each operation: the pack's own, and each lane's
405
423
  const owners = (): Record<string, DerivedOwner> => ({ ...api.owners(), ...lanesOwners(parts.lanes ?? {}) });
406
- return derivedRequestScopes(m, Object.assign(served, { owners, serves, workspaces, ...(upgrade ? { upgrade } : {}) }));
424
+ return derivedRequestScopes(m, Object.assign(served, { owners, serves, workspaces, boards: Boolean(parts.board), draws: drawsUrl, ...(upgrade ? { upgrade } : {}) }));
407
425
  }
408
426
 
409
427
  /** The vendor's gateway over its lanes: the lane a request goes to (a door to the lane that declares it, then the first
@@ -464,6 +482,19 @@ export function createVendorFetch(manifest: VendorManifest, lanes: Record<string
464
482
  const lane = laneRouter(manifest.vendor, manifest.lanes, lanes);
465
483
  return Object.assign(async (request: Request): Promise<Response> => {
466
484
  const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
485
+ // the vendor's board is the frames of the lanes that lay one out, each drawn when any lane's screen takes its url
486
+ if (request.method === 'GET' && path === '/twin/board') {
487
+ const frames: BoardFrame[] = [];
488
+ for (const l of Object.values(lanes)) {
489
+ if (!l.fetch.boards) continue;
490
+ try {
491
+ const answer = await l.fetch(new Request(request.url, { headers: request.headers }));
492
+ if (answer.ok) frames.push(...(((await answer.json()) as { frames?: BoardFrame[] }).frames ?? []));
493
+ } catch { /* a lane whose board does not answer lays out nothing */ }
494
+ }
495
+ const draws = (url: string): boolean => Object.values(lanes).some((l) => l.fetch.draws?.(url) ?? false);
496
+ return Response.json({ frames: frames.map((f) => ({ ...f, drawn: draws(f.url) })) });
497
+ }
467
498
  if (request.method === 'GET' && path === '/twin' && manifest.discovery) {
468
499
  // the vendor's screens are its lanes' (a dashboard drawn by one lane is the vendor's)
469
500
  const screens = lanesWorkspaces(lanes);
package/src/runtime.ts CHANGED
@@ -17,7 +17,7 @@ export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, ReadOnlyRequestError, runA
17
17
  export { compileSurface, createDerivedFetch, matchOperation } from './derived.ts';
18
18
  export { currentTraceparent, deliveryTraceHeaders, newTraceparent, parseTraceparent, runWithRequestTrace, runWithTraceparent, TRACEPARENT_HEADER, traceparentForDelivery, validTraceparent } from './trace-context.ts';
19
19
  export type { Traceparent } from './trace-context.ts';
20
- export type { DerivedCall, DerivedCoreOutcome, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedOwner, DerivedSurface } from './derived.ts';
20
+ export type { DerivedCall, DerivedCoreOutcome, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedOwner, DerivedSurface, BoardFrame } from './derived.ts';
21
21
  export { authRefusal, bindSemantics, coreFor, crossCutting, flagOf, derivedRequestScopes, observeTransitions, resourcesOfType as twinResourcesOfType, semanticsContext, stateOf, transitionFor, parseBracketForm, readParams, render as renderDerived, serveCore, sse, vendorError } from './derived-core.ts';
22
22
  export { deliverEvents, fillEnvelope, signEvent, signEventAll, verifyEvent } from './events.ts';
23
23
  export { equalSecrets, hmac, jwks, jwtDecode, jwtSign, jwtVerify, keyId, lettersFrom, md5, sha256, SIGNING_ALGORITHMS, uuidFrom } from './signing.ts';