{"record":{"id":"6e3f13891f64e6b9","repo":"koala73/worldmonitor","slug":"duplicate-key","errorCode":"DUPLICATE_KEY","errorMessage":"DUPLICATE_KEY","messagePattern":"DUPLICATE_KEY","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/apiKeys.ts","lineNumber":118,"sourceCode":"      const toRevoke = active.slice(0, active.length - (MAX_KEYS_PER_USER - 1));\n      const now = Date.now();\n      for (const key of toRevoke) {\n        await ctx.db.patch(key._id, { revokedAt: now });\n      }\n      // After revoking overflow keys there is always exactly one slot free.\n      activeCount = MAX_KEYS_PER_USER - 1;\n    }\n    if (activeCount >= MAX_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(\"userApiKeys\")\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(\"userApiKeys\", {\n      userId,\n      name: args.name.trim(),\n      keyPrefix: args.keyPrefix,\n      keyHash: args.keyHash,\n      scopes,\n      companyMonitoringAccountId: companyMonitoringAccount?.logicalAccountId,\n      createdAt: Date.now(),\n    });\n\n    return {\n      id,\n      name: args.name.trim(),\n      keyPrefix: args.keyPrefix,\n      scopes,\n      companyMonitoringAccountId: companyMonitoringAccount?.logicalAccountId,","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/apiKeys.ts#L100-L136","documentation":"Thrown by createApiKey as a belt-and-suspenders guard when a row in userApiKeys already exists with the same keyHash (SHA-256). SHA-256 collision is astronomically unlikely, so this practically indicates the same plaintext key was generated twice and submitted. The by_keyHash index is queried to enforce uniqueness at the application layer.","triggerScenarios":"Calling createApiKey with a keyHash that is already present in the userApiKeys table (same plaintext key generated twice, or a retry that re-submitted the same key); a deterministic/rng-seeded test key that collides with an existing row.","commonSituations":"A createApiKey request was retried (network blip, double-click) with the same generated key; a test fixture reused a hardcoded key; a non-cryptographic RNG produced a duplicate token.","solutions":["Generate a fresh plaintext key for each createApiKey call — never reuse a key value across calls.","If this was an accidental retry, discard the duplicate and use the already-created key (look it up via listApiKeys by prefix).","Ensure the key generator uses a cryptographically secure RNG (crypto.getRandomValues / crypto.subtle)."],"exampleFix":"// before — reusing the same key on retry\nawait createApiKey(ctx, { name, keyPrefix, keyHash }); // retry -> DUPLICATE_KEY\n// after — generate a brand-new key each call\nconst plaintext = generateSecureRandomKey(); // fresh each time\nconst keyPrefix = derivePrefix(plaintext);\nconst keyHash = await sha256Hex(plaintext);\nawait createApiKey(ctx, { name, keyPrefix, keyHash });","handlingStrategy":"validation","validationCode":"// Generate a fresh plaintext key per call; never reuse\nconst plaintext = generateSecureRandomKey();\nconst keyHash = await sha256Hex(plaintext);\n// Optionally pre-check uniqueness\nconst keys = await listApiKeys(ctx, {});\nif (keys.some(k => k.keyHash === keyHash)) throw new Error(\"key collision, regenerate\");\nawait createApiKey(ctx, { name, keyPrefix, keyHash });","typeGuard":null,"tryCatchPattern":"try {\n  await createApiKey(ctx, { name, keyPrefix, keyHash });\n} catch (e) {\n  if (e instanceof ConvexError && e.message === \"DUPLICATE_KEY\") {\n    // regenerate a fresh key and retry once\n  } else throw e;\n}","preventionTips":["Always use a cryptographically secure RNG for key generation.","Never retry createApiKey with the same key value.","On duplicate, discard and generate a new key rather than reusing."],"tags":["convex","api-keys","duplicate","uniqueness","crypto"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}