{"record":{"id":"6b0d505ad4327ee9","repo":"koala73/worldmonitor","slug":"already-revoked-6b0d50","errorCode":"ALREADY_REVOKED","errorMessage":"ALREADY_REVOKED","messagePattern":"ALREADY_REVOKED","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"warning","filePath":"convex/mcpProTokens.ts","lineNumber":240,"sourceCode":"\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":222,"sourceCodeEnd":247,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/mcpProTokens.ts#L222-L247","documentation":"Thrown by `revokeProMcpToken` when the token row exists, is owned by the caller, but already has `revokedAt` set. This guards against double-revoke producing confusing audit trails. Plain-string ConvexError; `err.data === \"ALREADY_REVOKED\"`. Distinct from NOT_FOUND (row missing/foreign) — here the row is found and owned but already in the revoked state.","triggerScenarios":"Calling revoke on a token that was already revoked: a double-click on the revoke button; a retry after a network timeout where the first call actually succeeded; the silent oldest-rotation in `issueProMcpToken` revoked it, and the user then manually revokes the same row.","commonSituations":"User clicks revoke twice before the UI updates; a client-side retry library re-fires the mutation after the first succeeded; a token was auto-rotated by the cap logic and the stale UI still shows it as active.","solutions":["Disable the revoke button immediately on click (optimistic UI) and refresh the list after.","On `err.data === \"ALREADY_REVOKED\"`, treat as idempotent success — the desired state (revoked) is achieved.","Use a request-in-flight guard so a double-click only fires one mutation.","After revoke, optimistically mark the row revoked in local state before the server confirms."],"exampleFix":"// before\nconst revoke = (id) => convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId: id });\n\n// after — idempotent handling + optimistic UI\nconst revoke = async (id) => {\n  setTokens((t) => t.map((x) => x.id === id ? { ...x, revokedAt: Date.now() } : x));\n  try {\n    await convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId: id });\n  } catch (err) {\n    if (err.data === \"ALREADY_REVOKED\" || err.data === \"NOT_FOUND\") return; // desired state\n    throw err;\n  }\n};","handlingStrategy":"try-catch","validationCode":"// Optimistically mark revoked before the call to prevent double-revoke\nsetTokens((t) => t.map((x) => x._id === id ? { ...x, revokedAt: Date.now() } : x));","typeGuard":"function isAlreadyRevoked(row: { revokedAt?: number | null }): boolean {\n  return !!row.revokedAt;\n}","tryCatchPattern":"try {\n  await convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId });\n} catch (err) {\n  if (err.data === \"ALREADY_REVOKED\" || err.data === \"NOT_FOUND\") return; // desired state\n  throw err;\n}","preventionTips":["Disable the revoke button on click (optimistic UI).","Treat ALREADY_REVOKED and NOT_FOUND as idempotent success.","Guard against double-clicks with a request-in-flight flag.","The cap-rotation logic can auto-revoke a token — refresh the list after issue."],"tags":["convex","mcp","idempotency","revoke"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}