@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/dist/server.d.ts CHANGED
@@ -121,6 +121,61 @@ declare class OpenPartnerServer {
121
121
  }): Promise<IdentifyResponse>;
122
122
  private request;
123
123
  }
124
+ interface ReferralCookieOptions {
125
+ /** Cookie name. Default `op_ref`, the one the browser SDK reads back. */
126
+ name?: string;
127
+ /** Share it across subdomains, e.g. ".yourbrand.com" (land on www., buy on app.). */
128
+ domain?: string;
129
+ /** Lifetime in days. Default 90. */
130
+ days?: number;
131
+ /** Send it over HTTPS only. Default true; set false for plain-http local development. */
132
+ secure?: boolean;
133
+ /** Query parameter that carries the click on the landing URL. Default `cref`. */
134
+ queryParam?: string;
135
+ }
136
+ /**
137
+ * The `Set-Cookie` header value that keeps a partner click on your own
138
+ * domain, for a request whose URL carries `?cref=`; null otherwise. Send it
139
+ * on the landing page's own response.
140
+ *
141
+ * Why from the server: Safari deletes what a page's scripts store
142
+ * (localStorage, and cookies written through document.cookie) after 7 days
143
+ * without a visit. A cookie your server sets on the page's own response
144
+ * keeps its full lifetime, so a visitor who clicks today and buys in a
145
+ * month, without signing up in between, is still credited. Not HttpOnly:
146
+ * the browser SDK reads it back for identify() and Stripe links. Set it
147
+ * from the server that serves the page itself: Safari caps a cookie from a
148
+ * server on another IP (an API host on a different provider) at 7 days too.
149
+ *
150
+ * const header = referralCookieHeader(req.url, { domain: '.yourbrand.com' });
151
+ * if (header) res.append('Set-Cookie', header);
152
+ */
153
+ declare function referralCookieHeader(url: string, options?: ReferralCookieOptions): string | null;
154
+ /** The slice of Node's request and response the middleware uses. */
155
+ interface CookieRequest {
156
+ url?: string;
157
+ originalUrl?: string;
158
+ }
159
+ interface CookieResponse {
160
+ getHeader(name: string): number | string | string[] | undefined;
161
+ setHeader(name: string, value: string | string[]): unknown;
162
+ }
163
+ /**
164
+ * Express / Connect middleware: sets the referral cookie on every request
165
+ * that arrives with `?cref=`, adding to any Set-Cookie already queued.
166
+ *
167
+ * app.use(referralCookieMiddleware({ domain: '.yourbrand.com' }));
168
+ */
169
+ declare function referralCookieMiddleware(options?: ReferralCookieOptions): (req: CookieRequest, res: CookieResponse, next: () => void) => void;
170
+ /**
171
+ * Read the referral back off a request's `Cookie` header: for identify()
172
+ * from your backend at signup, or `client_reference_id` on a Checkout
173
+ * Session you create. Null when there's none.
174
+ *
175
+ * const cref = readReferralCookie(req.headers.cookie);
176
+ * if (cref) await op.identify({ cref, userId: user.id });
177
+ */
178
+ declare function readReferralCookie(cookieHeader: string | null | undefined, name?: string): string | null;
124
179
 
125
- export { ConversionResponse, EventResponse, IdentifyResponse, OpenPartnerServer };
126
- export type { OpenPartnerServerConfig, TrackConversionParams, TrackEventParams };
180
+ export { ConversionResponse, EventResponse, IdentifyResponse, OpenPartnerServer, readReferralCookie, referralCookieHeader, referralCookieMiddleware };
181
+ export type { OpenPartnerServerConfig, ReferralCookieOptions, TrackConversionParams, TrackEventParams };
package/dist/server.js CHANGED
@@ -97,8 +97,53 @@ var OpenPartnerServer = class {
97
97
  );
98
98
  }
99
99
  };
