1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 10:23:52 +00:00

Merge branch 'main' into auth/pm-26209/bugfix-desktop-error-on-auth-request-approval

This commit is contained in:
rr-bw
2025-11-04 11:29:52 -08:00
219 changed files with 4998 additions and 1567 deletions

View File

@@ -14,10 +14,4 @@ export abstract class AuditService {
* @returns A promise that resolves to an array of BreachAccountResponse objects.
*/
abstract breachedAccounts: (username: string) => Promise<BreachAccountResponse[]>;
/**
* Checks if a domain is known for phishing.
* @param domain The domain to check.
* @returns A promise that resolves to a boolean indicating if the domain is known for phishing.
*/
abstract getKnownPhishingDomains: () => Promise<string[]>;
}

View File

@@ -1,8 +1,13 @@
import { map, Observable } from "rxjs";
import { combineLatest, map, Observable } from "rxjs";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { UserId } from "../../../types/guid";
import { PolicyType } from "../../enums";
import { OrganizationData } from "../../models/data/organization.data";
import { Organization } from "../../models/domain/organization";
import { PolicyService } from "../policy/policy.service.abstraction";
export function canAccessVaultTab(org: Organization): boolean {
return org.canViewAllCollections;
@@ -51,6 +56,17 @@ export function canAccessOrgAdmin(org: Organization): boolean {
);
}
export function canAccessEmergencyAccess(
userId: UserId,
configService: ConfigService,
policyService: PolicyService,
) {
return combineLatest([
configService.getFeatureFlag$(FeatureFlag.AutoConfirm),
policyService.policiesByType$(PolicyType.AutoConfirm, userId),
]).pipe(map(([enabled, policies]) => !enabled || !policies.some((p) => p.enabled)));
}
/**
* @deprecated Please use the general `getById` custom rxjs operator instead.
*/

View File

