@paymos/sdk 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,464 @@
1
+ // src/index.ts
2
+ import { randomUUID } from "crypto";
3
+
4
+ // src/errors.ts
5
+ var PaymosError = class extends Error {
6
+ constructor(message, options) {
7
+ super(message, options);
8
+ this.name = new.target.name;
9
+ }
10
+ };
11
+ var ConfigurationError = class extends PaymosError {
12
+ };
13
+ var SignatureMismatchError = class extends PaymosError {
14
+ };
15
+ var TimestampSkewError = class extends PaymosError {
16
+ };
17
+ var ApiError = class extends PaymosError {
18
+ status;
19
+ body;
20
+ headers;
21
+ problem;
22
+ constructor(status, body, headers) {
23
+ const problem = parseProblem(body);
24
+ const detail = problem?.detail ?? problem?.errors?.[0]?.message ?? problem?.code ?? problem?.title;
25
+ super(`Paymos API ${status}: ${detail || body || "empty response"}`);
26
+ this.status = status;
27
+ this.body = body;
28
+ this.headers = headers;
29
+ this.problem = problem;
30
+ }
31
+ get code() {
32
+ return this.problem?.errors?.[0]?.code ?? this.problem?.code ?? "";
33
+ }
34
+ get field() {
35
+ return this.problem?.errors?.[0]?.field ?? this.problem?.field ?? null;
36
+ }
37
+ get errors() {
38
+ return this.problem?.errors ?? [];
39
+ }
40
+ get retryAfterSeconds() {
41
+ const value = this.headers.get("retry-after");
42
+ if (!value) return null;
43
+ if (/^\d+$/.test(value)) return Number(value);
44
+ const date = Date.parse(value);
45
+ return Number.isNaN(date) ? null : Math.max(0, Math.ceil((date - Date.now()) / 1e3));
46
+ }
47
+ };
48
+ var ValidationError = class extends ApiError {
49
+ };
50
+ var AuthenticationError = class extends ApiError {
51
+ };
52
+ var NotFoundError = class extends ApiError {
53
+ };
54
+ var ConflictError = class extends ApiError {
55
+ };
56
+ var GoneError = class extends ApiError {
57
+ };
58
+ var RateLimitError = class extends ApiError {
59
+ };
60
+ var ServerError = class extends ApiError {
61
+ };
62
+ var UnavailableError = class extends ServerError {
63
+ };
64
+ function apiErrorFromResponse(status, body, headers) {
65
+ if (status === 400) return new ValidationError(status, body, headers);
66
+ if (status === 401 || status === 403) return new AuthenticationError(status, body, headers);
67
+ if (status === 404) return new NotFoundError(status, body, headers);
68
+ if (status === 409) return new ConflictError(status, body, headers);
69
+ if (status === 410) return new GoneError(status, body, headers);
70
+ if (status === 429) return new RateLimitError(status, body, headers);
71
+ if (status === 503) return new UnavailableError(status, body, headers);
72
+ if (status >= 500) return new ServerError(status, body, headers);
73
+ return new ApiError(status, body, headers);
74
+ }
75
+ function parseProblem(body) {
76
+ if (!body) return null;
77
+ try {
78
+ const parsed = JSON.parse(body);
79
+ return parsed !== null && typeof parsed === "object" ? parsed : null;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ // src/signing.ts
86
+ import { createHash, createHmac } from "crypto";
87
+ function stringToSign(timestamp, method, path, query = "", body = "") {
88
+ const bodyHash = body === "" ? "" : createHash("sha256").update(body, "utf8").digest("hex");
89
+ return `${timestamp}
90
+ ${method.toUpperCase()}
91
+ ${path}
92
+ ${query}
93
+ ${bodyHash}`;
94
+ }
95
+ function sign(secret, value) {
96
+ return createHmac("sha256", secret).update(value, "utf8").digest("base64");
97
+ }
98
+ function authorizationHeader(apiKey, apiSecret, timestamp, method, path, query = "", body = "") {
99
+ const signature = sign(apiSecret, stringToSign(timestamp, method, path, query, body));
100
+ return `HMAC-SHA256 ${apiKey}:${signature}`;
101
+ }
102
+ function encodePathSegment(value) {
103
+ return rfc3986(value);
104
+ }
105
+ function buildQuery(filters) {
106
+ const valuesByKey = filters;
107
+ const parts = [];
108
+ for (const key of Object.keys(filters).sort()) {
109
+ const raw = valuesByKey[key];
110
+ if (raw === void 0 || raw === null) continue;
111
+ const values = Array.isArray(raw) ? [...raw].sort() : [raw];
112
+ if (values.length === 0) throw new TypeError(`Paymos list filter cannot be empty: ${key}`);
113
+ for (const value of values) {
114
+ if (typeof value !== "string" && typeof value !== "number" || String(value) === "") {
115
+ throw new TypeError(`Invalid Paymos list filter: ${key}`);
116
+ }
117
+ parts.push(`${rfc3986(key)}=${rfc3986(String(value))}`);
118
+ }
119
+ }
120
+ return parts.length === 0 ? "" : `?${parts.join("&")}`;
121
+ }
122
+ function rfc3986(value) {
123
+ return encodeURIComponent(value).replace(
124
+ /[!'()*]/g,
125
+ (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`
126
+ );
127
+ }
128
+
129
+ // src/version.ts
130
+ var SDK_VERSION = "1.0.0";
131
+
132
+ // src/http.ts
133
+ var HttpClient = class {
134
+ options;
135
+ constructor(options) {
136
+ this.options = normalizeOptions(options);
137
+ }
138
+ async request(method, path, payload, query = "") {
139
+ const body = payload === void 0 ? "" : JSON.stringify(payload);
140
+ const url = `${this.options.baseUrl}${path}${query}`;
141
+ let attempt = 0;
142
+ while (true) {
143
+ const timestamp = Math.floor(this.options.clock() / 1e3).toString();
144
+ const headers = new Headers({
145
+ Authorization: authorizationHeader(
146
+ this.options.apiKey,
147
+ this.options.apiSecret,
148
+ timestamp,
149
+ method,
150
+ path,
151
+ query,
152
+ body
153
+ ),
154
+ "X-Request-Timestamp": timestamp,
155
+ "Content-Type": "application/json",
156
+ Accept: "application/json",
157
+ "User-Agent": `paymos-typescript/${SDK_VERSION}`
158
+ });
159
+ const controller = new AbortController();
160
+ const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs);
161
+ let response;
162
+ try {
163
+ const requestInit = {
164
+ method,
165
+ headers,
166
+ signal: controller.signal
167
+ };
168
+ if (body !== "") requestInit.body = body;
169
+ response = await this.options.fetch(url, requestInit);
170
+ } catch (error) {
171
+ if (attempt >= this.options.maxRetries || !isIdempotent(method)) {
172
+ throw new PaymosError(`Paymos request failed: ${errorMessage(error)}`, { cause: error });
173
+ }
174
+ await sleep(backoff(this.options.baseDelayMs, ++attempt));
175
+ continue;
176
+ } finally {
177
+ clearTimeout(timeout);
178
+ }
179
+ if (shouldRetry(method, response.status) && attempt < this.options.maxRetries) {
180
+ const retryAfter = retryAfterMs(response.headers.get("retry-after"));
181
+ await sleep(Math.max(backoff(this.options.baseDelayMs, ++attempt), retryAfter ?? 0));
182
+ continue;
183
+ }
184
+ const responseBody = await response.text();
185
+ if (!response.ok) throw apiErrorFromResponse(response.status, responseBody, response.headers);
186
+ if (responseBody === "") return {};
187
+ try {
188
+ return JSON.parse(responseBody);
189
+ } catch (error) {
190
+ throw new PaymosError("Paymos API returned invalid JSON.", { cause: error });
191
+ }
192
+ }
193
+ }
194
+ };
195
+ function normalizeOptions(options) {
196
+ if (!options || typeof options !== "object") throw new ConfigurationError("Client options are required.");
197
+ const apiKey = requireString(options.apiKey, "apiKey");
198
+ const apiSecret = requireString(options.apiSecret, "apiSecret");
199
+ const baseUrl = requireString(options.baseUrl ?? "https://api.paymos.io", "baseUrl").replace(/\/$/, "");
200
+ const timeoutMs = options.timeoutMs ?? 3e4;
201
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
202
+ throw new ConfigurationError("timeoutMs must be a positive integer.");
203
+ }
204
+ const maxRetries = options.retry === false ? 0 : options.retry?.maxRetries ?? 2;
205
+ const baseDelayMs = options.retry === false ? 0 : options.retry?.baseDelayMs ?? 150;
206
+ if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) {
207
+ throw new ConfigurationError("retry.maxRetries must be a non-negative integer.");
208
+ }
209
+ if (!Number.isSafeInteger(baseDelayMs) || baseDelayMs < 0) {
210
+ throw new ConfigurationError("retry.baseDelayMs must be a non-negative integer.");
211
+ }
212
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
213
+ if (typeof fetchImplementation !== "function") throw new ConfigurationError("A Fetch implementation is required.");
214
+ return {
215
+ apiKey,
216
+ apiSecret,
217
+ baseUrl,
218
+ timeoutMs,
219
+ maxRetries,
220
+ baseDelayMs,
221
+ fetch: fetchImplementation,
222
+ clock: options.clock ?? Date.now
223
+ };
224
+ }
225
+ function requireString(value, name) {
226
+ if (typeof value !== "string" || value.trim() === "") {
227
+ throw new ConfigurationError(`${name} must be a non-empty string.`);
228
+ }
229
+ return value;
230
+ }
231
+ function isIdempotent(method) {
232
+ return ["GET", "HEAD", "OPTIONS"].includes(method.toUpperCase());
233
+ }
234
+ function shouldRetry(method, status) {
235
+ return status === 429 || status >= 500 && isIdempotent(method);
236
+ }
237
+ function backoff(baseDelayMs, attempt) {
238
+ return baseDelayMs * 2 ** Math.max(0, attempt - 1);
239
+ }
240
+ function retryAfterMs(value) {
241
+ if (!value) return null;
242
+ if (/^\d+$/.test(value)) return Number(value) * 1e3;
243
+ const date = Date.parse(value);
244
+ return Number.isNaN(date) ? null : Math.max(0, date - Date.now());
245
+ }
246
+ function sleep(ms) {
247
+ return ms <= 0 ? Promise.resolve() : new Promise((resolve) => setTimeout(resolve, ms));
248
+ }
249
+ function errorMessage(error) {
250
+ return error instanceof Error ? error.message : String(error);
251
+ }
252
+
253
+ // src/resources.ts
254
+ var InvoicesResource = class {
255
+ constructor(http) {
256
+ this.http = http;
257
+ }
258
+ http;
259
+ create(params) {
260
+ return this.http.request("POST", "/v1/invoices", params);
261
+ }
262
+ get(invoiceId) {
263
+ return this.http.request("GET", `/v1/invoices/${encodePathSegment(invoiceId)}`);
264
+ }
265
+ list(params = {}) {
266
+ return this.http.request("GET", "/v1/invoices", void 0, buildQuery(params));
267
+ }
268
+ async *iterate(params = {}, maxPages = 100) {
269
+ assertMaxPages(maxPages);
270
+ let cursor = params.cursor;
271
+ for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
272
+ const page = await this.list(cursor === void 0 ? params : { ...params, cursor });
273
+ yield* page.items;
274
+ const next = page.next_cursor || void 0;
275
+ if (next === void 0) return;
276
+ if (next === cursor) throw new Error("Paymos API returned the same pagination cursor twice.");
277
+ cursor = next;
278
+ }
279
+ }
280
+ cancel(invoiceId, reason) {
281
+ assertReason(reason);
282
+ return this.http.request("POST", `/v1/invoices/${encodePathSegment(invoiceId)}/cancel`, { reason });
283
+ }
284
+ confirmPayment(invoiceId, params) {
285
+ return this.http.request("POST", `/v1/invoices/${encodePathSegment(invoiceId)}/confirm-payment`, params);
286
+ }
287
+ simulatePayment(invoiceId, stage) {
288
+ return this.http.request("POST", `/v1/sandbox/invoices/${encodePathSegment(invoiceId)}/simulate-payment`, {
289
+ stage
290
+ });
291
+ }
292
+ };
293
+ var WithdrawalsResource = class {
294
+ constructor(http) {
295
+ this.http = http;
296
+ }
297
+ http;
298
+ create(params) {
299
+ return this.http.request("POST", "/v1/withdrawals", params);
300
+ }
301
+ get(withdrawalId) {
302
+ return this.http.request("GET", `/v1/withdrawals/${encodePathSegment(withdrawalId)}`);
303
+ }
304
+ list(params = {}) {
305
+ return this.http.request("GET", "/v1/withdrawals", void 0, buildQuery(params));
306
+ }
307
+ async *iterate(params = {}, maxPages = 100) {
308
+ assertMaxPages(maxPages);
309
+ let cursor = params.cursor;
310
+ for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
311
+ const page = await this.list(cursor === void 0 ? params : { ...params, cursor });
312
+ yield* page.items;
313
+ const next = page.next_cursor || void 0;
314
+ if (next === void 0) return;
315
+ if (next === cursor) throw new Error("Paymos API returned the same pagination cursor twice.");
316
+ cursor = next;
317
+ }
318
+ }
319
+ cancel(withdrawalId, reason) {
320
+ assertReason(reason);
321
+ return this.http.request("POST", `/v1/withdrawals/${encodePathSegment(withdrawalId)}/cancel`, { reason });
322
+ }
323
+ simulateCompletion(withdrawalId) {
324
+ return this.http.request(
325
+ "POST",
326
+ `/v1/sandbox/withdrawals/${encodePathSegment(withdrawalId)}/simulate-completion`
327
+ );
328
+ }
329
+ };
330
+ var BalancesResource = class {
331
+ constructor(http) {
332
+ this.http = http;
333
+ }
334
+ http;
335
+ get() {
336
+ return this.http.request("GET", "/v1/balances");
337
+ }
338
+ };
339
+ var SystemResource = class {
340
+ constructor(http) {
341
+ this.http = http;
342
+ }
343
+ http;
344
+ time() {
345
+ return this.http.request("GET", "/v1/time");
346
+ }
347
+ };
348
+ function assertReason(reason) {
349
+ if (typeof reason !== "string" || reason.trim() === "" || reason.length > 500) {
350
+ throw new TypeError("Cancellation reason must contain 1 to 500 characters.");
351
+ }
352
+ }
353
+ function assertMaxPages(maxPages) {
354
+ if (!Number.isSafeInteger(maxPages) || maxPages < 1) {
355
+ throw new TypeError("maxPages must be a positive integer.");
356
+ }
357
+ }
358
+
359
+ // src/webhooks.ts
360
+ import { createHmac as createHmac2, timingSafeEqual } from "crypto";
361
+ var WebhookVerifier = class {
362
+ constructor(secret, toleranceSeconds = 300) {
363
+ this.secret = secret;
364
+ this.toleranceSeconds = toleranceSeconds;
365
+ if (typeof secret !== "string" || secret.trim() === "") {
366
+ throw new TypeError("Webhook secret must be a non-empty string.");
367
+ }
368
+ if (!Number.isSafeInteger(toleranceSeconds) || toleranceSeconds < 0) {
369
+ throw new TypeError("Webhook tolerance must be a non-negative integer.");
370
+ }
371
+ }
372
+ secret;
373
+ toleranceSeconds;
374
+ verify(signatureHeader, rawBody, now = Math.floor(Date.now() / 1e3)) {
375
+ try {
376
+ this.assertValid(signatureHeader, rawBody, now);
377
+ return true;
378
+ } catch {
379
+ return false;
380
+ }
381
+ }
382
+ assertValid(signatureHeader, rawBody, now = Math.floor(Date.now() / 1e3)) {
383
+ const parsed = parseHeader(signatureHeader);
384
+ if (!parsed) throw new SignatureMismatchError("Webhook signature header is missing or malformed.");
385
+ if (Math.abs(now - parsed.timestamp) > this.toleranceSeconds) {
386
+ throw new TimestampSkewError("Webhook timestamp is outside the allowed tolerance.");
387
+ }
388
+ const expected = createHmac2("sha256", this.secret).update(`${parsed.timestamp}.`).update(rawBody).digest();
389
+ const matched = parsed.signatures.some((signature) => {
390
+ if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false;
391
+ const candidate = Buffer.from(signature, "hex");
392
+ return candidate.length === expected.length && timingSafeEqual(candidate, expected);
393
+ });
394
+ if (!matched) throw new SignatureMismatchError("Webhook signature does not match payload.");
395
+ }
396
+ constructEvent(signatureHeader, rawBody, now = Math.floor(Date.now() / 1e3)) {
397
+ this.assertValid(signatureHeader, rawBody, now);
398
+ try {
399
+ return JSON.parse(rawBody.toString());
400
+ } catch (error) {
401
+ throw new TypeError("Webhook payload is not valid JSON.", { cause: error });
402
+ }
403
+ }
404
+ };
405
+ function parseHeader(header) {
406
+ if (typeof header !== "string") return null;
407
+ let timestamp;
408
+ const signatures = [];
409
+ for (const part of header.split(",")) {
410
+ const separator = part.indexOf("=");
411
+ if (separator < 1) continue;
412
+ const key = part.slice(0, separator).trim();
413
+ const value = part.slice(separator + 1).trim();
414
+ if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
415
+ if (key === "v1" && value !== "") signatures.push(value);
416
+ }
417
+ return timestamp === void 0 || signatures.length === 0 ? null : { timestamp, signatures };
418
+ }
419
+
420
+ // src/index.ts
421
+ var Paymos = class {
422
+ invoices;
423
+ withdrawals;
424
+ balances;
425
+ system;
426
+ constructor(options) {
427
+ const http = new HttpClient(options);
428
+ this.invoices = new InvoicesResource(http);
429
+ this.withdrawals = new WithdrawalsResource(http);
430
+ this.balances = new BalancesResource(http);
431
+ this.system = new SystemResource(http);
432
+ }
433
+ };
434
+ function externalOrderId(prefix = "order") {
435
+ if (!/^[A-Za-z0-9_-]{1,32}$/.test(prefix)) {
436
+ throw new TypeError("externalOrderId prefix must contain 1 to 32 letters, digits, underscores, or hyphens.");
437
+ }
438
+ return `${prefix}_${randomUUID()}`;
439
+ }
440
+ export {
441
+ ApiError,
442
+ AuthenticationError,
443
+ ConfigurationError,
444
+ ConflictError,
445
+ GoneError,
446
+ NotFoundError,
447
+ Paymos,
448
+ PaymosError,
449
+ RateLimitError,
450
+ SDK_VERSION,
451
+ ServerError,
452
+ SignatureMismatchError,
453
+ TimestampSkewError,
454
+ UnavailableError,
455
+ ValidationError,
456
+ WebhookVerifier,
457
+ apiErrorFromResponse,
458
+ authorizationHeader,
459
+ buildQuery,
460
+ encodePathSegment,
461
+ externalOrderId,
462
+ sign,
463
+ stringToSign
464
+ };
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@paymos/sdk",
3
+ "version": "1.0.0",
4
+ "description": "Official TypeScript and JavaScript SDK for the Paymos Merchant API",
5
+ "keywords": [
6
+ "paymos",
7
+ "payments",
8
+ "stablecoins",
9
+ "usdt",
10
+ "usdc",
11
+ "crypto",
12
+ "sdk"
13
+ ],
14
+ "homepage": "https://paymos.io/docs/quick-start",
15
+ "bugs": "https://github.com/Paymos-labs/typescript-sdk/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/Paymos-labs/typescript-sdk.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": "Paymos Labs <support@paymos.io>",
22
+ "type": "module",
23
+ "main": "./dist/index.cjs",
24
+ "module": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "require": "./dist/index.cjs"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE",
37
+ "CHANGELOG.md"
38
+ ],
39
+ "sideEffects": false,
40
+ "engines": {
41
+ "node": ">=22.12"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
45
+ "check": "tsc --noEmit",
46
+ "test": "vitest run",
47
+ "test:coverage": "vitest run --coverage",
48
+ "prepack": "npm run check && npm test && npm run build"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.10.0",
52
+ "@vitest/coverage-v8": "^3.2.0",
53
+ "tsup": "^8.5.0",
54
+ "typescript": "^5.8.0",
55
+ "vitest": "^3.2.0"
56
+ },
57
+ "overrides": {
58
+ "esbuild": "0.28.1"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public",
62
+ "provenance": true
63
+ }
64
+ }