@volter/world-runtime 2.0.55 → 3.0.1

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 (121) hide show
  1. package/dist/known-external-services.json +5 -4
  2. package/dist/src/actions-runner-cli.d.ts +2 -0
  3. package/dist/src/actions-runner-cli.js +25 -0
  4. package/dist/src/actions-runner.d.ts +18 -0
  5. package/dist/src/actions-runner.js +281 -0
  6. package/dist/src/ancestry.js +1 -1
  7. package/dist/src/app-url.js +1 -1
  8. package/dist/src/backings.d.ts +29 -0
  9. package/dist/src/backings.js +45 -0
  10. package/dist/src/branch.js +3 -2
  11. package/dist/src/browser-proxy-cli.js +1 -1
  12. package/dist/src/catalog-index.d.ts +38 -0
  13. package/dist/src/catalog-index.js +37 -0
  14. package/dist/src/catalog.d.ts +16 -3
  15. package/dist/src/catalog.js +69 -26
  16. package/dist/src/changeset.d.ts +1 -1
  17. package/dist/src/changeset.js +9 -11
  18. package/dist/src/cli-args.d.ts +4 -0
  19. package/dist/src/cli-args.js +18 -0
  20. package/dist/src/cli-commands.d.ts +1 -0
  21. package/dist/src/cli-commands.js +1216 -0
  22. package/dist/src/cli.js +8 -1300
  23. package/dist/src/configs.js +1 -1
  24. package/dist/src/console-apart.d.ts +2 -2
  25. package/dist/src/console-apart.js +5 -5
  26. package/dist/src/consumers.js +1 -1
  27. package/dist/src/covers.d.ts +1 -1
  28. package/dist/src/covers.js +94 -30
  29. package/dist/src/env-output.d.ts +1 -1
  30. package/dist/src/env-output.js +2 -2
  31. package/dist/src/fixture-env.js +18 -26
  32. package/dist/src/host-cli.js +4 -2
  33. package/dist/src/host-fault-fixture.js +6 -0
  34. package/dist/src/host-ready.d.ts +8 -2
  35. package/dist/src/host-ready.js +16 -4
  36. package/dist/src/host-worker.js +6 -1
  37. package/dist/src/host.d.ts +2 -0
  38. package/dist/src/host.js +9 -1
  39. package/dist/src/index.d.ts +2 -2
  40. package/dist/src/index.js +2 -2
  41. package/dist/src/init.d.ts +33 -23
  42. package/dist/src/init.js +228 -96
  43. package/dist/src/lifecycle-record.js +2 -2
  44. package/dist/src/local-branches.js +1 -1
  45. package/dist/src/origin.d.ts +1 -1
  46. package/dist/src/origin.js +12 -10
  47. package/dist/src/pack-facts.d.ts +30 -6
  48. package/dist/src/pack-facts.js +13 -3
  49. package/dist/src/pglite-backing.js +2 -1
  50. package/dist/src/placeholder.d.ts +1 -1
  51. package/dist/src/placeholder.js +12 -4
  52. package/dist/src/project-inspect.d.ts +1 -1
  53. package/dist/src/project-inspect.js +2 -2
  54. package/dist/src/redirect-proxy.d.ts +5 -0
  55. package/dist/src/redirect-proxy.js +8 -1
  56. package/dist/src/redis-backing.js +4 -2
  57. package/dist/src/reflect.js +1 -1
  58. package/dist/src/root.d.ts +11 -33
  59. package/dist/src/root.js +28 -59
  60. package/dist/src/runtime-test-support.js +1 -1
  61. package/dist/src/runtime.d.ts +7 -1
  62. package/dist/src/runtime.js +317 -53
  63. package/dist/src/schema.d.ts +27 -5
  64. package/dist/src/schema.js +13 -12
  65. package/dist/src/served-world.d.ts +11 -35
  66. package/dist/src/served-world.js +54 -141
  67. package/dist/src/tail.js +1 -1
  68. package/dist/src/up-cli.d.ts +3 -0
  69. package/dist/src/up-cli.js +55 -0
  70. package/dist/src/websocket-relay.d.ts +1 -1
  71. package/dist/src/world-origins.d.ts +1 -1
  72. package/dist/src/world-origins.js +1 -1
  73. package/dist/src/world-view.d.ts +1 -1
  74. package/dist/src/world-view.js +2 -2
  75. package/known-external-services.json +5 -4
  76. package/package.json +11 -11
  77. package/src/actions-runner-cli.ts +24 -0
  78. package/src/actions-runner.ts +244 -0
  79. package/src/ancestry.ts +1 -1
  80. package/src/app-url.ts +1 -1
  81. package/src/backings.ts +55 -0
  82. package/src/branch.ts +3 -2
  83. package/src/browser-proxy-cli.ts +1 -1
  84. package/src/catalog-index.ts +44 -0
  85. package/src/catalog.ts +67 -24
  86. package/src/changeset.ts +10 -13
  87. package/src/cli-args.ts +17 -0
  88. package/src/cli-commands.ts +1070 -0
  89. package/src/cli.ts +9 -1139
  90. package/src/configs.ts +1 -1
  91. package/src/console-apart.ts +5 -5
  92. package/src/consumers.ts +1 -1
  93. package/src/covers.ts +83 -30
  94. package/src/env-output.ts +2 -2
  95. package/src/fixture-env.ts +18 -26
  96. package/src/host-cli.ts +4 -2
  97. package/src/host-ready.ts +13 -4
  98. package/src/host-worker.ts +6 -2
  99. package/src/host.ts +11 -2
  100. package/src/index.ts +2 -2
  101. package/src/init.ts +222 -101
  102. package/src/lifecycle-record.ts +2 -2
  103. package/src/local-branches.ts +1 -1
  104. package/src/origin.ts +12 -10
  105. package/src/pack-facts.ts +22 -6
  106. package/src/pglite-backing.ts +2 -1
  107. package/src/placeholder.ts +13 -4
  108. package/src/project-inspect.ts +2 -2
  109. package/src/redirect-proxy.ts +9 -1
  110. package/src/redis-backing.ts +4 -2
  111. package/src/reflect.ts +1 -1
  112. package/src/root.ts +33 -65
  113. package/src/runtime-test-support.ts +1 -1
  114. package/src/runtime.ts +261 -55
  115. package/src/schema.ts +26 -13
  116. package/src/served-world.ts +50 -130
  117. package/src/tail.ts +1 -1
  118. package/src/up-cli.ts +55 -0
  119. package/src/websocket-relay.ts +1 -1
  120. package/src/world-origins.ts +1 -1
  121. package/src/world-view.ts +3 -3
