/** * First-touch referral attribution — server side. * * The browser captures an anonymous visitor's *first* landing context (referral * source, UTM params, referring host, landing path) and persists it across the * signup boundary in a first-party cookie named `an_ft` (see * `client/analytics.ts`). On signup, the auth hook reads that cookie off the * request and enriches the canonical server-side `signup` event so we can * measure where new users came from and how apps spread (virality). * * The browser also keeps the visitor's *last* visit that had a source in an * `an_lt` cookie. First touch answers "how did they find us?" and credits * acquisition; last touch answers "what brought them to sign up?" and credits * the post, video, or person that converted them. * * First touch also has a durable copy, `an_fts`, that our server sets for a * year (see `first-touch-route.ts`). Signup reads both first-touch cookies and * keeps the one `preferFirstTouch` picks. * * This module is intentionally pure and dependency-free so it is trivially * unit-testable and can never throw into the signup path. The single hard rule: * parsing untrusted cookie input must NEVER throw — every accessor is defensive. */ /** * The decoded first-touch attribution object. Mirrors the compact JSON the * client writes into the `an_ft` cookie / `an_attribution` localStorage key. * Every field is optional — the client omits empty fields to keep the cookie * small, and a malformed/absent cookie yields `null`. */ export interface FirstTouchAttribution { ref?: string; via?: string; utm_source?: string; utm_medium?: string; utm_campaign?: string; utm_content?: string; utm_term?: string; gclid?: string; msclkid?: string; vector_source?: string; landing_path?: string; landing_referrer?: string; site_referrer?: string; site_landing_path?: string; landed_at?: string; capture_truncated?: string; } /** The decoded `an_lt` cookie: the latest visit that had a source. */ export interface LastTouchAttribution { ref?: string; via?: string; utm_source?: string; utm_medium?: string; utm_campaign?: string; utm_content?: string; utm_term?: string; gclid?: string; msclkid?: string; vector_source?: string; landing_referrer?: string; site_referrer?: string; site_landing_path?: string; landing_path?: string; touched_at?: string; capture_truncated?: string; } export type SignupOrigin = "browser_signup" /** The framework's Google OAuth callback owns this event. */ | "google_oauth" /** The signup began at a remote MCP OAuth authorize page. */ | "mcp_oauth" /** Federated SSO provisioning an identity into a sibling app. */ | "sso_jit"; export interface SignupAttributionContext { attribution: Record; anonymousId?: string; sessionId?: string; } export declare const SIGNUP_ATTRIBUTION_HEADER_NAME = "x-agent-native-signup-attribution"; export declare const FIRST_TOUCH_COOKIE_NAME = "an_ft"; export declare const LAST_TOUCH_COOKIE_NAME = "an_lt"; /** * Parse a raw `Cookie:` header into a flat name→value map. Tolerates missing * input, extra whitespace, `=` inside values, and malformed pairs. Never throws. */ export declare function parseCookieHeader(cookieHeader: string | null | undefined): Record; export declare function decodeFirstTouchValue(value: string | null | undefined): FirstTouchAttribution | null; export declare function decodeLastTouchValue(value: string | null | undefined): LastTouchAttribution | null; /** * Decode the year-long `an_fts` cookie. A value from another format version * is unreadable here, so it holds no touch. */ export declare function decodeFirstTouchServerValue(value: string | null | undefined): FirstTouchAttribution | null; /** * Read the `an_ft` first-touch attribution out of a raw `Cookie:` header. * Returns `null` when the cookie is absent or unparseable. Never throws. */ export declare function readFirstTouchAttribution(cookieHeader: string | null | undefined): FirstTouchAttribution | null; /** * The `an_fts` cookie value for a first touch, dropping the lowest-priority * fields until `name=value` fits its budget. */ export declare function encodeFirstTouchServerCookieValue(touch: FirstTouchAttribution): string; /** * Read the `an_lt` last-touch attribution out of a raw `Cookie:` header. * Returns `null` when the cookie is absent or unparseable. Never throws. */ export declare function readLastTouchAttribution(cookieHeader: string | null | undefined): LastTouchAttribution | null; export declare function readAnalyticsAnonymousId(cookieHeader: string | null | undefined): string | undefined; export declare function readAnalyticsSessionId(cookieHeader: string | null | undefined): string | undefined; export declare function deriveReferralSource(ft: FirstTouchAttribution | LastTouchAttribution | null): string; export declare function deriveSignupAttribution(ft: FirstTouchAttribution | null): Record; export declare function deriveLastTouchAttribution(lt: LastTouchAttribution | null): Record; /** * Convenience: read the cookie header and derive signup attribution in one * call. Never throws; falls back to `{ referral_source: "direct" }` on any * error. */ export declare function signupAttributionFromCookieHeader(cookieHeader: string | null | undefined): Record; /** * Capture all browser attribution needed by the server-side signup event. * Keep this as one boundary helper so every signup entry point carries the * same values into Better Auth's user-create hook. * * Returns `undefined` when no attribution or anonymous ID identifies a signup * context. A session ID alone cannot establish direct traffic. */ export declare function signupAttributionContextFromCookieHeader(cookieHeader: string | null | undefined): SignupAttributionContext | undefined; export declare function encodeSignupAttributionContext(context: SignupAttributionContext): string; export declare function decodeSignupAttributionContext(value: string | null | undefined): SignupAttributionContext | undefined; /** * Stamp the explicit handoff onto a Better Auth request/API header set. * * This is the only writer of the handoff header, and it always writes: with a * context it sets ours, without one it deletes whatever was there. The header * is unsigned and outranks the request cookie when the hook reads it, so an * inbound copy from the public internet is an attacker-supplied `anonymous_id` * and campaign for someone else's signup row. Never merge — replace. */ export declare function addSignupAttributionHeader(headers: HeadersInit | undefined, context: SignupAttributionContext | undefined): Headers; export declare function signupAttributionContextFromHeaders(headers: Headers | null | undefined): SignupAttributionContext | undefined;