@waffo/pancake-ts 0.17.0 → 0.19.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 CHANGED
@@ -4,6 +4,41 @@ 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.19.0] - 2026-08-18
8
+
9
+ Subscription products can now charge for the trial period.
10
+
11
+ ### Added
12
+
13
+ - **`PriceInfo.trialAmount`** — trial period price as a display string, subscription products only. Omit it for a free trial. It requires `metadata.trialDays` on the product and must be lower than `amount`; the API rejects either violation with a 400.
14
+ - **`PriceSnapshot`** — the type `CreateCheckoutSessionParams.priceSnapshot` accepts. Same shape as `PriceInfo` without `trialAmount`.
15
+
16
+ ### Changed
17
+
18
+ - **`CreateCheckoutSessionParams.priceSnapshot` is typed `PriceSnapshot` rather than `PriceInfo`.** A session-level override replaces the regular period price; the trial price comes from the product version locked into the session, so a `trialAmount` passed here would be dropped server-side. Existing calls compile unchanged — the fields `PriceSnapshot` declares are exactly the two `PriceInfo` had before `trialAmount` was added.
19
+
20
+ ## [0.18.0] - 2026-08-08
21
+
22
+ Customer sessions never reached the API, and webhook retries were rejected as replays.
23
+
24
+ ### Fixed
25
+
26
+ - **Customer session requests now send `X-Environment`.** A session token carries no environment of its own, so the gateway requires the header next to the Bearer credential and rejects the request with a 400 without it. The header was missing, which made every `client.customer(...)` method unusable: `cancelSubscription`, `cancelOnetimeOrder`, `reactivateSubscription`, `createRefundTicket`, `resubmitRefundTicket`, and `graphql.query`. API Key requests were never affected — the gateway derives their environment from the key.
27
+ - **Webhook verification no longer rejects legitimate retries.** The signature timestamp is stamped once, before the first delivery attempt, and retries reuse the original header — so the last retry of a schedule arrives with a timestamp as old as the schedule itself (observed above 31 minutes). Against the old 5-minute window every late retry failed verification as a suspected replay. `verifyWebhook` now allows timestamps up to **45 minutes** old.
28
+
29
+ ### Added
30
+
31
+ - **`WaffoPancakeConfig.environment`** — `"test"` or `"prod"`, the environment customer sessions operate in.
32
+ - **`CustomerSessionOptions`** — second argument to `client.customer(token, options)`, overriding the config for a single session.
33
+ - **`VerifyWebhookOptions.futureToleranceMs`** — how far in the future a signature timestamp may be, default `60000` (1 minute). Raise it for a receiving server with known clock skew.
34
+
35
+ ### Changed
36
+
37
+ - **`client.customer(token)` requires an environment** from either the config or the per-session options, and throws `WaffoPancakeError` (400, `layer: "sdk"`) when neither supplies one. There is no default — guessing would route the call to the other environment. This turns a request that always failed at the gateway into a local error; no working call changes behavior. Migration: add `environment` to your client config, or pass `client.customer(token, { environment: "test" })`.
38
+ - `client.buyer(token, options)` (deprecated) accepts and forwards the same options.
39
+ - **`VerifyWebhookOptions.toleranceMs` default raised from `300000` to `2700000`, and the window is now asymmetric** — matching the gateway's API Key check, which pairs a wide past-facing window with a tight future-facing one. `toleranceMs` now means "how far in the past"; the future direction is `futureToleranceMs`. `toleranceMs: 0` still disables the check entirely. A captured request stays replayable for longer under the wider window, so keep your handler idempotent on the event `id` — that, not the window, is the real defense.
40
+
41
+
7
42
  ## [0.17.0] - 2026-08-03
8
43
 
9
44
  `supportEmail` and `website` were never applied by the update-store endpoint — passing them was silently ignored.
package/README.md CHANGED
@@ -40,13 +40,14 @@ res.json({ checkoutUrl: result.checkoutUrl });
40
40
 
41
41
  ## Configuration
42
42
 
43
- | Parameter | Type | Required | Description |
44
- | ------------------ | ---------------------------- | -------- | ---------------------------------------------------------------------------------- |
45
- | `merchantId` | `string` | Yes | Merchant ID in `MER_{base62}` format |
46
- | `privateKey` | `string` | Yes | RSA private key in PEM format (auto-normalized, see [docs](docs/api-reference.md)) |
47
- | `baseUrl` | `string` | No | API base URL override |
48
- | `fetch` | `typeof fetch` | No | Custom fetch implementation |
49
- | `webhookPublicKey` | `string \| { test?, prod? }` | No | Custom webhook public key(s) |
43
+ | Parameter | Type | Required | Description |
44
+ | ------------------ | ---------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------- |
45
+ | `merchantId` | `string` | Yes | Merchant ID in `MER_{base62}` format |
46
+ | `privateKey` | `string` | Yes | RSA private key in PEM format (auto-normalized, see [docs](docs/api-reference.md)) |
47
+ | `baseUrl` | `string` | No | API base URL override |
48
+ | `environment` | `"test" \| "prod"` | For customer sessions | Sent as `X-Environment`. No default — override per session with `client.customer(token, { environment })` |
49
+ | `fetch` | `typeof fetch` | No | Custom fetch implementation |
50
+ | `webhookPublicKey` | `string \| { test?, prod? }` | No | Custom webhook public key(s) |
50
51
 
