blaaiz-nodejs-sdk 1.2.0 → 1.3.1

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
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> {
@@ -367,7 +372,8 @@ export declare class Blaaiz {
367
372
  public webhooks: WebhookService;
368
373
 
369
374
  constructor(apiKey: string, options?: BlaaizOptions);
370
-
375
+ constructor(options: BlaaizOptions);
376
+
371
377
  testConnection(): Promise<boolean>;
372
378
 
373
379
  createCompletePayout(payoutConfig: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blaaiz-nodejs-sdk",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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,170 @@
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
+ ...this.defaultHeaders,
132
+ 'Content-Type': 'application/x-www-form-urlencoded',
133
+ 'Content-Length': Buffer.byteLength(body)
134
+ },
135
+ timeout: this.timeout
136
+ }
137
+
138
+ const client = url.protocol === 'https:' ? https : http
139
+ const req = client.request(options, (res) => {
140
+ let responseData = ''
141
+
142
+ res.on('data', (chunk) => {
143
+ responseData += chunk
144
+ })
145
+
146
+ res.on('end', () => {
147
+ resolve({ status: res.statusCode, body: responseData })
148
+ })
149
+ })
150
+
151
+ req.on('error', (error) => {
152
+ reject(error)
153
+ })
154
+
155
+ req.on('timeout', () => {
156
+ req.destroy()
157
+ reject(new Error('OAuth token request timeout'))
158
+ })
159
+
160
+ req.write(body)
161
+ req.end()
162
+ })
163
+ }
164
+
23
165
  async makeRequest (method, endpoint, data = null, headers = {}) {
166
+ const authHeaders = await this.getAuthHeaders()
167
+
24
168
  return new Promise((resolve, reject) => {
25
169
  const url = new URL(endpoint, this.baseURL)
26
170
  const options = {
@@ -28,7 +172,7 @@ class BlaaizAPIClient {
28
172
  port: url.port || (url.protocol === 'https:' ? 443 : 80),
29
173
  path: url.pathname + url.search,
30
174
  method: method.toUpperCase(),
31
- headers: { ...this.defaultHeaders, ...headers },
175
+ headers: { ...this.defaultHeaders, ...authHeaders, ...headers },
32
176
  timeout: this.timeout
33
177
  }
34
178
 
@@ -93,4 +237,6 @@ class BlaaizAPIClient {
93
237
  }
94
238
  }
95
239
 
240
+ BlaaizAPIClient.ALL_SCOPES = ALL_SCOPES
241
+
96
242
  module.exports = BlaaizAPIClient