Files
paperclip/scripts/migrate-inline-env-secrets.ts
Dotta f60c1001ec refactor: rename packages to @paperclipai and CLI binary to paperclipai
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>
2026-03-03 08:45:26 -06:00

126 lines
3.8 KiB
TypeScript

import { eq } from "drizzle-orm";
import { agents, createDb } from "@paperclipai/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();