diff --git a/apps/mana/apps/web/src/lib/modules/writing/stores/drafts.svelte.ts b/apps/mana/apps/web/src/lib/modules/writing/stores/drafts.svelte.ts index 2d0eb4d65..98d68f187 100644 --- a/apps/mana/apps/web/src/lib/modules/writing/stores/drafts.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/writing/stores/drafts.svelte.ts @@ -12,7 +12,7 @@ * store and then call `pointToVersion` — never append to an existing version. */ -import { encryptRecord } from '$lib/data/crypto'; +import { encryptRecord, decryptRecord } from '$lib/data/crypto'; import { emitDomainEvent } from '$lib/data/events'; import { getActiveSpace } from '$lib/data/scope'; import { getEffectiveUserId } from '$lib/data/current-user'; @@ -150,6 +150,9 @@ export const draftsStore = { async updateBriefing(id: string, briefingPatch: Partial) { const existing = await draftTable.get(id); if (!existing) return; + // briefing is in the encrypt-list — decrypt before merging or the + // ciphertext blob spreads garbage into the patch. + await decryptRecord('writingDrafts', existing); const merged: DraftBriefing = { ...existing.briefing, ...briefingPatch }; await draftsStore.updateDraft(id, { briefing: merged }); }, @@ -227,6 +230,9 @@ export const draftsStore = { ) { const source = await draftVersionTable.get(sourceVersionId); if (!source) throw new Error(`Version ${sourceVersionId} not found`); + // content is encrypted — decrypt so the checkpoint copies plaintext, + // not the ciphertext blob. + await decryptRecord('writingDraftVersions', source); const existing = await draftVersionTable.where('draftId').equals(draftId).toArray(); const nextNumber = Math.max(0, ...existing.map((v) => v.versionNumber)) + 1; diff --git a/apps/mana/apps/web/src/lib/modules/writing/stores/generations.svelte.ts b/apps/mana/apps/web/src/lib/modules/writing/stores/generations.svelte.ts index 139f3d0c4..dc2d82a4c 100644 --- a/apps/mana/apps/web/src/lib/modules/writing/stores/generations.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/writing/stores/generations.svelte.ts @@ -15,7 +15,7 @@ * back into the same current version in-place. */ -import { encryptRecord } from '$lib/data/crypto'; +import { encryptRecord, decryptRecord } from '$lib/data/crypto'; import { emitDomainEvent } from '$lib/data/events'; import { generationTable, draftTable, draftVersionTable, writingStyleTable } from '../collections'; import { callWritingGeneration } from '../api'; @@ -98,13 +98,21 @@ export const generationsStore = { ): Promise { const draft = await draftTable.get(draftId); if (!draft) throw new Error(`Draft ${draftId} not found`); + // title / briefing / styleOverrides / references are all in the + // encrypt-list — without decrypting them first, references is a + // ciphertext string and refs.map() blows up. + await decryptRecord('writingDrafts', draft); const generationId = crypto.randomUUID(); - const kind: GenerationKind = - draft.currentVersionId && - (await draftVersionTable.get(draft.currentVersionId))?.content?.trim() - ? 'full-regenerate' - : 'draft-from-brief'; + let priorContent = ''; + if (draft.currentVersionId) { + const priorRow = await draftVersionTable.get(draft.currentVersionId); + if (priorRow) { + await decryptRecord('writingDraftVersions', priorRow); + priorContent = priorRow.content ?? ''; + } + } + const kind: GenerationKind = priorContent.trim() ? 'full-regenerate' : 'draft-from-brief'; const resolved = await loadStyle(draft.styleId); const stylePreset = resolved?.source === 'preset' @@ -281,6 +289,9 @@ export const generationsStore = { ): Promise<{ generationId: string; refined: string }> { const draft = await draftTable.get(draftId); if (!draft) throw new Error(`Draft ${draftId} not found`); + // briefing.language sits behind the briefing-encryption — read after + // decrypting or the ciphertext-shaped value crashes property access. + await decryptRecord('writingDrafts', draft); const resolved = await loadStyle(draft.styleId); const stylePreset = @@ -413,6 +424,9 @@ export const generationsStore = { ): Promise<{ before: string; after: string }> { const existing = await draftVersionTable.get(versionId); if (!existing) throw new Error(`Version ${versionId} not found`); + // content is encrypted; splice in plaintext or we'd build the new + // version by concatenating ciphertext with the replacement. + await decryptRecord('writingDraftVersions', existing); const before = existing.content; const after = before.slice(0, selection.start) + replacement + before.slice(selection.end);