{"record":{"id":"051adc9e963ac266","repo":"koala73/worldmonitor","slug":"invalid-prefix","errorCode":"INVALID_PREFIX","errorMessage":"INVALID_PREFIX","messagePattern":"INVALID_PREFIX","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/apiKeys.ts","lineNumber":81,"sourceCode":"    ) {\n      throw new ConvexError(\"API_ACCESS_REQUIRED\");\n    }\n\n    const scopes = normalizeCompanyMonitoringScopes(args.scopes);\n    // Issuing a scoped key is a first-use entry point, so it provisions the\n    // root. Requesting no scopes must stay entirely off Company Monitoring.\n    const companyMonitoringAccount = scopes\n      ? await ensureActiveAccount(ctx, userId, entitlement)\n      : null;\n    if (scopes && !companyMonitoringAccount) {\n      throw new ConvexError(\"COMPANY_MONITORING_ACCESS_DENIED\");\n    }\n\n    if (!args.name.trim()) {\n      throw new ConvexError(\"INVALID_NAME\");\n    }\n    if (!/^wm_[a-f0-9]{5}$/.test(args.keyPrefix)) {\n      throw new ConvexError(\"INVALID_PREFIX\");\n    }\n    if (!/^[a-f0-9]{64}$/.test(args.keyHash)) {\n      throw new ConvexError(\"INVALID_HASH\");\n    }\n\n    // Enforce per-user key limit (count only non-revoked keys).\n    //\n    // API keys intentionally reject at the cap instead of silently rotating a\n    // valid key. If a prior race left too many active rows, converge by\n    // revoking enough oldest overflow rows to make room for this create.\n    const existing = await ctx.db\n      .query(\"userApiKeys\")\n      .withIndex(\"by_userId\", (q) => q.eq(\"userId\", userId))\n      .collect();\n    const active = existing.filter((k) => !k.revokedAt);\n    let activeCount = active.length;\n    if (active.length > MAX_KEYS_PER_USER) {\n      active.sort((a, b) => a.createdAt - b.createdAt);","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/apiKeys.ts#L63-L99","documentation":"Thrown by createApiKey when args.keyPrefix does not match the regex ^wm_[a-f0-9]{5}$. The prefix is the visible portion of the API key shown in the UI (format: wm_ followed by exactly 5 lowercase hex characters) and must be derived from the generated plaintext key. It is a format guard ensuring the prefix matches the canonical key scheme.","triggerScenarios":"Calling createApiKey with a keyPrefix that is missing the wm_ prefix, has uppercase hex, wrong length, non-hex characters, or is derived from a key generator that does not produce the wm_<5-hex> shape.","commonSituations":"The client-side key generator produced a different prefix format (e.g. used uppercase, omitted the wm_ scheme, or sliced the wrong number of characters); a manually constructed prefix for testing did not follow the format; version mismatch between the key-generation utility and the server expectation.","solutions":["Generate the prefix from the plaintext key as exactly `wm_` + the first 5 lowercase hex characters of the random token portion.","Ensure the client key-generation code produces lowercase hex only.","Verify the prefix string passed to createApiKey matches /^wm_[a-f0-9]{5}$/ before sending."],"exampleFix":"// before\nawait createApiKey(ctx, { name, keyPrefix: \"WM_AbC12\", keyHash });\n// after — derive prefix from the generated key\nconst token = crypto.getRandomValues(new Uint8Array(16));\nconst hex = [...token].map(b => b.toString(16).padStart(2, \"0\")).join(\"\");\nconst keyPrefix = `wm_${hex.slice(0, 5)}`; // wm_ + 5 lowercase hex\nawait createApiKey(ctx, { name, keyPrefix, keyHash });","handlingStrategy":"validation","validationCode":"function validPrefix(p: string): boolean {\n  return /^wm_[a-f0-9]{5}$/.test(p);\n}\nif (!validPrefix(keyPrefix)) throw new Error(\"keyPrefix must be wm_ + 5 lowercase hex\");\nawait createApiKey(ctx, { name, keyPrefix, keyHash });","typeGuard":"function isKeyPrefix(p: unknown): p is string {\n  return typeof p === \"string\" && /^wm_[a-f0-9]{5}$/.test(p);\n}","tryCatchPattern":"try {\n  await createApiKey(ctx, { name, keyPrefix, keyHash });\n} catch (e) {\n  if (e instanceof ConvexError && e.message === \"INVALID_PREFIX\") {\n    keyPrefix = derivePrefix(plaintextKey); // regenerate correctly\n    await createApiKey(ctx, { name, keyPrefix, keyHash });\n  } else throw e;\n}","preventionTips":["Derive the prefix from the plaintext key with a single tested helper.","Unit-test the prefix generator against the regex.","Never hand-construct the prefix string for production keys."],"tags":["convex","api-keys","validation","regex","input-validation"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}