100
+ var REFERRAL_RE = /^[A-Za-z0-9_-]{1,200}$/;
101
+ function referralCookieHeader(url, options = {}) {
102
+ let referral;
103
+ try {
104
+ referral = new URL(url, "http://localhost").searchParams.get(options.queryParam ?? "cref");
105
+ } catch {
106
+ return null;
107
+ }
108
+ if (!referral || !REFERRAL_RE.test(referral)) return null;
109
+ const name = options.name ?? "op_ref";
110
+ const maxAge = Math.round((options.days ?? 90) * 86400);
111
+ const parts = [`${name}=${referral}`, `Max-Age=${maxAge}`, "Path=/", "SameSite=Lax"];
112
+ if (options.domain) parts.push(`Domain=${options.domain}`);
113
+ if (options.secure !== false) parts.push("Secure");
114
+ return parts.join("; ");
115
+ }
116
+ function referralCookieMiddleware(options = {}) {
117
+ return (req, res, next) => {
118
+ const header = referralCookieHeader(req.originalUrl ?? req.url ?? "", options);
119
+ if (header) {
120
+ const existing = res.getHeader("set-cookie");
121
+ const list = existing == null ? [] : Array.isArray(existing) ? existing : [String(existing)];
122
+ res.setHeader("set-cookie", [...list, header]);
123
+ }
124
+ next();
125
+ };
126
+ }
127
+ function readReferralCookie(cookieHeader, name = "op_ref") {
128
+ if (!cookieHeader) return null;
129
+ for (const part of cookieHeader.split(";")) {
130
+ const eq = part.indexOf("=");
131
+ if (eq === -1 || part.slice(0, eq).trim() !== name) continue;
132
+ let value;
133
+ try {
134
+ value = decodeURIComponent(part.slice(eq + 1).trim());
135
+ } catch {
136
+ return null;
137
+ }
138
+ return REFERRAL_RE.test(value) ? value : null;
139
+ }
140
+ return null;
141
+ }
100
142
  export {
101
143
  OpenPartnerError,
102
- OpenPartnerServer
144
+ OpenPartnerServer,
145
+ readReferralCookie,
146
+ referralCookieHeader,
147
+ referralCookieMiddleware
103
148
  };
104
149
  //# sourceMappingURL=server.js.map
