1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const axios_1 = require("axios");
|
4 | const Logger_1 = require("./Helpers/Logger");
|
5 | const ErrorCodes_1 = require("./Helpers/ErrorCodes");
|
6 | const Rsa_1 = require("./Crypto/Rsa");
|
7 | const Aes_1 = require("./Crypto/Aes");
|
8 | const ApiAdapter_1 = require("./ApiAdapter");
|
9 | const Session_1 = require("./Session");
|
10 | const LocalstorageStore_1 = require("./Stores/LocalstorageStore");
|
11 | const index_1 = require("./Api/index");
|
12 | const FIVE_MINUTES_MS = 300000;
|
13 | class BunqJSClient {
|
14 | |
15 |
|
16 |
|
17 |
|
18 | constructor(storageInterface = false, loggerInterface = Logger_1.default) {
|
19 | this.apiKey = false;
|
20 | |
21 |
|
22 |
|
23 |
|
24 |
|
25 | this.keepAlive = true;
|
26 | |
27 |
|
28 |
|
29 |
|
30 | this.fetchingNewSession = false;
|
31 | |
32 |
|
33 |
|
34 |
|
35 | this.errorCodes = ErrorCodes_1.default;
|
36 | |
37 |
|
38 |
|
39 | this.expiryTimerCallback = () => {
|
40 |
|
41 | if (this.keepAlive === false) {
|
42 | this.clearExpiryTimer();
|
43 | return false;
|
44 | }
|
45 |
|
46 | this.getUsers(true)
|
47 | .then(users => {
|
48 |
|
49 | this.logger.debug("Triggered session refresh");
|
50 | })
|
51 | .catch(error => {
|
52 |
|
53 | this.logger.error(error);
|
54 | });
|
55 |
|
56 | this.setExpiryTimer(true);
|
57 | };
|
58 | if (storageInterface === false) {
|
59 | if (typeof navigator === "undefined") {
|
60 |
|
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 |
|
70 | this.Session = new Session_1.default(this.storageInterface, this.logger);
|
71 |
|
72 | this.ApiAdapter = new ApiAdapter_1.default(this.Session, this.logger, this);
|
73 |
|
74 | this.api = index_1.default(this);
|
75 | }
|
76 | |
77 |
|
78 |
|
79 |
|
80 | async run(apiKey, allowedIps = [], environment = "SANDBOX", encryptionKey = false) {
|
81 | this.logger.debug("bunqJSClient run");
|
82 | this.apiKey = apiKey;
|
83 |
|
84 | await this.Session.setup(this.apiKey, allowedIps, environment, encryptionKey);
|
85 |
|
86 | this.setExpiryTimer();
|
87 |
|
88 | await this.ApiAdapter.setup();
|
89 | }
|
90 | |
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 | setKeepAlive(keepAlive) {
|
97 | this.keepAlive = keepAlive;
|
98 | }
|
99 | |
100 |
|
101 |
|
102 |
|
103 | setRequestProxies(enabledProxies) {
|
104 | this.ApiAdapter.RequestLimitFactory.setEnabledProxies(enabledProxies);
|
105 | }
|
106 | |
107 |
|
108 |
|
109 |
|
110 | async install() {
|
111 | if (this.Session.verifyInstallation() === false) {
|
112 |
|
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 |
|
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 |
|
124 | await this.Session.storeSession();
|
125 | }
|
126 | return true;
|
127 | }
|
128 | |
129 |
|
130 |
|
131 |
|
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 |
|
141 | this.Session.deviceId = deviceId;
|
142 |
|
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 |
|
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 |
|
158 | await this.Session.setupKeypair(true);
|
159 |
|
160 | await this.Session.storeSession();
|
161 | }
|
162 |
|
163 | throw error;
|
164 | }
|
165 | }
|
166 | return true;
|
167 | }
|
168 | |
169 |
|
170 |
|
171 |
|
172 | async registerSession() {
|
173 | if (this.Session.verifySessionInstallation() === false) {
|
174 | try {
|
175 |
|
176 | this.fetchingNewSession = this.generateSession();
|
177 |
|
178 | await this.fetchingNewSession;
|
179 | }
|
180 | catch (exception) {
|
181 |
|
182 | this.fetchingNewSession = false;
|
183 |
|
184 | throw exception;
|
185 | }
|
186 |
|
187 | this.fetchingNewSession = false;
|
188 | }
|
189 | return true;
|
190 | }
|
191 | |
192 |
|
193 |
|
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 |
|
208 | throw {
|
209 | errorCode: this.errorCodes.INSTALLATION_HAS_SESSION,
|
210 | error: error
|
211 | };
|
212 | }
|
213 | }
|
214 |
|
215 | throw error;
|
216 | }
|
217 | this.logger.debug("response.token.created:" + response.token.created);
|
218 |
|
219 | const createdDate = new Date(response.token.created + " UTC");
|
220 | let sessionTimeout;
|
221 |
|
222 | let userInfoParsed = this.getUserType(response.user_info);
|
223 |
|
224 | if (userInfoParsed.isOAuth === false) {
|
225 |
|
226 | sessionTimeout = userInfoParsed.info.session_timeout;
|
227 | this.logger.debug("Received userInfoParsed.info.session_timeout from api: " + userInfoParsed.info.session_timeout);
|
228 |
|
229 | this.Session.isOAuthKey = false;
|
230 |
|
231 | this.Session.userInfo = response.user_info;
|
232 | }
|
233 | else {
|
234 |
|
235 | sessionTimeout = this.parseOauthUser(userInfoParsed);
|
236 | }
|
237 |
|
238 | sessionTimeout = sessionTimeout * 1000;
|
239 |
|
240 | createdDate.setTime(createdDate.getTime() + sessionTimeout);
|
241 |
|
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 |
|
249 | await this.Session.storeSession();
|
250 |
|
251 | this.setExpiryTimer();
|
252 | return true;
|
253 | }
|
254 | |
255 |
|
256 |
|
257 |
|
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 |
|
264 | this.Session.encryptionKey = encryptionKey;
|
265 |
|
266 | return this.Session.storeSession();
|
267 | }
|
268 | |
269 |
|
270 |
|
271 |
|
272 |
|
273 | parseOauthUser(userInfoParsed) {
|
274 |
|
275 | const requestedByUserParsed = this.getUserType(userInfoParsed.info.requested_by_user);
|
276 | const grantedByUserParsed = this.getUserType(userInfoParsed.info.granted_by_user);
|
277 |
|
278 | const sessionTimeout = requestedByUserParsed.info.session_timeout;
|
279 | this.logger.debug("Received requestedByUserParsed.info.session_timeout from api: " +
|
280 | requestedByUserParsed.info.session_timeout);
|
281 |
|
282 | if (!grantedByUserParsed.info.id) {
|
283 | grantedByUserParsed.info.id = userInfoParsed.info.id;
|
284 | }
|
285 |
|
286 | this.Session.isOAuthKey = true;
|
287 |
|
288 | this.Session.userInfo["UserApiKey"] = grantedByUserParsed.info;
|
289 | return sessionTimeout;
|
290 | }
|
291 | |
292 |
|
293 |
|
294 |
|
295 | async createCredentials() {
|
296 | const limiter = this.ApiAdapter.RequestLimitFactory.create("/credential-password-ip-request", "POST");
|
297 |
|
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 |
|
307 |
|
308 |
|
309 |
|
310 | async checkCredentialStatus(uuid) {
|
311 | const limiter = this.ApiAdapter.RequestLimitFactory.create("/credential-password-ip-request", "GET");
|
312 |
|
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 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 |
|
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 |
|
334 | const response = await axios_1.default({
|
335 | method: "POST",
|
336 | url: url
|
337 | });
|
338 | const data = response.data;
|
339 |
|
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 |
|
347 |
|
348 |
|
349 |
|
350 |
|
351 |
|
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 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
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 |
|
382 |
|
383 | setExpiryTimer(shortTimeout = false) {
|
384 | if (typeof process !== "undefined" && process.env.ENV_CI === "true") {
|
385 |
|
386 | return false;
|
387 | }
|
388 |
|
389 | if (this.keepAlive === false) {
|
390 | this.clearExpiryTimer();
|
391 | return false;
|
392 | }
|
393 | if (this.Session.sessionExpiryTime) {
|
394 |
|
395 | const expiresInMilliseconds = this.calculateSessionExpiry();
|
396 |
|
397 | const timeoutRequestDuration = expiresInMilliseconds - 15000;
|
398 |
|
399 | this.clearExpiryTimer();
|
400 |
|
401 | this.Session.sessionExpiryTimeChecker = setTimeout(this.expiryTimerCallback, timeoutRequestDuration);
|
402 | }
|
403 | }
|
404 | |
405 |
|
406 |
|
407 | clearExpiryTimer() {
|
408 | if (this.Session.sessionExpiryTimeChecker !== null) {
|
409 | clearTimeout(this.Session.sessionExpiryTimeChecker);
|
410 | }
|
411 | }
|
412 | |
413 |
|
414 |
|
415 |
|
416 |
|
417 | calculateSessionExpiry(shortTimeout = false) {
|
418 |
|
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 |
|
429 |
|
430 |
|
431 | async destroySession() {
|
432 | if (this.Session.verifyInstallation() &&
|
433 | this.Session.verifyDeviceInstallation() &&
|
434 | this.Session.verifySessionInstallation()) {
|
435 |
|
436 | try {
|
437 | await this.api.sessionServer.delete();
|
438 | }
|
439 | catch (ex) { }
|
440 | }
|
441 |
|
442 | this.clearExpiryTimer();
|
443 |
|
444 | await this.Session.destroySession();
|
445 | }
|
446 | |
447 |
|
448 |
|
449 |
|
450 | async destroyApiSession(save = false) {
|
451 |
|
452 | this.clearExpiryTimer();
|
453 |
|
454 | await this.Session.destroyApiSession(save);
|
455 | }
|
456 | |
457 |
|
458 |
|
459 |
|
460 | async getUser(userType, updated = false) {
|
461 | if (updated) {
|
462 |
|
463 | const userList = await this.api.user.list();
|
464 |
|
465 | const userInfoParsed = this.getUserType(userList);
|
466 | if (userInfoParsed.isOAuth) {
|
467 |
|
468 | this.parseOauthUser(userInfoParsed);
|
469 | }
|
470 | else {
|
471 |
|
472 | this.Session.userInfo[userInfoParsed.type] = userInfoParsed.info;
|
473 | }
|
474 | }
|
475 |
|
476 | return this.Session.userInfo[userType];
|
477 | }
|
478 | |
479 |
|
480 |
|
481 |
|
482 | async getUsers(updated = false) {
|
483 | if (updated) {
|
484 |
|
485 | const userList = await this.api.user.list();
|
486 |
|
487 | const userInfoParsed = this.getUserType(userList);
|
488 | if (userInfoParsed.isOAuth) {
|
489 |
|
490 | this.parseOauthUser(userInfoParsed);
|
491 | }
|
492 | else {
|
493 |
|
494 | this.Session.userInfo[userInfoParsed.type] = userInfoParsed.info;
|
495 | }
|
496 | }
|
497 |
|
498 | return this.Session.userInfo;
|
499 | }
|
500 | |
501 |
|
502 |
|
503 |
|
504 |
|
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 | }
|
538 | exports.default = BunqJSClient;
|