mirror of
https://github.com/bitwarden/browser
synced 2026-03-02 03:21:19 +00:00
Merge branch 'tools/pm-23531/rename-send-created-to-active-send-icon' into tools/pm-21776/update-send-access-copy
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";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ClientSettings, LogLevel, BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { SdkClientFactory } from "../src/platform/abstractions/sdk/sdk-client-factory";
|
||||
|
||||
export class DefaultSdkClientFactory implements SdkClientFactory {
|
||||
createSdkClient(settings?: ClientSettings, log_level?: LogLevel): Promise<BitwardenClient> {
|
||||
createSdkClient(
|
||||
...args: ConstructorParameters<typeof BitwardenClient>
|
||||
): Promise<BitwardenClient> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
45
libs/common/src/auth/utils/assert-non-nullish.util.ts
Normal file
45
libs/common/src/auth/utils/assert-non-nullish.util.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Asserts that a value is non-nullish (not `null` or `undefined`); throws if value is nullish.
|
||||
*
|
||||
* @param val the value to check
|
||||
* @param name the name of the value to include in the error message
|
||||
* @param ctx context to optionally append to the error message
|
||||
* @throws if the value is null or undefined
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```
|
||||
* // `newPasswordHint` can have an empty string as a valid value, so we check non-nullish
|
||||
* this.assertNonNullish(
|
||||
* passwordInputResult.newPasswordHint,
|
||||
* "newPasswordHint",
|
||||
* "Could not set initial password."
|
||||
* );
|
||||
* // Output error message: "newPasswordHint is null or undefined. Could not set initial password."
|
||||
* ```
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* If you use this method repeatedly to check several values, it may help to assign any
|
||||
* additional context (`ctx`) to a variable and pass it in to each call. This prevents the
|
||||
* call from reformatting vertically via prettier in your text editor, taking up multiple lines.
|
||||
*
|
||||
* For example:
|
||||
* ```
|
||||
* const ctx = "Could not set initial password.";
|
||||
*
|
||||
* this.assertNonNullish(valueOne, "valueOne", ctx);
|
||||
* this.assertNonNullish(valueTwo, "valueTwo", ctx);
|
||||
* this.assertNonNullish(valueThree, "valueThree", ctx);
|
||||
* ```
|
||||
*/
|
||||
export function assertNonNullish<T>(
|
||||
val: T,
|
||||
name: string,
|
||||
ctx?: string,
|
||||
): asserts val is NonNullable<T> {
|
||||
if (val == null) {
|
||||
// If context is provided, append it to the error message with a space before it.
|
||||
throw new Error(`${name} is null or undefined.${ctx ? ` ${ctx}` : ""}`);
|
||||
}
|
||||
}
|
||||
46
libs/common/src/auth/utils/assert-truthy.util.ts
Normal file
46
libs/common/src/auth/utils/assert-truthy.util.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Asserts that a value is truthy; throws if value is falsy.
|
||||
*
|
||||
* @param val the value to check
|
||||
* @param name the name of the value to include in the error message
|
||||
* @param ctx context to optionally append to the error message
|
||||
* @throws if the value is falsy (`false`, `""`, `0`, `null`, `undefined`, `void`, or `NaN`)
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```
|
||||
* this.assertTruthy(
|
||||
* this.organizationId,
|
||||
* "organizationId",
|
||||
* "Could not set initial password."
|
||||
* );
|
||||
* // Output error message: "organizationId is falsy. Could not set initial password."
|
||||
* ```
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* If you use this method repeatedly to check several values, it may help to assign any
|
||||
* additional context (`ctx`) to a variable and pass it in to each call. This prevents the
|
||||
* call from reformatting vertically via prettier in your text editor, taking up multiple lines.
|
||||
*
|
||||
* For example:
|
||||
* ```
|
||||
* const ctx = "Could not set initial password.";
|
||||
*
|
||||
* this.assertTruthy(valueOne, "valueOne", ctx);
|
||||
* this.assertTruthy(valueTwo, "valueTwo", ctx);
|
||||
* this.assertTruthy(valueThree, "valueThree", ctx);
|
||||
*/
|
||||
export function assertTruthy<T>(
|
||||
val: T,
|
||||
name: string,
|
||||
ctx?: string,
|
||||
): asserts val is Exclude<T, false | "" | 0 | null | undefined | void | 0n> {
|
||||
// Because `NaN` is a value (not a type) of type 'number', that means we cannot add
|
||||
// it to the list of falsy values in the type assertion. Instead, we check for it
|
||||
// separately at runtime.
|
||||
if (!val || (typeof val === "number" && Number.isNaN(val))) {
|
||||
// If context is provided, append it to the error message with a space before it.
|
||||
throw new Error(`${name} is falsy.${ctx ? ` ${ctx}` : ""}`);
|
||||
}
|
||||
}
|
||||
2
libs/common/src/auth/utils/index.ts
Normal file
2
libs/common/src/auth/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { assertTruthy } from "./assert-truthy.util";
|
||||
export { assertNonNullish } from "./assert-non-nullish.util";
|
||||
@@ -27,6 +27,7 @@ export enum DeviceType {
|
||||
WindowsCLI = 23,
|
||||
MacOsCLI = 24,
|
||||
LinuxCLI = 25,
|
||||
DuckDuckGoBrowser = 26,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,6 +56,7 @@ export const DeviceTypeMetadata: Record<DeviceType, DeviceTypeMetadata> = {
|
||||
[DeviceType.IEBrowser]: { category: "webVault", platform: "IE" },
|
||||
[DeviceType.SafariBrowser]: { category: "webVault", platform: "Safari" },
|
||||
[DeviceType.VivaldiBrowser]: { category: "webVault", platform: "Vivaldi" },
|
||||
[DeviceType.DuckDuckGoBrowser]: { category: "webVault", platform: "DuckDuckGo" },
|
||||
[DeviceType.UnknownBrowser]: { category: "webVault", platform: "Unknown" },
|
||||
[DeviceType.WindowsDesktop]: { category: "desktop", platform: "Windows" },
|
||||
[DeviceType.MacOsDesktop]: { category: "desktop", platform: "macOS" },
|
||||
|
||||
@@ -90,4 +90,7 @@ export enum EventType {
|
||||
OrganizationDomain_NotVerified = 2003,
|
||||
|
||||
Secret_Retrieved = 2100,
|
||||
Secret_Created = 2101,
|
||||
Secret_Edited = 2102,
|
||||
Secret_Deleted = 2103,
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export enum FeatureFlag {
|
||||
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
|
||||
PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup",
|
||||
UseOrganizationWarningsService = "use-organization-warnings-service",
|
||||
AllowTrialLengthZero = "pm-20322-allow-trial-length-0",
|
||||
|
||||
/* Data Insights and Reporting */
|
||||
EnableRiskInsightsNotifications = "enable-risk-insights-notifications",
|
||||
@@ -55,6 +56,7 @@ export enum FeatureFlag {
|
||||
PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms",
|
||||
EndUserNotifications = "pm-10609-end-user-notifications",
|
||||
RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy",
|
||||
PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp",
|
||||
|
||||
/* Platform */
|
||||
IpcChannelFramework = "ipc-channel-framework",
|
||||
@@ -99,6 +101,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.EndUserNotifications]: FALSE,
|
||||
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
||||
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
|
||||
[FeatureFlag.PM19315EndUserActivationMvp]: FALSE,
|
||||
|
||||
/* Auth */
|
||||
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
|
||||
@@ -112,6 +115,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
|
||||
[FeatureFlag.PM19956_RequireProviderPaymentMethodDuringSetup]: FALSE,
|
||||
[FeatureFlag.UseOrganizationWarningsService]: FALSE,
|
||||
[FeatureFlag.AllowTrialLengthZero]: FALSE,
|
||||
|
||||
/* Key Management */
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
|
||||
@@ -29,5 +29,5 @@ export enum NotificationType {
|
||||
Notification = 20,
|
||||
NotificationStatus = 21,
|
||||
|
||||
PendingSecurityTasks = 22,
|
||||
RefreshSecurityTasks = 22,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export class PasswordHistoryExport {
|
||||
|
||||
static toDomain(req: PasswordHistoryExport, domain = new Password()) {
|
||||
domain.password = req.password != null ? new EncString(req.password) : null;
|
||||
domain.lastUsedDate = req.lastUsedDate;
|
||||
domain.lastUsedDate = req.lastUsedDate ? new Date(req.lastUsedDate) : null;
|
||||
return domain;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export class Fido2AuthenticatorError extends Error {
|
||||
}
|
||||
|
||||
export interface PublicKeyCredentialDescriptor {
|
||||
id: Uint8Array;
|
||||
id: ArrayBuffer;
|
||||
transports?: ("ble" | "hybrid" | "internal" | "nfc" | "usb")[];
|
||||
type: "public-key";
|
||||
}
|
||||
@@ -155,9 +155,9 @@ export interface Fido2AuthenticatorGetAssertionParams {
|
||||
|
||||
export interface Fido2AuthenticatorGetAssertionResult {
|
||||
selectedCredential: {
|
||||
id: Uint8Array;
|
||||
userHandle?: Uint8Array;
|
||||
id: ArrayBuffer;
|
||||
userHandle?: ArrayBuffer;
|
||||
};
|
||||
authenticatorData: Uint8Array;
|
||||
signature: Uint8Array;
|
||||
authenticatorData: ArrayBuffer;
|
||||
signature: ArrayBuffer;
|
||||
}
|
||||
|
||||
@@ -1,9 +1 @@
|
||||
import { LogLevelType } from "../enums/log-level-type.enum";
|
||||
|
||||
export abstract class LogService {
|
||||
abstract debug(message?: any, ...optionalParams: any[]): void;
|
||||
abstract info(message?: any, ...optionalParams: any[]): void;
|
||||
abstract warning(message?: any, ...optionalParams: any[]): void;
|
||||
abstract error(message?: any, ...optionalParams: any[]): void;
|
||||
abstract write(level: LogLevelType, message?: any, ...optionalParams: any[]): void;
|
||||
}
|
||||
export { LogService } from "@bitwarden/logging";
|
||||
|
||||
@@ -28,6 +28,15 @@ export abstract class PlatformUtilsService {
|
||||
abstract getApplicationVersionNumber(): Promise<string>;
|
||||
abstract supportsWebAuthn(win: Window): boolean;
|
||||
abstract supportsDuo(): boolean;
|
||||
/**
|
||||
* Returns true if the device supports autofill functionality
|
||||
*/
|
||||
abstract supportsAutofill(): boolean;
|
||||
/**
|
||||
* Returns true if the device supports native file downloads without
|
||||
* the need for `target="_blank"`
|
||||
*/
|
||||
abstract supportsFileDownloads(): boolean;
|
||||
/**
|
||||
* @deprecated use `@bitwarden/components/ToastService.showToast` instead
|
||||
*
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum LogLevelType {
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
export { LogLevel as LogLevelType } from "@bitwarden/logging";
|
||||
|
||||
@@ -31,22 +31,35 @@ export type TimeoutManager = {
|
||||
class SignalRLogger implements ILogger {
|
||||
constructor(private readonly logService: LogService) {}
|
||||
|
||||
redactMessage(message: string): string {
|
||||
const ACCESS_TOKEN_TEXT = "access_token=";
|
||||
// Redact the access token from the logs if it exists.
|
||||
const accessTokenIndex = message.indexOf(ACCESS_TOKEN_TEXT);
|
||||
if (accessTokenIndex !== -1) {
|
||||
return message.substring(0, accessTokenIndex + ACCESS_TOKEN_TEXT.length) + "[REDACTED]";
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
log(logLevel: LogLevel, message: string): void {
|
||||
const redactedMessage = `[SignalR] ${this.redactMessage(message)}`;
|
||||
|
||||
switch (logLevel) {
|
||||
case LogLevel.Critical:
|
||||
this.logService.error(message);
|
||||
this.logService.error(redactedMessage);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
this.logService.error(message);
|
||||
this.logService.error(redactedMessage);
|
||||
break;
|
||||
case LogLevel.Warning:
|
||||
this.logService.warning(message);
|
||||
this.logService.warning(redactedMessage);
|
||||
break;
|
||||
case LogLevel.Information:
|
||||
this.logService.info(message);
|
||||
this.logService.info(redactedMessage);
|
||||
break;
|
||||
case LogLevel.Debug:
|
||||
this.logService.debug(message);
|
||||
this.logService.debug(redactedMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { interceptConsole, restoreConsole } from "../../../spec";
|
||||
import { ConsoleLogService } from "@bitwarden/logging";
|
||||
|
||||
import { ConsoleLogService } from "./console-log.service";
|
||||
import { interceptConsole, restoreConsole } from "../../../spec";
|
||||
|
||||
describe("ConsoleLogService", () => {
|
||||
const error = new Error("this is an error");
|
||||
|
||||
@@ -1,59 +1 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { LogService as LogServiceAbstraction } from "../abstractions/log.service";
|
||||
import { LogLevelType } from "../enums/log-level-type.enum";
|
||||
|
||||
export class ConsoleLogService implements LogServiceAbstraction {
|
||||
protected timersMap: Map<string, [number, number]> = new Map();
|
||||
|
||||
constructor(
|
||||
protected isDev: boolean,
|
||||
protected filter: (level: LogLevelType) => boolean = null,
|
||||
) {}
|
||||
|
||||
debug(message?: any, ...optionalParams: any[]) {
|
||||
if (!this.isDev) {
|
||||
return;
|
||||
}
|
||||
this.write(LogLevelType.Debug, message, ...optionalParams);
|
||||
}
|
||||
|
||||
info(message?: any, ...optionalParams: any[]) {
|
||||
this.write(LogLevelType.Info, message, ...optionalParams);
|
||||
}
|
||||
|
||||
warning(message?: any, ...optionalParams: any[]) {
|
||||
this.write(LogLevelType.Warning, message, ...optionalParams);
|
||||
}
|
||||
|
||||
error(message?: any, ...optionalParams: any[]) {
|
||||
this.write(LogLevelType.Error, message, ...optionalParams);
|
||||
}
|
||||
|
||||
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
|
||||
if (this.filter != null && this.filter(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (level) {
|
||||
case LogLevelType.Debug:
|
||||
// eslint-disable-next-line
|
||||
console.log(message, ...optionalParams);
|
||||
break;
|
||||
case LogLevelType.Info:
|
||||
// eslint-disable-next-line
|
||||
console.log(message, ...optionalParams);
|
||||
break;
|
||||
case LogLevelType.Warning:
|
||||
// eslint-disable-next-line
|
||||
console.warn(message, ...optionalParams);
|
||||
break;
|
||||
case LogLevelType.Error:
|
||||
// eslint-disable-next-line
|
||||
console.error(message, ...optionalParams);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
export { ConsoleLogService } from "@bitwarden/logging";
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("credential-id-utils", () => {
|
||||
new Uint8Array([
|
||||
0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07,
|
||||
0xe7,
|
||||
]),
|
||||
]).buffer,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("credential-id-utils", () => {
|
||||
new Uint8Array([
|
||||
0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07,
|
||||
0xe7,
|
||||
]),
|
||||
]).buffer,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { Fido2Utils } from "./fido2-utils";
|
||||
import { guidToRawFormat } from "./guid-utils";
|
||||
|
||||
export function parseCredentialId(encodedCredentialId: string): Uint8Array {
|
||||
export function parseCredentialId(encodedCredentialId: string): ArrayBuffer {
|
||||
try {
|
||||
if (encodedCredentialId.startsWith("b64.")) {
|
||||
return Fido2Utils.stringToBuffer(encodedCredentialId.slice(4));
|
||||
}
|
||||
|
||||
return guidToRawFormat(encodedCredentialId);
|
||||
return guidToRawFormat(encodedCredentialId).buffer;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
@@ -18,13 +18,16 @@ export function parseCredentialId(encodedCredentialId: string): Uint8Array {
|
||||
/**
|
||||
* Compares two credential IDs for equality.
|
||||
*/
|
||||
export function compareCredentialIds(a: Uint8Array, b: Uint8Array): boolean {
|
||||
if (a.length !== b.length) {
|
||||
export function compareCredentialIds(a: ArrayBuffer, b: ArrayBuffer): boolean {
|
||||
if (a.byteLength !== b.byteLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
const viewA = new Uint8Array(a);
|
||||
const viewB = new Uint8Array(b);
|
||||
|
||||
for (let i = 0; i < viewA.length; i++) {
|
||||
if (viewA[i] !== viewB[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,7 +514,7 @@ async function getPrivateKeyFromFido2Credential(
|
||||
const keyBuffer = Fido2Utils.stringToBuffer(fido2Credential.keyValue);
|
||||
return await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
keyBuffer,
|
||||
new Uint8Array(keyBuffer),
|
||||
{
|
||||
name: fido2Credential.keyAlgorithm,
|
||||
namedCurve: fido2Credential.keyCurve,
|
||||
|
||||
@@ -127,9 +127,9 @@ export class Fido2ClientService<ParentWindowReference>
|
||||
}
|
||||
|
||||
const userId = Fido2Utils.stringToBuffer(params.user.id);
|
||||
if (userId.length < 1 || userId.length > 64) {
|
||||
if (userId.byteLength < 1 || userId.byteLength > 64) {
|
||||
this.logService?.warning(
|
||||
`[Fido2Client] Invalid 'user.id' length: ${params.user.id} (${userId.length})`,
|
||||
`[Fido2Client] Invalid 'user.id' length: ${params.user.id} (${userId.byteLength})`,
|
||||
);
|
||||
throw new TypeError("Invalid 'user.id' length");
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ export class Fido2Utils {
|
||||
.replace(/=/g, "");
|
||||
}
|
||||
|
||||
static stringToBuffer(str: string): Uint8Array {
|
||||
return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str));
|
||||
static stringToBuffer(str: string): ArrayBuffer {
|
||||
return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str)).buffer;
|
||||
}
|
||||
|
||||
static bufferSourceToUint8Array(bufferSource: BufferSource): Uint8Array {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
BitwardenClient,
|
||||
ClientSettings,
|
||||
DeviceType as SdkDeviceType,
|
||||
TokenProvider,
|
||||
} from "@bitwarden/sdk-internal";
|
||||
|
||||
import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data";
|
||||
@@ -41,6 +42,17 @@ import { EncryptedString } from "../../models/domain/enc-string";
|
||||
// blocking the creation of an internal client for that user.
|
||||
const UnsetClient = Symbol("UnsetClient");
|
||||
|
||||
/**
|
||||
* A token provider that exposes the access token to the SDK.
|
||||
*/
|
||||
class JsTokenProvider implements TokenProvider {
|
||||
constructor() {}
|
||||
|
||||
async get_access_token(): Promise<string | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultSdkService implements SdkService {
|
||||
private sdkClientOverrides = new BehaviorSubject<{
|
||||
[userId: UserId]: Rc<BitwardenClient> | typeof UnsetClient;
|
||||
@@ -51,7 +63,7 @@ export class DefaultSdkService implements SdkService {
|
||||
concatMap(async (env) => {
|
||||
await SdkLoadService.Ready;
|
||||
const settings = this.toSettings(env);
|
||||
return await this.sdkClientFactory.createSdkClient(settings);
|
||||
return await this.sdkClientFactory.createSdkClient(new JsTokenProvider(), settings);
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
@@ -151,7 +163,10 @@ export class DefaultSdkService implements SdkService {
|
||||
}
|
||||
|
||||
const settings = this.toSettings(env);
|
||||
const client = await this.sdkClientFactory.createSdkClient(settings);
|
||||
const client = await this.sdkClientFactory.createSdkClient(
|
||||
new JsTokenProvider(),
|
||||
settings,
|
||||
);
|
||||
|
||||
await this.initializeClient(
|
||||
userId,
|
||||
|
||||
@@ -6,12 +6,13 @@ import { any, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, map, of, timeout } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { StorageServiceProvider } from "@bitwarden/storage-core";
|
||||
|
||||
import { awaitAsync, trackEmissions } from "../../../../spec";
|
||||
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
||||
import { Account } from "../../../auth/abstractions/account.service";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
||||
import { StateDefinition } from "../state-definition";
|
||||
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||
import { UserKeyDefinition } from "../user-key-definition";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { StorageServiceProvider } from "@bitwarden/storage-core";
|
||||
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
||||
import { GlobalState } from "../global-state";
|
||||
import { GlobalStateProvider } from "../global-state.provider";
|
||||
import { KeyDefinition } from "../key-definition";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core";
|
||||
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "../../abstractions/storage.service";
|
||||
import { GlobalState } from "../global-state";
|
||||
import { KeyDefinition, globalKeyBuilder } from "../key-definition";
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { StorageServiceProvider } from "@bitwarden/storage-core";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
||||
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||
import { UserKeyDefinition } from "../user-key-definition";
|
||||
import { SingleUserState } from "../user-state";
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Observable, combineLatest, of } from "rxjs";
|
||||
|
||||
import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "../../abstractions/storage.service";
|
||||
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||
import { UserKeyDefinition } from "../user-key-definition";
|
||||
import { CombinedState, SingleUserState } from "../user-state";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { StorageServiceProvider } from "@bitwarden/storage-core";
|
||||
|
||||
import { mockAccountServiceWith } from "../../../../spec/fake-account-service";
|
||||
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
||||
import { KeyDefinition } from "../key-definition";
|
||||
import { StateDefinition } from "../state-definition";
|
||||
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||
|
||||
@@ -15,12 +15,10 @@ import {
|
||||
} from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core";
|
||||
|
||||
import { StorageKey } from "../../../types/state";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "../../abstractions/storage.service";
|
||||
import { DebugOptions } from "../key-definition";
|
||||
import { populateOptionsWithDefault, StateUpdateOptions } from "../state-update-options";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AbstractStorageService } from "../../abstractions/storage.service";
|
||||
import { AbstractStorageService } from "@bitwarden/storage-core";
|
||||
|
||||
export async function getStoredValue<T>(
|
||||
key: string,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
StorageServiceProvider,
|
||||
} from "@bitwarden/storage-core";
|
||||
|
||||
import { FakeGlobalStateProvider } from "../../../spec";
|
||||
import { AbstractStorageService, ObservableStorageService } from "../abstractions/storage.service";
|
||||
import { StorageServiceProvider } from "../services/storage-service.provider";
|
||||
|
||||
import { StateDefinition } from "./state-definition";
|
||||
import { STATE_LOCK_EVENT, StateEventRegistrarService } from "./state-event-registrar.service";
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
StorageServiceProvider,
|
||||
} from "@bitwarden/storage-core";
|
||||
|
||||
import { FakeGlobalStateProvider } from "../../../spec";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { AbstractStorageService, ObservableStorageService } from "../abstractions/storage.service";
|
||||
import { StorageServiceProvider } from "../services/storage-service.provider";
|
||||
|
||||
import { STATE_LOCK_EVENT } from "./state-event-registrar.service";
|
||||
import { StateEventRunnerService } from "./state-event-runner.service";
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { StorageServiceProvider } from "@bitwarden/storage-core";
|
||||
|
||||
import { UserId } from "../../types/guid";
|
||||
import { StorageServiceProvider } from "../services/storage-service.provider";
|
||||
|
||||
import { GlobalState } from "./global-state";
|
||||
import { GlobalStateProvider } from "./global-state.provider";
|
||||
|
||||
@@ -97,7 +97,7 @@ import { PaymentResponse } from "../billing/models/response/payment.response";
|
||||
import { PlanResponse } from "../billing/models/response/plan.response";
|
||||
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
||||
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
||||
import { DeviceType } from "../enums";
|
||||
import { ClientType, DeviceType } from "../enums";
|
||||
import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request";
|
||||
import { VaultTimeoutSettingsService } from "../key-management/vault-timeout";
|
||||
@@ -154,8 +154,6 @@ export type HttpOperations = {
|
||||
export class ApiService implements ApiServiceAbstraction {
|
||||
private device: DeviceType;
|
||||
private deviceType: string;
|
||||
private isWebClient = false;
|
||||
private isDesktopClient = false;
|
||||
private refreshTokenPromise: Promise<string> | undefined;
|
||||
|
||||
/**
|
||||
@@ -178,22 +176,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
) {
|
||||
this.device = platformUtilsService.getDevice();
|
||||
this.deviceType = this.device.toString();
|
||||
this.isWebClient =
|
||||
this.device === DeviceType.IEBrowser ||
|
||||
this.device === DeviceType.ChromeBrowser ||
|
||||
this.device === DeviceType.EdgeBrowser ||
|
||||
this.device === DeviceType.FirefoxBrowser ||
|
||||
this.device === DeviceType.OperaBrowser ||
|
||||
this.device === DeviceType.SafariBrowser ||
|
||||
this.device === DeviceType.UnknownBrowser ||
|
||||
this.device === DeviceType.VivaldiBrowser;
|
||||
this.isDesktopClient =
|
||||
this.device === DeviceType.WindowsDesktop ||
|
||||
this.device === DeviceType.MacOsDesktop ||
|
||||
this.device === DeviceType.LinuxDesktop ||
|
||||
this.device === DeviceType.WindowsCLI ||
|
||||
this.device === DeviceType.MacOsCLI ||
|
||||
this.device === DeviceType.LinuxCLI;
|
||||
}
|
||||
|
||||
// Auth APIs
|
||||
@@ -838,7 +820,9 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
// Sync APIs
|
||||
|
||||
async getSync(): Promise<SyncResponse> {
|
||||
const path = this.isDesktopClient || this.isWebClient ? "/sync?excludeDomains=true" : "/sync";
|
||||
const path = !this.platformUtilsService.supportsAutofill()
|
||||
? "/sync?excludeDomains=true"
|
||||
: "/sync";
|
||||
const r = await this.send("GET", path, null, true, true);
|
||||
return new SyncResponse(r);
|
||||
}
|
||||
@@ -1875,7 +1859,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
|
||||
private async getCredentials(): Promise<RequestCredentials> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
if (!this.isWebClient || env.hasBaseUrl()) {
|
||||
if (this.platformUtilsService.getClientType() !== ClientType.Web || env.hasBaseUrl()) {
|
||||
return "include";
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -2,7 +2,9 @@ import { Opaque } from "type-fest";
|
||||
|
||||
export type Guid = Opaque<string, "Guid">;
|
||||
|
||||
export type UserId = Opaque<string, "UserId">;
|
||||
// Convenience re-export of UserId from it's original location, any library that
|
||||
// wants to be lower level than common should instead import it from user-core.
|
||||
export { UserId } from "@bitwarden/user-core";
|
||||
export type OrganizationId = Opaque<string, "OrganizationId">;
|
||||
export type CollectionId = Opaque<string, "CollectionId">;
|
||||
export type ProviderId = Opaque<string, "ProviderId">;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,8 @@ describe("AttachmentView", () => {
|
||||
});
|
||||
|
||||
it("should return an AttachmentView from an SdkAttachmentView", () => {
|
||||
jest.spyOn(SymmetricCryptoKey, "fromString").mockReturnValue("mockKey" as any);
|
||||
|
||||
const sdkAttachmentView = {
|
||||
id: "id",
|
||||
url: "url",
|
||||
@@ -33,6 +35,7 @@ describe("AttachmentView", () => {
|
||||
sizeName: "sizeName",
|
||||
fileName: "fileName",
|
||||
key: "encKeyB64_fromString",
|
||||
decryptedKey: "decryptedKey_B64",
|
||||
} as SdkAttachmentView;
|
||||
|
||||
const result = AttachmentView.fromSdkAttachmentView(sdkAttachmentView);
|
||||
@@ -43,14 +46,20 @@ describe("AttachmentView", () => {
|
||||
size: "size",
|
||||
sizeName: "sizeName",
|
||||
fileName: "fileName",
|
||||
key: null,
|
||||
key: "mockKey",
|
||||
encryptedKey: new EncString(sdkAttachmentView.key as string),
|
||||
});
|
||||
|
||||
expect(SymmetricCryptoKey.fromString).toHaveBeenCalledWith("decryptedKey_B64");
|
||||
});
|
||||
});
|
||||
|
||||
describe("toSdkAttachmentView", () => {
|
||||
it("should convert AttachmentView to SdkAttachmentView", () => {
|
||||
const mockKey = {
|
||||
toBase64: jest.fn().mockReturnValue("keyB64"),
|
||||
} as any;
|
||||
|
||||
const attachmentView = new AttachmentView();
|
||||
attachmentView.id = "id";
|
||||
attachmentView.url = "url";
|
||||
@@ -58,8 +67,10 @@ describe("AttachmentView", () => {
|
||||
attachmentView.sizeName = "sizeName";
|
||||
attachmentView.fileName = "fileName";
|
||||
attachmentView.encryptedKey = new EncString("encKeyB64");
|
||||
attachmentView.key = mockKey;
|
||||
|
||||
const result = attachmentView.toSdkAttachmentView();
|
||||
|
||||
expect(result).toEqual({
|
||||
id: "id",
|
||||
url: "url",
|
||||
@@ -67,6 +78,7 @@ describe("AttachmentView", () => {
|
||||
sizeName: "sizeName",
|
||||
fileName: "fileName",
|
||||
key: "encKeyB64",
|
||||
decryptedKey: "keyB64",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,6 +59,8 @@ export class AttachmentView implements View {
|
||||
sizeName: this.sizeName,
|
||||
fileName: this.fileName,
|
||||
key: this.encryptedKey?.toJSON(),
|
||||
// TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete
|
||||
decryptedKey: this.key ? this.key.toBase64() : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +78,8 @@ export class AttachmentView implements View {
|
||||
view.size = obj.size ?? null;
|
||||
view.sizeName = obj.sizeName ?? null;
|
||||
view.fileName = obj.fileName ?? null;
|
||||
// TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete
|
||||
view.key = obj.key ? SymmetricCryptoKey.fromString(obj.decryptedKey) : null;
|
||||
view.encryptedKey = new EncString(obj.key);
|
||||
|
||||
return view;
|
||||
|
||||
@@ -383,7 +383,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
const decCiphers = await this.getDecryptedCiphers(userId);
|
||||
if (decCiphers != null && decCiphers.length !== 0) {
|
||||
await this.reindexCiphers(userId);
|
||||
return await this.getDecryptedCiphers(userId);
|
||||
return decCiphers;
|
||||
}
|
||||
|
||||
const decrypted = await this.decryptCiphers(await this.getAll(userId), userId);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
CipherType as SdkCipherType,
|
||||
CipherView as SdkCipherView,
|
||||
CipherListView,
|
||||
Attachment as SdkAttachment,
|
||||
AttachmentView as SdkAttachmentView,
|
||||
} from "@bitwarden/sdk-internal";
|
||||
|
||||
import { mockEnc } from "../../../spec";
|
||||
@@ -311,7 +311,9 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
const expectedDecryptedContent = new Uint8Array([5, 6, 7, 8]);
|
||||
|
||||
jest.spyOn(cipher, "toSdkCipher").mockReturnValue({ id: "id" } as SdkCipher);
|
||||
jest.spyOn(attachment, "toSdkAttachmentView").mockReturnValue({ id: "a1" } as SdkAttachment);
|
||||
jest
|
||||
.spyOn(attachment, "toSdkAttachmentView")
|
||||
.mockReturnValue({ id: "a1" } as SdkAttachmentView);
|
||||
mockSdkClient.vault().attachments().decrypt_buffer.mockReturnValue(expectedDecryptedContent);
|
||||
|
||||
const result = await cipherEncryptionService.decryptAttachmentContent(
|
||||
|
||||
@@ -91,7 +91,6 @@ export class RestrictedItemTypesService {
|
||||
* Restriction logic:
|
||||
* - If cipher type is not restricted by any org → allowed
|
||||
* - If cipher belongs to an org that allows this type → allowed
|
||||
* - If cipher is personal vault and any org allows this type → allowed
|
||||
* - Otherwise → restricted
|
||||
*/
|
||||
isCipherRestricted(cipher: CipherLike, restrictedTypes: RestrictedCipherType[]): boolean {
|
||||
@@ -108,8 +107,8 @@ export class RestrictedItemTypesService {
|
||||
return !restriction.allowViewOrgIds.includes(cipher.organizationId);
|
||||
}
|
||||
|
||||
// For personal vault ciphers: restricted only if NO organizations allow this type
|
||||
return restriction.allowViewOrgIds.length === 0;
|
||||
// Cipher is restricted by at least one organization, restrict it
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -365,7 +365,7 @@ describe("Default task service", () => {
|
||||
const subscription = service.listenForTaskNotifications();
|
||||
|
||||
const notification = {
|
||||
type: NotificationType.PendingSecurityTasks,
|
||||
type: NotificationType.RefreshSecurityTasks,
|
||||
} as NotificationResponse;
|
||||
mockNotifications$.next([notification, userId]);
|
||||
|
||||
@@ -390,7 +390,7 @@ describe("Default task service", () => {
|
||||
const subscription = service.listenForTaskNotifications();
|
||||
|
||||
const notification = {
|
||||
type: NotificationType.PendingSecurityTasks,
|
||||
type: NotificationType.RefreshSecurityTasks,
|
||||
} as NotificationResponse;
|
||||
mockNotifications$.next([notification, "other-user-id" as UserId]);
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ export class DefaultTaskService implements TaskService {
|
||||
return this.notificationService.notifications$.pipe(
|
||||
filter(
|
||||
([notification, userId]) =>
|
||||
notification.type === NotificationType.PendingSecurityTasks &&
|
||||
notification.type === NotificationType.RefreshSecurityTasks &&
|
||||
filterByUserIds.includes(userId),
|
||||
),
|
||||
map(([, userId]) => userId),
|
||||
|
||||
22
libs/common/src/vault/utils/get-web-store-url.ts
Normal file
22
libs/common/src/vault/utils/get-web-store-url.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
|
||||
/**
|
||||
* Returns the web store URL for the Bitwarden browser extension based on the device type.
|
||||
* @defaults Bitwarden download page
|
||||
*/
|
||||
export const getWebStoreUrl = (deviceType: DeviceType): string => {
|
||||
switch (deviceType) {
|
||||
case DeviceType.ChromeBrowser:
|
||||
return "https://chromewebstore.google.com/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb";
|
||||
case DeviceType.FirefoxBrowser:
|
||||
return "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/";
|
||||
case DeviceType.SafariBrowser:
|
||||
return "https://apps.apple.com/us/app/bitwarden/id1352778147?mt=12";
|
||||
case DeviceType.OperaBrowser:
|
||||
return "https://addons.opera.com/extensions/details/bitwarden-free-password-manager/";
|
||||
case DeviceType.EdgeBrowser:
|
||||
return "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh";
|
||||
default:
|
||||
return "https://bitwarden.com/download/#downloads-web-browser";
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user