1
0
mirror of https://github.com/bitwarden/jslib synced 2025-12-10 21:33:17 +00:00

Split jslib into multiple modules (#363)

* Split jslib into multiple modules
This commit is contained in:
Oscar Hinton
2021-06-03 18:58:57 +02:00
committed by GitHub
parent b1d9b84eae
commit 1016bbfb9e
509 changed files with 8838 additions and 1887 deletions

1280
node/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
node/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "@bitwarden/jslib-node",
"version": "0.0.0",
"description": "Common code used across Bitwarden JavaScript projects.",
"keywords": [
"bitwarden"
],
"author": "Bitwarden Inc.",
"homepage": "https://bitwarden.com",
"repository": {
"type": "git",
"url": "https://github.com/bitwarden/jslib"
},
"license": "GPL-3.0",
"scripts": {
"clean": "rimraf dist/**/*",
"build": "npm run clean && tsc",
"build:watch": "npm run clean && tsc -watch",
"lint": "tslint 'src/**/*.ts' 'spec/**/*.ts'",
"lint:fix": "tslint 'src/**/*.ts' 'spec/**/*.ts' --fix"
},
"devDependencies": {
"@types/inquirer": "^7.3.1",
"@types/lowdb": "^1.0.10",
"@types/node": "^14.17.1",
"@types/node-fetch": "^2.5.10",
"rimraf": "^3.0.2",
"typescript": "4.1.5"
},
"dependencies": {
"@bitwarden/jslib-common": "file:../common",
"chalk": "^4.1.1",
"commander": "7.2.0",
"form-data": "4.0.0",
"https-proxy-agent": "5.0.0",
"inquirer": "8.0.0",
"lowdb": "1.0.0",
"node-fetch": "^2.6.1"
}
}

108
node/src/cli/baseProgram.ts Normal file
View File

@@ -0,0 +1,108 @@
import * as chalk from 'chalk';
import { Response } from './models/response';
import { ListResponse } from './models/response/listResponse';
import { MessageResponse } from './models/response/messageResponse';
import { StringResponse } from './models/response/stringResponse';
import { UserService } from 'jslib-common/abstractions/user.service';
export abstract class BaseProgram {
constructor(
private userService: UserService,
private writeLn: (s: string, finalLine: boolean, error: boolean) => void) { }
protected processResponse(response: Response, exitImmediately = false, dataProcessor: () => string = null) {
if (!response.success) {
if (process.env.BW_QUIET !== 'true') {
if (process.env.BW_RESPONSE === 'true') {
this.writeLn(this.getJson(response), true, false);
} else {
this.writeLn(chalk.redBright(response.message), true, true);
}
}
const exitCode = process.env.BW_CLEANEXIT ? 0 : 1;
if (exitImmediately) {
process.exit(exitCode);
} else {
process.exitCode = exitCode;
}
return;
}
if (process.env.BW_RESPONSE === 'true') {
this.writeLn(this.getJson(response), true, false);
} else if (response.data != null) {
let out: string = dataProcessor != null ? dataProcessor() : null;
if (out == null) {
if (response.data.object === 'string') {
const data = (response.data as StringResponse).data;
if (data != null) {
out = data;
}
} else if (response.data.object === 'list') {
out = this.getJson((response.data as ListResponse).data);
} else if (response.data.object === 'message') {
out = this.getMessage(response);
} else {
out = this.getJson(response.data);
}
}
if (out != null && process.env.BW_QUIET !== 'true') {
this.writeLn(out, true, false);
}
}
if (exitImmediately) {
process.exit(0);
} else {
process.exitCode = 0;
}
}
protected getJson(obj: any): string {
if (process.env.BW_PRETTY === 'true') {
return JSON.stringify(obj, null, ' ');
} else {
return JSON.stringify(obj);
}
}
protected getMessage(response: Response): string {
const message = (response.data as MessageResponse);
if (process.env.BW_RAW === 'true') {
return message.raw;
}
let out: string = '';
if (message.title != null) {
if (message.noColor) {
out = message.title;
} else {
out = chalk.greenBright(message.title);
}
}
if (message.message != null) {
if (message.title != null) {
out += '\n';
}
out += message.message;
}
return out.trim() === '' ? null : out;
}
protected async exitIfAuthed() {
const authed = await this.userService.isAuthenticated();
if (authed) {
const email = await this.userService.getEmail();
this.processResponse(Response.error('You are already logged in as ' + email + '.'), true);
}
}
protected async exitIfNotAuthed() {
const authed = await this.userService.isAuthenticated();
if (!authed) {
this.processResponse(Response.error('You are not logged in.'), true);
}
}
}

