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