From 6aa454c646e8046d400f6e7344e0f24bb5e36838 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 25 Feb 2026 09:36:58 +0000 Subject: [PATCH 01/20] Add widget lifecycle module --- .../widget-lifecycle/element-web/README.md | 75 +++++++ .../element-web/e2e/fixture/widget.html | 61 +++++ .../element-web/e2e/widget-lifecycle.spec.ts | 210 ++++++++++++++++++ .../widget-lifecycle/element-web/package.json | 24 ++ .../element-web/src/WidgetLifecycleModule.ts | 88 ++++++++ .../element-web/src/config.ts | 54 +++++ .../widget-lifecycle/element-web/src/index.ts | 12 + .../src/utils/constructWidgetPermissions.ts | 27 +++ .../element-web/src/utils/matchPattern.ts | 13 ++ .../src/utils/normalizeWidgetUrl.ts | 16 ++ .../tests/WidgetLifecycleModule.test.ts | 140 ++++++++++++ .../element-web/tests/config.test.ts | 88 ++++++++ .../tests/constructWidgetPermissions.test.ts | 75 +++++++ .../element-web/tsconfig.json | 7 + .../element-web/vite.config.ts | 27 +++ .../element-web/vitest.config.ts | 15 ++ 16 files changed, 932 insertions(+) create mode 100644 modules/widget-lifecycle/element-web/README.md create mode 100644 modules/widget-lifecycle/element-web/e2e/fixture/widget.html create mode 100644 modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts create mode 100644 modules/widget-lifecycle/element-web/package.json create mode 100644 modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts create mode 100644 modules/widget-lifecycle/element-web/src/config.ts create mode 100644 modules/widget-lifecycle/element-web/src/index.ts create mode 100644 modules/widget-lifecycle/element-web/src/utils/constructWidgetPermissions.ts create mode 100644 modules/widget-lifecycle/element-web/src/utils/matchPattern.ts create mode 100644 modules/widget-lifecycle/element-web/src/utils/normalizeWidgetUrl.ts create mode 100644 modules/widget-lifecycle/element-web/tests/WidgetLifecycleModule.test.ts create mode 100644 modules/widget-lifecycle/element-web/tests/config.test.ts create mode 100644 modules/widget-lifecycle/element-web/tests/constructWidgetPermissions.test.ts create mode 100644 modules/widget-lifecycle/element-web/tsconfig.json create mode 100644 modules/widget-lifecycle/element-web/vite.config.ts create mode 100644 modules/widget-lifecycle/element-web/vitest.config.ts diff --git a/modules/widget-lifecycle/element-web/README.md b/modules/widget-lifecycle/element-web/README.md new file mode 100644 index 0000000000..dd4c5ab6d1 --- /dev/null +++ b/modules/widget-lifecycle/element-web/README.md @@ -0,0 +1,75 @@ +# @element-hq/element-web-module-widget-lifecycle + +Widget lifecycle module for Element Web. + +Supports the following configuration options under the configuration key `io.element.element-web-modules.widget-lifecycle`: + +| Key | Type | Description | +| ------------------ | ------ | ------------------------------------------------ | +| widget_permissions | object | Map of widget URL patterns to approval settings. | + +Each widget configuration can use the following options: + +- `preload_approved` - if true, the preload dialog is not displayed for this widget. +- `identity_approved` - if true, requests for an identity token are automatically accepted. +- `capabilities_approved` - a list of capabilities that should be approved for this widget. + +The widget URL and capability strings can use a trailing `*` to match multiple widgets or capabilities. +This is useful when widgets have multiple routes or capabilities include variable state keys. + +## Matching and precedence + +- Widget URLs are normalized by stripping query parameters and hash fragments before matching. +- Patterns ending in `*` are matched by prefix; other patterns must match exactly. +- If multiple rules match, the most specific match wins per field. +- The capabilities allow-list is not merged across rules; the most specific rule that defines it wins. + +## Example configuration (exact match) + +```json +{ + "io.element.element-web-modules.widget-lifecycle": { + "widget_permissions": { + "https://widget.example.com/": { + "preload_approved": true, + "identity_approved": true, + "capabilities_approved": [ + "org.matrix.msc2931.navigate", + "org.matrix.msc2762.receive.state_event:m.room.power_levels" + ] + } + } + } +} +``` + +## Example configuration (wildcards) + +```json +{ + "io.element.element-web-modules.widget-lifecycle": { + "widget_permissions": { + "https://widget.example.com/*": { + "preload_approved": true, + "identity_approved": true, + "capabilities_approved": [ + "org.matrix.msc2931.navigate", + "org.matrix.msc2762.receive.state_event:m.room.power_levels", + "org.matrix.msc2762.send.state_event:net.custom_event#*" + ] + } + } + } +} +``` + +## Copyright & License + +Copyright (c) 2025 New Vector Ltd + +This software is multi licensed by New Vector Ltd (Element). It can be used either: + +(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR + +(2) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to). +Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses. diff --git a/modules/widget-lifecycle/element-web/e2e/fixture/widget.html b/modules/widget-lifecycle/element-web/e2e/fixture/widget.html new file mode 100644 index 0000000000..271e18d163 --- /dev/null +++ b/modules/widget-lifecycle/element-web/e2e/fixture/widget.html @@ -0,0 +1,61 @@ + + + + Demo Widget + + + +

Hello unknown!

