{"record":{"id":"612612be45880e1f","repo":"koala73/worldmonitor","slug":"key-limit-reached-612612","errorCode":"KEY_LIMIT_REACHED","errorMessage":"KEY_LIMIT_REACHED","messagePattern":"KEY_LIMIT_REACHED","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/embedKeys.ts","lineNumber":103,"sourceCode":"    if (!args.name.trim()) {\n      throw new ConvexError(\"INVALID_NAME\");\n    }\n    if (!/^wme_[a-f0-9]{5}$/.test(args.keyPrefix)) {\n      throw new ConvexError(\"INVALID_PREFIX\");\n    }\n    if (!/^[a-f0-9]{64}$/.test(args.keyHash)) {\n      throw new ConvexError(\"INVALID_HASH\");\n    }\n    const allowedOrigins = normalizeAllowedOrigins(args.allowedOrigins);\n\n    const active = await ctx.db\n      .query(\"embedKeys\")\n      .withIndex(\"by_userId_revokedAt\", (q) =>\n        q.eq(\"userId\", userId).eq(\"revokedAt\", undefined),\n      )\n      .collect();\n    if (active.length >= MAX_EMBED_KEYS_PER_USER) {\n      throw new ConvexError(\"KEY_LIMIT_REACHED\");\n    }\n\n    // Guard against duplicate hash (astronomically unlikely, but belt-and-suspenders)\n    const dup = await ctx.db\n      .query(\"embedKeys\")\n      .withIndex(\"by_keyHash\", (q) => q.eq(\"keyHash\", args.keyHash))\n      .first();\n    if (dup) {\n      throw new ConvexError(\"DUPLICATE_KEY\");\n    }\n\n    const id = await ctx.db.insert(\"embedKeys\", {\n      userId,\n      name: args.name.trim(),\n      keyPrefix: args.keyPrefix,\n      keyHash: args.keyHash,\n      allowedOrigins,\n      createdAt: Date.now(),","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/embedKeys.ts#L85-L121","documentation":"createEmbedKey counts the caller's active (non-revoked) embed keys via the by_userId_revokedAt index and throws \"KEY_LIMIT_REACHED\" when there are already MAX_EMBED_KEYS_PER_USER (5) of them. This is a quota guard: embed keys are published in partner HTML, so the server caps how many live keys each account can hold. The mutation is rejected before the duplicate-hash check and insert.","triggerScenarios":"Calling createEmbedKey when the authenticated user already has 5 embedKeys rows with revokedAt === undefined (undefined means 'not revoked' in the index query). Revoked keys do not count, so this only fires on the 6th concurrent active key.","commonSituations":"Partner integrations spinning up a new embed key per site or environment without revoking old ones; retry storms creating duplicates after perceived failures; test suites minting keys repeatedly under one shared account; or a team account that pooled usage into a single user and exhausted its 5-key allowance.","solutions":["Call revokeEmbedKey on unused active keys first (releases quota since only non-revoked keys count), then create the new key.","Reuse an existing active key instead of minting a new one — list them with listEmbedKeys and pick an unrevoked one.","If the limit legitimately blocks a real use case, request a higher MAX_EMBED_KEYS_PER_USER or split usage across accounts per product policy.","Add client-side guard: fetch listEmbedKeys, count keys with revokedAt === null, and prompt cleanup before allowing creation."],"exampleFix":"// before\nawait client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash }); // KEY_LIMIT_REACHED\n// after\nconst keys = await client.query(api.embedKeys.listEmbedKeys, {});\nfor (const k of keys.filter(k => k.revokedAt === null).slice(0, keys.length - 4)) {\n  await client.mutation(api.embedKeys.revokeEmbedKey, { keyId: k.id });\n}\nawait client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash });","handlingStrategy":"validation","validationCode":"const MAX_EMBED_KEYS_PER_USER = 5;\nasync function canCreateEmbedKey(client) {\n  const keys = await client.query(api.embedKeys.listEmbedKeys, {});\n  return keys.filter(k => k.revokedAt === null).length < MAX_EMBED_KEYS_PER_USER;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash });\n} catch (e) {\n  if (String(e).includes('KEY_LIMIT_REACHED')) {\n    openKeyCleanupDialog(); // prompt user to revoke unused keys\n  } else throw e;\n}","preventionTips":["Check the active-key count with listEmbedKeys before minting a new key.","Revoke keys as part of offboarding sites/environments, not just creating them.","Never share one account across many integrations that each mint keys.","In tests, revoke keys in afterEach to avoid quota buildup."],"tags":["quota","rate-limit","convex","api-keys"],"backgroundTag":"resource-limit-exceeded","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"}