21 changed files with 1355 additions and 12 deletions
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
import { CredentialAlgorithm, CredentialType } from "./type"; |
||||
|
||||
/** Credential generator metadata common across credential generators */ |
||||
export type AlgorithmMetadata = { |
||||
/** Uniquely identifies the credential configuration |
||||
* @example |
||||
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
|
||||
* // to pattern test whether the credential describes a forwarder algorithm
|
||||
* const meta : AlgorithmMetadata = // ...
|
||||
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; |
||||
*/ |
||||
id: CredentialAlgorithm; |
||||
|
||||
/** The kind of credential generated by this configuration */ |
||||
category: CredentialType; |
||||
|
||||
/** Used to order credential algorithms for display purposes. |
||||
* Items with lesser weights appear before entries with greater |
||||
* weights (i.e. ascending sort). |
||||
*/ |
||||
weight: number; |
||||
|
||||
/** Localization keys */ |
||||
i18nKeys: { |
||||
/** descriptive name of the algorithm */ |
||||
name: string; |
||||
|
||||
/** explanatory text for the algorithm */ |
||||
description?: string; |
||||
|
||||
/** labels the generate action */ |
||||
generateCredential: string; |
||||
|
||||
/** message informing users when the generator produces a new credential */ |
||||
credentialGenerated: string; |
||||
|
||||
/* labels the action that assigns a generated value to a domain object */ |
||||
useCredential: string; |
||||
|
||||
/** labels the generated output */ |
||||
credentialType: string; |
||||
|
||||
/** labels the copy output action */ |
||||
copyCredential: string; |
||||
}; |
||||
|
||||
/** fine-tunings for generator user experiences */ |
||||
capabilities: { |
||||
/** `true` when the generator supports autogeneration |
||||
* @remarks this property is useful when credential generation |
||||
* carries side effects, such as configuring a service external |
||||
* to Bitwarden. |
||||
*/ |
||||
autogenerate: boolean; |
||||
|
||||
/** Well-known fields to display on the options panel or collect from the environment. |
||||
* @remarks: at present, this is only used by forwarders |
||||
*/ |
||||
fields: string[]; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
import { deepFreeze } from "@bitwarden/common/tools/util"; |
||||
|
||||
/** algorithms for generating credentials */ |
||||
export const Algorithm = Object.freeze({ |
||||
/** A password composed of random characters */ |
||||
password: "password", |
||||
|
||||
/** A password composed of random words from the EFF word list */ |
||||
passphrase: "passphrase", |
||||
|
||||
/** A username composed of words from the EFF word list */ |
||||
username: "username", |
||||
|
||||
/** An email username composed of random characters */ |
||||
catchall: "catchall", |
||||
|
||||
/** An email username composed of words from the EFF word list */ |
||||
plusAddress: "subaddress", |
||||
} as const); |
||||
|
||||
/** categorizes credentials according to their use-case outside of Bitwarden */ |
||||
export const Type = Object.freeze({ |
||||
password: "password", |
||||
username: "username", |
||||
email: "email", |
||||
} as const); |
||||
|
||||
/** categorizes settings according to their expected use-case within Bitwarden */ |
||||
export const Profile = Object.freeze({ |
||||
/** account-level generator options. This is the default. |
||||
* @remarks these are the options displayed on the generator tab |
||||
*/ |
||||
account: "account", |
||||
|
||||
// FIXME: consider adding a profile for bitwarden's master password
|
||||
}); |
||||
|
||||
/** Credential generation algorithms grouped by purpose. */ |
||||
export const AlgorithmsByType = deepFreeze({ |
||||
/** Algorithms that produce passwords */ |
||||
[Type.password]: [Algorithm.password, Algorithm.passphrase] as const, |
||||
|
||||
/** Algorithms that produce usernames */ |
||||
[Type.username]: [Algorithm.username] as const, |
||||
|
||||
/** Algorithms that produce email addresses */ |
||||
[Type.email]: [Algorithm.catchall, Algorithm.plusAddress] as const, |
||||
} as const); |
||||
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
import { mock } from "jest-mock-extended"; |
||||
|
||||
import { EmailRandomizer } from "../../engine"; |
||||
import { CatchallConstraints } from "../../policies/catchall-constraints"; |
||||
import { CatchallGenerationOptions, GeneratorDependencyProvider } from "../../types"; |
||||
import { Profile } from "../data"; |
||||
import { CoreProfileMetadata } from "../profile-metadata"; |
||||
import { isCoreProfile } from "../util"; |
||||
|
||||
import catchall from "./catchall"; |
||||
|
||||
const dependencyProvider = mock<GeneratorDependencyProvider>(); |
||||
|
||||
describe("email - catchall generator metadata", () => { |
||||
describe("engine.create", () => { |
||||
it("returns an email randomizer", () => { |
||||
expect(catchall.engine.create(dependencyProvider)).toBeInstanceOf(EmailRandomizer); |
||||
}); |
||||
}); |
||||
|
||||
describe("profiles[account]", () => { |
||||
let accountProfile: CoreProfileMetadata<CatchallGenerationOptions> = null; |
||||
beforeEach(() => { |
||||
const profile = catchall.profiles[Profile.account]; |
||||
if (isCoreProfile(profile)) { |
||||
accountProfile = profile; |
||||
} |
||||
}); |
||||
|
||||
describe("storage.options.deserializer", () => { |
||||
it("returns its input", () => { |
||||
const value: CatchallGenerationOptions = { |
||||
catchallType: "random", |
||||
catchallDomain: "example.com", |
||||
}; |
||||
|
||||
const result = accountProfile.storage.options.deserializer(value); |
||||
|
||||
expect(result).toBe(value); |
||||
}); |
||||
}); |
||||
|
||||
describe("constraints.create", () => { |
||||
// these tests check that the wiring is correct by exercising the behavior
|
||||
// of functionality encapsulated by `create`. These methods may fail if the
|
||||
// enclosed behaviors change.
|
||||
|
||||
it("creates a catchall constraints", () => { |
||||
const context = { defaultConstraints: {} }; |
||||
|
||||
const constraints = accountProfile.constraints.create([], context); |
||||
|
||||
expect(constraints).toBeInstanceOf(CatchallConstraints); |
||||
}); |
||||
|
||||
it("extracts the domain from context.email", () => { |
||||
const context = { email: "foo@example.com", defaultConstraints: {} }; |
||||
|
||||
const constraints = accountProfile.constraints.create([], context) as CatchallConstraints; |
||||
|
||||
expect(constraints.domain).toEqual("example.com"); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; |
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; |
||||
import { deepFreeze } from "@bitwarden/common/tools/util"; |
||||
|
||||
import { EmailRandomizer } from "../../engine"; |
||||
import { CatchallConstraints } from "../../policies/catchall-constraints"; |
||||
import { |
||||
CatchallGenerationOptions, |
||||
CredentialGenerator, |
||||
GeneratorDependencyProvider, |
||||
} from "../../types"; |
||||
import { Algorithm, Type, Profile } from "../data"; |
||||
import { GeneratorMetadata } from "../generator-metadata"; |
||||
|
||||
const catchall: GeneratorMetadata<CatchallGenerationOptions> = deepFreeze({ |
||||
id: Algorithm.catchall, |
||||
category: Type.email, |
||||
weight: 210, |
||||
i18nKeys: { |
||||
name: "catchallEmail", |
||||
description: "catchallEmailDesc", |
||||
credentialType: "email", |
||||
generateCredential: "generateEmail", |
||||
credentialGenerated: "emailGenerated", |
||||
copyCredential: "copyEmail", |
||||
useCredential: "useThisEmail", |
||||
}, |
||||
capabilities: { |
||||
autogenerate: true, |
||||
fields: [], |
||||
}, |
||||
engine: { |
||||
create( |
||||
dependencies: GeneratorDependencyProvider, |
||||
): CredentialGenerator<CatchallGenerationOptions> { |
||||
return new EmailRandomizer(dependencies.randomizer); |
||||
}, |
||||
}, |
||||
profiles: { |
||||
[Profile.account]: { |
||||
type: "core", |
||||
storage: { |
||||
key: "catchallGeneratorSettings", |
||||
target: "object", |
||||
format: "plain", |
||||
classifier: new PublicClassifier<CatchallGenerationOptions>([ |
||||
"catchallType", |
||||
"catchallDomain", |
||||
]), |
||||
state: GENERATOR_DISK, |
||||
initial: { |
||||
catchallType: "random", |
||||
catchallDomain: "", |
||||
}, |
||||
options: { |
||||
deserializer: (value) => value, |
||||
clearOn: ["logout"], |
||||
}, |
||||
}, |
||||
constraints: { |
||||
default: { catchallDomain: { minLength: 1 } }, |
||||
create(_policies, context) { |
||||
return new CatchallConstraints(context.email ?? ""); |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
export default catchall; |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
// Forwarders are pending integration with the extension API
|
||||
//
|
||||
// They use the 300-block of weights and derive their metadata
|
||||
// using logic similar to `toCredentialGeneratorConfiguration`
|
||||
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
import { mock } from "jest-mock-extended"; |
||||
|
||||
import { EmailRandomizer } from "../../engine"; |
||||
import { SubaddressConstraints } from "../../policies/subaddress-constraints"; |
||||
import { SubaddressGenerationOptions, GeneratorDependencyProvider } from "../../types"; |
||||
import { Profile } from "../data"; |
||||
import { CoreProfileMetadata } from "../profile-metadata"; |
||||
import { isCoreProfile } from "../util"; |
||||
|
||||
import plusAddress from "./plus-address"; |
||||
|
||||
const dependencyProvider = mock<GeneratorDependencyProvider>(); |
||||
|
||||
describe("email - plus address generator metadata", () => { |
||||
describe("engine.create", () => { |
||||
it("returns an email randomizer", () => { |
||||
expect(plusAddress.engine.create(dependencyProvider)).toBeInstanceOf(EmailRandomizer); |
||||
}); |
||||
}); |
||||
|
||||
describe("profiles[account]", () => { |
||||
let accountProfile: CoreProfileMetadata<SubaddressGenerationOptions> = null; |
||||
beforeEach(() => { |
||||
const profile = plusAddress.profiles[Profile.account]; |
||||
if (isCoreProfile(profile)) { |
||||
accountProfile = profile; |
||||
} |
||||
}); |
||||
|
||||
describe("storage.options.deserializer", () => { |
||||
it("returns its input", () => { |
||||
const value: SubaddressGenerationOptions = { |
||||
subaddressType: "random", |
||||
subaddressEmail: "foo@example.com", |
||||
}; |
||||
|
||||
const result = accountProfile.storage.options.deserializer(value); |
||||
|
||||
expect(result).toBe(value); |
||||
}); |
||||
}); |
||||
|
||||
describe("constraints.create", () => { |
||||
// these tests check that the wiring is correct by exercising the behavior
|
||||
// of functionality encapsulated by `create`. These methods may fail if the
|
||||
// enclosed behaviors change.
|
||||
|
||||
it("creates a subaddress constraints", () => { |
||||
const context = { defaultConstraints: {} }; |
||||
|
||||
const constraints = accountProfile.constraints.create([], context); |
||||
|
||||
expect(constraints).toBeInstanceOf(SubaddressConstraints); |
||||
}); |
||||
|
||||
it("sets the constraint email to context.email", () => { |
||||
const context = { email: "bar@example.com", defaultConstraints: {} }; |
||||
|
||||
const constraints = accountProfile.constraints.create([], context) as SubaddressConstraints; |
||||
|
||||
expect(constraints.email).toEqual("bar@example.com"); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; |
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; |
||||
import { deepFreeze } from "@bitwarden/common/tools/util"; |
||||
|
||||
import { EmailRandomizer } from "../../engine"; |
||||
import { SubaddressConstraints } from "../../policies/subaddress-constraints"; |
||||
import { |
||||
CredentialGenerator, |
||||
GeneratorDependencyProvider, |
||||
SubaddressGenerationOptions, |
||||
} from "../../types"; |
||||
import { Algorithm, Profile, Type } from "../data"; |
||||
import { GeneratorMetadata } from "../generator-metadata"; |
||||
|
||||
const plusAddress: GeneratorMetadata<SubaddressGenerationOptions> = deepFreeze({ |
||||
id: Algorithm.plusAddress, |
||||
category: Type.email, |
||||
weight: 200, |
||||
i18nKeys: { |
||||
name: "plusAddressedEmail", |
||||
description: "plusAddressedEmailDesc", |
||||
credentialType: "email", |
||||
generateCredential: "generateEmail", |
||||
credentialGenerated: "emailGenerated", |
||||
copyCredential: "copyEmail", |
||||
useCredential: "useThisEmail", |
||||
}, |
||||
capabilities: { |
||||
autogenerate: true, |
||||
fields: [], |
||||
}, |
||||
engine: { |
||||
create( |
||||
dependencies: GeneratorDependencyProvider, |
||||
): CredentialGenerator<SubaddressGenerationOptions> { |
||||
return new EmailRandomizer(dependencies.randomizer); |
||||
}, |
||||
}, |
||||
profiles: { |
||||
[Profile.account]: { |
||||
type: "core", |
||||
storage: { |
||||
key: "subaddressGeneratorSettings", |
||||
target: "object", |
||||
format: "plain", |
||||
classifier: new PublicClassifier<SubaddressGenerationOptions>([ |
||||
"subaddressType", |
||||
"subaddressEmail", |
||||
]), |
||||
state: GENERATOR_DISK, |
||||
initial: { |
||||
subaddressType: "random", |
||||
subaddressEmail: "", |
||||
}, |
||||
options: { |
||||
deserializer(value) { |
||||
return value; |
||||
}, |
||||
clearOn: ["logout"], |
||||
}, |
||||
}, |
||||
constraints: { |
||||
default: {}, |
||||
create(_policy, context) { |
||||
return new SubaddressConstraints(context.email ?? ""); |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
export default plusAddress; |
||||
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
import { CredentialGenerator, GeneratorDependencyProvider } from "../types"; |
||||
|
||||
import { AlgorithmMetadata } from "./algorithm-metadata"; |
||||
import { Profile } from "./data"; |
||||
import { ProfileMetadata } from "./profile-metadata"; |
||||
|
||||
/** Extends the algorithm metadata with storage and engine configurations. |
||||
* @example |
||||
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
|
||||
* // to pattern test whether the credential describes a forwarder algorithm
|
||||
* const meta : CredentialGeneratorInfo = // ...
|
||||
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; |
||||
*/ |
||||
export type GeneratorMetadata<Options> = AlgorithmMetadata & { |
||||
/** An algorithm that generates credentials when ran. */ |
||||
engine: { |
||||
/** Factory for the generator |
||||
*/ |
||||
create: (randomizer: GeneratorDependencyProvider) => CredentialGenerator<Options>; |
||||
}; |
||||
|
||||
/** Defines parameters for credential generation */ |
||||
profiles: { |
||||
/** profiles supported by this generator; when `undefined`, |
||||
* the generator does not support the profile. |
||||
*/ |
||||
[K in keyof typeof Profile]?: ProfileMetadata<Options>; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
|
||||
import { mock } from "jest-mock-extended"; |
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums"; |
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; |
||||
|
||||
import { PasswordRandomizer } from "../../engine"; |
||||
import { PassphrasePolicyConstraints } from "../../policies"; |
||||
import { PassphraseGenerationOptions, GeneratorDependencyProvider } from "../../types"; |
||||
import { Profile } from "../data"; |
||||
import { CoreProfileMetadata } from "../profile-metadata"; |
||||
import { isCoreProfile } from "../util"; |
||||
|
||||
import effPassphrase from "./eff-word-list"; |
||||
|
||||
const dependencyProvider = mock<GeneratorDependencyProvider>(); |
||||
|
||||
describe("password - eff words generator metadata", () => { |
||||
describe("engine.create", () => { |
||||
it("returns an email randomizer", () => { |
||||
expect(effPassphrase.engine.create(dependencyProvider)).toBeInstanceOf(PasswordRandomizer); |
||||
}); |
||||
}); |
||||
|
||||
describe("profiles[account]", () => { |
||||
let accountProfile: CoreProfileMetadata<PassphraseGenerationOptions> = null; |
||||
beforeEach(() => { |
||||
const profile = effPassphrase.profiles[Profile.account]; |
||||
if (isCoreProfile(profile)) { |
||||
accountProfile = profile; |
||||
} |
||||
}); |
||||
|
||||
describe("storage.options.deserializer", () => { |
||||
it("returns its input", () => { |
||||
const value: PassphraseGenerationOptions = { ...accountProfile.storage.initial }; |
||||
|
||||
const result = accountProfile.storage.options.deserializer(value); |
||||
|
||||
expect(result).toBe(value); |
||||
}); |
||||
}); |
||||
|
||||
describe("constraints.create", () => { |
||||
// these tests check that the wiring is correct by exercising the behavior
|
||||
// of functionality encapsulated by `create`. These methods may fail if the
|
||||
// enclosed behaviors change.
|
||||
|
||||
it("creates a passphrase policy constraints", () => { |
||||
const context = { defaultConstraints: accountProfile.constraints.default }; |
||||
|
||||
const constraints = accountProfile.constraints.create([], context); |
||||
|
||||
expect(constraints).toBeInstanceOf(PassphrasePolicyConstraints); |
||||
}); |
||||
|
||||
it("forwards the policy to the constraints", () => { |
||||
const context = { defaultConstraints: accountProfile.constraints.default }; |
||||
const policies = [ |
||||
{ |
||||
type: PolicyType.PasswordGenerator, |
||||
data: { |
||||
minNumberWords: 6, |
||||
capitalize: false, |
||||
includeNumber: false, |
||||
}, |
||||
}, |
||||
] as Policy[]; |
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context); |
||||
|
||||
expect(constraints.constraints.numWords.min).toEqual(6); |
||||
}); |
||||
|
||||
it("combines multiple policies in the constraints", () => { |
||||
const context = { defaultConstraints: accountProfile.constraints.default }; |
||||
const policies = [ |
||||
{ |
||||
type: PolicyType.PasswordGenerator, |
||||
data: { |
||||
minNumberWords: 6, |
||||
capitalize: false, |
||||
includeNumber: false, |
||||
}, |
||||
}, |
||||
{ |
||||
type: PolicyType.PasswordGenerator, |
||||
data: { |
||||
minNumberWords: 3, |
||||
capitalize: true, |
||||
includeNumber: false, |
||||
}, |
||||
}, |
||||
] as Policy[]; |
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context); |
||||
|
||||
expect(constraints.constraints.numWords.min).toEqual(6); |
||||
expect(constraints.constraints.capitalize.requiredValue).toEqual(true); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums"; |
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; |
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; |
||||
import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; |
||||
|
||||
import { PasswordRandomizer } from "../../engine"; |
||||
import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies"; |
||||
import { |
||||
CredentialGenerator, |
||||
GeneratorDependencyProvider, |
||||
PassphraseGenerationOptions, |
||||
} from "../../types"; |
||||
import { Algorithm, Profile, Type } from "../data"; |
||||
import { GeneratorMetadata } from "../generator-metadata"; |
||||
|
||||
const passphrase: GeneratorMetadata<PassphraseGenerationOptions> = { |
||||
id: Algorithm.passphrase, |
||||
category: Type.password, |
||||
weight: 110, |
||||
i18nKeys: { |
||||
name: "passphrase", |
||||
credentialType: "passphrase", |
||||
generateCredential: "generatePassphrase", |
||||
credentialGenerated: "passphraseGenerated", |
||||
copyCredential: "copyPassphrase", |
||||
useCredential: "useThisPassphrase", |
||||
}, |
||||
capabilities: { |
||||
autogenerate: false, |
||||
fields: [], |
||||
}, |
||||
engine: { |
||||
create( |
||||
dependencies: GeneratorDependencyProvider, |
||||
): CredentialGenerator<PassphraseGenerationOptions> { |
||||
return new PasswordRandomizer(dependencies.randomizer); |
||||
}, |
||||
}, |
||||
profiles: { |
||||
[Profile.account]: { |
||||
type: "core", |
||||
storage: { |
||||
key: "passphraseGeneratorSettings", |
||||
target: "object", |
||||
format: "plain", |
||||
classifier: new PublicClassifier<PassphraseGenerationOptions>([ |
||||
"numWords", |
||||
"wordSeparator", |
||||
"capitalize", |
||||
"includeNumber", |
||||
]), |
||||
state: GENERATOR_DISK, |
||||
initial: { |
||||
numWords: 6, |
||||
wordSeparator: "-", |
||||
capitalize: false, |
||||
includeNumber: false, |
||||
}, |
||||
options: { |
||||
deserializer(value) { |
||||
return value; |
||||
}, |
||||
clearOn: ["logout"], |
||||
}, |
||||
} satisfies ObjectKey<PassphraseGenerationOptions>, |
||||
constraints: { |
||||
type: PolicyType.PasswordGenerator, |
||||
default: { |
||||
wordSeparator: { maxLength: 1 }, |
||||
numWords: { |
||||
min: 3, |
||||
max: 20, |
||||
recommendation: 6, |
||||
}, |
||||
}, |
||||
create(policies, context) { |
||||
const initial = { |
||||
minNumberWords: 0, |
||||
capitalize: false, |
||||
includeNumber: false, |
||||
}; |
||||
const policy = policies.reduce(passphraseLeastPrivilege, initial); |
||||
const constraints = new PassphrasePolicyConstraints(policy, context.defaultConstraints); |
||||
return constraints; |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}; |
||||
|
||||
export default passphrase; |
||||
@ -0,0 +1,105 @@
@@ -0,0 +1,105 @@
|
||||
import { mock } from "jest-mock-extended"; |
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums"; |
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; |
||||
|
||||
import { PasswordRandomizer } from "../../engine"; |
||||
import { DynamicPasswordPolicyConstraints } from "../../policies"; |
||||
import { PasswordGenerationOptions, GeneratorDependencyProvider } from "../../types"; |
||||
import { Profile } from "../data"; |
||||
import { CoreProfileMetadata } from "../profile-metadata"; |
||||
import { isCoreProfile } from "../util"; |
||||
|
||||
import password from "./random-password"; |
||||
|
||||
const dependencyProvider = mock<GeneratorDependencyProvider>(); |
||||
|
||||
describe("password - characters generator metadata", () => { |
||||
describe("engine.create", () => { |
||||
it("returns an email randomizer", () => { |
||||
expect(password.engine.create(dependencyProvider)).toBeInstanceOf(PasswordRandomizer); |
||||
}); |
||||
}); |
||||
|
||||
describe("profiles[account]", () => { |
||||
let accountProfile: CoreProfileMetadata<PasswordGenerationOptions> = null; |
||||
beforeEach(() => { |
||||
const profile = password.profiles[Profile.account]; |
||||
if (isCoreProfile(profile)) { |
||||
accountProfile = profile; |
||||
} |
||||
}); |
||||
|
||||
describe("storage.options.deserializer", () => { |
||||
it("returns its input", () => { |
||||
const value: PasswordGenerationOptions = { ...accountProfile.storage.initial }; |
||||
|
||||
const result = accountProfile.storage.options.deserializer(value); |
||||
|
||||
expect(result).toBe(value); |
||||
}); |
||||
}); |
||||
|
||||
describe("constraints.create", () => { |
||||
// these tests check that the wiring is correct by exercising the behavior
|
||||
// of functionality encapsulated by `create`. These methods may fail if the
|
||||
// enclosed behaviors change.
|
||||
|
||||
it("creates a passphrase policy constraints", () => { |
||||
const context = { defaultConstraints: accountProfile.constraints.default }; |
||||
|
||||
const constraints = accountProfile.constraints.create([], context); |
||||
|
||||
expect(constraints).toBeInstanceOf(DynamicPasswordPolicyConstraints); |
||||
}); |
||||
|
||||
it("forwards the policy to the constraints", () => { |
||||
const context = { defaultConstraints: accountProfile.constraints.default }; |
||||
const policies = [ |
||||
{ |
||||
type: PolicyType.PasswordGenerator, |
||||
enabled: true, |
||||
data: { |
||||
minLength: 10, |
||||
capitalize: false, |
||||
useNumbers: false, |
||||
}, |
||||
}, |
||||
] as Policy[]; |
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context); |
||||
|
||||
expect(constraints.constraints.length.min).toEqual(10); |
||||
}); |
||||
|
||||
it("combines multiple policies in the constraints", () => { |
||||
const context = { defaultConstraints: accountProfile.constraints.default }; |
||||
const policies = [ |
||||
{ |
||||
type: PolicyType.PasswordGenerator, |
||||
enabled: true, |
||||
data: { |
||||
minLength: 14, |
||||
useSpecial: false, |
||||
useNumbers: false, |
||||
}, |
||||
}, |
||||
{ |
||||
type: PolicyType.PasswordGenerator, |
||||
enabled: true, |
||||
data: { |
||||
minLength: 10, |
||||
useSpecial: true, |
||||
includeNumber: false, |
||||
}, |
||||
}, |
||||
] as Policy[]; |
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context); |
||||
|
||||
expect(constraints.constraints.length.min).toEqual(14); |
||||
expect(constraints.constraints.special.requiredValue).toEqual(true); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,117 @@
@@ -0,0 +1,117 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums"; |
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; |
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; |
||||
import { deepFreeze } from "@bitwarden/common/tools/util"; |
||||
|
||||
import { PasswordRandomizer } from "../../engine"; |
||||
import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies"; |
||||
import { |
||||
CredentialGenerator, |
||||
GeneratorDependencyProvider, |
||||
PasswordGeneratorSettings, |
||||
} from "../../types"; |
||||
import { Algorithm, Profile, Type } from "../data"; |
||||
import { GeneratorMetadata } from "../generator-metadata"; |
||||
|
||||
const password: GeneratorMetadata<PasswordGeneratorSettings> = deepFreeze({ |
||||
id: Algorithm.password, |
||||
category: Type.password, |
||||
weight: 100, |
||||
i18nKeys: { |
||||
name: "password", |
||||
generateCredential: "generatePassword", |
||||
credentialGenerated: "passwordGenerated", |
||||
credentialType: "password", |
||||
copyCredential: "copyPassword", |
||||
useCredential: "useThisPassword", |
||||
}, |
||||
capabilities: { |
||||
autogenerate: true, |
||||
fields: [], |
||||
}, |
||||
engine: { |
||||
create( |
||||
dependencies: GeneratorDependencyProvider, |
||||
): CredentialGenerator<PasswordGeneratorSettings> { |
||||
return new PasswordRandomizer(dependencies.randomizer); |
||||
}, |
||||
}, |
||||
profiles: { |
||||
[Profile.account]: { |
||||
type: "core", |
||||
storage: { |
||||
key: "passwordGeneratorSettings", |
||||
target: "object", |
||||
format: "plain", |
||||
classifier: new PublicClassifier<PasswordGeneratorSettings>([ |
||||
"length", |
||||
"ambiguous", |
||||
"uppercase", |
||||
"minUppercase", |
||||
"lowercase", |
||||
"minLowercase", |
||||
"number", |
||||
"minNumber", |
||||
"special", |
||||
"minSpecial", |
||||
]), |
||||
state: GENERATOR_DISK, |
||||
initial: { |
||||
length: 14, |
||||
ambiguous: true, |
||||
uppercase: true, |
||||
minUppercase: 1, |
||||
lowercase: true, |
||||
minLowercase: 1, |
||||
number: true, |
||||
minNumber: 1, |
||||
special: false, |
||||
minSpecial: 0, |
||||
}, |
||||
options: { |
||||
deserializer(value) { |
||||
return value; |
||||
}, |
||||
clearOn: ["logout"], |
||||
}, |
||||
}, |
||||
constraints: { |
||||
type: PolicyType.PasswordGenerator, |
||||
default: { |
||||
length: { |
||||
min: 5, |
||||
max: 128, |
||||
recommendation: 14, |
||||
}, |
||||
minNumber: { |
||||
min: 0, |
||||
max: 9, |
||||
}, |
||||
minSpecial: { |
||||
min: 0, |
||||
max: 9, |
||||
}, |
||||
}, |
||||
create(policies, context) { |
||||
const initial = { |
||||
minLength: 0, |
||||
useUppercase: false, |
||||
useLowercase: false, |
||||
useNumbers: false, |
||||
numberCount: 0, |
||||
useSpecial: false, |
||||
specialCount: 0, |
||||
}; |
||||
const policy = policies.reduce(passwordLeastPrivilege, initial); |
||||
const constraints = new DynamicPasswordPolicyConstraints( |
||||
policy, |
||||
context.defaultConstraints, |
||||
); |
||||
return constraints; |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
export default password; |
||||
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums"; |
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; |
||||
import { SiteId } from "@bitwarden/common/tools/extension"; |
||||
import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; |
||||
import { Constraints } from "@bitwarden/common/tools/types"; |
||||
|
||||
import { GeneratorConstraints } from "../types"; |
||||
|
||||
export type ProfileContext<Options> = { |
||||
/** The email address for the current user; |
||||
* `undefined` when no email is available. |
||||
*/ |
||||
email?: string; |
||||
|
||||
/** Default application limits for the profile */ |
||||
defaultConstraints: Constraints<Options>; |
||||
}; |
||||
|
||||
type ProfileConstraints<Options> = { |
||||
/** The key used to locate this profile's policies in the admin console. |
||||
* When this type is undefined, no policy is defined for the profile. |
||||
*/ |
||||
type?: PolicyType; |
||||
|
||||
/** default application limits for this profile; these are overridden |
||||
* by the policy |
||||
*/ |
||||
default: Constraints<Options>; |
||||
|
||||
/** Constructs generator constraints from a policy. |
||||
* @param policies the administrative policy to apply to the provided constraints |
||||
* When `type` is undefined then `policy` is `undefined` this is an empty array. |
||||
* @param defaultConstraints application constraints; typically those defined in |
||||
* the `default` member, above. |
||||
* @returns the generator constraints to apply to this profile's options. |
||||
*/ |
||||
create: (policies: Policy[], context: ProfileContext<Options>) => GeneratorConstraints<Options>; |
||||
}; |
||||
|
||||
/** Generator profiles partition generator operations |
||||
* according to where they're used within the password |
||||
* manager. Core profiles store their data using the |
||||
* generator's system storage. |
||||
*/ |
||||
export type CoreProfileMetadata<Options> = { |
||||
/** distinguishes profile metadata types */ |
||||
type: "core"; |
||||
|
||||
/** plaintext import buffer */ |
||||
import?: ObjectKey<Options, Record<string, never>, Options> & { format: "plain" }; |
||||
|
||||
/** persistent storage location */ |
||||
storage: ObjectKey<Options>; |
||||
|
||||
/** policy enforced when saving the options */ |
||||
constraints: ProfileConstraints<Options>; |
||||
}; |
||||
|
||||
/** Generator profiles partition generator operations |
||||
* according to where they're used within the password |
||||
* manager. Extension profiles store their data |
||||
* using the extension system. |
||||
*/ |
||||
export type ExtensionProfileMetadata<Options, Site extends SiteId> = { |
||||
/** distinguishes profile metadata types */ |
||||
type: "extension"; |
||||
|
||||
/** The extension site described by this metadata */ |
||||
site: Site; |
||||
|
||||
constraints: ProfileConstraints<Options>; |
||||
}; |
||||
|
||||
/** Generator profiles partition generator operations |
||||
* according to where they're used within the password |
||||
* manager |
||||
*/ |
||||
export type ProfileMetadata<Options> = |
||||
| CoreProfileMetadata<Options> |
||||
| ExtensionProfileMetadata<Options, "forwarder">; |
||||
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
import { VendorId } from "@bitwarden/common/tools/extension"; |
||||
|
||||
import { AlgorithmsByType, Profile, Type } from "./data"; |
||||
|
||||
/** categorizes credentials according to their use-case outside of Bitwarden */ |
||||
export type CredentialType = keyof typeof Type; |
||||
|
||||
/** categorizes credentials according to their expected use-case within Bitwarden */ |
||||
export type GeneratorProfile = keyof typeof Profile; |
||||
|
||||
/** A type of password that may be generated by the credential generator. */ |
||||
export type PasswordAlgorithm = (typeof AlgorithmsByType.password)[number]; |
||||
|
||||
/** A type of username that may be generated by the credential generator. */ |
||||
export type UsernameAlgorithm = (typeof AlgorithmsByType.username)[number]; |
||||
|
||||
/** A type of email address that may be generated by the credential generator. */ |
||||
export type EmailAlgorithm = (typeof AlgorithmsByType.email)[number] | ForwarderExtensionId; |
||||
|
||||
/** Identifies a forwarding service */ |
||||
export type ForwarderExtensionId = { forwarder: VendorId }; |
||||
|
||||
/** A type of credential that can be generated by the credential generator. */ |
||||
// this is defined in terms of `AlgorithmsByType` to typecheck the keys of
|
||||
// `AlgorithmsByType` against the keys of `CredentialType`.
|
||||
export type CredentialAlgorithm = |
||||
| (typeof AlgorithmsByType)[CredentialType][number] |
||||
| ForwarderExtensionId; |
||||
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
import { mock } from "jest-mock-extended"; |
||||
|
||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; |
||||
|
||||
import { UsernameRandomizer } from "../../engine"; |
||||
import { EffUsernameGenerationOptions, GeneratorDependencyProvider } from "../../types"; |
||||
import { Profile } from "../data"; |
||||
import { CoreProfileMetadata } from "../profile-metadata"; |
||||
import { isCoreProfile } from "../util"; |
||||
|
||||
import effWordList from "./eff-word-list"; |
||||
|
||||
const dependencyProvider = mock<GeneratorDependencyProvider>(); |
||||
|
||||
describe("username - eff words generator metadata", () => { |
||||
describe("engine.create", () => { |
||||
it("returns an email randomizer", () => { |
||||
expect(effWordList.engine.create(dependencyProvider)).toBeInstanceOf(UsernameRandomizer); |
||||
}); |
||||
}); |
||||
|
||||
describe("profiles[account]", () => { |
||||
let accountProfile: CoreProfileMetadata<EffUsernameGenerationOptions> = null; |
||||
beforeEach(() => { |
||||
const profile = effWordList.profiles[Profile.account]; |
||||
if (isCoreProfile(profile)) { |
||||
accountProfile = profile; |
||||
} |
||||
}); |
||||
|
||||
describe("storage.options.deserializer", () => { |
||||
it("returns its input", () => { |
||||
const value: EffUsernameGenerationOptions = { |
||||
wordCapitalize: true, |
||||
wordIncludeNumber: true, |
||||
}; |
||||
|
||||
const result = accountProfile.storage.options.deserializer(value); |
||||
|
||||
expect(result).toBe(value); |
||||
}); |
||||
}); |
||||
|
||||
describe("constraints.create", () => { |
||||
// these tests check that the wiring is correct by exercising the behavior
|
||||
// of functionality encapsulated by `create`. These methods may fail if the
|
||||
// enclosed behaviors change.
|
||||
|
||||
it("creates a effWordList constraints", () => { |
||||
const context = { defaultConstraints: {} }; |
||||
|
||||
const constraints = accountProfile.constraints.create([], context); |
||||
|
||||
expect(constraints).toBeInstanceOf(IdentityConstraint); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; |
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; |
||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; |
||||
import { deepFreeze } from "@bitwarden/common/tools/util"; |
||||
|
||||
import { UsernameRandomizer } from "../../engine"; |
||||
import { |
||||
CredentialGenerator, |
||||
EffUsernameGenerationOptions, |
||||
GeneratorDependencyProvider, |
||||
} from "../../types"; |
||||
import { Algorithm, Profile, Type } from "../data"; |
||||
import { GeneratorMetadata } from "../generator-metadata"; |
||||
|
||||
const effWordList: GeneratorMetadata<EffUsernameGenerationOptions> = deepFreeze({ |
||||
id: Algorithm.username, |
||||
category: Type.username, |
||||
weight: 400, |
||||
i18nKeys: { |
||||
name: "randomWord", |
||||
credentialType: "username", |
||||
generateCredential: "generateUsername", |
||||
credentialGenerated: "usernameGenerated", |
||||
copyCredential: "copyUsername", |
||||
useCredential: "useThisUsername", |
||||
}, |
||||
capabilities: { |
||||
autogenerate: true, |
||||
fields: [], |
||||
}, |
||||
engine: { |
||||
create( |
||||
dependencies: GeneratorDependencyProvider, |
||||
): CredentialGenerator<EffUsernameGenerationOptions> { |
||||
return new UsernameRandomizer(dependencies.randomizer); |
||||
}, |
||||
}, |
||||
profiles: { |
||||
[Profile.account]: { |
||||
type: "core", |
||||
storage: { |
||||
key: "effUsernameGeneratorSettings", |
||||
target: "object", |
||||
format: "plain", |
||||
classifier: new PublicClassifier<EffUsernameGenerationOptions>([ |
||||
"wordCapitalize", |
||||
"wordIncludeNumber", |
||||
]), |
||||
state: GENERATOR_DISK, |
||||
initial: { |
||||
wordCapitalize: false, |
||||
wordIncludeNumber: false, |
||||
website: null, |
||||
}, |
||||
options: { |
||||
deserializer: (value) => value, |
||||
clearOn: ["logout"], |
||||
}, |
||||
}, |
||||
constraints: { |
||||
default: {}, |
||||
create(_policies, _context) { |
||||
return new IdentityConstraint<EffUsernameGenerationOptions>(); |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
export default effWordList; |
||||
@ -0,0 +1,218 @@
@@ -0,0 +1,218 @@
|
||||
import { VendorId } from "@bitwarden/common/tools/extension"; |
||||
|
||||
import { Algorithm, AlgorithmsByType } from "./data"; |
||||
import { ProfileMetadata } from "./profile-metadata"; |
||||
import { |
||||
isPasswordAlgorithm, |
||||
isUsernameAlgorithm, |
||||
isForwarderExtensionId, |
||||
isEmailAlgorithm, |
||||
isSameAlgorithm, |
||||
isCoreProfile, |
||||
isForwarderProfile, |
||||
} from "./util"; |
||||
|
||||
describe("credential generator metadata utility functions", () => { |
||||
describe("isPasswordAlgorithm", () => { |
||||
it("returns `true` when the algorithm is a password algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.password) { |
||||
expect(isPasswordAlgorithm(algorithm)).toBe(true); |
||||
} |
||||
}); |
||||
|
||||
it("returns `false` when the algorithm is an email algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.email) { |
||||
expect(isPasswordAlgorithm(algorithm)).toBe(false); |
||||
} |
||||
}); |
||||
|
||||
it("returns `false` when the algorithm is a username algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.username) { |
||||
expect(isPasswordAlgorithm(algorithm)).toBe(false); |
||||
} |
||||
}); |
||||
|
||||
it("returns `false` when the algorithm is a forwarder extension", () => { |
||||
expect(isPasswordAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe("isUsernameAlgorithm", () => { |
||||
it("returns `false` when the algorithm is a password algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.password) { |
||||
expect(isUsernameAlgorithm(algorithm)).toBe(false); |
||||
} |
||||
}); |
||||
|
||||
it("returns `false` when the algorithm is an email algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.email) { |
||||
expect(isUsernameAlgorithm(algorithm)).toBe(false); |
||||
} |
||||
}); |
||||
|
||||
it("returns `true` when the algorithm is a username algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.username) { |
||||
expect(isUsernameAlgorithm(algorithm)).toBe(true); |
||||
} |
||||
}); |
||||
|
||||
it("returns `false` when the algorithm is a forwarder extension", () => { |
||||
expect(isUsernameAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe("isForwarderExtensionId", () => { |
||||
it("returns `false` when the algorithm is a password algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.password) { |
||||
expect(isForwarderExtensionId(algorithm)).toBe(false); |
||||
} |
||||
}); |
||||
|
||||
it("returns `false` when the algorithm is an email algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.email) { |
||||
expect(isForwarderExtensionId(algorithm)).toBe(false); |
||||
} |
||||
}); |
||||
|
||||
it("returns `false` when the algorithm is a username algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.username) { |
||||
expect(isForwarderExtensionId(algorithm)).toBe(false); |
||||
} |
||||
}); |
||||
|
||||
it("returns `true` when the algorithm is a forwarder extension", () => { |
||||
expect(isForwarderExtensionId({ forwarder: "bitwarden" as VendorId })).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe("isEmailAlgorithm", () => { |
||||
it("returns `false` when the algorithm is a password algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.password) { |
||||
expect(isEmailAlgorithm(algorithm)).toBe(false); |
||||
} |
||||
}); |
||||
|
||||
it("returns `true` when the algorithm is an email algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.email) { |
||||
expect(isEmailAlgorithm(algorithm)).toBe(true); |
||||
} |
||||
}); |
||||
|
||||
it("returns `false` when the algorithm is a username algorithm", () => { |
||||
for (const algorithm of AlgorithmsByType.username) { |
||||
expect(isEmailAlgorithm(algorithm)).toBe(false); |
||||
} |
||||
}); |
||||
|
||||
it("returns `true` when the algorithm is a forwarder extension", () => { |
||||
expect(isEmailAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe("isSameAlgorithm", () => { |
||||
it("returns `true` when the algorithms are equal", () => { |
||||
// identical object
|
||||
expect(isSameAlgorithm(Algorithm.catchall, Algorithm.catchall)).toBe(true); |
||||
|
||||
// equal object
|
||||
expect(isSameAlgorithm(Algorithm.catchall, `${Algorithm.catchall}`)).toBe(true); |
||||
}); |
||||
|
||||
it("returns `false` when the algorithms are different", () => { |
||||
// not an exhaustive list
|
||||
expect(isSameAlgorithm(Algorithm.catchall, Algorithm.passphrase)).toBe(false); |
||||
expect(isSameAlgorithm(Algorithm.passphrase, Algorithm.password)).toBe(false); |
||||
expect(isSameAlgorithm(Algorithm.password, Algorithm.plusAddress)).toBe(false); |
||||
expect(isSameAlgorithm(Algorithm.plusAddress, Algorithm.username)).toBe(false); |
||||
expect(isSameAlgorithm(Algorithm.username, Algorithm.passphrase)).toBe(false); |
||||
}); |
||||
|
||||
it("returns `true` when the algorithms refer to a forwarder with a matching vendor", () => { |
||||
const someVendor = { forwarder: "bitwarden" as VendorId }; |
||||
const sameVendor = { forwarder: "bitwarden" as VendorId }; |
||||
expect(isSameAlgorithm(someVendor, sameVendor)).toBe(true); |
||||
}); |
||||
|
||||
it("returns `false` when the algorithms refer to a forwarder with a different vendor", () => { |
||||
const someVendor = { forwarder: "bitwarden" as VendorId }; |
||||
const sameVendor = { forwarder: "bytewarden" as VendorId }; |
||||
expect(isSameAlgorithm(someVendor, sameVendor)).toBe(false); |
||||
}); |
||||
|
||||
it("returns `false` when the algorithms refer to a forwarder and a core algorithm", () => { |
||||
const someVendor = { forwarder: "bitwarden" as VendorId }; |
||||
// not an exhaustive list
|
||||
expect(isSameAlgorithm(someVendor, Algorithm.plusAddress)).toBe(false); |
||||
expect(isSameAlgorithm(Algorithm.username, someVendor)).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe("isCoreProfile", () => { |
||||
it("returns `true` when the profile's type is `core`", () => { |
||||
const profile: ProfileMetadata<object> = { |
||||
type: "core", |
||||
storage: null, |
||||
constraints: { |
||||
default: {}, |
||||
create: () => null, |
||||
}, |
||||
}; |
||||
|
||||
expect(isCoreProfile(profile)).toBe(true); |
||||
}); |
||||
|
||||
it("returns `false` when the profile's type is `extension`", () => { |
||||
const profile: ProfileMetadata<object> = { |
||||
type: "extension", |
||||
site: "forwarder", |
||||
constraints: { |
||||
default: {}, |
||||
create: () => null, |
||||
}, |
||||
}; |
||||
|
||||
expect(isCoreProfile(profile)).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe("isForwarderProfile", () => { |
||||
it("returns `false` when the profile's type is `core`", () => { |
||||
const profile: ProfileMetadata<object> = { |
||||
type: "core", |
||||
storage: null, |
||||
constraints: { |
||||
default: {}, |
||||
create: () => null, |
||||
}, |
||||
}; |
||||
|
||||
expect(isForwarderProfile(profile)).toBe(false); |
||||
}); |
||||
|
||||
it("returns `true` when the profile's type is `extension` and the site is `forwarder`", () => { |
||||
const profile: ProfileMetadata<object> = { |
||||
type: "extension", |
||||
site: "forwarder", |
||||
constraints: { |
||||
default: {}, |
||||
create: () => null, |
||||
}, |
||||
}; |
||||
|
||||
expect(isForwarderProfile(profile)).toBe(true); |
||||
}); |
||||
|
||||
it("returns `false` when the profile's type is `extension` and the site is not `forwarder`", () => { |
||||
const profile: ProfileMetadata<object> = { |
||||
type: "extension", |
||||
site: "not-a-forwarder" as any, |
||||
constraints: { |
||||
default: {}, |
||||
create: () => null, |
||||
}, |
||||
}; |
||||
|
||||
expect(isForwarderProfile(profile)).toBe(false); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
import { AlgorithmsByType } from "./data"; |
||||
import { CoreProfileMetadata, ExtensionProfileMetadata, ProfileMetadata } from "./profile-metadata"; |
||||
import { |
||||
CredentialAlgorithm, |
||||
EmailAlgorithm, |
||||
ForwarderExtensionId, |
||||
PasswordAlgorithm, |
||||
UsernameAlgorithm, |
||||
} from "./type"; |
||||
|
||||
/** Returns true when the input algorithm is a password algorithm. */ |
||||
export function isPasswordAlgorithm( |
||||
algorithm: CredentialAlgorithm, |
||||
): algorithm is PasswordAlgorithm { |
||||
return AlgorithmsByType.password.includes(algorithm as any); |
||||
} |
||||
|
||||
/** Returns true when the input algorithm is a username algorithm. */ |
||||
export function isUsernameAlgorithm( |
||||
algorithm: CredentialAlgorithm, |
||||
): algorithm is UsernameAlgorithm { |
||||
return AlgorithmsByType.username.includes(algorithm as any); |
||||
} |
||||
|
||||
/** Returns true when the input algorithm is a forwarder integration. */ |
||||
export function isForwarderExtensionId( |
||||
algorithm: CredentialAlgorithm, |
||||
): algorithm is ForwarderExtensionId { |
||||
return algorithm && typeof algorithm === "object" && "forwarder" in algorithm; |
||||
} |
||||
|
||||
/** Returns true when the input algorithm is an email algorithm. */ |
||||
export function isEmailAlgorithm(algorithm: CredentialAlgorithm): algorithm is EmailAlgorithm { |
||||
return AlgorithmsByType.email.includes(algorithm as any) || isForwarderExtensionId(algorithm); |
||||
} |
||||
|
||||
/** Returns true when the algorithms are the same. */ |
||||
export function isSameAlgorithm(lhs: CredentialAlgorithm, rhs: CredentialAlgorithm) { |
||||
if (lhs === rhs) { |
||||
return true; |
||||
} else if (isForwarderExtensionId(lhs) && isForwarderExtensionId(rhs)) { |
||||
return lhs.forwarder === rhs.forwarder; |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/** Returns true when the input describes a core profile. */ |
||||
export function isCoreProfile<Options>( |
||||
value: ProfileMetadata<Options>, |
||||
): value is CoreProfileMetadata<Options> { |
||||
return value.type === "core"; |
||||
} |
||||
|
||||
/** Returns true when the input describes a forwarder extension profile. */ |
||||
export function isForwarderProfile<Options>( |
||||
value: ProfileMetadata<Options>, |
||||
): value is ExtensionProfileMetadata<Options, "forwarder"> { |
||||
return value.type === "extension" && value.site === "forwarder"; |
||||
} |
||||
Loading…
Reference in new issue