mirror of
https://github.com/kennethnym/aris.git
synced 2026-03-23 18:41:17 +00:00
* feat(backend): add DB persistence layer Replace raw pg Pool with Drizzle ORM backed by Bun.sql. Add per-user source configuration table (user_sources). Migrate Better Auth to drizzle-adapter. Add AES-256-GCM credential encryption. Co-authored-by: Ona <no-reply@ona.com> * fix(backend): set updatedAt explicitly in all mutations onConflictDoUpdate bypasses Drizzle's $onUpdate hook. Set updatedAt explicitly in all mutation methods. Co-authored-by: Ona <no-reply@ona.com> * fix(backend): add composite index on user_sources Add (user_id, enabled) index for the enabled() query path. Co-authored-by: Ona <no-reply@ona.com> --------- Co-authored-by: Ona <no-reply@ona.com>
63 lines
1.9 KiB
TypeScript
63 lines
1.9 KiB
TypeScript
import { randomBytes } from "node:crypto"
|
|
import { describe, expect, test } from "bun:test"
|
|
|
|
import { CredentialEncryptor } from "./crypto.ts"
|
|
|
|
const TEST_KEY = randomBytes(32).toString("base64")
|
|
|
|
describe("CredentialEncryptor", () => {
|
|
const encryptor = new CredentialEncryptor(TEST_KEY)
|
|
|
|
test("round-trip with simple string", () => {
|
|
const plaintext = "hello world"
|
|
const encrypted = encryptor.encrypt(plaintext)
|
|
expect(encryptor.decrypt(encrypted)).toBe(plaintext)
|
|
})
|
|
|
|
test("round-trip with JSON credentials", () => {
|
|
const credentials = JSON.stringify({
|
|
accessToken: "ya29.a0AfH6SMB...",
|
|
refreshToken: "1//0dx...",
|
|
expiresAt: "2025-12-01T00:00:00Z",
|
|
})
|
|
const encrypted = encryptor.encrypt(credentials)
|
|
expect(encryptor.decrypt(encrypted)).toBe(credentials)
|
|
})
|
|
|
|
test("round-trip with empty string", () => {
|
|
const encrypted = encryptor.encrypt("")
|
|
expect(encryptor.decrypt(encrypted)).toBe("")
|
|
})
|
|
|
|
test("round-trip with unicode", () => {
|
|
const plaintext = "日本語テスト 🔐"
|
|
const encrypted = encryptor.encrypt(plaintext)
|
|
expect(encryptor.decrypt(encrypted)).toBe(plaintext)
|
|
})
|
|
|
|
test("each encryption produces different ciphertext (unique IV)", () => {
|
|
const plaintext = "same input"
|
|
const a = encryptor.encrypt(plaintext)
|
|
const b = encryptor.encrypt(plaintext)
|
|
expect(a).not.toEqual(b)
|
|
expect(encryptor.decrypt(a)).toBe(plaintext)
|
|
expect(encryptor.decrypt(b)).toBe(plaintext)
|
|
})
|
|
|
|
test("tampered ciphertext throws", () => {
|
|
const encrypted = encryptor.encrypt("secret")
|
|
encrypted[13]! ^= 0xff
|
|
expect(() => encryptor.decrypt(encrypted)).toThrow()
|
|
})
|
|
|
|
test("truncated data throws", () => {
|
|
expect(() => encryptor.decrypt(Buffer.alloc(10))).toThrow("Encrypted data is too short")
|
|
})
|
|
|
|
test("throws when key is wrong length", () => {
|
|
expect(() => new CredentialEncryptor(Buffer.from("too-short").toString("base64"))).toThrow(
|
|
"must be 32 bytes",
|
|
)
|
|
})
|
|
})
|