@nile-squad/nylonpay-ts 1.2.0 → 1.3.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.d.ts CHANGED
@@ -89,6 +89,13 @@ type CollectPaymentInput = {
89
89
  method?: PaymentMethod;
90
90
  bank?: BankDetails;
91
91
  metadata?: Record<string, string>;
92
+ /**
93
+ * Business labels to attach to the transaction. Normalized to lowercase;
94
+ * reserved tags (`"live"`, `"test"`) are managed automatically. Max 10 tags,
95
+ * each up to 50 characters. Useful for grouping payments by campaign, product,
96
+ * team, or any merchant-defined dimension.
97
+ */
98
+ tags?: string[];
92
99
  };
93
100
  /**
94
101
  * Input for initiating a payout. Use this to disburse funds to a
@@ -102,6 +109,8 @@ type MakePayoutInput = {
102
109
  description: string;
103
110
  reference?: string;
104
111
  metadata?: Record<string, string>;
112
+ /** Business labels — same normalization rules as {@link CollectPaymentInput.tags}. */
113
+ tags?: string[];
105
114
  };
106
115
  /**
107
116
  * Input for a one-shot status check. Does not start polling; returns
@@ -140,6 +149,8 @@ type CreateInvoiceInput = {
140
149
  redirectUrl?: string;
141
150
  reference?: string;
142
151
  metadata?: Record<string, string>;
152
+ /** Business labels — same normalization rules as {@link CollectPaymentInput.tags}. */
153
+ tags?: string[];
143
154
  };
144
155
  /**
145
156
  * Input for verifying a webhook signature. Operates on raw payload bytes
@@ -414,6 +425,59 @@ type EventData = {
414
425
  * data (if available), and timestamp.
415
426
  */
416
427
  type PaymentEventHandler = (data: EventData) => void;
428
+ /**
429
+ * Lightweight transaction summary returned by `listTransactions`. Carries the
430
+ * fields needed for listing and aggregation without the full provider detail
431
+ * that `getTransaction` exposes. `tags` is included so merchants can see which
432
+ * labels a row carries while paginating.
433
+ */
434
+ type TransactionSummary = {
435
+ id: string;
436
+ reference: string;
437
+ amount: number;
438
+ currency: Currency;
439
+ status: TransactionStatus;
440
+ type: TransactionType;
441
+ method: string | null;
442
+ mode: TransactionMode;
443
+ /** Smart tags on the transaction (system + developer). */
444
+ tags: string[];
445
+ createdAt: string;
446
+ updatedAt: string;
447
+ };
448
+ /**
449
+ * Input for listing and filtering transactions. All fields are optional —
450
+ * omitting filters returns the most recent transactions for the calling
451
+ * key's account and mode.
452
+ */
453
+ type ListTransactionsInput = {
454
+ /** Filter to transactions carrying ALL of these tags (AND semantics). */
455
+ tags?: string[];
456
+ status?: TransactionStatus;
457
+ type?: TransactionType;
458
+ /** Maximum number of results (1–100, default 20). */
459
+ limit?: number;
460
+ /** Zero-based offset for pagination (default 0). */
461
+ offset?: number;
462
+ /** ISO 8601 datetime — only transactions created at or after this time. */
463
+ createdAfter?: string;
464
+ /** ISO 8601 datetime — only transactions created at or before this time. */
465
+ createdBefore?: string;
466
+ };
467
+ /**
468
+ * Response from `listTransactions`. Includes the page of summaries, the count
469
+ * returned, and the effective pagination/filter parameters so callers can build
470
+ * pagination UIs without round-tripping for metadata.
471
+ */
472
+ type ListTransactionsResponse = {
473
+ transactions: TransactionSummary[];
474
+ /** Number of transactions in this page (≤ limit). */
475
+ count: number;
476
+ limit: number;
477
+ offset: number;
478
+ /** The normalized tags that were applied as filters (empty if no tag filter). */
479
+ tags: string[];
480
+ };
417
481
  /**
418
482
  * SDK instance returned by the factory. Provides all payment operations
419
483
  * and the webhook verification utility.
@@ -569,6 +633,38 @@ interface NylonPaySdk {
569
633
  * ```
570
634
  */
