1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 21:50:15 +00:00

Merge branch 'main' into platform/add-comments-to-renovate

This commit is contained in:
Todd Martin
2025-02-03 13:54:07 -05:00
36 changed files with 3627 additions and 6145 deletions

3
.github/CODEOWNERS vendored
View File

@@ -97,12 +97,15 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev
.github/workflows/scan.yml @bitwarden/team-platform-dev
.github/workflows/test.yml @bitwarden/team-platform-dev
.github/workflows/version-auto-bump.yml @bitwarden/team-platform-dev
# ESLint custom rules
libs/eslint @bitwarden/team-platform-dev
## Autofill team files ##
apps/browser/src/autofill @bitwarden/team-autofill-dev
apps/desktop/src/autofill @bitwarden/team-autofill-dev
libs/common/src/autofill @bitwarden/team-autofill-dev
apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev
# DuckDuckGo integration
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev

View File

@@ -220,6 +220,8 @@
"@storybook/angular",
"@storybook/manager-api",
"@storybook/theming",
"@typescript-eslint/utils",
"@typescript-eslint/rule-tester",
"@types/react",
"autoprefixer",
"bootstrap",

View File

@@ -88,7 +88,7 @@ jobs:
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
- name: Upload results to codecov.io
uses: codecov/test-results-action@9739113ad922ea0a9abb4b2c0f8bf6a4aa8ef820 # v1.0.1
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -982,8 +982,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
const queueLength = this.mutationsQueue.length;
if (!this.domQueryService.pageContainsShadowDomElements()) {
// Checking if a page contains shadowDOM elements is a heavy operation and doesn't have to be done immediately, so we can call this within an idle moment on the event loop.
requestIdleCallbackPolyfill(this.checkPageContainsShadowDom, { timeout: 500 });
this.checkPageContainsShadowDom();
}
for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) {

View File

@@ -5,3 +5,4 @@ index.node
npm-debug.log*
*.node
dist
windows_pluginauthenticator_bindings.rs

View File

@@ -410,6 +410,26 @@ dependencies = [
"serde",
]
[[package]]
name = "bindgen"
version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.8.0"
@@ -553,6 +573,15 @@ dependencies = [
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -593,6 +622,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.5.27"
@@ -1458,6 +1498,15 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.14"
@@ -2259,6 +2308,16 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
@@ -2437,6 +2496,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -3447,6 +3512,13 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-plugin-authenticator"
version = "0.0.0"
dependencies = [
"bindgen",
]
[[package]]
name = "windows-registry"
version = "0.4.0"

View File

@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["napi", "core", "proxy", "macos_provider"]
members = ["napi", "core", "proxy", "macos_provider", "windows-plugin-authenticator"]
[workspace.dependencies]
anyhow = "=1.0.94"

View File

@@ -0,0 +1,9 @@
[package]
name = "windows-plugin-authenticator"
version = "0.0.0"
edition = "2021"
license = "GPL-3.0"
publish = false
[target.'cfg(target_os = "windows")'.build-dependencies]
bindgen = "0.71.1"

View File

@@ -0,0 +1,23 @@
# windows-plugin-authenticator
This is an internal crate that's meant to be a safe abstraction layer over the generated Rust bindings for the Windows WebAuthn Plugin Authenticator API's.
You can find more information about the Windows WebAuthn API's [here](https://github.com/microsoft/webauthn).
## Building
To build this crate, set the following environment variables:
- `LIBCLANG_PATH` -> the path to the `bin` directory of your LLVM install ([more info](https://rust-lang.github.io/rust-bindgen/requirements.html?highlight=libclang_path#installing-clang))
### Bash Example
```
export LIBCLANG_PATH='C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
```
### PowerShell Example
```
$env:LIBCLANG_PATH = 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
```

View File

@@ -0,0 +1,22 @@
fn main() {
#[cfg(target_os = "windows")]
windows();
}
#[cfg(target_os = "windows")]
fn windows() {
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let bindings = bindgen::Builder::default()
.header("pluginauthenticator.hpp")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings.");
bindings
.write_to_file(format!(
"{}\\windows_pluginauthenticator_bindings.rs",
out_dir
))
.expect("Couldn't write bindings.");
}

View File

@@ -0,0 +1,231 @@
/*
Bitwarden's pluginauthenticator.hpp
Source: https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h
This is a C++ header file, so the extension has been manually
changed from `.h` to `.hpp`, so bindgen will automatically
generate the correct C++ bindings.
More Info: https://rust-lang.github.io/rust-bindgen/cpp.html
*/
/* this ALWAYS GENERATED file contains the definitions for the interfaces */
/* File created by MIDL compiler version 8.01.0628 */
/* @@MIDL_FILE_HEADING( ) */
/* verify that the <rpcndr.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCNDR_H_VERSION__
#define __REQUIRED_RPCNDR_H_VERSION__ 501
#endif
/* verify that the <rpcsal.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCSAL_H_VERSION__
#define __REQUIRED_RPCSAL_H_VERSION__ 100
#endif
#include "rpc.h"
#include "rpcndr.h"
#ifndef __RPCNDR_H_VERSION__
#error this stub requires an updated version of <rpcndr.h>
#endif /* __RPCNDR_H_VERSION__ */
#ifndef COM_NO_WINDOWS_H
#include "windows.h"
#include "ole2.h"
#endif /*COM_NO_WINDOWS_H*/
#ifndef __pluginauthenticator_h__
#define __pluginauthenticator_h__
#if defined(_MSC_VER) && (_MSC_VER >= 1020)
#pragma once
#endif
#ifndef DECLSPEC_XFGVIRT
#if defined(_CONTROL_FLOW_GUARD_XFG)
#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func))
#else
#define DECLSPEC_XFGVIRT(base, func)
#endif
#endif
/* Forward Declarations */
#ifndef __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
#define __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
typedef interface EXPERIMENTAL_IPluginAuthenticator EXPERIMENTAL_IPluginAuthenticator;
#endif /* __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ */
/* header files for imported files */
#include "oaidl.h"
#include "webauthn.h"
#ifdef __cplusplus
extern "C"{
#endif
/* interface __MIDL_itf_pluginauthenticator_0000_0000 */
/* [local] */
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST
{
HWND hWnd;
GUID transactionId;
DWORD cbRequestSignature;
/* [size_is] */ byte *pbRequestSignature;
DWORD cbEncodedRequest;
/* [size_is] */ byte *pbEncodedRequest;
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE
{
DWORD cbEncodedResponse;
/* [size_is] */ byte *pbEncodedResponse;
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
{
GUID transactionId;
DWORD cbRequestSignature;
/* [size_is] */ byte *pbRequestSignature;
} EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec;
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec;
#ifndef __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
#define __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
/* interface EXPERIMENTAL_IPluginAuthenticator */
/* [unique][version][uuid][object] */
EXTERN_C const IID IID_EXPERIMENTAL_IPluginAuthenticator;
#if defined(__cplusplus) && !defined(CINTERFACE)
MIDL_INTERFACE("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")
EXPERIMENTAL_IPluginAuthenticator : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginMakeCredential(
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginGetAssertion(
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginCancelOperation(
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0;
};
#else /* C style interface */
typedef struct EXPERIMENTAL_IPluginAuthenticatorVtbl
{
BEGIN_INTERFACE
DECLSPEC_XFGVIRT(IUnknown, QueryInterface)
HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in REFIID riid,
/* [annotation][iid_is][out] */
_COM_Outptr_ void **ppvObject);
DECLSPEC_XFGVIRT(IUnknown, AddRef)
ULONG ( STDMETHODCALLTYPE *AddRef )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
DECLSPEC_XFGVIRT(IUnknown, Release)
ULONG ( STDMETHODCALLTYPE *Release )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginMakeCredential)
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginMakeCredential )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginGetAssertion)
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginGetAssertion )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginCancelOperation)
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginCancelOperation )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request);
END_INTERFACE
} EXPERIMENTAL_IPluginAuthenticatorVtbl;
interface EXPERIMENTAL_IPluginAuthenticator
{
CONST_VTBL struct EXPERIMENTAL_IPluginAuthenticatorVtbl *lpVtbl;
};
#ifdef COBJMACROS
#define EXPERIMENTAL_IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \
( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) )
#define EXPERIMENTAL_IPluginAuthenticator_AddRef(This) \
( (This)->lpVtbl -> AddRef(This) )
#define EXPERIMENTAL_IPluginAuthenticator_Release(This) \
( (This)->lpVtbl -> Release(This) )
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginMakeCredential(This,request,response) \
( (This)->lpVtbl -> EXPERIMENTAL_PluginMakeCredential(This,request,response) )
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginGetAssertion(This,request,response) \
( (This)->lpVtbl -> EXPERIMENTAL_PluginGetAssertion(This,request,response) )
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginCancelOperation(This,request) \
( (This)->lpVtbl -> EXPERIMENTAL_PluginCancelOperation(This,request) )
#endif /* COBJMACROS */
#endif /* C style interface */
#endif /* __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ */
/* Additional Prototypes for ALL interfaces */
unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * );
unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * );
/* end of Additional Prototypes */
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,11 @@
#![cfg(target_os = "windows")]
mod pa;
pub fn get_version_number() -> u64 {
unsafe { pa::WebAuthNGetApiVersionNumber() }.into()
}
pub fn add_authenticator() {
unimplemented!();
}

