@bitpal/checkout 0.10.0 → 0.11.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/README.md CHANGED
@@ -249,9 +249,13 @@ the response, or `release()` if you could not.
249
249
 
250
250
  ### Payer side
251
251
 
252
- The fee split needs two authorizations, which a stock x402 client does not know about.
253
- `createX402Payment` reads the `402` and builds both. You supply the signer; the SDK has no crypto
254
- dependency.
252
+ The payer signs **one** standard EIP-3009 authorization, so the `X-PAYMENT` payload is plain x402:
253
+ `{ signature, authorization }`. The fee split happens on-chain — `payTo` in the `402` is not the
254
+ merchant's wallet but a CREATE2 address that commits to the whole split, so nothing extra has to be
255
+ signed for it.
256
+
257
+ `createX402Payment` reads the `402` and builds the header. You supply the signer; the SDK has no
258
+ crypto dependency.
255
259
 
256
260
  ```ts
257
261
  import { createX402Payment } from '@bitpal/checkout';
@@ -277,6 +281,10 @@ if (res.status === 402) {
277
281
  Amounts are human-readable (`'1.00'`), the same model as `line_items[].amount` — the server converts.
278
282
  Pass `amountAtomic` instead if you already have raw units.
279
283
 
284
+ The `402` also carries a `feeBreakdown` (net / fee / bps / recipient). It is informational — a payer
285
+ can ignore it entirely and still pay correctly, because the split is already committed by the `payTo`
286
+ address. It is there so you can see where the money goes without reading the chain.
287
+
280
288
  EOA signers only — smart-contract wallets cannot sign EIP-3009 authorizations. Amounts are exact:
281
289
  over- and underpayment are both rejected, and this lane has no refund path. Authorizations are valid
282
290
  for 90–3600 seconds (the server's floor is 60s; the extra 30s absorbs clock skew and round trips).
@@ -308,6 +316,26 @@ store wiring session creation + webhook verification + idempotency.
308
316
  pnpm build # tsup → dist/ (ESM + .d.ts)
309
317
  ```
310
318
 
319
+ ## Migrating to 0.11.0 (breaking — x402 payer only)
320
+
321
+ The `X-PAYMENT` payload changed from BitPal's two-authorization shape to the **x402 standard**:
322
+
323
+ ```
324
+ 0.10.0 payload: { merchant: {...}, fee: {...} } two signatures
325
+ 0.11.0 payload: { signature, authorization } one signature — plain x402
326
+ ```
327
+
328
+ - If you call `createX402Payment`, nothing changes in your code. It signs once now instead of twice,
329
+ and the wallet prompts once.
330
+ - If you built the payload by hand, rebuild it in the standard shape.
331
+ - `splitSignature` is gone. `normalizeSignature` replaces it — same input handling (65-byte or
332
+ EIP-2098 compact, `v` of 0/1/27/28) but it returns a 65-byte packed hex string, which is what the
333
+ payload carries.
334
+ - The `402` is unchanged apart from `payTo`, which now points at the settlement address rather than
335
+ the merchant wallet. Both `X402Gate` and `x402()` handle this for you.
336
+
337
+ Old headers are rejected — a payer on 0.10.0 gets a fresh `402` and re-signs, so no payment is lost.
338
+
311
339
  ## Migrating from 0.3.x → 0.5.0 (breaking)
312
340
 
313
341
  - **Webhook verification now requires the timestamp** and enforces replay defense. Pass the
