{"record":{"id":"cbd23aaba7d1fecc","repo":"koala73/worldmonitor","slug":"embed-access-required","errorCode":"EMBED_ACCESS_REQUIRED","errorMessage":"EMBED_ACCESS_REQUIRED","messagePattern":"EMBED_ACCESS_REQUIRED","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/embedKeys.ts","lineNumber":82,"sourceCode":"\n    const entitlement = await ctx.db\n      .query(\"entitlements\")\n      .withIndex(\"by_userId\", (q) => q.eq(\"userId\", userId))\n      .first();\n    // Merge before gating, unlike createApiKey. `apiAccess` has existed since\n    // the first entitlement row, so reading it raw is safe; `embedAccess` is\n    // new, so EVERY row written before this deploy omits it and the predicate\n    // is fail-closed on `undefined`. Gating on the stored value alone would\n    // lock every existing paid subscriber out of the feature until a Dodo\n    // billing event happened to rewrite their row.\n    const merged = entitlement\n      ? {\n          features: mergeEntitlementFeatures(entitlement.planKey, entitlement.features),\n          validUntil: entitlement.validUntil,\n        }\n      : null;\n    if (!hasAccountEmbedAccess(identity?.plan, merged, Date.now())) {\n      throw new ConvexError(\"EMBED_ACCESS_REQUIRED\");\n    }\n\n    if (!args.name.trim()) {\n      throw new ConvexError(\"INVALID_NAME\");\n    }\n    if (!/^wme_[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    const allowedOrigins = normalizeAllowedOrigins(args.allowedOrigins);\n\n    const active = await ctx.db\n      .query(\"embedKeys\")\n      .withIndex(\"by_userId_revokedAt\", (q) =>\n        q.eq(\"userId\", userId).eq(\"revokedAt\", undefined),\n      )","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/embedKeys.ts#L64-L100","documentation":"createEmbedKey is gated by the shared hasAccountEmbedAccess predicate, which is fail-closed: it requires a verified Clerk PRO plan or an active paid embed entitlement (merged with planKey via mergeEntitlementFeatures, honoring validUntil). ConvexError(\"EMBED_ACCESS_REQUIRED\") is thrown when the authenticated user holds no such access. Note the gate is deliberately NOT the apiAccess flag — embed keys are mintable by every paid tier.","triggerScenarios":"An authenticated user calls createEmbedKey while: they have no entitlements row at all; their entitlement row predates the embedAccess feature (row omits the field) and their plan is not PRO; embedAccess is false in both the stored features and the plan defaults; the entitlement's validUntil is in the past; or identity.plan is not PRO.","commonSituations":"Free-tier user attempting to mint an embed key; a paying customer whose Dodo billing webhook has not yet written/updated the entitlements row (new deploy wrote no embedAccess field on their old row); an expired subscription whose validUntil lapsed; testing with a Clerk account whose plan metadata is not PRO.","solutions":["Upgrade the Clerk account to PRO or purchase an embed entitlement, then retry.","Check the entitlements row for the user in the Convex dashboard (query entitlements by userId) to see if embedAccess/validUntil is what you expect.","If the user paid but the row is stale, retrigger the billing webhook (Dodo) so the entitlement row is rewritten with embedAccess.","Verify your client is authenticated as the intended user — resolveUserIdentity/requireUserId resolve the JWT identity, and a wrong signed-in account fails the gate."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Best-effort pre-check from app state (server gate is authoritative)\nconst canMint = user.plan === \"pro\" || (entitlement?.features?.embedAccess &&\n  (!entitlement.validUntil || entitlement.validUntil > Date.now()));\nif (!canMint) showUpgradePrompt();","typeGuard":"function hasEmbedAccess(plan: string | undefined, ent: { features: Record<string, boolean>; validUntil?: number } | null): boolean {\n  if (plan === \"pro\") return true;\n  return !!ent && ent.features.embedAccess === true &&\n    (ent.validUntil === undefined || ent.validUntil > Date.now());\n}","tryCatchPattern":"try {\n  await api.embedKeys.createEmbedKey(args);\n} catch (e) {\n  if (e instanceof ConvexError && e.data === \"EMBED_ACCESS_REQUIRED\") {\n    // route to upgrade/purchase flow for embed access\n  } else throw e;\n}","preventionTips":["Hide/disable the create-embed-key UI for accounts without embed access","Gate on the merged entitlement, not raw stored features — old rows omit embedAccess","Check entitlement validUntil freshness before enabling the flow","Confirm billing webhooks (Dodo) have written the entitlements row after purchase"],"tags":["convex","authorization","entitlements","embed-keys"],"backgroundTag":"permission-denied","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}