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

  1. Generate a fresh plaintext key for each createApiKey call — never reuse a key value across calls.
  2. If this was an accidental retry, discard the duplicate and use the already-created key (look it up via listApiKeys by prefix).
  3. 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

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.