@volter/world-runtime 2.0.26 → 2.0.28

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 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);
@@ -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";
@@ -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
- if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2)
420
- || (value.startsWith("'") && value.endsWith("'") && value.length >= 2)) {
421
- value = value.slice(1, -1);
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
@@ -12,9 +12,22 @@ export declare function sealTwinCredential(worldRoot: string, vendor: string, pa
12
12
  export declare function sealedCredentialInfo(worldRoot: string, vendor: string): {
13
13
  placedAt: string;
14
14
  fingerprint: string;
15
+ sealedBy: 'vault' | 'key';
15
16
  } | null;
16
17
  /** The signing secret sealed beside the credential (`{ headers, signingSecret }`), for ingest; null when none. */
17
18
  export declare function twinSigningSecret(worldRoot: string, vendor: string): Promise<string | null>;
19
+ /**
20
+ * A root's credential when none is piped in. A vault is never required (decision 0033): signed in, the vault's
21
+ * `kv/vendors/<vendor>` record (its `credential` field: a token, or the payload's JSON) is the real credential, and a
22
+ * vault that answers but refuses stops the seal rather than falling back; otherwise, or when the vault holds none or
23
+ * cannot be reached, the one credential the repo's `.env` files hold for the vendor (`.env.local` over `.env`, root
24
+ * and workspace members). `notes` say why a source was passed over, and are always shown.
25
+ */
26
+ export declare function rootCredentialInput(worldRoot: string, vendor: string, env?: NodeJS.ProcessEnv): Promise<{
27
+ input: string | null;
28
+ from: string;
29
+ notes: string[];
30
+ }>;
18
31
  /** A bare token becomes the vendor's own header. A JSON object is taken as the payload. */
19
32
  export declare function credentialPayloadFrom(input: string, vendor: string): CredentialPayload;
20
33
  /** 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 { dirname, join, resolve } from 'node:path';
13
- import { authStrategyFor, getActiveWorldStore, getPack, sealingKey, buildRemoteExecute, clearRoot, openSealedCredential, readRoot, registerStateSystem, sealCredential, stateDirName, stateSystemFor, validateRemoteOrigin, writeRoot, } from '@volter/world-core';
11
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { dirname, join, relative, resolve } from 'node:path';
14
+ import { authStrategyFor, currentSealer, openSealedAt, getActiveWorldStore, getPack, buildRemoteExecute, clearRoot, readRoot, registerStateSystem, 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
@@ -26,7 +31,7 @@ export function credentialPath(worldRoot, vendor) {
26
31
  return join(resolve(worldRoot), stateDirName(), 'credentials', `${vendor}.json`);
27
32
  }
28
33
  export async function sealTwinCredential(worldRoot, vendor, payload, at = new Date().toISOString()) {
29
- const sealed = await sealCredential(sealingKey(), payload, at, vendor);
34
+ const sealed = await currentSealer().seal(payload, at, vendor);
30
35
  getActiveWorldStore().write(credentialPath(worldRoot, vendor), `${JSON.stringify(sealed, null, 2)}\n`, { secret: true });
31
36
  return { placedAt: sealed.placedAt, fingerprint: sealed.fingerprint };
32
37
  }
@@ -35,16 +40,137 @@ export function sealedCredentialInfo(worldRoot, vendor) {
35
40
  if (text === null)
36
41
  return null;
37
42
  const sealed = JSON.parse(text);
38
- return { placedAt: sealed.placedAt, fingerprint: sealed.fingerprint };
43
+ // what wraps it: a vault's transit key (v: 2) or a key the host holds (v: 1), so an operator sees what still needs one
44
+ return { placedAt: sealed.placedAt, fingerprint: sealed.fingerprint, sealedBy: sealed.v === 2 ? 'vault' : 'key' };
39
45
  }
40
46
  /** The signing secret sealed beside the credential (`{ headers, signingSecret }`), for ingest; null when none. */
41
47
  export async function twinSigningSecret(worldRoot, vendor) {
42
- const text = getActiveWorldStore().read(credentialPath(worldRoot, vendor));
43
- if (text === null)
48
+ const payload = await openSealedAt(credentialPath(worldRoot, vendor), vendor);
49
+ if (payload === null)
44
50
  return null;
45
- 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();
@@ -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;
@@ -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
  });
@@ -26,7 +26,7 @@ import { getActiveWorldStore, withAncestryLock, captureHistory, historyAtInstant
26
26
  // to `.volter/token` and `.volter/token.read` (0600).
27
27
  // One writer per world by construction: the doors run in this process, serialized per twin.
28
28
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
29
- import { blobDigest, getActiveBlobStore, loadCheck, mirrorShellUnder, openSealedCredential, readTwinRequestJournal, sealingKey, serveHttp, TWIN_PREFIX_HEADER, validateRemoteOrigin } from '@volter/world-core';
29
+ import { blobDigest, openSealedAt, getActiveBlobStore, loadCheck, mirrorShellUnder, readTwinRequestJournal, serveHttp, TWIN_PREFIX_HEADER, validateRemoteOrigin } from '@volter/world-core';
30
30
  import { APP_READ_ENDPOINT_ENV } from "./init.js";
31
31
  import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass } from '@volter/world-access';
32
32
  import { createHash } from 'node:crypto';
@@ -912,10 +912,9 @@ export class WorldDoors {
912
912
  * token removed and the sealed credential's headers replacing any of the same name. The vendor's
913
913
  * answer (status, headers, body) is streamed back as it arrives. */
914
914
  async forward(request, url, rest, name, origin) {
915
- const sealed = getActiveWorldStore().read(credentialPath(this.worldRoot, name));
916
- if (sealed === null)
915
+ const credential = await openSealedAt(credentialPath(this.worldRoot, name), name);
916
+ if (credential === null)
917
917
  return Response.json({ error: `the ${name} link has no credential sealed` }, { status: 409 });
918
- const credential = await openSealedCredential(sealingKey(), JSON.parse(sealed), name);
919
918
  const base = validateRemoteOrigin(origin);
920
919
  const target = new URL(`${base.origin}/${rest}${url.search}`);
921
920
  if (target.origin !== base.origin)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "2.0.26",
3
+ "version": "2.0.28",
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",
@@ -67,14 +67,14 @@
67
67
  "node": ">=22.3"
68
68
  },
69
69
  "peerDependencies": {
70
- "@volter/world-core": "2.0.15",
70
+ "@volter/world-core": "2.0.16",
71
71
  "@volter/world-console": "2.0.10"
72
72
  },
73
73
  "dependencies": {
74
74
  "@electric-sql/pglite": "0.5.8",
75
75
  "@electric-sql/pglite-pgvector": "0.0.9",
76
76
  "@volter/world-access": "2.0.2",
77
- "@volter/world-core": "2.0.15",
77
+ "@volter/world-core": "2.0.16",
78
78
  "pg-gateway": "0.3.0-beta.4",
79
79
  "smol-toml": "^1.8.0"
80
80
  },
@@ -84,6 +84,6 @@
84
84
  }
85
85
  },
