@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/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: "Response missing status field",
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: "Response signature missing",
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: "Response signature verification failed",
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 ? `Request timed out after ${timeoutMs}ms` : String(error),
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
- emitEvent("error", parseError(error).message);
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
- `Reference mismatch: expected ${state.reference} but got ${response.reference}`
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("Polling timeout: exceeded maximum attempts");
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("Polling timeout: exceeded maximum duration");
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
- const payloadString = decodePayload(input.payload);
643
- const payloadBytes = Buffer.from(payloadString, "utf8");
644
- const expectedSignature = createHmac("sha256", input.secret).update(payloadBytes).digest("hex");
645
- const providedBuffer = Buffer.from(input.signature, "hex");
646
- const expectedBuffer = Buffer.from(expectedSignature, "hex");
647
- if (providedBuffer.length !== expectedBuffer.length) {
648
- return false;
649
- }
650
- if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
651
- return false;
652
- }
653
- const toleranceSeconds = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
654
- if (toleranceSeconds <= 0) {
655
- return true;
656
- }
657
- const timestampMs = extractSignedTimestampMs(payloadString);
658
- if (timestampMs === null) {
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
- const reference = resolveReference(input.reference);
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 = { ...mutated, reference: mutated.reference ?? reference };
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
- const reference = resolveReference(input.reference);
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 = { ...mutated, reference: mutated.reference ?? reference };
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
- const reference = resolveReference(input.reference);
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 = { ...mutated, reference: mutated.reference ?? reference };
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
- const reference = resolveReference(input.reference);
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 = { ...mutated, reference: mutated.reference ?? reference };
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 }