View File

@@ -0,0 +1,316 @@
import * as program from 'commander';
import * as http from 'http';
import * as inquirer from 'inquirer';
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { AuthResult } from 'jslib-common/models/domain/authResult';
import { TwoFactorEmailRequest } from 'jslib-common/models/request/twoFactorEmailRequest';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { Response } from '../models/response';
import { MessageResponse } from '../models/response/messageResponse';
import { NodeUtils } from 'jslib-common/misc/nodeUtils';
import { Utils } from 'jslib-common/misc/utils';
// tslint:disable-next-line
const open = require('open');
export class LoginCommand {
protected validatedParams: () => Promise<any>;
protected success: () => Promise<MessageResponse>;
protected canInteract: boolean;
protected clientId: string;
private ssoRedirectUri: string = null;
constructor(protected authService: AuthService, protected apiService: ApiService,
protected i18nService: I18nService, protected environmentService: EnvironmentService,
protected passwordGenerationService: PasswordGenerationService,
protected cryptoFunctionService: CryptoFunctionService, protected platformUtilsService: PlatformUtilsService,
clientId: string) {
this.clientId = clientId;
}
async run(email: string, password: string, options: program.OptionValues) {
this.canInteract = process.env.BW_NOINTERACTION !== 'true';
let ssoCodeVerifier: string = null;
let ssoCode: string = null;
let clientId: string = null;
let clientSecret: string = null;
if (options.apikey != null) {
const storedClientId: string = process.env.BW_CLIENTID;
const storedClientSecret: string = process.env.BW_CLIENTSECRET;
if (storedClientId == null) {
if (this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: 'input',
name: 'clientId',
message: 'client_id:',
});
clientId = answer.clientId;
} else {
clientId = null;
}
} else {
clientId = storedClientId;
}
if (this.canInteract && storedClientSecret == null) {
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: 'input',
name: 'clientSecret',
message: 'client_secret:',
});
clientSecret = answer.clientSecret;
} else {
clientSecret = storedClientSecret;
}
} else if (options.sso != null && this.canInteract) {
const passwordOptions: any = {
type: 'password',
length: 64,
uppercase: true,
lowercase: true,
numbers: true,
special: false,
};
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, 'sha256');
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
try {
ssoCode = await this.getSsoCode(codeChallenge, state);
} catch {
return Response.badRequest('Something went wrong. Try again.');
}
} else {
if ((email == null || email === '') && this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: 'input',
name: 'email',
message: 'Email address:',
});
email = answer.email;
}
if (email == null || email.trim() === '') {
return Response.badRequest('Email address is required.');
}
if (email.indexOf('@') === -1) {
return Response.badRequest('Email address is invalid.');
}
if (password == null || password === '') {
if (options.passwordfile) {
password = await NodeUtils.readFirstLine(options.passwordfile);
} else if (options.passwordenv && process.env[options.passwordenv]) {
password = process.env[options.passwordenv];
} else if (this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: 'password',
name: 'password',
message: 'Master password:',
});
password = answer.password;
}
}
if (password == null || password === '') {
return Response.badRequest('Master password is required.');
}
}
let twoFactorToken: string = options.code;
let twoFactorMethod: TwoFactorProviderType = null;
try {
if (options.method != null) {
twoFactorMethod = parseInt(options.method, null);
}
} catch (e) {
return Response.error('Invalid two-step login method.');
}
try {
if (this.validatedParams != null) {
await this.validatedParams();
}
let response: AuthResult = null;
if (twoFactorToken != null && twoFactorMethod != null) {
if (clientId != null && clientSecret != null) {
response = await this.authService.logInApiKeyComplete(clientId, clientSecret, twoFactorMethod,
twoFactorToken, false);
} else if (ssoCode != null && ssoCodeVerifier != null) {
response = await this.authService.logInSsoComplete(ssoCode, ssoCodeVerifier, this.ssoRedirectUri,
twoFactorMethod, twoFactorToken, false);
} else {
response = await this.authService.logInComplete(email, password, twoFactorMethod,
twoFactorToken, false);
}
} else {
if (clientId != null && clientSecret != null) {
response = await this.authService.logInApiKey(clientId, clientSecret);
} else if (ssoCode != null && ssoCodeVerifier != null) {
response = await this.authService.logInSso(ssoCode, ssoCodeVerifier, this.ssoRedirectUri);
} else {
response = await this.authService.logIn(email, password);
}
if (response.twoFactor) {
let selectedProvider: any = null;
const twoFactorProviders = this.authService.getSupportedTwoFactorProviders(null);
if (twoFactorProviders.length === 0) {
return Response.badRequest('No providers available for this client.');
}
if (twoFactorMethod != null) {
try {
selectedProvider = twoFactorProviders.filter(p => p.type === twoFactorMethod)[0];
} catch (e) {
return Response.error('Invalid two-step login method.');
}
}
if (selectedProvider == null) {
if (twoFactorProviders.length === 1) {
selectedProvider = twoFactorProviders[0];
} else if (this.canInteract) {
const twoFactorOptions = twoFactorProviders.map(p => p.name);
twoFactorOptions.push(new inquirer.Separator());
twoFactorOptions.push('Cancel');
const answer: inquirer.Answers =
await inquirer.createPromptModule({ output: process.stderr })({
type: 'list',
name: 'method',
message: 'Two-step login method:',
choices: twoFactorOptions,
});
const i = twoFactorOptions.indexOf(answer.method);
if (i === (twoFactorOptions.length - 1)) {
return Response.error('Login failed.');
}
selectedProvider = twoFactorProviders[i];
}
if (selectedProvider == null) {
return Response.error('Login failed. No provider selected.');
}
}
if (twoFactorToken == null && response.twoFactorProviders.size > 1 &&
selectedProvider.type === TwoFactorProviderType.Email) {
const emailReq = new TwoFactorEmailRequest(this.authService.email,
this.authService.masterPasswordHash);
await this.apiService.postTwoFactorEmail(emailReq);
}
if (twoFactorToken == null) {
if (this.canInteract) {
const answer: inquirer.Answers =
await inquirer.createPromptModule({ output: process.stderr })({
type: 'input',
name: 'token',
message: 'Two-step login code:',
});
twoFactorToken = answer.token;
}
if (twoFactorToken == null || twoFactorToken === '') {
return Response.badRequest('Code is required.');
}
}
response = await this.authService.logInTwoFactor(selectedProvider.type,
twoFactorToken, false);
}
}
if (response.twoFactor) {
return Response.error('Login failed.');
}
if (response.resetMasterPassword) {
return Response.error('In order to log in with SSO from the CLI, you must first log in' +
' through the web vault to set your master password.');
}
if (this.success != null) {
const res = await this.success();
return Response.success(res);
} else {
const res = new MessageResponse('You are logged in!', null);
return Response.success(res);
}
} catch (e) {
return Response.error(e);
}
}
private async getSsoCode(codeChallenge: string, state: string): Promise<string> {
return new Promise((resolve, reject) => {
const callbackServer = http.createServer((req, res) => {
const urlString = 'http://localhost' + req.url;
const url = new URL(urlString);
const code = url.searchParams.get('code');
const receivedState = url.searchParams.get('state');
res.setHeader('Content-Type', 'text/html');
if (code != null && receivedState != null && this.checkState(receivedState, state)) {
res.writeHead(200);
res.end('<html><head><title>Success | Bitwarden CLI</title></head><body>' +
'<h1>Successfully authenticated with the Bitwarden CLI</h1>' +
'<p>You may now close this tab and return to the terminal.</p>' +
'</body></html>');
callbackServer.close(() => resolve(code));
} else {
res.writeHead(400);
res.end('<html><head><title>Failed | Bitwarden CLI</title></head><body>' +
'<h1>Something went wrong logging into the Bitwarden CLI</h1>' +
'<p>You may now close this tab and return to the terminal.</p>' +
'</body></html>');
callbackServer.close(() => reject());
}
});
let foundPort = false;
let webUrl = this.environmentService.getWebVaultUrl();
if (webUrl == null) {
webUrl = 'https://vault.bitwarden.com';
}
for (let port = 8065; port <= 8070; port++) {
try {
this.ssoRedirectUri = 'http://localhost:' + port;
callbackServer.listen(port, () => {
this.platformUtilsService.launchUri(webUrl + '/#/sso?clientId=' + this.clientId +
'&redirectUri=' + encodeURIComponent(this.ssoRedirectUri) +
'&state=' + state + '&codeChallenge=' + codeChallenge);
});
foundPort = true;
break;
} catch { }
}
if (!foundPort) {
reject();
}
});
}
private checkState(state: string, checkState: string): boolean {
if (state === null || state === undefined) {
return false;
}
if (checkState === null || checkState === undefined) {
return false;
}
const stateSplit = state.split('_identifier=');
const checkStateSplit = checkState.split('_identifier=');
return stateSplit[0] === checkStateSplit[0];
}
}

