@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.
@@ -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.1.7",
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": {