@paymos/sdk 1.0.0 → 2.0.3

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/CHANGELOG.md CHANGED
@@ -1,9 +1,30 @@
1
- # Changelog
2
-
3
- ## 1.0.0
4
-
5
- - Initial official TypeScript and JavaScript SDK.
6
- - Invoice create, get, list, iterate, cancel, confirm, and sandbox simulation.
7
- - Withdrawal create, get, list, iterate, cancel, and sandbox simulation.
8
- - Balance retrieval.
9
- - HMAC-SHA256 request signing, typed RFC 9457 errors, bounded retries, and webhook verification.
1
+ # Changelog
2
+
3
+ ## [2.0.3] - 2026-07-19
4
+
5
+ - fix: align SDK problem details semantics
6
+
7
+ ## [2.0.2] - 2026-07-12
8
+
9
+ - fix(typescript-sdk): retry trusted npm publication
10
+
11
+ ## [2.0.1] - 2026-07-12
12
+
13
+ - fix(release): align Ruby metadata and retry npm publication
14
+
15
+ ## [2.0.0] - 2026-07-12
16
+
17
+ - feat(sdk): harden official package release gates
18
+ - feat(typescript-sdk): harden typed merchant contracts
19
+ - test(sdks): align webhook conformance envelope
20
+ - refactor(sdk): expose idiomatic camelCase contracts
21
+ - feat(sdk): add official Merchant API clients
22
+ - feat(ecosystem): automate SDK and plugin releases
23
+
24
+ ## 1.0.0
25
+
26
+ - Initial official TypeScript and JavaScript SDK.
27
+ - Invoice create, get, list, iterate, cancel, confirm, and sandbox simulation.
28
+ - Withdrawal create, get, list, iterate, cancel, and sandbox simulation.
29
+ - Balance retrieval.
30
+ - HMAC-SHA256 request signing, typed RFC 9457 errors, bounded retries, and webhook verification.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Paymos Labs
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paymos Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,117 +1,117 @@
1
- # Paymos TypeScript SDK
2
-
3
- Official server-side TypeScript and JavaScript SDK for the [Paymos Merchant API](https://paymos.io/docs/quick-start).
4
-
5
- > Do not use an API secret in browser or mobile code. This package targets trusted Node.js backends and serverless runtimes with Node.js crypto support.
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install @paymos/sdk
11
- ```
12
-
13
- Node.js 22.12 or newer is required. The package ships ESM, CommonJS, and TypeScript declarations and has no runtime dependencies.
14
-
15
- ## Quick start
16
-
17
- ```ts
18
- import { Paymos, externalOrderId } from "@paymos/sdk";
19
-
20
- const paymos = new Paymos({
21
- apiKey: process.env.PAYMOS_API_KEY!,
22
- apiSecret: process.env.PAYMOS_API_SECRET!,
23
- });
24
-
25
- const invoice = await paymos.invoices.create({
26
- project_id: "prj_xxxxxxxxxxxx",
27
- amount: "49.95",
28
- currency: "USD",
29
- external_order_id: externalOrderId("order"),
30
- });
31
-
32
- console.log(invoice.payment_url);
33
- ```
34
-
35
- Use decimal strings for money. Never convert API amounts to JavaScript `number`.
36
-
37
- ## List and iterate
38
-
39
- ```ts
40
- const page = await paymos.invoices.list({
41
- project_id: "prj_xxxxxxxxxxxx",
42
- status: ["paid", "paid_over"],
43
- limit: 50,
44
- });
45
-
46
- for await (const invoice of paymos.invoices.iterate({ status: ["paid"] }, 10)) {
47
- console.log(invoice.invoice_id);
48
- }
49
- ```
50
-
51
- The API uses opaque forward-only cursors. The second `iterate` argument is a client-side maximum page count.
52
-
53
- ## Withdrawals and balances
54
-
55
- ```ts
56
- const withdrawal = await paymos.withdrawals.create({
57
- destination_address: "TRX...whitelisted...address",
58
- network: "tron",
59
- currency: "USDT",
60
- amount: "50.00",
61
- external_order_id: externalOrderId("payout"),
62
- });
63
-
64
- const balances = await paymos.balances.get();
65
- ```
66
-
67
- Use a Payout key (`rk_test_...` or `rk_live_...`) for withdrawals. Destinations must already be whitelisted.
68
-
69
- ## Webhooks
70
-
71
- Always verify the exact raw request body before parsing JSON:
72
-
73
- ```ts
74
- import { WebhookVerifier } from "@paymos/sdk";
75
-
76
- const verifier = new WebhookVerifier(process.env.PAYMOS_WEBHOOK_SECRET!);
77
- const event = verifier.constructEvent(
78
- request.headers.get("x-webhook-signature") ?? "",
79
- rawBody,
80
- );
81
-
82
- switch (event.event_type) {
83
- case "invoice.paid":
84
- // Fulfil idempotently by event.event_id.
85
- break;
86
- }
87
- ```
88
-
89
- The verifier accepts multiple `v1` signatures during secret rotation, uses constant-time comparison, and rejects timestamps outside the default five-minute tolerance.
90
-
91
- ## Errors and retries
92
-
93
- ```ts
94
- import { RateLimitError, ValidationError } from "@paymos/sdk";
95
-
96
- try {
97
- await paymos.balances.get();
98
- } catch (error) {
99
- if (error instanceof ValidationError) console.error(error.code, error.field, error.errors);
100
- if (error instanceof RateLimitError) console.error(error.retryAfterSeconds);
101
- throw error;
102
- }
103
- ```
104
-
105
- The SDK retries `429` responses and retries `5xx` only for idempotent methods. It never blindly retries a side-effecting POST after a server error.
106
-
107
- ## Links
108
-
109
- - [API documentation](https://paymos.io/docs/quick-start)
110
- - [Invoices](https://paymos.io/docs/invoices/create)
111
- - [Withdrawals](https://paymos.io/docs/withdrawals/create)
112
- - [Webhooks](https://paymos.io/docs/webhooks)
113
- - [Issues](https://github.com/Paymos-labs/typescript-sdk/issues)
114
-
115
- ## License
116
-
117
- MIT
1
+ # Paymos TypeScript SDK
2
+
3
+ Official server-side TypeScript and JavaScript SDK for the [Paymos Merchant API](https://paymos.io/docs/server-sdks).
4
+
5
+ > Do not use an API secret in browser or mobile code. This package targets trusted Node.js backends and serverless runtimes with Node.js crypto support.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @paymos/sdk
11
+ ```
12
+
13
+ Node.js 22.12 or newer is required. The package ships ESM, CommonJS, and TypeScript declarations and has no runtime dependencies.
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { Paymos, externalOrderId } from "@paymos/sdk";
19
+
20
+ const paymos = new Paymos({
21
+ apiKey: process.env.PAYMOS_API_KEY!,
22
+ apiSecret: process.env.PAYMOS_API_SECRET!,
23
+ });
24
+
25
+ const invoice = await paymos.invoices.create({
26
+ projectId: "prj_xxxxxxxxxxxx",
27
+ amount: "49.95",
28
+ currency: "USD",
29
+ externalOrderId: externalOrderId("order"),
30
+ });
31
+
32
+ console.log(invoice.paymentUrl);
33
+ ```
34
+
35
+ Use decimal strings for money. Never convert API amounts to JavaScript `number`.
36
+
37
+ ## List and iterate
38
+
39
+ ```ts
40
+ const page = await paymos.invoices.list({
41
+ projectId: "prj_xxxxxxxxxxxx",
42
+ status: ["paid", "paid_over"],
43
+ limit: 50,
44
+ });
45
+
46
+ for await (const invoice of paymos.invoices.iterate({ status: ["paid"] }, 10)) {
47
+ console.log(invoice.invoiceId);
48
+ }
49
+ ```
50
+
51
+ The API uses opaque forward-only cursors. The second `iterate` argument is a client-side maximum page count.
52
+
53
+ ## Withdrawals and balances
54
+
55
+ ```ts
56
+ const withdrawal = await paymos.withdrawals.create({
57
+ destinationAddress: "TRX...whitelisted...address",
58
+ network: "TRC20",
59
+ currency: "USDT",
60
+ amount: "50.00",
61
+ externalOrderId: externalOrderId("payout"),
62
+ });
63
+
64
+ const balances = await paymos.balances.get();
65
+ ```
66
+
67
+ Use a Payout key (`rk_test_...` or `rk_live_...`) for withdrawals. Destinations must already be whitelisted.
68
+
69
+ ## Webhooks
70
+
71
+ Always verify the exact raw request body before parsing JSON:
72
+
73
+ ```ts
74
+ import { WebhookVerifier } from "@paymos/sdk";
75
+
76
+ const verifier = new WebhookVerifier(process.env.PAYMOS_WEBHOOK_SECRET!);
77
+ const event = verifier.constructEvent(
78
+ request.headers.get("x-webhook-signature") ?? "",
79
+ rawBody,
80
+ );
81
+
82
+ switch (event.eventType) {
83
+ case "invoice.paid":
84
+ // Fulfil idempotently by event.eventId.
85
+ break;
86
+ }
87
+ ```
88
+
89
+ The verifier accepts multiple `v1` signatures during secret rotation, uses constant-time comparison, and rejects timestamps outside the default five-minute tolerance.
90
+
91
+ ## Errors and retries
92
+
93
+ ```ts
94
+ import { RateLimitError, ValidationError } from "@paymos/sdk";
95
+
96
+ try {
97
+ await paymos.balances.get();
98
+ } catch (error) {
99
+ if (error instanceof ValidationError) console.error(error.code, error.field, error.errors);
100
+ if (error instanceof RateLimitError) console.error(error.retryAfterSeconds);
101
+ throw error;
102
+ }
103
+ ```
104
+
105
+ The SDK retries `429` responses and retries `5xx` only for idempotent methods. It never blindly retries a side-effecting POST after a server error.
106
+
107
+ ## Links
108
+
109
+ - [API documentation](https://paymos.io/docs/server-sdks)
110
+ - [Invoices](https://paymos.io/docs/invoices/create)
111
+ - [Withdrawals](https://paymos.io/docs/withdrawals/create)
112
+ - [Webhooks](https://paymos.io/docs/webhooks)
113
+ - [Issues](https://github.com/Paymos-labs/typescript-sdk/issues)
114
+
115
+ ## License
116
+
117
+ MIT
package/dist/index.cjs CHANGED
@@ -47,6 +47,37 @@ __export(index_exports, {
47
47
  module.exports = __toCommonJS(index_exports);
48
48
  var import_node_crypto3 = require("crypto");
49
49
 
50
+ // src/wire.ts
51
+ function toWire(value) {
52
+ return mapKeys(value, (key) => key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`));
53
+ }
54
+ function fromWire(value) {
55
+ return mapKeys(value, (key) => key.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase()));
56
+ }
57
+ function mapKeys(value, rename) {
58
+ if (Array.isArray(value)) return value.map((item) => mapKeys(item, rename));
59
+ if (!isPlainObject(value)) return value;
60
+ const mapped = {};
61
+ for (const [key, item] of Object.entries(value)) {
62
+ const mappedKey = rename(key);
63
+ if (Object.hasOwn(mapped, mappedKey)) {
64
+ throw new TypeError(`Duplicate Paymos field after case conversion: ${mappedKey}`);
65
+ }
66
+ Object.defineProperty(mapped, mappedKey, {
67
+ value: mapKeys(item, rename),
68
+ enumerable: true,
69
+ configurable: true,
70
+ writable: true
71
+ });
72
+ }
73
+ return mapped;
74
+ }
75
+ function isPlainObject(value) {
76
+ if (value === null || typeof value !== "object") return false;
77
+ const prototype = Object.getPrototypeOf(value);
78
+ return prototype === Object.prototype || prototype === null;
79
+ }
80
+
50
81
  // src/errors.ts
