@bitpal/checkout 0.2.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/LICENSE +21 -0
- package/README.md +221 -392
- package/dist/index.cjs +374 -0
- package/dist/index.d.cts +566 -0
- package/dist/index.d.ts +266 -411
- package/dist/index.js +118 -201
- package/package.json +32 -16
- package/MIGRATION_v0.2.md +0 -135
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BitPal: () => BitPal,
|
|
34
|
+
BitPalCheckoutClient: () => BitPalCheckoutClient,
|
|
35
|
+
BitPalError: () => BitPalError,
|
|
36
|
+
CHECKOUT_WEBHOOK_EVENTS: () => CHECKOUT_WEBHOOK_EVENTS,
|
|
37
|
+
PricesResource: () => PricesResource,
|
|
38
|
+
ProductsResource: () => ProductsResource,
|
|
39
|
+
WEBHOOK_HEADERS: () => WEBHOOK_HEADERS,
|
|
40
|
+
fromAtomicUSDC: () => fromAtomicUSDC,
|
|
41
|
+
parseWebhookEvent: () => parseWebhookEvent,
|
|
42
|
+
toAtomicUSDC: () => toAtomicUSDC,
|
|
43
|
+
verifyWebhookSignature: () => verifyWebhookSignature
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(index_exports);
|
|
46
|
+
|
|
47
|
+
// src/catalog.ts
|
|
48
|
+
function toQuery(params) {
|
|
49
|
+
if (!params) return "";
|
|
50
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0);
|
|
51
|
+
if (entries.length === 0) return "";
|
|
52
|
+
const qs = new URLSearchParams(entries.map(([k, v]) => [k, String(v)]));
|
|
53
|
+
return `?${qs.toString()}`;
|
|
54
|
+
}
|
|
55
|
+
var ProductsResource = class {
|
|
56
|
+
constructor(request) {
|
|
57
|
+
this.request = request;
|
|
58
|
+
}
|
|
59
|
+
request;
|
|
60
|
+
/** POST /v1/checkout/products */
|
|
61
|
+
create(params) {
|
|
62
|
+
return this.request("POST", "/v1/checkout/products", params);
|
|
63
|
+
}
|
|
64
|
+
/** GET /v1/checkout/products */
|
|
65
|
+
list(params) {
|
|
66
|
+
return this.request("GET", `/v1/checkout/products${toQuery(params)}`);
|
|
67
|
+
}
|
|
68
|
+
/** GET /v1/checkout/products/:id */
|
|
69
|
+
retrieve(productId) {
|
|
70
|
+
return this.request("GET", `/v1/checkout/products/${encodeURIComponent(productId)}`);
|
|
71
|
+
}
|
|
72
|
+
/** PATCH /v1/checkout/products/:id */
|
|
73
|
+
update(productId, params) {
|
|
74
|
+
return this.request("PATCH", `/v1/checkout/products/${encodeURIComponent(productId)}`, params);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var PricesResource = class {
|
|
78
|
+
constructor(request) {
|
|
79
|
+
this.request = request;
|
|
80
|
+
}
|
|
81
|
+
request;
|
|
82
|
+
/** POST /v1/checkout/products/:id/prices */
|
|
83
|
+
create(productId, params) {
|
|
84
|
+
return this.request("POST", `/v1/checkout/products/${encodeURIComponent(productId)}/prices`, params);
|
|
85
|
+
}
|
|
86
|
+
/** GET /v1/checkout/products/:id/prices */
|
|
87
|
+
list(productId, params) {
|
|
88
|
+
return this.request("GET", `/v1/checkout/products/${encodeURIComponent(productId)}/prices${toQuery(params)}`);
|
|
89
|
+
}
|
|
90
|
+
/** GET /v1/checkout/prices/:id */
|
|
91
|
+
retrieve(priceId) {
|
|
92
|
+
return this.request("GET", `/v1/checkout/prices/${encodeURIComponent(priceId)}`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* PATCH /v1/checkout/prices/:id — nickname(표시 메타)만.
|
|
96
|
+
* 금액류 필드(amount_atomic 등)는 immutable — 서버가 PRICE_IMMUTABLE_FIELD로 거부한다.
|
|
97
|
+
* 가격 변경은 `prices.create()`로 새 price를 만들 것. "그만 팔기"는 해당 Price 를 쓰는
|
|
98
|
+
* Payment Link 를 inactive 로 한다(Price 자체에는 active/inactive 개념이 없다).
|
|
99
|
+
*/
|
|
100
|
+
update(priceId, params) {
|
|
101
|
+
return this.request("PATCH", `/v1/checkout/prices/${encodeURIComponent(priceId)}`, params);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/client.ts
|
|
106
|
+
var DEFAULT_BASE_URL = "https://api.bitpal.io";
|
|
107
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
108
|
+
var BitPalCheckoutClient = class {
|
|
109
|
+
/** 서버 메서드용 secret 키. 공개(buyer-facing) 메서드는 없이도 동작. */
|
|
110
|
+
apiKey;
|
|
111
|
+
baseUrl;
|
|
112
|
+
timeout;
|
|
113
|
+
/** PROD-LINK-3a-SDK: Product 카탈로그 관리 (server-side 전용) */
|
|
114
|
+
products;
|
|
115
|
+
/** PROD-LINK-3a-SDK: immutable Price 관리 — 가격 변경은 비활성화 + 새 price 생성 */
|
|
116
|
+
prices;
|
|
117
|
+
constructor(config) {
|
|
118
|
+
this.apiKey = config.apiKey;
|
|
119
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
120
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
121
|
+
const requester = (method, path, body) => this.request(method, path, body);
|
|
122
|
+
this.products = new ProductsResource(requester);
|
|
123
|
+
this.prices = new PricesResource(requester);
|
|
124
|
+
}
|
|
125
|
+
/* ─── 내부 HTTP ─── */
|
|
126
|
+
async request(method, path, body, extraHeaders) {
|
|
127
|
+
const url = `${this.baseUrl}${path}`;
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch(url, {
|
|
132
|
+
method,
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": "application/json",
|
|
135
|
+
// 공개 메서드(session_token 기반)는 apiKey 없이 호출 가능 — 있을 때만 Authorization 첨부.
|
|
136
|
+
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
|
|
137
|
+
...extraHeaders ?? {}
|
|
138
|
+
},
|
|
139
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
140
|
+
signal: controller.signal
|
|
141
|
+
});
|
|
142
|
+
const json = await res.json();
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
const msg = json.error ?? `HTTP ${res.status}`;
|
|
145
|
+
throw new BitPalError(String(msg), res.status);
|
|
146
|
+
}
|
|
147
|
+
return json;
|
|
148
|
+
} finally {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/* ─── Sessions ─── */
|
|
153
|
+
/**
|
|
154
|
+
* 체크아웃 세션 생성.
|
|
155
|
+
*
|
|
156
|
+
* 모든 `line_items[].amount`는 atomic μUSDC 정수 문자열이어야 한다 ($1 = "1000000").
|
|
157
|
+
* decimal 표기는 거부된다 — `toAtomicUSDC()` 헬퍼 사용 권장.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* import { BitPal, toAtomicUSDC } from '@bitpal/checkout';
|
|
161
|
+
* const bp = new BitPal('bp_test_...');
|
|
162
|
+
* const { data } = await bp.checkout.createSession({
|
|
163
|
+
* line_items: [{ name: 'Pro', amount: toAtomicUSDC('29.00'), currency: 'USDC' }],
|
|
164
|
+
* });
|
|
165
|
+
* console.log(data.url); // → buyer를 redirect할 결제 페이지
|
|
166
|
+
*
|
|
167
|
+
* // 멀티체인 허용 — 구매자가 Base/Optimism 중 선택(하나뿐이면 자동 선택):
|
|
168
|
+
* await bp.checkout.createSession({
|
|
169
|
+
* line_items: [{ name: 'Pro', amount: toAtomicUSDC('29.00'), currency: 'USDC' }],
|
|
170
|
+
* allowed_pay_chains: ['eip155:84532', 'eip155:11155420'],
|
|
171
|
+
* });
|
|
172
|
+
* // 단일 체인 고정은 pay_chain 사용(둘은 상호 배타).
|
|
173
|
+
*/
|
|
174
|
+
async createSession(params) {
|
|
175
|
+
if (params.pay_chain && params.allowed_pay_chains && params.allowed_pay_chains.length > 0) {
|
|
176
|
+
throw new Error("[BitPal] pay_chain and allowed_pay_chains are mutually exclusive \u2014 use only one.");
|
|
177
|
+
}
|
|
178
|
+
const res = await this.request(
|
|
179
|
+
"POST",
|
|
180
|
+
"/v1/checkout/sessions",
|
|
181
|
+
params
|
|
182
|
+
);
|
|
183
|
+
if (res.data) {
|
|
184
|
+
if (res.data.checkout_url) {
|
|
185
|
+
res.data.url = res.data.checkout_url;
|
|
186
|
+
} else if (!res.data.url) {
|
|
187
|
+
res.data.url = `${this.baseUrl.replace("api.", "")}/pay/${res.data.id}`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return res;
|
|
191
|
+
}
|
|
192
|
+
/** 세션 조회 */
|
|
193
|
+
async getSession(sessionId) {
|
|
194
|
+
return this.request(
|
|
195
|
+
"GET",
|
|
196
|
+
`/v1/checkout/sessions/${sessionId}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
/** 세션 목록 조회 */
|
|
200
|
+
async listSessions(params) {
|
|
201
|
+
const qs = new URLSearchParams();
|
|
202
|
+
if (params?.status) qs.set("status", params.status);
|
|
203
|
+
if (params?.limit) qs.set("limit", String(params.limit));
|
|
204
|
+
if (params?.offset) qs.set("offset", String(params.offset));
|
|
205
|
+
const query = qs.toString();
|
|
206
|
+
return this.request(
|
|
207
|
+
"GET",
|
|
208
|
+
`/v1/checkout/sessions${query ? `?${query}` : ""}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
/* ─── 비수탁 결제 (deposit-address) ─── */
|
|
212
|
+
/**
|
|
213
|
+
* 공개 세션 조회 (`GET /v1/checkout/sessions/:id/public`) — 인증 불요.
|
|
214
|
+
*
|
|
215
|
+
* buyer-facing 결제 페이지가 표시/자산선택에 쓰는 서버 권위 뷰. `payment_options` 로
|
|
216
|
+
* 구매자 선택 가능한 chain×token 을 얻고, 발급 후에는 `deposit_address` 가 채워진다.
|
|
217
|
+
*/
|
|
218
|
+
async getPublicSession(sessionId) {
|
|
219
|
+
return this.request(
|
|
220
|
+
"GET",
|
|
221
|
+
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/public`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 주문별 비수탁 입금주소 발급 (`POST /v1/checkout/sessions/:id/deposit-address`).
|
|
226
|
+
*
|
|
227
|
+
* 비수탁 결제 진입점. 구매자가 자산(멀티옵션이면 chain/token)과 환불 주소를 확정하면 서버가
|
|
228
|
+
* 그 주문 전용 CREATE2 forwarder 주소를 돌려준다. 구매자는 아무 지갑/거래소에서 **정확한 금액**을
|
|
229
|
+
* 그 주소로 일반 송금하면 된다(서명·연결 불필요). watcher 가 감지 → keeper 가 정산.
|
|
230
|
+
*
|
|
231
|
+
* refundAddress 는 입금주소에 커밋되어 발급 후 변경 불가. session_token 은 createSession 응답값.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* const { data } = await bitpal.checkout.issueDepositAddress(sessionId, {
|
|
236
|
+
* session_token: session.session_token!,
|
|
237
|
+
* refundAddress: '0xBuyerWallet...',
|
|
238
|
+
* chain: 'eip155:8453', // 멀티옵션 세션만 필요
|
|
239
|
+
* token: 'USDC',
|
|
240
|
+
* });
|
|
241
|
+
* console.log(data.depositAddress, data.expectedAmount); // 이 주소로 정확 금액 송금
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
async issueDepositAddress(sessionId, params) {
|
|
245
|
+
return this.request(
|
|
246
|
+
"POST",
|
|
247
|
+
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/deposit-address`,
|
|
248
|
+
params
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 경량 상태 폴링 (`GET /v1/checkout/sessions/:id/status`) — 인증 불요.
|
|
253
|
+
*
|
|
254
|
+
* status: awaiting_deposit → deposit_detected → paid_confirmed/swept. 확정/정산은 webhook 으로도
|
|
255
|
+
* 통지되므로, 서버 사이드 확정은 webhook 을 신뢰(폴링은 buyer UX 용).
|
|
256
|
+
*/
|
|
257
|
+
async getStatus(sessionId) {
|
|
258
|
+
return this.request(
|
|
259
|
+
"GET",
|
|
260
|
+
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/status`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
/* ─── Fees ─── */
|
|
264
|
+
/** 수수료 미리보기 — `/v1/checkout/fee-preview` (Phase 1 credit system 폐기 후 canonical path) */
|
|
265
|
+
async previewFee(amount) {
|
|
266
|
+
return this.request(
|
|
267
|
+
"GET",
|
|
268
|
+
`/v1/checkout/fee-preview?amount=${encodeURIComponent(amount)}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
var BitPalError = class extends Error {
|
|
273
|
+
statusCode;
|
|
274
|
+
constructor(message, statusCode) {
|
|
275
|
+
super(message);
|
|
276
|
+
this.name = "BitPalError";
|
|
277
|
+
this.statusCode = statusCode;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// src/webhook.ts
|
|
282
|
+
var CHECKOUT_WEBHOOK_EVENTS = [
|
|
283
|
+
"checkout.session.created",
|
|
284
|
+
"checkout.session.deposit_detected",
|
|
285
|
+
"checkout.session.paid",
|
|
286
|
+
"checkout.session.settled",
|
|
287
|
+
"checkout.session.expired",
|
|
288
|
+
"checkout.session.unresolved",
|
|
289
|
+
"checkout.refund.recorded"
|
|
290
|
+
];
|
|
291
|
+
var WEBHOOK_HEADERS = {
|
|
292
|
+
signature: "X-Webhook-Signature-256",
|
|
293
|
+
timestamp: "X-Webhook-Timestamp",
|
|
294
|
+
event: "X-Webhook-Event",
|
|
295
|
+
eventId: "X-Webhook-Id",
|
|
296
|
+
deliveryId: "X-Webhook-Delivery-Id"
|
|
297
|
+
};
|
|
298
|
+
var SIGNATURE_PREFIX = "sha256=";
|
|
299
|
+
async function verifyWebhookSignature(payload, signature, secret, opts) {
|
|
300
|
+
if (!payload || !signature || !secret) return false;
|
|
301
|
+
if (!signature.startsWith(SIGNATURE_PREFIX)) return false;
|
|
302
|
+
const timestamp = opts?.timestamp;
|
|
303
|
+
if (!timestamp) return false;
|
|
304
|
+
const parsedMs = Date.parse(timestamp);
|
|
305
|
+
if (!Number.isFinite(parsedMs)) return false;
|
|
306
|
+
if (new Date(parsedMs).toISOString() !== timestamp) return false;
|
|
307
|
+
const toleranceMs = (opts?.toleranceSec ?? 300) * 1e3;
|
|
308
|
+
if (Math.abs(Date.now() - parsedMs) > toleranceMs) return false;
|
|
309
|
+
const sigHex = signature.slice(SIGNATURE_PREFIX.length);
|
|
310
|
+
if (!/^[0-9a-f]{64}$/.test(sigHex)) return false;
|
|
311
|
+
try {
|
|
312
|
+
const crypto = await import("crypto");
|
|
313
|
+
const expectedHex = crypto.createHmac("sha256", secret).update(`${timestamp}.${payload}`, "utf8").digest("hex");
|
|
314
|
+
if (sigHex.length !== expectedHex.length) return false;
|
|
315
|
+
return crypto.timingSafeEqual(
|
|
316
|
+
Buffer.from(sigHex, "hex"),
|
|
317
|
+
Buffer.from(expectedHex, "hex")
|
|
318
|
+
);
|
|
319
|
+
} catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function parseWebhookEvent(body) {
|
|
324
|
+
const parsed = JSON.parse(body);
|
|
325
|
+
if (typeof parsed.id !== "string" || typeof parsed.event !== "string" || typeof parsed.created_at !== "string" || typeof parsed.data !== "object" || parsed.data === null) {
|
|
326
|
+
throw new Error("Invalid webhook envelope \u2014 expected {id, event, created_at, data}");
|
|
327
|
+
}
|
|
328
|
+
return parsed;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/amount.ts
|
|
332
|
+
var USDC_DECIMALS = 6n;
|
|
333
|
+
var USDC_SCALE = 10n ** USDC_DECIMALS;
|
|
334
|
+
function toAtomicUSDC(decimal) {
|
|
335
|
+
if (typeof decimal !== "string" || !/^\d+(\.\d{1,6})?$/.test(decimal)) {
|
|
336
|
+
throw new Error(`toAtomicUSDC: invalid decimal '${decimal}' (max 6 fractional digits, no negatives)`);
|
|
337
|
+
}
|
|
338
|
+
const [whole = "0", frac = ""] = decimal.split(".");
|
|
339
|
+
const fracPadded = frac.padEnd(Number(USDC_DECIMALS), "0");
|
|
340
|
+
return (BigInt(whole) * USDC_SCALE + BigInt(fracPadded || "0")).toString();
|
|
341
|
+
}
|
|
342
|
+
function fromAtomicUSDC(atomic) {
|
|
343
|
+
if (typeof atomic !== "string" || !/^\d+$/.test(atomic)) {
|
|
344
|
+
throw new Error(`fromAtomicUSDC: invalid atomic '${atomic}' (must be non-negative integer string)`);
|
|
345
|
+
}
|
|
346
|
+
const big = BigInt(atomic);
|
|
347
|
+
const whole = big / USDC_SCALE;
|
|
348
|
+
const frac = big % USDC_SCALE;
|
|
349
|
+
const fracStr = frac.toString().padStart(Number(USDC_DECIMALS), "0").replace(/0+$/, "");
|
|
350
|
+
return fracStr.length > 0 ? `${whole}.${fracStr.padEnd(2, "0")}` : `${whole}.00`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/index.ts
|
|
354
|
+
var BitPal = class {
|
|
355
|
+
checkout;
|
|
356
|
+
constructor(config) {
|
|
357
|
+
const cfg = typeof config === "string" ? { apiKey: config } : config;
|
|
358
|
+
this.checkout = new BitPalCheckoutClient(cfg);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
362
|
+
0 && (module.exports = {
|
|
363
|
+
BitPal,
|
|
364
|
+
BitPalCheckoutClient,
|
|
365
|
+
BitPalError,
|
|
366
|
+
CHECKOUT_WEBHOOK_EVENTS,
|
|
367
|
+
PricesResource,
|
|
368
|
+
ProductsResource,
|
|
369
|
+
WEBHOOK_HEADERS,
|
|
370
|
+
fromAtomicUSDC,
|
|
371
|
+
parseWebhookEvent,
|
|
372
|
+
toAtomicUSDC,
|
|
373
|
+
verifyWebhookSignature
|
|
374
|
+
});
|