mirror of
https://github.com/bitwarden/browser
synced 2026-03-01 11:01:17 +00:00
Merge remote-tracking branch 'origin' into auth/pm-23620/auth-request-answering-service
This commit is contained in:
@@ -3,8 +3,9 @@
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionDetailsResponse,
|
||||
CollectionRequest,
|
||||
CollectionResponse,
|
||||
CreateCollectionRequest,
|
||||
UpdateCollectionRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
|
||||
import { OrganizationConnectionType } from "../admin-console/enums";
|
||||
@@ -270,12 +271,12 @@ export abstract class ApiService {
|
||||
): Promise<ListResponse<CollectionAccessDetailsResponse>>;
|
||||
abstract postCollection(
|
||||
organizationId: string,
|
||||
request: CollectionRequest,
|
||||
request: CreateCollectionRequest,
|
||||
): Promise<CollectionDetailsResponse>;
|
||||
abstract putCollection(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: CollectionRequest,
|
||||
request: UpdateCollectionRequest,
|
||||
): Promise<CollectionDetailsResponse>;
|
||||
abstract deleteCollection(organizationId: string, id: string): Promise<any>;
|
||||
abstract deleteManyCollections(organizationId: string, collectionIds: string[]): Promise<any>;
|
||||
|
||||
@@ -28,7 +28,6 @@ export enum FeatureFlag {
|
||||
PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features",
|
||||
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
|
||||
UseOrganizationWarningsService = "use-organization-warnings-service",
|
||||
AllowTrialLengthZero = "pm-20322-allow-trial-length-0",
|
||||
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
|
||||
PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover",
|
||||
PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings",
|
||||
@@ -51,10 +50,10 @@ export enum FeatureFlag {
|
||||
PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy",
|
||||
PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp",
|
||||
|
||||
/* Platform */
|
||||
IpcChannelFramework = "ipc-channel-framework",
|
||||
PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@@ -93,7 +92,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
||||
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
|
||||
[FeatureFlag.PM22134SdkCipherListView]: FALSE,
|
||||
[FeatureFlag.PM19315EndUserActivationMvp]: FALSE,
|
||||
[FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
|
||||
|
||||
/* Auth */
|
||||
@@ -104,7 +102,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE,
|
||||
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
|
||||
[FeatureFlag.UseOrganizationWarningsService]: FALSE,
|
||||
[FeatureFlag.AllowTrialLengthZero]: FALSE,
|
||||
[FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE,
|
||||
[FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE,
|
||||
[FeatureFlag.PM22415_TaxIDWarnings]: FALSE,
|
||||
@@ -116,6 +113,7 @@ export const DefaultFeatureFlagValue = {
|
||||
|
||||
/* Platform */
|
||||
[FeatureFlag.IpcChannelFramework]: FALSE,
|
||||
[FeatureFlag.PushNotificationsWhenLocked]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
44
libs/common/src/models/export/cipher.export.spec.ts
Normal file
44
libs/common/src/models/export/cipher.export.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
|
||||
import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.export";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
describe("Cipher Export", () => {
|
||||
describe("toView", () => {
|
||||
it.each([[null], [undefined]])(
|
||||
"should preserve existing date values when request dates are nullish (=%p)",
|
||||
(nullishDate) => {
|
||||
const existingView = new CipherView();
|
||||
existingView.creationDate = new Date("2023-01-01T00:00:00Z");
|
||||
existingView.revisionDate = new Date("2023-01-02T00:00:00Z");
|
||||
existingView.deletedDate = new Date("2023-01-03T00:00:00Z");
|
||||
|
||||
const request = CipherExport.template();
|
||||
request.type = CipherType.SecureNote;
|
||||
request.secureNote = SecureNoteExport.template();
|
||||
request.creationDate = nullishDate as any;
|
||||
request.revisionDate = nullishDate as any;
|
||||
request.deletedDate = nullishDate as any;
|
||||
|
||||
const resultView = CipherExport.toView(request, existingView);
|
||||
expect(resultView.creationDate).toEqual(existingView.creationDate);
|
||||
expect(resultView.revisionDate).toEqual(existingView.revisionDate);
|
||||
expect(resultView.deletedDate).toEqual(existingView.deletedDate);
|
||||
},
|
||||
);
|
||||
|
||||
it("should set date values when request dates are provided", () => {
|
||||
const request = CipherExport.template();
|
||||
request.type = CipherType.SecureNote;
|
||||
request.secureNote = SecureNoteExport.template();
|
||||
request.creationDate = new Date("2023-01-01T00:00:00Z");
|
||||
request.revisionDate = new Date("2023-01-02T00:00:00Z");
|
||||
request.deletedDate = new Date("2023-01-03T00:00:00Z");
|
||||
|
||||
const resultView = CipherExport.toView(request);
|
||||
expect(resultView.creationDate).toEqual(request.creationDate);
|
||||
expect(resultView.revisionDate).toEqual(request.revisionDate);
|
||||
expect(resultView.deletedDate).toEqual(request.deletedDate);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -81,9 +81,9 @@ export class CipherExport {
|
||||
view.passwordHistory = req.passwordHistory.map((ph) => PasswordHistoryExport.toView(ph));
|
||||
}
|
||||
|
||||
view.creationDate = req.creationDate ? new Date(req.creationDate) : null;
|
||||
view.revisionDate = req.revisionDate ? new Date(req.revisionDate) : null;
|
||||
view.deletedDate = req.deletedDate ? new Date(req.deletedDate) : null;
|
||||
view.creationDate = req.creationDate ? new Date(req.creationDate) : view.creationDate;
|
||||
view.revisionDate = req.revisionDate ? new Date(req.revisionDate) : view.revisionDate;
|
||||
view.deletedDate = req.deletedDate ? new Date(req.deletedDate) : view.deletedDate;
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
/** Temporary re-export. This should not be used for new imports */
|
||||
export { KeyGenerationService } from "../../key-management/crypto/key-generation/key-generation.service";
|
||||
@@ -2,25 +2,136 @@ import { Observable, shareReplay } from "rxjs";
|
||||
|
||||
import { IpcClient, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal";
|
||||
|
||||
/**
|
||||
* Entry point for inter-process communication (IPC).
|
||||
*
|
||||
* - {@link IpcService.init} should be called in the initialization phase of the client.
|
||||
* - This service owns the underlying {@link IpcClient} lifecycle and starts it during initialization.
|
||||
*
|
||||
* ## Usage
|
||||
*
|
||||
* ### Publish / Subscribe
|
||||
* There are 2 main ways of sending and receiving messages over IPC in TypeScript:
|
||||
*
|
||||
* #### 1. TypeScript only JSON-based messages
|
||||
* This is the simplest form of IPC, where messages are sent as untyped JSON objects.
|
||||
* This is useful for simple message passing without the need for Rust code.
|
||||
*
|
||||
* ```typescript
|
||||
* // Send a message
|
||||
* await ipcService.send(OutgoingMessage.new_json_payload({ my: "data" }, "BrowserBackground", "my-topic"));
|
||||
*
|
||||
* // Receive messages
|
||||
* ipcService.messages$.subscribe((message: IncomingMessage) => {
|
||||
* if (message.topic === "my-topic") {
|
||||
* const data = incomingMessage.parse_payload_as_json();
|
||||
* console.log("Received message:", data);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* #### 2. Rust compatible messages
|
||||
* If you need to send messages that can also be handled by Rust code you can use typed Rust structs
|
||||
* together with Rust functions to send and receive messages. For more information on typed structs
|
||||
* refer to `TypedOutgoingMessage` and `TypedIncomingMessage` in the SDK.
|
||||
*
|
||||
* For examples on how to use the RPC framework with Rust see the section below.
|
||||
*
|
||||
* ### RPC (Request / Response)
|
||||
* The RPC functionality is more complex than simple message passing and requires Rust code
|
||||
* to send and receive calls. For this reason, the service also exposes the underlying
|
||||
* {@link IpcClient} so it can be passed directly into Rust code.
|
||||
*
|
||||
* #### Rust code
|
||||
* ```rust
|
||||
* #[wasm_bindgen(js_name = ipcRegisterPingHandler)]
|
||||
* pub async fn ipc_register_ping_handler(ipc_client: &JsIpcClient) {
|
||||
* ipc_client
|
||||
* .client
|
||||
* // See Rust docs for more information on how to implement a handler
|
||||
* .register_rpc_handler(PingHandler::new())
|
||||
* .await;
|
||||
* }
|
||||
*
|
||||
* #[wasm_bindgen(js_name = ipcRequestPing)]
|
||||
* pub async fn ipc_request_ping(
|
||||
* ipc_client: &JsIpcClient,
|
||||
* destination: Endpoint,
|
||||
* abort_signal: Option<AbortSignal>,
|
||||
* ) -> Result<PingResponse, RequestError> {
|
||||
* ipc_client
|
||||
* .client
|
||||
* .request(
|
||||
* PingRequest,
|
||||
* destination,
|
||||
* abort_signal.map(|c| c.to_cancellation_token()),
|
||||
* )
|
||||
* .await
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* #### TypeScript code
|
||||
* ```typescript
|
||||
* import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
* import { IpcClient, ipcRegisterPingHandler, ipcRequestPing } from "@bitwarden/sdk-internal";
|
||||
*
|
||||
* class MyService {
|
||||
* constructor(private ipcService: IpcService) {}
|
||||
*
|
||||
* async init() {
|
||||
* await ipcRegisterPingHandler(this.ipcService.client);
|
||||
* }
|
||||
*
|
||||
* async ping(destination: Endpoint): Promise<PingResponse> {
|
||||
* return await ipcRequestPing(this.ipcService.client, destination);
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export abstract class IpcService {
|
||||
private _client?: IpcClient;
|
||||
|
||||
/**
|
||||
* Access to the underlying {@link IpcClient} for advanced/Rust RPC usage.
|
||||
*
|
||||
* @throws If the service has not been initialized.
|
||||
*/
|
||||
get client(): IpcClient {
|
||||
if (!this._client) {
|
||||
throw new Error("IpcService not initialized");
|
||||
throw new Error("IpcService not initialized. Call init() first.");
|
||||
}
|
||||
return this._client;
|
||||
}
|
||||
|
||||
private _messages$?: Observable<IncomingMessage>;
|
||||
protected get messages$(): Observable<IncomingMessage> {
|
||||
|
||||
/**
|
||||
* Hot stream of {@link IncomingMessage} from the IPC layer.
|
||||
*
|
||||
* @remarks
|
||||
* - Uses `shareReplay({ bufferSize: 0, refCount: true })`, so no events are replayed to late subscribers.
|
||||
* Subscribe early if you must not miss messages.
|
||||
*
|
||||
* @throws If the service has not been initialized.
|
||||
*/
|
||||
get messages$(): Observable<IncomingMessage> {
|
||||
if (!this._messages$) {
|
||||
throw new Error("IpcService not initialized");
|
||||
throw new Error("IpcService not initialized. Call init() first.");
|
||||
}
|
||||
return this._messages$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the service and starts the IPC client.
|
||||
*/
|
||||
abstract init(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Wires the provided {@link IpcClient}, starts it, and sets up the message stream.
|
||||
*
|
||||
* - Starts the client via `client.start()`.
|
||||
* - Subscribes to the client's receive loop and exposes it through {@link messages$}.
|
||||
* - Implementations may override `init` but should call this helper exactly once.
|
||||
*/
|
||||
protected async initWithClient(client: IpcClient): Promise<void> {
|
||||
this._client = client;
|
||||
await this._client.start();
|
||||
@@ -47,6 +158,12 @@ export abstract class IpcService {
|
||||
}).pipe(shareReplay({ bufferSize: 0, refCount: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an {@link OutgoingMessage} over IPC.
|
||||
*
|
||||
* @param message The message to send.
|
||||
* @throws If the service is not initialized or the underlying client fails to send.
|
||||
*/
|
||||
async send(message: OutgoingMessage) {
|
||||
await this.client.send(message);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, Subject
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import { awaitAsync } from "../../../../spec";
|
||||
import { Matrix } from "../../../../spec/matrix";
|
||||
@@ -16,6 +16,7 @@ import { NotificationType } from "../../../enums";
|
||||
import { NotificationResponse } from "../../../models/response/notification.response";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { AppIdService } from "../../abstractions/app-id.service";
|
||||
import { ConfigService } from "../../abstractions/config/config.service";
|
||||
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { MessageSender } from "../../messaging";
|
||||
@@ -71,6 +72,14 @@ describe("NotificationsService", () => {
|
||||
authRequestAnsweringService = mock<AuthRequestAnsweringServiceAbstraction>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
// For these tests, use the active-user implementation (feature flag disabled)
|
||||
configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => {
|
||||
const flagValueByFlag: Partial<Record<FeatureFlag, boolean>> = {
|
||||
[FeatureFlag.PushNotificationsWhenLocked]: true,
|
||||
};
|
||||
return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any;
|
||||
});
|
||||
|
||||
activeAccount = new BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>(null);
|
||||
accountService.activeAccount$ = activeAccount.asObservable();
|
||||
|
||||
@@ -235,10 +244,9 @@ describe("NotificationsService", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
// Temporarily rolling back server notifications being connected while locked
|
||||
// { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked },
|
||||
// { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked },
|
||||
// { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked },
|
||||
{ initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked },
|
||||
{ initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked },
|
||||
{ initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked },
|
||||
{ initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Unlocked },
|
||||
])(
|
||||
"does not re-connect when the user transitions from $initialStatus to $updatedStatus",
|
||||
@@ -263,11 +271,7 @@ describe("NotificationsService", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
// Temporarily disabling server notifications connecting while in a locked state
|
||||
// AuthenticationStatus.Locked,
|
||||
AuthenticationStatus.Unlocked,
|
||||
])(
|
||||
it.each([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked])(
|
||||
"connects when a user transitions from logged out to %s",
|
||||
async (newStatus: AuthenticationStatus) => {
|
||||
emitActiveUser(mockUser1);
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
@@ -32,6 +31,7 @@ import {
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
||||
import { AppIdService } from "../../abstractions/app-id.service";
|
||||
import { ConfigService } from "../../abstractions/config/config.service";
|
||||
import { EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { MessagingService } from "../../abstractions/messaging.service";
|
||||
@@ -138,14 +138,25 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
||||
);
|
||||
}
|
||||
|
||||
// This method name is a lie currently as we also have an access token
|
||||
// when locked, this is eventually where we want to be but it increases load
|
||||
// on signalR so we are rolling back until we can move the load of browser to
|
||||
// web push.
|
||||
private hasAccessToken$(userId: UserId) {
|
||||
return this.authService.authStatusFor$(userId).pipe(
|
||||
map((authStatus) => authStatus === AuthenticationStatus.Unlocked),
|
||||
distinctUntilChanged(),
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PushNotificationsWhenLocked).pipe(
|
||||
switchMap((featureFlagEnabled) => {
|
||||
if (featureFlagEnabled) {
|
||||
return this.authService.authStatusFor$(userId).pipe(
|
||||
map(
|
||||
(authStatus) =>
|
||||
authStatus === AuthenticationStatus.Locked ||
|
||||
authStatus === AuthenticationStatus.Unlocked,
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
} else {
|
||||
return this.authService.authStatusFor$(userId).pipe(
|
||||
map((authStatus) => authStatus === AuthenticationStatus.Unlocked),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
/** Temporary re-export. This should not be used for new imports */
|
||||
export { DefaultKeyGenerationService as KeyGenerationService } from "../../key-management/crypto/key-generation/default-key-generation.service";
|
||||
@@ -0,0 +1,64 @@
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherRecordMapper } from "@bitwarden/common/vault/models/domain/cipher-sdk-mapper";
|
||||
import { StateClient, Repository } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { StateProvider, UserKeyDefinition } from "../../state";
|
||||
|
||||
export async function initializeState(
|
||||
userId: UserId,
|
||||
stateClient: StateClient,
|
||||
stateProvider: StateProvider,
|
||||
): Promise<void> {
|
||||
await stateClient.register_cipher_repository(
|
||||
new RepositoryRecord(userId, stateProvider, new CipherRecordMapper()),
|
||||
);
|
||||
}
|
||||
|
||||
export interface SdkRecordMapper<ClientType, SdkType> {
|
||||
userKeyDefinition(): UserKeyDefinition<Record<string, ClientType>>;
|
||||
toSdk(value: ClientType): SdkType;
|
||||
fromSdk(value: SdkType): ClientType;
|
||||
}
|
||||
|
||||
class RepositoryRecord<ClientType, SdkType> implements Repository<SdkType> {
|
||||
constructor(
|
||||
private userId: UserId,
|
||||
private stateProvider: StateProvider,
|
||||
private mapper: SdkRecordMapper<ClientType, SdkType>,
|
||||
) {}
|
||||
|
||||
async get(id: string): Promise<SdkType | null> {
|
||||
const prov = this.stateProvider.getUser(this.userId, this.mapper.userKeyDefinition());
|
||||
const data = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {})));
|
||||
const element = data[id];
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
return this.mapper.toSdk(element);
|
||||
}
|
||||
|
||||
async list(): Promise<SdkType[]> {
|
||||
const prov = this.stateProvider.getUser(this.userId, this.mapper.userKeyDefinition());
|
||||
const elements = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {})));
|
||||
return Object.values(elements).map((element) => this.mapper.toSdk(element));
|
||||
}
|
||||
|
||||
async set(id: string, value: SdkType): Promise<void> {
|
||||
const prov = this.stateProvider.getUser(this.userId, this.mapper.userKeyDefinition());
|
||||
const elements = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {})));
|
||||
elements[id] = this.mapper.fromSdk(value);
|
||||
await prov.update(() => elements);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const prov = this.stateProvider.getUser(this.userId, this.mapper.userKeyDefinition());
|
||||
const elements = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {})));
|
||||
if (!elements[id]) {
|
||||
return;
|
||||
}
|
||||
delete elements[id];
|
||||
await prov.update(() => elements);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,13 @@ import { BehaviorSubject, firstValueFrom, of } from "rxjs";
|
||||
import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { ObservableTracker } from "../../../../spec";
|
||||
import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service";
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
ObservableTracker,
|
||||
} from "../../../../spec";
|
||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { EncryptedString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { UserKey } from "../../../types/key";
|
||||
@@ -17,6 +22,7 @@ import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
|
||||
import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service";
|
||||
import { UserNotLoggedInError } from "../../abstractions/sdk/sdk.service";
|
||||
import { Rc } from "../../misc/reference-counting/rc";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
|
||||
import { DefaultSdkService } from "./default-sdk.service";
|
||||
@@ -33,10 +39,11 @@ describe("DefaultSdkService", () => {
|
||||
let sdkClientFactory!: MockProxy<SdkClientFactory>;
|
||||
let environmentService!: MockProxy<EnvironmentService>;
|
||||
let platformUtilsService!: MockProxy<PlatformUtilsService>;
|
||||
let accountService!: MockProxy<AccountService>;
|
||||
let kdfConfigService!: MockProxy<KdfConfigService>;
|
||||
let keyService!: MockProxy<KeyService>;
|
||||
let service!: DefaultSdkService;
|
||||
let accountService!: FakeAccountService;
|
||||
let fakeStateProvider!: FakeStateProvider;
|
||||
|
||||
beforeEach(async () => {
|
||||
await new TestSdkLoadService().loadAndInit();
|
||||
@@ -44,9 +51,11 @@ describe("DefaultSdkService", () => {
|
||||
sdkClientFactory = mock<SdkClientFactory>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
accountService = mock<AccountService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
keyService = mock<KeyService>();
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
fakeStateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
// Can't use `of(mock<Environment>())` for some reason
|
||||
environmentService.environment$ = new BehaviorSubject(mock<Environment>());
|
||||
@@ -58,6 +67,7 @@ describe("DefaultSdkService", () => {
|
||||
accountService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
fakeStateProvider,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -219,5 +229,9 @@ describe("DefaultSdkService", () => {
|
||||
function createMockClient(): MockProxy<BitwardenClient> {
|
||||
const client = mock<BitwardenClient>();
|
||||
client.crypto.mockReturnValue(mock());
|
||||
client.platform.mockReturnValue({
|
||||
state: jest.fn().mockReturnValue(mock()),
|
||||
free: mock(),
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service";
|
||||
import { SdkService, UserNotLoggedInError } from "../../abstractions/sdk/sdk.service";
|
||||
import { compareValues } from "../../misc/compare-values";
|
||||
import { Rc } from "../../misc/reference-counting/rc";
|
||||
import { StateProvider } from "../../state";
|
||||
|
||||
import { initializeState } from "./client-managed-state";
|
||||
|
||||
// A symbol that represents an overriden client that is explicitly set to undefined,
|
||||
// blocking the creation of an internal client for that user.
|
||||
@@ -81,6 +84,7 @@ export class DefaultSdkService implements SdkService {
|
||||
private accountService: AccountService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private keyService: KeyService,
|
||||
private stateProvider: StateProvider,
|
||||
private userAgent: string | null = null,
|
||||
) {}
|
||||
|
||||
@@ -241,6 +245,9 @@ export class DefaultSdkService implements SdkService {
|
||||
.map(([k, v]) => [k, v.key as UnsignedSharedKey]),
|
||||
),
|
||||
});
|
||||
|
||||
// Initialize the SDK managed database and the client managed repositories.
|
||||
await initializeState(userId, client.platform().state(), this.stateProvider);
|
||||
}
|
||||
|
||||
private toSettings(env: Environment): ClientSettings {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
export abstract class ActiveUserAccessor {
|
||||
/**
|
||||
* Returns a stream of the current active user for the application. The stream either emits the user id for that account
|
||||
* or returns null if there is no current active user.
|
||||
*/
|
||||
abstract activeUserId$: Observable<UserId | null>;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { DeriveDefinition } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { DerivedStateProvider } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { DerivedState } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { GlobalStateProvider } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { GlobalState } from "@bitwarden/state";
|
||||
@@ -1,36 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable, distinctUntilChanged } from "rxjs";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { ActiveUserAccessor } from "../active-user.accessor";
|
||||
import { UserKeyDefinition } from "../user-key-definition";
|
||||
import { ActiveUserState } from "../user-state";
|
||||
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
|
||||
|
||||
import { DefaultActiveUserState } from "./default-active-user-state";
|
||||
|
||||
export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
||||
activeUserId$: Observable<UserId | undefined>;
|
||||
|
||||
constructor(
|
||||
private readonly activeAccountAccessor: ActiveUserAccessor,
|
||||
private readonly singleUserStateProvider: SingleUserStateProvider,
|
||||
) {
|
||||
this.activeUserId$ = this.activeAccountAccessor.activeUserId$.pipe(
|
||||
// To avoid going to storage when we don't need to, only get updates when there is a true change.
|
||||
distinctUntilChanged((a, b) => (a == null || b == null ? a == b : a === b)), // Treat null and undefined as equal
|
||||
);
|
||||
}
|
||||
|
||||
get<T>(keyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
|
||||
// All other providers cache the creation of their corresponding `State` objects, this instance
|
||||
// doesn't need to do that since it calls `SingleUserStateProvider` it will go through their caching
|
||||
// layer, because of that, the creation of this instance is quite simple and not worth caching.
|
||||
return new DefaultActiveUserState(
|
||||
keyDefinition,
|
||||
this.activeUserId$,
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { DefaultActiveUserState } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { DefaultDerivedStateProvider } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { DefaultDerivedState } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { DefaultGlobalStateProvider } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { DefaultGlobalState } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { DefaultSingleUserStateProvider } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { DefaultSingleUserState } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { DefaultStateProvider } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { InlineDerivedState, InlineDerivedStateProvider } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { StateBase } from "@bitwarden/state";
|
||||
@@ -1 +1,6 @@
|
||||
import { StateUpdateOptions as RequiredStateUpdateOptions } from "@bitwarden/state";
|
||||
|
||||
export * from "@bitwarden/state";
|
||||
export { ActiveUserAccessor } from "@bitwarden/state-internal";
|
||||
|
||||
export type StateUpdateOptions<T, TCombine> = Partial<RequiredStateUpdateOptions<T, TCombine>>;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { KeyDefinition, KeyDefinitionOptions } from "@bitwarden/state";
|
||||
@@ -1,4 +1 @@
|
||||
export { StateDefinition } from "@bitwarden/state";
|
||||
|
||||
// To be removed once references are updated to point to @bitwarden/storage-core
|
||||
export { StorageLocation, ClientLocations } from "@bitwarden/storage-core";
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export {
|
||||
StateEventRegistrarService,
|
||||
StateEventInfo,
|
||||
STATE_LOCK_EVENT,
|
||||
STATE_LOGOUT_EVENT,
|
||||
} from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { StateEventRunnerService } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { StateProvider } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { SerializedMemoryStorageService as MemoryStorageService } from "@bitwarden/storage-core";
|
||||
@@ -1 +0,0 @@
|
||||
export { UserKeyDefinition, UserKeyDefinitionOptions } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { ActiveUserStateProvider, SingleUserStateProvider } from "@bitwarden/state";
|
||||
@@ -1 +0,0 @@
|
||||
export { ActiveUserState, SingleUserState, CombinedState } from "@bitwarden/state";
|
||||
@@ -7,8 +7,9 @@ import { firstValueFrom } from "rxjs";
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionDetailsResponse,
|
||||
CollectionRequest,
|
||||
CollectionResponse,
|
||||
CreateCollectionRequest,
|
||||
UpdateCollectionRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -727,7 +728,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
|
||||
async postCollection(
|
||||
organizationId: string,
|
||||
request: CollectionRequest,
|
||||
request: CreateCollectionRequest,
|
||||
): Promise<CollectionDetailsResponse> {
|
||||
const r = await this.send(
|
||||
"POST",
|
||||
@@ -742,7 +743,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
async putCollection(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: CollectionRequest,
|
||||
request: UpdateCollectionRequest,
|
||||
): Promise<CollectionDetailsResponse> {
|
||||
const r = await this.send(
|
||||
"PUT",
|
||||
|
||||
@@ -9,7 +9,12 @@ export abstract class SearchService {
|
||||
abstract indexedEntityId$(userId: UserId): Observable<IndexedEntityId | null>;
|
||||
|
||||
abstract clearIndex(userId: UserId): Promise<void>;
|
||||
abstract isSearchable(userId: UserId, query: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Checks if the query is long enough to be searchable.
|
||||
* For short Lunr.js queries (starts with '>'), a valid search index must exist for the user.
|
||||
*/
|
||||
abstract isSearchable(userId: UserId, query: string | null): Promise<boolean>;
|
||||
abstract indexCiphers(
|
||||
userId: UserId,
|
||||
ciphersToIndex: CipherView[],
|
||||
|
||||
22
libs/common/src/vault/models/domain/cipher-sdk-mapper.ts
Normal file
22
libs/common/src/vault/models/domain/cipher-sdk-mapper.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { SdkRecordMapper } from "@bitwarden/common/platform/services/sdk/client-managed-state";
|
||||
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
import { Cipher as SdkCipher } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { ENCRYPTED_CIPHERS } from "../../services/key-state/ciphers.state";
|
||||
import { CipherData } from "../data/cipher.data";
|
||||
|
||||
import { Cipher } from "./cipher";
|
||||
|
||||
export class CipherRecordMapper implements SdkRecordMapper<CipherData, SdkCipher> {
|
||||
userKeyDefinition(): UserKeyDefinition<Record<string, CipherData>> {
|
||||
return ENCRYPTED_CIPHERS;
|
||||
}
|
||||
|
||||
toSdk(value: CipherData): SdkCipher {
|
||||
return new Cipher(value).toSdkCipher();
|
||||
}
|
||||
|
||||
fromSdk(value: SdkCipher): CipherData {
|
||||
throw new Error("Cipher.fromSdk is not implemented yet");
|
||||
}
|
||||
}
|
||||
@@ -183,8 +183,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
]).pipe(
|
||||
filter(([ciphers, _, keys]) => ciphers != null && keys != null), // Skip if ciphers haven't been loaded yor synced yet
|
||||
switchMap(() => this.getAllDecrypted(userId)),
|
||||
tap(async (decrypted) => {
|
||||
await this.searchService.indexCiphers(userId, decrypted);
|
||||
tap(() => {
|
||||
this.messageSender.send("updateOverlayCiphers");
|
||||
}),
|
||||
);
|
||||
@@ -216,7 +215,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
if (value == null) {
|
||||
await this.searchService.clearIndex(userId);
|
||||
} else {
|
||||
await this.searchService.indexCiphers(userId, value);
|
||||
void this.searchService.indexCiphers(userId, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
97
libs/common/src/vault/services/search.service.spec.ts
Normal file
97
libs/common/src/vault/services/search.service.spec.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||
|
||||
import { SearchService } from "./search.service";
|
||||
|
||||
describe("SearchService", () => {
|
||||
let fakeStateProvider: FakeStateProvider;
|
||||
let service: SearchService;
|
||||
|
||||
const userId = "user-id" as UserId;
|
||||
const mockLogService = {
|
||||
error: jest.fn(),
|
||||
measure: jest.fn(),
|
||||
};
|
||||
const mockLocale$ = new BehaviorSubject<string>("en");
|
||||
const mockI18nService = {
|
||||
locale$: mockLocale$.asObservable(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
|
||||
service = new SearchService(
|
||||
mockLogService as unknown as LogService,
|
||||
mockI18nService as unknown as I18nService,
|
||||
fakeStateProvider,
|
||||
);
|
||||
});
|
||||
|
||||
describe("isSearchable", () => {
|
||||
let mockIndex$: jest.Mock;
|
||||
beforeEach(() => {
|
||||
mockIndex$ = jest.fn();
|
||||
service["index$"] = mockIndex$;
|
||||
});
|
||||
|
||||
it("returns false if the query is empty", async () => {
|
||||
const result = await service.isSearchable(userId, "");
|
||||
expect(result).toBe(false);
|
||||
// Ensure we do not call the expensive index$ method
|
||||
expect(mockIndex$).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns false if the query is null", async () => {
|
||||
const result = await service.isSearchable(userId, null as any);
|
||||
expect(result).toBe(false);
|
||||
// Ensure we do not call the expensive index$ method
|
||||
expect(mockIndex$).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("return true if the query is longer than searchableMinLength", async () => {
|
||||
service["searchableMinLength"] = 3;
|
||||
const result = await service.isSearchable(userId, "test");
|
||||
expect(result).toBe(true);
|
||||
// Ensure we do not call the expensive index$ method
|
||||
expect(mockIndex$).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns false if the query is shorter than searchableMinLength", async () => {
|
||||
service["searchableMinLength"] = 5;
|
||||
const result = await service.isSearchable(userId, "test");
|
||||
expect(result).toBe(false);
|
||||
// Ensure we do not call the expensive index$ method
|
||||
expect(mockIndex$).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns false for short Lunr query with missing index", async () => {
|
||||
mockIndex$.mockReturnValue(new BehaviorSubject(null));
|
||||
service["searchableMinLength"] = 3;
|
||||
const result = await service.isSearchable(userId, ">l");
|
||||
expect(result).toBe(false);
|
||||
expect(mockIndex$).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
it("returns false for long Lunr query with missing index", async () => {
|
||||
mockIndex$.mockReturnValue(new BehaviorSubject(null));
|
||||
service["searchableMinLength"] = 3;
|
||||
const result = await service.isSearchable(userId, ">longer");
|
||||
expect(result).toBe(false);
|
||||
expect(mockIndex$).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
it("returns true for short Lunr query with index", async () => {
|
||||
mockIndex$.mockReturnValue(new BehaviorSubject(true));
|
||||
service["searchableMinLength"] = 3;
|
||||
const result = await service.isSearchable(userId, ">l");
|
||||
expect(result).toBe(true);
|
||||
expect(mockIndex$).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@ import * as lunr from "lunr";
|
||||
import { Observable, firstValueFrom, map } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { perUserCache$ } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
|
||||
import { UriMatchStrategy } from "../../models/domain/domain-service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
@@ -21,6 +23,9 @@ import { CipherType } from "../enums/cipher-type";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
import { CipherViewLike, CipherViewLikeUtils } from "../utils/cipher-view-like-utils";
|
||||
|
||||
// Time to wait before performing a search after the user stops typing.
|
||||
export const SearchTextDebounceInterval = 200; // milliseconds
|
||||
|
||||
export type SerializedLunrIndex = {
|
||||
version: string;
|
||||
fields: string[];
|
||||
@@ -101,11 +106,19 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
return this.stateProvider.getUser(userId, LUNR_SEARCH_INDEX);
|
||||
}
|
||||
|
||||
private index$(userId: UserId): Observable<lunr.Index | null> {
|
||||
private index$ = perUserCache$((userId: UserId) => {
|
||||
return this.searchIndexState(userId).state$.pipe(
|
||||
map((searchIndex) => (searchIndex ? lunr.Index.load(searchIndex) : null)),
|
||||
map((searchIndex) => {
|
||||
let index: lunr.Index | null = null;
|
||||
if (searchIndex) {
|
||||
const loadTime = performance.now();
|
||||
index = lunr.Index.load(searchIndex);
|
||||
this.logService.measure(loadTime, "Vault", "SearchService", "index load");
|
||||
}
|
||||
return index;
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
private searchIndexEntityIdState(userId: UserId): SingleUserState<IndexedEntityId | null> {
|
||||
return this.stateProvider.getUser(userId, LUNR_SEARCH_INDEXED_ENTITY_ID);
|
||||
@@ -129,17 +142,22 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
await this.searchIsIndexingState(userId).update(() => null);
|
||||
}
|
||||
|
||||
async isSearchable(userId: UserId, query: string): Promise<boolean> {
|
||||
const time = performance.now();
|
||||
async isSearchable(userId: UserId, query: string | null): Promise<boolean> {
|
||||
query = SearchService.normalizeSearchQuery(query);
|
||||
const index = await this.getIndexForSearch(userId);
|
||||
const notSearchable =
|
||||
query == null ||
|
||||
(index == null && query.length < this.searchableMinLength) ||
|
||||
(index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0);
|
||||
|
||||
this.logService.measure(time, "Vault", "SearchService", "isSearchable");
|
||||
return !notSearchable;
|
||||
// Nothing to search if the query is null
|
||||
if (query == null || query === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isLunrQuery = query.indexOf(">") === 0;
|
||||
if (isLunrQuery) {
|
||||
// Lunr queries always require an index
|
||||
return (await this.getIndexForSearch(userId)) != null;
|
||||
}
|
||||
|
||||
// Regular queries only require a minimum length
|
||||
return query.length >= this.searchableMinLength;
|
||||
}
|
||||
|
||||
async indexCiphers(
|
||||
@@ -205,6 +223,7 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
ciphers: C[],
|
||||
): Promise<C[]> {
|
||||
const results: C[] = [];
|
||||
const searchStartTime = performance.now();
|
||||
if (query != null) {
|
||||
query = SearchService.normalizeSearchQuery(query.trim().toLowerCase());
|
||||
}
|
||||
@@ -236,7 +255,9 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
const index = await this.getIndexForSearch(userId);
|
||||
if (index == null) {
|
||||
// Fall back to basic search if index is not available
|
||||
return this.searchCiphersBasic(ciphers, query);
|
||||
const basicResults = this.searchCiphersBasic(ciphers, query);
|
||||
this.logService.measure(searchStartTime, "Vault", "SearchService", "basic search complete");
|
||||
return basicResults;
|
||||
}
|
||||
|
||||
const ciphersMap = new Map<string, C>();
|
||||
@@ -270,6 +291,7 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
}
|
||||
});
|
||||
}
|
||||
this.logService.measure(searchStartTime, "Vault", "SearchService", "search complete");
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -404,11 +426,30 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
if (u.uri == null || u.uri === "") {
|
||||
return;
|
||||
}
|
||||
if (u.hostname != null) {
|
||||
uris.push(u.hostname);
|
||||
return;
|
||||
}
|
||||
|
||||
// Match ports
|
||||
const portMatch = u.uri.match(/:(\d+)(?:[/?#]|$)/);
|
||||
const port = portMatch?.[1];
|
||||
|
||||
let uri = u.uri;
|
||||
|
||||
if (u.hostname !== null) {
|
||||
uris.push(u.hostname);
|
||||
if (port) {
|
||||
uris.push(`${u.hostname}:${port}`);
|
||||
uris.push(port);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
const slash = uri.indexOf("/");
|
||||
const hostPart = slash > -1 ? uri.substring(0, slash) : uri;
|
||||
uris.push(hostPart);
|
||||
if (port) {
|
||||
uris.push(`${hostPart}`);
|
||||
uris.push(port);
|
||||
}
|
||||
}
|
||||
|
||||
if (u.match !== UriMatchStrategy.RegularExpression) {
|
||||
const protocolIndex = uri.indexOf("://");
|
||||
if (protocolIndex > -1) {
|
||||
|
||||
Reference in New Issue
Block a user