paybridge 0.1.3 → 0.2.2

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.
Files changed (50) hide show
  1. package/README.md +87 -7
  2. package/dist/circuit-breaker-store.d.ts +27 -0
  3. package/dist/circuit-breaker-store.js +25 -0
  4. package/dist/circuit-breaker.d.ts +30 -0
  5. package/dist/circuit-breaker.js +86 -0
  6. package/dist/crypto/base.d.ts +15 -0
  7. package/dist/crypto/base.js +24 -0
  8. package/dist/crypto/index.d.ts +35 -0
  9. package/dist/crypto/index.js +95 -0
  10. package/dist/crypto/mock.d.ts +15 -0
  11. package/dist/crypto/mock.js +112 -0
  12. package/dist/crypto/moonpay.d.ts +33 -0
  13. package/dist/crypto/moonpay.js +261 -0
  14. package/dist/crypto/router.d.ts +36 -0
  15. package/dist/crypto/router.js +287 -0
  16. package/dist/crypto/types.d.ts +89 -0
  17. package/dist/crypto/types.js +5 -0
  18. package/dist/crypto/yellowcard.d.ts +56 -0
  19. package/dist/crypto/yellowcard.js +311 -0
  20. package/dist/index.d.ts +10 -1
  21. package/dist/index.js +60 -3
  22. package/dist/providers/base.d.ts +5 -0
  23. package/dist/providers/flutterwave.d.ts +36 -0
  24. package/dist/providers/flutterwave.js +339 -0
  25. package/dist/providers/ozow.d.ts +20 -2
  26. package/dist/providers/ozow.js +158 -114
  27. package/dist/providers/payfast.d.ts +40 -0
  28. package/dist/providers/payfast.js +352 -0
  29. package/dist/providers/paystack.d.ts +37 -0
  30. package/dist/providers/paystack.js +336 -0
  31. package/dist/providers/peach.d.ts +50 -0
  32. package/dist/providers/peach.js +302 -0
  33. package/dist/providers/softycomp.d.ts +106 -0
  34. package/dist/providers/softycomp.js +229 -10
  35. package/dist/providers/stripe.d.ts +38 -0
  36. package/dist/providers/stripe.js +367 -0
  37. package/dist/providers/yoco.d.ts +12 -0
  38. package/dist/providers/yoco.js +148 -61
  39. package/dist/router.d.ts +33 -0
  40. package/dist/router.js +282 -0
  41. package/dist/routing-types.d.ts +39 -0
  42. package/dist/routing-types.js +14 -0
  43. package/dist/stores/redis.d.ts +30 -0
  44. package/dist/stores/redis.js +42 -0
  45. package/dist/strategies.d.ts +18 -0
  46. package/dist/strategies.js +44 -0
  47. package/dist/types.d.ts +4 -2
  48. package/dist/utils/fetch.d.ts +24 -0
  49. package/dist/utils/fetch.js +74 -0
  50. package/package.json +7 -4