@@ -65,6 +65,13 @@ export type WorldManifestV2 = {
65
65
  };
66
66
  /** The env value `init` writes for a name whose fake must be minted per instance (a service-account JSON with a real throwaway key). */
67
67
  export declare const MINT_ENV = "$mint";
68
+ /** An env value `$issue:<service id>`: the credential that service's twin issues through its descriptor's
69
+ * `credentialDoor`, asked for at boot once the twin is up (never committed, like `$mint`). */
70
+ export declare const ISSUE_ENV_PREFIX = "$issue:";
71
+ /** The env value `init` writes for the application's own origin (its base URL, its auth URL): resolved when a command
72
+ * attaches, from the application URL the booter recorded (`volter-world app-url <world> --set`), so the app names
73
+ * the address it is reached at, which only its booter knows (a browser tab's preview origin, a local port). */
74
+ export declare const APP_URL_ENV = "$app-url";
68
75
  export type WorldShareProvider = 'cloudflare-quick' | 'command';
69
76
  /**
70
77
  * EXTERNAL (self-managed) service type.
@@ -103,6 +110,9 @@ export type WorldExternalDiscoverSource = 'status' | 'up';
103
110
  */
104
111
  export type WorldExternalDiscover = {
105
112
  as: string;
113
+ /** The wire the discovered connection speaks (`postgres`, `mysql`, `mongodb`, `redis`), so a coverage report binds it
114
+ * to the application's driver instead of calling it unknown. */
115
+ protocol?: string;
106
116
  source?: WorldExternalDiscoverSource;
107
117
  jsonPath?: string;
108
118
  pattern?: string;
@@ -159,6 +169,14 @@ export type WorldServiceConfig = {
159
169
  '//'?: string;
160
170
  id: string;
161
171
  type?: WorldServiceType;
172
+ /** The credential this twin issues the World's applications at boot (its pack's `credentialDoor`, copied by `init`):
173
+ * `up` POSTs `body` to `path` on the running twin and sets each name of `fill` whose value is `$issue:<this id>` to
174
+ * the answer's field `fill` names for it. */
175
+ issues?: {
176
+ path: string;
177
+ body: Record<string, unknown>;
178
+ fill: Record<string, string>;
179
+ };
162
180
  /**
163
181
  * The twin PACKAGE this service boots from (`@volter/twin-<vendor>`), resolved from the world
164
182
  * root at boot by node's own `node_modules` walk (docs/concepts/worlds.md#the-config-and-the-running-world).
@@ -191,11 +209,6 @@ export type WorldServiceConfig = {
191
209
  * to loopback services directly while presenting a disguised world to the app/user.
192
210
  */
193
211
  controlPlane?: boolean;
194
- /** The account a vendor's own screens (its mirror) open signed in as: its handle, username or email, which the pack
195
- * resolves against the accounts this World holds (`<vendor>MirrorSignIn`). Nothing secret is written here. */
196
- signIn?: {
197
- as: string;
198
- };
199
212
  injectEnv?: string;
200
213
  /**
201
214
  * Additional env vars exported into the generated world env after this service starts.
@@ -337,6 +350,11 @@ export type WorldServiceInstance = {
337
350
  port?: number;
338
351
  /** Absent for 'external' services — their URL is discovered into env vars, not assigned by us. */
339
352
  url?: string;
353
+ /** The twin's side ports (TwinPack.sidePorts: the SMTP twin's mailbox), by name: `volter-world url <world> <id>.<name>`. */
354
+ sides?: Record<string, {
355
+ port: number;
356
+ url: string;
357
+ }>;
340
358
  /** 0 for 'external' services — they have no process we own (the external tool manages it). */
341
359
  pid: number;
342
360
  log: string;
@@ -454,6 +472,10 @@ export type WorldInstance = {
454
472
  clonedAt?: string;
455
473
  fetchedAt?: string;
456
474
  };
475
+ /** The facts files of the packs this World resolved from installed packages (each released pack carries its own,
476
+ * `generated/pack-facts.json`): the kernel's pack-facts loader overlays them on the facts it was built with
477
+ * (docs/contributing/architecture.md, "The catalog: where twins come from"). */
478
+ packFacts?: string[];
457
479
  };
458
480
  export declare const DEFAULT_WORLD_SELECTION: WorldSelection;
459
481
  export declare const LEGACY_WORLD_SELECTION: WorldSelection;
@@ -3,6 +3,13 @@ export const WORLD_SCHEMA_VERSION = 2;
3
3
  export const WORLD_USAGES = ['application', 'dependencies', 'build', 'deployment'];
4
4
  /** The env value `init` writes for a name whose fake must be minted per instance (a service-account JSON with a real throwaway key). */
5
5
  export const MINT_ENV = '$mint';
6
+ /** An env value `$issue:<service id>`: the credential that service's twin issues through its descriptor's
7
+ * `credentialDoor`, asked for at boot once the twin is up (never committed, like `$mint`). */
8
+ export const ISSUE_ENV_PREFIX = '$issue:';
9
+ /** The env value `init` writes for the application's own origin (its base URL, its auth URL): resolved when a command
10
+ * attaches, from the application URL the booter recorded (`volter-world app-url <world> --set`), so the app names
11
+ * the address it is reached at, which only its booter knows (a browser tab's preview origin, a local port). */
12
+ export const APP_URL_ENV = '$app-url';
6
13
  function assertStringArray(value, what, path) {
7
14
  if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== 'string')) {
8
15
  throw new Error(`${what} must be a non-empty string array in ${path}`);
@@ -74,10 +81,13 @@ function assertExternalServiceConfig(service, path) {
74
81
  if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) {
75
82
  throw new Error(`External service "${id}" discover entries must be objects in ${path}`);
76
83
  }
77
- assertKeys(mapping, ['as', 'source', 'jsonPath', 'pattern'], `External service "${id}" discover entry`, path);
84
+ assertKeys(mapping, ['as', 'protocol', 'source', 'jsonPath', 'pattern'], `External service "${id}" discover entry`, path);
78
85
  if (!mapping.as || typeof mapping.as !== 'string') {
79
86
  throw new Error(`External service "${id}" discover entry must define string "as" in ${path}`);
80
87
  }
88
+ if (mapping.protocol !== undefined && !['postgres', 'mysql', 'mongodb', 'redis', 'http'].includes(mapping.protocol)) {
89
+ throw new Error(`External service "${id}" discover "${mapping.as}" protocol must be postgres, mysql, mongodb, redis or http in ${path}`);
90
+ }
81
91
  const hasJson = typeof mapping.jsonPath === 'string';
82
92
  const hasPattern = typeof mapping.pattern === 'string';
83
93
  if (hasJson === hasPattern) {
@@ -360,7 +370,7 @@ export function assertWorldSelection(value, path) {
360
370
  function serviceV2ToLegacy(value, path) {
361
371
  if (!record(value))
362
372
  throw new Error(`World service must be an object in ${path}`);
363
- assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root', 'signIn'], `Service ${String(value.id ?? '')}`, path, true);
373
+ assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root'], `Service ${String(value.id ?? '')}`, path, true);
364
374
  const source = value.source === undefined ? {} : value.source;
365
375
  const execution = value.execution === undefined ? {} : value.execution;
366
376
  const endpoint = value.endpoint === undefined ? {} : value.endpoint;
@@ -381,13 +391,6 @@ function serviceV2ToLegacy(value, path) {
381
391
  throw new Error(`World service must define string id in ${path}`);
382
392
  if (value.description !== undefined && typeof value.description !== 'string')
383
393
  throw new Error(`Service "${value.id}" description must be a string in ${path}`);
384
- if (value.signIn !== undefined) {
385
- if (!record(value.signIn))
386
- throw new Error(`Service "${value.id}" signIn must be an object in ${path}`);
387
- assertKeys(value.signIn, ['as'], `Service "${value.id}" signIn`, path);
388
- if (typeof value.signIn.as !== 'string' || value.signIn.as.trim() === '')
389
- throw new Error(`Service "${value.id}" signIn.as must name an account (a handle, username or email) in ${path}`);
390
- }
391
394
  if (value.description !== undefined && value['//'] !== undefined)
392
395
  throw new Error(`Service "${value.id}" must not set both description and // in ${path}`);
393
396
  if (execution.cwd !== undefined && typeof execution.cwd !== 'string')
@@ -412,7 +415,7 @@ function serviceV2ToLegacy(value, path) {
412
415
  for (const mapping of bindings.discover) {
413
416
  if (!record(mapping))
414
417
  throw new Error(`Service "${value.id}" bindings.discover entries must be objects in ${path}`);
415
- assertKeys(mapping, ['as', 'source', 'jsonPath', 'pattern'], `Service "${value.id}" bindings.discover entry`, path);
418
+ assertKeys(mapping, ['as', 'protocol', 'source', 'jsonPath', 'pattern'], `Service "${value.id}" bindings.discover entry`, path);
416
419
  }
417
420
  }
418
421
  const type = value.type;
@@ -446,7 +449,6 @@ function serviceV2ToLegacy(value, path) {
446
449
  ...(bindings.injectEnvTemplates === undefined ? {} : { injectEnvTemplates: bindings.injectEnvTemplates }),
447
450
  ...(bindings.cliRedirect === undefined ? {} : { cliRedirect: bindings.cliRedirect }),
448
451
  ...(value.root === undefined ? {} : { root: value.root }),
449
- ...(value.signIn === undefined ? {} : { signIn: { as: value.signIn.as } }),
450
452
  ...(external === undefined ? {} : { external }),
451
453
  };
452
454
  if (type !== 'external' && bindings.discover !== undefined)
@@ -584,7 +586,6 @@ function serviceToV2(service) {
584
586
  ...(Object.keys(endpoint).length ? { endpoint } : {}),
585
587
  ...(Object.keys(bindings).length ? { bindings } : {}),
586
588
  ...(service.root === undefined ? {} : { root: service.root }),
587
- ...(service.signIn === undefined ? {} : { signIn: service.signIn }),
588
589
  };
589
590
  }
590
591
  /** Serialize normalized intent as the committed format-2 manifest. */
@@ -1,7 +1,7 @@
1
- import { type HistoryReference } from '@volter/world-core';
2
- import { type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core';
1
+ import { type HistoryReference } from '@volter/world-core/runtime';
2
+ import { type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core/runtime';
3
3
  import { type TrustedIssuer } from '@volter/world-access';
4
- import { type Changeset, type Receipt } from '@volter/world-core';
4
+ import { type Changeset, type Receipt } from '@volter/world-core/runtime';
5
5
  import { rootForControlRoot } from './root.js';
6
6
  import { type WorldInstance } from './schema.js';
7
7
  export declare const TOKEN_HEADER = "x-volter-token";
@@ -101,7 +101,7 @@ export type DoorHost = {
101
101
  reload: () => WorldLayout;
102
102
  /** The twin's wire for a request whose path is the twin's own; null when the world has no such twin. */
103
103
  twinFetch: (vendor: string) => ((request: Request) => Promise<Response>) | null;
104
- /** The world's byte-stream doors (a TCP protocol a twin serves: smtp's own, planetscale's mysql),
104
+ /** The world's byte-stream doors (a TCP protocol a twin serves: smtp's),
105
105
  * by the id a local World gives that listener's service, when this host carries bytes over a
106
106
  * WebSocket (a hosted World takes no inbound TCP). */
107
107
  streams?: () => Record<string, {
@@ -111,22 +111,6 @@ export type DoorHost = {
111
111
  /** Where this host reads a twin's scenario (the handlers document a generative twin serves from,
112
112
  * re-read on every request), or null when the twin takes none here. */
113
113
  scenarioPath?: (vendor: string) => string | null;
114
- /** A twin's mirror as its pack builds it: `index.html` (the shell) or `assets/<file>`, or null when
115
- * the twin has none here. Every host answers the same mount (`/<served>/<vendor>/mirror/`); only
116
- * where the files come from differs (built at serve under Bun, prebuilt under Node, static assets
117
- * on Cloudflare). */
118
- mirror?: (vendor: string, file: string) => Promise<Response | null>;
119
- /** Whether the twin has a mirror on this host, without building it. */
120
- hasMirror?: (vendor: string) => boolean | Promise<boolean>;
121
- /** The account and token a twin's screens open signed in as, resolved by its pack from the twin's state at `root`
122
- * for the account the World's config names (`signIn.as`); null when the pack cannot, or has no such account. */
123
- mirrorSignIn?: (vendor: string, as: string, ctx: {
124
- root: string;
125
- twin: (path: string, init?: RequestInit) => Promise<Response>;
126
- }) => Promise<{
127
- account: string;
128
- token: string;
129
- } | null>;
130
114
  /** Where this World's clock lives in its store (the file every twin reads `worldNow()` from). */
131
115
  clockPath?: () => string;
132
116
  /** The origin a browser reaches this World at (`http://<world>--<org>.localhost:<port>`), when the
@@ -267,7 +251,7 @@ export declare class WorldDoors {
267
251
  private scope;
268
252
  /** The request's scope, whether it came by a token the caller presented or by a browser's session
269
253
  * (the cookie `POST …/session` set), and the token that opened it. Every form presented is a
270
- * candidate: a mirror page sends the VENDOR's bearer (the token its sign-in holds) beside the World's
254
+ * candidate: a screen's page sends the VENDOR's bearer (the token its sign-in holds) beside the World's
271
255
  * session cookie, and the cookie is what opens the World. With a browser origin of its own
272
256
  * (`browserOrigin`), a World honours its session only on that origin: a session cookie on any other
273
257
  * (a host's shared origin, path-addressed) opens nothing. */
@@ -308,18 +292,10 @@ export declare class WorldDoors {
308
292
  private controlRoot;
309
293
  private stateOf;
310
294
  handle(request: Request): Promise<Response>;
311
- /** THE MIRROR MOUNT: `/<served>/<vendor>/mirror/` and `…/mirror/assets/<file>`, keyless and GET only —
312
- * the pack's own shell and client. The shell's <base> is the twin's place in this World, so the
313
- * mirror reads and writes the World's wire with the browser's session (`POST …/session`); any
314
- * other path under mirror/ is one of the mirror's own routes (a reloaded deep link) and gets the
315
- * shell. A link is not a twin and has no mirror. */
316
- private mirror;
317
- /** A vendor's screens' sign-in (`signIn.as`), once per account every twelve hours while this World is served (rotating the
318
- * tokens forgets them): the pack finds the account and mints its credential through the twin's own doors; a failure
319
- * is not kept, so the next page asks again. An account or credential that is not plain printable text is refused. */
320
- private readonly signIns;
321
- private readonly signInAt;
322
- private signedIn;
295
+ /** A twin's workspaces as its discovery door lists them (`GET /twin`'s `screens`: each built workspace screen's id and
296
+ * path), which a person is shown at `/<served>/<vendor><path>`. Asked again after a minute, as `enforcesRead` is. */
297
+ private readonly workspaces;
298
+ private screensOf;
323
299
  /** Whether a twin enforces a read-scope request itself (its manifest names `requestScopes: ['read']`):
324
300
  * only then may the read token reach it with anything but GET or HEAD. Asked once per twin. */
325
301
  private readonly readEnforced;
@@ -342,7 +318,7 @@ export declare class WorldDoors {
342
318
  /** The world's own doors under `/-/<org>/<world>/`. */
343
319
  private door;
344
320
  /** A browser session for this World: the presented token as an HttpOnly cookie scoped to the World's
345
- * paths, so a page served under it (a twin's mirror) reads the wire as the token's holder, in the
321
+ * paths, so a page served under it (a vendor's screen) reads the wire as the token's holder, in the
346
322
  * token's scope. */
347
323
  /** THE LOCAL SESSION: a World served on this machine's loopback never asks the person at it for a token. Any
348
324
  * website can send requests to 127.0.0.1, so what stands in for the token is proof the request is this World's own
@@ -364,7 +340,7 @@ export declare class WorldDoors {
364
340
  private branchesDoor;
365
341
  /** The newest entry's instant across the World's twins (ms), or null when it has none. */
366
342
  private newestEntry;
367
- /** THE MAP: every twin and what it holds, by resource type, and whether it has a mirror. */
343
+ /** THE MAP: every twin and what it holds, by resource type, and the workspaces it shows. */
368
344
  private map;
369
345
  /** THE TIMELINE: the entries of every twin's log merged, newest first, by each entry's instant, then
370
346
  * twin, then position. The World keeps no order across twins finer than its clock, and this claims
@@ -1,4 +1,4 @@
1
- import { getActiveWorldStore, withAncestryLock, captureHistory, historyAtInstant, historyDigest, historyEntries, readHistoryView, worldPaths } from '@volter/world-core';
1
+ import { getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant, historyDigest, historyEntries, readHistoryView, worldPaths } from '@volter/world-core/runtime';
2
2
  // A SERVED WORLD (docs/reference/http-api.md#a-served-world): `volter world serve` puts a world at
3
3
  // `http://<host>:<port>/<org>/<world>/`. Each twin's vendor API is under its vendor id — the
4
4
  // addressed grammar RH2 depends on — and the world's own doors are under `/-/<org>/<world>/`:
@@ -19,24 +19,25 @@ import { getActiveWorldStore, withAncestryLock, captureHistory, historyAtInstant
19
19
  // GET/PUT clock, POST clock/advance the frozen instant every twin stamps from, forward only
20
20
  // GET/POST branches, DELETE branches/<org>/<world> the host's branches of this World ("as of" views)
21
21
  // GET history?at=<instant> each twin's history cut at an instant (where such a branch starts)
22
- // GET /<org>/<world>/<vendor>/mirror/ the twin's mirror, keyless: the vendor's own UI over this World's wire
22
+ // GET /<org>/<world>/<vendor>/<path> the vendor's own screens, the pack's workspaces (its `GET /twin` lists them)
23
23
  // Tokens ride `x-volter-token`: the token opens everything, the read token only reads — on the vendor
24
24
  // wire, a twin that enforces it (`requestScopes: ['read']`) is handed the read token's other requests
25
25
  // marked read-only and refuses the writes itself. `serve` mints both, prints them once and writes them
26
26
  // to `.volter/token` and `.volter/token.read` (0600).
27
27
  // One writer per world by construction: the doors run in this process, serialized per twin.
28
28
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
29
- import { CALLER_HEADER, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, assertSafeChangesetName, blobDigest, destinationOf, openSealedAt, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin } from '@volter/world-core';
29
+ import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin } from '@volter/world-core/runtime';
30
30
  import { APP_READ_ENDPOINT_ENV } from "./init.js";
31
+ import { packFacts } from "./pack-facts.js";
31
32
  import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass } from '@volter/world-access';
32
33
  import { createHash } from 'node:crypto';
33
34
  import { dirname, join, resolve } from 'node:path';
34
- import { appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, changesetContentHash, changesetReadiness, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow, parentEntries, readBranchMeta, rebaseBranch, } from '@volter/world-core';
35
+ import { appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, changesetContentHash, changesetReadiness, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow, parentEntries, readBranchMeta, rebaseBranch, } from '@volter/world-core/runtime';
35
36
  import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from "./changeset.js";
36
37
  import { fetchFromOrigin } from "./origin.js";
37
38
  import { CONSOLE_BASE, consoleRedirect, serveConsoleApart } from "./console-apart.js";
38
39
  import { loadWorldConfig, worldChangesetRules, writeWorldConfig } from "./configs.js";
39
- import { adaptersFor, credentialPath, packMirror, credentialPayloadFrom, deployTwin, deployWorld, loadWorldChecks, twinSigningSecret, materializeRoots, refreshPosture, refreshTwin, rootForControlRoot, rewrapTwinCredentials, sealTwinCredential, sealedCredentialInfo, bindLegacyCredentials, setTwinRoot, spanMs } from "./root.js";
40
+ import { adaptersFor, credentialPath, credentialPayloadFrom, deployTwin, deployWorld, loadWorldChecks, twinSigningSecret, materializeRoots, refreshPosture, refreshTwin, rootForControlRoot, rewrapTwinCredentials, sealTwinCredential, sealedCredentialInfo, bindLegacyCredentials, setTwinRoot, spanMs } from "./root.js";
40
41
  import { clockFile, downWorld, listWorlds, statusWorld, upWorld } from "./runtime.js";
41
42
  import { assertChangesetRules } from "./schema.js";
42
43
  export const TOKEN_HEADER = 'x-volter-token';
@@ -131,15 +132,6 @@ export async function mountWorld(name, opts = {}) {
131
132
  const declared = loadWorldConfig(configRef, worldRoot).config.services.find((s) => s.id === vendor)?.colocate?.scenarioPath;
132
133
  return declared ? resolve(worldRoot, declared) : null;
133
134
  },
134
- // a local mirror is the pack's own, built by its builder (at serve under Bun, prebuilt under Node)
135
- mirror: (vendor, file) => localMirrorFile(worldRoot, vendor, file),
136
- hasMirror: async (vendor) => (await localMirror(worldRoot, vendor)) !== null,
137
- mirrorSignIn: async (vendor, as, ctx) => { const mirror = await localMirror(worldRoot, vendor); try {
138
- return (await mirror?.signIn?.(as, ctx)) ?? null;
139
- }
140
- catch {
141
- return null;
142
- } },
143
135
  clockPath: () => clockFile(worldRoot, name),
144
136
  ...(opts.branches?.(served) ? { branches: opts.branches(served) } : {}),
145
137
  ...(opts.browserOrigin ? { browserOrigin: () => opts.browserOrigin(served), originOf: (other) => opts.browserOrigin(other) } : {}),
@@ -151,7 +143,9 @@ export async function mountWorld(name, opts = {}) {
151
143
  const target = instance?.services[vendor]?.url;
152
144
  if (!target)
153
145
  return null;
154
- return (request) => { const at = new URL(request.url); return fetch(`${target.replace(/\/+$/, '')}${at.pathname}${at.search}`, { method: request.method, headers: request.headers, ...(request.body ? { body: request.body, duplex: 'half' } : {}), redirect: 'manual' }); };
146
+ // the body as bytes, none when empty: an empty body streamed is sent chunked with no last chunk (Bun 1.2), and the
147
+ // twin's server reads the next request on the connection as its continuation (HPE_INVALID_CHUNK_SIZE)
148
+ return async (request) => { const at = new URL(request.url); const body = request.body ? new Uint8Array(await request.arrayBuffer()) : undefined; return fetch(`${target.replace(/\/+$/, '')}${at.pathname}${at.search}`, { method: request.method, headers: request.headers, ...(body?.byteLength ? { body } : {}), redirect: 'manual' }); };
155
149
  },
156
150
  };
157
151
  const world = new WorldDoors(name, worldRoot, configRef, served, host, held('token') ?? mintToken('tok_'), held('token.read') ?? mintToken('tok_r_'));
@@ -239,37 +233,6 @@ export async function mountWorld(name, opts = {}) {
239
233
  }
240
234
  return mounted;
241
235
  }
242
- /** A local World's mirrors: each pack's builders, found once per process, and each client built once. */
243
- const localMirrors = new Map();
244
- const builtMirrorFiles = new Map();
245
- function localMirror(worldRoot, vendor) {
246
- const key = `${worldRoot}\0${vendor}`;
247
- let held = localMirrors.get(key);
248
- if (!held) {
249
- held = packMirror(vendor, worldRoot).catch(() => null);
250
- localMirrors.set(key, held);
251
- }
252
- return held;
253
- }
254
- async function localMirrorFile(worldRoot, vendor, file) {
255
- const mirror = await localMirror(worldRoot, vendor);
256
- if (!mirror)
257
- return null;
258
- if (file === 'index.html')
259
- return new Response(mirror.html(), { headers: { 'content-type': 'text/html; charset=utf-8' } });
260
- const build = file === 'assets/app.js' ? mirror.client : file === 'assets/styles.css' ? mirror.styles : undefined;
261
- if (!build)
262
- return null;
263
- const key = `${worldRoot}\0${vendor}\0${file}`;
264
- let held = builtMirrorFiles.get(key);
265
- // a failed build is not kept: the next request builds again
266
- if (!held) {
267
- held = build().catch(() => { builtMirrorFiles.delete(key); return null; });
268
- builtMirrorFiles.set(key, held);
269
- }
270
- const body = await held;
271
- return body === null ? null : new Response(body, { headers: { 'content-type': file.endsWith('.css') ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8' } });
272
- }
273
236
  /** Boot the world (state kept) and serve it on its own port. Announces and returns after boot; `stop` downs the world. */
274
237
  export async function serveWorld(name, opts = {}) {
275
238
  const mounted = await mountWorld(name, opts);
@@ -362,7 +325,6 @@ export function worldOriginLabel(served) {
362
325
  /** The header the doors set on a read-scope request to a twin: the twin refuses the caller's writes
363
326
  * and still makes the vendor's own moves. It only restricts, so a twin trusts it without a token. */
364
327
  export const READ_ONLY_HEADER = 'x-volter-read-only';
365
- const MIRROR_ASSET = /^assets\/[A-Za-z0-9._-]+$/;
366
328
  const SPAN = /^(\d+(?:\.\d+)?)(s|m|h|d)$/;
367
329
  const SPAN_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
368
330
  /** A W3C traceparent's trace id, or null when the value is not one. */
@@ -399,7 +361,7 @@ export class WorldDoors {
399
361
  }
400
362
  /** New tokens; a request presenting the old ones is refused from the next call on. */
401
363
  // new tokens end everything the old ones reached: every session and every named key
402
- retoken(token, readToken) { this.token = token; this.readToken = readToken; this.endSessions(); this.keepKeys([]); this.signIns.clear(); }
364
+ retoken(token, readToken) { this.token = token; this.readToken = readToken; this.endSessions(); this.keepKeys([]); }
403
365
  /** BROWSER SESSIONS: the session cookie carries an opaque id, never a token, so a leaked cookie is one session and
404
366
  * not the World's write token; rotating the tokens ends every session. Kept in the World's store, so a restart
405
367
  * keeps them; each lasts 30 days from its opening. */
@@ -635,7 +597,7 @@ export class WorldDoors {
635
597
  scope(request) { return this.credential(request).scope; }
636
598
  /** The request's scope, whether it came by a token the caller presented or by a browser's session
637
599
  * (the cookie `POST …/session` set), and the token that opened it. Every form presented is a
638
- * candidate: a mirror page sends the VENDOR's bearer (the token its sign-in holds) beside the World's
600
+ * candidate: a screen's page sends the VENDOR's bearer (the token its sign-in holds) beside the World's
639
601
  * session cookie, and the cookie is what opens the World. With a browser origin of its own
640
602
  * (`browserOrigin`), a World honours its session only on that origin: a session cookie on any other
641
603
  * (a host's shared origin, path-addressed) opens nothing. */
@@ -807,15 +769,21 @@ export class WorldDoors {
807
769
  // its env is what the client reads, with ${host}/${port} the bridge's listener
808
770
  const ws = url.origin.replace(/^http/, 'ws');
809
771
  const streams = Object.fromEntries(Object.entries(this.host.streams?.() ?? {}).map(([id, { protocol }]) => [id, { protocol, url: `${ws}/-/${this.served}/streams/${id}`, env: streamEnv(id, protocol) }]));
810
- // a twin whose SDK is pointed by an app-read endpoint (TUNNEL_SERVER_URL, QSTASH_URL): that name, at its place here
811
- const endpoints = Object.fromEntries(Object.keys(vendors).flatMap((v) => { const name = APP_READ_ENDPOINT_ENV[v]?.injectEnv; return name ? [[name, vendors[v]]] : []; }));
772
+ // each twin's endpoint variables as its descriptor declares them (`endpointEnv`, or the app-read table's), at its
773
+ // place here: the name, and each template on its URL (a template on a listener's host and port is a stream's)
774
+ const endpoints = Object.fromEntries(Object.keys(vendors).flatMap((v) => {
775
+ const declared = packFacts()[v]?.endpointEnv;
776
+ const name = declared?.name ?? APP_READ_ENDPOINT_ENV[v]?.injectEnv;
777
+ const templates = declared?.templates ?? APP_READ_ENDPOINT_ENV[v]?.injectEnvTemplates ?? {};
778
+ return [
779
+ ...(name ? [[name, vendors[v]]] : []),
780
+ ...Object.entries(templates).filter(([, t]) => !/\$\{(?:host|port)\}/.test(t)).map(([n, t]) => [n, t.replaceAll('${url}', vendors[v])]),
781
+ ];
782
+ }));
812
783
  return Response.json({ name: this.served, vendors, ca: null, proxy: null, env: { ...endpoints, VOLTER_TWINS_KEY: presented }, ...(Object.keys(streams).length ? { streams } : {}) });
813
784
  }
814
785
  if (url.pathname.startsWith(prefix)) {
815
786
  const rest = url.pathname.slice(prefix.length);
816
- const mirror = /^([a-z0-9_-]+)\/mirror(?:\/(.*))?$/.exec(rest);
817
- if (mirror)
818
- return await this.mirror(request, url, mirror[1], mirror[2] ?? '');
819
787
  return await this.wire(request, url, rest);
820
788
  }
821
789
  return Response.json({ error: `not this world: serving /${this.served}/` }, { status: 404 });
@@ -824,71 +792,26 @@ export class WorldDoors {
824
792
  return Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
825
793
  }
826
794
  }
827
- /** THE MIRROR MOUNT: `/<served>/<vendor>/mirror/` and `…/mirror/assets/<file>`, keyless and GET only —
828
- * the pack's own shell and client. The shell's <base> is the twin's place in this World, so the
829
- * mirror reads and writes the World's wire with the browser's session (`POST …/session`); any
830
- * other path under mirror/ is one of the mirror's own routes (a reloaded deep link) and gets the
831
- * shell. A link is not a twin and has no mirror. */
832
- async mirror(request, url, vendor, rest, /** answered at the twin's place (the vendor's site) */ atSite = false) {
833
- if (request.method !== 'GET' && request.method !== 'HEAD')
834
- return Response.json({ error: 'the mirror mount serves the shell and its assets only' }, { status: 405, headers: { allow: 'GET, HEAD' } });
835
- const isAsset = MIRROR_ASSET.test(rest);
836
- if (rest.startsWith('assets/') && !isAsset)
837
- return Response.json({ error: 'no such asset' }, { status: 404 });
838
- if (!this.up().services[vendor] || !this.host.mirror)
839
- return Response.json({ error: `no mirror for ${vendor} in ${this.served}` }, { status: 404 });
840
- const file = await this.host.mirror(vendor, isAsset ? rest : 'index.html');
841
- if (!file || file.status !== 200)
842
- return Response.json({ error: isAsset ? 'no such asset' : `no mirror for ${vendor} in ${this.served}` }, { status: 404 });
843
- if (isAsset)
844
- return file;
845
- // the shell's own address is the vendor's site at the twin's place (wire, above): a page opened here goes there, so
846
- // a mirror that routes by its path sees its home, not `mirror/` (the browser keeps any `#/…` route)
847
- if (rest === '' && !atSite && ['document', 'iframe'].includes(request.headers.get('sec-fetch-dest') ?? ''))
848
- return new Response(null, { status: 302, headers: { location: `/${this.served}/${vendor}/${url.search}` } });
849
- // the shell's <base> is the twin's place under this World (its reads go to the World's wire), its assets stay under
850
- // mirror/, and its `#/…` links keep working under that base (world-core mirror-shell.ts)
851
- let html = mirrorShellUnder(await file.text(), `/${this.served}/${vendor}/`);
852
- // SIGNED IN AS the config says (`signIn.as`): the screens are handed the account and a credential the World minted
853
- // for it, before they start. Only to a request that may write this World (a read link signs no one in, and minting
854
- // is a write), on the vendor's site (never the keyless mount), at the World's own origin: a shared origin's other
855
- // pages, and a rebound name reaching this port, are not this World's pages. Checked here, whoever called.
856
- const own = this.host.browserOrigin?.() ?? null;
857
- const signs = atSite && own !== null && url.host === new URL(own).host && this.credential(request).scope === 'write';
858
- const as = signs ? loadWorldConfig(this.configRef, this.worldRoot).config.services.find((s) => s.id === vendor)?.signIn?.as : undefined;
859
- const given = as ? await this.signedIn(vendor, as, url.origin) : null;
860
- if (given) {
861
- // data, never markup: every character that could end the script or the line is escaped, and the replacement is a
862
- // function, so no `$&` or `$'` in a value is expanded into the page
863
- const data = JSON.stringify({ account: given.account, token: given.token }).replace(/[<>&\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
864
- html = html.replace(/<script/i, (tag) => `<script>window.__VOLTER_SIGN_IN__=${data};</script>${tag}`);
865
- }
866
- // where the console lives on the World's own origin, only that origin frames the World's pages (no clickjacking);
867
- // a page that carries a credential is never stored, and differs by who asked
868
- return new Response(html, { status: 200, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': given ? 'no-store, private' : 'no-cache', vary: given ? 'Sec-Fetch-Dest, Cookie, Authorization' : 'Sec-Fetch-Dest', ...(own ? { 'content-security-policy': "frame-ancestors 'self'" } : {}) } });
869
- }
870
- /** A vendor's screens' sign-in (`signIn.as`), once per account every twelve hours while this World is served (rotating the
871
- * tokens forgets them): the pack finds the account and mints its credential through the twin's own doors; a failure
872
- * is not kept, so the next page asks again. An account or credential that is not plain printable text is refused. */
873
- signIns = new Map();
874
- signInAt = new Map();
875
- signedIn(vendor, as, origin) {
876
- const key = `${vendor}\0${as}`;
877
- if (Date.now() - (this.signInAt.get(key) ?? 0) > 43_200_000)
878
- this.signIns.delete(key);
879
- let held = this.signIns.get(key);
880
- if (!held) {
881
- this.signInAt.set(key, Date.now());
882
- const twin = this.host.twinFetch(vendor);
883
- held = !twin || !this.host.mirrorSignIn ? Promise.resolve(null) : this.host.mirrorSignIn(vendor, as, {
884
- root: this.controlRoot(vendor),
885
- twin: (path, init) => twin(new Request(`${origin}${path.startsWith('/') ? path : `/${path}`}`, init)),
886
- }).then((given) => (given && /^[\x21-\x7e]{1,512}$/.test(given.token) && /^[^\x00-\x1f\x7f]{1,256}$/.test(given.account) ? given : null)).catch(() => null);
887
- this.signIns.set(key, held);
888
- void held.then((given) => { if (!given)
889
- this.signIns.delete(key); });
890
- }
891
- return held;
795
+ /** A twin's workspaces as its discovery door lists them (`GET /twin`'s `screens`: each built workspace screen's id and
796
+ * path), which a person is shown at `/<served>/<vendor><path>`. Asked again after a minute, as `enforcesRead` is. */
797
+ workspaces = new Map();
798
+ async screensOf(vendor) {
799
+ const known = this.workspaces.get(vendor);
800
+ if (known !== undefined && Date.now() - known.at < 60_000)
801
+ return known.screens;
802
+ let screens = [];
803
+ const twin = this.host.twinFetch(vendor);
804
+ try {
805
+ const answer = twin ? await twin(new Request('http://world.local/twin', { headers: { [READ_ONLY_HEADER]: '1' } })) : null;
806
+ const listed = answer?.ok ? (await answer.json()).screens : undefined;
807
+ if (Array.isArray(listed))
808
+ screens = listed.filter((x) => typeof x?.id === 'string' && typeof x?.path === 'string').map(({ id, path }) => ({ id, path }));
809
+ }
810
+ catch {
811
+ screens = [];
812
+ }
813
+ this.workspaces.set(vendor, { screens, at: Date.now() });
814
+ return screens;
892
815
  }
893
816
  /** Whether a twin enforces a read-scope request itself (its manifest names `requestScopes: ['read']`):
894
817
  * only then may the read token reach it with anything but GET or HEAD. Asked once per twin. */
@@ -951,7 +874,7 @@ export class WorldDoors {
951
874
  headers.delete(CALLER_HEADER);
952
875
  const who = this.callerOf(request);
953
876
  if (who)
954
- headers.set(CALLER_HEADER, who);
877
+ headers.set(CALLER_HEADER, callerForHeader(who));
955
878
  headers.set('traceparent', traceparentForDelivery(headers.get('traceparent')) ?? newTraceparent());
956
879
  // at a twin that enforces the read scope, every request of the read token is marked read-only (a GET
957
880
  // that would write for its caller is refused too); at any other, its GET passes as it always has
@@ -972,19 +895,6 @@ export class WorldDoors {
972
895
  // an upgrade the twin answered (a tunnel's control or visitor socket) goes back as it is
973
896
  if (answer.status === 101)
974
897
  return answer;
975
- // THE VENDOR'S SITE: a page the twin does not serve as a page itself (it serves an OAuth screen, a redirect) is the
976
- // vendor's own screens, the mirror, answered at that very address: the twin's place is the vendor's site, and a
977
- // mirror's routes are real URLs, reloadable and linkable, as a dev server falls back to its app for unknown pages
978
- const servedAsPage = (answer.status >= 300 && answer.status < 400) || (answer.status < 300 && (answer.headers.get('content-type') ?? '').includes('text/html'));
979
- // a credential reached it: the manifest's keyless GET never becomes a page (and never signs anyone in). A resource
980
- // the twin answers (a JSON link, a stored file, a PDF: 2xx) opened in a tab is that resource, as the vendor's own
981
- // address would be; an address it refuses or has nothing at (a browser brings no vendor token), and the site's
982
- // home, are the screens
983
- const unanswered = answer.status >= 400 || tail.join('/') === '';
984
- if (page && scope !== null && !servedAsPage && unanswered && (await this.host.hasMirror?.(vendor))) {
985
- await answer.body?.cancel().catch(() => undefined);
986
- return await this.mirror(request, url, vendor, '', true);
987
- }
988
898
  // staleness is a fact on the wire: a twin with a root says when the vendor was last observed
989
899
  const out = new Headers(answer.headers);
990
900
  const observedAt = this.observedAt(vendor);
@@ -998,7 +908,8 @@ export class WorldDoors {
998
908
  const type = (answer.headers.get('content-type') ?? '').toLowerCase();
999
909
  if (page && answer.status < 300 && /^(image\/svg\+xml|application\/xhtml\+xml|text\/xml|application\/xml)\b/.test(type))
1000
910
  out.set('content-security-policy', 'sandbox');
1001
- // a page and an API call to one address are answered differently (the vendor's site): caches keep them apart
911
+ // a page and an API call to one address may be answered differently (a stored file opened as a page is sandboxed
912
+ // above): caches keep them apart
1002
913
  out.append('vary', 'Sec-Fetch-Dest');
1003
914
  const setCookies = answer.headers.getSetCookie();
1004
915
  if (setCookies.some((c) => /^\s*(?:__Host-)?volter_world=/i.test(c))) {
@@ -1177,7 +1088,7 @@ export class WorldDoors {
1177
1088
  return Response.json({ error: 'token required' }, { status: 401 });
1178
1089
  const parts = path.split('/').filter(Boolean);
1179
1090
  const [kind, name, verb] = parts;
1180
- // a browser session is opened by either token: the read token's is how a view-only link opens a mirror
1091
+ // a browser session is opened by either token: the read token's is how a view-only link opens a vendor's screens
1181
1092
  // the session gate comes first: a page that holds only the cookie (a sibling World's, the same site) can neither
1182
1093
  // re-issue nor end the session through this door
1183
1094
  const foreign = this.crossOriginSession(request, via);
@@ -1204,7 +1115,8 @@ export class WorldDoors {
1204
1115
  if (Number.isNaN(Date.parse(instant)))
1205
1116
  return Response.json({ error: 'history?at=<ISO-8601 instant>' }, { status: 400 });
1206
1117
  const views = {};
1207
- withAncestryLock(() => { for (const vendor of Object.keys(this.up().services))
1118
+ const vendors = Object.keys(this.up().services);
1119
+ withHistoryLocks(vendors.map((vendor) => worldPaths(this.stateOf(vendor), this.controlRoot(vendor)).dir), () => { for (const vendor of vendors)
1208
1120
  views[vendor] = historyAtInstant(this.stateOf(vendor), instant, this.controlRoot(vendor)); });
1209
1121
  return Response.json({ world: this.served, at: new Date(Date.parse(instant)).toISOString(), views });
1210
1122
  }
@@ -1606,7 +1518,7 @@ export class WorldDoors {
1606
1518
  refresh = null;
1607
1519
  }
1608
1520
  const last = [...parentEntries(state, this.controlRoot(name))].reverse().find((e) => e.landsId && e.receipt);
1609
- return Response.json({ twin: name, state, protocol: this.up().services[name]?.protocol ?? null, mirror: Boolean(await this.host.hasMirror?.(name)), root: root ?? null, credential: sealedCredentialInfo(this.worldRoot, name), refresh, position: wholeLog(state, this.controlRoot(name)).length, lastReceipt: last?.receipt ?? null });
1521
+ return Response.json({ twin: name, state, protocol: this.up().services[name]?.protocol ?? null, screens: await this.screensOf(name), root: root ?? null, credential: sealedCredentialInfo(this.worldRoot, name), refresh, position: wholeLog(state, this.controlRoot(name)).length, lastReceipt: last?.receipt ?? null });
1610
1522
  }
1611
1523
  if (verb === 'refresh' && request.method === 'POST') {
1612
1524
  const root = rootForControlRoot(this.controlRoot(name), name);
@@ -1618,7 +1530,7 @@ export class WorldDoors {
1618
1530
  return Response.json({ error: `no such door: ${request.method} /-/${this.served}/${path}` }, { status: 404 });
1619
1531
  }
1620
1532
  /** A browser session for this World: the presented token as an HttpOnly cookie scoped to the World's
1621
- * paths, so a page served under it (a twin's mirror) reads the wire as the token's holder, in the
1533
+ * paths, so a page served under it (a vendor's screen) reads the wire as the token's holder, in the
1622
1534
  * token's scope. */
1623
1535
  /** THE LOCAL SESSION: a World served on this machine's loopback never asks the person at it for a token. Any
1624
1536
  * website can send requests to 127.0.0.1, so what stands in for the token is proof the request is this World's own
@@ -1821,7 +1733,7 @@ export class WorldDoors {
1821
1733
  }
1822
1734
  return newest;
1823
1735
  }
1824
- /** THE MAP: every twin and what it holds, by resource type, and whether it has a mirror. */
1736
+ /** THE MAP: every twin and what it holds, by resource type, and the workspaces it shows. */
1825
1737
  async map() {
1826
1738
  const twins = [];
1827
1739
  for (const vendor of Object.keys(this.up().services)) {
@@ -1835,7 +1747,7 @@ export class WorldDoors {
1835
1747
  counts.set(r.type, (counts.get(r.type) ?? 0) + 1);
1836
1748
  twins.push({
1837
1749
  twin: vendor, position: wholeLog(state, controlRoot).length,
1838
- mirror: Boolean(await this.host.hasMirror?.(vendor)),
1750
+ screens: await this.screensOf(vendor),
1839
1751
  root: rootForControlRoot(controlRoot, vendor) ?? null,
1840
1752
  resources: [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([type, count]) => ({ type, count })),
1841
1753
  });
@@ -1987,7 +1899,8 @@ export async function landChangeset(name, worldRoot, instance, changeset, expect
1987
1899
  byTwin.set(item.service, [...(byTwin.get(item.service) ?? []), item.action]);
1988
1900
  }
1989
1901
  const landedByTwin = new Map();
1990
- withAncestryLock(() => {
1902
+ const pushed = [...new Set([...Object.keys(expected).filter((twin) => instance.services[twin]), ...byTwin.keys()])];
1903
+ withHistoryLocks(pushed.map((twin) => worldPaths(stateOf(twin), join(instance.dirs.data, twin)).dir), () => {
1991
1904
  for (const [twin, at] of Object.entries(expected)) {
1992
1905
  if (!instance.services[twin])
1993
1906
  throw new Error(`Unknown expected twin ${twin}`);
package/dist/src/tail.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // notice, never an error.
9
9
  import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
10
10
  import { basename, dirname, join } from 'node:path';
11
- import { stateDirName } from '@volter/world-core';
11
+ import { stateDirName } from '@volter/world-core/runtime';
12
12
  import { statusWorld } from "./runtime.js";
13
13
  /** Every action ledger a world service currently owns. One world service can record actions
14
14
  * under one or more STATE services (see runtime.ts `worldServiceStateService`), so this returns
@@ -0,0 +1,3 @@
1
+ import type { upWorld } from './runtime.js';
2
+ export declare function upCommand(subject: string | undefined, rest: string[], root: string): Promise<void>;
3
+ export declare function printReady(instance: Awaited<ReturnType<typeof upWorld>>): void;