mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[PM-328] Move Send to Tools (#5104)
* Move send in libs/common * Move send in libs/angular * Move send in browser * Move send in cli * Move send in desktop * Move send in web
This commit is contained in:
committed by
GitHub
parent
e645688f8a
commit
e238ea20a9
151
apps/cli/src/tools/send/commands/create.command.ts
Normal file
151
apps/cli/src/tools/send/commands/create.command.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
|
||||
import { Response } from "../../../models/response";
|
||||
import { CliUtils } from "../../../utils";
|
||||
import { SendTextResponse } from "../models/send-text.response";
|
||||
import { SendResponse } from "../models/send.response";
|
||||
|
||||
export class SendCreateCommand {
|
||||
constructor(
|
||||
private sendService: SendService,
|
||||
private stateService: StateService,
|
||||
private environmentService: EnvironmentService,
|
||||
private sendApiService: SendApiService
|
||||
) {}
|
||||
|
||||
async run(requestJson: any, cmdOptions: Record<string, any>) {
|
||||
let req: any = null;
|
||||
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
|
||||
requestJson = await CliUtils.readStdin();
|
||||
}
|
||||
|
||||
if (requestJson == null || requestJson === "") {
|
||||
return Response.badRequest("`requestJson` was not provided.");
|
||||
}
|
||||
|
||||
if (typeof requestJson !== "string") {
|
||||
req = requestJson;
|
||||
req.deletionDate = req.deletionDate == null ? null : new Date(req.deletionDate);
|
||||
req.expirationDate = req.expirationDate == null ? null : new Date(req.expirationDate);
|
||||
} else {
|
||||
try {
|
||||
const reqJson = Buffer.from(requestJson, "base64").toString();
|
||||
req = SendResponse.fromJson(reqJson);
|
||||
|
||||
if (req == null) {
|
||||
throw new Error("Null request");
|
||||
}
|
||||
} catch (e) {
|
||||
return Response.badRequest("Error parsing the encoded request data.");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
req.deletionDate == null ||
|
||||
isNaN(new Date(req.deletionDate).getTime()) ||
|
||||
new Date(req.deletionDate) <= new Date()
|
||||
) {
|
||||
return Response.badRequest("Must specify a valid deletion date after the current time");
|
||||
}
|
||||
|
||||
if (req.expirationDate != null && isNaN(new Date(req.expirationDate).getTime())) {
|
||||
return Response.badRequest("Unable to parse expirationDate: " + req.expirationDate);
|
||||
}
|
||||
|
||||
const normalizedOptions = new Options(cmdOptions);
|
||||
return this.createSend(req, normalizedOptions);
|
||||
}
|
||||
|
||||
private async createSend(req: SendResponse, options: Options) {
|
||||
const filePath = req.file?.fileName ?? options.file;
|
||||
const text = req.text?.text ?? options.text;
|
||||
const hidden = req.text?.hidden ?? options.hidden;
|
||||
const password = req.password ?? options.password;
|
||||
const maxAccessCount = req.maxAccessCount ?? options.maxAccessCount;
|
||||
|
||||
req.key = null;
|
||||
req.maxAccessCount = maxAccessCount;
|
||||
|
||||
switch (req.type) {
|
||||
case SendType.File:
|
||||
if (process.env.BW_SERVE === "true") {
|
||||
return Response.error(
|
||||
"Creating a file-based Send is unsupported through the `serve` command at this time."
|
||||
);
|
||||
}
|
||||
|
||||
if (!(await this.stateService.getCanAccessPremium())) {
|
||||
return Response.error("Premium status is required to use this feature.");
|
||||
}
|
||||
|
||||
if (filePath == null) {
|
||||
return Response.badRequest(
|
||||
"Must specify a file to Send either with the --file option or in the request JSON."
|
||||
);
|
||||
}
|
||||
|
||||
req.file.fileName = path.basename(filePath);
|
||||
break;
|
||||
case SendType.Text:
|
||||
if (text == null) {
|
||||
return Response.badRequest(
|
||||
"Must specify text content to Send either with the --text option or in the request JSON."
|
||||
);
|
||||
}
|
||||
req.text = new SendTextResponse();
|
||||
req.text.text = text;
|
||||
req.text.hidden = hidden;
|
||||
break;
|
||||
default:
|
||||
return Response.badRequest(
|
||||
"Unknown Send type " + SendType[req.type] + ". Valid types are: file, text"
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
let fileBuffer: ArrayBuffer = null;
|
||||
if (req.type === SendType.File) {
|
||||
fileBuffer = NodeUtils.bufferToArrayBuffer(fs.readFileSync(filePath));
|
||||
}
|
||||
|
||||
const sendView = SendResponse.toView(req);
|
||||
const [encSend, fileData] = await this.sendService.encrypt(sendView, fileBuffer, password);
|
||||
// Add dates from template
|
||||
encSend.deletionDate = sendView.deletionDate;
|
||||
encSend.expirationDate = sendView.expirationDate;
|
||||
|
||||
await this.sendApiService.save([encSend, fileData]);
|
||||
const newSend = await this.sendService.getFromState(encSend.id);
|
||||
const decSend = await newSend.decrypt();
|
||||
const res = new SendResponse(decSend, this.environmentService.getWebVaultUrl());
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Options {
|
||||
file: string;
|
||||
text: string;
|
||||
maxAccessCount: number;
|
||||
password: string;
|
||||
hidden: boolean;
|
||||
|
||||
constructor(passedOptions: Record<string, any>) {
|
||||
this.file = passedOptions?.file;
|
||||
this.text = passedOptions?.text;
|
||||
this.password = passedOptions?.password;
|
||||
this.hidden = CliUtils.convertBooleanOption(passedOptions?.hidden);
|
||||
this.maxAccessCount =
|
||||
passedOptions?.maxAccessCount != null ? parseInt(passedOptions.maxAccessCount, null) : null;
|
||||
}
|
||||
}
|
||||
23
apps/cli/src/tools/send/commands/delete.command.ts
Normal file
23
apps/cli/src/tools/send/commands/delete.command.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
|
||||
import { Response } from "../../../models/response";
|
||||
|
||||
export class SendDeleteCommand {
|
||||
constructor(private sendService: SendService, private sendApiService: SendApiService) {}
|
||||
|
||||
async run(id: string) {
|
||||
const send = await this.sendService.getFromState(id);
|
||||
|
||||
if (send == null) {
|
||||
return Response.notFound();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sendApiService.delete(id);
|
||||
return Response.success();
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
92
apps/cli/src/tools/send/commands/edit.command.ts
Normal file
92
apps/cli/src/tools/send/commands/edit.command.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
|
||||
import { Response } from "../../../models/response";
|
||||
import { CliUtils } from "../../../utils";
|
||||
import { SendResponse } from "../models/send.response";
|
||||
|
||||
import { SendGetCommand } from "./get.command";
|
||||
|
||||
export class SendEditCommand {
|
||||
constructor(
|
||||
private sendService: SendService,
|
||||
private stateService: StateService,
|
||||
private getCommand: SendGetCommand,
|
||||
private sendApiService: SendApiService
|
||||
) {}
|
||||
|
||||
async run(requestJson: string, cmdOptions: Record<string, any>): Promise<Response> {
|
||||
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
|
||||
requestJson = await CliUtils.readStdin();
|
||||
}
|
||||
|
||||
if (requestJson == null || requestJson === "") {
|
||||
return Response.badRequest("`requestJson` was not provided.");
|
||||
}
|
||||
|
||||
let req: SendResponse = null;
|
||||
if (typeof requestJson !== "string") {
|
||||
req = requestJson;
|
||||
req.deletionDate = req.deletionDate == null ? null : new Date(req.deletionDate);
|
||||
req.expirationDate = req.expirationDate == null ? null : new Date(req.expirationDate);
|
||||
} else {
|
||||
try {
|
||||
const reqJson = Buffer.from(requestJson, "base64").toString();
|
||||
req = SendResponse.fromJson(reqJson);
|
||||
} catch (e) {
|
||||
return Response.badRequest("Error parsing the encoded request data.");
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedOptions = new Options(cmdOptions);
|
||||
req.id = normalizedOptions.itemId || req.id;
|
||||
|
||||
if (req.id != null) {
|
||||
req.id = req.id.toLowerCase();
|
||||
}
|
||||
|
||||
const send = await this.sendService.getFromState(req.id);
|
||||
|
||||
if (send == null) {
|
||||
return Response.notFound();
|
||||
}
|
||||
|
||||
if (send.type !== req.type) {
|
||||
return Response.badRequest("Cannot change a Send's type");
|
||||
}
|
||||
|
||||
if (send.type === SendType.File && !(await this.stateService.getCanAccessPremium())) {
|
||||
return Response.error("Premium status is required to use this feature.");
|
||||
}
|
||||
|
||||
let sendView = await send.decrypt();
|
||||
sendView = SendResponse.toView(req, sendView);
|
||||
|
||||
if (typeof req.password !== "string" || req.password === "") {
|
||||
req.password = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const [encSend, encFileData] = await this.sendService.encrypt(sendView, null, req.password);
|
||||
// Add dates from template
|
||||
encSend.deletionDate = sendView.deletionDate;
|
||||
encSend.expirationDate = sendView.expirationDate;
|
||||
|
||||
await this.sendApiService.save([encSend, encFileData]);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
|
||||
return await this.getCommand.run(send.id, {});
|
||||
}
|
||||
}
|
||||
|
||||
class Options {
|
||||
itemId: string;
|
||||
|
||||
constructor(passedOptions: Record<string, any>) {
|
||||
this.itemId = passedOptions?.itemId || passedOptions?.itemid;
|
||||
}
|
||||
}
|
||||
83
apps/cli/src/tools/send/commands/get.command.ts
Normal file
83
apps/cli/src/tools/send/commands/get.command.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import * as program from "commander";
|
||||
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
|
||||
import { DownloadCommand } from "../../../commands/download.command";
|
||||
import { Response } from "../../../models/response";
|
||||
import { SendResponse } from "../models/send.response";
|
||||
|
||||
export class SendGetCommand extends DownloadCommand {
|
||||
constructor(
|
||||
private sendService: SendService,
|
||||
private environmentService: EnvironmentService,
|
||||
private searchService: SearchService,
|
||||
cryptoService: CryptoService
|
||||
) {
|
||||
super(cryptoService);
|
||||
}
|
||||
|
||||
async run(id: string, options: program.OptionValues) {
|
||||
const serveCommand = process.env.BW_SERVE === "true";
|
||||
if (serveCommand && !Utils.isGuid(id)) {
|
||||
return Response.badRequest("`" + id + "` is not a GUID.");
|
||||
}
|
||||
|
||||
let sends = await this.getSendView(id);
|
||||
if (sends == null) {
|
||||
return Response.notFound();
|
||||
}
|
||||
|
||||
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||
let filter = (s: SendView) => true;
|
||||
let selector = async (s: SendView): Promise<Response> =>
|
||||
Response.success(new SendResponse(s, webVaultUrl));
|
||||
if (!serveCommand && options?.text != null) {
|
||||
filter = (s) => {
|
||||
return filter(s) && s.text != null;
|
||||
};
|
||||
selector = async (s) => {
|
||||
// Write to stdout and response success so we get the text string only to stdout
|
||||
process.stdout.write(s.text.text);
|
||||
return Response.success();
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(sends)) {
|
||||
if (filter != null) {
|
||||
sends = sends.filter(filter);
|
||||
}
|
||||
if (sends.length > 1) {
|
||||
return Response.multipleResults(sends.map((s) => s.id));
|
||||
}
|
||||
if (sends.length > 0) {
|
||||
return selector(sends[0]);
|
||||
} else {
|
||||
return Response.notFound();
|
||||
}
|
||||
}
|
||||
|
||||
return selector(sends);
|
||||
}
|
||||
|
||||
private async getSendView(id: string): Promise<SendView | SendView[]> {
|
||||
if (Utils.isGuid(id)) {
|
||||
const send = await this.sendService.getFromState(id);
|
||||
if (send != null) {
|
||||
return await send.decrypt();
|
||||
}
|
||||
} else if (id.trim() !== "") {
|
||||
let sends = await this.sendService.getAllDecryptedFromState();
|
||||
sends = this.searchService.searchSends(sends, id);
|
||||
if (sends.length > 1) {
|
||||
return sends;
|
||||
} else if (sends.length > 0) {
|
||||
return sends[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
apps/cli/src/tools/send/commands/index.ts
Normal file
7
apps/cli/src/tools/send/commands/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./create.command";
|
||||
export * from "./delete.command";
|
||||
export * from "./edit.command";
|
||||
export * from "./get.command";
|
||||
export * from "./list.command";
|
||||
export * from "./receive.command";
|
||||
export * from "./remove-password.command";
|
||||
36
apps/cli/src/tools/send/commands/list.command.ts
Normal file
36
apps/cli/src/tools/send/commands/list.command.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
|
||||
import { Response } from "../../../models/response";
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { SendResponse } from "../models/send.response";
|
||||
|
||||
export class SendListCommand {
|
||||
constructor(
|
||||
private sendService: SendService,
|
||||
private environmentService: EnvironmentService,
|
||||
private searchService: SearchService
|
||||
) {}
|
||||
|
||||
async run(cmdOptions: Record<string, any>): Promise<Response> {
|
||||
let sends = await this.sendService.getAllDecryptedFromState();
|
||||
|
||||
const normalizedOptions = new Options(cmdOptions);
|
||||
if (normalizedOptions.search != null && normalizedOptions.search.trim() !== "") {
|
||||
sends = this.searchService.searchSends(sends, normalizedOptions.search);
|
||||
}
|
||||
|
||||
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||
const res = new ListResponse(sends.map((s) => new SendResponse(s, webVaultUrl)));
|
||||
return Response.success(res);
|
||||
}
|
||||
}
|
||||
|
||||
class Options {
|
||||
search: string;
|
||||
|
||||
constructor(passedOptions: Record<string, any>) {
|
||||
this.search = passedOptions?.search;
|
||||
}
|
||||
}
|
||||
176
apps/cli/src/tools/send/commands/receive.command.ts
Normal file
176
apps/cli/src/tools/send/commands/receive.command.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import * as program from "commander";
|
||||
import * as inquirer from "inquirer";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access";
|
||||
import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request";
|
||||
import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
|
||||
import { DownloadCommand } from "../../../commands/download.command";
|
||||
import { Response } from "../../../models/response";
|
||||
import { SendAccessResponse } from "../models/send-access.response";
|
||||
|
||||
export class SendReceiveCommand extends DownloadCommand {
|
||||
private canInteract: boolean;
|
||||
private decKey: SymmetricCryptoKey;
|
||||
private sendAccessRequest: SendAccessRequest;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
cryptoService: CryptoService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private environmentService: EnvironmentService,
|
||||
private sendApiService: SendApiService
|
||||
) {
|
||||
super(cryptoService);
|
||||
}
|
||||
|
||||
async run(url: string, options: program.OptionValues): Promise<Response> {
|
||||
this.canInteract = process.env.BW_NOINTERACTION !== "true";
|
||||
|
||||
let urlObject: URL;
|
||||
try {
|
||||
urlObject = new URL(url);
|
||||
} catch (e) {
|
||||
return Response.badRequest("Failed to parse the provided Send url");
|
||||
}
|
||||
|
||||
const apiUrl = this.getApiUrl(urlObject);
|
||||
const [id, key] = this.getIdAndKey(urlObject);
|
||||
|
||||
if (Utils.isNullOrWhitespace(id) || Utils.isNullOrWhitespace(key)) {
|
||||
return Response.badRequest("Failed to parse url, the url provided is not a valid Send url");
|
||||
}
|
||||
|
||||
const keyArray = Utils.fromUrlB64ToArray(key);
|
||||
this.sendAccessRequest = new SendAccessRequest();
|
||||
|
||||
let password = options.password;
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
if (password != null && password !== "") {
|
||||
this.sendAccessRequest.password = await this.getUnlockedPassword(password, keyArray);
|
||||
}
|
||||
|
||||
const response = await this.sendRequest(apiUrl, id, keyArray);
|
||||
|
||||
if (response instanceof Response) {
|
||||
// Error scenario
|
||||
return response;
|
||||
}
|
||||
|
||||
if (options.obj != null) {
|
||||
return Response.success(new SendAccessResponse(response));
|
||||
}
|
||||
|
||||
switch (response.type) {
|
||||
case SendType.Text:
|
||||
// Write to stdout and response success so we get the text string only to stdout
|
||||
process.stdout.write(response?.text?.text);
|
||||
return Response.success();
|
||||
case SendType.File: {
|
||||
const downloadData = await this.sendApiService.getSendFileDownloadData(
|
||||
response,
|
||||
this.sendAccessRequest,
|
||||
apiUrl
|
||||
);
|
||||
return await this.saveAttachmentToFile(
|
||||
downloadData.url,
|
||||
this.decKey,
|
||||
response?.file?.fileName,
|
||||
options.output
|
||||
);
|
||||
}
|
||||
default:
|
||||
return Response.success(new SendAccessResponse(response));
|
||||
}
|
||||
}
|
||||
|
||||
private getIdAndKey(url: URL): [string, string] {
|
||||
const result = url.hash.slice(1).split("/").slice(-2);
|
||||
return [result[0], result[1]];
|
||||
}
|
||||
|
||||
private getApiUrl(url: URL) {
|
||||
const urls = this.environmentService.getUrls();
|
||||
if (url.origin === "https://send.bitwarden.com") {
|
||||
return "https://vault.bitwarden.com/api";
|
||||
} else if (url.origin === urls.api) {
|
||||
return url.origin;
|
||||
} else if (this.platformUtilsService.isDev() && url.origin === urls.webVault) {
|
||||
return urls.api;
|
||||
} else {
|
||||
return url.origin + "/api";
|
||||
}
|
||||
}
|
||||
|
||||
private async getUnlockedPassword(password: string, keyArray: ArrayBuffer) {
|
||||
const passwordHash = await this.cryptoFunctionService.pbkdf2(
|
||||
password,
|
||||
keyArray,
|
||||
"sha256",
|
||||
100000
|
||||
);
|
||||
return Utils.fromBufferToB64(passwordHash);
|
||||
}
|
||||
|
||||
private async sendRequest(
|
||||
url: string,
|
||||
id: string,
|
||||
key: ArrayBuffer
|
||||
): Promise<Response | SendAccessView> {
|
||||
try {
|
||||
const sendResponse = await this.sendApiService.postSendAccess(
|
||||
id,
|
||||
this.sendAccessRequest,
|
||||
url
|
||||
);
|
||||
|
||||
const sendAccess = new SendAccess(sendResponse);
|
||||
this.decKey = await this.cryptoService.makeSendKey(key);
|
||||
return await sendAccess.decrypt(this.decKey);
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorResponse) {
|
||||
if (e.statusCode === 401) {
|
||||
if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "password",
|
||||
name: "password",
|
||||
message: "Send password:",
|
||||
});
|
||||
|
||||
// reattempt with new password
|
||||
this.sendAccessRequest.password = await this.getUnlockedPassword(answer.password, key);
|
||||
return await this.sendRequest(url, id, key);
|
||||
}
|
||||
|
||||
return Response.badRequest("Incorrect or missing password");
|
||||
} else if (e.statusCode === 405) {
|
||||
return Response.badRequest("Bad Request");
|
||||
} else if (e.statusCode === 404) {
|
||||
return Response.notFound();
|
||||
}
|
||||
}
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
apps/cli/src/tools/send/commands/remove-password.command.ts
Normal file
22
apps/cli/src/tools/send/commands/remove-password.command.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { SendService } from "@bitwarden/common/tools/send/services//send.service.abstraction";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
|
||||
import { Response } from "../../../models/response";
|
||||
import { SendResponse } from "../models/send.response";
|
||||
|
||||
export class SendRemovePasswordCommand {
|
||||
constructor(private sendService: SendService, private sendApiService: SendApiService) {}
|
||||
|
||||
async run(id: string) {
|
||||
try {
|
||||
await this.sendApiService.removePassword(id);
|
||||
|
||||
const updatedSend = await this.sendService.get(id);
|
||||
const decSend = await updatedSend.decrypt();
|
||||
const res = new SendResponse(decSend);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
apps/cli/src/tools/send/index.ts
Normal file
1
apps/cli/src/tools/send/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./commands";
|
||||
41
apps/cli/src/tools/send/models/send-access.response.ts
Normal file
41
apps/cli/src/tools/send/models/send-access.response.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view";
|
||||
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
import { SendFileResponse } from "./send-file.response";
|
||||
import { SendTextResponse } from "./send-text.response";
|
||||
|
||||
export class SendAccessResponse implements BaseResponse {
|
||||
static template(): SendAccessResponse {
|
||||
const req = new SendAccessResponse();
|
||||
req.name = "Send name";
|
||||
req.type = SendType.Text;
|
||||
req.text = null;
|
||||
req.file = null;
|
||||
return req;
|
||||
}
|
||||
|
||||
object = "send-access";
|
||||
id: string;
|
||||
name: string;
|
||||
type: SendType;
|
||||
text: SendTextResponse;
|
||||
file: SendFileResponse;
|
||||
|
||||
constructor(o?: SendAccessView) {
|
||||
if (o == null) {
|
||||
return;
|
||||
}
|
||||
this.id = o.id;
|
||||
this.name = o.name;
|
||||
this.type = o.type;
|
||||
|
||||
if (o.type === SendType.Text && o.text != null) {
|
||||
this.text = new SendTextResponse(o.text);
|
||||
}
|
||||
if (o.type === SendType.File && o.file != null) {
|
||||
this.file = new SendFileResponse(o.file);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
apps/cli/src/tools/send/models/send-file.response.ts
Normal file
36
apps/cli/src/tools/send/models/send-file.response.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { SendFileView } from "@bitwarden/common/tools/send/models/view/send-file.view";
|
||||
|
||||
export class SendFileResponse {
|
||||
static template(fileName = "file attachment location"): SendFileResponse {
|
||||
const req = new SendFileResponse();
|
||||
req.fileName = fileName;
|
||||
return req;
|
||||
}
|
||||
|
||||
static toView(file: SendFileResponse, view = new SendFileView()) {
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
view.id = file.id;
|
||||
view.size = file.size;
|
||||
view.sizeName = file.sizeName;
|
||||
view.fileName = file.fileName;
|
||||
return view;
|
||||
}
|
||||
|
||||
id: string;
|
||||
size: string;
|
||||
sizeName: string;
|
||||
fileName: string;
|
||||
|
||||
constructor(o?: SendFileView) {
|
||||
if (o == null) {
|
||||
return;
|
||||
}
|
||||
this.id = o.id;
|
||||
this.size = o.size;
|
||||
this.sizeName = o.sizeName;
|
||||
this.fileName = o.fileName;
|
||||
}
|
||||
}
|
||||
30
apps/cli/src/tools/send/models/send-text.response.ts
Normal file
30
apps/cli/src/tools/send/models/send-text.response.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { SendTextView } from "@bitwarden/common/tools/send/models/view/send-text.view";
|
||||
|
||||
export class SendTextResponse {
|
||||
static template(text = "Text contained in the send.", hidden = false): SendTextResponse {
|
||||
const req = new SendTextResponse();
|
||||
req.text = text;
|
||||
req.hidden = hidden;
|
||||
return req;
|
||||
}
|
||||
|
||||
static toView(text: SendTextResponse, view = new SendTextView()) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
view.text = text.text;
|
||||
view.hidden = text.hidden;
|
||||
return view;
|
||||
}
|
||||
text: string;
|
||||
hidden: boolean;
|
||||
|
||||
constructor(o?: SendTextView) {
|
||||
if (o == null) {
|
||||
return;
|
||||
}
|
||||
this.text = o.text;
|
||||
this.hidden = o.hidden;
|
||||
}
|
||||
}
|
||||
124
apps/cli/src/tools/send/models/send.response.ts
Normal file
124
apps/cli/src/tools/send/models/send.response.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
import { SendFileResponse } from "./send-file.response";
|
||||
import { SendTextResponse } from "./send-text.response";
|
||||
|
||||
const dateProperties: string[] = [
|
||||
Utils.nameOf<SendResponse>("deletionDate"),
|
||||
Utils.nameOf<SendResponse>("expirationDate"),
|
||||
];
|
||||
|
||||
export class SendResponse implements BaseResponse {
|
||||
static template(sendType?: SendType, deleteInDays = 7): SendResponse {
|
||||
const req = new SendResponse();
|
||||
req.name = "Send name";
|
||||
req.notes = "Some notes about this send.";
|
||||
req.type = sendType === SendType.File ? SendType.File : SendType.Text;
|
||||
req.text = sendType === SendType.Text ? SendTextResponse.template() : null;
|
||||
req.file = sendType === SendType.File ? SendFileResponse.template() : null;
|
||||
req.maxAccessCount = null;
|
||||
req.deletionDate = this.getStandardDeletionDate(deleteInDays);
|
||||
req.expirationDate = null;
|
||||
req.password = null;
|
||||
req.disabled = false;
|
||||
req.hideEmail = false;
|
||||
return req;
|
||||
}
|
||||
|
||||
static toView(send: SendResponse, view = new SendView()): SendView {
|
||||
if (send == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
view.id = send.id;
|
||||
view.accessId = send.accessId;
|
||||
view.name = send.name;
|
||||
view.notes = send.notes;
|
||||
view.key = send.key == null ? null : Utils.fromB64ToArray(send.key);
|
||||
view.type = send.type;
|
||||
view.file = SendFileResponse.toView(send.file);
|
||||
view.text = SendTextResponse.toView(send.text);
|
||||
view.maxAccessCount = send.maxAccessCount;
|
||||
view.accessCount = send.accessCount;
|
||||
view.revisionDate = send.revisionDate;
|
||||
view.deletionDate = send.deletionDate;
|
||||
view.expirationDate = send.expirationDate;
|
||||
view.password = send.password;
|
||||
view.disabled = send.disabled;
|
||||
view.hideEmail = send.hideEmail;
|
||||
return view;
|
||||
}
|
||||
|
||||
static fromJson(json: string) {
|
||||
return JSON.parse(json, (key, value) => {
|
||||
if (dateProperties.includes(key)) {
|
||||
return value == null ? null : new Date(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
private static getStandardDeletionDate(days: number) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + days * 86400000); // ms per day
|
||||
return d;
|
||||
}
|
||||
|
||||
object = "send";
|
||||
id: string;
|
||||
accessId: string;
|
||||
accessUrl: string;
|
||||
name: string;
|
||||
notes: string;
|
||||
key: string;
|
||||
type: SendType;
|
||||
text: SendTextResponse;
|
||||
file: SendFileResponse;
|
||||
maxAccessCount?: number;
|
||||
accessCount: number;
|
||||
revisionDate: Date;
|
||||
deletionDate: Date;
|
||||
expirationDate: Date;
|
||||
password: string;
|
||||
passwordSet: boolean;
|
||||
disabled: boolean;
|
||||
hideEmail: boolean;
|
||||
|
||||
constructor(o?: SendView, webVaultUrl?: string) {
|
||||
if (o == null) {
|
||||
return;
|
||||
}
|
||||
this.id = o.id;
|
||||
this.accessId = o.accessId;
|
||||
let sendLinkBaseUrl = webVaultUrl;
|
||||
if (sendLinkBaseUrl == null) {
|
||||
sendLinkBaseUrl = "https://send.bitwarden.com/#";
|
||||
} else {
|
||||
sendLinkBaseUrl += "/#/send/";
|
||||
}
|
||||
this.accessUrl = sendLinkBaseUrl + this.accessId + "/" + o.urlB64Key;
|
||||
this.name = o.name;
|
||||
this.notes = o.notes;
|
||||
this.key = Utils.fromBufferToB64(o.key);
|
||||
this.type = o.type;
|
||||
this.maxAccessCount = o.maxAccessCount;
|
||||
this.accessCount = o.accessCount;
|
||||
this.revisionDate = o.revisionDate;
|
||||
this.deletionDate = o.deletionDate;
|
||||
this.expirationDate = o.expirationDate;
|
||||
this.passwordSet = o.password != null;
|
||||
this.disabled = o.disabled;
|
||||
this.hideEmail = o.hideEmail;
|
||||
|
||||
if (o.type === SendType.Text && o.text != null) {
|
||||
this.text = new SendTextResponse(o.text);
|
||||
}
|
||||
if (o.type === SendType.File && o.file != null) {
|
||||
this.file = new SendFileResponse(o.file);
|
||||
}
|
||||
}
|
||||
}
|
||||
344
apps/cli/src/tools/send/send.program.ts
Normal file
344
apps/cli/src/tools/send/send.program.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import * as chalk from "chalk";
|
||||
import * as program from "commander";
|
||||
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
|
||||
import { Main } from "../../bw";
|
||||
import { GetCommand } from "../../commands/get.command";
|
||||
import { Response } from "../../models/response";
|
||||
import { Program } from "../../program";
|
||||
import { CliUtils } from "../../utils";
|
||||
|
||||
import {
|
||||
SendCreateCommand,
|
||||
SendDeleteCommand,
|
||||
SendEditCommand,
|
||||
SendGetCommand,
|
||||
SendListCommand,
|
||||
SendReceiveCommand,
|
||||
SendRemovePasswordCommand,
|
||||
} from "./commands";
|
||||
import { SendFileResponse } from "./models/send-file.response";
|
||||
import { SendTextResponse } from "./models/send-text.response";
|
||||
import { SendResponse } from "./models/send.response";
|
||||
|
||||
const writeLn = CliUtils.writeLn;
|
||||
|
||||
export class SendProgram extends Program {
|
||||
constructor(main: Main) {
|
||||
super(main);
|
||||
}
|
||||
|
||||
async register() {
|
||||
program.addCommand(this.sendCommand());
|
||||
// receive is accessible both at `bw receive` and `bw send receive`
|
||||
program.addCommand(this.receiveCommand());
|
||||
}
|
||||
|
||||
private sendCommand(): program.Command {
|
||||
return new program.Command("send")
|
||||
.arguments("<data>")
|
||||
.description(
|
||||
"Work with Bitwarden sends. A Send can be quickly created using this command or subcommands can be used to fine-tune the Send",
|
||||
{
|
||||
data: "The data to Send. Specify as a filepath with the --file option",
|
||||
}
|
||||
)
|
||||
.option("-f, --file", "Specifies that <data> is a filepath")
|
||||
.option(
|
||||
"-d, --deleteInDays <days>",
|
||||
"The number of days in the future to set deletion date, defaults to 7",
|
||||
"7"
|
||||
)
|
||||
.option("-a, --maxAccessCount <amount>", "The amount of max possible accesses.")
|
||||
.option("--hidden", "Hide <data> in web by default. Valid only if --file is not set.")
|
||||
.option(
|
||||
"-n, --name <name>",
|
||||
"The name of the Send. Defaults to a guid for text Sends and the filename for files."
|
||||
)
|
||||
.option("--notes <notes>", "Notes to add to the Send.")
|
||||
.option(
|
||||
"--fullObject",
|
||||
"Specifies that the full Send object should be returned rather than just the access url."
|
||||
)
|
||||
.addCommand(this.listCommand())
|
||||
.addCommand(this.templateCommand())
|
||||
.addCommand(this.getCommand())
|
||||
.addCommand(this.receiveCommand())
|
||||
.addCommand(this.createCommand())
|
||||
.addCommand(this.editCommand())
|
||||
.addCommand(this.removePasswordCommand())
|
||||
.addCommand(this.deleteCommand())
|
||||
.action(async (data: string, options: program.OptionValues) => {
|
||||
const encodedJson = this.makeSendJson(data, options);
|
||||
|
||||
let response: Response;
|
||||
if (encodedJson instanceof Response) {
|
||||
response = encodedJson;
|
||||
} else {
|
||||
response = await this.runCreate(encodedJson, options);
|
||||
}
|
||||
|
||||
this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private receiveCommand(): program.Command {
|
||||
return new program.Command("receive")
|
||||
.arguments("<url>")
|
||||
.description("Access a Bitwarden Send from a url")
|
||||
.option("--password <password>", "Password needed to access the Send.")
|
||||
.option("--passwordenv <passwordenv>", "Environment variable storing the Send's password")
|
||||
.option(
|
||||
"--passwordfile <passwordfile>",
|
||||
"Path to a file containing the Sends password as its first line"
|
||||
)
|
||||
.option("--obj", "Return the Send's json object rather than the Send's content")
|
||||
.option("--output <location>", "Specify a file path to save a File-type Send to")
|
||||
.on("--help", () => {
|
||||
writeLn("");
|
||||
writeLn(
|
||||
"If a password is required, the provided password is used or the user is prompted."
|
||||
);
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(async (url: string, options: program.OptionValues) => {
|
||||
const cmd = new SendReceiveCommand(
|
||||
this.main.apiService,
|
||||
this.main.cryptoService,
|
||||
this.main.cryptoFunctionService,
|
||||
this.main.platformUtilsService,
|
||||
this.main.environmentService,
|
||||
this.main.sendApiService
|
||||
);
|
||||
const response = await cmd.run(url, options);
|
||||
this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private listCommand(): program.Command {
|
||||
return new program.Command("list")
|
||||
|
||||
.description("List all the Sends owned by you")
|
||||
.on("--help", () => {
|
||||
writeLn(chalk("This is in the list command"));
|
||||
})
|
||||
.action(async (options: program.OptionValues) => {
|
||||
await this.exitIfLocked();
|
||||
const cmd = new SendListCommand(
|
||||
this.main.sendService,
|
||||
this.main.environmentService,
|
||||
this.main.searchService
|
||||
);
|
||||
const response = await cmd.run(options);
|
||||
this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private templateCommand(): program.Command {
|
||||
return new program.Command("template")
|
||||
.arguments("<object>")
|
||||
.description("Get json templates for send objects", {
|
||||
object: "Valid objects are: send.text, send.file",
|
||||
})
|
||||
.action(async (object) => {
|
||||
const cmd = new GetCommand(
|
||||
this.main.cipherService,
|
||||
this.main.folderService,
|
||||
this.main.collectionService,
|
||||
this.main.totpService,
|
||||
this.main.auditService,
|
||||
this.main.cryptoService,
|
||||
this.main.stateService,
|
||||
this.main.searchService,
|
||||
this.main.apiService,
|
||||
this.main.organizationService
|
||||
);
|
||||
const response = await cmd.run("template", object, null);
|
||||
this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private getCommand(): program.Command {
|
||||
return new program.Command("get")
|
||||
.arguments("<id>")
|
||||
.description("Get Sends owned by you.")
|
||||
.option("--output <output>", "Output directory or filename for attachment.")
|
||||
.option("--text", "Specifies to return the text content of a Send")
|
||||
.on("--help", () => {
|
||||
writeLn("");
|
||||
writeLn(" Id:");
|
||||
writeLn("");
|
||||
writeLn(" Search term or Send's globally unique `id`.");
|
||||
writeLn("");
|
||||
writeLn(" If raw output is specified and no output filename or directory is given for");
|
||||
writeLn(" an attachment query, the attachment content is written to stdout.");
|
||||
writeLn("");
|
||||
writeLn(" Examples:");
|
||||
writeLn("");
|
||||
writeLn(" bw send get searchText");
|
||||
writeLn(" bw send get id");
|
||||
writeLn(" bw send get searchText --text");
|
||||
writeLn(" bw send get searchText --file");
|
||||
writeLn(" bw send get searchText --file --output ../Photos/photo.jpg");
|
||||
writeLn(" bw send get searchText --file --raw");
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(async (id: string, options: program.OptionValues) => {
|
||||
await this.exitIfLocked();
|
||||
const cmd = new SendGetCommand(
|
||||
this.main.sendService,
|
||||
this.main.environmentService,
|
||||
this.main.searchService,
|
||||
this.main.cryptoService
|
||||
);
|
||||
const response = await cmd.run(id, options);
|
||||
this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private createCommand(): program.Command {
|
||||
return new program.Command("create")
|
||||
.arguments("[encodedJson]")
|
||||
.description("create a Send", {
|
||||
encodedJson: "JSON object to upload. Can also be piped in through stdin.",
|
||||
})
|
||||
.option("--file <path>", "file to Send. Can also be specified in parent's JSON.")
|
||||
.option("--text <text>", "text to Send. Can also be specified in parent's JSON.")
|
||||
.option("--hidden", "text hidden flag. Valid only with the --text option.")
|
||||
.option(
|
||||
"--password <password>",
|
||||
"optional password to access this Send. Can also be specified in JSON"
|
||||
)
|
||||
.on("--help", () => {
|
||||
writeLn("");
|
||||
writeLn("Note:");
|
||||
writeLn(" Options specified in JSON take precedence over command options");
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(
|
||||
async (
|
||||
encodedJson: string,
|
||||
options: program.OptionValues,
|
||||
args: { parent: program.Command }
|
||||
) => {
|
||||
// Work-around to support `--fullObject` option for `send create --fullObject`
|
||||
// Calling `option('--fullObject', ...)` above won't work due to Commander doesn't like same option
|
||||
// to be defind on both parent-command and sub-command
|
||||
const { fullObject = false } = args.parent.opts();
|
||||
const mergedOptions = {
|
||||
...options,
|
||||
fullObject: fullObject,
|
||||
};
|
||||
|
||||
const response = await this.runCreate(encodedJson, mergedOptions);
|
||||
this.processResponse(response);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private editCommand(): program.Command {
|
||||
return new program.Command("edit")
|
||||
.arguments("[encodedJson]")
|
||||
.description("edit a Send", {
|
||||
encodedJson:
|
||||
"Updated JSON object to save. If not provided, encodedJson is read from stdin.",
|
||||
})
|
||||
.option("--itemid <itemid>", "Overrides the itemId provided in [encodedJson]")
|
||||
.on("--help", () => {
|
||||
writeLn("");
|
||||
writeLn("Note:");
|
||||
writeLn(" You cannot update a File-type Send's file. Just delete and remake it");
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(async (encodedJson: string, options: program.OptionValues) => {
|
||||
await this.exitIfLocked();
|
||||
const getCmd = new SendGetCommand(
|
||||
this.main.sendService,
|
||||
this.main.environmentService,
|
||||
this.main.searchService,
|
||||
this.main.cryptoService
|
||||
);
|
||||
const cmd = new SendEditCommand(
|
||||
this.main.sendService,
|
||||
this.main.stateService,
|
||||
getCmd,
|
||||
this.main.sendApiService
|
||||
);
|
||||
const response = await cmd.run(encodedJson, options);
|
||||
this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private deleteCommand(): program.Command {
|
||||
return new program.Command("delete")
|
||||
.arguments("<id>")
|
||||
.description("delete a Send", {
|
||||
id: "The id of the Send to delete.",
|
||||
})
|
||||
.action(async (id: string) => {
|
||||
await this.exitIfLocked();
|
||||
const cmd = new SendDeleteCommand(this.main.sendService, this.main.sendApiService);
|
||||
const response = await cmd.run(id);
|
||||
this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private removePasswordCommand(): program.Command {
|
||||
return new program.Command("remove-password")
|
||||
.arguments("<id>")
|
||||
.description("removes the saved password from a Send.", {
|
||||
id: "The id of the Send to alter.",
|
||||
})
|
||||
.action(async (id: string) => {
|
||||
await this.exitIfLocked();
|
||||
const cmd = new SendRemovePasswordCommand(this.main.sendService, this.main.sendApiService);
|
||||
const response = await cmd.run(id);
|
||||
this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private makeSendJson(data: string, options: program.OptionValues) {
|
||||
let sendFile = null;
|
||||
let sendText = null;
|
||||
let name = Utils.newGuid();
|
||||
let type = SendType.Text;
|
||||
if (options.file != null) {
|
||||
data = path.resolve(data);
|
||||
if (!fs.existsSync(data)) {
|
||||
return Response.badRequest("data path does not exist");
|
||||
}
|
||||
|
||||
sendFile = SendFileResponse.template(data);
|
||||
name = path.basename(data);
|
||||
type = SendType.File;
|
||||
} else {
|
||||
sendText = SendTextResponse.template(data, options.hidden);
|
||||
}
|
||||
|
||||
const template = Utils.assign(SendResponse.template(null, options.deleteInDays), {
|
||||
name: options.name ?? name,
|
||||
notes: options.notes,
|
||||
file: sendFile,
|
||||
text: sendText,
|
||||
type: type,
|
||||
});
|
||||
|
||||
return Buffer.from(JSON.stringify(template), "utf8").toString("base64");
|
||||
}
|
||||
|
||||
private async runCreate(encodedJson: string, options: program.OptionValues) {
|
||||
await this.exitIfLocked();
|
||||
const cmd = new SendCreateCommand(
|
||||
this.main.sendService,
|
||||
this.main.stateService,
|
||||
this.main.environmentService,
|
||||
this.main.sendApiService
|
||||
);
|
||||
return await cmd.run(encodedJson, options);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user