1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 02:03:39 +00:00

Merge branch 'main' into PM-26250-Explore-options-to-enable-direct-importer-for-mac-app-store-build

This commit is contained in:
John Harrington
2025-12-12 15:42:53 -07:00
committed by GitHub
341 changed files with 8890 additions and 4500 deletions

View File

@@ -154,7 +154,6 @@
"@types/glob",
"@types/lowdb",
"@types/node",
"@types/node-forge",
"@types/node-ipc",
"@yao-pkg/pkg",
"anyhow",
@@ -192,12 +191,10 @@
"napi",
"napi-build",
"napi-derive",
"node-forge",
"node-ipc",
"nx",
"oo7",
"oslog",
"parse5",
"pin-project",
"pkg",
"postcss",
@@ -215,6 +212,8 @@
"simplelog",
"style-loader",
"sysinfo",
"tokio",
"tokio-util",
"tracing",
"tracing-subscriber",
"ts-node",
@@ -261,6 +260,11 @@
groupName: "windows",
matchPackageNames: ["windows", "windows-core", "windows-future", "windows-registry"],
},
{
// We need to group all tokio-related packages together to avoid build errors caused by version incompatibilities.
groupName: "tokio",
matchPackageNames: ["bytes", "tokio", "tokio-util"],
},
{
// We group all webpack build-related minor and patch updates together to reduce PR noise.
// We include patch updates here because we want PRs for webpack patch updates and it's in this group.
@@ -409,14 +413,16 @@
},
{
matchPackageNames: [
"@types/node-forge",
"aes",
"big-integer",
"cbc",
"linux-keyutils",
"memsec",
"node-forge",
"rsa",
"russh-cryptovec",
"sha2",
"memsec",
"linux-keyutils",
],
description: "Key Management owned dependencies",
commitMessagePrefix: "[deps] KM:",

View File

@@ -209,7 +209,7 @@ jobs:
- name: Set up environment
run: |
sudo apt-get update
sudo apt-get -y install pkg-config libxss-dev rpm musl-dev musl-tools flatpak flatpak-builder
sudo apt-get -y install pkg-config libxss-dev rpm flatpak flatpak-builder
- name: Set up Snap
run: sudo snap install snapcraft --classic
@@ -262,12 +262,10 @@ jobs:
env:
PKG_CONFIG_ALLOW_CROSS: true
PKG_CONFIG_ALL_STATIC: true
TARGET: musl
# Note: It is important that we use the release build because some compute heavy
# operations such as key derivation for oo7 on linux are too slow in debug mode
run: |
rustup target add x86_64-unknown-linux-musl
node build.js --target=x86_64-unknown-linux-musl --release
node build.js --release
- name: Build application
run: npm run dist:lin
@@ -367,7 +365,7 @@ jobs:
- name: Set up environment
run: |
sudo apt-get update
sudo apt-get -y install pkg-config libxss-dev rpm musl-dev musl-tools flatpak flatpak-builder squashfs-tools ruby ruby-dev rubygems build-essential
sudo apt-get -y install pkg-config libxss-dev rpm flatpak flatpak-builder squashfs-tools ruby ruby-dev rubygems build-essential
sudo gem install --no-document fpm
- name: Set up Snap
@@ -427,12 +425,10 @@ jobs:
env:
PKG_CONFIG_ALLOW_CROSS: true
PKG_CONFIG_ALL_STATIC: true
TARGET: musl
# Note: It is important that we use the release build because some compute heavy
# operations such as key derivation for oo7 on linux are too slow in debug mode
run: |
rustup target add aarch64-unknown-linux-musl
node build.js --target=aarch64-unknown-linux-musl --release
node build.js --release
- name: Check index.d.ts generated
if: github.event_name == 'pull_request' && steps.cache.outputs.cache-hit != 'true'
@@ -587,7 +583,9 @@ jobs:
- name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native
run: node build.js cross-platform
env:
MODE: ${{ github.event_name == 'workflow_call' && '--release' || '' }}
run: node build.js cross-platform "$env:MODE"
- name: Build
run: npm run build
@@ -850,7 +848,9 @@ jobs:
- name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native
run: node build.js cross-platform
env:
MODE: ${{ github.event_name == 'workflow_call' && '--release' || '' }}
run: node build.js cross-platform "$env:MODE"
- name: Build
run: npm run build
@@ -1206,7 +1206,9 @@ jobs:
- name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native
run: node build.js cross-platform
env:
MODE: ${{ github.event_name == 'workflow_call' && '--release' || '' }}
run: node build.js cross-platform "$MODE"
- name: Build application (dev)
run: npm run build
@@ -1428,7 +1430,9 @@ jobs:
- name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native
run: node build.js cross-platform
env:
MODE: ${{ github.event_name == 'workflow_call' && '--release' || '' }}
run: node build.js cross-platform "$MODE"
- name: Build
if: steps.build-cache.outputs.cache-hit != 'true'
@@ -1709,7 +1713,9 @@ jobs:
- name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native
run: node build.js cross-platform
env:
MODE: ${{ github.event_name == 'workflow_call' && '--release' || '' }}
run: node build.js cross-platform "$MODE"
- name: Build
if: steps.build-cache.outputs.cache-hit != 'true'

View File

@@ -187,6 +187,8 @@ jobs:
with:
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
owner: ${{ github.repository_owner }}
repositories: self-host
- name: Trigger Bitwarden lite build
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0

View File

@@ -98,6 +98,14 @@ jobs:
working-directory: apps/desktop/artifacts
run: mv "Bitwarden-${PKG_VERSION}-universal.pkg" "Bitwarden-${PKG_VERSION}-universal.pkg.archive"
- name: Rename .tar.gz to include version
env:
PKG_VERSION: ${{ steps.version.outputs.version }}
working-directory: apps/desktop/artifacts
run: |
mv "bitwarden_desktop_x64.tar.gz" "bitwarden_${PKG_VERSION}_x64.tar.gz"
mv "bitwarden_desktop_arm64.tar.gz" "bitwarden_${PKG_VERSION}_arm64.tar.gz"
- name: Create Release
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
if: ${{ steps.release_channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }}

View File

@@ -75,7 +75,7 @@ jobs:
- name: Trigger test-all workflow in browser-interactions-testing
if: steps.changed-files.outputs.monitored == 'true'
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ steps.app-token.outputs.token }}
repository: "bitwarden/browser-interactions-testing"

View File

