@waffo/pancake-ts 0.1.7 → 0.2.0
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 +32 -2
- package/README.md +282 -364
- package/dist/index.cjs +303 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +502 -53
- package/dist/index.d.ts +502 -53
- package/dist/index.js +303 -37
- package/dist/index.js.map +1 -1
- package/docs/api-reference.md +877 -0
- package/docs/graphql-guide.md +664 -0
- package/docs/webhook-guide.md +456 -0
- package/package.json +2 -1
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# Webhook Guide
|
|
2
|
+
|
|
3
|
+
Waffo Pancake sends webhook events to your configured endpoint when payment, subscription, and refund state changes occur. The SDK provides `verifyWebhook()` to validate signatures and parse events.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
- **Algorithm**: RSA-SHA256 with environment-specific key pairs
|
|
8
|
+
- **Dual environment**: Test and production use separate key pairs; the SDK resolves the correct key automatically
|
|
9
|
+
- **Multi-level key loading**: Config parameter → environment variable → built-in hardcoded key
|
|
10
|
+
- **Replay protection**: 5-minute timestamp tolerance by default
|
|
11
|
+
- **Environment auto-detection**: Tries the production key first, falls back to test
|
|
12
|
+
|
|
13
|
+
## Signature Verification
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
1. Parse X-Waffo-Signature header → t (timestamp) + v1 (Base64 signature)
|
|
17
|
+
2. Build signature input: `${t}.${rawBody}`
|
|
18
|
+
3. Verify v1 with RSA-SHA256 using the Waffo public key
|
|
19
|
+
4. Check timestamp (default 5-minute tolerance to prevent replay attacks)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Express
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts";
|
|
28
|
+
|
|
29
|
+
// IMPORTANT: Use raw body — parsed JSON will break signature verification
|
|
30
|
+
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const event = verifyWebhook(
|
|
33
|
+
req.body.toString("utf-8"),
|
|
34
|
+
req.headers["x-waffo-signature"] as string,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Respond immediately, process asynchronously
|
|
38
|
+
res.status(200).send("OK");
|
|
39
|
+
|
|
40
|
+
// Use event.id for idempotent deduplication
|
|
41
|
+
switch (event.eventType) {
|
|
42
|
+
case WebhookEventType.OrderCompleted:
|
|
43
|
+
console.log(`Order ${event.data.orderId} completed`);
|
|
44
|
+
break;
|
|
45
|
+
case WebhookEventType.SubscriptionActivated:
|
|
46
|
+
console.log(`Subscription activated for ${event.data.buyerEmail}`);
|
|
47
|
+
break;
|
|
48
|
+
case WebhookEventType.SubscriptionCanceled:
|
|
49
|
+
console.log(`Subscription canceled: ${event.data.orderId}`);
|
|
50
|
+
break;
|
|
51
|
+
case WebhookEventType.RefundSucceeded:
|
|
52
|
+
console.log(`Refund ${event.data.amount} ${event.data.currency}`);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
res.status(401).send("Invalid signature");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Next.js App Router
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { verifyWebhook } from "@waffo/pancake-ts";
|
|
65
|
+
|
|
66
|
+
export async function POST(request: Request) {
|
|
67
|
+
const body = await request.text();
|
|
68
|
+
const sig = request.headers.get("x-waffo-signature");
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const event = verifyWebhook(body, sig);
|
|
72
|
+
// handle event ...
|
|
73
|
+
return new Response("OK");
|
|
74
|
+
} catch {
|
|
75
|
+
return new Response("Invalid signature", { status: 401 });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Options
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// Specify environment explicitly (skip auto-detection)
|
|
84
|
+
const event = verifyWebhook(body, sig, { environment: "prod" });
|
|
85
|
+
|
|
86
|
+
// Disable replay protection (useful for testing)
|
|
87
|
+
const event = verifyWebhook(body, sig, { toleranceMs: 0 });
|
|
88
|
+
|
|
89
|
+
// Custom tolerance window (10 minutes)
|
|
90
|
+
const event = verifyWebhook(body, sig, { toleranceMs: 600000 });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Parameters
|
|
94
|
+
|
|
95
|
+
| Parameter | Type | Description |
|
|
96
|
+
|-----------|------|-------------|
|
|
97
|
+
| `payload` | `string` | Raw request body string (must be unparsed) |
|
|
98
|
+
| `signatureHeader` | `string \| undefined \| null` | `X-Waffo-Signature` header value (format: `t=<timestamp>,v1=<signature>`) |
|
|
99
|
+
| `options` | `VerifyWebhookOptions` | Optional configuration |
|
|
100
|
+
|
|
101
|
+
### `VerifyWebhookOptions`
|
|
102
|
+
|
|
103
|
+
| Field | Type | Default | Description |
|
|
104
|
+
|-------|------|---------|-------------|
|
|
105
|
+
| `environment` | `"test" \| "prod"` | auto-detect | Which environment's key to resolve. When omitted, tries prod first, then test. Ignored when `publicKey` is set. |
|
|
106
|
+
| `toleranceMs` | `number` | `300000` (5 min) | Timestamp tolerance in ms. Set to `0` to skip timestamp check |
|
|
107
|
+
| `publicKey` | `string` | — | Per-call public key override (highest priority, skips all resolution) |
|
|
108
|
+
| `publicKeys` | `string \| { test?, prod? }` | — | Config-level key(s) for the resolution chain. Typically injected automatically by `client.webhooks.verify()` |
|
|
109
|
+
|
|
110
|
+
## Dual-Environment Public Key Architecture
|
|
111
|
+
|
|
112
|
+
Waffo Pancake uses **separate RSA key pairs** for test and production environments. Webhook events from test mode are signed with the test private key; production events are signed with the production private key. The SDK must use the corresponding public key to verify each event.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
┌──────────────────┐
|
|
116
|
+
│ Waffo Server │
|
|
117
|
+
├──────────────────┤
|
|
118
|
+
│ Test Private Key │──sign──→ test webhook events
|
|
119
|
+
│ Prod Private Key │──sign──→ prod webhook events
|
|
120
|
+
└──────────────────┘
|
|
121
|
+
│
|
|
122
|
+
▼
|
|
123
|
+
┌──────────────────┐
|
|
124
|
+
│ Your Server │
|
|
125
|
+
│ (SDK verify) │
|
|
126
|
+
├──────────────────┤
|
|
127
|
+
│ Test Public Key │──verify──→ test webhook events
|
|
128
|
+
│ Prod Public Key │──verify──→ prod webhook events
|
|
129
|
+
└──────────────────┘
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
When `environment` is specified, the SDK uses only the key for that environment. When omitted, the SDK **auto-detects** by trying the production key first, then falling back to the test key.
|
|
133
|
+
|
|
134
|
+
### Why Dual Keys?
|
|
135
|
+
|
|
136
|
+
- **Isolation**: Test and production environments are cryptographically separated. A test key cannot verify a production event and vice versa.
|
|
137
|
+
- **Key rotation**: Keys can be rotated independently per environment without affecting the other.
|
|
138
|
+
- **Security boundary**: Even if a test private key is compromised, production webhook integrity is unaffected.
|
|
139
|
+
|
|
140
|
+
## Multi-Level Public Key Resolution
|
|
141
|
+
|
|
142
|
+
For each environment, the SDK resolves the public key by walking a **6-level fallback chain**. The first non-empty value wins:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
┌─────────────────────────────────────────────────────────┐
|
|
146
|
+
│ Resolution Chain │
|
|
147
|
+
│ (per environment: test/prod) │
|
|
148
|
+
├─────┬───────────────────────────────────────────────────┤
|
|
149
|
+
│ 1 │ options.publicKey (per-call override) │ ← highest priority
|
|
150
|
+
├─────┼───────────────────────────────────────────────────┤
|
|
151
|
+
│ 2 │ config.webhookPublicKey[env] │
|
|
152
|
+
│ │ (WaffoPancakeConfig per-env object key) │
|
|
153
|
+
├─────┼───────────────────────────────────────────────────┤
|
|
154
|
+
│ 3 │ config.webhookPublicKey (string) │
|
|
155
|
+
│ │ (WaffoPancakeConfig shared key) │
|
|
156
|
+
├─────┼───────────────────────────────────────────────────┤
|
|
157
|
+
│ 4 │ WAFFO_WEBHOOK_TEST_PUBLIC_KEY (test) │
|
|
158
|
+
│ │ WAFFO_WEBHOOK_PROD_PUBLIC_KEY (prod) │
|
|
159
|
+
│ │ (environment variable, per-env) │
|
|
160
|
+
├─────┼───────────────────────────────────────────────────┤
|
|
161
|
+
│ 5 │ WAFFO_WEBHOOK_PUBLIC_KEY │
|
|
162
|
+
│ │ (environment variable, shared) │
|
|
163
|
+
├─────┼───────────────────────────────────────────────────┤
|
|
164
|
+
│ 6 │ Built-in hardcoded key │ ← default fallback
|
|
165
|
+
│ │ (SDK-embedded Waffo public key) │
|
|
166
|
+
└─────┴───────────────────────────────────────────────────┘
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Level 1 — Per-call Override (`options.publicKey`)
|
|
170
|
+
|
|
171
|
+
The highest priority. When set, the SDK uses this key directly and **skips the entire resolution chain** — config keys, env vars, and built-in keys are all ignored. The `environment` option is also ignored.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Use a specific key for this one call
|
|
175
|
+
const event = verifyWebhook(body, sig, {
|
|
176
|
+
publicKey: "-----BEGIN PUBLIC KEY-----\nMIIBIjAN...",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Or via client instance
|
|
180
|
+
const event = client.webhooks.verify(body, sig, {
|
|
181
|
+
publicKey: rotatedKey,
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Use cases**: Key rotation testing, debugging with a known key, temporary override during migration.
|
|
186
|
+
|
|
187
|
+
### Level 2 — Config Per-Environment Keys (`webhookPublicKey: { test, prod }`)
|
|
188
|
+
|
|
189
|
+
Pass an object with `test` and/or `prod` fields to `WaffoPancakeConfig.webhookPublicKey`. The SDK picks the key matching the resolved environment.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
const client = new WaffoPancake({
|
|
193
|
+
merchantId: "MER_xxx",
|
|
194
|
+
privateKey: "...",
|
|
195
|
+
webhookPublicKey: {
|
|
196
|
+
test: process.env.MY_TEST_PUB_KEY!,
|
|
197
|
+
prod: process.env.MY_PROD_PUB_KEY!,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Uses test key
|
|
202
|
+
client.webhooks.verify(body, sig, { environment: "test" });
|
|
203
|
+
|
|
204
|
+
// Uses prod key
|
|
205
|
+
client.webhooks.verify(body, sig, { environment: "prod" });
|
|
206
|
+
|
|
207
|
+
// Auto-detect: tries prod first, then test
|
|
208
|
+
client.webhooks.verify(body, sig);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
You can provide only one environment — the other falls through to env vars or built-in keys:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
webhookPublicKey: {
|
|
215
|
+
prod: customProdKey,
|
|
216
|
+
// test: not set → falls through to env var → built-in test key
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Level 3 — Config Shared Key (`webhookPublicKey: string`)
|
|
221
|
+
|
|
222
|
+
A single string key applies to **both** environments. Useful when you use the same key pair for test and production (e.g., self-hosted deployments).
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const client = new WaffoPancake({
|
|
226
|
+
merchantId: "MER_xxx",
|
|
227
|
+
privateKey: "...",
|
|
228
|
+
webhookPublicKey: process.env.WAFFO_PUB_KEY!, // used for both test and prod
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Level 4 — Environment Variables (Per-Environment) ⭐ Recommended Migration Path
|
|
233
|
+
|
|
234
|
+
When no config key is found, the SDK reads from process environment variables:
|
|
235
|
+
|
|
236
|
+
| Environment | Variable Name |
|
|
237
|
+
|-------------|---------------|
|
|
238
|
+
| test | `WAFFO_WEBHOOK_TEST_PUBLIC_KEY` |
|
|
239
|
+
| prod | `WAFFO_WEBHOOK_PROD_PUBLIC_KEY` |
|
|
240
|
+
|
|
241
|
+
> **When built-in hardcoded keys become invalid (e.g., Waffo rotates platform keys, or you migrate to a self-hosted deployment), the minimum-effort fix is to set environment variables. No code changes, no redeployment of application code — just update the env vars in your hosting platform (Vercel, AWS, Docker, etc.) and the SDK picks them up automatically on the next request.**
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# .env, Vercel dashboard, AWS Parameter Store, Docker env, etc.
|
|
245
|
+
WAFFO_WEBHOOK_TEST_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjAN..."
|
|
246
|
+
WAFFO_WEBHOOK_PROD_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjAN..."
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// No code changes needed — same code as before
|
|
251
|
+
const event = verifyWebhook(body, sig);
|
|
252
|
+
// SDK auto-reads from env vars when built-in keys fail to match
|
|
253
|
+
|
|
254
|
+
// Or via client — also zero code change
|
|
255
|
+
const client = new WaffoPancake({
|
|
256
|
+
merchantId: "MER_xxx",
|
|
257
|
+
privateKey: "...",
|
|
258
|
+
// No webhookPublicKey needed — env vars take effect automatically
|
|
259
|
+
});
|
|
260
|
+
client.webhooks.verify(body, sig, { environment: "prod" });
|
|
261
|
+
// → reads WAFFO_WEBHOOK_PROD_PUBLIC_KEY
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Migration checklist when hardcoded keys expire:**
|
|
265
|
+
|
|
266
|
+
1. Obtain the new public keys from the Waffo dashboard or your platform admin
|
|
267
|
+
2. Set `WAFFO_WEBHOOK_PROD_PUBLIC_KEY` (and `WAFFO_WEBHOOK_TEST_PUBLIC_KEY` if needed) in your environment
|
|
268
|
+
3. Done — no code changes, no package upgrade, no redeployment of application code
|
|
269
|
+
|
|
270
|
+
### Level 5 — Environment Variable (Shared)
|
|
271
|
+
|
|
272
|
+
A single env var for both environments:
|
|
273
|
+
|
|
274
|
+
| Variable Name | Used for |
|
|
275
|
+
|---------------|----------|
|
|
276
|
+
| `WAFFO_WEBHOOK_PUBLIC_KEY` | Both test and prod |
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
WAFFO_WEBHOOK_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjAN..."
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Level 6 — Built-in Hardcoded Keys (Default)
|
|
283
|
+
|
|
284
|
+
If no custom key is found at any level, the SDK uses its embedded Waffo public keys. These are the official Waffo Pancake platform keys and are the default for most users.
|
|
285
|
+
|
|
286
|
+
**No configuration required** — this is the zero-config default.
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// Simplest usage — built-in keys handle everything
|
|
290
|
+
const event = verifyWebhook(body, sig);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Resolution Examples
|
|
294
|
+
|
|
295
|
+
| Scenario | Config | Env Var | Result (prod) |
|
|
296
|
+
|----------|--------|---------|----------------|
|
|
297
|
+
| Default (no config) | — | — | Built-in prod key |
|
|
298
|
+
| Shared config key | `webhookPublicKey: "KEY_A"` | — | `KEY_A` |
|
|
299
|
+
| Per-env config | `webhookPublicKey: { prod: "KEY_B" }` | — | `KEY_B` |
|
|
300
|
+
| Env var only | — | `WAFFO_WEBHOOK_PROD_PUBLIC_KEY=KEY_C` | `KEY_C` |
|
|
301
|
+
| Config + env var | `webhookPublicKey: { prod: "KEY_D" }` | `WAFFO_WEBHOOK_PROD_PUBLIC_KEY=KEY_E` | `KEY_D` (config wins) |
|
|
302
|
+
| Per-call override | `webhookPublicKey: { prod: "KEY_F" }` | — | `options.publicKey` wins |
|
|
303
|
+
|
|
304
|
+
## Public Key Formats
|
|
305
|
+
|
|
306
|
+
All public key inputs at every level (config, env vars, per-call) are automatically **normalized** by the SDK. The following formats are accepted:
|
|
307
|
+
|
|
308
|
+
| Format | Example | Notes |
|
|
309
|
+
|--------|---------|-------|
|
|
310
|
+
| Standard SPKI PEM | `-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----` | Recommended |
|
|
311
|
+
| PKCS#1 PEM | `-----BEGIN RSA PUBLIC KEY-----\n...` | Also accepted |
|
|
312
|
+
| Literal `\n` (env vars) | `"-----BEGIN PUBLIC KEY-----\\nMIIB..."` | Common in `.env` files and CI secrets |
|
|
313
|
+
| Windows line endings | `\r\n` | Converted to `\n` |
|
|
314
|
+
| Raw base64 (no headers) | `MIIBIjANBgkqhki...` | Wrapped with SPKI headers automatically |
|
|
315
|
+
| Single-line base64 | Header + all base64 on one line + footer | Re-wrapped to 64-char lines |
|
|
316
|
+
|
|
317
|
+
Normalization is applied **on every call** — there is no eager validation at construction time (unlike `privateKey`). Invalid keys produce a descriptive error at verification time.
|
|
318
|
+
|
|
319
|
+
## Two Verification APIs
|
|
320
|
+
|
|
321
|
+
### Standalone Function — `verifyWebhook()`
|
|
322
|
+
|
|
323
|
+
Best for simple setups where you don't need the SDK client. Uses env vars and built-in keys by default.
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { verifyWebhook } from "@waffo/pancake-ts";
|
|
327
|
+
|
|
328
|
+
const event = verifyWebhook(body, sig); // built-in keys
|
|
329
|
+
const event = verifyWebhook(body, sig, { environment: "prod" }); // explicit env
|
|
330
|
+
const event = verifyWebhook(body, sig, { publicKey: customKey }); // per-call key
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Client Instance Method — `client.webhooks.verify()`
|
|
334
|
+
|
|
335
|
+
Best when you already have a `WaffoPancake` client. Automatically injects config-level keys into the resolution chain.
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
import { WaffoPancake } from "@waffo/pancake-ts";
|
|
339
|
+
|
|
340
|
+
const client = new WaffoPancake({
|
|
341
|
+
merchantId: "...",
|
|
342
|
+
privateKey: "...",
|
|
343
|
+
webhookPublicKey: {
|
|
344
|
+
test: testKey,
|
|
345
|
+
prod: prodKey,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const event = client.webhooks.verify(body, sig); // auto-detect with config keys
|
|
350
|
+
const event = client.webhooks.verify(body, sig, { environment: "test" }); // explicit env
|
|
351
|
+
const event = client.webhooks.verify(body, sig, { publicKey: oneOff }); // per-call override
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Both APIs share the same underlying verification logic and resolution chain.
|
|
355
|
+
|
|
356
|
+
## Event Payload
|
|
357
|
+
|
|
358
|
+
### `WebhookEvent<T>`
|
|
359
|
+
|
|
360
|
+
| Field | Type | Description |
|
|
361
|
+
|-------|------|-------------|
|
|
362
|
+
| `id` | `string` | Delivery record unique ID (UUID) — use for idempotent deduplication |
|
|
363
|
+
| `timestamp` | `string` | Event timestamp (ISO 8601 UTC) |
|
|
364
|
+
| `eventType` | `string` | Event type (e.g. `"order.completed"`) |
|
|
365
|
+
| `eventId` | `string` | Business event ID (e.g. payment ID) |
|
|
366
|
+
| `storeId` | `string` | Store ID the event belongs to |
|
|
367
|
+
| `mode` | `string` | Environment (`"test"` or `"prod"`) |
|
|
368
|
+
| `data` | `T` | Event data (defaults to `WebhookEventData`) |
|
|
369
|
+
|
|
370
|
+
### `WebhookEventData`
|
|
371
|
+
|
|
372
|
+
| Field | Type | Description |
|
|
373
|
+
|-------|------|-------------|
|
|
374
|
+
| `orderId` | `string` | Associated order ID |
|
|
375
|
+
| `buyerEmail` | `string` | Buyer email address |
|
|
376
|
+
| `currency` | `string` | Currency code (ISO 4217) |
|
|
377
|
+
| `amount` | `string` | Amount in display format (e.g., `"29.00"` for $29.00 USD, `"4500"` for ¥4500 JPY) |
|
|
378
|
+
| `taxAmount` | `string` | Tax amount in display format (e.g., `"2.90"`) |
|
|
379
|
+
| `productName` | `string` | Product name |
|
|
380
|
+
|
|
381
|
+
## Event Types
|
|
382
|
+
|
|
383
|
+
| Enum Value | String | Trigger |
|
|
384
|
+
|------------|--------|---------|
|
|
385
|
+
| `OrderCompleted` | `order.completed` | One-time order first payment succeeded |
|
|
386
|
+
| `SubscriptionActivated` | `subscription.activated` | New subscription activated |
|
|
387
|
+
| `SubscriptionPaymentSucceeded` | `subscription.payment_succeeded` | Subscription renewal payment succeeded |
|
|
388
|
+
| `SubscriptionCanceling` | `subscription.canceling` | Buyer initiated cancellation (expires at end of billing period) |
|
|
389
|
+
| `SubscriptionUncanceled` | `subscription.uncanceled` | Buyer withdrew cancellation request |
|
|
390
|
+
| `SubscriptionUpdated` | `subscription.updated` | Subscription product changed (upgrade/downgrade) |
|
|
391
|
+
| `SubscriptionCanceled` | `subscription.canceled` | Subscription fully terminated |
|
|
392
|
+
| `SubscriptionPastDue` | `subscription.past_due` | Renewal payment failed (past due) |
|
|
393
|
+
| `RefundSucceeded` | `refund.succeeded` | Refund completed successfully |
|
|
394
|
+
| `RefundFailed` | `refund.failed` | Refund failed |
|
|
395
|
+
|
|
396
|
+
## Key Rotation & Migration
|
|
397
|
+
|
|
398
|
+
### Scenario: Built-in hardcoded keys are no longer valid
|
|
399
|
+
|
|
400
|
+
This can happen when Waffo rotates its platform key pair, or when you switch to a self-hosted deployment with custom keys.
|
|
401
|
+
|
|
402
|
+
**Minimum-effort fix — set environment variables (zero code change):**
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
# Just add these to your hosting environment:
|
|
406
|
+
WAFFO_WEBHOOK_PROD_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjAN..."
|
|
407
|
+
WAFFO_WEBHOOK_TEST_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjAN..."
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The SDK automatically checks env vars before falling back to hardcoded keys. Your existing `verifyWebhook(body, sig)` or `client.webhooks.verify(body, sig)` calls continue to work without any code change.
|
|
411
|
+
|
|
412
|
+
### Scenario: Gradual key rotation
|
|
413
|
+
|
|
414
|
+
When rotating keys, the old key remains valid for a transition period:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// During transition: both old and new keys work
|
|
418
|
+
// The SDK auto-detect tries multiple keys, so both will be accepted
|
|
419
|
+
|
|
420
|
+
// After transition: update the env var to the new key
|
|
421
|
+
// Old signed events will fail verification — this is expected
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Choosing the right level
|
|
425
|
+
|
|
426
|
+
| Situation | Recommended Level | Why |
|
|
427
|
+
|-----------|-------------------|-----|
|
|
428
|
+
| Standard Waffo Pancake user | Level 6 (default) | Built-in keys just work, zero config |
|
|
429
|
+
| Built-in keys expired | Level 4 (env var) | No code changes, set env var and done |
|
|
430
|
+
| Self-hosted deployment | Level 2/3 (config) | Custom keys are part of your app config |
|
|
431
|
+
| Testing a new key | Level 1 (per-call) | One-off override, no permanent change |
|
|
432
|
+
| CI/CD with different keys | Level 4 (env var) | Each environment sets its own env var |
|
|
433
|
+
|
|
434
|
+
## Retry Mechanism
|
|
435
|
+
|
|
436
|
+
When delivery fails (non-2xx response or timeout), the system automatically retries using **exponential backoff** (managed by the underlying message queue). Default: 3 retries.
|
|
437
|
+
|
|
438
|
+
| Delivery Status | Description |
|
|
439
|
+
|----------------|-------------|
|
|
440
|
+
| `pending` | Created, waiting for delivery or retrying |
|
|
441
|
+
| `success` | Delivery successful (server returned 2xx) |
|
|
442
|
+
| `failed` | All retries exhausted, final failure |
|
|
443
|
+
|
|
444
|
+
You can view each delivery's status, HTTP status code, and response content in the dashboard's Webhook logs.
|
|
445
|
+
|
|
446
|
+
> **Note**: The same business event (same `eventType` + `eventId`) creates only one delivery record and won't be duplicated. However, the same delivery may arrive multiple times due to retries — always deduplicate using `event.id`.
|
|
447
|
+
|
|
448
|
+
## Best Practices
|
|
449
|
+
|
|
450
|
+
1. **Respond quickly** — Return 200 immediately and process the event asynchronously. Waffo retries on timeout.
|
|
451
|
+
2. **Deduplicate** — Use `event.id` (delivery record UUID) as an idempotency key to handle redeliveries.
|
|
452
|
+
3. **Verify all events** — Always call `verifyWebhook()` before processing. Never trust unverified payloads.
|
|
453
|
+
4. **Use raw body** — The signature is computed over the raw request body. Parsing JSON first will break verification.
|
|
454
|
+
5. **Specify environment when known** — If your endpoint only receives test or prod events, pass `{ environment: "test" }` or `{ environment: "prod" }` to skip unnecessary key attempts and get clearer error messages.
|
|
455
|
+
6. **Use env vars for secrets** — Prefer `WAFFO_WEBHOOK_PROD_PUBLIC_KEY` env vars over hardcoding keys in source code. The SDK reads them automatically.
|
|
456
|
+
7. **Key rotation** — During rotation, temporarily use `options.publicKey` per-call to test the new key, then update config/env vars once confirmed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waffo/pancake-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "TypeScript SDK for Waffo Pancake API (Merchant API Key authentication)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
|
+
"docs",
|
|
23
24
|
"CHANGELOG.md"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|