@volter/world-runtime 2.0.54 → 3.0.0

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
package/src/schema.ts CHANGED
@@ -39,6 +39,13 @@ export type WorldManifestV2 = {
39
39
 
40
40
  /** The env value `init` writes for a name whose fake must be minted per instance (a service-account JSON with a real throwaway key). */
41
41
  export const MINT_ENV = '$mint';
42
+ /** An env value `$issue:<service id>`: the credential that service's twin issues through its descriptor's
43
+ * `credentialDoor`, asked for at boot once the twin is up (never committed, like `$mint`). */
44
+ export const ISSUE_ENV_PREFIX = '$issue:';
45
+ /** The env value `init` writes for the application's own origin (its base URL, its auth URL): resolved when a command
46
+ * attaches, from the application URL the booter recorded (`volter-world app-url <world> --set`), so the app names
47
+ * the address it is reached at, which only its booter knows (a browser tab's preview origin, a local port). */
48
+ export const APP_URL_ENV = '$app-url';
42
49
  export type WorldShareProvider = 'cloudflare-quick' | 'command';
43
50
 
44
51
  /**
@@ -80,6 +87,9 @@ export type WorldExternalDiscoverSource = 'status' | 'up';
80
87
  */
81
88
  export type WorldExternalDiscover = {
82
89
  as: string;
90
+ /** The wire the discovered connection speaks (`postgres`, `mysql`, `mongodb`, `redis`), so a coverage report binds it
91
+ * to the application's driver instead of calling it unknown. */
92
+ protocol?: string;
83
93
  source?: WorldExternalDiscoverSource;
84
94
  jsonPath?: string;
85
95
  pattern?: string;
@@ -142,6 +152,10 @@ export type WorldServiceConfig = {
142
152
  '//'?: string;
143
153
  id: string;
144
154
  type?: WorldServiceType;
155
+ /** The credential this twin issues the World's applications at boot (its pack's `credentialDoor`, copied by `init`):
156
+ * `up` POSTs `body` to `path` on the running twin and sets each name of `fill` whose value is `$issue:<this id>` to
157
+ * the answer's field `fill` names for it. */
158
+ issues?: { path: string; body: Record<string, unknown>; fill: Record<string, string> };
145
159
  /**
146
160
  * The twin PACKAGE this service boots from (`@volter/twin-<vendor>`), resolved from the world
147
161
  * root at boot by node's own `node_modules` walk (docs/concepts/worlds.md#the-config-and-the-running-world).
@@ -174,9 +188,6 @@ export type WorldServiceConfig = {
174
188
  * to loopback services directly while presenting a disguised world to the app/user.
175
189
  */
176
190
  controlPlane?: boolean;
177
- /** The account a vendor's own screens (its mirror) open signed in as: its handle, username or email, which the pack
178
- * resolves against the accounts this World holds (`<vendor>MirrorSignIn`). Nothing secret is written here. */
179
- signIn?: { as: string };
180
191
  injectEnv?: string;
181
192
  /**
182
193
  * Additional env vars exported into the generated world env after this service starts.
@@ -297,6 +308,8 @@ export type WorldServiceInstance = {
297
308
  port?: number;
298
309
  /** Absent for 'external' services — their URL is discovered into env vars, not assigned by us. */
299
310
  url?: string;
311
+ /** The twin's side ports (TwinPack.sidePorts: the SMTP twin's mailbox), by name: `volter-world url <world> <id>.<name>`. */
312
+ sides?: Record<string, { port: number; url: string }>;
300
313
  /** 0 for 'external' services — they have no process we own (the external tool manages it). */
301
314
  pid: number;
302
315
  log: string;
@@ -404,6 +417,10 @@ export type WorldInstance = {
404
417
  /** The ORIGIN this world clones from and fetches (docs/concepts/the-model.md): a hosted twin's
405
418
  * namespace — its canonical history. Absent = the placeholder is the only origin. */
406
419
  origin?: { url: string; namespace: string; clonedAt?: string; fetchedAt?: string };
420
+ /** The facts files of the packs this World resolved from installed packages (each released pack carries its own,
421
+ * `generated/pack-facts.json`): the kernel's pack-facts loader overlays them on the facts it was built with
422
+ * (docs/contributing/architecture.md, "The catalog: where twins come from"). */
423
+ packFacts?: string[];
407
424
  };
408
425
 
409
426
  function assertStringArray(value: unknown, what: string, path: string): asserts value is string[] {
@@ -471,10 +488,13 @@ function assertExternalServiceConfig(service: Partial<WorldServiceConfig>, path:
471
488
  if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) {
472
489
  throw new Error(`External service "${id}" discover entries must be objects in ${path}`);
473
490
  }
474
- assertKeys(mapping as unknown as Record<string, unknown>, ['as', 'source', 'jsonPath', 'pattern'], `External service "${id}" discover entry`, path);
491
+ assertKeys(mapping as unknown as Record<string, unknown>, ['as', 'protocol', 'source', 'jsonPath', 'pattern'], `External service "${id}" discover entry`, path);
475
492
  if (!mapping.as || typeof mapping.as !== 'string') {
476
493
  throw new Error(`External service "${id}" discover entry must define string "as" in ${path}`);
477
494
  }
495
+ if (mapping.protocol !== undefined && !['postgres', 'mysql', 'mongodb', 'redis', 'http'].includes(mapping.protocol)) {
496
+ throw new Error(`External service "${id}" discover "${mapping.as}" protocol must be postgres, mysql, mongodb, redis or http in ${path}`);
497
+ }
478
498
  const hasJson = typeof mapping.jsonPath === 'string';
479
499
  const hasPattern = typeof mapping.pattern === 'string';
480
500
  if (hasJson === hasPattern) {
@@ -716,7 +736,7 @@ export function assertWorldSelection(value: unknown, path: string): asserts valu
716
736
 
717
737
  function serviceV2ToLegacy(value: unknown, path: string): WorldServiceConfig {
718
738
  if (!record(value)) throw new Error(`World service must be an object in ${path}`);
719
- assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root', 'signIn'], `Service ${String(value.id ?? '')}`, path, true);
739
+ assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root'], `Service ${String(value.id ?? '')}`, path, true);
720
740
  const source = value.source === undefined ? {} : value.source;
721
741
  const execution = value.execution === undefined ? {} : value.execution;
722
742
  const endpoint = value.endpoint === undefined ? {} : value.endpoint;
@@ -733,11 +753,6 @@ function serviceV2ToLegacy(value: unknown, path: string): WorldServiceConfig {
733
753
  assertKeys(environment, ['values'], 'service.execution.environment', path);
734
754
  if (typeof value.id !== 'string' || value.id === '') throw new Error(`World service must define string id in ${path}`);
735
755
  if (value.description !== undefined && typeof value.description !== 'string') throw new Error(`Service "${value.id}" description must be a string in ${path}`);
736
- if (value.signIn !== undefined) {
737
- if (!record(value.signIn)) throw new Error(`Service "${value.id}" signIn must be an object in ${path}`);
738
- assertKeys(value.signIn, ['as'], `Service "${value.id}" signIn`, path);
739
- if (typeof value.signIn.as !== 'string' || value.signIn.as.trim() === '') throw new Error(`Service "${value.id}" signIn.as must name an account (a handle, username or email) in ${path}`);
740
- }
741
756
  if (value.description !== undefined && value['//'] !== undefined) throw new Error(`Service "${value.id}" must not set both description and // in ${path}`);
742
757
  if (execution.cwd !== undefined && typeof execution.cwd !== 'string') throw new Error(`Service "${value.id}" execution.cwd must be a string in ${path}`);
743
758
  if (execution.controlPlane !== undefined && typeof execution.controlPlane !== 'boolean') throw new Error(`Service "${value.id}" execution.controlPlane must be a boolean in ${path}`);
@@ -754,7 +769,7 @@ function serviceV2ToLegacy(value: unknown, path: string): WorldServiceConfig {
754
769
  if (!Array.isArray(bindings.discover)) throw new Error(`Service "${value.id}" bindings.discover must be an array in ${path}`);
755
770
  for (const mapping of bindings.discover) {
756
771
  if (!record(mapping)) throw new Error(`Service "${value.id}" bindings.discover entries must be objects in ${path}`);
757
- assertKeys(mapping, ['as', 'source', 'jsonPath', 'pattern'], `Service "${value.id}" bindings.discover entry`, path);
772
+ assertKeys(mapping, ['as', 'protocol', 'source', 'jsonPath', 'pattern'], `Service "${value.id}" bindings.discover entry`, path);
758
773
  }
759
774
  }
760
775
  const type = value.type;
@@ -787,7 +802,6 @@ function serviceV2ToLegacy(value: unknown, path: string): WorldServiceConfig {
787
802
  ...(bindings.injectEnvTemplates === undefined ? {} : { injectEnvTemplates: bindings.injectEnvTemplates as Record<string, string> }),
788
803
  ...(bindings.cliRedirect === undefined ? {} : { cliRedirect: bindings.cliRedirect as Record<string, string> }),
789
804
  ...(value.root === undefined ? {} : { root: value.root as WorldServiceConfig['root'] }),
790
- ...(value.signIn === undefined ? {} : { signIn: { as: (value.signIn as { as: string }).as } }),
791
805
  ...(external === undefined ? {} : { external }),
792
806
  };
793
807
  if (type !== 'external' && bindings.discover !== undefined) throw new Error(`Service "${legacy.id}" bindings.discover is only valid for external services in ${path}`);
@@ -908,7 +922,6 @@ function serviceToV2(service: WorldServiceConfig): Record<string, unknown> {
908
922
  ...(Object.keys(endpoint).length ? { endpoint } : {}),
909
923
  ...(Object.keys(bindings).length ? { bindings } : {}),
910
924
  ...(service.root === undefined ? {} : { root: service.root }),
911
- ...(service.signIn === undefined ? {} : { signIn: service.signIn }),
912
925
  };
913
926
  }
914
927
 
@@ -1,4 +1,4 @@
1
- import { getActiveWorldStore, withAncestryLock, captureHistory, historyAtInstant, historyDigest, historyEntries, historyLength, readHistoryView, worldPaths, type HistoryReference } from '@volter/world-core';
1
+ import { getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant, historyDigest, historyEntries, historyLength, readHistoryView, worldPaths, type HistoryReference } 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,27 +19,28 @@ 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, readdirSync, readFileSync, writeFileSync , rmSync} from 'node:fs';
29
- import { CALLER_HEADER, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, type TwinRequestJournalCursor, assertSafeChangesetName, blobDigest, destinationOf, openSealedAt, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core';
29
+ import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, type TwinRequestJournalCursor, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core/runtime';
30
30
  import { APP_READ_ENDPOINT_ENV } from './init.ts';
31
+ import { packFacts } from './pack-facts.ts';
31
32
  import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass, type TrustedIssuer, type WorldCredentials } from '@volter/world-access';
32
33
  import { createHash } from 'node:crypto';
33
34
  import { dirname, join, resolve } from 'node:path';
34
35
  import {
35
36
  appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, changesetContentHash, changesetReadiness, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow,
36
37
  parentEntries, readBranchMeta, rebaseBranch, type Changeset, type Entry, type Receipt, type RootConfig,
37
- } from '@volter/world-core';
38
+ } from '@volter/world-core/runtime';
38
39
  import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from './changeset.ts';
39
40
  import { fetchFromOrigin } from './origin.ts';
40
41
  import { CONSOLE_BASE, consoleRedirect, serveConsoleApart, type ConsoleMount } from './console-apart.ts';
41
42
  import { loadWorldConfig, worldChangesetRules, writeWorldConfig } from './configs.ts';
42
- import { adaptersFor, credentialPath, packMirror, type PackMirror, credentialPayloadFrom, deployTwin, deployWorld, loadWorldChecks, twinSigningSecret, materializeRoots, refreshPosture, refreshTwin, rootForControlRoot, rewrapTwinCredentials, sealTwinCredential, sealedCredentialInfo, bindLegacyCredentials, setTwinRoot, spanMs } from './root.ts';
43
+ import { adaptersFor, credentialPath, credentialPayloadFrom, deployTwin, deployWorld, loadWorldChecks, twinSigningSecret, materializeRoots, refreshPosture, refreshTwin, rootForControlRoot, rewrapTwinCredentials, sealTwinCredential, sealedCredentialInfo, bindLegacyCredentials, setTwinRoot, spanMs } from './root.ts';
43
44
  import { clockFile, downWorld, listWorlds, statusWorld, upWorld } from './runtime.ts';
44
45
  import { assertChangesetRules, type WorldChangesetRules, type WorldInstance } from './schema.ts';
45
46
 
@@ -143,10 +144,6 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
143
144
  const declared = loadWorldConfig(configRef, worldRoot).config.services.find((s) => s.id === vendor)?.colocate?.scenarioPath;
144
145
  return declared ? resolve(worldRoot, declared) : null;
145
146
  },
146
- // a local mirror is the pack's own, built by its builder (at serve under Bun, prebuilt under Node)
147
- mirror: (vendor, file) => localMirrorFile(worldRoot, vendor, file),
148
- hasMirror: async (vendor) => (await localMirror(worldRoot, vendor)) !== null,
149
- mirrorSignIn: async (vendor, as, ctx) => { const mirror = await localMirror(worldRoot, vendor); try { return (await mirror?.signIn?.(as, ctx)) ?? null; } catch { return null; } },
150
147
  clockPath: () => clockFile(worldRoot, name),
151
148
  ...(opts.branches?.(served) ? { branches: opts.branches(served)! } : {}),
152
149
  ...(opts.browserOrigin ? { browserOrigin: () => opts.browserOrigin!(served), originOf: (other: string) => opts.browserOrigin!(other) } : {}),
@@ -157,7 +154,9 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
157
154
  twinFetch: (vendor) => {
158
155
  const target = instance?.services[vendor]?.url;
159
156
  if (!target) return null;
160
- 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' } as RequestInit); };
157
+ // the body as bytes, none when empty: an empty body streamed is sent chunked with no last chunk (Bun 1.2), and the
158
+ // twin's server reads the next request on the connection as its continuation (HPE_INVALID_CHUNK_SIZE)
159
+ 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' } as RequestInit); };
161
160
  },
162
161
  };
163
162
  const world = new WorldDoors(name, worldRoot, configRef, served, host, held('token') ?? mintToken('tok_'), held('token.read') ?? mintToken('tok_r_'));
@@ -222,28 +221,6 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
222
221
  return mounted;
223
222
  }
224
223
 
225
- /** A local World's mirrors: each pack's builders, found once per process, and each client built once. */
226
- const localMirrors = new Map<string, Promise<PackMirror | null>>();
227
- const builtMirrorFiles = new Map<string, Promise<string | null>>();
228
- function localMirror(worldRoot: string, vendor: string): Promise<PackMirror | null> {
229
- const key = `${worldRoot}\0${vendor}`;
230
- let held = localMirrors.get(key);
231
- if (!held) { held = packMirror(vendor, worldRoot).catch(() => null); localMirrors.set(key, held); }
232
- return held;
233
- }
234
- async function localMirrorFile(worldRoot: string, vendor: string, file: string): Promise<Response | null> {
235
- const mirror = await localMirror(worldRoot, vendor);
236
- if (!mirror) return null;
237
- if (file === 'index.html') return new Response(mirror.html(), { headers: { 'content-type': 'text/html; charset=utf-8' } });
238
- const build = file === 'assets/app.js' ? mirror.client : file === 'assets/styles.css' ? mirror.styles : undefined;
239
- if (!build) return null;
240
- const key = `${worldRoot}\0${vendor}\0${file}`;
241
- let held = builtMirrorFiles.get(key);
242
- // a failed build is not kept: the next request builds again
243
- if (!held) { held = build().catch(() => { builtMirrorFiles.delete(key); return null; }); builtMirrorFiles.set(key, held); }
244
- const body = await held;
245
- return body === null ? null : new Response(body, { headers: { 'content-type': file.endsWith('.css') ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8' } });
246
- }
247
224
 
248
225
  /** Boot the world (state kept) and serve it on its own port. Announces and returns after boot; `stop` downs the world. */
249
226
  export async function serveWorld(name: string, opts: { root?: string; port?: number; host?: string; consolePort?: number; announce?: (info: { name: string; base: string; token: string; readToken: string; console: string | null }) => void } = {}): Promise<ServedWorld> {
@@ -306,7 +283,7 @@ export type DoorHost = {
306
283
  reload: () => WorldLayout;
307
284
  /** The twin's wire for a request whose path is the twin's own; null when the world has no such twin. */
308
285
  twinFetch: (vendor: string) => ((request: Request) => Promise<Response>) | null;
309
- /** The world's byte-stream doors (a TCP protocol a twin serves: smtp's own, planetscale's mysql),
286
+ /** The world's byte-stream doors (a TCP protocol a twin serves: smtp's),
310
287
  * by the id a local World gives that listener's service, when this host carries bytes over a
311
288
  * WebSocket (a hosted World takes no inbound TCP). */
312
289
  streams?: () => Record<string, { protocol: string }>;
@@ -314,16 +291,6 @@ export type DoorHost = {
314
291
  /** Where this host reads a twin's scenario (the handlers document a generative twin serves from,
315
292
  * re-read on every request), or null when the twin takes none here. */
316
293
  scenarioPath?: (vendor: string) => string | null;
317
- /** A twin's mirror as its pack builds it: `index.html` (the shell) or `assets/<file>`, or null when
318
- * the twin has none here. Every host answers the same mount (`/<served>/<vendor>/mirror/`); only
319
- * where the files come from differs (built at serve under Bun, prebuilt under Node, static assets
320
- * on Cloudflare). */
321
- mirror?: (vendor: string, file: string) => Promise<Response | null>;
322
- /** Whether the twin has a mirror on this host, without building it. */
323
- hasMirror?: (vendor: string) => boolean | Promise<boolean>;
324
- /** The account and token a twin's screens open signed in as, resolved by its pack from the twin's state at `root`
325
- * for the account the World's config names (`signIn.as`); null when the pack cannot, or has no such account. */
326
- mirrorSignIn?: (vendor: string, as: string, ctx: { root: string; twin: (path: string, init?: RequestInit) => Promise<Response> }) => Promise<{ account: string; token: string } | null>;
327
294
  /** Where this World's clock lives in its store (the file every twin reads `worldNow()` from). */
328
295
  clockPath?: () => string;
329
296
  /** The origin a browser reaches this World at (`http://<world>--<org>.localhost:<port>`), when the
@@ -375,7 +342,6 @@ export function worldOriginLabel(served: string): string | null {
375
342
  /** The header the doors set on a read-scope request to a twin: the twin refuses the caller's writes
376
343
  * and still makes the vendor's own moves. It only restricts, so a twin trusts it without a token. */
377
344
  export const READ_ONLY_HEADER = 'x-volter-read-only';
378
- const MIRROR_ASSET = /^assets\/[A-Za-z0-9._-]+$/;
379
345
  const SPAN = /^(\d+(?:\.\d+)?)(s|m|h|d)$/;
380
346
  const SPAN_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 } as const;
381
347
  /** A W3C traceparent's trace id, or null when the value is not one. */
@@ -400,7 +366,7 @@ export class WorldDoors {
400
366
  constructor(readonly name: string, readonly worldRoot: string, readonly configRef: string, readonly served: string, private readonly host: DoorHost, token: string, readToken: string) { this.token = token; this.readToken = readToken; }
401
367
  /** New tokens; a request presenting the old ones is refused from the next call on. */
402
368
  // new tokens end everything the old ones reached: every session and every named key
403
- retoken(token: string, readToken: string): void { this.token = token; this.readToken = readToken; this.endSessions(); this.keepKeys([]); this.signIns.clear(); }
369
+ retoken(token: string, readToken: string): void { this.token = token; this.readToken = readToken; this.endSessions(); this.keepKeys([]); }
404
370
 
405
371
  /** BROWSER SESSIONS: the session cookie carries an opaque id, never a token, so a leaked cookie is one session and
406
372
  * not the World's write token; rotating the tokens ends every session. Kept in the World's store, so a restart
@@ -578,7 +544,7 @@ export class WorldDoors {
578
544
  private scope(request: Request): 'write' | 'read' | null { return this.credential(request).scope; }
579
545
  /** The request's scope, whether it came by a token the caller presented or by a browser's session
580
546
  * (the cookie `POST …/session` set), and the token that opened it. Every form presented is a
581
- * candidate: a mirror page sends the VENDOR's bearer (the token its sign-in holds) beside the World's
547
+ * candidate: a screen's page sends the VENDOR's bearer (the token its sign-in holds) beside the World's
582
548
  * session cookie, and the cookie is what opens the World. With a browser origin of its own
583
549
  * (`browserOrigin`), a World honours its session only on that origin: a session cookie on any other
584
550
  * (a host's shared origin, path-addressed) opens nothing. */
@@ -724,14 +690,21 @@ export class WorldDoors {
724
690
  // its env is what the client reads, with ${host}/${port} the bridge's listener
725
691
  const ws = url.origin.replace(/^http/, 'ws');
726
692
  const streams = Object.fromEntries(Object.entries(this.host.streams?.() ?? {}).map(([id, { protocol }]) => [id, { protocol, url: `${ws}/-/${this.served}/streams/${id}`, env: streamEnv(id, protocol) }]));
727
- // a twin whose SDK is pointed by an app-read endpoint (TUNNEL_SERVER_URL, QSTASH_URL): that name, at its place here
728
- const endpoints = Object.fromEntries(Object.keys(vendors).flatMap((v) => { const name = APP_READ_ENDPOINT_ENV[v]?.injectEnv; return name ? [[name, vendors[v]!]] : []; }));
693
+ // each twin's endpoint variables as its descriptor declares them (`endpointEnv`, or the app-read table's), at its
694
+ // place here: the name, and each template on its URL (a template on a listener's host and port is a stream's)
695
+ const endpoints = Object.fromEntries(Object.keys(vendors).flatMap((v) => {
696
+ const declared = packFacts()[v]?.endpointEnv;
697
+ const name = declared?.name ?? APP_READ_ENDPOINT_ENV[v]?.injectEnv;
698
+ const templates = declared?.templates ?? APP_READ_ENDPOINT_ENV[v]?.injectEnvTemplates ?? {};
699
+ return [
700
+ ...(name ? [[name, vendors[v]!]] : []),
701
+ ...Object.entries(templates).filter(([, t]) => !/\$\{(?:host|port)\}/.test(t)).map(([n, t]) => [n, t.replaceAll('${url}', vendors[v]!)]),
702
+ ];
703
+ }));
729
704
  return Response.json({ name: this.served, vendors, ca: null, proxy: null, env: { ...endpoints, VOLTER_TWINS_KEY: presented }, ...(Object.keys(streams).length ? { streams } : {}) });
730
705
  }
731
706
  if (url.pathname.startsWith(prefix)) {
732
707
  const rest = url.pathname.slice(prefix.length);
733
- const mirror = /^([a-z0-9_-]+)\/mirror(?:\/(.*))?$/.exec(rest);
734
- if (mirror) return await this.mirror(request, url, mirror[1]!, mirror[2] ?? '');
735
708
  return await this.wire(request, url, rest);
736
709
  }
737
710
  return Response.json({ error: `not this world: serving /${this.served}/` }, { status: 404 });
@@ -740,64 +713,21 @@ export class WorldDoors {
740
713
  }
741
714
  }
742
715
 
743
- /** THE MIRROR MOUNT: `/<served>/<vendor>/mirror/` and `…/mirror/assets/<file>`, keyless and GET only —
744
- * the pack's own shell and client. The shell's <base> is the twin's place in this World, so the
745
- * mirror reads and writes the World's wire with the browser's session (`POST …/session`); any
746
- * other path under mirror/ is one of the mirror's own routes (a reloaded deep link) and gets the
747
- * shell. A link is not a twin and has no mirror. */
748
- private async mirror(request: Request, url: URL, vendor: string, rest: string, /** answered at the twin's place (the vendor's site) */ atSite = false): Promise<Response> {
749
- if (request.method !== 'GET' && request.method !== 'HEAD') return Response.json({ error: 'the mirror mount serves the shell and its assets only' }, { status: 405, headers: { allow: 'GET, HEAD' } });
750
- const isAsset = MIRROR_ASSET.test(rest);
751
- if (rest.startsWith('assets/') && !isAsset) return Response.json({ error: 'no such asset' }, { status: 404 });
752
- if (!this.up().services[vendor] || !this.host.mirror) return Response.json({ error: `no mirror for ${vendor} in ${this.served}` }, { status: 404 });
753
- const file = await this.host.mirror(vendor, isAsset ? rest : 'index.html');
754
- if (!file || file.status !== 200) return Response.json({ error: isAsset ? 'no such asset' : `no mirror for ${vendor} in ${this.served}` }, { status: 404 });
755
- if (isAsset) return file;
756
- // the shell's own address is the vendor's site at the twin's place (wire, above): a page opened here goes there, so
757
- // a mirror that routes by its path sees its home, not `mirror/` (the browser keeps any `#/…` route)
758
- if (rest === '' && !atSite && ['document', 'iframe'].includes(request.headers.get('sec-fetch-dest') ?? '')) return new Response(null, { status: 302, headers: { location: `/${this.served}/${vendor}/${url.search}` } });
759
- // the shell's <base> is the twin's place under this World (its reads go to the World's wire), its assets stay under
760
- // mirror/, and its `#/…` links keep working under that base (world-core mirror-shell.ts)
761
- let html = mirrorShellUnder(await file.text(), `/${this.served}/${vendor}/`);
762
- // SIGNED IN AS the config says (`signIn.as`): the screens are handed the account and a credential the World minted
763
- // for it, before they start. Only to a request that may write this World (a read link signs no one in, and minting
764
- // is a write), on the vendor's site (never the keyless mount), at the World's own origin: a shared origin's other
765
- // pages, and a rebound name reaching this port, are not this World's pages. Checked here, whoever called.
766
- const own = this.host.browserOrigin?.() ?? null;
767
- const signs = atSite && own !== null && url.host === new URL(own).host && this.credential(request).scope === 'write';
768
- const as = signs ? loadWorldConfig(this.configRef, this.worldRoot).config.services.find((s) => s.id === vendor)?.signIn?.as : undefined;
769
- const given = as ? await this.signedIn(vendor, as, url.origin) : null;
770
- if (given) {
771
- // data, never markup: every character that could end the script or the line is escaped, and the replacement is a
772
- // function, so no `$&` or `$'` in a value is expanded into the page
773
- const data = JSON.stringify({ account: given.account, token: given.token }).replace(/[<>&\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
774
- html = html.replace(/<script/i, (tag) => `<script>window.__VOLTER_SIGN_IN__=${data};</script>${tag}`);
775
- }
776
- // where the console lives on the World's own origin, only that origin frames the World's pages (no clickjacking);
777
- // a page that carries a credential is never stored, and differs by who asked
778
- 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'" } : {}) } });
779
- }
780
-
781
- /** A vendor's screens' sign-in (`signIn.as`), once per account every twelve hours while this World is served (rotating the
782
- * tokens forgets them): the pack finds the account and mints its credential through the twin's own doors; a failure
783
- * is not kept, so the next page asks again. An account or credential that is not plain printable text is refused. */
784
- private readonly signIns = new Map<string, Promise<{ account: string; token: string } | null>>();
785
- private readonly signInAt = new Map<string, number>();
786
- private signedIn(vendor: string, as: string, origin: string): Promise<{ account: string; token: string } | null> {
787
- const key = `${vendor}\0${as}`;
788
- if (Date.now() - (this.signInAt.get(key) ?? 0) > 43_200_000) this.signIns.delete(key);
789
- let held = this.signIns.get(key);
790
- if (!held) {
791
- this.signInAt.set(key, Date.now());
792
- const twin = this.host.twinFetch(vendor);
793
- held = !twin || !this.host.mirrorSignIn ? Promise.resolve(null) : this.host.mirrorSignIn(vendor, as, {
794
- root: this.controlRoot(vendor),
795
- twin: (path, init) => twin(new Request(`${origin}${path.startsWith('/') ? path : `/${path}`}`, init)),
796
- }).then((given) => (given && /^[\x21-\x7e]{1,512}$/.test(given.token) && /^[^\x00-\x1f\x7f]{1,256}$/.test(given.account) ? given : null)).catch(() => null);
797
- this.signIns.set(key, held);
798
- void held.then((given) => { if (!given) this.signIns.delete(key); });
799
- }
800
- return held;
716
+ /** A twin's workspaces as its discovery door lists them (`GET /twin`'s `screens`: each built workspace screen's id and
717
+ * path), which a person is shown at `/<served>/<vendor><path>`. Asked again after a minute, as `enforcesRead` is. */
718
+ private readonly workspaces = new Map<string, { screens: Array<{ id: string; path: string }>; at: number }>();
719
+ private async screensOf(vendor: string): Promise<Array<{ id: string; path: string }>> {
720
+ const known = this.workspaces.get(vendor);
721
+ if (known !== undefined && Date.now() - known.at < 60_000) return known.screens;
722
+ let screens: Array<{ id: string; path: string }> = [];
723
+ const twin = this.host.twinFetch(vendor);
724
+ try {
725
+ const answer = twin ? await twin(new Request('http://world.local/twin', { headers: { [READ_ONLY_HEADER]: '1' } })) : null;
726
+ const listed = answer?.ok ? ((await answer.json()) as { screens?: unknown }).screens : undefined;
727
+ if (Array.isArray(listed)) screens = listed.filter((x): x is { id: string; path: string } => typeof x?.id === 'string' && typeof x?.path === 'string').map(({ id, path }) => ({ id, path }));
728
+ } catch { screens = []; }
729
+ this.workspaces.set(vendor, { screens, at: Date.now() });
730
+ return screens;
801
731
  }
802
732
 
803
733
  /** Whether a twin enforces a read-scope request itself (its manifest names `requestScopes: ['read']`):
@@ -848,7 +778,7 @@ export class WorldDoors {
848
778
  // the request's trace: the caller's trace-id goes on (or a new one), under a parent-id the World mints, which the
849
779
  // request's log entries carry, so each entry is joined to its caller by an id no caller chose
850
780
  headers.delete(CALLER_HEADER);
851
- const who = this.callerOf(request); if (who) headers.set(CALLER_HEADER, who);
781
+ const who = this.callerOf(request); if (who) headers.set(CALLER_HEADER, callerForHeader(who));
852
782
  headers.set('traceparent', traceparentForDelivery(headers.get('traceparent')) ?? newTraceparent());
853
783
  // at a twin that enforces the read scope, every request of the read token is marked read-only (a GET
854
784
  // that would write for its caller is refused too); at any other, its GET passes as it always has
@@ -863,19 +793,6 @@ export class WorldDoors {
863
793
  const answer = await twin(new Request(`${url.origin}/${tail.join('/')}${url.search}`, { method: request.method, headers, ...(body === undefined ? {} : { body }), redirect: 'manual' }));
864
794
  // an upgrade the twin answered (a tunnel's control or visitor socket) goes back as it is
865
795
  if (answer.status === 101) return answer;
866
- // THE VENDOR'S SITE: a page the twin does not serve as a page itself (it serves an OAuth screen, a redirect) is the
867
- // vendor's own screens, the mirror, answered at that very address: the twin's place is the vendor's site, and a
868
- // mirror's routes are real URLs, reloadable and linkable, as a dev server falls back to its app for unknown pages
869
- const servedAsPage = (answer.status >= 300 && answer.status < 400) || (answer.status < 300 && (answer.headers.get('content-type') ?? '').includes('text/html'));
870
- // a credential reached it: the manifest's keyless GET never becomes a page (and never signs anyone in). A resource
871
- // the twin answers (a JSON link, a stored file, a PDF: 2xx) opened in a tab is that resource, as the vendor's own
872
- // address would be; an address it refuses or has nothing at (a browser brings no vendor token), and the site's
873
- // home, are the screens
874
- const unanswered = answer.status >= 400 || tail.join('/') === '';
875
- if (page && scope !== null && !servedAsPage && unanswered && (await this.host.hasMirror?.(vendor!))) {
876
- await answer.body?.cancel().catch(() => undefined);
877
- return await this.mirror(request, url, vendor!, '', true);
878
- }
879
796
  // staleness is a fact on the wire: a twin with a root says when the vendor was last observed
880
797
  const out = new Headers(answer.headers);
881
798
  const observedAt = this.observedAt(vendor!);
@@ -887,7 +804,8 @@ export class WorldDoors {
887
804
  // an origin of its own with no scripts, so it can neither read the session's World nor make it a key
888
805
  const type = (answer.headers.get('content-type') ?? '').toLowerCase();
889
806
  if (page && answer.status < 300 && /^(image\/svg\+xml|application\/xhtml\+xml|text\/xml|application\/xml)\b/.test(type)) out.set('content-security-policy', 'sandbox');
890
- // a page and an API call to one address are answered differently (the vendor's site): caches keep them apart
807
+ // a page and an API call to one address may be answered differently (a stored file opened as a page is sandboxed
808
+ // above): caches keep them apart
891
809
  out.append('vary', 'Sec-Fetch-Dest');
892
810
  const setCookies = answer.headers.getSetCookie();
893
811
  if (setCookies.some((c) => /^\s*(?:__Host-)?volter_world=/i.test(c))) {
@@ -1031,7 +949,7 @@ export class WorldDoors {
1031
949
  if (scope === null) return Response.json({ error: 'token required' }, { status: 401 });
1032
950
  const parts = path.split('/').filter(Boolean);
1033
951
  const [kind, name, verb] = parts;
1034
- // a browser session is opened by either token: the read token's is how a view-only link opens a mirror
952
+ // a browser session is opened by either token: the read token's is how a view-only link opens a vendor's screens
1035
953
  // the session gate comes first: a page that holds only the cookie (a sibling World's, the same site) can neither
1036
954
  // re-issue nor end the session through this door
1037
955
  const foreign = this.crossOriginSession(request, via);
@@ -1051,7 +969,8 @@ export class WorldDoors {
1051
969
  const instant = url.searchParams.get('at') ?? '';
1052
970
  if (Number.isNaN(Date.parse(instant))) return Response.json({ error: 'history?at=<ISO-8601 instant>' }, { status: 400 });
1053
971
  const views: Record<string, HistoryReference> = {};
1054
- withAncestryLock(() => { for (const vendor of Object.keys(this.up().services)) views[vendor] = historyAtInstant(this.stateOf(vendor), instant, this.controlRoot(vendor)); });
972
+ const vendors = Object.keys(this.up().services);
973
+ withHistoryLocks(vendors.map((vendor) => worldPaths(this.stateOf(vendor), this.controlRoot(vendor)).dir), () => { for (const vendor of vendors) views[vendor] = historyAtInstant(this.stateOf(vendor), instant, this.controlRoot(vendor)); });
1055
974
  return Response.json({ world: this.served, at: new Date(Date.parse(instant)).toISOString(), views });
1056
975
  }
1057
976
  if (kind === 'map' && !name && request.method === 'GET') return Response.json(await this.map());
@@ -1358,7 +1277,7 @@ export class WorldDoors {
1358
1277
  const marker = getActiveWorldStore().read(join(this.controlRoot(name), stateDirName(), 'world', name, 'refresh.json'));
1359
1278
  let refresh: unknown = null; try { if (marker !== null) refresh = JSON.parse(marker); } catch { refresh = null; }
1360
1279
  const last = [...parentEntries(state, this.controlRoot(name))].reverse().find((e) => e.landsId && e.receipt);
1361
- 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 });
1280
+ 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 });
1362
1281
  }
1363
1282
  if (verb === 'refresh' && request.method === 'POST') {
1364
1283
  const root = rootForControlRoot(this.controlRoot(name), name);
@@ -1370,7 +1289,7 @@ export class WorldDoors {
1370
1289
  }
1371
1290
 
1372
1291
  /** A browser session for this World: the presented token as an HttpOnly cookie scoped to the World's
1373
- * paths, so a page served under it (a twin's mirror) reads the wire as the token's holder, in the
1292
+ * paths, so a page served under it (a vendor's screen) reads the wire as the token's holder, in the
1374
1293
  * token's scope. */
1375
1294
  /** THE LOCAL SESSION: a World served on this machine's loopback never asks the person at it for a token. Any
1376
1295
  * 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
@@ -1536,7 +1455,7 @@ export class WorldDoors {
1536
1455
  return newest;
1537
1456
  }
1538
1457
 
1539
- /** THE MAP: every twin and what it holds, by resource type, and whether it has a mirror. */
1458
+ /** THE MAP: every twin and what it holds, by resource type, and the workspaces it shows. */
1540
1459
  private async map(): Promise<unknown> {
1541
1460
  const twins: unknown[] = [];
1542
1461
  for (const vendor of Object.keys(this.up().services)) {
@@ -1547,7 +1466,7 @@ export class WorldDoors {
1547
1466
  for (const r of readTree(state, controlRoot)) if (r.deleted !== true && !r.type.startsWith('_')) counts.set(r.type, (counts.get(r.type) ?? 0) + 1);
1548
1467
  twins.push({
1549
1468
  twin: vendor, position: wholeLog(state, controlRoot).length,
1550
- mirror: Boolean(await this.host.hasMirror?.(vendor)),
1469
+ screens: await this.screensOf(vendor),
1551
1470
  root: rootForControlRoot(controlRoot, vendor) ?? null,
1552
1471
  resources: [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([type, count]) => ({ type, count })),
1553
1472
  });
@@ -1676,7 +1595,8 @@ export async function landChangeset(name: string, worldRoot: string, instance: W
1676
1595
  byTwin.set(item.service, [...(byTwin.get(item.service) ?? []), item.action as Entry]);
1677
1596
  }
1678
1597
  const landedByTwin = new Map<string, string[]>();
1679
- withAncestryLock(() => {
1598
+ const pushed = [...new Set([...Object.keys(expected).filter((twin) => instance.services[twin]), ...byTwin.keys()])];
1599
+ withHistoryLocks(pushed.map((twin) => worldPaths(stateOf(twin), join(instance.dirs.data, twin)).dir), () => {
1680
1600
  for (const [twin, at] of Object.entries(expected)) {
1681
1601
  if (!instance.services[twin]) throw new Error(`Unknown expected twin ${twin}`);
1682
1602
  const head = captureHistory(stateOf(twin), join(instance.dirs.data, twin));
package/src/tail.ts 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.ts';
13
13
 
14
14
  /** The slice of a ledger row `tail` renders. Rows are vendor-recorded TwinActions
package/src/up-cli.ts ADDED
@@ -0,0 +1,55 @@
1
+ // `volter-world up`, loaded on its own (./cli.ts): it hands the boot to its owner process (./up-task.ts) before
2
+ // any other command's modules load, so a World's start never waits on them. Where each module is compiled as
3
+ // the process starts (a browser tab's engine compiles every module it loads), those modules were most of the
4
+ // CLI's time before the owner process began.
5
+ import { optionValue, ownerOption } from './cli-args.ts';
6
+ import type { upWorld } from './runtime.ts';
7
+ import { superviseWorldUp } from './up-task.ts';
8
+
9
+ export async function upCommand(subject: string | undefined, rest: string[], root: string): Promise<void> {
10
+ if (rest.some(arg => arg === '--env-file' || arg.startsWith('--env-file='))) throw new Error('--env-file is not an input for this command. Use --env-out=<new-output-path>; never pass credentials or custody as an output.');
11
+ if (!subject) throw new Error('volter-world up: missing config');
12
+ const envFile = optionValue(rest, '--env-out');
13
+ if (!envFile) throw new Error('volter-world up: missing required --env-out=<path>');
14
+ const name = optionValue(rest, '--name') || undefined;
15
+ const mode = optionValue(rest, '--mode', 'local') as 'local' | 'share' | 'sealed';
16
+ const isolationArg = optionValue(rest, '--isolation');
17
+ if (isolationArg && isolationArg !== 'process' && isolationArg !== 'colocated' && isolationArg !== 'worker') {
18
+ throw new Error(`volter-world up: invalid --isolation "${isolationArg}" (want process|colocated|worker)`);
19
+ }
20
+ const isolation = (isolationArg || undefined) as 'process' | 'colocated' | 'worker' | undefined;
21
+ // The boot runs in its own owner process (./up-task.ts): a closed terminal does not leave it half-done.
22
+ // --keep-state: boot over the state the World's directory already holds (a World carried somewhere else, e.g. a
23
+ // browser tab's image, comes up over its data instead of starting empty, as a fresh `up` does)
24
+ const keepState = rest.includes('--keep-state');
25
+ const instance = await superviseWorldUp(subject, { name, root, mode, envFile, forceEnvOutput: rest.includes('--force'), owner: ownerOption(rest), ...(isolation ? { isolation } : {}), ...(keepState ? { keepState: true } : {}) });
26
+ printReady(instance);
27
+ }
28
+
29
+ export function printReady(instance: Awaited<ReturnType<typeof upWorld>>): void {
30
+ process.stdout.write(`World ${instance.name} is ready (${instance.config})
31
+
32
+ Services:
33
+ ${Object.values(instance.services).map((service) => ` ${service.id}: ${service.url ?? '(no listener — a World output)'}`).join('\n')}
34
+
35
+ This instance started CLEAN (a world's state lives only while it runs; the world dir is the
36
+ reproducible story) — set the clock and run your seed now, after every up.
37
+
38
+ The two moves: if the twin stores it, create it through the vendor's own API (seed as the
39
+ person, move the clock first for history). Everything else — judgment, lookups, faults — is
40
+ a handler in the world dir's handlers/<vendor>.json. EVERY twin explains itself at
41
+ GET <url>/twin; a scripted twin's GET <url>/twin/scenario lists handlers + misses.
42
+
43
+ Env:
44
+ ${instance.envFile}
45
+
46
+ Instance:
47
+ ${instance.dirs.instance}/instance.json
48
+
49
+ Stop:
50
+ volter-world down ${instance.name}
51
+
52
+ For a bounded job, use volter-world run ... -- <command> to tear down automatically on exit.
53
+ Persistent up does not expire or infer that your sessions are idle.
54
+ `);
55
+ }
@@ -1,4 +1,4 @@
1
- import type { ServeHttpOptions } from '@volter/world-core';
1
+ import type { ServeHttpOptions } from '@volter/world-core/runtime';
2
2
 
3
3
  /** A message relay for the World's TLS fronts. Transport state is connection-local. */
4
4
  export function websocketRelay(targetFor: (request: Request) => string | undefined): NonNullable<ServeHttpOptions['upgrade']> {
@@ -31,7 +31,7 @@ export function worldDoorUnderConsole(path: string, served: (name: string) => bo
31
31
  return Boolean(m && served(`${m[1]}/${m[2]}`));
32
32
  }
33
33
 
34
- /** Whether a path on a World's origin belongs to that World (its wire, its doors, its mirrors) or to
34
+ /** Whether a path on a World's origin belongs to that World (its wire, its doors, its vendors' screens) or to
35
35
  * what every origin carries (`/-/ping`, the console): a World's origin serves no other World. */
36
36
  export function pathOfWorld(path: string, served: string, consoleBase: string): boolean {
37
37
  return path === '/-/ping' || path === consoleBase || path.startsWith(`${consoleBase}/`) || path.startsWith(`/${served}/`) || path.startsWith(`/-/${served}/`) || path === `/${served}` || path === `/-/${served}`;
package/src/world-view.ts CHANGED
@@ -1,11 +1,11 @@
1
- // THE LOCAL VIEW: one World served for a person to step into — its doors, its mirrors, its branches
1
+ // THE LOCAL VIEW: one World served for a person to step into — its doors, its vendors' screens, its branches
2
2
  // ("as of" views) and the console, on one origin (docs/contributing/architecture.md, "Viewing a
3
3
  // World"). `volter world view` is this; the runtime's `up` gains no step. A host that serves many
4
4
  // Worlds (world-host) answers the same doors; this serves one, and the branches made of it.
5
5
  import { existsSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
6
6
  import { loadWorldConfig } from './configs.ts';
7
7
  import { join, resolve } from 'node:path';
8
- import { serveHttp, stateDirName } from '@volter/world-core';
8
+ import { serveHttp, stateDirName } from '@volter/world-core/runtime';
9
9
  import { LocalBranches } from './local-branches.ts';
10
10
  import { mountWorld, type MountedWorld } from './served-world.ts';
11
11
  import { localWorldOrigin, pathOfWorld, worldDoorUnderConsole, worldOfHost } from './world-origins.ts';
@@ -28,7 +28,7 @@ function branchRoots(dir: string): string[] {
28
28
  return out.sort();
29
29
  }
30
30
 
31
- export async function serveWorldView(name: string, opts: { /** the person signed in on this machine (`volter login`): this World's own page acts as them */ localPerson?: string; root?: string; port?: number; host?: string; console?: ViewConsole; announce?: (line: string) => void; /** each World a browser steps into at an origin of its own (on by default on loopback) */ origins?: boolean; /** the World's own origin as a browser reaches it through a proxy on this machine (a desktop serving it as an app):
31
+ export async function serveWorldView(name: string, opts: { /** the person signed in on this machine (`volter login`): this World's own page acts as them */ localPerson?: string; root?: string; port?: number; host?: string; console?: ViewConsole; announce?: (line: string) => void; /** each World a browser steps into at an origin of its own (on by default on loopback) */ origins?: boolean; /** the World's own origin as a browser reaches it through a proxy on this machine (an app that serves it as one of its own pages):
32
32
  * a request the proxy forwards from that host (X-Forwarded-Host) is answered as at that origin, its local session included */ origin?: string } = {}): Promise<WorldView> {
33
33
  const root = resolve(opts.root ?? process.cwd());
34
34
  const host = opts.host ?? '127.0.0.1';