feat(crypto): phase 7.2 — encrypt storeless modules (questions, links, documents, meals)

Five storeless modules whose writes happen directly from view files
(no central store yet) get the same encryption treatment by wrapping
each .add/.update call site with encryptRecord and each read site
with decryptRecord(s). Registry entries are also corrected to match
the actual schemas — the previous Phase 1 placeholder names guessed
the wrong field names.

Registry corrections + flips
----------------------------
  - meals: was ['description', 'notes', 'aiAnalysis'] → now
    ['description', 'portionSize'] (LocalMeal has neither notes nor
    aiAnalysis on the schema; portionSize is a short user label same
    sensitivity as description)
  - documents: was ['title', 'content', 'body'] → now
    ['title', 'content'] (LocalDocument uses content, no body column)
  - links: was ['title', 'description', 'targetUrl'] → now
    ['title', 'description']. originalUrl STAYS PLAINTEXT — the
    public redirect handler resolves shortCode → originalUrl on every
    click, encrypting it would force the redirect path to do an async
    decrypt before issuing the 302
  - questions: was ['title', 'body', 'notes'] → now
    ['title', 'description'] (LocalQuestion uses description)
  - answers: was ['body'] → now ['content'] (LocalAnswer uses content)

All five tables flipped to enabled:true.

Write sites wrapped
-------------------
Each call site builds the row/diff as a typed object, runs
encryptRecord on it, then calls table.add / table.update:

  - questions/views/DetailView.svelte (saveField)
  - questions/[id]/+page.svelte (saveEdit + answer.add)
  - questions/new/+page.svelte (initial create)
  - uload/+page.svelte (createLink + saveEdit)
  - uload/views/DetailView.svelte (saveField)
  - context/documents/+page.svelte (handleCreateDocument)
  - context/documents/[id]/+page.svelte (handleSave with encrypted diff)
  - context/spaces/[id]/+page.svelte (handleCreateDocument)
  - nutriphi/add/+page.svelte (handleSubmit)

Pure metadata writes (toggle pinned, toggle isActive, soft-delete via
deletedAt) are intentionally NOT wrapped — they touch zero encrypted
fields so encryptRecord would be a no-op anyway.

Read sites decrypted
--------------------
  - questions/queries.ts: useAllQuestions, useAnswersByQuestion
  - questions/views/DetailView.svelte (liveQuery clone)
  - questions/ListView.svelte (Workbench)
  - uload/queries.ts: allLinks$, useAllLinks, useLinkById
  - uload/views/DetailView.svelte (liveQuery clone)
  - uload/ListView.svelte
  - uload/settings/+page.svelte (decrypts before serializing the
    JSON export — otherwise the user would download ciphertext)
  - context/queries.ts: useAllDocuments, useSpaceDocuments
  - context/ListView.svelte
  - cross-app-queries.useRecentDocuments (dashboard widget)
  - nutriphi/queries.ts: useAllMeals
  - nutriphi/ListView.svelte

The cards/dashboard widget for nutrition only reads m.nutrition (the
plaintext numeric breakdown), so it stays untouched. nutriphi/history
benefits transparently because it consumes useAllMeals which now
decrypts.

Why
---
Closes the second-tier plaintext gaps. The five tables flipped here
were on the registry from day one but stuck behind enabled:false
because no central store existed to hook into. Phase 7.2 takes the
pragmatic approach of wrapping at each call site rather than blocking
on a store extraction refactor — same end result for security, much
smaller diff. A future store consolidation pass can collapse the
duplication without changing the encryption surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-07 21:29:32 +02:00
parent c875b4e966
commit 40b7069eb0
20 changed files with 152 additions and 73 deletions

View file