571
635
  createInvoice(input: CreateInvoiceInput): Promise<Result<InvoiceResponse, string>>;
636
+ /**
637
+ * List transactions for the calling key's account, with optional filtering.
638
+ * Returns lightweight summaries — use `getTransaction` for full detail.
639
+ *
640
+ * Tag filters use AND semantics: `tags: ["vip", "promo"]` returns only
641
+ * transactions tagged with BOTH labels. Tags are normalized (lowercase) before
642
+ * matching, so `"VIP"` and `"vip"` match the same rows.
643
+ *
644
+ * @example
645
+ * ```ts
646
+ * // All transactions tagged "vip" from the last week
647
+ * const result = await nylonpay.listTransactions({
648
+ * tags: ["vip"],
649
+ * createdAfter: new Date(Date.now() - 7 * 24 * 3600 * 1000).toISOString(),
650
+ * });
651
+ * if (result.isOk) console.log(result.value.transactions);
652
+ * ```
653
+ */
654
+ listTransactions(input?: ListTransactionsInput): Promise<Result<ListTransactionsResponse, string>>;
655
+ /**
656
+ * Convenience wrapper around `listTransactions` that filters by a single tag.
657
+ * Equivalent to `listTransactions({ tags: [tag], ...options })`.
658
+ *
659
+ * @example
660
+ * ```ts
661
+ * const result = await nylonpay.getTransactionsByTag("campaign-q2");
662
+ * if (result.isOk) {
663
+ * const total = result.value.transactions.reduce((s, t) => s + t.amount, 0);
664
+ * }
665
+ * ```
666
+ */
667
+ getTransactionsByTag(tag: string, options?: Omit<ListTransactionsInput, "tags">): Promise<Result<ListTransactionsResponse, string>>;
572
668
  /**
573
669
  * Verify that an incoming webhook payload was signed by Nylon Pay.
574
670
  * Operates on raw payload bytes (string or Uint8Array), not parsed JSON,
@@ -735,7 +831,7 @@ declare function parseError(error: string): SdkError;
735
831
  * retries hours later, is re-stamped and re-signed, so this never rejects
736
832
  * legitimate traffic. Pass `toleranceSeconds: 0` to skip this check.
737
833
  *
738
- * @returns True when the signature is valid and (when enforced) the webhook is fresh
834
+ * @returns True when the signature is valid and (when enforced) the webhook is fresh. Never throws — returns false on any error.
739
835
  */
740
836
  declare function verifyWebhookSignature(input: VerifyWebhookInput): boolean;
741
837
 
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createHash, createHmac, timingSafeEqual, randomBytes } from 'crypto';
1
+ import { createHash, createHmac, timingSafeEqual, randomUUID, randomBytes } from 'crypto';
2
2
  import { Ok, Err, safeTry } from 'slang-ts';
3
3
  import { type, platform, arch, release, hostname } from 'os';
4
4
 
@@ -70,10 +70,12 @@ var SDK_ACTIONS = {
70
70
  makePayoutAndResolve: "sdk-make-payout-and-resolve",
71
71
  getStatus: "sdk-get-status",
72
72
  getTransaction: "sdk-get-transaction",
73
+ listTransactions: "sdk-list-transactions",
73
74
  verifyPhone: "sdk-verify-phone",
74
75
  createInvoice: "sdk-create-invoice"
75
76
  };