View File

@@ -0,0 +1,19 @@
import * as program from 'commander';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { Response } from '../models/response';
import { MessageResponse } from '../models/response/messageResponse';
export class LogoutCommand {
constructor(private authService: AuthService, private i18nService: I18nService,
private logoutCallback: () => Promise<void>) { }
async run() {
await this.logoutCallback();
this.authService.logOut(() => { /* Do nothing */ });
const res = new MessageResponse('You have logged out.', null);
return Response.success(res);
}
}

View File

@@ -0,0 +1,86 @@
import * as program from 'commander';
import * as fetch from 'node-fetch';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { Response } from '../models/response';
import { MessageResponse } from '../models/response/messageResponse';
export class UpdateCommand {
inPkg: boolean = false;
constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private repoName: string, private executableName: string, private showExtendedMessage: boolean) {
this.inPkg = !!(process as any).pkg;
}
async run(): Promise<Response> {
const currentVersion = await this.platformUtilsService.getApplicationVersion();
const response = await fetch.default('https://api.github.com/repos/bitwarden/' +
this.repoName + '/releases/latest');
if (response.status === 200) {
const responseJson = await response.json();
const res = new MessageResponse(null, null);
const tagName: string = responseJson.tag_name;
if (tagName === ('v' + currentVersion)) {
res.title = 'No update available.';
res.noColor = true;
return Response.success(res);
}
let downloadUrl: string = null;
if (responseJson.assets != null) {
for (const a of responseJson.assets) {
const download: string = a.browser_download_url;
if (download == null) {
continue;
}
if (download.indexOf('.zip') === -1) {
continue;
}
if (process.platform === 'win32' && download.indexOf(this.executableName + '-windows') > -1) {
downloadUrl = download;
break;
} else if (process.platform === 'darwin' && download.indexOf(this.executableName + '-macos') > -1) {
downloadUrl = download;
break;
} else if (process.platform === 'linux' && download.indexOf(this.executableName + '-linux') > -1) {
downloadUrl = download;
break;
}
}
}
res.title = 'A new version is available: ' + tagName;
if (downloadUrl == null) {
downloadUrl = 'https://github.com/bitwarden/' + this.repoName + '/releases';
} else {
res.raw = downloadUrl;
}
res.message = '';
if (responseJson.body != null && responseJson.body !== '') {
res.message = responseJson.body + '\n\n';
}
res.message += 'You can download this update at ' + downloadUrl;
if (this.showExtendedMessage) {
if (this.inPkg) {
res.message += '\n\nIf you installed this CLI through a package manager ' +
'you should probably update using its update command instead.';
} else {
res.message += '\n\nIf you installed this CLI through NPM ' +
'you should update using `npm install -g @bitwarden/' + this.repoName + '`';
}
}
return Response.success(res);
} else {
return Response.error('Error contacting update API: ' + response.status);
}
}
}

