1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 10:23:52 +00:00

Merge branch 'main' into beeep/dev-container

This commit is contained in:
Conner Turnbull
2026-02-05 09:59:27 -05:00
committed by GitHub
56 changed files with 765 additions and 241 deletions

4
.github/CODEOWNERS vendored
View File

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

View File

@@ -313,7 +313,6 @@
"@types/inquirer",
"@types/koa",
"@types/koa__multer",
"@types/koa__router",
"@types/koa-bodyparser",
"@types/koa-json",
"@types/lunr",

View File

@@ -6123,6 +6123,12 @@
"whyAmISeeingThis": {
"message": "Why am I seeing this?"
},
"items": {
"message": "Items"
},
"searchResults": {
"message": "Search results"
},
"resizeSideNavigation": {
"message": "Resize side navigation"
},

View File

@@ -1110,6 +1110,7 @@ export default class MainBackground {
this.logService,
this.platformUtilsService,
this.configService,
this.sdkService,
),
);

View File

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

View File

@@ -107,20 +107,32 @@
@if (vaultState === null) {
<vault-fade-in-out>
@if (!(loading$ | async)) {
<app-autofill-vault-list-items></app-autofill-vault-list-items>
<app-vault-list-items-container
[title]="'favorites' | i18n"
[ciphers]="(favoriteCiphers$ | async) || []"
id="favorites"
collapsibleKey="favorites"
></app-vault-list-items-container>
<app-vault-list-items-container
[title]="'allItems' | i18n"
[ciphers]="(remainingCiphers$ | async) || []"
id="allItems"
disableSectionMargin
collapsibleKey="allItems"
></app-vault-list-items-container>
<!--If there is search text fold all the filtered ciphers into one container-->
@if (hasSearchText$ | async) {
<app-vault-list-items-container
[title]="'searchResults' | i18n"
[ciphers]="(filteredCiphers$ | async) || []"
id="allItems"
disableSectionMargin
collapsibleKey="allItems"
></app-vault-list-items-container>
} @else {
<app-autofill-vault-list-items></app-autofill-vault-list-items>
<app-vault-list-items-container
[title]="'favorites' | i18n"
[ciphers]="(favoriteCiphers$ | async) || []"
id="favorites"
collapsibleKey="favorites"
></app-vault-list-items-container>
<!--Change the title header when a filter is applied-->
<app-vault-list-items-container
[title]="((numberOfAppliedFilters$ | async) === 0 ? 'allItems' : 'items') | i18n"
[ciphers]="(remainingCiphers$ | async) || []"
id="allItems"
disableSectionMargin
collapsibleKey="allItems"
></app-vault-list-items-container>
}
}
</vault-fade-in-out>
}

View File

@@ -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<boolean>(false),
favoriteCiphers$: new BehaviorSubject<any[]>([]),
remainingCiphers$: new BehaviorSubject<any[]>([]),
filteredCiphers$: new BehaviorSubject<any[]>([]),
cipherCount$: new BehaviorSubject<number>(0),
loading$: new BehaviorSubject<boolean>(true),
hasSearchText$: new BehaviorSubject<boolean>(false),
} as Partial<VaultPopupItemsService>;
const filtersSvc = {
const filtersSvc: any = {
allFilters$: new Subject<any>(),
filters$: new BehaviorSubject<any>({}),
filterVisibilityState$: new BehaviorSubject<any>({}),
} as Partial<VaultPopupListFiltersService>;
numberOfAppliedFilters$: new BehaviorSubject<number>(0),
};
const loadingSvc: any = {
loading$: new BehaviorSubject<boolean>(false),
};
const activeAccount$ = new BehaviorSubject<FakeAccount | null>({ 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<boolean>;
const vaultLoading$ = loadingSvc.loading$ as unknown as BehaviorSubject<boolean>;
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject<any>;
const readySubject$ = component["readySubject"] as unknown as BehaviorSubject<boolean>;
const values: boolean[] = [];
getObs<boolean>(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<boolean>;
const itemsLoading$ = itemsSvc.loading$ as unknown as BehaviorSubject<boolean>;
const vaultLoading$ = loadingSvc.loading$ as unknown as BehaviorSubject<boolean>;
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject<any>;
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<any>;
// 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<any>;
// 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<any>;
// 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<any>;
// 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<any>;
// 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();

View File

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

View File

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

View File

@@ -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<PopupCipherViewLike[]> = 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.

View File

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

View File

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

View File

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

View File

@@ -928,6 +928,7 @@ export class ServiceContainer {
this.logService,
this.platformUtilsService,
this.configService,
this.sdkService,
),
);

View File

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

View File

@@ -9,6 +9,7 @@ members = [
"napi",
"process_isolation",
"proxy",
"ssh_agent",
"windows_plugin_authenticator",
]

View File

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

View File

@@ -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<ssh_key::private::PrivateKey> for PrivateKey {
type Error = anyhow::Error;
fn try_from(key: ssh_key::private::PrivateKey) -> Result<Self, Self::Error> {
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<u8>,
}
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(_)));
}
}

