@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 +123 -258
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -23
- package/dist/index.d.ts +54 -23
- package/dist/index.js +123 -258
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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
|
|
599
|
-
* instance (singleton per key+url
|
|
600
|
-
* fresh instance
|
|
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 +
|
|
617
|
-
* { force: true } is passed. Use your test keys for sandbox,
|
|
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
|
|
667
|
-
*
|
|
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
|
-
* @
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
|
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
|
};
|