UNPKG

21.5 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const axios_1 = require("axios");
4const Logger_1 = require("./Helpers/Logger");
5const ErrorCodes_1 = require("./Helpers/ErrorCodes");
6const Rsa_1 = require("./Crypto/Rsa");
7const Aes_1 = require("./Crypto/Aes");
8const ApiAdapter_1 = require("./ApiAdapter");
9const Session_1 = require("./Session");
10const LocalstorageStore_1 = require("./Stores/LocalstorageStore");
11const index_1 = require("./Api/index");
12const FIVE_MINUTES_MS = 300000;
13class BunqJSClient {
14 /**
15 * @param {StorageInterface|false} storageInterface
16 * @param {LoggerInterface} loggerInterface
17 */
18 constructor(storageInterface = false, loggerInterface = Logger_1.default) {
19 this.apiKey = false;
20 /**
21 * Decides whether the session is kept alive (which will be slightly faster)
22 * or creates a new session when required
23 * @type {boolean}
24 */
25 this.keepAlive = true;
26 /**
27 * Contains the promise for fetching a new session to prevent duplicate requests
28 * @type {boolean}
29 */
30 this.fetchingNewSession = false;
31 /**
32 * A list of all custom bunqJSClient error codes to make error handling easier
33 * @type {{INSTALLATION_HAS_SESSION}}
34 */
35 this.errorCodes = ErrorCodes_1.default;
36 /**
37 * Handles the expiry timer checker callback
38 */
39 this.expiryTimerCallback = () => {
40 // check if keepAlive is enabled
41 if (this.keepAlive === false) {
42 this.clearExpiryTimer();
43 return false;
44 }
45 // update users, don't wait for it to finish
46 this.getUsers(true)
47 .then(users => {
48 // do nothing
49 this.logger.debug("Triggered session refresh");
50 })
51 .catch(error => {
52 // log the error
53 this.logger.error(error);
54 });
55 // set the timer again for a shorter duration (max 5 minutes)
56 this.setExpiryTimer(true);
57 };
58 if (storageInterface === false) {
59 if (typeof navigator === "undefined") {
60 // NodeJS environment with no custom store defined
61 throw new Error("No custom storageInterface was defined in the constructor!");
62 }
63 this.storageInterface = LocalstorageStore_1.default();
64 }
65 else {
66 this.storageInterface = storageInterface;
67 }
68 this.logger = loggerInterface;
69 // create a new session instance
70 this.Session = new Session_1.default(this.storageInterface, this.logger);
71 // setup the api adapter using our session context
72 this.ApiAdapter = new ApiAdapter_1.default(this.Session, this.logger, this);
73 // register the endpoints
74 this.api = index_1.default(this);
75 }
76 /**
77 * Starts the client and sets up the required components
78 * @returns {Promise.<void>}
79 */
80 async run(apiKey, allowedIps = [], environment = "SANDBOX", encryptionKey = false) {
81 this.logger.debug("bunqJSClient run");
82 this.apiKey = apiKey;
83 // setup the session with our apiKey and ip whitelist
84 await this.Session.setup(this.apiKey, allowedIps, environment, encryptionKey);
85 // set our automatic timer to check for expiry time
86 this.setExpiryTimer();
87 // setup the api adapter using our session
88 await this.ApiAdapter.setup();
89 }
90 /**
91 * If true, polling requests will be sent to try and keep the current session
92 * alive instead of creating a new session when required
93 * If false, a new session will be created when required
94 * @param {boolean} keepAlive
95 */
96 setKeepAlive(keepAlive) {
97 this.keepAlive = keepAlive;
98 }
99 /**
100 * Use one or more proxies when sending requests. Proxies are used
101 * @param enabledProxies
102 */
103 setRequestProxies(enabledProxies) {
104 this.ApiAdapter.RequestLimitFactory.setEnabledProxies(enabledProxies);
105 }
106 /**
107 * Installs this application
108 * @returns {Promise<boolean>}
109 */
110 async install() {
111 if (this.Session.verifyInstallation() === false) {
112 // check if Session is ready to execute the request
113 if (!this.Session.publicKey) {
114 throw new Error("No public key is set yet, make sure you setup an encryption key with BunqJSClient->run()");
115 }
116 const response = await this.api.installation.add();
117 // update the session properties
118 this.Session.serverPublicKeyPem = response.serverPublicKey;
119 this.Session.serverPublicKey = await Rsa_1.publicKeyFromPem(response.serverPublicKey);
120 this.Session.installToken = response.token.token;
121 this.Session.installUpdated = new Date(response.token.updated);
122 this.Session.installCreated = new Date(response.token.created);
123 // update storage
124 await this.Session.storeSession();
125 }
126 return true;
127 }
128 /**
129 * Registers a new device for this installation
130 * @param {string} deviceName
131 * @returns {Promise<boolean>}
132 */
133 async registerDevice(deviceName = "My Device") {
134 if (this.Session.verifyDeviceInstallation() === false) {
135 try {
136 const deviceId = await this.api.deviceRegistration.add({
137 description: deviceName,
138 permitted_ips: this.Session.allowedIps
139 });
140 // update the session properties
141 this.Session.deviceId = deviceId;
142 // update storage
143 await this.Session.storeSession();
144 }
145 catch (error) {
146 if (!error.response) {
147 throw error;
148 }
149 const response = error.response;
150 if (response.status === 400) {
151 // we have a permission/formatting issue, destroy the installation
152 this.Session.serverPublicKeyPem = null;
153 this.Session.serverPublicKey = null;
154 this.Session.installToken = null;
155 this.Session.installUpdated = null;
156 this.Session.installCreated = null;
157 // force creation of a new keypair since the old one is no longer 'unique'
158 await this.Session.setupKeypair(true);
159 // store the removed information
160 await this.Session.storeSession();
161 }
162 // rethrow the error
163 throw error;
164 }
165 }
166 return true;
167 }
168 /**
169 * Registers a new session when required for this device and installation
170 * @returns {Promise<boolean>}
171 */
172 async registerSession() {
173 if (this.Session.verifySessionInstallation() === false) {
174 try {
175 // generate the session using the bunq API
176 this.fetchingNewSession = this.generateSession();
177 // wait for it to finish
178 await this.fetchingNewSession;
179 }
180 catch (exception) {
181 // set fetching status to false
182 this.fetchingNewSession = false;
183 // re-throw the exception
184 throw exception;
185 }
186 // finished fetching/checking status so set to false
187 this.fetchingNewSession = false;
188 }
189 return true;
190 }
191 /**
192 * Send the actual request and handle it
193 * @returns {Promise<boolean>}
194 */
195 async generateSession() {
196 let response = null;
197 try {
198 this.logger.debug(" === Attempting to fetch session");
199 response = await this.api.sessionServer.add();
200 }
201 catch (error) {
202 if (error.response && error.response.data.Error) {
203 const responseError = error.response.data.Error[0];
204 const description = responseError.error_description;
205 this.logger.error("bunq API error: " + description);
206 if (description === "Authentication token already has a user session.") {
207 // add custom error code
208 throw {
209 errorCode: this.errorCodes.INSTALLATION_HAS_SESSION,
210 error: error
211 };
212 }
213 }
214 // rethrow the exact error
215 throw error;
216 }
217 this.logger.debug("response.token.created:" + response.token.created);
218 // based on account setting we set a expire date
219 const createdDate = new Date(response.token.created + " UTC");
220 let sessionTimeout;
221 // parse the correct user info from response
222 let userInfoParsed = this.getUserType(response.user_info);
223 // differentiate between oauth api keys and non-oauth api keys
224 if (userInfoParsed.isOAuth === false) {
225 // get the session timeout
226 sessionTimeout = userInfoParsed.info.session_timeout;
227 this.logger.debug("Received userInfoParsed.info.session_timeout from api: " + userInfoParsed.info.session_timeout);
228 // set isOAuth to false
229 this.Session.isOAuthKey = false;
230 // set user info
231 this.Session.userInfo = response.user_info;
232 }
233 else {
234 // parse the user info
235 sessionTimeout = this.parseOauthUser(userInfoParsed);
236 }
237 // turn time into MS
238 sessionTimeout = sessionTimeout * 1000;
239 // calculate the expiry time
240 createdDate.setTime(createdDate.getTime() + sessionTimeout);
241 // set the session information
242 this.Session.sessionExpiryTime = createdDate;
243 this.Session.sessionTimeout = sessionTimeout;
244 this.Session.sessionId = response.id;
245 this.Session.sessionToken = response.token.token;
246 this.Session.sessionTokenId = response.token.id;
247 this.logger.debug("calculated expireDate: " + createdDate + " current date: " + new Date());
248 // update storage
249 await this.Session.storeSession();
250 // update the timer
251 this.setExpiryTimer();
252 return true;
253 }
254 /**
255 * Change the encryption key and
256 * @param {string} encryptionKey
257 * @returns {Promise<boolean>}
258 */
259 async changeEncryptionKey(encryptionKey) {
260 if (!Aes_1.validateKey(encryptionKey)) {
261 throw new Error("Invalid EAS key given! Invalid characters or length (16,24,32 length)");
262 }
263 // change the encryption key
264 this.Session.encryptionKey = encryptionKey;
265 // update the storage
266 return this.Session.storeSession();
267 }
268 /**
269 * Handles the oauth type users
270 * @param userInfoParsed
271 * @returns {any}
272 */
273 parseOauthUser(userInfoParsed) {
274 // parse the granted and request by user objects
275 const requestedByUserParsed = this.getUserType(userInfoParsed.info.requested_by_user);
276 const grantedByUserParsed = this.getUserType(userInfoParsed.info.granted_by_user);
277 // get the session timeout from request_by_user
278 const sessionTimeout = requestedByUserParsed.info.session_timeout;
279 this.logger.debug("Received requestedByUserParsed.info.session_timeout from api: " +
280 requestedByUserParsed.info.session_timeout);
281 // set user id if none is set
282 if (!grantedByUserParsed.info.id) {
283 grantedByUserParsed.info.id = userInfoParsed.info.id;
284 }
285 // make sure we set isOAuth to true to handle it more easily
286 this.Session.isOAuthKey = true;
287 // set user info for granted by user
288 this.Session.userInfo["UserApiKey"] = grantedByUserParsed.info;
289 return sessionTimeout;
290 }
291 /**
292 * Create a new credential password ip
293 * @returns {Promise<any>}
294 */
295 async createCredentials() {
296 const limiter = this.ApiAdapter.RequestLimitFactory.create("/credential-password-ip-request", "POST");
297 // send a unsigned request to the endpoint to create a new credential password ip
298 const response = await limiter.run(async (axiosClient) => this.ApiAdapter.post(`https://api.tinker.bunq.com/v1/credential-password-ip-request`, {}, {}, {
299 disableVerification: true,
300 disableSigning: true,
301 skipSessionCheck: true
302 }));
303 return response.Response[0].UserCredentialPasswordIpRequest;
304 }
305 /**
306 * Check if a credential password ip has been accepted
307 * @param {string} uuid
308 * @returns {Promise<any>}
309 */
310 async checkCredentialStatus(uuid) {
311 const limiter = this.ApiAdapter.RequestLimitFactory.create("/credential-password-ip-request", "GET");
312 // send a unsigned request to the endpoint to create a new credential password ip with the uuid
313 const response = await limiter.run(async (axiosClient) => this.ApiAdapter.get(`https://api.tinker.bunq.com/v1/credential-password-ip-request/${uuid}`, {}, {
314 disableVerification: true,
315 disableSigning: true,
316 skipSessionCheck: true
317 }));
318 return response.Response[0].UserCredentialPasswordIpRequest;
319 }
320 /**
321 *
322 * @param {string} clientId
323 * @param {string} clientSecret
324 * @param {string} redirectUri
325 * @param {string} code
326 * @param {string|false} state
327 * @param {boolean} sandbox
328 * @param {string} grantType
329 * @returns {Promise<string>}
330 */
331 async exchangeOAuthToken(clientId, clientSecret, redirectUri, code, state = false, sandbox = false, grantType = "authorization_code") {
332 const url = this.formatOAuthKeyExchangeUrl(clientId, clientSecret, redirectUri, code, sandbox, grantType);
333 // send the request
334 const response = await axios_1.default({
335 method: "POST",
336 url: url
337 });
338 const data = response.data;
339 // check if a state has to be checked and validate it
340 if (state && state !== data.state) {
341 throw new Error("Given state does not match token exchange state!");
342 }
343 return data.access_token;
344 }
345 /**
346 * Formats a correct bunq OAuth url to begin the login flow
347 * @param {string} clientId
348 * @param {string} redirectUri
349 * @param {string|false} state
350 * @param {boolean} sandbox
351 * @returns {string}
352 */
353 formatOAuthAuthorizationRequestUrl(clientId, redirectUri, state = false, sandbox = false) {
354 const stateParam = state ? `&state=${state}` : "";
355 const baseUrl = sandbox ? "https://oauth.sandbox.bunq.com" : "https://oauth.bunq.com";
356 return (`${baseUrl}/auth?response_type=code&` +
357 `client_id=${clientId}&` +
358 `redirect_uri=${redirectUri}` +
359 stateParam);
360 }
361 /**
362 * Formats the given parameters into the url used for the token exchange
363 * @param {string} clientId
364 * @param {string} clientSecret
365 * @param {string} redirectUri
366 * @param {string} code
367 * @param {boolean} sandbox
368 * @param {string} grantType
369 * @returns {string}
370 */
371 formatOAuthKeyExchangeUrl(clientId, clientSecret, redirectUri, code, sandbox = false, grantType = "authorization_code") {
372 const baseUrl = sandbox ? "https://api-oauth.sandbox.bunq.com" : "https://api.oauth.bunq.com";
373 return (`${baseUrl}/v1/token?` +
374 `grant_type=${grantType}&` +
375 `code=${code}&` +
376 `client_id=${clientId}&` +
377 `client_secret=${clientSecret}&` +
378 `redirect_uri=${redirectUri}`);
379 }
380 /**
381 * Sets an automatic timer to keep the session alive when possible
382 */
383 setExpiryTimer(shortTimeout = false) {
384 if (typeof process !== "undefined" && process.env.ENV_CI === "true") {
385 // disable in CI
386 return false;
387 }
388 // check if keepAlive is enabled
389 if (this.keepAlive === false) {
390 this.clearExpiryTimer();
391 return false;
392 }
393 if (this.Session.sessionExpiryTime) {
394 // calculate amount of milliseconds until expire time
395 const expiresInMilliseconds = this.calculateSessionExpiry();
396 // 15 seconds before it expires we want to reset it
397 const timeoutRequestDuration = expiresInMilliseconds - 15000;
398 // clear existing timer if required
399 this.clearExpiryTimer();
400 // set the timeout
401 this.Session.sessionExpiryTimeChecker = setTimeout(this.expiryTimerCallback, timeoutRequestDuration);
402 }
403 }
404 /**
405 * Resets the session expiry timer
406 */
407 clearExpiryTimer() {
408 if (this.Session.sessionExpiryTimeChecker !== null) {
409 clearTimeout(this.Session.sessionExpiryTimeChecker);
410 }
411 }
412 /**
413 * Calculate in how many milliseconds the session will expire
414 * @param {boolean} shortTimeout
415 * @returns {number}
416 */
417 calculateSessionExpiry(shortTimeout = false) {
418 // if shortTimeout is set maximize the expiry to 5 minutes
419 if (shortTimeout) {
420 return !this.Session.sessionTimeout || this.Session.sessionTimeout > FIVE_MINUTES_MS
421 ? FIVE_MINUTES_MS
422 : this.Session.sessionTimeout;
423 }
424 const currentTime = new Date();
425 return this.Session.sessionExpiryTime.getTime() - currentTime.getTime();
426 }
427 /**
428 * Destroys the current installation and session and all variables associated with it
429 * @returns {Promise<void>}
430 */
431 async destroySession() {
432 if (this.Session.verifyInstallation() &&
433 this.Session.verifyDeviceInstallation() &&
434 this.Session.verifySessionInstallation()) {
435 // we have a valid installation, try to delete the remote session
436 try {
437 await this.api.sessionServer.delete();
438 }
439 catch (ex) { }
440 }
441 // clear the session timer if set
442 this.clearExpiryTimer();
443 // destroy the stored session
444 await this.Session.destroySession();
445 }
446 /**
447 * Destroys the current session and all variables associated with it
448 * @param save
449 */
450 async destroyApiSession(save = false) {
451 // clear the session timer if set
452 this.clearExpiryTimer();
453 // destroy the stored session
454 await this.Session.destroyApiSession(save);
455 }
456 /**
457 * Returns the registered user for the session of a specific type
458 * @returns {any}
459 */
460 async getUser(userType, updated = false) {
461 if (updated) {
462 // update the user info and update session data
463 const userList = await this.api.user.list();
464 // parse user type from user list
465 const userInfoParsed = this.getUserType(userList);
466 if (userInfoParsed.isOAuth) {
467 // get info from the userapikey object
468 this.parseOauthUser(userInfoParsed);
469 }
470 else {
471 // set updated info
472 this.Session.userInfo[userInfoParsed.type] = userInfoParsed.info;
473 }
474 }
475 // return the user if we have one
476 return this.Session.userInfo[userType];
477 }
478 /**
479 * Returns the registered users for the session
480 * @returns {any}
481 */
482 async getUsers(updated = false) {
483 if (updated) {
484 // update the user info and update session data
485 const userList = await this.api.user.list();
486 // parse user type from user list
487 const userInfoParsed = this.getUserType(userList);
488 if (userInfoParsed.isOAuth) {
489 // get info from the userapikey object
490 this.parseOauthUser(userInfoParsed);
491 }
492 else {
493 // set updated info
494 this.Session.userInfo[userInfoParsed.type] = userInfoParsed.info;
495 }
496 }
497 // return the users
498 return this.Session.userInfo;
499 }
500 /**
501 * Receives an object with an unknown user type and returns an object with
502 * the correct info and a isOAuth boolean
503 * @param userInfo
504 * @returns {{info: any; isOAuth: boolean}}
505 */
506 getUserType(userInfo) {
507 if (userInfo.UserCompany !== undefined) {
508 return {
509 info: userInfo.UserCompany,
510 type: "UserCompany",
511 isOAuth: false
512 };
513 }
514 else if (userInfo.UserPerson !== undefined) {
515 return {
516 info: userInfo.UserPerson,
517 type: "UserPerson",
518 isOAuth: false
519 };
520 }
521 else if (userInfo.UserLight !== undefined) {
522 return {
523 info: userInfo.UserLight,
524 type: "UserLight",
525 isOAuth: false
526 };
527 }
528 else if (userInfo.UserApiKey !== undefined) {
529 return {
530 info: userInfo.UserApiKey,
531 type: "UserApiKey",
532 isOAuth: true
533 };
534 }
535 throw new Error("No supported account type found! (Not one of UserLight, UserPerson, UserApiKey or UserCompany)");
536 }
537}
538exports.default = BunqJSClient;