76
77
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
78
+ var MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
77
79
  function generateFingerprint() {
78
80
  const components = [
79
81
  `type:${type()}`,
@@ -268,6 +270,17 @@ function createTransport({
268
270
  body: bodyString,
269
271
  signal: controller.signal
270
272
  });
273
+ const contentLength = response.headers?.get("content-length");
274
+ if (contentLength && Number(contentLength) > MAX_RESPONSE_BYTES) {
275
+ cleanup();
276
+ return Err(
277
+ JSON.stringify({
278
+ category: "internal",
279
+ message: "Received an invalid response from the server",
280
+ retryable: false
281
+ })
282
+ );
283
+ }
271
284
  if (!response.ok) {
272
285
  const statusCode = response.status;
273
286
  const retryable = RETRYABLE_STATUS_CODES.has(statusCode);
@@ -655,43 +668,44 @@ function extractSignedTimestampMs(payloadString) {
655
668
  return null;
656
669
  }
657
670
  function verifyWebhookSignature(input) {
658
- const payloadString = decodePayload(input.payload);
659
- const payloadBytes = Buffer.from(payloadString, "utf8");
660
- const expectedSignature = createHmac("sha256", input.secret).update(payloadBytes).digest("hex");
661
- const providedBuffer = Buffer.from(input.signature, "hex");
662
- const expectedBuffer = Buffer.from(expectedSignature, "hex");
663
- if (providedBuffer.length !== expectedBuffer.length) {
664
- return false;
665
- }
666
- if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
667
- return false;
668
- }
669
- const toleranceSeconds = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
670
- if (toleranceSeconds <= 0) {
671
- return true;
672
- }
673
- const timestampMs = extractSignedTimestampMs(payloadString);
674
- if (timestampMs === null) {
671
+ try {
672
+ const payloadString = decodePayload(input.payload);
673
+ const payloadBytes = Buffer.from(payloadString, "utf8");
674
+ const expectedSignature = createHmac("sha256", input.secret).update(payloadBytes).digest("hex");
675
+ const providedBuffer = Buffer.from(input.signature, "hex");
676
+ const expectedBuffer = Buffer.from(expectedSignature, "hex");
677
+ if (providedBuffer.length !== expectedBuffer.length) {
678
+ return false;
679
+ }
680
+ if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
681
+ return false;
682
+ }
683
+ const toleranceSeconds = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
684
+ if (toleranceSeconds === 0) {
685
+ return true;
686
+ }
687
+ if (toleranceSeconds < 0) {
688
+ return false;
689
+ }
690
+ const timestampMs = extractSignedTimestampMs(payloadString);
691
+ if (timestampMs === null) {
692
+ return false;
693
+ }
694
+ const ageMs = Math.abs(Date.now() - timestampMs);
695
+ return ageMs <= toleranceSeconds * 1e3;
696
+ } catch {
675
697
  return false;
676
698
  }
677
- const ageMs = Math.abs(Date.now() - timestampMs);
678
- return ageMs <= toleranceSeconds * 1e3;
679
699
  }
680
700
 
681
701
  // src/sdk.ts
682
- function generateReference() {
683
- return randomBytes(16).toString("hex").slice(0, 15);
684
- }
685
- var REFERENCE_MIN_LENGTH = 13;
686
- var REFERENCE_MAX_LENGTH = 15;
702
+ var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
687
703
  function resolveReference(reference) {
688
704
  if (reference === void 0) {
689
- return generateReference();
705
+ return randomUUID();
690
706
  }
691
- if (reference.length < REFERENCE_MIN_LENGTH || reference.length > REFERENCE_MAX_LENGTH) {
692
- throwValidation(
693
- `reference must be ${REFERENCE_MIN_LENGTH}\u2013${REFERENCE_MAX_LENGTH} characters`
694
- );
707
+ if (!UUID_REGEX.test(reference)) {
708
+ throwValidation("reference must be a valid UUID");
695
709
  }
696
710
  return reference;
697
711
  }
@@ -958,6 +972,19 @@ function createSdkInstance(config) {
958
972
  }
959
973
  return Err(result.error);
960
974
  }
975
+ async function listTransactions(input) {
976
+ const result = await transport.send({
977
+ action: SDK_ACTIONS.listTransactions,
978
+ payload: input ?? {}
979
+ });
980
+ if (result.isOk) {
981
+ return Ok(result.value);
982
+ }
983
+ return Err(result.error);
984
+ }
985
+ async function getTransactionsByTag(tag, options) {
986
+ return listTransactions({ ...options, tags: [tag] });
987
+ }
961
988
  function verifyWebhook(input) {
962
989
  return verifyWebhookSignature(input);
963
990
  }
@@ -968,6 +995,8 @@ function createSdkInstance(config) {
968
995
  makePayoutAndResolve,
969
996
  getStatus,
970
997
  getTransaction,
998
+ listTransactions,
999
+ getTransactionsByTag,
971
1000
  verifyPhone,
972
1001
  createInvoice,
973
1002
  verifyWebhookSignature: verifyWebhook