koala73/worldmonitor · error · ConvexError
DUPLICATE_KEY
DUPLICATE_KEY
Error message
DUPLICATE_KEY
What it means
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.
Source
Thrown at convex/apiKeys.ts:118
const toRevoke = active.slice(0, active.length - (MAX_KEYS_PER_USER - 1));
const now = Date.now();
for (const key of toRevoke) {
await ctx.db.patch(key._id, { revokedAt: now });
}
// After revoking overflow keys there is always exactly one slot free.
activeCount = MAX_KEYS_PER_USER - 1;
}
if (activeCount >= MAX_KEYS_PER_USER) {
throw new ConvexError("KEY_LIMIT_REACHED");
}
// Guard against duplicate hash (astronomically unlikely, but belt-and-suspenders)
const dup = await ctx.db
.query("userApiKeys")
.withIndex("by_keyHash", (q) => q.eq("keyHash", args.keyHash))
.first();
if (dup) {
throw new ConvexError("DUPLICATE_KEY");
}
const id = await ctx.db.insert("userApiKeys", {
userId,
name: args.name.trim(),
keyPrefix: args.keyPrefix,
keyHash: args.keyHash,
scopes,
companyMonitoringAccountId: companyMonitoringAccount?.logicalAccountId,
createdAt: Date.now(),
});
return {
id,
name: args.name.trim(),
keyPrefix: args.keyPrefix,
scopes,
companyMonitoringAccountId: companyMonitoringAccount?.logicalAccountId,View on GitHub (pinned to ffec79ac33)
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).
Example fix
// before — reusing the same key on retry
await createApiKey(ctx, { name, keyPrefix, keyHash }); // retry -> DUPLICATE_KEY
// after — generate a brand-new key each call
const plaintext = generateSecureRandomKey(); // fresh each time
const keyPrefix = derivePrefix(plaintext);
const keyHash = await sha256Hex(plaintext);
await createApiKey(ctx, { name, keyPrefix, keyHash }); Defensive patterns
Strategy: validation
Validate before calling
// Generate a fresh plaintext key per call; never reuse
const plaintext = generateSecureRandomKey();
const keyHash = await sha256Hex(plaintext);
// Optionally pre-check uniqueness
const keys = await listApiKeys(ctx, {});
if (keys.some(k => k.keyHash === keyHash)) throw new Error("key collision, regenerate");
await createApiKey(ctx, { name, keyPrefix, keyHash }); Try / catch
try {
await createApiKey(ctx, { name, keyPrefix, keyHash });
} catch (e) {
if (e instanceof ConvexError && e.message === "DUPLICATE_KEY") {
// regenerate a fresh key and retry once
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/6e3f13891f64e6b9.
Report an issue: GitHub.