koala73/worldmonitor · error · ConvexError

INVALID_PREFIX

INVALID_PREFIX

Error message

INVALID_PREFIX

What it means

Input validation on the API-key create mutation: args.keyPrefix does not match the required shape ^wm_[a-f0-9]{5}$ — a 'wm_' prefix followed by exactly five lowercase hex characters. The prefix is generated client-side from the raw key and stored for display, so a malformed prefix means the client generated the key incorrectly or the value was tampered with.

Solutions

  1. Regenerate the API key client-side using the correct 'wm_' + 5 lowercase hex chars prefix format
  2. Validate the prefix against ^wm_[a-f0-9]{5}$ before calling the create mutation
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at convex/apiKeys.ts:81 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/051adc9e963ac266. Report an issue: GitHub.

Appendix: source

Thrown at convex/apiKeys.ts:81

    ) {
      throw new ConvexError("API_ACCESS_REQUIRED");
    }

    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);

View on GitHub (pinned to eeab0a219f)