{"record":{"id":"8bb30b2f930581ee","repo":"koala73/worldmonitor","slug":"invalid-api-key-scopes","errorCode":"INVALID_API_KEY_SCOPES","errorMessage":"INVALID_API_KEY_SCOPES","messagePattern":"INVALID_API_KEY_SCOPES","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/apiKeys.ts","lineNumber":20,"sourceCode":"import { internalMutation, internalQuery, mutation, query } from \"./_generated/server\";\nimport { requireUserId, resolveUserId } from \"./lib/auth\";\nimport { activeAccountForOwner } from \"./companyMonitoring/_shared\";\nimport { ensureActiveAccount } from \"./companyMonitoring/accounts\";\nimport {\n  COMPANY_MONITORING_RPC_SCOPES,\n  type CompanyMonitoringApiScope,\n} from \"../shared/company-monitoring-contract\";\n\n/** Maximum number of active (non-revoked) API keys per user. */\nconst MAX_KEYS_PER_USER = 5;\nconst COMPANY_MONITORING_SCOPES = [\n  ...new Set(Object.values(COMPANY_MONITORING_RPC_SCOPES)),\n] as CompanyMonitoringApiScope[];\n\nfunction normalizeCompanyMonitoringScopes(scopes: string[] | undefined) {\n  if (!scopes || scopes.length === 0) return undefined;\n  if (scopes.length > COMPANY_MONITORING_SCOPES.length || new Set(scopes).size !== scopes.length) {\n    throw new ConvexError(\"INVALID_API_KEY_SCOPES\");\n  }\n  if (scopes.some((scope) => !(COMPANY_MONITORING_SCOPES as readonly string[]).includes(scope))) {\n    throw new ConvexError(\"INVALID_API_KEY_SCOPES\");\n  }\n  return [...scopes].sort() as CompanyMonitoringApiScope[];\n}\n\n// ---------------------------------------------------------------------------\n// Public mutations & queries (require Clerk JWT via ctx.auth)\n// ---------------------------------------------------------------------------\n\n/**\n * Create a new API key.\n *\n * The caller must generate the random key client-side (or in the HTTP action)\n * and pass the SHA-256 hex hash + the first 8 chars (prefix) here.\n * The plaintext key is NEVER stored in Convex.\n *","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/apiKeys.ts#L2-L38","documentation":"Thrown by normalizeCompanyMonitoringScopes when the scopes array is longer than the total set of valid Company Monitoring scopes, OR contains duplicates (Set size !== array length). The total valid scope set is derived from COMPANY_MONITORING_RPC_SCOPES in shared/company-monitoring-contract. The check rejects oversized or duplicate arrays before even examining individual entries, since either condition proves the array cannot be a subset of valid scopes.","triggerScenarios":"Calling createApiKey with scopes containing duplicate entries (e.g. ['read','read']); calling with a scopes array longer than the total number of valid Company Monitoring RPC scopes.","commonSituations":"Client concatenating scope lists without dedup; a UI checkbox group allowing duplicate submissions; a misconfigured script passing every scope twice; stale cache returning a duplicated scope list.","solutions":["Dedupe the scopes array client-side before submitting ([...new Set(scopes)]).","Ensure the array length never exceeds the total valid Company Monitoring scope count.","If the error persists, log the array to confirm no upstream source is duplicating entries."],"exampleFix":"// before\nawait createApiKey({ name, keyPrefix, keyHash, scopes: ['read','write','read'] });\n// after\nconst scopes = [...new Set(['read','write'])];\nawait createApiKey({ name, keyPrefix, keyHash, scopes });","handlingStrategy":"validation","validationCode":"import { COMPANY_MONITORING_RPC_SCOPES } from '../shared/company-monitoring-contract';\nconst VALID = Object.values(COMPANY_MONITORING_RPC_SCOPES);\nfunction normalizeScopes(scopes) {\n  if (!scopes || scopes.length === 0) return undefined;\n  const unique = [...new Set(scopes)];\n  if (unique.length > VALID.length) throw new Error('too many scopes');\n  if (unique.some(s => !VALID.includes(s))) throw new Error('unknown scope');\n  return unique.sort();\n}","typeGuard":"import { COMPANY_MONITORING_RPC_SCOPES } from '../shared/company-monitoring-contract';\nconst VALID = new Set(Object.values(COMPANY_MONITORING_RPC_SCOPES));\nfunction isValidScopeList(input): input is string[] {\n  if (!Array.isArray(input)) return false;\n  const seen = new Set();\n  for (const s of input) {\n    if (typeof s !== 'string' || seen.has(s) || !VALID.has(s)) return false;\n    seen.add(s);\n  }\n  return input.length <= VALID.size;\n}","tryCatchPattern":"try {\n  await createApiKey(args);\n} catch (e) {\n  if (e instanceof ConvexError && e.message === 'INVALID_API_KEY_SCOPES') {\n    // dedupe, drop unknown scopes, and retry\n  } else throw e;\n}","preventionTips":["Dedupe the scopes array ([...new Set(scopes)]) before submitting.","Source the valid scope list from the same shared contract module the backend uses.","Add a unit test that asserts every client-offered scope exists in COMPANY_MONITORING_RPC_SCOPES."],"tags":["convex","validation","api-keys","scopes"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}