@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
@@ -1,6 +1,6 @@
1
1
  import { copyFileSync, constants, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
2
  import { basename, dirname, join, resolve } from 'node:path';
3
- import { getActiveWorldStore, stateDirName } from '@volter/world-core';
3
+ import { getActiveWorldStore, stateDirName } from '@volter/world-core/runtime';
4
4
  import { assertWorldConfig, LEGACY_WORLD_SELECTION, worldConfigDocument } from "./schema.js";
5
5
  export function resolveConfigPath(config, root = process.cwd()) {
6
6
  const candidates = config.includes('/') || config.endsWith('.json')
@@ -1,4 +1,4 @@
1
- import { type HttpServer } from '@volter/world-core';
1
+ import { type HttpServer } from '@volter/world-core/runtime';
2
2
  /** What a console offers a host: its shell and assets under `base`, told where the Worlds are served. */
3
3
  export type ConsoleMount = {
4
4
  handle: (request: Request, base: string, opts?: {
@@ -23,7 +23,7 @@ export declare function serveConsoleApart(mount: ConsoleMount, opts: {
23
23
  }>;
24
24
  /** The console for a World served elsewhere (a hosted World), on this machine. Two local listeners, as
25
25
  * `volter world serve` has: the console on one origin, its doors forwarded to the host; and the Worlds'
26
- * origin on the next port, forwarding everything (a twin's mirror, the session door) to the host, so
26
+ * origin on the next port, forwarding everything (a vendor's screens, the session door) to the host, so
27
27
  * the console and the pages twins serve are same-site as they are locally, and still two origins. A
28
28
  * host keeps its tenants' names to itself, so the World is named by the URL: `<origin>/<org>/<world>`. */
29
29
  export declare function serveConsoleFor(target: string, opts?: {
@@ -1,10 +1,10 @@
1
1
  // THE CONSOLE ON AN ORIGIN OF ITS OWN. The console keeps the token a person types in its tab's
2
- // storage; the Worlds' origin serves what twins serve (a mirror bundle, a tunnel's visitor pages,
2
+ // storage; the Worlds' origin serves what twins serve (a vendor's screens, a tunnel's visitor pages,
3
3
  // an app's own HTML), and a script there could read any storage of that origin. So a host serves the
4
4
  // console on a listener of its own: the console's shell and bundle, and the host's `/-/` doors
5
5
  // forwarded to the host — never a twin's path, so no twin-served script ever runs on this origin.
6
- // The console opens a World's pages (a twin's mirror) on the Worlds' origin, which it is told.
7
- import { serveHttp } from '@volter/world-core';
6
+ // The console opens a World's pages (a vendor's screens) on the Worlds' origin, which it is told.
7
+ import { serveHttp } from '@volter/world-core/runtime';
8
8
  export const CONSOLE_BASE = '/-/console';
9
9
  /** Serve `mount` on its own listener; doors under `/-/` go to `upstream` (the Worlds' origin). */
10
10
  /** `upstream`: where this listener reaches the host's doors (its own address); `worlds`: the origin the
@@ -41,7 +41,7 @@ export async function serveConsoleApart(mount, opts) {
41
41
  }
42
42
  /** The console for a World served elsewhere (a hosted World), on this machine. Two local listeners, as
43
43
  * `volter world serve` has: the console on one origin, its doors forwarded to the host; and the Worlds'
44
- * origin on the next port, forwarding everything (a twin's mirror, the session door) to the host, so
44
+ * origin on the next port, forwarding everything (a vendor's screens, the session door) to the host, so
45
45
  * the console and the pages twins serve are same-site as they are locally, and still two origins. A
46
46
  * host keeps its tenants' names to itself, so the World is named by the URL: `<origin>/<org>/<world>`. */
47
47
  export async function serveConsoleFor(target, opts = {}) {
@@ -86,7 +86,7 @@ export async function serveConsoleFor(target, opts = {}) {
86
86
  // the World session's cookie, kept on this plain-http loopback origin (a Secure cookie is https-only)
87
87
  for (const c of answer.headers.getSetCookie())
88
88
  out.append('set-cookie', c.replace(/;\s*Secure/i, ''));
89
- // the host's own redirects (the session door's, a twin root's to its mirror) stay on this origin
89
+ // the host's own redirects (the session door's) stay on this origin
90
90
  const location = out.get('location');
91
91
  if (location?.startsWith(origin))
92
92
  out.set('location', `${url.origin}${location.slice(origin.length)}`);
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { hostname } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { stateDirName, withFileLock } from '@volter/world-core';
5
+ import { stateDirName, withFileLock } from '@volter/world-core/runtime';
6
6
  export const CONSUMER_HEARTBEAT_MS = 5000;
7
7
  const STALE_MS = 30_000;
8
8
  export function checkedOwner(owner) {
@@ -77,7 +77,7 @@ export declare function isMappedScopeSibling(name: string): boolean;
77
77
  * `'acknowledged'` was adjudicated acceptable and auto-acknowledges with its recorded
78
78
  * reason); `notExternal` suppresses vendor-adjacent glue (protocol/type/UI helpers, auth
79
79
  * libraries); `patterns` are the external-shape heuristics. AN ENTRY DIES THE DAY ITS PACK
80
- * IS BORN — scripts/packless-claims.test.ts enforces disjointness with pack adoption and
80
+ * IS BORN — scripts/packless-claims.ts enforces disjointness with pack adoption and
81
81
  * SDK_TWINS (the `@planetscale/database`/twin#255 lesson, mechanized). Lazy + cached like
82
82
  * pack-facts.ts's loader — though note this module ALREADY does module-scope I/O via the
83
83
  * overlayCoversMaps call below, so laziness here is consistency, not a load-bearing
@@ -21,15 +21,16 @@
21
21
  // coverage. An external-service-shaped dep/env var with no vendor mapping →
22
22
  // unknown-sdk → exit 1 by default (`--allow-unknown` downgrades to a warning:
23
23
  // unknowns still print, but stop failing the proof).
24
- import { readFileSync } from 'node:fs';
25
- import { dirname, join, resolve } from 'node:path';
24
+ import { existsSync, readFileSync } from 'node:fs';
25
+ import { dirname, join, relative, resolve } from 'node:path';
26
26
  import { fileURLToPath } from 'node:url';
27
+ import { vendorOfPackModule } from '@volter/world-core/runtime';
27
28
  import { loadWorldConfig } from "./configs.js";
28
29
  import { injectableVendorKeys, loadInject, twinUrlVendorFor } from "./inject-map.js";
29
- import { connectionProtocol, projectConnections, projectDependencies, projectEnvNames, projectFetchUrls, projectNpmRegistries, PYPI_TWINS, SDK_TWINS } from "./project-inspect.js";
30
+ import { connectionProtocol, projectConnections, projectDependencies, projectManifestDirs, projectEnvNames, projectFetchUrls, projectNpmRegistries, PYPI_TWINS, SDK_TWINS } from "./project-inspect.js";
30
31
  import { overlayCoversMaps, packFacts } from "./pack-facts.js";
31
32
  import { statusWorld } from "./runtime.js";
32
- import { worldSelectionIncludes } from "./schema.js";
33
+ import { ISSUE_ENV_PREFIX, worldSelectionIncludes } from "./schema.js";
33
34
  /** identifiers are compared case/punctuation-insensitively: 's3', 'S3_TWIN_URL' stem,
34
35
  * a 'stripe' service id and the 'ai-gateway' pack name all normalize cleanly. */
35
36
  function normalizeId(text) {
@@ -71,7 +72,7 @@ export function injectorEnvNameForKey(key) {
71
72
  /** Vendor-shaped env var detection (the secondary signal). The var must end in a
72
73
  * credential/endpoint suffix; the remaining stem (framework prefixes stripped,
73
74
  * normalized) is looked up here. */
74
- // Exported for scripts/packless-claims.test.ts, which checks the packless registry against
75
+ // Exported for scripts/packless-claims.ts, which checks the packless registry against
75
76
  // the OVERLAID map (legacy entries + descriptor adoption.envStems in one view) — importing
76
77
  // the real map is unbreakable by formatting, unlike the source-parsing it replaced (§9
77
78
  // skeptic M3, 2026-08-31: a reflow or quote-style change silently emptied the parsed set).
@@ -114,6 +115,24 @@ function envVendorStem(name, suffix) {
114
115
  return null;
115
116
  return normalizeId(stripped.slice(0, match.index));
116
117
  }
118
+ /** The vendor a credential-shaped env name's stem names: the stem left once the suffix is stripped, or else the longest
119
+ * declared stem the name begins with (googleoauth's GOOGLE_CLIENT for GOOGLE_CLIENT_ID, whose `_CLIENT_ID` suffix
120
+ * leaves only GOOGLE). */
121
+ function envStemVendor(name, suffix) {
122
+ const stem = envVendorStem(name, suffix);
123
+ if (stem === null)
124
+ return undefined;
125
+ if (ENV_STEM_VENDORS[stem])
126
+ return ENV_STEM_VENDORS[stem];
127
+ const words = name.replace(ENV_FRAMEWORK_PREFIX, '').split('_');
128
+ // a declared stem of more than one word: a one-word stem (X's `X`) would claim every name it begins
129
+ for (let k = words.length - 1; k >= 2; k--) {
130
+ const vendor = ENV_STEM_VENDORS[normalizeId(words.slice(0, k).join('_'))];
131
+ if (vendor)
132
+ return vendor;
133
+ }
134
+ return undefined;
135
+ }
117
136
  /** Is this env NAME shaped like a CREDENTIAL/endpoint at all (any broad vendor suffix)? The
118
137
  * question `init` asks before deciding whether a value in a repo's `.env.example` may be copied
119
138
  * verbatim: `APP_NAME` may, `ANYTHING_SECRET` may not — a committed example file is exactly where
@@ -121,7 +140,7 @@ function envVendorStem(name, suffix) {
121
140
  * ALWAYS replaced by a fake, whether or not its stem maps to a vendor this repo twins. Exported
122
141
  * from here so the emission path and the proof read ONE definition of "credential-shaped". */
123
142
  export function isCredentialShapedEnvName(name) {
124
- return ENV_SUFFIX_BROAD.test(name.replace(ENV_FRAMEWORK_PREFIX, ''));
143
+ return name === 'GOOGLE_APPLICATION_CREDENTIALS' || ENV_SUFFIX_BROAD.test(name.replace(ENV_FRAMEWORK_PREFIX, ''));
125
144
  }
126
145
  /** The vendor a credential-shaped env NAME betrays, or null. Same broad-suffix route the
127
146
  * coverage proof's secondary signal uses (`STRIPE_SECRET_KEY` → 'stripe'). */
@@ -138,8 +157,7 @@ export function envNameVendor(name) {
138
157
  // So: this asks "is this name a VENDOR CREDENTIAL that must be faked?" (a hostname is not), while
139
158
  // `detectRepoVendors` asks "which vendors does this repo talk to?" (the mail path certainly is
140
159
  // one). Same string, two honest answers.
141
- const stem = envVendorStem(name, ENV_SUFFIX_BROAD);
142
- return stem === null ? null : ENV_STEM_VENDORS[stem] ?? null;
160
+ return envStemVendor(name, ENV_SUFFIX_BROAD) ?? null;
143
161
  }
144
162
  /** SMTP-shaped env names — the raw-protocol mail path (nodemailer et al.) the HTTP-vendor
145
163
  * heuristics above are structurally blind to. The Cal.com blind run's booking-confirmation
@@ -168,7 +186,7 @@ function isSmtpEnvSignal(name) {
168
186
  const SDK_SCOPE_VENDORS = {
169
187
  // (empty — every entry has moved onto its pack descriptor. See the note below.)
170
188
  };
171
- // descriptor-first migration (adding-a-twin.md §3), COMPLETE for these three tables: every pack declares its own
189
+ // descriptor-first migration (architecture.md, "The descriptor"), COMPLETE for these three tables: every pack declares its own
172
190
  // scopes / env stems / world ids (`adoption` on its descriptor, compiled into the committed
173
191
  // pack-facts artifact), and all three literals above are now empty — they are populated entirely
174
192
  // by the overlay below. They are kept as the overlay's targets, not as homes: the object identity
@@ -261,10 +279,11 @@ function servicePackNames(service) {
261
279
  ...(service.colocate?.module ? [service.colocate.module] : []),
262
280
  ];
263
281
  return commandParts.flatMap((part) => {
264
- const fromPath = part.match(/packages\/twin\/([a-z0-9-]+)\//);
282
+ const fromPath = part.match(/(?:^|[/\\])twin-packs-p[23][/\\]([a-z0-9-]+)[/\\]/);
265
283
  const fromBin = part.match(/^world-([a-z0-9-]+)$/);
266
284
  const fromPkg = part.match(/(?:^|[/\\])@volter\/twin-([a-z0-9-]+)(?:[/\\]|$)/);
267
- return [fromPath?.[1], fromBin?.[1], fromPkg?.[1]].filter((name) => name !== undefined);
285
+ const fromManifest = /\.(?:[cm]?js|tsx?)$/.test(part) ? vendorOfPackModule(part) : undefined;
286
+ return [fromManifest, fromPath?.[1], fromBin?.[1], fromPkg?.[1]].filter((name) => name !== undefined);
268
287
  });
269
288
  }
270
289
  /** Endpoint wiring owned by a service, never a random URL from global config/env. Config
@@ -282,15 +301,10 @@ function serviceConnections(services, effectiveEnv) {
282
301
  const values = instance ? service.env : config?.injectEnvTemplates ?? {};
283
302
  for (const [name, value] of Object.entries(values)) {
284
303
  const protocol = connectionProtocol(value);
285
- const command = [...(Array.isArray(service.command) ? service.command : []), ...('args' in service ? service.args ?? [] : [])];
286
304
  // The pack-transport veto is about NATIVE schemes (a mysql:// template on an HTTP twin); an
287
305
  // http(s) endpoint template on any twin is that twin's own HTTP door.
288
- const incompatiblePack = protocol !== null && facts.some((fact) => {
289
- if (fact.nativeTransport?.protocol === protocol && command.includes(fact.nativeTransport.flag))
290
- return false;
291
- return fact.transport !== 'raw-tcp' || !Object.values(fact.endpointEnv?.templates ?? {})
292
- .some((template) => connectionProtocol(template) === protocol && protocol !== null);
293
- });
306
+ const incompatiblePack = protocol !== null && facts.some((fact) => fact.transport !== 'raw-tcp' || !Object.values(fact.endpointEnv?.templates ?? {})
307
+ .some((template) => connectionProtocol(template) === protocol && protocol !== null));
294
308
  let routed = false;
295
309
  if (protocol !== null && !incompatiblePack) {
296
310
  if (instance) {
@@ -323,7 +337,7 @@ function serviceConnections(services, effectiveEnv) {
323
337
  bindings.set(name, { protocol: template.includes('${url}') || /^https?:/.test(template) ? 'http' : null, label: `${service.id} (${name}; cliRedirect)` });
324
338
  }
325
339
  for (const mapping of config?.external?.discover ?? []) {
326
- bindings.set(mapping.as, { protocol: null, label: `${service.id} (${mapping.as}; discover after up)` });
340
+ bindings.set(mapping.as, { protocol: mapping.protocol ?? null, label: `${service.id} (${mapping.as}; discover after up)` });
327
341
  }
328
342
  }
329
343
  }
@@ -362,11 +376,7 @@ function addServiceIds(ids, service) {
362
376
  .some(([name, value]) => httpNames.has(name) && (/^(?:https?|wss?):/.test(value) || value === '${url}'));
363
377
  const nativeOnly = packNames.length === 0 && endpointValues.some((value) => connectionProtocol(value) !== null) &&
364
378
  !vendorHttpEndpoint;
365
- const nativeMode = packNames.some((name) => {
366
- const mode = packFacts()[name]?.nativeTransport;
367
- return mode && [...(Array.isArray(service.command) ? service.command : []), ...(service.args ?? [])].includes(mode.flag);
368
- });
369
- const interceptable = !nativeOnly && !nativeMode && (injectorReads || appConfigured || hostMatched);
379
+ const interceptable = !nativeOnly && (injectorReads || appConfigured || hostMatched);
370
380
  // twin + external services cover their vendor by identity; a plain 'process'
371
381
  // service only counts via an explicit twin marker (injectEnv/*_TWIN_URL, pack path).
372
382
  if (type === 'twin' || type === 'external') {
@@ -407,7 +417,14 @@ export function worldTwinInventory(world, root) {
407
417
  const ids = new Map();
408
418
  for (const service of config.services)
409
419
  addServiceIds(ids, service);
410
- return { source: 'config', path, ids, connections: serviceConnections(config.services), selection: config.selection, declared: declaredTwin(config.services) };
420
+ const connections = serviceConnections(config.services);
421
+ // a name the World's env leaves to a twin's credential door (`$issue:<service>`) is that twin's, whatever scheme the
422
+ // issued value has: the twin that issues it serves it
423
+ for (const [name, value] of Object.entries(config.env ?? {})) {
424
+ if (value.startsWith(ISSUE_ENV_PREFIX))
425
+ connections.set(name, { protocol: 'issued', label: `${value.slice(ISSUE_ENV_PREFIX.length)} (${name}; issued)` });
426
+ }
427
+ return { source: 'config', path, ids, connections, selection: config.selection, declared: declaredTwin(config.services) };
411
428
  }
412
429
  catch (error) {
413
430
  const configError = error instanceof Error ? error.message : String(error);
@@ -495,8 +512,7 @@ export function detectRepoVendors(repoPath) {
495
512
  detected.set('smtp', [...(detected.get('smtp') ?? []), `env: ${name}`]);
496
513
  continue;
497
514
  }
498
- const stem = envVendorStem(name, ENV_SUFFIX_BROAD);
499
- const vendor = stem === null ? undefined : ENV_STEM_VENDORS[stem];
515
+ const vendor = envStemVendor(name, ENV_SUFFIX_BROAD);
500
516
  if (vendor)
501
517
  detected.set(vendor, [...(detected.get(vendor) ?? []), `env: ${name}`]);
502
518
  }
@@ -545,6 +561,53 @@ export function detectRepoVendors(repoPath) {
545
561
  detected.set(vendor, [...(detected.get(vendor) ?? []), source]);
546
562
  }
547
563
  }
564
+ // a platform's config file that betrays the vendor (a vercel.json declaring crons: the platform calls the app's routes)
565
+ for (const [vendor, facts] of Object.entries(packFacts())) {
566
+ for (const config of facts.adoption?.configFiles ?? []) {
567
+ for (const dir of projectManifestDirs(repo)) {
568
+ const path = join(dir, config.file);
569
+ if (!existsSync(path))
570
+ continue;
571
+ let parsed;
572
+ try {
573
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
574
+ }
575
+ catch {
576
+ continue;
577
+ }
578
+ if (config.when !== undefined && !(parsed && typeof parsed === 'object' && config.when in parsed))
579
+ continue;
580
+ const where = relative(repo, path) || config.file;
581
+ const evidence = config.usage === 'application' ? `config: ${where}${config.when ? ` (${config.when})` : ''}` : `${config.usage}-tool: config: ${where}`;
582
+ detected.set(vendor, [...(detected.get(vendor) ?? []), evidence]);
583
+ }
584
+ }
585
+ }
586
+ // Exact names a descriptor's credential door fills are evidence too (ADC names a file, not a _KEY scalar), when the
587
+ // name names the vendor: its first word is the first word of one of the vendor's env stems (GOOGLE_APPLICATION_CREDENTIALS
588
+ // for GOOGLE_OAUTH), or it holds one of the stems whole (SUPERCODE_TUNNEL_SERVER_URL for TUNNEL). A generic name a door
589
+ // also fills (DATABASE_URL, NODE_AUTH_TOKEN) is every vendor's and so no vendor's evidence; a name that already is the
590
+ // vendor's evidence is not counted twice.
591
+ for (const [vendor, facts] of Object.entries(packFacts())) {
592
+ const stems = facts.adoption?.envStems ?? [];
593
+ const words = new Set(stems.map((stem) => stem.split('_')[0]));
594
+ const names = (name) => {
595
+ const bare = name.replace(ENV_FRAMEWORK_PREFIX, '');
596
+ return words.has(bare.split('_')[0]) || stems.some((stem) => `_${bare}_`.includes(`_${stem}_`));
597
+ };
598
+ for (const name of Object.keys(facts.credentialDoor?.fill ?? {})) {
599
+ if (!envNames.includes(name) || !names(name))
600
+ continue;
601
+ if (!(detected.get(vendor) ?? []).includes(`env: ${name}`))
602
+ detected.set(vendor, [...(detected.get(vendor) ?? []), `env: ${name} (credential issuer)`]);
603
+ }
604
+ }
605
+ // Expand the declared issuer relation transitively, once per vendor; cycles cannot keep detection running.
606
+ for (const vendor of detected.keys()) {
607
+ const issuer = packFacts()[vendor]?.credentialIssuer;
608
+ if (issuer && !detected.has(issuer))
609
+ detected.set(issuer, [`credential issuer: ${vendor}`]);
610
+ }
548
611
  // unknown external-service-shaped signals (no vendor mapping). A dep from a scope with a
549
612
  // twin-mapped sibling counts even when its own name is not service-shaped — the Dub run's
550
613
  // @upstash/redis + @upstash/ratelimit disappeared exactly here while @upstash/qstash mapped.
@@ -569,7 +632,7 @@ export function detectRepoVendors(repoPath) {
569
632
  if (isSmtpEnvSignal(name))
570
633
  continue; // now a DETECTED vendor (above), no longer an unknown
571
634
  const stem = envVendorStem(name, ENV_SUFFIX_STRICT);
572
- if (stem === null || ENV_STEM_VENDORS[stem] || ENV_STEM_IGNORE.has(stem))
635
+ if (stem === null || envStemVendor(name, ENV_SUFFIX_STRICT) || ENV_STEM_IGNORE.has(stem))
573
636
  continue;
574
637
  // The registry's notExternal suppressions apply to ENV STEMS too — the census adjudicated
575
638
  // 40 app-internal stems ("names no vendor") that this loop kept flagging because only the
@@ -709,9 +772,10 @@ export function coverWorld(world, repoPath, options = {}) {
709
772
  // An HTTP-valued endpoint variable the world injects from a twin (its endpoint env or a
710
773
  // template) is routed to that twin; only an HTTP URL nobody in the world supplies stays unknown.
711
774
  const injectedHttp = requirement.protocol === null && binding?.protocol === 'http';
712
- const unresolved = !injectedHttp && (requirement.protocol === null || requirement.env === undefined || (binding !== undefined && binding.protocol === null));
775
+ const issued = binding?.protocol === 'issued';
776
+ const unresolved = !injectedHttp && !issued && (requirement.protocol === null || requirement.env === undefined || (binding !== undefined && binding.protocol === null));
713
777
  let status = unresolved ? 'unknown-sdk'
714
- : injectedHttp || binding?.protocol === requirement.protocol ? 'covered' : 'missing';
778
+ : injectedHttp || issued || binding?.protocol === requirement.protocol ? 'covered' : 'missing';
715
779
  if (status !== 'covered' && acknowledge[vendor] !== undefined) {
716
780
  status = 'acknowledged';
717
781
  acknowledged.push({ vendor, reason: acknowledge[vendor] });
@@ -9,6 +9,6 @@ export type EnvOutputClaim = {
9
9
  backupDir: string;
10
10
  };
11
11
  /** Refuse a custody/input file before any World service or lifecycle mutation starts. */
12
- export declare function claimEnvOutput(path: string, backupDir: string, prior?: PriorOutput, force?: boolean): EnvOutputClaim;
12
+ export declare function claimEnvOutput(path: string, backupDir: string, priors?: PriorOutput[], force?: boolean): EnvOutputClaim;
13
13
  export declare function publishEnvOutput(claim: EnvOutputClaim, contents: string): void;
14
14
  export {};
@@ -15,9 +15,9 @@ function inspect(path) {
15
15
  }
16
16
  }
17
17
  /** Refuse a custody/input file before any World service or lifecycle mutation starts. */
18
- export function claimEnvOutput(path, backupDir, prior, force = false) {
18
+ export function claimEnvOutput(path, backupDir, priors = [], force = false) {
19
19
  const contents = inspect(path);
20
- if (contents !== undefined && !force && !(prior?.path === path && Buffer.from(prior.contents).equals(contents))) {
20
+ if (contents !== undefined && !force && !priors.some((prior) => prior.path === path && Buffer.from(prior.contents).equals(contents))) {
21
21
  throw new Error(`World refuses existing --env-out file it does not own: ${path}. Choose a new output path; never use a custody file. --force explicitly replaces a file with a private backup.`);
22
22
  }
23
23
  if (force && contents !== undefined) {
@@ -134,28 +134,20 @@ export function isGoogleServiceAccountEnvName(name) {
134
134
  * hostname and sends the request to the local twin, while the slug remains unmistakably fake.
135
135
  *
136
136
  * Like CREDENTIAL_SHAPES below, this is duplicated kernel data with an explicit canonical source:
137
- * world-runtime must not import a pack, and the tests pin every value against the injector's actual
138
- * VENDOR_HOSTS predicate so either side changing makes the contract fail loudly. */
137
+ * world-runtime must not import a pack, and the tests pin every value against the injector's host
138
+ * rules (the pack's facts) so either side changing makes the contract fail loudly. */
139
139
  const ENDPOINT_SHAPES = [
140
- // the vendor's own API URL, which the World routes to its twin: an app that reads a base-URL override (empty in its
141
- // example, the vendor's URL as its default) gets the vendor's URL, never an unreachable fake
142
- { match: /^DYNADOT_BASE_URL$/, value: 'https://api.dynadot.com/api3.json', source: 'packages/twin/dynadot/src/index.ts hosts (api.dynadot.com); dubinc/dub apps/web/lib/dynadot/client.ts default' },
143
140
  // Vercel KV's REST URL (the Upstash Redis REST API under Vercel's names): an upstash host the twin serves
144
- { match: /(^|_)KV_REST_API_URL$/, value: 'https://twin-fake.upstash.io', source: 'packages/world-core inject VENDOR_HOSTS.upstash; lukevella/rallly KV_REST_API_URL' },
141
+ { match: /(^|_)KV_REST_API_URL$/, value: 'https://twin-fake.upstash.io', source: 'the upstash pack\'s host rules (.upstash.io); lukevella/rallly KV_REST_API_URL' },
145
142
  {
146
143
  match: /(^|_)UPSTASH_REDIS_REST_URL$/,
147
144
  value: 'https://twin-fake.upstash.io',
148
- source: 'packages/world-core/inject.cjs VENDOR_HOSTS.upstash',
149
- },
150
- {
151
- match: /(^|_)UPSTASH_VECTOR_REST_URL$/,
152
- value: 'https://twin-fake-us1-vector.upstash.io',
153
- source: 'packages/world-core/inject.cjs VENDOR_HOSTS.upstashvector',
145
+ source: 'the upstash pack\'s host rules (.upstash.io)',
154
146
  },
155
147
  {
156
148
  match: /(^|_)TINYBIRD_API_URL$/,
157
149
  value: 'https://api.tinybird.co',
158
- source: 'packages/world-core/inject.cjs VENDOR_HOSTS.tinybird',
150
+ source: 'the tinybird pack\'s host rules (api.tinybird.co)',
159
151
  },
160
152
  ];
161
153
  /** Credentials whose SHAPE the receiving twin actually checks.
@@ -174,19 +166,20 @@ const CREDENTIAL_SHAPES = [
174
166
  // application id, so a structurally wrong fake breaks before any call reaches the twin.
175
167
  { match: /(^|_)DISCORD_[A-Z0-9_]*(TOKEN|SECRET)$/, value: () => `${btoa('1'.repeat(18)).replace(/=+$/, '')}.Gtwinf.${'t'.repeat(38)}`, source: 'a Discord bot token: three dot-separated base64 segments, the first the application id' },
176
168
  // resend-twin.ts enforces bearer auth of the form `re_<...>` whenever a key is presented.
177
- { match: /(^|_)RESEND_[A-Z0-9_]*(KEY|TOKEN|SECRET)$/, value: (name) => `re_twinfake_${name.toLowerCase().replace(/_/g, '')}`, source: 'packages/twin/resend/src/resend-twin.ts' },
169
+ { match: /(^|_)RESEND_[A-Z0-9_]*(KEY|TOKEN|SECRET)$/, value: (name) => `re_twinfake_${name.toLowerCase().replace(/_/g, '')}`, source: 'earlier @volter/twin-resend resend-twin.ts' },
178
170
  // The vercel twin answers only a bearer it minted, its bootstrap token first (`vercel.platform.token_identity`); any
179
171
  // other value is Vercel's 403 "invalid token", so every call an app makes to its Vercel project fails.
180
- { match: /(^|_)VERCEL_[A-Z0-9_]*(TOKEN|API_KEY)$/, value: () => 'vercel_twin_bootstrap_token', source: 'packages/twin/vercel/src/vercel-runtime.ts VERCEL_TWIN_TOKEN' },
172
+ { match: /(^|_)VERCEL_[A-Z0-9_]*(TOKEN|API_KEY)$/, value: () => 'vercel_twin_bootstrap_token', source: 'earlier @volter/twin-vercel vercel-runtime.ts VERCEL_TWIN_TOKEN' },
181
173
  // The tremendous twin takes a key of the shape Tremendous issues, `TEST_` for the sandbox organization at
182
174
  // testflight.tremendous.com and `PROD_` for production (manifest.ts `auth.keyFormat`); any other value is Tremendous's
183
175
  // 401. The `TEST_` prefix is what selects the sandbox host in clients that branch on it (dub's
184
176
  // lib/tremendous/configuration.ts).
185
- { match: /(^|_)TREMENDOUS_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'TEST_twin_sandbox_api_key', source: 'packages/twin/tremendous/src/manifest.ts auth.keyFormat' },
186
- // Clerk: the SECRET key is opaque to the twin (any sk_ shape), but the PUBLISHABLE key is parsed by
177
+ { match: /(^|_)TREMENDOUS_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'TEST_twin_sandbox_api_key', source: 'earlier @volter/twin-tremendous manifest.ts auth.keyFormat' },
178
+ // Clerk: the SECRET key is the instance's own (the clerk twin takes this one and the keys its API Keys door makes,
179
+ // earlier @volter/twin-clerk server.ts CLERK_TWIN_SECRET_KEY), and the PUBLISHABLE key is parsed by
187
180
  // clerk-js — `pk_test_<base64(frontendApiHost$)>` names the frontend-API host the browser calls.
188
181
  // The world issues one naming the dev-instance family the clerk pack CLAIMS by suffix
189
- // (`.clerk.accounts.dev`, packages/twin/clerk/src/index.ts hosts), so the boundary — the injector
182
+ // (`.clerk.accounts.dev`, earlier @volter/twin-clerk index.ts hosts), so the boundary — the injector
190
183
  // server-side, the browser proxy browser-side — resolves it to the twin, and an app CSP that
191
184
  // allowlists that host stays legal (peak drive 2026-08-26).
192
185
  // Stripe's keys carry their kind in their prefix, and applications check it before calling (Cal.com's Stripe app
@@ -194,15 +187,14 @@ const CREDENTIAL_SHAPES = [
194
187
  { match: /(^|_)STRIPE_[A-Z0-9_]*WEBHOOK_SECRET$/, value: () => 'whsec_twinfake0000000000000000000000000000', source: 'https://docs.stripe.com/webhooks#verify-events' },
195
188
  { match: /(^|_)STRIPE_[A-Z0-9_]*(SECRET|PRIVATE|API)(_API)?_KEY$/, value: () => 'sk_test_twinfake000000000000000000000000000000', source: 'https://docs.stripe.com/keys (sk_test_)' },
196
189
  { match: /(^|_)STRIPE_[A-Z0-9_]*(PUBLISHABLE|PUBLIC)(_API)?_KEY$/, value: () => 'pk_test_twinfake000000000000000000000000000000', source: 'https://docs.stripe.com/keys (pk_test_)' },
197
- // The slack twin answers a bearer that is none of Slack's token kinds with `invalid_auth`, as Slack does; any `xoxb-`
198
- // acts as the workspace bot, and an app-level token is `xapp-`.
199
- { match: /(^|_)SLACK_[A-Z0-9_]*APP_TOKEN$/, value: () => 'xapp-1-twinfake', source: 'packages/twin/slack/src/slack-server.ts (bearer /^(xox|xapp-)/)' },
200
- { match: /(^|_)SLACK_([A-Z0-9_]*_)?BOT_TOKEN$|(^|_)SLACK_TOKEN$/, value: () => 'xoxb-twinfake', source: 'packages/twin/slack/src/slack-server.ts (bearer /^(xox|xapp-)/); GET /twin identity (any other xoxb-* is the workspace bot)' },
201
190
  // tavily-auth.ts and firecrawl-auth.ts refuse a key without the vendor's prefix with the vendor's 401
202
- { match: /(^|_)TAVILY_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'tvly-twinfake00000000000000000000', source: 'packages/twin/tavily/src/tavily-auth.ts' },
203
- { match: /(^|_)FIRECRAWL_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'fc-twinfake00000000000000000000000', source: 'packages/twin/firecrawl/src/firecrawl-auth.ts FIRECRAWL_KEY_PREFIX' },
204
- { match: /(^|_)CLERK_[A-Z0-9_]*SECRET[A-Z0-9_]*$/, value: () => 'sk_test_twinfake000000000000000000000000000000', source: 'packages/twin/clerk/src/clerk-twin.ts' },
205
- { match: /(^|_)CLERK_[A-Z0-9_]*PUBLISHABLE[A-Z0-9_]*$/, value: () => `pk_test_${Buffer.from('twin.clerk.accounts.dev$').toString('base64')}`, source: 'packages/twin/clerk/src/index.ts' },
191
+ { match: /(^|_)TAVILY_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'tvly-twinfake00000000000000000000', source: 'earlier @volter/twin-tavily tavily-auth.ts' },
192
+ { match: /(^|_)FIRECRAWL_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'fc-twinfake00000000000000000000000', source: 'earlier @volter/twin-firecrawl firecrawl-auth.ts FIRECRAWL_KEY_PREFIX' },
193
+ // a Clerk webhook's signing secret is Svix's (`whsec_` and base64), which the app's verifier decodes; the World registers
194
+ // its endpoint with the same secret (the Clerk twin's webhooks door takes `signing_secret`)
195
+ { match: /(^|_)CLERK_WEBHOOK(_SIGNING)?_SECRET$/, value: () => `whsec_${Buffer.from('twin-fake-clerk-webhook-secret!!').toString('base64')}`, source: 'https://clerk.com/docs/webhooks/sync-data (CLERK_WEBHOOK_SIGNING_SECRET, whsec_); https://docs.svix.com/receiving/verifying-payloads/how-manual' },
196
+ { match: /(^|_)CLERK_[A-Z0-9_]*SECRET[A-Z0-9_]*$/, value: () => 'sk_test_twinfake000000000000000000000000000000', source: 'earlier @volter/twin-clerk manifest.ts' },
197
+ { match: /(^|_)CLERK_[A-Z0-9_]*PUBLISHABLE[A-Z0-9_]*$/, value: () => `pk_test_${Buffer.from('twin.clerk.accounts.dev$').toString('base64')}`, source: 'earlier @volter/twin-clerk index.ts' },
206
198
  ];
207
199
  /** Keys the APPLICATION parses before it can use them: a fake that is not the shape its own code decodes throws at the
208
200
  * first use, far from any vendor. Each entry names the reader it satisfies; names differ in what they need (cal.com's
@@ -31,11 +31,13 @@ for (let i = 0; i < argv.length; i++) {
31
31
  const scenarioPath = flags.find((f) => f.startsWith('scenario='))?.slice('scenario='.length);
32
32
  // the World's managed database a pack declaring `managedDatabase` is bound to (runtime.ts, managedDatabaseFor)
33
33
  const database = flags.find((f) => f.startsWith('database='))?.slice('database='.length);
34
+ // the twin's side ports the runtime allocated (TwinPack.sidePorts): `side.<factory option>=<port>`
35
+ const sideOptions = Object.fromEntries(flags.filter((f) => /^side\.[A-Za-z0-9]+=\d+$/.test(f)).map((f) => { const [k, v] = f.slice('side.'.length).split('='); return [k, Number(v)]; }));
34
36
  if (!id || !module || !exportName || !port || !root) {
35
- process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>][|database=<url>])\n`);
37
+ process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>][|database=<url>][|side.<option>=<port>…])\n`);
36
38
  process.exit(2);
37
39
  }
38
- specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}), ...(database ? { database } : {}) });
40
+ specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}), ...(database ? { database } : {}), ...(Object.keys(sideOptions).length ? { sideOptions } : {}) });
39
41
  }
40
42
  if (specs.length === 0) {
41
43
  process.stderr.write('no --spec given\n');
@@ -11,6 +11,12 @@
11
11
  // POST /fault/exit → process.exit(1) (shared: kills host; worker: kills only that worker)
12
12
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
+ import { registerPack } from '@volter/world-core/runtime';
15
+ // The host checks the loaded descriptor before it invokes a factory (architecture, "The catalog"): each twin id the host
16
+ // tests serve with this fixture is a Protocol 3 pack, registered at import in whichever process or worker loads it.
17
+ for (const vendor of ['a', 'b', 'async', 'fixed', 'flaky', 'pending', 'recording', 'rejecting']) {
18
+ registerPack({ vendor, protocol: '3', transport: 'rest', resources: [] });
19
+ }
14
20
  export function createFaultTwinServer(options = {}) {
15
21
  const root = options.root;
16
22
  const countFile = root ? join(root, 'count') : undefined;
@@ -1,4 +1,10 @@
1
1
  /** Only the host, after all starts have returned and ports were checked, publishes this receipt. */
2
2
  export declare function publishHostReady(path: string, boot: string, ports: Record<string, number>): void;
3
- /** Receipt admission is finite and cancellation-aware; stale or malformed state is never readiness. */
4
- export declare function waitForHostReady(path: string, boot: string, ports: ReadonlyMap<string, number>, timeoutMs: number, abort: AbortSignal): Promise<void>;
3
+ /**
4
+ * Receipt admission is cancellation-aware; stale or malformed state is never
5
+ * readiness. `timeoutMs` bounds a host that stops making progress, not a slow
6
+ * one: each change in `progress` (the host log's size, which grows with every
7
+ * twin it starts) begins the bound again, so a host still starting its twins
8
+ * on a busy machine is waited for, and one silent for `timeoutMs` is not.
9
+ */
10
+ export declare function waitForHostReady(path: string, boot: string, ports: ReadonlyMap<string, number>, timeoutMs: number, abort: AbortSignal, progress?: () => number): Promise<void>;
@@ -9,9 +9,16 @@ export function publishHostReady(path, boot, ports) {
9
9
  writeFileSync(temporary, `${JSON.stringify({ version: 1, boot, ports })}\n`, { mode: 0o600 });
10
10
  renameSync(temporary, path);
11
11
  }
12
- /** Receipt admission is finite and cancellation-aware; stale or malformed state is never readiness. */
13
- export async function waitForHostReady(path, boot, ports, timeoutMs, abort) {
14
- const started = Date.now();
12
+ /**
13
+ * Receipt admission is cancellation-aware; stale or malformed state is never
14
+ * readiness. `timeoutMs` bounds a host that stops making progress, not a slow
15
+ * one: each change in `progress` (the host log's size, which grows with every
16
+ * twin it starts) begins the bound again, so a host still starting its twins
17
+ * on a busy machine is waited for, and one silent for `timeoutMs` is not.
18
+ */
19
+ export async function waitForHostReady(path, boot, ports, timeoutMs, abort, progress = () => 0) {
20
+ let started = Date.now();
21
+ let seen = progress();
15
22
  for (;;) {
16
23
  abort.throwIfAborted();
17
24
  let bytes;
@@ -35,8 +42,13 @@ export async function waitForHostReady(path, boot, ports, timeoutMs, abort) {
35
42
  }
36
43
  return;
37
44
  }
45
+ const now = progress();
46
+ if (now !== seen) {
47
+ seen = now;
48
+ started = Date.now();
49
+ }
38
50
  if (Date.now() - started >= timeoutMs)
39
- throw new Error(`The co-located host did not complete every declared start within ${timeoutMs} ms.`);
51
+ throw new Error(`The co-located host made no progress for ${timeoutMs} ms before completing every declared start.`);
40
52
  await new Promise((resolve, reject) => {
41
53
  const cancel = () => { clearTimeout(timer); abort.removeEventListener('abort', cancel); reject(abort.reason); };
42
54
  const timer = setTimeout(() => { abort.removeEventListener('abort', cancel); resolve(); }, Math.min(25, Math.max(1, timeoutMs - (Date.now() - started))));
@@ -1,3 +1,4 @@
1
+ import { getPack, registerPack } from '@volter/world-core/runtime';
1
2
  // Worker entry for 'worker' isolation: one Worker thread per twin. It owns its event
2
3
  // loop + heap, so a CPU hog / leak / hard crash here is contained to this thread — the
3
4
  // host does NOT restart it: it gives up on the worker (fires a `gaveup` event → doctor
@@ -6,11 +7,15 @@ import { parentPort, workerData } from 'node:worker_threads';
6
7
  import { importModule } from "./import-module.js";
7
8
  const spec = workerData;
8
9
  const mod = await importModule(spec.module);
10
+ const descriptor = mod.pack ?? getPack(spec.id);
11
+ if (!descriptor || descriptor.vendor !== spec.id)
12
+ throw new Error(`Twin ${spec.id}: the loaded module declares no matching pack descriptor`);
13
+ registerPack(descriptor);
9
14
  const factory = mod[spec.export];
10
15
  if (typeof factory !== 'function') {
11
16
  throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
12
17
  }
13
18
  // Awaited: an async factory (fly) must post `ready` only once its listener is bound.
14
- const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.database !== undefined ? { database: spec.database } : {}) });
19
+ const server = await factory({ ...(spec.sideOptions ?? {}), port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.database !== undefined ? { database: spec.database } : {}) });
15
20
  parentPort?.postMessage({ type: 'ready', port: server.port });
16
21
  // The Bun.serve listener keeps this worker's event loop alive; nothing else to do.
@@ -14,6 +14,8 @@ export type ColocatedTwinSpec = {
14
14
  /** The World's managed database a pack declaring `managedDatabase` serves over (runtime.ts, managedDatabaseFor):
15
15
  * the colocated form of the spawn path's `serve <arg> <url>`. */
16
16
  database?: string;
17
+ /** The twin's side ports the runtime allocated (TwinPack.sidePorts), as the factory's options (`inspectPort`). */
18
+ sideOptions?: Record<string, number>;
17
19
  };
18
20
  export type HostIsolation = 'shared' | 'worker';
19
21
  export type ColocatedHost = {
package/dist/src/host.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { getPack, registerPack } from '@volter/world-core/runtime';
1
2
  // Co-located twin host — run many twins in ONE process instead of one OS process
2
3
  // per twin. The twin core is a pure, transport-agnostic handler (see a pack's
3
4
  // `handle<Vendor>TwinRequest`), so nothing requires a process per twin; this host
@@ -26,6 +27,10 @@ import { pathToFileURL } from 'node:url';
26
27
  import { parallelStartup } from "./startup.js";
27
28
  async function loadFactory(spec) {
28
29
  const mod = await importModule(spec.module);
30
+ const descriptor = mod.pack ?? getPack(spec.id);
31
+ if (!descriptor || descriptor.vendor !== spec.id)
32
+ throw new Error(`Twin ${spec.id}: the loaded module declares no matching pack descriptor`);
33
+ registerPack(descriptor);
29
34
  const factory = mod[spec.export];
30
35
  if (typeof factory !== 'function') {
31
36
  throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
@@ -82,13 +87,16 @@ export async function startColocatedHost(specs, options = {}) {
82
87
  try {
83
88
  await parallelStartup(specs, async (spec) => {
84
89
  const factory = await loadFactory(spec);
85
- const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}), ...(spec.database !== undefined ? { database: spec.database } : {}) });
90
+ const server = await factory({ ...(spec.sideOptions ?? {}), port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}), ...(spec.database !== undefined ? { database: spec.database } : {}) });
86
91
  // Record the handle before validating it, so rollback also owns a bad factory result.
87
92
  servers.push(server);
88
93
  if (!Number.isInteger(server.port) || server.port < 1 || server.port > 65535 || (spec.port !== 0 && server.port !== spec.port)) {
89
94
  throw new Error(`Twin "${spec.id}" bound port ${server.port}, requested ${spec.port}.`);
90
95
  }
91
96
  ports[spec.id] = server.port;
97
+ // Each start is progress the runtime's readiness wait counts (host-ready.ts); the line says when,
98
+ // in milliseconds since the host process began, since the log's own lines carry no time.
99
+ onEvent({ twin: spec.id, kind: 'started', detail: `port ${server.port} at ${Math.round(performance.now())} ms` });
92
100
  });
93
101
  }
94
102
  catch (error) {
@@ -21,9 +21,9 @@ export { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, inj
21
21
  export type { CoverageReport, CoverageRow, CoverageStatus, CoverageOptions, RepoVendorSignals } from './covers.js';
22
22
  export { formatInitReport, initWorld, packCatalog, planWorldInit, renderEnvFile, writeWorldInit } from './init.js';
23
23
  export { STATE_GITIGNORE } from './init.js';
24
- export { clockFile } from './runtime.js';
24
+ export { clockFile, instanceDir } from './runtime.js';
25
25
  export { checkoutCatalog, findInstalledPackage, installedCatalog, resolveCatalog, resolveServicePackage, twinPackageName, TWIN_PACKAGE_PREFIX, type Catalog, type CatalogKind, packCli, packEntry } from './catalog.js';
26
- export { MINT_ENV } from './schema.js';
26
+ export { ISSUE_ENV_PREFIX, MINT_ENV } from './schema.js';
27
27
  export type { EnvDisposition, InitEnvRow, InitInfraStub, InitOptions, InitPlan, InitResult, InitVendorPlan, InitWiring, } from './init.js';
28
28
  export { deriveWorldManifest, findWorldRefFile, isRemoteWorldRef, resolveWorldRef, WORLD_REF_FILE, worldManifest, } from './attach.js';
29
29
  export type { ResolvedWorldRef, WorldManifest, WorldRefSource } from './attach.js';
package/dist/src/index.js CHANGED
@@ -19,9 +19,9 @@ export { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, inj
19
19
  // world config + env file OUTSIDE the repo (fixture-env), and prove the emission with `covers`.
20
20
  export { formatInitReport, initWorld, packCatalog, planWorldInit, renderEnvFile, writeWorldInit } from "./init.js";
21
21
  export { STATE_GITIGNORE } from "./init.js";
22
- export { clockFile } from "./runtime.js";
22
+ export { clockFile, instanceDir } from "./runtime.js";
23
23
  export { checkoutCatalog, findInstalledPackage, installedCatalog, resolveCatalog, resolveServicePackage, twinPackageName, TWIN_PACKAGE_PREFIX, packCli, packEntry } from "./catalog.js";
24
- export { MINT_ENV } from "./schema.js";
24
+ export { ISSUE_ENV_PREFIX, MINT_ENV } from "./schema.js";
25
25
  export { deriveWorldManifest, findWorldRefFile, isRemoteWorldRef, resolveWorldRef, WORLD_REF_FILE, worldManifest, } from "./attach.js";
26
26
  export { parseSni, readReflectRoutes, reflectRoutesPath, reflectManifestPath, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, CA_TRUST_ENV, ATTACHED_CA_PATH, startReflectFront, startReflectResolver, writeReflectRoutes, } from "./reflect.js";
27
27
  export { startColocatedHost } from "./host.js";