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