mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 18:33:50 +00:00
Merge branch 'main' into feature/passkey-provider
This commit is contained in:
@@ -163,7 +163,7 @@ export abstract class ApiService {
|
||||
): Promise<
|
||||
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
|
||||
>;
|
||||
abstract refreshIdentityToken(): Promise<any>;
|
||||
abstract refreshIdentityToken(userId?: UserId): Promise<any>;
|
||||
|
||||
abstract getProfile(): Promise<ProfileResponse>;
|
||||
abstract getUserSubscription(): Promise<SubscriptionResponse>;
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("WebAuthnLoginService", () => {
|
||||
|
||||
// We must do this to make the mocked classes available for all the
|
||||
// assertCredential(...) tests.
|
||||
global.PublicKeyCredential = MockPublicKeyCredential;
|
||||
global.PublicKeyCredential = MockPublicKeyCredential as any;
|
||||
global.AuthenticatorAssertionResponse = MockAuthenticatorAssertionResponse;
|
||||
|
||||
// Save the original navigator
|
||||
@@ -316,6 +316,10 @@ class MockPublicKeyCredential implements PublicKeyCredential {
|
||||
static isUserVerifyingPlatformAuthenticatorAvailable(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
function buildCredentialAssertionOptions(): WebAuthnLoginCredentialAssertionOptionsView {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { combineLatest, map, Observable, startWith, switchMap } from "rxjs";
|
||||
import { combineLatest, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
@@ -205,7 +205,7 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti
|
||||
this.showInlineMenuCardsState = this.stateProvider.getActive(SHOW_INLINE_MENU_CARDS);
|
||||
this.showInlineMenuCards$ = combineLatest([
|
||||
this.showInlineMenuCardsState.state$.pipe(map((x) => x ?? true)),
|
||||
this.restrictedItemTypesService.restricted$.pipe(startWith([])),
|
||||
this.restrictedItemTypesService.restricted$,
|
||||
]).pipe(
|
||||
map(
|
||||
([enabled, restrictions]) =>
|
||||
|
||||
@@ -25,6 +25,7 @@ export enum FeatureFlag {
|
||||
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
|
||||
PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover",
|
||||
PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings",
|
||||
PM24996_ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog",
|
||||
|
||||
/* Key Management */
|
||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||
@@ -101,6 +102,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
|
||||
[FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE,
|
||||
[FeatureFlag.PM22415_TaxIDWarnings]: FALSE,
|
||||
[FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog]: FALSE,
|
||||
|
||||
/* Key Management */
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
|
||||
@@ -17,13 +17,13 @@ const CanLaunchWhitelist = [
|
||||
];
|
||||
|
||||
export class SafeUrls {
|
||||
static canLaunch(uri: string): boolean {
|
||||
static canLaunch(uri: string | null | undefined): boolean {
|
||||
if (Utils.isNullOrWhitespace(uri)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < CanLaunchWhitelist.length; i++) {
|
||||
if (uri.indexOf(CanLaunchWhitelist[i]) === 0) {
|
||||
if (uri!.indexOf(CanLaunchWhitelist[i]) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static getDomain(uriString: string): string {
|
||||
static getDomain(uriString: string | null | undefined): string {
|
||||
if (Utils.isNullOrWhitespace(uriString)) {
|
||||
return null;
|
||||
}
|
||||
@@ -457,7 +457,7 @@ export class Utils {
|
||||
return str == null || typeof str !== "string" || str.trim() === "";
|
||||
}
|
||||
|
||||
static isNullOrEmpty(str: string | null): boolean {
|
||||
static isNullOrEmpty(str: string | null | undefined): boolean {
|
||||
return str == null || typeof str !== "string" || str == "";
|
||||
}
|
||||
|
||||
@@ -479,7 +479,7 @@ export class Utils {
|
||||
return (Object.keys(obj).filter((k) => Number.isNaN(+k)) as K[]).map((k) => obj[k]);
|
||||
}
|
||||
|
||||
static getUrl(uriString: string): URL {
|
||||
static getUrl(uriString: string | undefined | null): URL {
|
||||
if (this.isNullOrWhitespace(uriString)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ export async function getCredentialsForAutofill(
|
||||
cipherId: cipher.id,
|
||||
credentialId: credId,
|
||||
rpId: credential.rpId,
|
||||
userHandle: credential.userHandle,
|
||||
userName: credential.userName,
|
||||
};
|
||||
userHandle: credential.userHandle!,
|
||||
userName: credential.userName!,
|
||||
} satisfies Fido2CredentialAutofillView;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-ma
|
||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import {
|
||||
ObservableTracker,
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
ObservableTracker,
|
||||
} from "../../../../spec";
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { EncryptedString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { UserId } from "../../../types/guid";
|
||||
@@ -46,6 +47,7 @@ describe("DefaultSdkService", () => {
|
||||
let service!: DefaultSdkService;
|
||||
let accountService!: FakeAccountService;
|
||||
let fakeStateProvider!: FakeStateProvider;
|
||||
let apiService!: MockProxy<ApiService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await new TestSdkLoadService().loadAndInit();
|
||||
@@ -55,6 +57,7 @@ describe("DefaultSdkService", () => {
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
keyService = mock<KeyService>();
|
||||
apiService = mock<ApiService>();
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
fakeStateProvider = new FakeStateProvider(accountService);
|
||||
@@ -72,6 +75,7 @@ describe("DefaultSdkService", () => {
|
||||
accountService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
apiService,
|
||||
fakeStateProvider,
|
||||
configService,
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
UnsignedSharedKey,
|
||||
} from "@bitwarden/sdk-internal";
|
||||
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { DeviceType } from "../../../enums/device-type.enum";
|
||||
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
|
||||
@@ -43,7 +44,7 @@ import { StateProvider } from "../../state";
|
||||
|
||||
import { initializeState } from "./client-managed-state";
|
||||
|
||||
// A symbol that represents an overriden client that is explicitly set to undefined,
|
||||
// A symbol that represents an overridden client that is explicitly set to undefined,
|
||||
// blocking the creation of an internal client for that user.
|
||||
const UnsetClient = Symbol("UnsetClient");
|
||||
|
||||
@@ -51,10 +52,17 @@ const UnsetClient = Symbol("UnsetClient");
|
||||
* A token provider that exposes the access token to the SDK.
|
||||
*/
|
||||
class JsTokenProvider implements TokenProvider {
|
||||
constructor() {}
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private userId?: UserId,
|
||||
) {}
|
||||
|
||||
async get_access_token(): Promise<string | undefined> {
|
||||
return undefined;
|
||||
if (this.userId == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await this.apiService.getActiveBearerToken(this.userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +76,10 @@ export class DefaultSdkService implements SdkService {
|
||||
concatMap(async (env) => {
|
||||
await SdkLoadService.Ready;
|
||||
const settings = this.toSettings(env);
|
||||
const client = await this.sdkClientFactory.createSdkClient(new JsTokenProvider(), settings);
|
||||
const client = await this.sdkClientFactory.createSdkClient(
|
||||
new JsTokenProvider(this.apiService),
|
||||
settings,
|
||||
);
|
||||
await this.loadFeatureFlags(client);
|
||||
return client;
|
||||
}),
|
||||
@@ -87,6 +98,7 @@ export class DefaultSdkService implements SdkService {
|
||||
private accountService: AccountService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private keyService: KeyService,
|
||||
private apiService: ApiService,
|
||||
private stateProvider: StateProvider,
|
||||
private configService: ConfigService,
|
||||
private userAgent: string | null = null,
|
||||
@@ -173,7 +185,7 @@ export class DefaultSdkService implements SdkService {
|
||||
|
||||
const settings = this.toSettings(env);
|
||||
const client = await this.sdkClientFactory.createSdkClient(
|
||||
new JsTokenProvider(),
|
||||
new JsTokenProvider(this.apiService, userId),
|
||||
settings,
|
||||
);
|
||||
|
||||
|
||||
@@ -329,10 +329,8 @@ describe("DefaultSyncService", () => {
|
||||
// Mock the value of this observable because it's used in `syncProfile`. Without it, the test breaks.
|
||||
keyConnectorService.convertAccountRequired$ = of(false);
|
||||
|
||||
// Baseline date/time to compare sync time to, in order to avoid needing to use some kind of fake date provider.
|
||||
const beforeSync = Date.now();
|
||||
jest.useFakeTimers({ now: Date.now() });
|
||||
|
||||
// send it!
|
||||
await sut.fullSync(true, defaultSyncOptions);
|
||||
|
||||
expectUpdateCallCount(mockUserState, 1);
|
||||
@@ -340,9 +338,10 @@ describe("DefaultSyncService", () => {
|
||||
const updateCall = mockUserState.update.mock.calls[0];
|
||||
// Get the first argument to update(...) -- this will be the date callback that returns the date of the last successful sync
|
||||
const dateCallback = updateCall[0];
|
||||
const actualTime = dateCallback() as Date;
|
||||
const actualDate = dateCallback() as Date;
|
||||
|
||||
expect(Math.abs(actualTime.getTime() - beforeSync)).toBeLessThan(1);
|
||||
expect(actualDate.getTime()).toEqual(jest.now());
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("updates last sync time when no sync is necessary", async () => {
|
||||
|
||||
@@ -47,6 +47,7 @@ export class Attachment extends Domain {
|
||||
): Promise<AttachmentView> {
|
||||
const view = await this.decryptObj<Attachment, AttachmentView>(
|
||||
this,
|
||||
// @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now.
|
||||
new AttachmentView(this),
|
||||
["fileName"],
|
||||
orgId,
|
||||
|
||||
@@ -63,7 +63,6 @@ describe("Card", () => {
|
||||
expect(view).toEqual({
|
||||
_brand: "brand",
|
||||
_number: "number",
|
||||
_subTitle: null,
|
||||
cardholderName: "cardHolder",
|
||||
code: "code",
|
||||
expMonth: "expMonth",
|
||||
|
||||
@@ -161,6 +161,8 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
|
||||
await this.decryptObj<Cipher, CipherView>(
|
||||
this,
|
||||
// @ts-expect-error Ciphers have optional Ids which are getting swallowed by the ViewEncryptableKeys type
|
||||
// The ViewEncryptableKeys type should be fixed to allow for optional Ids, but is out of scope for now.
|
||||
model,
|
||||
["name", "notes"],
|
||||
this.organizationId,
|
||||
|
||||
@@ -56,6 +56,7 @@ export class Fido2Credential extends Domain {
|
||||
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<Fido2CredentialView> {
|
||||
const view = await this.decryptObj<Fido2Credential, Fido2CredentialView>(
|
||||
this,
|
||||
// @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now.
|
||||
new Fido2CredentialView(),
|
||||
[
|
||||
"credentialId",
|
||||
|
||||
@@ -39,6 +39,7 @@ export class Field extends Domain {
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<FieldView> {
|
||||
return this.decryptObj<Field, FieldView>(
|
||||
this,
|
||||
// @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now.
|
||||
new FieldView(this),
|
||||
["name", "value"],
|
||||
orgId,
|
||||
|
||||
@@ -112,7 +112,6 @@ describe("Identity", () => {
|
||||
expect(view).toEqual({
|
||||
_firstName: "mockFirstName",
|
||||
_lastName: "mockLastName",
|
||||
_subTitle: null,
|
||||
address1: "mockAddress1",
|
||||
address2: "mockAddress2",
|
||||
address3: "mockAddress3",
|
||||
|
||||
@@ -56,10 +56,6 @@ describe("LoginUri", () => {
|
||||
const view = await loginUri.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
_canLaunch: null,
|
||||
_domain: null,
|
||||
_host: null,
|
||||
_hostname: null,
|
||||
_uri: "uri",
|
||||
match: 3,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { mockEnc, mockFromJson } from "../../../../spec";
|
||||
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||
import { UriMatchStrategy } from "../../../models/domain/domain-service";
|
||||
import { LoginData } from "../../models/data/login.data";
|
||||
import { Login } from "../../models/domain/login";
|
||||
import { LoginUri } from "../../models/domain/login-uri";
|
||||
@@ -82,12 +82,7 @@ describe("Login DTO", () => {
|
||||
totp: "encrypted totp",
|
||||
uris: [
|
||||
{
|
||||
match: null as UriMatchStrategySetting,
|
||||
_uri: "decrypted uri",
|
||||
_domain: null as string,
|
||||
_hostname: null as string,
|
||||
_host: null as string,
|
||||
_canLaunch: null as boolean,
|
||||
},
|
||||
],
|
||||
autofillOnPageLoad: true,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal";
|
||||
@@ -10,12 +8,12 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
|
||||
import { Attachment } from "../domain/attachment";
|
||||
|
||||
export class AttachmentView implements View {
|
||||
id: string = null;
|
||||
url: string = null;
|
||||
size: string = null;
|
||||
sizeName: string = null;
|
||||
fileName: string = null;
|
||||
key: SymmetricCryptoKey = null;
|
||||
id?: string;
|
||||
url?: string;
|
||||
size?: string;
|
||||
sizeName?: string;
|
||||
fileName?: string;
|
||||
key?: SymmetricCryptoKey;
|
||||
/**
|
||||
* The SDK returns an encrypted key for the attachment.
|
||||
*/
|
||||
@@ -35,7 +33,7 @@ export class AttachmentView implements View {
|
||||
get fileSize(): number {
|
||||
try {
|
||||
if (this.size != null) {
|
||||
return parseInt(this.size, null);
|
||||
return parseInt(this.size);
|
||||
}
|
||||
} catch {
|
||||
// Invalid file size.
|
||||
@@ -71,7 +69,7 @@ export class AttachmentView implements View {
|
||||
fileName: this.fileName,
|
||||
key: this.encryptedKey?.toSdk(),
|
||||
// TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete
|
||||
decryptedKey: this.key ? this.key.toBase64() : null,
|
||||
decryptedKey: this.key ? this.key.toBase64() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,13 +82,13 @@ export class AttachmentView implements View {
|
||||
}
|
||||
|
||||
const view = new AttachmentView();
|
||||
view.id = obj.id ?? null;
|
||||
view.url = obj.url ?? null;
|
||||
view.size = obj.size ?? null;
|
||||
view.sizeName = obj.sizeName ?? null;
|
||||
view.fileName = obj.fileName ?? null;
|
||||
view.id = obj.id;
|
||||
view.url = obj.url;
|
||||
view.size = obj.size;
|
||||
view.sizeName = obj.sizeName;
|
||||
view.fileName = obj.fileName;
|
||||
// TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete
|
||||
view.key = obj.decryptedKey ? SymmetricCryptoKey.fromString(obj.decryptedKey) : null;
|
||||
view.key = obj.decryptedKey ? SymmetricCryptoKey.fromString(obj.decryptedKey) : undefined;
|
||||
view.encryptedKey = obj.key ? new EncString(obj.key) : undefined;
|
||||
|
||||
return view;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { CardView as SdkCardView } from "@bitwarden/sdk-internal";
|
||||
@@ -12,45 +10,45 @@ import { ItemView } from "./item.view";
|
||||
|
||||
export class CardView extends ItemView implements SdkCardView {
|
||||
@linkedFieldOption(LinkedId.CardholderName, { sortPosition: 0 })
|
||||
cardholderName: string = null;
|
||||
cardholderName: string | undefined;
|
||||
@linkedFieldOption(LinkedId.ExpMonth, { sortPosition: 3, i18nKey: "expirationMonth" })
|
||||
expMonth: string = null;
|
||||
expMonth: string | undefined;
|
||||
@linkedFieldOption(LinkedId.ExpYear, { sortPosition: 4, i18nKey: "expirationYear" })
|
||||
expYear: string = null;
|
||||
expYear: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Code, { sortPosition: 5, i18nKey: "securityCode" })
|
||||
code: string = null;
|
||||
code: string | undefined;
|
||||
|
||||
private _brand: string = null;
|
||||
private _number: string = null;
|
||||
private _subTitle: string = null;
|
||||
private _brand?: string;
|
||||
private _number?: string;
|
||||
private _subTitle?: string;
|
||||
|
||||
get maskedCode(): string {
|
||||
return this.code != null ? "•".repeat(this.code.length) : null;
|
||||
get maskedCode(): string | undefined {
|
||||
return this.code != null ? "•".repeat(this.code.length) : undefined;
|
||||
}
|
||||
|
||||
get maskedNumber(): string {
|
||||
return this.number != null ? "•".repeat(this.number.length) : null;
|
||||
get maskedNumber(): string | undefined {
|
||||
return this.number != null ? "•".repeat(this.number.length) : undefined;
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.Brand, { sortPosition: 2 })
|
||||
get brand(): string {
|
||||
get brand(): string | undefined {
|
||||
return this._brand;
|
||||
}
|
||||
set brand(value: string) {
|
||||
set brand(value: string | undefined) {
|
||||
this._brand = value;
|
||||
this._subTitle = null;
|
||||
this._subTitle = undefined;
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.Number, { sortPosition: 1 })
|
||||
get number(): string {
|
||||
get number(): string | undefined {
|
||||
return this._number;
|
||||
}
|
||||
set number(value: string) {
|
||||
set number(value: string | undefined) {
|
||||
this._number = value;
|
||||
this._subTitle = null;
|
||||
this._subTitle = undefined;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
get subTitle(): string | undefined {
|
||||
if (this._subTitle == null) {
|
||||
this._subTitle = this.brand;
|
||||
if (this.number != null && this.number.length >= 4) {
|
||||
@@ -69,11 +67,11 @@ export class CardView extends ItemView implements SdkCardView {
|
||||
return this._subTitle;
|
||||
}
|
||||
|
||||
get expiration(): string {
|
||||
const normalizedYear = normalizeExpiryYearFormat(this.expYear);
|
||||
get expiration(): string | undefined {
|
||||
const normalizedYear = this.expYear ? normalizeExpiryYearFormat(this.expYear) : undefined;
|
||||
|
||||
if (!this.expMonth && !normalizedYear) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let exp = this.expMonth != null ? ("0" + this.expMonth).slice(-2) : "__";
|
||||
@@ -82,14 +80,14 @@ export class CardView extends ItemView implements SdkCardView {
|
||||
return exp;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<CardView>>): CardView {
|
||||
static fromJSON(obj: Partial<Jsonify<CardView>> | undefined): CardView {
|
||||
return Object.assign(new CardView(), obj);
|
||||
}
|
||||
|
||||
// ref https://stackoverflow.com/a/5911300
|
||||
static getCardBrandByPatterns(cardNum: string): string {
|
||||
static getCardBrandByPatterns(cardNum: string | undefined | null): string | undefined {
|
||||
if (cardNum == null || typeof cardNum !== "string" || cardNum.trim() === "") {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Visa
|
||||
@@ -146,25 +144,21 @@ export class CardView extends ItemView implements SdkCardView {
|
||||
return "Visa";
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an SDK CardView to a CardView.
|
||||
*/
|
||||
static fromSdkCardView(obj: SdkCardView): CardView | undefined {
|
||||
if (obj == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromSdkCardView(obj: SdkCardView): CardView {
|
||||
const cardView = new CardView();
|
||||
|
||||
cardView.cardholderName = obj.cardholderName ?? null;
|
||||
cardView.brand = obj.brand ?? null;
|
||||
cardView.number = obj.number ?? null;
|
||||
cardView.expMonth = obj.expMonth ?? null;
|
||||
cardView.expYear = obj.expYear ?? null;
|
||||
cardView.code = obj.code ?? null;
|
||||
cardView.cardholderName = obj.cardholderName;
|
||||
cardView.brand = obj.brand;
|
||||
cardView.number = obj.number;
|
||||
cardView.expMonth = obj.expMonth;
|
||||
cardView.expYear = obj.expYear;
|
||||
cardView.code = obj.code;
|
||||
|
||||
return cardView;
|
||||
}
|
||||
|
||||
@@ -109,6 +109,72 @@ describe("CipherView", () => {
|
||||
expect(actual.key).toBeInstanceOf(EncString);
|
||||
expect(actual.key?.toJSON()).toBe(cipherKeyObject.toJSON());
|
||||
});
|
||||
|
||||
it("fromJSON should always restore top-level CipherView properties", () => {
|
||||
jest.spyOn(LoginView, "fromJSON").mockImplementation(mockFromJson);
|
||||
// Create a fully populated CipherView instance
|
||||
const original = new CipherView();
|
||||
original.id = "test-id";
|
||||
original.organizationId = "org-id";
|
||||
original.folderId = "folder-id";
|
||||
original.name = "test-name";
|
||||
original.notes = "test-notes";
|
||||
original.type = CipherType.Login;
|
||||
original.favorite = true;
|
||||
original.organizationUseTotp = true;
|
||||
original.permissions = new CipherPermissionsApi();
|
||||
original.edit = true;
|
||||
original.viewPassword = false;
|
||||
original.localData = { lastUsedDate: Date.now() };
|
||||
original.login = new LoginView();
|
||||
original.identity = new IdentityView();
|
||||
original.card = new CardView();
|
||||
original.secureNote = new SecureNoteView();
|
||||
original.sshKey = new SshKeyView();
|
||||
original.attachments = [];
|
||||
original.fields = [];
|
||||
original.passwordHistory = [];
|
||||
original.collectionIds = ["collection-1"];
|
||||
original.revisionDate = new Date("2022-01-01");
|
||||
original.creationDate = new Date("2022-01-02");
|
||||
original.deletedDate = new Date("2022-01-03");
|
||||
original.archivedDate = new Date("2022-01-04");
|
||||
original.reprompt = CipherRepromptType.Password;
|
||||
original.key = new EncString("test-key");
|
||||
original.decryptionFailure = true;
|
||||
|
||||
// Serialize and deserialize
|
||||
const json = original.toJSON();
|
||||
const restored = CipherView.fromJSON(json as any);
|
||||
|
||||
// Get all enumerable properties from the original instance
|
||||
const originalProps = Object.keys(original);
|
||||
|
||||
// Check that all properties exist on the restored instance
|
||||
for (const prop of originalProps) {
|
||||
try {
|
||||
expect(restored).toHaveProperty(prop);
|
||||
} catch {
|
||||
throw new Error(`Property '${prop}' is missing from restored instance`);
|
||||
}
|
||||
|
||||
// For non-function, non-getter properties, verify the value is defined
|
||||
const descriptor = Object.getOwnPropertyDescriptor(CipherView.prototype, prop);
|
||||
if (!descriptor?.get && typeof (original as any)[prop] !== "function") {
|
||||
try {
|
||||
expect((restored as any)[prop]).toBeDefined();
|
||||
} catch {
|
||||
throw new Error(`Property '${prop}' is undefined in restored instance`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify restored instance has the same properties as original
|
||||
const restoredProps = Object.keys(restored!).sort();
|
||||
const sortedOriginalProps = originalProps.sort();
|
||||
|
||||
expect(restoredProps).toEqual(sortedOriginalProps);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromSdkCipherView", () => {
|
||||
@@ -180,15 +246,12 @@ describe("CipherView", () => {
|
||||
folderId: "folderId",
|
||||
collectionIds: ["collectionId"],
|
||||
name: "name",
|
||||
notes: null,
|
||||
type: CipherType.Login,
|
||||
favorite: true,
|
||||
edit: true,
|
||||
reprompt: CipherRepromptType.None,
|
||||
organizationUseTotp: false,
|
||||
viewPassword: true,
|
||||
localData: undefined,
|
||||
permissions: undefined,
|
||||
attachments: [
|
||||
{
|
||||
id: "attachmentId",
|
||||
@@ -224,7 +287,6 @@ describe("CipherView", () => {
|
||||
passwordHistory: [],
|
||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||
revisionDate: new Date("2022-01-02T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -283,18 +345,12 @@ describe("CipherView", () => {
|
||||
restore: true,
|
||||
delete: true,
|
||||
},
|
||||
deletedDate: undefined,
|
||||
creationDate: "2022-01-02T12:00:00.000Z",
|
||||
revisionDate: "2022-01-02T12:00:00.000Z",
|
||||
attachments: [],
|
||||
passwordHistory: [],
|
||||
login: undefined,
|
||||
identity: undefined,
|
||||
card: undefined,
|
||||
secureNote: undefined,
|
||||
sshKey: undefined,
|
||||
fields: [],
|
||||
} as SdkCipherView);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { asUuid, uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { ItemView } from "@bitwarden/common/vault/models/view/item.view";
|
||||
import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { View } from "../../../models/view/view";
|
||||
@@ -26,18 +25,18 @@ import { SshKeyView } from "./ssh-key.view";
|
||||
export class CipherView implements View, InitializerMetadata {
|
||||
readonly initializerKey = InitializerKey.CipherView;
|
||||
|
||||
id: string = null;
|
||||
organizationId: string | undefined = null;
|
||||
folderId: string = null;
|
||||
name: string = null;
|
||||
notes: string = null;
|
||||
type: CipherType = null;
|
||||
id: string = "";
|
||||
organizationId?: string;
|
||||
folderId?: string;
|
||||
name: string = "";
|
||||
notes?: string;
|
||||
type: CipherType = CipherType.Login;
|
||||
favorite = false;
|
||||
organizationUseTotp = false;
|
||||
permissions: CipherPermissionsApi = new CipherPermissionsApi();
|
||||
permissions?: CipherPermissionsApi = new CipherPermissionsApi();
|
||||
edit = false;
|
||||
viewPassword = true;
|
||||
localData: LocalData;
|
||||
localData?: LocalData;
|
||||
login = new LoginView();
|
||||
identity = new IdentityView();
|
||||
card = new CardView();
|
||||
@@ -46,11 +45,11 @@ export class CipherView implements View, InitializerMetadata {
|
||||
attachments: AttachmentView[] = [];
|
||||
fields: FieldView[] = [];
|
||||
passwordHistory: PasswordHistoryView[] = [];
|
||||
collectionIds: string[] = null;
|
||||
revisionDate: Date = null;
|
||||
creationDate: Date = null;
|
||||
deletedDate: Date | null = null;
|
||||
archivedDate: Date | null = null;
|
||||
collectionIds: string[] = [];
|
||||
revisionDate: Date;
|
||||
creationDate: Date;
|
||||
deletedDate?: Date;
|
||||
archivedDate?: Date;
|
||||
reprompt: CipherRepromptType = CipherRepromptType.None;
|
||||
// We need a copy of the encrypted key so we can pass it to
|
||||
// the SdkCipherView during encryption
|
||||
@@ -63,6 +62,7 @@ export class CipherView implements View, InitializerMetadata {
|
||||
|
||||
constructor(c?: Cipher) {
|
||||
if (!c) {
|
||||
this.creationDate = this.revisionDate = new Date();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export class CipherView implements View, InitializerMetadata {
|
||||
this.key = c.key;
|
||||
}
|
||||
|
||||
private get item() {
|
||||
private get item(): ItemView | undefined {
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
return this.login;
|
||||
@@ -102,10 +102,10 @@ export class CipherView implements View, InitializerMetadata {
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
get subTitle(): string | undefined {
|
||||
return this.item?.subTitle;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export class CipherView implements View, InitializerMetadata {
|
||||
}
|
||||
|
||||
get hasAttachments(): boolean {
|
||||
return this.attachments && this.attachments.length > 0;
|
||||
return !!this.attachments && this.attachments.length > 0;
|
||||
}
|
||||
|
||||
get hasOldAttachments(): boolean {
|
||||
@@ -132,11 +132,11 @@ export class CipherView implements View, InitializerMetadata {
|
||||
return this.fields && this.fields.length > 0;
|
||||
}
|
||||
|
||||
get passwordRevisionDisplayDate(): Date {
|
||||
get passwordRevisionDisplayDate(): Date | undefined {
|
||||
if (this.type !== CipherType.Login || this.login == null) {
|
||||
return null;
|
||||
return undefined;
|
||||
} else if (this.login.password == null || this.login.password === "") {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
return this.login.passwordRevisionDate;
|
||||
}
|
||||
@@ -170,23 +170,17 @@ export class CipherView implements View, InitializerMetadata {
|
||||
* Determines if the cipher can be launched in a new browser tab.
|
||||
*/
|
||||
get canLaunch(): boolean {
|
||||
return this.type === CipherType.Login && this.login.canLaunch;
|
||||
return this.type === CipherType.Login && this.login!.canLaunch;
|
||||
}
|
||||
|
||||
linkedFieldValue(id: LinkedIdType) {
|
||||
const linkedFieldOption = this.linkedFieldOptions?.get(id);
|
||||
if (linkedFieldOption == null) {
|
||||
return null;
|
||||
const item = this.item;
|
||||
if (linkedFieldOption == null || item == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const item = this.item;
|
||||
return this.item[linkedFieldOption.propertyKey as keyof typeof item];
|
||||
}
|
||||
|
||||
linkedFieldI18nKey(id: LinkedIdType): string {
|
||||
return this.linkedFieldOptions.get(id)?.i18nKey;
|
||||
return item[linkedFieldOption.propertyKey as keyof typeof item];
|
||||
}
|
||||
|
||||
// This is used as a marker to indicate that the cipher view object still has its prototype
|
||||
@@ -194,23 +188,42 @@ export class CipherView implements View, InitializerMetadata {
|
||||
return this;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<DeepJsonify<CipherView>>): CipherView {
|
||||
static fromJSON(obj: Partial<DeepJsonify<CipherView>>): CipherView | null {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const view = new CipherView();
|
||||
const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
||||
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||
const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
||||
const archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate);
|
||||
const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a));
|
||||
const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f));
|
||||
const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph));
|
||||
const permissions = CipherPermissionsApi.fromJSON(obj.permissions);
|
||||
let key: EncString | undefined;
|
||||
view.type = obj.type ?? CipherType.Login;
|
||||
view.id = obj.id ?? "";
|
||||
view.organizationId = obj.organizationId ?? undefined;
|
||||
view.folderId = obj.folderId ?? undefined;
|
||||
view.collectionIds = obj.collectionIds ?? [];
|
||||
view.name = obj.name ?? "";
|
||||
view.notes = obj.notes;
|
||||
view.edit = obj.edit ?? false;
|
||||
view.viewPassword = obj.viewPassword ?? true;
|
||||
view.favorite = obj.favorite ?? false;
|
||||
view.organizationUseTotp = obj.organizationUseTotp ?? false;
|
||||
view.localData = obj.localData ? obj.localData : undefined;
|
||||
view.permissions = obj.permissions ? CipherPermissionsApi.fromJSON(obj.permissions) : undefined;
|
||||
view.reprompt = obj.reprompt ?? CipherRepromptType.None;
|
||||
view.decryptionFailure = obj.decryptionFailure ?? false;
|
||||
if (obj.creationDate) {
|
||||
view.creationDate = new Date(obj.creationDate);
|
||||
}
|
||||
if (obj.revisionDate) {
|
||||
view.revisionDate = new Date(obj.revisionDate);
|
||||
}
|
||||
view.deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate);
|
||||
view.archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate);
|
||||
view.attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a)) ?? [];
|
||||
view.fields = obj.fields?.map((f: any) => FieldView.fromJSON(f)) ?? [];
|
||||
view.passwordHistory =
|
||||
obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph)) ?? [];
|
||||
|
||||
if (obj.key != null) {
|
||||
let key: EncString | undefined;
|
||||
if (typeof obj.key === "string") {
|
||||
// If the key is a string, we need to parse it as EncString
|
||||
key = EncString.fromJSON(obj.key);
|
||||
@@ -218,20 +231,9 @@ export class CipherView implements View, InitializerMetadata {
|
||||
// If the key is already an EncString instance, we can use it directly
|
||||
key = obj.key;
|
||||
}
|
||||
view.key = key;
|
||||
}
|
||||
|
||||
Object.assign(view, obj, {
|
||||
creationDate: creationDate,
|
||||
revisionDate: revisionDate,
|
||||
deletedDate: deletedDate,
|
||||
archivedDate: archivedDate,
|
||||
attachments: attachments,
|
||||
fields: fields,
|
||||
passwordHistory: passwordHistory,
|
||||
permissions: permissions,
|
||||
key: key,
|
||||
});
|
||||
|
||||
switch (obj.type) {
|
||||
case CipherType.Card:
|
||||
view.card = CardView.fromJSON(obj.card);
|
||||
@@ -264,46 +266,54 @@ export class CipherView implements View, InitializerMetadata {
|
||||
}
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = uuidAsString(obj.id) ?? null;
|
||||
cipherView.organizationId = uuidAsString(obj.organizationId) ?? null;
|
||||
cipherView.folderId = uuidAsString(obj.folderId) ?? null;
|
||||
cipherView.id = uuidAsString(obj.id);
|
||||
cipherView.organizationId = uuidAsString(obj.organizationId);
|
||||
cipherView.folderId = uuidAsString(obj.folderId);
|
||||
cipherView.name = obj.name;
|
||||
cipherView.notes = obj.notes ?? null;
|
||||
cipherView.notes = obj.notes;
|
||||
cipherView.type = obj.type;
|
||||
cipherView.favorite = obj.favorite;
|
||||
cipherView.organizationUseTotp = obj.organizationUseTotp;
|
||||
cipherView.permissions = CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions);
|
||||
cipherView.permissions = obj.permissions
|
||||
? CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions)
|
||||
: undefined;
|
||||
cipherView.edit = obj.edit;
|
||||
cipherView.viewPassword = obj.viewPassword;
|
||||
cipherView.localData = fromSdkLocalData(obj.localData);
|
||||
cipherView.attachments =
|
||||
obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? [];
|
||||
cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? [];
|
||||
obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)!) ?? [];
|
||||
cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)!) ?? [];
|
||||
cipherView.passwordHistory =
|
||||
obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? [];
|
||||
obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)!) ?? [];
|
||||
cipherView.collectionIds = obj.collectionIds?.map((i) => uuidAsString(i)) ?? [];
|
||||
cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||
cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
||||
cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
||||
cipherView.archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate);
|
||||
cipherView.revisionDate = new Date(obj.revisionDate);
|
||||
cipherView.creationDate = new Date(obj.creationDate);
|
||||
cipherView.deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate);
|
||||
cipherView.archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate);
|
||||
cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None;
|
||||
cipherView.key = EncString.fromJSON(obj.key);
|
||||
cipherView.key = obj.key ? EncString.fromJSON(obj.key) : undefined;
|
||||
|
||||
switch (obj.type) {
|
||||
case CipherType.Card:
|
||||
cipherView.card = CardView.fromSdkCardView(obj.card);
|
||||
cipherView.card = obj.card ? CardView.fromSdkCardView(obj.card) : new CardView();
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
cipherView.identity = IdentityView.fromSdkIdentityView(obj.identity);
|
||||
cipherView.identity = obj.identity
|
||||
? IdentityView.fromSdkIdentityView(obj.identity)
|
||||
: new IdentityView();
|
||||
break;
|
||||
case CipherType.Login:
|
||||
cipherView.login = LoginView.fromSdkLoginView(obj.login);
|
||||
cipherView.login = obj.login ? LoginView.fromSdkLoginView(obj.login) : new LoginView();
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
cipherView.secureNote = SecureNoteView.fromSdkSecureNoteView(obj.secureNote);
|
||||
cipherView.secureNote = obj.secureNote
|
||||
? SecureNoteView.fromSdkSecureNoteView(obj.secureNote)
|
||||
: new SecureNoteView();
|
||||
break;
|
||||
case CipherType.SshKey:
|
||||
cipherView.sshKey = SshKeyView.fromSdkSshKeyView(obj.sshKey);
|
||||
cipherView.sshKey = obj.sshKey
|
||||
? SshKeyView.fromSdkSshKeyView(obj.sshKey)
|
||||
: new SshKeyView();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -354,19 +364,19 @@ export class CipherView implements View, InitializerMetadata {
|
||||
|
||||
switch (this.type) {
|
||||
case CipherType.Card:
|
||||
sdkCipherView.card = this.card.toSdkCardView();
|
||||
sdkCipherView.card = this.card?.toSdkCardView();
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
sdkCipherView.identity = this.identity.toSdkIdentityView();
|
||||
sdkCipherView.identity = this.identity?.toSdkIdentityView();
|
||||
break;
|
||||
case CipherType.Login:
|
||||
sdkCipherView.login = this.login.toSdkLoginView();
|
||||
sdkCipherView.login = this.login?.toSdkLoginView();
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
sdkCipherView.secureNote = this.secureNote.toSdkSecureNoteView();
|
||||
sdkCipherView.secureNote = this.secureNote?.toSdkSecureNoteView();
|
||||
break;
|
||||
case CipherType.SshKey:
|
||||
sdkCipherView.sshKey = this.sshKey.toSdkSshKeyView();
|
||||
sdkCipherView.sshKey = this.sshKey?.toSdkSshKeyView();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
@@ -10,21 +8,55 @@ import {
|
||||
import { ItemView } from "./item.view";
|
||||
|
||||
export class Fido2CredentialView extends ItemView {
|
||||
credentialId: string;
|
||||
keyType: "public-key";
|
||||
keyAlgorithm: "ECDSA";
|
||||
keyCurve: "P-256";
|
||||
keyValue: string;
|
||||
rpId: string;
|
||||
userHandle: string;
|
||||
userName: string;
|
||||
counter: number;
|
||||
rpName: string;
|
||||
userDisplayName: string;
|
||||
discoverable: boolean;
|
||||
creationDate: Date = null;
|
||||
credentialId!: string;
|
||||
keyType!: "public-key";
|
||||
keyAlgorithm!: "ECDSA";
|
||||
keyCurve!: "P-256";
|
||||
keyValue!: string;
|
||||
rpId!: string;
|
||||
userHandle?: string;
|
||||
userName?: string;
|
||||
counter!: number;
|
||||
rpName?: string;
|
||||
userDisplayName?: string;
|
||||
discoverable: boolean = false;
|
||||
creationDate!: Date;
|
||||
|
||||
get subTitle(): string {
|
||||
constructor(f?: {
|
||||
credentialId: string;
|
||||
keyType: "public-key";
|
||||
keyAlgorithm: "ECDSA";
|
||||
keyCurve: "P-256";
|
||||
keyValue: string;
|
||||
rpId: string;
|
||||
userHandle?: string;
|
||||
userName?: string;
|
||||
counter: number;
|
||||
rpName?: string;
|
||||
userDisplayName?: string;
|
||||
discoverable?: boolean;
|
||||
creationDate: Date;
|
||||
}) {
|
||||
super();
|
||||
if (f == null) {
|
||||
return;
|
||||
}
|
||||
this.credentialId = f.credentialId;
|
||||
this.keyType = f.keyType;
|
||||
this.keyAlgorithm = f.keyAlgorithm;
|
||||
this.keyCurve = f.keyCurve;
|
||||
this.keyValue = f.keyValue;
|
||||
this.rpId = f.rpId;
|
||||
this.userHandle = f.userHandle;
|
||||
this.userName = f.userName;
|
||||
this.counter = f.counter;
|
||||
this.rpName = f.rpName;
|
||||
this.userDisplayName = f.userDisplayName;
|
||||
this.discoverable = f.discoverable ?? false;
|
||||
this.creationDate = f.creationDate;
|
||||
}
|
||||
|
||||
get subTitle(): string | undefined {
|
||||
return this.userDisplayName;
|
||||
}
|
||||
|
||||
@@ -43,21 +75,21 @@ export class Fido2CredentialView extends ItemView {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const view = new Fido2CredentialView();
|
||||
view.credentialId = obj.credentialId;
|
||||
view.keyType = obj.keyType as "public-key";
|
||||
view.keyAlgorithm = obj.keyAlgorithm as "ECDSA";
|
||||
view.keyCurve = obj.keyCurve as "P-256";
|
||||
view.rpId = obj.rpId;
|
||||
view.userHandle = obj.userHandle;
|
||||
view.userName = obj.userName;
|
||||
view.counter = parseInt(obj.counter);
|
||||
view.rpName = obj.rpName;
|
||||
view.userDisplayName = obj.userDisplayName;
|
||||
view.discoverable = obj.discoverable?.toLowerCase() === "true" ? true : false;
|
||||
view.creationDate = obj.creationDate ? new Date(obj.creationDate) : null;
|
||||
|
||||
return view;
|
||||
return new Fido2CredentialView({
|
||||
credentialId: obj.credentialId,
|
||||
keyType: obj.keyType as "public-key",
|
||||
keyAlgorithm: obj.keyAlgorithm as "ECDSA",
|
||||
keyCurve: obj.keyCurve as "P-256",
|
||||
keyValue: obj.keyValue,
|
||||
rpId: obj.rpId,
|
||||
userHandle: obj.userHandle,
|
||||
userName: obj.userName,
|
||||
counter: parseInt(obj.counter),
|
||||
rpName: obj.rpName,
|
||||
userDisplayName: obj.userDisplayName,
|
||||
discoverable: obj.discoverable?.toLowerCase() === "true",
|
||||
creationDate: new Date(obj.creationDate),
|
||||
});
|
||||
}
|
||||
|
||||
toSdkFido2CredentialFullView(): Fido2CredentialFullView {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { FieldView as SdkFieldView, FieldType as SdkFieldType } from "@bitwarden/sdk-internal";
|
||||
@@ -9,13 +7,13 @@ import { FieldType, LinkedIdType } from "../../enums";
|
||||
import { Field } from "../domain/field";
|
||||
|
||||
export class FieldView implements View {
|
||||
name: string = null;
|
||||
value: string = null;
|
||||
type: FieldType = null;
|
||||
name?: string;
|
||||
value?: string;
|
||||
type: FieldType = FieldType.Text;
|
||||
newField = false; // Marks if the field is new and hasn't been saved
|
||||
showValue = false;
|
||||
showCount = false;
|
||||
linkedId: LinkedIdType = null;
|
||||
linkedId?: LinkedIdType;
|
||||
|
||||
constructor(f?: Field) {
|
||||
if (!f) {
|
||||
@@ -26,8 +24,8 @@ export class FieldView implements View {
|
||||
this.linkedId = f.linkedId;
|
||||
}
|
||||
|
||||
get maskedValue(): string {
|
||||
return this.value != null ? "••••••••" : null;
|
||||
get maskedValue(): string | undefined {
|
||||
return this.value != null ? "••••••••" : undefined;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<FieldView>>): FieldView {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { IdentityView as SdkIdentityView } from "@bitwarden/sdk-internal";
|
||||
@@ -12,65 +10,65 @@ import { ItemView } from "./item.view";
|
||||
|
||||
export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
@linkedFieldOption(LinkedId.Title, { sortPosition: 0 })
|
||||
title: string = null;
|
||||
title: string | undefined;
|
||||
@linkedFieldOption(LinkedId.MiddleName, { sortPosition: 2 })
|
||||
middleName: string = null;
|
||||
middleName: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Address1, { sortPosition: 12 })
|
||||
address1: string = null;
|
||||
address1: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Address2, { sortPosition: 13 })
|
||||
address2: string = null;
|
||||
address2: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Address3, { sortPosition: 14 })
|
||||
address3: string = null;
|
||||
address3: string | undefined;
|
||||
@linkedFieldOption(LinkedId.City, { sortPosition: 15, i18nKey: "cityTown" })
|
||||
city: string = null;
|
||||
city: string | undefined;
|
||||
@linkedFieldOption(LinkedId.State, { sortPosition: 16, i18nKey: "stateProvince" })
|
||||
state: string = null;
|
||||
state: string | undefined;
|
||||
@linkedFieldOption(LinkedId.PostalCode, { sortPosition: 17, i18nKey: "zipPostalCode" })
|
||||
postalCode: string = null;
|
||||
postalCode: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Country, { sortPosition: 18 })
|
||||
country: string = null;
|
||||
country: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Company, { sortPosition: 6 })
|
||||
company: string = null;
|
||||
company: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Email, { sortPosition: 10 })
|
||||
email: string = null;
|
||||
email: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Phone, { sortPosition: 11 })
|
||||
phone: string = null;
|
||||
phone: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Ssn, { sortPosition: 7 })
|
||||
ssn: string = null;
|
||||
ssn: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Username, { sortPosition: 5 })
|
||||
username: string = null;
|
||||
username: string | undefined;
|
||||
@linkedFieldOption(LinkedId.PassportNumber, { sortPosition: 8 })
|
||||
passportNumber: string = null;
|
||||
passportNumber: string | undefined;
|
||||
@linkedFieldOption(LinkedId.LicenseNumber, { sortPosition: 9 })
|
||||
licenseNumber: string = null;
|
||||
licenseNumber: string | undefined;
|
||||
|
||||
private _firstName: string = null;
|
||||
private _lastName: string = null;
|
||||
private _subTitle: string = null;
|
||||
private _firstName: string | undefined;
|
||||
private _lastName: string | undefined;
|
||||
private _subTitle: string | undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.FirstName, { sortPosition: 1 })
|
||||
get firstName(): string {
|
||||
get firstName(): string | undefined {
|
||||
return this._firstName;
|
||||
}
|
||||
set firstName(value: string) {
|
||||
set firstName(value: string | undefined) {
|
||||
this._firstName = value;
|
||||
this._subTitle = null;
|
||||
this._subTitle = undefined;
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.LastName, { sortPosition: 4 })
|
||||
get lastName(): string {
|
||||
get lastName(): string | undefined {
|
||||
return this._lastName;
|
||||
}
|
||||
set lastName(value: string) {
|
||||
set lastName(value: string | undefined) {
|
||||
this._lastName = value;
|
||||
this._subTitle = null;
|
||||
this._subTitle = undefined;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
get subTitle(): string | undefined {
|
||||
if (this._subTitle == null && (this.firstName != null || this.lastName != null)) {
|
||||
this._subTitle = "";
|
||||
if (this.firstName != null) {
|
||||
@@ -88,7 +86,7 @@ export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.FullName, { sortPosition: 3 })
|
||||
get fullName(): string {
|
||||
get fullName(): string | undefined {
|
||||
if (
|
||||
this.title != null ||
|
||||
this.firstName != null ||
|
||||
@@ -111,11 +109,11 @@ export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get fullAddress(): string {
|
||||
let address = this.address1;
|
||||
get fullAddress(): string | undefined {
|
||||
let address = this.address1 ?? "";
|
||||
if (!Utils.isNullOrWhitespace(this.address2)) {
|
||||
if (!Utils.isNullOrWhitespace(address)) {
|
||||
address += ", ";
|
||||
@@ -131,9 +129,9 @@ export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
return address;
|
||||
}
|
||||
|
||||
get fullAddressPart2(): string {
|
||||
get fullAddressPart2(): string | undefined {
|
||||
if (this.city == null && this.state == null && this.postalCode == null) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
const city = this.city || "-";
|
||||
const state = this.state;
|
||||
@@ -146,7 +144,7 @@ export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
return addressPart2;
|
||||
}
|
||||
|
||||
get fullAddressForCopy(): string {
|
||||
get fullAddressForCopy(): string | undefined {
|
||||
let address = this.fullAddress;
|
||||
if (this.city != null || this.state != null || this.postalCode != null) {
|
||||
address += "\n" + this.fullAddressPart2;
|
||||
@@ -157,38 +155,34 @@ export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
return address;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<IdentityView>>): IdentityView {
|
||||
static fromJSON(obj: Partial<Jsonify<IdentityView>> | undefined): IdentityView {
|
||||
return Object.assign(new IdentityView(), obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the SDK IdentityView to an IdentityView.
|
||||
*/
|
||||
static fromSdkIdentityView(obj: SdkIdentityView): IdentityView | undefined {
|
||||
if (obj == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromSdkIdentityView(obj: SdkIdentityView): IdentityView {
|
||||
const identityView = new IdentityView();
|
||||
|
||||
identityView.title = obj.title ?? null;
|
||||
identityView.firstName = obj.firstName ?? null;
|
||||
identityView.middleName = obj.middleName ?? null;
|
||||
identityView.lastName = obj.lastName ?? null;
|
||||
identityView.address1 = obj.address1 ?? null;
|
||||
identityView.address2 = obj.address2 ?? null;
|
||||
identityView.address3 = obj.address3 ?? null;
|
||||
identityView.city = obj.city ?? null;
|
||||
identityView.state = obj.state ?? null;
|
||||
identityView.postalCode = obj.postalCode ?? null;
|
||||
identityView.country = obj.country ?? null;
|
||||
identityView.company = obj.company ?? null;
|
||||
identityView.email = obj.email ?? null;
|
||||
identityView.phone = obj.phone ?? null;
|
||||
identityView.ssn = obj.ssn ?? null;
|
||||
identityView.username = obj.username ?? null;
|
||||
identityView.passportNumber = obj.passportNumber ?? null;
|
||||
identityView.licenseNumber = obj.licenseNumber ?? null;
|
||||
identityView.title = obj.title;
|
||||
identityView.firstName = obj.firstName;
|
||||
identityView.middleName = obj.middleName;
|
||||
identityView.lastName = obj.lastName;
|
||||
identityView.address1 = obj.address1;
|
||||
identityView.address2 = obj.address2;
|
||||
identityView.address3 = obj.address3;
|
||||
identityView.city = obj.city;
|
||||
identityView.state = obj.state;
|
||||
identityView.postalCode = obj.postalCode;
|
||||
identityView.country = obj.country;
|
||||
identityView.company = obj.company;
|
||||
identityView.email = obj.email;
|
||||
identityView.phone = obj.phone;
|
||||
identityView.ssn = obj.ssn;
|
||||
identityView.username = obj.username;
|
||||
identityView.passportNumber = obj.passportNumber;
|
||||
identityView.licenseNumber = obj.licenseNumber;
|
||||
|
||||
return identityView;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { View } from "../../../models/view/view";
|
||||
import { LinkedMetadata } from "../../linked-field-option.decorator";
|
||||
|
||||
export abstract class ItemView implements View {
|
||||
linkedFieldOptions: Map<number, LinkedMetadata>;
|
||||
abstract get subTitle(): string;
|
||||
linkedFieldOptions?: Map<number, LinkedMetadata>;
|
||||
abstract get subTitle(): string | undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { LoginUriView as SdkLoginUriView } from "@bitwarden/sdk-internal";
|
||||
@@ -11,13 +9,13 @@ import { Utils } from "../../../platform/misc/utils";
|
||||
import { LoginUri } from "../domain/login-uri";
|
||||
|
||||
export class LoginUriView implements View {
|
||||
match: UriMatchStrategySetting = null;
|
||||
match?: UriMatchStrategySetting;
|
||||
|
||||
private _uri: string = null;
|
||||
private _domain: string = null;
|
||||
private _hostname: string = null;
|
||||
private _host: string = null;
|
||||
private _canLaunch: boolean = null;
|
||||
private _uri?: string;
|
||||
private _domain?: string;
|
||||
private _hostname?: string;
|
||||
private _host?: string;
|
||||
private _canLaunch?: boolean;
|
||||
|
||||
constructor(u?: LoginUri) {
|
||||
if (!u) {
|
||||
@@ -27,59 +25,59 @@ export class LoginUriView implements View {
|
||||
this.match = u.match;
|
||||
}
|
||||
|
||||
get uri(): string {
|
||||
get uri(): string | undefined {
|
||||
return this._uri;
|
||||
}
|
||||
set uri(value: string) {
|
||||
set uri(value: string | undefined) {
|
||||
this._uri = value;
|
||||
this._domain = null;
|
||||
this._canLaunch = null;
|
||||
this._domain = undefined;
|
||||
this._canLaunch = undefined;
|
||||
}
|
||||
|
||||
get domain(): string {
|
||||
get domain(): string | undefined {
|
||||
if (this._domain == null && this.uri != null) {
|
||||
this._domain = Utils.getDomain(this.uri);
|
||||
if (this._domain === "") {
|
||||
this._domain = null;
|
||||
this._domain = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this._domain;
|
||||
}
|
||||
|
||||
get hostname(): string {
|
||||
get hostname(): string | undefined {
|
||||
if (this.match === UriMatchStrategy.RegularExpression) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
if (this._hostname == null && this.uri != null) {
|
||||
this._hostname = Utils.getHostname(this.uri);
|
||||
if (this._hostname === "") {
|
||||
this._hostname = null;
|
||||
this._hostname = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this._hostname;
|
||||
}
|
||||
|
||||
get host(): string {
|
||||
get host(): string | undefined {
|
||||
if (this.match === UriMatchStrategy.RegularExpression) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
if (this._host == null && this.uri != null) {
|
||||
this._host = Utils.getHost(this.uri);
|
||||
if (this._host === "") {
|
||||
this._host = null;
|
||||
this._host = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this._host;
|
||||
}
|
||||
|
||||
get hostnameOrUri(): string {
|
||||
get hostnameOrUri(): string | undefined {
|
||||
return this.hostname != null ? this.hostname : this.uri;
|
||||
}
|
||||
|
||||
get hostOrUri(): string {
|
||||
get hostOrUri(): string | undefined {
|
||||
return this.host != null ? this.host : this.uri;
|
||||
}
|
||||
|
||||
@@ -104,7 +102,10 @@ export class LoginUriView implements View {
|
||||
return this._canLaunch;
|
||||
}
|
||||
|
||||
get launchUri(): string {
|
||||
get launchUri(): string | undefined {
|
||||
if (this.uri == null) {
|
||||
return undefined;
|
||||
}
|
||||
return this.uri.indexOf("://") < 0 && !Utils.isNullOrWhitespace(Utils.getDomain(this.uri))
|
||||
? "http://" + this.uri
|
||||
: this.uri;
|
||||
@@ -141,7 +142,7 @@ export class LoginUriView implements View {
|
||||
matchesUri(
|
||||
targetUri: string,
|
||||
equivalentDomains: Set<string>,
|
||||
defaultUriMatch: UriMatchStrategySetting = null,
|
||||
defaultUriMatch?: UriMatchStrategySetting,
|
||||
/** When present, will override the match strategy for the cipher if it is `Never` with `Domain` */
|
||||
overrideNeverMatchStrategy?: true,
|
||||
): boolean {
|
||||
@@ -198,7 +199,7 @@ export class LoginUriView implements View {
|
||||
|
||||
if (Utils.DomainMatchBlacklist.has(this.domain)) {
|
||||
const domainUrlHost = Utils.getHost(targetUri);
|
||||
return !Utils.DomainMatchBlacklist.get(this.domain).has(domainUrlHost);
|
||||
return !Utils.DomainMatchBlacklist.get(this.domain)!.has(domainUrlHost);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -29,11 +29,6 @@ describe("LoginView", () => {
|
||||
});
|
||||
|
||||
describe("fromSdkLoginView", () => {
|
||||
it("should return undefined when the input is null", () => {
|
||||
const result = LoginView.fromSdkLoginView(null as unknown as SdkLoginView);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return a LoginView from an SdkLoginView", () => {
|
||||
jest.spyOn(LoginUriView, "fromSdkLoginUriView").mockImplementation(mockFromSdk);
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||
@@ -15,15 +13,15 @@ import { LoginUriView } from "./login-uri.view";
|
||||
|
||||
export class LoginView extends ItemView {
|
||||
@linkedFieldOption(LinkedId.Username, { sortPosition: 0 })
|
||||
username: string = null;
|
||||
username: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Password, { sortPosition: 1 })
|
||||
password: string = null;
|
||||
password: string | undefined;
|
||||
|
||||
passwordRevisionDate?: Date = null;
|
||||
totp: string = null;
|
||||
passwordRevisionDate?: Date;
|
||||
totp: string | undefined;
|
||||
uris: LoginUriView[] = [];
|
||||
autofillOnPageLoad: boolean = null;
|
||||
fido2Credentials: Fido2CredentialView[] = null;
|
||||
autofillOnPageLoad: boolean | undefined;
|
||||
fido2Credentials: Fido2CredentialView[] = [];
|
||||
|
||||
constructor(l?: Login) {
|
||||
super();
|
||||
@@ -35,15 +33,15 @@ export class LoginView extends ItemView {
|
||||
this.autofillOnPageLoad = l.autofillOnPageLoad;
|
||||
}
|
||||
|
||||
get uri(): string {
|
||||
return this.hasUris ? this.uris[0].uri : null;
|
||||
get uri(): string | undefined {
|
||||
return this.hasUris ? this.uris[0].uri : undefined;
|
||||
}
|
||||
|
||||
get maskedPassword(): string {
|
||||
return this.password != null ? "••••••••" : null;
|
||||
get maskedPassword(): string | undefined {
|
||||
return this.password != null ? "••••••••" : undefined;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
get subTitle(): string | undefined {
|
||||
// if there's a passkey available, use that as a fallback
|
||||
if (Utils.isNullOrEmpty(this.username) && this.fido2Credentials?.length > 0) {
|
||||
return this.fido2Credentials[0].userName;
|
||||
@@ -60,14 +58,14 @@ export class LoginView extends ItemView {
|
||||
return !Utils.isNullOrWhitespace(this.totp);
|
||||
}
|
||||
|
||||
get launchUri(): string {
|
||||
get launchUri(): string | undefined {
|
||||
if (this.hasUris) {
|
||||
const uri = this.uris.find((u) => u.canLaunch);
|
||||
if (uri != null) {
|
||||
return uri.launchUri;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get hasUris(): boolean {
|
||||
@@ -81,7 +79,7 @@ export class LoginView extends ItemView {
|
||||
matchesUri(
|
||||
targetUri: string,
|
||||
equivalentDomains: Set<string>,
|
||||
defaultUriMatch: UriMatchStrategySetting = null,
|
||||
defaultUriMatch?: UriMatchStrategySetting,
|
||||
/** When present, will override the match strategy for the cipher if it is `Never` with `Domain` */
|
||||
overrideNeverMatchStrategy?: true,
|
||||
): boolean {
|
||||
@@ -94,17 +92,20 @@ export class LoginView extends ItemView {
|
||||
);
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<DeepJsonify<LoginView>>): LoginView {
|
||||
const passwordRevisionDate =
|
||||
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
||||
const uris = obj.uris.map((uri) => LoginUriView.fromJSON(uri));
|
||||
const fido2Credentials = obj.fido2Credentials?.map((key) => Fido2CredentialView.fromJSON(key));
|
||||
static fromJSON(obj: Partial<DeepJsonify<LoginView>> | undefined): LoginView {
|
||||
if (obj == undefined) {
|
||||
return new LoginView();
|
||||
}
|
||||
|
||||
return Object.assign(new LoginView(), obj, {
|
||||
passwordRevisionDate,
|
||||
uris,
|
||||
fido2Credentials,
|
||||
});
|
||||
const loginView = Object.assign(new LoginView(), obj) as LoginView;
|
||||
|
||||
loginView.passwordRevisionDate =
|
||||
obj.passwordRevisionDate == null ? undefined : new Date(obj.passwordRevisionDate);
|
||||
loginView.uris = obj.uris?.map((uri) => LoginUriView.fromJSON(uri)) ?? [];
|
||||
loginView.fido2Credentials =
|
||||
obj.fido2Credentials?.map((key) => Fido2CredentialView.fromJSON(key)) ?? [];
|
||||
|
||||
return loginView;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,25 +116,21 @@ export class LoginView extends ItemView {
|
||||
* the FIDO2 credentials in encrypted form. We can decrypt them later using a separate
|
||||
* call to client.vault().ciphers().decrypt_fido2_credentials().
|
||||
*/
|
||||
static fromSdkLoginView(obj: SdkLoginView): LoginView | undefined {
|
||||
if (obj == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromSdkLoginView(obj: SdkLoginView): LoginView {
|
||||
const loginView = new LoginView();
|
||||
|
||||
loginView.username = obj.username ?? null;
|
||||
loginView.password = obj.password ?? null;
|
||||
loginView.username = obj.username;
|
||||
loginView.password = obj.password;
|
||||
loginView.passwordRevisionDate =
|
||||
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
||||
loginView.totp = obj.totp ?? null;
|
||||
loginView.autofillOnPageLoad = obj.autofillOnPageLoad ?? null;
|
||||
obj.passwordRevisionDate == null ? undefined : new Date(obj.passwordRevisionDate);
|
||||
loginView.totp = obj.totp;
|
||||
loginView.autofillOnPageLoad = obj.autofillOnPageLoad;
|
||||
loginView.uris =
|
||||
obj.uris
|
||||
?.filter((uri) => uri.uri != null && uri.uri !== "")
|
||||
.map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || [];
|
||||
.map((uri) => LoginUriView.fromSdkLoginUriView(uri)!) || [];
|
||||
// FIDO2 credentials are not decrypted here, they remain encrypted
|
||||
loginView.fido2Credentials = null;
|
||||
loginView.fido2Credentials = [];
|
||||
|
||||
return loginView;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SecureNoteView as SdkSecureNoteView } from "@bitwarden/sdk-internal";
|
||||
@@ -10,7 +8,7 @@ import { SecureNote } from "../domain/secure-note";
|
||||
import { ItemView } from "./item.view";
|
||||
|
||||
export class SecureNoteView extends ItemView implements SdkSecureNoteView {
|
||||
type: SecureNoteType = null;
|
||||
type: SecureNoteType = SecureNoteType.Generic;
|
||||
|
||||
constructor(n?: SecureNote) {
|
||||
super();
|
||||
@@ -21,24 +19,20 @@ export class SecureNoteView extends ItemView implements SdkSecureNoteView {
|
||||
this.type = n.type;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
return null;
|
||||
get subTitle(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<SecureNoteView>>): SecureNoteView {
|
||||
static fromJSON(obj: Partial<Jsonify<SecureNoteView>> | undefined): SecureNoteView {
|
||||
return Object.assign(new SecureNoteView(), obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the SDK SecureNoteView to a SecureNoteView.
|
||||
*/
|
||||
static fromSdkSecureNoteView(obj: SdkSecureNoteView): SecureNoteView | undefined {
|
||||
if (!obj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromSdkSecureNoteView(obj: SdkSecureNoteView): SecureNoteView {
|
||||
const secureNoteView = new SecureNoteView();
|
||||
secureNoteView.type = obj.type ?? null;
|
||||
secureNoteView.type = obj.type;
|
||||
|
||||
return secureNoteView;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SshKeyView as SdkSshKeyView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { SshKey } from "../domain/ssh-key";
|
||||
|
||||
import { ItemView } from "./item.view";
|
||||
|
||||
export class SshKeyView extends ItemView {
|
||||
privateKey: string = null;
|
||||
publicKey: string = null;
|
||||
keyFingerprint: string = null;
|
||||
|
||||
constructor(n?: SshKey) {
|
||||
super();
|
||||
if (!n) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
privateKey!: string;
|
||||
publicKey!: string;
|
||||
keyFingerprint!: string;
|
||||
|
||||
get maskedPrivateKey(): string {
|
||||
if (!this.privateKey || this.privateKey.length === 0) {
|
||||
@@ -43,23 +32,19 @@ export class SshKeyView extends ItemView {
|
||||
return this.keyFingerprint;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<SshKeyView>>): SshKeyView {
|
||||
static fromJSON(obj: Partial<Jsonify<SshKeyView>> | undefined): SshKeyView {
|
||||
return Object.assign(new SshKeyView(), obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the SDK SshKeyView to a SshKeyView.
|
||||
*/
|
||||
static fromSdkSshKeyView(obj: SdkSshKeyView): SshKeyView | undefined {
|
||||
if (!obj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromSdkSshKeyView(obj: SdkSshKeyView): SshKeyView {
|
||||
const sshKeyView = new SshKeyView();
|
||||
|
||||
sshKeyView.privateKey = obj.privateKey ?? null;
|
||||
sshKeyView.publicKey = obj.publicKey ?? null;
|
||||
sshKeyView.keyFingerprint = obj.fingerprint ?? null;
|
||||
sshKeyView.privateKey = obj.privateKey;
|
||||
sshKeyView.publicKey = obj.publicKey;
|
||||
sshKeyView.keyFingerprint = obj.fingerprint;
|
||||
|
||||
return sshKeyView;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import { CipherView } from "../models/view/cipher.view";
|
||||
import { LoginUriView } from "../models/view/login-uri.view";
|
||||
|
||||
import { CipherService } from "./cipher.service";
|
||||
import { ENCRYPTED_CIPHERS } from "./key-state/ciphers.state";
|
||||
|
||||
const ENCRYPTED_TEXT = "This data has been encrypted";
|
||||
function encryptText(clearText: string | Uint8Array) {
|
||||
@@ -817,4 +818,87 @@ describe("Cipher Service", () => {
|
||||
expect(failures).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("replace (no upsert)", () => {
|
||||
// In order to set up initial state we need to manually update the encrypted state
|
||||
// which will result in an emission. All tests will have this baseline emission.
|
||||
const TEST_BASELINE_EMISSIONS = 1;
|
||||
|
||||
const makeCipher = (id: string): CipherData =>
|
||||
({
|
||||
...cipherData,
|
||||
id,
|
||||
name: `Enc ${id}`,
|
||||
}) as CipherData;
|
||||
|
||||
const tick = async () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
const setEncryptedState = async (data: Record<CipherId, CipherData>, uid = userId) => {
|
||||
// Directly set the encrypted state, this will result in a single emission
|
||||
await stateProvider.getUser(uid, ENCRYPTED_CIPHERS).update(() => data);
|
||||
// match service’s “next tick” behavior so subscribers see it
|
||||
await tick();
|
||||
};
|
||||
|
||||
it("emits and calls updateEncryptedCipherState when current state is empty and replace({}) is called", async () => {
|
||||
// Ensure empty state
|
||||
await setEncryptedState({});
|
||||
|
||||
const emissions: Array<Record<CipherId, CipherData>> = [];
|
||||
const sub = cipherService.ciphers$(userId).subscribe((v) => emissions.push(v));
|
||||
await tick();
|
||||
|
||||
const spy = jest.spyOn<any, any>(cipherService, "updateEncryptedCipherState");
|
||||
|
||||
// Calling replace with empty object MUST still update to trigger init emissions
|
||||
await cipherService.replace({}, userId);
|
||||
await tick();
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(emissions.length).toBeGreaterThanOrEqual(TEST_BASELINE_EMISSIONS + 1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it("does NOT emit or call updateEncryptedCipherState when state is non-empty and identical", async () => {
|
||||
const A = makeCipher("A");
|
||||
await setEncryptedState({ [A.id as CipherId]: A });
|
||||
|
||||
const emissions: Array<Record<CipherId, CipherData>> = [];
|
||||
const sub = cipherService.ciphers$(userId).subscribe((v) => emissions.push(v));
|
||||
await tick();
|
||||
|
||||
const spy = jest.spyOn<any, any>(cipherService, "updateEncryptedCipherState");
|
||||
|
||||
// identical snapshot → short-circuit path
|
||||
await cipherService.replace({ [A.id as CipherId]: A }, userId);
|
||||
await tick();
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(emissions.length).toBe(TEST_BASELINE_EMISSIONS);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it("emits and calls updateEncryptedCipherState when the provided state differs from current", async () => {
|
||||
const A = makeCipher("A");
|
||||
await setEncryptedState({ [A.id as CipherId]: A });
|
||||
|
||||
const emissions: Array<Record<CipherId, CipherData>> = [];
|
||||
const sub = cipherService.ciphers$(userId).subscribe((v) => emissions.push(v));
|
||||
await tick();
|
||||
|
||||
const spy = jest.spyOn<any, any>(cipherService, "updateEncryptedCipherState");
|
||||
|
||||
const B = makeCipher("B");
|
||||
await cipherService.replace({ [B.id as CipherId]: B }, userId);
|
||||
await tick();
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(emissions.length).toBeGreaterThanOrEqual(TEST_BASELINE_EMISSIONS + 1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1172,6 +1172,18 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
async replace(ciphers: { [id: string]: CipherData }, userId: UserId): Promise<any> {
|
||||
const current = (await firstValueFrom(this.encryptedCiphersState(userId).state$)) ?? {};
|
||||
|
||||
// The extension relies on chrome.storage.StorageArea.onChanged to detect updates.
|
||||
// If stored and provided data are identical, this event doesn’t fire and the ciphers$
|
||||
// observable won’t emit a new value. In this case we can skip the update to avoid calling
|
||||
// clearCache and causing an empty state.
|
||||
// If the current state is empty (eg. for new users), we still want to perform the update to ensure
|
||||
// we trigger an emission as many subscribers rely on it during initialization.
|
||||
if (Object.keys(current).length > 0 && JSON.stringify(current) === JSON.stringify(ciphers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.updateEncryptedCipherState(() => ciphers, userId);
|
||||
}
|
||||
|
||||
@@ -1185,13 +1197,16 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
userId: UserId = null,
|
||||
): Promise<Record<CipherId, CipherData>> {
|
||||
userId ||= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
|
||||
await this.clearCache(userId);
|
||||
|
||||
const updatedCiphers = await this.stateProvider
|
||||
.getUser(userId, ENCRYPTED_CIPHERS)
|
||||
.update((current) => {
|
||||
const result = update(current ?? {});
|
||||
return result;
|
||||
});
|
||||
|
||||
// Some state storage providers (e.g. Electron) don't update the state immediately, wait for next tick
|
||||
// Otherwise, subscribers to cipherViews$ can get stale data
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
@@ -272,11 +272,16 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
||||
return [];
|
||||
}
|
||||
|
||||
const decryptFolderPromises = folders.map((f) =>
|
||||
f.decryptWithKey(userKey, this.encryptService),
|
||||
);
|
||||
const decryptedFolders = await Promise.all(decryptFolderPromises);
|
||||
decryptedFolders.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||
const decryptFolderPromises = folders.map(async (f) => {
|
||||
try {
|
||||
return await f.decryptWithKey(userKey, this.encryptService);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
const decryptedFolders = (await Promise.all(decryptFolderPromises))
|
||||
.filter((p) => p !== null)
|
||||
.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||
|
||||
const noneFolder = new FolderView();
|
||||
noneFolder.name = this.i18nService.t("noneFolder");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Observable, combineLatest, map, shareReplay, startWith } from "rxjs";
|
||||
import { Observable, combineLatest, map, shareReplay } from "rxjs";
|
||||
|
||||
import { ActiveUserState, GlobalState, StateProvider } from "../../../platform/state";
|
||||
import { VaultSettingsService as VaultSettingsServiceAbstraction } from "../../abstractions/vault-settings/vault-settings.service";
|
||||
@@ -31,7 +31,7 @@ export class VaultSettingsService implements VaultSettingsServiceAbstraction {
|
||||
*/
|
||||
readonly showCardsCurrentTab$: Observable<boolean> = combineLatest([
|
||||
this.showCardsCurrentTabState.state$.pipe(map((x) => x ?? true)),
|
||||
this.restrictedItemTypesService.restricted$.pipe(startWith([])),
|
||||
this.restrictedItemTypesService.restricted$,
|
||||
]).pipe(
|
||||
map(
|
||||
([enabled, restrictions]) =>
|
||||
|
||||
@@ -298,6 +298,10 @@ describe("CipherViewLikeUtils", () => {
|
||||
(cipherView.attachments as any) = null;
|
||||
|
||||
expect(CipherViewLikeUtils.hasAttachments(cipherView)).toBe(false);
|
||||
|
||||
cipherView.attachments = [];
|
||||
|
||||
expect(CipherViewLikeUtils.hasAttachments(cipherView)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export function perUserCache$<TValue>(
|
||||
create(userId),
|
||||
clearBuffer$.pipe(
|
||||
filter((clearId) => clearId === userId || clearId === null),
|
||||
map(() => null),
|
||||
map((): any => null),
|
||||
),
|
||||
).pipe(shareReplay({ bufferSize: 1, refCount: false }));
|
||||
cache.set(userId, observable);
|
||||
|
||||
Reference in New Issue
Block a user