1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 05:00:10 +00:00

add documentation

This commit is contained in:
✨ Audrey ✨
2025-02-27 12:45:01 -05:00
parent 3f40d72b5c
commit 86324e5744
6 changed files with 46 additions and 32 deletions

View File

@@ -1,4 +1,4 @@
import { EMPTY, Observable, defer, of, shareReplay } from "rxjs";
import { shareReplay } from "rxjs";
import { Account } from "../../auth/abstractions/account.service";
import { BoundDependency } from "../dependencies";
@@ -7,7 +7,6 @@ import { UserStateSubject } from "../state/user-state-subject";
import { UserStateSubjectDependencyProvider } from "../state/user-state-subject-dependency-provider";
import { ExtensionRegistry } from "./extension-registry.abstraction";
import { ExtensionSite } from "./extension-site";
import { ExtensionProfileMetadata, SiteId, VendorId } from "./type";
import { toObjectKey } from "./util";
@@ -15,6 +14,10 @@ import { toObjectKey } from "./util";
* These extensions integrate 3rd party services into Bitwarden.
*/
export class ExtensionService {
/** Instantiate the extension service.
* @param registry provides runtime status for extension sites
* @param providers provide persistent data
*/
constructor(
private registry: ExtensionRegistry,
private readonly providers: UserStateSubjectDependencyProvider,
@@ -26,6 +29,12 @@ export class ExtensionService {
private log: SemanticLogger;
/** Get a subject bound to a user's extension settings
* @param profile the site's extension profile
* @param vendor the vendor integrated at the extension site
* @param dependencies.account$ the account to which the settings are bound
* @returns a subject bound to the requested user's generator settings
*/
settings<Settings extends object, Site extends SiteId>(
profile: ExtensionProfileMetadata<Settings, Site>,
vendor: VendorId,
@@ -38,25 +47,17 @@ export class ExtensionService {
const key = toObjectKey(profile, metadata);
const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true }));
// FIXME: load and apply constraints
const subject = new UserStateSubject(key, this.providers, { account$ });
return subject;
}
/** Look up extension metadata for a site
* @param site defines the site to retrieve.
* @returns the extensions available at the site.
*/
site(site: SiteId) {
return this.registry.build(site);
}
/** Look up extension metadata for a site.
* @param site defines the site to retrieve.
* @returns an observable that emits the extension sites available at the
* moment of subscription and then completes. If the extension site is not
* available, the observable completes without emitting.
*/
site$(site: SiteId): Observable<ExtensionSite> {
return defer(() => {
const extensions = this.registry.build(site);
return extensions ? of(extensions) : EMPTY;
});
}
}

View File

@@ -115,6 +115,10 @@ export type ExtensionSet =
all: true;
};
/** A key for storing JavaScript objects (`{ an: "example" }`)
* in the extension profile system.
* @remarks The omitted keys are filled by the extension service.
*/
export type ExtensionStorageKey<Options> = Omit<
ObjectKey<Options>,
"target" | "state" | "format" | "classifier"

View File

