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.
@@ -0,0 +1,240 @@
1
+ /**
2
+ * The provider-neutral shapes. Every gateway client normalises into these, so
3
+ * application code can switch bKash for Nagad without rewriting order handling,
4
+ * while `raw` always keeps the untouched gateway response for the cases where
5
+ * the normalisation is not enough.
6
+ */
7
+ type PaymentStatus = "initiated" | "pending" | "completed" | "failed" | "cancelled";
8
+ type PaymentIntent = "sale" | "authorization";
9
+ interface CreatePaymentInput {
10
+ /** BDT. Accepts "12.50" or 12.5; normalised to a 2-decimal string. */
11
+ amount: string | number;
12
+ /** Your order or invoice id. Reaches the gateway and comes back on the webhook. */
13
+ reference: string;
14
+ /**
15
+ * Identifies the payer to the gateway. For bKash, passing the wallet number
16
+ * pre-fills it on the bKash entry screen.
17
+ */
18
+ payerReference?: string;
19
+ /** Overrides the client's configured callback URL for this payment only. */
20
+ callbackUrl?: string;
21
+ /** `"sale"` captures immediately; `"authorization"` holds funds for later capture. */
22
+ intent?: PaymentIntent;
23
+ currency?: "BDT";
24
+ /** Provider-specific extras, passed through untouched. */
25
+ extra?: Record<string, string>;
26
+ }
27
+ interface CreatedPayment {
28
+ provider: string;
29
+ /** Gateway id for this attempt. Store it against your order before redirecting. */
30
+ paymentId: string;
31
+ /**
32
+ * Where to send the customer. `null` when the flow needs no redirect — a bKash
33
+ * agreement payment, for instance, is authorised with a PIN alone.
34
+ */
35
+ redirectUrl: string | null;
36
+ status: PaymentStatus;
37
+ amount: string;
38
+ currency: string;
39
+ reference?: string;
40
+ /** When the gateway stops accepting this payment id. bKash: 24 hours. */
41
+ expiresAt?: Date;
42
+ raw: unknown;
43
+ }
44
+ interface Payment {
45
+ provider: string;
46
+ paymentId: string;
47
+ /** The financial transaction id, present once money has actually moved. */
48
+ transactionId: string | null;
49
+ status: PaymentStatus;
50
+ amount: string;
51
+ currency: string;
52
+ reference?: string;
53
+ /** Payer's wallet or account number, when the gateway discloses it. */
54
+ payerAccount?: string;
55
+ completedAt?: Date;
56
+ raw: unknown;
57
+ }
58
+ interface RefundInput {
59
+ paymentId: string;
60
+ /** The original transaction id, not the payment id. */
61
+ transactionId: string;
62
+ /** Omit for a full refund — the provider reads the refundable balance itself. */
63
+ amount?: string | number;
64
+ /** Free text kept on the refund record. A default is substituted if omitted. */
65
+ reason?: string;
66
+ /** Item identifier. A default is substituted if omitted. */
67
+ sku?: string;
68
+ }
69
+ interface Refund {
70
+ provider: string;
71
+ refundTransactionId: string;
72
+ originalTransactionId: string;
73
+ status: PaymentStatus;
74
+ amount: string;
75
+ currency: string;
76
+ completedAt?: Date;
77
+ raw: unknown;
78
+ }
79
+ type WebhookEventType = "payment.completed" | "payment.failed" | "subscription.confirmation" | "unknown";
80
+ interface WebhookEvent {
81
+ provider: string;
82
+ type: WebhookEventType;
83
+ /** Gateway id for this message. Use it to make your handler idempotent. */
84
+ eventId?: string;
85
+ transactionId?: string;
86
+ reference?: string;
87
+ amount?: string;
88
+ currency?: string;
89
+ payerAccount?: string;
90
+ occurredAt?: Date;
91
+ raw: unknown;
92
+ }
93
+ /** What a verifier needs from an inbound HTTP request, framework-independent. */
94
+ interface RawWebhookRequest {
95
+ /** The exact bytes of the body. Parsing it before verification breaks the signature. */
96
+ body: string;
97
+ headers: Record<string, string | string[] | undefined>;
98
+ }
99
+
100
+ interface Logger {
101
+ debug(message: string, meta?: Record<string, unknown>): void;
102
+ warn(message: string, meta?: Record<string, unknown>): void;
103
+ error(message: string, meta?: Record<string, unknown>): void;
104
+ }
105
+ declare const noopLogger: Logger;
106
+ interface HttpResponse<T> {
107
+ status: number;
108
+ headers: Headers;
109
+ body: T;
110
+ text: string;
111
+ }
112
+ interface RequestOptions {
113
+ method?: "GET" | "POST";
114
+ headers?: Record<string, string>;
115
+ /** Serialised as JSON. Omit for GET. */
116
+ json?: unknown;
117
+ /** Per-request timeout. Default 30s — bKash PIN flows are genuinely slow. */
118
+ timeoutMs?: number;
119
+ /**
120
+ * Retry count for transport failures and 5xx.
121
+ *
122
+ * Defaults to 0 and should stay 0 for anything that moves money. A create or
123
+ * execute call that times out may still have succeeded at the gateway, so the
124
+ * safe recovery is to query the payment, not to fire it again.
125
+ */
126
+ retries?: number;
127
+ /** Base backoff in ms; each attempt waits base * 2^n plus jitter. */
128
+ retryBaseMs?: number;
129
+ signal?: AbortSignal;
130
+ }
131
+ declare function redactHeaders(headers: Record<string, string>): Record<string, string>;
132
+ /**
133
+ * JSON-over-HTTP with a timeout, optional bounded retry and no dependencies.
134
+ *
135
+ * Resolves for any HTTP status that produced a parseable JSON body, including
136
+ * 4xx — gateways signal business errors with a 200 and an errorCode as often as
137
+ * with a status, so status interpretation belongs to the caller. Throws only
138
+ * when there is no usable answer at all.
139
+ */
140
+ declare function requestJson<T = unknown>(url: string, options?: RequestOptions, ctx?: {
141
+ provider: string;
142
+ logger?: Logger;
143
+ }): Promise<HttpResponse<T>>;
144
+
145
+ /**
146
+ * The seam every gateway implements.
147
+ *
148
+ * Deliberately small: the five things an order flow actually needs. Anything a
149
+ * gateway does beyond this — bKash agreements, for one — stays on that
150
+ * gateway's own client rather than being forced into a lowest common
151
+ * denominator that fits nobody.
152
+ */
153
+ interface PaymentProvider {
154
+ /** Stable id, e.g. `"bkash"`. Appears on every returned object and error. */
155
+ readonly id: string;
156
+ /** Start a payment. The customer is sent to `redirectUrl` when there is one. */
157
+ createPayment(input: CreatePaymentInput): Promise<CreatedPayment>;
158
+ /**
159
+ * Finalise a payment after the customer returns. Call this exactly once per
160
+ * payment id: gateways typically allow one execution only.
161
+ */
162
+ executePayment(paymentId: string): Promise<Payment>;
163
+ /** Read current state. Safe to call repeatedly — this is the recovery path. */
164
+ getPayment(paymentId: string): Promise<Payment>;
165
+ refund(input: RefundInput): Promise<Refund>;
166
+ /**
167
+ * Prove an inbound webhook came from the gateway and normalise it.
168
+ * Throws {@link import("./errors.ts").WebhookVerificationError} if it did not.
169
+ */
170
+ verifyWebhook(request: RawWebhookRequest): Promise<WebhookEvent>;
171
+ }
172
+ /** Narrow an unknown object to a PaymentProvider at a plugin boundary. */
173
+ declare function isPaymentProvider(value: unknown): value is PaymentProvider;
174
+
175
+ /**
176
+ * Where an access token lives between requests.
177
+ *
178
+ * This matters more than it looks. bKash blocks a merchant for an hour if the
179
+ * Refresh Token API is called more than twice in one hour, and the budget is
180
+ * counted per merchant account — not per process. A single-process memory store
181
+ * is therefore only correct while exactly one instance is running. Two pods, or
182
+ * a serverless function that cold-starts per request, will each believe they
183
+ * have a fresh budget and between them blow through it.
184
+ *
185
+ * For anything beyond one process, back this with something shared: Redis, a
186
+ * database row, or any KV — see {@link createKvTokenStore}.
187
+ */
188
+ interface TokenRecord {
189
+ idToken: string;
190
+ refreshToken: string;
191
+ /** Epoch ms at which the gateway stops accepting `idToken`. */
192
+ expiresAt: number;
193
+ /**
194
+ * Epoch ms of every token acquisition still inside the rolling window,
195
+ * oldest first — grants and refreshes alike.
196
+ */
197
+ acquisitions: number[];
198
+ /**
199
+ * The subset of {@link acquisitions} that were refreshes. This is what the
200
+ * gateway's refresh budget is actually counted against.
201
+ */
202
+ refreshes: number[];
203
+ }
204
+ interface TokenStore {
205
+ get(key: string): Promise<TokenRecord | null>;
206
+ set(key: string, record: TokenRecord): Promise<void>;
207
+ delete(key: string): Promise<void>;
208
+ /**
209
+ * Optional mutual exclusion around a token acquisition. Implement it on a
210
+ * shared store to stop N instances refreshing at the same moment; without it
211
+ * the client still de-duplicates in-flight refreshes within its own process.
212
+ */
213
+ withLock?<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T>;
214
+ }
215
+ /** Per-process store. Correct for a single long-lived instance, and nothing else. */
216
+ declare class MemoryTokenStore implements TokenStore {
217
+ #private;
218
+ get(key: string): Promise<TokenRecord | null>;
219
+ set(key: string, record: TokenRecord): Promise<void>;
220
+ delete(key: string): Promise<void>;
221
+ }
222
+ interface KvLike {
223
+ get(key: string): Promise<string | null | undefined>;
224
+ set(key: string, value: string, ttlSeconds?: number): Promise<unknown>;
225
+ del(key: string): Promise<unknown>;
226
+ /**
227
+ * Set only if absent, returning whether the write happened. Supplying this
228
+ * turns on cross-instance locking; without it, locking is skipped.
229
+ */
230
+ setIfAbsent?(key: string, value: string, ttlSeconds: number): Promise<boolean>;
231
+ }
232
+ /**
233
+ * Wrap any string KV — Redis, Upstash, Cloudflare KV, a Prisma table behind
234
+ * three functions — into a TokenStore.
235
+ */
236
+ declare function createKvTokenStore(kv: KvLike, opts?: {
237
+ prefix?: string;
238
+ }): TokenStore;
239
+
240
+ export { type CreatePaymentInput as C, type HttpResponse as H, type KvLike as K, type Logger as L, MemoryTokenStore as M, type PaymentProvider as P, type RawWebhookRequest as R, type TokenStore as T, type WebhookEvent as W, type CreatedPayment as a, type Payment as b, type RefundInput as c, type Refund as d, type PaymentIntent as e, type PaymentStatus as f, type RequestOptions as g, type TokenRecord as h, type WebhookEventType as i, createKvTokenStore as j, isPaymentProvider as k, requestJson as l, noopLogger as n, redactHeaders as r };
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "paykit-bd",
3
+ "version": "0.1.0",
4
+ "description": "Typed, zero-dependency payment clients for Bangladeshi gateways. bKash tokenized checkout with a correct token lifecycle and real AWS SNS webhook verification.",
5
+ "keywords": [
6
+ "bkash",
7
+ "bangladesh",
8
+ "payment",
9
+ "payment-gateway",
10
+ "tokenized-checkout",
11
+ "webhook",
12
+ "sns",
13
+ "nagad",
14
+ "sslcommerz",
15
+ "typescript"
16
+ ],
17
+ "license": "MIT",
18
+ "author": "joarder97",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/joarder97/paykit-bd.git"
22
+ },
23
+ "homepage": "https://github.com/joarder97/paykit-bd#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/joarder97/paykit-bd/issues"
26
+ },
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "engines": {
30
+ "node": ">=20.0.0"
31
+ },
32
+ "main": "./dist/index.cjs",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js",
39
+ "require": "./dist/index.cjs"
40
+ },
41
+ "./bkash": {
42
+ "types": "./dist/bkash/index.d.ts",
43
+ "import": "./dist/bkash/index.js",
44
+ "require": "./dist/bkash/index.cjs"
45
+ },
46
+ "./bkash/next": {
47
+ "types": "./dist/bkash/adapters/next.d.ts",
48
+ "import": "./dist/bkash/adapters/next.js",
49
+ "require": "./dist/bkash/adapters/next.cjs"
50
+ },
51
+ "./bkash/express": {
52
+ "types": "./dist/bkash/adapters/express.d.ts",
53
+ "import": "./dist/bkash/adapters/express.js",
54
+ "require": "./dist/bkash/adapters/express.cjs"
55
+ },
56
+ "./package.json": "./package.json"
57
+ },
58
+ "files": [
59
+ "dist",
60
+ "README.md",
61
+ "LICENSE"
62
+ ],
63
+ "scripts": {
64
+ "build": "tsup",
65
+ "typecheck": "tsc --noEmit",
66
+ "test": "node --test test/*.test.ts",
67
+ "smoke": "node scripts/sandbox-smoke.ts",
68
+ "check": "npm run typecheck && npm run test && npm run build",
69
+ "prepublishOnly": "npm run typecheck && npm run test && npm run verify:package",
70
+ "verify:package": "node scripts/verify-package.ts"
71
+ },
72
+ "devDependencies": {
73
+ "@types/node": "^22.10.2",
74
+ "tsup": "^8.3.5",
75
+ "typescript": "^5.7.2"
76
+ },
77
+ "packageManager": "pnpm@11.22.0"
78
+ }