@@ -436,8 +436,8 @@
"sync": {
"message": "Sync"
},
"syncVaultNow": {
"message": "Sync vault now"
"syncNow": {
"message": "Sync now"
},
"lastSync": {
"message": "Last sync:"
@@ -455,9 +455,6 @@
"bitWebVaultApp": {
"message": "Bitwarden web app"
},
"importItems": {
"message": "Import items"
},
"select": {
"message": "Select"
},
@@ -1325,8 +1322,11 @@
"exportFrom": {
"message": "Export from"
},
"exportVault": {
"message": "Export vault"
"export": {
"message": "Export"
},
"import": {
"message": "Import"
},
"fileFormat": {
"message": "File format"
@@ -1475,6 +1475,9 @@
"selectFile": {
"message": "Select a file"
},
"itemsTransferred": {
"message": "Items transferred"
},
"maxFileSize": {
"message": "Maximum file size is 500 MB."
},
@@ -3249,9 +3252,6 @@
"copyCustomFieldNameNotUnique": {
"message": "No unique identifier found."
},
"removeMasterPasswordForOrganizationUserKeyConnector": {
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
},
"organizationName": {
"message": "Organization name"
},
@@ -4215,10 +4215,6 @@
"ignore": {
"message": "Ignore"
},
"importData": {
"message": "Import data",
"description": "Used for the header of the import dialog, the import button and within the file-password-prompt"
},
"importError": {
"message": "Import error"
},
@@ -5888,6 +5884,45 @@
"cardNumberLabel": {
"message": "Card number"
},
"removeMasterPasswordForOrgUserKeyConnector":{
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
},
"continueWithLogIn": {
"message": "Continue with log in"
},
"doNotContinue": {
"message": "Do not continue"
},
"domain": {
"message": "Domain"
},
"keyConnectorDomainTooltip": {
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
},
"verifyYourOrganization": {
"message": "Verify your organization to log in"
},
"organizationVerified":{
"message": "Organization verified"
},
"domainVerified":{
"message": "Domain verified"
},
"leaveOrganizationContent": {
"message": "If you don't verify your organization, your access to the organization will be revoked."
},
"leaveNow": {
"message": "Leave now"
},
"verifyYourDomainToLogin": {
"message": "Verify your domain to log in"
},
"verifyYourDomainDescription": {
"message": "To continue with log in, verify this domain."
},
"confirmKeyConnectorOrganizationUserDescription": {
"message": "To continue with log in, verify the organization and domain."
},
"sessionTimeoutSettingsAction": {
"message": "Timeout action"
},
@@ -5937,5 +5972,53 @@
},
"upgrade": {
"message": "Upgrade"
},
"leaveConfirmationDialogTitle": {
"message": "Are you sure you want to leave?"
},
"leaveConfirmationDialogContentOne": {
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
},
"leaveConfirmationDialogContentTwo": {
"message": "Contact your admin to regain access."
},
"leaveConfirmationDialogConfirmButton": {
"message": "Leave $ORGANIZATION$",
"placeholders": {
"organization": {
"content": "$1",
"example": "My Org Name"
}
}
},
"howToManageMyVault": {
"message": "How do I manage my vault?"
},
"transferItemsToOrganizationTitle": {
"message": "Transfer items to $ORGANIZATION$",
"placeholders": {
"organization": {
"content": "$1",
"example": "My Org Name"
}
}
},
"transferItemsToOrganizationContent": {
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
"placeholders": {
"organization": {
"content": "$1",
"example": "My Org Name"
}
}
},
"acceptTransfer": {
"message": "Accept transfer"
},
"declineAndLeave": {
"message": "Decline and leave"
},
"whyAmISeeingThis": {
"message": "Why am I seeing this?"
}
}

View File

@@ -2,7 +2,7 @@
<button
*ngIf="currentAccount$ | async as currentAccount; else defaultButton"
type="button"
class="tw-rounded-full hover:tw-outline hover:tw-outline-1 hover:tw-outline-offset-1 hover:tw-outline-primary-600"
class="tw-rounded-full hover:tw-outline hover:tw-outline-1 hover:tw-outline-primary-600"
(click)="currentAccountClicked()"
>
<span class="tw-sr-only"> {{ "bitwardenAccount" | i18n }} {{ currentAccount.email }}</span>

View File

@@ -15,6 +15,7 @@ import {
} from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { AccountSwitcherService } from "./account-switcher.service";
@@ -71,11 +72,10 @@ describe("AccountSwitcherService", () => {
describe("availableAccounts$", () => {
it("should return all logged in accounts and an add account option when accounts are less than 5", async () => {
const accountInfo: AccountInfo = {
const accountInfo = mockAccountInfoWith({
name: "Test User 1",
email: "test1@email.com",
emailVerified: true,
};
});
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
accountsSubject.next({ ["1" as UserId]: accountInfo, ["2" as UserId]: accountInfo });
@@ -109,11 +109,10 @@ describe("AccountSwitcherService", () => {
const seedAccounts: Record<UserId, AccountInfo> = {};
const seedStatuses: Record<UserId, AuthenticationStatus> = {};
for (let i = 0; i < numberOfAccounts; i++) {
seedAccounts[`${i}` as UserId] = {
seedAccounts[`${i}` as UserId] = mockAccountInfoWith({
email: `test${i}@email.com`,
emailVerified: true,
name: "Test User ${i}",
};
});
seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked;
}
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
@@ -133,11 +132,10 @@ describe("AccountSwitcherService", () => {
);
it("excludes logged out accounts", async () => {
const user1AccountInfo: AccountInfo = {
const user1AccountInfo = mockAccountInfoWith({
name: "Test User 1",
email: "",
emailVerified: true,
};
});
accountsSubject.next({ ["1" as UserId]: user1AccountInfo });
authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut });
accountsSubject.next({

View File

@@ -102,6 +102,36 @@ describe("ExtensionLoginComponentService", () => {
});
});
describe("redirectToSsoLoginWithOrganizationSsoIdentifier", () => {
it("launches SSO browser window with correct Url", async () => {
const email = "test@bitwarden.com";
const state = "testState";
const expectedState = "testState:clientId=browser";
const codeVerifier = "testCodeVerifier";
const codeChallenge = "testCodeChallenge";
const orgSsoIdentifier = "org-sso-identifier";
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
await service.redirectToSsoLoginWithOrganizationSsoIdentifier(email, orgSsoIdentifier);
expect(ssoUrlService.buildSsoUrl).toHaveBeenCalledWith(
expect.any(String),
expect.any(String),
expect.any(String),
expect.any(String),
expect.any(String),
email,
orgSsoIdentifier,
);
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(expectedState);
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
expect(platformUtilsService.launchUri).toHaveBeenCalled();
});
});
describe("showBackButton", () => {
it("sets showBackButton in extensionAnonLayoutWrapperDataService", () => {
service.showBackButton(true);

View File

@@ -47,6 +47,7 @@ export class ExtensionLoginComponentService
email: string,
state: string,
codeChallenge: string,
orgSsoIdentifier?: string,
): Promise<void> {
const env = await firstValueFrom(this.environmentService.environment$);
const webVaultUrl = env.getWebVaultUrl();
@@ -60,6 +61,7 @@ export class ExtensionLoginComponentService
state,
codeChallenge,
email,
orgSsoIdentifier,
);
this.platformUtilsService.launchUri(webAppSsoUrl);

View File

@@ -3,9 +3,7 @@ import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/
/**
* Browser extension implementation of the device management component service
*/
export class ExtensionDeviceManagementComponentService
implements DeviceManagementComponentServiceAbstraction
{
export class ExtensionDeviceManagementComponentService implements DeviceManagementComponentServiceAbstraction {
/**
* Don't show header information in browser extension client
*/

View File

@@ -4,7 +4,7 @@ import { BehaviorSubject, firstValueFrom, of } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { ExtensionCommand } from "@bitwarden/common/autofill/constants";
@@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -80,11 +81,12 @@ describe("NotificationBackground", () => {
const organizationService = mock<OrganizationService>();
const userId = "testId" as UserId;
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
const activeAccountSubject = new BehaviorSubject({
id: userId,
...mockAccountInfoWith({
email: "test@example.com",
emailVerified: true,
name: "Test User",
}),
});
beforeEach(() => {

View File

@@ -18,6 +18,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@@ -123,9 +124,10 @@ describe("context-menu", () => {
autofillSettingsService.enableContextMenu$ = of(true);
accountService.activeAccount$ = of({
id: "userId" as UserId,
...mockAccountInfoWith({
email: "",
emailVerified: false,
name: undefined,
}),
});
});

View File

@@ -120,9 +120,7 @@ export type BrowserFido2ParentWindowReference = chrome.tabs.Tab;
* Browser implementation of the {@link Fido2UserInterfaceService}.
* The user interface is implemented as a popout and the service uses the browser's messaging API to communicate with it.
*/
export class BrowserFido2UserInterfaceService
implements Fido2UserInterfaceServiceAbstraction<BrowserFido2ParentWindowReference>
{
export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction<BrowserFido2ParentWindowReference> {
constructor(private authService: AuthService) {}
async newSession(

View File

@@ -129,7 +129,12 @@ export class AutofillInlineMenuContainer {
}
try {
const urlObj = new URL(url);
const isExtensionProtocol = /^[a-z]+(-[a-z]+)?-extension:$/i.test(urlObj.protocol);
const extensionProtocols = new Set([
"chrome-extension:",
"moz-extension:",
"safari-web-extension:",
]);
const isExtensionProtocol = extensionProtocols.has(urlObj.protocol);
if (!isExtensionProtocol) {
return false;

View File

@@ -15,9 +15,7 @@ import {
OverlayNotificationsExtensionMessageHandlers,
} from "../abstractions/overlay-notifications-content.service";
export class OverlayNotificationsContentService
implements OverlayNotificationsContentServiceInterface
{
export class OverlayNotificationsContentService implements OverlayNotificationsContentServiceInterface {
private notificationBarRootElement: HTMLElement | null = null;
private notificationBarElement: HTMLElement | null = null;
private notificationBarIframeElement: HTMLIFrameElement | null = null;

View File

@@ -16,9 +16,7 @@ import {
} from "./autofill-constants";
import AutofillService from "./autofill.service";
export class InlineMenuFieldQualificationService
implements InlineMenuFieldQualificationServiceInterface
{
export class InlineMenuFieldQualificationService implements InlineMenuFieldQualificationServiceInterface {
private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);
private excludedAutofillFieldTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes);
private usernameFieldTypes = new Set(["text", "email", "number", "tel"]);

View File

@@ -841,10 +841,7 @@ export default class MainBackground {
);
this.pinService = new PinService(
this.accountService,
this.encryptService,
this.kdfConfigService,
this.keyGenerationService,
this.logService,
this.keyService,
this.sdkService,
@@ -1112,7 +1109,7 @@ export default class MainBackground {
this.collectionService,
this.keyService,
this.encryptService,
this.pinService,
this.keyGenerationService,
this.accountService,
this.restrictedItemTypesService,
);
@@ -1120,7 +1117,7 @@ export default class MainBackground {
this.individualVaultExportService = new IndividualVaultExportService(
this.folderService,
this.cipherService,
this.pinService,
this.keyGenerationService,
this.keyService,
this.encryptService,
this.cryptoFunctionService,
@@ -1134,7 +1131,7 @@ export default class MainBackground {
this.organizationVaultExportService = new OrganizationVaultExportService(
this.cipherService,
this.exportApiService,
this.pinService,
this.keyGenerationService,
this.keyService,
this.encryptService,
this.cryptoFunctionService,

View File

@@ -48,7 +48,11 @@ export class ForegroundBrowserBiometricsService extends BiometricsService {
result: BiometricsStatus;
error: string;
}>(BiometricsCommands.GetBiometricsStatusForUser, { userId: id });
if (response != null) {
return response.result;
} else {
return BiometricsStatus.DesktopDisconnected;
}
}
async getShouldAutopromptNow(): Promise<boolean> {

View File

@@ -1,51 +0,0 @@
<popup-page>
<popup-header slot="header" pageTitle="{{ 'removeMasterPassword' | i18n }}">
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>
</popup-header>
@if (loading) {
<div class="tw-text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
} @else {
<p>{{ "removeMasterPasswordForOrganizationUserKeyConnector" | i18n }}</p>
<p class="tw-mb-0">{{ "organizationName" | i18n }}:</p>
<p class="tw-text-muted tw-mb-6">{{ organization.name }}</p>
<p class="tw-mb-0">{{ "keyConnectorDomain" | i18n }}:</p>
<p class="tw-text-muted tw-mb-6">{{ organization.keyConnectorUrl }}</p>
<button
type="button"
bitButton
buttonType="primary"
block
(click)="convert()"
[disabled]="action"
class="tw-mb-2"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="continuing"
></i>
{{ "removeMasterPassword" | i18n }}
</button>
<button type="button" bitButton block (click)="leave()" [disabled]="action">
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="leaving"
></i>
{{ "leaveOrganization" | i18n }}
</button>
}
</popup-page>

View File

@@ -1,14 +0,0 @@
// FIXME (PM-22628): angular imports are forbidden in background
// eslint-disable-next-line no-restricted-imports
import { Component } from "@angular/core";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-remove-password",
templateUrl: "remove-password.component.html",
standalone: false,
})
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}

View File

@@ -22,7 +22,7 @@ export type NavButton = {
templateUrl: "popup-tab-navigation.component.html",
imports: [CommonModule, LinkModule, RouterModule, JslibModule, IconModule],
host: {
class: "tw-block tw-h-full tw-w-full tw-flex tw-flex-col",
class: "tw-block tw-size-full tw-flex tw-flex-col",
},
})
export class PopupTabNavigationComponent {

View File

@@ -120,8 +120,8 @@ export class PopupRouterCacheService {
/**
* Navigate back in history
*/
async back() {
if (!BrowserPopupUtils.inPopup(window)) {
async back(updateCache = false) {
if (!updateCache && !BrowserPopupUtils.inPopup(window)) {
this.location.back();
return;
}

View File

@@ -43,7 +43,11 @@ import {
TwoFactorAuthGuard,
} from "@bitwarden/auth/angular";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
import {
LockComponent,
ConfirmKeyConnectorDomainComponent,
RemovePasswordComponent,
} from "@bitwarden/key-management-ui";
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
import { AuthExtensionRoute } from "../auth/popup/constants/auth-extension-route.constant";
@@ -59,7 +63,6 @@ import { NotificationsSettingsComponent } from "../autofill/popup/settings/notif
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
import { PhishingWarning } from "../dirt/phishing-detection/popup/phishing-warning.component";
import { ProtectedByComponent } from "../dirt/phishing-detection/popup/protected-by-component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import BrowserPopupUtils from "../platform/browser/browser-popup-utils";
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
import { RouteCacheOptions } from "../platform/services/popup-view-cache-background.service";
@@ -188,9 +191,22 @@ const routes: Routes = [
},
{
path: "remove-password",
component: RemovePasswordComponent,
component: ExtensionAnonLayoutWrapperComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
children: [
{
path: "",
component: RemovePasswordComponent,
data: {
pageTitle: {
key: "verifyYourOrganization",
},
showBackButton: false,
pageIcon: LockIcon,
} satisfies ExtensionAnonLayoutWrapperData,
},
],
},
{
path: "view-cipher",
@@ -646,7 +662,7 @@ const routes: Routes = [
component: ConfirmKeyConnectorDomainComponent,
data: {
pageTitle: {
key: "confirmKeyConnectorDomain",
key: "verifyYourOrganization",
},
showBackButton: true,
pageIcon: DomainIcon,

View File

@@ -13,8 +13,11 @@
</bit-callout>
</div>
} @else {
<div [@routerTransition]="getRouteElevation(outlet)">
<!-- eslint-disable-next-line -->
<div class="tw-h-screen tw-w-screen">
<div [@routerTransition]="getRouteElevation(outlet)" class="tw-size-full">
<router-outlet #outlet="outlet"></router-outlet>
</div>
<bit-toast-container></bit-toast-container>
</div>
}

View File

@@ -28,7 +28,6 @@ import { CurrentAccountComponent } from "../auth/popup/account-switching/current
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { PopOutComponent } from "../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
@@ -85,13 +84,7 @@ import "../platform/popup/locales";
CalloutModule,
LinkModule,
],
declarations: [
AppComponent,
ColorPasswordPipe,
ColorPasswordCountPipe,
TabsV2Component,
RemovePasswordComponent,
],
declarations: [AppComponent, ColorPasswordPipe, ColorPasswordCountPipe, TabsV2Component],
exports: [CalloutModule],
providers: [CurrencyPipe, DatePipe],
bootstrap: [AppComponent],

View File

@@ -1,31 +1,21 @@
<popup-page [disablePadding]="true">
<popup-header
slot="header"
[background]="'alt'"
[showBackButton]="showBackButton"
[pageTitle]="''"
>
<div class="tw-w-32">
<bit-icon *ngIf="showLogo" [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-icon>
</div>
<ng-container slot="end">
<app-pop-out></app-pop-out>
<app-current-account *ngIf="showAcctSwitcher && hasLoggedInAccount"></app-current-account>
</ng-container>
</popup-header>
<auth-anon-layout
[title]="pageTitle"
[subtitle]="pageSubtitle"
[icon]="pageIcon"
[showReadonlyHostname]="showReadonlyHostname"
[hideLogo]="true"
[hideLogo]="!showLogo"
[maxWidth]="maxWidth"
[hideFooter]="hideFooter"
[hideCardWrapper]="hideCardWrapper"
>
<router-outlet></router-outlet>
<div class="tw-flex tw-gap-2" slot="header-actions">
<app-pop-out></app-pop-out>
@if (showAcctSwitcher && hasLoggedInAccount) {
<app-current-account></app-current-account>
}
</div>
<router-outlet slot="secondary" name="secondary"></router-outlet>
<router-outlet slot="environment-selector" name="environment-selector"></router-outlet>
</auth-anon-layout>

View File

@@ -76,11 +76,14 @@ const decorators = (options: {
{
provide: AccountService,
useValue: {
// We can't use mockAccountInfoWith() here because we can't take a dependency on @bitwarden/common/spec.
// This is because that package relies on jest dependencies that aren't available here.
activeAccount$: of({
id: "test-user-id" as UserId,
name: "Test User 1",
email: "test@email.com",
emailVerified: true,
creationDate: "2024-01-01T00:00:00.000Z",
}),
},
},
@@ -238,6 +241,11 @@ export const DefaultContentExample: Story = {
},
],
}),
parameters: {
chromatic: {
viewports: [380, 1280],
},
},
};
// Dynamic Content Example

View File

@@ -1,453 +0,0 @@
@import "variables.scss";
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html {
overflow: hidden;
min-height: 600px;
height: 100%;
&.body-sm {
min-height: 500px;
}
&.body-xs {
min-height: 400px;
}
&.body-xxs {
min-height: 300px;
}
&.body-3xs {
min-height: 240px;
}
&.body-full {
min-height: unset;
width: 100%;
height: 100%;
& body {
width: 100%;
}
}
}
html,
body {
font-family: $font-family-sans-serif;
font-size: $font-size-base;
line-height: $line-height-base;
-webkit-font-smoothing: antialiased;
}
body {
width: 380px;
height: 100%;
position: relative;
min-height: inherit;
overflow: hidden;
color: $text-color;
background-color: $background-color;
@include themify($themes) {
color: themed("textColor");
background-color: themed("backgroundColor");
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: $font-family-sans-serif;
font-size: $font-size-base;
font-weight: normal;
}
p {
margin-bottom: 10px;
}
ul,
ol {
margin-bottom: 10px;
}
img {
border: none;
}
a:not(popup-page a, popup-tab-navigation a) {
text-decoration: none;
@include themify($themes) {
color: themed("primaryColor");
}
&:hover,
&:focus {
@include themify($themes) {
color: darken(themed("primaryColor"), 6%);
}
}
}
input:not(bit-form-field input, bit-search input, input[bitcheckbox]),
select:not(bit-form-field select),
textarea:not(bit-form-field textarea) {
@include themify($themes) {
color: themed("textColor");
background-color: themed("inputBackgroundColor");
}
}
input:not(input[bitcheckbox]),
select,
textarea,
button:not(bit-chip-select button) {
font-size: $font-size-base;
font-family: $font-family-sans-serif;
}
input[type*="date"] {
@include themify($themes) {
color-scheme: themed("dateInputColorScheme");
}
}
::-webkit-calendar-picker-indicator {
@include themify($themes) {
filter: themed("webkitCalendarPickerFilter");
}
}
::-webkit-calendar-picker-indicator:hover {
@include themify($themes) {
filter: themed("webkitCalendarPickerHoverFilter");
}
cursor: pointer;
}
select {
width: 100%;
padding: 0.35rem;
}
button {
cursor: pointer;
}
textarea {
resize: vertical;
}
app-root > div {
height: 100%;
width: 100%;
}
main::-webkit-scrollbar,
cdk-virtual-scroll-viewport::-webkit-scrollbar,
.vault-select::-webkit-scrollbar {
width: 10px;
height: 10px;
}
main::-webkit-scrollbar-track,
.vault-select::-webkit-scrollbar-track {
background-color: transparent;
}
cdk-virtual-scroll-viewport::-webkit-scrollbar-track {
@include themify($themes) {
background-color: themed("backgroundColor");
}
}
main::-webkit-scrollbar-thumb,
cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb,
.vault-select::-webkit-scrollbar-thumb {
border-radius: 10px;
margin-right: 1px;
@include themify($themes) {
background-color: themed("scrollbarColor");
}
&:hover {
@include themify($themes) {
background-color: themed("scrollbarHoverColor");
}
}
}
header:not(bit-callout header, bit-dialog header, popup-page header) {
height: 44px;
display: flex;
&:not(.no-theme) {
border-bottom: 1px solid #000000;
@include themify($themes) {
color: themed("headerColor");
background-color: themed("headerBackgroundColor");
border-bottom-color: themed("headerBorderColor");
}
}
.header-content {
display: flex;
flex: 1 1 auto;
}
.header-content > .right,
.header-content > .right > .right {
height: 100%;
}
.left,
.right {
flex: 1;
display: flex;
min-width: -webkit-min-content; /* Workaround to Chrome bug */
.header-icon {
margin-right: 5px;
}
}
.right {
justify-content: flex-end;
align-items: center;
app-avatar {
max-height: 30px;
margin-right: 5px;
}
}
.center {
display: flex;
align-items: center;
text-align: center;
min-width: 0;
}
.login-center {
margin: auto;
}
app-pop-out > button,
div > button:not(app-current-account button):not(.home-acc-switcher-btn),
div > a {
border: none;
padding: 0 10px;
text-decoration: none;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 100%;
white-space: pre;
&:not(.home-acc-switcher-btn):hover,
&:not(.home-acc-switcher-btn):focus {
@include themify($themes) {
background-color: themed("headerBackgroundHoverColor");
color: themed("headerColor");
}
}
&[disabled] {
opacity: 0.65;
cursor: default !important;
background-color: inherit !important;
}
i + span {
margin-left: 5px;
}
}
app-pop-out {
display: flex;
padding-right: 0.5em;
}
.title {
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search {
padding: 7px 10px;
width: 100%;
text-align: left;
position: relative;
display: flex;
.bwi {
position: absolute;
top: 15px;
left: 20px;
@include themify($themes) {
color: themed("headerInputPlaceholderColor");
}
}
input:not(bit-form-field input) {
width: 100%;
margin: 0;
border: none;
padding: 5px 10px 5px 30px;
border-radius: $border-radius;
@include themify($themes) {
background-color: themed("headerInputBackgroundColor");
color: themed("headerInputColor");
}
&::selection {
@include themify($themes) {
// explicitly set text selection to invert foreground/background
background-color: themed("headerInputColor");
color: themed("headerInputBackgroundColor");
}
}
&:focus {
border-radius: $border-radius;
outline: none;
@include themify($themes) {
background-color: themed("headerInputBackgroundFocusColor");
}
}
&::-webkit-input-placeholder {
@include themify($themes) {
color: themed("headerInputPlaceholderColor");
}
}
/** make the cancel button visible in both dark/light themes **/
&[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
height: 15px;
width: 15px;
background-repeat: no-repeat;
mask-image: url("../images/close-button-white.svg");
-webkit-mask-image: url("../images/close-button-white.svg");
@include themify($themes) {
background-color: themed("headerInputColor");
}
}
}
}
.left + .search,
.left + .sr-only + .search {
padding-left: 0;
.bwi {
left: 10px;
}
}
.search + .right {
margin-left: -10px;
}
}
.content {
padding: 15px 5px;
}
app-root {
width: 100%;
height: 100vh;
display: flex;
@include themify($themes) {
background-color: themed("backgroundColor");
}
}
main:not(popup-page main):not(auth-anon-layout main) {
position: absolute;
top: 44px;
bottom: 0;
left: 0;
right: 0;
overflow-y: auto;
overflow-x: hidden;
@include themify($themes) {
background-color: themed("backgroundColor");
}
&.no-header {
top: 0;
}
&.flex {
display: flex;
flex-flow: column;
height: calc(100% - 44px);
}
}
.center-content,
.no-items,
.full-loading-spinner {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex-direction: column;
flex-grow: 1;
}
.no-items,
.full-loading-spinner {
text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.no-items-image {
@include themify($themes) {
content: url("../images/search-desktop" + themed("svgSuffix"));
}
}
.bwi {
margin-bottom: 10px;
@include themify($themes) {
color: themed("disabledIconColor");
}
}
}
// cdk-virtual-scroll
.cdk-virtual-scroll-viewport {
width: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
}
.cdk-virtual-scroll-content-wrapper {
width: 100%;
}

View File

@@ -1,620 +0,0 @@
@import "variables.scss";
.box {
position: relative;
width: 100%;
&.first {
margin-top: 0;
}
.box-header {
margin: 0 10px 5px 10px;
text-transform: uppercase;
display: flex;
@include themify($themes) {
color: themed("headingColor");
}
}
.box-content {
@include themify($themes) {
background-color: themed("backgroundColor");
border-color: themed("borderColor");
}
&.box-content-padded {
padding: 10px 15px;
}
&.condensed .box-content-row,
.box-content-row.condensed {
padding-top: 5px;
padding-bottom: 5px;
}
&.no-hover .box-content-row,
.box-content-row.no-hover {
&:hover,
&:focus {
@include themify($themes) {
background-color: themed("boxBackgroundColor") !important;
}
}
}
&.single-line .box-content-row,
.box-content-row.single-line {
padding-top: 10px;
padding-bottom: 10px;
margin: 5px;
}
&.row-top-padding {
padding-top: 10px;
}
}
.box-footer {
margin: 0 5px 5px 5px;
padding: 0 10px 5px 10px;
font-size: $font-size-small;
button.btn {
font-size: $font-size-small;
padding: 0;
}
button.btn.primary {
font-size: $font-size-base;
padding: 7px 15px;
width: 100%;
&:hover {
@include themify($themes) {
border-color: themed("borderHoverColor") !important;
}
}
}
@include themify($themes) {
color: themed("mutedColor");
}
}
&.list {
margin: 10px 0 20px 0;
.box-content {
.virtual-scroll-item {
display: inline-block;
width: 100%;
}
.box-content-row {
text-decoration: none;
border-radius: $border-radius;
// background-color: $background-color;
@include themify($themes) {
color: themed("textColor");
background-color: themed("boxBackgroundColor");
}
&.padded {
padding-top: 10px;
padding-bottom: 10px;
}
&.no-hover {
&:hover {
@include themify($themes) {
background-color: themed("boxBackgroundColor") !important;
}
}
}
&:hover,
&:focus,
&.active {
@include themify($themes) {
background-color: themed("listItemBackgroundHoverColor");
}
}
&:focus {
border-left: 5px solid #000000;
padding-left: 5px;
@include themify($themes) {
border-left-color: themed("mutedColor");
}
}
.action-buttons {
.row-btn {
padding-left: 5px;
padding-right: 5px;
}
}
.text:not(.no-ellipsis),
.detail {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row-main {
display: flex;
min-width: 0;
align-items: normal;
.row-main-content {
min-width: 0;
}
}
}
&.single-line {
.box-content-row {
display: flex;
padding-top: 10px;
padding-bottom: 10px;
margin: 5px;
border-radius: $border-radius;
}
}
}
}
}
.box-content-row {
display: block;
padding: 5px 10px;
position: relative;
z-index: 1;
border-radius: $border-radius;
margin: 3px 5px;
@include themify($themes) {
background-color: themed("boxBackgroundColor");
}
&:last-child {
&:before {
border: none;
height: 0;
}
}
&.override-last:last-child:before {
border-bottom: 1px solid #000000;
@include themify($themes) {
border-bottom-color: themed("boxBorderColor");
}
}
&.last:last-child:before {
border-bottom: 1px solid #000000;
@include themify($themes) {
border-bottom-color: themed("boxBorderColor");
}
}
&:after {
content: "";
display: table;
clear: both;
}
&:hover,
&:focus,
&.active {
@include themify($themes) {
background-color: themed("boxBackgroundHoverColor");
}
}
&.pre {
white-space: pre;
overflow-x: auto;
}
&.pre-wrap {
white-space: pre-wrap;
overflow-x: auto;
}
.row-label,
label {
font-size: $font-size-small;
display: block;
width: 100%;
margin-bottom: 5px;
@include themify($themes) {
color: themed("mutedColor");
}
.sub-label {
margin-left: 10px;
}
}
.flex-label {
font-size: $font-size-small;
display: flex;
flex-grow: 1;
margin-bottom: 5px;
@include themify($themes) {
color: themed("mutedColor");
}
> a {
flex-grow: 0;
}
}
.text,
.detail {
display: block;
text-align: left;
@include themify($themes) {
color: themed("textColor");
}
}
.detail {
font-size: $font-size-small;
@include themify($themes) {
color: themed("mutedColor");
}
}
.img-right,
.txt-right {
float: right;
margin-left: 10px;
}
.row-main {
flex-grow: 1;
min-width: 0;
}
&.box-content-row-flex,
.box-content-row-flex,
&.box-content-row-checkbox,
&.box-content-row-link,
&.box-content-row-input,
&.box-content-row-slider,
&.box-content-row-multi {
display: flex;
align-items: center;
word-break: break-all;
&.box-content-row-word-break {
word-break: normal;
}
}
&.box-content-row-multi {
input:not([type="checkbox"]) {
width: 100%;
}
input + label.sr-only + select {
margin-top: 5px;
}
> a,
> button {
padding: 8px 8px 8px 4px;
margin: 0;
@include themify($themes) {
color: themed("dangerColor");
}
}
}
&.box-content-row-multi,
&.box-content-row-newmulti {
padding-left: 10px;
}
&.box-content-row-newmulti {
@include themify($themes) {
color: themed("primaryColor");
}
}
&.box-content-row-checkbox,
&.box-content-row-link,
&.box-content-row-input,
&.box-content-row-slider {
padding-top: 10px;
padding-bottom: 10px;
margin: 5px;
label,
.row-label {
font-size: $font-size-base;
display: block;
width: initial;
margin-bottom: 0;
@include themify($themes) {
color: themed("textColor");
}
}
> span {
@include themify($themes) {
color: themed("mutedColor");
}
}
> input {
margin: 0 0 0 auto;
padding: 0;
}
> * {
margin-right: 15px;
&:last-child {
margin-right: 0;
}
}
}
&.box-content-row-checkbox-left {
justify-content: flex-start;
> input {
margin: 0 15px 0 0;
}
}
&.box-content-row-input {
label {
white-space: nowrap;
}
input {
text-align: right;
&[type="number"] {
max-width: 50px;
}
}
}
&.box-content-row-slider {
input[type="range"] {
height: 10px;
}
input[type="number"] {
width: 45px;
}
label {
white-space: nowrap;
}
}
input:not([type="checkbox"]):not([type="radio"]),
textarea {
border: none;
width: 100%;
background-color: transparent !important;
&::-webkit-input-placeholder {
@include themify($themes) {
color: themed("inputPlaceholderColor");
}
}
&:not([type="file"]):focus {
outline: none;
}
}
select {
width: 100%;
border: 1px solid #000000;
border-radius: $border-radius;
padding: 7px 4px;
@include themify($themes) {
border-color: themed("inputBorderColor");
}
}
.action-buttons {
display: flex;
margin-left: 5px;
&.action-buttons-fixed {
align-self: start;
margin-top: 2px;
}
.row-btn {
cursor: pointer;
padding: 10px 8px;
background: none;
border: none;
@include themify($themes) {
color: themed("boxRowButtonColor");
}
&:hover,
&:focus {
@include themify($themes) {
color: themed("boxRowButtonHoverColor");
}
}
&.disabled,
&[disabled] {
@include themify($themes) {
color: themed("disabledIconColor");
opacity: themed("disabledBoxOpacity");
}
&:hover {
@include themify($themes) {
color: themed("disabledIconColor");
opacity: themed("disabledBoxOpacity");
}
}
cursor: default !important;
}
}
&.no-pad .row-btn {
padding-top: 0;
padding-bottom: 0;
}
}
&:not(.box-draggable-row) {
.action-buttons .row-btn:last-child {
margin-right: -3px;
}
}
&.box-draggable-row {
&.box-content-row-checkbox {
input[type="checkbox"] + .drag-handle {
margin-left: 10px;
}
}
}
.drag-handle {
cursor: move;
padding: 10px 2px 10px 8px;
user-select: none;
@include themify($themes) {
color: themed("mutedColor");
}
}
&.cdk-drag-preview {
position: relative;
display: flex;
align-items: center;
opacity: 0.8;
@include themify($themes) {
background-color: themed("boxBackgroundColor");
}
}
select.field-type {
margin: 5px 0 0 25px;
width: calc(100% - 25px);
}
.icon {
display: flex;
justify-content: center;
align-items: center;
min-width: 34px;
margin-left: -5px;
@include themify($themes) {
color: themed("mutedColor");
}
&.icon-small {
min-width: 25px;
}
img {
border-radius: $border-radius;
max-height: 20px;
max-width: 20px;
}
}
.progress {
display: flex;
height: 5px;
overflow: hidden;
margin: 5px -15px -10px;
.progress-bar {
display: flex;
flex-direction: column;
justify-content: center;
white-space: nowrap;
background-color: $brand-primary;
}
}
.radio-group {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 5px;
input {
flex-grow: 0;
}
label {
margin: 0 0 0 5px;
flex-grow: 1;
font-size: $font-size-base;
display: block;
width: 100%;
@include themify($themes) {
color: themed("textColor");
}
}
&.align-start {
align-items: start;
margin-top: 10px;
label {
margin-top: -4px;
}
}
}
}
.truncate {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
form {
.box {
.box-content {
.box-content-row {
&.no-hover {
&:hover {
@include themify($themes) {
background-color: themed("transparentColor") !important;
}
}
}
}
}
}
}

View File

@@ -1,118 +0,0 @@
@import "variables.scss";
.btn {
border-radius: $border-radius;
padding: 7px 15px;
border: 1px solid #000000;
font-size: $font-size-base;
text-align: center;
cursor: pointer;
@include themify($themes) {
background-color: themed("buttonBackgroundColor");
border-color: themed("buttonBorderColor");
color: themed("buttonColor");
}
&.primary {
@include themify($themes) {
color: themed("buttonPrimaryColor");
}
}
&.danger {
@include themify($themes) {
color: themed("buttonDangerColor");
}
}
&.callout-half {
font-weight: bold;
max-width: 50%;
}
&:hover:not([disabled]) {
cursor: pointer;
@include themify($themes) {
background-color: darken(themed("buttonBackgroundColor"), 1.5%);
border-color: darken(themed("buttonBorderColor"), 17%);
color: darken(themed("buttonColor"), 10%);
}
&.primary {
@include themify($themes) {
color: darken(themed("buttonPrimaryColor"), 6%);
}
}
&.danger {
@include themify($themes) {
color: darken(themed("buttonDangerColor"), 6%);
}
}
}
&:focus:not([disabled]) {
cursor: pointer;
outline: 0;
@include themify($themes) {
background-color: darken(themed("buttonBackgroundColor"), 6%);
border-color: darken(themed("buttonBorderColor"), 25%);
}
}
&[disabled] {
opacity: 0.65;
cursor: default !important;
}
&.block {
display: block;
width: calc(100% - 10px);
margin: 0 auto;
}
&.link,
&.neutral {
border: none !important;
background: none !important;
&:focus {
text-decoration: underline;
}
}
}
.action-buttons {
.btn {
&:focus {
outline: auto;
}
}
}
button.box-content-row {
display: block;
width: calc(100% - 10px);
text-align: left;
border-color: none;
@include themify($themes) {
background-color: themed("boxBackgroundColor");
}
}
button {
border: none;
background: transparent;
color: inherit;
}
.login-buttons {
.btn.block {
width: 100%;
margin-bottom: 10px;
}
}

View File

@@ -1,43 +0,0 @@
@import "variables.scss";
html.browser_safari {
&.safari_height_fix {
body {
height: 360px !important;
&.body-xs {
height: 300px !important;
}
&.body-full {
height: 100% !important;
}
}
}
header {
.search .bwi {
left: 20px;
}
.left + .search .bwi {
left: 10px;
}
}
.content {
&.login-page {
padding-top: 100px;
}
}
app-root {
border-width: 1px;
border-style: solid;
border-color: #000000;
}
&.theme_light app-root {
border-color: #777777;
}
}

View File

@@ -1,11 +0,0 @@
.row {
display: flex;
margin: 0 -15px;
width: 100%;
}
.col {
flex-basis: 0;
flex-grow: 1;
padding: 0 15px;
}

View File

@@ -1,348 +0,0 @@
@import "variables.scss";
small,
.small {
font-size: $font-size-small;
}
.bg-primary {
@include themify($themes) {
background-color: themed("primaryColor") !important;
}
}
.bg-success {
@include themify($themes) {
background-color: themed("successColor") !important;
}
}
.bg-danger {
@include themify($themes) {
background-color: themed("dangerColor") !important;
}
}
.bg-info {
@include themify($themes) {
background-color: themed("infoColor") !important;
}
}
.bg-warning {
@include themify($themes) {
background-color: themed("warningColor") !important;
}
}
.text-primary {
@include themify($themes) {
color: themed("primaryColor") !important;
}
}
.text-success {
@include themify($themes) {
color: themed("successColor") !important;
}
}
.text-muted {
@include themify($themes) {
color: themed("mutedColor") !important;
}
}
.text-default {
@include themify($themes) {
color: themed("textColor") !important;
}
}
.text-danger {
@include themify($themes) {
color: themed("dangerColor") !important;
}
}
.text-info {
@include themify($themes) {
color: themed("infoColor") !important;
}
}
.text-warning {
@include themify($themes) {
color: themed("warningColor") !important;
}
}
.text-center {
text-align: center;
}
.font-weight-semibold {
font-weight: 600;
}
p.lead {
font-size: $font-size-large;
margin-bottom: 20px;
font-weight: normal;
}
.flex-right {
margin-left: auto;
}
.flex-bottom {
margin-top: auto;
}
.no-margin {
margin: 0 !important;
}
.display-block {
display: block !important;
}
.monospaced {
font-family: $font-family-monospace;
}
.show-whitespace {
white-space: pre-wrap;
}
.img-responsive {
display: block;
max-width: 100%;
height: auto;
}
.img-rounded {
border-radius: $border-radius;
}
.select-index-top {
position: relative;
z-index: 100;
}
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
border: 0 !important;
}
:not(:focus) > .exists-only-on-parent-focus {
display: none;
}
.password-wrapper {
overflow-wrap: break-word;
white-space: pre-wrap;
min-width: 0;
}
.password-number {
@include themify($themes) {
color: themed("passwordNumberColor");
}
}
.password-special {
@include themify($themes) {
color: themed("passwordSpecialColor");
}
}
.password-character {
display: inline-flex;
flex-direction: column;
align-items: center;
width: 30px;
height: 36px;
font-weight: 600;
&:nth-child(odd) {
@include themify($themes) {
background-color: themed("backgroundColor");
}
}
}
.password-count {
white-space: nowrap;
font-size: 8px;
@include themify($themes) {
color: themed("passwordCountText") !important;
}
}
#duo-frame {
background: url("../images/loading.svg") 0 0 no-repeat;
width: 100%;
height: 470px;
margin-bottom: -10px;
iframe {
width: 100%;
height: 100%;
border: none;
}
}
#web-authn-frame {
width: 100%;
height: 40px;
iframe {
border: none;
height: 100%;
width: 100%;
}
}
body.linux-webauthn {
width: 485px !important;
#web-authn-frame {
iframe {
width: 375px;
margin: 0 55px;
}
}
}
app-root > #loading {
display: flex;
text-align: center;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
color: $text-muted;
@include themify($themes) {
color: themed("mutedColor");
}
}
app-vault-icon,
.app-vault-icon {
display: flex;
}
.logo-image {
margin: 0 auto;
width: 142px;
height: 21px;
background-size: 142px 21px;
background-repeat: no-repeat;
@include themify($themes) {
background-image: url("../images/logo-" + themed("logoSuffix") + "@2x.png");
}
@media (min-width: 219px) {
width: 189px;
height: 28px;
background-size: 189px 28px;
}
@media (min-width: 314px) {
width: 284px;
height: 43px;
background-size: 284px 43px;
}
}
[hidden] {
display: none !important;
}
.draggable {
cursor: move;
}
input[type="password"]::-ms-reveal {
display: none;
}
.flex {
display: flex;
&.flex-grow {
> * {
flex: 1;
}
}
}
// Text selection styles
// Set explicit selection styles (assumes primary accent color has sufficient
// contrast against the background, so its inversion is also still readable)
// and suppress user selection for most elements (to make it more app-like)
:not(bit-form-field input)::selection {
@include themify($themes) {
color: themed("backgroundColor");
background-color: themed("primaryAccentColor");
}
}
h1,
h2,
h3,
label,
a,
button,
p,
img,
.box-header,
.box-footer,
.callout,
.row-label,
.modal-title,
.overlay-container {
user-select: none;
&.user-select {
user-select: auto;
}
}
/* tweak for inconsistent line heights in cipher view */
.box-footer button,
.box-footer a {
line-height: 1;
}
// Workaround for slow performance on external monitors on Chrome + MacOS
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=971701#c64
@keyframes redraw {
0% {
opacity: 0.99;
}
100% {
opacity: 1;
}
}
html.force_redraw {
animation: redraw 1s linear infinite;
}
/* override for vault icon in browser (pre extension refresh) */
app-vault-icon:not(app-vault-list-items-container app-vault-icon) > div {
display: flex;
justify-content: center;
align-items: center;
float: left;
height: 36px;
width: 34px;
margin-left: -5px;
}

View File

@@ -1,144 +0,0 @@
@import "variables.scss";
app-home {
position: fixed;
height: 100%;
width: 100%;
.center-content {
margin-top: -50px;
height: calc(100% + 50px);
}
img {
width: 284px;
margin: 0 auto;
}
p.lead {
margin: 30px 0;
}
.btn + .btn {
margin-top: 10px;
}
button.settings-icon {
position: absolute;
top: 10px;
left: 10px;
@include themify($themes) {
color: themed("mutedColor");
}
&:not(:hover):not(:focus) {
span {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
}
&:hover,
&:focus {
text-decoration: none;
@include themify($themes) {
color: themed("primaryColor");
}
}
}
}
body.body-sm,
body.body-xs {
app-home {
.center-content {
margin-top: 0;
height: 100%;
}
p.lead {
margin: 15px 0;
}
}
}
body.body-full {
app-home {
.center-content {
margin-top: -80px;
height: calc(100% + 80px);
}
}
}
.createAccountLink {
padding: 30px 10px 0 10px;
}
.remember-email-check {
padding-top: 18px;
padding-left: 10px;
padding-bottom: 18px;
}
.login-buttons > button {
margin: 15px 0 15px 0;
}
.useBrowserlink {
margin-left: 5px;
margin-top: 20px;
span {
font-weight: 700;
font-size: $font-size-small;
}
}
.fido2-browser-selector-dropdown {
@include themify($themes) {
background-color: themed("boxBackgroundColor");
}
padding: 8px;
width: 100%;
box-shadow:
0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12),
0 1px 5px 0 rgba(0, 0, 0, 0.2);
border-radius: $border-radius;
}
.fido2-browser-selector-dropdown-item {
@include themify($themes) {
color: themed("textColor") !important;
}
width: 100%;
text-align: left;
padding: 0px 15px 0px 5px;
margin-bottom: 5px;
border-radius: 3px;
border: 1px solid transparent;
transition: all 0.2s ease-in-out;
&:hover {
@include themify($themes) {
background-color: themed("listItemBackgroundHoverColor") !important;
}
}
&:last-child {
margin-bottom: 0;
}
}
/** Temporary fix for avatar, will not be required once we migrate to tailwind preflight **/
bit-avatar svg {
display: block;
}

View File

@@ -1,23 +0,0 @@
@import "variables.scss";
@each $mfaType in $mfaTypes {
.mfaType#{$mfaType} {
content: url("../images/two-factor/" + $mfaType + ".png");
max-width: 100px;
}
}
.mfaType1 {
@include themify($themes) {
content: url("../images/two-factor/1" + themed("mfaLogoSuffix"));
max-width: 100px;
max-height: 45px;
}
}
.mfaType7 {
@include themify($themes) {
content: url("../images/two-factor/7" + themed("mfaLogoSuffix"));
max-width: 100px;
}
}

View File

@@ -1,13 +1,50 @@
@import "../../../../../libs/angular/src/scss/bwicons/styles/style.scss";
@import "variables.scss";
@import "../../../../../libs/angular/src/scss/icons.scss";
@import "base.scss";
@import "grid.scss";
@import "box.scss";
@import "buttons.scss";
@import "misc.scss";
@import "environment.scss";
@import "pages.scss";
@import "plugins.scss";
@import "@angular/cdk/overlay-prebuilt.css";
@import "../../../../../libs/components/src/multi-select/scss/bw.theme";
.cdk-virtual-scroll-content-wrapper {
width: 100%;
}
// MFA Types for logo styling with no dark theme alternative
$mfaTypes: 0, 2, 3, 4, 6;
@each $mfaType in $mfaTypes {
.mfaType#{$mfaType} {
content: url("../images/two-factor/" + $mfaType + ".png");
max-width: 100px;
}
}
.mfaType0 {
content: url("../images/two-factor/0.png");
max-width: 100px;
max-height: 45px;
}
.mfaType1 {
max-width: 100px;
max-height: 45px;
&:is(.theme_light *) {
content: url("../images/two-factor/1.png");
}
&:is(.theme_dark *) {
content: url("../images/two-factor/1-w.png");
}
}
.mfaType7 {
max-width: 100px;
&:is(.theme_light *) {
content: url("../images/two-factor/7.png");
}
&:is(.theme_dark *) {
content: url("../images/two-factor/7-w.png");
}
}

View File

@@ -1,4 +1,104 @@
@import "../../../../../libs/components/src/tw-theme.css";
@import "../../../../../libs/components/src/tw-theme-preflight.css";
@layer base {
html {
overflow: hidden;
min-height: 600px;
height: 100%;
&.body-sm {
min-height: 500px;
}
&.body-xs {
min-height: 400px;
}
&.body-xxs {
min-height: 300px;
}
&.body-3xs {
min-height: 240px;
}
&.body-full {
min-height: unset;
width: 100%;
height: 100%;
& body {
width: 100%;
}
}
}
html.browser_safari {
&.safari_height_fix {
body {
height: 360px !important;
&.body-xs {
height: 300px !important;
}
&.body-full {
height: 100% !important;
}
}
}
app-root {
border-width: 1px;
border-style: solid;
border-color: #000000;
}
&.theme_light app-root {
border-color: #777777;
}
}
body {
width: 380px;
height: 100%;
position: relative;
min-height: inherit;
overflow: hidden;
@apply tw-bg-background-alt;
}
/**
* Workaround for slow performance on external monitors on Chrome + MacOS
* See: https://bugs.chromium.org/p/chromium/issues/detail?id=971701#c64
*/
@keyframes redraw {
0% {
opacity: 0.99;
}
100% {
opacity: 1;
}
}
html.force_redraw {
animation: redraw 1s linear infinite;
}
/**
* Text selection style:
* suppress user selection for most elements (to make it more app-like)
*/
h1,
h2,
h3,
label,
a,
button,
p,
img {
user-select: none;
}
}
@layer components {
/** Safari Support */
@@ -19,4 +119,59 @@
html:not(.browser_safari) .tw-styled-scrollbar {
scrollbar-color: rgb(var(--color-secondary-500)) rgb(var(--color-background-alt));
}
#duo-frame {
background: url("../images/loading.svg") 0 0 no-repeat;
width: 100%;
height: 470px;
margin-bottom: -10px;
iframe {
width: 100%;
height: 100%;
border: none;
}
}
#web-authn-frame {
width: 100%;
height: 40px;
iframe {
border: none;
height: 100%;
width: 100%;
}
}
body.linux-webauthn {
width: 485px !important;
#web-authn-frame {
iframe {
width: 375px;
margin: 0 55px;
}
}
}
app-root > #loading {
display: flex;
text-align: center;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
@apply tw-text-muted;
}
/**
* Text selection style:
* Set explicit selection styles (assumes primary accent color has sufficient
* contrast against the background, so its inversion is also still readable)
*/
:not(bit-form-field input)::selection {
@apply tw-text-contrast;
@apply tw-bg-primary-700;
}
}

View File

@@ -1,178 +1,42 @@
$dark-icon-themes: "theme_dark";
/**
* DEPRECATED: DO NOT MODIFY OR USE!
*/
$dark-icon-themes: "theme_dark";
$font-family-sans-serif: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
$font-size-base: 16px;
$font-size-large: 18px;
$font-size-xlarge: 22px;
$font-size-xxlarge: 28px;
$font-size-small: 12px;
$text-color: #000000;
$border-color: #f0f0f0;
$border-color-dark: #ddd;
$list-item-hover: #fbfbfb;
$list-icon-color: #767679;
$disabled-box-opacity: 1;
$border-radius: 6px;
$line-height-base: 1.42857143;
$icon-hover-color: lighten($text-color, 50%);
$mfaTypes: 0, 2, 3, 4, 6;
$gray: #555;
$gray-light: #777;
$text-muted: $gray-light;
$brand-primary: #175ddc;
$brand-danger: #c83522;
$brand-success: #017e45;
$brand-info: #555555;
$brand-warning: #8b6609;
$brand-primary-accent: #1252a3;
$background-color: #f0f0f0;
$box-background-color: white;
$box-background-hover-color: $list-item-hover;
$box-border-color: $border-color;
$border-color-alt: #c3c5c7;
$button-border-color: darken($border-color-dark, 12%);
$button-background-color: white;
$button-color: lighten($text-color, 40%);
$button-color-primary: darken($brand-primary, 8%);
$button-color-danger: darken($brand-danger, 10%);
$code-color: #c01176;
$code-color-dark: #f08dc7;
$themes: (
light: (
textColor: $text-color,
hoverColorTransparent: rgba($text-color, 0.15),
borderColor: $border-color-dark,
backgroundColor: $background-color,
borderColorAlt: $border-color-alt,
backgroundColorAlt: #ffffff,
scrollbarColor: rgba(100, 100, 100, 0.2),
scrollbarHoverColor: rgba(100, 100, 100, 0.4),
boxBackgroundColor: $box-background-color,
boxBackgroundHoverColor: $box-background-hover-color,
boxBorderColor: $box-border-color,
tabBackgroundColor: #ffffff,
tabBackgroundHoverColor: $list-item-hover,
headerColor: #ffffff,
headerBackgroundColor: $brand-primary,
headerBackgroundHoverColor: rgba(255, 255, 255, 0.1),
headerBorderColor: $brand-primary,
headerInputBackgroundColor: darken($brand-primary, 8%),
headerInputBackgroundFocusColor: darken($brand-primary, 10%),
headerInputColor: #ffffff,
headerInputPlaceholderColor: lighten($brand-primary, 35%),
listItemBackgroundHoverColor: $list-item-hover,
disabledIconColor: $list-icon-color,
disabledBoxOpacity: $disabled-box-opacity,
headingColor: $gray-light,
labelColor: $gray-light,
mutedColor: $text-muted,
totpStrokeColor: $brand-primary,
boxRowButtonColor: $brand-primary,
boxRowButtonHoverColor: darken($brand-primary, 10%),
inputBorderColor: darken($border-color-dark, 7%),
inputBackgroundColor: #ffffff,
inputPlaceholderColor: lighten($gray-light, 35%),
buttonBackgroundColor: $button-background-color,
buttonBorderColor: $button-border-color,
buttonColor: $button-color,
buttonPrimaryColor: $button-color-primary,
buttonDangerColor: $button-color-danger,
primaryColor: $brand-primary,
primaryAccentColor: $brand-primary-accent,
dangerColor: $brand-danger,
successColor: $brand-success,
infoColor: $brand-info,
warningColor: $brand-warning,
logoSuffix: "dark",
mfaLogoSuffix: ".png",
passwordNumberColor: #007fde,
passwordSpecialColor: #c40800,
passwordCountText: #212529,
calloutBorderColor: $border-color-dark,
calloutBackgroundColor: $box-background-color,
toastTextColor: #ffffff,
svgSuffix: "-light.svg",
transparentColor: rgba(0, 0, 0, 0),
dateInputColorScheme: light,
// https://stackoverflow.com/a/53336754
webkitCalendarPickerFilter: invert(46%) sepia(69%) saturate(6397%) hue-rotate(211deg)
brightness(85%) contrast(103%),
// light has no hover so use same color
webkitCalendarPickerHoverFilter: invert(46%) sepia(69%) saturate(6397%) hue-rotate(211deg)
brightness(85%) contrast(103%),
codeColor: $code-color,
),
dark: (
textColor: #ffffff,
hoverColorTransparent: rgba($text-color, 0.15),
borderColor: #161c26,
backgroundColor: #161c26,
borderColorAlt: #6e788a,
backgroundColorAlt: #2f343d,
scrollbarColor: #6e788a,
scrollbarHoverColor: #8d94a5,
boxBackgroundColor: #2f343d,
boxBackgroundHoverColor: #3c424e,
boxBorderColor: #4c525f,
tabBackgroundColor: #2f343d,
tabBackgroundHoverColor: #3c424e,
headerColor: #ffffff,
headerBackgroundColor: #2f343d,
headerBackgroundHoverColor: #3c424e,
headerBorderColor: #161c26,
headerInputBackgroundColor: #3c424e,
headerInputBackgroundFocusColor: #4c525f,
headerInputColor: #ffffff,
headerInputPlaceholderColor: #bac0ce,
listItemBackgroundHoverColor: #3c424e,
disabledIconColor: #bac0ce,
disabledBoxOpacity: 0.5,
headingColor: #bac0ce,
labelColor: #bac0ce,
mutedColor: #bac0ce,
totpStrokeColor: #4c525f,
boxRowButtonColor: #bac0ce,
boxRowButtonHoverColor: #ffffff,
inputBorderColor: #4c525f,
inputBackgroundColor: #2f343d,
inputPlaceholderColor: #bac0ce,
buttonBackgroundColor: #3c424e,
buttonBorderColor: #4c525f,
buttonColor: #bac0ce,
buttonPrimaryColor: #6f9df1,
buttonDangerColor: #ff8d85,
primaryColor: #6f9df1,
primaryAccentColor: #6f9df1,
dangerColor: #ff8d85,
successColor: #52e07c,
infoColor: #a4b0c6,
warningColor: #ffeb66,
logoSuffix: "white",
mfaLogoSuffix: "-w.png",
passwordNumberColor: #6f9df1,
passwordSpecialColor: #ff8d85,
passwordCountText: #ffffff,
calloutBorderColor: #4c525f,
calloutBackgroundColor: #3c424e,
toastTextColor: #1f242e,
svgSuffix: "-dark.svg",
transparentColor: rgba(0, 0, 0, 0),
dateInputColorScheme: dark,
// https://stackoverflow.com/a/53336754 - must prepend brightness(0) saturate(100%) to dark themed date inputs
webkitCalendarPickerFilter: brightness(0) saturate(100%) invert(86%) sepia(19%) saturate(152%)
hue-rotate(184deg) brightness(87%) contrast(93%),
webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(100%) sepia(0%)
saturate(0%) hue-rotate(93deg) brightness(103%) contrast(103%),
codeColor: $code-color-dark,
),
);

View File

@@ -136,6 +136,7 @@ import {
DialogService,
ToastService,
} from "@bitwarden/components";
import { GeneratorServicesModule } from "@bitwarden/generator-components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import {
BiometricsService,
@@ -743,7 +744,7 @@ const safeProviders: SafeProvider[] = [
];
@NgModule({
imports: [JslibServicesModule],
imports: [JslibServicesModule, GeneratorServicesModule],
declarations: [],
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function
providers: safeProviders,

View File

@@ -16,6 +16,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
@@ -96,10 +97,11 @@ describe("SendV2Component", () => {
useValue: {
activeAccount$: of({
id: "123",
...mockAccountInfoWith({
email: "test@email.com",
emailVerified: true,
name: "Test User",
}),
}),
},
},
{ provide: AuthService, useValue: mock<AuthService>() },

View File

@@ -1,5 +1,5 @@
<popup-page>
<popup-header slot="header" [pageTitle]="'exportVault' | i18n" showBackButton>
<popup-header slot="header" [pageTitle]="'export' | i18n" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>
@@ -21,7 +21,7 @@
bitFormButton
buttonType="primary"
>
{{ "exportVault" | i18n }}
{{ "export" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" [popupBackAction]>
{{ "cancel" | i18n }}

View File

@@ -1,5 +1,5 @@
<popup-page>
<popup-header slot="header" [pageTitle]="'importData' | i18n" showBackButton>
<popup-header slot="header" [pageTitle]="'import' | i18n" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>
@@ -22,7 +22,7 @@
bitFormButton
buttonType="primary"
>
{{ "importData" | i18n }}
{{ "import" | i18n }}
</button>
</popup-footer>
</popup-page>

View File

@@ -51,6 +51,6 @@ export class AttachmentsV2Component {
/** Navigate the user back to the edit screen after uploading an attachment */
async navigateBack() {
await this.popupRouterCacheService.back();
await this.popupRouterCacheService.back(true);
}
}

View File

@@ -11,6 +11,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
@@ -60,10 +61,11 @@ describe("OpenAttachmentsComponent", () => {
const accountService = {
activeAccount$: of({
id: mockUserId,
...mockAccountInfoWith({
email: "test@email.com",
emailVerified: true,
name: "Test User",
}),
}),
};
const formStatusChange$ = new BehaviorSubject<"enabled" | "disabled">("enabled");

View File

@@ -15,7 +15,7 @@
<bit-item>
<button type="button" bit-item-content (click)="import()">
<div class="tw-flex tw-items-center tw-justify-center tw-gap-2">
<p>{{ "importItems" | i18n }}</p>
<p>{{ "import" | i18n }}</p>
<span
*ngIf="emptyVaultImportBadge$ | async"
bitBadge
@@ -30,7 +30,7 @@
</bit-item>
<bit-item>
<a bit-item-content routerLink="/export">
{{ "exportVault" | i18n }}
{{ "export" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
@@ -64,7 +64,7 @@
</bit-item>
<bit-item>
<button type="button" bit-item-content (click)="sync()">
{{ "syncVaultNow" | i18n }}
{{ "syncNow" | i18n }}
<span slot="secondary">{{ lastSync }}</span>
<i slot="end" class="bwi bwi-refresh" aria-hidden="true"></i>
</button>

View File

@@ -12,5 +12,6 @@ config.content = [
"../../libs/vault/src/**/*.{html,ts}",
"../../libs/pricing/src/**/*.{html,ts}",
];
config.corePlugins.preflight = true;
module.exports = config;

View File

@@ -113,20 +113,14 @@ export class LoginCommand {
} else if (options.sso != null && this.canInteract) {
// If the optional Org SSO Identifier isn't provided, the option value is `true`.
const orgSsoIdentifier = options.sso === true ? null : options.sso;
const passwordOptions: any = {
type: "password",
length: 64,
uppercase: true,
lowercase: true,
numbers: true,
special: false,
};
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
const ssoPromptData = await this.makeSsoPromptData();
ssoCodeVerifier = ssoPromptData.ssoCodeVerifier;
try {
const ssoParams = await this.openSsoPrompt(codeChallenge, state, orgSsoIdentifier);
const ssoParams = await this.openSsoPrompt(
ssoPromptData.codeChallenge,
ssoPromptData.state,
orgSsoIdentifier,
);
ssoCode = ssoParams.ssoCode;
orgIdentifier = ssoParams.orgIdentifier;
} catch {
@@ -231,9 +225,43 @@ export class LoginCommand {
new PasswordLoginCredentials(email, password, twoFactor),
);
}
// Begin Acting on initial AuthResult
if (response.requiresEncryptionKeyMigration) {
return Response.error(this.i18nService.t("legacyEncryptionUnsupported"));
}
// Opting for not checking feature flag since the server will not respond with
// SsoOrganizationIdentifier if the feature flag is not enabled.
if (response.requiresSso && this.canInteract) {
const ssoPromptData = await this.makeSsoPromptData();
ssoCodeVerifier = ssoPromptData.ssoCodeVerifier;
try {
const ssoParams = await this.openSsoPrompt(
ssoPromptData.codeChallenge,
ssoPromptData.state,
response.ssoOrganizationIdentifier,
);
ssoCode = ssoParams.ssoCode;
orgIdentifier = ssoParams.orgIdentifier;
if (ssoCode != null && ssoCodeVerifier != null) {
response = await this.loginStrategyService.logIn(
new SsoLoginCredentials(
ssoCode,
ssoCodeVerifier,
this.ssoRedirectUri,
orgIdentifier,
undefined, // email to look up 2FA token not required as CLI can't remember 2FA token
twoFactor,
),
);
}
} catch {
return Response.badRequest("Something went wrong. Try again.");
}
}
if (response.requiresTwoFactor) {
const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null);
if (twoFactorProviders.length === 0) {
@@ -279,6 +307,10 @@ export class LoginCommand {
if (twoFactorToken == null && selectedProvider.type === TwoFactorProviderType.Email) {
const emailReq = new TwoFactorEmailRequest();
emailReq.email = await this.loginStrategyService.getEmail();
// if the user was logging in with SSO, we need to include the SSO session token
if (response.ssoEmail2FaSessionToken != null) {
emailReq.ssoEmail2FaSessionToken = response.ssoEmail2FaSessionToken;
}
emailReq.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
await this.twoFactorApiService.postTwoFactorEmail(emailReq);
}
@@ -324,6 +356,7 @@ export class LoginCommand {
response = await this.loginStrategyService.logInNewDeviceVerification(newDeviceToken);
}
// We check response two factor again here since MFA could fail based on the logic on ln 226
if (response.requiresTwoFactor) {
return Response.error("Login failed.");
}
@@ -692,6 +725,27 @@ export class LoginCommand {
};
}
/// Generate SSO prompt data: code verifier, code challenge, and state
private async makeSsoPromptData(): Promise<{
ssoCodeVerifier: string;
codeChallenge: string;
state: string;
}> {
const passwordOptions: any = {
type: "password",
length: 64,
uppercase: true,
lowercase: true,
numbers: true,
special: false,
};
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
return { ssoCodeVerifier, codeChallenge, state };
}
private async openSsoPrompt(
codeChallenge: string,
state: string,

View File

@@ -15,6 +15,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
@@ -48,9 +49,10 @@ describe("UnlockCommand", () => {
const mockMasterPassword = "testExample";
const activeAccount: Account = {
id: "user-id" as UserId,
...mockAccountInfoWith({
email: "user@example.com",
emailVerified: true,
name: "User",
}),
};
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
const mockSessionKey = new Uint8Array(64) as CsprngArray;

View File

@@ -492,10 +492,7 @@ export class ServiceContainer {
const pinStateService = new PinStateService(this.stateProvider);
this.pinService = new PinService(
this.accountService,
this.encryptService,
this.kdfConfigService,
this.keyGenerationService,
this.logService,
this.keyService,
this.sdkService,
@@ -908,7 +905,7 @@ export class ServiceContainer {
this.collectionService,
this.keyService,
this.encryptService,
this.pinService,
this.keyGenerationService,
this.accountService,
this.restrictedItemTypesService,
);
@@ -916,7 +913,7 @@ export class ServiceContainer {
this.individualExportService = new IndividualVaultExportService(
this.folderService,
this.cipherService,
this.pinService,
this.keyGenerationService,
this.keyService,
this.encryptService,
this.cryptoFunctionService,
@@ -930,7 +927,7 @@ export class ServiceContainer {
this.organizationExportService = new OrganizationVaultExportService(
this.cipherService,
this.vaultExportApiService,
this.pinService,
this.keyGenerationService,
this.keyService,
this.encryptService,
this.cryptoFunctionService,

View File

@@ -2,21 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aead"
version = "0.5.2"
@@ -114,9 +99,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.94"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "arboard"
@@ -138,14 +123,14 @@ dependencies = [
[[package]]
name = "ashpd"
version = "0.11.0"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df"
checksum = "da0986d5b4f0802160191ad75f8d33ada000558757db3defb70299ca95d9fcbd"
dependencies = [
"enumflags2",
"futures-channel",
"futures-util",
"rand 0.9.1",
"rand 0.9.2",
"serde",
"serde_repr",
"tokio",
@@ -347,23 +332,8 @@ dependencies = [
"mockall",
"serial_test",
"tracing",
"windows 0.61.1",
"windows-core 0.61.0",
]
[[package]]
name = "backtrace"
version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
"windows",
"windows-core",
]
[[package]]
@@ -457,7 +427,7 @@ dependencies = [
"tokio",
"tracing",
"tracing-subscriber",
"windows 0.61.1",
"windows",
]
[[package]]
@@ -501,6 +471,12 @@ dependencies = [
"cipher",
]
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "byteorder"
version = "1.5.0"
@@ -509,9 +485,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "camino"
@@ -556,9 +532,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.46"
version = "1.2.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -615,7 +591,7 @@ dependencies = [
"hex",
"oo7",
"pbkdf2",
"rand 0.9.1",
"rand 0.9.2",
"rusqlite",
"security-framework",
"serde",
@@ -624,7 +600,7 @@ dependencies = [
"tokio",
"tracing",
"verifysign",
"windows 0.61.1",
"windows",
]
[[package]]
@@ -710,9 +686,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "convert_case"
version = "0.6.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
dependencies = [
"unicode-segmentation",
]
@@ -771,16 +747,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "ctor"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "ctor"
version = "0.5.0"
@@ -868,7 +834,7 @@ dependencies = [
"memsec",
"oo7",
"pin-project",
"rand 0.9.1",
"rand 0.9.2",
"scopeguard",
"secmem-proc",
"security-framework",
@@ -878,13 +844,13 @@ dependencies = [
"sha2",
"ssh-key",
"sysinfo",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tokio",
"tokio-util",
"tracing",
"typenum",
"widestring",
"windows 0.61.1",
"windows",
"windows-future",
"zbus",
"zbus_polkit",
@@ -1410,17 +1376,11 @@ dependencies = [
"polyval",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "glob"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "goblin"
@@ -1500,14 +1460,14 @@ dependencies = [
[[package]]
name = "homedir"
version = "0.3.4"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2"
checksum = "68df315d2857b2d8d2898be54a85e1d001bbbe0dbb5f8ef847b48dd3a23c4527"
dependencies = [
"cfg-if",
"nix 0.29.0",
"nix",
"widestring",
"windows 0.57.0",
"windows",
]
[[package]]
@@ -1664,6 +1624,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -1686,7 +1656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
dependencies = [
"cfg-if",
"windows-targets 0.53.3",
"windows-targets 0.48.5",
]
[[package]]
@@ -1842,15 +1812,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.3"
@@ -1890,32 +1851,33 @@ dependencies = [
[[package]]
name = "napi"
version = "2.16.17"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3"
checksum = "f1b74e3dce5230795bb4d2821b941706dee733c7308752507254b0497f39cad7"
dependencies = [
"bitflags",
"ctor 0.2.9",
"napi-derive",
"ctor",
"napi-build",
"napi-sys",
"once_cell",
"nohash-hasher",
"rustc-hash",
"tokio",
]
[[package]]
name = "napi-build"
version = "2.2.0"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03acbfa4f156a32188bfa09b86dc11a431b5725253fc1fc6f6df5bed273382c4"
checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14"
[[package]]
name = "napi-derive"
version = "2.16.13"
version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
checksum = "7552d5a579b834614bbd496db5109f1b9f1c758f08224b0dee1e408333adf0d0"
dependencies = [
"cfg-if",
"convert_case",
"ctor",
"napi-derive-backend",
"proc-macro2",
"quote",
@@ -1924,40 +1886,26 @@ dependencies = [
[[package]]
name = "napi-derive-backend"
version = "1.0.75"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
checksum = "5f6a81ac7486b70f2532a289603340862c06eea5a1e650c1ffeda2ce1238516a"
dependencies = [
"convert_case",
"once_cell",
"proc-macro2",
"quote",
"regex",
"semver",
"syn",
]
[[package]]
name = "napi-sys"
version = "2.4.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d"
dependencies = [
"libloading",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nix"
version = "0.30.1"
@@ -1971,6 +1919,12 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "nom"
version = "7.1.3"
@@ -2174,15 +2128,6 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -2191,9 +2136,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "oo7"
version = "0.4.3"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb23d3ec3527d65a83be1c1795cb883c52cfa57147d42acc797127df56fc489"
checksum = "e3299dd401feaf1d45afd8fd1c0586f10fcfb22f244bb9afa942cec73503b89d"
dependencies = [
"aes",
"ashpd",
@@ -2209,7 +2154,7 @@ dependencies = [
"num",
"num-bigint-dig",
"pbkdf2",
"rand 0.9.1",
"rand 0.9.2",
"serde",
"sha2",
"subtle",
@@ -2549,7 +2494,7 @@ dependencies = [
name = "process_isolation"
version = "0.0.0"
dependencies = [
"ctor 0.5.0",
"ctor",
"desktop_core",
"libc",
"tracing",
@@ -2592,9 +2537,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.1"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
@@ -2661,19 +2606,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.12",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
"thiserror 2.0.17",
]
[[package]]
@@ -2749,10 +2682,10 @@ dependencies = [
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
@@ -2799,6 +2732,12 @@ dependencies = [
"rustix 1.0.7",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.20"
@@ -2871,8 +2810,8 @@ dependencies = [
"libc",
"rustix 1.0.7",
"rustix-linux-procfs",
"thiserror 2.0.12",
"windows 0.61.1",
"thiserror 2.0.17",
"windows",
]
[[package]]
@@ -3069,12 +3008,12 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]]
name = "socket2"
version = "0.5.9"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -3198,7 +3137,7 @@ dependencies = [
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows 0.61.1",
"windows",
]
[[package]]
@@ -3240,11 +3179,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.12"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl 2.0.12",
"thiserror-impl 2.0.17",
]
[[package]]
@@ -3260,9 +3199,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.12"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
@@ -3290,11 +3229,10 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.45.0"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
@@ -3304,14 +3242,14 @@ dependencies = [
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
@@ -3320,9 +3258,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.13"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
dependencies = [
"bytes",
"futures-core",
@@ -3681,6 +3619,17 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.1"
@@ -3746,6 +3695,51 @@ dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wayland-backend"
version = "0.3.10"
@@ -3853,16 +3847,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.57.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.1"
@@ -3870,7 +3854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [
"windows-collections",
"windows-core 0.61.0",
"windows-core",
"windows-future",
"windows-link 0.1.3",
"windows-numerics",
@@ -3882,19 +3866,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core 0.61.0",
]
[[package]]
name = "windows-core"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement 0.57.0",
"windows-interface 0.57.0",
"windows-result 0.1.2",
"windows-targets 0.52.6",
"windows-core",
]
[[package]]
@@ -3903,8 +3875,8 @@ version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-implement",
"windows-interface",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
@@ -3916,21 +3888,10 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
dependencies = [
"windows-core 0.61.0",
"windows-core",
"windows-link 0.1.3",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
@@ -3942,17 +3903,6 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
@@ -3982,7 +3932,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.0",
"windows-core",
"windows-link 0.1.3",
]
@@ -3997,15 +3947,6 @@ dependencies = [
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -4263,8 +4204,8 @@ name = "windows_plugin_authenticator"
version = "0.0.0"
dependencies = [
"hex",
"windows 0.61.1",
"windows-core 0.61.0",
"windows",
"windows-core",
]
[[package]]
@@ -4435,9 +4376,9 @@ dependencies = [
[[package]]
name = "zbus"
version = "5.11.0"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7"
checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91"
dependencies = [
"async-broadcast",
"async-executor",
@@ -4453,14 +4394,15 @@ dependencies = [
"futures-core",
"futures-lite",
"hex",
"nix 0.30.1",
"nix",
"ordered-stream",
"serde",
"serde_repr",
"tokio",
"tracing",
"uds_windows",
"windows-sys 0.60.2",
"uuid",
"windows-sys 0.61.2",
"winnow",
"zbus_macros",
"zbus_names",
@@ -4469,9 +4411,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
version = "5.11.0"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca"
checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314"
dependencies = [
"proc-macro-crate",
"proc-macro2",

View File

@@ -21,13 +21,13 @@ publish = false
[workspace.dependencies]
aes = "=0.8.4"
aes-gcm = "=0.10.3"
anyhow = "=1.0.94"
anyhow = "=1.0.100"
arboard = { version = "=3.6.1", default-features = false }
ashpd = "=0.11.0"
ashpd = "=0.12.0"
base64 = "=0.22.1"
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" }
byteorder = "=1.5.0"
bytes = "=1.10.1"
bytes = "=1.11.0"
cbc = "=0.1.2"
chacha20poly1305 = "=0.10.1"
core-foundation = "=0.10.1"
@@ -37,18 +37,18 @@ ed25519 = "=2.2.3"
embed_plist = "=1.2.2"
futures = "=0.3.31"
hex = "=0.4.3"
homedir = "=0.3.4"
homedir = "=0.3.6"
interprocess = "=2.2.1"
libc = "=0.2.178"
linux-keyutils = "=0.2.4"
memsec = "=0.7.0"
napi = "=2.16.17"
napi-build = "=2.2.0"
napi-derive = "=2.16.13"
oo7 = "=0.4.3"
napi = "=3.3.0"
napi-build = "=2.2.3"
napi-derive = "=3.2.5"
oo7 = "=0.5.0"
pin-project = "=1.1.10"
pkcs8 = "=0.10.2"
rand = "=0.9.1"
rand = "=0.9.2"
rsa = "=0.9.6"
russh-cryptovec = "=0.7.3"
scopeguard = "=1.2.0"
@@ -61,9 +61,9 @@ sha2 = "=0.10.8"
ssh-encoding = "=0.2.0"
ssh-key = { version = "=0.6.7", default-features = false }
sysinfo = "=0.37.2"
thiserror = "=2.0.12"
tokio = "=1.45.0"
tokio-util = "=0.7.13"
thiserror = "=2.0.17"
tokio = "=1.48.0"
tokio-util = "=0.7.17"
tracing = "=0.1.41"
tracing-subscriber = { version = "=0.3.20", features = [
"fmt",
@@ -77,7 +77,7 @@ windows = "=0.61.1"
windows-core = "=0.61.0"
windows-future = "=0.2.0"
windows-registry = "=0.6.1"
zbus = "=5.11.0"
zbus = "=5.12.0"
zbus_polkit = "=5.0.0"
zeroizing-alloc = "=0.1.0"

View File

@@ -11,8 +11,8 @@ const rustTargetsMap = {
"aarch64-pc-windows-msvc": { nodeArch: 'arm64', platform: 'win32' },
"x86_64-apple-darwin": { nodeArch: 'x64', platform: 'darwin' },
"aarch64-apple-darwin": { nodeArch: 'arm64', platform: 'darwin' },
'x86_64-unknown-linux-musl': { nodeArch: 'x64', platform: 'linux' },
'aarch64-unknown-linux-musl': { nodeArch: 'arm64', platform: 'linux' },
'x86_64-unknown-linux-gnu': { nodeArch: 'x64', platform: 'linux' },
'aarch64-unknown-linux-gnu': { nodeArch: 'arm64', platform: 'linux' },
}
// Ensure the dist directory exists
@@ -113,8 +113,8 @@ if (process.platform === "linux") {
platformTargets.forEach(([target, _]) => {
installTarget(target);
buildNapiModule(target);
buildProxyBin(target);
buildImporterBinaries(target);
buildNapiModule(target, mode === "release");
buildProxyBin(target, mode === "release");
buildImporterBinaries(target, mode === "release");
buildProcessIsolation();
});

View File

@@ -18,7 +18,7 @@ use crate::{
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
BrowserConfig {
name: "Chrome",
data_dir: &[".config/google-chrome"],
data_dir: &[".config/google-chrome", "snap/chromium/common/chromium"],
},
BrowserConfig {
name: "Chromium",

View File

@@ -7,9 +7,9 @@ pub struct NativeImporterMetadata {
/// Identifies the importer
pub id: String,
/// Describes the strategies used to obtain imported data
pub loaders: Vec<&'static str>,
pub loaders: Vec<String>,
/// Identifies the instructions for the importer
pub instructions: &'static str,
pub instructions: String,
}
/// Returns a map of supported importers based on the current platform.
@@ -37,9 +37,9 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
PLATFORM_SUPPORTED_BROWSERS.iter().map(|b| b.name).collect();
for (id, browser_name) in IMPORTERS {
let mut loaders: Vec<&'static str> = vec!["file"];
let mut loaders: Vec<String> = vec!["file".to_string()];
if supported.contains(browser_name) {
loaders.push("chromium");
loaders.push("chromium".to_string());
}
if installed_browsers.contains(&browser_name.to_string()) {
@@ -48,7 +48,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
NativeImporterMetadata {
id: id.to_string(),
loaders,
instructions: "chromium",
instructions: "chromium".to_string(),
},
);
}
@@ -80,12 +80,9 @@ mod tests {
map.keys().cloned().collect()
}
fn get_loaders(
map: &HashMap<String, NativeImporterMetadata>,
id: &str,
) -> HashSet<&'static str> {
fn get_loaders(map: &HashMap<String, NativeImporterMetadata>, id: &str) -> HashSet<String> {
map.get(id)
.map(|m| m.loaders.iter().copied().collect::<HashSet<_>>())
.map(|m| m.loaders.iter().cloned().collect::<HashSet<_>>())
.unwrap_or_default()
}
@@ -108,7 +105,7 @@ mod tests {
for (key, meta) in map.iter() {
assert_eq!(&meta.id, key);
assert_eq!(meta.instructions, "chromium");
assert!(meta.loaders.contains(&"file"));
assert!(meta.loaders.contains(&"file".to_owned()));
}
}
@@ -148,7 +145,7 @@ mod tests {
for (key, meta) in map.iter() {
assert_eq!(&meta.id, key);
assert_eq!(meta.instructions, "chromium");
assert!(meta.loaders.contains(&"file"));
assert!(meta.loaders.contains(&"file".to_owned()));
}
}
@@ -184,7 +181,7 @@ mod tests {
for (key, meta) in map.iter() {
assert_eq!(&meta.id, key);
assert_eq!(meta.instructions, "chromium");
assert!(meta.loaders.contains(&"file"));
assert!(meta.loaders.contains(&"file".to_owned()));
}
}

View File

@@ -1,125 +1,7 @@
/* tslint:disable */
/* eslint-disable */
/* auto-generated by NAPI-RS */
export declare namespace passwords {
/** The error message returned when a password is not found during retrieval or deletion. */
export const PASSWORD_NOT_FOUND: string
/**
* Fetch the stored password from the keychain.
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
*/
export function getPassword(service: string, account: string): Promise<string>
/**
* Save the password to the keychain. Adds an entry if none exists otherwise updates the
* existing entry.
*/
export function setPassword(service: string, account: string, password: string): Promise<void>
/**
* Delete the stored password from the keychain.
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
*/
export function deletePassword(service: string, account: string): Promise<void>
/** Checks if the os secure storage is available */
export function isAvailable(): Promise<boolean>
}
export declare namespace biometrics {
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
export function available(): Promise<boolean>
export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise<string>
/**
* Retrieves the biometric secret for the given service and account.
* Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist.
*/
export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise<string>
/**
* Derives key material from biometric data. Returns a string encoded with a
* base64 encoded key and the base64 encoded challenge used to create it
* separated by a `|` character.
*
* If the iv is provided, it will be used as the challenge. Otherwise a random challenge will
* be generated.
*
* `format!("<key_base64>|<iv_base64>")`
*/
export function deriveKeyMaterial(iv?: string | undefined | null): Promise<OsDerivedKey>
export interface KeyMaterial {
osKeyPartB64: string
clientKeyPartB64?: string
}
export interface OsDerivedKey {
keyB64: string
ivB64: string
}
}
export declare namespace biometrics_v2 {
export function initBiometricSystem(): BiometricLockSystem
export function authenticate(biometricLockSystem: BiometricLockSystem, hwnd: Buffer, message: string): Promise<boolean>
export function authenticateAvailable(biometricLockSystem: BiometricLockSystem): Promise<boolean>
export function enrollPersistent(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
export function provideKey(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
export function unlock(biometricLockSystem: BiometricLockSystem, userId: string, hwnd: Buffer): Promise<Buffer>
export function unlockAvailable(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
export function hasPersistent(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
export function unenroll(biometricLockSystem: BiometricLockSystem, userId: string): Promise<void>
export class BiometricLockSystem { }
}
export declare namespace clipboards {
export function read(): Promise<string>
export function write(text: string, password: boolean): Promise<void>
}
export declare namespace sshagent {
export interface PrivateKey {
privateKey: string
name: string
cipherId: string
}
export interface SshKey {
privateKey: string
publicKey: string
keyFingerprint: string
}
export interface SshUiRequest {
cipherId?: string
isList: boolean
processName: string
isForwarding: boolean
namespace?: string
}
export function serve(callback: (err: Error | null, arg: SshUiRequest) => any): Promise<SshAgentState>
export function stop(agentState: SshAgentState): void
export function isRunning(agentState: SshAgentState): boolean
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
export function lock(agentState: SshAgentState): void
export function clearKeys(agentState: SshAgentState): void
export class SshAgentState { }
}
export declare namespace processisolations {
export function disableCoredumps(): Promise<void>
export function isCoreDumpingDisabled(): Promise<boolean>
export function isolateProcess(): Promise<void>
}
export declare namespace powermonitors {
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
export function isLockMonitorAvailable(): Promise<boolean>
}
export declare namespace windows_registry {
export function createKey(key: string, subkey: string, value: string): Promise<void>
export function deleteKey(key: string, subkey: string): Promise<void>
}
export declare namespace ipc {
export interface IpcMessage {
clientId: number
kind: IpcMessageType
message?: string
}
export const enum IpcMessageType {
Connected = 0,
Disconnected = 1,
Message = 2
}
export class IpcServer {
/* eslint-disable */
export declare namespace autofill {
export class AutofillIpcServer {
/**
* Create and start the IPC server without blocking.
*
@@ -127,34 +9,43 @@ export declare namespace ipc {
* connection and must be the same for both the server and client. @param callback
* This function will be called whenever a message is received from a client.
*/
static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise<IpcServer>
static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void, nativeStatusCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void): Promise<AutofillIpcServer>
/** Return the path to the IPC server. */
getPath(): string
/** Stop the IPC server. */
stop(): void
/**
* Send a message over the IPC server to all the connected clients
*
* @return The number of clients that the message was sent to. Note that the number of
* messages actually received may be less, as some clients could disconnect before
* receiving the message.
*/
send(message: string): number
completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number
completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number
completeError(clientId: number, sequenceNumber: number, error: string): number
}
export interface NativeStatus {
key: string
value: string
}
export declare namespace autostart {
export function setAutostart(autostart: boolean, params: Array<string>): Promise<void>
export interface PasskeyAssertionRequest {
rpId: string
clientDataHash: Array<number>
userVerification: UserVerification
allowedCredentials: Array<Array<number>>
windowXy: Position
}
export declare namespace autofill {
export function runCommand(value: string): Promise<string>
export const enum UserVerification {
Preferred = 'preferred',
Required = 'required',
Discouraged = 'discouraged'
export interface PasskeyAssertionResponse {
rpId: string
userHandle: Array<number>
signature: Array<number>
clientDataHash: Array<number>
authenticatorData: Array<number>
credentialId: Array<number>
}
export interface Position {
x: number
y: number
export interface PasskeyAssertionWithoutUserInterfaceRequest {
rpId: string
credentialId: Array<number>
userName: string
userHandle: Array<number>
recordIdentifier?: string
clientDataHash: Array<number>
userVerification: UserVerification
windowXy: Position
}
export interface PasskeyRegistrationRequest {
rpId: string
@@ -172,71 +63,77 @@ export declare namespace autofill {
credentialId: Array<number>
attestationObject: Array<number>
}
export interface PasskeyAssertionRequest {
rpId: string
clientDataHash: Array<number>
userVerification: UserVerification
allowedCredentials: Array<Array<number>>
windowXy: Position
export interface Position {
x: number
y: number
}
export interface PasskeyAssertionWithoutUserInterfaceRequest {
rpId: string
credentialId: Array<number>
userName: string
userHandle: Array<number>
recordIdentifier?: string
clientDataHash: Array<number>
userVerification: UserVerification
windowXy: Position
export function runCommand(value: string): Promise<string>
export const enum UserVerification {
Preferred = 'preferred',
Required = 'required',
Discouraged = 'discouraged'
}
export interface NativeStatus {
key: string
value: string
}
export interface PasskeyAssertionResponse {
rpId: string
userHandle: Array<number>
signature: Array<number>
clientDataHash: Array<number>
authenticatorData: Array<number>
credentialId: Array<number>
export declare namespace autostart {
export function setAutostart(autostart: boolean, params: Array<string>): Promise<void>
}
export class IpcServer {
export declare namespace autotype {
export function getForegroundWindowTitle(): string
export function typeInput(input: Array<number>, keyboardShortcut: Array<string>): void
}
export declare namespace biometrics {
export function available(): Promise<boolean>
/**
* Create and start the IPC server without blocking.
* Derives key material from biometric data. Returns a string encoded with a
* base64 encoded key and the base64 encoded challenge used to create it
* separated by a `|` character.
*
* @param name The endpoint name to listen on. This name uniquely identifies the IPC
* connection and must be the same for both the server and client. @param callback
* This function will be called whenever a message is received from a client.
* If the iv is provided, it will be used as the challenge. Otherwise a random challenge will
* be generated.
*
* `format!("<key_base64>|<iv_base64>")`
*/
static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void, nativeStatusCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void): Promise<IpcServer>
/** Return the path to the IPC server. */
getPath(): string
/** Stop the IPC server. */
stop(): void
completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number
completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number
completeError(clientId: number, sequenceNumber: number, error: string): number
export function deriveKeyMaterial(iv?: string | undefined | null): Promise<OsDerivedKey>
/**
* Retrieves the biometric secret for the given service and account.
* Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist.
*/
export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise<string>
export interface KeyMaterial {
osKeyPartB64: string
clientKeyPartB64?: string
}
export interface OsDerivedKey {
keyB64: string
ivB64: string
}
export declare namespace passkey_authenticator {
export function register(): void
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise<string>
}
export declare namespace logging {
export const enum LogLevel {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4
export declare namespace biometrics_v2 {
export class BiometricLockSystem {
}
export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void
export function authenticate(biometricLockSystem: BiometricLockSystem, hwnd: Buffer, message: string): Promise<boolean>
export function authenticateAvailable(biometricLockSystem: BiometricLockSystem): Promise<boolean>
export function enrollPersistent(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
export function hasPersistent(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
export function initBiometricSystem(): BiometricLockSystem
export function provideKey(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
export function unenroll(biometricLockSystem: BiometricLockSystem, userId: string): Promise<void>
export function unlock(biometricLockSystem: BiometricLockSystem, userId: string, hwnd: Buffer): Promise<Buffer>
export function unlockAvailable(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
}
export declare namespace chromium_importer {
export interface ProfileInfo {
id: string
name: string
}
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
export function getMetadata(): Record<string, NativeImporterMetadata>
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
export interface Login {
url: string
username: string
@@ -257,13 +154,130 @@ export declare namespace chromium_importer {
loaders: Array<string>
instructions: string
}
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
export function getMetadata(masBuild: boolean): Record<string, NativeImporterMetadata>
export function getAvailableProfiles(browser: string, masBuild: boolean): Promise<Array<ProfileInfo>>
export function importLogins(browser: string, profileId: string, masBuild: boolean): Promise<Array<LoginImportResult>>
export function requestBrowserAccess(browser: string, masBuild: boolean): Promise<void>
export interface ProfileInfo {
id: string
name: string
}
export declare namespace autotype {
export function getForegroundWindowTitle(): string
export function typeInput(input: Array<number>, keyboardShortcut: Array<string>): void
}
export declare namespace clipboards {
export function read(): Promise<string>
export function write(text: string, password: boolean): Promise<void>
}
export declare namespace ipc {
export class NativeIpcServer {
/**
* Create and start the IPC server without blocking.
*
* @param name The endpoint name to listen on. This name uniquely identifies the IPC
* connection and must be the same for both the server and client. @param callback
* This function will be called whenever a message is received from a client.
*/
static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise<NativeIpcServer>
/** Return the path to the IPC server. */
getPath(): string
/** Stop the IPC server. */
stop(): void
/**
* Send a message over the IPC server to all the connected clients
*
* @return The number of clients that the message was sent to. Note that the number of
* messages actually received may be less, as some clients could disconnect before
* receiving the message.
*/
send(message: string): number
}
export interface IpcMessage {
clientId: number
kind: IpcMessageType
message?: string
}
export const enum IpcMessageType {
Connected = 0,
Disconnected = 1,
Message = 2
}
}
export declare namespace logging {
export function initNapiLog(jsLogFn: ((err: Error | null, arg0: LogLevel, arg1: string) => any)): void
export const enum LogLevel {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4
}
}
export declare namespace passkey_authenticator {
export function register(): void
}
export declare namespace passwords {
/**
* Delete the stored password from the keychain.
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
*/
export function deletePassword(service: string, account: string): Promise<void>
/**
* Fetch the stored password from the keychain.
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
*/
export function getPassword(service: string, account: string): Promise<string>
/** Checks if the os secure storage is available */
export function isAvailable(): Promise<boolean>
/** The error message returned when a password is not found during retrieval or deletion. */
export const PASSWORD_NOT_FOUND: string
/**
* Save the password to the keychain. Adds an entry if none exists otherwise updates the
* existing entry.
*/
export function setPassword(service: string, account: string, password: string): Promise<void>
}
export declare namespace powermonitors {
export function isLockMonitorAvailable(): Promise<boolean>
export function onLock(callback: ((err: Error | null, ) => any)): Promise<void>
}
export declare namespace processisolations {
export function disableCoredumps(): Promise<void>
export function isCoreDumpingDisabled(): Promise<boolean>
export function isolateProcess(): Promise<void>
}
export declare namespace sshagent {
export class SshAgentState {
}
export function clearKeys(agentState: SshAgentState): void
export function isRunning(agentState: SshAgentState): boolean
export function lock(agentState: SshAgentState): void
export interface PrivateKey {
privateKey: string
name: string
cipherId: string
}
export function serve(callback: ((err: Error | null, arg: SshUiRequest) => Promise<boolean>)): Promise<SshAgentState>
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
export interface SshKey {
privateKey: string
publicKey: string
keyFingerprint: string
}
export interface SshUiRequest {
cipherId?: string
isList: boolean
processName: string
isForwarding: boolean
namespace?: string
}
export function stop(agentState: SshAgentState): void
}
export declare namespace windows_registry {
export function createKey(key: string, subkey: string, value: string): Promise<void>
export function deleteKey(key: string, subkey: string): Promise<void>
}

View File

@@ -82,20 +82,20 @@ switch (platform) {
switch (arch) {
case "x64":
nativeBinding = loadFirstAvailable(
["desktop_napi.linux-x64-musl.node", "desktop_napi.linux-x64-gnu.node"],
"@bitwarden/desktop-napi-linux-x64-musl",
["desktop_napi.linux-x64-gnu.node"],
"@bitwarden/desktop-napi-linux-x64-gnu",
);
break;
case "arm64":
nativeBinding = loadFirstAvailable(
["desktop_napi.linux-arm64-musl.node", "desktop_napi.linux-arm64-gnu.node"],
"@bitwarden/desktop-napi-linux-arm64-musl",
["desktop_napi.linux-arm64-gnu.node"],
"@bitwarden/desktop-napi-linux-arm64-gnu",
);
break;
case "arm":
nativeBinding = loadFirstAvailable(
["desktop_napi.linux-arm-musl.node", "desktop_napi.linux-arm-gnu.node"],
"@bitwarden/desktop-napi-linux-arm-musl",
["desktop_napi.linux-arm-gnu.node"],
"@bitwarden/desktop-napi-linux-arm-gnu",
);
localFileExisted = existsSync(join(__dirname, "desktop_napi.linux-arm-gnueabihf.node"));
try {

View File

@@ -3,27 +3,23 @@
"version": "0.1.0",
"description": "",
"scripts": {
"build": "napi build --platform --js false",
"build": "node scripts/build.js",
"test": "cargo test"
},
"author": "",
"license": "GPL-3.0",
"devDependencies": {
"@napi-rs/cli": "2.18.4"
"@napi-rs/cli": "3.2.0"
},
"napi": {
"name": "desktop_napi",
"triples": {
"defaults": true,
"additional": [
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-gnu",
"i686-pc-windows-msvc",
"armv7-unknown-linux-gnueabihf",
"binaryName": "desktop_napi",
"targets": [
"aarch64-apple-darwin",
"aarch64-unknown-linux-musl",
"aarch64-pc-windows-msvc"
"aarch64-pc-windows-msvc",
"aarch64-unknown-linux-gnu",
"armv7-unknown-linux-gnueabihf",
"i686-pc-windows-msvc",
"x86_64-unknown-linux-gnu"
]
}
}
}

View File

@@ -0,0 +1,22 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { execSync } = require('child_process');
const args = process.argv.slice(2);
const isRelease = args.includes('--release');
const argsString = args.join(' ');
if (isRelease) {
console.log('Building release mode.');
execSync(`napi build --platform --no-js ${argsString}`, { stdio: 'inherit'});
} else {
console.log('Building debug mode.');
execSync(`napi build --platform --no-js ${argsString}`, {
stdio: 'inherit',
env: { ...process.env, RUST_LOG: 'debug' }
});
}

View File

@@ -290,7 +290,7 @@ pub mod sshagent {
use napi::{
bindgen_prelude::Promise,
threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction},
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
};
use tokio::{self, sync::Mutex};
use tracing::error;
@@ -326,13 +326,15 @@ pub mod sshagent {
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
#[napi]
pub async fn serve(
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>,
callback: ThreadsafeFunction<SshUIRequest, Promise<bool>>,
) -> napi::Result<SshAgentState> {
let (auth_request_tx, mut auth_request_rx) =
tokio::sync::mpsc::channel::<desktop_core::ssh_agent::SshAgentUIRequest>(32);
let (auth_response_tx, auth_response_rx) =
tokio::sync::broadcast::channel::<(u32, bool)>(32);
let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx));
// Wrap callback in Arc so it can be shared across spawned tasks
let callback = Arc::new(callback);
tokio::spawn(async move {
let _ = auth_response_rx;
@@ -342,42 +344,50 @@ pub mod sshagent {
tokio::spawn(async move {
let auth_response_tx_arc = cloned_response_tx_arc;
let callback = cloned_callback;
let promise_result: Result<Promise<bool>, napi::Error> = callback
.call_async(Ok(SshUIRequest {
// In NAPI v3, obtain the JS callback return as a Promise<boolean> and await it
// in Rust
let (tx, rx) = std::sync::mpsc::channel::<Promise<bool>>();
let status = callback.call_with_return_value(
Ok(SshUIRequest {
cipher_id: request.cipher_id,
is_list: request.is_list,
process_name: request.process_name,
is_forwarding: request.is_forwarding,
namespace: request.namespace,
}))
.await;
match promise_result {
Ok(promise_result) => match promise_result.await {
Ok(result) => {
}),
ThreadsafeFunctionCallMode::Blocking,
move |ret: Result<Promise<bool>, napi::Error>, _env| {
if let Ok(p) = ret {
let _ = tx.send(p);
}
Ok(())
},
);
let result = if status == napi::Status::Ok {
match rx.recv() {
Ok(promise) => match promise.await {
Ok(v) => v,
Err(e) => {
error!(error = %e, "UI callback promise rejected");
false
}
},
Err(e) => {
error!(error = %e, "Failed to receive UI callback promise");
false
}
}
} else {
error!(error = ?status, "Calling UI callback failed");
false
};
let _ = auth_response_tx_arc
.lock()
.await
.send((request.request_id, result))
.expect("should be able to send auth response to agent");
}
Err(e) => {
error!(error = %e, "Calling UI callback promise was rejected");
let _ = auth_response_tx_arc
.lock()
.await
.send((request.request_id, false))
.expect("should be able to send auth response to agent");
}
},
Err(e) => {
error!(error = %e, "Calling UI callback could not create promise");
let _ = auth_response_tx_arc
.lock()
.await
.send((request.request_id, false))
.expect("should be able to send auth response to agent");
}
}
});
}
});
@@ -465,14 +475,12 @@ pub mod processisolations {
#[napi]
pub mod powermonitors {
use napi::{
threadsafe_function::{
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
},
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
tokio,
};
#[napi]
pub async fn on_lock(callback: ThreadsafeFunction<(), CalleeHandled>) -> napi::Result<()> {
pub async fn on_lock(callback: ThreadsafeFunction<()>) -> napi::Result<()> {
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32);
desktop_core::powermonitor::on_lock(tx)
.await
@@ -511,9 +519,7 @@ pub mod windows_registry {
#[napi]
pub mod ipc {
use desktop_core::ipc::server::{Message, MessageType};
use napi::threadsafe_function::{
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
#[napi(object)]
pub struct IpcMessage {
@@ -550,12 +556,12 @@ pub mod ipc {
}
#[napi]
pub struct IpcServer {
pub struct NativeIpcServer {
server: desktop_core::ipc::server::Server,
}
#[napi]
impl IpcServer {
impl NativeIpcServer {
/// Create and start the IPC server without blocking.
///
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC
@@ -566,7 +572,7 @@ pub mod ipc {
pub async fn listen(
name: String,
#[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")]
callback: ThreadsafeFunction<IpcMessage, ErrorStrategy::CalleeHandled>,
callback: ThreadsafeFunction<IpcMessage>,
) -> napi::Result<Self> {
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
tokio::spawn(async move {
@@ -583,7 +589,7 @@ pub mod ipc {
))
})?;
Ok(IpcServer { server })
Ok(NativeIpcServer { server })
}
/// Return the path to the IPC server.
@@ -630,8 +636,9 @@ pub mod autostart {
#[napi]
pub mod autofill {
use desktop_core::ipc::server::{Message, MessageType};
use napi::threadsafe_function::{
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
use napi::{
bindgen_prelude::FnArgs,
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tracing::error;
@@ -746,14 +753,14 @@ pub mod autofill {
}
#[napi]
pub struct IpcServer {
pub struct AutofillIpcServer {
server: desktop_core::ipc::server::Server,
}
// FIXME: Remove unwraps! They panic and terminate the whole application.
#[allow(clippy::unwrap_used)]
#[napi]
impl IpcServer {
impl AutofillIpcServer {
/// Create and start the IPC server without blocking.
///
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC
@@ -769,30 +776,24 @@ pub mod autofill {
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void"
)]
registration_callback: ThreadsafeFunction<
(u32, u32, PasskeyRegistrationRequest),
ErrorStrategy::CalleeHandled,
FnArgs<(u32, u32, PasskeyRegistrationRequest)>,
>,
#[napi(
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void"
)]
assertion_callback: ThreadsafeFunction<
(u32, u32, PasskeyAssertionRequest),
ErrorStrategy::CalleeHandled,
FnArgs<(u32, u32, PasskeyAssertionRequest)>,
>,
#[napi(
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void"
)]
assertion_without_user_interface_callback: ThreadsafeFunction<
(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest),
ErrorStrategy::CalleeHandled,
FnArgs<(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest)>,
>,
#[napi(
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void"
)]
native_status_callback: ThreadsafeFunction<
(u32, u32, NativeStatus),
ErrorStrategy::CalleeHandled,
>,
native_status_callback: ThreadsafeFunction<(u32, u32, NativeStatus)>,
) -> napi::Result<Self> {
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
tokio::spawn(async move {
@@ -817,7 +818,7 @@ pub mod autofill {
Ok(msg) => {
let value = msg
.value
.map(|value| (client_id, msg.sequence_number, value))
.map(|value| (client_id, msg.sequence_number, value).into())
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
assertion_callback
@@ -836,7 +837,7 @@ pub mod autofill {
Ok(msg) => {
let value = msg
.value
.map(|value| (client_id, msg.sequence_number, value))
.map(|value| (client_id, msg.sequence_number, value).into())
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
assertion_without_user_interface_callback
@@ -854,7 +855,7 @@ pub mod autofill {
Ok(msg) => {
let value = msg
.value
.map(|value| (client_id, msg.sequence_number, value))
.map(|value| (client_id, msg.sequence_number, value).into())
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
registration_callback
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
@@ -894,7 +895,7 @@ pub mod autofill {
))
})?;
Ok(IpcServer { server })
Ok(AutofillIpcServer { server })
}
/// Return the path to the IPC server.
@@ -987,19 +988,20 @@ pub mod logging {
use std::{fmt::Write, sync::OnceLock};
use napi::threadsafe_function::{
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
use napi::{
bindgen_prelude::FnArgs,
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
};
use tracing::Level;
use tracing_subscriber::{
filter::{EnvFilter, LevelFilter},
filter::EnvFilter,
fmt::format::{DefaultVisitor, Writer},
layer::SubscriberExt,
util::SubscriberInitExt,
Layer,
};
struct JsLogger(OnceLock<ThreadsafeFunction<(LogLevel, String), CalleeHandled>>);
struct JsLogger(OnceLock<ThreadsafeFunction<FnArgs<(LogLevel, String)>>>);
static JS_LOGGER: JsLogger = JsLogger(OnceLock::new());
#[napi]
@@ -1071,18 +1073,26 @@ pub mod logging {
let msg = (event.metadata().level().into(), buffer);
if let Some(logger) = JS_LOGGER.0.get() {
let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking);
let _ = logger.call(Ok(msg.into()), ThreadsafeFunctionCallMode::NonBlocking);
};
}
}
#[napi]
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) {
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<FnArgs<(LogLevel, String)>>) {
let _ = JS_LOGGER.0.set(js_log_fn);
// the log level hierarchy is determined by:
// - if RUST_LOG is detected at runtime
// - if RUST_LOG is provided at compile time
// - default to INFO
let filter = EnvFilter::builder()
// set the default log level to INFO.
.with_default_directive(LevelFilter::INFO.into())
.with_default_directive(
option_env!("RUST_LOG")
.unwrap_or("info")
.parse()
.expect("should provide valid log level at compile time."),
)
// parse directives from the RUST_LOG environment variable,
// overriding the default directive for matching targets.
.from_env_lossy();
@@ -1140,8 +1150,8 @@ pub mod chromium_importer {
#[napi(object)]
pub struct NativeImporterMetadata {
pub id: String,
pub loaders: Vec<&'static str>,
pub instructions: &'static str,
pub loaders: Vec<String>,
pub instructions: String,
}
impl From<_LoginImportResult> for LoginImportResult {
@@ -1237,7 +1247,7 @@ pub mod chromium_importer {
#[napi]
pub mod autotype {
#[napi]
pub fn get_foreground_window_title() -> napi::Result<String, napi::Status> {
pub fn get_foreground_window_title() -> napi::Result<String> {
autotype::get_foreground_window_title().map_err(|_| {
napi::Error::from_reason(
"Autotype Error: failed to get foreground window title".to_string(),

View File

@@ -14,8 +14,8 @@ tokio = { workspace = true, features = ["sync"] }
tracing = { workspace = true }
[target.'cfg(target_os = "macos")'.build-dependencies]
cc = "=1.2.46"
glob = "=0.3.2"
cc = "=1.2.49"
glob = "=0.3.3"
[lints]
workspace = true

View File

@@ -19,7 +19,7 @@
"yargs": "18.0.0"
},
"devDependencies": {
"@types/node": "22.19.1",
"@types/node": "22.19.2",
"typescript": "5.4.2"
}
},
@@ -117,9 +117,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.19.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
"version": "22.19.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz",
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
"license": "MIT",
"peer": true,
"dependencies": {

View File

@@ -24,7 +24,7 @@
"yargs": "18.0.0"
},
"devDependencies": {
"@types/node": "22.19.1",
"@types/node": "22.19.2",
"typescript": "5.4.2"
},
"_moduleAliases": {

View File

@@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.12.0",
"version": "2025.12.1",
"keywords": [
"bitwarden",
"password",

View File

@@ -42,14 +42,17 @@ import {
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
import {
LockComponent,
ConfirmKeyConnectorDomainComponent,
RemovePasswordComponent,
} from "@bitwarden/key-management-ui";
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
import { reactiveUnlockVaultGuard } from "../autofill/guards/reactive-vault-guard";
import { Fido2CreateComponent } from "../autofill/modal/credentials/fido2-create.component";
import { Fido2ExcludedCiphersComponent } from "../autofill/modal/credentials/fido2-excluded-ciphers.component";
import { Fido2VaultComponent } from "../autofill/modal/credentials/fido2-vault.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
import { VaultComponent } from "../vault/app/vault-v3/vault.component";
@@ -117,11 +120,6 @@ const routes: Routes = [
component: SendComponent,
canActivate: [authGuard],
},
{
path: "remove-password",
component: RemovePasswordComponent,
canActivate: [authGuard],
},
{
path: "fido2-assertion",
component: Fido2VaultComponent,
@@ -327,13 +325,24 @@ const routes: Routes = [
pageIcon: LockIcon,
} satisfies AnonLayoutWrapperData,
},
{
path: "remove-password",
component: RemovePasswordComponent,
canActivate: [authGuard],
data: {
pageTitle: {
key: "verifyYourOrganization",
},
pageIcon: LockIcon,
} satisfies RouteDataProperties & AnonLayoutWrapperData,
},
{
path: "confirm-key-connector-domain",
component: ConfirmKeyConnectorDomainComponent,
canActivate: [],
data: {
pageTitle: {
key: "confirmKeyConnectorDomain",
key: "verifyYourOrganization",
},
pageIcon: DomainIcon,
} satisfies RouteDataProperties & AnonLayoutWrapperData,

View File

@@ -15,7 +15,6 @@ import { DeleteAccountComponent } from "../auth/delete-account.component";
import { LoginModule } from "../auth/login/login.module";
import { SshAgentService } from "../autofill/services/ssh-agent.service";
import { PremiumComponent } from "../billing/app/accounts/premium.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module";
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
@@ -50,7 +49,6 @@ import { SharedModule } from "./shared/shared.module";
ColorPasswordCountPipe,
HeaderComponent,
PremiumComponent,
RemovePasswordComponent,
SearchComponent,
],
providers: [SshAgentService],

View File

@@ -3,7 +3,7 @@
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n"></bit-nav-logo>
<bit-nav-item icon="bwi-vault" [text]="'vault' | i18n" route="new-vault"></bit-nav-item>
<bit-nav-item icon="bwi-send" [text]="'send' | i18n" route="new-sends"></bit-nav-item>
<app-send-filters-nav></app-send-filters-nav>
</app-side-nav>
<router-outlet></router-outlet>

View File

@@ -1,3 +1,4 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RouterModule } from "@angular/router";
import { mock } from "jest-mock-extended";
@@ -5,8 +6,18 @@ import { mock } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { NavigationModule } from "@bitwarden/components";
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
import { DesktopLayoutComponent } from "./desktop-layout.component";
// Mock the child component to isolate DesktopLayoutComponent testing
@Component({
selector: "app-send-filters-nav",
template: "",
changeDetection: ChangeDetectionStrategy.OnPush,
})
class MockSendFiltersNavComponent {}
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
@@ -34,7 +45,12 @@ describe("DesktopLayoutComponent", () => {
useValue: mock<I18nService>(),
},
],
}).compileComponents();
})
.overrideComponent(DesktopLayoutComponent, {
remove: { imports: [SendFiltersNavComponent] },
add: { imports: [MockSendFiltersNavComponent] },
})
.compileComponents();
fixture = TestBed.createComponent(DesktopLayoutComponent);
component = fixture.componentInstance;
@@ -58,4 +74,11 @@ describe("DesktopLayoutComponent", () => {
expect(ngContent).toBeTruthy();
});
it("renders send filters navigation component", () => {
const compiled = fixture.nativeElement;
const sendFiltersNav = compiled.querySelector("app-send-filters-nav");
expect(sendFiltersNav).toBeTruthy();
});
});

View File

@@ -5,13 +5,22 @@ import { PasswordManagerLogo } from "@bitwarden/assets/svg";
import { LayoutComponent, NavigationModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
import { DesktopSideNavComponent } from "./desktop-side-nav.component";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-layout",
imports: [RouterModule, I18nPipe, LayoutComponent, NavigationModule, DesktopSideNavComponent],
imports: [
RouterModule,
I18nPipe,
LayoutComponent,
NavigationModule,
DesktopSideNavComponent,
SendFiltersNavComponent,
],
templateUrl: "./desktop-layout.component.html",
})
export class DesktopLayoutComponent {

View File

@@ -51,6 +51,7 @@ import {
} from "@bitwarden/common/auth/abstractions/auth.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { ClientType } from "@bitwarden/common/enums";
@@ -102,6 +103,7 @@ import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/s
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { GeneratorServicesModule } from "@bitwarden/generator-components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import {
KdfConfigService,
@@ -166,12 +168,12 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: BiometricsService,
useClass: RendererBiometricsService,
deps: [],
deps: [TokenService],
}),
safeProvider({
provide: DesktopBiometricsService,
useClass: RendererBiometricsService,
deps: [],
deps: [TokenService],
}),
safeProvider(NativeMessagingService),
safeProvider(BiometricMessageHandlerService),
@@ -201,8 +203,16 @@ const safeProviders: SafeProvider[] = [
// We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid
// the TokenService having to inject the PlatformUtilsService which introduces a
// circular dependency on Desktop only.
//
// For Windows portable builds, we disable secure storage to ensure tokens are
// stored on disk (in bitwarden-appdata) rather than in Windows Credential
// Manager, making them portable across machines. This allows users to move the USB drive
// between computers while maintaining authentication.
//
// Note: Portable mode does not use secure storage for read/write/clear operations,
// preventing any collision with tokens from a regular desktop installation.
provide: SUPPORTS_SECURE_STORAGE,
useValue: ELECTRON_SUPPORTS_SECURE_STORAGE,
useValue: ELECTRON_SUPPORTS_SECURE_STORAGE && !ipc.platform.isWindowsPortable,
}),
safeProvider({
provide: DEFAULT_VAULT_TIMEOUT,
@@ -499,7 +509,7 @@ const safeProviders: SafeProvider[] = [
];
@NgModule({
imports: [JslibServicesModule],
imports: [JslibServicesModule, GeneratorServicesModule],
declarations: [],
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function
providers: safeProviders,

View File

@@ -1,5 +1,5 @@
<bit-dialog #dialog dialogSize="large">
<span bitDialogTitle>{{ "exportVault" | i18n }}</span>
<span bitDialogTitle>{{ "export" | i18n }}</span>
<ng-container bitDialogContent>
<tools-export
(formLoading)="this.loading = $event"
@@ -17,7 +17,7 @@
bitFormButton
buttonType="primary"
>
{{ "exportVault" | i18n }}
{{ "export" | i18n }}
</button>
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}

View File

@@ -1,5 +1,5 @@
<bit-dialog #dialog dialogSize="large" background="alt">
<span bitDialogTitle>{{ "importData" | i18n }}</span>
<span bitDialogTitle>{{ "import" | i18n }}</span>
<ng-container bitDialogContent>
<div class="tw-relative">
<tools-import
@@ -27,7 +27,7 @@
bitFormButton
buttonType="primary"
>
{{ "importData" | i18n }}
{{ "import" | i18n }}
</button>
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}

View File

@@ -0,0 +1,25 @@
<bit-nav-group
icon="bwi-send"
[text]="'send' | i18n"
route="new-sends"
(click)="selectTypeAndNavigate()"
>
<bit-nav-item
icon="bwi-send"
[text]="'allSends' | i18n"
(click)="selectTypeAndNavigate(null); $event.stopPropagation()"
[forceActiveStyles]="activeSendType() === null"
></bit-nav-item>
<bit-nav-item
icon="bwi-file-text"
[text]="'sendTypeText' | i18n"
(click)="selectTypeAndNavigate(SendType.Text); $event.stopPropagation()"
[forceActiveStyles]="activeSendType() === SendType.Text"
></bit-nav-item>
<bit-nav-item
icon="bwi-file"
[text]="'sendTypeFile' | i18n"
(click)="selectTypeAndNavigate(SendType.File); $event.stopPropagation()"
[forceActiveStyles]="activeSendType() === SendType.File"
></bit-nav-item>
</bit-nav-group>

View File

@@ -0,0 +1,204 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Router, provideRouter } from "@angular/router";
import { RouterTestingHarness } from "@angular/router/testing";
import { BehaviorSubject } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { NavigationModule } from "@bitwarden/components";
import { SendListFiltersService } from "@bitwarden/send-ui";
import { SendFiltersNavComponent } from "./send-filters-nav.component";
@Component({ template: "", changeDetection: ChangeDetectionStrategy.OnPush })
class DummyComponent {}
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: true,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
describe("SendFiltersNavComponent", () => {
let component: SendFiltersNavComponent;
let fixture: ComponentFixture<SendFiltersNavComponent>;
let harness: RouterTestingHarness;
let filterFormValueSubject: BehaviorSubject<{ sendType: SendType | null }>;
let mockSendListFiltersService: Partial<SendListFiltersService>;
beforeEach(async () => {
filterFormValueSubject = new BehaviorSubject<{ sendType: SendType | null }>({
sendType: null,
});
mockSendListFiltersService = {
filterForm: {
value: { sendType: null },
valueChanges: filterFormValueSubject.asObservable(),
patchValue: jest.fn((value) => {
mockSendListFiltersService.filterForm.value = {
...mockSendListFiltersService.filterForm.value,
...value,
};
filterFormValueSubject.next(mockSendListFiltersService.filterForm.value);
}),
} as any,
filters$: filterFormValueSubject.asObservable(),
};
await TestBed.configureTestingModule({
imports: [SendFiltersNavComponent, NavigationModule],
providers: [
provideRouter([
{ path: "vault", component: DummyComponent },
{ path: "new-sends", component: DummyComponent },
]),
{
provide: SendListFiltersService,
useValue: mockSendListFiltersService,
},
{
provide: I18nService,
useValue: {
t: jest.fn((key) => key),
},
},
],
}).compileComponents();
// Create harness and navigate to initial route
harness = await RouterTestingHarness.create("/vault");
// Create the component fixture separately (not a routed component)
fixture = TestBed.createComponent(SendFiltersNavComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("creates component", () => {
expect(component).toBeTruthy();
});
it("renders bit-nav-group with Send icon and text", () => {
const compiled = fixture.nativeElement;
const navGroup = compiled.querySelector("bit-nav-group");
expect(navGroup).toBeTruthy();
expect(navGroup.getAttribute("icon")).toBe("bwi-send");
});
it("component exposes SendType enum for template", () => {
expect(component["SendType"]).toBe(SendType);
});
describe("isSendRouteActive", () => {
it("returns true when on /new-sends route", async () => {
await harness.navigateByUrl("/new-sends");
fixture.detectChanges();
expect(component["isSendRouteActive"]()).toBe(true);
});
it("returns false when not on /new-sends route", () => {
expect(component["isSendRouteActive"]()).toBe(false);
});
});
describe("activeSendType", () => {
it("returns the active send type when on send route and filter type is set", async () => {
await harness.navigateByUrl("/new-sends");
mockSendListFiltersService.filterForm.value = { sendType: SendType.Text };
filterFormValueSubject.next({ sendType: SendType.Text });
fixture.detectChanges();
expect(component["activeSendType"]()).toBe(SendType.Text);
});
it("returns undefined when not on send route", () => {
mockSendListFiltersService.filterForm.value = { sendType: SendType.Text };
filterFormValueSubject.next({ sendType: SendType.Text });
fixture.detectChanges();
expect(component["activeSendType"]()).toBeUndefined();
});
it("returns null when on send route but no type is selected", async () => {
await harness.navigateByUrl("/new-sends");
mockSendListFiltersService.filterForm.value = { sendType: null };
filterFormValueSubject.next({ sendType: null });
fixture.detectChanges();
expect(component["activeSendType"]()).toBeNull();
});
});
describe("selectTypeAndNavigate", () => {
it("clears the sendType filter when called with no parameter", async () => {
await component["selectTypeAndNavigate"]();
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
sendType: null,
});
});
it("updates filter form with Text type", async () => {
await component["selectTypeAndNavigate"](SendType.Text);
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
sendType: SendType.Text,
});
});
it("updates filter form with File type", async () => {
await component["selectTypeAndNavigate"](SendType.File);
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
sendType: SendType.File,
});
});
it("navigates to /new-sends when not on send route", async () => {
expect(harness.routeNativeElement?.textContent).toBeDefined();
await component["selectTypeAndNavigate"](SendType.Text);
const currentUrl = TestBed.inject(Router).url;
expect(currentUrl).toBe("/new-sends");
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
sendType: SendType.Text,
});
});
it("does not navigate when already on send route (component is reactive)", async () => {
await harness.navigateByUrl("/new-sends");
const router = TestBed.inject(Router);
const navigateSpy = jest.spyOn(router, "navigate");
await component["selectTypeAndNavigate"](SendType.Text);
expect(navigateSpy).not.toHaveBeenCalled();
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
sendType: SendType.Text,
});
});
it("navigates when clearing filter from different route", async () => {
await component["selectTypeAndNavigate"](); // No parameter = clear filter
const currentUrl = TestBed.inject(Router).url;
expect(currentUrl).toBe("/new-sends");
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
sendType: null,
});
});
});
});

View File

@@ -0,0 +1,54 @@
import { CommonModule } from "@angular/common";
import { ChangeDetectionStrategy, Component, computed, inject } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { NavigationEnd, Router } from "@angular/router";
import { filter, map, startWith } from "rxjs";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { NavigationModule } from "@bitwarden/components";
import { SendListFiltersService } from "@bitwarden/send-ui";
import { I18nPipe } from "@bitwarden/ui-common";
/**
* Navigation component that renders Send filter options in the sidebar.
* Fully reactive using signals - no manual subscriptions or method-based computed values.
* - Parent "Send" nav-group clears filter (shows all sends)
* - Child "Text"/"File" items set filter to specific type
* - Active states computed reactively from filter signal + route signal
*/
@Component({
selector: "app-send-filters-nav",
templateUrl: "./send-filters-nav.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, NavigationModule, I18nPipe],
})
export class SendFiltersNavComponent {
protected readonly SendType = SendType;
private readonly filtersService = inject(SendListFiltersService);
private readonly router = inject(Router);
private readonly currentFilter = toSignal(this.filtersService.filters$);
// Track whether current route is the send route
private readonly isSendRouteActive = toSignal(
this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map((event) => (event as NavigationEnd).urlAfterRedirects.includes("/new-sends")),
startWith(this.router.url.includes("/new-sends")),
),
{ initialValue: this.router.url.includes("/new-sends") },
);
// Computed: Active send type (null when on send route with no filter, undefined when not on send route)
protected readonly activeSendType = computed(() => {
return this.isSendRouteActive() ? this.currentFilter()?.sendType : undefined;
});
// Update send filter and navigate to /new-sends (only if not already there - send-v2 component reacts to filter changes)
protected async selectTypeAndNavigate(type?: SendType): Promise<void> {
this.filtersService.filterForm.patchValue({ sendType: type !== undefined ? type : null });
if (!this.router.url.includes("/new-sends")) {
await this.router.navigate(["/new-sends"]);
}
}
}

View File

@@ -1,4 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectorRef } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { FormBuilder } from "@angular/forms";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
@@ -15,6 +19,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { SendListFiltersService } from "@bitwarden/send-ui";
import * as utils from "../../../utils";
import { SearchBarService } from "../../layout/search/search-bar.service";
@@ -35,6 +40,8 @@ describe("SendV2Component", () => {
let broadcasterService: MockProxy<BroadcasterService>;
let accountService: MockProxy<AccountService>;
let policyService: MockProxy<PolicyService>;
let sendListFiltersService: SendListFiltersService;
let changeDetectorRef: MockProxy<ChangeDetectorRef>;
beforeEach(async () => {
sendService = mock<SendService>();
@@ -42,6 +49,13 @@ describe("SendV2Component", () => {
broadcasterService = mock<BroadcasterService>();
accountService = mock<AccountService>();
policyService = mock<PolicyService>();
changeDetectorRef = mock<ChangeDetectorRef>();
// Create real SendListFiltersService with mocked dependencies
const formBuilder = new FormBuilder();
const i18nService = mock<I18nService>();
i18nService.t.mockImplementation((key: string) => key);
sendListFiltersService = new SendListFiltersService(i18nService, formBuilder);
// Mock sendViews$ observable
sendService.sendViews$ = of([]);
@@ -51,6 +65,10 @@ describe("SendV2Component", () => {
accountService.activeAccount$ = of({ id: "test-user-id" } as any);
policyService.policyAppliesToUser$ = jest.fn().mockReturnValue(of(false));
// Mock SearchService methods needed by base component
const mockSearchService = mock<SearchService>();
mockSearchService.isSearchable.mockResolvedValue(false);
await TestBed.configureTestingModule({
imports: [SendV2Component],
providers: [
@@ -59,7 +77,7 @@ describe("SendV2Component", () => {
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: EnvironmentService, useValue: mock<EnvironmentService>() },
{ provide: BroadcasterService, useValue: broadcasterService },
{ provide: SearchService, useValue: mock<SearchService>() },
{ provide: SearchService, useValue: mockSearchService },
{ provide: PolicyService, useValue: policyService },
{ provide: SearchBarService, useValue: searchBarService },
{ provide: LogService, useValue: mock<LogService>() },
@@ -67,6 +85,8 @@ describe("SendV2Component", () => {
{ provide: DialogService, useValue: mock<DialogService>() },
{ provide: ToastService, useValue: mock<ToastService>() },
{ provide: AccountService, useValue: accountService },
{ provide: SendListFiltersService, useValue: sendListFiltersService },
{ provide: ChangeDetectorRef, useValue: changeDetectorRef },
],
}).compileComponents();
@@ -331,7 +351,6 @@ describe("SendV2Component", () => {
describe("load", () => {
it("sets loading states correctly", async () => {
jest.spyOn(component, "search").mockResolvedValue();
jest.spyOn(component, "selectAll");
expect(component.loaded).toBeFalsy();
@@ -341,14 +360,17 @@ describe("SendV2Component", () => {
expect(component.loaded).toBe(true);
});
it("calls selectAll when onSuccessfulLoad is not set", async () => {
it("sets up sendViews$ subscription", async () => {
const mockSends = [new SendView(), new SendView()];
sendService.sendViews$ = of(mockSends);
jest.spyOn(component, "search").mockResolvedValue();
jest.spyOn(component, "selectAll");
component.onSuccessfulLoad = null;
await component.load();
expect(component.selectAll).toHaveBeenCalled();
// Give observable time to emit
await new Promise((resolve) => setTimeout(resolve, 10));
expect(component.sends).toEqual(mockSends);
});
it("calls onSuccessfulLoad when it is set", async () => {

View File

@@ -2,8 +2,9 @@
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, OnInit, OnDestroy, ViewChild, NgZone, ChangeDetectorRef } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormsModule } from "@angular/forms";
import { mergeMap } from "rxjs";
import { mergeMap, Subscription } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
@@ -14,11 +15,13 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { SendListFiltersService } from "@bitwarden/send-ui";
import { invokeMenu, RendererMenuItem } from "../../../utils";
import { SearchBarService } from "../../layout/search/search-bar.service";
@@ -55,6 +58,9 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
// Tracks the current UI state: viewing list (None), adding new Send (Add), or editing existing Send (Edit)
action: Action = Action.None;
// Subscription for sendViews$ cleanup
private sendViewsSubscription: Subscription;
constructor(
sendService: SendService,
i18nService: I18nService,
@@ -71,6 +77,7 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
toastService: ToastService,
accountService: AccountService,
private cdr: ChangeDetectorRef,
private sendListFiltersService: SendListFiltersService,
) {
super(
sendService,
@@ -88,11 +95,16 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
);
// Listen to search bar changes and update the Send list filter
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.searchBarService.searchText$.subscribe((searchText) => {
this.searchBarService.searchText$.pipe(takeUntilDestroyed()).subscribe((searchText) => {
this.searchText = searchText;
this.searchTextChanged();
setTimeout(() => this.cdr.detectChanges(), 250);
});
// Listen to filter changes from sidebar navigation
this.sendListFiltersService.filterForm.valueChanges
.pipe(takeUntilDestroyed())
.subscribe((filters) => {
this.applySendTypeFilter(filters);
});
}
@@ -103,6 +115,10 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
await super.ngOnInit();
// Read current filter synchronously to avoid race condition on navigation
const currentFilter = this.sendListFiltersService.filterForm.value;
this.applySendTypeFilter(currentFilter);
// Listen for sync completion events to refresh the Send list
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
@@ -118,8 +134,18 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
await this.load();
}
// Apply send type filter to display: centralized logic for initial load and filter changes
private applySendTypeFilter(filters: Partial<{ sendType: SendType | null }>): void {
if (filters.sendType === null || filters.sendType === undefined) {
this.selectAll();
} else {
this.selectType(filters.sendType);
}
}
// Clean up subscriptions and disable search bar when component is destroyed
ngOnDestroy() {
this.sendViewsSubscription?.unsubscribe();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.searchBarService.setEnabled(false);
}
@@ -130,7 +156,12 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
// Note: The filter parameter is ignored in this implementation for desktop-specific behavior.
async load(filter: (send: SendView) => boolean = null) {
this.loading = true;
this.sendService.sendViews$
// Recreate subscription on each load (required for sync refresh)
// Manual cleanup in ngOnDestroy is intentional - load() is called multiple times
this.sendViewsSubscription?.unsubscribe();
this.sendViewsSubscription = this.sendService.sendViews$
.pipe(
mergeMap(async (sends) => {
this.sends = sends;
@@ -143,9 +174,6 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
.subscribe();
if (this.onSuccessfulLoad != null) {
await this.onSuccessfulLoad();
} else {
// Default action
this.selectAll();
}
this.loading = false;
this.loaded = true;

View File

@@ -136,6 +136,7 @@ describe("DesktopLoginComponentService", () => {
codeChallenge,
state,
email,
undefined,
);
} else {
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
@@ -145,4 +146,55 @@ describe("DesktopLoginComponentService", () => {
});
});
});
describe("redirectToSsoLoginWithOrganizationSsoIdentifier", () => {
// Array of all permutations of isAppImage and isDev
const permutations = [
[true, false], // Case 1: isAppImage true
[false, true], // Case 2: isDev true
[true, true], // Case 3: all true
[false, false], // Case 4: all false
];
permutations.forEach(([isAppImage, isDev]) => {
it("calls redirectToSso with orgSsoIdentifier", async () => {
(global as any).ipc.platform.isAppImage = isAppImage;
(global as any).ipc.platform.isDev = isDev;
const email = "test@bitwarden.com";
const state = "testState";
const codeVerifier = "testCodeVerifier";
const codeChallenge = "testCodeChallenge";
const orgSsoIdentifier = "orgSsoId";
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
await service.redirectToSsoLoginWithOrganizationSsoIdentifier(email, orgSsoIdentifier);
if (isAppImage || isDev) {
expect(ipc.platform.localhostCallbackService.openSsoPrompt).toHaveBeenCalledWith(
codeChallenge,
state,
email,
orgSsoIdentifier,
);
} else {
expect(ssoUrlService.buildSsoUrl).toHaveBeenCalledWith(
expect.any(String),
expect.any(String),
expect.any(String),
expect.any(String),
expect.any(String),
email,
orgSsoIdentifier,
);
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
expect(platformUtilsService.launchUri).toHaveBeenCalled();
}
});
});
});
});

View File

@@ -48,11 +48,12 @@ export class DesktopLoginComponentService
email: string,
state: string,
codeChallenge: string,
orgSsoIdentifier?: string,
): Promise<void> {
// For platforms that cannot support a protocol-based (e.g. bitwarden://) callback, we use a localhost callback
// Otherwise, we launch the SSO component in a browser window and wait for the callback
if (ipc.platform.isAppImage || ipc.platform.isDev) {
await this.initiateSsoThroughLocalhostCallback(email, state, codeChallenge);
await this.initiateSsoThroughLocalhostCallback(email, state, codeChallenge, orgSsoIdentifier);
} else {
const env = await firstValueFrom(this.environmentService.environment$);
const webVaultUrl = env.getWebVaultUrl();
@@ -66,6 +67,7 @@ export class DesktopLoginComponentService
state,
codeChallenge,
email,
orgSsoIdentifier,
);
this.platformUtilsService.launchUri(ssoWebAppUrl);
@@ -76,9 +78,15 @@ export class DesktopLoginComponentService
email: string,
state: string,
challenge: string,
orgSsoIdentifier?: string,
): Promise<void> {
try {
await ipc.platform.localhostCallbackService.openSsoPrompt(challenge, state, email);
await ipc.platform.localhostCallbackService.openSsoPrompt(
challenge,
state,
email,
orgSsoIdentifier,
);
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {

View File

@@ -37,7 +37,7 @@ export class MainSshAgentService {
init() {
// handle sign request passing to UI
sshagent
.serve(async (err: Error, sshUiRequest: sshagent.SshUiRequest) => {
.serve(async (err: Error | null, sshUiRequest: sshagent.SshUiRequest): Promise<boolean> => {
// clear all old (> SIGN_TIMEOUT) requests
this.requestResponses = this.requestResponses.filter(
(response) => response.timestamp > new Date(Date.now() - this.SIGN_TIMEOUT),

View File

@@ -7,6 +7,7 @@ import { AccountService, Account } from "@bitwarden/common/auth/abstractions/acc
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
@@ -40,9 +41,10 @@ describe("Fido2CreateComponent", () => {
const activeAccountSubject = new BehaviorSubject<Account | null>({
id: "test-user-id" as UserId,
...mockAccountInfoWith({
email: "test@example.com",
emailVerified: true,
name: "Test User",
}),
});
beforeEach(async () => {

View File

@@ -10,6 +10,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Account, UserId } from "@bitwarden/common/platform/models/domain/account";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service";
@@ -30,9 +31,10 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
beforeEach(() => {
mockAccountSubject = new BehaviorSubject<Account | null>({
id: mockUserId,
...mockAccountInfoWith({
email: "test@example.com",
emailVerified: true,
name: "Test User",
}),
});
mockFeatureFlagSubject = new BehaviorSubject<boolean>(true);
mockAuthStatusSubject = new BehaviorSubject<AuthenticationStatus>(

View File

@@ -43,9 +43,7 @@ export type NativeWindowObject = {
windowXy?: { x: number; y: number };
};
export class DesktopFido2UserInterfaceService
implements Fido2UserInterfaceServiceAbstraction<NativeWindowObject>
{
export class DesktopFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction<NativeWindowObject> {
constructor(
private authService: AuthService,
private cipherService: CipherService,

View File

@@ -1,5 +1,7 @@
import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
@@ -13,6 +15,10 @@ import { DesktopBiometricsService } from "./desktop.biometrics.service";
*/
@Injectable()
export class RendererBiometricsService extends DesktopBiometricsService {
constructor(private tokenService: TokenService) {
super();
}
async authenticateWithBiometrics(): Promise<boolean> {
return await ipc.keyManagement.biometric.authenticateWithBiometrics();
}
@@ -31,6 +37,10 @@ export class RendererBiometricsService extends DesktopBiometricsService {
}
async getBiometricsStatusForUser(id: UserId): Promise<BiometricsStatus> {
if ((await firstValueFrom(this.tokenService.hasAccessToken$(id))) === false) {
return BiometricsStatus.NotEnabledInConnectedDesktopApp;
}
return await ipc.keyManagement.biometric.getBiometricsStatusForUser(id);
}

View File

@@ -1,20 +0,0 @@
<div id="remove-password-page" *ngIf="!loading">
<div class="content">
<h1>{{ "removeMasterPassword" | i18n }}</h1>
<p>{{ "removeMasterPasswordForOrganizationUserKeyConnector" | i18n }}</p>
<p class="tw-mb-0">{{ "organizationName" | i18n }}:</p>
<p class="tw-text-muted tw-mb-6">{{ organization.name }}</p>
<p class="tw-mb-0">{{ "keyConnectorDomain" | i18n }}:</p>
<p class="tw-text-muted tw-mb-6">{{ organization.keyConnectorUrl }}</p>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="action" (click)="convert()">
<b [hidden]="continuing">{{ "removeMasterPassword" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!continuing" aria-hidden="true"></i>
</button>
<button type="button" class="btn secondary block" [disabled]="action" (click)="leave()">
<b [hidden]="leaving">{{ "leaveOrganization" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!leaving" aria-hidden="true"></i>
</button>
</div>
</div>
</div>

View File

@@ -1,12 +0,0 @@
import { Component } from "@angular/core";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-remove-password",
templateUrl: "remove-password.component.html",
standalone: false,
})
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}

View File

@@ -708,6 +708,9 @@
"addAttachment": {
"message": "Add attachment"
},
"itemsTransferred": {
"message": "Items transferred"
},
"fixEncryption": {
"message": "Fix encryption"
},
@@ -1195,8 +1198,8 @@
"followUs": {
"message": "Follow us"
},
"syncVault": {
"message": "Sync vault"
"syncNow": {
"message": "Sync now"
},
"changeMasterPass": {
"message": "Change master password"
@@ -1772,8 +1775,11 @@
"exportFrom": {
"message": "Export from"
},
"exportVault": {
"message": "Export vault"
"export": {
"message": "Export"
},
"import": {
"message": "Import"
},
"fileFormat": {
"message": "File format"
@@ -2634,9 +2640,6 @@
"removedMasterPassword": {
"message": "Master password removed"
},
"removeMasterPasswordForOrganizationUserKeyConnector": {
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
},
"organizationName": {
"message": "Organization name"
},
@@ -3492,10 +3495,6 @@
"aliasDomain": {
"message": "Alias domain"
},
"importData": {
"message": "Import data",
"description": "Used for the desktop menu item and the header of the import dialog"
},
"importError": {
"message": "Import error"
},
@@ -4334,6 +4333,45 @@
"upgradeToPremium": {
"message": "Upgrade to Premium"
},
"removeMasterPasswordForOrgUserKeyConnector":{
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
},
"continueWithLogIn": {
"message": "Continue with log in"
},
"doNotContinue": {
"message": "Do not continue"
},
"domain": {
"message": "Domain"
},
"keyConnectorDomainTooltip": {
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
},
"verifyYourOrganization": {
"message": "Verify your organization to log in"
},
"organizationVerified":{
"message": "Organization verified"
},
"domainVerified":{
"message": "Domain verified"
},
"leaveOrganizationContent": {
"message": "If you don't verify your organization, your access to the organization will be revoked."
},
"leaveNow": {
"message": "Leave now"
},
"verifyYourDomainToLogin": {
"message": "Verify your domain to log in"
},
"verifyYourDomainDescription": {
"message": "To continue with log in, verify this domain."
},
"confirmKeyConnectorOrganizationUserDescription": {
"message": "To continue with log in, verify the organization and domain."
},
"sessionTimeoutSettingsAction": {
"message": "Timeout action"
},
@@ -4395,5 +4433,53 @@
},
"upgrade": {
"message": "Upgrade"
},
"leaveConfirmationDialogTitle": {
"message": "Are you sure you want to leave?"
},
"leaveConfirmationDialogContentOne": {
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
},
"leaveConfirmationDialogContentTwo": {
"message": "Contact your admin to regain access."
},
"leaveConfirmationDialogConfirmButton": {
"message": "Leave $ORGANIZATION$",
"placeholders": {
"organization": {
"content": "$1",
"example": "My Org Name"
}
}
},
"howToManageMyVault": {
"message": "How do I manage my vault?"
},
"transferItemsToOrganizationTitle": {
"message": "Transfer items to $ORGANIZATION$",
"placeholders": {
"organization": {
"content": "$1",
"example": "My Org Name"
}
}
},
"transferItemsToOrganizationContent": {
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
"placeholders": {
"organization": {
"content": "$1",
"example": "My Org Name"
}
}
},
"acceptTransfer": {
"message": "Accept transfer"
},
"declineAndLeave": {
"message": "Decline and leave"
},
"whyAmISeeingThis": {
"message": "Why am I seeing this?"
}
}

View File

@@ -146,8 +146,8 @@ export class FileMenu extends FirstMenu implements IMenubarMenu {
private get syncVault(): MenuItemConstructorOptions {
return {
id: "syncVault",
label: this.localize("syncVault"),
id: "syncNow",
label: this.localize("syncNow"),
click: () => this.sendMessage("syncVault"),
enabled: this.hasAuthenticatedAccounts,
};
@@ -155,8 +155,8 @@ export class FileMenu extends FirstMenu implements IMenubarMenu {
private get importVault(): MenuItemConstructorOptions {
return {
id: "importVault",
label: this.localize("importData"),
id: "import",
label: this.localize("import"),
click: () => this.sendMessage("importVault"),
enabled: !this._isLocked,
};
@@ -164,8 +164,8 @@ export class FileMenu extends FirstMenu implements IMenubarMenu {
private get exportVault(): MenuItemConstructorOptions {
return {
id: "exportVault",
label: this.localize("exportVault"),
id: "export",
label: this.localize("export"),
click: () => this.sendMessage("exportVault"),
enabled: !this._isLocked,
};

View File

@@ -14,7 +14,7 @@ import { isDev } from "../utils";
import { WindowMain } from "./window.main";
export class NativeMessagingMain {
private ipcServer: ipc.IpcServer | null;
private ipcServer: ipc.NativeIpcServer | null;
private connected: number[] = [];
constructor(
@@ -78,7 +78,7 @@ export class NativeMessagingMain {
this.ipcServer.stop();
}
this.ipcServer = await ipc.IpcServer.listen("bw", (error, msg) => {
this.ipcServer = await ipc.NativeIpcServer.listen("bw", (error, msg) => {
switch (msg.kind) {
case ipc.IpcMessageType.Connected: {
this.connected.push(msg.clientId);

View File

@@ -1,12 +1,12 @@
{
"name": "@bitwarden/desktop",
"version": "2025.12.0",
"version": "2025.12.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/desktop",
"version": "2025.12.0",
"version": "2025.12.1",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi"

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.12.0",
"version": "2025.12.1",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",

View File

@@ -2,13 +2,11 @@
<bit-dialog>
<div class="tw-font-medium" bitDialogTitle>{{ "sshkeyApprovalTitle" | i18n }}</div>
<div bitDialogContent>
<app-callout
type="warning"
title="{{ 'agentForwardingWarningTitle' | i18n }}"
*ngIf="params.isAgentForwarding"
>
@if (params.isAgentForwarding) {
<bit-callout type="warning" title="{{ 'agentForwardingWarningTitle' | i18n }}">
{{ 'agentForwardingWarningText' | i18n }}
</app-callout>
</bit-callout>
}
<b>{{params.applicationName}}</b> {{ "sshkeyApprovalMessageInfix" | i18n }}
<b>{{params.cipherName}}</b>

View File

@@ -12,6 +12,7 @@ import {
FormFieldModule,
IconButtonModule,
DialogService,
CalloutModule,
} from "@bitwarden/components";
export interface ApproveSshRequestParams {
@@ -35,6 +36,7 @@ export interface ApproveSshRequestParams {
ReactiveFormsModule,
AsyncActionsModule,
FormFieldModule,
CalloutModule,
],
})
export class ApproveSshRequestComponent {

View File

@@ -21,7 +21,7 @@ export type RunCommandParams<C extends CommandDefinition> = {
export type RunCommandResult<C extends CommandDefinition> = C["output"];
export class NativeAutofillMain {
private ipcServer: autofill.IpcServer | null;
private ipcServer?: autofill.AutofillIpcServer;
private messageBuffer: BufferedMessage[] = [];
private listenerReady = false;
@@ -70,13 +70,13 @@ export class NativeAutofillMain {
},
);
this.ipcServer = await autofill.IpcServer.listen(
this.ipcServer = await autofill.AutofillIpcServer.listen(
"af",
// RegistrationCallback
(error, clientId, sequenceNumber, request) => {
if (error) {
this.logService.error("autofill.IpcServer.registration", error);
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
return;
}
this.safeSend("autofill.passkeyRegistration", {
@@ -89,7 +89,7 @@ export class NativeAutofillMain {
(error, clientId, sequenceNumber, request) => {
if (error) {
this.logService.error("autofill.IpcServer.assertion", error);
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
return;
}
this.safeSend("autofill.passkeyAssertion", {
@@ -102,7 +102,7 @@ export class NativeAutofillMain {
(error, clientId, sequenceNumber, request) => {
if (error) {
this.logService.error("autofill.IpcServer.assertion", error);
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
return;
}
this.safeSend("autofill.passkeyAssertionWithoutUserInterface", {
@@ -115,7 +115,7 @@ export class NativeAutofillMain {
(error, clientId, sequenceNumber, status) => {
if (error) {
this.logService.error("autofill.IpcServer.nativeStatus", error);
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
return;
}
this.safeSend("autofill.nativeStatus", {
@@ -137,19 +137,19 @@ export class NativeAutofillMain {
ipcMain.on("autofill.completePasskeyRegistration", (event, data) => {
this.logService.debug("autofill.completePasskeyRegistration", data);
const { clientId, sequenceNumber, response } = data;
this.ipcServer.completeRegistration(clientId, sequenceNumber, response);
this.ipcServer?.completeRegistration(clientId, sequenceNumber, response);
});
ipcMain.on("autofill.completePasskeyAssertion", (event, data) => {
this.logService.debug("autofill.completePasskeyAssertion", data);
const { clientId, sequenceNumber, response } = data;
this.ipcServer.completeAssertion(clientId, sequenceNumber, response);
this.ipcServer?.completeAssertion(clientId, sequenceNumber, response);
});
ipcMain.on("autofill.completeError", (event, data) => {
this.logService.debug("autofill.completeError", data);
const { clientId, sequenceNumber, error } = data;
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
});
}

View File

@@ -17,6 +17,7 @@ import {
isFlatpak,
isMacAppStore,
isSnapStore,
isWindowsPortable,
isWindowsStore,
} from "../utils";
@@ -108,8 +109,13 @@ const ephemeralStore = {
};
const localhostCallbackService = {
openSsoPrompt: (codeChallenge: string, state: string, email: string): Promise<void> => {
return ipcRenderer.invoke("openSsoPrompt", { codeChallenge, state, email });
openSsoPrompt: (
codeChallenge: string,
state: string,
email: string,
orgSsoIdentifier?: string,
): Promise<void> => {
return ipcRenderer.invoke("openSsoPrompt", { codeChallenge, state, email, orgSsoIdentifier });
},
};
@@ -128,6 +134,7 @@ export default {
isDev: isDev(),
isMacAppStore: isMacAppStore(),
isWindowsStore: isWindowsStore(),
isWindowsPortable: isWindowsPortable(),
isFlatpak: isFlatpak(),
isSnapStore: isSnapStore(),
isAppImage: isAppImage(),

View File

@@ -22,7 +22,7 @@ export class ElectronLogMainService extends BaseLogService {
return;
}
log.transports.file.level = "info";
log.transports.file.level = isDev() ? "debug" : "info";
if (this.logDir != null) {
log.transports.file.resolvePathFn = () => path.join(this.logDir, "app.log");
}

View File

@@ -25,20 +25,25 @@ export class SSOLocalhostCallbackService {
private messagingService: MessageSender,
private ssoUrlService: SsoUrlService,
) {
ipcMain.handle("openSsoPrompt", async (event, { codeChallenge, state, email }) => {
ipcMain.handle(
"openSsoPrompt",
async (event, { codeChallenge, state, email, orgSsoIdentifier }) => {
// Close any existing server before starting new one
if (this.currentServer) {
await this.closeCurrentServer();
}
return this.openSsoPrompt(codeChallenge, state, email).then(({ ssoCode, recvState }) => {
return this.openSsoPrompt(codeChallenge, state, email, orgSsoIdentifier).then(
({ ssoCode, recvState }) => {
this.messagingService.send("ssoCallback", {
code: ssoCode,
state: recvState,
redirectUri: this.ssoRedirectUri,
});
});
});
},
);
},
);
}
private async closeCurrentServer(): Promise<void> {
@@ -58,6 +63,7 @@ export class SSOLocalhostCallbackService {
codeChallenge: string,
state: string,
email: string,
orgSsoIdentifier?: string,
): Promise<{ ssoCode: string; recvState: string }> {
const env = await firstValueFrom(this.environmentService.environment$);
@@ -121,6 +127,7 @@ export class SSOLocalhostCallbackService {
state,
codeChallenge,
email,
orgSsoIdentifier,
);
// Set up error handler before attempting to listen

View File

@@ -2,7 +2,7 @@ import { NgZone } from "@angular/core";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, filter, firstValueFrom, of, take, timeout, timer } from "rxjs";
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
@@ -10,7 +10,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService } from "@bitwarden/common/spec";
import { mockAccountInfoWith, FakeAccountService } from "@bitwarden/common/spec";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
@@ -23,17 +23,15 @@ import { BiometricMessageHandlerService } from "./biometric-message-handler.serv
const SomeUser = "SomeUser" as UserId;
const AnotherUser = "SomeOtherUser" as UserId;
const accounts: Record<UserId, AccountInfo> = {
[SomeUser]: {
const accounts = {
[SomeUser]: mockAccountInfoWith({
name: "some user",
email: "some.user@example.com",
emailVerified: true,
},
[AnotherUser]: {
}),
[AnotherUser]: mockAccountInfoWith({
name: "some other user",
email: "some.other.user@example.com",
emailVerified: true,
},
}),
};
describe("BiometricMessageHandlerService", () => {

View File

@@ -24,7 +24,7 @@ export const MaxCheckedCount = 500;
* Maximum for bulk reinvite operations when the IncreaseBulkReinviteLimitForCloud
* feature flag is enabled on cloud environments.
*/
export const CloudBulkReinviteLimit = 4000;
export const CloudBulkReinviteLimit = 8000;
/**
* Returns true if the user matches the status, or where the status is `null`, if the user is active (not revoked).

View File

@@ -104,12 +104,12 @@
*ngIf="organization.use2fa && organization.isOwner"
></bit-nav-item>
<bit-nav-item
[text]="'importData' | i18n"
[text]="'import' | i18n"
route="settings/tools/import"
*ngIf="organization.canAccessImport"
></bit-nav-item>
<bit-nav-item
[text]="'exportVault' | i18n"
[text]="'export' | i18n"
route="settings/tools/export"
*ngIf="canAccessExport$ | async"
></bit-nav-item>

Some files were not shown because too many files have changed in this diff Show More