diff --git a/libs/common/src/tools/integration/integration-id.ts b/libs/common/src/tools/integration/integration-id.ts index 46b81c3c4c..a15db143ee 100644 --- a/libs/common/src/tools/integration/integration-id.ts +++ b/libs/common/src/tools/integration/integration-id.ts @@ -1,7 +1,13 @@ import { Opaque } from "type-fest"; +export const IntegrationIds = [ + "anonaddy", + "duckduckgo", + "fastmail", + "firefoxrelay", + "forwardemail", + "simplelogin", +] as const; + /** Identifies a vendor integrated into bitwarden */ -export type IntegrationId = Opaque< - "anonaddy" | "duckduckgo" | "fastmail" | "firefoxrelay" | "forwardemail" | "simplelogin", - "IntegrationId" ->; +export type IntegrationId = Opaque<(typeof IntegrationIds)[number], "IntegrationId">; diff --git a/libs/tools/generator/components/src/credential-generator.component.ts b/libs/tools/generator/components/src/credential-generator.component.ts index f177340157..dfec1fc7c7 100644 --- a/libs/tools/generator/components/src/credential-generator.component.ts +++ b/libs/tools/generator/components/src/credential-generator.component.ts @@ -25,10 +25,13 @@ import { CredentialGeneratorService, GeneratedCredential, Generators, + getForwarderConfiguration, isEmailAlgorithm, + isForwarderIntegration, isPasswordAlgorithm, isUsernameAlgorithm, PasswordAlgorithm, + toCredentialGeneratorConfiguration, } from "@bitwarden/generator-core"; /** root category that drills into username and email categories */ @@ -247,10 +250,15 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { case "passphrase": return this.generatorService.generate$(Generators.passphrase, dependencies); - - default: - throw new Error(`Invalid generator type: "${type}"`); } + + if (isForwarderIntegration(type)) { + const forwarder = getForwarderConfiguration(type.forwarder); + const configuration = toCredentialGeneratorConfiguration(forwarder); + return this.generatorService.generate$(configuration, dependencies); + } + + throw new Error(`Invalid generator type: "${type}"`); } /** Lists the credential types of the username algorithm box. */ diff --git a/libs/tools/generator/components/src/username-generator.component.ts b/libs/tools/generator/components/src/username-generator.component.ts index 3fb4be4823..5e40c2732e 100644 --- a/libs/tools/generator/components/src/username-generator.component.ts +++ b/libs/tools/generator/components/src/username-generator.component.ts @@ -22,9 +22,11 @@ import { CredentialGeneratorService, GeneratedCredential, Generators, + getForwarderConfiguration, isEmailAlgorithm, isForwarderIntegration, isUsernameAlgorithm, + toCredentialGeneratorConfiguration, } from "@bitwarden/generator-core"; /** Component that generates usernames and emails */ @@ -79,10 +81,13 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { this.generatorService .algorithms$(["email", "username"], { userId$: this.userId$ }) .pipe( - map((algorithms) => [ - this.toOptions(algorithms.filter(a => !isForwarderIntegration(a.id))), - this.toOptions(algorithms.filter(a => isForwarderIntegration(a.id))) - ] as const), + map( + (algorithms) => + [ + this.toOptions(algorithms.filter((a) => !isForwarderIntegration(a.id))), + this.toOptions(algorithms.filter((a) => isForwarderIntegration(a.id))), + ] as const, + ), takeUntil(this.destroyed), ) .subscribe(([type, forwarder]) => { @@ -182,10 +187,15 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { case "username": return this.generatorService.generate$(Generators.username, dependencies); - - default: - throw new Error(`Invalid generator type: "${type}"`); } + + if (isForwarderIntegration(type)) { + const forwarder = getForwarderConfiguration(type.forwarder); + const configuration = toCredentialGeneratorConfiguration(forwarder); + return this.generatorService.generate$(configuration, dependencies); + } + + throw new Error(`Invalid generator type: "${type}"`); } /** Lists the credential types supported by the component. */ diff --git a/libs/tools/generator/core/src/data/generators.ts b/libs/tools/generator/core/src/data/generators.ts index bfbdea46b3..638d0d2b95 100644 --- a/libs/tools/generator/core/src/data/generators.ts +++ b/libs/tools/generator/core/src/data/generators.ts @@ -3,7 +3,12 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { ApiSettings } from "@bitwarden/common/tools/integration/rpc"; import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; -import { EmailRandomizer, PasswordRandomizer, UsernameRandomizer } from "../engine"; +import { + EmailRandomizer, + ForwarderConfiguration, + PasswordRandomizer, + UsernameRandomizer, +} from "../engine"; import { Forwarder } from "../engine/forwarder"; import { DefaultPolicyEvaluator, @@ -26,7 +31,6 @@ import { CredentialGenerator, CredentialGeneratorConfiguration, EffUsernameGenerationOptions, - ForwarderIntegration, GeneratorDependencyProvider, NoPolicy, PassphraseGenerationOptions, @@ -43,8 +47,6 @@ import { DefaultPassphraseGenerationOptions } from "./default-passphrase-generat import { DefaultPasswordBoundaries } from "./default-password-boundaries"; import { DefaultPasswordGenerationOptions } from "./default-password-generation-options"; import { DefaultSubaddressOptions } from "./default-subaddress-generator-options"; -import { getForwarderConfiguration } from "./integrations"; - const PASSPHRASE = Object.freeze({ id: "passphrase", @@ -52,7 +54,9 @@ const PASSPHRASE = Object.freeze({ nameKey: "passphrase", onlyOnRequest: false, engine: { - create(dependencies: GeneratorDependencyProvider): CredentialGenerator { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { return new PasswordRandomizer(dependencies.randomizer); }, }, @@ -89,7 +93,9 @@ const PASSWORD = Object.freeze({ nameKey: "password", onlyOnRequest: false, engine: { - create(dependencies: GeneratorDependencyProvider): CredentialGenerator { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { return new PasswordRandomizer(dependencies.randomizer); }, }, @@ -134,7 +140,9 @@ const USERNAME = Object.freeze({ nameKey: "randomWord", onlyOnRequest: false, engine: { - create(dependencies: GeneratorDependencyProvider): CredentialGenerator { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { return new UsernameRandomizer(dependencies.randomizer); }, }, @@ -165,7 +173,9 @@ const CATCHALL = Object.freeze({ descriptionKey: "catchallEmailDesc", onlyOnRequest: false, engine: { - create(dependencies: GeneratorDependencyProvider): CredentialGenerator { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { return new EmailRandomizer(dependencies.randomizer); }, }, @@ -196,7 +206,9 @@ const SUBADDRESS = Object.freeze({ descriptionKey: "plusAddressedEmailDesc", onlyOnRequest: false, engine: { - create(dependencies: GeneratorDependencyProvider): CredentialGenerator { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { return new EmailRandomizer(dependencies.randomizer); }, }, @@ -220,13 +232,9 @@ const SUBADDRESS = Object.freeze({ }, } satisfies CredentialGeneratorConfiguration); -export function toCredentialGeneratorConfiguration(id :ForwarderIntegration) { - // TODO: eliminate the type erasure - const configuration = getForwarderConfiguration(id.forwarder) as any; - if(!configuration) { - throw new Error(`Invalid forwarder id: ${id.forwarder}`); - } - +export function toCredentialGeneratorConfiguration( + configuration: ForwarderConfiguration, +) { const forwarder = Object.freeze({ id: { forwarder: configuration.id }, category: "email", @@ -234,13 +242,15 @@ export function toCredentialGeneratorConfiguration(); }, - } + }, } satisfies CredentialGeneratorConfiguration); return forwarder; diff --git a/libs/tools/generator/core/src/data/integrations.ts b/libs/tools/generator/core/src/data/integrations.ts index f70a601d51..fac1f07a1b 100644 --- a/libs/tools/generator/core/src/data/integrations.ts +++ b/libs/tools/generator/core/src/data/integrations.ts @@ -9,6 +9,13 @@ import { FirefoxRelay } from "../integration/firefox-relay"; import { ForwardEmail } from "../integration/forward-email"; import { SimpleLogin } from "../integration/simple-login"; +/** Fixed list of integrations available to the application + * @example + * + * // Use `toCredentialGeneratorConfiguration(id :ForwarderIntegration)` + * // to convert an integration to a generator configuration + * const generator = toCredentialGeneratorConfiguration(Integrations.AddyIo); + */ export const Integrations = Object.freeze({ AddyIo, DuckDuckGo, @@ -18,13 +25,13 @@ export const Integrations = Object.freeze({ SimpleLogin, } as const); -const integrations = Object.fromEntries(Object.values(Integrations).map((i) => [i.id, i as ForwarderConfiguration])); +const integrations = new Map(Object.values(Integrations).map((i) => [i.id, i])); -export function getForwarderConfiguration(id: IntegrationId) : ForwarderConfiguration { - const maybeForwarder = integrations[id as string]; +export function getForwarderConfiguration(id: IntegrationId): ForwarderConfiguration { + const maybeForwarder = integrations.get(id); - if("forwarder" in maybeForwarder) { - return maybeForwarder as ForwarderConfiguration + if ("forwarder" in maybeForwarder) { + return maybeForwarder as ForwarderConfiguration; } else { return null; } diff --git a/libs/tools/generator/core/src/services/credential-generator.service.ts b/libs/tools/generator/core/src/services/credential-generator.service.ts index 9f8f61b6de..b20f216c7e 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.ts @@ -36,7 +36,7 @@ import { isDynamic } from "@bitwarden/common/tools/state/state-constraints-depen import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; import { Randomizer } from "../abstractions"; -import { Generators, toCredentialGeneratorConfiguration } from "../data"; +import { Generators, getForwarderConfiguration, toCredentialGeneratorConfiguration } from "../data"; import { availableAlgorithms } from "../policies/available-algorithms-policy"; import { mapPolicyToConstraints } from "../rx"; import { @@ -45,9 +45,12 @@ import { CredentialCategory, CredentialGeneratorInfo, CredentialPreference, - isForwarderIntegration + isForwarderIntegration, } from "../types"; -import { CredentialGeneratorConfiguration as Configuration, GeneratorDependencyProvider } from "../types/credential-generator-configuration"; +import { + CredentialGeneratorConfiguration as Configuration, + GeneratorDependencyProvider, +} from "../types/credential-generator-configuration"; import { GeneratorConstraints } from "../types/generator-constraints"; import { PREFERENCES } from "./credential-preferences"; @@ -65,7 +68,7 @@ type Generate$Dependencies = Simplify & Partial; - integration$?: Observable + integration$?: Observable; }; type Algorithms$Dependencies = Partial; @@ -76,14 +79,14 @@ export class CredentialGeneratorService { private stateProvider: StateProvider, private policyService: PolicyService, private apiService: ApiService, - private i18nService: I18nService + private i18nService: I18nService, ) {} - private getDependencyProvider() : GeneratorDependencyProvider { + private getDependencyProvider(): GeneratorDependencyProvider { return { client: new RestClient(this.apiService, this.i18nService), i18nService: this.i18nService, - randomizer: this.randomizer + randomizer: this.randomizer, }; } @@ -105,10 +108,7 @@ export class CredentialGeneratorService { // stream blocks until all of these values are received const website$ = dependencies?.website$ ?? new BehaviorSubject(null); - const integration$ = dependencies?.integration$ ?? new BehaviorSubject(null); - const request$ = combineLatest([website$, integration$]).pipe( - map(([website, integration]) => ({ website, integration })) - ); + const request$ = website$.pipe(map((website) => ({ website }))); const settings$ = this.settings$(configuration, dependencies); // monitor completion @@ -183,7 +183,9 @@ export class CredentialGeneratorService { return policies$; }), map((available) => { - const filtered = algorithms.filter((c) => isForwarderIntegration(c.id) || available.has(c.id)); + const filtered = algorithms.filter( + (c) => isForwarderIntegration(c.id) || available.has(c.id), + ); return filtered; }), ); @@ -212,8 +214,14 @@ export class CredentialGeneratorService { * @returns the requested metadata, or `null` if the metadata wasn't found. */ algorithm(id: CredentialAlgorithm): CredentialGeneratorInfo { - if(isForwarderIntegration(id)) { - return toCredentialGeneratorConfiguration(id); + if (isForwarderIntegration(id)) { + const forwarder = getForwarderConfiguration(id.forwarder); + if (!forwarder) { + throw new Error(`Invalid forwarder id: ${id.forwarder}`); + } + + const generator = toCredentialGeneratorConfiguration(forwarder); + return generator; } else { return Generators[id]; } diff --git a/libs/tools/generator/core/src/types/credential-generator-configuration.ts b/libs/tools/generator/core/src/types/credential-generator-configuration.ts index 829e0ff887..8f6f6024a9 100644 --- a/libs/tools/generator/core/src/types/credential-generator-configuration.ts +++ b/libs/tools/generator/core/src/types/credential-generator-configuration.ts @@ -9,14 +9,19 @@ import { CredentialAlgorithm, CredentialCategory, PolicyConfiguration } from ".. import { CredentialGenerator } from "./credential-generator"; export type GeneratorDependencyProvider = { - randomizer: Randomizer, - client: RestClient, - i18nService: I18nService + randomizer: Randomizer; + client: RestClient; + i18nService: I18nService; }; /** Credential generator metadata common across credential generators */ export type CredentialGeneratorInfo = { /** Uniquely identifies the credential configuration + * @example + * // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)` + * // to pattern test whether the credential describes a forwarder algorithm + * const meta : CredentialGeneratorInfo = // ... + * const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; */ id: CredentialAlgorithm; @@ -37,7 +42,13 @@ export type CredentialGeneratorInfo = { onlyOnRequest: boolean; }; -/** Credential generator metadata that relies upon typed setting and policy definitions. */ +/** Credential generator metadata that relies upon typed setting and policy definitions. + * @example + * // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)` + * // to pattern test whether the credential describes a forwarder algorithm + * const meta : CredentialGeneratorInfo = // ... + * const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; + */ export type CredentialGeneratorConfiguration = CredentialGeneratorInfo & { /** An algorithm that generates credentials when ran. */ engine: {