@bitpal/checkout 0.3.0 → 0.5.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 CHANGED
@@ -1,15 +1,80 @@
1
+ // src/catalog.ts
2
+ function toQuery(params) {
3
+ if (!params) return "";
4
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
5
+ if (entries.length === 0) return "";
6
+ const qs = new URLSearchParams(entries.map(([k, v]) => [k, String(v)]));
7
+ return `?${qs.toString()}`;
8
+ }
9
+ var ProductsResource = class {
10
+ constructor(request) {
11
+ this.request = request;
12
+ }
13
+ request;
14
+ /** POST /v1/checkout/products */
15
+ create(params) {
16
+ return this.request("POST", "/v1/checkout/products", params);
17
+ }
18
+ /** GET /v1/checkout/products */
19
+ list(params) {
20
+ return this.request("GET", `/v1/checkout/products${toQuery(params)}`);
21
+ }
22
+ /** GET /v1/checkout/products/:id */
23
+ retrieve(productId) {
24
+ return this.request("GET", `/v1/checkout/products/${encodeURIComponent(productId)}`);
25
+ }
26
+ /** PATCH /v1/checkout/products/:id */
27
+ update(productId, params) {
28
+ return this.request("PATCH", `/v1/checkout/products/${encodeURIComponent(productId)}`, params);
29
+ }
30
+ };
31
+ var PricesResource = class {
32
+ constructor(request) {
33
+ this.request = request;
34
+ }
35
+ request;
36
+ /** POST /v1/checkout/products/:id/prices */
37
+ create(productId, params) {
38
+ return this.request("POST", `/v1/checkout/products/${encodeURIComponent(productId)}/prices`, params);
39
+ }
40
+ /** GET /v1/checkout/products/:id/prices */
41
+ list(productId, params) {
42
+ return this.request("GET", `/v1/checkout/products/${encodeURIComponent(productId)}/prices${toQuery(params)}`);
43
+ }
44
+ /** GET /v1/checkout/prices/:id */
45
+ retrieve(priceId) {
46
+ return this.request("GET", `/v1/checkout/prices/${encodeURIComponent(priceId)}`);
47
+ }
48
+ /**
49
+ * PATCH /v1/checkout/prices/:id — nickname(표시 메타)만.
50
+ * 금액류 필드(amount_atomic 등)는 immutable — 서버가 PRICE_IMMUTABLE_FIELD로 거부한다.
51
+ * 가격 변경은 `prices.create()`로 새 price를 만들 것. "그만 팔기"는 해당 Price 를 쓰는
52
+ * Payment Link 를 inactive 로 한다(Price 자체에는 active/inactive 개념이 없다).
53
+ */
54
+ update(priceId, params) {
55
+ return this.request("PATCH", `/v1/checkout/prices/${encodeURIComponent(priceId)}`, params);
56
+ }
57
+ };
58
+
1
59
  // src/client.ts
2
60
  var DEFAULT_BASE_URL = "https://api.bitpal.io";
3
61
  var DEFAULT_TIMEOUT = 3e4;
