@openpartner/sdk 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/browser.ts","../src/shared.ts"],"sourcesContent":["/**\n * Browser SDK — runs inside a merchant's web app. Captures the `cref`\n * attribution token from landing-page URLs and the first-party cookie\n * the click router sets, stashes it in localStorage so it survives\n * Safari / ITP cookie expiry and SPA navigations, and stitches to the\n * authenticated user on signup or login.\n *\n * Typical integration:\n *\n * import { OpenPartner } from '@openpartner/sdk';\n *\n * const op = OpenPartner.init({ apiUrl: 'https://openpartner.example.com' });\n *\n * // After login/signup, once you know who the user is:\n * await op.identify(user.id);\n */\n\nimport { OpenPartnerError, requestJson, type IdentifyResponse } from './shared.js';\n\nexport { OpenPartnerError };\nexport type { IdentifyResponse };\n\nexport interface OpenPartnerConfig {\n /** Base URL of your OpenPartner API, e.g. \"https://openpartner.example.com\". */\n apiUrl: string;\n /** First-party cookie name set by the click router. Default `_cref`. */\n cookieName?: string;\n /** Query-string parameter that carries the cref on the landing URL. Default `cref`. */\n queryParam?: string;\n /** localStorage key the captured cref is stashed under. Default `openpartner:cref`. */\n storageKey?: string;\n /**\n * Called when identify() fails. Default: log to console. Pass a noop\n * to suppress entirely or hook into your observability stack.\n */\n onError?: (err: unknown) => void;\n}\n\nconst DEFAULTS = {\n cookieName: '_cref',\n queryParam: 'cref',\n storageKey: 'openpartner:cref',\n onError: (err: unknown) => {\n console.warn('[openpartner] identify failed', err);\n },\n};\n\nexport class OpenPartner {\n private config: Required<OpenPartnerConfig>;\n\n private constructor(config: OpenPartnerConfig) {\n this.config = { ...DEFAULTS, ...config } as Required<OpenPartnerConfig>;\n this.captureCref();\n }\n\n static init(config: OpenPartnerConfig): OpenPartner {\n return new OpenPartner(config);\n }\n\n /** The cref we're holding onto, if any. Useful for debugging. */\n getCref(): string | null {\n if (typeof localStorage === 'undefined') return null;\n return localStorage.getItem(this.config.storageKey);\n }\n\n /**\n * Captured referral token for this visitor. Pass to Stripe Checkout\n * as `client_reference_id` to enable webhook-based attribution\n * without an explicit identify() call:\n *\n * stripe.checkout.sessions.create({\n * client_reference_id: op.getReferral() ?? undefined,\n * // ...\n * });\n *\n * Returns null if the visitor didn't arrive via a partner link.\n */\n getReferral(): string | null {\n return this.getCref();\n }\n\n /** Explicitly discard the captured cref. */\n clear(): void {\n if (typeof localStorage === 'undefined') return;\n localStorage.removeItem(this.config.storageKey);\n }\n\n /**\n * Capture the cref from query string or cookie and stash in\n * localStorage. Runs on init so the value survives SPA navigations,\n * ITP cookie expiry, and subsequent page loads.\n */\n private captureCref(): void {\n if (typeof window === 'undefined' || typeof localStorage === 'undefined') return;\n\n const url = new URL(window.location.href);\n const queryCref = url.searchParams.get(this.config.queryParam);\n\n if (queryCref) {\n localStorage.setItem(this.config.storageKey, queryCref);\n return;\n }\n\n const cookieCref = this.readCookie(this.config.cookieName);\n if (cookieCref && !localStorage.getItem(this.config.storageKey)) {\n localStorage.setItem(this.config.storageKey, cookieCref);\n }\n }\n\n private readCookie(name: string): string | null {\n if (typeof document === 'undefined') return null;\n const match = document.cookie.match(new RegExp(`(^|; )${name}=([^;]+)`));\n return match ? decodeURIComponent(match[2]!) : null;\n }\n\n /**\n * Stitch the current captured click to an authenticated user. Call\n * this on login, signup, or anywhere the user becomes known.\n *\n * No-op if no cref was captured. Errors are reported through\n * `config.onError` by default — attribution is best-effort from the\n * browser — but you can await this and try/catch if you want to\n * surface failures explicitly.\n */\n async identify(userId: string): Promise<IdentifyResponse | null> {\n const cref = this.getCref();\n if (!cref) return null;\n\n try {\n return await requestJson<IdentifyResponse>(`${this.config.apiUrl}/attribution/identify`, {\n method: 'POST',\n body: { cref, userId, ts: Date.now() },\n keepalive: true,\n });\n } catch (err) {\n this.config.onError(err);\n return null;\n }\n }\n}\n","/**\n * Shared types + error class for both the browser and server entry\n * points.\n *\n * Keeping these in their own module means browser imports won't drag in\n * `fetch` polyfills or server-only code, and server imports won't touch\n * anything that references `window` or `document`.\n */\n\nexport class OpenPartnerError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly code?: string,\n public readonly detail?: unknown,\n ) {\n super(message);\n this.name = 'OpenPartnerError';\n }\n}\n\nexport interface IdentifyResponse {\n ok: boolean;\n identityId: string | null;\n firstStitch: boolean;\n backfilledAttributions?: number;\n}\n\nexport interface EventAttribution {\n status: 'attributed' | 'no_identity' | 'no_click' | 'outside_window' | 'already_attributed';\n model?: 'last_click' | 'first_click' | 'linear' | 'position';\n touches?: Array<{\n clickId: string;\n partnerId: string;\n weight: number;\n attributionId: string;\n commissionId?: string;\n }>;\n}\n\nexport interface EventResponse {\n ok: boolean;\n eventId: string;\n attribution: EventAttribution;\n}\n\n/**\n * Small fetch helper used by both entries. Normalizes non-2xx responses\n * into OpenPartnerError so callers can try/catch with a stable shape\n * instead of poking at Response objects themselves.\n */\nexport type JsonRequestInit = Omit<RequestInit, 'body'> & { body?: unknown };\n\nexport async function requestJson<T>(\n input: string,\n init: JsonRequestInit = {},\n fetchImpl: typeof fetch = globalThis.fetch,\n): Promise<T> {\n const headers = new Headers(init.headers);\n if (init.body !== undefined && !headers.has('content-type')) {\n headers.set('content-type', 'application/json');\n }\n const res = await fetchImpl(input, {\n ...init,\n headers,\n body:\n init.body === undefined\n ? undefined\n : typeof init.body === 'string'\n ? init.body\n : JSON.stringify(init.body),\n });\n\n const text = await res.text();\n let body: unknown = undefined;\n if (text) {\n try {\n body = JSON.parse(text);\n } catch {\n body = text;\n }\n }\n\n if (!res.ok) {\n const errBody = (body ?? {}) as { error?: string; detail?: unknown };\n throw new OpenPartnerError(\n errBody.error ?? `${res.status} ${res.statusText}`,\n res.status,\n errBody.error,\n errBody.detail ?? body,\n );\n }\n\n return body as T;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/browser.ts","../src/shared.ts"],"sourcesContent":["/**\n * Browser SDK — runs inside a merchant's web app. Captures the `cref`\n * attribution token from landing-page URLs and the first-party cookie\n * the click router sets, stashes it in localStorage so it survives SPA\n * navigations and later visits, and stitches to the authenticated user on\n * signup or login.\n *\n * Typical integration:\n *\n * import { OpenPartner } from '@openpartner/sdk';\n *\n * const op = OpenPartner.init({ apiUrl: 'https://openpartner.example.com' });\n *\n * // After login/signup, once you know who the user is:\n * await op.identify(user.id);\n *\n * With a captured referral it also hands the click to Stripe on its own:\n * Payment Links (buy.stripe.com) get `client_reference_id`, and Stripe\n * Pricing Table / Buy Button embeds get `client-reference-id`, including\n * ones added to the page later. A brand whose Stripe account is connected\n * to OpenPartner then needs no code for those checkouts. `stripe: false`\n * turns it off.\n *\n * How long a referral lasts. Safari deletes storage that a page's own\n * scripts wrote (localStorage, and cookies set through document.cookie)\n * after 7 days without a visit, so a visitor who clicks, leaves for a\n * fortnight and comes back to buy without signing up first arrives with\n * nothing. A cookie the brand's own server sets on the landing response\n * isn't capped that way: `referralCookieHeader` in `@openpartner/sdk/server`\n * writes one, and this SDK reads it back (`op_ref`), on every subdomain it\n * covers. Once identify() has run, the stitch lives server-side and no\n * browser storage matters.\n *\n * Consent. `consent: 'pending'` holds the referral in memory until\n * setConsent('granted'): nothing is read from or written to storage or\n * cookies, no Stripe link is tagged and nothing is sent until then.\n *\n * Landing report. When a click first arrives from the URL the SDK tells\n * OpenPartner (POST /attribution/landing), so Admin → Tracking health can\n * count the clicks that never reached a page running it. `reportLanding:\n * false` turns it off.\n *\n * No bundler? The same SDK ships as a script tag that starts itself\n * (auto.ts), served at /op.js by an OpenPartner portal.\n */\n\nimport { OpenPartnerError, requestJson, type IdentifyResponse } from './shared.js';\n\nexport { OpenPartnerError };\nexport type { IdentifyResponse };\n\nexport interface StripeLinkingConfig {\n /** Extra Payment Link hosts — a custom domain set up for Payment Links\n * in Stripe (e.g. \"pay.yourbrand.com\"). buy.stripe.com is always on. */\n linkHosts?: string[];\n}\n\n/** Whether this visitor agreed to referral tracking. */\nexport type ConsentState = 'granted' | 'pending' | 'denied';\n\nexport interface OpenPartnerConfig {\n /** Base URL of your OpenPartner API, e.g. \"https://openpartner.example.com\". */\n apiUrl: string;\n /** First-party cookie name set by the click router. Default `_cref`. */\n cookieName?: string;\n /** Query-string parameter that carries the cref on the landing URL. Default `cref`. */\n queryParam?: string;\n /** localStorage key the captured cref is stashed under. Default `openpartner:cref`. */\n storageKey?: string;\n /**\n * Hand the captured referral to Stripe Payment Links and Pricing Table /\n * Buy Button embeds on the page. Default on; `false` turns it off.\n */\n stripe?: boolean | StripeLinkingConfig;\n /**\n * Also keep the referral in a first-party cookie, so your server can read\n * it straight off the request when it creates a Stripe Checkout Session\n * (`client_reference_id: req.cookies.op_ref`). Off by default; the\n * script-tag build turns it on. `domain` shares it across subdomains\n * (\".yourbrand.com\") when visitors land on one and check out on another.\n *\n * The SDK reads this cookie back whether or not it writes it (`op_ref`\n * unless `name` says otherwise): that's how a page on another subdomain,\n * or a visit after Safari cleared localStorage, finds the referral, and\n * how one your server set (`referralCookieHeader`) reaches identify().\n */\n referralCookie?: false | { name?: string; domain?: string; days?: number };\n /**\n * Whether the visitor has agreed to referral tracking. Default\n * `'granted'`: the SDK stores the referral as soon as it loads. Behind a\n * consent banner, start at `'pending'` and call `setConsent()` from its\n * callback. Until then the referral from the URL is held in memory only,\n * so a visitor who agrees on the landing page is still credited.\n * `'denied'` uses nothing.\n */\n consent?: ConsentState;\n /**\n * Tell OpenPartner when a partner click arrives on your site, so Admin →\n * Tracking health can show clicks lost between the partner link and your\n * page. Sends the click id once per click, nothing about the visitor.\n * Default on; `false` turns it off.\n */\n reportLanding?: boolean;\n /**\n * Called when a call to OpenPartner fails: identify(), or a landing\n * report OpenPartner answered with `click_not_found` (which usually means\n * `apiUrl` names another program; other landing failures are dropped).\n * Default: log to console. Pass a noop to suppress entirely or\n * hook into your observability stack.\n */\n onError?: (err: unknown) => void;\n}\n\nconst DEFAULTS = {\n cookieName: '_cref',\n queryParam: 'cref',\n storageKey: 'openpartner:cref',\n stripe: true,\n referralCookie: false,\n consent: 'granted',\n reportLanding: true,\n onError: (err: unknown) => {\n console.warn('[openpartner] request failed', err);\n },\n};\n\n/** Stripe accepts a client_reference_id of letters, digits, \"-\" and \"_\",\n * up to 200, and silently drops anything else. A click id always fits; a\n * value that doesn't came from somewhere else and isn't sent. */\nconst STRIPE_REFERENCE_RE = /^[A-Za-z0-9_-]{1,200}$/;\nconst STRIPE_EMBED_SELECTOR = 'stripe-pricing-table, stripe-buy-button';\nconst REFERRAL_COOKIE_DEFAULT = 'op_ref';\n\nexport class OpenPartner {\n private config: Required<OpenPartnerConfig>;\n private observer: MutationObserver | null = null;\n private consent: ConsentState;\n /** The URL's referral while consent is pending, held in memory only. */\n private held: string | null = null;\n /** This page's referral, kept here too: where localStorage throws it is\n * the only copy until the cookie is written. */\n private current: string | null = null;\n /** The Stripe links and embeds this SDK tagged, and with what, so a\n * withdrawn consent can take the tags back off. */\n private tagged: Array<{ el: Element; value: string }> = [];\n\n private constructor(config: OpenPartnerConfig) {\n this.config = { ...DEFAULTS, ...config } as Required<OpenPartnerConfig>;\n this.consent = this.config.consent;\n if (this.consent === 'granted') this.start();\n else if (this.consent === 'pending') this.held = this.queryCref();\n // A returning visitor who has since said no: drop what an earlier\n // visit stored.\n else this.clear();\n }\n\n static init(config: OpenPartnerConfig): OpenPartner {\n return new OpenPartner(config);\n }\n\n /** The cref we're holding onto, if any. Useful for debugging. Null until\n * consent is granted. */\n getCref(): string | null {\n if (this.consent !== 'granted') return null;\n return this.storageGet() ?? this.current ?? this.readCookie(this.referralCookieName());\n }\n\n /**\n * Captured referral token for this visitor. Pass to Stripe Checkout\n * as `client_reference_id` to enable webhook-based attribution\n * without an explicit identify() call:\n *\n * stripe.checkout.sessions.create({\n * client_reference_id: op.getReferral() ?? undefined,\n * // ...\n * });\n *\n * Returns null if the visitor didn't arrive via a partner link, or\n * hasn't agreed to tracking yet.\n */\n getReferral(): string | null {\n return this.getCref();\n }\n\n /** Explicitly discard the captured cref, the referral cookie, and the\n * references this SDK added to Stripe links and embeds. */\n clear(): void {\n this.held = null;\n this.current = null;\n this.untagStripe();\n this.storageRemove();\n if (typeof document === 'undefined') return;\n const name = this.referralCookieName();\n if (!this.readCookie(name)) return;\n const cfg = this.config.referralCookie;\n document.cookie = `${name}=; Max-Age=0; Path=/`;\n if (cfg && cfg.domain) document.cookie = `${name}=; Max-Age=0; Path=/; Domain=${cfg.domain}`;\n }\n\n /**\n * Record the visitor's answer to your consent banner. `'granted'` stores\n * the referral (the one held since the page loaded, or one already\n * stored), starts tagging Stripe links and sends the landing report.\n * `'denied'` stops all of it, takes the references it added off Stripe\n * links and embeds, and deletes what was stored. Calling it again is\n * safe.\n */\n setConsent(state: 'granted' | 'denied'): void {\n if (state === 'granted') {\n if (this.consent === 'granted') return;\n this.consent = state;\n this.start();\n return;\n }\n this.consent = state;\n this.stopLinkingStripe();\n this.clear();\n }\n\n /**\n * Add the referral to every Stripe Payment Link and Pricing Table / Buy\n * Button embed under `root` that doesn't carry a reference already.\n * Runs on its own at init and for anything added later; call it yourself\n * only if you build Stripe elements inside a shadow root. Returns how\n * many it changed.\n */\n linkStripe(root: ParentNode = document): number {\n const referral = this.getReferral();\n if (!referral || !STRIPE_REFERENCE_RE.test(referral) || typeof document === 'undefined') return 0;\n const hosts = new Set(['buy.stripe.com', ...this.stripeLinkHosts()]);\n let changed = 0;\n\n const anchors: HTMLAnchorElement[] = [];\n if (root instanceof HTMLAnchorElement) anchors.push(root);\n anchors.push(...Array.from(root.querySelectorAll<HTMLAnchorElement>('a[href]')));\n for (const a of anchors) {\n let url: URL;\n try {\n url = new URL(a.href);\n } catch {\n continue;\n }\n // One the brand set itself (or an earlier pass) stays as it is.\n if (!hosts.has(url.hostname.toLowerCase()) || url.searchParams.has('client_reference_id')) continue;\n url.searchParams.set('client_reference_id', referral);\n a.href = url.toString();\n this.remember(a, referral);\n changed += 1;\n }\n\n const embeds: Element[] = [];\n if (root instanceof Element && root.matches(STRIPE_EMBED_SELECTOR)) embeds.push(root);\n embeds.push(...Array.from(root.querySelectorAll(STRIPE_EMBED_SELECTOR)));\n for (const el of embeds) {\n if (el.hasAttribute('client-reference-id')) continue;\n el.setAttribute('client-reference-id', referral);\n this.remember(el, referral);\n changed += 1;\n }\n return changed;\n }\n\n private remember(el: Element, value: string): void {\n // Elements an SPA has thrown away go, so the list can't grow forever.\n this.tagged = this.tagged.filter((t) => t.el.isConnected);\n this.tagged.push({ el, value });\n }\n\n /** Take back the references this SDK added. One the brand set, or one\n * changed since, stays. */\n private untagStripe(): void {\n for (const { el, value } of this.tagged) {\n if (el instanceof HTMLAnchorElement) {\n let url: URL;\n try {\n url = new URL(el.href);\n } catch {\n continue;\n }\n if (url.searchParams.get('client_reference_id') !== value) continue;\n url.searchParams.delete('client_reference_id');\n el.href = url.toString();\n } else if (el.getAttribute('client-reference-id') === value) {\n el.removeAttribute('client-reference-id');\n }\n }\n this.tagged = [];\n }\n\n /** Stop watching the page for new Stripe links and embeds. */\n stopLinkingStripe(): void {\n this.observer?.disconnect();\n this.observer = null;\n }\n\n private stripeLinkHosts(): string[] {\n const cfg = this.config.stripe;\n return typeof cfg === 'object' && cfg.linkHosts ? cfg.linkHosts.map((h) => h.trim().toLowerCase()).filter(Boolean) : [];\n }\n\n /** Everything the SDK does once it may: store, mirror, link, report. */\n private start(): void {\n const landed = this.captureCref(this.held);\n this.held = null;\n this.writeReferralCookie();\n if (this.config.stripe !== false) this.watchStripe();\n if (landed && this.config.reportLanding !== false) void this.reportLanding(landed);\n }\n\n /**\n * Link what's on the page now, then anything added or re-pointed later\n * (SPAs, Stripe embeds mounted on demand). Only with a referral: a visitor\n * who didn't come from a partner link costs nothing.\n *\n * Pricing Table and Buy Button embeds read client-reference-id when\n * Stripe's script renders them, so the script tag belongs in <head>,\n * ahead of the embed: the observer then sets the attribute as the\n * element is parsed, before Stripe upgrades it.\n */\n private watchStripe(): void {\n if (typeof document === 'undefined' || this.observer || !this.getReferral()) return;\n this.linkStripe(document);\n if (typeof MutationObserver === 'undefined') return;\n this.observer = new MutationObserver((records) => {\n for (const record of records) {\n if (record.type === 'attributes') {\n // An href re-pointed at a Payment Link.\n if (record.target instanceof HTMLAnchorElement) this.linkStripe(record.target);\n continue;\n }\n for (const node of Array.from(record.addedNodes)) {\n if (node instanceof Element) this.linkStripe(node);\n }\n }\n });\n // Our own edits only ever add a missing reference, so the second pass\n // they trigger finds nothing to change.\n this.observer.observe(document.documentElement, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['href'],\n });\n }\n\n /** The cref on the landing URL, if any. */\n private queryCref(): string | null {\n if (typeof window === 'undefined') return null;\n return new URL(window.location.href).searchParams.get(this.config.queryParam);\n }\n\n /**\n * Capture the cref from the URL (or the one held while consent was\n * pending), else the router's cookie, else the referral cookie, and\n * stash it in localStorage so it survives SPA navigations and later\n * page loads. Returns the cref when it's a click arriving now, one this\n * browser didn't have yet, for the landing report.\n */\n private captureCref(held: string | null): string | null {\n if (typeof window === 'undefined') return null;\n\n const incoming = held ?? this.queryCref();\n if (incoming) {\n const isNew = this.storageGet() !== incoming;\n this.current = incoming;\n this.storageSet(incoming);\n return isNew ? incoming : null;\n }\n\n if (this.storageGet()) return null;\n // A page that isn't the landing page: the router's own cookie (a\n // self-host whose router shares the brand's domain), then the referral\n // cookie a sibling subdomain or the brand's server wrote.\n const restored = this.readCookie(this.config.cookieName) ?? this.readCookie(this.referralCookieName());\n if (restored) {\n this.current = restored;\n this.storageSet(restored);\n }\n return null;\n }\n\n private referralCookieName(): string {\n const cfg = this.config.referralCookie;\n return (cfg && cfg.name) || REFERRAL_COOKIE_DEFAULT;\n }\n\n /**\n * Mirror the referral into the brand's own first-party cookie (when\n * configured). Left alone when it already holds this referral: in Safari\n * rewriting it from script would trade a server-set cookie's full\n * lifetime for the 7-day cap. A new click overwrites it.\n */\n private writeReferralCookie(): void {\n const cfg = this.config.referralCookie;\n if (!cfg || typeof document === 'undefined') return;\n const referral = this.getReferral();\n if (!referral || !STRIPE_REFERENCE_RE.test(referral)) return;\n const name = this.referralCookieName();\n if (this.readCookie(name) === referral) return;\n const maxAge = Math.round((cfg.days ?? 90) * 86400);\n const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';\n const domain = cfg.domain ? `; Domain=${cfg.domain}` : '';\n document.cookie = `${name}=${encodeURIComponent(referral)}; Max-Age=${maxAge}; Path=/; SameSite=Lax${domain}${secure}`;\n }\n\n private readCookie(name: string): string | null {\n if (typeof document === 'undefined') return null;\n const match = document.cookie.match(new RegExp(`(^|; )${name}=([^;]+)`));\n if (!match) return null;\n try {\n return decodeURIComponent(match[2]!);\n } catch {\n return null;\n }\n }\n\n // localStorage throws in some sandboxed iframes and with site data\n // blocked; the referral then lives in the cookie alone.\n private storageGet(): string | null {\n try {\n return typeof localStorage === 'undefined' ? null : localStorage.getItem(this.config.storageKey);\n } catch {\n return null;\n }\n }\n\n private storageSet(value: string): void {\n try {\n if (typeof localStorage !== 'undefined') localStorage.setItem(this.config.storageKey, value);\n } catch {\n // Storage unavailable; see storageGet.\n }\n }\n\n private storageRemove(): void {\n try {\n if (typeof localStorage !== 'undefined') localStorage.removeItem(this.config.storageKey);\n } catch {\n // Storage unavailable; see storageGet.\n }\n }\n\n /** Tell OpenPartner the click arrived. Best-effort, once per click. */\n private async reportLanding(cref: string): Promise<void> {\n if (!STRIPE_REFERENCE_RE.test(cref) || typeof fetch === 'undefined') return;\n try {\n await requestJson(`${this.config.apiUrl}/attribution/landing`, {\n method: 'POST',\n body: { cref },\n keepalive: true,\n });\n } catch (err) {\n // Only a click this program doesn't have is worth reporting: it\n // usually means apiUrl names another program. Anything else (offline,\n // rate limited, an API from before the landing report) is dropped:\n // the report is a diagnostic, never part of attribution.\n if (err instanceof OpenPartnerError && err.code === 'click_not_found') this.config.onError(err);\n }\n }\n\n /**\n * Stitch the current captured click to an authenticated user. Call\n * this on login, signup, or anywhere the user becomes known.\n *\n * No-op if no cref was captured, or consent isn't granted. Errors are\n * reported through `config.onError` by default — attribution is\n * best-effort from the browser — but you can await this and try/catch\n * if you want to surface failures explicitly.\n */\n async identify(userId: string): Promise<IdentifyResponse | null> {\n const cref = this.getCref();\n if (!cref) return null;\n\n try {\n return await requestJson<IdentifyResponse>(`${this.config.apiUrl}/attribution/identify`, {\n method: 'POST',\n body: { cref, userId, ts: Date.now() },\n keepalive: true,\n });\n } catch (err) {\n this.config.onError(err);\n return null;\n }\n }\n}\n","/**\n * Shared types + error class for both the browser and server entry\n * points.\n *\n * Keeping these in their own module means browser imports won't drag in\n * `fetch` polyfills or server-only code, and server imports won't touch\n * anything that references `window` or `document`.\n */\n\nexport class OpenPartnerError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly code?: string,\n public readonly detail?: unknown,\n ) {\n super(message);\n this.name = 'OpenPartnerError';\n }\n}\n\nexport interface IdentifyResponse {\n ok: boolean;\n identityId: string | null;\n firstStitch: boolean;\n backfilledAttributions?: number;\n}\n\nexport interface EventAttribution {\n /**\n * 'not_attributed' only appears on a replay (`replayed: true`) of an event\n * that didn't attribute the first time; which miss it was isn't stored.\n * 'corrective': refund / chargeback / dispute_created / invoice_payment_failed —\n * recorded, never attributed.\n */\n status:\n | 'attributed'\n | 'no_identity'\n | 'no_click'\n | 'outside_window'\n | 'already_attributed'\n | 'not_attributed'\n | 'corrective';\n model?: 'last_click' | 'first_click' | 'linear' | 'position';\n touches?: Array<{\n clickId: string;\n partnerId: string;\n weight: number;\n attributionId: string;\n commissionId?: string;\n }>;\n}\n\nexport interface EventResponse {\n ok: boolean;\n eventId: string;\n attribution: EventAttribution;\n /** True when an earlier call with the same externalEventId already\n * recorded this event; eventId and attribution are that call's. */\n replayed?: boolean;\n}\n\nexport interface ConversionResponse extends EventResponse {\n /** The user the conversion was recorded under: the userId you passed, or\n * the click's own `click:<clickId>` user. */\n userId: string;\n}\n\n/**\n * Small fetch helper used by both entries. Normalizes non-2xx responses\n * into OpenPartnerError so callers can try/catch with a stable shape\n * instead of poking at Response objects themselves.\n */\nexport type JsonRequestInit = Omit<RequestInit, 'body'> & { body?: unknown };\n\nexport async function requestJson<T>(\n input: string,\n init: JsonRequestInit = {},\n fetchImpl: typeof fetch = globalThis.fetch,\n): Promise<T> {\n const headers = new Headers(init.headers);\n if (init.body !== undefined && !headers.has('content-type')) {\n headers.set('content-type', 'application/json');\n }\n const res = await fetchImpl(input, {\n ...init,\n headers,\n body:\n init.body === undefined\n ? undefined\n : typeof init.body === 'string'\n ? init.body\n : JSON.stringify(init.body),\n });\n\n const text = await res.text();\n let body: unknown = undefined;\n if (text) {\n try {\n body = JSON.parse(text);\n } catch {\n body = text;\n }\n }\n\n if (!res.ok) {\n const errBody = (body ?? {}) as { error?: string; detail?: unknown };\n throw new OpenPartnerError(\n errBody.error ?? `${res.status} ${res.statusText}`,\n res.status,\n errBody.error,\n errBody.detail ?? body,\n );\n }\n\n return body as T;\n}\n"],"mappings":"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","names":[]}
@@ -1,12 +1,12 @@
1
- import { I as IdentifyResponse } from './shared-CLfBjPeb.cjs';
2
- export { O as OpenPartnerError } from './shared-CLfBjPeb.cjs';
1
+ import { I as IdentifyResponse } from './shared-dh1vlldf.cjs';
2
+ export { O as OpenPartnerError } from './shared-dh1vlldf.cjs';
3
3
 
4
4
  /**
5
5
  * Browser SDK — runs inside a merchant's web app. Captures the `cref`
6
6
  * attribution token from landing-page URLs and the first-party cookie
7
- * the click router sets, stashes it in localStorage so it survives
8
- * Safari / ITP cookie expiry and SPA navigations, and stitches to the
9
- * authenticated user on signup or login.
7
+ * the click router sets, stashes it in localStorage so it survives SPA
8
+ * navigations and later visits, and stitches to the authenticated user on
9
+ * signup or login.
10
10
  *
11
11
  * Typical integration:
12
12
  *
@@ -16,8 +16,44 @@ export { O as OpenPartnerError } from './shared-CLfBjPeb.cjs';
16
16
  *
17
17
  * // After login/signup, once you know who the user is:
18
18
  * await op.identify(user.id);
19
+ *
20
+ * With a captured referral it also hands the click to Stripe on its own:
21
+ * Payment Links (buy.stripe.com) get `client_reference_id`, and Stripe
22
+ * Pricing Table / Buy Button embeds get `client-reference-id`, including
23
+ * ones added to the page later. A brand whose Stripe account is connected
24
+ * to OpenPartner then needs no code for those checkouts. `stripe: false`
25
+ * turns it off.
26
+ *
27
+ * How long a referral lasts. Safari deletes storage that a page's own
28
+ * scripts wrote (localStorage, and cookies set through document.cookie)
29
+ * after 7 days without a visit, so a visitor who clicks, leaves for a
30
+ * fortnight and comes back to buy without signing up first arrives with
31
+ * nothing. A cookie the brand's own server sets on the landing response
32
+ * isn't capped that way: `referralCookieHeader` in `@openpartner/sdk/server`
33
+ * writes one, and this SDK reads it back (`op_ref`), on every subdomain it
34
+ * covers. Once identify() has run, the stitch lives server-side and no
35
+ * browser storage matters.
36
+ *
37
+ * Consent. `consent: 'pending'` holds the referral in memory until
38
+ * setConsent('granted'): nothing is read from or written to storage or
39
+ * cookies, no Stripe link is tagged and nothing is sent until then.
40
+ *
41
+ * Landing report. When a click first arrives from the URL the SDK tells
42
+ * OpenPartner (POST /attribution/landing), so Admin → Tracking health can
43
+ * count the clicks that never reached a page running it. `reportLanding:
44
+ * false` turns it off.
45
+ *
46
+ * No bundler? The same SDK ships as a script tag that starts itself
47
+ * (auto.ts), served at /op.js by an OpenPartner portal.
19
48
  */
20
49
 
50
+ interface StripeLinkingConfig {
51
+ /** Extra Payment Link hosts — a custom domain set up for Payment Links
52
+ * in Stripe (e.g. "pay.yourbrand.com"). buy.stripe.com is always on. */
53
+ linkHosts?: string[];
54
+ }
55
+ /** Whether this visitor agreed to referral tracking. */
56
+ type ConsentState = 'granted' | 'pending' | 'denied';
21
57
  interface OpenPartnerConfig {
22
58
  /** Base URL of your OpenPartner API, e.g. "https://openpartner.example.com". */
23
59
  apiUrl: string;
@@ -28,16 +64,68 @@ interface OpenPartnerConfig {
28
64
  /** localStorage key the captured cref is stashed under. Default `openpartner:cref`. */
29
65
  storageKey?: string;
30
66
  /**
31
- * Called when identify() fails. Default: log to console. Pass a noop
32
- * to suppress entirely or hook into your observability stack.
67
+ * Hand the captured referral to Stripe Payment Links and Pricing Table /
68
+ * Buy Button embeds on the page. Default on; `false` turns it off.
69
+ */
70
+ stripe?: boolean | StripeLinkingConfig;
71
+ /**
72
+ * Also keep the referral in a first-party cookie, so your server can read
73
+ * it straight off the request when it creates a Stripe Checkout Session
74
+ * (`client_reference_id: req.cookies.op_ref`). Off by default; the
75
+ * script-tag build turns it on. `domain` shares it across subdomains
76
+ * (".yourbrand.com") when visitors land on one and check out on another.
77
+ *
78
+ * The SDK reads this cookie back whether or not it writes it (`op_ref`
79
+ * unless `name` says otherwise): that's how a page on another subdomain,
80
+ * or a visit after Safari cleared localStorage, finds the referral, and
81
+ * how one your server set (`referralCookieHeader`) reaches identify().
82
+ */
83
+ referralCookie?: false | {
84
+ name?: string;
85
+ domain?: string;
86
+ days?: number;
87
+ };
88
+ /**
89
+ * Whether the visitor has agreed to referral tracking. Default
90
+ * `'granted'`: the SDK stores the referral as soon as it loads. Behind a
91
+ * consent banner, start at `'pending'` and call `setConsent()` from its
92
+ * callback. Until then the referral from the URL is held in memory only,
93
+ * so a visitor who agrees on the landing page is still credited.
94
+ * `'denied'` uses nothing.
95
+ */
96
+ consent?: ConsentState;
97
+ /**
98
+ * Tell OpenPartner when a partner click arrives on your site, so Admin →
99
+ * Tracking health can show clicks lost between the partner link and your
100
+ * page. Sends the click id once per click, nothing about the visitor.
101
+ * Default on; `false` turns it off.
102
+ */
103
+ reportLanding?: boolean;
104
+ /**
105
+ * Called when a call to OpenPartner fails: identify(), or a landing
106
+ * report OpenPartner answered with `click_not_found` (which usually means
107
+ * `apiUrl` names another program; other landing failures are dropped).
108
+ * Default: log to console. Pass a noop to suppress entirely or
109
+ * hook into your observability stack.
33
110
  */
34
111
  onError?: (err: unknown) => void;
35
112
  }
36
113
  declare class OpenPartner {
37
114
  private config;
115
+ private observer;
116
+ private consent;
117
+ /** The URL's referral while consent is pending, held in memory only. */
118
+ private held;
119
+ /** This page's referral, kept here too: where localStorage throws it is
120
+ * the only copy until the cookie is written. */
121
+ private current;
122
+ /** The Stripe links and embeds this SDK tagged, and with what, so a
123
+ * withdrawn consent can take the tags back off. */
124
+ private tagged;
38
125
  private constructor();
39
126
  static init(config: OpenPartnerConfig): OpenPartner;
40
- /** The cref we're holding onto, if any. Useful for debugging. */
127
+ /** The cref we're holding onto, if any. Useful for debugging. Null until
128
+ * consent is granted. */
41
129
  getCref(): string | null;
42
130
  /**
43
131
  * Captured referral token for this visitor. Pass to Stripe Checkout
@@ -49,28 +137,85 @@ declare class OpenPartner {
49
137
  * // ...
50
138
  * });
51
139
  *
52
- * Returns null if the visitor didn't arrive via a partner link.
140
+ * Returns null if the visitor didn't arrive via a partner link, or
141
+ * hasn't agreed to tracking yet.
53
142
  */
54
143
  getReferral(): string | null;
55
- /** Explicitly discard the captured cref. */
144
+ /** Explicitly discard the captured cref, the referral cookie, and the
145
+ * references this SDK added to Stripe links and embeds. */
56
146
  clear(): void;
57
147
  /**
58
- * Capture the cref from query string or cookie and stash in
59
- * localStorage. Runs on init so the value survives SPA navigations,
60
- * ITP cookie expiry, and subsequent page loads.
148
+ * Record the visitor's answer to your consent banner. `'granted'` stores
149
+ * the referral (the one held since the page loaded, or one already
150
+ * stored), starts tagging Stripe links and sends the landing report.
151
+ * `'denied'` stops all of it, takes the references it added off Stripe
152
+ * links and embeds, and deletes what was stored. Calling it again is
153
+ * safe.
154
+ */
155
+ setConsent(state: 'granted' | 'denied'): void;
156
+ /**
157
+ * Add the referral to every Stripe Payment Link and Pricing Table / Buy
158
+ * Button embed under `root` that doesn't carry a reference already.
159
+ * Runs on its own at init and for anything added later; call it yourself
160
+ * only if you build Stripe elements inside a shadow root. Returns how
161
+ * many it changed.
162
+ */
163
+ linkStripe(root?: ParentNode): number;
164
+ private remember;
165
+ /** Take back the references this SDK added. One the brand set, or one
166
+ * changed since, stays. */
167
+ private untagStripe;
168
+ /** Stop watching the page for new Stripe links and embeds. */
169
+ stopLinkingStripe(): void;
170
+ private stripeLinkHosts;
171
+ /** Everything the SDK does once it may: store, mirror, link, report. */
172
+ private start;
173
+ /**
174
+ * Link what's on the page now, then anything added or re-pointed later
175
+ * (SPAs, Stripe embeds mounted on demand). Only with a referral: a visitor
176
+ * who didn't come from a partner link costs nothing.
177
+ *
178
+ * Pricing Table and Buy Button embeds read client-reference-id when
179
+ * Stripe's script renders them, so the script tag belongs in <head>,
180
+ * ahead of the embed: the observer then sets the attribute as the
181
+ * element is parsed, before Stripe upgrades it.
182
+ */
183
+ private watchStripe;
184
+ /** The cref on the landing URL, if any. */
185
+ private queryCref;
186
+ /**
187
+ * Capture the cref from the URL (or the one held while consent was
188
+ * pending), else the router's cookie, else the referral cookie, and
189
+ * stash it in localStorage so it survives SPA navigations and later
190
+ * page loads. Returns the cref when it's a click arriving now, one this
191
+ * browser didn't have yet, for the landing report.
61
192
  */
62
193
  private captureCref;
194
+ private referralCookieName;
195
+ /**
196
+ * Mirror the referral into the brand's own first-party cookie (when
197
+ * configured). Left alone when it already holds this referral: in Safari
198
+ * rewriting it from script would trade a server-set cookie's full
199
+ * lifetime for the 7-day cap. A new click overwrites it.
200
+ */
201
+ private writeReferralCookie;
63
202
  private readCookie;
203
+ private storageGet;
204
+ private storageSet;
205
+ private storageRemove;
206
+ /** Tell OpenPartner the click arrived. Best-effort, once per click. */
207
+ private reportLanding;
64
208
  /**
65
209
  * Stitch the current captured click to an authenticated user. Call
66
210
  * this on login, signup, or anywhere the user becomes known.
67
211
  *
68
- * No-op if no cref was captured. Errors are reported through
69
- * `config.onError` by default — attribution is best-effort from the
70
- * browser — but you can await this and try/catch if you want to
71
- * surface failures explicitly.
212
+ * No-op if no cref was captured, or consent isn't granted. Errors are
213
+ * reported through `config.onError` by default — attribution is
214
+ * best-effort from the browser — but you can await this and try/catch
215
+ * if you want to surface failures explicitly.
72
216
  */
73
217
  identify(userId: string): Promise<IdentifyResponse | null>;
74
218
  }
75
219
 
76
- export { IdentifyResponse, OpenPartner, type OpenPartnerConfig };
220
+ export { IdentifyResponse, OpenPartner };
221
+ export type { ConsentState, OpenPartnerConfig, StripeLinkingConfig };
package/dist/browser.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { I as IdentifyResponse } from './shared-CLfBjPeb.js';
2
- export { O as OpenPartnerError } from './shared-CLfBjPeb.js';
1
+ import { I as IdentifyResponse } from './shared-dh1vlldf.js';
2
+ export { O as OpenPartnerError } from './shared-dh1vlldf.js';
3
3
 
4
4
  /**
5
5
  * Browser SDK — runs inside a merchant's web app. Captures the `cref`
6
6
  * attribution token from landing-page URLs and the first-party cookie
7
- * the click router sets, stashes it in localStorage so it survives
8
- * Safari / ITP cookie expiry and SPA navigations, and stitches to the
9
- * authenticated user on signup or login.
7
+ * the click router sets, stashes it in localStorage so it survives SPA
8
+ * navigations and later visits, and stitches to the authenticated user on
9
+ * signup or login.
10
10
  *
11
11
  * Typical integration:
12
12
  *
@@ -16,8 +16,44 @@ export { O as OpenPartnerError } from './shared-CLfBjPeb.js';
16
16
  *
17
17
  * // After login/signup, once you know who the user is:
18
18
  * await op.identify(user.id);
19
+ *
20
+ * With a captured referral it also hands the click to Stripe on its own:
21
+ * Payment Links (buy.stripe.com) get `client_reference_id`, and Stripe
22
+ * Pricing Table / Buy Button embeds get `client-reference-id`, including
23
+ * ones added to the page later. A brand whose Stripe account is connected
24
+ * to OpenPartner then needs no code for those checkouts. `stripe: false`
25
+ * turns it off.
26
+ *
27
+ * How long a referral lasts. Safari deletes storage that a page's own
28
+ * scripts wrote (localStorage, and cookies set through document.cookie)
29
+ * after 7 days without a visit, so a visitor who clicks, leaves for a
30
+ * fortnight and comes back to buy without signing up first arrives with
31
+ * nothing. A cookie the brand's own server sets on the landing response
32
+ * isn't capped that way: `referralCookieHeader` in `@openpartner/sdk/server`
33
+ * writes one, and this SDK reads it back (`op_ref`), on every subdomain it
34
+ * covers. Once identify() has run, the stitch lives server-side and no
35
+ * browser storage matters.
36
+ *
37
+ * Consent. `consent: 'pending'` holds the referral in memory until
38
+ * setConsent('granted'): nothing is read from or written to storage or
39
+ * cookies, no Stripe link is tagged and nothing is sent until then.
40
+ *
41
+ * Landing report. When a click first arrives from the URL the SDK tells
42
+ * OpenPartner (POST /attribution/landing), so Admin → Tracking health can
43
+ * count the clicks that never reached a page running it. `reportLanding:
44
+ * false` turns it off.
45
+ *
46
+ * No bundler? The same SDK ships as a script tag that starts itself
47
+ * (auto.ts), served at /op.js by an OpenPartner portal.
19
48
  */
20
49
 
50
+ interface StripeLinkingConfig {
51
+ /** Extra Payment Link hosts — a custom domain set up for Payment Links
52
+ * in Stripe (e.g. "pay.yourbrand.com"). buy.stripe.com is always on. */
53
+ linkHosts?: string[];
54
+ }
55
+ /** Whether this visitor agreed to referral tracking. */
56
+ type ConsentState = 'granted' | 'pending' | 'denied';
21
57
  interface OpenPartnerConfig {
22
58
  /** Base URL of your OpenPartner API, e.g. "https://openpartner.example.com". */
23
59
  apiUrl: string;
@@ -28,16 +64,68 @@ interface OpenPartnerConfig {
28
64
  /** localStorage key the captured cref is stashed under. Default `openpartner:cref`. */
29
65
  storageKey?: string;
30
66
  /**
31
- * Called when identify() fails. Default: log to console. Pass a noop
32
- * to suppress entirely or hook into your observability stack.
67
+ * Hand the captured referral to Stripe Payment Links and Pricing Table /
68
+ * Buy Button embeds on the page. Default on; `false` turns it off.
69
+ */
70
+ stripe?: boolean | StripeLinkingConfig;
71
+ /**
72
+ * Also keep the referral in a first-party cookie, so your server can read
73
+ * it straight off the request when it creates a Stripe Checkout Session
74
+ * (`client_reference_id: req.cookies.op_ref`). Off by default; the
75
+ * script-tag build turns it on. `domain` shares it across subdomains
76
+ * (".yourbrand.com") when visitors land on one and check out on another.
77
+ *
78
+ * The SDK reads this cookie back whether or not it writes it (`op_ref`
79
+ * unless `name` says otherwise): that's how a page on another subdomain,
80
+ * or a visit after Safari cleared localStorage, finds the referral, and
81
+ * how one your server set (`referralCookieHeader`) reaches identify().
82
+ */
83
+ referralCookie?: false | {
84
+ name?: string;
85
+ domain?: string;
86
+ days?: number;
87
+ };
88
+ /**
89
+ * Whether the visitor has agreed to referral tracking. Default
90
+ * `'granted'`: the SDK stores the referral as soon as it loads. Behind a
91
+ * consent banner, start at `'pending'` and call `setConsent()` from its
92
+ * callback. Until then the referral from the URL is held in memory only,
93
+ * so a visitor who agrees on the landing page is still credited.
94
+ * `'denied'` uses nothing.
95
+ */
96
+ consent?: ConsentState;
97
+ /**
98
+ * Tell OpenPartner when a partner click arrives on your site, so Admin →
99
+ * Tracking health can show clicks lost between the partner link and your
100
+ * page. Sends the click id once per click, nothing about the visitor.
101
+ * Default on; `false` turns it off.
102
+ */
103
+ reportLanding?: boolean;
104
+ /**
105
+ * Called when a call to OpenPartner fails: identify(), or a landing
106
+ * report OpenPartner answered with `click_not_found` (which usually means
107
+ * `apiUrl` names another program; other landing failures are dropped).
108
+ * Default: log to console. Pass a noop to suppress entirely or
109
+ * hook into your observability stack.
33
110
  */
34
111
  onError?: (err: unknown) => void;
35
112
  }
36
113
  declare class OpenPartner {
37
114
  private config;
115
+ private observer;
116
+ private consent;
117
+ /** The URL's referral while consent is pending, held in memory only. */
118
+ private held;
119
+ /** This page's referral, kept here too: where localStorage throws it is
120
+ * the only copy until the cookie is written. */
121
+ private current;
122
+ /** The Stripe links and embeds this SDK tagged, and with what, so a
123
+ * withdrawn consent can take the tags back off. */
124
+ private tagged;
38
125
  private constructor();
39
126
  static init(config: OpenPartnerConfig): OpenPartner;
40
- /** The cref we're holding onto, if any. Useful for debugging. */
127
+ /** The cref we're holding onto, if any. Useful for debugging. Null until
128
+ * consent is granted. */
41
129
  getCref(): string | null;
42
130
  /**
43
131
  * Captured referral token for this visitor. Pass to Stripe Checkout
@@ -49,28 +137,85 @@ declare class OpenPartner {
49
137
  * // ...
50
138
  * });
51
139
  *
52
- * Returns null if the visitor didn't arrive via a partner link.
140
+ * Returns null if the visitor didn't arrive via a partner link, or
141
+ * hasn't agreed to tracking yet.
53
142
  */
54
143
  getReferral(): string | null;
55
- /** Explicitly discard the captured cref. */
144
+ /** Explicitly discard the captured cref, the referral cookie, and the
145
+ * references this SDK added to Stripe links and embeds. */
56
146
  clear(): void;
57
147
  /**
58
- * Capture the cref from query string or cookie and stash in
59
- * localStorage. Runs on init so the value survives SPA navigations,
60
- * ITP cookie expiry, and subsequent page loads.
148
+ * Record the visitor's answer to your consent banner. `'granted'` stores
149
+ * the referral (the one held since the page loaded, or one already
150
+ * stored), starts tagging Stripe links and sends the landing report.
151
+ * `'denied'` stops all of it, takes the references it added off Stripe
152
+ * links and embeds, and deletes what was stored. Calling it again is
153
+ * safe.
154
+ */
155
+ setConsent(state: 'granted' | 'denied'): void;
156
+ /**
157
+ * Add the referral to every Stripe Payment Link and Pricing Table / Buy
158
+ * Button embed under `root` that doesn't carry a reference already.
159
+ * Runs on its own at init and for anything added later; call it yourself
160
+ * only if you build Stripe elements inside a shadow root. Returns how
161
+ * many it changed.
162
+ */
163
+ linkStripe(root?: ParentNode): number;
164
+ private remember;
165
+ /** Take back the references this SDK added. One the brand set, or one
166
+ * changed since, stays. */
167
+ private untagStripe;
168
+ /** Stop watching the page for new Stripe links and embeds. */
169
+ stopLinkingStripe(): void;
170
+ private stripeLinkHosts;
171
+ /** Everything the SDK does once it may: store, mirror, link, report. */
172
+ private start;
173
+ /**
174
+ * Link what's on the page now, then anything added or re-pointed later
175
+ * (SPAs, Stripe embeds mounted on demand). Only with a referral: a visitor
176
+ * who didn't come from a partner link costs nothing.
177
+ *
178
+ * Pricing Table and Buy Button embeds read client-reference-id when
179
+ * Stripe's script renders them, so the script tag belongs in <head>,
180
+ * ahead of the embed: the observer then sets the attribute as the
181
+ * element is parsed, before Stripe upgrades it.
182
+ */
183
+ private watchStripe;
184
+ /** The cref on the landing URL, if any. */
185
+ private queryCref;
186
+ /**
187
+ * Capture the cref from the URL (or the one held while consent was
188
+ * pending), else the router's cookie, else the referral cookie, and
189
+ * stash it in localStorage so it survives SPA navigations and later
190
+ * page loads. Returns the cref when it's a click arriving now, one this
191
+ * browser didn't have yet, for the landing report.
61
192
  */
62
193
  private captureCref;
194
+ private referralCookieName;
195
+ /**
196
+ * Mirror the referral into the brand's own first-party cookie (when
197
+ * configured). Left alone when it already holds this referral: in Safari
198
+ * rewriting it from script would trade a server-set cookie's full
199
+ * lifetime for the 7-day cap. A new click overwrites it.
200
+ */
201
+ private writeReferralCookie;
63
202
  private readCookie;
203
+ private storageGet;
204
+ private storageSet;
205
+ private storageRemove;
206
+ /** Tell OpenPartner the click arrived. Best-effort, once per click. */
207
+ private reportLanding;
64
208
  /**
65
209
  * Stitch the current captured click to an authenticated user. Call
66
210
  * this on login, signup, or anywhere the user becomes known.
67
211
  *
68
- * No-op if no cref was captured. Errors are reported through
69
- * `config.onError` by default — attribution is best-effort from the
70
- * browser — but you can await this and try/catch if you want to
71
- * surface failures explicitly.
212
+ * No-op if no cref was captured, or consent isn't granted. Errors are
213
+ * reported through `config.onError` by default — attribution is
214
+ * best-effort from the browser — but you can await this and try/catch
215
+ * if you want to surface failures explicitly.
72
216
  */
73
217
  identify(userId: string): Promise<IdentifyResponse | null>;
74
218
  }
75
219
 
76
- export { IdentifyResponse, OpenPartner, type OpenPartnerConfig };
220
+ export { IdentifyResponse, OpenPartner };
221
+ export type { ConsentState, OpenPartnerConfig, StripeLinkingConfig };