blaaiz-nodejs-sdk 1.1.2 → 1.3.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 CHANGED
@@ -29,6 +29,43 @@ const isConnected = await blaaiz.testConnection();
29
29
  console.log('API Connected:', isConnected);
30
30
  ```
31
31
 
32
+ ## Authentication
33
+
34
+ The SDK supports two authentication methods.
35
+
36
+ ### OAuth 2.0 client credentials (recommended for new integrations)
37
+
38
+ Provide a `client_id` and `client_secret` and the SDK handles the OAuth
39
+ `client_credentials` flow for you: it fetches a bearer token from
40
+ `/oauth/token`, caches it in memory, and refreshes it automatically shortly
41
+ before it expires.
42
+
43
+ ```javascript
44
+ const { Blaaiz } = require('blaaiz-nodejs-sdk');
45
+
46
+ const blaaiz = new Blaaiz({
47
+ client_id: process.env.BLAAIZ_CLIENT_ID,
48
+ client_secret: process.env.BLAAIZ_CLIENT_SECRET,
49
+ // oauth_scope: 'wallet:read payout:create', // Optional: defaults to the full scope set
50
+ // baseURL: 'https://api.blaaiz.com', // Optional: defaults to dev environment
51
+ // timeout: 30000 // Optional: request timeout in milliseconds
52
+ });
53
+ ```
54
+
55
+ When `oauth_scope` is omitted the SDK requests the full set of supported scopes.
56
+
57
+ ### API key (legacy)
58
+
59
+ API keys are still supported for existing integrations.
60
+
61
+ ```javascript
62
+ const blaaiz = new Blaaiz('your-api-key-here');
63
+ // or, using the options object:
64
+ const blaaiz2 = new Blaaiz({ api_key: process.env.BLAAIZ_API_KEY });
65
+ ```
66
+
67
+ When both OAuth credentials and an API key are configured, OAuth is used.
68
+
32
69
  ## Features
33
70
 
34
71
  - **Customer Management**: Create, update, and manage customers with KYC verification
@@ -286,12 +323,17 @@ const payout = await blaaiz.payouts.initiate({
286
323
  to_currency_id: "NGN",
287
324
  bank_id: "bank-id", // Required for NGN
288
325
  account_number: "0123456789",
289
- phone_number: "+2348012345678" // Optional
326
+ phone_number: "+2348012345678", // Optional
327
+ note: "Acme Ltd" // Optional — appears in the transaction description; defaults to business name
290
328
  });
291
329
 
292
330
  console.log('Payout Status:', payout.data.transaction.status);
293
331
  ```
294
332
 
