Add secrets documentation and inline env migration script
Document secret storage in DATABASE.md and DEVELOPING.md. Update SPEC-implementation with company_secrets schema and indexes. Add migrate-inline-env-secrets script for converting existing plain env values to managed secrets (dry-run by default, --apply to commit). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
125
scripts/migrate-inline-env-secrets.ts
Normal file
125
scripts/migrate-inline-env-secrets.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { agents, createDb } from "@paperclip/db";
|
||||
import { secretService } from "../server/src/services/secrets.js";
|
||||
|
||||
const SENSITIVE_ENV_KEY_RE =
|
||||
/(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)/i;
|
||||
|
||||
type EnvBinding =
|
||||
| string
|
||||
| { type: "plain"; value: string }
|
||||
| { type: "secret_ref"; secretId: string; version?: number | "latest" };
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function toPlainValue(binding: unknown): string | null {
|
||||
if (typeof binding === "string") return binding;
|
||||
if (typeof binding !== "object" || binding === null || Array.isArray(binding)) return null;
|
||||
const rec = binding as Record<string, unknown>;
|
||||
if (rec.type === "plain" && typeof rec.value === "string") return rec.value;
|
||||
return null;
|
||||
}
|
||||
|
||||
function secretName(agentId: string, key: string) {
|
||||
return `agent_${agentId.slice(0, 8)}_${key.toLowerCase()}`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const dbUrl = process.env.DATABASE_URL;
|
||||
if (!dbUrl) {
|
||||
console.error("DATABASE_URL is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const apply = process.argv.includes("--apply");
|
||||
const db = createDb(dbUrl);
|
||||
const secrets = secretService(db);
|
||||
|
||||
const allAgents = await db.select().from(agents);
|
||||
let changedAgents = 0;
|
||||
let createdSecrets = 0;
|
||||
let rotatedSecrets = 0;
|
||||
|
||||
for (const agent of allAgents) {
|
||||
const adapterConfig = asRecord(agent.adapterConfig);
|
||||
if (!adapterConfig) continue;
|
||||
const env = asRecord(adapterConfig.env);
|
||||
if (!env) continue;
|
||||
|
||||
let changed = false;
|
||||
const nextEnv: Record<string, EnvBinding> = { ...(env as Record<string, EnvBinding>) };
|
||||
|
||||
for (const [key, rawBinding] of Object.entries(env)) {
|
||||
if (!SENSITIVE_ENV_KEY_RE.test(key)) continue;
|
||||
const plain = toPlainValue(rawBinding);
|
||||
if (plain === null) continue;
|
||||
if (plain.trim().length === 0) continue;
|
||||
|
||||
const name = secretName(agent.id, key);
|
||||
if (apply) {
|
||||
const existing = await secrets.getByName(agent.companyId, name);
|
||||
if (existing) {
|
||||
await secrets.rotate(
|
||||
existing.id,
|
||||
{ value: plain },
|
||||
{ userId: "migration", agentId: null },
|
||||
);
|
||||
rotatedSecrets += 1;
|
||||
nextEnv[key] = { type: "secret_ref", secretId: existing.id, version: "latest" };
|
||||
} else {
|
||||
const created = await secrets.create(
|
||||
agent.companyId,
|
||||
{
|
||||
name,
|
||||
provider: "local_encrypted",
|
||||
value: plain,
|
||||
description: `Migrated from agent ${agent.id} env ${key}`,
|
||||
},
|
||||
{ userId: "migration", agentId: null },
|
||||
);
|
||||
createdSecrets += 1;
|
||||
nextEnv[key] = { type: "secret_ref", secretId: created.id, version: "latest" };
|
||||
}
|
||||
} else {
|
||||
nextEnv[key] = {
|
||||
type: "secret_ref",
|
||||
secretId: `<would-create:${name}>`,
|
||||
version: "latest",
|
||||
};
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) continue;
|
||||
changedAgents += 1;
|
||||
|
||||
if (apply) {
|
||||
await db
|
||||
.update(agents)
|
||||
.set({
|
||||
adapterConfig: {
|
||||
...adapterConfig,
|
||||
env: nextEnv,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(agents.id, agent.id));
|
||||
}
|
||||
}
|
||||
|
||||
if (!apply) {
|
||||
console.log(`Dry run: ${changedAgents} agents would be updated`);
|
||||
console.log("Re-run with --apply to persist changes");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Updated ${changedAgents} agents, created ${createdSecrets} secrets, rotated ${rotatedSecrets} secrets`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
void main();
|
||||
Reference in New Issue
Block a user