Files
Primora/apps/cli/src/commands/inject.ts
T
Tomas DvorakandDevin 585bb04ab2 feat: encrypted secrets vault across CLI, desktop, dashboard, and backend
Local vault (apps/cli): clean-room PRMVLT01 format — Argon2id →
XChaCha20-Poly1305, AEAD-bound header, atomic writes. vault:* / secrets:*
/ inject (primora:// refs) / agent credential broker (dummy tokens in the
child, real secret on the wire) / TTL sessions / stack:* operator group.

Project vault (backend): core.project_secrets, AES-256-GCM via the
platform encryptor. Metadata-only list, audit-logged reveal, and
secret://NAME refs resolved at job delivery — refs stored, values never.

Surfaces: dashboard Vault page (+ local-vault card under the Tauri
shell), desktop vault window via IPC to the primora binary,
secrets:* --remote CLI mode, metadata/write-only MCP tools so agents
consume secrets via secret:// refs without holding plaintext.

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-24 18:46:37 +02:00

62 lines
2.3 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { CliError } from "../http.js";
import { dim } from "../out.js";
import { parseEnvFile } from "./secrets.js";
import { unlockVault, type VaultPasswordOptions } from "./vault.js";
const REF_RE = /^primora:\/\/(.+)$/;
/**
* `primora inject [--all] [--env-file PATH] -- <cmd>` — run one child process
* with vault secrets in its environment. Nothing written to disk, nothing in
* argv; the parent shell is unchanged. `primora://NAME` values inside the
* env-file resolve against the vault; literals stay literals.
*/
export async function cmdInject(
options: VaultPasswordOptions & { all?: boolean; envFile?: string },
): Promise<void> {
// cac does not model `--` passthrough — slice it out of argv directly.
const sep = process.argv.indexOf("--");
const child = sep >= 0 ? process.argv.slice(sep + 1) : [];
if (!child.length) {
throw new CliError("No command given.", "Usage: primora inject [--all] [--env-file .env] -- <cmd>");
}
const vault = await unlockVault(options);
const env: Record<string, string> = {};
// Default: whole vault. --all exists for readability when --env-file is also used.
if (options.all || !options.envFile) {
for (const [k, e] of Object.entries(vault.data.secrets)) env[k] = e.value;
}
if (options.envFile) {
for (const [key, raw] of Object.entries(parseEnvFile(options.envFile))) {
const ref = REF_RE.exec(raw);
if (!ref) {
env[key] = raw;
continue;
}
const entry = vault.data.secrets[ref[1]];
if (!entry) throw new CliError(`Unresolved ref: primora://${ref[1]} in ${options.envFile}`);
env[key] = entry.value;
}
}
dim(`injecting ${Object.keys(env).length} var${Object.keys(env).length === 1 ? "" : "s"} → ${child[0]}`);
const res = spawnSync(child[0], child.slice(1), {
env: { ...process.env, ...env } as NodeJS.ProcessEnv,
stdio: "inherit",
});
if (res.error) throw new CliError(`Failed to start ${child[0]}: ${res.error.message}`);
process.exit(res.status ?? 1);
}
/** Used by `stack up --vault`: render all vault entries as .env lines. */
export async function vaultAsEnvFile(
options: VaultPasswordOptions,
): Promise<Record<string, string>> {
const vault = await unlockVault(options);
return Object.fromEntries(Object.entries(vault.data.secrets).map(([k, e]) => [k, e.value]));
}