1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-16789] introduce extension metadata (#12717)

This commit is contained in:
✨ Audrey ✨
2025-01-15 10:47:02 -05:00
committed by GitHub
parent f6f4bc9d4b
commit e79dab8689
20 changed files with 1773 additions and 0 deletions

View File

@@ -0,0 +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

@@ -0,0 +1,104 @@
import { ExtensionSite } from "./extension-site";
import {
ExtensionMetadata,
ExtensionSet,
ExtensionPermission,
SiteId,
SiteMetadata,
VendorId,
VendorMetadata,
} from "./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.
*/
abstract registerSite: (meta: SiteMetadata) => this;
/** 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.
*/
abstract sites: () => { site: SiteMetadata; permission?: ExtensionPermission }[];
/** Get a site's metadata
* @param site identifies a site registration
* @return the site's metadata or `undefined` if the site isn't registered.
*/
abstract 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.
*/
abstract registerVendor: (meta: VendorMetadata) => this;
/** 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.
*/
abstract 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.
*/
abstract 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.
*/
abstract registerExtension: (meta: ExtensionMetadata) => this;
/** 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.
*/
abstract extension: (site: SiteId, vendor: VendorId) => ExtensionMetadata | undefined;
/** List all registered extensions and their permissions */
abstract 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.
*/
abstract setPermission: (set: ExtensionSet, permission: ExtensionPermission) => this;
/** Retrieves the current permission for the given extension set or `undefined` if
* a permission doesn't exist.
*/
abstract permission: (set: ExtensionSet) => ExtensionPermission | undefined;
/** Returns all registered extension rules. */
abstract permissions: () => { set: ExtensionSet; permission: ExtensionPermission }[];
/** 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.
*/
abstract build: (id: SiteId) => ExtensionSite | undefined;
}

View File

@@ -0,0 +1,20 @@
import { deepFreeze } from "../util";
import { ExtensionMetadata, SiteMetadata, VendorId } from "./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,24 @@
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 buildExtensionRegistry() {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
for (const site of Reflect.ownKeys(Extension) as string[]) {
registry.registerSite(Extension[site]);
}
for (const vendor of Vendors) {
registry.registerVendor(vendor);
}
for (const extension of VendorExtensions) {
registry.registerExtension(extension);
}
return registry;
}

View File

@@ -0,0 +1,12 @@
export { Site, Field, Permission } from "./data";
export {
SiteId,
FieldId,
VendorId,
ExtensionId,
ExtensionPermission,
SiteMetadata,
ExtensionMetadata,
VendorMetadata,
} from "./type";
export { ExtensionSite } from "./extension-site";

View File

@@ -0,0 +1,17 @@
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]: {
id: Site.forwarder,
availableFields: [Field.baseUrl, Field.domain, Field.prefix, Field.token],
},
};
export const AllowedPermissions: ReadonlyArray<keyof typeof Permission> = Object.freeze(
Object.values(Permission),
);

View File

