@volter/world-runtime 2.0.26 → 2.0.27
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/src/cli.js +3 -3
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/init.d.ts +9 -0
- package/dist/src/init.js +5 -4
- package/dist/src/root.d.ts +12 -0
- package/dist/src/root.js +128 -2
- package/dist/src/runtime.d.ts +4 -0
- package/dist/src/runtime.js +12 -2
- package/package.json +1 -1
- package/src/cli.ts +3 -3
- package/src/index.ts +3 -1
- package/src/init.ts +6 -5
- package/src/root.ts +101 -2
- package/src/runtime.ts +11 -2
package/dist/src/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
8
8
|
import { activateScript, branchWorld, checkoutWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, resetWorld, seedWorld, worldOrigin, approveWorldChangeset, checkPrerequisites, coverWorld, createWorldChangeset, diffWorld, doctorWorld, downWorld, findWorldChangeset, formatCoverageReport, formatInitReport, formatPrerequisiteChecks, formatProjectInspection, initWorld, inspectProject, isRemoteWorldRef, listWorldChangesets, listWorlds, markWorld, migrateWorldConfig, readReflectRoutes, reflectRoutesPath, replayWorldChangeset, resolveWorldRef, attachWorld, retireWorldConsumers, runWorld, shareWorldServices, shellWorld, startReflectFront, startReflectResolver, statusWorld, statusWorldChangeset, tailWorldActions, unshareWorld, urlsWorld, urlWorld, verifyWorldChangeset, worldManifest, writeReflectRoutes, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, ATTACHED_CA_PATH } from "./index.js";
|
|
9
9
|
import { formatApplication, formatChangeset, formatRebaseReport, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, parseVerifierExpression } from '@volter/world-core';
|
|
10
10
|
import { advertiseWorldManifest, fetchRemoteManifest, MANIFEST_PATH, remoteAttachEnv, startManifestServer } from "./serve.js";
|
|
11
|
-
import { clockFile, instanceDir, pruneWorlds } from "./runtime.js";
|
|
11
|
+
import { clockFile, instanceDir, pruneWorlds, withoutVaultSignIn } from "./runtime.js";
|
|
12
12
|
import { worldResourceReport } from "./resources.js";
|
|
13
13
|
import { worldConsumers, worldUsage } from "./consumers.js";
|
|
14
14
|
import { addAppHosts, appUrlUnsetMessage, detectAppUrl, readAppUrl, setAppUrl } from "./app-url.js";
|
|
@@ -990,7 +990,7 @@ async function main() {
|
|
|
990
990
|
const { bridgeStreams, drainStreams } = createRequire(import.meta.url)('@volter/world-core/stream-bridge');
|
|
991
991
|
const bridged = manifest.streams ? await bridgeStreams(manifest.streams, attachToken ?? manifest.env.VOLTER_TWINS_KEY ?? '') : { env: {} };
|
|
992
992
|
// VOLTER_WORLD names the World to the injector: a request addressed to its paths carries its key
|
|
993
|
-
const commandEnv = { ...process.env, ...env, ...bridged.env, VOLTER_WORLD: resolved.ref };
|
|
993
|
+
const commandEnv = withoutVaultSignIn({ ...process.env, ...env, ...bridged.env, VOLTER_WORLD: resolved.ref });
|
|
994
994
|
commandEnv.VOLTER_TWIN_INJECT_QUIET = head.includes('--verbose') ? '0' : commandEnv.VOLTER_TWIN_INJECT_QUIET ?? '1';
|
|
995
995
|
const child = spawn(command[0], command.slice(1), { windowsHide: true, env: commandEnv, stdio: 'inherit' });
|
|
996
996
|
const code = await new Promise((resolveExit) => { child.on('exit', (status) => resolveExit(status ?? 1)); child.on('error', () => resolveExit(127)); });
|
|
@@ -1012,7 +1012,7 @@ async function main() {
|
|
|
1012
1012
|
const manifest = readReflectManifest(attachRoot, resolved.ref);
|
|
1013
1013
|
if (!manifest)
|
|
1014
1014
|
throw new Error(`volter-world attach --via reflect: no reflect front is running for world ${resolved.ref} — start one: volter-world reflect ${resolved.ref} --target-ip <ip> --port 443 --resolver-port 53`);
|
|
1015
|
-
const env = { ...process.env, VOLTER_WORLD: resolved.ref, VOLTER_REFLECT_DNS: manifest.targetIp, VOLTER_WORLD_CA: manifest.caCertPath };
|
|
1015
|
+
const env = { ...withoutVaultSignIn(process.env), VOLTER_WORLD: resolved.ref, VOLTER_REFLECT_DNS: manifest.targetIp, VOLTER_WORLD_CA: manifest.caCertPath };
|
|
1016
1016
|
Object.assign(env, sessionTrustEnv(manifest.caCertPath, env));
|
|
1017
1017
|
let toRun = command;
|
|
1018
1018
|
const compose = splitDockerComposeArgs(command);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export { fetchFromOrigin, worldOrigin } from './origin.js';
|
|
|
39
39
|
export type { FetchOptions, FetchOutcome } from './origin.js';
|
|
40
40
|
export { branchWorld, checkoutWorld } from './branch.js';
|
|
41
41
|
export type { BranchOptions } from './branch.js';
|
|
42
|
-
export { deployWorld, refreshTwin, adaptersFor, credentialPath, credentialPayloadFrom, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from './root.js';
|
|
42
|
+
export { deployWorld, refreshTwin, adaptersFor, credentialPath, credentialPayloadFrom, rootCredentialInput, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from './root.js';
|
|
43
43
|
export type { DeployTwinOutcome, MaterializedRoot } from './root.js';
|
|
44
44
|
export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServeRecord, refreshSchedule, servedName, serveWorld, sessionsFile, keysFile, TOKEN_HEADER, WorldDoors } from './served-world.js';
|
|
45
45
|
export type { BranchDoors, BranchRow, DoorHost, MountedWorld, ServeRecord, WorldLayout } from './served-world.js';
|
|
@@ -51,3 +51,4 @@ export type { LocalBranchesOptions } from './local-branches.js';
|
|
|
51
51
|
export type { ServedWorld } from './served-world.js';
|
|
52
52
|
export { CONSOLE_BASE, consoleRedirect, serveConsoleApart, serveConsoleFor, type ConsoleMount } from './console-apart.js';
|
|
53
53
|
export { assertWorldStateRemovable } from './ancestry.js';
|
|
54
|
+
export { withoutVaultSignIn } from './runtime.js';
|
package/dist/src/index.js
CHANGED
|
@@ -29,10 +29,11 @@ export { advertiseWorldManifest, fetchRemoteManifest, MANIFEST_PATH, remoteAttac
|
|
|
29
29
|
export { resetWorld, seedEntryFor, seedWorld } from "./placeholder.js";
|
|
30
30
|
export { fetchFromOrigin, worldOrigin } from "./origin.js";
|
|
31
31
|
export { branchWorld, checkoutWorld } from "./branch.js";
|
|
32
|
-
export { deployWorld, refreshTwin, adaptersFor, credentialPath, credentialPayloadFrom, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from "./root.js";
|
|
32
|
+
export { deployWorld, refreshTwin, adaptersFor, credentialPath, credentialPayloadFrom, rootCredentialInput, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from "./root.js";
|
|
33
33
|
export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServeRecord, refreshSchedule, servedName, serveWorld, sessionsFile, keysFile, TOKEN_HEADER, WorldDoors } from "./served-world.js";
|
|
34
34
|
export { LocalBranches } from "./local-branches.js";
|
|
35
35
|
export { serveWorldView, VIEW_CONSOLE_BASE } from "./world-view.js";
|
|
36
36
|
export { LOCAL_ORIGIN_BASE, localWorldOrigin, pathOfWorld, worldDoorUnderConsole, worldOfHost } from "./world-origins.js";
|
|
37
37
|
export { CONSOLE_BASE, consoleRedirect, serveConsoleApart, serveConsoleFor } from "./console-apart.js";
|
|
38
38
|
export { assertWorldStateRemovable } from "./ancestry.js";
|
|
39
|
+
export { withoutVaultSignIn } from "./runtime.js";
|
package/dist/src/init.d.ts
CHANGED
|
@@ -202,6 +202,15 @@ export declare const APP_READ_ENDPOINT_ENV: Record<string, {
|
|
|
202
202
|
/** Render the private managed-infrastructure definition — deterministic, with no absolute paths.
|
|
203
203
|
* Only meaningful when `plan.compose` is non-empty (writeWorldInit skips it otherwise). */
|
|
204
204
|
export declare function renderComposeFile(plan: InitPlan): string;
|
|
205
|
+
export type ParsedEnvEntry = {
|
|
206
|
+
name: string;
|
|
207
|
+
value: string;
|
|
208
|
+
source: string;
|
|
209
|
+
};
|
|
210
|
+
/** Parse one dotenv-ish file into ordered NAME/value pairs. Deliberately conservative: the same
|
|
211
|
+
* `NAME=` line shape `projectEnvNames` detects, plus quote stripping, so the two views of a repo's
|
|
212
|
+
* env agree on which names exist. */
|
|
213
|
+
export declare function parseEnvFile(text: string, source: string): ParsedEnvEntry[];
|
|
205
214
|
export declare function planWorldInit(name: string, repoPath: string, options?: InitOptions): InitPlan;
|
|
206
215
|
/** The `app.env` rendering of the SAME map the config's `env` block holds — a dotenv for anything
|
|
207
216
|
* attached to the World (a shell, app process, or CI step), annotated with the
|
package/dist/src/init.js
CHANGED
|
@@ -409,16 +409,17 @@ export function renderComposeFile(plan) {
|
|
|
409
409
|
/** Parse one dotenv-ish file into ordered NAME/value pairs. Deliberately conservative: the same
|
|
410
410
|
* `NAME=` line shape `projectEnvNames` detects, plus quote stripping, so the two views of a repo's
|
|
411
411
|
* env agree on which names exist. */
|
|
412
|
-
function parseEnvFile(text, source) {
|
|
412
|
+
export function parseEnvFile(text, source) {
|
|
413
413
|
const entries = [];
|
|
414
414
|
for (const line of text.split(/\r?\n/)) {
|
|
415
415
|
const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=(.*)$/);
|
|
416
416
|
if (!match)
|
|
417
417
|
continue;
|
|
418
418
|
let value = match[2].trim();
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
419
|
+
// A quoted value ends at its closing quote; what follows it (` # a comment`) is not the value.
|
|
420
|
+
const quoted = value.match(/^(["'])(.*?)\1(?:\s+#.*)?$/);
|
|
421
|
+
if (quoted) {
|
|
422
|
+
value = quoted[2];
|
|
422
423
|
}
|
|
423
424
|
else {
|
|
424
425
|
// Unquoted: a `#` after whitespace opens a trailing comment (dotenv semantics). Without this
|
package/dist/src/root.d.ts
CHANGED
|
@@ -15,6 +15,18 @@ export declare function sealedCredentialInfo(worldRoot: string, vendor: string):
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** The signing secret sealed beside the credential (`{ headers, signingSecret }`), for ingest; null when none. */
|
|
17
17
|
export declare function twinSigningSecret(worldRoot: string, vendor: string): Promise<string | null>;
|
|
18
|
+
/**
|
|
19
|
+
* A root's credential when none is piped in. A vault is never required (decision 0033): signed in, the vault's
|
|
20
|
+
* `kv/vendors/<vendor>` record (its `credential` field: a token, or the payload's JSON) is the real credential, and a
|
|
21
|
+
* vault that answers but refuses stops the seal rather than falling back; otherwise, or when the vault holds none or
|
|
22
|
+
* cannot be reached, the one credential the repo's `.env` files hold for the vendor (`.env.local` over `.env`, root
|
|
23
|
+
* and workspace members). `notes` say why a source was passed over, and are always shown.
|
|
24
|
+
*/
|
|
25
|
+
export declare function rootCredentialInput(worldRoot: string, vendor: string, env?: NodeJS.ProcessEnv): Promise<{
|
|
26
|
+
input: string | null;
|
|
27
|
+
from: string;
|
|
28
|
+
notes: string[];
|
|
29
|
+
}>;
|
|
18
30
|
/** A bare token becomes the vendor's own header. A JSON object is taken as the payload. */
|
|
19
31
|
export declare function credentialPayloadFrom(input: string, vendor: string): CredentialPayload;
|
|
20
32
|
/** The root config carried in the state dir also names where the credential is sealed, so the
|
package/dist/src/root.js
CHANGED
|
@@ -8,14 +8,19 @@
|
|
|
8
8
|
// root through the pack's adapters over the kernel executor, with the world's checks in front;
|
|
9
9
|
// `deploy: auto` does the same the moment an entry is appended (the host installs the hook).
|
|
10
10
|
import { packEntry } from "./catalog.js";
|
|
11
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
12
|
-
import {
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
13
14
|
import { authStrategyFor, getActiveWorldStore, getPack, sealingKey, buildRemoteExecute, clearRoot, openSealedCredential, readRoot, registerStateSystem, sealCredential, stateDirName, stateSystemFor, validateRemoteOrigin, writeRoot, } from '@volter/world-core';
|
|
14
15
|
import { loadWorldConfig, writeWorldConfig } from "./configs.js";
|
|
15
16
|
import { changesetReadiness, collectObservations, foldObservations, loadChecks, openRootCredential, performEntries, rootCustody, protocolStanding } from '@volter/world-core';
|
|
16
17
|
import { findWorldChangeset, listWorldChangesets } from "./changeset.js";
|
|
17
18
|
import { importModule } from "./import-module.js";
|
|
18
19
|
import { statusWorld } from "./runtime.js";
|
|
20
|
+
import { envNameVendor } from "./covers.js";
|
|
21
|
+
import { parseEnvFile } from "./init.js";
|
|
22
|
+
import { fakeEnvValue } from "./fixture-env.js";
|
|
23
|
+
import { projectManifestDirs } from "./project-inspect.js";
|
|
19
24
|
import { findInstalledPackage, twinPackageName } from "./catalog.js";
|
|
20
25
|
// ── the user's sealing key ──────────────────────────────────────────────────────────────────
|
|
21
26
|
/** `$XDG_CONFIG_HOME/volter/kek`, else `~/.config/volter/kek`: the key every credential this user
|
|
@@ -45,6 +50,127 @@ export async function twinSigningSecret(worldRoot, vendor) {
|
|
|
45
50
|
const payload = await openSealedCredential(sealingKey(), JSON.parse(text), vendor);
|
|
46
51
|
return typeof payload.signingSecret === 'string' && payload.signingSecret.length >= 8 ? payload.signingSecret : null;
|
|
47
52
|
}
|
|
53
|
+
// ── where a root's credential comes from (company decision 0033) ────────────────────────────
|
|
54
|
+
/** A vault the person is signed in to, as `bao login` (or `vault login`) leaves it: an address, a token of the same
|
|
55
|
+
* CLI (`BAO_*` with `BAO_*`, `VAULT_*` with `VAULT_*`, else the CLI's token file), and its namespace if any. */
|
|
56
|
+
function signedInVault(env) {
|
|
57
|
+
for (const prefix of ['BAO', 'VAULT']) {
|
|
58
|
+
const addr = env[`${prefix}_ADDR`];
|
|
59
|
+
if (!addr)
|
|
60
|
+
continue;
|
|
61
|
+
let token = env[`${prefix}_TOKEN`];
|
|
62
|
+
if (!token) {
|
|
63
|
+
try {
|
|
64
|
+
token = readFileSync(join(homedir(), '.vault-token'), 'utf8').trim();
|
|
65
|
+
}
|
|
66
|
+
catch { /* none */ }
|
|
67
|
+
}
|
|
68
|
+
if (!token)
|
|
69
|
+
return null;
|
|
70
|
+
vaultAddress(`${prefix}_ADDR`, addr); // a token never travels in the clear off this machine
|
|
71
|
+
const namespace = env[`${prefix}_NAMESPACE`];
|
|
72
|
+
return { addr: addr.replace(/\/+$/, ''), token, ...(namespace ? { namespace } : {}) };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/** A vault's address: https, or http only to this machine; no userinfo. Any host or IP: a company vault often sits on a
|
|
77
|
+
* private name (`vault.acme.internal`, `10.1.2.3`), which a vendor's address may never be. */
|
|
78
|
+
function vaultAddress(variable, addr) {
|
|
79
|
+
let url;
|
|
80
|
+
try {
|
|
81
|
+
url = new URL(addr);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new Error(`${variable} is not a URL: ${addr}`);
|
|
85
|
+
}
|
|
86
|
+
if (url.username || url.password)
|
|
87
|
+
throw new Error(`${variable} must not carry credentials in its URL`);
|
|
88
|
+
// an IPv4 address in 127/8 (not a name that starts with "127."), localhost or [::1]
|
|
89
|
+
const loopback = url.hostname === 'localhost' || url.hostname === '[::1]' || /^127(\.\d{1,3}){3}$/.test(url.hostname);
|
|
90
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback))
|
|
91
|
+
throw new Error(`${variable} must be https (http only to this machine): ${url.origin}`);
|
|
92
|
+
}
|
|
93
|
+
// The suffixes that carry a credential itself; a publishable key, a webhook or signing secret, an id, a URL or a DSN is
|
|
94
|
+
// not what a root sends (covers.ts's broad list, less those).
|
|
95
|
+
const ROOT_SECRET_SUFFIX = /_(SECRET_ACCESS_KEY|SERVICE_ROLE_KEY|OAUTH_TOKEN|AUTH_TOKEN|ACCESS_TOKEN|BEARER_TOKEN|BOT_TOKEN|API_TOKEN|API_SECRET|API_KEY|APIKEY|SECRET_KEY|SECRET|TOKEN|KEY)$/;
|
|
96
|
+
// an OAuth app's client secret signs it in to the vendor; it is not the bearer a root sends
|
|
97
|
+
const NOT_A_ROOT_SECRET = /_(PUBLISHABLE_KEY|WEBHOOK_SECRET|SIGNING_SECRET|ACCESS_KEY_ID|CLIENT_ID|CLIENT_SECRET|CONSUMER_KEY|ACCOUNT_SID)$/;
|
|
98
|
+
/** A World's own fake for that name, or a twin's minted one: never a real credential. */
|
|
99
|
+
function isFakeValue(name, value) {
|
|
100
|
+
return value === fakeEnvValue(name) || /twin[-_]?fake|_twin_|twin_bootstrap|twin_sandbox/i.test(value);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A root's credential when none is piped in. A vault is never required (decision 0033): signed in, the vault's
|
|
104
|
+
* `kv/vendors/<vendor>` record (its `credential` field: a token, or the payload's JSON) is the real credential, and a
|
|
105
|
+
* vault that answers but refuses stops the seal rather than falling back; otherwise, or when the vault holds none or
|
|
106
|
+
* cannot be reached, the one credential the repo's `.env` files hold for the vendor (`.env.local` over `.env`, root
|
|
107
|
+
* and workspace members). `notes` say why a source was passed over, and are always shown.
|
|
108
|
+
*/
|
|
109
|
+
export async function rootCredentialInput(worldRoot, vendor, env = process.env) {
|
|
110
|
+
const notes = [];
|
|
111
|
+
const vault = signedInVault(env);
|
|
112
|
+
if (vault) {
|
|
113
|
+
const at = `${vault.addr}/v1/kv/data/vendors/${encodeURIComponent(vendor)}`;
|
|
114
|
+
let response;
|
|
115
|
+
try {
|
|
116
|
+
response = await fetch(at, { headers: { 'x-vault-token': vault.token, ...(vault.namespace ? { 'x-vault-namespace': vault.namespace } : {}) }, signal: AbortSignal.timeout(10_000) });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
notes.push(`the vault at ${vault.addr} did not answer (${error.message}; a private CA is not read from BAO_CACERT/VAULT_CACERT here)`);
|
|
120
|
+
}
|
|
121
|
+
if (response) {
|
|
122
|
+
if (response.status === 403)
|
|
123
|
+
throw new Error(`the vault at ${vault.addr} refused kv/vendors/${vendor} (403): sign in again, or with a token that may read it`);
|
|
124
|
+
if (response.ok) {
|
|
125
|
+
let credential;
|
|
126
|
+
try {
|
|
127
|
+
credential = (await response.json()).data?.data?.credential;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error(`the vault at ${vault.addr} answered kv/vendors/${vendor} with no JSON`);
|
|
131
|
+
}
|
|
132
|
+
if (typeof credential === 'string' && credential.trim())
|
|
133
|
+
return { input: credential, from: `vault ${vault.addr} kv/vendors/${vendor}`, notes };
|
|
134
|
+
if (credential && typeof credential === 'object' && !Array.isArray(credential) && typeof credential.headers === 'object') {
|
|
135
|
+
return { input: JSON.stringify(credential), from: `vault ${vault.addr} kv/vendors/${vendor}`, notes };
|
|
136
|
+
}
|
|
137
|
+
notes.push(`the vault's kv/vendors/${vendor} holds no credential (a token, or a payload with headers)`);
|
|
138
|
+
}
|
|
139
|
+
else if (response.status === 404)
|
|
140
|
+
notes.push(`the vault holds no kv/vendors/${vendor}`);
|
|
141
|
+
else
|
|
142
|
+
throw new Error(`the vault at ${vault.addr} answered ${response.status} for kv/vendors/${vendor}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else
|
|
146
|
+
notes.push('not signed in to a vault (BAO_ADDR or VAULT_ADDR, and a token)');
|
|
147
|
+
// every .env the repo's members hold; within a member, .env.local over .env
|
|
148
|
+
const found = new Map();
|
|
149
|
+
for (const dir of projectManifestDirs(resolve(worldRoot))) {
|
|
150
|
+
const member = new Map();
|
|
151
|
+
for (const file of ['.env', '.env.local']) {
|
|
152
|
+
const path = join(dir, file);
|
|
153
|
+
if (!existsSync(path))
|
|
154
|
+
continue;
|
|
155
|
+
for (const entry of parseEnvFile(readFileSync(path, 'utf8'), relative(resolve(worldRoot), path) || file)) {
|
|
156
|
+
if (envNameVendor(entry.name) !== vendor || !ROOT_SECRET_SUFFIX.test(entry.name) || NOT_A_ROOT_SECRET.test(entry.name))
|
|
157
|
+
continue;
|
|
158
|
+
if (!entry.value || isFakeValue(entry.name, entry.value))
|
|
159
|
+
continue;
|
|
160
|
+
member.set(entry.name, { value: entry.value, source: entry.source });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const [name, hit] of member)
|
|
164
|
+
found.set(`${name} (${hit.source})`, hit);
|
|
165
|
+
}
|
|
166
|
+
const values = new Set([...found.values()].map((hit) => hit.value));
|
|
167
|
+
if (values.size === 1)
|
|
168
|
+
return { input: [...values][0], from: [...found.keys()].join(', '), notes };
|
|
169
|
+
if (values.size > 1)
|
|
170
|
+
throw new Error(`the repo's .env files hold more than one ${vendor} credential (${[...found.keys()].join(', ')}); pipe the one to seal`);
|
|
171
|
+
notes.push(`the repo's .env files hold no ${vendor} credential`);
|
|
172
|
+
return { input: null, from: '', notes };
|
|
173
|
+
}
|
|
48
174
|
/** A bare token becomes the vendor's own header. A JSON object is taken as the payload. */
|
|
49
175
|
export function credentialPayloadFrom(input, vendor) {
|
|
50
176
|
const text = input.trim();
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -74,6 +74,10 @@ export type SharingClaim = {
|
|
|
74
74
|
};
|
|
75
75
|
/** Exported for direct drive: the liveness rule, pure over a claim. */
|
|
76
76
|
export declare function sharingClaimAlive(claim: SharingClaim): boolean;
|
|
77
|
+
/** A person's vault sign-in (`BAO_*`, `VAULT_*`) opens their real credentials (company decision 0033). The processes that
|
|
78
|
+
* run code that is not Volter's (a World's apps, a command attached to a World, a tunnel, a dependency install) are
|
|
79
|
+
* given an environment without it. */
|
|
80
|
+
export declare function withoutVaultSignIn<T extends Record<string, string | undefined>>(env: T): T;
|
|
77
81
|
/** The process's environment as the world admits it: what `stripEnv` names stays outside. */
|
|
78
82
|
export declare function admittedEnv(strip: string[] | undefined, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
79
83
|
export declare function saveWorldInstance(instance: WorldInstance): void;
|
package/dist/src/runtime.js
CHANGED
|
@@ -514,6 +514,16 @@ const CONTROL_PLANE_EGRESS_ENV = [
|
|
|
514
514
|
'https_proxy',
|
|
515
515
|
'all_proxy',
|
|
516
516
|
];
|
|
517
|
+
/** A person's vault sign-in (`BAO_*`, `VAULT_*`) opens their real credentials (company decision 0033). The processes that
|
|
518
|
+
* run code that is not Volter's (a World's apps, a command attached to a World, a tunnel, a dependency install) are
|
|
519
|
+
* given an environment without it. */
|
|
520
|
+
export function withoutVaultSignIn(env) {
|
|
521
|
+
const out = { ...env };
|
|
522
|
+
for (const name of Object.keys(out))
|
|
523
|
+
if (/^(BAO|VAULT)_/.test(name))
|
|
524
|
+
delete out[name];
|
|
525
|
+
return out;
|
|
526
|
+
}
|
|
517
527
|
/** The process's environment as the world admits it: what `stripEnv` names stays outside. */
|
|
518
528
|
export function admittedEnv(strip, env = process.env) {
|
|
519
529
|
const out = {};
|
|
@@ -529,7 +539,7 @@ export function admittedEnv(strip, env = process.env) {
|
|
|
529
539
|
continue;
|
|
530
540
|
out[k] = v;
|
|
531
541
|
}
|
|
532
|
-
return out;
|
|
542
|
+
return withoutVaultSignIn(out);
|
|
533
543
|
}
|
|
534
544
|
// The strip list of a running world, for `env`/`attach`: its config is the one the instance recorded.
|
|
535
545
|
function stripEnvOf(instanceEnv) {
|
|
@@ -2533,7 +2543,7 @@ async function shareWorldReserved(name, options) {
|
|
|
2533
2543
|
const out = openPrivateLog(log, 'w');
|
|
2534
2544
|
const child = spawn(tunnel.command, tunnel.args, { windowsHide: true,
|
|
2535
2545
|
cwd: root,
|
|
2536
|
-
env: process.env,
|
|
2546
|
+
env: withoutVaultSignIn(process.env),
|
|
2537
2547
|
detached: true,
|
|
2538
2548
|
stdio: ['ignore', out, out],
|
|
2539
2549
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.27",
|
|
4
4
|
"description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
package/src/cli.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { activateScript, branchWorld, checkoutWorld, fetchFromOrigin, pushWorldC
|
|
|
9
9
|
import { formatApplication, formatChangeset, formatRebaseReport, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, parseVerifierExpression } from '@volter/world-core';
|
|
10
10
|
import { advertiseWorldManifest, fetchRemoteManifest, MANIFEST_PATH, remoteAttachEnv, startManifestServer } from './serve.ts';
|
|
11
11
|
import type { WorldManifest } from './attach.ts';
|
|
12
|
-
import { clockFile, instanceDir, pruneWorlds } from './runtime.ts';
|
|
12
|
+
import { clockFile, instanceDir, pruneWorlds, withoutVaultSignIn } from './runtime.ts';
|
|
13
13
|
import { worldResourceReport } from './resources.ts';
|
|
14
14
|
import { worldConsumers, worldUsage } from './consumers.ts';
|
|
15
15
|
import { addAppHosts, appUrlUnsetMessage, detectAppUrl, readAppUrl, setAppUrl } from './app-url.ts';
|
|
@@ -866,7 +866,7 @@ async function main(): Promise<void> {
|
|
|
866
866
|
const { bridgeStreams, drainStreams } = createRequire(import.meta.url)('@volter/world-core/stream-bridge') as { bridgeStreams: (streams: WorldManifest['streams'], token: string) => Promise<{ env: Record<string, string> }>; drainStreams: (ms?: number) => Promise<void> };
|
|
867
867
|
const bridged = manifest.streams ? await bridgeStreams(manifest.streams, attachToken ?? manifest.env.VOLTER_TWINS_KEY ?? '') : { env: {} };
|
|
868
868
|
// VOLTER_WORLD names the World to the injector: a request addressed to its paths carries its key
|
|
869
|
-
const commandEnv: Record<string, string | undefined> = { ...process.env, ...env, ...bridged.env, VOLTER_WORLD: resolved.ref };
|
|
869
|
+
const commandEnv: Record<string, string | undefined> = withoutVaultSignIn({ ...process.env, ...env, ...bridged.env, VOLTER_WORLD: resolved.ref });
|
|
870
870
|
commandEnv.VOLTER_TWIN_INJECT_QUIET = head.includes('--verbose') ? '0' : commandEnv.VOLTER_TWIN_INJECT_QUIET ?? '1';
|
|
871
871
|
const child = spawn(command[0]!, command.slice(1), { windowsHide: true, env: commandEnv, stdio: 'inherit' });
|
|
872
872
|
const code = await new Promise<number>((resolveExit) => { child.on('exit', (status) => resolveExit(status ?? 1)); child.on('error', () => resolveExit(127)); });
|
|
@@ -886,7 +886,7 @@ async function main(): Promise<void> {
|
|
|
886
886
|
if (command.length === 0) throw new Error('volter-world attach: pass the command after --');
|
|
887
887
|
const manifest = readReflectManifest(attachRoot, resolved.ref);
|
|
888
888
|
if (!manifest) throw new Error(`volter-world attach --via reflect: no reflect front is running for world ${resolved.ref} — start one: volter-world reflect ${resolved.ref} --target-ip <ip> --port 443 --resolver-port 53`);
|
|
889
|
-
const env: Record<string, string> = { ...process.env as Record<string, string>, VOLTER_WORLD: resolved.ref, VOLTER_REFLECT_DNS: manifest.targetIp, VOLTER_WORLD_CA: manifest.caCertPath };
|
|
889
|
+
const env: Record<string, string> = { ...withoutVaultSignIn(process.env) as Record<string, string>, VOLTER_WORLD: resolved.ref, VOLTER_REFLECT_DNS: manifest.targetIp, VOLTER_WORLD_CA: manifest.caCertPath };
|
|
890
890
|
Object.assign(env, sessionTrustEnv(manifest.caCertPath, env));
|
|
891
891
|
let toRun = command;
|
|
892
892
|
const compose = splitDockerComposeArgs(command);
|
package/src/index.ts
CHANGED
|
@@ -161,7 +161,7 @@ export type { FetchOptions, FetchOutcome } from './origin.ts';
|
|
|
161
161
|
|
|
162
162
|
export { branchWorld, checkoutWorld } from './branch.ts';
|
|
163
163
|
export type { BranchOptions } from './branch.ts';
|
|
164
|
-
export { deployWorld, refreshTwin, adaptersFor, credentialPath, credentialPayloadFrom, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from './root.ts';
|
|
164
|
+
export { deployWorld, refreshTwin, adaptersFor, credentialPath, credentialPayloadFrom, rootCredentialInput, deployTwin, loadWorldChecks, materializeRoots, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, userKekPath } from './root.ts';
|
|
165
165
|
export type { DeployTwinOutcome, MaterializedRoot } from './root.ts';
|
|
166
166
|
export { landReceipts, mountWorld, READ_ONLY_HEADER, worldOriginLabel, readServeRecord, refreshSchedule, servedName, serveWorld, sessionsFile, keysFile, TOKEN_HEADER, WorldDoors } from './served-world.ts';
|
|
167
167
|
export type { BranchDoors, BranchRow, DoorHost, MountedWorld, ServeRecord, WorldLayout } from './served-world.ts';
|
|
@@ -174,3 +174,5 @@ export type { ServedWorld } from './served-world.ts';
|
|
|
174
174
|
export { CONSOLE_BASE, consoleRedirect, serveConsoleApart, serveConsoleFor, type ConsoleMount } from './console-apart.ts';
|
|
175
175
|
|
|
176
176
|
export { assertWorldStateRemovable } from './ancestry.ts';
|
|
177
|
+
|
|
178
|
+
export { withoutVaultSignIn } from './runtime.ts';
|
package/src/init.ts
CHANGED
|
@@ -607,20 +607,21 @@ export function renderComposeFile(plan: InitPlan): string {
|
|
|
607
607
|
return `${lines.join('\n')}\n`;
|
|
608
608
|
}
|
|
609
609
|
|
|
610
|
-
type ParsedEnvEntry = { name: string; value: string; source: string };
|
|
610
|
+
export type ParsedEnvEntry = { name: string; value: string; source: string };
|
|
611
611
|
|
|
612
612
|
/** Parse one dotenv-ish file into ordered NAME/value pairs. Deliberately conservative: the same
|
|
613
613
|
* `NAME=` line shape `projectEnvNames` detects, plus quote stripping, so the two views of a repo's
|
|
614
614
|
* env agree on which names exist. */
|
|
615
|
-
function parseEnvFile(text: string, source: string): ParsedEnvEntry[] {
|
|
615
|
+
export function parseEnvFile(text: string, source: string): ParsedEnvEntry[] {
|
|
616
616
|
const entries: ParsedEnvEntry[] = [];
|
|
617
617
|
for (const line of text.split(/\r?\n/)) {
|
|
618
618
|
const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=(.*)$/);
|
|
619
619
|
if (!match) continue;
|
|
620
620
|
let value = match[2]!.trim();
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
621
|
+
// A quoted value ends at its closing quote; what follows it (` # a comment`) is not the value.
|
|
622
|
+
const quoted = value.match(/^(["'])(.*?)\1(?:\s+#.*)?$/);
|
|
623
|
+
if (quoted) {
|
|
624
|
+
value = quoted[2]!;
|
|
624
625
|
} else {
|
|
625
626
|
// Unquoted: a `#` after whitespace opens a trailing comment (dotenv semantics). Without this
|
|
626
627
|
// a line like `NEXTAUTH_URL=http://localhost:3000 # dev` yields a value with the comment
|
package/src/root.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// root through the pack's adapters over the kernel executor, with the world's checks in front;
|
|
9
9
|
// `deploy: auto` does the same the moment an entry is appended (the host installs the hook).
|
|
10
10
|
import { packEntry } from './catalog.ts';
|
|
11
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
12
|
-
import {
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
13
14
|
import {
|
|
14
15
|
NO_SECRETS_CHECK, authStrategyFor, getActiveWorldStore, getPack, sealingKey, buildRemoteExecute, clearRoot, observeAppends, openSealedCredential, readRoot, registerStateSystem, sealCredential, stateDirName, stateSystemFor, type Check, type CredentialPayload, type PerformAction, type PushReport, type RemoteExecute, type RootConfig, type SealedCredential, type TwinAction, validateRemoteOrigin, writeRoot,
|
|
15
16
|
} from '@volter/world-core';
|
|
@@ -18,6 +19,10 @@ import { changesetReadiness, collectObservations, type Observation, foldObservat
|
|
|
18
19
|
import { findWorldChangeset, listWorldChangesets } from './changeset.ts';
|
|
19
20
|
import { importModule } from './import-module.ts';
|
|
20
21
|
import { statusWorld } from './runtime.ts';
|
|
22
|
+
import { envNameVendor } from './covers.ts';
|
|
23
|
+
import { parseEnvFile } from './init.ts';
|
|
24
|
+
import { fakeEnvValue } from './fixture-env.ts';
|
|
25
|
+
import { projectManifestDirs } from './project-inspect.ts';
|
|
21
26
|
import { findInstalledPackage, twinPackageName } from './catalog.ts';
|
|
22
27
|
import type { WorldConfig } from './schema.ts';
|
|
23
28
|
|
|
@@ -51,6 +56,100 @@ export async function twinSigningSecret(worldRoot: string, vendor: string): Prom
|
|
|
51
56
|
return typeof payload.signingSecret === 'string' && payload.signingSecret.length >= 8 ? payload.signingSecret : null;
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
// ── where a root's credential comes from (company decision 0033) ────────────────────────────
|
|
60
|
+
|
|
61
|
+
/** A vault the person is signed in to, as `bao login` (or `vault login`) leaves it: an address, a token of the same
|
|
62
|
+
* CLI (`BAO_*` with `BAO_*`, `VAULT_*` with `VAULT_*`, else the CLI's token file), and its namespace if any. */
|
|
63
|
+
function signedInVault(env: NodeJS.ProcessEnv): { addr: string; token: string; namespace?: string } | null {
|
|
64
|
+
for (const prefix of ['BAO', 'VAULT'] as const) {
|
|
65
|
+
const addr = env[`${prefix}_ADDR`];
|
|
66
|
+
if (!addr) continue;
|
|
67
|
+
let token = env[`${prefix}_TOKEN`];
|
|
68
|
+
if (!token) { try { token = readFileSync(join(homedir(), '.vault-token'), 'utf8').trim(); } catch { /* none */ } }
|
|
69
|
+
if (!token) return null;
|
|
70
|
+
vaultAddress(`${prefix}_ADDR`, addr); // a token never travels in the clear off this machine
|
|
71
|
+
const namespace = env[`${prefix}_NAMESPACE`];
|
|
72
|
+
return { addr: addr.replace(/\/+$/, ''), token, ...(namespace ? { namespace } : {}) };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A vault's address: https, or http only to this machine; no userinfo. Any host or IP: a company vault often sits on a
|
|
78
|
+
* private name (`vault.acme.internal`, `10.1.2.3`), which a vendor's address may never be. */
|
|
79
|
+
function vaultAddress(variable: string, addr: string): void {
|
|
80
|
+
let url: URL;
|
|
81
|
+
try { url = new URL(addr); } catch { throw new Error(`${variable} is not a URL: ${addr}`); }
|
|
82
|
+
if (url.username || url.password) throw new Error(`${variable} must not carry credentials in its URL`);
|
|
83
|
+
// an IPv4 address in 127/8 (not a name that starts with "127."), localhost or [::1]
|
|
84
|
+
const loopback = url.hostname === 'localhost' || url.hostname === '[::1]' || /^127(\.\d{1,3}){3}$/.test(url.hostname);
|
|
85
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) throw new Error(`${variable} must be https (http only to this machine): ${url.origin}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The suffixes that carry a credential itself; a publishable key, a webhook or signing secret, an id, a URL or a DSN is
|
|
89
|
+
// not what a root sends (covers.ts's broad list, less those).
|
|
90
|
+
const ROOT_SECRET_SUFFIX = /_(SECRET_ACCESS_KEY|SERVICE_ROLE_KEY|OAUTH_TOKEN|AUTH_TOKEN|ACCESS_TOKEN|BEARER_TOKEN|BOT_TOKEN|API_TOKEN|API_SECRET|API_KEY|APIKEY|SECRET_KEY|SECRET|TOKEN|KEY)$/;
|
|
91
|
+
// an OAuth app's client secret signs it in to the vendor; it is not the bearer a root sends
|
|
92
|
+
const NOT_A_ROOT_SECRET = /_(PUBLISHABLE_KEY|WEBHOOK_SECRET|SIGNING_SECRET|ACCESS_KEY_ID|CLIENT_ID|CLIENT_SECRET|CONSUMER_KEY|ACCOUNT_SID)$/;
|
|
93
|
+
/** A World's own fake for that name, or a twin's minted one: never a real credential. */
|
|
94
|
+
function isFakeValue(name: string, value: string): boolean {
|
|
95
|
+
return value === fakeEnvValue(name) || /twin[-_]?fake|_twin_|twin_bootstrap|twin_sandbox/i.test(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A root's credential when none is piped in. A vault is never required (decision 0033): signed in, the vault's
|
|
100
|
+
* `kv/vendors/<vendor>` record (its `credential` field: a token, or the payload's JSON) is the real credential, and a
|
|
101
|
+
* vault that answers but refuses stops the seal rather than falling back; otherwise, or when the vault holds none or
|
|
102
|
+
* cannot be reached, the one credential the repo's `.env` files hold for the vendor (`.env.local` over `.env`, root
|
|
103
|
+
* and workspace members). `notes` say why a source was passed over, and are always shown.
|
|
104
|
+
*/
|
|
105
|
+
export async function rootCredentialInput(worldRoot: string, vendor: string, env: NodeJS.ProcessEnv = process.env): Promise<{ input: string | null; from: string; notes: string[] }> {
|
|
106
|
+
const notes: string[] = [];
|
|
107
|
+
const vault = signedInVault(env);
|
|
108
|
+
if (vault) {
|
|
109
|
+
const at = `${vault.addr}/v1/kv/data/vendors/${encodeURIComponent(vendor)}`;
|
|
110
|
+
let response: Response | undefined;
|
|
111
|
+
try {
|
|
112
|
+
response = await fetch(at, { headers: { 'x-vault-token': vault.token, ...(vault.namespace ? { 'x-vault-namespace': vault.namespace } : {}) }, signal: AbortSignal.timeout(10_000) });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
notes.push(`the vault at ${vault.addr} did not answer (${(error as Error).message}; a private CA is not read from BAO_CACERT/VAULT_CACERT here)`);
|
|
115
|
+
}
|
|
116
|
+
if (response) {
|
|
117
|
+
if (response.status === 403) throw new Error(`the vault at ${vault.addr} refused kv/vendors/${vendor} (403): sign in again, or with a token that may read it`);
|
|
118
|
+
if (response.ok) {
|
|
119
|
+
let credential: unknown;
|
|
120
|
+
try { credential = ((await response.json()) as { data?: { data?: { credential?: unknown } } }).data?.data?.credential; }
|
|
121
|
+
catch { throw new Error(`the vault at ${vault.addr} answered kv/vendors/${vendor} with no JSON`); }
|
|
122
|
+
if (typeof credential === 'string' && credential.trim()) return { input: credential, from: `vault ${vault.addr} kv/vendors/${vendor}`, notes };
|
|
123
|
+
if (credential && typeof credential === 'object' && !Array.isArray(credential) && typeof (credential as { headers?: unknown }).headers === 'object') {
|
|
124
|
+
return { input: JSON.stringify(credential), from: `vault ${vault.addr} kv/vendors/${vendor}`, notes };
|
|
125
|
+
}
|
|
126
|
+
notes.push(`the vault's kv/vendors/${vendor} holds no credential (a token, or a payload with headers)`);
|
|
127
|
+
} else if (response.status === 404) notes.push(`the vault holds no kv/vendors/${vendor}`);
|
|
128
|
+
else throw new Error(`the vault at ${vault.addr} answered ${response.status} for kv/vendors/${vendor}`);
|
|
129
|
+
}
|
|
130
|
+
} else notes.push('not signed in to a vault (BAO_ADDR or VAULT_ADDR, and a token)');
|
|
131
|
+
// every .env the repo's members hold; within a member, .env.local over .env
|
|
132
|
+
const found = new Map<string, { value: string; source: string }>();
|
|
133
|
+
for (const dir of projectManifestDirs(resolve(worldRoot))) {
|
|
134
|
+
const member = new Map<string, { value: string; source: string }>();
|
|
135
|
+
for (const file of ['.env', '.env.local']) {
|
|
136
|
+
const path = join(dir, file);
|
|
137
|
+
if (!existsSync(path)) continue;
|
|
138
|
+
for (const entry of parseEnvFile(readFileSync(path, 'utf8'), relative(resolve(worldRoot), path) || file)) {
|
|
139
|
+
if (envNameVendor(entry.name) !== vendor || !ROOT_SECRET_SUFFIX.test(entry.name) || NOT_A_ROOT_SECRET.test(entry.name)) continue;
|
|
140
|
+
if (!entry.value || isFakeValue(entry.name, entry.value)) continue;
|
|
141
|
+
member.set(entry.name, { value: entry.value, source: entry.source });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const [name, hit] of member) found.set(`${name} (${hit.source})`, hit);
|
|
145
|
+
}
|
|
146
|
+
const values = new Set([...found.values()].map((hit) => hit.value));
|
|
147
|
+
if (values.size === 1) return { input: [...values][0]!, from: [...found.keys()].join(', '), notes };
|
|
148
|
+
if (values.size > 1) throw new Error(`the repo's .env files hold more than one ${vendor} credential (${[...found.keys()].join(', ')}); pipe the one to seal`);
|
|
149
|
+
notes.push(`the repo's .env files hold no ${vendor} credential`);
|
|
150
|
+
return { input: null, from: '', notes };
|
|
151
|
+
}
|
|
152
|
+
|
|
54
153
|
/** A bare token becomes the vendor's own header. A JSON object is taken as the payload. */
|
|
55
154
|
export function credentialPayloadFrom(input: string, vendor: string): CredentialPayload {
|
|
56
155
|
const text = input.trim();
|
package/src/runtime.ts
CHANGED
|
@@ -561,6 +561,15 @@ const CONTROL_PLANE_EGRESS_ENV = [
|
|
|
561
561
|
'all_proxy',
|
|
562
562
|
];
|
|
563
563
|
|
|
564
|
+
/** A person's vault sign-in (`BAO_*`, `VAULT_*`) opens their real credentials (company decision 0033). The processes that
|
|
565
|
+
* run code that is not Volter's (a World's apps, a command attached to a World, a tunnel, a dependency install) are
|
|
566
|
+
* given an environment without it. */
|
|
567
|
+
export function withoutVaultSignIn<T extends Record<string, string | undefined>>(env: T): T {
|
|
568
|
+
const out = { ...env };
|
|
569
|
+
for (const name of Object.keys(out)) if (/^(BAO|VAULT)_/.test(name)) delete out[name];
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
|
|
564
573
|
/** The process's environment as the world admits it: what `stripEnv` names stays outside. */
|
|
565
574
|
export function admittedEnv(strip: string[] | undefined, env: NodeJS.ProcessEnv = process.env): Record<string, string> {
|
|
566
575
|
const out: Record<string, string> = {};
|
|
@@ -573,7 +582,7 @@ export function admittedEnv(strip: string[] | undefined, env: NodeJS.ProcessEnv
|
|
|
573
582
|
if (rules.some((r) => ('prefix' in r ? k.startsWith(r.prefix) : k === r.name))) continue;
|
|
574
583
|
out[k] = v;
|
|
575
584
|
}
|
|
576
|
-
return out;
|
|
585
|
+
return withoutVaultSignIn(out);
|
|
577
586
|
}
|
|
578
587
|
// The strip list of a running world, for `env`/`attach`: its config is the one the instance recorded.
|
|
579
588
|
function stripEnvOf(instanceEnv: Record<string, string>): string[] | undefined {
|
|
@@ -2485,7 +2494,7 @@ async function shareWorldReserved(name: string, options: ShareWorldOptions): Pro
|
|
|
2485
2494
|
const out = openPrivateLog(log, 'w');
|
|
2486
2495
|
const child = spawn(tunnel.command, tunnel.args, { windowsHide: true,
|
|
2487
2496
|
cwd: root,
|
|
2488
|
-
env: process.env,
|
|
2497
|
+
env: withoutVaultSignIn(process.env),
|
|
2489
2498
|
detached: true,
|
|
2490
2499
|
stdio: ['ignore', out, out],
|
|
2491
2500
|
});
|