From f7e858ca208432956e5642afc07680274dfed4c2 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 17 Jul 2026 16:52:53 +0200 Subject: [PATCH] feat: prebuild dist for ESM + CJS --- .gitignore | 1 + dist/index.cjs | 175 ++++++++++++++++++++++++++++++++++++++++++++++ dist/index.d.ts | 107 ++++++++++++++++++++++++++++ dist/index.js | 170 ++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 51 ++++++++++++++ package.json | 4 +- 6 files changed, 506 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 dist/index.cjs create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/dist/index.cjs b/dist/index.cjs new file mode 100644 index 0000000..e0a3a68 --- /dev/null +++ b/dist/index.cjs @@ -0,0 +1,175 @@ +"use strict"; +/** + * @qbirr/sdk — Node.js client for the qbirr payment verification API. + * + * import { Qbirr } from '@qbirr/sdk'; + * const qb = new Qbirr({ apiKey: process.env.QBIRR_API_KEY! }); + * const r = await qb.verify({ + * provider: 'cbe', + * ref: 'FT23001234ABC', + * amount: 500, + * receiverName: 'YOUR NAME', + * receiverAccount: '1000017692643', + * }); + * console.log(r.verified, r.payer); + * + * Works on Node 18+ (uses the built-in fetch). No dependencies. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Qbirr = exports.QbirrError = void 0; +/** Thrown for network failures, HTTP errors, or non-JSON responses. */ +class QbirrError extends Error { + status; + body; + constructor(message, status, body) { + super(message); + this.status = status; + this.body = body; + this.name = 'QbirrError'; + } +} +exports.QbirrError = QbirrError; +const DEFAULT_BASE_URL = 'https://verify.qbirr.com'; +class Qbirr { + apiKey; + baseUrl; + timeoutMs; + fetchFn; + constructor(opts) { + if (!opts.apiKey) + throw new QbirrError('apiKey is required'); + this.apiKey = opts.apiKey; + this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ''); + this.timeoutMs = opts.timeoutMs ?? 30_000; + this.fetchFn = opts.fetch ?? globalThis.fetch; + if (!this.fetchFn) + throw new QbirrError('fetch is not available — pass options.fetch on Node < 18'); + } + /** + * Verify a single transaction. Returns `{ verified: false, error: '...' }` + * on a real verification failure (wrong amount, wrong receiver, expired + * receipt). Throws QbirrError for network/auth problems. + */ + async verify(req) { + const body = { + provider: req.provider, + ref: req.ref, + amount: req.amount, + receiver_name: req.receiverName, + }; + if (req.receiverAccount) + body.receiver_account = req.receiverAccount; + const data = await this.request('POST', '/api/v1/verify', body); + return data; + } + /** + * Verify by uploading the receipt itself — PDF or an image (PNG/JPEG). + * Images with a QR code (most bank receipts have one) are decoded and + * routed through the URL-based flow automatically. + * + * const buf = await fs.readFile('receipt.pdf'); + * await qb.verifyUpload({ + * provider: 'cbe', + * amount: 999, + * receiverName: 'YOUR NAME', + * receiverAccount: '1000017692643', + * file: buf, + * filename: 'receipt.pdf', + * contentType: 'application/pdf', + * }); + */ + async verifyUpload(opts) { + const form = new FormData(); + form.append('provider', opts.provider); + form.append('amount', String(opts.amount)); + form.append('receiver_name', opts.receiverName); + if (opts.receiverAccount) + form.append('receiver_account', opts.receiverAccount); + const blob = opts.file instanceof Blob + ? opts.file + : new Blob([opts.file], { type: opts.contentType || 'application/octet-stream' }); + form.append('file', blob, opts.filename || 'upload.pdf'); + return this.requestForm('POST', '/api/v1/verify/upload', form); + } + /** Returns the current month's quota usage for this API key. */ + async usage() { + return this.request('GET', '/api/v1/usage'); + } + async requestForm(method, path, form) { + const url = `${this.baseUrl}${path}`; + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), this.timeoutMs); + try { + const resp = await this.fetchFn(url, { + method, + headers: { 'X-API-Key': this.apiKey, 'User-Agent': '@qbirr/sdk-node/1.0.0' }, + body: form, + signal: ctrl.signal, + }); + const text = await resp.text(); + let parsed; + try { + parsed = text ? JSON.parse(text) : {}; + } + catch { + throw new QbirrError(`Non-JSON response from qbirr (HTTP ${resp.status})`, resp.status, text); + } + if (!resp.ok) + throw new QbirrError(parsed?.error || `HTTP ${resp.status}`, resp.status, parsed); + return parsed; + } + catch (e) { + if (e instanceof QbirrError) + throw e; + if (e?.name === 'AbortError') + throw new QbirrError(`Upload timed out after ${this.timeoutMs}ms`); + throw new QbirrError(`Network error: ${e.message}`); + } + finally { + clearTimeout(timer); + } + } + async request(method, path, body) { + const url = `${this.baseUrl}${path}`; + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), this.timeoutMs); + try { + const resp = await this.fetchFn(url, { + method, + headers: { + 'X-API-Key': this.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': '@qbirr/sdk-node/1.0.0', + }, + body: body ? JSON.stringify(body) : undefined, + signal: ctrl.signal, + }); + let parsed; + const text = await resp.text(); + try { + parsed = text ? JSON.parse(text) : {}; + } + catch { + throw new QbirrError(`Non-JSON response from qbirr (HTTP ${resp.status})`, resp.status, text); + } + if (!resp.ok) { + const msg = parsed?.error || `HTTP ${resp.status}`; + throw new QbirrError(msg, resp.status, parsed); + } + return parsed; + } + catch (e) { + if (e instanceof QbirrError) + throw e; + if (e?.name === 'AbortError') + throw new QbirrError(`Request timed out after ${this.timeoutMs}ms`); + throw new QbirrError(`Network error: ${e.message}`); + } + finally { + clearTimeout(timer); + } + } +} +exports.Qbirr = Qbirr; +exports.default = Qbirr; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..6dca560 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,107 @@ +/** + * @qbirr/sdk — Node.js client for the qbirr payment verification API. + * + * import { Qbirr } from '@qbirr/sdk'; + * const qb = new Qbirr({ apiKey: process.env.QBIRR_API_KEY! }); + * const r = await qb.verify({ + * provider: 'cbe', + * ref: 'FT23001234ABC', + * amount: 500, + * receiverName: 'YOUR NAME', + * receiverAccount: '1000017692643', + * }); + * console.log(r.verified, r.payer); + * + * Works on Node 18+ (uses the built-in fetch). No dependencies. + */ +export type Provider = 'cbe' | 'telebirr' | 'abyssinia' | 'dashen' | 'mpesa' | 'awash' | 'ebirr' | 'zamzam'; +export interface VerifyRequest { + provider: Provider; + /** Transaction reference (or, for Awash/eBirr, the SMS URL / URL token). */ + ref: string; + /** Expected payment amount in ETB. */ + amount: number; + /** Exact receiver name (case-insensitive, whitespace-normalised match). */ + receiverName: string; + /** Required for CBE and Bank of Abyssinia. Ignored for other providers. */ + receiverAccount?: string; +} +export interface VerifyResult { + /** True if the receipt is real, the amount matches, and the receiver matches. */ + verified: boolean; + /** Payer name as extracted from the receipt. */ + payer: string; + /** Amount that was actually paid (per the receipt). */ + amount: number; + /** Human-readable failure reason, empty on success. */ + error: string; +} +export interface UsageStatus { + used: number; + limit: number; + remaining: number; + plan: string; + /** ISO date — first instant of the current monthly window. */ + period_start: string; + /** ISO date — first instant of the next monthly window. */ + period_end: string; +} +export interface QbirrOptions { + /** Your API key from the qbirr dashboard. */ + apiKey: string; + /** Override base URL. Defaults to https://verify.qbirr.com */ + baseUrl?: string; + /** Per-request timeout (ms). Defaults to 30 000. */ + timeoutMs?: number; + /** Optional fetch override (for tests). */ + fetch?: typeof fetch; +} +/** Thrown for network failures, HTTP errors, or non-JSON responses. */ +export declare class QbirrError extends Error { + readonly status?: number | undefined; + readonly body?: unknown | undefined; + constructor(message: string, status?: number | undefined, body?: unknown | undefined); +} +export declare class Qbirr { + private readonly apiKey; + private readonly baseUrl; + private readonly timeoutMs; + private readonly fetchFn; + constructor(opts: QbirrOptions); + /** + * Verify a single transaction. Returns `{ verified: false, error: '...' }` + * on a real verification failure (wrong amount, wrong receiver, expired + * receipt). Throws QbirrError for network/auth problems. + */ + verify(req: VerifyRequest): Promise; + /** + * Verify by uploading the receipt itself — PDF or an image (PNG/JPEG). + * Images with a QR code (most bank receipts have one) are decoded and + * routed through the URL-based flow automatically. + * + * const buf = await fs.readFile('receipt.pdf'); + * await qb.verifyUpload({ + * provider: 'cbe', + * amount: 999, + * receiverName: 'YOUR NAME', + * receiverAccount: '1000017692643', + * file: buf, + * filename: 'receipt.pdf', + * contentType: 'application/pdf', + * }); + */ + verifyUpload(opts: { + provider: Provider; + amount: number; + receiverName: string; + receiverAccount?: string; + file: Buffer | Uint8Array | Blob; + filename?: string; + contentType?: string; + }): Promise; + /** Returns the current month's quota usage for this API key. */ + usage(): Promise; + private requestForm; + private request; +} +export default Qbirr; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..68d8182 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,170 @@ +/** + * @qbirr/sdk — Node.js client for the qbirr payment verification API. + * + * import { Qbirr } from '@qbirr/sdk'; + * const qb = new Qbirr({ apiKey: process.env.QBIRR_API_KEY! }); + * const r = await qb.verify({ + * provider: 'cbe', + * ref: 'FT23001234ABC', + * amount: 500, + * receiverName: 'YOUR NAME', + * receiverAccount: '1000017692643', + * }); + * console.log(r.verified, r.payer); + * + * Works on Node 18+ (uses the built-in fetch). No dependencies. + */ +/** Thrown for network failures, HTTP errors, or non-JSON responses. */ +export class QbirrError extends Error { + status; + body; + constructor(message, status, body) { + super(message); + this.status = status; + this.body = body; + this.name = 'QbirrError'; + } +} +const DEFAULT_BASE_URL = 'https://verify.qbirr.com'; +export class Qbirr { + apiKey; + baseUrl; + timeoutMs; + fetchFn; + constructor(opts) { + if (!opts.apiKey) + throw new QbirrError('apiKey is required'); + this.apiKey = opts.apiKey; + this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ''); + this.timeoutMs = opts.timeoutMs ?? 30_000; + this.fetchFn = opts.fetch ?? globalThis.fetch; + if (!this.fetchFn) + throw new QbirrError('fetch is not available — pass options.fetch on Node < 18'); + } + /** + * Verify a single transaction. Returns `{ verified: false, error: '...' }` + * on a real verification failure (wrong amount, wrong receiver, expired + * receipt). Throws QbirrError for network/auth problems. + */ + async verify(req) { + const body = { + provider: req.provider, + ref: req.ref, + amount: req.amount, + receiver_name: req.receiverName, + }; + if (req.receiverAccount) + body.receiver_account = req.receiverAccount; + const data = await this.request('POST', '/api/v1/verify', body); + return data; + } + /** + * Verify by uploading the receipt itself — PDF or an image (PNG/JPEG). + * Images with a QR code (most bank receipts have one) are decoded and + * routed through the URL-based flow automatically. + * + * const buf = await fs.readFile('receipt.pdf'); + * await qb.verifyUpload({ + * provider: 'cbe', + * amount: 999, + * receiverName: 'YOUR NAME', + * receiverAccount: '1000017692643', + * file: buf, + * filename: 'receipt.pdf', + * contentType: 'application/pdf', + * }); + */ + async verifyUpload(opts) { + const form = new FormData(); + form.append('provider', opts.provider); + form.append('amount', String(opts.amount)); + form.append('receiver_name', opts.receiverName); + if (opts.receiverAccount) + form.append('receiver_account', opts.receiverAccount); + const blob = opts.file instanceof Blob + ? opts.file + : new Blob([opts.file], { type: opts.contentType || 'application/octet-stream' }); + form.append('file', blob, opts.filename || 'upload.pdf'); + return this.requestForm('POST', '/api/v1/verify/upload', form); + } + /** Returns the current month's quota usage for this API key. */ + async usage() { + return this.request('GET', '/api/v1/usage'); + } + async requestForm(method, path, form) { + const url = `${this.baseUrl}${path}`; + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), this.timeoutMs); + try { + const resp = await this.fetchFn(url, { + method, + headers: { 'X-API-Key': this.apiKey, 'User-Agent': '@qbirr/sdk-node/1.0.0' }, + body: form, + signal: ctrl.signal, + }); + const text = await resp.text(); + let parsed; + try { + parsed = text ? JSON.parse(text) : {}; + } + catch { + throw new QbirrError(`Non-JSON response from qbirr (HTTP ${resp.status})`, resp.status, text); + } + if (!resp.ok) + throw new QbirrError(parsed?.error || `HTTP ${resp.status}`, resp.status, parsed); + return parsed; + } + catch (e) { + if (e instanceof QbirrError) + throw e; + if (e?.name === 'AbortError') + throw new QbirrError(`Upload timed out after ${this.timeoutMs}ms`); + throw new QbirrError(`Network error: ${e.message}`); + } + finally { + clearTimeout(timer); + } + } + async request(method, path, body) { + const url = `${this.baseUrl}${path}`; + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), this.timeoutMs); + try { + const resp = await this.fetchFn(url, { + method, + headers: { + 'X-API-Key': this.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': '@qbirr/sdk-node/1.0.0', + }, + body: body ? JSON.stringify(body) : undefined, + signal: ctrl.signal, + }); + let parsed; + const text = await resp.text(); + try { + parsed = text ? JSON.parse(text) : {}; + } + catch { + throw new QbirrError(`Non-JSON response from qbirr (HTTP ${resp.status})`, resp.status, text); + } + if (!resp.ok) { + const msg = parsed?.error || `HTTP ${resp.status}`; + throw new QbirrError(msg, resp.status, parsed); + } + return parsed; + } + catch (e) { + if (e instanceof QbirrError) + throw e; + if (e?.name === 'AbortError') + throw new QbirrError(`Request timed out after ${this.timeoutMs}ms`); + throw new QbirrError(`Network error: ${e.message}`); + } + finally { + clearTimeout(timer); + } + } +} +export default Qbirr; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2faeccb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,51 @@ +{ + "name": "@qbirr/sdk", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@qbirr/sdk", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^22.7.5", + "typescript": "^5.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json index 78e4c6e..a19e127 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "files": ["dist", "README.md"], "engines": { "node": ">=18" }, "scripts": { - "build": "tsc && tsc --module commonjs --outDir dist-cjs --declaration false 2>/dev/null && cp dist-cjs/index.js dist/index.cjs && rm -rf dist-cjs", - "prepare": "tsc && tsc --module commonjs --outDir dist-cjs --declaration false 2>/dev/null && cp dist-cjs/index.js dist/index.cjs && rm -rf dist-cjs" + "build": "tsc && cp dist/index.js dist/index.cjs", + "prepare": "" }, "devDependencies": { "typescript": "^5.7",