@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.
- package/dist/known-external-services.json +0 -1
- package/dist/src/actions-runner.js +53 -14
- package/dist/src/attach.d.ts +9 -0
- package/dist/src/attach.js +9 -2
- package/dist/src/cli-commands.js +19 -3
- package/dist/src/console-apart.js +3 -2
- package/dist/src/credential-door.d.ts +8 -0
- package/dist/src/credential-door.js +54 -0
- package/dist/src/fixture-env.js +2 -2
- package/dist/src/forwardable-headers.d.ts +1 -0
- package/dist/src/forwardable-headers.js +19 -0
- package/dist/src/go-mysql-binding.d.ts +2 -0
- package/dist/src/go-mysql-binding.js +7 -1
- package/dist/src/guest-runner.js +8 -4
- package/dist/src/host-ready.d.ts +7 -3
- package/dist/src/host-ready.js +16 -3
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.js +4 -1
- package/dist/src/infra.js +10 -5
- package/dist/src/init.d.ts +5 -0
- package/dist/src/init.js +157 -23
- package/dist/src/managed-api-needs.d.ts +101 -0
- package/dist/src/managed-api-needs.js +312 -0
- package/dist/src/managed-api-source.d.ts +24 -0
- package/dist/src/managed-api-source.js +59 -0
- package/dist/src/mongodb-backing.js +33 -58
- package/dist/src/mongodb-command.d.ts +21 -0
- package/dist/src/mongodb-command.js +317 -0
- package/dist/src/pglite-host.mjs +2 -2
- package/dist/src/placeholder.d.ts +24 -1
- package/dist/src/placeholder.js +105 -13
- package/dist/src/process-signal.d.mts +2 -0
- package/dist/src/process-signal.mjs +44 -2
- package/dist/src/project-inspect.d.ts +1 -1
- package/dist/src/project-inspect.js +8 -1
- package/dist/src/redirect-proxy.js +4 -19
- package/dist/src/reflect.js +2 -2
- package/dist/src/runtime.d.ts +5 -1
- package/dist/src/runtime.js +309 -58
- package/dist/src/schema.d.ts +10 -0
- package/dist/src/schema.js +76 -3
- package/dist/src/serve.js +24 -4
- package/dist/src/served-world.d.ts +2 -1
- package/dist/src/served-world.js +6 -4
- package/dist/src/service-command.d.ts +7 -3
- package/dist/src/service-command.js +53 -22
- package/dist/src/service-environment.d.ts +4 -0
- package/dist/src/service-environment.js +21 -0
- package/dist/src/service-recorder.js +9 -13
- package/known-external-services.json +0 -1
- package/package.json +5 -5
- package/src/actions-runner.ts +38 -10
- package/src/attach.ts +15 -2
- package/src/cli-commands.ts +18 -3
- package/src/console-apart.ts +3 -2
- package/src/credential-door.ts +54 -0
- package/src/fixture-env.ts +2 -2
- package/src/forwardable-headers.ts +15 -0
- package/src/go-mysql-binding.ts +8 -1
- package/src/guest-runner.ts +8 -4
- package/src/host-ready.ts +12 -3
- package/src/index.ts +5 -1
- package/src/infra.ts +8 -3
- package/src/init.ts +145 -23
- package/src/managed-api-needs.ts +285 -0
- package/src/managed-api-source.ts +67 -0
- package/src/mongodb-backing.ts +32 -48
- package/src/mongodb-command.ts +238 -0
- package/src/pglite-host.mjs +2 -2
- package/src/placeholder.ts +90 -13
- package/src/process-signal.d.mts +2 -0
- package/src/process-signal.mjs +44 -2
- package/src/project-inspect.ts +9 -2
- package/src/redirect-proxy.ts +4 -15
- package/src/reflect.ts +2 -2
- package/src/runtime.ts +270 -44
- package/src/schema.ts +73 -3
- package/src/serve.ts +22 -4
- package/src/served-world.ts +7 -5
- package/src/service-command.ts +41 -19
- package/src/service-environment.ts +21 -0
- 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
|
-
|
|
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
|
-
|
|
452
|
-
return
|
|
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
|
-
|
|
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
|
|
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 ?? [])
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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',
|