{"record":{"id":"b6b05d9a435f914d","repo":"koala73/worldmonitor","slug":"invalid-hash","errorCode":"INVALID_HASH","errorMessage":"INVALID_HASH","messagePattern":"INVALID_HASH","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/apiKeys.ts","lineNumber":84,"sourceCode":"\n    const scopes = normalizeCompanyMonitoringScopes(args.scopes);\n    // Issuing a scoped key is a first-use entry point, so it provisions the\n    // root. Requesting no scopes must stay entirely off Company Monitoring.\n    const companyMonitoringAccount = scopes\n      ? await ensureActiveAccount(ctx, userId, entitlement)\n      : null;\n    if (scopes && !companyMonitoringAccount) {\n      throw new ConvexError(\"COMPANY_MONITORING_ACCESS_DENIED\");\n    }\n\n    if (!args.name.trim()) {\n      throw new ConvexError(\"INVALID_NAME\");\n    }\n    if (!/^wm_[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\n    // Enforce per-user key limit (count only non-revoked keys).\n    //\n    // API keys intentionally reject at the cap instead of silently rotating a\n    // valid key. If a prior race left too many active rows, converge by\n    // revoking enough oldest overflow rows to make room for this create.\n    const existing = await ctx.db\n      .query(\"userApiKeys\")\n      .withIndex(\"by_userId\", (q) => q.eq(\"userId\", userId))\n      .collect();\n    const active = existing.filter((k) => !k.revokedAt);\n    let activeCount = active.length;\n    if (active.length > MAX_KEYS_PER_USER) {\n      active.sort((a, b) => a.createdAt - b.createdAt);\n      const toRevoke = active.slice(0, active.length - (MAX_KEYS_PER_USER - 1));\n      const now = Date.now();\n      for (const key of toRevoke) {","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/apiKeys.ts#L66-L102","documentation":"Thrown by createApiKey when args.keyHash does not match ^[a-f0-9]{64}$ — i.e. it must be exactly 64 lowercase hexadecimal characters (a SHA-256 digest). The plaintext key is never stored; only its SHA-256 hash is persisted, so this guard ensures the hash is well-formed for indexing and lookup.","triggerScenarios":"Calling createApiKey with a keyHash that is not a SHA-256 hex digest: wrong length, uppercase hex, base64 encoding, a raw byte string, or a hash computed with a different algorithm (e.g. MD5/SHA-1).","commonSituations":"The client computed the hash using base64 output instead of hex; used SHA-1 or SHA-512; forgot to lowercase the hex; passed the plaintext key by mistake; a test fixture used a truncated or placeholder hash.","solutions":["Compute the hash as SHA-256 of the plaintext key and encode as 64 lowercase hex characters.","Use the SubtleCrypto API: crypto.subtle.digest('SHA-256', encoded) then convert to lowercase hex.","Verify the hash matches /^[a-f0-9]{64}$/ before calling createApiKey."],"exampleFix":"// before\nawait createApiKey(ctx, { name, keyPrefix, keyHash: btoa(hashBytes) });\n// after\nconst data = new TextEncoder().encode(plaintextKey);\nconst digest = await crypto.subtle.digest(\"SHA-256\", data);\nconst keyHash = [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, \"0\")).join(\"\");\nawait createApiKey(ctx, { name, keyPrefix, keyHash });","handlingStrategy":"validation","validationCode":"async function sha256Hex(s: string): Promise<string> {\n  const d = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(s));\n  return [...new Uint8Array(d)].map(b => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\nconst keyHash = await sha256Hex(plaintextKey);\nif (!/^[a-f0-9]{64}$/.test(keyHash)) throw new Error(\"bad hash\");\nawait createApiKey(ctx, { name, keyPrefix, keyHash });","typeGuard":"function isSha256Hex(h: unknown): h is string {\n  return typeof h === \"string\" && /^[a-f0-9]{64}$/.test(h);\n}","tryCatchPattern":"try {\n  await createApiKey(ctx, { name, keyPrefix, keyHash });\n} catch (e) {\n  if (e instanceof ConvexError && e.message === \"INVALID_HASH\") {\n    keyHash = await sha256Hex(plaintextKey);\n    await createApiKey(ctx, { name, keyPrefix, keyHash });\n  } else throw e;\n}","preventionTips":["Use SubtleCrypto SHA-256 with lowercase hex output.","Unit-test the hash output length and charset.","Never pass base64 or uppercase hex to createApiKey."],"tags":["convex","api-keys","validation","crypto","sha256","input-validation"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}