@waffo/pancake-ts 0.2.0 → 0.2.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ All notable changes to `@waffo/pancake-ts` will be documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.2.2] - 2026-04-03
8
+
9
+ ### Fixed
10
+
11
+ - **Checkout idempotency** — Checkout methods (`anonymous.create()`, `authenticated.create()`, `createSession()`) now use time-windowed idempotency keys (60-second window) instead of fully deterministic keys. Previously, identical checkout params always produced the same `X-Idempotency-Key`, causing the gateway to return cached (and potentially expired) sessions. Now, same params within the same minute are still deduped (protects against network retries), but a new key is generated after the window elapses.
12
+ - **Timestamp consistency** — `Date.now()` is now called once per request and shared between signature timestamp and idempotency key calculation, eliminating a theoretical edge case where the two could land in different seconds.
13
+
14
+ ### Internal
15
+
16
+ - **`PostOptions` interface** — Extracted inline `{ idempotencyWindow?: number }` into a named type in `types.ts` (not publicly exported).
17
+
18
+ ## [0.2.1] - 2026-04-02
19
+
20
+ ### Added
21
+
22
+ - **Client-side input validation** — All resource methods now validate inputs before sending network requests. Checks include: required field presence, Short ID format (`STO_xxx`, `PROD_xxx`, etc.), ISO 4217 currency codes, ISO 3166-1 country codes, display-format amount strings, enum value ranges, and positive integers. Validation errors throw `WaffoPancakeError` with `status: 400` and `layer: "sdk"`, so developers catch them uniformly with API errors.
23
+ - **`ErrorLayer.Sdk`** — New `"sdk"` value in the `ErrorLayer` enum for client-side validation errors.
24
+
25
+ ### Fixed
26
+
27
+ - **Types** — `RefundTicketStatus` enum now includes all 9 statuses: added `UnderReview`, `Returned`, `Cancelled` (previously missing 3 values)
28
+ - **Types** — `RefundTicket.currentVersionId` corrected to `string | null` (was `string`)
29
+ - **Types** — `RefundTicket.versionNumber` corrected to `number | null` (was `number`)
30
+ - **Types** — `RefundTicket.versionData` corrected to `Record<string, unknown> | null` (was non-nullable)
31
+ - **Types** — `RefundTicket` now includes `createdAt` and `updatedAt` fields (previously missing)
32
+ - **Types** — `PriceInfo`, `Prices`, `WebhookEvent` JSDoc examples corrected from numeric amounts to display-format strings
33
+
7
34
  ## [0.2.0] - 2026-04-02
8
35
 
9
36
  ### Added
