@piprail/sdk 1.20.0 → 1.20.1

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,28 @@ All notable changes to `@piprail/sdk` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  versions follow [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.20.1] — 2026-06-11 — gate replay store: bounded + exception-safe
8
+
9
+ Patch — internal robustness on the gate's built-in replay protection. No API change, no visible
10
+ behaviour change, defaults identical.
11
+
12
+ ### Fixed
13
+ - **Bounded the default used-proof set.** It's now evicted past the replay window
14
+ (`maxTimeoutSeconds`) instead of growing for the life of the process — safe because the driver's
15
+ recency check rejects any proof that old anyway, so a dropped entry still can't be replayed. A
16
+ long-lived gate no longer slowly leaks memory. Custom `isUsed`/`markUsed` stores are unaffected
17
+ (give them a TTL = the window).
18
+ - **`onchain-proof` verification is now claim-release exception-safe.** If a driver's `verify()`
19
+ *throws* (an unexpected RPC exception) rather than returning a rejection, the gate now releases the
20
+ proof reservation before rethrowing — so a transient blip can't permanently burn an otherwise-valid
21
+ proof. This matches the `exact` path, which already did it.
22
+
23
+ ### Docs
24
+ - Rewrote **[Replay protection & recovery](https://docs.piprail.com/accepting-payments/replay-protection/)**
25
+ with the full "paid but didn't receive — what happens to the payment?" model (a recoverability
26
+ matrix, the at-most-once-by-design rationale, the bounded-memory behaviour, and the client's
27
+ never-re-pay `.ref` recovery).
28
+
7
29
  ## [1.20.0] — 2026-06-11 — discovery hardening: conformance-locked, accurate timing, PipRail-attributed
8
30
 
9
31
  A minor release focused on the discovery/registration subsystem — verified live against the real
package/dist/index.cjs CHANGED
@@ -4215,14 +4215,23 @@ function createPaymentGate(options) {
4215
4215
  };
4216
4216
  }
4217
4217
  const hasCustomStore = Boolean(options.isUsed || options.markUsed);
4218
- const localUsed = /* @__PURE__ */ new Set();
4218
+ const localUsed = /* @__PURE__ */ new Map();
4219
+ const replayWindowMs = maxTimeoutSeconds * 1e3;
4220
+ function pruneUsed(now) {
4221
+ for (const [key, expiry] of localUsed) {
4222
+ if (expiry > now) break;
4223
+ localUsed.delete(key);
4224
+ }
4225
+ }
4219
4226
  async function claimTx(ref) {
4220
4227
  if (hasCustomStore) {
4221
4228
  return options.isUsed ? Boolean(await options.isUsed(ref)) : false;
4222
4229
  }
4223
4230
  const key = ref.toLowerCase();
4231
+ const now = Date.now();
4232
+ pruneUsed(now);
4224
4233
  if (localUsed.has(key)) return true;
4225
- localUsed.add(key);
4234
+ localUsed.set(key, now + replayWindowMs);
4226
4235
  return false;
4227
4236
  }
4228
4237
  async function settleTx(ref, ok) {
@@ -4382,7 +4391,13 @@ function createPaymentGate(options) {
4382
4391
  }
4383
4392
  const ref = sig.payload.txHash;
4384
4393
  if (await claimTx(ref)) return rejection("tx_already_used", `Proof ${ref} was already redeemed.`);
4385
- const result = await spec.net.verify(ref, buildAccept(spec, sig.payload.nonce));
4394
+ let result;
4395
+ try {
4396
+ result = await spec.net.verify(ref, buildAccept(spec, sig.payload.nonce));
4397
+ } catch (err) {
4398
+ await settleTx(ref, false);
4399
+ throw err;
4400
+ }
4386
4401
  if (!result.ok) {
4387
4402
  await settleTx(ref, false);
4388
4403
  return rejection(result.error, result.detail);
package/dist/index.js CHANGED
@@ -4215,14 +4215,23 @@ function createPaymentGate(options) {
4215
4215
  };
4216
4216
  }
4217
4217
  const hasCustomStore = Boolean(options.isUsed || options.markUsed);
4218
- const localUsed = /* @__PURE__ */ new Set();
4218
+ const localUsed = /* @__PURE__ */ new Map();
4219
+ const replayWindowMs = maxTimeoutSeconds * 1e3;
4220
+ function pruneUsed(now) {
4221
+ for (const [key, expiry] of localUsed) {
4222
+ if (expiry > now) break;
4223
+ localUsed.delete(key);
4224
+ }
4225
+ }
4219
4226
  async function claimTx(ref) {
4220
4227
  if (hasCustomStore) {
4221
4228
  return options.isUsed ? Boolean(await options.isUsed(ref)) : false;
4222
4229
  }
4223
4230
  const key = ref.toLowerCase();
4231
+ const now = Date.now();
4232
+ pruneUsed(now);
4224
4233
  if (localUsed.has(key)) return true;
4225
- localUsed.add(key);
4234
+ localUsed.set(key, now + replayWindowMs);
4226
4235
  return false;
4227
4236
  }
4228
4237
  async function settleTx(ref, ok) {
@@ -4382,7 +4391,13 @@ function createPaymentGate(options) {
4382
4391
  }
4383
4392
  const ref = sig.payload.txHash;
4384
4393
  if (await claimTx(ref)) return rejection("tx_already_used", `Proof ${ref} was already redeemed.`);
4385
- const result = await spec.net.verify(ref, buildAccept(spec, sig.payload.nonce));
4394
+ let result;
4395
+ try {
4396
+ result = await spec.net.verify(ref, buildAccept(spec, sig.payload.nonce));
4397
+ } catch (err) {
4398
+ await settleTx(ref, false);
4399
+ throw err;
4400
+ }
4386
4401
  if (!result.ok) {
4387
4402
  await settleTx(ref, false);
4388
4403
  return rejection(result.error, result.detail);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piprail/sdk",
3
- "version": "1.20.0",
3
+ "version": "1.20.1",
4
4
  "description": "Accept x402 crypto payments across 29 chains — every major EVM chain plus Solana, TON, Tron, NEAR, Sui, Aptos, Algorand, Stellar & XRPL — in a couple of lines. No backend, no database, no fee; payments settle straight to your wallet.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",