@@ -554,6 +554,77 @@ describe("PolicyService", () => {
expect(result).toBe(false);
});
describe("SingleOrg policy exemptions", () => {
it("returns true for SingleOrg policy when AutoConfirm is enabled, even for users who can manage policies", async () => {
singleUserState.nextState(
arrayToRecord([
policyData("policy1", "org6", PolicyType.SingleOrg, true),
policyData("policy2", "org6", PolicyType.AutoConfirm, true),
]),
);
const result = await firstValueFrom(
policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId),
);
expect(result).toBe(true);
});
it("returns false for SingleOrg policy when user can manage policies and AutoConfirm is not enabled", async () => {
singleUserState.nextState(
arrayToRecord([policyData("policy1", "org6", PolicyType.SingleOrg, true)]),
);
const result = await firstValueFrom(
policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId),
);
expect(result).toBe(false);
});
it("returns false for SingleOrg policy when user can manage policies and AutoConfirm is disabled", async () => {
singleUserState.nextState(
arrayToRecord([
policyData("policy1", "org6", PolicyType.SingleOrg, true),
policyData("policy2", "org6", PolicyType.AutoConfirm, false),
]),
);
const result = await firstValueFrom(
policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId),
);
expect(result).toBe(false);
});
it("returns true for SingleOrg policy for regular users when AutoConfirm is not enabled", async () => {
singleUserState.nextState(
arrayToRecord([policyData("policy1", "org1", PolicyType.SingleOrg, true)]),
);
const result = await firstValueFrom(
policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId),
);
expect(result).toBe(true);
});
it("returns true for SingleOrg policy when AutoConfirm is enabled in a different organization", async () => {
singleUserState.nextState(
arrayToRecord([
policyData("policy1", "org6", PolicyType.SingleOrg, true),
policyData("policy2", "org1", PolicyType.AutoConfirm, true),
]),
);
const result = await firstValueFrom(
policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId),
);
expect(result).toBe(false);
});
});
});
describe("combinePoliciesIntoMasterPasswordPolicyOptions", () => {

View File

@@ -40,18 +40,16 @@ export class DefaultPolicyService implements PolicyService {
}
policiesByType$(policyType: PolicyType, userId: UserId) {
const filteredPolicies$ = this.policies$(userId).pipe(
map((policies) => policies.filter((p) => p.type === policyType)),
);
if (!userId) {
throw new Error("No userId provided");
}
const allPolicies$ = this.policies$(userId);
const organizations$ = this.organizationService.organizations$(userId);
return combineLatest([filteredPolicies$, organizations$]).pipe(
return combineLatest([allPolicies$, organizations$]).pipe(
map(([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)),
map((policies) => policies.filter((p) => p.type === policyType)),
);
}
@@ -77,7 +75,7 @@ export class DefaultPolicyService implements PolicyService {
policy.enabled &&
organization.status >= OrganizationUserStatusType.Accepted &&
organization.usePolicies &&
!this.isExemptFromPolicy(policy.type, organization)
!this.isExemptFromPolicy(policy.type, organization, policies)
);
});
}
@@ -265,7 +263,11 @@ export class DefaultPolicyService implements PolicyService {
* Determines whether an orgUser is exempt from a specific policy because of their role
* Generally orgUsers who can manage policies are exempt from them, but some policies are stricter
*/
private isExemptFromPolicy(policyType: PolicyType, organization: Organization) {
private isExemptFromPolicy(
policyType: PolicyType,
organization: Organization,
allPolicies: Policy[],
) {
switch (policyType) {
case PolicyType.MaximumVaultTimeout:
// Max Vault Timeout applies to everyone except owners
@@ -286,6 +288,14 @@ export class DefaultPolicyService implements PolicyService {
case PolicyType.OrganizationDataOwnership:
// organization data ownership policy applies to everyone except admins and owners
return organization.isAdmin;
case PolicyType.SingleOrg:
// Check if AutoConfirm policy is enabled for this organization
return allPolicies.find(
(p) =>
p.organizationId === organization.id && p.type === PolicyType.AutoConfirm && p.enabled,
)
? false
: organization.canManagePolicies;
default:
return organization.canManagePolicies;
}

View File

@@ -11,11 +11,13 @@ import { MasterPasswordPolicyResponse } from "./master-password-policy.response"
import { UserDecryptionOptionsResponse } from "./user-decryption-options/user-decryption-options.response";
export class IdentityTokenResponse extends BaseResponse {
// Authentication Information
accessToken: string;
expiresIn?: number;
refreshToken?: string;
tokenType: string;
// Decryption Information
resetMasterPassword: boolean;
privateKey: string; // userKeyEncryptedPrivateKey
key?: EncString; // masterKeyEncryptedUserKey

View File

@@ -0,0 +1,32 @@
import { Observable } from "rxjs";
import {
BusinessSubscriptionPricingTier,
PersonalSubscriptionPricingTier,
} from "../types/subscription-pricing-tier";
export abstract class SubscriptionPricingServiceAbstraction {
/**
* Gets personal subscription pricing tiers (Premium and Families).
* Throws any errors that occur during api request so callers must handle errors.
* @returns An observable of an array of personal subscription pricing tiers.
* @throws Error if any errors occur during api request.
*/
abstract getPersonalSubscriptionPricingTiers$(): Observable<PersonalSubscriptionPricingTier[]>;
/**
* Gets business subscription pricing tiers (Teams, Enterprise, and Custom).
* Throws any errors that occur during api request so callers must handle errors.
* @returns An observable of an array of business subscription pricing tiers.
* @throws Error if any errors occur during api request.
*/
abstract getBusinessSubscriptionPricingTiers$(): Observable<BusinessSubscriptionPricingTier[]>;
/**
* Gets developer subscription pricing tiers (Free, Teams, and Enterprise).
* Throws any errors that occur during api request so callers must handle errors.
* @returns An observable of an array of business subscription pricing tiers for developers.
* @throws Error if any errors occur during api request.
*/
abstract getDeveloperSubscriptionPricingTiers$(): Observable<BusinessSubscriptionPricingTier[]>;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,411 @@
import {
combineLatest,
from,
map,
Observable,
of,
shareReplay,
switchMap,
take,
throwError,
} from "rxjs";
import { catchError } from "rxjs/operators";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { PlanType } from "@bitwarden/common/billing/enums";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
import { PremiumPlanResponse } from "@bitwarden/common/billing/models/response/premium-plan.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/logging";
import { SubscriptionPricingServiceAbstraction } from "../abstractions/subscription-pricing.service.abstraction";
import {
BusinessSubscriptionPricingTier,
BusinessSubscriptionPricingTierIds,
PersonalSubscriptionPricingTier,
PersonalSubscriptionPricingTierIds,
SubscriptionCadenceIds,
} from "../types/subscription-pricing-tier";
export class DefaultSubscriptionPricingService implements SubscriptionPricingServiceAbstraction {
/**
* Fallback premium pricing used when the feature flag is disabled.
* These values represent the legacy pricing model and will not reflect
* server-side price changes. They are retained for backward compatibility
* during the feature flag rollout period.
*/
private static readonly FALLBACK_PREMIUM_SEAT_PRICE = 10;
private static readonly FALLBACK_PREMIUM_STORAGE_PRICE = 4;
constructor(
private billingApiService: BillingApiServiceAbstraction,
private configService: ConfigService,
private i18nService: I18nService,
private logService: LogService,
) {}
/**
* Gets personal subscription pricing tiers (Premium and Families).
* Throws any errors that occur during api request so callers must handle errors.
* @returns An observable of an array of personal subscription pricing tiers.
* @throws Error if any errors occur during api request.
*/
getPersonalSubscriptionPricingTiers$ = (): Observable<PersonalSubscriptionPricingTier[]> =>
combineLatest([this.premium$, this.families$]).pipe(
catchError((error: unknown) => {
this.logService.error("Failed to load personal subscription pricing tiers", error);
return throwError(() => error);
}),
);
/**
* Gets business subscription pricing tiers (Teams, Enterprise, and Custom).
* Throws any errors that occur during api request so callers must handle errors.
* @returns An observable of an array of business subscription pricing tiers.
* @throws Error if any errors occur during api request.
*/
getBusinessSubscriptionPricingTiers$ = (): Observable<BusinessSubscriptionPricingTier[]> =>
combineLatest([this.teams$, this.enterprise$, this.custom$]).pipe(
catchError((error: unknown) => {
this.logService.error("Failed to load business subscription pricing tiers", error);
return throwError(() => error);
}),
);
/**
* Gets developer subscription pricing tiers (Free, Teams, and Enterprise).
* Throws any errors that occur during api request so callers must handle errors.
* @returns An observable of an array of business subscription pricing tiers for developers.
* @throws Error if any errors occur during api request.
*/
getDeveloperSubscriptionPricingTiers$ = (): Observable<BusinessSubscriptionPricingTier[]> =>
combineLatest([this.free$, this.teams$, this.enterprise$]).pipe(
catchError((error: unknown) => {
this.logService.error("Failed to load developer subscription pricing tiers", error);
return throwError(() => error);
}),
);
private plansResponse$: Observable<ListResponse<PlanResponse>> = from(
this.billingApiService.getPlans(),
).pipe(shareReplay({ bufferSize: 1, refCount: false }));
private premiumPlanResponse$: Observable<PremiumPlanResponse> = from(
this.billingApiService.getPremiumPlan(),
).pipe(
catchError((error: unknown) => {
this.logService.error("Failed to fetch premium plan from API", error);
return throwError(() => error); // Re-throw to propagate to higher-level error handler
}),
shareReplay({ bufferSize: 1, refCount: false }),
);
private premium$: Observable<PersonalSubscriptionPricingTier> = this.configService
.getFeatureFlag$(FeatureFlag.PM26793_FetchPremiumPriceFromPricingService)
.pipe(
take(1), // Lock behavior at first subscription to prevent switching data sources mid-stream
switchMap((fetchPremiumFromPricingService) =>
fetchPremiumFromPricingService
? this.premiumPlanResponse$.pipe(
map((premiumPlan) => ({
seat: premiumPlan.seat.price,
storage: premiumPlan.storage.price,
})),
)
: of({
seat: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_SEAT_PRICE,
storage: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_STORAGE_PRICE,
}),
),
map((premiumPrices) => ({
id: PersonalSubscriptionPricingTierIds.Premium,
name: this.i18nService.t("premium"),
description: this.i18nService.t("planDescPremium"),
availableCadences: [SubscriptionCadenceIds.Annually],
passwordManager: {
type: "standalone",
annualPrice: premiumPrices.seat,
annualPricePerAdditionalStorageGB: premiumPrices.storage,
features: [
this.featureTranslations.builtInAuthenticator(),
this.featureTranslations.secureFileStorage(),
this.featureTranslations.emergencyAccess(),
this.featureTranslations.breachMonitoring(),
this.featureTranslations.andMoreFeatures(),
],
},
})),
);
private families$: Observable<PersonalSubscriptionPricingTier> = this.plansResponse$.pipe(
map((plans) => {
const familiesPlan = plans.data.find((plan) => plan.type === PlanType.FamiliesAnnually)!;
return {
id: PersonalSubscriptionPricingTierIds.Families,
name: this.i18nService.t("planNameFamilies"),
description: this.i18nService.t("planDescFamiliesV2"),
availableCadences: [SubscriptionCadenceIds.Annually],
passwordManager: {
type: "packaged",
users: familiesPlan.PasswordManager.baseSeats,
annualPrice: familiesPlan.PasswordManager.basePrice,
annualPricePerAdditionalStorageGB:
familiesPlan.PasswordManager.additionalStoragePricePerGb,
features: [
this.featureTranslations.premiumAccounts(),
this.featureTranslations.familiesUnlimitedSharing(),
this.featureTranslations.familiesUnlimitedCollections(),
this.featureTranslations.familiesSharedStorage(),
],
},
};
}),
);
private free$: Observable<BusinessSubscriptionPricingTier> = this.plansResponse$.pipe(
map((plans): BusinessSubscriptionPricingTier => {
const freePlan = plans.data.find((plan) => plan.type === PlanType.Free)!;
return {
id: BusinessSubscriptionPricingTierIds.Free,
name: this.i18nService.t("planNameFree"),
description: this.i18nService.t("planDescFreeV2", "1"),
availableCadences: [],
passwordManager: {
type: "free",
features: [
this.featureTranslations.limitedUsersV2(freePlan.PasswordManager.maxSeats),
this.featureTranslations.limitedCollectionsV2(freePlan.PasswordManager.maxCollections),
this.featureTranslations.alwaysFree(),
],
},
secretsManager: {
type: "free",
features: [
this.featureTranslations.twoSecretsIncluded(),
this.featureTranslations.projectsIncludedV2(freePlan.SecretsManager.maxProjects),
],
},
};
}),
);
private teams$: Observable<BusinessSubscriptionPricingTier> = this.plansResponse$.pipe(
map((plans) => {
const annualTeamsPlan = plans.data.find((plan) => plan.type === PlanType.TeamsAnnually)!;
return {
id: BusinessSubscriptionPricingTierIds.Teams,
name: this.i18nService.t("planNameTeams"),
description: this.i18nService.t("teamsPlanUpgradeMessage"),
availableCadences: [SubscriptionCadenceIds.Annually, SubscriptionCadenceIds.Monthly],
passwordManager: {
type: "scalable",
annualPricePerUser: annualTeamsPlan.PasswordManager.seatPrice,
annualPricePerAdditionalStorageGB:
annualTeamsPlan.PasswordManager.additionalStoragePricePerGb,
features: [
this.featureTranslations.secureItemSharing(),
this.featureTranslations.eventLogMonitoring(),
this.featureTranslations.directoryIntegration(),
this.featureTranslations.scimSupport(),
],
},
secretsManager: {
type: "scalable",
annualPricePerUser: annualTeamsPlan.SecretsManager.seatPrice,
annualPricePerAdditionalServiceAccount:
annualTeamsPlan.SecretsManager.additionalPricePerServiceAccount,
features: [
this.featureTranslations.unlimitedSecretsAndProjects(),
this.featureTranslations.includedMachineAccountsV2(
annualTeamsPlan.SecretsManager.baseServiceAccount,
),
],
},
};
}),
);
private enterprise$: Observable<BusinessSubscriptionPricingTier> = this.plansResponse$.pipe(
map((plans) => {
const annualEnterprisePlan = plans.data.find(
(plan) => plan.type === PlanType.EnterpriseAnnually,
)!;
return {
id: BusinessSubscriptionPricingTierIds.Enterprise,
name: this.i18nService.t("planNameEnterprise"),
description: this.i18nService.t("planDescEnterpriseV2"),
availableCadences: [SubscriptionCadenceIds.Annually, SubscriptionCadenceIds.Monthly],
passwordManager: {
type: "scalable",
annualPricePerUser: annualEnterprisePlan.PasswordManager.seatPrice,
annualPricePerAdditionalStorageGB:
annualEnterprisePlan.PasswordManager.additionalStoragePricePerGb,
features: [
this.featureTranslations.enterpriseSecurityPolicies(),
this.featureTranslations.passwordLessSso(),
this.featureTranslations.accountRecovery(),
this.featureTranslations.selfHostOption(),
this.featureTranslations.complimentaryFamiliesPlan(),
],
},
secretsManager: {
type: "scalable",
annualPricePerUser: annualEnterprisePlan.SecretsManager.seatPrice,
annualPricePerAdditionalServiceAccount:
annualEnterprisePlan.SecretsManager.additionalPricePerServiceAccount,
features: [
this.featureTranslations.unlimitedUsers(),
this.featureTranslations.includedMachineAccountsV2(
annualEnterprisePlan.SecretsManager.baseServiceAccount,
),
],
},
};
}),
);
private custom$: Observable<BusinessSubscriptionPricingTier> = this.plansResponse$.pipe(
map(
(): BusinessSubscriptionPricingTier => ({
id: BusinessSubscriptionPricingTierIds.Custom,
name: this.i18nService.t("planNameCustom"),
description: this.i18nService.t("planDescCustom"),
availableCadences: [],
passwordManager: {
type: "custom",
features: [
this.featureTranslations.strengthenCybersecurity(),
this.featureTranslations.boostProductivity(),
this.featureTranslations.seamlessIntegration(),
],
},
}),
),
);
private featureTranslations = {
builtInAuthenticator: () => ({
key: "builtInAuthenticator",
value: this.i18nService.t("builtInAuthenticator"),
}),
emergencyAccess: () => ({
key: "emergencyAccess",
value: this.i18nService.t("emergencyAccess"),
}),
breachMonitoring: () => ({
key: "breachMonitoring",
value: this.i18nService.t("breachMonitoring"),
}),
andMoreFeatures: () => ({
key: "andMoreFeatures",
value: this.i18nService.t("andMoreFeatures"),
}),
premiumAccounts: () => ({
key: "premiumAccounts",
value: this.i18nService.t("premiumAccounts"),
}),
secureFileStorage: () => ({
key: "secureFileStorage",
value: this.i18nService.t("secureFileStorage"),
}),
familiesUnlimitedSharing: () => ({
key: "familiesUnlimitedSharing",
value: this.i18nService.t("familiesUnlimitedSharing"),
}),
familiesUnlimitedCollections: () => ({
key: "familiesUnlimitedCollections",
value: this.i18nService.t("familiesUnlimitedCollections"),
}),
familiesSharedStorage: () => ({
key: "familiesSharedStorage",
value: this.i18nService.t("familiesSharedStorage"),
}),
limitedUsersV2: (users: number) => ({
key: "limitedUsersV2",
value: this.i18nService.t("limitedUsersV2", users),
}),
limitedCollectionsV2: (collections: number) => ({
key: "limitedCollectionsV2",
value: this.i18nService.t("limitedCollectionsV2", collections),
}),
alwaysFree: () => ({
key: "alwaysFree",
value: this.i18nService.t("alwaysFree"),
}),
twoSecretsIncluded: () => ({
key: "twoSecretsIncluded",
value: this.i18nService.t("twoSecretsIncluded"),
}),
projectsIncludedV2: (projects: number) => ({
key: "projectsIncludedV2",
value: this.i18nService.t("projectsIncludedV2", projects),
}),
secureItemSharing: () => ({
key: "secureItemSharing",
value: this.i18nService.t("secureItemSharing"),
}),
eventLogMonitoring: () => ({
key: "eventLogMonitoring",
value: this.i18nService.t("eventLogMonitoring"),
}),
directoryIntegration: () => ({
key: "directoryIntegration",
value: this.i18nService.t("directoryIntegration"),
}),
scimSupport: () => ({
key: "scimSupport",
value: this.i18nService.t("scimSupport"),
}),
unlimitedSecretsAndProjects: () => ({
key: "unlimitedSecretsAndProjects",
value: this.i18nService.t("unlimitedSecretsAndProjects"),
}),
includedMachineAccountsV2: (included: number) => ({
key: "includedMachineAccountsV2",
value: this.i18nService.t("includedMachineAccountsV2", included),
}),
enterpriseSecurityPolicies: () => ({
key: "enterpriseSecurityPolicies",
value: this.i18nService.t("enterpriseSecurityPolicies"),
}),
passwordLessSso: () => ({
key: "passwordLessSso",
value: this.i18nService.t("passwordLessSso"),
}),
accountRecovery: () => ({
key: "accountRecovery",
value: this.i18nService.t("accountRecovery"),
}),
selfHostOption: () => ({
key: "selfHostOption",
value: this.i18nService.t("selfHostOption"),
}),
complimentaryFamiliesPlan: () => ({
key: "complimentaryFamiliesPlan",
value: this.i18nService.t("complimentaryFamiliesPlan"),
}),
unlimitedUsers: () => ({
key: "unlimitedUsers",
value: this.i18nService.t("unlimitedUsers"),
}),
strengthenCybersecurity: () => ({
key: "strengthenCybersecurity",
value: this.i18nService.t("strengthenCybersecurity"),
}),
boostProductivity: () => ({
key: "boostProductivity",
value: this.i18nService.t("boostProductivity"),
}),
seamlessIntegration: () => ({
key: "seamlessIntegration",
value: this.i18nService.t("seamlessIntegration"),
}),
};
}

View File

@@ -0,0 +1,85 @@
export const PersonalSubscriptionPricingTierIds = {
Premium: "premium",
Families: "families",
} as const;
export const BusinessSubscriptionPricingTierIds = {
Free: "free",
Teams: "teams",
Enterprise: "enterprise",
Custom: "custom",
} as const;
export const SubscriptionCadenceIds = {
Annually: "annually",
Monthly: "monthly",
} as const;
export type PersonalSubscriptionPricingTierId =
(typeof PersonalSubscriptionPricingTierIds)[keyof typeof PersonalSubscriptionPricingTierIds];
export type BusinessSubscriptionPricingTierId =
(typeof BusinessSubscriptionPricingTierIds)[keyof typeof BusinessSubscriptionPricingTierIds];
export type SubscriptionCadence =
(typeof SubscriptionCadenceIds)[keyof typeof SubscriptionCadenceIds];
type HasFeatures = {
features: { key: string; value: string }[];
};
type HasAdditionalStorage = {
annualPricePerAdditionalStorageGB: number;
};
type StandalonePasswordManager = HasFeatures &
HasAdditionalStorage & {
type: "standalone";
annualPrice: number;
};
type PackagedPasswordManager = HasFeatures &
HasAdditionalStorage & {
type: "packaged";
users: number;
annualPrice: number;
};
type FreePasswordManager = HasFeatures & {
type: "free";
};
type CustomPasswordManager = HasFeatures & {
type: "custom";
};
type ScalablePasswordManager = HasFeatures &
HasAdditionalStorage & {
type: "scalable";
annualPricePerUser: number;
};
type FreeSecretsManager = HasFeatures & {
type: "free";
};
type ScalableSecretsManager = HasFeatures & {
type: "scalable";
annualPricePerUser: number;
annualPricePerAdditionalServiceAccount: number;
};
export type PersonalSubscriptionPricingTier = {
id: PersonalSubscriptionPricingTierId;
name: string;
description: string;
availableCadences: Omit<SubscriptionCadence, "monthly">[]; // personal plans are only ever annual
passwordManager: StandalonePasswordManager | PackagedPasswordManager;
};
export type BusinessSubscriptionPricingTier = {
id: BusinessSubscriptionPricingTierId;
name: string;
description: string;
availableCadences: SubscriptionCadence[];
passwordManager: FreePasswordManager | ScalablePasswordManager | CustomPasswordManager;
secretsManager?: FreeSecretsManager | ScalableSecretsManager;
};

View File

@@ -30,6 +30,7 @@ export enum FeatureFlag {
PM24996_ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog",
PM24033PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page",
PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service",
PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog",
/* Key Management */
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
@@ -118,6 +119,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog]: FALSE,
[FeatureFlag.PM24033PremiumUpgradeNewDesign]: FALSE,
[FeatureFlag.PM26793_FetchPremiumPriceFromPricingService]: FALSE,
[FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog]: FALSE,
/* Key Management */
[FeatureFlag.PrivateKeyRegeneration]: FALSE,

View File

@@ -22,13 +22,15 @@ export class MasterPasswordUnlockResponse extends BaseResponse {
this.kdf = new KdfConfigResponse(this.getResponseProperty("Kdf"));
const masterKeyEncryptedUserKey = this.getResponseProperty("MasterKeyEncryptedUserKey");
if (masterKeyEncryptedUserKey == null || typeof masterKeyEncryptedUserKey !== "string") {
// Note: MasterKeyEncryptedUserKey and masterKeyWrappedUserKey are the same thing, and
// used inconsistently in the codebase
const masterKeyWrappedUserKey = this.getResponseProperty("MasterKeyEncryptedUserKey");
if (masterKeyWrappedUserKey == null || typeof masterKeyWrappedUserKey !== "string") {
throw new Error(
"MasterPasswordUnlockResponse does not contain a valid master key encrypted user key",
);
}
this.masterKeyWrappedUserKey = masterKeyEncryptedUserKey as MasterKeyWrappedUserKey;
this.masterKeyWrappedUserKey = masterKeyWrappedUserKey as MasterKeyWrappedUserKey;
}
toMasterPasswordUnlockData() {

View File

@@ -80,9 +80,4 @@ export class AuditService implements AuditServiceAbstraction {
throw new Error();
}
}
async getKnownPhishingDomains(): Promise<string[]> {
const response = await this.apiService.send("GET", "/phishing-domains", null, true, true);
return response as string[];
}
}

View File

@@ -1,14 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AttachmentResponse } from "../response/attachment.response";
export class AttachmentData {
id: string;
url: string;
fileName: string;
key: string;
size: string;
sizeName: string;
id?: string;
url?: string;
fileName?: string;
key?: string;
size?: string;
sizeName?: string;
constructor(response?: AttachmentResponse) {
if (response == null) {

View File

@@ -1,14 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CardApi } from "../api/card.api";
export class CardData {
cardholderName: string;
brand: string;
number: string;
expMonth: string;
expYear: string;
code: string;
cardholderName?: string;
brand?: string;
number?: string;
expMonth?: string;
expYear?: string;
code?: string;
constructor(data?: CardApi) {
if (data == null) {

View File

@@ -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 { CipherRepromptType } from "../../enums/cipher-reprompt-type";
@@ -17,18 +15,18 @@ import { SecureNoteData } from "./secure-note.data";
import { SshKeyData } from "./ssh-key.data";
export class CipherData {
id: string;
organizationId: string;
folderId: string;
edit: boolean;
viewPassword: boolean;
permissions: CipherPermissionsApi;
organizationUseTotp: boolean;
favorite: boolean;
id: string = "";
organizationId?: string;
folderId?: string;
edit: boolean = false;
viewPassword: boolean = true;
permissions?: CipherPermissionsApi;
organizationUseTotp: boolean = false;
favorite: boolean = false;
revisionDate: string;
type: CipherType;
name: string;
notes: string;
type: CipherType = CipherType.Login;
name: string = "";
notes?: string;
login?: LoginData;
secureNote?: SecureNoteData;
card?: CardData;
@@ -39,13 +37,14 @@ export class CipherData {
passwordHistory?: PasswordHistoryData[];
collectionIds?: string[];
creationDate: string;
deletedDate: string | undefined;
archivedDate: string | undefined;
reprompt: CipherRepromptType;
key: string;
deletedDate?: string;
archivedDate?: string;
reprompt: CipherRepromptType = CipherRepromptType.None;
key?: string;
constructor(response?: CipherResponse, collectionIds?: string[]) {
if (response == null) {
this.creationDate = this.revisionDate = new Date().toISOString();
return;
}
@@ -101,7 +100,9 @@ export class CipherData {
static fromJSON(obj: Jsonify<CipherData>) {
const result = Object.assign(new CipherData(), obj);
result.permissions = CipherPermissionsApi.fromJSON(obj.permissions);
if (obj.permissions != null) {
result.permissions = CipherPermissionsApi.fromJSON(obj.permissions);
}
return result;
}
}

View File

@@ -1,21 +1,19 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Fido2CredentialApi } from "../api/fido2-credential.api";
export class Fido2CredentialData {
credentialId: string;
keyType: "public-key";
keyAlgorithm: "ECDSA";
keyCurve: "P-256";
keyValue: string;
rpId: string;
userHandle: string;
userName: string;
counter: string;
rpName: string;
userDisplayName: string;
discoverable: string;
creationDate: string;
credentialId!: string;
keyType!: string;
keyAlgorithm!: string;
keyCurve!: string;
keyValue!: string;
rpId!: string;
userHandle?: string;
userName?: string;
counter!: string;
rpName?: string;
userDisplayName?: string;
discoverable!: string;
creationDate!: string;
constructor(data?: Fido2CredentialApi) {
if (data == null) {

View File

@@ -1,13 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FieldType, LinkedIdType } from "../../enums";
import { FieldApi } from "../api/field.api";
export class FieldData {
type: FieldType;
name: string;
value: string;
linkedId: LinkedIdType | null;
type: FieldType = FieldType.Text;
name?: string;
value?: string;
linkedId?: LinkedIdType;
constructor(response?: FieldApi) {
if (response == null) {

View File

@@ -1,26 +1,24 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { IdentityApi } from "../api/identity.api";
export class IdentityData {
title: string;
firstName: string;
middleName: string;
lastName: string;
address1: string;
address2: string;
address3: string;
city: string;
state: string;
postalCode: string;
country: string;
company: string;
email: string;
phone: string;
ssn: string;
username: string;
passportNumber: string;
licenseNumber: string;
title?: string;
firstName?: string;
middleName?: string;
lastName?: string;
address1?: string;
address2?: string;
address3?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
company?: string;
email?: string;
phone?: string;
ssn?: string;
username?: string;
passportNumber?: string;
licenseNumber?: string;
constructor(data?: IdentityApi) {
if (data == null) {

View File

@@ -1,12 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { LoginUriApi } from "../api/login-uri.api";
export class LoginUriData {
uri: string;
uriChecksum: string;
match: UriMatchStrategySetting = null;
uri?: string;
uriChecksum?: string;
match?: UriMatchStrategySetting;
constructor(data?: LoginUriApi) {
if (data == null) {

View File

@@ -1,17 +1,15 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { LoginApi } from "../api/login.api";
import { Fido2CredentialData } from "./fido2-credential.data";
import { LoginUriData } from "./login-uri.data";
export class LoginData {
uris: LoginUriData[];
username: string;
password: string;
passwordRevisionDate: string;
totp: string;
autofillOnPageLoad: boolean;
uris?: LoginUriData[];
username?: string;
password?: string;
passwordRevisionDate?: string;
totp?: string;
autofillOnPageLoad?: boolean;
fido2Credentials?: Fido2CredentialData[];
constructor(data?: LoginApi) {

View File

@@ -1,10 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { PasswordHistoryResponse } from "../response/password-history.response";
export class PasswordHistoryData {
password: string;
lastUsedDate: string;
password!: string;
lastUsedDate!: string;
constructor(response?: PasswordHistoryResponse) {
if (response == null) {

View File

@@ -1,10 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { SecureNoteType } from "../../enums";
import { SecureNoteApi } from "../api/secure-note.api";
export class SecureNoteData {
type: SecureNoteType;
type: SecureNoteType = SecureNoteType.Generic;
constructor(data?: SecureNoteApi) {
if (data == null) {

View File

@@ -1,11 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { SshKeyApi } from "../api/ssh-key.api";
export class SshKeyData {
privateKey: string;
publicKey: string;
keyFingerprint: string;
privateKey!: string;
publicKey!: string;
keyFingerprint!: string;
constructor(data?: SshKeyApi) {
if (data == null) {

View File

@@ -39,6 +39,12 @@ describe("Attachment", () => {
key: undefined,
fileName: undefined,
});
expect(data.id).toBeUndefined();
expect(data.url).toBeUndefined();
expect(data.fileName).toBeUndefined();
expect(data.key).toBeUndefined();
expect(data.size).toBeUndefined();
expect(data.sizeName).toBeUndefined();
});
it("Convert", () => {

View File

@@ -29,6 +29,13 @@ describe("Card", () => {
expYear: undefined,
code: undefined,
});
expect(data.cardholderName).toBeUndefined();
expect(data.brand).toBeUndefined();
expect(data.number).toBeUndefined();
expect(data.expMonth).toBeUndefined();
expect(data.expYear).toBeUndefined();
expect(data.code).toBeUndefined();
});
it("Convert", () => {

View File

@@ -44,22 +44,22 @@ describe("Cipher DTO", () => {
const data = new CipherData();
const cipher = new Cipher(data);
expect(cipher.id).toBeUndefined();
expect(cipher.id).toEqual("");
expect(cipher.organizationId).toBeUndefined();
expect(cipher.folderId).toBeUndefined();
expect(cipher.name).toBeInstanceOf(EncString);
expect(cipher.notes).toBeUndefined();
expect(cipher.type).toBeUndefined();
expect(cipher.favorite).toBeUndefined();
expect(cipher.organizationUseTotp).toBeUndefined();
expect(cipher.edit).toBeUndefined();
expect(cipher.viewPassword).toBeUndefined();
expect(cipher.type).toEqual(CipherType.Login);
expect(cipher.favorite).toEqual(false);
expect(cipher.organizationUseTotp).toEqual(false);
expect(cipher.edit).toEqual(false);
expect(cipher.viewPassword).toEqual(true);
expect(cipher.revisionDate).toBeInstanceOf(Date);
expect(cipher.collectionIds).toEqual([]);
expect(cipher.localData).toBeUndefined();
expect(cipher.creationDate).toBeInstanceOf(Date);
expect(cipher.deletedDate).toBeUndefined();
expect(cipher.reprompt).toBeUndefined();
expect(cipher.reprompt).toEqual(CipherRepromptType.None);
expect(cipher.attachments).toBeUndefined();
expect(cipher.fields).toBeUndefined();
expect(cipher.passwordHistory).toBeUndefined();
@@ -836,6 +836,38 @@ describe("Cipher DTO", () => {
expect(actual).toBeInstanceOf(Cipher);
});
it("handles null permissions correctly without calling CipherPermissionsApi constructor", () => {
const spy = jest.spyOn(CipherPermissionsApi.prototype, "constructor" as any);
const revisionDate = new Date("2022-08-04T01:06:40.441Z");
const actual = Cipher.fromJSON({
name: "myName",
revisionDate: revisionDate.toISOString(),
permissions: null,
} as Jsonify<Cipher>);
expect(actual.permissions).toBeUndefined();
expect(actual).toBeInstanceOf(Cipher);
// Verify that CipherPermissionsApi constructor was not called for null permissions
expect(spy).not.toHaveBeenCalledWith(null);
spy.mockRestore();
});
it("calls CipherPermissionsApi constructor when permissions are provided", () => {
const spy = jest.spyOn(CipherPermissionsApi.prototype, "constructor" as any);
const revisionDate = new Date("2022-08-04T01:06:40.441Z");
const permissionsObj = { delete: true, restore: false };
const actual = Cipher.fromJSON({
name: "myName",
revisionDate: revisionDate.toISOString(),
permissions: permissionsObj,
} as Jsonify<Cipher>);
expect(actual.permissions).toBeInstanceOf(CipherPermissionsApi);
expect(actual.permissions.delete).toBe(true);
expect(actual.permissions.restore).toBe(false);
spy.mockRestore();
});
test.each([
// Test description, CipherType, expected output
["LoginView", CipherType.Login, { login: "myLogin_fromJSON" }],
@@ -1056,6 +1088,7 @@ describe("Cipher DTO", () => {
card: undefined,
secureNote: undefined,
sshKey: undefined,
data: undefined,
favorite: false,
reprompt: SdkCipherRepromptType.None,
organizationUseTotp: true,

View File

@@ -421,6 +421,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
card: undefined,
secureNote: undefined,
sshKey: undefined,
data: undefined,
};
switch (this.type) {

View File

@@ -29,7 +29,7 @@ describe("Field", () => {
const field = new Field(data);
expect(field).toEqual({
type: undefined,
type: FieldType.Text,
name: undefined,
value: undefined,
linkedId: undefined,

View File

@@ -53,6 +53,27 @@ describe("Identity", () => {
title: undefined,
username: undefined,
});
expect(data).toEqual({
title: undefined,
firstName: undefined,
middleName: undefined,
lastName: undefined,
address1: undefined,
address2: undefined,
address3: undefined,
city: undefined,
state: undefined,
postalCode: undefined,
country: undefined,
company: undefined,
email: undefined,
phone: undefined,
ssn: undefined,
username: undefined,
passportNumber: undefined,
licenseNumber: undefined,
});
});
it("Convert", () => {

View File

@@ -7,6 +7,7 @@ import { mockEnc, mockFromJson } from "../../../../spec";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import { UriMatchStrategy } from "../../../models/domain/domain-service";
import { LoginUriApi } from "../api/login-uri.api";
import { LoginUriData } from "../data/login-uri.data";
import { LoginUri } from "./login-uri";
@@ -31,6 +32,9 @@ describe("LoginUri", () => {
uri: undefined,
uriChecksum: undefined,
});
expect(data.uri).toBeUndefined();
expect(data.uriChecksum).toBeUndefined();
expect(data.match).toBeUndefined();
});
it("Convert", () => {
@@ -61,6 +65,23 @@ describe("LoginUri", () => {
});
});
it("handle null match", () => {
const apiData = Object.assign(new LoginUriApi(), {
uri: "testUri",
uriChecksum: "testChecksum",
match: null,
});
const loginUriData = new LoginUriData(apiData);
// The data model stores it as-is (null or undefined)
expect(loginUriData.match).toBeNull();
// But the domain model converts null to undefined
const loginUri = new LoginUri(loginUriData);
expect(loginUri.match).toBeUndefined();
});
describe("validateChecksum", () => {
let encryptService: MockProxy<EncryptService>;
@@ -118,7 +139,7 @@ describe("LoginUri", () => {
});
describe("SDK Login Uri Mapping", () => {
it("should map to SDK login uri", () => {
it("maps to SDK login uri", () => {
const loginUri = new LoginUri(data);
const sdkLoginUri = loginUri.toSdkLoginUri();

View File

@@ -25,6 +25,14 @@ describe("Login DTO", () => {
password: undefined,
totp: undefined,
});
expect(data.username).toBeUndefined();
expect(data.password).toBeUndefined();
expect(data.passwordRevisionDate).toBeUndefined();
expect(data.totp).toBeUndefined();
expect(data.autofillOnPageLoad).toBeUndefined();
expect(data.uris).toBeUndefined();
expect(data.fido2Credentials).toBeUndefined();
});
it("Convert from full LoginData", () => {

View File

@@ -111,10 +111,7 @@ export class Login extends Domain {
});
if (this.uris != null && this.uris.length > 0) {
l.uris = [];
this.uris.forEach((u) => {
l.uris.push(u.toLoginUriData());
});
l.uris = this.uris.map((u) => u.toLoginUriData());
}
if (this.fido2Credentials != null && this.fido2Credentials.length > 0) {

View File

@@ -20,6 +20,9 @@ describe("Password", () => {
expect(password).toBeInstanceOf(Password);
expect(password.password).toBeInstanceOf(EncString);
expect(password.lastUsedDate).toBeInstanceOf(Date);
expect(data.password).toBeUndefined();
expect(data.lastUsedDate).toBeUndefined();
});
it("Convert", () => {
@@ -83,4 +86,47 @@ describe("Password", () => {
});
});
});
describe("fromSdkPasswordHistory", () => {
beforeEach(() => {
jest.restoreAllMocks();
});
it("creates Password from SDK object", () => {
const sdkPasswordHistory = {
password: "2.encPassword|encryptedData" as EncryptedString,
lastUsedDate: "2022-01-31T12:00:00.000Z",
};
const password = Password.fromSdkPasswordHistory(sdkPasswordHistory);
expect(password).toBeInstanceOf(Password);
expect(password?.password).toBeInstanceOf(EncString);
expect(password?.password.encryptedString).toBe("2.encPassword|encryptedData");
expect(password?.lastUsedDate).toEqual(new Date("2022-01-31T12:00:00.000Z"));
});
it("returns undefined for null input", () => {
const result = Password.fromSdkPasswordHistory(null as any);
expect(result).toBeUndefined();
});
it("returns undefined for undefined input", () => {
const result = Password.fromSdkPasswordHistory(undefined);
expect(result).toBeUndefined();
});
it("handles empty SDK object", () => {
const sdkPasswordHistory = {
password: "" as EncryptedString,
lastUsedDate: "",
};
const password = Password.fromSdkPasswordHistory(sdkPasswordHistory);
expect(password).toBeInstanceOf(Password);
expect(password?.password).toBeInstanceOf(EncString);
expect(password?.lastUsedDate).toBeInstanceOf(Date);
});
});
});

View File

@@ -16,22 +16,27 @@ describe("SecureNote", () => {
const data = new SecureNoteData();
const secureNote = new SecureNote(data);
expect(secureNote).toEqual({
type: undefined,
});
expect(data).toBeDefined();
expect(secureNote).toEqual({ type: SecureNoteType.Generic });
expect(data.type).toBe(SecureNoteType.Generic);
});
it("Convert from undefined", () => {
const data = new SecureNoteData(undefined);
expect(data.type).toBe(SecureNoteType.Generic);
});
it("Convert", () => {
const secureNote = new SecureNote(data);
expect(secureNote).toEqual({
type: 0,
});
expect(secureNote).toEqual({ type: 0 });
expect(data.type).toBe(SecureNoteType.Generic);
});
it("toSecureNoteData", () => {
const secureNote = new SecureNote(data);
expect(secureNote.toSecureNoteData()).toEqual(data);
expect(secureNote.toSecureNoteData().type).toBe(SecureNoteType.Generic);
});
it("Decrypt", async () => {
@@ -49,6 +54,14 @@ describe("SecureNote", () => {
it("returns undefined if object is null", () => {
expect(SecureNote.fromJSON(null)).toBeUndefined();
});
it("creates SecureNote instance from JSON object", () => {
const jsonObj = { type: SecureNoteType.Generic };
const result = SecureNote.fromJSON(jsonObj);
expect(result).toBeInstanceOf(SecureNote);
expect(result.type).toBe(SecureNoteType.Generic);
});
});
describe("toSdkSecureNote", () => {
@@ -63,4 +76,71 @@ describe("SecureNote", () => {
});
});
});
describe("fromSdkSecureNote", () => {
it("returns undefined when null is provided", () => {
const result = SecureNote.fromSdkSecureNote(null);
expect(result).toBeUndefined();
});
it("returns undefined when undefined is provided", () => {
const result = SecureNote.fromSdkSecureNote(undefined);
expect(result).toBeUndefined();
});
it("creates SecureNote with Generic type from SDK object", () => {
const sdkSecureNote = {
type: SecureNoteType.Generic,
};
const result = SecureNote.fromSdkSecureNote(sdkSecureNote);
expect(result).toBeInstanceOf(SecureNote);
expect(result.type).toBe(SecureNoteType.Generic);
});
it("preserves the type value from SDK object", () => {
const sdkSecureNote = {
type: SecureNoteType.Generic,
};
const result = SecureNote.fromSdkSecureNote(sdkSecureNote);
expect(result.type).toBe(0);
});
it("creates a new SecureNote instance", () => {
const sdkSecureNote = {
type: SecureNoteType.Generic,
};
const result = SecureNote.fromSdkSecureNote(sdkSecureNote);
expect(result).not.toBe(sdkSecureNote);
expect(result).toBeInstanceOf(SecureNote);
});
it("handles SDK object with undefined type", () => {
const sdkSecureNote = {
type: undefined as SecureNoteType,
};
const result = SecureNote.fromSdkSecureNote(sdkSecureNote);
expect(result).toBeInstanceOf(SecureNote);
expect(result.type).toBeUndefined();
});
it("returns symmetric with toSdkSecureNote", () => {
const original = new SecureNote();
original.type = SecureNoteType.Generic;
const sdkFormat = original.toSdkSecureNote();
const reconstructed = SecureNote.fromSdkSecureNote(sdkFormat);
expect(reconstructed.type).toBe(original.type);
});
});
});

View File

@@ -1,4 +1,5 @@
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { EncString as SdkEncString, SshKey as SdkSshKey } from "@bitwarden/sdk-internal";
import { mockEnc } from "../../../../spec";
import { SshKeyApi } from "../api/ssh-key.api";
@@ -37,6 +38,9 @@ describe("Sshkey", () => {
expect(sshKey.privateKey).toBeInstanceOf(EncString);
expect(sshKey.publicKey).toBeInstanceOf(EncString);
expect(sshKey.keyFingerprint).toBeInstanceOf(EncString);
expect(data.privateKey).toBeUndefined();
expect(data.publicKey).toBeUndefined();
expect(data.keyFingerprint).toBeUndefined();
});
it("toSshKeyData", () => {
@@ -64,6 +68,21 @@ describe("Sshkey", () => {
it("returns undefined if object is null", () => {
expect(SshKey.fromJSON(null)).toBeUndefined();
});
it("creates SshKey instance from JSON object", () => {
const jsonObj = {
privateKey: "2.privateKey|encryptedData",
publicKey: "2.publicKey|encryptedData",
keyFingerprint: "2.keyFingerprint|encryptedData",
};
const result = SshKey.fromJSON(jsonObj);
expect(result).toBeInstanceOf(SshKey);
expect(result.privateKey).toBeDefined();
expect(result.publicKey).toBeDefined();
expect(result.keyFingerprint).toBeDefined();
});
});
describe("toSdkSshKey", () => {
@@ -78,4 +97,58 @@ describe("Sshkey", () => {
});
});
});
describe("fromSdkSshKey", () => {
it("returns undefined when null is provided", () => {
const result = SshKey.fromSdkSshKey(null);
expect(result).toBeUndefined();
});
it("returns undefined when undefined is provided", () => {
const result = SshKey.fromSdkSshKey(undefined);
expect(result).toBeUndefined();
});
it("creates SshKey from SDK object", () => {
const sdkSshKey: SdkSshKey = {
privateKey: "2.privateKey|encryptedData" as SdkEncString,
publicKey: "2.publicKey|encryptedData" as SdkEncString,
fingerprint: "2.keyFingerprint|encryptedData" as SdkEncString,
};
const result = SshKey.fromSdkSshKey(sdkSshKey);
expect(result).toBeInstanceOf(SshKey);
expect(result.privateKey).toBeDefined();
expect(result.publicKey).toBeDefined();
expect(result.keyFingerprint).toBeDefined();
});
it("creates a new SshKey instance", () => {
const sdkSshKey: SdkSshKey = {
privateKey: "2.privateKey|encryptedData" as SdkEncString,
publicKey: "2.publicKey|encryptedData" as SdkEncString,
fingerprint: "2.keyFingerprint|encryptedData" as SdkEncString,
};
const result = SshKey.fromSdkSshKey(sdkSshKey);
expect(result).not.toBe(sdkSshKey);
expect(result).toBeInstanceOf(SshKey);
});
it("is symmetric with toSdkSshKey", () => {
const original = new SshKey(data);
const sdkFormat = original.toSdkSshKey();
const reconstructed = SshKey.fromSdkSshKey(sdkFormat);
expect(reconstructed.privateKey.encryptedString).toBe(original.privateKey.encryptedString);
expect(reconstructed.publicKey.encryptedString).toBe(original.publicKey.encryptedString);
expect(reconstructed.keyFingerprint.encryptedString).toBe(
original.keyFingerprint.encryptedString,
);
});
});
});