creem-datafast 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Santiago Garcia
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,377 @@
1
+ # creem-datafast
2
+
3
+ [![CI](https://github.com/santigamo/creem-datafast/actions/workflows/ci.yml/badge.svg)](https://github.com/santigamo/creem-datafast/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
6
+ [![Coverage](https://img.shields.io/badge/coverage-88%25-brightgreen.svg)](https://github.com/santigamo/creem-datafast/actions/workflows/ci.yml)
7
+
8
+ Connect Creem payments to DataFast analytics without writing any glue code. One factory, automatic cookie capture, webhook forwarding.
9
+
10
+ - **Zero glue code** — one factory call wires up checkout attribution and webhook forwarding
11
+ - **Framework adapters** — Next.js App Router and Express 5 out of the box, or bring your own
12
+ - **Production-ready** — idempotent webhooks, retries with backoff, Web Crypto signature verification
13
+ - **Official Upstash adapter** — ready-made distributed idempotency for serverless and multi-instance deployments
14
+ - **Refund support** — forwards `refund.created` as `refunded: true` payment events
15
+ - **Currency-aware** — correctly converts zero-decimal (JPY) and three-decimal (KWD) currencies
16
+
17
+ ## How It Works
18
+
19
+ ```mermaid
20
+ sequenceDiagram
21
+ participant B as Browser
22
+ participant S as Your Server
23
+ participant C as Creem
24
+ participant D as DataFast
25
+
26
+ B->>S: POST /api/checkout (cookies)
27
+ Note right of S: reads datafast_visitor_id<br/>injects into metadata
28
+ S->>C: createCheckout()
29
+ C-->>B: redirect to checkoutUrl
30
+ B->>C: completes payment
31
+ C->>S: webhook (checkout.completed)
32
+ Note right of S: verifies signature<br/>deduplicates event<br/>maps payload
33
+ S->>D: POST /api/v1/payments
34
+ ```
35
+
36
+ 1. Your backend calls `createCheckout()` with the incoming `Request` or cookie header.
37
+ 2. The package injects `datafast_visitor_id` and `datafast_session_id` into Creem metadata without dropping the rest of your metadata.
38
+ 3. Creem redirects the customer to `checkoutUrl`.
39
+ 4. Creem sends `checkout.completed`, `subscription.paid`, and `refund.created` webhooks back to your server.
40
+ 5. `handleWebhook()` verifies `creem-signature`, deduplicates the event id, maps the payload, and forwards the payment or refund to DataFast.
41
+
42
+ Supported events: `checkout.completed`, `subscription.paid`, `refund.created`. Any other Creem event is ignored and returns `200 OK`. Initial subscription `checkout.completed` deliveries are acknowledged but ignored so the first subscription payment is attributed only once through `subscription.paid`.
43
+
44
+ ## Judge in 2 minutes
45
+
46
+ Use `example-next` for the fastest end-to-end proof. It shows the landing page, launches a real checkout, and logs both the webhook outcome and the payload forwarded to DataFast.
47
+
48
+ ```bash
49
+ pnpm install
50
+ cp example-next/.env.example example-next/.env.local
51
+ ```
52
+
53
+ Fill `example-next/.env.local` with real test values for `CREEM_API_KEY`, `CREEM_WEBHOOK_SECRET`, `DATAFAST_API_KEY`, `DATAFAST_WEBSITE_ID`, and `CREEM_PRODUCT_ID`. `APP_BASE_URL` can stay at `http://localhost:3000` for a local run.
54
+
55
+ ```bash
56
+ pnpm build
57
+ pnpm --filter example-next dev
58
+ ```
59
+
60
+ Then:
61
+
62
+ 1. Open `http://localhost:3000`.
63
+ 2. Click `Launch checkout via server cookie capture` to see the hosted checkout flow the package creates.
64
+ 3. In another terminal, send the fixed webhook fixture already used by the test suite:
65
+
66
+ ```bash
67
+ export CREEM_WEBHOOK_SECRET=your_real_webhook_secret
68
+ curl -i http://localhost:3000/api/webhook/creem \
69
+ -H "content-type: application/json" \
70
+ -H "creem-signature: $(node --input-type=module -e 'import { createHmac } from \"node:crypto\"; import { readFileSync } from \"node:fs\"; const rawBody = readFileSync(\"tests/fixtures/checkout-completed.json\", \"utf8\"); process.stdout.write(createHmac(\"sha256\", process.env.CREEM_WEBHOOK_SECRET).update(rawBody).digest(\"hex\"));')" \
71
+ --data-binary @tests/fixtures/checkout-completed.json
72
+ ```
73
+
74
+ You should see:
75
+
76
+ - `HTTP/1.1 200 OK` from the webhook route
77
+ - `[example-next] forwarding payload to DataFast ...`
78
+ - `[example-next] webhook processed ...`
79
+
80
+ If you prefer Express, swap the env file and dev command:
81
+
82
+ ```bash
83
+ cp example-express/.env.example example-express/.env.local
84
+ pnpm --filter example-express dev
85
+ ```
86
+
87
+ Use the same fixture and `curl` command against `http://localhost:3000/api/webhook/creem`. The key success signal there is `[example-express] forwarding payload to DataFast ...`.
88
+
89
+ For the longer setup, tunnel, and verification flow, see [`example-next/README.md`](./example-next/README.md), [`example-express/README.md`](./example-express/README.md), and [`docs/development.md`](./docs/development.md).
90
+
91
+ ## Integrate with AI Agents
92
+
93
+ Paste this prompt into Claude Code, Cursor, Codex, or any AI coding agent:
94
+
95
+ ```text
96
+ Use curl to download, read and follow: https://raw.githubusercontent.com/santigamo/creem-datafast/main/SKILL.md
97
+ ```
98
+
99
+ ## Installation
100
+
101
+ ```bash
102
+ pnpm add creem-datafast
103
+ ```
104
+
105
+ Internally the package wraps the official `creem` Core SDK, so you do not need to install `creem` separately in a normal consumer app.
106
+
107
+ ## Quickstart
108
+
109
+ ### Next.js
110
+
111
+ Install the package, create a shared client, then use the included route handler adapter.
112
+
113
+ ```ts
114
+ // lib/creem-datafast.ts
115
+ import { createCreemDataFast } from "creem-datafast";
116
+
117
+ export const creemDataFast = createCreemDataFast({
118
+ creemApiKey: process.env.CREEM_API_KEY!,
119
+ creemWebhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
120
+ datafastApiKey: process.env.DATAFAST_API_KEY!,
121
+ testMode: true
122
+ });
123
+ ```
124
+
125
+ ```ts
126
+ // app/api/checkout/route.ts
127
+ import { NextResponse } from "next/server";
128
+ import { creemDataFast } from "@/lib/creem-datafast";
129
+
130
+ export const runtime = "nodejs";
131
+
132
+ export async function POST(request: Request) {
133
+ const { checkoutUrl } = await creemDataFast.createCheckout(
134
+ {
135
+ productId: process.env.CREEM_PRODUCT_ID!,
136
+ successUrl: `${process.env.APP_BASE_URL!}/success`
137
+ },
138
+ { request }
139
+ );
140
+
141
+ return NextResponse.redirect(checkoutUrl, { status: 303 });
142
+ }
143
+ ```
144
+
145
+ ```ts
146
+ // app/api/webhook/creem/route.ts
147
+ import { createNextWebhookHandler } from "creem-datafast/next";
148
+ import { creemDataFast } from "@/lib/creem-datafast";
149
+
150
+ export const runtime = "nodejs";
151
+ export const POST = createNextWebhookHandler(creemDataFast);
152
+ ```
153
+
154
+ ### Express
155
+
156
+ Use the framework-agnostic core in your app layer and keep the webhook route on raw body middleware.
157
+
158
+ ```ts
159
+ import express from "express";
160
+ import { createCreemDataFast } from "creem-datafast";
161
+ import { createExpressWebhookHandler } from "creem-datafast/express";
162
+
163
+ const app = express();
164
+ const creemDataFast = createCreemDataFast({
165
+ creemApiKey: process.env.CREEM_API_KEY!,
166
+ creemWebhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
167
+ datafastApiKey: process.env.DATAFAST_API_KEY!,
168
+ testMode: true
169
+ });
170
+
171
+ app.post("/api/checkout", async (req, res) => {
172
+ const { checkoutUrl } = await creemDataFast.createCheckout(
173
+ {
174
+ productId: process.env.CREEM_PRODUCT_ID!,
175
+ successUrl: `${process.env.APP_BASE_URL!}/success`
176
+ },
177
+ {
178
+ request: { headers: req.headers, url: req.url }
179
+ }
180
+ );
181
+
182
+ res.redirect(303, checkoutUrl);
183
+ });
184
+
185
+ app.post(
186
+ "/api/webhook/creem",
187
+ express.raw({ type: "application/json" }),
188
+ createExpressWebhookHandler(creemDataFast)
189
+ );
190
+ ```
191
+
192
+ ### Framework-Agnostic
193
+
194
+ Use `handleWebhook()` directly when your framework is not Next.js or Express. You just need the raw request body as a string and the request headers.
195
+
196
+ ```ts
197
+ import { createCreemDataFast, InvalidCreemSignatureError } from "creem-datafast";
198
+
199
+ const creemDataFast = createCreemDataFast({
200
+ creemApiKey: process.env.CREEM_API_KEY!,
201
+ creemWebhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
202
+ datafastApiKey: process.env.DATAFAST_API_KEY!,
203
+ testMode: true
204
+ });
205
+
206
+ // Works with any Node.js framework, and the core flow is smoke-validated on Cloudflare Workers with injected Creem/DataFast boundaries.
207
+ async function handleCreemWebhook(rawBody: string, headers: Record<string, string>) {
208
+ try {
209
+ const result = await creemDataFast.handleWebhook({ rawBody, headers });
210
+
211
+ if (result.ignored) {
212
+ return { status: 200, body: "Ignored" };
213
+ }
214
+
215
+ return { status: 200, body: "OK" };
216
+ } catch (error) {
217
+ if (error instanceof InvalidCreemSignatureError) {
218
+ return { status: 400, body: "Invalid signature" };
219
+ }
220
+
221
+ return { status: 500, body: "Internal error" };
222
+ }
223
+ }
224
+ ```
225
+
226
+ ### Client-Side Helper
227
+
228
+ Use the browser helper when your checkout request originates from the browser and cookies are not automatically forwarded to your backend (e.g. cross-origin fetch calls). In same-origin setups the server-side cookie capture handles this automatically.
229
+
230
+ ```ts
231
+ import { appendDataFastTracking, getDataFastTracking } from "creem-datafast/client";
232
+
233
+ const tracking = getDataFastTracking();
234
+ const checkoutEndpoint = appendDataFastTracking("/api/checkout", tracking);
235
+
236
+ // Then use checkoutEndpoint as your fetch URL:
237
+ const response = await fetch(checkoutEndpoint, { method: "POST" });
238
+ ```
239
+
240
+ Tracking precedence during checkout creation is:
241
+
242
+ 1. `params.tracking`
243
+ 2. `params.metadata.datafast_*`
244
+ 3. `request.url` query params
245
+ 4. cookies, using `request.headers.cookie` first and `cookieHeader` only to fill missing tracking fields
246
+
247
+ ## Advanced
248
+
249
+ ### Custom webhook response logic (Next.js)
250
+
251
+ If you need custom response logic in Next.js, use `handleWebhookRequest()` instead of `createNextWebhookHandler()`. It reads the raw body for you and forwards the webhook through the same core path. Since `handleWebhookRequest()` is a low-level helper, you are responsible for catching `InvalidCreemSignatureError` (-> 400) and unexpected errors (-> 500).
252
+
253
+ ```ts
254
+ import { handleWebhookRequest } from "creem-datafast/next";
255
+ import { InvalidCreemSignatureError } from "creem-datafast";
256
+ import { creemDataFast } from "@/lib/creem-datafast";
257
+
258
+ export const runtime = "nodejs";
259
+
260
+ export async function POST(request: Request) {
261
+ try {
262
+ const result = await handleWebhookRequest(creemDataFast, request);
263
+
264
+ if (result.ignored) {
265
+ return new Response("Ignored", { status: 200 });
266
+ }
267
+
268
+ return new Response("OK", { status: 200 });
269
+ } catch (error) {
270
+ if (error instanceof InvalidCreemSignatureError) {
271
+ return new Response("Invalid signature", { status: 400 });
272
+ }
273
+
274
+ return new Response("Internal error", { status: 500 });
275
+ }
276
+ }
277
+ ```
278
+
279
+ ### Idempotency
280
+
281
+ `handleWebhook()` uses an in-process `MemoryIdempotencyStore` by default. This is convenient for local development and single-instance deployments, but it is not safe for multi-instance production environments because deduplication does not survive process restarts or span multiple instances.
282
+
283
+ Recommended production setup:
284
+
285
+ ```bash
286
+ pnpm add @upstash/redis
287
+ ```
288
+
289
+ ```ts
290
+ import { Redis } from "@upstash/redis";
291
+ import { createCreemDataFast } from "creem-datafast";
292
+ import { createUpstashIdempotencyStore } from "creem-datafast/idempotency/upstash";
293
+
294
+ const redis = new Redis({
295
+ url: process.env.UPSTASH_REDIS_REST_URL!,
296
+ token: process.env.UPSTASH_REDIS_REST_TOKEN!
297
+ });
298
+
299
+ export const creemDataFast = createCreemDataFast({
300
+ creemApiKey: process.env.CREEM_API_KEY!,
301
+ creemWebhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
302
+ datafastApiKey: process.env.DATAFAST_API_KEY!,
303
+ idempotencyStore: createUpstashIdempotencyStore(redis)
304
+ });
305
+ ```
306
+
307
+ See [`docs/production-idempotency.md`](./docs/production-idempotency.md) for the `IdempotencyStore` contract, TTL guidance, and how to implement a custom store.
308
+
309
+ ## Configuration
310
+
311
+ Constructor options for `createCreemDataFast()`:
312
+
313
+ - `creemApiKey`: Creem Core SDK API key.
314
+ - `creemWebhookSecret`: secret used to validate `creem-signature`.
315
+ - `datafastApiKey`: bearer token for DataFast payments.
316
+ - `testMode`: set to `true` to target `https://test-api.creem.io`. Defaults to `false`.
317
+ - `timeoutMs`: per-request timeout for DataFast forwarding. Defaults to `8000`.
318
+ - `retry.retries`: additional retry attempts after the initial DataFast request, so `1` means up to `2` total attempts. Defaults to `1`.
319
+ - `retry.baseDelayMs`: base backoff delay in milliseconds. Defaults to `250`.
320
+ - `retry.maxDelayMs`: maximum backoff delay in milliseconds. Defaults to `2000`.
321
+ - `strictTracking`: throw `MissingTrackingError` when no `datafast_visitor_id` is found at checkout. Defaults to `false`.
322
+ - `idempotencyStore`: custom `IdempotencyStore` for distributed deduplication.
323
+ - `logger`: inject a custom logger implementing `{ debug, info, warn, error }`.
324
+ - `creemClient`: inject a pre-configured Creem SDK instance instead of using `creemApiKey`.
325
+
326
+ ## API Reference
327
+
328
+ ```ts
329
+ import {
330
+ createCreemDataFast,
331
+ CreemDataFastError,
332
+ DataFastRequestError,
333
+ InvalidCreemSignatureError,
334
+ MissingTrackingError,
335
+ MemoryIdempotencyStore
336
+ } from "creem-datafast";
337
+ import { createNextWebhookHandler, handleWebhookRequest } from "creem-datafast/next";
338
+ import { createExpressWebhookHandler } from "creem-datafast/express";
339
+ import { appendDataFastTracking, getDataFastTracking } from "creem-datafast/client";
340
+ import { createUpstashIdempotencyStore } from "creem-datafast/idempotency/upstash";
341
+ ```
342
+
343
+ Root API:
344
+
345
+ - `createCreemDataFast(options)` — returns a `CreemDataFastClient`.
346
+ - `client.createCheckout(params, context?)` — creates a Creem checkout with injected DataFast tracking.
347
+ - `client.handleWebhook({ rawBody, headers })` — verifies, deduplicates, maps, and forwards a webhook.
348
+ - `client.verifyWebhookSignature(rawBody, headers)` — returns `true` or `false` for signature validity; throws `InvalidCreemSignatureError` when `creem-signature` is missing.
349
+
350
+ Error classes:
351
+
352
+ - `CreemDataFastError` — base class for all package errors.
353
+ - `InvalidCreemSignatureError` — webhook signature is missing or invalid.
354
+ - `MissingTrackingError` — thrown by `createCheckout()` when `strictTracking` is enabled and no `datafast_visitor_id` is found.
355
+ - `DataFastRequestError` — DataFast API request failed. Exposes `.retryable`, `.status`, and `.requestId`.
356
+
357
+ ## Troubleshooting
358
+
359
+ - Invalid webhook signature: make sure the handler reads the raw request body, not parsed JSON.
360
+ - Missing `creem-signature` header: `verifyWebhookSignature()` and `handleWebhook()` throw `InvalidCreemSignatureError` because the request is malformed.
361
+ - Missing visitor tracking: the checkout still works by default; enable `strictTracking` if you want the request to fail instead.
362
+ - Double-counted revenue from DataFast: if you use the DataFast tracking script alongside server-side webhook forwarding, the same payment can be recorded twice — once by the script detecting URL parameters on the success page, and once by the webhook. Add `data-disable-payments="true"` to the DataFast script tag when using `creem-datafast` for server-side attribution.
363
+ - Wrong amount format: Creem amounts are interpreted as minor units and converted into decimal major units before sending to DataFast.
364
+ - Refund semantics: `refund.created` forwards the refunded amount as a new DataFast payment with `refunded: true` and uses the Creem refund id as `transaction_id`.
365
+ - Duplicate forwards: the built-in `MemoryIdempotencyStore` is `dev / single-instance only`. For multi-instance deployments, pass a durable atomic `idempotencyStore` such as `createUpstashIdempotencyStore(redis)`. See [`docs/production-idempotency.md`](./docs/production-idempotency.md).
366
+ - Slow or flaky DataFast responses: forwarding uses an `8000ms` timeout by default and retries only network errors, timeouts, and `408` / `429` / `5xx` responses.
367
+
368
+ ## Compatibility
369
+
370
+ - Node 18+ runtime. ESM-only (`import`, not `require()`).
371
+ - Framework-agnostic core is smoke-validated on Cloudflare Workers and Bun.
372
+ - Next.js Route Handlers on the Node runtime.
373
+ - Express webhook routes using `express.raw({ type: "application/json" })`.
374
+
375
+ ## Development
376
+
377
+ See [`docs/development.md`](./docs/development.md) for package checks, CI pipeline, runnable examples, and manual local verification.
@@ -0,0 +1,46 @@
1
+ // src/core/errors.ts
2
+ var CreemDataFastError = class extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = new.target.name;
6
+ }
7
+ };
8
+ var InvalidCreemSignatureError = class extends CreemDataFastError {
9
+ };
10
+ var MissingTrackingError = class extends CreemDataFastError {
11
+ };
12
+ var UnsupportedWebhookEventError = class extends CreemDataFastError {
13
+ };
14
+ var DataFastRequestError = class extends CreemDataFastError {
15
+ constructor(message, details, errorOptions) {
16
+ super(message, errorOptions);
17
+ this.details = details;
18
+ }
19
+ get status() {
20
+ return this.details.status;
21
+ }
22
+ get statusText() {
23
+ return this.details.statusText;
24
+ }
25
+ get requestId() {
26
+ return this.details.requestId;
27
+ }
28
+ get retryable() {
29
+ return this.details.retryable;
30
+ }
31
+ get responseBody() {
32
+ return this.details.responseBody;
33
+ }
34
+ };
35
+ var TransactionHydrationError = class extends CreemDataFastError {
36
+ };
37
+
38
+ export {
39
+ CreemDataFastError,
40
+ InvalidCreemSignatureError,
41
+ MissingTrackingError,
42
+ UnsupportedWebhookEventError,
43
+ DataFastRequestError,
44
+ TransactionHydrationError
45
+ };
46
+ //# sourceMappingURL=chunk-EPUWLMCL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/errors.ts"],"sourcesContent":["export class CreemDataFastError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n }\n}\n\nexport class InvalidCreemSignatureError extends CreemDataFastError {}\n\nexport class MissingTrackingError extends CreemDataFastError {}\n\nexport class UnsupportedWebhookEventError extends CreemDataFastError {}\n\nexport class DataFastRequestError extends CreemDataFastError {\n constructor(\n message: string,\n public readonly details: {\n status?: number;\n statusText?: string;\n requestId?: string;\n retryable: boolean;\n responseBody?: unknown;\n },\n errorOptions?: ErrorOptions\n ) {\n super(message, errorOptions);\n }\n\n get status(): number | undefined {\n return this.details.status;\n }\n\n get statusText(): string | undefined {\n return this.details.statusText;\n }\n\n get requestId(): string | undefined {\n return this.details.requestId;\n }\n\n get retryable(): boolean {\n return this.details.retryable;\n }\n\n get responseBody(): unknown {\n return this.details.responseBody;\n }\n}\n\nexport class TransactionHydrationError extends CreemDataFastError {}\n"],"mappings":";AAAO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAAY,SAAiB,SAAwB;AACnD,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO,WAAW;AAAA,EACzB;AACF;AAEO,IAAM,6BAAN,cAAyC,mBAAmB;AAAC;AAE7D,IAAM,uBAAN,cAAmC,mBAAmB;AAAC;AAEvD,IAAM,+BAAN,cAA2C,mBAAmB;AAAC;AAE/D,IAAM,uBAAN,cAAmC,mBAAmB;AAAA,EAC3D,YACE,SACgB,SAOhB,cACA;AACA,UAAM,SAAS,YAAY;AATX;AAAA,EAUlB;AAAA,EAEA,IAAI,SAA6B;AAC/B,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,IAAI,aAAiC;AACnC,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,IAAI,YAAgC;AAClC,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,IAAI,YAAqB;AACvB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,IAAI,eAAwB;AAC1B,WAAO,KAAK,QAAQ;AAAA,EACtB;AACF;AAEO,IAAM,4BAAN,cAAwC,mBAAmB;AAAC;","names":[]}
@@ -0,0 +1,43 @@
1
+ // src/core/cookies.ts
2
+ function decodeCookieValue(value) {
3
+ try {
4
+ return decodeURIComponent(value);
5
+ } catch {
6
+ return value;
7
+ }
8
+ }
9
+ function parseCookieHeader(cookieHeader) {
10
+ const cookies = {};
11
+ for (const part of cookieHeader.split(";")) {
12
+ const trimmed = part.trim();
13
+ if (!trimmed) {
14
+ continue;
15
+ }
16
+ const separatorIndex = trimmed.indexOf("=");
17
+ if (separatorIndex === -1) {
18
+ continue;
19
+ }
20
+ const key = trimmed.slice(0, separatorIndex).trim();
21
+ const value = trimmed.slice(separatorIndex + 1).trim();
22
+ if (!key) {
23
+ continue;
24
+ }
25
+ cookies[key] = decodeCookieValue(value);
26
+ }
27
+ return cookies;
28
+ }
29
+ function readTrackingFromCookieHeader(cookieHeader) {
30
+ if (!cookieHeader) {
31
+ return {};
32
+ }
33
+ const cookies = parseCookieHeader(cookieHeader);
34
+ return {
35
+ visitorId: cookies.datafast_visitor_id,
36
+ sessionId: cookies.datafast_session_id
37
+ };
38
+ }
39
+
40
+ export {
41
+ readTrackingFromCookieHeader
42
+ };
43
+ //# sourceMappingURL=chunk-MHG2AARF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/cookies.ts"],"sourcesContent":["import type { DataFastTracking } from \"./types.js\";\n\nfunction decodeCookieValue(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nexport function parseCookieHeader(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (!trimmed) {\n continue;\n }\n\n const separatorIndex = trimmed.indexOf(\"=\");\n if (separatorIndex === -1) {\n continue;\n }\n\n const key = trimmed.slice(0, separatorIndex).trim();\n const value = trimmed.slice(separatorIndex + 1).trim();\n if (!key) {\n continue;\n }\n\n cookies[key] = decodeCookieValue(value);\n }\n\n return cookies;\n}\n\nexport function readTrackingFromCookieHeader(cookieHeader?: string): DataFastTracking {\n if (!cookieHeader) {\n return {};\n }\n\n const cookies = parseCookieHeader(cookieHeader);\n\n return {\n visitorId: cookies.datafast_visitor_id,\n sessionId: cookies.datafast_session_id\n };\n}\n"],"mappings":";AAEA,SAAS,kBAAkB,OAAuB;AAChD,MAAI;AACF,WAAO,mBAAmB,KAAK;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,kBAAkB,cAA8C;AAC9E,QAAM,UAAkC,CAAC;AAEzC,aAAW,QAAQ,aAAa,MAAM,GAAG,GAAG;AAC1C,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AAEA,UAAM,iBAAiB,QAAQ,QAAQ,GAAG;AAC1C,QAAI,mBAAmB,IAAI;AACzB;AAAA,IACF;AAEA,UAAM,MAAM,QAAQ,MAAM,GAAG,cAAc,EAAE,KAAK;AAClD,UAAM,QAAQ,QAAQ,MAAM,iBAAiB,CAAC,EAAE,KAAK;AACrD,QAAI,CAAC,KAAK;AACR;AAAA,IACF;AAEA,YAAQ,GAAG,IAAI,kBAAkB,KAAK;AAAA,EACxC;AAEA,SAAO;AACT;AAEO,SAAS,6BAA6B,cAAyC;AACpF,MAAI,CAAC,cAAc;AACjB,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,UAAU,kBAAkB,YAAY;AAE9C,SAAO;AAAA,IACL,WAAW,QAAQ;AAAA,IACnB,WAAW,QAAQ;AAAA,EACrB;AACF;","names":[]}
@@ -0,0 +1,6 @@
1
+ import { B as BrowserTrackingResult } from './types-4FOgdKj_.js';
2
+
3
+ declare function getDataFastTracking(cookieSource?: string): BrowserTrackingResult;
4
+ declare function appendDataFastTracking(inputUrl: string | URL, tracking?: BrowserTrackingResult): string;
5
+
6
+ export { BrowserTrackingResult, appendDataFastTracking, getDataFastTracking };
package/dist/client.js ADDED
@@ -0,0 +1,37 @@
1
+ import {
2
+ readTrackingFromCookieHeader
3
+ } from "./chunk-MHG2AARF.js";
4
+
5
+ // src/client/browser.ts
6
+ function resolveCookieSource(cookieSource) {
7
+ if (cookieSource !== void 0) {
8
+ return cookieSource;
9
+ }
10
+ if (typeof document === "undefined") {
11
+ return void 0;
12
+ }
13
+ return document.cookie;
14
+ }
15
+ function getDataFastTracking(cookieSource) {
16
+ return readTrackingFromCookieHeader(resolveCookieSource(cookieSource));
17
+ }
18
+ function appendDataFastTracking(inputUrl, tracking = getDataFastTracking()) {
19
+ const base = typeof window === "undefined" ? "http://localhost" : window.location.origin;
20
+ const url = inputUrl instanceof URL ? new URL(inputUrl.toString()) : new URL(inputUrl, base);
21
+ if (tracking.visitorId) {
22
+ url.searchParams.set("datafast_visitor_id", tracking.visitorId);
23
+ }
24
+ if (tracking.sessionId) {
25
+ url.searchParams.set("datafast_session_id", tracking.sessionId);
26
+ }
27
+ if (typeof inputUrl === "string" && inputUrl.startsWith("/")) {
28
+ const query = url.searchParams.toString();
29
+ return query ? `${url.pathname}?${query}` : url.pathname;
30
+ }
31
+ return url.toString();
32
+ }
33
+ export {
34
+ appendDataFastTracking,
35
+ getDataFastTracking
36
+ };
37
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/browser.ts"],"sourcesContent":["import { readTrackingFromCookieHeader } from \"../core/cookies.js\";\nimport type { BrowserTrackingResult } from \"../core/types.js\";\n\nfunction resolveCookieSource(cookieSource?: string): string | undefined {\n if (cookieSource !== undefined) {\n return cookieSource;\n }\n\n if (typeof document === \"undefined\") {\n return undefined;\n }\n\n return document.cookie;\n}\n\nexport function getDataFastTracking(cookieSource?: string): BrowserTrackingResult {\n return readTrackingFromCookieHeader(resolveCookieSource(cookieSource));\n}\n\nexport function appendDataFastTracking(\n inputUrl: string | URL,\n tracking: BrowserTrackingResult = getDataFastTracking()\n): string {\n const base = typeof window === \"undefined\" ? \"http://localhost\" : window.location.origin;\n const url = inputUrl instanceof URL ? new URL(inputUrl.toString()) : new URL(inputUrl, base);\n\n if (tracking.visitorId) {\n url.searchParams.set(\"datafast_visitor_id\", tracking.visitorId);\n }\n\n if (tracking.sessionId) {\n url.searchParams.set(\"datafast_session_id\", tracking.sessionId);\n }\n\n if (typeof inputUrl === \"string\" && inputUrl.startsWith(\"/\")) {\n const query = url.searchParams.toString();\n return query ? `${url.pathname}?${query}` : url.pathname;\n }\n\n return url.toString();\n}\n"],"mappings":";;;;;AAGA,SAAS,oBAAoB,cAA2C;AACtE,MAAI,iBAAiB,QAAW;AAC9B,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO;AAAA,EACT;AAEA,SAAO,SAAS;AAClB;AAEO,SAAS,oBAAoB,cAA8C;AAChF,SAAO,6BAA6B,oBAAoB,YAAY,CAAC;AACvE;AAEO,SAAS,uBACd,UACA,WAAkC,oBAAoB,GAC9C;AACR,QAAM,OAAO,OAAO,WAAW,cAAc,qBAAqB,OAAO,SAAS;AAClF,QAAM,MAAM,oBAAoB,MAAM,IAAI,IAAI,SAAS,SAAS,CAAC,IAAI,IAAI,IAAI,UAAU,IAAI;AAE3F,MAAI,SAAS,WAAW;AACtB,QAAI,aAAa,IAAI,uBAAuB,SAAS,SAAS;AAAA,EAChE;AAEA,MAAI,SAAS,WAAW;AACtB,QAAI,aAAa,IAAI,uBAAuB,SAAS,SAAS;AAAA,EAChE;AAEA,MAAI,OAAO,aAAa,YAAY,SAAS,WAAW,GAAG,GAAG;AAC5D,UAAM,QAAQ,IAAI,aAAa,SAAS;AACxC,WAAO,QAAQ,GAAG,IAAI,QAAQ,IAAI,KAAK,KAAK,IAAI;AAAA,EAClD;AAEA,SAAO,IAAI,SAAS;AACtB;","names":[]}
@@ -0,0 +1,5 @@
1
+ import { C as CreemDataFastClient, E as ExpressWebhookHandlerOptions, a as ExpressLikeRequest, b as ExpressLikeResponse } from './types-4FOgdKj_.js';
2
+
3
+ declare function createExpressWebhookHandler(client: CreemDataFastClient, options?: ExpressWebhookHandlerOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse) => Promise<void>;
4
+
5
+ export { ExpressLikeRequest, ExpressLikeResponse, ExpressWebhookHandlerOptions, createExpressWebhookHandler };
@@ -0,0 +1,30 @@
1
+ import {
2
+ InvalidCreemSignatureError
3
+ } from "./chunk-EPUWLMCL.js";
4
+
5
+ // src/adapters/express.ts
6
+ function getRawBody(body) {
7
+ return typeof body === "string" ? body : body.toString("utf8");
8
+ }
9
+ function createExpressWebhookHandler(client, options = {}) {
10
+ return async (req, res) => {
11
+ try {
12
+ await client.handleWebhook({
13
+ rawBody: getRawBody(req.body),
14
+ headers: req.headers
15
+ });
16
+ res.status(200).send("OK");
17
+ } catch (error) {
18
+ await options.onError?.(error);
19
+ if (error instanceof InvalidCreemSignatureError) {
20
+ res.status(400).send("Invalid signature");
21
+ return;
22
+ }
23
+ res.status(500).send("Internal error");
24
+ }
25
+ };
26
+ }
27
+ export {
28
+ createExpressWebhookHandler
29
+ };
30
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/express.ts"],"sourcesContent":["import { InvalidCreemSignatureError } from \"../core/errors.js\";\nimport type {\n CreemDataFastClient,\n ExpressLikeRequest,\n ExpressLikeResponse,\n ExpressWebhookHandlerOptions\n} from \"../core/types.js\";\n\nfunction getRawBody(body: Buffer | string): string {\n return typeof body === \"string\" ? body : body.toString(\"utf8\");\n}\n\nexport function createExpressWebhookHandler(\n client: CreemDataFastClient,\n options: ExpressWebhookHandlerOptions = {}\n): (req: ExpressLikeRequest, res: ExpressLikeResponse) => Promise<void> {\n return async (req: ExpressLikeRequest, res: ExpressLikeResponse) => {\n try {\n await client.handleWebhook({\n rawBody: getRawBody(req.body),\n headers: req.headers\n });\n\n res.status(200).send(\"OK\");\n } catch (error) {\n await options.onError?.(error);\n\n if (error instanceof InvalidCreemSignatureError) {\n res.status(400).send(\"Invalid signature\");\n return;\n }\n\n res.status(500).send(\"Internal error\");\n }\n };\n}\n"],"mappings":";;;;;AAQA,SAAS,WAAW,MAA+B;AACjD,SAAO,OAAO,SAAS,WAAW,OAAO,KAAK,SAAS,MAAM;AAC/D;AAEO,SAAS,4BACd,QACA,UAAwC,CAAC,GAC6B;AACtE,SAAO,OAAO,KAAyB,QAA6B;AAClE,QAAI;AACF,YAAM,OAAO,cAAc;AAAA,QACzB,SAAS,WAAW,IAAI,IAAI;AAAA,QAC5B,SAAS,IAAI;AAAA,MACf,CAAC;AAED,UAAI,OAAO,GAAG,EAAE,KAAK,IAAI;AAAA,IAC3B,SAAS,OAAO;AACd,YAAM,QAAQ,UAAU,KAAK;AAE7B,UAAI,iBAAiB,4BAA4B;AAC/C,YAAI,OAAO,GAAG,EAAE,KAAK,mBAAmB;AACxC;AAAA,MACF;AAEA,UAAI,OAAO,GAAG,EAAE,KAAK,gBAAgB;AAAA,IACvC;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,12 @@
1
+ import { Redis } from '@upstash/redis';
2
+ import { I as IdempotencyStore } from '../types-4FOgdKj_.js';
3
+
4
+ type UpstashRedisLike = Pick<Redis, "del" | "set">;
5
+ /**
6
+ * Production-ready idempotency store backed by Upstash Redis atomic commands.
7
+ *
8
+ * Install `@upstash/redis` in the consuming app and pass an initialized client.
9
+ */
10
+ declare function createUpstashIdempotencyStore(redis: UpstashRedisLike): IdempotencyStore;
11
+
12
+ export { createUpstashIdempotencyStore };
@@ -0,0 +1,24 @@
1
+ // src/idempotency/upstash.ts
2
+ function createUpstashIdempotencyStore(redis) {
3
+ return {
4
+ async claim(key, ttlSeconds = 300) {
5
+ const result = await redis.set(key, "processing", {
6
+ ex: ttlSeconds,
7
+ nx: true
8
+ });
9
+ return result === "OK";
10
+ },
11
+ async complete(key, ttlSeconds = 86400) {
12
+ await redis.set(key, "processed", {
13
+ ex: ttlSeconds
14
+ });
15
+ },
16
+ async release(key) {
17
+ await redis.del(key);
18
+ }
19
+ };
20
+ }
21
+ export {
22
+ createUpstashIdempotencyStore
23
+ };
24
+ //# sourceMappingURL=upstash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/idempotency/upstash.ts"],"sourcesContent":["import type { Redis } from \"@upstash/redis\";\n\nimport type { IdempotencyStore } from \"../core/types.js\";\n\ntype UpstashRedisLike = Pick<Redis, \"del\" | \"set\">;\n\n/**\n * Production-ready idempotency store backed by Upstash Redis atomic commands.\n *\n * Install `@upstash/redis` in the consuming app and pass an initialized client.\n */\nexport function createUpstashIdempotencyStore(redis: UpstashRedisLike): IdempotencyStore {\n return {\n async claim(key, ttlSeconds = 300) {\n const result = await redis.set(key, \"processing\", {\n ex: ttlSeconds,\n nx: true\n });\n return result === \"OK\";\n },\n async complete(key, ttlSeconds = 86400) {\n await redis.set(key, \"processed\", {\n ex: ttlSeconds\n });\n },\n async release(key) {\n await redis.del(key);\n }\n };\n}\n"],"mappings":";AAWO,SAAS,8BAA8B,OAA2C;AACvF,SAAO;AAAA,IACL,MAAM,MAAM,KAAK,aAAa,KAAK;AACjC,YAAM,SAAS,MAAM,MAAM,IAAI,KAAK,cAAc;AAAA,QAChD,IAAI;AAAA,QACJ,IAAI;AAAA,MACN,CAAC;AACD,aAAO,WAAW;AAAA,IACpB;AAAA,IACA,MAAM,SAAS,KAAK,aAAa,OAAO;AACtC,YAAM,MAAM,IAAI,KAAK,aAAa;AAAA,QAChC,IAAI;AAAA,MACN,CAAC;AAAA,IACH;AAAA,IACA,MAAM,QAAQ,KAAK;AACjB,YAAM,MAAM,IAAI,GAAG;AAAA,IACrB;AAAA,EACF;AACF;","names":[]}