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 +21 -0
- package/README.md +377 -0
- package/dist/chunk-EPUWLMCL.js +46 -0
- package/dist/chunk-EPUWLMCL.js.map +1 -0
- package/dist/chunk-MHG2AARF.js +43 -0
- package/dist/chunk-MHG2AARF.js.map +1 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +37 -0
- package/dist/client.js.map +1 -0
- package/dist/express.d.ts +5 -0
- package/dist/express.js +30 -0
- package/dist/express.js.map +1 -0
- package/dist/idempotency/upstash.d.ts +12 -0
- package/dist/idempotency/upstash.js +24 -0
- package/dist/idempotency/upstash.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +857 -0
- package/dist/index.js.map +1 -0
- package/dist/next.d.ts +20 -0
- package/dist/next.js +31 -0
- package/dist/next.js.map +1 -0
- package/dist/types-4FOgdKj_.d.ts +215 -0
- package/package.json +99 -0
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
|
+
[](https://github.com/santigamo/creem-datafast/actions/workflows/ci.yml)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](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":[]}
|
package/dist/client.d.ts
ADDED
|
@@ -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 };
|
package/dist/express.js
ADDED
|
@@ -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":[]}
|