@bitpal/checkout 0.7.0 → 0.9.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 CHANGED
@@ -37,13 +37,23 @@ __export(index_exports, {
37
37
  PaymentLinksResource: () => PaymentLinksResource,
38
38
  PricesResource: () => PricesResource,
39
39
  ProductsResource: () => ProductsResource,
40
+ TRANSFER_WITH_AUTHORIZATION_TYPES: () => TRANSFER_WITH_AUTHORIZATION_TYPES,
40
41
  WEBHOOK_HEADERS: () => WEBHOOK_HEADERS,
42
+ X402Gate: () => X402Gate,
43
+ X402_PAYMENT_HEADER: () => X402_PAYMENT_HEADER,
44
+ X402_PAYMENT_RESPONSE_HEADER: () => X402_PAYMENT_RESPONSE_HEADER,
45
+ createMemoryReplayStore: () => createMemoryReplayStore,
46
+ createX402Payment: () => createX402Payment,
47
+ decodePaymentHeader: () => decodePaymentHeader,
48
+ encodePaymentHeader: () => encodePaymentHeader,
41
49
  fromAtomic: () => fromAtomic,
42
50
  fromAtomicUSDC: () => fromAtomicUSDC,
43
51
  parseWebhookEvent: () => parseWebhookEvent,
52
+ splitSignature: () => splitSignature,
44
53
  toAtomic: () => toAtomic,
45
54
  toAtomicUSDC: () => toAtomicUSDC,
46
- verifyWebhookSignature: () => verifyWebhookSignature
55
+ verifyWebhookSignature: () => verifyWebhookSignature,
56
+ x402: () => x402
47
57
  });
48
58
  module.exports = __toCommonJS(index_exports);
49
59
 
@@ -305,12 +315,23 @@ var BitPalCheckoutClient = class {
305
315
  );
306
316
  }
307
317
  /* ─── Fees ─── */
308
- /** 수수료 미리보기 — `/v1/checkout/fee-preview` (Phase 1 credit system 폐기 후 canonical path) */
318
+ /**
319
+ * 수수료 미리보기 — `/v1/checkout/fee-preview`.
320
+ *
321
+ * `amount` 는 **사람 표기**다("100", "0.5") — line item / Price / x402 와 같은 모델이라
322
+ * decimals 를 계산할 필요가 없다. raw μUSD 를 직접 넣으려면 `amountAtomic` 을 쓴다(상호 배타).
323
+ * 응답은 계속 atomic 이다(계산 결과라 정밀도가 곧 계약이다).
324
+ *
325
+ * @example
326
+ * const { data } = await bp.checkout.previewFee('100'); // $100
327
+ * const { data } = await bp.checkout.previewFee({ amountAtomic: '100000000' });
328
+ */
309
329
  async previewFee(amount) {
310
- return this.request(
311
- "GET",
312
- `/v1/checkout/fee-preview?amount=${encodeURIComponent(amount)}`
313
- );
330
+ const qs = typeof amount === "string" ? `amount=${encodeURIComponent(amount)}` : `amount_atomic=${encodeURIComponent(amount.amountAtomic)}`;
331
+ if (typeof amount === "string" && !/^(0|[1-9]\d*)(\.\d{1,6})?$/.test(amount)) {
332
+ throw new Error('[BitPal] previewFee: amount must be a decimal string like "100" or "0.5" (max 6 decimals). For raw \u03BCUSD use { amountAtomic }.');
333
+ }
334
+ return this.request("GET", `/v1/checkout/fee-preview?${qs}`);
314
335
  }
315
336
  };
