Files
HealthLog/src/lib/ai/coach/bytes-codec.ts
T
Marc-André BombeckandClaude Opus 4.8 01d3afcdaf feat(coach): scaffold conversation-summary + facts storage
Extract the AES-256-GCM Bytes codec from persistence into a shared
bytes-codec module so the conversation summary and the new personal-fact
store reuse one ArrayBuffer-backed implementation. Add the CoachFact
table (migration 0116) for durable, user-scoped facts the Coach extracts
from conversations — encrypted fact text, plain category/confidence/
source for rank-without-decrypt, soft-delete for a reversible forget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:11:19 +02:00

33 lines
1.3 KiB
TypeScript

/**
* Shared AES-256-GCM ↔ `Bytes` codec for the Coach's encrypted columns.
*
* `coach_messages.encrypted_content`, `coach_conversations.summary_encrypted`,
* and `coach_facts.fact_encrypted` all store the same payload shape: the
* `encrypt()` string format from `@/lib/crypto` (`"<keyId>.<base64>"`) encoded
* as UTF-8 bytes. Centralised here so there is one ArrayBuffer-backed
* implementation every Coach persistence path shares.
*
* Prisma's `Bytes` type maps to `Uint8Array<ArrayBuffer>`, not Node's
* `Buffer<ArrayBufferLike>`, so we allocate a fresh ArrayBuffer-backed
* `Uint8Array` for writes to keep the structural type stable across Node
* versions.
*/
import { Buffer } from "node:buffer";
import { decrypt, encrypt } from "@/lib/crypto";
/** Encrypt a UTF-8 string into the `Bytes` payload the schema stores. */
export function encryptToBytes(plaintext: string): Uint8Array<ArrayBuffer> {
const ciphertext = encrypt(plaintext);
const encoded = Buffer.from(ciphertext, "utf8");
const out = new Uint8Array(new ArrayBuffer(encoded.byteLength));
out.set(encoded);
return out;
}
/** Decrypt a `Bytes` payload back to its plaintext. Throws on a bad key id. */
export function decryptFromBytes(buf: Uint8Array): string {
const text = Buffer.from(buf).toString("utf8");
return decrypt(text);
}