From 936aa50f9ae6f04d485fea316dea7e1a6a4ae55e Mon Sep 17 00:00:00 2001 From: Richard R Date: Sun, 31 May 2026 13:01:55 -0600 Subject: [PATCH] refactor(api): remove all legacy unclaimed user scope logic and enforce strict userId scoping Eliminate all code, tests, and utilities related to the legacy 'unclaimed' user scope. All API endpoints, document and audiobook storage, and access logic now require a valid authenticated userId and only operate on resources owned by that user. Remove related helper functions, test cases, and conditional flows for anonymous/unclaimed data. Update types, client APIs, and UI logic to reflect that only 'user' scope is supported. This simplifies ownership checks and removes ambiguity around document and audiobook access. --- docs-site/docs/configure/auth.md | 1 + src/app/api/audiobook/chapter/route.ts | 62 ++++++------------- src/app/api/audiobook/route.ts | 51 ++++++--------- src/app/api/audiobook/status/route.ts | 29 ++++----- .../api/documents/[id]/parsed/events/route.ts | 8 +-- src/app/api/documents/[id]/parsed/route.ts | 14 ++--- src/app/api/documents/[id]/settings/route.ts | 14 ++--- .../api/documents/blob/get/fallback/route.ts | 8 +-- .../api/documents/blob/get/presign/route.ts | 8 +-- .../documents/blob/preview/fallback/route.ts | 8 +-- src/app/api/documents/blob/preview/utils.ts | 12 ++-- .../api/documents/docx-to-pdf/upload/route.ts | 6 +- src/app/api/documents/route.ts | 61 +++++------------- src/app/api/tts/segments/ensure/route.ts | 2 +- src/components/doclist/views/DocumentTile.tsx | 7 +-- src/contexts/OnboardingFlowContext.tsx | 1 - src/db/index.ts | 8 +-- src/lib/client/api/documents.ts | 5 +- src/lib/client/changelog-check.ts | 6 +- src/lib/server/audiobooks/user-scope.ts | 20 ------ src/lib/server/tts/segments-auth.ts | 12 ++-- src/lib/server/user/resolve-state-scope.ts | 9 +-- src/types/documents.ts | 2 +- tests/unit/audiobook-scope.vitest.spec.ts | 30 --------- tests/unit/changelog-check.vitest.spec.ts | 4 +- tests/unit/onboarding-flow.vitest.spec.ts | 4 +- 26 files changed, 120 insertions(+), 272 deletions(-) delete mode 100644 src/lib/server/audiobooks/user-scope.ts delete mode 100644 tests/unit/audiobook-scope.vitest.spec.ts 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'); }); });