51
52
  The SDK auto-normalizes key formats: standard PEM, PKCS#1, literal `\n` from env vars, raw base64, and Windows line endings are all accepted.
52
53
 
@@ -73,7 +74,7 @@ Waffo supports two checkout modes based on whether the merchant knows the custom
73
74
 
74
75
  Both modes support **dynamic pricing** and **trial control** at checkout time:
75
76
 
76
- - `priceSnapshot` — override the product's stored price with a custom amount (e.g., coupon, volume discount)
77
+ - `priceSnapshot` — override the product's stored price with a custom amount (e.g., coupon, volume discount); for subscription products this replaces the regular period price only
77
78
  - `withTrial` — explicitly enable or disable the trial period for subscriptions (`true` = force trial, `false` = skip trial, omit = use default rules)
78
79
 
79
80
  ### Authenticated Checkout (Recommended)
@@ -232,7 +233,7 @@ See [Webhook Guide](docs/webhook-guide.md) for event types, `WebhookEventData` f
232
233
 
233
234
  Beyond checkout, you can let customers manage their own orders and subscriptions — for example, embedding a "Cancel Subscription" or "Request Refund" button in your site.
234
235
 
235
- Issue a session token, then use `client.customer(token)` to get a session with self-service methods:
236
+ Issue a session token, then use `client.customer(token, options?)` to get a session with self-service methods:
236
237
 
