Build AI Chief of Staff MVP: auth, mailbox sync, briefings, draft replies

Scaffolds a Next.js app with email/password auth, encrypted IMAP/SMTP/CalDAV
mailbox connections, a background sync job, Claude-generated daily briefings,
and an approve-before-send draft reply flow. Includes a clean design system
(light/dark) and deployment config (systemd unit, credential rotation script).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
This commit is contained in:
ogrechkoandClaude Sonnet 5 committed 2026-07-26 10:54:34 +00:00
1 parent facd4d71bd
commit bcf0e86618
65 files changed
+13413 -1

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
/**
* Rotates the AES-256-GCM master key used to encrypt stored mailbox
* credentials. Decrypts every stored IMAP/SMTP/CalDAV password under the
* old key and re-encrypts under a newly generated one, in a single
* transaction, then prints the new key so it can be written into
* /etc/ai-chief-of-staff/secrets.env.
*
* Usage: OLD_KEY=<base64> npx tsx scripts/rotate-credential-key.ts
* (reads the current key from CREDENTIALS_ENCRYPTION_KEY if OLD_KEY is unset)
*/
import crypto from "node:crypto";
import Database from "better-sqlite3";
import path from "node:path";
import { rotateCredential } from "../src/lib/crypto/credentials";
const dataDir = process.env.DATA_DIR ?? path.join(process.cwd(), "data");
const oldKeyB64 = process.env.OLD_KEY ?? process.env.CREDENTIALS_ENCRYPTION_KEY;
if (!oldKeyB64) {
console.error("Set OLD_KEY (or CREDENTIALS_ENCRYPTION_KEY) to the current base64-encoded key before running.");
process.exit(1);
}
const oldKey = Buffer.from(oldKeyB64, "base64");
if (oldKey.length !== 32) {
console.error("OLD_KEY must decode to exactly 32 bytes.");
process.exit(1);
}
const newKey = crypto.randomBytes(32);
const sqlite = new Database(path.join(dataDir, "db.sqlite"));
const rows = sqlite
.prepare(
"SELECT id, imap_password_enc, smtp_password_enc, caldav_password_enc FROM mailbox_credentials",
)
.all() as Array<{
id: string;
imap_password_enc: string | null;
smtp_password_enc: string | null;
caldav_password_enc: string | null;
}>;
const update = sqlite.prepare(
"UPDATE mailbox_credentials SET imap_password_enc = ?, smtp_password_enc = ?, caldav_password_enc = ? WHERE id = ?",
);
const rotateAll = sqlite.transaction(() => {
for (const row of rows) {
const imap = row.imap_password_enc ? rotateCredential(row.imap_password_enc, oldKey, newKey) : null;
const smtp = row.smtp_password_enc ? rotateCredential(row.smtp_password_enc, oldKey, newKey) : null;
const caldav = row.caldav_password_enc ? rotateCredential(row.caldav_password_enc, oldKey, newKey) : null;
update.run(imap, smtp, caldav, row.id);
}
});
rotateAll();
console.log(`Rotated ${rows.length} mailbox_credentials row(s).`);
console.log("\nNew key (update CREDENTIALS_ENCRYPTION_KEY in /etc/ai-chief-of-staff/secrets.env, then restart the service):\n");
console.log(newKey.toString("base64"));