mirror of
https://github.com/bitwarden/browser
synced 2026-02-04 10:43:47 +00:00
Merge branch 'main' into billing/PM-24996/implement-upgrade-from-free-dialog
This commit is contained in:
@@ -73,7 +73,7 @@
|
||||
"form-data": "4.0.4",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"inquirer": "8.2.6",
|
||||
"jsdom": "26.1.0",
|
||||
"jsdom": "27.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"koa": "2.16.1",
|
||||
"koa-bodyparser": "4.4.1",
|
||||
|
||||
@@ -11,6 +11,7 @@ use bitwarden_russh::{
|
||||
session_bind::SessionBindResult,
|
||||
ssh_agent::{self, SshKey},
|
||||
};
|
||||
use tracing::{error, info};
|
||||
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "unix.rs")]
|
||||
@@ -86,7 +87,7 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo, BitwardenSshKey>
|
||||
info: &peerinfo::models::PeerInfo,
|
||||
) -> bool {
|
||||
if !self.is_running() {
|
||||
println!("[BitwardenDesktopAgent] Agent is not running, but tried to call confirm");
|
||||
error!("Agent is not running, but tried to call confirm");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo, BitwardenSshKey>
|
||||
let request_data = match request_parser::parse_request(data) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
println!("[SSH Agent] Error while parsing request: {e}");
|
||||
error!(error = %e, "Error while parsing request");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -105,12 +106,12 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo, BitwardenSshKey>
|
||||
_ => None,
|
||||
};
|
||||
|
||||
println!(
|
||||
"[SSH Agent] Confirming request from application: {}, is_forwarding: {}, namespace: {}, host_key: {}",
|
||||
info!(
|
||||
is_forwarding = %info.is_forwarding(),
|
||||
namespace = ?namespace.as_ref(),
|
||||
host_key = %STANDARD.encode(info.host_key()),
|
||||
"Confirming request from application: {}",
|
||||
info.process_name(),
|
||||
info.is_forwarding(),
|
||||
namespace.clone().unwrap_or_default(),
|
||||
STANDARD.encode(info.host_key())
|
||||
);
|
||||
|
||||
let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe();
|
||||
@@ -172,7 +173,7 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo, BitwardenSshKey>
|
||||
connection_info.set_host_key(session_bind_info.host_key.clone());
|
||||
}
|
||||
SessionBindResult::SignatureFailure => {
|
||||
println!("[BitwardenDesktopAgent] Session bind failure: Signature failure");
|
||||
error!("Session bind failure: Signature failure");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +182,7 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo, BitwardenSshKey>
|
||||
impl BitwardenDesktopAgent<BitwardenSshKey> {
|
||||
pub fn stop(&self) {
|
||||
if !self.is_running() {
|
||||
println!("[BitwardenDesktopAgent] Tried to stop agent while it is not running");
|
||||
error!("Tried to stop agent while it is not running");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,7 +228,7 @@ impl BitwardenDesktopAgent<BitwardenSshKey> {
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[SSH Agent Native Module] Error while parsing key: {e}");
|
||||
error!(error=%e, "Error while parsing key");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,7 +266,7 @@ impl BitwardenDesktopAgent<BitwardenSshKey> {
|
||||
|
||||
fn get_request_id(&self) -> u32 {
|
||||
if !self.is_running() {
|
||||
println!("[BitwardenDesktopAgent] Agent is not running, but tried to get request id");
|
||||
error!("Agent is not running, but tried to get request id");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use tokio::{
|
||||
select,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, info};
|
||||
use windows::Win32::{Foundation::HANDLE, System::Pipes::GetNamedPipeClientProcessId};
|
||||
|
||||
use crate::ssh_agent::peerinfo::{self, models::PeerInfo};
|
||||
@@ -31,42 +32,38 @@ impl NamedPipeServerStream {
|
||||
pub fn new(cancellation_token: CancellationToken, is_running: Arc<AtomicBool>) -> Self {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||
tokio::spawn(async move {
|
||||
println!(
|
||||
"[SSH Agent Native Module] Creating named pipe server on {}",
|
||||
PIPE_NAME
|
||||
);
|
||||
info!("Creating named pipe server on {}", PIPE_NAME);
|
||||
let mut listener = match ServerOptions::new().create(PIPE_NAME) {
|
||||
Ok(pipe) => pipe,
|
||||
Err(err) => {
|
||||
println!("[SSH Agent Native Module] Encountered an error creating the first pipe. The system's openssh service must likely be disabled");
|
||||
println!("[SSH Agent Natvie Module] error: {}", err);
|
||||
Err(e) => {
|
||||
error!(error = %e, "Encountered an error creating the first pipe. The system's openssh service must likely be disabled");
|
||||
cancellation_token.cancel();
|
||||
is_running.store(false, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
};
|
||||
loop {
|
||||
println!("[SSH Agent Native Module] Waiting for connection");
|
||||
info!("Waiting for connection");
|
||||
select! {
|
||||
_ = cancellation_token.cancelled() => {
|
||||
println!("[SSH Agent Native Module] Cancellation token triggered, stopping named pipe server");
|
||||
info!("[SSH Agent Native Module] Cancellation token triggered, stopping named pipe server");
|
||||
break;
|
||||
}
|
||||
_ = listener.connect() => {
|
||||
println!("[SSH Agent Native Module] Incoming connection");
|
||||
info!("[SSH Agent Native Module] Incoming connection");
|
||||
let handle = HANDLE(listener.as_raw_handle());
|
||||
let mut pid = 0;
|
||||
unsafe {
|
||||
if let Err(e) = GetNamedPipeClientProcessId(handle, &mut pid) {
|
||||
println!("Error getting named pipe client process id {}", e);
|
||||
error!(error = %e, pid, "Faile to get named pipe client process id");
|
||||
continue
|
||||
}
|
||||
};
|
||||
|
||||
let peer_info = peerinfo::gather::get_peer_info(pid);
|
||||
let peer_info = match peer_info {
|
||||
Err(err) => {
|
||||
println!("Failed getting process info for pid {} {}", pid, err);
|
||||
Err(e) => {
|
||||
error!(error = %e, pid = %pid, "Failed getting process info");
|
||||
continue
|
||||
},
|
||||
Ok(info) => info,
|
||||
@@ -76,8 +73,8 @@ impl NamedPipeServerStream {
|
||||
|
||||
listener = match ServerOptions::new().create(PIPE_NAME) {
|
||||
Ok(pipe) => pipe,
|
||||
Err(err) => {
|
||||
println!("[SSH Agent Native Module] Encountered an error creating a new pipe {}", err);
|
||||
Err(e) => {
|
||||
error!(error = %e, "Encountered an error creating a new pipe");
|
||||
cancellation_token.cancel();
|
||||
is_running.store(false, Ordering::Relaxed);
|
||||
return;
|
||||
|
||||
@@ -12,6 +12,7 @@ use bitwarden_russh::ssh_agent;
|
||||
use homedir::my_home;
|
||||
use tokio::{net::UnixListener, sync::Mutex};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::ssh_agent::peercred_unix_listener_stream::PeercredUnixListenerStream;
|
||||
|
||||
@@ -36,14 +37,12 @@ impl BitwardenDesktopAgent<BitwardenSshKey> {
|
||||
let ssh_path = match std::env::var("BITWARDEN_SSH_AUTH_SOCK") {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
println!("[SSH Agent Native Module] BITWARDEN_SSH_AUTH_SOCK not set, using default path");
|
||||
info!("BITWARDEN_SSH_AUTH_SOCK not set, using default path");
|
||||
|
||||
let ssh_agent_directory = match my_home() {
|
||||
Ok(Some(home)) => home,
|
||||
_ => {
|
||||
println!(
|
||||
"[SSH Agent Native Module] Could not determine home directory"
|
||||
);
|
||||
info!("Could not determine home directory");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -65,10 +64,10 @@ impl BitwardenDesktopAgent<BitwardenSshKey> {
|
||||
}
|
||||
};
|
||||
|
||||
println!("[SSH Agent Native Module] Starting SSH Agent server on {ssh_path:?}");
|
||||
info!(socket = %ssh_path, "Starting SSH Agent server");
|
||||
let sockname = std::path::Path::new(&ssh_path);
|
||||
if let Err(e) = std::fs::remove_file(sockname) {
|
||||
println!("[SSH Agent Native Module] Could not remove existing socket file: {e}");
|
||||
error!(error = %e, socket = %ssh_path, "Could not remove existing socket file");
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
return;
|
||||
}
|
||||
@@ -79,7 +78,7 @@ impl BitwardenDesktopAgent<BitwardenSshKey> {
|
||||
// Only the current user should be able to access the socket
|
||||
if let Err(e) = fs::set_permissions(sockname, fs::Permissions::from_mode(0o600))
|
||||
{
|
||||
println!("[SSH Agent Native Module] Could not set socket permissions: {e}");
|
||||
error!(error = %e, socket = ?sockname, "Could not set socket permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,10 +99,10 @@ impl BitwardenDesktopAgent<BitwardenSshKey> {
|
||||
cloned_agent_state
|
||||
.is_running
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
println!("[SSH Agent Native Module] SSH Agent server exited");
|
||||
info!("SSH Agent server exited");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[SSH Agent Native Module] Error while starting agent server: {e}");
|
||||
error!(error = %e, socket = %ssh_path, "Unable to start start agent server");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<app-header></app-header>
|
||||
|
||||
<bit-container>
|
||||
@let organization = organization$ | async;
|
||||
@if (loading) {
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
@@ -13,18 +12,16 @@
|
||||
@if (!loading) {
|
||||
<bit-table>
|
||||
<ng-template body>
|
||||
@for (p of policies; track p.name) {
|
||||
@if (p.display$(organization, configService) | async) {
|
||||
<tr bitRow>
|
||||
<td bitCell ngPreserveWhitespaces>
|
||||
<button type="button" bitLink (click)="edit(p)">{{ p.name | i18n }}</button>
|
||||
@if (policiesEnabledMap.get(p.type)) {
|
||||
<span bitBadge variant="success">{{ "on" | i18n }}</span>
|
||||
}
|
||||
<small class="tw-text-muted tw-block">{{ p.description | i18n }}</small>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@for (p of policies$ | async; track p.type) {
|
||||
<tr bitRow>
|
||||
<td bitCell ngPreserveWhitespaces>
|
||||
<button type="button" bitLink (click)="edit(p)">{{ p.name | i18n }}</button>
|
||||
@if (policiesEnabledMap.get(p.type)) {
|
||||
<span bitBadge variant="success">{{ "on" | i18n }}</span>
|
||||
}
|
||||
<small class="tw-text-muted tw-block">{{ p.description | i18n }}</small>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
|
||||
@@ -2,8 +2,17 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom, lastValueFrom, Observable } from "rxjs";
|
||||
import { first, map } from "rxjs/operators";
|
||||
import {
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
lastValueFrom,
|
||||
Observable,
|
||||
of,
|
||||
switchMap,
|
||||
first,
|
||||
map,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import {
|
||||
getOrganizationById,
|
||||
@@ -11,7 +20,6 @@ import {
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -39,8 +47,7 @@ import { POLICY_EDIT_REGISTER } from "./policy-register-token";
|
||||
export class PoliciesComponent implements OnInit {
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
policies: readonly BasePolicyEditDefinition[];
|
||||
protected organization$: Observable<Organization>;
|
||||
policies$: Observable<BasePolicyEditDefinition[]>;
|
||||
|
||||
private orgPolicies: PolicyResponse[];
|
||||
protected policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||
@@ -63,28 +70,41 @@ export class PoliciesComponent implements OnInit {
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
this.organization$ = this.organizationService
|
||||
const organization$ = this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId));
|
||||
|
||||
this.policies = this.policyListService.getPolicies();
|
||||
this.policies$ = organization$.pipe(
|
||||
withLatestFrom(of(this.policyListService.getPolicies())),
|
||||
switchMap(([organization, policies]) => {
|
||||
return combineLatest(
|
||||
policies.map((policy) =>
|
||||
policy
|
||||
.display$(organization, this.configService)
|
||||
.pipe(map((shouldDisplay) => ({ policy, shouldDisplay }))),
|
||||
),
|
||||
);
|
||||
}),
|
||||
map((results) =>
|
||||
results.filter((result) => result.shouldDisplay).map((result) => result.policy),
|
||||
),
|
||||
);
|
||||
|
||||
await this.load();
|
||||
|
||||
// Handle policies component launch from Event message
|
||||
this.route.queryParams
|
||||
.pipe(first())
|
||||
combineLatest([this.route.queryParams.pipe(first()), this.policies$])
|
||||
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
||||
.subscribe(async (qParams) => {
|
||||
.subscribe(async ([qParams, policies]) => {
|
||||
if (qParams.policyId != null) {
|
||||
const policyIdFromEvents: string = qParams.policyId;
|
||||
for (const orgPolicy of this.orgPolicies) {
|
||||
if (orgPolicy.id === policyIdFromEvents) {
|
||||
for (let i = 0; i < this.policies.length; i++) {
|
||||
if (this.policies[i].type === orgPolicy.type) {
|
||||
for (let i = 0; i < policies.length; i++) {
|
||||
if (policies[i].type === orgPolicy.type) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.edit(this.policies[i]);
|
||||
this.edit(policies[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject, firstValueFrom, take, timeout } from "rxjs";
|
||||
|
||||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
@@ -18,7 +12,6 @@ import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { KdfConfigService, KdfType } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
PREMIUM_BANNER_REPROMPT_KEY,
|
||||
@@ -34,14 +27,9 @@ describe("VaultBannersService", () => {
|
||||
const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
|
||||
const getEmailVerified = jest.fn().mockResolvedValue(true);
|
||||
const lastSync$ = new BehaviorSubject<Date | null>(null);
|
||||
const userDecryptionOptions$ = new BehaviorSubject<UserDecryptionOptions>({
|
||||
hasMasterPassword: true,
|
||||
});
|
||||
const kdfConfig$ = new BehaviorSubject({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600000 });
|
||||
const accounts$ = new BehaviorSubject<Record<UserId, AccountInfo>>({
|
||||
[userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo,
|
||||
});
|
||||
const devices$ = new BehaviorSubject<DeviceView[]>([]);
|
||||
const pendingAuthRequests$ = new BehaviorSubject<Array<AuthRequestResponse>>([]);
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -64,32 +52,14 @@ describe("VaultBannersService", () => {
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: { isSelfHost },
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: { accounts$ },
|
||||
},
|
||||
{
|
||||
provide: KdfConfigService,
|
||||
useValue: { getKdfConfig$: () => kdfConfig$ },
|
||||
},
|
||||
{
|
||||
provide: SyncService,
|
||||
useValue: { lastSync$: () => lastSync$ },
|
||||
},
|
||||
{
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
useValue: {
|
||||
userDecryptionOptionsById$: () => userDecryptionOptions$,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DevicesServiceAbstraction,
|
||||
useValue: { getDevices$: () => devices$ },
|
||||
},
|
||||
{
|
||||
provide: AuthRequestServiceAbstraction,
|
||||
useValue: { getPendingAuthRequests$: () => pendingAuthRequests$ },
|
||||
@@ -197,45 +167,6 @@ describe("VaultBannersService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("KDFSettings", () => {
|
||||
beforeEach(async () => {
|
||||
userDecryptionOptions$.next({ hasMasterPassword: true });
|
||||
kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 });
|
||||
});
|
||||
|
||||
it("shows low KDF iteration banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner(userId)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show low KDF iteration banner if KDF type is not PBKDF2_SHA256", async () => {
|
||||
kdfConfig$.next({ kdfType: KdfType.Argon2id, iterations: 600001 });
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner(userId)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show low KDF for iterations about 600,000", async () => {
|
||||
kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 });
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner(userId)).toBe(false);
|
||||
});
|
||||
|
||||
it("dismisses low KDF iteration banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner(userId)).toBe(true);
|
||||
|
||||
await service.dismissBanner(userId, VisibleVaultBanner.KDFSettings);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner(userId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OutdatedBrowser", () => {
|
||||
beforeEach(async () => {
|
||||
// Hardcode `MSIE` in userAgent string
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take } from "rxjs";
|
||||
|
||||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -18,10 +15,8 @@ import {
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
import { PBKDF2KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management";
|
||||
|
||||
export const VisibleVaultBanner = {
|
||||
KDFSettings: "kdf-settings",
|
||||
OutdatedBrowser: "outdated-browser",
|
||||
Premium: "premium",
|
||||
VerifyEmail: "verify-email",
|
||||
@@ -64,9 +59,7 @@ export class VaultBannersService {
|
||||
private stateProvider: StateProvider,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private syncService: SyncService,
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
) {}
|
||||
|
||||
@@ -133,21 +126,6 @@ export class VaultBannersService {
|
||||
return needsVerification && !alreadyDismissed;
|
||||
}
|
||||
|
||||
/** Returns true when the low KDF iteration banner should be shown */
|
||||
async shouldShowLowKDFBanner(userId: UserId): Promise<boolean> {
|
||||
const hasLowKDF = (
|
||||
await firstValueFrom(this.userDecryptionOptionsService.userDecryptionOptionsById$(userId))
|
||||
)?.hasMasterPassword
|
||||
? await this.isLowKdfIteration(userId)
|
||||
: false;
|
||||
|
||||
const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
|
||||
VisibleVaultBanner.KDFSettings,
|
||||
);
|
||||
|
||||
return hasLowKDF && !alreadyDismissed;
|
||||
}
|
||||
|
||||
/** Dismiss the given banner and perform any respective side effects */
|
||||
async dismissBanner(userId: UserId, banner: SessionBanners): Promise<void> {
|
||||
if (banner === VisibleVaultBanner.Premium) {
|
||||
@@ -221,13 +199,4 @@ export class VaultBannersService {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async isLowKdfIteration(userId: UserId) {
|
||||
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId));
|
||||
return (
|
||||
kdfConfig != null &&
|
||||
kdfConfig.kdfType === KdfType.PBKDF2_SHA256 &&
|
||||
kdfConfig.iterations < PBKDF2KdfConfig.ITERATIONS.defaultValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,18 +25,6 @@
|
||||
</a>
|
||||
</bit-banner>
|
||||
|
||||
<bit-banner
|
||||
id="kdf-settings-banner"
|
||||
bannerType="warning"
|
||||
*ngIf="visibleBanners.includes(VisibleVaultBanner.KDFSettings)"
|
||||
(onClose)="dismissBanner(VisibleVaultBanner.KDFSettings)"
|
||||
>
|
||||
{{ "lowKDFIterationsBanner" | i18n }}
|
||||
<a bitLink linkType="secondary" routerLink="/settings/security/security-keys">
|
||||
{{ "changeKDFSettings" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
|
||||
<bit-banner
|
||||
id="pending-auth-request-banner"
|
||||
bannerType="info"
|
||||
|
||||
@@ -35,7 +35,6 @@ describe("VaultBannersComponent", () => {
|
||||
shouldShowPremiumBanner$: jest.fn((userId: UserId) => premiumBanner$),
|
||||
shouldShowUpdateBrowserBanner: jest.fn(),
|
||||
shouldShowVerifyEmailBanner: jest.fn(),
|
||||
shouldShowLowKDFBanner: jest.fn(),
|
||||
shouldShowPendingAuthRequestBanner: jest.fn((userId: UserId) =>
|
||||
Promise.resolve(pendingAuthRequest$.value),
|
||||
),
|
||||
@@ -48,7 +47,6 @@ describe("VaultBannersComponent", () => {
|
||||
messageSubject = new Subject<{ command: string }>();
|
||||
bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false);
|
||||
bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false);
|
||||
bannerService.shouldShowLowKDFBanner.mockResolvedValue(false);
|
||||
pendingAuthRequest$.next(false);
|
||||
premiumBanner$.next(false);
|
||||
|
||||
@@ -137,11 +135,6 @@ describe("VaultBannersComponent", () => {
|
||||
method: bannerService.shouldShowVerifyEmailBanner,
|
||||
banner: VisibleVaultBanner.VerifyEmail,
|
||||
},
|
||||
{
|
||||
name: "LowKDF",
|
||||
method: bannerService.shouldShowLowKDFBanner,
|
||||
banner: VisibleVaultBanner.KDFSettings,
|
||||
},
|
||||
].forEach(({ name, method, banner }) => {
|
||||
describe(name, () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -100,14 +100,12 @@ export class VaultBannersComponent implements OnInit {
|
||||
const showBrowserOutdated =
|
||||
await this.vaultBannerService.shouldShowUpdateBrowserBanner(activeUserId);
|
||||
const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner(activeUserId);
|
||||
const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner(activeUserId);
|
||||
const showPendingAuthRequest =
|
||||
await this.vaultBannerService.shouldShowPendingAuthRequestBanner(activeUserId);
|
||||
|
||||
this.visibleBanners = [
|
||||
showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null,
|
||||
showVerifyEmail ? VisibleVaultBanner.VerifyEmail : null,
|
||||
showLowKdf ? VisibleVaultBanner.KDFSettings : null,
|
||||
showPendingAuthRequest ? VisibleVaultBanner.PendingAuthRequest : null,
|
||||
].filter((banner) => banner !== null);
|
||||
}
|
||||
|
||||
@@ -8408,12 +8408,6 @@
|
||||
"groupSlashUser": {
|
||||
"message": "Group/User"
|
||||
},
|
||||
"lowKdfIterations": {
|
||||
"message": "Low KDF Iterations"
|
||||
},
|
||||
"updateLowKdfIterationsDesc": {
|
||||
"message": "Update your encryption settings to meet new security recommendations and improve account protection."
|
||||
},
|
||||
"kdfSettingsChangeLogoutWarning": {
|
||||
"message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss."
|
||||
},
|
||||
@@ -10031,12 +10025,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lowKDFIterationsBanner": {
|
||||
"message": "Low KDF iterations. Increase your iterations to improve the security of your account."
|
||||
},
|
||||
"changeKDFSettings": {
|
||||
"message": "Change KDF settings"
|
||||
},
|
||||
"secureYourInfrastructure": {
|
||||
"message": "Secure your infrastructure"
|
||||
},
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("Utils Service", () => {
|
||||
expect(b64String).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("should return an empty string for an empty ArrayBuffer", () => {
|
||||
runInBothEnvironments("should return empty string for an empty ArrayBuffer", () => {
|
||||
const buffer = new Uint8Array([]).buffer;
|
||||
const b64String = Utils.fromBufferToB64(buffer);
|
||||
expect(b64String).toBe("");
|
||||
@@ -312,6 +312,81 @@ describe("Utils Service", () => {
|
||||
const b64String = Utils.fromBufferToB64(null);
|
||||
expect(b64String).toBeNull();
|
||||
});
|
||||
|
||||
runInBothEnvironments("returns null for undefined input", () => {
|
||||
const b64 = Utils.fromBufferToB64(undefined as unknown as ArrayBuffer);
|
||||
expect(b64).toBeNull();
|
||||
});
|
||||
|
||||
runInBothEnvironments("returns empty string for empty input", () => {
|
||||
const b64 = Utils.fromBufferToB64(new ArrayBuffer(0));
|
||||
expect(b64).toBe("");
|
||||
});
|
||||
|
||||
runInBothEnvironments("accepts Uint8Array directly", () => {
|
||||
const u8 = new Uint8Array(asciiHelloWorldArray);
|
||||
const b64 = Utils.fromBufferToB64(u8);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("respects byteOffset/byteLength (view window)", () => {
|
||||
// [xx, 'hello world', yy] — view should only encode the middle slice
|
||||
const prefix = [1, 2, 3];
|
||||
const suffix = [4, 5];
|
||||
const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]);
|
||||
const view = new Uint8Array(all.buffer, prefix.length, asciiHelloWorldArray.length);
|
||||
const b64 = Utils.fromBufferToB64(view);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("handles DataView (ArrayBufferView other than Uint8Array)", () => {
|
||||
const u8 = new Uint8Array(asciiHelloWorldArray);
|
||||
const dv = new DataView(u8.buffer, 0, u8.byteLength);
|
||||
const b64 = Utils.fromBufferToB64(dv);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("handles DataView with offset/length window", () => {
|
||||
// Buffer: [xx, 'hello world', yy]
|
||||
const prefix = [9, 9, 9];
|
||||
const suffix = [8, 8];
|
||||
const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]);
|
||||
|
||||
// DataView over just the "hello world" window
|
||||
const dv = new DataView(all.buffer, prefix.length, asciiHelloWorldArray.length);
|
||||
|
||||
const b64 = Utils.fromBufferToB64(dv);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments(
|
||||
"encodes empty view (offset-length window of zero) as empty string",
|
||||
() => {
|
||||
const backing = new Uint8Array([1, 2, 3, 4]);
|
||||
const emptyView = new Uint8Array(backing.buffer, 2, 0);
|
||||
const b64 = Utils.fromBufferToB64(emptyView);
|
||||
expect(b64).toBe("");
|
||||
},
|
||||
);
|
||||
|
||||
runInBothEnvironments("does not mutate the input", () => {
|
||||
const original = new Uint8Array(asciiHelloWorldArray);
|
||||
const copyBefore = new Uint8Array(original); // snapshot
|
||||
Utils.fromBufferToB64(original);
|
||||
expect(original).toEqual(copyBefore); // unchanged
|
||||
});
|
||||
|
||||
it("produces the same Base64 in Node vs non-Node mode", () => {
|
||||
const bytes = new Uint8Array(asciiHelloWorldArray);
|
||||
|
||||
Utils.isNode = true;
|
||||
const nodeB64 = Utils.fromBufferToB64(bytes);
|
||||
|
||||
Utils.isNode = false;
|
||||
const browserB64 = Utils.fromBufferToB64(bytes);
|
||||
|
||||
expect(browserB64).toBe(nodeB64);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromB64ToArray(...)", () => {
|
||||
|
||||
@@ -128,15 +128,52 @@ export class Utils {
|
||||
return arr;
|
||||
}
|
||||
|
||||
static fromBufferToB64(buffer: ArrayBuffer): string {
|
||||
/**
|
||||
* Convert binary data into a Base64 string.
|
||||
*
|
||||
* Overloads are provided for two categories of input:
|
||||
*
|
||||
* 1. ArrayBuffer
|
||||
* - A raw, fixed-length chunk of memory (no element semantics).
|
||||
* - Example: `const buf = new ArrayBuffer(16);`
|
||||
*
|
||||
* 2. ArrayBufferView
|
||||
* - A *view* onto an existing buffer that gives the bytes meaning.
|
||||
* - Examples: Uint8Array, Int32Array, DataView, etc.
|
||||
* - Views can expose only a *window* of the underlying buffer via
|
||||
* `byteOffset` and `byteLength`.
|
||||
* Example:
|
||||
* ```ts
|
||||
* const buf = new ArrayBuffer(8);
|
||||
* const full = new Uint8Array(buf); // sees all 8 bytes
|
||||
* const half = new Uint8Array(buf, 4, 4); // sees only last 4 bytes
|
||||
* ```
|
||||
*
|
||||
* Returns:
|
||||
* - Base64 string for non-empty inputs,
|
||||
* - null if `buffer` is `null` or `undefined`
|
||||
* - empty string if `buffer` is empty (0 bytes)
|
||||
*/
|
||||
static fromBufferToB64(buffer: null | undefined): null;
|
||||
static fromBufferToB64(buffer: ArrayBuffer): string;
|
||||
static fromBufferToB64(buffer: ArrayBufferView): string;
|
||||
static fromBufferToB64(buffer: ArrayBuffer | ArrayBufferView | null | undefined): string | null {
|
||||
// Handle null / undefined input
|
||||
if (buffer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bytes: Uint8Array = Utils.normalizeToUint8Array(buffer);
|
||||
|
||||
// Handle empty input
|
||||
if (bytes.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (Utils.isNode) {
|
||||
return Buffer.from(buffer).toString("base64");
|
||||
return Buffer.from(bytes).toString("base64");
|
||||
} else {
|
||||
let binary = "";
|
||||
const bytes = new Uint8Array(buffer);
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
@@ -144,6 +181,30 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes input into a Uint8Array so we always have a uniform,
|
||||
* byte-level view of the data. This avoids dealing with differences
|
||||
* between ArrayBuffer (raw memory with no indexing) and other typed
|
||||
* views (which may have element sizes, offsets, and lengths).
|
||||
* @param buffer ArrayBuffer or ArrayBufferView (e.g. Uint8Array, DataView, etc.)
|
||||
*/
|
||||
private static normalizeToUint8Array(buffer: ArrayBuffer | ArrayBufferView): Uint8Array {
|
||||
/**
|
||||
* 1) Uint8Array: already bytes → use directly.
|
||||
* 2) ArrayBuffer: wrap whole buffer.
|
||||
* 3) Other ArrayBufferView (e.g., DataView, Int32Array):
|
||||
* wrap the view’s window (byteOffset..byteOffset+byteLength).
|
||||
*/
|
||||
if (buffer instanceof Uint8Array) {
|
||||
return buffer;
|
||||
} else if (buffer instanceof ArrayBuffer) {
|
||||
return new Uint8Array(buffer);
|
||||
} else {
|
||||
const view = buffer as ArrayBufferView;
|
||||
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
||||
}
|
||||
}
|
||||
|
||||
static fromBufferToUrlB64(buffer: ArrayBuffer): string {
|
||||
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
|
||||
}
|
||||
|
||||
242
package-lock.json
generated
242
package-lock.json
generated
@@ -45,7 +45,7 @@
|
||||
"form-data": "4.0.4",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"inquirer": "8.2.6",
|
||||
"jsdom": "26.1.0",
|
||||
"jsdom": "27.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"koa": "2.16.1",
|
||||
"koa-bodyparser": "4.4.1",
|
||||
@@ -208,7 +208,7 @@
|
||||
"form-data": "4.0.4",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"inquirer": "8.2.6",
|
||||
"jsdom": "26.1.0",
|
||||
"jsdom": "27.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"koa": "2.16.1",
|
||||
"koa-bodyparser": "4.4.1",
|
||||
@@ -2586,23 +2586,54 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz",
|
||||
"integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.3",
|
||||
"@csstools/css-color-parser": "^3.0.9",
|
||||
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||
"@csstools/css-tokenizer": "^3.0.3",
|
||||
"lru-cache": "^10.4.3"
|
||||
"@csstools/css-calc": "^2.1.4",
|
||||
"@csstools/css-color-parser": "^3.1.0",
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4",
|
||||
"lru-cache": "^11.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"license": "ISC"
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
|
||||
"integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "6.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.5.6.tgz",
|
||||
"integrity": "sha512-Mj3Hu9ymlsERd7WOsUKNUZnJYL4IZ/I9wVVYgtvOsWYiEFbkQ4G7VRIh2USxTVW4BBDIsLG+gBUgqOqf2Kvqow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/nwsapi": "^2.3.9",
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.1.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
|
||||
"integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/nwsapi": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
@@ -5378,9 +5409,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
|
||||
"integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -5420,9 +5451,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
|
||||
"integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -5435,7 +5466,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^5.0.2",
|
||||
"@csstools/color-helpers": "^5.1.0",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5468,6 +5499,28 @@
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz",
|
||||
"integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||
@@ -16919,6 +16972,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/big-integer": {
|
||||
"version": "1.6.52",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
||||
@@ -19109,6 +19171,19 @@
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
|
||||
"integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.12.2",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
@@ -19150,16 +19225,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.5.0.tgz",
|
||||
"integrity": "sha512-/7gw8TGrvH/0g564EnhgFZogTMVe+lifpB7LWU+PEsiq5o83TUXR3fDbzTRXOJhoJwck5IS9ez3Em5LNMMO2aw==",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz",
|
||||
"integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^3.2.0",
|
||||
"rrweb-cssom": "^0.8.0"
|
||||
"@asamuzakjp/css-color": "^4.0.3",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.0.14",
|
||||
"css-tree": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
@@ -19183,16 +19259,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
|
||||
"integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.0.0"
|
||||
"whatwg-url": "^15.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
@@ -26910,34 +26986,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "26.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"version": "27.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz",
|
||||
"integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
"@asamuzakjp/dom-selector": "^6.5.4",
|
||||
"cssstyle": "^5.3.0",
|
||||
"data-urls": "^6.0.0",
|
||||
"decimal.js": "^10.5.0",
|
||||
"html-encoding-sniffer": "^4.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"nwsapi": "^2.2.16",
|
||||
"parse5": "^7.2.1",
|
||||
"parse5": "^7.3.0",
|
||||
"rrweb-cssom": "^0.8.0",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^5.1.1",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^7.0.0",
|
||||
"webidl-conversions": "^8.0.0",
|
||||
"whatwg-encoding": "^3.1.1",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.1.1",
|
||||
"ws": "^8.18.0",
|
||||
"whatwg-url": "^15.0.0",
|
||||
"ws": "^8.18.2",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.0.0"
|
||||
@@ -26949,35 +27025,38 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/tldts": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||
"version": "7.0.16",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.16.tgz",
|
||||
"integrity": "sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.86"
|
||||
"tldts-core": "^7.0.16"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/tldts-core": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom/node_modules/tough-cookie": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
|
||||
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^6.1.32"
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/webidl-conversions": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
|
||||
"integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -28937,6 +29016,12 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
|
||||
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
@@ -30268,7 +30353,6 @@
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -31424,6 +31508,7 @@
|
||||
"version": "2.2.20",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
|
||||
"integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nx": {
|
||||
@@ -33413,7 +33498,6 @@
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -34711,7 +34795,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -36071,7 +36154,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -37597,9 +37679,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.9.tgz",
|
||||
"integrity": "sha512-/FGY1+CryHsxF9SFiPZlMOcwQsfABkAvOJO5VEKE8TNifVEqgMF7+UVXHGhm1z4gPUfvVS/EYcwhiRU3vUa1ag==",
|
||||
"version": "7.0.16",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz",
|
||||
"integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
@@ -37673,15 +37755,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
||||
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-dump": {
|
||||
@@ -39867,6 +39949,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -40807,16 +40890,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"version": "15.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
|
||||
"integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url/node_modules/webidl-conversions": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
|
||||
"integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
"form-data": "4.0.4",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"inquirer": "8.2.6",
|
||||
"jsdom": "26.1.0",
|
||||
"jsdom": "27.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"koa": "2.16.1",
|
||||
"koa-bodyparser": "4.4.1",
|
||||
|
||||
Reference in New Issue
Block a user