@waffo/pancake-ts 0.1.3 → 0.1.5

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,77 @@ 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.1.7] - 2026-03-20
8
+
9
+ ### Added
10
+
11
+ - **Custom webhook public key** — `WaffoPancakeConfig` accepts optional `webhookPublicKey` to override built-in Waffo public keys for webhook signature verification. Useful for self-hosted deployments or custom key rotation.
12
+ - **`client.webhooks.verify()`** — New resource namespace on the client instance. Uses the configured `webhookPublicKey` automatically; supports per-call override via `options.publicKey`.
13
+ - **`VerifyWebhookOptions.publicKey`** — The standalone `verifyWebhook()` function also accepts a custom public key per call, taking precedence over built-in keys and the `environment` option.
14
+ - **Public key normalization** — `normalizePublicKey()` handles the same flexible input formats as `normalizePrivateKey`: literal `\n` from environment variables, Windows `\r\n` line endings, raw base64 without PEM headers, single-line base64, and PKCS#1 (`BEGIN RSA PUBLIC KEY`) format. Applied automatically when a custom public key is used.
15
+
16
+ ## [0.1.6] - 2026-03-18
17
+
18
+ ### Changed
19
+
20
+ - **Types (BREAKING)** — `PriceInfo` removes `taxIncluded` field. Prices now only require `amount` and `taxCategory`. The system internally defaults to tax-exclusive pricing; `taxIncluded` may be re-introduced in a future version.
21
+ - **Types (BREAKING)** — `Store` removes `isPublic` field from response type. `UpdateStoreParams` removes `isPublic` field from input type. Store visibility is no longer configurable (defaults to private).
22
+
23
+ ### Migration
24
+
25
+ Remove `taxIncluded` from all `PriceInfo` / `Prices` objects:
26
+
27
+ ```diff
28
+ const { product } = await client.onetimeProducts.create({
29
+ storeId: "store_xxx",
30
+ name: "My Product",
31
+ prices: {
32
+ - USD: { amount: 2900, taxIncluded: false, taxCategory: "digital_goods" },
33
+ + USD: { amount: 2900, taxCategory: "digital_goods" },
34
+ },
35
+ });
36
+ ```
37
+
38
+ Remove `isPublic` from `stores.update()` calls:
39
+
40
+ ```diff
41
+ const { store } = await client.stores.update({
42
+ id: "store_xxx",
43
+ - isPublic: true,
44
+ name: "Updated Name",
45
+ });
46
+ ```
47
+
48
+ ## [0.1.5] - 2026-03-18
49
+
50
+ ### Changed
51
+
52
+ - **Types** — `CheckoutThemeSettings` removes `checkoutColorTextSecondary` field (7→6 fields). The secondary text color is now derived server-side from `checkoutColorText` and `checkoutColorCard` via color mixing. Merchants only need to configure 5 base color/radius fields; the remaining 7 PSP variables are computed automatically.
53
+
54
+ ### Migration
55
+
56
+ If your code references `checkoutColorTextSecondary`, remove it. The field is no longer accepted by the API and is silently ignored in stored data.
57
+
58
+ ```diff
59
+ const theme: CheckoutThemeSettings = {
60
+ checkoutLogo: null,
61
+ checkoutColorPrimary: "#6366f1",
62
+ checkoutColorBackground: "#ffffff",
63
+ checkoutColorCard: "#f9fafb",
64
+ checkoutColorText: "#111827",
65
+ - checkoutColorTextSecondary: "#6b7280",
66
+ checkoutBorderRadius: "0.5rem",
67
+ };
68
+ ```
69
+
70
+ ## [0.1.4] - 2026-03-16
71
+
72
+ ### Changed
73
+
74
+ - **Types** — `CheckoutSettings` adds `defaultDarkMode: boolean` field to match API response (store create/update)
75
+ - **Types** — `CreateCheckoutSessionParams` adds optional `darkMode` field for dark mode override
76
+ - **Types** — `CreateCheckoutSessionParams.storeId` changed from optional to required to match API spec
77
+
7
78
  ## [0.1.3] - 2026-03-11
8
79
 
9
80
  ### Added