+ + diff --git a/modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts b/modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts new file mode 100644 index 0000000000..268a9dece5 --- /dev/null +++ b/modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts @@ -0,0 +1,210 @@ +/* +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 SynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js"; + +import { test, expect } from "../../../../playwright/element-web-test.ts"; + +declare module "@element-hq/element-web-module-api" { + interface Config { + "io.element.element-web-modules.widget-lifecycle": { + widget_permissions: { + [url: string]: { + preload_approved?: boolean; + identity_approved?: boolean; + capabilities_approved?: string[]; + }; + }; + }; + } +} + +const WIDGET_URL = "http://localhost:8080/widget.html"; + +test.use({ + displayName: "Timmy", + synapseConfig: async ({ synapseConfig, _homeserver: homeserver }, use) => { + (homeserver as SynapseContainer).withConfigField("listeners[0].resources[0].names", ["client", "openid"]); + await use(synapseConfig); + }, + page: async ({ context, page, moduleDir }, use) => { + await context.route(`${WIDGET_URL}*`, async (route) => { + await route.fulfill({ path: `${moduleDir}/e2e/fixture/widget.html`, contentType: "text/html" }); + }); + + await page.goto("/"); + await use(page); + }, + bypassCSP: true, + launchOptions: { + args: ["--disable-web-security"], + }, +}); + +test.describe("Widget Lifecycle", () => { + test.describe("trusted widgets", () => { + test.use({ + config: { + "io.element.element-web-modules.widget-lifecycle": { + widget_permissions: { + [WIDGET_URL]: { + preload_approved: true, + identity_approved: true, + capabilities_approved: ["org.matrix.msc2762.receive.state_event:m.room.topic"], + }, + }, + }, + }, + }); + + test("auto-approves preload and identity", async ({ page, user, homeserver }, testInfo) => { + const bot = await homeserver.registerUser(`bot_${testInfo.testId}`, "password", "Bot"); + const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>( + "POST", + "/v3/createRoom", + bot.accessToken, + { + name: "Trusted Widget", + }, + ); + await homeserver.csApi.request<{ event_id: string }>( + "PUT", + `/v3/rooms/${encodeURIComponent(roomId)}/state/im.vector.modular.widgets/1`, + bot.accessToken, + { + id: "1", + creatorUserId: bot.userId, + type: "custom", + name: "Trusted Widget", + url: `${WIDGET_URL}?hsUrl=${encodeURIComponent(homeserver.baseUrl)}&caps=org.matrix.msc2762.receive.state_event:m.room.topic`, + }, + ); + await homeserver.csApi.request( + "PUT", + `/v3/rooms/${encodeURIComponent(roomId)}/state/io.element.widgets.layout/`, + bot.accessToken, + { + widgets: { + "1": { + container: "top", + }, + }, + }, + ); + await homeserver.csApi.request("POST", `/v3/rooms/${encodeURIComponent(roomId)}/invite`, bot.accessToken, { + user_id: user.userId, + }); + + await page.getByText("Trusted Widget").click(); + await page.getByRole("button", { name: "Accept" }).click(); + + await expect( + page + .frameLocator('iframe[title="Trusted Widget"]') + .getByRole("heading", { name: `Hello ${user.userId}!` }), + ).toBeVisible(); + }); + + test("prompts for capabilities not in the allowlist", async ({ page, user, homeserver }, testInfo) => { + const bot = await homeserver.registerUser(`bot_${testInfo.testId}`, "password", "Bot"); + const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>( + "POST", + "/v3/createRoom", + bot.accessToken, + { + name: "Capabilities Widget", + }, + ); + await homeserver.csApi.request<{ event_id: string }>( + "PUT", + `/v3/rooms/${encodeURIComponent(roomId)}/state/im.vector.modular.widgets/1`, + bot.accessToken, + { + id: "1", + creatorUserId: bot.userId, + type: "custom", + name: "Capabilities Widget", + url: `${WIDGET_URL}?hsUrl=${encodeURIComponent(homeserver.baseUrl)}&caps=org.matrix.msc2762.receive.state_event:m.room.topic,org.matrix.msc2762.receive.state_event:m.room.name`, + }, + ); + await homeserver.csApi.request( + "PUT", + `/v3/rooms/${encodeURIComponent(roomId)}/state/io.element.widgets.layout/`, + bot.accessToken, + { + widgets: { + "1": { + container: "top", + }, + }, + }, + ); + await homeserver.csApi.request("POST", `/v3/rooms/${encodeURIComponent(roomId)}/invite`, bot.accessToken, { + user_id: user.userId, + }); + + await page.getByText("Capabilities Widget").click(); + await page.getByRole("button", { name: "Accept" }).click(); + + await expect(page.getByRole("button", { name: "Approve" })).toBeVisible(); + }); + }); + + test.describe("untrusted widgets", () => { + test.use({ + config: { + "io.element.element-web-modules.widget-lifecycle": { + widget_permissions: {}, + }, + }, + }); + + test("shows dialogs for untrusted widgets", async ({ page, user, homeserver }, testInfo) => { + const bot = await homeserver.registerUser(`bot_${testInfo.testId}`, "password", "Bot"); + const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>( + "POST", + "/v3/createRoom", + bot.accessToken, + { + name: "Untrusted Widget", + }, + ); + await homeserver.csApi.request<{ event_id: string }>( + "PUT", + `/v3/rooms/${encodeURIComponent(roomId)}/state/im.vector.modular.widgets/1`, + bot.accessToken, + { + id: "1", + creatorUserId: bot.userId, + type: "custom", + name: "Untrusted Widget", + url: `${WIDGET_URL}?hsUrl=${encodeURIComponent(homeserver.baseUrl)}&caps=org.matrix.msc2762.receive.state_event:m.room.topic`, + }, + ); + await homeserver.csApi.request( + "PUT", + `/v3/rooms/${encodeURIComponent(roomId)}/state/io.element.widgets.layout/`, + bot.accessToken, + { + widgets: { + "1": { + container: "top", + }, + }, + }, + ); + await homeserver.csApi.request("POST", `/v3/rooms/${encodeURIComponent(roomId)}/invite`, bot.accessToken, { + user_id: user.userId, + }); + + await page.getByText("Untrusted Widget").click(); + await page.getByRole("button", { name: "Accept" }).click(); + + await expect(page.getByRole("button", { name: "Continue" })).toBeVisible(); + }); + }); +}); diff --git a/modules/widget-lifecycle/element-web/package.json b/modules/widget-lifecycle/element-web/package.json new file mode 100644 index 0000000000..b469ee086c --- /dev/null +++ b/modules/widget-lifecycle/element-web/package.json @@ -0,0 +1,24 @@ +{ + "name": "@element-hq/element-web-module-widget-lifecycle", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "lib/index.js", + "license": "SEE LICENSE IN README.md", + "scripts": { + "prepare": "vite build", + "lint:types": "tsc --noEmit", + "lint:codestyle": "echo 'handled by lint:eslint'", + "test": "vitest" + }, + "devDependencies": { + "@element-hq/element-web-module-api": "^1.0.0", + "@types/node": "^22.10.7", + "typescript": "^5.7.3", + "vite": "^7.1.11", + "vitest": "^4.0.0" + }, + "dependencies": { + "zod": "^4.0.0" + } +} diff --git a/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts b/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts new file mode 100644 index 0000000000..17453af6bc --- /dev/null +++ b/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts @@ -0,0 +1,88 @@ +/* +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"; +import { normalizeWidgetUrl } from "./utils/normalizeWidgetUrl"; + +/** Subset of {@link WidgetLifecycleApi} used by the module for registration only. */ +export type WidgetLifecycleApiAdapter = Pick< + WidgetLifecycleApi, + "registerPreloadApprover" | "registerIdentityApprover" | "registerCapabilitiesApprover" +>; + +type ModuleApi = Pick; + +/** + * 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.0.0"; + + private config: WidgetLifecycleModuleConfig = {}; + + public constructor(private api: ModuleApi) {} + + public async load(): Promise { + if (!this.api.widgetLifecycle?.registerPreloadApprover) { + 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 normalizedUrl = normalizeWidgetUrl(widget.templateUrl); + const configuration = constructWidgetPermissions(this.config, normalizedUrl); + return configuration.preload_approved === true; + } + + private preapproveIdentity(widget: WidgetDescriptor): boolean { + const normalizedUrl = normalizeWidgetUrl(widget.templateUrl); + const configuration = constructWidgetPermissions(this.config, normalizedUrl); + return configuration.identity_approved === true; + } + + private preapproveCapabilities( + widget: WidgetDescriptor, + requestedCapabilities: Set, + ): Set | undefined { + const normalizedUrl = normalizeWidgetUrl(widget.templateUrl); + const configuration = constructWidgetPermissions(this.config, normalizedUrl); + const capabilitiesApproved = configuration.capabilities_approved; + + if (!capabilitiesApproved) return undefined; + + const approvedCapabilities = new Set(); + for (const requestedCapability of requestedCapabilities) { + if (capabilitiesApproved.some((capability) => matchPattern(requestedCapability, capability))) { + approvedCapabilities.add(requestedCapability); + } + } + + return approvedCapabilities.size > 0 ? approvedCapabilities : undefined; + } +} + +export { WidgetLifecycleModule }; diff --git a/modules/widget-lifecycle/element-web/src/config.ts b/modules/widget-lifecycle/element-web/src/config.ts new file mode 100644 index 0000000000..0746ae37f1 --- /dev/null +++ b/modules/widget-lifecycle/element-web/src/config.ts @@ -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"; + +export 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; + +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; + +/** + * Parse and validate the widget lifecycle module configuration. + * Returns an empty config if the input is falsy; throws on schema violations. + */ +declare module "@element-hq/element-web-module-api" { + export interface Config { + [CONFIG_KEY]: z.input; + } +} + +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 ?? {}; +}; diff --git a/modules/widget-lifecycle/element-web/src/index.ts b/modules/widget-lifecycle/element-web/src/index.ts new file mode 100644 index 0000000000..843638fc29 --- /dev/null +++ b/modules/widget-lifecycle/element-web/src/index.ts @@ -0,0 +1,12 @@ +/* +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; diff --git a/modules/widget-lifecycle/element-web/src/utils/constructWidgetPermissions.ts b/modules/widget-lifecycle/element-web/src/utils/constructWidgetPermissions.ts new file mode 100644 index 0000000000..9f4ae1346d --- /dev/null +++ b/modules/widget-lifecycle/element-web/src/utils/constructWidgetPermissions.ts @@ -0,0 +1,27 @@ +/* +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 { 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" }); +} diff --git a/modules/widget-lifecycle/element-web/src/utils/matchPattern.ts b/modules/widget-lifecycle/element-web/src/utils/matchPattern.ts new file mode 100644 index 0000000000..7531474569 --- /dev/null +++ b/modules/widget-lifecycle/element-web/src/utils/matchPattern.ts @@ -0,0 +1,13 @@ +/* +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. +*/ + +/** + * 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; +} diff --git a/modules/widget-lifecycle/element-web/src/utils/normalizeWidgetUrl.ts b/modules/widget-lifecycle/element-web/src/utils/normalizeWidgetUrl.ts new file mode 100644 index 0000000000..794db023bf --- /dev/null +++ b/modules/widget-lifecycle/element-web/src/utils/normalizeWidgetUrl.ts @@ -0,0 +1,16 @@ +/* +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. +*/ + +/** + * Strips query parameters and fragment from a widget URL for matching against config patterns. + */ +export function normalizeWidgetUrl(widgetUrl: string): string { + const url = new URL(widgetUrl); + url.search = ""; + url.hash = ""; + return url.toString(); +} diff --git a/modules/widget-lifecycle/element-web/tests/WidgetLifecycleModule.test.ts b/modules/widget-lifecycle/element-web/tests/WidgetLifecycleModule.test.ts new file mode 100644 index 0000000000..9970a9aa00 --- /dev/null +++ b/modules/widget-lifecycle/element-web/tests/WidgetLifecycleModule.test.ts @@ -0,0 +1,140 @@ +/* +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 { describe, expect, it } from "vitest"; + +import WidgetLifecycleModule, { type WidgetLifecycleApiAdapter } from "../src/WidgetLifecycleModule"; +import type { + CapabilitiesApprover, + IdentityApprover, + PreloadApprover, + WidgetDescriptor, +} from "@element-hq/element-web-module-api"; + +const createApi = (config: unknown = {}) => { + const handlers: { + preload?: PreloadApprover; + identity?: IdentityApprover; + capabilities?: CapabilitiesApprover; + } = {}; + + const widgetLifecycle: WidgetLifecycleApiAdapter = { + registerPreloadApprover: (approver) => { + handlers.preload = approver; + }, + registerIdentityApprover: (approver) => { + handlers.identity = approver; + }, + registerCapabilitiesApprover: (approver) => { + handlers.capabilities = approver; + }, + }; + + return { + api: { + config: { + get: () => config, + }, + widgetLifecycle, + }, + handlers, + }; +}; + +const widget: WidgetDescriptor = { + id: "widget-id", + templateUrl: "https://example.com", + creatorUserId: "@user-id", + kind: "room", +}; + +describe("WidgetLifecycleModule", () => { + it("approves preload when configured", async () => { + const { api, handlers } = createApi({ + widget_permissions: { + "https://example.com/": { preload_approved: true }, + }, + }); + + const module = new WidgetLifecycleModule(api); + await module.load(); + + expect(await handlers.preload?.(widget)).toBe(true); + }); + + it("approves identity when configured", async () => { + const { api, handlers } = createApi({ + widget_permissions: { + "https://example.com/": { identity_approved: true }, + }, + }); + + const module = new WidgetLifecycleModule(api); + await module.load(); + + expect(await handlers.identity?.(widget)).toBe(true); + }); + + it("approves configured capabilities", async () => { + const { api, handlers } = createApi({ + widget_permissions: { + "https://example.com/": { + capabilities_approved: ["org.matrix.msc2931.navigate"], + }, + }, + }); + + const module = new WidgetLifecycleModule(api); + await module.load(); + + const approved = await handlers.capabilities?.( + widget, + new Set(["org.matrix.msc2931.navigate", "org.matrix.msc2762.timeline:*"]), + ); + + expect(approved).toEqual(new Set(["org.matrix.msc2931.navigate"])); + }); + + it("returns no approvals when not configured", async () => { + const { api, handlers } = createApi({}); + + const module = new WidgetLifecycleModule(api); + await module.load(); + + expect(await handlers.preload?.(widget)).toBe(false); + expect(await handlers.identity?.(widget)).toBe(false); + expect(await handlers.capabilities?.(widget, new Set(["org.matrix.msc2931.navigate"]))).toBeUndefined(); + }); + + it("returns no approvals when config is missing", async () => { + const { api, handlers } = createApi(undefined); + + const module = new WidgetLifecycleModule(api); + await module.load(); + + expect(await handlers.preload?.(widget)).toBe(false); + expect(await handlers.identity?.(widget)).toBe(false); + expect(await handlers.capabilities?.(widget, new Set(["org.matrix.msc2931.navigate"]))).toBeUndefined(); + }); + + it("fails closed on invalid config", async () => { + const { api, handlers } = createApi({ + widget_permissions: { + "https://example.com/": { + preload_approved: null, + }, + }, + }); + + const module = new WidgetLifecycleModule(api); + await module.load(); + + expect(await handlers.preload?.(widget)).toBe(false); + expect(await handlers.identity?.(widget)).toBe(false); + expect(await handlers.capabilities?.(widget, new Set(["org.matrix.msc2931.navigate"]))).toBeUndefined(); + }); +}); diff --git a/modules/widget-lifecycle/element-web/tests/config.test.ts b/modules/widget-lifecycle/element-web/tests/config.test.ts new file mode 100644 index 0000000000..ed131db338 --- /dev/null +++ b/modules/widget-lifecycle/element-web/tests/config.test.ts @@ -0,0 +1,88 @@ +/* +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 { describe, expect, it } from "vitest"; + +import { parseWidgetLifecycleConfig } from "../src/config"; + +describe("parseWidgetLifecycleConfig", () => { + it("accepts missing configuration", () => { + expect(parseWidgetLifecycleConfig(undefined)).toEqual({}); + }); + + it("accepts empty configuration", () => { + expect(parseWidgetLifecycleConfig({})).toEqual({}); + }); + + it("accepts valid configuration", () => { + expect( + parseWidgetLifecycleConfig({ + widget_permissions: { + "https://localhost": { + preload_approved: true, + identity_approved: false, + capabilities_approved: [], + }, + }, + }), + ).toEqual({ + "https://localhost": { + preload_approved: true, + identity_approved: false, + capabilities_approved: [], + }, + }); + }); + + it("accepts additional properties", () => { + expect( + parseWidgetLifecycleConfig({ + widget_permissions: { + "https://localhost": { + preload_approved: true, + identity_approved: false, + capabilities_approved: ["capability"], + additional: "tmp", + }, + }, + }), + ).toEqual({ + "https://localhost": { + preload_approved: true, + identity_approved: false, + capabilities_approved: ["capability"], + additional: "tmp", + }, + }); + }); + + it.each([ + { preload_approved: null }, + { preload_approved: 123 }, + { identity_approved: null }, + { identity_approved: 123 }, + { capabilities_approved: null }, + { capabilities_approved: 123 }, + { capabilities_approved: [undefined] }, + { capabilities_approved: [null] }, + { capabilities_approved: [123] }, + { capabilities_approved: [""] }, + ])("rejects invalid widget configuration %j", (patch) => { + expect(() => + parseWidgetLifecycleConfig({ + widget_permissions: { + "https://localhost": { + preload_approved: true, + identity_approved: false, + capabilities_approved: ["capability"], + ...patch, + }, + }, + }), + ).toThrow(); + }); +}); diff --git a/modules/widget-lifecycle/element-web/tests/constructWidgetPermissions.test.ts b/modules/widget-lifecycle/element-web/tests/constructWidgetPermissions.test.ts new file mode 100644 index 0000000000..e84881acd0 --- /dev/null +++ b/modules/widget-lifecycle/element-web/tests/constructWidgetPermissions.test.ts @@ -0,0 +1,75 @@ +/* +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 { describe, expect, it } from "vitest"; + +import { constructWidgetPermissions, sortLongestMatchLast } from "../src/utils/constructWidgetPermissions"; + +describe("constructWidgetPermissions", () => { + it("finds exact match", () => { + expect(constructWidgetPermissions({ "https://a.com/": { preload_approved: true } }, "https://a.com/")).toEqual({ + preload_approved: true, + }); + }); + + it("finds prefix match", () => { + expect( + constructWidgetPermissions({ "https://a.com/*": { preload_approved: true } }, "https://a.com/some"), + ).toEqual({ preload_approved: true }); + }); + + it("merges multiple permissions", () => { + expect( + constructWidgetPermissions( + { + "https://b.com/path": { + preload_approved: false, + capabilities_approved: ["org.matrix.msc2762.timeline:*"], + }, + "https://b.com/*": { + preload_approved: true, + identity_approved: true, + capabilities_approved: ["org.matrix.msc2931.navigate"], + }, + }, + "https://b.com/path", + ), + ).toEqual({ + preload_approved: false, + identity_approved: true, + capabilities_approved: ["org.matrix.msc2762.timeline:*"], + }); + }); + + it("skips unknown url", () => { + expect(constructWidgetPermissions({ "https://a.com/": { preload_approved: true } }, "https://a.com/x")).toEqual( + {}, + ); + }); +}); + +describe("sortLongestMatchLast", () => { + it("sorts longest match last", () => { + expect( + [ + "org.matrix.msc2762.receive.state_event:*", + "org.matrix.msc2762.receive.*", + "org.matrix.msc2762.receive.state_event:m.custom*", + "org.matrix.msc2762.receive.state_event:m.custom#state_key", + "org.matrix.msc2762.receive.state_event:m.custom#*", + "*", + ].sort(sortLongestMatchLast), + ).toEqual([ + "*", + "org.matrix.msc2762.receive.*", + "org.matrix.msc2762.receive.state_event:*", + "org.matrix.msc2762.receive.state_event:m.custom*", + "org.matrix.msc2762.receive.state_event:m.custom#*", + "org.matrix.msc2762.receive.state_event:m.custom#state_key", + ]); + }); +}); diff --git a/modules/widget-lifecycle/element-web/tsconfig.json b/modules/widget-lifecycle/element-web/tsconfig.json new file mode 100644 index 0000000000..8f4f6b8ace --- /dev/null +++ b/modules/widget-lifecycle/element-web/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "lib" + }, + "include": ["src"] +} diff --git a/modules/widget-lifecycle/element-web/vite.config.ts b/modules/widget-lifecycle/element-web/vite.config.ts new file mode 100644 index 0000000000..3e4bed43a0 --- /dev/null +++ b/modules/widget-lifecycle/element-web/vite.config.ts @@ -0,0 +1,27 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vite"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, "src/index.ts"), + name: "element-web-module-widget-lifecycle", + fileName: "index", + formats: ["es"], + }, + outDir: "lib", + target: "esnext", + sourcemap: true, + minify: false, + }, +}); diff --git a/modules/widget-lifecycle/element-web/vitest.config.ts b/modules/widget-lifecycle/element-web/vitest.config.ts new file mode 100644 index 0000000000..29cb2a09bd --- /dev/null +++ b/modules/widget-lifecycle/element-web/vitest.config.ts @@ -0,0 +1,15 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts"], + exclude: ["./e2e/**/*", "./node_modules/**/*"], + }, +}); From 5d091e91c3306f22a5e258bd26ed97b78e882fc3 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 4 Mar 2026 18:02:17 +0000 Subject: [PATCH 02/20] Update e2e tests and header Don't add normalisation of widget headers either --- .../widget-lifecycle/element-web/README.md | 1 - .../element-web/e2e/widget-lifecycle.spec.ts | 28 ++++++++++++++- .../element-web/src/WidgetLifecycleModule.ts | 11 ++---- .../widget-lifecycle/element-web/src/index.ts | 1 - .../src/utils/constructWidgetPermissions.ts | 1 + .../element-web/src/utils/matchPattern.ts | 1 + .../src/utils/normalizeWidgetUrl.ts | 16 --------- .../tests/WidgetLifecycleModule.test.ts | 2 +- .../tests/constructWidgetPermissions.test.ts | 1 + .../element-web/tests/matchPattern.test.ts | 35 +++++++++++++++++++ 10 files changed, 69 insertions(+), 28 deletions(-) delete mode 100644 modules/widget-lifecycle/element-web/src/utils/normalizeWidgetUrl.ts create mode 100644 modules/widget-lifecycle/element-web/tests/matchPattern.test.ts diff --git a/modules/widget-lifecycle/element-web/README.md b/modules/widget-lifecycle/element-web/README.md index dd4c5ab6d1..7cc0bc734c 100644 --- a/modules/widget-lifecycle/element-web/README.md +++ b/modules/widget-lifecycle/element-web/README.md @@ -19,7 +19,6 @@ This is useful when widgets have multiple routes or capabilities include variabl ## Matching and precedence -- Widget URLs are normalized by stripping query parameters and hash fragments before matching. - Patterns ending in `*` are matched by prefix; other patterns must match exactly. - If multiple rules match, the most specific match wins per field. - The capabilities allow-list is not merged across rules; the most specific rule that defines it wins. diff --git a/modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts b/modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts index 268a9dece5..ed3ac965d8 100644 --- a/modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts +++ b/modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts @@ -47,6 +47,8 @@ test.use({ test.describe("Widget Lifecycle", () => { test.describe("trusted widgets", () => { + // Configure the module to pre-approve the widget URL for preloading, identity tokens, + // and the m.room.topic state event capability. test.use({ config: { "io.element.element-web-modules.widget-lifecycle": { @@ -62,6 +64,9 @@ test.describe("Widget Lifecycle", () => { }); test("auto-approves preload and identity", async ({ page, user, homeserver }, testInfo) => { + // A bot creates a room with the widget pinned to the top panel, then invites the test user. + // Because the widget was added by a different user (the bot), Element would normally show a + // preload consent dialog before loading it — this test verifies that dialog is skipped. const bot = await homeserver.registerUser(`bot_${testInfo.testId}`, "password", "Bot"); const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>( "POST", @@ -102,6 +107,11 @@ test.describe("Widget Lifecycle", () => { await page.getByText("Trusted Widget").click(); await page.getByRole("button", { name: "Accept" }).click(); + // No preload dialog should appear — the widget loads immediately. + await expect(page.getByRole("button", { name: "Continue" })).not.toBeVisible(); + + // The widget greets the user by ID, proving the identity token was also auto-approved + // and passed to the widget without any consent prompts. await expect( page .frameLocator('iframe[title="Trusted Widget"]') @@ -119,6 +129,7 @@ test.describe("Widget Lifecycle", () => { name: "Capabilities Widget", }, ); + // The widget requests two capabilities: m.room.topic (in the allowlist) and m.room.name (not in the allowlist). await homeserver.csApi.request<{ event_id: string }>( "PUT", `/v3/rooms/${encodeURIComponent(roomId)}/state/im.vector.modular.widgets/1`, @@ -150,11 +161,13 @@ test.describe("Widget Lifecycle", () => { await page.getByText("Capabilities Widget").click(); await page.getByRole("button", { name: "Accept" }).click(); + // A capabilities approval dialog should appear since m.room.name was not pre-approved. await expect(page.getByRole("button", { name: "Approve" })).toBeVisible(); }); }); test.describe("untrusted widgets", () => { + // No widget URLs are pre-approved, so all lifecycle prompts should be shown to the user. test.use({ config: { "io.element.element-web-modules.widget-lifecycle": { @@ -163,7 +176,11 @@ test.describe("Widget Lifecycle", () => { }, }); - test("shows dialogs for untrusted widgets", async ({ page, user, homeserver }, testInfo) => { + test("shows preload, capabilities, and OpenID dialogs for untrusted widgets", async ({ + page, + user, + homeserver, + }, testInfo) => { const bot = await homeserver.registerUser(`bot_${testInfo.testId}`, "password", "Bot"); const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>( "POST", @@ -204,6 +221,15 @@ test.describe("Widget Lifecycle", () => { await page.getByText("Untrusted Widget").click(); await page.getByRole("button", { name: "Accept" }).click(); + // 1. Preload consent dialog — shown because the widget was added by another user (the bot). + await expect(page.getByRole("button", { name: "Continue" })).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); + + // 2. Capabilities dialog — the widget requests m.room.topic once it loads. + await expect(page.getByRole("button", { name: "Approve" })).toBeVisible(); + await page.getByRole("button", { name: "Approve" }).click(); + + // 3. OpenID identity dialog — the widget requests an identity token after capabilities are granted. await expect(page.getByRole("button", { name: "Continue" })).toBeVisible(); }); }); diff --git a/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts b/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts index 17453af6bc..ce17d783f8 100644 --- a/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts +++ b/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts @@ -6,11 +6,9 @@ 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"; -import { normalizeWidgetUrl } from "./utils/normalizeWidgetUrl"; /** Subset of {@link WidgetLifecycleApi} used by the module for registration only. */ export type WidgetLifecycleApiAdapter = Pick< @@ -53,14 +51,12 @@ export default class WidgetLifecycleModule implements Module { } private preapprovePreload(widget: WidgetDescriptor): boolean { - const normalizedUrl = normalizeWidgetUrl(widget.templateUrl); - const configuration = constructWidgetPermissions(this.config, normalizedUrl); + const configuration = constructWidgetPermissions(this.config, widget.templateUrl); return configuration.preload_approved === true; } private preapproveIdentity(widget: WidgetDescriptor): boolean { - const normalizedUrl = normalizeWidgetUrl(widget.templateUrl); - const configuration = constructWidgetPermissions(this.config, normalizedUrl); + const configuration = constructWidgetPermissions(this.config, widget.templateUrl); return configuration.identity_approved === true; } @@ -68,8 +64,7 @@ export default class WidgetLifecycleModule implements Module { widget: WidgetDescriptor, requestedCapabilities: Set, ): Set | undefined { - const normalizedUrl = normalizeWidgetUrl(widget.templateUrl); - const configuration = constructWidgetPermissions(this.config, normalizedUrl); + const configuration = constructWidgetPermissions(this.config, widget.templateUrl); const capabilitiesApproved = configuration.capabilities_approved; if (!capabilitiesApproved) return undefined; diff --git a/modules/widget-lifecycle/element-web/src/index.ts b/modules/widget-lifecycle/element-web/src/index.ts index 843638fc29..7cff3faacc 100644 --- a/modules/widget-lifecycle/element-web/src/index.ts +++ b/modules/widget-lifecycle/element-web/src/index.ts @@ -6,7 +6,6 @@ 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; diff --git a/modules/widget-lifecycle/element-web/src/utils/constructWidgetPermissions.ts b/modules/widget-lifecycle/element-web/src/utils/constructWidgetPermissions.ts index 9f4ae1346d..07943c1b82 100644 --- a/modules/widget-lifecycle/element-web/src/utils/constructWidgetPermissions.ts +++ b/modules/widget-lifecycle/element-web/src/utils/constructWidgetPermissions.ts @@ -1,5 +1,6 @@ /* 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. diff --git a/modules/widget-lifecycle/element-web/src/utils/matchPattern.ts b/modules/widget-lifecycle/element-web/src/utils/matchPattern.ts index 7531474569..8271649291 100644 --- a/modules/widget-lifecycle/element-web/src/utils/matchPattern.ts +++ b/modules/widget-lifecycle/element-web/src/utils/matchPattern.ts @@ -1,5 +1,6 @@ /* 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. diff --git a/modules/widget-lifecycle/element-web/src/utils/normalizeWidgetUrl.ts b/modules/widget-lifecycle/element-web/src/utils/normalizeWidgetUrl.ts deleted file mode 100644 index 794db023bf..0000000000 --- a/modules/widget-lifecycle/element-web/src/utils/normalizeWidgetUrl.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* -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. -*/ - -/** - * Strips query parameters and fragment from a widget URL for matching against config patterns. - */ -export function normalizeWidgetUrl(widgetUrl: string): string { - const url = new URL(widgetUrl); - url.search = ""; - url.hash = ""; - return url.toString(); -} diff --git a/modules/widget-lifecycle/element-web/tests/WidgetLifecycleModule.test.ts b/modules/widget-lifecycle/element-web/tests/WidgetLifecycleModule.test.ts index 9970a9aa00..d513a083f9 100644 --- a/modules/widget-lifecycle/element-web/tests/WidgetLifecycleModule.test.ts +++ b/modules/widget-lifecycle/element-web/tests/WidgetLifecycleModule.test.ts @@ -47,7 +47,7 @@ const createApi = (config: unknown = {}) => { const widget: WidgetDescriptor = { id: "widget-id", - templateUrl: "https://example.com", + templateUrl: "https://example.com/", creatorUserId: "@user-id", kind: "room", }; diff --git a/modules/widget-lifecycle/element-web/tests/constructWidgetPermissions.test.ts b/modules/widget-lifecycle/element-web/tests/constructWidgetPermissions.test.ts index e84881acd0..25bc60660d 100644 --- a/modules/widget-lifecycle/element-web/tests/constructWidgetPermissions.test.ts +++ b/modules/widget-lifecycle/element-web/tests/constructWidgetPermissions.test.ts @@ -1,5 +1,6 @@ /* 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. diff --git a/modules/widget-lifecycle/element-web/tests/matchPattern.test.ts b/modules/widget-lifecycle/element-web/tests/matchPattern.test.ts new file mode 100644 index 0000000000..0cc2c25c03 --- /dev/null +++ b/modules/widget-lifecycle/element-web/tests/matchPattern.test.ts @@ -0,0 +1,35 @@ +/* +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 { describe, expect, it } from "vitest"; + +import { matchPattern } from "../src/utils/matchPattern"; + +describe("matchPattern", () => { + it.each([ + "*", + "org.matrix.msc2762.receive.*", + "org.matrix.msc2762.receive.state_event:*", + "org.matrix.msc2762.receive.state_event:m.custom*", + "org.matrix.msc2762.receive.state_event:m.custom#*", + "org.matrix.msc2762.receive.state_event:m.custom#state_key", + "org.matrix.msc2762.receive.state_event:m.custom#state_key*", + ])("matches %s", (pattern) => { + expect(matchPattern("org.matrix.msc2762.receive.state_event:m.custom#state_key", pattern)).toBe(true); + }); + + it.each([ + "org.matrix.msc2762.receive.state_event:", + "org.matrix.msc2762.receive.state_event:m.custom", + "org.matrix.msc2762.receive.state_event:m.custom#other_key", + "org.matrix.msc2762.receive.*:m.custom#state_key", + "org.matrix.msc2762.receive.room_event:m.custom", + ])("does not match %s", (pattern) => { + expect(matchPattern("org.matrix.msc2762.receive.state_event:m.custom#state_key", pattern)).toBe(false); + }); +}); From 782b008b54906a90af58cb92929049047d551f00 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 6 Mar 2026 13:56:30 +0000 Subject: [PATCH 03/20] Fix e2e We need wildcard to match query params etc --- .../widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts b/modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts index ed3ac965d8..83ad62ec65 100644 --- a/modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts +++ b/modules/widget-lifecycle/element-web/e2e/widget-lifecycle.spec.ts @@ -53,7 +53,7 @@ test.describe("Widget Lifecycle", () => { config: { "io.element.element-web-modules.widget-lifecycle": { widget_permissions: { - [WIDGET_URL]: { + [`${WIDGET_URL}*`]: { preload_approved: true, identity_approved: true, capabilities_approved: ["org.matrix.msc2762.receive.state_event:m.room.topic"], From c2da26542e12a7844a1dd8e39ea48ae877521a02 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 6 Mar 2026 13:56:44 +0000 Subject: [PATCH 04/20] Remove double export --- .../widget-lifecycle/element-web/src/WidgetLifecycleModule.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts b/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts index ce17d783f8..144a799291 100644 --- a/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts +++ b/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts @@ -79,5 +79,3 @@ export default class WidgetLifecycleModule implements Module { return approvedCapabilities.size > 0 ? approvedCapabilities : undefined; } } - -export { WidgetLifecycleModule }; From 7d183ed15b2024acf2c2db18d14a4751413b68e9 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 6 Mar 2026 16:20:40 +0000 Subject: [PATCH 05/20] Fix coverage reporting --- modules/widget-lifecycle/element-web/package.json | 6 ++++-- modules/widget-lifecycle/element-web/vitest.config.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/modules/widget-lifecycle/element-web/package.json b/modules/widget-lifecycle/element-web/package.json index b469ee086c..4611eaecda 100644 --- a/modules/widget-lifecycle/element-web/package.json +++ b/modules/widget-lifecycle/element-web/package.json @@ -9,14 +9,16 @@ "prepare": "vite build", "lint:types": "tsc --noEmit", "lint:codestyle": "echo 'handled by lint:eslint'", - "test": "vitest" + "test": "vitest run --coverage" }, "devDependencies": { "@element-hq/element-web-module-api": "^1.0.0", "@types/node": "^22.10.7", + "@vitest/coverage-v8": "^4.0.0", "typescript": "^5.7.3", "vite": "^7.1.11", - "vitest": "^4.0.0" + "vitest": "^4.0.0", + "vitest-sonar-reporter": "^2.0.0" }, "dependencies": { "zod": "^4.0.0" diff --git a/modules/widget-lifecycle/element-web/vitest.config.ts b/modules/widget-lifecycle/element-web/vitest.config.ts index 29cb2a09bd..c9ca3971b0 100644 --- a/modules/widget-lifecycle/element-web/vitest.config.ts +++ b/modules/widget-lifecycle/element-web/vitest.config.ts @@ -6,10 +6,21 @@ Please see LICENSE files in the repository root for full details. */ import { defineConfig } from "vitest/config"; +import { env } from "node:process"; + +const isGHA = env["GITHUB_ACTIONS"] !== undefined; export default defineConfig({ test: { include: ["tests/**/*.test.ts"], exclude: ["./e2e/**/*", "./node_modules/**/*"], + reporters: isGHA + ? ["default", ["vitest-sonar-reporter", { outputFile: "coverage/sonar-report.xml" }]] + : ["default"], + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + reporter: ["lcov", "text"], + }, }, }); From 75464c163493bf1a8ade6c6830390b24856ed994 Mon Sep 17 00:00:00 2001 From: David Langley Date: Tue, 10 Mar 2026 14:19:31 +0000 Subject: [PATCH 06/20] Make sonar happy --- modules/widget-lifecycle/element-web/e2e/fixture/widget.html | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/widget-lifecycle/element-web/e2e/fixture/widget.html b/modules/widget-lifecycle/element-web/e2e/fixture/widget.html index 271e18d163..1667a73c44 100644 --- a/modules/widget-lifecycle/element-web/e2e/fixture/widget.html +++ b/modules/widget-lifecycle/element-web/e2e/fixture/widget.html @@ -1,4 +1,5 @@ + Demo Widget From 610a9494d141ffa54fe440274ae6a333753a7c6b Mon Sep 17 00:00:00 2001 From: David Langley Date: Tue, 10 Mar 2026 15:37:50 +0000 Subject: [PATCH 07/20] Fix coverage paths --- modules/widget-lifecycle/element-web/vitest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/widget-lifecycle/element-web/vitest.config.ts b/modules/widget-lifecycle/element-web/vitest.config.ts index c9ca3971b0..7ec0ff7fd7 100644 --- a/modules/widget-lifecycle/element-web/vitest.config.ts +++ b/modules/widget-lifecycle/element-web/vitest.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ coverage: { provider: "v8", include: ["src/**/*.ts"], - reporter: ["lcov", "text"], + reporter: [["lcov", { projectRoot: "../../../" }], "text"], }, }, }); From c058c187c46d0d1516fad859a6cab025600512d7 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 13 Mar 2026 16:19:27 +0000 Subject: [PATCH 08/20] Tidy up --- modules/widget-lifecycle/element-web/README.md | 11 ----------- .../element-web/e2e/fixture/widget.html | 16 +++++++++++++++- .../element-web/src/WidgetLifecycleModule.ts | 2 +- .../widget-lifecycle/element-web/src/config.ts | 8 ++++---- .../widget-lifecycle/element-web/vite.config.ts | 2 +- .../element-web/vitest.config.ts | 2 +- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/modules/widget-lifecycle/element-web/README.md b/modules/widget-lifecycle/element-web/README.md index 7cc0bc734c..cfe2569e4e 100644 --- a/modules/widget-lifecycle/element-web/README.md +++ b/modules/widget-lifecycle/element-web/README.md @@ -61,14 +61,3 @@ This is useful when widgets have multiple routes or capabilities include variabl } } ``` - -## Copyright & License - -Copyright (c) 2025 New Vector Ltd - -This software is multi licensed by New Vector Ltd (Element). It can be used either: - -(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR - -(2) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to). -Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses. diff --git a/modules/widget-lifecycle/element-web/e2e/fixture/widget.html b/modules/widget-lifecycle/element-web/e2e/fixture/widget.html index 1667a73c44..4c293c5c6d 100644 --- a/modules/widget-lifecycle/element-web/e2e/fixture/widget.html +++ b/modules/widget-lifecycle/element-web/e2e/fixture/widget.html @@ -1,4 +1,13 @@ - + @@ -19,6 +28,7 @@ let sendEventCount = 0; window.onmessage = async (ev) => { if (ev.data.action === "capabilities") { + // Step 1: Element is asking what capabilities this widget needs. window.parent.postMessage( Object.assign( { @@ -31,6 +41,8 @@ "*", ); } else if (ev.data.action === "notify_capabilities") { + // Step 2: Capabilities have been approved. Request an OpenID token + // so we can identify the current user in step 3. window.parent.postMessage( { api: "fromWidget", @@ -42,6 +54,8 @@ "*", ); } else if (ev.data.action === "get_openid" && ev.data.response?.state === "allowed") { + // Step 3: Token granted — exchange it for the user's Matrix ID and display it. + // Tests assert on this heading to verify identity was passed through correctly. const { access_token } = ev.data.response; const hsUrl = new URLSearchParams(window.location.search).get("hsUrl"); diff --git a/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts b/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts index 144a799291..ec2ca1034b 100644 --- a/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts +++ b/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts @@ -30,7 +30,7 @@ export default class WidgetLifecycleModule implements Module { public constructor(private api: ModuleApi) {} public async load(): Promise { - if (!this.api.widgetLifecycle?.registerPreloadApprover) { + 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.", ); diff --git a/modules/widget-lifecycle/element-web/src/config.ts b/modules/widget-lifecycle/element-web/src/config.ts index 0746ae37f1..ac22f816b2 100644 --- a/modules/widget-lifecycle/element-web/src/config.ts +++ b/modules/widget-lifecycle/element-web/src/config.ts @@ -32,16 +32,16 @@ const ModuleConfigSchema = z.partial( /** Map from URL patterns to their widget approval configuration. */ export type WidgetLifecycleModuleConfig = Record; -/** - * Parse and validate the widget lifecycle module configuration. - * Returns an empty config if the input is falsy; throws on schema violations. - */ declare module "@element-hq/element-web-module-api" { export interface Config { [CONFIG_KEY]: z.input; } } +/** + * 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 {}; diff --git a/modules/widget-lifecycle/element-web/vite.config.ts b/modules/widget-lifecycle/element-web/vite.config.ts index 3e4bed43a0..cc1fa98eb7 100644 --- a/modules/widget-lifecycle/element-web/vite.config.ts +++ b/modules/widget-lifecycle/element-web/vite.config.ts @@ -1,5 +1,5 @@ /* -Copyright 2025 New Vector Ltd. +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. diff --git a/modules/widget-lifecycle/element-web/vitest.config.ts b/modules/widget-lifecycle/element-web/vitest.config.ts index 7ec0ff7fd7..783608fb44 100644 --- a/modules/widget-lifecycle/element-web/vitest.config.ts +++ b/modules/widget-lifecycle/element-web/vitest.config.ts @@ -1,5 +1,5 @@ /* -Copyright 2025 New Vector Ltd. +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. From 242d04cef62869039a3a9ee695101bcd1ab81273 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Mon, 23 Mar 2026 12:41:34 +0530 Subject: [PATCH 09/20] Update screenshots --- .../preview-bar-linux.png | Bin 4055 -> 4048 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/modules/restricted-guests/element-web/e2e/snapshots/restricted-guests.spec.ts/preview-bar-linux.png b/modules/restricted-guests/element-web/e2e/snapshots/restricted-guests.spec.ts/preview-bar-linux.png index 1771b8e67d756bdab76549a8b0fe543af2a71d07..877e466ef5fd5aec6eb00816dd1d9bc1a6e90ede 100644 GIT binary patch literal 4048 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 Date: Mon, 23 Mar 2026 11:38:04 +0000 Subject: [PATCH 10/20] update moduleApiVersion to enforce module is actually present. --- .../widget-lifecycle/element-web/src/WidgetLifecycleModule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts b/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts index ec2ca1034b..6ad15e08bd 100644 --- a/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts +++ b/modules/widget-lifecycle/element-web/src/WidgetLifecycleModule.ts @@ -23,7 +23,7 @@ type ModuleApi = Pick; * requests based on URL-pattern rules defined in config.json. */ export default class WidgetLifecycleModule implements Module { - public static readonly moduleApiVersion = "^1.0.0"; + public static readonly moduleApiVersion = "^1.10.0"; private config: WidgetLifecycleModuleConfig = {}; From 5d25bd08a1340f2a3a51b9504a634c1771bf0c18 Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 23 Mar 2026 23:46:48 +0000 Subject: [PATCH 11/20] Trigger build From 0607a32595acbb88e74f7e26992fa8639115c0c8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 31 Mar 2026 09:43:09 +0100 Subject: [PATCH 12/20] Stabilise banner h2 --- modules/banner/element-web/src/Menu.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/banner/element-web/src/Menu.tsx b/modules/banner/element-web/src/Menu.tsx index db46c76a82..b984c1b58b 100644 --- a/modules/banner/element-web/src/Menu.tsx +++ b/modules/banner/element-web/src/Menu.tsx @@ -89,6 +89,8 @@ const CloseButton = styled.button` `; const CategoryHeading = styled.h2` + line-height: var(--cpd-font-line-height-tight); + letter-spacing: var(--cpd-font-letter-spacing-heading-lg); font-weight: 700; font-size: 12px; color: ${({ theme }): string => theme.subheadingColor}; From c6280dd875f84f4979eb484b655aeb66dd8bc9fa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 31 Mar 2026 11:45:36 +0100 Subject: [PATCH 13/20] Debug screenshot --- modules/banner/element-web/e2e/banner.spec.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/modules/banner/element-web/e2e/banner.spec.ts b/modules/banner/element-web/e2e/banner.spec.ts index eddc2d67e1..d60be88334 100644 --- a/modules/banner/element-web/e2e/banner.spec.ts +++ b/modules/banner/element-web/e2e/banner.spec.ts @@ -153,15 +153,7 @@ test.describe("Banner", () => { // Assert the sidebar looks as we expect await expect(axe).toHaveNoViolations(); await expect(sidebar).toMatchAriaSnapshot(); - await expect(page).toMatchScreenshot(`${type}_menu.png`, { - // We exclude this as we don't want to assert Element's styling, only our own - css: ` - #matrixchat { - opacity: 0; - background: orchid; - } - `, - }); + await expect(page).toMatchScreenshot(`${type}_menu.png`); }); await test.step("close menu", async () => { From 21396c25be99b4cb555b6ef9c64fe51887a8347d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 31 Mar 2026 12:05:17 +0100 Subject: [PATCH 14/20] Iterate styles --- modules/banner/element-web/src/index.tsx | 2 ++ modules/banner/element-web/src/style.css | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 modules/banner/element-web/src/style.css diff --git a/modules/banner/element-web/src/index.tsx b/modules/banner/element-web/src/index.tsx index 447fbf4969..f4196af06f 100644 --- a/modules/banner/element-web/src/index.tsx +++ b/modules/banner/element-web/src/index.tsx @@ -13,6 +13,7 @@ import Translations from "./translations.json"; import { ModuleConfig, CONFIG_KEY } from "./config"; import Banner from "./Banner"; import { name as ModuleName } from "../package.json"; +import style from "./style.css" with { type: "css" }; class BannerModule implements Module { public static readonly moduleApiVersion = "^1.0.0"; @@ -23,6 +24,7 @@ class BannerModule implements Module { public async load(): Promise { document.adoptedStyleSheets.push(compound); + document.adoptedStyleSheets.push(style); this.api.i18n.register(Translations); diff --git a/modules/banner/element-web/src/style.css b/modules/banner/element-web/src/style.css new file mode 100644 index 0000000000..6e573485f5 --- /dev/null +++ b/modules/banner/element-web/src/style.css @@ -0,0 +1,15 @@ +/* +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. +*/ + +/* Styles to ensure the banner does not push the app out of the viewport */ +body { + display: flex; + flex-direction: column; +} +#matrixchat { + flex: 1; +} From f2608caf345c94ad2a3d344fa4085f71fd65ba04 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 31 Mar 2026 12:32:27 +0100 Subject: [PATCH 15/20] Iterate --- modules/banner/element-web/e2e/banner.spec.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/modules/banner/element-web/e2e/banner.spec.ts b/modules/banner/element-web/e2e/banner.spec.ts index d60be88334..fd5021e95e 100644 --- a/modules/banner/element-web/e2e/banner.spec.ts +++ b/modules/banner/element-web/e2e/banner.spec.ts @@ -153,7 +153,22 @@ test.describe("Banner", () => { // Assert the sidebar looks as we expect await expect(axe).toHaveNoViolations(); await expect(sidebar).toMatchAriaSnapshot(); - await expect(page).toMatchScreenshot(`${type}_menu.png`); + await expect(page).toMatchScreenshot(`${type}_menu.png`, { + // We exclude this as we don't want to assert Element's styling, only our own + css: ` + #matrixchat { + opacity: 0; + background: orchid; + } + `, + }); + + // Verify that the #matrixchat root got shrunk to fit rather than exploding the viewport + const bodyBoundingBox = await page.locator("body").boundingBox(); + const rootBoundingBox = await page.locator("#matrixchat").boundingBox(); + const headerElement = await page.locator("#matrixchat").evaluate((e) => e.previousElementSibling); + const headerBoundingBox = headerElement?.getBoundingClientRect(); + expect(rootBoundingBox!.height + headerBoundingBox!.height).toEqual(bodyBoundingBox!.height); }); await test.step("close menu", async () => { From e772a104c23e8d70638d601217c8c3ef64b04329 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 31 Mar 2026 13:17:57 +0100 Subject: [PATCH 16/20] Iterate --- modules/banner/element-web/e2e/banner.spec.ts | 4 ++-- modules/banner/element-web/src/index.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/banner/element-web/e2e/banner.spec.ts b/modules/banner/element-web/e2e/banner.spec.ts index fd5021e95e..edaf519492 100644 --- a/modules/banner/element-web/e2e/banner.spec.ts +++ b/modules/banner/element-web/e2e/banner.spec.ts @@ -166,8 +166,8 @@ test.describe("Banner", () => { // Verify that the #matrixchat root got shrunk to fit rather than exploding the viewport const bodyBoundingBox = await page.locator("body").boundingBox(); const rootBoundingBox = await page.locator("#matrixchat").boundingBox(); - const headerElement = await page.locator("#matrixchat").evaluate((e) => e.previousElementSibling); - const headerBoundingBox = headerElement?.getBoundingClientRect(); + const headerBoundingBox = await page.getByTestId("banner").boundingBox(); + expect(rootBoundingBox!.height).toBeLessThan(bodyBoundingBox!.height); expect(rootBoundingBox!.height + headerBoundingBox!.height).toEqual(bodyBoundingBox!.height); }); diff --git a/modules/banner/element-web/src/index.tsx b/modules/banner/element-web/src/index.tsx index f4196af06f..538fb8b3af 100644 --- a/modules/banner/element-web/src/index.tsx +++ b/modules/banner/element-web/src/index.tsx @@ -36,6 +36,7 @@ class BannerModule implements Module { } const div = document.createElement("div"); + div.dataset.testid = "banner"; this.api.rootNode.before(div); const root = this.api.createRoot(div); From 60698a2b7824b6cf6a36e9b3816a7d8a31b19718 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:34:08 +0000 Subject: [PATCH 17/20] Update dependency @vector-im/compound-design-tokens to v8 --- modules/banner/element-web/package.json | 2 +- modules/restricted-guests/element-web/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/banner/element-web/package.json b/modules/banner/element-web/package.json index df45f81985..0fa0eecb03 100644 --- a/modules/banner/element-web/package.json +++ b/modules/banner/element-web/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@radix-ui/react-dialog": "^1.1.6", - "@vector-im/compound-design-tokens": "^6.0.0", + "@vector-im/compound-design-tokens": "^8.0.0", "@vector-im/compound-web": "^8.0.0", "framer-motion": "^12.4.10", "styled-components": "^6.1.18", diff --git a/modules/restricted-guests/element-web/package.json b/modules/restricted-guests/element-web/package.json index dfe5c0f0f3..ab4c4d9469 100644 --- a/modules/restricted-guests/element-web/package.json +++ b/modules/restricted-guests/element-web/package.json @@ -24,7 +24,7 @@ "vite-plugin-node-polyfills": "^0.25.0" }, "dependencies": { - "@vector-im/compound-design-tokens": "^6.0.0", + "@vector-im/compound-design-tokens": "^8.0.0", "@vector-im/compound-web": "^8.0.0", "styled-components": "^6.1.18", "zod": "^4.0.0" From 52233836989ce9c311fe2b86e95d8360b5259b2c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Apr 2026 15:26:26 +0100 Subject: [PATCH 18/20] Update screenshots --- .../banner.spec.ts/static-menu-linux.png | Bin 12708 -> 12675 bytes .../banner.spec.ts/univention-error-linux.png | Bin 8472 -> 8523 bytes .../univention-menu-loading-linux.png | Bin 6278 -> 5813 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/modules/banner/element-web/e2e/snapshots/banner.spec.ts/static-menu-linux.png b/modules/banner/element-web/e2e/snapshots/banner.spec.ts/static-menu-linux.png index e6e47a126002dfe46ef192060a8866f8ea713261..81982d60c09dfb43a77e82ac92b3f40bbb6c230d 100644 GIT binary patch literal 12675 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%0AdrNrRBLAsCquxkQkvFnQRZs2x3lzuVavm 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 literal 12708 zcmeHuX;f3$wr-Fr5a}!xQ4r873ls!Y1QbY6kwQd31&K%#ktWhcB=n)N6sQ6f6(J%` z5NV`GdI%&41xOnN=}7`2P3RJu5E2sdRy_BN_r|#IHowjocij4wuy^*_Ykq6aZ_fFx z#h=$KOeA;7?SeoclBSn0-GD%T1YdXb?%V-B&MRFJf!Q8WX5akpkr@NAH|;+R*Jb`bHJtBVx%6c)dw}Fk$g51L@~AAWhy-+a+-jVzxD6@vT`1BniDEx$=y?+_KLU|hWSLIMn?9C^es&ssI5JE>KP za7YdUsXHJfalIhF@G9)?AY4Ki(%Z80&L6j5|4>}`{_@}^0|~zXu4#j74#vhUF$+US zPeLFL@sQ|YSJl(m4^=LLyOm<#t{#&ldO>%#1No03KbU3Tm|lr73+i@~0PtKkHVe3o zO!KcA^TD5dea8I;+sH}}-`d&=WsPe|q=$28*=AT}#=&dH_N`44vuYlE*yy8KzwVKY zJ9&?NeHtR=iXS~CFgh>k>WqZEj@L|AvZ?GqCEp+Yb1epj)jYcN_UDA+PcF9!eD06p zR(!iM)hC^g`h1iWf!nC`WgfhfVH8C)D*7}%w$k@?cOU!Why0)WV{fSQ4rB{=3j-?> zYeWyg!qhZFedzL@&0@{&36&M2O3miPe_#!BJrX{)&@n5$GuPV*zn_1^{h%oT6nQjq z(#qyoz-3-i#oawMSEqLX1ko=b%L4^)+Cyl6j=5Qe)p4{7a{r|h5D2#!c_z}*VCAnH zy^3M$pT*GbMO%NplY+8M9HhK04nQI;isB4@48b}C$h&{k4Xax$A9!QTqo+K?=$wbv+IE^U?yU8;oBtl^Hq(D zDbDfGrlkD*{9Igk$X|c_McQzT z2-wE(e11nf8M$1~&WnG}eHWXexi=5EiprOcm^+CPEXf9dwozF)Zvmm{9n2hn8}XzR z^w!t=Ee*f^@yiu?*%PNK(3!*<9brclfDi`rGTd zh6>sq*0jbKznDZ1oNP`#lajT|kC@Hj9aF(!RsleAB2LlguAaaqWn+8v>Gd;A@wBE` z8FN_#<}=NZZR*afP~x90Ow#ZJ#CrRE5epL2E16KNt*z~35V$;k@}Rtk6Z5l)Qc+Qn zxy|-hLgpDbw@Zukc-lxBYOTk3?fmSZVK~N}JM0`EWlBD|Q|rT92P2+_LtO=0W<6lc zn^lhE6;{~Q)-~&Lun}zR^D-y58uav<=wm3VW-@xRIY#q##j!4)z%#&ti;b};p>zPZ z&cmLWb=-D^53P@y>KEYeuY=P}4O~WTPqpQyp;wQKK_I<%y(K4vrOswp6WjAgMsGmq zfL5|Ol7!X7l22A;bdqcGO~GbrEK zxdWn6eUG(F?v(FTm%FjnEZ-Spz~`=21K;x_L1pjOeE4w6{llsR4zgBwDb~_E9N1HM{;#Rp_w@_@zos`bDN>mc!HPFLK*3X=nv%k2Ij_JrF z8caRun?E<39y%@^VQtG^!J&SDKwgDqYBfSb*5AK>e=IDZ(|^c;h2n3ZoGdnTQhauO z;5>vD`Wam@s%3}_d6cs_YH>VQz+tu8ktS0i7`Zn7Pw^ua7SK?-I5#b%Lj>qj+ z*IO)Y2DEn};Wk?1ZMb3SbSjmr407jStBRg*YPTDa-Hl3WbvmtMeed-Tu=QS}PU()@ z^+CSwCm($K)OR>*@gOqQ_v4SUM?I$i`lqHWSHoMPBuRH;VW_0ScevEaDVUaxjV6b0ui>yW z4^5sNIOW#$8c=TOx)X64sSg%+83b89hy}m`jB!7bq!P@)mYLj~Z55@3nV21M#X8K- z&%eBx&}9i_TEC{LFR>APCc>%boP-V;-G7oWpI^{)HdSZe0~{=D513cx3y=1-R3m&( z>Nzc1X59wy8Ho@rQ&j)CAu7KBwrw+6yEc<+qhjkX8y5Wh4Bn6$2G#xDa&ixE0+Oztkvr89UA&^Z`AqiX=y6zX@v+9vTkcqGz2VkzrXi4*(j9Jrr< zzX`niUXC&GXCiwh<()~q)FaabW!LhNEVFxFUH}bf^PP>w5iyL2wM)~b`*qh&?2A?7 zn+-aKOdH{>lF_5(B@oC&^Np6fp5Y{sx|jbn?YMvNHAfvKPzfL>e>Mg7WF7`{hWzn6 zD8?X%ckMU;BKz3RJO8R1&n;k*@s`PY3zfPrZQSiYe0>P)}-tt z6bu6BP(sDhAmCO=#qjMP7*KkF0q11SY14eXX86Yjz$uk7Z{X)A|_wI%t8@OM8oN1w2( zGDvmeP~CF+X+^(DkY@`J%LC;CM`pF!?6m(_71cN*+7wyY6Vs7CI*ki_AoY9UfkP?TcB!kFE14K zFFjvzj*%Y2^o-H%Bi3-#RA{bVie!P4v$?rH!1??c0ZQ)uNHm?$;~2V_bR|3ejZOpG z>@D2T@ZP)KxCqV~G+R9J5kGkGPao|8VVyapi|hFd+ZCgJj+`w|#qjkNb6v%Xm5ir~ zTwHquVT)p~!H@AorRzp~JR?FL<0K-t09~#_9 z4&pcKQXZJNZ-M3G_7x6AOHkI+0%s{Yr1oQ zxO}xzZMQA~2J0G6Urj(noui!GY&_Sztx4d{CpE`}(?1zC&rg`^ZjW<#7mfKdUHzag zk~s8(smAcFBfqAX<p6Tfr@oqkoX( z)lxz#k3`}yoMhgHoK;9=V2PvC2oXUoJwaAUEiCL-bULVdW|7}fI3zD8XL!G-FCle^ zUhU}a&vGz{a(#QPhL)tS6yoyW0Bi9x)RxI88rZ%pzW;Oi*j_6U{EPaqgg_C!?J!uYVoE?;IA++><6!`wKt9*SQw;s`)6K-h$+qqOCDzq? z{t+S2D(z0(|0APth;r*ToCv&2Y8*xCOOi*`PAvse#1Yv8cx71WoypAE^oUbWOeExQ zjBa-Mkr1K%-f{aiZObZ_#ea!T%yCIsAUaZyJkk|Q3>{!bbj*qazL7f}^aWcbsTuX> zxI3w*#;+-CIxUpB_>Zt!-fY;r=%$Pe+_Id}6`T{ZTG5oyy3gVaQ5mm2XZk~I#!p5QU zu~&KhfGoyl3-Iw2S8Mkv3;ieHWwo01>mAR=P+QDL(zlk9BN%C@MFS7~K`lf`@Sx|I zmo{d6xZz@dv@nve*#S6cJfvW98rrtpVB)|l`;%p8k04THnvDOc1uWuXB3SbHC!9V% z67ghc&)6h#bM(Si#23TK_b8<*;9pqY_NW~_wPu>})<+nS{vYP^@0Lrw-}bc~<}pM~ zl2pPUfv-_NL+2tJ_D*P_1hxBssSUXmq4)~hEeb-RUZV99Euu=$ z2aZA@jr^(rq14Vz^@#&M%C{O#IK)G(vU93-`fd)FZ`oBq>UXuV1A1-cCca&52- zqLGi7dv$dONXWKv%R(BjFa6%ro+1l@bll$G2wT~yr-E{XK#tihkc74)F@)!+v9M?$ z86=p-lz{XM(YuG;dl)HBK}9WzvBufZNX*zg@1geW7x&?h6F<=xRbe7&tj zKRAANkx0^5xgkht^Wy6EJ;Gk80d6KZ+M~zAoxN}{s zRH{%04Y)FpM$kgt~Cmv}SpWYFD<4tD7q_3g^?N}-FV z)W?G-G9I!F8I767aiFty7=1v3?iGY(?CjaE(T`k)RfgX59|$JZ9lI6gmWV}rq*wk_jMMz_@S*s zt)T50^#1C%HnXu9DFs12s{)KgpHq-&Ozv=5{|J?zUyC2FLeC2`8@613dy;PoJ8tJ) z>k10Urb3iEb2)TbtTQceq_)1fJvUX)w=ZjDO1fq)bIq@IJU)CqB1sdG4hHP?_y1D3 zsPhibq#;(mT6M~jI{D0=y0kXtuq%eORcv?T#L*%xr>|Kvmf3Yv&&H~T?+(BG)ohK; zqAwP`e?Ry6orycBe5Cd6ab@6KSQpQy=5ur)IhbW*_w0ZU`ZAj#-eZWe<&{Z_cv;>zr9xO zPp9w*TZ%!OZ)31u)WbHCX-=J>krpHj2YAd6jL7bM5vjagk0f=9b|I#hoTZku*Vm|O6yk`HkDi(3eI?)_Hkd=_7EySHKC~68YN!P*l)l0Ki&Osw2meiskjnA>Y;po`cuF; z!I~Cc0Q{DC<>_#(vU~acJ*>R|qu#fpXfd>Mf?^l+( z+MQ@>q*69dx5$XQ)z{1R(_wMM)$+0(bw({5c;;Ec?_`OCsZ$$*%ug&3p5yd%dQob&+Mgtmz zDg{1)t`88OF!qx*3>ezWwue2z8lEm8ecEpC(0^j}v^K|`-cWXj{qj0)EE8o;Dm|58 zDm@Z#zqu@$vcS9o9-r9zIVa|hHoWl4v^cx(td+y{v^)t!A}y9PzUo!C`ZSx~P;)P! zxMtN9-g5VYZ}3!EUG&L&lkI^c{$0_+y6Ry|<;-m3* zf4vT@^GLf&;)%F$tZx=0`u(Vqa;s``tLFZ-(f+C)O{dQ=3XM_P-)suGNy+FX&Vmnv zy)BdI=_}0WJz`{`x3anS8GkZTQfHGq4d+hGO*ORwjX~W+f_kr6atPz~X+~V5{w!G6 z!*`e1y9+5}%#wqj@pc0p@gj~DnZ?Kfy*z6V+u{!<#^zQ>sxd9G7u`i_uG!b$XaJBEVNlqe_o&D3iH#T4rhVk+o+m2$~$k&hBB8#>%>I63Mj z?n{Yfk~HmgoSl4)VP{aPiG^IM>QhOzneJr6t?6@}Ng9^7r{fUgdMA+GA=)PliS5t* zNch9H{;o`>?l}6>5b+eI(_2cP`$vk;w$+uC6=kK)lYE&z-bAQpT zDlRDJxBCl&s|f_JK0Ltes%nFeKdV%t#LY9GBW;iBEqxlYzMNjs@wFwcxhyoE?;muG zVr9XL_aS@q7);GNG5<=iXh1e)`41H}uyYog=7*%tot4N}RTESdnO}N@Qg>N3wA>|G z`;)EKUbbHR@S*fuUTrPSOx~~dV}q!RR$_Ko$iPOSZElW_Z&=v|C1jyUt)$+AG0|PM z-DJ~UjR@=ZM~u5IQ28uZQN>dJ0Gt;-+|u#Mnv_^LcqeRY z^xV#Z{zMttRhYy3+a-1Nz8`sh98FqPFkNUe+hssSgcR!eF-5c6$D^x~+Z9OF`mnxl zPIIZ$Of_wrWTxTeSPn$|{#-*{x31;vr^#2{SY#8+-hV zje4}C_qNAzv|5V8Bv9hSqcR0rOhy(@Q_Mmo&;C@zH`o@vj4qW2cCQdJ;w|tTf*$Yl zZ1XtkR=4&vHOzTA&XTxiN9A&oI1*~ zJDX}UPix&@6KbxKx3?X5BkiR3^$(O+#ph_`HfQl#ZFe;;s3|_jA}!zxE52c^2T$as zy>rCSra?40(XvU>a%WsS`dL&yR{D@2&l-%XM;7$S9Pt--s?t5~(y!K8MHg9EHVD}A zfqGuiFTCfaYR--ho~Wh37s)wA@n$sFBv#`Esv#oF&(wuu4WZvD62FIheuB-OTC=rTut&(Xs$JQQ&x~Zvw z+zn=qxiK0deIJkMc`mI6(93vNc|1~!hSnI8BHffyY0nzt3nzo0WfYoY&m#*|<>XRq z(zAc=uan;aeB?d#{!0(|B|%DiF}klYF;T`;)!nO0-SgIq3Y*tkMa5D`4-)7 zJ`1**n`6AgKW%>C90bT&{M&p&CrRhC;~hVW$uMf=v2NR z1d#G%2v?>^OnUW9S{z+7?p+|oX*I#pP}`fcG3M>}rCJ)#X|>bX;#JbnA)S*(WDy`X zG^!}yJh6Z8VIRNQZp$KzpgeX)y%!9uM^7eM@pr-b37qS{PU8k0d&+~_)>eDMwad5F z5-gDAGKn%b4uR=NeaTt$^5|&L&zQvyo8XPB^dy! zt?N|78DT+#UNYu|=OGj^_asFfG40v%QsZZ^oxg%1|0)tjMwXLrOg*D%fsPD@sGt5l zN}A{E-dD`oSkm_z?%n2b1V3c>>Ya=X9dxT2ep{ER>R_9=>ye7`mr8nndW^&fm*ZI? zs0=fH)MM(n#q97v#LK@Ps4CG+v58d)P!J({ zIqM}s&NHzsYpW3r<;AzI1{|b92Oh3ndTkgNdfuHw5vqLVF$>sitbQys`6C2c4tc^Cn%KK4m2K7v+{dn zUJ$?^O4nB!ey5ru<)Amy>)4+A7El0_$;3KrPzA64F#jB)c)F|($T7={IZ9r9j&2M= zju0L=AKJMfo`SL?2cA!X?XK}JuAT3&vOGs)!MBNGd%uHlDUYZ5&;t2VV12DGZ4oQK zAWP&Qn@P4jfn&rNJ1N%WD<2Jn*@l+8H3WQ&lEuPVN_a$Pykr8Y?NHbk(5(jawew+j zHyKZLMg?ll;nPb>jZv7>(-cnba*5`dh4CNh-{|#T*J%TT!ILer&G2CV&Yyk>S)cp7 z+H53@Kz~FL%Vr6NXE`-ME_J5)I01{kzEIsCXX>f$ShrZSL@^z0Zm}Aq54^M6fsvc( zNx|Jta~V*I?|8IY#s)vu6W{w5eEgd_8C3=Qu`VZ6-(F-yLj}wyQv0421ldRXNq^|5 z_kN2#76OdV$GG?Z+B=dT>)RGs^8WHaBK3J;LwY4`@?CzL;uthlRq(7-e@^asQS(}R zXJ-w>J#${t<65d$Rc7Dpo{zb0RvF+7(gnM6t;YocfP*H=E%AWo{!4lEKh#bCzVUxo z@xRiT|HkTXto{zQ|9B7P@A&v16CdZn5m<@8*Xw`GdVz`l|8_n9UctXt@L!UV6!ITd zKp;Zeg8vctUr}sgvaD4>(S9C&M6gr-pPp%hZM%~cMI|BUWe-O_OuSKW6g1NQVs|ER vF5m!x!!RG5bKvuTxRG20d{smMpfJQxIVP%;(>MpNKunD-E)`$A^ZWk+sJu}ED%NIPK=lOg-2cdgQON#(}ytK4LMs`bcbMxUNjYp0giGN%l6&2O)g(yM*eC*jU zKhoIPaHt_N+K&$YHZ33oKv!Xd{qW%<4Z)yJZ2=(wx|GJorjHID6f**#i?fY`gNF_^ zH8ty44x$C1i;0eo{=^kmL=q603J?OIE3y`R@)Msp`qDy63n2iysBYb&uZW3ZMgVkm zJ$v?uY!ng#pnvPK3Vh;<7`xD20J^a5-MdE-0-%etNkNaEJqQ8N)%CDhfdF)E(b3%q z0npXi>VOacU0qZZn}tA^$BY0#2q6GK2!P8;b8}0R{U7=fW9Rep0ivU$qND7;XuEJu z@5s+104^7eO-+sApAm53x_682-mP25?@tJTOE2_8#D5x_I(IPI7GTfr-CBPP(V6j& zj(|%qv>fQ{r(`>z9kzpaCIrAGW)bMD6~RgDupR6RApkD1=9Y_6frt)H3NFg42KITC zZfxGQ`S>Sj>y-a@bM#H$k9J%k0Btp2%ze5J9=!863;({S<+*81HE*|b*j}H}{Xg#h z<*%RH3x6BH$6LeUBf6MkdiFT$eL6V=fe-Xx@{xmq)7~K}T92&h9)DBUYC&yn?cTk6 z?ZWlfU*D@&F9B^_4k75|rNQf~n_4UI#s7@*-V|jczqv_!%3I$0UF27>=_d;szmnV| z>at0}^5x5mii+A^6CE8rY}l~O%*-!+=}YPiuYbP!>cbB|Y!}MQ%ZCmfD&R8H$su_2 z=SO!1{_|^5kF7u9;6=|j7c6PA3-7+8x87_SIP`KU!C(IJm(tSGldjykbLZm4i?gz_ ze)OXs@lzXc$vw4fY2ABuOP_kG^_uL5AHJ&JRS!;|q0Z3BuNpLbbo9a(2A@36)9#7x z;eR-Kxw`Cf{4S;;Lxv=U4IDU7;g?}ou3R~G>{tPp+QEZ|e*EKSAD=rn^5&e!9cTVHtL zg&S_TA#(Z8fBv%-;a~sy*MZ`AA{Sc>K_~$?W`PN%+*@Y{vxPrw3&g$`b^FBOqVA;|i#>6-p8=IbfYT2yWkAFQp zGh3bHf@TEu|5CwoNB;Y_&Hi_f4(v1kONZ9c-|qN*`Q?{C``OQa;~U?&|Ni@J6b&Cf z{2%}LN93kOix$Pl$J+?J=bn3P3{Rds`I>94`RZ4{T2oWg`tD_AWmjEwmCNP&@|VAC zx-t%}o zx7>0|WN6u_{?@m?Wux7mkpK9{KiU?(J>Nd^$Rn-qYtP{Y1qFZj!ykVC``_EaZ@lrw zF=NI=ZfbqqEjl$6-{{OM1BvYEoXdGj(dGV1H=ZSdPxg57Gf0E@!= z@4s)SZMWWa*Ij`?z*ZKsXU{(AGqro#N8i7H|0h5B$zT5RmtX(-*9CNpqo+d`#|147 z>i>Q64F7aXf7?6AeWFAr+U4KFdYy>{|;DgJSEgLanMC2NqFn{Oe<;BIt*>wGZ z2Ody(BrjdMv~}ukbBEmA-2VOhZ{EE5_S)Pw?z@pYr=_LYn|71U1uSZjSxM`WZQ*n4t+!gmoKB}z zz+O!h&@r|&6#UYel{uS<#f({{Bd=XPTmi19ZA3Wujap29L+*glivAL%aLPG z?Oz-mb<+0~*^{KLnNw0y+Fogc?VI2HrtRlj2&@z~mh3sd?SCC?4BP7ZH^2Ffh2ZVC z-?jm7&&em8OjOkF-MekAVVCWFBNd3SrHB1#lM6e@>1G7BK@hoMO8~p+*T4R?!ck~F z&&c4nXZ}5V_SnkB_8M)f^4xRJtzEm;p7U+mziQR0PkriBY7>I- z?5CR%*rr3|f>py-@{!ek+gSWZSmZc1&$s*A#AN;Y^?%m29bqe&4#@b|uwYi3Eh5q)pza21OfPfCL3Y>J!(q+rkc{(`+Y4>zH_+dMv%SM9xT3!FG zwr}nbnefQm`4!if>dn2T3=Q$w!8VYuYJw_wCin~ z(0?Wowz(+aQtRXp+;aDMz6|m7(@)!zWO8!yxN+lb;MofYws-jClTWs8r(GR3Y}l~7 z@4nk!qT9N4tA)VYVf%u1tL+r7S+mCW`KM2xZnK7>q9WVIv+c&nPGJYxl%(yOmL%Jr zu-Sg(O&i{}u;0FY`?9del~$#n{NyK=xqt6|_q(={X;YZBVXX(TS1WB!(RLfcW<9UJ z{<^imCODB6oV08K{`Xxj-#lxxYkOtxsi&T@!4`S#@Mk~!nLV$zz0ZmjD{O4rtAGl3 z!CGL$DRPqy+F$(Q7giH{b->=;>R>5pjfg#U4<0O0T{4+SV)WL)a-A05h37`x}bl3xb@}6ciuYtyAK=D?fRq(`i2FY zE;f>V1~`Tzy?s|7$Na z1h!IW{Z=9Xo$f+^FiBIR{`Pju2k*6;UkR~@28bTM@9AQ zarUi1ey;&=iFM-dH91uWf71y+#TdB6xU!a#XsD~+S-5s(?y3!CwGFy# z1?wudZYV0Px%j8pP*eU|;ns?ppm6Rw7J~ZHM~8Xb{ZIVw+Vs^`r~RTgRBe59>f9Mq z7j3Tz)|bsp=R|a%()xI_<-rYYwYrDVA8TeM;Y7c|q^UBWn(d(;M&Ym`7>a?6v zzb-C+*@|gX9-1~iuaf7U^Kec}gFeY))8d@R-s&AU#N{~MU1L0H3l;|}979uM9qP=1 zP`e>BWk#thSV#HyfLE|?Djmmr6?;dMy|xHg*k)XcaQM)7S7qZguv+;m6d%* ztfNDBj&ThcJ$Xp@B8BdBK_j_51KoWu;|hd0ViU(qN!0&>m;oa)2Y(3X=fd|00#$`` zGHxE~>F@CjOPe@r)oWGZxnkYciPwAF!)ES0W%gWGQ7~^xdeUH9TnrtNIcG!FvAJb~ zzbxMl74Nw=e#E4O+iOo*chr~9yD4c#>0xPq{`b7@TUS*zgyyI_^Jdx!#PuKOy(v8_ zXH(VL!c$kVDRcsMdVhn5j+``eb;30nGUTZfEA3bHx>g~RU4xZc5Gpyb) zfAYv-13mWcvo`H*J8N$6y_Pe6#IS+3s`B1E{?VLG6?L|NS~c><`$9_xfAORnJW2D* z>qFE2(v?%v!e5{Gq|65v=byO14t)_ki4X0vWBb;O9b_~1B7c8m?c<%aaPg#+xN8TF z&bQMJR240qo|$5wi)UC$#*9_lB4^X!d-;xmwyay6ziRreLp|3H9XX?*qONvl?&J~I zhd$zn83k46TJB@VTv=E!Z&F(LBc@F+tZrz%sej7iiv4BtQ|xmek#lV7-cY+CyyBP` zUOKh^s5tGB{eRnL3{PFK+x{A=my8@Rc+7^HKvhoY_@nb8e`RO+^xHYruI&Ot5Zs$T zE#skA%7SsDC)}0cRoVJwGiI+k!y~DoYVDM9bMto8_D#5D^ayvL{FNEso4h*ow;rfi zGdW}SD6W z;uqoGjHEuHi*YFv@0&I%?&uuNzhhm#&y{@p_)#8*zc4#v>I;RH8Z`2@RF5h)E}JxN z&bHP)lHl&VB?Yzap~-Q<%8hfg#*Lp<=pT@j;(v7=sNOjD(e;(*Gcba?moIpxLhd1P zy@IuS3KuLY+S@jlz2Zit#M!tl-RcWJJR1CO6qO&4d-&)i_vs%Mr#rG^KD*VAg?XTYhm4$1|0)0lkxG6U^G@$EBW{w)Sd39d-sCkYJg%DilOZ75eR9(Q|g z>DmpIX|vnkL5Q4yypOIe%<|f#x~6FIsDG)&g?YOsdDCO#(&xYEu5k{%d5|q={X2tc znH#H1s{NTR-`UU29@*8KUM{sqcFOXXR^4hRQ&;-v_a@}~j-Gk)^k-J51_mW1#m1;% zue1<4qS9Tigu7iBU)Hif0E0+}e?Sx+}SdbY0TxO+bu75pm z$K>R(kpa^yb#X!ND7R|1PW;Za?LJq|&(~%SaMYK~OdYqmx@?bsf;aY@mmIy4=WWUx z?GAr^(xz;w++OLw!+reb-f?MnB!8@(UtauL^|T?0j@t4~6#=x$6tk51ou-14=lJ|}exJ{tx*Xy4Yr7h1D=I@zl0|nX6}7qAQTu+NR)4kjX!Z=h z*&E(fvWI)B*R`csXB6<2+tY?CX~dA&@Y}uPZcex+w{l-)wZ9?J8FShb7ET0&BCF}f4=s_%|idSSz{)x-zRtcs1ZYxV~1Dezfg4c z*9fl3czVvQ?&HUCI6W>$u`aRT-a=dE6xTZXC664QoSdFq_KZ#5)hX)B7mpjCTY1>! zO-W6NACT(buwrv{yMK2KO_CRt_qMq$tKUFwxWIgypZ; z{)Y8=P~yPzdJ)exAkoI_;gbAC5 z))h@n_m`x^IRh12Ua$1`oA~PRn+ACA7(K9X$)3Eall>#(o&Gn<+W$ToXKZNpy1!(_ z!o{^?CXG&;GPa=X`PXOMeYh;4PoTQk_J=&TPs{Y4?pq9;-igyj?Z_@#GI`S8G>>C{ z*~VhOGk<=5R*Ef&YeTc;{oC`N84wt%l4o;vgq8-Ock11tbBl>f&rB+qRT?^(lF-TQ zsyJ2)htu8Hsp`stBtKAY#d%@f$V ze8ukbnUloWj>DZB#=T-E90;Fq;g8{anyYebu7=1&5C(i8% z_^N9UhgRAdV?wXmpTpLoy{J6gy=C+Ejr)Vnp|)8Y9_pumRGewq?r^z64~6Q&W%HGu zIW|QX?bGiHE^I~+6PGb>;ix!!v0%}Zsn1k<#!vM2Jo9_>PMn+n>e8%{?z+-f^47dw z?SB}W@${-0p&cFPkO^z^7vGlb@)fOFR}vULB`c}DeuqZ#Xpd^kH|OSV^3^*NXRXbD zbhy)3oVVtc!t!9^glF;=r=4M=D<(F5`AbVPyup%<>+;qY``jt>Us^S3h&`g5iCIf$ zjp%24@3Zcnwz@Wc%GhgKKWK-}Eyk1i+<)5T6NbC}#rbQt`r{_g7}@XWEd$c$&%4d* zunp?$DNBl-p#gvVYd9+FPOtMo<(73T^LIOvvU4U3b=4N0^NU^iD^`{GPDpbnfBIcOR7*>X0Ca8q z>q7v9006VG3nl{$y-5650002!NklI!_olgS{fT0BQFy~-eg7qbS+IyO|8oTwMl@O5da^1_5z{J zGy;ObVC(o-n+mjE_#XfO0RR7gy(WbK000I_L_t&o0I0)NrOYIZ>i_@%07*qoM6N<$ Ef>zhV8vp?LYP93I)D4)@wa;(YU}8BiQXG%`w6xQZbSVo)Ge}>g7aTxXqlFjrm1P- zv~;yJyApMUv_!q`P&bup6WkWqHt@GyU~L}T23~EP+AfY|9OcjbKIb5!f}-NBcRU|g zc^%F<9KPWBJinjM=YVu=Y-|*OkC(>A@W^g#XlOWjs_xXOQ-9&>BO)SNy%0eNfR8;J z=BMiFYCozCkM^d)-=YPC0O%}iu%A46sx}bN`7Iy>K&MhySO4LO6JkaHbaJ+FaN?tn z>gyYHItP&g(8)wbMt2|^KKY4HG`+OY*hmO~PO3|n$Qz=fm=OS- zUH9(Y!W)Hz0DtJbtOB37A<8av6@X5xYuBz3gaGK|Y*NszdpAM=bavfrRv-YKTV!Mx zLI8AjwmKjLKxY>b!Dbp@iI}3qpFSs1&=u@&i zpgn9m?@S1QYs@0hQ7eLT*u%E7FN6TN#u^&0N(I8&IVrd*uNv6rRkW?)z>YJYpsiE> z@2JS(KZtbPAOI~jT+MyDc0PE=Zx;UTaO3l{>MP%FsP;d2`_8_AA7YY zPw8rk>VMwtqW9_G5cuEMLrJGj_}jcgM5G?w*fn-|=W0P!Rn?IrN9@Aj!GnAB=pmqm z>mdXkyfoOop}x5SU;598xZx2t@*C=PxTNu|--UnQf_}QB?rTZiBCeYhtX{o3KR>_a zHIb2#Lx&DcPf!2KSH7Z-@Y-vyJ@UvScA=!CWPiwzAp)*59UOu;f6=rn@Sop^cznxQ z51#)*L+*-tyYTKidh5-`{zI;p68!nke=aI2I_Jv$`}Z$jzC0r%)*@%8v!OoIjuN(|}WzrR8+!>(PscEW@S0De`9Zb_|4|;k3N6qAK68p{Xb2YYu;}?1gB1&vh(x7 zg9rcl&wr{7AAb1Zn{U4P_rL%BGIlrYU2LuXr$7Cvp|#gkUVQPz!jOuJ3cJr|Kl@p` za_iQuc1SJn_Q3}q*b%<-&O3E=b>}~b&wu9&-`es+*jnS93;+1XKf)i&?kwP9PMrAY zCqH@aiG>TpZ!UQJaeK4XXW-?zq7c;lOS#XV`u}e<_}*IT6pue|cP z&wcJ&-}=@A4?J+wO*ai2Hq1tQ_@-scmc_=#+6cV&-g^fO7%+YM^qX(K`RiZ*dVghQ zW%Iii7Z-ow3tw=#Twnd_S8crg;0HhW_rL!gzUgaU`&w33*1moF1`Zr(*C!+-*y8^$ zfBB2rX7^mMU_tou_U+rl7k>TgU)zJ*ZeM(Syd6NVUcDACUflfaC6_E&VjsPC@802C zKl7Q-%$zwhd~0(Q!WaJbx4+$T%YQAyhY$bmcfWh`4?OVTs+B)l{?xK3 zmn?ba=~eUQKmN$vOm&be3PIVg>p%LSy>||r(0j+4F8BECxh1geG#mfhwr#Twc01wQ z82;iHzi2}x5c)lBVY6nJBRnfuR*y|Bi0jJYxJCrte5YTRp?haiY9lRgj zJd%FxZnQvw|Cre$J=keZA*F^pJyFRM8u&(hit83m+gJS6$rDXhy7`j z3ww~ZW(3Urb?F&40J246Y3xx9D?S}!uQ{` z|5}+Jbm^vyHRt}Abok7C{&(8gD7A@zLIcSLPxDS&v(JD2^R^bS3&)QixAplsw?6U2 z6E=p!m)&l6%YU1?ckiy#EB3Z#1h(lAzF^g`m3(-$-!c~e5fXkJo9Em8ZDO)z%NFa} zz7U&4*u=q#(7Z=r6$oE&IIdRO@v&t40#*Ur6j%`2FZ|&6aoa4o;fAP=+9v4W5L|e( z(D^w%w61IPm(RVE&A07OJDr|0DX`gl_$FhU0yb-~k$>Ov4tBy1FZFG|ASNcJ<&3~a ztgYU|@b?Q``I5a25+GG!Fa}C=HedCQc`u6QBpdD4W zWvg-fGyLYKKmF;9jEuazye_Bz-4)vcuuZ{m2yAum$Rm%~?qKu6{%1e?+0vy;@4x?k zwcCC7-4~vf*Z{T=gs+@Ccdoq{VHa#K(;nZh&wtF!w3EEeMC`$BE@KU{-9cOS+s2{Y z&0guSBeC593x_?9-L3g$1!KFL-}uHi>`%Miwh3(#VVjErF2*Ww&NVAnty0J7;1Jw) z&t>jDKJ&~oc9Kj=N}4!vq76KI;lTC|pL*)4!Gi~f?{HIy?X=x<&pq}M-L74`ECkjL z+kY3dTWzOssEw>?T*0X!}Zfk)}aKbG(XW0V$ z&%0i}dDdpv_R8GTPd{ygE&STy&wu`NJAbdXyw937Yiw-WtAGl1!CGL$DSVR++F$=;>R>5pj)-5*R8{hwDX*I4kE5nsx$Cu=`@SbozH@V|yl;3GQvpYaU!rM&~3 zJp$l*!7o7t5Y*7n*bw}c=$HR%(IXUL`vRBrYtcmr0OqpN-3nBf z?%JAPRC)Cev9_}0^}JoBl>y<>wJ!uUMUM^jxNkiBzkaD3%G>;+*Ou>kZ05o_GnehH z4Ad0Qjla2WuD;^*wE)|I-v1Gs;H_8ptQc=0gzjs4T|s?^bHE4R=2 z-stp)mu0P3^-7svS69{R(_-B>#%ApGwKxLrhOzcE7`CYRf*-x6V(t7{b7s!UD)Q;- z@)fU{HRIt~v$M-M_gsccS{n39nvfFXJpESBm_aT_TX&7}q%2t;D0K`O8SPL<4uq<$ z>B)a{id<>0?OKuG)W<;ezS-ZqKQEBH;TP*i^=|Jw`O6B5tK>{vkoU}ZkJAyQ>s8N~ zX^WQj_VyYX?@|W~lxL40v*eKHv7PxDF;Q|QWGu}Z@Sb}_Tu}G8pmEa&g)UO)4i_+z%hTW8`#P>bkRyLOVf>5){SS!hJ0iWWaCxqLkHBA^w;*lQ z5YLSs&(M^q^VYv!9-1pw@0vQ;;~qM9|9P|L>eAdrGg1==+Tvo!i1YV>x%lxyPn`YlREUus3G;G|A1snEyYukBjO-ERwP_I@`$1RZB-RFYVu=Q+e@o$0kwYgt@j6)4!(kEw|Ej4m(&EO z{Y7hMq=ddcv5DyqF3&l8fgSuJcoKgeK46dS-8f-@&DisO;k8fPw57|ZCCBvZKQ6}( z+h3l)a&~&MeJ-A%$!T-e?+G7Gt@o8X``fZ^dCvOTV~2S94H-Qrx3s!y|GMcT1_wXl zh&j3Cms;-AkGVE4chR(z&__&}omWxYeAA7|%S(?HFHW}4eMHvjse5hJ*3f^7V`^yW z)cUic%}4g_nKNwUl0){_P_bfk-+|+|R{G1cf{#BgJN#F6kx##sL+#wIFa&`kIkVCp zeziCdGj7VHv^W)SSv6<=`U^ahYRfmxn7A-|Usdn;+s2J>`%7M(bJz3@!M}BX>Bi}4 z^It8jawUvT?i)C;dd-1zmz94G_o%6NC%NrX_n6Ul&r6GSMfvtGnVi04M{&TDbVu?a zr#JtF%yBbwkJMiH05Y?8dYy5%jZ5_Si*_uYI6130_@Qb_*Uz|TSzehV;f_gDM|-OG zy}J0}g?UE;?K(Dvo@6t}&)HZ|;fNbKBGys1K5OT((>GPUK7aCz7Y={;-6>P=pOoY* z%XuMl_PU~gYe@Q(WKVF8)o0{{`=%wi9o0oy_e{y!S>;T;J7Yp}w0HNq`I%Y!s?Po* z+?STvD|j&`dFuVM#>6zu(R}+h=XhO7cT67Rarp8w(`LSySEd1@Zy)JV>9$qVCN9|1 zyhjo^l)WOi$~`10CQyI2ZDGd5$@B7jeG`-8T*oW6EqrWC+2st3z@gPko-LJoP)v_N z)#1D)%kq!3%w><5G08DDZi{w#Lr;%d-@*Kn<8luhm*{T$S<%)bJNk~CoEG21ZnVG0 zsTrvQoGmA+fy#@0`WHOZ&gfE>27Yh;x|K(o7UnkPOC6WwagTpqu+HUm#3c^u?NCjb z!+XcNii4%Tz<}1*@BXs9O~w9RV_w?6Ze(ykR~OD5GjYd;?2<8y99#1&@O`E}pSvW< z6&yJar_R{<-dXP*4ah-hnKKfcwMTNZZLqm-Tl?~JqoQpnmo1-o$GW0T zTgy`Bx4wfAJ^+8kJ+>(?BhDt(mHE@h%q+;uJ}@mVH997B@k{PX=a5kYY(eYWA4o~x zR#8~tOLuuMer)#4uGs!ckv+4MSHHY|tQ|~s(PMW_$?-NFdD84>H;nWTNKA~5Qtc6U z)&E;oTyoGmJM+$|w-@g$@LEY{XNL9&T+^P}nBa3IB*lN&TDNl0@sTqNN{hWUW86K@ zd=WC;j>Gvy2VC)!=FE$?>ubVUvaiEy&kgS#Je0L73Ve3JFXb*t2z@T|Qq#BW&fYgY zX+n6w^ccB3cikAbDtAr&-mJY|SJp2!rT2Bz6wVzvaYse*Vc(Rv=u2L5^hjE?J$sxx z^z})Zv9o_{Z<+5-_nDh}#-!XCziDwv!Rr;X1|>MEO178!#O#8y>{wygx0UEs;w$53!Z-@^Y2N_Z*jGw>OFszs_fb988#{| zw5w!K_mOd~odvoezqiCr8?M9=gQ7!k_ly}8fAhMsqh%Go+5~4*n+MQ;RANl{*66sD zLC%7`6(vXfsnNY%ZdZ@uoTX_?j#m4sD!j*S>S3Qtt?yWSj?M2oYWH%*r6hz8rgz-f z_&$F*-lnNTPnWxQVAJZEFCOvveBPtMwMFY+Cr7l~RbG-iFZ*z%-Lc}Bofe&Ke??nz z#w7L)Z8z5X4i|ev2OJaLqH_%zImEqXN4bsg39;d8?5{2K(Ru_=rLJD?&>7X?blJ7O z_I!y+l7oeN%WC4>#rgJX$Iau$#Pw|ZSrG_$ z+V#k13$)|YZ}kDM@7ykDLhphg^rn5H-eS7AOpSI{>5J- zxH;{a1!LW39>?MIxEuw##sWw3Y?)I~<>-?%dRS6YYEtpDHhEWvs3}=Kaq_ydldgZb zK%iV-y>hX4u8P^rLJs`pr)d* z=%`$`ZTPS4>lQDZF*??9;eiw7^0Rv*N1=I_qwiMKU~uWVmwWz+HODLHCSskj(d>vfNE4~pv}9o|#av_Di? zw#z1{&Y1YVPG9-nk}8`|zPu}E^|JX>lKVQEkGfsQ=5*a0oQ5_ZOm*4oB~5>O1U23R zg=Ht5iA(ceTfbsn=D0YgTK_@?d}X_Azas9j?fDy4EtxmjCKs2Hq9|AY1W$_tmala0 zLF@B?g#MTHBA%;nf{oXcg*my!O(F7FiW=-^MkQ@DS!Z!vbbqU}ptb|8Drra+RjG<#)m1NcHu3|B&0YRtaP z`7={}g~>5af9cNMWxhUBUmG^OZ`_^Z`sb}UoIP{8Z*;8Fcd)qi?~`#x2WPLx3fC-M zUNwH&xRekZ zZ!7RQV;5&6+mg5{I9oopH~ZPX{vj%SE^A+KX%Kv;p6xoesF>9B#N2sB!GkFb9?XH# z)3tCo-MyWvD9c^F#WB+5%Uh9qW;)<>_YHmxOV%z~>PwrP7o-Sjf**uUH0O*TXH=yZinAn zQFSu7(oP#6eAWIEwifM0R%-f$ zWL>pSzcaY989`J`+M=anV(i6&Wiw_zTj7~JHLm-G-=k;3!kmBCR%VQLR~NmSy>WMi zV@TRF>*oY_bew~xY|2@Fdy>nWzj1S+f7pzS#Mb&99LeK6sw&yBZryfojWc21rkuxy zIlTqh8(+;U2_#H;HhX!>1va{(qElDDyfQ5=P`GV#_Lc&#J9+WT>!%H}XOuG`W97UN zeQfW2-aWH6RKcbB|Y`#ngfX>L~0`_Nk zIv_#-bT+mraN@)Xg(?ufhNS_}xzyFwgzkJYYLNgjBLF`3>;*zK zn*{^{f#&hA78Pi|@ZSIc0RR6*L#0Ik000I_L_t&o0BM_U0Qz*o-v9sr07*qoM6N<$ Ef(}8B0ssI2 diff --git a/modules/banner/element-web/e2e/snapshots/banner.spec.ts/univention-menu-loading-linux.png b/modules/banner/element-web/e2e/snapshots/banner.spec.ts/univention-menu-loading-linux.png index c2b44b9f6bb62ed54fd00d85d2c567bba2c8ba0b..b6ad990619011bcfdf079d4ccd1b81e596824cf4 100644 GIT binary patch delta 4164 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 Date: Thu, 2 Apr 2026 15:43:13 +0100 Subject: [PATCH 19/20] Update screenshot --- .../banner.spec.ts/univention-menu-linux.png | Bin 25423 -> 25424 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/modules/banner/element-web/e2e/snapshots/banner.spec.ts/univention-menu-linux.png b/modules/banner/element-web/e2e/snapshots/banner.spec.ts/univention-menu-linux.png index 0467cd03b27f34d5a873727ff716f87225581bf8..90f47fc89757c92fc68ed1fa144797c9ac80ef6c 100644 GIT binary patch literal 25424 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 literal 25423 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 Date: Mon, 6 Apr 2026 20:43:24 +0000 Subject: [PATCH 20/20] build(deps-dev): bump vite in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Updates `vite` from 7.3.1 to 7.3.2 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.3.2 dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- modules/banner/element-web/package.json | 2 +- modules/opendesk/element-web/package.json | 2 +- modules/restricted-guests/element-web/package.json | 2 +- modules/widget-lifecycle/element-web/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/banner/element-web/package.json b/modules/banner/element-web/package.json index 0fa0eecb03..5ed58cf056 100644 --- a/modules/banner/element-web/package.json +++ b/modules/banner/element-web/package.json @@ -20,7 +20,7 @@ "react": "19.2.4", "rollup-plugin-external-globals": "^0.13.0", "typescript": "^5.7.3", - "vite": "^7.1.11", + "vite": "^7.3.2", "vite-plugin-node-polyfills": "^0.25.0", "vite-plugin-svgr": "^4.3.0" }, diff --git a/modules/opendesk/element-web/package.json b/modules/opendesk/element-web/package.json index c0fa58b261..c03f5db255 100644 --- a/modules/opendesk/element-web/package.json +++ b/modules/opendesk/element-web/package.json @@ -26,7 +26,7 @@ "react-dom": "19.2.4", "rollup-plugin-external-globals": "^0.13.0", "typescript": "^5.7.3", - "vite": "^7.1.11", + "vite": "^7.3.2", "vite-plugin-node-polyfills": "^0.25.0" } } diff --git a/modules/restricted-guests/element-web/package.json b/modules/restricted-guests/element-web/package.json index ab4c4d9469..14f1539528 100644 --- a/modules/restricted-guests/element-web/package.json +++ b/modules/restricted-guests/element-web/package.json @@ -20,7 +20,7 @@ "react": "19.2.4", "rollup-plugin-external-globals": "^0.13.0", "typescript": "^5.7.3", - "vite": "^7.1.11", + "vite": "^7.3.2", "vite-plugin-node-polyfills": "^0.25.0" }, "dependencies": { diff --git a/modules/widget-lifecycle/element-web/package.json b/modules/widget-lifecycle/element-web/package.json index 4611eaecda..d45ba8f861 100644 --- a/modules/widget-lifecycle/element-web/package.json +++ b/modules/widget-lifecycle/element-web/package.json @@ -16,7 +16,7 @@ "@types/node": "^22.10.7", "@vitest/coverage-v8": "^4.0.0", "typescript": "^5.7.3", - "vite": "^7.1.11", + "vite": "^7.3.2", "vitest": "^4.0.0", "vitest-sonar-reporter": "^2.0.0" },