@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 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/nylon-pay/sdk-spec.md) — the canonical, language-agnostic contract for building Nylon Pay SDKs in any language.
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-123",
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-123" });
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-123" });
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-123" });
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/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,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 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
+ 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 ?? generateReference();
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
- if (config.hooks?.beforeCollect) {
814
- const mutated = await config.hooks.beforeCollect(payload);
815
- if (mutated != null)
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
- 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
- }
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 ?? generateReference();
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
- if (config.hooks?.beforeCollect) {
851
- const mutated = await config.hooks.beforeCollect(payload);
852
- if (mutated != null)
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
- 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
- }
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 ?? generateReference();
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
- if (config.hooks?.beforePayout) {
889
- const mutated = await config.hooks.beforePayout(payload);
890
- if (mutated != null)
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
- 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
- }
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 ?? generateReference();
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
- if (config.hooks?.beforePayout) {
931
- const mutated = await config.hooks.beforePayout(payload);
932
- if (mutated != null)
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
- 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
- }
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 ?? generateReference();
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 instanceKey = `${config.apiKey}:${baseUrl}`;
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
  };