4
62
  var BitPalCheckoutClient = class {
63
+ /** 서버 메서드용 secret 키. 공개(buyer-facing) 메서드는 없이도 동작. */
5
64
  apiKey;
6
65
  baseUrl;
7
66
  timeout;
67
+ /** PROD-LINK-3a-SDK: Product 카탈로그 관리 (server-side 전용) */
68
+ products;
69
+ /** PROD-LINK-3a-SDK: immutable Price 관리 — 가격 변경은 비활성화 + 새 price 생성 */
70
+ prices;
8
71
  constructor(config) {
9
- if (!config.apiKey) throw new Error("apiKey is required");
10
72
  this.apiKey = config.apiKey;
11
73
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
12
74
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
75
+ const requester = (method, path, body) => this.request(method, path, body);
76
+ this.products = new ProductsResource(requester);
77
+ this.prices = new PricesResource(requester);
13
78
  }
14
79
  /* ─── 내부 HTTP ─── */
15
80
  async request(method, path, body, extraHeaders) {
@@ -21,7 +86,8 @@ var BitPalCheckoutClient = class {
21
86
  method,
22
87
  headers: {
23
88
  "Content-Type": "application/json",
24
- Authorization: `Bearer ${this.apiKey}`,
89
+ // 공개 메서드(session_token 기반)는 apiKey 없이 호출 가능 — 있을 때만 Authorization 첨부.
90
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
25
91
  ...extraHeaders ?? {}
26
92
  },
27
93
  body: body ? JSON.stringify(body) : void 0,
@@ -51,8 +117,18 @@ var BitPalCheckoutClient = class {
51
117
  * line_items: [{ name: 'Pro', amount: toAtomicUSDC('29.00'), currency: 'USDC' }],
52
118
  * });
53
119
  * console.log(data.url); // → buyer를 redirect할 결제 페이지
120
+ *
121
+ * // 멀티체인 허용 — 구매자가 Base/Optimism 중 선택(하나뿐이면 자동 선택):
122
+ * await bp.checkout.createSession({
123
+ * line_items: [{ name: 'Pro', amount: toAtomicUSDC('29.00'), currency: 'USDC' }],
124
+ * allowed_pay_chains: ['eip155:84532', 'eip155:11155420'],
125
+ * });
126
+ * // 단일 체인 고정은 pay_chain 사용(둘은 상호 배타).
54
127
  */
55
128
  async createSession(params) {
129
+ if (params.pay_chain && params.allowed_pay_chains && params.allowed_pay_chains.length > 0) {
130
+ throw new Error("[BitPal] pay_chain and allowed_pay_chains are mutually exclusive \u2014 use only one.");
131
+ }
56
132
  const res = await this.request(
57
133
  "POST",
58
134
  "/v1/checkout/sessions",
@@ -86,148 +162,56 @@ var BitPalCheckoutClient = class {
86
162
  `/v1/checkout/sessions${query ? `?${query}` : ""}`
87
163
  );
88
164
  }
89
- /* ─── Refunds ─── */
90
- /**
91
- * Auto refund — escrow 보관 중일 때 컨트랙트가 buyer에게 직접 transfer.
92
- *
93
- * `params.amount` 미지정 시 backend가 잔액 전액(`type: 'full'`). 명시 시 `type: 'partial'`.
94
- * **Idempotency-Key 권장** — 같은 키로 재시도 시 backend가 같은 결과 반환.
95
- *
96
- * 머천트가 `frozen` 상태면 환불은 허용되지만, `suspended` 상태에서는 차단된다.
97
- *
98
- * @example
99
- * ```ts
100
- * import { randomUUID } from 'node:crypto';
101
- * const { data } = await bitpal.checkout.refundSession(
102
- * sessionId,
103
- * { amount: toAtomicUSDC('5.00'), reason: 'customer_requested' },
104
- * { idempotencyKey: randomUUID() },
105
- * );
106
- * console.log(data.txHash); // on-chain tx hash (또는 simulation 환경에서 sim_xxx)
107
- * ```
108
- */
109
- async refundSession(sessionId, params = {}, opts) {
110
- return this.request(
111
- "POST",
112
- `/v1/checkout/sessions/${encodeURIComponent(sessionId)}/refund`,
113
- params,
114
- opts?.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : void 0
115
- );
116
- }
165
+ /* ─── 비수탁 결제 (deposit-address) ─── */
117
166
  /**
118
- * Record refund — `status='settled'` 후 머천트가 외부 지갑에서 직접 송금하고 tx_hash를 입력.
119
- * 컨트랙트 호출 없음 (DB-only).
120
- *
121
- * **Idempotency-Key 권장**.
167
+ * 공개 세션 조회 (`GET /v1/checkout/sessions/:id/public`) — 인증 불요.
122
168
  *
123
- * @example
124
- * ```ts
125
- * await bitpal.checkout.recordRefund(
126
- * sessionId,
127
- * {
128
- * refund_amount: toAtomicUSDC('5.00'),
129
- * tx_hash: '0xabc...',
130
- * reason: 'manual_offchain_refund',
131
- * },
132
- * { idempotencyKey: randomUUID() },
133
- * );
134
- * ```
169
+ * buyer-facing 결제 페이지가 표시/자산선택에 쓰는 서버 권위 뷰. `payment_options` 로
170
+ * 구매자 선택 가능한 chain×token 을 얻고, 발급 후에는 `deposit_address` 가 채워진다.
135
171
  */
136
- async recordRefund(sessionId, params, opts) {
137
- return this.request(
138
- "POST",
139
- `/v1/checkout/sessions/${encodeURIComponent(sessionId)}/record-refund`,
140
- params,
141
- opts?.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : void 0
142
- );
143
- }
144
- /** 세션별 환불 내역 (auto + record 통합) */
145
- async listRefunds(sessionId) {
172
+ async getPublicSession(sessionId) {
146
173
  return this.request(
147
174
  "GET",
148
- `/v1/checkout/sessions/${encodeURIComponent(sessionId)}/refunds`
175
+ `/v1/checkout/sessions/${encodeURIComponent(sessionId)}/public`
149
176
  );
150
177
  }
151
- /* ─── External On-chain Payments ─── */
152
178
  /**
153
- * CheckoutEscrow에 직접 입금한 외부 결제를 BitPal에 제출·검증.
179
+ * 주문별 비수탁 입금주소 발급 (`POST /v1/checkout/sessions/:id/deposit-address`).
154
180
  *
155
- * Buyer가 merchant UI/SDK에서 `CheckoutEscrow.deposit*()` 함수를 직접 호출한 뒤
156
- * 결과 tx_hash를 이 메서드로 제출한다. BitPal은 receipt를 fetch하여
157
- * `PaymentDeposited` 이벤트를 검증하고 세션 상태를 갱신한다.
181
+ * 비수탁 결제 진입점. 구매자가 자산(멀티옵션이면 chain/token)과 환불 주소를 확정하면 서버가
182
+ * 그 주문 전용 CREATE2 forwarder 주소를 돌려준다. 구매자는 아무 지갑/거래소에서 **정확한 금액**을
183
+ * 그 주소로 일반 송금하면 된다(서명·연결 불필요). watcher 가 감지 → keeper 가 정산.
158
184
  *
159
- * **인증 방식:**
160
- * - Merchant server (API key): `Authorization: Bearer bp_test_*` 헤더 자동 첨부
161
- * - Buyer 측 (session_token): `params.session_token` 전달
185
+ * refundAddress 는 입금주소에 커밋되어 발급 후 변경 불가. session_token 은 createSession 응답값.
162
186
  *
163
187
  * @example
164
188
  * ```ts
165
- * // Merchant server — API key 인증
166
- * const { data } = await bitpal.checkout.submitExternalPayment(sessionId, {
167
- * tx_hash: '0xabc...',
168
- * chain: 'base',
189
+ * const { data } = await bitpal.checkout.issueDepositAddress(sessionId, {
190
+ * session_token: session.session_token!,
191
+ * refundAddress: '0xBuyerWallet...',
192
+ * chain: 'eip155:8453', // 멀티옵션 세션만 필요
193
+ * token: 'USDC',
169
194
  * });
170
- * console.log(data.validation_status); // 'valid' | 'underpaid' | 'overpaid' | ...
171
- * console.log(data.session_status); // 'paid' | 'unresolved_underpaid' | ...
195
+ * console.log(data.depositAddress, data.expectedAmount); // 이 주소로 정확 금액 송금
172
196
  * ```
173
197
  */
174
- async submitExternalPayment(sessionId, params, opts) {
198
+ async issueDepositAddress(sessionId, params) {
175
199
  return this.request(
176
200
  "POST",
177
- `/v1/checkout/sessions/${encodeURIComponent(sessionId)}/submit-external-payment`,
178
- params,
179
- opts?.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : void 0
180
- );
181
- }
182
- /**
183
- * Gasless checkout payment through the BitPal API relayer.
184
- *
185
- * The buyer signs **two** EIP-712 typed-data structures and the API calls
186
- * `CheckoutEscrow.depositWithAuthorization()` with the platform relayer:
187
- *
188
- * 1. **EIP-3009 USDC TransferWithAuthorization** (`params.authorization`) —
189
- * authorizes USDC transfer from buyer to escrow.
190
- * 2. **PaymentAuthorization** (`params.payment_auth`, since SDK v0.2.0) —
191
- * binds the entire payment intent (paymentId, merchant, amount, fee,
192
- * reserve, duration) to the buyer's signature. Without this, a compromised
193
- * gateway key could redirect funds to an attacker-controlled merchant.
194
- *
195
- * Build the PaymentAuthorization typed-data with `buildPaymentAuthTypedData()`.
196
- * The PaymentAuthorization nonce comes from `CheckoutEscrow.paymentAuthNonce(payer)`.
197
- *
198
- * Use the returned session/order state only as immediate UX feedback; the
199
- * merchant backend should finalize fulfillment from signed webhooks.
200
- *
201
- * @breaking-change v0.2.0 — `payment_auth` field is now required. v0.1.x clients
202
- * must upgrade their signing flow. See MIGRATION_v0.2.md.
203
- */
204
- async payWithAuthorization(sessionId, params, opts) {
205
- return this.request(
206
- "POST",
207
- `/v1/checkout/sessions/${encodeURIComponent(sessionId)}/submit-payment`,
208
- params,
209
- opts?.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : void 0
201
+ `/v1/checkout/sessions/${encodeURIComponent(sessionId)}/deposit-address`,
202
+ params
210
203
  );
211
204
  }
212
205
  /**
213
- * 세션의 external on-chain 결제 검증 기록 목록 조회.
214
- *
215
- * `getSession()`의 `payment_observations` 필드와 동일한 데이터를 반환하지만
216
- * 검증 기록만 명시적으로 조회할 때 사용한다.
206
+ * 경량 상태 폴링 (`GET /v1/checkout/sessions/:id/status`) — 인증 불요.
217
207
  *
218
- * @example
219
- * ```ts
220
- * const { data } = await bitpal.checkout.getExternalPaymentStatus(sessionId);
221
- * const latest = data[0];
222
- * if (latest?.finality_status === 'final_confirmed') {
223
- * // 12 confirmations 완료 — 결제 확정
224
- * }
225
- * ```
208
+ * status: awaiting_deposit → deposit_detected → paid_confirmed/swept. 확정/정산은 webhook 으로도
209
+ * 통지되므로, 서버 사이드 확정은 webhook 을 신뢰(폴링은 buyer UX 용).
226
210
  */
227
- async getExternalPaymentStatus(sessionId) {
211
+ async getStatus(sessionId) {
228
212
  return this.request(
229
213
  "GET",
230
- `/v1/checkout/sessions/${encodeURIComponent(sessionId)}/payment-observations`
214
+ `/v1/checkout/sessions/${encodeURIComponent(sessionId)}/status`
231
215
  );
232
216
  }
233
217
  /* ─── Fees ─── */
@@ -251,17 +235,12 @@ var BitPalError = class extends Error {
251
235
  // src/webhook.ts
252
236
  var CHECKOUT_WEBHOOK_EVENTS = [
253
237
  "checkout.session.created",
238
+ "checkout.session.deposit_detected",
254
239
  "checkout.session.paid",
255
240
  "checkout.session.settled",
256
241
  "checkout.session.expired",
257
242
  "checkout.session.unresolved",
258
- "checkout.session.release_deferred",
259
- "checkout.payment.failed",
260
- "checkout.payment.finalized",
261
- "checkout.payment.revoked",
262
- "checkout.refund.recorded",
263
- "checkout.refund.completed",
264
- "checkout.refund.partial"
243
+ "checkout.refund.recorded"
265
244
  ];
266
245
  var WEBHOOK_HEADERS = {
267
246
  signature: "X-Webhook-Signature-256",
@@ -271,14 +250,21 @@ var WEBHOOK_HEADERS = {
271
250
  deliveryId: "X-Webhook-Delivery-Id"
272
251
  };
273
252
  var SIGNATURE_PREFIX = "sha256=";
274
- async function verifyWebhookSignature(payload, signature, secret) {
253
+ async function verifyWebhookSignature(payload, signature, secret, opts) {
275
254
  if (!payload || !signature || !secret) return false;
276
255
  if (!signature.startsWith(SIGNATURE_PREFIX)) return false;
256
+ const timestamp = opts?.timestamp;
257
+ if (!timestamp) return false;
258
+ const parsedMs = Date.parse(timestamp);
259
+ if (!Number.isFinite(parsedMs)) return false;
260
+ if (new Date(parsedMs).toISOString() !== timestamp) return false;
261
+ const toleranceMs = (opts?.toleranceSec ?? 300) * 1e3;
262
+ if (Math.abs(Date.now() - parsedMs) > toleranceMs) return false;
277
263
  const sigHex = signature.slice(SIGNATURE_PREFIX.length);
278
- if (!/^[0-9a-f]+$/.test(sigHex)) return false;
264
+ if (!/^[0-9a-f]{64}$/.test(sigHex)) return false;
279
265
  try {
280
266
  const crypto = await import("crypto");
281
- const expectedHex = crypto.createHmac("sha256", secret).update(payload, "utf8").digest("hex");
267
+ const expectedHex = crypto.createHmac("sha256", secret).update(`${timestamp}.${payload}`, "utf8").digest("hex");
282
268
  if (sigHex.length !== expectedHex.length) return false;
283
269
  return crypto.timingSafeEqual(
284
270
  Buffer.from(sigHex, "hex"),
@@ -303,7 +289,7 @@ function toAtomicUSDC(decimal) {
303
289
  if (typeof decimal !== "string" || !/^\d+(\.\d{1,6})?$/.test(decimal)) {
304
290
  throw new Error(`toAtomicUSDC: invalid decimal '${decimal}' (max 6 fractional digits, no negatives)`);
305
291
  }
306
- const [whole, frac = ""] = decimal.split(".");
292
+ const [whole = "0", frac = ""] = decimal.split(".");
307
293
  const fracPadded = frac.padEnd(Number(USDC_DECIMALS), "0");
308
294
  return (BigInt(whole) * USDC_SCALE + BigInt(fracPadded || "0")).toString();
309
295
  }
@@ -318,74 +304,6 @@ function fromAtomicUSDC(atomic) {
318
304
  return fracStr.length > 0 ? `${whole}.${fracStr.padEnd(2, "0")}` : `${whole}.00`;
319
305
  }
320
306
 
321
- // src/typed-data.ts
322
- function buildPaymentAuthTypedData(args) {
323
- return {
324
- domain: {
325
- name: "CheckoutEscrow",
326
- version: "1",
327
- chainId: args.chainId,
328
- verifyingContract: args.escrowAddress
329
- },
330
- types: {
331
- PaymentAuthorization: [
332
- { name: "paymentId", type: "bytes32" },
333
- { name: "payer", type: "address" },
334
- { name: "merchant", type: "address" },
335
- { name: "token", type: "address" },
336
- { name: "amount", type: "uint256" },
337
- { name: "feeAmount", type: "uint256" },
338
- { name: "reserveAmount", type: "uint256" },
339
- { name: "duration", type: "uint256" },
340
- { name: "deadline", type: "uint256" },
341
- { name: "nonce", type: "uint256" }
342
- ]
343
- },
344
- primaryType: "PaymentAuthorization",
345
- message: {
346
- paymentId: args.paymentId,
347
- payer: args.payer,
348
- merchant: args.merchant,
349
- token: args.token,
350
- amount: BigInt(args.amount),
351
- feeAmount: BigInt(args.feeAmount),
352
- reserveAmount: BigInt(args.reserveAmount),
353
- duration: BigInt(args.duration),
354
- deadline: BigInt(args.deadline),
355
- nonce: BigInt(args.nonce)
356
- }
357
- };
358
- }
359
- function buildConfirmReceiptTypedData(args) {
360
- return {
361
- domain: {
362
- name: "CheckoutEscrow",
363
- version: "1",
364
- chainId: args.chainId,
365
- verifyingContract: args.escrowAddress
366
- },
367
- types: {
368
- ConfirmReceipt: [
369
- { name: "paymentId", type: "bytes32" },
370
- { name: "payer", type: "address" },
371
- { name: "deadline", type: "uint256" },
372
- { name: "nonce", type: "uint256" }
373
- ]
374
- },
375
- primaryType: "ConfirmReceipt",
376
- message: {
377
- paymentId: args.paymentId,
378
- payer: args.payer,
379
- deadline: BigInt(args.deadline),
380
- nonce: BigInt(args.nonce)
381
- }
382
- };
383
- }
384
- function sessionIdToPaymentId(sessionId) {
385
- const hex = Array.from(new TextEncoder().encode(sessionId)).map((b) => b.toString(16).padStart(2, "0")).join("").padEnd(64, "0");
386
- return `0x${hex}`;
387
- }
388
-
389
307
  // src/index.ts
390
308
  var BitPal = class {
391
309
  checkout;
@@ -399,12 +317,11 @@ export {
399
317
  BitPalCheckoutClient,
400
318
  BitPalError,
401
319
  CHECKOUT_WEBHOOK_EVENTS,
320
+ PricesResource,
321
+ ProductsResource,
402
322
  WEBHOOK_HEADERS,
403
- buildConfirmReceiptTypedData,
404
- buildPaymentAuthTypedData,
405
323
  fromAtomicUSDC,
406
324
  parseWebhookEvent,
407
- sessionIdToPaymentId,
408
325
  toAtomicUSDC,
409
326
  verifyWebhookSignature
410
327
  };
package/package.json CHANGED
@@ -1,21 +1,35 @@
1
1
  {
2
2
  "name": "@bitpal/checkout",
3
- "version": "0.3.0",
4
- "description": "BitPal Checkout SDK — crypto payment sessions, external on-chain payment verification, gasless checkout (with PaymentAuthorization double-defense), refunds, fee preview.",
5
- "license": "UNLICENSED",
3
+ "version": "0.5.0",
4
+ "description": "BitPal Checkout SDK — non-custodial crypto checkout: create sessions, issue per-order deposit addresses, poll status, verify signed webhooks.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/superlabs-x/bitpal-checkout-sdk.git"
9
+ },
10
+ "homepage": "https://github.com/superlabs-x/bitpal-checkout-sdk#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/superlabs-x/bitpal-checkout-sdk/issues"
13
+ },
6
14
  "type": "module",
7
- "main": "dist/index.js",
8
- "types": "dist/index.d.ts",
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
9
18
  "exports": {
10
19
  ".": {
11
- "import": "./dist/index.js",
12
- "types": "./dist/index.d.ts"
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
13
28
  }
14
29
  },
15
30
  "files": [
16
31
  "dist",
17
- "README.md",
18
- "MIGRATION_v0.2.md"
32
+ "README.md"
19
33
  ],
20
34
  "keywords": [
21
35
  "bitpal",
@@ -27,7 +41,7 @@
27
41
  "evm",
28
42
  "base",
29
43
  "stablecoin",
30
- "escrow"
44
+ "non-custodial"
31
45
  ],
32
46
  "publishConfig": {
33
47
  "access": "public"
@@ -35,15 +49,17 @@
35
49
  "engines": {
36
50
  "node": ">=18"
37
51
  },
52
+ "scripts": {
53
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
54
+ "lint": "tsc --noEmit",
55
+ "test": "vitest run",
56
+ "prepublishOnly": "npm test && npm run build",
57
+ "release": "npm publish --access public"
58
+ },
38
59
  "devDependencies": {
60
+ "@types/node": "^20.11.0",
39
61
  "tsup": "^8.5.1",
40
62
  "typescript": "^5.5.0",
41
63
  "vitest": "^2.1.9"
42
- },
43
- "scripts": {
44
- "build": "tsup src/index.ts --format esm --dts --clean",
45
- "lint": "tsc --noEmit",
46
- "test": "vitest run",
47
- "release": "pnpm publish --access public --no-git-checks"
48
64
  }
49
- }
65
+ }
package/MIGRATION_v0.2.md DELETED
@@ -1,135 +0,0 @@
1
- # @bitpal/checkout — Migration Guide v0.1.x → v0.2.0
2
-
3
- > Audit doc 11 §3 C-01 option 2 (PaymentAuthorization 이중방어) 적용으로 인한 SDK 메이저 bump.
4
-
5
- ## Why this change
6
-
7
- v0.1.x 까지의 `payWithAuthorization` 흐름은 buyer 의 EIP-3009 USDC sig 만 받았다. 이 sig 는
8
- `(from, to, value, validAfter, validBefore, nonce)` 만 바인딩하므로, **paymentId / merchant /
9
- amount / fee / reserve / duration** 은 호출자(BitPal gateway)가 자유롭게 지정 가능했다.
10
-
11
- → gateway 키 탈취 시 공격자가 (a) buyer 의 EIP-3009 sig 입수 후 (b) 자기 주소를 merchant 로 설정,
12
- (c) `depositWithAuthorization` 호출 → buyer 자금이 attacker merchant 로 흘러간다.
13
-
14
- v0.2.0 는 buyer 가 **결제 의도 전체** 에 별도 EIP-712 서명 (`PaymentAuthorization`) 을 한다. gateway
15
- 키 탈취 시에도 buyer 의도 밖 결제 생성 불가 (이중방어).
16
-
17
- ## Breaking changes
18
-
19
- ### `BitPal.checkout.payWithAuthorization(sessionId, params)`
20
-
21
- `params.payment_auth` 필드가 **필수** 가 됐다.
22
-
23
- **Before (v0.1.x):**
24
- ```ts
25
- await bp.checkout.payWithAuthorization(sessionId, {
26
- session_token: 'xyz...',
27
- chain: 'base',
28
- authorization: {
29
- from: buyer.address,
30
- validAfter: '0',
31
- validBefore: String(Math.floor(Date.now()/1000) + 3600),
32
- authNonce: '0x...',
33
- v: 27, r: '0x...', s: '0x...',
34
- },
35
- });
36
- ```
37
-
38
- **After (v0.2.0):**
39
- ```ts
40
- import {
41
- buildPaymentAuthTypedData,
42
- sessionIdToPaymentId,
43
- } from '@bitpal/checkout';
44
-
45
- // 1. PaymentAuthorization nonce 조회 (CheckoutEscrow.paymentAuthNonce)
46
- const nonce = await escrow.read.paymentAuthNonce([buyer.address]);
47
-
48
- // 2. PaymentAuthorization typed-data sig
49
- const paymentAuthDeadline = Math.floor(Date.now()/1000) + 3600;
50
- const paymentId = sessionIdToPaymentId(sessionId);
51
- const paymentAuthTd = buildPaymentAuthTypedData({
52
- escrowAddress: escrow.address,
53
- chainId: 8453,
54
- paymentId,
55
- payer: buyer.address,
56
- merchant: session.seller_wallet,
57
- token: session.token_address,
58
- amount: session.amount_total,
59
- feeAmount: '0',
60
- reserveAmount: '0',
61
- duration: '0',
62
- deadline: String(paymentAuthDeadline),
63
- nonce: String(nonce),
64
- });
65
- const paymentAuthSig = await wallet.signTypedData(paymentAuthTd);
66
- const paymentAuthParsed = parseSignature(paymentAuthSig); // viem helper
67
-
68
- // 3. USDC EIP-3009 sig (변경 없음 — 기존 흐름 그대로)
69
- const usdcSig = await wallet.signTypedData({ /* TransferWithAuthorization ... */ });
70
- const usdcParsed = parseSignature(usdcSig);
71
-
72
- // 4. payWithAuthorization 호출 — payment_auth 필드 추가
73
- await bp.checkout.payWithAuthorization(sessionId, {
74
- session_token: 'xyz...',
75
- chain: 'base',
76
- authorization: {
77
- from: buyer.address,
78
- validAfter: '0',
79
- validBefore: String(Math.floor(Date.now()/1000) + 3600),
80
- authNonce: '0x...',
81
- v: Number(usdcParsed.v), r: usdcParsed.r, s: usdcParsed.s,
82
- },
83
- // ← 신규 필수
84
- payment_auth: {
85
- deadline: String(paymentAuthDeadline),
86
- duration: '0',
87
- v: Number(paymentAuthParsed.v),
88
- r: paymentAuthParsed.r,
89
- s: paymentAuthParsed.s,
90
- },
91
- });
92
- ```
93
-
94
- ### 백엔드 응답에서 `payment_auth_nonce` 노출
95
-
96
- 서버는 `GET /v1/checkout/sessions/:id/public` 응답에 `payment_auth_nonce` 필드를 포함한다
97
- (Q5 적용 후). buyer UI 가 컨트랙트를 직접 호출하지 않고도 nonce 를 얻을 수 있다. 단,
98
- `merchant_wallet` (seller_wallet) 도 sig 에 들어가므로 public response 에서 확인할 수 있어야 한다.
99
-
100
- ## Non-breaking changes
101
-
102
- 다음 SDK 흐름은 변경 없음:
103
-
104
- - `createSession()`, `getSession()`, `listSessions()`, `previewFee()`
105
- - `refundSession()`, `recordRefund()`, `listRefunds()`
106
- - `submitExternalPayment()` (Mode A — buyer 가 본인 지갑으로 직접 deposit)
107
- - Webhook helpers (`verifyWebhookSignature`, `parseWebhookEvent`)
108
- - Amount helpers (`toAtomicUSDC`, `fromAtomicUSDC`)
109
-
110
- ## Why `payment_auth` is required (no opt-out)
111
-
112
- Q4 PR 적용 후 `CheckoutEscrow.depositWithAuthorization` 가 PaymentAuthorization sig 를 강제한다
113
- (컨트랙트 require). SDK 가 sig 없이 호출 시 백엔드가 `PAYMENT_AUTH_REQUIRED` (400) 로 거부.
114
-
115
- ## Compatibility matrix
116
-
117
- | SDK version | Backend (CheckoutEscrow contract) | Result |
118
- |---|---|---|
119
- | v0.1.x | post-Q5 (PaymentAuthorization 필수) | ❌ `PAYMENT_AUTH_REQUIRED` 400 |
120
- | v0.2.0 | post-Q5 | ✅ OK |
121
- | v0.2.0 | pre-Q5 (legacy 미배포 contract) | ❌ contract revert (signature 추가 인자 없음) |
122
-
123
- → SDK v0.2.0 와 컨트랙트 Q4+Q5 적용은 **동시 배포** 가 원칙. testnet 부터 함께 갈아끼움.
124
-
125
- ## New helpers
126
-
127
- - `buildPaymentAuthTypedData(args)` — `signTypedData` 호환 객체 생성
128
- - `buildConfirmReceiptTypedData(args)` — Q4 H-01 `confirmReceiptWithSignature` 용 (off-chain receipt confirmation 직접 구현 시 사용. 기본 흐름에서는 BitPal hosted page 가 처리)
129
- - `sessionIdToPaymentId(sessionId)` — 32-byte hex 변환 (= 백엔드 `toBytes32()`)
130
-
131
- ## See also
132
-
133
- - `docs/_dan/b2b-spec/2026-05-14-11-smart-contract-security-audit.md` §3 C-01
134
- - `docs/_dan/b2b-spec/2026-05-18-14-post-audit-remaining-hardening-plan.md` §4 (PR-C 실행 가이드)
135
- - README.md §"Gasless lane (Hosted / Gasless)"