{"record":{"id":"5709f0a36a2071f0","repo":"koala73/worldmonitor","slug":"invalid-hash-5709f0","errorCode":"INVALID_HASH","errorMessage":"INVALID_HASH","messagePattern":"INVALID_HASH","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/embedKeys.ts","lineNumber":92,"sourceCode":"    // billing event happened to rewrite their row.\n    const merged = entitlement\n      ? {\n          features: mergeEntitlementFeatures(entitlement.planKey, entitlement.features),\n          validUntil: entitlement.validUntil,\n        }\n      : null;\n    if (!hasAccountEmbedAccess(identity?.plan, merged, Date.now())) {\n      throw new ConvexError(\"EMBED_ACCESS_REQUIRED\");\n    }\n\n    if (!args.name.trim()) {\n      throw new ConvexError(\"INVALID_NAME\");\n    }\n    if (!/^wme_[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    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();","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/embedKeys.ts#L74-L110","documentation":"createEmbedKey in convex/embedKeys.ts throws \"INVALID_HASH\" as a ConvexError when the client-supplied keyHash fails the strict check /^[a-f0-9]{64}$/. The API expects the client to generate the random embed key locally, SHA-256 hash it, and send only the lowercase 64-character hex digest; the plaintext key is never stored server-side. This error means the hash argument is not a well-formed SHA-256 hex string, so the mutation aborts before any database work.","triggerScenarios":"Calling the createEmbedKey mutation with keyHash that is: not exactly 64 hex characters (e.g. a base64 digest, a truncated hash, a SHA-1/MD5 digest, or the raw plaintext key itself); contains uppercase hex letters (A-F) because the digest was uppercased; has whitespace, a '0x' prefix, or JSON-encoding artifacts; or was built with a hashing step that silently failed and produced undefined/empty string.","commonSituations":"Developers hashing with output encoding other than hex (e.g. base64 from crypto.subtle mishandling or a library default), calling .toUpperCase() on the digest, hashing a template literal that interpolated 'undefined', or porting code from the apiKeys flow with a different hash format. Frontend fetch wrappers that coerce args to strings can also inject whitespace or quotes.","solutions":["Compute the digest as lowercase hex: new Uint8Array(await crypto.subtle.digest('SHA-256', encoded)) then map each byte with toString(16).padStart(2, '0') and join.","Validate client-side before calling the mutation: /^[a-f0-9]{64}$/.test(keyHash), and abort with a friendly message if it fails.","Check that you are hashing the actual key material (not a JSON wrapper or the key's display name) and that no encoding step (base64, uppercasing, trimming) altered the digest.","If the hash comes from another service or stored config, verify the source value with a hex decoder to confirm it is 32 raw bytes / 64 hex chars."],"exampleFix":"// before\nconst digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key));\nconst keyHash = encodeBase64(digest); // INVALID_HASH: not 64-char hex\n// after\nconst bytes = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key)));\nconst keyHash = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); // lowercase 64-char hex","handlingStrategy":"validation","validationCode":"const SHA256_HEX_RE = /^[a-f0-9]{64}$/;\nfunction isValidKeyHash(keyHash) {\n  return typeof keyHash === 'string' && SHA256_HEX_RE.test(keyHash);\n}\nif (!isValidKeyHash(keyHash)) throw new Error('keyHash must be 64-char lowercase hex SHA-256');","typeGuard":"function isSha256Hex(v: unknown): v is string {\n  return typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);\n}","tryCatchPattern":"try {\n  await client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash });\n} catch (e) {\n  if (String(e).includes('INVALID_HASH')) {\n    showUserError('Key could not be created: the key digest is malformed. Please regenerate the key.');\n  } else throw e;\n}","preventionTips":["Always encode SHA-256 output as lowercase hex (padStart(2,'0') per byte); never base64 or uppercase.","Centralize hash generation in one utility used by every caller.","Assert the 64-hex-char invariant in unit tests for the hash helper.","Never send the plaintext key or key name where keyHash is expected."],"tags":["validation","hashing","convex","api-keys"],"backgroundTag":"invalid-argument-format","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"}