@nile-squad/nylonpay-ts 1.0.7 → 1.0.8

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.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/sdk.ts
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 DEFAULT_STREAMING = true;
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.localeCompare(secondKey)
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
- const isValid = verifyResponseSignature(
348
- strippedData,
349
- responseSignature,
350
- apiSecret
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
- function openStream(input) {
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
- }, state.pollIntervalMs);
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,65 @@ 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 payloadBytes = typeof input.payload === "string" ? Buffer.from(input.payload, "utf8") : Buffer.from(input.payload);
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
- return crypto.timingSafeEqual(providedBuffer, expectedBuffer);
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
+ async function runHook(hook, ...args) {
651
+ if (!hook || hook.enabled === false) return void 0;
652
+ const result = await slangTs.safeTry(async () => hook.fn(...args));
653
+ if (result.isOk) return result.value;
654
+ await slangTs.safeTry(async () => hook.onError(result.error));
655
+ return void 0;
656
+ }
766
657
  function throwValidation(message) {
767
658
  throw createSdkError({ category: "validation", message });
768
659
  }
@@ -796,9 +687,7 @@ function createSdkInstance(config) {
796
687
  }),
797
688
  pollIntervalMs: config.maxPollIntervalMs,
798
689
  maxPollDuration: config.maxPollDurationMs,
799
- maxPollAttempts: config.maxPollAttempts,
800
- streaming: config.streaming,
801
- openStream: config.streaming ? transport.openStream : void 0
690
+ maxPollAttempts: config.maxPollAttempts
802
691
  };
