@patalink/node-api-client 1.0.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/README.md ADDED
@@ -0,0 +1 @@
1
+ # patalink_node_api_client
@@ -0,0 +1,55 @@
1
+ interface PaymentRequest {
2
+ amount: number;
3
+ currency?: string;
4
+ paymentMethod: 'MTN' | 'Airtel' | 'PesaPal';
5
+ phoneNumber?: string;
6
+ customerName?: string;
7
+ customerEmail?: string;
8
+ }
9
+ interface PaymentResponse {
10
+ transactionId: string;
11
+ status: 'pending' | 'success' | 'failed';
12
+ providerRef?: string;
13
+ redirectUrl?: string;
14
+ message?: string;
15
+ }
16
+ interface TransactionStatus {
17
+ id: string;
18
+ status: 'pending' | 'success' | 'failed';
19
+ amount: number;
20
+ currency: string;
21
+ paymentMethod: string;
22
+ createdAt: string;
23
+ completedAt?: string;
24
+ providerRef?: string;
25
+ }
26
+ interface ClientConfig {
27
+ apiKey: string;
28
+ encryptionKey: string;
29
+ timeout?: number;
30
+ }
31
+ declare class PataLinkClient {
32
+ private apiKey;
33
+ private encryptionKey;
34
+ private timeout;
35
+ constructor(config: ClientConfig);
36
+ /**
37
+ * Encrypt payment data before sending to API
38
+ */
39
+ private encrypt;
40
+ /**
41
+ * Make authenticated request to API
42
+ */
43
+ private request;
44
+ /**
45
+ * Create a payment
46
+ */
47
+ createPayment(request: PaymentRequest): Promise<PaymentResponse>;
48
+ /**
49
+ * Get transaction status
50
+ */
51
+ getTransactionStatus(transactionId: string): Promise<TransactionStatus>;
52
+ }
53
+ declare function createClient(config: ClientConfig): PataLinkClient;
54
+
55
+ export { type ClientConfig, type PaymentRequest, type PaymentResponse, type TransactionStatus, createClient, PataLinkClient as default };
@@ -0,0 +1,55 @@
1
+ interface PaymentRequest {
2
+ amount: number;
3
+ currency?: string;
4
+ paymentMethod: 'MTN' | 'Airtel' | 'PesaPal';
5
+ phoneNumber?: string;
6
+ customerName?: string;
7
+ customerEmail?: string;
8
+ }
9
+ interface PaymentResponse {
10
+ transactionId: string;
11
+ status: 'pending' | 'success' | 'failed';
12
+ providerRef?: string;
13
+ redirectUrl?: string;
14
+ message?: string;
15
+ }
16
+ interface TransactionStatus {
17
+ id: string;
18
+ status: 'pending' | 'success' | 'failed';
19
+ amount: number;
20
+ currency: string;
21
+ paymentMethod: string;
22
+ createdAt: string;
23
+ completedAt?: string;
24
+ providerRef?: string;
25
+ }
26
+ interface ClientConfig {
27
+ apiKey: string;
28
+ encryptionKey: string;
29
+ timeout?: number;
30
+ }
31
+ declare class PataLinkClient {
32
+ private apiKey;
33
+ private encryptionKey;
34
+ private timeout;
35
+ constructor(config: ClientConfig);
36
+ /**
37
+ * Encrypt payment data before sending to API
38
+ */
39
+ private encrypt;
40
+ /**
41
+ * Make authenticated request to API
42
+ */
43
+ private request;
44
+ /**
45
+ * Create a payment
46
+ */
47
+ createPayment(request: PaymentRequest): Promise<PaymentResponse>;
48
+ /**
49
+ * Get transaction status
50
+ */
51
+ getTransactionStatus(transactionId: string): Promise<TransactionStatus>;
52
+ }
53
+ declare function createClient(config: ClientConfig): PataLinkClient;
54
+
55
+ export { type ClientConfig, type PaymentRequest, type PaymentResponse, type TransactionStatus, createClient, PataLinkClient as default };
package/dist/index.js ADDED
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ createClient: () => createClient,
34
+ default: () => index_default
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+ var CryptoJS = __toESM(require("crypto-js"));
38
+ var BASE_URL = "http://localhost:3001";
39
+ var PataLinkClient = class {
40
+ constructor(config) {
41
+ if (!config.apiKey) throw new Error("API key is required");
42
+ if (!config.encryptionKey) throw new Error("Encryption key is required");
43
+ this.apiKey = config.apiKey;
44
+ this.encryptionKey = config.encryptionKey;
45
+ this.timeout = config.timeout || 3e4;
46
+ }
47
+ /**
48
+ * Encrypt payment data before sending to API
49
+ */
50
+ encrypt(data) {
51
+ const dataString = JSON.stringify(data);
52
+ const keyWA = CryptoJS.enc.Base64.parse(this.encryptionKey);
53
+ const iv = CryptoJS.lib.WordArray.random(16);
54
+ const ivHex = iv.toString(CryptoJS.enc.Hex);
55
+ const ivWA = CryptoJS.enc.Hex.parse(ivHex);
56
+ const encrypted = CryptoJS.AES.encrypt(dataString, keyWA, {
57
+ iv: ivWA,
58
+ mode: CryptoJS.mode.CBC,
59
+ padding: CryptoJS.pad.Pkcs7
60
+ });
61
+ const encryptedBase64 = encrypted.ciphertext.toString(CryptoJS.enc.Base64);
62
+ const hmac = CryptoJS.HmacSHA256(dataString, keyWA).toString();
63
+ return {
64
+ encrypted: encryptedBase64,
65
+ iv: ivHex,
66
+ hmac
67
+ };
68
+ }
69
+ /**
70
+ * Make authenticated request to API
71
+ */
72
+ async request(endpoint, options = {}) {
73
+ const controller = new AbortController();
74
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
75
+ try {
76
+ const response = await fetch(`${BASE_URL}${endpoint}`, {
77
+ ...options,
78
+ headers: {
79
+ "Authorization": `Bearer ${this.apiKey}`,
80
+ "Content-Type": "application/json",
81
+ ...options.headers
82
+ },
83
+ signal: controller.signal
84
+ });
85
+ clearTimeout(timeoutId);
86
+ if (!response.ok) {
87
+ const error = await response.json();
88
+ throw new Error(error.message || `HTTP ${response.status}`);
89
+ }
90
+ return await response.json();
91
+ } catch (error) {
92
+ clearTimeout(timeoutId);
93
+ if (error instanceof Error && error.name === "AbortError") {
94
+ throw new Error("Request timeout");
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+ /**
100
+ * Create a payment
101
+ */
102
+ async createPayment(request) {
103
+ if (!request.amount) throw new Error("amount is required");
104
+ if (!request.paymentMethod) throw new Error("paymentMethod is required");
105
+ if (request.paymentMethod !== "PesaPal" && !request.phoneNumber) {
106
+ throw new Error("phoneNumber is required for mobile money payments");
107
+ }
108
+ if (request.amount < 100) {
109
+ throw new Error("Amount must be at least 100 RWF");
110
+ }
111
+ if (request.amount > 2e6 && request.paymentMethod !== "PesaPal") {
112
+ throw new Error("Mobile Money limit is 2,000,000 RWF");
113
+ }
114
+ const paymentData = {
115
+ amount: request.amount,
116
+ currency: request.currency || "RWF",
117
+ paymentMethod: request.paymentMethod,
118
+ supporterPhoneNumber: request.phoneNumber || "",
119
+ supporterName: request.customerName || "",
120
+ supporterEmail: request.customerEmail || ""
121
+ };
122
+ const encryptedPayload = this.encrypt(paymentData);
123
+ const response = await this.request("/api/pay", {
124
+ method: "POST",
125
+ headers: {
126
+ "x-encrypted": "true"
127
+ },
128
+ body: JSON.stringify(encryptedPayload)
129
+ });
130
+ return response;
131
+ }
132
+ /**
133
+ * Get transaction status
134
+ */
135
+ async getTransactionStatus(transactionId) {
136
+ if (!transactionId) throw new Error("transactionId is required");
137
+ return await this.request(`/api/pay/${transactionId}`, {
138
+ method: "GET"
139
+ });
140
+ }
141
+ /**
142
+ * Poll for transaction completion
143
+ */
144
+ // async waitForCompletion(
145
+ // transactionId: string,
146
+ // options?: {
147
+ // maxAttempts?: number;
148
+ // interval?: number;
149
+ // onProgress?: (attempt: number, status: string) => void;
150
+ // }
151
+ // ): Promise<TransactionStatus> {
152
+ // const maxAttempts = options?.maxAttempts || 60;
153
+ // const interval = options?.interval || 2000;
154
+ // let attempts = 0;
155
+ // while (attempts < maxAttempts) {
156
+ // attempts++;
157
+ // options?.onProgress?.(attempts, 'checking');
158
+ // const status = await this.getTransactionStatus(transactionId);
159
+ // if (status.status === 'success' || status.status === 'failed') {
160
+ // return status;
161
+ // }
162
+ // // Wait before next attempt
163
+ // await new Promise(resolve => setTimeout(resolve, interval));
164
+ // }
165
+ // throw new Error('Transaction timeout - still pending after maximum attempts');
166
+ // }
167
+ /**
168
+ * List all transactions for this account
169
+ */
170
+ // async listTransactions(options?: {
171
+ // limit?: number;
172
+ // offset?: number;
173
+ // status?: 'pending' | 'success' | 'failed';
174
+ // }): Promise<{ transactions: TransactionStatus[]; total: number }> {
175
+ // const params = new URLSearchParams();
176
+ // if (options?.limit) params.append('limit', options.limit.toString());
177
+ // if (options?.offset) params.append('offset', options.offset.toString());
178
+ // if (options?.status) params.append('status', options.status);
179
+ // const query = params.toString();
180
+ // const endpoint = `/v1/transactions${query ? `?${query}` : ''}`;
181
+ // return this.request<{ transactions: TransactionStatus[]; total: number }>(endpoint, {
182
+ // method: 'GET'
183
+ // });
184
+ // }
185
+ /**
186
+ * Rotate encryption key (generates new key, revokes old one after 24h)
187
+ */
188
+ // async rotateEncryptionKey(): Promise<{ newEncryptionKey: string; message: string }> {
189
+ // return this.request<{ newEncryptionKey: string; message: string }>('/v1/keys/rotate', {
190
+ // method: 'POST'
191
+ // });
192
+ // }
193
+ /**
194
+ * Get account information
195
+ */
196
+ // async getAccountInfo(): Promise<{
197
+ // id: string;
198
+ // email: string;
199
+ // walletBalance: number;
200
+ // pendingBalance: number;
201
+ // availableBalance: number;
202
+ // }> {
203
+ // return this.request('/v1/account', { method: 'GET' });
204
+ // }
205
+ /**
206
+ * Initiate withdrawal
207
+ */
208
+ // async createWithdrawal(request: {
209
+ // amount: number;
210
+ // phoneNumber: string;
211
+ // network: 'MTN' | 'Airtel';
212
+ // }): Promise<{ withdrawalId: string; status: string; message: string }> {
213
+ // if (!request.amount) throw new Error('amount is required');
214
+ // if (!request.phoneNumber) throw new Error('phoneNumber is required');
215
+ // if (request.amount < 100) throw new Error('Minimum withdrawal is 100 RWF');
216
+ // if (request.amount > 2000000) throw new Error('Maximum withdrawal is 2,000,000 RWF');
217
+ // return this.request('/v1/withdrawals', {
218
+ // method: 'POST',
219
+ // body: JSON.stringify(request)
220
+ // });
221
+ // }
222
+ /**
223
+ * List withdrawals
224
+ */
225
+ // async listWithdrawals(options?: {
226
+ // limit?: number;
227
+ // offset?: number;
228
+ // }): Promise<{ withdrawals: any[]; total: number }> {
229
+ // const params = new URLSearchParams();
230
+ // if (options?.limit) params.append('limit', options.limit.toString());
231
+ // if (options?.offset) params.append('offset', options.offset.toString());
232
+ // const query = params.toString();
233
+ // const endpoint = `/v1/withdrawals${query ? `?${query}` : ''}`;
234
+ // return this.request(endpoint, { method: 'GET' });
235
+ // }
236
+ };
237
+ function createClient(config) {
238
+ return new PataLinkClient(config);
239
+ }
240
+ var index_default = PataLinkClient;
241
+ // Annotate the CommonJS export names for ESM import in node:
242
+ 0 && (module.exports = {
243
+ createClient
244
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,209 @@
1
+ // src/index.ts
2
+ import * as CryptoJS from "crypto-js";
3
+ var BASE_URL = "http://localhost:3001";
4
+ var PataLinkClient = class {
5
+ constructor(config) {
6
+ if (!config.apiKey) throw new Error("API key is required");
7
+ if (!config.encryptionKey) throw new Error("Encryption key is required");
8
+ this.apiKey = config.apiKey;
9
+ this.encryptionKey = config.encryptionKey;
10
+ this.timeout = config.timeout || 3e4;
11
+ }
12
+ /**
13
+ * Encrypt payment data before sending to API
14
+ */
15
+ encrypt(data) {
16
+ const dataString = JSON.stringify(data);
17
+ const keyWA = CryptoJS.enc.Base64.parse(this.encryptionKey);
18
+ const iv = CryptoJS.lib.WordArray.random(16);
19
+ const ivHex = iv.toString(CryptoJS.enc.Hex);
20
+ const ivWA = CryptoJS.enc.Hex.parse(ivHex);
21
+ const encrypted = CryptoJS.AES.encrypt(dataString, keyWA, {
22
+ iv: ivWA,
23
+ mode: CryptoJS.mode.CBC,
24
+ padding: CryptoJS.pad.Pkcs7
25
+ });
26
+ const encryptedBase64 = encrypted.ciphertext.toString(CryptoJS.enc.Base64);
27
+ const hmac = CryptoJS.HmacSHA256(dataString, keyWA).toString();
28
+ return {
29
+ encrypted: encryptedBase64,
30
+ iv: ivHex,
31
+ hmac
32
+ };
33
+ }
34
+ /**
35
+ * Make authenticated request to API
36
+ */
37
+ async request(endpoint, options = {}) {
38
+ const controller = new AbortController();
39
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
40
+ try {
41
+ const response = await fetch(`${BASE_URL}${endpoint}`, {
42
+ ...options,
43
+ headers: {
44
+ "Authorization": `Bearer ${this.apiKey}`,
45
+ "Content-Type": "application/json",
46
+ ...options.headers
47
+ },
48
+ signal: controller.signal
49
+ });
50
+ clearTimeout(timeoutId);
51
+ if (!response.ok) {
52
+ const error = await response.json();
53
+ throw new Error(error.message || `HTTP ${response.status}`);
54
+ }
55
+ return await response.json();
56
+ } catch (error) {
57
+ clearTimeout(timeoutId);
58
+ if (error instanceof Error && error.name === "AbortError") {
59
+ throw new Error("Request timeout");
60
+ }
61
+ throw error;
62
+ }
63
+ }
64
+ /**
65
+ * Create a payment
66
+ */
67
+ async createPayment(request) {
68
+ if (!request.amount) throw new Error("amount is required");
69
+ if (!request.paymentMethod) throw new Error("paymentMethod is required");
70
+ if (request.paymentMethod !== "PesaPal" && !request.phoneNumber) {
71
+ throw new Error("phoneNumber is required for mobile money payments");
72
+ }
73
+ if (request.amount < 100) {
74
+ throw new Error("Amount must be at least 100 RWF");
75
+ }
76
+ if (request.amount > 2e6 && request.paymentMethod !== "PesaPal") {
77
+ throw new Error("Mobile Money limit is 2,000,000 RWF");
78
+ }
79
+ const paymentData = {
80
+ amount: request.amount,
81
+ currency: request.currency || "RWF",
82
+ paymentMethod: request.paymentMethod,
83
+ supporterPhoneNumber: request.phoneNumber || "",
84
+ supporterName: request.customerName || "",
85
+ supporterEmail: request.customerEmail || ""
86
+ };
87
+ const encryptedPayload = this.encrypt(paymentData);
88
+ const response = await this.request("/api/pay", {
89
+ method: "POST",
90
+ headers: {
91
+ "x-encrypted": "true"
92
+ },
93
+ body: JSON.stringify(encryptedPayload)
94
+ });
95
+ return response;
96
+ }
97
+ /**
98
+ * Get transaction status
99
+ */
100
+ async getTransactionStatus(transactionId) {
101
+ if (!transactionId) throw new Error("transactionId is required");
102
+ return await this.request(`/api/pay/${transactionId}`, {
103
+ method: "GET"
104
+ });
105
+ }
106
+ /**
107
+ * Poll for transaction completion
108
+ */
109
+ // async waitForCompletion(
110
+ // transactionId: string,
111
+ // options?: {
112
+ // maxAttempts?: number;
113
+ // interval?: number;
114
+ // onProgress?: (attempt: number, status: string) => void;
115
+ // }
116
+ // ): Promise<TransactionStatus> {
117
+ // const maxAttempts = options?.maxAttempts || 60;
118
+ // const interval = options?.interval || 2000;
119
+ // let attempts = 0;
120
+ // while (attempts < maxAttempts) {
121
+ // attempts++;
122
+ // options?.onProgress?.(attempts, 'checking');
123
+ // const status = await this.getTransactionStatus(transactionId);
124
+ // if (status.status === 'success' || status.status === 'failed') {
125
+ // return status;
126
+ // }
127
+ // // Wait before next attempt
128
+ // await new Promise(resolve => setTimeout(resolve, interval));
129
+ // }
130
+ // throw new Error('Transaction timeout - still pending after maximum attempts');
131
+ // }
132
+ /**
133
+ * List all transactions for this account
134
+ */
135
+ // async listTransactions(options?: {
136
+ // limit?: number;
137
+ // offset?: number;
138
+ // status?: 'pending' | 'success' | 'failed';
139
+ // }): Promise<{ transactions: TransactionStatus[]; total: number }> {
140
+ // const params = new URLSearchParams();
141
+ // if (options?.limit) params.append('limit', options.limit.toString());
142
+ // if (options?.offset) params.append('offset', options.offset.toString());
143
+ // if (options?.status) params.append('status', options.status);
144
+ // const query = params.toString();
145
+ // const endpoint = `/v1/transactions${query ? `?${query}` : ''}`;
146
+ // return this.request<{ transactions: TransactionStatus[]; total: number }>(endpoint, {
147
+ // method: 'GET'
148
+ // });
149
+ // }
150
+ /**
151
+ * Rotate encryption key (generates new key, revokes old one after 24h)
152
+ */
153
+ // async rotateEncryptionKey(): Promise<{ newEncryptionKey: string; message: string }> {
154
+ // return this.request<{ newEncryptionKey: string; message: string }>('/v1/keys/rotate', {
155
+ // method: 'POST'
156
+ // });
157
+ // }
158
+ /**
159
+ * Get account information
160
+ */
161
+ // async getAccountInfo(): Promise<{
162
+ // id: string;
163
+ // email: string;
164
+ // walletBalance: number;
165
+ // pendingBalance: number;
166
+ // availableBalance: number;
167
+ // }> {
168
+ // return this.request('/v1/account', { method: 'GET' });
169
+ // }
170
+ /**
171
+ * Initiate withdrawal
172
+ */
173
+ // async createWithdrawal(request: {
174
+ // amount: number;
175
+ // phoneNumber: string;
176
+ // network: 'MTN' | 'Airtel';
177
+ // }): Promise<{ withdrawalId: string; status: string; message: string }> {
178
+ // if (!request.amount) throw new Error('amount is required');
179
+ // if (!request.phoneNumber) throw new Error('phoneNumber is required');
180
+ // if (request.amount < 100) throw new Error('Minimum withdrawal is 100 RWF');
181
+ // if (request.amount > 2000000) throw new Error('Maximum withdrawal is 2,000,000 RWF');
182
+ // return this.request('/v1/withdrawals', {
183
+ // method: 'POST',
184
+ // body: JSON.stringify(request)
185
+ // });
186
+ // }
187
+ /**
188
+ * List withdrawals
189
+ */
190
+ // async listWithdrawals(options?: {
191
+ // limit?: number;
192
+ // offset?: number;
193
+ // }): Promise<{ withdrawals: any[]; total: number }> {
194
+ // const params = new URLSearchParams();
195
+ // if (options?.limit) params.append('limit', options.limit.toString());
196
+ // if (options?.offset) params.append('offset', options.offset.toString());
197
+ // const query = params.toString();
198
+ // const endpoint = `/v1/withdrawals${query ? `?${query}` : ''}`;
199
+ // return this.request(endpoint, { method: 'GET' });
200
+ // }
201
+ };
202
+ function createClient(config) {
203
+ return new PataLinkClient(config);
204
+ }
205
+ var index_default = PataLinkClient;
206
+ export {
207
+ createClient,
208
+ index_default as default
209
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@patalink/node-api-client",
3
+ "version": "1.0.0",
4
+ "description": "Official PataLink API client - Simple, secure payment integration",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist"],
16
+ "scripts": {
17
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
18
+ "test": "jest",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "patalink",
23
+ "payment",
24
+ "mtn",
25
+ "airtel",
26
+ "pesapal",
27
+ "rwanda",
28
+ "mobile-money"
29
+ ],
30
+ "author": "PataLink",
31
+ "license": "MIT",
32
+ "peerDependencies": {
33
+ "crypto-js": "^4.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/crypto-js": "^4.0.0",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.0.0"
39
+ },
40
+ "dependencies": {
41
+ "crypto-js": "^4.2.0"
42
+ }
43
+ }