package/README.md CHANGED
@@ -31,8 +31,8 @@ const { product } = await client.onetimeProducts.create({
31
31
  storeId: store.id,
32
32
  name: "E-Book: TypeScript Handbook",
33
33
  prices: {
34
- USD: { amount: 2900, taxIncluded: false, taxCategory: "digital_goods" },
35
- EUR: { amount: 2700, taxIncluded: true, taxCategory: "digital_goods" },
34
+ USD: { amount: 2900, taxCategory: "digital_goods" },
35
+ EUR: { amount: 2700, taxCategory: "digital_goods" },
36
36
  },
37
37
  });
38
38
 
@@ -59,6 +59,7 @@ const result = await client.graphql.query<{ stores: Array<{ id: string; name: st
59
59
  | `privateKey` | `string` | Yes | RSA private key (see [Private Key Formats](#private-key-formats) below) |
60
60
  | `baseUrl` | `string` | No | API base URL (default: `https://waffo-pancake-auth-service.vercel.app`) |
61
61
  | `fetch` | `typeof fetch` | No | Custom fetch implementation |
62
+ | `webhookPublicKey` | `string` | No | Custom RSA public key for webhook verification (see [Public Key Formats](#public-key-formats) below). Overrides built-in keys when set. |
62
63
 
63
64
  ### Private Key Formats
64
65
 
@@ -82,6 +83,19 @@ new WaffoPancake({ merchantId: "m_1", privateKey: fs.readFileSync("key.pem", "ut
82
83
  new WaffoPancake({ merchantId: "m_1", privateKey: rawBase64String }); // raw base64
83
84
  ```
84
85
 
86
+ ### Public Key Formats
87
+
88
+ The `webhookPublicKey` option (and the `publicKey` field in `VerifyWebhookOptions`) accepts the same flexible formats as private keys:
89
+
90
+ | Format | Example | Notes |
91
+ |--------|---------|-------|
92
+ | Standard SPKI PEM | `-----BEGIN PUBLIC KEY-----\n...` | Recommended |
93
+ | PKCS#1 PEM | `-----BEGIN RSA PUBLIC KEY-----\n...` | Also accepted |
94
+ | Literal `\n` (env vars) | `"-----BEGIN PUBLIC KEY-----\\nMIIB..."` | Common when stored in `.env` or CI secrets |
95
+ | Windows line endings | `\r\n` | Converted to `\n` |
96
+ | Raw base64 (no headers) | `MIIBIjANBgkqhki...` | Wrapped with SPKI headers automatically |
97
+ | Single-line base64 with headers | Header + all base64 on one line + footer | Re-wrapped to 64-char lines |
98
+
85
99
  ## Resources
86
100
 
87
101
  | Namespace | Methods | Description |
@@ -95,6 +109,7 @@ new WaffoPancake({ merchantId: "m_1", privateKey: rawBase64String });
95
109
  | `client.orders` | `cancelSubscription()` | Order management (pending→canceled, active→canceling) |
96
110
  | `client.checkout` | `createSession()` | Create a checkout session with trial toggle, billing detail, and price snapshot |
97
111
  | `client.graphql` | `query<T>()` | Typed GraphQL queries (Query only, no Mutations) |
112
+ | `client.webhooks` | `verify<T>()` | Webhook signature verification (uses configured `webhookPublicKey` or built-in keys) |
98
113
 
99
114
  See [API Reference](docs/api-reference.md) for complete parameter tables and return types.
100
115
 
@@ -260,9 +275,9 @@ const { product } = await client.onetimeProducts.create({
260
275
  name: "E-Book: TypeScript Handbook",
261
276
  description: "Complete TypeScript guide for developers",
262
277
  prices: {
263
- USD: { amount: 2900, taxIncluded: false, taxCategory: TaxCategory.DigitalGoods },
264
- EUR: { amount: 2700, taxIncluded: true, taxCategory: TaxCategory.DigitalGoods },
265
- JPY: { amount: 4500, taxIncluded: true, taxCategory: TaxCategory.DigitalGoods },
278
+ USD: { amount: 2900, taxCategory: TaxCategory.DigitalGoods },
279
+ EUR: { amount: 2700, taxCategory: TaxCategory.DigitalGoods },
280
+ JPY: { amount: 4500, taxCategory: TaxCategory.DigitalGoods },
266
281
  },
267
282
  media: [{ type: "image", url: "https://example.com/cover.jpg", alt: "Book cover" }],
268
283
  metadata: { sku: "ebook-ts-001" },
@@ -272,7 +287,7 @@ const { product } = await client.onetimeProducts.create({
272
287
  await client.onetimeProducts.update({
273
288
  id: product.id,
274
289
  name: "E-Book: TypeScript Handbook v2",
275
- prices: { USD: { amount: 3900, taxIncluded: false, taxCategory: "digital_goods" } },
290
+ prices: { USD: { amount: 3900, taxCategory: "digital_goods" } },
276
291
  });
277
292
 
278
293
  // Publish test version → production
@@ -291,7 +306,7 @@ const { product } = await client.subscriptionProducts.create({
291
306
  storeId: "store_xxx",
292
307
  name: "Pro Plan",
293
308
  billingPeriod: BillingPeriod.Monthly,
294
- prices: { USD: { amount: 999, taxIncluded: false, taxCategory: TaxCategory.SaaS } },
309
+ prices: { USD: { amount: 999, taxCategory: TaxCategory.SaaS } },
295
310
  description: "Unlimited access to all features",
296
311
  });
297
312
 
@@ -348,6 +363,7 @@ const session = await client.checkout.createSession({
348
363
 
349
364
  // Subscription with trial and billing detail
350
365
  const subSession = await client.checkout.createSession({
366
+ storeId: "store_xxx",
351
367
  productId: "prod_yyy",
352
368
  productType: CheckoutSessionProductType.Subscription,
353
369
  currency: "USD",
@@ -390,7 +406,9 @@ See [GraphQL Guide](docs/graphql-guide.md) for introspection, filters, paginatio
390
406
 
391
407
  ## Webhook Verification
392
408
 
393
- The SDK exports a standalone `verifyWebhook()` function with **embedded RSA-SHA256 public keys** for both test and production environments. No need to manage keys yourself.
409
+ Two ways to verify webhooks: the **standalone function** `verifyWebhook()` with built-in public keys, or the **client instance method** `client.webhooks.verify()` which uses the configured `webhookPublicKey`.
410
+
411
+ ### Option A — Standalone Function (built-in keys)
394
412
 
395
413
  ```typescript
396
414
  import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts";
@@ -444,6 +462,32 @@ const event = verifyWebhook(body, sig, { environment: "prod" });
444
462
  const event = verifyWebhook(body, sig, { toleranceMs: 0 }); // disable replay check
445
463
  ```
446
464
 
465
+ ### Option B — Client Instance Method (custom public key)
466
+
467
+ When you provide a `webhookPublicKey` in the client config, `client.webhooks.verify()` uses that key automatically. Useful for self-hosted deployments or custom key rotation.
468
+
469
+ ```typescript
470
+ const client = new WaffoPancake({
471
+ merchantId: process.env.WAFFO_MERCHANT_ID!,
472
+ privateKey: process.env.WAFFO_PRIVATE_KEY!,
473
+ webhookPublicKey: process.env.WAFFO_WEBHOOK_PUBLIC_KEY!, // any format accepted
474
+ });
475
+
476
+ // Uses the configured public key — no need to pass it per call
477
+ const event = client.webhooks.verify(rawBody, signatureHeader);
478
+
479
+ // You can still override per call if needed
480
+ const event = client.webhooks.verify(rawBody, sig, { publicKey: anotherKey });
481
+ ```
482
+
483
+ ### Standalone Function with Custom Key
484
+
485
+ You can also pass a custom key directly to the standalone function without creating a client:
486
+
487
+ ```typescript
488
+ const event = verifyWebhook(body, sig, { publicKey: process.env.MY_PUBLIC_KEY! });
489
+ ```
490
+
447
491
  See [Webhook Guide](docs/webhook-guide.md) for all 10 event types, signature algorithm, and best practices.
448
492
 
449
493
  ## Error Handling
@@ -536,7 +580,8 @@ src/
536
580
  ├── subscription-product-groups.ts
537
581
  ├── orders.ts
538
582
  ├── checkout.ts
539
- └── graphql.ts
583
+ ├── graphql.ts
584
+ └── webhooks.ts
540
585
  docs/
541
586
  ├── api-reference.md # Complete API reference
542
587
  ├── graphql-guide.md # GraphQL usage guide
package/dist/index.cjs CHANGED
@@ -63,6 +63,10 @@ var PKCS8_HEADER = "-----BEGIN PRIVATE KEY-----";
63
63
  var PKCS8_FOOTER = "-----END PRIVATE KEY-----";
64
64
  var PKCS1_HEADER = "-----BEGIN RSA PRIVATE KEY-----";
65
65
  var PKCS1_FOOTER = "-----END RSA PRIVATE KEY-----";
66
+ var SPKI_HEADER = "-----BEGIN PUBLIC KEY-----";
67
+ var SPKI_FOOTER = "-----END PUBLIC KEY-----";
68
+ var PKCS1_PUB_HEADER = "-----BEGIN RSA PUBLIC KEY-----";
69
+ var PKCS1_PUB_FOOTER = "-----END RSA PUBLIC KEY-----";
66
70
  function normalizePrivateKey(raw) {
67
71
  if (!raw || !raw.trim()) {
68
72
  throw new Error(
@@ -108,6 +112,51 @@ ${PKCS8_FOOTER}`;
108
112
  }
109
113
  return pem;
110
114
  }
115
+ function normalizePublicKey(raw) {
116
+ if (!raw || !raw.trim()) {
117
+ throw new Error(
118
+ "Public key is empty. Provide an RSA public key in PEM format."
119
+ );
120
+ }
121
+ let pem = raw.replace(/\\n/g, "\n").replace(/\r\n/g, "\n");
122
+ pem = pem.trim();
123
+ const hasSpkiHeader = pem.includes(SPKI_HEADER);
124
+ const hasPkcs1PubHeader = pem.includes(PKCS1_PUB_HEADER);
125
+ const hasHeader = hasSpkiHeader || hasPkcs1PubHeader;
126
+ if (hasHeader) {
127
+ const base64 = pem.replace(/-----BEGIN (?:RSA )?PUBLIC KEY-----/g, "").replace(/-----END (?:RSA )?PUBLIC KEY-----/g, "").replace(/\s+/g, "");
128
+ if (!base64) {
129
+ throw new Error(
130
+ "Public key contains PEM headers but no key data. Check the key content."
131
+ );
132
+ }
133
+ const header = hasPkcs1PubHeader ? PKCS1_PUB_HEADER : SPKI_HEADER;
134
+ const footer = hasPkcs1PubHeader ? PKCS1_PUB_FOOTER : SPKI_FOOTER;
135
+ const wrapped = base64.match(/.{1,64}/g).join("\n");
136
+ pem = `${header}
137
+ ${wrapped}
138
+ ${footer}`;
139
+ } else {
140
+ const base64 = pem.replace(/\s+/g, "");
141
+ if (!/^[A-Za-z0-9+/]+=*$/.test(base64)) {
142
+ throw new Error(
143
+ "Public key is not valid PEM or base64. Expected an RSA public key in PEM format or raw base64."
144
+ );
145
+ }
146
+ const wrapped = base64.match(/.{1,64}/g).join("\n");
147
+ pem = `${SPKI_HEADER}
148
+ ${wrapped}
149
+ ${SPKI_FOOTER}`;
150
+ }
151
+ try {
152
+ (0, import_node_crypto.createPublicKey)(pem);
153
+ } catch {
154
+ throw new Error(
155
+ "Public key could not be parsed. Ensure it is a valid RSA public key in SPKI or PKCS#1 (PEM) format."
156
+ );
157
+ }
158
+ return pem;
159
+ }
111
160
  function signRequest(method, path, timestamp, body, privateKey) {
112
161
  const bodyHash = (0, import_node_crypto.createHash)("sha256").update(body).digest("base64");
113
162
  const canonicalRequest = `${method}
@@ -259,7 +308,7 @@ var OnetimeProductsResource = class {
259
308
  * const { product } = await client.onetimeProducts.create({
260
309
  * storeId: "store_xxx",
261
310
  * name: "E-Book",
262
- * prices: { USD: { amount: 2900, taxIncluded: false, taxCategory: "digital_goods" } },
311
+ * prices: { USD: { amount: 2900, taxCategory: "digital_goods" } },
263
312
  * });
264
313
  */
265
314
  async create(params) {
@@ -275,7 +324,7 @@ var OnetimeProductsResource = class {
275
324
  * const { product } = await client.onetimeProducts.update({
276
325
  * id: "prod_xxx",
277
326
  * name: "E-Book v2",
278
- * prices: { USD: { amount: 3900, taxIncluded: false, taxCategory: "digital_goods" } },
327
+ * prices: { USD: { amount: 3900, taxCategory: "digital_goods" } },
279
328
  * });
280
329
  */
281
330
  async update(params) {
@@ -416,7 +465,6 @@ var StoresResource = class {
416
465
  * const { store } = await client.stores.update({
417
466
  * id: "store_xxx",
418
467
  * name: "Updated Name",
419
- * supportEmail: "help@example.com",
420
468
  * });
421
469
  */
422
470
  async update(params) {
@@ -515,7 +563,7 @@ var SubscriptionProductsResource = class {
515
563
  * storeId: "store_xxx",
516
564
  * name: "Pro Plan",
517
565
  * billingPeriod: "monthly",
518
- * prices: { USD: { amount: 999, taxIncluded: false, taxCategory: "saas" } },
566
+ * prices: { USD: { amount: 999, taxCategory: "saas" } },
519
567
  * });
520
568
  */
521
569
  async create(params) {
@@ -532,7 +580,7 @@ var SubscriptionProductsResource = class {
532
580
  * id: "prod_xxx",
533
581
  * name: "Pro Plan v2",
534
582
  * billingPeriod: "monthly",
535
- * prices: { USD: { amount: 1499, taxIncluded: false, taxCategory: "saas" } },
583
+ * prices: { USD: { amount: 1499, taxCategory: "saas" } },
536
584
  * });
537
585
  */
538
586
  async update(params) {
@@ -567,32 +615,6 @@ var SubscriptionProductsResource = class {
567
615
  }
568
616
  };
569
617
 
570
- // src/client.ts
571
- var WaffoPancake = class {
572
- http;
573
- auth;
574
- stores;
575
- storeMerchants;
576
- onetimeProducts;
577
- subscriptionProducts;
578
- subscriptionProductGroups;
579
- orders;
580
- checkout;
581
- graphql;
582
- constructor(config) {
583
- this.http = new HttpClient(config);
584
- this.auth = new AuthResource(this.http);
585
- this.stores = new StoresResource(this.http);
586
- this.storeMerchants = new StoreMerchantsResource(this.http);
587
- this.onetimeProducts = new OnetimeProductsResource(this.http);
588
- this.subscriptionProducts = new SubscriptionProductsResource(this.http);
589
- this.subscriptionProductGroups = new SubscriptionProductGroupsResource(this.http);
590
- this.orders = new OrdersResource(this.http);
591
- this.checkout = new CheckoutResource(this.http);
592
- this.graphql = new GraphQLResource(this.http);
593
- }
594
- };
595
-
596
618
  // src/webhooks.ts
597
619
  var import_node_crypto3 = require("crypto");
598
620
  var DEFAULT_TOLERANCE_MS = 5 * 60 * 1e3;
@@ -651,27 +673,97 @@ function verifyWebhook(payload, signatureHeader, options) {
651
673
  }
652
674
  }
653
675
  const signatureInput = `${t}.${payload}`;
654
- const env = options?.environment;
655
- if (env === "test") {
656
- if (!rsaVerify(signatureInput, v1, TEST_PUBLIC_KEY)) {
657
- throw new Error("Invalid webhook signature (test key)");
658
- }
659
- } else if (env === "prod") {
660
- if (!rsaVerify(signatureInput, v1, PROD_PUBLIC_KEY)) {
661
- throw new Error("Invalid webhook signature (prod key)");
676
+ const customKey = options?.publicKey;
677
+ if (customKey) {
678
+ const normalizedKey = normalizePublicKey(customKey);
679
+ if (!rsaVerify(signatureInput, v1, normalizedKey)) {
680
+ throw new Error("Invalid webhook signature (custom key)");
662
681
  }
663
682
  } else {
664
- const prodValid = rsaVerify(signatureInput, v1, PROD_PUBLIC_KEY);
665
- if (!prodValid) {
666
- const testValid = rsaVerify(signatureInput, v1, TEST_PUBLIC_KEY);
667
- if (!testValid) {
668
- throw new Error("Invalid webhook signature (tried both prod and test keys)");
683
+ const env = options?.environment;
684
+ if (env === "test") {
685
+ if (!rsaVerify(signatureInput, v1, TEST_PUBLIC_KEY)) {
686
+ throw new Error("Invalid webhook signature (test key)");
687
+ }
688
+ } else if (env === "prod") {
689
+ if (!rsaVerify(signatureInput, v1, PROD_PUBLIC_KEY)) {
690
+ throw new Error("Invalid webhook signature (prod key)");
691
+ }
692
+ } else {
693
+ const prodValid = rsaVerify(signatureInput, v1, PROD_PUBLIC_KEY);
694
+ if (!prodValid) {
695
+ const testValid = rsaVerify(signatureInput, v1, TEST_PUBLIC_KEY);
696
+ if (!testValid) {
697
+ throw new Error("Invalid webhook signature (tried both prod and test keys)");
698
+ }
669
699
  }
670
700
  }
671
701
  }
672
702
  return JSON.parse(payload);
673
703
  }
674
704
 
705
+ // src/resources/webhooks.ts
706
+ var WebhooksResource = class {
707
+ /** @param publicKey - Optional custom RSA public key (PEM or raw base64) */
708
+ constructor(publicKey) {
709
+ this.publicKey = publicKey;
710
+ }
711
+ /**
712
+ * Verify and parse an incoming webhook event.
713
+ *
714
+ * When the client was created with a `webhookPublicKey`, that key is used
715
+ * automatically. You can still override per-call via `options.publicKey`.
716
+ *
717
+ * @param payload - Raw request body string (must be unparsed)
718
+ * @param signatureHeader - Value of the `X-Waffo-Signature` header
719
+ * @param options - Verification options (optional)
720
+ * @returns Parsed webhook event
721
+ * @throws Error if signature is invalid, header is malformed, or timestamp is stale
722
+ *
723
+ * @example
724
+ * const event = client.webhooks.verify(rawBody, signatureHeader);
725
+ *
726
+ * @example
727
+ * // Override tolerance per call
728
+ * const event = client.webhooks.verify(rawBody, sig, { toleranceMs: 0 });
729
+ */
730
+ verify(payload, signatureHeader, options) {
731
+ const mergedOptions = {
732
+ ...options,
733
+ publicKey: options?.publicKey ?? this.publicKey
734
+ };
735
+ return verifyWebhook(payload, signatureHeader, mergedOptions);
736
+ }
737
+ };
738
+
739
+ // src/client.ts
740
+ var WaffoPancake = class {
741
+ http;
742
+ auth;
743
+ stores;
744
+ storeMerchants;
745
+ onetimeProducts;
746
+ subscriptionProducts;
747
+ subscriptionProductGroups;
748
+ orders;
749
+ checkout;
750
+ graphql;
751
+ webhooks;
752
+ constructor(config) {
753
+ this.http = new HttpClient(config);
754
+ this.auth = new AuthResource(this.http);
755
+ this.stores = new StoresResource(this.http);
756
+ this.storeMerchants = new StoreMerchantsResource(this.http);
757
+ this.onetimeProducts = new OnetimeProductsResource(this.http);
758
+ this.subscriptionProducts = new SubscriptionProductsResource(this.http);
759
+ this.subscriptionProductGroups = new SubscriptionProductGroupsResource(this.http);
760
+ this.orders = new OrdersResource(this.http);
761
+ this.checkout = new CheckoutResource(this.http);
762
+ this.graphql = new GraphQLResource(this.http);
763
+ this.webhooks = new WebhooksResource(config.webhookPublicKey);
764
+ }
765
+ };
766
+
675
767
  // src/types.ts
676
768
  var Environment = /* @__PURE__ */ ((Environment2) => {
677
769
  Environment2["Test"] = "test";