1
0
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:
Stephon Brown
2025-09-30 10:30:27 -04:00
16 changed files with 386 additions and 277 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(...)", () => {

View File

@@ -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 views 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
View File

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

View File

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