86
86
  "optionalDependencies": {
87
- "@volter/twin-mongodb": "0.1.15"
87
+ "@volter/twin-mongodb": "0.1.16"
88
88
  }
89
89
  }
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
- if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2)
622
- || (value.startsWith("'") && value.endsWith("'") && value.length >= 2)) {
623
- value = value.slice(1, -1);
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,16 +8,21 @@
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 { basename, dirname, join, resolve } from 'node:path';
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
- 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
+ NO_SECRETS_CHECK, authStrategyFor, currentSealer, openSealedAt, getActiveWorldStore, getPack, buildRemoteExecute, clearRoot, observeAppends, readRoot, registerStateSystem, 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';
16
17
  import { loadWorldConfig, writeWorldConfig } from './configs.ts';
17
18
  import { changesetReadiness, collectObservations, type Observation, foldObservations, loadChecks, openRootCredential, performEntries, rootCustody, protocolStanding, userKek, type DeployReport, type ObserveReport } from '@volter/world-core';
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
 
@@ -33,24 +38,118 @@ export function credentialPath(worldRoot: string, vendor: string): string {
33
38
  return join(resolve(worldRoot), stateDirName(), 'credentials', `${vendor}.json`);
34
39
  }
35
40
  export async function sealTwinCredential(worldRoot: string, vendor: string, payload: CredentialPayload, at: string = new Date().toISOString()): Promise<{ placedAt: string; fingerprint: string }> {
36
- const sealed = await sealCredential(sealingKey(), payload, at, vendor);
41
+ const sealed = await currentSealer().seal(payload, at, vendor);
37
42
  getActiveWorldStore().write(credentialPath(worldRoot, vendor), `${JSON.stringify(sealed, null, 2)}\n`, { secret: true });
38
43
  return { placedAt: sealed.placedAt, fingerprint: sealed.fingerprint };
39
44
  }
40
- export function sealedCredentialInfo(worldRoot: string, vendor: string): { placedAt: string; fingerprint: string } | null {
45
+ export function sealedCredentialInfo(worldRoot: string, vendor: string): { placedAt: string; fingerprint: string; sealedBy: 'vault' | 'key' } | null {
41
46
  const text = getActiveWorldStore().read(credentialPath(worldRoot, vendor));
42
47
  if (text === null) return null;
43
48
  const sealed = JSON.parse(text) as SealedCredential;
44
- return { placedAt: sealed.placedAt, fingerprint: sealed.fingerprint };
49
+ // what wraps it: a vault's transit key (v: 2) or a key the host holds (v: 1), so an operator sees what still needs one
50
+ return { placedAt: sealed.placedAt, fingerprint: sealed.fingerprint, sealedBy: sealed.v === 2 ? 'vault' : 'key' };
45
51
  }
46
52
  /** The signing secret sealed beside the credential (`{ headers, signingSecret }`), for ingest; null when none. */
47
53
  export async function twinSigningSecret(worldRoot: string, vendor: string): Promise<string | null> {
48
- const text = getActiveWorldStore().read(credentialPath(worldRoot, vendor));
49
- if (text === null) return null;
50
- const payload = await openSealedCredential(sealingKey(), JSON.parse(text) as SealedCredential, vendor);
54
+ const payload = await openSealedAt(credentialPath(worldRoot, vendor), vendor);
55
+ if (payload === null) return null;
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
  });
@@ -26,7 +26,7 @@ import { getActiveWorldStore, withAncestryLock, captureHistory, historyAtInstant
26
26
  // to `.volter/token` and `.volter/token.read` (0600).
27
27
  // One writer per world by construction: the doors run in this process, serialized per twin.
28
28
  import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync , rmSync} from 'node:fs';
29
- import { blobDigest, getActiveBlobStore, loadCheck, mirrorShellUnder, openSealedCredential, readTwinRequestJournal, sealingKey, serveHttp, TWIN_PREFIX_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core';
29
+ import { blobDigest, openSealedAt, getActiveBlobStore, loadCheck, mirrorShellUnder, readTwinRequestJournal, serveHttp, TWIN_PREFIX_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core';
30
30
  import { APP_READ_ENDPOINT_ENV } from './init.ts';
31
31
  import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass, type TrustedIssuer, type WorldCredentials } from '@volter/world-access';
32
32
  import { createHash } from 'node:crypto';
@@ -817,9 +817,8 @@ export class WorldDoors {
817
817
  * token removed and the sealed credential's headers replacing any of the same name. The vendor's
818
818
  * answer (status, headers, body) is streamed back as it arrives. */
819
819
  private async forward(request: Request, url: URL, rest: string, name: string, origin: string): Promise<Response> {
820
- const sealed = getActiveWorldStore().read(credentialPath(this.worldRoot, name));
821
- if (sealed === null) return Response.json({ error: `the ${name} link has no credential sealed` }, { status: 409 });
822
- const credential = await openSealedCredential(sealingKey(), JSON.parse(sealed) as SealedCredential, name);
820
+ const credential = await openSealedAt(credentialPath(this.worldRoot, name), name);
821
+ if (credential === null) return Response.json({ error: `the ${name} link has no credential sealed` }, { status: 409 });
823
822
  const base = validateRemoteOrigin(origin);
824
823
  const target = new URL(`${base.origin}/${rest}${url.search}`);
825
824
  if (target.origin !== base.origin) return Response.json({ error: 'the path left the link origin' }, { status: 400 });