@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.d.ts CHANGED
@@ -149,6 +149,14 @@ type VerifyWebhookInput = {
149
149
  payload: string | Uint8Array;
150
150
  signature: string;
151
151
  secret: string;
152
+ /**
153
+ * Replay-protection window in seconds. After the signature is verified, the
154
+ * timestamp carried inside the signed body must be within this many seconds of
155
+ * now, or verification fails. Defaults to 300 (5 minutes). Set to `0` to
156
+ * disable the freshness check (not recommended — a captured webhook then
157
+ * verifies forever).
158
+ */
159
+ toleranceSeconds?: number;
152
160
  };
153
161
  /**
154
162
  * Full transaction record returned by lookups, event handlers, and the
@@ -245,16 +253,39 @@ type AfterPayoutHook = (result: Result<{
245
253
  reference: string;
246
254
  status: string;
247
255
  }, string>, input: MakePayoutInput) => void | Promise<void>;
256
+ /**
257
+ * Wrapper applied to every lifecycle hook. The SDK runs `fn` inside `safeTry`,
258
+ * so a throw or rejection in merchant code never bubbles into the payment flow —
259
+ * it is routed to `onError` instead.
260
+ *
261
+ * WHY `onError` is required: an unhandled hook failure in a payments SDK is the
262
+ * worst kind of silent bug (the payment "succeeds" while a wallet credit or
263
+ * fulfillment side-effect was lost). Forcing the merchant to declare what
264
+ * happens on failure replaces both the old "throw and maybe crash" behaviour and
265
+ * a silent `catch {}` with an explicit, type-enforced decision.
266
+ */
267
+ type SdkHook<TFn> = {
268
+ /** Set `false` to disable this hook without removing its config. Default: true. */
269
+ enabled?: boolean;
270
+ /** The hook implementation. */
271
+ fn: TFn;
272
+ /**
273
+ * Required. Receives the thrown/rejected value if `fn` fails. Runs inside
274
+ * `safeTry` too, so an error here is contained as well.
275
+ */
276
+ onError: (error: unknown) => void | Promise<void>;
277
+ };
248
278
  /**
249
279
  * Lifecycle hooks registered once at SDK creation. Each hook fires on every
250
280
  * matching operation — use them for cross-cutting concerns like logging,
251
- * audit trails, and payload enrichment.
281
+ * audit trails, and payload enrichment. Every hook is wrapped in {@link SdkHook}
282
+ * so merchant code can never crash the payment flow.
252
283
  */
253
284
  type SdkHooks = {
254
- beforeCollect?: BeforeCollectHook;
255
- afterCollect?: AfterCollectHook;
256
- beforePayout?: BeforePayoutHook;
257
- afterPayout?: AfterPayoutHook;
285
+ beforeCollect?: SdkHook<BeforeCollectHook>;
286
+ afterCollect?: SdkHook<AfterCollectHook>;
287
+ beforePayout?: SdkHook<BeforePayoutHook>;
288
+ afterPayout?: SdkHook<AfterPayoutHook>;
258
289
  };
259
290
  /**
260
291
  * SDK configuration supplied by the merchant at initialization.
@@ -273,12 +304,6 @@ type NylonPayConfig = {
273
304
  maxPollIntervalMs?: number;
274
305
  maxPollDurationMs?: number;
275
306
  maxPollAttempts?: number;
276
- /**
277
- * Receive status updates over an SSE stream (default `true`), falling back to
278
- * polling automatically when the stream is unavailable. Set `false` to poll
279
- * only.
280
- */
281
- streaming?: boolean;
282
307
  fetch?: typeof globalThis.fetch;
283
308
  /** Force a new instance even if one already exists for this key+url pair. */
284
309
  force?: boolean;