View File

@@ -0,0 +1,15 @@
/*
The 'pa' (plugin authenticator) module will contain the generated
bindgen code.
The attributes below will suppress warnings from the generated code.
*/
#![cfg(target_os = "windows")]
#![allow(clippy::all)]
#![allow(warnings)]
include!(concat!(
env!("OUT_DIR"),
"/windows_pluginauthenticator_bindings.rs"
));

View File

@@ -1045,10 +1045,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
this.estimatedTax = invoice.taxAmount;
})
.catch((error) => {
const translatedMessage = this.i18nService.t(error.message);
this.toastService.showToast({
title: "",
variant: "error",
message: this.i18nService.t(error.message),
message:
!translatedMessage || translatedMessage === "" ? error.message : translatedMessage,
});
});
}

View File

@@ -294,6 +294,10 @@
</ng-template>
<ng-template #setupSelfHost>
<ng-container *ngIf="userOrg.hasReseller && resellerSeatsRemainingMessage">
<h2 bitTypography="h2" class="tw-mt-7">{{ "manageSubscription" | i18n }}</h2>
<p bitTypography="body1">{{ resellerSeatsRemainingMessage }}</p>
</ng-container>
<ng-container *ngIf="showSelfHost">
<h2 bitTypography="h2" class="tw-mt-7">
{{ "selfHostingTitleProper" | i18n }}

