mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
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>
This commit is contained in:
1 parent
7c494b60cb
commit
01d3afcdaf
4 files changed
+92
-24
No files matched your search
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -10,10 +10,8 @@
|
||||
* provenance (window names, metric tags, sample counts) and never raw
|
||||
* values, so it can be queried without decryption for analytics.
|
||||
*/
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
import { encrypt, decrypt } from "@/lib/crypto";
|
||||
import { decryptFromBytes, encryptToBytes } from "./bytes-codec";
|
||||
|
||||
import type {
|
||||
CoachConversationDTO,
|
||||
@@ -49,27 +47,6 @@ export function summariseTitle(input: string): string {
|
||||
return `${cut.trimEnd()}…`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a UTF-8 string as the AES-256-GCM payload format the schema
|
||||
* stores in `coach_messages.encrypted_content`.
|
||||
*
|
||||
* Prisma's `Bytes` type maps to `Uint8Array<ArrayBuffer>`, not Node's
|
||||
* `Buffer<ArrayBufferLike>`. We allocate a fresh ArrayBuffer-backed
|
||||
* Uint8Array so the structural type matches across Node versions.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
function decryptFromBytes(buf: Uint8Array): string {
|
||||
const text = Buffer.from(buf).toString("utf8");
|
||||
return decrypt(text);
|
||||
}
|
||||
|
||||
function provenanceToJson(provenance: CoachProvenance | null): string | null {
|
||||
if (!provenance) return null;
|
||||
return JSON.stringify(provenance);
|
||||
|
||||
Reference in new issue
Block a user