| /** | |
| * User-facing copy for a failed `/api/chat-analyst` response. | |
| * | |
| * Extracted from `src/components/ChatAnalystPanel.ts` as a zero-runtime-import | |
| * leaf, for the same reason `src/services/mcp-grant-denial.ts` and | |
| * `src/services/premium-denial.ts` exist: the panel module imports DOMPurify and | |
| * `marked` at module scope, both of which self-configure against the global DOM, | |
| * so nothing inside it is reachable from `tsx --test`. The decision below β | |
| * which denial is a retryable "we cannot confirm your access" vs a terminal | |
| * upsell β is exactly the #5600/#5622 flattening the sweep exists to remove, so | |
| * it needs to be executable by a test rather than asserted by a source grep. | |
| * | |
| * The only import is a TYPE, which erases at compile time. | |
| */ | |
| import type { PremiumDenialVerdict } from './premium-denial'; | |
| export const PRO_VERIFICATION_RETRY_MESSAGE = | |
| 'Verifying your Pro access β try again in a moment.'; | |
| /** | |
| * Whether a response is the shared billing-verification 503. | |
| * | |
| * `classifyDenialResponse` deliberately owns only 401/403 β its own tests pin | |
| * that a 5xx is not its business β so this check has to happen before it, or the | |
| * one state where the user's subscription is fine and waiting actually works | |
| * renders the least actionable string we have, `Error 503`. | |
| * | |
| * Keyed on the header rather than the body so the caller's response stream stays | |
| * intact for `classifyDenialResponse`. The header is readable cross-origin | |
| * because #5622 added it to `Access-Control-Expose-Headers`. | |
| */ | |
| export function isBillingVerificationDenial( | |
| status: number, | |
| billingVerificationHeader: string | null, | |
| ): boolean { | |
| return status === 503 && Boolean(billingVerificationHeader); | |
| } | |
| /** | |
| * Copy for a verdict `classifyDenialResponse` already produced. | |
| * | |
| * `entitlement_desync` and the billing-verification 503 share copy on purpose: | |
| * from the user's side they are the same situation β we cannot confirm your | |
| * access right now, and retrying helps. | |
| */ | |
| export function analystDenialMessage( | |
| status: number, | |
| verdict: PremiumDenialVerdict | null, | |
| ): string { | |
| switch (verdict) { | |
| case 'sign_in_required': | |
| return 'Sign in to use the analyst.'; | |
| case 'upgrade_required': | |
| return 'Pro subscription required.'; | |
| case 'entitlement_desync': | |
| return PRO_VERIFICATION_RETRY_MESSAGE; | |
| case 'access_denied': | |
| return 'Analyst temporarily unavailable β try again in a moment.'; | |
| default: | |
| return `Error ${status}`; | |
| } | |
| } | |