Skip to content

Commit

Permalink
[PM-16789] introduce extension metadata (#12717)
Browse files Browse the repository at this point in the history
  • Loading branch information
audreyality authored Jan 15, 2025
1 parent f6f4bc9 commit e79dab8
Show file tree
Hide file tree
Showing 20 changed files with 1,773 additions and 0 deletions.
26 changes: 26 additions & 0 deletions libs/common/src/tools/extension/data.ts
Original file line number Diff line number Diff line change
@@ -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);
104 changes: 104 additions & 0 deletions libs/common/src/tools/extension/extension-registry.abstraction.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions libs/common/src/tools/extension/extension-site.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 24 additions & 0 deletions libs/common/src/tools/extension/factory.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 12 additions & 0 deletions libs/common/src/tools/extension/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 17 additions & 0 deletions libs/common/src/tools/extension/metadata.ts
Original file line number Diff line number Diff line change
@@ -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),
);
Loading

0 comments on commit e79dab8

Please sign in to comment.