@volter/world-runtime 3.0.165 → 3.0.167

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 +5 -1
  18. package/dist/src/index.js +4 -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 +309 -58
  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 +5 -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 +270 -44
  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
@@ -1,3 +1,4 @@
1
+ import { type ManagedApiSourceReader } from './managed-api-source.js';
1
2
  import { type NativeServerInputs } from './native-server-inputs.js';
2
3
  import { type GoMysqlBinding } from './go-mysql-binding.js';
3
4
  import { type OriginalInputReference } from './original-inputs.js';
@@ -147,6 +148,8 @@ export type InitPlan = {
147
148
  stackNeeds?: InitStackNeed[];
148
149
  /** The same declared application names used by selection and its coverage proof. */
149
150
  stackEnvironmentNames?: string[];
151
+ /** Equality/cache identity from the live trusted reader; this digest grants no source custody. */
152
+ readonly applicationApiSourceIdentity?: string;
150
153
  /** the project files a World made for an image left unread: the source does not hold them, so they are the person's own */
151
154
  leftAsThePersonsOwn?: string[];
152
155
  /** the services emitted into the private managed-infrastructure definition */
@@ -228,6 +231,8 @@ export type InitOptions = {
228
231
  bare?: string;
229
232
  /** The project's own stack as its image builder read it (`needs.json`): architecture, "The project's own stack as evidence". */
230
233
  needs?: string;
234
+ /** Reviewed host attachment to immutable source/correspondence. No CLI flag or JSON bit selects it. */
235
+ managedApiSourceReader?: ManagedApiSourceReader;
231
236
  };
232
237
  /** One service of the project's stack, as `init` took it. */
233
238
  export type InitStackNeed = {
package/dist/src/init.js CHANGED
@@ -1,5 +1,8 @@
1
+ import { withCurrentCredentialDoor } from "./credential-door.js";
2
+ import { composeManagedApiNeeds9 } from "./managed-api-needs.js";
3
+ import { readManagedApiSourceSession } from "./managed-api-source.js";
1
4
  import { copyNativeServerInputs, readNativeServerInputs, nativeServerRuntimeReference } from "./native-server-inputs.js";
2
- import { goMysqlBinding, renderGoMysqlBinding } from "./go-mysql-binding.js";
5
+ import { goMysqlBinding, goMysqlBindingFields, renderGoMysqlBinding } from "./go-mysql-binding.js";
3
6
  import * as managedOriginalInputs from '@volter/world-core/managed-original-inputs';
4
7
  import { copyOriginalInputSnapshot, readOriginalInputSnapshot } from "./original-inputs.js";
5
8
  // `volter-world init <name> --repo <path>` — the DETERMINISTIC FRONT DOOR of world adoption.
@@ -48,7 +51,7 @@ import { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from "./catalog-in
48
51
  import { filesLeftAsThePersonsOwn, projectEnvReads, projectManifestDirs, projectMigrationTools, readOnlyWhatTheSourceHolds, sourceText } from "./project-inspect.js";
49
52
  import { overlayEndpointEnv, packFacts } from "./pack-facts.js";
50
53
  import { VENDOR_SERVER_BACKINGS } from "./backings.js";
51
- import { APP_URL_ENV, DEFAULT_WORLD_SELECTION, ISSUE_ENV_PREFIX, MINT_ENV, worldConfigDocument, worldSelectionIncludes } from "./schema.js";
54
+ import { APP_URL_ENV, DEFAULT_WORLD_SELECTION, ISSUE_ENV_PREFIX, MINT_ENV, assertWorldConfig, worldConfigDocument, worldSelectionIncludes } from "./schema.js";
52
55
  import { loadWorldConfig } from "./configs.js";
53
56
  import { parseInfraDefinition } from "./pglite-backing.js";
54
57
  import { ownedPath } from "./owned-path.js";
@@ -93,7 +96,7 @@ const APP_ORIGIN_ENV = /^(?:NEXT_PUBLIC_)?(?:APP_)?(?:BASE_)?URL$|^NEXT_PUBLIC_A
93
96
  const FAMILY_KIND = { postgres: 'postgres', mysql: 'mysql', mongodb: 'mongodb', redis: 'redis', search: 'search', clickhouse: 'clickhouse', memcached: 'memcached' };
94
97
  const CONVENTIONAL_NAMES = { postgres: ['DATABASE_URL'], mysql: ['DATABASE_URL'], mongodb: ['MONGODB_URI'], redis: ['REDIS_URL'], clickhouse: ['CLICKHOUSE_URL'] };
95
98
  /** The needs file, checked for its kind: a file of another shape is refused by name, never half-read. */
96
- function readStackNeeds(path) {
99
+ function readStackNeeds(path, repo, apiReader) {
97
100
  let document;
98
101
  try {
99
102
  document = JSON.parse(readFileSync(path, 'utf8'));
@@ -101,7 +104,13 @@ function readStackNeeds(path) {
101
104
  catch (error) {
102
105
  throw new Error(`volter init: --needs ${path} could not be read as JSON: ${error instanceof Error ? error.message : String(error)}`);
103
106
  }
104
- 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)) {
107
+ // A plain CLI has no trusted function and retains its explicit version9 refusal.
108
+ const apiSource = document.version === 9 ? readManagedApiSourceSession(repo, path, apiReader) : undefined;
109
+ if (apiReader !== undefined && apiSource === undefined)
110
+ throw new Error('volter init: an API source reader cannot be ignored by a non-version9 input');
111
+ if (apiSource)
112
+ document = apiSource.document;
113
+ 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)) {
105
114
  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)`);
106
115
  }
107
116
  const identifier = (name, what) => {
@@ -111,6 +120,7 @@ function readStackNeeds(path) {
111
120
  };
112
121
  const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? Object.entries(value) : [];
113
122
  return {
123
+ ...(apiSource === undefined ? {} : { apiSource }),
114
124
  composeFile: typeof document.composeFile === 'string' ? document.composeFile : 'the project\'s Compose file',
115
125
  ...(typeof document.application?.service === 'string' ? { application: document.application.service } : {}),
116
126
  // The application's own variables that name no service: its settings as the stack writes them, and those whose
@@ -120,7 +130,7 @@ function readStackNeeds(path) {
120
130
  needs: document.needs.filter((need) => typeof need.service === 'string').map((need) => ({
121
131
  service: need.service,
122
132
  ...(need.originalInputs === undefined ? {} : { originalInputs: (() => {
123
- if (document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7)
133
+ if (document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7 && document.version !== 9)
124
134
  throw new Error('volter init: original inputs require needs version4,5,6 or7');
125
135
  const reference = managedOriginalInputs.referenceOf(need.originalInputs);
126
136
  const source = readOriginalInputSnapshot(dirname(resolve(path)), reference).snapshot.manifest;
@@ -130,7 +140,7 @@ function readStackNeeds(path) {
130
140
  return reference;
131
141
  })() }),
132
142
  ...(need.nativeServerInputs === undefined ? {} : (() => {
133
- if (document.version !== 5 && document.version !== 7)
143
+ if (document.version !== 5 && document.version !== 7 && document.version !== 9)
134
144
  throw new Error('volter init: native managed input requires needs5 or7');
135
145
  const checked = readNativeServerInputs(dirname(resolve(path)), need.nativeServerInputs);
136
146
  const source = JSON.parse(checked.files.get('declaration.json').toString('utf8'));
@@ -145,14 +155,14 @@ function readStackNeeds(path) {
145
155
  // else (a newline, an `=`) would write lines of its own there.
146
156
  if (typeof entry.name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(entry.name))
147
157
  throw new Error(`volter init: --needs ${path} names a variable that is not an identifier under service ${String(need.service)}: ${JSON.stringify(entry.name)}`);
148
- 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))
158
+ 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))
149
159
  throw new Error(`volter init: --needs ${path} needs version3 and a valid declared vendor port`);
150
- 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 } : {}) };
160
+ 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 } : {}) };
151
161
  }),
152
162
  components: (() => {
153
163
  if (need.components === undefined)
154
164
  return [];
155
- if ((document.version !== 2 && document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7) || !Array.isArray(need.components))
165
+ 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))
156
166
  throw new Error(`volter init: --needs ${path} requires version 2,3,4,5,6 or7 for store components`);
157
167
  return need.components.map((part) => {
158
168
  if (!part || typeof part !== 'object' || Array.isArray(part))
@@ -168,7 +178,7 @@ function readStackNeeds(path) {
168
178
  portTargets: (() => {
169
179
  if (need.portTargets === undefined)
170
180
  return {};
171
- 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))
181
+ 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))
172
182
  throw new Error(`volter init: --needs ${path} needs version3 for named vendor ports`);
173
183
  return Object.fromEntries(Object.entries(need.portTargets).map(([name, value]) => {
174
184
  if (!Array.isArray(need.ports) || !need.ports.includes(name) || !Number.isInteger(value) || Number(value) < 1 || Number(value) > 65535)
@@ -231,8 +241,9 @@ overlayEndpointEnv(APP_READ_ENDPOINT_ENV);
231
241
  /** A twin service with its pack's credential door (`issues`) as the pack declares it now: a service kept from a saved
232
242
  * config gets the current door, so its names are issued by the door the pack has, never by a copy it has since fixed. */
233
243
  function withIssues(service) {
234
- const door = service.type === 'twin' ? packFacts()[service.id]?.credentialDoor : undefined;
235
- return door === undefined ? service : { ...service, issues: door };
244
+ if ((service.type ?? 'twin') !== 'twin')
245
+ return service;
246
+ return withCurrentCredentialDoor(service, packFacts()[service.id]?.credentialDoor);
236
247
  }
237
248
  function wiringFor(vendor) {
238
249
  // RULE 2: ask the injector, never guess. `aws` yields s3/secretsmanager here — the very keys
@@ -671,9 +682,22 @@ export function planWorldInit(name, repoPath, options = {}) {
671
682
  if (existing?.manifestVersion === 1) {
672
683
  throw new Error(`volter init: ${configPath} uses legacy format 1; run \`volter-world migrate-config ${configPath}\` before regenerating it`);
673
684
  }
674
- const selection = options.vendors
685
+ if (options.managedApiSourceReader !== undefined && options.needs === undefined)
686
+ throw new Error('volter init: an API source reader requires its explicit needs input');
687
+ const stack = options.needs ? readStackNeeds(resolve(options.needs), repo, options.managedApiSourceReader) : undefined;
688
+ const apiSource = stack?.apiSource;
689
+ const apiVendors = [...new Set(apiSource?.document.applicationApis.map(row => row.vendor) ?? [])];
690
+ const baseSelection = options.vendors
675
691
  ? { include: options.vendors.map((vendor) => ({ vendor })), exclude: [] }
676
692
  : existing?.selection ?? DEFAULT_WORLD_SELECTION;
693
+ for (const vendor of apiVendors) {
694
+ if (options.vendors !== undefined && !options.vendors.includes(vendor))
695
+ throw new Error(`volter init: API vendor ${vendor} conflicts with the explicit twin selection`);
696
+ if (baseSelection.exclude.some(item => (item.vendor === undefined || item.vendor === vendor) && (item.usage === undefined || item.usage === 'application')))
697
+ throw new Error(`volter init: API vendor ${vendor} conflicts with the saved selection exclusion`);
698
+ }
699
+ // A source API choice participates in deliberate selection without dropping existing included vendors.
700
+ const selection = apiSource ? { include: [...baseSelection.include, ...apiVendors.filter(vendor => !baseSelection.include.some(item => item.vendor === vendor && item.usage === 'application')).map(vendor => ({ vendor, usage: 'application' }))], exclude: [...baseSelection.exclude] } : baseSelection;
677
701
  const inRepo = isInside(repo, out);
678
702
  /** a path the config carries: relative to the world root when the config lives under it
679
703
  * (portable between teammates), absolute otherwise (a pilot emitted away from its root) */
@@ -687,8 +711,8 @@ export function planWorldInit(name, repoPath, options = {}) {
687
711
  // baked into the pack). Its handlers starter is the answering turns its own life scripts (journeys/scenario.json, the
688
712
  // World's handler format, its faults left out); its default data is src/semantics/seed.ts.
689
713
  const defaultsCopies = [];
690
- const stack = options.needs ? readStackNeeds(resolve(options.needs)) : undefined;
691
- const stackEnvironmentNames = stack ? [...new Set([...stack.settings.map(entry => entry.name), ...stack.unset])].sort() : [];
714
+ const apiNames = apiSource?.document.applicationApis.flatMap(row => [...row.endpoints, ...Object.keys(row.credentials)]) ?? [];
715
+ const stackEnvironmentNames = stack ? [...new Set([...stack.settings.map(entry => entry.name), ...stack.unset, ...apiNames])].sort() : [];
692
716
  const signals = options.vendors ? null : detectRepoVendors(repo, existing?.services, stackEnvironmentNames);
693
717
  const detected = options.vendors
694
718
  ? new Map(options.vendors.map((v) => [v, ['--twins']]))
@@ -696,6 +720,8 @@ export function planWorldInit(name, repoPath, options = {}) {
696
720
  const unknown = options.vendors
697
721
  ? new Map()
698
722
  : new Map([...signals.unknown].filter(([vendor]) => (signals.unknownUses.get(vendor) ?? []).some((use) => worldSelectionIncludes(selection, vendor, use.usage))));
723
+ for (const vendor of apiVendors)
724
+ detected.set(vendor, [...(detected.get(vendor) ?? []), `--needs ${stack.composeFile} service ${apiSource.document.application.service} explicit ${vendor}/s3 API`]);
699
725
  const vendors = [];
700
726
  const services = [];
701
727
  const existingIds = new Set(existing?.services.map((service) => service.id) ?? []);
@@ -841,8 +867,53 @@ export function planWorldInit(name, repoPath, options = {}) {
841
867
  source: { kind: catalogSource.kindOf(vendor), ...(catalogSource.version(vendor) !== undefined ? { version: catalogSource.version(vendor) } : {}) },
842
868
  });
843
869
  }
870
+ // Resolve the actual selected catalog service, never the application service or a writer-invented id.
871
+ const apiOwnerIds = new Map();
872
+ // Bind every source/execution field, including the extra Node loaders and environment used by startService.
873
+ // Environment key order is documentary; preload and argv order change execution. Readiness can run a child too.
874
+ const ownerExecution = (service) => JSON.stringify([
875
+ service.type ?? 'twin', service.package ?? null, service.version ?? null,
876
+ service.command ?? null, service.args ?? [], service.cwd ?? null, service.rootArg ?? null, service.portArg ?? null,
877
+ service.colocate?.module ?? null, service.colocate?.export ?? null, service.colocate?.scenarioPath ?? null,
878
+ service.preload ?? [], Object.entries(service.env ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
879
+ service.controlPlane ?? false, service.branchState ?? null, service.external ?? null,
880
+ service.ready === undefined || service.ready === 'tcp' ? null : [
881
+ service.ready.command ?? null, service.ready.args ?? [], service.ready.httpUrl ?? null,
882
+ service.ready.timeoutMs ?? null, service.ready.intervalMs ?? null,
883
+ ],
884
+ ]);
885
+ const apiOwnerExecutions = new Map();
886
+ for (const vendor of apiVendors) {
887
+ const selected = vendors.find(row => row.vendor === vendor);
888
+ if (!selected?.service || selected.service.type !== 'twin' || !catalog.has(vendor) || packFacts()[vendor]?.credentialDoor === undefined)
889
+ throw new Error(`volter init: explicit ${vendor}/s3 API has no actual selected twin/current credential door`);
890
+ const resolved = services.find(service => service.id === selected.service.id);
891
+ if (!resolved || ownerExecution(selected.service) !== ownerExecution(resolved))
892
+ throw new Error(`volter init: API owner ${selected.service.id} does not use its resolved catalog execution; revalidate the saved source`);
893
+ apiOwnerIds.set(vendor, selected.service.id);
894
+ apiOwnerExecutions.set(selected.service.id, ownerExecution(selected.service));
895
+ }
896
+ const apiOwners = () => apiVendors.map((vendor) => ({ vendor, api: 's3', serviceId: apiOwnerIds.get(vendor), currentDoor: packFacts()[vendor]?.credentialDoor }));
897
+ const composedServices = new Map();
898
+ const currentService = (service) => composedServices.get(service.id) ?? withIssues(service);
899
+ if (apiSource) {
900
+ apiSource.assertCurrent();
901
+ const base = { ...existing, id: name, manifestVersion: 2, selection,
902
+ env: { ...(existing?.env ?? {}) }, services: [...(existing?.services ?? []).map(withIssues), ...services.filter(service => !existingIds.has(service.id))] };
903
+ const proposed = composeManagedApiNeeds9({ document: apiSource.document, world: worldConfigDocument(base),
904
+ application: apiSource.application, owners: apiOwners() });
905
+ for (const service of assertWorldConfig(proposed.manifest, 'needs9 issuer planning').services)
906
+ if (apiOwnerExecutions.has(service.id))
907
+ composedServices.set(service.id, service);
908
+ // Keep normal later bindings on the same owner object, and leave unrelated service objects untouched.
909
+ for (let i = 0; i < services.length; i++)
910
+ services[i] = composedServices.get(services[i].id) ?? services[i];
911
+ for (const vendor of vendors)
912
+ if (vendor.service)
913
+ vendor.service = currentService(vendor.service);
914
+ }
844
915
  // --- env ------------------------------------------------------------------------------------
845
- const env = {};
916
+ const env = apiSource ? Object.create(null) : {};
846
917
  const envRows = [];
847
918
  const envSources = [];
848
919
  const bootRisks = [];
@@ -925,7 +996,7 @@ export function planWorldInit(name, repoPath, options = {}) {
925
996
  // is issued whatever its stem (Vercel's TEAM_ID_VERCEL, Upstash's KV_REST_API_TOKEN) once
926
997
  // that vendor's twin is in the World, and never when it is not.
927
998
  const issuedBy = new Map();
928
- for (const service of [...(existing?.services ?? []).map(withIssues), ...services.filter((service) => !existingIds.has(service.id))]) {
999
+ for (const service of [...(existing?.services ?? []), ...services.filter((service) => !existingIds.has(service.id))].map(currentService)) {
929
1000
  for (const name of Object.keys(service.issues?.fill ?? {}))
930
1001
  if (!issuedBy.has(name))
931
1002
  issuedBy.set(name, service.id);
@@ -1364,7 +1435,18 @@ export function planWorldInit(name, repoPath, options = {}) {
1364
1435
  },
1365
1436
  });
1366
1437
  }
1367
- const mergedServices = existing === undefined ? services : [...existing.services.map(withIssues), ...services.filter((service) => !existingIds.has(service.id))];
1438
+ // An inherited/bare explicit alias may not have entered generic env rows; it still has its exact owner.
1439
+ if (apiSource)
1440
+ for (const row of apiSource.document.applicationApis)
1441
+ for (const alias of Object.keys(row.credentials)) {
1442
+ const owner = apiOwnerIds.get(row.vendor);
1443
+ if (Object.hasOwn(env, alias) && env[alias] !== `${ISSUE_ENV_PREFIX}${owner}`)
1444
+ throw new Error(`volter init: explicit API alias ${alias} conflicts with its generated pending owner`);
1445
+ env[alias] = `${ISSUE_ENV_PREFIX}${owner}`;
1446
+ if (!envRows.some(entry => entry.name === alias))
1447
+ 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` });
1448
+ }
1449
+ const mergedServices = (existing === undefined ? services : [...existing.services, ...services.filter((service) => !existingIds.has(service.id))]).map(currentService);
1368
1450
  if (existing !== undefined) {
1369
1451
  const owners = new Map();
1370
1452
  for (const service of mergedServices) {
@@ -1391,7 +1473,7 @@ export function planWorldInit(name, repoPath, options = {}) {
1391
1473
  ...(Array.isArray(savedInfra) ? savedInfra : []),
1392
1474
  ...infra.filter((entry) => !savedInfraIds.has(entry.stub.id)).map((entry) => entry.stub),
1393
1475
  ];
1394
- const config = {
1476
+ let config = {
1395
1477
  ...comments,
1396
1478
  id: name,
1397
1479
  manifestVersion: 2,
@@ -1425,7 +1507,12 @@ export function planWorldInit(name, repoPath, options = {}) {
1425
1507
  ...(existing?.fixtures ? { fixtures: existing.fixtures } : {}),
1426
1508
  ...(mergedInfra.length ? { '//infra': mergedInfra } : {}),
1427
1509
  };
1428
- return {
1510
+ if (apiSource) {
1511
+ apiSource.assertCurrent();
1512
+ config = assertWorldConfig(composeManagedApiNeeds9({ document: apiSource.document, world: worldConfigDocument(config),
1513
+ application: apiSource.application, owners: apiOwners() }).manifest, 'needs9 final init planning');
1514
+ }
1515
+ const plan = {
1429
1516
  name,
1430
1517
  repo,
1431
1518
  out,
@@ -1445,7 +1532,8 @@ export function planWorldInit(name, repoPath, options = {}) {
1445
1532
  ? { entryPath: resolve(out, 'seed.ts'), storyPath: resolve(out, 'seeds', 'story.ts') }
1446
1533
  : null,
1447
1534
  config,
1448
- env: config.env,
1535
+ env: config.env ?? {},
1536
+ ...(apiSource === undefined ? {} : { applicationApiSourceIdentity: apiSource.identity }),
1449
1537
  vendors,
1450
1538
  envRows,
1451
1539
  envSources,
@@ -1466,6 +1554,30 @@ export function planWorldInit(name, repoPath, options = {}) {
1466
1554
  .sort((a, b) => a.name.localeCompare(b.name)),
1467
1555
  mintedEnv: [...mintedEnv].sort(),
1468
1556
  };
1557
+ // Every emission starts from this planner's original object, never from a copied identity marker.
1558
+ initPlanChecks.set(plan, () => { });
1559
+ if (apiSource) {
1560
+ Object.defineProperty(plan, 'applicationApiSourceIdentity', { value: apiSource.identity, enumerable: true, writable: false, configurable: false });
1561
+ initPlanChecks.set(plan, () => {
1562
+ apiSource.assertCurrent();
1563
+ if (plan.applicationApiSourceIdentity !== apiSource.identity)
1564
+ throw new Error('needs9 plan lost its source comparison identity');
1565
+ for (const [id, execution] of apiOwnerExecutions) {
1566
+ const owner = plan.config.services.find(service => service.id === id);
1567
+ if (!owner || ownerExecution(owner) !== execution)
1568
+ throw new Error(`needs9 selected owner ${id} changed its catalog execution after planning`);
1569
+ }
1570
+ const expectedEnv = plan.config.env ?? {};
1571
+ 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]))
1572
+ throw new Error('needs9 init environment preview differs from its planned World environment');
1573
+ const actual = worldConfigDocument(plan.config);
1574
+ const checked = composeManagedApiNeeds9({ document: apiSource.document, world: actual,
1575
+ application: apiSource.application, owners: apiOwners() }).manifest;
1576
+ if (JSON.stringify(checked) !== JSON.stringify(actual))
1577
+ throw new Error('needs9 planned bindings differ from the current source/descriptor composition');
1578
+ });
1579
+ }
1580
+ return plan;
1469
1581
  }
1470
1582
  // ---------------------------------------------------------------------------------------------
1471
1583
  // Emission
@@ -1523,13 +1635,34 @@ pages-sessions.json
1523
1635
  browsers/
1524
1636
  keys.json
1525
1637
  `;
1638
+ function initConfigDocument(config, path) {
1639
+ try {
1640
+ // Validate the saved shape before composition can replace an issuer or discard an unknown field.
1641
+ worldConfigDocument(config);
1642
+ return worldConfigDocument({ ...config, services: config.services.map(withIssues) });
1643
+ }
1644
+ catch (error) {
1645
+ throw new Error(`volter init: invalid credential/config carrier at ${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1646
+ }
1647
+ }
1648
+ const initPlanChecks = new WeakMap();
1649
+ function assertApiPlanCurrent(plan) {
1650
+ const check = initPlanChecks.get(plan);
1651
+ if (!check)
1652
+ throw new Error('volter init: output requires the original live init plan; copied or serialized plans must be planned again');
1653
+ check();
1654
+ }
1526
1655
  export function writeWorldInit(plan, options = {}) {
1656
+ assertApiPlanCurrent(plan);
1527
1657
  // A world already here is refused unless forced; a `.volter/` holding only running state
1528
1658
  // (worlds/, an env) is not a world and does not block init.
1529
1659
  if (existsSync(plan.configPath) && options.force !== true) {
1530
1660
  throw new Error(`volter init: a world already exists at ${plan.configPath}. `
1531
1661
  + 'Pass --force to regenerate it, or --out <dir> to emit elsewhere.');
1532
1662
  }
1663
+ // The complete carrier and current descriptor are checked before directories, snapshots or output writes.
1664
+ const document = initConfigDocument(plan.config, plan.configPath);
1665
+ const configBytes = `${JSON.stringify(document, null, 2)}\n`;
1533
1666
  // Recheck the planned name-derived paths before any output is written.
1534
1667
  for (const copy of plan.defaultsCopies)
1535
1668
  ownedPath(plan.out, relative(plan.out, copy.to));
@@ -1549,7 +1682,7 @@ export function writeWorldInit(plan, options = {}) {
1549
1682
  throw new Error('Original inputs have no retained source parent');
1550
1683
  copyOriginalInputSnapshot(service.originalInputsFrom, service.originalInputs, plan.out);
1551
1684
  }
1552
- writeFileSync(plan.configPath, `${JSON.stringify(worldConfigDocument(plan.config), null, 2)}\n`);
1685
+ writeFileSync(plan.configPath, configBytes);
1553
1686
  if (plan.inRepo)
1554
1687
  writeFileSync(join(plan.out, '.gitignore'), STATE_GITIGNORE);
1555
1688
  writeFileSync(plan.envPath, renderEnvFile(plan));
@@ -1674,7 +1807,8 @@ export function initWorld(name, repoPath, options = {}) {
1674
1807
  // The proof is kept in the config it was made against, beside the selection: the report printed below, as data.
1675
1808
  // Written after the proof because the proof reads the config's bytes; it reads nothing of `discovery.coverage`.
1676
1809
  plan.config.coverage = coverageRecord(coverage);
1677
- writeFileSync(plan.configPath, `${JSON.stringify(worldConfigDocument(plan.config), null, 2)}\n`);
1810
+ assertApiPlanCurrent(plan);
1811
+ writeFileSync(plan.configPath, `${JSON.stringify(initConfigDocument(plan.config, plan.configPath), null, 2)}\n`);
1678
1812
  const next = plan.inRepo && resolve(options.root ?? plan.repo) === plan.repo
1679
1813
  ? [
1680
1814
  'volter world up # starts the twins and loads the default data; the live env lands in .volter/world.env',
@@ -0,0 +1,101 @@
1
+ /** Needs9 metadata only. Plain CLI init without a live trusted source reader still refuses version9. No I/O, issuer or runtime attachment here. */
2
+ import { type OriginalInputReference } from '@volter/world-core/managed-original-inputs';
3
+ import { type CredentialDoor } from './credential-door.js';
4
+ import { type GoMysqlBinding } from './go-mysql-binding.js';
5
+ import { type NativeServerInputs } from './native-server-inputs.js';
6
+ import { type WorldManifestV2 } from './schema.js';
7
+ declare const APPLICATION_REASONS: readonly ["the declared application service", "builds this Dockerfile", "the service the stack builds", "runs the project's own image", "the stack's only service runs an image", "the one service that publishes a port with an image of no family"];
8
+ type ApplicationReason = typeof APPLICATION_REASONS[number];
9
+ export type ManagedApiNeed = {
10
+ service: string;
11
+ vendor: 'aws';
12
+ api: 's3';
13
+ endpoints: string[];
14
+ credentials: Record<string, string>;
15
+ cliRedirect: Record<string, '${url}'>;
16
+ issues: CredentialDoor & {
17
+ required: string[];
18
+ };
19
+ };
20
+ export type ManagedApiInfrastructureNeed = {
21
+ service: string;
22
+ image?: string;
23
+ builds?: string;
24
+ family?: string;
25
+ supplier: 'a World' | 'named by a catalog family' | 'nobody' | "the project's own build";
26
+ names: Array<{
27
+ name: string;
28
+ scheme?: string;
29
+ query?: string;
30
+ withPort?: true;
31
+ containerPort?: number;
32
+ } & Partial<GoMysqlBinding>>;
33
+ ports?: string[];
34
+ portTargets?: Record<string, number>;
35
+ components?: Array<{
36
+ name: string;
37
+ component: 'database' | 'user' | 'password';
38
+ }>;
39
+ originalInputs?: OriginalInputReference;
40
+ nativeServerInputs?: NativeServerInputs;
41
+ };
42
+ export type ManagedApiNeeds9 = {
43
+ kind: 'browser-substrate.stack-needs';
44
+ version: 9;
45
+ dockerfile: string;
46
+ target?: string;
47
+ composeFile: string;
48
+ application: {
49
+ service: string;
50
+ told: ApplicationReason;
51
+ image?: string;
52
+ target?: string;
53
+ notInTheImage: string[];
54
+ envFiles: string[];
55
+ };
56
+ needs: ManagedApiInfrastructureNeed[];
57
+ settings: Record<string, string>;
58
+ unset: Record<string, string[]>;
59
+ bare?: string[];
60
+ notTold?: string;
61
+ leftOut?: Array<{
62
+ what: string;
63
+ kind: string;
64
+ why: string;
65
+ wouldHaveBeen: string;
66
+ service?: string;
67
+ pointing?: Array<{
68
+ service: string;
69
+ variable: string;
70
+ }>;
71
+ }>;
72
+ said: string[];
73
+ applicationApis: ManagedApiNeed[];
74
+ };
75
+ /** Strict whole metadata shape, preserving common fields. References are metadata, not authenticated files. */
76
+ export declare function readManagedApiNeeds9(value: unknown): ManagedApiNeeds9;
77
+ export type ManagedApiOwner = {
78
+ vendor: 'aws';
79
+ api: 's3';
80
+ serviceId: string;
81
+ currentDoor: unknown;
82
+ };
83
+ export type ManagedApiApplication = {
84
+ service: string;
85
+ environment: Readonly<Record<string, string>>;
86
+ inheritedNames: readonly string[];
87
+ };
88
+ /** Data from a trusted caller's actual source reading; this validates shape, not custody. */
89
+ export declare function readManagedApiApplication(value: unknown): ManagedApiApplication;
90
+ /** Caller prerequisites: authenticated immutable source/full correspondence, supplied package/current descriptor,
91
+ * and deliberate owner selection. This metadata composition neither proves those prerequisites nor writes a World. */
92
+ export declare function composeManagedApiNeeds9(input: {
93
+ document: unknown;
94
+ world: unknown;
95
+ application: ManagedApiApplication;
96
+ owners: readonly ManagedApiOwner[];
97
+ }): {
98
+ needs: ManagedApiNeeds9;
99
+ manifest: WorldManifestV2 & Record<string, unknown>;
100
+ };
101
+ export {};