{"record":{"id":"652376c5a655e81e","repo":"koala73/worldmonitor","slug":"anon-claim-proof-required","errorCode":"ANON_CLAIM_PROOF_REQUIRED","errorMessage":"ANON_CLAIM_PROOF_REQUIRED","messagePattern":"ANON_CLAIM_PROOF_REQUIRED","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/billing.ts","lineNumber":3777,"sourceCode":" * server-side during checkout creation; a leaked bare UUID is not sufficient\n * ownership proof.\n *\n * @see https://github.com/koala73/worldmonitor/issues/2078\n */\nexport const claimSubscription = mutation({\n  args: { anonId: v.string(), claimToken: v.optional(v.string()) },\n  handler: async (ctx, args) => {\n    const realUserId = await requireUserId(ctx);\n\n    // Validate anonId is a UUID v4 (format produced by crypto.randomUUID() in user-identity.ts).\n    // Rejects injected Clerk IDs (\"user_xxx\") which are structurally distinct from UUID v4,\n    // preventing cross-user subscription theft via localStorage injection.\n    if (!ANON_ID_V4_REGEX.test(args.anonId) || args.anonId === realUserId) {\n      return { claimed: { subscriptions: 0, entitlements: 0, customers: 0, payments: 0 } };\n    }\n\n    if (args.claimToken !== undefined && !(await verifyAnonClaimToken(args.anonId, args.claimToken))) {\n      throw new ConvexError({ kind: \"ANON_CLAIM_PROOF_REQUIRED\" });\n    }\n\n    // Parallel reads for all anonId data — bounded to prevent runaway memory\n    const [subs, anonEntitlement, customers, payments, deletedCustomers] = await Promise.all([\n      ctx.db.query(\"subscriptions\").withIndex(\"by_userId\", (q) => q.eq(\"userId\", args.anonId)).take(50),\n      ctx.db.query(\"entitlements\").withIndex(\"by_userId\", (q) => q.eq(\"userId\", args.anonId)).first(),\n      ctx.db.query(\"customers\").withIndex(\"by_userId\", (q) => q.eq(\"userId\", args.anonId)).take(10),\n      ctx.db.query(\"paymentEvents\").withIndex(\"by_userId\", (q) => q.eq(\"userId\", args.anonId)).take(1000),\n      ctx.db.query(\"deletedSubscriptionCustomers\").withIndex(\"by_userId\", (q) => q.eq(\"userId\", args.anonId)).collect(),\n    ]);\n\n    const hasClaimableRows =\n      subs.length > 0 ||\n      anonEntitlement !== null ||\n      customers.length > 0 ||\n      payments.length > 0 ||\n      deletedCustomers.length > 0;\n    if (!hasClaimableRows) {","sourceCodeStart":3759,"sourceCodeEnd":3795,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/payments/billing.ts#L3759-L3795","documentation":"Structured ConvexError thrown by the claimSubscription mutation when an anonymous subscription is claimed without a valid claim token. Claiming requires server-issued ownership proof issued during checkout; a bare leaked anon UUID alone is not sufficient, preventing cross-user subscription theft via injected anon ids (see issue #2078).","triggerScenarios":"Thrown at convex/payments/billing.ts:3718 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Pass the claimToken that was issued server-side when the anonymous subscription was created","If the token was lost, use the recovery path instead of retrying with only the anonId","Never accept or forward anon ids from untrusted user input as claim proof"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}