@waffo/pancake-ts 0.1.5 → 0.1.9
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 +38 -6
- package/README.md +203 -336
- package/dist/index.cjs +166 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +333 -68
- package/dist/index.d.ts +333 -68
- package/dist/index.js +166 -55
- package/dist/index.js.map +1 -1
- package/docs/api-reference.md +786 -0
- package/docs/graphql-guide.md +664 -0
- package/docs/webhook-guide.md +456 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
TypeScript SDK for the Waffo Pancake Merchant of Record (MoR) payment platform.
|
|
4
4
|
|
|
5
|
-
- Zero runtime dependencies, ESM
|
|
5
|
+
- Zero runtime dependencies, ESM + CJS, Node >= 20
|
|
6
6
|
- Automatic RSA-SHA256 request signing with deterministic idempotency keys
|
|
7
7
|
- Full TypeScript type definitions (15 enums, 40+ interfaces)
|
|
8
8
|
- Webhook verification with embedded public keys (test/prod)
|
|
@@ -15,224 +15,223 @@ npm install @waffo/pancake-ts
|
|
|
15
15
|
|
|
16
16
|
## Quick Start
|
|
17
17
|
|
|
18
|
+
> Most merchants create stores and products in the [Dashboard](https://pancake.waffo.ai/dashboard). The SDK is primarily used for **checkout integration** — redirecting buyers from your site to the Waffo checkout page.
|
|
19
|
+
|
|
18
20
|
```typescript
|
|
19
21
|
import { WaffoPancake } from "@waffo/pancake-ts";
|
|
20
22
|
|
|
23
|
+
// Merchant ID and API Key are available in Dashboard > Settings > Developers
|
|
21
24
|
const client = new WaffoPancake({
|
|
22
|
-
merchantId: process.env.WAFFO_MERCHANT_ID!,
|
|
25
|
+
merchantId: process.env.WAFFO_MERCHANT_ID!, // MER_{base62} format
|
|
23
26
|
privateKey: process.env.WAFFO_PRIVATE_KEY!,
|
|
24
27
|
});
|
|
25
28
|
|
|
26
|
-
// Create a
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
const { product } = await client.onetimeProducts.create({
|
|
31
|
-
storeId: store.id,
|
|
32
|
-
name: "E-Book: TypeScript Handbook",
|
|
33
|
-
prices: {
|
|
34
|
-
USD: { amount: 2900, taxCategory: "digital_goods" },
|
|
35
|
-
EUR: { amount: 2700, taxCategory: "digital_goods" },
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Create a checkout session and redirect the buyer
|
|
40
|
-
const session = await client.checkout.createSession({
|
|
41
|
-
storeId: store.id,
|
|
42
|
-
productId: product.id,
|
|
29
|
+
// Create a checkout session — one call handles token + session + URL
|
|
30
|
+
const result = await client.checkout.authenticated.create({
|
|
31
|
+
storeId: "STO_xxx", // from Dashboard > Stores
|
|
32
|
+
productId: "PROD_xxx", // from Dashboard > Products
|
|
43
33
|
productType: "onetime",
|
|
44
34
|
currency: "USD",
|
|
35
|
+
buyerIdentity: req.user.email, // your user's identity
|
|
45
36
|
});
|
|
46
|
-
// => redirect buyer to session.checkoutUrl
|
|
47
37
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
38
|
+
// Redirect buyer to the checkout page (opens in new tab)
|
|
39
|
+
res.json({ checkoutUrl: result.checkoutUrl });
|
|
40
|
+
// => checkoutUrl includes #token=... (form pre-filled)
|
|
52
41
|
```
|
|
53
42
|
|
|
54
43
|
## Configuration
|
|
55
44
|
|
|
56
45
|
| Parameter | Type | Required | Description |
|
|
57
46
|
|-----------|------|----------|-------------|
|
|
58
|
-
| `merchantId` | `string` | Yes | Merchant ID
|
|
59
|
-
| `privateKey` | `string` | Yes | RSA private key (see [
|
|
60
|
-
| `baseUrl` | `string` | No | API base URL
|
|
47
|
+
| `merchantId` | `string` | Yes | Merchant ID in `MER_{base62}` format |
|
|
48
|
+
| `privateKey` | `string` | Yes | RSA private key in PEM format (auto-normalized, see [docs](docs/api-reference.md)) |
|
|
49
|
+
| `baseUrl` | `string` | No | API base URL override |
|
|
61
50
|
| `fetch` | `typeof fetch` | No | Custom fetch implementation |
|
|
62
|
-
| `webhookPublicKey` | `string` | No | Custom
|
|
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 |
|
|
51
|
+
| `webhookPublicKey` | `string \| { test?, prod? }` | No | Custom webhook public key(s) |
|
|
76
52
|
|
|
77
|
-
|
|
53
|
+
The SDK auto-normalizes key formats: standard PEM, PKCS#1, literal `\n` from env vars, raw base64, and Windows line endings are all accepted.
|
|
78
54
|
|
|
79
|
-
|
|
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 |
|
|
98
|
-
|
|
99
|
-
## Resources
|
|
55
|
+
## Checkout Integration
|
|
100
56
|
|
|
101
|
-
|
|
102
|
-
|-----------|---------|-------------|
|
|
103
|
-
| `client.auth` | `issueSessionToken()` | Issue a buyer session token (JWT) |
|
|
104
|
-
| `client.stores` | `create()` `update()` `delete()` | Store management (webhook, notification, checkout settings) |
|
|
105
|
-
| `client.storeMerchants` | `add()` `remove()` `updateRole()` | Store member management (coming soon, returns 501) |
|
|
106
|
-
| `client.onetimeProducts` | `create()` `update()` `publish()` `updateStatus()` | One-time product CRUD with multi-currency pricing and version management |
|
|
107
|
-
| `client.subscriptionProducts` | `create()` `update()` `publish()` `updateStatus()` | Subscription product CRUD with billing period and version management |
|
|
108
|
-
| `client.subscriptionProductGroups` | `create()` `update()` `delete()` `publish()` | Product groups for shared trial and plan switching |
|
|
109
|
-
| `client.orders` | `cancelSubscription()` | Order management (pending→canceled, active→canceling) |
|
|
110
|
-
| `client.checkout` | `createSession()` | Create a checkout session with trial toggle, billing detail, and price snapshot |
|
|
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) |
|
|
113
|
-
|
|
114
|
-
See [API Reference](docs/api-reference.md) for complete parameter tables and return types.
|
|
57
|
+
Waffo supports two checkout modes based on whether the merchant knows the buyer's identity:
|
|
115
58
|
|
|
116
|
-
|
|
59
|
+
- **Merchants with their own sites** know who the buyer is — they have user accounts, login systems, or collect buyer info before checkout. The merchant provides the buyer's identity upfront, and the checkout form arrives pre-filled.
|
|
60
|
+
- **Template stores and shared links** have no prior buyer context — the buyer arrives directly at the checkout page and fills in their own details.
|
|
117
61
|
|
|
118
|
-
|
|
62
|
+
| Mode | Method | Buyer Identity | Form State | Use Case |
|
|
63
|
+
|------|--------|---------------|------------|----------|
|
|
64
|
+
| **Authenticated** | `checkout.authenticated.create()` | Merchant provides | Pre-filled | Merchant sites with user accounts |
|
|
65
|
+
| **Anonymous** | `checkout.anonymous.create()` | Not provided | Empty | Template stores, one-time purchase links |
|
|
119
66
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
67
|
+
> **We recommend authenticated checkout whenever possible.** The most important reason: authenticated checkout binds the order to the `buyerIdentity` you provide, which is a **merchant-controlled stable identifier**. Even if the buyer changes the email on the checkout form, the order is still tied to the identity you specified. In anonymous mode, the buyer self-reports their email on the form — if they enter a different address, the system treats them as a new user, which means **previous orders become unlinked** and **subscription trial periods can be exploited** (a new email = a new user = a fresh trial).
|
|
68
|
+
>
|
|
69
|
+
> Anonymous checkout also uses the `shopper` role, which can **only create orders** (no cancellation, subscription management, or refund tickets) with a **1-minute single-use session**.
|
|
70
|
+
>
|
|
71
|
+
> | | Authenticated (`customer`) | Anonymous (`shopper`) |
|
|
72
|
+
> |---|---|---|
|
|
73
|
+
> | **Identity** | Merchant-provided, stable across orders | Self-reported email, may vary |
|
|
74
|
+
> | **Permissions** | Create orders, cancel orders, manage subscriptions, submit refund tickets | Create orders **only** |
|
|
75
|
+
> | **Session** | 5-minute TTL, auto-refreshes on each API call | 1-minute TTL, **single-use** (consumed on first API call) |
|
|
76
|
+
> | **Subscriptions** | Fully supported — buyers can manage, cancel, or reactivate | Not practical — buyer has no session to manage the subscription afterward |
|
|
125
77
|
|
|
126
|
-
###
|
|
78
|
+
### Authenticated Checkout (Recommended)
|
|
127
79
|
|
|
128
|
-
|
|
80
|
+
The merchant provides buyer identity — the SDK issues a session token, creates a checkout session, and returns a checkout URL with the token appended as a URL fragment. One call does everything.
|
|
129
81
|
|
|
130
82
|
```typescript
|
|
131
|
-
const
|
|
132
|
-
storeId: "
|
|
83
|
+
const result = await client.checkout.authenticated.create({
|
|
84
|
+
storeId: "STO_xxx",
|
|
85
|
+
productId: "PROD_xxx",
|
|
86
|
+
productType: "onetime",
|
|
87
|
+
currency: "USD",
|
|
133
88
|
buyerIdentity: "customer@example.com",
|
|
89
|
+
// Optional: pre-fill billing details
|
|
90
|
+
billingDetail: { country: "US", isBusiness: false },
|
|
134
91
|
});
|
|
92
|
+
// result.checkoutUrl = "https://pancake.waffo.ai/store/{slug}/checkout/{sessionId}#token={JWT}"
|
|
93
|
+
|
|
94
|
+
// Frontend — open in a new tab (recommended)
|
|
95
|
+
window.open(result.checkoutUrl, "_blank", "noopener,noreferrer");
|
|
135
96
|
```
|
|
136
97
|
|
|
137
|
-
|
|
98
|
+
The token is passed via the URL fragment (after `#`), which is never sent to the server and never appears in the `Referer` header.
|
|
138
99
|
|
|
139
|
-
|
|
100
|
+
### Anonymous Checkout
|
|
140
101
|
|
|
141
|
-
|
|
142
|
-
import { CheckoutSessionProductType } from "@waffo/pancake-ts";
|
|
102
|
+
No buyer identity required — the buyer fills in billing details manually on the checkout page.
|
|
143
103
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
104
|
+
```typescript
|
|
105
|
+
const result = await client.checkout.anonymous.create({
|
|
106
|
+
storeId: "STO_xxx",
|
|
107
|
+
productId: "PROD_xxx",
|
|
108
|
+
productType: "onetime",
|
|
148
109
|
currency: "USD",
|
|
149
|
-
buyerEmail: "customer@example.com",
|
|
150
|
-
successUrl: "https://example.com/thank-you",
|
|
151
110
|
});
|
|
152
|
-
//
|
|
153
|
-
// https://waffo.ai/store/{slug}/checkout/{sessionId}#token={JWT}
|
|
154
|
-
```
|
|
111
|
+
// result.checkoutUrl = "https://pancake.waffo.ai/store/{slug}/checkout/{sessionId}"
|
|
155
112
|
|
|
156
|
-
|
|
113
|
+
window.open(result.checkoutUrl, "_blank", "noopener,noreferrer");
|
|
114
|
+
```
|
|
157
115
|
|
|
158
|
-
###
|
|
116
|
+
### Opening the Checkout Page
|
|
159
117
|
|
|
160
|
-
**We recommend opening the checkout page in a new tab** rather than navigating in the current page
|
|
118
|
+
**We recommend opening the checkout page in a new tab** rather than navigating in the current page:
|
|
161
119
|
|
|
162
120
|
- Buyers can return to your site immediately after payment or if they close the checkout tab
|
|
163
121
|
- Merchant page state (cart, forms, scroll position) is preserved
|
|
164
122
|
- Payment flow is decoupled from the browsing experience, reducing checkout abandonment
|
|
165
123
|
|
|
166
124
|
```typescript
|
|
167
|
-
//
|
|
168
|
-
window.open(
|
|
125
|
+
// Recommended: open in a new tab
|
|
126
|
+
window.open(result.checkoutUrl, "_blank", "noopener,noreferrer");
|
|
169
127
|
|
|
170
128
|
// Or via an <a> tag
|
|
171
129
|
// <a href={checkoutUrl} target="_blank" rel="noopener noreferrer">Proceed to Checkout</a>
|
|
172
130
|
```
|
|
173
131
|
|
|
174
|
-
> **Not recommended:** `window.location.href =
|
|
132
|
+
> **Not recommended:** `window.location.href = result.checkoutUrl` replaces the current page, preventing buyers from returning to your site without browser back navigation.
|
|
133
|
+
|
|
134
|
+
See [API Reference — Checkout](docs/api-reference.md#checkout) for full parameter tables and `BillingDetail` field requirements.
|
|
135
|
+
|
|
136
|
+
## Webhook Verification
|
|
137
|
+
|
|
138
|
+
After a buyer completes payment, Waffo sends webhook events to your server. The SDK provides two ways to verify signatures:
|
|
175
139
|
|
|
176
|
-
###
|
|
140
|
+
### Standalone Function (built-in keys)
|
|
177
141
|
|
|
178
142
|
```typescript
|
|
179
|
-
import
|
|
180
|
-
import { WaffoPancake, CheckoutSessionProductType } from "@waffo/pancake-ts";
|
|
143
|
+
import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts";
|
|
181
144
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
145
|
+
// Express (IMPORTANT: use raw body — parsed JSON breaks signature verification)
|
|
146
|
+
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
const event = verifyWebhook(
|
|
149
|
+
req.body.toString("utf-8"),
|
|
150
|
+
req.headers["x-waffo-signature"] as string,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Respond immediately, process asynchronously
|
|
154
|
+
res.status(200).send("OK");
|
|
186
155
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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 });
|
|
156
|
+
switch (event.eventType) {
|
|
157
|
+
case WebhookEventType.OrderCompleted:
|
|
158
|
+
console.log(`Order ${event.data.orderId} completed`);
|
|
159
|
+
break;
|
|
160
|
+
case WebhookEventType.SubscriptionActivated:
|
|
161
|
+
console.log(`Subscription activated for ${event.data.buyerEmail}`);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
res.status(401).send("Invalid signature");
|
|
166
|
+
}
|
|
210
167
|
});
|
|
168
|
+
|
|
169
|
+
// Next.js App Router
|
|
170
|
+
export async function POST(request: Request) {
|
|
171
|
+
const body = await request.text();
|
|
172
|
+
const sig = request.headers.get("x-waffo-signature");
|
|
173
|
+
try {
|
|
174
|
+
const event = verifyWebhook(body, sig);
|
|
175
|
+
return new Response("OK");
|
|
176
|
+
} catch {
|
|
177
|
+
return new Response("Invalid signature", { status: 401 });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
211
180
|
```
|
|
212
181
|
|
|
182
|
+
### Client Instance Method (multi-level key resolution)
|
|
183
|
+
|
|
213
184
|
```typescript
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
185
|
+
const client = new WaffoPancake({
|
|
186
|
+
merchantId: "MER_xxx",
|
|
187
|
+
privateKey: "...",
|
|
188
|
+
webhookPublicKey: {
|
|
189
|
+
test: process.env.WAFFO_TEST_PUB_KEY!,
|
|
190
|
+
prod: process.env.WAFFO_PROD_PUB_KEY!,
|
|
191
|
+
},
|
|
219
192
|
});
|
|
220
|
-
const
|
|
221
|
-
window.open(checkoutUrl, "_blank", "noopener,noreferrer");
|
|
193
|
+
const event = client.webhooks.verify(rawBody, sig, { environment: "prod" });
|
|
222
194
|
```
|
|
223
195
|
|
|
224
|
-
|
|
196
|
+
See [Webhook Guide](docs/webhook-guide.md) for event types, dual-environment key architecture, key resolution chain, retry mechanism, and best practices.
|
|
225
197
|
|
|
226
|
-
|
|
198
|
+
## GraphQL — Typed Queries
|
|
227
199
|
|
|
228
200
|
```typescript
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
201
|
+
// Simple query
|
|
202
|
+
interface StoresQuery {
|
|
203
|
+
stores: Array<{ id: string; name: string; status: string }>;
|
|
204
|
+
}
|
|
205
|
+
const result = await client.graphql.query<StoresQuery>({
|
|
206
|
+
query: `query { stores { id name status } }`,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Query with variables
|
|
210
|
+
const product = await client.graphql.query({
|
|
211
|
+
query: `query ($id: ID!) { onetimeProduct(id: $id) { id name prices } }`,
|
|
212
|
+
variables: { id: "PROD_xxx" },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Nested relationships in a single request
|
|
216
|
+
const detail = await client.graphql.query({
|
|
217
|
+
query: `query ($id: ID!) {
|
|
218
|
+
store(id: $id) {
|
|
219
|
+
id name
|
|
220
|
+
onetimeProducts { id name status prices }
|
|
221
|
+
subscriptionProducts { id name billingPeriod status }
|
|
222
|
+
}
|
|
223
|
+
}`,
|
|
224
|
+
variables: { id: "STO_xxx" },
|
|
232
225
|
});
|
|
233
226
|
```
|
|
234
227
|
|
|
235
|
-
|
|
228
|
+
See [GraphQL Guide](docs/graphql-guide.md) for filters, analytics queries, delivery logs, and more.
|
|
229
|
+
|
|
230
|
+
## Programmatic Store & Product Management
|
|
231
|
+
|
|
232
|
+
> Most merchants manage stores and products in the [Dashboard](https://pancake.waffo.ai/dashboard). The following APIs are for merchants who need programmatic automation.
|
|
233
|
+
|
|
234
|
+
### Stores
|
|
236
235
|
|
|
237
236
|
```typescript
|
|
238
237
|
// Create a store
|
|
@@ -264,20 +263,20 @@ const { store: updated } = await client.stores.update({
|
|
|
264
263
|
const { store: deleted } = await client.stores.delete({ id: store.id });
|
|
265
264
|
```
|
|
266
265
|
|
|
267
|
-
###
|
|
266
|
+
### Products
|
|
268
267
|
|
|
269
268
|
```typescript
|
|
270
|
-
import { TaxCategory, ProductVersionStatus } from "@waffo/pancake-ts";
|
|
269
|
+
import { TaxCategory, BillingPeriod, ProductVersionStatus } from "@waffo/pancake-ts";
|
|
271
270
|
|
|
272
|
-
//
|
|
271
|
+
// One-time product with multi-currency pricing
|
|
273
272
|
const { product } = await client.onetimeProducts.create({
|
|
274
|
-
storeId: "
|
|
273
|
+
storeId: "STO_xxx",
|
|
275
274
|
name: "E-Book: TypeScript Handbook",
|
|
276
275
|
description: "Complete TypeScript guide for developers",
|
|
277
276
|
prices: {
|
|
278
|
-
USD: { amount:
|
|
279
|
-
EUR: { amount:
|
|
280
|
-
JPY: { amount: 4500, taxCategory: TaxCategory.DigitalGoods },
|
|
277
|
+
USD: { amount: "29.00", taxCategory: TaxCategory.DigitalGoods },
|
|
278
|
+
EUR: { amount: "27.00", taxCategory: TaxCategory.DigitalGoods },
|
|
279
|
+
JPY: { amount: "4500", taxCategory: TaxCategory.DigitalGoods },
|
|
281
280
|
},
|
|
282
281
|
media: [{ type: "image", url: "https://example.com/cover.jpg", alt: "Book cover" }],
|
|
283
282
|
metadata: { sku: "ebook-ts-001" },
|
|
@@ -287,7 +286,7 @@ const { product } = await client.onetimeProducts.create({
|
|
|
287
286
|
await client.onetimeProducts.update({
|
|
288
287
|
id: product.id,
|
|
289
288
|
name: "E-Book: TypeScript Handbook v2",
|
|
290
|
-
prices: { USD: { amount:
|
|
289
|
+
prices: { USD: { amount: "39.00", taxCategory: "digital_goods" } },
|
|
291
290
|
});
|
|
292
291
|
|
|
293
292
|
// Publish test version → production
|
|
@@ -295,40 +294,32 @@ await client.onetimeProducts.publish({ id: product.id });
|
|
|
295
294
|
|
|
296
295
|
// Deactivate
|
|
297
296
|
await client.onetimeProducts.updateStatus({ id: product.id, status: ProductVersionStatus.Inactive });
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
### Subscription Products — Create with Billing Period
|
|
301
|
-
|
|
302
|
-
```typescript
|
|
303
|
-
import { BillingPeriod, TaxCategory } from "@waffo/pancake-ts";
|
|
304
297
|
|
|
305
|
-
|
|
306
|
-
|
|
298
|
+
// Subscription product
|
|
299
|
+
const { product: sub } = await client.subscriptionProducts.create({
|
|
300
|
+
storeId: "STO_xxx",
|
|
307
301
|
name: "Pro Plan",
|
|
308
302
|
billingPeriod: BillingPeriod.Monthly,
|
|
309
|
-
prices: { USD: { amount:
|
|
310
|
-
description: "Unlimited access to all features",
|
|
303
|
+
prices: { USD: { amount: "9.99", taxCategory: TaxCategory.SaaS } },
|
|
311
304
|
});
|
|
312
|
-
|
|
313
|
-
// Same update/publish/updateStatus pattern as onetime products
|
|
314
|
-
await client.subscriptionProducts.publish({ id: product.id });
|
|
305
|
+
await client.subscriptionProducts.publish({ id: sub.id });
|
|
315
306
|
```
|
|
316
307
|
|
|
317
|
-
### Subscription Product Groups
|
|
308
|
+
### Subscription Product Groups
|
|
318
309
|
|
|
319
310
|
```typescript
|
|
320
311
|
// Create a group linking related subscription tiers
|
|
321
312
|
const { group } = await client.subscriptionProductGroups.create({
|
|
322
|
-
storeId: "
|
|
313
|
+
storeId: "STO_xxx",
|
|
323
314
|
name: "Pro Plans",
|
|
324
315
|
rules: { sharedTrial: true },
|
|
325
|
-
productIds: ["
|
|
316
|
+
productIds: ["PROD_aaa", "PROD_bbb"],
|
|
326
317
|
});
|
|
327
318
|
|
|
328
319
|
// Update members (full replacement, not merge)
|
|
329
320
|
await client.subscriptionProductGroups.update({
|
|
330
321
|
id: group.id,
|
|
331
|
-
productIds: ["
|
|
322
|
+
productIds: ["PROD_aaa", "PROD_bbb", "PROD_ccc"],
|
|
332
323
|
});
|
|
333
324
|
|
|
334
325
|
// Publish / delete
|
|
@@ -336,160 +327,15 @@ await client.subscriptionProductGroups.publish({ id: group.id });
|
|
|
336
327
|
await client.subscriptionProductGroups.delete({ id: group.id });
|
|
337
328
|
```
|
|
338
329
|
|
|
339
|
-
### Orders
|
|
330
|
+
### Orders
|
|
340
331
|
|
|
341
332
|
```typescript
|
|
342
333
|
const { orderId, status } = await client.orders.cancelSubscription({
|
|
343
|
-
orderId: "
|
|
334
|
+
orderId: "ORD_xxx",
|
|
344
335
|
});
|
|
345
336
|
// status: "canceled" (was pending) or "canceling" (was active, PSP notified)
|
|
346
337
|
```
|
|
347
338
|
|
|
348
|
-
### Checkout — Create a Session
|
|
349
|
-
|
|
350
|
-
```typescript
|
|
351
|
-
import { CheckoutSessionProductType } from "@waffo/pancake-ts";
|
|
352
|
-
|
|
353
|
-
// One-time product checkout
|
|
354
|
-
const session = await client.checkout.createSession({
|
|
355
|
-
storeId: "store_xxx",
|
|
356
|
-
productId: "prod_xxx",
|
|
357
|
-
productType: CheckoutSessionProductType.Onetime,
|
|
358
|
-
currency: "USD",
|
|
359
|
-
buyerEmail: "customer@example.com",
|
|
360
|
-
successUrl: "https://example.com/thank-you",
|
|
361
|
-
});
|
|
362
|
-
// => redirect buyer to session.checkoutUrl
|
|
363
|
-
|
|
364
|
-
// Subscription with trial and billing detail
|
|
365
|
-
const subSession = await client.checkout.createSession({
|
|
366
|
-
storeId: "store_xxx",
|
|
367
|
-
productId: "prod_yyy",
|
|
368
|
-
productType: CheckoutSessionProductType.Subscription,
|
|
369
|
-
currency: "USD",
|
|
370
|
-
withTrial: true,
|
|
371
|
-
billingDetail: { country: "US", isBusiness: false, state: "CA", postcode: "94105" },
|
|
372
|
-
});
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
### GraphQL — Typed Queries
|
|
376
|
-
|
|
377
|
-
```typescript
|
|
378
|
-
// Simple query
|
|
379
|
-
interface StoresQuery {
|
|
380
|
-
stores: Array<{ id: string; name: string; status: string }>;
|
|
381
|
-
}
|
|
382
|
-
const result = await client.graphql.query<StoresQuery>({
|
|
383
|
-
query: `query { stores { id name status } }`,
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// Query with variables
|
|
387
|
-
const product = await client.graphql.query({
|
|
388
|
-
query: `query ($id: ID!) { onetimeProduct(id: $id) { id name prices } }`,
|
|
389
|
-
variables: { id: "prod_xxx" },
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// Nested relationships in a single request
|
|
393
|
-
const detail = await client.graphql.query({
|
|
394
|
-
query: `query ($id: ID!) {
|
|
395
|
-
store(id: $id) {
|
|
396
|
-
id name
|
|
397
|
-
onetimeProducts { id name status prices }
|
|
398
|
-
subscriptionProducts { id name billingPeriod status }
|
|
399
|
-
}
|
|
400
|
-
}`,
|
|
401
|
-
variables: { id: "store_xxx" },
|
|
402
|
-
});
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
See [GraphQL Guide](docs/graphql-guide.md) for introspection, filters, pagination, and more examples.
|
|
406
|
-
|
|
407
|
-
## Webhook Verification
|
|
408
|
-
|
|
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)
|
|
412
|
-
|
|
413
|
-
```typescript
|
|
414
|
-
import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts";
|
|
415
|
-
|
|
416
|
-
// Express (IMPORTANT: use raw body — parsed JSON breaks signature verification)
|
|
417
|
-
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
|
|
418
|
-
try {
|
|
419
|
-
const event = verifyWebhook(
|
|
420
|
-
req.body.toString("utf-8"),
|
|
421
|
-
req.headers["x-waffo-signature"] as string,
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
// Respond immediately, process asynchronously
|
|
425
|
-
res.status(200).send("OK");
|
|
426
|
-
|
|
427
|
-
// Use event.id for idempotent deduplication
|
|
428
|
-
switch (event.eventType) {
|
|
429
|
-
case WebhookEventType.OrderCompleted:
|
|
430
|
-
console.log(`Order ${event.data.orderId} completed`);
|
|
431
|
-
break;
|
|
432
|
-
case WebhookEventType.SubscriptionActivated:
|
|
433
|
-
console.log(`Subscription activated for ${event.data.buyerEmail}`);
|
|
434
|
-
break;
|
|
435
|
-
case WebhookEventType.SubscriptionCanceled:
|
|
436
|
-
console.log(`Subscription canceled: ${event.data.orderId}`);
|
|
437
|
-
break;
|
|
438
|
-
case WebhookEventType.RefundSucceeded:
|
|
439
|
-
console.log(`Refund ${event.data.amount} ${event.data.currency}`);
|
|
440
|
-
break;
|
|
441
|
-
}
|
|
442
|
-
} catch {
|
|
443
|
-
res.status(401).send("Invalid signature");
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
// Next.js App Router
|
|
448
|
-
export async function POST(request: Request) {
|
|
449
|
-
const body = await request.text();
|
|
450
|
-
const sig = request.headers.get("x-waffo-signature");
|
|
451
|
-
try {
|
|
452
|
-
const event = verifyWebhook(body, sig);
|
|
453
|
-
// handle event ...
|
|
454
|
-
return new Response("OK");
|
|
455
|
-
} catch {
|
|
456
|
-
return new Response("Invalid signature", { status: 401 });
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Options: specify environment, disable/customize replay protection
|
|
461
|
-
const event = verifyWebhook(body, sig, { environment: "prod" });
|
|
462
|
-
const event = verifyWebhook(body, sig, { toleranceMs: 0 }); // disable replay check
|
|
463
|
-
```
|
|
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
|
-
|
|
491
|
-
See [Webhook Guide](docs/webhook-guide.md) for all 10 event types, signature algorithm, and best practices.
|
|
492
|
-
|
|
493
339
|
## Error Handling
|
|
494
340
|
|
|
495
341
|
API errors throw `WaffoPancakeError` with the HTTP status code and a call-stack-ordered errors array.
|
|
@@ -508,25 +354,44 @@ try {
|
|
|
508
354
|
}
|
|
509
355
|
```
|
|
510
356
|
|
|
357
|
+
## Resources
|
|
358
|
+
|
|
359
|
+
| Namespace | Methods | Description |
|
|
360
|
+
|-----------|---------|-------------|
|
|
361
|
+
| `client.checkout.authenticated` | `create()` | Authenticated checkout (recommended) |
|
|
362
|
+
| `client.checkout.anonymous` | `create()` | Anonymous checkout |
|
|
363
|
+
| `client.checkout` | `createSession()` | Low-level checkout session |
|
|
364
|
+
| `client.webhooks` | `verify<T>()` | Webhook signature verification |
|
|
365
|
+
| `client.graphql` | `query<T>()` | Typed GraphQL queries |
|
|
366
|
+
| `client.auth` | `issueSessionToken()` | Issue a buyer session token (JWT) |
|
|
367
|
+
| `client.stores` | `create()` `update()` `delete()` | Store management |
|
|
368
|
+
| `client.storeMerchants` | `add()` `remove()` `updateRole()` | Store members (coming soon) |
|
|
369
|
+
| `client.onetimeProducts` | `create()` `update()` `publish()` `updateStatus()` | One-time products |
|
|
370
|
+
| `client.subscriptionProducts` | `create()` `update()` `publish()` `updateStatus()` | Subscription products |
|
|
371
|
+
| `client.subscriptionProductGroups` | `create()` `update()` `delete()` `publish()` | Product groups |
|
|
372
|
+
| `client.orders` | `cancelSubscription()` | Order management |
|
|
373
|
+
|
|
374
|
+
## Documentation
|
|
375
|
+
|
|
376
|
+
| Document | Content |
|
|
377
|
+
|----------|---------|
|
|
378
|
+
| [API Reference](docs/api-reference.md) | Complete method reference — parameters, return types, `BillingDetail` fields |
|
|
379
|
+
| [GraphQL Guide](docs/graphql-guide.md) | Queries, filters, analytics, introspection, delivery logs |
|
|
380
|
+
| [Webhook Guide](docs/webhook-guide.md) | Signature verification, event types, key resolution, retry mechanism |
|
|
381
|
+
| [Changelog](CHANGELOG.md) | Version history and migration guides |
|
|
382
|
+
|
|
511
383
|
## Exports
|
|
512
384
|
|
|
513
|
-
### Classes
|
|
385
|
+
### Classes & Functions
|
|
514
386
|
|
|
515
387
|
| Export | Description |
|
|
516
388
|
|--------|-------------|
|
|
517
389
|
| `WaffoPancake` | SDK client with auto-signed requests |
|
|
518
390
|
| `WaffoPancakeError` | API error with status and call-stack errors |
|
|
519
|
-
|
|
520
|
-
### Functions
|
|
521
|
-
|
|
522
|
-
| Export | Description |
|
|
523
|
-
|--------|-------------|
|
|
524
|
-
| `verifyWebhook` | RSA-SHA256 webhook signature verification |
|
|
391
|
+
| `verifyWebhook` | Standalone webhook signature verification |
|
|
525
392
|
|
|
526
393
|
### Enums
|
|
527
394
|
|
|
528
|
-
Runtime-accessible values. Both `Enum.Value` and string literal syntax are supported.
|
|
529
|
-
|
|
530
395
|
| Export | Values |
|
|
531
396
|
|--------|--------|
|
|
532
397
|
| `Environment` | `Test`, `Prod` |
|
|
@@ -536,18 +401,18 @@ Runtime-accessible values. Both `Enum.Value` and string literal syntax are suppo
|
|
|
536
401
|
| `EntityStatus` | `Active`, `Inactive`, `Suspended` |
|
|
537
402
|
| `StoreRole` | `Owner`, `Admin`, `Member` |
|
|
538
403
|
| `OnetimeOrderStatus` | `Pending`, `Completed`, `Canceled` |
|
|
539
|
-
| `SubscriptionOrderStatus` | `Pending`, `Active`, `Canceling`, `
|
|
404
|
+
| `SubscriptionOrderStatus` | `Pending`, `Active`, `Canceling`, `PastDue`, `Closed`, `Canceled`, `Expired` |
|
|
540
405
|
| `PaymentStatus` | `Pending`, `Succeeded`, `Failed`, `Canceled` |
|
|
541
406
|
| `RefundTicketStatus` | `Pending`, `Approved`, `Rejected`, `Processing`, `Succeeded`, `Failed` |
|
|
542
407
|
| `RefundStatus` | `Succeeded`, `Failed` |
|
|
543
408
|
| `MediaType` | `Image`, `Video` |
|
|
544
409
|
| `CheckoutSessionProductType` | `Onetime`, `Subscription` |
|
|
545
|
-
| `ErrorLayer` | `Gateway`, `User`, `Store`, `Product`, `Order`, `GraphQL`, `Resource`, `Email` |
|
|
410
|
+
| `ErrorLayer` | `Gateway`, `User`, `Store`, `Product`, `Order`, `Ticket`, `GraphQL`, `Resource`, `Email` |
|
|
546
411
|
| `WebhookEventType` | `OrderCompleted`, `SubscriptionActivated`, `SubscriptionPaymentSucceeded`, `SubscriptionCanceling`, `SubscriptionUncanceled`, `SubscriptionUpdated`, `SubscriptionCanceled`, `SubscriptionPastDue`, `RefundSucceeded`, `RefundFailed` |
|
|
547
412
|
|
|
548
413
|
### Types
|
|
549
414
|
|
|
550
|
-
See [API Reference
|
|
415
|
+
Key types: `WaffoPancakeConfig`, `AuthenticatedCheckoutParams`, `AuthenticatedCheckoutResult`, `AnonymousCheckoutParams`, `CheckoutSessionResult`, `Store`, `OnetimeProductDetail`, `SubscriptionProductDetail`, `WebhookEvent<T>`, `GraphQLResponse<T>`, and 30+ more. See [API Reference](docs/api-reference.md#types) for the full list.
|
|
551
416
|
|
|
552
417
|
## Development
|
|
553
418
|
|
|
@@ -556,7 +421,7 @@ npm run lint # ESLint 9 (TypeScript ESLint + import order + JSDoc)
|
|
|
556
421
|
npm run test # Vitest
|
|
557
422
|
npm run test:watch # Vitest in watch mode
|
|
558
423
|
npm run test:coverage # Vitest with v8 coverage
|
|
559
|
-
npm run build #
|
|
424
|
+
npm run build # tsup → ESM + CJS + DTS
|
|
560
425
|
```
|
|
561
426
|
|
|
562
427
|
## Project Structure
|
|
@@ -580,11 +445,13 @@ src/
|
|
|
580
445
|
├── subscription-product-groups.ts
|
|
581
446
|
├── orders.ts
|
|
582
447
|
├── checkout.ts
|
|
448
|
+
├── checkout-anonymous.ts
|
|
449
|
+
├── checkout-authenticated.ts
|
|
583
450
|
├── graphql.ts
|
|
584
451
|
└── webhooks.ts
|
|
585
452
|
docs/
|
|
586
453
|
├── api-reference.md # Complete API reference
|
|
587
|
-
├── graphql-guide.md # GraphQL
|
|
454
|
+
├── graphql-guide.md # GraphQL queries & analytics
|
|
588
455
|
└── webhook-guide.md # Webhook verification guide
|
|
589
456
|
```
|
|
590
457
|
|