diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8bb15d37fdf..2582b96961d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -156,6 +156,8 @@ apps/desktop/macos/autofill-extension @bitwarden/team-autofill-desktop-dev apps/desktop/src/app/components/fido2placeholder.component.ts @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/windows_plugin_authenticator @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/autotype @bitwarden/team-autofill-desktop-dev +apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys +apps/desktop/desktop_native/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys apps/desktop/desktop_native/napi/src/autofill.rs @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/napi/src/autotype.rs @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/napi/src/sshagent.rs @bitwarden/team-autofill-desktop-dev @@ -164,8 +166,6 @@ apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-desktop-dev apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-desktop-dev apps/desktop/src/services/encrypted-message-handler.service.ts @bitwarden/team-autofill-desktop-dev .github/workflows/alert-ddg-files-modified.yml @bitwarden/team-autofill-desktop-dev -# SSH Agent -apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys ## UI Foundation ## .github/workflows/chromatic.yml @bitwarden/team-ui-foundation diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 718586a9b1a..6845e5f3829 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -313,7 +313,6 @@ "@types/inquirer", "@types/koa", "@types/koa__multer", - "@types/koa__router", "@types/koa-bodyparser", "@types/koa-json", "@types/lunr", diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 9f15bfd840f..5026f5e2799 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -6123,6 +6123,12 @@ "whyAmISeeingThis": { "message": "Why am I seeing this?" }, + "items": { + "message": "Items" + }, + "searchResults": { + "message": "Search results" + }, "resizeSideNavigation": { "message": "Resize side navigation" }, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index eb6d26357eb..e0cbcdc9f96 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1110,6 +1110,7 @@ export default class MainBackground { this.logService, this.platformUtilsService, this.configService, + this.sdkService, ), ); diff --git a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts index c37131b3ff1..f1614f800f2 100644 --- a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts @@ -6,7 +6,7 @@ import { firstValueFrom, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { AnchorLinkDirective, CalloutModule, BannerModule } from "@bitwarden/components"; +import { LinkComponent, CalloutModule, BannerModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; import { AtRiskPasswordCalloutData, AtRiskPasswordCalloutService } from "@bitwarden/vault"; @@ -15,7 +15,7 @@ import { AtRiskPasswordCalloutData, AtRiskPasswordCalloutService } from "@bitwar @Component({ selector: "vault-at-risk-password-callout", imports: [ - AnchorLinkDirective, + LinkComponent, CommonModule, RouterModule, CalloutModule, diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 20871b4b134..f0a6b0d6000 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -107,20 +107,32 @@ @if (vaultState === null) { @if (!(loading$ | async)) { - - - + + @if (hasSearchText$ | async) { + + } @else { + + + + + } } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts index d7824f3df58..a322fbc53dd 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts @@ -44,6 +44,7 @@ import { VaultPopupAutofillService } from "../../services/vault-popup-autofill.s import { VaultPopupCopyButtonsService } from "../../services/vault-popup-copy-buttons.service"; import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service"; +import { VaultPopupLoadingService } from "../../services/vault-popup-loading.service"; import { VaultPopupScrollPositionService } from "../../services/vault-popup-scroll-position.service"; import { AtRiskPasswordCalloutComponent } from "../at-risk-callout/at-risk-password-callout.component"; @@ -174,15 +175,21 @@ describe("VaultV2Component", () => { showDeactivatedOrg$: new BehaviorSubject(false), favoriteCiphers$: new BehaviorSubject([]), remainingCiphers$: new BehaviorSubject([]), + filteredCiphers$: new BehaviorSubject([]), cipherCount$: new BehaviorSubject(0), - loading$: new BehaviorSubject(true), + hasSearchText$: new BehaviorSubject(false), } as Partial; - const filtersSvc = { + const filtersSvc: any = { allFilters$: new Subject(), filters$: new BehaviorSubject({}), filterVisibilityState$: new BehaviorSubject({}), - } as Partial; + numberOfAppliedFilters$: new BehaviorSubject(0), + }; + + const loadingSvc: any = { + loading$: new BehaviorSubject(false), + }; const activeAccount$ = new BehaviorSubject({ id: "user-1" }); @@ -240,6 +247,7 @@ describe("VaultV2Component", () => { provideNoopAnimations(), { provide: VaultPopupItemsService, useValue: itemsSvc }, { provide: VaultPopupListFiltersService, useValue: filtersSvc }, + { provide: VaultPopupLoadingService, useValue: loadingSvc }, { provide: VaultPopupScrollPositionService, useValue: scrollSvc }, { provide: AccountService, @@ -366,18 +374,18 @@ describe("VaultV2Component", () => { }); it("loading$ is true when items loading or filters missing; false when both ready", () => { - const itemsLoading$ = itemsSvc.loading$ as unknown as BehaviorSubject; + const vaultLoading$ = loadingSvc.loading$ as unknown as BehaviorSubject; const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; const readySubject$ = component["readySubject"] as unknown as BehaviorSubject; const values: boolean[] = []; getObs(component, "loading$").subscribe((v) => values.push(!!v)); - itemsLoading$.next(true); + vaultLoading$.next(true); allFilters$.next({}); - itemsLoading$.next(false); + vaultLoading$.next(false); readySubject$.next(true); @@ -389,7 +397,7 @@ describe("VaultV2Component", () => { const component = fixture.componentInstance; const readySubject$ = component["readySubject"] as unknown as BehaviorSubject; - const itemsLoading$ = itemsSvc.loading$ as unknown as BehaviorSubject; + const vaultLoading$ = loadingSvc.loading$ as unknown as BehaviorSubject; const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; fixture.detectChanges(); @@ -400,7 +408,7 @@ describe("VaultV2Component", () => { ) as HTMLElement; // Unblock loading - itemsLoading$.next(false); + vaultLoading$.next(false); readySubject$.next(true); allFilters$.next({}); tick(); @@ -607,6 +615,127 @@ describe("VaultV2Component", () => { expect(spotlights.length).toBe(0); })); + it("does not render app-autofill-vault-list-items or favorites item container when hasSearchText$ is true", () => { + itemsSvc.hasSearchText$.next(true); + + const fixture = TestBed.createComponent(VaultV2Component); + component = fixture.componentInstance; + + const readySubject$ = component["readySubject"]; + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + // Unblock loading + readySubject$.next(true); + allFilters$.next({}); + fixture.detectChanges(); + + const autofillElement = fixture.debugElement.query(By.css("app-autofill-vault-list-items")); + expect(autofillElement).toBeFalsy(); + + const favoritesElement = fixture.debugElement.query(By.css("#favorites")); + expect(favoritesElement).toBeFalsy(); + }); + + it("does render app-autofill-vault-list-items and favorites item container when hasSearchText$ is false", () => { + // Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg) + itemsSvc.emptyVault$.next(false); + itemsSvc.noFilteredResults$.next(false); + itemsSvc.showDeactivatedOrg$.next(false); + itemsSvc.hasSearchText$.next(false); + loadingSvc.loading$.next(false); + + const fixture = TestBed.createComponent(VaultV2Component); + component = fixture.componentInstance; + + const readySubject$ = component["readySubject"]; + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + // Unblock loading + readySubject$.next(true); + allFilters$.next({}); + fixture.detectChanges(); + + const autofillElement = fixture.debugElement.query(By.css("app-autofill-vault-list-items")); + expect(autofillElement).toBeTruthy(); + + const favoritesElement = fixture.debugElement.query(By.css("#favorites")); + expect(favoritesElement).toBeTruthy(); + }); + + it("does set the title for allItems container to allItems when hasSearchText$ and numberOfAppliedFilters$ are false and 0 respectively", () => { + // Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg) + itemsSvc.emptyVault$.next(false); + itemsSvc.noFilteredResults$.next(false); + itemsSvc.showDeactivatedOrg$.next(false); + itemsSvc.hasSearchText$.next(false); + filtersSvc.numberOfAppliedFilters$.next(0); + loadingSvc.loading$.next(false); + + const fixture = TestBed.createComponent(VaultV2Component); + component = fixture.componentInstance; + + const readySubject$ = component["readySubject"]; + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + // Unblock loading + readySubject$.next(true); + allFilters$.next({}); + fixture.detectChanges(); + + const allItemsElement = fixture.debugElement.query(By.css("#allItems")); + const allItemsTitle = allItemsElement.componentInstance.title(); + expect(allItemsTitle).toBe("allItems"); + }); + + it("does set the title for allItems container to searchResults when hasSearchText$ is true", () => { + // Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg) + itemsSvc.emptyVault$.next(false); + itemsSvc.noFilteredResults$.next(false); + itemsSvc.showDeactivatedOrg$.next(false); + itemsSvc.hasSearchText$.next(true); + loadingSvc.loading$.next(false); + + const fixture = TestBed.createComponent(VaultV2Component); + component = fixture.componentInstance; + + const readySubject$ = component["readySubject"]; + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + // Unblock loading + readySubject$.next(true); + allFilters$.next({}); + fixture.detectChanges(); + + const allItemsElement = fixture.debugElement.query(By.css("#allItems")); + const allItemsTitle = allItemsElement.componentInstance.title(); + expect(allItemsTitle).toBe("searchResults"); + }); + + it("does set the title for allItems container to items when numberOfAppliedFilters$ is > 0", fakeAsync(() => { + // Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg) + itemsSvc.emptyVault$.next(false); + itemsSvc.noFilteredResults$.next(false); + itemsSvc.showDeactivatedOrg$.next(false); + itemsSvc.hasSearchText$.next(false); + filtersSvc.numberOfAppliedFilters$.next(1); + loadingSvc.loading$.next(false); + + const fixture = TestBed.createComponent(VaultV2Component); + component = fixture.componentInstance; + + const readySubject$ = component["readySubject"]; + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + // Unblock loading + readySubject$.next(true); + allFilters$.next({}); + fixture.detectChanges(); + + const allItemsElement = fixture.debugElement.query(By.css("#allItems")); + const allItemsTitle = allItemsElement.componentInstance.title(); + expect(allItemsTitle).toBe("items"); + })); + describe("AutoConfirmExtensionSetupDialog", () => { beforeEach(() => { autoConfirmDialogSpy.mockClear(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 51e735fb1ef..fce084542a9 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -160,6 +160,11 @@ export class VaultV2Component implements OnInit, OnDestroy { FeatureFlag.BrowserPremiumSpotlight, ); + protected readonly hasSearchText$ = this.vaultPopupItemsService.hasSearchText$; + protected readonly numberOfAppliedFilters$ = + this.vaultPopupListFiltersService.numberOfAppliedFilters$; + + protected filteredCiphers$ = this.vaultPopupItemsService.filteredCiphers$; protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$; protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$; protected allFilters$ = this.vaultPopupListFiltersService.allFilters$; diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 7cd73279c3d..093fdbfb66d 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -323,6 +323,25 @@ describe("VaultPopupItemsService", () => { }); }); + describe("filteredCiphers$", () => { + it("should filter filteredCipher$ down to search term", (done) => { + const cipherList = Object.values(allCiphers); + const searchText = "Login"; + + searchService.searchCiphers.mockImplementation(async () => { + return cipherList.filter((cipher) => { + return cipher.name.includes(searchText); + }); + }); + + service.filteredCiphers$.subscribe((ciphers) => { + // There are 10 ciphers but only 3 with "Login" in the name + expect(ciphers.length).toBe(3); + done(); + }); + }); + }); + describe("favoriteCiphers$", () => { it("should exclude autofill ciphers", (done) => { service.favoriteCiphers$.subscribe((ciphers) => { diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 321d7936806..7ccfc834c87 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -201,6 +201,15 @@ export class VaultPopupItemsService { shareReplay({ refCount: true, bufferSize: 1 }), ); + /** + * List of ciphers that are filtered using filters and search. + * Includes favorite ciphers and ciphers currently suggested for autofill. + * Ciphers are sorted by name. + */ + filteredCiphers$: Observable = this._filteredCipherList$.pipe( + shareReplay({ refCount: false, bufferSize: 1 }), + ); + /** * List of ciphers that can be used for autofill on the current tab. Includes cards and/or identities * if enabled in the vault settings. Ciphers are sorted by type, then by last used date, then by name. diff --git a/apps/cli/package.json b/apps/cli/package.json index 93658cce361..79653ec970f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -64,7 +64,7 @@ }, "dependencies": { "@koa/multer": "4.0.0", - "@koa/router": "14.0.0", + "@koa/router": "15.2.0", "big-integer": "1.6.52", "browser-hrtime": "1.1.8", "chalk": "4.1.2", diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 5bf19333f35..92632981154 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -1,7 +1,7 @@ import http from "node:http"; import net from "node:net"; -import * as koaRouter from "@koa/router"; +import { Router } from "@koa/router"; import { OptionValues } from "commander"; import * as koa from "koa"; import * as koaBodyParser from "koa-bodyparser"; @@ -29,7 +29,7 @@ export class ServeCommand { ); const server = new koa(); - const router = new koaRouter(); + const router = new Router(); process.env.BW_SERVE = "true"; process.env.BW_NOINTERACTION = "true"; diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index e0385534cb7..fbf3c778725 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import * as koaMulter from "@koa/multer"; -import * as koaRouter from "@koa/router"; +import { Router } from "@koa/router"; import * as koa from "koa"; import { firstValueFrom, map } from "rxjs"; @@ -218,7 +218,7 @@ export class OssServeConfigurator { ); } - async configureRouter(router: koaRouter) { + async configureRouter(router: Router) { router.get("/generate", async (ctx, next) => { const response = await this.generateCommand.run(ctx.request.query); this.processResponse(ctx.response, response); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 3e78eb36577..105d80b290b 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -928,6 +928,7 @@ export class ServiceContainer { this.logService, this.platformUtilsService, this.configService, + this.sdkService, ), ); diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index e5c197ef51c..7154c42ac89 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -377,9 +377,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "basic-toml" @@ -3295,6 +3295,9 @@ dependencies = [ "bcrypt-pbkdf", "ed25519-dalek", "num-bigint-dig", + "p256", + "p384", + "p521", "rand_core 0.6.4", "rsa", "sec1", @@ -3306,6 +3309,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ssh_agent" +version = "0.0.0" +dependencies = [ + "anyhow", + "base64", + "ssh-key", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index b3fac851026..09a4d603327 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -9,6 +9,7 @@ members = [ "napi", "process_isolation", "proxy", + "ssh_agent", "windows_plugin_authenticator", ] diff --git a/apps/desktop/desktop_native/ssh_agent/Cargo.toml b/apps/desktop/desktop_native/ssh_agent/Cargo.toml new file mode 100644 index 00000000000..becab28c356 --- /dev/null +++ b/apps/desktop/desktop_native/ssh_agent/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ssh_agent" +edition = { workspace = true } +license = { workspace = true } +version = { workspace = true } +publish = { workspace = true } + +[dependencies] +anyhow = { workspace = true } +base64 = { workspace = true } +ssh-key = { version = "=0.6.7", features = [ + "encryption", + "ed25519", + "rsa", + "rand_core", +] } + +[lints] +workspace = true diff --git a/apps/desktop/desktop_native/ssh_agent/src/crypto/mod.rs b/apps/desktop/desktop_native/ssh_agent/src/crypto/mod.rs new file mode 100644 index 00000000000..655e440dc78 --- /dev/null +++ b/apps/desktop/desktop_native/ssh_agent/src/crypto/mod.rs @@ -0,0 +1,184 @@ +//! Cryptographic key management for the SSH agent. +//! +//! This module provides the core primitive types and functionality for managing +//! SSH keys in the Bitwarden SSH agent. +//! +//! # Supported signing algorithms +//! +//! - Ed25519 +//! - RSA +//! +//! ECDSA keys are not currently supported (PM-29894) + +use std::fmt; + +use anyhow::anyhow; +use ssh_key::private::{Ed25519Keypair, RsaKeypair}; + +/// Represents an SSH key and its associated metadata. +#[derive(Clone)] +pub(crate) struct SSHKeyData { + /// Private key of the key pair + private_key: PrivateKey, + /// Public key of the key pair + public_key: PublicKey, + /// Human-readable name + name: String, + /// Vault cipher ID associated with the key pair + cipher_id: String, +} + +impl SSHKeyData { + /// Creates a new `SSHKeyData` instance. + /// + /// # Arguments + /// + /// * `private_key` - The private key component + /// * `public_key` - The public key component + /// * `name` - A human-readable name for the key + /// * `cipher_id` - The vault cipher identifier associated with this key + pub(crate) fn new( + private_key: PrivateKey, + public_key: PublicKey, + name: String, + cipher_id: String, + ) -> Self { + Self { + private_key, + public_key, + name, + cipher_id, + } + } + + /// # Returns + /// + /// A reference to the [`PublicKey`]. + pub(crate) fn public_key(&self) -> &PublicKey { + &self.public_key + } + + /// # Returns + /// + /// A reference to the [`PrivateKey`]. + pub(crate) fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + /// # Returns + /// + /// A reference to the human-readable name for this key. + pub(crate) fn name(&self) -> &String { + &self.name + } + + /// # Returns + /// + /// A reference to the cipher ID that links this key to a vault entry. + pub(crate) fn cipher_id(&self) -> &String { + &self.cipher_id + } +} + +/// Represents an SSH private key. +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum PrivateKey { + Ed25519(Ed25519Keypair), + Rsa(RsaKeypair), +} + +impl TryFrom for PrivateKey { + type Error = anyhow::Error; + + fn try_from(key: ssh_key::private::PrivateKey) -> Result { + match key.algorithm() { + ssh_key::Algorithm::Ed25519 => Ok(Self::Ed25519( + key.key_data() + .ed25519() + .ok_or(anyhow!("Failed to parse ed25519 key"))? + .to_owned(), + )), + ssh_key::Algorithm::Rsa { hash: _ } => Ok(Self::Rsa( + key.key_data() + .rsa() + .ok_or(anyhow!("Failed to parse RSA key"))? + .to_owned(), + )), + _ => Err(anyhow!("Unsupported key type")), + } + } +} + +/// Represents an SSH public key. +/// +/// Contains the algorithm identifier (e.g., "ssh-ed25519", "ssh-rsa") +/// and the binary blob of the public key data. +#[derive(Clone, Ord, Eq, PartialOrd, PartialEq)] +pub(crate) struct PublicKey { + pub alg: String, + pub blob: Vec, +} + +impl PublicKey { + pub(crate) fn alg(&self) -> &str { + &self.alg + } + + pub(crate) fn blob(&self) -> &[u8] { + &self.blob + } +} + +impl fmt::Debug for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PublicKey(\"{self}\")") + } +} + +impl fmt::Display for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use base64::{prelude::BASE64_STANDARD, Engine as _}; + + write!(f, "{} {}", self.alg(), BASE64_STANDARD.encode(self.blob())) + } +} + +#[cfg(test)] +mod tests { + use ssh_key::{ + private::{Ed25519Keypair, RsaKeypair}, + rand_core::OsRng, + LineEnding, + }; + + use super::*; + + const MIN_KEY_BIT_SIZE: usize = 2048; + + fn create_valid_ed25519_key_string() -> String { + let ed25519_keypair = Ed25519Keypair::random(&mut OsRng); + let ssh_key = + ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ed25519(ed25519_keypair), "") + .unwrap(); + ssh_key.to_openssh(LineEnding::LF).unwrap().to_string() + } + + #[test] + fn test_privatekey_from_ed25519() { + let key_string = create_valid_ed25519_key_string(); + let ssh_key = ssh_key::PrivateKey::from_openssh(&key_string).unwrap(); + + let private_key = PrivateKey::try_from(ssh_key).unwrap(); + assert!(matches!(private_key, PrivateKey::Ed25519(_))); + } + + #[test] + fn test_privatekey_from_rsa() { + let rsa_keypair = RsaKeypair::random(&mut OsRng, MIN_KEY_BIT_SIZE).unwrap(); + let ssh_key = + ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Rsa(rsa_keypair), "").unwrap(); + + let private_key = PrivateKey::try_from(ssh_key).unwrap(); + assert!(matches!(private_key, PrivateKey::Rsa(_))); + } +} diff --git a/apps/desktop/desktop_native/ssh_agent/src/lib.rs b/apps/desktop/desktop_native/ssh_agent/src/lib.rs new file mode 100644 index 00000000000..aaaf635e6c6 --- /dev/null +++ b/apps/desktop/desktop_native/ssh_agent/src/lib.rs @@ -0,0 +1,7 @@ +//! Bitwarden SSH Agent implementation +//! +//! + +#![allow(dead_code)] // TODO remove when all code is used in follow-up PR + +mod crypto; diff --git a/bitwarden_license/bit-cli/src/bit-serve-configurator.ts b/bitwarden_license/bit-cli/src/bit-serve-configurator.ts index 71df651d9d0..5a00dccb59b 100644 --- a/bitwarden_license/bit-cli/src/bit-serve-configurator.ts +++ b/bitwarden_license/bit-cli/src/bit-serve-configurator.ts @@ -1,4 +1,4 @@ -import * as koaRouter from "@koa/router"; +import { Router } from "@koa/router"; import { OssServeConfigurator } from "@bitwarden/cli/oss-serve-configurator"; @@ -16,7 +16,7 @@ export class BitServeConfigurator extends OssServeConfigurator { super(serviceContainer); } - override async configureRouter(router: koaRouter): Promise { + override async configureRouter(router: Router): Promise { // Register OSS endpoints await super.configureRouter(router); @@ -24,7 +24,7 @@ export class BitServeConfigurator extends OssServeConfigurator { this.serveDeviceApprovals(router); } - private serveDeviceApprovals(router: koaRouter) { + private serveDeviceApprovals(router: Router) { router.get("/device-approval/:organizationId", async (ctx, next) => { if (await this.errorIfLocked(ctx.response)) { await next(); diff --git a/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts b/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts index 74a91897a58..f2b51d6747a 100644 --- a/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts +++ b/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-restore.request.ts @@ -1,11 +1,11 @@ import { EncString } from "@bitwarden/sdk-internal"; export class OrganizationUserBulkRestoreRequest { - userIds: string[]; + ids: string[]; defaultUserCollectionName: EncString | undefined; - constructor(userIds: string[], defaultUserCollectionName?: EncString) { - this.userIds = userIds; + constructor(ids: string[], defaultUserCollectionName?: EncString) { + this.ids = ids; this.defaultUserCollectionName = defaultUserCollectionName; } } diff --git a/libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts index 0448b23e4d2..7eca35fd36e 100644 --- a/libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts @@ -258,7 +258,7 @@ describe("DefaultOrganizationUserService", () => { ).toHaveBeenCalledWith( mockOrganization.id, expect.objectContaining({ - userIds: mockUserIds, + ids: mockUserIds, defaultUserCollectionName: mockEncryptedCollectionName.encryptedString, }), ); diff --git a/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts b/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts index b16371198b3..645666c582d 100644 --- a/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts @@ -7,7 +7,7 @@ import { CsprngArray } from "../../../types/csprng"; export abstract class CryptoFunctionService { /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract pbkdf2( @@ -17,7 +17,7 @@ export abstract class CryptoFunctionService { iterations: number, ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract hkdf( @@ -28,7 +28,7 @@ export abstract class CryptoFunctionService { algorithm: "sha256" | "sha512", ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract hkdfExpand( @@ -38,7 +38,7 @@ export abstract class CryptoFunctionService { algorithm: "sha256" | "sha512", ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract hash( @@ -46,7 +46,7 @@ export abstract class CryptoFunctionService { algorithm: "sha1" | "sha256" | "sha512" | "md5", ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract hmacFast( @@ -56,7 +56,7 @@ export abstract class CryptoFunctionService { ): Promise; abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract aesDecryptFastParameters( @@ -66,7 +66,7 @@ export abstract class CryptoFunctionService { key: SymmetricCryptoKey, ): CbcDecryptParameters; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract aesDecryptFast({ @@ -76,7 +76,7 @@ export abstract class CryptoFunctionService { | { mode: "cbc"; parameters: CbcDecryptParameters } | { mode: "ecb"; parameters: EcbDecryptParameters }): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Only used by DDG integration until DDG uses PKCS#7 padding, and by lastpass importer. + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Only used by DDG integration until DDG uses PKCS#7 padding, and by lastpass importer. */ abstract aesDecrypt( data: Uint8Array, @@ -85,7 +85,7 @@ export abstract class CryptoFunctionService { mode: "cbc" | "ecb", ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract rsaEncrypt( @@ -94,7 +94,7 @@ export abstract class CryptoFunctionService { algorithm: "sha1", ): Promise; /** - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. Implement low-level crypto operations * in the SDK instead. Further, you should probably never find yourself using this low-level crypto function. */ abstract rsaDecrypt( diff --git a/libs/common/src/key-management/crypto/key-generation/key-generation.service.ts b/libs/common/src/key-management/crypto/key-generation/key-generation.service.ts index ddc3829aa9f..1114e892bb8 100644 --- a/libs/common/src/key-management/crypto/key-generation/key-generation.service.ts +++ b/libs/common/src/key-management/crypto/key-generation/key-generation.service.ts @@ -27,7 +27,7 @@ export abstract class KeyGenerationService { * Uses HKDF, see {@link https://datatracker.ietf.org/doc/html/rfc5869 RFC 5869} * for details. * - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function. + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. This is a low-level cryptographic function. * New functionality should not be built on top of it, and instead should be built in the sdk. * * @param bitLength Length of key material. @@ -44,7 +44,7 @@ export abstract class KeyGenerationService { /** * Derives a 64 byte key from key material. * - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function. + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. This is a low-level cryptographic function. * New functionality should not be built on top of it, and instead should be built in the sdk. * * @remark The key material should be generated from {@link createKey}, or {@link createKeyWithPurpose}. @@ -63,7 +63,7 @@ export abstract class KeyGenerationService { /** * Derives a 32 byte key from a password using a key derivation function. * - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function. + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. This is a low-level cryptographic function. * New functionality should not be built on top of it, and instead should be built in the sdk. * * @param password Password to derive the key from. @@ -80,7 +80,7 @@ export abstract class KeyGenerationService { /** * Derives a 64 byte key from a 32 byte key using a key derivation function. * - * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function. + * @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. CONTACT KEY MANAGEMENT IF YOU THINK YOU NEED TO. This is a low-level cryptographic function. * New functionality should not be built on top of it, and instead should be built in the sdk. * * @param key 32 byte key. diff --git a/libs/common/src/tools/providers.spec.ts b/libs/common/src/tools/providers.spec.ts index 5953e5ebab2..d457b1df85e 100644 --- a/libs/common/src/tools/providers.spec.ts +++ b/libs/common/src/tools/providers.spec.ts @@ -4,6 +4,7 @@ import { PolicyService } from "../admin-console/abstractions/policy/policy.servi import { ConfigService } from "../platform/abstractions/config/config.service"; import { LogService } from "../platform/abstractions/log.service"; import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; +import { SdkService } from "../platform/abstractions/sdk/sdk.service"; import { StateProvider } from "../platform/state"; import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider"; @@ -20,6 +21,7 @@ describe("SystemServiceProvider", () => { let mockLogger: LogService; let mockEnvironment: MockProxy; let mockConfigService: ConfigService; + let mockSdkService: SdkService; beforeEach(() => { jest.resetAllMocks(); @@ -31,6 +33,7 @@ describe("SystemServiceProvider", () => { mockLogger = mock(); mockEnvironment = mock(); mockConfigService = mock(); + mockSdkService = mock(); }); describe("createSystemServiceProvider", () => { @@ -45,6 +48,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result).toHaveProperty("policy", mockPolicy); @@ -66,6 +70,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result.extension).toBeInstanceOf(ExtensionService); @@ -83,6 +88,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(mockEnvironment.isDev).toHaveBeenCalledTimes(1); @@ -102,6 +108,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(mockEnvironment.isDev).toHaveBeenCalledTimes(1); @@ -121,6 +128,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result.extension).toBeInstanceOf(ExtensionService); @@ -138,6 +146,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result.policy).toBe(mockPolicy); @@ -154,6 +163,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result.configService).toBe(mockConfigService); @@ -170,6 +180,7 @@ describe("SystemServiceProvider", () => { mockLogger, mockEnvironment, mockConfigService, + mockSdkService, ); expect(result.environment).toBe(mockEnvironment); diff --git a/libs/common/src/tools/providers.ts b/libs/common/src/tools/providers.ts index ac42c556042..b1621f19c21 100644 --- a/libs/common/src/tools/providers.ts +++ b/libs/common/src/tools/providers.ts @@ -1,10 +1,10 @@ import { LogService } from "@bitwarden/logging"; -import { BitwardenClient } from "@bitwarden/sdk-internal"; import { StateProvider } from "@bitwarden/state"; import { PolicyService } from "../admin-console/abstractions/policy/policy.service.abstraction"; import { ConfigService } from "../platform/abstractions/config/config.service"; import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; +import { SdkService } from "../platform/abstractions/sdk/sdk.service"; import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider"; import { ExtensionRegistry } from "./extension/extension-registry.abstraction"; @@ -29,7 +29,7 @@ export type SystemServiceProvider = { readonly environment: PlatformUtilsService; /** SDK Service */ - readonly sdk?: BitwardenClient; + readonly sdk: SdkService; }; /** Constructs a system service provider. */ @@ -41,6 +41,7 @@ export function createSystemServiceProvider( logger: LogService, environment: PlatformUtilsService, configService: ConfigService, + sdk: SdkService, ): SystemServiceProvider { let log: LogProvider; if (environment.isDev()) { @@ -62,5 +63,6 @@ export function createSystemServiceProvider( log, configService, environment, + sdk, }; } diff --git a/libs/components/src/button/button.component.html b/libs/components/src/button/button.component.html index 26e0c3b4d3d..d8718340217 100644 --- a/libs/components/src/button/button.component.html +++ b/libs/components/src/button/button.component.html @@ -1,6 +1,14 @@ - - - + + + @if (startIcon()) { + + } +
+ +
+ @if (endIcon()) { + + }
@if (showLoadingStyle()) { diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 7cae8fe974d..1055d134e53 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -1,4 +1,4 @@ -import { NgClass } from "@angular/common"; +import { NgClass, NgTemplateOutlet } from "@angular/common"; import { input, HostBinding, @@ -14,6 +14,7 @@ import { debounce, interval } from "rxjs"; import { AriaDisableDirective } from "../a11y"; import { ButtonLikeAbstraction, ButtonType, ButtonSize } from "../shared/button-like.abstraction"; +import { BitwardenIcon } from "../shared/icon"; import { SpinnerComponent } from "../spinner"; import { ariaDisableElement } from "../utils"; @@ -71,7 +72,7 @@ const buttonStyles: Record = { selector: "button[bitButton], a[bitButton]", templateUrl: "button.component.html", providers: [{ provide: ButtonLikeAbstraction, useExisting: ButtonComponent }], - imports: [NgClass, SpinnerComponent], + imports: [NgClass, NgTemplateOutlet, SpinnerComponent], hostDirectives: [AriaDisableDirective], }) export class ButtonComponent implements ButtonLikeAbstraction { @@ -125,12 +126,23 @@ export class ButtonComponent implements ButtonLikeAbstraction { readonly buttonType = input("secondary"); + readonly startIcon = input(undefined); + + readonly endIcon = input(undefined); + readonly size = input("default"); readonly block = input(false, { transform: booleanAttribute }); readonly loading = model(false); + readonly startIconClasses = computed(() => { + return ["bwi", this.startIcon()]; + }); + + readonly endIconClasses = computed(() => { + return ["bwi", this.endIcon()]; + }); /** * Determine whether it is appropriate to display a loading spinner. We only want to show * a spinner if it's been more than 75 ms since the `loading` state began. This prevents diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 24c263f240a..9e8d23611ff 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -152,15 +152,13 @@ export const WithIcon: Story = { template: /*html*/ `
-
-
diff --git a/libs/components/src/callout/callout.stories.ts b/libs/components/src/callout/callout.stories.ts index ff1a8c16d5f..fb1a2d67a40 100644 --- a/libs/components/src/callout/callout.stories.ts +++ b/libs/components/src/callout/callout.stories.ts @@ -113,7 +113,7 @@ export const WithTextButton: Story = { template: ` (args)}>

The content of the callout

- Visit the help center + Visit the help center
`, }), diff --git a/libs/components/src/card/base-card/base-card.stories.ts b/libs/components/src/card/base-card/base-card.stories.ts index bae07dd1468..98814c1f9f4 100644 --- a/libs/components/src/card/base-card/base-card.stories.ts +++ b/libs/components/src/card/base-card/base-card.stories.ts @@ -1,6 +1,6 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; -import { AnchorLinkDirective } from "../../link"; +import { LinkComponent } from "../../link"; import { TypographyModule } from "../../typography"; import { BaseCardComponent } from "./base-card.component"; @@ -10,7 +10,7 @@ export default { component: BaseCardComponent, decorators: [ moduleMetadata({ - imports: [AnchorLinkDirective, TypographyModule], + imports: [LinkComponent, TypographyModule], }), ], parameters: { diff --git a/libs/components/src/layout/layout.component.ts b/libs/components/src/layout/layout.component.ts index da30b76a9f0..c71c4e73c6e 100644 --- a/libs/components/src/layout/layout.component.ts +++ b/libs/components/src/layout/layout.component.ts @@ -5,7 +5,7 @@ import { booleanAttribute, Component, ElementRef, inject, input, viewChild } fro import { RouterModule } from "@angular/router"; import { DrawerService } from "../dialog/drawer.service"; -import { LinkModule } from "../link"; +import { LinkComponent, LinkModule } from "../link"; import { SideNavService } from "../navigation/side-nav.service"; import { SharedModule } from "../shared"; @@ -52,11 +52,11 @@ export class LayoutComponent { * * @see https://github.com/angular/components/issues/10247#issuecomment-384060265 **/ - private readonly skipLink = viewChild.required>("skipLink"); + private readonly skipLink = viewChild.required("skipLink"); handleKeydown(ev: KeyboardEvent) { if (isNothingFocused()) { ev.preventDefault(); - this.skipLink().nativeElement.focus(); + this.skipLink().el.nativeElement.focus(); } } } diff --git a/libs/components/src/link/index.ts b/libs/components/src/link/index.ts index 480f5396de7..08617e813f5 100644 --- a/libs/components/src/link/index.ts +++ b/libs/components/src/link/index.ts @@ -1,2 +1,2 @@ -export * from "./link.directive"; +export * from "./link.component"; export * from "./link.module"; diff --git a/libs/components/src/link/link.component.html b/libs/components/src/link/link.component.html new file mode 100644 index 00000000000..810b65db519 --- /dev/null +++ b/libs/components/src/link/link.component.html @@ -0,0 +1,11 @@ +
+ @if (startIcon()) { + + } + + + + @if (endIcon()) { + + } +
diff --git a/libs/components/src/link/link.directive.ts b/libs/components/src/link/link.component.ts similarity index 59% rename from libs/components/src/link/link.directive.ts rename to libs/components/src/link/link.component.ts index 62f0d8b878f..d826a4633a9 100644 --- a/libs/components/src/link/link.directive.ts +++ b/libs/components/src/link/link.component.ts @@ -1,6 +1,14 @@ -import { input, HostBinding, Directive, inject, ElementRef, booleanAttribute } from "@angular/core"; +import { + ChangeDetectionStrategy, + Component, + computed, + input, + booleanAttribute, + inject, + ElementRef, +} from "@angular/core"; -import { AriaDisableDirective } from "../a11y"; +import { BitwardenIcon } from "../shared/icon"; import { ariaDisableElement } from "../utils"; export const LinkTypes = [ @@ -46,16 +54,16 @@ const commonStyles = [ "tw-transition", "tw-no-underline", "tw-cursor-pointer", - "hover:tw-underline", - "hover:tw-decoration-1", + "[&:hover_span]:tw-underline", + "[&.tw-test-hover_span]:tw-underline", + "[&:hover_span]:tw-decoration-[.125em]", + "[&.tw-test-hover_span]:tw-decoration-[.125em]", "disabled:tw-no-underline", "disabled:tw-cursor-not-allowed", "disabled:!tw-text-fg-disabled", "disabled:hover:!tw-text-fg-disabled", "disabled:hover:tw-no-underline", "focus-visible:tw-outline-none", - "focus-visible:tw-underline", - "focus-visible:tw-decoration-1", "focus-visible:before:tw-ring-border-focus", // Workaround for html button tag not being able to be set to `display: inline` @@ -72,8 +80,12 @@ const commonStyles = [ "before:tw-block", "before:tw-absolute", "before:-tw-inset-x-[0.1em]", + "before:-tw-inset-y-[0]", "before:tw-rounded-md", "before:tw-transition", + "before:tw-h-full", + "before:tw-w-[calc(100%_+_.25rem)]", + "before:tw-pointer-events-none", "focus-visible:before:tw-ring-2", "focus-visible:tw-z-10", "aria-disabled:tw-no-underline", @@ -83,47 +95,57 @@ const commonStyles = [ "aria-disabled:hover:tw-no-underline", ]; -@Directive() -abstract class LinkDirective { - readonly linkType = input("default"); -} - -/** - * Text Links and Buttons can use either the `` or `
-
-
@@ -203,7 +201,7 @@ export const Buttons: Story = { }, }; -export const Anchors: StoryObj = { +export const Anchors: StoryObj = { render: (args) => ({ props: { linkType: args.linkType, @@ -220,14 +218,12 @@ export const Anchors: StoryObj = { Anchor @@ -247,20 +243,57 @@ export const Inline: Story = { props: args, template: /*html*/ ` - On the internet paragraphs often contain inline links, but few know that can be used for similar purposes. + On the internet paragraphs often contain inline links with very long text that might break, but few know that can be used for similar purposes. `, }), }; -export const Inactive: Story = { +export const WithIcons: Story = { render: (args) => ({ props: args, template: /*html*/ ` - - -
- +
+ + + +
+ +
+
+ +
+
+ +
+
+ `, + }), + args: { + linkType: "primary", + }, +}; + +export const Inactive: Story = { + render: (args) => ({ + props: { + ...args, + onClick: () => { + alert("Button clicked! (This should not appear when disabled)"); + }, + }, + template: /*html*/ ` + + Links can not be inactive + +
+
`, }), diff --git a/libs/importer/src/components/importer-providers.ts b/libs/importer/src/components/importer-providers.ts index 18c148ebe2e..eb7e58e9259 100644 --- a/libs/importer/src/components/importer-providers.ts +++ b/libs/importer/src/components/importer-providers.ts @@ -13,6 +13,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider"; import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider"; import { ExtensionRegistry } from "@bitwarden/common/tools/extension/extension-registry.abstraction"; @@ -71,6 +72,7 @@ export const ImporterProviders: SafeProvider[] = [ LogService, PlatformUtilsService, ConfigService, + SdkService, ], }), safeProvider({ diff --git a/libs/tools/generator/components/src/generator-services.module.ts b/libs/tools/generator/components/src/generator-services.module.ts index 935f7dc2d60..39d0dd298a2 100644 --- a/libs/tools/generator/components/src/generator-services.module.ts +++ b/libs/tools/generator/components/src/generator-services.module.ts @@ -1,12 +1,10 @@ import { NgModule } from "@angular/core"; -import { from, take } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -126,7 +124,7 @@ export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken (featureFlag = ff)); const metadata = new providers.GeneratorMetadataProvider( userStateDeps, system, Object.values(BuiltIn), ); - const sdkService = featureFlag ? system.sdk : undefined; const profile = new providers.GeneratorProfileProvider(userStateDeps, system.policy); const generator: providers.GeneratorDependencyProvider = { randomizer: random, client: new RestClient(api, i18n), i18nService: i18n, - sdk: sdkService, + sdk: system.sdk, now: Date.now, }; diff --git a/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts b/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts index 03be21eeefb..09c7d62b1ad 100644 --- a/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts +++ b/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts @@ -1,3 +1,6 @@ +import { firstValueFrom } from "rxjs"; + +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { BitwardenClient, PassphraseGeneratorRequest, @@ -20,11 +23,11 @@ export class SdkPasswordRandomizer CredentialGenerator { /** Instantiates the password randomizer - * @param client access to SDK client to call upon password/passphrase generation + * @param service access to SDK client to call upon password/passphrase generation * @param currentTime gets the current datetime in epoch time */ constructor( - private client: BitwardenClient, + private service: SdkService, private currentTime: () => number, ) {} @@ -40,8 +43,9 @@ export class SdkPasswordRandomizer request: GenerateRequest, settings: PasswordGenerationOptions | PassphraseGenerationOptions, ) { + const sdk: BitwardenClient = await firstValueFrom(this.service.client$); if (isPasswordGenerationOptions(settings)) { - const password = await this.client.generator().password(convertPasswordRequest(settings)); + const password = await sdk.generator().password(convertPasswordRequest(settings)); return new GeneratedCredential( password, @@ -51,9 +55,7 @@ export class SdkPasswordRandomizer request.website, ); } else if (isPassphraseGenerationOptions(settings)) { - const passphrase = await this.client - .generator() - .passphrase(convertPassphraseRequest(settings)); + const passphrase = await sdk.generator().passphrase(convertPassphraseRequest(settings)); return new GeneratedCredential( passphrase, diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts index bdf021c50f3..015cc25a8ec 100644 --- a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts @@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine"; +import { SdkPasswordRandomizer } from "../../engine"; import { PassphrasePolicyConstraints } from "../../policies"; import { GeneratorDependencyProvider } from "../../providers"; import { PassphraseGenerationOptions } from "../../types"; @@ -22,16 +22,6 @@ describe("password - eff words generator metadata", () => { }); }); - describe("engine.create", () => { - const nonSdkDependencyProvider = mock(); - nonSdkDependencyProvider.sdk = undefined; - it("returns a password randomizer", () => { - expect(effPassphrase.engine.create(nonSdkDependencyProvider)).toBeInstanceOf( - PasswordRandomizer, - ); - }); - }); - describe("profiles[account]", () => { let accountProfile: CoreProfileMetadata | null = null; beforeEach(() => { diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts index fc96ce46c2b..d6d78c83293 100644 --- a/libs/tools/generator/core/src/metadata/password/eff-word-list.ts +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts @@ -3,7 +3,7 @@ import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; -import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine"; +import { SdkPasswordRandomizer } from "../../engine"; import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies"; import { GeneratorDependencyProvider } from "../../providers"; import { CredentialGenerator, PassphraseGenerationOptions } from "../../types"; @@ -30,9 +30,6 @@ const passphrase: GeneratorMetadata = { create( dependencies: GeneratorDependencyProvider, ): CredentialGenerator { - if (dependencies.sdk == undefined) { - return new PasswordRandomizer(dependencies.randomizer, dependencies.now); - } return new SdkPasswordRandomizer(dependencies.sdk, dependencies.now); }, }, diff --git a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts index 9efd5350c21..d066b9f1597 100644 --- a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts @@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine"; +import { SdkPasswordRandomizer } from "../../engine"; import { DynamicPasswordPolicyConstraints } from "../../policies"; import { GeneratorDependencyProvider } from "../../providers"; import { PasswordGenerationOptions } from "../../types"; @@ -22,14 +22,6 @@ describe("password - characters generator metadata", () => { }); }); - describe("engine.create", () => { - const nonSdkDependencyProvider = mock(); - nonSdkDependencyProvider.sdk = undefined; - it("returns a password randomizer", () => { - expect(password.engine.create(nonSdkDependencyProvider)).toBeInstanceOf(PasswordRandomizer); - }); - }); - describe("profiles[account]", () => { let accountProfile: CoreProfileMetadata = null!; beforeEach(() => { diff --git a/libs/tools/generator/core/src/metadata/password/random-password.ts b/libs/tools/generator/core/src/metadata/password/random-password.ts index 721be8dc3f0..d25ea1e8f46 100644 --- a/libs/tools/generator/core/src/metadata/password/random-password.ts +++ b/libs/tools/generator/core/src/metadata/password/random-password.ts @@ -3,7 +3,7 @@ import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; import { deepFreeze } from "@bitwarden/common/tools/util"; -import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine"; +import { SdkPasswordRandomizer } from "../../engine"; import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies"; import { GeneratorDependencyProvider } from "../../providers"; import { CredentialGenerator, PasswordGeneratorSettings } from "../../types"; @@ -30,9 +30,6 @@ const password: GeneratorMetadata = deepFreeze({ create( dependencies: GeneratorDependencyProvider, ): CredentialGenerator { - if (dependencies.sdk == undefined) { - return new PasswordRandomizer(dependencies.randomizer, dependencies.now); - } return new SdkPasswordRandomizer(dependencies.sdk, dependencies.now); }, }, diff --git a/libs/tools/generator/core/src/providers/generator-dependency-provider.ts b/libs/tools/generator/core/src/providers/generator-dependency-provider.ts index a6dbbeaa537..8700bbc8a24 100644 --- a/libs/tools/generator/core/src/providers/generator-dependency-provider.ts +++ b/libs/tools/generator/core/src/providers/generator-dependency-provider.ts @@ -1,6 +1,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { RestClient } from "@bitwarden/common/tools/integration/rpc"; -import { BitwardenClient } from "@bitwarden/sdk-internal"; import { Randomizer } from "../abstractions"; @@ -10,6 +10,6 @@ export type GeneratorDependencyProvider = { // FIXME: introduce `I18nKeyOrLiteral` into forwarder // structures and remove this dependency i18nService: I18nService; - sdk?: BitwardenClient; + sdk: SdkService; now: () => number; }; diff --git a/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts index 39ff74ad901..f79bb986325 100644 --- a/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts +++ b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts @@ -5,7 +5,6 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider"; import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction"; import { @@ -96,8 +95,6 @@ const SomePolicyService = mock(); const SomeExtensionService = mock(); -const SomeConfigService = mock; - const SomeSdkService = mock; const ApplicationProvider = { @@ -110,9 +107,6 @@ const ApplicationProvider = { /** Event monitoring and diagnostic interfaces */ log: disabledSemanticLoggerProvider, - /** Feature flag retrieval */ - configService: SomeConfigService, - /** SDK access for password generation */ sdk: SomeSdkService, } as unknown as SystemServiceProvider; diff --git a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts index 35d3d8725ff..a4aabbb6f19 100644 --- a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts @@ -28,6 +28,7 @@ export type OptionalInitialValues = { // Credit Card Information cardholderName?: string; number?: string; + brand?: string; expMonth?: string; expYear?: string; code?: string; diff --git a/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts b/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts index 3580b1fada8..04545730172 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts @@ -3,13 +3,13 @@ import { Component, inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { - ButtonLinkDirective, ButtonModule, + CenterPositionStrategy, DialogModule, + DialogRef, DialogService, DIALOG_DATA, - DialogRef, - CenterPositionStrategy, + LinkComponent, } from "@bitwarden/components"; export type AdvancedUriOptionDialogParams = { @@ -22,7 +22,7 @@ export type AdvancedUriOptionDialogParams = { // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "advanced-uri-option-dialog.component.html", - imports: [ButtonLinkDirective, ButtonModule, DialogModule, JslibModule], + imports: [LinkComponent, ButtonModule, DialogModule, JslibModule], }) export class AdvancedUriOptionDialogComponent { constructor(private dialogRef: DialogRef) {} diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts index 4b0cd0f5f90..650b4e29fe5 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts @@ -108,12 +108,17 @@ describe("CardDetailsSectionComponent", () => { const cardholderName = "Ron Burgundy"; const number = "4242 4242 4242 4242"; const code = "619"; + const brand = "Maestro"; + const expMonth = "5"; + const expYear = "2028"; const cardView = new CardView(); cardView.cardholderName = cardholderName; cardView.number = number; cardView.code = code; - cardView.brand = "Visa"; + cardView.brand = brand; + cardView.expMonth = expMonth; + cardView.expYear = expYear; getInitialCipherView.mockReturnValueOnce({ card: cardView }); @@ -123,7 +128,9 @@ describe("CardDetailsSectionComponent", () => { cardholderName, number, code, - brand: cardView.brand, + brand, + expMonth, + expYear, }); }); @@ -154,4 +161,27 @@ describe("CardDetailsSectionComponent", () => { expect(heading.nativeElement.textContent.trim()).toBe("cardDetails"); }); + + it("initializes `cardDetailsForm` from `initialValues` when provided and editing existing cipher", () => { + const initialCardholderName = "New Name"; + const initialBrand = "Amex"; + + (cipherFormProvider as any).config = { + initialValues: { + cardholderName: initialCardholderName, + brand: initialBrand, + }, + }; + + const existingCard = new CardView(); + existingCard.cardholderName = "Old Name"; + existingCard.brand = "Visa"; + + getInitialCipherView.mockReturnValueOnce({ card: existingCard }); + + component.ngOnInit(); + + expect(component.cardDetailsForm.value.cardholderName).toBe(initialCardholderName); + expect(component.cardDetailsForm.value.brand).toBe(initialBrand); + }); }); diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts index 5fa8d0af131..056b93b6b99 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts @@ -158,6 +158,7 @@ export class CardDetailsSectionComponent implements OnInit { this.cardDetailsForm.patchValue({ cardholderName: this.initialValues?.cardholderName ?? existingCard.cardholderName, number: this.initialValues?.number ?? existingCard.number, + brand: this.initialValues?.brand ?? existingCard.brand, expMonth: this.initialValues?.expMonth ?? existingCard.expMonth, expYear: this.initialValues?.expYear ?? existingCard.expYear, code: this.initialValues?.code ?? existingCard.code, diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index 3d0cc4c4414..05d2ecede72 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -12,9 +12,15 @@ - + {{ "changeAtRiskPassword" | i18n }} - diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 26e3f18b542..24713d3f612 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -30,7 +30,7 @@ import { CalloutModule, SearchModule, TypographyModule, - AnchorLinkDirective, + LinkComponent, } from "@bitwarden/components"; import { ChangeLoginPasswordService } from "../abstractions/change-login-password.service"; @@ -66,7 +66,7 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide ViewIdentitySectionsComponent, LoginCredentialsViewComponent, AutofillOptionsViewComponent, - AnchorLinkDirective, + LinkComponent, TypographyModule, ], }) diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index eb0e468fa4f..73e7c2706be 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -19,9 +19,9 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { BadgeModule, - ButtonLinkDirective, CardComponent, FormFieldModule, + LinkComponent, TypographyModule, } from "@bitwarden/components"; @@ -39,7 +39,7 @@ import { OrgIconDirective } from "../../components/org-icon.directive"; TypographyModule, OrgIconDirective, FormFieldModule, - ButtonLinkDirective, + LinkComponent, BadgeModule, ], }) diff --git a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts index 6b1a0e0d8aa..e829c003c5a 100644 --- a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts +++ b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts @@ -7,7 +7,7 @@ import { CipherId } from "@bitwarden/common/types/guid"; import { DIALOG_DATA, DialogRef, - AnchorLinkDirective, + LinkComponent, AsyncActionsModule, ButtonModule, DialogModule, @@ -32,7 +32,7 @@ export type DecryptionFailureDialogParams = { JslibModule, AsyncActionsModule, ButtonModule, - AnchorLinkDirective, + LinkComponent, ], }) export class DecryptionFailureDialogComponent { diff --git a/package-lock.json b/package-lock.json index 6352675d718..d899e4479d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,12 +23,12 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.506", - "@bitwarden/sdk-internal": "0.2.0-main.506", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.522", + "@bitwarden/sdk-internal": "0.2.0-main.522", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", - "@koa/router": "14.0.0", + "@koa/router": "15.2.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", "@ng-select/ng-select": "20.7.0", @@ -104,7 +104,6 @@ "@types/jsdom": "21.1.7", "@types/koa": "3.0.1", "@types/koa__multer": "2.0.7", - "@types/koa__router": "12.0.4", "@types/koa-bodyparser": "4.3.7", "@types/koa-json": "2.0.24", "@types/lowdb": "1.0.15", @@ -200,7 +199,7 @@ "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "4.0.0", - "@koa/router": "14.0.0", + "@koa/router": "15.2.0", "big-integer": "1.6.52", "browser-hrtime": "1.1.8", "chalk": "4.1.2", @@ -4982,9 +4981,10 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.506", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.506.tgz", - "integrity": "sha512-aRzcxOcj8vXxz0jN3q2xxj26zxBfjg3oRm5QXbWE7zXJ2PGrgxTaePca9pQYYpwgr7iufYMnZcq5dH+qttNEmA==", + "version": "0.2.0-main.522", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.522.tgz", + "integrity": "sha512-2wAbg30cGlDhSj14LaK2/ISuT91XPVeNgL/PU+eoxLhAehGKjAXdvZN3PSwFaAuaMbEFzlESvqC1pzzO4p/1zw==", + "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" } @@ -5086,9 +5086,10 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.506", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.506.tgz", - "integrity": "sha512-BbTSU5Acx74Hr32zDj2kV8sbdclyvdIti5t6kXnCvJmA5dZbu+5j5Xw1luS9mGL9Vfi4w3OjVug/TiSxyhwLzQ==", + "version": "0.2.0-main.522", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.522.tgz", + "integrity": "sha512-E+YqqX/FvGF0vGx6sNJfYaMj88C+rVo51fQPMSHoOePdryFcKQSJX706Glv86OMLMXE7Ln5Lua8LJRftlF/EFQ==", + "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" } @@ -8746,18 +8747,46 @@ } }, "node_modules/@koa/router": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-14.0.0.tgz", - "integrity": "sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-15.2.0.tgz", + "integrity": "sha512-7YUhq4W83cybfNa4E7JqJpWzoCTSvbnFltkvRaUaUX1ybFzlUoLNY1SqT8XmIAO6nGbFrev+FvJHw4mL+4WhuQ==", "license": "MIT", "dependencies": { - "debug": "^4.4.1", - "http-errors": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", "koa-compose": "^4.1.0", - "path-to-regexp": "^8.2.0" + "path-to-regexp": "^8.3.0" }, "engines": { "node": ">= 20" + }, + "peerDependencies": { + "koa": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "koa": { + "optional": false + } + } + }, + "node_modules/@koa/router/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@leichtgewicht/ip-codec": { @@ -15735,16 +15764,6 @@ "@types/koa": "*" } }, - "node_modules/@types/koa__router": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/@types/koa__router/-/koa__router-12.0.4.tgz", - "integrity": "sha512-Y7YBbSmfXZpa/m5UGGzb7XadJIRBRnwNY9cdAojZGp65Cpe5MAP3mOZE7e3bImt8dfKS4UFcR16SLH8L/z7PBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/koa": "*" - } - }, "node_modules/@types/koa-bodyparser": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/@types/koa-bodyparser/-/koa-bodyparser-4.3.7.tgz", @@ -21479,9 +21498,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -35514,12 +35533,13 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "license": "MIT", - "engines": { - "node": ">=16" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/path-type": { diff --git a/package.json b/package.json index 2cc60c08c9d..751c67afcd1 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,6 @@ "@types/jsdom": "21.1.7", "@types/koa": "3.0.1", "@types/koa__multer": "2.0.7", - "@types/koa__router": "12.0.4", "@types/koa-bodyparser": "4.3.7", "@types/koa-json": "2.0.24", "@types/lowdb": "1.0.15", @@ -162,12 +161,12 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.506", - "@bitwarden/sdk-internal": "0.2.0-main.506", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.522", + "@bitwarden/sdk-internal": "0.2.0-main.522", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", - "@koa/router": "14.0.0", + "@koa/router": "15.2.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", "@ng-select/ng-select": "20.7.0",