237
238
  ```typescript
238
239
  // Your backend — issue a session token for the customer
@@ -241,8 +242,9 @@ const { token } = await client.auth.issueSessionToken({
241
242
  buyerIdentity: req.user.email,
242
243
  });
243
244
 
244
- // Create a customer session
245
- const customer = client.customer(token);
245
+ // Create a customer session — environment is required here (or on the client config),
246
+ // because a session token carries none of its own
247
+ const customer = client.customer(token, { environment: "test" });
246
248
 
247
249
  // Cancel a subscription
248
250
  const { orderId, status } = await customer.cancelSubscription({ orderId: "ORD_xxx" });
@@ -545,7 +547,7 @@ try {
545
547
  | `client.checkout.authenticated` | `create()` | Authenticated checkout (recommended) |
546
548
  | `client.checkout.anonymous` | `create()` | Anonymous checkout |
547
549
  | `client.checkout` | `createSession()` | Low-level checkout session |
548
- | `client.customer(token)` | `cancelSubscription()` `cancelOnetimeOrder()` `reactivateSubscription()` `createRefundTicket()` `resubmitRefundTicket()` | Customer self-service |
550
+ | `client.customer(token, opts?)` | `cancelSubscription()` `cancelOnetimeOrder()` `reactivateSubscription()` `createRefundTicket()` `resubmitRefundTicket()` | Customer self-service |
549
551
  | `client.customer(token).graphql` | `query<T>()` | Customer-scoped GraphQL queries |
550
552
  | `client.webhooks` | `verify<T>()` `add()` `update()` `remove()` | Webhook config + signature verification |
551
553
  | `client.graphql` | `query<T>()` | Merchant GraphQL queries |
package/dist/index.cjs CHANGED
@@ -62,16 +62,21 @@ var WaffoPancakeError = class extends Error {
62
62
  var DEFAULT_BASE_URL = "https://api.waffo.ai";
63
63
  var CustomerHttpClient = class {
64
64
  token;
65
+ environment;
65
66
  baseUrl;
66
67
  _fetch;
67
- constructor(token, config) {
68
+ constructor(token, environment, config) {
68
69
  this.token = token;
70
+ this.environment = environment;
69
71
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
70
72
  this._fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
71
73
  }
72
74
  /**
73
75
  * Send a Bearer-authenticated POST and return the full envelope plus HTTP status.
74
76
  *
77
+ * Sends `Authorization: Bearer <token>` and `X-Environment` — the gateway
78
+ * requires both to accept a session token.
79
+ *
75
80
  * Does NOT throw on `errors[]` or non-2xx status — caller inspects the result.
76
81
  * Throws {@link WaffoPancakeError} only when the response body is not valid JSON.
77
82
  */
@@ -80,7 +85,8 @@ var CustomerHttpClient = class {
80
85
  method: "POST",
81
86
  headers: {
82
87
  "Content-Type": "application/json",
83
- Authorization: `Bearer ${this.token}`
88
+ Authorization: `Bearer ${this.token}`,
89
+ "X-Environment": this.environment
84
90
  },
85
91
  body: JSON.stringify(body)
86
92
  });
@@ -1120,7 +1126,8 @@ var SubscriptionProductsResource = class {
1120
1126
 
1121
1127
  // src/webhooks.ts
1122
1128
  var import_node_crypto3 = require("crypto");
1123
- var DEFAULT_TOLERANCE_MS = 5 * 60 * 1e3;
1129
+ var DEFAULT_TOLERANCE_MS = 45 * 60 * 1e3;
1130
+ var DEFAULT_FUTURE_TOLERANCE_MS = 60 * 1e3;
1124
1131
  var TEST_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
1125
1132
  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxnmRY6yMMA3lVqmAU6ZG
1126
1133
  b1sjL/+r/z6E+ZjkXaDAKiqOhk9rpazni0bNsGXwmftTPk9jy2wn+j6JHODD/WH/
@@ -1188,7 +1195,9 @@ function verifyWebhook(payload, signatureHeader, options) {
1188
1195
  if (Number.isNaN(timestampMs)) {
1189
1196
  throw new Error("Invalid timestamp in X-Waffo-Signature header");
1190
1197
  }
1191
- if (Math.abs(Date.now() - timestampMs) > toleranceMs) {
1198
+ const futureToleranceMs = options?.futureToleranceMs ?? DEFAULT_FUTURE_TOLERANCE_MS;
1199
+ const ageMs = Date.now() - timestampMs;
1200
+ if (ageMs > toleranceMs || ageMs < -futureToleranceMs) {
1192
1201
  throw new Error("Webhook timestamp outside tolerance window (possible replay attack)");
1193
1202
  }
1194
1203
  }
@@ -1381,19 +1390,40 @@ var WaffoPancake = class {
1381
1390
  * methods for order cancellation, subscription management, refund tickets,
1382
1391
  * and scoped GraphQL queries.
1383
1392
  *
1393
+ * Session tokens expire 5 minutes after issuance, so issue one right before
1394
+ * use rather than caching it.
1395
+ *
1384
1396
  * @param token - Session token from `client.auth.issueSessionToken()`
1397
+ * @param options - Per-session overrides
1385
1398
  * @returns A customer session with self-service methods
1399
+ * @throws {WaffoPancakeError} When no environment is available from either
1400
+ * `options.environment` or `WaffoPancakeConfig.environment`
1386
1401
  *
1387
1402
  * @example
1388
1403
  * const { token } = await client.auth.issueSessionToken({
1389
1404
  * storeId: "STO_xxx",
1390
1405
  * buyerIdentity: "customer@example.com",
1391
1406
  * });
1392
- * const customer = client.customer(token);
1407
+ * const customer = client.customer(token, { environment: "test" });
1393
1408
  * await customer.cancelSubscription({ orderId: "ORD_xxx" });
1409
+ *
1410
+ * @example
1411
+ * // Set the environment once on the client instead
1412
+ * const client = new WaffoPancake({ merchantId, privateKey, environment: "test" });
1413
+ * const customer = client.customer(token);
1394
1414
  */
1395
- customer(token) {
1396
- const customerHttp = new CustomerHttpClient(token, {
1415
+ customer(token, options) {
1416
+ const environment = options?.environment ?? this.config.environment;
1417
+ if (environment === void 0) {
1418
+ throw new WaffoPancakeError(400, [
1419
+ {
1420
+ message: "Missing required field: environment \u2014 set it on the client config or pass client.customer(token, { environment: 'test' | 'prod' })",
1421
+ layer: "sdk"
1422
+ }
1423
+ ]);
1424
+ }
1425
+ validateEnum("environment", environment, ["test", "prod"]);
1426
+ const customerHttp = new CustomerHttpClient(token, environment, {
1397
1427
  baseUrl: this.config.baseUrl,
1398
1428
  fetch: this.config.fetch
1399
1429
  });
@@ -1403,6 +1433,7 @@ var WaffoPancake = class {
1403
1433
  * Create a customer session for self-service operations.
1404
1434
  *
1405
1435
  * @param token - Session token from `client.auth.issueSessionToken()`
1436
+ * @param options - Per-session overrides
1406
1437
  * @returns A customer session with self-service methods
1407
1438
  *
1408
1439
  * @example
@@ -1412,8 +1443,8 @@ var WaffoPancake = class {
1412
1443
  *
1413
1444
  * @deprecated Use {@link WaffoPancake.customer} instead.
1414
1445
  */
1415
- buyer(token) {
1416
- return this.customer(token);
1446
+ buyer(token, options) {
1447
+ return this.customer(token, options);
1417
1448
  }
1418
1449
  };
1419
1450