AkurAI Build
Menu

popagent

public

Latest change 7f0ff66d6d9fb6468416c58bee46bd3d08169501 - Checkpoint browser channels and memory work 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 delete(resourceId: string, name: string) {
    await ensureSchema();
    const rows = await sql`
      DELETE FROM popagent_secrets
      WHERE resource_id = ${resourceId} AND name = ${name}
      RETURNING name
    `;
    return rows.length === 1;
  },

  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;
  },
};