View File

@@ -4,13 +4,17 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, lastValueFrom, Observable, Subject } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums";
import {
OrganizationApiKeyType,
OrganizationUserStatusType,
} from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
@@ -61,12 +65,15 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
showSubscription = true;
showSelfHost = false;
organizationIsManagedByConsolidatedBillingMSP = false;
resellerSeatsRemainingMessage: string;
protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon;
protected readonly teamsStarter = ProductTierType.TeamsStarter;
private destroy$ = new Subject<void>();
private seatsRemainingMessage: string;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
@@ -79,6 +86,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
private configService: ConfigService,
private toastService: ToastService,
private billingApiService: BillingApiServiceAbstraction,
private organizationUserApiService: OrganizationUserApiService,
) {}
async ngOnInit() {
@@ -104,6 +112,28 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
}
}
}
if (this.userOrg.hasReseller) {
const allUsers = await this.organizationUserApiService.getAllUsers(this.userOrg.id);
const userCount = allUsers.data.filter((user) =>
[
OrganizationUserStatusType.Invited,
OrganizationUserStatusType.Accepted,
OrganizationUserStatusType.Confirmed,
].includes(user.status),
).length;
const remainingSeats = this.userOrg.seats - userCount;
const seatsRemaining = this.i18nService.t(
"seatsRemaining",
remainingSeats.toString(),
this.userOrg.seats.toString(),
);
this.resellerSeatsRemainingMessage = seatsRemaining;
}
}
ngOnDestroy() {

View File

@@ -10333,5 +10333,18 @@
"example": "Acme c"
}
}
},
"seatsRemaining": {
"message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.",
"placeholders": {
"remaining": {
"content": "$1",
"example": "5"
},
"total": {
"content": "$2",
"example": "10"
}
}
}
}

