/*
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;
}
}