mirror of
https://github.com/bitwarden/browser
synced 2026-02-28 02:23:25 +00:00
Merge branch 'main' into SM-1301-getbyidsevent
This commit is contained in:
@@ -1,119 +1 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
StorageUpdate,
|
||||
} from "../src/platform/abstractions/storage.service";
|
||||
import { StorageOptions } from "../src/platform/models/domain/storage-options";
|
||||
|
||||
const INTERNAL_KEY = "__internal__";
|
||||
|
||||
export class FakeStorageService implements AbstractStorageService, ObservableStorageService {
|
||||
private store: Record<string, unknown>;
|
||||
private updatesSubject = new Subject<StorageUpdate>();
|
||||
private _valuesRequireDeserialization = false;
|
||||
|
||||
/**
|
||||
* Returns a mock of a {@see AbstractStorageService} for asserting the expected
|
||||
* amount of calls. It is not recommended to use this to mock implementations as
|
||||
* they are not respected.
|
||||
*/
|
||||
mock: MockProxy<AbstractStorageService>;
|
||||
|
||||
constructor(initial?: Record<string, unknown>) {
|
||||
this.store = initial ?? {};
|
||||
this.mock = mock<AbstractStorageService>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal store for this fake implementation, this bypasses any mock calls
|
||||
* or updates to the {@link updates$} observable.
|
||||
* @param store
|
||||
*/
|
||||
internalUpdateStore(store: Record<string, unknown>) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
get internalStore() {
|
||||
return this.store;
|
||||
}
|
||||
|
||||
internalUpdateValuesRequireDeserialization(value: boolean) {
|
||||
this._valuesRequireDeserialization = value;
|
||||
}
|
||||
|
||||
get valuesRequireDeserialization(): boolean {
|
||||
return this._valuesRequireDeserialization;
|
||||
}
|
||||
|
||||
get updates$() {
|
||||
return this.updatesSubject.asObservable();
|
||||
}
|
||||
|
||||
get<T>(key: string, options?: StorageOptions): Promise<T> {
|
||||
// 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.mock.get(key, options);
|
||||
const value = this.store[key] as T;
|
||||
return Promise.resolve(value);
|
||||
}
|
||||
has(key: string, options?: StorageOptions): Promise<boolean> {
|
||||
// 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.mock.has(key, options);
|
||||
return Promise.resolve(this.store[key] != null);
|
||||
}
|
||||
async save<T>(key: string, obj: T, options?: StorageOptions): Promise<void> {
|
||||
// These exceptions are copied from https://github.com/sindresorhus/conf/blob/608adb0c46fb1680ddbd9833043478367a64c120/source/index.ts#L193-L203
|
||||
// which is a library that is used by `ElectronStorageService`. We add them here to ensure that the behavior in our testing mirrors the real world.
|
||||
if (typeof key !== "string" && typeof key !== "object") {
|
||||
throw new TypeError(
|
||||
`Expected \`key\` to be of type \`string\` or \`object\`, got ${typeof key}`,
|
||||
);
|
||||
}
|
||||
|
||||
// We don't throw this error because ElectronStorageService automatically detects this case
|
||||
// and calls `delete()` instead of `set()`.
|
||||
// if (typeof key !== "object" && obj === undefined) {
|
||||
// throw new TypeError("Use `delete()` to clear values");
|
||||
// }
|
||||
|
||||
if (this._containsReservedKey(key)) {
|
||||
throw new TypeError(
|
||||
`Please don't use the ${INTERNAL_KEY} key, as it's used to manage this module internal operations.`,
|
||||
);
|
||||
}
|
||||
|
||||
// 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.mock.save(key, obj, options);
|
||||
this.store[key] = obj;
|
||||
this.updatesSubject.next({ key: key, updateType: "save" });
|
||||
}
|
||||
remove(key: string, options?: StorageOptions): Promise<void> {
|
||||
// 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.mock.remove(key, options);
|
||||
delete this.store[key];
|
||||
this.updatesSubject.next({ key: key, updateType: "remove" });
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private _containsReservedKey(key: string | Partial<unknown>): boolean {
|
||||
if (typeof key === "object") {
|
||||
const firsKey = Object.keys(key)[0];
|
||||
|
||||
if (firsKey === INTERNAL_KEY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export { FakeStorageService } from "@bitwarden/storage-test-utils";
|
||||
|
||||
@@ -18,6 +18,7 @@ export class AuthRequestResponse extends BaseResponse {
|
||||
responseDate?: string;
|
||||
isAnswered: boolean;
|
||||
isExpired: boolean;
|
||||
deviceId?: string; // could be null or empty
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -33,6 +34,7 @@ export class AuthRequestResponse extends BaseResponse {
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.requestApproved = this.getResponseProperty("RequestApproved");
|
||||
this.responseDate = this.getResponseProperty("ResponseDate");
|
||||
this.deviceId = this.getResponseProperty("RequestDeviceId");
|
||||
|
||||
const requestDate = new Date(this.creationDate);
|
||||
const requestDateUTC = Date.UTC(
|
||||
|
||||
@@ -12,13 +12,13 @@ import { ServerConfig } from "../platform/abstractions/config/server-config";
|
||||
export enum FeatureFlag {
|
||||
/* Admin Console Team */
|
||||
SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions",
|
||||
OptimizeNestedTraverseTypescript = "pm-21695-optimize-nested-traverse-typescript",
|
||||
CreateDefaultLocation = "pm-19467-create-default-location",
|
||||
|
||||
/* Auth */
|
||||
PM16117_SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor",
|
||||
PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor",
|
||||
PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence",
|
||||
PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals",
|
||||
|
||||
/* Autofill */
|
||||
BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain",
|
||||
@@ -45,7 +45,6 @@ export enum FeatureFlag {
|
||||
EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation",
|
||||
|
||||
/* Tools */
|
||||
ItemShare = "item-share",
|
||||
DesktopSendUIRefresh = "desktop-send-ui-refresh",
|
||||
|
||||
/* Vault */
|
||||
@@ -77,7 +76,6 @@ const FALSE = false as boolean;
|
||||
export const DefaultFeatureFlagValue = {
|
||||
/* Admin Console Team */
|
||||
[FeatureFlag.SeparateCustomRolePermissions]: FALSE,
|
||||
[FeatureFlag.OptimizeNestedTraverseTypescript]: FALSE,
|
||||
[FeatureFlag.CreateDefaultLocation]: FALSE,
|
||||
|
||||
/* Autofill */
|
||||
@@ -91,7 +89,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.EnableRiskInsightsNotifications]: FALSE,
|
||||
|
||||
/* Tools */
|
||||
[FeatureFlag.ItemShare]: FALSE,
|
||||
[FeatureFlag.DesktopSendUIRefresh]: FALSE,
|
||||
|
||||
/* Vault */
|
||||
@@ -107,6 +104,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
|
||||
[FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE,
|
||||
[FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE,
|
||||
[FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE,
|
||||
|
||||
/* Billing */
|
||||
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
||||
|
||||
@@ -14,7 +14,6 @@ import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
|
||||
import { SearchService } from "../../../abstractions/search.service";
|
||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
@@ -28,6 +27,7 @@ import { StateEventRunnerService } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { FolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SearchService } from "../../../vault/abstractions/search.service";
|
||||
import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service";
|
||||
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
|
||||
import { VaultTimeout, VaultTimeoutStringType } from "../types/vault-timeout.type";
|
||||
|
||||
@@ -12,7 +12,6 @@ import { LogoutReason } from "@bitwarden/auth/common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { SearchService } from "../../../abstractions/search.service";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
@@ -25,6 +24,7 @@ import { StateEventRunnerService } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { FolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SearchService } from "../../../vault/abstractions/search.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "../abstractions/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../abstractions/vault-timeout.service";
|
||||
|
||||
@@ -124,9 +124,9 @@ export class CipherExport {
|
||||
domain.passwordHistory = req.passwordHistory.map((ph) => PasswordHistoryExport.toDomain(ph));
|
||||
}
|
||||
|
||||
domain.creationDate = req.creationDate;
|
||||
domain.revisionDate = req.revisionDate;
|
||||
domain.deletedDate = req.deletedDate;
|
||||
domain.creationDate = req.creationDate ? new Date(req.creationDate) : null;
|
||||
domain.revisionDate = req.revisionDate ? new Date(req.revisionDate) : null;
|
||||
domain.deletedDate = req.deletedDate ? new Date(req.deletedDate) : null;
|
||||
return domain;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IpcClient, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-inte
|
||||
|
||||
export abstract class IpcService {
|
||||
private _client?: IpcClient;
|
||||
protected get client(): IpcClient {
|
||||
get client(): IpcClient {
|
||||
if (!this._client) {
|
||||
throw new Error("IpcService not initialized");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import {
|
||||
awaitAsync,
|
||||
FakeGlobalState,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
} from "../../../../spec";
|
||||
import { PushTechnology } from "../../../enums/push-technology.enum";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { ConfigService } from "../../abstractions/config/config.service";
|
||||
import { ServerConfig } from "../../abstractions/config/server-config";
|
||||
import { Supported } from "../../misc/support-status";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||
import { PushSettingsConfigResponse } from "../../models/response/server-config.response";
|
||||
import { KeyDefinition } from "../../state";
|
||||
|
||||
import { WebPushNotificationsApiService } from "./web-push-notifications-api.service";
|
||||
import { WebPushConnector } from "./webpush-connection.service";
|
||||
import {
|
||||
WEB_PUSH_SUBSCRIPTION_USERS,
|
||||
WorkerWebPushConnectionService,
|
||||
} from "./worker-webpush-connection.service";
|
||||
|
||||
const mockUser1 = "testUser1" as UserId;
|
||||
|
||||
const createSub = (key: string) => {
|
||||
return {
|
||||
options: { applicationServerKey: Utils.fromUrlB64ToArray(key), userVisibleOnly: true },
|
||||
endpoint: `web.push.endpoint/?${Utils.newGuid()}`,
|
||||
expirationTime: 5,
|
||||
getKey: () => null,
|
||||
toJSON: () => ({ endpoint: "something", keys: {}, expirationTime: 5 }),
|
||||
unsubscribe: () => Promise.resolve(true),
|
||||
} satisfies PushSubscription;
|
||||
};
|
||||
|
||||
describe("WorkerWebpushConnectionService", () => {
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let webPushApiService: MockProxy<WebPushNotificationsApiService>;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let pushManager: MockProxy<PushManager>;
|
||||
const userId = "testUser1" as UserId;
|
||||
|
||||
let sut: WorkerWebPushConnectionService;
|
||||
|
||||
beforeEach(() => {
|
||||
configService = mock();
|
||||
webPushApiService = mock();
|
||||
stateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
|
||||
pushManager = mock();
|
||||
|
||||
sut = new WorkerWebPushConnectionService(
|
||||
configService,
|
||||
webPushApiService,
|
||||
mock<ServiceWorkerRegistration>({ pushManager: pushManager }),
|
||||
stateProvider,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
type ExtractKeyDefinitionType<T> = T extends KeyDefinition<infer U> ? U : never;
|
||||
describe("supportStatus$", () => {
|
||||
let fakeGlobalState: FakeGlobalState<
|
||||
ExtractKeyDefinitionType<typeof WEB_PUSH_SUBSCRIPTION_USERS>
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeGlobalState = stateProvider.getGlobal(WEB_PUSH_SUBSCRIPTION_USERS) as FakeGlobalState<
|
||||
ExtractKeyDefinitionType<typeof WEB_PUSH_SUBSCRIPTION_USERS>
|
||||
>;
|
||||
});
|
||||
|
||||
test("when web push is supported, have an existing subscription, and we've already registered the user, should not call API", async () => {
|
||||
configService.serverConfig$ = of(
|
||||
new ServerConfig(
|
||||
new ServerConfigData({
|
||||
push: new PushSettingsConfigResponse({
|
||||
pushTechnology: PushTechnology.WebPush,
|
||||
vapidPublicKey: "dGVzdA",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const existingSubscription = createSub("dGVzdA");
|
||||
await fakeGlobalState.nextState({ [existingSubscription.endpoint]: [userId] });
|
||||
|
||||
pushManager.getSubscription.mockResolvedValue(existingSubscription);
|
||||
|
||||
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
|
||||
expect(supportStatus.type).toBe("supported");
|
||||
const service = (supportStatus as Supported<WebPushConnector>).service;
|
||||
expect(service).not.toBeFalsy();
|
||||
|
||||
const notificationsSub = service.notifications$.subscribe();
|
||||
|
||||
await awaitAsync(2);
|
||||
|
||||
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
|
||||
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
notificationsSub.unsubscribe();
|
||||
});
|
||||
|
||||
test("when web push is supported, have an existing subscription, and we haven't registered the user, should call API", async () => {
|
||||
configService.serverConfig$ = of(
|
||||
new ServerConfig(
|
||||
new ServerConfigData({
|
||||
push: new PushSettingsConfigResponse({
|
||||
pushTechnology: PushTechnology.WebPush,
|
||||
vapidPublicKey: "dGVzdA",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const existingSubscription = createSub("dGVzdA");
|
||||
await fakeGlobalState.nextState({
|
||||
[existingSubscription.endpoint]: ["otherUserId" as UserId],
|
||||
});
|
||||
|
||||
pushManager.getSubscription.mockResolvedValue(existingSubscription);
|
||||
|
||||
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
|
||||
expect(supportStatus.type).toBe("supported");
|
||||
const service = (supportStatus as Supported<WebPushConnector>).service;
|
||||
expect(service).not.toBeFalsy();
|
||||
|
||||
const notificationsSub = service.notifications$.subscribe();
|
||||
|
||||
await awaitAsync(2);
|
||||
|
||||
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
|
||||
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1);
|
||||
expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({
|
||||
[existingSubscription.endpoint]: ["otherUserId", mockUser1],
|
||||
});
|
||||
|
||||
notificationsSub.unsubscribe();
|
||||
});
|
||||
|
||||
test("when web push is supported, have an existing subscription, but it isn't in state, should call API and add to state", async () => {
|
||||
configService.serverConfig$ = of(
|
||||
new ServerConfig(
|
||||
new ServerConfigData({
|
||||
push: new PushSettingsConfigResponse({
|
||||
pushTechnology: PushTechnology.WebPush,
|
||||
vapidPublicKey: "dGVzdA",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const existingSubscription = createSub("dGVzdA");
|
||||
await fakeGlobalState.nextState({
|
||||
[existingSubscription.endpoint]: null!,
|
||||
});
|
||||
|
||||
pushManager.getSubscription.mockResolvedValue(existingSubscription);
|
||||
|
||||
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
|
||||
expect(supportStatus.type).toBe("supported");
|
||||
const service = (supportStatus as Supported<WebPushConnector>).service;
|
||||
expect(service).not.toBeFalsy();
|
||||
|
||||
const notificationsSub = service.notifications$.subscribe();
|
||||
|
||||
await awaitAsync(2);
|
||||
|
||||
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
|
||||
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1);
|
||||
expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({
|
||||
[existingSubscription.endpoint]: [mockUser1],
|
||||
});
|
||||
|
||||
notificationsSub.unsubscribe();
|
||||
});
|
||||
|
||||
test("when web push is supported, have an existing subscription, but state array is null, should call API and add to state", async () => {
|
||||
configService.serverConfig$ = of(
|
||||
new ServerConfig(
|
||||
new ServerConfigData({
|
||||
push: new PushSettingsConfigResponse({
|
||||
pushTechnology: PushTechnology.WebPush,
|
||||
vapidPublicKey: "dGVzdA",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const existingSubscription = createSub("dGVzdA");
|
||||
await fakeGlobalState.nextState({});
|
||||
|
||||
pushManager.getSubscription.mockResolvedValue(existingSubscription);
|
||||
|
||||
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
|
||||
expect(supportStatus.type).toBe("supported");
|
||||
const service = (supportStatus as Supported<WebPushConnector>).service;
|
||||
expect(service).not.toBeFalsy();
|
||||
|
||||
const notificationsSub = service.notifications$.subscribe();
|
||||
|
||||
await awaitAsync(2);
|
||||
|
||||
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
|
||||
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1);
|
||||
expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({
|
||||
[existingSubscription.endpoint]: [mockUser1],
|
||||
});
|
||||
|
||||
notificationsSub.unsubscribe();
|
||||
});
|
||||
|
||||
test("when web push is supported, but we don't have an existing subscription, should call the api and wipe out existing state", async () => {
|
||||
configService.serverConfig$ = of(
|
||||
new ServerConfig(
|
||||
new ServerConfigData({
|
||||
push: new PushSettingsConfigResponse({
|
||||
pushTechnology: PushTechnology.WebPush,
|
||||
vapidPublicKey: "dGVzdA",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const existingState = createSub("dGVzdA");
|
||||
await fakeGlobalState.nextState({ [existingState.endpoint]: [userId] });
|
||||
|
||||
pushManager.getSubscription.mockResolvedValue(null);
|
||||
const newSubscription = createSub("dGVzdA");
|
||||
pushManager.subscribe.mockResolvedValue(newSubscription);
|
||||
|
||||
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
|
||||
expect(supportStatus.type).toBe("supported");
|
||||
const service = (supportStatus as Supported<WebPushConnector>).service;
|
||||
expect(service).not.toBeFalsy();
|
||||
|
||||
const notificationsSub = service.notifications$.subscribe();
|
||||
|
||||
await awaitAsync(2);
|
||||
|
||||
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
|
||||
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1);
|
||||
expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({
|
||||
[newSubscription.endpoint]: [mockUser1],
|
||||
});
|
||||
|
||||
notificationsSub.unsubscribe();
|
||||
});
|
||||
|
||||
test("when web push is supported and no existing subscription, should call API", async () => {
|
||||
configService.serverConfig$ = of(
|
||||
new ServerConfig(
|
||||
new ServerConfigData({
|
||||
push: new PushSettingsConfigResponse({
|
||||
pushTechnology: PushTechnology.WebPush,
|
||||
vapidPublicKey: "dGVzdA",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
pushManager.getSubscription.mockResolvedValue(null);
|
||||
pushManager.subscribe.mockResolvedValue(createSub("dGVzdA"));
|
||||
|
||||
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
|
||||
expect(supportStatus.type).toBe("supported");
|
||||
const service = (supportStatus as Supported<WebPushConnector>).service;
|
||||
expect(service).not.toBeFalsy();
|
||||
|
||||
const notificationsSub = service.notifications$.subscribe();
|
||||
|
||||
await awaitAsync(2);
|
||||
|
||||
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
|
||||
expect(pushManager.subscribe).toHaveBeenCalledTimes(1);
|
||||
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
|
||||
|
||||
notificationsSub.unsubscribe();
|
||||
});
|
||||
|
||||
test("when web push is supported and existing subscription with different key, should call API", async () => {
|
||||
configService.serverConfig$ = of(
|
||||
new ServerConfig(
|
||||
new ServerConfigData({
|
||||
push: new PushSettingsConfigResponse({
|
||||
pushTechnology: PushTechnology.WebPush,
|
||||
vapidPublicKey: "dGVzdA",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
pushManager.getSubscription.mockResolvedValue(createSub("dGVzdF9hbHQ"));
|
||||
|
||||
pushManager.subscribe.mockResolvedValue(createSub("dGVzdA"));
|
||||
|
||||
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
|
||||
expect(supportStatus.type).toBe("supported");
|
||||
const service = (supportStatus as Supported<WebPushConnector>).service;
|
||||
expect(service).not.toBeFalsy();
|
||||
|
||||
const notificationsSub = service.notifications$.subscribe();
|
||||
|
||||
await awaitAsync(2);
|
||||
|
||||
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
|
||||
expect(pushManager.subscribe).toHaveBeenCalledTimes(1);
|
||||
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
|
||||
|
||||
notificationsSub.unsubscribe();
|
||||
});
|
||||
|
||||
test("when server config emits multiple times quickly while api call takes a long time will only call API once", async () => {
|
||||
configService.serverConfig$ = of(
|
||||
new ServerConfig(
|
||||
new ServerConfigData({
|
||||
push: new PushSettingsConfigResponse({
|
||||
pushTechnology: PushTechnology.WebPush,
|
||||
vapidPublicKey: "dGVzdA",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
pushManager.getSubscription.mockResolvedValue(createSub("dGVzdF9hbHQ"));
|
||||
|
||||
pushManager.subscribe.mockResolvedValue(createSub("dGVzdA"));
|
||||
|
||||
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
|
||||
expect(supportStatus.type).toBe("supported");
|
||||
const service = (supportStatus as Supported<WebPushConnector>).service;
|
||||
expect(service).not.toBeFalsy();
|
||||
|
||||
const notificationsSub = service.notifications$.subscribe();
|
||||
|
||||
await awaitAsync(2);
|
||||
|
||||
expect(pushManager.getSubscription).toHaveBeenCalledTimes(1);
|
||||
expect(pushManager.subscribe).toHaveBeenCalledTimes(1);
|
||||
expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1);
|
||||
|
||||
notificationsSub.unsubscribe();
|
||||
});
|
||||
|
||||
it("server config shows SignalR support should return not-supported", async () => {
|
||||
configService.serverConfig$ = of(
|
||||
new ServerConfig(
|
||||
new ServerConfigData({
|
||||
push: new PushSettingsConfigResponse({
|
||||
pushTechnology: PushTechnology.SignalR,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
|
||||
expect(supportStatus.type).toBe("not-supported");
|
||||
});
|
||||
|
||||
it("server config shows web push but no public key support should return not-supported", async () => {
|
||||
configService.serverConfig$ = of(
|
||||
new ServerConfig(
|
||||
new ServerConfigData({
|
||||
push: new PushSettingsConfigResponse({
|
||||
pushTechnology: PushTechnology.WebPush,
|
||||
vapidPublicKey: null,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1));
|
||||
expect(supportStatus.type).toBe("not-supported");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Subject,
|
||||
Subscription,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { PushTechnology } from "../../../enums/push-technology.enum";
|
||||
@@ -17,6 +18,7 @@ import { UserId } from "../../../types/guid";
|
||||
import { ConfigService } from "../../abstractions/config/config.service";
|
||||
import { SupportStatus } from "../../misc/support-status";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { KeyDefinition, StateProvider, WEB_PUSH_SUBSCRIPTION } from "../../state";
|
||||
|
||||
import { WebPushNotificationsApiService } from "./web-push-notifications-api.service";
|
||||
import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service";
|
||||
@@ -48,6 +50,7 @@ export class WorkerWebPushConnectionService implements WebPushConnectionService
|
||||
private readonly configService: ConfigService,
|
||||
private readonly webPushApiService: WebPushNotificationsApiService,
|
||||
private readonly serviceWorkerRegistration: ServiceWorkerRegistration,
|
||||
private readonly stateProvider: StateProvider,
|
||||
) {}
|
||||
|
||||
start(): Subscription {
|
||||
@@ -97,6 +100,7 @@ export class WorkerWebPushConnectionService implements WebPushConnectionService
|
||||
this.serviceWorkerRegistration,
|
||||
this.pushEvent,
|
||||
this.pushChangeEvent,
|
||||
this.stateProvider,
|
||||
),
|
||||
} satisfies SupportStatus<WebPushConnector>;
|
||||
}),
|
||||
@@ -114,20 +118,36 @@ class MyWebPushConnector implements WebPushConnector {
|
||||
private readonly serviceWorkerRegistration: ServiceWorkerRegistration,
|
||||
private readonly pushEvent$: Observable<PushEvent>,
|
||||
private readonly pushChangeEvent$: Observable<PushSubscriptionChangeEvent>,
|
||||
private readonly stateProvider: StateProvider,
|
||||
) {
|
||||
const subscriptionUsersState = this.stateProvider.getGlobal(WEB_PUSH_SUBSCRIPTION_USERS);
|
||||
this.notifications$ = this.getOrCreateSubscription$(this.vapidPublicKey).pipe(
|
||||
concatMap((subscription) => {
|
||||
return defer(() => {
|
||||
if (subscription == null) {
|
||||
throw new Error("Expected a non-null subscription.");
|
||||
}
|
||||
return this.webPushApiService.putSubscription(subscription.toJSON());
|
||||
}).pipe(
|
||||
switchMap(() => this.pushEvent$),
|
||||
map((e) => {
|
||||
return new NotificationResponse(e.data.json().data);
|
||||
}),
|
||||
);
|
||||
withLatestFrom(subscriptionUsersState.state$.pipe(map((x) => x ?? {}))),
|
||||
concatMap(async ([[isExistingSubscription, subscription], subscriptionUsers]) => {
|
||||
if (subscription == null) {
|
||||
throw new Error("Expected a non-null subscription.");
|
||||
}
|
||||
|
||||
// If this is a new subscription, we can clear state and start over
|
||||
if (!isExistingSubscription) {
|
||||
subscriptionUsers = {};
|
||||
}
|
||||
|
||||
// If the user is already subscribed, we don't need to do anything
|
||||
if (subscriptionUsers[subscription.endpoint]?.includes(this.userId)) {
|
||||
return;
|
||||
}
|
||||
subscriptionUsers[subscription.endpoint] ??= [];
|
||||
subscriptionUsers[subscription.endpoint].push(this.userId);
|
||||
// Update the state with the new subscription-user association
|
||||
await subscriptionUsersState.update(() => subscriptionUsers);
|
||||
|
||||
// Inform the server about the new subscription-user association
|
||||
await this.webPushApiService.putSubscription(subscription.toJSON());
|
||||
}),
|
||||
switchMap(() => this.pushEvent$),
|
||||
map((e) => {
|
||||
return new NotificationResponse(e.data.json().data);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -146,7 +166,7 @@ class MyWebPushConnector implements WebPushConnector {
|
||||
await this.serviceWorkerRegistration.pushManager.getSubscription();
|
||||
|
||||
if (existingSubscription == null) {
|
||||
return await this.pushManagerSubscribe(key);
|
||||
return [false, await this.pushManagerSubscribe(key)] as const;
|
||||
}
|
||||
|
||||
const subscriptionKey = Utils.fromBufferToUrlB64(
|
||||
@@ -159,12 +179,30 @@ class MyWebPushConnector implements WebPushConnector {
|
||||
if (subscriptionKey !== key) {
|
||||
// There is a subscription, but it's not for the current server, unsubscribe and then make a new one
|
||||
await existingSubscription.unsubscribe();
|
||||
return await this.pushManagerSubscribe(key);
|
||||
return [false, await this.pushManagerSubscribe(key)] as const;
|
||||
}
|
||||
|
||||
return existingSubscription;
|
||||
return [true, existingSubscription] as const;
|
||||
}),
|
||||
this.pushChangeEvent$.pipe(map((event) => event.newSubscription)),
|
||||
this.pushChangeEvent$.pipe(map((event) => [false, event.newSubscription] as const)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const WEB_PUSH_SUBSCRIPTION_USERS = new KeyDefinition<Record<string, UserId[]>>(
|
||||
WEB_PUSH_SUBSCRIPTION,
|
||||
"subUsers",
|
||||
{
|
||||
deserializer: (obj) => {
|
||||
if (obj == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const result: Record<string, UserId[]> = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
result[key] = Array.isArray(value) ? value : [];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -128,6 +128,9 @@ export const EXTENSION_INITIAL_INSTALL_DISK = new StateDefinition(
|
||||
"extensionInitialInstall",
|
||||
"disk",
|
||||
);
|
||||
export const WEB_PUSH_SUBSCRIPTION = new StateDefinition("webPushSubscription", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
|
||||
// Design System
|
||||
|
||||
|
||||
@@ -1813,6 +1813,11 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
if (authed) {
|
||||
const authHeader = await this.getActiveBearerToken();
|
||||
headers.set("Authorization", "Bearer " + authHeader);
|
||||
} else {
|
||||
// For unauthenticated requests, we need to tell the server what the device is for flag targeting,
|
||||
// since it won't be able to get it from the access token.
|
||||
const appId = await this.appIdService.getAppId();
|
||||
headers.set("Device-Identifier", appId);
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { SendView } from "../tools/send/models/view/send.view";
|
||||
import { IndexedEntityId, UserId } from "../types/guid";
|
||||
import { CipherView } from "../vault/models/view/cipher.view";
|
||||
import { SendView } from "../../tools/send/models/view/send.view";
|
||||
import { IndexedEntityId, UserId } from "../../types/guid";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
|
||||
export abstract class SearchService {
|
||||
indexedEntityId$: (userId: UserId) => Observable<IndexedEntityId | null>;
|
||||
12
libs/common/src/vault/enums/extension-page-urls.enum.ts
Normal file
12
libs/common/src/vault/enums/extension-page-urls.enum.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { UnionOfValues } from "../types/union-of-values";
|
||||
|
||||
/**
|
||||
* Available pages within the extension by their URL.
|
||||
* Useful when opening a specific page within the popup.
|
||||
*/
|
||||
export const ExtensionPageUrls: Record<string, `popup/index.html#/${string}`> = {
|
||||
Index: "popup/index.html#/",
|
||||
AtRiskPasswords: "popup/index.html#/at-risk-passwords",
|
||||
} as const;
|
||||
|
||||
export type ExtensionPageUrls = UnionOfValues<typeof ExtensionPageUrls>;
|
||||
@@ -3,3 +3,4 @@ export * from "./cipher-type";
|
||||
export * from "./field-type.enum";
|
||||
export * from "./linked-id-type.enum";
|
||||
export * from "./secure-note-type.enum";
|
||||
export * from "./extension-page-urls.enum";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const VaultMessages = {
|
||||
HasBwInstalled: "hasBwInstalled",
|
||||
checkBwInstalled: "checkIfBWExtensionInstalled",
|
||||
/** @deprecated use {@link OpenBrowserExtensionToUrl} */
|
||||
OpenAtRiskPasswords: "openAtRiskPasswords",
|
||||
OpenBrowserExtensionToUrl: "openBrowserExtensionToUrl",
|
||||
PopupOpened: "popupOpened",
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -353,14 +353,14 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
type: this.type,
|
||||
favorite: this.favorite ?? false,
|
||||
organizationUseTotp: this.organizationUseTotp ?? false,
|
||||
edit: this.edit,
|
||||
edit: this.edit ?? true,
|
||||
permissions: this.permissions
|
||||
? {
|
||||
delete: this.permissions.delete,
|
||||
restore: this.permissions.restore,
|
||||
}
|
||||
: undefined,
|
||||
viewPassword: this.viewPassword,
|
||||
viewPassword: this.viewPassword ?? true,
|
||||
localData: this.localData
|
||||
? {
|
||||
lastUsedDate: this.localData.lastUsedDate
|
||||
|
||||
@@ -97,8 +97,8 @@ export class LoginUri extends Domain {
|
||||
*/
|
||||
toSdkLoginUri(): SdkLoginUri {
|
||||
return {
|
||||
uri: this.uri.toJSON(),
|
||||
uriChecksum: this.uriChecksum.toJSON(),
|
||||
uri: this.uri?.toJSON(),
|
||||
uriChecksum: this.uriChecksum?.toJSON(),
|
||||
match: this.match,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,24 +36,6 @@ describe("serviceUtils", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("nestedTraverse_vNext", () => {
|
||||
it("should traverse a tree and add a node at the correct position given a valid path", () => {
|
||||
const nodeToBeAdded: FakeObject = { id: "1.2.1", name: "1.2.1" };
|
||||
const path = ["1", "1.2", "1.2.1"];
|
||||
|
||||
ServiceUtils.nestedTraverse_vNext(nodeTree, 0, path, nodeToBeAdded, null, "/");
|
||||
expect(nodeTree[0].children[1].children[0].node).toEqual(nodeToBeAdded);
|
||||
});
|
||||
|
||||
it("should combine the path for missing nodes and use as the added node name given an invalid path", () => {
|
||||
const nodeToBeAdded: FakeObject = { id: "blank", name: "blank" };
|
||||
const path = ["3", "3.1", "3.1.1"];
|
||||
|
||||
ServiceUtils.nestedTraverse_vNext(nodeTree, 0, path, nodeToBeAdded, null, "/");
|
||||
expect(nodeTree[2].children[0].node.name).toEqual("3.1/3.1.1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTreeNodeObject", () => {
|
||||
it("should return a matching node given a single tree branch and a valid id", () => {
|
||||
const id = "1.1.1";
|
||||
|
||||
@@ -4,64 +4,6 @@
|
||||
import { ITreeNodeObject, TreeNode } from "./models/domain/tree-node";
|
||||
|
||||
export class ServiceUtils {
|
||||
static nestedTraverse(
|
||||
nodeTree: TreeNode<ITreeNodeObject>[],
|
||||
partIndex: number,
|
||||
parts: string[],
|
||||
obj: ITreeNodeObject,
|
||||
parent: TreeNode<ITreeNodeObject> | undefined,
|
||||
delimiter: string,
|
||||
) {
|
||||
if (parts.length <= partIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const end: boolean = partIndex === parts.length - 1;
|
||||
const partName: string = parts[partIndex];
|
||||
|
||||
for (let i = 0; i < nodeTree.length; i++) {
|
||||
if (nodeTree[i].node.name !== partName) {
|
||||
continue;
|
||||
}
|
||||
if (end && nodeTree[i].node.id !== obj.id) {
|
||||
// Another node exists with the same name as the node being added
|
||||
nodeTree.push(new TreeNode(obj, parent, partName));
|
||||
return;
|
||||
}
|
||||
// Move down the tree to the next level
|
||||
ServiceUtils.nestedTraverse(
|
||||
nodeTree[i].children,
|
||||
partIndex + 1,
|
||||
parts,
|
||||
obj,
|
||||
nodeTree[i],
|
||||
delimiter,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's no node here with the same name...
|
||||
if (nodeTree.filter((n) => n.node.name === partName).length === 0) {
|
||||
// And we're at the end of the path given, add the node
|
||||
if (end) {
|
||||
nodeTree.push(new TreeNode(obj, parent, partName));
|
||||
return;
|
||||
}
|
||||
// And we're not at the end of the path, combine the current name with the next name
|
||||
// 1, *1.2, 1.2.1 becomes
|
||||
// 1, *1.2/1.2.1
|
||||
const newPartName = partName + delimiter + parts[partIndex + 1];
|
||||
ServiceUtils.nestedTraverse(
|
||||
nodeTree,
|
||||
0,
|
||||
[newPartName, ...parts.slice(partIndex + 2)],
|
||||
obj,
|
||||
parent,
|
||||
delimiter,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively adds a node to nodeTree
|
||||
* @param {TreeNode<ITreeNodeObject>[]} nodeTree - An array of TreeNodes that the node will be added to
|
||||
@@ -71,7 +13,7 @@ export class ServiceUtils {
|
||||
* @param {ITreeNodeObject} parent - The parent node of the `obj` node
|
||||
* @param {string} delimiter - The delimiter used to split the path string, will be used to combine the path for missing nodes
|
||||
*/
|
||||
static nestedTraverse_vNext(
|
||||
static nestedTraverse(
|
||||
nodeTree: TreeNode<ITreeNodeObject>[],
|
||||
partIndex: number,
|
||||
parts: string[],
|
||||
@@ -104,7 +46,7 @@ export class ServiceUtils {
|
||||
// 1, *1.2, 1.2.1 becomes
|
||||
// 1, *1.2/1.2.1
|
||||
const newPartName = partName + delimiter + parts[partIndex + 1];
|
||||
ServiceUtils.nestedTraverse_vNext(
|
||||
ServiceUtils.nestedTraverse(
|
||||
nodeTree,
|
||||
0,
|
||||
[newPartName, ...parts.slice(partIndex + 2)],
|
||||
@@ -114,7 +56,7 @@ export class ServiceUtils {
|
||||
);
|
||||
} else {
|
||||
// There is a node here with the same name, descend into it
|
||||
ServiceUtils.nestedTraverse_vNext(
|
||||
ServiceUtils.nestedTraverse(
|
||||
matchingNodes[0].children,
|
||||
partIndex + 1,
|
||||
parts,
|
||||
|
||||
@@ -10,7 +10,6 @@ import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-a
|
||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||
import { makeStaticByteArray, makeSymmetricCryptoKey } from "../../../spec/utils";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { SearchService } from "../../abstractions/search.service";
|
||||
import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
|
||||
import { BulkEncryptService } from "../../key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
@@ -29,6 +28,7 @@ import { CipherKey, OrgKey, UserKey } from "../../types/key";
|
||||
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||
import { EncryptionContext } from "../abstractions/cipher.service";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { SearchService } from "../abstractions/search.service";
|
||||
import { FieldType } from "../enums";
|
||||
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
|
||||
import { CipherType } from "../enums/cipher-type";
|
||||
|
||||
@@ -9,7 +9,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { SearchService } from "../../abstractions/search.service";
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
|
||||
@@ -38,6 +37,7 @@ import {
|
||||
EncryptionContext,
|
||||
} from "../abstractions/cipher.service";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { SearchService } from "../abstractions/search.service";
|
||||
import { FieldType } from "../enums";
|
||||
import { CipherType } from "../enums/cipher-type";
|
||||
import { CipherData } from "../models/data/cipher.data";
|
||||
|
||||
@@ -4,21 +4,21 @@ import * as lunr from "lunr";
|
||||
import { Observable, firstValueFrom, map } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
|
||||
import { UriMatchStrategy } from "../models/domain/domain-service";
|
||||
import { I18nService } from "../platform/abstractions/i18n.service";
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
import { UriMatchStrategy } from "../../models/domain/domain-service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import {
|
||||
SingleUserState,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
VAULT_SEARCH_MEMORY,
|
||||
} from "../platform/state";
|
||||
import { SendView } from "../tools/send/models/view/send.view";
|
||||
import { IndexedEntityId, UserId } from "../types/guid";
|
||||
import { FieldType } from "../vault/enums";
|
||||
import { CipherType } from "../vault/enums/cipher-type";
|
||||
import { CipherView } from "../vault/models/view/cipher.view";
|
||||
} from "../../platform/state";
|
||||
import { SendView } from "../../tools/send/models/view/send.view";
|
||||
import { IndexedEntityId, UserId } from "../../types/guid";
|
||||
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
|
||||
import { FieldType } from "../enums";
|
||||
import { CipherType } from "../enums/cipher-type";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
|
||||
export type SerializedLunrIndex = {
|
||||
version: string;
|
||||
Reference in New Issue
Block a user