@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 +78 -0
- package/README.md +185 -10
- package/dist/index.cjs +186 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -23
- package/dist/index.d.ts +63 -23
- package/dist/index.js +187 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
35
|
-
EUR: { amount: 2700,
|
|
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
|
|
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,
|
|
134
|
-
EUR: { amount: 2700,
|
|
135
|
-
JPY: { amount: 4500,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
606
|
-
if (
|
|
607
|
-
|
|
608
|
-
|
|
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
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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";
|