{"record":{"id":"117440308d766955","repo":"koala73/worldmonitor","slug":"unauthenticated-117440","errorCode":"UNAUTHENTICATED","errorMessage":"UNAUTHENTICATED","messagePattern":"UNAUTHENTICATED","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/userPreferences.ts","lineNumber":164,"sourceCode":"\n  return { ok: true };\n}\n\nexport const setPreferences = mutation({\n  args: {\n    variant: v.string(),\n    data: v.any(),\n    expectedSyncVersion: v.number(),\n    schemaVersion: v.optional(v.number()),\n  },\n  handler: async (ctx, args): Promise<SetPreferencesResult> => {\n    const identity = await ctx.auth.getUserIdentity();\n    // UNAUTHENTICATED throws as a structured ConvexError because it is rare\n    // auth drift / bad input we want surfaced in Sentry. Convex's\n    // wire format propagates `errorData` for object payloads so the edge\n    // handler routes via `err.data.kind`. (PR #3466 fixed the original\n    // string-data wire-strip bug.)\n    if (!identity) throw new ConvexError({ kind: \"UNAUTHENTICATED\" });\n    const userId = identity.subject;\n\n    // Run before the CAS read so stale expectedSyncVersion requests cannot\n    // bypass the authoritative direct-Convex backstop by intentionally\n    // returning CONFLICT forever. CONFLICT retries count as write attempts;\n    // the limit is sized for that worst-case retry profile.\n    const rateLimit = await checkUserPrefsWriteRateLimit(ctx, userId);\n    if (!rateLimit.ok) return rateLimit;\n\n    const existing = await ctx.db\n      .query(\"userPreferences\")\n      .withIndex(\"by_user_variant\", (q) =>\n        q.eq(\"userId\", userId).eq(\"variant\", args.variant),\n      )\n      .unique();\n\n    const data = preserveOmittedRollingDeploymentFields(existing?.data, args.data);\n    const blobSize = JSON.stringify(data).length;","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/userPreferences.ts#L146-L182","documentation":"`setPreferences` requires a Clerk identity via `ctx.auth.getUserIdentity()`. A null identity means there is no authenticated session, and the mutation throws a structured ConvexError `{ kind: \"UNAUTHENTICATED\" }` so the edge handler routes on `err.data.kind` and Sentry captures the drift. This is an auth-failure fast-fail, not a normal control-flow path.","triggerScenarios":"Calling `setPreferences` without a valid Clerk session: expired session token, signed-out client still making calls, or a cookie/token issue.","commonSituations":"Session expired while the SPA was idle; client fired the mutation after sign-out; auth cookie blocked by browser settings; Clerk keys misconfigured.","solutions":["Ensure the user is signed in before invoking setPreferences","Re-authenticate (refresh the Clerk session) and retry","Verify Clerk environment/keys are correct and the session cookie is present"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Only call setPreferences when authenticated.\nif (!clerk.user) { /* prompt sign-in; do not call */ }\nelse { await convex.mutation(api.userPreferences.setPreferences, args); }","typeGuard":"function isAuthenticated(user: { id: string | null } | null): user is { id: string } {\n  return Boolean(user && user.id);\n}","tryCatchPattern":"try {\n  await convex.mutation(api.userPreferences.setPreferences, args);\n} catch (err) {\n  if (err.data?.kind === 'UNAUTHENTICATED') {\n    // re-authenticate and retry; this is rare auth drift worth surfacing\n  } else { throw err; }\n}","preventionTips":["Gate preference writes behind a signed-in check","Refresh the Clerk session on long-idle SPAs before writing","Verify Clerk env keys are correct so sessions persist"],"tags":["preferences","auth","clerk","convex"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}