paykit-bd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,369 @@
1
+ 'use strict';
2
+
3
+ // src/core/errors.ts
4
+ var BRANDS = /* @__PURE__ */ Symbol.for("paykit-bd.error.brands");
5
+ function brandedInstanceOf(tag) {
6
+ return (value) => {
7
+ if (typeof value !== "object" || value === null) return false;
8
+ const brands = value[BRANDS];
9
+ return Array.isArray(brands) && brands.includes(tag);
10
+ };
11
+ }
12
+ var PaykitError = class extends Error {
13
+ static [Symbol.hasInstance] = brandedInstanceOf("PaykitError");
14
+ /** Class lineage, innermost last. Read `instanceof` instead of this. */
15
+ [BRANDS];
16
+ /** Gateway this came from — `"bkash"`, or `"paykit"` for local failures. */
17
+ provider;
18
+ /** Stable machine-readable code. Gateway codes are passed through verbatim. */
19
+ code;
20
+ /** True when retrying the identical request could plausibly succeed. */
21
+ retryable;
22
+ /** Untouched gateway response body, for logging. */
23
+ raw;
24
+ constructor(message, opts, brands = []) {
25
+ super(message, { cause: opts.cause });
26
+ this.name = new.target.name;
27
+ this[BRANDS] = ["PaykitError", ...brands];
28
+ this.provider = opts.provider;
29
+ this.code = opts.code;
30
+ this.retryable = opts.retryable ?? false;
31
+ this.raw = opts.raw;
32
+ }
33
+ toJSON() {
34
+ return {
35
+ name: this.name,
36
+ provider: this.provider,
37
+ code: this.code,
38
+ message: this.message,
39
+ retryable: this.retryable
40
+ };
41
+ }
42
+ };
43
+ var ProviderError = class extends PaykitError {
44
+ static [Symbol.hasInstance] = brandedInstanceOf("ProviderError");
45
+ constructor(message, opts, brands = []) {
46
+ super(message, opts, ["ProviderError", ...brands]);
47
+ }
48
+ };
49
+ var NetworkError = class extends PaykitError {
50
+ static [Symbol.hasInstance] = brandedInstanceOf("NetworkError");
51
+ /** HTTP status, when there was one. */
52
+ status;
53
+ constructor(message, opts) {
54
+ super(
55
+ message,
56
+ {
57
+ provider: opts.provider,
58
+ code: opts.code ?? "network_error",
59
+ retryable: true,
60
+ raw: opts.raw,
61
+ cause: opts.cause
62
+ },
63
+ ["NetworkError"]
64
+ );
65
+ this.status = opts.status;
66
+ }
67
+ };
68
+ var ConfigError = class extends PaykitError {
69
+ static [Symbol.hasInstance] = brandedInstanceOf("ConfigError");
70
+ constructor(message, opts = {}) {
71
+ super(
72
+ message,
73
+ {
74
+ provider: opts.provider ?? "paykit",
75
+ code: opts.code ?? "config_error",
76
+ retryable: false
77
+ },
78
+ ["ConfigError"]
79
+ );
80
+ }
81
+ };
82
+ var WebhookVerificationError = class extends PaykitError {
83
+ static [Symbol.hasInstance] = brandedInstanceOf("WebhookVerificationError");
84
+ constructor(message, opts) {
85
+ super(
86
+ message,
87
+ {
88
+ provider: opts.provider,
89
+ code: opts.code ?? "webhook_verification_failed",
90
+ retryable: false,
91
+ raw: opts.raw,
92
+ cause: opts.cause
93
+ },
94
+ ["WebhookVerificationError"]
95
+ );
96
+ }
97
+ };
98
+ var RateLimitError = class extends PaykitError {
99
+ static [Symbol.hasInstance] = brandedInstanceOf("RateLimitError");
100
+ /** Epoch ms when the guard will let the call through. */
101
+ retryAt;
102
+ constructor(message, opts) {
103
+ super(
104
+ message,
105
+ {
106
+ provider: opts.provider,
107
+ code: opts.code ?? "rate_limited",
108
+ retryable: true
109
+ },
110
+ ["RateLimitError"]
111
+ );
112
+ this.retryAt = opts.retryAt;
113
+ }
114
+ };
115
+ function brandCheckFor(tag) {
116
+ return brandedInstanceOf(tag);
117
+ }
118
+
119
+ // src/core/money.ts
120
+ var AMOUNT_RE = /^-?\d+(\.\d{1,2})?$/;
121
+ function toPoisha(amount) {
122
+ const text = typeof amount === "number" ? formatNumber(amount) : amount.trim();
123
+ if (!AMOUNT_RE.test(text)) {
124
+ throw new ConfigError(
125
+ `Invalid BDT amount ${JSON.stringify(amount)}: expected a decimal with at most 2 places, e.g. "12.50".`,
126
+ { code: "invalid_amount" }
127
+ );
128
+ }
129
+ const negative = text.startsWith("-");
130
+ const [whole = "0", fraction = ""] = (negative ? text.slice(1) : text).split(".");
131
+ const poisha = BigInt(whole) * 100n + BigInt(fraction.padEnd(2, "0"));
132
+ return negative ? -poisha : poisha;
133
+ }
134
+ function fromPoisha(poisha) {
135
+ const negative = poisha < 0n;
136
+ const abs = negative ? -poisha : poisha;
137
+ const whole = abs / 100n;
138
+ const fraction = (abs % 100n).toString().padStart(2, "0");
139
+ return `${negative ? "-" : ""}${whole}.${fraction}`;
140
+ }
141
+ function toAmountString(amount) {
142
+ return fromPoisha(toPoisha(amount));
143
+ }
144
+ function compareAmount(a, b) {
145
+ const left = toPoisha(a);
146
+ const right = toPoisha(b);
147
+ return left < right ? -1 : left > right ? 1 : 0;
148
+ }
149
+ function addAmount(a, b) {
150
+ return fromPoisha(toPoisha(a) + toPoisha(b));
151
+ }
152
+ function subtractAmount(a, b) {
153
+ return fromPoisha(toPoisha(a) - toPoisha(b));
154
+ }
155
+ function sumAmounts(amounts) {
156
+ return fromPoisha(amounts.reduce((total, a) => total + toPoisha(a), 0n));
157
+ }
158
+ function formatNumber(value) {
159
+ if (!Number.isFinite(value)) {
160
+ throw new ConfigError(`Invalid BDT amount ${value}: not a finite number.`, { code: "invalid_amount" });
161
+ }
162
+ return value.toFixed(2);
163
+ }
164
+
165
+ // src/core/http.ts
166
+ var noopLogger = {
167
+ debug() {
168
+ },
169
+ warn() {
170
+ },
171
+ error() {
172
+ }
173
+ };
174
+ var DEFAULT_TIMEOUT_MS = 3e4;
175
+ var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "password", "username", "x-app-key", "x-app-secret"]);
176
+ function redactHeaders(headers) {
177
+ const out = {};
178
+ for (const [key, value] of Object.entries(headers)) {
179
+ out[key] = SECRET_HEADERS.has(key.toLowerCase()) ? "[redacted]" : value;
180
+ }
181
+ return out;
182
+ }
183
+ async function requestJson(url, options = {}, ctx = { provider: "paykit" }) {
184
+ const {
185
+ method = "POST",
186
+ headers = {},
187
+ json,
188
+ timeoutMs = DEFAULT_TIMEOUT_MS,
189
+ retries = 0,
190
+ retryBaseMs = 300,
191
+ signal
192
+ } = options;
193
+ const logger = ctx.logger ?? noopLogger;
194
+ const requestHeaders = { Accept: "application/json", ...headers };
195
+ let payload;
196
+ if (json !== void 0) {
197
+ payload = JSON.stringify(json);
198
+ requestHeaders["Content-Type"] ??= "application/json";
199
+ }
200
+ let lastError;
201
+ for (let attempt = 0; attempt <= retries; attempt++) {
202
+ if (attempt > 0) {
203
+ const delay = retryBaseMs * 2 ** (attempt - 1) + Math.floor(Math.random() * retryBaseMs);
204
+ logger.warn("paykit: retrying request", { url, attempt, delay });
205
+ await sleep(delay, signal);
206
+ }
207
+ try {
208
+ const response = await fetch(url, {
209
+ method,
210
+ headers: requestHeaders,
211
+ body: payload,
212
+ signal: mergeSignals(signal, AbortSignal.timeout(timeoutMs))
213
+ });
214
+ const text = await response.text();
215
+ if (response.status >= 500) {
216
+ lastError = new NetworkError(`${ctx.provider}: gateway returned HTTP ${response.status}`, {
217
+ provider: ctx.provider,
218
+ code: "upstream_error",
219
+ status: response.status,
220
+ raw: text.slice(0, 2e3)
221
+ });
222
+ if (attempt < retries) continue;
223
+ throw lastError;
224
+ }
225
+ let body;
226
+ try {
227
+ body = text ? JSON.parse(text) : {};
228
+ } catch (cause) {
229
+ throw new NetworkError(
230
+ `${ctx.provider}: expected JSON but got ${describeBody(text)} (HTTP ${response.status})`,
231
+ { provider: ctx.provider, code: "invalid_json", status: response.status, raw: text.slice(0, 2e3), cause }
232
+ );
233
+ }
234
+ logger.debug("paykit: request complete", {
235
+ url,
236
+ method,
237
+ status: response.status,
238
+ headers: redactHeaders(requestHeaders)
239
+ });
240
+ return { status: response.status, headers: response.headers, body, text };
241
+ } catch (error) {
242
+ if (error instanceof NetworkError && error.code === "invalid_json") throw error;
243
+ lastError = error;
244
+ const aborted = signal?.aborted === true;
245
+ if (aborted || attempt >= retries) {
246
+ if (error instanceof NetworkError) throw error;
247
+ throw new NetworkError(`${ctx.provider}: request to ${url} failed`, {
248
+ provider: ctx.provider,
249
+ code: isTimeout(error) ? "timeout" : "network_error",
250
+ cause: error
251
+ });
252
+ }
253
+ }
254
+ }
255
+ throw lastError instanceof Error ? lastError : new NetworkError(`${ctx.provider}: request to ${url} failed`, { provider: ctx.provider });
256
+ }
257
+ function describeBody(text) {
258
+ const trimmed = text.trim();
259
+ if (!trimmed) return "an empty body";
260
+ if (trimmed.startsWith("<")) return "HTML (usually a proxy or WAF page)";
261
+ return `${JSON.stringify(trimmed.slice(0, 80))}\u2026`;
262
+ }
263
+ function isTimeout(error) {
264
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
265
+ }
266
+ function mergeSignals(...signals) {
267
+ const present = signals.filter((s) => s !== void 0);
268
+ return present.length === 1 ? present[0] : AbortSignal.any(present);
269
+ }
270
+ function sleep(ms, signal) {
271
+ return new Promise((resolve, reject) => {
272
+ if (signal?.aborted) {
273
+ reject(signal.reason);
274
+ return;
275
+ }
276
+ const timer = setTimeout(() => {
277
+ signal?.removeEventListener("abort", onAbort);
278
+ resolve();
279
+ }, ms);
280
+ const onAbort = () => {
281
+ clearTimeout(timer);
282
+ reject(signal?.reason);
283
+ };
284
+ signal?.addEventListener("abort", onAbort, { once: true });
285
+ });
286
+ }
287
+
288
+ // src/core/token-store.ts
289
+ var MemoryTokenStore = class {
290
+ #records = /* @__PURE__ */ new Map();
291
+ async get(key) {
292
+ return this.#records.get(key) ?? null;
293
+ }
294
+ async set(key, record) {
295
+ this.#records.set(key, record);
296
+ }
297
+ async delete(key) {
298
+ this.#records.delete(key);
299
+ }
300
+ };
301
+ function createKvTokenStore(kv, opts = {}) {
302
+ const prefix = opts.prefix ?? "paykit:token:";
303
+ const store = {
304
+ async get(key) {
305
+ const raw = await kv.get(prefix + key);
306
+ if (!raw) return null;
307
+ try {
308
+ return JSON.parse(raw);
309
+ } catch {
310
+ return null;
311
+ }
312
+ },
313
+ async set(key, record) {
314
+ const ttlSeconds = Math.max(60, Math.ceil((record.expiresAt - Date.now()) / 1e3) + 3600);
315
+ await kv.set(prefix + key, JSON.stringify(record), ttlSeconds);
316
+ },
317
+ async delete(key) {
318
+ await kv.del(prefix + key);
319
+ }
320
+ };
321
+ if (kv.setIfAbsent) {
322
+ const setIfAbsent = kv.setIfAbsent.bind(kv);
323
+ store.withLock = async function withLock(key, ttlMs, fn) {
324
+ const lockKey = `${prefix}lock:${key}`;
325
+ const ttlSeconds = Math.max(1, Math.ceil(ttlMs / 1e3));
326
+ const deadline = Date.now() + ttlMs;
327
+ while (Date.now() < deadline) {
328
+ if (await setIfAbsent(lockKey, String(Date.now()), ttlSeconds)) {
329
+ try {
330
+ return await fn();
331
+ } finally {
332
+ await kv.del(lockKey);
333
+ }
334
+ }
335
+ await new Promise((resolve) => setTimeout(resolve, 50 + Math.floor(Math.random() * 100)));
336
+ }
337
+ return fn();
338
+ };
339
+ }
340
+ return store;
341
+ }
342
+
343
+ // src/core/provider.ts
344
+ function isPaymentProvider(value) {
345
+ if (typeof value !== "object" || value === null) return false;
346
+ const candidate = value;
347
+ return typeof candidate["id"] === "string" && typeof candidate["createPayment"] === "function" && typeof candidate["executePayment"] === "function" && typeof candidate["getPayment"] === "function" && typeof candidate["refund"] === "function" && typeof candidate["verifyWebhook"] === "function";
348
+ }
349
+
350
+ exports.ConfigError = ConfigError;
351
+ exports.MemoryTokenStore = MemoryTokenStore;
352
+ exports.NetworkError = NetworkError;
353
+ exports.PaykitError = PaykitError;
354
+ exports.ProviderError = ProviderError;
355
+ exports.RateLimitError = RateLimitError;
356
+ exports.WebhookVerificationError = WebhookVerificationError;
357
+ exports.addAmount = addAmount;
358
+ exports.brandCheckFor = brandCheckFor;
359
+ exports.compareAmount = compareAmount;
360
+ exports.createKvTokenStore = createKvTokenStore;
361
+ exports.fromPoisha = fromPoisha;
362
+ exports.isPaymentProvider = isPaymentProvider;
363
+ exports.noopLogger = noopLogger;
364
+ exports.redactHeaders = redactHeaders;
365
+ exports.requestJson = requestJson;
366
+ exports.subtractAmount = subtractAmount;
367
+ exports.sumAmounts = sumAmounts;
368
+ exports.toAmountString = toAmountString;
369
+ exports.toPoisha = toPoisha;
@@ -0,0 +1,20 @@
1
+ export { C as ConfigError, N as NetworkError, a as PaykitError, P as ProviderError, R as RateLimitError, W as WebhookVerificationError, b as brandCheckFor } from './errors-CleuZfqT.cjs';
2
+ export { C as CreatePaymentInput, a as CreatedPayment, H as HttpResponse, K as KvLike, L as Logger, M as MemoryTokenStore, b as Payment, e as PaymentIntent, P as PaymentProvider, f as PaymentStatus, R as RawWebhookRequest, d as Refund, c as RefundInput, g as RequestOptions, h as TokenRecord, T as TokenStore, W as WebhookEvent, i as WebhookEventType, j as createKvTokenStore, k as isPaymentProvider, n as noopLogger, r as redactHeaders, l as requestJson } from './token-store-C8IMMLPJ.cjs';
3
+
4
+ /** Parse a BDT decimal string or number into integer poisha. */
5
+ declare function toPoisha(amount: string | number): bigint;
6
+ /** Format integer poisha back into the 2-decimal string the gateway expects. */
7
+ declare function fromPoisha(poisha: bigint): string;
8
+ /**
9
+ * Normalise any accepted amount into the canonical "123.45" form.
10
+ * Use this on every amount before it goes into a request body.
11
+ */
12
+ declare function toAmountString(amount: string | number): string;
13
+ /** -1, 0 or 1 — a total order on amounts that does not go through floats. */
14
+ declare function compareAmount(a: string | number, b: string | number): -1 | 0 | 1;
15
+ declare function addAmount(a: string | number, b: string | number): string;
16
+ declare function subtractAmount(a: string | number, b: string | number): string;
17
+ /** Sum a list of amounts. Useful for checking partial refunds against a total. */
18
+ declare function sumAmounts(amounts: Array<string | number>): string;
19
+
20
+ export { addAmount, compareAmount, fromPoisha, subtractAmount, sumAmounts, toAmountString, toPoisha };
@@ -0,0 +1,20 @@
1
+ export { C as ConfigError, N as NetworkError, a as PaykitError, P as ProviderError, R as RateLimitError, W as WebhookVerificationError, b as brandCheckFor } from './errors-CleuZfqT.js';
2
+ export { C as CreatePaymentInput, a as CreatedPayment, H as HttpResponse, K as KvLike, L as Logger, M as MemoryTokenStore, b as Payment, e as PaymentIntent, P as PaymentProvider, f as PaymentStatus, R as RawWebhookRequest, d as Refund, c as RefundInput, g as RequestOptions, h as TokenRecord, T as TokenStore, W as WebhookEvent, i as WebhookEventType, j as createKvTokenStore, k as isPaymentProvider, n as noopLogger, r as redactHeaders, l as requestJson } from './token-store-C8IMMLPJ.js';
3
+
4
+ /** Parse a BDT decimal string or number into integer poisha. */
5
+ declare function toPoisha(amount: string | number): bigint;
6
+ /** Format integer poisha back into the 2-decimal string the gateway expects. */
7
+ declare function fromPoisha(poisha: bigint): string;
8
+ /**
9
+ * Normalise any accepted amount into the canonical "123.45" form.
10
+ * Use this on every amount before it goes into a request body.
11
+ */
12
+ declare function toAmountString(amount: string | number): string;
13
+ /** -1, 0 or 1 — a total order on amounts that does not go through floats. */
14
+ declare function compareAmount(a: string | number, b: string | number): -1 | 0 | 1;
15
+ declare function addAmount(a: string | number, b: string | number): string;
16
+ declare function subtractAmount(a: string | number, b: string | number): string;
17
+ /** Sum a list of amounts. Useful for checking partial refunds against a total. */
18
+ declare function sumAmounts(amounts: Array<string | number>): string;
19
+
20
+ export { addAmount, compareAmount, fromPoisha, subtractAmount, sumAmounts, toAmountString, toPoisha };