@nile-squad/nylonpay-ts 1.0.7 → 1.0.9
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 +8 -6
- package/dist/index.cjs +141 -263
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -27
- package/dist/index.d.ts +58 -27
- package/dist/index.js +141 -263
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Server-side SDK for integrating Nylon Pay into merchant applications. Supports TypeScript and JavaScript (ESM and CJS).
|
|
4
4
|
|
|
5
|
-
This package is the reference implementation of the [Nylon Pay SDK Spec](https://github.com/nile-squad/specs/blob/main/
|
|
5
|
+
This package is the reference implementation of the [Nylon Pay SDK Spec](https://github.com/nile-squad/specs/blob/main/nylonpay-sdk-spec/spec.md) — the canonical, language-agnostic contract for building Nylon Pay SDKs in any language.
|
|
6
6
|
|
|
7
7
|
[Full documentation](https://docs.nylonpay.nilesquad.com/docs)
|
|
8
8
|
|
|
@@ -61,14 +61,16 @@ const payment = await nylonpay.collectPayment({
|
|
|
61
61
|
customer: { name: "Jane", phoneNumber: "+256700000000" },
|
|
62
62
|
description: "Order #1234",
|
|
63
63
|
method: "mobileMoney",
|
|
64
|
-
reference: "ORDER-
|
|
64
|
+
reference: "ORDER-2026-001",
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
payment.on("success", ({ transaction }) => { /* ... */ });
|
|
68
68
|
payment.on("failed", ({ error }) => { /* ... */ });
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
`reference` is optional and auto-generated if omitted.
|
|
71
|
+
`reference` is optional and auto-generated if omitted. A supplied reference must be
|
|
72
|
+
**13 to 15 characters**; the SDK throws a `validation` error otherwise. A raw UUID is
|
|
73
|
+
36 characters and will be rejected, so use a short id of your own or omit the field.
|
|
72
74
|
|
|
73
75
|
### collectPaymentAndResolve
|
|
74
76
|
|
|
@@ -120,7 +122,7 @@ const result = await nylonpay.makePayoutAndResolve({
|
|
|
120
122
|
One-shot status check for a transaction. Does not poll, returns the current server-side state.
|
|
121
123
|
|
|
122
124
|
```ts
|
|
123
|
-
const result = await nylonpay.getStatus({ reference: "ORDER-
|
|
125
|
+
const result = await nylonpay.getStatus({ reference: "ORDER-2026-001" });
|
|
124
126
|
if (result.isOk) console.log(result.value.status);
|
|
125
127
|
```
|
|
126
128
|
|
|
@@ -129,7 +131,7 @@ if (result.isOk) console.log(result.value.status);
|
|
|
129
131
|
Look up a full transaction record by `id` or `reference`. At least one must be provided.
|
|
130
132
|
|
|
131
133
|
```ts
|
|
132
|
-
const result = await nylonpay.getTransaction({ reference: "ORDER-
|
|
134
|
+
const result = await nylonpay.getTransaction({ reference: "ORDER-2026-001" });
|
|
133
135
|
if (result.isOk) console.log(result.value.failureReason);
|
|
134
136
|
```
|
|
135
137
|
|
|
@@ -216,7 +218,7 @@ All operations return `Result<T, string>` from [slang-ts](https://github.com/nil
|
|
|
216
218
|
```ts
|
|
217
219
|
import { parseError } from "@nile-squad/nylonpay-ts";
|
|
218
220
|
|
|
219
|
-
const result = await nylonpay.getStatus({ reference: "ORDER-
|
|
221
|
+
const result = await nylonpay.getStatus({ reference: "ORDER-2026-001" });
|
|
220
222
|
if (!result.isOk) {
|
|
221
223
|
const error = parseError(result.error);
|
|
222
224
|
if (error.retryable) {
|
package/dist/index.cjs
CHANGED
|
@@ -4,7 +4,7 @@ var crypto = require('crypto');
|
|
|
4
4
|
var slangTs = require('slang-ts');
|
|
5
5
|
var os = require('os');
|
|
6
6
|
|
|
7
|
-
// src/
|
|
7
|
+
// src/create-nylon-pay.ts
|
|
8
8
|
|
|
9
9
|
// src/pubsub.ts
|
|
10
10
|
function createEmitter() {
|
|
@@ -63,9 +63,7 @@ var DEFAULT_MAX_RETRIES = 3;
|
|
|
63
63
|
var DEFAULT_MAX_POLL_INTERVAL_MS = 2e3;
|
|
64
64
|
var DEFAULT_MAX_POLL_DURATION_MS = 3e5;
|
|
65
65
|
var DEFAULT_MAX_POLL_ATTEMPTS = 150;
|
|
66
|
-
var
|
|
67
|
-
var STREAM_PATH = "/sse/transaction";
|
|
68
|
-
var MAX_STREAM_RECONNECTS = 2;
|
|
66
|
+
var POLL_JITTER_MS = 250;
|
|
69
67
|
var SDK_SERVICE = "sdk";
|
|
70
68
|
var SDK_ACTIONS = {
|
|
71
69
|
collectPayment: "sdk-collect-payment",
|
|
@@ -93,13 +91,22 @@ function generateFingerprint() {
|
|
|
93
91
|
function generateNonce(length = 16) {
|
|
94
92
|
return crypto.randomBytes(length).toString("hex");
|
|
95
93
|
}
|
|
94
|
+
function compareByCodePoint(first, second) {
|
|
95
|
+
if (first < second) {
|
|
96
|
+
return -1;
|
|
97
|
+
}
|
|
98
|
+
if (first > second) {
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
96
103
|
function sortValue(value) {
|
|
97
104
|
if (Array.isArray(value)) {
|
|
98
105
|
return value.map((entry) => sortValue(entry));
|
|
99
106
|
}
|
|
100
107
|
if (value && typeof value === "object") {
|
|
101
108
|
const sortedEntries = Object.entries(value).sort(
|
|
102
|
-
([firstKey], [secondKey]) => firstKey
|
|
109
|
+
([firstKey], [secondKey]) => compareByCodePoint(firstKey, secondKey)
|
|
103
110
|
);
|
|
104
111
|
return Object.fromEntries(
|
|
105
112
|
sortedEntries.map(([entryKey, entryValue]) => [
|
|
@@ -123,42 +130,6 @@ function createSignature(input) {
|
|
|
123
130
|
function createTimestamp() {
|
|
124
131
|
return Date.now().toString();
|
|
125
132
|
}
|
|
126
|
-
|
|
127
|
-
// src/sse-parse.ts
|
|
128
|
-
function parseBlock(block) {
|
|
129
|
-
let event = "message";
|
|
130
|
-
const dataLines = [];
|
|
131
|
-
for (const rawLine of block.split("\n")) {
|
|
132
|
-
const line = rawLine.replace(/\r$/, "");
|
|
133
|
-
if (line.startsWith(":")) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (line.startsWith("event:")) {
|
|
137
|
-
event = line.slice("event:".length).trim();
|
|
138
|
-
} else if (line.startsWith("data:")) {
|
|
139
|
-
dataLines.push(line.slice("data:".length).trim());
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (dataLines.length === 0) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
return { event, data: dataLines.join("\n") };
|
|
146
|
-
}
|
|
147
|
-
function parseSseBuffer(buffer) {
|
|
148
|
-
const messages = [];
|
|
149
|
-
let rest = buffer;
|
|
150
|
-
let separator = rest.indexOf("\n\n");
|
|
151
|
-
while (separator !== -1) {
|
|
152
|
-
const block = rest.slice(0, separator);
|
|
153
|
-
rest = rest.slice(separator + 2);
|
|
154
|
-
const message = parseBlock(block);
|
|
155
|
-
if (message) {
|
|
156
|
-
messages.push(message);
|
|
157
|
-
}
|
|
158
|
-
separator = rest.indexOf("\n\n");
|
|
159
|
-
}
|
|
160
|
-
return { messages, rest };
|
|
161
|
-
}
|
|
162
133
|
function verifyResponseSignature(data, signature, secret) {
|
|
163
134
|
const expectedSignature = crypto.createHmac("sha256", secret).update(createCanonicalPayload(data)).digest("hex");
|
|
164
135
|
const providedBuffer = Buffer.from(signature, "hex");
|
|
@@ -171,13 +142,6 @@ function verifyResponseSignature(data, signature, secret) {
|
|
|
171
142
|
|
|
172
143
|
// src/transport.ts
|
|
173
144
|
var CACHED_FINGERPRINT = generateFingerprint();
|
|
174
|
-
function streamUrl(baseUrl) {
|
|
175
|
-
try {
|
|
176
|
-
return new URL(baseUrl).origin + STREAM_PATH;
|
|
177
|
-
} catch {
|
|
178
|
-
return baseUrl.replace(/\/api\/services\/?$/u, "") + STREAM_PATH;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
145
|
var KNOWN_CATEGORIES = /* @__PURE__ */ new Set([
|
|
182
146
|
"auth",
|
|
183
147
|
"validation",
|
|
@@ -343,22 +307,30 @@ function createTransport({
|
|
|
343
307
|
const { status, message, data } = responseBody;
|
|
344
308
|
if (status === true) {
|
|
345
309
|
const { data: strippedData, responseSignature } = stripResponseSignature(data);
|
|
346
|
-
if (responseSignature) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
310
|
+
if (!responseSignature) {
|
|
311
|
+
cleanup();
|
|
312
|
+
return slangTs.Err(
|
|
313
|
+
JSON.stringify({
|
|
314
|
+
category: "internal",
|
|
315
|
+
message: "Response signature missing",
|
|
316
|
+
retryable: false
|
|
317
|
+
})
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const isValid = verifyResponseSignature(
|
|
321
|
+
strippedData,
|
|
322
|
+
responseSignature,
|
|
323
|
+
apiSecret
|
|
324
|
+
);
|
|
325
|
+
if (!isValid) {
|
|
326
|
+
cleanup();
|
|
327
|
+
return slangTs.Err(
|
|
328
|
+
JSON.stringify({
|
|
329
|
+
category: "internal",
|
|
330
|
+
message: "Response signature verification failed",
|
|
331
|
+
retryable: false
|
|
332
|
+
})
|
|
351
333
|
);
|
|
352
|
-
if (!isValid) {
|
|
353
|
-
cleanup();
|
|
354
|
-
return slangTs.Err(
|
|
355
|
-
JSON.stringify({
|
|
356
|
-
category: "internal",
|
|
357
|
-
message: "Response signature verification failed",
|
|
358
|
-
retryable: false
|
|
359
|
-
})
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
334
|
}
|
|
363
335
|
cleanup();
|
|
364
336
|
return slangTs.Ok(strippedData);
|
|
@@ -383,91 +355,7 @@ function createTransport({
|
|
|
383
355
|
}
|
|
384
356
|
return attempt(0);
|
|
385
357
|
}
|
|
386
|
-
|
|
387
|
-
const controller = new AbortController();
|
|
388
|
-
let closed = false;
|
|
389
|
-
const close = () => {
|
|
390
|
-
if (closed) {
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
closed = true;
|
|
394
|
-
controller.abort();
|
|
395
|
-
};
|
|
396
|
-
const payload = {
|
|
397
|
-
reference: input.reference,
|
|
398
|
-
_fingerprint: CACHED_FINGERPRINT
|
|
399
|
-
};
|
|
400
|
-
const headers = buildAuthHeaders({ apiKey, apiSecret, payload });
|
|
401
|
-
const url = streamUrl(baseUrl);
|
|
402
|
-
const run = async () => {
|
|
403
|
-
const fetchResult = await slangTs.safeTry(
|
|
404
|
-
() => fetchImpl(url, {
|
|
405
|
-
method: "POST",
|
|
406
|
-
headers,
|
|
407
|
-
body: JSON.stringify(payload),
|
|
408
|
-
signal: controller.signal
|
|
409
|
-
})
|
|
410
|
-
);
|
|
411
|
-
if (fetchResult.isErr) {
|
|
412
|
-
if (!closed) {
|
|
413
|
-
input.onError(fetchResult.error);
|
|
414
|
-
}
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
const response = fetchResult.value;
|
|
418
|
-
if (!response.ok || !response.body) {
|
|
419
|
-
const textResult = await slangTs.safeTry(() => response.text());
|
|
420
|
-
const message = textResult.isOk ? textResult.value : `HTTP ${response.status}`;
|
|
421
|
-
if (!closed) {
|
|
422
|
-
input.onError(message || `HTTP ${response.status}`);
|
|
423
|
-
}
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
const reader = response.body.getReader();
|
|
427
|
-
const decoder = new TextDecoder();
|
|
428
|
-
let buffer = "";
|
|
429
|
-
while (!closed) {
|
|
430
|
-
const chunkResult = await slangTs.safeTry(() => reader.read());
|
|
431
|
-
if (chunkResult.isErr) {
|
|
432
|
-
if (!closed) {
|
|
433
|
-
input.onError(chunkResult.error);
|
|
434
|
-
}
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
const chunk = chunkResult.value;
|
|
438
|
-
if (chunk.done) {
|
|
439
|
-
break;
|
|
440
|
-
}
|
|
441
|
-
buffer += decoder.decode(chunk.value, { stream: true });
|
|
442
|
-
const { messages, rest } = parseSseBuffer(buffer);
|
|
443
|
-
buffer = rest;
|
|
444
|
-
for (const message of messages) {
|
|
445
|
-
if (message.event === "status") {
|
|
446
|
-
const statusResult = await slangTs.safeTry(
|
|
447
|
-
() => JSON.parse(message.data)
|
|
448
|
-
);
|
|
449
|
-
if (statusResult.isOk) {
|
|
450
|
-
input.onStatus(statusResult.value);
|
|
451
|
-
}
|
|
452
|
-
} else if (message.event === "error") {
|
|
453
|
-
const errDataResult = await slangTs.safeTry(
|
|
454
|
-
() => JSON.parse(message.data)
|
|
455
|
-
);
|
|
456
|
-
const text = errDataResult.isOk ? errDataResult.value.message ?? message.data : message.data;
|
|
457
|
-
close();
|
|
458
|
-
input.onError(text);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
if (!closed) {
|
|
464
|
-
input.onClose();
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
void run();
|
|
468
|
-
return { close };
|
|
469
|
-
}
|
|
470
|
-
return { send, openStream, parseError };
|
|
358
|
+
return { send, parseError };
|
|
471
359
|
}
|
|
472
360
|
function parseError(error) {
|
|
473
361
|
try {
|
|
@@ -517,11 +405,7 @@ function createPaymentInstance(initialResponse, deps) {
|
|
|
517
405
|
fetchTransaction: deps.fetchTransaction,
|
|
518
406
|
pollIntervalMs: deps.pollIntervalMs ?? 2e3,
|
|
519
407
|
maxPollDuration: deps.maxPollDuration ?? 3e5,
|
|
520
|
-
maxPollAttempts: deps.maxPollAttempts ?? 150
|
|
521
|
-
streaming: deps.streaming ?? false,
|
|
522
|
-
openStream: deps.openStream,
|
|
523
|
-
streamHandle: null,
|
|
524
|
-
streamReconnects: 0
|
|
408
|
+
maxPollAttempts: deps.maxPollAttempts ?? 150
|
|
525
409
|
};
|
|
526
410
|
function resolveWithError(error) {
|
|
527
411
|
state.resolved = true;
|
|
@@ -557,6 +441,9 @@ function createPaymentInstance(initialResponse, deps) {
|
|
|
557
441
|
stopUpdates();
|
|
558
442
|
}
|
|
559
443
|
async function handleStatusUpdate(response) {
|
|
444
|
+
if (state.resolved) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
560
447
|
if (response.reference !== state.reference) {
|
|
561
448
|
resolveWithError(
|
|
562
449
|
`Reference mismatch: expected ${state.reference} but got ${response.reference}`
|
|
@@ -590,10 +477,11 @@ function createPaymentInstance(initialResponse, deps) {
|
|
|
590
477
|
if (state.resolved || state.pollingTimer) {
|
|
591
478
|
return;
|
|
592
479
|
}
|
|
480
|
+
const delay2 = state.pollIntervalMs + Math.random() * POLL_JITTER_MS;
|
|
593
481
|
state.pollingTimer = setTimeout(() => {
|
|
594
482
|
state.pollingTimer = null;
|
|
595
483
|
void pollStatus();
|
|
596
|
-
},
|
|
484
|
+
}, delay2);
|
|
597
485
|
}
|
|
598
486
|
async function pollStatus() {
|
|
599
487
|
if (state.resolved) {
|
|
@@ -621,42 +509,6 @@ function createPaymentInstance(initialResponse, deps) {
|
|
|
621
509
|
}
|
|
622
510
|
scheduleNextPoll();
|
|
623
511
|
}
|
|
624
|
-
function closeStream() {
|
|
625
|
-
if (state.streamHandle) {
|
|
626
|
-
state.streamHandle.close();
|
|
627
|
-
state.streamHandle = null;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
function startStream() {
|
|
631
|
-
if (state.resolved || !state.openStream) {
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
state.streamHandle = state.openStream({
|
|
635
|
-
reference: state.reference,
|
|
636
|
-
onStatus: (status) => {
|
|
637
|
-
void handleStatusUpdate(status);
|
|
638
|
-
},
|
|
639
|
-
onError: () => handleStreamFailure(),
|
|
640
|
-
onClose: () => handleStreamFailure()
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
function handleStreamFailure() {
|
|
644
|
-
closeStream();
|
|
645
|
-
if (state.resolved) {
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
if (state.streamReconnects < MAX_STREAM_RECONNECTS) {
|
|
649
|
-
state.streamReconnects += 1;
|
|
650
|
-
const backoff = 500 * 2 ** (state.streamReconnects - 1) + Math.random() * 250;
|
|
651
|
-
setTimeout(() => {
|
|
652
|
-
if (!state.resolved) {
|
|
653
|
-
startStream();
|
|
654
|
-
}
|
|
655
|
-
}, backoff);
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
scheduleNextPoll();
|
|
659
|
-
}
|
|
660
512
|
function startUpdates() {
|
|
661
513
|
if (TERMINAL_STATES.has(state.status)) {
|
|
662
514
|
setTimeout(() => {
|
|
@@ -664,10 +516,6 @@ function createPaymentInstance(initialResponse, deps) {
|
|
|
664
516
|
}, 0);
|
|
665
517
|
return;
|
|
666
518
|
}
|
|
667
|
-
if (state.streaming && state.openStream) {
|
|
668
|
-
startStream();
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
519
|
scheduleNextPoll();
|
|
672
520
|
}
|
|
673
521
|
function stopUpdates() {
|
|
@@ -675,7 +523,6 @@ function createPaymentInstance(initialResponse, deps) {
|
|
|
675
523
|
clearTimeout(state.pollingTimer);
|
|
676
524
|
state.pollingTimer = null;
|
|
677
525
|
}
|
|
678
|
-
closeStream();
|
|
679
526
|
}
|
|
680
527
|
function on(event, handler) {
|
|
681
528
|
state.emitter.on(event, handler);
|
|
@@ -748,21 +595,78 @@ function createPaymentInstance(initialResponse, deps) {
|
|
|
748
595
|
}
|
|
749
596
|
return paymentInstance;
|
|
750
597
|
}
|
|
598
|
+
var DEFAULT_TOLERANCE_SECONDS = 300;
|
|
599
|
+
function decodePayload(payload) {
|
|
600
|
+
return typeof payload === "string" ? payload : Buffer.from(payload).toString("utf8");
|
|
601
|
+
}
|
|
602
|
+
function extractSignedTimestampMs(payloadString) {
|
|
603
|
+
let parsed;
|
|
604
|
+
try {
|
|
605
|
+
parsed = JSON.parse(payloadString);
|
|
606
|
+
} catch {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
if (!parsed || typeof parsed !== "object") {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
const raw = parsed.timestamp;
|
|
613
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
614
|
+
return raw < 1e12 ? raw * 1e3 : raw;
|
|
615
|
+
}
|
|
616
|
+
if (typeof raw === "string") {
|
|
617
|
+
const ms = Date.parse(raw);
|
|
618
|
+
return Number.isNaN(ms) ? null : ms;
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
751
622
|
function verifyWebhookSignature(input) {
|
|
752
|
-
const
|
|
623
|
+
const payloadString = decodePayload(input.payload);
|
|
624
|
+
const payloadBytes = Buffer.from(payloadString, "utf8");
|
|
753
625
|
const expectedSignature = crypto.createHmac("sha256", input.secret).update(payloadBytes).digest("hex");
|
|
754
626
|
const providedBuffer = Buffer.from(input.signature, "hex");
|
|
755
627
|
const expectedBuffer = Buffer.from(expectedSignature, "hex");
|
|
756
628
|
if (providedBuffer.length !== expectedBuffer.length) {
|
|
757
629
|
return false;
|
|
758
630
|
}
|
|
759
|
-
|
|
631
|
+
if (!crypto.timingSafeEqual(providedBuffer, expectedBuffer)) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
const toleranceSeconds = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
635
|
+
if (toleranceSeconds <= 0) {
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
const timestampMs = extractSignedTimestampMs(payloadString);
|
|
639
|
+
if (timestampMs === null) {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
const ageMs = Math.abs(Date.now() - timestampMs);
|
|
643
|
+
return ageMs <= toleranceSeconds * 1e3;
|
|
760
644
|
}
|
|
761
645
|
|
|
762
646
|
// src/sdk.ts
|
|
763
647
|
function generateReference() {
|
|
764
648
|
return crypto.randomBytes(16).toString("hex").slice(0, 15);
|
|
765
649
|
}
|
|
650
|
+
var REFERENCE_MIN_LENGTH = 13;
|
|
651
|
+
var REFERENCE_MAX_LENGTH = 15;
|
|
652
|
+
function resolveReference(reference) {
|
|
653
|
+
if (reference === void 0) {
|
|
654
|
+
return generateReference();
|
|
655
|
+
}
|
|
656
|
+
if (reference.length < REFERENCE_MIN_LENGTH || reference.length > REFERENCE_MAX_LENGTH) {
|
|
657
|
+
throwValidation(
|
|
658
|
+
`reference must be ${REFERENCE_MIN_LENGTH}\u2013${REFERENCE_MAX_LENGTH} characters`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
return reference;
|
|
662
|
+
}
|
|
663
|
+
async function runHook(hook, ...args) {
|
|
664
|
+
if (!hook || hook.enabled === false) return void 0;
|
|
665
|
+
const result = await slangTs.safeTry(async () => hook.fn(...args));
|
|
666
|
+
if (result.isOk) return result.value;
|
|
667
|
+
await slangTs.safeTry(async () => hook.onError(result.error));
|
|
668
|
+
return void 0;
|
|
669
|
+
}
|
|
766
670
|
function throwValidation(message) {
|
|
767
671
|
throw createSdkError({ category: "validation", message });
|
|
768
672
|
}
|
|
@@ -796,12 +700,10 @@ function createSdkInstance(config) {
|
|
|
796
700
|
}),
|
|
797
701
|
pollIntervalMs: config.maxPollIntervalMs,
|
|
798
702
|
maxPollDuration: config.maxPollDurationMs,
|
|
799
|
-
maxPollAttempts: config.maxPollAttempts
|
|
800
|
-
streaming: config.streaming,
|
|
801
|
-
openStream: config.streaming ? transport.openStream : void 0
|
|
703
|
+
maxPollAttempts: config.maxPollAttempts
|
|
802
704
|
};
|
|
803
705
|
async function collectPayment(input) {
|
|
804
|
-
const reference = input.reference
|
|
706
|
+
const reference = resolveReference(input.reference);
|
|
805
707
|
validateAmount(input.amount);
|
|
806
708
|
validateNonEmpty(input.customer.name, "customer.name");
|
|
807
709
|
validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
|
|
@@ -810,24 +712,18 @@ function createSdkInstance(config) {
|
|
|
810
712
|
throwValidation('bank details are required when method is "bank"');
|
|
811
713
|
}
|
|
812
714
|
let payload = { ...input, reference };
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
payload = { ...mutated, reference: mutated.reference ?? reference };
|
|
817
|
-
}
|
|
715
|
+
const mutated = await runHook(config.hooks?.beforeCollect, payload);
|
|
716
|
+
if (mutated != null)
|
|
717
|
+
payload = { ...mutated, reference: mutated.reference ?? reference };
|
|
818
718
|
const result = await transport.send({
|
|
819
719
|
action: SDK_ACTIONS.collectPayment,
|
|
820
720
|
payload
|
|
821
721
|
});
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}) : slangTs.Err(result.error),
|
|
828
|
-
payload
|
|
829
|
-
);
|
|
830
|
-
}
|
|
722
|
+
await runHook(
|
|
723
|
+
config.hooks?.afterCollect,
|
|
724
|
+
result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
|
|
725
|
+
payload
|
|
726
|
+
);
|
|
831
727
|
if (result.isErr) {
|
|
832
728
|
const sdkErr = parseError(result.error);
|
|
833
729
|
return createPaymentInstance(
|
|
@@ -838,7 +734,7 @@ function createSdkInstance(config) {
|
|
|
838
734
|
return createPaymentInstance(result.value, commonDeps);
|
|
839
735
|
}
|
|
840
736
|
async function collectPaymentAndResolve(input) {
|
|
841
|
-
const reference = input.reference
|
|
737
|
+
const reference = resolveReference(input.reference);
|
|
842
738
|
validateAmount(input.amount);
|
|
843
739
|
validateNonEmpty(input.customer.name, "customer.name");
|
|
844
740
|
validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
|
|
@@ -847,31 +743,25 @@ function createSdkInstance(config) {
|
|
|
847
743
|
throwValidation('bank details are required when method is "bank"');
|
|
848
744
|
}
|
|
849
745
|
let payload = { ...input, reference };
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
payload = { ...mutated, reference: mutated.reference ?? reference };
|
|
854
|
-
}
|
|
746
|
+
const mutated = await runHook(config.hooks?.beforeCollect, payload);
|
|
747
|
+
if (mutated != null)
|
|
748
|
+
payload = { ...mutated, reference: mutated.reference ?? reference };
|
|
855
749
|
const result = await transport.send({
|
|
856
750
|
action: SDK_ACTIONS.collectPaymentAndResolve,
|
|
857
751
|
payload
|
|
858
752
|
});
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}) : slangTs.Err(result.error),
|
|
865
|
-
payload
|
|
866
|
-
);
|
|
867
|
-
}
|
|
753
|
+
await runHook(
|
|
754
|
+
config.hooks?.afterCollect,
|
|
755
|
+
result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
|
|
756
|
+
payload
|
|
757
|
+
);
|
|
868
758
|
if (result.isOk) {
|
|
869
759
|
return slangTs.Ok(result.value);
|
|
870
760
|
}
|
|
871
761
|
return slangTs.Err(result.error);
|
|
872
762
|
}
|
|
873
763
|
async function makePayout(input) {
|
|
874
|
-
const reference = input.reference
|
|
764
|
+
const reference = resolveReference(input.reference);
|
|
875
765
|
validateAmount(input.amount);
|
|
876
766
|
validateNonEmpty(input.customer.name, "customer.name");
|
|
877
767
|
validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
|
|
@@ -885,24 +775,18 @@ function createSdkInstance(config) {
|
|
|
885
775
|
"destination.accountNumber"
|
|
886
776
|
);
|
|
887
777
|
let payload = { ...input, reference };
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
payload = { ...mutated, reference: mutated.reference ?? reference };
|
|
892
|
-
}
|
|
778
|
+
const mutated = await runHook(config.hooks?.beforePayout, payload);
|
|
779
|
+
if (mutated != null)
|
|
780
|
+
payload = { ...mutated, reference: mutated.reference ?? reference };
|
|
893
781
|
const result = await transport.send({
|
|
894
782
|
action: SDK_ACTIONS.makePayout,
|
|
895
783
|
payload
|
|
896
784
|
});
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
}) : slangTs.Err(result.error),
|
|
903
|
-
payload
|
|
904
|
-
);
|
|
905
|
-
}
|
|
785
|
+
await runHook(
|
|
786
|
+
config.hooks?.afterPayout,
|
|
787
|
+
result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
|
|
788
|
+
payload
|
|
789
|
+
);
|
|
906
790
|
if (result.isErr) {
|
|
907
791
|
const sdkErr = parseError(result.error);
|
|
908
792
|
return createPaymentInstance(
|
|
@@ -913,7 +797,7 @@ function createSdkInstance(config) {
|
|
|
913
797
|
return createPaymentInstance(result.value, commonDeps);
|
|
914
798
|
}
|
|
915
799
|
async function makePayoutAndResolve(input) {
|
|
916
|
-
const reference = input.reference
|
|
800
|
+
const reference = resolveReference(input.reference);
|
|
917
801
|
validateAmount(input.amount);
|
|
918
802
|
validateNonEmpty(input.customer.name, "customer.name");
|
|
919
803
|
validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
|
|
@@ -927,24 +811,18 @@ function createSdkInstance(config) {
|
|
|
927
811
|
"destination.accountNumber"
|
|
928
812
|
);
|
|
929
813
|
let payload = { ...input, reference };
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
payload = { ...mutated, reference: mutated.reference ?? reference };
|
|
934
|
-
}
|
|
814
|
+
const mutated = await runHook(config.hooks?.beforePayout, payload);
|
|
815
|
+
if (mutated != null)
|
|
816
|
+
payload = { ...mutated, reference: mutated.reference ?? reference };
|
|
935
817
|
const result = await transport.send({
|
|
936
818
|
action: SDK_ACTIONS.makePayoutAndResolve,
|
|
937
819
|
payload
|
|
938
820
|
});
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}) : slangTs.Err(result.error),
|
|
945
|
-
payload
|
|
946
|
-
);
|
|
947
|
-
}
|
|
821
|
+
await runHook(
|
|
822
|
+
config.hooks?.afterPayout,
|
|
823
|
+
result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
|
|
824
|
+
payload
|
|
825
|
+
);
|
|
948
826
|
if (result.isOk) {
|
|
949
827
|
return slangTs.Ok(result.value);
|
|
950
828
|
}
|
|
@@ -986,7 +864,7 @@ function createSdkInstance(config) {
|
|
|
986
864
|
return slangTs.Err(result.error);
|
|
987
865
|
}
|
|
988
866
|
async function createInvoice(input) {
|
|
989
|
-
const reference = input.reference
|
|
867
|
+
const reference = resolveReference(input.reference);
|
|
990
868
|
validateAmount(input.amount);
|
|
991
869
|
validateNonEmpty(input.description, "description");
|
|
992
870
|
if (input.items) {
|
|
@@ -1044,7 +922,8 @@ function createNylonPay(config) {
|
|
|
1044
922
|
throw new Error('apiSecret must start with "nps_"');
|
|
1045
923
|
}
|
|
1046
924
|
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
1047
|
-
const
|
|
925
|
+
const secretHash = crypto.createHash("sha256").update(config.apiSecret).digest("hex").slice(0, 16);
|
|
926
|
+
const instanceKey = `${config.apiKey}:${baseUrl}:${secretHash}`;
|
|
1048
927
|
if (!config.force) {
|
|
1049
928
|
const existing = instances.get(instanceKey);
|
|
1050
929
|
if (existing) return existing;
|
|
@@ -1058,7 +937,6 @@ function createNylonPay(config) {
|
|
|
1058
937
|
maxPollIntervalMs: config.maxPollIntervalMs ?? DEFAULT_MAX_POLL_INTERVAL_MS,
|
|
1059
938
|
maxPollDurationMs: config.maxPollDurationMs ?? DEFAULT_MAX_POLL_DURATION_MS,
|
|
1060
939
|
maxPollAttempts: config.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS,
|
|
1061
|
-
streaming: config.streaming ?? DEFAULT_STREAMING,
|
|
1062
940
|
fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
|
|
1063
941
|
hooks: config.hooks
|
|
1064
942
|
};
|