@@ -0,0 +1,33 @@
1
+ /**
2
+ * PayBridgeRouter — Multi-provider routing with fallback
3
+ */
4
+ import { PayBridge } from './index';
5
+ import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent, Provider } from './types';
6
+ import { RoutingStrategy, FallbackConfig } from './routing-types';
7
+ export interface PayBridgeRouterConfig {
8
+ providers: Array<{
9
+ provider: PayBridge;
10
+ weight?: number;
11
+ priority?: number;
12
+ }>;
13
+ strategy?: RoutingStrategy;
14
+ fallback?: FallbackConfig;
15
+ circuitBreakerStore?: import('./circuit-breaker-store').CircuitBreakerStore;
16
+ }
17
+ export declare class PayBridgeRouter {
18
+ private providers;
19
+ private strategy;
20
+ private fallback;
21
+ private circuitBreakers;
22
+ private rrIndex;
23
+ private config;
24
+ constructor(config: PayBridgeRouterConfig);
25
+ createPayment(params: CreatePaymentParams): Promise<PaymentResult>;
26
+ createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionResult>;
27
+ getPayment(id: string, provider?: string): Promise<PaymentResult>;
28
+ refund(params: RefundParams, provider?: string): Promise<RefundResult>;
29
+ parseWebhook(body: any, headers: any, providerName: Provider): WebhookEvent;
30
+ verifyWebhook(body: any, headers: any, providerName: Provider): boolean;
31
+ private filterProviders;
32
+ private sleep;
33
+ }
package/dist/router.js ADDED
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ /**
3
+ * PayBridgeRouter — Multi-provider routing with fallback
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PayBridgeRouter = void 0;
7
+ const routing_types_1 = require("./routing-types");
8
+ const strategies_1 = require("./strategies");
9
+ const circuit_breaker_1 = require("./circuit-breaker");
10
+ const fetch_1 = require("./utils/fetch");
11
+ function sanitizeErrorMessage(msg) {
12
+ if (!msg)
13
+ return 'unknown error';
14
+ return msg
15
+ .replace(/\b[A-Za-z0-9_-]{32,}\b/g, '[REDACTED]')
16
+ .replace(/(api[_-]?key|secret|token|password)["':=\s]+\S+/gi, '$1=[REDACTED]')
17
+ .slice(0, 500);
18
+ }
19
+ class PayBridgeRouter {
20
+ constructor(config) {
21
+ this.rrIndex = 0;
22
+ this.config = config;
23
+ this.providers = config.providers.map(p => ({
24
+ instance: p.provider,
25
+ weight: p.weight,
26
+ priority: p.priority,
27
+ }));
28
+ this.strategy = config.strategy ?? 'cheapest';
29
+ this.fallback = {
30
+ enabled: config.fallback?.enabled ?? true,
31
+ maxAttempts: config.fallback?.maxAttempts ?? 3,
32
+ retryDelayMs: config.fallback?.retryDelayMs ?? 250,
33
+ };
34
+ this.circuitBreakers = new Map();
35
+ for (const p of this.providers) {
36
+ const name = p.instance.getProviderName();
37
+ this.circuitBreakers.set(name, new circuit_breaker_1.CircuitBreaker(name, {
38
+ store: config.circuitBreakerStore,
39
+ }));
40
+ }
41
+ }
42
+ async createPayment(params) {
43
+ if (!Number.isFinite(params.amount) || params.amount <= 0) {
44
+ throw new Error('Invalid amount: must be a positive finite number');
45
+ }
46
+ const context = {
47
+ amount: params.amount,
48
+ currency: params.currency,
49
+ };
50
+ const filtered = this.filterProviders(params.currency, params.amount);
51
+ if (filtered.length === 0) {
52
+ throw new Error(`No providers support currency ${params.currency} with amount ${params.amount}`);
53
+ }
54
+ const strategyFn = (0, strategies_1.getStrategy)(this.strategy);
55
+ const ordered = strategyFn(filtered, context, () => {
56
+ const idx = this.rrIndex;
57
+ this.rrIndex = (this.rrIndex + 1) % filtered.length;
58
+ return idx;
59
+ });
60
+ const attempts = [];
61
+ let lastError = null;
62
+ for (const providerMeta of ordered) {
63
+ const providerName = providerMeta.instance.getProviderName();
64
+ const breaker = this.circuitBreakers.get(providerName);
65
+ if (breaker && (await breaker.isOpen())) {
66
+ attempts.push({
67
+ provider: providerName,
68
+ status: 'skipped',
69
+ errorMessage: 'Circuit breaker open',
70
+ latencyMs: 0,
71
+ });
72
+ continue;
73
+ }
74
+ const startTime = Date.now();
75
+ try {
76
+ const result = await providerMeta.instance.createPayment(params);
77
+ const latencyMs = Date.now() - startTime;
78
+ if (breaker)
79
+ await breaker.recordSuccess();
80
+ attempts.push({
81
+ provider: providerName,
82
+ status: 'success',
83
+ latencyMs,
84
+ });
85
+ const routingMeta = {
86
+ attempts,
87
+ chosenProvider: providerName,
88
+ strategy: this.strategy,
89
+ };
90
+ return {
91
+ ...result,
92
+ routingMeta,
93
+ };
94
+ }
95
+ catch (error) {
96
+ const latencyMs = Date.now() - startTime;
97
+ lastError = error;
98
+ const isRateLimited = error instanceof fetch_1.HttpError &&
99
+ (error.status === 429 || (error.status === 503 && error.retryAfterMs !== undefined));
100
+ if (isRateLimited) {
101
+ attempts.push({
102
+ provider: providerName,
103
+ status: 'failed',
104
+ errorCode: 'RATE_LIMITED',
105
+ errorMessage: sanitizeErrorMessage(error.message),
106
+ latencyMs,
107
+ });
108
+ }
109
+ else {
110
+ if (breaker)
111
+ await breaker.recordFailure();
112
+ let errorCode = error.code;
113
+ if (error instanceof fetch_1.FetchTimeoutError) {
114
+ errorCode = 'TIMEOUT';
115
+ }
116
+ attempts.push({
117
+ provider: providerName,
118
+ status: 'failed',
119
+ errorCode,
120
+ errorMessage: sanitizeErrorMessage(error.message),
121
+ latencyMs,
122
+ });
123
+ }
124
+ if (!this.fallback.enabled || attempts.length >= (this.fallback.maxAttempts ?? 3)) {
125
+ break;
126
+ }
127
+ await this.sleep(this.fallback.retryDelayMs ?? 250);
128
+ }
129
+ }
130
+ throw new routing_types_1.RoutingError(`All providers failed: ${lastError?.message || 'Unknown error'}`, attempts);
131
+ }
132
+ async createSubscription(params) {
133
+ const context = {
134
+ amount: params.amount,
135
+ currency: params.currency,
136
+ };
137
+ const filtered = this.filterProviders(params.currency, params.amount);
138
+ if (filtered.length === 0) {
139
+ throw new Error(`No providers support currency ${params.currency} with amount ${params.amount}`);
140
+ }
141
+ const strategyFn = (0, strategies_1.getStrategy)(this.strategy);
142
+ const ordered = strategyFn(filtered, context, () => {
143
+ const idx = this.rrIndex;
144
+ this.rrIndex = (this.rrIndex + 1) % filtered.length;
145
+ return idx;
146
+ });
147
+ const attempts = [];
148
+ let lastError = null;
149
+ for (const providerMeta of ordered) {
150
+ const providerName = providerMeta.instance.getProviderName();
151
+ const breaker = this.circuitBreakers.get(providerName);
152
+ if (breaker && (await breaker.isOpen())) {
153
+ attempts.push({
154
+ provider: providerName,
155
+ status: 'skipped',
156
+ errorMessage: 'Circuit breaker open',
157
+ latencyMs: 0,
158
+ });
159
+ continue;
160
+ }
161
+ const startTime = Date.now();
162
+ try {
163
+ const result = await providerMeta.instance.createSubscription(params);
164
+ const latencyMs = Date.now() - startTime;
165
+ if (breaker)
166
+ await breaker.recordSuccess();
167
+ attempts.push({
168
+ provider: providerName,
169
+ status: 'success',
170
+ latencyMs,
171
+ });
172
+ return result;
173
+ }
174
+ catch (error) {
175
+ const latencyMs = Date.now() - startTime;
176
+ lastError = error;
177
+ const isRateLimited = error instanceof fetch_1.HttpError &&
178
+ (error.status === 429 || (error.status === 503 && error.retryAfterMs !== undefined));
179
+ if (isRateLimited) {
180
+ attempts.push({
181
+ provider: providerName,
182
+ status: 'failed',
183
+ errorCode: 'RATE_LIMITED',
184
+ errorMessage: sanitizeErrorMessage(error.message),
185
+ latencyMs,
186
+ });
187
+ }
188
+ else {
189
+ if (breaker)
190
+ await breaker.recordFailure();
191
+ let errorCode = error.code;
192
+ if (error instanceof fetch_1.FetchTimeoutError) {
193
+ errorCode = 'TIMEOUT';
194
+ }
195
+ attempts.push({
196
+ provider: providerName,
197
+ status: 'failed',
198
+ errorCode,
199
+ errorMessage: sanitizeErrorMessage(error.message),
200
+ latencyMs,
201
+ });
202
+ }
203
+ if (!this.fallback.enabled || attempts.length >= (this.fallback.maxAttempts ?? 3)) {
204
+ break;
205
+ }
206
+ await this.sleep(this.fallback.retryDelayMs ?? 250);
207
+ }
208
+ }
209
+ throw new routing_types_1.RoutingError(`All providers failed for subscription: ${lastError?.message || 'Unknown error'}`, attempts);
210
+ }
211
+ async getPayment(id, provider) {
212
+ if (provider) {
213
+ const providerMeta = this.providers.find(p => p.instance.getProviderName() === provider);
214
+ if (!providerMeta) {
215
+ throw new Error(`Provider ${provider} not found in router`);
216
+ }
217
+ return providerMeta.instance.getPayment(id);
218
+ }
219
+ let lastError = null;
220
+ for (const providerMeta of this.providers) {
221
+ try {
222
+ return await providerMeta.instance.getPayment(id);
223
+ }
224
+ catch (error) {
225
+ lastError = error;
226
+ }
227
+ }
228
+ throw new Error(`Payment ${id} not found in any provider: ${lastError?.message || 'Unknown error'}`);
229
+ }
230
+ async refund(params, provider) {
231
+ if (provider) {
232
+ const providerMeta = this.providers.find(p => p.instance.getProviderName() === provider);
233
+ if (!providerMeta) {
234
+ throw new Error(`Provider ${provider} not found in router`);
235
+ }
236
+ return providerMeta.instance.refund(params);
237
+ }
238
+ let lastError = null;
239
+ for (const providerMeta of this.providers) {
240
+ try {
241
+ return await providerMeta.instance.refund(params);
242
+ }
243
+ catch (error) {
244
+ lastError = error;
245
+ }
246
+ }
247
+ throw new Error(`Refund for payment ${params.paymentId} failed on all providers: ${lastError?.message || 'Unknown error'}`);
248
+ }
249
+ parseWebhook(body, headers, providerName) {
250
+ const providerMeta = this.providers.find(p => p.instance.getProviderName() === providerName);
251
+ if (!providerMeta) {
252
+ throw new Error(`Unknown provider for webhook: ${providerName}`);
253
+ }
254
+ return providerMeta.instance.parseWebhook(body, headers);
255
+ }
256
+ verifyWebhook(body, headers, providerName) {
257
+ const providerMeta = this.providers.find(p => p.instance.getProviderName() === providerName);
258
+ if (!providerMeta) {
259
+ throw new Error(`Unknown provider for webhook: ${providerName}`);
260
+ }
261
+ return providerMeta.instance.verifyWebhook(body, headers);
262
+ }
263
+ filterProviders(currency, amount) {
264
+ return this.providers.filter(p => {
265
+ const caps = p.instance.provider.getCapabilities();
266
+ if (!caps.currencies.includes(currency)) {
267
+ return false;
268
+ }
269
+ if (caps.minAmount !== undefined && amount < caps.minAmount) {
270
+ return false;
271
+ }
272
+ if (caps.maxAmount !== undefined && amount > caps.maxAmount) {
273
+ return false;
274
+ }
275
+ return true;
276
+ });
277
+ }
278
+ sleep(ms) {
279
+ return new Promise(resolve => setTimeout(resolve, ms));
280
+ }
281
+ }
282
+ exports.PayBridgeRouter = PayBridgeRouter;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Types for payment routing
3
+ */
4
+ import { Currency } from './types';
5
+ export interface ProviderFees {
6
+ fixed: number;
7
+ percent: number;
8
+ currency: Currency;
9
+ }
10
+ export interface ProviderCapabilities {
11
+ fees: ProviderFees;
12
+ currencies: string[];
13
+ minAmount?: number;
14
+ maxAmount?: number;
15
+ avgLatencyMs?: number;
16
+ country: string;
17
+ }
18
+ export interface RoutingAttempt {
19
+ provider: string;
20
+ status: 'success' | 'failed' | 'skipped';
21
+ errorCode?: string;
22
+ errorMessage?: string;
23
+ latencyMs: number;
24
+ }
25
+ export interface RoutingMeta {
26
+ attempts: RoutingAttempt[];
27
+ chosenProvider: string;
28
+ strategy: RoutingStrategy;
29
+ }
30
+ export type RoutingStrategy = 'cheapest' | 'fastest' | 'priority' | 'round-robin';
31
+ export interface FallbackConfig {
32
+ enabled: boolean;
33
+ maxAttempts?: number;
34
+ retryDelayMs?: number;
35
+ }
36
+ export declare class RoutingError extends Error {
37
+ readonly attempts: RoutingAttempt[];
38
+ constructor(message: string, attempts: RoutingAttempt[]);
39
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /**
3
+ * Types for payment routing
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RoutingError = void 0;
7
+ class RoutingError extends Error {
8
+ constructor(message, attempts) {
9
+ super(message);
10
+ this.name = 'RoutingError';
11
+ this.attempts = attempts;
12
+ }
13
+ }
14
+ exports.RoutingError = RoutingError;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Redis-backed circuit breaker store adapter
3
+ */
4
+ import { CircuitBreakerStore } from '../circuit-breaker-store';
5
+ /**
6
+ * Duck-typed Redis client interface.
7
+ * Compatible with both ioredis and node-redis v4+.
8
+ */
9
+ export interface RedisLike {
10
+ get(key: string): Promise<string | null>;
11
+ set(key: string, value: string, ...args: any[]): Promise<any>;
12
+ del(key: string): Promise<any>;
13
+ }
14
+ export interface RedisStoreOptions {
15
+ prefix?: string;
16
+ }
17
+ /**
18
+ * Create a Redis-backed circuit breaker store.
19
+ * Works with ioredis and node-redis v4+ (duck-types get/set/del).
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import Redis from 'ioredis';
24
+ * import { createRedisCircuitBreakerStore } from 'paybridge';
25
+ *
26
+ * const redis = new Redis(process.env.REDIS_URL);
27
+ * const store = createRedisCircuitBreakerStore(redis, { prefix: 'app:cb:' });
28
+ * ```
29
+ */
30
+ export declare function createRedisCircuitBreakerStore(client: RedisLike, options?: RedisStoreOptions): CircuitBreakerStore;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * Redis-backed circuit breaker store adapter
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createRedisCircuitBreakerStore = createRedisCircuitBreakerStore;
7
+ /**
8
+ * Create a Redis-backed circuit breaker store.
9
+ * Works with ioredis and node-redis v4+ (duck-types get/set/del).
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import Redis from 'ioredis';
14
+ * import { createRedisCircuitBreakerStore } from 'paybridge';
15
+ *
16
+ * const redis = new Redis(process.env.REDIS_URL);
17
+ * const store = createRedisCircuitBreakerStore(redis, { prefix: 'app:cb:' });
18
+ * ```
19
+ */
20
+ function createRedisCircuitBreakerStore(client, options = {}) {
21
+ const prefix = options.prefix ?? 'paybridge:cb:';
22
+ return {
23
+ async get(key) {
24
+ const raw = await client.get(prefix + key);
25
+ if (!raw)
26
+ return null;
27
+ return JSON.parse(raw);
28
+ },
29
+ async set(key, snapshot, ttlMs) {
30
+ const value = JSON.stringify(snapshot);
31
+ if (ttlMs !== undefined) {
32
+ await client.set(prefix + key, value, 'PX', ttlMs);
33
+ }
34
+ else {
35
+ await client.set(prefix + key, value);
36
+ }
37
+ },
38
+ async delete(key) {
39
+ await client.del(prefix + key);
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Routing strategies for multi-provider payments
3
+ */
4
+ import { PayBridge } from './index';
5
+ import { Currency } from './types';
6
+ import { RoutingStrategy } from './routing-types';
7
+ export interface ProviderWithMeta {
8
+ instance: PayBridge;
9
+ weight?: number;
10
+ priority?: number;
11
+ }
12
+ export interface StrategyContext {
13
+ amount: number;
14
+ currency: Currency;
15
+ }
16
+ export type Strategy = (providers: ProviderWithMeta[], context: StrategyContext, getRRIndex?: () => number) => ProviderWithMeta[];
17
+ export declare const strategies: Record<RoutingStrategy, Strategy>;
18
+ export declare function getStrategy(name: RoutingStrategy): Strategy;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /**
3
+ * Routing strategies for multi-provider payments
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.strategies = void 0;
7
+ exports.getStrategy = getStrategy;
8
+ exports.strategies = {
9
+ cheapest: (providers, context) => {
10
+ return [...providers].sort((a, b) => {
11
+ const capsA = a.instance.provider.getCapabilities();
12
+ const capsB = b.instance.provider.getCapabilities();
13
+ const feeA = capsA.fees.fixed + (context.amount * capsA.fees.percent) / 100;
14
+ const feeB = capsB.fees.fixed + (context.amount * capsB.fees.percent) / 100;
15
+ return feeA - feeB;
16
+ });
17
+ },
18
+ fastest: (providers, _context) => {
19
+ return [...providers].sort((a, b) => {
20
+ const capsA = a.instance.provider.getCapabilities();
21
+ const capsB = b.instance.provider.getCapabilities();
22
+ const latencyA = capsA.avgLatencyMs ?? 1000;
23
+ const latencyB = capsB.avgLatencyMs ?? 1000;
24
+ return latencyA - latencyB;
25
+ });
26
+ },
27
+ priority: (providers, _context) => {
28
+ return [...providers].sort((a, b) => {
29
+ const priorityA = a.priority ?? 0;
30
+ const priorityB = b.priority ?? 0;
31
+ return priorityB - priorityA;
32
+ });
33
+ },
34
+ 'round-robin': (providers, _context, getRRIndex) => {
35
+ if (!getRRIndex) {
36
+ return providers;
37
+ }
38
+ const idx = getRRIndex();
39
+ return [...providers.slice(idx), ...providers.slice(0, idx)];
40
+ },
41
+ };
42
+ function getStrategy(name) {
43
+ return exports.strategies[name];
44
+ }
package/dist/types.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * PayBridge — Unified payment SDK types
3
3
  */
4
- export type Provider = 'softycomp' | 'yoco' | 'ozow' | 'payfast' | 'paystack' | 'stripe' | 'peach';
4
+ export type Provider = 'softycomp' | 'yoco' | 'ozow' | 'payfast' | 'paystack' | 'stripe' | 'peach' | 'flutterwave';
5
5
  export type PaymentStatus = 'pending' | 'completed' | 'failed' | 'cancelled' | 'refunded';
6
6
  export type SubscriptionInterval = 'weekly' | 'monthly' | 'yearly';
7
- export type Currency = 'ZAR' | 'USD' | 'EUR' | 'GBP' | 'NGN';
7
+ export type Currency = 'ZAR' | 'USD' | 'EUR' | 'GBP' | 'NGN' | 'KES' | 'UGX' | 'GHS' | string;
8
8
  export type WebhookEventType = 'payment.pending' | 'payment.completed' | 'payment.failed' | 'payment.cancelled' | 'subscription.created' | 'subscription.cancelled' | 'refund.completed';
9
9
  export interface PayBridgeConfig {
10
10
  /** Payment provider to use */
@@ -77,6 +77,8 @@ export interface PaymentResult {
77
77
  expiresAt?: string;
78
78
  /** Raw provider response */
79
79
  raw?: any;
80
+ /** Routing metadata (populated by PayBridgeRouter) */
81
+ routingMeta?: import('./routing-types').RoutingMeta;
80
82
  }
81
83
  export interface CreateSubscriptionParams {
82
84
  /** Amount in major currency unit (e.g. 299.00 for R299) */
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Fetch utilities with timeout and HTTP error handling
3
+ */
4
+ export interface FetchOptions extends RequestInit {
5
+ timeoutMs?: number;
6
+ }
7
+ export declare class FetchTimeoutError extends Error {
8
+ readonly name = "FetchTimeoutError";
9
+ readonly url: string;
10
+ readonly timeoutMs: number;
11
+ constructor(url: string, timeoutMs: number);
12
+ }
13
+ export declare class HttpError extends Error {
14
+ readonly name = "HttpError";
15
+ readonly status: number;
16
+ readonly retryAfterMs?: number;
17
+ readonly body?: string;
18
+ constructor(status: number, message: string, opts?: {
19
+ retryAfterMs?: number;
20
+ body?: string;
21
+ });
22
+ }
23
+ export declare function timedFetch(url: string, opts?: FetchOptions): Promise<Response>;
24
+ export declare function timedFetchOrThrow(url: string, opts?: FetchOptions): Promise<Response>;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ /**
3
+ * Fetch utilities with timeout and HTTP error handling
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HttpError = exports.FetchTimeoutError = void 0;
7
+ exports.timedFetch = timedFetch;
8
+ exports.timedFetchOrThrow = timedFetchOrThrow;
9
+ class FetchTimeoutError extends Error {
10
+ constructor(url, timeoutMs) {
11
+ super(`Request to ${url} timed out after ${timeoutMs}ms`);
12
+ this.name = 'FetchTimeoutError';
13
+ this.url = url;
14
+ this.timeoutMs = timeoutMs;
15
+ }
16
+ }
17
+ exports.FetchTimeoutError = FetchTimeoutError;
18
+ class HttpError extends Error {
19
+ constructor(status, message, opts = {}) {
20
+ super(message);
21
+ this.name = 'HttpError';
22
+ this.status = status;
23
+ this.retryAfterMs = opts.retryAfterMs;
24
+ this.body = opts.body;
25
+ }
26
+ }
27
+ exports.HttpError = HttpError;
28
+ const DEFAULT_TIMEOUT_MS = 30000;
29
+ async function timedFetch(url, opts = {}) {
30
+ const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: callerSignal, ...rest } = opts;
31
+ const controller = new AbortController();
32
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
33
+ if (callerSignal) {
34
+ if (callerSignal.aborted)
35
+ controller.abort();
36
+ else
37
+ callerSignal.addEventListener('abort', () => controller.abort(), { once: true });
38
+ }
39
+ try {
40
+ return await fetch(url, { ...rest, signal: controller.signal });
41
+ }
42
+ catch (err) {
43
+ if (err?.name === 'AbortError') {
44
+ throw new FetchTimeoutError(url, timeoutMs);
45
+ }
46
+ throw err;
47
+ }
48
+ finally {
49
+ clearTimeout(timeoutId);
50
+ }
51
+ }
52
+ async function timedFetchOrThrow(url, opts = {}) {
53
+ const response = await timedFetch(url, opts);
54
+ if (!response.ok) {
55
+ let body = '';
56
+ try {
57
+ body = await response.text();
58
+ }
59
+ catch { }
60
+ const retryAfterHeader = response.headers.get('retry-after');
61
+ const retryAfterMs = retryAfterHeader ? parseRetryAfter(retryAfterHeader) : undefined;
62
+ throw new HttpError(response.status, `HTTP ${response.status}: ${body.slice(0, 200) || response.statusText}`, { retryAfterMs, body });
63
+ }
64
+ return response;
65
+ }
66
+ function parseRetryAfter(value) {
67
+ const seconds = parseInt(value, 10);
68
+ if (!isNaN(seconds))
69
+ return seconds * 1000;
70
+ const dateMs = Date.parse(value);
71
+ if (!isNaN(dateMs))
72
+ return Math.max(0, dateMs - Date.now());
73
+ return undefined;
74
+ }
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "paybridge",
3
- "version": "0.1.3",
4
- "description": "One API, every payment provider. Unified payment SDK for Node.js SoftyComp, Yoco, Ozow, and more.",
3
+ "version": "0.2.2",
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",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
- "prepublishOnly": "npm run build",
10
- "test": "echo \"Error: no test specified\" && exit 1"
9
+ "clean": "rm -rf dist",
10
+ "prepublishOnly": "npm run clean && npm run build",
11
+ "test": "tsc && tsc --project tsconfig.test.json && node --test 'dist-test/**/*.test.js'",
12
+ "test:e2e:moonpay": "tsx tests/e2e/moonpay-sandbox.ts",
13
+ "test:e2e:yellowcard": "tsx tests/e2e/yellowcard-sandbox.ts"
11
14
  },
12
15
  "keywords": [
13
16
  "payments",