View File

@@ -0,0 +1,45 @@
import { BaseResponse } from './response/baseResponse';
export class Response {
static error(error: any, data?: any): Response {
const res = new Response();
res.success = false;
if (typeof (error) === 'string') {
res.message = error;
} else {
res.message = error.message != null ? error.message :
error.toString() === '[object Object]' ? JSON.stringify(error) : error.toString();
}
res.data = data;
return res;
}
static notFound(): Response {
return Response.error('Not found.');
}
static badRequest(message: string): Response {
return Response.error(message);
}
static multipleResults(ids: string[]): Response {
let msg = 'More than one result was found. Try getting a specific object by `id` instead. ' +
'The following objects were found:';
ids.forEach(id => {
msg += '\n' + id;
});
return Response.error(msg, ids);
}
static success(data?: BaseResponse): Response {
const res = new Response();
res.success = true;
res.data = data;
return res;
}
success: boolean;
message: string;
errorCode: number;
data: BaseResponse;
}

View File

@@ -0,0 +1,3 @@
export interface BaseResponse {
object: string;
}

View File

@@ -0,0 +1,11 @@
import { BaseResponse } from './baseResponse';
export class ListResponse implements BaseResponse {
object: string;
data: BaseResponse[];
constructor(data: BaseResponse[]) {
this.object = 'list';
this.data = data;
}
}

View File

