diff --git a/apps/mana/apps/web/src/lib/data/current-user.ts b/apps/mana/apps/web/src/lib/data/current-user.ts new file mode 100644 index 000000000..4c965a8f0 --- /dev/null +++ b/apps/mana/apps/web/src/lib/data/current-user.ts @@ -0,0 +1,36 @@ +/** + * Single source of truth for the current authenticated user id. + * + * Why a separate module? + * The data layer (database.ts hooks) needs to know who is writing each + * record so it can stamp `userId` automatically. Importing the auth store + * directly would couple the data layer to UI state and create a circular + * dependency. Instead, the root layout pushes the current user id here on + * every auth state change. + * + * Guest mode: when no user is signed in, records are stamped with the + * `GUEST_USER_ID` sentinel. The mana-sync backend treats these as anonymous + * and rejects them at the RLS layer once auth is required. + */ + +export const GUEST_USER_ID = 'guest'; + +let currentUserId: string | null = null; + +/** Updates the active user. Pass `null` for sign-out / guest. */ +export function setCurrentUserId(id: string | null): void { + currentUserId = id; +} + +/** Returns the active user id, or `null` if unauthenticated. */ +export function getCurrentUserId(): string | null { + return currentUserId; +} + +/** + * Returns the user id to stamp on local records: real user when signed in, + * `GUEST_USER_ID` otherwise. Always non-null so it can be used as a key. + */ +export function getEffectiveUserId(): string { + return currentUserId ?? GUEST_USER_ID; +} diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index 4b3fb9380..b5b5c39ce 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -11,6 +11,7 @@ import Dexie, { type EntityTable } from 'dexie'; import { trackFirstContent } from '$lib/stores/funnel-tracking'; import { fire as fireTrigger } from '$lib/triggers/registry'; import { checkInlineSuggestion } from '$lib/triggers/inline-suggest'; +import { getEffectiveUserId } from './current-user'; // ─── Database ────────────────────────────────────────────── @@ -576,6 +577,14 @@ for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) { if (_applyingServerChanges) return; const now = new Date().toISOString(); + // Auto-stamp the active user. Module stores never set userId themselves, + // preventing accidental impersonation and removing all hardcoded + // 'guest'/'local' fallbacks scattered across query files. + const objRecord = obj as Record; + if (objRecord.userId === undefined || objRecord.userId === null) { + objRecord.userId = getEffectiveUserId(); + } + // Stamp every real field with the create-time so future LWW comparisons // have a baseline. Mutates obj in place — Dexie persists the mutation. const ft: Record = {}; @@ -583,7 +592,7 @@ for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) { if (isInternalKey(key)) continue; ft[key] = now; } - (obj as Record)[FIELD_TIMESTAMPS_KEY] = ft; + objRecord[FIELD_TIMESTAMPS_KEY] = ft; // Build payload for pending-change WITHOUT the internal timestamp map const { [FIELD_TIMESTAMPS_KEY]: _omit, ...dataForSync } = obj as Record; @@ -613,6 +622,12 @@ for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) { const now = new Date().toISOString(); const fields: Record = {}; + // userId is immutable after creation. Silently strip any attempt to + // reassign it from a local update so a buggy or malicious caller can + // never re-parent records to a different user. + const mods = modifications as Record; + if ('userId' in mods) delete mods.userId; + // Merge field timestamps: keep existing, overwrite for each modified field const existingFT = ((obj as Record)[FIELD_TIMESTAMPS_KEY] as diff --git a/apps/mana/apps/web/src/lib/data/guest-migration.ts b/apps/mana/apps/web/src/lib/data/guest-migration.ts new file mode 100644 index 000000000..b2df98d02 --- /dev/null +++ b/apps/mana/apps/web/src/lib/data/guest-migration.ts @@ -0,0 +1,90 @@ +/** + * Guest → User data migration. + * + * In guest mode, the Dexie creating-hook stamps every record with + * `userId = GUEST_USER_ID`. Sync push is also a no-op while there is no + * auth token, so the records live purely in the local IndexedDB. + * + * When the user signs in for the first time we want their existing local + * data to be theirs. This module walks every sync-tracked table, finds + * records owned by `guest`, and re-creates them under the active user id. + * + * Why delete-and-re-add instead of an in-place update? + * The Dexie updating-hook deliberately strips `userId` from modifications + * (immutable once created), and even if we bypassed that, an `op: 'update'` + * pending-change would only ship the userId field to the server — other + * clients pulling the change would see a fresh record with just an id and + * userId. By deleting and re-adding we generate a clean `op: 'insert'` + * with the full record payload. + * + * Deleting in guest mode is safe because nothing was ever pushed to the + * server: `_pendingChanges` is cleared as part of the migration too, so the + * delete is purely local and never reaches the sync layer. + */ + +import { db, SYNC_APP_MAP, FIELD_TIMESTAMPS_KEY } from './database'; +import { GUEST_USER_ID } from './current-user'; + +export interface GuestMigrationResult { + migratedRecords: number; + tablesTouched: number; +} + +/** + * Re-stamps every `userId === 'guest'` record under the active user id. + * + * Caller must ensure `setCurrentUserId(newUserId)` was already invoked so + * that the Dexie creating-hook picks up the right id when re-inserting. + */ +export async function migrateGuestDataToUser(newUserId: string): Promise { + if (!newUserId || newUserId === GUEST_USER_ID) { + return { migratedRecords: 0, tablesTouched: 0 }; + } + + // Drop any pending changes accumulated during guest mode — they were + // never pushed (no auth token) and reference the old guest userId. The + // re-inserts below will produce fresh pending changes that should NOT be + // wiped, so this MUST happen before the migration loop. + await db.table('_pendingChanges').clear(); + + let migratedRecords = 0; + let tablesTouched = 0; + + for (const tables of Object.values(SYNC_APP_MAP)) { + for (const tableName of tables) { + const table = db.table(tableName); + + // Filter scan: userId is not indexed (and we don't want to widen the + // schema for a one-shot migration). Tables are typically small at + // this point because guest mode only stores what one person typed. + const guestRecords = await table + .filter((r: unknown) => (r as Record).userId === GUEST_USER_ID) + .toArray(); + + if (guestRecords.length === 0) continue; + tablesTouched++; + + // One transaction per table keeps the delete+add pair atomic and + // avoids leaving the table half-migrated if Dexie throws partway. + await db.transaction('rw', table, async () => { + for (const oldRecord of guestRecords) { + const record = oldRecord as Record; + const id = record.id as string; + + // Strip the bookkeeping fields the creating-hook will rebuild. + // Importantly, drop `userId` so the hook stamps the new id from + // getEffectiveUserId() instead of preserving 'guest'. + const { userId: _oldUser, [FIELD_TIMESTAMPS_KEY]: _oldFt, ...clean } = record; + void _oldUser; + void _oldFt; + + await table.delete(id); + await table.add({ ...clean, id }); + migratedRecords++; + } + }); + } + } + + return { migratedRecords, tablesTouched }; +} diff --git a/apps/mana/apps/web/src/lib/modules/calc/queries.ts b/apps/mana/apps/web/src/lib/modules/calc/queries.ts index c7c71d40b..b8d8111ad 100644 --- a/apps/mana/apps/web/src/lib/modules/calc/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/calc/queries.ts @@ -12,7 +12,7 @@ import type { Calculation, SavedFormula } from '@calc/shared'; export function toCalculation(local: LocalCalculation): Calculation { return { id: local.id, - userId: 'local', + userId: local.userId ?? '', mode: local.mode, expression: local.expression, result: local.result, @@ -24,7 +24,7 @@ export function toCalculation(local: LocalCalculation): Calculation { export function toSavedFormula(local: LocalSavedFormula): SavedFormula { return { id: local.id, - userId: 'local', + userId: local.userId ?? '', name: local.name, expression: local.expression, description: local.description ?? undefined, diff --git a/apps/mana/apps/web/src/lib/modules/cards/queries.ts b/apps/mana/apps/web/src/lib/modules/cards/queries.ts index c53c10934..5ffa55236 100644 --- a/apps/mana/apps/web/src/lib/modules/cards/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/cards/queries.ts @@ -13,7 +13,6 @@ import type { LocalDeck, LocalCard, Deck, Card } from './types'; export function toDeck(local: LocalDeck): Deck { return { id: local.id, - userId: 'local', title: local.name, description: local.description ?? undefined, color: local.color, diff --git a/apps/mana/apps/web/src/lib/modules/cards/types.ts b/apps/mana/apps/web/src/lib/modules/cards/types.ts index c5f063040..03b333d0e 100644 --- a/apps/mana/apps/web/src/lib/modules/cards/types.ts +++ b/apps/mana/apps/web/src/lib/modules/cards/types.ts @@ -27,7 +27,6 @@ export interface LocalCard extends BaseRecord { export interface Deck { id: string; - userId: string; title: string; description?: string; color: string; diff --git a/apps/mana/apps/web/src/lib/modules/chat/queries.ts b/apps/mana/apps/web/src/lib/modules/chat/queries.ts index a3d82662b..050fdbe67 100644 --- a/apps/mana/apps/web/src/lib/modules/chat/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/chat/queries.ts @@ -18,7 +18,6 @@ import type { export function toConversation(local: LocalConversation): Conversation { return { id: local.id, - userId: local.userId ?? 'guest', modelId: local.modelId ?? '', templateId: local.templateId ?? undefined, spaceId: local.spaceId ?? undefined, @@ -35,7 +34,6 @@ export function toConversation(local: LocalConversation): Conversation { export function toTemplate(local: LocalTemplate): Template { return { id: local.id, - userId: local.userId ?? 'guest', name: local.name, description: local.description || null, systemPrompt: local.systemPrompt, diff --git a/apps/mana/apps/web/src/lib/modules/chat/types.ts b/apps/mana/apps/web/src/lib/modules/chat/types.ts index ae92084f0..56fd13e60 100644 --- a/apps/mana/apps/web/src/lib/modules/chat/types.ts +++ b/apps/mana/apps/web/src/lib/modules/chat/types.ts @@ -36,7 +36,6 @@ export interface LocalTemplate extends BaseRecord { export interface Conversation { id: string; - userId: string; modelId: string; templateId?: string; spaceId?: string; @@ -60,7 +59,6 @@ export interface Message { export interface Template { id: string; - userId: string; name: string; description: string | null; systemPrompt: string; diff --git a/apps/mana/apps/web/src/lib/modules/contacts/queries.ts b/apps/mana/apps/web/src/lib/modules/contacts/queries.ts index fc6f80dea..f6879d7c5 100644 --- a/apps/mana/apps/web/src/lib/modules/contacts/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/contacts/queries.ts @@ -15,7 +15,6 @@ export function toContact(local: LocalContact): Contact { return { id: local.id, - userId: 'local', firstName, lastName, displayName, diff --git a/apps/mana/apps/web/src/lib/modules/contacts/types.ts b/apps/mana/apps/web/src/lib/modules/contacts/types.ts index 32fbc0d20..f41d01c8f 100644 --- a/apps/mana/apps/web/src/lib/modules/contacts/types.ts +++ b/apps/mana/apps/web/src/lib/modules/contacts/types.ts @@ -35,7 +35,6 @@ export interface LocalContact extends BaseRecord { export interface Contact { id: string; - userId: string; firstName?: string | null; lastName?: string | null; displayName?: string | null; diff --git a/apps/mana/apps/web/src/lib/modules/memoro/types.ts b/apps/mana/apps/web/src/lib/modules/memoro/types.ts index 12513f603..3c906bb60 100644 --- a/apps/mana/apps/web/src/lib/modules/memoro/types.ts +++ b/apps/mana/apps/web/src/lib/modules/memoro/types.ts @@ -7,7 +7,6 @@ import type { BaseRecord } from '@mana/local-store'; export type ProcessingStatus = 'pending' | 'processing' | 'completed' | 'failed'; export interface LocalMemo extends BaseRecord { - userId?: string; title: string | null; intro: string | null; transcript: string | null; @@ -45,7 +44,6 @@ export interface LocalMemo extends BaseRecord { export interface LocalMemory extends BaseRecord { memoId: string; - userId?: string; title: string; content: string | null; metadata?: Record; diff --git a/apps/mana/apps/web/src/lib/modules/planta/queries.ts b/apps/mana/apps/web/src/lib/modules/planta/queries.ts index ed5c4aa83..509bf3b12 100644 --- a/apps/mana/apps/web/src/lib/modules/planta/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/planta/queries.ts @@ -25,7 +25,6 @@ import type { export function toPlant(local: LocalPlant): Plant { return { id: local.id, - userId: 'local', name: local.name, scientificName: local.scientificName ?? undefined, commonName: local.commonName ?? undefined, @@ -49,7 +48,6 @@ export function toPlantPhoto(local: LocalPlantPhoto): PlantPhoto { return { id: local.id, plantId: local.plantId, - userId: 'local', storagePath: local.storagePath, publicUrl: local.publicUrl ?? undefined, filename: local.filename, @@ -69,7 +67,6 @@ export function toWateringSchedule(local: LocalWateringSchedule): WateringSchedu return { id: local.id, plantId: local.plantId, - userId: 'local', frequencyDays: local.frequencyDays, lastWateredAt: local.lastWateredAt ? new Date(local.lastWateredAt) : undefined, nextWateringAt: local.nextWateringAt ? new Date(local.nextWateringAt) : undefined, @@ -85,7 +82,6 @@ export function toWateringLog(local: LocalWateringLog): WateringLog { return { id: local.id, plantId: local.plantId, - userId: 'local', wateredAt: new Date(local.wateredAt), notes: local.notes ?? undefined, createdAt: new Date(local.createdAt ?? new Date().toISOString()), diff --git a/apps/mana/apps/web/src/lib/modules/planta/types.ts b/apps/mana/apps/web/src/lib/modules/planta/types.ts index 4437297e8..ce1a804d3 100644 --- a/apps/mana/apps/web/src/lib/modules/planta/types.ts +++ b/apps/mana/apps/web/src/lib/modules/planta/types.ts @@ -62,7 +62,6 @@ export interface LocalWateringLog extends BaseRecord { export interface Plant { id: string; - userId: string; name: string; scientificName?: string; commonName?: string; @@ -83,7 +82,6 @@ export interface Plant { export interface PlantPhoto { id: string; plantId: string; - userId: string; storagePath: string; publicUrl?: string; filename: string; @@ -100,7 +98,6 @@ export interface PlantPhoto { export interface WateringSchedule { id: string; plantId: string; - userId: string; frequencyDays: number; lastWateredAt?: Date; nextWateringAt?: Date; @@ -113,7 +110,6 @@ export interface WateringSchedule { export interface WateringLog { id: string; plantId: string; - userId: string; wateredAt: Date; notes?: string; createdAt: Date; diff --git a/apps/mana/apps/web/src/lib/modules/presi/queries.ts b/apps/mana/apps/web/src/lib/modules/presi/queries.ts index d548a415b..bf220fb64 100644 --- a/apps/mana/apps/web/src/lib/modules/presi/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/presi/queries.ts @@ -14,7 +14,6 @@ import type { LocalDeck, LocalSlide, Deck, Slide } from './types'; export function toDeck(local: LocalDeck): Deck { return { id: local.id, - userId: 'local', title: local.title, description: local.description ?? undefined, themeId: local.themeId ?? undefined, diff --git a/apps/mana/apps/web/src/lib/modules/presi/types.ts b/apps/mana/apps/web/src/lib/modules/presi/types.ts index a0f4170a1..188a4c395 100644 --- a/apps/mana/apps/web/src/lib/modules/presi/types.ts +++ b/apps/mana/apps/web/src/lib/modules/presi/types.ts @@ -30,7 +30,6 @@ export interface SlideContent { export interface Deck { id: string; - userId: string; title: string; description?: string; themeId?: string; diff --git a/apps/mana/apps/web/src/lib/modules/storage/queries.ts b/apps/mana/apps/web/src/lib/modules/storage/queries.ts index 7e8a62a8c..00c43e22e 100644 --- a/apps/mana/apps/web/src/lib/modules/storage/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/storage/queries.ts @@ -12,7 +12,6 @@ import type { LocalFile, LocalFolder, LocalFileTag } from './types'; export interface StorageFile { id: string; - userId: string; name: string; originalName: string; mimeType: string; @@ -30,7 +29,6 @@ export interface StorageFile { export interface StorageFolder { id: string; - userId: string; name: string; description: string | null; color: string | null; @@ -46,7 +44,6 @@ export interface StorageFolder { export interface StorageTag { id: string; - userId: string; name: string; color: string | null; createdAt: string; @@ -57,7 +54,6 @@ export interface StorageTag { export function toFile(local: LocalFile): StorageFile { return { id: local.id, - userId: 'local', name: local.name, originalName: local.originalName, mimeType: local.mimeType, @@ -77,7 +73,6 @@ export function toFile(local: LocalFile): StorageFile { export function toFolder(local: LocalFolder): StorageFolder { return { id: local.id, - userId: 'local', name: local.name, description: local.description ?? null, color: local.color ?? null, @@ -100,7 +95,6 @@ export function toTag(local: { }): StorageTag { return { id: local.id, - userId: 'local', name: local.name, color: local.color ?? null, createdAt: local.createdAt ?? new Date().toISOString(), diff --git a/apps/mana/apps/web/src/lib/modules/times/queries.ts b/apps/mana/apps/web/src/lib/modules/times/queries.ts index 09013070b..dbf4f9889 100644 --- a/apps/mana/apps/web/src/lib/modules/times/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/times/queries.ts @@ -130,7 +130,6 @@ export function toSettings(local: LocalSettings): TimesSettings { export function toAlarm(local: LocalAlarm): Alarm { return { id: local.id, - userId: 'local', label: local.label, time: local.time, enabled: local.enabled, @@ -146,7 +145,6 @@ export function toAlarm(local: LocalAlarm): Alarm { export function toCountdownTimer(local: LocalCountdownTimer): Timer { return { id: local.id, - userId: 'local', label: local.label, durationSeconds: local.durationSeconds, remainingSeconds: local.remainingSeconds, @@ -162,7 +160,6 @@ export function toCountdownTimer(local: LocalCountdownTimer): Timer { export function toWorldClock(local: LocalWorldClock): WorldClock { return { id: local.id, - userId: 'local', timezone: local.timezone, cityName: local.cityName, sortOrder: local.sortOrder, diff --git a/apps/mana/apps/web/src/lib/modules/times/stores/session-alarms.svelte.ts b/apps/mana/apps/web/src/lib/modules/times/stores/session-alarms.svelte.ts index 2b04783ab..45dbfb976 100644 --- a/apps/mana/apps/web/src/lib/modules/times/stores/session-alarms.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/times/stores/session-alarms.svelte.ts @@ -62,7 +62,6 @@ export const sessionAlarmsStore = { const now = new Date().toISOString(); const alarm: Alarm = { id: generateSessionId(), - userId: 'guest', label: input.label || null, time: input.time, enabled: input.enabled ?? true, diff --git a/apps/mana/apps/web/src/lib/modules/times/stores/session-countdown-timers.svelte.ts b/apps/mana/apps/web/src/lib/modules/times/stores/session-countdown-timers.svelte.ts index 8909191b6..e467010a7 100644 --- a/apps/mana/apps/web/src/lib/modules/times/stores/session-countdown-timers.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/times/stores/session-countdown-timers.svelte.ts @@ -63,7 +63,6 @@ export const sessionCountdownTimersStore = { const now = new Date().toISOString(); const timer: Timer = { id: generateSessionId(), - userId: 'guest', label: input.label || null, durationSeconds: input.durationSeconds, remainingSeconds: input.durationSeconds, diff --git a/apps/mana/apps/web/src/lib/modules/times/types.ts b/apps/mana/apps/web/src/lib/modules/times/types.ts index 8c7121861..216586a8a 100644 --- a/apps/mana/apps/web/src/lib/modules/times/types.ts +++ b/apps/mana/apps/web/src/lib/modules/times/types.ts @@ -251,7 +251,6 @@ export type TimerStatus = 'idle' | 'running' | 'paused' | 'finished'; export interface Alarm { id: string; - userId: string; label: string | null; time: string; // HH:MM:SS format enabled: boolean; @@ -285,7 +284,6 @@ export interface UpdateAlarmInput { export interface Timer { id: string; - userId: string; label: string | null; durationSeconds: number; remainingSeconds: number | null; @@ -311,7 +309,6 @@ export interface UpdateTimerInput { export interface WorldClock { id: string; - userId: string; timezone: string; // IANA timezone e.g. 'America/New_York' cityName: string; sortOrder: number; diff --git a/apps/mana/apps/web/src/lib/modules/todo/queries.ts b/apps/mana/apps/web/src/lib/modules/todo/queries.ts index 0337194d5..f25107265 100644 --- a/apps/mana/apps/web/src/lib/modules/todo/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/todo/queries.ts @@ -20,7 +20,6 @@ export function toTask(local: LocalTask): Task { return { id: local.id, projectId: (local as Record).projectId as string | null | undefined, - userId: local.userId ?? 'guest', title: local.title, description: local.description, dueDate: local.dueDate, diff --git a/apps/mana/apps/web/src/lib/modules/todo/types.ts b/apps/mana/apps/web/src/lib/modules/todo/types.ts index b20bcc6a5..f0db21e2d 100644 --- a/apps/mana/apps/web/src/lib/modules/todo/types.ts +++ b/apps/mana/apps/web/src/lib/modules/todo/types.ts @@ -24,7 +24,6 @@ export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'; export interface LocalTask extends BaseRecord { title: string; description?: string; - userId?: string; projectId?: string | null; priority: TaskPriority; isCompleted: boolean; @@ -45,7 +44,6 @@ export interface LocalTaskTag extends BaseRecord { export interface LocalReminder extends BaseRecord { taskId: string; - userId?: string; minutesBefore: number; type: 'push' | 'email' | 'both'; status: 'pending' | 'sent' | 'failed'; @@ -101,7 +99,6 @@ export interface LocalTodoProject extends BaseRecord { export interface Task { id: string; projectId?: string | null; - userId: string; title: string; description?: string | null; dueDate?: string | null; diff --git a/apps/mana/apps/web/src/routes/+layout.svelte b/apps/mana/apps/web/src/routes/+layout.svelte index 9ccb53d1e..a91a59ebb 100644 --- a/apps/mana/apps/web/src/routes/+layout.svelte +++ b/apps/mana/apps/web/src/routes/+layout.svelte @@ -5,24 +5,51 @@ import { authStore } from '$lib/stores/auth.svelte'; import { networkStore } from '$lib/stores/network.svelte'; import { loadAutomations } from '$lib/triggers'; + import { setCurrentUserId } from '$lib/data/current-user'; + import { migrateGuestDataToUser } from '$lib/data/guest-migration'; import SuggestionToast from '$lib/components/SuggestionToast.svelte'; import OfflineIndicator from '$lib/components/OfflineIndicator.svelte'; import PwaUpdatePrompt from '$lib/components/PwaUpdatePrompt.svelte'; let { children } = $props(); - onMount(async () => { + // Tracks whether we have already attempted the guest → user migration in + // this app load. The migration is idempotent (no guest records → no-op) + // so this just prevents redundant table scans on every auth state change. + let guestMigrationAttempted = false; + + // Push the active user id into the data layer whenever auth state changes. + // The Dexie creating-hook reads this to auto-stamp `userId` on every record, + // so module stores never need to know who the current user is. + $effect(() => { + const userId = authStore.user?.id ?? null; + setCurrentUserId(userId); + + // First time we see an authenticated user in this session, lift any + // guest records into their account so the data they typed before + // signing up follows them. + if (userId && !guestMigrationAttempted) { + guestMigrationAttempted = true; + migrateGuestDataToUser(userId).catch((err) => { + console.error('[mana] guest → user migration failed:', err); + }); + } + }); + + onMount(() => { // Initialize theme const cleanupTheme = theme.initialize(); // Initialize network status tracking networkStore.initialize(); - // Initialize auth - await authStore.initialize(); - - // Load cross-module automation triggers - await loadAutomations(); + // Auth + automation loading is async — fire and forget. Returning + // cleanup from an async onMount would silently drop it, so the async + // work runs in an inner IIFE while the outer arrow stays sync. + void (async () => { + await authStore.initialize(); + await loadAutomations(); + })(); return () => { cleanupTheme(); diff --git a/packages/local-store/src/types.ts b/packages/local-store/src/types.ts index ac3e6fa4f..beda23192 100644 --- a/packages/local-store/src/types.ts +++ b/packages/local-store/src/types.ts @@ -5,6 +5,11 @@ /** Base record that all local-store entities must extend. */ export interface BaseRecord { id: string; + /** + * Owner of this record. Auto-stamped by the Dexie creating-hook from the + * active session user; module stores must never set this themselves. + */ + userId?: string; createdAt?: string; updatedAt?: string; deletedAt?: string | null;