* fix: hand provider OAuth callbacks back to initiating brand * fix: encrypt provider OAuth handoff payloads * fix: purge expired provider OAuth handoffs
28 lines
1.4 KiB
TypeScript
28 lines
1.4 KiB
TypeScript
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
|
|
|
|
// Match the existing extension credential-encryption pattern with a dedicated
|
|
// key purpose. Both callback domains already require this server-only secret.
|
|
function getKey(): Buffer {
|
|
const secret = process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
if (!secret) throw new Error('SUPABASE_SERVICE_ROLE_KEY is required')
|
|
return createHash('sha256').update('arcim-migration:handoff:v1:' + secret).digest()
|
|
}
|
|
|
|
export function encryptHandoffValue(plaintext: string, context: string): string {
|
|
const iv = randomBytes(12)
|
|
const cipher = createCipheriv('aes-256-gcm', getKey(), iv)
|
|
cipher.setAAD(Buffer.from(context))
|
|
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
|
|
return 'v1:' + Buffer.concat([iv, cipher.getAuthTag(), encrypted]).toString('base64url')
|
|
}
|
|
|
|
export function decryptHandoffValue(ciphertext: string, context: string): string {
|
|
// Never accept an old plaintext value as a successful decryption.
|
|
if (!ciphertext.startsWith('v1:')) throw new Error('Invalid OAuth handoff ciphertext')
|
|
const combined = Buffer.from(ciphertext.slice(3), 'base64url')
|
|
const decipher = createDecipheriv('aes-256-gcm', getKey(), combined.subarray(0, 12))
|
|
decipher.setAAD(Buffer.from(context))
|
|
decipher.setAuthTag(combined.subarray(12, 28))
|
|
return Buffer.concat([decipher.update(combined.subarray(28)), decipher.final()]).toString('utf8')
|
|
}
|