1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

rename new integration types to extension types

The first pass of the integration logic mixed names between
"integration" and "extension". For example, the remote service
has an "IntegrationId" whereas the part of the password manager
it extended has an "ExtensionPointId".

The new types have a more consistent approach. The remote
service now distinguishes the integrated product from the
vendor providing it, and the logic within the password manager
is now all the "extension". The specific location being extended
is now called the "site" or "extension site".

The main reason "extension" was chosen over "integration" is simply
so that the new and old types remain distinct in the folder hierarchy.
This commit is contained in:
✨ Audrey ✨
2024-12-27 11:32:36 -05:00
parent 54b2be3707
commit b8d5d3c0a6
24 changed files with 519 additions and 289 deletions

View File

@@ -5,7 +5,7 @@ import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { OrganizationEncryptor } from "./cryptography/organization-encryptor.abstraction";
import { UserEncryptor } from "./cryptography/user-encryptor.abstraction";
import { VendorId } from "./integration/metadata/type";
import { VendorId } from "./extension/metadata/type";
/** error emitted when the `SingleUserDependency` changes Ids */
export type UserChangedError = {

View File

@@ -1,19 +1,20 @@
import { Field, Vendor } from "../data";
import { Extension } from "../extension";
import { IntegrationMetadata } from "../type";
import { Field, Vendor } from "../metadata/data";
import { Extension } from "../metadata/extension";
import { ExtensionMetadata } from "../metadata/type";
export const AddyIo = {
id: Vendor.anonaddy,
name: "Addy.io",
};
export const AddyIoIntegrations: IntegrationMetadata[] = [
export const AddyIoIntegrations: ExtensionMetadata[] = [
{
extension: Extension.forwarder,
site: Extension.forwarder,
product: {
vendor: AddyIo,
},
host: {
authorization: "bearer",
selfHost: "maybe",
baseUrl: "https://app.addy.io",
},

View File

@@ -0,0 +1,23 @@
import { Field, Vendor } from "../metadata/data";
import { Extension } from "../metadata/extension";
import { ExtensionMetadata } from "../metadata/type";
export const DuckDuckGo = {
id: Vendor.duckduckgo,
name: "DuckDuckGo",
};
export const DuckDuckGoIntegrations: ExtensionMetadata[] = [
{
site: Extension.forwarder,
product: {
vendor: DuckDuckGo,
},
host: {
authorization: "bearer",
selfHost: "never",
baseUrl: "https://quack.duckduckgo.com/api",
},
requestedFields: [Field.token],
},
];

View File

@@ -1,6 +1,6 @@
import { Field, Vendor } from "../data";
import { Extension } from "../extension";
import { IntegrationMetadata } from "../type";
import { Field, Vendor } from "../metadata/data";
import { Extension } from "../metadata/extension";
import { ExtensionMetadata } from "../metadata/type";
export const Fastmail = {
id: Vendor.fastmail,
@@ -8,13 +8,14 @@ export const Fastmail = {
};
// integration-wide configuration
export const FastmailIntegrations: IntegrationMetadata[] = [
export const FastmailIntegrations: ExtensionMetadata[] = [
{
extension: Extension.forwarder,
site: Extension.forwarder,
product: {
vendor: Fastmail,
},
host: {
authorization: "bearer",
selfHost: "maybe",
baseUrl: "https://api.fastmail.com",
},

View File

@@ -1,19 +1,20 @@
import { Field, Vendor } from "../data";
import { Extension } from "../extension";
import { IntegrationMetadata } from "../type";
import { Field, Vendor } from "../metadata/data";
import { Extension } from "../metadata/extension";
import { ExtensionMetadata } from "../metadata/type";
export const ForwardEmail = {
id: Vendor.forwardemail,
name: "Forward Email",
};
export const ForwardEmailIntegrations: IntegrationMetadata[] = [
export const ForwardEmailIntegrations: ExtensionMetadata[] = [
{
extension: Extension.forwarder,
site: Extension.forwarder,
product: {
vendor: ForwardEmail,
},
host: {
authorization: "basic-username",
selfHost: "never",
baseUrl: "https://api.forwardemail.net",
},

View File

@@ -1,20 +1,21 @@
import { Field, Vendor } from "../data";
import { Extension } from "../extension";
import { IntegrationMetadata } from "../type";
import { Field, Vendor } from "../metadata/data";
import { Extension } from "../metadata/extension";
import { ExtensionMetadata } from "../metadata/type";
export const Mozilla = {
id: Vendor.mozilla,
name: "Mozilla",
};
export const MozillaIntegrations: IntegrationMetadata[] = [
export const MozillaIntegrations: ExtensionMetadata[] = [
{
extension: Extension.forwarder,
site: Extension.forwarder,
product: {
vendor: Mozilla,
name: "Firefox Relay",
},
host: {
authorization: "token",
selfHost: "never",
baseUrl: "https://relay.firefox.com/api",
},

View File

@@ -1,19 +1,20 @@
import { Field, Vendor } from "../data";
import { Extension } from "../extension";
import { IntegrationMetadata, VendorMetadata } from "../type";
import { Field, Vendor } from "../metadata/data";
import { Extension } from "../metadata/extension";
import { ExtensionMetadata, VendorMetadata } from "../metadata/type";
export const SimpleLogin: VendorMetadata = {
id: Vendor.simplelogin,
name: "SimpleLogin",
};
export const SimpleLoginIntegrations: IntegrationMetadata[] = [
export const SimpleLoginIntegrations: ExtensionMetadata[] = [
{
extension: Extension.forwarder,
site: Extension.forwarder,
product: {
vendor: SimpleLogin,
},
host: {
authentication: true,
selfHost: "maybe",
baseUrl: "https://app.simplelogin.io",
},

View File

@@ -0,0 +1,29 @@
import { Vendor } from "../metadata/data";
import { VendorMetadata } from "../metadata/type";
export const VendorInfo: Record<string, VendorMetadata> = {
[Vendor.anonaddy]: {
id: Vendor.anonaddy,
name: "Addy.io",
},
[Vendor.duckduckgo]: {
id: Vendor.duckduckgo,
name: "DuckDuckGo",
},
[Vendor.fastmail]: {
id: Vendor.fastmail,
name: "Fastmail",
},
[Vendor.mozilla]: {
id: Vendor.mozilla,
name: "Mozilla",
},
[Vendor.forwardemail]: {
id: Vendor.forwardemail,
name: "Forward Email",
},
[Vendor.simplelogin]: {
id: Vendor.simplelogin,
name: "SimpleLogin",
},
};

View File

@@ -0,0 +1,20 @@
import { deepFreeze } from "../util";
import { ExtensionMetadata, SiteMetadata, VendorId } from "./metadata/type";
/** Describes the capabilities of an extension site.
* This type is immutable.
*/
export class ExtensionSite {
/** instantiate the extension site
* @param site describes the extension site
* @param vendors describes the available vendors
* @param extensions describes the available extensions
*/
constructor(
readonly site: Readonly<SiteMetadata>,
readonly extensions: ReadonlyMap<VendorId, Readonly<ExtensionMetadata>>,
) {
deepFreeze(this);
}
}

View File

@@ -0,0 +1,15 @@
import { StateProvider } from "@bitwarden/common/platform/state";
import { LegacyEncryptorProvider } from "../cryptography/legacy-encryptor-provider";
import { DefaultIntegrationRegistry } from "./metadata/default-extension-registry";
export class ExtensionService {
constructor(
private readonly registry: DefaultIntegrationRegistry,
private readonly stateProvider: StateProvider,
private readonly encryptorProvider: LegacyEncryptorProvider,
) {}
// TODO: implement the service
}

View File

@@ -0,0 +1,178 @@
import { ExtensionSite } from "../extension-site";
import { ExtensionPermission, ExtensionRegistry } from "./extension-registry.abstraction";
import {
SiteMetadata,
SiteId,
ExtensionMetadata,
ExtensionSet,
VendorId,
VendorMetadata,
} from "./type";
/** Tracks extension points and the integrations that extend them. */
export class DefaultIntegrationRegistry implements ExtensionRegistry {
private allRule: ExtensionPermission = "default";
private siteRegistrations = new Map<SiteId, SiteMetadata>();
private siteRules = new Map<SiteId, ExtensionPermission>();
private vendorRegistrations = new Map<VendorId, VendorMetadata>();
private vendorRules = new Map<VendorId, ExtensionPermission>();
private integrations = new Array<ExtensionMetadata>();
private siteIntegrationsByVendor = new Map<VendorId, Map<SiteId, number>>();
private vendorIntegrationsBySite = new Map<SiteId, Map<VendorId, number>>();
registerSite(meta: SiteMetadata): this {
if (!this.siteRegistrations.has(meta.id)) {
this.siteRegistrations.set(meta.id, meta);
}
return this;
}
sites() {
const sites: { site: SiteMetadata; permission?: ExtensionPermission }[] = [];
for (const [k, site] of this.siteRegistrations.entries()) {
const s: (typeof sites)[number] = { site };
const permission = this.siteRules.get(k);
if (permission) {
s.permission = permission;
}
sites.push(s);
}
return sites;
}
registerVendor(meta: VendorMetadata): this {
if (!this.vendorRegistrations.has(meta.id)) {
this.vendorRegistrations.set(meta.id, meta);
}
return this;
}
vendors() {
const vendors: { vendor: VendorMetadata; permission?: ExtensionPermission }[] = [];
for (const [k, vendor] of this.vendorRegistrations.entries()) {
const s: (typeof vendors)[number] = { vendor };
const permission = this.vendorRules.get(k);
if (permission) {
s.permission = permission;
}
vendors.push(s);
}
return vendors;
}
setPermission(set: ExtensionSet, permission: ExtensionPermission): this {
if ("all" in set && set.all) {
this.allRule = permission;
} else if ("vendor" in set) {
this.vendorRules.set(set.vendor, permission);
} else if ("site" in set) {
this.siteRules.set(set.site, permission);
} else {
throw new Error(`Unrecognized integration set received: ${JSON.stringify(set)}.`);
}
return this;
}
permission(set: ExtensionSet) {
if ("all" in set && set.all) {
return this.allRule;
} else if ("vendor" in set) {
return this.vendorRules.get(set.vendor);
} else if ("site" in set) {
return this.siteRules.get(set.site);
} else {
return undefined;
}
}
permissions() {
const rules: { set: ExtensionSet; permission: ExtensionPermission }[] = [];
rules.push({ set: { all: true }, permission: this.allRule });
for (const [site, permission] of this.siteRules.entries()) {
rules.push({ set: { site }, permission });
}
for (const [vendor, permission] of this.vendorRules.entries()) {
rules.push({ set: { vendor }, permission });
}
return rules;
}
registerExtension(meta: ExtensionMetadata): this {
if (!this.siteRegistrations.has(meta.site.id)) {
throw new Error(`Unrecognized site: ${meta.site.id}`);
} else if (!this.vendorRegistrations.has(meta.product.vendor.id)) {
throw new Error(`Unrecognized vendor: ${meta.product.vendor.id}`);
}
// is the integration registered?
const vendorMap =
this.vendorIntegrationsBySite.get(meta.site.id) ?? new Map<VendorId, number>();
if (vendorMap.has(meta.product.vendor.id)) {
return;
}
// if not, register it
const siteMap =
this.siteIntegrationsByVendor.get(meta.product.vendor.id) ?? new Map<SiteId, number>();
const index = this.integrations.push(meta) - 1;
vendorMap.set(meta.product.vendor.id, index);
siteMap.set(meta.site.id, index);
return this;
}
build(id: SiteId): ExtensionSite | undefined {
const site = this.siteRegistrations.get(id);
if (!site) {
return undefined;
}
if (this.allRule === "deny") {
return new ExtensionSite(site, new Map());
}
const extensions = new Map<VendorId, ExtensionMetadata>();
const entries = this.vendorIntegrationsBySite.get(id)?.entries() ?? ([] as const);
for (const [vendor, maybeIndex] of entries) {
// prepare rules
const vendorRule = this.vendorRules.get(vendor) ?? this.allRule;
const siteRule = this.siteRules.get(id) ?? this.allRule;
const rules = [vendorRule, siteRule, this.allRule];
// evaluate rules
const extension = rules.includes("deny")
? undefined
: rules.includes("allow")
? this.integrations[maybeIndex]
: rules.includes("none")
? undefined
: rules.includes("default")
? this.integrations[maybeIndex]
: undefined;
// the presence of an extension indicates it's accessible
if (extension) {
extensions.set(vendor, extension);
}
}
const extensionSite = new ExtensionSite(site, extensions);
return extensionSite;
}
}

View File

@@ -0,0 +1,70 @@
import { ExtensionSite } from "../extension-site";
import { SiteMetadata, ExtensionMetadata, ExtensionSet, VendorMetadata, SiteId } from "./type";
/** Permission levels for metadata.
* * default - unless a rule denies access, allow it. This is the
* default permission.
* * none - unless a rule allows access, deny it.
* * allow - access is explicitly granted to use an integration.
* * deny - access is explicitly prohibited for this integration. This
* rule overrides allow rules.
*/
export type ExtensionPermission = "default" | "none" | "allow" | "deny";
export abstract class ExtensionRegistry {
/** Registers a site supporting extensibility.
* @param site identifies the site being extended
* @param meta configures the extension site
* @return self for method chaining.
*/
registerSite: (meta: SiteMetadata) => this;
/** List all registered extension sites with their integration rule, if any.
* @returns a list of all extension sites. `rule` is defined when the site
* is associated with an integration rule.
*/
sites: () => { site: SiteMetadata; permission?: ExtensionPermission }[];
/** Registers a vendor providing an integration
* @param site - identifies the site being extended
* @param meta - configures the extension site
* @return self for method chaining.
*/
registerVendor: (meta: VendorMetadata) => this;
/** List all registered vendors with their integration rule, if any.
* @returns a list of all extension sites. `rule` is defined when the site
* is associated with an integration rule.
*/
vendors: () => { vendor: VendorMetadata; permission?: ExtensionPermission }[];
/** Registers an integration provided by a vendor to an extension site.
* The vendor and site MUST be registered before the integration.
* @param site - identifies the site being extended
* @param meta - configures the extension site
* @return self for method chaining.
*/
registerExtension: (meta: ExtensionMetadata) => this;
/** Registers a rule. Only 1 rule can be registered for each integration set.
* The last-registered rule wins.
* @param set the collection of integrations affected by the rule
* @param permission the permission for the collection
* @return self for method chaining.
*/
setPermission: (set: ExtensionSet, permission: ExtensionPermission) => this;
/** Retrieves the current rule for the given integration set or undefined if a rule
* doesn't exist. */
permission: (set: ExtensionSet) => ExtensionPermission | undefined;
/** Returns all registered integration rules. */
permissions: () => { set: ExtensionSet; permission: ExtensionPermission }[];
/** Creates a point-in-time snapshot of the registry's contents with integration
* permissions applied for the provided SiteId.
* @returns the extension site, or `undefined` if the site is not registered.
*/
build: (id: SiteId) => ExtensionSite | undefined;
}

View File

@@ -1,7 +1,7 @@
import { Field, Site } from "./data";
import { ExtensionMetadata } from "./type";
import { SiteMetadata } from "./type";
export const Extension: Record<string, ExtensionMetadata> = {
export const Extension: Record<string, SiteMetadata> = {
[Site.forwarder]: {
id: Site.forwarder,
availableFields: [Field.baseUrl, Field.domain, Field.prefix, Field.token],

View File

@@ -0,0 +1,36 @@
import { AddyIo, AddyIoIntegrations } from "../data/addy-io";
import { DuckDuckGo, DuckDuckGoIntegrations } from "../data/duck-duck-go";
import { Fastmail, FastmailIntegrations } from "../data/fastmail";
import { ForwardEmail, ForwardEmailIntegrations } from "../data/forward-email";
import { Mozilla, MozillaIntegrations } from "../data/mozilla";
import { SimpleLogin, SimpleLoginIntegrations } from "../data/simple-login";
import { DefaultIntegrationRegistry } from "./default-extension-registry";
import { Extension } from "./extension";
import { ExtensionMetadata, VendorMetadata } from "./type";
/** Constructs the integration registry */
export function buildRegistry() {
// FIXME: find a better way to build the registry than a hard-coded factory function
function registerAll(vendor: VendorMetadata, integrations: ExtensionMetadata[]) {
registry.registerVendor(vendor);
for (const integration of integrations) {
registry.registerExtension(integration);
}
}
const registry = new DefaultIntegrationRegistry();
for (const site of Reflect.ownKeys(Extension) as string[]) {
registry.registerSite(Extension[site]);
}
registerAll(AddyIo, AddyIoIntegrations);
registerAll(DuckDuckGo, DuckDuckGoIntegrations);
registerAll(Fastmail, FastmailIntegrations);
registerAll(ForwardEmail, ForwardEmailIntegrations);
registerAll(Mozilla, MozillaIntegrations);
registerAll(SimpleLogin, SimpleLoginIntegrations);
return registry;
}

View File

@@ -1,33 +1,50 @@
import { FieldsBySite, Site, VendorsByExtension } from "./data";
/** well-known name for a feature extensible through an integration. */
export type ExtensionSite = keyof typeof Site;
export type SiteId = keyof typeof Site;
/** well-known name for a field surfaced from an extension site to a vendor. */
export type DisclosedField = (typeof FieldsBySite)[ExtensionSite][number];
export type DisclosedField = (typeof FieldsBySite)[SiteId][number];
/** Identifies a vendor integrated into bitwarden */
export type VendorId = (typeof VendorsByExtension)[ExtensionSite][number];
export type VendorId = (typeof VendorsByExtension)[SiteId][number];
/** The capabilities and descriptive content for an integration */
export type ExtensionMetadata = {
export type SiteMetadata = {
/** Uniquely identifies the integrator. */
id: ExtensionSite;
id: SiteId;
/** Lists the fields disclosed by the extension to the vendor */
availableFields: DisclosedField[];
};
type TokenHeader =
| {
/** Transmit the token as the value of an `Authentication` header */
authentication: true;
}
| {
/** Transmit the token as an `Authorization` header and a formatted value
* * `bearer` uses OAUTH-2.0 bearer token format
* * `token` prefixes the token with "Token"
* * `basic-username` uses HTTP Basic authentication format, encoding the
* token as the username.
*/
authorization: "bearer" | "token" | "basic-username";
};
/** Catalogues an integration's hosting status.
* selfHost: "never" always uses the service's base URL
* selfHost: "maybe" allows the user to override the service's
* base URL with their own.
* selfHost: "always" requires a base URL.
*/
export type ApiHost =
| { selfHost: "never"; baseUrl: string }
| { selfHost: "maybe"; baseUrl: string }
| { selfHost: "always" };
export type ApiHost = TokenHeader &
(
| { selfHost: "never"; baseUrl: string }
| { selfHost: "maybe"; baseUrl: string }
| { selfHost: "always" }
);
/** The capabilities and descriptive content for an integration */
export type VendorMetadata = {
@@ -39,9 +56,9 @@ export type VendorMetadata = {
};
/** Describes an integration provided by a vendor */
export type IntegrationMetadata = {
export type ExtensionMetadata = {
/** The part of Bitwarden extended by the vendor's services */
extension: ExtensionMetadata;
site: SiteMetadata;
/** Product description */
product: {
@@ -61,3 +78,21 @@ export type IntegrationMetadata = {
*/
requestedFields: DisclosedField[];
};
/** Identifies a collection of integrations.
*/
export type ExtensionSet =
| {
/** A set of integrations sharing an extension point */
site: SiteId;
}
| {
/** A set of integrations sharing a vendor */
vendor: VendorId;
}
| {
/** The total set of integrations. This is used to set a categorical
* rule affecting all integrations.
*/
all: true;
};

View File

@@ -1,48 +0,0 @@
import { Jsonify } from "type-fest";
import { INTEGRATION_DISK } from "@bitwarden/common/platform/state";
import { PrivateClassifier } from "../private-classifier";
import { ObjectKey } from "../state/object-key";
import { IntegrationRegistry } from "./metadata/registry";
import { ExtensionSite, VendorId } from "./metadata/type";
export class IntegrationKey<T> {
constructor(
private site: ExtensionSite,
private deserialize: (json: Jsonify<T>) => T,
private initial?: T,
) {}
/** Constructs an object key
* @remarks this method should only be called by the integration service.
* It is not provided for general use.
*/
toObjectKey(vendor: VendorId, registry: IntegrationRegistry): ObjectKey<T> {
const integration = registry.getIntegration(this.site, vendor);
// an integrator can only store fields that are exported from the
// extension point *and* that were requested by the extension. All
// of the data stored by the extension is, for the moment, private.
const fields: any[] = integration.extension.availableFields.filter((available) =>
integration.requestedFields.includes(available),
);
const classifier = new PrivateClassifier(fields);
const objectKey: ObjectKey<T> = {
target: "object",
key: `${integration.extension.id}.${integration.product.vendor.id}`,
state: INTEGRATION_DISK,
classifier,
format: "classified",
options: {
deserializer: this.deserialize,
clearOn: ["logout"],
},
initial: this.initial,
};
return objectKey;
}
}

View File

@@ -1,58 +0,0 @@
import { BehaviorSubject, combineLatest, firstValueFrom, map, Observable } from "rxjs";
import { StateProvider } from "@bitwarden/common/platform/state";
import { LegacyEncryptorProvider } from "../cryptography/legacy-encryptor-provider";
import { SingleUserDependency, SingleVendorDependency } from "../dependencies";
import { UserStateSubject } from "../state/user-state-subject";
import { IntegrationKey } from "./integration-key";
import { IntegrationRegistry } from "./metadata/registry";
import { ExtensionSite, IntegrationMetadata } from "./metadata/type";
const DEFAULT_INTEGRATION_STORAGE_FRAME = 512;
export class IntegrationService {
constructor(
private readonly registry: IntegrationRegistry,
private readonly stateProvider: StateProvider,
private readonly encryptorProvider: LegacyEncryptorProvider,
) {}
/** Observe the integrations available at an extension site */
integrations$(site: ExtensionSite): Observable<IntegrationMetadata[]> {
const integrations = this.registry.getIntegrations(site);
// slot into a behavior subject so that the integration data is always available
// downstream; once dynamic registrations are supported this could be extended
// to publish updated lists without changing the signature
const integrations$ = new BehaviorSubject(integrations);
return integrations$.asObservable();
}
/** Create a subject monitoring the integration's private data store */
state$<T extends object>(
key: IntegrationKey<T>,
dependencies: SingleUserDependency & SingleVendorDependency,
): Promise<UserStateSubject<T>> {
const singleUserEncryptor$ = this.encryptorProvider.userEncryptor$(
DEFAULT_INTEGRATION_STORAGE_FRAME,
dependencies,
);
const result$ = combineLatest([singleUserEncryptor$, dependencies.singleVendorId$]).pipe(
map(([encryptor, vendor]) => {
const subject = new UserStateSubject<T, T, Record<string, never>>(
key.toObjectKey(vendor, this.registry),
(key) => this.stateProvider.getUser(encryptor.userId, key),
{ singleUserEncryptor$ },
);
return subject;
}),
);
return firstValueFrom(result$);
}
}

View File

@@ -1,35 +0,0 @@
import { Extension } from "./extension";
import { IntegrationRegistry } from "./registry";
import { IntegrationMetadata, VendorMetadata } from "./type";
import { AddyIo, AddyIoIntegrations } from "./vendor/addy-io";
import { DuckDuckGo, DuckDuckGoIntegrations } from "./vendor/duck-duck-go";
import { Fastmail, FastmailIntegrations } from "./vendor/fastmail";
import { ForwardEmail, ForwardEmailIntegrations } from "./vendor/forward-email";
import { Mozilla, MozillaIntegrations } from "./vendor/mozilla";
import { SimpleLogin, SimpleLoginIntegrations } from "./vendor/simple-login";
/** Constructs the integration registry */
export function buildRegistry() {
// FIXME: find a better way to build the registry than a hard-coded factory function
function registerAll(vendor: VendorMetadata, integrations: IntegrationMetadata[]) {
registry.registerVendor(vendor);
for (const integration of integrations) {
registry.registerIntegration(integration);
}
}
const registry = new IntegrationRegistry();
for (const site of Reflect.ownKeys(Extension) as string[]) {
registry.registerExtension(Extension[site]);
}
registerAll(AddyIo, AddyIoIntegrations);
registerAll(DuckDuckGo, DuckDuckGoIntegrations);
registerAll(Fastmail, FastmailIntegrations);
registerAll(ForwardEmail, ForwardEmailIntegrations);
registerAll(Mozilla, MozillaIntegrations);
registerAll(SimpleLogin, SimpleLoginIntegrations);
return registry;
}

View File

@@ -1,87 +0,0 @@
import { ExtensionPointId } from "../extension-point-id";
import {
ExtensionMetadata,
ExtensionSite,
IntegrationMetadata,
VendorId,
VendorMetadata,
} from "./type";
/** Tracks extension points and the integrations that extend them. */
export class IntegrationRegistry {
private extensions = new Map<ExtensionSite, ExtensionMetadata>();
private vendors = new Map<VendorId, VendorMetadata>();
private integrations = new Map<ExtensionSite, Map<VendorId, IntegrationMetadata>>();
/** Registers a site supporting extensibility.
* @param site - identifies the site being extended
* @param meta - configures the extension site
* @return self for method chaining.
*/
registerExtension(meta: ExtensionMetadata): IntegrationRegistry {
if (!this.extensions.has(meta.id)) {
this.extensions.set(meta.id, meta);
}
return this;
}
getExtension(site: ExtensionPointId): ExtensionMetadata | undefined {
const extension = this.extensions.get(site);
return extension ? extension : undefined;
}
/** Registers a site supporting extensibility.
* @param site - identifies the site being extended
* @param meta - configures the extension site
* @return self for method chaining.
*/
registerVendor(meta: VendorMetadata): IntegrationRegistry {
if (!this.vendors.has(meta.id)) {
this.vendors.set(meta.id, meta);
}
return this;
}
getVendor(vendorId: VendorId): VendorMetadata | undefined {
const vendor = this.vendors.get(vendorId);
return vendor ? vendor : undefined;
}
/** Registers an integration provided by a vendor to an extension site.
* The vendor and site MUST be registered before the integration.
* @param site - identifies the site being extended
* @param meta - configures the extension site
* @return self for method chaining.
*/
registerIntegration(meta: IntegrationMetadata): IntegrationRegistry {
if (!this.extensions.has(meta.extension.id)) {
throw new Error(`Unrecognized site: ${meta.extension.id}`);
} else if (!this.vendors.has(meta.product.vendor.id)) {
throw new Error(`Unrecognized vendor: ${meta.product.vendor.id}`);
}
let integrations = this.integrations.get(meta.extension.id);
if (!integrations) {
integrations = new Map();
this.integrations.set(meta.extension.id, integrations);
}
if (!integrations.has(meta.product.vendor.id)) {
integrations.set(meta.product.vendor.id, meta);
}
return this;
}
getIntegration(site: ExtensionPointId, vendor: VendorId): IntegrationMetadata | undefined {
return this.integrations.get(site)?.get(vendor) ?? undefined;
}
getIntegrations(site: ExtensionPointId): IntegrationMetadata[] {
const integrations = Array.from(this.integrations.get(site)?.values() ?? []);
return integrations;
}
}

View File

@@ -1,22 +0,0 @@
import { Field, Vendor } from "../data";
import { Extension } from "../extension";
import { IntegrationMetadata } from "../type";
export const DuckDuckGo = {
id: Vendor.duckduckgo,
name: "DuckDuckGo",
};
export const DuckDuckGoIntegrations: IntegrationMetadata[] = [
{
extension: Extension.forwarder,
product: {
vendor: DuckDuckGo,
},
host: {
selfHost: "never",
baseUrl: "https://quack.duckduckgo.com/api",
},
requestedFields: [Field.token],
},
];

View File

@@ -0,0 +1,19 @@
/** Recursively freeze an object's own keys
* @param value the value to freeze
* @returns `value`
* @remarks this function is derived from MDN's `deepFreeze`, which
* has been committed to the public domain.
*/
export function deepFreeze<T extends object>(value: T): Readonly<T> {
const keys = Reflect.ownKeys(value) as (keyof T)[];
for (const key of keys) {
const own = value[key];
if ((own && typeof own === "object") || typeof own === "function") {
deepFreeze(own);
}
}
return Object.freeze(value);
}

View File

@@ -0,0 +1,49 @@
import { RpcConfiguration } from "@bitwarden/common/tools/integration/rpc";
import { GenerationRequest } from "@bitwarden/common/tools/types";
import { ForwarderContext } from "../engine";
import { GeneratorMetadata } from "../metadata/generator-metadata";
import { NoPolicy } from "../types";
/** Fields that may be requested by a forwarder integration */
export type ForwarderSettings = {
/** bearer token that authenticates bitwarden to the forwarder.
* This is required to issue an API request.
*/
token: string;
/** The base URL of the forwarder's API.
* When this is empty, the forwarder's default production API is used.
*/
baseUrl: string;
/** The domain part of the generated email address.
* @remarks The domain should be authorized by the forwarder before
* submitting a request through bitwarden.
* @example If the domain is `domain.io` and the generated username
* is `jd`, then the generated email address will be `jd@domain.io`
*/
domain: string;
/** A prefix joined to the generated email address' username.
* @example If the prefix is `foo`, the generated username is `bar`,
* and the domain is `domain.io`, then the generated email address is `
* then the generated username is `foobar@domain.io`.
*/
prefix: string;
};
/** Forwarder-specific static definition */
export type ForwarderMetadata = GeneratorMetadata<ForwarderSettings, NoPolicy> & {
/** createForwardingEmail RPC definition */
createForwardingEmail: RpcConfiguration<
GenerationRequest,
ForwarderContext<ForwarderSettings>,
string
>;
/** getAccountId RPC definition; the response updates `accountId` which has a
* structural mixin type `RequestAccount`.
*/
getAccountId?: RpcConfiguration<GenerationRequest, ForwarderContext<ForwarderSettings>, string>;
};

View File

@@ -47,6 +47,7 @@ export type GeneratorMetadata<Options, Policy> = AlgorithmMetadata & {
};
};
/** Defines parameters for policy transformations */
policy: {
/** Combines multiple policies set by the administrative console into
* a single policy.