'use node'; import { createCipheriv, createDecipheriv, createHash, randomBytes, } from 'node:crypto'; import { ConvexError } from 'convex/values'; const getSecret = () => { const secret = process.env.SPOON_ENCRYPTION_KEY?.trim() ?? process.env.INSTANCE_SECRET?.trim(); if (!secret) { throw new ConvexError( 'SPOON_ENCRYPTION_KEY is not configured. Add it before storing user API keys.', ); } return secret; }; const getKey = () => createHash('sha256').update(getSecret()).digest(); export const encryptSecret = (plaintext: string) => { const iv = randomBytes(12); const cipher = createCipheriv('aes-256-gcm', getKey(), iv); const ciphertext = Buffer.concat([ cipher.update(plaintext, 'utf8'), cipher.final(), ]); const tag = cipher.getAuthTag(); return [ iv.toString('base64url'), tag.toString('base64url'), ciphertext.toString('base64url'), ].join('.'); }; export const decryptSecret = (encrypted: string) => { const [ivRaw, tagRaw, ciphertextRaw] = encrypted.split('.'); if (!ivRaw || !tagRaw || !ciphertextRaw) { throw new ConvexError('Stored secret has an invalid format.'); } const decipher = createDecipheriv( 'aes-256-gcm', getKey(), Buffer.from(ivRaw, 'base64url'), ); decipher.setAuthTag(Buffer.from(tagRaw, 'base64url')); const plaintext = Buffer.concat([ decipher.update(Buffer.from(ciphertextRaw, 'base64url')), decipher.final(), ]); return plaintext.toString('utf8'); };