@volter/world-runtime 3.0.166 → 3.0.168

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 (82) hide show
  1. package/dist/known-external-services.json +0 -1
  2. package/dist/src/actions-runner.js +53 -14
  3. package/dist/src/attach.d.ts +9 -0
  4. package/dist/src/attach.js +9 -2
  5. package/dist/src/cli-commands.js +19 -3
  6. package/dist/src/console-apart.js +3 -2
  7. package/dist/src/credential-door.d.ts +8 -0
  8. package/dist/src/credential-door.js +54 -0
  9. package/dist/src/fixture-env.js +2 -2
  10. package/dist/src/forwardable-headers.d.ts +1 -0
  11. package/dist/src/forwardable-headers.js +19 -0
  12. package/dist/src/go-mysql-binding.d.ts +2 -0
  13. package/dist/src/go-mysql-binding.js +7 -1
  14. package/dist/src/guest-runner.js +8 -4
  15. package/dist/src/host-ready.d.ts +7 -3
  16. package/dist/src/host-ready.js +16 -3
  17. package/dist/src/index.d.ts +6 -1
  18. package/dist/src/index.js +7 -1
  19. package/dist/src/infra.js +10 -5
  20. package/dist/src/init.d.ts +5 -0
  21. package/dist/src/init.js +157 -23
  22. package/dist/src/managed-api-needs.d.ts +101 -0
  23. package/dist/src/managed-api-needs.js +312 -0
  24. package/dist/src/managed-api-source.d.ts +24 -0
  25. package/dist/src/managed-api-source.js +59 -0
  26. package/dist/src/mongodb-backing.js +33 -58
  27. package/dist/src/mongodb-command.d.ts +21 -0
  28. package/dist/src/mongodb-command.js +317 -0
  29. package/dist/src/pglite-host.mjs +2 -2
  30. package/dist/src/placeholder.d.ts +24 -1
  31. package/dist/src/placeholder.js +105 -13
  32. package/dist/src/process-signal.d.mts +2 -0
  33. package/dist/src/process-signal.mjs +44 -2
  34. package/dist/src/project-inspect.d.ts +1 -1
  35. package/dist/src/project-inspect.js +8 -1
  36. package/dist/src/redirect-proxy.js +4 -19
  37. package/dist/src/reflect.js +2 -2
  38. package/dist/src/runtime.d.ts +5 -1
  39. package/dist/src/runtime.js +311 -59
  40. package/dist/src/schema.d.ts +10 -0
  41. package/dist/src/schema.js +76 -3
  42. package/dist/src/serve.js +24 -4
  43. package/dist/src/served-world.d.ts +2 -1
  44. package/dist/src/served-world.js +6 -4
  45. package/dist/src/service-command.d.ts +7 -3
  46. package/dist/src/service-command.js +53 -22
  47. package/dist/src/service-environment.d.ts +4 -0
  48. package/dist/src/service-environment.js +21 -0
  49. package/dist/src/service-recorder.js +9 -13
  50. package/known-external-services.json +0 -1
  51. package/package.json +5 -5
  52. package/src/actions-runner.ts +38 -10
  53. package/src/attach.ts +15 -2
  54. package/src/cli-commands.ts +18 -3
  55. package/src/console-apart.ts +3 -2
  56. package/src/credential-door.ts +54 -0
  57. package/src/fixture-env.ts +2 -2
  58. package/src/forwardable-headers.ts +15 -0
  59. package/src/go-mysql-binding.ts +8 -1
  60. package/src/guest-runner.ts +8 -4
  61. package/src/host-ready.ts +12 -3
  62. package/src/index.ts +8 -1
  63. package/src/infra.ts +8 -3
  64. package/src/init.ts +145 -23
  65. package/src/managed-api-needs.ts +285 -0
  66. package/src/managed-api-source.ts +67 -0
  67. package/src/mongodb-backing.ts +32 -48
  68. package/src/mongodb-command.ts +238 -0
  69. package/src/pglite-host.mjs +2 -2
  70. package/src/placeholder.ts +90 -13
  71. package/src/process-signal.d.mts +2 -0
  72. package/src/process-signal.mjs +44 -2
  73. package/src/project-inspect.ts +9 -2
  74. package/src/redirect-proxy.ts +4 -15
  75. package/src/reflect.ts +2 -2
  76. package/src/runtime.ts +272 -45
  77. package/src/schema.ts +73 -3
  78. package/src/serve.ts +22 -4
  79. package/src/served-world.ts +7 -5
  80. package/src/service-command.ts +41 -19
  81. package/src/service-environment.ts +21 -0
  82. package/src/service-recorder.ts +9 -9
