paybridge 0.3.1 → 0.5.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/README.md +56 -1
- package/dist/circuit-breaker.d.ts +2 -0
- package/dist/circuit-breaker.js +9 -0
- package/dist/crypto/router.d.ts +9 -0
- package/dist/crypto/router.js +143 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +39 -1
- package/dist/ledger.d.ts +36 -0
- package/dist/ledger.js +32 -0
- package/dist/providers/adyen.d.ts +46 -0
- package/dist/providers/adyen.js +289 -0
- package/dist/providers/mercadopago.d.ts +36 -0
- package/dist/providers/mercadopago.js +297 -0
- package/dist/providers/razorpay.d.ts +37 -0
- package/dist/providers/razorpay.js +328 -0
- package/dist/router-events.d.ts +16 -0
- package/dist/router-events.js +11 -0
- package/dist/router.d.ts +9 -0
- package/dist/router.js +258 -6
- package/dist/stores/redis-ledger.d.ts +8 -0
- package/dist/stores/redis-ledger.js +52 -0
- package/dist/stores/redis.d.ts +6 -0
- package/dist/tracer.d.ts +9 -0
- package/dist/tracer.js +10 -0
- package/dist/types.d.ts +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -194,6 +194,9 @@ app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
|
194
194
|
| **Stripe** | ✅ | ✅ | ✅ | ✅ | **Production** |
|
|
195
195
|
| **Peach Payments** | ✅ | ⛔ | ✅ | ✅ | **Production** |
|
|
196
196
|
| **Flutterwave** | ✅ | ✅ | ✅ | ✅ | **Production** |
|
|
197
|
+
| **Adyen** | ✅ | ⛔ | ✅ | ✅ | **Production** |
|
|
198
|
+
| **Mercado Pago** | ✅ | ✅ | ✅ | ✅ | **Production** |
|
|
199
|
+
| **Razorpay** | ✅ | ✅ | ✅ | ✅ | **Production** |
|
|
197
200
|
|
|
198
201
|
### Crypto on/off-ramp providers
|
|
199
202
|
|
|
@@ -207,7 +210,7 @@ app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
|
207
210
|
**Notes:**
|
|
208
211
|
- `⛔` 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.
|
|
209
212
|
- **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.
|
|
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`.
|
|
213
|
+
- **Sandbox testing.** PayFast / PayStack / Stripe / Peach / Flutterwave / Adyen / Mercado Pago / Razorpay 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`.
|
|
211
214
|
|
|
212
215
|
## Provider Configuration
|
|
213
216
|
|
|
@@ -258,6 +261,58 @@ const pay = new PayBridge({
|
|
|
258
261
|
|
|
259
262
|
**Docs:** [Ozow Hub](https://hub.ozow.com)
|
|
260
263
|
|
|
264
|
+
### Adyen
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const pay = new PayBridge({
|
|
268
|
+
provider: 'adyen',
|
|
269
|
+
credentials: {
|
|
270
|
+
apiKey: 'your_api_key',
|
|
271
|
+
merchantAccount: 'YourMerchantAccount',
|
|
272
|
+
liveUrlPrefix: 'abc123' // Only for live mode
|
|
273
|
+
},
|
|
274
|
+
sandbox: true,
|
|
275
|
+
webhookSecret: 'your_hmac_key_hex'
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Docs:** [Adyen API Explorer](https://docs.adyen.com/api-explorer/)
|
|
280
|
+
|
|
281
|
+
**Note:** Adyen subscriptions require recurring tokenization flow (not yet supported). Use Stripe or PayFast for subscriptions.
|
|
282
|
+
|
|
283
|
+
### Mercado Pago
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
const pay = new PayBridge({
|
|
287
|
+
provider: 'mercadopago',
|
|
288
|
+
credentials: {
|
|
289
|
+
apiKey: 'TEST-...' // Or APP_USR-... for live
|
|
290
|
+
},
|
|
291
|
+
sandbox: true,
|
|
292
|
+
webhookSecret: 'your_webhook_secret'
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Docs:** [Mercado Pago Developers](https://www.mercadopago.com/developers/en/reference)
|
|
297
|
+
|
|
298
|
+
### Razorpay
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
const pay = new PayBridge({
|
|
302
|
+
provider: 'razorpay',
|
|
303
|
+
credentials: {
|
|
304
|
+
apiKey: 'rzp_test_...', // key_id
|
|
305
|
+
secretKey: 'your_key_secret'
|
|
306
|
+
},
|
|
307
|
+
sandbox: true,
|
|
308
|
+
webhookSecret: 'your_webhook_secret'
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Docs:** [Razorpay API Reference](https://razorpay.com/docs/api)
|
|
313
|
+
|
|
314
|
+
**Note:** Razorpay webhooks do not include timestamp-based replay protection.
|
|
315
|
+
|
|
261
316
|
## Switch Providers in 1 Line
|
|
262
317
|
|
|
263
318
|
```typescript
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Supports pluggable storage for multi-instance deployments.
|
|
4
4
|
* Not atomic across processes — eventual consistency accepted.
|
|
5
5
|
*/
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
6
7
|
import { CircuitBreakerStore } from './circuit-breaker-store';
|
|
7
8
|
export declare enum CircuitState {
|
|
8
9
|
CLOSED = "CLOSED",
|
|
@@ -15,6 +16,7 @@ export interface CircuitBreakerConfig {
|
|
|
15
16
|
store?: CircuitBreakerStore;
|
|
16
17
|
}
|
|
17
18
|
export declare class CircuitBreaker {
|
|
19
|
+
readonly events: EventEmitter<any>;
|
|
18
20
|
private readonly key;
|
|
19
21
|
private readonly failureThreshold;
|
|
20
22
|
private readonly resetTimeoutMs;
|
package/dist/circuit-breaker.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.CircuitBreaker = exports.CircuitState = void 0;
|
|
9
|
+
const node_events_1 = require("node:events");
|
|
9
10
|
const circuit_breaker_store_1 = require("./circuit-breaker-store");
|
|
10
11
|
var CircuitState;
|
|
11
12
|
(function (CircuitState) {
|
|
@@ -15,6 +16,7 @@ var CircuitState;
|
|
|
15
16
|
})(CircuitState || (exports.CircuitState = CircuitState = {}));
|
|
16
17
|
class CircuitBreaker {
|
|
17
18
|
constructor(key = 'default', config = {}) {
|
|
19
|
+
this.events = new node_events_1.EventEmitter();
|
|
18
20
|
this.key = key;
|
|
19
21
|
this.failureThreshold = config.failureThreshold ?? 5;
|
|
20
22
|
this.resetTimeoutMs = config.resetTimeoutMs ?? 30000;
|
|
@@ -26,6 +28,7 @@ class CircuitBreaker {
|
|
|
26
28
|
if (Date.now() >= snapshot.nextAttemptTime) {
|
|
27
29
|
snapshot.state = CircuitState.HALF_OPEN;
|
|
28
30
|
await this.saveSnapshot(snapshot);
|
|
31
|
+
this.events.emit('half_opened', this.key);
|
|
29
32
|
return false;
|
|
30
33
|
}
|
|
31
34
|
return true;
|
|
@@ -33,12 +36,16 @@ class CircuitBreaker {
|
|
|
33
36
|
return false;
|
|
34
37
|
}
|
|
35
38
|
async recordSuccess() {
|
|
39
|
+
const prevSnapshot = await this.getSnapshot();
|
|
36
40
|
const snapshot = {
|
|
37
41
|
state: CircuitState.CLOSED,
|
|
38
42
|
failureCount: 0,
|
|
39
43
|
nextAttemptTime: 0,
|
|
40
44
|
};
|
|
41
45
|
await this.saveSnapshot(snapshot);
|
|
46
|
+
if (prevSnapshot.state !== CircuitState.CLOSED) {
|
|
47
|
+
this.events.emit('closed', this.key);
|
|
48
|
+
}
|
|
42
49
|
}
|
|
43
50
|
async recordFailure() {
|
|
44
51
|
const snapshot = await this.getSnapshot();
|
|
@@ -47,11 +54,13 @@ class CircuitBreaker {
|
|
|
47
54
|
snapshot.state = CircuitState.OPEN;
|
|
48
55
|
snapshot.nextAttemptTime = Date.now() + this.resetTimeoutMs;
|
|
49
56
|
await this.saveSnapshot(snapshot, this.resetTimeoutMs + 5000);
|
|
57
|
+
this.events.emit('opened', this.key);
|
|
50
58
|
}
|
|
51
59
|
else if (snapshot.failureCount >= this.failureThreshold) {
|
|
52
60
|
snapshot.state = CircuitState.OPEN;
|
|
53
61
|
snapshot.nextAttemptTime = Date.now() + this.resetTimeoutMs;
|
|
54
62
|
await this.saveSnapshot(snapshot, this.resetTimeoutMs + 5000);
|
|
63
|
+
this.events.emit('opened', this.key);
|
|
55
64
|
}
|
|
56
65
|
else {
|
|
57
66
|
await this.saveSnapshot(snapshot);
|
package/dist/crypto/router.d.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { CryptoRamp } from './index';
|
|
5
5
|
import { OnRampParams, OffRampParams, RampQuote, RampResult } from './types';
|
|
6
|
+
import { RouterEventEmitter } from '../router-events';
|
|
7
|
+
import type { LedgerStore } from '../ledger';
|
|
8
|
+
import type { TracerLike } from '../tracer';
|
|
6
9
|
export interface CryptoRampRouterConfig {
|
|
7
10
|
providers: Array<{
|
|
8
11
|
provider: CryptoRamp;
|
|
@@ -17,13 +20,18 @@ export interface CryptoRampRouterConfig {
|
|
|
17
20
|
allowExperimental?: boolean;
|
|
18
21
|
circuitBreakerStore?: import('../circuit-breaker-store').CircuitBreakerStore;
|
|
19
22
|
idempotencyStore?: import('../webhook-idempotency-store').IdempotencyStore;
|
|
23
|
+
ledger?: LedgerStore;
|
|
24
|
+
tracer?: TracerLike;
|
|
20
25
|
}
|
|
21
26
|
export declare class CryptoRampRouter {
|
|
27
|
+
readonly events: RouterEventEmitter;
|
|
22
28
|
private providers;
|
|
23
29
|
private strategy;
|
|
24
30
|
private fallback;
|
|
25
31
|
private allowExperimental;
|
|
26
32
|
private circuitBreakers;
|
|
33
|
+
private ledger?;
|
|
34
|
+
private tracer;
|
|
27
35
|
private rrIndex;
|
|
28
36
|
private config;
|
|
29
37
|
constructor(config: CryptoRampRouterConfig);
|
|
@@ -34,4 +42,5 @@ export declare class CryptoRampRouter {
|
|
|
34
42
|
private filterProviders;
|
|
35
43
|
private orderProviders;
|
|
36
44
|
private sleep;
|
|
45
|
+
private recordLedgerEntry;
|
|
37
46
|
}
|
package/dist/crypto/router.js
CHANGED
|
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.CryptoRampRouter = void 0;
|
|
7
7
|
const routing_types_1 = require("../routing-types");
|
|
8
8
|
const circuit_breaker_1 = require("../circuit-breaker");
|
|
9
|
+
const router_events_1 = require("../router-events");
|
|
10
|
+
const tracer_1 = require("../tracer");
|
|
9
11
|
function sanitizeErrorMessage(msg) {
|
|
10
12
|
if (!msg)
|
|
11
13
|
return 'unknown error';
|
|
@@ -16,6 +18,7 @@ function sanitizeErrorMessage(msg) {
|
|
|
16
18
|
}
|
|
17
19
|
class CryptoRampRouter {
|
|
18
20
|
constructor(config) {
|
|
21
|
+
this.events = new router_events_1.RouterEventEmitter();
|
|
19
22
|
this.rrIndex = 0;
|
|
20
23
|
this.config = config;
|
|
21
24
|
this.providers = config.providers.map(p => ({
|
|
@@ -29,12 +32,36 @@ class CryptoRampRouter {
|
|
|
29
32
|
retryDelayMs: config.fallback?.retryDelayMs ?? 250,
|
|
30
33
|
};
|
|
31
34
|
this.allowExperimental = config.allowExperimental ?? false;
|
|
35
|
+
this.ledger = config.ledger;
|
|
36
|
+
this.tracer = config.tracer ?? tracer_1.noopTracer;
|
|
32
37
|
this.circuitBreakers = new Map();
|
|
33
38
|
for (const p of this.providers) {
|
|
34
39
|
const name = p.instance.getProviderName();
|
|
35
|
-
|
|
40
|
+
const breaker = new circuit_breaker_1.CircuitBreaker(name, {
|
|
36
41
|
store: config.circuitBreakerStore,
|
|
37
|
-
})
|
|
42
|
+
});
|
|
43
|
+
breaker.events.on('opened', (key) => {
|
|
44
|
+
this.events.emitEvent({
|
|
45
|
+
type: 'circuit.opened',
|
|
46
|
+
provider: key,
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
breaker.events.on('half_opened', (key) => {
|
|
51
|
+
this.events.emitEvent({
|
|
52
|
+
type: 'circuit.half_opened',
|
|
53
|
+
provider: key,
|
|
54
|
+
timestamp: new Date().toISOString(),
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
breaker.events.on('closed', (key) => {
|
|
58
|
+
this.events.emitEvent({
|
|
59
|
+
type: 'circuit.closed',
|
|
60
|
+
provider: key,
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
this.circuitBreakers.set(name, breaker);
|
|
38
65
|
}
|
|
39
66
|
}
|
|
40
67
|
async getQuote(direction, fiatAmount, fiatCurrency, cryptoAsset, network) {
|
|
@@ -114,6 +141,13 @@ class CryptoRampRouter {
|
|
|
114
141
|
continue;
|
|
115
142
|
}
|
|
116
143
|
const startTime = Date.now();
|
|
144
|
+
this.events.emitEvent({
|
|
145
|
+
type: 'attempt.start',
|
|
146
|
+
provider: providerName,
|
|
147
|
+
operation: 'createOnRamp',
|
|
148
|
+
attempt: attempts.length + 1,
|
|
149
|
+
timestamp: new Date().toISOString(),
|
|
150
|
+
});
|
|
117
151
|
try {
|
|
118
152
|
const result = await providerMeta.instance.createOnRamp(params);
|
|
119
153
|
const latencyMs = Date.now() - startTime;
|
|
@@ -124,6 +158,32 @@ class CryptoRampRouter {
|
|
|
124
158
|
status: 'success',
|
|
125
159
|
latencyMs,
|
|
126
160
|
});
|
|
161
|
+
this.events.emitEvent({
|
|
162
|
+
type: 'attempt.success',
|
|
163
|
+
provider: providerName,
|
|
164
|
+
operation: 'createOnRamp',
|
|
165
|
+
durationMs: latencyMs,
|
|
166
|
+
attempt: attempts.length,
|
|
167
|
+
timestamp: new Date().toISOString(),
|
|
168
|
+
});
|
|
169
|
+
this.events.emitEvent({
|
|
170
|
+
type: 'request.success',
|
|
171
|
+
provider: providerName,
|
|
172
|
+
operation: 'createOnRamp',
|
|
173
|
+
durationMs: latencyMs,
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
});
|
|
176
|
+
await this.recordLedgerEntry({
|
|
177
|
+
id: `${providerName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
178
|
+
timestamp: new Date().toISOString(),
|
|
179
|
+
operation: 'createOnRamp',
|
|
180
|
+
provider: providerName,
|
|
181
|
+
providerId: result.id,
|
|
182
|
+
status: 'success',
|
|
183
|
+
amount: params.fiatAmount,
|
|
184
|
+
currency: params.fiatCurrency,
|
|
185
|
+
durationMs: latencyMs,
|
|
186
|
+
});
|
|
127
187
|
const routingMeta = {
|
|
128
188
|
attempts,
|
|
129
189
|
chosenProvider: providerName,
|
|
@@ -146,12 +206,28 @@ class CryptoRampRouter {
|
|
|
146
206
|
errorMessage: sanitizeErrorMessage(error.message),
|
|
147
207
|
latencyMs,
|
|
148
208
|
});
|
|
209
|
+
this.events.emitEvent({
|
|
210
|
+
type: 'attempt.failure',
|
|
211
|
+
provider: providerName,
|
|
212
|
+
operation: 'createOnRamp',
|
|
213
|
+
durationMs: latencyMs,
|
|
214
|
+
errorCode: error.code,
|
|
215
|
+
errorMessage: sanitizeErrorMessage(error.message),
|
|
216
|
+
attempt: attempts.length,
|
|
217
|
+
timestamp: new Date().toISOString(),
|
|
218
|
+
});
|
|
149
219
|
if (!this.fallback.enabled || attempts.length >= this.fallback.maxAttempts) {
|
|
150
220
|
break;
|
|
151
221
|
}
|
|
152
222
|
await this.sleep(this.fallback.retryDelayMs);
|
|
153
223
|
}
|
|
154
224
|
}
|
|
225
|
+
this.events.emitEvent({
|
|
226
|
+
type: 'request.failure',
|
|
227
|
+
operation: 'createOnRamp',
|
|
228
|
+
errorMessage: lastError?.message || 'All providers failed',
|
|
229
|
+
timestamp: new Date().toISOString(),
|
|
230
|
+
});
|
|
155
231
|
throw new routing_types_1.RoutingError(`All providers failed: ${lastError?.message || 'Unknown error'}`, attempts);
|
|
156
232
|
}
|
|
157
233
|
async createOffRamp(params) {
|
|
@@ -178,6 +254,13 @@ class CryptoRampRouter {
|
|
|
178
254
|
continue;
|
|
179
255
|
}
|
|
180
256
|
const startTime = Date.now();
|
|
257
|
+
this.events.emitEvent({
|
|
258
|
+
type: 'attempt.start',
|
|
259
|
+
provider: providerName,
|
|
260
|
+
operation: 'createOffRamp',
|
|
261
|
+
attempt: attempts.length + 1,
|
|
262
|
+
timestamp: new Date().toISOString(),
|
|
263
|
+
});
|
|
181
264
|
try {
|
|
182
265
|
const result = await providerMeta.instance.createOffRamp(params);
|
|
183
266
|
const latencyMs = Date.now() - startTime;
|
|
@@ -188,6 +271,32 @@ class CryptoRampRouter {
|
|
|
188
271
|
status: 'success',
|
|
189
272
|
latencyMs,
|
|
190
273
|
});
|
|
274
|
+
this.events.emitEvent({
|
|
275
|
+
type: 'attempt.success',
|
|
276
|
+
provider: providerName,
|
|
277
|
+
operation: 'createOffRamp',
|
|
278
|
+
durationMs: latencyMs,
|
|
279
|
+
attempt: attempts.length,
|
|
280
|
+
timestamp: new Date().toISOString(),
|
|
281
|
+
});
|
|
282
|
+
this.events.emitEvent({
|
|
283
|
+
type: 'request.success',
|
|
284
|
+
provider: providerName,
|
|
285
|
+
operation: 'createOffRamp',
|
|
286
|
+
durationMs: latencyMs,
|
|
287
|
+
timestamp: new Date().toISOString(),
|
|
288
|
+
});
|
|
289
|
+
await this.recordLedgerEntry({
|
|
290
|
+
id: `${providerName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
291
|
+
timestamp: new Date().toISOString(),
|
|
292
|
+
operation: 'createOffRamp',
|
|
293
|
+
provider: providerName,
|
|
294
|
+
providerId: result.id,
|
|
295
|
+
status: 'success',
|
|
296
|
+
amount: params.cryptoAmount,
|
|
297
|
+
currency: params.fiatCurrency,
|
|
298
|
+
durationMs: latencyMs,
|
|
299
|
+
});
|
|
191
300
|
const routingMeta = {
|
|
192
301
|
attempts,
|
|
193
302
|
chosenProvider: providerName,
|
|
@@ -210,12 +319,28 @@ class CryptoRampRouter {
|
|
|
210
319
|
errorMessage: sanitizeErrorMessage(error.message),
|
|
211
320
|
latencyMs,
|
|
212
321
|
});
|
|
322
|
+
this.events.emitEvent({
|
|
323
|
+
type: 'attempt.failure',
|
|
324
|
+
provider: providerName,
|
|
325
|
+
operation: 'createOffRamp',
|
|
326
|
+
durationMs: latencyMs,
|
|
327
|
+
errorCode: error.code,
|
|
328
|
+
errorMessage: sanitizeErrorMessage(error.message),
|
|
329
|
+
attempt: attempts.length,
|
|
330
|
+
timestamp: new Date().toISOString(),
|
|
331
|
+
});
|
|
213
332
|
if (!this.fallback.enabled || attempts.length >= this.fallback.maxAttempts) {
|
|
214
333
|
break;
|
|
215
334
|
}
|
|
216
335
|
await this.sleep(this.fallback.retryDelayMs);
|
|
217
336
|
}
|
|
218
337
|
}
|
|
338
|
+
this.events.emitEvent({
|
|
339
|
+
type: 'request.failure',
|
|
340
|
+
operation: 'createOffRamp',
|
|
341
|
+
errorMessage: lastError?.message || 'All providers failed',
|
|
342
|
+
timestamp: new Date().toISOString(),
|
|
343
|
+
});
|
|
219
344
|
throw new routing_types_1.RoutingError(`All providers failed: ${lastError?.message || 'Unknown error'}`, attempts);
|
|
220
345
|
}
|
|
221
346
|
async getRamp(id, provider) {
|
|
@@ -291,5 +416,21 @@ class CryptoRampRouter {
|
|
|
291
416
|
sleep(ms) {
|
|
292
417
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
293
418
|
}
|
|
419
|
+
async recordLedgerEntry(entry) {
|
|
420
|
+
if (!this.ledger)
|
|
421
|
+
return;
|
|
422
|
+
try {
|
|
423
|
+
await this.ledger.append(entry);
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
this.events.emitEvent({
|
|
427
|
+
type: 'attempt.failure',
|
|
428
|
+
operation: entry.operation,
|
|
429
|
+
provider: entry.provider,
|
|
430
|
+
errorMessage: 'Ledger write failed',
|
|
431
|
+
timestamp: new Date().toISOString(),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
294
435
|
}
|
|
295
436
|
exports.CryptoRampRouter = CryptoRampRouter;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,8 +16,12 @@ export * from './strategies';
|
|
|
16
16
|
export * from './router';
|
|
17
17
|
export * from './crypto';
|
|
18
18
|
export * from './webhook-idempotency-store';
|
|
19
|
+
export * from './router-events';
|
|
20
|
+
export * from './ledger';
|
|
21
|
+
export * from './tracer';
|
|
19
22
|
export { createRedisCircuitBreakerStore, type RedisLike, type RedisStoreOptions } from './stores/redis';
|
|
20
23
|
export { createRedisIdempotencyStore, type RedisIdempotencyStoreOptions } from './stores/redis-idempotency';
|
|
24
|
+
export { createRedisLedgerStore, type RedisLedgerStoreOptions } from './stores/redis-ledger';
|
|
21
25
|
export declare class PayBridge {
|
|
22
26
|
readonly provider: PaymentProvider;
|
|
23
27
|
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.createRedisIdempotencyStore = exports.createRedisCircuitBreakerStore = void 0;
|
|
23
|
+
exports.PayBridge = exports.createRedisLedgerStore = 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");
|
|
@@ -29,6 +29,9 @@ const stripe_1 = require("./providers/stripe");
|
|
|
29
29
|
const payfast_1 = require("./providers/payfast");
|
|
30
30
|
const paystack_1 = require("./providers/paystack");
|
|
31
31
|
const flutterwave_1 = require("./providers/flutterwave");
|
|
32
|
+
const adyen_1 = require("./providers/adyen");
|
|
33
|
+
const mercadopago_1 = require("./providers/mercadopago");
|
|
34
|
+
const razorpay_1 = require("./providers/razorpay");
|
|
32
35
|
__exportStar(require("./types"), exports);
|
|
33
36
|
__exportStar(require("./utils/currency"), exports);
|
|
34
37
|
__exportStar(require("./utils/fetch"), exports);
|
|
@@ -39,10 +42,15 @@ __exportStar(require("./strategies"), exports);
|
|
|
39
42
|
__exportStar(require("./router"), exports);
|
|
40
43
|
__exportStar(require("./crypto"), exports);
|
|
41
44
|
__exportStar(require("./webhook-idempotency-store"), exports);
|
|
45
|
+
__exportStar(require("./router-events"), exports);
|
|
46
|
+
__exportStar(require("./ledger"), exports);
|
|
47
|
+
__exportStar(require("./tracer"), exports);
|
|
42
48
|
var redis_1 = require("./stores/redis");
|
|
43
49
|
Object.defineProperty(exports, "createRedisCircuitBreakerStore", { enumerable: true, get: function () { return redis_1.createRedisCircuitBreakerStore; } });
|
|
44
50
|
var redis_idempotency_1 = require("./stores/redis-idempotency");
|
|
45
51
|
Object.defineProperty(exports, "createRedisIdempotencyStore", { enumerable: true, get: function () { return redis_idempotency_1.createRedisIdempotencyStore; } });
|
|
52
|
+
var redis_ledger_1 = require("./stores/redis-ledger");
|
|
53
|
+
Object.defineProperty(exports, "createRedisLedgerStore", { enumerable: true, get: function () { return redis_ledger_1.createRedisLedgerStore; } });
|
|
46
54
|
class PayBridge {
|
|
47
55
|
constructor(config) {
|
|
48
56
|
this.provider = this.createProvider(config);
|
|
@@ -130,6 +138,36 @@ class PayBridge {
|
|
|
130
138
|
sandbox,
|
|
131
139
|
webhookSecret,
|
|
132
140
|
});
|
|
141
|
+
case 'adyen':
|
|
142
|
+
if (!credentials.apiKey || !credentials.merchantAccount) {
|
|
143
|
+
throw new Error('Adyen requires apiKey and merchantAccount');
|
|
144
|
+
}
|
|
145
|
+
return new adyen_1.AdyenProvider({
|
|
146
|
+
apiKey: credentials.apiKey,
|
|
147
|
+
merchantAccount: credentials.merchantAccount,
|
|
148
|
+
liveUrlPrefix: credentials.liveUrlPrefix,
|
|
149
|
+
sandbox,
|
|
150
|
+
webhookSecret,
|
|
151
|
+
});
|
|
152
|
+
case 'mercadopago':
|
|
153
|
+
if (!credentials.apiKey) {
|
|
154
|
+
throw new Error('Mercado Pago requires apiKey (access token TEST-* or APP_USR-*)');
|
|
155
|
+
}
|
|
156
|
+
return new mercadopago_1.MercadoPagoProvider({
|
|
157
|
+
accessToken: credentials.apiKey,
|
|
158
|
+
sandbox,
|
|
159
|
+
webhookSecret,
|
|
160
|
+
});
|
|
161
|
+
case 'razorpay':
|
|
162
|
+
if (!credentials.apiKey || !credentials.secretKey) {
|
|
163
|
+
throw new Error('Razorpay requires apiKey (key_id rzp_test_* or rzp_live_*) and secretKey (key_secret)');
|
|
164
|
+
}
|
|
165
|
+
return new razorpay_1.RazorpayProvider({
|
|
166
|
+
keyId: credentials.apiKey,
|
|
167
|
+
keySecret: credentials.secretKey,
|
|
168
|
+
sandbox,
|
|
169
|
+
webhookSecret,
|
|
170
|
+
});
|
|
133
171
|
default:
|
|
134
172
|
throw new Error(`Unknown provider: ${provider}`);
|
|
135
173
|
}
|
package/dist/ledger.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface LedgerEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
timestamp: string;
|
|
4
|
+
operation: 'createPayment' | 'createSubscription' | 'getPayment' | 'refund' | 'createOnRamp' | 'createOffRamp' | 'getRamp' | 'parseWebhook';
|
|
5
|
+
provider: string;
|
|
6
|
+
reference?: string;
|
|
7
|
+
providerId?: string;
|
|
8
|
+
status: 'attempted' | 'success' | 'failed' | 'rate_limited' | 'timeout';
|
|
9
|
+
amount?: number;
|
|
10
|
+
currency?: string;
|
|
11
|
+
durationMs?: number;
|
|
12
|
+
errorCode?: string;
|
|
13
|
+
errorMessage?: string;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface LedgerQuery {
|
|
17
|
+
reference?: string;
|
|
18
|
+
provider?: string;
|
|
19
|
+
status?: LedgerEntry['status'];
|
|
20
|
+
fromTimestamp?: string;
|
|
21
|
+
toTimestamp?: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface LedgerStore {
|
|
25
|
+
append(entry: LedgerEntry): Promise<void>;
|
|
26
|
+
query(filter: LedgerQuery): Promise<LedgerEntry[]>;
|
|
27
|
+
}
|
|
28
|
+
export declare class InMemoryLedgerStore implements LedgerStore {
|
|
29
|
+
private entries;
|
|
30
|
+
private maxSize;
|
|
31
|
+
constructor(opts?: {
|
|
32
|
+
maxSize?: number;
|
|
33
|
+
});
|
|
34
|
+
append(entry: LedgerEntry): Promise<void>;
|
|
35
|
+
query(filter: LedgerQuery): Promise<LedgerEntry[]>;
|
|
36
|
+
}
|
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryLedgerStore = void 0;
|
|
4
|
+
class InMemoryLedgerStore {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
this.entries = [];
|
|
7
|
+
this.maxSize = opts.maxSize ?? 10000;
|
|
8
|
+
}
|
|
9
|
+
async append(entry) {
|
|
10
|
+
this.entries.push(entry);
|
|
11
|
+
if (this.entries.length > this.maxSize) {
|
|
12
|
+
this.entries.splice(0, this.entries.length - this.maxSize);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async query(filter) {
|
|
16
|
+
let results = this.entries;
|
|
17
|
+
if (filter.reference)
|
|
18
|
+
results = results.filter(e => e.reference === filter.reference);
|
|
19
|
+
if (filter.provider)
|
|
20
|
+
results = results.filter(e => e.provider === filter.provider);
|
|
21
|
+
if (filter.status)
|
|
22
|
+
results = results.filter(e => e.status === filter.status);
|
|
23
|
+
if (filter.fromTimestamp)
|
|
24
|
+
results = results.filter(e => e.timestamp >= filter.fromTimestamp);
|
|
25
|
+
if (filter.toTimestamp)
|
|
26
|
+
results = results.filter(e => e.timestamp <= filter.toTimestamp);
|
|
27
|
+
if (filter.limit)
|
|
28
|
+
results = results.slice(0, filter.limit);
|
|
29
|
+
return [...results];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.InMemoryLedgerStore = InMemoryLedgerStore;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adyen payment provider
|
|
3
|
+
* Global payment platform supporting 150+ countries
|
|
4
|
+
* @see https://docs.adyen.com/api-explorer/Checkout/71/overview
|
|
5
|
+
*/
|
|
6
|
+
import { PaymentProvider } from './base';
|
|
7
|
+
import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent } from '../types';
|
|
8
|
+
import { ProviderCapabilities } from '../routing-types';
|
|
9
|
+
interface AdyenConfig {
|
|
10
|
+
apiKey: string;
|
|
11
|
+
merchantAccount: string;
|
|
12
|
+
liveUrlPrefix?: string;
|
|
13
|
+
webhookSecret?: string;
|
|
14
|
+
sandbox?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class AdyenProvider extends PaymentProvider {
|
|
17
|
+
readonly name = "adyen";
|
|
18
|
+
readonly supportedCurrencies: string[];
|
|
19
|
+
private apiKey;
|
|
20
|
+
private merchantAccount;
|
|
21
|
+
private liveUrlPrefix?;
|
|
22
|
+
private webhookSecret?;
|
|
23
|
+
private sandbox;
|
|
24
|
+
private baseUrl;
|
|
25
|
+
constructor(config: AdyenConfig);
|
|
26
|
+
private apiRequest;
|
|
27
|
+
createPayment(params: CreatePaymentParams): Promise<PaymentResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Adyen subscriptions require recurring tokenization flow (shopperReference + recurring contract).
|
|
30
|
+
* This is not yet supported by paybridge's simple checkout URL model.
|
|
31
|
+
* Use Stripe or PayFast for subscriptions.
|
|
32
|
+
*/
|
|
33
|
+
createSubscription(_params: CreateSubscriptionParams): Promise<SubscriptionResult>;
|
|
34
|
+
getPayment(id: string): Promise<PaymentResult>;
|
|
35
|
+
refund(params: RefundParams): Promise<RefundResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Parse Adyen webhook notification.
|
|
38
|
+
* Note: Adyen webhooks contain multiple notificationItems in a batch.
|
|
39
|
+
* This method returns only the FIRST item (paybridge webhook interface is single-event).
|
|
40
|
+
* Multi-event batches require custom handling outside paybridge.
|
|
41
|
+
*/
|
|
42
|
+
parseWebhook(body: any, _headers?: any): WebhookEvent;
|
|
43
|
+
verifyWebhook(body: string | Buffer, _headers?: any): boolean;
|
|
44
|
+
getCapabilities(): ProviderCapabilities;
|
|
45
|
+
}
|
|
46
|
+
export {};
|