mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-29 00:01:20 +00:00
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>
This commit is contained in:
1 parent
31bc23386b
commit
585bb04ab2
52 files changed
+3929
-142
No files matched your search
@@ -0,0 +1,61 @@
|
||||
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]));
|
||||
}
|
||||
Reference in new issue
Block a user