koala73/worldmonitor · error · ConvexError
INVALID_PREFIX
INVALID_PREFIX
Error message
INVALID_PREFIX
What it means
createEmbedKey validates the display prefix against /^wme_[a-f0-9]{5}$/. The client generates the key and sends only the SHA-256 hash plus a short prefix for UI display (shown-once discipline); the prefix must literally start with "wme_" followed by exactly 5 lowercase hex characters. ConvexError("INVALID_PREFIX") is thrown for any other shape.
Solutions
- Generate the prefix as exactly "wme_" + 5 lowercase hex characters, e.g. `"wme_" + randomHex(5)` using crypto.getRandomValues.
- Lowercase any hex before sending: `prefix.toLowerCase()`.
- Send only the first 5 hex chars of the generated key as keyPrefix, never the full key or hash.
- Validate client-side with the same regex /^wme_[a-f0-9]{5}$/ before calling the mutation.
Example fix
// before
const key = crypto.randomUUID().replace(/-/g, "");
await api.embedKeys.createEmbedKey({ name, keyPrefix: key.slice(0, 8), keyHash: sha256(key) });
// after
const key = hexRandom(64); // 64 lowercase hex chars
const keyPrefix = "wme_" + key.slice(0, 5); // matches /^wme_[a-f0-9]{5}$/
await api.embedKeys.createEmbedKey({ name, keyPrefix, keyHash: await sha256Hex(key) }); Defensive patterns
Strategy: validation
Validate before calling
if (!/^wme_[a-f0-9]{5}$/.test(keyPrefix)) throw new Error(`keyPrefix must match wme_ + 5 lowercase hex chars, got: ${keyPrefix}`); Type guard
function isValidEmbedPrefix(p: unknown): p is string {
return typeof p === "string" && /^wme_[a-f0-9]{5}$/.test(p);
} Try / catch
try {
await api.embedKeys.createEmbedKey({ ...args, keyPrefix });
} catch (e) {
if (e instanceof ConvexError && e.data === "INVALID_PREFIX") {
// regenerate the key client-side with the wme_ + 5-hex prefix format
} else throw e;
} Prevention
- Centralize key generation in one helper that emits prefix "wme_" + 5 lowercase hex chars
- Never reuse the apiKeys.ts prefix convention for embed keys
- Lowercase hex before slicing the prefix
- Send exactly 5 hex chars, not the full key, as keyPrefix
When it happens
Trigger: Calling createEmbedKey with keyPrefix that: uses a different product prefix (e.g. "wmk_", "sk_"), has fewer/more than 5 hex chars, contains uppercase hex (ABCDEF) instead of lowercase, includes the full key instead of the 5-char prefix, is empty, or was generated by code copied from the API-keys flow (apiKeys.ts) with a different prefix convention.
Common situations: Reusing key-generation code from the regular API keys module (different prefix), uppercase hex from a toUpperCase() slip or Number formatting, passing the whole generated key string as the prefix, hand-crafted test payloads guessing the format.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/0919c3b0b9284585.
Report an issue: GitHub.
Appendix: source
Thrown at convex/embedKeys.ts:89
// new, so EVERY row written before this deploy omits it and the predicate
// is fail-closed on `undefined`. Gating on the stored value alone would
// lock every existing paid subscriber out of the feature until a Dodo
// 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.dbView on GitHub (pinned to 7d06c8633d)