View File

@@ -11,6 +11,8 @@ import rxjs from "eslint-plugin-rxjs";
import angularRxjs from "eslint-plugin-rxjs-angular";
import storybook from "eslint-plugin-storybook";
import platformPlugins from "./libs/eslint/platform/index.mjs";
export default tseslint.config(
...storybook.configs["flat/recommended"],
{
@@ -28,6 +30,7 @@ export default tseslint.config(
plugins: {
rxjs: rxjs,
"rxjs-angular": angularRxjs,
"@bitwarden/platform": platformPlugins,
},
languageOptions: {
parserOptions: {
@@ -66,7 +69,7 @@ export default tseslint.config(
"@angular-eslint/no-outputs-metadata-property": 0,
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": 0,
"@bitwarden/platform/required-using": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }],
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
"@typescript-eslint/no-floating-promises": "error",

View File

@@ -30,6 +30,7 @@ module.exports = {
"<rootDir>/libs/billing/jest.config.js",
"<rootDir>/libs/common/jest.config.js",
"<rootDir>/libs/components/jest.config.js",
"<rootDir>/libs/eslint/jest.config.js",
"<rootDir>/libs/tools/export/vault-export/vault-export-core/jest.config.js",
"<rootDir>/libs/tools/generator/core/jest.config.js",
"<rootDir>/libs/tools/generator/components/jest.config.js",

View File

@@ -120,7 +120,7 @@
<div class="tw-mb-6" *ngIf="sentInitialCode">
{{ "enterVerificationCodeSentToEmail" | i18n }}
<p class="mb-0">
<p class="tw-mb-0">
<button bitLink type="button" linkType="primary" (click)="requestOTP()">
{{ "resendCode" | i18n }}
</button>

View File

@@ -3,6 +3,7 @@ import { Observable } from "rxjs";
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { UserId } from "../../../types/guid";
import { Rc } from "../../misc/reference-counting/rc";
export abstract class SdkService {
/**
@@ -27,5 +28,5 @@ export abstract class SdkService {
*
* @param userId
*/
abstract userClient$(userId: UserId): Observable<BitwardenClient | undefined>;
abstract userClient$(userId: UserId): Observable<Rc<BitwardenClient> | undefined>;
}

View File

@@ -0,0 +1,93 @@
// Temporary workaround for Symbol.dispose
// remove when https://github.com/jestjs/jest/issues/14874 is resolved and *released*
const disposeSymbol: unique symbol = Symbol("Symbol.dispose");
const asyncDisposeSymbol: unique symbol = Symbol("Symbol.asyncDispose");
(Symbol as any).asyncDispose ??= asyncDisposeSymbol as unknown as SymbolConstructor["asyncDispose"];
(Symbol as any).dispose ??= disposeSymbol as unknown as SymbolConstructor["dispose"];
// Import needs to be after the workaround
import { Rc } from "./rc";
export class FreeableTestValue {
isFreed = false;
free() {
this.isFreed = true;
}
}
describe("Rc", () => {
let value: FreeableTestValue;
let rc: Rc<FreeableTestValue>;
beforeEach(() => {
value = new FreeableTestValue();
rc = new Rc(value);
});
it("should increase refCount when taken", () => {
rc.take();
expect(rc["refCount"]).toBe(1);
});
it("should return value on take", () => {
// eslint-disable-next-line @bitwarden/platform/required-using
const reference = rc.take();
expect(reference.value).toBe(value);
});
it("should decrease refCount when disposing reference", () => {
// eslint-disable-next-line @bitwarden/platform/required-using
const reference = rc.take();
reference[Symbol.dispose]();
expect(rc["refCount"]).toBe(0);
});
it("should automatically decrease refCount when reference goes out of scope", () => {
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
using reference = rc.take();
}
expect(rc["refCount"]).toBe(0);
});
it("should not free value when refCount reaches 0 if not marked for disposal", () => {
// eslint-disable-next-line @bitwarden/platform/required-using
const reference = rc.take();
reference[Symbol.dispose]();
expect(value.isFreed).toBe(false);
});
it("should free value when refCount reaches 0 and rc is marked for disposal", () => {
// eslint-disable-next-line @bitwarden/platform/required-using
const reference = rc.take();
rc.markForDisposal();
reference[Symbol.dispose]();
expect(value.isFreed).toBe(true);
});
it("should free value when marked for disposal if refCount is 0", () => {
// eslint-disable-next-line @bitwarden/platform/required-using
const reference = rc.take();
reference[Symbol.dispose]();
rc.markForDisposal();
expect(value.isFreed).toBe(true);
});
it("should throw error when trying to take a disposed reference", () => {
rc.markForDisposal();
expect(() => rc.take()).toThrow();
});
});

View File

@@ -0,0 +1,76 @@
import { UsingRequired } from "../using-required";
export type Freeable = { free: () => void };
/**
* Reference counted disposable value.
* This class is used to manage the lifetime of a value that needs to be
* freed of at a specific time but might still be in-use when that happens.
*/
export class Rc<T extends Freeable> {
private markedForDisposal = false;
private refCount = 0;
private value: T;
constructor(value: T) {
this.value = value;
}
/**
* Use this function when you want to use the underlying object.
* This will guarantee that you have a reference to the object
* and that it won't be freed until your reference goes out of scope.
*
* This function must be used with the `using` keyword.
*
* @example
* ```typescript
* function someFunction(rc: Rc<SomeValue>) {
* using reference = rc.take();
* reference.value.doSomething();
* // reference is automatically disposed here
* }
* ```
*
* @returns The value.
*/
take(): Ref<T> {
if (this.markedForDisposal) {
throw new Error("Cannot take a reference to a value marked for disposal");
}
this.refCount++;
return new Ref(() => this.release(), this.value);
}
/**
* Mark this Rc for disposal. When the refCount reaches 0, the value
* will be freed.
*/
markForDisposal() {
this.markedForDisposal = true;
this.freeIfPossible();
}
private release() {
this.refCount--;
this.freeIfPossible();
}
private freeIfPossible() {
if (this.refCount === 0 && this.markedForDisposal) {
this.value.free();
}
}
}
export class Ref<T extends Freeable> implements UsingRequired {
constructor(
private readonly release: () => void,
readonly value: T,
) {}
[Symbol.dispose]() {
this.release();
}
}

View File

@@ -0,0 +1,11 @@
export type Disposable = { [Symbol.dispose]: () => void };
/**
* Types implementing this type must be used together with the `using` keyword
*
* @example using ref = rc.take();
*/
// We want to use `interface` here because it creates a separate type.
// Type aliasing would not expose `UsingRequired` to the linter.
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface UsingRequired extends Disposable {}

View File

@@ -10,6 +10,7 @@ import { UserKey } from "../../../types/key";
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
import { Rc } from "../../misc/reference-counting/rc";
import { EncryptedString } from "../../models/domain/enc-string";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
@@ -75,15 +76,14 @@ describe("DefaultSdkService", () => {
});
it("creates an SDK client when called the first time", async () => {
const result = await firstValueFrom(service.userClient$(userId));
await firstValueFrom(service.userClient$(userId));
expect(result).toBe(mockClient);
expect(sdkClientFactory.createSdkClient).toHaveBeenCalled();
});
it("does not create an SDK client when called the second time with same userId", async () => {
const subject_1 = new BehaviorSubject(undefined);
const subject_2 = new BehaviorSubject(undefined);
const subject_1 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
const subject_2 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
// Use subjects to ensure the subscription is kept alive
service.userClient$(userId).subscribe(subject_1);
@@ -92,14 +92,14 @@ describe("DefaultSdkService", () => {
// Wait for the next tick to ensure all async operations are done
await new Promise(process.nextTick);
expect(subject_1.value).toBe(mockClient);
expect(subject_2.value).toBe(mockClient);
expect(subject_1.value.take().value).toBe(mockClient);
expect(subject_2.value.take().value).toBe(mockClient);
expect(sdkClientFactory.createSdkClient).toHaveBeenCalledTimes(1);
});
it("destroys the SDK client when all subscriptions are closed", async () => {
const subject_1 = new BehaviorSubject(undefined);
const subject_2 = new BehaviorSubject(undefined);
const subject_1 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
const subject_2 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
const subscription_1 = service.userClient$(userId).subscribe(subject_1);
const subscription_2 = service.userClient$(userId).subscribe(subject_2);
await new Promise(process.nextTick);
@@ -107,6 +107,7 @@ describe("DefaultSdkService", () => {
subscription_1.unsubscribe();
subscription_2.unsubscribe();
await new Promise(process.nextTick);
expect(mockClient.free).toHaveBeenCalledTimes(1);
});
@@ -114,7 +115,7 @@ describe("DefaultSdkService", () => {
const userKey$ = new BehaviorSubject(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey);
keyService.userKey$.calledWith(userId).mockReturnValue(userKey$);
const subject = new BehaviorSubject(undefined);
const subject = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
service.userClient$(userId).subscribe(subject);
await new Promise(process.nextTick);

View File

@@ -30,10 +30,11 @@ import { PlatformUtilsService } from "../../abstractions/platform-utils.service"
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
import { SdkService } from "../../abstractions/sdk/sdk.service";
import { compareValues } from "../../misc/compare-values";
import { Rc } from "../../misc/reference-counting/rc";
import { EncryptedString } from "../../models/domain/enc-string";
export class DefaultSdkService implements SdkService {
private sdkClientCache = new Map<UserId, Observable<BitwardenClient>>();
private sdkClientCache = new Map<UserId, Observable<Rc<BitwardenClient>>>();
client$ = this.environmentService.environment$.pipe(
concatMap(async (env) => {
@@ -58,7 +59,7 @@ export class DefaultSdkService implements SdkService {
private userAgent: string = null,
) {}
userClient$(userId: UserId): Observable<BitwardenClient | undefined> {
userClient$(userId: UserId): Observable<Rc<BitwardenClient> | undefined> {
// TODO: Figure out what happens when the user logs out
if (this.sdkClientCache.has(userId)) {
return this.sdkClientCache.get(userId);
@@ -88,32 +89,31 @@ export class DefaultSdkService implements SdkService {
// switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value.
switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => {
// Create our own observable to be able to implement clean-up logic
return new Observable<BitwardenClient>((subscriber) => {
let client: BitwardenClient;
return new Observable<Rc<BitwardenClient>>((subscriber) => {
const createAndInitializeClient = async () => {
if (privateKey == null || userKey == null) {
return undefined;
}
const settings = this.toSettings(env);
client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info);
const client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info);
await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys);
return client;
};
let client: Rc<BitwardenClient>;
createAndInitializeClient()
.then((c) => {
client = c;
subscriber.next(c);
client = c === undefined ? undefined : new Rc(c);
subscriber.next(client);
})
.catch((e) => {
subscriber.error(e);
});
return () => client?.free();
return () => client?.markForDisposal();
});
}),
tap({

2
libs/eslint/empty.ts Normal file
View File

@@ -0,0 +1,2 @@
// This file is used to avoid TS errors. This package only uses `tsconfig.json` for dynamically generated test files but
// TS doesn't know that in the CI.

View File

@@ -0,0 +1,10 @@
const sharedConfig = require("../../libs/shared/jest.config.angular");
/** @type {import('jest').Config} */
module.exports = {
...sharedConfig,
testMatch: ["**/+(*.)+(spec).+(mjs)"],
displayName: "libs/eslint tests",
preset: "jest-preset-angular",
setupFilesAfterEnv: ["<rootDir>/test.setup.mjs"],
};

View File

@@ -0,0 +1,3 @@
import requiredUsing from "./required-using.mjs";
export default { rules: { "required-using": requiredUsing } };

View File

@@ -0,0 +1,83 @@
import { ESLintUtils } from "@typescript-eslint/utils";
export const errorMessage = "'using' keyword is required but not used";
export default {
meta: {
type: "problem",
docs: {
description: "Ensure objects implementing UsingRequired are used with the using keyword",
category: "Best Practices",
recommended: false,
},
schema: [],
},
create(context) {
const parserServices = ESLintUtils.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
// Function to check if a type implements the `UsingRequired` interface
function implementsUsingRequired(type) {
const symbol = type.getSymbol();
if (!symbol) {
return false;
}
const declarations = symbol.getDeclarations() || [];
for (const declaration of declarations) {
const heritageClauses = declaration.heritageClauses || [];
for (const clause of heritageClauses) {
if (
clause.types.some(
(typeExpression) =>
checker.typeToString(checker.getTypeAtLocation(typeExpression.expression)) ===
"UsingRequired",
)
) {
return true;
}
}
}
return false;
}
// Function to check if a function call returns a `UsingRequired`
function returnsUsingRequired(node) {
if (node.type === "CallExpression") {
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const returnType = checker.getTypeAtLocation(tsNode);
return implementsUsingRequired(returnType);
}
return false;
}
return {
VariableDeclarator(node) {
// Skip if `using` is already present
if (node.parent.type === "VariableDeclaration" && node.parent.kind === "using") {
return;
}
// Check if the initializer returns a `UsingRequired`
if (node.init && returnsUsingRequired(node.init)) {
context.report({
node,
message: errorMessage,
});
}
},
AssignmentExpression(node) {
// Check if the right-hand side returns a `UsingRequired`
if (returnsUsingRequired(node.right)) {
context.report({
node,
message: errorMessage,
});
}
},
};
},
};

View File

@@ -0,0 +1,98 @@
import { RuleTester } from "@typescript-eslint/rule-tester";
import rule, { errorMessage } from "./required-using.mjs";
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
project: [__dirname + "/../tsconfig.spec.json"],
projectService: {
allowDefaultProject: ["*.ts*"],
},
tsconfigRootDir: __dirname + "/..",
},
},
});
const setup = `
interface UsingRequired {}
class Ref implements UsingRequired {}
const rc = {
take(): Ref {
return new Ref();
},
};
`;
ruleTester.run("required-using", rule.default, {
valid: [
{
name: "Direct declaration with `using`",
code: `
${setup}
using client = rc.take();
`,
},
{
name: "Function reference with `using`",
code: `
${setup}
const t = rc.take;
using client = t();
`,
},
],
invalid: [
{
name: "Direct declaration without `using`",
code: `
${setup}
const client = rc.take();
`,
errors: [
{
message: errorMessage,
},
],
},
{
name: "Assignment without `using`",
code: `
${setup}
let client;
client = rc.take();
`,
errors: [
{
message: errorMessage,
},
],
},
{
name: "Function reference without `using`",
code: `
${setup}
const t = rc.take;
const client = t();
`,
errors: [
{
message: errorMessage,
},
],
},
{
name: "Destructuring without `using`",
code: `
${setup}
const { value } = rc.take();
`,
errors: [
{
message: errorMessage,
},
],
},
],
});

View File

@@ -0,0 +1,8 @@
/* eslint-disable no-undef */
import { clearImmediate, setImmediate } from "node:timers";
Object.defineProperties(globalThis, {
clearImmediate: { value: clearImmediate },
setImmediate: { value: setImmediate },
});

View File

@@ -0,0 +1,5 @@
{
"extends": "../shared/tsconfig",
"compilerOptions": {},
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "./tsconfig.json"
}

8881
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -78,6 +78,8 @@
"@types/proper-lockfile": "4.1.4",
"@types/retry": "0.12.5",
"@types/zxcvbn": "4.4.5",
"@typescript-eslint/rule-tester": "8.22.0",
"@typescript-eslint/utils": "8.22.0",
"@webcomponents/custom-elements": "1.6.0",
"@yao-pkg/pkg": "5.16.1",
"angular-eslint": "18.4.3",