@waffo/pancake-ts 0.1.3 → 0.1.7

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,80 @@ 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.webhookPublicKey` accepts `string` (shared) or `{ test?, prod? }` (per-environment) to override built-in keys.
12
+ - **Multi-level key resolution** — Webhook public key is resolved per environment: `options.publicKey` (per-call) → config key → `WAFFO_WEBHOOK_{TEST|PROD}_PUBLIC_KEY` env var → `WAFFO_WEBHOOK_PUBLIC_KEY` env var → built-in hardcoded key.
13
+ - **`client.webhooks.verify()`** — New resource namespace on the client instance. Injects config-level keys into the resolution chain automatically; supports per-call override via `options.publicKey`.
14
+ - **`VerifyWebhookOptions.publicKey`** — Per-call override for the standalone `verifyWebhook()` function (highest priority, skips all resolution).
15
+ - **`VerifyWebhookOptions.publicKeys`** — Config-level key(s) for the resolution chain (typically injected by `client.webhooks.verify()`).
16
+ - **`WebhookPublicKeys` type** — `string | { test?: string; prod?: string }`, exported from the package.
17
+ - **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 at every level of the resolution chain.
18
+
19
+ ## [0.1.6] - 2026-03-18
20
+
21
+ ### Changed
22
+
23
+ - **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.
24
+ - **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).
25
+
26
+ ### Migration
27
+
28
+ Remove `taxIncluded` from all `PriceInfo` / `Prices` objects:
29
+
30
+ ```diff
31
+ const { product } = await client.onetimeProducts.create({
32
+ storeId: "store_xxx",
33
+ name: "My Product",
34
+ prices: {
35
+ - USD: { amount: 2900, taxIncluded: false, taxCategory: "digital_goods" },
36
+ + USD: { amount: 2900, taxCategory: "digital_goods" },
37
+ },
38
+ });
39
+ ```
40
+
41
+ Remove `isPublic` from `stores.update()` calls:
42
+
43
+ ```diff
44
+ const { store } = await client.stores.update({
45
+ id: "store_xxx",
46
+ - isPublic: true,
47
+ name: "Updated Name",
48
+ });
49
+ ```
50
+
51
+ ## [0.1.5] - 2026-03-18
52
+
53
+ ### Changed
54
+
55
+ - **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.
56
+
57
+ ### Migration
58
+
59
+ If your code references `checkoutColorTextSecondary`, remove it. The field is no longer accepted by the API and is silently ignored in stored data.
60
+
61
+ ```diff
62
+ const theme: CheckoutThemeSettings = {
63
+ checkoutLogo: null,
64
+ checkoutColorPrimary: "#6366f1",
65
+ checkoutColorBackground: "#ffffff",
66
+ checkoutColorCard: "#f9fafb",
67
+ checkoutColorText: "#111827",
68
+ - checkoutColorTextSecondary: "#6b7280",
69
+ checkoutBorderRadius: "0.5rem",
70
+ };
71
+ ```
72
+
73
+ ## [0.1.4] - 2026-03-16
74
+
75
+ ### Changed
76
+
77
+ - **Types** — `CheckoutSettings` adds `defaultDarkMode: boolean` field to match API response (store create/update)
78
+ - **Types** — `CreateCheckoutSessionParams` adds optional `darkMode` field for dark mode override
79
+ - **Types** — `CreateCheckoutSessionParams.storeId` changed from optional to required to match API spec
80
+
7
81
  ## [0.1.3] - 2026-03-11
8
82
 
9
83
  ### 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 \| { test?, prod? }` | No | Custom webhook public key(s) (see [Webhook Public Key Resolution](#webhook-public-key-resolution) below) |
62
63
 
63
64
  ### Private Key Formats
64
65
 
@@ -82,6 +83,53 @@ 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
+ ### Webhook Public Key Resolution
87
+
88
+ The SDK resolves the webhook verification public key per environment using a multi-level fallback chain:
89
+
90
+ | Priority | Source | Description |
91
+ |----------|--------|-------------|
92
+ | 1 | `options.publicKey` | Per-call override (highest priority, skips all resolution) |
93
+ | 2 | `config.webhookPublicKey[env]` | Config object per-environment key |
94
+ | 3 | `config.webhookPublicKey` (string) | Config shared key (both environments) |
95
+ | 4 | `WAFFO_WEBHOOK_TEST_PUBLIC_KEY` / `WAFFO_WEBHOOK_PROD_PUBLIC_KEY` | Environment variable per-environment |
96
+ | 5 | `WAFFO_WEBHOOK_PUBLIC_KEY` | Environment variable shared |
97
+ | 6 | Built-in hardcoded key | SDK-embedded Waffo public key (default) |
98
+
99
+ ```typescript
100
+ // Shared key for both environments
101
+ new WaffoPancake({ merchantId: "m_1", privateKey: "...", webhookPublicKey: "MIIBIjAN..." });
102
+
103
+ // Per-environment keys
104
+ new WaffoPancake({
105
+ merchantId: "m_1",
106
+ privateKey: "...",
107
+ webhookPublicKey: {
108
+ test: process.env.WAFFO_TEST_PUB_KEY!,
109
+ prod: process.env.WAFFO_PROD_PUB_KEY!,
110
+ },
111
+ });
112
+
113
+ // Or rely on environment variables (no config needed)
114
+ // export WAFFO_WEBHOOK_TEST_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
115
+ // export WAFFO_WEBHOOK_PROD_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
116
+ new WaffoPancake({ merchantId: "m_1", privateKey: "..." });
117
+ // => SDK auto-reads from env vars, falls back to built-in keys
118
+ ```
119
+
120
+ ### Public Key Formats
121
+
122
+ All public key inputs (config, env vars, per-call) accept the same flexible formats as private keys:
123
+
124
+ | Format | Example | Notes |
125
+ |--------|---------|-------|
126
+ | Standard SPKI PEM | `-----BEGIN PUBLIC KEY-----\n...` | Recommended |
127
+ | PKCS#1 PEM | `-----BEGIN RSA PUBLIC KEY-----\n...` | Also accepted |
128
+ | Literal `\n` (env vars) | `"-----BEGIN PUBLIC KEY-----\\nMIIB..."` | Common when stored in `.env` or CI secrets |
129
+ | Windows line endings | `\r\n` | Converted to `\n` |
130
+ | Raw base64 (no headers) | `MIIBIjANBgkqhki...` | Wrapped with SPKI headers automatically |
131
+ | Single-line base64 with headers | Header + all base64 on one line + footer | Re-wrapped to 64-char lines |
132
+
85
133
  ## Resources
86
134
 
87
135
  | Namespace | Methods | Description |
@@ -95,6 +143,7 @@ new WaffoPancake({ merchantId: "m_1", privateKey: rawBase64String });
95
143
  | `client.orders` | `cancelSubscription()` | Order management (pending→canceled, active→canceling) |
96
144
  | `client.checkout` | `createSession()` | Create a checkout session with trial toggle, billing detail, and price snapshot |
97
145
  | `client.graphql` | `query<T>()` | Typed GraphQL queries (Query only, no Mutations) |
146
+ | `client.webhooks` | `verify<T>()` | Webhook signature verification (uses configured `webhookPublicKey` or built-in keys) |
98
147
 
99
148
  See [API Reference](docs/api-reference.md) for complete parameter tables and return types.
100
149
 
@@ -260,9 +309,9 @@ const { product } = await client.onetimeProducts.create({
260
309
  name: "E-Book: TypeScript Handbook",
261
310
  description: "Complete TypeScript guide for developers",
262
311
  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 },
312
+ USD: { amount: 2900, taxCategory: TaxCategory.DigitalGoods },
313
+ EUR: { amount: 2700, taxCategory: TaxCategory.DigitalGoods },
314
+ JPY: { amount: 4500, taxCategory: TaxCategory.DigitalGoods },
266
315
  },
267
316
  media: [{ type: "image", url: "https://example.com/cover.jpg", alt: "Book cover" }],
268
317
  metadata: { sku: "ebook-ts-001" },
@@ -272,7 +321,7 @@ const { product } = await client.onetimeProducts.create({
272
321
  await client.onetimeProducts.update({
273
322
  id: product.id,
274
323
  name: "E-Book: TypeScript Handbook v2",
275
- prices: { USD: { amount: 3900, taxIncluded: false, taxCategory: "digital_goods" } },
324
+ prices: { USD: { amount: 3900, taxCategory: "digital_goods" } },
276
325
  });
277
326
 
278
327
  // Publish test version → production
@@ -291,7 +340,7 @@ const { product } = await client.subscriptionProducts.create({
291
340
  storeId: "store_xxx",
292
341
  name: "Pro Plan",
293
342
  billingPeriod: BillingPeriod.Monthly,
294
- prices: { USD: { amount: 999, taxIncluded: false, taxCategory: TaxCategory.SaaS } },
343
+ prices: { USD: { amount: 999, taxCategory: TaxCategory.SaaS } },
295
344
  description: "Unlimited access to all features",
296
345
  });
297
346
 
@@ -348,6 +397,7 @@ const session = await client.checkout.createSession({
348
397
 
349
398
  // Subscription with trial and billing detail
350
399
  const subSession = await client.checkout.createSession({
400
+ storeId: "store_xxx",
351
401
  productId: "prod_yyy",
352
402
  productType: CheckoutSessionProductType.Subscription,
353
403
  currency: "USD",
@@ -390,7 +440,9 @@ See [GraphQL Guide](docs/graphql-guide.md) for introspection, filters, paginatio
390
440
 
391
441
  ## Webhook Verification
392
442
 
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.
443
+ 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`.
444
+
445
+ ### Option A — Standalone Function (built-in keys)
394
446
 
395
447
  ```typescript
396
448
  import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts";
@@ -444,7 +496,34 @@ const event = verifyWebhook(body, sig, { environment: "prod" });
444
496
  const event = verifyWebhook(body, sig, { toleranceMs: 0 }); // disable replay check
445
497
  ```
446
498
 
447
- See [Webhook Guide](docs/webhook-guide.md) for all 10 event types, signature algorithm, and best practices.
499
+ ### Option B Client Instance Method (multi-level key resolution)
500
+
501
+ `client.webhooks.verify()` uses the [multi-level fallback chain](#webhook-public-key-resolution) automatically: config keys → env vars → built-in keys.
502
+
503
+ ```typescript
504
+ // Per-environment keys via config
505
+ const client = new WaffoPancake({
506
+ merchantId: process.env.WAFFO_MERCHANT_ID!,
507
+ privateKey: process.env.WAFFO_PRIVATE_KEY!,
508
+ webhookPublicKey: {
509
+ test: process.env.WAFFO_TEST_PUB_KEY!,
510
+ prod: process.env.WAFFO_PROD_PUB_KEY!,
511
+ },
512
+ });
513
+ const event = client.webhooks.verify(rawBody, sig, { environment: "prod" });
514
+
515
+ // Or rely on env vars (WAFFO_WEBHOOK_TEST_PUBLIC_KEY / WAFFO_WEBHOOK_PROD_PUBLIC_KEY)
516
+ const client2 = new WaffoPancake({
517
+ merchantId: process.env.WAFFO_MERCHANT_ID!,
518
+ privateKey: process.env.WAFFO_PRIVATE_KEY!,
519
+ });
520
+ const event2 = client2.webhooks.verify(rawBody, sig); // auto-detect environment
521
+
522
+ // Per-call override (highest priority, skips all resolution)
523
+ const event3 = client.webhooks.verify(rawBody, sig, { publicKey: oneOffKey });
524
+ ```
525
+
526
+ See [Webhook Guide](docs/webhook-guide.md) for event types, signature algorithm, public key resolution, and best practices.
448
527
 
449
528
  ## Error Handling
450
529
 
@@ -503,7 +582,7 @@ Runtime-accessible values. Both `Enum.Value` and string literal syntax are suppo
503
582
 
504
583
  ### Types
505
584
 
506
- See [API Reference — Types](docs/api-reference.md#types) for the full list of 40+ exported interfaces.
585
+ Key types: `WaffoPancakeConfig`, `WebhookPublicKeys`, `VerifyWebhookOptions`, `WebhookEvent<T>`, `Store`, `OnetimeProductDetail`, `SubscriptionProductDetail`, `CheckoutSessionResult`, `GraphQLResponse<T>`, and 30+ more. See [API Reference — Types](docs/api-reference.md#types) for the full list.
507
586
 
508
587
  ## Development
509
588
 
@@ -536,7 +615,8 @@ src/
536
615
  ├── subscription-product-groups.ts
537
616
  ├── orders.ts
538
617
  ├── checkout.ts
539
- └── graphql.ts
618
+ ├── graphql.ts
619
+ └── webhooks.ts
540
620
  docs/
541
621
  ├── api-reference.md # Complete API reference
542
622
  ├── 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;
@@ -632,6 +654,23 @@ function rsaVerify(signatureInput, v1, publicKey) {
632
654
  verifier.update(signatureInput);
633
655
  return verifier.verify(publicKey, v1, "base64");
634
656
  }
657
+ function resolveKeyForEnv(env, configKeys) {
658
+ if (typeof configKeys === "string") {
659
+ return normalizePublicKey(configKeys);
660
+ }
661
+ if (configKeys?.[env]) {
662
+ return normalizePublicKey(configKeys[env]);
663
+ }
664
+ const envSpecific = env === "test" ? process.env.WAFFO_WEBHOOK_TEST_PUBLIC_KEY : process.env.WAFFO_WEBHOOK_PROD_PUBLIC_KEY;
665
+ if (envSpecific) {
666
+ return normalizePublicKey(envSpecific);
667
+ }
668
+ const generic = process.env.WAFFO_WEBHOOK_PUBLIC_KEY;
669
+ if (generic) {
670
+ return normalizePublicKey(generic);
671
+ }
672
+ return env === "test" ? TEST_PUBLIC_KEY : PROD_PUBLIC_KEY;
673
+ }
635
674
  function verifyWebhook(payload, signatureHeader, options) {
636
675
  if (!signatureHeader) {
637
676
  throw new Error("Missing X-Waffo-Signature header");
@@ -651,27 +690,103 @@ function verifyWebhook(payload, signatureHeader, options) {
651
690
  }
652
691
  }
653
692
  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)");
693
+ const directKey = options?.publicKey;
694
+ if (directKey) {
695
+ const normalizedKey = normalizePublicKey(directKey);
696
+ if (!rsaVerify(signatureInput, v1, normalizedKey)) {
697
+ throw new Error("Invalid webhook signature (custom key)");
662
698
  }
663
699
  } 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)");
700
+ const configKeys = options?.publicKeys;
701
+ const env = options?.environment;
702
+ if (env === "test" || env === "prod") {
703
+ const key = resolveKeyForEnv(env, configKeys);
704
+ if (!rsaVerify(signatureInput, v1, key)) {
705
+ throw new Error(`Invalid webhook signature (${env} key)`);
706
+ }
707
+ } else {
708
+ const prodKey = resolveKeyForEnv("prod", configKeys);
709
+ if (!rsaVerify(signatureInput, v1, prodKey)) {
710
+ const testKey = resolveKeyForEnv("test", configKeys);
711
+ if (!rsaVerify(signatureInput, v1, testKey)) {
712
+ throw new Error("Invalid webhook signature (tried both prod and test keys)");
713
+ }
669
714
  }
670
715
  }
671
716
  }
672
717
  return JSON.parse(payload);
673
718
  }
674
719
 
720
+ // src/resources/webhooks.ts
721
+ var WebhooksResource = class {
722
+ /** @param publicKeys - Optional config-level public key(s) from WaffoPancakeConfig */
723
+ constructor(publicKeys) {
724
+ this.publicKeys = publicKeys;
725
+ }
726
+ /**
727
+ * Verify and parse an incoming webhook event.
728
+ *
729
+ * Key resolution order:
730
+ * 1. `options.publicKey` — per-call override (highest priority)
731
+ * 2. `config.webhookPublicKey[env]` or `config.webhookPublicKey` (string)
732
+ * 3. `WAFFO_WEBHOOK_{TEST|PROD}_PUBLIC_KEY` environment variable
733
+ * 4. `WAFFO_WEBHOOK_PUBLIC_KEY` environment variable
734
+ * 5. Built-in hardcoded key
735
+ *
736
+ * @param payload - Raw request body string (must be unparsed)
737
+ * @param signatureHeader - Value of the `X-Waffo-Signature` header
738
+ * @param options - Verification options (optional)
739
+ * @returns Parsed webhook event
740
+ * @throws Error if signature is invalid, header is malformed, or timestamp is stale
741
+ *
742
+ * @example
743
+ * const event = client.webhooks.verify(rawBody, signatureHeader);
744
+ *
745
+ * @example
746
+ * // Specify environment
747
+ * const event = client.webhooks.verify(rawBody, sig, { environment: "test" });
748
+ *
749
+ * @example
750
+ * // Per-call key override
751
+ * const event = client.webhooks.verify(rawBody, sig, { publicKey: oneOffKey });
752
+ */
753
+ verify(payload, signatureHeader, options) {
754
+ const mergedOptions = {
755
+ ...options,
756
+ publicKeys: options?.publicKeys ?? this.publicKeys
757
+ };
758
+ return verifyWebhook(payload, signatureHeader, mergedOptions);
759
+ }
760
+ };
761
+
762
+ // src/client.ts
763
+ var WaffoPancake = class {
764
+ http;
765
+ auth;
766
+ stores;
767
+ storeMerchants;
768
+ onetimeProducts;
769
+ subscriptionProducts;
770
+ subscriptionProductGroups;
771
+ orders;
772
+ checkout;
773
+ graphql;
774
+ webhooks;
775
+ constructor(config) {
776
+ this.http = new HttpClient(config);
777
+ this.auth = new AuthResource(this.http);
778
+ this.stores = new StoresResource(this.http);
779
+ this.storeMerchants = new StoreMerchantsResource(this.http);
780
+ this.onetimeProducts = new OnetimeProductsResource(this.http);
781
+ this.subscriptionProducts = new SubscriptionProductsResource(this.http);
782
+ this.subscriptionProductGroups = new SubscriptionProductGroupsResource(this.http);
783
+ this.orders = new OrdersResource(this.http);
784
+ this.checkout = new CheckoutResource(this.http);
785
+ this.graphql = new GraphQLResource(this.http);
786
+ this.webhooks = new WebhooksResource(config.webhookPublicKey);
787
+ }
788
+ };
789
+
675
790
  // src/types.ts
676
791
  var Environment = /* @__PURE__ */ ((Environment2) => {
677
792
  Environment2["Test"] = "test";