1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-30 07:03:26 +00:00

Merge branch 'main' into autofill/pm-27195/register-autotype-svc-with-login

This commit is contained in:
neuronull
2025-11-04 10:24:30 -07:00
49 changed files with 784 additions and 242 deletions

View File

@@ -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

View File

@@ -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({

View File

@@ -17,7 +17,7 @@
<div class="tw-size-[95px] tw-content-center">
<bit-icon [icon]="sendCreatedIcon"></bit-icon>
</div>
<h3 tabindex="0" appAutofocus class="tw-font-semibold">
<h3 tabindex="0" appAutofocus class="tw-font-medium">
{{ "createdSendSuccessfully" | i18n }}
</h3>
<p class="tw-text-center">

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"] }

View File

@@ -6,6 +6,7 @@ import { LogService } from "@bitwarden/logging";
import { WindowMain } from "../../main/window.main";
import { stringIsNotUndefinedNullAndEmpty } from "../../utils";
import { AutotypeConfig } from "../models/autotype-configure";
import { AutotypeVaultData } from "../models/autotype-vault-data";
import { AUTOTYPE_IPC_CHANNELS } from "../models/ipc-channels";
import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut";
@@ -49,18 +50,12 @@ export class MainDesktopAutotypeService {
this.setKeyboardShortcut(newKeyboardShortcut);
});
ipcMain.on(AUTOTYPE_IPC_CHANNELS.EXECUTE, (_event, data) => {
const { response } = data;
ipcMain.on(AUTOTYPE_IPC_CHANNELS.EXECUTE, (_event, vaultData: AutotypeVaultData) => {
if (
stringIsNotUndefinedNullAndEmpty(response.username) &&
stringIsNotUndefinedNullAndEmpty(response.password)
stringIsNotUndefinedNullAndEmpty(vaultData.username) &&
stringIsNotUndefinedNullAndEmpty(vaultData.password)
) {
this.doAutotype(
response.username,
response.password,
this.autotypeKeyboardShortcut.getArrayFormat(),
);
this.doAutotype(vaultData, this.autotypeKeyboardShortcut.getArrayFormat());
}
});
@@ -137,8 +132,9 @@ export class MainDesktopAutotypeService {
}
}
private doAutotype(username: string, password: string, keyboardShortcut: string[]) {
const inputPattern = username + "\t" + password;
private doAutotype(vaultData: AutotypeVaultData, keyboardShortcut: string[]) {
const TAB = "\t";
const inputPattern = vaultData.username + TAB + vaultData.password;
const inputArray = new Array<number>(inputPattern.length);
for (let i = 0; i < inputPattern.length; i++) {

View File

@@ -0,0 +1,8 @@
/**
* Vault data used in autotype operations.
* `username` and `password` are guaranteed to be not null/undefined.
*/
export interface AutotypeVaultData {
username: string;
password: string;
}

View File

@@ -6,6 +6,7 @@ import { Command } from "../platform/main/autofill/command";
import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main";
import { AutotypeConfig } from "./models/autotype-configure";
import { AutotypeVaultData } from "./models/autotype-vault-data";
import { AUTOTYPE_IPC_CHANNELS } from "./models/ipc-channels";
export default {
@@ -145,10 +146,7 @@ export default {
listenAutotypeRequest: (
fn: (
windowTitle: string,
completeCallback: (
error: Error | null,
response: { username?: string; password?: string },
) => void,
completeCallback: (error: Error | null, response: AutotypeVaultData | null) => void,
) => void,
) => {
ipcRenderer.on(
@@ -161,7 +159,7 @@ export default {
) => {
const { windowTitle } = data;
fn(windowTitle, (error, response) => {
fn(windowTitle, (error, vaultData) => {
if (error) {
ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.EXECUTION_ERROR, {
windowTitle,
@@ -170,10 +168,9 @@ export default {
return;
}
ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.EXECUTE, {
windowTitle,
response,
});
if (vaultData !== null) {
ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.EXECUTE, vaultData);
}
});
},
);

View File

@@ -0,0 +1,50 @@
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { getAutotypeVaultData } from "./desktop-autotype.service";
describe("getAutotypeVaultData", () => {
it("should return vault data when cipher has username and password", () => {
const cipherView = new CipherView();
cipherView.login.username = "foo";
cipherView.login.password = "bar";
const [error, vaultData] = getAutotypeVaultData(cipherView);
expect(error).toBeNull();
expect(vaultData?.username).toEqual("foo");
expect(vaultData?.password).toEqual("bar");
});
it("should return error when firstCipher is undefined", () => {
const cipherView = undefined;
const [error, vaultData] = getAutotypeVaultData(cipherView);
expect(vaultData).toBeNull();
expect(error).toBeDefined();
expect(error?.message).toEqual("No matching vault item.");
});
it("should return error when username is undefined", () => {
const cipherView = new CipherView();
cipherView.login.username = undefined;
cipherView.login.password = "bar";
const [error, vaultData] = getAutotypeVaultData(cipherView);
expect(vaultData).toBeNull();
expect(error).toBeDefined();
expect(error?.message).toEqual("Vault item is undefined.");
});
it("should return error when password is undefined", () => {
const cipherView = new CipherView();
cipherView.login.username = "foo";
cipherView.login.password = undefined;
const [error, vaultData] = getAutotypeVaultData(cipherView);
expect(vaultData).toBeNull();
expect(error).toBeDefined();
expect(error?.message).toEqual("Vault item is undefined.");
});
});

View File

@@ -32,6 +32,7 @@ import { LogService } from "@bitwarden/logging";
import { UserId } from "@bitwarden/user-core";
import { AutotypeConfig } from "../models/autotype-configure";
import { AutotypeVaultData } from "../models/autotype-vault-data";
import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service";
@@ -43,6 +44,8 @@ export const AUTOTYPE_ENABLED = new KeyDefinition<boolean | null>(
{ deserializer: (b) => b },
);
export type Result<T, E = Error> = [E, null] | [null, T];
/*
Valid windows shortcut keys: Control, Alt, Super, Shift, letters A - Z
Valid macOS shortcut keys: Control, Alt, Command, Shift, letters A - Z
@@ -121,11 +124,8 @@ export class DesktopAutotypeService implements OnDestroy {
ipc.autofill.listenAutotypeRequest(async (windowTitle, callback) => {
const possibleCiphers = await this.matchCiphersToWindowTitle(windowTitle);
const firstCipher = possibleCiphers?.at(0);
return callback(null, {
username: firstCipher?.login?.username,
password: firstCipher?.login?.password,
});
const [error, vaultData] = getAutotypeVaultData(firstCipher);
callback(error, vaultData);
});
// If `autotypeDefaultPolicy` is `true` for a user's organization, and the
@@ -245,3 +245,23 @@ export class DesktopAutotypeService implements OnDestroy {
this.destroy$.complete();
}
}
/**
* @return an `AutotypeVaultData` object or an `Error` if the
* cipher or vault data within are undefined.
*/
export function getAutotypeVaultData(
cipherView: CipherView | undefined,
): Result<AutotypeVaultData> {
if (!cipherView) {
return [Error("No matching vault item."), null];
} else if (cipherView.login.username === undefined || cipherView.login.password === undefined) {
return [Error("Vault item is undefined."), null];
} else {
const vaultData: AutotypeVaultData = {
username: cipherView.login.username,
password: cipherView.login.password,
};
return [null, vaultData];
}
}

View File

@@ -1,6 +1,6 @@
<form [bitSubmit]="submit" [formGroup]="approveSshRequestForm">
<bit-dialog>
<div class="tw-font-semibold" bitDialogTitle>{{ "sshkeyApprovalTitle" | i18n }}</div>
<div class="tw-font-medium" bitDialogTitle>{{ "sshkeyApprovalTitle" | i18n }}</div>
<div bitDialogContent>
<app-callout
type="warning"

View File

@@ -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);
});
});
});

View File

@@ -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<boolean>(
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<boolean | null> {
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<void> {
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<void> {
if (!userId) {
throw new Error("UserId is required. Cannot clear 'premiumInterest'.");
}
await this.stateProvider.setUserState(PREMIUM_INTEREST_KEY, null, userId);
}
}

View File

@@ -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({

View File

@@ -21,7 +21,7 @@
<div class="tw-col-span-3">
<div class="tw-border tw-border-solid tw-border-secondary-300 tw-rounded" data-testid="filters">
<div
class="tw-bg-background-alt tw-border-0 tw-border-b tw-border-solid tw-border-secondary-100 tw-rounded-t tw-px-5 tw-py-2.5 tw-font-semibold tw-uppercase"
class="tw-bg-background-alt tw-border-0 tw-border-b tw-border-solid tw-border-secondary-100 tw-rounded-t tw-px-5 tw-py-2.5 tw-font-medium tw-uppercase"
data-testid="filters-header"
>
{{ "filters" | i18n }}

View File

@@ -6,7 +6,8 @@
<dirt-risk-insights-loading></dirt-risk-insights-loading>
} @else {
<!-- Check final states after initial calls have been completed -->
@if (!(dataService.hasReportData$ | async)) {
@if (isRiskInsightsActivityTabFeatureEnabled && !(dataService.hasReportData$ | async)) {
<!-- Show empty state only when feature flag is enabled and there's no report data -->
<div class="tw-flex tw-justify-center tw-items-center tw-min-h-[70vh] tw-w-full">
@if (!hasCiphers) {
<!-- Show Empty state when there are no applications (no ciphers to make reports on) -->
@@ -33,47 +34,47 @@
}
</div>
} @else {
<!-- Show screen when there is report data -->
<!-- Show screen when there is report data OR when feature flag is disabled (show tabs even without data) -->
<div class="tw-min-h-screen tw-flex tw-flex-col">
<div>
<h1 bitTypography="h1">{{ "riskInsights" | i18n }}</h1>
<div class="tw-text-main tw-max-w-4xl tw-mb-2" *ngIf="appsCount > 0">
{{ "reviewAtRiskPasswords" | i18n }}
</div>
@if (dataLastUpdated) {
<div
class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-4 tw-my-4 tw-flex tw-items-center"
>
<i
class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] tw-text-muted"
aria-hidden="true"
></i>
@let isRunningReport = dataService.isGeneratingReport$ | async;
<div
class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-4 tw-my-4 tw-flex tw-items-center"
>
<i
class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] tw-text-muted"
aria-hidden="true"
></i>
@if (dataLastUpdated) {
<span class="tw-mx-4">{{
"dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
}}</span>
@let isRunningReport = dataService.isGeneratingReport$ | async;
<span class="tw-flex tw-justify-center">
<button
*ngIf="!isRunningReport"
type="button"
bitButton
buttonType="secondary"
class="tw-border-none !tw-font-normal tw-cursor-pointer !tw-py-0"
tabindex="0"
[bitAction]="generateReport.bind(this)"
>
{{ "riskInsightsRunReport" | i18n }}
</button>
<span>
<i
*ngIf="isRunningReport"
class="bwi bwi-spinner bwi-spin tw-text-muted tw-text-[1.2rem]"
aria-hidden="true"
></i>
</span>
}
<span class="tw-flex tw-justify-center">
<button
*ngIf="!isRunningReport"
type="button"
bitButton
buttonType="secondary"
class="tw-border-none !tw-font-normal tw-cursor-pointer !tw-py-0"
tabindex="0"
[bitAction]="generateReport.bind(this)"
>
{{ "riskInsightsRunReport" | i18n }}
</button>
<span>
<i
*ngIf="isRunningReport"
class="bwi bwi-spinner bwi-spin tw-text-muted tw-text-[1.2rem]"
aria-hidden="true"
></i>
</span>
</div>
}
</span>
</div>
</div>
<div class="tw-flex-1 tw-flex tw-flex-col">

View File

@@ -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<boolean | null> {
return null;
} // no-op
async setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise<void> {} // no-op
async clearPremiumInterest(userId: UserId): Promise<void> {} // no-op
}

View File

@@ -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<boolean | null>;
abstract setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise<void>;
abstract clearPremiumInterest(userId: UserId): Promise<void>;
}

View File

@@ -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({

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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<CipherData>) {
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;
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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", () => {

View File

@@ -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", () => {

View File

@@ -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<Cipher>);
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<Cipher>);
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" }],
@@ -1056,6 +1088,7 @@ describe("Cipher DTO", () => {
card: undefined,
secureNote: undefined,
sshKey: undefined,
data: undefined,
favorite: false,
reprompt: SdkCipherRepromptType.None,
organizationUseTotp: true,

View File

@@ -421,6 +421,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
card: undefined,
secureNote: undefined,
sshKey: undefined,
data: undefined,
};
switch (this.type) {

View File

@@ -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,

View File

@@ -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", () => {

View File

@@ -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<EncryptService>;
@@ -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();

View File

@@ -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", () => {

View File

@@ -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) {

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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,
);
});
});
});

