@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.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 };