333
+ #### Passing additional fields
334
+
335
+ `payouts.initiate()` forwards the entire payload verbatim to the API, so any field documented in the [Blaaiz API reference](https://docs.business.blaaiz.com) can be included even if it isn't listed in this README. For example, `note` sets the transaction description (defaulting to the business name when omitted). This means the SDK stays compatible with new API fields without requiring an update here.
336
+
295
337
  #### Bank Transfer Payout (GBP)
296
338
 
297
339
  ```javascript
@@ -691,6 +733,8 @@ The Blaaiz API has a rate limit of 100 requests per minute. The SDK automaticall
691
733
 
692
734
  The SDK provides built-in webhook signature verification to ensure webhook authenticity. The signature is computed using HMAC-SHA256 with the format `timestamp.payload`.
693
735
 
736
+ > **Important:** `payload` must be the **raw request body string** exactly as received. Do not pass a parsed object — parsing and re-serializing (e.g. `JSON.stringify`) changes the bytes and will break signature verification. Capture the raw body with `express.raw({ type: 'application/json' })` and pass `req.body.toString()` (see the Express example below).
737
+
694
738
  ```javascript
695
739
  const { Blaaiz } = require('blaaiz-nodejs-sdk');
696
740
 
@@ -698,7 +742,7 @@ const blaaiz = new Blaaiz('your-api-key');
698
742
 
699
743
  // Method 1: Verify signature manually
700
744
  const isValid = blaaiz.webhooks.verifySignature(
701
- payload, // Raw webhook payload (string or object)
745
+ payload, // Raw request body string (do not pass a parsed object)
702
746
  signature, // Signature from webhook headers (x-blaaiz-signature)
703
747
  timestamp, // Timestamp from webhook headers (x-blaaiz-timestamp)
704
748
  webhookSecret // Your webhook secret key
@@ -713,7 +757,7 @@ if (isValid) {
713
757
  // Method 2: Construct verified event (recommended)
714
758
  try {
715
759
  const event = blaaiz.webhooks.constructEvent(
716
- payload, // Raw webhook payload
760
+ payload, // Raw request body string (do not pass a parsed object)
717
761
  signature, // Signature from webhook headers
718
762
  timestamp, // Timestamp from webhook headers
719
763
  webhookSecret // Your webhook secret key
package/index.d.ts CHANGED
@@ -4,7 +4,12 @@
4
4
 
5
5
  export interface BlaaizOptions {
6
6
  baseURL?: string;
7
+ base_url?: string;
7
8
  timeout?: number;
9
+ api_key?: string;
10
+ client_id?: string;
11
+ client_secret?: string;
12
+ oauth_scope?: string;
8
13
  }
9
14
 
10
15
  export interface BlaaizResponse<T = any> {
@@ -139,6 +144,7 @@ export interface PayoutData {
139
144
  wallet_address?: string;
140
145
  wallet_network?: string;
141
146
  wallet_token?: string;
147
+ note?: string;
142
148
  }
143
149
 
144
150
  export interface PayoutResponse {
@@ -347,8 +353,8 @@ export declare class WebhookService {
347
353
  get(): Promise<BlaaizResponse<WebhookData>>;
348
354
  update(webhookData: Partial<WebhookData>): Promise<BlaaizResponse<any>>;
349
355
  replay(replayData: WebhookReplayData): Promise<BlaaizResponse<any>>;
350
- verifySignature(payload: string | object, signature: string, secret: string): boolean;
351
- constructEvent(payload: string | object, signature: string, secret: string): WebhookEvent;
356
+ verifySignature(payload: string, signature: string, timestamp: string, secret: string): boolean;
357
+ constructEvent(payload: string, signature: string, timestamp: string, secret: string): WebhookEvent;
352
358
  }
353
359
 
354
360
  // Main SDK Class
@@ -366,7 +372,8 @@ export declare class Blaaiz {
366
372
  public webhooks: WebhookService;
367
373
 
368
374
  constructor(apiKey: string, options?: BlaaizOptions);
369
-
375
+ constructor(options: BlaaizOptions);
376
+
370
377
  testConnection(): Promise<boolean>;
371
378
 
372
379
  createCompletePayout(payoutConfig: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blaaiz-nodejs-sdk",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "Official Node.js SDK for Blaaiz RaaS (Remittance as a Service) API",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/src/client.js CHANGED
@@ -1,26 +1,169 @@
1
1
  const https = require('https')
2
2
  const http = require('http')
3
- const { URL } = require('url')
3
+ const { URL, URLSearchParams } = require('url')
4
4
  const BlaaizError = require('./error')
5
5
 
6
+ const ALL_SCOPES = [
7
+ 'wallet:read', 'currency:read', 'bank:read', 'customer:read', 'customer:write',
8
+ 'beneficiary:read', 'virtual-account:read', 'virtual-account:create', 'virtual-account:close',
9
+ 'collection:create', 'collection:crypto:create', 'collection:interac:accept',
10
+ 'payout:create', 'swap:create', 'transaction:read', 'fees:read', 'file:upload',
11
+ 'webhook:read', 'webhook:write', 'webhook:replay', 'rates:read'
12
+ ]
13
+
14
+ function isPresent (value) {
15
+ return typeof value === 'string' && value !== '' && value !== '0'
16
+ }
17
+
6
18
  class BlaaizAPIClient {
7
19
  constructor (apiKey, options = {}) {
8
- if (!apiKey) {
9
- throw new Error('API key is required')
20
+ if (apiKey && typeof apiKey === 'object') {
21
+ options = apiKey
22
+ apiKey = undefined
10
23
  }
11
24
 
12
- this.apiKey = apiKey
13
- this.baseURL = options.baseURL || 'https://api-dev.blaaiz.com'
25
+ this.clientId = options.client_id || ''
26
+ this.clientSecret = options.client_secret || ''
27
+ this.oauthScope = (options.oauth_scope === undefined || options.oauth_scope === null)
28
+ ? ALL_SCOPES.join(' ')
29
+ : options.oauth_scope
30
+ this.apiKey = apiKey || options.api_key || ''
31
+ this.baseURL = options.baseURL || options.base_url || 'https://api-dev.blaaiz.com'
14
32
  this.timeout = options.timeout || 30000
33
+
34
+ this.useOAuth = isPresent(this.clientId) && isPresent(this.clientSecret)
35
+
36
+ if (!this.useOAuth && !isPresent(this.apiKey)) {
37
+ throw new BlaaizError(
38
+ 'Authentication required: provide either client_id and client_secret for OAuth, or an API key for legacy authentication'
39
+ )
40
+ }
41
+
42
+ this.accessToken = null
43
+ this.tokenExpiresAt = null
44
+
15
45
  this.defaultHeaders = {
16
- 'x-blaaiz-api-key': this.apiKey,
17
46
  Accept: 'application/json',
18
47
  'Content-Type': 'application/json',
19
48
  'User-Agent': 'Blaaiz-NodeJS-SDK/1.0.0'
20
49
  }
21
50
  }
22
51
 
52
+ async getOAuthToken () {
53
+ const now = Math.floor(Date.now() / 1000)
54
+ if (this.accessToken && this.tokenExpiresAt && now < this.tokenExpiresAt) {
55
+ return this.accessToken
56
+ }
57
+
58
+ const body = new URLSearchParams({
59
+ grant_type: 'client_credentials',
60
+ client_id: this.clientId,
61
+ client_secret: this.clientSecret,
62
+ scope: this.oauthScope
63
+ }).toString()
64
+
65
+ let response
66
+ try {
67
+ response = await this._requestToken(body)
68
+ } catch (error) {
69
+ throw new BlaaizError(
70
+ `OAuth token request failed: ${error.message}`,
71
+ null,
72
+ 'OAUTH_ERROR'
73
+ )
74
+ }
75
+
76
+ if (response.status < 200 || response.status >= 300) {
77
+ let errorData = null
78
+ try {
79
+ errorData = JSON.parse(response.body)
80
+ } catch (error) {
81
+ errorData = null
82
+ }
83
+ throw new BlaaizError(
84
+ (errorData && (errorData.error_description || errorData.message)) ||
85
+ `OAuth token request failed: HTTP ${response.status}`,
86
+ response.status,
87
+ (errorData && errorData.error) || 'OAUTH_ERROR'
88
+ )
89
+ }
90
+
91
+ let parsed = null
92
+ try {
93
+ parsed = JSON.parse(response.body)
94
+ } catch (error) {
95
+ parsed = null
96
+ }
97
+
98
+ if (!parsed || !parsed.access_token) {
99
+ throw new BlaaizError(
100
+ 'Failed to parse OAuth token response',
101
+ response.status,
102
+ 'OAUTH_PARSE_ERROR'
103
+ )
104
+ }
105
+
106
+ let expiresIn = Number(parsed.expires_in)
107
+ if (!Number.isFinite(expiresIn)) expiresIn = 900
108
+ this.accessToken = parsed.access_token
109
+ this.tokenExpiresAt = Math.floor(Date.now() / 1000) + expiresIn - 60
110
+
111
+ return this.accessToken
112
+ }
113
+
114
+ async getAuthHeaders () {
115
+ if (this.useOAuth) {
116
+ const token = await this.getOAuthToken()
117
+ return { Authorization: `Bearer ${token}` }
118
+ }
119
+ return { 'x-blaaiz-api-key': this.apiKey }
120
+ }
121
+
122
+ _requestToken (body) {
123
+ return new Promise((resolve, reject) => {
124
+ const url = new URL('/oauth/token', this.baseURL)
125
+ const options = {
126
+ hostname: url.hostname,
127
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
128
+ path: url.pathname + url.search,
129
+ method: 'POST',
130
+ headers: {
131
+ 'Content-Type': 'application/x-www-form-urlencoded',
132
+ 'Content-Length': Buffer.byteLength(body)
133
+ },
134
+ timeout: this.timeout
135
+ }
136
+
137
+ const client = url.protocol === 'https:' ? https : http
138
+ const req = client.request(options, (res) => {
139
+ let responseData = ''
140
+
141
+ res.on('data', (chunk) => {
142
+ responseData += chunk
143
+ })
144
+
145
+ res.on('end', () => {
146
+ resolve({ status: res.statusCode, body: responseData })
147
+ })
148
+ })
149
+
150
+ req.on('error', (error) => {
151
+ reject(error)
152
+ })
153
+
154
+ req.on('timeout', () => {
155
+ req.destroy()
156
+ reject(new Error('OAuth token request timeout'))
157
+ })
158
+
159
+ req.write(body)
160
+ req.end()
161
+ })
162
+ }
163
+
23
164
  async makeRequest (method, endpoint, data = null, headers = {}) {
165
+ const authHeaders = await this.getAuthHeaders()
166
+
24
167
  return new Promise((resolve, reject) => {
25
168
  const url = new URL(endpoint, this.baseURL)
26
169
  const options = {
@@ -28,7 +171,7 @@ class BlaaizAPIClient {
28
171
  port: url.port || (url.protocol === 'https:' ? 443 : 80),
29
172
  path: url.pathname + url.search,
30
173
  method: method.toUpperCase(),
31
- headers: { ...this.defaultHeaders, ...headers },
174
+ headers: { ...this.defaultHeaders, ...authHeaders, ...headers },
32
175
  timeout: this.timeout
33
176
  }
34
177
 
@@ -93,4 +236,6 @@ class BlaaizAPIClient {
93
236
  }
94
237
  }
95
238
 
239
+ BlaaizAPIClient.ALL_SCOPES = ALL_SCOPES
240
+
96
241
  module.exports = BlaaizAPIClient
@@ -54,13 +54,16 @@ class WebhookService {
54
54
  throw new Error('Webhook secret is required for signature verification')
55
55
  }
56
56
 
57
- const crypto = require('crypto')
57
+ // The payload must be the raw request body string. Parsing and
58
+ // re-serializing an object changes the bytes and breaks verification.
59
+ if (typeof payload !== 'string') {
60
+ throw new Error('Webhook payload must be the raw request body string. Do not pass a parsed object — parsing and re-serializing changes the bytes and breaks signature verification. Use the raw body (e.g. express.raw({ type: "application/json" }) then req.body.toString()).')
61
+ }
58
62
 
59
- // Convert payload to string if it's an object
60
- const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload)
63
+ const crypto = require('crypto')
61
64
 
62
65
  // Create signed payload with timestamp
63
- const signedPayload = `${timestamp}.${payloadString}`
66
+ const signedPayload = `${timestamp}.${payload}`
64
67
 
65
68
  // Create HMAC signature
66
69
  const expectedSignature = crypto