@waffo/pancake-ts 0.1.2 → 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,84 @@ 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
+
78
+ ## [0.1.3] - 2026-03-11
79
+
80
+ ### Added
81
+
82
+ - **Private key normalization** — `privateKey` is automatically normalized at construction time. Accepts literal `\n` from environment variables, Windows `\r\n` line endings, raw base64 without PEM headers, single-line base64, and PKCS#1 (`BEGIN RSA PRIVATE KEY`) format. Invalid or empty keys throw a descriptive error immediately instead of failing on the first API call.
83
+ - **Checkout integration guide** in README — Step-by-step instructions (Issue Token → Create Session → Open Checkout Page) with recommendation to open the checkout URL in a new browser tab.
84
+
7
85
  ## [0.1.2] - 2026-03-11
8
86
 
9
87
  ### Fixed
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
 
@@ -56,9 +56,45 @@ const result = await client.graphql.query<{ stores: Array<{ id: string; name: st
56
56
  | Parameter | Type | Required | Description |
57
57
  |-----------|------|----------|-------------|
58
58
  | `merchantId` | `string` | Yes | Merchant ID, sent as `X-Merchant-Id` header |
59
- | `privateKey` | `string` | Yes | RSA private key in PEM format for request signing |
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. |
63
+
64
+ ### Private Key Formats
65
+
66
+ The SDK automatically normalizes `privateKey` at construction time, so all of the following formats are accepted:
67
+
68
+ | Format | Example | Notes |
69
+ |--------|---------|-------|
70
+ | Standard PKCS#8 PEM | `-----BEGIN PRIVATE KEY-----\n...` | Recommended |
71
+ | PKCS#1 PEM | `-----BEGIN RSA PRIVATE KEY-----\n...` | Also accepted |
72
+ | Literal `\n` (env vars) | `"-----BEGIN PRIVATE KEY-----\\nMIIE..."` | Common when stored in `.env` or CI secrets |
73
+ | Windows line endings | `\r\n` | Converted to `\n` |
74
+ | Raw base64 (no headers) | `MIIEvQIBADANBgkqhki...` | Wrapped with PKCS#8 headers automatically |
75
+ | Single-line base64 with headers | Header + all base64 on one line + footer | Re-wrapped to 64-char lines |
76
+
77
+ If the key is invalid or empty, the constructor throws a descriptive error immediately rather than failing silently on the first API call.
78
+
79
+ ```typescript
80
+ // All of these work:
81
+ new WaffoPancake({ merchantId: "m_1", privateKey: process.env.PRIVATE_KEY! }); // .env with literal \n
82
+ new WaffoPancake({ merchantId: "m_1", privateKey: fs.readFileSync("key.pem", "utf8") }); // file read
83
+ new WaffoPancake({ merchantId: "m_1", privateKey: rawBase64String }); // raw base64
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 |
62
98
 
63
99
  ## Resources
64
100
 
@@ -73,9 +109,118 @@ const result = await client.graphql.query<{ stores: Array<{ id: string; name: st
73
109
  | `client.orders` | `cancelSubscription()` | Order management (pending→canceled, active→canceling) |
74
110
  | `client.checkout` | `createSession()` | Create a checkout session with trial toggle, billing detail, and price snapshot |
75
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) |
76
113
 
77
114
  See [API Reference](docs/api-reference.md) for complete parameter tables and return types.
78
115
 
116
+ ## Checkout Integration
117
+
118
+ Guide buyers from your site to the Waffo checkout page in three steps:
119
+
120
+ ```
121
+ 1. Issue Session Token → Obtain a buyer identity credential (JWT)
122
+ 2. Create Checkout Session → Create a session and get the checkout URL
123
+ 3. Open Checkout Page → Open the checkout in a new browser tab
124
+ ```
125
+
126
+ ### Step 1 — Issue a Session Token
127
+
128
+ Your backend requests a Session Token on behalf of the buyer. The token carries the buyer's identity and is used by the checkout page to load order details and place orders.
129
+
130
+ ```typescript
131
+ const { token } = await client.auth.issueSessionToken({
132
+ storeId: "store_xxx",
133
+ buyerIdentity: "customer@example.com",
134
+ });
135
+ ```
136
+
137
+ ### Step 2 — Create a Checkout Session
138
+
139
+ Create a checkout session with your API Key. The response includes a checkout URL with the token embedded in the URL fragment.
140
+
141
+ ```typescript
142
+ import { CheckoutSessionProductType } from "@waffo/pancake-ts";
143
+
144
+ const session = await client.checkout.createSession({
145
+ storeId: "store_xxx",
146
+ productId: "prod_xxx",
147
+ productType: CheckoutSessionProductType.Onetime,
148
+ currency: "USD",
149
+ buyerEmail: "customer@example.com",
150
+ successUrl: "https://example.com/thank-you",
151
+ });
152
+ // session.checkoutUrl format:
153
+ // https://waffo.ai/store/{slug}/checkout/{sessionId}#token={JWT}
154
+ ```
155
+
156
+ The token is passed via the URL fragment (after `#`), which is never sent to the server and never appears in the `Referer` header.
157
+
158
+ ### Step 3 — Open Checkout Page (New Tab)
159
+
160
+ **We recommend opening the checkout page in a new tab** rather than navigating in the current page. Benefits:
161
+
162
+ - Buyers can return to your site immediately after payment or if they close the checkout tab
163
+ - Merchant page state (cart, forms, scroll position) is preserved
164
+ - Payment flow is decoupled from the browsing experience, reducing checkout abandonment
165
+
166
+ ```typescript
167
+ // Frontend — recommended: open in a new tab
168
+ window.open(session.checkoutUrl, "_blank", "noopener,noreferrer");
169
+
170
+ // Or via an <a> tag
171
+ // <a href={checkoutUrl} target="_blank" rel="noopener noreferrer">Proceed to Checkout</a>
172
+ ```
173
+
174
+ > **Not recommended:** `window.location.href = session.checkoutUrl` replaces the current page, preventing buyers from returning to your site without browser back navigation.
175
+
176
+ ### Complete Example (Express)
177
+
178
+ ```typescript
179
+ import express from "express";
180
+ import { WaffoPancake, CheckoutSessionProductType } from "@waffo/pancake-ts";
181
+
182
+ const client = new WaffoPancake({
183
+ merchantId: process.env.WAFFO_MERCHANT_ID!,
184
+ privateKey: process.env.WAFFO_PRIVATE_KEY!,
185
+ });
186
+
187
+ const app = express();
188
+
189
+ app.post("/api/checkout", async (req, res) => {
190
+ const { productId, currency, buyerEmail } = req.body;
191
+
192
+ // Step 1: Issue session token
193
+ const { token } = await client.auth.issueSessionToken({
194
+ storeId: "store_xxx",
195
+ buyerIdentity: buyerEmail,
196
+ });
197
+
198
+ // Step 2: Create checkout session
199
+ const session = await client.checkout.createSession({
200
+ storeId: "store_xxx",
201
+ productId,
202
+ productType: CheckoutSessionProductType.Onetime,
203
+ currency,
204
+ buyerEmail,
205
+ successUrl: "https://example.com/thank-you",
206
+ });
207
+
208
+ // Return URL to frontend (frontend opens in new tab)
209
+ res.json({ checkoutUrl: session.checkoutUrl });
210
+ });
211
+ ```
212
+
213
+ ```typescript
214
+ // Frontend
215
+ const res = await fetch("/api/checkout", {
216
+ method: "POST",
217
+ headers: { "Content-Type": "application/json" },
218
+ body: JSON.stringify({ productId: "prod_xxx", currency: "USD", buyerEmail: "customer@example.com" }),
219
+ });
220
+ const { checkoutUrl } = await res.json();
221
+ window.open(checkoutUrl, "_blank", "noopener,noreferrer");
222
+ ```
223
+
79
224
  ## Usage Examples
80
225
 
81
226
  ### Auth — Issue a Buyer Session Token
@@ -130,9 +275,9 @@ const { product } = await client.onetimeProducts.create({
130
275
  name: "E-Book: TypeScript Handbook",
131
276
  description: "Complete TypeScript guide for developers",
132
277
  prices: {
133
- USD: { amount: 2900, taxIncluded: false, taxCategory: TaxCategory.DigitalGoods },
134
- EUR: { amount: 2700, taxIncluded: true, taxCategory: TaxCategory.DigitalGoods },
135
- 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 },
136
281
  },
137
282
  media: [{ type: "image", url: "https://example.com/cover.jpg", alt: "Book cover" }],
138
283
  metadata: { sku: "ebook-ts-001" },
@@ -142,7 +287,7 @@ const { product } = await client.onetimeProducts.create({
142
287
  await client.onetimeProducts.update({
143
288
  id: product.id,
144
289
  name: "E-Book: TypeScript Handbook v2",
145
- prices: { USD: { amount: 3900, taxIncluded: false, taxCategory: "digital_goods" } },
290
+ prices: { USD: { amount: 3900, taxCategory: "digital_goods" } },
146
291
  });
147
292
 
148
293
  // Publish test version → production
@@ -161,7 +306,7 @@ const { product } = await client.subscriptionProducts.create({
161
306
  storeId: "store_xxx",
162
307
  name: "Pro Plan",
163
308
  billingPeriod: BillingPeriod.Monthly,
164
- prices: { USD: { amount: 999, taxIncluded: false, taxCategory: TaxCategory.SaaS } },
309
+ prices: { USD: { amount: 999, taxCategory: TaxCategory.SaaS } },
165
310
  description: "Unlimited access to all features",
166
311
  });
167
312
 
@@ -218,6 +363,7 @@ const session = await client.checkout.createSession({
218
363
 
219
364
  // Subscription with trial and billing detail
220
365
  const subSession = await client.checkout.createSession({
366
+ storeId: "store_xxx",
221
367
  productId: "prod_yyy",
222
368
  productType: CheckoutSessionProductType.Subscription,
223
369
  currency: "USD",
@@ -260,7 +406,9 @@ See [GraphQL Guide](docs/graphql-guide.md) for introspection, filters, paginatio
260
406
 
261
407
  ## Webhook Verification
262
408
 
263
- 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)
264
412
 
265
413
  ```typescript
266
414
  import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts";
@@ -314,6 +462,32 @@ const event = verifyWebhook(body, sig, { environment: "prod" });
314
462
  const event = verifyWebhook(body, sig, { toleranceMs: 0 }); // disable replay check
315
463
  ```
316
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
+
317
491
  See [Webhook Guide](docs/webhook-guide.md) for all 10 event types, signature algorithm, and best practices.
318
492
 
319
493
  ## Error Handling
@@ -406,7 +580,8 @@ src/
406
580
  ├── subscription-product-groups.ts
407
581
  ├── orders.ts
408
582
  ├── checkout.ts
409
- └── graphql.ts
583
+ ├── graphql.ts
584
+ └── webhooks.ts
410
585
  docs/
411
586
  ├── api-reference.md # Complete API reference
412
587
  ├── graphql-guide.md # GraphQL usage guide
package/dist/index.cjs CHANGED
@@ -59,6 +59,104 @@ var WaffoPancakeError = class extends Error {
59
59
 
60
60
  // src/signing.ts
61
61
  var import_node_crypto = require("crypto");
62
+ var PKCS8_HEADER = "-----BEGIN PRIVATE KEY-----";
63
+ var PKCS8_FOOTER = "-----END PRIVATE KEY-----";
64
+ var PKCS1_HEADER = "-----BEGIN RSA PRIVATE KEY-----";
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-----";
70
+ function normalizePrivateKey(raw) {
71
+ if (!raw || !raw.trim()) {
72
+ throw new Error(
73
+ "Private key is empty. Provide an RSA private key in PEM format."
74
+ );
75
+ }
76
+ let pem = raw.replace(/\\n/g, "\n").replace(/\r\n/g, "\n");
77
+ pem = pem.trim();
78
+ const hasPkcs8Header = pem.includes(PKCS8_HEADER);
79
+ const hasPkcs1Header = pem.includes(PKCS1_HEADER);
80
+ const hasHeader = hasPkcs8Header || hasPkcs1Header;
81
+ if (hasHeader) {
82
+ const base64 = pem.replace(/-----BEGIN (?:RSA )?PRIVATE KEY-----/g, "").replace(/-----END (?:RSA )?PRIVATE KEY-----/g, "").replace(/\s+/g, "");
83
+ if (!base64) {
84
+ throw new Error(
85
+ "Private key contains PEM headers but no key data. Check the key content."
86
+ );
87
+ }
88
+ const header = hasPkcs1Header ? PKCS1_HEADER : PKCS8_HEADER;
89
+ const footer = hasPkcs1Header ? PKCS1_FOOTER : PKCS8_FOOTER;
90
+ const wrapped = base64.match(/.{1,64}/g).join("\n");
91
+ pem = `${header}
92
+ ${wrapped}
93
+ ${footer}`;
94
+ } else {
95
+ const base64 = pem.replace(/\s+/g, "");
96
+ if (!/^[A-Za-z0-9+/]+=*$/.test(base64)) {
97
+ throw new Error(
98
+ "Private key is not valid PEM or base64. Expected an RSA private key in PEM format or raw base64."
99
+ );
100
+ }
101
+ const wrapped = base64.match(/.{1,64}/g).join("\n");
102
+ pem = `${PKCS8_HEADER}
103
+ ${wrapped}
104
+ ${PKCS8_FOOTER}`;
105
+ }
106
+ try {
107
+ (0, import_node_crypto.createPrivateKey)(pem);
108
+ } catch {
109
+ throw new Error(
110
+ "Private key could not be parsed. Ensure it is a valid RSA private key in PKCS#8 or PKCS#1 (PEM) format."
111
+ );
112
+ }
113
+ return pem;
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
+ }
62
160
  function signRequest(method, path, timestamp, body, privateKey) {
63
161
  const bodyHash = (0, import_node_crypto.createHash)("sha256").update(body).digest("base64");
64
162
  const canonicalRequest = `${method}
@@ -79,7 +177,7 @@ var HttpClient = class {
79
177
  _fetch;
80
178
  constructor(config) {
81
179
  this.merchantId = config.merchantId;
82
- this.privateKey = config.privateKey;
180
+ this.privateKey = normalizePrivateKey(config.privateKey);
83
181
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
84
182
  this._fetch = config.fetch ?? fetch;
85
183
  }
@@ -210,7 +308,7 @@ var OnetimeProductsResource = class {
210
308
  * const { product } = await client.onetimeProducts.create({
211
309
  * storeId: "store_xxx",
212
310
  * name: "E-Book",
213
- * prices: { USD: { amount: 2900, taxIncluded: false, taxCategory: "digital_goods" } },
311
+ * prices: { USD: { amount: 2900, taxCategory: "digital_goods" } },
214
312
  * });
215
313
  */
216
314
  async create(params) {
@@ -226,7 +324,7 @@ var OnetimeProductsResource = class {
226
324
  * const { product } = await client.onetimeProducts.update({
227
325
  * id: "prod_xxx",
228
326
  * name: "E-Book v2",
229
- * prices: { USD: { amount: 3900, taxIncluded: false, taxCategory: "digital_goods" } },
327
+ * prices: { USD: { amount: 3900, taxCategory: "digital_goods" } },
230
328
  * });
231
329
  */
232
330
  async update(params) {
@@ -367,7 +465,6 @@ var StoresResource = class {
367
465
  * const { store } = await client.stores.update({
368
466
  * id: "store_xxx",
369
467
  * name: "Updated Name",
370
- * supportEmail: "help@example.com",
371
468
  * });
372
469
  */
373
470
  async update(params) {
@@ -466,7 +563,7 @@ var SubscriptionProductsResource = class {
466
563
  * storeId: "store_xxx",
467
564
  * name: "Pro Plan",
468
565
  * billingPeriod: "monthly",
469
- * prices: { USD: { amount: 999, taxIncluded: false, taxCategory: "saas" } },
566
+ * prices: { USD: { amount: 999, taxCategory: "saas" } },
470
567
  * });
471
568
  */
472
569
  async create(params) {
@@ -483,7 +580,7 @@ var SubscriptionProductsResource = class {
483
580
  * id: "prod_xxx",
484
581
  * name: "Pro Plan v2",
485
582
  * billingPeriod: "monthly",
486
- * prices: { USD: { amount: 1499, taxIncluded: false, taxCategory: "saas" } },
583
+ * prices: { USD: { amount: 1499, taxCategory: "saas" } },
487
584
  * });
488
585
  */
489
586
  async update(params) {
@@ -518,32 +615,6 @@ var SubscriptionProductsResource = class {
518
615
  }
519
616
  };
520
617
 
521
- // src/client.ts
522
- var WaffoPancake = class {
523
- http;
524
- auth;
525
- stores;
526
- storeMerchants;
527
- onetimeProducts;
528
- subscriptionProducts;
529
- subscriptionProductGroups;
530
- orders;
531
- checkout;
532
- graphql;
533
- constructor(config) {
534
- this.http = new HttpClient(config);
535
- this.auth = new AuthResource(this.http);
536
- this.stores = new StoresResource(this.http);
537
- this.storeMerchants = new StoreMerchantsResource(this.http);
538
- this.onetimeProducts = new OnetimeProductsResource(this.http);
539
- this.subscriptionProducts = new SubscriptionProductsResource(this.http);
540
- this.subscriptionProductGroups = new SubscriptionProductGroupsResource(this.http);
541
- this.orders = new OrdersResource(this.http);
542
- this.checkout = new CheckoutResource(this.http);
543
- this.graphql = new GraphQLResource(this.http);
544
- }
545
- };
546
-
547
618
  // src/webhooks.ts
548
619
  var import_node_crypto3 = require("crypto");
549
620
  var DEFAULT_TOLERANCE_MS = 5 * 60 * 1e3;
@@ -602,27 +673,97 @@ function verifyWebhook(payload, signatureHeader, options) {
602
673
  }
603
674
  }
604
675
  const signatureInput = `${t}.${payload}`;
605
- const env = options?.environment;
606
- if (env === "test") {
607
- if (!rsaVerify(signatureInput, v1, TEST_PUBLIC_KEY)) {
608
- throw new Error("Invalid webhook signature (test key)");
609
- }
610
- } else if (env === "prod") {
611
- if (!rsaVerify(signatureInput, v1, PROD_PUBLIC_KEY)) {
612
- 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)");
613
681
  }
614
682
  } else {
615
- const prodValid = rsaVerify(signatureInput, v1, PROD_PUBLIC_KEY);
616
- if (!prodValid) {
617
- const testValid = rsaVerify(signatureInput, v1, TEST_PUBLIC_KEY);
618
- if (!testValid) {
619
- 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
+ }
620
699
  }
621
700
  }
622
701
  }
623
702
  return JSON.parse(payload);
624
703
  }
625
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
+
626
767
  // src/types.ts
627
768
  var Environment = /* @__PURE__ */ ((Environment2) => {
628
769
  Environment2["Test"] = "test";