/* RFC-0169: the closed paid-feature catalog, the resolved-entitlements shape, and the isEntitled() reader. Stripe Entitlements is the source of truth; this module is the framework- agnostic contract consumed by the build-time resolver, the feature gates, and runtime endpoints. Do not call Stripe here — the resolver command (site OS) performs network I/O and passes the mapped lookup keys to resolveEntitlements(). Do not build a custom entitlements store — Stripe is the source of truth. RFC-0741: add multi-currency feature for multi-currency build pipeline. RFC-0932: add external-link-qr feature for QR code modal entitlement module. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. RFC-1198: cookie-free referral attribution — /r/ partner landings + voluntary referrer field Replace the RFC-1191 wg_ref cookie capture with URL+form attribution: static /r/ and //r/ partner landings (noindex, zero JS, consent-gated via publicNameConsentRef), ?ref prefill on the existing referrerField rail, server-side slug resolution against the bundled partners.yaml, referral_partner_slug/name on the Lagebild ingress contract, legacy /?ref -> /r/ 302 in the generated worker, and full removal of the cookie capture module (wg_ref, referral.attribution entitlement, payload.attribution). RFC-0169, RFC-0706 */ import { parse as yamlParse } from "yaml"; /** Closed catalog of paid features that can gate a site's compiled modules. */ export const ENTITLED_FEATURES = [ "blog", "integrations.channels", "integrations.crm", "integrations.chat", "analytics", "pseo", "team.profiles", // RFC-0240: Angebot modular productization "offer", "booking", "trust", "i18n-extra", "automation", // RFC-0288: Agent Surface action tier (AI-agent-invocable capabilities) "agent.actions", // RFC-0706 / ADR-0028: Nachweisregister commercial module "nachweis", // RFC-0741: multi-currency entitled feature "multi-currency", // RFC-0932: external-link QR code modal module "external-link-qr", ] as const; /** * Features that are always enabled for all sites, regardless of Stripe or override state. * These are platform-level capabilities that every site gets for free. */ export const FREE_ENTITLED_FEATURES: EntitledFeature[] = ["external-link-qr"]; export type EntitledFeature = (typeof ENTITLED_FEATURES)[number]; /** Stripe Feature `lookup_key` → EntitledFeature. The single mapping point. */ export const STRIPE_FEATURE_LOOKUP_MAP: Record = { feature_blog: "blog", feature_integrations_channels: "integrations.channels", feature_integrations_crm: "integrations.crm", feature_integrations_chat: "integrations.chat", feature_analytics: "analytics", feature_pseo: "pseo", feature_team_profiles: "team.profiles", // RFC-0240 feature_offer: "offer", feature_booking: "booking", feature_trust: "trust", feature_i18n_extra: "i18n-extra", feature_automation: "automation", // RFC-0288 feature_agent_actions: "agent.actions", // RFC-0706 feature_nachweis: "nachweis", // RFC-0741 feature_multi_currency: "multi-currency", // RFC-0932 feature_external_link_qr: "external-link-qr", }; /** * RFC-0196: Stripe Feature `lookup_key` → Programmatic Surface index budget (top-K indexable pages * by substance). A site's tier is the highest budget among its active entitlement lookup-keys. */ export const PSEO_TIER_BUDGET: Record = { feature_pseo: 12, // base tier "Быть найденным" — ≈12 target pages feature_pseo_regional: 500, // regional-hub upsell — unlocks d3–d4 feature_pseo_pro: 5000, feature_pseo_scale: 50000, }; /** RFC-0240: tiers that unlock the d3 (region) and d4 levels of the local family. */ export const PSEO_REGIONAL_TIERS = [ "feature_pseo_regional", "feature_pseo_pro", "feature_pseo_scale", ] as const; /** Resolve the index budget for a set of active Stripe lookup-keys (max tier; undefined ⇒ none). */ export function resolvePseoBudget(lookupKeys: readonly string[]): number | undefined { let budget: number | undefined; for (const key of lookupKeys) { const tier = PSEO_TIER_BUDGET[key]; if (tier !== undefined) budget = budget === undefined ? tier : Math.max(budget, tier); } return budget; } export type EntitlementSource = "stripe" | "override" | "none"; /** The generated, content-driven feature set written by `entitlements.resolve`. */ export interface ResolvedEntitlements { customerId: string | null; features: EntitledFeature[]; source: EntitlementSource; /** * RFC-0196: Programmatic Surface tier. `indexBudget` caps the number of indexable generated * pages (top-K by substance score); absent ⇒ unbounded (fail-open). Set by the tier mapping. */ pseo?: { indexBudget?: number; /** RFC-0240: whether the resolved tier is the regional-hub tier or higher (unlocks d3–d4). */ regionalUnlocked?: boolean; }; } export function isValidFeature(value: string): value is EntitledFeature { return (ENTITLED_FEATURES as readonly string[]).includes(value); } /** * Pure resolver. The site-OS command performs the Stripe network call and passes the * mapped lookup keys as `stripeFeatures`; an offline `override` wins for dev/CI. */ export function resolveEntitlements(input: { customerId: string | null; override?: string[]; stripeFeatures?: string[]; /** RFC-0196: optional Programmatic Surface index budget (tier-derived). */ pseoIndexBudget?: number; /** * RFC-0240: explicit regional-hub-or-higher unlock for offline/override mode, where no Stripe * lookup key is available to derive it from. */ pseoRegionalUnlocked?: boolean; /** RFC-0240: raw Stripe lookup keys (Stripe mode only) — used to derive the regional-tier unlock. */ stripeLookupKeys?: string[]; }): ResolvedEntitlements { const dedupe = (values: string[]): EntitledFeature[] => Array.from(new Set(values.filter(isValidFeature))); const regionalUnlocked = input.pseoRegionalUnlocked ?? (input.stripeLookupKeys ?? []).some((key) => (PSEO_REGIONAL_TIERS as readonly string[]).includes(key), ); const pseo = typeof input.pseoIndexBudget === "number" || regionalUnlocked ? { pseo: { indexBudget: input.pseoIndexBudget, regionalUnlocked } } : {}; const mergeFree = (features: string[]): EntitledFeature[] => dedupe([...features, ...FREE_ENTITLED_FEATURES]); if (input.override && input.override.length > 0) { return { customerId: input.customerId, features: mergeFree(input.override), source: "override", ...pseo, }; } if (input.stripeFeatures) { return { customerId: input.customerId, features: mergeFree(input.stripeFeatures), source: "stripe", ...pseo, }; } return { customerId: input.customerId, features: dedupe([...FREE_ENTITLED_FEATURES]), source: "none", ...pseo, }; } /** Reader used by build gates (which modules compile) and runtime endpoints. */ export function isEntitled( resolved: ResolvedEntitlements | null | undefined, feature: EntitledFeature, ): boolean { return !!resolved?.features.includes(feature); } /** * RFC-1191: parse an `entitlements.generated.yaml` document into its feature list, or null when * the document is absent or malformed. Runtime endpoints must inline the file at build time * (`import.meta.glob` + `?raw`) — the worker bundle has no filesystem access to `src/`. */ export function parseEntitledFeatures(raw: string | null | undefined): string[] | null { if (!raw) return null; try { const parsed = yamlParse(raw) as { features?: unknown } | null; return parsed && Array.isArray(parsed.features) ? parsed.features.map(String) : null; } catch { return null; } }