@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 +21 -0
- package/README.md +151 -0
- package/dist/browser.cjs +166 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +76 -0
- package/dist/browser.d.ts +76 -0
- package/dist/browser.js +140 -0
- package/dist/browser.js.map +1 -0
- package/dist/server.cjs +120 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +77 -0
- package/dist/server.d.ts +77 -0
- package/dist/server.js +94 -0
- package/dist/server.js.map +1 -0
- package/dist/shared-CLfBjPeb.d.cts +38 -0
- package/dist/shared-CLfBjPeb.d.ts +38 -0
- package/package.json +56 -0
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.
|
package/dist/browser.cjs
ADDED
|
@@ -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":"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","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 };
|
package/dist/browser.js
ADDED
|
@@ -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":[]}
|
package/dist/server.cjs
ADDED
|
@@ -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 };
|
package/dist/server.d.ts
ADDED
|
@@ -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
|
+
}
|