sellfolk 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/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # sellfolk
2
+
3
+ Two-line tracking SDK for [Sellfolk](https://sellfolk.com). Attribute signups, trials and paid conversions back to the seller who sent the visitor.
4
+
5
+ - ✅ **2 KB minified** browser bundle (sub-1 KB gzipped)
6
+ - ✅ **Zero dependencies** on the server SDK (Node 20+ native fetch)
7
+ - ✅ **`keepalive: true`** — fire-and-forget event delivery, survives page unload
8
+ - ✅ **`localStorage`** with 60-day ref expiry
9
+ - ✅ **Never throws** — silent failure path. Your SaaS keeps running.
10
+ - ESM + CJS + minified CDN bundle
11
+
12
+ ---
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install sellfolk
18
+ pnpm add sellfolk
19
+ yarn add sellfolk
20
+ ```
21
+
22
+ Or drop the CDN bundle into your HTML (see [Plain HTML](#plain-html) below).
23
+
24
+ ---
25
+
26
+ ## Quickstart — browser
27
+
28
+ ```ts
29
+ import sellfolk from 'sellfolk'
30
+
31
+ // One-line init. Do this at the root of your app.
32
+ sellfolk.init('sk_live_acme_a8e92f...')
33
+
34
+ // Track any event. sellfolk auto-attaches the ref_id captured from ?ref= on init.
35
+ sellfolk.track('signup', { email: 'jane@stripe.com' })
36
+
37
+ // Fire when real money moves.
38
+ sellfolk.conversion({ amount: 199.00, email: 'jane@stripe.com' })
39
+ ```
40
+
41
+ The first time a visitor lands on your site via `https://yoursite.com?ref=jane123-acme`, sellfolk captures `jane123-acme` and stores it in `localStorage` for 60 days. Every subsequent `track()` or `conversion()` call automatically attaches that ref id — even on different pages, days later, after sign-in.
42
+
43
+ ---
44
+
45
+ ## Framework recipes
46
+
47
+ ### Vue 3 / Nuxt
48
+
49
+ ```ts
50
+ // plugins/sellfolk.client.ts
51
+ import sellfolk from 'sellfolk'
52
+
53
+ export default defineNuxtPlugin(() => {
54
+ sellfolk.init('sk_live_acme_a8e92f...')
55
+ })
56
+
57
+ // Then anywhere in your components:
58
+ import sellfolk from 'sellfolk'
59
+ sellfolk.track('signup', { email: user.value.email })
60
+ ```
61
+
62
+ ### React / Next.js
63
+
64
+ ```tsx
65
+ // app/providers.tsx (Next 13+) or a top-level layout
66
+ 'use client'
67
+ import { useEffect } from 'react'
68
+ import sellfolk from 'sellfolk'
69
+
70
+ export function SaaSBoostProvider({ children }: { children: React.ReactNode }) {
71
+ useEffect(() => {
72
+ sellfolk.init('sk_live_acme_a8e92f...')
73
+ }, [])
74
+ return children
75
+ }
76
+
77
+ // In a component:
78
+ import sellfolk from 'sellfolk'
79
+
80
+ function SignupForm() {
81
+ async function onSubmit(email: string) {
82
+ await fetch('/api/signup', { method: 'POST', body: JSON.stringify({ email }) })
83
+ sellfolk.track('signup', { email })
84
+ }
85
+ ...
86
+ }
87
+ ```
88
+
89
+ ### Plain HTML
90
+
91
+ ```html
92
+ <script src="https://cdn.jsdelivr.net/npm/sellfolk/dist/sellfolk.min.js"></script>
93
+ <script>
94
+ sellfolk.init('sk_live_acme_a8e92f...')
95
+ sellfolk.track('signup', { email: 'jane@stripe.com' })
96
+ </script>
97
+ ```
98
+
99
+ The IIFE bundle exposes a global `sellfolk`.
100
+
101
+ ---
102
+
103
+ ## Server SDK
104
+
105
+ Use the server SDK from inside your billing webhook so conversions are reported the moment money moves — they don't depend on a browser session.
106
+
107
+ ```ts
108
+ import { SellfolkServer } from 'sellfolk/server'
109
+
110
+ const sb = new SellfolkServer(process.env.SELLFOLK_KEY!)
111
+
112
+ // Inside your Stripe / Polar / Lemonsqueezy webhook handler:
113
+ export async function handleOrderPaid(order) {
114
+ await sb.conversion({
115
+ email: order.customer_email,
116
+ amount: order.amount_total / 100,
117
+ currency: order.currency,
118
+ })
119
+ }
120
+ ```
121
+
122
+ The server SDK uses Node 20+'s native fetch — **zero runtime deps**.
123
+
124
+ By default it never throws. If you want errors to propagate (e.g. so your queue retries):
125
+
126
+ ```ts
127
+ const sb = new SellfolkServer(key, { throwOnError: true })
128
+ ```
129
+
130
+ ---
131
+
132
+ ## API reference
133
+
134
+ ### Browser
135
+
136
+ #### `sellfolk.init(apiKey, options?)`
137
+
138
+ Initialize the SDK. Captures `?ref=` from the URL if present (otherwise reads the stored ref) and prepares the network client.
139
+
140
+ | Option | Type | Default | Description |
141
+ | --------------------- | -------- | -------------------------------- | ------------------------------------------------- |
142
+ | `apiBase` | string | `https://api.sellfolk.com` | Override (testing / self-hosted backends). |
143
+ | `forceClearOnEmptyUrl`| boolean | `false` | When true and URL has no `?ref=`, wipe stored ref.|
144
+ | `now` | function | `Date.now` | Deterministic clock (mostly for tests). |
145
+
146
+ #### `sellfolk.track(event, payload?)`
147
+
148
+ Send a non-conversion event. Common values: `'signup'`, `'trial'`, `'demo'`, `'newsletter'`. Custom names are allowed and recorded as `type='custom'` with `customName=<event>`.
149
+
150
+ #### `sellfolk.conversion(payload)`
151
+
152
+ Fire a conversion. `amount` is required. The backend attributes this to the stored ref id.
153
+
154
+ | Field | Type | Required | Notes |
155
+ | ---------- | -------- | -------- | ---------------------------------------------- |
156
+ | `amount` | number | ✅ | In your store's currency. |
157
+ | `email` | string | optional | Privacy-truncated server-side before display. |
158
+ | `currency` | string | optional | 3-letter ISO code. Defaults to USD server-side.|
159
+
160
+ #### `sellfolk.getRef()` / `sellfolk.clearRef()`
161
+
162
+ Read or clear the currently-stored ref id. Useful for "Was this user referred?" UI or to drop attribution on logout.
163
+
164
+ ### Server
165
+
166
+ #### `new SellfolkServer(apiKey, options?)`
167
+
168
+ | Option | Type | Default |
169
+ | -------------- | --------- | ----------------------------- |
170
+ | `apiBase` | string | `https://api.sellfolk.com` |
171
+ | `throwOnError` | boolean | `false` |
172
+ | `fetch` | function | `globalThis.fetch` |
173
+
174
+ #### `sb.track(event, payload?)` / `sb.conversion(payload)`
175
+
176
+ Same shape as the browser SDK. Both are async.
177
+
178
+ ---
179
+
180
+ ## How attribution works
181
+
182
+ 1. A seller shares `https://sellfolk.com/r/jane123-acme`
183
+ 2. The visitor clicks. Sellfolk logs the click and 302-redirects to `https://yoursite.com?ref=jane123-acme`
184
+ 3. Your site loads. `sellfolk.init()` captures `jane123-acme` and stores it in `localStorage` for 60 days.
185
+ 4. The visitor browses, signs up, starts a trial, eventually pays.
186
+ 5. Each event (`signup`, `trial`, `conversion`) is reported with the stored ref id.
187
+ 6. Sellfolk attributes the conversion to `jane` and pays her per your listing's terms.
188
+
189
+ If the visitor returns from a different referral link later, the most recent click wins. If they wipe their browser storage, the chain is broken — that's by design (and matches every other attribution tool).
190
+
191
+ ---
192
+
193
+ ## License
194
+
195
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,169 @@
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 __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ DEFAULT_API_BASE: () => DEFAULT_API_BASE,
24
+ REF_STORAGE_KEY: () => REF_STORAGE_KEY,
25
+ REF_TTL_MS: () => REF_TTL_MS,
26
+ clearStoredRef: () => clearStoredRef,
27
+ default: () => src_default,
28
+ extractRefFromUrl: () => extractRefFromUrl,
29
+ readStoredRef: () => readStoredRef,
30
+ writeStoredRef: () => writeStoredRef
31
+ });
32
+ module.exports = __toCommonJS(src_exports);
33
+ var DEFAULT_API_BASE = "https://api.sellfolk.com";
34
+ var REF_STORAGE_KEY = "sellfolk_ref";
35
+ var REF_TTL_MS = 60 * 24 * 60 * 60 * 1e3;
36
+ var state = {
37
+ apiKey: null,
38
+ apiBase: DEFAULT_API_BASE,
39
+ refId: null,
40
+ now: () => Date.now()
41
+ };
42
+ function readStoredRef(now = Date.now) {
43
+ try {
44
+ if (typeof localStorage === "undefined") return null;
45
+ const raw = localStorage.getItem(REF_STORAGE_KEY);
46
+ if (!raw) return null;
47
+ const parsed = JSON.parse(raw);
48
+ if (!parsed || typeof parsed.refId !== "string" || typeof parsed.expiresAt !== "number") return null;
49
+ if (now() > parsed.expiresAt) {
50
+ try {
51
+ localStorage.removeItem(REF_STORAGE_KEY);
52
+ } catch {
53
+ }
54
+ return null;
55
+ }
56
+ return parsed.refId;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+ function writeStoredRef(refId, now = Date.now) {
62
+ try {
63
+ if (typeof localStorage === "undefined") return;
64
+ const stored = { refId, expiresAt: now() + REF_TTL_MS };
65
+ localStorage.setItem(REF_STORAGE_KEY, JSON.stringify(stored));
66
+ } catch {
67
+ }
68
+ }
69
+ function clearStoredRef() {
70
+ try {
71
+ if (typeof localStorage === "undefined") return;
72
+ localStorage.removeItem(REF_STORAGE_KEY);
73
+ } catch {
74
+ }
75
+ }
76
+ function extractRefFromUrl(href) {
77
+ try {
78
+ const url = href ?? (typeof window !== "undefined" ? window.location.href : "");
79
+ if (!url) return null;
80
+ const u = new URL(url);
81
+ const ref = u.searchParams.get("ref");
82
+ return ref && ref.length > 0 && ref.length <= 80 ? ref : null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ function dispatch(path, body) {
88
+ if (!state.apiKey) return;
89
+ try {
90
+ fetch(`${state.apiBase}${path}`, {
91
+ method: "POST",
92
+ headers: {
93
+ "Content-Type": "application/json",
94
+ "X-API-Key": state.apiKey
95
+ },
96
+ body: JSON.stringify({ ...body, refId: state.refId }),
97
+ keepalive: true
98
+ }).catch(() => {
99
+ });
100
+ } catch {
101
+ }
102
+ }
103
+ var sellfolk = {
104
+ /**
105
+ * Initialize the SDK with your public API key.
106
+ *
107
+ * sellfolk.init('sk_live_acme_a8e92f...')
108
+ *
109
+ * On first call, captures `?ref=` from the URL (if present) and stores it
110
+ * with a 60-day expiry. Subsequent track() / conversion() calls automatically
111
+ * include the stored ref_id.
112
+ */
113
+ init(apiKey, options = {}) {
114
+ state.apiKey = apiKey;
115
+ state.apiBase = options.apiBase ?? DEFAULT_API_BASE;
116
+ if (options.now) state.now = options.now;
117
+ const urlRef = extractRefFromUrl();
118
+ if (urlRef) {
119
+ writeStoredRef(urlRef, state.now);
120
+ state.refId = urlRef;
121
+ } else if (options.forceClearOnEmptyUrl) {
122
+ clearStoredRef();
123
+ state.refId = null;
124
+ } else {
125
+ state.refId = readStoredRef(state.now);
126
+ }
127
+ },
128
+ /**
129
+ * Track an event. Common values: 'signup', 'trial', 'demo', 'newsletter'.
130
+ * Custom event names are allowed (they go in as type='custom' with
131
+ * customName=<event>).
132
+ */
133
+ track(event, payload = {}) {
134
+ dispatch("/api/v1/track", { event, ...payload });
135
+ },
136
+ /**
137
+ * Fire a conversion event — call this when real money moves.
138
+ * Example: inside your Stripe / Polar webhook handler.
139
+ */
140
+ conversion(payload) {
141
+ dispatch("/api/v1/conversion", payload);
142
+ },
143
+ /**
144
+ * Read the current stored ref_id (e.g. for logging or attribution debugging).
145
+ * Returns null if no ref has been captured yet.
146
+ */
147
+ getRef() {
148
+ return state.refId;
149
+ },
150
+ /** Programmatically clear the stored ref (e.g. on logout). */
151
+ clearRef() {
152
+ clearStoredRef();
153
+ state.refId = null;
154
+ },
155
+ // ── Testing handles (not part of public API) ──────────────────────────────
156
+ _state: state
157
+ };
158
+ var src_default = sellfolk;
159
+ // Annotate the CommonJS export names for ESM import in node:
160
+ 0 && (module.exports = {
161
+ DEFAULT_API_BASE,
162
+ REF_STORAGE_KEY,
163
+ REF_TTL_MS,
164
+ clearStoredRef,
165
+ extractRefFromUrl,
166
+ readStoredRef,
167
+ writeStoredRef
168
+ });
169
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * sellfolk — Browser SDK\n *\n * Two lines to install:\n *\n * import sellfolk from 'sellfolk'\n * sellfolk.init('sk_live_...')\n *\n * Then call:\n * sellfolk.track('signup', { email })\n * sellfolk.conversion({ amount: 49.99, email })\n *\n * Design rules:\n * • Never throws. If anything fails, we swallow it. Your SaaS keeps running.\n * • `keepalive: true` on every fetch so requests survive page unload.\n * • Captures `?ref=` on init() and stores it in localStorage with 60-day expiry.\n * • All subsequent event calls auto-attach the stored ref_id.\n */\n\nexport const DEFAULT_API_BASE = 'https://api.sellfolk.com'\n\nexport const REF_STORAGE_KEY = 'sellfolk_ref'\n\nexport const REF_TTL_MS = 60 * 24 * 60 * 60 * 1000 // 60 days\n\nexport interface InitOptions {\n /** Override the API base URL (used for testing or self-hosted backends). */\n apiBase?: string\n /** Provide a deterministic clock — primarily for tests. */\n now?: () => number\n /**\n * If true, *replace* the stored ref with the one in the URL even when the\n * URL has none. Useful in tests; defaults to false (URL ref always wins,\n * but missing URL ref leaves the stored value untouched).\n */\n forceClearOnEmptyUrl?: boolean\n}\n\nexport interface StoredRef {\n refId: string\n expiresAt: number\n}\n\nexport interface TrackPayload {\n /** Optional email — privacy-truncated server-side before display. */\n email?: string\n /** Any extra metadata for the event. */\n [key: string]: unknown\n}\n\nexport interface ConversionPayload {\n amount: number\n email?: string\n currency?: string\n /** Anything else you want to attach (order_id, plan, etc.). */\n [key: string]: unknown\n}\n\n// ── Internal state ──────────────────────────────────────────────────────────\ninterface State {\n apiKey: string | null\n apiBase: string\n refId: string | null\n now: () => number\n}\n\nconst state: State = {\n apiKey: null,\n apiBase: DEFAULT_API_BASE,\n refId: null,\n now: () => Date.now(),\n}\n\n// ── Storage helpers (defensive — localStorage may be missing/disabled) ──────\nexport function readStoredRef(now: () => number = Date.now): string | null {\n try {\n if (typeof localStorage === 'undefined') return null\n const raw = localStorage.getItem(REF_STORAGE_KEY)\n if (!raw) return null\n const parsed = JSON.parse(raw) as StoredRef\n if (!parsed || typeof parsed.refId !== 'string' || typeof parsed.expiresAt !== 'number') return null\n if (now() > parsed.expiresAt) {\n try { localStorage.removeItem(REF_STORAGE_KEY) } catch { /* ignored */ }\n return null\n }\n return parsed.refId\n } catch {\n return null\n }\n}\n\nexport function writeStoredRef(refId: string, now: () => number = Date.now): void {\n try {\n if (typeof localStorage === 'undefined') return\n const stored: StoredRef = { refId, expiresAt: now() + REF_TTL_MS }\n localStorage.setItem(REF_STORAGE_KEY, JSON.stringify(stored))\n } catch {\n // ignored\n }\n}\n\nexport function clearStoredRef(): void {\n try {\n if (typeof localStorage === 'undefined') return\n localStorage.removeItem(REF_STORAGE_KEY)\n } catch {\n // ignored\n }\n}\n\n// ── URL parsing ─────────────────────────────────────────────────────────────\nexport function extractRefFromUrl(href?: string): string | null {\n try {\n const url = href ?? (typeof window !== 'undefined' ? window.location.href : '')\n if (!url) return null\n const u = new URL(url)\n const ref = u.searchParams.get('ref')\n return ref && ref.length > 0 && ref.length <= 80 ? ref : null\n } catch {\n return null\n }\n}\n\n// ── Network ─────────────────────────────────────────────────────────────────\nfunction dispatch(path: string, body: Record<string, unknown>): void {\n if (!state.apiKey) return\n try {\n // We intentionally do not await — keepalive ensures the request finishes\n // even if the page is unloading.\n fetch(`${state.apiBase}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': state.apiKey,\n },\n body: JSON.stringify({ ...body, refId: state.refId }),\n keepalive: true,\n }).catch(() => { /* never throw */ })\n } catch {\n // Browsers without fetch (or with extreme restrictions) — silent.\n }\n}\n\n// ── Public API ──────────────────────────────────────────────────────────────\nconst sellfolk = {\n /**\n * Initialize the SDK with your public API key.\n *\n * sellfolk.init('sk_live_acme_a8e92f...')\n *\n * On first call, captures `?ref=` from the URL (if present) and stores it\n * with a 60-day expiry. Subsequent track() / conversion() calls automatically\n * include the stored ref_id.\n */\n init(apiKey: string, options: InitOptions = {}): void {\n state.apiKey = apiKey\n state.apiBase = options.apiBase ?? DEFAULT_API_BASE\n if (options.now) state.now = options.now\n\n const urlRef = extractRefFromUrl()\n if (urlRef) {\n writeStoredRef(urlRef, state.now)\n state.refId = urlRef\n } else if (options.forceClearOnEmptyUrl) {\n clearStoredRef()\n state.refId = null\n } else {\n state.refId = readStoredRef(state.now)\n }\n },\n\n /**\n * Track an event. Common values: 'signup', 'trial', 'demo', 'newsletter'.\n * Custom event names are allowed (they go in as type='custom' with\n * customName=<event>).\n */\n track(event: string, payload: TrackPayload = {}): void {\n dispatch('/api/v1/track', { event, ...payload })\n },\n\n /**\n * Fire a conversion event — call this when real money moves.\n * Example: inside your Stripe / Polar webhook handler.\n */\n conversion(payload: ConversionPayload): void {\n dispatch('/api/v1/conversion', payload)\n },\n\n /**\n * Read the current stored ref_id (e.g. for logging or attribution debugging).\n * Returns null if no ref has been captured yet.\n */\n getRef(): string | null {\n return state.refId\n },\n\n /** Programmatically clear the stored ref (e.g. on logout). */\n clearRef(): void {\n clearStoredRef()\n state.refId = null\n },\n\n // ── Testing handles (not part of public API) ──────────────────────────────\n _state: state,\n}\n\nexport default sellfolk\n"],"mappings":"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","names":[]}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * sellfolk — Browser SDK
3
+ *
4
+ * Two lines to install:
5
+ *
6
+ * import sellfolk from 'sellfolk'
7
+ * sellfolk.init('sk_live_...')
8
+ *
9
+ * Then call:
10
+ * sellfolk.track('signup', { email })
11
+ * sellfolk.conversion({ amount: 49.99, email })
12
+ *
13
+ * Design rules:
14
+ * • Never throws. If anything fails, we swallow it. Your SaaS keeps running.
15
+ * • `keepalive: true` on every fetch so requests survive page unload.
16
+ * • Captures `?ref=` on init() and stores it in localStorage with 60-day expiry.
17
+ * • All subsequent event calls auto-attach the stored ref_id.
18
+ */
19
+ declare const DEFAULT_API_BASE = "https://api.sellfolk.com";
20
+ declare const REF_STORAGE_KEY = "sellfolk_ref";
21
+ declare const REF_TTL_MS: number;
22
+ interface InitOptions {
23
+ /** Override the API base URL (used for testing or self-hosted backends). */
24
+ apiBase?: string;
25
+ /** Provide a deterministic clock — primarily for tests. */
26
+ now?: () => number;
27
+ /**
28
+ * If true, *replace* the stored ref with the one in the URL even when the
29
+ * URL has none. Useful in tests; defaults to false (URL ref always wins,
30
+ * but missing URL ref leaves the stored value untouched).
31
+ */
32
+ forceClearOnEmptyUrl?: boolean;
33
+ }
34
+ interface StoredRef {
35
+ refId: string;
36
+ expiresAt: number;
37
+ }
38
+ interface TrackPayload {
39
+ /** Optional email — privacy-truncated server-side before display. */
40
+ email?: string;
41
+ /** Any extra metadata for the event. */
42
+ [key: string]: unknown;
43
+ }
44
+ interface ConversionPayload {
45
+ amount: number;
46
+ email?: string;
47
+ currency?: string;
48
+ /** Anything else you want to attach (order_id, plan, etc.). */
49
+ [key: string]: unknown;
50
+ }
51
+ interface State {
52
+ apiKey: string | null;
53
+ apiBase: string;
54
+ refId: string | null;
55
+ now: () => number;
56
+ }
57
+ declare function readStoredRef(now?: () => number): string | null;
58
+ declare function writeStoredRef(refId: string, now?: () => number): void;
59
+ declare function clearStoredRef(): void;
60
+ declare function extractRefFromUrl(href?: string): string | null;
61
+ declare const sellfolk: {
62
+ /**
63
+ * Initialize the SDK with your public API key.
64
+ *
65
+ * sellfolk.init('sk_live_acme_a8e92f...')
66
+ *
67
+ * On first call, captures `?ref=` from the URL (if present) and stores it
68
+ * with a 60-day expiry. Subsequent track() / conversion() calls automatically
69
+ * include the stored ref_id.
70
+ */
71
+ init(apiKey: string, options?: InitOptions): void;
72
+ /**
73
+ * Track an event. Common values: 'signup', 'trial', 'demo', 'newsletter'.
74
+ * Custom event names are allowed (they go in as type='custom' with
75
+ * customName=<event>).
76
+ */
77
+ track(event: string, payload?: TrackPayload): void;
78
+ /**
79
+ * Fire a conversion event — call this when real money moves.
80
+ * Example: inside your Stripe / Polar webhook handler.
81
+ */
82
+ conversion(payload: ConversionPayload): void;
83
+ /**
84
+ * Read the current stored ref_id (e.g. for logging or attribution debugging).
85
+ * Returns null if no ref has been captured yet.
86
+ */
87
+ getRef(): string | null;
88
+ /** Programmatically clear the stored ref (e.g. on logout). */
89
+ clearRef(): void;
90
+ _state: State;
91
+ };
92
+
93
+ export { type ConversionPayload, DEFAULT_API_BASE, type InitOptions, REF_STORAGE_KEY, REF_TTL_MS, type StoredRef, type TrackPayload, clearStoredRef, sellfolk as default, extractRefFromUrl, readStoredRef, writeStoredRef };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * sellfolk — Browser SDK
3
+ *
4
+ * Two lines to install:
5
+ *
6
+ * import sellfolk from 'sellfolk'
7
+ * sellfolk.init('sk_live_...')
8
+ *
9
+ * Then call:
10
+ * sellfolk.track('signup', { email })
11
+ * sellfolk.conversion({ amount: 49.99, email })
12
+ *
13
+ * Design rules:
14
+ * • Never throws. If anything fails, we swallow it. Your SaaS keeps running.
15
+ * • `keepalive: true` on every fetch so requests survive page unload.
16
+ * • Captures `?ref=` on init() and stores it in localStorage with 60-day expiry.
17
+ * • All subsequent event calls auto-attach the stored ref_id.
18
+ */
19
+ declare const DEFAULT_API_BASE = "https://api.sellfolk.com";
20
+ declare const REF_STORAGE_KEY = "sellfolk_ref";
21
+ declare const REF_TTL_MS: number;
22
+ interface InitOptions {
23
+ /** Override the API base URL (used for testing or self-hosted backends). */
24
+ apiBase?: string;
25
+ /** Provide a deterministic clock — primarily for tests. */
26
+ now?: () => number;
27
+ /**
28
+ * If true, *replace* the stored ref with the one in the URL even when the
29
+ * URL has none. Useful in tests; defaults to false (URL ref always wins,
30
+ * but missing URL ref leaves the stored value untouched).
31
+ */
32
+ forceClearOnEmptyUrl?: boolean;
33
+ }
34
+ interface StoredRef {
35
+ refId: string;
36
+ expiresAt: number;
37
+ }
38
+ interface TrackPayload {
39
+ /** Optional email — privacy-truncated server-side before display. */
40
+ email?: string;
41
+ /** Any extra metadata for the event. */
42
+ [key: string]: unknown;
43
+ }
44
+ interface ConversionPayload {
45
+ amount: number;
46
+ email?: string;
47
+ currency?: string;
48
+ /** Anything else you want to attach (order_id, plan, etc.). */
49
+ [key: string]: unknown;
50
+ }
51
+ interface State {
52
+ apiKey: string | null;
53
+ apiBase: string;
54
+ refId: string | null;
55
+ now: () => number;
56
+ }
57
+ declare function readStoredRef(now?: () => number): string | null;
58
+ declare function writeStoredRef(refId: string, now?: () => number): void;
59
+ declare function clearStoredRef(): void;
60
+ declare function extractRefFromUrl(href?: string): string | null;
61
+ declare const sellfolk: {
62
+ /**
63
+ * Initialize the SDK with your public API key.
64
+ *
65
+ * sellfolk.init('sk_live_acme_a8e92f...')
66
+ *
67
+ * On first call, captures `?ref=` from the URL (if present) and stores it
68
+ * with a 60-day expiry. Subsequent track() / conversion() calls automatically
69
+ * include the stored ref_id.
70
+ */
71
+ init(apiKey: string, options?: InitOptions): void;
72
+ /**
73
+ * Track an event. Common values: 'signup', 'trial', 'demo', 'newsletter'.
74
+ * Custom event names are allowed (they go in as type='custom' with
75
+ * customName=<event>).
76
+ */
77
+ track(event: string, payload?: TrackPayload): void;
78
+ /**
79
+ * Fire a conversion event — call this when real money moves.
80
+ * Example: inside your Stripe / Polar webhook handler.
81
+ */
82
+ conversion(payload: ConversionPayload): void;
83
+ /**
84
+ * Read the current stored ref_id (e.g. for logging or attribution debugging).
85
+ * Returns null if no ref has been captured yet.
86
+ */
87
+ getRef(): string | null;
88
+ /** Programmatically clear the stored ref (e.g. on logout). */
89
+ clearRef(): void;
90
+ _state: State;
91
+ };
92
+
93
+ export { type ConversionPayload, DEFAULT_API_BASE, type InitOptions, REF_STORAGE_KEY, REF_TTL_MS, type StoredRef, type TrackPayload, clearStoredRef, sellfolk as default, extractRefFromUrl, readStoredRef, writeStoredRef };
package/dist/index.js ADDED
@@ -0,0 +1,138 @@
1
+ // src/index.ts
2
+ var DEFAULT_API_BASE = "https://api.sellfolk.com";
3
+ var REF_STORAGE_KEY = "sellfolk_ref";
4
+ var REF_TTL_MS = 60 * 24 * 60 * 60 * 1e3;
5
+ var state = {
6
+ apiKey: null,
7
+ apiBase: DEFAULT_API_BASE,
8
+ refId: null,
9
+ now: () => Date.now()
10
+ };
11
+ function readStoredRef(now = Date.now) {
12
+ try {
13
+ if (typeof localStorage === "undefined") return null;
14
+ const raw = localStorage.getItem(REF_STORAGE_KEY);
15
+ if (!raw) return null;
16
+ const parsed = JSON.parse(raw);
17
+ if (!parsed || typeof parsed.refId !== "string" || typeof parsed.expiresAt !== "number") return null;
18
+ if (now() > parsed.expiresAt) {
19
+ try {
20
+ localStorage.removeItem(REF_STORAGE_KEY);
21
+ } catch {
22
+ }
23
+ return null;
24
+ }
25
+ return parsed.refId;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ function writeStoredRef(refId, now = Date.now) {
31
+ try {
32
+ if (typeof localStorage === "undefined") return;
33
+ const stored = { refId, expiresAt: now() + REF_TTL_MS };
34
+ localStorage.setItem(REF_STORAGE_KEY, JSON.stringify(stored));
35
+ } catch {
36
+ }
37
+ }
38
+ function clearStoredRef() {
39
+ try {
40
+ if (typeof localStorage === "undefined") return;
41
+ localStorage.removeItem(REF_STORAGE_KEY);
42
+ } catch {
43
+ }
44
+ }
45
+ function extractRefFromUrl(href) {
46
+ try {
47
+ const url = href ?? (typeof window !== "undefined" ? window.location.href : "");
48
+ if (!url) return null;
49
+ const u = new URL(url);
50
+ const ref = u.searchParams.get("ref");
51
+ return ref && ref.length > 0 && ref.length <= 80 ? ref : null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+ function dispatch(path, body) {
57
+ if (!state.apiKey) return;
58
+ try {
59
+ fetch(`${state.apiBase}${path}`, {
60
+ method: "POST",
61
+ headers: {
62
+ "Content-Type": "application/json",
63
+ "X-API-Key": state.apiKey
64
+ },
65
+ body: JSON.stringify({ ...body, refId: state.refId }),
66
+ keepalive: true
67
+ }).catch(() => {
68
+ });
69
+ } catch {
70
+ }
71
+ }
72
+ var sellfolk = {
73
+ /**
74
+ * Initialize the SDK with your public API key.
75
+ *
76
+ * sellfolk.init('sk_live_acme_a8e92f...')
77
+ *
78
+ * On first call, captures `?ref=` from the URL (if present) and stores it
79
+ * with a 60-day expiry. Subsequent track() / conversion() calls automatically
80
+ * include the stored ref_id.
81
+ */
82
+ init(apiKey, options = {}) {
83
+ state.apiKey = apiKey;
84
+ state.apiBase = options.apiBase ?? DEFAULT_API_BASE;
85
+ if (options.now) state.now = options.now;
86
+ const urlRef = extractRefFromUrl();
87
+ if (urlRef) {
88
+ writeStoredRef(urlRef, state.now);
89
+ state.refId = urlRef;
90
+ } else if (options.forceClearOnEmptyUrl) {
91
+ clearStoredRef();
92
+ state.refId = null;
93
+ } else {
94
+ state.refId = readStoredRef(state.now);
95
+ }
96
+ },
97
+ /**
98
+ * Track an event. Common values: 'signup', 'trial', 'demo', 'newsletter'.
99
+ * Custom event names are allowed (they go in as type='custom' with
100
+ * customName=<event>).
101
+ */
102
+ track(event, payload = {}) {
103
+ dispatch("/api/v1/track", { event, ...payload });
104
+ },
105
+ /**
106
+ * Fire a conversion event — call this when real money moves.
107
+ * Example: inside your Stripe / Polar webhook handler.
108
+ */
109
+ conversion(payload) {
110
+ dispatch("/api/v1/conversion", payload);
111
+ },
112
+ /**
113
+ * Read the current stored ref_id (e.g. for logging or attribution debugging).
114
+ * Returns null if no ref has been captured yet.
115
+ */
116
+ getRef() {
117
+ return state.refId;
118
+ },
119
+ /** Programmatically clear the stored ref (e.g. on logout). */
120
+ clearRef() {
121
+ clearStoredRef();
122
+ state.refId = null;
123
+ },
124
+ // ── Testing handles (not part of public API) ──────────────────────────────
125
+ _state: state
126
+ };
127
+ var src_default = sellfolk;
128
+ export {
129
+ DEFAULT_API_BASE,
130
+ REF_STORAGE_KEY,
131
+ REF_TTL_MS,
132
+ clearStoredRef,
133
+ src_default as default,
134
+ extractRefFromUrl,
135
+ readStoredRef,
136
+ writeStoredRef
137
+ };
138
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * sellfolk — Browser SDK\n *\n * Two lines to install:\n *\n * import sellfolk from 'sellfolk'\n * sellfolk.init('sk_live_...')\n *\n * Then call:\n * sellfolk.track('signup', { email })\n * sellfolk.conversion({ amount: 49.99, email })\n *\n * Design rules:\n * • Never throws. If anything fails, we swallow it. Your SaaS keeps running.\n * • `keepalive: true` on every fetch so requests survive page unload.\n * • Captures `?ref=` on init() and stores it in localStorage with 60-day expiry.\n * • All subsequent event calls auto-attach the stored ref_id.\n */\n\nexport const DEFAULT_API_BASE = 'https://api.sellfolk.com'\n\nexport const REF_STORAGE_KEY = 'sellfolk_ref'\n\nexport const REF_TTL_MS = 60 * 24 * 60 * 60 * 1000 // 60 days\n\nexport interface InitOptions {\n /** Override the API base URL (used for testing or self-hosted backends). */\n apiBase?: string\n /** Provide a deterministic clock — primarily for tests. */\n now?: () => number\n /**\n * If true, *replace* the stored ref with the one in the URL even when the\n * URL has none. Useful in tests; defaults to false (URL ref always wins,\n * but missing URL ref leaves the stored value untouched).\n */\n forceClearOnEmptyUrl?: boolean\n}\n\nexport interface StoredRef {\n refId: string\n expiresAt: number\n}\n\nexport interface TrackPayload {\n /** Optional email — privacy-truncated server-side before display. */\n email?: string\n /** Any extra metadata for the event. */\n [key: string]: unknown\n}\n\nexport interface ConversionPayload {\n amount: number\n email?: string\n currency?: string\n /** Anything else you want to attach (order_id, plan, etc.). */\n [key: string]: unknown\n}\n\n// ── Internal state ──────────────────────────────────────────────────────────\ninterface State {\n apiKey: string | null\n apiBase: string\n refId: string | null\n now: () => number\n}\n\nconst state: State = {\n apiKey: null,\n apiBase: DEFAULT_API_BASE,\n refId: null,\n now: () => Date.now(),\n}\n\n// ── Storage helpers (defensive — localStorage may be missing/disabled) ──────\nexport function readStoredRef(now: () => number = Date.now): string | null {\n try {\n if (typeof localStorage === 'undefined') return null\n const raw = localStorage.getItem(REF_STORAGE_KEY)\n if (!raw) return null\n const parsed = JSON.parse(raw) as StoredRef\n if (!parsed || typeof parsed.refId !== 'string' || typeof parsed.expiresAt !== 'number') return null\n if (now() > parsed.expiresAt) {\n try { localStorage.removeItem(REF_STORAGE_KEY) } catch { /* ignored */ }\n return null\n }\n return parsed.refId\n } catch {\n return null\n }\n}\n\nexport function writeStoredRef(refId: string, now: () => number = Date.now): void {\n try {\n if (typeof localStorage === 'undefined') return\n const stored: StoredRef = { refId, expiresAt: now() + REF_TTL_MS }\n localStorage.setItem(REF_STORAGE_KEY, JSON.stringify(stored))\n } catch {\n // ignored\n }\n}\n\nexport function clearStoredRef(): void {\n try {\n if (typeof localStorage === 'undefined') return\n localStorage.removeItem(REF_STORAGE_KEY)\n } catch {\n // ignored\n }\n}\n\n// ── URL parsing ─────────────────────────────────────────────────────────────\nexport function extractRefFromUrl(href?: string): string | null {\n try {\n const url = href ?? (typeof window !== 'undefined' ? window.location.href : '')\n if (!url) return null\n const u = new URL(url)\n const ref = u.searchParams.get('ref')\n return ref && ref.length > 0 && ref.length <= 80 ? ref : null\n } catch {\n return null\n }\n}\n\n// ── Network ─────────────────────────────────────────────────────────────────\nfunction dispatch(path: string, body: Record<string, unknown>): void {\n if (!state.apiKey) return\n try {\n // We intentionally do not await — keepalive ensures the request finishes\n // even if the page is unloading.\n fetch(`${state.apiBase}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': state.apiKey,\n },\n body: JSON.stringify({ ...body, refId: state.refId }),\n keepalive: true,\n }).catch(() => { /* never throw */ })\n } catch {\n // Browsers without fetch (or with extreme restrictions) — silent.\n }\n}\n\n// ── Public API ──────────────────────────────────────────────────────────────\nconst sellfolk = {\n /**\n * Initialize the SDK with your public API key.\n *\n * sellfolk.init('sk_live_acme_a8e92f...')\n *\n * On first call, captures `?ref=` from the URL (if present) and stores it\n * with a 60-day expiry. Subsequent track() / conversion() calls automatically\n * include the stored ref_id.\n */\n init(apiKey: string, options: InitOptions = {}): void {\n state.apiKey = apiKey\n state.apiBase = options.apiBase ?? DEFAULT_API_BASE\n if (options.now) state.now = options.now\n\n const urlRef = extractRefFromUrl()\n if (urlRef) {\n writeStoredRef(urlRef, state.now)\n state.refId = urlRef\n } else if (options.forceClearOnEmptyUrl) {\n clearStoredRef()\n state.refId = null\n } else {\n state.refId = readStoredRef(state.now)\n }\n },\n\n /**\n * Track an event. Common values: 'signup', 'trial', 'demo', 'newsletter'.\n * Custom event names are allowed (they go in as type='custom' with\n * customName=<event>).\n */\n track(event: string, payload: TrackPayload = {}): void {\n dispatch('/api/v1/track', { event, ...payload })\n },\n\n /**\n * Fire a conversion event — call this when real money moves.\n * Example: inside your Stripe / Polar webhook handler.\n */\n conversion(payload: ConversionPayload): void {\n dispatch('/api/v1/conversion', payload)\n },\n\n /**\n * Read the current stored ref_id (e.g. for logging or attribution debugging).\n * Returns null if no ref has been captured yet.\n */\n getRef(): string | null {\n return state.refId\n },\n\n /** Programmatically clear the stored ref (e.g. on logout). */\n clearRef(): void {\n clearStoredRef()\n state.refId = null\n },\n\n // ── Testing handles (not part of public API) ──────────────────────────────\n _state: state,\n}\n\nexport default sellfolk\n"],"mappings":"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","names":[]}
@@ -0,0 +1 @@
1
+ "use strict";var sellfolk=(()=>{var l=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var S=Object.prototype.hasOwnProperty;var m=(t,e)=>{for(var r in e)l(t,r,{get:e[r],enumerable:!0})},w=(t,e,r,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of y(e))!S.call(t,i)&&i!==r&&l(t,i,{get:()=>e[i],enumerable:!(o=g(e,i))||o.enumerable});return t};var I=t=>w(l({},"__esModule",{value:!0}),t);var R={};m(R,{DEFAULT_API_BASE:()=>s,REF_STORAGE_KEY:()=>a,REF_TTL_MS:()=>x,clearStoredRef:()=>f,default:()=>v,extractRefFromUrl:()=>p,readStoredRef:()=>u,writeStoredRef:()=>d});var s="https://api.sellfolk.com",a="sellfolk_ref",x=5184e6,n={apiKey:null,apiBase:s,refId:null,now:()=>Date.now()};function u(t=Date.now){try{if(typeof localStorage>"u")return null;let e=localStorage.getItem(a);if(!e)return null;let r=JSON.parse(e);if(!r||typeof r.refId!="string"||typeof r.expiresAt!="number")return null;if(t()>r.expiresAt){try{localStorage.removeItem(a)}catch{}return null}return r.refId}catch{return null}}function d(t,e=Date.now){try{if(typeof localStorage>"u")return;let r={refId:t,expiresAt:e()+5184e6};localStorage.setItem(a,JSON.stringify(r))}catch{}}function f(){try{if(typeof localStorage>"u")return;localStorage.removeItem(a)}catch{}}function p(t){try{let e=t??(typeof window<"u"?window.location.href:"");if(!e)return null;let o=new URL(e).searchParams.get("ref");return o&&o.length>0&&o.length<=80?o:null}catch{return null}}function c(t,e){if(n.apiKey)try{fetch(`${n.apiBase}${t}`,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":n.apiKey},body:JSON.stringify({...e,refId:n.refId}),keepalive:!0}).catch(()=>{})}catch{}}var h={init(t,e={}){n.apiKey=t,n.apiBase=e.apiBase??s,e.now&&(n.now=e.now);let r=p();r?(d(r,n.now),n.refId=r):e.forceClearOnEmptyUrl?(f(),n.refId=null):n.refId=u(n.now)},track(t,e={}){c("/api/v1/track",{event:t,...e})},conversion(t){c("/api/v1/conversion",t)},getRef(){return n.refId},clearRef(){f(),n.refId=null},_state:n},v=h;return I(R);})();
@@ -0,0 +1,73 @@
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 __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ SellfolkError: () => SellfolkError,
24
+ SellfolkServer: () => SellfolkServer
25
+ });
26
+ module.exports = __toCommonJS(server_exports);
27
+ var DEFAULT_API_BASE = "https://api.sellfolk.com";
28
+ var SellfolkServer = class {
29
+ constructor(apiKey, options = {}) {
30
+ this.apiKey = apiKey;
31
+ this.apiBase = options.apiBase ?? DEFAULT_API_BASE;
32
+ this.throwOnError = options.throwOnError ?? false;
33
+ this._fetch = options.fetch ?? globalThis.fetch;
34
+ }
35
+ /** Track a non-conversion event (signup, trial, demo, newsletter, custom). */
36
+ async track(event, payload = {}) {
37
+ await this._post("/api/v1/track", { event, ...payload });
38
+ }
39
+ /** Track a paid conversion. Call from your billing webhook. */
40
+ async conversion(payload) {
41
+ await this._post("/api/v1/conversion", payload);
42
+ }
43
+ async _post(path, body) {
44
+ try {
45
+ const res = await this._fetch(`${this.apiBase}${path}`, {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ "X-API-Key": this.apiKey
50
+ },
51
+ body: JSON.stringify(body)
52
+ });
53
+ if (!res.ok && this.throwOnError) {
54
+ throw new SellfolkError(`Sellfolk ${path} returned ${res.status}`, res.status);
55
+ }
56
+ } catch (err) {
57
+ if (this.throwOnError) throw err;
58
+ }
59
+ }
60
+ };
61
+ var SellfolkError = class extends Error {
62
+ constructor(message, status) {
63
+ super(message);
64
+ this.name = "SellfolkError";
65
+ this.status = status;
66
+ }
67
+ };
68
+ // Annotate the CommonJS export names for ESM import in node:
69
+ 0 && (module.exports = {
70
+ SellfolkError,
71
+ SellfolkServer
72
+ });
73
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * sellfolk/server — Server SDK\n *\n * Node 20+ (uses native fetch). Zero runtime dependencies.\n *\n * import { SellfolkServer } from 'sellfolk/server'\n * const sb = new SellfolkServer(process.env.SELLFOLK_KEY!)\n * await sb.conversion({ email, amount, currency })\n *\n * Design rules:\n * • Never throws unless you explicitly opt-in with `throwOnError: true`.\n * • Methods are async — await them inside webhook handlers so retries work.\n */\n\nconst DEFAULT_API_BASE = 'https://api.sellfolk.com'\n\nexport interface ServerOptions {\n apiBase?: string\n /** When true, propagate fetch errors. Default: false (swallowed). */\n throwOnError?: boolean\n /** Custom fetch implementation (mostly for tests). */\n fetch?: typeof fetch\n}\n\nexport interface ConversionPayload {\n email: string\n amount: number\n currency?: string\n /** If omitted, the backend can still attribute via signed cookies or other channels. */\n refId?: string\n /** Arbitrary metadata (order_id, plan, etc.). */\n [key: string]: unknown\n}\n\nexport interface TrackPayload {\n refId?: string\n email?: string\n [key: string]: unknown\n}\n\nexport class SellfolkServer {\n private apiKey: string\n private apiBase: string\n private throwOnError: boolean\n private _fetch: typeof fetch\n\n constructor(apiKey: string, options: ServerOptions = {}) {\n this.apiKey = apiKey\n this.apiBase = options.apiBase ?? DEFAULT_API_BASE\n this.throwOnError = options.throwOnError ?? false\n this._fetch = options.fetch ?? globalThis.fetch\n }\n\n /** Track a non-conversion event (signup, trial, demo, newsletter, custom). */\n async track(event: string, payload: TrackPayload = {}): Promise<void> {\n await this._post('/api/v1/track', { event, ...payload })\n }\n\n /** Track a paid conversion. Call from your billing webhook. */\n async conversion(payload: ConversionPayload): Promise<void> {\n await this._post('/api/v1/conversion', payload)\n }\n\n private async _post(path: string, body: Record<string, unknown>): Promise<void> {\n try {\n const res = await this._fetch(`${this.apiBase}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n },\n body: JSON.stringify(body),\n })\n if (!res.ok && this.throwOnError) {\n throw new SellfolkError(`Sellfolk ${path} returned ${res.status}`, res.status)\n }\n } catch (err) {\n if (this.throwOnError) throw err\n // Silent failure — webhook handlers should never crash because our API\n // hiccuped. The conversion will be reconciled later if needed.\n }\n }\n}\n\nexport class SellfolkError extends Error {\n status?: number\n constructor(message: string, status?: number) {\n super(message)\n this.name = 'SellfolkError'\n this.status = status\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,IAAM,mBAAmB;AA0BlB,IAAM,iBAAN,MAAqB;AAAA,EAM1B,YAAY,QAAgB,UAAyB,CAAC,GAAG;AACvD,SAAK,SAAS;AACd,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,eAAe,QAAQ,gBAAgB;AAC5C,SAAK,SAAS,QAAQ,SAAS,WAAW;AAAA,EAC5C;AAAA;AAAA,EAGA,MAAM,MAAM,OAAe,UAAwB,CAAC,GAAkB;AACpE,UAAM,KAAK,MAAM,iBAAiB,EAAE,OAAO,GAAG,QAAQ,CAAC;AAAA,EACzD;AAAA;AAAA,EAGA,MAAM,WAAW,SAA2C;AAC1D,UAAM,KAAK,MAAM,sBAAsB,OAAO;AAAA,EAChD;AAAA,EAEA,MAAc,MAAM,MAAc,MAA8C;AAC9E,QAAI;AACF,YAAM,MAAM,MAAM,KAAK,OAAO,GAAG,KAAK,OAAO,GAAG,IAAI,IAAI;AAAA,QACtD,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,aAAa,KAAK;AAAA,QACpB;AAAA,QACA,MAAM,KAAK,UAAU,IAAI;AAAA,MAC3B,CAAC;AACD,UAAI,CAAC,IAAI,MAAM,KAAK,cAAc;AAChC,cAAM,IAAI,cAAc,YAAY,IAAI,aAAa,IAAI,MAAM,IAAI,IAAI,MAAM;AAAA,MAC/E;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,KAAK,aAAc,OAAM;AAAA,IAG/B;AAAA,EACF;AACF;AAEO,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAEvC,YAAY,SAAiB,QAAiB;AAC5C,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;","names":[]}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * sellfolk/server — Server SDK
3
+ *
4
+ * Node 20+ (uses native fetch). Zero runtime dependencies.
5
+ *
6
+ * import { SellfolkServer } from 'sellfolk/server'
7
+ * const sb = new SellfolkServer(process.env.SELLFOLK_KEY!)
8
+ * await sb.conversion({ email, amount, currency })
9
+ *
10
+ * Design rules:
11
+ * • Never throws unless you explicitly opt-in with `throwOnError: true`.
12
+ * • Methods are async — await them inside webhook handlers so retries work.
13
+ */
14
+ interface ServerOptions {
15
+ apiBase?: string;
16
+ /** When true, propagate fetch errors. Default: false (swallowed). */
17
+ throwOnError?: boolean;
18
+ /** Custom fetch implementation (mostly for tests). */
19
+ fetch?: typeof fetch;
20
+ }
21
+ interface ConversionPayload {
22
+ email: string;
23
+ amount: number;
24
+ currency?: string;
25
+ /** If omitted, the backend can still attribute via signed cookies or other channels. */
26
+ refId?: string;
27
+ /** Arbitrary metadata (order_id, plan, etc.). */
28
+ [key: string]: unknown;
29
+ }
30
+ interface TrackPayload {
31
+ refId?: string;
32
+ email?: string;
33
+ [key: string]: unknown;
34
+ }
35
+ declare class SellfolkServer {
36
+ private apiKey;
37
+ private apiBase;
38
+ private throwOnError;
39
+ private _fetch;
40
+ constructor(apiKey: string, options?: ServerOptions);
41
+ /** Track a non-conversion event (signup, trial, demo, newsletter, custom). */
42
+ track(event: string, payload?: TrackPayload): Promise<void>;
43
+ /** Track a paid conversion. Call from your billing webhook. */
44
+ conversion(payload: ConversionPayload): Promise<void>;
45
+ private _post;
46
+ }
47
+ declare class SellfolkError extends Error {
48
+ status?: number;
49
+ constructor(message: string, status?: number);
50
+ }
51
+
52
+ export { type ConversionPayload, SellfolkError, SellfolkServer, type ServerOptions, type TrackPayload };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * sellfolk/server — Server SDK
3
+ *
4
+ * Node 20+ (uses native fetch). Zero runtime dependencies.
5
+ *
6
+ * import { SellfolkServer } from 'sellfolk/server'
7
+ * const sb = new SellfolkServer(process.env.SELLFOLK_KEY!)
8
+ * await sb.conversion({ email, amount, currency })
9
+ *
10
+ * Design rules:
11
+ * • Never throws unless you explicitly opt-in with `throwOnError: true`.
12
+ * • Methods are async — await them inside webhook handlers so retries work.
13
+ */
14
+ interface ServerOptions {
15
+ apiBase?: string;
16
+ /** When true, propagate fetch errors. Default: false (swallowed). */
17
+ throwOnError?: boolean;
18
+ /** Custom fetch implementation (mostly for tests). */
19
+ fetch?: typeof fetch;
20
+ }
21
+ interface ConversionPayload {
22
+ email: string;
23
+ amount: number;
24
+ currency?: string;
25
+ /** If omitted, the backend can still attribute via signed cookies or other channels. */
26
+ refId?: string;
27
+ /** Arbitrary metadata (order_id, plan, etc.). */
28
+ [key: string]: unknown;
29
+ }
30
+ interface TrackPayload {
31
+ refId?: string;
32
+ email?: string;
33
+ [key: string]: unknown;
34
+ }
35
+ declare class SellfolkServer {
36
+ private apiKey;
37
+ private apiBase;
38
+ private throwOnError;
39
+ private _fetch;
40
+ constructor(apiKey: string, options?: ServerOptions);
41
+ /** Track a non-conversion event (signup, trial, demo, newsletter, custom). */
42
+ track(event: string, payload?: TrackPayload): Promise<void>;
43
+ /** Track a paid conversion. Call from your billing webhook. */
44
+ conversion(payload: ConversionPayload): Promise<void>;
45
+ private _post;
46
+ }
47
+ declare class SellfolkError extends Error {
48
+ status?: number;
49
+ constructor(message: string, status?: number);
50
+ }
51
+
52
+ export { type ConversionPayload, SellfolkError, SellfolkServer, type ServerOptions, type TrackPayload };
package/dist/server.js ADDED
@@ -0,0 +1,47 @@
1
+ // src/server.ts
2
+ var DEFAULT_API_BASE = "https://api.sellfolk.com";
3
+ var SellfolkServer = class {
4
+ constructor(apiKey, options = {}) {
5
+ this.apiKey = apiKey;
6
+ this.apiBase = options.apiBase ?? DEFAULT_API_BASE;
7
+ this.throwOnError = options.throwOnError ?? false;
8
+ this._fetch = options.fetch ?? globalThis.fetch;
9
+ }
10
+ /** Track a non-conversion event (signup, trial, demo, newsletter, custom). */
11
+ async track(event, payload = {}) {
12
+ await this._post("/api/v1/track", { event, ...payload });
13
+ }
14
+ /** Track a paid conversion. Call from your billing webhook. */
15
+ async conversion(payload) {
16
+ await this._post("/api/v1/conversion", payload);
17
+ }
18
+ async _post(path, body) {
19
+ try {
20
+ const res = await this._fetch(`${this.apiBase}${path}`, {
21
+ method: "POST",
22
+ headers: {
23
+ "Content-Type": "application/json",
24
+ "X-API-Key": this.apiKey
25
+ },
26
+ body: JSON.stringify(body)
27
+ });
28
+ if (!res.ok && this.throwOnError) {
29
+ throw new SellfolkError(`Sellfolk ${path} returned ${res.status}`, res.status);
30
+ }
31
+ } catch (err) {
32
+ if (this.throwOnError) throw err;
33
+ }
34
+ }
35
+ };
36
+ var SellfolkError = class extends Error {
37
+ constructor(message, status) {
38
+ super(message);
39
+ this.name = "SellfolkError";
40
+ this.status = status;
41
+ }
42
+ };
43
+ export {
44
+ SellfolkError,
45
+ SellfolkServer
46
+ };
47
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * sellfolk/server — Server SDK\n *\n * Node 20+ (uses native fetch). Zero runtime dependencies.\n *\n * import { SellfolkServer } from 'sellfolk/server'\n * const sb = new SellfolkServer(process.env.SELLFOLK_KEY!)\n * await sb.conversion({ email, amount, currency })\n *\n * Design rules:\n * • Never throws unless you explicitly opt-in with `throwOnError: true`.\n * • Methods are async — await them inside webhook handlers so retries work.\n */\n\nconst DEFAULT_API_BASE = 'https://api.sellfolk.com'\n\nexport interface ServerOptions {\n apiBase?: string\n /** When true, propagate fetch errors. Default: false (swallowed). */\n throwOnError?: boolean\n /** Custom fetch implementation (mostly for tests). */\n fetch?: typeof fetch\n}\n\nexport interface ConversionPayload {\n email: string\n amount: number\n currency?: string\n /** If omitted, the backend can still attribute via signed cookies or other channels. */\n refId?: string\n /** Arbitrary metadata (order_id, plan, etc.). */\n [key: string]: unknown\n}\n\nexport interface TrackPayload {\n refId?: string\n email?: string\n [key: string]: unknown\n}\n\nexport class SellfolkServer {\n private apiKey: string\n private apiBase: string\n private throwOnError: boolean\n private _fetch: typeof fetch\n\n constructor(apiKey: string, options: ServerOptions = {}) {\n this.apiKey = apiKey\n this.apiBase = options.apiBase ?? DEFAULT_API_BASE\n this.throwOnError = options.throwOnError ?? false\n this._fetch = options.fetch ?? globalThis.fetch\n }\n\n /** Track a non-conversion event (signup, trial, demo, newsletter, custom). */\n async track(event: string, payload: TrackPayload = {}): Promise<void> {\n await this._post('/api/v1/track', { event, ...payload })\n }\n\n /** Track a paid conversion. Call from your billing webhook. */\n async conversion(payload: ConversionPayload): Promise<void> {\n await this._post('/api/v1/conversion', payload)\n }\n\n private async _post(path: string, body: Record<string, unknown>): Promise<void> {\n try {\n const res = await this._fetch(`${this.apiBase}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n },\n body: JSON.stringify(body),\n })\n if (!res.ok && this.throwOnError) {\n throw new SellfolkError(`Sellfolk ${path} returned ${res.status}`, res.status)\n }\n } catch (err) {\n if (this.throwOnError) throw err\n // Silent failure — webhook handlers should never crash because our API\n // hiccuped. The conversion will be reconciled later if needed.\n }\n }\n}\n\nexport class SellfolkError extends Error {\n status?: number\n constructor(message: string, status?: number) {\n super(message)\n this.name = 'SellfolkError'\n this.status = status\n }\n}\n"],"mappings":";AAcA,IAAM,mBAAmB;AA0BlB,IAAM,iBAAN,MAAqB;AAAA,EAM1B,YAAY,QAAgB,UAAyB,CAAC,GAAG;AACvD,SAAK,SAAS;AACd,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,eAAe,QAAQ,gBAAgB;AAC5C,SAAK,SAAS,QAAQ,SAAS,WAAW;AAAA,EAC5C;AAAA;AAAA,EAGA,MAAM,MAAM,OAAe,UAAwB,CAAC,GAAkB;AACpE,UAAM,KAAK,MAAM,iBAAiB,EAAE,OAAO,GAAG,QAAQ,CAAC;AAAA,EACzD;AAAA;AAAA,EAGA,MAAM,WAAW,SAA2C;AAC1D,UAAM,KAAK,MAAM,sBAAsB,OAAO;AAAA,EAChD;AAAA,EAEA,MAAc,MAAM,MAAc,MAA8C;AAC9E,QAAI;AACF,YAAM,MAAM,MAAM,KAAK,OAAO,GAAG,KAAK,OAAO,GAAG,IAAI,IAAI;AAAA,QACtD,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,aAAa,KAAK;AAAA,QACpB;AAAA,QACA,MAAM,KAAK,UAAU,IAAI;AAAA,MAC3B,CAAC;AACD,UAAI,CAAC,IAAI,MAAM,KAAK,cAAc;AAChC,cAAM,IAAI,cAAc,YAAY,IAAI,aAAa,IAAI,MAAM,IAAI,IAAI,MAAM;AAAA,MAC/E;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,KAAK,aAAc,OAAM;AAAA,IAG/B;AAAA,EACF;AACF;AAEO,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAEvC,YAAY,SAAiB,QAAiB;AAC5C,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "sellfolk",
3
+ "version": "0.1.0",
4
+ "description": "Sellfolk SDK — track signups, trials, and conversions from your referral sellers.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./server": {
15
+ "import": "./dist/server.js",
16
+ "require": "./dist/server.cjs",
17
+ "types": "./dist/server.d.ts"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsup --watch",
26
+ "test": "vitest run",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "devDependencies": {
30
+ "tsup": "^8.5.0",
31
+ "typescript": "^5.8.3",
32
+ "vitest": "^3.2.4"
33
+ }
34
+ }