803
692
  async function collectPayment(input) {
804
693
  const reference = input.reference ?? generateReference();
@@ -810,24 +699,18 @@ function createSdkInstance(config) {
810
699
  throwValidation('bank details are required when method is "bank"');
811
700
  }
812
701
  let payload = { ...input, reference };
813
- if (config.hooks?.beforeCollect) {
814
- const mutated = await config.hooks.beforeCollect(payload);
815
- if (mutated != null)
816
- payload = { ...mutated, reference: mutated.reference ?? reference };
817
- }
702
+ const mutated = await runHook(config.hooks?.beforeCollect, payload);
703
+ if (mutated != null)
704
+ payload = { ...mutated, reference: mutated.reference ?? reference };
818
705
  const result = await transport.send({
819
706
  action: SDK_ACTIONS.collectPayment,
820
707
  payload
821
708
  });
822
- if (config.hooks?.afterCollect) {
823
- await config.hooks.afterCollect(
824
- result.isOk ? slangTs.Ok({
825
- reference: result.value.reference,
826
- status: result.value.status
827
- }) : slangTs.Err(result.error),
828
- payload
829
- );
830
- }
709
+ await runHook(
710
+ config.hooks?.afterCollect,
711
+ result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
712
+ payload
713
+ );
831
714
  if (result.isErr) {
832
715
  const sdkErr = parseError(result.error);
833
716
  return createPaymentInstance(
@@ -847,24 +730,18 @@ function createSdkInstance(config) {
847
730
  throwValidation('bank details are required when method is "bank"');
848
731
  }
849
732
  let payload = { ...input, reference };
850
- if (config.hooks?.beforeCollect) {
851
- const mutated = await config.hooks.beforeCollect(payload);
852
- if (mutated != null)
853
- payload = { ...mutated, reference: mutated.reference ?? reference };
854
- }
733
+ const mutated = await runHook(config.hooks?.beforeCollect, payload);
734
+ if (mutated != null)
735
+ payload = { ...mutated, reference: mutated.reference ?? reference };
855
736
  const result = await transport.send({
856
737
  action: SDK_ACTIONS.collectPaymentAndResolve,
857
738
  payload
858
739
  });
859
- if (config.hooks?.afterCollect) {
860
- await config.hooks.afterCollect(
861
- result.isOk ? slangTs.Ok({
862
- reference: result.value.reference,
863
- status: result.value.status
864
- }) : slangTs.Err(result.error),
865
- payload
866
- );
867
- }
740
+ await runHook(
741
+ config.hooks?.afterCollect,
742
+ result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
743
+ payload
744
+ );
868
745
  if (result.isOk) {
869
746
  return slangTs.Ok(result.value);
870
747
  }
@@ -885,24 +762,18 @@ function createSdkInstance(config) {
885
762
  "destination.accountNumber"
886
763
  );
887
764
  let payload = { ...input, reference };
888
- if (config.hooks?.beforePayout) {
889
- const mutated = await config.hooks.beforePayout(payload);
890
- if (mutated != null)
891
- payload = { ...mutated, reference: mutated.reference ?? reference };
892
- }
765
+ const mutated = await runHook(config.hooks?.beforePayout, payload);
766
+ if (mutated != null)
767
+ payload = { ...mutated, reference: mutated.reference ?? reference };
893
768
  const result = await transport.send({
894
769
  action: SDK_ACTIONS.makePayout,
895
770
  payload
896
771
  });
897
- if (config.hooks?.afterPayout) {
898
- await config.hooks.afterPayout(
899
- result.isOk ? slangTs.Ok({
900
- reference: result.value.reference,
901
- status: result.value.status
902
- }) : slangTs.Err(result.error),
903
- payload
904
- );
905
- }
772
+ await runHook(
773
+ config.hooks?.afterPayout,
774
+ result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
775
+ payload
776
+ );
906
777
  if (result.isErr) {
907
778
  const sdkErr = parseError(result.error);
908
779
  return createPaymentInstance(
@@ -927,24 +798,18 @@ function createSdkInstance(config) {
927
798
  "destination.accountNumber"
928
799
  );
929
800
  let payload = { ...input, reference };
930
- if (config.hooks?.beforePayout) {
931
- const mutated = await config.hooks.beforePayout(payload);
932
- if (mutated != null)
933
- payload = { ...mutated, reference: mutated.reference ?? reference };
934
- }
801
+ const mutated = await runHook(config.hooks?.beforePayout, payload);
802
+ if (mutated != null)
803
+ payload = { ...mutated, reference: mutated.reference ?? reference };
935
804
  const result = await transport.send({
936
805
  action: SDK_ACTIONS.makePayoutAndResolve,
937
806
  payload
938
807
  });
939
- if (config.hooks?.afterPayout) {
940
- await config.hooks.afterPayout(
941
- result.isOk ? slangTs.Ok({
942
- reference: result.value.reference,
943
- status: result.value.status
944
- }) : slangTs.Err(result.error),
945
- payload
946
- );
947
- }
808
+ await runHook(
809
+ config.hooks?.afterPayout,
810
+ result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
811
+ payload
812
+ );
948
813
  if (result.isOk) {
949
814
  return slangTs.Ok(result.value);
950
815
  }
@@ -1044,7 +909,8 @@ function createNylonPay(config) {
1044
909
  throw new Error('apiSecret must start with "nps_"');
1045
910
  }
1046
911
  const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
1047
- const instanceKey = `${config.apiKey}:${baseUrl}`;
912
+ const secretHash = crypto.createHash("sha256").update(config.apiSecret).digest("hex").slice(0, 16);
913
+ const instanceKey = `${config.apiKey}:${baseUrl}:${secretHash}`;
1048
914
  if (!config.force) {
1049
915
  const existing = instances.get(instanceKey);
1050
916
  if (existing) return existing;
@@ -1058,7 +924,6 @@ function createNylonPay(config) {
1058
924
  maxPollIntervalMs: config.maxPollIntervalMs ?? DEFAULT_MAX_POLL_INTERVAL_MS,
1059
925
  maxPollDurationMs: config.maxPollDurationMs ?? DEFAULT_MAX_POLL_DURATION_MS,
1060
926
  maxPollAttempts: config.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS,
1061
- streaming: config.streaming ?? DEFAULT_STREAMING,
1062
927
  fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
1063
928
  hooks: config.hooks
1064
929
  };