Absorb remainder of element-modules into monorepo
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { Api, Module, WidgetDescriptor, WidgetLifecycleApi } from "@element-hq/element-web-module-api";
|
||||
import { CONFIG_KEY, parseWidgetLifecycleConfig, type WidgetLifecycleModuleConfig } from "./config";
|
||||
import { constructWidgetPermissions } from "./utils/constructWidgetPermissions";
|
||||
import { matchPattern } from "./utils/matchPattern";
|
||||
|
||||
/** Subset of {@link WidgetLifecycleApi} used by the module for registration only. */
|
||||
export type WidgetLifecycleApiAdapter = Pick<
|
||||
WidgetLifecycleApi,
|
||||
"registerPreloadApprover" | "registerIdentityApprover" | "registerCapabilitiesApprover"
|
||||
>;
|
||||
|
||||
type ModuleApi = Pick<Api, "config" | "widgetLifecycle">;
|
||||
|
||||
/**
|
||||
* Module that auto-approves widget preloading, identity token requests, and capability
|
||||
* requests based on URL-pattern rules defined in config.json.
|
||||
*/
|
||||
export default class WidgetLifecycleModule implements Module {
|
||||
public static readonly moduleApiVersion = "^1.10.0";
|
||||
|
||||
private config: WidgetLifecycleModuleConfig = {};
|
||||
|
||||
public constructor(private api: ModuleApi) {}
|
||||
|
||||
public async load(): Promise<void> {
|
||||
if (!this.api.widgetLifecycle) {
|
||||
throw new Error(
|
||||
"Widget lifecycle API is not available. Update Element Web to a build that provides widget lifecycle module support.",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
this.config = parseWidgetLifecycleConfig(this.api.config.get(CONFIG_KEY));
|
||||
} catch (error) {
|
||||
console.error("[WidgetLifecycle] Failed to init module", error);
|
||||
this.config = {};
|
||||
}
|
||||
|
||||
this.api.widgetLifecycle.registerPreloadApprover((widget) => this.preapprovePreload(widget));
|
||||
this.api.widgetLifecycle.registerIdentityApprover((widget) => this.preapproveIdentity(widget));
|
||||
this.api.widgetLifecycle.registerCapabilitiesApprover((widget, requested) =>
|
||||
this.preapproveCapabilities(widget, requested),
|
||||
);
|
||||
}
|
||||
|
||||
private preapprovePreload(widget: WidgetDescriptor): boolean {
|
||||
const configuration = constructWidgetPermissions(this.config, widget.templateUrl);
|
||||
return configuration.preload_approved === true;
|
||||
}
|
||||
|
||||
private preapproveIdentity(widget: WidgetDescriptor): boolean {
|
||||
const configuration = constructWidgetPermissions(this.config, widget.templateUrl);
|
||||
return configuration.identity_approved === true;
|
||||
}
|
||||
|
||||
private preapproveCapabilities(
|
||||
widget: WidgetDescriptor,
|
||||
requestedCapabilities: Set<string>,
|
||||
): Set<string> | undefined {
|
||||
const configuration = constructWidgetPermissions(this.config, widget.templateUrl);
|
||||
const capabilitiesApproved = configuration.capabilities_approved;
|
||||
|
||||
if (!capabilitiesApproved) return undefined;
|
||||
|
||||
const approvedCapabilities = new Set<string>();
|
||||
for (const requestedCapability of requestedCapabilities) {
|
||||
if (capabilitiesApproved.some((capability) => matchPattern(requestedCapability, capability))) {
|
||||
approvedCapabilities.add(requestedCapability);
|
||||
}
|
||||
}
|
||||
|
||||
return approvedCapabilities.size > 0 ? approvedCapabilities : undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { z } from "zod/mini";
|
||||
|
||||
z.config(z.locales.en());
|
||||
|
||||
/** The config.json key under which widget lifecycle module configuration is stored. */
|
||||
export const CONFIG_KEY = "io.element.element-web-modules.widget-lifecycle";
|
||||
|
||||
const WidgetConfigurationSchema = z.partial(
|
||||
z.looseObject({
|
||||
preload_approved: z.boolean(),
|
||||
identity_approved: z.boolean(),
|
||||
capabilities_approved: z.array(z.string().check(z.minLength(1))),
|
||||
}),
|
||||
);
|
||||
|
||||
/** Per-widget approval settings: preload, identity, and capabilities. */
|
||||
export type WidgetConfiguration = z.infer<typeof WidgetConfigurationSchema>;
|
||||
|
||||
const ModuleConfigSchema = z.partial(
|
||||
z.looseObject({
|
||||
widget_permissions: z.record(z.string(), WidgetConfigurationSchema),
|
||||
}),
|
||||
);
|
||||
|
||||
/** Map from URL patterns to their widget approval configuration. */
|
||||
export type WidgetLifecycleModuleConfig = Record<string, WidgetConfiguration>;
|
||||
|
||||
declare module "@element-hq/element-web-module-api" {
|
||||
export interface Config {
|
||||
[CONFIG_KEY]: z.input<typeof ModuleConfigSchema>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate the widget lifecycle module configuration.
|
||||
* Returns an empty config if the input is falsy; throws on schema violations.
|
||||
*/
|
||||
export const parseWidgetLifecycleConfig = (value: unknown): WidgetLifecycleModuleConfig => {
|
||||
if (!value) return {};
|
||||
|
||||
const result = ModuleConfigSchema.safeParse(value);
|
||||
if (!result.success) {
|
||||
throw new Error(`Errors in the module configuration for "${CONFIG_KEY}": ${result.error.message}`);
|
||||
}
|
||||
|
||||
return result.data.widget_permissions ?? {};
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { ModuleFactory } from "@element-hq/element-web-module-api";
|
||||
import WidgetLifecycleModule from "./WidgetLifecycleModule";
|
||||
|
||||
export default WidgetLifecycleModule satisfies ModuleFactory;
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { WidgetLifecycleModuleConfig, WidgetConfiguration } from "../config";
|
||||
import { matchPattern } from "./matchPattern";
|
||||
|
||||
/**
|
||||
* Returns the WidgetConfiguration for a widget.
|
||||
* If multiple WidgetConfigurations match, the most specific match wins per field.
|
||||
*/
|
||||
export function constructWidgetPermissions(
|
||||
config: WidgetLifecycleModuleConfig,
|
||||
widgetUrl: string,
|
||||
): WidgetConfiguration {
|
||||
const widgetPermissionsMatched = Object.keys(config).filter((pattern) => matchPattern(widgetUrl, pattern));
|
||||
|
||||
return widgetPermissionsMatched.sort(sortLongestMatchLast).reduce((prev, key) => ({ ...prev, ...config[key] }), {});
|
||||
}
|
||||
|
||||
/** Sort strings alphabetically so longer, more-specific patterns are applied last. */
|
||||
export function sortLongestMatchLast(a: string, b: string): number {
|
||||
return a.localeCompare(b, "en", { sensitivity: "base" });
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if string matches pattern. Pattern can end with '*' to support prefix matching.
|
||||
*/
|
||||
export function matchPattern(value: string, pattern: string): boolean {
|
||||
return pattern.endsWith("*") ? value.startsWith(pattern.slice(0, pattern.length - 1)) : value === pattern;
|
||||
}
|
||||
Reference in New Issue
Block a user