{"record":{"id":"cdc76c37a50cd13e","repo":"koala73/worldmonitor","slug":"not-found-cdc76c","errorCode":"NOT_FOUND","errorMessage":"NOT_FOUND","messagePattern":"NOT_FOUND","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/mcpProTokens.ts","lineNumber":237,"sourceCode":"    }));\n  },\n});\n\n/**\n * Revoke a Pro MCP token row owned by the current user.\n *\n * Tenancy gate: the caller must own the row. Non-owner attempts surface\n * as `NOT_FOUND` (don't leak existence of other users' tokens). Mirrors\n * `apiKeys.revokeApiKey`.\n */\nexport const revokeProMcpToken = mutation({\n  args: { tokenId: v.id(\"mcpProTokens\") },\n  handler: async (ctx, args) => {\n    const userId = await requireUserId(ctx);\n    const row = await ctx.db.get(args.tokenId);\n\n    if (!row || row.userId !== userId) {\n      throw new ConvexError(\"NOT_FOUND\");\n    }\n    if (row.revokedAt) {\n      throw new ConvexError(\"ALREADY_REVOKED\");\n    }\n\n    await ctx.db.patch(args.tokenId, { revokedAt: Date.now() });\n    return { ok: true };\n  },\n});\n","sourceCodeStart":219,"sourceCodeEnd":247,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/mcpProTokens.ts#L219-L247","documentation":"Thrown by `revokeProMcpToken` when the token row is missing OR does not belong to the calling user. This is a tenancy gate: non-owner attempts surface as `NOT_FOUND` (not \"forbidden\") so the API doesn't leak the existence of other users' tokens — mirroring `apiKeys.revokeApiKey`. Plain-string ConvexError; `err.data === \"NOT_FOUND\"`. Occurs after `requireUserId` (so the caller IS authenticated) but the token id is wrong or foreign.","triggerScenarios":"Calling `revokeProMcpToken` with a tokenId that was already deleted, a typo'd/guessed id, or a token belonging to a different user. Also a stale client holding a tokenId from a deleted account, or a token id from a different Convex deployment.","commonSituations":"The client's token list is stale (a token was rotated/revoked by the cap logic in `issueProMcpToken` and the list wasn't refreshed); a user has multiple sessions and one revokes a token another is acting on; a cross-account bug passes the wrong userId's token id.","solutions":["Refresh the token list (`listProMcpTokens`) after any revoke or issue operation before showing actions on rows.","On `err.data === \"NOT_FOUND\"`, treat as already-gone (idempotent success) and remove the row from the client UI.","Verify the tokenId comes from the current user's own list, not a cached/global list.","Do not retry with the same id — NOT_FOUND is deterministic for a given id+user."],"exampleFix":"// before\nawait convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId: row.id });\n\n// after — treat NOT_FOUND as idempotent removal\ntry {\n  await convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId: row.id });\n} catch (err) {\n  if (err.data === \"NOT_FOUND\") {\n    // token already gone — remove from UI silently\n  } else throw err;\n}\nsetTokens((t) => t.filter((x) => x.id !== row.id));","handlingStrategy":"try-catch","validationCode":"// Ensure the tokenId comes from the current user's own list before revoking\nconst mine = await convex.query(api.mcpProTokens.listProMcpTokens);\nif (!mine.some((t) => t._id === tokenId)) { removeRow(tokenId); return; }","typeGuard":"function ownsToken(row: { userId: string } | null, userId: string): boolean {\n  return !!row && row.userId === userId;\n}","tryCatchPattern":"try {\n  await convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId });\n} catch (err) {\n  if (err.data === \"NOT_FOUND\") removeRow(tokenId); // idempotent — already gone\n  else throw err;\n}","preventionTips":["Refresh the token list after any issue/revoke before showing actions.","Treat NOT_FOUND as idempotent success (token already gone).","NOT_FOUND deliberately hides foreign tokens — don't retry with the same id.","Verify tokenId comes from the current user's list."],"tags":["convex","mcp","tenancy","not-found"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}