{"record":{"id":"adcc8077bd33315b","repo":"koala73/worldmonitor","slug":"user-id-required","errorCode":"USER_ID_REQUIRED","errorMessage":"USER_ID_REQUIRED","messagePattern":"USER_ID_REQUIRED","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/billing.ts","lineNumber":3678,"sourceCode":" * Public action callable from the browser. Auth-gated via requireUserId(ctx).\n */\nexport const getCustomerPortalUrl = action({\n  args: {},\n  handler: async (ctx, _args) => {\n    const userId = await requireUserId(ctx);\n    return createCustomerPortalUrlForUser(ctx, userId);\n  },\n});\n\n/**\n * Internal action callable from the edge gateway to create a user-scoped\n * Dodo Customer Portal session after the Clerk JWT has been verified there.\n */\nexport const internalGetCustomerPortalUrl = internalAction({\n  args: { userId: v.string() },\n  handler: async (ctx, args) => {\n    if (!args.userId) {\n      throw new ConvexError({ kind: \"USER_ID_REQUIRED\" });\n    }\n    return createCustomerPortalUrlForUser(ctx, args.userId);\n  },\n});\n\n// ---------------------------------------------------------------------------\n// Subscription claim (anon ID → authenticated user migration)\n// ---------------------------------------------------------------------------\n\n/**\n * Claims subscription, entitlement, and customer records from an anonymous\n * browser ID to the currently authenticated user.\n *\n * LIMITATION: Until Clerk auth is wired into the ConvexClient, anonymous\n * purchases are keyed to a `crypto.randomUUID()` stored in localStorage\n * (`wm-anon-id`). If the user clears storage, switches browsers, or later\n * creates a real account, there is no automatic way to link the purchase.\n *","sourceCodeStart":3660,"sourceCodeEnd":3696,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/billing.ts#L3660-L3696","documentation":"Thrown by the `internalGetCustomerPortalUrl` internal action (called from the edge gateway after Clerk JWT verification) when `args.userId` is empty/falsy. It is an object-typed ConvexError (`{ kind: \"USER_ID_REQUIRED\" }`). This guards the portal-creation path that runs without Convex's own auth context, so it must be passed an explicit userId.","triggerScenarios":"The edge gateway extracts the subject from a verified Clerk JWT but passes an empty string (JWT had no `sub` claim, or extraction returned undefined/empty) into `internalGetCustomerPortalUrl({ userId })`.","commonSituations":"Edge handler received a JWT missing the `sub` claim; a code path that reads `userId` from the wrong field or falls back to empty; the gateway was called from a test/stub without a real subject.","solutions":["In the edge gateway, validate the JWT `sub` is a non-empty string before calling the internal action.","If `sub` is missing, reject the request with 401 at the edge instead of forwarding an empty userId.","Confirm the Clerk JWT template includes the `sub` claim."],"exampleFix":"// before (edge gateway)\nawait convex.query(internal.payments.billing.internalGetCustomerPortalUrl, { userId: jwt.sub ?? \"\" });\n\n// after\nconst userId = jwt.sub;\nif (!userId) return new Response(\"Unauthorized\", { status: 401 });\nawait convex.query(internal.payments.billing.internalGetCustomerPortalUrl, { userId });","handlingStrategy":"validation","validationCode":"// In the edge gateway\nconst userId = verifiedJwt.sub;\nif (!userId || typeof userId !== \"string\") {\n  return new Response(\"Unauthorized\", { status: 401 });\n}\nawait convex.query(internal.payments.billing.internalGetCustomerPortalUrl, { userId });","typeGuard":"function isNonEmptyUserId(s: unknown): s is string {\n  return typeof s === \"string\" && s.length > 0;\n}","tryCatchPattern":null,"preventionTips":["Validate JWT `sub` is a non-empty string at the edge before calling the internal action.","Ensure the Clerk JWT template exposes the `sub` claim."],"tags":["validation","convex","billing","auth","edge"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}