@avvio/payments 0.1.0 → 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/CHANGELOG.md +190 -5
- package/ERRORS.md +120 -58
- package/QUICKSTART.md +162 -54
- package/README.md +136 -37
- package/index.d.ts +801 -82
- package/package.json +11 -5
- package/src/cli.js +204 -20
- package/src/client.js +576 -58
- package/src/mcp.js +279 -29
package/src/client.js
CHANGED
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
* being "nothing" is worth more than any convenience a library would buy.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
createHash,
|
|
15
|
+
createPrivateKey,
|
|
16
|
+
createSign,
|
|
17
|
+
generateKeyPairSync,
|
|
18
|
+
randomUUID,
|
|
19
|
+
} = require('node:crypto');
|
|
14
20
|
|
|
15
21
|
/** Thrown for any non-2xx. Carries enough to act on without parsing prose. */
|
|
16
22
|
class PayoutsError extends Error {
|
|
@@ -142,7 +148,9 @@ function stableKey(...parts) {
|
|
|
142
148
|
undefined,
|
|
143
149
|
);
|
|
144
150
|
}
|
|
145
|
-
const empty = parts.findIndex(
|
|
151
|
+
const empty = parts.findIndex(
|
|
152
|
+
(p) => p === undefined || p === null || p === '',
|
|
153
|
+
);
|
|
146
154
|
if (empty !== -1) {
|
|
147
155
|
throw new PayoutsError(
|
|
148
156
|
400,
|
|
@@ -193,7 +201,7 @@ function backoffMs(attempt, retryAfterSeconds) {
|
|
|
193
201
|
class PayoutsClient {
|
|
194
202
|
/**
|
|
195
203
|
* @param {object} opts
|
|
196
|
-
* @param {string} opts.apiKey
|
|
204
|
+
* @param {string} opts.apiKey Complete `avvio_live_*` or `avvio_test_*` bearer key.
|
|
197
205
|
* @param {string} opts.orgId Your organization id.
|
|
198
206
|
* @param {string} [opts.baseUrl] Defaults to the AVVIO_BASE_URL env var.
|
|
199
207
|
* @param {number} [opts.timeoutMs]
|
|
@@ -201,6 +209,8 @@ class PayoutsClient {
|
|
|
201
209
|
constructor(opts = {}) {
|
|
202
210
|
this.apiKey = opts.apiKey || process.env.AVVIO_API_KEY;
|
|
203
211
|
this.orgId = opts.orgId || process.env.AVVIO_ORG_ID;
|
|
212
|
+
// PARKED request-signing setup. Restore with the signing block in `_send`.
|
|
213
|
+
// const privatePem = opts.privateKeyPem || process.env.AVVIO_PRIVATE_KEY;
|
|
204
214
|
// `business/api/v1`, NOT `api/v1`. The bare `api/v1` prefix on this host is
|
|
205
215
|
// the CONSUMER backend that the mobile app talks to — a different service.
|
|
206
216
|
// Cloudflare routes only `/business/*` to this one. Worse than a 404: the
|
|
@@ -211,7 +221,12 @@ class PayoutsClient {
|
|
|
211
221
|
opts.baseUrl ||
|
|
212
222
|
process.env.AVVIO_BASE_URL ||
|
|
213
223
|
'https://api.avvio.xyz/business/api/v1'
|
|
214
|
-
)
|
|
224
|
+
);
|
|
225
|
+
// Trailing slashes, trimmed without `/\/+$/`: that pattern is quadratic on
|
|
226
|
+
// a long run of slashes, and this value can come from an env var.
|
|
227
|
+
let baseEnd = this.baseUrl.length;
|
|
228
|
+
while (baseEnd > 0 && this.baseUrl[baseEnd - 1] === '/') baseEnd--;
|
|
229
|
+
this.baseUrl = this.baseUrl.slice(0, baseEnd);
|
|
215
230
|
this.timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
216
231
|
// Attempts AFTER the first, so 2 means up to three calls. Set 0 to disable.
|
|
217
232
|
this.maxRetries = opts.maxRetries === undefined ? 2 : opts.maxRetries;
|
|
@@ -226,18 +241,32 @@ class PayoutsClient {
|
|
|
226
241
|
'Missing organization id. Set AVVIO_ORG_ID, or pass { orgId } to the client.',
|
|
227
242
|
);
|
|
228
243
|
}
|
|
244
|
+
if (
|
|
245
|
+
!/^avvio_(?:live|test)_[0-9a-f]{32}_[A-Za-z0-9_-]{43}$/.test(this.apiKey)
|
|
246
|
+
) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
'Unsupported API credential. Copy the complete avvio_live_* or avvio_test_* API key shown at issuance.',
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
// PARKED request-signing constructor behavior. The bearer API key is the
|
|
252
|
+
// complete credential; AVVIO_PRIVATE_KEY is intentionally ignored.
|
|
253
|
+
// if (!privatePem) {
|
|
254
|
+
// throw new Error(
|
|
255
|
+
// 'This signed API key id requires a private signing key. Set AVVIO_PRIVATE_KEY, or pass { privateKeyPem } to the client.',
|
|
256
|
+
// );
|
|
257
|
+
// }
|
|
258
|
+
// this.privateKey = privatePem ? createPrivateKey(privatePem) : null;
|
|
229
259
|
|
|
230
260
|
/**
|
|
231
261
|
* `'test'` or `'live'`, from the key prefix — so a test suite can assert it
|
|
232
262
|
* is not about to pay real people, before it sends anything.
|
|
233
263
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* again, because two places deciding is two places to get it wrong.
|
|
264
|
+
* The constructor has already rejected every other credential shape, so
|
|
265
|
+
* this cannot silently interpret an unknown value as live. `doctor` reads
|
|
266
|
+
* this rather than deciding again, because two places deciding is two
|
|
267
|
+
* places to get it wrong.
|
|
239
268
|
*/
|
|
240
|
-
this.mode = this.apiKey.startsWith('
|
|
269
|
+
this.mode = this.apiKey.startsWith('avvio_test_') ? 'test' : 'live';
|
|
241
270
|
}
|
|
242
271
|
|
|
243
272
|
// ─── transport ───
|
|
@@ -254,16 +283,20 @@ class PayoutsClient {
|
|
|
254
283
|
* as prose.
|
|
255
284
|
*
|
|
256
285
|
* The rule: a mutation may only be retried while REUSING its idempotency key.
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
286
|
+
* Every mutation carries one — supplied by the caller, or minted here once
|
|
287
|
+
* per logical call and reused across its attempts — because the server
|
|
288
|
+
* honours the header on every POST, PATCH and DELETE. So a retry reuses the
|
|
289
|
+
* key by construction and cannot become a second payment; a mutation that
|
|
290
|
+
* somehow has no key is never retried, because nothing makes it safe.
|
|
261
291
|
*
|
|
262
292
|
* `DUPLICATE_REQUEST_DETECTED` is never retried at any level: it means an
|
|
263
293
|
* identical body arrived under a DIFFERENT key, and retrying is exactly the
|
|
264
294
|
* behaviour it exists to refuse.
|
|
265
295
|
*/
|
|
266
296
|
async request(method, path, opts = {}) {
|
|
297
|
+
if (method !== 'GET' && !opts.idempotencyKey) {
|
|
298
|
+
opts = { ...opts, idempotencyKey: randomUUID() };
|
|
299
|
+
}
|
|
267
300
|
const idempotent = method === 'GET' || Boolean(opts.idempotencyKey);
|
|
268
301
|
let attempt = 0;
|
|
269
302
|
|
|
@@ -286,7 +319,11 @@ class PayoutsClient {
|
|
|
286
319
|
}
|
|
287
320
|
}
|
|
288
321
|
|
|
289
|
-
async _send(
|
|
322
|
+
async _send(
|
|
323
|
+
method,
|
|
324
|
+
path,
|
|
325
|
+
{ body, idempotencyKey, query, headers: extra } = {},
|
|
326
|
+
) {
|
|
290
327
|
// Every failure from a mutation carries the key it used. Without this, the
|
|
291
328
|
// instruction "retry with the same idempotency key" names a value the
|
|
292
329
|
// caller has no way to obtain — which is worse than saying nothing, because
|
|
@@ -308,6 +345,30 @@ class PayoutsClient {
|
|
|
308
345
|
if (idempotencyKey) headers['idempotency-key'] = idempotencyKey;
|
|
309
346
|
Object.assign(headers, extra || {});
|
|
310
347
|
|
|
348
|
+
// PARKED request-signing block. Restore only with server verification and
|
|
349
|
+
// the matching public documentation.
|
|
350
|
+
// if (this.privateKey) {
|
|
351
|
+
// const serialized = body ? JSON.stringify(body) : '';
|
|
352
|
+
// const timestamp = String(Math.floor(Date.now() / 1000));
|
|
353
|
+
// const nonce = randomUUID();
|
|
354
|
+
// const canonical = [
|
|
355
|
+
// this.apiKey,
|
|
356
|
+
// method.toUpperCase(),
|
|
357
|
+
// url.pathname + url.search,
|
|
358
|
+
// createHash('sha256').update(serialized, 'utf8').digest('hex'),
|
|
359
|
+
// idempotencyKey || '',
|
|
360
|
+
// timestamp,
|
|
361
|
+
// nonce,
|
|
362
|
+
// ].join('\n');
|
|
363
|
+
// headers['x-anzo-signature'] = createSign('sha256')
|
|
364
|
+
// .update(canonical)
|
|
365
|
+
// .end()
|
|
366
|
+
// .sign(this.privateKey)
|
|
367
|
+
// .toString('base64');
|
|
368
|
+
// headers['x-anzo-timestamp'] = timestamp;
|
|
369
|
+
// headers['x-anzo-nonce'] = nonce;
|
|
370
|
+
// }
|
|
371
|
+
|
|
311
372
|
let res;
|
|
312
373
|
try {
|
|
313
374
|
res = await fetch(url, {
|
|
@@ -344,6 +405,13 @@ class PayoutsClient {
|
|
|
344
405
|
parsed = { message: text.slice(0, 500) };
|
|
345
406
|
}
|
|
346
407
|
|
|
408
|
+
// A generic HTTP client considers every 2xx successful. This payload is
|
|
409
|
+
// authentication work, not a payout response, so fail closed if a signed
|
|
410
|
+
// client ever receives one instead of quietly handing it to business code.
|
|
411
|
+
if (res.status === 202 && parsed?.type === 'SIGNATURE_CHALLENGE') {
|
|
412
|
+
throw tag(new PayoutsError(res.status, parsed, requestId));
|
|
413
|
+
}
|
|
414
|
+
|
|
347
415
|
if (!res.ok) {
|
|
348
416
|
const err = new PayoutsError(res.status, parsed, requestId);
|
|
349
417
|
// The server tells us when its window resets; guessing is worse. Only
|
|
@@ -363,16 +431,22 @@ class PayoutsClient {
|
|
|
363
431
|
if (replayed && parsed && typeof parsed === 'object') {
|
|
364
432
|
// Surfaced so a caller reconciling a retry storm can tell "we already did
|
|
365
433
|
// this" from "we just did this".
|
|
366
|
-
Object.defineProperty(parsed, 'replayed', {
|
|
434
|
+
Object.defineProperty(parsed, 'replayed', {
|
|
435
|
+
value: true,
|
|
436
|
+
enumerable: false,
|
|
437
|
+
});
|
|
367
438
|
}
|
|
368
439
|
return parsed;
|
|
369
440
|
}
|
|
370
441
|
|
|
371
442
|
// ─── discovery ───
|
|
372
443
|
|
|
373
|
-
/** Every currency you can pay out to,
|
|
374
|
-
corridors() {
|
|
375
|
-
|
|
444
|
+
/** Every currency you can pay out to, or the exact routed currency requested. */
|
|
445
|
+
corridors(currency) {
|
|
446
|
+
const query = currency
|
|
447
|
+
? `?currency=${encodeURIComponent(String(currency).toUpperCase())}`
|
|
448
|
+
: '';
|
|
449
|
+
return this.request('GET', `/recipients/${this.orgId}/corridors${query}`);
|
|
376
450
|
}
|
|
377
451
|
|
|
378
452
|
/**
|
|
@@ -382,8 +456,8 @@ class PayoutsClient {
|
|
|
382
456
|
* depend on how your organization is routed, and we may re-route you.
|
|
383
457
|
*/
|
|
384
458
|
async requirements(currency) {
|
|
385
|
-
const { corridors } = await this.corridors();
|
|
386
459
|
const want = String(currency).toUpperCase();
|
|
460
|
+
const { corridors } = await this.corridors(want);
|
|
387
461
|
const match = (corridors || []).find(
|
|
388
462
|
(c) => String(c.currency || '').toUpperCase() === want,
|
|
389
463
|
);
|
|
@@ -405,6 +479,21 @@ class PayoutsClient {
|
|
|
405
479
|
return match;
|
|
406
480
|
}
|
|
407
481
|
|
|
482
|
+
/**
|
|
483
|
+
* The stated payment reasons this organization may use.
|
|
484
|
+
*
|
|
485
|
+
* Some corridors require one — `purposeOfPayment` on a payout, `paymentReason`
|
|
486
|
+
* on a quote you accept — and this is the list they are validated against.
|
|
487
|
+
* Read it rather than guessing: a rejected value is a 400 on a payout you have
|
|
488
|
+
* already promised somebody.
|
|
489
|
+
*/
|
|
490
|
+
paymentReasons() {
|
|
491
|
+
return this.request(
|
|
492
|
+
'GET',
|
|
493
|
+
`/payments/organizations/${this.orgId}/payment-reasons`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
408
497
|
// ─── pricing ───
|
|
409
498
|
|
|
410
499
|
/**
|
|
@@ -454,6 +543,99 @@ class PayoutsClient {
|
|
|
454
543
|
});
|
|
455
544
|
}
|
|
456
545
|
|
|
546
|
+
/** One beneficiary, by the id we returned. */
|
|
547
|
+
getBeneficiary(recipientId) {
|
|
548
|
+
return this.request(
|
|
549
|
+
'GET',
|
|
550
|
+
`/recipients/${this.orgId}/${encodeURIComponent(recipientId)}`,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* One beneficiary, by YOUR id for them.
|
|
556
|
+
*
|
|
557
|
+
* `externalId` was already the key that makes creation idempotent; this reads
|
|
558
|
+
* it back, so "worker 4471" resolves to a `destinationAccountId` in one call
|
|
559
|
+
* rather than paging your whole book and filtering client-side. Unique per
|
|
560
|
+
* organization, so this is exactly one beneficiary or a 404.
|
|
561
|
+
*/
|
|
562
|
+
getBeneficiaryByExternalId(externalId) {
|
|
563
|
+
return this.request(
|
|
564
|
+
'GET',
|
|
565
|
+
`/recipients/${this.orgId}/external/${encodeURIComponent(externalId)}`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Correct a beneficiary's own details: name, email, phone, country, and
|
|
571
|
+
* whether they are an individual or a business.
|
|
572
|
+
*
|
|
573
|
+
* **The bank account behind a payment method is not editable, by design.**
|
|
574
|
+
* The rail validated that account, and swapping it underneath would send the
|
|
575
|
+
* next payout somewhere you never registered. A wrong account is a NEW
|
|
576
|
+
* payment method, and the old one is deleted.
|
|
577
|
+
*/
|
|
578
|
+
updateBeneficiary(recipientId, patch = {}, { idempotencyKey } = {}) {
|
|
579
|
+
return this.request(
|
|
580
|
+
'PATCH',
|
|
581
|
+
`/recipients/${this.orgId}/${encodeURIComponent(recipientId)}`,
|
|
582
|
+
{
|
|
583
|
+
idempotencyKey,
|
|
584
|
+
// Only what was named. Sending `email: undefined` as an explicit null
|
|
585
|
+
// would ask the server to clear a field the caller never mentioned.
|
|
586
|
+
body: {
|
|
587
|
+
...(patch.type !== undefined ? { type: patch.type } : {}),
|
|
588
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
589
|
+
...(patch.email !== undefined ? { email: patch.email } : {}),
|
|
590
|
+
...(patch.phone !== undefined ? { phone: patch.phone } : {}),
|
|
591
|
+
...(patch.country !== undefined ? { country: patch.country } : {}),
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Remove a beneficiary and every payment method on it.
|
|
599
|
+
*
|
|
600
|
+
* Payouts already sent are history, not references — deleting cancels
|
|
601
|
+
* nothing in flight.
|
|
602
|
+
*/
|
|
603
|
+
deleteBeneficiary(recipientId, { idempotencyKey } = {}) {
|
|
604
|
+
return this.request(
|
|
605
|
+
'DELETE',
|
|
606
|
+
`/recipients/${this.orgId}/${encodeURIComponent(recipientId)}`,
|
|
607
|
+
{ idempotencyKey },
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Remove one way of paying a beneficiary — an account that closed, or one
|
|
613
|
+
* entered wrong. The beneficiary and their other methods are untouched, and
|
|
614
|
+
* the `destinationAccountId` this method carried stops being payable.
|
|
615
|
+
*/
|
|
616
|
+
deleteBeneficiaryMethod(recipientId, methodId, { idempotencyKey } = {}) {
|
|
617
|
+
return this.request(
|
|
618
|
+
'DELETE',
|
|
619
|
+
`/recipients/${this.orgId}/${encodeURIComponent(recipientId)}/methods/${encodeURIComponent(methodId)}`,
|
|
620
|
+
{ idempotencyKey },
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* The full account details behind one payment method.
|
|
626
|
+
*
|
|
627
|
+
* Lists carry only `last4`. This returns what was actually registered, so you
|
|
628
|
+
* can show someone the account on file before they authorize a payout, or
|
|
629
|
+
* confirm what you stored matches what we hold. Deliberately per-method: bulk
|
|
630
|
+
* reads do not move full account numbers around.
|
|
631
|
+
*/
|
|
632
|
+
getBeneficiaryMethodDetails(recipientId, methodId) {
|
|
633
|
+
return this.request(
|
|
634
|
+
'GET',
|
|
635
|
+
`/recipients/${this.orgId}/${encodeURIComponent(recipientId)}/methods/${encodeURIComponent(methodId)}/details`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
457
639
|
// ─── paying ───
|
|
458
640
|
|
|
459
641
|
/**
|
|
@@ -506,6 +688,13 @@ class PayoutsClient {
|
|
|
506
688
|
* receive and the send is refused if the binding quote moved further than
|
|
507
689
|
* `maxRateDrift` from it. Enforced server-side, so it protects every caller
|
|
508
690
|
* rather than only the ones using this package.
|
|
691
|
+
*
|
|
692
|
+
* `amountLeg: 'destination'` changes what `amount` MEANS: the beneficiary
|
|
693
|
+
* receives exactly that figure, in THEIR currency, and the fees are added to
|
|
694
|
+
* what you are debited rather than taken out of it. Measured on a live
|
|
695
|
+
* corridor, naming 3400 MXN debited 201.879397 USDC and paid out 3400.00.
|
|
696
|
+
* Refused with `EXACT_OUTPUT_UNSUPPORTED` where the routing cannot lock it —
|
|
697
|
+
* check `capabilities.exactOutput` on the corridors call first.
|
|
509
698
|
*/
|
|
510
699
|
payout(p) {
|
|
511
700
|
const expect =
|
|
@@ -519,7 +708,10 @@ class PayoutsClient {
|
|
|
519
708
|
body: {
|
|
520
709
|
amount: p.amount,
|
|
521
710
|
destinationAccountId: p.destinationAccountId,
|
|
522
|
-
...(
|
|
711
|
+
...(p.amountLeg ? { amountLeg: p.amountLeg } : {}),
|
|
712
|
+
...(expect !== undefined
|
|
713
|
+
? { expectDestination: String(expect) }
|
|
714
|
+
: {}),
|
|
523
715
|
...(p.maxRateDrift !== undefined
|
|
524
716
|
? { maxDriftBps: toDriftBps(p.maxRateDrift) }
|
|
525
717
|
: {}),
|
|
@@ -533,7 +725,9 @@ class PayoutsClient {
|
|
|
533
725
|
// Says "I know this looks like the last one, and I mean it". Only ever
|
|
534
726
|
// set deliberately: it switches off the guard that catches a retry
|
|
535
727
|
// arriving under a fresh key.
|
|
536
|
-
...(p.allowDuplicate
|
|
728
|
+
...(p.allowDuplicate
|
|
729
|
+
? { headers: { 'x-allow-duplicate': 'true' } }
|
|
730
|
+
: {}),
|
|
537
731
|
},
|
|
538
732
|
);
|
|
539
733
|
}
|
|
@@ -549,9 +743,17 @@ class PayoutsClient {
|
|
|
549
743
|
*
|
|
550
744
|
* At-least-once: dedupe on `id`.
|
|
551
745
|
*/
|
|
552
|
-
listEvents({ since, limit, payoutId } = {}) {
|
|
746
|
+
listEvents({ since, limit, payoutId, type } = {}) {
|
|
553
747
|
return this.request('GET', `/payments/organizations/${this.orgId}/events`, {
|
|
554
|
-
|
|
748
|
+
// `type` narrows the feed to the families you book: a string
|
|
749
|
+
// ('payout.completed,payout.returned') or an array. Unknown values are a
|
|
750
|
+
// 400, never an empty page.
|
|
751
|
+
query: {
|
|
752
|
+
since,
|
|
753
|
+
limit,
|
|
754
|
+
payoutId,
|
|
755
|
+
type: Array.isArray(type) ? type.join(',') : type,
|
|
756
|
+
},
|
|
555
757
|
});
|
|
556
758
|
}
|
|
557
759
|
|
|
@@ -569,10 +771,10 @@ class PayoutsClient {
|
|
|
569
771
|
* At-least-once by design: `since` is inclusive, so a resumed run re-reads the
|
|
570
772
|
* row at your watermark. Dedupe on `id`.
|
|
571
773
|
*/
|
|
572
|
-
async *eachEvent({ since, limit = 100, payoutId } = {}) {
|
|
774
|
+
async *eachEvent({ since, limit = 100, payoutId, type } = {}) {
|
|
573
775
|
let cursor = since;
|
|
574
776
|
for (;;) {
|
|
575
|
-
const page = await this.listEvents({ since: cursor, limit, payoutId });
|
|
777
|
+
const page = await this.listEvents({ since: cursor, limit, payoutId, type });
|
|
576
778
|
for (const event of page.data || []) yield event;
|
|
577
779
|
if (!page.hasMore || !page.nextSince || page.nextSince === cursor) return;
|
|
578
780
|
cursor = page.nextSince;
|
|
@@ -589,7 +791,13 @@ class PayoutsClient {
|
|
|
589
791
|
* empty page with `hasMore` is a real state and swallowing it would rebuild
|
|
590
792
|
* the silent-truncation bug the API was fixed to remove.
|
|
591
793
|
*/
|
|
592
|
-
async *eachPayout({
|
|
794
|
+
async *eachPayout({
|
|
795
|
+
limit = 50,
|
|
796
|
+
status,
|
|
797
|
+
endUserId,
|
|
798
|
+
reference,
|
|
799
|
+
updatedSince,
|
|
800
|
+
} = {}) {
|
|
593
801
|
let cursor;
|
|
594
802
|
for (;;) {
|
|
595
803
|
const page = await this.listPayouts({
|
|
@@ -601,7 +809,8 @@ class PayoutsClient {
|
|
|
601
809
|
updatedSince,
|
|
602
810
|
});
|
|
603
811
|
for (const payout of page.data || []) yield payout;
|
|
604
|
-
if (!page.hasMore || !page.nextCursor || page.nextCursor === cursor)
|
|
812
|
+
if (!page.hasMore || !page.nextCursor || page.nextCursor === cursor)
|
|
813
|
+
return;
|
|
605
814
|
cursor = page.nextCursor;
|
|
606
815
|
}
|
|
607
816
|
}
|
|
@@ -609,8 +818,18 @@ class PayoutsClient {
|
|
|
609
818
|
/**
|
|
610
819
|
* How to fund a payout that came back with `requiresFunding: true`.
|
|
611
820
|
*
|
|
612
|
-
* Where to send, how much, on which network
|
|
613
|
-
*
|
|
821
|
+
* Where to send, how much, on which network. Your funds stay in your wallet
|
|
822
|
+
* until you move them — we hold no key and cannot move them.
|
|
823
|
+
*
|
|
824
|
+
* A pure read: poll it as often as you like. `expiresAt` is normally `null`,
|
|
825
|
+
* because there is no countdown on the deposit address — the rail decides
|
|
826
|
+
* when an unfunded payout is over and reports that as the payout's own
|
|
827
|
+
* status. Before sending against instructions you fetched a while ago, read
|
|
828
|
+
* `getPayout()` rather than watching a clock.
|
|
829
|
+
*
|
|
830
|
+
* `signableOperations` is no longer returned here; it moved to
|
|
831
|
+
* a dashboard action: they are operations for the organization's own wallet,
|
|
832
|
+
* which an API key cannot sign.
|
|
614
833
|
*/
|
|
615
834
|
getFunding(payoutId) {
|
|
616
835
|
return this.request(
|
|
@@ -619,11 +838,7 @@ class PayoutsClient {
|
|
|
619
838
|
);
|
|
620
839
|
}
|
|
621
840
|
|
|
622
|
-
/**
|
|
623
|
-
* Proof you sent the funds: a `transactionHash` you broadcast, or
|
|
624
|
-
* `signedOperations` you signed from `getFunding()`. Which one depends on how
|
|
625
|
-
* you hold the money, not on anything we prefer.
|
|
626
|
-
*/
|
|
841
|
+
/** Proof you sent the funds: the transaction hash you broadcast. */
|
|
627
842
|
confirmFunding(payoutId, proof = {}) {
|
|
628
843
|
return this.request(
|
|
629
844
|
'POST',
|
|
@@ -633,12 +848,6 @@ class PayoutsClient {
|
|
|
633
848
|
...(proof.transactionHash
|
|
634
849
|
? { transactionHash: proof.transactionHash }
|
|
635
850
|
: {}),
|
|
636
|
-
...(proof.signedOperations
|
|
637
|
-
? { signedOperations: proof.signedOperations }
|
|
638
|
-
: {}),
|
|
639
|
-
...(proof.tamperProofSignature
|
|
640
|
-
? { tamperProofSignature: proof.tamperProofSignature }
|
|
641
|
-
: {}),
|
|
642
851
|
},
|
|
643
852
|
idempotencyKey: proof.idempotencyKey || randomUUID(),
|
|
644
853
|
},
|
|
@@ -677,12 +886,128 @@ class PayoutsClient {
|
|
|
677
886
|
* declaration used to claim an array, so code written off it threw on the
|
|
678
887
|
* first call.
|
|
679
888
|
*/
|
|
680
|
-
listPayouts({
|
|
889
|
+
listPayouts({
|
|
890
|
+
limit,
|
|
891
|
+
cursor,
|
|
892
|
+
status,
|
|
893
|
+
endUserId,
|
|
894
|
+
reference,
|
|
895
|
+
updatedSince,
|
|
896
|
+
} = {}) {
|
|
681
897
|
return this.request('GET', `/payments/organizations/${this.orgId}/orders`, {
|
|
682
898
|
query: { limit, cursor, status, endUserId, reference, updatedSince },
|
|
683
899
|
});
|
|
684
900
|
}
|
|
685
901
|
|
|
902
|
+
// ─── mass payouts ───
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Submit up to 1,000 payouts as one run. Each item is exactly a `payout()`
|
|
906
|
+
* body (the wire shape — `expectDestination`, `maxDriftBps`).
|
|
907
|
+
*
|
|
908
|
+
* The `202` means RECEIVED, not paid: every line is validated first (nothing
|
|
909
|
+
* priced, nothing debited), then the valid lines become ordinary payouts.
|
|
910
|
+
* With `autoCommit: true` — the default — a run with zero validation errors
|
|
911
|
+
* proceeds straight to creation; otherwise the batch holds at
|
|
912
|
+
* `awaiting_confirmation` for `confirmPayoutBatch` or `cancelPayoutBatch`.
|
|
913
|
+
*
|
|
914
|
+
* The idempotency key covers the RUN: a submit loop that dies and resubmits
|
|
915
|
+
* the same file under the same key gets the same batch back — never a second
|
|
916
|
+
* payroll. Persist your own key before you send; the generated fallback only
|
|
917
|
+
* protects retries inside this process.
|
|
918
|
+
*
|
|
919
|
+
* `externalReferenceId` is required and unique per organization: it is the
|
|
920
|
+
* guard the key cannot be, against the same file re-run under a FRESH key.
|
|
921
|
+
* A repeat is refused with `PAYOUT_BATCH_DUPLICATE_REFERENCE` naming the
|
|
922
|
+
* original batch. Sent as-is; the server validates it, so a missing id is
|
|
923
|
+
* the server's 400, not a silent omission here.
|
|
924
|
+
*/
|
|
925
|
+
createPayoutBatch(p = {}) {
|
|
926
|
+
return this.request(
|
|
927
|
+
'POST',
|
|
928
|
+
`/payments/organizations/${this.orgId}/payouts/batches`,
|
|
929
|
+
{
|
|
930
|
+
body: {
|
|
931
|
+
externalReferenceId: p.externalReferenceId,
|
|
932
|
+
items: p.items,
|
|
933
|
+
...(p.autoCommit !== undefined ? { autoCommit: p.autoCommit } : {}),
|
|
934
|
+
},
|
|
935
|
+
idempotencyKey: p.idempotencyKey || randomUUID(),
|
|
936
|
+
// Same meaning as on a single payout: "I know this run looks like the
|
|
937
|
+
// last one, and I mean it".
|
|
938
|
+
...(p.allowDuplicate ? { headers: { 'x-allow-duplicate': 'true' } } : {}),
|
|
939
|
+
},
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** A PAGE of batches, newest first: `{ data, hasMore, nextCursor }`. */
|
|
944
|
+
listPayoutBatches({ status, externalReferenceId, limit, cursor } = {}) {
|
|
945
|
+
return this.request(
|
|
946
|
+
'GET',
|
|
947
|
+
`/payments/organizations/${this.orgId}/payouts/batches`,
|
|
948
|
+
{ query: { status, externalReferenceId, limit, cursor } },
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* One run, with its counts. A batch tracks CREATION, not settlement:
|
|
954
|
+
* `completed` means every line either became a payout or was refused. Once a
|
|
955
|
+
* line is `created`, watch the payout, not the item.
|
|
956
|
+
*/
|
|
957
|
+
getPayoutBatch(batchId) {
|
|
958
|
+
return this.request(
|
|
959
|
+
'GET',
|
|
960
|
+
`/payments/organizations/${this.orgId}/payouts/batches/${encodeURIComponent(batchId)}`,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* The lines of a run, in submitted order, with the instruction you sent
|
|
966
|
+
* echoed back verbatim — join errors to your own file by content, not by
|
|
967
|
+
* counting rows. `status: 'invalid'` is the review screen; `'created'` joins
|
|
968
|
+
* the run to the payout ledger; `'requires_review'` are lines whose outcome
|
|
969
|
+
* could not be established and were deliberately NOT retried — contact
|
|
970
|
+
* support with the batchId rather than resubmitting them.
|
|
971
|
+
*
|
|
972
|
+
* The CSV export (`?format=csv`) is a plain file download, not a JSON page —
|
|
973
|
+
* fetch it over HTTP directly. This method returns the JSON page.
|
|
974
|
+
*/
|
|
975
|
+
listPayoutBatchItems(batchId, { status, limit, cursor } = {}) {
|
|
976
|
+
return this.request(
|
|
977
|
+
'GET',
|
|
978
|
+
`/payments/organizations/${this.orgId}/payouts/batches/${encodeURIComponent(batchId)}/items`,
|
|
979
|
+
{ query: { status, limit, cursor } },
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Proceed with the valid lines of a held run. Only legal while the batch is
|
|
985
|
+
* `awaiting_confirmation`; anything else is `PAYOUT_BATCH_NOT_CONFIRMABLE`,
|
|
986
|
+
* naming where it actually is. Invalid lines stay refused — correct them and
|
|
987
|
+
* resubmit as a new batch.
|
|
988
|
+
*/
|
|
989
|
+
confirmPayoutBatch(batchId, { idempotencyKey } = {}) {
|
|
990
|
+
return this.request(
|
|
991
|
+
'POST',
|
|
992
|
+
`/payments/organizations/${this.orgId}/payouts/batches/${encodeURIComponent(batchId)}/confirm`,
|
|
993
|
+
{ idempotencyKey: idempotencyKey || randomUUID() },
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Stop a run before any payout exists. Honoured in `received`, `validating`
|
|
999
|
+
* and `awaiting_confirmation` only; once creation begins the run is
|
|
1000
|
+
* committed and this is `PAYOUT_BATCH_NOT_CANCELABLE` — cancel individual
|
|
1001
|
+
* payouts while they are still `pending` via `cancelPayout` instead.
|
|
1002
|
+
*/
|
|
1003
|
+
cancelPayoutBatch(batchId, { idempotencyKey } = {}) {
|
|
1004
|
+
return this.request(
|
|
1005
|
+
'POST',
|
|
1006
|
+
`/payments/organizations/${this.orgId}/payouts/batches/${encodeURIComponent(batchId)}/cancel`,
|
|
1007
|
+
{ idempotencyKey: idempotencyKey || randomUUID() },
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
686
1011
|
/** Sandbox only: credit the test balance so you can send. */
|
|
687
1012
|
/**
|
|
688
1013
|
* Register a sandbox webhook endpoint and get its signing secret.
|
|
@@ -691,19 +1016,61 @@ class PayoutsClient {
|
|
|
691
1016
|
* this returns it rather than storing it for you. Live endpoints are managed
|
|
692
1017
|
* from the dashboard on purpose: a credential that could repoint its own
|
|
693
1018
|
* webhook URL could redirect every payout notification.
|
|
1019
|
+
*
|
|
1020
|
+
* No `Idempotency-Key`, and so no retry: the server ignores the header here
|
|
1021
|
+
* because a stored replay would keep the secret for seven days. A timeout
|
|
1022
|
+
* means "register again"; an extra endpoint is a delete, a leaked secret is
|
|
1023
|
+
* not.
|
|
694
1024
|
*/
|
|
695
|
-
createWebhookEndpoint({ url, events
|
|
696
|
-
return this.
|
|
1025
|
+
createWebhookEndpoint({ url, events } = {}) {
|
|
1026
|
+
return this._send(
|
|
697
1027
|
'POST',
|
|
698
1028
|
`/payments/organizations/${this.orgId}/sandbox/webhook-endpoints`,
|
|
699
|
-
{
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1029
|
+
{ body: { url, ...(events ? { events } : {}) } },
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* The webhook endpoints registered for your organization.
|
|
1035
|
+
*
|
|
1036
|
+
* Read-only, and it stays that way. Registering, pausing, deleting and
|
|
1037
|
+
* replaying are dashboard actions: a credential that could repoint its own
|
|
1038
|
+
* webhook URL could quietly redirect every payout notification you receive.
|
|
1039
|
+
* The signing secret is never returned here — you are shown it once, when the
|
|
1040
|
+
* endpoint is created.
|
|
1041
|
+
*
|
|
1042
|
+
* Note the path: `/organizations/{orgId}/…`, not the `/payments/…` prefix the
|
|
1043
|
+
* payout routes use.
|
|
1044
|
+
*/
|
|
1045
|
+
webhookEndpoints() {
|
|
1046
|
+
return this.request(
|
|
1047
|
+
'GET',
|
|
1048
|
+
`/organizations/${this.orgId}/webhook-endpoints`,
|
|
703
1049
|
);
|
|
704
1050
|
}
|
|
705
1051
|
|
|
706
|
-
/**
|
|
1052
|
+
/**
|
|
1053
|
+
* The 50 most recent delivery attempts for one endpoint, newest first —
|
|
1054
|
+
* "did you send me that event", without opening a dashboard.
|
|
1055
|
+
*
|
|
1056
|
+
* `eventId` is the `svix-id` we sent, so it is what your dedupe keys on.
|
|
1057
|
+
* `lastError` is the STATUS your server answered with on the last failed
|
|
1058
|
+
* attempt (`HTTP 500`) or the transport error, never your response body — we
|
|
1059
|
+
* do not store one. `nextAttemptAt` is when we try again; retries back off
|
|
1060
|
+
* over roughly 70 hours, after which only the event feed still has it.
|
|
1061
|
+
* Payloads are not returned.
|
|
1062
|
+
*
|
|
1063
|
+
* This is the endpoint from `webhookEndpoints()`. The sandbox registration
|
|
1064
|
+
* view is `webhookDeliveries()`, below.
|
|
1065
|
+
*/
|
|
1066
|
+
webhookEndpointDeliveries(endpointId) {
|
|
1067
|
+
return this.request(
|
|
1068
|
+
'GET',
|
|
1069
|
+
`/organizations/${this.orgId}/webhook-endpoints/${encodeURIComponent(endpointId)}/deliveries`,
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/** What we sent, what came back, and what we retried. Sandbox endpoints. */
|
|
707
1074
|
webhookDeliveries(endpointId) {
|
|
708
1075
|
return this.request(
|
|
709
1076
|
'GET',
|
|
@@ -711,6 +1078,84 @@ class PayoutsClient {
|
|
|
711
1078
|
);
|
|
712
1079
|
}
|
|
713
1080
|
|
|
1081
|
+
/**
|
|
1082
|
+
* Payouts and batch runs waiting on your approvers: `{ data }`.
|
|
1083
|
+
*
|
|
1084
|
+
* When your organization requires M-of-N approval on API payouts, `payout()`
|
|
1085
|
+
* and `confirmPayoutBatch()` above the threshold answer 202 with
|
|
1086
|
+
* `{ status: 'pending_approval', approvalId, … }` instead of a payout. This is
|
|
1087
|
+
* that queue. Approving and rejecting are human actions in the dashboard and
|
|
1088
|
+
* deliberately have no method here — a key cannot approve.
|
|
1089
|
+
*/
|
|
1090
|
+
listApprovals({ status, limit } = {}) {
|
|
1091
|
+
return this.request(
|
|
1092
|
+
'GET',
|
|
1093
|
+
`/payments/organizations/${this.orgId}/payouts/approvals`,
|
|
1094
|
+
{ query: { status, limit } },
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* One approval, by the `approvalId` a 202 gave you. Once `status` is
|
|
1100
|
+
* `executed`, `payoutId` is the payout it became.
|
|
1101
|
+
*/
|
|
1102
|
+
getApproval(approvalId) {
|
|
1103
|
+
return this.request(
|
|
1104
|
+
'GET',
|
|
1105
|
+
`/payments/organizations/${this.orgId}/payouts/approvals/${encodeURIComponent(approvalId)}`,
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* A PAGE of audit events, newest first: `{ data, hasMore, nextCursor }`.
|
|
1111
|
+
*
|
|
1112
|
+
* Who did what, from where, with which credential — every audited mutation on
|
|
1113
|
+
* your organization, refused attempts included. Rows carry the key PREFIX you
|
|
1114
|
+
* named it by, never the internal id. A read-only key may read this.
|
|
1115
|
+
*/
|
|
1116
|
+
listAuditEvents({
|
|
1117
|
+
cursor,
|
|
1118
|
+
limit,
|
|
1119
|
+
action,
|
|
1120
|
+
resourceId,
|
|
1121
|
+
apiKey,
|
|
1122
|
+
actorUserId,
|
|
1123
|
+
createdAfter,
|
|
1124
|
+
createdBefore,
|
|
1125
|
+
} = {}) {
|
|
1126
|
+
return this.request(
|
|
1127
|
+
'GET',
|
|
1128
|
+
`/payments/organizations/${this.orgId}/audit-events`,
|
|
1129
|
+
{
|
|
1130
|
+
query: {
|
|
1131
|
+
cursor,
|
|
1132
|
+
limit,
|
|
1133
|
+
action,
|
|
1134
|
+
resourceId,
|
|
1135
|
+
apiKey,
|
|
1136
|
+
actorUserId,
|
|
1137
|
+
createdAfter,
|
|
1138
|
+
createdBefore,
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Every audit event matching a filter, newest first, paged for you. Same
|
|
1146
|
+
* stop rule as `eachPayout`: ends when the server says there is no more, or
|
|
1147
|
+
* when the cursor stops advancing.
|
|
1148
|
+
*/
|
|
1149
|
+
async *eachAuditEvent({ limit = 100, ...filters } = {}) {
|
|
1150
|
+
let cursor;
|
|
1151
|
+
for (;;) {
|
|
1152
|
+
const page = await this.listAuditEvents({ ...filters, limit, cursor });
|
|
1153
|
+
for (const row of page.data || []) yield row;
|
|
1154
|
+
if (!page.hasMore || !page.nextCursor || page.nextCursor === cursor) return;
|
|
1155
|
+
cursor = page.nextCursor;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
714
1159
|
/**
|
|
715
1160
|
* Mint a one-time link for the person being paid.
|
|
716
1161
|
*
|
|
@@ -741,18 +1186,60 @@ class PayoutsClient {
|
|
|
741
1186
|
}
|
|
742
1187
|
|
|
743
1188
|
/**
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
1189
|
+
* A PAGE of balance transactions, newest first: `{ data, hasMore, nextCursor }`.
|
|
1190
|
+
*
|
|
1191
|
+
* Every change to what you can spend — funding, payouts, returns, holds and
|
|
1192
|
+
* their release, adjustments — each with `balanceAfter`. Reconcile your
|
|
1193
|
+
* ledger row by row against this; `id` is the cursor and the dedupe key.
|
|
1194
|
+
* `type` takes a string (`'payout,funding'`) or an array.
|
|
1195
|
+
*
|
|
1196
|
+
* Served on every environment; an empty page means no rows yet, not an
|
|
1197
|
+
* error.
|
|
747
1198
|
*/
|
|
748
|
-
|
|
1199
|
+
listBalanceTransactions({
|
|
1200
|
+
cursor,
|
|
1201
|
+
limit,
|
|
1202
|
+
type,
|
|
1203
|
+
orderId,
|
|
1204
|
+
currency,
|
|
1205
|
+
createdAfter,
|
|
1206
|
+
createdBefore,
|
|
1207
|
+
} = {}) {
|
|
749
1208
|
return this.request(
|
|
750
1209
|
'GET',
|
|
751
|
-
`/payments/organizations/${this.orgId}/
|
|
752
|
-
{
|
|
1210
|
+
`/payments/organizations/${this.orgId}/balance_transactions`,
|
|
1211
|
+
{
|
|
1212
|
+
query: {
|
|
1213
|
+
cursor,
|
|
1214
|
+
limit,
|
|
1215
|
+
type: Array.isArray(type) ? type.join(',') : type,
|
|
1216
|
+
orderId,
|
|
1217
|
+
currency,
|
|
1218
|
+
createdAfter,
|
|
1219
|
+
createdBefore,
|
|
1220
|
+
},
|
|
1221
|
+
},
|
|
753
1222
|
);
|
|
754
1223
|
}
|
|
755
1224
|
|
|
1225
|
+
/**
|
|
1226
|
+
* Every balance transaction matching a filter, newest first, paged for you.
|
|
1227
|
+
*
|
|
1228
|
+
* for await (const t of avvio.eachBalanceTransaction({ type: 'payout' })) { … }
|
|
1229
|
+
*
|
|
1230
|
+
* Same stop rule as `eachPayout`: ends when the server says there is no
|
|
1231
|
+
* more, or when the cursor stops advancing.
|
|
1232
|
+
*/
|
|
1233
|
+
async *eachBalanceTransaction({ limit = 100, ...filters } = {}) {
|
|
1234
|
+
let cursor;
|
|
1235
|
+
for (;;) {
|
|
1236
|
+
const page = await this.listBalanceTransactions({ ...filters, limit, cursor });
|
|
1237
|
+
for (const row of page.data || []) yield row;
|
|
1238
|
+
if (!page.hasMore || !page.nextCursor || page.nextCursor === cursor) return;
|
|
1239
|
+
cursor = page.nextCursor;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
756
1243
|
fund(amount = '5000.00', idempotencyKey) {
|
|
757
1244
|
return this.request(
|
|
758
1245
|
'POST',
|
|
@@ -761,14 +1248,24 @@ class PayoutsClient {
|
|
|
761
1248
|
);
|
|
762
1249
|
}
|
|
763
1250
|
|
|
764
|
-
/**
|
|
765
|
-
|
|
1251
|
+
/**
|
|
1252
|
+
* What your organization is bound by, read live: payout caps (`null` = no
|
|
1253
|
+
* cap), the approval threshold and M, effective features, rate limits per
|
|
1254
|
+
* minute, idempotency windows, the currencies that need a `purposeOfPayment`,
|
|
1255
|
+
* and where to read corridors, events and the ledger. Read it first.
|
|
1256
|
+
*/
|
|
1257
|
+
getPolicy() {
|
|
766
1258
|
return this.request(
|
|
767
1259
|
'GET',
|
|
768
|
-
`/payments/organizations/${this.orgId}/
|
|
1260
|
+
`/payments/organizations/${this.orgId}/policy`,
|
|
769
1261
|
);
|
|
770
1262
|
}
|
|
771
1263
|
|
|
1264
|
+
/** What you can currently send. */
|
|
1265
|
+
balance() {
|
|
1266
|
+
return this.request('GET', `/payments/organizations/${this.orgId}/balance`);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
772
1269
|
/** Where to wire funds to top up the balance payouts debit. */
|
|
773
1270
|
fundingAccounts() {
|
|
774
1271
|
return this.request(
|
|
@@ -789,7 +1286,28 @@ const {
|
|
|
789
1286
|
WebhookVerificationError,
|
|
790
1287
|
} = require('./webhooks');
|
|
791
1288
|
|
|
1289
|
+
/**
|
|
1290
|
+
* Generate a signing pair. The private half is yours and must never be sent to
|
|
1291
|
+
* us — register `publicKeyPem` on the key, keep `privateKeyPem` in your secret
|
|
1292
|
+
* store, and pass it to the client.
|
|
1293
|
+
*
|
|
1294
|
+
* Here rather than in the docs as an openssl incantation, because a partner who
|
|
1295
|
+
* has to look up `openssl ecparam` is a partner who might paste the wrong half.
|
|
1296
|
+
*/
|
|
1297
|
+
function generateSigningKeyPair() {
|
|
1298
|
+
const { publicKey, privateKey } = generateKeyPairSync('ec', {
|
|
1299
|
+
namedCurve: 'prime256v1',
|
|
1300
|
+
});
|
|
1301
|
+
return {
|
|
1302
|
+
publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
|
|
1303
|
+
privateKeyPem: privateKey
|
|
1304
|
+
.export({ type: 'pkcs8', format: 'pem' })
|
|
1305
|
+
.toString(),
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
792
1309
|
module.exports = {
|
|
1310
|
+
generateSigningKeyPair,
|
|
793
1311
|
PayoutsClient,
|
|
794
1312
|
PayoutsError,
|
|
795
1313
|
stableKey,
|