@crediblemark/buayar 0.4.0 → 0.6.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/cli/index.js +643 -0
- package/dist/index.d.mts +131 -1
- package/dist/index.d.ts +131 -1
- package/dist/index.js +514 -30
- package/dist/index.mjs +489 -13
- package/package.json +12 -4
package/dist/index.mjs
CHANGED
|
@@ -1980,11 +1980,11 @@ var XenditProvider = class extends BasePaymentProvider {
|
|
|
1980
1980
|
amount: integerAmount,
|
|
1981
1981
|
reference_id: orderId,
|
|
1982
1982
|
description: productDetails,
|
|
1983
|
-
customer
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
},
|
|
1983
|
+
// Xendit Payment Requests API rejects the inline `customer` object
|
|
1984
|
+
// with API_VALIDATION_ERROR. Attribution is only supported via a
|
|
1985
|
+
// pre-created `customer_id` (Customer API), so we forward that when
|
|
1986
|
+
// provided through providerParams and otherwise omit customer data.
|
|
1987
|
+
...params.providerParams?.customer_id ? { customer_id: params.providerParams.customer_id } : {},
|
|
1988
1988
|
payment_method: paymentMethodPayload,
|
|
1989
1989
|
...params.providerParams
|
|
1990
1990
|
};
|
|
@@ -2413,12 +2413,218 @@ Digest:${digest}`;
|
|
|
2413
2413
|
return safeCompare(incomingSignature, expectedSignature);
|
|
2414
2414
|
}
|
|
2415
2415
|
|
|
2416
|
+
// src/providers/doku/snap.ts
|
|
2417
|
+
import crypto3 from "crypto";
|
|
2418
|
+
function snapTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
2419
|
+
const wib = new Date(date.getTime() + 7 * 60 * 60 * 1e3);
|
|
2420
|
+
return wib.toISOString().slice(0, 19) + "+07:00";
|
|
2421
|
+
}
|
|
2422
|
+
function minifyJson(obj) {
|
|
2423
|
+
return JSON.stringify(obj);
|
|
2424
|
+
}
|
|
2425
|
+
function sha256Hex(body) {
|
|
2426
|
+
const raw = typeof body === "string" ? body : minifyJson(body);
|
|
2427
|
+
return crypto3.createHash("sha256").update(raw).digest("hex").toLowerCase();
|
|
2428
|
+
}
|
|
2429
|
+
function generateSnapSymmetricSignature(clientSecret, method, endpointUrl, accessToken, body, timestamp) {
|
|
2430
|
+
const hash = sha256Hex(body);
|
|
2431
|
+
const stringToSign = `${method}:${endpointUrl}:${accessToken}:${hash}:${timestamp}`;
|
|
2432
|
+
return crypto3.createHmac("sha512", clientSecret).update(stringToSign).digest("base64");
|
|
2433
|
+
}
|
|
2434
|
+
function generateSnapAsymmetricSignature(privateKey, clientId, timestamp) {
|
|
2435
|
+
const stringToSign = `${clientId}|${timestamp}`;
|
|
2436
|
+
const key = crypto3.createPrivateKey(normalizePem(privateKey));
|
|
2437
|
+
return crypto3.sign("RSA-SHA256", Buffer.from(stringToSign, "utf8"), key).toString("base64");
|
|
2438
|
+
}
|
|
2439
|
+
function normalizePem(key) {
|
|
2440
|
+
const trimmed = key.trim();
|
|
2441
|
+
if (trimmed.includes("-----BEGIN")) {
|
|
2442
|
+
return trimmed;
|
|
2443
|
+
}
|
|
2444
|
+
return [
|
|
2445
|
+
"-----BEGIN PRIVATE KEY-----",
|
|
2446
|
+
trimmed.replace(/\s+/g, "").match(/.{1,64}/g)?.join("\n") || trimmed,
|
|
2447
|
+
"-----END PRIVATE KEY-----"
|
|
2448
|
+
].join("\n");
|
|
2449
|
+
}
|
|
2450
|
+
function snapExternalId(prefix = "") {
|
|
2451
|
+
return `${prefix}${Date.now()}${Math.floor(Math.random() * 1e3)}`;
|
|
2452
|
+
}
|
|
2453
|
+
function verifySnapWebhookSignature(headers, body, clientSecret, endpointUrl = "/api/payment/webhook") {
|
|
2454
|
+
if (!clientSecret) return false;
|
|
2455
|
+
const incoming = headers["x-signature"] || headers["X-SIGNATURE"] || headers["signature"] || headers["Signature"] || "";
|
|
2456
|
+
if (!incoming) return false;
|
|
2457
|
+
const timestamp = headers["x-timestamp"] || headers["X-TIMESTAMP"] || headers["timestamp"] || headers["Timestamp"] || snapTimestamp();
|
|
2458
|
+
const computed = generateSnapSymmetricSignature(
|
|
2459
|
+
clientSecret,
|
|
2460
|
+
"POST",
|
|
2461
|
+
endpointUrl,
|
|
2462
|
+
"",
|
|
2463
|
+
body,
|
|
2464
|
+
timestamp
|
|
2465
|
+
);
|
|
2466
|
+
return safeCompare(incoming, computed);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// src/clients/snap.ts
|
|
2470
|
+
var SnapClient = class {
|
|
2471
|
+
options;
|
|
2472
|
+
token = null;
|
|
2473
|
+
tokenExpiry = 0;
|
|
2474
|
+
constructor(options) {
|
|
2475
|
+
this.options = {
|
|
2476
|
+
clientId: options.clientId || options.merchantCode || "",
|
|
2477
|
+
clientSecret: options.clientSecret || options.apiKey || options.serverKey || options.secretKey || "",
|
|
2478
|
+
privateKey: options.privateKey,
|
|
2479
|
+
sandbox: !!options.sandbox,
|
|
2480
|
+
merchantId: options.merchantId,
|
|
2481
|
+
terminalId: options.terminalId,
|
|
2482
|
+
partnerServiceId: options.partnerServiceId,
|
|
2483
|
+
channelId: options.channelId || "H2H"
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
get baseUrl() {
|
|
2487
|
+
return this.options.sandbox ? "https://api-sandbox.doku.com" : "https://api.doku.com";
|
|
2488
|
+
}
|
|
2489
|
+
get isConfigured() {
|
|
2490
|
+
return Boolean(this.options.clientId && this.options.clientSecret);
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Get (cached) B2B access token. Mints a new one when expired.
|
|
2494
|
+
*/
|
|
2495
|
+
async getAccessToken() {
|
|
2496
|
+
const now = Date.now();
|
|
2497
|
+
if (this.token && now < this.tokenExpiry) {
|
|
2498
|
+
return this.token;
|
|
2499
|
+
}
|
|
2500
|
+
const { clientId, clientSecret, privateKey } = this.options;
|
|
2501
|
+
if (!privateKey) {
|
|
2502
|
+
throw new Error("DOKU SNAP: RSA privateKey required to obtain B2B access token");
|
|
2503
|
+
}
|
|
2504
|
+
const endpoint = "/authorization/v1/access-token/b2b";
|
|
2505
|
+
const timestamp = snapTimestamp();
|
|
2506
|
+
const signature = generateSnapAsymmetricSignature(privateKey, clientId, timestamp);
|
|
2507
|
+
const response = await fetch(this.baseUrl + endpoint, {
|
|
2508
|
+
method: "POST",
|
|
2509
|
+
headers: {
|
|
2510
|
+
"Content-Type": "application/json",
|
|
2511
|
+
"X-CLIENT-KEY": clientId,
|
|
2512
|
+
"X-TIMESTAMP": timestamp,
|
|
2513
|
+
"X-SIGNATURE": signature
|
|
2514
|
+
},
|
|
2515
|
+
body: JSON.stringify({ grantType: "client_credentials" })
|
|
2516
|
+
});
|
|
2517
|
+
const text = await response.text();
|
|
2518
|
+
let data = null;
|
|
2519
|
+
try {
|
|
2520
|
+
data = JSON.parse(text);
|
|
2521
|
+
} catch {
|
|
2522
|
+
}
|
|
2523
|
+
if (!response.ok || !data?.accessToken) {
|
|
2524
|
+
throw new Error(data?.responseMessage || data?.message || `DOKU SNAP get-token failed: HTTP ${response.status} - ${text}`);
|
|
2525
|
+
}
|
|
2526
|
+
if (!clientSecret) {
|
|
2527
|
+
}
|
|
2528
|
+
this.token = data.accessToken;
|
|
2529
|
+
const expiresIn = Number(data.expiresIn || 900);
|
|
2530
|
+
this.tokenExpiry = now + (expiresIn - 30) * 1e3;
|
|
2531
|
+
return this.token;
|
|
2532
|
+
}
|
|
2533
|
+
clearToken() {
|
|
2534
|
+
this.token = null;
|
|
2535
|
+
this.tokenExpiry = 0;
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Perform a signed SNAP transaction request.
|
|
2539
|
+
* @param method GET/POST
|
|
2540
|
+
* @param endpoint request-target path (e.g. /virtual-accounts/...)
|
|
2541
|
+
* @param body request body (object)
|
|
2542
|
+
* @param opts extra headers (X-DEVICE-ID, X-IP-ADDRESS, etc.)
|
|
2543
|
+
*/
|
|
2544
|
+
async request(method, endpoint, body, opts = {}) {
|
|
2545
|
+
const { clientId, clientSecret, channelId } = this.options;
|
|
2546
|
+
if (!clientSecret) {
|
|
2547
|
+
throw new Error("DOKU SNAP: clientSecret (Secret Key) required for transaction signing");
|
|
2548
|
+
}
|
|
2549
|
+
const accessToken = await this.getAccessToken();
|
|
2550
|
+
const timestamp = snapTimestamp();
|
|
2551
|
+
const externalId = opts.externalId || snapExternalId();
|
|
2552
|
+
const payload = body === void 0 ? "" : body;
|
|
2553
|
+
const signature = generateSnapSymmetricSignature(
|
|
2554
|
+
clientSecret,
|
|
2555
|
+
method,
|
|
2556
|
+
endpoint,
|
|
2557
|
+
accessToken,
|
|
2558
|
+
payload,
|
|
2559
|
+
timestamp
|
|
2560
|
+
);
|
|
2561
|
+
const headers = {
|
|
2562
|
+
"Content-Type": "application/json",
|
|
2563
|
+
"X-PARTNER-ID": clientId,
|
|
2564
|
+
"X-EXTERNAL-ID": externalId,
|
|
2565
|
+
"X-TIMESTAMP": timestamp,
|
|
2566
|
+
"X-SIGNATURE": signature,
|
|
2567
|
+
"CHANNEL-ID": channelId || "H2H",
|
|
2568
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
2569
|
+
...opts.deviceId ? { "X-DEVICE-ID": opts.deviceId } : {},
|
|
2570
|
+
...opts.ipAddress ? { "X-IP-ADDRESS": opts.ipAddress } : {},
|
|
2571
|
+
...opts.extraHeaders || {}
|
|
2572
|
+
};
|
|
2573
|
+
const response = await fetch(this.baseUrl + endpoint, {
|
|
2574
|
+
method,
|
|
2575
|
+
headers,
|
|
2576
|
+
body: method === "POST" && body !== void 0 ? JSON.stringify(body) : void 0
|
|
2577
|
+
});
|
|
2578
|
+
const text = await response.text();
|
|
2579
|
+
let data = null;
|
|
2580
|
+
try {
|
|
2581
|
+
data = JSON.parse(text);
|
|
2582
|
+
} catch {
|
|
2583
|
+
}
|
|
2584
|
+
if (!response.ok) {
|
|
2585
|
+
const err = data?.responseMessage || data?.error?.message || data?.message || `HTTP ${response.status}`;
|
|
2586
|
+
throw new Error(`${err}${data?.responseCode ? ` (${data.responseCode})` : ""}`);
|
|
2587
|
+
}
|
|
2588
|
+
return data;
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
|
|
2416
2592
|
// src/providers/doku/provider.ts
|
|
2417
2593
|
var DokuProvider = class extends BasePaymentProvider {
|
|
2418
2594
|
name = "doku";
|
|
2419
2595
|
getBaseUrl(sandbox) {
|
|
2420
2596
|
return sandbox ? "https://api-sandbox.doku.com" : "https://api.doku.com";
|
|
2421
2597
|
}
|
|
2598
|
+
/**
|
|
2599
|
+
* DETEKSI mode integrasi DOKU:
|
|
2600
|
+
* - SNAP : kredensial baru (Client ID `doku_...` + Secret Key `SK-...`) + opsional RSA privateKey
|
|
2601
|
+
* untuk Get Token B2B. Diaktifkan via extra.snap / extra.dokuMode="snap" / doku_ prefix.
|
|
2602
|
+
* - Legacy: Jokul v2 (Client-Id + Signature HMAC-SHA256), default jika bukan SNAP.
|
|
2603
|
+
*/
|
|
2604
|
+
isSnap(config) {
|
|
2605
|
+
if (config.extra?.snap === true || config.extra?.snap === "true") return true;
|
|
2606
|
+
if (config.extra?.dokuMode === "snap") return true;
|
|
2607
|
+
const clientId = String(config.merchantCode || config.merchantId || config.clientKey || "").trim().toLowerCase();
|
|
2608
|
+
return /^doku[_:-]/.test(clientId);
|
|
2609
|
+
}
|
|
2610
|
+
buildSnap(config) {
|
|
2611
|
+
const clientId = config.merchantCode || config.merchantId || config.clientKey || "";
|
|
2612
|
+
const clientSecret = config.apiKey || config.serverKey || config.secretKey || "";
|
|
2613
|
+
return new SnapClient({
|
|
2614
|
+
clientId,
|
|
2615
|
+
clientSecret,
|
|
2616
|
+
privateKey: config.privateKey,
|
|
2617
|
+
sandbox: !!config.sandbox,
|
|
2618
|
+
merchantId: config.extra?.merchantId || config.projectId || "",
|
|
2619
|
+
terminalId: config.extra?.terminalId || "",
|
|
2620
|
+
partnerServiceId: config.extra?.partnerServiceId || "",
|
|
2621
|
+
channelId: config.extra?.channelId || "H2H"
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
/** Helper: jumlah integer → format SNAP 2-desimal (".00"). */
|
|
2625
|
+
snapAmount(amount, currency = "IDR") {
|
|
2626
|
+
return { value: amount.toFixed(2), currency };
|
|
2627
|
+
}
|
|
2422
2628
|
async createInvoice(params, config) {
|
|
2423
2629
|
const { orderId, amount, productDetails, customer, returnUrl } = params;
|
|
2424
2630
|
const clientId = config.merchantCode || config.merchantId || config.clientKey || "";
|
|
@@ -2429,6 +2635,9 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2429
2635
|
const isDirect = !!dokuMethod;
|
|
2430
2636
|
const baseUrl = this.getBaseUrl(sandbox);
|
|
2431
2637
|
try {
|
|
2638
|
+
if (this.isSnap(config)) {
|
|
2639
|
+
return await this.createSnapInvoice(params, config, baseUrl);
|
|
2640
|
+
}
|
|
2432
2641
|
if (isDirect) {
|
|
2433
2642
|
const endpoint = dokuMethod.endpoint;
|
|
2434
2643
|
const url = `${baseUrl}${endpoint}`;
|
|
@@ -2616,7 +2825,178 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2616
2825
|
};
|
|
2617
2826
|
}
|
|
2618
2827
|
}
|
|
2828
|
+
/**
|
|
2829
|
+
* DOKU SNAP: buat transaksi (Create VA / Generate QRIS / e-Wallet Payment).
|
|
2830
|
+
* Autentikasi via B2B token + symmetric HMAC-SHA512 signature.
|
|
2831
|
+
*/
|
|
2832
|
+
async createSnapInvoice(params, config, baseUrl) {
|
|
2833
|
+
const { orderId, amount, productDetails, customer, returnUrl } = params;
|
|
2834
|
+
const snap = this.buildSnap(config);
|
|
2835
|
+
const integerAmount = Math.round(amount);
|
|
2836
|
+
const method = String(params.paymentMethod || "").toLowerCase();
|
|
2837
|
+
const bankMap = {
|
|
2838
|
+
bca: "VIRTUAL_ACCOUNT_BCA",
|
|
2839
|
+
mandiri: "VIRTUAL_ACCOUNT_BANK_MANDIRI",
|
|
2840
|
+
bri: "VIRTUAL_ACCOUNT_BRI",
|
|
2841
|
+
bni: "VIRTUAL_ACCOUNT_BNI",
|
|
2842
|
+
permata: "VIRTUAL_ACCOUNT_BANK_PERMATA",
|
|
2843
|
+
cimb: "VIRTUAL_ACCOUNT_BANK_CIMB",
|
|
2844
|
+
danamon: "VIRTUAL_ACCOUNT_BANK_DANAMON",
|
|
2845
|
+
bsi: "VIRTUAL_ACCOUNT_BANK_SYARIAH_MANDIRI",
|
|
2846
|
+
sinarmas: "VIRTUAL_ACCOUNT_SINARMAS",
|
|
2847
|
+
bjb: "VIRTUAL_ACCOUNT_BANK_BJB",
|
|
2848
|
+
btn: "VIRTUAL_ACCOUNT_BTN",
|
|
2849
|
+
bnc: "VIRTUAL_ACCOUNT_BNC"
|
|
2850
|
+
};
|
|
2851
|
+
const isVa = method.includes("_va");
|
|
2852
|
+
const isQris = method === "qris" || method.includes("qris");
|
|
2853
|
+
try {
|
|
2854
|
+
if (isVa) {
|
|
2855
|
+
return await this.snapCreateVA(params, config, snap, baseUrl, bankMap);
|
|
2856
|
+
}
|
|
2857
|
+
if (isQris) {
|
|
2858
|
+
return await this.snapGenerateQRIS(params, config, snap, baseUrl);
|
|
2859
|
+
}
|
|
2860
|
+
return await this.snapEWalletPayment(params, config, snap, baseUrl);
|
|
2861
|
+
} catch (e) {
|
|
2862
|
+
return {
|
|
2863
|
+
success: false,
|
|
2864
|
+
provider: "doku",
|
|
2865
|
+
orderId,
|
|
2866
|
+
amount: integerAmount,
|
|
2867
|
+
rawResponse: null,
|
|
2868
|
+
error: e.message || "Failed to make SNAP request to DOKU API"
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
/** Create Virtual Account (SNAP) — DOKU Generate Payment Code. */
|
|
2873
|
+
async snapCreateVA(params, config, snap, baseUrl, bankMap) {
|
|
2874
|
+
const { orderId, amount, productDetails, customer } = params;
|
|
2875
|
+
const method = String(params.paymentMethod || "").toLowerCase();
|
|
2876
|
+
const bankKey = method.replace(/_va$/, "");
|
|
2877
|
+
const channel = config.extra?.vaChannel || bankMap[bankKey] || `VIRTUAL_ACCOUNT_${bankKey.toUpperCase()}`;
|
|
2878
|
+
const partnerServiceId = (config.extra?.partnerServiceId || "").replace(/\s/g, "");
|
|
2879
|
+
const customerNo = (config.extra?.customerNo || "").slice(0, 20) || String(Date.now()).slice(-12);
|
|
2880
|
+
const virtualAccountNo = (partnerServiceId + customerNo).slice(0, 28);
|
|
2881
|
+
const reusable = config.extra?.reusableStatus === true;
|
|
2882
|
+
const currency = params.currency || "IDR";
|
|
2883
|
+
const body = {
|
|
2884
|
+
partnerServiceId,
|
|
2885
|
+
customerNo,
|
|
2886
|
+
virtualAccountNo,
|
|
2887
|
+
virtualAccountName: customer.name || "Customer",
|
|
2888
|
+
virtualAccountEmail: customer.email,
|
|
2889
|
+
virtualAccountPhone: customer.phone,
|
|
2890
|
+
trxId: orderId,
|
|
2891
|
+
totalAmount: this.snapAmount(amount, currency),
|
|
2892
|
+
virtualAccountTrxType: "C",
|
|
2893
|
+
additionalInfo: {
|
|
2894
|
+
channel,
|
|
2895
|
+
virtualAccountConfig: { reusableStatus: reusable }
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
if (params.providerParams) {
|
|
2899
|
+
Object.assign(body.additionalInfo.virtualAccountConfig, params.providerParams);
|
|
2900
|
+
}
|
|
2901
|
+
if (config.extra?.expiredDate) body.expiredDate = config.extra.expiredDate;
|
|
2902
|
+
const endpoint = "/virtual-accounts/bi-snap-va/v1.1/transfer-va/create-va";
|
|
2903
|
+
const data = await snap.request("POST", endpoint, body);
|
|
2904
|
+
const vaData = data.virtualAccountData || {};
|
|
2905
|
+
return {
|
|
2906
|
+
success: true,
|
|
2907
|
+
provider: "doku",
|
|
2908
|
+
orderId: vaData.trxId || orderId,
|
|
2909
|
+
amount: Number(vaData.totalAmount?.value || amount) || Math.round(amount),
|
|
2910
|
+
reference: vaData.virtualAccountNo || orderId,
|
|
2911
|
+
vaNumber: vaData.virtualAccountNo,
|
|
2912
|
+
vaBank: bankKey.toUpperCase(),
|
|
2913
|
+
paymentUrl: vaData.additionalInfo?.howToPayPage,
|
|
2914
|
+
expiresAt: vaData.expiredDate ? new Date(vaData.expiredDate) : void 0,
|
|
2915
|
+
rawResponse: data
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
/** Generate QRIS (SNAP) — dynamic QRIS MPM. */
|
|
2919
|
+
async snapGenerateQRIS(params, config, snap, baseUrl) {
|
|
2920
|
+
const { orderId, amount } = params;
|
|
2921
|
+
const currency = params.currency || "IDR";
|
|
2922
|
+
const merchantId = config.extra?.merchantId || config.projectId || "";
|
|
2923
|
+
const terminalId = config.extra?.terminalId || "0001";
|
|
2924
|
+
const body = {
|
|
2925
|
+
partnerReferenceNo: orderId,
|
|
2926
|
+
amount: this.snapAmount(amount, currency),
|
|
2927
|
+
merchantId,
|
|
2928
|
+
terminalId,
|
|
2929
|
+
validityPeriod: config.extra?.validityPeriod || new Date(Date.now() + 3600 * 1e3).toISOString(),
|
|
2930
|
+
additionalInfo: {
|
|
2931
|
+
postalCode: config.extra?.postalCode || "10110",
|
|
2932
|
+
feeType: 1
|
|
2933
|
+
}
|
|
2934
|
+
};
|
|
2935
|
+
if (params.providerParams) {
|
|
2936
|
+
Object.assign(body.additionalInfo, params.providerParams);
|
|
2937
|
+
}
|
|
2938
|
+
const endpoint = "/snap-adapter/b2b/v1.0/qr/qr-mpm-generate";
|
|
2939
|
+
const data = await snap.request("POST", endpoint, body);
|
|
2940
|
+
return {
|
|
2941
|
+
success: true,
|
|
2942
|
+
provider: "doku",
|
|
2943
|
+
orderId: data.partnerReferenceNo || orderId,
|
|
2944
|
+
amount: Number(data.amount?.value || amount) || Math.round(amount),
|
|
2945
|
+
reference: data.referenceNo || orderId,
|
|
2946
|
+
qrString: data.qrContent,
|
|
2947
|
+
rawResponse: data
|
|
2948
|
+
};
|
|
2949
|
+
}
|
|
2950
|
+
/** e-Wallet payment (SNAP) — DANA / OVO / ShopeePay via payment-host-to-host. */
|
|
2951
|
+
async snapEWalletPayment(params, config, snap, baseUrl) {
|
|
2952
|
+
const { orderId, amount } = params;
|
|
2953
|
+
const method = String(params.paymentMethod || "").toLowerCase();
|
|
2954
|
+
const currency = params.currency || "IDR";
|
|
2955
|
+
const returnUrl = params.returnUrl || config.returnUrl || "";
|
|
2956
|
+
const channelMap = {
|
|
2957
|
+
dana: "EMONEY_DANA_SNAP",
|
|
2958
|
+
ovo: "EMONEY_OVO_SNAP",
|
|
2959
|
+
shopeepay: "EMONEY_SHOPEEPAY_SNAP"
|
|
2960
|
+
};
|
|
2961
|
+
const channel = config.extra?.ewalletChannel || channelMap[method] || `EMONEY_${method.toUpperCase()}_SNAP`;
|
|
2962
|
+
const body = {
|
|
2963
|
+
partnerReferenceNo: orderId,
|
|
2964
|
+
amount: this.snapAmount(amount, currency),
|
|
2965
|
+
pointOfInitiation: "pc",
|
|
2966
|
+
urlParam: {
|
|
2967
|
+
url: returnUrl || "https://example.com/return",
|
|
2968
|
+
type: "PAY_RETURN",
|
|
2969
|
+
isDeepLink: "N"
|
|
2970
|
+
},
|
|
2971
|
+
additionalInfo: {
|
|
2972
|
+
channel,
|
|
2973
|
+
orderTitle: params.productDetails || `Pembayaran ${orderId}`,
|
|
2974
|
+
supportDeepLinkCheckoutUrl: "false"
|
|
2975
|
+
}
|
|
2976
|
+
};
|
|
2977
|
+
if (params.providerParams) {
|
|
2978
|
+
Object.assign(body.additionalInfo, params.providerParams);
|
|
2979
|
+
}
|
|
2980
|
+
const endpoint = "/direct-debit/core/v1/debit/payment-host-to-host";
|
|
2981
|
+
const data = await snap.request("POST", endpoint, body, {
|
|
2982
|
+
deviceId: config.extra?.deviceId,
|
|
2983
|
+
ipAddress: config.extra?.ipAddress
|
|
2984
|
+
});
|
|
2985
|
+
return {
|
|
2986
|
+
success: true,
|
|
2987
|
+
provider: "doku",
|
|
2988
|
+
orderId: data.partnerReferenceNo || orderId,
|
|
2989
|
+
amount: Number(data.amount?.value || amount) || Math.round(amount),
|
|
2990
|
+
reference: data.partnerReferenceNo || orderId,
|
|
2991
|
+
paymentUrl: data.webRedirectUrl || data.paymentUrl,
|
|
2992
|
+
rawResponse: data
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2619
2995
|
async verifyCallback(body, config) {
|
|
2996
|
+
const headers = config.extra?.headers || {};
|
|
2997
|
+
if (this.isSnap(config)) {
|
|
2998
|
+
return this.verifySnapCallback(body, config, headers);
|
|
2999
|
+
}
|
|
2620
3000
|
const rawStatus = (body.transaction?.status || body.status || "").toUpperCase();
|
|
2621
3001
|
const isPaid = rawStatus === "SUCCESS" || rawStatus === "PAID" || rawStatus === "SETTLED";
|
|
2622
3002
|
const isPending = rawStatus === "PENDING";
|
|
@@ -2626,7 +3006,6 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2626
3006
|
const amount = body.order?.amount || body.amount || 0;
|
|
2627
3007
|
const status = isPaid ? "paid" : isPending ? "pending" : isExpired ? "expired" : "failed";
|
|
2628
3008
|
const secretKey = config.secretKey || config.apiKey || "";
|
|
2629
|
-
const headers = config.extra?.headers || {};
|
|
2630
3009
|
const signature = headers["signature"] || headers["Signature"] || config.extra?.dokuSignature || config.extra?.signatureHeader;
|
|
2631
3010
|
const clientId = config.merchantCode || config.clientKey || "";
|
|
2632
3011
|
let isValid = true;
|
|
@@ -2647,6 +3026,40 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2647
3026
|
rawPayload: body
|
|
2648
3027
|
};
|
|
2649
3028
|
}
|
|
3029
|
+
/**
|
|
3030
|
+
* Verifikasi webhook / notifikasi DOKU SNAP.
|
|
3031
|
+
* Signature dibangun dengan symmetric HMAC-SHA512 (AccessToken kosong).
|
|
3032
|
+
*/
|
|
3033
|
+
verifySnapCallback(body, config, headers) {
|
|
3034
|
+
const clientSecret = config.apiKey || config.serverKey || config.secretKey || "";
|
|
3035
|
+
const endpointUrl = config.extra?.notificationPath || headers["x-path"] || config.extra?.headers?.["request-target"] || "/api/payment/webhook";
|
|
3036
|
+
let isValid = true;
|
|
3037
|
+
const incomingSig = headers["x-signature"] || headers["X-SIGNATURE"] || headers["signature"] || headers["Signature"] || "";
|
|
3038
|
+
if (incomingSig && clientSecret) {
|
|
3039
|
+
isValid = verifySnapWebhookSignature(headers, body, clientSecret, endpointUrl);
|
|
3040
|
+
}
|
|
3041
|
+
const explicitStatus = String(body.transactionStatus || body.status || body.latestTransactionStatus || "").toUpperCase();
|
|
3042
|
+
const isPaid = explicitStatus === "SUCCESS" || explicitStatus === "PAID" || explicitStatus === "SETTLED" || explicitStatus === "00" || !explicitStatus && Boolean(body.paidAmount?.value ?? body.totalAmount?.value);
|
|
3043
|
+
const isPending = explicitStatus === "PENDING" || explicitStatus === "11" || explicitStatus === "ONGOING";
|
|
3044
|
+
const isFailed = explicitStatus === "FAILED" || explicitStatus === "DECLINED" || Boolean(explicitStatus) && !isPaid && !isPending && explicitStatus !== "00";
|
|
3045
|
+
const isExpired = explicitStatus === "EXPIRED";
|
|
3046
|
+
const orderId = body.trxId || body.partnerReferenceNo || body.originalPartnerReferenceNo || body.order?.invoice_number || body.invoice_number || body.order_id || "";
|
|
3047
|
+
const paidValue = body.paidAmount?.value ?? body.totalAmount?.value ?? body.amount?.value ?? body.amount ?? 0;
|
|
3048
|
+
const status = isPaid ? "paid" : isPending ? "pending" : isExpired ? "expired" : "failed";
|
|
3049
|
+
return {
|
|
3050
|
+
isValid,
|
|
3051
|
+
provider: "doku",
|
|
3052
|
+
orderId: String(orderId),
|
|
3053
|
+
amount: Number(paidValue) || 0,
|
|
3054
|
+
status,
|
|
3055
|
+
isPaid,
|
|
3056
|
+
isPending,
|
|
3057
|
+
isFailed,
|
|
3058
|
+
isExpired,
|
|
3059
|
+
statusCode: explicitStatus || "SUCCESS",
|
|
3060
|
+
rawPayload: body
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
2650
3063
|
async getPaymentMethods(params, config) {
|
|
2651
3064
|
const staticMethods = [
|
|
2652
3065
|
{
|
|
@@ -2806,6 +3219,9 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2806
3219
|
const clientId = config.merchantCode || config.merchantId || config.clientKey || "";
|
|
2807
3220
|
const secretKey = config.apiKey || config.serverKey || config.secretKey || "";
|
|
2808
3221
|
const sandbox = !!config.sandbox;
|
|
3222
|
+
if (this.isSnap(config)) {
|
|
3223
|
+
return this.snapCheckTransaction(params, config, clientId);
|
|
3224
|
+
}
|
|
2809
3225
|
const endpoint = `/orders/v1/status/${merchantOrderId}`;
|
|
2810
3226
|
const url = `${this.getBaseUrl(sandbox)}${endpoint}`;
|
|
2811
3227
|
const headers = generateDokuHeaders(clientId, secretKey, endpoint);
|
|
@@ -2883,6 +3299,58 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2883
3299
|
};
|
|
2884
3300
|
}
|
|
2885
3301
|
}
|
|
3302
|
+
/** Cek status transaksi SNAP (menggunakan Query QRIS bila ref berasal dari QRIS). */
|
|
3303
|
+
async snapCheckTransaction(params, config, clientId) {
|
|
3304
|
+
const { merchantOrderId } = params;
|
|
3305
|
+
const snap = this.buildSnap(config);
|
|
3306
|
+
try {
|
|
3307
|
+
const body = {
|
|
3308
|
+
originalPartnerReferenceNo: merchantOrderId,
|
|
3309
|
+
serviceCode: "47",
|
|
3310
|
+
merchantId: config.extra?.merchantId || config.projectId || ""
|
|
3311
|
+
};
|
|
3312
|
+
const data = await snap.request("POST", "/snap-adapter/b2b/v1.0/qr/qr-mpm-query", body);
|
|
3313
|
+
const txStatus = String(data.latestTransactionStatus || "").toUpperCase();
|
|
3314
|
+
const isPaid = txStatus === "SUCCESS" || txStatus === "00" || txStatus === "PAID" || txStatus === "SETTLED";
|
|
3315
|
+
const isPending = txStatus === "PENDING" || txStatus === "ONGOING" || txStatus === "11";
|
|
3316
|
+
const isExpired = txStatus === "EXPIRED";
|
|
3317
|
+
const isFailed = txStatus === "FAILED" || txStatus === "DECLINED" || Boolean(txStatus) && !isPaid && !isPending && !isExpired;
|
|
3318
|
+
const status = isPaid ? "paid" : isPending ? "pending" : isExpired ? "expired" : "failed";
|
|
3319
|
+
return {
|
|
3320
|
+
success: true,
|
|
3321
|
+
provider: "doku",
|
|
3322
|
+
orderId: data.originalPartnerReferenceNo || merchantOrderId,
|
|
3323
|
+
reference: data.originalReferenceNo || "",
|
|
3324
|
+
amount: Number(data.amount?.value || 0),
|
|
3325
|
+
statusCode: txStatus || String(data.responseCode || ""),
|
|
3326
|
+
status,
|
|
3327
|
+
isPaid,
|
|
3328
|
+
isPending,
|
|
3329
|
+
isFailed,
|
|
3330
|
+
isExpired,
|
|
3331
|
+
statusMessage: txStatus || data.responseMessage || "",
|
|
3332
|
+
paymentType: "QRIS",
|
|
3333
|
+
rawResponse: data
|
|
3334
|
+
};
|
|
3335
|
+
} catch (e) {
|
|
3336
|
+
return {
|
|
3337
|
+
success: false,
|
|
3338
|
+
provider: "doku",
|
|
3339
|
+
orderId: merchantOrderId,
|
|
3340
|
+
reference: "",
|
|
3341
|
+
amount: 0,
|
|
3342
|
+
statusCode: "ERROR",
|
|
3343
|
+
status: "failed",
|
|
3344
|
+
isPaid: false,
|
|
3345
|
+
isPending: false,
|
|
3346
|
+
isFailed: true,
|
|
3347
|
+
isExpired: false,
|
|
3348
|
+
statusMessage: e.message || "Failed to check SNAP transaction status",
|
|
3349
|
+
error: e.message || "Failed to check SNAP transaction status",
|
|
3350
|
+
rawResponse: null
|
|
3351
|
+
};
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
2886
3354
|
};
|
|
2887
3355
|
|
|
2888
3356
|
// src/providers/prismalink/signature.ts
|
|
@@ -3276,15 +3744,15 @@ var PrismalinkProvider = class extends BasePaymentProvider {
|
|
|
3276
3744
|
};
|
|
3277
3745
|
|
|
3278
3746
|
// src/providers/faspay/signature.ts
|
|
3279
|
-
import
|
|
3747
|
+
import crypto4 from "crypto";
|
|
3280
3748
|
function generateFaspaySignature(userId, password, billNo) {
|
|
3281
|
-
const md5Hash =
|
|
3282
|
-
return
|
|
3749
|
+
const md5Hash = crypto4.createHash("md5").update(`${userId}${password}${billNo}`).digest("hex");
|
|
3750
|
+
return crypto4.createHash("sha1").update(md5Hash).digest("hex");
|
|
3283
3751
|
}
|
|
3284
3752
|
function verifyFaspaySignature(userId, password, billNo, paymentStatusCode, incomingSignature) {
|
|
3285
3753
|
if (!incomingSignature || !password) return false;
|
|
3286
|
-
const md5Hash =
|
|
3287
|
-
const computed =
|
|
3754
|
+
const md5Hash = crypto4.createHash("md5").update(`${userId}${password}${billNo}${paymentStatusCode}`).digest("hex");
|
|
3755
|
+
const computed = crypto4.createHash("sha1").update(md5Hash).digest("hex");
|
|
3288
3756
|
const simpleComputed = generateFaspaySignature(userId, password, billNo);
|
|
3289
3757
|
return safeCompare(incomingSignature, computed) || safeCompare(incomingSignature, simpleComputed);
|
|
3290
3758
|
}
|
|
@@ -3697,10 +4165,10 @@ var FaspayProvider = class extends BasePaymentProvider {
|
|
|
3697
4165
|
};
|
|
3698
4166
|
|
|
3699
4167
|
// src/providers/finpay/signature.ts
|
|
3700
|
-
import
|
|
4168
|
+
import crypto5 from "crypto";
|
|
3701
4169
|
function generateFinpaySignature(merchantId, orderId, amount, merchantKey) {
|
|
3702
4170
|
const data = `${merchantId}%${orderId}%${Math.round(amount)}%${merchantKey}`;
|
|
3703
|
-
return
|
|
4171
|
+
return crypto5.createHmac("sha512", merchantKey).update(data).digest("hex");
|
|
3704
4172
|
}
|
|
3705
4173
|
function verifyFinpaySignature(merchantId, orderId, amount, merchantKey, incomingSignature) {
|
|
3706
4174
|
if (!incomingSignature || !merchantKey) return false;
|
|
@@ -9442,6 +9910,7 @@ export {
|
|
|
9442
9910
|
PrismalinkProvider,
|
|
9443
9911
|
RazorpayClient,
|
|
9444
9912
|
RazorpayProvider,
|
|
9913
|
+
SnapClient,
|
|
9445
9914
|
SquareClient,
|
|
9446
9915
|
SquareProvider,
|
|
9447
9916
|
StripeClient,
|
|
@@ -9465,6 +9934,8 @@ export {
|
|
|
9465
9934
|
generateNicepayToken,
|
|
9466
9935
|
generateOyHeaders,
|
|
9467
9936
|
generatePrismalinkSignature,
|
|
9937
|
+
generateSnapAsymmetricSignature,
|
|
9938
|
+
generateSnapSymmetricSignature,
|
|
9468
9939
|
getDuitkuInquirySignatures,
|
|
9469
9940
|
getDuitkuPaymentMethodsSignature,
|
|
9470
9941
|
getDuitkuStatusSignatures,
|
|
@@ -9472,6 +9943,7 @@ export {
|
|
|
9472
9943
|
getXenditAuthHeader,
|
|
9473
9944
|
hmacSha256,
|
|
9474
9945
|
md5,
|
|
9946
|
+
minifyJson,
|
|
9475
9947
|
parseCoreChargeResponse,
|
|
9476
9948
|
paymentManager,
|
|
9477
9949
|
resolveConfigFromEnv,
|
|
@@ -9479,7 +9951,10 @@ export {
|
|
|
9479
9951
|
serializePaypalParams,
|
|
9480
9952
|
serializeStripeParams,
|
|
9481
9953
|
sha256,
|
|
9954
|
+
sha256Hex,
|
|
9482
9955
|
sha512,
|
|
9956
|
+
snapExternalId,
|
|
9957
|
+
snapTimestamp,
|
|
9483
9958
|
toCanonicalPaymentMethod,
|
|
9484
9959
|
toDokuPaymentMethod,
|
|
9485
9960
|
toDuitkuPaymentMethod,
|
|
@@ -9505,6 +9980,7 @@ export {
|
|
|
9505
9980
|
verifyPayuWebhook,
|
|
9506
9981
|
verifyPrismalinkSignature,
|
|
9507
9982
|
verifyRazorpayWebhook,
|
|
9983
|
+
verifySnapWebhookSignature,
|
|
9508
9984
|
verifySquareWebhook,
|
|
9509
9985
|
verifyStripeWebhook,
|
|
9510
9986
|
verifyTwoCheckoutWebhook,
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediblemark/buayar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Unified Payment Gateway SDK for Node.js & TypeScript — 19 providers (Midtrans, Xendit, Duitku, Stripe, PayPal, Adyen, Razorpay, Square, Checkout.com, PayU, Braintree, 2Checkout, DOKU, iPaymu, PrismaLink, Faspay, Finpay, Nicepay, OY!) with zero-code switching via .env",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"buayar": "./dist/cli/index.js"
|
|
10
|
+
},
|
|
8
11
|
"exports": {
|
|
9
12
|
".": {
|
|
10
13
|
"types": "./dist/index.d.ts",
|
|
@@ -68,14 +71,19 @@
|
|
|
68
71
|
},
|
|
69
72
|
"license": "MIT",
|
|
70
73
|
"scripts": {
|
|
71
|
-
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
72
|
-
"dev": "tsup src/index.ts --format cjs,esm --dts --watch"
|
|
74
|
+
"build": "tsup src/index.ts --format cjs,esm --dts && tsup src/cli/index.ts --format cjs --platform node --out-dir dist/cli && node -e \"const fs=require('fs');const f='dist/cli/index.js';const s=fs.readFileSync(f,'utf8');if(!s.startsWith('#!'))fs.writeFileSync(f,'#!/usr/bin/env node\\n'+s);fs.chmodSync(f,0o755);\"",
|
|
75
|
+
"dev": "tsup src/index.ts src/cli/index.ts --format cjs,esm --dts --out-dir dist --watch",
|
|
76
|
+
"init": "node ./dist/cli/index.js init",
|
|
77
|
+
"postinstall": "echo \"🐊 Buayar terpasang! Jalankan \\\"npx buayar init\\\" untuk membuat boilerplate payment yang siap pakai.\"",
|
|
78
|
+
"test": "bun test"
|
|
73
79
|
},
|
|
74
80
|
"devDependencies": {
|
|
75
81
|
"@types/bun": "^1.4.0",
|
|
76
82
|
"@types/node": "^24.12.2",
|
|
77
83
|
"tsup": "^8.5.1",
|
|
78
84
|
"typescript": "^5.9.3"
|
|
85
|
+
},
|
|
86
|
+
"dependencies": {
|
|
87
|
+
"@clack/prompts": "^1.7.0"
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
|
-
|