package/dist/index.cjs CHANGED
@@ -222,18 +222,26 @@ var HttpClient = class {
222
222
  *
223
223
  * Behavior:
224
224
  * - Generates a deterministic `X-Idempotency-Key` from `merchantId + path + body` (same request produces same key)
225
+ * - When `idempotencyWindow` is set, a floored timestamp is mixed into the key so identical params produce
226
+ * a new key after the window elapses (useful for checkout where repeated creation is intentional)
225
227
  * - Auto-builds RSA-SHA256 signature (`X-Merchant-Id` / `X-Timestamp` / `X-Signature`)
226
228
  * - Unwraps the response envelope: returns `data` on success, throws `WaffoPancakeError` on failure
227
229
  *
228
230
  * @param path - API path (e.g. `/v1/actions/store/create-store`)
229
231
  * @param body - Request body object
232
+ * @param options - Optional settings
233
+ * @param options.idempotencyWindow - Time window in seconds for idempotency key rotation (e.g. 60 = per-minute dedup)
230
234
  * @returns Parsed `data` field from the response
231
235
  * @throws {WaffoPancakeError} When the API returns errors
232
236
  */
233
- async post(path, body) {
237
+ async post(path, body, options) {
234
238
  const bodyStr = JSON.stringify(body);
235
- const timestamp = Math.floor(Date.now() / 1e3).toString();
239
+ const now = Date.now();
240
+ const timestampSec = Math.floor(now / 1e3);
241
+ const timestamp = timestampSec.toString();
236
242
  const signature = signRequest("POST", path, timestamp, bodyStr, this.privateKey);
243
+ const idempotencyBase = `${this.merchantId}:${path}:${bodyStr}`;
244
+ const idempotencyInput = options?.idempotencyWindow ? `${idempotencyBase}:${Math.floor(timestampSec / options.idempotencyWindow)}` : idempotencyBase;
237
245
  const response = await this._fetch(`${this.baseUrl}${path}`, {
238
246
  method: "POST",
239
247
  headers: {
@@ -241,7 +249,7 @@ var HttpClient = class {
241
249
  "X-Merchant-Id": this.merchantId,
242
250
  "X-Timestamp": timestamp,
243
251
  "X-Signature": signature,
244
- "X-Idempotency-Key": (0, import_node_crypto2.createHash)("sha256").update(`${this.merchantId}:${path}:${bodyStr}`).digest("hex")
252
+ "X-Idempotency-Key": (0, import_node_crypto2.createHash)("sha256").update(idempotencyInput).digest("hex")
245
253
  },
246
254
  body: bodyStr
247
255
  });
@@ -253,6 +261,105 @@ var HttpClient = class {
253
261
  }
254
262
  };
255
263
 
264
+ // src/validation.ts
265
+ var SHORT_ID_REGEX = /^[A-Z]{2,4}_[A-Za-z0-9]+$/;
266
+ var CURRENCY_CODE_REGEX = /^[A-Z]{3}$/;
267
+ var COUNTRY_CODE_REGEX = /^[A-Z]{2}$/;
268
+ var AMOUNT_STRING_REGEX = /^\d+(\.\d+)?$/;
269
+ var SHORT_ID_LABELS = {
270
+ STO: "Store",
271
+ PROD: "Product",
272
+ ORD: "Order",
273
+ PAY: "Payment",
274
+ REF: "Refund",
275
+ TKT: "Ticket",
276
+ MER: "Merchant"
277
+ };
278
+ function fail(message) {
279
+ throw new WaffoPancakeError(400, [{ message, layer: "sdk" }]);
280
+ }
281
+ function validateRequired(field, value) {
282
+ if (value === void 0 || value === null) {
283
+ fail(`Missing required field: ${field}`);
284
+ }
285
+ if (typeof value === "string" && value.trim() === "") {
286
+ fail(`${field} cannot be empty`);
287
+ }
288
+ }
289
+ function validateShortId(field, value, prefix) {
290
+ validateRequired(field, value);
291
+ const label = SHORT_ID_LABELS[prefix] ?? prefix;
292
+ if (!SHORT_ID_REGEX.test(value)) {
293
+ fail(`Invalid ${field}: expected ${label} Short ID format (${prefix}_xxx), got "${value}"`);
294
+ }
295
+ if (!value.startsWith(`${prefix}_`)) {
296
+ fail(`Invalid ${field}: expected ${prefix}_ prefix (${label}), got "${value.split("_")[0]}_"`);
297
+ }
298
+ }
299
+ function validateCurrencyCode(field, value) {
300
+ validateRequired(field, value);
301
+ if (!CURRENCY_CODE_REGEX.test(value)) {
302
+ fail(`Invalid ${field}: expected 3-letter ISO 4217 currency code (e.g., "USD"), got "${value}"`);
303
+ }
304
+ }
305
+ function validateAmountString(field, value) {
306
+ validateRequired(field, value);
307
+ if (!AMOUNT_STRING_REGEX.test(value)) {
308
+ fail(`Invalid ${field}: expected numeric string in display format (e.g., "9.99", "1000"), got "${value}"`);
309
+ }
310
+ }
311
+ function validateEnum(field, value, allowed) {
312
+ validateRequired(field, value);
313
+ if (!allowed.includes(value)) {
314
+ fail(`Invalid ${field}: expected one of [${allowed.join(", ")}], got "${value}"`);
315
+ }
316
+ }
317
+ function validatePositiveInteger(field, value) {
318
+ if (!Number.isInteger(value) || value <= 0) {
319
+ fail(`Invalid ${field}: expected positive integer, got ${value}`);
320
+ }
321
+ }
322
+ function validateCountryCode(field, value) {
323
+ validateRequired(field, value);
324
+ if (!COUNTRY_CODE_REGEX.test(value)) {
325
+ fail(`Invalid ${field}: expected 2-letter ISO 3166-1 country code (e.g., "US"), got "${value}"`);
326
+ }
327
+ }
328
+ function validatePrices(field, prices) {
329
+ validateRequired(field, prices);
330
+ const entries = Object.entries(prices);
331
+ if (entries.length === 0) {
332
+ fail(`${field} must contain at least one currency`);
333
+ }
334
+ for (const [currency, info] of entries) {
335
+ validateCurrencyCode(`${field}.${currency} (key)`, currency);
336
+ validateAmountString(`${field}.${currency}.amount`, info.amount);
337
+ validateRequired(`${field}.${currency}.taxCategory`, info.taxCategory);
338
+ }
339
+ }
340
+ function validateBillingDetail(detail) {
341
+ validateCountryCode("billingDetail.country", detail.country);
342
+ if (typeof detail.isBusiness !== "boolean") {
343
+ fail(`Invalid billingDetail.isBusiness: expected boolean, got ${typeof detail.isBusiness}`);
344
+ }
345
+ }
346
+ function validateCheckoutCommon(params) {
347
+ validateShortId("storeId", params.storeId, "STO");
348
+ validateShortId("productId", params.productId, "PROD");
349
+ validateEnum("productType", params.productType, ["onetime", "subscription"]);
350
+ validateCurrencyCode("currency", params.currency);
351
+ if (params.priceSnapshot) {
352
+ validateAmountString("priceSnapshot.amount", params.priceSnapshot.amount);
353
+ validateRequired("priceSnapshot.taxCategory", params.priceSnapshot.taxCategory);
354
+ }
355
+ if (params.billingDetail) {
356
+ validateBillingDetail(params.billingDetail);
357
+ }
358
+ if (params.expiresInSeconds !== void 0) {
359
+ validatePositiveInteger("expiresInSeconds", params.expiresInSeconds);
360
+ }
361
+ }
362
+
256
363
  // src/resources/auth.ts
257
364
  var AuthResource = class {
258
365
  constructor(http) {
@@ -271,6 +378,8 @@ var AuthResource = class {
271
378
  * });
272
379
  */
273
380
  async issueSessionToken(params) {
381
+ validateShortId("storeId", params.storeId, "STO");
382
+ validateRequired("buyerIdentity", params.buyerIdentity);
274
383
  return this.http.post("/v1/actions/auth/issue-session-token", params);
275
384
  }
276
385
  };
@@ -294,6 +403,7 @@ var BuyerSession = class {
294
403
  * // status: "canceled" (was pending) or "canceling" (was active)
295
404
  */
296
405
  async cancelSubscription(params) {
406
+ validateShortId("orderId", params.orderId, "ORD");
297
407
  return this.http.post(
298
408
  "/v1/actions/subscription-order/cancel-order",
299
409
  params
@@ -309,6 +419,7 @@ var BuyerSession = class {
309
419
  * const { orderId, status } = await buyer.cancelOnetimeOrder({ orderId: "ORD_xxx" });
310
420
  */
311
421
  async cancelOnetimeOrder(params) {
422
+ validateShortId("orderId", params.orderId, "ORD");
312
423
  return this.http.post(
313
424
  "/v1/actions/onetime-order/cancel-order",
314
425
  params
@@ -325,6 +436,7 @@ var BuyerSession = class {
325
436
  * // status: "active"
326
437
  */
327
438
  async reactivateSubscription(params) {
439
+ validateShortId("orderId", params.orderId, "ORD");
328
440
  return this.http.post(
329
441
  "/v1/actions/subscription-order/reactivate-order",
330
442
  params
@@ -344,6 +456,10 @@ var BuyerSession = class {
344
456
  * });
345
457
  */
346
458
  async createRefundTicket(params) {
459
+ validateShortId("paymentId", params.paymentId, "PAY");
460
+ validateRequired("reason", params.reason);
461
+ validateAmountString("requestedAmount.amount", params.requestedAmount.amount);
462
+ validateCurrencyCode("requestedAmount.currency", params.requestedAmount.currency);
347
463
  return this.http.post(
348
464
  "/v1/actions/refund-ticket/create-ticket",
349
465
  params
@@ -364,6 +480,11 @@ var BuyerSession = class {
364
480
  * });
365
481
  */
366
482
  async resubmitRefundTicket(params) {
483
+ validateShortId("ticketId", params.ticketId, "TKT");
484
+ validateShortId("paymentId", params.paymentId, "PAY");
485
+ validateRequired("reason", params.reason);
486
+ validateAmountString("requestedAmount.amount", params.requestedAmount.amount);
487
+ validateCurrencyCode("requestedAmount.currency", params.requestedAmount.currency);
367
488
  return this.http.post(
368
489
  "/v1/actions/refund-ticket/resubmit-ticket",
369
490
  params
@@ -386,6 +507,7 @@ var BuyerGraphQL = class {
386
507
  * });
387
508
  */
388
509
  async query(params) {
510
+ validateRequired("query", params.query);
389
511
  return this.http.post("/v1/graphql", params);
390
512
  }
391
513
  };
@@ -411,9 +533,11 @@ var CheckoutAnonymousResource = class {
411
533
  * // Redirect to result.checkoutUrl
412
534
  */
413
535
  async create(params) {
536
+ validateCheckoutCommon(params);
414
537
  return this.http.post(
415
538
  "/v1/actions/checkout/create-session",
416
- params
539
+ params,
540
+ { idempotencyWindow: 60 }
417
541
  );
418
542
  }
419
543
  };
@@ -446,16 +570,18 @@ var CheckoutAuthenticatedResource = class {
446
570
  * // Redirect to result.checkoutUrl (includes #token=...)
447
571
  */
448
572
  async create(params) {
573
+ validateCheckoutCommon(params);
574
+ validateRequired("buyerIdentity", params.buyerIdentity);
449
575
  const { buyerIdentity, buyerEmail, ...sessionFields } = params;
450
576
  const [tokenResult, sessionResult] = await Promise.all([
451
577
  this.http.post("/v1/actions/auth/issue-session-token", {
452
578
  storeId: params.storeId,
453
579
  buyerIdentity
454
- }),
580
+ }, { idempotencyWindow: 60 }),
455
581
  this.http.post("/v1/actions/checkout/create-session", {
456
582
  ...sessionFields,
457
583
  buyerEmail: buyerEmail ?? buyerIdentity
458
- })
584
+ }, { idempotencyWindow: 60 })
459
585
  ]);
460
586
  return {
461
587
  sessionId: sessionResult.sessionId,
@@ -498,7 +624,7 @@ var CheckoutResource = class {
498
624
  * // Redirect to session.checkoutUrl
499
625
  */
500
626
  async createSession(params) {
501
- return this.http.post("/v1/actions/checkout/create-session", params);
627
+ return this.http.post("/v1/actions/checkout/create-session", params, { idempotencyWindow: 60 });
502
628
  }
503
629
  };
504
630
 
@@ -526,6 +652,7 @@ var GraphQLResource = class {
526
652
  * });
527
653
  */
528
654
  async query(params) {
655
+ validateRequired("query", params.query);
529
656
  return this.http.post("/v1/graphql", params);
530
657
  }
531
658
  };
@@ -549,6 +676,9 @@ var OnetimeProductsResource = class {
549
676
  * });
550
677
  */
551
678
  async create(params) {
679
+ validateShortId("storeId", params.storeId, "STO");
680
+ validateRequired("name", params.name);
681
+ validatePrices("prices", params.prices);
552
682
  return this.http.post("/v1/actions/onetime-product/create-product", params);
553
683
  }
554
684
  /**
@@ -565,6 +695,9 @@ var OnetimeProductsResource = class {
565
695
  * });
566
696
  */
567
697
  async update(params) {
698
+ validateShortId("id", params.id, "PROD");
699
+ validateRequired("name", params.name);
700
+ validatePrices("prices", params.prices);
568
701
  return this.http.post("/v1/actions/onetime-product/update-product", params);
569
702
  }
570
703
  /**
@@ -577,6 +710,7 @@ var OnetimeProductsResource = class {
577
710
  * const { product } = await client.onetimeProducts.publish({ id: "PROD_xxx" });
578
711
  */
579
712
  async publish(params) {
713
+ validateShortId("id", params.id, "PROD");
580
714
  return this.http.post("/v1/actions/onetime-product/publish-product", params);
581
715
  }
582
716
  /**
@@ -592,6 +726,8 @@ var OnetimeProductsResource = class {
592
726
  * });
593
727
  */
594
728
  async updateStatus(params) {
729
+ validateShortId("id", params.id, "PROD");
730
+ validateEnum("status", params.status, ["active", "inactive"]);
595
731
  return this.http.post("/v1/actions/onetime-product/update-status", params);
596
732
  }
597
733
  };
@@ -617,6 +753,7 @@ var OrdersResource = class {
617
753
  * // status: "canceled" or "canceling"
618
754
  */
619
755
  async cancelSubscription(params) {
756
+ validateShortId("orderId", params.orderId, "ORD");
620
757
  return this.http.post("/v1/actions/subscription-order/cancel-order", params);
621
758
  }
622
759
  };
@@ -640,6 +777,9 @@ var StoreMerchantsResource = class {
640
777
  * });
641
778
  */
642
779
  async add(params) {
780
+ validateShortId("storeId", params.storeId, "STO");
781
+ validateRequired("email", params.email);
782
+ validateEnum("role", params.role, ["admin", "member"]);
643
783
  return this.http.post("/v1/actions/store-merchant/add-merchant", params);
644
784
  }
645
785
  /**
@@ -655,6 +795,8 @@ var StoreMerchantsResource = class {
655
795
  * });
656
796
  */
657
797
  async remove(params) {
798
+ validateShortId("storeId", params.storeId, "STO");
799
+ validateShortId("merchantId", params.merchantId, "MER");
658
800
  return this.http.post("/v1/actions/store-merchant/remove-merchant", params);
659
801
  }
660
802
  /**
@@ -671,6 +813,9 @@ var StoreMerchantsResource = class {
671
813
  * });
672
814
  */
673
815
  async updateRole(params) {
816
+ validateShortId("storeId", params.storeId, "STO");
817
+ validateShortId("merchantId", params.merchantId, "MER");
818
+ validateEnum("role", params.role, ["admin", "member"]);
674
819
  return this.http.post("/v1/actions/store-merchant/update-role", params);
675
820
  }
676
821
  };
@@ -690,6 +835,7 @@ var StoresResource = class {
690
835
  * const { store } = await client.stores.create({ name: "My Store" });
691
836
  */
692
837
  async create(params) {
838
+ validateRequired("name", params.name);
693
839
  return this.http.post("/v1/actions/store/create-store", params);
694
840
  }
695
841
  /**
@@ -705,6 +851,7 @@ var StoresResource = class {
705
851
  * });
706
852
  */
707
853
  async update(params) {
854
+ validateShortId("id", params.id, "STO");
708
855
  return this.http.post("/v1/actions/store/update-store", params);
709
856
  }
710
857
  /**
@@ -717,6 +864,7 @@ var StoresResource = class {
717
864
  * const { store } = await client.stores.delete({ id: "STO_xxx" });
718
865
  */
719
866
  async delete(params) {
867
+ validateShortId("id", params.id, "STO");
720
868
  return this.http.post("/v1/actions/store/delete-store", params);
721
869
  }
722
870
  };
@@ -741,6 +889,8 @@ var SubscriptionProductGroupsResource = class {
741
889
  * });
742
890
  */
743
891
  async create(params) {
892
+ validateShortId("storeId", params.storeId, "STO");
893
+ validateRequired("name", params.name);
744
894
  return this.http.post("/v1/actions/subscription-product-group/create-group", params);
745
895
  }
746
896
  /**
@@ -756,6 +906,7 @@ var SubscriptionProductGroupsResource = class {
756
906
  * });
757
907
  */
758
908
  async update(params) {
909
+ validateRequired("id", params.id);
759
910
  return this.http.post("/v1/actions/subscription-product-group/update-group", params);
760
911
  }
761
912
  /**
@@ -768,6 +919,7 @@ var SubscriptionProductGroupsResource = class {
768
919
  * const { group } = await client.subscriptionProductGroups.delete({ id: "GRP_xxx" });
769
920
  */
770
921
  async delete(params) {
922
+ validateRequired("id", params.id);
771
923
  return this.http.post("/v1/actions/subscription-product-group/delete-group", params);
772
924
  }
773
925
  /**
@@ -780,6 +932,7 @@ var SubscriptionProductGroupsResource = class {
780
932
  * const { group } = await client.subscriptionProductGroups.publish({ id: "GRP_xxx" });
781
933
  */
782
934
  async publish(params) {
935
+ validateRequired("id", params.id);
783
936
  return this.http.post("/v1/actions/subscription-product-group/publish-group", params);
784
937
  }
785
938
  };
@@ -804,6 +957,10 @@ var SubscriptionProductsResource = class {
804
957
  * });
805
958
  */
806
959
  async create(params) {
960
+ validateShortId("storeId", params.storeId, "STO");
961
+ validateRequired("name", params.name);
962
+ validateEnum("billingPeriod", params.billingPeriod, ["weekly", "monthly", "quarterly", "yearly"]);
963
+ validatePrices("prices", params.prices);
807
964
  return this.http.post("/v1/actions/subscription-product/create-product", params);
808
965
  }
809
966
  /**
@@ -821,6 +978,10 @@ var SubscriptionProductsResource = class {
821
978
  * });
822
979
  */
823
980
  async update(params) {
981
+ validateShortId("id", params.id, "PROD");
982
+ validateRequired("name", params.name);
983
+ validateEnum("billingPeriod", params.billingPeriod, ["weekly", "monthly", "quarterly", "yearly"]);
984
+ validatePrices("prices", params.prices);
824
985
  return this.http.post("/v1/actions/subscription-product/update-product", params);
825
986
  }
826
987
  /**
@@ -833,6 +994,7 @@ var SubscriptionProductsResource = class {
833
994
  * const { product } = await client.subscriptionProducts.publish({ id: "PROD_xxx" });
834
995
  */
835
996
  async publish(params) {
997
+ validateShortId("id", params.id, "PROD");
836
998
  return this.http.post("/v1/actions/subscription-product/publish-product", params);
837
999
  }
838
1000
  /**
@@ -848,6 +1010,8 @@ var SubscriptionProductsResource = class {
848
1010
  * });
849
1011
  */
850
1012
  async updateStatus(params) {
1013
+ validateShortId("id", params.id, "PROD");
1014
+ validateEnum("status", params.status, ["active", "inactive"]);
851
1015
  return this.http.post("/v1/actions/subscription-product/update-status", params);
852
1016
  }
853
1017
  };
@@ -1116,11 +1280,14 @@ var PaymentStatus = /* @__PURE__ */ ((PaymentStatus2) => {
1116
1280
  })(PaymentStatus || {});
1117
1281
  var RefundTicketStatus = /* @__PURE__ */ ((RefundTicketStatus2) => {
1118
1282
  RefundTicketStatus2["Pending"] = "pending";
1283
+ RefundTicketStatus2["UnderReview"] = "under_review";
1119
1284
  RefundTicketStatus2["Approved"] = "approved";
1120
1285
  RefundTicketStatus2["Rejected"] = "rejected";
1286
+ RefundTicketStatus2["Returned"] = "returned";
1121
1287
  RefundTicketStatus2["Processing"] = "processing";
1122
1288
  RefundTicketStatus2["Succeeded"] = "succeeded";
1123
1289
  RefundTicketStatus2["Failed"] = "failed";
1290
+ RefundTicketStatus2["Cancelled"] = "cancelled";
1124
1291
  return RefundTicketStatus2;
1125
1292
  })(RefundTicketStatus || {});
1126
1293
  var RefundStatus = /* @__PURE__ */ ((RefundStatus2) => {
@@ -1148,6 +1315,7 @@ var ErrorLayer = /* @__PURE__ */ ((ErrorLayer2) => {
1148
1315
  ErrorLayer2["GraphQL"] = "graphql";
1149
1316
  ErrorLayer2["Resource"] = "resource";
1150
1317
  ErrorLayer2["Email"] = "email";
1318
+ ErrorLayer2["Sdk"] = "sdk";
1151
1319
  return ErrorLayer2;
1152
1320
  })(ErrorLayer || {});
1153
1321
  var WebhookEventType = /* @__PURE__ */ ((WebhookEventType2) => {