@volter/world-runtime 3.0.166 → 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/dist/src/init.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
|
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 ?? [])
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 {};
|