{"record":{"id":"4e2166610edfecdc","repo":"koala73/worldmonitor","slug":"duplicate-key-4e2166","errorCode":"DUPLICATE_KEY","errorMessage":"DUPLICATE_KEY","messagePattern":"DUPLICATE_KEY","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/embedKeys.ts","lineNumber":112,"sourceCode":"    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(),\n    });\n\n    return { id, name: args.name.trim(), keyPrefix: args.keyPrefix, allowedOrigins };\n  },\n});\n\n/** List all embed keys for the current user (active + revoked). */\nexport const listEmbedKeys = query({\n  args: {},","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/embedKeys.ts#L94-L130","documentation":"createEmbedKey looks up an existing embedKeys row by keyHash on the by_keyHash index and throws \"DUPLICATE_KEY\" if one is found. Since only the SHA-256 hash of the key is stored, two rows with the same hash would be indistinguishable at validation time; this belt-and-suspenders guard rejects a second registration of the same key material. (Collisions are astronomically unlikely, so this usually means the identical key was submitted twice.)","triggerScenarios":"Calling createEmbedKey twice with the same generated key (double-click on the create button, optimistic-UI retry after a slow response, or re-running a migration/script that mints keys); the same key was already registered under this or another user.","commonSituations":"UI not disabling the submit button during the in-flight mutation; a backend job replayed after a timeout; a developer re-running a seed script with a deterministic/fixed key; or copying the same keyHash fixture into multiple test calls.","solutions":["Treat it as success if the existing key is yours: query listEmbedKeys and reuse the already-registered key rather than creating a new one.","Generate a fresh cryptographically random key and retry — a new key yields a new hash and passes the guard.","Debounce/disable the create button while the mutation is in flight, and make client retries idempotent (check for the key before re-submitting).","If the key was revoked earlier, note that the duplicate check queries by hash regardless of revocation — you must generate a new key, not re-register the old hash."],"exampleFix":"// before\nawait client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash }); // DUPLICATE_KEY on retry\n// after\ntry {\n  await client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash });\n} catch (e) {\n  if (!String(e).includes('DUPLICATE_KEY')) throw e; // idempotent retry: key already exists\n}","handlingStrategy":"try-catch","validationCode":"const keys = await client.query(api.embedKeys.listEmbedKeys, {});\nif (keys.some(k => k.revokedAt === null)) {\n  // reuse an existing active key instead of minting a new one\n  return keys.find(k => k.revokedAt === null);\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash });\n} catch (e) {\n  if (String(e).includes('DUPLICATE_KEY')) {\n    return await client.query(api.embedKeys.listEmbedKeys, {}).then(ks => ks.find(k => k.revokedAt === null));\n  } else throw e;\n}","preventionTips":["Disable the create button while the mutation is in flight (single in-flight request).","Make create flows idempotent: reuse existing active keys before generating new ones.","Never use fixed/deterministic keys in seeds or scripts — always generate fresh randomness.","Remember the duplicate check ignores revocation: a revoked key's hash still blocks re-registration."],"tags":["uniqueness","duplicate","convex","api-keys"],"backgroundTag":"file-already-exists","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"}