@@ -0,0 +1,15 @@
import { BaseResponse } from './baseResponse';
export class MessageResponse implements BaseResponse {
object: string;
title: string;
message: string;
raw: string;
noColor = false;
constructor(title: string, message: string) {
this.object = 'message';
this.title = title;
this.message = message;
}
}

View File

@@ -0,0 +1,11 @@
import { BaseResponse } from './baseResponse';
export class StringResponse implements BaseResponse {
object: string;
data: string;
constructor(data: string) {
this.object = 'string';
this.data = data;
}
}

View File

@@ -0,0 +1,157 @@
import * as child_process from 'child_process';
import { DeviceType } from 'jslib-common/enums/deviceType';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
// tslint:disable-next-line
const open = require('open');
export class CliPlatformUtilsService implements PlatformUtilsService {
identityClientId: string;
private deviceCache: DeviceType = null;
constructor(identityClientId: string, private packageJson: any) {
this.identityClientId = identityClientId;
}
getDevice(): DeviceType {
if (!this.deviceCache) {
switch (process.platform) {
case 'win32':
this.deviceCache = DeviceType.WindowsDesktop;
break;
case 'darwin':
this.deviceCache = DeviceType.MacOsDesktop;
break;
case 'linux':
default:
this.deviceCache = DeviceType.LinuxDesktop;
break;
}
}
return this.deviceCache;
}
getDeviceString(): string {
const device = DeviceType[this.getDevice()].toLowerCase();
return device.replace('desktop', '');
}
isFirefox() {
return false;
}
isChrome() {
return false;
}
isEdge() {
return false;
}
isOpera() {
return false;
}
isVivaldi() {
return false;
}
isSafari() {
return false;
}
isIE() {
return false;
}
isMacAppStore() {
return false;
}
isViewOpen() {
return Promise.resolve(false);
}
lockTimeout(): number {
return null;
}
launchUri(uri: string, options?: any): void {
if (process.platform === 'linux') {
child_process.spawnSync('xdg-open', [uri]);
} else {
open(uri);
}
}
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {
throw new Error('Not implemented.');
}
getApplicationVersion(): Promise<string> {
return Promise.resolve(this.packageJson.version);
}
supportsWebAuthn(win: Window) {
return false;
}
supportsDuo(): boolean {
return false;
}
showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[],
options?: any): void {
throw new Error('Not implemented.');
}
showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string):
Promise<boolean> {
throw new Error('Not implemented.');
}
showPasswordDialog(title: string, body: string, passwordValidation: (value: string) => Promise<boolean>):
Promise<boolean> {
throw new Error('Not implemented.');
}
isDev(): boolean {
return process.env.BWCLI_ENV === 'development';
}
isSelfHost(): boolean {
return false;
}
copyToClipboard(text: string, options?: any): void {
throw new Error('Not implemented.');
}
readFromClipboard(options?: any): Promise<string> {
throw new Error('Not implemented.');
}
supportsBiometric(): Promise<boolean> {
return Promise.resolve(false);
}
authenticateBiometric(): Promise<boolean> {
return Promise.resolve(false);
}
getDefaultSystemTheme() {
return Promise.resolve('light' as 'light' | 'dark');
}
onDefaultSystemThemeChange() {
/* noop */
}
supportsSecureStorage(): boolean {
return false;
}
}

View File

@@ -0,0 +1,23 @@
import { LogLevelType } from 'jslib-common/enums/logLevelType';
import { ConsoleLogService as BaseConsoleLogService } from 'jslib-common/services/consoleLog.service';
export class ConsoleLogService extends BaseConsoleLogService {
constructor(isDev: boolean, filter: (level: LogLevelType) => boolean = null) {
super(isDev, filter);
}
write(level: LogLevelType, message: string) {
if (this.filter != null && this.filter(level)) {
return;
}
if (process.env.BW_RESPONSE === 'true') {
// tslint:disable-next-line
console.error(message);
return;
}
super.write(level, message);
}
}

0
node/src/globals.d.ts vendored Normal file
View File

View File

