@nile-squad/nylonpay-ts 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -10
- package/dist/index.cjs +150 -114
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +150 -114
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -74,6 +74,7 @@ var SDK_ACTIONS = {
|
|
|
74
74
|
createInvoice: "sdk-create-invoice"
|
|
75
75
|
};
|
|
76
76
|
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
77
|
+
var MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
77
78
|
function generateFingerprint() {
|
|
78
79
|
const components = [
|
|
79
80
|
`type:${type()}`,
|
|
@@ -253,13 +254,13 @@ function createTransport({
|
|
|
253
254
|
async function send(request) {
|
|
254
255
|
const envelope = buildEnvelope(request);
|
|
255
256
|
const signedPayload = envelope.payload;
|
|
256
|
-
const headers = buildAuthHeaders({
|
|
257
|
-
apiKey,
|
|
258
|
-
apiSecret,
|
|
259
|
-
payload: signedPayload
|
|
260
|
-
});
|
|
261
257
|
const bodyString = JSON.stringify(envelope);
|
|
262
258
|
async function attempt(currentAttempt) {
|
|
259
|
+
const headers = buildAuthHeaders({
|
|
260
|
+
apiKey,
|
|
261
|
+
apiSecret,
|
|
262
|
+
payload: signedPayload
|
|
263
|
+
});
|
|
263
264
|
const { controller, cleanup } = withTimeout(timeoutMs);
|
|
264
265
|
try {
|
|
265
266
|
const response = await fetchImpl(baseUrl, {
|
|
@@ -268,6 +269,17 @@ function createTransport({
|
|
|
268
269
|
body: bodyString,
|
|
269
270
|
signal: controller.signal
|
|
270
271
|
});
|
|
272
|
+
const contentLength = response.headers?.get("content-length");
|
|
273
|
+
if (contentLength && Number(contentLength) > MAX_RESPONSE_BYTES) {
|
|
274
|
+
cleanup();
|
|
275
|
+
return Err(
|
|
276
|
+
JSON.stringify({
|
|
277
|
+
category: "internal",
|
|
278
|
+
message: "Received an invalid response from the server",
|
|
279
|
+
retryable: false
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
}
|
|
271
283
|
if (!response.ok) {
|
|
272
284
|
const statusCode = response.status;
|
|
273
285
|
const retryable = RETRYABLE_STATUS_CODES.has(statusCode);
|
|
@@ -298,7 +310,7 @@ function createTransport({
|
|
|
298
310
|
return Err(
|
|
299
311
|
JSON.stringify({
|
|
300
312
|
category: "internal",
|
|
301
|
-
message: "
|
|
313
|
+
message: "Received an invalid response from the server",
|
|
302
314
|
retryable: false
|
|
303
315
|
})
|
|
304
316
|
);
|
|
@@ -311,7 +323,7 @@ function createTransport({
|
|
|
311
323
|
return Err(
|
|
312
324
|
JSON.stringify({
|
|
313
325
|
category: "internal",
|
|
314
|
-
message: "
|
|
326
|
+
message: "Could not verify the server response",
|
|
315
327
|
retryable: false
|
|
316
328
|
})
|
|
317
329
|
);
|
|
@@ -326,7 +338,7 @@ function createTransport({
|
|
|
326
338
|
return Err(
|
|
327
339
|
JSON.stringify({
|
|
328
340
|
category: "internal",
|
|
329
|
-
message: "
|
|
341
|
+
message: "Could not verify the server response",
|
|
330
342
|
retryable: false
|
|
331
343
|
})
|
|
332
344
|
);
|
|
@@ -342,7 +354,7 @@ function createTransport({
|
|
|
342
354
|
const isAbort = error instanceof DOMException && error.name === "AbortError";
|
|
343
355
|
const sdkError = {
|
|
344
356
|
category: isAbort ? "timeout" : "network",
|
|
345
|
-
message: isAbort ?
|
|
357
|
+
message: isAbort ? "The request timed out" : "Could not reach the server, check your network connection and try again",
|
|
346
358
|
retryable: true
|
|
347
359
|
};
|
|
348
360
|
if (currentAttempt < maxRetries) {
|
|
@@ -408,10 +420,16 @@ function createPaymentInstance(initialResponse, deps) {
|
|
|
408
420
|
maxPollDuration: deps.maxPollDuration ?? 3e5,
|
|
409
421
|
maxPollAttempts: deps.maxPollAttempts ?? 150
|
|
410
422
|
};
|
|
411
|
-
function resolveWithError(error) {
|
|
423
|
+
function resolveWithError(error, category, retryable) {
|
|
412
424
|
state.resolved = true;
|
|
413
425
|
stopUpdates();
|
|
414
|
-
|
|
426
|
+
const parsed = parseError(error);
|
|
427
|
+
emitEvent(
|
|
428
|
+
"error",
|
|
429
|
+
parsed.message,
|
|
430
|
+
category ?? parsed.category,
|
|
431
|
+
parsed.retryable
|
|
432
|
+
);
|
|
415
433
|
}
|
|
416
434
|
function emitEvent(event, error, category, retryable) {
|
|
417
435
|
const data = {
|
|
@@ -448,7 +466,8 @@ function createPaymentInstance(initialResponse, deps) {
|
|
|
448
466
|
}
|
|
449
467
|
if (response.reference !== state.reference) {
|
|
450
468
|
resolveWithError(
|
|
451
|
-
|
|
469
|
+
"Received a status update for a different transaction",
|
|
470
|
+
"internal"
|
|
452
471
|
);
|
|
453
472
|
return;
|
|
454
473
|
}
|
|
@@ -470,7 +489,7 @@ function createPaymentInstance(initialResponse, deps) {
|
|
|
470
489
|
if (parsed.category === "not_found") {
|
|
471
490
|
return;
|
|
472
491
|
}
|
|
473
|
-
emitEvent("error", parsed.message);
|
|
492
|
+
emitEvent("error", parsed.message, parsed.category, parsed.retryable);
|
|
474
493
|
state.resolved = true;
|
|
475
494
|
stopUpdates();
|
|
476
495
|
}
|
|
@@ -490,11 +509,17 @@ function createPaymentInstance(initialResponse, deps) {
|
|
|
490
509
|
return;
|
|
491
510
|
}
|
|
492
511
|
if (state.pollAttempts >= state.maxPollAttempts) {
|
|
493
|
-
resolveWithError(
|
|
512
|
+
resolveWithError(
|
|
513
|
+
"Timed out waiting for the transaction status to update",
|
|
514
|
+
"timeout"
|
|
515
|
+
);
|
|
494
516
|
return;
|
|
495
517
|
}
|
|
496
518
|
if (Date.now() - state.pollStartTime >= state.maxPollDuration) {
|
|
497
|
-
resolveWithError(
|
|
519
|
+
resolveWithError(
|
|
520
|
+
"Timed out waiting for the transaction status to update",
|
|
521
|
+
"timeout"
|
|
522
|
+
);
|
|
498
523
|
return;
|
|
499
524
|
}
|
|
500
525
|
state.pollAttempts += 1;
|
|
@@ -614,6 +639,9 @@ function normalizePhone(phone) {
|
|
|
614
639
|
}
|
|
615
640
|
return normalized;
|
|
616
641
|
}
|
|
642
|
+
function isValidPhoneFormat(normalizedPhone) {
|
|
643
|
+
return /^\d{9,15}$/.test(normalizedPhone);
|
|
644
|
+
}
|
|
617
645
|
var DEFAULT_TOLERANCE_SECONDS = 300;
|
|
618
646
|
function decodePayload(payload) {
|
|
619
647
|
return typeof payload === "string" ? payload : Buffer.from(payload).toString("utf8");
|
|
@@ -639,27 +667,34 @@ function extractSignedTimestampMs(payloadString) {
|
|
|
639
667
|
return null;
|
|
640
668
|
}
|
|
641
669
|
function verifyWebhookSignature(input) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
670
|
+
try {
|
|
671
|
+
const payloadString = decodePayload(input.payload);
|
|
672
|
+
const payloadBytes = Buffer.from(payloadString, "utf8");
|
|
673
|
+
const expectedSignature = createHmac("sha256", input.secret).update(payloadBytes).digest("hex");
|
|
674
|
+
const providedBuffer = Buffer.from(input.signature, "hex");
|
|
675
|
+
const expectedBuffer = Buffer.from(expectedSignature, "hex");
|
|
676
|
+
if (providedBuffer.length !== expectedBuffer.length) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
const toleranceSeconds = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
683
|
+
if (toleranceSeconds === 0) {
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
if (toleranceSeconds < 0) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const timestampMs = extractSignedTimestampMs(payloadString);
|
|
690
|
+
if (timestampMs === null) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
const ageMs = Math.abs(Date.now() - timestampMs);
|
|
694
|
+
return ageMs <= toleranceSeconds * 1e3;
|
|
695
|
+
} catch {
|
|
659
696
|
return false;
|
|
660
697
|
}
|
|
661
|
-
const ageMs = Math.abs(Date.now() - timestampMs);
|
|
662
|
-
return ageMs <= toleranceSeconds * 1e3;
|
|
663
698
|
}
|
|
664
699
|
|
|
665
700
|
// src/sdk.ts
|
|
@@ -710,6 +745,56 @@ function validateNonEmpty(value, fieldName) {
|
|
|
710
745
|
throwValidation(`${fieldName} is required`);
|
|
711
746
|
}
|
|
712
747
|
}
|
|
748
|
+
function validatePhoneFormat(normalizedPhone, fieldName) {
|
|
749
|
+
if (!isValidPhoneFormat(normalizedPhone)) {
|
|
750
|
+
throwValidation(`${fieldName} must be a valid phone number`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function prepareCollectPayload(input) {
|
|
754
|
+
const reference = resolveReference(input.reference);
|
|
755
|
+
validateCollectionAmount(input.amount);
|
|
756
|
+
validateNonEmpty(input.customer.name, "customer.name");
|
|
757
|
+
validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
|
|
758
|
+
const normalizedPhone = normalizePhone(input.customer.phoneNumber);
|
|
759
|
+
validatePhoneFormat(normalizedPhone, "customer.phoneNumber");
|
|
760
|
+
validateNonEmpty(input.description, "description");
|
|
761
|
+
if (input.method === "bank" && !input.bank) {
|
|
762
|
+
throwValidation('bank details are required when method is "bank"');
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
...input,
|
|
766
|
+
reference,
|
|
767
|
+
customer: { ...input.customer, phoneNumber: normalizedPhone }
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function preparePayoutPayload(input) {
|
|
771
|
+
const reference = resolveReference(input.reference);
|
|
772
|
+
validatePayoutAmount(input.amount);
|
|
773
|
+
validateNonEmpty(input.customer.name, "customer.name");
|
|
774
|
+
validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
|
|
775
|
+
const normalizedPhone = normalizePhone(input.customer.phoneNumber);
|
|
776
|
+
validatePhoneFormat(normalizedPhone, "customer.phoneNumber");
|
|
777
|
+
validateNonEmpty(input.description, "description");
|
|
778
|
+
validateNonEmpty(
|
|
779
|
+
input.destination.accountHolderName,
|
|
780
|
+
"destination.accountHolderName"
|
|
781
|
+
);
|
|
782
|
+
validateNonEmpty(
|
|
783
|
+
input.destination.accountNumber,
|
|
784
|
+
"destination.accountNumber"
|
|
785
|
+
);
|
|
786
|
+
return {
|
|
787
|
+
...input,
|
|
788
|
+
reference,
|
|
789
|
+
customer: { ...input.customer, phoneNumber: normalizedPhone }
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
function applyBeforeHookMutation(mutated, current, prepare) {
|
|
793
|
+
return prepare({
|
|
794
|
+
...mutated,
|
|
795
|
+
reference: mutated.reference ?? current.reference
|
|
796
|
+
});
|
|
797
|
+
}
|
|
713
798
|
function createSdkInstance(config) {
|
|
714
799
|
const transport = createTransport({
|
|
715
800
|
apiKey: config.apiKey,
|
|
@@ -733,23 +818,15 @@ function createSdkInstance(config) {
|
|
|
733
818
|
maxPollAttempts: config.maxPollAttempts
|
|
734
819
|
};
|
|
735
820
|
async function collectPayment(input) {
|
|
736
|
-
|
|
737
|
-
validateCollectionAmount(input.amount);
|
|
738
|
-
validateNonEmpty(input.customer.name, "customer.name");
|
|
739
|
-
validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
|
|
740
|
-
const normalizedPhone = normalizePhone(input.customer.phoneNumber);
|
|
741
|
-
validateNonEmpty(input.description, "description");
|
|
742
|
-
if (input.method === "bank" && !input.bank) {
|
|
743
|
-
throwValidation('bank details are required when method is "bank"');
|
|
744
|
-
}
|
|
745
|
-
let payload = {
|
|
746
|
-
...input,
|
|
747
|
-
reference,
|
|
748
|
-
customer: { ...input.customer, phoneNumber: normalizedPhone }
|
|
749
|
-
};
|
|
821
|
+
let payload = prepareCollectPayload(input);
|
|
750
822
|
const mutated = await runHook(config.hooks?.beforeCollect, payload);
|
|
751
|
-
if (mutated != null)
|
|
752
|
-
payload =
|
|
823
|
+
if (mutated != null) {
|
|
824
|
+
payload = applyBeforeHookMutation(
|
|
825
|
+
mutated,
|
|
826
|
+
payload,
|
|
827
|
+
prepareCollectPayload
|
|
828
|
+
);
|
|
829
|
+
}
|
|
753
830
|
const result = await transport.send({
|
|
754
831
|
action: SDK_ACTIONS.collectPayment,
|
|
755
832
|
payload
|
|
@@ -757,35 +834,27 @@ function createSdkInstance(config) {
|
|
|
757
834
|
await runHook(
|
|
758
835
|
config.hooks?.afterCollect,
|
|
759
836
|
result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
|
|
760
|
-
payload
|
|
837
|
+
{ ...payload, raw: input }
|
|
761
838
|
);
|
|
762
839
|
if (result.isErr) {
|
|
763
840
|
const sdkErr = parseError(result.error);
|
|
764
841
|
return createPaymentInstance(
|
|
765
|
-
{ reference, status: "pending" },
|
|
842
|
+
{ reference: payload.reference, status: "pending" },
|
|
766
843
|
{ ...commonDeps, initialError: sdkErr }
|
|
767
844
|
);
|
|
768
845
|
}
|
|
769
846
|
return createPaymentInstance(result.value, commonDeps);
|
|
770
847
|
}
|
|
771
848
|
async function collectPaymentAndResolve(input) {
|
|
772
|
-
|
|
773
|
-
validateCollectionAmount(input.amount);
|
|
774
|
-
validateNonEmpty(input.customer.name, "customer.name");
|
|
775
|
-
validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
|
|
776
|
-
const normalizedPhone = normalizePhone(input.customer.phoneNumber);
|
|
777
|
-
validateNonEmpty(input.description, "description");
|
|
778
|
-
if (input.method === "bank" && !input.bank) {
|
|
779
|
-
throwValidation('bank details are required when method is "bank"');
|
|
780
|
-
}
|
|
781
|
-
let payload = {
|
|
782
|
-
...input,
|
|
783
|
-
reference,
|
|
784
|
-
customer: { ...input.customer, phoneNumber: normalizedPhone }
|
|
785
|
-
};
|
|
849
|
+
let payload = prepareCollectPayload(input);
|
|
786
850
|
const mutated = await runHook(config.hooks?.beforeCollect, payload);
|
|
787
|
-
if (mutated != null)
|
|
788
|
-
payload =
|
|
851
|
+
if (mutated != null) {
|
|
852
|
+
payload = applyBeforeHookMutation(
|
|
853
|
+
mutated,
|
|
854
|
+
payload,
|
|
855
|
+
prepareCollectPayload
|
|
856
|
+
);
|
|
857
|
+
}
|
|
789
858
|
const result = await transport.send({
|
|
790
859
|
action: SDK_ACTIONS.collectPaymentAndResolve,
|
|
791
860
|
payload
|
|
@@ -793,7 +862,7 @@ function createSdkInstance(config) {
|
|
|
793
862
|
await runHook(
|
|
794
863
|
config.hooks?.afterCollect,
|
|
795
864
|
result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
|
|
796
|
-
payload
|
|
865
|
+
{ ...payload, raw: input }
|
|
797
866
|
);
|
|
798
867
|
if (result.isOk) {
|
|
799
868
|
return Ok(result.value);
|
|
@@ -801,28 +870,11 @@ function createSdkInstance(config) {
|
|
|
801
870
|
return Err(result.error);
|
|
802
871
|
}
|
|
803
872
|
async function makePayout(input) {
|
|
804
|
-
|
|
805
|
-
validatePayoutAmount(input.amount);
|
|
806
|
-
validateNonEmpty(input.customer.name, "customer.name");
|
|
807
|
-
validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
|
|
808
|
-
const normalizedPhone = normalizePhone(input.customer.phoneNumber);
|
|
809
|
-
validateNonEmpty(input.description, "description");
|
|
810
|
-
validateNonEmpty(
|
|
811
|
-
input.destination.accountHolderName,
|
|
812
|
-
"destination.accountHolderName"
|
|
813
|
-
);
|
|
814
|
-
validateNonEmpty(
|
|
815
|
-
input.destination.accountNumber,
|
|
816
|
-
"destination.accountNumber"
|
|
817
|
-
);
|
|
818
|
-
let payload = {
|
|
819
|
-
...input,
|
|
820
|
-
reference,
|
|
821
|
-
customer: { ...input.customer, phoneNumber: normalizedPhone }
|
|
822
|
-
};
|
|
873
|
+
let payload = preparePayoutPayload(input);
|
|
823
874
|
const mutated = await runHook(config.hooks?.beforePayout, payload);
|
|
824
|
-
if (mutated != null)
|
|
825
|
-
payload =
|
|
875
|
+
if (mutated != null) {
|
|
876
|
+
payload = applyBeforeHookMutation(mutated, payload, preparePayoutPayload);
|
|
877
|
+
}
|
|
826
878
|
const result = await transport.send({
|
|
827
879
|
action: SDK_ACTIONS.makePayout,
|
|
828
880
|
payload
|
|
@@ -830,40 +882,23 @@ function createSdkInstance(config) {
|
|
|
830
882
|
await runHook(
|
|
831
883
|
config.hooks?.afterPayout,
|
|
832
884
|
result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
|
|
833
|
-
payload
|
|
885
|
+
{ ...payload, raw: input }
|
|
834
886
|
);
|
|
835
887
|
if (result.isErr) {
|
|
836
888
|
const sdkErr = parseError(result.error);
|
|
837
889
|
return createPaymentInstance(
|
|
838
|
-
{ reference, status: "pending" },
|
|
890
|
+
{ reference: payload.reference, status: "pending" },
|
|
839
891
|
{ ...commonDeps, initialError: sdkErr }
|
|
840
892
|
);
|
|
841
893
|
}
|
|
842
894
|
return createPaymentInstance(result.value, commonDeps);
|
|
843
895
|
}
|
|
844
896
|
async function makePayoutAndResolve(input) {
|
|
845
|
-
|
|
846
|
-
validatePayoutAmount(input.amount);
|
|
847
|
-
validateNonEmpty(input.customer.name, "customer.name");
|
|
848
|
-
validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
|
|
849
|
-
const normalizedPhone = normalizePhone(input.customer.phoneNumber);
|
|
850
|
-
validateNonEmpty(input.description, "description");
|
|
851
|
-
validateNonEmpty(
|
|
852
|
-
input.destination.accountHolderName,
|
|
853
|
-
"destination.accountHolderName"
|
|
854
|
-
);
|
|
855
|
-
validateNonEmpty(
|
|
856
|
-
input.destination.accountNumber,
|
|
857
|
-
"destination.accountNumber"
|
|
858
|
-
);
|
|
859
|
-
let payload = {
|
|
860
|
-
...input,
|
|
861
|
-
reference,
|
|
862
|
-
customer: { ...input.customer, phoneNumber: normalizedPhone }
|
|
863
|
-
};
|
|
897
|
+
let payload = preparePayoutPayload(input);
|
|
864
898
|
const mutated = await runHook(config.hooks?.beforePayout, payload);
|
|
865
|
-
if (mutated != null)
|
|
866
|
-
payload =
|
|
899
|
+
if (mutated != null) {
|
|
900
|
+
payload = applyBeforeHookMutation(mutated, payload, preparePayoutPayload);
|
|
901
|
+
}
|
|
867
902
|
const result = await transport.send({
|
|
868
903
|
action: SDK_ACTIONS.makePayoutAndResolve,
|
|
869
904
|
payload
|
|
@@ -871,7 +906,7 @@ function createSdkInstance(config) {
|
|
|
871
906
|
await runHook(
|
|
872
907
|
config.hooks?.afterPayout,
|
|
873
908
|
result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
|
|
874
|
-
payload
|
|
909
|
+
{ ...payload, raw: input }
|
|
875
910
|
);
|
|
876
911
|
if (result.isOk) {
|
|
877
912
|
return Ok(result.value);
|
|
@@ -905,6 +940,7 @@ function createSdkInstance(config) {
|
|
|
905
940
|
async function verifyPhone(input) {
|
|
906
941
|
validateNonEmpty(input.phoneNumber, "phoneNumber");
|
|
907
942
|
const normalizedPhone = normalizePhone(input.phoneNumber);
|
|
943
|
+
validatePhoneFormat(normalizedPhone, "phoneNumber");
|
|
908
944
|
const result = await transport.send({
|
|
909
945
|
action: SDK_ACTIONS.verifyPhone,
|
|
910
946
|
payload: { ...input, phoneNumber: normalizedPhone }
|