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/LICENSE +21 -0
- package/README.md +168 -0
- package/dist/bkash/adapters/express.cjs +128 -0
- package/dist/bkash/adapters/express.d.cts +50 -0
- package/dist/bkash/adapters/express.d.ts +50 -0
- package/dist/bkash/adapters/express.js +125 -0
- package/dist/bkash/adapters/next.cjs +383 -0
- package/dist/bkash/adapters/next.d.cts +64 -0
- package/dist/bkash/adapters/next.d.ts +64 -0
- package/dist/bkash/adapters/next.js +379 -0
- package/dist/bkash/index.cjs +1335 -0
- package/dist/bkash/index.d.cts +104 -0
- package/dist/bkash/index.d.ts +104 -0
- package/dist/bkash/index.js +1315 -0
- package/dist/client-UNDtdWhu.d.cts +475 -0
- package/dist/client-Y9603twG.d.ts +475 -0
- package/dist/errors-CleuZfqT.d.cts +103 -0
- package/dist/errors-CleuZfqT.d.ts +103 -0
- package/dist/index.cjs +369 -0
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +348 -0
- package/dist/token-store-C8IMMLPJ.d.cts +240 -0
- package/dist/token-store-C8IMMLPJ.d.ts +240 -0
- package/package.json +78 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// src/core/errors.ts
|
|
6
|
+
var BRANDS = /* @__PURE__ */ Symbol.for("paykit-bd.error.brands");
|
|
7
|
+
function brandedInstanceOf(tag) {
|
|
8
|
+
return (value) => {
|
|
9
|
+
if (typeof value !== "object" || value === null) return false;
|
|
10
|
+
const brands = value[BRANDS];
|
|
11
|
+
return Array.isArray(brands) && brands.includes(tag);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
var PaykitError = class extends Error {
|
|
15
|
+
static [Symbol.hasInstance] = brandedInstanceOf("PaykitError");
|
|
16
|
+
/** Class lineage, innermost last. Read `instanceof` instead of this. */
|
|
17
|
+
[BRANDS];
|
|
18
|
+
/** Gateway this came from — `"bkash"`, or `"paykit"` for local failures. */
|
|
19
|
+
provider;
|
|
20
|
+
/** Stable machine-readable code. Gateway codes are passed through verbatim. */
|
|
21
|
+
code;
|
|
22
|
+
/** True when retrying the identical request could plausibly succeed. */
|
|
23
|
+
retryable;
|
|
24
|
+
/** Untouched gateway response body, for logging. */
|
|
25
|
+
raw;
|
|
26
|
+
constructor(message, opts, brands = []) {
|
|
27
|
+
super(message, { cause: opts.cause });
|
|
28
|
+
this.name = new.target.name;
|
|
29
|
+
this[BRANDS] = ["PaykitError", ...brands];
|
|
30
|
+
this.provider = opts.provider;
|
|
31
|
+
this.code = opts.code;
|
|
32
|
+
this.retryable = opts.retryable ?? false;
|
|
33
|
+
this.raw = opts.raw;
|
|
34
|
+
}
|
|
35
|
+
toJSON() {
|
|
36
|
+
return {
|
|
37
|
+
name: this.name,
|
|
38
|
+
provider: this.provider,
|
|
39
|
+
code: this.code,
|
|
40
|
+
message: this.message,
|
|
41
|
+
retryable: this.retryable
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var WebhookVerificationError = class extends PaykitError {
|
|
46
|
+
static [Symbol.hasInstance] = brandedInstanceOf("WebhookVerificationError");
|
|
47
|
+
constructor(message, opts) {
|
|
48
|
+
super(
|
|
49
|
+
message,
|
|
50
|
+
{
|
|
51
|
+
provider: opts.provider,
|
|
52
|
+
code: opts.code ?? "webhook_verification_failed",
|
|
53
|
+
retryable: false,
|
|
54
|
+
raw: opts.raw,
|
|
55
|
+
cause: opts.cause
|
|
56
|
+
},
|
|
57
|
+
["WebhookVerificationError"]
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/core/http.ts
|
|
63
|
+
var noopLogger = {
|
|
64
|
+
debug() {
|
|
65
|
+
},
|
|
66
|
+
warn() {
|
|
67
|
+
},
|
|
68
|
+
error() {
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
function parseBkashCompactTime(value) {
|
|
72
|
+
if (!value) return void 0;
|
|
73
|
+
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(value.trim());
|
|
74
|
+
if (!match) return void 0;
|
|
75
|
+
const [, y, mo, d, h, mi, s] = match;
|
|
76
|
+
const date = new Date(
|
|
77
|
+
Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s)) - 6 * 60 * 6e4
|
|
78
|
+
);
|
|
79
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/bkash/webhook.ts
|
|
83
|
+
var SNS_CERT_HOST = /^sns\.[a-z0-9-]+\.amazonaws\.com(\.cn)?$/i;
|
|
84
|
+
var BkashWebhookVerifier = class _BkashWebhookVerifier {
|
|
85
|
+
#options;
|
|
86
|
+
#logger;
|
|
87
|
+
#fetch;
|
|
88
|
+
#certCache = /* @__PURE__ */ new Map();
|
|
89
|
+
constructor(options = {}) {
|
|
90
|
+
this.#options = options;
|
|
91
|
+
this.#logger = options.logger ?? noopLogger;
|
|
92
|
+
this.#fetch = options.fetchImpl ?? fetch;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Verify an inbound request and normalise it.
|
|
96
|
+
*
|
|
97
|
+
* `request.body` must be the raw bytes as received. A body that has been
|
|
98
|
+
* parsed and re-serialised will not match the signature.
|
|
99
|
+
*/
|
|
100
|
+
async verify(request) {
|
|
101
|
+
const envelope = this.#parseEnvelope(request.body);
|
|
102
|
+
await this.#verifySignature(envelope);
|
|
103
|
+
this.#checkTopic(envelope);
|
|
104
|
+
this.#checkAge(envelope);
|
|
105
|
+
return this.#normalise(envelope);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Confirm an SNS subscription by visiting its SubscribeURL.
|
|
109
|
+
*
|
|
110
|
+
* Deliberately not automatic: it is an outbound call that switches on real
|
|
111
|
+
* payment traffic, so your handler decides when to make it. Verify the
|
|
112
|
+
* message first — this method re-checks the URL host but assumes the
|
|
113
|
+
* signature was already proven.
|
|
114
|
+
*/
|
|
115
|
+
async confirmSubscription(envelope) {
|
|
116
|
+
const url = envelope.SubscribeURL;
|
|
117
|
+
if (!url) {
|
|
118
|
+
throw new WebhookVerificationError("bKash: subscription confirmation has no SubscribeURL", {
|
|
119
|
+
provider: "bkash",
|
|
120
|
+
code: "missing_subscribe_url",
|
|
121
|
+
raw: envelope
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
assertSnsUrl(url, "SubscribeURL");
|
|
125
|
+
const response = await this.#fetch(url, { method: "GET" });
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
throw new WebhookVerificationError(
|
|
128
|
+
`bKash: SNS subscription confirmation failed with HTTP ${response.status}`,
|
|
129
|
+
{ provider: "bkash", code: "subscribe_failed", raw: await response.text().catch(() => "") }
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
this.#logger.debug("paykit/bkash: SNS subscription confirmed", { topicArn: envelope.TopicArn });
|
|
133
|
+
}
|
|
134
|
+
/** Parse an already-verified envelope's inner payment message. */
|
|
135
|
+
static parseMessage(envelope) {
|
|
136
|
+
if (!envelope.Message) return null;
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(envelope.Message);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
#parseEnvelope(body) {
|
|
144
|
+
if (!body || body.trim() === "") {
|
|
145
|
+
throw new WebhookVerificationError("bKash: empty webhook body", {
|
|
146
|
+
provider: "bkash",
|
|
147
|
+
code: "empty_body"
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(body);
|
|
152
|
+
} catch (cause) {
|
|
153
|
+
throw new WebhookVerificationError("bKash: webhook body is not JSON", {
|
|
154
|
+
provider: "bkash",
|
|
155
|
+
code: "invalid_body",
|
|
156
|
+
raw: body.slice(0, 500),
|
|
157
|
+
cause
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async #verifySignature(envelope) {
|
|
162
|
+
const { Signature, SignatureVersion, SigningCertURL, Type } = envelope;
|
|
163
|
+
if (!Signature || !SigningCertURL || !Type) {
|
|
164
|
+
throw new WebhookVerificationError(
|
|
165
|
+
"bKash: webhook is missing Type, Signature or SigningCertURL \u2014 it is not an SNS message",
|
|
166
|
+
{ provider: "bkash", code: "not_sns", raw: envelope }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const algorithm = SignatureVersion === "2" ? "RSA-SHA256" : "RSA-SHA1";
|
|
170
|
+
if (SignatureVersion !== "1" && SignatureVersion !== "2") {
|
|
171
|
+
throw new WebhookVerificationError(
|
|
172
|
+
`bKash: unsupported SNS SignatureVersion ${JSON.stringify(SignatureVersion)}`,
|
|
173
|
+
{ provider: "bkash", code: "unsupported_signature_version", raw: envelope }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
assertSnsUrl(SigningCertURL, "SigningCertURL");
|
|
177
|
+
const pem = await this.#loadCertificate(SigningCertURL);
|
|
178
|
+
const canonical = canonicalString(envelope);
|
|
179
|
+
let valid;
|
|
180
|
+
try {
|
|
181
|
+
valid = crypto.createVerify(algorithm).update(canonical, "utf8").verify(pem, Signature, "base64");
|
|
182
|
+
} catch (cause) {
|
|
183
|
+
throw new WebhookVerificationError("bKash: SNS signature could not be checked", {
|
|
184
|
+
provider: "bkash",
|
|
185
|
+
code: "signature_check_failed",
|
|
186
|
+
cause
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (!valid) {
|
|
190
|
+
throw new WebhookVerificationError(
|
|
191
|
+
"bKash: SNS signature does not match the message. Treat the payload as forged, and check that the raw request body reached the verifier unmodified.",
|
|
192
|
+
{ provider: "bkash", code: "signature_mismatch" }
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
#checkTopic(envelope) {
|
|
197
|
+
const expected = this.#options.topicArn;
|
|
198
|
+
if (!expected) {
|
|
199
|
+
this.#logger.warn(
|
|
200
|
+
"paykit/bkash: webhook accepted without a pinned topicArn \u2014 any Amazon-signed SNS topic will pass"
|
|
201
|
+
);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const allowed = Array.isArray(expected) ? expected : [expected];
|
|
205
|
+
if (!envelope.TopicArn || !allowed.includes(envelope.TopicArn)) {
|
|
206
|
+
throw new WebhookVerificationError(
|
|
207
|
+
`bKash: message came from SNS topic ${JSON.stringify(envelope.TopicArn)}, which is not one of yours`,
|
|
208
|
+
{ provider: "bkash", code: "topic_mismatch", raw: envelope.TopicArn }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
#checkAge(envelope) {
|
|
213
|
+
const maxAgeMs = this.#options.maxAgeMs;
|
|
214
|
+
if (!maxAgeMs || !envelope.Timestamp) return;
|
|
215
|
+
const sent = new Date(envelope.Timestamp).getTime();
|
|
216
|
+
if (Number.isNaN(sent)) return;
|
|
217
|
+
if (Date.now() - sent > maxAgeMs) {
|
|
218
|
+
throw new WebhookVerificationError(
|
|
219
|
+
`bKash: message is older than the configured maxAgeMs (${maxAgeMs}ms)`,
|
|
220
|
+
{ provider: "bkash", code: "message_too_old", raw: envelope.Timestamp }
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
#normalise(envelope) {
|
|
225
|
+
const base = {
|
|
226
|
+
provider: "bkash",
|
|
227
|
+
eventId: envelope.MessageId,
|
|
228
|
+
raw: envelope
|
|
229
|
+
};
|
|
230
|
+
if (envelope.Type === "SubscriptionConfirmation" || envelope.Type === "UnsubscribeConfirmation") {
|
|
231
|
+
return { ...base, type: "subscription.confirmation" };
|
|
232
|
+
}
|
|
233
|
+
const message = _BkashWebhookVerifier.parseMessage(envelope);
|
|
234
|
+
if (!message) return { ...base, type: "unknown" };
|
|
235
|
+
const completed = (message.transactionStatus ?? "").toLowerCase() === "completed";
|
|
236
|
+
return {
|
|
237
|
+
...base,
|
|
238
|
+
type: completed ? "payment.completed" : "payment.failed",
|
|
239
|
+
transactionId: message.trxID,
|
|
240
|
+
reference: message.merchantInvoiceNumber ?? message.transactionReference,
|
|
241
|
+
amount: message.amount,
|
|
242
|
+
currency: message.currency ?? "BDT",
|
|
243
|
+
payerAccount: message.debitMSISDN,
|
|
244
|
+
occurredAt: parseBkashCompactTime(message.dateTime)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async #loadCertificate(url) {
|
|
248
|
+
const ttl = this.#options.certCacheMs ?? 24 * 60 * 60 * 1e3;
|
|
249
|
+
const cached = this.#certCache.get(url);
|
|
250
|
+
if (cached && Date.now() - cached.fetchedAt < ttl) return cached.pem;
|
|
251
|
+
const response = await this.#fetch(url, { method: "GET", signal: AbortSignal.timeout(1e4) });
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
throw new WebhookVerificationError(
|
|
254
|
+
`bKash: could not fetch the SNS signing certificate (HTTP ${response.status})`,
|
|
255
|
+
{ provider: "bkash", code: "cert_fetch_failed", raw: url }
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const pem = await response.text();
|
|
259
|
+
if (!pem.includes("BEGIN CERTIFICATE") && !pem.includes("BEGIN PUBLIC KEY")) {
|
|
260
|
+
throw new WebhookVerificationError("bKash: SigningCertURL did not return a PEM certificate", {
|
|
261
|
+
provider: "bkash",
|
|
262
|
+
code: "cert_invalid",
|
|
263
|
+
raw: pem.slice(0, 200)
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
this.#certCache.set(url, { pem, fetchedAt: Date.now() });
|
|
267
|
+
return pem;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
function canonicalString(envelope) {
|
|
271
|
+
const fields = envelope.Type === "SubscriptionConfirmation" || envelope.Type === "UnsubscribeConfirmation" ? ["Message", "MessageId", "SubscribeURL", "Timestamp", "Token", "TopicArn", "Type"] : ["Message", "MessageId", "Subject", "Timestamp", "TopicArn", "Type"];
|
|
272
|
+
let canonical = "";
|
|
273
|
+
for (const field of fields) {
|
|
274
|
+
const value = envelope[field];
|
|
275
|
+
if (value === void 0 || value === null) continue;
|
|
276
|
+
canonical += `${field}
|
|
277
|
+
${value}
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
return canonical;
|
|
281
|
+
}
|
|
282
|
+
function assertSnsUrl(rawUrl, field) {
|
|
283
|
+
let url;
|
|
284
|
+
try {
|
|
285
|
+
url = new URL(rawUrl);
|
|
286
|
+
} catch {
|
|
287
|
+
throw new WebhookVerificationError(`bKash: ${field} is not a URL`, {
|
|
288
|
+
provider: "bkash",
|
|
289
|
+
code: "cert_url_invalid",
|
|
290
|
+
raw: rawUrl
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (url.protocol !== "https:") {
|
|
294
|
+
throw new WebhookVerificationError(`bKash: ${field} must be https`, {
|
|
295
|
+
provider: "bkash",
|
|
296
|
+
code: "cert_url_insecure",
|
|
297
|
+
raw: rawUrl
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (!SNS_CERT_HOST.test(url.hostname)) {
|
|
301
|
+
throw new WebhookVerificationError(
|
|
302
|
+
`bKash: ${field} points at ${url.hostname}, which is not an Amazon SNS host. This is what a forged notification looks like \u2014 the payload is not from bKash.`,
|
|
303
|
+
{ provider: "bkash", code: "cert_url_untrusted", raw: rawUrl }
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
if (field === "SigningCertURL" && !url.pathname.endsWith(".pem")) {
|
|
307
|
+
throw new WebhookVerificationError(`bKash: ${field} does not point at a .pem file`, {
|
|
308
|
+
provider: "bkash",
|
|
309
|
+
code: "cert_url_invalid",
|
|
310
|
+
raw: rawUrl
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return url;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/bkash/adapters/next.ts
|
|
317
|
+
function createBkashWebhookHandler(client, options = {}) {
|
|
318
|
+
return async function POST(request) {
|
|
319
|
+
const rawBody = await request.text();
|
|
320
|
+
let event;
|
|
321
|
+
try {
|
|
322
|
+
event = await client.verifyWebhook({ body: rawBody, headers: headersToObject(request.headers) });
|
|
323
|
+
} catch (error) {
|
|
324
|
+
await options.onVerificationFailure?.(error, rawBody);
|
|
325
|
+
const message = error instanceof WebhookVerificationError ? error.message : "webhook verification failed";
|
|
326
|
+
return Response.json({ ok: false, error: message }, { status: 400 });
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
if (event.type === "subscription.confirmation") {
|
|
330
|
+
const envelope = event.raw;
|
|
331
|
+
const shouldConfirm = await options.onSubscriptionConfirmation?.(envelope) ?? false;
|
|
332
|
+
if (shouldConfirm) await client.webhooks.confirmSubscription(envelope);
|
|
333
|
+
return Response.json({ ok: true, confirmed: shouldConfirm });
|
|
334
|
+
}
|
|
335
|
+
if (event.type === "payment.completed") {
|
|
336
|
+
await options.onPaymentCompleted?.(event);
|
|
337
|
+
} else {
|
|
338
|
+
await options.onOtherEvent?.(event);
|
|
339
|
+
}
|
|
340
|
+
return Response.json({ ok: true });
|
|
341
|
+
} catch (error) {
|
|
342
|
+
return Response.json(
|
|
343
|
+
{ ok: false, error: error instanceof PaykitError ? error.message : "handler failed" },
|
|
344
|
+
{ status: 500 }
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function createBkashCallbackHandler(client, options = {}) {
|
|
350
|
+
return async function GET(request) {
|
|
351
|
+
const url = new URL(request.url);
|
|
352
|
+
const query = client.constructor.parseCallback(url.searchParams);
|
|
353
|
+
const paymentId = query.paymentID;
|
|
354
|
+
if (!paymentId) {
|
|
355
|
+
const handled = await options.onFailure?.(new Error("callback has no paymentID"), void 0);
|
|
356
|
+
return handled ?? redirect(options.failureUrl ?? "/", url);
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const payment = query.status === "success" ? await client.executePayment(paymentId) : await client.getPayment(paymentId);
|
|
360
|
+
const handled = await options.onSettled?.(payment);
|
|
361
|
+
if (handled) return handled;
|
|
362
|
+
const destination = payment.status === "completed" ? options.successUrl ?? "/" : options.failureUrl ?? "/";
|
|
363
|
+
return redirect(destination, url);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
const handled = await options.onFailure?.(error, paymentId);
|
|
366
|
+
return handled ?? redirect(options.failureUrl ?? "/", url);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function redirect(destination, base) {
|
|
371
|
+
return Response.redirect(new URL(destination, base).toString(), 303);
|
|
372
|
+
}
|
|
373
|
+
function headersToObject(headers) {
|
|
374
|
+
const out = {};
|
|
375
|
+
headers.forEach((value, key) => {
|
|
376
|
+
out[key] = value;
|
|
377
|
+
});
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
exports.BkashWebhookVerifier = BkashWebhookVerifier;
|
|
382
|
+
exports.createBkashCallbackHandler = createBkashCallbackHandler;
|
|
383
|
+
exports.createBkashWebhookHandler = createBkashWebhookHandler;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { W as WebhookEvent } from '../../token-store-C8IMMLPJ.cjs';
|
|
2
|
+
import { B as BkashClient, S as SnsEnvelope } from '../../client-UNDtdWhu.cjs';
|
|
3
|
+
export { a as BkashWebhookVerifier } from '../../client-UNDtdWhu.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Next.js App Router route handlers.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // app/api/bkash/webhook/route.ts
|
|
10
|
+
* import { createBkashWebhookHandler } from "paykit-bd/bkash/next";
|
|
11
|
+
* export const POST = createBkashWebhookHandler(bkash, {
|
|
12
|
+
* onPaymentCompleted: async (event) => { await fulfilOrder(event.reference!); },
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* No Next.js import is needed — these are plain `Request` → `Response`
|
|
17
|
+
* functions, so they also work in any fetch-based runtime.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
interface WebhookHandlerOptions {
|
|
21
|
+
/** A genuine, completed payment. Do your fulfilment here. */
|
|
22
|
+
onPaymentCompleted?: (event: WebhookEvent) => Promise<void> | void;
|
|
23
|
+
/** A genuine notification that is not a completed payment. */
|
|
24
|
+
onOtherEvent?: (event: WebhookEvent) => Promise<void> | void;
|
|
25
|
+
/**
|
|
26
|
+
* Called for an SNS SubscriptionConfirmation. Return true to confirm the
|
|
27
|
+
* subscription, which turns on live notification delivery.
|
|
28
|
+
*
|
|
29
|
+
* Default is false — confirming is an outbound action that starts real
|
|
30
|
+
* traffic, so it is yours to trigger, usually once during onboarding.
|
|
31
|
+
*/
|
|
32
|
+
onSubscriptionConfirmation?: (envelope: SnsEnvelope) => Promise<boolean> | boolean;
|
|
33
|
+
/** Called when verification fails. The request is already being rejected. */
|
|
34
|
+
onVerificationFailure?: (error: unknown, rawBody: string) => Promise<void> | void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build a POST handler for the bKash IPN endpoint.
|
|
38
|
+
*
|
|
39
|
+
* Status codes are chosen for how SNS behaves: 200 means delivered and SNS
|
|
40
|
+
* stops; a 5xx makes SNS retry, which is what you want when your own
|
|
41
|
+
* fulfilment threw. A forged message gets 400 and no retry.
|
|
42
|
+
*/
|
|
43
|
+
declare function createBkashWebhookHandler(client: BkashClient, options?: WebhookHandlerOptions): (request: Request) => Promise<Response>;
|
|
44
|
+
interface CallbackHandlerOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Called after the payment has been executed and its real outcome is known.
|
|
47
|
+
* Return a Response to control the redirect; returning nothing sends the
|
|
48
|
+
* customer to `successUrl` or `failureUrl`.
|
|
49
|
+
*/
|
|
50
|
+
onSettled?: (result: Awaited<ReturnType<BkashClient["executePayment"]>>) => Promise<Response | void> | Response | void;
|
|
51
|
+
onFailure?: (error: unknown, paymentId: string | undefined) => Promise<Response | void> | Response | void;
|
|
52
|
+
successUrl?: string;
|
|
53
|
+
failureUrl?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a GET handler for the URL bKash redirects the customer back to.
|
|
57
|
+
*
|
|
58
|
+
* It executes the payment, because the redirect itself proves nothing: the
|
|
59
|
+
* customer's browser followed that URL and could have edited it. `status=success`
|
|
60
|
+
* in the query string is a hint, never the decision.
|
|
61
|
+
*/
|
|
62
|
+
declare function createBkashCallbackHandler(client: BkashClient, options?: CallbackHandlerOptions): (request: Request) => Promise<Response>;
|
|
63
|
+
|
|
64
|
+
export { type CallbackHandlerOptions, type WebhookHandlerOptions, createBkashCallbackHandler, createBkashWebhookHandler };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { W as WebhookEvent } from '../../token-store-C8IMMLPJ.js';
|
|
2
|
+
import { B as BkashClient, S as SnsEnvelope } from '../../client-Y9603twG.js';
|
|
3
|
+
export { a as BkashWebhookVerifier } from '../../client-Y9603twG.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Next.js App Router route handlers.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // app/api/bkash/webhook/route.ts
|
|
10
|
+
* import { createBkashWebhookHandler } from "paykit-bd/bkash/next";
|
|
11
|
+
* export const POST = createBkashWebhookHandler(bkash, {
|
|
12
|
+
* onPaymentCompleted: async (event) => { await fulfilOrder(event.reference!); },
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* No Next.js import is needed — these are plain `Request` → `Response`
|
|
17
|
+
* functions, so they also work in any fetch-based runtime.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
interface WebhookHandlerOptions {
|
|
21
|
+
/** A genuine, completed payment. Do your fulfilment here. */
|
|
22
|
+
onPaymentCompleted?: (event: WebhookEvent) => Promise<void> | void;
|
|
23
|
+
/** A genuine notification that is not a completed payment. */
|
|
24
|
+
onOtherEvent?: (event: WebhookEvent) => Promise<void> | void;
|
|
25
|
+
/**
|
|
26
|
+
* Called for an SNS SubscriptionConfirmation. Return true to confirm the
|
|
27
|
+
* subscription, which turns on live notification delivery.
|
|
28
|
+
*
|
|
29
|
+
* Default is false — confirming is an outbound action that starts real
|
|
30
|
+
* traffic, so it is yours to trigger, usually once during onboarding.
|
|
31
|
+
*/
|
|
32
|
+
onSubscriptionConfirmation?: (envelope: SnsEnvelope) => Promise<boolean> | boolean;
|
|
33
|
+
/** Called when verification fails. The request is already being rejected. */
|
|
34
|
+
onVerificationFailure?: (error: unknown, rawBody: string) => Promise<void> | void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build a POST handler for the bKash IPN endpoint.
|
|
38
|
+
*
|
|
39
|
+
* Status codes are chosen for how SNS behaves: 200 means delivered and SNS
|
|
40
|
+
* stops; a 5xx makes SNS retry, which is what you want when your own
|
|
41
|
+
* fulfilment threw. A forged message gets 400 and no retry.
|
|
42
|
+
*/
|
|
43
|
+
declare function createBkashWebhookHandler(client: BkashClient, options?: WebhookHandlerOptions): (request: Request) => Promise<Response>;
|
|
44
|
+
interface CallbackHandlerOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Called after the payment has been executed and its real outcome is known.
|
|
47
|
+
* Return a Response to control the redirect; returning nothing sends the
|
|
48
|
+
* customer to `successUrl` or `failureUrl`.
|
|
49
|
+
*/
|
|
50
|
+
onSettled?: (result: Awaited<ReturnType<BkashClient["executePayment"]>>) => Promise<Response | void> | Response | void;
|
|
51
|
+
onFailure?: (error: unknown, paymentId: string | undefined) => Promise<Response | void> | Response | void;
|
|
52
|
+
successUrl?: string;
|
|
53
|
+
failureUrl?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a GET handler for the URL bKash redirects the customer back to.
|
|
57
|
+
*
|
|
58
|
+
* It executes the payment, because the redirect itself proves nothing: the
|
|
59
|
+
* customer's browser followed that URL and could have edited it. `status=success`
|
|
60
|
+
* in the query string is a hint, never the decision.
|
|
61
|
+
*/
|
|
62
|
+
declare function createBkashCallbackHandler(client: BkashClient, options?: CallbackHandlerOptions): (request: Request) => Promise<Response>;
|
|
63
|
+
|
|
64
|
+
export { type CallbackHandlerOptions, type WebhookHandlerOptions, createBkashCallbackHandler, createBkashWebhookHandler };
|