@nile-squad/nylonpay-ts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,777 @@
1
+ import { createHash, createHmac, timingSafeEqual, randomBytes } from 'crypto';
2
+ import { Ok, Err } from 'slang-ts';
3
+ import { type, platform, arch, release, hostname } from 'os';
4
+
5
+ // src/sdk.ts
6
+
7
+ // src/pubsub.ts
8
+ function createEmitter() {
9
+ const state = {
10
+ listeners: /* @__PURE__ */ new Map()
11
+ };
12
+ function on(event, handler) {
13
+ if (!state.listeners.has(event)) {
14
+ state.listeners.set(event, /* @__PURE__ */ new Set());
15
+ }
16
+ state.listeners.get(event).add(handler);
17
+ return () => off(event, handler);
18
+ }
19
+ function once(event, handler) {
20
+ const wrapper = (data) => {
21
+ off(event, wrapper);
22
+ handler(data);
23
+ };
24
+ on(event, wrapper);
25
+ return emitter;
26
+ }
27
+ function off(event, handler) {
28
+ const handlers = state.listeners.get(event);
29
+ if (handlers) {
30
+ handlers.delete(handler);
31
+ }
32
+ }
33
+ function emit(event, data) {
34
+ const handlers = state.listeners.get(event);
35
+ if (!handlers || handlers.size === 0) return;
36
+ for (const handler of handlers) {
37
+ try {
38
+ handler(data);
39
+ } catch {
40
+ }
41
+ }
42
+ }
43
+ function clear(event) {
44
+ if (event) {
45
+ state.listeners.delete(event);
46
+ } else {
47
+ state.listeners.clear();
48
+ }
49
+ }
50
+ function listenerCount(event) {
51
+ return state.listeners.get(event)?.size ?? 0;
52
+ }
53
+ const emitter = { on, once, off, emit, clear, listenerCount };
54
+ return emitter;
55
+ }
56
+
57
+ // src/payment.ts
58
+ var STATUS_TO_EVENT = {
59
+ successful: "success",
60
+ failed: "failed",
61
+ processing: "processing",
62
+ cancelled: "cancelled"
63
+ };
64
+ function statusToEvent(status) {
65
+ return STATUS_TO_EVENT[status] ?? null;
66
+ }
67
+ var TERMINAL_STATES = /* @__PURE__ */ new Set([
68
+ "successful",
69
+ "failed",
70
+ "cancelled"
71
+ ]);
72
+ function createPaymentInstance(initialResponse, deps) {
73
+ const state = {
74
+ reference: initialResponse.reference,
75
+ status: initialResponse.status,
76
+ transaction: null,
77
+ pollingTimer: null,
78
+ resolved: false,
79
+ pollAttempts: 0,
80
+ pollStartTime: Date.now(),
81
+ emitter: createEmitter(),
82
+ fetchStatus: deps.fetchStatus,
83
+ fetchTransaction: deps.fetchTransaction,
84
+ pollIntervalMs: deps.pollIntervalMs ?? 2e3,
85
+ maxPollDuration: deps.maxPollDuration ?? 3e5,
86
+ maxPollAttempts: deps.maxPollAttempts ?? 150
87
+ };
88
+ function resolveWithError(error) {
89
+ state.resolved = true;
90
+ stopPolling();
91
+ emitEvent("error", error);
92
+ }
93
+ function emitEvent(event, error) {
94
+ const data = {
95
+ event,
96
+ transaction: state.transaction ?? void 0,
97
+ error,
98
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
99
+ };
100
+ state.emitter.emit(event, data);
101
+ }
102
+ async function handleTerminalState(status) {
103
+ const txResult = await state.fetchTransaction({
104
+ reference: state.reference
105
+ });
106
+ if (txResult.isOk) {
107
+ state.transaction = txResult.value;
108
+ const event = statusToEvent(status);
109
+ if (event) {
110
+ emitEvent(event);
111
+ }
112
+ } else {
113
+ emitEvent("error", `Failed to fetch transaction: ${txResult.error}`);
114
+ }
115
+ state.resolved = true;
116
+ stopPolling();
117
+ }
118
+ async function handleStatusUpdate(response) {
119
+ if (response.reference !== state.reference) {
120
+ resolveWithError(
121
+ `Reference mismatch: expected ${state.reference} but got ${response.reference}`
122
+ );
123
+ return;
124
+ }
125
+ const newStatus = response.status;
126
+ const oldStatus = state.status;
127
+ state.status = newStatus;
128
+ if (newStatus !== oldStatus) {
129
+ const event = statusToEvent(newStatus);
130
+ if (event) {
131
+ if (TERMINAL_STATES.has(newStatus)) {
132
+ await handleTerminalState(newStatus);
133
+ return;
134
+ }
135
+ emitEvent(event);
136
+ }
137
+ }
138
+ }
139
+ function handlePollError(error) {
140
+ const isNotFound = error.includes("not found") || error.includes("NOT_FOUND");
141
+ if (isNotFound) {
142
+ return;
143
+ }
144
+ emitEvent("error", error);
145
+ state.resolved = true;
146
+ stopPolling();
147
+ }
148
+ function scheduleNextPoll() {
149
+ if (state.resolved || state.pollingTimer) {
150
+ return;
151
+ }
152
+ state.pollingTimer = setTimeout(() => {
153
+ state.pollingTimer = null;
154
+ void pollStatus();
155
+ }, state.pollIntervalMs);
156
+ }
157
+ async function pollStatus() {
158
+ if (state.resolved) {
159
+ stopPolling();
160
+ return;
161
+ }
162
+ if (state.pollAttempts >= state.maxPollAttempts) {
163
+ resolveWithError("Polling timeout: exceeded maximum attempts");
164
+ return;
165
+ }
166
+ if (Date.now() - state.pollStartTime >= state.maxPollDuration) {
167
+ resolveWithError("Polling timeout: exceeded maximum duration");
168
+ return;
169
+ }
170
+ state.pollAttempts += 1;
171
+ const result = await state.fetchStatus({ reference: state.reference });
172
+ if (result.isOk) {
173
+ await handleStatusUpdate(result.value);
174
+ } else {
175
+ handlePollError(result.error);
176
+ }
177
+ if (state.resolved) {
178
+ stopPolling();
179
+ return;
180
+ }
181
+ scheduleNextPoll();
182
+ }
183
+ function startPolling() {
184
+ scheduleNextPoll();
185
+ }
186
+ function stopPolling() {
187
+ if (state.pollingTimer) {
188
+ clearTimeout(state.pollingTimer);
189
+ state.pollingTimer = null;
190
+ }
191
+ }
192
+ function on(event, handler) {
193
+ state.emitter.on(event, handler);
194
+ return paymentInstance;
195
+ }
196
+ function off(event, handler) {
197
+ state.emitter.off(event, handler);
198
+ return paymentInstance;
199
+ }
200
+ function once(event, handler) {
201
+ state.emitter.once(event, handler);
202
+ return paymentInstance;
203
+ }
204
+ function wait() {
205
+ return new Promise((resolve, reject) => {
206
+ if (state.resolved) {
207
+ if (state.status === "successful" && state.transaction) {
208
+ resolve(state.transaction);
209
+ } else {
210
+ reject(new Error(`Payment ${state.status ?? "error"}`));
211
+ }
212
+ return;
213
+ }
214
+ function onSuccess() {
215
+ cleanup();
216
+ if (state.transaction) {
217
+ resolve(state.transaction);
218
+ } else {
219
+ reject(
220
+ new Error("Payment successful but transaction data unavailable")
221
+ );
222
+ }
223
+ }
224
+ function onFailed() {
225
+ cleanup();
226
+ reject(new Error("Payment failed"));
227
+ }
228
+ function onCancelled() {
229
+ cleanup();
230
+ reject(new Error("Payment cancelled"));
231
+ }
232
+ function onError(data) {
233
+ cleanup();
234
+ const eventData = data;
235
+ reject(new Error(eventData.error ?? "Payment error"));
236
+ }
237
+ function cleanup() {
238
+ state.emitter.off("success", onSuccess);
239
+ state.emitter.off("failed", onFailed);
240
+ state.emitter.off("cancelled", onCancelled);
241
+ state.emitter.off("error", onError);
242
+ }
243
+ state.emitter.on("success", onSuccess);
244
+ state.emitter.on("failed", onFailed);
245
+ state.emitter.on("cancelled", onCancelled);
246
+ state.emitter.on("error", onError);
247
+ });
248
+ }
249
+ const paymentInstance = {
250
+ get reference() {
251
+ return state.reference;
252
+ },
253
+ get status() {
254
+ return state.status;
255
+ },
256
+ on,
257
+ once,
258
+ off,
259
+ wait
260
+ };
261
+ startPolling();
262
+ return paymentInstance;
263
+ }
264
+
265
+ // src/sdk.config.ts
266
+ var DEFAULT_BASE_URL = "https://api.nylonpay.io/api/services";
267
+ var DEFAULT_TIMEOUT_MS = 3e4;
268
+ var DEFAULT_MAX_RETRIES = 3;
269
+ var DEFAULT_MAX_POLL_INTERVAL_MS = 2e3;
270
+ var DEFAULT_MAX_POLL_DURATION_MS = 3e5;
271
+ var DEFAULT_MAX_POLL_ATTEMPTS = 150;
272
+ var SDK_SERVICE = "sdk";
273
+ var SDK_ACTIONS = {
274
+ collectPayment: "sdk-collect-payment",
275
+ collectPaymentAndResolve: "sdk-collect-payment-and-resolve",
276
+ makePayout: "sdk-make-payout",
277
+ makePayoutAndResolve: "sdk-make-payout-and-resolve",
278
+ getStatus: "sdk-get-status",
279
+ getTransaction: "sdk-get-transaction",
280
+ verifyPhone: "sdk-verify-phone",
281
+ createInvoice: "sdk-create-invoice"
282
+ };
283
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
284
+ function generateFingerprint() {
285
+ const components = [
286
+ `type:${type()}`,
287
+ `platform:${platform()}`,
288
+ `arch:${arch()}`,
289
+ `release:${release()}`,
290
+ `hostname:${hostname()}`,
291
+ `node:${process.versions.node}`,
292
+ `v8:${process.versions.v8}`
293
+ ].join("|");
294
+ return createHash("sha256").update(components).digest("hex");
295
+ }
296
+ function generateNonce(length = 16) {
297
+ return randomBytes(length).toString("hex");
298
+ }
299
+ function sortValue(value) {
300
+ if (Array.isArray(value)) {
301
+ return value.map((entry) => sortValue(entry));
302
+ }
303
+ if (value && typeof value === "object") {
304
+ const sortedEntries = Object.entries(value).sort(
305
+ ([firstKey], [secondKey]) => firstKey.localeCompare(secondKey)
306
+ );
307
+ return Object.fromEntries(
308
+ sortedEntries.map(([entryKey, entryValue]) => [
309
+ entryKey,
310
+ sortValue(entryValue)
311
+ ])
312
+ );
313
+ }
314
+ return value;
315
+ }
316
+ function createCanonicalPayload(payload) {
317
+ return JSON.stringify(sortValue(payload));
318
+ }
319
+ function createSignaturePayload(input) {
320
+ return `${input.fingerprint}.${input.nonce}.${input.timestamp}.${createCanonicalPayload(input.payload)}`;
321
+ }
322
+ function createSignature(input) {
323
+ const payload = createSignaturePayload(input);
324
+ return createHmac("sha256", input.secret).update(payload).digest("hex");
325
+ }
326
+ function createTimestamp() {
327
+ return Date.now().toString();
328
+ }
329
+ function verifyResponseSignature(data, signature, secret) {
330
+ const expectedSignature = createHmac("sha256", secret).update(createCanonicalPayload(data)).digest("hex");
331
+ const providedBuffer = Buffer.from(signature, "hex");
332
+ const expectedBuffer = Buffer.from(expectedSignature, "hex");
333
+ if (providedBuffer.length !== expectedBuffer.length) {
334
+ return false;
335
+ }
336
+ return timingSafeEqual(providedBuffer, expectedBuffer);
337
+ }
338
+
339
+ // src/transport.ts
340
+ var CACHED_FINGERPRINT = generateFingerprint();
341
+ function calculateBackoff(attempt) {
342
+ const base = 2 ** attempt * 1e3;
343
+ const jitter = Math.random() * 500;
344
+ return base + jitter;
345
+ }
346
+ function delay(ms) {
347
+ return new Promise((resolve) => setTimeout(resolve, ms));
348
+ }
349
+ function stripResponseSignature(payload) {
350
+ if (!payload || typeof payload !== "object" || !("_responseSignature" in payload)) {
351
+ return { data: payload, responseSignature: null };
352
+ }
353
+ const { _responseSignature, ...rest } = payload;
354
+ return {
355
+ data: rest,
356
+ responseSignature: typeof _responseSignature === "string" ? _responseSignature : null
357
+ };
358
+ }
359
+ function buildEnvelope({
360
+ action,
361
+ payload
362
+ }) {
363
+ return {
364
+ intent: "execute",
365
+ service: SDK_SERVICE,
366
+ action,
367
+ payload: {
368
+ ...payload,
369
+ _fingerprint: CACHED_FINGERPRINT
370
+ }
371
+ };
372
+ }
373
+ function buildAuthHeaders({
374
+ apiKey,
375
+ apiSecret,
376
+ payload
377
+ }) {
378
+ const nonce = generateNonce();
379
+ const timestamp = createTimestamp();
380
+ const signature = createSignature({
381
+ fingerprint: CACHED_FINGERPRINT,
382
+ nonce,
383
+ timestamp,
384
+ payload,
385
+ secret: apiSecret
386
+ });
387
+ return {
388
+ "content-type": "application/json",
389
+ "x-nylon-key": apiKey,
390
+ "x-nylon-nonce": nonce,
391
+ "x-nylon-signature": signature,
392
+ "x-nylon-timestamp": timestamp
393
+ };
394
+ }
395
+ function withTimeout(timeoutMs) {
396
+ const controller = new AbortController();
397
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
398
+ return { controller, cleanup: () => clearTimeout(timeoutId) };
399
+ }
400
+ function createTransport({
401
+ apiKey,
402
+ apiSecret,
403
+ baseUrl = DEFAULT_BASE_URL,
404
+ timeoutMs = DEFAULT_TIMEOUT_MS,
405
+ maxRetries = DEFAULT_MAX_RETRIES,
406
+ fetch: fetchImpl
407
+ }) {
408
+ async function send(request) {
409
+ const envelope = buildEnvelope(request);
410
+ const signedPayload = envelope.payload;
411
+ const headers = buildAuthHeaders({
412
+ apiKey,
413
+ apiSecret,
414
+ payload: signedPayload
415
+ });
416
+ const bodyString = JSON.stringify(envelope);
417
+ async function attempt(currentAttempt) {
418
+ const { controller, cleanup } = withTimeout(timeoutMs);
419
+ try {
420
+ const response = await fetchImpl(baseUrl, {
421
+ method: "POST",
422
+ headers,
423
+ body: bodyString,
424
+ signal: controller.signal
425
+ });
426
+ if (!response.ok) {
427
+ const statusCode = response.status;
428
+ const retryable = RETRYABLE_STATUS_CODES.has(statusCode);
429
+ let errorMessage = `HTTP ${statusCode}`;
430
+ try {
431
+ const errorBody = await response.json();
432
+ if (errorBody && typeof errorBody === "object" && "message" in errorBody) {
433
+ errorMessage = String(errorBody.message);
434
+ }
435
+ } catch {
436
+ errorMessage = response.statusText || errorMessage;
437
+ }
438
+ if (retryable && currentAttempt < maxRetries) {
439
+ cleanup();
440
+ await delay(calculateBackoff(currentAttempt));
441
+ return attempt(currentAttempt + 1);
442
+ }
443
+ const sdkError = {
444
+ code: `HTTP_${statusCode}`,
445
+ message: errorMessage,
446
+ statusCode,
447
+ retryable
448
+ };
449
+ cleanup();
450
+ return Err(JSON.stringify(sdkError));
451
+ }
452
+ const responseBody = await response.json();
453
+ if (!responseBody || typeof responseBody !== "object" || !("status" in responseBody)) {
454
+ cleanup();
455
+ return Err(
456
+ JSON.stringify({
457
+ code: "INVALID_RESPONSE",
458
+ message: "Response missing status field",
459
+ retryable: false
460
+ })
461
+ );
462
+ }
463
+ const { status, message, data } = responseBody;
464
+ if (status === true) {
465
+ const { data: strippedData, responseSignature } = stripResponseSignature(data);
466
+ if (responseSignature) {
467
+ const isValid = verifyResponseSignature(
468
+ strippedData,
469
+ responseSignature,
470
+ apiSecret
471
+ );
472
+ if (!isValid) {
473
+ cleanup();
474
+ return Err(
475
+ JSON.stringify({
476
+ code: "RESPONSE_TAMPERED",
477
+ message: "Response signature verification failed",
478
+ retryable: false
479
+ })
480
+ );
481
+ }
482
+ }
483
+ cleanup();
484
+ return Ok(strippedData);
485
+ }
486
+ const parsedError = parseError(message);
487
+ cleanup();
488
+ return Err(JSON.stringify(parsedError));
489
+ } catch (error) {
490
+ cleanup();
491
+ const isAbort = error instanceof DOMException && error.name === "AbortError";
492
+ const sdkError = {
493
+ code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
494
+ message: isAbort ? `Request timed out after ${timeoutMs}ms` : String(error),
495
+ retryable: true
496
+ };
497
+ if (currentAttempt < maxRetries) {
498
+ await delay(calculateBackoff(currentAttempt));
499
+ return attempt(currentAttempt + 1);
500
+ }
501
+ return Err(JSON.stringify(sdkError));
502
+ }
503
+ }
504
+ return attempt(0);
505
+ }
506
+ return { send, parseError };
507
+ }
508
+ function parseError(error) {
509
+ try {
510
+ const parsed = JSON.parse(error);
511
+ if (parsed && typeof parsed === "object" && "code" in parsed && "message" in parsed && typeof parsed.code === "string" && typeof parsed.message === "string") {
512
+ return parsed;
513
+ }
514
+ } catch {
515
+ }
516
+ return { code: "UNKNOWN", message: error };
517
+ }
518
+ function verifyWebhookSignature(input) {
519
+ const payloadBytes = typeof input.payload === "string" ? Buffer.from(input.payload, "utf8") : Buffer.from(input.payload);
520
+ const expectedSignature = createHmac("sha256", input.secret).update(payloadBytes).digest("hex");
521
+ const providedBuffer = Buffer.from(input.signature, "hex");
522
+ const expectedBuffer = Buffer.from(expectedSignature, "hex");
523
+ if (providedBuffer.length !== expectedBuffer.length) {
524
+ return false;
525
+ }
526
+ return timingSafeEqual(providedBuffer, expectedBuffer);
527
+ }
528
+
529
+ // src/sdk.ts
530
+ function generateReference() {
531
+ return randomBytes(16).toString("hex").slice(0, 15);
532
+ }
533
+ function validateAmount(amount) {
534
+ if (!Number.isInteger(amount) || amount <= 0) {
535
+ throw new Error("amount must be a positive integer");
536
+ }
537
+ }
538
+ function validateNonEmpty(value, fieldName) {
539
+ if (!value || value.trim() === "") {
540
+ throw new Error(`${fieldName} is required`);
541
+ }
542
+ }
543
+ function createSdkInstance(config) {
544
+ const transport = createTransport({
545
+ apiKey: config.apiKey,
546
+ apiSecret: config.apiSecret,
547
+ baseUrl: config.baseUrl,
548
+ timeoutMs: config.timeoutMs,
549
+ maxRetries: config.maxRetries,
550
+ fetch: config.fetch
551
+ });
552
+ const commonDeps = {
553
+ fetchStatus: (input) => transport.send({
554
+ action: SDK_ACTIONS.getStatus,
555
+ payload: input
556
+ }),
557
+ fetchTransaction: (input) => transport.send({
558
+ action: SDK_ACTIONS.getTransaction,
559
+ payload: input
560
+ }),
561
+ pollIntervalMs: config.maxPollIntervalMs,
562
+ maxPollDuration: config.maxPollDurationMs,
563
+ maxPollAttempts: config.maxPollAttempts
564
+ };
565
+ async function collectPayment(input) {
566
+ const reference = input.reference ?? generateReference();
567
+ validateAmount(input.amount);
568
+ validateNonEmpty(input.customer.name, "customer.name");
569
+ validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
570
+ validateNonEmpty(input.description, "description");
571
+ if (input.method === "bank" && !input.bank) {
572
+ throw new Error('bank details are required when method is "bank"');
573
+ }
574
+ const payload = { ...input, reference };
575
+ const result = await transport.send({
576
+ action: SDK_ACTIONS.collectPayment,
577
+ payload
578
+ });
579
+ if (result.isOk) {
580
+ return createPaymentInstance(result.value, commonDeps);
581
+ }
582
+ return createPaymentInstance(
583
+ { reference, status: "pending" },
584
+ {
585
+ fetchStatus: async () => Err(result.error),
586
+ fetchTransaction: async () => Err(result.error),
587
+ pollIntervalMs: 0,
588
+ maxPollAttempts: 1,
589
+ maxPollDuration: Number.MAX_SAFE_INTEGER
590
+ }
591
+ );
592
+ }
593
+ async function collectPaymentAndResolve(input) {
594
+ const reference = input.reference ?? generateReference();
595
+ validateAmount(input.amount);
596
+ validateNonEmpty(input.customer.name, "customer.name");
597
+ validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
598
+ validateNonEmpty(input.description, "description");
599
+ if (input.method === "bank" && !input.bank) {
600
+ throw new Error('bank details are required when method is "bank"');
601
+ }
602
+ const payload = { ...input, reference };
603
+ const result = await transport.send({
604
+ action: SDK_ACTIONS.collectPaymentAndResolve,
605
+ payload
606
+ });
607
+ if (result.isOk) {
608
+ return Ok(result.value);
609
+ }
610
+ return Err(result.error);
611
+ }
612
+ async function makePayout(input) {
613
+ const reference = input.reference ?? generateReference();
614
+ validateAmount(input.amount);
615
+ validateNonEmpty(input.customer.name, "customer.name");
616
+ validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
617
+ validateNonEmpty(input.description, "description");
618
+ validateNonEmpty(
619
+ input.destination.accountHolderName,
620
+ "destination.accountHolderName"
621
+ );
622
+ validateNonEmpty(
623
+ input.destination.accountNumber,
624
+ "destination.accountNumber"
625
+ );
626
+ const payload = { ...input, reference };
627
+ const result = await transport.send({
628
+ action: SDK_ACTIONS.makePayout,
629
+ payload
630
+ });
631
+ if (result.isOk) {
632
+ return createPaymentInstance(result.value, commonDeps);
633
+ }
634
+ return createPaymentInstance(
635
+ { reference, status: "pending" },
636
+ {
637
+ fetchStatus: async () => Err(result.error),
638
+ fetchTransaction: async () => Err(result.error),
639
+ pollIntervalMs: 0,
640
+ maxPollAttempts: 1,
641
+ maxPollDuration: Number.MAX_SAFE_INTEGER
642
+ }
643
+ );
644
+ }
645
+ async function makePayoutAndResolve(input) {
646
+ const reference = input.reference ?? generateReference();
647
+ validateAmount(input.amount);
648
+ validateNonEmpty(input.customer.name, "customer.name");
649
+ validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
650
+ validateNonEmpty(input.description, "description");
651
+ validateNonEmpty(
652
+ input.destination.accountHolderName,
653
+ "destination.accountHolderName"
654
+ );
655
+ validateNonEmpty(
656
+ input.destination.accountNumber,
657
+ "destination.accountNumber"
658
+ );
659
+ const payload = { ...input, reference };
660
+ const result = await transport.send({
661
+ action: SDK_ACTIONS.makePayoutAndResolve,
662
+ payload
663
+ });
664
+ if (result.isOk) {
665
+ return Ok(result.value);
666
+ }
667
+ return Err(result.error);
668
+ }
669
+ async function getStatus(input) {
670
+ validateNonEmpty(input.reference, "reference");
671
+ const result = await transport.send({
672
+ action: SDK_ACTIONS.getStatus,
673
+ payload: input
674
+ });
675
+ if (result.isOk) {
676
+ return Ok(result.value);
677
+ }
678
+ return Err(result.error);
679
+ }
680
+ async function getTransaction(input) {
681
+ if (!input.id && !input.reference) {
682
+ throw new Error("id or reference is required");
683
+ }
684
+ const result = await transport.send({
685
+ action: SDK_ACTIONS.getTransaction,
686
+ payload: input
687
+ });
688
+ if (result.isOk) {
689
+ return Ok(result.value);
690
+ }
691
+ return Err(result.error);
692
+ }
693
+ async function verifyPhone(input) {
694
+ validateNonEmpty(input.phoneNumber, "phoneNumber");
695
+ const result = await transport.send({
696
+ action: SDK_ACTIONS.verifyPhone,
697
+ payload: input
698
+ });
699
+ if (result.isOk) {
700
+ return Ok(result.value);
701
+ }
702
+ return Err(result.error);
703
+ }
704
+ async function createInvoice(input) {
705
+ const reference = input.reference ?? generateReference();
706
+ validateAmount(input.amount);
707
+ validateNonEmpty(input.description, "description");
708
+ if (input.items) {
709
+ if (input.items.length > 50) {
710
+ throw new Error("items must not exceed 50");
711
+ }
712
+ for (const item of input.items) {
713
+ if (!Number.isInteger(item.quantity) || item.quantity <= 0) {
714
+ throw new Error("item quantity must be a positive integer");
715
+ }
716
+ if (!Number.isInteger(item.unitPrice) || item.unitPrice <= 0) {
717
+ throw new Error("item unitPrice must be a positive integer");
718
+ }
719
+ }
720
+ }
721
+ const payload = { ...input, reference };
722
+ const result = await transport.send({
723
+ action: SDK_ACTIONS.createInvoice,
724
+ payload
725
+ });
726
+ if (result.isOk) {
727
+ return Ok(result.value);
728
+ }
729
+ return Err(result.error);
730
+ }
731
+ function verifyWebhook(input) {
732
+ return verifyWebhookSignature(input);
733
+ }
734
+ return {
735
+ collectPayment,
736
+ collectPaymentAndResolve,
737
+ makePayout,
738
+ makePayoutAndResolve,
739
+ getStatus,
740
+ getTransaction,
741
+ verifyPhone,
742
+ createInvoice,
743
+ verifyWebhookSignature: verifyWebhook
744
+ };
745
+ }
746
+
747
+ // src/create-nylon-pay.ts
748
+ function createNylonPay(config) {
749
+ if (!config.apiKey) {
750
+ throw new Error("apiKey is required");
751
+ }
752
+ if (!config.apiKey.startsWith("npk_")) {
753
+ throw new Error('apiKey must start with "npk_"');
754
+ }
755
+ if (!config.apiSecret) {
756
+ throw new Error("apiSecret is required");
757
+ }
758
+ if (!config.apiSecret.startsWith("nps_")) {
759
+ throw new Error('apiSecret must start with "nps_"');
760
+ }
761
+ const resolvedConfig = {
762
+ apiKey: config.apiKey,
763
+ apiSecret: config.apiSecret,
764
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
765
+ timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
766
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
767
+ maxPollIntervalMs: config.maxPollIntervalMs ?? DEFAULT_MAX_POLL_INTERVAL_MS,
768
+ maxPollDurationMs: config.maxPollDurationMs ?? DEFAULT_MAX_POLL_DURATION_MS,
769
+ maxPollAttempts: config.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS,
770
+ fetch: config.fetch ?? globalThis.fetch.bind(globalThis)
771
+ };
772
+ return createSdkInstance(resolvedConfig);
773
+ }
774
+
775
+ export { createNylonPay, parseError, verifyWebhookSignature };
776
+ //# sourceMappingURL=index.js.map
777
+ //# sourceMappingURL=index.js.map