From 906ac951752abb7e3c6127467b1685afcd613419 Mon Sep 17 00:00:00 2001
From: Alex <55413326+AlexRubik@users.noreply.github.com>
Date: Mon, 3 Nov 2025 17:03:11 -0500
Subject: [PATCH 01/11] [PM-26908] feature flag for empty state component
(#17179)
---
.../risk-insights.component.html | 65 ++++++++++---------
1 file changed, 33 insertions(+), 32 deletions(-)
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html
index b7d05c73768..15ccd3241e4 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html
@@ -6,7 +6,8 @@
} @else {
- @if (!(dataService.hasReportData$ | async)) {
+ @if (isRiskInsightsActivityTabFeatureEnabled && !(dataService.hasReportData$ | async)) {
+
{{ "riskInsights" | i18n }}
0">
{{ "reviewAtRiskPasswords" | i18n }}
- @if (dataLastUpdated) {
-
-
+ @let isRunningReport = dataService.isGeneratingReport$ | async;
+
+
+ @if (dataLastUpdated) {
{{
"dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
}}
- @let isRunningReport = dataService.isGeneratingReport$ | async;
-
-
-
-
-
+ }
+
+
+
+
-
- }
+
+
From 5c33b2dc89e823be29f9513c8b6f7008cc177704 Mon Sep 17 00:00:00 2001
From: rr-bw <102181210+rr-bw@users.noreply.github.com>
Date: Mon, 3 Nov 2025 14:42:21 -0800
Subject: [PATCH 02/11] (Billing) [PM-27562] Create PremiumInterestStateService
(#17139)
Creates a `PremiumInterestStateService` that manages state which conveys whether or not a user intends to setup a premium subscription. Implemented in Web only. No-op for other clients.
This will apply for users who began the registration process on https://bitwarden.com/go/start-premium/, which is a marketing page designed to streamline users who intend to setup a premium subscription after registration.
---
...web-premium-interest-state.service.spec.ts | 147 ++++++++++++++++++
.../web-premium-interest-state.service.ts | 44 ++++++
apps/web/src/app/core/core.module.ts | 7 +
.../noop-premium-interest-state.service.ts | 14 ++
...mium-interest-state.service.abstraction.ts | 14 ++
.../src/services/jslib-services.module.ts | 7 +
libs/state/src/core/state-definitions.ts | 1 +
7 files changed, 234 insertions(+)
create mode 100644 apps/web/src/app/billing/services/premium-interest/web-premium-interest-state.service.spec.ts
create mode 100644 apps/web/src/app/billing/services/premium-interest/web-premium-interest-state.service.ts
create mode 100644 libs/angular/src/billing/services/premium-interest/noop-premium-interest-state.service.ts
create mode 100644 libs/angular/src/billing/services/premium-interest/premium-interest-state.service.abstraction.ts
diff --git a/apps/web/src/app/billing/services/premium-interest/web-premium-interest-state.service.spec.ts b/apps/web/src/app/billing/services/premium-interest/web-premium-interest-state.service.spec.ts
new file mode 100644
index 00000000000..086c7504040
--- /dev/null
+++ b/apps/web/src/app/billing/services/premium-interest/web-premium-interest-state.service.spec.ts
@@ -0,0 +1,147 @@
+import { firstValueFrom } from "rxjs";
+
+import {
+ FakeAccountService,
+ FakeStateProvider,
+ mockAccountServiceWith,
+} from "@bitwarden/common/spec";
+import { newGuid } from "@bitwarden/guid";
+import { UserId } from "@bitwarden/user-core";
+
+import {
+ PREMIUM_INTEREST_KEY,
+ WebPremiumInterestStateService,
+} from "./web-premium-interest-state.service";
+
+describe("WebPremiumInterestStateService", () => {
+ let service: WebPremiumInterestStateService;
+ let stateProvider: FakeStateProvider;
+ let accountService: FakeAccountService;
+
+ const mockUserId = newGuid() as UserId;
+ const mockUserEmail = "user@example.com";
+
+ beforeEach(() => {
+ accountService = mockAccountServiceWith(mockUserId, { email: mockUserEmail });
+ stateProvider = new FakeStateProvider(accountService);
+ service = new WebPremiumInterestStateService(stateProvider);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("getPremiumInterest", () => {
+ it("should throw an error when userId is not provided", async () => {
+ const promise = service.getPremiumInterest(null);
+
+ await expect(promise).rejects.toThrow("UserId is required. Cannot get 'premiumInterest'.");
+ });
+
+ it("should return null when no value is set", async () => {
+ const result = await service.getPremiumInterest(mockUserId);
+
+ expect(result).toBeNull();
+ });
+
+ it("should return true when value is set to true", async () => {
+ await stateProvider.setUserState(PREMIUM_INTEREST_KEY, true, mockUserId);
+
+ const result = await service.getPremiumInterest(mockUserId);
+
+ expect(result).toBe(true);
+ });
+
+ it("should return false when value is set to false", async () => {
+ await stateProvider.setUserState(PREMIUM_INTEREST_KEY, false, mockUserId);
+
+ const result = await service.getPremiumInterest(mockUserId);
+
+ expect(result).toBe(false);
+ });
+
+ it("should use getUserState$ to retrieve the value", async () => {
+ const getUserStateSpy = jest.spyOn(stateProvider, "getUserState$");
+ await stateProvider.setUserState(PREMIUM_INTEREST_KEY, true, mockUserId);
+
+ await service.getPremiumInterest(mockUserId);
+
+ expect(getUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, mockUserId);
+ });
+ });
+
+ describe("setPremiumInterest", () => {
+ it("should throw an error when userId is not provided", async () => {
+ const promise = service.setPremiumInterest(null, true);
+
+ await expect(promise).rejects.toThrow("UserId is required. Cannot set 'premiumInterest'.");
+ });
+
+ it("should set the value to true", async () => {
+ await service.setPremiumInterest(mockUserId, true);
+
+ const result = await firstValueFrom(
+ stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
+ );
+
+ expect(result).toBe(true);
+ });
+
+ it("should set the value to false", async () => {
+ await service.setPremiumInterest(mockUserId, false);
+
+ const result = await firstValueFrom(
+ stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
+ );
+
+ expect(result).toBe(false);
+ });
+
+ it("should update an existing value", async () => {
+ await service.setPremiumInterest(mockUserId, true);
+ await service.setPremiumInterest(mockUserId, false);
+
+ const result = await firstValueFrom(
+ stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
+ );
+
+ expect(result).toBe(false);
+ });
+
+ it("should use setUserState to store the value", async () => {
+ const setUserStateSpy = jest.spyOn(stateProvider, "setUserState");
+
+ await service.setPremiumInterest(mockUserId, true);
+
+ expect(setUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, true, mockUserId);
+ });
+ });
+
+ describe("clearPremiumInterest", () => {
+ it("should throw an error when userId is not provided", async () => {
+ const promise = service.clearPremiumInterest(null);
+
+ await expect(promise).rejects.toThrow("UserId is required. Cannot clear 'premiumInterest'.");
+ });
+
+ it("should clear the value by setting it to null", async () => {
+ await service.setPremiumInterest(mockUserId, true);
+ await service.clearPremiumInterest(mockUserId);
+
+ const result = await firstValueFrom(
+ stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
+ );
+
+ expect(result).toBeNull();
+ });
+
+ it("should use setUserState with null to clear the value", async () => {
+ const setUserStateSpy = jest.spyOn(stateProvider, "setUserState");
+ await service.setPremiumInterest(mockUserId, true);
+
+ await service.clearPremiumInterest(mockUserId);
+
+ expect(setUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, null, mockUserId);
+ });
+ });
+});
diff --git a/apps/web/src/app/billing/services/premium-interest/web-premium-interest-state.service.ts b/apps/web/src/app/billing/services/premium-interest/web-premium-interest-state.service.ts
new file mode 100644
index 00000000000..f66fba559f4
--- /dev/null
+++ b/apps/web/src/app/billing/services/premium-interest/web-premium-interest-state.service.ts
@@ -0,0 +1,44 @@
+import { Injectable } from "@angular/core";
+import { firstValueFrom } from "rxjs";
+
+import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction";
+import { BILLING_MEMORY, StateProvider, UserKeyDefinition } from "@bitwarden/state";
+import { UserId } from "@bitwarden/user-core";
+
+export const PREMIUM_INTEREST_KEY = new UserKeyDefinition(
+ BILLING_MEMORY,
+ "premiumInterest",
+ {
+ deserializer: (value: boolean) => value,
+ clearOn: ["lock", "logout"],
+ },
+);
+
+@Injectable()
+export class WebPremiumInterestStateService implements PremiumInterestStateService {
+ constructor(private stateProvider: StateProvider) {}
+
+ async getPremiumInterest(userId: UserId): Promise {
+ if (!userId) {
+ throw new Error("UserId is required. Cannot get 'premiumInterest'.");
+ }
+
+ return await firstValueFrom(this.stateProvider.getUserState$(PREMIUM_INTEREST_KEY, userId));
+ }
+
+ async setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise {
+ if (!userId) {
+ throw new Error("UserId is required. Cannot set 'premiumInterest'.");
+ }
+
+ await this.stateProvider.setUserState(PREMIUM_INTEREST_KEY, premiumInterest, userId);
+ }
+
+ async clearPremiumInterest(userId: UserId): Promise {
+ if (!userId) {
+ throw new Error("UserId is required. Cannot clear 'premiumInterest'.");
+ }
+
+ await this.stateProvider.setUserState(PREMIUM_INTEREST_KEY, null, userId);
+ }
+}
diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts
index 9619c3e23bf..72117f547d4 100644
--- a/apps/web/src/app/core/core.module.ts
+++ b/apps/web/src/app/core/core.module.ts
@@ -14,6 +14,7 @@ import { DefaultDeviceManagementComponentService } from "@bitwarden/angular/auth
import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction";
import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password";
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
+import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
CLIENT_TYPE,
@@ -129,6 +130,7 @@ import {
WebSetInitialPasswordService,
} from "../auth";
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
+import { WebPremiumInterestStateService } from "../billing/services/premium-interest/web-premium-interest-state.service";
import { HtmlStorageService } from "../core/html-storage.service";
import { I18nService } from "../core/i18n.service";
import { WebFileDownloadService } from "../core/web-file-download.service";
@@ -421,6 +423,11 @@ const safeProviders: SafeProvider[] = [
Router,
],
}),
+ safeProvider({
+ provide: PremiumInterestStateService,
+ useClass: WebPremiumInterestStateService,
+ deps: [StateProvider],
+ }),
];
@NgModule({
diff --git a/libs/angular/src/billing/services/premium-interest/noop-premium-interest-state.service.ts b/libs/angular/src/billing/services/premium-interest/noop-premium-interest-state.service.ts
new file mode 100644
index 00000000000..f941e86e0d0
--- /dev/null
+++ b/libs/angular/src/billing/services/premium-interest/noop-premium-interest-state.service.ts
@@ -0,0 +1,14 @@
+import { Injectable } from "@angular/core";
+
+import { UserId } from "@bitwarden/user-core";
+
+import { PremiumInterestStateService } from "./premium-interest-state.service.abstraction";
+
+@Injectable()
+export class NoopPremiumInterestStateService implements PremiumInterestStateService {
+ async getPremiumInterest(userId: UserId): Promise {
+ return null;
+ } // no-op
+ async setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise {} // no-op
+ async clearPremiumInterest(userId: UserId): Promise {} // no-op
+}
diff --git a/libs/angular/src/billing/services/premium-interest/premium-interest-state.service.abstraction.ts b/libs/angular/src/billing/services/premium-interest/premium-interest-state.service.abstraction.ts
new file mode 100644
index 00000000000..850560df38c
--- /dev/null
+++ b/libs/angular/src/billing/services/premium-interest/premium-interest-state.service.abstraction.ts
@@ -0,0 +1,14 @@
+import { UserId } from "@bitwarden/user-core";
+
+/**
+ * A service that manages state which conveys whether or not a user has expressed interest
+ * in setting up a premium subscription. This applies for users who began the registration
+ * process on https://bitwarden.com/go/start-premium/, which is a marketing page designed
+ * to streamline users who intend to setup a premium subscription after registration.
+ * - Implemented in Web only. No-op for other clients.
+ */
+export abstract class PremiumInterestStateService {
+ abstract getPremiumInterest(userId: UserId): Promise;
+ abstract setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise;
+ abstract clearPremiumInterest(userId: UserId): Promise;
+}
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index fc9c5b8b15c..38ce3c0fcc2 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -380,6 +380,8 @@ import { DefaultSetInitialPasswordService } from "../auth/password-management/se
import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction";
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
+import { NoopPremiumInterestStateService } from "../billing/services/premium-interest/noop-premium-interest-state.service";
+import { PremiumInterestStateService } from "../billing/services/premium-interest/premium-interest-state.service.abstraction";
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
import { DocumentLangSetter } from "../platform/i18n";
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
@@ -1724,6 +1726,11 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultNewDeviceVerificationComponentService,
deps: [],
}),
+ safeProvider({
+ provide: PremiumInterestStateService,
+ useClass: NoopPremiumInterestStateService,
+ deps: [],
+ }),
];
@NgModule({
diff --git a/libs/state/src/core/state-definitions.ts b/libs/state/src/core/state-definitions.ts
index 782dafe1ee2..42d7f5aaaf8 100644
--- a/libs/state/src/core/state-definitions.ts
+++ b/libs/state/src/core/state-definitions.ts
@@ -40,6 +40,7 @@ export const AUTO_CONFIRM = new StateDefinition("autoConfirm", "disk");
// Billing
export const BILLING_DISK = new StateDefinition("billing", "disk");
+export const BILLING_MEMORY = new StateDefinition("billing", "memory");
// Auth
From 0b65442d5e9b77aa35acca8fce5c59ae96961ed1 Mon Sep 17 00:00:00 2001
From: Matt Andreko
Date: Mon, 3 Nov 2025 17:54:26 -0500
Subject: [PATCH 03/11] :wrench: update cargo-deny version in ci (#17174)
---
.github/workflows/lint.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 21786339299..c14abd7cd86 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -119,9 +119,9 @@ jobs:
run: cargo sort --workspace --check
- name: Install cargo-deny
- uses: taiki-e/install-action@v2
+ uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
with:
- tool: cargo-deny
+ tool: cargo-deny@0.18.5
- name: Run cargo deny
working-directory: ./apps/desktop/desktop_native
From 86b213aa8e4489a149f4541e8f8826d00cb253e1 Mon Sep 17 00:00:00 2001
From: Mick Letofsky
Date: Tue, 4 Nov 2025 10:08:18 +0100
Subject: [PATCH 04/11] [PM-25820] - Migrate cipher data model to adhere to
ts-strict (#17073)
* Migrate cipher data model to adhere to ts-strict & added unit tests where applicable
---
.../src/vault/models/data/attachment.data.ts | 14 ++-
.../common/src/vault/models/data/card.data.ts | 14 ++-
.../src/vault/models/data/cipher.data.ts | 37 ++++----
.../models/data/fido2-credential.data.ts | 28 +++---
.../src/vault/models/data/field.data.ts | 10 +-
.../src/vault/models/data/identity.data.ts | 38 ++++----
.../src/vault/models/data/login-uri.data.ts | 8 +-
.../src/vault/models/data/login.data.ts | 14 ++-
.../models/data/password-history.data.ts | 6 +-
.../src/vault/models/data/secure-note.data.ts | 4 +-
.../src/vault/models/data/ssh-key.data.ts | 8 +-
.../vault/models/domain/attachment.spec.ts | 6 ++
.../src/vault/models/domain/card.spec.ts | 7 ++
.../src/vault/models/domain/cipher.spec.ts | 46 ++++++++--
.../src/vault/models/domain/field.spec.ts | 2 +-
.../src/vault/models/domain/identity.spec.ts | 21 +++++
.../src/vault/models/domain/login-uri.spec.ts | 23 ++++-
.../src/vault/models/domain/login.spec.ts | 8 ++
libs/common/src/vault/models/domain/login.ts | 5 +-
.../src/vault/models/domain/password.spec.ts | 46 ++++++++++
.../vault/models/domain/secure-note.spec.ts | 92 +++++++++++++++++--
.../src/vault/models/domain/ssh-key.spec.ts | 73 +++++++++++++++
22 files changed, 391 insertions(+), 119 deletions(-)
diff --git a/libs/common/src/vault/models/data/attachment.data.ts b/libs/common/src/vault/models/data/attachment.data.ts
index dfc9f9d1afa..dde5db24b57 100644
--- a/libs/common/src/vault/models/data/attachment.data.ts
+++ b/libs/common/src/vault/models/data/attachment.data.ts
@@ -1,14 +1,12 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { AttachmentResponse } from "../response/attachment.response";
export class AttachmentData {
- id: string;
- url: string;
- fileName: string;
- key: string;
- size: string;
- sizeName: string;
+ id?: string;
+ url?: string;
+ fileName?: string;
+ key?: string;
+ size?: string;
+ sizeName?: string;
constructor(response?: AttachmentResponse) {
if (response == null) {
diff --git a/libs/common/src/vault/models/data/card.data.ts b/libs/common/src/vault/models/data/card.data.ts
index 677c33f4886..8345c345fd7 100644
--- a/libs/common/src/vault/models/data/card.data.ts
+++ b/libs/common/src/vault/models/data/card.data.ts
@@ -1,14 +1,12 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { CardApi } from "../api/card.api";
export class CardData {
- cardholderName: string;
- brand: string;
- number: string;
- expMonth: string;
- expYear: string;
- code: string;
+ cardholderName?: string;
+ brand?: string;
+ number?: string;
+ expMonth?: string;
+ expYear?: string;
+ code?: string;
constructor(data?: CardApi) {
if (data == null) {
diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts
index 4921cce8df2..743ea941b0e 100644
--- a/libs/common/src/vault/models/data/cipher.data.ts
+++ b/libs/common/src/vault/models/data/cipher.data.ts
@@ -1,5 +1,3 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
@@ -17,18 +15,18 @@ import { SecureNoteData } from "./secure-note.data";
import { SshKeyData } from "./ssh-key.data";
export class CipherData {
- id: string;
- organizationId: string;
- folderId: string;
- edit: boolean;
- viewPassword: boolean;
- permissions: CipherPermissionsApi;
- organizationUseTotp: boolean;
- favorite: boolean;
+ id: string = "";
+ organizationId?: string;
+ folderId?: string;
+ edit: boolean = false;
+ viewPassword: boolean = true;
+ permissions?: CipherPermissionsApi;
+ organizationUseTotp: boolean = false;
+ favorite: boolean = false;
revisionDate: string;
- type: CipherType;
- name: string;
- notes: string;
+ type: CipherType = CipherType.Login;
+ name: string = "";
+ notes?: string;
login?: LoginData;
secureNote?: SecureNoteData;
card?: CardData;
@@ -39,13 +37,14 @@ export class CipherData {
passwordHistory?: PasswordHistoryData[];
collectionIds?: string[];
creationDate: string;
- deletedDate: string | undefined;
- archivedDate: string | undefined;
- reprompt: CipherRepromptType;
- key: string;
+ deletedDate?: string;
+ archivedDate?: string;
+ reprompt: CipherRepromptType = CipherRepromptType.None;
+ key?: string;
constructor(response?: CipherResponse, collectionIds?: string[]) {
if (response == null) {
+ this.creationDate = this.revisionDate = new Date().toISOString();
return;
}
@@ -101,7 +100,9 @@ export class CipherData {
static fromJSON(obj: Jsonify) {
const result = Object.assign(new CipherData(), obj);
- result.permissions = CipherPermissionsApi.fromJSON(obj.permissions);
+ if (obj.permissions != null) {
+ result.permissions = CipherPermissionsApi.fromJSON(obj.permissions);
+ }
return result;
}
}
diff --git a/libs/common/src/vault/models/data/fido2-credential.data.ts b/libs/common/src/vault/models/data/fido2-credential.data.ts
index 94716e8d86c..602b74f9805 100644
--- a/libs/common/src/vault/models/data/fido2-credential.data.ts
+++ b/libs/common/src/vault/models/data/fido2-credential.data.ts
@@ -1,21 +1,19 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { Fido2CredentialApi } from "../api/fido2-credential.api";
export class Fido2CredentialData {
- credentialId: string;
- keyType: "public-key";
- keyAlgorithm: "ECDSA";
- keyCurve: "P-256";
- keyValue: string;
- rpId: string;
- userHandle: string;
- userName: string;
- counter: string;
- rpName: string;
- userDisplayName: string;
- discoverable: string;
- creationDate: string;
+ credentialId!: string;
+ keyType!: string;
+ keyAlgorithm!: string;
+ keyCurve!: string;
+ keyValue!: string;
+ rpId!: string;
+ userHandle?: string;
+ userName?: string;
+ counter!: string;
+ rpName?: string;
+ userDisplayName?: string;
+ discoverable!: string;
+ creationDate!: string;
constructor(data?: Fido2CredentialApi) {
if (data == null) {
diff --git a/libs/common/src/vault/models/data/field.data.ts b/libs/common/src/vault/models/data/field.data.ts
index cf9df69a6b0..a63e903e665 100644
--- a/libs/common/src/vault/models/data/field.data.ts
+++ b/libs/common/src/vault/models/data/field.data.ts
@@ -1,13 +1,11 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { FieldType, LinkedIdType } from "../../enums";
import { FieldApi } from "../api/field.api";
export class FieldData {
- type: FieldType;
- name: string;
- value: string;
- linkedId: LinkedIdType | null;
+ type: FieldType = FieldType.Text;
+ name?: string;
+ value?: string;
+ linkedId?: LinkedIdType;
constructor(response?: FieldApi) {
if (response == null) {
diff --git a/libs/common/src/vault/models/data/identity.data.ts b/libs/common/src/vault/models/data/identity.data.ts
index 158daace371..de854df1ec6 100644
--- a/libs/common/src/vault/models/data/identity.data.ts
+++ b/libs/common/src/vault/models/data/identity.data.ts
@@ -1,26 +1,24 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { IdentityApi } from "../api/identity.api";
export class IdentityData {
- title: string;
- firstName: string;
- middleName: string;
- lastName: string;
- address1: string;
- address2: string;
- address3: string;
- city: string;
- state: string;
- postalCode: string;
- country: string;
- company: string;
- email: string;
- phone: string;
- ssn: string;
- username: string;
- passportNumber: string;
- licenseNumber: string;
+ title?: string;
+ firstName?: string;
+ middleName?: string;
+ lastName?: string;
+ address1?: string;
+ address2?: string;
+ address3?: string;
+ city?: string;
+ state?: string;
+ postalCode?: string;
+ country?: string;
+ company?: string;
+ email?: string;
+ phone?: string;
+ ssn?: string;
+ username?: string;
+ passportNumber?: string;
+ licenseNumber?: string;
constructor(data?: IdentityApi) {
if (data == null) {
diff --git a/libs/common/src/vault/models/data/login-uri.data.ts b/libs/common/src/vault/models/data/login-uri.data.ts
index 852dad4e112..ea3a1d9adce 100644
--- a/libs/common/src/vault/models/data/login-uri.data.ts
+++ b/libs/common/src/vault/models/data/login-uri.data.ts
@@ -1,12 +1,10 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { LoginUriApi } from "../api/login-uri.api";
export class LoginUriData {
- uri: string;
- uriChecksum: string;
- match: UriMatchStrategySetting = null;
+ uri?: string;
+ uriChecksum?: string;
+ match?: UriMatchStrategySetting;
constructor(data?: LoginUriApi) {
if (data == null) {
diff --git a/libs/common/src/vault/models/data/login.data.ts b/libs/common/src/vault/models/data/login.data.ts
index 0fe021d923c..8c0aba0fdaa 100644
--- a/libs/common/src/vault/models/data/login.data.ts
+++ b/libs/common/src/vault/models/data/login.data.ts
@@ -1,17 +1,15 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { LoginApi } from "../api/login.api";
import { Fido2CredentialData } from "./fido2-credential.data";
import { LoginUriData } from "./login-uri.data";
export class LoginData {
- uris: LoginUriData[];
- username: string;
- password: string;
- passwordRevisionDate: string;
- totp: string;
- autofillOnPageLoad: boolean;
+ uris?: LoginUriData[];
+ username?: string;
+ password?: string;
+ passwordRevisionDate?: string;
+ totp?: string;
+ autofillOnPageLoad?: boolean;
fido2Credentials?: Fido2CredentialData[];
constructor(data?: LoginApi) {
diff --git a/libs/common/src/vault/models/data/password-history.data.ts b/libs/common/src/vault/models/data/password-history.data.ts
index 75a51ed3728..465b5e59b8d 100644
--- a/libs/common/src/vault/models/data/password-history.data.ts
+++ b/libs/common/src/vault/models/data/password-history.data.ts
@@ -1,10 +1,8 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { PasswordHistoryResponse } from "../response/password-history.response";
export class PasswordHistoryData {
- password: string;
- lastUsedDate: string;
+ password!: string;
+ lastUsedDate!: string;
constructor(response?: PasswordHistoryResponse) {
if (response == null) {
diff --git a/libs/common/src/vault/models/data/secure-note.data.ts b/libs/common/src/vault/models/data/secure-note.data.ts
index 7d109398ab7..5556417ef9b 100644
--- a/libs/common/src/vault/models/data/secure-note.data.ts
+++ b/libs/common/src/vault/models/data/secure-note.data.ts
@@ -1,10 +1,8 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { SecureNoteType } from "../../enums";
import { SecureNoteApi } from "../api/secure-note.api";
export class SecureNoteData {
- type: SecureNoteType;
+ type: SecureNoteType = SecureNoteType.Generic;
constructor(data?: SecureNoteApi) {
if (data == null) {
diff --git a/libs/common/src/vault/models/data/ssh-key.data.ts b/libs/common/src/vault/models/data/ssh-key.data.ts
index 2b93c93d931..1e06a1a7df5 100644
--- a/libs/common/src/vault/models/data/ssh-key.data.ts
+++ b/libs/common/src/vault/models/data/ssh-key.data.ts
@@ -1,11 +1,9 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { SshKeyApi } from "../api/ssh-key.api";
export class SshKeyData {
- privateKey: string;
- publicKey: string;
- keyFingerprint: string;
+ privateKey!: string;
+ publicKey!: string;
+ keyFingerprint!: string;
constructor(data?: SshKeyApi) {
if (data == null) {
diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts
index 972c77537ff..77bb3eda38d 100644
--- a/libs/common/src/vault/models/domain/attachment.spec.ts
+++ b/libs/common/src/vault/models/domain/attachment.spec.ts
@@ -39,6 +39,12 @@ describe("Attachment", () => {
key: undefined,
fileName: undefined,
});
+ expect(data.id).toBeUndefined();
+ expect(data.url).toBeUndefined();
+ expect(data.fileName).toBeUndefined();
+ expect(data.key).toBeUndefined();
+ expect(data.size).toBeUndefined();
+ expect(data.sizeName).toBeUndefined();
});
it("Convert", () => {
diff --git a/libs/common/src/vault/models/domain/card.spec.ts b/libs/common/src/vault/models/domain/card.spec.ts
index a4d242329a4..185c2fa4b8f 100644
--- a/libs/common/src/vault/models/domain/card.spec.ts
+++ b/libs/common/src/vault/models/domain/card.spec.ts
@@ -29,6 +29,13 @@ describe("Card", () => {
expYear: undefined,
code: undefined,
});
+
+ expect(data.cardholderName).toBeUndefined();
+ expect(data.brand).toBeUndefined();
+ expect(data.number).toBeUndefined();
+ expect(data.expMonth).toBeUndefined();
+ expect(data.expYear).toBeUndefined();
+ expect(data.code).toBeUndefined();
});
it("Convert", () => {
diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts
index 4052c9e5338..7503d71573f 100644
--- a/libs/common/src/vault/models/domain/cipher.spec.ts
+++ b/libs/common/src/vault/models/domain/cipher.spec.ts
@@ -44,22 +44,22 @@ describe("Cipher DTO", () => {
const data = new CipherData();
const cipher = new Cipher(data);
- expect(cipher.id).toBeUndefined();
+ expect(cipher.id).toEqual("");
expect(cipher.organizationId).toBeUndefined();
expect(cipher.folderId).toBeUndefined();
expect(cipher.name).toBeInstanceOf(EncString);
expect(cipher.notes).toBeUndefined();
- expect(cipher.type).toBeUndefined();
- expect(cipher.favorite).toBeUndefined();
- expect(cipher.organizationUseTotp).toBeUndefined();
- expect(cipher.edit).toBeUndefined();
- expect(cipher.viewPassword).toBeUndefined();
+ expect(cipher.type).toEqual(CipherType.Login);
+ expect(cipher.favorite).toEqual(false);
+ expect(cipher.organizationUseTotp).toEqual(false);
+ expect(cipher.edit).toEqual(false);
+ expect(cipher.viewPassword).toEqual(true);
expect(cipher.revisionDate).toBeInstanceOf(Date);
expect(cipher.collectionIds).toEqual([]);
expect(cipher.localData).toBeUndefined();
expect(cipher.creationDate).toBeInstanceOf(Date);
expect(cipher.deletedDate).toBeUndefined();
- expect(cipher.reprompt).toBeUndefined();
+ expect(cipher.reprompt).toEqual(CipherRepromptType.None);
expect(cipher.attachments).toBeUndefined();
expect(cipher.fields).toBeUndefined();
expect(cipher.passwordHistory).toBeUndefined();
@@ -836,6 +836,38 @@ describe("Cipher DTO", () => {
expect(actual).toBeInstanceOf(Cipher);
});
+ it("handles null permissions correctly without calling CipherPermissionsApi constructor", () => {
+ const spy = jest.spyOn(CipherPermissionsApi.prototype, "constructor" as any);
+ const revisionDate = new Date("2022-08-04T01:06:40.441Z");
+ const actual = Cipher.fromJSON({
+ name: "myName",
+ revisionDate: revisionDate.toISOString(),
+ permissions: null,
+ } as Jsonify);
+
+ expect(actual.permissions).toBeUndefined();
+ expect(actual).toBeInstanceOf(Cipher);
+ // Verify that CipherPermissionsApi constructor was not called for null permissions
+ expect(spy).not.toHaveBeenCalledWith(null);
+ spy.mockRestore();
+ });
+
+ it("calls CipherPermissionsApi constructor when permissions are provided", () => {
+ const spy = jest.spyOn(CipherPermissionsApi.prototype, "constructor" as any);
+ const revisionDate = new Date("2022-08-04T01:06:40.441Z");
+ const permissionsObj = { delete: true, restore: false };
+ const actual = Cipher.fromJSON({
+ name: "myName",
+ revisionDate: revisionDate.toISOString(),
+ permissions: permissionsObj,
+ } as Jsonify);
+
+ expect(actual.permissions).toBeInstanceOf(CipherPermissionsApi);
+ expect(actual.permissions.delete).toBe(true);
+ expect(actual.permissions.restore).toBe(false);
+ spy.mockRestore();
+ });
+
test.each([
// Test description, CipherType, expected output
["LoginView", CipherType.Login, { login: "myLogin_fromJSON" }],
diff --git a/libs/common/src/vault/models/domain/field.spec.ts b/libs/common/src/vault/models/domain/field.spec.ts
index d99336adad0..0a4bc8e3c29 100644
--- a/libs/common/src/vault/models/domain/field.spec.ts
+++ b/libs/common/src/vault/models/domain/field.spec.ts
@@ -29,7 +29,7 @@ describe("Field", () => {
const field = new Field(data);
expect(field).toEqual({
- type: undefined,
+ type: FieldType.Text,
name: undefined,
value: undefined,
linkedId: undefined,
diff --git a/libs/common/src/vault/models/domain/identity.spec.ts b/libs/common/src/vault/models/domain/identity.spec.ts
index c2c2363fa0d..411f6d1c9ea 100644
--- a/libs/common/src/vault/models/domain/identity.spec.ts
+++ b/libs/common/src/vault/models/domain/identity.spec.ts
@@ -53,6 +53,27 @@ describe("Identity", () => {
title: undefined,
username: undefined,
});
+
+ expect(data).toEqual({
+ title: undefined,
+ firstName: undefined,
+ middleName: undefined,
+ lastName: undefined,
+ address1: undefined,
+ address2: undefined,
+ address3: undefined,
+ city: undefined,
+ state: undefined,
+ postalCode: undefined,
+ country: undefined,
+ company: undefined,
+ email: undefined,
+ phone: undefined,
+ ssn: undefined,
+ username: undefined,
+ passportNumber: undefined,
+ licenseNumber: undefined,
+ });
});
it("Convert", () => {
diff --git a/libs/common/src/vault/models/domain/login-uri.spec.ts b/libs/common/src/vault/models/domain/login-uri.spec.ts
index 982b435384b..2effd1bb9fe 100644
--- a/libs/common/src/vault/models/domain/login-uri.spec.ts
+++ b/libs/common/src/vault/models/domain/login-uri.spec.ts
@@ -7,6 +7,7 @@ import { mockEnc, mockFromJson } from "../../../../spec";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import { UriMatchStrategy } from "../../../models/domain/domain-service";
+import { LoginUriApi } from "../api/login-uri.api";
import { LoginUriData } from "../data/login-uri.data";
import { LoginUri } from "./login-uri";
@@ -31,6 +32,9 @@ describe("LoginUri", () => {
uri: undefined,
uriChecksum: undefined,
});
+ expect(data.uri).toBeUndefined();
+ expect(data.uriChecksum).toBeUndefined();
+ expect(data.match).toBeUndefined();
});
it("Convert", () => {
@@ -61,6 +65,23 @@ describe("LoginUri", () => {
});
});
+ it("handle null match", () => {
+ const apiData = Object.assign(new LoginUriApi(), {
+ uri: "testUri",
+ uriChecksum: "testChecksum",
+ match: null,
+ });
+
+ const loginUriData = new LoginUriData(apiData);
+
+ // The data model stores it as-is (null or undefined)
+ expect(loginUriData.match).toBeNull();
+
+ // But the domain model converts null to undefined
+ const loginUri = new LoginUri(loginUriData);
+ expect(loginUri.match).toBeUndefined();
+ });
+
describe("validateChecksum", () => {
let encryptService: MockProxy;
@@ -118,7 +139,7 @@ describe("LoginUri", () => {
});
describe("SDK Login Uri Mapping", () => {
- it("should map to SDK login uri", () => {
+ it("maps to SDK login uri", () => {
const loginUri = new LoginUri(data);
const sdkLoginUri = loginUri.toSdkLoginUri();
diff --git a/libs/common/src/vault/models/domain/login.spec.ts b/libs/common/src/vault/models/domain/login.spec.ts
index 9f03e225b7f..6ebcfea057a 100644
--- a/libs/common/src/vault/models/domain/login.spec.ts
+++ b/libs/common/src/vault/models/domain/login.spec.ts
@@ -25,6 +25,14 @@ describe("Login DTO", () => {
password: undefined,
totp: undefined,
});
+
+ expect(data.username).toBeUndefined();
+ expect(data.password).toBeUndefined();
+ expect(data.passwordRevisionDate).toBeUndefined();
+ expect(data.totp).toBeUndefined();
+ expect(data.autofillOnPageLoad).toBeUndefined();
+ expect(data.uris).toBeUndefined();
+ expect(data.fido2Credentials).toBeUndefined();
});
it("Convert from full LoginData", () => {
diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts
index 13342c69014..a9cec13fc7c 100644
--- a/libs/common/src/vault/models/domain/login.ts
+++ b/libs/common/src/vault/models/domain/login.ts
@@ -111,10 +111,7 @@ export class Login extends Domain {
});
if (this.uris != null && this.uris.length > 0) {
- l.uris = [];
- this.uris.forEach((u) => {
- l.uris.push(u.toLoginUriData());
- });
+ l.uris = this.uris.map((u) => u.toLoginUriData());
}
if (this.fido2Credentials != null && this.fido2Credentials.length > 0) {
diff --git a/libs/common/src/vault/models/domain/password.spec.ts b/libs/common/src/vault/models/domain/password.spec.ts
index 2e37c5e8375..4b2de34beca 100644
--- a/libs/common/src/vault/models/domain/password.spec.ts
+++ b/libs/common/src/vault/models/domain/password.spec.ts
@@ -20,6 +20,9 @@ describe("Password", () => {
expect(password).toBeInstanceOf(Password);
expect(password.password).toBeInstanceOf(EncString);
expect(password.lastUsedDate).toBeInstanceOf(Date);
+
+ expect(data.password).toBeUndefined();
+ expect(data.lastUsedDate).toBeUndefined();
});
it("Convert", () => {
@@ -83,4 +86,47 @@ describe("Password", () => {
});
});
});
+
+ describe("fromSdkPasswordHistory", () => {
+ beforeEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("creates Password from SDK object", () => {
+ const sdkPasswordHistory = {
+ password: "2.encPassword|encryptedData" as EncryptedString,
+ lastUsedDate: "2022-01-31T12:00:00.000Z",
+ };
+
+ const password = Password.fromSdkPasswordHistory(sdkPasswordHistory);
+
+ expect(password).toBeInstanceOf(Password);
+ expect(password?.password).toBeInstanceOf(EncString);
+ expect(password?.password.encryptedString).toBe("2.encPassword|encryptedData");
+ expect(password?.lastUsedDate).toEqual(new Date("2022-01-31T12:00:00.000Z"));
+ });
+
+ it("returns undefined for null input", () => {
+ const result = Password.fromSdkPasswordHistory(null as any);
+ expect(result).toBeUndefined();
+ });
+
+ it("returns undefined for undefined input", () => {
+ const result = Password.fromSdkPasswordHistory(undefined);
+ expect(result).toBeUndefined();
+ });
+
+ it("handles empty SDK object", () => {
+ const sdkPasswordHistory = {
+ password: "" as EncryptedString,
+ lastUsedDate: "",
+ };
+
+ const password = Password.fromSdkPasswordHistory(sdkPasswordHistory);
+
+ expect(password).toBeInstanceOf(Password);
+ expect(password?.password).toBeInstanceOf(EncString);
+ expect(password?.lastUsedDate).toBeInstanceOf(Date);
+ });
+ });
});
diff --git a/libs/common/src/vault/models/domain/secure-note.spec.ts b/libs/common/src/vault/models/domain/secure-note.spec.ts
index 4c8e8d470ca..e445e9ea035 100644
--- a/libs/common/src/vault/models/domain/secure-note.spec.ts
+++ b/libs/common/src/vault/models/domain/secure-note.spec.ts
@@ -16,22 +16,27 @@ describe("SecureNote", () => {
const data = new SecureNoteData();
const secureNote = new SecureNote(data);
- expect(secureNote).toEqual({
- type: undefined,
- });
+ expect(data).toBeDefined();
+ expect(secureNote).toEqual({ type: SecureNoteType.Generic });
+ expect(data.type).toBe(SecureNoteType.Generic);
+ });
+
+ it("Convert from undefined", () => {
+ const data = new SecureNoteData(undefined);
+ expect(data.type).toBe(SecureNoteType.Generic);
});
it("Convert", () => {
const secureNote = new SecureNote(data);
- expect(secureNote).toEqual({
- type: 0,
- });
+ expect(secureNote).toEqual({ type: 0 });
+ expect(data.type).toBe(SecureNoteType.Generic);
});
it("toSecureNoteData", () => {
const secureNote = new SecureNote(data);
expect(secureNote.toSecureNoteData()).toEqual(data);
+ expect(secureNote.toSecureNoteData().type).toBe(SecureNoteType.Generic);
});
it("Decrypt", async () => {
@@ -49,6 +54,14 @@ describe("SecureNote", () => {
it("returns undefined if object is null", () => {
expect(SecureNote.fromJSON(null)).toBeUndefined();
});
+
+ it("creates SecureNote instance from JSON object", () => {
+ const jsonObj = { type: SecureNoteType.Generic };
+ const result = SecureNote.fromJSON(jsonObj);
+
+ expect(result).toBeInstanceOf(SecureNote);
+ expect(result.type).toBe(SecureNoteType.Generic);
+ });
});
describe("toSdkSecureNote", () => {
@@ -63,4 +76,71 @@ describe("SecureNote", () => {
});
});
});
+
+ describe("fromSdkSecureNote", () => {
+ it("returns undefined when null is provided", () => {
+ const result = SecureNote.fromSdkSecureNote(null);
+
+ expect(result).toBeUndefined();
+ });
+
+ it("returns undefined when undefined is provided", () => {
+ const result = SecureNote.fromSdkSecureNote(undefined);
+
+ expect(result).toBeUndefined();
+ });
+
+ it("creates SecureNote with Generic type from SDK object", () => {
+ const sdkSecureNote = {
+ type: SecureNoteType.Generic,
+ };
+
+ const result = SecureNote.fromSdkSecureNote(sdkSecureNote);
+
+ expect(result).toBeInstanceOf(SecureNote);
+ expect(result.type).toBe(SecureNoteType.Generic);
+ });
+
+ it("preserves the type value from SDK object", () => {
+ const sdkSecureNote = {
+ type: SecureNoteType.Generic,
+ };
+
+ const result = SecureNote.fromSdkSecureNote(sdkSecureNote);
+
+ expect(result.type).toBe(0);
+ });
+
+ it("creates a new SecureNote instance", () => {
+ const sdkSecureNote = {
+ type: SecureNoteType.Generic,
+ };
+
+ const result = SecureNote.fromSdkSecureNote(sdkSecureNote);
+
+ expect(result).not.toBe(sdkSecureNote);
+ expect(result).toBeInstanceOf(SecureNote);
+ });
+
+ it("handles SDK object with undefined type", () => {
+ const sdkSecureNote = {
+ type: undefined as SecureNoteType,
+ };
+
+ const result = SecureNote.fromSdkSecureNote(sdkSecureNote);
+
+ expect(result).toBeInstanceOf(SecureNote);
+ expect(result.type).toBeUndefined();
+ });
+
+ it("returns symmetric with toSdkSecureNote", () => {
+ const original = new SecureNote();
+ original.type = SecureNoteType.Generic;
+
+ const sdkFormat = original.toSdkSecureNote();
+ const reconstructed = SecureNote.fromSdkSecureNote(sdkFormat);
+
+ expect(reconstructed.type).toBe(original.type);
+ });
+ });
});
diff --git a/libs/common/src/vault/models/domain/ssh-key.spec.ts b/libs/common/src/vault/models/domain/ssh-key.spec.ts
index 38228e54a4a..10149ebc82d 100644
--- a/libs/common/src/vault/models/domain/ssh-key.spec.ts
+++ b/libs/common/src/vault/models/domain/ssh-key.spec.ts
@@ -1,4 +1,5 @@
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
+import { EncString as SdkEncString, SshKey as SdkSshKey } from "@bitwarden/sdk-internal";
import { mockEnc } from "../../../../spec";
import { SshKeyApi } from "../api/ssh-key.api";
@@ -37,6 +38,9 @@ describe("Sshkey", () => {
expect(sshKey.privateKey).toBeInstanceOf(EncString);
expect(sshKey.publicKey).toBeInstanceOf(EncString);
expect(sshKey.keyFingerprint).toBeInstanceOf(EncString);
+ expect(data.privateKey).toBeUndefined();
+ expect(data.publicKey).toBeUndefined();
+ expect(data.keyFingerprint).toBeUndefined();
});
it("toSshKeyData", () => {
@@ -64,6 +68,21 @@ describe("Sshkey", () => {
it("returns undefined if object is null", () => {
expect(SshKey.fromJSON(null)).toBeUndefined();
});
+
+ it("creates SshKey instance from JSON object", () => {
+ const jsonObj = {
+ privateKey: "2.privateKey|encryptedData",
+ publicKey: "2.publicKey|encryptedData",
+ keyFingerprint: "2.keyFingerprint|encryptedData",
+ };
+
+ const result = SshKey.fromJSON(jsonObj);
+
+ expect(result).toBeInstanceOf(SshKey);
+ expect(result.privateKey).toBeDefined();
+ expect(result.publicKey).toBeDefined();
+ expect(result.keyFingerprint).toBeDefined();
+ });
});
describe("toSdkSshKey", () => {
@@ -78,4 +97,58 @@ describe("Sshkey", () => {
});
});
});
+
+ describe("fromSdkSshKey", () => {
+ it("returns undefined when null is provided", () => {
+ const result = SshKey.fromSdkSshKey(null);
+
+ expect(result).toBeUndefined();
+ });
+
+ it("returns undefined when undefined is provided", () => {
+ const result = SshKey.fromSdkSshKey(undefined);
+
+ expect(result).toBeUndefined();
+ });
+
+ it("creates SshKey from SDK object", () => {
+ const sdkSshKey: SdkSshKey = {
+ privateKey: "2.privateKey|encryptedData" as SdkEncString,
+ publicKey: "2.publicKey|encryptedData" as SdkEncString,
+ fingerprint: "2.keyFingerprint|encryptedData" as SdkEncString,
+ };
+
+ const result = SshKey.fromSdkSshKey(sdkSshKey);
+
+ expect(result).toBeInstanceOf(SshKey);
+ expect(result.privateKey).toBeDefined();
+ expect(result.publicKey).toBeDefined();
+ expect(result.keyFingerprint).toBeDefined();
+ });
+
+ it("creates a new SshKey instance", () => {
+ const sdkSshKey: SdkSshKey = {
+ privateKey: "2.privateKey|encryptedData" as SdkEncString,
+ publicKey: "2.publicKey|encryptedData" as SdkEncString,
+ fingerprint: "2.keyFingerprint|encryptedData" as SdkEncString,
+ };
+
+ const result = SshKey.fromSdkSshKey(sdkSshKey);
+
+ expect(result).not.toBe(sdkSshKey);
+ expect(result).toBeInstanceOf(SshKey);
+ });
+
+ it("is symmetric with toSdkSshKey", () => {
+ const original = new SshKey(data);
+ const sdkFormat = original.toSdkSshKey();
+ const reconstructed = SshKey.fromSdkSshKey(sdkFormat);
+
+ expect(reconstructed.privateKey.encryptedString).toBe(original.privateKey.encryptedString);
+ expect(reconstructed.publicKey.encryptedString).toBe(original.publicKey.encryptedString);
+ expect(reconstructed.keyFingerprint.encryptedString).toBe(
+ original.keyFingerprint.encryptedString,
+ );
+ });
+ });
});
From ff611338f932456171883874ccfb98043c2679e1 Mon Sep 17 00:00:00 2001
From: Mick Letofsky
Date: Tue, 4 Nov 2025 15:20:54 +0100
Subject: [PATCH 05/11] [PM-4735] - Refactor event handling of the browser
close listener (#16798)
---
.../services/browser-fido2-user-interface.service.ts | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts
index 8de48a49a8e..5e523a1a48d 100644
--- a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts
+++ b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts
@@ -6,7 +6,7 @@ import {
filter,
firstValueFrom,
fromEvent,
- fromEventPattern,
+ map,
merge,
Observable,
Subject,
@@ -28,6 +28,7 @@ import {
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { BrowserApi } from "../../../platform/browser/browser-api";
+import { fromChromeEvent } from "../../../platform/browser/from-chrome-event";
// FIXME (PM-22628): Popup imports are forbidden in background
// eslint-disable-next-line no-restricted-imports
import { closeFido2Popout, openFido2Popout } from "../../../vault/popup/utils/vault-popout-window";
@@ -232,12 +233,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
}
});
- this.windowClosed$ = fromEventPattern(
- // FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener
- // and test that it doesn't break. Tracking Ticket: https://bitwarden.atlassian.net/browse/PM-4735
- // eslint-disable-next-line no-restricted-syntax
- (handler: any) => chrome.windows.onRemoved.addListener(handler),
- (handler: any) => chrome.windows.onRemoved.removeListener(handler),
+ this.windowClosed$ = fromChromeEvent(chrome.windows.onRemoved).pipe(
+ map(([windowId]) => windowId),
);
BrowserFido2UserInterfaceSession.sendMessage({
From 9803cc98b4c05dfa930df77a385796ec41062fe8 Mon Sep 17 00:00:00 2001
From: neuronull <9162534+neuronull@users.noreply.github.com>
Date: Tue, 4 Nov 2025 06:35:34 -0800
Subject: [PATCH 06/11] Desktop Native remove `log` deps (#17135)
---
apps/desktop/desktop_native/Cargo.lock | 35 ++-----------------
apps/desktop/desktop_native/Cargo.toml | 3 --
.../desktop_native/macos_provider/Cargo.toml | 3 +-
3 files changed, 3 insertions(+), 38 deletions(-)
diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock
index 9a69ca62a1f..18ea0337a04 100644
--- a/apps/desktop/desktop_native/Cargo.lock
+++ b/apps/desktop/desktop_native/Cargo.lock
@@ -900,19 +900,6 @@ dependencies = [
"syn",
]
-[[package]]
-name = "dashmap"
-version = "5.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
-dependencies = [
- "cfg-if",
- "hashbrown 0.14.5",
- "lock_api",
- "once_cell",
- "parking_lot_core",
-]
-
[[package]]
name = "der"
version = "0.7.10"
@@ -1533,12 +1520,6 @@ dependencies = [
"subtle",
]
-[[package]]
-name = "hashbrown"
-version = "0.14.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
-
[[package]]
name = "hashbrown"
version = "0.15.3"
@@ -1554,7 +1535,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
- "hashbrown 0.15.3",
+ "hashbrown",
]
[[package]]
@@ -1719,7 +1700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
- "hashbrown 0.15.3",
+ "hashbrown",
]
[[package]]
@@ -1891,7 +1872,6 @@ version = "0.0.0"
dependencies = [
"desktop_core",
"futures",
- "oslog",
"serde",
"serde_json",
"tokio",
@@ -2379,17 +2359,6 @@ dependencies = [
"windows-sys 0.59.0",
]
-[[package]]
-name = "oslog"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969"
-dependencies = [
- "cc",
- "dashmap",
- "log",
-]
-
[[package]]
name = "p256"
version = "0.13.2"
diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml
index 4b5c1335c6b..edc15675c86 100644
--- a/apps/desktop/desktop_native/Cargo.toml
+++ b/apps/desktop/desktop_native/Cargo.toml
@@ -41,13 +41,11 @@ interprocess = "=2.2.1"
keytar = "=0.1.6"
libc = "=0.2.172"
linux-keyutils = "=0.2.4"
-log = "=0.4.25"
memsec = "=0.7.0"
napi = "=2.16.17"
napi-build = "=2.2.0"
napi-derive = "=2.16.13"
oo7 = "=0.4.3"
-oslog = "=0.2.0"
pin-project = "=1.1.10"
pkcs8 = "=0.10.2"
rand = "=0.9.1"
@@ -60,7 +58,6 @@ security-framework-sys = "=2.15.0"
serde = "=1.0.209"
serde_json = "=1.0.127"
sha2 = "=0.10.8"
-simplelog = "=0.12.2"
ssh-encoding = "=0.2.0"
ssh-key = { version = "=0.6.7", default-features = false }
sysinfo = "=0.35.0"
diff --git a/apps/desktop/desktop_native/macos_provider/Cargo.toml b/apps/desktop/desktop_native/macos_provider/Cargo.toml
index 97a8b7d545a..ea44f3d9a27 100644
--- a/apps/desktop/desktop_native/macos_provider/Cargo.toml
+++ b/apps/desktop/desktop_native/macos_provider/Cargo.toml
@@ -21,12 +21,11 @@ serde_json = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
tokio-util = { workspace = true }
tracing = { workspace = true }
-tracing-oslog = "0.3.0"
tracing-subscriber = { workspace = true }
uniffi = { workspace = true, features = ["cli"] }
[target.'cfg(target_os = "macos")'.dependencies]
-oslog = { workspace = true }
+tracing-oslog = "0.3.0"
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
From 9dfc0fe14f200d3b3922f568f6e6d8a1003e2919 Mon Sep 17 00:00:00 2001
From: Bernd Schoolmann
Date: Tue, 4 Nov 2025 15:46:19 +0100
Subject: [PATCH 07/11] Fix broken build on latest SDK (#17208)
* Fix broken build on latest SDK
* Fix test
---
.../src/vault/models/domain/cipher.spec.ts | 1 +
libs/common/src/vault/models/domain/cipher.ts | 1 +
package-lock.json | 16 ++++++++--------
package.json | 4 ++--
4 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts
index 7503d71573f..87301928c57 100644
--- a/libs/common/src/vault/models/domain/cipher.spec.ts
+++ b/libs/common/src/vault/models/domain/cipher.spec.ts
@@ -1088,6 +1088,7 @@ describe("Cipher DTO", () => {
card: undefined,
secureNote: undefined,
sshKey: undefined,
+ data: undefined,
favorite: false,
reprompt: SdkCipherRepromptType.None,
organizationUseTotp: true,
diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts
index 5e284232936..5739a9a50a7 100644
--- a/libs/common/src/vault/models/domain/cipher.ts
+++ b/libs/common/src/vault/models/domain/cipher.ts
@@ -421,6 +421,7 @@ export class Cipher extends Domain implements Decryptable {
card: undefined,
secureNote: undefined,
sshKey: undefined,
+ data: undefined,
};
switch (this.type) {
diff --git a/package-lock.json b/package-lock.json
index c8f825319e4..9636184c5ee 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,8 +23,8 @@
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/router": "19.2.14",
- "@bitwarden/commercial-sdk-internal": "0.2.0-main.365",
- "@bitwarden/sdk-internal": "0.2.0-main.365",
+ "@bitwarden/commercial-sdk-internal": "0.2.0-main.369",
+ "@bitwarden/sdk-internal": "0.2.0-main.369",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",
@@ -4607,9 +4607,9 @@
"link": true
},
"node_modules/@bitwarden/commercial-sdk-internal": {
- "version": "0.2.0-main.365",
- "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.365.tgz",
- "integrity": "sha512-yRc2k29rKMxss6qH2TP91VcE6tNR6/A2ASZMj+Om2MEaanV82zcx89dkShh6RP0jXICM+c/m6BgGkmu+1Pcp8w==",
+ "version": "0.2.0-main.369",
+ "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.369.tgz",
+ "integrity": "sha512-O+EaPQJQah9j3yWzgw+dwFk5iOxPXdKf1FDeykbt+cxygSYbWTR60RXenG1LysknOdy8fiTfHEaPD+LP1LxrdA==",
"license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT",
"dependencies": {
"type-fest": "^4.41.0"
@@ -4712,9 +4712,9 @@
"link": true
},
"node_modules/@bitwarden/sdk-internal": {
- "version": "0.2.0-main.365",
- "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.365.tgz",
- "integrity": "sha512-x0sqAuyknFOGf5ZfbuFTxL0olMiGyyLbJ10tXCYHnrkjdspdNm2BGZc64NQgXz5h+PH1Uwtow/01o/a4F0YTHw==",
+ "version": "0.2.0-main.369",
+ "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.369.tgz",
+ "integrity": "sha512-gyp4Wd1YbkANA0/RNxHfVk+DuiJqxItzk/YUyQ2HsLeP07xOljftmA0XspLQz59ovs7e1jHMCpH1r/XcyKiQSw==",
"license": "GPL-3.0",
"dependencies": {
"type-fest": "^4.41.0"
diff --git a/package.json b/package.json
index 181e003bf28..c1becca3a31 100644
--- a/package.json
+++ b/package.json
@@ -160,8 +160,8 @@
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/router": "19.2.14",
- "@bitwarden/sdk-internal": "0.2.0-main.365",
- "@bitwarden/commercial-sdk-internal": "0.2.0-main.365",
+ "@bitwarden/sdk-internal": "0.2.0-main.369",
+ "@bitwarden/commercial-sdk-internal": "0.2.0-main.369",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",
From a13a81ba81a1428cde7055956f84c8cd79a7d363 Mon Sep 17 00:00:00 2001
From: Vicki League
Date: Tue, 4 Nov 2025 09:56:12 -0500
Subject: [PATCH 08/11] [PM-26984] Use medium instead of semibold or bold
(#17193)
---
.../key-connector/confirm-key-connector-domain.component.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/key-management-ui/src/key-connector/confirm-key-connector-domain.component.html b/libs/key-management-ui/src/key-connector/confirm-key-connector-domain.component.html
index 6cf151d4604..11b34a8409f 100644
--- a/libs/key-management-ui/src/key-connector/confirm-key-connector-domain.component.html
+++ b/libs/key-management-ui/src/key-connector/confirm-key-connector-domain.component.html
@@ -9,7 +9,7 @@
} @else {
-
{{ "keyConnectorDomain" | i18n }}:
+
{{ "keyConnectorDomain" | i18n }}:
{{ keyConnectorUrl }}
From 573d4219f9abad0f84a8b88a2725cc6448fd5ca8 Mon Sep 17 00:00:00 2001
From: Vicki League
Date: Tue, 4 Nov 2025 10:18:53 -0500
Subject: [PATCH 09/11] [PM-26984] Use medium instead of semibold or bold
(#17192)
---
apps/desktop/src/platform/components/approve-ssh-request.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/desktop/src/platform/components/approve-ssh-request.html b/apps/desktop/src/platform/components/approve-ssh-request.html
index b7005872f25..55092788079 100644
--- a/apps/desktop/src/platform/components/approve-ssh-request.html
+++ b/apps/desktop/src/platform/components/approve-ssh-request.html
@@ -1,6 +1,6 @@