@payclave/sdk-server 0.1.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 +17 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/index.cjs +814 -0
- package/dist/index.d.cts +212 -0
- package/dist/index.d.ts +212 -0
- package/dist/index.js +802 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
var PAYCLAVE_WEBHOOK_EVENT_TYPES = [
|
|
6
|
+
"invoice.paid",
|
|
7
|
+
"invoice.expired",
|
|
8
|
+
"payment.failed",
|
|
9
|
+
"payment.underpaid",
|
|
10
|
+
"payment.overpaid"
|
|
11
|
+
];
|
|
12
|
+
var PAYCLAVE_API_ERROR_CODES = [
|
|
13
|
+
"API_KEY_FORBIDDEN",
|
|
14
|
+
"API_KEY_REQUIRED",
|
|
15
|
+
"AUTH_FORBIDDEN",
|
|
16
|
+
"AUTH_REQUIRED",
|
|
17
|
+
"DEFAULT_WALLET_REQUIRED",
|
|
18
|
+
"INTERNAL_JOB_UNAUTHORIZED",
|
|
19
|
+
"INTERNAL_SERVER_ERROR",
|
|
20
|
+
"INVALID_AMOUNT",
|
|
21
|
+
"INVALID_API_KEY",
|
|
22
|
+
"INVALID_EXPIRY",
|
|
23
|
+
"INVALID_IDEMPOTENCY_KEY",
|
|
24
|
+
"INVALID_DEPOSIT_NETWORK",
|
|
25
|
+
"INVALID_WALLET_PAYMENT_ATTEMPT",
|
|
26
|
+
"INVALID_TX_HASH",
|
|
27
|
+
"INVALID_WALLET_ADDRESS",
|
|
28
|
+
"NOT_FOUND",
|
|
29
|
+
"RATE_LIMIT_EXCEEDED",
|
|
30
|
+
"REQUEST_ERROR",
|
|
31
|
+
"SCAN_OR_COPY_UNAVAILABLE",
|
|
32
|
+
"WALLET_PAYMENTS_UNAVAILABLE",
|
|
33
|
+
"UNSUPPORTED_DEFAULT_WALLET",
|
|
34
|
+
"VALIDATION_ERROR",
|
|
35
|
+
"WEBHOOK_DELIVERY_NOT_FOUND",
|
|
36
|
+
"WEBHOOK_ENDPOINT_NOT_FOUND",
|
|
37
|
+
"WEBHOOK_EVENT_UNSUPPORTED",
|
|
38
|
+
"WEBHOOK_SIGNATURE_INVALID",
|
|
39
|
+
"CHECKOUT_SESSION_NOT_PAYABLE",
|
|
40
|
+
"INVOICE_NOT_FOUND",
|
|
41
|
+
"INVOICE_STATE_MISMATCH",
|
|
42
|
+
"INVOICE_TRANSITION_GUARD_FAILED",
|
|
43
|
+
"IDEMPOTENCY_KEY_CONFLICT"
|
|
44
|
+
];
|
|
45
|
+
var PAYCLAVE_SERVER_SDK_ERROR_CODES = [
|
|
46
|
+
"INVALID_SECRET_KEY",
|
|
47
|
+
"INVALID_API_BASE_URL",
|
|
48
|
+
"INVALID_TIMEOUT",
|
|
49
|
+
"INVALID_ID",
|
|
50
|
+
"INVALID_AMOUNT",
|
|
51
|
+
"INVALID_EXPIRY",
|
|
52
|
+
"INVALID_METADATA",
|
|
53
|
+
"INVALID_IDEMPOTENCY_KEY",
|
|
54
|
+
"INVALID_LIMIT",
|
|
55
|
+
"INVALID_WEBHOOK_URL",
|
|
56
|
+
"INVALID_REQUEST_BODY",
|
|
57
|
+
"INVALID_RESPONSE",
|
|
58
|
+
"NETWORK_ERROR",
|
|
59
|
+
"TIMEOUT",
|
|
60
|
+
"ABORTED"
|
|
61
|
+
];
|
|
62
|
+
var PAYCLAVE_WEBHOOK_VERIFICATION_ERROR_CODES = [
|
|
63
|
+
"WEBHOOK_SIGNATURE_INVALID",
|
|
64
|
+
"WEBHOOK_TIMESTAMP_INVALID",
|
|
65
|
+
"WEBHOOK_TIMESTAMP_OUTSIDE_TOLERANCE",
|
|
66
|
+
"WEBHOOK_TOLERANCE_INVALID",
|
|
67
|
+
"WEBHOOK_PAYLOAD_INVALID"
|
|
68
|
+
];
|
|
69
|
+
var PayclaveError = class extends Error {
|
|
70
|
+
code;
|
|
71
|
+
status;
|
|
72
|
+
requestId;
|
|
73
|
+
details;
|
|
74
|
+
constructor({
|
|
75
|
+
message,
|
|
76
|
+
code,
|
|
77
|
+
status,
|
|
78
|
+
requestId,
|
|
79
|
+
details,
|
|
80
|
+
cause
|
|
81
|
+
}) {
|
|
82
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
83
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
84
|
+
this.name = "PayclaveError";
|
|
85
|
+
this.code = code;
|
|
86
|
+
this.status = status;
|
|
87
|
+
this.requestId = requestId ?? null;
|
|
88
|
+
this.details = details;
|
|
89
|
+
}
|
|
90
|
+
toJSON() {
|
|
91
|
+
return {
|
|
92
|
+
name: this.name,
|
|
93
|
+
message: this.message,
|
|
94
|
+
code: this.code,
|
|
95
|
+
status: this.status,
|
|
96
|
+
requestId: this.requestId,
|
|
97
|
+
details: this.details
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var PAYCLAVE_SERVER_SDK_NAME = "payclave-sdk-server";
|
|
102
|
+
var PAYCLAVE_SERVER_SDK_VERSION = "0.1.0";
|
|
103
|
+
var defaultApiBaseUrl = "https://api.payclave.com";
|
|
104
|
+
var defaultFetcher = (input, init) => fetch(input, init);
|
|
105
|
+
var secretKeyPattern = /^sk_(test|live)_[A-Za-z0-9_-]+$/;
|
|
106
|
+
var amountPattern = /^(0|[1-9]\d*)(\.\d{1,6})?$/;
|
|
107
|
+
var webhookUrlMessage = "Webhook URL must be a valid http or https URL.";
|
|
108
|
+
var defaultWebhookToleranceSeconds = 300;
|
|
109
|
+
var minExpiryMinutes = 5;
|
|
110
|
+
var maxExpiryMinutes = 24 * 60;
|
|
111
|
+
var supportedWebhookEventTypes = new Set(PAYCLAVE_WEBHOOK_EVENT_TYPES);
|
|
112
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
113
|
+
var isPlainObject = (value) => {
|
|
114
|
+
if (!isRecord(value)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const prototype = Object.getPrototypeOf(value);
|
|
118
|
+
return prototype === Object.prototype || prototype === null;
|
|
119
|
+
};
|
|
120
|
+
var isStringOrNull = (value) => typeof value === "string" || value === null;
|
|
121
|
+
var isMetadata = (value) => isPlainObject(value);
|
|
122
|
+
var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
123
|
+
var joinUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
124
|
+
var buildClientHeader = () => `${PAYCLAVE_SERVER_SDK_NAME}/${PAYCLAVE_SERVER_SDK_VERSION}`;
|
|
125
|
+
var assertSecretKey = (secretKey) => {
|
|
126
|
+
if (typeof secretKey !== "string" || !secretKeyPattern.test(secretKey)) {
|
|
127
|
+
throw new PayclaveError({
|
|
128
|
+
message: "A valid Payclave secret key is required.",
|
|
129
|
+
code: "INVALID_SECRET_KEY",
|
|
130
|
+
status: 0
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
var normalizeApiBaseUrl = (apiBaseUrl) => {
|
|
135
|
+
if (typeof apiBaseUrl !== "string") {
|
|
136
|
+
throw new PayclaveError({
|
|
137
|
+
message: "Payclave API base URL must be a valid HTTP or HTTPS URL.",
|
|
138
|
+
code: "INVALID_API_BASE_URL",
|
|
139
|
+
status: 0
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const value = apiBaseUrl.trim();
|
|
143
|
+
let url;
|
|
144
|
+
try {
|
|
145
|
+
url = new URL(value);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw new PayclaveError({
|
|
148
|
+
message: "Payclave API base URL must be a valid HTTP or HTTPS URL.",
|
|
149
|
+
code: "INVALID_API_BASE_URL",
|
|
150
|
+
status: 0,
|
|
151
|
+
cause: error
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
155
|
+
throw new PayclaveError({
|
|
156
|
+
message: "Payclave API base URL must use http or https.",
|
|
157
|
+
code: "INVALID_API_BASE_URL",
|
|
158
|
+
status: 0
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (url.search || url.hash) {
|
|
162
|
+
throw new PayclaveError({
|
|
163
|
+
message: "Payclave API base URL must not include query or hash parts.",
|
|
164
|
+
code: "INVALID_API_BASE_URL",
|
|
165
|
+
status: 0
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return value.replace(/\/+$/, "");
|
|
169
|
+
};
|
|
170
|
+
var assertTimeoutMs = (timeoutMs) => {
|
|
171
|
+
if (timeoutMs !== void 0 && (!Number.isInteger(timeoutMs) || timeoutMs < 1)) {
|
|
172
|
+
throw new PayclaveError({
|
|
173
|
+
message: "Timeout must be a positive integer number of milliseconds.",
|
|
174
|
+
code: "INVALID_TIMEOUT",
|
|
175
|
+
status: 0
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var assertAmount = (amount) => {
|
|
180
|
+
if (typeof amount !== "string" || !amountPattern.test(amount) || Number(amount) <= 0) {
|
|
181
|
+
throw new PayclaveError({
|
|
182
|
+
message: "Amount must be a positive USDT value with up to 6 decimals.",
|
|
183
|
+
code: "INVALID_AMOUNT",
|
|
184
|
+
status: 0
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var assertOptionalExpiry = (expiresInMinutes) => {
|
|
189
|
+
if (expiresInMinutes !== void 0 && (!Number.isInteger(expiresInMinutes) || expiresInMinutes < minExpiryMinutes || expiresInMinutes > maxExpiryMinutes)) {
|
|
190
|
+
throw new PayclaveError({
|
|
191
|
+
message: "Expiry must be between 5 minutes and 24 hours.",
|
|
192
|
+
code: "INVALID_EXPIRY",
|
|
193
|
+
status: 0
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
var assertIdentifier = (value, label) => {
|
|
198
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
199
|
+
throw new PayclaveError({
|
|
200
|
+
message: `${label} is required.`,
|
|
201
|
+
code: "INVALID_ID",
|
|
202
|
+
status: 0
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
var assertOptionalMetadata = (metadata) => {
|
|
207
|
+
if (metadata !== void 0 && !isPlainObject(metadata)) {
|
|
208
|
+
throw new PayclaveError({
|
|
209
|
+
message: "Metadata must be a JSON object.",
|
|
210
|
+
code: "INVALID_METADATA",
|
|
211
|
+
status: 0
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
var assertIdempotencyKey = (key) => {
|
|
216
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
217
|
+
throw new PayclaveError({
|
|
218
|
+
message: "Idempotency key must not be empty.",
|
|
219
|
+
code: "INVALID_IDEMPOTENCY_KEY",
|
|
220
|
+
status: 0
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (key.length > 255) {
|
|
224
|
+
throw new PayclaveError({
|
|
225
|
+
message: "Idempotency key must be 255 characters or fewer.",
|
|
226
|
+
code: "INVALID_IDEMPOTENCY_KEY",
|
|
227
|
+
status: 0
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
for (const character of key) {
|
|
231
|
+
const codePoint = character.charCodeAt(0);
|
|
232
|
+
if (codePoint < 33 || codePoint > 126) {
|
|
233
|
+
throw new PayclaveError({
|
|
234
|
+
message: "Idempotency key must contain visible ASCII characters only.",
|
|
235
|
+
code: "INVALID_IDEMPOTENCY_KEY",
|
|
236
|
+
status: 0
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var normalizeWebhookUrl = (url) => {
|
|
242
|
+
if (typeof url !== "string") {
|
|
243
|
+
throw new PayclaveError({
|
|
244
|
+
message: webhookUrlMessage,
|
|
245
|
+
code: "INVALID_WEBHOOK_URL",
|
|
246
|
+
status: 0
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const value = url.trim();
|
|
250
|
+
let parsed;
|
|
251
|
+
try {
|
|
252
|
+
parsed = new URL(value);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
throw new PayclaveError({
|
|
255
|
+
message: webhookUrlMessage,
|
|
256
|
+
code: "INVALID_WEBHOOK_URL",
|
|
257
|
+
status: 0,
|
|
258
|
+
cause: error
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (parsed.host === "" || parsed.username !== "" || parsed.password !== "" || parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
262
|
+
throw new PayclaveError({
|
|
263
|
+
message: webhookUrlMessage,
|
|
264
|
+
code: "INVALID_WEBHOOK_URL",
|
|
265
|
+
status: 0
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return value;
|
|
269
|
+
};
|
|
270
|
+
var assertWebhookEventTypes = (eventTypes) => {
|
|
271
|
+
if (eventTypes === void 0) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (!Array.isArray(eventTypes)) {
|
|
275
|
+
throw new PayclaveError({
|
|
276
|
+
message: "Webhook event types must be an array.",
|
|
277
|
+
code: "WEBHOOK_EVENT_UNSUPPORTED",
|
|
278
|
+
status: 0
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
for (const eventType of eventTypes) {
|
|
282
|
+
if (!supportedWebhookEventTypes.has(eventType)) {
|
|
283
|
+
throw new PayclaveError({
|
|
284
|
+
message: "Webhook event type is not supported.",
|
|
285
|
+
code: "WEBHOOK_EVENT_UNSUPPORTED",
|
|
286
|
+
status: 0
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
var assertPositiveIntegerLimit = (limit) => {
|
|
292
|
+
if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
293
|
+
throw new PayclaveError({
|
|
294
|
+
message: "Limit must be an integer from 1 to 100.",
|
|
295
|
+
code: "INVALID_LIMIT",
|
|
296
|
+
status: 0
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
var serializeBody = (body) => {
|
|
301
|
+
try {
|
|
302
|
+
return JSON.stringify(body);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
throw new PayclaveError({
|
|
305
|
+
message: "Request body could not be serialized as JSON.",
|
|
306
|
+
code: "INVALID_REQUEST_BODY",
|
|
307
|
+
status: 0,
|
|
308
|
+
cause: error
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
var compact = (input) => {
|
|
313
|
+
const out = {};
|
|
314
|
+
for (const [key, value] of Object.entries(input)) {
|
|
315
|
+
if (value !== void 0) {
|
|
316
|
+
out[key] = value;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
};
|
|
321
|
+
var encodePathSegment = (value) => encodeURIComponent(value);
|
|
322
|
+
var isFailureResponse = (value) => {
|
|
323
|
+
if (!isRecord(value) || value.success !== false || !isRecord(value.error)) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
const meta = value.meta;
|
|
327
|
+
return typeof value.error.code === "string" && value.error.code.trim() !== "" && typeof value.error.message === "string" && value.error.message.trim() !== "" && (meta === void 0 || isRecord(meta) && (meta.requestId === void 0 || typeof meta.requestId === "string"));
|
|
328
|
+
};
|
|
329
|
+
var isSuccessResponse = (value, guard) => isRecord(value) && value.success === true && guard(value.data);
|
|
330
|
+
var parseResponse = async (response, guard) => {
|
|
331
|
+
const body = await response.json().catch(() => null);
|
|
332
|
+
const fallbackRequestId = response.headers.get("X-Request-ID");
|
|
333
|
+
if (isFailureResponse(body)) {
|
|
334
|
+
throw new PayclaveError({
|
|
335
|
+
message: body.error.message,
|
|
336
|
+
code: body.error.code,
|
|
337
|
+
status: response.status,
|
|
338
|
+
requestId: body.meta?.requestId ?? fallbackRequestId,
|
|
339
|
+
details: body.error.details
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (!response.ok || !isSuccessResponse(body, guard)) {
|
|
343
|
+
throw new PayclaveError({
|
|
344
|
+
message: "Payclave returned an invalid response.",
|
|
345
|
+
code: "INVALID_RESPONSE",
|
|
346
|
+
status: response.status,
|
|
347
|
+
requestId: fallbackRequestId
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return body.data;
|
|
351
|
+
};
|
|
352
|
+
var runFetch = async ({
|
|
353
|
+
fetcher,
|
|
354
|
+
url,
|
|
355
|
+
init,
|
|
356
|
+
externalSignal,
|
|
357
|
+
timeoutMs
|
|
358
|
+
}) => {
|
|
359
|
+
if (externalSignal?.aborted) {
|
|
360
|
+
throw new PayclaveError({
|
|
361
|
+
message: "Payclave request was aborted before it started.",
|
|
362
|
+
code: "ABORTED",
|
|
363
|
+
status: 0,
|
|
364
|
+
cause: externalSignal.reason
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
const controller = new AbortController();
|
|
368
|
+
const onExternalAbort = () => {
|
|
369
|
+
controller.abort(externalSignal?.reason);
|
|
370
|
+
};
|
|
371
|
+
if (externalSignal) {
|
|
372
|
+
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
373
|
+
}
|
|
374
|
+
let timeoutHandle;
|
|
375
|
+
let timedOut = false;
|
|
376
|
+
if (typeof timeoutMs === "number" && timeoutMs > 0) {
|
|
377
|
+
timeoutHandle = setTimeout(() => {
|
|
378
|
+
timedOut = true;
|
|
379
|
+
controller.abort(
|
|
380
|
+
new DOMException(
|
|
381
|
+
`Payclave request timed out after ${timeoutMs}ms.`,
|
|
382
|
+
"TimeoutError"
|
|
383
|
+
)
|
|
384
|
+
);
|
|
385
|
+
}, timeoutMs);
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
return await fetcher(url, { ...init, signal: controller.signal });
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (timedOut) {
|
|
391
|
+
throw new PayclaveError({
|
|
392
|
+
message: `Payclave request timed out after ${timeoutMs}ms.`,
|
|
393
|
+
code: "TIMEOUT",
|
|
394
|
+
status: 0,
|
|
395
|
+
cause: error
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (externalSignal?.aborted || controller.signal.aborted) {
|
|
399
|
+
throw new PayclaveError({
|
|
400
|
+
message: "Payclave request was aborted.",
|
|
401
|
+
code: "ABORTED",
|
|
402
|
+
status: 0,
|
|
403
|
+
cause: externalSignal?.reason ?? error
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
throw new PayclaveError({
|
|
407
|
+
message: error instanceof Error ? error.message : "Network request failed.",
|
|
408
|
+
code: "NETWORK_ERROR",
|
|
409
|
+
status: 0,
|
|
410
|
+
cause: error
|
|
411
|
+
});
|
|
412
|
+
} finally {
|
|
413
|
+
if (timeoutHandle !== void 0) {
|
|
414
|
+
clearTimeout(timeoutHandle);
|
|
415
|
+
}
|
|
416
|
+
if (externalSignal) {
|
|
417
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
var isCreatedCheckoutSession = (value) => {
|
|
422
|
+
if (!isRecord(value)) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
return typeof value.id === "string" && typeof value.checkoutId === "string" && value.object === "checkout_session" && typeof value.mode === "string" && value.status === "pending" && typeof value.invoiceId === "string" && typeof value.invoiceNumber === "string" && typeof value.amountDue === "string" && typeof value.currencyCode === "string" && typeof value.chainId === "number" && typeof value.tokenSymbol === "string" && typeof value.tokenContract === "string" && typeof value.settlementWalletAddress === "string" && isStringOrNull(value.externalReference) && isStringOrNull(value.customerEmail) && isMetadata(value.metadata) && typeof value.checkoutUrl === "string" && typeof value.expiresAt === "string" && typeof value.createdAt === "string";
|
|
426
|
+
};
|
|
427
|
+
var isCheckoutSession = (value) => {
|
|
428
|
+
if (!isRecord(value)) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
return typeof value.id === "string" && typeof value.checkoutId === "string" && value.object === "checkout_session" && typeof value.mode === "string" && typeof value.status === "string" && isStringOrNull(value.invoiceId) && isStringOrNull(value.invoiceNumber) && isStringOrNull(value.externalReference) && isStringOrNull(value.customerEmail) && isMetadata(value.metadata) && typeof value.checkoutUrl === "string" && typeof value.expiresAt === "string" && isStringOrNull(value.completedAt) && typeof value.createdAt === "string";
|
|
432
|
+
};
|
|
433
|
+
var isInvoice = (value) => {
|
|
434
|
+
if (!isRecord(value)) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
return typeof value.id === "string" && typeof value.invoiceNumber === "string" && value.object === "invoice" && typeof value.mode === "string" && typeof value.status === "string" && typeof value.amountDue === "string" && typeof value.amountReceived === "string" && typeof value.currencyCode === "string" && typeof value.chainId === "number" && typeof value.tokenContract === "string" && typeof value.settlementWalletAddress === "string" && isStringOrNull(value.externalReference) && isMetadata(value.metadata) && typeof value.expiresAt === "string" && isStringOrNull(value.paidAt) && isStringOrNull(value.cancelledAt) && typeof value.createdAt === "string";
|
|
438
|
+
};
|
|
439
|
+
var isPayment = (value) => {
|
|
440
|
+
if (!isRecord(value)) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
return typeof value.id === "string" && value.object === "payment" && typeof value.invoiceId === "string" && typeof value.invoiceNumber === "string" && isStringOrNull(value.transactionId) && typeof value.mode === "string" && typeof value.status === "string" && typeof value.amountExpected === "string" && typeof value.amountReceived === "string" && typeof value.chainId === "number" && typeof value.settlementWalletAddress === "string" && isStringOrNull(value.failureReason) && isStringOrNull(value.confirmedAt) && typeof value.createdAt === "string";
|
|
444
|
+
};
|
|
445
|
+
var isWebhookEndpoint = (value) => {
|
|
446
|
+
if (!isRecord(value)) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
return typeof value.id === "string" && value.object === "webhook_endpoint" && typeof value.mode === "string" && typeof value.url === "string" && typeof value.enabled === "boolean" && isStringArray(value.eventTypes) && typeof value.signingSecret === "string" && typeof value.createdAt === "string";
|
|
450
|
+
};
|
|
451
|
+
var isWebhookEvent = (value) => {
|
|
452
|
+
if (!isRecord(value)) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
return typeof value.id === "string" && value.object === "event" && typeof value.type === "string" && typeof value.mode === "string" && typeof value.createdAt === "string" && isRecord(value.data);
|
|
456
|
+
};
|
|
457
|
+
var isWebhookDeliveryAttemptSummary = (value) => {
|
|
458
|
+
if (!isRecord(value)) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
return (typeof value.responseStatusCode === "number" || value.responseStatusCode === null) && isStringOrNull(value.errorMessage) && typeof value.attemptedAt === "string";
|
|
462
|
+
};
|
|
463
|
+
var isWebhookDelivery = (value) => {
|
|
464
|
+
if (!isRecord(value)) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
return typeof value.id === "string" && value.object === "webhook_delivery" && typeof value.endpointId === "string" && isStringOrNull(value.invoiceId) && typeof value.eventType === "string" && typeof value.status === "string" && typeof value.idempotencyKey === "string" && isWebhookEvent(value.payload) && typeof value.nextAttemptAt === "string" && isStringOrNull(value.deliveredAt) && typeof value.attemptCount === "number" && (value.latestAttempt === null || isWebhookDeliveryAttemptSummary(value.latestAttempt)) && typeof value.createdAt === "string";
|
|
468
|
+
};
|
|
469
|
+
var isWebhookDeliveryArray = (value) => Array.isArray(value) && value.every(isWebhookDelivery);
|
|
470
|
+
var normalizePayloadBytes = (payload) => {
|
|
471
|
+
if (typeof payload === "string") {
|
|
472
|
+
return Buffer.from(payload, "utf8");
|
|
473
|
+
}
|
|
474
|
+
if (Buffer.isBuffer(payload)) {
|
|
475
|
+
return payload;
|
|
476
|
+
}
|
|
477
|
+
if (!(payload instanceof Uint8Array)) {
|
|
478
|
+
throw new PayclaveError({
|
|
479
|
+
message: "Payclave webhook payload must be a string, Buffer, or Uint8Array.",
|
|
480
|
+
code: "WEBHOOK_PAYLOAD_INVALID",
|
|
481
|
+
status: 0
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return Buffer.from(payload);
|
|
485
|
+
};
|
|
486
|
+
var currentUnixTimestamp = (now) => {
|
|
487
|
+
if (now instanceof Date) {
|
|
488
|
+
return Math.floor(now.getTime() / 1e3);
|
|
489
|
+
}
|
|
490
|
+
if (typeof now === "number") {
|
|
491
|
+
return Math.floor(now);
|
|
492
|
+
}
|
|
493
|
+
if (now !== void 0) {
|
|
494
|
+
return Number.NaN;
|
|
495
|
+
}
|
|
496
|
+
return Math.floor(Date.now() / 1e3);
|
|
497
|
+
};
|
|
498
|
+
var parseSignatureHeader = (signature) => {
|
|
499
|
+
if (typeof signature !== "string" || signature.trim() === "") {
|
|
500
|
+
throw new PayclaveError({
|
|
501
|
+
message: "Payclave webhook signature is missing.",
|
|
502
|
+
code: "WEBHOOK_SIGNATURE_INVALID",
|
|
503
|
+
status: 0
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
const parts = signature.split(",").map((part) => part.trim()).filter(Boolean);
|
|
507
|
+
let timestamp = null;
|
|
508
|
+
const signatures = [];
|
|
509
|
+
for (const part of parts) {
|
|
510
|
+
const separator = part.indexOf("=");
|
|
511
|
+
if (separator === -1) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const key = part.slice(0, separator);
|
|
515
|
+
const value = part.slice(separator + 1);
|
|
516
|
+
if (key === "t") {
|
|
517
|
+
timestamp = /^\d+$/.test(value) ? Number(value) : Number.NaN;
|
|
518
|
+
} else if (key === "v1" && value) {
|
|
519
|
+
signatures.push(value);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (!Number.isSafeInteger(timestamp) || timestamp === null) {
|
|
523
|
+
throw new PayclaveError({
|
|
524
|
+
message: "Payclave webhook timestamp is invalid.",
|
|
525
|
+
code: "WEBHOOK_TIMESTAMP_INVALID",
|
|
526
|
+
status: 0
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (signatures.length === 0) {
|
|
530
|
+
throw new PayclaveError({
|
|
531
|
+
message: "Payclave webhook signature is missing.",
|
|
532
|
+
code: "WEBHOOK_SIGNATURE_INVALID",
|
|
533
|
+
status: 0
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
return { timestamp, signatures };
|
|
537
|
+
};
|
|
538
|
+
var expectedWebhookSignature = (payload, secret, timestamp) => {
|
|
539
|
+
const mac = createHmac("sha256", secret);
|
|
540
|
+
mac.update(String(timestamp));
|
|
541
|
+
mac.update(".");
|
|
542
|
+
mac.update(normalizePayloadBytes(payload));
|
|
543
|
+
return mac.digest("hex");
|
|
544
|
+
};
|
|
545
|
+
var timingSafeHexEqual = (actual, expected) => {
|
|
546
|
+
if (actual.length !== expected.length || !/^[a-fA-F0-9]+$/.test(actual)) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
const actualBytes = Buffer.from(actual, "hex");
|
|
550
|
+
const expectedBytes = Buffer.from(expected, "hex");
|
|
551
|
+
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
|
|
552
|
+
};
|
|
553
|
+
function assertPayclaveWebhookSignature({
|
|
554
|
+
payload,
|
|
555
|
+
signature,
|
|
556
|
+
secret,
|
|
557
|
+
toleranceSeconds = defaultWebhookToleranceSeconds,
|
|
558
|
+
now
|
|
559
|
+
}) {
|
|
560
|
+
if (!Number.isFinite(toleranceSeconds) || toleranceSeconds < 0) {
|
|
561
|
+
throw new PayclaveError({
|
|
562
|
+
message: "Payclave webhook tolerance must be a non-negative number.",
|
|
563
|
+
code: "WEBHOOK_TOLERANCE_INVALID",
|
|
564
|
+
status: 0
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
if (typeof secret !== "string") {
|
|
568
|
+
throw new PayclaveError({
|
|
569
|
+
message: "Payclave webhook secret is required.",
|
|
570
|
+
code: "WEBHOOK_SIGNATURE_INVALID",
|
|
571
|
+
status: 0
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
const trimmedSecret = secret.trim();
|
|
575
|
+
if (!trimmedSecret) {
|
|
576
|
+
throw new PayclaveError({
|
|
577
|
+
message: "Payclave webhook secret is required.",
|
|
578
|
+
code: "WEBHOOK_SIGNATURE_INVALID",
|
|
579
|
+
status: 0
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
const parsed = parseSignatureHeader(signature);
|
|
583
|
+
const nowTimestamp = currentUnixTimestamp(now);
|
|
584
|
+
if (!Number.isSafeInteger(nowTimestamp)) {
|
|
585
|
+
throw new PayclaveError({
|
|
586
|
+
message: "Payclave webhook current timestamp is invalid.",
|
|
587
|
+
code: "WEBHOOK_TIMESTAMP_INVALID",
|
|
588
|
+
status: 0
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (toleranceSeconds > 0 && Math.abs(nowTimestamp - parsed.timestamp) > toleranceSeconds) {
|
|
592
|
+
throw new PayclaveError({
|
|
593
|
+
message: "Payclave webhook timestamp is outside the allowed tolerance.",
|
|
594
|
+
code: "WEBHOOK_TIMESTAMP_OUTSIDE_TOLERANCE",
|
|
595
|
+
status: 0
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
const expected = expectedWebhookSignature(payload, trimmedSecret, parsed.timestamp);
|
|
599
|
+
if (!parsed.signatures.some(
|
|
600
|
+
(candidate) => timingSafeHexEqual(candidate, expected)
|
|
601
|
+
)) {
|
|
602
|
+
throw new PayclaveError({
|
|
603
|
+
message: "Payclave webhook signature is invalid.",
|
|
604
|
+
code: "WEBHOOK_SIGNATURE_INVALID",
|
|
605
|
+
status: 0
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function verifyPayclaveWebhookSignature(input) {
|
|
610
|
+
try {
|
|
611
|
+
assertPayclaveWebhookSignature(input);
|
|
612
|
+
return true;
|
|
613
|
+
} catch {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function constructPayclaveWebhookEvent(input) {
|
|
618
|
+
assertPayclaveWebhookSignature(input);
|
|
619
|
+
let parsed;
|
|
620
|
+
try {
|
|
621
|
+
parsed = JSON.parse(normalizePayloadBytes(input.payload).toString("utf8"));
|
|
622
|
+
} catch (error) {
|
|
623
|
+
throw new PayclaveError({
|
|
624
|
+
message: "Payclave webhook payload is not valid JSON.",
|
|
625
|
+
code: "WEBHOOK_PAYLOAD_INVALID",
|
|
626
|
+
status: 0,
|
|
627
|
+
cause: error
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (!isWebhookEvent(parsed)) {
|
|
631
|
+
throw new PayclaveError({
|
|
632
|
+
message: "Payclave webhook payload is not a valid event.",
|
|
633
|
+
code: "WEBHOOK_PAYLOAD_INVALID",
|
|
634
|
+
status: 0
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
return parsed;
|
|
638
|
+
}
|
|
639
|
+
function createPayclaveServerClient({
|
|
640
|
+
secretKey,
|
|
641
|
+
apiBaseUrl = defaultApiBaseUrl,
|
|
642
|
+
fetcher = defaultFetcher,
|
|
643
|
+
timeoutMs
|
|
644
|
+
}) {
|
|
645
|
+
assertSecretKey(secretKey);
|
|
646
|
+
assertTimeoutMs(timeoutMs);
|
|
647
|
+
const normalizedApiBaseUrl = normalizeApiBaseUrl(apiBaseUrl);
|
|
648
|
+
const request = async ({
|
|
649
|
+
method,
|
|
650
|
+
path,
|
|
651
|
+
body,
|
|
652
|
+
idempotencyKey,
|
|
653
|
+
signal,
|
|
654
|
+
guard
|
|
655
|
+
}) => {
|
|
656
|
+
const headers = {
|
|
657
|
+
Accept: "application/json",
|
|
658
|
+
Authorization: `Bearer ${secretKey}`,
|
|
659
|
+
"X-Payclave-Client": buildClientHeader()
|
|
660
|
+
};
|
|
661
|
+
if (body !== void 0) {
|
|
662
|
+
headers["Content-Type"] = "application/json";
|
|
663
|
+
}
|
|
664
|
+
if (idempotencyKey !== void 0) {
|
|
665
|
+
assertIdempotencyKey(idempotencyKey);
|
|
666
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
667
|
+
}
|
|
668
|
+
const response = await runFetch({
|
|
669
|
+
fetcher,
|
|
670
|
+
url: joinUrl(normalizedApiBaseUrl, path),
|
|
671
|
+
init: {
|
|
672
|
+
method,
|
|
673
|
+
headers,
|
|
674
|
+
body: body === void 0 ? void 0 : serializeBody(body)
|
|
675
|
+
},
|
|
676
|
+
externalSignal: signal,
|
|
677
|
+
timeoutMs
|
|
678
|
+
});
|
|
679
|
+
return parseResponse(response, guard);
|
|
680
|
+
};
|
|
681
|
+
const createCheckoutSession = (input) => {
|
|
682
|
+
assertAmount(input.amount);
|
|
683
|
+
assertOptionalExpiry(input.expiresInMinutes);
|
|
684
|
+
assertOptionalMetadata(input.metadata);
|
|
685
|
+
return request({
|
|
686
|
+
method: "POST",
|
|
687
|
+
path: "/v1/checkout-sessions",
|
|
688
|
+
body: compact({
|
|
689
|
+
amount: input.amount,
|
|
690
|
+
customerEmail: input.customerEmail,
|
|
691
|
+
externalReference: input.externalReference,
|
|
692
|
+
successUrl: input.successUrl,
|
|
693
|
+
cancelUrl: input.cancelUrl,
|
|
694
|
+
metadata: input.metadata,
|
|
695
|
+
expiresInMinutes: input.expiresInMinutes
|
|
696
|
+
}),
|
|
697
|
+
idempotencyKey: input.idempotencyKey,
|
|
698
|
+
signal: input.signal,
|
|
699
|
+
guard: isCreatedCheckoutSession
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
const getCheckoutSession = (id, options = {}) => {
|
|
703
|
+
assertIdentifier(id, "Checkout session id");
|
|
704
|
+
return request({
|
|
705
|
+
method: "GET",
|
|
706
|
+
path: `/v1/checkout-sessions/${encodePathSegment(id)}`,
|
|
707
|
+
signal: options.signal,
|
|
708
|
+
guard: isCheckoutSession
|
|
709
|
+
});
|
|
710
|
+
};
|
|
711
|
+
const createInvoice = (input) => {
|
|
712
|
+
assertAmount(input.amount);
|
|
713
|
+
assertOptionalExpiry(input.expiresInMinutes);
|
|
714
|
+
assertOptionalMetadata(input.metadata);
|
|
715
|
+
return request({
|
|
716
|
+
method: "POST",
|
|
717
|
+
path: "/v1/invoices",
|
|
718
|
+
body: compact({
|
|
719
|
+
amount: input.amount,
|
|
720
|
+
externalReference: input.externalReference,
|
|
721
|
+
metadata: input.metadata,
|
|
722
|
+
expiresInMinutes: input.expiresInMinutes
|
|
723
|
+
}),
|
|
724
|
+
idempotencyKey: input.idempotencyKey,
|
|
725
|
+
signal: input.signal,
|
|
726
|
+
guard: isInvoice
|
|
727
|
+
});
|
|
728
|
+
};
|
|
729
|
+
const getInvoice = (id, options = {}) => {
|
|
730
|
+
assertIdentifier(id, "Invoice id");
|
|
731
|
+
return request({
|
|
732
|
+
method: "GET",
|
|
733
|
+
path: `/v1/invoices/${encodePathSegment(id)}`,
|
|
734
|
+
signal: options.signal,
|
|
735
|
+
guard: isInvoice
|
|
736
|
+
});
|
|
737
|
+
};
|
|
738
|
+
const getPayment = (id, options = {}) => {
|
|
739
|
+
assertIdentifier(id, "Payment id");
|
|
740
|
+
return request({
|
|
741
|
+
method: "GET",
|
|
742
|
+
path: `/v1/payments/${encodePathSegment(id)}`,
|
|
743
|
+
signal: options.signal,
|
|
744
|
+
guard: isPayment
|
|
745
|
+
});
|
|
746
|
+
};
|
|
747
|
+
const createWebhookEndpoint = (input) => {
|
|
748
|
+
const url = normalizeWebhookUrl(input.url);
|
|
749
|
+
assertWebhookEventTypes(input.eventTypes);
|
|
750
|
+
return request({
|
|
751
|
+
method: "POST",
|
|
752
|
+
path: "/v1/webhook-endpoints",
|
|
753
|
+
body: compact({
|
|
754
|
+
url,
|
|
755
|
+
eventTypes: input.eventTypes
|
|
756
|
+
}),
|
|
757
|
+
signal: input.signal,
|
|
758
|
+
guard: isWebhookEndpoint
|
|
759
|
+
});
|
|
760
|
+
};
|
|
761
|
+
const createTestWebhook = (input) => {
|
|
762
|
+
assertIdentifier(input.endpointId, "Webhook endpoint id");
|
|
763
|
+
assertWebhookEventTypes(
|
|
764
|
+
input.eventType === void 0 ? void 0 : [input.eventType]
|
|
765
|
+
);
|
|
766
|
+
return request({
|
|
767
|
+
method: "POST",
|
|
768
|
+
path: "/v1/webhooks/test",
|
|
769
|
+
body: compact({
|
|
770
|
+
endpointId: input.endpointId,
|
|
771
|
+
eventType: input.eventType
|
|
772
|
+
}),
|
|
773
|
+
signal: input.signal,
|
|
774
|
+
guard: isWebhookDelivery
|
|
775
|
+
});
|
|
776
|
+
};
|
|
777
|
+
const listWebhookDeliveries = (input = {}) => {
|
|
778
|
+
const limit = input.limit;
|
|
779
|
+
if (limit !== void 0) {
|
|
780
|
+
assertPositiveIntegerLimit(limit);
|
|
781
|
+
}
|
|
782
|
+
const query = limit !== void 0 ? `?limit=${encodeURIComponent(String(limit))}` : "";
|
|
783
|
+
return request({
|
|
784
|
+
method: "GET",
|
|
785
|
+
path: `/v1/webhook-deliveries${query}`,
|
|
786
|
+
signal: input.signal,
|
|
787
|
+
guard: isWebhookDeliveryArray
|
|
788
|
+
});
|
|
789
|
+
};
|
|
790
|
+
return {
|
|
791
|
+
createCheckoutSession,
|
|
792
|
+
getCheckoutSession,
|
|
793
|
+
createInvoice,
|
|
794
|
+
getInvoice,
|
|
795
|
+
getPayment,
|
|
796
|
+
createWebhookEndpoint,
|
|
797
|
+
createTestWebhook,
|
|
798
|
+
listWebhookDeliveries
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export { PAYCLAVE_API_ERROR_CODES, PAYCLAVE_SERVER_SDK_ERROR_CODES, PAYCLAVE_SERVER_SDK_NAME, PAYCLAVE_SERVER_SDK_VERSION, PAYCLAVE_WEBHOOK_EVENT_TYPES, PAYCLAVE_WEBHOOK_VERIFICATION_ERROR_CODES, PayclaveError, assertPayclaveWebhookSignature, constructPayclaveWebhookEvent, createPayclaveServerClient, verifyPayclaveWebhookSignature };
|