View File

@@ -11,7 +11,7 @@
<form [formGroup]="formGroup" [bitSubmit]="submit" id="import_form_importForm">
<bit-section>
<bit-section-header>
<h2 class="tw-font-bold" bitTypography="h6">{{ "destination" | i18n }}</h2>
<h2 class="tw-font-medium" bitTypography="h6">{{ "destination" | i18n }}</h2>
</bit-section-header>
<bit-card>
<bit-form-field [hidden]="isFromAC">
@@ -62,7 +62,7 @@
<bit-section>
<bit-section-header>
<h2 class="tw-font-bold" bitTypography="h6">{{ "data" | i18n }}</h2>
<h2 class="tw-font-medium" bitTypography="h6">{{ "data" | i18n }}</h2>
</bit-section-header>
<bit-card>
<bit-form-field class="@2xl:tw-w-1/2">
@@ -70,7 +70,7 @@
<bit-select formControlName="format">
<bit-option value="" label="-- {{ 'select' | i18n }} --" />
<bit-option
class="tw-font-bold tw-text-muted tw-text-xs"
class="tw-font-medium tw-text-muted tw-text-xs"
value="-"
label="{{ 'commonImportFormats' | i18n }}"
disabled

View File

@@ -9,7 +9,7 @@
</div>
} @else {
<div class="tw-mb-4">
<p class="tw-mb-1 tw-text-sm tw-font-semibold">{{ "keyConnectorDomain" | i18n }}:</p>
<p class="tw-mb-1 tw-text-sm tw-font-medium">{{ "keyConnectorDomain" | i18n }}:</p>
<p class="tw-text-muted tw-break-all">{{ keyConnectorUrl }}</p>
</div>

View File

@@ -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

View File

@@ -1,6 +1,6 @@
<bit-section *ngIf="sends?.length > 0" disableMargin>
<bit-section-header>
<h2 class="tw-font-bold" bitTypography="h6">
<h2 class="tw-font-medium" bitTypography="h6">
{{ headerText }}
</h2>
<span bitTypography="body1" slot="end">{{ sends.length }}</span>

16
package-lock.json generated
View File

@@ -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"

View File

@@ -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",