@openpartner/sdk 0.2.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,79 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+
5
+ Closes the places where a partner click could go missing in the browser, and
6
+ reports clicks that arrive so Admin → Tracking health can show the ones that
7
+ don't. No breaking API changes; the one new default behaviour is the landing
8
+ report (see Compatibility).
9
+
10
+ ### Added
11
+
12
+ - **Consent mode.** `consent: 'granted' | 'pending' | 'denied'` in
13
+ `OpenPartner.init()` (script tag: `data-consent="pending"`) and
14
+ `setConsent('granted' | 'denied')`.
15
+ - `'pending'` holds the click from the URL in memory only. Nothing is read
16
+ from or written to storage or cookies, no Stripe link is tagged and no
17
+ request is sent until `setConsent('granted')`, which then works even
18
+ after a single-page app has moved off the landing URL.
19
+ - `'denied'`, at load or later, deletes what an earlier visit stored and
20
+ takes the `client_reference_id` the SDK added back off Stripe Payment
21
+ Links and embeds. A reference you set yourself stays. Safe to call more
22
+ than once.
23
+ - The default is `'granted'`: integrations that don't set it behave as
24
+ before.
25
+ - **Landing report.** When a click first arrives from the URL, the SDK posts
26
+ its id once to `POST /attribution/landing` (the page's hostname comes from
27
+ the browser's `Origin` header; nothing else about the visitor is sent).
28
+ Admin → Tracking health compares landings with clicks to show the clicks
29
+ lost between the partner link and your page. Turn it off with
30
+ `reportLanding: false` (script tag: `data-landing="off"`).
31
+ - **Server-side referral cookie** in `@openpartner/sdk/server`, for Safari,
32
+ which deletes storage a page's scripts wrote after 7 days without a visit.
33
+ A cookie your own server sets on the landing response isn't capped that way,
34
+ and the browser SDK reads it back.
35
+ - `referralCookieHeader(url, options?)`: the `Set-Cookie` value for a
36
+ request whose URL carries `?cref=`, or `null`.
37
+ - `referralCookieMiddleware(options?)`: Express / Connect middleware that
38
+ sets it, adding to any `Set-Cookie` already queued.
39
+ - `readReferralCookie(cookieHeader, name?)`: the referral off a `Cookie`
40
+ header, for `identify()` from your backend or `client_reference_id` on a
41
+ Checkout Session.
42
+
43
+ ### Changed
44
+
45
+ - **The referral is read back from the `op_ref` cookie** (or
46
+ `referralCookie.name`) when localStorage is empty: on a second subdomain
47
+ (land on `www.`, sign up on `app.` with `referralCookie.domain` /
48
+ `data-cookie-domain` set), after Safari clears localStorage, or where
49
+ localStorage is blocked. Previously only localStorage and the router's
50
+ `_cref` cookie were read.
51
+ - **An `op_ref` cookie that already holds the referral isn't rewritten** from
52
+ script, so one your server set keeps its lifetime in Safari. A new click
53
+ still replaces it.
54
+ - **A click from the URL is kept in memory as well,** so the referral cookie,
55
+ Stripe tagging and `identify()` still get it where localStorage throws.
56
+ - **`clear()` also expires the referral cookie** and removes the Stripe
57
+ references the SDK added.
58
+ - **`onError`'s default log line** is now `[openpartner] request failed` (was
59
+ `identify failed`): it also covers the landing report.
60
+
61
+ ### Compatibility
62
+
63
+ - The landing report is on by default and needs an OpenPartner API that has
64
+ `POST /attribution/landing` (OpenPartner Cloud, or a self-hosted instance
65
+ from October 24 2026 on). An older API answers 404, and the SDK ignores it:
66
+ only a `click_not_found` from a current API reaches `onError`, because that
67
+ usually means `apiUrl` names another program.
68
+ - The script tag an OpenPartner portal serves at `/op.js` is built from this
69
+ source and already behaves this way.
70
+
71
+ ## 0.2.0
72
+
73
+ Published to npm on October 20 2026: Stripe Payment Link and Pricing Table /
74
+ Buy Button linking, the script-tag build, the `op_ref` referral cookie, and the
75
+ server SDK (`trackEvent`, `trackConversion`, `identify`).
76
+
77
+ ## 0.1.0
78
+
79
+ The first published version.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Client + server SDK for [OpenPartner](https://github.com/getcoherence/openpartner).
4
4
 
5
- - **Browser** — captures the `cref` attribution token from landing URLs and the first-party cookie, survives Safari / ITP cookie expiry via localStorage, and stitches to a logged-in user on auth.
5
+ - **Browser** — captures the `cref` attribution token from landing URLs and first-party cookies, keeps it across pages and subdomains, waits for consent when you ask it to, and stitches to a logged-in user on auth.
6
6
  - **Server** — posts conversion events, stitches server-side where needed, with typed errors.
7
7
 
8
8
  Zero runtime dependencies. ~4 KB gzipped browser bundle.
@@ -13,7 +13,7 @@ npm install @openpartner/sdk
13
13
 
14
14
  ## Browser
15
15
 
16
- Embed once on every page of your marketing site and product. The SDK runs on init, pulling the cref out of `?cref=…` (first visit from a partner link) or the `_cref` cookie (subsequent pages) and stashing it in localStorage.
16
+ Embed once on every page of your marketing site and product. The SDK runs on init, pulling the cref out of `?cref=…` (first visit from a partner link), or on later pages out of localStorage or a first-party cookie (`_cref` from the click router, `op_ref` from a sibling subdomain or your server), and stashing it in localStorage.
17
17
 
18
18
  ```ts
19
19
  import { OpenPartner } from '@openpartner/sdk';
@@ -40,9 +40,55 @@ OpenPartner.init({
40
40
  onError: (err) => Sentry.captureException(err),
41
41
  stripe: { linkHosts: ['pay.yourbrand.com'] }, // or false — see below
42
42
  referralCookie: { name: 'op_ref', domain: '.yourbrand.com' }, // off by default
43
+ consent: 'pending', // default 'granted' — see Consent below
44
+ reportLanding: true, // default on — see Landing report below
43
45
  });
44
46
  ```
45
47
 
48
+ ### Subdomains
49
+
50
+ A visitor who lands on `www.yourbrand.com` and signs up on `app.yourbrand.com` has two localStorages, one per subdomain. Share the referral through a cookie on the parent domain: `referralCookie: { domain: '.yourbrand.com' }` (script tag: `data-cookie-domain=".yourbrand.com"`) on every subdomain. The SDK on `app.` reads `op_ref` back when its own storage is empty, so `identify()` and Stripe links work there too. A journey across two different domains (`yourbrand.com` → `yourbrand-shop.com`) has no shared cookie: carry `?cref=` on the link between them.
51
+
52
+ ### Safari and long gaps
53
+
54
+ Safari deletes storage a page's scripts wrote (localStorage, and cookies set through `document.cookie`) after 7 days without a visit. A visitor who clicks, comes back after a fortnight and buys without signing up first would arrive with nothing. Two things close that gap:
55
+
56
+ - **Call `identify()` as early as you can.** Once a click is stitched to a user, the link lives on OpenPartner's side and no browser storage matters. An email capture or account creation before checkout is enough.
57
+ - **Set the referral cookie from your own server**, on the landing page's response. A cookie your server sets keeps its full lifetime in Safari, and the browser SDK reads it back:
58
+
59
+ ```ts
60
+ import { referralCookieHeader, referralCookieMiddleware } from '@openpartner/sdk/server';
61
+
62
+ // Express / Connect:
63
+ app.use(referralCookieMiddleware({ domain: '.yourbrand.com' }));
64
+
65
+ // Anything else (Next.js middleware, a Worker): set the header yourself.
66
+ const header = referralCookieHeader(request.url, { domain: '.yourbrand.com' });
67
+ if (header) response.headers.append('Set-Cookie', header);
68
+ ```
69
+
70
+ Set it from the server that serves the page. Safari caps a cookie at 7 days when it comes from a server on a different IP address than the site, such as an API hosted on another provider. The SDK never rewrites an `op_ref` cookie that already holds the referral, because rewriting it from script would bring the 7-day cap back.
71
+
72
+ ### Consent
73
+
74
+ Behind a consent banner, start the SDK with `consent: 'pending'` and tell it the answer:
75
+
76
+ ```ts
77
+ const op = OpenPartner.init({ apiUrl, consent: 'pending' });
78
+
79
+ yourConsentBanner.onChange((choices) => {
80
+ op.setConsent(choices.marketing ? 'granted' : 'denied');
81
+ });
82
+ ```
83
+
84
+ While consent is pending, the SDK keeps the click from the URL in memory and does nothing else: it doesn't read or write storage or cookies, tag Stripe links, or call OpenPartner. `setConsent('granted')` then works as if the page had just loaded, even after the visitor has navigated within a single-page app. `setConsent('denied')` stops everything, takes the references the SDK added back off Stripe links and embeds (one you set yourself stays), and deletes what was stored. Starting with `consent: 'denied'` deletes what an earlier visit stored. `getReferral()` and `identify()` return `null` until consent is granted. Script tag: `data-consent="pending"`, then `openpartner.setConsent('granted')`.
85
+
86
+ Clicks from visitors who never consent can't be credited, and Admin → Tracking health counts them among the clicks that never landed.
87
+
88
+ ### Landing report
89
+
90
+ When a partner click first arrives from the URL, the SDK tells OpenPartner (`POST /attribution/landing`, the click id and nothing else; the page's hostname comes from the browser's `Origin` header). **Admin → Tracking health** compares landings with clicks to show the ones lost between the partner link and your page: a redirect that dropped `?cref=`, a page without the script, a consent banner. A `click_not_found` from it reaches `onError` and usually means `apiUrl` names another program; any other failure is dropped, because the report is a diagnostic and never part of attribution. Turn it off with `reportLanding: false` (script tag: `data-landing="off"`).
91
+
46
92
  ### Stripe links and embeds
47
93
 
48
94
  When the visitor came from a partner link, the SDK hands that click to Stripe for you:
@@ -62,10 +108,11 @@ An OpenPartner portal serves a self-starting build of this SDK at `/op.js`. Past
62
108
  <script src="https://app.openpartner.dev/op.js" data-api-url="https://app.openpartner.dev/api/t/your-slug"></script>
63
109
  ```
64
110
 
65
- **Admin → Integrations** shows the exact tag for your program. It does everything above, keeps the referral in an `op_ref` cookie on your domain, and exposes `window.openpartner` (`openpartner.identify(userId)`, `openpartner.getReferral()`). Optional attributes:
111
+ **Admin → Integrations** shows the exact tag for your program. It does everything above, keeps the referral in an `op_ref` cookie on your domain, and exposes `window.openpartner` (`openpartner.identify(userId)`, `openpartner.getReferral()`, `openpartner.setConsent('granted')`). Optional attributes:
66
112
  - `data-stripe-hosts="pay.yourbrand.com"`: a custom Payment Link domain.
67
113
  - `data-cookie-domain=".yourbrand.com"`: share `op_ref` across subdomains.
68
- - `data-stripe="off"`, `data-cookie="off"`: turn either off.
114
+ - `data-consent="pending"`: wait for `openpartner.setConsent()` (see [Consent](#consent)).
115
+ - `data-stripe="off"`, `data-cookie="off"`, `data-landing="off"`: turn any of them off.
69
116
 
70
117
  The npm package carries the same file at `dist-global/openpartner.js`.
71
118
 
@@ -166,7 +213,7 @@ OpenPartner has to know which Stripe customer is which referred user. Link them
166
213
  });
167
214
  ```
168
215
 
169
- With the [script tag](#script-tag-no-bundler) (or `referralCookie` set), your backend can read the referral straight off the request instead: `client_reference_id: req.cookies.op_ref`.
216
+ With the [script tag](#script-tag-no-bundler) (or `referralCookie` set), your backend can read the referral straight off the request instead: `client_reference_id: req.cookies.op_ref`, or `readReferralCookie(req.headers.cookie)` from `@openpartner/sdk/server` without a cookie parser. The same call lets your backend stitch at signup, which works even where browser scripts are blocked: `await op.identify({ cref, userId: user.id })`.
170
217
 
171
218
  A Stripe Payment Link can't run your code, so let the partner link fill it in: set the program's destination URL to the Payment Link with `?client_reference_id={click_id}`, and the click router puts the click id there.
172
219
 
package/dist/browser.cjs CHANGED
@@ -75,28 +75,42 @@ var DEFAULTS = {
75
75
  storageKey: "openpartner:cref",
76
76
  stripe: true,
77
77
  referralCookie: false,
78
+ consent: "granted",
79
+ reportLanding: true,
78
80
  onError: (err) => {
79
- console.warn("[openpartner] identify failed", err);
81
+ console.warn("[openpartner] request failed", err);
80
82
  }
81
83
  };
82
84
  var STRIPE_REFERENCE_RE = /^[A-Za-z0-9_-]{1,200}$/;
83
85
  var STRIPE_EMBED_SELECTOR = "stripe-pricing-table, stripe-buy-button";
86
+ var REFERRAL_COOKIE_DEFAULT = "op_ref";
84
87
  var OpenPartner = class _OpenPartner {
85
88
  constructor(config) {
86
89
  __publicField(this, "config");
87
90
  __publicField(this, "observer", null);
91
+ __publicField(this, "consent");
92
+ /** The URL's referral while consent is pending, held in memory only. */
93
+ __publicField(this, "held", null);
94
+ /** This page's referral, kept here too: where localStorage throws it is
95
+ * the only copy until the cookie is written. */
96
+ __publicField(this, "current", null);
97
+ /** The Stripe links and embeds this SDK tagged, and with what, so a
98
+ * withdrawn consent can take the tags back off. */
99
+ __publicField(this, "tagged", []);
88
100
  this.config = { ...DEFAULTS, ...config };
89
- this.captureCref();
90
- this.writeReferralCookie();
91
- if (this.config.stripe !== false) this.watchStripe();
101
+ this.consent = this.config.consent;
102
+ if (this.consent === "granted") this.start();
103
+ else if (this.consent === "pending") this.held = this.queryCref();
104
+ else this.clear();
92
105
  }
93
106
  static init(config) {
94
107
  return new _OpenPartner(config);
95
108
  }
96
- /** The cref we're holding onto, if any. Useful for debugging. */
109
+ /** The cref we're holding onto, if any. Useful for debugging. Null until
110
+ * consent is granted. */
97
111
  getCref() {
98
- if (typeof localStorage === "undefined") return null;
99
- return localStorage.getItem(this.config.storageKey);
112
+ if (this.consent !== "granted") return null;
113
+ return this.storageGet() ?? this.current ?? this.readCookie(this.referralCookieName());
100
114
  }
101
115
  /**
102
116
  * Captured referral token for this visitor. Pass to Stripe Checkout
@@ -108,15 +122,44 @@ var OpenPartner = class _OpenPartner {
108
122
  * // ...
109
123
  * });
110
124
  *
111
- * Returns null if the visitor didn't arrive via a partner link.
125
+ * Returns null if the visitor didn't arrive via a partner link, or
126
+ * hasn't agreed to tracking yet.
112
127
  */
113
128
  getReferral() {
114
129
  return this.getCref();
115
130
  }
116
- /** Explicitly discard the captured cref. */
131
+ /** Explicitly discard the captured cref, the referral cookie, and the
132
+ * references this SDK added to Stripe links and embeds. */
117
133
  clear() {
118
- if (typeof localStorage === "undefined") return;
119
- localStorage.removeItem(this.config.storageKey);
134
+ this.held = null;
135
+ this.current = null;
136
+ this.untagStripe();
137
+ this.storageRemove();
138
+ if (typeof document === "undefined") return;
139
+ const name = this.referralCookieName();
140
+ if (!this.readCookie(name)) return;
141
+ const cfg = this.config.referralCookie;
142
+ document.cookie = `${name}=; Max-Age=0; Path=/`;
143
+ if (cfg && cfg.domain) document.cookie = `${name}=; Max-Age=0; Path=/; Domain=${cfg.domain}`;
144
+ }
145
+ /**
146
+ * Record the visitor's answer to your consent banner. `'granted'` stores
147
+ * the referral (the one held since the page loaded, or one already
148
+ * stored), starts tagging Stripe links and sends the landing report.
149
+ * `'denied'` stops all of it, takes the references it added off Stripe
150
+ * links and embeds, and deletes what was stored. Calling it again is
151
+ * safe.
152
+ */
153
+ setConsent(state) {
154
+ if (state === "granted") {
155
+ if (this.consent === "granted") return;
156
+ this.consent = state;
157
+ this.start();
158
+ return;
159
+ }
160
+ this.consent = state;
161
+ this.stopLinkingStripe();
162
+ this.clear();
120
163
  }
121
164
  /**
122
165
  * Add the referral to every Stripe Payment Link and Pricing Table / Buy
@@ -143,6 +186,7 @@ var OpenPartner = class _OpenPartner {
143
186
  if (!hosts.has(url.hostname.toLowerCase()) || url.searchParams.has("client_reference_id")) continue;
144
187
  url.searchParams.set("client_reference_id", referral);
145
188
  a.href = url.toString();
189
+ this.remember(a, referral);
146
190
  changed += 1;
147
191
  }
148
192
  const embeds = [];
@@ -151,10 +195,35 @@ var OpenPartner = class _OpenPartner {
151
195
  for (const el of embeds) {
152
196
  if (el.hasAttribute("client-reference-id")) continue;
153
197
  el.setAttribute("client-reference-id", referral);
198
+ this.remember(el, referral);
154
199
  changed += 1;
155
200
  }
156
201
  return changed;
157
202
  }
203
+ remember(el, value) {
204
+ this.tagged = this.tagged.filter((t) => t.el.isConnected);
205
+ this.tagged.push({ el, value });
206
+ }
207
+ /** Take back the references this SDK added. One the brand set, or one
208
+ * changed since, stays. */
209
+ untagStripe() {
210
+ for (const { el, value } of this.tagged) {
211
+ if (el instanceof HTMLAnchorElement) {
212
+ let url;
213
+ try {
214
+ url = new URL(el.href);
215
+ } catch {
216
+ continue;
217
+ }
218
+ if (url.searchParams.get("client_reference_id") !== value) continue;
219
+ url.searchParams.delete("client_reference_id");
220
+ el.href = url.toString();
221
+ } else if (el.getAttribute("client-reference-id") === value) {
222
+ el.removeAttribute("client-reference-id");
223
+ }
224
+ }
225
+ this.tagged = [];
226
+ }
158
227
  /** Stop watching the page for new Stripe links and embeds. */
159
228
  stopLinkingStripe() {
160
229
  this.observer?.disconnect();
@@ -164,6 +233,14 @@ var OpenPartner = class _OpenPartner {
164
233
  const cfg = this.config.stripe;
165
234
  return typeof cfg === "object" && cfg.linkHosts ? cfg.linkHosts.map((h) => h.trim().toLowerCase()).filter(Boolean) : [];
166
235
  }
236
+ /** Everything the SDK does once it may: store, mirror, link, report. */
237
+ start() {
238
+ const landed = this.captureCref(this.held);
239
+ this.held = null;
240
+ this.writeReferralCookie();
241
+ if (this.config.stripe !== false) this.watchStripe();
242
+ if (landed && this.config.reportLanding !== false) void this.reportLanding(landed);
243
+ }
167
244
  /**
168
245
  * Link what's on the page now, then anything added or re-pointed later
169
246
  * (SPAs, Stripe embeds mounted on demand). Only with a referral: a visitor
@@ -175,7 +252,7 @@ var OpenPartner = class _OpenPartner {
175
252
  * element is parsed, before Stripe upgrades it.
176
253
  */
177
254
  watchStripe() {
178
- if (typeof document === "undefined" || !this.getReferral()) return;
255
+ if (typeof document === "undefined" || this.observer || !this.getReferral()) return;
179
256
  this.linkStripe(document);
180
257
  if (typeof MutationObserver === "undefined") return;
181
258
  this.observer = new MutationObserver((records) => {
@@ -196,32 +273,52 @@ var OpenPartner = class _OpenPartner {
196
273
  attributeFilter: ["href"]
197
274
  });
198
275
  }
276
+ /** The cref on the landing URL, if any. */
277
+ queryCref() {
278
+ if (typeof window === "undefined") return null;
279
+ return new URL(window.location.href).searchParams.get(this.config.queryParam);
280
+ }
199
281
  /**
200
- * Capture the cref from query string or cookie and stash in
201
- * localStorage. Runs on init so the value survives SPA navigations,
202
- * ITP cookie expiry, and subsequent page loads.
282
+ * Capture the cref from the URL (or the one held while consent was
283
+ * pending), else the router's cookie, else the referral cookie, and
284
+ * stash it in localStorage so it survives SPA navigations and later
285
+ * page loads. Returns the cref when it's a click arriving now, one this
286
+ * browser didn't have yet, for the landing report.
203
287
  */
204
- captureCref() {
205
- if (typeof window === "undefined" || typeof localStorage === "undefined") return;
206
- const url = new URL(window.location.href);
207
- const queryCref = url.searchParams.get(this.config.queryParam);
208
- if (queryCref) {
209
- localStorage.setItem(this.config.storageKey, queryCref);
210
- return;
288
+ captureCref(held) {
289
+ if (typeof window === "undefined") return null;
290
+ const incoming = held ?? this.queryCref();
291
+ if (incoming) {
292
+ const isNew = this.storageGet() !== incoming;
293
+ this.current = incoming;
294
+ this.storageSet(incoming);
295
+ return isNew ? incoming : null;
211
296
  }
212
- const cookieCref = this.readCookie(this.config.cookieName);
213
- if (cookieCref && !localStorage.getItem(this.config.storageKey)) {
214
- localStorage.setItem(this.config.storageKey, cookieCref);
297
+ if (this.storageGet()) return null;
298
+ const restored = this.readCookie(this.config.cookieName) ?? this.readCookie(this.referralCookieName());
299
+ if (restored) {
300
+ this.current = restored;
301
+ this.storageSet(restored);
215
302
  }
303
+ return null;
304
+ }
305
+ referralCookieName() {
306
+ const cfg = this.config.referralCookie;
307
+ return cfg && cfg.name || REFERRAL_COOKIE_DEFAULT;
216
308
  }
217
- /** Mirror the referral into the brand's own first-party cookie (when
218
- * configured), refreshed on every page load that has one. */
309
+ /**
310
+ * Mirror the referral into the brand's own first-party cookie (when
311
+ * configured). Left alone when it already holds this referral: in Safari
312
+ * rewriting it from script would trade a server-set cookie's full
313
+ * lifetime for the 7-day cap. A new click overwrites it.
314
+ */
219
315
  writeReferralCookie() {
220
316
  const cfg = this.config.referralCookie;
221
317
  if (!cfg || typeof document === "undefined") return;
222
318
  const referral = this.getReferral();
223
319
  if (!referral || !STRIPE_REFERENCE_RE.test(referral)) return;
224
- const name = cfg.name ?? "op_ref";
320
+ const name = this.referralCookieName();
321
+ if (this.readCookie(name) === referral) return;
225
322
  const maxAge = Math.round((cfg.days ?? 90) * 86400);
226
323
  const secure = typeof location !== "undefined" && location.protocol === "https:" ? "; Secure" : "";
227
324
  const domain = cfg.domain ? `; Domain=${cfg.domain}` : "";
@@ -230,16 +327,55 @@ var OpenPartner = class _OpenPartner {
230
327
  readCookie(name) {
231
328
  if (typeof document === "undefined") return null;
232
329
  const match = document.cookie.match(new RegExp(`(^|; )${name}=([^;]+)`));
233
- return match ? decodeURIComponent(match[2]) : null;
330
+ if (!match) return null;
331
+ try {
332
+ return decodeURIComponent(match[2]);
333
+ } catch {
334
+ return null;
335
+ }
336
+ }
337
+ // localStorage throws in some sandboxed iframes and with site data
338
+ // blocked; the referral then lives in the cookie alone.
339
+ storageGet() {
340
+ try {
341
+ return typeof localStorage === "undefined" ? null : localStorage.getItem(this.config.storageKey);
342
+ } catch {
343
+ return null;
344
+ }
345
+ }
346
+ storageSet(value) {
347
+ try {
348
+ if (typeof localStorage !== "undefined") localStorage.setItem(this.config.storageKey, value);
349
+ } catch {
350
+ }
351
+ }
352
+ storageRemove() {
353
+ try {
354
+ if (typeof localStorage !== "undefined") localStorage.removeItem(this.config.storageKey);
355
+ } catch {
356
+ }
357
+ }
358
+ /** Tell OpenPartner the click arrived. Best-effort, once per click. */
359
+ async reportLanding(cref) {
360
+ if (!STRIPE_REFERENCE_RE.test(cref) || typeof fetch === "undefined") return;
361
+ try {
362
+ await requestJson(`${this.config.apiUrl}/attribution/landing`, {
363
+ method: "POST",
364
+ body: { cref },
365
+ keepalive: true
366
+ });
367
+ } catch (err) {
368
+ if (err instanceof OpenPartnerError && err.code === "click_not_found") this.config.onError(err);
369
+ }
234
370
  }
235
371
  /**
236
372
  * Stitch the current captured click to an authenticated user. Call
237
373
  * this on login, signup, or anywhere the user becomes known.
238
374
  *
239
- * No-op if no cref was captured. Errors are reported through
240
- * `config.onError` by default — attribution is best-effort from the
241
- * browser — but you can await this and try/catch if you want to
242
- * surface failures explicitly.
375
+ * No-op if no cref was captured, or consent isn't granted. Errors are
376
+ * reported through `config.onError` by default — attribution is
377
+ * best-effort from the browser — but you can await this and try/catch
378
+ * if you want to surface failures explicitly.
243
379
  */
244
380
  async identify(userId) {
245
381
  const cref = this.getCref();
@@ -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 * 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 * 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\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 referralCookie?: false | { name?: string; domain?: string; days?: number };\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 stripe: true,\n referralCookie: false,\n onError: (err: unknown) => {\n console.warn('[openpartner] identify 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';\n\nexport class OpenPartner {\n private config: Required<OpenPartnerConfig>;\n private observer: MutationObserver | null = null;\n\n private constructor(config: OpenPartnerConfig) {\n this.config = { ...DEFAULTS, ...config } as Required<OpenPartnerConfig>;\n this.captureCref();\n this.writeReferralCookie();\n if (this.config.stripe !== false) this.watchStripe();\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 * 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 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 changed += 1;\n }\n return changed;\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 /**\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.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 /**\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 /** Mirror the referral into the brand's own first-party cookie (when\n * configured), refreshed on every page load that has one. */\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 = cfg.name ?? 'op_ref';\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 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 /**\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
+ {"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":[]}