/*
[RFC-1222] Trust-handoff referral-partner registry contract — typed loader for
the site-authored `content/referral-partners/partners.yaml` with an explicit
lifecycle (pending|active|retired) and a structured consent union. The schema
gates identity: active partners need recorded consent, pending partners emit
the landing minus identity (anonymous copy), retired partners keep a nameless
status page. Resolver and landing
consumers read this module; client-safe constants stay in ./patterns.ts.
- No legacy dual path — the v1 fields (`active`, `publicNameConsentRef`) are schema-invalid by design.
- No runtime fetch — the loader parses an already-bundled YAML payload (build-time inlining).
- No client bundle — this module is server/build-side; import patterns.ts from client code.
- RFC-1222: schema v2 — `status` lifecycle replaces `active`, structured `consent` union (none|document|channel) replaces `publicNameConsentRef`, `role` + `retiredAt` added, schema enforces active⇒consent granted and retired⇒retiredAt, slug-PII guard exported; forward-only, no v1 acceptance.
- RFC-1258: schema v3 — `quote`/`quoteConsent` replaced forward-only by `statement`/`statementConsent`/`statementConfirmedAt`; `basis[]` (claim+verifiedBy+date), per-partner `scope`, `validUntil` (required on active), consent-gated `website`/`photo`, `relationToClientWorld`; expiry is data + validator, the lifecycle enum stays closed (DNA-19).
- REVIEW-CODE-2026-10-11-01 fixes: uk phrase-guard patterns + explicit-delimiter uk negation (JS \b is ASCII-only), lang-aware sentence splitting, thousands-separator-safe money parsing, confirms aria-label, photo leading-slash contract, sectionNavItems statement/passtnicht anchors.
- RFC-1277: schema v4 — isClient, instagram/instagramConsent, approval audit record, per-block display switches (hide a block without deleting data or consent); gates mirror the website/photo pattern.
- RFC-1277: review fixes: JSON-twin honors display.*; drop orphaned landing: copy facts; heroAbout requires statement gate; CSS class + dead ghost CTA; v6 wording in validators/docs; REVIEW_TOKENS word boundaries; schema-gate tests (81+73 green)
RFC-1198
*/
import { z } from "zod";
import { parse as yamlParse } from "yaml";
import { REFERRAL_SLUG_PATTERN, REF_PARAM_PATTERN, REFERRER_MAX_LENGTH } from "./patterns.ts";
export { REFERRAL_SLUG_PATTERN, REF_PARAM_PATTERN, REFERRER_MAX_LENGTH };
/** RFC-1258 phrase guard — closed denylist of unverifiable claim patterns for
authored referral copy (brief §9: "garantiert", "der beste", "viele
zufriedene Kunden", …). The validator flags a sentence matching a pattern
unless the same sentence also carries a negation token (de: nicht/kein/
ohne; uk: не/без/жодного) — negated forms ("Wir garantieren nicht") are
legitimate. Extend via RFC, never silently. */
export const REFERRAL_PHRASE_GUARD_PATTERNS: readonly {
readonly label: string;
readonly pattern: RegExp;
}[] = [
{ label: "garantiert", pattern: /\bgarant\w*\b/i },
{
label: "der/die/das beste",
pattern: /\bbeste\b|\bbesten\b|\bbester\b|\bbestes\b|\bbestem\b/i,
},
{ label: "viele zufriedene Kunden", pattern: /\bviele\s+zufriedene\s+Kunden\b/i },
{ label: "Platz 1 bei Google", pattern: /\bPlatz\s*1\b|\bPosition\s*1\b/i },
{ label: "Nummer 1", pattern: /\bNummer\s*1\b/i },
{ label: "гарантія", pattern: /гарант[а-яіїєґ]*/i },
{ label: "найкращий", pattern: /найкращ[а-яіїєґ]*/i },
{
label: "багато задоволених клієнтів",
pattern: /багато\s+задоволених\s+клієнтів/i,
},
];
/** Negation tokens that legitimize a sentence carrying a guard pattern —
German and Ukrainian (the registry's two locales). Cyrillic needs explicit
delimiters: JS `\b` is ASCII-only (`\w` = [A-Za-z0-9_]), so `\bне\b` never
matches — the negation word is matched with punctuation/space delimiters. */
export const REFERRAL_PHRASE_NEGATION_PATTERN =
/\bnicht\b|\bkein\w*\b|\bohne\b|\bnie\b|(^|[\s,.!?;:«»„"'\-])не([\s,.!?;:«»„"']|$)|(^|[\s,.!?;:«»„"'\-])без([\s,.!?;:«»„"']|$)|жодн[а-яіїєґ]*/i;
/** Slugs that may never name a partner — platform-owned namespaces only.
Partner slugs live under the `/r/` prefix and cannot collide with authored
routes; this list guards only against names that would mimic platform
machinery inside that namespace. Site-specific page slugs are deliberately
NOT listed — they are site content, not platform constants. */
export const RESERVED_REFERRAL_SLUGS: readonly string[] = [
"admin",
"api",
"portal",
"r",
"robots",
"sitemap",
"well-known",
];
/** Open ISO-639-1 language code — the site's `system.md i18n.supported`
decides which locales actually emit; the schema stays language-agnostic. */
const LangCodeSchema = z.string().regex(/^[a-z]{2}$/);
/** Localized display phrase map — `{ de: "…", uk: "…" }`. Site-authored
because legal wording must be authored, never derived. */
const LocalizedPhraseSchema = z.record(LangCodeSchema, z.string().min(1));
const IsoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
/** RFC-1222 + referral-brief-v1: partner lifecycle. Pending partners emit the
landing minus identity while public-name consent is unrecorded; active
partners emit the full landing; retired partners keep a nameless status
page; paused partners emit nothing public — the slug falls back to the
neutral /r/ document (referral-brief-v1 mode D). */
export const ReferralPartnerStatusSchema = z.enum(["pending", "active", "retired", "paused"]);
export type ReferralPartnerStatus = z.infer;
/** RFC-1222: structured consent record aligned with the `pbp/consent@1`
method vocabulary. `none` records the explicit absence of consent — a
partner declaring it can never be `active`. `document` points at a
pbp/consent entity under `business-profile/{lang}/trust/consents/`.
`channel` records the venue consent arrived on. `grantedAt` is mandatory
on every granted variant. Members are strict — an unrecognised key fails
the file rather than being silently stripped. */
export const ReferralConsentSchema = z.discriminatedUnion("method", [
z.object({ method: z.literal("none") }).strict(),
z
.object({
method: z.literal("document"),
ref: z.string().min(1),
grantedAt: IsoDateSchema,
})
.strict(),
z
.object({
method: z.literal("channel"),
channel: z.enum(["email", "telegram", "whatsapp", "phone", "in-person"]),
grantedAt: IsoDateSchema,
})
.strict(),
]);
export type ReferralConsent = z.infer;
/** Granted-consent subset (document|channel) — used for the quote gate where
`none` is meaningless: an unconsented quote simply stays absent. */
const GrantedConsentSchema = z.discriminatedUnion("method", [
z
.object({
method: z.literal("document"),
ref: z.string().min(1),
grantedAt: IsoDateSchema,
})
.strict(),
z
.object({
method: z.literal("channel"),
channel: z.enum(["email", "telegram", "whatsapp", "phone", "in-person"]),
grantedAt: IsoDateSchema,
})
.strict(),
]);
export const ReferralPartnerSchema = z
.object({
slug: z
.string()
.regex(REFERRAL_SLUG_PATTERN)
.min(3)
.max(32)
.refine((s) => !RESERVED_REFERRAL_SLUGS.includes(s), { message: "reserved slug" }),
/** Public display name — consent-gated: withheld while `pending`,
never emitted on `retired`/`paused` pages or nameless JSON docs.
Optional (referral-brief-v1 mode B): a partner may exist with no
recorded name — the landing then runs anonymous. */
displayName: z.string().min(1).optional(),
/** Localized public role statement (`{ de: "…", uk: "…" }`). Falls under
the same consent gate as displayName — it describes the same person. */
role: LocalizedPhraseSchema.optional(),
/** RFC-1258 (replaces RFC-1222 `quote`): the recommender's own voice —
2–4 first-person sentences, consent-gated, dated. Never authored by
an agent; the content carries the recommender's questionnaire answers. */
statement: LocalizedPhraseSchema.optional(),
/** RFC-1258: recorded consent covering the statement (same union shape
the former `quoteConsent` used). Required iff `statement` is set. */
statementConsent: GrantedConsentSchema.optional(),
/** RFC-1258: the date the statement was confirmed with the recommender —
rendered as the signature date under the statement. Required iff
`statement` is set. */
statementConfirmedAt: IsoDateSchema.optional(),
/** RFC-1258: "Was ich selbst geprüft habe" — each item: claim (localized)
+ who verified it + the date. Items the recommender did not verify are
simply absent; requires `statement`. */
basis: z
.array(
z
.object({
claim: LocalizedPhraseSchema,
verifiedBy: z.enum(["recommender", "warpgogol"]),
date: IsoDateSchema,
})
.strict(),
)
.optional(),
/** RFC-1258: the recommender's own fit view — localized lists. Requires
`statement`; renders in the recommender's voice section, never in the
studio's transparency card (two voices, two scopes). */
scope: z
.object({
fits: z.array(LocalizedPhraseSchema).min(1),
doesNotFit: z.array(LocalizedPhraseSchema).min(1),
})
.strict()
.optional(),
/** RFC-1258: the recommendation holds until this date — required on
`active` partners (validator enforces future-at-validate-time; the
schema stays time-free per DNA-105). */
validUntil: IsoDateSchema.optional(),
/** RFC-1258: the recommender's website — consent-gated like the name. */
website: z.string().url().optional(),
websiteConsent: GrantedConsentSchema.optional(),
/** RFC-1258: the recommender's photo — a site-root-relative public path
(e.g. "/r/assets/ostpost/portrait.jpg" under `public/`; the landing is
static and outside the content-collection image pipeline), consent-
gated like the name. */
photo: z
.string()
.min(1)
.regex(/^\//, "photo must be a site-root-relative path starting with /")
.optional(),
photoConsent: GrantedConsentSchema.optional(),
/** RFC-1258: one phrase about the recommender's relation to the client's
world ("Ich begleite Betriebe, die …") — consent-gated like role. */
relationToClientWorld: LocalizedPhraseSchema.optional(),
/** RFC-1277 (brief v3 p.3): is the recommender a Warpgogol client —
her own answer. `false` renders an explicit "not a client" line in
the basis block; absent = unanswered, nothing renders. */
isClient: z.boolean().optional(),
/** RFC-1277 (brief v3 1.2): the recommender's Instagram — consent-gated
like `website`; renders as a second verification link in the hero. */
instagram: z.string().url().optional(),
instagramConsent: GrantedConsentSchema.optional(),
/** RFC-1277 (brief v3 1.3/§4): the recommender's written approval of the
final preview page — the audit record, not a render gate (consent
gates stay `statementConsent` + `statementConfirmedAt`). */
approval: z
.object({
approvedOn: IsoDateSchema,
/** Mail id / file reference of the approval letter. */
evidence: z.string().min(1),
})
.strict()
.optional(),
/** RFC-1277 (operator decision): per-block display switches — hide a
rendered voice block without deleting data or consent. Absent flag
= shown; `false` may only hide, never reveal — every block still
requires its data and consent. */
display: z
.object({
/** brief 3.1 — statement text + signature (gates basis/fits too). */
statement: z.boolean().optional(),
/** brief 3.2 — basis list + the isClient line. */
basis: z.boolean().optional(),
/** brief 3.3 — her fits / does-not-fit view (scope). */
fits: z.boolean().optional(),
/** relationToClientWorld line under the hero role. */
heroAbout: z.boolean().optional(),
/** website + instagram verification links in the hero. */
links: z.boolean().optional(),
/** the consent-gated portrait. */
photo: z.boolean().optional(),
})
.strict()
.optional(),
compensation: z.enum(["referral-fee", "none", "steward"]),
/** Language codes for the localized landings. Open set (ISO-639-1) — the
site's `system.md i18n.supported` decides which of them actually emit;
the unprefixed `/r/` canonical landing always emits for active
partners in the site default language. */
locales: z.array(LangCodeSchema).min(1),
/** referral-brief-v1 `lang_default`: first-display language for the
partner's shared link — the worker redirects the unprefixed
`/r/` to this locale's twin when set and ≠ site default. */
langDefault: LangCodeSchema.optional(),
status: ReferralPartnerStatusSchema,
consent: ReferralConsentSchema,
/** Present iff status is "retired" — the status page cites the date. */
retiredAt: IsoDateSchema.optional(),
})
.strict()
.superRefine((partner, ctx) => {
if (partner.status === "active" && partner.consent.method === "none") {
ctx.addIssue({
code: "custom",
message: `active partner "${partner.slug}" has consent.method none — consent must be recorded before emission`,
path: ["consent"],
});
}
if (partner.status === "active" && !partner.displayName) {
ctx.addIssue({
code: "custom",
message: `active partner "${partner.slug}" has no displayName — recorded consent covers a name that must exist`,
path: ["displayName"],
});
}
if (partner.status === "retired" && !partner.retiredAt) {
ctx.addIssue({
code: "custom",
message: `retired partner "${partner.slug}" requires retiredAt`,
path: ["retiredAt"],
});
}
if (partner.status !== "retired" && partner.retiredAt) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries retiredAt but is not retired`,
path: ["retiredAt"],
});
}
// RFC-1222 slug-privacy guard: the slug must not spell a display-name
// token — the URL itself would leak the identity it pseudonymizes.
if (partner.displayName && slugContainsNameToken(partner.slug, partner.displayName)) {
ctx.addIssue({
code: "custom",
message: `partner slug "${partner.slug}" contains a displayName token — pick a pseudonymous slug`,
path: ["slug"],
});
}
// RFC-1258 statement gate (replaces the RFC-1222 quote rules): the
// recommender's voice needs recorded consent, active status, a confirmed
// date, and locale membership.
if (partner.statement) {
if (!partner.statementConsent) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries a statement but no statementConsent — the recommender's voice needs recorded consent`,
path: ["statementConsent"],
});
}
if (!partner.statementConfirmedAt) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries a statement but no statementConfirmedAt — a dated claim requires its confirmation date`,
path: ["statementConfirmedAt"],
});
}
if (partner.status !== "active") {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries a statement but is not active — the voice renders only on the named landing`,
path: ["statement"],
});
}
for (const lang of Object.keys(partner.statement)) {
if (!partner.locales.includes(lang)) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" statement.${lang} is outside declared locales`,
path: ["statement", lang],
});
}
}
}
if (partner.statementConsent && !partner.statement) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" records statementConsent but has no statement`,
path: ["statementConsent"],
});
}
if (partner.statementConfirmedAt && !partner.statement) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" records statementConfirmedAt but has no statement`,
path: ["statementConfirmedAt"],
});
}
if (partner.basis) {
if (!partner.statement) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries basis but no statement — the checked-facts list belongs to the recommender's voice`,
path: ["basis"],
});
}
partner.basis.forEach((item, index) => {
if (!item.date || !item.verifiedBy) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" basis[${index}] lacks date or verifiedBy — every basis item carries fact + verifier + date`,
path: ["basis", index],
});
}
for (const lang of Object.keys(item.claim)) {
if (!partner.locales.includes(lang)) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" basis[${index}].claim.${lang} is outside declared locales`,
path: ["basis", index, "claim", lang],
});
}
}
});
}
if (partner.scope) {
if (!partner.statement) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries scope but no statement — the fit view belongs to the recommender's voice`,
path: ["scope"],
});
}
for (const list of ["fits", "doesNotFit"] as const) {
for (const phrase of partner.scope[list]) {
for (const lang of Object.keys(phrase)) {
if (!partner.locales.includes(lang)) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" scope.${list} phrase.${lang} is outside declared locales`,
path: ["scope", list],
});
}
}
}
}
}
if (partner.status === "active" && !partner.validUntil) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" is active without validUntil — a dated recommendation expires; set the term the recommender agreed to`,
path: ["validUntil"],
});
}
if (partner.website && !partner.websiteConsent) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries a website but no websiteConsent — independent verification paths are consent-gated`,
path: ["websiteConsent"],
});
}
if (partner.websiteConsent && !partner.website) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" records websiteConsent but has no website`,
path: ["websiteConsent"],
});
}
if (partner.website && partner.status !== "active") {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries a website but is not active — verification paths render only on the named landing`,
path: ["website"],
});
}
if (partner.photo && !partner.photoConsent) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries a photo but no photoConsent — the portrait is consent-gated like the name`,
path: ["photoConsent"],
});
}
if (partner.photoConsent && !partner.photo) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" records photoConsent but has no photo`,
path: ["photoConsent"],
});
}
if (partner.photo && partner.status !== "active") {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries a photo but is not active — the portrait renders only on the named landing`,
path: ["photo"],
});
}
// RFC-1277 gates — mirror the website/photo pattern.
if (partner.instagram && !partner.instagramConsent) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries an instagram link but no instagramConsent — verification paths are consent-gated`,
path: ["instagramConsent"],
});
}
if (partner.instagramConsent && !partner.instagram) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" records instagramConsent but has no instagram`,
path: ["instagramConsent"],
});
}
if (partner.instagram && partner.status !== "active") {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries an instagram link but is not active — verification paths render only on the named landing`,
path: ["instagram"],
});
}
if (partner.isClient !== undefined && !partner.statement) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries isClient but no statement — the client-status fact belongs to the recommender's voice`,
path: ["isClient"],
});
}
if (partner.approval && !partner.statement) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" records approval but has no statement — approval covers the published voice`,
path: ["approval"],
});
}
if (partner.relationToClientWorld) {
if (!partner.statementConsent) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" carries relationToClientWorld but no statementConsent — the relation phrase is part of the recommender's voice`,
path: ["statementConsent"],
});
}
for (const lang of Object.keys(partner.relationToClientWorld)) {
if (!partner.locales.includes(lang)) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" relationToClientWorld.${lang} is outside declared locales`,
path: ["relationToClientWorld", lang],
});
}
}
}
if (partner.langDefault && !partner.locales.includes(partner.langDefault)) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" langDefault "${partner.langDefault}" is outside declared locales`,
path: ["langDefault"],
});
}
if (partner.role) {
for (const lang of Object.keys(partner.role)) {
if (!partner.locales.includes(lang)) {
ctx.addIssue({
code: "custom",
message: `partner "${partner.slug}" role.${lang} is outside declared locales`,
path: ["role", lang],
});
}
}
}
});
export type ReferralPartner = z.infer;
/** Site-authored landing copy facts — the pieces of the referral landing that
vary per site (economics, audience, role term, legal pointer). The platform
owns the sentence structure; the site owns these phrases. */
export const ReferralLandingConfigSchema = z
.object({
/** Reward display phrase per compensation variant, interpolated into the
disclosure (e.g. `referral-fee: { de: "70 €" }` → "erhält Anna dafür
70 € von …"; `steward` is appended after ", unter anderem"). */
rewardPhrases: z
.object({
"referral-fee": LocalizedPhraseSchema.optional(),
steward: LocalizedPhraseSchema.optional(),
})
.strict()
.optional(),
/** Role term for the steward disclosure variant (e.g. "Markt-Steward"). */
stewardRole: LocalizedPhraseSchema.optional(),
/** referral-brief-v1: AGB section anchors per language for in-copy
references — `lang → anchorKey → anchor` (e.g.
`{ de: { b2b: "sec-1-4" } }`). */
agbAnchors: z.record(LangCodeSchema, z.record(z.string().min(1), z.string().min(1))).optional(),
})
.strict();
export const ReferralPartnersFileSchema = z
.object({
/** Optional per-language slug of the site's referral-program explainer
page (e.g. `{ de: "verantwortungsvolle-empfehlungen" }`). Site-authored —
the platform must not hardcode site routes. When absent, landings omit
the program link. */
programRoutes: z.record(LangCodeSchema, z.string().min(1)).optional(),
/** Optional per-language slug of the site's pricing/scope page — target of
the landing's "Leistungsumfang prüfen →" link next to the open price. */
pricingRoutes: z.record(LangCodeSchema, z.string().min(1)).optional(),
/** Optional site-authored landing copy facts (reward phrases, steward
role term, AGB anchors). */
landing: ReferralLandingConfigSchema.optional(),
partners: z.array(ReferralPartnerSchema).default([]),
})
.superRefine((file, ctx) => {
const seen = new Set();
for (const partner of file.partners) {
if (seen.has(partner.slug)) {
ctx.addIssue({
code: "custom",
message: `duplicate partner slug "${partner.slug}"`,
path: ["partners"],
});
}
seen.add(partner.slug);
}
});
export type ReferralLandingConfig = z.infer;
export type ReferralPartnersFile = z.infer;
/** Partners classified by lifecycle — `active` emits landings and resolves,
`retired` emits a nameless status page, `pending` emits the landing minus
identity (no displayName/role) while public-name consent is unrecorded,
`paused` emits nothing public (referral-brief-v1 mode D — the /r/
request falls back to the neutral document). */
export interface ReferralPartnerLifecycleSets {
readonly active: ReferralPartner[];
readonly retired: ReferralPartner[];
readonly pending: ReferralPartner[];
readonly paused: ReferralPartner[];
}
/** Lifecycle statuses that emit a referral route twin (landing and/or
/r/.json). `paused` is deliberately absent — emitters and the parity
gate must stay locked on this single set even if the enum grows. */
export const REFERRAL_ROUTE_EMITTING_STATUSES: ReadonlySet = new Set([
"active",
"pending",
"retired",
]);
export type ReferralPartnerWarn = (code: string, data: Record) => void;
/** RFC-1222: strict source validation for `referral.partners.validate` —
returns the parsed file or the full issue list (loaders swallow these into
a single warn; the author-stage guard needs each violation named). */
export function validateReferralPartnersSource(
raw: string,
): { file: ReferralPartnersFile; issues: string[] } | { file: null; issues: string[] } {
let yaml: unknown;
try {
yaml = yamlLike(raw);
} catch (error) {
return { file: null, issues: [`yaml: ${String(error).slice(0, 300)}`] };
}
const parsed = ReferralPartnersFileSchema.safeParse(yaml);
if (!parsed.success) {
return {
file: null,
issues: parsed.error.issues.map(
(issue) => `${issue.path.join(".") || "file"}: ${issue.message}`,
),
};
}
return { file: parsed.data, issues: [] };
}
/**
* Parses `partners.yaml` payloads into lifecycle-classified partner sets. A
* malformed file warns (`referral.partners.invalid`) and contributes nothing —
* a bad registry must not fail a build, and strict whole-file validation
* (schema + duplicate slugs) keeps a typo from silently dropping one partner
* while emitting the rest.
*/
export function loadReferralPartners(
globbed: Record,
warn: ReferralPartnerWarn = () => {},
): ReferralPartnerLifecycleSets {
const sets: ReferralPartnerLifecycleSets = { active: [], retired: [], pending: [], paused: [] };
const seen = new Set();
for (const raw of Object.values(globbed)) {
let parsed: ReferralPartnersFile;
try {
parsed = ReferralPartnersFileSchema.parse(yamlLike(raw));
} catch (error) {
const detail =
error instanceof z.ZodError
? (error.issues[0]?.message ?? "schema")
: String(error).slice(0, 200);
warn("referral.partners.invalid", { reason: "schema", detail });
continue;
}
for (const partner of parsed.partners) {
if (RESERVED_REFERRAL_SLUGS.includes(partner.slug)) {
warn("referral.partners.reserved", { slug: partner.slug });
continue;
}
if (seen.has(partner.slug)) {
warn("referral.partners.duplicate", { slug: partner.slug });
continue;
}
seen.add(partner.slug);
sets[partner.status].push(partner);
}
}
return sets;
}
/** Active partners only — the set that may resolve names and emit landings. */
export function loadActiveReferralPartners(
globbed: Record,
warn: ReferralPartnerWarn = () => {},
): ReferralPartner[] {
return loadReferralPartners(globbed, warn).active;
}
/**
* Server-side resolution of the voluntary `referrer` raw input to an ACTIVE
* partner. Trim + case-insensitive match. Returns null when the input is
* empty, over-length, matches no active partner, or hits a pending/retired
* partner — a retired slug must never re-attribute. The raw value still flows
* as `referrer_uri` for manual review (RFC-0567 rail).
*/
export function resolveReferralPartner(
rawInput: string,
partners: readonly ReferralPartner[],
): ReferralPartner | null {
const candidate = rawInput.trim().toLowerCase();
if (!candidate || candidate.length > REFERRER_MAX_LENGTH) return null;
return partners.find((p) => p.slug === candidate && p.status === "active") ?? null;
}
/**
* Reads the optional site-authored `programRoutes` map (lang → page slug) from
* the same partners.yaml. Returns null when absent or malformed — the landing
* then omits the program link rather than guessing a site route.
*/
export function loadReferralProgramRoutes(
globbed: Record,
): Record | null {
for (const raw of Object.values(globbed)) {
try {
const parsed = ReferralPartnersFileSchema.parse(yamlLike(raw));
return parsed.programRoutes ?? null;
} catch {
return null;
}
}
return null;
}
/** Reads the optional site-authored `pricingRoutes` map (lang → page slug). */
export function loadReferralPricingRoutes(
globbed: Record,
): Record | null {
for (const raw of Object.values(globbed)) {
try {
const parsed = ReferralPartnersFileSchema.parse(yamlLike(raw));
return parsed.pricingRoutes ?? null;
} catch {
return null;
}
}
return null;
}
/**
* Reads the optional site-authored `landing` copy-facts block from the same
* partners.yaml. Returns null when absent or malformed — landings then use
* the generic site-agnostic phrases (no reward amount, generic audience).
*/
export function loadReferralLandingConfig(
globbed: Record,
): ReferralLandingConfig | null {
for (const raw of Object.values(globbed)) {
try {
const parsed = ReferralPartnersFileSchema.parse(yamlLike(raw));
return parsed.landing ?? null;
} catch {
return null;
}
}
return null;
}
/**
* RFC-1222 slug-privacy guard: a slug must not contain any token of the
* partner's displayName — otherwise the URL itself leaks the identity it is
* meant to pseudonymize. Tokens are lowercase alphanumeric runs ≥3 chars
* compared against the slug's dash-separated segments (a segment match means
* the name is legible in the URL; substrings inside a longer invented token
* are not a leak).
*/
export function slugContainsNameToken(slug: string, displayName: string): boolean {
const nameTokens = displayName
.toLowerCase()
.split(/[^a-z0-9äöüß]+/u)
.filter((t) => t.length >= 3);
const slugTokens = new Set(slug.toLowerCase().split("-").filter(Boolean));
return nameTokens.some((t) => slugTokens.has(t));
}
/** Partners files are authored YAML; a JSON body (e.g. generated fixture) is
also accepted verbatim. */
function yamlLike(raw: string): unknown {
const trimmed = raw.trim();
if (trimmed.startsWith("{")) return JSON.parse(trimmed);
return yamlParse(trimmed);
}