@@ -0,0 +1,923 @@
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, VendorMetadata } from "./type";
import { Bitwarden } from "./vendor/bitwarden";
// 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, name: "Some Product" },
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: [],
});
// In the following tests, not-null assertions (`!`) indicate that
// the returned object should never be null or undefined given
// the conditions defined within the test case
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("ignores repeated extensions with nonregistered vendors", () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, []);
registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension);
// precondition: the vendor is already registered
expect(registry.extension(SomeSiteId, SomeVendorId)).toBeDefined();
const result = registry
.registerExtension({
...SomeExtension,
requestedFields: [Field.domain],
})
.extension(SomeSiteId, SomeVendorId);
expect(result).toEqual(SomeExtension);
});
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,286 @@
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,
VendorId,
VendorMetadata,
} from "./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 extensionsBySiteByVendor = 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 extensionsByVendor =
this.extensionsBySiteByVendor.get(meta.site.id) ?? new Map<VendorId, number>();
if (extensionsByVendor.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;
extensionsByVendor.set(vendor.id, index);
this.extensionsBySiteByVendor.set(site.id, extensionsByVendor);
return this;
}
extension(site: SiteId, vendor: VendorId): ExtensionMetadata | undefined {
const index = this.extensionsBySiteByVendor.get(site)?.get(vendor) ?? -1;
if (index < 0) {
return undefined;
} else {
return this.extensionRegistrations[index];
}
}
private getPermissions(site: SiteId, vendor: VendorId): ExtensionPermission[] {
const permissions = [
this.sitePermissions.get(site),
this.vendorPermissions.get(vendor),
this.allPermission,
// Need to cast away `undefined` because typescript isn't
// aware that the filter eliminates undefined elements
].filter((p) => !!p) as ExtensionPermission[];
return permissions;
}
extensions(): ReadonlyArray<{
extension: ExtensionMetadata;
permissions: ExtensionPermission[];
}> {
const extensions = [];
for (const extension of this.extensionRegistrations) {
const permissions = this.getPermissions(extension.site.id, extension.product.vendor.id);
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.extensionsBySiteByVendor.get(id)?.entries() ?? ([] as const);
for (const [vendor, index] of entries) {
const permissions = this.getPermissions(id, vendor);
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

@@ -0,0 +1,109 @@
import { Opaque } from "type-fest";
import { Site, Field, Permission } from "./data";
/** 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 FieldId = keyof typeof Field;
/** Identifies a vendor extending bitwarden */
export type VendorId = Opaque<"vendor", string>;
/** uniquely identifies an extension. */
export type ExtensionId = { site: SiteId; vendor: VendorId };
/** Permission levels for metadata. */
export type ExtensionPermission = keyof typeof Permission;
/** The capabilities and descriptive content for an extension */
export type SiteMetadata = {
/** Uniquely identifies the extension site. */
id: SiteId;
/** Lists the fields disclosed by the extension to the vendor */
availableFields: FieldId[];
};
/** The capabilities and descriptive content for an extension */
export type VendorMetadata = {
/** Uniquely identifies the vendor. */
id: VendorId;
/** Brand name of the service providing the extension. */
name: string;
};
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 extension'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 = TokenHeader &
(
| { selfHost: "never"; baseUrl: string }
| { selfHost: "maybe"; baseUrl: string }
| { 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 */
readonly site: Readonly<SiteMetadata>;
/** Product description */
readonly product: Readonly<ProductMetadata>;
/** Hosting provider capabilities required by the extension */
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.
*/
readonly requestedFields: ReadonlyArray<Readonly<FieldId>>;
};
/** Identifies a collection of extensions.
*/
export type ExtensionSet =
| {
/** A set of extensions sharing an extension point */
site: SiteId;
}
| {
/** A set of extensions sharing a vendor */
vendor: VendorId;
}
| {
/** The total set of extensions. This is used to set a categorical
* rule affecting all extensions.
*/
all: true;
};

View File

@@ -0,0 +1,25 @@
import { Field } from "../data";
import { Extension } from "../metadata";
import { ExtensionMetadata, VendorMetadata } from "../type";
import { Vendor } from "./data";
export const AddyIo: VendorMetadata = {
id: Vendor.addyio,
name: "Addy.io",
};
export const AddyIoExtensions: ExtensionMetadata[] = [
{
site: Extension.forwarder,
product: {
vendor: AddyIo,
},
host: {
authorization: "bearer",
selfHost: "maybe",
baseUrl: "https://app.addy.io",
},
requestedFields: [Field.token, Field.baseUrl, Field.domain],
},
];

View File

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

View File

@@ -0,0 +1,11 @@
import { VendorId } from "../type";
export const Vendor = Object.freeze({
addyio: "addyio" as VendorId,
bitwarden: "bitwarden" as VendorId, // RESERVED
duckduckgo: "duckduckgo" as VendorId,
fastmail: "fastmail" as VendorId,
forwardemail: "forwardemail" as VendorId,
mozilla: "mozilla" as VendorId,
simplelogin: "simplelogin" as VendorId,
} as const);

View File

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

View File

@@ -0,0 +1,25 @@
import { Field } from "../data";
import { Extension } from "../metadata";
import { ExtensionMetadata, VendorMetadata } from "../type";
import { Vendor } from "./data";
export const Fastmail: VendorMetadata = {
id: Vendor.fastmail,
name: "Fastmail",
};
export const FastmailExtensions: ExtensionMetadata[] = [
{
site: Extension.forwarder,
product: {
vendor: Fastmail,
},
host: {
authorization: "bearer",
selfHost: "maybe",
baseUrl: "https://api.fastmail.com",
},
requestedFields: [Field.token],
},
];

View File

@@ -0,0 +1,25 @@
import { Field } from "../data";
import { Extension } from "../metadata";
import { ExtensionMetadata, VendorMetadata } from "../type";
import { Vendor } from "./data";
export const ForwardEmail: VendorMetadata = {
id: Vendor.forwardemail,
name: "Forward Email",
};
export const ForwardEmailExtensions: ExtensionMetadata[] = [
{
site: Extension.forwarder,
product: {
vendor: ForwardEmail,
},
host: {
authorization: "basic-username",
selfHost: "never",
baseUrl: "https://api.forwardemail.net",
},
requestedFields: [Field.domain, Field.token],
},
];

View File

@@ -0,0 +1,30 @@
import { deepFreeze } from "../../util";
import { AddyIo, AddyIoExtensions } from "./addyio";
import { Bitwarden } from "./bitwarden";
import { DuckDuckGo, DuckDuckGoExtensions } from "./duckduckgo";
import { Fastmail, FastmailExtensions } from "./fastmail";
import { ForwardEmail, ForwardEmailExtensions } from "./forwardemail";
import { Mozilla, MozillaExtensions } from "./mozilla";
import { SimpleLogin, SimpleLoginExtensions } from "./simplelogin";
export const Vendors = deepFreeze([
AddyIo,
Bitwarden,
DuckDuckGo,
Fastmail,
ForwardEmail,
Mozilla,
SimpleLogin,
]);
export const VendorExtensions = deepFreeze(
[
AddyIoExtensions,
DuckDuckGoExtensions,
FastmailExtensions,
ForwardEmailExtensions,
MozillaExtensions,
SimpleLoginExtensions,
].flat(),
);

View File

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

View File

@@ -0,0 +1,33 @@
# Vendors
This folder contains vendor-specific logic that extends the
Bitwarden password manager.
## Vendor IDs
A vendor's ID is used to identify and trace the code provided by
a vendor across Bitwarden. There are a few rules that vendor ids
must follow:
1. They should be human-readable. (No UUIDs.)
2. They may only contain lowercase ASCII characters and numbers.
3. They must retain backwards compatibility with prior versions.
As such, any given ID may not not match the vendor's present
brand identity. Said branding may be stored in `VendorMetadata.name`.
## Core files
There are 4 vendor-independent files in this directory.
- `data.ts` - core metadata used for system initialization
- `index.ts` - exports vendor metadata
- `README.md` - this file
## Vendor definitions
Each vendor should have one and only one definition, whose name
MUST match their `VendorId`. The vendor is free to use either a
single file (e.g. `bitwarden.ts`) or a folder containing multiple
files (e.g. `bitwarden/extension.ts`, `bitwarden/forwarder.ts`) to
host their files.

View File

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

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);
}