51
82
  var PaymosError = class extends Error {
52
83
  constructor(message, options) {
@@ -66,8 +97,8 @@ var ApiError = class extends PaymosError {
66
97
  headers;
67
98
  problem;
68
99
  constructor(status, body, headers) {
69
- const problem = parseProblem(body);
70
- const detail = problem?.detail ?? problem?.errors?.[0]?.message ?? problem?.code ?? problem?.title;
100
+ const problem = parseProblem(status, body);
101
+ const detail = problem?.detail || problem?.code || problem?.title;
71
102
  super(`Paymos API ${status}: ${detail || body || "empty response"}`);
72
103
  this.status = status;
73
104
  this.body = body;
@@ -75,10 +106,10 @@ var ApiError = class extends PaymosError {
75
106
  this.problem = problem;
76
107
  }
77
108
  get code() {
78
- return this.problem?.errors?.[0]?.code ?? this.problem?.code ?? "";
109
+ return this.problem?.code ?? "";
79
110
  }
80
111
  get field() {
81
- return this.problem?.errors?.[0]?.field ?? this.problem?.field ?? null;
112
+ return this.problem?.field ?? null;
82
113
  }
83
114
  get errors() {
84
115
  return this.problem?.errors ?? [];
@@ -118,11 +149,13 @@ function apiErrorFromResponse(status, body, headers) {
118
149
  if (status >= 500) return new ServerError(status, body, headers);
119
150
  return new ApiError(status, body, headers);
120
151
  }
121
- function parseProblem(body) {
152
+ function parseProblem(status, body) {
122
153
  if (!body) return null;
123
154
  try {
124
155
  const parsed = JSON.parse(body);
125
- return parsed !== null && typeof parsed === "object" ? parsed : null;
156
+ if (parsed === null || typeof parsed !== "object") return null;
157
+ const problem = fromWire(parsed);
158
+ return typeof problem.type === "string" && typeof problem.title === "string" && typeof problem.status === "number" && Number.isInteger(problem.status) && problem.status === status && typeof problem.detail === "string" && typeof problem.code === "string" ? problem : null;
126
159
  } catch {
127
160
  return null;
128
161
  }
@@ -173,7 +206,7 @@ function rfc3986(value) {
173
206
  }
174
207
 
175
208
  // src/version.ts
176
- var SDK_VERSION = "1.0.0";
209
+ var SDK_VERSION = "2.0.3";
177
210
 
178
211
  // src/http.ts
179
212
  var HttpClient = class {
@@ -182,7 +215,7 @@ var HttpClient = class {
182
215
  this.options = normalizeOptions(options);
183
216
  }
184
217
  async request(method, path, payload, query = "") {
185
- const body = payload === void 0 ? "" : JSON.stringify(payload);
218
+ const body = payload === void 0 ? "" : JSON.stringify(toWire(payload));
186
219
  const url = `${this.options.baseUrl}${path}${query}`;
187
220
  let attempt = 0;
188
221
  while (true) {
@@ -224,6 +257,7 @@ var HttpClient = class {
224
257
  }
225
258
  if (shouldRetry(method, response.status) && attempt < this.options.maxRetries) {
226
259
  const retryAfter = retryAfterMs(response.headers.get("retry-after"));
260
+ await response.body?.cancel();
227
261
  await sleep(Math.max(backoff(this.options.baseDelayMs, ++attempt), retryAfter ?? 0));
228
262
  continue;
229
263
  }
@@ -231,7 +265,7 @@ var HttpClient = class {
231
265
  if (!response.ok) throw apiErrorFromResponse(response.status, responseBody, response.headers);
232
266
  if (responseBody === "") return {};
233
267
  try {
234
- return JSON.parse(responseBody);
268
+ return fromWire(JSON.parse(responseBody));
235
269
  } catch (error) {
236
270
  throw new PaymosError("Paymos API returned invalid JSON.", { cause: error });
237
271
  }
@@ -309,7 +343,7 @@ var InvoicesResource = class {
309
343
  return this.http.request("GET", `/v1/invoices/${encodePathSegment(invoiceId)}`);
310
344
  }
311
345
  list(params = {}) {
312
- return this.http.request("GET", "/v1/invoices", void 0, buildQuery(params));
346
+ return this.http.request("GET", "/v1/invoices", void 0, buildQuery(toWire(params)));
313
347
  }
314
348
  async *iterate(params = {}, maxPages = 100) {
315
349
  assertMaxPages(maxPages);
@@ -317,7 +351,7 @@ var InvoicesResource = class {
317
351
  for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
318
352
  const page = await this.list(cursor === void 0 ? params : { ...params, cursor });
319
353
  yield* page.items;
320
- const next = page.next_cursor || void 0;
354
+ const next = page.nextCursor || void 0;
321
355
  if (next === void 0) return;
322
356
  if (next === cursor) throw new Error("Paymos API returned the same pagination cursor twice.");
323
357
  cursor = next;
@@ -348,7 +382,7 @@ var WithdrawalsResource = class {
348
382
  return this.http.request("GET", `/v1/withdrawals/${encodePathSegment(withdrawalId)}`);
349
383
  }
350
384
  list(params = {}) {
351
- return this.http.request("GET", "/v1/withdrawals", void 0, buildQuery(params));
385
+ return this.http.request("GET", "/v1/withdrawals", void 0, buildQuery(toWire(params)));
352
386
  }
353
387
  async *iterate(params = {}, maxPages = 100) {
354
388
  assertMaxPages(maxPages);
@@ -356,7 +390,7 @@ var WithdrawalsResource = class {
356
390
  for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
357
391
  const page = await this.list(cursor === void 0 ? params : { ...params, cursor });
358
392
  yield* page.items;
359
- const next = page.next_cursor || void 0;
393
+ const next = page.nextCursor || void 0;
360
394
  if (next === void 0) return;
361
395
  if (next === cursor) throw new Error("Paymos API returned the same pagination cursor twice.");
362
396
  cursor = next;
@@ -442,8 +476,11 @@ var WebhookVerifier = class {
442
476
  constructEvent(signatureHeader, rawBody, now = Math.floor(Date.now() / 1e3)) {
443
477
  this.assertValid(signatureHeader, rawBody, now);
444
478
  try {
445
- return JSON.parse(rawBody.toString());
479
+ const event = fromWire(JSON.parse(rawBody.toString()));
480
+ assertEventEnvelope(event);
481
+ return event;
446
482
  } catch (error) {
483
+ if (error instanceof TypeError) throw error;
447
484
  throw new TypeError("Webhook payload is not valid JSON.", { cause: error });
448
485
  }
449
486
  }
@@ -451,16 +488,39 @@ var WebhookVerifier = class {
451
488
  function parseHeader(header) {
452
489
  if (typeof header !== "string") return null;
453
490
  let timestamp;
491
+ let timestampCount = 0;
454
492
  const signatures = [];
455
493
  for (const part of header.split(",")) {
456
494
  const separator = part.indexOf("=");
457
495
  if (separator < 1) continue;
458
496
  const key = part.slice(0, separator).trim();
459
497
  const value = part.slice(separator + 1).trim();
460
- if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
498
+ if (key === "t" && /^\d+$/.test(value)) {
499
+ timestamp = Number(value);
500
+ timestampCount += 1;
501
+ }
461
502
  if (key === "v1" && value !== "") signatures.push(value);
462
503
  }
463
- return timestamp === void 0 || signatures.length === 0 ? null : { timestamp, signatures };
504
+ return timestamp === void 0 || timestampCount !== 1 || !Number.isSafeInteger(timestamp) || signatures.length === 0 ? null : { timestamp, signatures };
505
+ }
506
+ function assertEventEnvelope(value) {
507
+ if (value === null || typeof value !== "object") throw new TypeError("Webhook payload must be an object.");
508
+ const event = value;
509
+ if (typeof event.eventId !== "string" || event.eventId.trim() === "") {
510
+ throw new TypeError("Webhook eventId must be a non-empty string.");
511
+ }
512
+ if (typeof event.eventType !== "string" || event.eventType.trim() === "") {
513
+ throw new TypeError("Webhook eventType must be a non-empty string.");
514
+ }
515
+ if (!Number.isSafeInteger(event.version) || event.version < 1) {
516
+ throw new TypeError("Webhook version must be a positive integer.");
517
+ }
518
+ if (!Number.isSafeInteger(event.occurredAt) || event.occurredAt < 0) {
519
+ throw new TypeError("Webhook occurredAt must be a non-negative Unix timestamp.");
520
+ }
521
+ if (event.data === null || typeof event.data !== "object") {
522
+ throw new TypeError("Webhook data must be an object.");
523
+ }
464
524
  }
465
525
 
466
526
  // src/index.ts