package/dist/index.cjs CHANGED
@@ -48,8 +48,8 @@ __export(index_exports, {
48
48
  encodePaymentHeader: () => encodePaymentHeader,
49
49
  fromAtomic: () => fromAtomic,
50
50
  fromAtomicUSDC: () => fromAtomicUSDC,
51
+ normalizeSignature: () => normalizeSignature,
51
52
  parseWebhookEvent: () => parseWebhookEvent,
52
- splitSignature: () => splitSignature,
53
53
  toAtomic: () => toAtomic,
54
54
  toAtomicUSDC: () => toAtomicUSDC,
55
55
  verifyWebhookSignature: () => verifyWebhookSignature,
@@ -544,11 +544,11 @@ var X402Gate = class {
544
544
  } catch {
545
545
  return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
546
546
  }
547
- const merchant = payload?.payload?.merchant;
548
- if (!merchant?.from || !merchant?.nonce) {
547
+ const auth = payload?.payload?.authorization;
548
+ if (!auth?.from || !auth?.nonce) {
549
549
  return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
550
550
  }
551
- const key = `${payload.network}:${merchant.from.toLowerCase()}:${merchant.nonce.toLowerCase()}`;
551
+ const key = `${payload.network}:${auth.from.toLowerCase()}:${auth.nonce.toLowerCase()}`;
552
552
  const claim = await this.store.claim(key, this.maxTimeoutSeconds);
553
553
  if (claim === "used") {
554
554
  return {
@@ -586,7 +586,12 @@ var X402Gate = class {
586
586
  },
587
587
  paymentRequirements: {
588
588
  asset: accept.asset,
589
+ // payer 가 서명한 목적지 = 분배 조건을 커밋한 정산 주소.
589
590
  payTo: accept.payTo,
591
+ // 머천트 정산 지갑. facilitator 가 이 값으로 정산 주소를 재계산해 payTo 와 대조한다 —
592
+ // CREATE2 는 단방향이라 payTo 에서 되돌릴 수 없으므로 따로 보내야 한다.
593
+ // 우리 설정값을 그대로 쓴다(402 응답에서 읽지 않는다 — 그러면 재계산의 의미가 없다).
594
+ merchant: this.opts.payTo,
590
595
  maxAmountRequired: accept.maxAmountRequired,
591
596
  resource: accept.resource
592
597
  }
@@ -638,7 +643,7 @@ var X402Gate = class {
638
643
  txHash: settled.tx_hash,
639
644
  feeTxHash: settled.fee_tx_hash ?? null,
640
645
  network: settled.network,
641
- payer: merchant.from,
646
+ payer: auth.from,
642
647
  amount: accept.maxAmountRequired,
643
648
  idempotent: settled.idempotent ?? false
644
649
  };
@@ -650,7 +655,7 @@ var X402Gate = class {
650
655
  success: true,
651
656
  transaction: settled.tx_hash,
652
657
  network: settled.network,
653
- payer: merchant.from,
658
+ payer: auth.from,
654
659
  paymentId: settled.payment_id
655
660
  }),
656
661
  // 소진 확정은 **응답을 실제로 내준 뒤**다. 핸들러가 throw 하거나 프로세스가 죽으면 결제만 하고
@@ -771,18 +776,22 @@ function assertPaymentPayload(value) {
771
776
  throw new Error("[BitPal] x402: invalid network.");
772
777
  }
773
778
  const inner = asObject(root.payload, "payload");
774
- const merchant = assertAuthorization(inner.merchant, "merchant");
775
- const fee = inner.fee === void 0 ? void 0 : assertAuthorization(inner.fee, "fee");
779
+ if (typeof inner.signature !== "string" || !PACKED_SIG_RE.test(inner.signature)) {
780
+ throw new Error("[BitPal] x402: invalid payload.signature (expected 65-byte packed hex).");
781
+ }
776
782
  return {
777
783
  x402Version: 1,
778
784
  scheme: "exact",
779
785
  network: root.network,
780
- payload: fee ? { merchant, fee } : { merchant }
786
+ payload: {
787
+ signature: inner.signature,
788
+ authorization: assertAuthorization(inner.authorization, "authorization")
789
+ }
781
790
  };
782
791
  }
792
+ var PACKED_SIG_RE = /^0x[0-9a-fA-F]{130}$/;
783
793
  function assertAuthorization(value, label) {
784
- const a = asObject(value, `${label} authorization`);
785
- const sig = asObject(a.signature, `${label} signature`);
794
+ const a = asObject(value, label);
786
795
  const check = (ok, field) => {
787
796
  if (!ok) throw new Error(`[BitPal] x402: invalid ${label}.${field}.`);
788
797
  };
@@ -792,17 +801,13 @@ function assertAuthorization(value, label) {
792
801
  check(typeof a.validAfter === "string" && DECIMAL_RE.test(a.validAfter), "validAfter");
793
802
  check(typeof a.validBefore === "string" && DECIMAL_RE.test(a.validBefore), "validBefore");
794
803
  check(typeof a.nonce === "string" && HEX32_RE.test(a.nonce), "nonce");
795
- check(typeof sig.v === "number" && Number.isInteger(sig.v), "signature.v");
796
- check(typeof sig.r === "string" && HEX32_RE.test(sig.r), "signature.r");
797
- check(typeof sig.s === "string" && HEX32_RE.test(sig.s), "signature.s");
798
804
  return {
799
805
  from: a.from,
800
806
  to: a.to,
801
807
  value: a.value,
802
808
  validAfter: a.validAfter,
803
809
  validBefore: a.validBefore,
804
- nonce: a.nonce,
805
- signature: { v: sig.v, r: sig.r, s: sig.s }
810
+ nonce: a.nonce
806
811
  };
807
812
  }
808
813
  function asObject(value, label) {
@@ -876,49 +881,33 @@ async function createX402Payment(options) {
876
881
  }
877
882
  const validAfter = "0";
878
883
  const validBefore = String(now + validFor);
879
- const nextNonce = options.randomNonce ?? randomNonce;
880
- const fee = accept.feeBreakdown;
881
- const feeAmount = fee ? BigInt(fee.feeAmount) : 0n;
882
- const merchantValue = fee ? fee.netAmount : accept.maxAmountRequired;
883
- const merchant = await signAuthorization({
884
- domain,
885
- signTypedData: options.signTypedData,
884
+ const nonce = (options.randomNonce ?? randomNonce)();
885
+ const authorization = {
886
886
  from: options.from,
887
887
  to: accept.payTo,
888
- value: merchantValue,
888
+ value: accept.maxAmountRequired,
889
889
  validAfter,
890
890
  validBefore,
891
- nonce: nextNonce()
892
- });
893
- let feeLeg;
894
- if (feeAmount > 0n) {
895
- if (!fee?.feeRecipient) {
896
- throw new Error("[BitPal] x402: feeAmount > 0 but feeRecipient is missing in the 402 response.");
897
- }
898
- feeLeg = await signAuthorization({
891
+ nonce
892
+ };
893
+ const signature = normalizeSignature(
894
+ await options.signTypedData({
899
895
  domain,
900
- signTypedData: options.signTypedData,
901
- from: options.from,
902
- to: fee.feeRecipient,
903
- value: fee.feeAmount,
904
- validAfter,
905
- validBefore,
906
- nonce: nextNonce()
907
- });
908
- if (feeLeg.nonce.toLowerCase() === merchant.nonce.toLowerCase()) {
909
- throw new Error("[BitPal] x402: the two authorizations must use different nonces.");
910
- }
911
- }
896
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
897
+ primaryType: "TransferWithAuthorization",
898
+ message: authorization
899
+ })
900
+ );
912
901
  const payload = {
913
902
  x402Version: 1,
914
903
  scheme: "exact",
915
904
  network: accept.network,
916
- payload: feeLeg ? { merchant, fee: feeLeg } : { merchant }
905
+ payload: { signature, authorization }
917
906
  };
918
907
  return {
919
908
  header: encodePaymentHeader(payload),
920
909
  payload,
921
- totalAmount: (BigInt(merchantValue) + feeAmount).toString()
910
+ totalAmount: accept.maxAmountRequired
922
911
  };
923
912
  }
924
913
  function resolveAccept(input) {
@@ -929,41 +918,24 @@ function resolveAccept(input) {
929
918
  }
930
919
  return input;
931
920
  }
932
- async function signAuthorization(args) {
933
- const message = {
934
- from: args.from,
935
- to: args.to,
936
- value: args.value,
937
- validAfter: args.validAfter,
938
- validBefore: args.validBefore,
939
- nonce: args.nonce
940
- };
941
- const signature = await args.signTypedData({
942
- domain: args.domain,
943
- types: TRANSFER_WITH_AUTHORIZATION_TYPES,
944
- primaryType: "TransferWithAuthorization",
945
- message
946
- });
947
- return { ...message, signature: splitSignature(signature) };
948
- }
949
- function splitSignature(signature) {
921
+ function normalizeSignature(signature) {
950
922
  const hex = signature.startsWith("0x") ? signature.slice(2) : signature;
951
923
  if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== 130 && hex.length !== 128) {
952
924
  throw new Error(
953
925
  `[BitPal] x402: expected a 65-byte or 64-byte (EIP-2098) hex signature, got ${signature.length} chars.`
954
926
  );
955
927
  }
956
- const r = `0x${hex.slice(0, 64)}`;
928
+ const r = hex.slice(0, 64);
957
929
  if (hex.length === 128) {
958
930
  const yParityAndS = BigInt(`0x${hex.slice(64, 128)}`);
959
931
  const yParity = Number(yParityAndS >> 255n & 1n);
960
- const s2 = yParityAndS & (1n << 255n) - 1n;
961
- return { v: 27 + yParity, r, s: `0x${s2.toString(16).padStart(64, "0")}` };
932
+ const s2 = (yParityAndS & (1n << 255n) - 1n).toString(16).padStart(64, "0");
933
+ return `0x${r}${s2}${(27 + yParity).toString(16).padStart(2, "0")}`;
962
934
  }
963
- const s = `0x${hex.slice(64, 128)}`;
935
+ const s = hex.slice(64, 128);
964
936
  const raw = parseInt(hex.slice(128, 130), 16);
965
- if (raw === 27 || raw === 28) return { v: raw, r, s };
966
- if (raw === 0 || raw === 1) return { v: raw + 27, r, s };
937
+ if (raw === 27 || raw === 28) return `0x${r}${s}${raw.toString(16)}`;
938
+ if (raw === 0 || raw === 1) return `0x${r}${s}${(raw + 27).toString(16)}`;
967
939
  throw new Error(`[BitPal] x402: unexpected signature v=${raw} (expected 0, 1, 27 or 28).`);
968
940
  }
969
941
  function randomNonce() {
@@ -1005,8 +977,8 @@ var BitPal = class {
1005
977
  encodePaymentHeader,
1006
978
  fromAtomic,
1007
979
  fromAtomicUSDC,
980
+ normalizeSignature,
1008
981
  parseWebhookEvent,
1009
- splitSignature,
1010
982
  toAtomic,
1011
983
  toAtomicUSDC,
1012
984
  verifyWebhookSignature,
package/dist/index.d.cts CHANGED
@@ -750,6 +750,7 @@ interface X402Requirements {
750
750
  x402Version: 1;
751
751
  accepts: X402Accept[];
752
752
  }
753
+ /** EIP-3009 `TransferWithAuthorization` 의 서명 대상 필드. x402 표준 `payload.authorization`. */
753
754
  interface Eip3009Authorization {
754
755
  from: string;
755
756
  to: string;
@@ -757,21 +758,23 @@ interface Eip3009Authorization {
757
758
  validAfter: string;
758
759
  validBefore: string;
759
760
  nonce: string;
760
- signature: {
761
- v: number;
762
- r: string;
763
- s: string;
764
- };
765
761
  }
762
+ /**
763
+ * x402 표준 payload 그대로다 — 단일 authorization + 65바이트 packed 서명.
764
+ *
765
+ * 0.10.0 까지는 `{ merchant, fee }` 2건이었다. 포워더 없이 수수료를 나누려면 payer 가 두 번
766
+ * 서명해야 했고, 그 탓에 순정 x402 클라이언트가 BitPal 머천트에게 결제할 수 없었다.
767
+ * 이제 분배는 `payTo`(포워더) 주소가 커밋하므로 서명이 1건이면 된다.
768
+ */
766
769
  interface X402PaymentPayload {
767
770
  x402Version: 1;
768
771
  scheme: 'exact';
769
772
  /** x402 wire 이름 — 'base' / 'base-sepolia'. */
770
773
  network: string;
771
774
  payload: {
772
- merchant: Eip3009Authorization;
773
- /** 서버가 계산한 feeAmount > 0 이면 필수. */
774
- fee?: Eip3009Authorization;
775
+ /** 65바이트 packed hex(`0x` + r + s + v). */
776
+ signature: string;
777
+ authorization: Eip3009Authorization;
775
778
  };
776
779
  }
777
780
  /** `ok` = 이번 호출이 최초 점유 / `inflight` = 다른 요청이 처리 중 / `used` = 이미 리소스를 내줌. */
@@ -1080,11 +1083,18 @@ declare function createX402Payment(options: CreateX402PaymentOptions): Promise<X
1080
1083
  * 65바이트(r‖s‖v)와 64바이트 EIP-2098 compact(r‖yParityAndS) 둘 다 받는다 — viem 의
1081
1084
  * `signTypedData` 는 65바이트를 주지만 일부 지갑·라이브러리는 compact 를 돌려준다.
1082
1085
  */
1083
- declare function splitSignature(signature: string): {
1084
- v: number;
1085
- r: string;
1086
- s: string;
1087
- };
1086
+ /**
1087
+ * 지갑이 돌려준 서명을 **65바이트 packed hex** 로 수렴시킨다. x402 표준이 그 형태다.
1088
+ *
1089
+ * 두 가지를 정규화한다:
1090
+ * - EIP-2098 compact(64바이트) — 일부 지갑이 이걸 낸다. s 의 최상위 비트가 yParity 라 떼어내고
1091
+ * v 를 복원해 65바이트로 편다.
1092
+ * - v 가 0/1 인 경우 — 온체인 ecrecover 규약인 27/28 로 맞춘다.
1093
+ *
1094
+ * 서버가 130 hex 만 받으므로 여기서 안 맞추면 그쪽에서 거부된다. 그 외 값은 recover 가 어긋나
1095
+ * 어차피 실패하므로 여기서 끊는다.
1096
+ */
1097
+ declare function normalizeSignature(signature: string): string;
1088
1098
 
1089
1099
  /**
1090
1100
  * @bitpal/checkout — BitPal Checkout SDK
@@ -1114,4 +1124,4 @@ declare class BitPal {
1114
1124
  constructor(config: BitPalConfig | string);
1115
1125
  }
1116
1126
 
1117
- export { type ApiResponse, BitPal, BitPalCheckoutClient, type BitPalConfig, BitPalError, CHECKOUT_WEBHOOK_EVENTS, type CheckoutLineItemInput, type CheckoutPaymentLink, type CheckoutPrice, type CheckoutProduct, type CheckoutSession, type CheckoutWebhookEvent, type CreatePaymentLinkParams, type CreatePriceParams, type CreateProductParams, type CreateSessionParams, type CreateX402PaymentOptions, type DepositAddress, type Eip3009Authorization, type Eip712Domain, type FeePreview, type IdempotencyOptions, type IssueDepositAddressParams, type LineItem, type ListPaymentLinksParams, type ListPricesParams, type ListProductsParams, type ListResponse, PaymentLinksResource, type PaymentOption, type PriceLineItem, PricesResource, ProductsResource, type PublicSession, type SessionStatus, TRANSFER_WITH_AUTHORIZATION_TYPES, type UpdatePaymentLinkParams, type UpdatePriceParams, type UpdateProductParams, WEBHOOK_HEADERS, type WebhookPayload, type X402Accept, type X402ClaimState, type X402FeeBreakdown, X402Gate, type X402GateOptions, type X402GateResult, type X402MiddlewareOptions, type X402PaymentPayload, type X402PaymentResult, type X402ReplayStore, type X402Request, type X402Requirements, type X402Response, type X402SettledPayment, type X402Signer, type X402TypedDataRequest, X402_PAYMENT_HEADER, X402_PAYMENT_RESPONSE_HEADER, createMemoryReplayStore, createX402Payment, decodePaymentHeader, encodePaymentHeader, fromAtomic, fromAtomicUSDC, parseWebhookEvent, splitSignature, toAtomic, toAtomicUSDC, verifyWebhookSignature, x402 };
1127
+ export { type ApiResponse, BitPal, BitPalCheckoutClient, type BitPalConfig, BitPalError, CHECKOUT_WEBHOOK_EVENTS, type CheckoutLineItemInput, type CheckoutPaymentLink, type CheckoutPrice, type CheckoutProduct, type CheckoutSession, type CheckoutWebhookEvent, type CreatePaymentLinkParams, type CreatePriceParams, type CreateProductParams, type CreateSessionParams, type CreateX402PaymentOptions, type DepositAddress, type Eip3009Authorization, type Eip712Domain, type FeePreview, type IdempotencyOptions, type IssueDepositAddressParams, type LineItem, type ListPaymentLinksParams, type ListPricesParams, type ListProductsParams, type ListResponse, PaymentLinksResource, type PaymentOption, type PriceLineItem, PricesResource, ProductsResource, type PublicSession, type SessionStatus, TRANSFER_WITH_AUTHORIZATION_TYPES, type UpdatePaymentLinkParams, type UpdatePriceParams, type UpdateProductParams, WEBHOOK_HEADERS, type WebhookPayload, type X402Accept, type X402ClaimState, type X402FeeBreakdown, X402Gate, type X402GateOptions, type X402GateResult, type X402MiddlewareOptions, type X402PaymentPayload, type X402PaymentResult, type X402ReplayStore, type X402Request, type X402Requirements, type X402Response, type X402SettledPayment, type X402Signer, type X402TypedDataRequest, X402_PAYMENT_HEADER, X402_PAYMENT_RESPONSE_HEADER, createMemoryReplayStore, createX402Payment, decodePaymentHeader, encodePaymentHeader, fromAtomic, fromAtomicUSDC, normalizeSignature, parseWebhookEvent, toAtomic, toAtomicUSDC, verifyWebhookSignature, x402 };
package/dist/index.d.ts CHANGED
@@ -750,6 +750,7 @@ interface X402Requirements {
750
750
  x402Version: 1;
751
751
  accepts: X402Accept[];
752
752
  }
753
+ /** EIP-3009 `TransferWithAuthorization` 의 서명 대상 필드. x402 표준 `payload.authorization`. */
753
754
  interface Eip3009Authorization {
754
755
  from: string;
755
756
  to: string;
@@ -757,21 +758,23 @@ interface Eip3009Authorization {
757
758
  validAfter: string;
758
759
  validBefore: string;
759
760
  nonce: string;
760
- signature: {
761
- v: number;
762
- r: string;
763
- s: string;
764
- };
765
761
  }
762
+ /**
763
+ * x402 표준 payload 그대로다 — 단일 authorization + 65바이트 packed 서명.
764
+ *
765
+ * 0.10.0 까지는 `{ merchant, fee }` 2건이었다. 포워더 없이 수수료를 나누려면 payer 가 두 번
766
+ * 서명해야 했고, 그 탓에 순정 x402 클라이언트가 BitPal 머천트에게 결제할 수 없었다.
767
+ * 이제 분배는 `payTo`(포워더) 주소가 커밋하므로 서명이 1건이면 된다.
768
+ */
766
769
  interface X402PaymentPayload {
767
770
  x402Version: 1;
768
771
  scheme: 'exact';
769
772
  /** x402 wire 이름 — 'base' / 'base-sepolia'. */
770
773
  network: string;
771
774
  payload: {
772
- merchant: Eip3009Authorization;
773
- /** 서버가 계산한 feeAmount > 0 이면 필수. */
774
- fee?: Eip3009Authorization;
775
+ /** 65바이트 packed hex(`0x` + r + s + v). */
776
+ signature: string;
777
+ authorization: Eip3009Authorization;
775
778
  };
776
779
  }
777
780
  /** `ok` = 이번 호출이 최초 점유 / `inflight` = 다른 요청이 처리 중 / `used` = 이미 리소스를 내줌. */
@@ -1080,11 +1083,18 @@ declare function createX402Payment(options: CreateX402PaymentOptions): Promise<X
1080
1083
  * 65바이트(r‖s‖v)와 64바이트 EIP-2098 compact(r‖yParityAndS) 둘 다 받는다 — viem 의
1081
1084
  * `signTypedData` 는 65바이트를 주지만 일부 지갑·라이브러리는 compact 를 돌려준다.
1082
1085
  */
1083
- declare function splitSignature(signature: string): {
1084
- v: number;
1085
- r: string;
1086
- s: string;
1087
- };
1086
+ /**
1087
+ * 지갑이 돌려준 서명을 **65바이트 packed hex** 로 수렴시킨다. x402 표준이 그 형태다.
1088
+ *
1089
+ * 두 가지를 정규화한다:
1090
+ * - EIP-2098 compact(64바이트) — 일부 지갑이 이걸 낸다. s 의 최상위 비트가 yParity 라 떼어내고
1091
+ * v 를 복원해 65바이트로 편다.
1092
+ * - v 가 0/1 인 경우 — 온체인 ecrecover 규약인 27/28 로 맞춘다.
1093
+ *
1094
+ * 서버가 130 hex 만 받으므로 여기서 안 맞추면 그쪽에서 거부된다. 그 외 값은 recover 가 어긋나
1095
+ * 어차피 실패하므로 여기서 끊는다.
1096
+ */
1097
+ declare function normalizeSignature(signature: string): string;
1088
1098
 
1089
1099
  /**
1090
1100
  * @bitpal/checkout — BitPal Checkout SDK
@@ -1114,4 +1124,4 @@ declare class BitPal {
1114
1124
  constructor(config: BitPalConfig | string);
1115
1125
  }
1116
1126
 
1117
- export { type ApiResponse, BitPal, BitPalCheckoutClient, type BitPalConfig, BitPalError, CHECKOUT_WEBHOOK_EVENTS, type CheckoutLineItemInput, type CheckoutPaymentLink, type CheckoutPrice, type CheckoutProduct, type CheckoutSession, type CheckoutWebhookEvent, type CreatePaymentLinkParams, type CreatePriceParams, type CreateProductParams, type CreateSessionParams, type CreateX402PaymentOptions, type DepositAddress, type Eip3009Authorization, type Eip712Domain, type FeePreview, type IdempotencyOptions, type IssueDepositAddressParams, type LineItem, type ListPaymentLinksParams, type ListPricesParams, type ListProductsParams, type ListResponse, PaymentLinksResource, type PaymentOption, type PriceLineItem, PricesResource, ProductsResource, type PublicSession, type SessionStatus, TRANSFER_WITH_AUTHORIZATION_TYPES, type UpdatePaymentLinkParams, type UpdatePriceParams, type UpdateProductParams, WEBHOOK_HEADERS, type WebhookPayload, type X402Accept, type X402ClaimState, type X402FeeBreakdown, X402Gate, type X402GateOptions, type X402GateResult, type X402MiddlewareOptions, type X402PaymentPayload, type X402PaymentResult, type X402ReplayStore, type X402Request, type X402Requirements, type X402Response, type X402SettledPayment, type X402Signer, type X402TypedDataRequest, X402_PAYMENT_HEADER, X402_PAYMENT_RESPONSE_HEADER, createMemoryReplayStore, createX402Payment, decodePaymentHeader, encodePaymentHeader, fromAtomic, fromAtomicUSDC, parseWebhookEvent, splitSignature, toAtomic, toAtomicUSDC, verifyWebhookSignature, x402 };
1127
+ export { type ApiResponse, BitPal, BitPalCheckoutClient, type BitPalConfig, BitPalError, CHECKOUT_WEBHOOK_EVENTS, type CheckoutLineItemInput, type CheckoutPaymentLink, type CheckoutPrice, type CheckoutProduct, type CheckoutSession, type CheckoutWebhookEvent, type CreatePaymentLinkParams, type CreatePriceParams, type CreateProductParams, type CreateSessionParams, type CreateX402PaymentOptions, type DepositAddress, type Eip3009Authorization, type Eip712Domain, type FeePreview, type IdempotencyOptions, type IssueDepositAddressParams, type LineItem, type ListPaymentLinksParams, type ListPricesParams, type ListProductsParams, type ListResponse, PaymentLinksResource, type PaymentOption, type PriceLineItem, PricesResource, ProductsResource, type PublicSession, type SessionStatus, TRANSFER_WITH_AUTHORIZATION_TYPES, type UpdatePaymentLinkParams, type UpdatePriceParams, type UpdateProductParams, WEBHOOK_HEADERS, type WebhookPayload, type X402Accept, type X402ClaimState, type X402FeeBreakdown, X402Gate, type X402GateOptions, type X402GateResult, type X402MiddlewareOptions, type X402PaymentPayload, type X402PaymentResult, type X402ReplayStore, type X402Request, type X402Requirements, type X402Response, type X402SettledPayment, type X402Signer, type X402TypedDataRequest, X402_PAYMENT_HEADER, X402_PAYMENT_RESPONSE_HEADER, createMemoryReplayStore, createX402Payment, decodePaymentHeader, encodePaymentHeader, fromAtomic, fromAtomicUSDC, normalizeSignature, parseWebhookEvent, toAtomic, toAtomicUSDC, verifyWebhookSignature, x402 };
package/dist/index.js CHANGED
@@ -485,11 +485,11 @@ var X402Gate = class {
485
485
  } catch {
486
486
  return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
487
487
  }
488
- const merchant = payload?.payload?.merchant;
489
- if (!merchant?.from || !merchant?.nonce) {
488
+ const auth = payload?.payload?.authorization;
489
+ if (!auth?.from || !auth?.nonce) {
490
490
  return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
491
491
  }
492
- const key = `${payload.network}:${merchant.from.toLowerCase()}:${merchant.nonce.toLowerCase()}`;
492
+ const key = `${payload.network}:${auth.from.toLowerCase()}:${auth.nonce.toLowerCase()}`;
493
493
  const claim = await this.store.claim(key, this.maxTimeoutSeconds);
494
494
  if (claim === "used") {
495
495
  return {
@@ -527,7 +527,12 @@ var X402Gate = class {
527
527
  },
528
528
  paymentRequirements: {
529
529
  asset: accept.asset,
530
+ // payer 가 서명한 목적지 = 분배 조건을 커밋한 정산 주소.
530
531
  payTo: accept.payTo,
532
+ // 머천트 정산 지갑. facilitator 가 이 값으로 정산 주소를 재계산해 payTo 와 대조한다 —
533
+ // CREATE2 는 단방향이라 payTo 에서 되돌릴 수 없으므로 따로 보내야 한다.
534
+ // 우리 설정값을 그대로 쓴다(402 응답에서 읽지 않는다 — 그러면 재계산의 의미가 없다).
535
+ merchant: this.opts.payTo,
531
536
  maxAmountRequired: accept.maxAmountRequired,
532
537
  resource: accept.resource
533
538
  }
@@ -579,7 +584,7 @@ var X402Gate = class {
579
584
  txHash: settled.tx_hash,
580
585
  feeTxHash: settled.fee_tx_hash ?? null,
581
586
  network: settled.network,
582
- payer: merchant.from,
587
+ payer: auth.from,
583
588
  amount: accept.maxAmountRequired,
584
589
  idempotent: settled.idempotent ?? false
585
590
  };
@@ -591,7 +596,7 @@ var X402Gate = class {
591
596
  success: true,
592
597
  transaction: settled.tx_hash,
593
598
  network: settled.network,
594
- payer: merchant.from,
599
+ payer: auth.from,
595
600
  paymentId: settled.payment_id
596
601
  }),
597
602
  // 소진 확정은 **응답을 실제로 내준 뒤**다. 핸들러가 throw 하거나 프로세스가 죽으면 결제만 하고
@@ -712,18 +717,22 @@ function assertPaymentPayload(value) {
712
717
  throw new Error("[BitPal] x402: invalid network.");
713
718
  }
714
719
  const inner = asObject(root.payload, "payload");
715
- const merchant = assertAuthorization(inner.merchant, "merchant");
716
- const fee = inner.fee === void 0 ? void 0 : assertAuthorization(inner.fee, "fee");
720
+ if (typeof inner.signature !== "string" || !PACKED_SIG_RE.test(inner.signature)) {
721
+ throw new Error("[BitPal] x402: invalid payload.signature (expected 65-byte packed hex).");
722
+ }
717
723
  return {
718
724
  x402Version: 1,
719
725
  scheme: "exact",
720
726
  network: root.network,
721
- payload: fee ? { merchant, fee } : { merchant }
727
+ payload: {
728
+ signature: inner.signature,
729
+ authorization: assertAuthorization(inner.authorization, "authorization")
730
+ }
722
731
  };
723
732
  }
733
+ var PACKED_SIG_RE = /^0x[0-9a-fA-F]{130}$/;
724
734
  function assertAuthorization(value, label) {
725
- const a = asObject(value, `${label} authorization`);
726
- const sig = asObject(a.signature, `${label} signature`);
735
+ const a = asObject(value, label);
727
736
  const check = (ok, field) => {
728
737
  if (!ok) throw new Error(`[BitPal] x402: invalid ${label}.${field}.`);
729
738
  };
@@ -733,17 +742,13 @@ function assertAuthorization(value, label) {
733
742
  check(typeof a.validAfter === "string" && DECIMAL_RE.test(a.validAfter), "validAfter");
734
743
  check(typeof a.validBefore === "string" && DECIMAL_RE.test(a.validBefore), "validBefore");
735
744
  check(typeof a.nonce === "string" && HEX32_RE.test(a.nonce), "nonce");
736
- check(typeof sig.v === "number" && Number.isInteger(sig.v), "signature.v");
737
- check(typeof sig.r === "string" && HEX32_RE.test(sig.r), "signature.r");
738
- check(typeof sig.s === "string" && HEX32_RE.test(sig.s), "signature.s");
739
745
  return {
740
746
  from: a.from,
741
747
  to: a.to,
742
748
  value: a.value,
743
749
  validAfter: a.validAfter,
744
750
  validBefore: a.validBefore,
745
- nonce: a.nonce,
746
- signature: { v: sig.v, r: sig.r, s: sig.s }
751
+ nonce: a.nonce
747
752
  };
748
753
  }
749
754
  function asObject(value, label) {
@@ -817,49 +822,33 @@ async function createX402Payment(options) {
817
822
  }
818
823
  const validAfter = "0";
819
824
  const validBefore = String(now + validFor);
820
- const nextNonce = options.randomNonce ?? randomNonce;
821
- const fee = accept.feeBreakdown;
822
- const feeAmount = fee ? BigInt(fee.feeAmount) : 0n;
823
- const merchantValue = fee ? fee.netAmount : accept.maxAmountRequired;
824
- const merchant = await signAuthorization({
825
- domain,
826
- signTypedData: options.signTypedData,
825
+ const nonce = (options.randomNonce ?? randomNonce)();
826
+ const authorization = {
827
827
  from: options.from,
828
828
  to: accept.payTo,
829
- value: merchantValue,
829
+ value: accept.maxAmountRequired,
830
830
  validAfter,
831
831
  validBefore,
832
- nonce: nextNonce()
833
- });
834
- let feeLeg;
835
- if (feeAmount > 0n) {
836
- if (!fee?.feeRecipient) {
837
- throw new Error("[BitPal] x402: feeAmount > 0 but feeRecipient is missing in the 402 response.");
838
- }
839
- feeLeg = await signAuthorization({
832
+ nonce
833
+ };
834
+ const signature = normalizeSignature(
835
+ await options.signTypedData({
840
836
  domain,
841
- signTypedData: options.signTypedData,
842
- from: options.from,
843
- to: fee.feeRecipient,
844
- value: fee.feeAmount,
845
- validAfter,
846
- validBefore,
847
- nonce: nextNonce()
848
- });
849
- if (feeLeg.nonce.toLowerCase() === merchant.nonce.toLowerCase()) {
850
- throw new Error("[BitPal] x402: the two authorizations must use different nonces.");
851
- }
852
- }
837
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
838
+ primaryType: "TransferWithAuthorization",
839
+ message: authorization
840
+ })
841
+ );
853
842
  const payload = {
854
843
  x402Version: 1,
855
844
  scheme: "exact",
856
845
  network: accept.network,
857
- payload: feeLeg ? { merchant, fee: feeLeg } : { merchant }
846
+ payload: { signature, authorization }
858
847
  };
859
848
  return {
860
849
  header: encodePaymentHeader(payload),
861
850
  payload,
862
- totalAmount: (BigInt(merchantValue) + feeAmount).toString()
851
+ totalAmount: accept.maxAmountRequired
863
852
  };
864
853
  }
865
854
  function resolveAccept(input) {
@@ -870,41 +859,24 @@ function resolveAccept(input) {
870
859
  }
871
860
  return input;
872
861
  }
873
- async function signAuthorization(args) {
874
- const message = {
875
- from: args.from,
876
- to: args.to,
877
- value: args.value,
878
- validAfter: args.validAfter,
879
- validBefore: args.validBefore,
880
- nonce: args.nonce
881
- };
882
- const signature = await args.signTypedData({
883
- domain: args.domain,
884
- types: TRANSFER_WITH_AUTHORIZATION_TYPES,
885
- primaryType: "TransferWithAuthorization",
886
- message
887
- });
888
- return { ...message, signature: splitSignature(signature) };
889
- }
890
- function splitSignature(signature) {
862
+ function normalizeSignature(signature) {
891
863
  const hex = signature.startsWith("0x") ? signature.slice(2) : signature;
892
864
  if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== 130 && hex.length !== 128) {
893
865
  throw new Error(
894
866
  `[BitPal] x402: expected a 65-byte or 64-byte (EIP-2098) hex signature, got ${signature.length} chars.`
895
867
  );
896
868
  }
897
- const r = `0x${hex.slice(0, 64)}`;
869
+ const r = hex.slice(0, 64);
898
870
  if (hex.length === 128) {
899
871
  const yParityAndS = BigInt(`0x${hex.slice(64, 128)}`);
900
872
  const yParity = Number(yParityAndS >> 255n & 1n);
901
- const s2 = yParityAndS & (1n << 255n) - 1n;
902
- return { v: 27 + yParity, r, s: `0x${s2.toString(16).padStart(64, "0")}` };
873
+ const s2 = (yParityAndS & (1n << 255n) - 1n).toString(16).padStart(64, "0");
874
+ return `0x${r}${s2}${(27 + yParity).toString(16).padStart(2, "0")}`;
903
875
  }
904
- const s = `0x${hex.slice(64, 128)}`;
876
+ const s = hex.slice(64, 128);
905
877
  const raw = parseInt(hex.slice(128, 130), 16);
906
- if (raw === 27 || raw === 28) return { v: raw, r, s };
907
- if (raw === 0 || raw === 1) return { v: raw + 27, r, s };
878
+ if (raw === 27 || raw === 28) return `0x${r}${s}${raw.toString(16)}`;
879
+ if (raw === 0 || raw === 1) return `0x${r}${s}${(raw + 27).toString(16)}`;
908
880
  throw new Error(`[BitPal] x402: unexpected signature v=${raw} (expected 0, 1, 27 or 28).`);
909
881
  }
910
882
  function randomNonce() {
@@ -945,8 +917,8 @@ export {
945
917
  encodePaymentHeader,
946
918
  fromAtomic,
947
919
  fromAtomicUSDC,
920
+ normalizeSignature,
948
921
  parseWebhookEvent,
949
- splitSignature,
950
922
  toAtomic,
951
923
  toAtomicUSDC,
952
924
  verifyWebhookSignature,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitpal/checkout",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "BitPal Checkout SDK — non-custodial crypto checkout: create sessions, issue per-order deposit addresses, poll status, verify signed webhooks, and charge per API call with x402.",
5
5
  "license": "MIT",
6
6
  "repository": {