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/dist/index.js ADDED
@@ -0,0 +1,857 @@
1
+ import {
2
+ readTrackingFromCookieHeader
3
+ } from "./chunk-MHG2AARF.js";
4
+ import {
5
+ CreemDataFastError,
6
+ DataFastRequestError,
7
+ InvalidCreemSignatureError,
8
+ MissingTrackingError,
9
+ TransactionHydrationError,
10
+ UnsupportedWebhookEventError
11
+ } from "./chunk-EPUWLMCL.js";
12
+
13
+ // src/core/headers.ts
14
+ function isHeadersInstance(headers) {
15
+ return typeof Headers !== "undefined" && headers instanceof Headers;
16
+ }
17
+ function normalizeArrayValue(value) {
18
+ if (typeof value === "string") {
19
+ return value;
20
+ }
21
+ if (Array.isArray(value)) {
22
+ return value[0];
23
+ }
24
+ return void 0;
25
+ }
26
+ function getHeaderValue(headers, name) {
27
+ if (isHeadersInstance(headers)) {
28
+ return headers.get(name) ?? headers.get(name.toLowerCase()) ?? void 0;
29
+ }
30
+ const expected = name.toLowerCase();
31
+ for (const [key, value] of Object.entries(headers)) {
32
+ if (key.toLowerCase() === expected) {
33
+ return normalizeArrayValue(value);
34
+ }
35
+ }
36
+ return void 0;
37
+ }
38
+
39
+ // src/core/metadata.ts
40
+ function asMetadataString(value) {
41
+ if (typeof value === "string" && value.length > 0) {
42
+ return value;
43
+ }
44
+ return void 0;
45
+ }
46
+ function readTrackingFromMetadata(metadata) {
47
+ if (!metadata) {
48
+ return {};
49
+ }
50
+ return {
51
+ visitorId: asMetadataString(metadata.datafast_visitor_id),
52
+ sessionId: asMetadataString(metadata.datafast_session_id)
53
+ };
54
+ }
55
+ function mergeTrackingIntoMetadata(metadata, tracking, options = {}) {
56
+ const result = { ...metadata ?? {} };
57
+ const captureSessionId = options.captureSessionId ?? true;
58
+ const preferTracking = options.preferTracking ?? false;
59
+ if (tracking.visitorId && (preferTracking || result.datafast_visitor_id === void 0)) {
60
+ result.datafast_visitor_id = tracking.visitorId;
61
+ }
62
+ if (captureSessionId && tracking.sessionId && (preferTracking || result.datafast_session_id === void 0)) {
63
+ result.datafast_session_id = tracking.sessionId;
64
+ }
65
+ return result;
66
+ }
67
+
68
+ // src/core/checkout.ts
69
+ function hasExplicitTracking(tracking) {
70
+ return Boolean(tracking?.visitorId || tracking?.sessionId);
71
+ }
72
+ function resolveTracking(explicit, metadataTracking, queryTracking, cookieTracking, captureSessionId) {
73
+ return {
74
+ visitorId: explicit?.visitorId ?? metadataTracking.visitorId ?? queryTracking.visitorId ?? cookieTracking.visitorId,
75
+ sessionId: captureSessionId ? explicit?.sessionId ?? metadataTracking.sessionId ?? queryTracking.sessionId ?? cookieTracking.sessionId : void 0
76
+ };
77
+ }
78
+ function resolveCookieTracking(context) {
79
+ const requestCookieTracking = readTrackingFromCookieHeader(
80
+ context?.request ? getHeaderValue(context.request.headers, "cookie") : void 0
81
+ );
82
+ const fallbackCookieTracking = readTrackingFromCookieHeader(context?.cookieHeader);
83
+ return {
84
+ visitorId: requestCookieTracking.visitorId ?? fallbackCookieTracking.visitorId,
85
+ sessionId: requestCookieTracking.sessionId ?? fallbackCookieTracking.sessionId
86
+ };
87
+ }
88
+ function readTrackingFromRequestUrl(request) {
89
+ const requestUrl = request?.url;
90
+ if (!requestUrl) {
91
+ return {};
92
+ }
93
+ try {
94
+ const url = new URL(requestUrl, "http://localhost");
95
+ return {
96
+ visitorId: url.searchParams.get("datafast_visitor_id") ?? void 0,
97
+ sessionId: url.searchParams.get("datafast_session_id") ?? void 0
98
+ };
99
+ } catch {
100
+ return {};
101
+ }
102
+ }
103
+ async function createCheckout(params, context, dependencies) {
104
+ const cookieTracking = resolveCookieTracking(context);
105
+ const metadataTracking = readTrackingFromMetadata(params.metadata);
106
+ const queryTracking = readTrackingFromRequestUrl(context?.request);
107
+ const tracking = resolveTracking(
108
+ params.tracking,
109
+ metadataTracking,
110
+ queryTracking,
111
+ cookieTracking,
112
+ dependencies.captureSessionId
113
+ );
114
+ const strictTracking = context?.strictTracking ?? dependencies.strictTracking;
115
+ if (strictTracking && !tracking.visitorId) {
116
+ throw new MissingTrackingError("Missing datafast_visitor_id while strict tracking is enabled.");
117
+ }
118
+ if (!tracking.visitorId) {
119
+ dependencies.logger.warn("Creating Creem checkout without DataFast visitor tracking.");
120
+ }
121
+ const finalMetadata = mergeTrackingIntoMetadata(params.metadata, tracking, {
122
+ captureSessionId: dependencies.captureSessionId,
123
+ preferTracking: hasExplicitTracking(params.tracking)
124
+ });
125
+ const raw = await dependencies.creem.createCheckout({
126
+ productId: params.productId,
127
+ successUrl: params.successUrl,
128
+ requestId: params.requestId,
129
+ units: params.units,
130
+ discountCode: params.discountCode,
131
+ customer: params.customer,
132
+ customFields: params.customFields,
133
+ metadata: finalMetadata
134
+ });
135
+ const checkoutId = raw.id;
136
+ const checkoutUrl = raw.checkoutUrl ?? raw.checkout_url;
137
+ if (typeof checkoutId !== "string" || typeof checkoutUrl !== "string") {
138
+ throw new CreemDataFastError("Creem checkout response is missing id or checkoutUrl.");
139
+ }
140
+ return {
141
+ checkoutId,
142
+ checkoutUrl,
143
+ injectedTracking: tracking,
144
+ finalMetadata,
145
+ raw
146
+ };
147
+ }
148
+
149
+ // src/core/creem-client.ts
150
+ import { Creem } from "creem";
151
+ function assertCreemClient(candidate) {
152
+ if (!candidate || typeof candidate !== "object" || !("checkouts" in candidate) || !("transactions" in candidate)) {
153
+ throw new CreemDataFastError("Invalid creem client provided.");
154
+ }
155
+ }
156
+ function createCreemClient(options) {
157
+ const sdkCandidate = options.creemClient ?? (options.creemApiKey ? new Creem({
158
+ apiKey: options.creemApiKey,
159
+ serverIdx: options.testMode ? 1 : 0
160
+ }) : void 0);
161
+ if (!sdkCandidate) {
162
+ throw new CreemDataFastError("Missing creemApiKey or creemClient.");
163
+ }
164
+ assertCreemClient(sdkCandidate);
165
+ if (!sdkCandidate.checkouts || typeof sdkCandidate.checkouts.create !== "function" || !sdkCandidate.transactions || typeof sdkCandidate.transactions.getById !== "function") {
166
+ throw new CreemDataFastError("Provided creem client does not expose the expected SDK methods.");
167
+ }
168
+ return {
169
+ async createCheckout(request) {
170
+ return sdkCandidate.checkouts.create(request);
171
+ },
172
+ async getTransactionById(transactionId) {
173
+ return sdkCandidate.transactions.getById(transactionId);
174
+ }
175
+ };
176
+ }
177
+
178
+ // src/core/logger.ts
179
+ var noop = () => void 0;
180
+ var noopLogger = {
181
+ debug: noop,
182
+ info: noop,
183
+ warn: noop,
184
+ error: noop
185
+ };
186
+ function resolveLogger(logger) {
187
+ return logger ?? noopLogger;
188
+ }
189
+
190
+ // src/core/datafast-client.ts
191
+ var DATAFAST_PAYMENTS_URL = "https://datafa.st/api/v1/payments";
192
+ var DEFAULT_TIMEOUT_MS = 8e3;
193
+ var DEFAULT_RETRIES = 1;
194
+ var DEFAULT_BASE_DELAY_MS = 250;
195
+ var DEFAULT_MAX_DELAY_MS = 2e3;
196
+ var MAX_ERROR_BODY_LENGTH = 1024;
197
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
198
+ function resolveFetch(fetchImplementation) {
199
+ const resolved = fetchImplementation ?? globalThis.fetch;
200
+ if (!resolved) {
201
+ throw new DataFastRequestError("Fetch implementation is required to call DataFast.", {
202
+ retryable: false
203
+ });
204
+ }
205
+ return resolved;
206
+ }
207
+ function parseResponseBody(body) {
208
+ if (!body) {
209
+ return void 0;
210
+ }
211
+ try {
212
+ return JSON.parse(body);
213
+ } catch {
214
+ return body;
215
+ }
216
+ }
217
+ function truncateString(value) {
218
+ if (value.length <= MAX_ERROR_BODY_LENGTH) {
219
+ return value;
220
+ }
221
+ return `${value.slice(0, MAX_ERROR_BODY_LENGTH)}...[truncated]`;
222
+ }
223
+ function sanitizeResponseBody(body) {
224
+ if (body === void 0) {
225
+ return void 0;
226
+ }
227
+ if (typeof body === "string") {
228
+ return truncateString(body);
229
+ }
230
+ try {
231
+ const serialized = JSON.stringify(body);
232
+ if (serialized.length <= MAX_ERROR_BODY_LENGTH) {
233
+ return body;
234
+ }
235
+ return truncateString(serialized);
236
+ } catch {
237
+ return "[unserializable response body]";
238
+ }
239
+ }
240
+ function normalizePositiveInteger(value, fallback) {
241
+ if (value === void 0) {
242
+ return fallback;
243
+ }
244
+ if (!Number.isFinite(value) || value < 0) {
245
+ return fallback;
246
+ }
247
+ return Math.floor(value);
248
+ }
249
+ function resolveRetryConfig(retry) {
250
+ return {
251
+ retries: normalizePositiveInteger(retry?.retries, DEFAULT_RETRIES),
252
+ baseDelayMs: normalizePositiveInteger(retry?.baseDelayMs, DEFAULT_BASE_DELAY_MS),
253
+ maxDelayMs: normalizePositiveInteger(retry?.maxDelayMs, DEFAULT_MAX_DELAY_MS)
254
+ };
255
+ }
256
+ function resolveTimeoutMs(timeoutMs) {
257
+ return normalizePositiveInteger(timeoutMs, DEFAULT_TIMEOUT_MS);
258
+ }
259
+ function isAbortError(error) {
260
+ return error instanceof DOMException && error.name === "AbortError";
261
+ }
262
+ function getRequestId(response) {
263
+ return response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? response.headers.get("x-datafast-request-id") ?? void 0;
264
+ }
265
+ function isRetryableStatus(status) {
266
+ return RETRYABLE_STATUSES.has(status);
267
+ }
268
+ function computeDelayMs(attempt, retry) {
269
+ return Math.min(retry.baseDelayMs * 2 ** attempt + Math.random() * 100, retry.maxDelayMs);
270
+ }
271
+ async function sleep(delayMs) {
272
+ if (delayMs <= 0) {
273
+ return;
274
+ }
275
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
276
+ }
277
+ function createDataFastClient(options) {
278
+ const fetchImplementation = resolveFetch(options.fetch);
279
+ const logger = resolveLogger(options.logger);
280
+ const timeoutMs = resolveTimeoutMs(options.timeoutMs);
281
+ const retry = resolveRetryConfig(options.retry);
282
+ return {
283
+ async sendPayment(payload) {
284
+ for (let attempt = 0; attempt <= retry.retries; attempt += 1) {
285
+ const controller = new AbortController();
286
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
287
+ try {
288
+ const response = await fetchImplementation(DATAFAST_PAYMENTS_URL, {
289
+ method: "POST",
290
+ headers: {
291
+ Authorization: `Bearer ${options.datafastApiKey}`,
292
+ "Content-Type": "application/json"
293
+ },
294
+ body: JSON.stringify(payload),
295
+ signal: controller.signal
296
+ });
297
+ const responseText = await response.text();
298
+ const responseBody = sanitizeResponseBody(parseResponseBody(responseText));
299
+ if (response.ok) {
300
+ return responseBody;
301
+ }
302
+ const retryable = isRetryableStatus(response.status);
303
+ const error = new DataFastRequestError(
304
+ `DataFast request failed with status ${response.status}.`,
305
+ {
306
+ status: response.status,
307
+ statusText: response.statusText,
308
+ requestId: getRequestId(response),
309
+ retryable,
310
+ responseBody
311
+ }
312
+ );
313
+ if (retryable && attempt < retry.retries) {
314
+ logger.warn("Retrying DataFast request after retryable response.", {
315
+ attempt: attempt + 1,
316
+ nextAttempt: attempt + 2,
317
+ requestId: error.requestId,
318
+ status: error.status,
319
+ statusText: error.statusText
320
+ });
321
+ await sleep(computeDelayMs(attempt, retry));
322
+ continue;
323
+ }
324
+ throw error;
325
+ } catch (error) {
326
+ const retryable = isAbortError(error) || !(error instanceof DataFastRequestError);
327
+ if (retryable && attempt < retry.retries) {
328
+ logger.warn("Retrying DataFast request after transport failure.", {
329
+ attempt: attempt + 1,
330
+ nextAttempt: attempt + 2,
331
+ reason: isAbortError(error) ? "timeout" : "network_error"
332
+ });
333
+ await sleep(computeDelayMs(attempt, retry));
334
+ continue;
335
+ }
336
+ if (error instanceof DataFastRequestError) {
337
+ throw error;
338
+ }
339
+ if (isAbortError(error)) {
340
+ logger.warn("DataFast request timed out.", {
341
+ attempts: attempt + 1,
342
+ timeoutMs
343
+ });
344
+ throw new DataFastRequestError(
345
+ `DataFast request timed out after ${timeoutMs}ms.`,
346
+ {
347
+ retryable: true
348
+ },
349
+ { cause: error }
350
+ );
351
+ }
352
+ throw new DataFastRequestError(
353
+ "DataFast request failed due to a network error.",
354
+ {
355
+ retryable: true
356
+ },
357
+ { cause: error instanceof Error ? error : void 0 }
358
+ );
359
+ } finally {
360
+ clearTimeout(timeout);
361
+ }
362
+ }
363
+ throw new DataFastRequestError("DataFast request failed unexpectedly.", {
364
+ retryable: false
365
+ });
366
+ }
367
+ };
368
+ }
369
+
370
+ // src/core/idempotency.ts
371
+ var MemoryIdempotencyStore = class {
372
+ values = /* @__PURE__ */ new Map();
373
+ getActiveEntry(key) {
374
+ const entry = this.values.get(key);
375
+ if (!entry) {
376
+ return void 0;
377
+ }
378
+ if (Date.now() > entry.expiresAt) {
379
+ this.values.delete(key);
380
+ return void 0;
381
+ }
382
+ return entry;
383
+ }
384
+ async claim(key, ttlSeconds = 300) {
385
+ if (this.getActiveEntry(key)) {
386
+ return false;
387
+ }
388
+ this.values.set(key, {
389
+ expiresAt: Date.now() + ttlSeconds * 1e3,
390
+ status: "processing"
391
+ });
392
+ return true;
393
+ }
394
+ async complete(key, ttlSeconds = 86400) {
395
+ this.values.set(key, {
396
+ expiresAt: Date.now() + ttlSeconds * 1e3,
397
+ status: "processed"
398
+ });
399
+ }
400
+ async release(key) {
401
+ this.values.delete(key);
402
+ }
403
+ };
404
+ function resolveIdempotencyStore(store) {
405
+ return store ?? new MemoryIdempotencyStore();
406
+ }
407
+ function getIdempotencyKey(eventId) {
408
+ return `creem:event:${eventId}`;
409
+ }
410
+ async function claimEvent(eventId, store, ttlSeconds) {
411
+ return store.claim(getIdempotencyKey(eventId), ttlSeconds);
412
+ }
413
+ async function completeEvent(eventId, store, ttlSeconds) {
414
+ await store.complete(getIdempotencyKey(eventId), ttlSeconds);
415
+ }
416
+ async function releaseEvent(eventId, store) {
417
+ await store.release(getIdempotencyKey(eventId));
418
+ }
419
+
420
+ // src/core/signature.ts
421
+ var textEncoder = new TextEncoder();
422
+ function normalizeSignature(signature) {
423
+ return signature.trim().replace(/^sha256=/i, "");
424
+ }
425
+ function hexToUint8Array(input) {
426
+ if (input.length === 0 || input.length % 2 !== 0 || /[^0-9a-f]/iu.test(input)) {
427
+ return void 0;
428
+ }
429
+ const bytes = new Uint8Array(input.length / 2);
430
+ for (let index = 0; index < input.length; index += 2) {
431
+ const byte = Number.parseInt(input.slice(index, index + 2), 16);
432
+ if (Number.isNaN(byte)) {
433
+ return void 0;
434
+ }
435
+ bytes[index / 2] = byte;
436
+ }
437
+ return bytes;
438
+ }
439
+ function getWebCrypto() {
440
+ const subtle = globalThis.crypto?.subtle;
441
+ if (subtle) {
442
+ return subtle;
443
+ }
444
+ throw new CreemDataFastError("Web Crypto API is required to verify Creem signatures.");
445
+ }
446
+ function toArrayBuffer(bytes) {
447
+ if (bytes.buffer instanceof ArrayBuffer) {
448
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
449
+ }
450
+ return Uint8Array.from(bytes).buffer;
451
+ }
452
+ function extractHeader(headers, name) {
453
+ return getHeaderValue(headers, name);
454
+ }
455
+ async function verifyCreemSignature(rawBody, webhookSecret, signature) {
456
+ const signatureBytes = hexToUint8Array(normalizeSignature(signature).toLowerCase());
457
+ if (!signatureBytes) {
458
+ return false;
459
+ }
460
+ const subtle = getWebCrypto();
461
+ const key = await subtle.importKey(
462
+ "raw",
463
+ textEncoder.encode(webhookSecret),
464
+ {
465
+ name: "HMAC",
466
+ hash: "SHA-256"
467
+ },
468
+ false,
469
+ ["verify"]
470
+ );
471
+ return subtle.verify(
472
+ "HMAC",
473
+ key,
474
+ toArrayBuffer(signatureBytes),
475
+ toArrayBuffer(textEncoder.encode(rawBody))
476
+ );
477
+ }
478
+
479
+ // src/core/amount.ts
480
+ var ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
481
+ "BIF",
482
+ "CLP",
483
+ "DJF",
484
+ "GNF",
485
+ "JPY",
486
+ "KMF",
487
+ "KRW",
488
+ "MGA",
489
+ "PYG",
490
+ "RWF",
491
+ "UGX",
492
+ "VND",
493
+ "VUV",
494
+ "XAF",
495
+ "XOF",
496
+ "XPF"
497
+ ]);
498
+ var THREE_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set(["BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"]);
499
+ function currencyExponent(currency) {
500
+ const normalized = currency.toUpperCase();
501
+ if (ZERO_DECIMAL_CURRENCIES.has(normalized)) {
502
+ return 0;
503
+ }
504
+ if (THREE_DECIMAL_CURRENCIES.has(normalized)) {
505
+ return 3;
506
+ }
507
+ return 2;
508
+ }
509
+ function minorToMajor(amount, currency) {
510
+ const exponent = currencyExponent(currency);
511
+ return amount / 10 ** exponent;
512
+ }
513
+
514
+ // src/core/mapper.ts
515
+ function asRecord(value) {
516
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
517
+ return void 0;
518
+ }
519
+ return value;
520
+ }
521
+ function readMetadataValue(metadata, key) {
522
+ const value = metadata?.[key];
523
+ return typeof value === "string" && value.length > 0 ? value : void 0;
524
+ }
525
+ function toIsoTimestamp(value) {
526
+ if (typeof value === "string" && value.length > 0) {
527
+ return value;
528
+ }
529
+ if (typeof value !== "number" || Number.isNaN(value)) {
530
+ return void 0;
531
+ }
532
+ const ms = value > 1e12 ? value : value * 1e3;
533
+ return new Date(ms).toISOString();
534
+ }
535
+ function toCustomer(value) {
536
+ return typeof value === "object" && value !== null ? value : void 0;
537
+ }
538
+ function resolveCustomerId(value) {
539
+ if (typeof value === "string" && value.length > 0) {
540
+ return value;
541
+ }
542
+ return toCustomer(value)?.id;
543
+ }
544
+ function withOptionalFields(base, fields) {
545
+ const result = { ...base };
546
+ for (const [key, value] of Object.entries(fields)) {
547
+ if (value !== void 0) {
548
+ result[key] = value;
549
+ }
550
+ }
551
+ return result;
552
+ }
553
+ function getCheckoutMetadata(event) {
554
+ return asRecord(event.object?.metadata) ?? asRecord(event.object?.order?.metadata);
555
+ }
556
+ function getSubscriptionMetadata(event) {
557
+ return asRecord(event.object?.metadata);
558
+ }
559
+ function getRefundVisitorId(event) {
560
+ return readMetadataValue(asRecord(event.object?.metadata), "datafast_visitor_id") ?? readMetadataValue(asRecord(event.object?.transaction?.metadata), "datafast_visitor_id");
561
+ }
562
+ function getRefundAmount(event) {
563
+ return event.object?.refund_amount ?? event.object?.refundAmount;
564
+ }
565
+ function getRefundCurrency(event) {
566
+ return event.object?.refund_currency ?? event.object?.refundCurrency;
567
+ }
568
+ function getRefundTimestamp(event) {
569
+ return toIsoTimestamp(
570
+ event.object?.created_at ?? event.object?.createdAt ?? event.created_at ?? event.createdAt ?? event.object?.transaction?.created_at ?? event.object?.transaction?.createdAt
571
+ );
572
+ }
573
+ function isRefundRenewal(event) {
574
+ const transaction = event.object?.transaction;
575
+ return Boolean(typeof transaction?.subscription === "string" && transaction.subscription.length > 0) || transaction?.type === "invoice";
576
+ }
577
+ function mapCheckoutCompletedToPayment(event) {
578
+ const order = event.object?.order;
579
+ if (!order) {
580
+ throw new CreemDataFastError("checkout.completed payload is missing order.");
581
+ }
582
+ const customerValue = event.object?.customer;
583
+ const customer = toCustomer(customerValue);
584
+ const metadata = getCheckoutMetadata(event);
585
+ return withOptionalFields(
586
+ {
587
+ amount: minorToMajor(order.amount, order.currency),
588
+ currency: order.currency,
589
+ transaction_id: order.id,
590
+ renewal: false
591
+ },
592
+ {
593
+ customer_id: resolveCustomerId(customerValue),
594
+ datafast_visitor_id: readMetadataValue(metadata, "datafast_visitor_id"),
595
+ email: customer?.email,
596
+ name: customer?.name
597
+ }
598
+ );
599
+ }
600
+ function mapSubscriptionPaidToPayment(event, transaction) {
601
+ const customerValue = event.object?.customer;
602
+ const customer = toCustomer(customerValue);
603
+ const metadata = getSubscriptionMetadata(event);
604
+ const product = event.object?.product;
605
+ const transactionId = transaction?.id ?? event.object?.last_transaction_id ?? event.object?.lastTransactionId;
606
+ if (!transactionId) {
607
+ throw new CreemDataFastError("subscription.paid payload is missing last_transaction_id.");
608
+ }
609
+ if (!transaction) {
610
+ if (!product || typeof product.price !== "number" || typeof product.currency !== "string") {
611
+ throw new CreemDataFastError(
612
+ "subscription.paid payload is missing product pricing for fallback mapping."
613
+ );
614
+ }
615
+ }
616
+ return withOptionalFields(
617
+ {
618
+ amount: transaction ? minorToMajor(transaction.amount, transaction.currency) : minorToMajor(product.price, product.currency),
619
+ currency: transaction?.currency ?? product.currency,
620
+ transaction_id: transactionId,
621
+ renewal: true
622
+ },
623
+ {
624
+ customer_id: resolveCustomerId(customerValue),
625
+ datafast_visitor_id: readMetadataValue(metadata, "datafast_visitor_id"),
626
+ email: customer?.email,
627
+ name: customer?.name,
628
+ timestamp: transaction?.timestamp ?? event.object?.last_transaction_date ?? event.object?.lastTransactionDate
629
+ }
630
+ );
631
+ }
632
+ function mapRefundCreatedToPayment(event) {
633
+ const refundId = event.object?.id;
634
+ const refundAmount = getRefundAmount(event);
635
+ const refundCurrency = getRefundCurrency(event);
636
+ const customerValue = event.object?.customer ?? event.object?.transaction?.customer;
637
+ const customer = toCustomer(customerValue);
638
+ if (typeof refundId !== "string" || refundId.length === 0) {
639
+ throw new CreemDataFastError("refund.created payload is missing refund id.");
640
+ }
641
+ if (typeof refundAmount !== "number" || typeof refundCurrency !== "string") {
642
+ throw new CreemDataFastError("refund.created payload is missing refund amount or currency.");
643
+ }
644
+ return withOptionalFields(
645
+ {
646
+ amount: minorToMajor(refundAmount, refundCurrency),
647
+ currency: refundCurrency,
648
+ refunded: true,
649
+ renewal: isRefundRenewal(event),
650
+ transaction_id: refundId
651
+ },
652
+ {
653
+ customer_id: resolveCustomerId(customerValue),
654
+ datafast_visitor_id: getRefundVisitorId(event),
655
+ email: customer?.email,
656
+ name: customer?.name,
657
+ timestamp: getRefundTimestamp(event)
658
+ }
659
+ );
660
+ }
661
+
662
+ // src/core/transaction.ts
663
+ function toIsoTimestamp2(input) {
664
+ if (typeof input !== "number" || Number.isNaN(input)) {
665
+ return void 0;
666
+ }
667
+ const ms = input > 1e12 ? input : input * 1e3;
668
+ return new Date(ms).toISOString();
669
+ }
670
+ async function hydrateTransaction(creem, transactionId) {
671
+ try {
672
+ const rawTransaction = await creem.getTransactionById(transactionId);
673
+ if (!rawTransaction || typeof rawTransaction.id !== "string" || typeof rawTransaction.amount !== "number" || typeof rawTransaction.currency !== "string") {
674
+ throw new TransactionHydrationError("Creem transaction response is missing required fields.");
675
+ }
676
+ return {
677
+ amount: rawTransaction.amount,
678
+ currency: rawTransaction.currency,
679
+ id: rawTransaction.id,
680
+ timestamp: toIsoTimestamp2(rawTransaction.createdAt ?? rawTransaction.created_at)
681
+ };
682
+ } catch (error) {
683
+ if (error instanceof TransactionHydrationError) {
684
+ throw error;
685
+ }
686
+ throw new TransactionHydrationError(`Failed to hydrate transaction ${transactionId}.`, {
687
+ cause: error
688
+ });
689
+ }
690
+ }
691
+
692
+ // src/core/webhook.ts
693
+ function getEventType(payload) {
694
+ const eventType = payload.eventType ?? payload.event_type;
695
+ return typeof eventType === "string" ? eventType : void 0;
696
+ }
697
+ function getEventId(payload) {
698
+ return typeof payload.id === "string" ? payload.id : void 0;
699
+ }
700
+ function isSupportedWebhookEvent(eventType) {
701
+ return eventType === "checkout.completed" || eventType === "subscription.paid" || eventType === "refund.created";
702
+ }
703
+ function parseWebhookPayload(rawBody) {
704
+ return JSON.parse(rawBody);
705
+ }
706
+ function isInitialSubscriptionCheckout(payload) {
707
+ const orderType = payload.object?.order?.type;
708
+ const subscription = payload.object?.subscription;
709
+ return orderType === "recurring" || typeof subscription === "string" || typeof subscription === "object" && subscription !== null;
710
+ }
711
+ async function handleWebhook(params, dependencies) {
712
+ const signature = extractHeader(params.headers, "creem-signature");
713
+ if (!signature) {
714
+ throw new InvalidCreemSignatureError("Missing creem-signature header.");
715
+ }
716
+ if (!await verifyCreemSignature(params.rawBody, dependencies.creemWebhookSecret, signature)) {
717
+ throw new InvalidCreemSignatureError("Invalid Creem webhook signature.");
718
+ }
719
+ const payload = parseWebhookPayload(params.rawBody);
720
+ const eventType = getEventType(payload);
721
+ const eventId = getEventId(payload);
722
+ if (!eventType) {
723
+ throw new UnsupportedWebhookEventError("Webhook payload is missing eventType.");
724
+ }
725
+ if (!isSupportedWebhookEvent(eventType)) {
726
+ return {
727
+ ok: true,
728
+ ignored: true,
729
+ eventId,
730
+ eventType,
731
+ reason: "unsupported_event"
732
+ };
733
+ }
734
+ if (!eventId) {
735
+ throw new UnsupportedWebhookEventError("Supported webhook payload is missing id.");
736
+ }
737
+ const idempotencyStore = resolveIdempotencyStore(dependencies.idempotencyStore);
738
+ const canProcess = await claimEvent(
739
+ eventId,
740
+ idempotencyStore,
741
+ dependencies.idempotencyInFlightTtlSeconds
742
+ );
743
+ if (!canProcess) {
744
+ return {
745
+ ok: true,
746
+ ignored: true,
747
+ eventId,
748
+ eventType,
749
+ reason: "duplicate_event"
750
+ };
751
+ }
752
+ if (eventType === "checkout.completed" && isInitialSubscriptionCheckout(payload)) {
753
+ await completeEvent(eventId, idempotencyStore, dependencies.idempotencyProcessedTtlSeconds);
754
+ return {
755
+ ok: true,
756
+ ignored: true,
757
+ eventId,
758
+ eventType,
759
+ reason: "delegated_to_subscription_paid"
760
+ };
761
+ }
762
+ let normalizedPayload;
763
+ let datafastResponse;
764
+ try {
765
+ normalizedPayload = eventType === "checkout.completed" ? mapCheckoutCompletedToPayment(payload) : eventType === "refund.created" ? mapRefundCreatedToPayment(payload) : await (async () => {
766
+ const subscriptionPayload = payload;
767
+ const lastTransactionId = subscriptionPayload.object?.last_transaction_id ?? subscriptionPayload.object?.lastTransactionId;
768
+ if (dependencies.hydrateTransactionOnSubscriptionPaid && lastTransactionId) {
769
+ try {
770
+ const transaction = await hydrateTransaction(
771
+ dependencies.creem,
772
+ lastTransactionId
773
+ );
774
+ return mapSubscriptionPaidToPayment(subscriptionPayload, transaction);
775
+ } catch (error) {
776
+ dependencies.logger.warn(
777
+ "Falling back to subscription product pricing after transaction hydration failure.",
778
+ {
779
+ error,
780
+ lastTransactionId
781
+ }
782
+ );
783
+ }
784
+ }
785
+ return mapSubscriptionPaidToPayment(subscriptionPayload);
786
+ })();
787
+ datafastResponse = await dependencies.datafast.sendPayment(normalizedPayload);
788
+ } catch (error) {
789
+ await releaseEvent(eventId, idempotencyStore);
790
+ throw error;
791
+ }
792
+ await completeEvent(eventId, idempotencyStore, dependencies.idempotencyProcessedTtlSeconds);
793
+ return {
794
+ ok: true,
795
+ ignored: false,
796
+ eventId,
797
+ eventType,
798
+ deduplicated: false,
799
+ payload: normalizedPayload,
800
+ datafastResponse
801
+ };
802
+ }
803
+
804
+ // src/index.ts
805
+ function createCreemDataFast(options) {
806
+ const logger = resolveLogger(options.logger);
807
+ const creem = createCreemClient(options);
808
+ const datafast = createDataFastClient(options);
809
+ const captureSessionId = options.captureSessionId ?? true;
810
+ const strictTracking = options.strictTracking ?? false;
811
+ const hydrateTransactionOnSubscriptionPaid = options.hydrateTransactionOnSubscriptionPaid ?? true;
812
+ const idempotencyStore = options.idempotencyStore ?? new MemoryIdempotencyStore();
813
+ const idempotencyInFlightTtlSeconds = options.idempotencyInFlightTtlSeconds ?? 300;
814
+ const idempotencyProcessedTtlSeconds = options.idempotencyProcessedTtlSeconds ?? 86400;
815
+ return {
816
+ createCheckout(params, context) {
817
+ return createCheckout(params, context, {
818
+ creem,
819
+ captureSessionId,
820
+ strictTracking,
821
+ logger
822
+ });
823
+ },
824
+ handleWebhook(params) {
825
+ return handleWebhook(params, {
826
+ creemWebhookSecret: options.creemWebhookSecret,
827
+ datafast,
828
+ creem,
829
+ idempotencyStore,
830
+ idempotencyInFlightTtlSeconds,
831
+ idempotencyProcessedTtlSeconds,
832
+ hydrateTransactionOnSubscriptionPaid,
833
+ logger
834
+ });
835
+ },
836
+ /**
837
+ * Returns `true` for a valid signature and `false` for an invalid one.
838
+ * Throws `InvalidCreemSignatureError` when the `creem-signature` header is missing.
839
+ */
840
+ async verifyWebhookSignature(rawBody, headers) {
841
+ const signature = extractHeader(headers, "creem-signature");
842
+ if (!signature) {
843
+ throw new InvalidCreemSignatureError("Missing creem-signature header.");
844
+ }
845
+ return verifyCreemSignature(rawBody, options.creemWebhookSecret, signature);
846
+ }
847
+ };
848
+ }
849
+ export {
850
+ CreemDataFastError,
851
+ DataFastRequestError,
852
+ InvalidCreemSignatureError,
853
+ MemoryIdempotencyStore,
854
+ MissingTrackingError,
855
+ createCreemDataFast
856
+ };
857
+ //# sourceMappingURL=index.js.map