Spaces:
Paused
Paused
| import { randomBytes } from "node:crypto"; | |
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import type { PaperclipConfig } from "../config/schema.js"; | |
| import type { CheckResult } from "./index.js"; | |
| import { resolveRuntimeLikePath } from "./path-resolver.js"; | |
| function decodeMasterKey(raw: string): Buffer | null { | |
| const trimmed = raw.trim(); | |
| if (!trimmed) return null; | |
| if (/^[A-Fa-f0-9]{64}$/.test(trimmed)) { | |
| return Buffer.from(trimmed, "hex"); | |
| } | |
| try { | |
| const decoded = Buffer.from(trimmed, "base64"); | |
| if (decoded.length === 32) return decoded; | |
| } catch { | |
| // ignored | |
| } | |
| if (Buffer.byteLength(trimmed, "utf8") === 32) { | |
| return Buffer.from(trimmed, "utf8"); | |
| } | |
| return null; | |
| } | |
| function withStrictModeNote( | |
| base: Pick<CheckResult, "name" | "status" | "message" | "canRepair" | "repair" | "repairHint">, | |
| config: PaperclipConfig, | |
| ): CheckResult { | |
| const strictModeDisabledInDeployedSetup = | |
| config.database.mode === "postgres" && config.secrets.strictMode === false; | |
| if (!strictModeDisabledInDeployedSetup) return base; | |
| if (base.status === "fail") return base; | |
| return { | |
| ...base, | |
| status: "warn", | |
| message: `${base.message}; strict secret mode is disabled for postgres deployment`, | |
| repairHint: base.repairHint | |
| ? `${base.repairHint}. Consider enabling secrets.strictMode` | |
| : "Consider enabling secrets.strictMode", | |
| }; | |
| } | |
| export function secretsCheck(config: PaperclipConfig, configPath?: string): CheckResult { | |
| const provider = config.secrets.provider; | |
| if (provider !== "local_encrypted") { | |
| return { | |
| name: "Secrets adapter", | |
| status: "fail", | |
| message: `${provider} is configured, but this build only supports local_encrypted`, | |
| canRepair: false, | |
| repairHint: "Run `paperclipai configure --section secrets` and set provider to local_encrypted", | |
| }; | |
| } | |
| const envMasterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY; | |
| if (envMasterKey && envMasterKey.trim().length > 0) { | |
| if (!decodeMasterKey(envMasterKey)) { | |
| return { | |
| name: "Secrets adapter", | |
| status: "fail", | |
| message: | |
| "PAPERCLIP_SECRETS_MASTER_KEY is invalid (expected 32-byte base64, 64-char hex, or raw 32-char string)", | |
| canRepair: false, | |
| repairHint: "Set PAPERCLIP_SECRETS_MASTER_KEY to a valid key or unset it to use a key file", | |
| }; | |
| } | |
| return withStrictModeNote( | |
| { | |
| name: "Secrets adapter", | |
| status: "pass", | |
| message: "Local encrypted provider configured via PAPERCLIP_SECRETS_MASTER_KEY", | |
| }, | |
| config, | |
| ); | |
| } | |
| const keyFileOverride = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; | |
| const configuredPath = | |
| keyFileOverride && keyFileOverride.trim().length > 0 | |
| ? keyFileOverride.trim() | |
| : config.secrets.localEncrypted.keyFilePath; | |
| const keyFilePath = resolveRuntimeLikePath(configuredPath, configPath); | |
| if (!fs.existsSync(keyFilePath)) { | |
| return withStrictModeNote( | |
| { | |
| name: "Secrets adapter", | |
| status: "warn", | |
| message: `Secrets key file does not exist yet: ${keyFilePath}`, | |
| canRepair: true, | |
| repair: () => { | |
| fs.mkdirSync(path.dirname(keyFilePath), { recursive: true }); | |
| fs.writeFileSync(keyFilePath, randomBytes(32).toString("base64"), { | |
| encoding: "utf8", | |
| mode: 0o600, | |
| }); | |
| try { | |
| fs.chmodSync(keyFilePath, 0o600); | |
| } catch { | |
| // best effort | |
| } | |
| }, | |
| repairHint: "Run with --repair to create a local encrypted secrets key file", | |
| }, | |
| config, | |
| ); | |
| } | |
| let raw: string; | |
| try { | |
| raw = fs.readFileSync(keyFilePath, "utf8"); | |
| } catch (err) { | |
| return { | |
| name: "Secrets adapter", | |
| status: "fail", | |
| message: `Could not read secrets key file: ${err instanceof Error ? err.message : String(err)}`, | |
| canRepair: false, | |
| repairHint: "Check file permissions or set PAPERCLIP_SECRETS_MASTER_KEY", | |
| }; | |
| } | |
| if (!decodeMasterKey(raw)) { | |
| return { | |
| name: "Secrets adapter", | |
| status: "fail", | |
| message: `Invalid key material in ${keyFilePath}`, | |
| canRepair: false, | |
| repairHint: "Replace with valid key material or delete it and run doctor --repair", | |
| }; | |
| } | |
| return withStrictModeNote( | |
| { | |
| name: "Secrets adapter", | |
| status: "pass", | |
| message: `Local encrypted provider configured with key file ${keyFilePath}`, | |
| }, | |
| config, | |
| ); | |
| } | |