mirror of
https://github.com/bitwarden/clients.git
synced 2026-06-04 21:04:29 +08:00
Add new SendApiServer that uses the SDK (#20170)
* Add new SendApiServer that uses the SDK
This commit is contained in:
parent
f0bc8d0c46
commit
f9c569379c
@ -215,8 +215,10 @@ import {
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/password-strength";
|
||||
import { createSystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { SendApiServiceSelector } from "@bitwarden/common/tools/send/services/send-api-service.selector";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
||||
import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendSdkApiService } from "@bitwarden/common/tools/send/services/send-sdk-api.service";
|
||||
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
|
||||
import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
@ -1150,11 +1152,22 @@ export default class MainBackground {
|
||||
this.encryptService,
|
||||
this.configService,
|
||||
);
|
||||
this.sendApiService = new SendApiService(
|
||||
const legacySendApiService = new SendApiService(
|
||||
this.apiService,
|
||||
this.fileUploadService,
|
||||
this.sendService,
|
||||
);
|
||||
this.sendApiService = new SendApiServiceSelector(
|
||||
this.configService,
|
||||
legacySendApiService,
|
||||
new SendSdkApiService(
|
||||
this.sdkService,
|
||||
legacySendApiService,
|
||||
this.sendService,
|
||||
this.accountService,
|
||||
this.logService,
|
||||
),
|
||||
);
|
||||
|
||||
this.avatarService = new AvatarService(this.apiService, this.stateProvider);
|
||||
|
||||
|
||||
@ -156,7 +156,9 @@ import {
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/password-strength";
|
||||
import { createSystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { SendApiServiceSelector } from "@bitwarden/common/tools/send/services/send-api-service.selector";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
||||
import { SendSdkApiService } from "@bitwarden/common/tools/send/services/send-sdk-api.service";
|
||||
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@ -319,7 +321,7 @@ export class ServiceContainer {
|
||||
folderApiService: FolderApiService;
|
||||
userVerificationApiService: UserVerificationApiService;
|
||||
organizationApiService: OrganizationApiServiceAbstraction;
|
||||
sendApiService: SendApiService;
|
||||
sendApiService: SendApiServiceSelector;
|
||||
sendTokenService: SendTokenService;
|
||||
sendPasswordService: SendPasswordService;
|
||||
devicesApiService: DevicesApiServiceAbstraction;
|
||||
@ -652,11 +654,22 @@ export class ServiceContainer {
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.sendApiService = this.sendApiService = new SendApiService(
|
||||
const legacySendApiService = new SendApiService(
|
||||
this.apiService,
|
||||
this.fileUploadService,
|
||||
this.sendService,
|
||||
);
|
||||
this.sendApiService = new SendApiServiceSelector(
|
||||
this.configService,
|
||||
legacySendApiService,
|
||||
new SendSdkApiService(
|
||||
this.sdkService,
|
||||
legacySendApiService,
|
||||
this.sendService,
|
||||
this.accountService,
|
||||
this.logService,
|
||||
),
|
||||
);
|
||||
|
||||
this.sendPasswordService = new DefaultSendPasswordService(this.cryptoFunctionService);
|
||||
|
||||
|
||||
@ -310,8 +310,10 @@ import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/password-strength";
|
||||
import { SendApiServiceSelector } from "@bitwarden/common/tools/send/services/send-api-service.selector";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
||||
import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendSdkApiService } from "@bitwarden/common/tools/send/services/send-sdk-api.service";
|
||||
import { SendStateProvider as SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
||||
import { SendStateProvider as SendStateProviderAbstraction } from "@bitwarden/common/tools/send/services/send-state.provider.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
|
||||
@ -936,10 +938,20 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SendApiServiceAbstraction,
|
||||
provide: SendApiService,
|
||||
useClass: SendApiService,
|
||||
deps: [ApiServiceAbstraction, FileUploadServiceAbstraction, InternalSendService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SendSdkApiService,
|
||||
useClass: SendSdkApiService,
|
||||
deps: [SdkService, SendApiService, InternalSendService, AccountServiceAbstraction, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SendApiServiceAbstraction,
|
||||
useClass: SendApiServiceSelector,
|
||||
deps: [ConfigService, SendApiService, SendSdkApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: KeyApiService,
|
||||
useClass: DefaultKeyApiService,
|
||||
|
||||
@ -62,6 +62,7 @@ export enum FeatureFlag {
|
||||
|
||||
/* Tools */
|
||||
SendControls = "pm-31885-send-controls",
|
||||
Pm30110SdkSendsApi = "pm-30110-sdk-sends-api",
|
||||
SendEventLogging = "pm-36560-send-event-logging",
|
||||
|
||||
/* DIRT */
|
||||
@ -142,6 +143,7 @@ export const DefaultFeatureFlagValue = {
|
||||
|
||||
/* Tools */
|
||||
[FeatureFlag.SendControls]: FALSE,
|
||||
[FeatureFlag.Pm30110SdkSendsApi]: FALSE,
|
||||
[FeatureFlag.SendEventLogging]: FALSE,
|
||||
|
||||
/* DIRT */
|
||||
|
||||
@ -0,0 +1,318 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { SendAccessToken } from "../../../auth/send-access";
|
||||
import { FeatureFlag } from "../../../enums/feature-flag.enum";
|
||||
import { ConfigService } from "../../../platform/abstractions/config/config.service";
|
||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||
import { Send } from "../models/domain/send";
|
||||
import { SendAccessRequest } from "../models/request/send-access.request";
|
||||
import { SendAccessView } from "../models/view/send-access.view";
|
||||
import { AuthType } from "../types/auth-type";
|
||||
import { SendType } from "../types/send-type";
|
||||
|
||||
import { SendApiServiceSelector } from "./send-api-service.selector";
|
||||
import { SendApiService } from "./send-api.service";
|
||||
import { SendSdkApiService } from "./send-sdk-api.service";
|
||||
|
||||
describe("SendApiServiceSelector", () => {
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let legacy: MockProxy<SendApiService>;
|
||||
let sdk: MockProxy<SendSdkApiService>;
|
||||
let flag$: BehaviorSubject<boolean>;
|
||||
|
||||
function buildSelector(initialFlag: boolean): SendApiServiceSelector {
|
||||
flag$ = new BehaviorSubject<boolean>(initialFlag);
|
||||
configService.getFeatureFlag$.mockImplementation((key) =>
|
||||
key === FeatureFlag.Pm30110SdkSendsApi ? flag$.asObservable() : (undefined as any),
|
||||
);
|
||||
return new SendApiServiceSelector(configService, legacy, sdk);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
configService = mock<ConfigService>();
|
||||
legacy = mock<SendApiService>();
|
||||
sdk = mock<SendSdkApiService>();
|
||||
});
|
||||
|
||||
describe("save", () => {
|
||||
it("routes to legacy when creating a file send, even with the flag on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
const send = new Send();
|
||||
send.id = null;
|
||||
send.type = SendType.File;
|
||||
const buffer = mock<EncArrayBuffer>();
|
||||
|
||||
await selector.save([send, buffer]);
|
||||
|
||||
expect(legacy.save).toHaveBeenCalledWith([send, buffer]);
|
||||
expect(sdk.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to SDK for text creates when the flag is on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
const send = new Send();
|
||||
send.id = null;
|
||||
send.type = SendType.Text;
|
||||
send.authType = AuthType.None;
|
||||
const buffer = mock<EncArrayBuffer>();
|
||||
|
||||
await selector.save([send, buffer]);
|
||||
|
||||
expect(sdk.save).toHaveBeenCalledWith([send, buffer]);
|
||||
expect(legacy.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to SDK for file edits when the flag is on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
const send = new Send();
|
||||
send.id = "existing-id";
|
||||
send.type = SendType.File;
|
||||
send.authType = AuthType.None;
|
||||
const buffer = mock<EncArrayBuffer>();
|
||||
|
||||
await selector.save([send, buffer]);
|
||||
|
||||
expect(sdk.save).toHaveBeenCalledWith([send, buffer]);
|
||||
expect(legacy.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to legacy for password-protected creates, even with the flag on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
const send = new Send();
|
||||
send.id = null;
|
||||
send.type = SendType.Text;
|
||||
send.authType = AuthType.Password;
|
||||
const buffer = mock<EncArrayBuffer>();
|
||||
|
||||
await selector.save([send, buffer]);
|
||||
|
||||
expect(legacy.save).toHaveBeenCalledWith([send, buffer]);
|
||||
expect(sdk.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to legacy for password-protected edits, even with the flag on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
const send = new Send();
|
||||
send.id = "existing-id";
|
||||
send.type = SendType.Text;
|
||||
send.authType = AuthType.Password;
|
||||
const buffer = mock<EncArrayBuffer>();
|
||||
|
||||
await selector.save([send, buffer]);
|
||||
|
||||
expect(legacy.save).toHaveBeenCalledWith([send, buffer]);
|
||||
expect(sdk.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to legacy when the flag is off", async () => {
|
||||
const selector = buildSelector(false);
|
||||
const send = new Send();
|
||||
send.id = "existing-id";
|
||||
send.type = SendType.Text;
|
||||
const buffer = mock<EncArrayBuffer>();
|
||||
|
||||
await selector.save([send, buffer]);
|
||||
|
||||
expect(legacy.save).toHaveBeenCalledWith([send, buffer]);
|
||||
expect(sdk.save).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
["delete", (s: SendApiServiceSelector) => s.delete("id")],
|
||||
["removePassword", (s: SendApiServiceSelector) => s.removePassword("id")],
|
||||
["deleteSend", (s: SendApiServiceSelector) => s.deleteSend("id")],
|
||||
])("%s — flag-controlled, no overrides", (methodName, invoke) => {
|
||||
it("routes to SDK when the flag is on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
|
||||
await invoke(selector);
|
||||
|
||||
expect((sdk as any)[methodName]).toHaveBeenCalledWith("id");
|
||||
expect((legacy as any)[methodName]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to legacy when the flag is off", async () => {
|
||||
const selector = buildSelector(false);
|
||||
|
||||
await invoke(selector);
|
||||
|
||||
expect((legacy as any)[methodName]).toHaveBeenCalledWith("id");
|
||||
expect((sdk as any)[methodName]).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
["getSend", (s: SendApiServiceSelector) => s.getSend("id"), ["id"]],
|
||||
["getSends", (s: SendApiServiceSelector) => s.getSends(), []],
|
||||
["putSendRemovePassword", (s: SendApiServiceSelector) => s.putSendRemovePassword("id"), ["id"]],
|
||||
])("%s — always legacy", (methodName, invoke, expectedArgs) => {
|
||||
it.each([true, false])("routes to legacy regardless of flag (flag=%s)", async (flagOn) => {
|
||||
const selector = buildSelector(flagOn);
|
||||
|
||||
await invoke(selector);
|
||||
|
||||
expect((legacy as any)[methodName]).toHaveBeenCalledWith(...expectedArgs);
|
||||
expect((sdk as any)[methodName]).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("postSendAccess", () => {
|
||||
const request = new SendAccessRequest();
|
||||
|
||||
it("routes to legacy when apiUrl is supplied, even with the flag on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
|
||||
await selector.postSendAccess("id", request, "https://other.example");
|
||||
|
||||
expect(legacy.postSendAccess).toHaveBeenCalledWith("id", request, "https://other.example");
|
||||
expect(sdk.postSendAccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to SDK without apiUrl when the flag is on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
|
||||
await selector.postSendAccess("id", request);
|
||||
|
||||
expect(sdk.postSendAccess).toHaveBeenCalledWith("id", request);
|
||||
expect(legacy.postSendAccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to legacy without apiUrl when the flag is off", async () => {
|
||||
const selector = buildSelector(false);
|
||||
|
||||
await selector.postSendAccess("id", request);
|
||||
|
||||
expect(legacy.postSendAccess).toHaveBeenCalledWith("id", request);
|
||||
expect(sdk.postSendAccess).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("postSendAccessV2", () => {
|
||||
const accessToken: SendAccessToken = { token: "tok" } as SendAccessToken;
|
||||
|
||||
it("routes to legacy when apiUrl is supplied, even with the flag on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
|
||||
await selector.postSendAccessV2(accessToken, "https://other.example");
|
||||
|
||||
expect(legacy.postSendAccessV2).toHaveBeenCalledWith(accessToken, "https://other.example");
|
||||
expect(sdk.postSendAccessV2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to SDK without apiUrl when the flag is on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
|
||||
await selector.postSendAccessV2(accessToken);
|
||||
|
||||
expect(sdk.postSendAccessV2).toHaveBeenCalledWith(accessToken);
|
||||
expect(legacy.postSendAccessV2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to legacy without apiUrl when the flag is off", async () => {
|
||||
const selector = buildSelector(false);
|
||||
|
||||
await selector.postSendAccessV2(accessToken);
|
||||
|
||||
expect(legacy.postSendAccessV2).toHaveBeenCalledWith(accessToken);
|
||||
expect(sdk.postSendAccessV2).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSendFileDownloadData", () => {
|
||||
const accessView = mock<SendAccessView>();
|
||||
const request = new SendAccessRequest();
|
||||
|
||||
it("routes to legacy when apiUrl is supplied, even with the flag on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
|
||||
await selector.getSendFileDownloadData(accessView, request, "https://other.example");
|
||||
|
||||
expect(legacy.getSendFileDownloadData).toHaveBeenCalledWith(
|
||||
accessView,
|
||||
request,
|
||||
"https://other.example",
|
||||
);
|
||||
expect(sdk.getSendFileDownloadData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to SDK without apiUrl when the flag is on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
|
||||
await selector.getSendFileDownloadData(accessView, request);
|
||||
|
||||
expect(sdk.getSendFileDownloadData).toHaveBeenCalledWith(accessView, request);
|
||||
expect(legacy.getSendFileDownloadData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to legacy without apiUrl when the flag is off", async () => {
|
||||
const selector = buildSelector(false);
|
||||
|
||||
await selector.getSendFileDownloadData(accessView, request);
|
||||
|
||||
expect(legacy.getSendFileDownloadData).toHaveBeenCalledWith(accessView, request);
|
||||
expect(sdk.getSendFileDownloadData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSendFileDownloadDataV2", () => {
|
||||
const accessView = mock<SendAccessView>();
|
||||
const accessToken: SendAccessToken = { token: "tok" } as SendAccessToken;
|
||||
|
||||
it("routes to legacy when apiUrl is supplied, even with the flag on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
|
||||
await selector.getSendFileDownloadDataV2(accessView, accessToken, "https://other.example");
|
||||
|
||||
expect(legacy.getSendFileDownloadDataV2).toHaveBeenCalledWith(
|
||||
accessView,
|
||||
accessToken,
|
||||
"https://other.example",
|
||||
);
|
||||
expect(sdk.getSendFileDownloadDataV2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to SDK without apiUrl when the flag is on", async () => {
|
||||
const selector = buildSelector(true);
|
||||
|
||||
await selector.getSendFileDownloadDataV2(accessView, accessToken);
|
||||
|
||||
expect(sdk.getSendFileDownloadDataV2).toHaveBeenCalledWith(accessView, accessToken);
|
||||
expect(legacy.getSendFileDownloadDataV2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to legacy without apiUrl when the flag is off", async () => {
|
||||
const selector = buildSelector(false);
|
||||
|
||||
await selector.getSendFileDownloadDataV2(accessView, accessToken);
|
||||
|
||||
expect(legacy.getSendFileDownloadDataV2).toHaveBeenCalledWith(accessView, accessToken);
|
||||
expect(sdk.getSendFileDownloadDataV2).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("feature flag caching", () => {
|
||||
it("subscribes to the feature flag once across many calls", async () => {
|
||||
const selector = buildSelector(true);
|
||||
|
||||
await selector.delete("a");
|
||||
await selector.delete("b");
|
||||
await selector.deleteSend("c");
|
||||
await selector.removePassword("d");
|
||||
|
||||
expect(configService.getFeatureFlag$).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("picks up flag changes for subsequent calls", async () => {
|
||||
const selector = buildSelector(true);
|
||||
|
||||
await selector.delete("first");
|
||||
flag$.next(false);
|
||||
await selector.delete("second");
|
||||
|
||||
expect(sdk.delete).toHaveBeenCalledWith("first");
|
||||
expect(legacy.delete).toHaveBeenCalledWith("second");
|
||||
});
|
||||
});
|
||||
});
|
||||
168
libs/common/src/tools/send/services/send-api-service.selector.ts
Normal file
168
libs/common/src/tools/send/services/send-api-service.selector.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { Observable, firstValueFrom, map, shareReplay } from "rxjs";
|
||||
|
||||
import { SendAccessToken } from "../../../auth/send-access";
|
||||
import { FeatureFlag } from "../../../enums/feature-flag.enum";
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { ConfigService } from "../../../platform/abstractions/config/config.service";
|
||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||
import { Send } from "../models/domain/send";
|
||||
import { SendAccessRequest } from "../models/request/send-access.request";
|
||||
import { SendAccessResponse } from "../models/response/send-access.response";
|
||||
import { SendFileDownloadDataResponse } from "../models/response/send-file-download-data.response";
|
||||
import { SendResponse } from "../models/response/send.response";
|
||||
import { SendAccessView } from "../models/view/send-access.view";
|
||||
import { AuthType } from "../types/auth-type";
|
||||
import { SendType } from "../types/send-type";
|
||||
|
||||
import { SendApiService } from "./send-api.service";
|
||||
import { SendApiService as SendApiServiceAbstraction } from "./send-api.service.abstraction";
|
||||
import { SendSdkApiService } from "./send-sdk-api.service";
|
||||
|
||||
/**
|
||||
* Selects between {@link SendApiService} and {@link SendSdkApiService} based on the
|
||||
* `pm-30110-sdk-sends-api` feature flag.
|
||||
*
|
||||
* Methods whose return type is a wire-encrypted shape the SDK cannot produce (`getSend`,
|
||||
* `getSends`, `putSendRemovePassword`) always route to legacy. Mutations and access-side
|
||||
* methods are flag-controlled; the SDK service refetches the encrypted form via legacy
|
||||
* after mutations to keep `InternalSendService` coherent.
|
||||
*
|
||||
* A "cross-instance Send" is a Send hosted on a different Bitwarden server than the
|
||||
* client is signed in to — typically the CLI receiving a self-hosted or EU-cloud Send
|
||||
* link. Callers signal this by passing `apiUrl`; the selector routes those calls to
|
||||
* legacy because the SDK client targets only its configured environment.
|
||||
*/
|
||||
export class SendApiServiceSelector implements SendApiServiceAbstraction {
|
||||
private readonly service$: Observable<SendApiServiceAbstraction>;
|
||||
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
private sendApiService: SendApiService,
|
||||
private sendSdkApiService: SendSdkApiService,
|
||||
) {
|
||||
this.service$ = configService.getFeatureFlag$(FeatureFlag.Pm30110SdkSendsApi).pipe(
|
||||
map((useSdk) => (useSdk ? this.sendSdkApiService : this.sendApiService)),
|
||||
shareReplay({ bufferSize: 1, refCount: false }),
|
||||
);
|
||||
}
|
||||
|
||||
private getService(): Promise<SendApiServiceAbstraction> {
|
||||
return firstValueFrom(this.service$);
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes saves to SDK when the flag is on, except for two cases that fall back to
|
||||
* legacy regardless: new file sends (the SDK generates its own send key, which
|
||||
* wouldn't match the caller's pre-encrypted file buffer) and password-protected
|
||||
* sends (the SDK re-applies PBKDF2 to `auth.password`, double-hashing the keyB64
|
||||
* the client already derived).
|
||||
*/
|
||||
async save(sendData: [Send, EncArrayBuffer]): Promise<Send> {
|
||||
const [send] = sendData;
|
||||
if (send.id == null && send.type === SendType.File) {
|
||||
return this.sendApiService.save(sendData);
|
||||
}
|
||||
if (send.authType === AuthType.Password) {
|
||||
return this.sendApiService.save(sendData);
|
||||
}
|
||||
return (await this.getService()).save(sendData);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<any> {
|
||||
return (await this.getService()).delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the auth from a send and updates local state.
|
||||
*
|
||||
* Under the SDK flag this calls the V2 endpoint, which removes **all auth** on the
|
||||
* send (password and any other auth type), not just the password. The legacy path
|
||||
* uses V1, which removes only the password. Callers that need the V1 semantics
|
||||
* specifically should keep the flag off until V2 ships everywhere.
|
||||
*/
|
||||
async removePassword(id: string): Promise<any> {
|
||||
return (await this.getService()).removePassword(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Always routed to legacy. Returns a wire-encrypted `SendResponse`, which the SDK
|
||||
* cannot produce (the SDK only exposes plaintext views).
|
||||
*/
|
||||
async getSend(id: string): Promise<SendResponse> {
|
||||
return this.sendApiService.getSend(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesses a send. Routes to legacy whenever `apiUrl` is supplied (cross-instance
|
||||
* receive, e.g. the CLI opening a self-hosted Send link while signed in to a
|
||||
* different server) because the SDK client targets only its configured environment.
|
||||
*/
|
||||
async postSendAccess(
|
||||
id: string,
|
||||
request: SendAccessRequest,
|
||||
apiUrl?: string,
|
||||
): Promise<SendAccessResponse> {
|
||||
if (apiUrl != null) {
|
||||
return this.sendApiService.postSendAccess(id, request, apiUrl);
|
||||
}
|
||||
return (await this.getService()).postSendAccess(id, request);
|
||||
}
|
||||
|
||||
async postSendAccessV2(
|
||||
accessToken: SendAccessToken,
|
||||
apiUrl?: string,
|
||||
): Promise<SendAccessResponse> {
|
||||
if (apiUrl != null) {
|
||||
return this.sendApiService.postSendAccessV2(accessToken, apiUrl);
|
||||
}
|
||||
return (await this.getService()).postSendAccessV2(accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Always routed to legacy. Returns a wire-encrypted list of `SendResponse`, which the
|
||||
* SDK cannot produce; see {@link getSend}.
|
||||
*/
|
||||
async getSends(): Promise<ListResponse<SendResponse>> {
|
||||
return this.sendApiService.getSends();
|
||||
}
|
||||
|
||||
/**
|
||||
* Always routed to legacy. The selector's `removePassword` is the higher-level flow that
|
||||
* also refreshes local state; this lower-level method returns a wire-encrypted
|
||||
* `SendResponse` the SDK cannot produce.
|
||||
*
|
||||
* Note that the legacy `PUT /sends/{id}/remove-password` endpoint is V1 — it removes
|
||||
* only the password, not all auth types. See {@link removePassword} for the V2
|
||||
* (all-auth) behavior under the SDK flag.
|
||||
*/
|
||||
async putSendRemovePassword(id: string): Promise<SendResponse> {
|
||||
return this.sendApiService.putSendRemovePassword(id);
|
||||
}
|
||||
|
||||
async deleteSend(id: string): Promise<any> {
|
||||
return (await this.getService()).deleteSend(id);
|
||||
}
|
||||
|
||||
/** See {@link postSendAccess} — cross-instance callers (those passing `apiUrl`) route to legacy. */
|
||||
async getSendFileDownloadData(
|
||||
send: SendAccessView,
|
||||
request: SendAccessRequest,
|
||||
apiUrl?: string,
|
||||
): Promise<SendFileDownloadDataResponse> {
|
||||
if (apiUrl != null) {
|
||||
return this.sendApiService.getSendFileDownloadData(send, request, apiUrl);
|
||||
}
|
||||
return (await this.getService()).getSendFileDownloadData(send, request);
|
||||
}
|
||||
|
||||
async getSendFileDownloadDataV2(
|
||||
send: SendAccessView,
|
||||
accessToken: SendAccessToken,
|
||||
apiUrl?: string,
|
||||
): Promise<SendFileDownloadDataResponse> {
|
||||
if (apiUrl != null) {
|
||||
return this.sendApiService.getSendFileDownloadDataV2(send, accessToken, apiUrl);
|
||||
}
|
||||
return (await this.getService()).getSendFileDownloadDataV2(send, accessToken);
|
||||
}
|
||||
}
|
||||
@ -4,10 +4,8 @@ import { ListResponse } from "../../../models/response/list.response";
|
||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||
import { Send } from "../models/domain/send";
|
||||
import { SendAccessRequest } from "../models/request/send-access.request";
|
||||
import { SendRequest } from "../models/request/send.request";
|
||||
import { SendAccessResponse } from "../models/response/send-access.response";
|
||||
import { SendFileDownloadDataResponse } from "../models/response/send-file-download-data.response";
|
||||
import { SendFileUploadDataResponse } from "../models/response/send-file-upload-data.response";
|
||||
import { SendResponse } from "../models/response/send.response";
|
||||
import { SendAccessView } from "../models/view/send-access.view";
|
||||
|
||||
@ -23,10 +21,6 @@ export abstract class SendApiService {
|
||||
apiUrl?: string,
|
||||
): Promise<SendAccessResponse>;
|
||||
abstract getSends(): Promise<ListResponse<SendResponse>>;
|
||||
abstract postSend(request: SendRequest): Promise<SendResponse>;
|
||||
abstract postFileTypeSend(request: SendRequest): Promise<SendFileUploadDataResponse>;
|
||||
abstract postSendFile(sendId: string, fileId: string, data: FormData): Promise<any>;
|
||||
abstract putSend(id: string, request: SendRequest): Promise<SendResponse>;
|
||||
abstract putSendRemovePassword(id: string): Promise<SendResponse>;
|
||||
abstract deleteSend(id: string): Promise<any>;
|
||||
abstract getSendFileDownloadData(
|
||||
@ -39,10 +33,6 @@ export abstract class SendApiService {
|
||||
accessToken: SendAccessToken,
|
||||
apiUrl?: string,
|
||||
): Promise<SendFileDownloadDataResponse>;
|
||||
abstract renewSendFileUploadUrl(
|
||||
sendId: string,
|
||||
fileId: string,
|
||||
): Promise<SendFileUploadDataResponse>;
|
||||
abstract removePassword(id: string): Promise<any>;
|
||||
abstract delete(id: string): Promise<any>;
|
||||
abstract save(sendData: [Send, EncArrayBuffer]): Promise<Send>;
|
||||
|
||||
@ -118,39 +118,6 @@ export class SendApiService implements SendApiServiceAbstraction {
|
||||
return new ListResponse(r, SendResponse);
|
||||
}
|
||||
|
||||
async postSend(request: SendRequest): Promise<SendResponse> {
|
||||
const r = await this.apiService.send("POST", "/sends", request, true, true);
|
||||
return new SendResponse(r);
|
||||
}
|
||||
|
||||
async postFileTypeSend(request: SendRequest): Promise<SendFileUploadDataResponse> {
|
||||
const r = await this.apiService.send("POST", "/sends/file/v2", request, true, true);
|
||||
return new SendFileUploadDataResponse(r);
|
||||
}
|
||||
|
||||
async renewSendFileUploadUrl(
|
||||
sendId: string,
|
||||
fileId: string,
|
||||
): Promise<SendFileUploadDataResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/sends/" + sendId + "/file/" + fileId,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new SendFileUploadDataResponse(r);
|
||||
}
|
||||
|
||||
postSendFile(sendId: string, fileId: string, data: FormData): Promise<any> {
|
||||
return this.apiService.send("POST", "/sends/" + sendId + "/file/" + fileId, data, true, false);
|
||||
}
|
||||
|
||||
async putSend(id: string, request: SendRequest): Promise<SendResponse> {
|
||||
const r = await this.apiService.send("PUT", "/sends/" + id, request, true, true);
|
||||
return new SendResponse(r);
|
||||
}
|
||||
|
||||
async putSendRemovePassword(id: string): Promise<SendResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"PUT",
|
||||
@ -187,6 +154,39 @@ export class SendApiService implements SendApiServiceAbstraction {
|
||||
|
||||
// Send File Upload methods
|
||||
|
||||
private async postSend(request: SendRequest): Promise<SendResponse> {
|
||||
const r = await this.apiService.send("POST", "/sends", request, true, true);
|
||||
return new SendResponse(r);
|
||||
}
|
||||
|
||||
private async postFileTypeSend(request: SendRequest): Promise<SendFileUploadDataResponse> {
|
||||
const r = await this.apiService.send("POST", "/sends/file/v2", request, true, true);
|
||||
return new SendFileUploadDataResponse(r);
|
||||
}
|
||||
|
||||
private async renewSendFileUploadUrl(
|
||||
sendId: string,
|
||||
fileId: string,
|
||||
): Promise<SendFileUploadDataResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/sends/" + sendId + "/file/" + fileId,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new SendFileUploadDataResponse(r);
|
||||
}
|
||||
|
||||
private postSendFile(sendId: string, fileId: string, data: FormData): Promise<any> {
|
||||
return this.apiService.send("POST", "/sends/" + sendId + "/file/" + fileId, data, true, false);
|
||||
}
|
||||
|
||||
private async putSend(id: string, request: SendRequest): Promise<SendResponse> {
|
||||
const r = await this.apiService.send("PUT", "/sends/" + id, request, true, true);
|
||||
return new SendResponse(r);
|
||||
}
|
||||
|
||||
private async upload(sendData: [Send, EncArrayBuffer]): Promise<SendResponse> {
|
||||
const request = new SendRequest(sendData[0], sendData[1]?.buffer.byteLength);
|
||||
|
||||
|
||||
344
libs/common/src/tools/send/services/send-sdk-api.service.ts
Normal file
344
libs/common/src/tools/send/services/send-sdk-api.service.ts
Normal file
@ -0,0 +1,344 @@
|
||||
import { catchError, firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
PasswordManagerClient,
|
||||
SendAddRequest,
|
||||
SendAuthType,
|
||||
SendEditRequest,
|
||||
SendId as SdkSendId,
|
||||
SendView as SdkSendView,
|
||||
SendViewType,
|
||||
} from "@bitwarden/sdk-internal";
|
||||
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { SendAccessToken } from "../../../auth/send-access";
|
||||
import { getUserId } from "../../../auth/services/account.service";
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { SdkService, asUuid } from "../../../platform/abstractions/sdk/sdk.service";
|
||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { SendData } from "../models/data/send.data";
|
||||
import { Send } from "../models/domain/send";
|
||||
import { SendAccessRequest } from "../models/request/send-access.request";
|
||||
import { SendAccessResponse } from "../models/response/send-access.response";
|
||||
import { SendFileDownloadDataResponse } from "../models/response/send-file-download-data.response";
|
||||
import { SendResponse } from "../models/response/send.response";
|
||||
import { SendAccessView } from "../models/view/send-access.view";
|
||||
import { SendView } from "../models/view/send.view";
|
||||
import { AuthType } from "../types/auth-type";
|
||||
import { SendType } from "../types/send-type";
|
||||
|
||||
import { SendApiService } from "./send-api.service";
|
||||
import { SendApiService as SendApiServiceAbstraction } from "./send-api.service.abstraction";
|
||||
import { InternalSendService } from "./send.service.abstraction";
|
||||
|
||||
/**
|
||||
* SDK-backed implementation of `SendApiService`. Save/removePassword mutate via the SDK
|
||||
* then refetch via legacy to keep `InternalSendService` populated with `EncString`-shaped
|
||||
* data. Methods returning wire-encrypted shapes have no SDK equivalent and are routed to
|
||||
* legacy by `SendApiServiceSelector`; the throw-stubs here guard direct callers.
|
||||
*/
|
||||
export class SendSdkApiService implements SendApiServiceAbstraction {
|
||||
constructor(
|
||||
private sdkService: SdkService,
|
||||
private legacySendApiService: SendApiService,
|
||||
private sendService: InternalSendService,
|
||||
private accountService: AccountService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Saves a send via the SDK. After the mutation, refetches the wire-encrypted form via
|
||||
* the legacy service to keep `InternalSendService` populated with `EncString`-shaped
|
||||
* data. Patches the input with server-assigned id/accessId on create so callers reading
|
||||
* those after `save()` continue to work.
|
||||
*
|
||||
* Two cases the SDK cannot currently handle are routed to legacy by
|
||||
* `SendApiServiceSelector` and rejected here as a guard for direct callers:
|
||||
*
|
||||
* - **New file sends.** `SendService.encrypt` produces a pre-encrypted buffer under a
|
||||
* client-derived key; the SDK's `create_file_send` generates its own key.
|
||||
* - **Password-protected sends.** `Send.password` is already the PBKDF2-derived
|
||||
* `keyB64`, but the SDK's `SendAuthType::auth_data`
|
||||
* (`bitwarden-send/src/send.rs:160-163`) re-applies PBKDF2 to whatever string sits in
|
||||
* `auth.password`, double-hashing the value. No skip-hash variant exists on the
|
||||
* public request types.
|
||||
*/
|
||||
async save(sendData: [Send, EncArrayBuffer]): Promise<Send> {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const [send] = sendData;
|
||||
if (send.id == null && send.type === SendType.File) {
|
||||
throw new Error("SendSdkApiService.save: file send creation requires SendApiService.");
|
||||
}
|
||||
if (send.authType === AuthType.Password) {
|
||||
throw new Error("SendSdkApiService.save: password-protected sends require SendApiService.");
|
||||
}
|
||||
const sendView = await send.decrypt(userId);
|
||||
const sdkView = await this.mutateSend(sendView, userId);
|
||||
|
||||
// Patch server-assigned identifiers onto the input for callers that read them after
|
||||
// save (matches the legacy SendApiService contract). The server always returns id
|
||||
// and accessId on a successful create.
|
||||
const sendId = sdkView.id as unknown as string;
|
||||
if (send.id == null) {
|
||||
send.id = sendId;
|
||||
send.accessId = sdkView.accessId as unknown as string;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.refreshSendFromServer(sendId);
|
||||
} catch (error) {
|
||||
// The SDK mutation already landed on the server; only the encrypted-form refetch
|
||||
// failed. Surfacing this as a save error would prompt the user to retry, which on
|
||||
// a new send would duplicate it server-side. Log and return the caller's input
|
||||
// Send — its EncString fields are still valid (the caller encrypted them moments
|
||||
// ago), so decryption in post-save paths works. InternalSendService reconciles on
|
||||
// the next sync.
|
||||
this.logService.error(`Send refresh failed after successful mutation: ${error}`);
|
||||
return send;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<any> {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
return await ref.value.sends().delete(asUuid<SdkSendId>(id));
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to delete send: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
await this.sendService.delete(id);
|
||||
}
|
||||
|
||||
// Note: the SDK calls the V2 endpoint which removes all auth (password and any other
|
||||
// auth type), not just the password.
|
||||
async removePassword(id: string): Promise<any> {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
return await ref.value.sends().remove_password(asUuid<SdkSendId>(id));
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to remove send auth: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
await this.refreshSendFromServer(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported via SDK — returns wire-encrypted `SendResponse`, which the SDK cannot
|
||||
* produce. `SendApiServiceSelector` routes calls to `SendApiService`; this stub catches
|
||||
* direct callers that bypass the selector.
|
||||
*/
|
||||
getSend(_id: string): Promise<SendResponse> {
|
||||
return Promise.reject(new Error("SendSdkApiService.getSend: use SendApiService."));
|
||||
}
|
||||
|
||||
// `apiUrl` is intentionally omitted; `SendApiServiceSelector` routes per-call `apiUrl`
|
||||
// to the legacy service.
|
||||
async postSendAccess(id: string, request: SendAccessRequest): Promise<SendAccessResponse> {
|
||||
const sdk: PasswordManagerClient = await firstValueFrom(this.sdkService.client$);
|
||||
const view = await sdk.sends().access_send_v1(id, request.password ?? undefined);
|
||||
return new SendAccessResponse(view);
|
||||
}
|
||||
|
||||
async postSendAccessV2(accessToken: SendAccessToken): Promise<SendAccessResponse> {
|
||||
const sdk: PasswordManagerClient = await firstValueFrom(this.sdkService.client$);
|
||||
const view = await sdk.sends().access_send(accessToken.token);
|
||||
return new SendAccessResponse(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported via SDK — returns wire-encrypted `ListResponse<SendResponse>`, which
|
||||
* the SDK cannot produce. `SendApiServiceSelector` routes calls to `SendApiService`;
|
||||
* this stub catches direct callers that bypass the selector.
|
||||
*/
|
||||
getSends(): Promise<ListResponse<SendResponse>> {
|
||||
return Promise.reject(new Error("SendSdkApiService.getSends: use SendApiService."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported via SDK — returns wire-encrypted `SendResponse`, which the SDK cannot
|
||||
* produce. `SendApiServiceSelector` routes calls to `SendApiService`; this stub catches
|
||||
* direct callers that bypass the selector.
|
||||
*/
|
||||
putSendRemovePassword(_id: string): Promise<SendResponse> {
|
||||
return Promise.reject(
|
||||
new Error("SendSdkApiService.putSendRemovePassword: use SendApiService."),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteSend(id: string): Promise<any> {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
return firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
return await ref.value.sends().delete(asUuid<SdkSendId>(id));
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to delete send: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// `apiUrl` is intentionally omitted; `SendApiServiceSelector` routes per-call `apiUrl`
|
||||
// to the legacy service.
|
||||
async getSendFileDownloadData(
|
||||
send: SendAccessView,
|
||||
request: SendAccessRequest,
|
||||
): Promise<SendFileDownloadDataResponse> {
|
||||
const sdk: PasswordManagerClient = await firstValueFrom(this.sdkService.client$);
|
||||
const data = await sdk
|
||||
.sends()
|
||||
.get_file_download_data_v1(send.id, send.file.id, request.password ?? undefined);
|
||||
return new SendFileDownloadDataResponse(data);
|
||||
}
|
||||
|
||||
// `apiUrl` is intentionally omitted; `SendApiServiceSelector` routes per-call `apiUrl`
|
||||
// to the legacy service.
|
||||
async getSendFileDownloadDataV2(
|
||||
send: SendAccessView,
|
||||
accessToken: SendAccessToken,
|
||||
): Promise<SendFileDownloadDataResponse> {
|
||||
const sdk: PasswordManagerClient = await firstValueFrom(this.sdkService.client$);
|
||||
const data = await sdk.sends().get_file_download_data(accessToken.token, send.file.id);
|
||||
return new SendFileDownloadDataResponse(data);
|
||||
}
|
||||
|
||||
private async mutateSend(sendView: SendView, userId: UserId): Promise<SdkSendView> {
|
||||
return await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
const sendsClient = ref.value.sends();
|
||||
if (sendView.id == null) {
|
||||
return await sendsClient.create(this.buildSendAddRequest(sendView));
|
||||
}
|
||||
return await sendsClient.edit(
|
||||
asUuid<SdkSendId>(sendView.id),
|
||||
this.buildSendEditRequest(sendView),
|
||||
);
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to upload send: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// After the SDK executes a mutation server-side, refetch the wire-encrypted form via
|
||||
// the legacy API so InternalSendService stores EncString-shaped data and consumers
|
||||
// that decrypt the returned Send work correctly.
|
||||
private async refreshSendFromServer(id: string): Promise<Send> {
|
||||
const response = await this.legacySendApiService.getSend(id);
|
||||
const data = new SendData(response);
|
||||
await this.sendService.upsert(data);
|
||||
return new Send(data);
|
||||
}
|
||||
|
||||
private buildSendAddRequest(sendView: SendView): SendAddRequest {
|
||||
return {
|
||||
name: sendView.name,
|
||||
notes: sendView.notes ?? undefined,
|
||||
viewType: this.buildSendViewType(sendView),
|
||||
maxAccessCount: sendView.maxAccessCount ?? undefined,
|
||||
disabled: sendView.disabled,
|
||||
hideEmail: sendView.hideEmail,
|
||||
deletionDate: this.requireDeletionDate(sendView).toISOString(),
|
||||
expirationDate: sendView.expirationDate?.toISOString() ?? undefined,
|
||||
auth: this.buildSendAuth(sendView),
|
||||
};
|
||||
}
|
||||
|
||||
private buildSendEditRequest(sendView: SendView): SendEditRequest {
|
||||
return {
|
||||
name: sendView.name,
|
||||
notes: sendView.notes ?? undefined,
|
||||
viewType: this.buildSendViewType(sendView),
|
||||
maxAccessCount: sendView.maxAccessCount ?? undefined,
|
||||
disabled: sendView.disabled,
|
||||
hideEmail: sendView.hideEmail,
|
||||
deletionDate: this.requireDeletionDate(sendView).toISOString(),
|
||||
expirationDate: sendView.expirationDate?.toISOString() ?? undefined,
|
||||
auth: this.buildSendAuth(sendView),
|
||||
};
|
||||
}
|
||||
|
||||
// SendView.deletionDate is typed as `Date` but declared with a `null` default, and
|
||||
// the file is `@ts-strict-ignore`. The SDK's SendAddRequest/SendEditRequest require a
|
||||
// non-null `DateTime<Utc>`. Fail fast at this boundary so a missing date surfaces as
|
||||
// a clear error rather than a confusing `.toISOString()` TypeError or a server-side
|
||||
// validation failure.
|
||||
private requireDeletionDate(sendView: SendView): Date {
|
||||
if (sendView.deletionDate == null) {
|
||||
throw new Error("Send is missing a deletion date.");
|
||||
}
|
||||
return sendView.deletionDate;
|
||||
}
|
||||
|
||||
private buildSendViewType(sendView: SendView): SendViewType {
|
||||
if (sendView.type === SendType.File) {
|
||||
if (sendView.file == null || !sendView.file.fileName) {
|
||||
throw new Error("File send is missing a file name.");
|
||||
}
|
||||
return {
|
||||
File: {
|
||||
id: sendView.file.id ?? undefined,
|
||||
fileName: sendView.file.fileName,
|
||||
size: sendView.file.size?.toString() ?? undefined,
|
||||
sizeName: sendView.file.sizeName ?? undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
Text: {
|
||||
text: sendView.text?.text ?? undefined,
|
||||
hidden: sendView.text?.hidden ?? false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// `AuthType.Password` is unreachable here: SendApiServiceSelector and the guard in
|
||||
// `save()` both route password-protected sends to the legacy service because the SDK
|
||||
// re-applies PBKDF2 to `auth.password`. When the SDK gains a skip-hash/preserve
|
||||
// variant, the routing changes and a `case AuthType.Password` branch comes back.
|
||||
private buildSendAuth(sendView: SendView): SendAuthType {
|
||||
switch (sendView.authType) {
|
||||
case AuthType.Email:
|
||||
if (sendView.emails == null || sendView.emails.length === 0) {
|
||||
throw new Error("Email-protected send is missing recipient emails.");
|
||||
}
|
||||
return { type: "emails", emails: sendView.emails };
|
||||
case AuthType.None:
|
||||
default:
|
||||
return { type: "none" };
|
||||
}
|
||||
}
|
||||
}
|
||||
78
package-lock.json
generated
78
package-lock.json
generated
@ -23,9 +23,9 @@
|
||||
"@angular/platform-browser": "21.2.11",
|
||||
"@angular/platform-browser-dynamic": "21.2.11",
|
||||
"@angular/router": "21.2.11",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.798",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.803",
|
||||
"@bitwarden/desktop-napi": "file:apps/desktop/desktop_native/napi",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.798",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.803",
|
||||
"@electron/fuses": "2.1.1",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@koa/multer": "4.0.0",
|
||||
@ -4067,9 +4067,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@bitwarden/commercial-sdk-internal": {
|
||||
"version": "0.2.0-main.798",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.798.tgz",
|
||||
"integrity": "sha512-Wdo5Y6z0LemYd32qsxnhqc448fVg0OQXbV/lfgL68GpU3cv+0G9fDijHy8KXlhegCtodCIg22uS7t0MRp6libw==",
|
||||
"version": "0.2.0-main.803",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.803.tgz",
|
||||
"integrity": "sha512-DE+CvtCUXlR25gVwSByiTFuYkfSC2OIS4KL3Uvelu3A5l3o7Lf2xa3ZIrw1VWVcxns5HA2o79jKdzqyuO9dIyw==",
|
||||
"license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT",
|
||||
"dependencies": {
|
||||
"type-fest": "^5.0.0"
|
||||
@ -4187,9 +4187,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@bitwarden/sdk-internal": {
|
||||
"version": "0.2.0-main.798",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.798.tgz",
|
||||
"integrity": "sha512-1lNWgkjX/gw4KBm+XFf0a0QTOaXm2QPO5Ti4KfQlF2oYEnehWgoPjYAMEzbCM3650LEWUPfriVC3Z/Tofj2vZw==",
|
||||
"version": "0.2.0-main.803",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.803.tgz",
|
||||
"integrity": "sha512-hftVgjFgdKkPKhnKFMgQ+6OLRVOX60qAiHdgk8qQccUfHFT44Dhr/B0tbWr+V6LxOtJ+q1Htm4iWhJO57Th4jw==",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"type-fest": "^5.0.0"
|
||||
@ -5517,28 +5517,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/windows-sign": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz",
|
||||
"integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cross-dirname": "^0.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"fs-extra": "^11.1.1",
|
||||
"minimist": "^1.2.8",
|
||||
"postject": "^1.0.0-alpha.6"
|
||||
},
|
||||
"bin": {
|
||||
"electron-windows-sign": "bin/electron-windows-sign.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
@ -20192,15 +20170,6 @@
|
||||
"integrity": "sha512-vt/iQokU0mtrT7ceRU75FSmWnIh5JFpLsUUUWYRmztYekOGm0ZbCuzwFTbNkq41k92y+0B8ChscFhRN9DhVZEA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-dirname": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz",
|
||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
||||
@ -35324,36 +35293,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postject": {
|
||||
"version": "1.0.0-alpha.6",
|
||||
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
|
||||
"integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^9.4.0"
|
||||
},
|
||||
"bin": {
|
||||
"postject": "dist/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postject/node_modules/commander": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/powershell-utils": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
|
||||
@ -39365,6 +39304,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
|
||||
@ -174,9 +174,9 @@
|
||||
"@angular/platform-browser": "21.2.11",
|
||||
"@angular/platform-browser-dynamic": "21.2.11",
|
||||
"@angular/router": "21.2.11",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.798",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.803",
|
||||
"@bitwarden/desktop-napi": "file:apps/desktop/desktop_native/napi",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.798",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.803",
|
||||
"@electron/fuses": "2.1.1",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@koa/multer": "4.0.0",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user