From b579bc8f96e18a242884621103519afdb3bcad0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Wed, 3 Apr 2024 13:48:33 -0400 Subject: [PATCH] [PM-6818] legacy generator service adapter (#8582) * introduce legacy generators * introduce generator navigation service * Introduce default options. These accept a userId so that they can be policy-defined * replace `GeneratorOptions` with backwards compatible `GeneratorNavigation` --- .../components/generator.component.ts | 11 +- ...enerator-navigation.service.abstraction.ts | 42 + .../generator-strategy.abstraction.ts | 3 + .../generator.service.abstraction.ts | 3 + .../src/tools/generator/abstractions/index.ts | 1 + ...password-generation.service.abstraction.ts | 11 +- ...username-generation.service.abstraction.ts | 3 +- .../default-generator.service.spec.ts | 16 + .../generator/default-generator.service.ts | 11 +- .../src/tools/generator/generator-options.ts | 8 +- .../src/tools/generator/generator-type.ts | 2 + .../tools/generator/key-definition.spec.ts | 15 +- .../src/tools/generator/key-definitions.ts | 22 +- ...legacy-password-generation.service.spec.ts | 470 +++++++++++ .../legacy-password-generation.service.ts | 184 +++++ ...legacy-username-generation.service.spec.ts | 748 ++++++++++++++++++ .../legacy-username-generation.service.ts | 383 +++++++++ ...fault-generator-nativation.service.spec.ts | 100 +++ .../default-generator-navigation.service.ts | 71 ++ .../generator-navigation-evaluator.spec.ts | 64 ++ .../generator-navigation-evaluator.ts | 43 + .../generator-navigation-policy.spec.ts | 63 ++ .../navigation/generator-navigation-policy.ts | 39 + .../navigation/generator-navigation.ts | 26 + .../src/tools/generator/navigation/index.ts | 3 + .../src/tools/generator/passphrase/index.ts | 5 +- .../passphrase-generator-strategy.spec.ts | 19 +- .../passphrase-generator-strategy.ts | 14 +- .../src/tools/generator/password/index.ts | 2 +- .../password/password-generation.service.ts | 20 +- .../password/password-generator-options.ts | 12 +- .../password-generator-strategy.spec.ts | 11 + .../password/password-generator-strategy.ts | 14 +- .../username/catchall-generator-options.ts | 23 +- .../catchall-generator-strategy.spec.ts | 24 +- .../username/catchall-generator-strategy.ts | 16 +- .../eff-username-generator-options.ts | 12 +- .../eff-username-generator-strategy.spec.ts | 13 + .../eff-username-generator-strategy.ts | 14 +- .../forwarder-generator-strategy.spec.ts | 5 + .../username/forwarder-generator-strategy.ts | 5 +- .../username/forwarders/addy-io.spec.ts | 17 +- .../generator/username/forwarders/addy-io.ts | 22 + .../username/forwarders/duck-duck-go.spec.ts | 17 +- .../username/forwarders/duck-duck-go.ts | 18 + .../username/forwarders/fastmail.spec.ts | 17 +- .../generator/username/forwarders/fastmail.ts | 22 + .../username/forwarders/firefox-relay.spec.ts | 17 +- .../username/forwarders/firefox-relay.ts | 18 + .../username/forwarders/forward-email.spec.ts | 17 +- .../username/forwarders/forward-email.ts | 20 + .../username/forwarders/simple-login.spec.ts | 17 +- .../username/forwarders/simple-login.ts | 20 + .../src/tools/generator/username/index.ts | 2 +- .../generator/username/options/constants.ts | 69 -- .../username/options/generator-options.ts | 107 +-- .../tools/generator/username/options/index.ts | 2 - .../username/options/utilities.spec.ts | 243 ------ .../generator/username/options/utilities.ts | 72 -- .../username/subaddress-generator-options.ts | 18 +- .../subaddress-generator-strategy.spec.ts | 27 +- .../username/subaddress-generator-strategy.ts | 25 +- .../username/username-generation-options.ts | 40 +- .../username/username-generation.service.ts | 3 +- 64 files changed, 2759 insertions(+), 622 deletions(-) create mode 100644 libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts rename libs/common/src/tools/generator/{password => abstractions}/password-generation.service.abstraction.ts (69%) rename libs/common/src/tools/generator/{username => abstractions}/username-generation.service.abstraction.ts (75%) create mode 100644 libs/common/src/tools/generator/generator-type.ts create mode 100644 libs/common/src/tools/generator/legacy-password-generation.service.spec.ts create mode 100644 libs/common/src/tools/generator/legacy-password-generation.service.ts create mode 100644 libs/common/src/tools/generator/legacy-username-generation.service.spec.ts create mode 100644 libs/common/src/tools/generator/legacy-username-generation.service.ts create mode 100644 libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts create mode 100644 libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts create mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts create mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts create mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts create mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-policy.ts create mode 100644 libs/common/src/tools/generator/navigation/generator-navigation.ts create mode 100644 libs/common/src/tools/generator/navigation/index.ts delete mode 100644 libs/common/src/tools/generator/username/options/utilities.spec.ts delete mode 100644 libs/common/src/tools/generator/username/options/utilities.ts diff --git a/libs/angular/src/tools/generator/components/generator.component.ts b/libs/angular/src/tools/generator/components/generator.component.ts index d1c82a37b3..d1857a88ad 100644 --- a/libs/angular/src/tools/generator/components/generator.component.ts +++ b/libs/angular/src/tools/generator/components/generator.component.ts @@ -33,7 +33,7 @@ export class GeneratorComponent implements OnInit { subaddressOptions: any[]; catchallOptions: any[]; forwardOptions: EmailForwarderOptions[]; - usernameOptions: UsernameGeneratorOptions = {}; + usernameOptions: UsernameGeneratorOptions = { website: null }; passwordOptions: PasswordGeneratorOptions = {}; username = "-"; password = "-"; @@ -199,12 +199,12 @@ export class GeneratorComponent implements OnInit { } async sliderInput() { - this.normalizePasswordOptions(); + await this.normalizePasswordOptions(); this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions); } async savePasswordOptions(regenerate = true) { - this.normalizePasswordOptions(); + await this.normalizePasswordOptions(); await this.passwordGenerationService.saveOptions(this.passwordOptions); if (regenerate && this.regenerateWithoutButtonPress()) { @@ -271,7 +271,7 @@ export class GeneratorComponent implements OnInit { return this.type !== "username" || this.usernameOptions.type !== "forwarded"; } - private normalizePasswordOptions() { + private async normalizePasswordOptions() { // Application level normalize options dependent on class variables this.passwordOptions.ambiguous = !this.avoidAmbiguous; @@ -290,9 +290,8 @@ export class GeneratorComponent implements OnInit { } } - this.passwordGenerationService.normalizeOptions( + await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions( this.passwordOptions, - this.enforcedPasswordPolicyOptions, ); this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength); diff --git a/libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts new file mode 100644 index 0000000000..e9fb7e0bb4 --- /dev/null +++ b/libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts @@ -0,0 +1,42 @@ +import { Observable } from "rxjs"; + +import { UserId } from "../../../types/guid"; +import { GeneratorNavigation } from "../navigation/generator-navigation"; +import { GeneratorNavigationPolicy } from "../navigation/generator-navigation-policy"; + +import { PolicyEvaluator } from "./policy-evaluator.abstraction"; + +/** Loads and stores generator navigational data + */ +export abstract class GeneratorNavigationService { + /** An observable monitoring the options saved to disk. + * The observable updates when the options are saved. + * @param userId: Identifies the user making the request + */ + options$: (userId: UserId) => Observable; + + /** Gets the default options. */ + defaults$: (userId: UserId) => Observable; + + /** An observable monitoring the options used to enforce policy. + * The observable updates when the policy changes. + * @param userId: Identifies the user making the request + */ + evaluator$: ( + userId: UserId, + ) => Observable>; + + /** Enforces the policy on the given options + * @param userId: Identifies the user making the request + * @param options the options to enforce the policy on + * @returns a new instance of the options with the policy enforced + */ + enforcePolicy: (userId: UserId, options: GeneratorNavigation) => Promise; + + /** Saves the navigation options to disk. + * @param userId: Identifies the user making the request + * @param options the options to save + * @returns a promise that resolves when the options are saved + */ + saveOptions: (userId: UserId, options: GeneratorNavigation) => Promise; +} diff --git a/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts index eda02f7cdc..7cfe320abe 100644 --- a/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts @@ -17,6 +17,9 @@ export abstract class GeneratorStrategy { */ durableState: (userId: UserId) => SingleUserState; + /** Gets the default options. */ + defaults$: (userId: UserId) => Observable; + /** Identifies the policy enforced by the generator. */ policy: PolicyType; diff --git a/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts index f1820ed707..adb1165552 100644 --- a/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts @@ -21,6 +21,9 @@ export abstract class GeneratorService { */ evaluator$: (userId: UserId) => Observable>; + /** Gets the default options. */ + defaults$: (userId: UserId) => Observable; + /** Enforces the policy on the given options * @param userId: Identifies the user making the request * @param options the options to enforce the policy on diff --git a/libs/common/src/tools/generator/abstractions/index.ts b/libs/common/src/tools/generator/abstractions/index.ts index 03285dd5ff..13dce17d17 100644 --- a/libs/common/src/tools/generator/abstractions/index.ts +++ b/libs/common/src/tools/generator/abstractions/index.ts @@ -1,3 +1,4 @@ +export { GeneratorNavigationService } from "./generator-navigation.service.abstraction"; export { GeneratorService } from "./generator.service.abstraction"; export { GeneratorStrategy } from "./generator-strategy.abstraction"; export { PolicyEvaluator } from "./policy-evaluator.abstraction"; diff --git a/libs/common/src/tools/generator/password/password-generation.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/password-generation.service.abstraction.ts similarity index 69% rename from libs/common/src/tools/generator/password/password-generation.service.abstraction.ts rename to libs/common/src/tools/generator/abstractions/password-generation.service.abstraction.ts index b8dac20972..b3bd30be5c 100644 --- a/libs/common/src/tools/generator/password/password-generation.service.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/password-generation.service.abstraction.ts @@ -1,8 +1,8 @@ import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options"; +import { GeneratedPasswordHistory } from "../password/generated-password-history"; +import { PasswordGeneratorOptions } from "../password/password-generator-options"; -import { GeneratedPasswordHistory } from "./generated-password-history"; -import { PasswordGeneratorOptions } from "./password-generator-options"; - +/** @deprecated Use {@link GeneratorService} with a password or passphrase {@link GeneratorStrategy} instead. */ export abstract class PasswordGenerationServiceAbstraction { generatePassword: (options: PasswordGeneratorOptions) => Promise; generatePassphrase: (options: PasswordGeneratorOptions) => Promise; @@ -10,13 +10,8 @@ export abstract class PasswordGenerationServiceAbstraction { enforcePasswordGeneratorPoliciesOnOptions: ( options: PasswordGeneratorOptions, ) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>; - getPasswordGeneratorPolicyOptions: () => Promise; saveOptions: (options: PasswordGeneratorOptions) => Promise; getHistory: () => Promise; addHistory: (password: string) => Promise; clear: (userId?: string) => Promise; - normalizeOptions: ( - options: PasswordGeneratorOptions, - enforcedPolicyOptions: PasswordGeneratorPolicyOptions, - ) => void; } diff --git a/libs/common/src/tools/generator/username/username-generation.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/username-generation.service.abstraction.ts similarity index 75% rename from libs/common/src/tools/generator/username/username-generation.service.abstraction.ts rename to libs/common/src/tools/generator/abstractions/username-generation.service.abstraction.ts index 05affef0e2..02b25e6113 100644 --- a/libs/common/src/tools/generator/username/username-generation.service.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/username-generation.service.abstraction.ts @@ -1,5 +1,6 @@ -import { UsernameGeneratorOptions } from "./username-generation-options"; +import { UsernameGeneratorOptions } from "../username/username-generation-options"; +/** @deprecated Use {@link GeneratorService} with a username {@link GeneratorStrategy} instead. */ export abstract class UsernameGenerationServiceAbstraction { generateUsername: (options: UsernameGeneratorOptions) => Promise; generateWord: (options: UsernameGeneratorOptions) => Promise; diff --git a/libs/common/src/tools/generator/default-generator.service.spec.ts b/libs/common/src/tools/generator/default-generator.service.spec.ts index 53a46c4963..c93aec44d9 100644 --- a/libs/common/src/tools/generator/default-generator.service.spec.ts +++ b/libs/common/src/tools/generator/default-generator.service.spec.ts @@ -37,6 +37,7 @@ function mockGeneratorStrategy(config?: { userState?: SingleUserState; policy?: PolicyType; evaluator?: any; + defaults?: any; }) { const durableState = config?.userState ?? new FakeSingleUserState(SomeUser); @@ -45,6 +46,7 @@ function mockGeneratorStrategy(config?: { // whether they're used properly are guaranteed to test // the value from `config`. durableState: jest.fn(() => durableState), + defaults$: jest.fn(() => new BehaviorSubject(config?.defaults)), policy: config?.policy ?? PolicyType.DisableSend, toEvaluator: jest.fn(() => pipe(map(() => config?.evaluator ?? mock>())), @@ -72,6 +74,20 @@ describe("Password generator service", () => { }); }); + describe("defaults$", () => { + it("should retrieve default state from the service", async () => { + const policy = mockPolicyService(); + const defaults = {}; + const strategy = mockGeneratorStrategy({ defaults }); + const service = new DefaultGeneratorService(strategy, policy); + + const result = await firstValueFrom(service.defaults$(SomeUser)); + + expect(strategy.defaults$).toHaveBeenCalledWith(SomeUser); + expect(result).toBe(defaults); + }); + }); + describe("saveOptions()", () => { it("should trigger an options$ update", async () => { const policy = mockPolicyService(); diff --git a/libs/common/src/tools/generator/default-generator.service.ts b/libs/common/src/tools/generator/default-generator.service.ts index 34aacee695..7fd794472c 100644 --- a/libs/common/src/tools/generator/default-generator.service.ts +++ b/libs/common/src/tools/generator/default-generator.service.ts @@ -21,17 +21,22 @@ export class DefaultGeneratorService implements GeneratorServic private _evaluators$ = new Map>>(); - /** {@link GeneratorService.options$()} */ + /** {@link GeneratorService.options$} */ options$(userId: UserId) { return this.strategy.durableState(userId).state$; } + /** {@link GeneratorService.defaults$} */ + defaults$(userId: UserId) { + return this.strategy.defaults$(userId); + } + /** {@link GeneratorService.saveOptions} */ async saveOptions(userId: UserId, options: Options): Promise { await this.strategy.durableState(userId).update(() => options); } - /** {@link GeneratorService.evaluator$()} */ + /** {@link GeneratorService.evaluator$} */ evaluator$(userId: UserId) { let evaluator$ = this._evaluators$.get(userId); @@ -59,7 +64,7 @@ export class DefaultGeneratorService implements GeneratorServic return evaluator$; } - /** {@link GeneratorService.enforcePolicy()} */ + /** {@link GeneratorService.enforcePolicy} */ async enforcePolicy(userId: UserId, options: Options): Promise { const policy = await firstValueFrom(this.evaluator$(userId)); const evaluated = policy.applyPolicy(options); diff --git a/libs/common/src/tools/generator/generator-options.ts b/libs/common/src/tools/generator/generator-options.ts index 4f8eb293ab..d3d08025fa 100644 --- a/libs/common/src/tools/generator/generator-options.ts +++ b/libs/common/src/tools/generator/generator-options.ts @@ -1,3 +1,5 @@ -export type GeneratorOptions = { - type?: "password" | "username"; -}; +// this export provided solely for backwards compatibility +export { + /** @deprecated use `GeneratorNavigation` from './navigation' instead. */ + GeneratorNavigation as GeneratorOptions, +} from "./navigation/generator-navigation"; diff --git a/libs/common/src/tools/generator/generator-type.ts b/libs/common/src/tools/generator/generator-type.ts new file mode 100644 index 0000000000..f17eeb9c92 --- /dev/null +++ b/libs/common/src/tools/generator/generator-type.ts @@ -0,0 +1,2 @@ +/** The kind of credential being generated. */ +export type GeneratorType = "password" | "passphrase" | "username"; diff --git a/libs/common/src/tools/generator/key-definition.spec.ts b/libs/common/src/tools/generator/key-definition.spec.ts index f21767e77e..9cbbc44e14 100644 --- a/libs/common/src/tools/generator/key-definition.spec.ts +++ b/libs/common/src/tools/generator/key-definition.spec.ts @@ -10,9 +10,18 @@ import { FASTMAIL_FORWARDER, DUCK_DUCK_GO_FORWARDER, ADDY_IO_FORWARDER, + GENERATOR_SETTINGS, } from "./key-definitions"; describe("Key definitions", () => { + describe("GENERATOR_SETTINGS", () => { + it("should pass through deserialization", () => { + const value = {}; + const result = GENERATOR_SETTINGS.deserializer(value); + expect(result).toBe(value); + }); + }); + describe("PASSWORD_SETTINGS", () => { it("should pass through deserialization", () => { const value = {}; @@ -31,7 +40,7 @@ describe("Key definitions", () => { describe("EFF_USERNAME_SETTINGS", () => { it("should pass through deserialization", () => { - const value = {}; + const value = { website: null as string }; const result = EFF_USERNAME_SETTINGS.deserializer(value); expect(result).toBe(value); }); @@ -39,7 +48,7 @@ describe("Key definitions", () => { describe("CATCHALL_SETTINGS", () => { it("should pass through deserialization", () => { - const value = {}; + const value = { website: null as string }; const result = CATCHALL_SETTINGS.deserializer(value); expect(result).toBe(value); }); @@ -47,7 +56,7 @@ describe("Key definitions", () => { describe("SUBADDRESS_SETTINGS", () => { it("should pass through deserialization", () => { - const value = {}; + const value = { website: null as string }; const result = SUBADDRESS_SETTINGS.deserializer(value); expect(result).toBe(value); }); diff --git a/libs/common/src/tools/generator/key-definitions.ts b/libs/common/src/tools/generator/key-definitions.ts index d51af70f2e..2f35169612 100644 --- a/libs/common/src/tools/generator/key-definitions.ts +++ b/libs/common/src/tools/generator/key-definitions.ts @@ -1,6 +1,7 @@ -import { GENERATOR_DISK, KeyDefinition } from "../../platform/state"; +import { GENERATOR_DISK, GENERATOR_MEMORY, KeyDefinition } from "../../platform/state"; import { GeneratedCredential } from "./history/generated-credential"; +import { GeneratorNavigation } from "./navigation/generator-navigation"; import { PassphraseGenerationOptions } from "./passphrase/passphrase-generation-options"; import { PasswordGenerationOptions } from "./password/password-generation-options"; import { SecretClassifier } from "./state/secret-classifier"; @@ -15,6 +16,15 @@ import { } from "./username/options/forwarder-options"; import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; +/** plaintext password generation options */ +export const GENERATOR_SETTINGS = new KeyDefinition( + GENERATOR_MEMORY, + "generatorSettings", + { + deserializer: (value) => value, + }, +); + /** plaintext password generation options */ export const PASSWORD_SETTINGS = new KeyDefinition( GENERATOR_DISK, @@ -42,7 +52,7 @@ export const EFF_USERNAME_SETTINGS = new KeyDefinition( GENERATOR_DISK, "catchallGeneratorSettings", @@ -51,7 +61,7 @@ export const CATCHALL_SETTINGS = new KeyDefinition( }, ); -/** email subaddress generation options */ +/** plaintext configuration for an email subaddress. */ export const SUBADDRESS_SETTINGS = new KeyDefinition( GENERATOR_DISK, "subaddressGeneratorSettings", @@ -60,6 +70,7 @@ export const SUBADDRESS_SETTINGS = new KeyDefinition( GENERATOR_DISK, "addyIoForwarder", @@ -68,6 +79,7 @@ export const ADDY_IO_FORWARDER = new KeyDefinition( GENERATOR_DISK, "duckDuckGoForwarder", @@ -76,6 +88,7 @@ export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition( }, ); +/** backing store configuration for {@link Forwarders.FastMail} */ export const FASTMAIL_FORWARDER = new KeyDefinition( GENERATOR_DISK, "fastmailForwarder", @@ -84,6 +97,7 @@ export const FASTMAIL_FORWARDER = new KeyDefinition( GENERATOR_DISK, "firefoxRelayForwarder", @@ -92,6 +106,7 @@ export const FIREFOX_RELAY_FORWARDER = new KeyDefinition( }, ); +/** backing store configuration for {@link Forwarders.ForwardEmail} */ export const FORWARD_EMAIL_FORWARDER = new KeyDefinition( GENERATOR_DISK, "forwardEmailForwarder", @@ -100,6 +115,7 @@ export const FORWARD_EMAIL_FORWARDER = new KeyDefinition( GENERATOR_DISK, "simpleLoginForwarder", diff --git a/libs/common/src/tools/generator/legacy-password-generation.service.spec.ts b/libs/common/src/tools/generator/legacy-password-generation.service.spec.ts new file mode 100644 index 0000000000..093c68b3e8 --- /dev/null +++ b/libs/common/src/tools/generator/legacy-password-generation.service.spec.ts @@ -0,0 +1,470 @@ +/** + * include structuredClone in test environment. + * @jest-environment ../../../shared/test.environment.ts + */ +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { mockAccountServiceWith } from "../../../spec"; +import { UserId } from "../../types/guid"; + +import { GeneratorNavigationService, GeneratorService } from "./abstractions"; +import { LegacyPasswordGenerationService } from "./legacy-password-generation.service"; +import { DefaultGeneratorNavigation, GeneratorNavigation } from "./navigation/generator-navigation"; +import { GeneratorNavigationEvaluator } from "./navigation/generator-navigation-evaluator"; +import { GeneratorNavigationPolicy } from "./navigation/generator-navigation-policy"; +import { + DefaultPassphraseGenerationOptions, + PassphraseGenerationOptions, + PassphraseGeneratorOptionsEvaluator, + PassphraseGeneratorPolicy, +} from "./passphrase"; +import { DisabledPassphraseGeneratorPolicy } from "./passphrase/passphrase-generator-policy"; +import { + DefaultPasswordGenerationOptions, + PasswordGenerationOptions, + PasswordGeneratorOptions, + PasswordGeneratorOptionsEvaluator, + PasswordGeneratorPolicy, +} from "./password"; +import { DisabledPasswordGeneratorPolicy } from "./password/password-generator-policy"; + +const SomeUser = "some user" as UserId; + +function createPassphraseGenerator( + options: PassphraseGenerationOptions = {}, + policy: PassphraseGeneratorPolicy = DisabledPassphraseGeneratorPolicy, +) { + let savedOptions = options; + const generator = mock>({ + evaluator$(id: UserId) { + const evaluator = new PassphraseGeneratorOptionsEvaluator(policy); + return of(evaluator); + }, + options$(id: UserId) { + return of(savedOptions); + }, + defaults$(id: UserId) { + return of(DefaultPassphraseGenerationOptions); + }, + saveOptions(userId, options) { + savedOptions = options; + return Promise.resolve(); + }, + }); + + return generator; +} + +function createPasswordGenerator( + options: PasswordGenerationOptions = {}, + policy: PasswordGeneratorPolicy = DisabledPasswordGeneratorPolicy, +) { + let savedOptions = options; + const generator = mock>({ + evaluator$(id: UserId) { + const evaluator = new PasswordGeneratorOptionsEvaluator(policy); + return of(evaluator); + }, + options$(id: UserId) { + return of(savedOptions); + }, + defaults$(id: UserId) { + return of(DefaultPasswordGenerationOptions); + }, + saveOptions(userId, options) { + savedOptions = options; + return Promise.resolve(); + }, + }); + + return generator; +} + +function createNavigationGenerator( + options: GeneratorNavigation = {}, + policy: GeneratorNavigationPolicy = {}, +) { + let savedOptions = options; + const generator = mock({ + evaluator$(id: UserId) { + const evaluator = new GeneratorNavigationEvaluator(policy); + return of(evaluator); + }, + options$(id: UserId) { + return of(savedOptions); + }, + defaults$(id: UserId) { + return of(DefaultGeneratorNavigation); + }, + saveOptions(userId, options) { + savedOptions = options; + return Promise.resolve(); + }, + }); + + return generator; +} + +describe("LegacyPasswordGenerationService", () => { + // NOTE: in all tests, `null` constructor arguments are not used by the test. + // They're set to `null` to avoid setting up unnecessary mocks. + + describe("generatePassword", () => { + it("invokes the inner password generator to generate passwords", async () => { + const innerPassword = createPasswordGenerator(); + const generator = new LegacyPasswordGenerationService(null, null, innerPassword, null); + const options = { type: "password" } as PasswordGeneratorOptions; + + await generator.generatePassword(options); + + expect(innerPassword.generate).toHaveBeenCalledWith(options); + }); + + it("invokes the inner passphrase generator to generate passphrases", async () => { + const innerPassphrase = createPassphraseGenerator(); + const generator = new LegacyPasswordGenerationService(null, null, null, innerPassphrase); + const options = { type: "passphrase" } as PasswordGeneratorOptions; + + await generator.generatePassword(options); + + expect(innerPassphrase.generate).toHaveBeenCalledWith(options); + }); + }); + + describe("generatePassphrase", () => { + it("invokes the inner passphrase generator", async () => { + const innerPassphrase = createPassphraseGenerator(); + const generator = new LegacyPasswordGenerationService(null, null, null, innerPassphrase); + const options = {} as PasswordGeneratorOptions; + + await generator.generatePassphrase(options); + + expect(innerPassphrase.generate).toHaveBeenCalledWith(options); + }); + }); + + describe("getOptions", () => { + it("combines options from its inner services", async () => { + const innerPassword = createPasswordGenerator({ + length: 29, + minLength: 20, + ambiguous: false, + uppercase: true, + minUppercase: 1, + lowercase: false, + minLowercase: 2, + number: true, + minNumber: 3, + special: false, + minSpecial: 4, + }); + const innerPassphrase = createPassphraseGenerator({ + numWords: 10, + wordSeparator: "-", + capitalize: true, + includeNumber: false, + }); + const navigation = createNavigationGenerator({ + type: "passphrase", + username: "word", + forwarder: "simplelogin", + }); + const accountService = mockAccountServiceWith(SomeUser); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [result] = await generator.getOptions(); + + expect(result).toEqual({ + type: "passphrase", + username: "word", + forwarder: "simplelogin", + length: 29, + minLength: 20, + ambiguous: false, + uppercase: true, + minUppercase: 1, + lowercase: false, + minLowercase: 2, + number: true, + minNumber: 3, + special: false, + minSpecial: 4, + numWords: 10, + wordSeparator: "-", + capitalize: true, + includeNumber: false, + }); + }); + + it("sets default options when an inner service lacks a value", async () => { + const innerPassword = createPasswordGenerator(null); + const innerPassphrase = createPassphraseGenerator(null); + const navigation = createNavigationGenerator(null); + const accountService = mockAccountServiceWith(SomeUser); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [result] = await generator.getOptions(); + + expect(result).toEqual({ + ...DefaultGeneratorNavigation, + ...DefaultPassphraseGenerationOptions, + ...DefaultPasswordGenerationOptions, + }); + }); + + it("combines policies from its inner services", async () => { + const innerPassword = createPasswordGenerator( + {}, + { + minLength: 20, + numberCount: 10, + specialCount: 11, + useUppercase: true, + useLowercase: false, + useNumbers: true, + useSpecial: false, + }, + ); + const innerPassphrase = createPassphraseGenerator( + {}, + { + minNumberWords: 5, + capitalize: true, + includeNumber: false, + }, + ); + const accountService = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator( + {}, + { + defaultType: "password", + }, + ); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [, policy] = await generator.getOptions(); + + expect(policy).toEqual({ + defaultType: "password", + minLength: 20, + numberCount: 10, + specialCount: 11, + useUppercase: true, + useLowercase: false, + useNumbers: true, + useSpecial: false, + minNumberWords: 5, + capitalize: true, + includeNumber: false, + }); + }); + }); + + describe("enforcePasswordGeneratorPoliciesOnOptions", () => { + it("returns its options parameter with password policy applied", async () => { + const innerPassword = createPasswordGenerator( + {}, + { + minLength: 15, + numberCount: 5, + specialCount: 5, + useUppercase: true, + useLowercase: true, + useNumbers: true, + useSpecial: true, + }, + ); + const innerPassphrase = createPassphraseGenerator(); + const accountService = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator(); + const options = { + type: "password" as const, + }; + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options); + + expect(result).toBe(options); + expect(result).toMatchObject({ + length: 15, + minLength: 15, + minLowercase: 1, + minNumber: 5, + minUppercase: 1, + minSpecial: 5, + uppercase: true, + lowercase: true, + number: true, + special: true, + }); + }); + + it("returns its options parameter with passphrase policy applied", async () => { + const innerPassword = createPasswordGenerator(); + const innerPassphrase = createPassphraseGenerator( + {}, + { + minNumberWords: 5, + capitalize: true, + includeNumber: true, + }, + ); + const accountService = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator(); + const options = { + type: "passphrase" as const, + }; + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options); + + expect(result).toBe(options); + expect(result).toMatchObject({ + numWords: 5, + capitalize: true, + includeNumber: true, + }); + }); + + it("returns the applied policy", async () => { + const innerPassword = createPasswordGenerator( + {}, + { + minLength: 20, + numberCount: 10, + specialCount: 11, + useUppercase: true, + useLowercase: false, + useNumbers: true, + useSpecial: false, + }, + ); + const innerPassphrase = createPassphraseGenerator( + {}, + { + minNumberWords: 5, + capitalize: true, + includeNumber: false, + }, + ); + const accountService = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator( + {}, + { + defaultType: "password", + }, + ); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({}); + + expect(policy).toEqual({ + defaultType: "password", + minLength: 20, + numberCount: 10, + specialCount: 11, + useUppercase: true, + useLowercase: false, + useNumbers: true, + useSpecial: false, + minNumberWords: 5, + capitalize: true, + includeNumber: false, + }); + }); + }); + + describe("saveOptions", () => { + it("loads saved password options", async () => { + const innerPassword = createPasswordGenerator(); + const innerPassphrase = createPassphraseGenerator(); + const navigation = createNavigationGenerator(); + const accountService = mockAccountServiceWith(SomeUser); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + const options = { + type: "password" as const, + username: "word" as const, + forwarder: "simplelogin" as const, + length: 29, + minLength: 20, + ambiguous: false, + uppercase: true, + minUppercase: 1, + lowercase: false, + minLowercase: 2, + number: true, + minNumber: 3, + special: false, + minSpecial: 4, + }; + await generator.saveOptions(options); + + const [result] = await generator.getOptions(); + + expect(result).toMatchObject(options); + }); + + it("loads saved passphrase options", async () => { + const innerPassword = createPasswordGenerator(); + const innerPassphrase = createPassphraseGenerator(); + const navigation = createNavigationGenerator(); + const accountService = mockAccountServiceWith(SomeUser); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + const options = { + type: "passphrase" as const, + username: "word" as const, + forwarder: "simplelogin" as const, + numWords: 10, + wordSeparator: "-", + capitalize: true, + includeNumber: false, + }; + await generator.saveOptions(options); + + const [result] = await generator.getOptions(); + + expect(result).toMatchObject(options); + }); + }); +}); diff --git a/libs/common/src/tools/generator/legacy-password-generation.service.ts b/libs/common/src/tools/generator/legacy-password-generation.service.ts new file mode 100644 index 0000000000..0b429b356b --- /dev/null +++ b/libs/common/src/tools/generator/legacy-password-generation.service.ts @@ -0,0 +1,184 @@ +import { concatMap, zip, map, firstValueFrom } from "rxjs"; + +import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; +import { PasswordGeneratorPolicyOptions } from "../../admin-console/models/domain/password-generator-policy-options"; +import { AccountService } from "../../auth/abstractions/account.service"; +import { CryptoService } from "../../platform/abstractions/crypto.service"; +import { StateProvider } from "../../platform/state"; + +import { GeneratorService, GeneratorNavigationService } from "./abstractions"; +import { PasswordGenerationServiceAbstraction } from "./abstractions/password-generation.service.abstraction"; +import { DefaultGeneratorService } from "./default-generator.service"; +import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service"; +import { + PassphraseGenerationOptions, + PassphraseGeneratorPolicy, + PassphraseGeneratorStrategy, +} from "./passphrase"; +import { + PasswordGenerationOptions, + PasswordGenerationService, + PasswordGeneratorOptions, + PasswordGeneratorPolicy, + PasswordGeneratorStrategy, +} from "./password"; + +export function legacyPasswordGenerationServiceFactory( + cryptoService: CryptoService, + policyService: PolicyService, + accountService: AccountService, + stateProvider: StateProvider, +): PasswordGenerationServiceAbstraction { + // FIXME: Once the password generation service is replaced with this service + // in the clients, factor out the deprecated service in its entirety. + const deprecatedService = new PasswordGenerationService(cryptoService, null, null); + + const passwords = new DefaultGeneratorService( + new PasswordGeneratorStrategy(deprecatedService, stateProvider), + policyService, + ); + + const passphrases = new DefaultGeneratorService( + new PassphraseGeneratorStrategy(deprecatedService, stateProvider), + policyService, + ); + + const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService); + + return new LegacyPasswordGenerationService(accountService, navigation, passwords, passphrases); +} + +/** Adapts the generator 2.0 design to 1.0 angular services. */ +export class LegacyPasswordGenerationService implements PasswordGenerationServiceAbstraction { + constructor( + private readonly accountService: AccountService, + private readonly navigation: GeneratorNavigationService, + private readonly passwords: GeneratorService< + PasswordGenerationOptions, + PasswordGeneratorPolicy + >, + private readonly passphrases: GeneratorService< + PassphraseGenerationOptions, + PassphraseGeneratorPolicy + >, + ) {} + + generatePassword(options: PasswordGeneratorOptions) { + if (options.type === "password") { + return this.passwords.generate(options); + } else { + return this.passphrases.generate(options); + } + } + + generatePassphrase(options: PasswordGeneratorOptions) { + return this.passphrases.generate(options); + } + + async getOptions() { + const options$ = this.accountService.activeAccount$.pipe( + concatMap((activeUser) => + zip( + this.passwords.options$(activeUser.id), + this.passwords.defaults$(activeUser.id), + this.passwords.evaluator$(activeUser.id), + this.passphrases.options$(activeUser.id), + this.passphrases.defaults$(activeUser.id), + this.passphrases.evaluator$(activeUser.id), + this.navigation.options$(activeUser.id), + this.navigation.defaults$(activeUser.id), + this.navigation.evaluator$(activeUser.id), + ), + ), + map( + ([ + passwordOptions, + passwordDefaults, + passwordEvaluator, + passphraseOptions, + passphraseDefaults, + passphraseEvaluator, + generatorOptions, + generatorDefaults, + generatorEvaluator, + ]) => { + const options: PasswordGeneratorOptions = Object.assign( + {}, + passwordOptions ?? passwordDefaults, + passphraseOptions ?? passphraseDefaults, + generatorOptions ?? generatorDefaults, + ); + + const policy = Object.assign( + new PasswordGeneratorPolicyOptions(), + passwordEvaluator.policy, + passphraseEvaluator.policy, + generatorEvaluator.policy, + ); + + return [options, policy] as [PasswordGenerationOptions, PasswordGeneratorPolicyOptions]; + }, + ), + ); + + const options = await firstValueFrom(options$); + return options; + } + + async enforcePasswordGeneratorPoliciesOnOptions(options: PasswordGeneratorOptions) { + const options$ = this.accountService.activeAccount$.pipe( + concatMap((activeUser) => + zip( + this.passwords.evaluator$(activeUser.id), + this.passphrases.evaluator$(activeUser.id), + this.navigation.evaluator$(activeUser.id), + ), + ), + map(([passwordEvaluator, passphraseEvaluator, navigationEvaluator]) => { + const policy = Object.assign( + new PasswordGeneratorPolicyOptions(), + passwordEvaluator.policy, + passphraseEvaluator.policy, + navigationEvaluator.policy, + ); + + const navigationApplied = navigationEvaluator.applyPolicy(options); + const navigationSanitized = { + ...options, + ...navigationEvaluator.sanitize(navigationApplied), + }; + if (options.type === "password") { + const applied = passwordEvaluator.applyPolicy(navigationSanitized); + const sanitized = passwordEvaluator.sanitize(applied); + return [sanitized, policy]; + } else { + const applied = passphraseEvaluator.applyPolicy(navigationSanitized); + const sanitized = passphraseEvaluator.sanitize(applied); + return [sanitized, policy]; + } + }), + ); + + const [sanitized, policy] = await firstValueFrom(options$); + return [ + // callers assume this function updates the options parameter + Object.assign(options, sanitized), + policy, + ] as [PasswordGenerationOptions, PasswordGeneratorPolicyOptions]; + } + + async saveOptions(options: PasswordGeneratorOptions) { + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + await this.navigation.saveOptions(activeAccount.id, options); + if (options.type === "password") { + await this.passwords.saveOptions(activeAccount.id, options); + } else { + await this.passphrases.saveOptions(activeAccount.id, options); + } + } + + getHistory: () => Promise; + addHistory: (password: string) => Promise; + clear: (userId?: string) => Promise; +} diff --git a/libs/common/src/tools/generator/legacy-username-generation.service.spec.ts b/libs/common/src/tools/generator/legacy-username-generation.service.spec.ts new file mode 100644 index 0000000000..41d9c78dd2 --- /dev/null +++ b/libs/common/src/tools/generator/legacy-username-generation.service.spec.ts @@ -0,0 +1,748 @@ +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { mockAccountServiceWith } from "../../../spec"; +import { UserId } from "../../types/guid"; + +import { GeneratorNavigationService, GeneratorService } from "./abstractions"; +import { DefaultPolicyEvaluator } from "./default-policy-evaluator"; +import { LegacyUsernameGenerationService } from "./legacy-username-generation.service"; +import { DefaultGeneratorNavigation, GeneratorNavigation } from "./navigation/generator-navigation"; +import { GeneratorNavigationEvaluator } from "./navigation/generator-navigation-evaluator"; +import { GeneratorNavigationPolicy } from "./navigation/generator-navigation-policy"; +import { NoPolicy } from "./no-policy"; +import { UsernameGeneratorOptions } from "./username"; +import { + CatchallGenerationOptions, + DefaultCatchallOptions, +} from "./username/catchall-generator-options"; +import { + DefaultEffUsernameOptions, + EffUsernameGenerationOptions, +} from "./username/eff-username-generator-options"; +import { DefaultAddyIoOptions } from "./username/forwarders/addy-io"; +import { DefaultDuckDuckGoOptions } from "./username/forwarders/duck-duck-go"; +import { DefaultFastmailOptions } from "./username/forwarders/fastmail"; +import { DefaultFirefoxRelayOptions } from "./username/forwarders/firefox-relay"; +import { DefaultForwardEmailOptions } from "./username/forwarders/forward-email"; +import { DefaultSimpleLoginOptions } from "./username/forwarders/simple-login"; +import { Forwarders } from "./username/options/constants"; +import { + ApiOptions, + EmailDomainOptions, + EmailPrefixOptions, + SelfHostedApiOptions, +} from "./username/options/forwarder-options"; +import { + DefaultSubaddressOptions, + SubaddressGenerationOptions, +} from "./username/subaddress-generator-options"; + +const SomeUser = "userId" as UserId; + +function createGenerator(options: Options, defaults: Options) { + let savedOptions = options; + const generator = mock>({ + evaluator$(id: UserId) { + const evaluator = new DefaultPolicyEvaluator(); + return of(evaluator); + }, + options$(id: UserId) { + return of(savedOptions); + }, + defaults$(id: UserId) { + return of(defaults); + }, + saveOptions: jest.fn((userId, options) => { + savedOptions = options; + return Promise.resolve(); + }), + }); + + return generator; +} + +function createNavigationGenerator( + options: GeneratorNavigation = {}, + policy: GeneratorNavigationPolicy = {}, +) { + let savedOptions = options; + const generator = mock({ + evaluator$(id: UserId) { + const evaluator = new GeneratorNavigationEvaluator(policy); + return of(evaluator); + }, + options$(id: UserId) { + return of(savedOptions); + }, + defaults$(id: UserId) { + return of(DefaultGeneratorNavigation); + }, + saveOptions: jest.fn((userId, options) => { + savedOptions = options; + return Promise.resolve(); + }), + }); + + return generator; +} + +describe("LegacyUsernameGenerationService", () => { + // NOTE: in all tests, `null` constructor arguments are not used by the test. + // They're set to `null` to avoid setting up unnecessary mocks. + describe("generateUserName", () => { + it("should generate a catchall username", async () => { + const options = { type: "catchall" } as UsernameGeneratorOptions; + const catchall = createGenerator(null, null); + catchall.generate.mockReturnValue(Promise.resolve("catchall@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + catchall, + null, + null, + null, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateUsername(options); + + expect(catchall.generate).toHaveBeenCalledWith(options); + expect(result).toBe("catchall@example.com"); + }); + + it("should generate an EFF word username", async () => { + const options = { type: "word" } as UsernameGeneratorOptions; + const effWord = createGenerator(null, null); + effWord.generate.mockReturnValue(Promise.resolve("eff word")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + effWord, + null, + null, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateUsername(options); + + expect(effWord.generate).toHaveBeenCalledWith(options); + expect(result).toBe("eff word"); + }); + + it("should generate a subaddress username", async () => { + const options = { type: "subaddress" } as UsernameGeneratorOptions; + const subaddress = createGenerator(null, null); + subaddress.generate.mockReturnValue(Promise.resolve("subaddress@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + subaddress, + null, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateUsername(options); + + expect(subaddress.generate).toHaveBeenCalledWith(options); + expect(result).toBe("subaddress@example.com"); + }); + + it("should generate a forwarder username", async () => { + // set up an arbitrary forwarder for the username test; all forwarders tested in their own tests + const options = { + type: "forwarded", + forwardedService: Forwarders.AddyIo.id, + } as UsernameGeneratorOptions; + const addyIo = createGenerator(null, null); + addyIo.generate.mockReturnValue(Promise.resolve("addyio@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + addyIo, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateUsername(options); + + expect(addyIo.generate).toHaveBeenCalledWith({}); + expect(result).toBe("addyio@example.com"); + }); + }); + + describe("generateCatchall", () => { + it("should generate a catchall username", async () => { + const options = { type: "catchall" } as UsernameGeneratorOptions; + const catchall = createGenerator(null, null); + catchall.generate.mockReturnValue(Promise.resolve("catchall@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + catchall, + null, + null, + null, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateCatchall(options); + + expect(catchall.generate).toHaveBeenCalledWith(options); + expect(result).toBe("catchall@example.com"); + }); + }); + + describe("generateSubaddress", () => { + it("should generate a subaddress username", async () => { + const options = { type: "subaddress" } as UsernameGeneratorOptions; + const subaddress = createGenerator(null, null); + subaddress.generate.mockReturnValue(Promise.resolve("subaddress@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + subaddress, + null, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateSubaddress(options); + + expect(subaddress.generate).toHaveBeenCalledWith(options); + expect(result).toBe("subaddress@example.com"); + }); + }); + + describe("generateForwarded", () => { + it("should generate a AddyIo username", async () => { + const options = { + forwardedService: Forwarders.AddyIo.id, + forwardedAnonAddyApiToken: "token", + forwardedAnonAddyBaseUrl: "https://example.com", + forwardedAnonAddyDomain: "example.com", + website: "example.com", + } as UsernameGeneratorOptions; + const addyIo = createGenerator(null, null); + addyIo.generate.mockReturnValue(Promise.resolve("addyio@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + addyIo, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateForwarded(options); + + expect(addyIo.generate).toHaveBeenCalledWith({ + token: "token", + baseUrl: "https://example.com", + domain: "example.com", + website: "example.com", + }); + expect(result).toBe("addyio@example.com"); + }); + + it("should generate a DuckDuckGo username", async () => { + const options = { + forwardedService: Forwarders.DuckDuckGo.id, + forwardedDuckDuckGoToken: "token", + website: "example.com", + } as UsernameGeneratorOptions; + const duckDuckGo = createGenerator(null, null); + duckDuckGo.generate.mockReturnValue(Promise.resolve("ddg@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + null, + duckDuckGo, + null, + null, + null, + null, + ); + + const result = await generator.generateForwarded(options); + + expect(duckDuckGo.generate).toHaveBeenCalledWith({ + token: "token", + website: "example.com", + }); + expect(result).toBe("ddg@example.com"); + }); + + it("should generate a Fastmail username", async () => { + const options = { + forwardedService: Forwarders.Fastmail.id, + forwardedFastmailApiToken: "token", + website: "example.com", + } as UsernameGeneratorOptions; + const fastmail = createGenerator(null, null); + fastmail.generate.mockReturnValue(Promise.resolve("fastmail@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + null, + null, + fastmail, + null, + null, + null, + ); + + const result = await generator.generateForwarded(options); + + expect(fastmail.generate).toHaveBeenCalledWith({ + token: "token", + website: "example.com", + }); + expect(result).toBe("fastmail@example.com"); + }); + + it("should generate a FirefoxRelay username", async () => { + const options = { + forwardedService: Forwarders.FirefoxRelay.id, + forwardedFirefoxApiToken: "token", + website: "example.com", + } as UsernameGeneratorOptions; + const firefoxRelay = createGenerator(null, null); + firefoxRelay.generate.mockReturnValue(Promise.resolve("firefoxrelay@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + null, + null, + null, + firefoxRelay, + null, + null, + ); + + const result = await generator.generateForwarded(options); + + expect(firefoxRelay.generate).toHaveBeenCalledWith({ + token: "token", + website: "example.com", + }); + expect(result).toBe("firefoxrelay@example.com"); + }); + + it("should generate a ForwardEmail username", async () => { + const options = { + forwardedService: Forwarders.ForwardEmail.id, + forwardedForwardEmailApiToken: "token", + forwardedForwardEmailDomain: "example.com", + website: "example.com", + } as UsernameGeneratorOptions; + const forwardEmail = createGenerator(null, null); + forwardEmail.generate.mockReturnValue(Promise.resolve("forwardemail@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + null, + null, + null, + null, + forwardEmail, + null, + ); + + const result = await generator.generateForwarded(options); + + expect(forwardEmail.generate).toHaveBeenCalledWith({ + token: "token", + domain: "example.com", + website: "example.com", + }); + expect(result).toBe("forwardemail@example.com"); + }); + + it("should generate a SimpleLogin username", async () => { + const options = { + forwardedService: Forwarders.SimpleLogin.id, + forwardedSimpleLoginApiKey: "token", + forwardedSimpleLoginBaseUrl: "https://example.com", + website: "example.com", + } as UsernameGeneratorOptions; + const simpleLogin = createGenerator(null, null); + simpleLogin.generate.mockReturnValue(Promise.resolve("simplelogin@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + simpleLogin, + ); + + const result = await generator.generateForwarded(options); + + expect(simpleLogin.generate).toHaveBeenCalledWith({ + token: "token", + baseUrl: "https://example.com", + website: "example.com", + }); + expect(result).toBe("simplelogin@example.com"); + }); + }); + + describe("getOptions", () => { + it("combines options from its inner generators", async () => { + const account = mockAccountServiceWith(SomeUser); + + const navigation = createNavigationGenerator({ + type: "username", + username: "catchall", + forwarder: Forwarders.AddyIo.id, + }); + + const catchall = createGenerator( + { + catchallDomain: "example.com", + catchallType: "random", + website: null, + }, + null, + ); + + const effUsername = createGenerator( + { + wordCapitalize: true, + wordIncludeNumber: false, + website: null, + }, + null, + ); + + const subaddress = createGenerator( + { + subaddressType: "random", + subaddressEmail: "foo@example.com", + website: null, + }, + null, + ); + + const addyIo = createGenerator( + { + token: "addyIoToken", + domain: "addyio.example.com", + baseUrl: "https://addyio.api.example.com", + website: null, + }, + null, + ); + + const duckDuckGo = createGenerator( + { + token: "ddgToken", + website: null, + }, + null, + ); + + const fastmail = createGenerator( + { + token: "fastmailToken", + domain: "fastmail.example.com", + prefix: "foo", + website: null, + }, + null, + ); + + const firefoxRelay = createGenerator( + { + token: "firefoxToken", + website: null, + }, + null, + ); + + const forwardEmail = createGenerator( + { + token: "forwardEmailToken", + domain: "example.com", + website: null, + }, + null, + ); + + const simpleLogin = createGenerator( + { + token: "simpleLoginToken", + baseUrl: "https://simplelogin.api.example.com", + website: null, + }, + null, + ); + + const generator = new LegacyUsernameGenerationService( + account, + navigation, + catchall, + effUsername, + subaddress, + addyIo, + duckDuckGo, + fastmail, + firefoxRelay, + forwardEmail, + simpleLogin, + ); + + const result = await generator.getOptions(); + + expect(result).toEqual({ + type: "catchall", + wordCapitalize: true, + wordIncludeNumber: false, + subaddressType: "random", + subaddressEmail: "foo@example.com", + catchallType: "random", + catchallDomain: "example.com", + forwardedService: Forwarders.AddyIo.id, + forwardedAnonAddyApiToken: "addyIoToken", + forwardedAnonAddyDomain: "addyio.example.com", + forwardedAnonAddyBaseUrl: "https://addyio.api.example.com", + forwardedDuckDuckGoToken: "ddgToken", + forwardedFirefoxApiToken: "firefoxToken", + forwardedFastmailApiToken: "fastmailToken", + forwardedForwardEmailApiToken: "forwardEmailToken", + forwardedForwardEmailDomain: "example.com", + forwardedSimpleLoginApiKey: "simpleLoginToken", + forwardedSimpleLoginBaseUrl: "https://simplelogin.api.example.com", + }); + }); + + it("sets default options when an inner service lacks a value", async () => { + const account = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator(null); + const catchall = createGenerator(null, DefaultCatchallOptions); + const effUsername = createGenerator( + null, + DefaultEffUsernameOptions, + ); + const subaddress = createGenerator( + null, + DefaultSubaddressOptions, + ); + const addyIo = createGenerator( + null, + DefaultAddyIoOptions, + ); + const duckDuckGo = createGenerator(null, DefaultDuckDuckGoOptions); + const fastmail = createGenerator( + null, + DefaultFastmailOptions, + ); + const firefoxRelay = createGenerator(null, DefaultFirefoxRelayOptions); + const forwardEmail = createGenerator( + null, + DefaultForwardEmailOptions, + ); + const simpleLogin = createGenerator(null, DefaultSimpleLoginOptions); + + const generator = new LegacyUsernameGenerationService( + account, + navigation, + catchall, + effUsername, + subaddress, + addyIo, + duckDuckGo, + fastmail, + firefoxRelay, + forwardEmail, + simpleLogin, + ); + + const result = await generator.getOptions(); + + expect(result).toEqual({ + type: DefaultGeneratorNavigation.username, + catchallType: DefaultCatchallOptions.catchallType, + catchallDomain: DefaultCatchallOptions.catchallDomain, + wordCapitalize: DefaultEffUsernameOptions.wordCapitalize, + wordIncludeNumber: DefaultEffUsernameOptions.wordIncludeNumber, + subaddressType: DefaultSubaddressOptions.subaddressType, + subaddressEmail: DefaultSubaddressOptions.subaddressEmail, + forwardedService: DefaultGeneratorNavigation.forwarder, + forwardedAnonAddyApiToken: DefaultAddyIoOptions.token, + forwardedAnonAddyDomain: DefaultAddyIoOptions.domain, + forwardedAnonAddyBaseUrl: DefaultAddyIoOptions.baseUrl, + forwardedDuckDuckGoToken: DefaultDuckDuckGoOptions.token, + forwardedFastmailApiToken: DefaultFastmailOptions.token, + forwardedFirefoxApiToken: DefaultFirefoxRelayOptions.token, + forwardedForwardEmailApiToken: DefaultForwardEmailOptions.token, + forwardedForwardEmailDomain: DefaultForwardEmailOptions.domain, + forwardedSimpleLoginApiKey: DefaultSimpleLoginOptions.token, + forwardedSimpleLoginBaseUrl: DefaultSimpleLoginOptions.baseUrl, + }); + }); + }); + + describe("saveOptions", () => { + it("saves option sets to its inner generators", async () => { + const account = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator({ type: "password" }); + const catchall = createGenerator(null, null); + const effUsername = createGenerator(null, null); + const subaddress = createGenerator(null, null); + const addyIo = createGenerator(null, null); + const duckDuckGo = createGenerator(null, null); + const fastmail = createGenerator(null, null); + const firefoxRelay = createGenerator(null, null); + const forwardEmail = createGenerator(null, null); + const simpleLogin = createGenerator(null, null); + + const generator = new LegacyUsernameGenerationService( + account, + navigation, + catchall, + effUsername, + subaddress, + addyIo, + duckDuckGo, + fastmail, + firefoxRelay, + forwardEmail, + simpleLogin, + ); + + await generator.saveOptions({ + type: "catchall", + wordCapitalize: true, + wordIncludeNumber: false, + subaddressType: "random", + subaddressEmail: "foo@example.com", + catchallType: "random", + catchallDomain: "example.com", + forwardedService: Forwarders.AddyIo.id, + forwardedAnonAddyApiToken: "addyIoToken", + forwardedAnonAddyDomain: "addyio.example.com", + forwardedAnonAddyBaseUrl: "https://addyio.api.example.com", + forwardedDuckDuckGoToken: "ddgToken", + forwardedFirefoxApiToken: "firefoxToken", + forwardedFastmailApiToken: "fastmailToken", + forwardedForwardEmailApiToken: "forwardEmailToken", + forwardedForwardEmailDomain: "example.com", + forwardedSimpleLoginApiKey: "simpleLoginToken", + forwardedSimpleLoginBaseUrl: "https://simplelogin.api.example.com", + website: null, + }); + + expect(navigation.saveOptions).toHaveBeenCalledWith(SomeUser, { + type: "password", + username: "catchall", + forwarder: Forwarders.AddyIo.id, + }); + + expect(catchall.saveOptions).toHaveBeenCalledWith(SomeUser, { + catchallDomain: "example.com", + catchallType: "random", + website: null, + }); + + expect(effUsername.saveOptions).toHaveBeenCalledWith(SomeUser, { + wordCapitalize: true, + wordIncludeNumber: false, + website: null, + }); + + expect(subaddress.saveOptions).toHaveBeenCalledWith(SomeUser, { + subaddressType: "random", + subaddressEmail: "foo@example.com", + website: null, + }); + + expect(addyIo.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "addyIoToken", + domain: "addyio.example.com", + baseUrl: "https://addyio.api.example.com", + website: null, + }); + + expect(duckDuckGo.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "ddgToken", + website: null, + }); + + expect(fastmail.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "fastmailToken", + website: null, + }); + + expect(firefoxRelay.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "firefoxToken", + website: null, + }); + + expect(forwardEmail.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "forwardEmailToken", + domain: "example.com", + website: null, + }); + + expect(simpleLogin.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "simpleLoginToken", + baseUrl: "https://simplelogin.api.example.com", + website: null, + }); + }); + }); +}); diff --git a/libs/common/src/tools/generator/legacy-username-generation.service.ts b/libs/common/src/tools/generator/legacy-username-generation.service.ts new file mode 100644 index 0000000000..7611a86c27 --- /dev/null +++ b/libs/common/src/tools/generator/legacy-username-generation.service.ts @@ -0,0 +1,383 @@ +import { zip, firstValueFrom, map, concatMap } from "rxjs"; + +import { ApiService } from "../../abstractions/api.service"; +import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "../../auth/abstractions/account.service"; +import { CryptoService } from "../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { I18nService } from "../../platform/abstractions/i18n.service"; +import { StateProvider } from "../../platform/state"; + +import { GeneratorService, GeneratorNavigationService } from "./abstractions"; +import { UsernameGenerationServiceAbstraction } from "./abstractions/username-generation.service.abstraction"; +import { DefaultGeneratorService } from "./default-generator.service"; +import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service"; +import { GeneratorNavigation } from "./navigation/generator-navigation"; +import { NoPolicy } from "./no-policy"; +import { + CatchallGeneratorStrategy, + SubaddressGeneratorStrategy, + EffUsernameGeneratorStrategy, +} from "./username"; +import { CatchallGenerationOptions } from "./username/catchall-generator-options"; +import { EffUsernameGenerationOptions } from "./username/eff-username-generator-options"; +import { AddyIoForwarder } from "./username/forwarders/addy-io"; +import { DuckDuckGoForwarder } from "./username/forwarders/duck-duck-go"; +import { FastmailForwarder } from "./username/forwarders/fastmail"; +import { FirefoxRelayForwarder } from "./username/forwarders/firefox-relay"; +import { ForwardEmailForwarder } from "./username/forwarders/forward-email"; +import { SimpleLoginForwarder } from "./username/forwarders/simple-login"; +import { Forwarders } from "./username/options/constants"; +import { + ApiOptions, + EmailDomainOptions, + EmailPrefixOptions, + RequestOptions, + SelfHostedApiOptions, +} from "./username/options/forwarder-options"; +import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; +import { UsernameGeneratorOptions } from "./username/username-generation-options"; +import { UsernameGenerationService } from "./username/username-generation.service"; + +type MappedOptions = { + generator: GeneratorNavigation; + algorithms: { + catchall: CatchallGenerationOptions; + effUsername: EffUsernameGenerationOptions; + subaddress: SubaddressGenerationOptions; + }; + forwarders: { + addyIo: SelfHostedApiOptions & EmailDomainOptions & RequestOptions; + duckDuckGo: ApiOptions & RequestOptions; + fastmail: ApiOptions & EmailPrefixOptions & RequestOptions; + firefoxRelay: ApiOptions & RequestOptions; + forwardEmail: ApiOptions & EmailDomainOptions & RequestOptions; + simpleLogin: SelfHostedApiOptions & RequestOptions; + }; +}; + +export function legacyPasswordGenerationServiceFactory( + apiService: ApiService, + i18nService: I18nService, + cryptoService: CryptoService, + encryptService: EncryptService, + policyService: PolicyService, + accountService: AccountService, + stateProvider: StateProvider, +): UsernameGenerationServiceAbstraction { + // FIXME: Once the username generation service is replaced with this service + // in the clients, factor out the deprecated service in its entirety. + const deprecatedService = new UsernameGenerationService(cryptoService, null, null); + + const effUsername = new DefaultGeneratorService( + new EffUsernameGeneratorStrategy(deprecatedService, stateProvider), + policyService, + ); + + const subaddress = new DefaultGeneratorService( + new SubaddressGeneratorStrategy(deprecatedService, stateProvider), + policyService, + ); + + const catchall = new DefaultGeneratorService( + new CatchallGeneratorStrategy(deprecatedService, stateProvider), + policyService, + ); + + const addyIo = new DefaultGeneratorService( + new AddyIoForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), + policyService, + ); + + const duckDuckGo = new DefaultGeneratorService( + new DuckDuckGoForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), + policyService, + ); + + const fastmail = new DefaultGeneratorService( + new FastmailForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), + policyService, + ); + + const firefoxRelay = new DefaultGeneratorService( + new FirefoxRelayForwarder( + apiService, + i18nService, + encryptService, + cryptoService, + stateProvider, + ), + policyService, + ); + + const forwardEmail = new DefaultGeneratorService( + new ForwardEmailForwarder( + apiService, + i18nService, + encryptService, + cryptoService, + stateProvider, + ), + policyService, + ); + + const simpleLogin = new DefaultGeneratorService( + new SimpleLoginForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), + policyService, + ); + + const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService); + + return new LegacyUsernameGenerationService( + accountService, + navigation, + catchall, + effUsername, + subaddress, + addyIo, + duckDuckGo, + fastmail, + firefoxRelay, + forwardEmail, + simpleLogin, + ); +} + +/** Adapts the generator 2.0 design to 1.0 angular services. */ +export class LegacyUsernameGenerationService implements UsernameGenerationServiceAbstraction { + constructor( + private readonly accountService: AccountService, + private readonly navigation: GeneratorNavigationService, + private readonly catchall: GeneratorService, + private readonly effUsername: GeneratorService, + private readonly subaddress: GeneratorService, + private readonly addyIo: GeneratorService, + private readonly duckDuckGo: GeneratorService, + private readonly fastmail: GeneratorService, + private readonly firefoxRelay: GeneratorService, + private readonly forwardEmail: GeneratorService, + private readonly simpleLogin: GeneratorService, + ) {} + + generateUsername(options: UsernameGeneratorOptions) { + if (options.type === "catchall") { + return this.generateCatchall(options); + } else if (options.type === "subaddress") { + return this.generateSubaddress(options); + } else if (options.type === "forwarded") { + return this.generateForwarded(options); + } else { + return this.generateWord(options); + } + } + + generateWord(options: UsernameGeneratorOptions) { + return this.effUsername.generate(options); + } + + generateSubaddress(options: UsernameGeneratorOptions) { + return this.subaddress.generate(options); + } + + generateCatchall(options: UsernameGeneratorOptions) { + return this.catchall.generate(options); + } + + generateForwarded(options: UsernameGeneratorOptions) { + if (!options.forwardedService) { + return null; + } + + const stored = this.toStoredOptions(options); + switch (options.forwardedService) { + case Forwarders.AddyIo.id: + return this.addyIo.generate(stored.forwarders.addyIo); + case Forwarders.DuckDuckGo.id: + return this.duckDuckGo.generate(stored.forwarders.duckDuckGo); + case Forwarders.Fastmail.id: + return this.fastmail.generate(stored.forwarders.fastmail); + case Forwarders.FirefoxRelay.id: + return this.firefoxRelay.generate(stored.forwarders.firefoxRelay); + case Forwarders.ForwardEmail.id: + return this.forwardEmail.generate(stored.forwarders.forwardEmail); + case Forwarders.SimpleLogin.id: + return this.simpleLogin.generate(stored.forwarders.simpleLogin); + } + } + + getOptions() { + const options$ = this.accountService.activeAccount$.pipe( + concatMap((account) => + zip( + this.navigation.options$(account.id), + this.navigation.defaults$(account.id), + this.catchall.options$(account.id), + this.catchall.defaults$(account.id), + this.effUsername.options$(account.id), + this.effUsername.defaults$(account.id), + this.subaddress.options$(account.id), + this.subaddress.defaults$(account.id), + this.addyIo.options$(account.id), + this.addyIo.defaults$(account.id), + this.duckDuckGo.options$(account.id), + this.duckDuckGo.defaults$(account.id), + this.fastmail.options$(account.id), + this.fastmail.defaults$(account.id), + this.firefoxRelay.options$(account.id), + this.firefoxRelay.defaults$(account.id), + this.forwardEmail.options$(account.id), + this.forwardEmail.defaults$(account.id), + this.simpleLogin.options$(account.id), + this.simpleLogin.defaults$(account.id), + ), + ), + map( + ([ + generatorOptions, + generatorDefaults, + catchallOptions, + catchallDefaults, + effUsernameOptions, + effUsernameDefaults, + subaddressOptions, + subaddressDefaults, + addyIoOptions, + addyIoDefaults, + duckDuckGoOptions, + duckDuckGoDefaults, + fastmailOptions, + fastmailDefaults, + firefoxRelayOptions, + firefoxRelayDefaults, + forwardEmailOptions, + forwardEmailDefaults, + simpleLoginOptions, + simpleLoginDefaults, + ]) => + this.toUsernameOptions({ + generator: generatorOptions ?? generatorDefaults, + algorithms: { + catchall: catchallOptions ?? catchallDefaults, + effUsername: effUsernameOptions ?? effUsernameDefaults, + subaddress: subaddressOptions ?? subaddressDefaults, + }, + forwarders: { + addyIo: addyIoOptions ?? addyIoDefaults, + duckDuckGo: duckDuckGoOptions ?? duckDuckGoDefaults, + fastmail: fastmailOptions ?? fastmailDefaults, + firefoxRelay: firefoxRelayOptions ?? firefoxRelayDefaults, + forwardEmail: forwardEmailOptions ?? forwardEmailDefaults, + simpleLogin: simpleLoginOptions ?? simpleLoginDefaults, + }, + }), + ), + ); + + return firstValueFrom(options$); + } + + async saveOptions(options: UsernameGeneratorOptions) { + const stored = this.toStoredOptions(options); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a.id))); + + // generator settings needs to preserve whether password or passphrase is selected, + // so `navigationOptions` is mutated. + let navigationOptions = await firstValueFrom(this.navigation.options$(userId)); + navigationOptions = Object.assign(navigationOptions, stored.generator); + await this.navigation.saveOptions(userId, navigationOptions); + + // overwrite all other settings with latest values + await Promise.all([ + this.catchall.saveOptions(userId, stored.algorithms.catchall), + this.effUsername.saveOptions(userId, stored.algorithms.effUsername), + this.subaddress.saveOptions(userId, stored.algorithms.subaddress), + this.addyIo.saveOptions(userId, stored.forwarders.addyIo), + this.duckDuckGo.saveOptions(userId, stored.forwarders.duckDuckGo), + this.fastmail.saveOptions(userId, stored.forwarders.fastmail), + this.firefoxRelay.saveOptions(userId, stored.forwarders.firefoxRelay), + this.forwardEmail.saveOptions(userId, stored.forwarders.forwardEmail), + this.simpleLogin.saveOptions(userId, stored.forwarders.simpleLogin), + ]); + } + + private toStoredOptions(options: UsernameGeneratorOptions) { + const forwarders = { + addyIo: { + baseUrl: options.forwardedAnonAddyBaseUrl, + token: options.forwardedAnonAddyApiToken, + domain: options.forwardedAnonAddyDomain, + website: options.website, + }, + duckDuckGo: { + token: options.forwardedDuckDuckGoToken, + website: options.website, + }, + fastmail: { + token: options.forwardedFastmailApiToken, + website: options.website, + }, + firefoxRelay: { + token: options.forwardedFirefoxApiToken, + website: options.website, + }, + forwardEmail: { + token: options.forwardedForwardEmailApiToken, + domain: options.forwardedForwardEmailDomain, + website: options.website, + }, + simpleLogin: { + token: options.forwardedSimpleLoginApiKey, + baseUrl: options.forwardedSimpleLoginBaseUrl, + website: options.website, + }, + }; + + const generator = { + username: options.type, + forwarder: options.forwardedService, + }; + + const algorithms = { + effUsername: { + wordCapitalize: options.wordCapitalize, + wordIncludeNumber: options.wordIncludeNumber, + website: options.website, + }, + subaddress: { + subaddressType: options.subaddressType, + subaddressEmail: options.subaddressEmail, + website: options.website, + }, + catchall: { + catchallType: options.catchallType, + catchallDomain: options.catchallDomain, + website: options.website, + }, + }; + + return { generator, algorithms, forwarders } as MappedOptions; + } + + private toUsernameOptions(options: MappedOptions) { + return { + type: options.generator.username, + wordCapitalize: options.algorithms.effUsername.wordCapitalize, + wordIncludeNumber: options.algorithms.effUsername.wordIncludeNumber, + subaddressType: options.algorithms.subaddress.subaddressType, + subaddressEmail: options.algorithms.subaddress.subaddressEmail, + catchallType: options.algorithms.catchall.catchallType, + catchallDomain: options.algorithms.catchall.catchallDomain, + forwardedService: options.generator.forwarder, + forwardedAnonAddyApiToken: options.forwarders.addyIo.token, + forwardedAnonAddyDomain: options.forwarders.addyIo.domain, + forwardedAnonAddyBaseUrl: options.forwarders.addyIo.baseUrl, + forwardedDuckDuckGoToken: options.forwarders.duckDuckGo.token, + forwardedFirefoxApiToken: options.forwarders.firefoxRelay.token, + forwardedFastmailApiToken: options.forwarders.fastmail.token, + forwardedForwardEmailApiToken: options.forwarders.forwardEmail.token, + forwardedForwardEmailDomain: options.forwarders.forwardEmail.domain, + forwardedSimpleLoginApiKey: options.forwarders.simpleLogin.token, + forwardedSimpleLoginBaseUrl: options.forwarders.simpleLogin.baseUrl, + } as UsernameGeneratorOptions; + } +} diff --git a/libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts b/libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts new file mode 100644 index 0000000000..6853542bb7 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts @@ -0,0 +1,100 @@ +/** + * include structuredClone in test environment. + * @jest-environment ../../../../shared/test.environment.ts + */ +import { mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; +import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; +import { UserId } from "../../../types/guid"; +import { GENERATOR_SETTINGS } from "../key-definitions"; + +import { + GeneratorNavigationEvaluator, + DefaultGeneratorNavigationService, + DefaultGeneratorNavigation, +} from "./"; + +const SomeUser = "some user" as UserId; + +describe("DefaultGeneratorNavigationService", () => { + describe("options$", () => { + it("emits options", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const settings = { type: "password" as const }; + await stateProvider.setUserState(GENERATOR_SETTINGS, settings, SomeUser); + const navigation = new DefaultGeneratorNavigationService(stateProvider, null); + + const result = await firstValueFrom(navigation.options$(SomeUser)); + + expect(result).toEqual(settings); + }); + }); + + describe("defaults$", () => { + it("emits default options", async () => { + const navigation = new DefaultGeneratorNavigationService(null, null); + + const result = await firstValueFrom(navigation.defaults$(SomeUser)); + + expect(result).toEqual(DefaultGeneratorNavigation); + }); + }); + + describe("evaluator$", () => { + it("emits a GeneratorNavigationEvaluator", async () => { + const policyService = mock({ + getAll$() { + return of([]); + }, + }); + const navigation = new DefaultGeneratorNavigationService(null, policyService); + + const result = await firstValueFrom(navigation.evaluator$(SomeUser)); + + expect(result).toBeInstanceOf(GeneratorNavigationEvaluator); + }); + }); + + describe("enforcePolicy", () => { + it("applies policy", async () => { + const policyService = mock({ + getAll$(_type: PolicyType, _user: UserId) { + return of([ + new Policy({ + id: "" as any, + organizationId: "" as any, + enabled: true, + type: PolicyType.PasswordGenerator, + data: { defaultType: "password" }, + }), + ]); + }, + }); + const navigation = new DefaultGeneratorNavigationService(null, policyService); + const options = {}; + + const result = await navigation.enforcePolicy(SomeUser, options); + + expect(result).toMatchObject({ type: "password" }); + }); + }); + + describe("saveOptions", () => { + it("updates options$", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const navigation = new DefaultGeneratorNavigationService(stateProvider, null); + const settings = { type: "password" as const }; + + await navigation.saveOptions(SomeUser, settings); + const result = await firstValueFrom(navigation.options$(SomeUser)); + + expect(result).toEqual(settings); + }); + }); +}); diff --git a/libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts b/libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts new file mode 100644 index 0000000000..3199efc8c3 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts @@ -0,0 +1,71 @@ +import { BehaviorSubject, Observable, firstValueFrom, map } from "rxjs"; + +import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "../../../admin-console/enums"; +import { StateProvider } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; +import { GeneratorNavigationService } from "../abstractions/generator-navigation.service.abstraction"; +import { GENERATOR_SETTINGS } from "../key-definitions"; +import { reduceCollection } from "../reduce-collection.operator"; + +import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation"; +import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator"; +import { DisabledGeneratorNavigationPolicy, preferPassword } from "./generator-navigation-policy"; + +export class DefaultGeneratorNavigationService implements GeneratorNavigationService { + /** instantiates the password generator strategy. + * @param stateProvider provides durable state + * @param policy provides the policy to enforce + */ + constructor( + private readonly stateProvider: StateProvider, + private readonly policy: PolicyService, + ) {} + + /** An observable monitoring the options saved to disk. + * The observable updates when the options are saved. + * @param userId: Identifies the user making the request + */ + options$(userId: UserId): Observable { + return this.stateProvider.getUserState$(GENERATOR_SETTINGS, userId); + } + + /** Gets the default options. */ + defaults$(userId: UserId): Observable { + return new BehaviorSubject({ ...DefaultGeneratorNavigation }); + } + + /** An observable monitoring the options used to enforce policy. + * The observable updates when the policy changes. + * @param userId: Identifies the user making the request + */ + evaluator$(userId: UserId) { + const evaluator$ = this.policy.getAll$(PolicyType.PasswordGenerator, userId).pipe( + reduceCollection(preferPassword, DisabledGeneratorNavigationPolicy), + map((policy) => new GeneratorNavigationEvaluator(policy)), + ); + + return evaluator$; + } + + /** Enforces the policy on the given options + * @param userId: Identifies the user making the request + * @param options the options to enforce the policy on + * @returns a new instance of the options with the policy enforced + */ + async enforcePolicy(userId: UserId, options: GeneratorNavigation) { + const evaluator = await firstValueFrom(this.evaluator$(userId)); + const applied = evaluator.applyPolicy(options); + const sanitized = evaluator.sanitize(applied); + return sanitized; + } + + /** Saves the navigation options to disk. + * @param userId: Identifies the user making the request + * @param options the options to save + * @returns a promise that resolves when the options are saved + */ + async saveOptions(userId: UserId, options: GeneratorNavigation): Promise { + await this.stateProvider.setUserState(GENERATOR_SETTINGS, options, userId); + } +} diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts b/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts new file mode 100644 index 0000000000..58560fb5a0 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts @@ -0,0 +1,64 @@ +import { DefaultGeneratorNavigation } from "./generator-navigation"; +import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator"; + +describe("GeneratorNavigationEvaluator", () => { + describe("policyInEffect", () => { + it.each([["passphrase"], ["password"]] as const)( + "returns true if the policy has a defaultType (= %p)", + (defaultType) => { + const evaluator = new GeneratorNavigationEvaluator({ defaultType }); + + expect(evaluator.policyInEffect).toEqual(true); + }, + ); + + it.each([[undefined], [null], ["" as any]])( + "returns false if the policy has a falsy defaultType (= %p)", + (defaultType) => { + const evaluator = new GeneratorNavigationEvaluator({ defaultType }); + + expect(evaluator.policyInEffect).toEqual(false); + }, + ); + }); + + describe("applyPolicy", () => { + it("returns the input options", () => { + const evaluator = new GeneratorNavigationEvaluator(null); + const options = { type: "password" as const }; + + const result = evaluator.applyPolicy(options); + + expect(result).toEqual(options); + }); + }); + + describe("sanitize", () => { + it.each([["passphrase"], ["password"]] as const)( + "defaults options to the policy's default type (= %p) when a policy is in effect", + (defaultType) => { + const evaluator = new GeneratorNavigationEvaluator({ defaultType }); + + const result = evaluator.sanitize({}); + + expect(result).toEqual({ type: defaultType }); + }, + ); + + it("defaults options to the default generator navigation type when a policy is not in effect", () => { + const evaluator = new GeneratorNavigationEvaluator(null); + + const result = evaluator.sanitize({}); + + expect(result.type).toEqual(DefaultGeneratorNavigation.type); + }); + + it("retains the options type when it is set", () => { + const evaluator = new GeneratorNavigationEvaluator({ defaultType: "passphrase" }); + + const result = evaluator.sanitize({ type: "password" }); + + expect(result).toEqual({ type: "password" }); + }); + }); +}); diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts b/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts new file mode 100644 index 0000000000..e580f130b5 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts @@ -0,0 +1,43 @@ +import { PolicyEvaluator } from "../abstractions"; + +import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation"; +import { GeneratorNavigationPolicy } from "./generator-navigation-policy"; + +/** Enforces policy for generator navigation options. + */ +export class GeneratorNavigationEvaluator + implements PolicyEvaluator +{ + /** Instantiates the evaluator. + * @param policy The policy applied by the evaluator. When this conflicts with + * the defaults, the policy takes precedence. + */ + constructor(readonly policy: GeneratorNavigationPolicy) {} + + /** {@link PolicyEvaluator.policyInEffect} */ + get policyInEffect(): boolean { + return this.policy?.defaultType ? true : false; + } + + /** Apply policy to the input options. + * @param options The options to build from. These options are not altered. + * @returns A new password generation request with policy applied. + */ + applyPolicy(options: GeneratorNavigation): GeneratorNavigation { + return options; + } + + /** Ensures internal options consistency. + * @param options The options to cascade. These options are not altered. + * @returns A passphrase generation request with cascade applied. + */ + sanitize(options: GeneratorNavigation): GeneratorNavigation { + const defaultType = this.policyInEffect + ? this.policy.defaultType + : DefaultGeneratorNavigation.type; + return { + ...options, + type: options.type ?? defaultType, + }; + } +} diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts b/libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts new file mode 100644 index 0000000000..ed8fe731a7 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts @@ -0,0 +1,63 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; +import { PolicyId } from "../../../types/guid"; + +import { DisabledGeneratorNavigationPolicy, preferPassword } from "./generator-navigation-policy"; + +function createPolicy( + data: any, + type: PolicyType = PolicyType.PasswordGenerator, + enabled: boolean = true, +) { + return new Policy({ + id: "id" as PolicyId, + organizationId: "organizationId", + data, + enabled, + type, + }); +} + +describe("leastPrivilege", () => { + it("should return the accumulator when the policy type does not apply", () => { + const policy = createPolicy({}, PolicyType.RequireSso); + + const result = preferPassword(DisabledGeneratorNavigationPolicy, policy); + + expect(result).toEqual(DisabledGeneratorNavigationPolicy); + }); + + it("should return the accumulator when the policy is not enabled", () => { + const policy = createPolicy({}, PolicyType.PasswordGenerator, false); + + const result = preferPassword(DisabledGeneratorNavigationPolicy, policy); + + expect(result).toEqual(DisabledGeneratorNavigationPolicy); + }); + + it("should take the %p from the policy", () => { + const policy = createPolicy({ defaultType: "passphrase" }); + + const result = preferPassword({ ...DisabledGeneratorNavigationPolicy }, policy); + + expect(result).toEqual({ defaultType: "passphrase" }); + }); + + it("should override passphrase with password", () => { + const policy = createPolicy({ defaultType: "password" }); + + const result = preferPassword({ defaultType: "passphrase" }, policy); + + expect(result).toEqual({ defaultType: "password" }); + }); + + it("should not override password", () => { + const policy = createPolicy({ defaultType: "passphrase" }); + + const result = preferPassword({ defaultType: "password" }, policy); + + expect(result).toEqual({ defaultType: "password" }); + }); +}); diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-policy.ts b/libs/common/src/tools/generator/navigation/generator-navigation-policy.ts new file mode 100644 index 0000000000..25c2a73337 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/generator-navigation-policy.ts @@ -0,0 +1,39 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; +import { GeneratorType } from "../generator-type"; + +/** Policy settings affecting password generator navigation */ +export type GeneratorNavigationPolicy = { + /** The type of generator that should be shown by default when opening + * the password generator. + */ + defaultType?: GeneratorType; +}; + +/** Reduces a policy into an accumulator by preferring the password generator + * type to other generator types. + * @param acc the accumulator + * @param policy the policy to reduce + * @returns the resulting `GeneratorNavigationPolicy` + */ +export function preferPassword( + acc: GeneratorNavigationPolicy, + policy: Policy, +): GeneratorNavigationPolicy { + const isEnabled = policy.type === PolicyType.PasswordGenerator && policy.enabled; + if (!isEnabled) { + return acc; + } + + const isOverridable = acc.defaultType !== "password" && policy.data.defaultType; + const result = isOverridable ? { ...acc, defaultType: policy.data.defaultType } : acc; + + return result; +} + +/** The default options for password generation policy. */ +export const DisabledGeneratorNavigationPolicy: GeneratorNavigationPolicy = Object.freeze({ + defaultType: undefined, +}); diff --git a/libs/common/src/tools/generator/navigation/generator-navigation.ts b/libs/common/src/tools/generator/navigation/generator-navigation.ts new file mode 100644 index 0000000000..6a07385286 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/generator-navigation.ts @@ -0,0 +1,26 @@ +import { GeneratorType } from "../generator-type"; +import { ForwarderId } from "../username/options"; +import { UsernameGeneratorType } from "../username/options/generator-options"; + +/** Stores credential generator UI state. */ + +export type GeneratorNavigation = { + /** The kind of credential being generated. + * @remarks The legacy generator only supports "password" and "passphrase". + * The componentized generator supports all values. + */ + type?: GeneratorType; + + /** When `type === "username"`, this stores the username algorithm. */ + username?: UsernameGeneratorType; + + /** When `username === "forwarded"`, this stores the forwarder implementation. */ + forwarder?: ForwarderId | ""; +}; +/** The default options for password generation. */ + +export const DefaultGeneratorNavigation: Partial = Object.freeze({ + type: "password", + username: "word", + forwarder: "", +}); diff --git a/libs/common/src/tools/generator/navigation/index.ts b/libs/common/src/tools/generator/navigation/index.ts new file mode 100644 index 0000000000..86194f471a --- /dev/null +++ b/libs/common/src/tools/generator/navigation/index.ts @@ -0,0 +1,3 @@ +export { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator"; +export { DefaultGeneratorNavigationService } from "./default-generator-navigation.service"; +export { GeneratorNavigation, DefaultGeneratorNavigation } from "./generator-navigation"; diff --git a/libs/common/src/tools/generator/passphrase/index.ts b/libs/common/src/tools/generator/passphrase/index.ts index 175f15663e..3bbe925301 100644 --- a/libs/common/src/tools/generator/passphrase/index.ts +++ b/libs/common/src/tools/generator/passphrase/index.ts @@ -2,4 +2,7 @@ export { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator"; export { PassphraseGeneratorPolicy } from "./passphrase-generator-policy"; export { PassphraseGeneratorStrategy } from "./passphrase-generator-strategy"; -export { DefaultPassphraseGenerationOptions } from "./passphrase-generation-options"; +export { + DefaultPassphraseGenerationOptions, + PassphraseGenerationOptions, +} from "./passphrase-generation-options"; diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts index b7f09bd717..adcfc39527 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts @@ -2,7 +2,6 @@ * include structuredClone in test environment. * @jest-environment ../../../../shared/test.environment.ts */ - import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; @@ -12,12 +11,16 @@ import { PolicyType } from "../../../admin-console/enums"; import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; import { PASSPHRASE_SETTINGS } from "../key-definitions"; -import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction"; import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy"; -import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from "."; +import { + DefaultPassphraseGenerationOptions, + PassphraseGeneratorOptionsEvaluator, + PassphraseGeneratorStrategy, +} from "."; const SomeUser = "some user" as UserId; @@ -71,6 +74,16 @@ describe("Password generation strategy", () => { }); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new PassphraseGeneratorStrategy(null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultPassphraseGenerationOptions); + }); + }); + describe("cache_ms", () => { it("should be a positive non-zero number", () => { const legacy = mock(); diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts index f193b2b326..1a7c24082f 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts @@ -1,14 +1,17 @@ -import { map, pipe } from "rxjs"; +import { BehaviorSubject, map, pipe } from "rxjs"; import { GeneratorStrategy } from ".."; import { PolicyType } from "../../../admin-console/enums"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; import { PASSPHRASE_SETTINGS } from "../key-definitions"; -import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction"; import { reduceCollection } from "../reduce-collection.operator"; -import { PassphraseGenerationOptions } from "./passphrase-generation-options"; +import { + PassphraseGenerationOptions, + DefaultPassphraseGenerationOptions, +} from "./passphrase-generation-options"; import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator"; import { DisabledPassphraseGeneratorPolicy, @@ -36,6 +39,11 @@ export class PassphraseGeneratorStrategy return this.stateProvider.getUser(id, PASSPHRASE_SETTINGS); } + /** Gets the default options. */ + defaults$(_: UserId) { + return new BehaviorSubject({ ...DefaultPassphraseGenerationOptions }).asObservable(); + } + /** {@link GeneratorStrategy.policy} */ get policy() { return PolicyType.PasswordGenerator; diff --git a/libs/common/src/tools/generator/password/index.ts b/libs/common/src/tools/generator/password/index.ts index 0fcbbf5616..e17ab8201c 100644 --- a/libs/common/src/tools/generator/password/index.ts +++ b/libs/common/src/tools/generator/password/index.ts @@ -6,6 +6,6 @@ export { PasswordGeneratorStrategy } from "./password-generator-strategy"; // legacy interfaces export { PasswordGeneratorOptions } from "./password-generator-options"; -export { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; +export { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; export { PasswordGenerationService } from "./password-generation.service"; export { GeneratedPasswordHistory } from "./generated-password-history"; diff --git a/libs/common/src/tools/generator/password/password-generation.service.ts b/libs/common/src/tools/generator/password/password-generation.service.ts index eb1f08d97e..fced2dfe43 100644 --- a/libs/common/src/tools/generator/password/password-generation.service.ts +++ b/libs/common/src/tools/generator/password/password-generation.service.ts @@ -5,10 +5,10 @@ import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { EFFLongWordList } from "../../../platform/misc/wordlist"; import { EncString } from "../../../platform/models/domain/enc-string"; +import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; import { PassphraseGeneratorOptionsEvaluator } from "../passphrase/passphrase-generator-options-evaluator"; import { GeneratedPasswordHistory } from "./generated-password-history"; -import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; import { PasswordGeneratorOptions } from "./password-generator-options"; import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator"; @@ -341,24 +341,6 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId }); } - normalizeOptions( - options: PasswordGeneratorOptions, - enforcedPolicyOptions: PasswordGeneratorPolicyOptions, - ) { - const evaluator = - options.type == "password" - ? new PasswordGeneratorOptionsEvaluator(enforcedPolicyOptions) - : new PassphraseGeneratorOptionsEvaluator(enforcedPolicyOptions); - - const evaluatedOptions = evaluator.applyPolicy(options); - const santizedOptions = evaluator.sanitize(evaluatedOptions); - - // callers assume this function updates the options parameter - Object.assign(options, santizedOptions); - - return options; - } - private capitalize(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } diff --git a/libs/common/src/tools/generator/password/password-generator-options.ts b/libs/common/src/tools/generator/password/password-generator-options.ts index a0b42b3032..aa0a6f7dab 100644 --- a/libs/common/src/tools/generator/password/password-generator-options.ts +++ b/libs/common/src/tools/generator/password/password-generator-options.ts @@ -1,3 +1,4 @@ +import { GeneratorNavigation } from "../navigation/generator-navigation"; import { PassphraseGenerationOptions } from "../passphrase/passphrase-generation-options"; import { PasswordGenerationOptions } from "./password-generation-options"; @@ -6,12 +7,5 @@ import { PasswordGenerationOptions } from "./password-generation-options"; * This type includes all properties suitable for reactive data binding. */ export type PasswordGeneratorOptions = PasswordGenerationOptions & - PassphraseGenerationOptions & { - /** The algorithm to use for credential generation. - * Properties on @see PasswordGenerationOptions should be processed - * only when `type === "password"`. - * Properties on @see PassphraseGenerationOptions should be processed - * only when `type === "passphrase"`. - */ - type?: "password" | "passphrase"; - }; + PassphraseGenerationOptions & + GeneratorNavigation; diff --git a/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts b/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts index 9bfa5b5f35..5efc6a85a7 100644 --- a/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts @@ -17,6 +17,7 @@ import { PASSWORD_SETTINGS } from "../key-definitions"; import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy"; import { + DefaultPasswordGenerationOptions, PasswordGenerationServiceAbstraction, PasswordGeneratorOptionsEvaluator, PasswordGeneratorStrategy, @@ -82,6 +83,16 @@ describe("Password generation strategy", () => { }); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new PasswordGeneratorStrategy(null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultPasswordGenerationOptions); + }); + }); + describe("cache_ms", () => { it("should be a positive non-zero number", () => { const legacy = mock(); diff --git a/libs/common/src/tools/generator/password/password-generator-strategy.ts b/libs/common/src/tools/generator/password/password-generator-strategy.ts index f8d618128b..e98ae6fb16 100644 --- a/libs/common/src/tools/generator/password/password-generator-strategy.ts +++ b/libs/common/src/tools/generator/password/password-generator-strategy.ts @@ -1,14 +1,17 @@ -import { map, pipe } from "rxjs"; +import { BehaviorSubject, map, pipe } from "rxjs"; import { GeneratorStrategy } from ".."; import { PolicyType } from "../../../admin-console/enums"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; import { PASSWORD_SETTINGS } from "../key-definitions"; import { reduceCollection } from "../reduce-collection.operator"; -import { PasswordGenerationOptions } from "./password-generation-options"; -import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; +import { + DefaultPasswordGenerationOptions, + PasswordGenerationOptions, +} from "./password-generation-options"; import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator"; import { DisabledPasswordGeneratorPolicy, @@ -35,6 +38,11 @@ export class PasswordGeneratorStrategy return this.stateProvider.getUser(id, PASSWORD_SETTINGS); } + /** Gets the default options. */ + defaults$(_: UserId) { + return new BehaviorSubject({ ...DefaultPasswordGenerationOptions }).asObservable(); + } + /** {@link GeneratorStrategy.policy} */ get policy() { return PolicyType.PasswordGenerator; diff --git a/libs/common/src/tools/generator/username/catchall-generator-options.ts b/libs/common/src/tools/generator/username/catchall-generator-options.ts index 7e9950ec45..bddf98f757 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-options.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-options.ts @@ -1,10 +1,21 @@ +import { RequestOptions } from "./options/forwarder-options"; +import { UsernameGenerationMode } from "./options/generator-options"; + /** Settings supported when generating an email subaddress */ export type CatchallGenerationOptions = { - type?: "random" | "website-name"; - domain?: string; -}; + /** selects the generation algorithm for the catchall email address. */ + catchallType?: UsernameGenerationMode; -/** The default options for email subaddress generation. */ -export const DefaultCatchallOptions: Partial = Object.freeze({ - type: "random", + /** The domain part of the generated email address. + * @example If the domain is `domain.io` and the generated username + * is `jd`, then the generated email address will be `jd@mydomain.io` + */ + catchallDomain?: string; +} & RequestOptions; + +/** The default options for catchall address generation. */ +export const DefaultCatchallOptions: CatchallGenerationOptions = Object.freeze({ + catchallType: "random", + catchallDomain: "", + website: null, }); diff --git a/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts index 339e4b2720..52cfa00aaf 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts @@ -10,6 +10,8 @@ import { UserId } from "../../../types/guid"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { CATCHALL_SETTINGS } from "../key-definitions"; +import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options"; + import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; @@ -47,6 +49,16 @@ describe("Email subaddress list generation strategy", () => { }); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new CatchallGeneratorStrategy(null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultCatchallOptions); + }); + }); + describe("cache_ms", () => { it("should be a positive non-zero number", () => { const legacy = mock(); @@ -70,16 +82,14 @@ describe("Email subaddress list generation strategy", () => { const legacy = mock(); const strategy = new CatchallGeneratorStrategy(legacy, null); const options = { - type: "website-name" as const, - domain: "example.com", - }; + catchallType: "website-name", + catchallDomain: "example.com", + website: "foo.com", + } as CatchallGenerationOptions; await strategy.generate(options); - expect(legacy.generateCatchall).toHaveBeenCalledWith({ - catchallType: "website-name" as const, - catchallDomain: "example.com", - }); + expect(legacy.generateCatchall).toHaveBeenCalledWith(options); }); }); }); diff --git a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts index 6b36ebd50b..5111b06e90 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts @@ -1,15 +1,15 @@ -import { map, pipe } from "rxjs"; +import { BehaviorSubject, map, pipe } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; +import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { CATCHALL_SETTINGS } from "../key-definitions"; import { NoPolicy } from "../no-policy"; -import { CatchallGenerationOptions } from "./catchall-generator-options"; -import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; +import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options"; const ONE_MINUTE = 60 * 1000; @@ -30,6 +30,11 @@ export class CatchallGeneratorStrategy return this.stateProvider.getUser(id, CATCHALL_SETTINGS); } + /** {@link GeneratorStrategy.defaults$} */ + defaults$(userId: UserId) { + return new BehaviorSubject({ ...DefaultCatchallOptions }).asObservable(); + } + /** {@link GeneratorStrategy.policy} */ get policy() { // Uses password generator since there aren't policies @@ -49,9 +54,6 @@ export class CatchallGeneratorStrategy /** {@link GeneratorStrategy.generate} */ generate(options: CatchallGenerationOptions) { - return this.usernameService.generateCatchall({ - catchallDomain: options.domain, - catchallType: options.type, - }); + return this.usernameService.generateCatchall(options); } } diff --git a/libs/common/src/tools/generator/username/eff-username-generator-options.ts b/libs/common/src/tools/generator/username/eff-username-generator-options.ts index 868149c2fd..07890b3d55 100644 --- a/libs/common/src/tools/generator/username/eff-username-generator-options.ts +++ b/libs/common/src/tools/generator/username/eff-username-generator-options.ts @@ -1,11 +1,17 @@ -/** Settings supported when generating an ASCII username */ +import { RequestOptions } from "./options/forwarder-options"; + +/** Settings supported when generating a username using the EFF word list */ export type EffUsernameGenerationOptions = { + /** when true, the word is capitalized */ wordCapitalize?: boolean; + + /** when true, a random number is appended to the username */ wordIncludeNumber?: boolean; -}; +} & RequestOptions; /** The default options for EFF long word generation. */ -export const DefaultEffUsernameOptions: Partial = Object.freeze({ +export const DefaultEffUsernameOptions: EffUsernameGenerationOptions = Object.freeze({ wordCapitalize: false, wordIncludeNumber: false, + website: null, }); diff --git a/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts index 821b4bb7dc..9b0e4cc069 100644 --- a/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts @@ -10,6 +10,8 @@ import { UserId } from "../../../types/guid"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { EFF_USERNAME_SETTINGS } from "../key-definitions"; +import { DefaultEffUsernameOptions } from "./eff-username-generator-options"; + import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; @@ -47,6 +49,16 @@ describe("EFF long word list generation strategy", () => { }); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new EffUsernameGeneratorStrategy(null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultEffUsernameOptions); + }); + }); + describe("cache_ms", () => { it("should be a positive non-zero number", () => { const legacy = mock(); @@ -72,6 +84,7 @@ describe("EFF long word list generation strategy", () => { const options = { wordCapitalize: false, wordIncludeNumber: false, + website: null as string, }; await strategy.generate(options); diff --git a/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts b/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts index 133b4e7777..1a4efdcb44 100644 --- a/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts @@ -1,15 +1,18 @@ -import { map, pipe } from "rxjs"; +import { BehaviorSubject, map, pipe } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; +import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { EFF_USERNAME_SETTINGS } from "../key-definitions"; import { NoPolicy } from "../no-policy"; -import { EffUsernameGenerationOptions } from "./eff-username-generator-options"; -import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; +import { + DefaultEffUsernameOptions, + EffUsernameGenerationOptions, +} from "./eff-username-generator-options"; const ONE_MINUTE = 60 * 1000; @@ -30,6 +33,11 @@ export class EffUsernameGeneratorStrategy return this.stateProvider.getUser(id, EFF_USERNAME_SETTINGS); } + /** {@link GeneratorStrategy.defaults$} */ + defaults$(userId: UserId) { + return new BehaviorSubject({ ...DefaultEffUsernameOptions }).asObservable(); + } + /** {@link GeneratorStrategy.policy} */ get policy() { // Uses password generator since there aren't policies diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts index 30dd620484..c2a606eae0 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts @@ -15,6 +15,7 @@ import { DUCK_DUCK_GO_FORWARDER } from "../key-definitions"; import { SecretState } from "../state/secret-state"; import { ForwarderGeneratorStrategy } from "./forwarder-generator-strategy"; +import { DefaultDuckDuckGoOptions } from "./forwarders/duck-duck-go"; import { ApiOptions } from "./options/forwarder-options"; class TestForwarder extends ForwarderGeneratorStrategy { @@ -30,6 +31,10 @@ class TestForwarder extends ForwarderGeneratorStrategy { // arbitrary. return DUCK_DUCK_GO_FORWARDER; } + + defaults$ = (userId: UserId) => { + return of(DefaultDuckDuckGoOptions); + }; } const SomeUser = "some user" as UserId; diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts index 8b78f22634..086e347669 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts @@ -1,4 +1,4 @@ -import { map, pipe } from "rxjs"; +import { Observable, map, pipe } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; @@ -79,6 +79,9 @@ export abstract class ForwarderGeneratorStrategy< return new UserKeyEncryptor(this.encryptService, this.keyService, packer); } + /** Gets the default options. */ + abstract defaults$: (userId: UserId) => Observable; + /** Determine where forwarder configuration is stored */ protected abstract readonly key: KeyDefinition; diff --git a/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts b/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts index c2428aefca..f42ca23c11 100644 --- a/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts @@ -2,12 +2,17 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { firstValueFrom } from "rxjs"; + +import { UserId } from "../../../../types/guid"; import { ADDY_IO_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; -import { AddyIoForwarder } from "./addy-io"; +import { AddyIoForwarder, DefaultAddyIoOptions } from "./addy-io"; import { mockApiService, mockI18nService } from "./mocks.jest"; +const SomeUser = "some user" as UserId; + describe("Addy.io Forwarder", () => { it("key returns the Addy IO forwarder key", () => { const forwarder = new AddyIoForwarder(null, null, null, null, null); @@ -15,6 +20,16 @@ describe("Addy.io Forwarder", () => { expect(forwarder.key).toBe(ADDY_IO_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new AddyIoForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultAddyIoOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); diff --git a/libs/common/src/tools/generator/username/forwarders/addy-io.ts b/libs/common/src/tools/generator/username/forwarders/addy-io.ts index 2db69e2396..3e4960f7e7 100644 --- a/libs/common/src/tools/generator/username/forwarders/addy-io.ts +++ b/libs/common/src/tools/generator/username/forwarders/addy-io.ts @@ -1,13 +1,23 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { ADDY_IO_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { EmailDomainOptions, SelfHostedApiOptions } from "../options/forwarder-options"; +export const DefaultAddyIoOptions: SelfHostedApiOptions & EmailDomainOptions = Object.freeze({ + website: null, + baseUrl: "https://app.addy.io", + token: "", + domain: "", +}); + /** Generates a forwarding address for addy.io (formerly anon addy) */ export class AddyIoForwarder extends ForwarderGeneratorStrategy< SelfHostedApiOptions & EmailDomainOptions @@ -34,6 +44,11 @@ export class AddyIoForwarder extends ForwarderGeneratorStrategy< return ADDY_IO_FORWARDER; } + /** {@link ForwarderGeneratorStrategy.defaults$} */ + defaults$ = (userId: UserId) => { + return new BehaviorSubject({ ...DefaultAddyIoOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: SelfHostedApiOptions & EmailDomainOptions) => { if (!options.token || options.token === "") { @@ -91,3 +106,10 @@ export class AddyIoForwarder extends ForwarderGeneratorStrategy< } }; } + +export const DefaultOptions = Object.freeze({ + website: null, + baseUrl: "https://app.addy.io", + domain: "", + token: "", +}); diff --git a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts index 211eaead6d..b836ca2bef 100644 --- a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts @@ -2,12 +2,17 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { firstValueFrom } from "rxjs"; + +import { UserId } from "../../../../types/guid"; import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; -import { DuckDuckGoForwarder } from "./duck-duck-go"; +import { DuckDuckGoForwarder, DefaultDuckDuckGoOptions } from "./duck-duck-go"; import { mockApiService, mockI18nService } from "./mocks.jest"; +const SomeUser = "some user" as UserId; + describe("DuckDuckGo Forwarder", () => { it("key returns the Duck Duck Go forwarder key", () => { const forwarder = new DuckDuckGoForwarder(null, null, null, null, null); @@ -15,6 +20,16 @@ describe("DuckDuckGo Forwarder", () => { expect(forwarder.key).toBe(DUCK_DUCK_GO_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new DuckDuckGoForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultDuckDuckGoOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); diff --git a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts index daf4f7b444..9b5d93d742 100644 --- a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts +++ b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts @@ -1,13 +1,21 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { ApiOptions } from "../options/forwarder-options"; +export const DefaultDuckDuckGoOptions: ApiOptions = Object.freeze({ + website: null, + token: "", +}); + /** Generates a forwarding address for DuckDuckGo */ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy { /** Instantiates the forwarder @@ -32,6 +40,11 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy return DUCK_DUCK_GO_FORWARDER; } + /** {@link ForwarderGeneratorStrategy.defaults$} */ + defaults$ = (userId: UserId) => { + return new BehaviorSubject({ ...DefaultDuckDuckGoOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: ApiOptions): Promise => { if (!options.token || options.token === "") { @@ -68,3 +81,8 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy } }; } + +export const DefaultOptions = Object.freeze({ + website: null, + token: "", +}); diff --git a/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts b/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts index bab2b93966..895f32f7ee 100644 --- a/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts @@ -2,13 +2,18 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { firstValueFrom } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; +import { UserId } from "../../../../types/guid"; import { FASTMAIL_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; -import { FastmailForwarder } from "./fastmail"; +import { FastmailForwarder, DefaultFastmailOptions } from "./fastmail"; import { mockI18nService } from "./mocks.jest"; +const SomeUser = "some user" as UserId; + type MockResponse = { status: number; body: any }; // fastmail calls nativeFetch first to resolve the accountId, @@ -52,6 +57,16 @@ describe("Fastmail Forwarder", () => { expect(forwarder.key).toBe(FASTMAIL_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new FastmailForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultFastmailOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(AccountIdSuccess, EmptyResponse); diff --git a/libs/common/src/tools/generator/username/forwarders/fastmail.ts b/libs/common/src/tools/generator/username/forwarders/fastmail.ts index b4e2b56695..9d62cd0039 100644 --- a/libs/common/src/tools/generator/username/forwarders/fastmail.ts +++ b/libs/common/src/tools/generator/username/forwarders/fastmail.ts @@ -1,13 +1,23 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { FASTMAIL_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { EmailPrefixOptions, ApiOptions } from "../options/forwarder-options"; +export const DefaultFastmailOptions: ApiOptions & EmailPrefixOptions = Object.freeze({ + website: null, + domain: "", + prefix: "", + token: "", +}); + /** Generates a forwarding address for Fastmail */ export class FastmailForwarder extends ForwarderGeneratorStrategy { /** Instantiates the forwarder @@ -32,6 +42,11 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy { + return new BehaviorSubject({ ...DefaultFastmailOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: ApiOptions & EmailPrefixOptions) => { if (!options.token || options.token === "") { @@ -141,3 +156,10 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy { it("key returns the Firefox Relay forwarder key", () => { const forwarder = new FirefoxRelayForwarder(null, null, null, null, null); @@ -15,6 +20,16 @@ describe("Firefox Relay Forwarder", () => { expect(forwarder.key).toBe(FIREFOX_RELAY_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new FirefoxRelayForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultFirefoxRelayOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); diff --git a/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts b/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts index 1308852224..a4122c53f8 100644 --- a/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts +++ b/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts @@ -1,13 +1,21 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { FIREFOX_RELAY_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { ApiOptions } from "../options/forwarder-options"; +export const DefaultFirefoxRelayOptions: ApiOptions = Object.freeze({ + website: null, + token: "", +}); + /** Generates a forwarding address for Firefox Relay */ export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy { /** Instantiates the forwarder @@ -32,6 +40,11 @@ export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy { + return new BehaviorSubject({ ...DefaultFirefoxRelayOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: ApiOptions) => { if (!options.token || options.token === "") { @@ -75,3 +88,8 @@ export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy { it("key returns the Forward Email forwarder key", () => { const forwarder = new ForwardEmailForwarder(null, null, null, null, null); @@ -15,6 +20,16 @@ describe("ForwardEmail Forwarder", () => { expect(forwarder.key).toBe(FORWARD_EMAIL_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new ForwardEmailForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultForwardEmailOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); diff --git a/libs/common/src/tools/generator/username/forwarders/forward-email.ts b/libs/common/src/tools/generator/username/forwarders/forward-email.ts index eb6e3cd0c6..93f4680414 100644 --- a/libs/common/src/tools/generator/username/forwarders/forward-email.ts +++ b/libs/common/src/tools/generator/username/forwarders/forward-email.ts @@ -1,14 +1,23 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { Utils } from "../../../../platform/misc/utils"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { FORWARD_EMAIL_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { EmailDomainOptions, ApiOptions } from "../options/forwarder-options"; +export const DefaultForwardEmailOptions: ApiOptions & EmailDomainOptions = Object.freeze({ + website: null, + token: "", + domain: "", +}); + /** Generates a forwarding address for Forward Email */ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy< ApiOptions & EmailDomainOptions @@ -35,6 +44,11 @@ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy< return FORWARD_EMAIL_FORWARDER; } + /** {@link ForwarderGeneratorStrategy.defaults$} */ + defaults$ = (userId: UserId) => { + return new BehaviorSubject({ ...DefaultForwardEmailOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: ApiOptions & EmailDomainOptions) => { if (!options.token || options.token === "") { @@ -96,3 +110,9 @@ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy< } }; } + +export const DefaultOptions = Object.freeze({ + website: null, + token: "", + domain: "", +}); diff --git a/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts b/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts index 1120d49ce3..c53e783270 100644 --- a/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts @@ -2,11 +2,16 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { firstValueFrom } from "rxjs"; + +import { UserId } from "../../../../types/guid"; import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; import { mockApiService, mockI18nService } from "./mocks.jest"; -import { SimpleLoginForwarder } from "./simple-login"; +import { SimpleLoginForwarder, DefaultSimpleLoginOptions } from "./simple-login"; + +const SomeUser = "some user" as UserId; describe("SimpleLogin Forwarder", () => { it("key returns the Simple Login forwarder key", () => { @@ -15,6 +20,16 @@ describe("SimpleLogin Forwarder", () => { expect(forwarder.key).toBe(SIMPLE_LOGIN_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new SimpleLoginForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultSimpleLoginOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); diff --git a/libs/common/src/tools/generator/username/forwarders/simple-login.ts b/libs/common/src/tools/generator/username/forwarders/simple-login.ts index 33bd8e3d4e..d047fc42d1 100644 --- a/libs/common/src/tools/generator/username/forwarders/simple-login.ts +++ b/libs/common/src/tools/generator/username/forwarders/simple-login.ts @@ -1,13 +1,22 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { SelfHostedApiOptions } from "../options/forwarder-options"; +export const DefaultSimpleLoginOptions: SelfHostedApiOptions = Object.freeze({ + website: null, + baseUrl: "https://app.simplelogin.io", + token: "", +}); + /** Generates a forwarding address for Simple Login */ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy { /** Instantiates the forwarder @@ -32,6 +41,11 @@ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy { + return new BehaviorSubject({ ...DefaultSimpleLoginOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: SelfHostedApiOptions) => { if (!options.token || options.token === "") { @@ -80,3 +94,9 @@ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy { - describe("forAllForwarders", () => { - it("runs the function on every forwarder.", () => { - const result = forAllForwarders(TestOptions, (_, id) => id); - expect(result).toEqual([ - "anonaddy", - "duckduckgo", - "fastmail", - "firefoxrelay", - "forwardemail", - "simplelogin", - ]); - }); - }); - - describe("getForwarderOptions", () => { - it("should return null for unsupported services", () => { - expect(getForwarderOptions("unsupported", DefaultOptions)).toBeNull(); - }); - - let options: UsernameGeneratorOptions = null; - beforeEach(() => { - options = structuredClone(TestOptions); - }); - - it.each([ - [TestOptions.forwarders.addyIo, "anonaddy"], - [TestOptions.forwarders.duckDuckGo, "duckduckgo"], - [TestOptions.forwarders.fastMail, "fastmail"], - [TestOptions.forwarders.firefoxRelay, "firefoxrelay"], - [TestOptions.forwarders.forwardEmail, "forwardemail"], - [TestOptions.forwarders.simpleLogin, "simplelogin"], - ])("should return an %s for %p", (forwarderOptions, service) => { - const forwarder = getForwarderOptions(service, options); - expect(forwarder).toEqual(forwarderOptions); - }); - - it("should return a reference to the forwarder", () => { - const forwarder = getForwarderOptions("anonaddy", options); - expect(forwarder).toBe(options.forwarders.addyIo); - }); - }); - - describe("falsyDefault", () => { - it("should not modify values with truthy items", () => { - const input = { - a: "a", - b: 1, - d: [1], - }; - - const output = falsyDefault(input, { - a: "b", - b: 2, - d: [2], - }); - - expect(output).toEqual(input); - }); - - it("should modify values with falsy items", () => { - const input = { - a: "", - b: 0, - c: false, - d: [] as number[], - e: [0] as number[], - f: null as string, - g: undefined as string, - }; - - const output = falsyDefault(input, { - a: "a", - b: 1, - c: true, - d: [1], - e: [1], - f: "a", - g: "a", - }); - - expect(output).toEqual({ - a: "a", - b: 1, - c: true, - d: [1], - e: [1], - f: "a", - g: "a", - }); - }); - - it("should traverse nested objects", () => { - const input = { - a: { - b: { - c: "", - }, - }, - }; - - const output = falsyDefault(input, { - a: { - b: { - c: "c", - }, - }, - }); - - expect(output).toEqual({ - a: { - b: { - c: "c", - }, - }, - }); - }); - - it("should add missing defaults", () => { - const input = {}; - - const output = falsyDefault(input, { - a: "a", - b: [1], - c: {}, - d: { e: 1 }, - }); - - expect(output).toEqual({ - a: "a", - b: [1], - c: {}, - d: { e: 1 }, - }); - }); - - it("should ignore missing defaults", () => { - const input = { - a: "", - b: 0, - c: false, - d: [] as number[], - e: [0] as number[], - f: null as string, - g: undefined as string, - }; - - const output = falsyDefault(input, {}); - - expect(output).toEqual({ - a: "", - b: 0, - c: false, - d: [] as number[], - e: [0] as number[], - f: null as string, - g: undefined as string, - }); - }); - - it.each([[null], [undefined]])("should ignore %p defaults", (defaults) => { - const input = { - a: "", - b: 0, - c: false, - d: [] as number[], - e: [0] as number[], - f: null as string, - g: undefined as string, - }; - - const output = falsyDefault(input, defaults); - - expect(output).toEqual({ - a: "", - b: 0, - c: false, - d: [] as number[], - e: [0] as number[], - f: null as string, - g: undefined as string, - }); - }); - }); -}); diff --git a/libs/common/src/tools/generator/username/options/utilities.ts b/libs/common/src/tools/generator/username/options/utilities.ts deleted file mode 100644 index ba0c6c291f..0000000000 --- a/libs/common/src/tools/generator/username/options/utilities.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { DefaultOptions, Forwarders } from "./constants"; -import { ApiOptions, ForwarderId } from "./forwarder-options"; -import { MaybeLeakedOptions, UsernameGeneratorOptions } from "./generator-options"; - -/** runs the callback on each forwarder configuration */ -export function forAllForwarders( - options: UsernameGeneratorOptions, - callback: (options: ApiOptions, id: ForwarderId) => T, -) { - const results = []; - for (const forwarder of Object.values(Forwarders).map((f) => f.id)) { - const forwarderOptions = getForwarderOptions(forwarder, options); - if (forwarderOptions) { - results.push(callback(forwarderOptions, forwarder)); - } - } - return results; -} - -/** Gets the options for the specified forwarding service with defaults applied. - * This method mutates `options`. - * @param service Identifies the service whose options should be loaded. - * @param options The options to load from. - * @returns A reference to the options for the specified service. - */ -export function getForwarderOptions( - service: string, - options: UsernameGeneratorOptions, -): ApiOptions & MaybeLeakedOptions { - if (service === Forwarders.AddyIo.id) { - return falsyDefault(options.forwarders.addyIo, DefaultOptions.forwarders.addyIo); - } else if (service === Forwarders.DuckDuckGo.id) { - return falsyDefault(options.forwarders.duckDuckGo, DefaultOptions.forwarders.duckDuckGo); - } else if (service === Forwarders.Fastmail.id) { - return falsyDefault(options.forwarders.fastMail, DefaultOptions.forwarders.fastMail); - } else if (service === Forwarders.FirefoxRelay.id) { - return falsyDefault(options.forwarders.firefoxRelay, DefaultOptions.forwarders.firefoxRelay); - } else if (service === Forwarders.ForwardEmail.id) { - return falsyDefault(options.forwarders.forwardEmail, DefaultOptions.forwarders.forwardEmail); - } else if (service === Forwarders.SimpleLogin.id) { - return falsyDefault(options.forwarders.simpleLogin, DefaultOptions.forwarders.simpleLogin); - } else { - return null; - } -} - -/** - * Recursively applies default values from `defaults` to falsy or - * missing properties in `value`. - * - * @remarks This method is not aware of the - * object's prototype or metadata, such as readonly or frozen fields. - * It should only be used on plain objects. - * - * @param value - The value to fill in. This parameter is mutated. - * @param defaults - The default values to use. - * @returns the mutated `value`. - */ -export function falsyDefault(value: T, defaults: Partial): T { - // iterate keys in defaults because `value` may be missing keys - for (const key in defaults) { - if (defaults[key] instanceof Object) { - // `any` type is required because typescript can't predict the type of `value[key]`. - const target: any = value[key] || (defaults[key] instanceof Array ? [] : {}); - value[key] = falsyDefault(target, defaults[key]); - } else if (!value[key]) { - value[key] = defaults[key]; - } - } - - return value; -} diff --git a/libs/common/src/tools/generator/username/subaddress-generator-options.ts b/libs/common/src/tools/generator/username/subaddress-generator-options.ts index a43b8798ed..dc38b2a6ea 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-options.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-options.ts @@ -1,10 +1,18 @@ +import { RequestOptions } from "./options/forwarder-options"; +import { UsernameGenerationMode } from "./options/generator-options"; + /** Settings supported when generating an email subaddress */ export type SubaddressGenerationOptions = { - type?: "random" | "website-name"; - email?: string; -}; + /** selects the generation algorithm for the catchall email address. */ + subaddressType?: UsernameGenerationMode; + + /** the email address the subaddress is applied to. */ + subaddressEmail?: string; +} & RequestOptions; /** The default options for email subaddress generation. */ -export const DefaultSubaddressOptions: Partial = Object.freeze({ - type: "random", +export const DefaultSubaddressOptions: SubaddressGenerationOptions = Object.freeze({ + subaddressType: "random", + subaddressEmail: "", + website: null, }); diff --git a/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts index 59a2b56172..827bc7aed0 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts @@ -10,6 +10,11 @@ import { UserId } from "../../../types/guid"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { SUBADDRESS_SETTINGS } from "../key-definitions"; +import { + DefaultSubaddressOptions, + SubaddressGenerationOptions, +} from "./subaddress-generator-options"; + import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; @@ -47,6 +52,16 @@ describe("Email subaddress list generation strategy", () => { }); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new SubaddressGeneratorStrategy(null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultSubaddressOptions); + }); + }); + describe("cache_ms", () => { it("should be a positive non-zero number", () => { const legacy = mock(); @@ -70,16 +85,14 @@ describe("Email subaddress list generation strategy", () => { const legacy = mock(); const strategy = new SubaddressGeneratorStrategy(legacy, null); const options = { - type: "website-name" as const, - email: "someone@example.com", - }; + subaddressType: "website-name", + subaddressEmail: "someone@example.com", + website: "foo.com", + } as SubaddressGenerationOptions; await strategy.generate(options); - expect(legacy.generateSubaddress).toHaveBeenCalledWith({ - subaddressType: "website-name" as const, - subaddressEmail: "someone@example.com", - }); + expect(legacy.generateSubaddress).toHaveBeenCalledWith(options); }); }); }); diff --git a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts index 1ae0cb9142..818741f8a9 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts @@ -1,19 +1,26 @@ -import { map, pipe } from "rxjs"; +import { BehaviorSubject, map, pipe } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; +import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { SUBADDRESS_SETTINGS } from "../key-definitions"; import { NoPolicy } from "../no-policy"; -import { SubaddressGenerationOptions } from "./subaddress-generator-options"; -import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; +import { + DefaultSubaddressOptions, + SubaddressGenerationOptions, +} from "./subaddress-generator-options"; const ONE_MINUTE = 60 * 1000; -/** Strategy for creating an email subaddress */ +/** Strategy for creating an email subaddress + * @remarks The subaddress is the part following the `+`. + * For example, if the email address is `jd+xyz@domain.io`, + * the subaddress is `xyz`. + */ export class SubaddressGeneratorStrategy implements GeneratorStrategy { @@ -30,6 +37,11 @@ export class SubaddressGeneratorStrategy return this.stateProvider.getUser(id, SUBADDRESS_SETTINGS); } + /** {@link GeneratorStrategy.defaults$} */ + defaults$(userId: UserId) { + return new BehaviorSubject({ ...DefaultSubaddressOptions }).asObservable(); + } + /** {@link GeneratorStrategy.policy} */ get policy() { // Uses password generator since there aren't policies @@ -49,9 +61,6 @@ export class SubaddressGeneratorStrategy /** {@link GeneratorStrategy.generate} */ generate(options: SubaddressGenerationOptions) { - return this.usernameService.generateSubaddress({ - subaddressEmail: options.email, - subaddressType: options.type, - }); + return this.usernameService.generateSubaddress(options); } } diff --git a/libs/common/src/tools/generator/username/username-generation-options.ts b/libs/common/src/tools/generator/username/username-generation-options.ts index 2cb1e8dfd6..b52b4c0848 100644 --- a/libs/common/src/tools/generator/username/username-generation-options.ts +++ b/libs/common/src/tools/generator/username/username-generation-options.ts @@ -1,21 +1,23 @@ +import { CatchallGenerationOptions } from "./catchall-generator-options"; import { EffUsernameGenerationOptions } from "./eff-username-generator-options"; +import { ForwarderId, RequestOptions } from "./options/forwarder-options"; +import { UsernameGeneratorType } from "./options/generator-options"; +import { SubaddressGenerationOptions } from "./subaddress-generator-options"; -export type UsernameGeneratorOptions = EffUsernameGenerationOptions & { - type?: "word" | "subaddress" | "catchall" | "forwarded"; - subaddressType?: "random" | "website-name"; - subaddressEmail?: string; - catchallType?: "random" | "website-name"; - catchallDomain?: string; - website?: string; - forwardedService?: string; - forwardedAnonAddyApiToken?: string; - forwardedAnonAddyDomain?: string; - forwardedAnonAddyBaseUrl?: string; - forwardedDuckDuckGoToken?: string; - forwardedFirefoxApiToken?: string; - forwardedFastmailApiToken?: string; - forwardedForwardEmailApiToken?: string; - forwardedForwardEmailDomain?: string; - forwardedSimpleLoginApiKey?: string; - forwardedSimpleLoginBaseUrl?: string; -}; +export type UsernameGeneratorOptions = EffUsernameGenerationOptions & + SubaddressGenerationOptions & + CatchallGenerationOptions & + RequestOptions & { + type?: UsernameGeneratorType; + forwardedService?: ForwarderId | ""; + forwardedAnonAddyApiToken?: string; + forwardedAnonAddyDomain?: string; + forwardedAnonAddyBaseUrl?: string; + forwardedDuckDuckGoToken?: string; + forwardedFirefoxApiToken?: string; + forwardedFastmailApiToken?: string; + forwardedForwardEmailApiToken?: string; + forwardedForwardEmailDomain?: string; + forwardedSimpleLoginApiKey?: string; + forwardedSimpleLoginBaseUrl?: string; + }; diff --git a/libs/common/src/tools/generator/username/username-generation.service.ts b/libs/common/src/tools/generator/username/username-generation.service.ts index 245e7575b7..1ee642da5e 100644 --- a/libs/common/src/tools/generator/username/username-generation.service.ts +++ b/libs/common/src/tools/generator/username/username-generation.service.ts @@ -2,6 +2,7 @@ import { ApiService } from "../../../abstractions/api.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { EFFLongWordList } from "../../../platform/misc/wordlist"; +import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; import { AnonAddyForwarder, @@ -14,10 +15,10 @@ import { SimpleLoginForwarder, } from "./email-forwarders"; import { UsernameGeneratorOptions } from "./username-generation-options"; -import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; const DefaultOptions: UsernameGeneratorOptions = { type: "word", + website: null, wordCapitalize: true, wordIncludeNumber: true, subaddressType: "random",