Fix live journal freeze and passkey login on localhost.
Harden live log init with safe per-entry decrypt, stable loading state, and no parallel list scan in live mode. Improve multi-sail picker UX, stop WebAuthn retry after user cancel, redirect 127.0.0.1 to localhost, and tolerate missing appearance prefs table. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { clearLogbookKeysCache } from './logbookKeys.js'
|
||||
import { PlausibleEvents, trackPlausibleEvent } from './analytics.js'
|
||||
import { db } from './db.js'
|
||||
import { apiFetch, apiJson } from './api.js'
|
||||
import { isWebAuthnUserAbortError } from '../utils/passkeyHost.js'
|
||||
|
||||
const API_BASE = '/api/auth'
|
||||
|
||||
@@ -361,7 +362,11 @@ export async function loginUser(username?: string): Promise<LoginResult> {
|
||||
const prfRequested = !!options.extensions?.prf
|
||||
try {
|
||||
credentialResponse = await startAuthentication({ optionsJSON: options })
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
// User cancelled or timed out — never open a second platform prompt.
|
||||
if (isWebAuthnUserAbortError(err)) {
|
||||
throw err
|
||||
}
|
||||
if (prfRequested) {
|
||||
console.warn('Passkey authentication with PRF extension failed, retrying without PRF:', err)
|
||||
if (options.extensions) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { tryDecryptEntryPayload } from './quickEventLog.js'
|
||||
|
||||
vi.mock('./crypto.js', () => ({
|
||||
decryptJson: vi.fn(async (_c: string, _i: string, _t: string) => {
|
||||
throw new Error('decrypt failed')
|
||||
}),
|
||||
encryptJson: vi.fn()
|
||||
}))
|
||||
|
||||
describe('tryDecryptEntryPayload', () => {
|
||||
it('returns null when decryption fails', async () => {
|
||||
const result = await tryDecryptEntryPayload(
|
||||
{ encryptedData: 'x', iv: 'y', tag: 'z' },
|
||||
new ArrayBuffer(32)
|
||||
)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from './db.js'
|
||||
import { getActiveMasterKey } from './auth.js'
|
||||
import { getLogbookKey } from './logbookKeys.js'
|
||||
import { ensureLogbookKey, getLogbookKey } from './logbookKeys.js'
|
||||
import { decryptJson, encryptJson } from './crypto.js'
|
||||
import { syncLogbook } from './sync.js'
|
||||
import {
|
||||
@@ -24,12 +24,36 @@ export interface LoadedEntry {
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
type EncryptedRecord = {
|
||||
encryptedData: string
|
||||
iv: string
|
||||
tag: string
|
||||
}
|
||||
|
||||
async function getMasterKey(logbookId: string): Promise<ArrayBuffer> {
|
||||
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
|
||||
if (!masterKey) throw new Error('Encryption key not found. Please log in.')
|
||||
return masterKey
|
||||
}
|
||||
|
||||
/** Decrypt one record; skip corrupt or legacy entries instead of aborting the whole scan. */
|
||||
export async function tryDecryptEntryPayload(
|
||||
record: EncryptedRecord,
|
||||
key: ArrayBuffer
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
try {
|
||||
return await decryptJson(record.encryptedData, record.iv, record.tag, key)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function sortEntriesNewestFirst<T extends { updatedAt: string }>(entries: T[]): T[] {
|
||||
return [...entries].sort(
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
)
|
||||
}
|
||||
|
||||
function tankLevelsFromData(data: Record<string, unknown>) {
|
||||
const fw = (data.freshwater as Record<string, number> | undefined) ?? {
|
||||
morning: 0, refilled: 0, evening: 0, consumption: 0
|
||||
@@ -110,7 +134,7 @@ export async function loadEntry(logbookId: string, entryId: string): Promise<Loa
|
||||
const masterKey = await getMasterKey(logbookId)
|
||||
const record = await db.entries.get(entryId)
|
||||
if (!record) return null
|
||||
const data = await decryptJson(record.encryptedData, record.iv, record.tag, masterKey)
|
||||
const data = await tryDecryptEntryPayload(record, masterKey)
|
||||
if (!data) return null
|
||||
return { payloadId: record.payloadId, updatedAt: record.updatedAt, data }
|
||||
}
|
||||
@@ -118,10 +142,10 @@ export async function loadEntry(logbookId: string, entryId: string): Promise<Loa
|
||||
export async function findTodayEntryId(logbookId: string): Promise<string | null> {
|
||||
const todayStr = new Date().toISOString().substring(0, 10)
|
||||
const masterKey = await getMasterKey(logbookId)
|
||||
const local = await db.entries.where({ logbookId }).toArray()
|
||||
const local = sortEntriesNewestFirst(await db.entries.where({ logbookId }).toArray())
|
||||
|
||||
for (const entry of local) {
|
||||
const decrypted = await decryptJson(entry.encryptedData, entry.iv, entry.tag, masterKey)
|
||||
const decrypted = await tryDecryptEntryPayload(entry, masterKey)
|
||||
if (decrypted && String(decrypted.date) === todayStr) {
|
||||
return entry.payloadId
|
||||
}
|
||||
@@ -135,8 +159,10 @@ export async function createTodayEntry(logbookId: string): Promise<string> {
|
||||
const decryptedEntries: Array<LogEntryTankSource & TravelDaySortable> = []
|
||||
|
||||
for (const entry of localEntries) {
|
||||
const decrypted = await decryptJson(entry.encryptedData, entry.iv, entry.tag, masterKey)
|
||||
if (decrypted) decryptedEntries.push(decrypted as LogEntryTankSource & TravelDaySortable)
|
||||
const decrypted = await tryDecryptEntryPayload(entry, masterKey)
|
||||
if (decrypted) {
|
||||
decryptedEntries.push(decrypted as LogEntryTankSource & TravelDaySortable)
|
||||
}
|
||||
}
|
||||
|
||||
decryptedEntries.sort(compareTravelDaysChronological)
|
||||
@@ -185,6 +211,7 @@ export async function createTodayEntry(logbookId: string): Promise<string> {
|
||||
}
|
||||
|
||||
export async function findOrCreateTodayEntry(logbookId: string): Promise<string> {
|
||||
await ensureLogbookKey(logbookId)
|
||||
const existing = await findTodayEntryId(logbookId)
|
||||
if (existing) return existing
|
||||
return createTodayEntry(logbookId)
|
||||
|
||||
Reference in New Issue
Block a user