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:
26
libs/common/src/tools/extension/data.ts
Normal file
26
libs/common/src/tools/extension/data.ts
Normal 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);
|
||||
@@ -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;
|
||||
}
|
||||
20
libs/common/src/tools/extension/extension-site.ts
Normal file
20
libs/common/src/tools/extension/extension-site.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
24
libs/common/src/tools/extension/factory.ts
Normal file
24
libs/common/src/tools/extension/factory.ts
Normal 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;
|
||||
}
|
||||
12
libs/common/src/tools/extension/index.ts
Normal file
12
libs/common/src/tools/extension/index.ts
Normal 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";
|
||||
17
libs/common/src/tools/extension/metadata.ts
Normal file
17
libs/common/src/tools/extension/metadata.ts
Normal 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),
|
||||
);
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
286
libs/common/src/tools/extension/runtime-extension-registry.ts
Normal file
286
libs/common/src/tools/extension/runtime-extension-registry.ts
Normal 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");
|
||||
}
|
||||
109
libs/common/src/tools/extension/type.ts
Normal file
109
libs/common/src/tools/extension/type.ts
Normal 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;
|
||||
};
|
||||
25
libs/common/src/tools/extension/vendor/addyio.ts
vendored
Normal file
25
libs/common/src/tools/extension/vendor/addyio.ts
vendored
Normal 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],
|
||||
},
|
||||
];
|
||||
8
libs/common/src/tools/extension/vendor/bitwarden.ts
vendored
Normal file
8
libs/common/src/tools/extension/vendor/bitwarden.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import { VendorMetadata } from "../type";
|
||||
|
||||
import { Vendor } from "./data";
|
||||
|
||||
export const Bitwarden: VendorMetadata = Object.freeze({
|
||||
id: Vendor.bitwarden,
|
||||
name: "Bitwarden",
|
||||
});
|
||||
11
libs/common/src/tools/extension/vendor/data.ts
vendored
Normal file
11
libs/common/src/tools/extension/vendor/data.ts
vendored
Normal 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);
|
||||
25
libs/common/src/tools/extension/vendor/duckduckgo.ts
vendored
Normal file
25
libs/common/src/tools/extension/vendor/duckduckgo.ts
vendored
Normal 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],
|
||||
},
|
||||
];
|
||||
25
libs/common/src/tools/extension/vendor/fastmail.ts
vendored
Normal file
25
libs/common/src/tools/extension/vendor/fastmail.ts
vendored
Normal 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],
|
||||
},
|
||||
];
|
||||
25
libs/common/src/tools/extension/vendor/forwardemail.ts
vendored
Normal file
25
libs/common/src/tools/extension/vendor/forwardemail.ts
vendored
Normal 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],
|
||||
},
|
||||
];
|
||||
30
libs/common/src/tools/extension/vendor/index.ts
vendored
Normal file
30
libs/common/src/tools/extension/vendor/index.ts
vendored
Normal 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(),
|
||||
);
|
||||
26
libs/common/src/tools/extension/vendor/mozilla.ts
vendored
Normal file
26
libs/common/src/tools/extension/vendor/mozilla.ts
vendored
Normal 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],
|
||||
},
|
||||
];
|
||||
33
libs/common/src/tools/extension/vendor/readme.md
vendored
Normal file
33
libs/common/src/tools/extension/vendor/readme.md
vendored
Normal 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.
|
||||
25
libs/common/src/tools/extension/vendor/simplelogin.ts
vendored
Normal file
25
libs/common/src/tools/extension/vendor/simplelogin.ts
vendored
Normal 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],
|
||||
},
|
||||
];
|
||||
19
libs/common/src/tools/util.ts
Normal file
19
libs/common/src/tools/util.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user