@nile-squad/nylonpay-ts 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -55,21 +55,6 @@ function createEmitter() {
55
55
  const emitter = { on, once, off, emit, clear, listenerCount };
56
56
  return emitter;
57
57
  }
58
- function generateFingerprint() {
59
- const components = [
60
- `type:${os.type()}`,
61
- `platform:${os.platform()}`,
62
- `arch:${os.arch()}`,
63
- `release:${os.release()}`,
64
- `hostname:${os.hostname()}`,
65
- `node:${process.versions.node}`,
66
- `v8:${process.versions.v8}`
67
- ].join("|");
68
- return crypto.createHash("sha256").update(components).digest("hex");
69
- }
70
- function generateNonce(length = 16) {
71
- return crypto.randomBytes(length).toString("hex");
72
- }
73
58
 
74
59
  // src/sdk.config.ts
75
60
  var DEFAULT_BASE_URL = "https://api.nylonpay.nilesquad.com/api/services";
@@ -78,6 +63,9 @@ var DEFAULT_MAX_RETRIES = 3;
78
63
  var DEFAULT_MAX_POLL_INTERVAL_MS = 2e3;
79
64
  var DEFAULT_MAX_POLL_DURATION_MS = 3e5;
80
65
  var DEFAULT_MAX_POLL_ATTEMPTS = 150;
66
+ var DEFAULT_STREAMING = true;
67
+ var STREAM_PATH = "/sse/transaction";
68
+ var MAX_STREAM_RECONNECTS = 2;
81
69
  var SDK_SERVICE = "sdk";
