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 +47 -3
- package/index.d.ts +10 -3
- package/package.json +1 -1
- package/src/client.js +152 -7
- package/src/services/WebhookService.js +7 -4
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
|
|
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
|
|
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
|
|
351
|
-
constructEvent(payload: string
|
|
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
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 (
|
|
9
|
-
|
|
20
|
+
if (apiKey && typeof apiKey === 'object') {
|
|
21
|
+
options = apiKey
|
|
22
|
+
apiKey = undefined
|
|
10
23
|
}
|
|
11
24
|
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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}.${
|
|
66
|
+
const signedPayload = `${timestamp}.${payload}`
|
|
64
67
|
|
|
65
68
|
// Create HMAC signature
|
|
66
69
|
const expectedSignature = crypto
|