koala73/worldmonitor · error · ConvexError
INVALID_HASH
INVALID_HASH
Error message
INVALID_HASH
What it means
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.
Source
Thrown at convex/apiKeys.ts:84
const scopes = normalizeCompanyMonitoringScopes(args.scopes);
// Issuing a scoped key is a first-use entry point, so it provisions the
// root. Requesting no scopes must stay entirely off Company Monitoring.
const companyMonitoringAccount = scopes
? await ensureActiveAccount(ctx, userId, entitlement)
: null;
if (scopes && !companyMonitoringAccount) {
throw new ConvexError("COMPANY_MONITORING_ACCESS_DENIED");
}
if (!args.name.trim()) {
throw new ConvexError("INVALID_NAME");
}
if (!/^wm_[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");
}
// Enforce per-user key limit (count only non-revoked keys).
//
// API keys intentionally reject at the cap instead of silently rotating a
// valid key. If a prior race left too many active rows, converge by
// revoking enough oldest overflow rows to make room for this create.
const existing = await ctx.db
.query("userApiKeys")
.withIndex("by_userId", (q) => q.eq("userId", userId))
.collect();
const active = existing.filter((k) => !k.revokedAt);
let activeCount = active.length;
if (active.length > MAX_KEYS_PER_USER) {
active.sort((a, b) => a.createdAt - b.createdAt);
const toRevoke = active.slice(0, active.length - (MAX_KEYS_PER_USER - 1));
const now = Date.now();
for (const key of toRevoke) {View on GitHub (pinned to ffec79ac33)
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.
Example fix
// before
await createApiKey(ctx, { name, keyPrefix, keyHash: btoa(hashBytes) });
// after
const data = new TextEncoder().encode(plaintextKey);
const digest = await crypto.subtle.digest("SHA-256", data);
const keyHash = [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, "0")).join("");
await createApiKey(ctx, { name, keyPrefix, keyHash }); Defensive patterns
Strategy: validation
Validate before calling
async function sha256Hex(s: string): Promise<string> {
const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
return [...new Uint8Array(d)].map(b => b.toString(16).padStart(2, "0")).join("");
}
const keyHash = await sha256Hex(plaintextKey);
if (!/^[a-f0-9]{64}$/.test(keyHash)) throw new Error("bad hash");
await createApiKey(ctx, { name, keyPrefix, keyHash }); Type guard
function isSha256Hex(h: unknown): h is string {
return typeof h === "string" && /^[a-f0-9]{64}$/.test(h);
} Try / catch
try {
await createApiKey(ctx, { name, keyPrefix, keyHash });
} catch (e) {
if (e instanceof ConvexError && e.message === "INVALID_HASH") {
keyHash = await sha256Hex(plaintextKey);
await createApiKey(ctx, { name, keyPrefix, keyHash });
} else throw e;
} Prevention
- Use SubtleCrypto SHA-256 with lowercase hex output.
- Unit-test the hash output length and charset.
- Never pass base64 or uppercase hex to createApiKey.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- INVALID_NAME
- INVALID_PREFIX
- INVALID_API_KEY_SCOPES
- DUPLICATE_KEY
- [initRamp] rampCurve entries must be positive integers
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/b6b05d9a435f914d.
Report an issue: GitHub.