diff --git a/docs-site/docs/configure/auth.md b/docs-site/docs/configure/auth.md index 4967bd1..6332449 100644 --- a/docs-site/docs/configure/auth.md +++ b/docs-site/docs/configure/auth.md @@ -61,3 +61,4 @@ Admin assignment is reconciled on every session resolution, so removing an email ## Claim modal note - You may still see old anonymous settings/progress available to claim from older deployments. +- Legacy `unclaimed` data is only surfaced through the claim flow; normal authenticated routes are scoped to your current user id. diff --git a/src/app/api/audiobook/chapter/route.ts b/src/app/api/audiobook/chapter/route.ts index c0342a3..70e78f7 100644 --- a/src/app/api/audiobook/chapter/route.ts +++ b/src/app/api/audiobook/chapter/route.ts @@ -4,7 +4,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; import { randomUUID } from 'crypto'; -import { and, eq, inArray } from 'drizzle-orm'; +import { and, eq } from 'drizzle-orm'; import { db } from '@/db'; import { audiobooks, audiobookChapters } from '@/db/schema'; import { requireAuthContext } from '@/lib/server/auth/auth'; @@ -26,8 +26,7 @@ import { ffprobeAudio, } from '@/lib/server/audiobooks/chapters'; import { isS3Configured } from '@/lib/server/storage/s3'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; -import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobooks/user-scope'; +import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace'; import { getFFmpegPath } from '@/lib/server/audiobooks/ffmpeg-bin'; import { generateTTSBuffer } from '@/lib/server/tts/generate'; import { resolveTtsCredentials } from '@/lib/server/admin/resolve-credentials'; @@ -285,29 +284,18 @@ export async function POST(request: NextRequest) { const ctxOrRes = await requireAuthContext(request); if (ctxOrRes instanceof Response) return ctxOrRes; + if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const { userId, user } = ctxOrRes; + const { user } = ctxOrRes; + const storageUserId = ctxOrRes.userId; const runtimeConfig = await getResolvedRuntimeConfig(); const testNamespace = getOpenReaderTestNamespace(request.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const { preferredUserId, allowedUserIds } = buildAllowedAudiobookUserIds(userId, unclaimedUserId); const bookId = data.bookId || randomUUID(); if (!isSafeId(bookId)) { return NextResponse.json({ error: 'Invalid bookId parameter' }, { status: 400 }); } - const existingBookRows = await db - .select({ userId: audiobooks.userId }) - .from(audiobooks) - .where(and(eq(audiobooks.id, bookId), inArray(audiobooks.userId, allowedUserIds))); - const storageUserId = - pickAudiobookOwner( - existingBookRows.map((book: { userId: string }) => book.userId), - preferredUserId, - unclaimedUserId, - ) ?? preferredUserId; - await db .insert(audiobooks) .values({ @@ -540,7 +528,7 @@ export async function POST(request: NextRequest) { ipAuthenticated: runtimeConfig.ttsIpDailyLimitAuthenticated, }); - if (userId && ttsRateLimitEnabled) { + if (ttsRateLimitEnabled) { const isAnonymous = Boolean(user?.isAnonymous); const charCount = data.text.length; const ip = getClientIp(request); @@ -551,7 +539,7 @@ export async function POST(request: NextRequest) { } const rateLimitResult = await rateLimiter.checkAndIncrementLimit( - { id: userId, isAnonymous }, + { id: storageUserId, isAnonymous }, charCount, { deviceId: device?.deviceId ?? null, @@ -808,26 +796,20 @@ export async function GET(request: NextRequest) { const ctxOrRes = await requireAuthContext(request); if (ctxOrRes instanceof Response) return ctxOrRes; + if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const { userId } = ctxOrRes; + const storageUserId = ctxOrRes.userId; const testNamespace = getOpenReaderTestNamespace(request.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const { preferredUserId, allowedUserIds } = buildAllowedAudiobookUserIds(userId, unclaimedUserId); const existingBookRows = await db .select({ userId: audiobooks.userId }) .from(audiobooks) - .where(and(eq(audiobooks.id, bookId), inArray(audiobooks.userId, allowedUserIds))); - const existingBookUserId = pickAudiobookOwner( - existingBookRows.map((book: { userId: string }) => book.userId), - preferredUserId, - unclaimedUserId, - ); + .where(and(eq(audiobooks.id, bookId), eq(audiobooks.userId, storageUserId))); - if (!existingBookUserId) { + if (existingBookRows.length === 0) { return NextResponse.json({ error: 'Book not found' }, { status: 404 }); } - const objects = await listAudiobookObjects(bookId, existingBookUserId, testNamespace); + const objects = await listAudiobookObjects(bookId, storageUserId, testNamespace); const chapter = findChapterFileNameByIndex( objects.map((object) => object.fileName), chapterIndex, @@ -839,7 +821,7 @@ export async function GET(request: NextRequest) { .where( and( eq(audiobookChapters.bookId, bookId), - eq(audiobookChapters.userId, existingBookUserId), + eq(audiobookChapters.userId, storageUserId), eq(audiobookChapters.chapterIndex, chapterIndex), ), ); @@ -848,7 +830,7 @@ export async function GET(request: NextRequest) { let buffer: Buffer; try { - buffer = await getAudiobookObjectBuffer(bookId, existingBookUserId, chapter.fileName, testNamespace); + buffer = await getAudiobookObjectBuffer(bookId, storageUserId, chapter.fileName, testNamespace); } catch (error) { if (isMissingBlobError(error)) { await db @@ -856,7 +838,7 @@ export async function GET(request: NextRequest) { .where( and( eq(audiobookChapters.bookId, bookId), - eq(audiobookChapters.userId, existingBookUserId), + eq(audiobookChapters.userId, storageUserId), eq(audiobookChapters.chapterIndex, chapterIndex), ), ); @@ -905,22 +887,16 @@ export async function DELETE(request: NextRequest) { const ctxOrRes = await requireAuthContext(request); if (ctxOrRes instanceof Response) return ctxOrRes; + if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const { userId } = ctxOrRes; + const storageUserId = ctxOrRes.userId; const testNamespace = getOpenReaderTestNamespace(request.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const { preferredUserId, allowedUserIds } = buildAllowedAudiobookUserIds(userId, unclaimedUserId); const existingBookRows = await db .select({ userId: audiobooks.userId }) .from(audiobooks) - .where(and(eq(audiobooks.id, bookId), inArray(audiobooks.userId, allowedUserIds))); - const storageUserId = pickAudiobookOwner( - existingBookRows.map((book: { userId: string }) => book.userId), - preferredUserId, - unclaimedUserId, - ); + .where(and(eq(audiobooks.id, bookId), eq(audiobooks.userId, storageUserId))); - if (!storageUserId) { + if (existingBookRows.length === 0) { return NextResponse.json({ error: 'Book not found' }, { status: 404 }); } diff --git a/src/app/api/audiobook/route.ts b/src/app/api/audiobook/route.ts index 033b010..336c772 100644 --- a/src/app/api/audiobook/route.ts +++ b/src/app/api/audiobook/route.ts @@ -3,7 +3,7 @@ import { spawn } from 'child_process'; import { mkdtemp, readFile, rm, writeFile } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; -import { and, eq, inArray } from 'drizzle-orm'; +import { and, eq } from 'drizzle-orm'; import { db } from '@/db'; import { audiobooks, audiobookChapters } from '@/db/schema'; import { requireAuthContext } from '@/lib/server/auth/auth'; @@ -23,9 +23,8 @@ import { ffprobeAudio, } from '@/lib/server/audiobooks/chapters'; import { isS3Configured } from '@/lib/server/storage/s3'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; +import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace'; import { getFFmpegPath } from '@/lib/server/audiobooks/ffmpeg-bin'; -import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobooks/user-scope'; import type { TTSAudiobookFormat } from '@/types/tts'; export const dynamic = 'force-dynamic'; @@ -168,25 +167,19 @@ export async function GET(request: NextRequest) { const ctxOrRes = await requireAuthContext(request); if (ctxOrRes instanceof Response) return ctxOrRes; + if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const { userId } = ctxOrRes; + const storageUserId = ctxOrRes.userId; const testNamespace = getOpenReaderTestNamespace(request.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const { preferredUserId, allowedUserIds } = buildAllowedAudiobookUserIds(userId, unclaimedUserId); const existingBookRows = await db .select({ userId: audiobooks.userId }) .from(audiobooks) - .where(and(eq(audiobooks.id, bookId), inArray(audiobooks.userId, allowedUserIds))); - const existingBookUserId = pickAudiobookOwner( - existingBookRows.map((book: { userId: string }) => book.userId), - preferredUserId, - unclaimedUserId, - ); - if (!existingBookUserId) { + .where(and(eq(audiobooks.id, bookId), eq(audiobooks.userId, storageUserId))); + if (existingBookRows.length === 0) { return NextResponse.json({ error: 'Book not found' }, { status: 404 }); } - const objects = await listAudiobookObjects(bookId, existingBookUserId, testNamespace); + const objects = await listAudiobookObjects(bookId, storageUserId, testNamespace); const objectNames = objects.map((item) => item.fileName); const chapters = listChapterObjects(objectNames); if (chapters.length === 0) { @@ -205,9 +198,9 @@ export async function GET(request: NextRequest) { if (objectNames.includes(completeName) && objectNames.includes(manifestName)) { try { - const manifest = JSON.parse((await getAudiobookObjectBuffer(bookId, existingBookUserId, manifestName, testNamespace)).toString('utf8')); + const manifest = JSON.parse((await getAudiobookObjectBuffer(bookId, storageUserId, manifestName, testNamespace)).toString('utf8')); if (JSON.stringify(manifest) === JSON.stringify(signature)) { - const cached = await getAudiobookObjectBuffer(bookId, existingBookUserId, completeName, testNamespace); + const cached = await getAudiobookObjectBuffer(bookId, storageUserId, completeName, testNamespace); return new NextResponse(streamBuffer(cached), { headers: { 'Content-Type': chapterFileMimeType(format), @@ -220,14 +213,14 @@ export async function GET(request: NextRequest) { // Force regeneration below. } - await deleteAudiobookObject(bookId, existingBookUserId, completeName, testNamespace).catch(() => {}); - await deleteAudiobookObject(bookId, existingBookUserId, manifestName, testNamespace).catch(() => {}); + await deleteAudiobookObject(bookId, storageUserId, completeName, testNamespace).catch(() => {}); + await deleteAudiobookObject(bookId, storageUserId, manifestName, testNamespace).catch(() => {}); } const chapterRows = await db .select({ chapterIndex: audiobookChapters.chapterIndex, duration: audiobookChapters.duration }) .from(audiobookChapters) - .where(and(eq(audiobookChapters.bookId, bookId), eq(audiobookChapters.userId, existingBookUserId))); + .where(and(eq(audiobookChapters.bookId, bookId), eq(audiobookChapters.userId, storageUserId))); const durationByIndex = new Map(); for (const row of chapterRows) { durationByIndex.set(row.chapterIndex, Number(row.duration ?? 0)); @@ -241,7 +234,7 @@ export async function GET(request: NextRequest) { const localChapters: Array<{ index: number; title: string; localPath: string; duration: number }> = []; for (const chapter of chapters) { const localPath = join(workDir, chapter.fileName); - const bytes = await getAudiobookObjectBuffer(bookId, existingBookUserId, chapter.fileName, testNamespace); + const bytes = await getAudiobookObjectBuffer(bookId, storageUserId, chapter.fileName, testNamespace); await writeFile(localPath, bytes); let duration = 0; @@ -356,10 +349,10 @@ export async function GET(request: NextRequest) { await ensurePositiveDuration(outputPath, request.signal); const outputBytes = await readFile(outputPath); - await putAudiobookObject(bookId, existingBookUserId, completeName, outputBytes, chapterFileMimeType(format), testNamespace); + await putAudiobookObject(bookId, storageUserId, completeName, outputBytes, chapterFileMimeType(format), testNamespace); await putAudiobookObject( bookId, - existingBookUserId, + storageUserId, manifestName, Buffer.from(JSON.stringify(signature, null, 2), 'utf8'), 'application/json; charset=utf-8', @@ -404,21 +397,15 @@ export async function DELETE(request: NextRequest) { const ctxOrRes = await requireAuthContext(request); if (ctxOrRes instanceof Response) return ctxOrRes; - const { userId } = ctxOrRes; + if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const storageUserId = ctxOrRes.userId; const testNamespace = getOpenReaderTestNamespace(request.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const { preferredUserId, allowedUserIds } = buildAllowedAudiobookUserIds(userId, unclaimedUserId); const existingBookRows = await db .select({ userId: audiobooks.userId }) .from(audiobooks) - .where(and(eq(audiobooks.id, bookId), inArray(audiobooks.userId, allowedUserIds))); - const storageUserId = pickAudiobookOwner( - existingBookRows.map((book: { userId: string }) => book.userId), - preferredUserId, - unclaimedUserId, - ); + .where(and(eq(audiobooks.id, bookId), eq(audiobooks.userId, storageUserId))); - if (!storageUserId) { + if (existingBookRows.length === 0) { return NextResponse.json({ error: 'Book not found' }, { status: 404 }); } diff --git a/src/app/api/audiobook/status/route.ts b/src/app/api/audiobook/status/route.ts index 9a1f561..6416dc3 100644 --- a/src/app/api/audiobook/status/route.ts +++ b/src/app/api/audiobook/status/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { and, eq, inArray } from 'drizzle-orm'; +import { and, eq } from 'drizzle-orm'; import { db } from '@/db'; import { audiobooks, audiobookChapters } from '@/db/schema'; import { requireAuthContext } from '@/lib/server/auth/auth'; @@ -7,8 +7,7 @@ import { getAudiobookObjectBuffer, isMissingBlobError, listAudiobookObjects } fr import { decodeChapterFileName } from '@/lib/server/audiobooks/chapters'; import { pruneAudiobookChaptersNotOnDisk } from '@/lib/server/audiobooks/prune'; import { isS3Configured } from '@/lib/server/storage/s3'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; -import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobooks/user-scope'; +import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace'; import type { AudiobookGenerationSettings } from '@/types/client'; import type { TTSAudiobookChapter, TTSAudiobookFormat } from '@/types/tts'; import { errorToLog, serverLogger } from '@/lib/server/logger'; @@ -73,22 +72,16 @@ export async function GET(request: NextRequest) { const ctxOrRes = await requireAuthContext(request); if (ctxOrRes instanceof Response) return ctxOrRes; + if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const { userId } = ctxOrRes; + const storageUserId = ctxOrRes.userId; const testNamespace = getOpenReaderTestNamespace(request.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const { preferredUserId, allowedUserIds } = buildAllowedAudiobookUserIds(userId, unclaimedUserId); const existingBookRows = await db .select({ userId: audiobooks.userId }) .from(audiobooks) - .where(and(eq(audiobooks.id, bookId), inArray(audiobooks.userId, allowedUserIds))); - const existingBookUserId = pickAudiobookOwner( - existingBookRows.map((book: { userId: string }) => book.userId), - preferredUserId, - unclaimedUserId, - ); + .where(and(eq(audiobooks.id, bookId), eq(audiobooks.userId, storageUserId))); - if (!existingBookUserId) { + if (existingBookRows.length === 0) { return NextResponse.json({ chapters: [], exists: false, @@ -98,20 +91,20 @@ export async function GET(request: NextRequest) { }); } - const objects = await listAudiobookObjects(bookId, existingBookUserId, testNamespace); + const objects = await listAudiobookObjects(bookId, storageUserId, testNamespace); const objectNames = objects.map((object) => object.fileName); const chapterObjects = listChapterObjects(objectNames); await pruneAudiobookChaptersNotOnDisk( bookId, - existingBookUserId, + storageUserId, chapterObjects.map((chapter) => chapter.index), ); const chapterRows = await db .select({ chapterIndex: audiobookChapters.chapterIndex, duration: audiobookChapters.duration }) .from(audiobookChapters) - .where(and(eq(audiobookChapters.bookId, bookId), eq(audiobookChapters.userId, existingBookUserId))); + .where(and(eq(audiobookChapters.bookId, bookId), eq(audiobookChapters.userId, storageUserId))); const durationByIndex = new Map(); for (const row of chapterRows) { durationByIndex.set(row.chapterIndex, Number(row.duration ?? 0)); @@ -128,7 +121,7 @@ export async function GET(request: NextRequest) { let settings: AudiobookGenerationSettings | null = null; try { - settings = JSON.parse((await getAudiobookObjectBuffer(bookId, existingBookUserId, 'audiobook.meta.json', testNamespace)).toString('utf8')) as AudiobookGenerationSettings; + settings = JSON.parse((await getAudiobookObjectBuffer(bookId, storageUserId, 'audiobook.meta.json', testNamespace)).toString('utf8')) as AudiobookGenerationSettings; } catch (error) { if (!isMissingBlobError(error)) throw error; settings = null; @@ -139,7 +132,7 @@ export async function GET(request: NextRequest) { if (!exists) { // Deleting the audiobook row cascades to audiobookChapters via bookFk - await db.delete(audiobooks).where(and(eq(audiobooks.id, bookId), eq(audiobooks.userId, existingBookUserId))); + await db.delete(audiobooks).where(and(eq(audiobooks.id, bookId), eq(audiobooks.userId, storageUserId))); return NextResponse.json({ chapters: [], exists: false, diff --git a/src/app/api/documents/[id]/parsed/events/route.ts b/src/app/api/documents/[id]/parsed/events/route.ts index e4590ca..927c6ff 100644 --- a/src/app/api/documents/[id]/parsed/events/route.ts +++ b/src/app/api/documents/[id]/parsed/events/route.ts @@ -10,7 +10,7 @@ import { fetchWorkerOperationState } from '@/lib/server/compute/worker-op-state' import { isValidDocumentId } from '@/lib/server/documents/blobstore'; import { normalizeParseStatus, parseDocumentParseState } from '@/lib/server/documents/parse-state'; import { healStaleDocumentParseState } from '@/lib/server/documents/parse-state-healing'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; +import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace'; import { isS3Configured } from '@/lib/server/storage/s3'; import { createRequestLogger, hashForLog } from '@/lib/server/logger'; import { errorResponse } from '@/lib/server/errors/next-response'; @@ -147,6 +147,7 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string const authCtxOrRes = await requireAuthContext(req); if (authCtxOrRes instanceof Response) return authCtxOrRes; + if (!authCtxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const params = await ctx.params; const id = (params.id || '').trim().toLowerCase(); @@ -159,10 +160,9 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string : null; const testNamespace = getOpenReaderTestNamespace(req.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const storageUserId = authCtxOrRes.userId ?? unclaimedUserId; + const storageUserId = authCtxOrRes.userId; const storageUserIdHash = hashForLog(storageUserId); - const allowedUserIds = [storageUserId, unclaimedUserId]; + const allowedUserIds = [storageUserId]; const row = await loadPreferredRow({ documentId: id, diff --git a/src/app/api/documents/[id]/parsed/route.ts b/src/app/api/documents/[id]/parsed/route.ts index 0301e88..6ed1d96 100644 --- a/src/app/api/documents/[id]/parsed/route.ts +++ b/src/app/api/documents/[id]/parsed/route.ts @@ -21,7 +21,7 @@ import { stringifyDocumentParseState, } from '@/lib/server/documents/parse-state'; import { healStaleDocumentParseState } from '@/lib/server/documents/parse-state-healing'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; +import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace'; import { checkJobRate, recordJobEvent, getPdfLayoutRateConfig } from '@/lib/server/rate-limit/job-rate-limiter'; import { buildComputeRateLimitedResponse } from '@/lib/server/rate-limit/problem-response'; import { getResolvedRuntimeConfig } from '@/lib/server/runtime-config'; @@ -196,6 +196,7 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string const authCtxOrRes = await requireAuthContext(req); if (authCtxOrRes instanceof Response) return authCtxOrRes; + if (!authCtxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const params = await ctx.params; const id = (params.id || '').trim().toLowerCase(); @@ -206,9 +207,8 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string } const testNamespace = getOpenReaderTestNamespace(req.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const storageUserId = authCtxOrRes.userId ?? unclaimedUserId; - const allowedUserIds = [storageUserId, unclaimedUserId]; + const storageUserId = authCtxOrRes.userId; + const allowedUserIds = [storageUserId]; const rows = await loadRows({ documentId: id, allowedUserIds }); const row = pickPreferredRow(rows, storageUserId); @@ -347,6 +347,7 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string const authCtxOrRes = await requireAuthContext(req); if (authCtxOrRes instanceof Response) return authCtxOrRes; + if (!authCtxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const params = await ctx.params; const id = (params.id || '').trim().toLowerCase(); @@ -363,9 +364,8 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string } const testNamespace = getOpenReaderTestNamespace(req.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const storageUserId = authCtxOrRes.userId ?? unclaimedUserId; - const allowedUserIds = [storageUserId, unclaimedUserId]; + const storageUserId = authCtxOrRes.userId; + const allowedUserIds = [storageUserId]; const rows = await loadRows({ documentId: id, allowedUserIds }); const row = pickPreferredRow(rows, storageUserId); diff --git a/src/app/api/documents/[id]/settings/route.ts b/src/app/api/documents/[id]/settings/route.ts index 478ddf5..0b2b282 100644 --- a/src/app/api/documents/[id]/settings/route.ts +++ b/src/app/api/documents/[id]/settings/route.ts @@ -1,9 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; -import { and, eq, inArray } from 'drizzle-orm'; +import { and, eq } from 'drizzle-orm'; import { db } from '@/db'; import { documentSettings, documents } from '@/db/schema'; import { requireAuthContext } from '@/lib/server/auth/auth'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; import { mergeDocumentSettings } from '@/lib/shared/document-settings'; import { DEFAULT_DOCUMENT_SETTINGS, type DocumentSettings } from '@/types/document-settings'; import { coerceTimestampMs, nowTimestampMs } from '@/lib/shared/timestamps'; @@ -35,21 +34,19 @@ function parseStored(value: unknown): DocumentSettings { } async function resolveDocumentAccess(req: NextRequest, documentId: string): Promise< - | { ownerUserId: string; allowedUserIds: string[] } + | { ownerUserId: string } | Response > { const authCtxOrRes = await requireAuthContext(req); if (authCtxOrRes instanceof Response) return authCtxOrRes; + if (!authCtxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const testNamespace = getOpenReaderTestNamespace(req.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const storageUserId = authCtxOrRes.userId ?? unclaimedUserId; - const allowedUserIds = [storageUserId, unclaimedUserId]; + const storageUserId = authCtxOrRes.userId; const rows = await db .select({ userId: documents.userId }) .from(documents) - .where(and(eq(documents.id, documentId), inArray(documents.userId, allowedUserIds))) + .where(and(eq(documents.id, documentId), eq(documents.userId, storageUserId))) .limit(1); if (!rows[0]) { @@ -58,7 +55,6 @@ async function resolveDocumentAccess(req: NextRequest, documentId: string): Prom return { ownerUserId: rows[0].userId, - allowedUserIds, }; } diff --git a/src/app/api/documents/blob/get/fallback/route.ts b/src/app/api/documents/blob/get/fallback/route.ts index 841e070..1929aeb 100644 --- a/src/app/api/documents/blob/get/fallback/route.ts +++ b/src/app/api/documents/blob/get/fallback/route.ts @@ -5,7 +5,7 @@ import { documents } from '@/db/schema'; import { requireAuthContext } from '@/lib/server/auth/auth'; import { contentTypeForName } from '@/lib/server/storage/library-mount'; import { getDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents/blobstore'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; +import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace'; import { isS3Configured } from '@/lib/server/storage/s3'; import { errorToLog, serverLogger } from '@/lib/server/logger'; import { errorResponse } from '@/lib/server/errors/next-response'; @@ -34,11 +34,11 @@ export async function GET(req: NextRequest) { const ctxOrRes = await requireAuthContext(req); if (ctxOrRes instanceof Response) return ctxOrRes; + if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const testNamespace = getOpenReaderTestNamespace(req.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const storageUserId = ctxOrRes.userId ?? unclaimedUserId; - const allowedUserIds = [storageUserId, unclaimedUserId]; + const storageUserId = ctxOrRes.userId; + const allowedUserIds = [storageUserId]; const url = new URL(req.url); const id = (url.searchParams.get('id') || '').trim().toLowerCase(); diff --git a/src/app/api/documents/blob/get/presign/route.ts b/src/app/api/documents/blob/get/presign/route.ts index cdb0265..4992354 100644 --- a/src/app/api/documents/blob/get/presign/route.ts +++ b/src/app/api/documents/blob/get/presign/route.ts @@ -4,7 +4,7 @@ import { db } from '@/db'; import { documents } from '@/db/schema'; import { requireAuthContext } from '@/lib/server/auth/auth'; import { isValidDocumentId, presignGet } from '@/lib/server/documents/blobstore'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; +import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace'; import { isS3Configured } from '@/lib/server/storage/s3'; import { errorToLog, serverLogger } from '@/lib/server/logger'; import { errorResponse } from '@/lib/server/errors/next-response'; @@ -24,11 +24,11 @@ export async function GET(req: NextRequest) { const ctxOrRes = await requireAuthContext(req); if (ctxOrRes instanceof Response) return ctxOrRes; + if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const testNamespace = getOpenReaderTestNamespace(req.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const storageUserId = ctxOrRes.userId ?? unclaimedUserId; - const allowedUserIds = [storageUserId, unclaimedUserId]; + const storageUserId = ctxOrRes.userId; + const allowedUserIds = [storageUserId]; const url = new URL(req.url); const id = (url.searchParams.get('id') || '').trim().toLowerCase(); diff --git a/src/app/api/documents/blob/preview/fallback/route.ts b/src/app/api/documents/blob/preview/fallback/route.ts index 949e785..c67da8d 100644 --- a/src/app/api/documents/blob/preview/fallback/route.ts +++ b/src/app/api/documents/blob/preview/fallback/route.ts @@ -20,7 +20,7 @@ import { isPreviewableDocumentType, } from '@/lib/server/documents/previews'; import { extractRawTextSnippet } from '@/lib/server/documents/text-snippets'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; +import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace'; import { isS3Configured } from '@/lib/server/storage/s3'; export const dynamic = 'force-dynamic'; @@ -52,11 +52,11 @@ export async function GET(req: NextRequest) { const ctxOrRes = await requireAuthContext(req); if (ctxOrRes instanceof Response) return ctxOrRes; + if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const testNamespace = getOpenReaderTestNamespace(req.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const storageUserId = ctxOrRes.userId ?? unclaimedUserId; - const allowedUserIds = [storageUserId, unclaimedUserId]; + const storageUserId = ctxOrRes.userId; + const allowedUserIds = [storageUserId]; const url = new URL(req.url); const id = (url.searchParams.get('id') || '').trim().toLowerCase(); diff --git a/src/app/api/documents/blob/preview/utils.ts b/src/app/api/documents/blob/preview/utils.ts index ef11138..937942f 100644 --- a/src/app/api/documents/blob/preview/utils.ts +++ b/src/app/api/documents/blob/preview/utils.ts @@ -5,7 +5,7 @@ import { documents } from '@/db/schema'; import { requireAuthContext } from '@/lib/server/auth/auth'; import { isValidDocumentId } from '@/lib/server/documents/blobstore'; import { isPreviewableDocumentType } from '@/lib/server/documents/previews'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; +import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace'; import { isS3Configured } from '@/lib/server/storage/s3'; export function s3NotConfiguredResponse(): NextResponse { @@ -37,15 +37,11 @@ export async function validatePreviewRequest(req: NextRequest): Promise null); const documentsData = parseDocumentPayload(body); @@ -171,7 +171,7 @@ export async function POST(req: NextRequest) { type: doc.type, size: headSize, lastModified: doc.lastModified, - scope: storageUserId === unclaimedUserId ? 'unclaimed' : 'user', + scope: 'user', }); await enqueueDocumentPreview( @@ -224,11 +224,10 @@ export async function GET(req: NextRequest) { const ctxOrRes = await requireAuthContext(req); if (ctxOrRes instanceof Response) return ctxOrRes; + if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const testNamespace = getOpenReaderTestNamespace(req.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const storageUserId = ctxOrRes.userId ?? unclaimedUserId; - const allowedUserIds = [storageUserId, unclaimedUserId]; + const storageUserId = ctxOrRes.userId; + const allowedUserIds = [storageUserId]; const url = new URL(req.url); const idsParam = url.searchParams.get('ids'); @@ -259,21 +258,7 @@ export async function GET(req: NextRequest) { parsedJsonKey: string | null; }>; - const preferredById = new Map(); - for (const row of rows) { - const existing = preferredById.get(row.id); - if (!existing) { - preferredById.set(row.id, row); - continue; - } - const isRowPrimary = row.userId === storageUserId; - const isExistingPrimary = existing.userId === storageUserId; - if (isRowPrimary && !isExistingPrimary) { - preferredById.set(row.id, row); - } - } - - const results: BaseDocument[] = Array.from(preferredById.values()).map((doc) => { + const results: BaseDocument[] = rows.map((doc) => { const type = normalizeDocumentType(doc.type, doc.name); return { id: doc.id, @@ -283,7 +268,7 @@ export async function GET(req: NextRequest) { type, parseStatus: type === 'pdf' ? normalizeParseStatus(parseDocumentParseState(doc.parseState).status) : null, parsedJsonKey: doc.parsedJsonKey, - scope: doc.userId === unclaimedUserId ? 'unclaimed' : 'user', + scope: 'user', }; }); @@ -306,37 +291,19 @@ export async function DELETE(req: NextRequest) { const ctxOrRes = await requireAuthContext(req); if (ctxOrRes instanceof Response) return ctxOrRes; + if (!ctxOrRes.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const testNamespace = getOpenReaderTestNamespace(req.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const storageUserId = ctxOrRes.userId ?? unclaimedUserId; + const storageUserId = ctxOrRes.userId; const url = new URL(req.url); const idsParam = url.searchParams.get('ids'); const scopeParam = (url.searchParams.get('scope') || '').toLowerCase().trim(); - - const wantsUnclaimed = scopeParam === 'unclaimed'; - const wantsUser = scopeParam === '' || scopeParam === 'user'; - - if (!wantsUser && !wantsUnclaimed) { - return NextResponse.json( - { error: "Invalid scope. Expected 'user' (default) or 'unclaimed'." }, - { status: 400 }, - ); + if (scopeParam && scopeParam !== 'user') { + return NextResponse.json({ error: "Invalid scope. Expected 'user' (default)." }, { status: 400 }); } - if (wantsUnclaimed && ctxOrRes.user?.isAnonymous) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const targetUserIds = Array.from( - new Set( - [ - ...(wantsUser ? [storageUserId] : []), - ...(wantsUnclaimed ? [unclaimedUserId] : []), - ].filter(Boolean), - ), - ); + const targetUserIds = [storageUserId]; if (targetUserIds.length === 0) { return NextResponse.json({ success: true, deleted: 0 }); diff --git a/src/app/api/tts/segments/ensure/route.ts b/src/app/api/tts/segments/ensure/route.ts index 191a5b0..fdd6b2d 100644 --- a/src/app/api/tts/segments/ensure/route.ts +++ b/src/app/api/tts/segments/ensure/route.ts @@ -468,7 +468,7 @@ export async function POST(request: NextRequest) { segmentId: segment.segmentId, }); - if (scope.authEnabled && scope.userId && ttsRateLimitEnabled) { + if (ttsRateLimitEnabled) { const charCount = segment.text.length; const ip = getClientIp(request); const device = scope.isAnonymousUser ? getOrCreateDeviceId(request) : null; diff --git a/src/components/doclist/views/DocumentTile.tsx b/src/components/doclist/views/DocumentTile.tsx index e43ddef..467847b 100644 --- a/src/components/doclist/views/DocumentTile.tsx +++ b/src/components/doclist/views/DocumentTile.tsx @@ -6,8 +6,6 @@ import { Button } from '@headlessui/react'; import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons'; import type { DocumentListDocument, IconSize } from '@/types/documents'; import { DocumentPreview } from '@/components/doclist/DocumentPreview'; -import { useAuthSession } from '@/hooks/useAuthSession'; -import { useAuthConfig } from '@/contexts/AuthRateLimitContext'; import { useDocumentSelection } from '../dnd/DocumentSelectionContext'; import { DND_DOCUMENT, documentIdentityKey, type DocumentDragItem } from '../dnd/dndTypes'; @@ -69,13 +67,10 @@ export function DocumentTile({ onDelete, onMergeIntoFolder, }: DocumentTileProps) { - const { authEnabled } = useAuthConfig(); - const { data: session } = useAuthSession(); const href = `/${doc.type}/${encodeURIComponent(doc.id)}`; const selection = useDocumentSelection(); - const isAnonymousAuthed = Boolean(authEnabled && session?.user?.isAnonymous); - const showDeleteButton = !(isAnonymousAuthed && doc.scope === 'unclaimed'); + const showDeleteButton = true; const isSelected = selection.isSelected(doc); const isInFolder = Boolean(doc.folderId); diff --git a/src/contexts/OnboardingFlowContext.tsx b/src/contexts/OnboardingFlowContext.tsx index b45981c..e1cadaf 100644 --- a/src/contexts/OnboardingFlowContext.tsx +++ b/src/contexts/OnboardingFlowContext.tsx @@ -234,7 +234,6 @@ export function OnboardingFlowProvider({ children }: { children: ReactNode }) { useEffect(() => { return scheduleChangelogCheck({ - authEnabled: true, isSessionPending, sessionUserId: userId, appVersion: runtimeConfig.appVersion, diff --git a/src/db/index.ts b/src/db/index.ts index ae265e1..a6e43ec 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -23,9 +23,10 @@ const seededUserIds = new Set(); /** * Ensure a system/placeholder user row exists in the user table for `userId`. * All user-facing tables now have userId foreign keys with ON DELETE CASCADE - * referencing the user table. When auth is disabled the app stores data under - * the 'unclaimed' userId (and namespace variants like 'unclaimed::ns'), so - * those rows must exist before any data can be inserted. + * referencing the user table. The legacy first-enable-auth claim migration can + * still move rows from the historical 'unclaimed' userId (and namespaced + * variants like 'unclaimed::ns'), so those placeholder rows must exist before + * any migration transfer can run. * * This is safe to call repeatedly — it short-circuits via an in-memory Set * and uses INSERT … ON CONFLICT/OR IGNORE at the SQL level. @@ -109,4 +110,3 @@ export const db: any = new Proxy({} as any, { return typeof value === 'function' ? value.bind(instance) : value; }, }); - diff --git a/src/lib/client/api/documents.ts b/src/lib/client/api/documents.ts index 7eb4570..b943354 100644 --- a/src/lib/client/api/documents.ts +++ b/src/lib/client/api/documents.ts @@ -341,14 +341,11 @@ export async function uploadDocuments(files: File[], options?: UploadOptions): P return uploadDocumentSources(sources, options); } -export async function deleteDocuments(options?: { ids?: string[]; scope?: 'user' | 'unclaimed'; signal?: AbortSignal }): Promise { +export async function deleteDocuments(options?: { ids?: string[]; signal?: AbortSignal }): Promise { const params = new URLSearchParams(); if (options?.ids?.length) { params.set('ids', options.ids.join(',')); } - if (options?.scope) { - params.set('scope', options.scope); - } const url = params.toString() ? `/api/documents?${params.toString()}` : '/api/documents'; const res = await fetch(url, { method: 'DELETE', signal: options?.signal }); diff --git a/src/lib/client/changelog-check.ts b/src/lib/client/changelog-check.ts index 70b1571..9514e94 100644 --- a/src/lib/client/changelog-check.ts +++ b/src/lib/client/changelog-check.ts @@ -9,7 +9,6 @@ export type ChangelogVersionCheckResponse = { type RefLike = { current: string | null }; export type RunChangelogCheckArgs = { - authEnabled: boolean; isSessionPending: boolean; sessionUserId: string | null | undefined; appVersion: string | null | undefined; @@ -23,13 +22,12 @@ export type RunChangelogCheckArgs = { export async function runChangelogCheck(args: RunChangelogCheckArgs): Promise { const sessionUserId = args.sessionUserId ?? null; - if (args.authEnabled && (args.isSessionPending || !sessionUserId)) return; + if (args.isSessionPending || !sessionUserId) return; const currentVersion = normalizeVersion(args.appVersion || ''); if (!currentVersion) return; - const userKey = sessionUserId ?? 'server-unclaimed'; - const checkKey = `${userKey}:${currentVersion}`; + const checkKey = `${sessionUserId}:${currentVersion}`; if (args.completedRef.current === checkKey) return; if (args.inFlightRef.current === checkKey) return; args.inFlightRef.current = checkKey; diff --git a/src/lib/server/audiobooks/user-scope.ts b/src/lib/server/audiobooks/user-scope.ts deleted file mode 100644 index 03753d3..0000000 --- a/src/lib/server/audiobooks/user-scope.ts +++ /dev/null @@ -1,20 +0,0 @@ -export function buildAllowedAudiobookUserIds( - userId: string | null, - unclaimedUserId: string, -): { preferredUserId: string; allowedUserIds: string[] } { - const preferredUserId = userId ?? unclaimedUserId; - const allowedUserIds = Array.from(new Set([preferredUserId, unclaimedUserId])); - return { preferredUserId, allowedUserIds }; -} - -export function pickAudiobookOwner( - existingUserIds: string[], - preferredUserId: string, - unclaimedUserId: string, -): string | null { - const existing = new Set(existingUserIds); - // Keep resumed writes on unclaimed scope when the book already exists there. - if (existing.has(unclaimedUserId)) return unclaimedUserId; - if (existing.has(preferredUserId)) return preferredUserId; - return existingUserIds[0] ?? null; -} diff --git a/src/lib/server/tts/segments-auth.ts b/src/lib/server/tts/segments-auth.ts index ab10d35..ff19de3 100644 --- a/src/lib/server/tts/segments-auth.ts +++ b/src/lib/server/tts/segments-auth.ts @@ -3,14 +3,13 @@ import type { NextRequest } from 'next/server'; import { db } from '@/db'; import { documents } from '@/db/schema'; import { requireAuthContext } from '@/lib/server/auth/auth'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; +import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace'; import type { ReaderType } from '@/types/user-state'; export type ResolvedSegmentDocumentScope = { testNamespace: string | null; storageUserId: string; - authEnabled: true; - userId: string | null; + userId: string; isAnonymousUser: boolean; documentVersion: number; readerType: ReaderType; @@ -28,11 +27,11 @@ export async function resolveSegmentDocumentScope( ): Promise { const ctxOrRes = await requireAuthContext(request); if (ctxOrRes instanceof Response) return ctxOrRes; + if (!ctxOrRes.userId) return Response.json({ error: 'Unauthorized' }, { status: 401 }); const testNamespace = getOpenReaderTestNamespace(request.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace); - const storageUserId = ctxOrRes.userId ?? unclaimedUserId; - const allowedUserIds = [storageUserId, unclaimedUserId]; + const storageUserId = ctxOrRes.userId; + const allowedUserIds = [storageUserId]; const rows = (await db .select({ @@ -55,7 +54,6 @@ export async function resolveSegmentDocumentScope( return { testNamespace, storageUserId: doc.userId, - authEnabled: true, userId: ctxOrRes.userId, isAnonymousUser: Boolean(ctxOrRes.user?.isAnonymous), documentVersion: Number(doc.lastModified), diff --git a/src/lib/server/user/resolve-state-scope.ts b/src/lib/server/user/resolve-state-scope.ts index 01b6488..584b0e3 100644 --- a/src/lib/server/user/resolve-state-scope.ts +++ b/src/lib/server/user/resolve-state-scope.ts @@ -1,13 +1,12 @@ import type { NextRequest } from 'next/server'; import type { AuthContext } from '@/lib/server/auth/auth'; import { requireAuthContext } from '@/lib/server/auth/auth'; -import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace'; +import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace'; export type ResolvedUserStateScope = { auth: AuthContext; namespace: string | null; ownerUserId: string; - unclaimedUserId: string; }; export async function resolveUserStateScope( @@ -15,16 +14,14 @@ export async function resolveUserStateScope( ): Promise { const auth = await requireAuthContext(req); if (auth instanceof Response) return auth; + if (!auth.userId) return Response.json({ error: 'Unauthorized' }, { status: 401 }); const namespace = getOpenReaderTestNamespace(req.headers); - const unclaimedUserId = getUnclaimedUserIdForNamespace(namespace); - const ownerUserId = auth.userId ?? unclaimedUserId; + const ownerUserId = auth.userId; return { auth, namespace, ownerUserId, - unclaimedUserId, }; } - diff --git a/src/types/documents.ts b/src/types/documents.ts index e9fcc42..b50bd40 100644 --- a/src/types/documents.ts +++ b/src/types/documents.ts @@ -9,7 +9,7 @@ export interface BaseDocument { type: DocumentType; parseStatus?: 'pending' | 'running' | 'ready' | 'failed' | null; parsedJsonKey?: string | null; - scope?: 'user' | 'unclaimed'; + scope?: 'user'; folderId?: string; isConverting?: boolean; } diff --git a/tests/unit/audiobook-scope.vitest.spec.ts b/tests/unit/audiobook-scope.vitest.spec.ts deleted file mode 100644 index d53c9c2..0000000 --- a/tests/unit/audiobook-scope.vitest.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '../../src/lib/server/audiobooks/user-scope'; - -describe('audiobook scope selection', () => { - test('includes both preferred and unclaimed scopes', () => { - const result = buildAllowedAudiobookUserIds('user-123', 'unclaimed::ns'); - expect(result.preferredUserId).toBe('user-123'); - expect(result.allowedUserIds).toEqual(['user-123', 'unclaimed::ns']); - }); - - test('deduplicates preferred/unclaimed ids when they are the same', () => { - const result = buildAllowedAudiobookUserIds('unclaimed::ns', 'unclaimed::ns'); - expect(result.allowedUserIds).toEqual(['unclaimed::ns']); - }); - - test('prefers unclaimed owner when both scopes exist', () => { - const owner = pickAudiobookOwner(['user-123', 'unclaimed::ns'], 'user-123', 'unclaimed::ns'); - expect(owner).toBe('unclaimed::ns'); - }); - - test('falls back to preferred owner when unclaimed is missing', () => { - const owner = pickAudiobookOwner(['user-123'], 'user-123', 'unclaimed::ns'); - expect(owner).toBe('user-123'); - }); - - test('returns null when no matching owners exist', () => { - const owner = pickAudiobookOwner([], 'user-123', 'unclaimed::ns'); - expect(owner).toBeNull(); - }); -}); diff --git a/tests/unit/changelog-check.vitest.spec.ts b/tests/unit/changelog-check.vitest.spec.ts index 19d8642..383e8dd 100644 --- a/tests/unit/changelog-check.vitest.spec.ts +++ b/tests/unit/changelog-check.vitest.spec.ts @@ -14,7 +14,6 @@ describe('changelog check scheduling', () => { let openCalls = 0; const args = { - authEnabled: true, isSessionPending: false, sessionUserId: 'u1', appVersion: '3.3.0', @@ -48,13 +47,12 @@ describe('changelog check scheduling', () => { expect(inFlightRef.current).toBeNull(); }); - test('does not run when auth is enabled and session is pending', async () => { + test('does not run while session is pending', async () => { const completedRef = { current: null as string | null }; const inFlightRef = { current: null as string | null }; let apiCalls = 0; const cleanup = scheduleChangelogCheck({ - authEnabled: true, isSessionPending: true, sessionUserId: null, appVersion: '3.3.0', diff --git a/tests/unit/onboarding-flow.vitest.spec.ts b/tests/unit/onboarding-flow.vitest.spec.ts index 6a23fd8..8569171 100644 --- a/tests/unit/onboarding-flow.vitest.spec.ts +++ b/tests/unit/onboarding-flow.vitest.spec.ts @@ -64,7 +64,7 @@ describe('onboarding flow resolver', () => { migrationRequired: false, changelogPending: false, }); - const noAuthStep = resolveNextOnboardingStep({ + const minimalStep = resolveNextOnboardingStep({ privacyRequired: false, privacyAccepted: false, claimEligible: false, @@ -74,7 +74,7 @@ describe('onboarding flow resolver', () => { }); expect(authStep).toBe('done'); - expect(noAuthStep).toBe('done'); + expect(minimalStep).toBe('done'); }); });