{"record":{"id":"8a953f9ce32cbb99","repo":"koala73/worldmonitor","slug":"api-access-required","errorCode":"API_ACCESS_REQUIRED","errorMessage":"API_ACCESS_REQUIRED","messagePattern":"API_ACCESS_REQUIRED","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/apiKeys.ts","lineNumber":64,"sourceCode":"    keyHash: v.string(),\n    scopes: v.optional(v.array(v.string())),\n  },\n  handler: async (ctx, args) => {\n    const userId = await requireUserId(ctx);\n\n    // Entitlement gate: only users with apiAccess may create API keys.\n    // This is catalog-driven — Pro (tier 1) has apiAccess=false;\n    // API_STARTER+ (tier 2+) have apiAccess=true.\n    const entitlement = await ctx.db\n      .query(\"entitlements\")\n      .withIndex(\"by_userId\", (q) => q.eq(\"userId\", userId))\n      .first();\n    if (\n      !entitlement ||\n      entitlement.validUntil < Date.now() ||\n      !entitlement.features.apiAccess\n    ) {\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    }","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/apiKeys.ts#L46-L82","documentation":"Thrown by createApiKey when the caller's entitlement row is missing, expired (validUntil < Date.now()), or has features.apiAccess === false. The apiAccess feature flag is catalog-driven: Pro (tier 1) has apiAccess=false; API_STARTER and above (tier 2+) have apiAccess=true. Only plans with apiAccess may mint API keys.","triggerScenarios":"Calling createApiKey while on a Pro (tier 1) plan; calling after entitlement expiry; calling before any entitlement row exists for the user; calling during a billing-lapse window.","commonSituations":"A Pro user trying to use the API without upgrading; a recently downgraded user whose entitlement row reflects the loss of apiAccess; a brand-new user who has not yet subscribed; a billing webhook delay leaving the row stale.","solutions":["Upgrade to an API_STARTER+ plan (tier 2 or higher) to obtain apiAccess=true.","If recently upgraded, wait for the entitlement webhook to write the new row, or re-check entitlement before retrying.","Surface an upgrade CTA in the UI on receipt of this error rather than retrying.","Confirm the entitlement row's validUntil is in the future and features.apiAccess is true via getEntitlement before calling createApiKey."],"exampleFix":"// before\nawait createApiKey({ name, keyPrefix, keyHash }); // Pro plan\n// after\n// upgrade to API_STARTER+, then:\nconst ent = await getEntitlement();\nif (ent?.features.apiAccess && ent.validUntil > Date.now()) {\n  await createApiKey({ name, keyPrefix, keyHash });\n}","handlingStrategy":"validation","validationCode":"// Before calling createApiKey, confirm entitlement:\nconst ent = await getEntitlement();\nconst hasApiAccess = !!ent && ent.validUntil > Date.now() && ent.features.apiAccess === true;\nif (!hasApiAccess) {\n  // surface upgrade CTA, do not call createApiKey\n}","typeGuard":"function hasApiAccess(ent): boolean {\n  return !!ent\n    && typeof ent.validUntil === 'number' && ent.validUntil > Date.now()\n    && !!ent.features && ent.features.apiAccess === true;\n}","tryCatchPattern":"try {\n  await createApiKey(args);\n} catch (e) {\n  if (e instanceof ConvexError && e.message === 'API_ACCESS_REQUIRED') {\n    // route to upgrade/plan-selection flow; do not retry without a plan change\n  } else throw e;\n}","preventionTips":["GATE the createApiKey UI behind an entitlement check that verifies features.apiAccess === true and validUntil is in the future.","After plan upgrades, await the entitlement webhook write before enabling the API keys panel.","Surface an upgrade CTA (not a retry loop) on API_ACCESS_REQUIRED."],"tags":["convex","entitlements","billing","api-keys","authorization"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}