Menu
popagent
publicLatest change 6edc7e928c57dab23c0973ed3f7ee70b3ebb72d5 - Initial popagent baseline by AkurAI Build
import { SQL } from "bun";
import { retryableInit } from "./retryable-init";
const connectionString = process.env.DATABASE_URL;
if (!connectionString) throw new Error("DATABASE_URL is required");
const sql = new SQL(connectionString);
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const ensureSchema = retryableInit(async () => {
await sql`
CREATE TABLE IF NOT EXISTS popagent_secrets (
resource_id TEXT NOT NULL,
name TEXT NOT NULL,
ciphertext BYTEA NOT NULL,
iv BYTEA NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (resource_id, name)
)
`;
});
async function encryptionKey() {
const encoded = process.env.POPAGENT_SECRET_KEY;
if (!encoded) throw new Error("POPAGENT_SECRET_KEY is required to use secret tools");
const bytes = Uint8Array.fromBase64(encoded);
if (bytes.byteLength !== 32) throw new Error("POPAGENT_SECRET_KEY must be a base64-encoded 32-byte key");
return crypto.subtle.importKey("raw", bytes, "AES-GCM", false, ["encrypt", "decrypt"]);
}
function additionalData(resourceId: string, name: string) {
return encoder.encode(`${resourceId}\0${name}`);
}
async function encrypt(resourceId: string, name: string, value: string) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv, additionalData: additionalData(resourceId, name) },
await encryptionKey(),
encoder.encode(value),
);
return { ciphertext: new Uint8Array(ciphertext), iv };
}
async function decrypt(resourceId: string, name: string, ciphertext: Uint8Array, iv: Uint8Array) {
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv as Uint8Array<ArrayBuffer>, additionalData: additionalData(resourceId, name) },
await encryptionKey(),
ciphertext as Uint8Array<ArrayBuffer>,
);
return decoder.decode(plaintext);
}
export const secrets = {
async store(resourceId: string, name: string, value: string) {
await ensureSchema();
const encrypted = await encrypt(resourceId, name, value);
const rows = await sql`
INSERT INTO popagent_secrets (resource_id, name, ciphertext, iv)
VALUES (${resourceId}, ${name}, ${encrypted.ciphertext}, ${encrypted.iv})
ON CONFLICT (resource_id, name) DO NOTHING
RETURNING name
`;
return rows.length === 1;
},
async recall(resourceId: string, name: string) {
await ensureSchema();
const rows = await sql`
SELECT ciphertext, iv
FROM popagent_secrets
WHERE resource_id = ${resourceId} AND name = ${name}
` as Array<{ ciphertext: Uint8Array; iv: Uint8Array }>;
const row = rows[0];
if (!row) return undefined;
return decrypt(resourceId, name, row.ciphertext, row.iv);
},
async update(resourceId: string, name: string, value: string) {
await ensureSchema();
const encrypted = await encrypt(resourceId, name, value);
const rows = await sql`
UPDATE popagent_secrets
SET ciphertext = ${encrypted.ciphertext}, iv = ${encrypted.iv}, updated_at = NOW()
WHERE resource_id = ${resourceId} AND name = ${name}
RETURNING name
`;
return rows.length === 1;
},
};