koala73/worldmonitor · error · ConvexError

INVALID_HASH

INVALID_HASH

Error message

INVALID_HASH

What it means

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.

Solutions

  1. 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.
  2. Validate client-side before calling the mutation: /^[a-f0-9]{64}$/.test(keyHash), and abort with a friendly message if it fails.
  3. 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.
  4. 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.

Example fix

// before
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key));
const keyHash = encodeBase64(digest); // INVALID_HASH: not 64-char hex
// after
const bytes = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key)));
const keyHash = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); // lowercase 64-char hex
Defensive patterns

Strategy: validation

Validate before calling

const SHA256_HEX_RE = /^[a-f0-9]{64}$/;
function isValidKeyHash(keyHash) {
  return typeof keyHash === 'string' && SHA256_HEX_RE.test(keyHash);
}
if (!isValidKeyHash(keyHash)) throw new Error('keyHash must be 64-char lowercase hex SHA-256');

Type guard

function isSha256Hex(v: unknown): v is string {
  return typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
}

Try / catch

try {
  await client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash });
} catch (e) {
  if (String(e).includes('INVALID_HASH')) {
    showUserError('Key could not be created: the key digest is malformed. Please regenerate the key.');
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/5709f0a36a2071f0. Report an issue: GitHub.

Appendix: source

Thrown at convex/embedKeys.ts:92

    // billing event happened to rewrite their row.
    const merged = entitlement
      ? {
          features: mergeEntitlementFeatures(entitlement.planKey, entitlement.features),
          validUntil: entitlement.validUntil,
        }
      : null;
    if (!hasAccountEmbedAccess(identity?.plan, merged, Date.now())) {
      throw new ConvexError("EMBED_ACCESS_REQUIRED");
    }

    if (!args.name.trim()) {
      throw new ConvexError("INVALID_NAME");
    }
    if (!/^wme_[a-f0-9]{5}$/.test(args.keyPrefix)) {
      throw new ConvexError("INVALID_PREFIX");
    }
    if (!/^[a-f0-9]{64}$/.test(args.keyHash)) {
      throw new ConvexError("INVALID_HASH");
    }
    const allowedOrigins = normalizeAllowedOrigins(args.allowedOrigins);

    const active = await ctx.db
      .query("embedKeys")
      .withIndex("by_userId_revokedAt", (q) =>
        q.eq("userId", userId).eq("revokedAt", undefined),
      )
      .collect();
    if (active.length >= MAX_EMBED_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("embedKeys")
      .withIndex("by_keyHash", (q) => q.eq("keyHash", args.keyHash))
      .first();

View on GitHub (pinned to 7d06c8633d)