316
337
  var BitPalError = class extends Error {
@@ -408,6 +429,549 @@ function fromAtomicUSDC(atomic) {
408
429
  return fromAtomic(atomic, 6);
409
430
  }
410
431
 
432
+ // src/x402.ts
433
+ var X402_PAYMENT_HEADER = "x-payment";
434
+ var X402_PAYMENT_RESPONSE_HEADER = "X-PAYMENT-RESPONSE";
435
+ var RESIGN_REQUIRED_STATUSES = /* @__PURE__ */ new Set(["reverted", "expired", "failed"]);
436
+ var DEFAULT_BASE_URL2 = "https://api.bitpal.io";
437
+ var DEFAULT_TIMEOUT2 = 6e4;
438
+ var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
439
+ var MIN_VALIDITY_SECONDS = 60;
440
+ var MAX_VALIDITY_SECONDS = 3600;
441
+ var CONSUMED_TTL_SECONDS = 24 * 60 * 60;
442
+ var MEMORY_STORE_MAX_ENTRIES = 5e4;
443
+ var MAX_PAYMENT_HEADER_BYTES = 8192;
444
+ function createMemoryReplayStore() {
445
+ const entries = /* @__PURE__ */ new Map();
446
+ const prune = (now) => {
447
+ for (const [k, v] of entries) {
448
+ if (v.expiresAt <= now) entries.delete(k);
449
+ }
450
+ if (entries.size <= MEMORY_STORE_MAX_ENTRIES) return;
451
+ for (const [k, v] of entries) {
452
+ if (entries.size <= MEMORY_STORE_MAX_ENTRIES) break;
453
+ if (v.state === "inflight") entries.delete(k);
454
+ }
455
+ };
456
+ return {
457
+ claim(key, inflightTtlSeconds) {
458
+ const now = Date.now();
459
+ prune(now);
460
+ const found = entries.get(key);
461
+ if (found) return found.state;
462
+ if (entries.size >= MEMORY_STORE_MAX_ENTRIES) {
463
+ throw new BitPalError(
464
+ "[BitPal] x402: replay store is full. Pass a shared replayStore (Redis/DB) for this traffic level.",
465
+ 503
466
+ );
467
+ }
468
+ entries.set(key, { state: "inflight", expiresAt: now + inflightTtlSeconds * 1e3 });
469
+ return "ok";
470
+ },
471
+ commit(key) {
472
+ entries.set(key, { state: "used", expiresAt: Date.now() + CONSUMED_TTL_SECONDS * 1e3 });
473
+ },
474
+ release(key) {
475
+ entries.delete(key);
476
+ }
477
+ };
478
+ }
479
+ var X402Gate = class {
480
+ opts;
481
+ baseUrl;
482
+ timeout;
483
+ store;
484
+ maxTimeoutSeconds;
485
+ constructor(opts) {
486
+ if (!opts.apiKey) throw new Error("[BitPal] x402: apiKey is required.");
487
+ if (!/^0x[a-fA-F0-9]{40}$/.test(opts.payTo)) {
488
+ throw new Error("[BitPal] x402: payTo must be a 0x EVM address (your registered payout wallet).");
489
+ }
490
+ const hasHuman = opts.amount != null;
491
+ const hasAtomic = opts.amountAtomic != null;
492
+ if (hasHuman === hasAtomic) {
493
+ throw new Error('[BitPal] x402: provide exactly one of `amount` ("0.1") or `amountAtomic` ("100000").');
494
+ }
495
+ if (hasHuman && !/^(0|[1-9]\d*)(\.\d{1,6})?$/.test(opts.amount)) {
496
+ throw new Error('[BitPal] x402: amount must be a decimal string like "0.1" or "29" (max 6 decimals).');
497
+ }
498
+ if (hasAtomic && !/^\d+$/.test(opts.amountAtomic)) {
499
+ throw new Error('[BitPal] x402: amountAtomic must be an integer string (USDC 6dp, $1 = "1000000").');
500
+ }
501
+ const timeoutSeconds = opts.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
502
+ if (timeoutSeconds < MIN_VALIDITY_SECONDS || timeoutSeconds > MAX_VALIDITY_SECONDS) {
503
+ throw new Error(
504
+ `[BitPal] x402: maxTimeoutSeconds must be between ${MIN_VALIDITY_SECONDS} and ${MAX_VALIDITY_SECONDS} (got ${timeoutSeconds}).`
505
+ );
506
+ }
507
+ this.opts = opts;
508
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/$/, "");
509
+ this.timeout = opts.timeout ?? DEFAULT_TIMEOUT2;
510
+ this.store = opts.replayStore ?? createMemoryReplayStore();
511
+ this.maxTimeoutSeconds = opts.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
512
+ }
513
+ /** 이 리소스의 402 바디를 서버에서 받아온다(수수료 분해 포함). */
514
+ async requirements(resource) {
515
+ return this.request("POST", "/v1/x402/requirements", {
516
+ payTo: this.opts.payTo,
517
+ // 사람 표기 → atomic 변환은 **서버가** 한다. SDK 가 decimals 를 알 필요가 없다.
518
+ ...this.opts.amount != null ? { amount: this.opts.amount } : { amountAtomic: this.opts.amountAtomic },
519
+ resource,
520
+ ...this.opts.network ? { network: this.opts.network } : {},
521
+ ...this.opts.asset ? { asset: this.opts.asset } : {},
522
+ ...this.opts.description ? { description: this.opts.description } : {},
523
+ ...this.opts.mimeType ? { mimeType: this.opts.mimeType } : {},
524
+ maxTimeoutSeconds: this.maxTimeoutSeconds
525
+ });
526
+ }
527
+ /**
528
+ * `X-PAYMENT` 헤더 1건을 처리한다. 헤더가 없거나 깨졌으면 402, 정산까지 끝나면 `settled`.
529
+ *
530
+ * 순서가 중요하다: **점유 → verify → settle → 소진 기록**. 점유를 verify 앞에 두어야 같은
531
+ * authorization 을 동시에 밀어 넣어도 한 요청만 리소스를 받는다.
532
+ */
533
+ async collect(input) {
534
+ const { header, resource } = input;
535
+ if (!header) return this.paymentRequired(resource);
536
+ let payload;
537
+ try {
538
+ payload = decodePaymentHeader(header);
539
+ } catch {
540
+ return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
541
+ }
542
+ const merchant = payload?.payload?.merchant;
543
+ if (!merchant?.from || !merchant?.nonce) {
544
+ return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
545
+ }
546
+ const key = `${payload.network}:${merchant.from.toLowerCase()}:${merchant.nonce.toLowerCase()}`;
547
+ const claim = await this.store.claim(key, this.maxTimeoutSeconds);
548
+ if (claim === "used") {
549
+ return {
550
+ kind: "already_used",
551
+ status: 409,
552
+ body: {
553
+ error: "PAYMENT_ALREADY_USED",
554
+ message: "This payment has already been redeemed. One payment unlocks one response."
555
+ }
556
+ };
557
+ }
558
+ if (claim === "inflight") {
559
+ return {
560
+ kind: "unconfirmed",
561
+ status: 503,
562
+ body: {
563
+ error: "PAYMENT_IN_PROGRESS",
564
+ message: "This payment is still being settled. Retry with the same X-PAYMENT header."
565
+ }
566
+ };
567
+ }
568
+ let accept;
569
+ try {
570
+ accept = await this.firstAccept(resource);
571
+ } catch (err) {
572
+ await this.store.release(key);
573
+ throw err;
574
+ }
575
+ const body = {
576
+ x402Version: 1,
577
+ paymentPayload: {
578
+ scheme: "exact",
579
+ network: payload.network,
580
+ payload: payload.payload
581
+ },
582
+ paymentRequirements: {
583
+ asset: accept.asset,
584
+ payTo: accept.payTo,
585
+ maxAmountRequired: accept.maxAmountRequired,
586
+ resource: accept.resource
587
+ }
588
+ };
589
+ try {
590
+ await this.request("POST", "/v1/x402/verify", body);
591
+ } catch (err) {
592
+ await this.store.release(key);
593
+ if (err instanceof BitPalError && err.statusCode < 500) {
594
+ return this.paymentRequired(resource, err.message, accept);
595
+ }
596
+ throw err;
597
+ }
598
+ let settled;
599
+ try {
600
+ settled = await this.request("POST", "/v1/x402/settle", body);
601
+ } catch (err) {
602
+ if (err instanceof BitPalError && err.statusCode < 500) {
603
+ await this.store.release(key);
604
+ return this.paymentRequired(resource, err.message, accept);
605
+ }
606
+ await this.store.release(key);
607
+ return {
608
+ kind: "unconfirmed",
609
+ status: 503,
610
+ body: {
611
+ error: "SETTLEMENT_UNCONFIRMED",
612
+ message: "Payment settlement is not confirmed yet. Retry this request with the same X-PAYMENT header \u2014 settlement is idempotent."
613
+ }
614
+ };
615
+ }
616
+ if (settled.status !== "confirmed") {
617
+ await this.store.release(key);
618
+ if (RESIGN_REQUIRED_STATUSES.has(settled.status)) {
619
+ return this.paymentRequired(resource, `PAYMENT_${settled.status.toUpperCase()}`, accept);
620
+ }
621
+ return {
622
+ kind: "unconfirmed",
623
+ status: 503,
624
+ body: {
625
+ error: "SETTLEMENT_UNCONFIRMED",
626
+ message: `Payment is still settling (${settled.status}). Retry with the same X-PAYMENT header \u2014 do not create a new payment.`,
627
+ paymentId: settled.payment_id
628
+ }
629
+ };
630
+ }
631
+ const payment = {
632
+ paymentId: settled.payment_id,
633
+ txHash: settled.tx_hash,
634
+ feeTxHash: settled.fee_tx_hash ?? null,
635
+ network: settled.network,
636
+ payer: merchant.from,
637
+ amount: accept.maxAmountRequired,
638
+ idempotent: settled.idempotent ?? false
639
+ };
640
+ return {
641
+ kind: "settled",
642
+ status: 200,
643
+ payment,
644
+ paymentResponseHeader: encodeBase64Json({
645
+ success: true,
646
+ transaction: settled.tx_hash,
647
+ network: settled.network,
648
+ payer: merchant.from,
649
+ paymentId: settled.payment_id
650
+ }),
651
+ // 소진 확정은 **응답을 실제로 내준 뒤**다. 핸들러가 throw 하거나 프로세스가 죽으면 결제만 하고
652
+ // 리소스를 못 받는데, 그때 commit 되어 있으면 재시도가 409 로 막힌다.
653
+ commit: () => Promise.resolve(this.store.commit(key)),
654
+ release: () => Promise.resolve(this.store.release(key))
655
+ };
656
+ }
657
+ /* ─── 내부 ─── */
658
+ async firstAccept(resource) {
659
+ const requirements = await this.requirements(resource);
660
+ const accept = requirements.accepts[0];
661
+ if (!accept) {
662
+ throw new BitPalError("[BitPal] x402: no payment option available for this API key mode.", 500);
663
+ }
664
+ return accept;
665
+ }
666
+ async paymentRequired(resource, error, known) {
667
+ const requirements = known ? { x402Version: 1, accepts: [known] } : await this.requirements(resource);
668
+ return {
669
+ kind: "payment_required",
670
+ status: 402,
671
+ body: error ? { ...requirements, error } : requirements
672
+ };
673
+ }
674
+ async request(method, path, body) {
675
+ const controller = new AbortController();
676
+ const timer = setTimeout(() => controller.abort(), this.timeout);
677
+ try {
678
+ const res = await fetch(`${this.baseUrl}${path}`, {
679
+ method,
680
+ headers: {
681
+ "Content-Type": "application/json",
682
+ Authorization: `Bearer ${this.opts.apiKey}`
683
+ },
684
+ body: body ? JSON.stringify(body) : void 0,
685
+ signal: controller.signal
686
+ });
687
+ const json = await res.json();
688
+ if (!res.ok) {
689
+ throw new BitPalError(String(json.error ?? `HTTP ${res.status}`), res.status);
690
+ }
691
+ return json.data;
692
+ } catch (err) {
693
+ if (err instanceof BitPalError) throw err;
694
+ throw new BitPalError(err instanceof Error ? err.message : String(err), 599);
695
+ } finally {
696
+ clearTimeout(timer);
697
+ }
698
+ }
699
+ };
700
+ function x402(options) {
701
+ const gate = new X402Gate(options);
702
+ return async function x402Middleware(req, res, next) {
703
+ try {
704
+ const result = await gate.collect({
705
+ header: readHeader(req, X402_PAYMENT_HEADER),
706
+ resource: resolveResource(req, options.resource)
707
+ });
708
+ if (result.kind !== "settled") {
709
+ res.status(result.status).json(result.body);
710
+ return;
711
+ }
712
+ res.setHeader(X402_PAYMENT_RESPONSE_HEADER, result.paymentResponseHeader);
713
+ req.x402Payment = result.payment;
714
+ if (typeof res.once === "function") {
715
+ let finalized = false;
716
+ const finalize = (delivered) => {
717
+ if (finalized) return;
718
+ finalized = true;
719
+ const done = delivered ? result.commit() : result.release();
720
+ void done.catch(() => void 0);
721
+ };
722
+ res.once("finish", () => finalize((res.statusCode ?? 200) < 500));
723
+ res.once("close", () => finalize(res.writableEnded === true && (res.statusCode ?? 200) < 500));
724
+ } else {
725
+ await result.commit();
726
+ }
727
+ if (options.onSettled) await options.onSettled(result.payment, req);
728
+ next();
729
+ } catch (err) {
730
+ next(err);
731
+ }
732
+ };
733
+ }
734
+ function readHeader(req, name) {
735
+ const direct = req.headers?.[name] ?? req.headers?.[name.toUpperCase()];
736
+ const value = Array.isArray(direct) ? direct[0] : direct;
737
+ if (value) return value;
738
+ return req.get?.(name);
739
+ }
740
+ function resolveResource(req, override) {
741
+ if (typeof override === "function") return override(req);
742
+ if (override) return override;
743
+ const host = req.get?.("host") ?? readHeader(req, "host") ?? "localhost";
744
+ const protocol = req.protocol ?? "https";
745
+ const raw = req.path != null ? `${req.baseUrl ?? ""}${req.path}` : req.originalUrl ?? req.url ?? "/";
746
+ const path = raw.split("?")[0] || "/";
747
+ return `${protocol}://${host}${path}`;
748
+ }
749
+ function decodePaymentHeader(header) {
750
+ if (header.length > MAX_PAYMENT_HEADER_BYTES) {
751
+ throw new Error("[BitPal] x402: X-PAYMENT header too large.");
752
+ }
753
+ const trimmed = header.trim();
754
+ const json = trimmed.startsWith("{") ? trimmed : decodeBase64(trimmed);
755
+ const parsed = JSON.parse(json);
756
+ return assertPaymentPayload(parsed);
757
+ }
758
+ var ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
759
+ var HEX32_RE = /^0x[a-fA-F0-9]{64}$/;
760
+ var DECIMAL_RE = /^\d{1,78}$/;
761
+ function assertPaymentPayload(value) {
762
+ const root = asObject(value, "payment payload");
763
+ if (root.x402Version !== 1) throw new Error("[BitPal] x402: unsupported x402Version.");
764
+ if (root.scheme !== "exact") throw new Error("[BitPal] x402: unsupported scheme.");
765
+ if (typeof root.network !== "string" || root.network.length === 0 || root.network.length > 64) {
766
+ throw new Error("[BitPal] x402: invalid network.");
767
+ }
768
+ const inner = asObject(root.payload, "payload");
769
+ const merchant = assertAuthorization(inner.merchant, "merchant");
770
+ const fee = inner.fee === void 0 ? void 0 : assertAuthorization(inner.fee, "fee");
771
+ return {
772
+ x402Version: 1,
773
+ scheme: "exact",
774
+ network: root.network,
775
+ payload: fee ? { merchant, fee } : { merchant }
776
+ };
777
+ }
778
+ function assertAuthorization(value, label) {
779
+ const a = asObject(value, `${label} authorization`);
780
+ const sig = asObject(a.signature, `${label} signature`);
781
+ const check = (ok, field) => {
782
+ if (!ok) throw new Error(`[BitPal] x402: invalid ${label}.${field}.`);
783
+ };
784
+ check(typeof a.from === "string" && ADDRESS_RE.test(a.from), "from");
785
+ check(typeof a.to === "string" && ADDRESS_RE.test(a.to), "to");
786
+ check(typeof a.value === "string" && DECIMAL_RE.test(a.value), "value");
787
+ check(typeof a.validAfter === "string" && DECIMAL_RE.test(a.validAfter), "validAfter");
788
+ check(typeof a.validBefore === "string" && DECIMAL_RE.test(a.validBefore), "validBefore");
789
+ check(typeof a.nonce === "string" && HEX32_RE.test(a.nonce), "nonce");
790
+ check(typeof sig.v === "number" && Number.isInteger(sig.v), "signature.v");
791
+ check(typeof sig.r === "string" && HEX32_RE.test(sig.r), "signature.r");
792
+ check(typeof sig.s === "string" && HEX32_RE.test(sig.s), "signature.s");
793
+ return {
794
+ from: a.from,
795
+ to: a.to,
796
+ value: a.value,
797
+ validAfter: a.validAfter,
798
+ validBefore: a.validBefore,
799
+ nonce: a.nonce,
800
+ signature: { v: sig.v, r: sig.r, s: sig.s }
801
+ };
802
+ }
803
+ function asObject(value, label) {
804
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
805
+ throw new Error(`[BitPal] x402: ${label} must be an object.`);
806
+ }
807
+ return value;
808
+ }
809
+ function encodePaymentHeader(payload) {
810
+ return encodeBase64Json(payload);
811
+ }
812
+ function encodeBase64Json(value) {
813
+ const json = JSON.stringify(value);
814
+ const g = globalThis;
815
+ if (typeof g.btoa === "function") {
816
+ const bytes = new TextEncoder().encode(json);
817
+ let binary = "";
818
+ for (const b of bytes) binary += String.fromCharCode(b);
819
+ return g.btoa(binary);
820
+ }
821
+ return bufferFrom(json, "utf8").toString("base64");
822
+ }
823
+ function decodeBase64(value) {
824
+ const g = globalThis;
825
+ if (typeof g.atob === "function") {
826
+ const binary = g.atob(value);
827
+ const bytes = new Uint8Array(binary.length);
828
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
829
+ return new TextDecoder().decode(bytes);
830
+ }
831
+ return bufferFrom(value, "base64").toString("utf8");
832
+ }
833
+ function bufferFrom(value, encoding) {
834
+ const g = globalThis;
835
+ if (!g.Buffer) throw new Error("[BitPal] x402: no base64 implementation available in this runtime.");
836
+ return g.Buffer.from(value, encoding);
837
+ }
838
+
839
+ // src/x402-pay.ts
840
+ var TRANSFER_WITH_AUTHORIZATION_TYPES = {
841
+ TransferWithAuthorization: [
842
+ { name: "from", type: "address" },
843
+ { name: "to", type: "address" },
844
+ { name: "value", type: "uint256" },
845
+ { name: "validAfter", type: "uint256" },
846
+ { name: "validBefore", type: "uint256" },
847
+ { name: "nonce", type: "bytes32" }
848
+ ]
849
+ };
850
+ var CLOCK_SKEW_MARGIN_SECONDS = 30;
851
+ var SERVER_MIN_VALIDITY_SECONDS = 60;
852
+ var MIN_VALIDITY_SECONDS2 = SERVER_MIN_VALIDITY_SECONDS + CLOCK_SKEW_MARGIN_SECONDS;
853
+ var MAX_VALIDITY_SECONDS2 = 3600;
854
+ async function createX402Payment(options) {
855
+ const accept = resolveAccept(options.requirements);
856
+ const now = Math.floor((options.now?.() ?? Date.now()) / 1e3);
857
+ const validFor = options.validForSeconds ?? accept.maxTimeoutSeconds;
858
+ if (validFor < MIN_VALIDITY_SECONDS2 || validFor > MAX_VALIDITY_SECONDS2) {
859
+ throw new Error(
860
+ `[BitPal] x402: validForSeconds must be between ${MIN_VALIDITY_SECONDS2} and ${MAX_VALIDITY_SECONDS2} (got ${validFor}). The server requires ${SERVER_MIN_VALIDITY_SECONDS}s minimum; the extra ${CLOCK_SKEW_MARGIN_SECONDS}s absorbs clock skew and round-trip latency.`
861
+ );
862
+ }
863
+ const domain = {
864
+ name: accept.extra.name,
865
+ version: accept.extra.version,
866
+ chainId: accept.extra.chainId,
867
+ verifyingContract: accept.asset
868
+ };
869
+ if (!domain.chainId) {
870
+ throw new Error("[BitPal] x402: the 402 response is missing extra.chainId \u2014 cannot build the EIP-712 domain.");
871
+ }
872
+ const validAfter = "0";
873
+ const validBefore = String(now + validFor);
874
+ const nextNonce = options.randomNonce ?? randomNonce;
875
+ const fee = accept.feeBreakdown;
876
+ const feeAmount = fee ? BigInt(fee.feeAmount) : 0n;
877
+ const merchantValue = fee ? fee.netAmount : accept.maxAmountRequired;
878
+ const merchant = await signAuthorization({
879
+ domain,
880
+ signTypedData: options.signTypedData,
881
+ from: options.from,
882
+ to: accept.payTo,
883
+ value: merchantValue,
884
+ validAfter,
885
+ validBefore,
886
+ nonce: nextNonce()
887
+ });
888
+ let feeLeg;
889
+ if (feeAmount > 0n) {
890
+ if (!fee?.feeRecipient) {
891
+ throw new Error("[BitPal] x402: feeAmount > 0 but feeRecipient is missing in the 402 response.");
892
+ }
893
+ feeLeg = await signAuthorization({
894
+ domain,
895
+ signTypedData: options.signTypedData,
896
+ from: options.from,
897
+ to: fee.feeRecipient,
898
+ value: fee.feeAmount,
899
+ validAfter,
900
+ validBefore,
901
+ nonce: nextNonce()
902
+ });
903
+ if (feeLeg.nonce.toLowerCase() === merchant.nonce.toLowerCase()) {
904
+ throw new Error("[BitPal] x402: the two authorizations must use different nonces.");
905
+ }
906
+ }
907
+ const payload = {
908
+ x402Version: 1,
909
+ scheme: "exact",
910
+ network: accept.network,
911
+ payload: feeLeg ? { merchant, fee: feeLeg } : { merchant }
912
+ };
913
+ return {
914
+ header: encodePaymentHeader(payload),
915
+ payload,
916
+ totalAmount: (BigInt(merchantValue) + feeAmount).toString()
917
+ };
918
+ }
919
+ function resolveAccept(input) {
920
+ if ("accepts" in input) {
921
+ const first = input.accepts?.[0];
922
+ if (!first) throw new Error("[BitPal] x402: the 402 response has no accepts[] entry.");
923
+ return first;
924
+ }
925
+ return input;
926
+ }
927
+ async function signAuthorization(args) {
928
+ const message = {
929
+ from: args.from,
930
+ to: args.to,
931
+ value: args.value,
932
+ validAfter: args.validAfter,
933
+ validBefore: args.validBefore,
934
+ nonce: args.nonce
935
+ };
936
+ const signature = await args.signTypedData({
937
+ domain: args.domain,
938
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
939
+ primaryType: "TransferWithAuthorization",
940
+ message
941
+ });
942
+ return { ...message, signature: splitSignature(signature) };
943
+ }
944
+ function splitSignature(signature) {
945
+ const hex = signature.startsWith("0x") ? signature.slice(2) : signature;
946
+ if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== 130 && hex.length !== 128) {
947
+ throw new Error(
948
+ `[BitPal] x402: expected a 65-byte or 64-byte (EIP-2098) hex signature, got ${signature.length} chars.`
949
+ );
950
+ }
951
+ const r = `0x${hex.slice(0, 64)}`;
952
+ if (hex.length === 128) {
953
+ const yParityAndS = BigInt(`0x${hex.slice(64, 128)}`);
954
+ const yParity = Number(yParityAndS >> 255n & 1n);
955
+ const s2 = yParityAndS & (1n << 255n) - 1n;
956
+ return { v: 27 + yParity, r, s: `0x${s2.toString(16).padStart(64, "0")}` };
957
+ }
958
+ const s = `0x${hex.slice(64, 128)}`;
959
+ const raw = parseInt(hex.slice(128, 130), 16);
960
+ if (raw === 27 || raw === 28) return { v: raw, r, s };
961
+ if (raw === 0 || raw === 1) return { v: raw + 27, r, s };
962
+ throw new Error(`[BitPal] x402: unexpected signature v=${raw} (expected 0, 1, 27 or 28).`);
963
+ }
964
+ function randomNonce() {
965
+ const g = globalThis;
966
+ if (!g.crypto?.getRandomValues) {
967
+ throw new Error("[BitPal] x402: crypto.getRandomValues is unavailable \u2014 pass randomNonce explicitly.");
968
+ }
969
+ const bytes = g.crypto.getRandomValues(new Uint8Array(32));
970
+ let hex = "";
971
+ for (const b of bytes) hex += b.toString(16).padStart(2, "0");
972
+ return `0x${hex}`;
973
+ }
974
+
411
975
  // src/index.ts
412
976
  var BitPal = class {
413
977
  checkout;
@@ -425,11 +989,21 @@ var BitPal = class {
425
989
  PaymentLinksResource,
426
990
  PricesResource,
427
991
  ProductsResource,
992
+ TRANSFER_WITH_AUTHORIZATION_TYPES,
428
993
  WEBHOOK_HEADERS,
994
+ X402Gate,
995
+ X402_PAYMENT_HEADER,
996
+ X402_PAYMENT_RESPONSE_HEADER,
997
+ createMemoryReplayStore,
998
+ createX402Payment,
999
+ decodePaymentHeader,
1000
+ encodePaymentHeader,
429
1001
  fromAtomic,
430
1002
  fromAtomicUSDC,
431
1003
  parseWebhookEvent,
1004
+ splitSignature,
432
1005
  toAtomic,
433
1006
  toAtomicUSDC,
434
- verifyWebhookSignature
1007
+ verifyWebhookSignature,
1008
+ x402
435
1009
  });