feat(db): auth_provider_config table + AES-256-GCM encryption helpers

Renumbered migration 0021 → 0023 to resolve conflict with pet_image and
logo_key migrations that landed on main after this branch was created.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
groombook-engineer[bot]
2026-04-02 17:16:35 +00:00
parent 83704b9777
commit f4acf5be23
7 changed files with 2364 additions and 2 deletions
+96
View File
@@ -0,0 +1,96 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { encryptSecret, decryptSecret } from "@groombook/db";
describe("encryptSecret / decryptSecret", () => {
const originalEnv = process.env.BETTER_AUTH_SECRET;
beforeEach(() => {
process.env.BETTER_AUTH_SECRET = "test-secret-key-for-unit-tests-32bytes!";
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env.BETTER_AUTH_SECRET = originalEnv;
} else {
delete process.env.BETTER_AUTH_SECRET;
}
});
it("encrypts and decrypts a simple secret", () => {
const plaintext = "my-client-secret-123";
const encrypted = encryptSecret(plaintext);
const decrypted = decryptSecret(encrypted);
expect(decrypted).toBe(plaintext);
});
it("produces output in iv:ciphertext:authTag format", () => {
const encrypted = encryptSecret("test");
const parts = encrypted.split(":");
expect(parts).toHaveLength(3);
// Each part should be valid base64
parts.forEach((part) => {
expect(() => Buffer.from(part, "base64")).not.toThrow();
});
});
it("different plaintexts produce different ciphertexts", () => {
const encrypted1 = encryptSecret("secret1");
const encrypted2 = encryptSecret("secret2");
expect(encrypted1).not.toBe(encrypted2);
});
it("same plaintext produces different ciphertexts (due to random IV)", () => {
const encrypted1 = encryptSecret("same-secret");
const encrypted2 = encryptSecret("same-secret");
expect(encrypted1).not.toBe(encrypted2);
// But both should decrypt to the same value
expect(decryptSecret(encrypted1)).toBe("same-secret");
expect(decryptSecret(encrypted2)).toBe("same-secret");
});
it("throws if BETTER_AUTH_SECRET is not set", () => {
delete process.env.BETTER_AUTH_SECRET;
expect(() => encryptSecret("test")).toThrow(
"BETTER_AUTH_SECRET environment variable is required"
);
});
it("throws when decrypting invalid format (wrong number of parts)", () => {
const encrypted = encryptSecret("test");
// Replace the last ":authTag" part by matching colon + non-colon chars at the end
const invalid = encrypted.replace(/:[^:]+$/, "");
expect(() => decryptSecret(invalid)).toThrow(
"Invalid encrypted value format: expected iv:ciphertext:authTag"
);
});
it("handles empty string secret", () => {
const plaintext = "";
const encrypted = encryptSecret(plaintext);
const decrypted = decryptSecret(encrypted);
expect(decrypted).toBe(plaintext);
});
it("handles unicode secret", () => {
const plaintext = "密码🔐中文";
const encrypted = encryptSecret(plaintext);
const decrypted = decryptSecret(encrypted);
expect(decrypted).toBe(plaintext);
});
it("handles long secret", () => {
const plaintext = "a".repeat(10000);
const encrypted = encryptSecret(plaintext);
const decrypted = decryptSecret(encrypted);
expect(decrypted).toBe(plaintext);
});
});