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:
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
@@ -313,7 +313,6 @@
|
||||
"@types/inquirer",
|
||||
"@types/koa",
|
||||
"@types/koa__multer",
|
||||
"@types/koa__router",
|
||||
"@types/koa-bodyparser",
|
||||
"@types/koa-json",
|
||||
"@types/lunr",
|
||||
|
||||
@@ -6123,6 +6123,12 @@
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
},
|
||||
"items": {
|
||||
"message": "Items"
|
||||
},
|
||||
"searchResults": {
|
||||
"message": "Search results"
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
|
||||
@@ -1110,6 +1110,7 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
this.platformUtilsService,
|
||||
this.configService,
|
||||
this.sdkService,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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$;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -928,6 +928,7 @@ export class ServiceContainer {
|
||||
this.logService,
|
||||
this.platformUtilsService,
|
||||
this.configService,
|
||||
this.sdkService,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
16
apps/desktop/desktop_native/Cargo.lock
generated
16
apps/desktop/desktop_native/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -9,6 +9,7 @@ members = [
|
||||
"napi",
|
||||
"process_isolation",
|
||||
"proxy",
|
||||
"ssh_agent",
|
||||
"windows_plugin_authenticator",
|
||||
]
|
||||
|
||||
|
||||
19
apps/desktop/desktop_native/ssh_agent/Cargo.toml
Normal file
19
apps/desktop/desktop_native/ssh_agent/Cargo.toml
Normal 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
|
||||
184
apps/desktop/desktop_native/ssh_agent/src/crypto/mod.rs
Normal file
184
apps/desktop/desktop_native/ssh_agent/src/crypto/mod.rs
Normal 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(_)));
|
||||
}
|
||||
}
|
||||
7
apps/desktop/desktop_native/ssh_agent/src/lib.rs
Normal file
7
apps/desktop/desktop_native/ssh_agent/src/lib.rs
Normal 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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ describe("DefaultOrganizationUserService", () => {
|
||||
).toHaveBeenCalledWith(
|
||||
mockOrganization.id,
|
||||
expect.objectContaining({
|
||||
userIds: mockUserIds,
|
||||
ids: mockUserIds,
|
||||
defaultUserCollectionName: mockEncryptedCollectionName.encryptedString,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
`,
|
||||
}),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./link.directive";
|
||||
export * from "./link.component";
|
||||
export * from "./link.module";
|
||||
|
||||
11
libs/components/src/link/link.component.html
Normal file
11
libs/components/src/link/link.component.html
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
`,
|
||||
}),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,6 +28,7 @@ export type OptionalInitialValues = {
|
||||
// Credit Card Information
|
||||
cardholderName?: string;
|
||||
number?: string;
|
||||
brand?: string;
|
||||
expMonth?: string;
|
||||
expYear?: string;
|
||||
code?: string;
|
||||
|
||||
@@ -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>) {}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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
90
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user