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

View File

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

View File

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

View File

@@ -98,6 +98,14 @@ jobs:
working-directory: apps/desktop/artifacts working-directory: apps/desktop/artifacts
run: mv "Bitwarden-${PKG_VERSION}-universal.pkg" "Bitwarden-${PKG_VERSION}-universal.pkg.archive" 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 - name: Create Release
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
if: ${{ steps.release_channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }} 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 - name: Trigger test-all workflow in browser-interactions-testing
if: steps.changed-files.outputs.monitored == 'true' 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: with:
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
repository: "bitwarden/browser-interactions-testing" repository: "bitwarden/browser-interactions-testing"

View File

@@ -436,8 +436,8 @@
"sync": { "sync": {
"message": "Sync" "message": "Sync"
}, },
"syncVaultNow": { "syncNow": {
"message": "Sync vault now" "message": "Sync now"
}, },
"lastSync": { "lastSync": {
"message": "Last sync:" "message": "Last sync:"
@@ -455,9 +455,6 @@
"bitWebVaultApp": { "bitWebVaultApp": {
"message": "Bitwarden web app" "message": "Bitwarden web app"
}, },
"importItems": {
"message": "Import items"
},
"select": { "select": {
"message": "Select" "message": "Select"
}, },
@@ -1325,8 +1322,11 @@
"exportFrom": { "exportFrom": {
"message": "Export from" "message": "Export from"
}, },
"exportVault": { "export": {
"message": "Export vault" "message": "Export"
},
"import": {
"message": "Import"
}, },
"fileFormat": { "fileFormat": {
"message": "File format" "message": "File format"
@@ -1475,6 +1475,9 @@
"selectFile": { "selectFile": {
"message": "Select a file" "message": "Select a file"
}, },
"itemsTransferred": {
"message": "Items transferred"
},
"maxFileSize": { "maxFileSize": {
"message": "Maximum file size is 500 MB." "message": "Maximum file size is 500 MB."
}, },
@@ -3249,9 +3252,6 @@
"copyCustomFieldNameNotUnique": { "copyCustomFieldNameNotUnique": {
"message": "No unique identifier found." "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": { "organizationName": {
"message": "Organization name" "message": "Organization name"
}, },
@@ -4215,10 +4215,6 @@
"ignore": { "ignore": {
"message": "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": { "importError": {
"message": "Import error" "message": "Import error"
}, },
@@ -5888,6 +5884,45 @@
"cardNumberLabel": { "cardNumberLabel": {
"message": "Card number" "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": { "sessionTimeoutSettingsAction": {
"message": "Timeout action" "message": "Timeout action"
}, },
@@ -5937,5 +5972,53 @@
}, },
"upgrade": { "upgrade": {
"message": "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 <button
*ngIf="currentAccount$ | async as currentAccount; else defaultButton" *ngIf="currentAccount$ | async as currentAccount; else defaultButton"
type="button" 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()" (click)="currentAccountClicked()"
> >
<span class="tw-sr-only"> {{ "bitwardenAccount" | i18n }} {{ currentAccount.email }}</span> <span class="tw-sr-only"> {{ "bitwardenAccount" | i18n }} {{ currentAccount.email }}</span>

View File

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

View File

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

View File

@@ -3,9 +3,7 @@ import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/
/** /**
* Browser extension implementation of the device management component service * Browser extension implementation of the device management component service
*/ */
export class ExtensionDeviceManagementComponentService export class ExtensionDeviceManagementComponentService implements DeviceManagementComponentServiceAbstraction {
implements DeviceManagementComponentServiceAbstraction
{
/** /**
* Don't show header information in browser extension client * 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 { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { 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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { ExtensionCommand } from "@bitwarden/common/autofill/constants"; 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 { ThemeTypes } from "@bitwarden/common/platform/enums";
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.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 { UserId } from "@bitwarden/common/types/guid";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -80,11 +81,12 @@ describe("NotificationBackground", () => {
const organizationService = mock<OrganizationService>(); const organizationService = mock<OrganizationService>();
const userId = "testId" as UserId; const userId = "testId" as UserId;
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({ const activeAccountSubject = new BehaviorSubject({
id: userId, id: userId,
...mockAccountInfoWith({
email: "test@example.com", email: "test@example.com",
emailVerified: true,
name: "Test User", name: "Test User",
}),
}); });
beforeEach(() => { 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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@@ -123,9 +124,10 @@ describe("context-menu", () => {
autofillSettingsService.enableContextMenu$ = of(true); autofillSettingsService.enableContextMenu$ = of(true);
accountService.activeAccount$ = of({ accountService.activeAccount$ = of({
id: "userId" as UserId, id: "userId" as UserId,
...mockAccountInfoWith({
email: "", email: "",
emailVerified: false,
name: undefined, name: undefined,
}),
}); });
}); });

View File

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

View File

@@ -129,7 +129,12 @@ export class AutofillInlineMenuContainer {
} }
try { try {
const urlObj = new URL(url); 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) { if (!isExtensionProtocol) {
return false; return false;

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,11 @@ export class ForegroundBrowserBiometricsService extends BiometricsService {
result: BiometricsStatus; result: BiometricsStatus;
error: string; error: string;
}>(BiometricsCommands.GetBiometricsStatusForUser, { userId: id }); }>(BiometricsCommands.GetBiometricsStatusForUser, { userId: id });
if (response != null) {
return response.result; return response.result;
} else {
return BiometricsStatus.DesktopDisconnected;
}
} }
async getShouldAutopromptNow(): Promise<boolean> { 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", templateUrl: "popup-tab-navigation.component.html",
imports: [CommonModule, LinkModule, RouterModule, JslibModule, IconModule], imports: [CommonModule, LinkModule, RouterModule, JslibModule, IconModule],
host: { 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 { export class PopupTabNavigationComponent {

View File

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

View File

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

View File

@@ -13,8 +13,11 @@
</bit-callout> </bit-callout>
</div> </div>
} @else { } @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> <router-outlet #outlet="outlet"></router-outlet>
</div> </div>
<bit-toast-container></bit-toast-container> <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 { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.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 { PopOutComponent } from "../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
@@ -85,13 +84,7 @@ import "../platform/popup/locales";
CalloutModule, CalloutModule,
LinkModule, LinkModule,
], ],
declarations: [ declarations: [AppComponent, ColorPasswordPipe, ColorPasswordCountPipe, TabsV2Component],
AppComponent,
ColorPasswordPipe,
ColorPasswordCountPipe,
TabsV2Component,
RemovePasswordComponent,
],
exports: [CalloutModule], exports: [CalloutModule],
providers: [CurrencyPipe, DatePipe], providers: [CurrencyPipe, DatePipe],
bootstrap: [AppComponent], bootstrap: [AppComponent],

View File

@@ -1,31 +1,21 @@
<popup-page [disablePadding]="true"> <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 <auth-anon-layout
[title]="pageTitle" [title]="pageTitle"
[subtitle]="pageSubtitle" [subtitle]="pageSubtitle"
[icon]="pageIcon" [icon]="pageIcon"
[showReadonlyHostname]="showReadonlyHostname" [showReadonlyHostname]="showReadonlyHostname"
[hideLogo]="true" [hideLogo]="!showLogo"
[maxWidth]="maxWidth" [maxWidth]="maxWidth"
[hideFooter]="hideFooter" [hideFooter]="hideFooter"
[hideCardWrapper]="hideCardWrapper" [hideCardWrapper]="hideCardWrapper"
> >
<router-outlet></router-outlet> <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="secondary" name="secondary"></router-outlet>
<router-outlet slot="environment-selector" name="environment-selector"></router-outlet> <router-outlet slot="environment-selector" name="environment-selector"></router-outlet>
</auth-anon-layout> </auth-anon-layout>

View File

@@ -76,11 +76,14 @@ const decorators = (options: {
{ {
provide: AccountService, provide: AccountService,
useValue: { 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({ activeAccount$: of({
id: "test-user-id" as UserId, id: "test-user-id" as UserId,
name: "Test User 1", name: "Test User 1",
email: "test@email.com", email: "test@email.com",
emailVerified: true, 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 // 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 "../../../../../libs/angular/src/scss/bwicons/styles/style.scss";
@import "variables.scss"; @import "variables.scss";
@import "../../../../../libs/angular/src/scss/icons.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 "@angular/cdk/overlay-prebuilt.css";
@import "../../../../../libs/components/src/multi-select/scss/bw.theme"; @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 { @layer components {
/** Safari Support */ /** Safari Support */
@@ -19,4 +119,59 @@
html:not(.browser_safari) .tw-styled-scrollbar { html:not(.browser_safari) .tw-styled-scrollbar {
scrollbar-color: rgb(var(--color-secondary-500)) rgb(var(--color-background-alt)); 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-sans-serif: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; $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; $text-color: #000000;
$border-color: #f0f0f0;
$border-color-dark: #ddd; $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-primary: #175ddc;
$brand-danger: #c83522;
$brand-success: #017e45; $brand-success: #017e45;
$brand-info: #555555;
$brand-warning: #8b6609;
$brand-primary-accent: #1252a3;
$background-color: #f0f0f0; $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-primary: darken($brand-primary, 8%);
$button-color-danger: darken($brand-danger, 10%);
$code-color: #c01176;
$code-color-dark: #f08dc7;
$themes: ( $themes: (
light: ( light: (
textColor: $text-color, textColor: $text-color,
hoverColorTransparent: rgba($text-color, 0.15),
borderColor: $border-color-dark, borderColor: $border-color-dark,
backgroundColor: $background-color, 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%), inputBorderColor: darken($border-color-dark, 7%),
inputBackgroundColor: #ffffff, inputBackgroundColor: #ffffff,
inputPlaceholderColor: lighten($gray-light, 35%),
buttonBackgroundColor: $button-background-color,
buttonBorderColor: $button-border-color,
buttonColor: $button-color,
buttonPrimaryColor: $button-color-primary, buttonPrimaryColor: $button-color-primary,
buttonDangerColor: $button-color-danger,
primaryColor: $brand-primary, primaryColor: $brand-primary,
primaryAccentColor: $brand-primary-accent,
dangerColor: $brand-danger,
successColor: $brand-success, successColor: $brand-success,
infoColor: $brand-info,
warningColor: $brand-warning,
logoSuffix: "dark",
mfaLogoSuffix: ".png",
passwordNumberColor: #007fde, passwordNumberColor: #007fde,
passwordSpecialColor: #c40800, 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: ( dark: (
textColor: #ffffff, textColor: #ffffff,
hoverColorTransparent: rgba($text-color, 0.15),
borderColor: #161c26, borderColor: #161c26,
backgroundColor: #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, inputBorderColor: #4c525f,
inputBackgroundColor: #2f343d, inputBackgroundColor: #2f343d,
inputPlaceholderColor: #bac0ce,
buttonBackgroundColor: #3c424e,
buttonBorderColor: #4c525f,
buttonColor: #bac0ce,
buttonPrimaryColor: #6f9df1, buttonPrimaryColor: #6f9df1,
buttonDangerColor: #ff8d85,
primaryColor: #6f9df1, primaryColor: #6f9df1,
primaryAccentColor: #6f9df1,
dangerColor: #ff8d85,
successColor: #52e07c, successColor: #52e07c,
infoColor: #a4b0c6,
warningColor: #ffeb66,
logoSuffix: "white",
mfaLogoSuffix: "-w.png",
passwordNumberColor: #6f9df1, passwordNumberColor: #6f9df1,
passwordSpecialColor: #ff8d85, 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, DialogService,
ToastService, ToastService,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { GeneratorServicesModule } from "@bitwarden/generator-components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { import {
BiometricsService, BiometricsService,
@@ -743,7 +744,7 @@ const safeProviders: SafeProvider[] = [
]; ];
@NgModule({ @NgModule({
imports: [JslibServicesModule], imports: [JslibServicesModule, GeneratorServicesModule],
declarations: [], declarations: [],
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function // Do not register your dependency here! Add it to the typesafeProviders array using the helper function
providers: safeProviders, 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
@@ -96,10 +97,11 @@ describe("SendV2Component", () => {
useValue: { useValue: {
activeAccount$: of({ activeAccount$: of({
id: "123", id: "123",
...mockAccountInfoWith({
email: "test@email.com", email: "test@email.com",
emailVerified: true,
name: "Test User", name: "Test User",
}), }),
}),
}, },
}, },
{ provide: AuthService, useValue: mock<AuthService>() }, { provide: AuthService, useValue: mock<AuthService>() },

View File

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

View File

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

View File

@@ -51,6 +51,6 @@ export class AttachmentsV2Component {
/** Navigate the user back to the edit screen after uploading an attachment */ /** Navigate the user back to the edit screen after uploading an attachment */
async navigateBack() { 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 { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
@@ -60,10 +61,11 @@ describe("OpenAttachmentsComponent", () => {
const accountService = { const accountService = {
activeAccount$: of({ activeAccount$: of({
id: mockUserId, id: mockUserId,
...mockAccountInfoWith({
email: "test@email.com", email: "test@email.com",
emailVerified: true,
name: "Test User", name: "Test User",
}), }),
}),
}; };
const formStatusChange$ = new BehaviorSubject<"enabled" | "disabled">("enabled"); const formStatusChange$ = new BehaviorSubject<"enabled" | "disabled">("enabled");

View File

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

View File

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

View File

@@ -113,20 +113,14 @@ export class LoginCommand {
} else if (options.sso != null && this.canInteract) { } else if (options.sso != null && this.canInteract) {
// If the optional Org SSO Identifier isn't provided, the option value is `true`. // If the optional Org SSO Identifier isn't provided, the option value is `true`.
const orgSsoIdentifier = options.sso === true ? null : options.sso; const orgSsoIdentifier = options.sso === true ? null : options.sso;
const passwordOptions: any = { const ssoPromptData = await this.makeSsoPromptData();
type: "password", ssoCodeVerifier = ssoPromptData.ssoCodeVerifier;
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);
try { try {
const ssoParams = await this.openSsoPrompt(codeChallenge, state, orgSsoIdentifier); const ssoParams = await this.openSsoPrompt(
ssoPromptData.codeChallenge,
ssoPromptData.state,
orgSsoIdentifier,
);
ssoCode = ssoParams.ssoCode; ssoCode = ssoParams.ssoCode;
orgIdentifier = ssoParams.orgIdentifier; orgIdentifier = ssoParams.orgIdentifier;
} catch { } catch {
@@ -231,9 +225,43 @@ export class LoginCommand {
new PasswordLoginCredentials(email, password, twoFactor), new PasswordLoginCredentials(email, password, twoFactor),
); );
} }
// Begin Acting on initial AuthResult
if (response.requiresEncryptionKeyMigration) { if (response.requiresEncryptionKeyMigration) {
return Response.error(this.i18nService.t("legacyEncryptionUnsupported")); 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) { if (response.requiresTwoFactor) {
const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null); const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null);
if (twoFactorProviders.length === 0) { if (twoFactorProviders.length === 0) {
@@ -279,6 +307,10 @@ export class LoginCommand {
if (twoFactorToken == null && selectedProvider.type === TwoFactorProviderType.Email) { if (twoFactorToken == null && selectedProvider.type === TwoFactorProviderType.Email) {
const emailReq = new TwoFactorEmailRequest(); const emailReq = new TwoFactorEmailRequest();
emailReq.email = await this.loginStrategyService.getEmail(); 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(); emailReq.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
await this.twoFactorApiService.postTwoFactorEmail(emailReq); await this.twoFactorApiService.postTwoFactorEmail(emailReq);
} }
@@ -324,6 +356,7 @@ export class LoginCommand {
response = await this.loginStrategyService.logInNewDeviceVerification(newDeviceToken); 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) { if (response.requiresTwoFactor) {
return Response.error("Login failed."); 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( private async openSsoPrompt(
codeChallenge: string, codeChallenge: string,
state: 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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; 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 { CsprngArray } from "@bitwarden/common/types/csprng";
import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
@@ -48,9 +49,10 @@ describe("UnlockCommand", () => {
const mockMasterPassword = "testExample"; const mockMasterPassword = "testExample";
const activeAccount: Account = { const activeAccount: Account = {
id: "user-id" as UserId, id: "user-id" as UserId,
...mockAccountInfoWith({
email: "user@example.com", email: "user@example.com",
emailVerified: true,
name: "User", name: "User",
}),
}; };
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
const mockSessionKey = new Uint8Array(64) as CsprngArray; const mockSessionKey = new Uint8Array(64) as CsprngArray;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,9 +7,9 @@ pub struct NativeImporterMetadata {
/// Identifies the importer /// Identifies the importer
pub id: String, pub id: String,
/// Describes the strategies used to obtain imported data /// Describes the strategies used to obtain imported data
pub loaders: Vec<&'static str>, pub loaders: Vec<String>,
/// Identifies the instructions for the importer /// Identifies the instructions for the importer
pub instructions: &'static str, pub instructions: String,
} }
/// Returns a map of supported importers based on the current platform. /// 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(); PLATFORM_SUPPORTED_BROWSERS.iter().map(|b| b.name).collect();
for (id, browser_name) in IMPORTERS { 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) { if supported.contains(browser_name) {
loaders.push("chromium"); loaders.push("chromium".to_string());
} }
if installed_browsers.contains(&browser_name.to_string()) { if installed_browsers.contains(&browser_name.to_string()) {
@@ -48,7 +48,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
NativeImporterMetadata { NativeImporterMetadata {
id: id.to_string(), id: id.to_string(),
loaders, loaders,
instructions: "chromium", instructions: "chromium".to_string(),
}, },
); );
} }
@@ -80,12 +80,9 @@ mod tests {
map.keys().cloned().collect() map.keys().cloned().collect()
} }
fn get_loaders( fn get_loaders(map: &HashMap<String, NativeImporterMetadata>, id: &str) -> HashSet<String> {
map: &HashMap<String, NativeImporterMetadata>,
id: &str,
) -> HashSet<&'static str> {
map.get(id) map.get(id)
.map(|m| m.loaders.iter().copied().collect::<HashSet<_>>()) .map(|m| m.loaders.iter().cloned().collect::<HashSet<_>>())
.unwrap_or_default() .unwrap_or_default()
} }
@@ -108,7 +105,7 @@ mod tests {
for (key, meta) in map.iter() { for (key, meta) in map.iter() {
assert_eq!(&meta.id, key); assert_eq!(&meta.id, key);
assert_eq!(meta.instructions, "chromium"); 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() { for (key, meta) in map.iter() {
assert_eq!(&meta.id, key); assert_eq!(&meta.id, key);
assert_eq!(meta.instructions, "chromium"); 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() { for (key, meta) in map.iter() {
assert_eq!(&meta.id, key); assert_eq!(&meta.id, key);
assert_eq!(meta.instructions, "chromium"); 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 */ /* auto-generated by NAPI-RS */
/* eslint-disable */
export declare namespace passwords { export declare namespace autofill {
/** The error message returned when a password is not found during retrieval or deletion. */ export class AutofillIpcServer {
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 {
/** /**
* Create and start the IPC server without blocking. * 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 * 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. * 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. */ /** Return the path to the IPC server. */
getPath(): string getPath(): string
/** Stop the IPC server. */ /** Stop the IPC server. */
stop(): void stop(): void
/** completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number
* Send a message over the IPC server to all the connected clients completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number
* completeError(clientId: number, sequenceNumber: number, error: string): number
* @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 NativeStatus {
key: string
value: string
} }
export declare namespace autostart { export interface PasskeyAssertionRequest {
export function setAutostart(autostart: boolean, params: Array<string>): Promise<void> rpId: string
clientDataHash: Array<number>
userVerification: UserVerification
allowedCredentials: Array<Array<number>>
windowXy: Position
} }
export declare namespace autofill { export interface PasskeyAssertionResponse {
export function runCommand(value: string): Promise<string> rpId: string
export const enum UserVerification { userHandle: Array<number>
Preferred = 'preferred', signature: Array<number>
Required = 'required', clientDataHash: Array<number>
Discouraged = 'discouraged' authenticatorData: Array<number>
credentialId: Array<number>
} }
export interface Position { export interface PasskeyAssertionWithoutUserInterfaceRequest {
x: number rpId: string
y: number credentialId: Array<number>
userName: string
userHandle: Array<number>
recordIdentifier?: string
clientDataHash: Array<number>
userVerification: UserVerification
windowXy: Position
} }
export interface PasskeyRegistrationRequest { export interface PasskeyRegistrationRequest {
rpId: string rpId: string
@@ -172,71 +63,77 @@ export declare namespace autofill {
credentialId: Array<number> credentialId: Array<number>
attestationObject: Array<number> attestationObject: Array<number>
} }
export interface PasskeyAssertionRequest { export interface Position {
rpId: string x: number
clientDataHash: Array<number> y: number
userVerification: UserVerification
allowedCredentials: Array<Array<number>>
windowXy: Position
} }
export interface PasskeyAssertionWithoutUserInterfaceRequest { export function runCommand(value: string): Promise<string>
rpId: string export const enum UserVerification {
credentialId: Array<number> Preferred = 'preferred',
userName: string Required = 'required',
userHandle: Array<number> Discouraged = 'discouraged'
recordIdentifier?: string
clientDataHash: Array<number>
userVerification: UserVerification
windowXy: Position
} }
export interface NativeStatus {
key: string
value: string
} }
export interface PasskeyAssertionResponse {
rpId: string export declare namespace autostart {
userHandle: Array<number> export function setAutostart(autostart: boolean, params: Array<string>): Promise<void>
signature: Array<number>
clientDataHash: Array<number>
authenticatorData: Array<number>
credentialId: Array<number>
} }
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 * If the iv is provided, it will be used as the challenge. Otherwise a random challenge will
* connection and must be the same for both the server and client. @param callback * be generated.
* This function will be called whenever a message is received from a client. *
* `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> export function deriveKeyMaterial(iv?: string | undefined | null): Promise<OsDerivedKey>
/** Return the path to the IPC server. */ /**
getPath(): string * Retrieves the biometric secret for the given service and account.
/** Stop the IPC server. */ * Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist.
stop(): void */
completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise<string>
completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number export interface KeyMaterial {
completeError(clientId: number, sequenceNumber: number, error: string): number osKeyPartB64: string
clientKeyPartB64?: string
} }
export interface OsDerivedKey {
keyB64: string
ivB64: string
} }
export declare namespace passkey_authenticator { export function prompt(hwnd: Buffer, message: string): Promise<boolean>
export function register(): void 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 { export declare namespace biometrics_v2 {
Trace = 0, export class BiometricLockSystem {
Debug = 1,
Info = 2,
Warn = 3,
Error = 4
} }
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 declare namespace chromium_importer {
export interface ProfileInfo { export function getAvailableProfiles(browser: string): Array<ProfileInfo>
id: string /** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
name: string export function getMetadata(): Record<string, NativeImporterMetadata>
} export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
export interface Login { export interface Login {
url: string url: string
username: string username: string
@@ -257,13 +154,130 @@ export declare namespace chromium_importer {
loaders: Array<string> loaders: Array<string>
instructions: string instructions: string
} }
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */ export interface ProfileInfo {
export function getMetadata(masBuild: boolean): Record<string, NativeImporterMetadata> id: string
export function getAvailableProfiles(browser: string, masBuild: boolean): Promise<Array<ProfileInfo>> name: string
export function importLogins(browser: string, profileId: string, masBuild: boolean): Promise<Array<LoginImportResult>>
export function requestBrowserAccess(browser: string, masBuild: boolean): Promise<void>
} }
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) { switch (arch) {
case "x64": case "x64":
nativeBinding = loadFirstAvailable( nativeBinding = loadFirstAvailable(
["desktop_napi.linux-x64-musl.node", "desktop_napi.linux-x64-gnu.node"], ["desktop_napi.linux-x64-gnu.node"],
"@bitwarden/desktop-napi-linux-x64-musl", "@bitwarden/desktop-napi-linux-x64-gnu",
); );
break; break;
case "arm64": case "arm64":
nativeBinding = loadFirstAvailable( nativeBinding = loadFirstAvailable(
["desktop_napi.linux-arm64-musl.node", "desktop_napi.linux-arm64-gnu.node"], ["desktop_napi.linux-arm64-gnu.node"],
"@bitwarden/desktop-napi-linux-arm64-musl", "@bitwarden/desktop-napi-linux-arm64-gnu",
); );
break; break;
case "arm": case "arm":
nativeBinding = loadFirstAvailable( nativeBinding = loadFirstAvailable(
["desktop_napi.linux-arm-musl.node", "desktop_napi.linux-arm-gnu.node"], ["desktop_napi.linux-arm-gnu.node"],
"@bitwarden/desktop-napi-linux-arm-musl", "@bitwarden/desktop-napi-linux-arm-gnu",
); );
localFileExisted = existsSync(join(__dirname, "desktop_napi.linux-arm-gnueabihf.node")); localFileExisted = existsSync(join(__dirname, "desktop_napi.linux-arm-gnueabihf.node"));
try { try {

View File

@@ -3,27 +3,23 @@
"version": "0.1.0", "version": "0.1.0",
"description": "", "description": "",
"scripts": { "scripts": {
"build": "napi build --platform --js false", "build": "node scripts/build.js",
"test": "cargo test" "test": "cargo test"
}, },
"author": "", "author": "",
"license": "GPL-3.0", "license": "GPL-3.0",
"devDependencies": { "devDependencies": {
"@napi-rs/cli": "2.18.4" "@napi-rs/cli": "3.2.0"
}, },
"napi": { "napi": {
"name": "desktop_napi", "binaryName": "desktop_napi",
"triples": { "targets": [
"defaults": true,
"additional": [
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-gnu",
"i686-pc-windows-msvc",
"armv7-unknown-linux-gnueabihf",
"aarch64-apple-darwin", "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::{ use napi::{
bindgen_prelude::Promise, bindgen_prelude::Promise,
threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction}, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
}; };
use tokio::{self, sync::Mutex}; use tokio::{self, sync::Mutex};
use tracing::error; use tracing::error;
@@ -326,13 +326,15 @@ pub mod sshagent {
#[allow(clippy::unused_async)] // FIXME: Remove unused async! #[allow(clippy::unused_async)] // FIXME: Remove unused async!
#[napi] #[napi]
pub async fn serve( pub async fn serve(
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>, callback: ThreadsafeFunction<SshUIRequest, Promise<bool>>,
) -> napi::Result<SshAgentState> { ) -> napi::Result<SshAgentState> {
let (auth_request_tx, mut auth_request_rx) = let (auth_request_tx, mut auth_request_rx) =
tokio::sync::mpsc::channel::<desktop_core::ssh_agent::SshAgentUIRequest>(32); tokio::sync::mpsc::channel::<desktop_core::ssh_agent::SshAgentUIRequest>(32);
let (auth_response_tx, auth_response_rx) = let (auth_response_tx, auth_response_rx) =
tokio::sync::broadcast::channel::<(u32, bool)>(32); tokio::sync::broadcast::channel::<(u32, bool)>(32);
let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); 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 { tokio::spawn(async move {
let _ = auth_response_rx; let _ = auth_response_rx;
@@ -342,42 +344,50 @@ pub mod sshagent {
tokio::spawn(async move { tokio::spawn(async move {
let auth_response_tx_arc = cloned_response_tx_arc; let auth_response_tx_arc = cloned_response_tx_arc;
let callback = cloned_callback; let callback = cloned_callback;
let promise_result: Result<Promise<bool>, napi::Error> = callback // In NAPI v3, obtain the JS callback return as a Promise<boolean> and await it
.call_async(Ok(SshUIRequest { // 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, cipher_id: request.cipher_id,
is_list: request.is_list, is_list: request.is_list,
process_name: request.process_name, process_name: request.process_name,
is_forwarding: request.is_forwarding, is_forwarding: request.is_forwarding,
namespace: request.namespace, namespace: request.namespace,
})) }),
.await; ThreadsafeFunctionCallMode::Blocking,
match promise_result { move |ret: Result<Promise<bool>, napi::Error>, _env| {
Ok(promise_result) => match promise_result.await { if let Ok(p) = ret {
Ok(result) => { 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 let _ = auth_response_tx_arc
.lock() .lock()
.await .await
.send((request.request_id, result)) .send((request.request_id, result))
.expect("should be able to send auth response to agent"); .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] #[napi]
pub mod powermonitors { pub mod powermonitors {
use napi::{ use napi::{
threadsafe_function::{ threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
},
tokio, tokio,
}; };
#[napi] #[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); let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32);
desktop_core::powermonitor::on_lock(tx) desktop_core::powermonitor::on_lock(tx)
.await .await
@@ -511,9 +519,7 @@ pub mod windows_registry {
#[napi] #[napi]
pub mod ipc { pub mod ipc {
use desktop_core::ipc::server::{Message, MessageType}; use desktop_core::ipc::server::{Message, MessageType};
use napi::threadsafe_function::{ use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
#[napi(object)] #[napi(object)]
pub struct IpcMessage { pub struct IpcMessage {
@@ -550,12 +556,12 @@ pub mod ipc {
} }
#[napi] #[napi]
pub struct IpcServer { pub struct NativeIpcServer {
server: desktop_core::ipc::server::Server, server: desktop_core::ipc::server::Server,
} }
#[napi] #[napi]
impl IpcServer { impl NativeIpcServer {
/// Create and start the IPC server without blocking. /// Create and start the IPC server without blocking.
/// ///
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC /// @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( pub async fn listen(
name: String, name: String,
#[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")] #[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")]
callback: ThreadsafeFunction<IpcMessage, ErrorStrategy::CalleeHandled>, callback: ThreadsafeFunction<IpcMessage>,
) -> napi::Result<Self> { ) -> napi::Result<Self> {
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32); let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
tokio::spawn(async move { tokio::spawn(async move {
@@ -583,7 +589,7 @@ pub mod ipc {
)) ))
})?; })?;
Ok(IpcServer { server }) Ok(NativeIpcServer { server })
} }
/// Return the path to the IPC server. /// Return the path to the IPC server.
@@ -630,8 +636,9 @@ pub mod autostart {
#[napi] #[napi]
pub mod autofill { pub mod autofill {
use desktop_core::ipc::server::{Message, MessageType}; use desktop_core::ipc::server::{Message, MessageType};
use napi::threadsafe_function::{ use napi::{
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode, bindgen_prelude::FnArgs,
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
}; };
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tracing::error; use tracing::error;
@@ -746,14 +753,14 @@ pub mod autofill {
} }
#[napi] #[napi]
pub struct IpcServer { pub struct AutofillIpcServer {
server: desktop_core::ipc::server::Server, server: desktop_core::ipc::server::Server,
} }
// FIXME: Remove unwraps! They panic and terminate the whole application. // FIXME: Remove unwraps! They panic and terminate the whole application.
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
#[napi] #[napi]
impl IpcServer { impl AutofillIpcServer {
/// Create and start the IPC server without blocking. /// Create and start the IPC server without blocking.
/// ///
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC /// @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" ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void"
)] )]
registration_callback: ThreadsafeFunction< registration_callback: ThreadsafeFunction<
(u32, u32, PasskeyRegistrationRequest), FnArgs<(u32, u32, PasskeyRegistrationRequest)>,
ErrorStrategy::CalleeHandled,
>, >,
#[napi( #[napi(
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void" ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void"
)] )]
assertion_callback: ThreadsafeFunction< assertion_callback: ThreadsafeFunction<
(u32, u32, PasskeyAssertionRequest), FnArgs<(u32, u32, PasskeyAssertionRequest)>,
ErrorStrategy::CalleeHandled,
>, >,
#[napi( #[napi(
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void" ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void"
)] )]
assertion_without_user_interface_callback: ThreadsafeFunction< assertion_without_user_interface_callback: ThreadsafeFunction<
(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest), FnArgs<(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest)>,
ErrorStrategy::CalleeHandled,
>, >,
#[napi( #[napi(
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void" ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void"
)] )]
native_status_callback: ThreadsafeFunction< native_status_callback: ThreadsafeFunction<(u32, u32, NativeStatus)>,
(u32, u32, NativeStatus),
ErrorStrategy::CalleeHandled,
>,
) -> napi::Result<Self> { ) -> napi::Result<Self> {
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32); let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
tokio::spawn(async move { tokio::spawn(async move {
@@ -817,7 +818,7 @@ pub mod autofill {
Ok(msg) => { Ok(msg) => {
let value = msg let value = msg
.value .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:?}"))); .map_err(|e| napi::Error::from_reason(format!("{e:?}")));
assertion_callback assertion_callback
@@ -836,7 +837,7 @@ pub mod autofill {
Ok(msg) => { Ok(msg) => {
let value = msg let value = msg
.value .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:?}"))); .map_err(|e| napi::Error::from_reason(format!("{e:?}")));
assertion_without_user_interface_callback assertion_without_user_interface_callback
@@ -854,7 +855,7 @@ pub mod autofill {
Ok(msg) => { Ok(msg) => {
let value = msg let value = msg
.value .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:?}"))); .map_err(|e| napi::Error::from_reason(format!("{e:?}")));
registration_callback registration_callback
.call(value, ThreadsafeFunctionCallMode::NonBlocking); .call(value, ThreadsafeFunctionCallMode::NonBlocking);
@@ -894,7 +895,7 @@ pub mod autofill {
)) ))
})?; })?;
Ok(IpcServer { server }) Ok(AutofillIpcServer { server })
} }
/// Return the path to the IPC server. /// Return the path to the IPC server.
@@ -987,19 +988,20 @@ pub mod logging {
use std::{fmt::Write, sync::OnceLock}; use std::{fmt::Write, sync::OnceLock};
use napi::threadsafe_function::{ use napi::{
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode, bindgen_prelude::FnArgs,
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
}; };
use tracing::Level; use tracing::Level;
use tracing_subscriber::{ use tracing_subscriber::{
filter::{EnvFilter, LevelFilter}, filter::EnvFilter,
fmt::format::{DefaultVisitor, Writer}, fmt::format::{DefaultVisitor, Writer},
layer::SubscriberExt, layer::SubscriberExt,
util::SubscriberInitExt, util::SubscriberInitExt,
Layer, Layer,
}; };
struct JsLogger(OnceLock<ThreadsafeFunction<(LogLevel, String), CalleeHandled>>); struct JsLogger(OnceLock<ThreadsafeFunction<FnArgs<(LogLevel, String)>>>);
static JS_LOGGER: JsLogger = JsLogger(OnceLock::new()); static JS_LOGGER: JsLogger = JsLogger(OnceLock::new());
#[napi] #[napi]
@@ -1071,18 +1073,26 @@ pub mod logging {
let msg = (event.metadata().level().into(), buffer); let msg = (event.metadata().level().into(), buffer);
if let Some(logger) = JS_LOGGER.0.get() { if let Some(logger) = JS_LOGGER.0.get() {
let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking); let _ = logger.call(Ok(msg.into()), ThreadsafeFunctionCallMode::NonBlocking);
}; };
} }
} }
#[napi] #[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); 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() let filter = EnvFilter::builder()
// set the default log level to INFO. .with_default_directive(
.with_default_directive(LevelFilter::INFO.into()) 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, // parse directives from the RUST_LOG environment variable,
// overriding the default directive for matching targets. // overriding the default directive for matching targets.
.from_env_lossy(); .from_env_lossy();
@@ -1140,8 +1150,8 @@ pub mod chromium_importer {
#[napi(object)] #[napi(object)]
pub struct NativeImporterMetadata { pub struct NativeImporterMetadata {
pub id: String, pub id: String,
pub loaders: Vec<&'static str>, pub loaders: Vec<String>,
pub instructions: &'static str, pub instructions: String,
} }
impl From<_LoginImportResult> for LoginImportResult { impl From<_LoginImportResult> for LoginImportResult {
@@ -1237,7 +1247,7 @@ pub mod chromium_importer {
#[napi] #[napi]
pub mod autotype { pub mod autotype {
#[napi] #[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(|_| { autotype::get_foreground_window_title().map_err(|_| {
napi::Error::from_reason( napi::Error::from_reason(
"Autotype Error: failed to get foreground window title".to_string(), "Autotype Error: failed to get foreground window title".to_string(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n"></bit-nav-logo> <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-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> </app-side-nav>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@@ -1,3 +1,4 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RouterModule } from "@angular/router"; import { RouterModule } from "@angular/router";
import { mock } from "jest-mock-extended"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { NavigationModule } from "@bitwarden/components"; import { NavigationModule } from "@bitwarden/components";
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
import { DesktopLayoutComponent } from "./desktop-layout.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", { Object.defineProperty(window, "matchMedia", {
writable: true, writable: true,
value: jest.fn().mockImplementation((query) => ({ value: jest.fn().mockImplementation((query) => ({
@@ -34,7 +45,12 @@ describe("DesktopLayoutComponent", () => {
useValue: mock<I18nService>(), useValue: mock<I18nService>(),
}, },
], ],
}).compileComponents(); })
.overrideComponent(DesktopLayoutComponent, {
remove: { imports: [SendFiltersNavComponent] },
add: { imports: [MockSendFiltersNavComponent] },
})
.compileComponents();
fixture = TestBed.createComponent(DesktopLayoutComponent); fixture = TestBed.createComponent(DesktopLayoutComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -58,4 +74,11 @@ describe("DesktopLayoutComponent", () => {
expect(ngContent).toBeTruthy(); 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 { LayoutComponent, NavigationModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common"; import { I18nPipe } from "@bitwarden/ui-common";
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
import { DesktopSideNavComponent } from "./desktop-side-nav.component"; import { DesktopSideNavComponent } from "./desktop-side-nav.component";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({ @Component({
selector: "app-layout", selector: "app-layout",
imports: [RouterModule, I18nPipe, LayoutComponent, NavigationModule, DesktopSideNavComponent], imports: [
RouterModule,
I18nPipe,
LayoutComponent,
NavigationModule,
DesktopSideNavComponent,
SendFiltersNavComponent,
],
templateUrl: "./desktop-layout.component.html", templateUrl: "./desktop-layout.component.html",
}) })
export class DesktopLayoutComponent { export class DesktopLayoutComponent {

View File

@@ -51,6 +51,7 @@ import {
} from "@bitwarden/common/auth/abstractions/auth.service"; } from "@bitwarden/common/auth/abstractions/auth.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.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 { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { ClientType } from "@bitwarden/common/enums"; 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 { SyncService } from "@bitwarden/common/platform/sync";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService, ToastService } from "@bitwarden/components"; import { DialogService, ToastService } from "@bitwarden/components";
import { GeneratorServicesModule } from "@bitwarden/generator-components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { import {
KdfConfigService, KdfConfigService,
@@ -166,12 +168,12 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: BiometricsService, provide: BiometricsService,
useClass: RendererBiometricsService, useClass: RendererBiometricsService,
deps: [], deps: [TokenService],
}), }),
safeProvider({ safeProvider({
provide: DesktopBiometricsService, provide: DesktopBiometricsService,
useClass: RendererBiometricsService, useClass: RendererBiometricsService,
deps: [], deps: [TokenService],
}), }),
safeProvider(NativeMessagingService), safeProvider(NativeMessagingService),
safeProvider(BiometricMessageHandlerService), safeProvider(BiometricMessageHandlerService),
@@ -201,8 +203,16 @@ const safeProviders: SafeProvider[] = [
// We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid // We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid
// the TokenService having to inject the PlatformUtilsService which introduces a // the TokenService having to inject the PlatformUtilsService which introduces a
// circular dependency on Desktop only. // 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, provide: SUPPORTS_SECURE_STORAGE,
useValue: ELECTRON_SUPPORTS_SECURE_STORAGE, useValue: ELECTRON_SUPPORTS_SECURE_STORAGE && !ipc.platform.isWindowsPortable,
}), }),
safeProvider({ safeProvider({
provide: DEFAULT_VAULT_TIMEOUT, provide: DEFAULT_VAULT_TIMEOUT,
@@ -499,7 +509,7 @@ const safeProviders: SafeProvider[] = [
]; ];
@NgModule({ @NgModule({
imports: [JslibServicesModule], imports: [JslibServicesModule, GeneratorServicesModule],
declarations: [], declarations: [],
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function // Do not register your dependency here! Add it to the typesafeProviders array using the helper function
providers: safeProviders, providers: safeProviders,

View File

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

View File

@@ -1,5 +1,5 @@
<bit-dialog #dialog dialogSize="large" background="alt"> <bit-dialog #dialog dialogSize="large" background="alt">
<span bitDialogTitle>{{ "importData" | i18n }}</span> <span bitDialogTitle>{{ "import" | i18n }}</span>
<ng-container bitDialogContent> <ng-container bitDialogContent>
<div class="tw-relative"> <div class="tw-relative">
<tools-import <tools-import
@@ -27,7 +27,7 @@
bitFormButton bitFormButton
buttonType="primary" buttonType="primary"
> >
{{ "importData" | i18n }} {{ "import" | i18n }}
</button> </button>
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose> <button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }} {{ "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 { ComponentFixture, TestBed } from "@angular/core/testing";
import { FormBuilder } from "@angular/forms";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs"; 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 { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { DialogService, ToastService } from "@bitwarden/components"; import { DialogService, ToastService } from "@bitwarden/components";
import { SendListFiltersService } from "@bitwarden/send-ui";
import * as utils from "../../../utils"; import * as utils from "../../../utils";
import { SearchBarService } from "../../layout/search/search-bar.service"; import { SearchBarService } from "../../layout/search/search-bar.service";
@@ -35,6 +40,8 @@ describe("SendV2Component", () => {
let broadcasterService: MockProxy<BroadcasterService>; let broadcasterService: MockProxy<BroadcasterService>;
let accountService: MockProxy<AccountService>; let accountService: MockProxy<AccountService>;
let policyService: MockProxy<PolicyService>; let policyService: MockProxy<PolicyService>;
let sendListFiltersService: SendListFiltersService;
let changeDetectorRef: MockProxy<ChangeDetectorRef>;
beforeEach(async () => { beforeEach(async () => {
sendService = mock<SendService>(); sendService = mock<SendService>();
@@ -42,6 +49,13 @@ describe("SendV2Component", () => {
broadcasterService = mock<BroadcasterService>(); broadcasterService = mock<BroadcasterService>();
accountService = mock<AccountService>(); accountService = mock<AccountService>();
policyService = mock<PolicyService>(); 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 // Mock sendViews$ observable
sendService.sendViews$ = of([]); sendService.sendViews$ = of([]);
@@ -51,6 +65,10 @@ describe("SendV2Component", () => {
accountService.activeAccount$ = of({ id: "test-user-id" } as any); accountService.activeAccount$ = of({ id: "test-user-id" } as any);
policyService.policyAppliesToUser$ = jest.fn().mockReturnValue(of(false)); 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({ await TestBed.configureTestingModule({
imports: [SendV2Component], imports: [SendV2Component],
providers: [ providers: [
@@ -59,7 +77,7 @@ describe("SendV2Component", () => {
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() }, { provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: EnvironmentService, useValue: mock<EnvironmentService>() }, { provide: EnvironmentService, useValue: mock<EnvironmentService>() },
{ provide: BroadcasterService, useValue: broadcasterService }, { provide: BroadcasterService, useValue: broadcasterService },
{ provide: SearchService, useValue: mock<SearchService>() }, { provide: SearchService, useValue: mockSearchService },
{ provide: PolicyService, useValue: policyService }, { provide: PolicyService, useValue: policyService },
{ provide: SearchBarService, useValue: searchBarService }, { provide: SearchBarService, useValue: searchBarService },
{ provide: LogService, useValue: mock<LogService>() }, { provide: LogService, useValue: mock<LogService>() },
@@ -67,6 +85,8 @@ describe("SendV2Component", () => {
{ provide: DialogService, useValue: mock<DialogService>() }, { provide: DialogService, useValue: mock<DialogService>() },
{ provide: ToastService, useValue: mock<ToastService>() }, { provide: ToastService, useValue: mock<ToastService>() },
{ provide: AccountService, useValue: accountService }, { provide: AccountService, useValue: accountService },
{ provide: SendListFiltersService, useValue: sendListFiltersService },
{ provide: ChangeDetectorRef, useValue: changeDetectorRef },
], ],
}).compileComponents(); }).compileComponents();
@@ -331,7 +351,6 @@ describe("SendV2Component", () => {
describe("load", () => { describe("load", () => {
it("sets loading states correctly", async () => { it("sets loading states correctly", async () => {
jest.spyOn(component, "search").mockResolvedValue(); jest.spyOn(component, "search").mockResolvedValue();
jest.spyOn(component, "selectAll");
expect(component.loaded).toBeFalsy(); expect(component.loaded).toBeFalsy();
@@ -341,14 +360,17 @@ describe("SendV2Component", () => {
expect(component.loaded).toBe(true); 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, "search").mockResolvedValue();
jest.spyOn(component, "selectAll");
component.onSuccessfulLoad = null;
await component.load(); 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 () => { it("calls onSuccessfulLoad when it is set", async () => {

View File

@@ -2,8 +2,9 @@
// @ts-strict-ignore // @ts-strict-ignore
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, OnInit, OnDestroy, ViewChild, NgZone, ChangeDetectorRef } from "@angular/core"; import { Component, OnInit, OnDestroy, ViewChild, NgZone, ChangeDetectorRef } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormsModule } from "@angular/forms"; import { FormsModule } from "@angular/forms";
import { mergeMap } from "rxjs"; import { mergeMap, Subscription } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { DialogService, ToastService } from "@bitwarden/components"; import { DialogService, ToastService } from "@bitwarden/components";
import { SendListFiltersService } from "@bitwarden/send-ui";
import { invokeMenu, RendererMenuItem } from "../../../utils"; import { invokeMenu, RendererMenuItem } from "../../../utils";
import { SearchBarService } from "../../layout/search/search-bar.service"; 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) // Tracks the current UI state: viewing list (None), adding new Send (Add), or editing existing Send (Edit)
action: Action = Action.None; action: Action = Action.None;
// Subscription for sendViews$ cleanup
private sendViewsSubscription: Subscription;
constructor( constructor(
sendService: SendService, sendService: SendService,
i18nService: I18nService, i18nService: I18nService,
@@ -71,6 +77,7 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
toastService: ToastService, toastService: ToastService,
accountService: AccountService, accountService: AccountService,
private cdr: ChangeDetectorRef, private cdr: ChangeDetectorRef,
private sendListFiltersService: SendListFiltersService,
) { ) {
super( super(
sendService, sendService,
@@ -88,11 +95,16 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
); );
// Listen to search bar changes and update the Send list filter // Listen to search bar changes and update the Send list filter
// eslint-disable-next-line rxjs-angular/prefer-takeuntil this.searchBarService.searchText$.pipe(takeUntilDestroyed()).subscribe((searchText) => {
this.searchBarService.searchText$.subscribe((searchText) => {
this.searchText = searchText; this.searchText = searchText;
this.searchTextChanged(); 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(); 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 // Listen for sync completion events to refresh the Send list
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { 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. // 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(); 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 // Clean up subscriptions and disable search bar when component is destroyed
ngOnDestroy() { ngOnDestroy() {
this.sendViewsSubscription?.unsubscribe();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.searchBarService.setEnabled(false); 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. // Note: The filter parameter is ignored in this implementation for desktop-specific behavior.
async load(filter: (send: SendView) => boolean = null) { async load(filter: (send: SendView) => boolean = null) {
this.loading = true; 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( .pipe(
mergeMap(async (sends) => { mergeMap(async (sends) => {
this.sends = sends; this.sends = sends;
@@ -143,9 +174,6 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
.subscribe(); .subscribe();
if (this.onSuccessfulLoad != null) { if (this.onSuccessfulLoad != null) {
await this.onSuccessfulLoad(); await this.onSuccessfulLoad();
} else {
// Default action
this.selectAll();
} }
this.loading = false; this.loading = false;
this.loaded = true; this.loaded = true;

View File

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

View File

@@ -37,7 +37,7 @@ export class MainSshAgentService {
init() { init() {
// handle sign request passing to UI // handle sign request passing to UI
sshagent sshagent
.serve(async (err: Error, sshUiRequest: sshagent.SshUiRequest) => { .serve(async (err: Error | null, sshUiRequest: sshagent.SshUiRequest): Promise<boolean> => {
// clear all old (> SIGN_TIMEOUT) requests // clear all old (> SIGN_TIMEOUT) requests
this.requestResponses = this.requestResponses.filter( this.requestResponses = this.requestResponses.filter(
(response) => response.timestamp > new Date(Date.now() - this.SIGN_TIMEOUT), (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 { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
@@ -40,9 +41,10 @@ describe("Fido2CreateComponent", () => {
const activeAccountSubject = new BehaviorSubject<Account | null>({ const activeAccountSubject = new BehaviorSubject<Account | null>({
id: "test-user-id" as UserId, id: "test-user-id" as UserId,
...mockAccountInfoWith({
email: "test@example.com", email: "test@example.com",
emailVerified: true,
name: "Test User", name: "Test User",
}),
}); });
beforeEach(async () => { 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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Account, UserId } from "@bitwarden/common/platform/models/domain/account"; import { Account, UserId } from "@bitwarden/common/platform/models/domain/account";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service"; import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service";
@@ -30,9 +31,10 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
beforeEach(() => { beforeEach(() => {
mockAccountSubject = new BehaviorSubject<Account | null>({ mockAccountSubject = new BehaviorSubject<Account | null>({
id: mockUserId, id: mockUserId,
...mockAccountInfoWith({
email: "test@example.com", email: "test@example.com",
emailVerified: true,
name: "Test User", name: "Test User",
}),
}); });
mockFeatureFlagSubject = new BehaviorSubject<boolean>(true); mockFeatureFlagSubject = new BehaviorSubject<boolean>(true);
mockAuthStatusSubject = new BehaviorSubject<AuthenticationStatus>( mockAuthStatusSubject = new BehaviorSubject<AuthenticationStatus>(

View File

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

View File

@@ -1,5 +1,7 @@
import { Injectable } from "@angular/core"; 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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key"; import { UserKey } from "@bitwarden/common/types/key";
@@ -13,6 +15,10 @@ import { DesktopBiometricsService } from "./desktop.biometrics.service";
*/ */
@Injectable() @Injectable()
export class RendererBiometricsService extends DesktopBiometricsService { export class RendererBiometricsService extends DesktopBiometricsService {
constructor(private tokenService: TokenService) {
super();
}
async authenticateWithBiometrics(): Promise<boolean> { async authenticateWithBiometrics(): Promise<boolean> {
return await ipc.keyManagement.biometric.authenticateWithBiometrics(); return await ipc.keyManagement.biometric.authenticateWithBiometrics();
} }
@@ -31,6 +37,10 @@ export class RendererBiometricsService extends DesktopBiometricsService {
} }
async getBiometricsStatusForUser(id: UserId): Promise<BiometricsStatus> { async getBiometricsStatusForUser(id: UserId): Promise<BiometricsStatus> {
if ((await firstValueFrom(this.tokenService.hasAccessToken$(id))) === false) {
return BiometricsStatus.NotEnabledInConnectedDesktopApp;
}
return await ipc.keyManagement.biometric.getBiometricsStatusForUser(id); 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": { "addAttachment": {
"message": "Add attachment" "message": "Add attachment"
}, },
"itemsTransferred": {
"message": "Items transferred"
},
"fixEncryption": { "fixEncryption": {
"message": "Fix encryption" "message": "Fix encryption"
}, },
@@ -1195,8 +1198,8 @@
"followUs": { "followUs": {
"message": "Follow us" "message": "Follow us"
}, },
"syncVault": { "syncNow": {
"message": "Sync vault" "message": "Sync now"
}, },
"changeMasterPass": { "changeMasterPass": {
"message": "Change master password" "message": "Change master password"
@@ -1772,8 +1775,11 @@
"exportFrom": { "exportFrom": {
"message": "Export from" "message": "Export from"
}, },
"exportVault": { "export": {
"message": "Export vault" "message": "Export"
},
"import": {
"message": "Import"
}, },
"fileFormat": { "fileFormat": {
"message": "File format" "message": "File format"
@@ -2634,9 +2640,6 @@
"removedMasterPassword": { "removedMasterPassword": {
"message": "Master password removed" "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": { "organizationName": {
"message": "Organization name" "message": "Organization name"
}, },
@@ -3492,10 +3495,6 @@
"aliasDomain": { "aliasDomain": {
"message": "Alias domain" "message": "Alias domain"
}, },
"importData": {
"message": "Import data",
"description": "Used for the desktop menu item and the header of the import dialog"
},
"importError": { "importError": {
"message": "Import error" "message": "Import error"
}, },
@@ -4334,6 +4333,45 @@
"upgradeToPremium": { "upgradeToPremium": {
"message": "Upgrade to Premium" "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": { "sessionTimeoutSettingsAction": {
"message": "Timeout action" "message": "Timeout action"
}, },
@@ -4395,5 +4433,53 @@
}, },
"upgrade": { "upgrade": {
"message": "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 { private get syncVault(): MenuItemConstructorOptions {
return { return {
id: "syncVault", id: "syncNow",
label: this.localize("syncVault"), label: this.localize("syncNow"),
click: () => this.sendMessage("syncVault"), click: () => this.sendMessage("syncVault"),
enabled: this.hasAuthenticatedAccounts, enabled: this.hasAuthenticatedAccounts,
}; };
@@ -155,8 +155,8 @@ export class FileMenu extends FirstMenu implements IMenubarMenu {
private get importVault(): MenuItemConstructorOptions { private get importVault(): MenuItemConstructorOptions {
return { return {
id: "importVault", id: "import",
label: this.localize("importData"), label: this.localize("import"),
click: () => this.sendMessage("importVault"), click: () => this.sendMessage("importVault"),
enabled: !this._isLocked, enabled: !this._isLocked,
}; };
@@ -164,8 +164,8 @@ export class FileMenu extends FirstMenu implements IMenubarMenu {
private get exportVault(): MenuItemConstructorOptions { private get exportVault(): MenuItemConstructorOptions {
return { return {
id: "exportVault", id: "export",
label: this.localize("exportVault"), label: this.localize("export"),
click: () => this.sendMessage("exportVault"), click: () => this.sendMessage("exportVault"),
enabled: !this._isLocked, enabled: !this._isLocked,
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ export type RunCommandParams<C extends CommandDefinition> = {
export type RunCommandResult<C extends CommandDefinition> = C["output"]; export type RunCommandResult<C extends CommandDefinition> = C["output"];
export class NativeAutofillMain { export class NativeAutofillMain {
private ipcServer: autofill.IpcServer | null; private ipcServer?: autofill.AutofillIpcServer;
private messageBuffer: BufferedMessage[] = []; private messageBuffer: BufferedMessage[] = [];
private listenerReady = false; private listenerReady = false;
@@ -70,13 +70,13 @@ export class NativeAutofillMain {
}, },
); );
this.ipcServer = await autofill.IpcServer.listen( this.ipcServer = await autofill.AutofillIpcServer.listen(
"af", "af",
// RegistrationCallback // RegistrationCallback
(error, clientId, sequenceNumber, request) => { (error, clientId, sequenceNumber, request) => {
if (error) { if (error) {
this.logService.error("autofill.IpcServer.registration", error); this.logService.error("autofill.IpcServer.registration", error);
this.ipcServer.completeError(clientId, sequenceNumber, String(error)); this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
return; return;
} }
this.safeSend("autofill.passkeyRegistration", { this.safeSend("autofill.passkeyRegistration", {
@@ -89,7 +89,7 @@ export class NativeAutofillMain {
(error, clientId, sequenceNumber, request) => { (error, clientId, sequenceNumber, request) => {
if (error) { if (error) {
this.logService.error("autofill.IpcServer.assertion", error); this.logService.error("autofill.IpcServer.assertion", error);
this.ipcServer.completeError(clientId, sequenceNumber, String(error)); this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
return; return;
} }
this.safeSend("autofill.passkeyAssertion", { this.safeSend("autofill.passkeyAssertion", {
@@ -102,7 +102,7 @@ export class NativeAutofillMain {
(error, clientId, sequenceNumber, request) => { (error, clientId, sequenceNumber, request) => {
if (error) { if (error) {
this.logService.error("autofill.IpcServer.assertion", error); this.logService.error("autofill.IpcServer.assertion", error);
this.ipcServer.completeError(clientId, sequenceNumber, String(error)); this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
return; return;
} }
this.safeSend("autofill.passkeyAssertionWithoutUserInterface", { this.safeSend("autofill.passkeyAssertionWithoutUserInterface", {
@@ -115,7 +115,7 @@ export class NativeAutofillMain {
(error, clientId, sequenceNumber, status) => { (error, clientId, sequenceNumber, status) => {
if (error) { if (error) {
this.logService.error("autofill.IpcServer.nativeStatus", error); this.logService.error("autofill.IpcServer.nativeStatus", error);
this.ipcServer.completeError(clientId, sequenceNumber, String(error)); this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
return; return;
} }
this.safeSend("autofill.nativeStatus", { this.safeSend("autofill.nativeStatus", {
@@ -137,19 +137,19 @@ export class NativeAutofillMain {
ipcMain.on("autofill.completePasskeyRegistration", (event, data) => { ipcMain.on("autofill.completePasskeyRegistration", (event, data) => {
this.logService.debug("autofill.completePasskeyRegistration", data); this.logService.debug("autofill.completePasskeyRegistration", data);
const { clientId, sequenceNumber, response } = data; const { clientId, sequenceNumber, response } = data;
this.ipcServer.completeRegistration(clientId, sequenceNumber, response); this.ipcServer?.completeRegistration(clientId, sequenceNumber, response);
}); });
ipcMain.on("autofill.completePasskeyAssertion", (event, data) => { ipcMain.on("autofill.completePasskeyAssertion", (event, data) => {
this.logService.debug("autofill.completePasskeyAssertion", data); this.logService.debug("autofill.completePasskeyAssertion", data);
const { clientId, sequenceNumber, response } = data; const { clientId, sequenceNumber, response } = data;
this.ipcServer.completeAssertion(clientId, sequenceNumber, response); this.ipcServer?.completeAssertion(clientId, sequenceNumber, response);
}); });
ipcMain.on("autofill.completeError", (event, data) => { ipcMain.on("autofill.completeError", (event, data) => {
this.logService.debug("autofill.completeError", data); this.logService.debug("autofill.completeError", data);
const { clientId, sequenceNumber, error } = 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, isFlatpak,
isMacAppStore, isMacAppStore,
isSnapStore, isSnapStore,
isWindowsPortable,
isWindowsStore, isWindowsStore,
} from "../utils"; } from "../utils";
@@ -108,8 +109,13 @@ const ephemeralStore = {
}; };
const localhostCallbackService = { const localhostCallbackService = {
openSsoPrompt: (codeChallenge: string, state: string, email: string): Promise<void> => { openSsoPrompt: (
return ipcRenderer.invoke("openSsoPrompt", { codeChallenge, state, email }); 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(), isDev: isDev(),
isMacAppStore: isMacAppStore(), isMacAppStore: isMacAppStore(),
isWindowsStore: isWindowsStore(), isWindowsStore: isWindowsStore(),
isWindowsPortable: isWindowsPortable(),
isFlatpak: isFlatpak(), isFlatpak: isFlatpak(),
isSnapStore: isSnapStore(), isSnapStore: isSnapStore(),
isAppImage: isAppImage(), isAppImage: isAppImage(),

View File

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

View File

@@ -25,20 +25,25 @@ export class SSOLocalhostCallbackService {
private messagingService: MessageSender, private messagingService: MessageSender,
private ssoUrlService: SsoUrlService, 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 // Close any existing server before starting new one
if (this.currentServer) { if (this.currentServer) {
await this.closeCurrentServer(); 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", { this.messagingService.send("ssoCallback", {
code: ssoCode, code: ssoCode,
state: recvState, state: recvState,
redirectUri: this.ssoRedirectUri, redirectUri: this.ssoRedirectUri,
}); });
}); },
}); );
},
);
} }
private async closeCurrentServer(): Promise<void> { private async closeCurrentServer(): Promise<void> {
@@ -58,6 +63,7 @@ export class SSOLocalhostCallbackService {
codeChallenge: string, codeChallenge: string,
state: string, state: string,
email: string, email: string,
orgSsoIdentifier?: string,
): Promise<{ ssoCode: string; recvState: string }> { ): Promise<{ ssoCode: string; recvState: string }> {
const env = await firstValueFrom(this.environmentService.environment$); const env = await firstValueFrom(this.environmentService.environment$);
@@ -121,6 +127,7 @@ export class SSOLocalhostCallbackService {
state, state,
codeChallenge, codeChallenge,
email, email,
orgSsoIdentifier,
); );
// Set up error handler before attempting to listen // 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 { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, filter, firstValueFrom, of, take, timeout, timer } from "rxjs"; 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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; 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 { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@@ -23,17 +23,15 @@ import { BiometricMessageHandlerService } from "./biometric-message-handler.serv
const SomeUser = "SomeUser" as UserId; const SomeUser = "SomeUser" as UserId;
const AnotherUser = "SomeOtherUser" as UserId; const AnotherUser = "SomeOtherUser" as UserId;
const accounts: Record<UserId, AccountInfo> = { const accounts = {
[SomeUser]: { [SomeUser]: mockAccountInfoWith({
name: "some user", name: "some user",
email: "some.user@example.com", email: "some.user@example.com",
emailVerified: true, }),
}, [AnotherUser]: mockAccountInfoWith({
[AnotherUser]: {
name: "some other user", name: "some other user",
email: "some.other.user@example.com", email: "some.other.user@example.com",
emailVerified: true, }),
},
}; };
describe("BiometricMessageHandlerService", () => { describe("BiometricMessageHandlerService", () => {

View File

@@ -24,7 +24,7 @@ export const MaxCheckedCount = 500;
* Maximum for bulk reinvite operations when the IncreaseBulkReinviteLimitForCloud * Maximum for bulk reinvite operations when the IncreaseBulkReinviteLimitForCloud
* feature flag is enabled on cloud environments. * 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). * 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" *ngIf="organization.use2fa && organization.isOwner"
></bit-nav-item> ></bit-nav-item>
<bit-nav-item <bit-nav-item
[text]="'importData' | i18n" [text]="'import' | i18n"
route="settings/tools/import" route="settings/tools/import"
*ngIf="organization.canAccessImport" *ngIf="organization.canAccessImport"
></bit-nav-item> ></bit-nav-item>
<bit-nav-item <bit-nav-item
[text]="'exportVault' | i18n" [text]="'export' | i18n"
route="settings/tools/export" route="settings/tools/export"
*ngIf="canAccessExport$ | async" *ngIf="canAccessExport$ | async"
></bit-nav-item> ></bit-nav-item>

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