{"record":{"id":"9f5f88638c7d625f","repo":"koala73/worldmonitor","slug":"not-found-9f5f88","errorCode":"NOT_FOUND","errorMessage":"NOT_FOUND","messagePattern":"NOT_FOUND","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/embedKeys.ts","lineNumber":164,"sourceCode":"      keyPrefix: k.keyPrefix,\n      createdAt: k.createdAt,\n      lastUsedAt: k.lastUsedAt,\n      revokedAt: k.revokedAt,\n      supersededAt: k.supersededAt,\n      allowedOrigins: k.allowedOrigins,\n    }));\n  },\n});\n\n/** Revoke an embed key owned by the current user. */\nexport const revokeEmbedKey = mutation({\n  args: { keyId: v.id(\"embedKeys\") },\n  handler: async (ctx, args) => {\n    const userId = await requireUserId(ctx);\n    const key = await ctx.db.get(args.keyId);\n\n    if (!key || key.userId !== userId) {\n      throw new ConvexError(\"NOT_FOUND\");\n    }\n    if (key.revokedAt) {\n      throw new ConvexError(\"ALREADY_REVOKED\");\n    }\n\n    await ctx.db.patch(args.keyId, { revokedAt: Date.now() });\n    return { ok: true, keyHash: key.keyHash };\n  },\n});\n\n// ---------------------------------------------------------------------------\n// Internal (service-to-service) — called from HTTP actions / middleware\n// ---------------------------------------------------------------------------\n\n/**\n * Look up an embed key by its SHA-256 hash.\n * Returns the key row (with userId) if found and not revoked, else null.\n * Used by the embed edge handler to resolve the embedding account.","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/embedKeys.ts#L146-L182","documentation":"revokeEmbedKey fetches the embedKeys document by keyId and throws \"NOT_FOUND\" when no row exists with that ID or when the row belongs to a different user (key.userId !== the caller's userId from requireUserId). Ownership mismatches are deliberately reported as NOT_FOUND rather than a permission error so callers cannot probe for other users' key IDs. No database change is made.","triggerScenarios":"Calling the revokeEmbedKey mutation with: a keyId from another user's account (or another environment); an ID that was already deleted or never existed; a key ID from the wrong table (e.g. an apiKey id passed to embedKeys.revokeEmbedKey); or a stale ID cached in the client after data was reset/migrated.","commonSituations":"Frontend holding a stale key list after switching Clerk accounts; tests reusing fixtures across users; passing an apiKeys table ID into the embed-keys revoker; or a hard-coded ID from a dev environment used against production.","solutions":["Refresh the key list via listEmbedKeys and revoke using an id from that response — never a cached or hand-copied ID.","Confirm you are authenticated as the user who owns the key (the check compares against the Clerk-derived userId).","Verify the ID comes from the embedKeys table, not apiKeys or another table — Convex IDs are table-scoped.","If the key exists but belongs to another user, you cannot revoke it; have the owner revoke it, and stop treating NOT_FOUND as a probe oracle for other users' keys."],"exampleFix":"// before\nawait client.mutation(api.embedKeys.revokeEmbedKey, { keyId: props.keyId }); // stale/foreign id\n// after\nconst keys = await client.query(api.embedKeys.listEmbedKeys, {});\nconst key = keys.find(k => k.id === props.keyId && k.revokedAt === null);\nif (!key) return; // nothing to revoke for this user\nawait client.mutation(api.embedKeys.revokeEmbedKey, { keyId: key.id });","handlingStrategy":"validation","validationCode":"const keys = await client.query(api.embedKeys.listEmbedKeys, {});\nconst ownsKey = keys.some(k => k.id === keyId);\nif (!ownsKey) throw new Error('Refusing to revoke: key not in current user\\'s embed key list');","typeGuard":"function isOwnedEmbedKeyId(keyId: string, keys: { id: string; revokedAt: number | null }[]): keyId is string {\n  return keys.some(k => k.id === keyId);\n}","tryCatchPattern":"try {\n  await client.mutation(api.embedKeys.revokeEmbedKey, { keyId });\n} catch (e) {\n  if (String(e).includes('NOT_FOUND')) {\n    await refreshKeyList(); // stale id or foreign key — resync and inform the user\n  } else throw e;\n}","preventionTips":["Only pass IDs obtained from listEmbedKeys for the current session/user.","Refresh the key list after account switches and before destructive actions.","Keep table IDs namespaced in client code (embed vs api keys) to avoid cross-table mistakes.","Never hard-code key IDs from one environment into another."],"tags":["not-found","ownership","convex","authorization"],"backgroundTag":"record-not-found","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}