@@ -5,7 +5,10 @@ import { ObjectKey } from "../state/object-key";
import { ExtensionMetadata, ExtensionProfileMetadata, SiteId } from "./type";
/** Binds an extension profile to an extension site */
/** Create an object key from an extension instance and a site profile.
* @param profile the extension profile to bind
* @param extension the extension metadata to bind
*/
export function toObjectKey<Settings extends object, Site extends SiteId>(
profile: ExtensionProfileMetadata<Settings, Site>,
extension: ExtensionMetadata,

View File

@@ -1,7 +1,6 @@
import { mock } from "jest-mock-extended";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { VendorId } from "../extension";
import { IntegrationContext } from "./integration-context";
import { IntegrationId } from "./integration-id";
@@ -9,7 +8,7 @@ import { IntegrationMetadata } from "./integration-metadata";
const EXAMPLE_META = Object.freeze({
// arbitrary
id: "simplelogin" as IntegrationId & VendorId,
id: "simplelogin" as IntegrationId,
name: "Example",
// arbitrary
extends: ["forwarder"],
@@ -26,7 +25,7 @@ describe("IntegrationContext", () => {
describe("baseUrl", () => {
it("outputs the base url from metadata", () => {
const context = new IntegrationContext(EXAMPLE_META, null!, i18n);
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
const result = context.baseUrl();
@@ -35,14 +34,14 @@ describe("IntegrationContext", () => {
it("throws when the baseurl isn't defined in metadata", () => {
const noBaseUrl: IntegrationMetadata = {
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
id: "simplelogin" as IntegrationId, // arbitrary
name: "Example",
extends: ["forwarder"], // arbitrary
selfHost: "maybe",
};
i18n.t.mockReturnValue("error");
const context = new IntegrationContext(noBaseUrl, null!, i18n);
const context = new IntegrationContext(noBaseUrl, null, i18n);
expect(() => context.baseUrl()).toThrow("error");
});
@@ -57,7 +56,7 @@ describe("IntegrationContext", () => {
it("ignores settings when selfhost is 'never'", () => {
const selfHostNever: IntegrationMetadata = {
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
id: "simplelogin" as IntegrationId, // arbitrary
name: "Example",
extends: ["forwarder"], // arbitrary
baseUrl: "example.com",
@@ -72,7 +71,7 @@ describe("IntegrationContext", () => {
it("always reads the settings when selfhost is 'always'", () => {
const selfHostAlways: IntegrationMetadata = {
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
id: "simplelogin" as IntegrationId, // arbitrary
name: "Example",
extends: ["forwarder"], // arbitrary
baseUrl: "example.com",
@@ -87,7 +86,7 @@ describe("IntegrationContext", () => {
it("fails when the settings are empty and selfhost is 'always'", () => {
const selfHostAlways: IntegrationMetadata = {
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
id: "simplelogin" as IntegrationId, // arbitrary
name: "Example",
extends: ["forwarder"], // arbitrary
baseUrl: "example.com",
@@ -102,14 +101,14 @@ describe("IntegrationContext", () => {
it("reads from the metadata by default when selfhost is 'maybe'", () => {
const selfHostMaybe: IntegrationMetadata = {
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
id: "simplelogin" as IntegrationId, // arbitrary
name: "Example",
extends: ["forwarder"], // arbitrary
baseUrl: "example.com",
selfHost: "maybe",
};
const context = new IntegrationContext(selfHostMaybe, null!, i18n);
const context = new IntegrationContext(selfHostMaybe, null, i18n);
const result = context.baseUrl();
@@ -118,7 +117,7 @@ describe("IntegrationContext", () => {
it("overrides the metadata when selfhost is 'maybe'", () => {
const selfHostMaybe: IntegrationMetadata = {
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
id: "simplelogin" as IntegrationId, // arbitrary
name: "Example",
extends: ["forwarder"], // arbitrary
baseUrl: "example.com",
@@ -175,7 +174,7 @@ describe("IntegrationContext", () => {
describe("website", () => {
it("returns the website", () => {
const context = new IntegrationContext(EXAMPLE_META, null!, i18n);
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
const result = context.website({ website: "www.example.com" });
@@ -183,7 +182,7 @@ describe("IntegrationContext", () => {
});
it("returns an empty string when the website is not specified", () => {
const context = new IntegrationContext(EXAMPLE_META, null!, i18n);
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
const result = context.website({ website: undefined });
@@ -193,7 +192,7 @@ describe("IntegrationContext", () => {
describe("generatedBy", () => {
it("creates generated by text", () => {
const context = new IntegrationContext(EXAMPLE_META, null!, i18n);
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
i18n.t.mockReturnValue("result");
const result = context.generatedBy({ website: null });
@@ -203,7 +202,7 @@ describe("IntegrationContext", () => {
});
it("creates generated by text including the website", () => {
const context = new IntegrationContext(EXAMPLE_META, null!, i18n);
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
i18n.t.mockReturnValue("result");
const result = context.generatedBy({ website: "www.example.com" });

View File

@@ -1,10 +1,12 @@
import { AlgorithmsByType as ABT } from "./data";
import { CredentialType, CredentialAlgorithm } from "./type";
// `CredentialAlgorithm` is defined in terms of `ABT`; supplying
// type information in the barrel file breaks a circular dependency.
/** Credential generation algorithms grouped by purpose. */
export const AlgorithmsByType: Record<CredentialType, ReadonlyArray<CredentialAlgorithm>> = ABT;
export { Profile, Type } from "./data";
export { toForwarderMetadata } from "./email/forwarder";
export { GeneratorMetadata } from "./generator-metadata";
export { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "./profile-metadata";
export { GeneratorProfile, CredentialAlgorithm, CredentialType } from "./type";

View File

@@ -31,6 +31,11 @@ export function isForwarderExtensionId(
return algorithm && typeof algorithm === "object" && "forwarder" in algorithm;
}
/** Extract a `VendorId` from a `CredentialAlgorithm`.
* @param algorithm the algorithm containing the vendor id
* @returns the vendor id if the algorithm identifies a forwarder extension.
* Otherwise, undefined.
*/
export function toVendorId(algorithm: CredentialAlgorithm): VendorId | undefined {
if (isForwarderExtensionId(algorithm)) {
return algorithm.forwarder as VendorId;