@openpartner/sdk 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenPartner contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # @openpartner/sdk
2
+
3
+ Client + server SDK for [OpenPartner](https://github.com/getcoherence/openpartner).
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.
6
+ - **Server** — posts conversion events, stitches server-side where needed, with typed errors.
7
+
8
+ Zero runtime dependencies. ~4 KB gzipped browser bundle.
9
+
10
+ ```bash
11
+ npm install @openpartner/sdk
12
+ ```
13
+
14
+ ## Browser
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.
17
+
18
+ ```ts
19
+ import { OpenPartner } from '@openpartner/sdk';
20
+
21
+ const op = OpenPartner.init({
22
+ apiUrl: 'https://openpartner.example.com',
23
+ });
24
+
25
+ // On signup or login — any time you learn the user's id:
26
+ await op.identify(currentUser.id);
27
+ ```
28
+
29
+ That's the whole browser integration. The `identify()` call POSTs `{ cref, userId }` to `/attribution/identify` on your OpenPartner API. Errors are logged to the console by default — pass `onError` to hook in observability, or `await` the call and handle errors yourself.
30
+
31
+ ### Config
32
+
33
+ ```ts
34
+ OpenPartner.init({
35
+ apiUrl: 'https://openpartner.example.com',
36
+ // Optional:
37
+ cookieName: '_cref', // first-party cookie set by the click router
38
+ queryParam: 'cref', // query param on landing URLs
39
+ storageKey: 'openpartner:cref', // localStorage key
40
+ onError: (err) => Sentry.captureException(err),
41
+ });
42
+ ```
43
+
44
+ ### Framework notes
45
+
46
+ - **React / Vue / SolidJS**: call `OpenPartner.init()` once at the top of your app. Call `op.identify()` from your auth callback.
47
+ - **Next.js App Router**: put `OpenPartner.init()` in a `'use client'` provider component mounted in your root layout.
48
+ - **SPA with route-based auth**: `captureCref()` runs on init, so you only need one init per page load. Subsequent SPA navigations don't need to re-init.
49
+
50
+ ## Server
51
+
52
+ Use the server SDK in your backend — Node 18+, Bun, Deno, Cloudflare Workers (native fetch).
53
+
54
+ ```ts
55
+ import { OpenPartnerServer } from '@openpartner/sdk/server';
56
+
57
+ const op = new OpenPartnerServer({
58
+ apiUrl: process.env.OPENPARTNER_API_URL!,
59
+ apiKey: process.env.OPENPARTNER_API_KEY!,
60
+ });
61
+
62
+ // On signup (no revenue yet — still useful for funnel analysis):
63
+ await op.trackEvent({ userId: user.id, type: 'signup' });
64
+
65
+ // On first paid invoice:
66
+ await op.trackEvent({
67
+ userId: user.id,
68
+ type: 'invoice_paid',
69
+ value: 249,
70
+ currency: 'USD',
71
+ metadata: { stripeInvoiceId: invoice.id },
72
+ });
73
+ ```
74
+
75
+ The response includes the attribution result:
76
+
77
+ ```ts
78
+ {
79
+ ok: true,
80
+ eventId: '01K…',
81
+ attribution: {
82
+ status: 'attributed', // or 'no_identity' / 'no_click' / 'outside_window'
83
+ model: 'last_click',
84
+ touches: [
85
+ { clickId, partnerId, weight: 1, attributionId, commissionId }
86
+ ],
87
+ }
88
+ }
89
+ ```
90
+
91
+ For multi-touch campaigns (`linear`, `position`, etc.) `touches` will contain one entry per attributed click with a fractional `weight`.
92
+
93
+ ### Stripe webhooks
94
+
95
+ OpenPartner's API has a built-in `/webhooks/stripe` handler that ingests `customer.created`, `customer.subscription.created`, and `invoice.paid` automatically. If you're already using Stripe with OpenPartner, point your Stripe webhook at OpenPartner's API directly — you don't need to call `trackEvent` from your own code.
96
+
97
+ Call `trackEvent` from your backend when:
98
+
99
+ - You don't use Stripe (or Stripe isn't the source of the conversion).
100
+ - You want to track custom event types (`trial_started`, `feature_activated`, etc.).
101
+ - You want to override the event timestamp (`ts: '2026-04-20T12:00:00Z'`).
102
+
103
+ ### Error handling
104
+
105
+ All failures throw an `OpenPartnerError` with:
106
+
107
+ ```ts
108
+ try {
109
+ await op.trackEvent({ userId, type: 'signup' });
110
+ } catch (err) {
111
+ if (err instanceof OpenPartnerError) {
112
+ console.error(err.status, err.code, err.detail);
113
+ }
114
+ }
115
+ ```
116
+
117
+ - `status` — HTTP status (400, 401, 403, 404, 500…)
118
+ - `code` — the API's machine-readable error string (e.g. `invalid_body`)
119
+ - `detail` — the full response body (often Zod's `flatten()` output)
120
+
121
+ ### Custom transport
122
+
123
+ For tests or specialized environments, pass your own `fetch`:
124
+
125
+ ```ts
126
+ new OpenPartnerServer({
127
+ apiUrl,
128
+ apiKey,
129
+ fetch: customFetch,
130
+ });
131
+ ```
132
+
133
+ ## Attribution model
134
+
135
+ ```
136
+ click (on vendor router) ← Click row on vendor's OpenPartner
137
+ ↓
138
+ identify(cref, userId) ← Identity row stitches click to user
139
+ ↓
140
+ trackEvent(userId, type, value)← Event row
141
+ ↓
142
+ attribution engine ← Applies campaign model, emits Attribution + Commission rows
143
+ ```
144
+
145
+ - **click** — visitor lands on `https://vendor.example.com/r/<slug>`. The OpenPartner router sets `_cref` cookie and writes a `Click` row.
146
+ - **identify** — once the visitor authenticates on the vendor's site, the browser SDK (or server SDK) calls `/attribution/identify` which writes an `Identity` row stitching `cref → userId`.
147
+ - **trackEvent** — any subsequent conversion event is written as an `Event` row. The attribution engine walks `Event.userId → Identity → Click → Campaign`, checks the attribution window, applies the configured model (`last_click` / `first_click` / `linear` / `position`), and writes `Attribution` + `Commission` rows.
148
+
149
+ ## License
150
+
151
+ MIT.
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21
+
22
+ // src/browser.ts
23
+ var browser_exports = {};
24
+ __export(browser_exports, {
25
+ OpenPartner: () => OpenPartner,
26
+ OpenPartnerError: () => OpenPartnerError
27
+ });
28
+ module.exports = __toCommonJS(browser_exports);
29
+
30
+ // src/shared.ts
31
+ var OpenPartnerError = class extends Error {
32
+ constructor(message, status, code, detail) {
33
+ super(message);
34
+ __publicField(this, "status", status);
35
+ __publicField(this, "code", code);
36
+ __publicField(this, "detail", detail);
37
+ this.name = "OpenPartnerError";
38
+ }
39
+ };
40
+ async function requestJson(input, init = {}, fetchImpl = globalThis.fetch) {
41
+ const headers = new Headers(init.headers);
42
+ if (init.body !== void 0 && !headers.has("content-type")) {
43
+ headers.set("content-type", "application/json");
44
+ }
45
+ const res = await fetchImpl(input, {
46
+ ...init,
47
+ headers,
48
+ body: init.body === void 0 ? void 0 : typeof init.body === "string" ? init.body : JSON.stringify(init.body)
49
+ });
50
+ const text = await res.text();
51
+ let body = void 0;
52
+ if (text) {
53
+ try {
54
+ body = JSON.parse(text);
55
+ } catch {
56
+ body = text;
57
+ }
58
+ }
59
+ if (!res.ok) {
60
+ const errBody = body ?? {};
61
+ throw new OpenPartnerError(
62
+ errBody.error ?? `${res.status} ${res.statusText}`,
63
+ res.status,
64
+ errBody.error,
65
+ errBody.detail ?? body
66
+ );
67
+ }
68
+ return body;
69
+ }
70
+
71
+ // src/browser.ts
72
+ var DEFAULTS = {
73
+ cookieName: "_cref",
74
+ queryParam: "cref",
75
+ storageKey: "openpartner:cref",
76
+ onError: (err) => {
77
+ console.warn("[openpartner] identify failed", err);
78
+ }
79
+ };
80
+ var OpenPartner = class _OpenPartner {
81
+ constructor(config) {
82
+ __publicField(this, "config");
83
+ this.config = { ...DEFAULTS, ...config };
84
+ this.captureCref();
85
+ }
86
+ static init(config) {
87
+ return new _OpenPartner(config);
88
+ }
89
+ /** The cref we're holding onto, if any. Useful for debugging. */
90
+ getCref() {
91
+ if (typeof localStorage === "undefined") return null;
92
+ return localStorage.getItem(this.config.storageKey);
93
+ }
94
+ /**
95
+ * Captured referral token for this visitor. Pass to Stripe Checkout
96
+ * as `client_reference_id` to enable webhook-based attribution
97
+ * without an explicit identify() call:
98
+ *
99
+ * stripe.checkout.sessions.create({
100
+ * client_reference_id: op.getReferral() ?? undefined,
101
+ * // ...
102
+ * });
103
+ *
104
+ * Returns null if the visitor didn't arrive via a partner link.
105
+ */
106
+ getReferral() {
107
+ return this.getCref();
108
+ }
109
+ /** Explicitly discard the captured cref. */
110
+ clear() {
111
+ if (typeof localStorage === "undefined") return;
112
+ localStorage.removeItem(this.config.storageKey);
113
+ }
114
+ /**
115
+ * Capture the cref from query string or cookie and stash in
116
+ * localStorage. Runs on init so the value survives SPA navigations,
117
+ * ITP cookie expiry, and subsequent page loads.
118
+ */
119
+ captureCref() {
120
+ if (typeof window === "undefined" || typeof localStorage === "undefined") return;
121
+ const url = new URL(window.location.href);
122
+ const queryCref = url.searchParams.get(this.config.queryParam);
123
+ if (queryCref) {
124
+ localStorage.setItem(this.config.storageKey, queryCref);
125
+ return;
126
+ }
127
+ const cookieCref = this.readCookie(this.config.cookieName);
128
+ if (cookieCref && !localStorage.getItem(this.config.storageKey)) {
129
+ localStorage.setItem(this.config.storageKey, cookieCref);
130
+ }
131
+ }
132
+ readCookie(name) {
133
+ if (typeof document === "undefined") return null;
134
+ const match = document.cookie.match(new RegExp(`(^|; )${name}=([^;]+)`));
135
+ return match ? decodeURIComponent(match[2]) : null;
136
+ }
137
+ /**
138
+ * Stitch the current captured click to an authenticated user. Call
139
+ * this on login, signup, or anywhere the user becomes known.
140
+ *
141
+ * No-op if no cref was captured. Errors are reported through
142
+ * `config.onError` by default — attribution is best-effort from the
143
+ * browser — but you can await this and try/catch if you want to
144
+ * surface failures explicitly.
145
+ */
146
+ async identify(userId) {
147
+ const cref = this.getCref();
148
+ if (!cref) return null;
149
+ try {
150
+ return await requestJson(`${this.config.apiUrl}/attribution/identify`, {
151
+ method: "POST",
152
+ body: { cref, userId, ts: Date.now() },
153
+ keepalive: true
154
+ });
155
+ } catch (err) {
156
+ this.config.onError(err);
157
+ return null;
158
+ }
159
+ }
160
+ };
161
+ // Annotate the CommonJS export names for ESM import in node:
162
+ 0 && (module.exports = {
163
+ OpenPartner,
164
+ OpenPartnerError
165
+ });
166
+ //# sourceMappingURL=browser.cjs.map
@@ -0,0 +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":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC1C,YACE,SACgB,QACA,MACA,QAChB;AACA,UAAM,OAAO;AAJG;AACA;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAkCA,eAAsB,YACpB,OACA,OAAwB,CAAC,GACzB,YAA0B,WAAW,OACzB;AACZ,QAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,MAAI,KAAK,SAAS,UAAa,CAAC,QAAQ,IAAI,cAAc,GAAG;AAC3D,YAAQ,IAAI,gBAAgB,kBAAkB;AAAA,EAChD;AACA,QAAM,MAAM,MAAM,UAAU,OAAO;AAAA,IACjC,GAAG;AAAA,IACH;AAAA,IACA,MACE,KAAK,SAAS,SACV,SACA,OAAO,KAAK,SAAS,WACnB,KAAK,OACL,KAAK,UAAU,KAAK,IAAI;AAAA,EAClC,CAAC;AAED,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,MAAI,OAAgB;AACpB,MAAI,MAAM;AACR,QAAI;AACF,aAAO,KAAK,MAAM,IAAI;AAAA,IACxB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,UAAW,QAAQ,CAAC;AAC1B,UAAM,IAAI;AAAA,MACR,QAAQ,SAAS,GAAG,IAAI,MAAM,IAAI,IAAI,UAAU;AAAA,MAChD,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ,UAAU;AAAA,IACpB;AAAA,EACF;AAEA,SAAO;AACT;;;ADxDA,IAAM,WAAW;AAAA,EACf,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,SAAS,CAAC,QAAiB;AACzB,YAAQ,KAAK,iCAAiC,GAAG;AAAA,EACnD;AACF;AAEO,IAAM,cAAN,MAAM,aAAY;AAAA,EAGf,YAAY,QAA2B;AAF/C,wBAAQ;AAGN,SAAK,SAAS,EAAE,GAAG,UAAU,GAAG,OAAO;AACvC,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,OAAO,KAAK,QAAwC;AAClD,WAAO,IAAI,aAAY,MAAM;AAAA,EAC/B;AAAA;AAAA,EAGA,UAAyB;AACvB,QAAI,OAAO,iBAAiB,YAAa,QAAO;AAChD,WAAO,aAAa,QAAQ,KAAK,OAAO,UAAU;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,cAA6B;AAC3B,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,OAAO,iBAAiB,YAAa;AACzC,iBAAa,WAAW,KAAK,OAAO,UAAU;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,cAAoB;AAC1B,QAAI,OAAO,WAAW,eAAe,OAAO,iBAAiB,YAAa;AAE1E,UAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,UAAM,YAAY,IAAI,aAAa,IAAI,KAAK,OAAO,UAAU;AAE7D,QAAI,WAAW;AACb,mBAAa,QAAQ,KAAK,OAAO,YAAY,SAAS;AACtD;AAAA,IACF;AAEA,UAAM,aAAa,KAAK,WAAW,KAAK,OAAO,UAAU;AACzD,QAAI,cAAc,CAAC,aAAa,QAAQ,KAAK,OAAO,UAAU,GAAG;AAC/D,mBAAa,QAAQ,KAAK,OAAO,YAAY,UAAU;AAAA,IACzD;AAAA,EACF;AAAA,EAEQ,WAAW,MAA6B;AAC9C,QAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,UAAM,QAAQ,SAAS,OAAO,MAAM,IAAI,OAAO,SAAS,IAAI,UAAU,CAAC;AACvE,WAAO,QAAQ,mBAAmB,MAAM,CAAC,CAAE,IAAI;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,SAAS,QAAkD;AAC/D,UAAM,OAAO,KAAK,QAAQ;AAC1B,QAAI,CAAC,KAAM,QAAO;AAElB,QAAI;AACF,aAAO,MAAM,YAA8B,GAAG,KAAK,OAAO,MAAM,yBAAyB;AAAA,QACvF,QAAQ;AAAA,QACR,MAAM,EAAE,MAAM,QAAQ,IAAI,KAAK,IAAI,EAAE;AAAA,QACrC,WAAW;AAAA,MACb,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,WAAK,OAAO,QAAQ,GAAG;AACvB,aAAO;AAAA,IACT;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,76 @@
1
+ import { I as IdentifyResponse } from './shared-CLfBjPeb.cjs';
2
+ export { O as OpenPartnerError } from './shared-CLfBjPeb.cjs';
3
+
4
+ /**
5
+ * Browser SDK — runs inside a merchant's web app. Captures the `cref`
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.
10
+ *
11
+ * Typical integration:
12
+ *
13
+ * import { OpenPartner } from '@openpartner/sdk';
14
+ *
15
+ * const op = OpenPartner.init({ apiUrl: 'https://openpartner.example.com' });
16
+ *
17
+ * // After login/signup, once you know who the user is:
18
+ * await op.identify(user.id);
19
+ */
20
+
21
+ interface OpenPartnerConfig {
22
+ /** Base URL of your OpenPartner API, e.g. "https://openpartner.example.com". */
23
+ apiUrl: string;
24
+ /** First-party cookie name set by the click router. Default `_cref`. */
25
+ cookieName?: string;
26
+ /** Query-string parameter that carries the cref on the landing URL. Default `cref`. */
27
+ queryParam?: string;
28
+ /** localStorage key the captured cref is stashed under. Default `openpartner:cref`. */
29
+ storageKey?: string;
30
+ /**
31
+ * Called when identify() fails. Default: log to console. Pass a noop
32
+ * to suppress entirely or hook into your observability stack.
33
+ */
34
+ onError?: (err: unknown) => void;
35
+ }
36
+ declare class OpenPartner {
37
+ private config;
38
+ private constructor();
39
+ static init(config: OpenPartnerConfig): OpenPartner;
40
+ /** The cref we're holding onto, if any. Useful for debugging. */
41
+ getCref(): string | null;
42
+ /**
43
+ * Captured referral token for this visitor. Pass to Stripe Checkout
44
+ * as `client_reference_id` to enable webhook-based attribution
45
+ * without an explicit identify() call:
46
+ *
47
+ * stripe.checkout.sessions.create({
48
+ * client_reference_id: op.getReferral() ?? undefined,
49
+ * // ...
50
+ * });
51
+ *
52
+ * Returns null if the visitor didn't arrive via a partner link.
53
+ */
54
+ getReferral(): string | null;
55
+ /** Explicitly discard the captured cref. */
56
+ clear(): void;
57
+ /**
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.
61
+ */
62
+ private captureCref;
63
+ private readCookie;
64
+ /**
65
+ * Stitch the current captured click to an authenticated user. Call
66
+ * this on login, signup, or anywhere the user becomes known.
67
+ *
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.
72
+ */
73
+ identify(userId: string): Promise<IdentifyResponse | null>;
74
+ }
75
+
76
+ export { IdentifyResponse, OpenPartner, type OpenPartnerConfig };
@@ -0,0 +1,76 @@
1
+ import { I as IdentifyResponse } from './shared-CLfBjPeb.js';
2
+ export { O as OpenPartnerError } from './shared-CLfBjPeb.js';
3
+
4
+ /**
5
+ * Browser SDK — runs inside a merchant's web app. Captures the `cref`
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.
10
+ *
11
+ * Typical integration:
12
+ *
13
+ * import { OpenPartner } from '@openpartner/sdk';
14
+ *
15
+ * const op = OpenPartner.init({ apiUrl: 'https://openpartner.example.com' });
16
+ *
17
+ * // After login/signup, once you know who the user is:
18
+ * await op.identify(user.id);
19
+ */
20
+
21
+ interface OpenPartnerConfig {
22
+ /** Base URL of your OpenPartner API, e.g. "https://openpartner.example.com". */
23
+ apiUrl: string;
24
+ /** First-party cookie name set by the click router. Default `_cref`. */
25
+ cookieName?: string;
26
+ /** Query-string parameter that carries the cref on the landing URL. Default `cref`. */
27
+ queryParam?: string;
28
+ /** localStorage key the captured cref is stashed under. Default `openpartner:cref`. */
29
+ storageKey?: string;
30
+ /**
31
+ * Called when identify() fails. Default: log to console. Pass a noop
32
+ * to suppress entirely or hook into your observability stack.
33
+ */
34
+ onError?: (err: unknown) => void;
35
+ }
36
+ declare class OpenPartner {
37
+ private config;
38
+ private constructor();
39
+ static init(config: OpenPartnerConfig): OpenPartner;
40
+ /** The cref we're holding onto, if any. Useful for debugging. */
41
+ getCref(): string | null;
42
+ /**
43
+ * Captured referral token for this visitor. Pass to Stripe Checkout
44
+ * as `client_reference_id` to enable webhook-based attribution
45
+ * without an explicit identify() call:
46
+ *
47
+ * stripe.checkout.sessions.create({
48
+ * client_reference_id: op.getReferral() ?? undefined,
49
+ * // ...
50
+ * });
51
+ *
52
+ * Returns null if the visitor didn't arrive via a partner link.
53
+ */
54
+ getReferral(): string | null;
55
+ /** Explicitly discard the captured cref. */
56
+ clear(): void;
57
+ /**
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.
61
+ */
62
+ private captureCref;
63
+ private readCookie;
64
+ /**
65
+ * Stitch the current captured click to an authenticated user. Call
66
+ * this on login, signup, or anywhere the user becomes known.
67
+ *
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.
72
+ */
73
+ identify(userId: string): Promise<IdentifyResponse | null>;
74
+ }
75
+
76
+ export { IdentifyResponse, OpenPartner, type OpenPartnerConfig };
@@ -0,0 +1,140 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/shared.ts
6
+ var OpenPartnerError = class extends Error {
7
+ constructor(message, status, code, detail) {
8
+ super(message);
9
+ __publicField(this, "status", status);
10
+ __publicField(this, "code", code);
11
+ __publicField(this, "detail", detail);
12
+ this.name = "OpenPartnerError";
13
+ }
14
+ };
15
+ async function requestJson(input, init = {}, fetchImpl = globalThis.fetch) {
16
+ const headers = new Headers(init.headers);
17
+ if (init.body !== void 0 && !headers.has("content-type")) {
18
+ headers.set("content-type", "application/json");
19
+ }
20
+ const res = await fetchImpl(input, {
21
+ ...init,
22
+ headers,
23
+ body: init.body === void 0 ? void 0 : typeof init.body === "string" ? init.body : JSON.stringify(init.body)
24
+ });
25
+ const text = await res.text();
26
+ let body = void 0;
27
+ if (text) {
28
+ try {
29
+ body = JSON.parse(text);
30
+ } catch {
31
+ body = text;
32
+ }
33
+ }
34
+ if (!res.ok) {
35
+ const errBody = body ?? {};
36
+ throw new OpenPartnerError(
37
+ errBody.error ?? `${res.status} ${res.statusText}`,
38
+ res.status,
39
+ errBody.error,
40
+ errBody.detail ?? body
41
+ );
42
+ }
43
+ return body;
44
+ }
45
+
46
+ // src/browser.ts
47
+ var DEFAULTS = {
48
+ cookieName: "_cref",
49
+ queryParam: "cref",
50
+ storageKey: "openpartner:cref",
51
+ onError: (err) => {
52
+ console.warn("[openpartner] identify failed", err);
53
+ }
54
+ };
55
+ var OpenPartner = class _OpenPartner {
56
+ constructor(config) {
57
+ __publicField(this, "config");
58
+ this.config = { ...DEFAULTS, ...config };
59
+ this.captureCref();
60
+ }
61
+ static init(config) {
62
+ return new _OpenPartner(config);
63
+ }
64
+ /** The cref we're holding onto, if any. Useful for debugging. */
65
+ getCref() {
66
+ if (typeof localStorage === "undefined") return null;
67
+ return localStorage.getItem(this.config.storageKey);
68
+ }
69
+ /**
70
+ * Captured referral token for this visitor. Pass to Stripe Checkout
71
+ * as `client_reference_id` to enable webhook-based attribution
72
+ * without an explicit identify() call:
73
+ *
74
+ * stripe.checkout.sessions.create({
75
+ * client_reference_id: op.getReferral() ?? undefined,
76
+ * // ...
77
+ * });
78
+ *
79
+ * Returns null if the visitor didn't arrive via a partner link.
80
+ */
81
+ getReferral() {
82
+ return this.getCref();
83
+ }
84
+ /** Explicitly discard the captured cref. */
85
+ clear() {
86
+ if (typeof localStorage === "undefined") return;
87
+ localStorage.removeItem(this.config.storageKey);
88
+ }
89
+ /**
90
+ * Capture the cref from query string or cookie and stash in
91
+ * localStorage. Runs on init so the value survives SPA navigations,
92
+ * ITP cookie expiry, and subsequent page loads.
93
+ */
94
+ captureCref() {
95
+ if (typeof window === "undefined" || typeof localStorage === "undefined") return;
96
+ const url = new URL(window.location.href);
97
+ const queryCref = url.searchParams.get(this.config.queryParam);
98
+ if (queryCref) {
99
+ localStorage.setItem(this.config.storageKey, queryCref);
100
+ return;
101
+ }
102
+ const cookieCref = this.readCookie(this.config.cookieName);
103
+ if (cookieCref && !localStorage.getItem(this.config.storageKey)) {
104
+ localStorage.setItem(this.config.storageKey, cookieCref);
105
+ }
106
+ }
107
+ readCookie(name) {
108
+ if (typeof document === "undefined") return null;
109
+ const match = document.cookie.match(new RegExp(`(^|; )${name}=([^;]+)`));
110
+ return match ? decodeURIComponent(match[2]) : null;
111
+ }
112
+ /**
113
+ * Stitch the current captured click to an authenticated user. Call
114
+ * this on login, signup, or anywhere the user becomes known.
115
+ *
116
+ * No-op if no cref was captured. Errors are reported through
117
+ * `config.onError` by default — attribution is best-effort from the
118
+ * browser — but you can await this and try/catch if you want to
119
+ * surface failures explicitly.
120
+ */
121
+ async identify(userId) {
122
+ const cref = this.getCref();
123
+ if (!cref) return null;
124
+ try {
125
+ return await requestJson(`${this.config.apiUrl}/attribution/identify`, {
126
+ method: "POST",
127
+ body: { cref, userId, ts: Date.now() },
128
+ keepalive: true
129
+ });
130
+ } catch (err) {
131
+ this.config.onError(err);
132
+ return null;
133
+ }
134
+ }
135
+ };
136
+ export {
137
+ OpenPartner,
138
+ OpenPartnerError
139
+ };
140
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared.ts","../src/browser.ts"],"sourcesContent":["/**\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","/**\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"],"mappings":"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","names":[]}
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21
+
22
+ // src/server.ts
23
+ var server_exports = {};
24
+ __export(server_exports, {
25
+ OpenPartnerError: () => OpenPartnerError,
26
+ OpenPartnerServer: () => OpenPartnerServer
27
+ });
28
+ module.exports = __toCommonJS(server_exports);
29
+
30
+ // src/shared.ts
31
+ var OpenPartnerError = class extends Error {
32
+ constructor(message, status, code, detail) {
33
+ super(message);
34
+ __publicField(this, "status", status);
35
+ __publicField(this, "code", code);
36
+ __publicField(this, "detail", detail);
37
+ this.name = "OpenPartnerError";
38
+ }
39
+ };
40
+ async function requestJson(input, init = {}, fetchImpl = globalThis.fetch) {
41
+ const headers = new Headers(init.headers);
42
+ if (init.body !== void 0 && !headers.has("content-type")) {
43
+ headers.set("content-type", "application/json");
44
+ }
45
+ const res = await fetchImpl(input, {
46
+ ...init,
47
+ headers,
48
+ body: init.body === void 0 ? void 0 : typeof init.body === "string" ? init.body : JSON.stringify(init.body)
49
+ });
50
+ const text = await res.text();
51
+ let body = void 0;
52
+ if (text) {
53
+ try {
54
+ body = JSON.parse(text);
55
+ } catch {
56
+ body = text;
57
+ }
58
+ }
59
+ if (!res.ok) {
60
+ const errBody = body ?? {};
61
+ throw new OpenPartnerError(
62
+ errBody.error ?? `${res.status} ${res.statusText}`,
63
+ res.status,
64
+ errBody.error,
65
+ errBody.detail ?? body
66
+ );
67
+ }
68
+ return body;
69
+ }
70
+
71
+ // src/server.ts
72
+ var OpenPartnerServer = class {
73
+ constructor(config) {
74
+ __publicField(this, "apiUrl");
75
+ __publicField(this, "apiKey");
76
+ __publicField(this, "fetchImpl");
77
+ if (!config.apiUrl) throw new OpenPartnerError("apiUrl is required", 400, "config_missing");
78
+ if (!config.apiKey) throw new OpenPartnerError("apiKey is required", 400, "config_missing");
79
+ this.apiUrl = config.apiUrl.replace(/\/$/, "");
80
+ this.apiKey = config.apiKey;
81
+ this.fetchImpl = config.fetch ?? globalThis.fetch;
82
+ }
83
+ /**
84
+ * Record a conversion event. The server walks the userId back to a
85
+ * click, applies the campaign's attribution model, and writes the
86
+ * resulting attribution + commission rows.
87
+ */
88
+ async trackEvent(params) {
89
+ return this.request("/attribution/events", { method: "POST", body: params });
90
+ }
91
+ /**
92
+ * Stitch an attribution token (cref) to a known userId. Normally the
93
+ * browser SDK does this; use this overload if your backend receives
94
+ * the cref from the browser (e.g. as part of a signup payload) and
95
+ * wants to own the stitch call.
96
+ */
97
+ async identify(params) {
98
+ return this.request("/attribution/identify", {
99
+ method: "POST",
100
+ body: params
101
+ });
102
+ }
103
+ async request(path, init) {
104
+ const headers = new Headers(init.headers);
105
+ headers.set("authorization", `Bearer ${this.apiKey}`);
106
+ return requestJson(
107
+ `${this.apiUrl}${path}`,
108
+ { ...init, headers },
109
+ // Inject the configured fetch so tests (or alternate transports)
110
+ // can intercept.
111
+ this.fetchImpl
112
+ );
113
+ }
114
+ };
115
+ // Annotate the CommonJS export names for ESM import in node:
116
+ 0 && (module.exports = {
117
+ OpenPartnerError,
118
+ OpenPartnerServer
119
+ });
120
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/shared.ts"],"sourcesContent":["/**\n * Server SDK — runs inside a merchant's backend. Used for the\n * server-to-server pieces of the attribution pipeline: posting\n * conversion events (signup / invoice_paid / custom), stitching\n * clicks to users from the backend, and reading dashboard snapshots.\n *\n * Typical integration:\n *\n * import { OpenPartnerServer } from '@openpartner/sdk/server';\n *\n * const op = new OpenPartnerServer({\n * apiUrl: process.env.OPENPARTNER_API_URL!,\n * apiKey: process.env.OPENPARTNER_API_KEY!,\n * });\n *\n * // On signup:\n * await op.trackEvent({ userId, type: 'signup' });\n *\n * // On invoice paid:\n * await op.trackEvent({ userId, type: 'invoice_paid', value: 249, currency: 'USD' });\n */\n\nimport { OpenPartnerError, requestJson, type EventResponse, type IdentifyResponse, type JsonRequestInit } from './shared.js';\n\nexport { OpenPartnerError };\nexport type { EventResponse, IdentifyResponse };\nexport type { EventAttribution } from './shared.js';\n\nexport interface OpenPartnerServerConfig {\n /** Base URL of the OpenPartner API. */\n apiUrl: string;\n /**\n * Admin or scoped API key. For event ingest you need admin or a key\n * with the right scope (the core API currently uses admin-only for\n * /attribution/events). Never expose this to the browser.\n */\n apiKey: string;\n /** Optional fetch override for tests or custom transports. */\n fetch?: typeof fetch;\n}\n\nexport interface TrackEventParams {\n /** The user the event pertains to. Must match how the browser SDK called identify(). */\n userId: string;\n /** Event type — typically 'signup' | 'trial_started' | 'subscription_created' | 'invoice_paid' | custom. */\n type: string;\n /** Dollar (or currency) value of the event, for percent-commission rules. */\n value?: number;\n /** ISO 4217 currency code. Defaults to USD on the server if omitted. */\n currency?: string;\n /** Arbitrary metadata you want to attach (stripeEventId, invoiceId, etc). */\n metadata?: Record<string, unknown>;\n /** Event time. Defaults to now on the server. Pass ISO 8601 to override. */\n ts?: string;\n}\n\nexport class OpenPartnerServer {\n private readonly apiUrl: string;\n private readonly apiKey: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(config: OpenPartnerServerConfig) {\n if (!config.apiUrl) throw new OpenPartnerError('apiUrl is required', 400, 'config_missing');\n if (!config.apiKey) throw new OpenPartnerError('apiKey is required', 400, 'config_missing');\n this.apiUrl = config.apiUrl.replace(/\\/$/, '');\n this.apiKey = config.apiKey;\n this.fetchImpl = config.fetch ?? globalThis.fetch;\n }\n\n /**\n * Record a conversion event. The server walks the userId back to a\n * click, applies the campaign's attribution model, and writes the\n * resulting attribution + commission rows.\n */\n async trackEvent(params: TrackEventParams): Promise<EventResponse> {\n return this.request<EventResponse>('/attribution/events', { method: 'POST', body: params });\n }\n\n /**\n * Stitch an attribution token (cref) to a known userId. Normally the\n * browser SDK does this; use this overload if your backend receives\n * the cref from the browser (e.g. as part of a signup payload) and\n * wants to own the stitch call.\n */\n async identify(params: { cref: string; userId: string; ts?: number }): Promise<IdentifyResponse> {\n return this.request<IdentifyResponse>('/attribution/identify', {\n method: 'POST',\n body: params,\n });\n }\n\n private async request<T>(path: string, init: JsonRequestInit): Promise<T> {\n const headers = new Headers(init.headers);\n headers.set('authorization', `Bearer ${this.apiKey}`);\n return requestJson<T>(\n `${this.apiUrl}${path}`,\n { ...init, headers },\n // Inject the configured fetch so tests (or alternate transports)\n // can intercept.\n this.fetchImpl,\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":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC1C,YACE,SACgB,QACA,MACA,QAChB;AACA,UAAM,OAAO;AAJG;AACA;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAkCA,eAAsB,YACpB,OACA,OAAwB,CAAC,GACzB,YAA0B,WAAW,OACzB;AACZ,QAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,MAAI,KAAK,SAAS,UAAa,CAAC,QAAQ,IAAI,cAAc,GAAG;AAC3D,YAAQ,IAAI,gBAAgB,kBAAkB;AAAA,EAChD;AACA,QAAM,MAAM,MAAM,UAAU,OAAO;AAAA,IACjC,GAAG;AAAA,IACH;AAAA,IACA,MACE,KAAK,SAAS,SACV,SACA,OAAO,KAAK,SAAS,WACnB,KAAK,OACL,KAAK,UAAU,KAAK,IAAI;AAAA,EAClC,CAAC;AAED,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,MAAI,OAAgB;AACpB,MAAI,MAAM;AACR,QAAI;AACF,aAAO,KAAK,MAAM,IAAI;AAAA,IACxB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,UAAW,QAAQ,CAAC;AAC1B,UAAM,IAAI;AAAA,MACR,QAAQ,SAAS,GAAG,IAAI,MAAM,IAAI,IAAI,UAAU;AAAA,MAChD,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ,UAAU;AAAA,IACpB;AAAA,EACF;AAEA,SAAO;AACT;;;ADtCO,IAAM,oBAAN,MAAwB;AAAA,EAK7B,YAAY,QAAiC;AAJ7C,wBAAiB;AACjB,wBAAiB;AACjB,wBAAiB;AAGf,QAAI,CAAC,OAAO,OAAQ,OAAM,IAAI,iBAAiB,sBAAsB,KAAK,gBAAgB;AAC1F,QAAI,CAAC,OAAO,OAAQ,OAAM,IAAI,iBAAiB,sBAAsB,KAAK,gBAAgB;AAC1F,SAAK,SAAS,OAAO,OAAO,QAAQ,OAAO,EAAE;AAC7C,SAAK,SAAS,OAAO;AACrB,SAAK,YAAY,OAAO,SAAS,WAAW;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,WAAW,QAAkD;AACjE,WAAO,KAAK,QAAuB,uBAAuB,EAAE,QAAQ,QAAQ,MAAM,OAAO,CAAC;AAAA,EAC5F;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,SAAS,QAAkF;AAC/F,WAAO,KAAK,QAA0B,yBAAyB;AAAA,MAC7D,QAAQ;AAAA,MACR,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,QAAW,MAAc,MAAmC;AACxE,UAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,YAAQ,IAAI,iBAAiB,UAAU,KAAK,MAAM,EAAE;AACpD,WAAO;AAAA,MACL,GAAG,KAAK,MAAM,GAAG,IAAI;AAAA,MACrB,EAAE,GAAG,MAAM,QAAQ;AAAA;AAAA;AAAA,MAGnB,KAAK;AAAA,IACP;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,77 @@
1
+ import { E as EventResponse, I as IdentifyResponse } from './shared-CLfBjPeb.cjs';
2
+ export { a as EventAttribution, O as OpenPartnerError } from './shared-CLfBjPeb.cjs';
3
+
4
+ /**
5
+ * Server SDK — runs inside a merchant's backend. Used for the
6
+ * server-to-server pieces of the attribution pipeline: posting
7
+ * conversion events (signup / invoice_paid / custom), stitching
8
+ * clicks to users from the backend, and reading dashboard snapshots.
9
+ *
10
+ * Typical integration:
11
+ *
12
+ * import { OpenPartnerServer } from '@openpartner/sdk/server';
13
+ *
14
+ * const op = new OpenPartnerServer({
15
+ * apiUrl: process.env.OPENPARTNER_API_URL!,
16
+ * apiKey: process.env.OPENPARTNER_API_KEY!,
17
+ * });
18
+ *
19
+ * // On signup:
20
+ * await op.trackEvent({ userId, type: 'signup' });
21
+ *
22
+ * // On invoice paid:
23
+ * await op.trackEvent({ userId, type: 'invoice_paid', value: 249, currency: 'USD' });
24
+ */
25
+
26
+ interface OpenPartnerServerConfig {
27
+ /** Base URL of the OpenPartner API. */
28
+ apiUrl: string;
29
+ /**
30
+ * Admin or scoped API key. For event ingest you need admin or a key
31
+ * with the right scope (the core API currently uses admin-only for
32
+ * /attribution/events). Never expose this to the browser.
33
+ */
34
+ apiKey: string;
35
+ /** Optional fetch override for tests or custom transports. */
36
+ fetch?: typeof fetch;
37
+ }
38
+ interface TrackEventParams {
39
+ /** The user the event pertains to. Must match how the browser SDK called identify(). */
40
+ userId: string;
41
+ /** Event type — typically 'signup' | 'trial_started' | 'subscription_created' | 'invoice_paid' | custom. */
42
+ type: string;
43
+ /** Dollar (or currency) value of the event, for percent-commission rules. */
44
+ value?: number;
45
+ /** ISO 4217 currency code. Defaults to USD on the server if omitted. */
46
+ currency?: string;
47
+ /** Arbitrary metadata you want to attach (stripeEventId, invoiceId, etc). */
48
+ metadata?: Record<string, unknown>;
49
+ /** Event time. Defaults to now on the server. Pass ISO 8601 to override. */
50
+ ts?: string;
51
+ }
52
+ declare class OpenPartnerServer {
53
+ private readonly apiUrl;
54
+ private readonly apiKey;
55
+ private readonly fetchImpl;
56
+ constructor(config: OpenPartnerServerConfig);
57
+ /**
58
+ * Record a conversion event. The server walks the userId back to a
59
+ * click, applies the campaign's attribution model, and writes the
60
+ * resulting attribution + commission rows.
61
+ */
62
+ trackEvent(params: TrackEventParams): Promise<EventResponse>;
63
+ /**
64
+ * Stitch an attribution token (cref) to a known userId. Normally the
65
+ * browser SDK does this; use this overload if your backend receives
66
+ * the cref from the browser (e.g. as part of a signup payload) and
67
+ * wants to own the stitch call.
68
+ */
69
+ identify(params: {
70
+ cref: string;
71
+ userId: string;
72
+ ts?: number;
73
+ }): Promise<IdentifyResponse>;
74
+ private request;
75
+ }
76
+
77
+ export { EventResponse, IdentifyResponse, OpenPartnerServer, type OpenPartnerServerConfig, type TrackEventParams };
@@ -0,0 +1,77 @@
1
+ import { E as EventResponse, I as IdentifyResponse } from './shared-CLfBjPeb.js';
2
+ export { a as EventAttribution, O as OpenPartnerError } from './shared-CLfBjPeb.js';
3
+
4
+ /**
5
+ * Server SDK — runs inside a merchant's backend. Used for the
6
+ * server-to-server pieces of the attribution pipeline: posting
7
+ * conversion events (signup / invoice_paid / custom), stitching
8
+ * clicks to users from the backend, and reading dashboard snapshots.
9
+ *
10
+ * Typical integration:
11
+ *
12
+ * import { OpenPartnerServer } from '@openpartner/sdk/server';
13
+ *
14
+ * const op = new OpenPartnerServer({
15
+ * apiUrl: process.env.OPENPARTNER_API_URL!,
16
+ * apiKey: process.env.OPENPARTNER_API_KEY!,
17
+ * });
18
+ *
19
+ * // On signup:
20
+ * await op.trackEvent({ userId, type: 'signup' });
21
+ *
22
+ * // On invoice paid:
23
+ * await op.trackEvent({ userId, type: 'invoice_paid', value: 249, currency: 'USD' });
24
+ */
25
+
26
+ interface OpenPartnerServerConfig {
27
+ /** Base URL of the OpenPartner API. */
28
+ apiUrl: string;
29
+ /**
30
+ * Admin or scoped API key. For event ingest you need admin or a key
31
+ * with the right scope (the core API currently uses admin-only for
32
+ * /attribution/events). Never expose this to the browser.
33
+ */
34
+ apiKey: string;
35
+ /** Optional fetch override for tests or custom transports. */
36
+ fetch?: typeof fetch;
37
+ }
38
+ interface TrackEventParams {
39
+ /** The user the event pertains to. Must match how the browser SDK called identify(). */
40
+ userId: string;
41
+ /** Event type — typically 'signup' | 'trial_started' | 'subscription_created' | 'invoice_paid' | custom. */
42
+ type: string;
43
+ /** Dollar (or currency) value of the event, for percent-commission rules. */
44
+ value?: number;
45
+ /** ISO 4217 currency code. Defaults to USD on the server if omitted. */
46
+ currency?: string;
47
+ /** Arbitrary metadata you want to attach (stripeEventId, invoiceId, etc). */
48
+ metadata?: Record<string, unknown>;
49
+ /** Event time. Defaults to now on the server. Pass ISO 8601 to override. */
50
+ ts?: string;
51
+ }
52
+ declare class OpenPartnerServer {
53
+ private readonly apiUrl;
54
+ private readonly apiKey;
55
+ private readonly fetchImpl;
56
+ constructor(config: OpenPartnerServerConfig);
57
+ /**
58
+ * Record a conversion event. The server walks the userId back to a
59
+ * click, applies the campaign's attribution model, and writes the
60
+ * resulting attribution + commission rows.
61
+ */
62
+ trackEvent(params: TrackEventParams): Promise<EventResponse>;
63
+ /**
64
+ * Stitch an attribution token (cref) to a known userId. Normally the
65
+ * browser SDK does this; use this overload if your backend receives
66
+ * the cref from the browser (e.g. as part of a signup payload) and
67
+ * wants to own the stitch call.
68
+ */
69
+ identify(params: {
70
+ cref: string;
71
+ userId: string;
72
+ ts?: number;
73
+ }): Promise<IdentifyResponse>;
74
+ private request;
75
+ }
76
+
77
+ export { EventResponse, IdentifyResponse, OpenPartnerServer, type OpenPartnerServerConfig, type TrackEventParams };
package/dist/server.js ADDED
@@ -0,0 +1,94 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/shared.ts
6
+ var OpenPartnerError = class extends Error {
7
+ constructor(message, status, code, detail) {
8
+ super(message);
9
+ __publicField(this, "status", status);
10
+ __publicField(this, "code", code);
11
+ __publicField(this, "detail", detail);
12
+ this.name = "OpenPartnerError";
13
+ }
14
+ };
15
+ async function requestJson(input, init = {}, fetchImpl = globalThis.fetch) {
16
+ const headers = new Headers(init.headers);
17
+ if (init.body !== void 0 && !headers.has("content-type")) {
18
+ headers.set("content-type", "application/json");
19
+ }
20
+ const res = await fetchImpl(input, {
21
+ ...init,
22
+ headers,
23
+ body: init.body === void 0 ? void 0 : typeof init.body === "string" ? init.body : JSON.stringify(init.body)
24
+ });
25
+ const text = await res.text();
26
+ let body = void 0;
27
+ if (text) {
28
+ try {
29
+ body = JSON.parse(text);
30
+ } catch {
31
+ body = text;
32
+ }
33
+ }
34
+ if (!res.ok) {
35
+ const errBody = body ?? {};
36
+ throw new OpenPartnerError(
37
+ errBody.error ?? `${res.status} ${res.statusText}`,
38
+ res.status,
39
+ errBody.error,
40
+ errBody.detail ?? body
41
+ );
42
+ }
43
+ return body;
44
+ }
45
+
46
+ // src/server.ts
47
+ var OpenPartnerServer = class {
48
+ constructor(config) {
49
+ __publicField(this, "apiUrl");
50
+ __publicField(this, "apiKey");
51
+ __publicField(this, "fetchImpl");
52
+ if (!config.apiUrl) throw new OpenPartnerError("apiUrl is required", 400, "config_missing");
53
+ if (!config.apiKey) throw new OpenPartnerError("apiKey is required", 400, "config_missing");
54
+ this.apiUrl = config.apiUrl.replace(/\/$/, "");
55
+ this.apiKey = config.apiKey;
56
+ this.fetchImpl = config.fetch ?? globalThis.fetch;
57
+ }
58
+ /**
59
+ * Record a conversion event. The server walks the userId back to a
60
+ * click, applies the campaign's attribution model, and writes the
61
+ * resulting attribution + commission rows.
62
+ */
63
+ async trackEvent(params) {
64
+ return this.request("/attribution/events", { method: "POST", body: params });
65
+ }
66
+ /**
67
+ * Stitch an attribution token (cref) to a known userId. Normally the
68
+ * browser SDK does this; use this overload if your backend receives
69
+ * the cref from the browser (e.g. as part of a signup payload) and
70
+ * wants to own the stitch call.
71
+ */
72
+ async identify(params) {
73
+ return this.request("/attribution/identify", {
74
+ method: "POST",
75
+ body: params
76
+ });
77
+ }
78
+ async request(path, init) {
79
+ const headers = new Headers(init.headers);
80
+ headers.set("authorization", `Bearer ${this.apiKey}`);
81
+ return requestJson(
82
+ `${this.apiUrl}${path}`,
83
+ { ...init, headers },
84
+ // Inject the configured fetch so tests (or alternate transports)
85
+ // can intercept.
86
+ this.fetchImpl
87
+ );
88
+ }
89
+ };
90
+ export {
91
+ OpenPartnerError,
92
+ OpenPartnerServer
93
+ };
94
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared.ts","../src/server.ts"],"sourcesContent":["/**\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","/**\n * Server SDK — runs inside a merchant's backend. Used for the\n * server-to-server pieces of the attribution pipeline: posting\n * conversion events (signup / invoice_paid / custom), stitching\n * clicks to users from the backend, and reading dashboard snapshots.\n *\n * Typical integration:\n *\n * import { OpenPartnerServer } from '@openpartner/sdk/server';\n *\n * const op = new OpenPartnerServer({\n * apiUrl: process.env.OPENPARTNER_API_URL!,\n * apiKey: process.env.OPENPARTNER_API_KEY!,\n * });\n *\n * // On signup:\n * await op.trackEvent({ userId, type: 'signup' });\n *\n * // On invoice paid:\n * await op.trackEvent({ userId, type: 'invoice_paid', value: 249, currency: 'USD' });\n */\n\nimport { OpenPartnerError, requestJson, type EventResponse, type IdentifyResponse, type JsonRequestInit } from './shared.js';\n\nexport { OpenPartnerError };\nexport type { EventResponse, IdentifyResponse };\nexport type { EventAttribution } from './shared.js';\n\nexport interface OpenPartnerServerConfig {\n /** Base URL of the OpenPartner API. */\n apiUrl: string;\n /**\n * Admin or scoped API key. For event ingest you need admin or a key\n * with the right scope (the core API currently uses admin-only for\n * /attribution/events). Never expose this to the browser.\n */\n apiKey: string;\n /** Optional fetch override for tests or custom transports. */\n fetch?: typeof fetch;\n}\n\nexport interface TrackEventParams {\n /** The user the event pertains to. Must match how the browser SDK called identify(). */\n userId: string;\n /** Event type — typically 'signup' | 'trial_started' | 'subscription_created' | 'invoice_paid' | custom. */\n type: string;\n /** Dollar (or currency) value of the event, for percent-commission rules. */\n value?: number;\n /** ISO 4217 currency code. Defaults to USD on the server if omitted. */\n currency?: string;\n /** Arbitrary metadata you want to attach (stripeEventId, invoiceId, etc). */\n metadata?: Record<string, unknown>;\n /** Event time. Defaults to now on the server. Pass ISO 8601 to override. */\n ts?: string;\n}\n\nexport class OpenPartnerServer {\n private readonly apiUrl: string;\n private readonly apiKey: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(config: OpenPartnerServerConfig) {\n if (!config.apiUrl) throw new OpenPartnerError('apiUrl is required', 400, 'config_missing');\n if (!config.apiKey) throw new OpenPartnerError('apiKey is required', 400, 'config_missing');\n this.apiUrl = config.apiUrl.replace(/\\/$/, '');\n this.apiKey = config.apiKey;\n this.fetchImpl = config.fetch ?? globalThis.fetch;\n }\n\n /**\n * Record a conversion event. The server walks the userId back to a\n * click, applies the campaign's attribution model, and writes the\n * resulting attribution + commission rows.\n */\n async trackEvent(params: TrackEventParams): Promise<EventResponse> {\n return this.request<EventResponse>('/attribution/events', { method: 'POST', body: params });\n }\n\n /**\n * Stitch an attribution token (cref) to a known userId. Normally the\n * browser SDK does this; use this overload if your backend receives\n * the cref from the browser (e.g. as part of a signup payload) and\n * wants to own the stitch call.\n */\n async identify(params: { cref: string; userId: string; ts?: number }): Promise<IdentifyResponse> {\n return this.request<IdentifyResponse>('/attribution/identify', {\n method: 'POST',\n body: params,\n });\n }\n\n private async request<T>(path: string, init: JsonRequestInit): Promise<T> {\n const headers = new Headers(init.headers);\n headers.set('authorization', `Bearer ${this.apiKey}`);\n return requestJson<T>(\n `${this.apiUrl}${path}`,\n { ...init, headers },\n // Inject the configured fetch so tests (or alternate transports)\n // can intercept.\n this.fetchImpl,\n );\n }\n}\n"],"mappings":";;;;;AASO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC1C,YACE,SACgB,QACA,MACA,QAChB;AACA,UAAM,OAAO;AAJG;AACA;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAkCA,eAAsB,YACpB,OACA,OAAwB,CAAC,GACzB,YAA0B,WAAW,OACzB;AACZ,QAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,MAAI,KAAK,SAAS,UAAa,CAAC,QAAQ,IAAI,cAAc,GAAG;AAC3D,YAAQ,IAAI,gBAAgB,kBAAkB;AAAA,EAChD;AACA,QAAM,MAAM,MAAM,UAAU,OAAO;AAAA,IACjC,GAAG;AAAA,IACH;AAAA,IACA,MACE,KAAK,SAAS,SACV,SACA,OAAO,KAAK,SAAS,WACnB,KAAK,OACL,KAAK,UAAU,KAAK,IAAI;AAAA,EAClC,CAAC;AAED,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,MAAI,OAAgB;AACpB,MAAI,MAAM;AACR,QAAI;AACF,aAAO,KAAK,MAAM,IAAI;AAAA,IACxB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,UAAW,QAAQ,CAAC;AAC1B,UAAM,IAAI;AAAA,MACR,QAAQ,SAAS,GAAG,IAAI,MAAM,IAAI,IAAI,UAAU;AAAA,MAChD,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ,UAAU;AAAA,IACpB;AAAA,EACF;AAEA,SAAO;AACT;;;ACtCO,IAAM,oBAAN,MAAwB;AAAA,EAK7B,YAAY,QAAiC;AAJ7C,wBAAiB;AACjB,wBAAiB;AACjB,wBAAiB;AAGf,QAAI,CAAC,OAAO,OAAQ,OAAM,IAAI,iBAAiB,sBAAsB,KAAK,gBAAgB;AAC1F,QAAI,CAAC,OAAO,OAAQ,OAAM,IAAI,iBAAiB,sBAAsB,KAAK,gBAAgB;AAC1F,SAAK,SAAS,OAAO,OAAO,QAAQ,OAAO,EAAE;AAC7C,SAAK,SAAS,OAAO;AACrB,SAAK,YAAY,OAAO,SAAS,WAAW;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,WAAW,QAAkD;AACjE,WAAO,KAAK,QAAuB,uBAAuB,EAAE,QAAQ,QAAQ,MAAM,OAAO,CAAC;AAAA,EAC5F;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,SAAS,QAAkF;AAC/F,WAAO,KAAK,QAA0B,yBAAyB;AAAA,MAC7D,QAAQ;AAAA,MACR,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,QAAW,MAAc,MAAmC;AACxE,UAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,YAAQ,IAAI,iBAAiB,UAAU,KAAK,MAAM,EAAE;AACpD,WAAO;AAAA,MACL,GAAG,KAAK,MAAM,GAAG,IAAI;AAAA,MACrB,EAAE,GAAG,MAAM,QAAQ;AAAA;AAAA;AAAA,MAGnB,KAAK;AAAA,IACP;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared types + error class for both the browser and server entry
3
+ * points.
4
+ *
5
+ * Keeping these in their own module means browser imports won't drag in
6
+ * `fetch` polyfills or server-only code, and server imports won't touch
7
+ * anything that references `window` or `document`.
8
+ */
9
+ declare class OpenPartnerError extends Error {
10
+ readonly status: number;
11
+ readonly code?: string | undefined;
12
+ readonly detail?: unknown;
13
+ constructor(message: string, status: number, code?: string | undefined, detail?: unknown);
14
+ }
15
+ interface IdentifyResponse {
16
+ ok: boolean;
17
+ identityId: string | null;
18
+ firstStitch: boolean;
19
+ backfilledAttributions?: number;
20
+ }
21
+ interface EventAttribution {
22
+ status: 'attributed' | 'no_identity' | 'no_click' | 'outside_window' | 'already_attributed';
23
+ model?: 'last_click' | 'first_click' | 'linear' | 'position';
24
+ touches?: Array<{
25
+ clickId: string;
26
+ partnerId: string;
27
+ weight: number;
28
+ attributionId: string;
29
+ commissionId?: string;
30
+ }>;
31
+ }
32
+ interface EventResponse {
33
+ ok: boolean;
34
+ eventId: string;
35
+ attribution: EventAttribution;
36
+ }
37
+
38
+ export { type EventResponse as E, type IdentifyResponse as I, OpenPartnerError as O, type EventAttribution as a };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared types + error class for both the browser and server entry
3
+ * points.
4
+ *
5
+ * Keeping these in their own module means browser imports won't drag in
6
+ * `fetch` polyfills or server-only code, and server imports won't touch
7
+ * anything that references `window` or `document`.
8
+ */
9
+ declare class OpenPartnerError extends Error {
10
+ readonly status: number;
11
+ readonly code?: string | undefined;
12
+ readonly detail?: unknown;
13
+ constructor(message: string, status: number, code?: string | undefined, detail?: unknown);
14
+ }
15
+ interface IdentifyResponse {
16
+ ok: boolean;
17
+ identityId: string | null;
18
+ firstStitch: boolean;
19
+ backfilledAttributions?: number;
20
+ }
21
+ interface EventAttribution {
22
+ status: 'attributed' | 'no_identity' | 'no_click' | 'outside_window' | 'already_attributed';
23
+ model?: 'last_click' | 'first_click' | 'linear' | 'position';
24
+ touches?: Array<{
25
+ clickId: string;
26
+ partnerId: string;
27
+ weight: number;
28
+ attributionId: string;
29
+ commissionId?: string;
30
+ }>;
31
+ }
32
+ interface EventResponse {
33
+ ok: boolean;
34
+ eventId: string;
35
+ attribution: EventAttribution;
36
+ }
37
+
38
+ export { type EventResponse as E, type IdentifyResponse as I, OpenPartnerError as O, type EventAttribution as a };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@openpartner/sdk",
3
+ "version": "0.1.0",
4
+ "description": "OpenPartner SDK. Browser client for click → identity stitching + Node server SDK for conversion events and attribution reads.",
5
+ "type": "module",
6
+ "main": "./dist/browser.cjs",
7
+ "module": "./dist/browser.js",
8
+ "types": "./dist/browser.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/browser.d.ts",
12
+ "import": "./dist/browser.js",
13
+ "require": "./dist/browser.cjs"
14
+ },
15
+ "./server": {
16
+ "types": "./dist/server.d.ts",
17
+ "import": "./dist/server.js",
18
+ "require": "./dist/server.cjs"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "keywords": [
29
+ "openpartner",
30
+ "affiliate",
31
+ "attribution",
32
+ "referral",
33
+ "partner",
34
+ "sdk"
35
+ ],
36
+ "homepage": "https://github.com/getcoherence/openpartner/tree/main/packages/sdk",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/getcoherence/openpartner.git",
40
+ "directory": "packages/sdk"
41
+ },
42
+ "license": "MIT",
43
+ "sideEffects": false,
44
+ "devDependencies": {
45
+ "jsdom": "^29.0.2",
46
+ "tsup": "^8.5.1",
47
+ "typescript": "5.4.5",
48
+ "vitest": "^1.6.0"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "dev": "tsup --watch",
53
+ "lint": "tsc --noEmit",
54
+ "test": "vitest run --passWithNoTests"
55
+ }
56
+ }