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

unit test extension registry

This commit is contained in:
✨ Audrey ✨
2024-12-31 11:50:26 -05:00
parent 6c84f21d77
commit 55476cecb0
12 changed files with 1300 additions and 220 deletions

View File

@@ -1,10 +1,26 @@
/** well-known name for a feature extensible through an extension. */
export const Site = Object.freeze({
forwarder: "forwarder",
} as const);
/** well-known name for a field surfaced from an extension site to a vendor. */
export const Field = Object.freeze({
token: "token",
baseUrl: "baseUrl",
domain: "domain",
prefix: "prefix",
} as const);
/** Permission levels for metadata. */
export const Permission = Object.freeze({
/** unless a rule denies access, allow it. If a permission is `null`
* or `undefined` it should be treated as `Permission.default`.
*/
default: "default",
/** unless a rule allows access, deny it. */
none: "none",
/** access is explicitly granted to use an extension. */
allow: "allow",
/** access is explicitly prohibited for this extension. This rule overrides allow rules. */
deny: "deny",
} as const);

View File

@@ -1,166 +0,0 @@
import { ExtensionPermission, ExtensionRegistry } from "./extension-registry.abstraction";
import { ExtensionSite } from "./extension-site";
import { SiteMetadata, SiteId, ExtensionMetadata, ExtensionSet } from "./type";
import { VendorId, VendorMetadata } from "./vendor/type";
/** Tracks extension sites and the vendors that extend them. */
export class DefaultExtensionRegistry implements ExtensionRegistry {
private allRule: ExtensionPermission = "default";
private siteRegistrations = new Map<SiteId, SiteMetadata>();
private sitePermissions = new Map<SiteId, ExtensionPermission>();
private vendorRegistrations = new Map<VendorId, VendorMetadata>();
private vendorPermissions = new Map<VendorId, ExtensionPermission>();
private extensions = new Array<ExtensionMetadata>();
private vendorExtensionsBySite = 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.sitePermissions.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.vendorPermissions.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.vendorPermissions.set(set.vendor, permission);
} else if ("site" in set) {
this.sitePermissions.set(set.site, permission);
} else {
throw new Error(`Unrecognized extension 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.vendorPermissions.get(set.vendor);
} else if ("site" in set) {
return this.sitePermissions.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.sitePermissions.entries()) {
rules.push({ set: { site }, permission });
}
for (const [vendor, permission] of this.vendorPermissions.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 extension registered?
const vendorMap = this.vendorExtensionsBySite.get(meta.site.id) ?? new Map<VendorId, number>();
if (vendorMap.has(meta.product.vendor.id)) {
return;
}
// if not, register it
const index = this.extensions.push(meta) - 1;
vendorMap.set(meta.product.vendor.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.vendorExtensionsBySite.get(id)?.entries() ?? ([] as const);
for (const [vendor, maybeIndex] of entries) {
// prepare rules
const vendorRule = this.vendorPermissions.get(vendor) ?? this.allRule;
const siteRule = this.sitePermissions.get(id) ?? this.allRule;
const rules = [vendorRule, siteRule, this.allRule];
// evaluate rules
const extension = rules.includes("deny")
? undefined
: rules.includes("allow")
? this.extensions[maybeIndex]
: rules.includes("none")
? undefined
: rules.includes("default")
? this.extensions[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

@@ -1,62 +1,88 @@
import { ExtensionSite } from "./extension-site";
import { SiteMetadata, ExtensionMetadata, ExtensionSet, SiteId } from "./type";
import { VendorMetadata } from "./vendor/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 extension.
* * deny - access is explicitly prohibited for this extension. This
* rule overrides allow rules.
*/
export type ExtensionPermission = "default" | "none" | "allow" | "deny";
import { ExtensionMetadata, ExtensionSet, ExtensionPermission, SiteId, SiteMetadata } from "./type";
import { VendorId, VendorMetadata } from "./vendor/type";
/** Tracks extension sites and the vendors that extend them. */
export abstract class ExtensionRegistry {
/** Registers a site supporting extensibility.
* Each site may only be registered once. Calls after the first for
* the same SiteId have no effect.
* @param site identifies the site being extended
* @param meta configures the extension site
* @return self for method chaining.
* @remarks The registry initializes with a set of allowed sites and fields.
* `registerSite` drops a registration and trims its allowed fields to only
* those indicated in the allow list.
*/
registerSite: (meta: SiteMetadata) => this;
/** List all registered extension sites with their extension rule, if any.
* @returns a list of all extension sites. `rule` is defined when the site
* is associated with an extension rule.
/** List all registered extension sites with their extension permission, if any.
* @returns a list of all extension sites. `permission` is defined when the site
* is associated with an extension permission.
*/
sites: () => { site: SiteMetadata; permission?: ExtensionPermission }[];
/** Registers a vendor providing an extension
/** Get a site's metadata
* @param site identifies a site registration
* @return the site's metadata or `undefined` if the site isn't registered.
*/
site: (site: SiteId) => SiteMetadata | undefined;
/** Registers a vendor providing an extension.
* Each vendor may only be registered once. Calls after the first for
* the same VendorId have no effect.
* @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 extension rule, if any.
* @returns a list of all extension sites. `rule` is defined when the site
* is associated with an extension rule.
/** List all registered vendors with their permissions, if any.
* @returns a list of all extension sites. `permission` is defined when the site
* is associated with an extension permission.
*/
vendors: () => { vendor: VendorMetadata; permission?: ExtensionPermission }[];
/** Get a vendor's metadata
* @param site identifies a vendor registration
* @return the vendor's metadata or `undefined` if the vendor isn't registered.
*/
vendor: (vendor: VendorId) => VendorMetadata | undefined;
/** Registers an extension provided by a vendor to an extension site.
* The vendor and site MUST be registered before the extension.
* Each extension may only be registered once. Calls after the first for
* the same SiteId and VendorId have no effect.
* @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 extension set.
* The last-registered rule wins.
* @param set the collection of extensions affected by the rule
/** Get an extensions metadata
* @param site identifies the extension's site
* @param vendor identifies the extension's vendor
* @return the extension's metadata or `undefined` if the extension isn't registered.
*/
extension: (site: SiteId, vendor: VendorId) => ExtensionMetadata | undefined;
/** List all registered extensions and their permissions */
extensions: () => ReadonlyArray<{
extension: ExtensionMetadata;
permissions: ExtensionPermission[];
}>;
/** Registers a permission. Only 1 permission can be registered for each extension set.
* Calls after the first *replace* the registered permission.
* @param set the collection of extensions affected by the permission
* @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 extension set or undefined if a rule
* doesn't exist. */
/** Retrieves the current permission for the given extension set or `undefined` if
* a permission doesn't exist.
*/
permission: (set: ExtensionSet) => ExtensionPermission | undefined;
/** Returns all registered extension rules. */
@@ -64,6 +90,7 @@ export abstract class ExtensionRegistry {
/** Creates a point-in-time snapshot of the registry's contents with extension
* permissions applied for the provided SiteId.
* @param id identifies the extension site to create.
* @returns the extension site, or `undefined` if the site is not registered.
*/
build: (id: SiteId) => ExtensionSite | undefined;

View File

@@ -2,11 +2,11 @@ import { StateProvider } from "@bitwarden/common/platform/state";
import { LegacyEncryptorProvider } from "../cryptography/legacy-encryptor-provider";
import { DefaultExtensionRegistry } from "./default-extension-registry";
import { RuntimeExtensionRegistry } from "./runtime-extension-registry";
export class ExtensionService {
constructor(
private readonly registry: DefaultExtensionRegistry,
private readonly registry: RuntimeExtensionRegistry,
private readonly stateProvider: StateProvider,
private readonly encryptorProvider: LegacyEncryptorProvider,
) {}

View File

@@ -1,12 +1,12 @@
import { DefaultExtensionRegistry } from "./default-extension-registry";
import { Extension } from "./metadata";
import { DefaultFields, DefaultSites, Extension } from "./metadata";
import { RuntimeExtensionRegistry } from "./runtime-extension-registry";
import { VendorExtensions, Vendors } from "./vendor";
// FIXME: find a better way to build the registry than a hard-coded factory function
/** Constructs the extension registry */
export function buildRegistry() {
const registry = new DefaultExtensionRegistry();
export function buildExtensionRegistry() {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
for (const site of Reflect.ownKeys(Extension) as string[]) {
registry.registerSite(Extension[site]);

View File

@@ -1,5 +1,9 @@
import { Field, Site } from "./data";
import { SiteMetadata } from "./type";
import { Field, Site, Permission } from "./data";
import { FieldId, SiteId, SiteMetadata } from "./type";
export const DefaultSites: SiteId[] = Object.freeze(Object.keys(Site) as any);
export const DefaultFields: FieldId[] = Object.freeze(Object.keys(Field) as any);
export const Extension: Record<string, SiteMetadata> = {
[Site.forwarder]: {
@@ -8,6 +12,6 @@ export const Extension: Record<string, SiteMetadata> = {
},
};
export const FieldsBySite = {
[Site.forwarder]: [Field.token, Field.baseUrl, Field.domain, Field.prefix],
} as const;
export const AllowedPermissions: ReadonlyArray<keyof typeof Permission> = Object.freeze(
Object.values(Permission),
);

View File

@@ -0,0 +1,904 @@
import { deepFreeze } from "../util";
import { Field, Site, Permission } from "./data";
import { ExtensionSite } from "./extension-site";
import { DefaultFields, DefaultSites } from "./metadata";
import { RuntimeExtensionRegistry } from "./runtime-extension-registry";
import { ExtensionMetadata, SiteId, SiteMetadata } from "./type";
import { Bitwarden } from "./vendor/bitwarden";
import { VendorMetadata } from "./vendor/type";
// arbitrary test entities
const SomeSiteId: SiteId = Site.forwarder;
const SomeSite: SiteMetadata = Object.freeze({
id: SomeSiteId,
availableFields: [],
});
const SomeVendor = Bitwarden;
const SomeVendorId = SomeVendor.id;
const SomeExtension: ExtensionMetadata = deepFreeze({
site: SomeSite,
product: { vendor: SomeVendor },
host: { authorization: "bearer", selfHost: "maybe", baseUrl: "https://vault.bitwarden.com" },
requestedFields: [],
});
const JustTrustUs: VendorMetadata = Object.freeze({
id: "justrustus" as any,
name: "JustTrust.Us",
});
const JustTrustUsExtension: ExtensionMetadata = deepFreeze({
site: SomeSite,
product: { vendor: JustTrustUs },
host: { authorization: "bearer", selfHost: "maybe", baseUrl: "https://justrust.us" },
requestedFields: [],
});
describe("RuntimeExtensionRegistry", () => {
describe("registerSite", () => {
it("registers an extension site", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
const result = registry.registerSite(SomeSite).site(SomeSiteId);
expect(result).toEqual(SomeSite);
});
it("interns the site", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
const result = registry.registerSite(SomeSite).site(SomeSiteId);
expect(result).not.toBe(SomeSite);
});
it("registers an extension site with fields", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
const site: SiteMetadata = {
...SomeSite,
availableFields: [Field.baseUrl],
};
const result = registry.registerSite(site).site(SomeSiteId);
expect(result).toEqual(site);
});
it("ignores unavailable sites", () => {
const registry = new RuntimeExtensionRegistry([], []);
const ignored: SiteMetadata = {
id: "an-unavailable-site" as any,
availableFields: [],
};
const result = registry.registerSite(ignored).sites();
expect(result).toEqual([]);
});
it("ignores duplicate registrations", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
const ignored: SiteMetadata = {
...SomeSite,
availableFields: [Field.token],
};
const result = registry.registerSite(SomeSite).registerSite(ignored).site(SomeSiteId);
expect(result).toEqual(SomeSite);
});
it("ignores unknown available fields", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
const ignored: SiteMetadata = {
...SomeSite,
availableFields: [SomeSite.availableFields, "ignored" as any],
};
const { availableFields } = registry.registerSite(ignored).site(SomeSiteId);
expect(availableFields).toEqual(SomeSite.availableFields);
});
it("freezes the site definition", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
const site = registry.registerSite(SomeSite).site(SomeSiteId);
// reassigning `availableFields` throws b/c the object is frozen
expect(() => (site.availableFields = [Field.domain])).toThrow();
});
});
describe("site", () => {
it("returns `undefined` for an unknown site", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
const result = registry.site(SomeSiteId);
expect(result).toBeUndefined();
});
it("returns the same result when called repeatedly", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
registry.registerSite(SomeSite);
const first = registry.site(SomeSiteId);
const second = registry.site(SomeSiteId);
expect(first).toBe(second);
});
});
describe("sites", () => {
it("lists registered sites", () => {
const registry = new RuntimeExtensionRegistry([SomeSiteId, "bar"] as any[], DefaultFields);
const barSite: SiteMetadata = {
id: "bar" as any,
availableFields: [],
};
const result = registry.registerSite(SomeSite).registerSite(barSite).sites();
expect(result.some(({ site }) => site.id === SomeSiteId)).toBeTrue();
expect(result.some(({ site }) => site.id === barSite.id)).toBeTrue();
});
it("includes permissions for a site", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
const result = registry
.registerSite(SomeSite)
.setPermission({ site: SomeSite.id }, Permission.allow)
.sites();
expect(result).toEqual([{ site: SomeSite, permission: Permission.allow }]);
});
it("ignores duplicate registrations", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
const ignored: SiteMetadata = {
...SomeSite,
availableFields: [Field.token],
};
const result = registry.registerSite(SomeSite).registerSite(ignored).sites();
expect(result).toEqual([{ site: SomeSite }]);
});
it("ignores permissions for other sites", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
const result = registry
.registerSite(SomeSite)
.setPermission({ site: SomeSite.id }, Permission.allow)
.setPermission({ site: "bar" as any }, Permission.deny)
.sites();
expect(result).toEqual([{ site: SomeSite, permission: Permission.allow }]);
});
});
describe("registerVendor", () => {
it("registers a vendor", () => {
const registry = new RuntimeExtensionRegistry([], []);
const result = registry.registerVendor(SomeVendor).vendors();
expect(result).toEqual([{ vendor: SomeVendor }]);
});
it("freezes the vendor definition", () => {
const registry = new RuntimeExtensionRegistry([], []);
// copy `SomeVendor` because it is already frozen
const original: VendorMetadata = { ...SomeVendor };
const [{ vendor }] = registry.registerVendor(original).vendors();
// reassigning `name` throws b/c the object is frozen
expect(() => (vendor.name = "Bytewarden")).toThrow();
});
});
describe("vendor", () => {
it("returns `undefined` for an unknown site", () => {
const registry = new RuntimeExtensionRegistry([], []);
const result = registry.vendor(SomeVendorId);
expect(result).toBeUndefined();
});
it("returns the same result when called repeatedly", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
registry.registerVendor(SomeVendor);
const first = registry.vendor(SomeVendorId);
const second = registry.vendor(SomeVendorId);
expect(first).toBe(second);
});
});
describe("vendors", () => {
it("lists registered vendors", () => {
const registry = new RuntimeExtensionRegistry([], []);
registry.registerVendor(SomeVendor).registerVendor(JustTrustUs);
const result = registry.vendors();
expect(result.some(({ vendor }) => vendor.id === SomeVendorId)).toBeTrue();
expect(result.some(({ vendor }) => vendor.id === JustTrustUs.id)).toBeTrue();
});
it("includes permissions for a vendor", () => {
const registry = new RuntimeExtensionRegistry([], []);
const result = registry
.registerVendor(SomeVendor)
.setPermission({ vendor: SomeVendorId }, Permission.allow)
.vendors();
expect(result).toEqual([{ vendor: SomeVendor, permission: Permission.allow }]);
});
it("ignores duplicate registrations", () => {
const registry = new RuntimeExtensionRegistry([], []);
const vendor: VendorMetadata = SomeVendor;
const ignored: VendorMetadata = {
...SomeVendor,
name: "Duplicate",
};
const result = registry.registerVendor(vendor).registerVendor(ignored).vendors();
expect(result).toEqual([{ vendor }]);
});
it("ignores permissions for other sites", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
registry.registerVendor(SomeVendor).setPermission({ vendor: SomeVendorId }, Permission.allow);
const result = registry.setPermission({ vendor: JustTrustUs.id }, Permission.deny).vendors();
expect(result).toEqual([{ vendor: SomeVendor, permission: Permission.allow }]);
});
});
describe("setPermission", () => {
it("sets the all permission", () => {
const registry = new RuntimeExtensionRegistry([], []);
const target = { all: true } as const;
const permission = registry.setPermission(target, Permission.allow).permission(target);
expect(permission).toEqual(Permission.allow);
});
it("sets a vendor permission", () => {
const registry = new RuntimeExtensionRegistry([], []);
const target = { vendor: SomeVendorId };
const permission = registry.setPermission(target, Permission.allow).permission(target);
expect(permission).toEqual(Permission.allow);
});
it("sets a site permission", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
const target = { site: SomeSiteId };
const permission = registry.setPermission(target, Permission.allow).permission(target);
expect(permission).toEqual(Permission.allow);
});
it("ignores a site permission unless it is in the allowed sites list", () => {
const registry = new RuntimeExtensionRegistry([], []);
const target = { site: SomeSiteId };
const permission = registry.setPermission(target, Permission.allow).permission(target);
expect(permission).toBeUndefined();
});
it("throws when a permission is invalid", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
expect(() => registry.setPermission({ all: true }, "invalid" as any)).toThrow();
});
it("throws when the extension set is the wrong type", () => {
const registry = new RuntimeExtensionRegistry([], []);
const target = { invalid: "invalid" } as any;
expect(() => registry.setPermission(target, Permission.allow)).toThrow();
});
});
describe("permission", () => {
it("gets the default all permission", () => {
const registry = new RuntimeExtensionRegistry([], []);
const target = { all: true } as const;
const permission = registry.permission(target);
expect(permission).toEqual(Permission.default);
});
it("gets an all permission", () => {
const registry = new RuntimeExtensionRegistry([], []);
const target = { all: true } as const;
registry.setPermission(target, Permission.none);
const permission = registry.permission(target);
expect(permission).toEqual(Permission.none);
});
it("gets a vendor permission", () => {
const registry = new RuntimeExtensionRegistry([], []);
const target = { vendor: SomeVendorId };
registry.setPermission(target, Permission.allow);
const permission = registry.permission(target);
expect(permission).toEqual(Permission.allow);
});
it("gets a site permission", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
const target = { site: SomeSiteId };
registry.setPermission(target, Permission.allow);
const permission = registry.permission(target);
expect(permission).toEqual(Permission.allow);
});
it("gets a vendor permission", () => {
const registry = new RuntimeExtensionRegistry([], []);
const target = { vendor: SomeVendorId };
registry.setPermission(target, Permission.allow);
const permission = registry.permission(target);
expect(permission).toEqual(Permission.allow);
});
it("returns undefined when the extension set is the wrong type", () => {
const registry = new RuntimeExtensionRegistry([], []);
const target = { invalid: "invalid" } as any;
const permission = registry.permission(target);
expect(permission).toBeUndefined();
});
});
describe("permissions", () => {
it("returns a default all permission by default", () => {
const registry = new RuntimeExtensionRegistry([], []);
const permission = registry.permissions();
expect(permission).toEqual([{ set: { all: true }, permission: Permission.default }]);
});
it("returns the all permission", () => {
const registry = new RuntimeExtensionRegistry([], []);
registry.setPermission({ all: true }, Permission.none);
const permission = registry.permissions();
expect(permission).toEqual([{ set: { all: true }, permission: Permission.none }]);
});
it("includes site permissions", () => {
const registry = new RuntimeExtensionRegistry([SomeSiteId, "bar"] as any[], DefaultFields);
registry.registerSite(SomeSite).setPermission({ site: SomeSiteId }, Permission.allow);
registry
.registerSite({
id: "bar" as any,
availableFields: [],
})
.setPermission({ site: "bar" as any }, Permission.deny);
const result = registry.permissions();
expect(
result.some((p: any) => p.set.site === SomeSiteId && p.permission === Permission.allow),
).toBeTrue();
expect(
result.some((p: any) => p.set.site === "bar" && p.permission === Permission.deny),
).toBeTrue();
});
it("includes vendor permissions", () => {
const registry = new RuntimeExtensionRegistry([], DefaultFields);
registry.registerVendor(SomeVendor).setPermission({ vendor: SomeVendorId }, Permission.allow);
registry
.registerVendor(JustTrustUs)
.setPermission({ vendor: JustTrustUs.id }, Permission.deny);
const result = registry.permissions();
expect(
result.some((p: any) => p.set.vendor === SomeVendorId && p.permission === Permission.allow),
).toBeTrue();
expect(
result.some(
(p: any) => p.set.vendor === JustTrustUs.id && p.permission === Permission.deny,
),
).toBeTrue();
});
});
describe("registerExtension", () => {
it("registers an extension", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry.registerSite(SomeSite).registerVendor(SomeVendor);
const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId);
expect(result).toEqual(SomeExtension);
});
it("ignores extensions with nonregistered sites", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry.registerVendor(SomeVendor);
// precondition: the site is not registered
expect(registry.site(SomeSiteId)).toBeUndefined();
const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId);
expect(result).toBeUndefined();
});
it("ignores extensions with nonregistered vendors", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry.registerSite(SomeSite);
// precondition: the vendor is not registered
expect(registry.vendor(SomeVendorId)).toBeUndefined();
const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId);
expect(result).toBeUndefined();
});
it("interns site metadata", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry.registerSite(SomeSite).registerVendor(SomeVendor);
const internedSite = registry.site(SomeSiteId);
const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId);
expect(result.site).toBe(internedSite);
});
it("interns vendor metadata", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry.registerSite(SomeSite).registerVendor(SomeVendor);
const internedVendor = registry.vendor(SomeVendorId);
const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId);
expect(result.product.vendor).toBe(internedVendor);
});
it("freezes the extension metadata", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension);
const extension = registry.extension(SomeSiteId, SomeVendorId);
// field assignments & mutation functions throw b/c the object is frozen
expect(() => ((extension.site as any) = SomeSite)).toThrow();
expect(() => ((extension.product.vendor as any) = SomeVendor)).toThrow();
expect(() => ((extension.product.name as any) = "SomeVendor")).toThrow();
expect(() => ((extension.host as any) = {})).toThrow();
expect(() => ((extension.host.selfHost as any) = {})).toThrow();
expect(() => ((extension.host as any).authorization = "basic")).toThrow();
expect(() => ((extension.host as any).baseUrl = "https://www.example.com")).toThrow();
expect(() => ((extension.requestedFields as any) = [Field.baseUrl])).toThrow();
expect(() => (extension.requestedFields as any).push(Field.baseUrl)).toThrow();
});
});
describe("extension", () => {
describe("extension", () => {
it("returns `undefined` for an unknown extension", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
const result = registry.extension(SomeSiteId, SomeVendorId);
expect(result).toBeUndefined();
});
it("interns the extension", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension);
const first = registry.extension(SomeSiteId, SomeVendorId);
const second = registry.extension(SomeSiteId, SomeVendorId);
expect(first).toBe(second);
});
});
describe("extensions", () => {
it("lists registered extensions", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
registry.registerSite(SomeSite);
registry.registerVendor(SomeVendor).registerExtension(SomeExtension);
registry.registerVendor(JustTrustUs).registerExtension(JustTrustUsExtension);
const result = registry.extensions();
expect(
result.some(
({ extension }) =>
extension.site.id === SomeSiteId && extension.product.vendor.id === SomeVendorId,
),
).toBeTrue();
expect(
result.some(
({ extension }) =>
extension.site.id === SomeSiteId && extension.product.vendor.id === JustTrustUs.id,
),
).toBeTrue();
});
it("includes permissions for extensions", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension)
.setPermission({ vendor: SomeVendorId }, Permission.allow);
const result = registry.extensions();
expect(
result.some(
({ extension, permissions }) =>
extension.site.id === SomeSiteId &&
extension.product.vendor.id === SomeVendorId &&
permissions.includes(Permission.allow),
),
).toBeTrue();
});
});
describe("build", () => {
it("builds an empty extension site when no extensions are registered", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry.registerSite(SomeSite).registerVendor(SomeVendor);
const result = registry.build(SomeSiteId);
expect(result.extensions.size).toBe(0);
});
it("builds an extension site with all registered extensions", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension);
const expected = registry.extension(SomeSiteId, SomeVendorId);
const result = registry.build(SomeSiteId);
expect(result).toBeInstanceOf(ExtensionSite);
expect(result.extensions.get(SomeVendorId)).toBe(expected);
});
it("returns `undefined` for an unknown site", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
const result = registry.build(SomeSiteId);
expect(result).toBeUndefined();
});
describe("when the all permission is `default`", () => {
const allPermission = Permission.default;
it("builds an extension site with all registered extensions", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension)
.setPermission({ all: true }, Permission.default);
const expected = registry.extension(SomeSiteId, SomeVendorId);
const result = registry.build(SomeSiteId);
expect(result.extensions.get(SomeVendorId)).toBe(expected);
});
it.each([[Permission.default], [Permission.allow]])(
"includes sites with `%p` permission",
(permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ site: SomeSiteId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension);
},
);
it.each([[Permission.none], [Permission.deny]])(
"ignores sites with `%p` permission",
(permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ site: SomeSiteId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.size).toBe(0);
},
);
it.each([[Permission.default], [Permission.allow]])(
"includes vendors with `%p` permission",
(permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ vendor: SomeVendorId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension);
},
);
it.each([[Permission.none], [Permission.deny]])(
"ignores vendors with `%p` permission",
(permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ vendor: SomeVendorId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.size).toBe(0);
},
);
});
describe("when the all permission is `none`", () => {
const allPermission = Permission.none;
it("builds an empty extension site", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension)
.setPermission({ all: true }, Permission.none);
const result = registry.build(SomeSiteId);
expect(result).toBeInstanceOf(ExtensionSite);
expect(result.extensions.size).toBe(0);
});
it.each([[Permission.allow]])("includes sites with `%p` permission", (permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ site: SomeSiteId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension);
});
it.each([[Permission.default], [Permission.none], [Permission.deny]])(
"ignores sites with `%p` permission",
(permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ site: SomeSiteId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.size).toBe(0);
},
);
it.each([[Permission.allow]])("includes vendors with `%p` permission", (permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ vendor: SomeVendorId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension);
});
it.each([[Permission.default], [Permission.none], [Permission.deny]])(
"ignores vendors with `%p` permission",
(permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ vendor: SomeVendorId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.size).toBe(0);
},
);
});
describe("when the all permission is `allow`", () => {
const allPermission = Permission.allow;
it("builds an extension site with all registered extensions", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension)
.setPermission({ all: true }, Permission.default);
const expected = registry.extension(SomeSiteId, SomeVendorId);
const result = registry.build(SomeSiteId);
expect(result).toBeInstanceOf(ExtensionSite);
expect(result.extensions.get(SomeVendorId)).toBe(expected);
});
it.each([[Permission.default], [Permission.none], [Permission.allow]])(
"includes sites with `%p` permission",
(permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ site: SomeSiteId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension);
},
);
it.each([[Permission.deny]])("ignores sites with `%p` permission", (permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ site: SomeSiteId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.size).toBe(0);
});
it.each([[Permission.default], [Permission.none], [Permission.allow]])(
"includes vendors with `%p` permission",
(permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ vendor: SomeVendorId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension);
},
);
it.each([[Permission.deny]])("ignores vendors with `%p` permission", (permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ vendor: SomeVendorId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.size).toBe(0);
});
});
describe("when the all permission is `deny`", () => {
const allPermission = Permission.deny;
it("builds an empty extension site", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension)
.setPermission({ all: true }, Permission.deny);
const result = registry.build(SomeSiteId);
expect(result).toBeInstanceOf(ExtensionSite);
expect(result.extensions.size).toBe(0);
});
it.each([[Permission.default], [Permission.none], [Permission.allow], [Permission.deny]])(
"ignores sites with `%p` permission",
(permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ site: SomeSiteId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.size).toBe(0);
},
);
it.each([[Permission.default], [Permission.none], [Permission.allow], [Permission.deny]])(
"ignores vendors with `%p` permission",
(permission) => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry
.registerSite(SomeSite)
.registerVendor(SomeVendor)
.registerExtension(SomeExtension);
registry.setPermission({ all: true }, allPermission);
registry.setPermission({ vendor: SomeVendorId }, permission);
const result = registry.build(SomeSiteId);
expect(result.extensions.size).toBe(0);
},
);
});
});
});
});

View File

@@ -0,0 +1,280 @@
import { deepFreeze } from "../util";
import { ExtensionRegistry } from "./extension-registry.abstraction";
import { ExtensionSite } from "./extension-site";
import { AllowedPermissions } from "./metadata";
import {
ExtensionMetadata,
ExtensionPermission,
ExtensionSet,
FieldId,
ProductMetadata,
SiteMetadata,
SiteId,
} from "./type";
import { VendorId, VendorMetadata } from "./vendor/type";
/** Tracks extension sites and the vendors that extend them in application memory. */
export class RuntimeExtensionRegistry implements ExtensionRegistry {
/** Instantiates the extension registry
* @param allowedSites sites that are valid for use by any extension;
* this is most useful to disable an extension site that is only
* available on a specific client.
* @param allowedFields fields that are valid for use by any extension;
* this is most useful to prohibit access to a field via policy.
*/
constructor(
private readonly allowedSites: SiteId[],
private readonly allowedFields: FieldId[],
) {
Object.freeze(this.allowedFields);
Object.freeze(this.allowedSites);
}
private allPermission: ExtensionPermission = "default";
private siteRegistrations = new Map<SiteId, SiteMetadata>();
private sitePermissions = new Map<SiteId, ExtensionPermission>();
private vendorRegistrations = new Map<VendorId, VendorMetadata>();
private vendorPermissions = new Map<VendorId, ExtensionPermission>();
private extensionRegistrations = new Array<ExtensionMetadata>();
private vendorExtensionsBySite = new Map<SiteId, Map<VendorId, number>>();
registerSite(site: SiteMetadata): this {
if (!this.allowedSites.includes(site.id)) {
return this;
}
// verify requested fields are on the list of valid fields to expose to
// an extension
const availableFields = site.availableFields.filter((field) =>
this.allowedFields.includes(field),
);
const validated: SiteMetadata = deepFreeze({ id: site.id, availableFields });
if (!this.siteRegistrations.has(site.id)) {
this.siteRegistrations.set(site.id, validated);
}
return this;
}
site(site: SiteId): SiteMetadata | undefined {
const result = this.siteRegistrations.get(site);
return result;
}
sites() {
const sites: { site: SiteMetadata; permission?: ExtensionPermission }[] = [];
for (const [k, site] of this.siteRegistrations.entries()) {
const s: (typeof sites)[number] = { site };
const permission = this.sitePermissions.get(k);
if (permission) {
s.permission = permission;
}
sites.push(s);
}
return sites;
}
registerVendor(vendor: VendorMetadata): this {
if (!this.vendorRegistrations.has(vendor.id)) {
const frozen = deepFreeze(vendor);
this.vendorRegistrations.set(vendor.id, frozen);
}
return this;
}
vendor(vendor: VendorId): VendorMetadata | undefined {
const result = this.vendorRegistrations.get(vendor);
return result;
}
vendors() {
const vendors: { vendor: VendorMetadata; permission?: ExtensionPermission }[] = [];
for (const [k, vendor] of this.vendorRegistrations.entries()) {
const s: (typeof vendors)[number] = { vendor };
const permission = this.vendorPermissions.get(k);
if (permission) {
s.permission = permission;
}
vendors.push(s);
}
return vendors;
}
setPermission(set: ExtensionSet, permission: ExtensionPermission): this {
if (!AllowedPermissions.includes(permission)) {
throw new Error(`invalid extension permission: ${permission}`);
}
if ("all" in set && set.all) {
this.allPermission = permission;
} else if ("vendor" in set) {
this.vendorPermissions.set(set.vendor, permission);
} else if ("site" in set) {
if (this.allowedSites.includes(set.site)) {
this.sitePermissions.set(set.site, permission);
}
} else {
throw new Error(`Unrecognized extension set received: ${JSON.stringify(set)}.`);
}
return this;
}
permission(set: ExtensionSet) {
if ("all" in set && set.all) {
return this.allPermission;
} else if ("vendor" in set) {
return this.vendorPermissions.get(set.vendor);
} else if ("site" in set) {
return this.sitePermissions.get(set.site);
} else {
return undefined;
}
}
permissions() {
const rules: { set: ExtensionSet; permission: ExtensionPermission }[] = [];
rules.push({ set: { all: true }, permission: this.allPermission });
for (const [site, permission] of this.sitePermissions.entries()) {
rules.push({ set: { site }, permission });
}
for (const [vendor, permission] of this.vendorPermissions.entries()) {
rules.push({ set: { vendor }, permission });
}
return rules;
}
registerExtension(meta: ExtensionMetadata): this {
const site = this.siteRegistrations.get(meta.site.id);
const vendor = this.vendorRegistrations.get(meta.product.vendor.id);
if (!site || !vendor) {
return this;
}
// exit early if the extension is already registered
const vendorMap = this.vendorExtensionsBySite.get(meta.site.id) ?? new Map<VendorId, number>();
if (vendorMap.has(meta.product.vendor.id)) {
return this;
}
// create immutable copy; this updates the vendor and site with
// their internalized representation to provide reference equality
// across registrations
const product: ProductMetadata = { vendor };
if (meta.product.name) {
product.name = meta.product.name;
}
const extension: ExtensionMetadata = Object.freeze({
site,
product: Object.freeze(product),
host: Object.freeze({ ...meta.host }),
requestedFields: Object.freeze([...meta.requestedFields]),
});
// register it
const index = this.extensionRegistrations.push(extension) - 1;
vendorMap.set(vendor.id, index);
this.vendorExtensionsBySite.set(site.id, vendorMap);
return this;
}
extension(site: SiteId, vendor: VendorId): ExtensionMetadata | undefined {
const index = this.vendorExtensionsBySite.get(site)?.get(vendor) ?? -1;
if (index < 0) {
return undefined;
} else {
return this.extensionRegistrations[index];
}
}
extensions(): ReadonlyArray<{
extension: ExtensionMetadata;
permissions: ExtensionPermission[];
}> {
const extensions = [];
for (const extension of this.extensionRegistrations) {
const permissions = [
this.vendorPermissions.get(extension.product.vendor.id),
this.sitePermissions.get(extension.site.id),
this.allPermission,
];
extensions.push({ extension, permissions });
}
return extensions;
}
build(id: SiteId): ExtensionSite | undefined {
const site = this.siteRegistrations.get(id);
if (!site) {
return undefined;
}
if (this.allPermission === "deny") {
return new ExtensionSite(site, new Map());
}
const extensions = new Map<VendorId, ExtensionMetadata>();
const entries = this.vendorExtensionsBySite.get(id)?.entries() ?? ([] as const);
for (const [vendor, index] of entries) {
const permissions = [
this.vendorPermissions.get(vendor),
this.sitePermissions.get(id),
this.allPermission,
];
const extension = evaluate(permissions, this.extensionRegistrations[index]);
if (extension) {
extensions.set(vendor, extension);
}
}
const extensionSite = new ExtensionSite(site, extensions);
return extensionSite;
}
}
function evaluate(
permissions: ExtensionPermission[],
value: ExtensionMetadata,
): ExtensionMetadata | undefined {
// deny always wins
if (permissions.includes("deny")) {
return undefined;
}
// allow overrides implicit permissions
if (permissions.includes("allow")) {
return value;
}
// none permission becomes a deny
if (permissions.includes("none")) {
return undefined;
}
// default permission becomes an allow
if (permissions.includes("default")) {
return value;
}
// if no permission is recognized, throw. This code is unreachable.
throw new Error("failed to recognize any permissions");
}

View File

@@ -1,12 +1,14 @@
import { Site } from "./data";
import { FieldsBySite } from "./metadata";
import { Site, Field, Permission } from "./data";
import { VendorId, VendorMetadata } from "./vendor/type";
/** well-known name for a feature extensible through an extension. */
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)[SiteId][number];
export type FieldId = keyof typeof Field;
/** Permission levels for metadata. */
export type ExtensionPermission = keyof typeof Permission;
/** The capabilities and descriptive content for an extension */
export type SiteMetadata = {
@@ -14,7 +16,7 @@ export type SiteMetadata = {
id: SiteId;
/** Lists the fields disclosed by the extension to the vendor */
availableFields: DisclosedField[];
availableFields: FieldId[];
};
type TokenHeader =
@@ -45,28 +47,31 @@ export type ApiHost = TokenHeader &
| { selfHost: "always" }
);
/** Describes a branded product */
export type ProductMetadata = {
/** The vendor providing the extension */
vendor: VendorMetadata;
/** The branded name of the product, if it varies from the Vendor name */
name?: string;
};
/** Describes an extension provided by a vendor */
export type ExtensionMetadata = {
/** The part of Bitwarden extended by the vendor's services */
site: SiteMetadata;
readonly site: Readonly<SiteMetadata>;
/** Product description */
product: {
/** The vendor providing the extension */
vendor: VendorMetadata;
/** The branded name of the product, if it varies from the Vendor name */
name?: string;
};
readonly product: Readonly<ProductMetadata>;
/** Hosting provider capabilities required by the extension */
host: ApiHost;
readonly host: Readonly<ApiHost>;
/** Lists the fields disclosed by the extension to the vendor.
* This should be a subset of the `availableFields` listed in
* the extension.
*/
requestedFields: DisclosedField[];
readonly requestedFields: ReadonlyArray<Readonly<FieldId>>;
};
/** Identifies a collection of extensions.

View File

@@ -0,0 +1,7 @@
import { Vendor } from "./data";
import { VendorMetadata } from "./type";
export const Bitwarden: VendorMetadata = Object.freeze({
id: Vendor.bitwarden,
name: "Bitwarden",
});

View File

@@ -1,8 +1,9 @@
export const Vendor = Object.freeze({
addyio: "addyio",
bitwarden: "bitwarden", // RESERVED
duckduckgo: "duckduckgo",
fastmail: "fastmail",
mozilla: "mozilla",
forwardemail: "forwardemail",
mozilla: "mozilla",
simplelogin: "simplelogin",
} as const);

View File

@@ -1,14 +1,16 @@
import { deepFreeze } from "../../util";
import { AddyIo, AddyIoExtensions } from "./addy-io";
import { DuckDuckGo, DuckDuckGoExtensions } from "./duck-duck-go";
import { AddyIo, AddyIoExtensions } from "./addyio";
import { Bitwarden } from "./bitwarden";
import { DuckDuckGo, DuckDuckGoExtensions } from "./duckduckgo";
import { Fastmail, FastmailExtensions } from "./fastmail";
import { ForwardEmail, ForwardEmailExtensions } from "./forward-email";
import { ForwardEmail, ForwardEmailExtensions } from "./forwardemail";
import { Mozilla, MozillaExtensions } from "./mozilla";
import { SimpleLogin, SimpleLoginExtensions } from "./simple-login";
import { SimpleLogin, SimpleLoginExtensions } from "./simplelogin";
export const Vendors = deepFreeze([
AddyIo,
Bitwarden,
DuckDuckGo,
Fastmail,
ForwardEmail,