Rename all workspace packages from @paperclip/* to @paperclipai/* and the CLI binary from `paperclip` to `paperclipai` in preparation for npm publishing. Bump CLI version to 0.1.0 and add package metadata (description, keywords, license, repository, files). Update all imports, documentation, user-facing messages, and tests accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
147 lines
4.4 KiB
TypeScript
147 lines
4.4 KiB
TypeScript
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,
|
|
);
|
|
}
|