@@ -595,9 +620,9 @@ interface PaymentInstance {
595
620
  * Factory function to create a Nylon Pay SDK instance.
596
621
  * This is the main entry point for merchants.
597
622
  *
598
- * Calling createNylonPay with the same apiKey and baseUrl returns the same
599
- * instance (singleton per key+url pair). Pass { force: true } to create a
600
- * fresh instance and replace the cached one.
623
+ * Calling createNylonPay with the same apiKey, apiSecret and baseUrl returns the
624
+ * same instance (singleton per key+secret+url). Rotating the secret yields a
625
+ * fresh instance. Pass { force: true } to force a new instance regardless.
601
626
  *
602
627
  * @example
603
628
  * ```ts
@@ -613,9 +638,9 @@ interface PaymentInstance {
613
638
  /**
614
639
  * Create a Nylon Pay SDK instance.
615
640
  *
616
- * Returns the same instance for the same apiKey + baseUrl combination unless
617
- * { force: true } is passed. Use your test keys for sandbox, production keys
618
- * for live.
641
+ * Returns the same instance for the same apiKey + apiSecret + baseUrl
642
+ * combination unless { force: true } is passed. Use your test keys for sandbox,
643
+ * production keys for live.
619
644
  *
620
645
  * @param config - SDK configuration with apiKey and apiSecret
621
646
  * @returns SDK instance with all payment operations
@@ -663,13 +688,19 @@ declare function parseError(error: string): SdkError;
663
688
  */
664
689
 
665
690
  /**
666
- * Verify that a webhook payload was signed by Nylon Pay.
667
- * Operates on raw payload bytes, NOT parsed JSON (spec invariant #8).
691
+ * Verify that a webhook payload was genuinely sent by Nylon Pay.
692
+ *
693
+ * Two checks, both must pass:
694
+ * 1. **Authenticity** — HMAC-SHA256 over the raw payload bytes (NOT parsed
695
+ * JSON, spec invariant #8) matches the provided signature.
696
+ * 2. **Freshness** — the `timestamp` carried inside the signed body is within
697
+ * `toleranceSeconds` of now (default 300s). This is what stops a replay: a
698
+ * captured `(body, signature)` pair stays cryptographically valid forever,
699
+ * but its embedded timestamp goes stale. Every genuine delivery, including
700
+ * retries hours later, is re-stamped and re-signed, so this never rejects
701
+ * legitimate traffic. Pass `toleranceSeconds: 0` to skip this check.
668
702
  *
669
- * @param input.payload - Raw request body as string or Uint8Array
670
- * @param input.signature - Signature from the webhook header
671
- * @param input.secret - Merchant's webhook secret
672
- * @returns True when the signature is valid
703
+ * @returns True when the signature is valid and (when enforced) the webhook is fresh
673
704
  */
674
705
  declare function verifyWebhookSignature(input: VerifyWebhookInput): boolean;
675
706
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { createHash, createHmac, timingSafeEqual, randomBytes } from 'crypto';
2
2
  import { Ok, Err, safeTry } from 'slang-ts';
3
3
  import { type, platform, arch, release, hostname } from 'os';
4
4
 
5
- // src/sdk.ts
5
+ // src/create-nylon-pay.ts
6
6
 
7
7
  // src/pubsub.ts
8
8
  function createEmitter() {
@@ -61,9 +61,7 @@ var DEFAULT_MAX_RETRIES = 3;
61
61
  var DEFAULT_MAX_POLL_INTERVAL_MS = 2e3;
62
62
  var DEFAULT_MAX_POLL_DURATION_MS = 3e5;
63
63
  var DEFAULT_MAX_POLL_ATTEMPTS = 150;
64
- var DEFAULT_STREAMING = true;
65
- var STREAM_PATH = "/sse/transaction";
66
- var MAX_STREAM_RECONNECTS = 2;
64
+ var POLL_JITTER_MS = 250;
67
65
  var SDK_SERVICE = "sdk";
68
66
  var SDK_ACTIONS = {
69
67
  collectPayment: "sdk-collect-payment",
@@ -91,13 +89,22 @@ function generateFingerprint() {
91
89
  function generateNonce(length = 16) {
92
90
  return randomBytes(length).toString("hex");
93
91
  }
92
+ function compareByCodePoint(first, second) {
93
+ if (first < second) {
94
+ return -1;
95
+ }
96
+ if (first > second) {
97
+ return 1;
98
+ }
99
+ return 0;
100
+ }
94
101
  function sortValue(value) {
95
102
  if (Array.isArray(value)) {
96
103
  return value.map((entry) => sortValue(entry));
97
104
  }
98
105
  if (value && typeof value === "object") {
99
106
  const sortedEntries = Object.entries(value).sort(
100
- ([firstKey], [secondKey]) => firstKey.localeCompare(secondKey)
107
+ ([firstKey], [secondKey]) => compareByCodePoint(firstKey, secondKey)
101
108
  );
102
109
  return Object.fromEntries(
103
110
  sortedEntries.map(([entryKey, entryValue]) => [
@@ -121,42 +128,6 @@ function createSignature(input) {
121
128
  function createTimestamp() {
122
129
  return Date.now().toString();
123
130
  }
124
-
125
- // src/sse-parse.ts
126
- function parseBlock(block) {
127
- let event = "message";
128
- const dataLines = [];
129
- for (const rawLine of block.split("\n")) {
130
- const line = rawLine.replace(/\r$/, "");
131
- if (line.startsWith(":")) {
132
- continue;
133
- }
134
- if (line.startsWith("event:")) {
135
- event = line.slice("event:".length).trim();
136
- } else if (line.startsWith("data:")) {
137
- dataLines.push(line.slice("data:".length).trim());
138
- }
139
- }
140
- if (dataLines.length === 0) {
141
- return null;
142
- }
143
- return { event, data: dataLines.join("\n") };
144
- }
145
- function parseSseBuffer(buffer) {
146
- const messages = [];
147
- let rest = buffer;
148
- let separator = rest.indexOf("\n\n");
149
- while (separator !== -1) {
150
- const block = rest.slice(0, separator);
151
- rest = rest.slice(separator + 2);
152
- const message = parseBlock(block);
153
- if (message) {
154
- messages.push(message);
155
- }
156
- separator = rest.indexOf("\n\n");
157
- }
158
- return { messages, rest };
159
- }
160
131
  function verifyResponseSignature(data, signature, secret) {
161
132
  const expectedSignature = createHmac("sha256", secret).update(createCanonicalPayload(data)).digest("hex");
162
133
  const providedBuffer = Buffer.from(signature, "hex");
@@ -169,13 +140,6 @@ function verifyResponseSignature(data, signature, secret) {
169
140
 
170
141
  // src/transport.ts
171
142
  var CACHED_FINGERPRINT = generateFingerprint();
172
- function streamUrl(baseUrl) {
173
- try {
174
- return new URL(baseUrl).origin + STREAM_PATH;
175
- } catch {
176
- return baseUrl.replace(/\/api\/services\/?$/u, "") + STREAM_PATH;
177
- }
178
- }
179
143
  var KNOWN_CATEGORIES = /* @__PURE__ */ new Set([
180
144
  "auth",
181
145
  "validation",
@@ -341,22 +305,30 @@ function createTransport({
341
305
  const { status, message, data } = responseBody;
342
306
  if (status === true) {
343
307
  const { data: strippedData, responseSignature } = stripResponseSignature(data);
344
- if (responseSignature) {
345
- const isValid = verifyResponseSignature(
346
- strippedData,
347
- responseSignature,
348
- apiSecret
308
+ if (!responseSignature) {
309
+ cleanup();
310
+ return Err(
311
+ JSON.stringify({
312
+ category: "internal",
313
+ message: "Response signature missing",
314
+ retryable: false
315
+ })
316
+ );
317
+ }
318
+ const isValid = verifyResponseSignature(
319
+ strippedData,
320
+ responseSignature,
321
+ apiSecret
322
+ );
323
+ if (!isValid) {
324
+ cleanup();
325
+ return Err(
326
+ JSON.stringify({
327
+ category: "internal",
328
+ message: "Response signature verification failed",
329
+ retryable: false
330
+ })
349
331
  );
350
- if (!isValid) {
351
- cleanup();
352
- return Err(
353
- JSON.stringify({
354
- category: "internal",
355
- message: "Response signature verification failed",
356
- retryable: false
357
- })
358
- );
359
- }
360
332
  }
361
333
  cleanup();
362
334
  return Ok(strippedData);
@@ -381,91 +353,7 @@ function createTransport({
381
353
  }
382
354
  return attempt(0);
383
355
  }
384
- function openStream(input) {
385
- const controller = new AbortController();
386
- let closed = false;
387
- const close = () => {
388
- if (closed) {
389
- return;
390
- }
391
- closed = true;
392
- controller.abort();
393
- };
394
- const payload = {
395
- reference: input.reference,
396
- _fingerprint: CACHED_FINGERPRINT
397
- };
398
- const headers = buildAuthHeaders({ apiKey, apiSecret, payload });
399
- const url = streamUrl(baseUrl);
400
- const run = async () => {
401
- const fetchResult = await safeTry(
402
- () => fetchImpl(url, {
403
- method: "POST",
404
- headers,
405
- body: JSON.stringify(payload),
406
- signal: controller.signal
407
- })
408
- );
409
- if (fetchResult.isErr) {
410
- if (!closed) {
411
- input.onError(fetchResult.error);
412
- }
413
- return;
414
- }
415
- const response = fetchResult.value;
416
- if (!response.ok || !response.body) {
417
- const textResult = await safeTry(() => response.text());
418
- const message = textResult.isOk ? textResult.value : `HTTP ${response.status}`;
419
- if (!closed) {
420
- input.onError(message || `HTTP ${response.status}`);
421
- }
422
- return;
423
- }
424
- const reader = response.body.getReader();
425
- const decoder = new TextDecoder();
426
- let buffer = "";
427
- while (!closed) {
428
- const chunkResult = await safeTry(() => reader.read());
429
- if (chunkResult.isErr) {
430
- if (!closed) {
431
- input.onError(chunkResult.error);
432
- }
433
- return;
434
- }
435
- const chunk = chunkResult.value;
436
- if (chunk.done) {
437
- break;
438
- }
439
- buffer += decoder.decode(chunk.value, { stream: true });
440
- const { messages, rest } = parseSseBuffer(buffer);
441
- buffer = rest;
442
- for (const message of messages) {
443
- if (message.event === "status") {
444
- const statusResult = await safeTry(
445
- () => JSON.parse(message.data)
446
- );
447
- if (statusResult.isOk) {
448
- input.onStatus(statusResult.value);
449
- }
450
- } else if (message.event === "error") {
451
- const errDataResult = await safeTry(
452
- () => JSON.parse(message.data)
453
- );
454
- const text = errDataResult.isOk ? errDataResult.value.message ?? message.data : message.data;
455
- close();
456
- input.onError(text);
457
- return;
458
- }
459
- }
460
- }
461
- if (!closed) {
462
- input.onClose();
463
- }
464
- };
465
- void run();
466
- return { close };
467
- }
468
- return { send, openStream, parseError };
356
+ return { send, parseError };
469
357
  }
470
358
  function parseError(error) {
471
359
  try {
@@ -515,11 +403,7 @@ function createPaymentInstance(initialResponse, deps) {
515
403
  fetchTransaction: deps.fetchTransaction,
516
404
  pollIntervalMs: deps.pollIntervalMs ?? 2e3,
517
405
  maxPollDuration: deps.maxPollDuration ?? 3e5,
518
- maxPollAttempts: deps.maxPollAttempts ?? 150,
519
- streaming: deps.streaming ?? false,
520
- openStream: deps.openStream,
521
- streamHandle: null,
522
- streamReconnects: 0
406
+ maxPollAttempts: deps.maxPollAttempts ?? 150
523
407
  };
524
408
  function resolveWithError(error) {
525
409
  state.resolved = true;
@@ -555,6 +439,9 @@ function createPaymentInstance(initialResponse, deps) {
555
439
  stopUpdates();
556
440
  }
557
441
  async function handleStatusUpdate(response) {
442
+ if (state.resolved) {
443
+ return;
444
+ }
558
445
  if (response.reference !== state.reference) {
559
446
  resolveWithError(
560
447
  `Reference mismatch: expected ${state.reference} but got ${response.reference}`
@@ -588,10 +475,11 @@ function createPaymentInstance(initialResponse, deps) {
588
475
  if (state.resolved || state.pollingTimer) {
589
476
  return;
590
477
  }
478
+ const delay2 = state.pollIntervalMs + Math.random() * POLL_JITTER_MS;
591
479
  state.pollingTimer = setTimeout(() => {
592
480
  state.pollingTimer = null;
593
481
  void pollStatus();
594
- }, state.pollIntervalMs);
482
+ }, delay2);
595
483
  }
596
484
  async function pollStatus() {
597
485
  if (state.resolved) {
@@ -619,42 +507,6 @@ function createPaymentInstance(initialResponse, deps) {
619
507
  }
620
508
  scheduleNextPoll();
621
509
  }
622
- function closeStream() {
623
- if (state.streamHandle) {
624
- state.streamHandle.close();
625
- state.streamHandle = null;
626
- }
627
- }
628
- function startStream() {
629
- if (state.resolved || !state.openStream) {
630
- return;
631
- }
632
- state.streamHandle = state.openStream({
633
- reference: state.reference,
634
- onStatus: (status) => {
635
- void handleStatusUpdate(status);
636
- },
637
- onError: () => handleStreamFailure(),
638
- onClose: () => handleStreamFailure()
639
- });
640
- }
641
- function handleStreamFailure() {
642
- closeStream();
643
- if (state.resolved) {
644
- return;
645
- }
646
- if (state.streamReconnects < MAX_STREAM_RECONNECTS) {
647
- state.streamReconnects += 1;
648
- const backoff = 500 * 2 ** (state.streamReconnects - 1) + Math.random() * 250;
649
- setTimeout(() => {
650
- if (!state.resolved) {
651
- startStream();
652
- }
653
- }, backoff);
654
- return;
655
- }
656
- scheduleNextPoll();
657
- }
658
510
  function startUpdates() {
659
511
  if (TERMINAL_STATES.has(state.status)) {
660
512
  setTimeout(() => {
@@ -662,10 +514,6 @@ function createPaymentInstance(initialResponse, deps) {
662
514
  }, 0);
663
515
  return;
664
516
  }
665
- if (state.streaming && state.openStream) {
666
- startStream();
667
- return;
668
- }
669
517
  scheduleNextPoll();
670
518
  }
671
519
  function stopUpdates() {
@@ -673,7 +521,6 @@ function createPaymentInstance(initialResponse, deps) {
673
521
  clearTimeout(state.pollingTimer);
674
522
  state.pollingTimer = null;
675
523
  }
676
- closeStream();
677
524
  }
678
525
  function on(event, handler) {
679
526
  state.emitter.on(event, handler);
@@ -746,21 +593,65 @@ function createPaymentInstance(initialResponse, deps) {
746
593
  }
747
594
  return paymentInstance;
748
595
  }
596
+ var DEFAULT_TOLERANCE_SECONDS = 300;
597
+ function decodePayload(payload) {
598
+ return typeof payload === "string" ? payload : Buffer.from(payload).toString("utf8");
599
+ }
600
+ function extractSignedTimestampMs(payloadString) {
601
+ let parsed;
602
+ try {
603
+ parsed = JSON.parse(payloadString);
604
+ } catch {
605
+ return null;
606
+ }
607
+ if (!parsed || typeof parsed !== "object") {
608
+ return null;
609
+ }
610
+ const raw = parsed.timestamp;
611
+ if (typeof raw === "number" && Number.isFinite(raw)) {
612
+ return raw < 1e12 ? raw * 1e3 : raw;
613
+ }
614
+ if (typeof raw === "string") {
615
+ const ms = Date.parse(raw);
616
+ return Number.isNaN(ms) ? null : ms;
617
+ }
618
+ return null;
619
+ }
749
620
  function verifyWebhookSignature(input) {
750
- const payloadBytes = typeof input.payload === "string" ? Buffer.from(input.payload, "utf8") : Buffer.from(input.payload);
621
+ const payloadString = decodePayload(input.payload);
622
+ const payloadBytes = Buffer.from(payloadString, "utf8");
751
623
  const expectedSignature = createHmac("sha256", input.secret).update(payloadBytes).digest("hex");
752
624
  const providedBuffer = Buffer.from(input.signature, "hex");
753
625
  const expectedBuffer = Buffer.from(expectedSignature, "hex");
754
626
  if (providedBuffer.length !== expectedBuffer.length) {
755
627
  return false;
756
628
  }
757
- return timingSafeEqual(providedBuffer, expectedBuffer);
629
+ if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
630
+ return false;
631
+ }
632
+ const toleranceSeconds = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
633
+ if (toleranceSeconds <= 0) {
634
+ return true;
635
+ }
636
+ const timestampMs = extractSignedTimestampMs(payloadString);
637
+ if (timestampMs === null) {
638
+ return false;
639
+ }
640
+ const ageMs = Math.abs(Date.now() - timestampMs);
641
+ return ageMs <= toleranceSeconds * 1e3;
758
642
  }
759
643
 
760
644
  // src/sdk.ts
761
645
  function generateReference() {
762
646
  return randomBytes(16).toString("hex").slice(0, 15);
763
647
  }
648
+ async function runHook(hook, ...args) {
649
+ if (!hook || hook.enabled === false) return void 0;
650
+ const result = await safeTry(async () => hook.fn(...args));
651
+ if (result.isOk) return result.value;
652
+ await safeTry(async () => hook.onError(result.error));
653
+ return void 0;
654
+ }
764
655
  function throwValidation(message) {
765
656
  throw createSdkError({ category: "validation", message });
766
657
  }
@@ -794,9 +685,7 @@ function createSdkInstance(config) {
794
685
  }),
795
686
  pollIntervalMs: config.maxPollIntervalMs,
796
687
  maxPollDuration: config.maxPollDurationMs,
797
- maxPollAttempts: config.maxPollAttempts,
798
- streaming: config.streaming,
799
- openStream: config.streaming ? transport.openStream : void 0
688
+ maxPollAttempts: config.maxPollAttempts
800
689
  };
801
690
  async function collectPayment(input) {
802
691
  const reference = input.reference ?? generateReference();
@@ -808,24 +697,18 @@ function createSdkInstance(config) {
808
697
  throwValidation('bank details are required when method is "bank"');
809
698
  }
810
699
  let payload = { ...input, reference };
811
- if (config.hooks?.beforeCollect) {
812
- const mutated = await config.hooks.beforeCollect(payload);
813
- if (mutated != null)
814
- payload = { ...mutated, reference: mutated.reference ?? reference };
815
- }
700
+ const mutated = await runHook(config.hooks?.beforeCollect, payload);
701
+ if (mutated != null)
702
+ payload = { ...mutated, reference: mutated.reference ?? reference };
816
703
  const result = await transport.send({
817
704
  action: SDK_ACTIONS.collectPayment,
818
705
  payload
819
706
  });
820
- if (config.hooks?.afterCollect) {
821
- await config.hooks.afterCollect(
822
- result.isOk ? Ok({
823
- reference: result.value.reference,
824
- status: result.value.status
825
- }) : Err(result.error),
826
- payload
827
- );
828
- }
707
+ await runHook(
708
+ config.hooks?.afterCollect,
709
+ result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
710
+ payload
711
+ );
829
712
  if (result.isErr) {
830
713
  const sdkErr = parseError(result.error);
831
714
  return createPaymentInstance(
@@ -845,24 +728,18 @@ function createSdkInstance(config) {
845
728
  throwValidation('bank details are required when method is "bank"');
846
729
  }
847
730
  let payload = { ...input, reference };
848
- if (config.hooks?.beforeCollect) {
849
- const mutated = await config.hooks.beforeCollect(payload);
850
- if (mutated != null)
851
- payload = { ...mutated, reference: mutated.reference ?? reference };
852
- }
731
+ const mutated = await runHook(config.hooks?.beforeCollect, payload);
732
+ if (mutated != null)
733
+ payload = { ...mutated, reference: mutated.reference ?? reference };
853
734
  const result = await transport.send({
854
735
  action: SDK_ACTIONS.collectPaymentAndResolve,
855
736
  payload
856
737
  });
857
- if (config.hooks?.afterCollect) {
858
- await config.hooks.afterCollect(
859
- result.isOk ? Ok({
860
- reference: result.value.reference,
861
- status: result.value.status
862
- }) : Err(result.error),
863
- payload
864
- );
865
- }
738
+ await runHook(
739
+ config.hooks?.afterCollect,
740
+ result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
741
+ payload
742
+ );
866
743
  if (result.isOk) {
867
744
  return Ok(result.value);
868
745
  }
@@ -883,24 +760,18 @@ function createSdkInstance(config) {
883
760
  "destination.accountNumber"
884
761
  );
885
762
  let payload = { ...input, reference };
886
- if (config.hooks?.beforePayout) {
887
- const mutated = await config.hooks.beforePayout(payload);
888
- if (mutated != null)
889
- payload = { ...mutated, reference: mutated.reference ?? reference };
890
- }
763
+ const mutated = await runHook(config.hooks?.beforePayout, payload);
764
+ if (mutated != null)
765
+ payload = { ...mutated, reference: mutated.reference ?? reference };
891
766
  const result = await transport.send({
892
767
  action: SDK_ACTIONS.makePayout,
893
768
  payload
894
769
  });
895
- if (config.hooks?.afterPayout) {
896
- await config.hooks.afterPayout(
897
- result.isOk ? Ok({
898
- reference: result.value.reference,
899
- status: result.value.status
900
- }) : Err(result.error),
901
- payload
902
- );
903
- }
770
+ await runHook(
771
+ config.hooks?.afterPayout,
772
+ result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
773
+ payload
774
+ );
904
775
  if (result.isErr) {
905
776
  const sdkErr = parseError(result.error);
906
777
  return createPaymentInstance(
@@ -925,24 +796,18 @@ function createSdkInstance(config) {
925
796
  "destination.accountNumber"
926
797
  );
927
798
  let payload = { ...input, reference };
928
- if (config.hooks?.beforePayout) {
929
- const mutated = await config.hooks.beforePayout(payload);
930
- if (mutated != null)
931
- payload = { ...mutated, reference: mutated.reference ?? reference };
932
- }
799
+ const mutated = await runHook(config.hooks?.beforePayout, payload);
800
+ if (mutated != null)
801
+ payload = { ...mutated, reference: mutated.reference ?? reference };
933
802
  const result = await transport.send({
934
803
  action: SDK_ACTIONS.makePayoutAndResolve,
935
804
  payload
936
805
  });
937
- if (config.hooks?.afterPayout) {
938
- await config.hooks.afterPayout(
939
- result.isOk ? Ok({
940
- reference: result.value.reference,
941
- status: result.value.status
942
- }) : Err(result.error),
943
- payload
944
- );
945
- }
806
+ await runHook(
807
+ config.hooks?.afterPayout,
808
+ result.isOk ? Ok({ reference: result.value.reference, status: result.value.status }) : Err(result.error),
809
+ payload
810
+ );
946
811
  if (result.isOk) {
947
812
  return Ok(result.value);
948
813
  }
@@ -1042,7 +907,8 @@ function createNylonPay(config) {
1042
907
  throw new Error('apiSecret must start with "nps_"');
1043
908
  }
1044
909
  const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
1045
- const instanceKey = `${config.apiKey}:${baseUrl}`;
910
+ const secretHash = createHash("sha256").update(config.apiSecret).digest("hex").slice(0, 16);
911
+ const instanceKey = `${config.apiKey}:${baseUrl}:${secretHash}`;
1046
912
  if (!config.force) {
1047
913
  const existing = instances.get(instanceKey);
1048
914
  if (existing) return existing;
@@ -1056,7 +922,6 @@ function createNylonPay(config) {
1056
922
  maxPollIntervalMs: config.maxPollIntervalMs ?? DEFAULT_MAX_POLL_INTERVAL_MS,
1057
923
  maxPollDurationMs: config.maxPollDurationMs ?? DEFAULT_MAX_POLL_DURATION_MS,
1058
924
  maxPollAttempts: config.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS,
1059
- streaming: config.streaming ?? DEFAULT_STREAMING,
1060
925
  fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
1061
926
  hooks: config.hooks
1062
927
  };