@ -272,13 +272,17 @@ export function useRecentDecks(limit = 5) {
/** Recent documents + spaces. */
export function useRecentDocuments(limit = 5) {
return useLiveQueryWithDefault(async () => {
return db
// title + content are encrypted on disk; the dashboard surfaces the
// title so we have to decrypt before returning. limit is applied
// pre-decrypt to keep the batch small.
const visible = await db
.table<LocalDocument>('documents')
.orderBy('updatedAt')
.reverse()
.filter((d) => !d.deletedAt)
.limit(limit)
.toArray();
return decryptRecords('documents', visible);
}, [] as LocalDocument[]);
}

View file

@ -117,7 +117,12 @@ export const ENCRYPTION_REGISTRY: Record<string, EncryptionConfig> = {
cycleDayLogs: { enabled: true, fields: ['notes', 'mood'] },
// ─── NutriPhi ────────────────────────────────────────────
meals: { enabled: false, fields: ['description', 'notes', 'aiAnalysis'] },
// LocalMeal only has `description` as user-typed text (mealType /
// inputType / nutrition numbers stay plaintext for the daily-summary
// aggregations and the calorie-progress widget). portionSize is a
// short label like "1 Tasse" — same sensitivity as description, so
// we encrypt it too.
meals: { enabled: true, fields: ['description', 'portionSize'] },
// ─── Planta ──────────────────────────────────────────────
// `name` is NOT in the schema index for plants (only isActive +
@ -140,7 +145,11 @@ export const ENCRYPTION_REGISTRY: Record<string, EncryptionConfig> = {
slides: { enabled: true, fields: ['content'] },
// ─── Context ─────────────────────────────────────────────
documents: { enabled: false, fields: ['title', 'content', 'body'] },
// LocalDocument has `title` + `content` (no `body` column on the
// schema). DocumentType (text/context/prompt) and the spaceId
// foreign key stay plaintext so the workspace tree still groups
// documents per space without a key.
documents: { enabled: true, fields: ['title', 'content'] },
// ─── Storage ─────────────────────────────────────────────
files: { enabled: false, fields: ['name', 'originalName', 'notes'] },
@ -153,12 +162,12 @@ export const ENCRYPTION_REGISTRY: Record<string, EncryptionConfig> = {
mukkePlaylists: { enabled: false, fields: ['name', 'description'] },
// ─── Questions ───────────────────────────────────────────
// Writes from views are not yet routed through a store — registry
// is set so future store creation gets encryption automatically;
// existing direct db.table().update() call sites in the views need
// to migrate to a store before they actually flow through encryptRecord.
questions: { enabled: false, fields: ['title', 'body', 'notes'] },
answers: { enabled: false, fields: ['body'] },
// LocalQuestion uses `title` + `description`; LocalAnswer uses
// `content` (not `body`). The view-driven write sites are wrapped
// directly via encryptRecord at each call site since this module
// has no central store yet.
questions: { enabled: true, fields: ['title', 'description'] },
answers: { enabled: true, fields: ['content'] },
// ─── Events (social gatherings) ──────────────────────────
socialEvents: { enabled: false, fields: ['title', 'description', 'notes'] },
@ -172,7 +181,13 @@ export const ENCRYPTION_REGISTRY: Record<string, EncryptionConfig> = {
transactions: { enabled: true, fields: ['description', 'note'] },
// ─── uLoad ───────────────────────────────────────────────
links: { enabled: false, fields: ['title', 'description', 'targetUrl'] },
// `originalUrl` STAYS PLAINTEXT — the redirect handler resolves
// shortCode → originalUrl on every click, encrypting it would force
// the public redirect path to do an async decrypt before the 302.
// shortCode is a public lookup key. We encrypt the user-typed
// metadata (title + description) which is the part the user actually
// expects to be private, and leave the routing primitives alone.
links: { enabled: true, fields: ['title', 'description'] },
manaLinks: { enabled: false, fields: ['label', 'url', 'notes'] },
// ─── Inventar ────────────────────────────────────────────

View file

@ -5,6 +5,7 @@
<script lang="ts">
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import type { LocalContextSpace, LocalDocument } from './types';
let spaces = $state<LocalContextSpace[]>([]);
@ -24,10 +25,9 @@
$effect(() => {
const sub = liveQuery(async () => {
return db
.table<LocalDocument>('documents')
.toArray()
.then((all) => all.filter((d) => !d.deletedAt));
const all = await db.table<LocalDocument>('documents').toArray();
const visible = all.filter((d) => !d.deletedAt);
return decryptRecords('documents', visible);
}).subscribe((val) => {
documents = val ?? [];
});

View file

@ -8,6 +8,7 @@
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import type { LocalContextSpace, LocalDocument, Space, Document, DocumentType } from './types';
// ─── Type Converters ──────────────────────────────────────
@ -60,8 +61,9 @@ export function useAllSpaces() {
export function useAllDocuments() {
return useLiveQueryWithDefault(async () => {
const locals = await db.table<LocalDocument>('documents').toArray();
return locals
.filter((d) => !d.deletedAt)
const visible = locals.filter((d) => !d.deletedAt);
const decrypted = await decryptRecords('documents', visible);
return decrypted
.map(toDocument)
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
}, [] as Document[]);
@ -75,8 +77,9 @@ export function useSpaceDocuments(spaceId: string) {
.where('spaceId')
.equals(spaceId)
.toArray();
return locals
.filter((d) => !d.deletedAt)
const visible = locals.filter((d) => !d.deletedAt);
const decrypted = await decryptRecords('documents', visible);
return decrypted
.map(toDocument)
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
}, [] as Document[]);

View file

@ -5,6 +5,7 @@
<script lang="ts">
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import type { LocalMeal, LocalGoal } from './types';
let meals = $state<LocalMeal[]>([]);
@ -14,10 +15,9 @@
$effect(() => {
const sub = liveQuery(async () => {
return db
.table<LocalMeal>('meals')
.toArray()
.then((all) => all.filter((m) => !m.deletedAt));
const all = await db.table<LocalMeal>('meals').toArray();
const visible = all.filter((m) => !m.deletedAt);
return decryptRecords('meals', visible);
}).subscribe((val) => {
meals = val ?? [];
});

View file

@ -6,6 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import type {
LocalMeal,
LocalGoal,
@ -39,7 +40,9 @@ export function toMealWithNutrition(local: LocalMeal): MealWithNutrition {
export function useAllMeals() {
return liveQuery(async () => {
const locals = await db.table<LocalMeal>('meals').toArray();
return locals.filter((m) => !m.deletedAt).map(toMealWithNutrition);
const visible = locals.filter((m) => !m.deletedAt);
const decrypted = await decryptRecords('meals', visible);
return decrypted.map(toMealWithNutrition);
});
}

View file

@ -5,6 +5,7 @@
<script lang="ts">
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import type { LocalQuestion, LocalCollection } from './types';
import type { ViewProps } from '$lib/app-registry';
@ -15,10 +16,9 @@
$effect(() => {
const sub = liveQuery(async () => {
return db
.table<LocalQuestion>('questions')
.toArray()
.then((all) => all.filter((q) => !q.deletedAt));
const all = await db.table<LocalQuestion>('questions').toArray();
const visible = all.filter((q) => !q.deletedAt);
return decryptRecords('questions', visible);
}).subscribe((val) => {
questions = val ?? [];
});

View file

@ -6,6 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import type { LocalCollection, LocalQuestion, LocalAnswer } from './types';
// ─── Shared Types (inline to avoid cross-app dependency) ───
@ -114,7 +115,9 @@ export function useAllCollections() {
export function useAllQuestions() {
return liveQuery(async () => {
const locals = await db.table<LocalQuestion>('questions').toArray();
return locals.filter((q) => !q.deletedAt).map(toQuestion);
const visible = locals.filter((q) => !q.deletedAt);
const decrypted = await decryptRecords('questions', visible);
return decrypted.map(toQuestion);
});
}
@ -122,7 +125,9 @@ export function useAllQuestions() {
export function useAnswersByQuestion(questionId: string) {
return liveQuery(async () => {
const locals = await db.table<LocalAnswer>('answers').toArray();
return locals.filter((a) => !a.deletedAt && a.questionId === questionId).map(toAnswer);
const visible = locals.filter((a) => !a.deletedAt && a.questionId === questionId);
const decrypted = await decryptRecords('answers', visible);
return decrypted.map(toAnswer);
});
}

View file

@ -5,6 +5,7 @@
<script lang="ts">
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
import { toastStore } from '@mana/shared-ui/toast';
import { Trash } from '@mana/shared-icons';
import type { ViewProps } from '$lib/app-registry';
@ -32,31 +33,36 @@
});
$effect(() => {
const sub = liveQuery(() => db.table<LocalQuestion>('questions').get(questionId)).subscribe(
(val) => {
question = val ?? null;
if (val && !focused) {
editTitle = val.title;
editDescription = val.description ?? '';
editStatus = val.status;
editPriority = val.priority;
editResearchDepth = val.researchDepth;
}
const sub = liveQuery(async () => {
const raw = await db.table<LocalQuestion>('questions').get(questionId);
// title + description are encrypted on disk; decrypt a clone so
// the inline editor binds to plaintext.
return raw ? await decryptRecord('questions', { ...raw }) : null;
}).subscribe((val) => {
question = val ?? null;
if (val && !focused) {
editTitle = val.title;
editDescription = val.description ?? '';
editStatus = val.status;
editPriority = val.priority;
editResearchDepth = val.researchDepth;
}
);
});
return () => sub.unsubscribe();
});
async function saveField() {
focused = false;
await db.table('questions').update(questionId, {
const diff: Record<string, unknown> = {
title: editTitle.trim() || question?.title || 'Ohne Titel',
description: editDescription.trim() || undefined,
status: editStatus,
priority: editPriority,
researchDepth: editResearchDepth,
updatedAt: new Date().toISOString(),
});
};
await encryptRecord('questions', diff);
await db.table('questions').update(questionId, diff);
}
async function handleSelectChange() {

View file

@ -5,6 +5,7 @@
<script lang="ts">
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import type { LocalLink, LocalFolder } from './types';
import type { ViewProps } from '$lib/app-registry';
@ -15,10 +16,9 @@
$effect(() => {
const sub = liveQuery(async () => {
return db
.table<LocalLink>('links')
.toArray()
.then((all) => all.filter((l) => !l.deletedAt && l.isActive));
const all = await db.table<LocalLink>('links').toArray();
const visible = all.filter((l) => !l.deletedAt && l.isActive);
return decryptRecords('links', visible);
}).subscribe((val) => {
links = val ?? [];
});

View file

@ -8,6 +8,7 @@
import { liveQuery } from 'dexie';
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
import { db } from '$lib/data/database';
import { decryptRecord, decryptRecords } from '$lib/data/crypto';
import type { LocalLink, LocalTag, LocalFolder, LocalLinkTag } from './types';
// ─── Shared View Types ────────────────────────────────────
@ -133,7 +134,9 @@ export function toLinkTag(local: LocalLinkTag): LinkTag {
export function allLinks$() {
return liveQuery(async () => {
const locals = await db.table<LocalLink>('links').toArray();
return locals.filter((l) => !l.deletedAt).map(toLink);
const visible = locals.filter((l) => !l.deletedAt);
const decrypted = await decryptRecords('links', visible);
return decrypted.map(toLink);
});
}
@ -163,7 +166,9 @@ export function allLinkTags$() {
export function useAllLinks() {
return useLiveQueryWithDefault(async () => {
const locals = await db.table<LocalLink>('links').toArray();
return locals.filter((l) => !l.deletedAt).map(toLink);
const visible = locals.filter((l) => !l.deletedAt);
const decrypted = await decryptRecords('links', visible);
return decrypted.map(toLink);
}, [] as Link[]);
}
@ -194,7 +199,8 @@ export function useLinkById(id: string) {
if (!id) return null;
const local = await db.table<LocalLink>('links').get(id);
if (!local || local.deletedAt) return null;
return toLink(local);
const decrypted = await decryptRecord('links', { ...local });
return toLink(decrypted);
},
null as Link | null
);

View file

@ -5,6 +5,7 @@
<script lang="ts">
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
import { toastStore } from '@mana/shared-ui/toast';
import { Trash } from '@mana/shared-icons';
import type { ViewProps } from '$lib/app-registry';
@ -33,7 +34,12 @@
});
$effect(() => {
const sub = liveQuery(() => db.table<LocalLink>('links').get(linkId)).subscribe((val) => {
const sub = liveQuery(async () => {
const raw = await db.table<LocalLink>('links').get(linkId);
// title + description are encrypted on disk; decrypt a clone so
// the inline editor binds to plaintext.
return raw ? await decryptRecord('links', { ...raw }) : null;
}).subscribe((val) => {
link = val ?? null;
if (val && !focused) {
editTitle = val.title ?? '';
@ -49,7 +55,7 @@
async function saveField() {
focused = false;
await db.table('links').update(linkId, {
const diff: Record<string, unknown> = {
title: editTitle.trim() || undefined,
originalUrl: editOriginalUrl.trim() || link?.originalUrl || '',
customCode: editCustomCode.trim() || undefined,
@ -57,7 +63,9 @@
isActive: editIsActive,
expiresAt: editExpiresAt ? new Date(editExpiresAt).toISOString() : null,
updatedAt: new Date().toISOString(),
});
};
await encryptRecord('links', diff);
await db.table('links').update(linkId, diff);
}
async function handleActiveToggle() {

View file

@ -8,7 +8,8 @@
getAllDocumentTags,
} from '$lib/modules/context/queries';
import { documentTable } from '$lib/modules/context/collections';
import type { DocumentType } from '$lib/modules/context/types';
import { encryptRecord } from '$lib/data/crypto';
import type { DocumentType, LocalDocument } from '$lib/modules/context/types';
let searchQuery = $state('');
let typeFilter = $state<DocumentType | 'all'>('all');
@ -32,7 +33,7 @@
async function handleCreateDocument() {
const id = crypto.randomUUID();
await documentTable.add({
const row: LocalDocument = {
id,
spaceId: null,
title: 'Neues Dokument',
@ -41,7 +42,9 @@
shortId: null,
pinned: false,
metadata: null,
});
};
await encryptRecord('documents', row);
await documentTable.add(row);
goto(`/context/documents/${id}`);
}

View file

@ -4,6 +4,7 @@
import { ArrowLeft, Trash } from '@mana/shared-icons';
import { useAllDocuments, findDocumentById } from '$lib/modules/context/queries';
import { documentTable } from '$lib/modules/context/collections';
import { encryptRecord } from '$lib/data/crypto';
import type { DocumentType } from '$lib/modules/context/types';
let showDeleteConfirm = $state(false);
@ -47,7 +48,7 @@
.map((t) => t.trim())
.filter(Boolean);
const wordCount = editContent.split(/\s+/).filter(Boolean).length;
await documentTable.update(docId, {
const diff: Record<string, unknown> = {
title: editTitle,
content: editContent,
type: editType,
@ -56,7 +57,9 @@
tags,
word_count: wordCount,
},
});
};
await encryptRecord('documents', diff);
await documentTable.update(docId, diff);
saving = false;
}

View file

@ -11,7 +11,8 @@
findSpaceById,
} from '$lib/modules/context/queries';
import { contextSpaceTable, documentTable } from '$lib/modules/context/collections';
import type { DocumentType } from '$lib/modules/context/types';
import { encryptRecord } from '$lib/data/crypto';
import type { DocumentType, LocalDocument } from '$lib/modules/context/types';
let editingName = $state(false);
let editName = $state('');
@ -39,7 +40,7 @@
async function handleCreateDocument() {
const id = crypto.randomUUID();
await documentTable.add({
const row: LocalDocument = {
id,
spaceId,
title: 'Neues Dokument',
@ -48,7 +49,9 @@
shortId: null,
pinned: false,
metadata: null,
});
};
await encryptRecord('documents', row);
await documentTable.add(row);
goto(`/context/documents/${id}`);
}

View file

@ -2,6 +2,7 @@
import { _ } from 'svelte-i18n';
import { goto } from '$app/navigation';
import { db } from '$lib/data/database';
import { encryptRecord } from '$lib/data/crypto';
import { useAllFavorites } from '$lib/modules/nutriphi/queries';
import { MEAL_TYPE_LABELS, suggestMealType } from '$lib/modules/nutriphi/constants';
import type { MealType, NutritionData } from '$lib/modules/nutriphi/types';
@ -64,7 +65,7 @@
}
: null;
await db.table('meals').add({
const row: Record<string, unknown> = {
id: crypto.randomUUID(),
date: today,
mealType,
@ -75,7 +76,9 @@
nutrition,
createdAt: now,
updatedAt: now,
});
};
await encryptRecord('meals', row);
await db.table('meals').add(row);
goto('/nutriphi');
} catch {

View file

@ -2,6 +2,7 @@
import { _ } from 'svelte-i18n';
import { page } from '$app/stores';
import { db } from '$lib/data/database';
import { encryptRecord } from '$lib/data/crypto';
import {
useAllQuestions,
useAnswersByQuestion,
@ -67,11 +68,13 @@
async function saveEdit() {
if (!question || !editTitle.trim()) return;
await db.table('questions').update(question.id, {
const diff: Record<string, unknown> = {
title: editTitle.trim(),
description: editDescription.trim() || null,
updatedAt: new Date().toISOString(),
});
};
await encryptRecord('questions', diff);
await db.table('questions').update(question.id, diff);
editing = false;
}
@ -98,7 +101,7 @@
try {
const now = new Date().toISOString();
await db.table('answers').add({
const row: Record<string, unknown> = {
id: crypto.randomUUID(),
questionId: question.id,
researchResultId: null,
@ -108,7 +111,9 @@
isAccepted: false,
createdAt: now,
updatedAt: now,
});
};
await encryptRecord('answers', row);
await db.table('answers').add(row);
newAnswer = '';
// Mark question as answered if it was open

View file

@ -2,6 +2,7 @@
import { _ } from 'svelte-i18n';
import { goto } from '$app/navigation';
import { db } from '$lib/data/database';
import { encryptRecord } from '$lib/data/crypto';
import { useAllCollections } from '$lib/modules/questions/queries';
import type { ResearchDepth, QuestionPriority } from '$lib/modules/questions/types';
import { ArrowLeft, Lightning, Clock, Sparkle } from '@mana/shared-icons';
@ -63,7 +64,7 @@
const now = new Date().toISOString();
const id = crypto.randomUUID();
await db.table('questions').add({
const row: Record<string, unknown> = {
id,
collectionId: collectionId || null,
title: title.trim(),
@ -74,7 +75,9 @@
researchDepth,
createdAt: now,
updatedAt: now,
});
};
await encryptRecord('questions', row);
await db.table('questions').add(row);
goto(`/questions/${id}`);
} catch (e) {

View file

@ -13,6 +13,7 @@
type StatusFilter,
} from '$lib/modules/uload/queries';
import { linkTable, uloadFolderTable } from '$lib/modules/uload/collections';
import { encryptRecord } from '$lib/data/crypto';
import type { LocalLink } from '$lib/modules/uload/types';
import {
CaretRight,
@ -141,7 +142,7 @@
return;
}
await linkTable.add({
const newRow: LocalLink = {
id: crypto.randomUUID(),
shortCode,
customCode: newCustomCode || null,
@ -159,7 +160,9 @@
utmCampaign: newUtmCampaign || null,
folderId: selectedFolderId,
order: links.length,
} satisfies LocalLink);
};
await encryptRecord('links', newRow);
await linkTable.add(newRow);
toast.success(`Link erstellt: ${shortCode}`);
newUrl = '';
newTitle = '';
@ -205,7 +208,7 @@
return;
}
await linkTable.update(editingLink.id, {
const diff: Record<string, unknown> = {
originalUrl: editUrl,
title: editTitle || null,
utmSource: editUtmSource || null,
@ -214,7 +217,9 @@
expiresAt: editExpiresAt || null,
password: editPassword || null,
maxClicks,
});
};
await encryptRecord('links', diff);
await linkTable.update(editingLink.id, diff);
toast.success('Link aktualisiert');
editingLink = null;
}

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { ArrowLeft, Trash, DownloadSimple } from '@mana/shared-icons';
import { linkTable, uloadTagTable, uloadFolderTable, linkTagTable } from '$lib/modules/uload';
import { decryptRecords } from '$lib/data/crypto';
import { useAllLinks, useAllTags, useAllFolders } from '$lib/modules/uload';
import { toast } from 'svelte-sonner';
@ -20,7 +21,10 @@
}
async function exportData() {
const allLinks = await linkTable.toArray();
// Decrypt links before serializing the export — otherwise the user
// would download a JSON of ciphertext blobs they couldn't restore.
const rawLinks = await linkTable.toArray();
const allLinks = await decryptRecords('links', rawLinks);
const allTags = await uloadTagTable.toArray();
const allFolders = await uloadFolderTable.toArray();
const allLinkTags = await linkTagTable.toArray();