Absorb remainder of element-modules into monorepo

This commit is contained in:
Michael Telatynski
2026-06-08 10:32:12 +01:00
112 changed files with 4569 additions and 8 deletions
@@ -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;
}
}
+54
View File
@@ -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 ?? {};
};
+11
View File
@@ -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;
}