All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m9s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m47s
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 9s
CI / Auth e2e pack (push) Successful in 5m25s
CI / Import/export fidelity gate (push) Successful in 45s
New apps/backup service (ADR 0015): nightly pg_dump -Fc plus one tar of the uploads/plugins volumes as a consistent restore set on a new backups volume, retention prune that never removes the newest complete set, atomic status.json for the readiness/admin consumers (#85/#86), and a failure mail sent directly via nodemailer (the api may be the broken part) with de/en texts in the shared mails catalog. BACKUP_RUN_ONCE=1 gives the on-demand path; deploy/backup/restore.sh automates the documented restore runbook. The pure secret-store helpers moved to @dorfteich/shared so the sidecar resolves the wizard-written SMTP relay exactly like the api. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
63 lines
2.6 KiB
TypeScript
63 lines
2.6 KiB
TypeScript
/**
|
|
* The env-backed secret store (security.md §Secrets, issue #80): secrets the
|
|
* setup wizard collects in the browser (SMTP credentials) are persisted as a
|
|
* mode-600 dotenv-style file on a volume — never as database rows. The file
|
|
* extends the environment: `overlayEnv` fills only variables the process
|
|
* environment does not set. These pure format/merge helpers live in shared
|
|
* because two services read the store the api writes: the api itself and the
|
|
* backup sidecar (issue #83), which needs the wizard's SMTP relay for its
|
|
* failure mail. File I/O stays with each service.
|
|
*/
|
|
|
|
/** Parses the dotenv-style store content. Ignores blank lines and comments. */
|
|
export function parseSecretsFile(content: string): Record<string, string> {
|
|
const secrets: Record<string, string> = {};
|
|
for (const line of content.split('\n')) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
const eq = trimmed.indexOf('=');
|
|
if (eq <= 0) continue;
|
|
const key = trimmed.slice(0, eq).trim();
|
|
let value = trimmed.slice(eq + 1).trim();
|
|
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
value = value.slice(1, -1).replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
}
|
|
secrets[key] = value;
|
|
}
|
|
return secrets;
|
|
}
|
|
|
|
/** Serializes secrets with double-quoted, escaped values (dotenv-compatible). */
|
|
export function serializeSecrets(secrets: Record<string, string>): string {
|
|
const lines = [
|
|
'# Managed by Dorfteich (setup wizard). Values here fill environment',
|
|
'# variables that the container environment does not set explicitly.',
|
|
];
|
|
for (const [key, value] of Object.entries(secrets)) {
|
|
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
lines.push(`${key}="${escaped}"`);
|
|
}
|
|
return lines.join('\n') + '\n';
|
|
}
|
|
|
|
/**
|
|
* Merges the store under the real environment: explicit env vars win, store
|
|
* values fill the gaps (and Zod defaults fill whatever remains at parse
|
|
* time). Empty strings count as unset on both sides — compose passes
|
|
* `${SMTP_HOST:-}` as `""` for variables the stage `.env` does not define,
|
|
* and those must not shadow wizard-written store values or schema defaults.
|
|
*/
|
|
export function overlayEnv(
|
|
env: Record<string, string | undefined>,
|
|
secrets: Record<string, string>,
|
|
): Record<string, string | undefined> {
|
|
const merged: Record<string, string | undefined> = {};
|
|
for (const [key, value] of Object.entries(secrets)) {
|
|
if (value !== '') merged[key] = value;
|
|
}
|
|
for (const [key, value] of Object.entries(env)) {
|
|
if (value !== undefined && value !== '') merged[key] = value;
|
|
}
|
|
return merged;
|
|
}
|