View File

@@ -0,0 +1,7 @@
//! Bitwarden SSH Agent implementation
//!
//! <https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#RFC4253>
#![allow(dead_code)] // TODO remove when all code is used in follow-up PR
mod crypto;

View File

@@ -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<void> {
override async configureRouter(router: Router): Promise<void> {
// 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();

View File

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

View File

@@ -258,7 +258,7 @@ describe("DefaultOrganizationUserService", () => {
).toHaveBeenCalledWith(
mockOrganization.id,
expect.objectContaining({
userIds: mockUserIds,
ids: mockUserIds,
defaultUserCollectionName: mockEncryptedCollectionName.encryptedString,
}),
);

View File

@@ -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<Uint8Array>;
/**
* @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<Uint8Array>;
/**
* @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<Uint8Array>;
/**
* @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<Uint8Array>;
/**
* @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<Uint8Array | string>;
abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise<boolean>;
/**
* @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<Uint8Array | string>;
/**
* @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<Uint8Array | string> }
| { mode: "ecb"; parameters: EcbDecryptParameters<Uint8Array | string> }): Promise<string>;
/**
* @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<Uint8Array>;
/**
* @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<Uint8Array>;
/**
* @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(

View File

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

View File

@@ -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<PlatformUtilsService>;
let mockConfigService: ConfigService;
let mockSdkService: SdkService;
beforeEach(() => {
jest.resetAllMocks();
@@ -31,6 +33,7 @@ describe("SystemServiceProvider", () => {
mockLogger = mock<LogService>();
mockEnvironment = mock<PlatformUtilsService>();
mockConfigService = mock<ConfigService>();
mockSdkService = mock<SdkService>();
});
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);

View File

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

View File

@@ -1,6 +1,14 @@
<span class="tw-relative">
<span [ngClass]="{ 'tw-invisible': showLoadingStyle() }">
<ng-content></ng-content>
<span class="tw-relative tw-flex tw-items-center tw-justify-center">
<span [class.tw-invisible]="showLoadingStyle()" class="tw-flex tw-items-center tw-gap-2">
@if (startIcon()) {
<i class="{{ startIconClasses() }}"></i>
}
<div>
<ng-content></ng-content>
</div>
@if (endIcon()) {
<i class="{{ endIconClasses() }}"></i>
}
</span>
@if (showLoadingStyle()) {
<span class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center">

View File

@@ -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<ButtonType, string[]> = {
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<ButtonType>("secondary");
readonly startIcon = input<BitwardenIcon | undefined>(undefined);
readonly endIcon = input<BitwardenIcon | undefined>(undefined);
readonly size = input<ButtonSize>("default");
readonly block = input(false, { transform: booleanAttribute });
readonly loading = model<boolean>(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

View File

@@ -152,15 +152,13 @@ export const WithIcon: Story = {
template: /*html*/ `
<span class="tw-flex tw-gap-8">
<div>
<button type="button" bitButton [buttonType]="buttonType" [block]="block">
<i class="bwi bwi-plus tw-me-2"></i>
<button type="button" startIcon="bwi-plus" bitButton [buttonType]="buttonType" [block]="block">
Button label
</button>
</div>
<div>
<button type="button" bitButton [buttonType]="buttonType" [block]="block">
<button type="button" endIcon="bwi-plus" bitButton [buttonType]="buttonType" [block]="block">
Button label
<i class="bwi bwi-plus tw-ms-2"></i>
</button>
</div>
</span>

View File

@@ -113,7 +113,7 @@ export const WithTextButton: Story = {
template: `
<bit-callout ${formatArgsForCodeSnippet<CalloutComponent>(args)}>
<p class="tw-mb-2">The content of the callout</p>
<a bitLink> Visit the help center<i aria-hidden="true" class="bwi bwi-fw bwi-sm bwi-angle-right"></i> </a>
<a bitLink endIcon="bwi-angle-right">Visit the help center</a>
</bit-callout>
`,
}),

View File

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

View File

@@ -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<ElementRef<HTMLElement>>("skipLink");
private readonly skipLink = viewChild.required<LinkComponent>("skipLink");
handleKeydown(ev: KeyboardEvent) {
if (isNothingFocused()) {
ev.preventDefault();
this.skipLink().nativeElement.focus();
this.skipLink().el.nativeElement.focus();
}
}
}

View File

@@ -1,2 +1,2 @@
export * from "./link.directive";
export * from "./link.component";
export * from "./link.module";

View File

@@ -0,0 +1,11 @@
<div class="tw-flex tw-gap-2 tw-items-center">
@if (startIcon()) {
<i [class]="['bwi', startIcon()]" aria-hidden="true"></i>
}
<span>
<ng-content></ng-content>
</span>
@if (endIcon()) {
<i [class]="['bwi', endIcon()]" aria-hidden="true"></i>
}
</div>

View File

@@ -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<LinkType>("default");
}
/**
* Text Links and Buttons can use either the `<a>` or `<button>` tags. Choose which based on the action the button takes:
* - if navigating to a new page, use a `<a>`
* - if taking an action on the current page, use a `<button>`
* Text buttons or links are most commonly used in paragraphs of text or in forms to customize actions or show/hide additional form options.
*/
@Directive({
selector: "a[bitLink]",
@Component({
selector: "a[bitLink], button[bitLink]",
templateUrl: "./link.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
"[class]": "classList()",
// This is for us to be able to correctly aria-disable the button and capture clicks.
// It's normally added via the AriaDisableDirective as a host directive.
// But, we're not able to conditionally apply the host directive based on if this is a button or not
"[attr.bit-aria-disable]": "isButton ? true : null",
},
})
export class AnchorLinkDirective extends LinkDirective {
@HostBinding("class") get classList() {
return ["before:-tw-inset-y-[0.125rem]"]
.concat(commonStyles)
.concat(linkStyles[this.linkType()] ?? []);
}
}
@Directive({
selector: "button[bitLink]",
hostDirectives: [AriaDisableDirective],
})
export class ButtonLinkDirective extends LinkDirective {
private el = inject(ElementRef<HTMLButtonElement>);
export class LinkComponent {
readonly el = inject(ElementRef<HTMLElement>);
/**
* The variant of link you want to render
* @default "primary"
*/
readonly linkType = input<LinkType>("primary");
/**
* The leading icon to display within the link
* @default undefined
*/
readonly startIcon = input<BitwardenIcon | undefined>(undefined);
/**
* The trailing icon to display within the link
* @default undefined
*/
readonly endIcon = input<BitwardenIcon | undefined>(undefined);
/**
* Whether the button is disabled
* @default false
* @note Only applicable if the link is rendered as a button
*/
readonly disabled = input(false, { transform: booleanAttribute });
@HostBinding("class") get classList() {
return ["before:-tw-inset-y-[0.25rem]"]
protected readonly isButton = this.el.nativeElement.tagName === "BUTTON";
readonly classList = computed(() => {
return [!this.isButton && "tw-inline-flex"]
.concat(commonStyles)
.concat(linkStyles[this.linkType()] ?? []);
});
focus() {
this.el.nativeElement.focus();
}
constructor() {
super();
ariaDisableElement(this.el.nativeElement, this.disabled);
if (this.isButton) {
ariaDisableElement(this.el.nativeElement, this.disabled);
}
}
}

View File

@@ -1,9 +1,9 @@
import { NgModule } from "@angular/core";
import { AnchorLinkDirective, ButtonLinkDirective } from "./link.directive";
import { LinkComponent } from "./link.component";
@NgModule({
imports: [AnchorLinkDirective, ButtonLinkDirective],
exports: [AnchorLinkDirective, ButtonLinkDirective],
imports: [LinkComponent],
exports: [LinkComponent],
})
export class LinkModule {}

View File

@@ -2,7 +2,7 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { formatArgsForCodeSnippet } from "../../../../.storybook/format-args-for-code-snippet";
import { AnchorLinkDirective, ButtonLinkDirective, LinkTypes } from "./link.directive";
import { LinkComponent, LinkTypes } from "./link.component";
import { LinkModule } from "./link.module";
export default {
@@ -26,7 +26,7 @@ export default {
},
} as Meta;
type Story = StoryObj<ButtonLinkDirective>;
type Story = StoryObj<LinkComponent>;
export const Default: Story = {
render: (args) => ({
@@ -40,9 +40,9 @@ export const Default: Story = {
: "tw-bg-transparent",
},
template: /*html*/ `
<div class="tw-p-2" [class]="backgroundClass">
<a bitLink href="" ${formatArgsForCodeSnippet<ButtonLinkDirective>(args)}>Your text here</a>
</div>
<div class="tw-p-2" [class]="backgroundClass">
<a bitLink href="#" ${formatArgsForCodeSnippet<LinkComponent>(args)}>Your text here</a>
</div>
`,
}),
args: {
@@ -181,14 +181,12 @@ export const Buttons: Story = {
<button type="button" bitLink [linkType]="linkType">Button</button>
</div>
<div class="tw-block tw-p-2">
<button type="button" bitLink [linkType]="linkType">
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
<button type="button" bitLink [linkType]="linkType" startIcon="bwi-plus-circle">
Add Icon Button
</button>
</div>
<div class="tw-block tw-p-2">
<button type="button" bitLink [linkType]="linkType">
<i class="bwi bwi-fw bwi-sm bwi-angle-right" aria-hidden="true"></i>
<button type="button" bitLink [linkType]="linkType" endIcon="bwi-angle-right">
Chevron Icon Button
</button>
</div>
@@ -203,7 +201,7 @@ export const Buttons: Story = {
},
};
export const Anchors: StoryObj<AnchorLinkDirective> = {
export const Anchors: StoryObj<LinkComponent> = {
render: (args) => ({
props: {
linkType: args.linkType,
@@ -220,14 +218,12 @@ export const Anchors: StoryObj<AnchorLinkDirective> = {
<a bitLink [linkType]="linkType" href="#">Anchor</a>
</div>
<div class="tw-block tw-p-2">
<a bitLink [linkType]="linkType" href="#">
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
<a bitLink [linkType]="linkType" href="#" startIcon="bwi-plus-circle">
Add Icon Anchor
</a>
</div>
<div class="tw-block tw-p-2">
<a bitLink [linkType]="linkType" href="#">
<i class="bwi bwi-fw bwi-sm bwi-angle-right" aria-hidden="true"></i>
<a bitLink [linkType]="linkType" href="#" endIcon="bwi-angle-right">
Chevron Icon Anchor
</a>
</div>
@@ -247,20 +243,57 @@ export const Inline: Story = {
props: args,
template: /*html*/ `
<span class="tw-text-main">
On the internet paragraphs often contain <a bitLink href="#">inline links</a>, but few know that <button type="button" bitLink>buttons</button> can be used for similar purposes.
On the internet paragraphs often contain <a bitLink href="#">inline links with very long text that might break</a>, but few know that <button type="button" bitLink>buttons</button> can be used for similar purposes.
</span>
`,
}),
};
export const Inactive: Story = {
export const WithIcons: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<button type="button" bitLink disabled linkType="primary" class="tw-me-2">Primary</button>
<button type="button" bitLink disabled linkType="secondary" class="tw-me-2">Secondary</button>
<div class="tw-bg-bg-contrast tw-p-2 tw-inline-block">
<button type="button" bitLink disabled linkType="contrast">Contrast</button>
<div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-600': linkType === 'contrast' }">
<div class="tw-block tw-p-2">
<a bitLink [linkType]="linkType" href="#" startIcon="bwi-star">Start icon link</a>
</div>
<div class="tw-block tw-p-2">
<a bitLink [linkType]="linkType" href="#" endIcon="bwi-external-link">External link</a>
</div>
<div class="tw-block tw-p-2">
<a bitLink [linkType]="linkType" href="#" startIcon="bwi-angle-left" endIcon="bwi-angle-right">Both icons</a>
</div>
<div class="tw-block tw-p-2">
<button type="button" bitLink [linkType]="linkType" startIcon="bwi-plus-circle">Add item</button>
</div>
<div class="tw-block tw-p-2">
<button type="button" bitLink [linkType]="linkType" endIcon="bwi-angle-right">Next</button>
</div>
<div class="tw-block tw-p-2">
<button type="button" bitLink [linkType]="linkType" startIcon="bwi-download" endIcon="bwi-check">Download complete</button>
</div>
</div>
`,
}),
args: {
linkType: "primary",
},
};
export const Inactive: Story = {
render: (args) => ({
props: {
...args,
onClick: () => {
alert("Button clicked! (This should not appear when disabled)");
},
},
template: /*html*/ `
<button type="button" bitLink (click)="onClick()" disabled linkType="primary" class="tw-me-2">Primary button</button>
<a bitLink href="" disabled linkType="primary" class="tw-me-2">Links can not be inactive</a>
<button type="button" bitLink disabled linkType="secondary" class="tw-me-2">Secondary button</button>
<div class="tw-bg-primary-600 tw-p-2 tw-inline-block">
<button type="button" bitLink disabled linkType="contrast">Contrast button</button>
</div>
`,
}),

View File

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

View File

@@ -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<SystemServiceProvi
}),
safeProvider({
provide: GENERATOR_SERVICE_PROVIDER,
useFactory: (
useFactory: async (
system: SystemServiceProvider,
random: Randomizer,
encryptor: LegacyEncryptorProvider,
@@ -141,25 +139,19 @@ export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvi
now: Date.now,
} satisfies UserStateSubjectDependencyProvider;
const featureFlagObs$ = from(
system.configService.getFeatureFlag(FeatureFlag.UseSdkPasswordGenerators),
);
let featureFlag: boolean = false;
featureFlagObs$.pipe(take(1)).subscribe((ff) => (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,
};

View File

@@ -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<PasswordGenerationOptions>
{
/** 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,

View File

@@ -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<GeneratorDependencyProvider>();
nonSdkDependencyProvider.sdk = undefined;
it("returns a password randomizer", () => {
expect(effPassphrase.engine.create(nonSdkDependencyProvider)).toBeInstanceOf(
PasswordRandomizer,
);
});
});
describe("profiles[account]", () => {
let accountProfile: CoreProfileMetadata<PassphraseGenerationOptions> | null = null;
beforeEach(() => {

View File

@@ -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<PassphraseGenerationOptions> = {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<PassphraseGenerationOptions> {
if (dependencies.sdk == undefined) {
return new PasswordRandomizer(dependencies.randomizer, dependencies.now);
}
return new SdkPasswordRandomizer(dependencies.sdk, dependencies.now);
},
},

View File

@@ -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<GeneratorDependencyProvider>();
nonSdkDependencyProvider.sdk = undefined;
it("returns a password randomizer", () => {
expect(password.engine.create(nonSdkDependencyProvider)).toBeInstanceOf(PasswordRandomizer);
});
});
describe("profiles[account]", () => {
let accountProfile: CoreProfileMetadata<PasswordGenerationOptions> = null!;
beforeEach(() => {

View File

@@ -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<PasswordGeneratorSettings> = deepFreeze({
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<PasswordGeneratorSettings> {
if (dependencies.sdk == undefined) {
return new PasswordRandomizer(dependencies.randomizer, dependencies.now);
}
return new SdkPasswordRandomizer(dependencies.sdk, dependencies.now);
},
},

View File

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

View File

@@ -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<PolicyService>();
const SomeExtensionService = mock<ExtensionService>();
const SomeConfigService = mock<ConfigService>;
const SomeSdkService = mock<BitwardenClient>;
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;

View File

@@ -28,6 +28,7 @@ export type OptionalInitialValues = {
// Credit Card Information
cardholderName?: string;
number?: string;
brand?: string;
expMonth?: string;
expYear?: string;
code?: string;

View File

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

View File

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

View File

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

View File

@@ -12,9 +12,15 @@
</bit-callout>
<bit-callout *ngIf="showChangePasswordLink()" type="warning" [title]="''">
<a bitLink href="#" appStopClick (click)="launchChangePassword()" linkType="secondary">
<a
bitLink
href="#"
appStopClick
(click)="launchChangePassword()"
linkType="secondary"
endIcon="bwi-external-link"
>
{{ "changeAtRiskPassword" | i18n }}
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
</a>
</bit-callout>

View File

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

View File

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

View File

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

90
package-lock.json generated
View File

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

View File

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