82
70
  var SDK_ACTIONS = {
83
71
  collectPayment: "sdk-collect-payment",
@@ -90,6 +78,21 @@ var SDK_ACTIONS = {
90
78
  createInvoice: "sdk-create-invoice"
91
79
  };
92
80
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
81
+ function generateFingerprint() {
82
+ const components = [
83
+ `type:${os.type()}`,
84
+ `platform:${os.platform()}`,
85
+ `arch:${os.arch()}`,
86
+ `release:${os.release()}`,
87
+ `hostname:${os.hostname()}`,
88
+ `node:${process.versions.node}`,
89
+ `v8:${process.versions.v8}`
90
+ ].join("|");
91
+ return crypto.createHash("sha256").update(components).digest("hex");
92
+ }
93
+ function generateNonce(length = 16) {
94
+ return crypto.randomBytes(length).toString("hex");
95
+ }
93
96
  function sortValue(value) {
94
97
  if (Array.isArray(value)) {
95
98
  return value.map((entry) => sortValue(entry));
@@ -120,6 +123,42 @@ function createSignature(input) {
120
123
  function createTimestamp() {
121
124
  return Date.now().toString();
122
125
  }
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
+ }
123
162
  function verifyResponseSignature(data, signature, secret) {
124
163
  const expectedSignature = crypto.createHmac("sha256", secret).update(createCanonicalPayload(data)).digest("hex");
125
164
  const providedBuffer = Buffer.from(signature, "hex");
@@ -132,6 +171,55 @@ function verifyResponseSignature(data, signature, secret) {
132
171
 
133
172
  // src/transport.ts
134
173
  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
+ var KNOWN_CATEGORIES = /* @__PURE__ */ new Set([
182
+ "auth",
183
+ "validation",
184
+ "limit",
185
+ "rate_limit",
186
+ "account",
187
+ "provider",
188
+ "not_found",
189
+ "internal",
190
+ "network",
191
+ "timeout"
192
+ ]);
193
+ var STATUS_CATEGORY = {
194
+ 408: "timeout",
195
+ 429: "rate_limit"
196
+ };
197
+ var ERROR_TYPE_SUFFIX = /^(.*?)\s*--\s*error-type:\s*([a-z_]+)\s*$/is;
198
+ function parseCategoryFromMessage(message) {
199
+ const match = ERROR_TYPE_SUFFIX.exec(message);
200
+ if (match?.[2] && KNOWN_CATEGORIES.has(match[2])) {
201
+ return {
202
+ category: match[2],
203
+ message: match[1] ?? message
204
+ };
205
+ }
206
+ return { category: null, message };
207
+ }
208
+ function buildHttpError(params) {
209
+ const parsed = parseCategoryFromMessage(params.message);
210
+ const category = parsed.category ?? STATUS_CATEGORY[params.statusCode] ?? (params.statusCode >= 500 ? "internal" : "validation");
211
+ return {
212
+ category,
213
+ message: parsed.message,
214
+ retryable: RETRYABLE_STATUS_CODES.has(params.statusCode)
215
+ };
216
+ }
217
+ function createSdkError(error) {
218
+ return Object.assign(new Error(error.message), {
219
+ category: error.category,
220
+ retryable: error.retryable
221
+ });
222
+ }
135
223
  function calculateBackoff(attempt) {
136
224
  const base = 2 ** attempt * 1e3;
137
225
  const jitter = Math.random() * 500;
@@ -234,21 +322,19 @@ function createTransport({
234
322
  await delay(calculateBackoff(currentAttempt));
235
323
  return attempt(currentAttempt + 1);
236
324
  }
237
- const sdkError = {
238
- code: `HTTP_${statusCode}`,
239
- message: errorMessage,
240
- statusCode,
241
- retryable
242
- };
243
325
  cleanup();
244
- return slangTs.Err(JSON.stringify(sdkError));
326
+ return slangTs.Err(
327
+ JSON.stringify(
328
+ buildHttpError({ message: errorMessage, statusCode })
329
+ )
330
+ );
245
331
  }
246
332
  const responseBody = await response.json();
247
333
  if (!responseBody || typeof responseBody !== "object" || !("status" in responseBody)) {
248
334
  cleanup();
249
335
  return slangTs.Err(
250
336
  JSON.stringify({
251
- code: "INVALID_RESPONSE",
337
+ category: "internal",
252
338
  message: "Response missing status field",
253
339
  retryable: false
254
340
  })
@@ -267,7 +353,7 @@ function createTransport({
267
353
  cleanup();
268
354
  return slangTs.Err(
269
355
  JSON.stringify({
270
- code: "RESPONSE_TAMPERED",
356
+ category: "internal",
271
357
  message: "Response signature verification failed",
272
358
  retryable: false
273
359
  })
@@ -284,7 +370,7 @@ function createTransport({
284
370
  cleanup();
285
371
  const isAbort = error instanceof DOMException && error.name === "AbortError";
286
372
  const sdkError = {
287
- code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
373
+ category: isAbort ? "timeout" : "network",
288
374
  message: isAbort ? `Request timed out after ${timeoutMs}ms` : String(error),
289
375
  retryable: true
290
376
  };
@@ -297,17 +383,105 @@ function createTransport({
297
383
  }
298
384
  return attempt(0);
299
385
  }
300
- return { send, parseError };
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 };
301
471
  }
302
472
  function parseError(error) {
303
473
  try {
304
474
  const parsed = JSON.parse(error);
305
- if (parsed && typeof parsed === "object" && "code" in parsed && "message" in parsed && typeof parsed.code === "string" && typeof parsed.message === "string") {
475
+ if (parsed && typeof parsed === "object" && "category" in parsed && "message" in parsed && typeof parsed.category === "string" && typeof parsed.message === "string") {
306
476
  return parsed;
307
477
  }
308
478
  } catch {
309
479
  }
310
- return { code: "UNKNOWN", message: error };
480
+ const fromSuffix = parseCategoryFromMessage(error);
481
+ return {
482
+ category: fromSuffix.category ?? "internal",
483
+ message: fromSuffix.message
484
+ };
311
485
  }
312
486
 
313
487
  // src/payment.ts
@@ -343,11 +517,15 @@ function createPaymentInstance(initialResponse, deps) {
343
517
  fetchTransaction: deps.fetchTransaction,
344
518
  pollIntervalMs: deps.pollIntervalMs ?? 2e3,
345
519
  maxPollDuration: deps.maxPollDuration ?? 3e5,
346
- maxPollAttempts: deps.maxPollAttempts ?? 150
520
+ maxPollAttempts: deps.maxPollAttempts ?? 150,
521
+ streaming: deps.streaming ?? false,
522
+ openStream: deps.openStream,
523
+ streamHandle: null,
524
+ streamReconnects: 0
347
525
  };
348
526
  function resolveWithError(error) {
349
527
  state.resolved = true;
350
- stopPolling();
528
+ stopUpdates();
351
529
  emitEvent("error", parseError(error).message);
352
530
  }
353
531
  function emitEvent(event, error) {
@@ -374,7 +552,7 @@ function createPaymentInstance(initialResponse, deps) {
374
552
  emitEvent("error", `Failed to fetch transaction: ${txResult.error}`);
375
553
  }
376
554
  state.resolved = true;
377
- stopPolling();
555
+ stopUpdates();
378
556
  }
379
557
  async function handleStatusUpdate(response) {
380
558
  if (response.reference !== state.reference) {
@@ -398,13 +576,13 @@ function createPaymentInstance(initialResponse, deps) {
398
576
  }
399
577
  }
400
578
  function handlePollError(error) {
401
- const isNotFound = error.includes("not found") || error.includes("NOT_FOUND");
402
- if (isNotFound) {
579
+ const parsed = parseError(error);
580
+ if (parsed.category === "not_found") {
403
581
  return;
404
582
  }
405
- emitEvent("error", parseError(error).message);
583
+ emitEvent("error", parsed.message);
406
584
  state.resolved = true;
407
- stopPolling();
585
+ stopUpdates();
408
586
  }
409
587
  function scheduleNextPoll() {
410
588
  if (state.resolved || state.pollingTimer) {
@@ -417,7 +595,7 @@ function createPaymentInstance(initialResponse, deps) {
417
595
  }
418
596
  async function pollStatus() {
419
597
  if (state.resolved) {
420
- stopPolling();
598
+ stopUpdates();
421
599
  return;
422
600
  }
423
601
  if (state.pollAttempts >= state.maxPollAttempts) {
@@ -436,25 +614,66 @@ function createPaymentInstance(initialResponse, deps) {
436
614
  handlePollError(result.error);
437
615
  }
438
616
  if (state.resolved) {
439
- stopPolling();
617
+ stopUpdates();
618
+ return;
619
+ }
620
+ scheduleNextPoll();
621
+ }
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);
440
654
  return;
441
655
  }
442
656
  scheduleNextPoll();
443
657
  }
444
- function startPolling() {
445
- if (TERMINAL_STATES.has(state.status ?? "pending")) {
658
+ function startUpdates() {
659
+ if (TERMINAL_STATES.has(state.status)) {
446
660
  setTimeout(() => {
447
661
  void handleTerminalState(state.status);
448
662
  }, 0);
449
663
  return;
450
664
  }
665
+ if (state.streaming && state.openStream) {
666
+ startStream();
667
+ return;
668
+ }
451
669
  scheduleNextPoll();
452
670
  }
453
- function stopPolling() {
671
+ function stopUpdates() {
454
672
  if (state.pollingTimer) {
455
673
  clearTimeout(state.pollingTimer);
456
674
  state.pollingTimer = null;
457
675
  }
676
+ closeStream();
458
677
  }
459
678
  function on(event, handler) {
460
679
  state.emitter.on(event, handler);
@@ -516,7 +735,7 @@ function createPaymentInstance(initialResponse, deps) {
516
735
  off,
517
736
  wait
518
737
  };
519
- startPolling();
738
+ startUpdates();
520
739
  return paymentInstance;
521
740
  }
522
741
  function verifyWebhookSignature(input) {
@@ -534,14 +753,17 @@ function verifyWebhookSignature(input) {
534
753
  function generateReference() {
535
754
  return crypto.randomBytes(16).toString("hex").slice(0, 15);
536
755
  }
756
+ function throwValidation(message) {
757
+ throw createSdkError({ category: "validation", message });
758
+ }
537
759
  function validateAmount(amount) {
538
760
  if (!Number.isInteger(amount) || amount <= 0) {
539
- throw new Error("amount must be a positive integer");
761
+ throwValidation("amount must be a positive integer");
540
762
  }
541
763
  }
542
764
  function validateNonEmpty(value, fieldName) {
543
765
  if (!value || value.trim() === "") {
544
- throw new Error(`${fieldName} is required`);
766
+ throwValidation(`${fieldName} is required`);
545
767
  }
546
768
  }
547
769
  function createSdkInstance(config) {
@@ -564,7 +786,9 @@ function createSdkInstance(config) {
564
786
  }),
565
787
  pollIntervalMs: config.maxPollIntervalMs,
566
788
  maxPollDuration: config.maxPollDurationMs,
567
- maxPollAttempts: config.maxPollAttempts
789
+ maxPollAttempts: config.maxPollAttempts,
790
+ streaming: config.streaming,
791
+ openStream: config.streaming ? transport.openStream : void 0
568
792
  };
569
793
  async function collectPayment(input) {
570
794
  const reference = input.reference ?? generateReference();
@@ -573,7 +797,7 @@ function createSdkInstance(config) {
573
797
  validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
574
798
  validateNonEmpty(input.description, "description");
575
799
  if (input.method === "bank" && !input.bank) {
576
- throw new Error('bank details are required when method is "bank"');
800
+ throwValidation('bank details are required when method is "bank"');
577
801
  }
578
802
  let payload = { ...input, reference };
579
803
  if (config.hooks?.beforeCollect) {
@@ -594,19 +818,10 @@ function createSdkInstance(config) {
594
818
  payload
595
819
  );
596
820
  }
597
- if (result.isOk) {
598
- return createPaymentInstance(result.value, commonDeps);
599
- }
600
- return createPaymentInstance(
601
- { reference, status: "pending" },
602
- {
603
- fetchStatus: async () => slangTs.Err(result.error),
604
- fetchTransaction: async () => slangTs.Err(result.error),
605
- pollIntervalMs: 0,
606
- maxPollAttempts: 1,
607
- maxPollDuration: Number.MAX_SAFE_INTEGER
608
- }
609
- );
821
+ if (result.isErr) {
822
+ throw createSdkError(parseError(result.error));
823
+ }
824
+ return createPaymentInstance(result.value, commonDeps);
610
825
  }
611
826
  async function collectPaymentAndResolve(input) {
612
827
  const reference = input.reference ?? generateReference();
@@ -615,7 +830,7 @@ function createSdkInstance(config) {
615
830
  validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
616
831
  validateNonEmpty(input.description, "description");
617
832
  if (input.method === "bank" && !input.bank) {
618
- throw new Error('bank details are required when method is "bank"');
833
+ throwValidation('bank details are required when method is "bank"');
619
834
  }
620
835
  let payload = { ...input, reference };
621
836
  if (config.hooks?.beforeCollect) {
@@ -674,19 +889,10 @@ function createSdkInstance(config) {
674
889
  payload
675
890
  );
676
891
  }
677
- if (result.isOk) {
678
- return createPaymentInstance(result.value, commonDeps);
679
- }
680
- return createPaymentInstance(
681
- { reference, status: "pending" },
682
- {
683
- fetchStatus: async () => slangTs.Err(result.error),
684
- fetchTransaction: async () => slangTs.Err(result.error),
685
- pollIntervalMs: 0,
686
- maxPollAttempts: 1,
687
- maxPollDuration: Number.MAX_SAFE_INTEGER
688
- }
689
- );
892
+ if (result.isErr) {
893
+ throw createSdkError(parseError(result.error));
894
+ }
895
+ return createPaymentInstance(result.value, commonDeps);
690
896
  }
691
897
  async function makePayoutAndResolve(input) {
692
898
  const reference = input.reference ?? generateReference();
@@ -739,7 +945,7 @@ function createSdkInstance(config) {
739
945
  }
740
946
  async function getTransaction(input) {
741
947
  if (!input.id && !input.reference) {
742
- throw new Error("id or reference is required");
948
+ throwValidation("id or reference is required");
743
949
  }
744
950
  const result = await transport.send({
745
951
  action: SDK_ACTIONS.getTransaction,
@@ -767,14 +973,14 @@ function createSdkInstance(config) {
767
973
  validateNonEmpty(input.description, "description");
768
974
  if (input.items) {
769
975
  if (input.items.length > 50) {
770
- throw new Error("items must not exceed 50");
976
+ throwValidation("items must not exceed 50");
771
977
  }
772
978
  for (const item of input.items) {
773
979
  if (!Number.isInteger(item.quantity) || item.quantity <= 0) {
774
- throw new Error("item quantity must be a positive integer");
980
+ throwValidation("item quantity must be a positive integer");
775
981
  }
776
982
  if (!Number.isInteger(item.unitPrice) || item.unitPrice <= 0) {
777
- throw new Error("item unitPrice must be a positive integer");
983
+ throwValidation("item unitPrice must be a positive integer");
778
984
  }
779
985
  }
780
986
  }
@@ -834,6 +1040,7 @@ function createNylonPay(config) {
834
1040
  maxPollIntervalMs: config.maxPollIntervalMs ?? DEFAULT_MAX_POLL_INTERVAL_MS,
835
1041
  maxPollDurationMs: config.maxPollDurationMs ?? DEFAULT_MAX_POLL_DURATION_MS,
836
1042
  maxPollAttempts: config.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS,
1043
+ streaming: config.streaming ?? DEFAULT_STREAMING,
837
1044
  fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
838
1045
  hooks: config.hooks
839
1046
  };
@@ -843,6 +1050,7 @@ function createNylonPay(config) {
843
1050
  }
844
1051
 
845
1052
  exports.createNylonPay = createNylonPay;
1053
+ exports.createSdkError = createSdkError;
846
1054
  exports.parseError = parseError;
847
1055
  exports.verifyWebhookSignature = verifyWebhookSignature;
848
1056
  //# sourceMappingURL=index.cjs.map