paybridge 0.2.3 → 0.3.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/README.md CHANGED
@@ -50,6 +50,8 @@ Perfect for demos, learning, and rapid prototyping. See [playground/README.md](p
50
50
 
51
51
  ## Quick Start
52
52
 
53
+ > **Upgrading from 0.1 or 0.2?** See [docs/migration.md](docs/migration.md).
54
+
53
55
  ### One-time Payment
54
56
 
55
57
  ```typescript
@@ -205,7 +207,7 @@ app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
205
207
  **Notes:**
206
208
  - `⛔` marks features the underlying provider's API doesn't support — those methods throw a clear error explaining the limitation. Use a different provider for that capability or use `PayBridgeRouter` to route accordingly.
207
209
  - **Yellow Card** is gated behind `@experimental` until partner API documentation is verified — it logs a warning on instantiation. Do not use in production without partner-confirmed spec.
208
- - **Sandbox testing.** PayFast / PayStack / Stripe / Peach / Flutterwave are wired and unit-tested, but have not yet been validated against live sandbox credentials. Run a sandbox transaction before going live.
210
+ - **Sandbox testing.** PayFast / PayStack / Stripe / Peach / Flutterwave are wired and unit-tested, but have not yet been validated against live sandbox credentials. To validate against real sandboxes, set the relevant `*_API_KEY` env vars and run `npm run test:e2e:sandbox`.
209
211
 
210
212
  ## Provider Configuration
211
213
 
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * CryptoRamp — Unified crypto on/off-ramp SDK
3
3
  */
4
+ import { CryptoRampProvider } from './base';
4
5
  import { OnRampParams, OffRampParams, RampQuote, RampResult, CryptoRampCapabilities } from './types';
5
6
  export * from './types';
6
7
  export * from './base';
@@ -21,7 +22,9 @@ export interface CryptoRampConfig {
21
22
  }
22
23
  export declare class CryptoRamp {
23
24
  private provider;
24
- constructor(config: CryptoRampConfig);
25
+ constructor(config: CryptoRampConfig | {
26
+ provider: CryptoRampProvider;
27
+ });
25
28
  private createProvider;
26
29
  getQuote(direction: 'on' | 'off', fiatAmount: number, fiatCurrency: string, cryptoAsset: string, network: string): Promise<RampQuote>;
27
30
  createOnRamp(params: OnRampParams): Promise<RampResult>;
@@ -29,7 +29,12 @@ __exportStar(require("./mock"), exports);
29
29
  __exportStar(require("./router"), exports);
30
30
  class CryptoRamp {
31
31
  constructor(config) {
32
- this.provider = this.createProvider(config);
32
+ if ('provider' in config && typeof config.provider === 'object' && 'name' in config.provider) {
33
+ this.provider = config.provider;
34
+ }
35
+ else {
36
+ this.provider = this.createProvider(config);
37
+ }
33
38
  }
34
39
  createProvider(config) {
35
40
  const { provider, credentials, sandbox = true, webhookSecret } = config;
@@ -8,7 +8,7 @@ export interface CryptoRampRouterConfig {
8
8
  provider: CryptoRamp;
9
9
  priority?: number;
10
10
  }>;
11
- strategy?: 'cheapest' | 'priority' | 'round-robin';
11
+ strategy?: 'cheapest' | 'priority' | 'round-robin' | 'fastest';
12
12
  fallback?: {
13
13
  enabled: boolean;
14
14
  maxAttempts?: number;
@@ -16,6 +16,7 @@ export interface CryptoRampRouterConfig {
16
16
  };
17
17
  allowExperimental?: boolean;
18
18
  circuitBreakerStore?: import('../circuit-breaker-store').CircuitBreakerStore;
19
+ idempotencyStore?: import('../webhook-idempotency-store').IdempotencyStore;
19
20
  }
20
21
  export declare class CryptoRampRouter {
21
22
  private providers;
@@ -265,6 +265,14 @@ class CryptoRampRouter {
265
265
  const feeB = direction === 'on' ? capsB.fees.onRampPercent : capsB.fees.offRampPercent;
266
266
  return feeA - feeB;
267
267
  });
268
+ case 'fastest':
269
+ return [...providers].sort((a, b) => {
270
+ const capsA = a.instance.getCapabilities();
271
+ const capsB = b.instance.getCapabilities();
272
+ const latencyA = capsA.avgLatencyMs ?? Number.MAX_SAFE_INTEGER;
273
+ const latencyB = capsB.avgLatencyMs ?? Number.MAX_SAFE_INTEGER;
274
+ return latencyA - latencyB;
275
+ });
268
276
  case 'priority':
269
277
  return [...providers].sort((a, b) => {
270
278
  const priorityA = a.priority ?? 0;
@@ -85,5 +85,6 @@ export interface CryptoRampCapabilities {
85
85
  onRampPercent: number;
86
86
  offRampPercent: number;
87
87
  };
88
+ avgLatencyMs?: number;
88
89
  experimental?: boolean;
89
90
  }
package/dist/index.d.ts CHANGED
@@ -15,7 +15,9 @@ export * from './circuit-breaker-store';
15
15
  export * from './strategies';
16
16
  export * from './router';
17
17
  export * from './crypto';
18
+ export * from './webhook-idempotency-store';
18
19
  export { createRedisCircuitBreakerStore, type RedisLike, type RedisStoreOptions } from './stores/redis';
20
+ export { createRedisIdempotencyStore, type RedisIdempotencyStoreOptions } from './stores/redis-idempotency';
19
21
  export declare class PayBridge {
20
22
  readonly provider: PaymentProvider;
21
23
  constructor(config: PayBridgeConfig);
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
20
20
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
21
21
  };
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.PayBridge = exports.createRedisCircuitBreakerStore = void 0;
23
+ exports.PayBridge = exports.createRedisIdempotencyStore = exports.createRedisCircuitBreakerStore = void 0;
24
24
  const softycomp_1 = require("./providers/softycomp");
25
25
  const yoco_1 = require("./providers/yoco");
26
26
  const ozow_1 = require("./providers/ozow");
@@ -38,8 +38,11 @@ __exportStar(require("./circuit-breaker-store"), exports);
38
38
  __exportStar(require("./strategies"), exports);
39
39
  __exportStar(require("./router"), exports);
40
40
  __exportStar(require("./crypto"), exports);
41
+ __exportStar(require("./webhook-idempotency-store"), exports);
41
42
  var redis_1 = require("./stores/redis");
42
43
  Object.defineProperty(exports, "createRedisCircuitBreakerStore", { enumerable: true, get: function () { return redis_1.createRedisCircuitBreakerStore; } });
44
+ var redis_idempotency_1 = require("./stores/redis-idempotency");
45
+ Object.defineProperty(exports, "createRedisIdempotencyStore", { enumerable: true, get: function () { return redis_idempotency_1.createRedisIdempotencyStore; } });
43
46
  class PayBridge {
44
47
  constructor(config) {
45
48
  this.provider = this.createProvider(config);
package/dist/router.d.ts CHANGED
@@ -4,6 +4,13 @@
4
4
  import { PayBridge } from './index';
5
5
  import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent, Provider } from './types';
6
6
  import { RoutingStrategy, FallbackConfig } from './routing-types';
7
+ import type { IdempotencyStore } from './webhook-idempotency-store';
8
+ export declare class WebhookDuplicateError extends Error {
9
+ readonly name = "WebhookDuplicateError";
10
+ readonly eventId: string;
11
+ readonly provider: string;
12
+ constructor(provider: string, eventId: string);
13
+ }
7
14
  export interface PayBridgeRouterConfig {
8
15
  providers: Array<{
9
16
  provider: PayBridge;
@@ -13,12 +20,14 @@ export interface PayBridgeRouterConfig {
13
20
  strategy?: RoutingStrategy;
14
21
  fallback?: FallbackConfig;
15
22
  circuitBreakerStore?: import('./circuit-breaker-store').CircuitBreakerStore;
23
+ idempotencyStore?: IdempotencyStore;
16
24
  }
17
25
  export declare class PayBridgeRouter {
18
26
  private providers;
19
27
  private strategy;
20
28
  private fallback;
21
29
  private circuitBreakers;
30
+ private idempotencyStore?;
22
31
  private rrIndex;
23
32
  private config;
24
33
  constructor(config: PayBridgeRouterConfig);
@@ -26,7 +35,7 @@ export declare class PayBridgeRouter {
26
35
  createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionResult>;
27
36
  getPayment(id: string, provider?: string): Promise<PaymentResult>;
28
37
  refund(params: RefundParams, provider?: string): Promise<RefundResult>;
29
- parseWebhook(body: any, headers: any, providerName: Provider): WebhookEvent;
38
+ parseWebhook(body: any, headers: any, providerName: Provider): Promise<WebhookEvent>;
30
39
  verifyWebhook(body: any, headers: any, providerName: Provider): boolean;
31
40
  private filterProviders;
32
41
  private sleep;
package/dist/router.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * PayBridgeRouter — Multi-provider routing with fallback
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.PayBridgeRouter = void 0;
6
+ exports.PayBridgeRouter = exports.WebhookDuplicateError = void 0;
7
7
  const routing_types_1 = require("./routing-types");
8
8
  const strategies_1 = require("./strategies");
9
9
  const circuit_breaker_1 = require("./circuit-breaker");
@@ -16,6 +16,15 @@ function sanitizeErrorMessage(msg) {
16
16
  .replace(/(api[_-]?key|secret|token|password)["':=\s]+\S+/gi, '$1=[REDACTED]')
17
17
  .slice(0, 500);
18
18
  }
19
+ class WebhookDuplicateError extends Error {
20
+ constructor(provider, eventId) {
21
+ super(`Webhook event already processed: ${provider}:${eventId}`);
22
+ this.name = 'WebhookDuplicateError';
23
+ this.eventId = eventId;
24
+ this.provider = provider;
25
+ }
26
+ }
27
+ exports.WebhookDuplicateError = WebhookDuplicateError;
19
28
  class PayBridgeRouter {
20
29
  constructor(config) {
21
30
  this.rrIndex = 0;
@@ -31,6 +40,7 @@ class PayBridgeRouter {
31
40
  maxAttempts: config.fallback?.maxAttempts ?? 3,
32
41
  retryDelayMs: config.fallback?.retryDelayMs ?? 250,
33
42
  };
43
+ this.idempotencyStore = config.idempotencyStore;
34
44
  this.circuitBreakers = new Map();
35
45
  for (const p of this.providers) {
36
46
  const name = p.instance.getProviderName();
@@ -246,12 +256,24 @@ class PayBridgeRouter {
246
256
  }
247
257
  throw new Error(`Refund for payment ${params.paymentId} failed on all providers: ${lastError?.message || 'Unknown error'}`);
248
258
  }
249
- parseWebhook(body, headers, providerName) {
259
+ async parseWebhook(body, headers, providerName) {
250
260
  const providerMeta = this.providers.find(p => p.instance.getProviderName() === providerName);
251
261
  if (!providerMeta) {
252
262
  throw new Error(`Unknown provider for webhook: ${providerName}`);
253
263
  }
254
- return providerMeta.instance.parseWebhook(body, headers);
264
+ const event = providerMeta.instance.parseWebhook(body, headers);
265
+ if (this.idempotencyStore) {
266
+ const eventId = event.payment?.id || event.subscription?.id || event.refund?.id;
267
+ if (eventId) {
268
+ const key = `${providerName}:${eventId}`;
269
+ const ttlMs = 24 * 60 * 60 * 1000;
270
+ const isNew = await this.idempotencyStore.recordIfNew(key, ttlMs);
271
+ if (!isNew) {
272
+ throw new WebhookDuplicateError(providerName, eventId);
273
+ }
274
+ }
275
+ }
276
+ return event;
255
277
  }
256
278
  verifyWebhook(body, headers, providerName) {
257
279
  const providerMeta = this.providers.find(p => p.instance.getProviderName() === providerName);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Redis-backed idempotency store adapter
3
+ */
4
+ import type { RedisLike } from './redis';
5
+ import type { IdempotencyStore } from '../webhook-idempotency-store';
6
+ export interface RedisIdempotencyStoreOptions {
7
+ redis: RedisLike;
8
+ keyPrefix?: string;
9
+ }
10
+ export declare function createRedisIdempotencyStore(opts: RedisIdempotencyStoreOptions): IdempotencyStore;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ /**
3
+ * Redis-backed idempotency store adapter
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createRedisIdempotencyStore = createRedisIdempotencyStore;
7
+ function createRedisIdempotencyStore(opts) {
8
+ const prefix = opts.keyPrefix ?? 'pb:idem:';
9
+ return {
10
+ async recordIfNew(key, ttlMs) {
11
+ const result = await opts.redis.set(`${prefix}${key}`, '1', 'PX', ttlMs, 'NX');
12
+ return result === 'OK';
13
+ },
14
+ };
15
+ }
@@ -19,8 +19,8 @@ exports.strategies = {
19
19
  return [...providers].sort((a, b) => {
20
20
  const capsA = a.instance.provider.getCapabilities();
21
21
  const capsB = b.instance.provider.getCapabilities();
22
- const latencyA = capsA.avgLatencyMs ?? 1000;
23
- const latencyB = capsB.avgLatencyMs ?? 1000;
22
+ const latencyA = capsA.avgLatencyMs ?? Number.MAX_SAFE_INTEGER;
23
+ const latencyB = capsB.avgLatencyMs ?? Number.MAX_SAFE_INTEGER;
24
24
  return latencyA - latencyB;
25
25
  });
26
26
  },
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Webhook idempotency storage abstraction
3
+ */
4
+ export interface IdempotencyStore {
5
+ recordIfNew(key: string, ttlMs: number): Promise<boolean>;
6
+ }
7
+ export declare class InMemoryIdempotencyStore implements IdempotencyStore {
8
+ private store;
9
+ private cleanupInterval?;
10
+ constructor(opts?: {
11
+ cleanupIntervalMs?: number;
12
+ });
13
+ recordIfNew(key: string, ttlMs: number): Promise<boolean>;
14
+ private cleanup;
15
+ destroy(): void;
16
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ /**
3
+ * Webhook idempotency storage abstraction
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.InMemoryIdempotencyStore = void 0;
7
+ class InMemoryIdempotencyStore {
8
+ constructor(opts = {}) {
9
+ this.store = new Map();
10
+ if (opts.cleanupIntervalMs) {
11
+ this.cleanupInterval = setInterval(() => this.cleanup(), opts.cleanupIntervalMs);
12
+ this.cleanupInterval.unref?.();
13
+ }
14
+ }
15
+ async recordIfNew(key, ttlMs) {
16
+ const now = Date.now();
17
+ const existing = this.store.get(key);
18
+ if (existing !== undefined && existing > now) {
19
+ return false;
20
+ }
21
+ this.store.set(key, now + ttlMs);
22
+ return true;
23
+ }
24
+ cleanup() {
25
+ const now = Date.now();
26
+ const keysToDelete = [];
27
+ this.store.forEach((expiresAt, k) => {
28
+ if (expiresAt <= now)
29
+ keysToDelete.push(k);
30
+ });
31
+ keysToDelete.forEach(k => this.store.delete(k));
32
+ }
33
+ destroy() {
34
+ if (this.cleanupInterval)
35
+ clearInterval(this.cleanupInterval);
36
+ this.store.clear();
37
+ }
38
+ }
39
+ exports.InMemoryIdempotencyStore = InMemoryIdempotencyStore;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paybridge",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "One API for fiat + crypto payments. Multi-provider routing, automatic failover, MoonPay on/off-ramp. SA-first, global-ready.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,7 +11,8 @@
11
11
  "prepublishOnly": "npm run clean && npm run build",
12
12
  "test": "tsc && tsc --project tsconfig.test.json && node --test 'dist-test/**/*.test.js'",
13
13
  "test:e2e:moonpay": "tsx tests/e2e/moonpay-sandbox.ts",
14
- "test:e2e:yellowcard": "tsx tests/e2e/yellowcard-sandbox.ts"
14
+ "test:e2e:yellowcard": "tsx tests/e2e/yellowcard-sandbox.ts",
15
+ "test:e2e:sandbox": "tsx tests/e2e/sandbox-validate.ts"
15
16
  },
16
17
  "keywords": [
17
18
  "payments",
@@ -49,6 +50,7 @@
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/node": "^18.19.130",
53
+ "tsx": "^4.19.2",
52
54
  "typescript": "^5.9.3"
53
55
  }
54
56
  }