@@ -0,0 +1,115 @@
import * as fs from 'fs';
import * as lowdb from 'lowdb';
import * as FileSync from 'lowdb/adapters/FileSync';
import * as path from 'path';
import { LogService } from 'jslib-common/abstractions/log.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { NodeUtils } from 'jslib-common/misc/nodeUtils';
import { Utils } from 'jslib-common/misc/utils';
export class LowdbStorageService implements StorageService {
protected dataFilePath: string;
private db: lowdb.LowdbSync<any>;
private defaults: any;
constructor(protected logService: LogService, defaults?: any, private dir?: string, private allowCache = false) {
this.defaults = defaults;
}
async init() {
this.logService.info('Initializing lowdb storage service.');
let adapter: lowdb.AdapterSync<any>;
if (Utils.isNode && this.dir != null) {
if (!fs.existsSync(this.dir)) {
this.logService.warning(`Could not find dir, "${this.dir}"; creating it instead.`);
NodeUtils.mkdirpSync(this.dir, '700');
this.logService.info(`Created dir "${this.dir}".`);
}
this.dataFilePath = path.join(this.dir, 'data.json');
if (!fs.existsSync(this.dataFilePath)) {
this.logService.warning(`Could not find data file, "${this.dataFilePath}"; creating it instead.`);
fs.writeFileSync(this.dataFilePath, '', { mode: 0o600 });
fs.chmodSync(this.dataFilePath, 0o600);
this.logService.info(`Created data file "${this.dataFilePath}" with chmod 600.`);
} else {
this.logService.info(`db file "${this.dataFilePath} already exists"; using existing db`);
}
await this.lockDbFile(() => {
adapter = new FileSync(this.dataFilePath);
});
}
try {
this.logService.info('Attempting to create lowdb storage adapter.');
this.db = lowdb(adapter);
this.logService.info('Successfully created lowdb storage adapter.');
} catch (e) {
if (e instanceof SyntaxError) {
this.logService.warning(`Error creating lowdb storage adapter, "${e.message}"; emptying data file.`);
if (fs.existsSync(this.dataFilePath)) {
const backupPath = this.dataFilePath + '.bak';
this.logService.warning(`Writing backup of data file to ${backupPath}`);
await fs.copyFile(this.dataFilePath, backupPath, err => {
this.logService.warning(`Error while creating data file backup, "${e.message}". No backup may have been created.`);
});
}
adapter.write({});
this.db = lowdb(adapter);
} else {
this.logService.error(`Error creating lowdb storage adapter, "${e.message}".`);
throw e;
}
}
if (this.defaults != null) {
this.lockDbFile(() => {
this.logService.info('Writing defaults.');
this.readForNoCache();
this.db.defaults(this.defaults).write();
this.logService.info('Successfully wrote defaults to db.');
});
}
}
get<T>(key: string): Promise<T> {
return this.lockDbFile(() => {
this.readForNoCache();
const val = this.db.get(key).value();
this.logService.debug(`Successfully read ${key} from db`);
if (val == null) {
return null;
}
return val as T;
});
}
save(key: string, obj: any): Promise<any> {
return this.lockDbFile(() => {
this.readForNoCache();
this.db.set(key, obj).write();
this.logService.debug(`Successfully wrote ${key} to db`);
return;
});
}
remove(key: string): Promise<any> {
return this.lockDbFile(() => {
this.readForNoCache();
this.db.unset(key).write();
this.logService.debug(`Successfully removed ${key} from db`);
return;
});
}
protected async lockDbFile<T>(action: () => T): Promise<T> {
// Lock methods implemented in clients
return Promise.resolve(action());
}
private readForNoCache() {
if (!this.allowCache) {
this.db.read();
}
}
}

View File

@@ -0,0 +1,29 @@
import * as FormData from 'form-data';
import { HttpsProxyAgent } from 'https-proxy-agent';
import * as fe from 'node-fetch';
import { ApiService } from 'jslib-common/services/api.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
(global as any).fetch = fe.default;
(global as any).Request = fe.Request;
(global as any).Response = fe.Response;
(global as any).Headers = fe.Headers;
(global as any).FormData = FormData;
export class NodeApiService extends ApiService {
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService,
logoutCallback: (expired: boolean) => Promise<void>, customUserAgent: string = null) {
super(tokenService, platformUtilsService, logoutCallback, customUserAgent);
}
nativeFetch(request: Request): Promise<Response> {
const proxy = process.env.http_proxy || process.env.https_proxy;
if (proxy) {
(request as any).agent = new HttpsProxyAgent(proxy);
}
return fetch(request);
}
}

31
node/tsconfig.json Normal file
View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"pretty": true,
"moduleResolution": "node",
"noImplicitAny": true,
"target": "ES6",
"module": "commonjs",
"lib": ["es5", "es6", "es7", "dom"],
"sourceMap": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declarationDir": "dist/types",
"outDir": "dist",
"types": [],
"paths": {
"jslib-common/*": [
"../common/src/*"
]
}
},
"include": [
"src",
"spec"
],
"exclude": [
"node_modules",
"dist"
]
}