@@ -1 +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 /**\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","/**\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 — externalEventId makes retries safe:\n * await op.trackEvent({ userId, type: 'invoice_paid', value: 249, currency: 'USD', externalEventId: invoice.id });\n *\n * // Holding only the click id (from the link's {click_id} macro)? One call:\n * await op.trackConversion({ clickId, type: 'loan_funded', value: 5000, externalEventId: `${loan.id}:funded` });\n */\n\nimport {\n OpenPartnerError,\n requestJson,\n type ConversionResponse,\n type EventResponse,\n type IdentifyResponse,\n type JsonRequestInit,\n} from './shared.js';\n\nexport { OpenPartnerError };\nexport type { ConversionResponse, 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. Event ingest (/attribution/events) accepts\n * an admin key or a scoped key with `events:write`. Never expose this\n * 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 * Idempotency key: your system's stable id for this one event (order id,\n * invoice id, CRM deal-stage change id), max 120 chars. Strongly\n * recommended. A retried call with the same key returns the original\n * event and attribution (`replayed: true`) instead of recording and\n * commissioning it twice. Unique per event, not per order: an order's\n * `subscription_created` and its `invoice_paid` need different keys.\n * Reusing a key for a different userId / type / value / currency throws\n * an `OpenPartnerError` with code `external_event_id_conflict` (409).\n */\n externalEventId?: string;\n}\n\nexport interface TrackConversionParams {\n /** The click id the partner link handed you: the `{click_id}` macro in the\n * destination URL, or the `cref` query parameter. */\n clickId: string;\n /** Event type, e.g. 'loan_funded', 'opportunity_won', 'invoice_paid'. */\n type: string;\n /** Value of the conversion, for percent-commission rules. */\n value?: number;\n /** ISO 4217 currency code. Defaults to USD on the server if omitted. */\n currency?: string;\n /**\n * Required idempotency key: your stable id for this one conversion (loan\n * id plus stage, order id), max 120 chars. A retry returns the original\n * result (`replayed: true`); reusing it for a different click or\n * conversion throws `external_event_id_conflict` (409).\n */\n externalEventId: string;\n /**\n * Your own id for the user, if you have one. Omit it and the conversion is\n * recorded under the click's own user, credited to that click alone.\n */\n userId?: string;\n metadata?: Record<string, unknown>;\n /** Event time. Defaults to now on the server. ISO 8601. */\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 * Record a conversion against a click in one call, for when you have the\n * click id but no identify()'d user: a lender reporting a funded loan, a\n * CRM closing a deal. Stitches the click and records the event together.\n * Throws `click_not_found` (404) for a click outside your program and\n * `click_flagged` (422) for one flagged as fraud.\n */\n async trackConversion(params: TrackConversionParams): Promise<ConversionResponse> {\n return this.request<ConversionResponse>('/attribution/conversions', { 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;AAwDA,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;;;ACZO,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;AAAA,EASA,MAAM,gBAAgB,QAA4D;AAChF,WAAO,KAAK,QAA4B,4BAA4B,EAAE,QAAQ,QAAQ,MAAM,OAAO,CAAC;AAAA,EACtG;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":[]}
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 /**\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","/**\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 — externalEventId makes retries safe:\n * await op.trackEvent({ userId, type: 'invoice_paid', value: 249, currency: 'USD', externalEventId: invoice.id });\n *\n * // Holding only the click id (from the link's {click_id} macro)? One call:\n * await op.trackConversion({ clickId, type: 'loan_funded', value: 5000, externalEventId: `${loan.id}:funded` });\n */\n\nimport {\n OpenPartnerError,\n requestJson,\n type ConversionResponse,\n type EventResponse,\n type IdentifyResponse,\n type JsonRequestInit,\n} from './shared.js';\n\nexport { OpenPartnerError };\nexport type { ConversionResponse, 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. Event ingest (/attribution/events) accepts\n * an admin key or a scoped key with `events:write`. Never expose this\n * 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 * Idempotency key: your system's stable id for this one event (order id,\n * invoice id, CRM deal-stage change id), max 120 chars. Strongly\n * recommended. A retried call with the same key returns the original\n * event and attribution (`replayed: true`) instead of recording and\n * commissioning it twice. Unique per event, not per order: an order's\n * `subscription_created` and its `invoice_paid` need different keys.\n * Reusing a key for a different userId / type / value / currency throws\n * an `OpenPartnerError` with code `external_event_id_conflict` (409).\n */\n externalEventId?: string;\n}\n\nexport interface TrackConversionParams {\n /** The click id the partner link handed you: the `{click_id}` macro in the\n * destination URL, or the `cref` query parameter. */\n clickId: string;\n /** Event type, e.g. 'loan_funded', 'opportunity_won', 'invoice_paid'. */\n type: string;\n /** Value of the conversion, for percent-commission rules. */\n value?: number;\n /** ISO 4217 currency code. Defaults to USD on the server if omitted. */\n currency?: string;\n /**\n * Required idempotency key: your stable id for this one conversion (loan\n * id plus stage, order id), max 120 chars. A retry returns the original\n * result (`replayed: true`); reusing it for a different click or\n * conversion throws `external_event_id_conflict` (409).\n */\n externalEventId: string;\n /**\n * Your own id for the user, if you have one. Omit it and the conversion is\n * recorded under the click's own user, credited to that click alone.\n */\n userId?: string;\n metadata?: Record<string, unknown>;\n /** Event time. Defaults to now on the server. ISO 8601. */\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 * Record a conversion against a click in one call, for when you have the\n * click id but no identify()'d user: a lender reporting a funded loan, a\n * CRM closing a deal. Stitches the click and records the event together.\n * Throws `click_not_found` (404) for a click outside your program and\n * `click_flagged` (422) for one flagged as fraud.\n */\n async trackConversion(params: TrackConversionParams): Promise<ConversionResponse> {\n return this.request<ConversionResponse>('/attribution/conversions', { 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// ---------------------------------------------------------------------------\n// The referral cookie, set from your own server\n// ---------------------------------------------------------------------------\n\nexport interface ReferralCookieOptions {\n /** Cookie name. Default `op_ref`, the one the browser SDK reads back. */\n name?: string;\n /** Share it across subdomains, e.g. \".yourbrand.com\" (land on www., buy on app.). */\n domain?: string;\n /** Lifetime in days. Default 90. */\n days?: number;\n /** Send it over HTTPS only. Default true; set false for plain-http local development. */\n secure?: boolean;\n /** Query parameter that carries the click on the landing URL. Default `cref`. */\n queryParam?: string;\n}\n\n/** A click id, or anything Stripe would accept as client_reference_id. */\nconst REFERRAL_RE = /^[A-Za-z0-9_-]{1,200}$/;\n\n/**\n * The `Set-Cookie` header value that keeps a partner click on your own\n * domain, for a request whose URL carries `?cref=`; null otherwise. Send it\n * on the landing page's own response.\n *\n * Why from the server: Safari deletes what a page's scripts store\n * (localStorage, and cookies written through document.cookie) after 7 days\n * without a visit. A cookie your server sets on the page's own response\n * keeps its full lifetime, so a visitor who clicks today and buys in a\n * month, without signing up in between, is still credited. Not HttpOnly:\n * the browser SDK reads it back for identify() and Stripe links. Set it\n * from the server that serves the page itself: Safari caps a cookie from a\n * server on another IP (an API host on a different provider) at 7 days too.\n *\n * const header = referralCookieHeader(req.url, { domain: '.yourbrand.com' });\n * if (header) res.append('Set-Cookie', header);\n */\nexport function referralCookieHeader(url: string, options: ReferralCookieOptions = {}): string | null {\n let referral: string | null;\n try {\n referral = new URL(url, 'http://localhost').searchParams.get(options.queryParam ?? 'cref');\n } catch {\n return null;\n }\n if (!referral || !REFERRAL_RE.test(referral)) return null;\n const name = options.name ?? 'op_ref';\n const maxAge = Math.round((options.days ?? 90) * 86400);\n const parts = [`${name}=${referral}`, `Max-Age=${maxAge}`, 'Path=/', 'SameSite=Lax'];\n if (options.domain) parts.push(`Domain=${options.domain}`);\n if (options.secure !== false) parts.push('Secure');\n return parts.join('; ');\n}\n\n/** The slice of Node's request and response the middleware uses. */\ninterface CookieRequest {\n url?: string;\n originalUrl?: string;\n}\ninterface CookieResponse {\n getHeader(name: string): number | string | string[] | undefined;\n setHeader(name: string, value: string | string[]): unknown;\n}\n\n/**\n * Express / Connect middleware: sets the referral cookie on every request\n * that arrives with `?cref=`, adding to any Set-Cookie already queued.\n *\n * app.use(referralCookieMiddleware({ domain: '.yourbrand.com' }));\n */\nexport function referralCookieMiddleware(options: ReferralCookieOptions = {}) {\n return (req: CookieRequest, res: CookieResponse, next: () => void): void => {\n const header = referralCookieHeader(req.originalUrl ?? req.url ?? '', options);\n if (header) {\n const existing = res.getHeader('set-cookie');\n const list = existing == null ? [] : Array.isArray(existing) ? existing : [String(existing)];\n res.setHeader('set-cookie', [...list, header]);\n }\n next();\n };\n}\n\n/**\n * Read the referral back off a request's `Cookie` header: for identify()\n * from your backend at signup, or `client_reference_id` on a Checkout\n * Session you create. Null when there's none.\n *\n * const cref = readReferralCookie(req.headers.cookie);\n * if (cref) await op.identify({ cref, userId: user.id });\n */\nexport function readReferralCookie(cookieHeader: string | null | undefined, name = 'op_ref'): string | null {\n if (!cookieHeader) return null;\n for (const part of cookieHeader.split(';')) {\n const eq = part.indexOf('=');\n if (eq === -1 || part.slice(0, eq).trim() !== name) continue;\n let value: string;\n try {\n value = decodeURIComponent(part.slice(eq + 1).trim());\n } catch {\n return null;\n }\n return REFERRAL_RE.test(value) ? value : null;\n }\n return null;\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- "use strict";(()=>{var R=Object.defineProperty,v=Object.defineProperties;var E=Object.getOwnPropertyDescriptors;var m=Object.getOwnPropertySymbols;var C=Object.prototype.hasOwnProperty,I=Object.prototype.propertyIsEnumerable;var g=(r,e,t)=>e in r?R(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,f=(r,e)=>{for(var t in e||(e={}))C.call(e,t)&&g(r,t,e[t]);if(m)for(var t of m(e))I.call(e,t)&&g(r,t,e[t]);return r},h=(r,e)=>v(r,E(e));var l=(r,e,t)=>g(r,typeof e!="symbol"?e+"":e,t);var p=class extends Error{constructor(t,n,i,s){super(t);l(this,"status",n);l(this,"code",i);l(this,"detail",s);this.name="OpenPartnerError"}};async function y(r,e={},t=globalThis.fetch){var a,c;let n=new Headers(e.headers);e.body!==void 0&&!n.has("content-type")&&n.set("content-type","application/json");let i=await t(r,h(f({},e),{headers:n,body:e.body===void 0?void 0:typeof e.body=="string"?e.body:JSON.stringify(e.body)})),s=await i.text(),o;if(s)try{o=JSON.parse(s)}catch(u){o=s}if(!i.ok){let u=o!=null?o:{};throw new p((a=u.error)!=null?a:`${i.status} ${i.statusText}`,i.status,u.error,(c=u.detail)!=null?c:o)}return o}var P={cookieName:"_cref",queryParam:"cref",storageKey:"openpartner:cref",stripe:!0,referralCookie:!1,onError:r=>{console.warn("[openpartner] identify failed",r)}},b=/^[A-Za-z0-9_-]{1,200}$/,k="stripe-pricing-table, stripe-buy-button",d=class r{constructor(e){l(this,"config");l(this,"observer",null);this.config=f(f({},P),e),this.captureCref(),this.writeReferralCookie(),this.config.stripe!==!1&&this.watchStripe()}static init(e){return new r(e)}getCref(){return typeof localStorage=="undefined"?null:localStorage.getItem(this.config.storageKey)}getReferral(){return this.getCref()}clear(){typeof localStorage!="undefined"&&localStorage.removeItem(this.config.storageKey)}linkStripe(e=document){let t=this.getReferral();if(!t||!b.test(t)||typeof document=="undefined")return 0;let n=new Set(["buy.stripe.com",...this.stripeLinkHosts()]),i=0,s=[];e instanceof HTMLAnchorElement&&s.push(e),s.push(...Array.from(e.querySelectorAll("a[href]")));for(let a of s){let c;try{c=new URL(a.href)}catch(u){continue}!n.has(c.hostname.toLowerCase())||c.searchParams.has("client_reference_id")||(c.searchParams.set("client_reference_id",t),a.href=c.toString(),i+=1)}let o=[];e instanceof Element&&e.matches(k)&&o.push(e),o.push(...Array.from(e.querySelectorAll(k)));for(let a of o)a.hasAttribute("client-reference-id")||(a.setAttribute("client-reference-id",t),i+=1);return i}stopLinkingStripe(){var e;(e=this.observer)==null||e.disconnect(),this.observer=null}stripeLinkHosts(){let e=this.config.stripe;return typeof e=="object"&&e.linkHosts?e.linkHosts.map(t=>t.trim().toLowerCase()).filter(Boolean):[]}watchStripe(){typeof document=="undefined"||!this.getReferral()||(this.linkStripe(document),typeof MutationObserver!="undefined"&&(this.observer=new MutationObserver(e=>{for(let t of e){if(t.type==="attributes"){t.target instanceof HTMLAnchorElement&&this.linkStripe(t.target);continue}for(let n of Array.from(t.addedNodes))n instanceof Element&&this.linkStripe(n)}}),this.observer.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["href"]})))}captureCref(){if(typeof window=="undefined"||typeof localStorage=="undefined")return;let t=new URL(window.location.href).searchParams.get(this.config.queryParam);if(t){localStorage.setItem(this.config.storageKey,t);return}let n=this.readCookie(this.config.cookieName);n&&!localStorage.getItem(this.config.storageKey)&&localStorage.setItem(this.config.storageKey,n)}writeReferralCookie(){var a,c;let e=this.config.referralCookie;if(!e||typeof document=="undefined")return;let t=this.getReferral();if(!t||!b.test(t))return;let n=(a=e.name)!=null?a:"op_ref",i=Math.round(((c=e.days)!=null?c:90)*86400),s=typeof location!="undefined"&&location.protocol==="https:"?"; Secure":"",o=e.domain?`; Domain=${e.domain}`:"";document.cookie=`${n}=${encodeURIComponent(t)}; Max-Age=${i}; Path=/; SameSite=Lax${o}${s}`}readCookie(e){if(typeof document=="undefined")return null;let t=document.cookie.match(new RegExp(`(^|; )${e}=([^;]+)`));return t?decodeURIComponent(t[2]):null}async identify(e){let t=this.getCref();if(!t)return null;try{return await y(`${this.config.apiUrl}/attribution/identify`,{method:"POST",body:{cref:t,userId:e,ts:Date.now()},keepalive:!0})}catch(n){return this.config.onError(n),null}}};function x(r){var i,s;let e=r.dataset,t=((i=e.apiUrl)!=null?i:"").trim();if(!t)try{t=`${new URL(r.src).origin}/api`}catch(o){t="/api"}let n=((s=e.stripeHosts)!=null?s:"").split(",").map(o=>o.trim()).filter(Boolean);return{apiUrl:t.replace(/\/$/,""),stripe:e.stripe==="off"?!1:{linkHosts:n},referralCookie:e.cookie==="off"?!1:f({name:"op_ref"},e.cookieDomain?{domain:e.cookieDomain}:{})}}var w,S;if(typeof window!="undefined"&&typeof document!="undefined"&&!window.openpartner){let r=document.currentScript;window.OpenPartner=d,window.openpartner=d.init(x({src:(w=r==null?void 0:r.src)!=null?w:window.location.href,dataset:f({},(S=r==null?void 0:r.dataset)!=null?S:{})}))}})();
1
+ "use strict";(()=>{var C=Object.defineProperty,S=Object.defineProperties;var R=Object.getOwnPropertyDescriptors;var y=Object.getOwnPropertySymbols;var E=Object.prototype.hasOwnProperty,L=Object.prototype.propertyIsEnumerable;var g=(n,e,t)=>e in n?C(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t,l=(n,e)=>{for(var t in e||(e={}))E.call(e,t)&&g(n,t,e[t]);if(y)for(var t of y(e))L.call(e,t)&&g(n,t,e[t]);return n},b=(n,e)=>S(n,R(e));var c=(n,e,t)=>g(n,typeof e!="symbol"?e+"":e,t);var u=class extends Error{constructor(t,r,i,a){super(t);c(this,"status",r);c(this,"code",i);c(this,"detail",a);this.name="OpenPartnerError"}};async function h(n,e={},t=globalThis.fetch){var s,f;let r=new Headers(e.headers);e.body!==void 0&&!r.has("content-type")&&r.set("content-type","application/json");let i=await t(n,b(l({},e),{headers:r,body:e.body===void 0?void 0:typeof e.body=="string"?e.body:JSON.stringify(e.body)})),a=await i.text(),o;if(a)try{o=JSON.parse(a)}catch(d){o=a}if(!i.ok){let d=o!=null?o:{};throw new u((s=d.error)!=null?s:`${i.status} ${i.statusText}`,i.status,d.error,(f=d.detail)!=null?f:o)}return o}var P={cookieName:"_cref",queryParam:"cref",storageKey:"openpartner:cref",stripe:!0,referralCookie:!1,consent:"granted",reportLanding:!0,onError:n=>{console.warn("[openpartner] request failed",n)}},m=/^[A-Za-z0-9_-]{1,200}$/,k="stripe-pricing-table, stripe-buy-button",A="op_ref",p=class n{constructor(e){c(this,"config");c(this,"observer",null);c(this,"consent");c(this,"held",null);c(this,"current",null);c(this,"tagged",[]);this.config=l(l({},P),e),this.consent=this.config.consent,this.consent==="granted"?this.start():this.consent==="pending"?this.held=this.queryCref():this.clear()}static init(e){return new n(e)}getCref(){var e,t;return this.consent!=="granted"?null:(t=(e=this.storageGet())!=null?e:this.current)!=null?t:this.readCookie(this.referralCookieName())}getReferral(){return this.getCref()}clear(){if(this.held=null,this.current=null,this.untagStripe(),this.storageRemove(),typeof document=="undefined")return;let e=this.referralCookieName();if(!this.readCookie(e))return;let t=this.config.referralCookie;document.cookie=`${e}=; Max-Age=0; Path=/`,t&&t.domain&&(document.cookie=`${e}=; Max-Age=0; Path=/; Domain=${t.domain}`)}setConsent(e){if(e==="granted"){if(this.consent==="granted")return;this.consent=e,this.start();return}this.consent=e,this.stopLinkingStripe(),this.clear()}linkStripe(e=document){let t=this.getReferral();if(!t||!m.test(t)||typeof document=="undefined")return 0;let r=new Set(["buy.stripe.com",...this.stripeLinkHosts()]),i=0,a=[];e instanceof HTMLAnchorElement&&a.push(e),a.push(...Array.from(e.querySelectorAll("a[href]")));for(let s of a){let f;try{f=new URL(s.href)}catch(d){continue}!r.has(f.hostname.toLowerCase())||f.searchParams.has("client_reference_id")||(f.searchParams.set("client_reference_id",t),s.href=f.toString(),this.remember(s,t),i+=1)}let o=[];e instanceof Element&&e.matches(k)&&o.push(e),o.push(...Array.from(e.querySelectorAll(k)));for(let s of o)s.hasAttribute("client-reference-id")||(s.setAttribute("client-reference-id",t),this.remember(s,t),i+=1);return i}remember(e,t){this.tagged=this.tagged.filter(r=>r.el.isConnected),this.tagged.push({el:e,value:t})}untagStripe(){for(let{el:e,value:t}of this.tagged)if(e instanceof HTMLAnchorElement){let r;try{r=new URL(e.href)}catch(i){continue}if(r.searchParams.get("client_reference_id")!==t)continue;r.searchParams.delete("client_reference_id"),e.href=r.toString()}else e.getAttribute("client-reference-id")===t&&e.removeAttribute("client-reference-id");this.tagged=[]}stopLinkingStripe(){var e;(e=this.observer)==null||e.disconnect(),this.observer=null}stripeLinkHosts(){let e=this.config.stripe;return typeof e=="object"&&e.linkHosts?e.linkHosts.map(t=>t.trim().toLowerCase()).filter(Boolean):[]}start(){let e=this.captureCref(this.held);this.held=null,this.writeReferralCookie(),this.config.stripe!==!1&&this.watchStripe(),e&&this.config.reportLanding!==!1&&this.reportLanding(e)}watchStripe(){typeof document=="undefined"||this.observer||!this.getReferral()||(this.linkStripe(document),typeof MutationObserver!="undefined"&&(this.observer=new MutationObserver(e=>{for(let t of e){if(t.type==="attributes"){t.target instanceof HTMLAnchorElement&&this.linkStripe(t.target);continue}for(let r of Array.from(t.addedNodes))r instanceof Element&&this.linkStripe(r)}}),this.observer.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["href"]})))}queryCref(){return typeof window=="undefined"?null:new URL(window.location.href).searchParams.get(this.config.queryParam)}captureCref(e){var i;if(typeof window=="undefined")return null;let t=e!=null?e:this.queryCref();if(t){let a=this.storageGet()!==t;return this.current=t,this.storageSet(t),a?t:null}if(this.storageGet())return null;let r=(i=this.readCookie(this.config.cookieName))!=null?i:this.readCookie(this.referralCookieName());return r&&(this.current=r,this.storageSet(r)),null}referralCookieName(){let e=this.config.referralCookie;return e&&e.name||A}writeReferralCookie(){var s;let e=this.config.referralCookie;if(!e||typeof document=="undefined")return;let t=this.getReferral();if(!t||!m.test(t))return;let r=this.referralCookieName();if(this.readCookie(r)===t)return;let i=Math.round(((s=e.days)!=null?s:90)*86400),a=typeof location!="undefined"&&location.protocol==="https:"?"; Secure":"",o=e.domain?`; Domain=${e.domain}`:"";document.cookie=`${r}=${encodeURIComponent(t)}; Max-Age=${i}; Path=/; SameSite=Lax${o}${a}`}readCookie(e){if(typeof document=="undefined")return null;let t=document.cookie.match(new RegExp(`(^|; )${e}=([^;]+)`));if(!t)return null;try{return decodeURIComponent(t[2])}catch(r){return null}}storageGet(){try{return typeof localStorage=="undefined"?null:localStorage.getItem(this.config.storageKey)}catch(e){return null}}storageSet(e){try{typeof localStorage!="undefined"&&localStorage.setItem(this.config.storageKey,e)}catch(t){}}storageRemove(){try{typeof localStorage!="undefined"&&localStorage.removeItem(this.config.storageKey)}catch(e){}}async reportLanding(e){if(!(!m.test(e)||typeof fetch=="undefined"))try{await h(`${this.config.apiUrl}/attribution/landing`,{method:"POST",body:{cref:e},keepalive:!0})}catch(t){t instanceof u&&t.code==="click_not_found"&&this.config.onError(t)}}async identify(e){let t=this.getCref();if(!t)return null;try{return await h(`${this.config.apiUrl}/attribution/identify`,{method:"POST",body:{cref:t,userId:e,ts:Date.now()},keepalive:!0})}catch(r){return this.config.onError(r),null}}};function x(n){var a,o;let e=n.dataset,t=((a=e.apiUrl)!=null?a:"").trim();if(!t)try{t=`${new URL(n.src).origin}/api`}catch(s){t="/api"}let r=((o=e.stripeHosts)!=null?o:"").split(",").map(s=>s.trim()).filter(Boolean),i=e.consent==="pending"||e.consent==="denied"?e.consent:"granted";return{apiUrl:t.replace(/\/$/,""),stripe:e.stripe==="off"?!1:{linkHosts:r},referralCookie:e.cookie==="off"?!1:l({name:"op_ref"},e.cookieDomain?{domain:e.cookieDomain}:{}),consent:i,reportLanding:e.landing!=="off"}}var v,w;if(typeof window!="undefined"&&typeof document!="undefined"&&!window.openpartner){let n=document.currentScript;window.OpenPartner=p,window.openpartner=p.init(x({src:(v=n==null?void 0:n.src)!=null?v:window.location.href,dataset:l({},(w=n==null?void 0:n.dataset)!=null?w:{})}))}})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpartner/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "OpenPartner SDK. Browser client for click → identity stitching + Node server SDK for conversion events and attribution reads.",
5
5
  "type": "module",
6
6
  "main": "./dist/browser.cjs",
@@ -38,7 +38,8 @@
38
38
  "files": [
39
39
  "dist",
40
40
  "dist-global",
41
- "README.md"
41
+ "README.md",
42
+ "CHANGELOG.md"
42
43
  ],
43
44
  "publishConfig": {
44
45
  "access": "public"