mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-29 12:55:21 +01:00
[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`
This commit is contained in:
parent
ff3ff89e20
commit
b579bc8f96
@ -33,7 +33,7 @@ export class GeneratorComponent implements OnInit {
|
|||||||
subaddressOptions: any[];
|
subaddressOptions: any[];
|
||||||
catchallOptions: any[];
|
catchallOptions: any[];
|
||||||
forwardOptions: EmailForwarderOptions[];
|
forwardOptions: EmailForwarderOptions[];
|
||||||
usernameOptions: UsernameGeneratorOptions = {};
|
usernameOptions: UsernameGeneratorOptions = { website: null };
|
||||||
passwordOptions: PasswordGeneratorOptions = {};
|
passwordOptions: PasswordGeneratorOptions = {};
|
||||||
username = "-";
|
username = "-";
|
||||||
password = "-";
|
password = "-";
|
||||||
@ -199,12 +199,12 @@ export class GeneratorComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sliderInput() {
|
async sliderInput() {
|
||||||
this.normalizePasswordOptions();
|
await this.normalizePasswordOptions();
|
||||||
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async savePasswordOptions(regenerate = true) {
|
async savePasswordOptions(regenerate = true) {
|
||||||
this.normalizePasswordOptions();
|
await this.normalizePasswordOptions();
|
||||||
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
||||||
|
|
||||||
if (regenerate && this.regenerateWithoutButtonPress()) {
|
if (regenerate && this.regenerateWithoutButtonPress()) {
|
||||||
@ -271,7 +271,7 @@ export class GeneratorComponent implements OnInit {
|
|||||||
return this.type !== "username" || this.usernameOptions.type !== "forwarded";
|
return this.type !== "username" || this.usernameOptions.type !== "forwarded";
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizePasswordOptions() {
|
private async normalizePasswordOptions() {
|
||||||
// Application level normalize options dependent on class variables
|
// Application level normalize options dependent on class variables
|
||||||
this.passwordOptions.ambiguous = !this.avoidAmbiguous;
|
this.passwordOptions.ambiguous = !this.avoidAmbiguous;
|
||||||
|
|
||||||
@ -290,9 +290,8 @@ export class GeneratorComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.passwordGenerationService.normalizeOptions(
|
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(
|
||||||
this.passwordOptions,
|
this.passwordOptions,
|
||||||
this.enforcedPasswordPolicyOptions,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
|
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
|
||||||
|
@ -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<GeneratorNavigation>;
|
||||||
|
|
||||||
|
/** Gets the default options. */
|
||||||
|
defaults$: (userId: UserId) => Observable<GeneratorNavigation>;
|
||||||
|
|
||||||
|
/** 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<PolicyEvaluator<GeneratorNavigationPolicy, GeneratorNavigation>>;
|
||||||
|
|
||||||
|
/** 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<GeneratorNavigation>;
|
||||||
|
|
||||||
|
/** 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<void>;
|
||||||
|
}
|
@ -17,6 +17,9 @@ export abstract class GeneratorStrategy<Options, Policy> {
|
|||||||
*/
|
*/
|
||||||
durableState: (userId: UserId) => SingleUserState<Options>;
|
durableState: (userId: UserId) => SingleUserState<Options>;
|
||||||
|
|
||||||
|
/** Gets the default options. */
|
||||||
|
defaults$: (userId: UserId) => Observable<Options>;
|
||||||
|
|
||||||
/** Identifies the policy enforced by the generator. */
|
/** Identifies the policy enforced by the generator. */
|
||||||
policy: PolicyType;
|
policy: PolicyType;
|
||||||
|
|
||||||
|
@ -21,6 +21,9 @@ export abstract class GeneratorService<Options, Policy> {
|
|||||||
*/
|
*/
|
||||||
evaluator$: (userId: UserId) => Observable<PolicyEvaluator<Policy, Options>>;
|
evaluator$: (userId: UserId) => Observable<PolicyEvaluator<Policy, Options>>;
|
||||||
|
|
||||||
|
/** Gets the default options. */
|
||||||
|
defaults$: (userId: UserId) => Observable<Options>;
|
||||||
|
|
||||||
/** Enforces the policy on the given options
|
/** Enforces the policy on the given options
|
||||||
* @param userId: Identifies the user making the request
|
* @param userId: Identifies the user making the request
|
||||||
* @param options the options to enforce the policy on
|
* @param options the options to enforce the policy on
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export { GeneratorNavigationService } from "./generator-navigation.service.abstraction";
|
||||||
export { GeneratorService } from "./generator.service.abstraction";
|
export { GeneratorService } from "./generator.service.abstraction";
|
||||||
export { GeneratorStrategy } from "./generator-strategy.abstraction";
|
export { GeneratorStrategy } from "./generator-strategy.abstraction";
|
||||||
export { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
export { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
|
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";
|
/** @deprecated Use {@link GeneratorService} with a password or passphrase {@link GeneratorStrategy} instead. */
|
||||||
import { PasswordGeneratorOptions } from "./password-generator-options";
|
|
||||||
|
|
||||||
export abstract class PasswordGenerationServiceAbstraction {
|
export abstract class PasswordGenerationServiceAbstraction {
|
||||||
generatePassword: (options: PasswordGeneratorOptions) => Promise<string>;
|
generatePassword: (options: PasswordGeneratorOptions) => Promise<string>;
|
||||||
generatePassphrase: (options: PasswordGeneratorOptions) => Promise<string>;
|
generatePassphrase: (options: PasswordGeneratorOptions) => Promise<string>;
|
||||||
@ -10,13 +10,8 @@ export abstract class PasswordGenerationServiceAbstraction {
|
|||||||
enforcePasswordGeneratorPoliciesOnOptions: (
|
enforcePasswordGeneratorPoliciesOnOptions: (
|
||||||
options: PasswordGeneratorOptions,
|
options: PasswordGeneratorOptions,
|
||||||
) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
||||||
getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>;
|
|
||||||
saveOptions: (options: PasswordGeneratorOptions) => Promise<void>;
|
saveOptions: (options: PasswordGeneratorOptions) => Promise<void>;
|
||||||
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
||||||
addHistory: (password: string) => Promise<void>;
|
addHistory: (password: string) => Promise<void>;
|
||||||
clear: (userId?: string) => Promise<void>;
|
clear: (userId?: string) => Promise<void>;
|
||||||
normalizeOptions: (
|
|
||||||
options: PasswordGeneratorOptions,
|
|
||||||
enforcedPolicyOptions: PasswordGeneratorPolicyOptions,
|
|
||||||
) => void;
|
|
||||||
}
|
}
|
@ -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 {
|
export abstract class UsernameGenerationServiceAbstraction {
|
||||||
generateUsername: (options: UsernameGeneratorOptions) => Promise<string>;
|
generateUsername: (options: UsernameGeneratorOptions) => Promise<string>;
|
||||||
generateWord: (options: UsernameGeneratorOptions) => Promise<string>;
|
generateWord: (options: UsernameGeneratorOptions) => Promise<string>;
|
@ -37,6 +37,7 @@ function mockGeneratorStrategy(config?: {
|
|||||||
userState?: SingleUserState<any>;
|
userState?: SingleUserState<any>;
|
||||||
policy?: PolicyType;
|
policy?: PolicyType;
|
||||||
evaluator?: any;
|
evaluator?: any;
|
||||||
|
defaults?: any;
|
||||||
}) {
|
}) {
|
||||||
const durableState =
|
const durableState =
|
||||||
config?.userState ?? new FakeSingleUserState<PasswordGenerationOptions>(SomeUser);
|
config?.userState ?? new FakeSingleUserState<PasswordGenerationOptions>(SomeUser);
|
||||||
@ -45,6 +46,7 @@ function mockGeneratorStrategy(config?: {
|
|||||||
// whether they're used properly are guaranteed to test
|
// whether they're used properly are guaranteed to test
|
||||||
// the value from `config`.
|
// the value from `config`.
|
||||||
durableState: jest.fn(() => durableState),
|
durableState: jest.fn(() => durableState),
|
||||||
|
defaults$: jest.fn(() => new BehaviorSubject(config?.defaults)),
|
||||||
policy: config?.policy ?? PolicyType.DisableSend,
|
policy: config?.policy ?? PolicyType.DisableSend,
|
||||||
toEvaluator: jest.fn(() =>
|
toEvaluator: jest.fn(() =>
|
||||||
pipe(map(() => config?.evaluator ?? mock<PolicyEvaluator<any, any>>())),
|
pipe(map(() => config?.evaluator ?? mock<PolicyEvaluator<any, any>>())),
|
||||||
@ -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()", () => {
|
describe("saveOptions()", () => {
|
||||||
it("should trigger an options$ update", async () => {
|
it("should trigger an options$ update", async () => {
|
||||||
const policy = mockPolicyService();
|
const policy = mockPolicyService();
|
||||||
|
@ -21,17 +21,22 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic
|
|||||||
|
|
||||||
private _evaluators$ = new Map<UserId, Observable<PolicyEvaluator<Policy, Options>>>();
|
private _evaluators$ = new Map<UserId, Observable<PolicyEvaluator<Policy, Options>>>();
|
||||||
|
|
||||||
/** {@link GeneratorService.options$()} */
|
/** {@link GeneratorService.options$} */
|
||||||
options$(userId: UserId) {
|
options$(userId: UserId) {
|
||||||
return this.strategy.durableState(userId).state$;
|
return this.strategy.durableState(userId).state$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link GeneratorService.defaults$} */
|
||||||
|
defaults$(userId: UserId) {
|
||||||
|
return this.strategy.defaults$(userId);
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link GeneratorService.saveOptions} */
|
/** {@link GeneratorService.saveOptions} */
|
||||||
async saveOptions(userId: UserId, options: Options): Promise<void> {
|
async saveOptions(userId: UserId, options: Options): Promise<void> {
|
||||||
await this.strategy.durableState(userId).update(() => options);
|
await this.strategy.durableState(userId).update(() => options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorService.evaluator$()} */
|
/** {@link GeneratorService.evaluator$} */
|
||||||
evaluator$(userId: UserId) {
|
evaluator$(userId: UserId) {
|
||||||
let evaluator$ = this._evaluators$.get(userId);
|
let evaluator$ = this._evaluators$.get(userId);
|
||||||
|
|
||||||
@ -59,7 +64,7 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic
|
|||||||
return evaluator$;
|
return evaluator$;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorService.enforcePolicy()} */
|
/** {@link GeneratorService.enforcePolicy} */
|
||||||
async enforcePolicy(userId: UserId, options: Options): Promise<Options> {
|
async enforcePolicy(userId: UserId, options: Options): Promise<Options> {
|
||||||
const policy = await firstValueFrom(this.evaluator$(userId));
|
const policy = await firstValueFrom(this.evaluator$(userId));
|
||||||
const evaluated = policy.applyPolicy(options);
|
const evaluated = policy.applyPolicy(options);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
export type GeneratorOptions = {
|
// this export provided solely for backwards compatibility
|
||||||
type?: "password" | "username";
|
export {
|
||||||
};
|
/** @deprecated use `GeneratorNavigation` from './navigation' instead. */
|
||||||
|
GeneratorNavigation as GeneratorOptions,
|
||||||
|
} from "./navigation/generator-navigation";
|
||||||
|
2
libs/common/src/tools/generator/generator-type.ts
Normal file
2
libs/common/src/tools/generator/generator-type.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/** The kind of credential being generated. */
|
||||||
|
export type GeneratorType = "password" | "passphrase" | "username";
|
@ -10,9 +10,18 @@ import {
|
|||||||
FASTMAIL_FORWARDER,
|
FASTMAIL_FORWARDER,
|
||||||
DUCK_DUCK_GO_FORWARDER,
|
DUCK_DUCK_GO_FORWARDER,
|
||||||
ADDY_IO_FORWARDER,
|
ADDY_IO_FORWARDER,
|
||||||
|
GENERATOR_SETTINGS,
|
||||||
} from "./key-definitions";
|
} from "./key-definitions";
|
||||||
|
|
||||||
describe("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", () => {
|
describe("PASSWORD_SETTINGS", () => {
|
||||||
it("should pass through deserialization", () => {
|
it("should pass through deserialization", () => {
|
||||||
const value = {};
|
const value = {};
|
||||||
@ -31,7 +40,7 @@ describe("Key definitions", () => {
|
|||||||
|
|
||||||
describe("EFF_USERNAME_SETTINGS", () => {
|
describe("EFF_USERNAME_SETTINGS", () => {
|
||||||
it("should pass through deserialization", () => {
|
it("should pass through deserialization", () => {
|
||||||
const value = {};
|
const value = { website: null as string };
|
||||||
const result = EFF_USERNAME_SETTINGS.deserializer(value);
|
const result = EFF_USERNAME_SETTINGS.deserializer(value);
|
||||||
expect(result).toBe(value);
|
expect(result).toBe(value);
|
||||||
});
|
});
|
||||||
@ -39,7 +48,7 @@ describe("Key definitions", () => {
|
|||||||
|
|
||||||
describe("CATCHALL_SETTINGS", () => {
|
describe("CATCHALL_SETTINGS", () => {
|
||||||
it("should pass through deserialization", () => {
|
it("should pass through deserialization", () => {
|
||||||
const value = {};
|
const value = { website: null as string };
|
||||||
const result = CATCHALL_SETTINGS.deserializer(value);
|
const result = CATCHALL_SETTINGS.deserializer(value);
|
||||||
expect(result).toBe(value);
|
expect(result).toBe(value);
|
||||||
});
|
});
|
||||||
@ -47,7 +56,7 @@ describe("Key definitions", () => {
|
|||||||
|
|
||||||
describe("SUBADDRESS_SETTINGS", () => {
|
describe("SUBADDRESS_SETTINGS", () => {
|
||||||
it("should pass through deserialization", () => {
|
it("should pass through deserialization", () => {
|
||||||
const value = {};
|
const value = { website: null as string };
|
||||||
const result = SUBADDRESS_SETTINGS.deserializer(value);
|
const result = SUBADDRESS_SETTINGS.deserializer(value);
|
||||||
expect(result).toBe(value);
|
expect(result).toBe(value);
|
||||||
});
|
});
|
||||||
|
@ -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 { GeneratedCredential } from "./history/generated-credential";
|
||||||
|
import { GeneratorNavigation } from "./navigation/generator-navigation";
|
||||||
import { PassphraseGenerationOptions } from "./passphrase/passphrase-generation-options";
|
import { PassphraseGenerationOptions } from "./passphrase/passphrase-generation-options";
|
||||||
import { PasswordGenerationOptions } from "./password/password-generation-options";
|
import { PasswordGenerationOptions } from "./password/password-generation-options";
|
||||||
import { SecretClassifier } from "./state/secret-classifier";
|
import { SecretClassifier } from "./state/secret-classifier";
|
||||||
@ -15,6 +16,15 @@ import {
|
|||||||
} from "./username/options/forwarder-options";
|
} from "./username/options/forwarder-options";
|
||||||
import { SubaddressGenerationOptions } from "./username/subaddress-generator-options";
|
import { SubaddressGenerationOptions } from "./username/subaddress-generator-options";
|
||||||
|
|
||||||
|
/** plaintext password generation options */
|
||||||
|
export const GENERATOR_SETTINGS = new KeyDefinition<GeneratorNavigation>(
|
||||||
|
GENERATOR_MEMORY,
|
||||||
|
"generatorSettings",
|
||||||
|
{
|
||||||
|
deserializer: (value) => value,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/** plaintext password generation options */
|
/** plaintext password generation options */
|
||||||
export const PASSWORD_SETTINGS = new KeyDefinition<PasswordGenerationOptions>(
|
export const PASSWORD_SETTINGS = new KeyDefinition<PasswordGenerationOptions>(
|
||||||
GENERATOR_DISK,
|
GENERATOR_DISK,
|
||||||
@ -42,7 +52,7 @@ export const EFF_USERNAME_SETTINGS = new KeyDefinition<EffUsernameGenerationOpti
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/** catchall email generation options */
|
/** plaintext configuration for a domain catch-all address. */
|
||||||
export const CATCHALL_SETTINGS = new KeyDefinition<CatchallGenerationOptions>(
|
export const CATCHALL_SETTINGS = new KeyDefinition<CatchallGenerationOptions>(
|
||||||
GENERATOR_DISK,
|
GENERATOR_DISK,
|
||||||
"catchallGeneratorSettings",
|
"catchallGeneratorSettings",
|
||||||
@ -51,7 +61,7 @@ export const CATCHALL_SETTINGS = new KeyDefinition<CatchallGenerationOptions>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/** email subaddress generation options */
|
/** plaintext configuration for an email subaddress. */
|
||||||
export const SUBADDRESS_SETTINGS = new KeyDefinition<SubaddressGenerationOptions>(
|
export const SUBADDRESS_SETTINGS = new KeyDefinition<SubaddressGenerationOptions>(
|
||||||
GENERATOR_DISK,
|
GENERATOR_DISK,
|
||||||
"subaddressGeneratorSettings",
|
"subaddressGeneratorSettings",
|
||||||
@ -60,6 +70,7 @@ export const SUBADDRESS_SETTINGS = new KeyDefinition<SubaddressGenerationOptions
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link Forwarders.AddyIo} */
|
||||||
export const ADDY_IO_FORWARDER = new KeyDefinition<SelfHostedApiOptions & EmailDomainOptions>(
|
export const ADDY_IO_FORWARDER = new KeyDefinition<SelfHostedApiOptions & EmailDomainOptions>(
|
||||||
GENERATOR_DISK,
|
GENERATOR_DISK,
|
||||||
"addyIoForwarder",
|
"addyIoForwarder",
|
||||||
@ -68,6 +79,7 @@ export const ADDY_IO_FORWARDER = new KeyDefinition<SelfHostedApiOptions & EmailD
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link Forwarders.DuckDuckGo} */
|
||||||
export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition<ApiOptions>(
|
export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition<ApiOptions>(
|
||||||
GENERATOR_DISK,
|
GENERATOR_DISK,
|
||||||
"duckDuckGoForwarder",
|
"duckDuckGoForwarder",
|
||||||
@ -76,6 +88,7 @@ export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition<ApiOptions>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link Forwarders.FastMail} */
|
||||||
export const FASTMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailPrefixOptions>(
|
export const FASTMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailPrefixOptions>(
|
||||||
GENERATOR_DISK,
|
GENERATOR_DISK,
|
||||||
"fastmailForwarder",
|
"fastmailForwarder",
|
||||||
@ -84,6 +97,7 @@ export const FASTMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailPrefixOpti
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link Forwarders.FireFoxRelay} */
|
||||||
export const FIREFOX_RELAY_FORWARDER = new KeyDefinition<ApiOptions>(
|
export const FIREFOX_RELAY_FORWARDER = new KeyDefinition<ApiOptions>(
|
||||||
GENERATOR_DISK,
|
GENERATOR_DISK,
|
||||||
"firefoxRelayForwarder",
|
"firefoxRelayForwarder",
|
||||||
@ -92,6 +106,7 @@ export const FIREFOX_RELAY_FORWARDER = new KeyDefinition<ApiOptions>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link Forwarders.ForwardEmail} */
|
||||||
export const FORWARD_EMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailDomainOptions>(
|
export const FORWARD_EMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailDomainOptions>(
|
||||||
GENERATOR_DISK,
|
GENERATOR_DISK,
|
||||||
"forwardEmailForwarder",
|
"forwardEmailForwarder",
|
||||||
@ -100,6 +115,7 @@ export const FORWARD_EMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailDomai
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link forwarders.SimpleLogin} */
|
||||||
export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition<SelfHostedApiOptions>(
|
export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition<SelfHostedApiOptions>(
|
||||||
GENERATOR_DISK,
|
GENERATOR_DISK,
|
||||||
"simpleLoginForwarder",
|
"simpleLoginForwarder",
|
||||||
|
@ -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<GeneratorService<PassphraseGenerationOptions, PassphraseGeneratorPolicy>>({
|
||||||
|
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<GeneratorService<PasswordGenerationOptions, PasswordGeneratorPolicy>>({
|
||||||
|
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<GeneratorNavigationService>({
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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<any[]>;
|
||||||
|
addHistory: (password: string) => Promise<void>;
|
||||||
|
clear: (userId?: string) => Promise<void>;
|
||||||
|
}
|
@ -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: Options, defaults: Options) {
|
||||||
|
let savedOptions = options;
|
||||||
|
const generator = mock<GeneratorService<Options, NoPolicy>>({
|
||||||
|
evaluator$(id: UserId) {
|
||||||
|
const evaluator = new DefaultPolicyEvaluator<Options>();
|
||||||
|
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<GeneratorNavigationService>({
|
||||||
|
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<CatchallGenerationOptions>(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<EffUsernameGenerationOptions>(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<SubaddressGenerationOptions>(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<SelfHostedApiOptions & EmailDomainOptions>(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<CatchallGenerationOptions>(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<SubaddressGenerationOptions>(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<SelfHostedApiOptions & EmailDomainOptions>(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<ApiOptions>(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<ApiOptions & EmailPrefixOptions>(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<ApiOptions>(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<ApiOptions & EmailDomainOptions>(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<SelfHostedApiOptions>(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<CatchallGenerationOptions>(
|
||||||
|
{
|
||||||
|
catchallDomain: "example.com",
|
||||||
|
catchallType: "random",
|
||||||
|
website: null,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const effUsername = createGenerator<EffUsernameGenerationOptions>(
|
||||||
|
{
|
||||||
|
wordCapitalize: true,
|
||||||
|
wordIncludeNumber: false,
|
||||||
|
website: null,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const subaddress = createGenerator<SubaddressGenerationOptions>(
|
||||||
|
{
|
||||||
|
subaddressType: "random",
|
||||||
|
subaddressEmail: "foo@example.com",
|
||||||
|
website: null,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(
|
||||||
|
{
|
||||||
|
token: "addyIoToken",
|
||||||
|
domain: "addyio.example.com",
|
||||||
|
baseUrl: "https://addyio.api.example.com",
|
||||||
|
website: null,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const duckDuckGo = createGenerator<ApiOptions>(
|
||||||
|
{
|
||||||
|
token: "ddgToken",
|
||||||
|
website: null,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(
|
||||||
|
{
|
||||||
|
token: "fastmailToken",
|
||||||
|
domain: "fastmail.example.com",
|
||||||
|
prefix: "foo",
|
||||||
|
website: null,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firefoxRelay = createGenerator<ApiOptions>(
|
||||||
|
{
|
||||||
|
token: "firefoxToken",
|
||||||
|
website: null,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(
|
||||||
|
{
|
||||||
|
token: "forwardEmailToken",
|
||||||
|
domain: "example.com",
|
||||||
|
website: null,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const simpleLogin = createGenerator<SelfHostedApiOptions>(
|
||||||
|
{
|
||||||
|
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<CatchallGenerationOptions>(null, DefaultCatchallOptions);
|
||||||
|
const effUsername = createGenerator<EffUsernameGenerationOptions>(
|
||||||
|
null,
|
||||||
|
DefaultEffUsernameOptions,
|
||||||
|
);
|
||||||
|
const subaddress = createGenerator<SubaddressGenerationOptions>(
|
||||||
|
null,
|
||||||
|
DefaultSubaddressOptions,
|
||||||
|
);
|
||||||
|
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(
|
||||||
|
null,
|
||||||
|
DefaultAddyIoOptions,
|
||||||
|
);
|
||||||
|
const duckDuckGo = createGenerator<ApiOptions>(null, DefaultDuckDuckGoOptions);
|
||||||
|
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(
|
||||||
|
null,
|
||||||
|
DefaultFastmailOptions,
|
||||||
|
);
|
||||||
|
const firefoxRelay = createGenerator<ApiOptions>(null, DefaultFirefoxRelayOptions);
|
||||||
|
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(
|
||||||
|
null,
|
||||||
|
DefaultForwardEmailOptions,
|
||||||
|
);
|
||||||
|
const simpleLogin = createGenerator<SelfHostedApiOptions>(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<CatchallGenerationOptions>(null, null);
|
||||||
|
const effUsername = createGenerator<EffUsernameGenerationOptions>(null, null);
|
||||||
|
const subaddress = createGenerator<SubaddressGenerationOptions>(null, null);
|
||||||
|
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, null);
|
||||||
|
const duckDuckGo = createGenerator<ApiOptions>(null, null);
|
||||||
|
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(null, null);
|
||||||
|
const firefoxRelay = createGenerator<ApiOptions>(null, null);
|
||||||
|
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(null, null);
|
||||||
|
const simpleLogin = createGenerator<SelfHostedApiOptions>(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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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<CatchallGenerationOptions, NoPolicy>,
|
||||||
|
private readonly effUsername: GeneratorService<EffUsernameGenerationOptions, NoPolicy>,
|
||||||
|
private readonly subaddress: GeneratorService<SubaddressGenerationOptions, NoPolicy>,
|
||||||
|
private readonly addyIo: GeneratorService<SelfHostedApiOptions & EmailDomainOptions, NoPolicy>,
|
||||||
|
private readonly duckDuckGo: GeneratorService<ApiOptions, NoPolicy>,
|
||||||
|
private readonly fastmail: GeneratorService<ApiOptions & EmailPrefixOptions, NoPolicy>,
|
||||||
|
private readonly firefoxRelay: GeneratorService<ApiOptions, NoPolicy>,
|
||||||
|
private readonly forwardEmail: GeneratorService<ApiOptions & EmailDomainOptions, NoPolicy>,
|
||||||
|
private readonly simpleLogin: GeneratorService<SelfHostedApiOptions, NoPolicy>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<PolicyService>({
|
||||||
|
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<PolicyService>({
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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<GeneratorNavigation> {
|
||||||
|
return this.stateProvider.getUserState$(GENERATOR_SETTINGS, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the default options. */
|
||||||
|
defaults$(userId: UserId): Observable<GeneratorNavigation> {
|
||||||
|
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<void> {
|
||||||
|
await this.stateProvider.setUserState(GENERATOR_SETTINGS, options, userId);
|
||||||
|
}
|
||||||
|
}
|
@ -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" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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<GeneratorNavigationPolicy, GeneratorNavigation>
|
||||||
|
{
|
||||||
|
/** 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -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" });
|
||||||
|
});
|
||||||
|
});
|
@ -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,
|
||||||
|
});
|
@ -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<GeneratorNavigation> = Object.freeze({
|
||||||
|
type: "password",
|
||||||
|
username: "word",
|
||||||
|
forwarder: "",
|
||||||
|
});
|
3
libs/common/src/tools/generator/navigation/index.ts
Normal file
3
libs/common/src/tools/generator/navigation/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
|
||||||
|
export { DefaultGeneratorNavigationService } from "./default-generator-navigation.service";
|
||||||
|
export { GeneratorNavigation, DefaultGeneratorNavigation } from "./generator-navigation";
|
@ -2,4 +2,7 @@
|
|||||||
export { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
export { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
||||||
export { PassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
export { PassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
||||||
export { PassphraseGeneratorStrategy } from "./passphrase-generator-strategy";
|
export { PassphraseGeneratorStrategy } from "./passphrase-generator-strategy";
|
||||||
export { DefaultPassphraseGenerationOptions } from "./passphrase-generation-options";
|
export {
|
||||||
|
DefaultPassphraseGenerationOptions,
|
||||||
|
PassphraseGenerationOptions,
|
||||||
|
} from "./passphrase-generation-options";
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
* include structuredClone in test environment.
|
* include structuredClone in test environment.
|
||||||
* @jest-environment ../../../../shared/test.environment.ts
|
* @jest-environment ../../../../shared/test.environment.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { of, firstValueFrom } from "rxjs";
|
import { of, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
@ -12,12 +11,16 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
|
||||||
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||||
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
|
||||||
|
|
||||||
import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
||||||
|
|
||||||
import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from ".";
|
import {
|
||||||
|
DefaultPassphraseGenerationOptions,
|
||||||
|
PassphraseGeneratorOptionsEvaluator,
|
||||||
|
PassphraseGeneratorStrategy,
|
||||||
|
} from ".";
|
||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
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", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import { map, pipe } from "rxjs";
|
import { BehaviorSubject, map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { GeneratorStrategy } from "..";
|
import { GeneratorStrategy } from "..";
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
|
||||||
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||||
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
|
||||||
import { reduceCollection } from "../reduce-collection.operator";
|
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 { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
||||||
import {
|
import {
|
||||||
DisabledPassphraseGeneratorPolicy,
|
DisabledPassphraseGeneratorPolicy,
|
||||||
@ -36,6 +39,11 @@ export class PassphraseGeneratorStrategy
|
|||||||
return this.stateProvider.getUser(id, PASSPHRASE_SETTINGS);
|
return this.stateProvider.getUser(id, PASSPHRASE_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets the default options. */
|
||||||
|
defaults$(_: UserId) {
|
||||||
|
return new BehaviorSubject({ ...DefaultPassphraseGenerationOptions }).asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
get policy() {
|
get policy() {
|
||||||
return PolicyType.PasswordGenerator;
|
return PolicyType.PasswordGenerator;
|
||||||
|
@ -6,6 +6,6 @@ export { PasswordGeneratorStrategy } from "./password-generator-strategy";
|
|||||||
|
|
||||||
// legacy interfaces
|
// legacy interfaces
|
||||||
export { PasswordGeneratorOptions } from "./password-generator-options";
|
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 { PasswordGenerationService } from "./password-generation.service";
|
||||||
export { GeneratedPasswordHistory } from "./generated-password-history";
|
export { GeneratedPasswordHistory } from "./generated-password-history";
|
||||||
|
@ -5,10 +5,10 @@ import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
|||||||
import { StateService } from "../../../platform/abstractions/state.service";
|
import { StateService } from "../../../platform/abstractions/state.service";
|
||||||
import { EFFLongWordList } from "../../../platform/misc/wordlist";
|
import { EFFLongWordList } from "../../../platform/misc/wordlist";
|
||||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
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 { PassphraseGeneratorOptionsEvaluator } from "../passphrase/passphrase-generator-options-evaluator";
|
||||||
|
|
||||||
import { GeneratedPasswordHistory } from "./generated-password-history";
|
import { GeneratedPasswordHistory } from "./generated-password-history";
|
||||||
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
|
||||||
import { PasswordGeneratorOptions } from "./password-generator-options";
|
import { PasswordGeneratorOptions } from "./password-generator-options";
|
||||||
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
||||||
|
|
||||||
@ -341,24 +341,6 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
|||||||
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
|
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) {
|
private capitalize(str: string) {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { GeneratorNavigation } from "../navigation/generator-navigation";
|
||||||
import { PassphraseGenerationOptions } from "../passphrase/passphrase-generation-options";
|
import { PassphraseGenerationOptions } from "../passphrase/passphrase-generation-options";
|
||||||
|
|
||||||
import { PasswordGenerationOptions } from "./password-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.
|
* This type includes all properties suitable for reactive data binding.
|
||||||
*/
|
*/
|
||||||
export type PasswordGeneratorOptions = PasswordGenerationOptions &
|
export type PasswordGeneratorOptions = PasswordGenerationOptions &
|
||||||
PassphraseGenerationOptions & {
|
PassphraseGenerationOptions &
|
||||||
/** The algorithm to use for credential generation.
|
GeneratorNavigation;
|
||||||
* 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";
|
|
||||||
};
|
|
||||||
|
@ -17,6 +17,7 @@ import { PASSWORD_SETTINGS } from "../key-definitions";
|
|||||||
import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy";
|
import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DefaultPasswordGenerationOptions,
|
||||||
PasswordGenerationServiceAbstraction,
|
PasswordGenerationServiceAbstraction,
|
||||||
PasswordGeneratorOptionsEvaluator,
|
PasswordGeneratorOptionsEvaluator,
|
||||||
PasswordGeneratorStrategy,
|
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", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import { map, pipe } from "rxjs";
|
import { BehaviorSubject, map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { GeneratorStrategy } from "..";
|
import { GeneratorStrategy } from "..";
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
|
||||||
import { PASSWORD_SETTINGS } from "../key-definitions";
|
import { PASSWORD_SETTINGS } from "../key-definitions";
|
||||||
import { reduceCollection } from "../reduce-collection.operator";
|
import { reduceCollection } from "../reduce-collection.operator";
|
||||||
|
|
||||||
import { PasswordGenerationOptions } from "./password-generation-options";
|
import {
|
||||||
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
DefaultPasswordGenerationOptions,
|
||||||
|
PasswordGenerationOptions,
|
||||||
|
} from "./password-generation-options";
|
||||||
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
||||||
import {
|
import {
|
||||||
DisabledPasswordGeneratorPolicy,
|
DisabledPasswordGeneratorPolicy,
|
||||||
@ -35,6 +38,11 @@ export class PasswordGeneratorStrategy
|
|||||||
return this.stateProvider.getUser(id, PASSWORD_SETTINGS);
|
return this.stateProvider.getUser(id, PASSWORD_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets the default options. */
|
||||||
|
defaults$(_: UserId) {
|
||||||
|
return new BehaviorSubject({ ...DefaultPasswordGenerationOptions }).asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
get policy() {
|
get policy() {
|
||||||
return PolicyType.PasswordGenerator;
|
return PolicyType.PasswordGenerator;
|
||||||
|
@ -1,10 +1,21 @@
|
|||||||
|
import { RequestOptions } from "./options/forwarder-options";
|
||||||
|
import { UsernameGenerationMode } from "./options/generator-options";
|
||||||
|
|
||||||
/** Settings supported when generating an email subaddress */
|
/** Settings supported when generating an email subaddress */
|
||||||
export type CatchallGenerationOptions = {
|
export type CatchallGenerationOptions = {
|
||||||
type?: "random" | "website-name";
|
/** selects the generation algorithm for the catchall email address. */
|
||||||
domain?: string;
|
catchallType?: UsernameGenerationMode;
|
||||||
};
|
|
||||||
|
|
||||||
/** The default options for email subaddress generation. */
|
/** The domain part of the generated email address.
|
||||||
export const DefaultCatchallOptions: Partial<CatchallGenerationOptions> = Object.freeze({
|
* @example If the domain is `domain.io` and the generated username
|
||||||
type: "random",
|
* 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,
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,8 @@ import { UserId } from "../../../types/guid";
|
|||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { CATCHALL_SETTINGS } from "../key-definitions";
|
import { CATCHALL_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
|
import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options";
|
||||||
|
|
||||||
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
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", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
@ -70,16 +82,14 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
type: "website-name" as const,
|
catchallType: "website-name",
|
||||||
domain: "example.com",
|
catchallDomain: "example.com",
|
||||||
};
|
website: "foo.com",
|
||||||
|
} as CatchallGenerationOptions;
|
||||||
|
|
||||||
await strategy.generate(options);
|
await strategy.generate(options);
|
||||||
|
|
||||||
expect(legacy.generateCatchall).toHaveBeenCalledWith({
|
expect(legacy.generateCatchall).toHaveBeenCalledWith(options);
|
||||||
catchallType: "website-name" as const,
|
|
||||||
catchallDomain: "example.com",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { map, pipe } from "rxjs";
|
import { BehaviorSubject, map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
|
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { CATCHALL_SETTINGS } from "../key-definitions";
|
import { CATCHALL_SETTINGS } from "../key-definitions";
|
||||||
import { NoPolicy } from "../no-policy";
|
import { NoPolicy } from "../no-policy";
|
||||||
|
|
||||||
import { CatchallGenerationOptions } from "./catchall-generator-options";
|
import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options";
|
||||||
import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";
|
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
@ -30,6 +30,11 @@ export class CatchallGeneratorStrategy
|
|||||||
return this.stateProvider.getUser(id, CATCHALL_SETTINGS);
|
return this.stateProvider.getUser(id, CATCHALL_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link GeneratorStrategy.defaults$} */
|
||||||
|
defaults$(userId: UserId) {
|
||||||
|
return new BehaviorSubject({ ...DefaultCatchallOptions }).asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
get policy() {
|
get policy() {
|
||||||
// Uses password generator since there aren't policies
|
// Uses password generator since there aren't policies
|
||||||
@ -49,9 +54,6 @@ export class CatchallGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.generate} */
|
/** {@link GeneratorStrategy.generate} */
|
||||||
generate(options: CatchallGenerationOptions) {
|
generate(options: CatchallGenerationOptions) {
|
||||||
return this.usernameService.generateCatchall({
|
return this.usernameService.generateCatchall(options);
|
||||||
catchallDomain: options.domain,
|
|
||||||
catchallType: options.type,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 = {
|
export type EffUsernameGenerationOptions = {
|
||||||
|
/** when true, the word is capitalized */
|
||||||
wordCapitalize?: boolean;
|
wordCapitalize?: boolean;
|
||||||
|
|
||||||
|
/** when true, a random number is appended to the username */
|
||||||
wordIncludeNumber?: boolean;
|
wordIncludeNumber?: boolean;
|
||||||
};
|
} & RequestOptions;
|
||||||
|
|
||||||
/** The default options for EFF long word generation. */
|
/** The default options for EFF long word generation. */
|
||||||
export const DefaultEffUsernameOptions: Partial<EffUsernameGenerationOptions> = Object.freeze({
|
export const DefaultEffUsernameOptions: EffUsernameGenerationOptions = Object.freeze({
|
||||||
wordCapitalize: false,
|
wordCapitalize: false,
|
||||||
wordIncludeNumber: false,
|
wordIncludeNumber: false,
|
||||||
|
website: null,
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,8 @@ import { UserId } from "../../../types/guid";
|
|||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
|
import { DefaultEffUsernameOptions } from "./eff-username-generator-options";
|
||||||
|
|
||||||
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
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", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
@ -72,6 +84,7 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
const options = {
|
const options = {
|
||||||
wordCapitalize: false,
|
wordCapitalize: false,
|
||||||
wordIncludeNumber: false,
|
wordIncludeNumber: false,
|
||||||
|
website: null as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
await strategy.generate(options);
|
await strategy.generate(options);
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { map, pipe } from "rxjs";
|
import { BehaviorSubject, map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
|
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||||
import { NoPolicy } from "../no-policy";
|
import { NoPolicy } from "../no-policy";
|
||||||
|
|
||||||
import { EffUsernameGenerationOptions } from "./eff-username-generator-options";
|
import {
|
||||||
import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";
|
DefaultEffUsernameOptions,
|
||||||
|
EffUsernameGenerationOptions,
|
||||||
|
} from "./eff-username-generator-options";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
@ -30,6 +33,11 @@ export class EffUsernameGeneratorStrategy
|
|||||||
return this.stateProvider.getUser(id, EFF_USERNAME_SETTINGS);
|
return this.stateProvider.getUser(id, EFF_USERNAME_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link GeneratorStrategy.defaults$} */
|
||||||
|
defaults$(userId: UserId) {
|
||||||
|
return new BehaviorSubject({ ...DefaultEffUsernameOptions }).asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
get policy() {
|
get policy() {
|
||||||
// Uses password generator since there aren't policies
|
// Uses password generator since there aren't policies
|
||||||
|
@ -15,6 +15,7 @@ import { DUCK_DUCK_GO_FORWARDER } from "../key-definitions";
|
|||||||
import { SecretState } from "../state/secret-state";
|
import { SecretState } from "../state/secret-state";
|
||||||
|
|
||||||
import { ForwarderGeneratorStrategy } from "./forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "./forwarder-generator-strategy";
|
||||||
|
import { DefaultDuckDuckGoOptions } from "./forwarders/duck-duck-go";
|
||||||
import { ApiOptions } from "./options/forwarder-options";
|
import { ApiOptions } from "./options/forwarder-options";
|
||||||
|
|
||||||
class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
||||||
@ -30,6 +31,10 @@ class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
|||||||
// arbitrary.
|
// arbitrary.
|
||||||
return DUCK_DUCK_GO_FORWARDER;
|
return DUCK_DUCK_GO_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaults$ = (userId: UserId) => {
|
||||||
|
return of(DefaultDuckDuckGoOptions);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
const SomeUser = "some user" as UserId;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { map, pipe } from "rxjs";
|
import { Observable, map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
@ -79,6 +79,9 @@ export abstract class ForwarderGeneratorStrategy<
|
|||||||
return new UserKeyEncryptor(this.encryptService, this.keyService, packer);
|
return new UserKeyEncryptor(this.encryptService, this.keyService, packer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets the default options. */
|
||||||
|
abstract defaults$: (userId: UserId) => Observable<Options>;
|
||||||
|
|
||||||
/** Determine where forwarder configuration is stored */
|
/** Determine where forwarder configuration is stored */
|
||||||
protected abstract readonly key: KeyDefinition<Options>;
|
protected abstract readonly key: KeyDefinition<Options>;
|
||||||
|
|
||||||
|
@ -2,12 +2,17 @@
|
|||||||
* include Request in test environment.
|
* include Request in test environment.
|
||||||
* @jest-environment ../../../../shared/test.environment.ts
|
* @jest-environment ../../../../shared/test.environment.ts
|
||||||
*/
|
*/
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { ADDY_IO_FORWARDER } from "../../key-definitions";
|
import { ADDY_IO_FORWARDER } from "../../key-definitions";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
|
|
||||||
import { AddyIoForwarder } from "./addy-io";
|
import { AddyIoForwarder, DefaultAddyIoOptions } from "./addy-io";
|
||||||
import { mockApiService, mockI18nService } from "./mocks.jest";
|
import { mockApiService, mockI18nService } from "./mocks.jest";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Addy.io Forwarder", () => {
|
describe("Addy.io Forwarder", () => {
|
||||||
it("key returns the Addy IO forwarder key", () => {
|
it("key returns the Addy IO forwarder key", () => {
|
||||||
const forwarder = new AddyIoForwarder(null, null, null, null, null);
|
const forwarder = new AddyIoForwarder(null, null, null, null, null);
|
||||||
@ -15,6 +20,16 @@ describe("Addy.io Forwarder", () => {
|
|||||||
expect(forwarder.key).toBe(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)", () => {
|
describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => {
|
||||||
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
||||||
const apiService = mockApiService(200, {});
|
const apiService = mockApiService(200, {});
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { ADDY_IO_FORWARDER } from "../../key-definitions";
|
import { ADDY_IO_FORWARDER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { EmailDomainOptions, SelfHostedApiOptions } from "../options/forwarder-options";
|
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) */
|
/** Generates a forwarding address for addy.io (formerly anon addy) */
|
||||||
export class AddyIoForwarder extends ForwarderGeneratorStrategy<
|
export class AddyIoForwarder extends ForwarderGeneratorStrategy<
|
||||||
SelfHostedApiOptions & EmailDomainOptions
|
SelfHostedApiOptions & EmailDomainOptions
|
||||||
@ -34,6 +44,11 @@ export class AddyIoForwarder extends ForwarderGeneratorStrategy<
|
|||||||
return ADDY_IO_FORWARDER;
|
return ADDY_IO_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||||
|
defaults$ = (userId: UserId) => {
|
||||||
|
return new BehaviorSubject({ ...DefaultAddyIoOptions });
|
||||||
|
};
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||||
generate = async (options: SelfHostedApiOptions & EmailDomainOptions) => {
|
generate = async (options: SelfHostedApiOptions & EmailDomainOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
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: "",
|
||||||
|
});
|
||||||
|
@ -2,12 +2,17 @@
|
|||||||
* include Request in test environment.
|
* include Request in test environment.
|
||||||
* @jest-environment ../../../../shared/test.environment.ts
|
* @jest-environment ../../../../shared/test.environment.ts
|
||||||
*/
|
*/
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions";
|
import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
|
|
||||||
import { DuckDuckGoForwarder } from "./duck-duck-go";
|
import { DuckDuckGoForwarder, DefaultDuckDuckGoOptions } from "./duck-duck-go";
|
||||||
import { mockApiService, mockI18nService } from "./mocks.jest";
|
import { mockApiService, mockI18nService } from "./mocks.jest";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("DuckDuckGo Forwarder", () => {
|
describe("DuckDuckGo Forwarder", () => {
|
||||||
it("key returns the Duck Duck Go forwarder key", () => {
|
it("key returns the Duck Duck Go forwarder key", () => {
|
||||||
const forwarder = new DuckDuckGoForwarder(null, null, null, null, null);
|
const forwarder = new DuckDuckGoForwarder(null, null, null, null, null);
|
||||||
@ -15,6 +20,16 @@ describe("DuckDuckGo Forwarder", () => {
|
|||||||
expect(forwarder.key).toBe(DUCK_DUCK_GO_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)", () => {
|
describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => {
|
||||||
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
||||||
const apiService = mockApiService(200, {});
|
const apiService = mockApiService(200, {});
|
||||||
|
@ -1,13 +1,21 @@
|
|||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions";
|
import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { ApiOptions } from "../options/forwarder-options";
|
import { ApiOptions } from "../options/forwarder-options";
|
||||||
|
|
||||||
|
export const DefaultDuckDuckGoOptions: ApiOptions = Object.freeze({
|
||||||
|
website: null,
|
||||||
|
token: "",
|
||||||
|
});
|
||||||
|
|
||||||
/** Generates a forwarding address for DuckDuckGo */
|
/** Generates a forwarding address for DuckDuckGo */
|
||||||
export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
||||||
/** Instantiates the forwarder
|
/** Instantiates the forwarder
|
||||||
@ -32,6 +40,11 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions>
|
|||||||
return DUCK_DUCK_GO_FORWARDER;
|
return DUCK_DUCK_GO_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||||
|
defaults$ = (userId: UserId) => {
|
||||||
|
return new BehaviorSubject({ ...DefaultDuckDuckGoOptions });
|
||||||
|
};
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||||
generate = async (options: ApiOptions): Promise<string> => {
|
generate = async (options: ApiOptions): Promise<string> => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
@ -68,3 +81,8 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DefaultOptions = Object.freeze({
|
||||||
|
website: null,
|
||||||
|
token: "",
|
||||||
|
});
|
||||||
|
@ -2,13 +2,18 @@
|
|||||||
* include Request in test environment.
|
* include Request in test environment.
|
||||||
* @jest-environment ../../../../shared/test.environment.ts
|
* @jest-environment ../../../../shared/test.environment.ts
|
||||||
*/
|
*/
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { FASTMAIL_FORWARDER } from "../../key-definitions";
|
import { FASTMAIL_FORWARDER } from "../../key-definitions";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
|
|
||||||
import { FastmailForwarder } from "./fastmail";
|
import { FastmailForwarder, DefaultFastmailOptions } from "./fastmail";
|
||||||
import { mockI18nService } from "./mocks.jest";
|
import { mockI18nService } from "./mocks.jest";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
type MockResponse = { status: number; body: any };
|
type MockResponse = { status: number; body: any };
|
||||||
|
|
||||||
// fastmail calls nativeFetch first to resolve the accountId,
|
// fastmail calls nativeFetch first to resolve the accountId,
|
||||||
@ -52,6 +57,16 @@ describe("Fastmail Forwarder", () => {
|
|||||||
expect(forwarder.key).toBe(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)", () => {
|
describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => {
|
||||||
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
||||||
const apiService = mockApiService(AccountIdSuccess, EmptyResponse);
|
const apiService = mockApiService(AccountIdSuccess, EmptyResponse);
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { FASTMAIL_FORWARDER } from "../../key-definitions";
|
import { FASTMAIL_FORWARDER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { EmailPrefixOptions, ApiOptions } from "../options/forwarder-options";
|
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 */
|
/** Generates a forwarding address for Fastmail */
|
||||||
export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & EmailPrefixOptions> {
|
export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & EmailPrefixOptions> {
|
||||||
/** Instantiates the forwarder
|
/** Instantiates the forwarder
|
||||||
@ -32,6 +42,11 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & E
|
|||||||
return FASTMAIL_FORWARDER;
|
return FASTMAIL_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||||
|
defaults$ = (userId: UserId) => {
|
||||||
|
return new BehaviorSubject({ ...DefaultFastmailOptions });
|
||||||
|
};
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||||
generate = async (options: ApiOptions & EmailPrefixOptions) => {
|
generate = async (options: ApiOptions & EmailPrefixOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
@ -141,3 +156,10 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & E
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DefaultOptions = Object.freeze({
|
||||||
|
website: null,
|
||||||
|
domain: "",
|
||||||
|
prefix: "",
|
||||||
|
token: "",
|
||||||
|
});
|
||||||
|
@ -2,12 +2,17 @@
|
|||||||
* include Request in test environment.
|
* include Request in test environment.
|
||||||
* @jest-environment ../../../../shared/test.environment.ts
|
* @jest-environment ../../../../shared/test.environment.ts
|
||||||
*/
|
*/
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { FIREFOX_RELAY_FORWARDER } from "../../key-definitions";
|
import { FIREFOX_RELAY_FORWARDER } from "../../key-definitions";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
|
|
||||||
import { FirefoxRelayForwarder } from "./firefox-relay";
|
import { FirefoxRelayForwarder, DefaultFirefoxRelayOptions } from "./firefox-relay";
|
||||||
import { mockApiService, mockI18nService } from "./mocks.jest";
|
import { mockApiService, mockI18nService } from "./mocks.jest";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Firefox Relay Forwarder", () => {
|
describe("Firefox Relay Forwarder", () => {
|
||||||
it("key returns the Firefox Relay forwarder key", () => {
|
it("key returns the Firefox Relay forwarder key", () => {
|
||||||
const forwarder = new FirefoxRelayForwarder(null, null, null, null, null);
|
const forwarder = new FirefoxRelayForwarder(null, null, null, null, null);
|
||||||
@ -15,6 +20,16 @@ describe("Firefox Relay Forwarder", () => {
|
|||||||
expect(forwarder.key).toBe(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)", () => {
|
describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => {
|
||||||
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
||||||
const apiService = mockApiService(200, {});
|
const apiService = mockApiService(200, {});
|
||||||
|
@ -1,13 +1,21 @@
|
|||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { FIREFOX_RELAY_FORWARDER } from "../../key-definitions";
|
import { FIREFOX_RELAY_FORWARDER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { ApiOptions } from "../options/forwarder-options";
|
import { ApiOptions } from "../options/forwarder-options";
|
||||||
|
|
||||||
|
export const DefaultFirefoxRelayOptions: ApiOptions = Object.freeze({
|
||||||
|
website: null,
|
||||||
|
token: "",
|
||||||
|
});
|
||||||
|
|
||||||
/** Generates a forwarding address for Firefox Relay */
|
/** Generates a forwarding address for Firefox Relay */
|
||||||
export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
||||||
/** Instantiates the forwarder
|
/** Instantiates the forwarder
|
||||||
@ -32,6 +40,11 @@ export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy<ApiOptions
|
|||||||
return FIREFOX_RELAY_FORWARDER;
|
return FIREFOX_RELAY_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||||
|
defaults$ = (userId: UserId) => {
|
||||||
|
return new BehaviorSubject({ ...DefaultFirefoxRelayOptions });
|
||||||
|
};
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||||
generate = async (options: ApiOptions) => {
|
generate = async (options: ApiOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
@ -75,3 +88,8 @@ export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy<ApiOptions
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DefaultOptions = Object.freeze({
|
||||||
|
website: null,
|
||||||
|
token: "",
|
||||||
|
});
|
||||||
|
@ -2,12 +2,17 @@
|
|||||||
* include Request in test environment.
|
* include Request in test environment.
|
||||||
* @jest-environment ../../../../shared/test.environment.ts
|
* @jest-environment ../../../../shared/test.environment.ts
|
||||||
*/
|
*/
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { FORWARD_EMAIL_FORWARDER } from "../../key-definitions";
|
import { FORWARD_EMAIL_FORWARDER } from "../../key-definitions";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
|
|
||||||
import { ForwardEmailForwarder } from "./forward-email";
|
import { ForwardEmailForwarder, DefaultForwardEmailOptions } from "./forward-email";
|
||||||
import { mockApiService, mockI18nService } from "./mocks.jest";
|
import { mockApiService, mockI18nService } from "./mocks.jest";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("ForwardEmail Forwarder", () => {
|
describe("ForwardEmail Forwarder", () => {
|
||||||
it("key returns the Forward Email forwarder key", () => {
|
it("key returns the Forward Email forwarder key", () => {
|
||||||
const forwarder = new ForwardEmailForwarder(null, null, null, null, null);
|
const forwarder = new ForwardEmailForwarder(null, null, null, null, null);
|
||||||
@ -15,6 +20,16 @@ describe("ForwardEmail Forwarder", () => {
|
|||||||
expect(forwarder.key).toBe(FORWARD_EMAIL_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)", () => {
|
describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => {
|
||||||
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
||||||
const apiService = mockApiService(200, {});
|
const apiService = mockApiService(200, {});
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { Utils } from "../../../../platform/misc/utils";
|
import { Utils } from "../../../../platform/misc/utils";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { FORWARD_EMAIL_FORWARDER } from "../../key-definitions";
|
import { FORWARD_EMAIL_FORWARDER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { EmailDomainOptions, ApiOptions } from "../options/forwarder-options";
|
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 */
|
/** Generates a forwarding address for Forward Email */
|
||||||
export class ForwardEmailForwarder extends ForwarderGeneratorStrategy<
|
export class ForwardEmailForwarder extends ForwarderGeneratorStrategy<
|
||||||
ApiOptions & EmailDomainOptions
|
ApiOptions & EmailDomainOptions
|
||||||
@ -35,6 +44,11 @@ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy<
|
|||||||
return FORWARD_EMAIL_FORWARDER;
|
return FORWARD_EMAIL_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||||
|
defaults$ = (userId: UserId) => {
|
||||||
|
return new BehaviorSubject({ ...DefaultForwardEmailOptions });
|
||||||
|
};
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||||
generate = async (options: ApiOptions & EmailDomainOptions) => {
|
generate = async (options: ApiOptions & EmailDomainOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
@ -96,3 +110,9 @@ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DefaultOptions = Object.freeze({
|
||||||
|
website: null,
|
||||||
|
token: "",
|
||||||
|
domain: "",
|
||||||
|
});
|
||||||
|
@ -2,11 +2,16 @@
|
|||||||
* include Request in test environment.
|
* include Request in test environment.
|
||||||
* @jest-environment ../../../../shared/test.environment.ts
|
* @jest-environment ../../../../shared/test.environment.ts
|
||||||
*/
|
*/
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions";
|
import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
|
|
||||||
import { mockApiService, mockI18nService } from "./mocks.jest";
|
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", () => {
|
describe("SimpleLogin Forwarder", () => {
|
||||||
it("key returns the Simple Login forwarder key", () => {
|
it("key returns the Simple Login forwarder key", () => {
|
||||||
@ -15,6 +20,16 @@ describe("SimpleLogin Forwarder", () => {
|
|||||||
expect(forwarder.key).toBe(SIMPLE_LOGIN_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)", () => {
|
describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => {
|
||||||
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
|
||||||
const apiService = mockApiService(200, {});
|
const apiService = mockApiService(200, {});
|
||||||
|
@ -1,13 +1,22 @@
|
|||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
|
import { UserId } from "../../../../types/guid";
|
||||||
import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions";
|
import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { SelfHostedApiOptions } from "../options/forwarder-options";
|
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 */
|
/** Generates a forwarding address for Simple Login */
|
||||||
export class SimpleLoginForwarder extends ForwarderGeneratorStrategy<SelfHostedApiOptions> {
|
export class SimpleLoginForwarder extends ForwarderGeneratorStrategy<SelfHostedApiOptions> {
|
||||||
/** Instantiates the forwarder
|
/** Instantiates the forwarder
|
||||||
@ -32,6 +41,11 @@ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy<SelfHostedA
|
|||||||
return SIMPLE_LOGIN_FORWARDER;
|
return SIMPLE_LOGIN_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||||
|
defaults$ = (userId: UserId) => {
|
||||||
|
return new BehaviorSubject({ ...DefaultSimpleLoginOptions });
|
||||||
|
};
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||||
generate = async (options: SelfHostedApiOptions) => {
|
generate = async (options: SelfHostedApiOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
@ -80,3 +94,9 @@ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy<SelfHostedA
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DefaultOptions = Object.freeze({
|
||||||
|
website: null,
|
||||||
|
baseUrl: "https://app.simplelogin.io",
|
||||||
|
token: "",
|
||||||
|
});
|
||||||
|
@ -2,5 +2,5 @@ export { EffUsernameGeneratorStrategy } from "./eff-username-generator-strategy"
|
|||||||
export { CatchallGeneratorStrategy } from "./catchall-generator-strategy";
|
export { CatchallGeneratorStrategy } from "./catchall-generator-strategy";
|
||||||
export { SubaddressGeneratorStrategy } from "./subaddress-generator-strategy";
|
export { SubaddressGeneratorStrategy } from "./subaddress-generator-strategy";
|
||||||
export { UsernameGeneratorOptions } from "./username-generation-options";
|
export { UsernameGeneratorOptions } from "./username-generation-options";
|
||||||
export { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";
|
export { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
||||||
export { UsernameGenerationService } from "./username-generation.service";
|
export { UsernameGenerationService } from "./username-generation.service";
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { ForwarderMetadata } from "./forwarder-options";
|
import { ForwarderMetadata } from "./forwarder-options";
|
||||||
import { UsernameGeneratorOptions } from "./generator-options";
|
|
||||||
|
|
||||||
/** Metadata about an email forwarding service.
|
/** Metadata about an email forwarding service.
|
||||||
* @remarks This is used to populate the forwarder selection list
|
* @remarks This is used to populate the forwarder selection list
|
||||||
@ -48,71 +47,3 @@ export const Forwarders = Object.freeze({
|
|||||||
validForSelfHosted: true,
|
validForSelfHosted: true,
|
||||||
} as ForwarderMetadata),
|
} as ForwarderMetadata),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Padding values used to prevent leaking the length of the encrypted options. */
|
|
||||||
export const SecretPadding = Object.freeze({
|
|
||||||
/** The length to pad out encrypted members. This should be at least as long
|
|
||||||
* as the JSON content for the longest JSON payload being encrypted.
|
|
||||||
*/
|
|
||||||
length: 512,
|
|
||||||
|
|
||||||
/** The character to use for padding. */
|
|
||||||
character: "0",
|
|
||||||
|
|
||||||
/** A regular expression for detecting invalid padding. When the character
|
|
||||||
* changes, this should be updated to include the new padding pattern.
|
|
||||||
*/
|
|
||||||
hasInvalidPadding: /[^0]/,
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Default options for username generation. */
|
|
||||||
// freeze all the things to prevent mutation
|
|
||||||
export const DefaultOptions: UsernameGeneratorOptions = Object.freeze({
|
|
||||||
type: "word",
|
|
||||||
website: "",
|
|
||||||
word: Object.freeze({
|
|
||||||
capitalize: true,
|
|
||||||
includeNumber: true,
|
|
||||||
}),
|
|
||||||
subaddress: Object.freeze({
|
|
||||||
algorithm: "random",
|
|
||||||
email: "",
|
|
||||||
}),
|
|
||||||
catchall: Object.freeze({
|
|
||||||
algorithm: "random",
|
|
||||||
domain: "",
|
|
||||||
}),
|
|
||||||
forwarders: Object.freeze({
|
|
||||||
service: Forwarders.Fastmail.id,
|
|
||||||
fastMail: Object.freeze({
|
|
||||||
website: null,
|
|
||||||
domain: "",
|
|
||||||
prefix: "",
|
|
||||||
token: "",
|
|
||||||
}),
|
|
||||||
addyIo: Object.freeze({
|
|
||||||
website: null,
|
|
||||||
baseUrl: "https://app.addy.io",
|
|
||||||
domain: "",
|
|
||||||
token: "",
|
|
||||||
}),
|
|
||||||
forwardEmail: Object.freeze({
|
|
||||||
website: null,
|
|
||||||
token: "",
|
|
||||||
domain: "",
|
|
||||||
}),
|
|
||||||
simpleLogin: Object.freeze({
|
|
||||||
website: null,
|
|
||||||
baseUrl: "https://app.simplelogin.io",
|
|
||||||
token: "",
|
|
||||||
}),
|
|
||||||
duckDuckGo: Object.freeze({
|
|
||||||
website: null,
|
|
||||||
token: "",
|
|
||||||
}),
|
|
||||||
firefoxRelay: Object.freeze({
|
|
||||||
website: null,
|
|
||||||
token: "",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
@ -1,98 +1,13 @@
|
|||||||
import {
|
/** ways you can generate usernames
|
||||||
ApiOptions,
|
* "word" generates a username from the eff word list
|
||||||
EmailDomainOptions,
|
* "subaddress" creates a subaddress of an email.
|
||||||
EmailPrefixOptions,
|
* "catchall" uses a domain's catchall address
|
||||||
ForwarderId,
|
* "forwarded" uses an email forwarding service
|
||||||
SelfHostedApiOptions,
|
|
||||||
} from "./forwarder-options";
|
|
||||||
|
|
||||||
/** Configuration for username generation algorithms. */
|
|
||||||
export type AlgorithmOptions = {
|
|
||||||
/** selects the generation algorithm for the username.
|
|
||||||
* "random" generates a random string.
|
|
||||||
* "website-name" generates a username based on the website's name.
|
|
||||||
*/
|
*/
|
||||||
algorithm: "random" | "website-name";
|
export type UsernameGeneratorType = "word" | "subaddress" | "catchall" | "forwarded";
|
||||||
};
|
|
||||||
|
|
||||||
/** Identifies encrypted options that could have leaked from the configuration. */
|
/** Several username generators support two generation modes
|
||||||
export type MaybeLeakedOptions = {
|
* "random" selects one or more random words from the EFF word list
|
||||||
/** When true, encrypted options were previously stored as plaintext.
|
* "website-name" includes the domain in the generated username
|
||||||
* @remarks This is used to alert the user that the token should be
|
|
||||||
* regenerated. If a token has always been stored encrypted,
|
|
||||||
* this should be omitted.
|
|
||||||
*/
|
*/
|
||||||
wasPlainText?: true;
|
export type UsernameGenerationMode = "random" | "website-name";
|
||||||
};
|
|
||||||
|
|
||||||
/** Options for generating a username.
|
|
||||||
* @remarks This type includes all fields so that the generator
|
|
||||||
* remembers the user's configuration for each type of username
|
|
||||||
* and forwarder.
|
|
||||||
*/
|
|
||||||
export type UsernameGeneratorOptions = {
|
|
||||||
/** selects the property group used for username generation */
|
|
||||||
type?: "word" | "subaddress" | "catchall" | "forwarded";
|
|
||||||
|
|
||||||
/** When generating a forwarding address for a vault item, this should contain
|
|
||||||
* the domain the vault item supplies to the generator.
|
|
||||||
* @example If the user is creating a vault item for `https://www.domain.io/login`,
|
|
||||||
* then this should be `www.domain.io`.
|
|
||||||
*/
|
|
||||||
website?: string;
|
|
||||||
|
|
||||||
/** When true, the username generator saves options immediately
|
|
||||||
* after they're loaded. Otherwise this option should not be defined.
|
|
||||||
* */
|
|
||||||
saveOnLoad?: true;
|
|
||||||
|
|
||||||
/* Configures generation of a username from the EFF word list */
|
|
||||||
word: {
|
|
||||||
/** when true, the word is capitalized */
|
|
||||||
capitalize?: boolean;
|
|
||||||
|
|
||||||
/** when true, a random number is appended to the username */
|
|
||||||
includeNumber?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Configures generation of 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`.
|
|
||||||
*/
|
|
||||||
subaddress: AlgorithmOptions & {
|
|
||||||
/** the email address the subaddress is applied to. */
|
|
||||||
email?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Configures generation for a domain catch-all address.
|
|
||||||
*/
|
|
||||||
catchall: AlgorithmOptions & EmailDomainOptions;
|
|
||||||
|
|
||||||
/** Configures generation for an email forwarding service address.
|
|
||||||
*/
|
|
||||||
forwarders: {
|
|
||||||
/** The service to use for email forwarding.
|
|
||||||
* @remarks This determines which forwarder-specific options to use.
|
|
||||||
*/
|
|
||||||
service?: ForwarderId;
|
|
||||||
|
|
||||||
/** {@link Forwarders.AddyIo} */
|
|
||||||
addyIo: SelfHostedApiOptions & EmailDomainOptions & MaybeLeakedOptions;
|
|
||||||
|
|
||||||
/** {@link Forwarders.DuckDuckGo} */
|
|
||||||
duckDuckGo: ApiOptions & MaybeLeakedOptions;
|
|
||||||
|
|
||||||
/** {@link Forwarders.FastMail} */
|
|
||||||
fastMail: ApiOptions & EmailPrefixOptions & MaybeLeakedOptions;
|
|
||||||
|
|
||||||
/** {@link Forwarders.FireFoxRelay} */
|
|
||||||
firefoxRelay: ApiOptions & MaybeLeakedOptions;
|
|
||||||
|
|
||||||
/** {@link Forwarders.ForwardEmail} */
|
|
||||||
forwardEmail: ApiOptions & EmailDomainOptions & MaybeLeakedOptions;
|
|
||||||
|
|
||||||
/** {@link forwarders.SimpleLogin} */
|
|
||||||
simpleLogin: SelfHostedApiOptions & MaybeLeakedOptions;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -1,3 +1 @@
|
|||||||
export { UsernameGeneratorOptions } from "./generator-options";
|
|
||||||
export { DefaultOptions } from "./constants";
|
|
||||||
export { ForwarderId, ForwarderMetadata } from "./forwarder-options";
|
export { ForwarderId, ForwarderMetadata } from "./forwarder-options";
|
||||||
|
@ -1,243 +0,0 @@
|
|||||||
/**
|
|
||||||
* include structuredClone in test environment.
|
|
||||||
* @jest-environment ../../../../shared/test.environment.ts
|
|
||||||
*/
|
|
||||||
import { DefaultOptions, Forwarders } from "./constants";
|
|
||||||
import { UsernameGeneratorOptions } from "./generator-options";
|
|
||||||
import { getForwarderOptions, falsyDefault, forAllForwarders } from "./utilities";
|
|
||||||
|
|
||||||
const TestOptions: UsernameGeneratorOptions = {
|
|
||||||
type: "word",
|
|
||||||
website: "example.com",
|
|
||||||
word: {
|
|
||||||
capitalize: true,
|
|
||||||
includeNumber: true,
|
|
||||||
},
|
|
||||||
subaddress: {
|
|
||||||
algorithm: "random",
|
|
||||||
email: "foo@example.com",
|
|
||||||
},
|
|
||||||
catchall: {
|
|
||||||
algorithm: "random",
|
|
||||||
domain: "example.com",
|
|
||||||
},
|
|
||||||
forwarders: {
|
|
||||||
service: Forwarders.Fastmail.id,
|
|
||||||
fastMail: {
|
|
||||||
website: null,
|
|
||||||
domain: "httpbin.com",
|
|
||||||
prefix: "foo",
|
|
||||||
token: "some-token",
|
|
||||||
},
|
|
||||||
addyIo: {
|
|
||||||
website: null,
|
|
||||||
baseUrl: "https://app.addy.io",
|
|
||||||
domain: "example.com",
|
|
||||||
token: "some-token",
|
|
||||||
},
|
|
||||||
forwardEmail: {
|
|
||||||
website: null,
|
|
||||||
token: "some-token",
|
|
||||||
domain: "example.com",
|
|
||||||
},
|
|
||||||
simpleLogin: {
|
|
||||||
website: null,
|
|
||||||
baseUrl: "https://app.simplelogin.io",
|
|
||||||
token: "some-token",
|
|
||||||
},
|
|
||||||
duckDuckGo: {
|
|
||||||
website: null,
|
|
||||||
token: "some-token",
|
|
||||||
},
|
|
||||||
firefoxRelay: {
|
|
||||||
website: null,
|
|
||||||
token: "some-token",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("Username Generation Options", () => {
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -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<T>(
|
|
||||||
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<T>(value: T, defaults: Partial<T>): 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;
|
|
||||||
}
|
|
@ -1,10 +1,18 @@
|
|||||||
|
import { RequestOptions } from "./options/forwarder-options";
|
||||||
|
import { UsernameGenerationMode } from "./options/generator-options";
|
||||||
|
|
||||||
/** Settings supported when generating an email subaddress */
|
/** Settings supported when generating an email subaddress */
|
||||||
export type SubaddressGenerationOptions = {
|
export type SubaddressGenerationOptions = {
|
||||||
type?: "random" | "website-name";
|
/** selects the generation algorithm for the catchall email address. */
|
||||||
email?: string;
|
subaddressType?: UsernameGenerationMode;
|
||||||
};
|
|
||||||
|
/** the email address the subaddress is applied to. */
|
||||||
|
subaddressEmail?: string;
|
||||||
|
} & RequestOptions;
|
||||||
|
|
||||||
/** The default options for email subaddress generation. */
|
/** The default options for email subaddress generation. */
|
||||||
export const DefaultSubaddressOptions: Partial<SubaddressGenerationOptions> = Object.freeze({
|
export const DefaultSubaddressOptions: SubaddressGenerationOptions = Object.freeze({
|
||||||
type: "random",
|
subaddressType: "random",
|
||||||
|
subaddressEmail: "",
|
||||||
|
website: null,
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,11 @@ import { UserId } from "../../../types/guid";
|
|||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DefaultSubaddressOptions,
|
||||||
|
SubaddressGenerationOptions,
|
||||||
|
} from "./subaddress-generator-options";
|
||||||
|
|
||||||
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
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", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
@ -70,16 +85,14 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
type: "website-name" as const,
|
subaddressType: "website-name",
|
||||||
email: "someone@example.com",
|
subaddressEmail: "someone@example.com",
|
||||||
};
|
website: "foo.com",
|
||||||
|
} as SubaddressGenerationOptions;
|
||||||
|
|
||||||
await strategy.generate(options);
|
await strategy.generate(options);
|
||||||
|
|
||||||
expect(legacy.generateSubaddress).toHaveBeenCalledWith({
|
expect(legacy.generateSubaddress).toHaveBeenCalledWith(options);
|
||||||
subaddressType: "website-name" as const,
|
|
||||||
subaddressEmail: "someone@example.com",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
import { map, pipe } from "rxjs";
|
import { BehaviorSubject, map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
|
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||||
import { NoPolicy } from "../no-policy";
|
import { NoPolicy } from "../no-policy";
|
||||||
|
|
||||||
import { SubaddressGenerationOptions } from "./subaddress-generator-options";
|
import {
|
||||||
import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";
|
DefaultSubaddressOptions,
|
||||||
|
SubaddressGenerationOptions,
|
||||||
|
} from "./subaddress-generator-options";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
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
|
export class SubaddressGeneratorStrategy
|
||||||
implements GeneratorStrategy<SubaddressGenerationOptions, NoPolicy>
|
implements GeneratorStrategy<SubaddressGenerationOptions, NoPolicy>
|
||||||
{
|
{
|
||||||
@ -30,6 +37,11 @@ export class SubaddressGeneratorStrategy
|
|||||||
return this.stateProvider.getUser(id, SUBADDRESS_SETTINGS);
|
return this.stateProvider.getUser(id, SUBADDRESS_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link GeneratorStrategy.defaults$} */
|
||||||
|
defaults$(userId: UserId) {
|
||||||
|
return new BehaviorSubject({ ...DefaultSubaddressOptions }).asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
get policy() {
|
get policy() {
|
||||||
// Uses password generator since there aren't policies
|
// Uses password generator since there aren't policies
|
||||||
@ -49,9 +61,6 @@ export class SubaddressGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.generate} */
|
/** {@link GeneratorStrategy.generate} */
|
||||||
generate(options: SubaddressGenerationOptions) {
|
generate(options: SubaddressGenerationOptions) {
|
||||||
return this.usernameService.generateSubaddress({
|
return this.usernameService.generateSubaddress(options);
|
||||||
subaddressEmail: options.email,
|
|
||||||
subaddressType: options.type,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
|
import { CatchallGenerationOptions } from "./catchall-generator-options";
|
||||||
import { EffUsernameGenerationOptions } from "./eff-username-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 & {
|
export type UsernameGeneratorOptions = EffUsernameGenerationOptions &
|
||||||
type?: "word" | "subaddress" | "catchall" | "forwarded";
|
SubaddressGenerationOptions &
|
||||||
subaddressType?: "random" | "website-name";
|
CatchallGenerationOptions &
|
||||||
subaddressEmail?: string;
|
RequestOptions & {
|
||||||
catchallType?: "random" | "website-name";
|
type?: UsernameGeneratorType;
|
||||||
catchallDomain?: string;
|
forwardedService?: ForwarderId | "";
|
||||||
website?: string;
|
|
||||||
forwardedService?: string;
|
|
||||||
forwardedAnonAddyApiToken?: string;
|
forwardedAnonAddyApiToken?: string;
|
||||||
forwardedAnonAddyDomain?: string;
|
forwardedAnonAddyDomain?: string;
|
||||||
forwardedAnonAddyBaseUrl?: string;
|
forwardedAnonAddyBaseUrl?: string;
|
||||||
@ -18,4 +20,4 @@ export type UsernameGeneratorOptions = EffUsernameGenerationOptions & {
|
|||||||
forwardedForwardEmailDomain?: string;
|
forwardedForwardEmailDomain?: string;
|
||||||
forwardedSimpleLoginApiKey?: string;
|
forwardedSimpleLoginApiKey?: string;
|
||||||
forwardedSimpleLoginBaseUrl?: string;
|
forwardedSimpleLoginBaseUrl?: string;
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@ import { ApiService } from "../../../abstractions/api.service";
|
|||||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
import { StateService } from "../../../platform/abstractions/state.service";
|
import { StateService } from "../../../platform/abstractions/state.service";
|
||||||
import { EFFLongWordList } from "../../../platform/misc/wordlist";
|
import { EFFLongWordList } from "../../../platform/misc/wordlist";
|
||||||
|
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AnonAddyForwarder,
|
AnonAddyForwarder,
|
||||||
@ -14,10 +15,10 @@ import {
|
|||||||
SimpleLoginForwarder,
|
SimpleLoginForwarder,
|
||||||
} from "./email-forwarders";
|
} from "./email-forwarders";
|
||||||
import { UsernameGeneratorOptions } from "./username-generation-options";
|
import { UsernameGeneratorOptions } from "./username-generation-options";
|
||||||
import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";
|
|
||||||
|
|
||||||
const DefaultOptions: UsernameGeneratorOptions = {
|
const DefaultOptions: UsernameGeneratorOptions = {
|
||||||
type: "word",
|
type: "word",
|
||||||
|
website: null,
|
||||||
wordCapitalize: true,
|
wordCapitalize: true,
|
||||||
wordIncludeNumber: true,
|
wordIncludeNumber: true,
|
||||||
subaddressType: "random",
|
subaddressType: "random",
|
||||||
|
Loading…
Reference in New Issue
Block a user