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

  1. Generate the prefix as exactly "wme_" + 5 lowercase hex characters, e.g. `"wme_" + randomHex(5)` using crypto.getRandomValues.
  2. Lowercase any hex before sending: `prefix.toLowerCase()`.
  3. Send only the first 5 hex chars of the generated key as keyPrefix, never the full key or hash.
  4. 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

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.db

View on GitHub (pinned to 7d06c8633d)