package/src/init.ts CHANGED
@@ -1,5 +1,8 @@
1
+ import { withCurrentCredentialDoor } from './credential-door.ts';
2
+ import { composeManagedApiNeeds9, type ManagedApiOwner } from './managed-api-needs.ts';
3
+ import { readManagedApiSourceSession, type ManagedApiSourceReader, type ManagedApiSourceSession } from './managed-api-source.ts';
1
4
  import { copyNativeServerInputs, readNativeServerInputs, nativeServerRuntimeReference, type NativeServerInputs } from './native-server-inputs.ts';
2
- import { goMysqlBinding, renderGoMysqlBinding, type GoMysqlBinding } from './go-mysql-binding.ts';
5
+ import { goMysqlBinding, goMysqlBindingFields, renderGoMysqlBinding, type GoMysqlBinding } from './go-mysql-binding.ts';
3
6
  import * as managedOriginalInputs from '@volter/world-core/managed-original-inputs';
4
7
  import { copyOriginalInputSnapshot, readOriginalInputSnapshot, type OriginalInputReference } from './original-inputs.ts';
5
8
  // `volter-world init <name> --repo <path>` — the DETERMINISTIC FRONT DOOR of world adoption.
@@ -59,7 +62,7 @@ import { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from './catalog-in
59
62
  import { filesLeftAsThePersonsOwn, projectEnvReads, projectManifestDirs, projectMigrationTools, readOnlyWhatTheSourceHolds, sourceText, type ProjectMigrationTool } from './project-inspect.ts';
60
63
  import { overlayEndpointEnv, packFacts } from './pack-facts.ts';
61
64
  import { VENDOR_SERVER_BACKINGS } from './backings.ts';
62
- import { APP_URL_ENV, DEFAULT_WORLD_SELECTION, ISSUE_ENV_PREFIX, MINT_ENV, worldConfigDocument, worldSelectionIncludes, type WorldConfig, type WorldSelection, type WorldServiceConfig } from './schema.ts';
65
+ import { APP_URL_ENV, DEFAULT_WORLD_SELECTION, ISSUE_ENV_PREFIX, MINT_ENV, assertWorldConfig, worldConfigDocument, worldSelectionIncludes, type WorldConfig, type WorldSelection, type WorldServiceConfig } from './schema.ts';
63
66
  import { loadWorldConfig } from './configs.ts';
64
67
  import { parseInfraDefinition } from './pglite-backing.ts';
65
68
  import { ownedPath } from './owned-path.ts';
@@ -228,6 +231,8 @@ export type InitPlan = {
228
231
  stackNeeds?: InitStackNeed[];
229
232
  /** The same declared application names used by selection and its coverage proof. */
230
233
  stackEnvironmentNames?: string[];
234
+ /** Equality/cache identity from the live trusted reader; this digest grants no source custody. */
235
+ readonly applicationApiSourceIdentity?: string;
231
236
  /** the project files a World made for an image left unread: the source does not hold them, so they are the person's own */
232
237
  leftAsThePersonsOwn?: string[];
233
238
  /** the services emitted into the private managed-infrastructure definition */
@@ -295,6 +300,8 @@ export type InitOptions = {
295
300
  bare?: string;
296
301
  /** The project's own stack as its image builder read it (`needs.json`): architecture, "The project's own stack as evidence". */
297
302
  needs?: string;
303
+ /** Reviewed host attachment to immutable source/correspondence. No CLI flag or JSON bit selects it. */
304
+ managedApiSourceReader?: ManagedApiSourceReader;
298
305
  };
299
306
 
300
307
  /** One service of the project's stack, as `init` took it. */
@@ -321,11 +328,15 @@ type StackConnectionComponent = "database" | "user" | "password";
321
328
  type StackNeedsFile = { kind?: unknown; version?: unknown; composeFile?: unknown; application?: { service?: unknown }; settings?: unknown; unset?: unknown; needs?: Array<{ service?: unknown; image?: unknown; family?: unknown; supplier?: unknown; names?: Array<{ name?: unknown; scheme?: unknown; query?: unknown; withPort?: unknown; containerPort?: unknown; dsnFormat?: unknown; endpoint?: unknown; dsnPrefix?: unknown; dsnProtocol?: unknown }>; ports?: unknown; portTargets?: unknown; components?: unknown; originalInputs?: unknown; nativeServerInputs?: unknown }> };
322
329
 
323
330
  /** The needs file, checked for its kind: a file of another shape is refused by name, never half-read. */
324
- function readStackNeeds(path: string): { composeFile: string; application?: string; settings: Array<{ name: string; value: string }>; unset: string[]; needs: Array<{ service: string; image?: string; family?: string; names: Array<{ name: string; scheme?: string; query?: string; withPort?: true; containerPort?: number } & Partial<GoMysqlBinding>>; originalInputs?: OriginalInputReference; nativeServerInputs?: NativeServerInputs; nativeCredentialAliases?: string[]; ports: string[]; portTargets: Record<string, number>; components: Array<{ name: string; component: StackConnectionComponent }> }> } {
331
+ function readStackNeeds(path: string, repo: string, apiReader?: ManagedApiSourceReader): { apiSource?: ManagedApiSourceSession; composeFile: string; application?: string; settings: Array<{ name: string; value: string }>; unset: string[]; needs: Array<{ service: string; image?: string; family?: string; names: Array<{ name: string; scheme?: string; query?: string; withPort?: true; containerPort?: number } & Partial<GoMysqlBinding>>; originalInputs?: OriginalInputReference; nativeServerInputs?: NativeServerInputs; nativeCredentialAliases?: string[]; ports: string[]; portTargets: Record<string, number>; components: Array<{ name: string; component: StackConnectionComponent }> }> } {
325
332
  let document: StackNeedsFile;
326
333
  try { document = JSON.parse(readFileSync(path, 'utf8')) as StackNeedsFile; }
327
334
  catch (error) { throw new Error(`volter init: --needs ${path} could not be read as JSON: ${error instanceof Error ? error.message : String(error)}`); }
328
- if (document.kind !== 'browser-substrate.stack-needs' || (document.version !== 1 && document.version !== 2 && document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7) || !Array.isArray(document.needs)) {
335
+ // A plain CLI has no trusted function and retains its explicit version9 refusal.
336
+ const apiSource = document.version === 9 ? readManagedApiSourceSession(repo, path, apiReader) : undefined;
337
+ if (apiReader !== undefined && apiSource === undefined) throw new Error('volter init: an API source reader cannot be ignored by a non-version9 input');
338
+ if (apiSource) document = apiSource.document;
339
+ if (document.kind !== 'browser-substrate.stack-needs' || (document.version !== 1 && document.version !== 2 && document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7 && document.version !== 9) || !Array.isArray(document.needs)) {
329
340
  throw new Error(`volter init: --needs ${path} is not a stack-needs file (kind "browser-substrate.stack-needs", version 1, 2, 3, 4, 5, 6 or 7)`);
330
341
  }
331
342
  const identifier = (name: string, what: string): string => {
@@ -334,6 +345,7 @@ function readStackNeeds(path: string): { composeFile: string; application?: stri
334
345
  };
335
346
  const record = (value: unknown): Array<[string, unknown]> => value && typeof value === 'object' && !Array.isArray(value) ? Object.entries(value as Record<string, unknown>) : [];
336
347
  return {
348
+ ...(apiSource === undefined ? {} : { apiSource }),
337
349
  composeFile: typeof document.composeFile === 'string' ? document.composeFile : 'the project\'s Compose file',
338
350
  ...(typeof document.application?.service === 'string' ? { application: document.application.service } : {}),
339
351
  // The application's own variables that name no service: its settings as the stack writes them, and those whose
@@ -343,7 +355,7 @@ function readStackNeeds(path: string): { composeFile: string; application?: stri
343
355
  needs: document.needs.filter((need) => typeof need.service === 'string').map((need) => ({
344
356
  service: need.service as string,
345
357
  ...(need.originalInputs === undefined ? {} : { originalInputs: (() => {
346
- if (document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7) throw new Error('volter init: original inputs require needs version4,5,6 or7');
358
+ if (document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7 && document.version !== 9) throw new Error('volter init: original inputs require needs version4,5,6 or7');
347
359
  const reference = managedOriginalInputs.referenceOf(need.originalInputs);
348
360
  const source = readOriginalInputSnapshot(dirname(resolve(path)), reference).snapshot.manifest;
349
361
  const image = source.start.service.image as { raw?: unknown } | undefined;
@@ -351,7 +363,7 @@ function readStackNeeds(path: string): { composeFile: string; application?: stri
351
363
  return reference;
352
364
  })() }),
353
365
  ...(need.nativeServerInputs === undefined ? {} : (() => {
354
- if (document.version !== 5 && document.version !== 7) throw new Error('volter init: native managed input requires needs5 or7');
366
+ if (document.version !== 5 && document.version !== 7 && document.version !== 9) throw new Error('volter init: native managed input requires needs5 or7');
355
367
  const checked = readNativeServerInputs(dirname(resolve(path)), need.nativeServerInputs);
356
368
  const source = JSON.parse(checked.files.get('declaration.json')!.toString('utf8')) as { name?: unknown; image?: { raw?: unknown } };
357
369
  if (need.family !== 'mysql' || need.supplier !== 'a World' || source.name !== need.service || source.image?.raw !== need.image)
@@ -364,12 +376,12 @@ function readStackNeeds(path: string): { composeFile: string; application?: stri
364
376
  // A name becomes a key of the World's environment file: an identifier, or it is refused by name. Anything
365
377
  // else (a newline, an `=`) would write lines of its own there.
366
378
  if (typeof entry.name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(entry.name)) throw new Error(`volter init: --needs ${path} names a variable that is not an identifier under service ${String(need.service)}: ${JSON.stringify(entry.name)}`);
367
- if (entry.containerPort !== undefined && ((document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7) || !Number.isInteger(entry.containerPort) || Number(entry.containerPort) < 1 || Number(entry.containerPort) > 65535)) throw new Error(`volter init: --needs ${path} needs version3 and a valid declared vendor port`);
368
- return { name: entry.name, ...goMysqlBinding(entry, document.version, entry.name, COMPOSE_INFRA.mysql!.containerPort), ...(entry.containerPort !== undefined ? { containerPort: Number(entry.containerPort) } : {}), ...(typeof entry.scheme === 'string' && /^[a-z][a-z0-9+.-]*$/i.test(entry.scheme) ? { scheme: entry.scheme } : {}), ...(typeof entry.query === 'string' ? { query: entry.query } : {}), ...(entry.withPort === true ? { withPort: true as const } : {}) };
379
+ if (entry.containerPort !== undefined && ((document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7 && document.version !== 9) || !Number.isInteger(entry.containerPort) || Number(entry.containerPort) < 1 || Number(entry.containerPort) > 65535)) throw new Error(`volter init: --needs ${path} needs version3 and a valid declared vendor port`);
380
+ return { name: entry.name, ...(apiSource ? goMysqlBindingFields(entry, entry.name, COMPOSE_INFRA.mysql!.containerPort) : goMysqlBinding(entry, document.version, entry.name, COMPOSE_INFRA.mysql!.containerPort)), ...(entry.containerPort !== undefined ? { containerPort: Number(entry.containerPort) } : {}), ...(typeof entry.scheme === 'string' && /^[a-z][a-z0-9+.-]*$/i.test(entry.scheme) ? { scheme: entry.scheme } : {}), ...(typeof entry.query === 'string' ? { query: entry.query } : {}), ...(entry.withPort === true ? { withPort: true as const } : {}) };
369
381
  }),
370
382
  components: (() => {
371
383
  if (need.components === undefined) return [];
372
- if ((document.version !== 2 && document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7) || !Array.isArray(need.components)) throw new Error(`volter init: --needs ${path} requires version 2,3,4,5,6 or7 for store components`);
384
+ if ((document.version !== 2 && document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7 && document.version !== 9) || !Array.isArray(need.components)) throw new Error(`volter init: --needs ${path} requires version 2,3,4,5,6 or7 for store components`);
373
385
  return need.components.map((part: unknown) => {
374
386
  if (!part || typeof part !== 'object' || Array.isArray(part)) throw new Error(`volter init: --needs ${path} has an invalid store component`);
375
387
  const field = part as { name?: unknown; component?: unknown };
@@ -381,7 +393,7 @@ function readStackNeeds(path: string): { composeFile: string; application?: stri
381
393
  ports: (Array.isArray(need.ports) ? need.ports as unknown[] : []).map((name) => identifier(String(name), `the port variables of service ${String(need.service)}`)),
382
394
  portTargets: (() => {
383
395
  if (need.portTargets === undefined) return {};
384
- if ((document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7) || !need.portTargets || typeof need.portTargets !== 'object' || Array.isArray(need.portTargets)) throw new Error(`volter init: --needs ${path} needs version3 for named vendor ports`);
396
+ if ((document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7 && document.version !== 9) || !need.portTargets || typeof need.portTargets !== 'object' || Array.isArray(need.portTargets)) throw new Error(`volter init: --needs ${path} needs version3 for named vendor ports`);
385
397
  return Object.fromEntries(Object.entries(need.portTargets).map(([name, value]) => {
386
398
  if (!Array.isArray(need.ports) || !need.ports.includes(name) || !Number.isInteger(value) || Number(value) < 1 || Number(value) > 65535) throw new Error(`volter init: --needs ${path} has an invalid named vendor port`);
387
399
  return [identifier(name, 'named vendor ports'), Number(value)];
@@ -448,8 +460,8 @@ overlayEndpointEnv(APP_READ_ENDPOINT_ENV);
448
460
  /** A twin service with its pack's credential door (`issues`) as the pack declares it now: a service kept from a saved
449
461
  * config gets the current door, so its names are issued by the door the pack has, never by a copy it has since fixed. */
450
462
  function withIssues(service: WorldServiceConfig): WorldServiceConfig {
451
- const door = service.type === 'twin' ? packFacts()[service.id]?.credentialDoor : undefined;
452
- return door === undefined ? service : { ...service, issues: door };
463
+ if ((service.type ?? 'twin') !== 'twin') return service;
464
+ return withCurrentCredentialDoor(service, packFacts()[service.id]?.credentialDoor);
453
465
  }
454
466
 
455
467
  function wiringFor(vendor: string): {
@@ -893,9 +905,20 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
893
905
  if (existing?.manifestVersion === 1) {
894
906
  throw new Error(`volter init: ${configPath} uses legacy format 1; run \`volter-world migrate-config ${configPath}\` before regenerating it`);
895
907
  }
896
- const selection: WorldSelection = options.vendors
908
+ if (options.managedApiSourceReader !== undefined && options.needs === undefined) throw new Error('volter init: an API source reader requires its explicit needs input');
909
+ const stack = options.needs ? readStackNeeds(resolve(options.needs), repo, options.managedApiSourceReader) : undefined;
910
+ const apiSource = stack?.apiSource;
911
+ const apiVendors = [...new Set(apiSource?.document.applicationApis.map(row => row.vendor) ?? [])];
912
+ const baseSelection: WorldSelection = options.vendors
897
913
  ? { include: options.vendors.map((vendor) => ({ vendor })), exclude: [] }
898
914
  : existing?.selection ?? DEFAULT_WORLD_SELECTION;
915
+ for (const vendor of apiVendors) {
916
+ if (options.vendors !== undefined && !options.vendors.includes(vendor)) throw new Error(`volter init: API vendor ${vendor} conflicts with the explicit twin selection`);
917
+ if (baseSelection.exclude.some(item => (item.vendor === undefined || item.vendor === vendor) && (item.usage === undefined || item.usage === 'application')))
918
+ throw new Error(`volter init: API vendor ${vendor} conflicts with the saved selection exclusion`);
919
+ }
920
+ // A source API choice participates in deliberate selection without dropping existing included vendors.
921
+ const selection: WorldSelection = apiSource ? { include: [...baseSelection.include, ...apiVendors.filter(vendor => !baseSelection.include.some(item => item.vendor === vendor && item.usage === 'application')).map(vendor => ({ vendor, usage: 'application' as const }))], exclude: [...baseSelection.exclude] } : baseSelection;
899
922
  const inRepo = isInside(repo, out);
900
923
  /** a path the config carries: relative to the world root when the config lives under it
901
924
  * (portable between teammates), absolute otherwise (a pilot emitted away from its root) */
@@ -910,8 +933,8 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
910
933
  // baked into the pack). Its handlers starter is the answering turns its own life scripts (journeys/scenario.json, the
911
934
  // World's handler format, its faults left out); its default data is src/semantics/seed.ts.
912
935
  const defaultsCopies: InitPlan['defaultsCopies'] = [];
913
- const stack = options.needs ? readStackNeeds(resolve(options.needs)) : undefined;
914
- const stackEnvironmentNames = stack ? [...new Set([...stack.settings.map(entry => entry.name), ...stack.unset])].sort() : [];
936
+ const apiNames = apiSource?.document.applicationApis.flatMap(row => [...row.endpoints, ...Object.keys(row.credentials)]) ?? [];
937
+ const stackEnvironmentNames = stack ? [...new Set([...stack.settings.map(entry => entry.name), ...stack.unset, ...apiNames])].sort() : [];
915
938
  const signals = options.vendors ? null : detectRepoVendors(repo, existing?.services, stackEnvironmentNames);
916
939
  const detected = options.vendors
917
940
  ? new Map<string, string[]>(options.vendors.map((v) => [v, ['--twins']]))
@@ -920,6 +943,8 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
920
943
  ? new Map<string, string[]>()
921
944
  : new Map([...signals!.unknown].filter(([vendor]) => (signals!.unknownUses.get(vendor) ?? []).some((use) => worldSelectionIncludes(selection, vendor, use.usage))));
922
945
 
946
+ for (const vendor of apiVendors) detected.set(vendor, [...(detected.get(vendor) ?? []), `--needs ${stack!.composeFile} service ${apiSource!.document.application.service} explicit ${vendor}/s3 API`]);
947
+
923
948
  const vendors: InitVendorPlan[] = [];
924
949
  const services: WorldServiceConfig[] = [];
925
950
  const existingIds = new Set(existing?.services.map((service) => service.id) ?? []);
@@ -1055,8 +1080,48 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
1055
1080
  });
1056
1081
  }
1057
1082
 
1083
+ // Resolve the actual selected catalog service, never the application service or a writer-invented id.
1084
+ const apiOwnerIds = new Map<string, string>();
1085
+ // Bind every source/execution field, including the extra Node loaders and environment used by startService.
1086
+ // Environment key order is documentary; preload and argv order change execution. Readiness can run a child too.
1087
+ const ownerExecution = (service: WorldServiceConfig): string => JSON.stringify([
1088
+ service.type ?? 'twin', service.package ?? null, service.version ?? null,
1089
+ service.command ?? null, service.args ?? [], service.cwd ?? null, service.rootArg ?? null, service.portArg ?? null,
1090
+ service.colocate?.module ?? null, service.colocate?.export ?? null, service.colocate?.scenarioPath ?? null,
1091
+ service.preload ?? [], Object.entries(service.env ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
1092
+ service.controlPlane ?? false, service.branchState ?? null, service.external ?? null,
1093
+ service.ready === undefined || service.ready === 'tcp' ? null : [
1094
+ service.ready.command ?? null, service.ready.args ?? [], service.ready.httpUrl ?? null,
1095
+ service.ready.timeoutMs ?? null, service.ready.intervalMs ?? null,
1096
+ ],
1097
+ ]);
1098
+ const apiOwnerExecutions = new Map<string, string>();
1099
+ for (const vendor of apiVendors) {
1100
+ const selected = vendors.find(row => row.vendor === vendor);
1101
+ if (!selected?.service || selected.service.type !== 'twin' || !catalog.has(vendor) || packFacts()[vendor]?.credentialDoor === undefined)
1102
+ throw new Error(`volter init: explicit ${vendor}/s3 API has no actual selected twin/current credential door`);
1103
+ const resolved = services.find(service => service.id === selected.service!.id);
1104
+ if (!resolved || ownerExecution(selected.service) !== ownerExecution(resolved)) throw new Error(`volter init: API owner ${selected.service.id} does not use its resolved catalog execution; revalidate the saved source`);
1105
+ apiOwnerIds.set(vendor, selected.service.id);
1106
+ apiOwnerExecutions.set(selected.service.id, ownerExecution(selected.service));
1107
+ }
1108
+ const apiOwners = (): ManagedApiOwner[] => apiVendors.map((vendor): ManagedApiOwner => ({ vendor, api: 's3', serviceId: apiOwnerIds.get(vendor)!, currentDoor: packFacts()[vendor]?.credentialDoor }));
1109
+ const composedServices = new Map<string, WorldServiceConfig>();
1110
+ const currentService = (service: WorldServiceConfig): WorldServiceConfig => composedServices.get(service.id) ?? withIssues(service);
1111
+ if (apiSource) {
1112
+ apiSource.assertCurrent();
1113
+ const base: WorldConfig = { ...existing, id: name, manifestVersion: 2, selection,
1114
+ env: { ...(existing?.env ?? {}) }, services: [...(existing?.services ?? []).map(withIssues), ...services.filter(service => !existingIds.has(service.id))] };
1115
+ const proposed = composeManagedApiNeeds9({ document: apiSource.document, world: worldConfigDocument(base),
1116
+ application: apiSource.application, owners: apiOwners() });
1117
+ for (const service of assertWorldConfig(proposed.manifest, 'needs9 issuer planning').services) if (apiOwnerExecutions.has(service.id)) composedServices.set(service.id, service);
1118
+ // Keep normal later bindings on the same owner object, and leave unrelated service objects untouched.
1119
+ for (let i = 0; i < services.length; i++) services[i] = composedServices.get(services[i]!.id) ?? services[i]!;
1120
+ for (const vendor of vendors) if (vendor.service) vendor.service = currentService(vendor.service);
1121
+ }
1122
+
1058
1123
  // --- env ------------------------------------------------------------------------------------
1059
- const env: Record<string, string> = {};
1124
+ const env: Record<string, string> = apiSource ? Object.create(null) : {};
1060
1125
  const envRows: InitEnvRow[] = [];
1061
1126
  const envSources: string[] = [];
1062
1127
  const bootRisks: InitBootRisk[] = [];
@@ -1125,7 +1190,7 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
1125
1190
  // is issued whatever its stem (Vercel's TEAM_ID_VERCEL, Upstash's KV_REST_API_TOKEN) once
1126
1191
  // that vendor's twin is in the World, and never when it is not.
1127
1192
  const issuedBy = new Map<string, string>();
1128
- for (const service of [...(existing?.services ?? []).map(withIssues), ...services.filter((service) => !existingIds.has(service.id))]) {
1193
+ for (const service of [...(existing?.services ?? []), ...services.filter((service) => !existingIds.has(service.id))].map(currentService)) {
1129
1194
  for (const name of Object.keys(service.issues?.fill ?? {})) if (!issuedBy.has(name)) issuedBy.set(name, service.id);
1130
1195
  }
1131
1196
  const standInStems = new Set<string>();
@@ -1533,7 +1598,14 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
1533
1598
  });
1534
1599
  }
1535
1600
 
1536
- const mergedServices = existing === undefined ? services : [...existing.services.map(withIssues), ...services.filter((service) => !existingIds.has(service.id))];
1601
+ // An inherited/bare explicit alias may not have entered generic env rows; it still has its exact owner.
1602
+ if (apiSource) for (const row of apiSource.document.applicationApis) for (const alias of Object.keys(row.credentials)) {
1603
+ const owner = apiOwnerIds.get(row.vendor)!;
1604
+ if (Object.hasOwn(env, alias) && env[alias] !== `${ISSUE_ENV_PREFIX}${owner}`) throw new Error(`volter init: explicit API alias ${alias} conflicts with its generated pending owner`);
1605
+ env[alias] = `${ISSUE_ENV_PREFIX}${owner}`;
1606
+ if (!envRows.some(entry => entry.name === alias)) envRows.push({ name: alias, disposition: 'issued', source: `${stack!.composeFile} service ${row.service} explicit API`, reason: `${owner} source-required scalar alias; up asks its current credential door` });
1607
+ }
1608
+ const mergedServices = (existing === undefined ? services : [...existing.services, ...services.filter((service) => !existingIds.has(service.id))]).map(currentService);
1537
1609
  if (existing !== undefined) {
1538
1610
  const owners = new Map<string, string>();
1539
1611
  for (const service of mergedServices) {
@@ -1560,7 +1632,7 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
1560
1632
  ...(Array.isArray(savedInfra) ? savedInfra : []),
1561
1633
  ...infra.filter((entry) => !savedInfraIds.has(entry.stub.id)).map((entry) => entry.stub),
1562
1634
  ];
1563
- const config: WorldConfig & { '//infra'?: unknown[]; catalog?: { sha: string } } = {
1635
+ let config: WorldConfig = {
1564
1636
  ...comments,
1565
1637
  id: name,
1566
1638
  manifestVersion: 2,
@@ -1595,7 +1667,13 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
1595
1667
  ...(mergedInfra.length ? { '//infra': mergedInfra } : {}),
1596
1668
  };
1597
1669
 
1598
- return {
1670
+ if (apiSource) {
1671
+ apiSource.assertCurrent();
1672
+ config = assertWorldConfig(composeManagedApiNeeds9({ document: apiSource.document, world: worldConfigDocument(config),
1673
+ application: apiSource.application, owners: apiOwners() }).manifest, 'needs9 final init planning');
1674
+ }
1675
+
1676
+ const plan: InitPlan = {
1599
1677
  name,
1600
1678
  repo,
1601
1679
  out,
@@ -1615,7 +1693,8 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
1615
1693
  ? { entryPath: resolve(out, 'seed.ts'), storyPath: resolve(out, 'seeds', 'story.ts') }
1616
1694
  : null,
1617
1695
  config,
1618
- env: config.env as Record<string, string>,
1696
+ env: config.env ?? {},
1697
+ ...(apiSource === undefined ? {} : { applicationApiSourceIdentity: apiSource.identity }),
1619
1698
  vendors,
1620
1699
  envRows,
1621
1700
  envSources,
@@ -1636,6 +1715,27 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
1636
1715
  .sort((a, b) => a.name.localeCompare(b.name)),
1637
1716
  mintedEnv: [...mintedEnv].sort(),
1638
1717
  };
1718
+ // Every emission starts from this planner's original object, never from a copied identity marker.
1719
+ initPlanChecks.set(plan, () => {});
1720
+ if (apiSource) {
1721
+ Object.defineProperty(plan, 'applicationApiSourceIdentity', { value: apiSource.identity, enumerable: true, writable: false, configurable: false });
1722
+ initPlanChecks.set(plan, () => {
1723
+ apiSource.assertCurrent();
1724
+ if (plan.applicationApiSourceIdentity !== apiSource.identity) throw new Error('needs9 plan lost its source comparison identity');
1725
+ for (const [id, execution] of apiOwnerExecutions) {
1726
+ const owner = plan.config.services.find(service => service.id === id);
1727
+ if (!owner || ownerExecution(owner) !== execution) throw new Error(`needs9 selected owner ${id} changed its catalog execution after planning`);
1728
+ }
1729
+ const expectedEnv = plan.config.env ?? {};
1730
+ if (Object.keys(plan.env).length !== Object.keys(expectedEnv).length || Object.keys(expectedEnv).some(key => !Object.hasOwn(plan.env, key) || plan.env[key] !== expectedEnv[key]))
1731
+ throw new Error('needs9 init environment preview differs from its planned World environment');
1732
+ const actual = worldConfigDocument(plan.config);
1733
+ const checked = composeManagedApiNeeds9({ document: apiSource.document, world: actual,
1734
+ application: apiSource.application, owners: apiOwners() }).manifest;
1735
+ if (JSON.stringify(checked) !== JSON.stringify(actual)) throw new Error('needs9 planned bindings differ from the current source/descriptor composition');
1736
+ });
1737
+ }
1738
+ return plan;
1639
1739
  }
1640
1740
 
1641
1741
  // ---------------------------------------------------------------------------------------------
@@ -1695,7 +1795,25 @@ browsers/
1695
1795
  keys.json
1696
1796
  `;
1697
1797
 
1798
+ function initConfigDocument(config: WorldConfig, path: string): ReturnType<typeof worldConfigDocument> {
1799
+ try {
1800
+ // Validate the saved shape before composition can replace an issuer or discard an unknown field.
1801
+ worldConfigDocument(config);
1802
+ return worldConfigDocument({ ...config, services: config.services.map(withIssues) });
1803
+ } catch (error) {
1804
+ throw new Error(`volter init: invalid credential/config carrier at ${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1805
+ }
1806
+ }
1807
+
1808
+ const initPlanChecks = new WeakMap<InitPlan, () => void>();
1809
+ function assertApiPlanCurrent(plan: InitPlan): void {
1810
+ const check = initPlanChecks.get(plan);
1811
+ if (!check) throw new Error('volter init: output requires the original live init plan; copied or serialized plans must be planned again');
1812
+ check();
1813
+ }
1814
+
1698
1815
  export function writeWorldInit(plan: InitPlan, options: { force?: boolean } = {}): void {
1816
+ assertApiPlanCurrent(plan);
1699
1817
  // A world already here is refused unless forced; a `.volter/` holding only running state
1700
1818
  // (worlds/, an env) is not a world and does not block init.
1701
1819
  if (existsSync(plan.configPath) && options.force !== true) {
@@ -1704,6 +1822,9 @@ export function writeWorldInit(plan: InitPlan, options: { force?: boolean } = {}
1704
1822
  + 'Pass --force to regenerate it, or --out <dir> to emit elsewhere.',
1705
1823
  );
1706
1824
  }
1825
+ // The complete carrier and current descriptor are checked before directories, snapshots or output writes.
1826
+ const document = initConfigDocument(plan.config, plan.configPath);
1827
+ const configBytes = `${JSON.stringify(document, null, 2)}\n`;
1707
1828
  // Recheck the planned name-derived paths before any output is written.
1708
1829
  for (const copy of plan.defaultsCopies) ownedPath(plan.out, relative(plan.out, copy.to));
1709
1830
  for (const service of plan.compose) if (service.nativeServerInputs)
@@ -1717,7 +1838,7 @@ export function writeWorldInit(plan: InitPlan, options: { force?: boolean } = {}
1717
1838
  if (!service.originalInputsFrom) throw new Error('Original inputs have no retained source parent');
1718
1839
  copyOriginalInputSnapshot(service.originalInputsFrom, service.originalInputs, plan.out);
1719
1840
  }
1720
- writeFileSync(plan.configPath, `${JSON.stringify(worldConfigDocument(plan.config), null, 2)}\n`);
1841
+ writeFileSync(plan.configPath, configBytes);
1721
1842
  if (plan.inRepo) writeFileSync(join(plan.out, '.gitignore'), STATE_GITIGNORE);
1722
1843
  writeFileSync(plan.envPath, renderEnvFile(plan));
1723
1844
  if (plan.composePath !== null) writeFileSync(plan.composePath, renderComposeFile(plan));
@@ -1842,7 +1963,8 @@ export function initWorld(name: string, repoPath: string, options: InitOptions =
1842
1963
  // The proof is kept in the config it was made against, beside the selection: the report printed below, as data.
1843
1964
  // Written after the proof because the proof reads the config's bytes; it reads nothing of `discovery.coverage`.
1844
1965
  plan.config.coverage = coverageRecord(coverage);
1845
- writeFileSync(plan.configPath, `${JSON.stringify(worldConfigDocument(plan.config), null, 2)}\n`);
1966
+ assertApiPlanCurrent(plan);
1967
+ writeFileSync(plan.configPath, `${JSON.stringify(initConfigDocument(plan.config, plan.configPath), null, 2)}\n`);
1846
1968
  const next = plan.inRepo && resolve(options.root ?? plan.repo) === plan.repo
1847
1969
  ? [
1848
1970
  'volter world up # starts the twins and loads the default data; the live env lands in .volter/world.env',