@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
|
@@ -1017,7 +1017,6 @@
|
|
|
1017
1017
|
"organizationlicense": "generic app-internal env stem, names no vendor",
|
|
1018
1018
|
"osso": "defunct SAML SaaS (Osso), no live vendor surface",
|
|
1019
1019
|
"postgrest": "self-instance client; targets the app's own self-hosted deployment, which is local in a keyless world",
|
|
1020
|
-
"postizoauth": "repo-local stem; env-stem extraction firing on an app-internal name, no vendor exists",
|
|
1021
1020
|
"seedoauth2": "repo-local stem; env-stem extraction firing on an app-internal name, no vendor exists",
|
|
1022
1021
|
"seedplatformoauth": "repo-local stem; env-stem extraction firing on an app-internal name, no vendor exists",
|
|
1023
1022
|
"sink": "repo-local stem; env-stem extraction firing on an app-internal name, no vendor exists",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// What it does not do (named, so a workflow that needs them fails plainly): matrices beyond their first combination,
|
|
9
9
|
// services and containers, composite and JavaScript actions from the marketplace, reusable workflows, concurrency groups.
|
|
10
10
|
import { errorMessage } from "./error-message.js";
|
|
11
|
+
import { worldRequiredCredentialGate } from "./runtime.js";
|
|
11
12
|
import { spawn } from 'node:child_process';
|
|
12
13
|
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
14
|
import { join } from 'node:path';
|
|
@@ -110,19 +111,50 @@ function readKv(file) {
|
|
|
110
111
|
}
|
|
111
112
|
return out;
|
|
112
113
|
}
|
|
113
|
-
function sh(command, cwd, env, shell, log) {
|
|
114
|
-
|
|
114
|
+
function sh(command, cwd, env, shell, log, requireEnv) {
|
|
115
|
+
requireEnv(env, 'Actions command');
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
115
117
|
const file = join(cwd, `.volter-step-${Date.now()}-${Math.random().toString(36).slice(2)}.sh`);
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
118
|
+
let failure = undefined;
|
|
119
|
+
const finish = (code, signal) => {
|
|
120
|
+
const primary = failure ?? (signal ? Object.assign(new Error(`Actions command terminated by ${signal}`), { signal })
|
|
121
|
+
: code === null ? new Error('Actions command closed without an exit status') : undefined);
|
|
122
|
+
try {
|
|
123
|
+
rmSync(file, { force: true });
|
|
124
|
+
}
|
|
125
|
+
catch (cleanup) {
|
|
126
|
+
reject(primary === undefined ? cleanup : new AggregateError([primary, cleanup], 'Actions command and script cleanup failed', { cause: primary }));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (primary !== undefined)
|
|
130
|
+
reject(primary);
|
|
131
|
+
else
|
|
132
|
+
resolve(code);
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
writeFileSync(file, command);
|
|
136
|
+
const argv = shell === 'pwsh' ? ['pwsh', ['-File', file]] : shell === 'sh' ? ['sh', ['-e', file]] : ['bash', ['--noprofile', '--norc', '-eo', 'pipefail', file]];
|
|
137
|
+
const child = spawn(argv[0], [...argv[1]], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
138
|
+
// Spawn errors settle at close, after the owned child's streams close; retain the original native cause.
|
|
139
|
+
child.on('error', (error) => { failure ??= error; });
|
|
140
|
+
const output = (d) => {
|
|
141
|
+
try {
|
|
142
|
+
for (const l of d.toString().split('\n'))
|
|
143
|
+
if (l)
|
|
144
|
+
log(` ${l}`);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
failure ??= error;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
child.stdout.on('data', output);
|
|
151
|
+
child.stderr.on('data', output);
|
|
152
|
+
child.on('close', finish);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
failure = error;
|
|
156
|
+
finish(null, null);
|
|
157
|
+
}
|
|
126
158
|
});
|
|
127
159
|
}
|
|
128
160
|
/** One of GitHub's own actions, performed by the runner; an action it cannot perform fails by name. */
|
|
@@ -151,6 +183,12 @@ function builtin(step, env, workspace) {
|
|
|
151
183
|
/** One run: started, each job's steps run in order of `needs`, completed with its conclusion. */
|
|
152
184
|
export async function runOnce(opts, repo, run) {
|
|
153
185
|
const log = opts.log ?? (() => { });
|
|
186
|
+
// The runner's original World context, before job/step overrides, owns its required values.
|
|
187
|
+
const instancePath = process.env.VOLTER_WORLD_INSTANCE ?? opts.env.VOLTER_WORLD_INSTANCE;
|
|
188
|
+
if (!instancePath && (process.env.VOLTER_WORLD || opts.env.VOLTER_WORLD))
|
|
189
|
+
throw new Error('Actions runner has no local credential declaration');
|
|
190
|
+
const requireEnv = instancePath ? worldRequiredCredentialGate(JSON.parse(readFileSync(instancePath, 'utf8')))
|
|
191
|
+
: (_env, _boundary) => { };
|
|
154
192
|
const id = Number(run.id);
|
|
155
193
|
const started = await api(opts, 'POST', `/_twin/repos/${repo}/actions/runs/${id}/start`);
|
|
156
194
|
if (started.status !== 200) {
|
|
@@ -175,7 +213,7 @@ export async function runOnce(opts, repo, run) {
|
|
|
175
213
|
const root = mkdtempSync(join(opts.workRoot, `run-${id}-`));
|
|
176
214
|
const workspace = join(root, 'workspace');
|
|
177
215
|
const clone = `${opts.github.replace(/\/$/, '').replace(/^(https?:\/\/)/, `$1x-access-token:${jobToken}@`)}/${repo}.git`;
|
|
178
|
-
const git = async (args, cwd) => sh(`git ${args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`, cwd, { ...process.env, ...opts.env }, 'bash', log);
|
|
216
|
+
const git = async (args, cwd) => sh(`git ${args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`, cwd, { ...process.env, ...opts.env }, 'bash', log, requireEnv);
|
|
179
217
|
let conclusion = 'success';
|
|
180
218
|
try {
|
|
181
219
|
mkdirSync(root, { recursive: true });
|
|
@@ -233,6 +271,7 @@ export async function runOnce(opts, repo, run) {
|
|
|
233
271
|
continue;
|
|
234
272
|
}
|
|
235
273
|
log(` step ${i + 1}: ${step.name ?? step.uses ?? String(step.run).split('\n')[0]}`);
|
|
274
|
+
requireEnv(env, 'Actions step environment');
|
|
236
275
|
let ok;
|
|
237
276
|
if (step.uses) {
|
|
238
277
|
// its inputs are expressions too (`registry-url: ${{ vars.NPM_REGISTRY }}`)
|
|
@@ -243,7 +282,7 @@ export async function runOnce(opts, repo, run) {
|
|
|
243
282
|
}
|
|
244
283
|
else {
|
|
245
284
|
const cwd = join(workspace, step['working-directory'] ?? job.defaults?.run?.['working-directory'] ?? '');
|
|
246
|
-
ok = (await sh(expand(String(step.run ?? ''), stepScope), cwd, env, step.shell ?? job.defaults?.run?.shell ?? 'bash', log)) === 0;
|
|
285
|
+
ok = (await sh(expand(String(step.run ?? ''), stepScope), cwd, env, step.shell ?? job.defaults?.run?.shell ?? 'bash', log, requireEnv)) === 0;
|
|
247
286
|
}
|
|
248
287
|
if (step.id)
|
|
249
288
|
scope.steps[step.id] = { outputs: readKv(files.output), outcome: ok ? 'success' : 'failure', conclusion: ok || step['continue-on-error'] ? 'success' : 'failure' };
|
package/dist/src/attach.d.ts
CHANGED
|
@@ -43,7 +43,16 @@ export interface WorldManifest {
|
|
|
43
43
|
url: string;
|
|
44
44
|
env: Record<string, string>;
|
|
45
45
|
}>;
|
|
46
|
+
/** what a browser is started with to reach the World's pages as a person's does (Chrome's flags): the World's proxy as
|
|
47
|
+
* its proxy, and this World's CA alone trusted, by its key's SPKI hash, which Chrome matches in the chain the proxy
|
|
48
|
+
* presents (each leaf followed by the CA). Chrome honours the trust flag only with a `--user-data-dir` of its own,
|
|
49
|
+
* so the browser trusts this World's CA and no other profile is touched. Present when the World runs its proxy. */
|
|
50
|
+
browser?: {
|
|
51
|
+
args: string[];
|
|
52
|
+
};
|
|
46
53
|
}
|
|
54
|
+
/** The base64 SHA-256 of a certificate's SubjectPublicKeyInfo, as Chrome's `--ignore-certificate-errors-spki-list` takes it. */
|
|
55
|
+
export declare function spkiHash(pem: string): string;
|
|
47
56
|
/** Derive the manifest from a world's generated env map (the local rendering).
|
|
48
57
|
* Pure — the CA is inlined only when its file is readable. */
|
|
49
58
|
export declare function deriveWorldManifest(name: string, worldEnv: Record<string, string | undefined>): WorldManifest;
|
package/dist/src/attach.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
// delegates to runWithWorldEnv, `--via direct` prints the manifest. Reflect and
|
|
7
7
|
// remote refs are proposals (docs/guides/route-a-cli-through-the-world.md) and are refused loudly, never
|
|
8
8
|
// silently degraded.
|
|
9
|
+
import { createHash, X509Certificate } from 'node:crypto';
|
|
9
10
|
import { existsSync, readFileSync } from 'node:fs';
|
|
10
11
|
import { dirname, join, resolve } from 'node:path';
|
|
11
12
|
import { activeVendorMap } from "./redirect-proxy.js";
|
|
12
|
-
import { statusWorld } from "./runtime.js";
|
|
13
|
+
import { refuseRequiredCredentialTransport, statusWorld } from "./runtime.js";
|
|
13
14
|
import { worldNetworkPolicyFromEnv } from '@volter/world-core/network-policy';
|
|
14
15
|
export const WORLD_REF_FILE = '.volter-world';
|
|
15
16
|
/** A world reference is remote when it is a URL; every other ref is a local world name. */
|
|
@@ -51,6 +52,10 @@ export function resolveWorldRef(explicit, options = {}) {
|
|
|
51
52
|
return { ref: fromFile.ref, source: 'file', file: fromFile.file };
|
|
52
53
|
throw new Error(`no world reference: pass one (volter-world attach <world> …), set VOLTER_WORLD, or commit a ${WORLD_REF_FILE} file`);
|
|
53
54
|
}
|
|
55
|
+
/** The base64 SHA-256 of a certificate's SubjectPublicKeyInfo, as Chrome's `--ignore-certificate-errors-spki-list` takes it. */
|
|
56
|
+
export function spkiHash(pem) {
|
|
57
|
+
return createHash('sha256').update(new X509Certificate(pem).publicKey.export({ type: 'spki', format: 'der' })).digest('base64');
|
|
58
|
+
}
|
|
54
59
|
// Internal runtime vars are plumbing, not suggested credentials. CA-trust
|
|
55
60
|
// bundle vars are local renderings of `ca` (they hold machine-local paths), so
|
|
56
61
|
// a manifest never carries them — an attacher re-renders trust from `ca`.
|
|
@@ -81,10 +86,12 @@ export function deriveWorldManifest(name, worldEnv) {
|
|
|
81
86
|
}
|
|
82
87
|
const policy = worldNetworkPolicyFromEnv(worldEnv);
|
|
83
88
|
const network = policy === undefined ? undefined : (({ version, world, ...permission }) => permission)(policy);
|
|
84
|
-
|
|
89
|
+
const proxy = worldEnv.VOLTER_WORLD_PROXY ?? null;
|
|
90
|
+
return { name, vendors, ca, proxy, env, ...(network === undefined ? {} : { network }), ...(proxy && ca ? { browser: { args: [`--proxy-server=${proxy}`, `--ignore-certificate-errors-spki-list=${spkiHash(ca)}`] } } : {}) };
|
|
85
91
|
}
|
|
86
92
|
/** The manifest of a local world instance (name resolved against `root` like every other verb). */
|
|
87
93
|
export function worldManifest(name, root = process.cwd()) {
|
|
88
94
|
const status = statusWorld(name, root);
|
|
95
|
+
refuseRequiredCredentialTransport(status, 'WorldManifest publication');
|
|
89
96
|
return deriveWorldManifest(name, status.env);
|
|
90
97
|
}
|
package/dist/src/cli-commands.js
CHANGED
|
@@ -133,6 +133,9 @@ function printHelp() {
|
|
|
133
133
|
# ref: explicit → $VOLTER_WORLD → nearest .volter-world file (docs/guides/route-a-cli-through-the-world.md)
|
|
134
134
|
# --via env (default): run the command attached; --via direct: print the manifest JSON
|
|
135
135
|
volter-world manifest <name> [--root <repo>] # the world manifest (vendors map, CA, traffic proxy, suggested env) as JSON
|
|
136
|
+
volter-world browser-flags <name> [--root <repo>]
|
|
137
|
+
# Chrome's flags for the World's pages: its proxy, and its CA alone trusted (by SPKI);
|
|
138
|
+
# pass them with a --user-data-dir of the browser's own
|
|
136
139
|
volter-world reflect <name> --target-ip <ip> [--resolver-ip <ip>] [--port <p>] [--resolver-port <p>] [--upstream <dns-ip>] [--root <repo>]
|
|
137
140
|
# run the reflect front (SNI TLS door) + resolver in the foreground (docs/ATTACH.md);
|
|
138
141
|
# --port 443 --resolver-port 53 is what containers can reach (attach --via reflect);
|
|
@@ -950,7 +953,7 @@ export async function main() {
|
|
|
950
953
|
// The local env attach, every app's CMD path, needs the World's ref and its runtime alone: the two load together,
|
|
951
954
|
// and the remote door (serve.ts) and the reflect front (reflect.ts, ca-trust.ts) load only on their own paths.
|
|
952
955
|
// All five were imported one after another first (S2 R16: Dub's attach, 225 bodies in 341 ms before its `next`).
|
|
953
|
-
const [{ resolveWorldRef, isRemoteWorldRef, worldManifest }, { withoutVaultSignIn, instanceDir, attachWorld }] = await Promise.all([import("./attach.js"), import("./runtime.js")]);
|
|
956
|
+
const [{ resolveWorldRef, isRemoteWorldRef, worldManifest }, { withoutVaultSignIn, instanceDir, attachWorld, statusWorld: readAttachStatus, refuseRequiredCredentialTransport }] = await Promise.all([import("./attach.js"), import("./runtime.js")]);
|
|
954
957
|
// `subject` may be the ref, a flag, or the `--` separator — reassemble and split.
|
|
955
958
|
const args = [subject, ...rest].filter((arg) => arg !== undefined);
|
|
956
959
|
const split = args.indexOf('--');
|
|
@@ -1011,6 +1014,7 @@ export async function main() {
|
|
|
1011
1014
|
// being the caller's to point (a container's --dns, a resolver on the host).
|
|
1012
1015
|
if (command.length === 0)
|
|
1013
1016
|
throw new Error('volter-world attach: pass the command after --');
|
|
1017
|
+
refuseRequiredCredentialTransport(readAttachStatus(resolved.ref, attachRoot), 'reflect attachment');
|
|
1014
1018
|
const [{ readReflectManifest, splitDockerComposeArgs, composeOverrideForReflect, dockerComposeWithOverride, ATTACHED_CA_PATH }, { sessionTrustEnv }] = await Promise.all([import("./reflect.js"), import("./ca-trust.js")]);
|
|
1015
1019
|
const manifest = readReflectManifest(attachRoot, resolved.ref);
|
|
1016
1020
|
if (!manifest)
|
|
@@ -1050,11 +1054,22 @@ export async function main() {
|
|
|
1050
1054
|
process.stdout.write(`${JSON.stringify(worldManifest(subject, root), null, 2)}\n`);
|
|
1051
1055
|
return;
|
|
1052
1056
|
}
|
|
1057
|
+
if (cmd === 'browser-flags') {
|
|
1058
|
+
const { worldManifest } = await import("./attach.js");
|
|
1059
|
+
if (!subject)
|
|
1060
|
+
throw new Error('volter-world browser-flags: missing world name');
|
|
1061
|
+
const browser = worldManifest(subject, root).browser;
|
|
1062
|
+
if (!browser)
|
|
1063
|
+
throw new Error(`volter-world browser-flags: World "${subject}" runs no proxy (it has no twinned host or network policy, or this host opens no raw sockets), so a browser has no route to its pages`);
|
|
1064
|
+
process.stdout.write(`${browser.args.join('\n')}\n`);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1053
1067
|
if (cmd === 'reflect') {
|
|
1054
1068
|
const { reflectRoutesPath, startReflectFront, startReflectResolver, readReflectRoutes, writeReflectManifest, clearReflectManifest } = await import("./reflect.js");
|
|
1055
|
-
const { statusWorld, instanceDir } = await import("./runtime.js");
|
|
1069
|
+
const { statusWorld, instanceDir, refuseRequiredCredentialTransport } = await import("./runtime.js");
|
|
1056
1070
|
if (!subject)
|
|
1057
1071
|
throw new Error('volter-world reflect: missing world name');
|
|
1072
|
+
refuseRequiredCredentialTransport(statusWorld(subject, root), 'reflect front publication');
|
|
1058
1073
|
const targetIp = optionValue(rest, '--target-ip');
|
|
1059
1074
|
if (!targetIp)
|
|
1060
1075
|
throw new Error('volter-world reflect: --target-ip <ip> is required (the address attachers reach the front at)');
|
|
@@ -1088,7 +1103,7 @@ export async function main() {
|
|
|
1088
1103
|
}
|
|
1089
1104
|
if (cmd === 'serve') {
|
|
1090
1105
|
const { startReflectFront } = await import("./reflect.js");
|
|
1091
|
-
const { statusWorld, instanceDir } = await import("./runtime.js");
|
|
1106
|
+
const { statusWorld, instanceDir, refuseRequiredCredentialTransport } = await import("./runtime.js");
|
|
1092
1107
|
const { startManifestServer, advertiseWorldManifest, MANIFEST_PATH } = await import("./serve.js");
|
|
1093
1108
|
if (!subject)
|
|
1094
1109
|
throw new Error('volter-world serve: missing world name');
|
|
@@ -1104,6 +1119,7 @@ export async function main() {
|
|
|
1104
1119
|
}
|
|
1105
1120
|
if (advertisedUrl.protocol !== 'https:')
|
|
1106
1121
|
throw new Error('volter-world serve: the advertised origin must be https (the door terminates TLS with the session CA)');
|
|
1122
|
+
refuseRequiredCredentialTransport(statusWorld(subject, root), 'remote WorldManifest serving');
|
|
1107
1123
|
const frontPort = Number(advertisedUrl.port) || 443;
|
|
1108
1124
|
const front = await startReflectFront({
|
|
1109
1125
|
envLoader: () => statusWorld(subject, root).env,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// The console opens a World's pages (a vendor's screens) on the Worlds' origin, which it is told.
|
|
7
7
|
import { errorMessage } from "./error-message.js";
|
|
8
8
|
import { serveHttp } from '@volter/world-core/runtime';
|
|
9
|
+
import { forwardableHeaders } from "./forwardable-headers.js";
|
|
9
10
|
export const CONSOLE_BASE = '/-/console';
|
|
10
11
|
/** Serve `mount` on its own listener; doors under `/-/` go to `upstream` (the Worlds' origin). */
|
|
11
12
|
/** `upstream`: where this listener reaches the host's doors (its own address); `worlds`: the origin the
|
|
@@ -26,7 +27,7 @@ export async function serveConsoleApart(mount, opts) {
|
|
|
26
27
|
return Response.json({ error: 'the console serves the console and the host\'s doors only' }, { status: 404 });
|
|
27
28
|
// a door, forwarded: the caller's token header rides along; no cookie goes up (this origin holds
|
|
28
29
|
// none) and none comes back (a World's session belongs to the Worlds' origin, set there)
|
|
29
|
-
const headers =
|
|
30
|
+
const headers = forwardableHeaders(request.headers);
|
|
30
31
|
for (const h of ['host', 'cookie', 'origin', 'referer'])
|
|
31
32
|
headers.delete(h);
|
|
32
33
|
const body = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.arrayBuffer();
|
|
@@ -75,7 +76,7 @@ export async function serveConsoleFor(target, opts = {}) {
|
|
|
75
76
|
hostname, port: opts.port !== undefined && opts.port !== 0 ? opts.port + 1 : 0, twinRequestJournal: false,
|
|
76
77
|
async fetch(request) {
|
|
77
78
|
const url = new URL(request.url);
|
|
78
|
-
const headers =
|
|
79
|
+
const headers = forwardableHeaders(request.headers);
|
|
79
80
|
for (const h of ['host', 'origin', 'referer'])
|
|
80
81
|
headers.delete(h);
|
|
81
82
|
const body = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.arrayBuffer();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WorldServiceConfig } from './schema.js';
|
|
2
|
+
export type CredentialDoor = NonNullable<WorldServiceConfig['issues']>;
|
|
3
|
+
/** The answer's own scalar property, without a file, directory or side transform. */
|
|
4
|
+
export declare function isScalarCredentialField(value: string | undefined): boolean;
|
|
5
|
+
/** A saved alias extends the current descriptor; it cannot replace its issuer or fields. */
|
|
6
|
+
export declare function composeCredentialDoor(saved: CredentialDoor | undefined, current: CredentialDoor | undefined, service: string): CredentialDoor | undefined;
|
|
7
|
+
/** Non-pack process doors keep their own contract. Omitted type means twin, as at startup. */
|
|
8
|
+
export declare function withCurrentCredentialDoor(service: WorldServiceConfig, current: CredentialDoor | undefined): WorldServiceConfig;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** The answer's own scalar property, without a file, directory or side transform. */
|
|
2
|
+
export function isScalarCredentialField(value) {
|
|
3
|
+
return value !== undefined && value.length > 0 && !/^(?:file|directory|side):/.test(value);
|
|
4
|
+
}
|
|
5
|
+
/** A saved alias extends the current descriptor; it cannot replace its issuer or fields. */
|
|
6
|
+
export function composeCredentialDoor(saved, current, service) {
|
|
7
|
+
if (current === undefined) {
|
|
8
|
+
if (saved !== undefined)
|
|
9
|
+
throw new Error(`World service ${service}: its current pack has no credential door; revalidate its declared aliases`);
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const aliases = [];
|
|
13
|
+
const fields = new Set(Object.values(current.fill).filter(isScalarCredentialField));
|
|
14
|
+
for (const [name, field] of Object.entries(saved?.fill ?? {})) {
|
|
15
|
+
if (Object.hasOwn(current.fill, name)) {
|
|
16
|
+
if (current.fill[name] !== field)
|
|
17
|
+
throw new Error(`World service ${service}: credential binding ${name} conflicts with its current pack; revalidate the source`);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || !isScalarCredentialField(field) || !fields.has(field))
|
|
21
|
+
throw new Error(`World service ${service}: alias ${name} does not name a current scalar credential field; revalidate the source`);
|
|
22
|
+
aliases.push([name, field]);
|
|
23
|
+
}
|
|
24
|
+
const fill = Object.fromEntries([...Object.entries(current.fill), ...aliases]);
|
|
25
|
+
const required = saved?.required;
|
|
26
|
+
if (required !== undefined) {
|
|
27
|
+
if (!Array.isArray(required))
|
|
28
|
+
throw new Error(`World service ${service}: required credential names must be an array`);
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const name of required) {
|
|
31
|
+
if (typeof name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || seen.has(name)
|
|
32
|
+
|| !Object.hasOwn(fill, name) || !isScalarCredentialField(fill[name]) || !fields.has(fill[name])) {
|
|
33
|
+
throw new Error(`World service ${service}: required credential ${String(name)} does not name a current scalar field`);
|
|
34
|
+
}
|
|
35
|
+
seen.add(name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { path: current.path, body: current.body, fill, ...(required === undefined ? {} : { required: [...required] }) };
|
|
39
|
+
}
|
|
40
|
+
/** Non-pack process doors keep their own contract. Omitted type means twin, as at startup. */
|
|
41
|
+
export function withCurrentCredentialDoor(service, current) {
|
|
42
|
+
if ((service.type ?? 'twin') !== 'twin')
|
|
43
|
+
return service;
|
|
44
|
+
const door = composeCredentialDoor(service.issues, current, service.id);
|
|
45
|
+
if (door === undefined)
|
|
46
|
+
return service;
|
|
47
|
+
for (const name of Object.keys(door.fill)) {
|
|
48
|
+
if (Object.hasOwn(current.fill, name) && !door.required?.includes(name))
|
|
49
|
+
continue;
|
|
50
|
+
if (name === service.injectEnv || Object.hasOwn(service.injectEnvTemplates ?? {}, name) || Object.hasOwn(service.cliRedirect ?? {}, name))
|
|
51
|
+
throw new Error(`World service ${service.id}: credential alias ${name} conflicts with an endpoint binding`);
|
|
52
|
+
}
|
|
53
|
+
return { ...service, issues: door };
|
|
54
|
+
}
|
package/dist/src/fixture-env.js
CHANGED
|
@@ -185,7 +185,7 @@ const CREDENTIAL_SHAPES = [
|
|
|
185
185
|
// Stripe's keys carry their kind in their prefix, and applications check it before calling (Cal.com's Stripe app
|
|
186
186
|
// refuses a key that is not `sk_…`/`pk_…`); a webhook signing secret is `whsec_…`. Test mode, as a World is.
|
|
187
187
|
{ match: /(^|_)STRIPE_[A-Z0-9_]*WEBHOOK_SECRET$/, value: () => 'whsec_twinfake0000000000000000000000000000', source: 'https://docs.stripe.com/webhooks#verify-events' },
|
|
188
|
-
{ match: /(^|_)STRIPE_[A-Z0-9_]*(SECRET|PRIVATE|API)(_API)?_KEY$/, value: () => '
|
|
188
|
+
{ match: /(^|_)STRIPE_[A-Z0-9_]*(SECRET|PRIVATE|API)(_API)?_KEY$/, value: () => 'sk_test' + '_twinfake000000000000000000000000000000', source: 'https://docs.stripe.com/keys (sk_test_)' },
|
|
189
189
|
{ match: /(^|_)STRIPE_[A-Z0-9_]*(PUBLISHABLE|PUBLIC)(_API)?_KEY$/, value: () => 'pk_test_twinfake000000000000000000000000000000', source: 'https://docs.stripe.com/keys (pk_test_)' },
|
|
190
190
|
// tavily-auth.ts and firecrawl-auth.ts refuse a key without the vendor's prefix with the vendor's 401
|
|
191
191
|
{ match: /(^|_)TAVILY_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'tvly-twinfake00000000000000000000', source: 'earlier @volter/twin-tavily tavily-auth.ts' },
|
|
@@ -193,7 +193,7 @@ const CREDENTIAL_SHAPES = [
|
|
|
193
193
|
// a Clerk webhook's signing secret is Svix's (`whsec_` and base64), which the app's verifier decodes; the World registers
|
|
194
194
|
// its endpoint with the same secret (the Clerk twin's webhooks door takes `signing_secret`)
|
|
195
195
|
{ match: /(^|_)CLERK_WEBHOOK(_SIGNING)?_SECRET$/, value: () => `whsec_${Buffer.from('twin-fake-clerk-webhook-secret!!').toString('base64')}`, source: 'https://clerk.com/docs/webhooks/sync-data (CLERK_WEBHOOK_SIGNING_SECRET, whsec_); https://docs.svix.com/receiving/verifying-payloads/how-manual' },
|
|
196
|
-
{ match: /(^|_)CLERK_[A-Z0-9_]*SECRET[A-Z0-9_]*$/, value: () => '
|
|
196
|
+
{ match: /(^|_)CLERK_[A-Z0-9_]*SECRET[A-Z0-9_]*$/, value: () => 'sk_test' + '_twinfake000000000000000000000000000000', source: 'earlier @volter/twin-clerk manifest.ts' },
|
|
197
197
|
{ match: /(^|_)CLERK_[A-Z0-9_]*PUBLISHABLE[A-Z0-9_]*$/, value: () => `pk_test_${Buffer.from('twin.clerk.accounts.dev$').toString('base64')}`, source: 'earlier @volter/twin-clerk index.ts' },
|
|
198
198
|
];
|
|
199
199
|
/** Keys the APPLICATION parses before it can use them: a fake that is not the shape its own code decodes throws at the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function forwardableHeaders(incoming: Headers): Headers;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The headers of one connection, not of the request (RFC 9110 §7.6.1), with `expect`: the hop that received them
|
|
2
|
+
* answered them (Node's server sends the 100 Continue itself). Node's fetch refuses `transfer-encoding`, `keep-alive`,
|
|
3
|
+
* `upgrade`, `expect` and a `connection` other than close/keep-alive outright, so a request forwarded with them fails
|
|
4
|
+
* `fetch failed (UND_ERR_INVALID_ARG)`; the body streams on and fetch frames it afresh. Every World path that forwards
|
|
5
|
+
* a received request into fetch (the redirect proxy, a served World's twin routes, a reflected host) copies its
|
|
6
|
+
* headers through this. */
|
|
7
|
+
const HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'te', 'trailer', 'upgrade', 'expect'];
|
|
8
|
+
const TOKEN = /^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
|
|
9
|
+
export function forwardableHeaders(incoming) {
|
|
10
|
+
const headers = new Headers(incoming);
|
|
11
|
+
for (const listed of (incoming.get('connection') ?? '').split(',')) {
|
|
12
|
+
const name = listed.trim();
|
|
13
|
+
if (TOKEN.test(name))
|
|
14
|
+
headers.delete(name);
|
|
15
|
+
}
|
|
16
|
+
for (const h of HOP_BY_HOP)
|
|
17
|
+
headers.delete(h);
|
|
18
|
+
return headers;
|
|
19
|
+
}
|
|
@@ -7,6 +7,8 @@ export type GoMysqlBinding = {
|
|
|
7
7
|
};
|
|
8
8
|
/** Only versioned, explicitly typed data selects a driver grammar. */
|
|
9
9
|
export declare function goMysqlBinding(entry: Record<string, unknown>, version: unknown, name: string, declaredPort: number): Partial<GoMysqlBinding>;
|
|
10
|
+
/** The driver fields themselves; callers own their separate input-version gate. */
|
|
11
|
+
export declare function goMysqlBindingFields(entry: Record<string, unknown>, name: string, declaredPort: number): Partial<GoMysqlBinding>;
|
|
10
12
|
export declare function renderGoMysqlBinding(entry: GoMysqlBinding & {
|
|
11
13
|
name: string;
|
|
12
14
|
containerPort?: number;
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/** Only versioned, explicitly typed data selects a driver grammar. */
|
|
2
2
|
export function goMysqlBinding(entry, version, name, declaredPort) {
|
|
3
|
+
if (entry.dsnFormat !== undefined && version !== 6 && version !== 7)
|
|
4
|
+
throw new Error(`volter init: ${name} needs a valid stack-needs version6/7 Go MySQL binding`);
|
|
5
|
+
return goMysqlBindingFields(entry, name, declaredPort);
|
|
6
|
+
}
|
|
7
|
+
/** The driver fields themselves; callers own their separate input-version gate. */
|
|
8
|
+
export function goMysqlBindingFields(entry, name, declaredPort) {
|
|
3
9
|
if (entry.dsnFormat === undefined) {
|
|
4
10
|
if (entry.endpoint !== undefined || entry.dsnPrefix !== undefined || entry.dsnProtocol !== undefined)
|
|
5
11
|
throw new Error(`volter init: ${name} has DSN fields without a declared format`);
|
|
6
12
|
return {};
|
|
7
13
|
}
|
|
8
|
-
if (
|
|
14
|
+
if (entry.dsnFormat !== 'go-mysql' || entry.scheme !== 'mysql' || entry.withPort !== undefined
|
|
9
15
|
|| (entry.endpoint !== 'explicit-port' && entry.endpoint !== 'default-port') || (entry.dsnPrefix !== '' && entry.dsnPrefix !== 'mysql://')
|
|
10
16
|
|| (entry.dsnProtocol !== '' && entry.dsnProtocol !== 'tcp') || !Number.isInteger(entry.containerPort) || Number(entry.containerPort) < 1 || Number(entry.containerPort) > 65535
|
|
11
17
|
|| (entry.endpoint === 'default-port' && entry.containerPort !== declaredPort)
|
package/dist/src/guest-runner.js
CHANGED
|
@@ -8,9 +8,10 @@ import { allocatePort } from "./tcp-port.js";
|
|
|
8
8
|
import { ownedPath, assertOwnedRealPath } from "./owned-path.js";
|
|
9
9
|
import { commandLifetime, commandProcessGroup } from "./command-lifetime.js";
|
|
10
10
|
import { beginGuestProcess, retainGuestProcess, readGuestProcesses, releaseGuestProcesses } from './guest-processes.mjs';
|
|
11
|
-
import { retireOwnedProcessGroups } from "./runtime.js";
|
|
11
|
+
import { refuseRequiredCredentialTransport, retireOwnedProcessGroups } from "./runtime.js";
|
|
12
12
|
import { guestControl } from './guest-control.mjs';
|
|
13
13
|
import { admitGuestArgv, guestCommandGap, guestEnvironment } from './guest-command.mjs';
|
|
14
|
+
import { forwardableHeaders } from "./forwardable-headers.js";
|
|
14
15
|
const delay = (ms) => new Promise(r => setTimeout(r, ms));
|
|
15
16
|
// Re-enrolling image metadata must not lose ownership of guests already running in this World.
|
|
16
17
|
const pools = new Map();
|
|
@@ -192,6 +193,7 @@ function worldEnv() {
|
|
|
192
193
|
if (!instancePath)
|
|
193
194
|
throw new Error('guest execution requires a World instance');
|
|
194
195
|
const instance = JSON.parse(readFileSync(instancePath, 'utf8'));
|
|
196
|
+
refuseRequiredCredentialTransport(instance, 'light machine application transport', 'WORLD_MACHINE_CAPABILITY_GAP');
|
|
195
197
|
// proxy-env has the completed vendor map; the early twin process env may not.
|
|
196
198
|
const current = JSON.parse(readFileSync(join(dirname(instancePath), 'proxy-env.json'), 'utf8'));
|
|
197
199
|
const env = { ...instance.env, ...current };
|
|
@@ -339,7 +341,7 @@ export function createLightGuestPool(options) {
|
|
|
339
341
|
async start({ id }) { const g = guests.get(id); if (!g)
|
|
340
342
|
throw new Error('Substrate guest launch was not prepared'); return { name: id, url: `http://127.0.0.1:${g.agentPort}` }; },
|
|
341
343
|
async pause({ name }) { const g = live(name); await ask(g.child, 'pause'); update(g, { state: 'paused' }); },
|
|
342
|
-
async resume({ name }) { const g = guests.get(name); if (!g || g.status.state !== 'paused')
|
|
344
|
+
async resume({ name }) { worldEnv(); const g = guests.get(name); if (!g || g.status.state !== 'paused')
|
|
343
345
|
throw new Error('Substrate guest cannot resume'); await ask(g.child, 'resume'); update(g, { state: 'started' }); },
|
|
344
346
|
async stop({ name }) { await stop(name); },
|
|
345
347
|
running() { return new Set([...guests].filter(([, g]) => g.status.state === 'started').map(([name]) => name)); },
|
|
@@ -347,6 +349,7 @@ export function createLightGuestPool(options) {
|
|
|
347
349
|
return {
|
|
348
350
|
kind: 'light',
|
|
349
351
|
async admitRun(spec) {
|
|
352
|
+
worldEnv(); // Required application bindings have no admitted guest transport.
|
|
350
353
|
if (!/^[a-zA-Z0-9_-]+$/.test(spec.name))
|
|
351
354
|
throw new Error('invalid guest name');
|
|
352
355
|
const image = options.images?.[spec.image];
|
|
@@ -527,6 +530,7 @@ export function createLightGuestPool(options) {
|
|
|
527
530
|
}
|
|
528
531
|
},
|
|
529
532
|
async exec(name, spec) {
|
|
533
|
+
worldEnv(); // Re-read the owning boot before a later command can start.
|
|
530
534
|
const g = live(name);
|
|
531
535
|
const queue = [];
|
|
532
536
|
let wake, done = false;
|
|
@@ -552,8 +556,8 @@ export function createLightGuestPool(options) {
|
|
|
552
556
|
const g = live(name);
|
|
553
557
|
if (!g.ports.has(port))
|
|
554
558
|
throw new Error('guest port is not declared');
|
|
555
|
-
const url = new URL(request.url), headers =
|
|
556
|
-
for (const key of ['host', '
|
|
559
|
+
const url = new URL(request.url), headers = forwardableHeaders(request.headers);
|
|
560
|
+
for (const key of ['host', 'content-length'])
|
|
557
561
|
headers.delete(key);
|
|
558
562
|
const init = { method: request.method, headers, body: ['GET', 'HEAD'].includes(request.method) ? undefined : await request.arrayBuffer(), signal: request.signal, redirect: 'manual' };
|
|
559
563
|
return g.runtime.request(port, url.pathname + url.search, init);
|
package/dist/src/host-ready.d.ts
CHANGED
|
@@ -12,12 +12,16 @@ export type HostReadyTimes = {
|
|
|
12
12
|
} & Partial<HostStartTimes>;
|
|
13
13
|
/** Only the host, after all starts have returned and ports were checked, publishes this receipt. */
|
|
14
14
|
export declare function publishHostReady(path: string, boot: string, ports: Record<string, number>, times?: HostStartTimes, streams?: readonly string[]): void;
|
|
15
|
+
/** The twins a co-located host's log says it has started (host-cli's `event twin=<id> kind=started` lines). Only
|
|
16
|
+
* starts count: a twin's own output, the recorder's time marks or another twin's errors are not progress. */
|
|
17
|
+
export declare function startedTwins(log: string): number;
|
|
15
18
|
/**
|
|
16
19
|
* Receipt admission is cancellation-aware; stale or malformed state is never
|
|
17
20
|
* readiness. `timeoutMs` bounds a host that stops making progress, not a slow
|
|
18
|
-
* one: each change in `progress` (the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* one: each change in `progress` (the count of twins started, `startedTwins`)
|
|
22
|
+
* begins the bound again, so a host still starting its twins on a busy machine
|
|
23
|
+
* is waited for, and one that starts no further twin for `timeoutMs` is not.
|
|
24
|
+
* Starts are finite, so the whole wait is at most (twins + 1) × `timeoutMs`.
|
|
21
25
|
* Resolves with what the receipt says of the host's start: when it was published, its steps before, and which declared
|
|
22
26
|
* twins' main ports are byte streams.
|
|
23
27
|
*/
|
package/dist/src/host-ready.js
CHANGED
|
@@ -11,12 +11,25 @@ export function publishHostReady(path, boot, ports, times, streams) {
|
|
|
11
11
|
writeFileSync(temporary, `${JSON.stringify({ version: 1, boot, ports, at: Date.now(), ...times, ...(streams?.length ? { streams } : {}) })}\n`, { mode: 0o600 });
|
|
12
12
|
renameSync(temporary, path);
|
|
13
13
|
}
|
|
14
|
+
/** The twins a co-located host's log says it has started (host-cli's `event twin=<id> kind=started` lines). Only
|
|
15
|
+
* starts count: a twin's own output, the recorder's time marks or another twin's errors are not progress. */
|
|
16
|
+
export function startedTwins(log) {
|
|
17
|
+
let text;
|
|
18
|
+
try {
|
|
19
|
+
text = readFileSync(log, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
return new Set(Array.from(text.matchAll(/^event twin=(\S+) kind=started\b/gmu), (match) => match[1])).size;
|
|
25
|
+
}
|
|
14
26
|
/**
|
|
15
27
|
* Receipt admission is cancellation-aware; stale or malformed state is never
|
|
16
28
|
* readiness. `timeoutMs` bounds a host that stops making progress, not a slow
|
|
17
|
-
* one: each change in `progress` (the
|
|
18
|
-
*
|
|
19
|
-
*
|
|
29
|
+
* one: each change in `progress` (the count of twins started, `startedTwins`)
|
|
30
|
+
* begins the bound again, so a host still starting its twins on a busy machine
|
|
31
|
+
* is waited for, and one that starts no further twin for `timeoutMs` is not.
|
|
32
|
+
* Starts are finite, so the whole wait is at most (twins + 1) × `timeoutMs`.
|
|
20
33
|
* Resolves with what the receipt says of the host's start: when it was published, its steps before, and which declared
|
|
21
34
|
* twins' main ports are byte streams.
|
|
22
35
|
*/
|
package/dist/src/index.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export { coverWorld, coverageRecord, detectRepoVendors, envNameVendor, formatCov
|
|
|
21
21
|
export type { CoverageReport, CoverageRow, CoverageStatus, CoverageOptions, RepoVendorSignals } from './covers.js';
|
|
22
22
|
export { formatInitReport, initWorld, packCatalog, planWorldInit, renderEnvFile, writeWorldInit } from './init.js';
|
|
23
23
|
export { STATE_GITIGNORE } from './init.js';
|
|
24
|
+
export { MANAGED_API_SOURCE_READER_CONTRACT } from './managed-api-source.js';
|
|
25
|
+
export type { ManagedApiSourceReader, ManagedApiSourceReading } from './managed-api-source.js';
|
|
26
|
+
/** Current resolved-catalog metadata; callers authenticate its supplying package bytes separately. */
|
|
27
|
+
export { packFacts, type PackFacts } from './pack-facts.js';
|
|
24
28
|
export { clockFile, instanceDir } from './runtime.js';
|
|
25
29
|
export { setWorldClock, writeWorldClock } from './clock.js';
|
|
26
30
|
export { checkoutCatalog, findInstalledPackage, installedCatalog, resolveCatalog, resolveServicePackage, twinPackageName, TWIN_PACKAGE_PREFIX, type Catalog, type CatalogKind, packCli, packEntry } from './catalog.js';
|
|
@@ -35,7 +39,7 @@ export { startColocatedHost } from './host.js';
|
|
|
35
39
|
export type { ColocatedTwinSpec, ColocatedHost, HostIsolation } from './host.js';
|
|
36
40
|
export { advertiseWorldManifest, fetchRemoteManifest, MANIFEST_PATH, remoteAttachEnv, startManifestServer, } from './serve.js';
|
|
37
41
|
export type { ManifestServerHandle, ManifestServerOptions } from './serve.js';
|
|
38
|
-
export { resetWorld, seedEntryFor, seedWorld } from './placeholder.js';
|
|
42
|
+
export { resetWorld, seedEntryFor, seedServedWorld, seedWorld, type SeedServedOutcome } from './placeholder.js';
|
|
39
43
|
export type { SeedWorldOptions, SeedWorldOutcome } from './placeholder.js';
|
|
40
44
|
export { fetchFromOrigin, worldOrigin } from './origin.js';
|
|
41
45
|
export type { FetchOptions, FetchOutcome } from './origin.js';
|
package/dist/src/index.js
CHANGED
|
@@ -19,6 +19,9 @@ export { coverWorld, coverageRecord, detectRepoVendors, envNameVendor, formatCov
|
|
|
19
19
|
// world config + env file OUTSIDE the repo (fixture-env), and prove the emission with `covers`.
|
|
20
20
|
export { formatInitReport, initWorld, packCatalog, planWorldInit, renderEnvFile, writeWorldInit } from "./init.js";
|
|
21
21
|
export { STATE_GITIGNORE } from "./init.js";
|
|
22
|
+
export { MANAGED_API_SOURCE_READER_CONTRACT } from "./managed-api-source.js";
|
|
23
|
+
/** Current resolved-catalog metadata; callers authenticate its supplying package bytes separately. */
|
|
24
|
+
export { packFacts } from "./pack-facts.js";
|
|
22
25
|
export { clockFile, instanceDir } from "./runtime.js";
|
|
23
26
|
export { setWorldClock, writeWorldClock } from "./clock.js";
|
|
24
27
|
export { checkoutCatalog, findInstalledPackage, installedCatalog, resolveCatalog, resolveServicePackage, twinPackageName, TWIN_PACKAGE_PREFIX, packCli, packEntry } from "./catalog.js";
|
|
@@ -27,7 +30,7 @@ export { deriveWorldManifest, findWorldRefFile, isRemoteWorldRef, resolveWorldRe
|
|
|
27
30
|
export { parseSni, readReflectRoutes, reflectRoutesPath, reflectManifestPath, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, CA_TRUST_ENV, ATTACHED_CA_PATH, startReflectFront, startReflectResolver, writeReflectRoutes, } from "./reflect.js";
|
|
28
31
|
export { startColocatedHost } from "./host.js";
|
|
29
32
|
export { advertiseWorldManifest, fetchRemoteManifest, MANIFEST_PATH, remoteAttachEnv, startManifestServer, } from "./serve.js";
|
|
30
|
-
export { resetWorld, seedEntryFor, seedWorld } from "./placeholder.js";
|
|
33
|
+
export { resetWorld, seedEntryFor, seedServedWorld, seedWorld } from "./placeholder.js";
|
|
31
34
|
export { fetchFromOrigin, worldOrigin } from "./origin.js";
|
|
32
35
|
export { branchWorld, checkoutWorld } from "./branch.js";
|
|
33
36
|
export { deployWorld, refreshTwin, adaptersFor, credentialPath, rewrapTwinCredentials, credentialPayloadFrom, rootCredentialInput, bindLegacyCredentials, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from "./root.js";
|
package/dist/src/infra.js
CHANGED
|
@@ -17,18 +17,23 @@ import { containerlessDataDirectory, infraConnections, liveDatabaseBehind, liveD
|
|
|
17
17
|
import { errorMessage } from "./error-message.js";
|
|
18
18
|
/** Whether a program is on PATH, read from the file system without starting a process: where a missing program
|
|
19
19
|
* still costs a process round trip (a browser tab's kernel), the container probe is not spawned at all. */
|
|
20
|
+
/** Whether a command is on PATH. On Windows the variable may be spelled `Path` (a copied environment is not
|
|
21
|
+
* case-insensitive as `process.env` is) and a command is found with PATHEXT's extensions, as the shell finds it. */
|
|
20
22
|
function onPath(name, env) {
|
|
21
|
-
|
|
23
|
+
const windows = process.platform === 'win32';
|
|
24
|
+
const value = windows ? Object.entries(env).find(([key]) => key.toUpperCase() === 'PATH')?.[1] : env.PATH;
|
|
25
|
+
const names = windows ? ['', ...(Object.entries(env).find(([key]) => key.toUpperCase() === 'PATHEXT')?.[1] ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)].map(ext => name + ext) : [name];
|
|
26
|
+
return (value ?? '').split(delimiter).some((dir) => {
|
|
22
27
|
if (!dir)
|
|
23
28
|
return false;
|
|
24
|
-
try {
|
|
25
|
-
const path = join(dir,
|
|
29
|
+
return names.some((candidate) => { try {
|
|
30
|
+
const path = join(dir, candidate);
|
|
26
31
|
accessSync(path, constants.X_OK);
|
|
27
32
|
return statSync(path).isFile();
|
|
28
33
|
}
|
|
29
34
|
catch {
|
|
30
35
|
return false;
|
|
31
|
-
}
|
|
36
|
+
} });
|
|
32
37
|
});
|
|
33
38
|
}
|
|
34
39
|
/** A command run to its end without blocking this process: in the World's own `up` the phase shares the event loop
|
|
@@ -248,7 +253,7 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
248
253
|
return 2;
|
|
249
254
|
}
|
|
250
255
|
// No docker on PATH is the same absence the probe's ENOENT reports, read without a process.
|
|
251
|
-
if (
|
|
256
|
+
if (!onPath('docker', env))
|
|
252
257
|
return 'pglite';
|
|
253
258
|
const probe = await runCommand('docker', ['info', '--format', '{{.ServerVersion}}'], { env, timeout: 10_000, ...(signal ? { signal } : {}) });
|
|
254
259
|
signal?.throwIfAborted();
|