From 80c7d35a18fca778599d7876aef20804d3f2fe6d Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 18 Nov 2024 17:08:52 +0000 Subject: [PATCH] Add an expiry time to inbound webhooks (#984) * Add logic to enable generic hook expiry * Add storage for hook expiry warnings. * Migrate generic hooks / add expiry field * Allow reporting a specific error and status code for generic webhooks * Report the specific error when a message fails to send * Refactor input class to better support datetime * Remove single use of innerChild * Add UI support for expiry configuration * Add new packages * Add warnings when the timer is about to expire. * Add send expiry notice config option * lint * document new option s * Fixup test * Add tests for expiry * Add textual command for setting a duration on a webhook. * Add e2e test for inbound hooks. * changelog * Add a configuration option to force webhooks to expire. * update config.sample.yml * fix field not working --- changelog.d/985.feature | 1 + config.sample.yml | 3 + docs/setup/webhooks.md | 16 + package.json | 7 +- spec/generic-hooks.spec.ts | 119 +++ src/Bridge.ts | 14 +- src/Connections/GenericHook.ts | 182 ++++- src/Connections/SetupConnection.ts | 16 +- src/MatrixSender.ts | 2 +- src/Stores/MemoryStorageProvider.ts | 9 + src/Stores/RedisStorageProvider.ts | 10 + src/Stores/StorageProvider.ts | 3 + src/Widgets/BridgeWidgetApi.ts | 2 +- src/config/Config.ts | 60 +- src/config/Defaults.ts | 2 + src/config/sections/generichooks.ts | 74 ++ src/config/sections/index.ts | 3 +- src/generic/Router.ts | 13 +- src/generic/types.ts | 14 +- tests/connections/GenericHookTest.ts | 136 ++- web/components/RoomConfigView.tsx | 3 +- web/components/elements/Button.tsx | 4 +- .../elements/InputField.module.scss | 11 +- web/components/elements/InputField.tsx | 28 +- web/components/elements/ListItem.tsx | 11 +- .../roomConfig/GenericWebhookConfig.tsx | 107 ++- .../roomConfig/GitlabRepoConfig.tsx | 2 +- web/components/roomConfig/RoomConfig.tsx | 4 +- web/styling.scss | 30 - yarn.lock | 773 +++++++----------- 30 files changed, 1002 insertions(+), 657 deletions(-) create mode 100644 changelog.d/985.feature create mode 100644 spec/generic-hooks.spec.ts create mode 100644 src/config/sections/generichooks.ts diff --git a/changelog.d/985.feature b/changelog.d/985.feature new file mode 100644 index 000000000..db3e6d8a3 --- /dev/null +++ b/changelog.d/985.feature @@ -0,0 +1 @@ +Add support for setting an expiry time on a webhook. See the documentation on [Generic Webhooks](https://matrix-org.github.io/matrix-hookshot/latest/setup/webhooks.html) for more information. diff --git a/config.sample.yml b/config.sample.yml index 6360b22b6..b351530ab 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -102,10 +102,13 @@ listeners: # enabled: false # outbound: false # enableHttpGet: false +# sendExpiryNotice: false +# requireExpiryTime: false # urlPrefix: https://example.com/webhook/ # userIdPrefix: _webhooks_ # allowJsTransformationFunctions: false # waitForComplete: false +# maxExpiryTime: 30d #feeds: # # (Optional) Configure this to enable RSS/Atom feed support diff --git a/docs/setup/webhooks.md b/docs/setup/webhooks.md index d3cc096df..afcbcfb5f 100644 --- a/docs/setup/webhooks.md +++ b/docs/setup/webhooks.md @@ -15,6 +15,8 @@ generic: allowJsTransformationFunctions: false waitForComplete: false enableHttpGet: false + # maxExpiryTime: 30d + # sendExpiryNotice: false # userIdPrefix: webhook_ ``` @@ -43,6 +45,13 @@ has been sent (`true`). By default this is `false`. `enableHttpGet` means that webhooks can be triggered by `GET` requests, in addition to `POST` and `PUT`. This was previously on by default, but is now disabled due to concerns mentioned below. +`maxExpiryTime` sets an upper limit on how long a webhook can be valid for before the bridge expires it. By default this is unlimited. This +takes a duration represented by a string. E.g. "30d" is 30 days. See [this page](https://github.com/jkroso/parse-duration?tab=readme-ov-file#available-unit-types-are) +for available units. Additionally: + + - `sendExpiryNotice` configures whether a message is sent into a room when the connection is close to expiring. + - `requireExpiryTime` forbids creating a webhook without a expiry time. This does not apply to existing webhooks. + You may set a `userIdPrefix` to create a specific user for each new webhook connection in a room. For example, a connection with a name like `example` for a prefix of `webhook_` will create a user called `@webhook_example:example.com`. If you enable this option, you need to configure the user to be part of your registration file e.g.: @@ -117,6 +126,13 @@ can specify this either globally in your config, or on the widget with `waitForC If you make use of the `webhookResponse` feature, you will need to enable `waitForComplete` as otherwise hookshot will immeditately respond with it's default response values. + +#### Expiring webhooks + +Webhooks can be configured to expire, such that beyond a certain date they will fail any incoming requests. Currently this expiry time +is mutable, so anybody able to configure connections will be able to change the expiry date. Hookshot will send a notice to the room +at large when the webhook has less than 3 days until it's due to expire (if `sendExpiryNotice` is set). + ### JavaScript Transformations
diff --git a/package.json b/package.json index 1e23d16f8..f5864bba1 100644 --- a/package.json +++ b/package.json @@ -48,11 +48,13 @@ "@octokit/rest": "^20.0.2", "@octokit/webhooks": "^12.0.10", "@sentry/node": "^7.52.1", - "@vector-im/compound-design-tokens": "^1.3.0", - "@vector-im/compound-web": "^4.8.0", + "@vector-im/compound-design-tokens": "^2.0.1", + "@vector-im/compound-web": "^7.3.0", "ajv": "^8.11.0", "axios": "^1.7.4", + "clsx": "^2.1.1", "cors": "^2.8.5", + "date-fns": "^4.1.0", "express": "^4.20.0", "figma-js": "^1.14.0", "helmet": "^7.1.0", @@ -67,6 +69,7 @@ "mime": "^4.0.1", "node-emoji": "^2.1.3", "p-queue": "^6.6.2", + "parse-duration": "^1.1.0", "preact-render-to-string": "^6.3.1", "prom-client": "^15.1.0", "quickjs-emscripten": "^0.26.0", diff --git a/spec/generic-hooks.spec.ts b/spec/generic-hooks.spec.ts new file mode 100644 index 000000000..8b1f266b4 --- /dev/null +++ b/spec/generic-hooks.spec.ts @@ -0,0 +1,119 @@ +import { E2ESetupTestTimeout, E2ETestEnv, E2ETestMatrixClient } from "./util/e2e-test"; +import { describe, it } from "@jest/globals"; +import { GenericHookConnection } from "../src/Connections"; +import { TextualMessageEventContent } from "matrix-bot-sdk"; +import { add } from "date-fns/add"; + +async function createInboundConnection(user: E2ETestMatrixClient, botMxid: string, roomId: string, duration?: string) { + const join = user.waitForRoomJoin({ sender: botMxid, roomId }); + const connectionEvent = user.waitForRoomEvent({ + eventType: GenericHookConnection.CanonicalEventType, + stateKey: 'test', + sender: botMxid + }); + await user.inviteUser(botMxid, roomId); + await user.setUserPowerLevel(botMxid, roomId, 50); + await join; + + // Note: Here we create the DM proactively so this works across multiple + // tests. + // Get the DM room so we can get the token. + const dmRoomId = await user.dms.getOrCreateDm(botMxid); + + await user.sendText(roomId, '!hookshot webhook test' + (duration ? ` ${duration}` : "")); + // Test the contents of this. + await connectionEvent; + + const msgPromise = user.waitForRoomEvent({ sender: botMxid, eventType: "m.room.message", roomId: dmRoomId }); + const { data: msgData } = await msgPromise; + const msgContent = msgData.content as unknown as TextualMessageEventContent; + const [_unused1, _unused2, url] = msgContent.body.split('\n'); + return url; +} + +describe('Inbound (Generic) Webhooks', () => { + let testEnv: E2ETestEnv; + + beforeAll(async () => { + const webhooksPort = 9500 + E2ETestEnv.workerId; + testEnv = await E2ETestEnv.createTestEnv({ + matrixLocalparts: ['user'], + config: { + generic: { + enabled: true, + // Prefer to wait for complete as it reduces the concurrency of the test. + waitForComplete: true, + urlPrefix: `http://localhost:${webhooksPort}` + }, + listeners: [{ + port: webhooksPort, + bindAddress: '0.0.0.0', + // Bind to the SAME listener to ensure we don't have conflicts. + resources: ['webhooks'], + }], + } + }); + await testEnv.setUp(); + }, E2ESetupTestTimeout); + + afterAll(() => { + return testEnv?.tearDown(); + }); + + it('should be able to create a new webhook and handle an incoming request.', async () => { + const user = testEnv.getUser('user'); + const roomId = await user.createRoom({ name: 'My Test Webhooks room'}); + const okMsg = user.waitForRoomEvent({ eventType: "m.room.message", sender: testEnv.botMxid, roomId }); + const url = await createInboundConnection(user, testEnv.botMxid, roomId); + expect((await okMsg).data.content.body).toEqual('Room configured to bridge webhooks. See admin room for secret url.'); + + const expectedMsg = user.waitForRoomEvent({ eventType: "m.room.message", sender: testEnv.botMxid, roomId }); + const req = await fetch(url, { + method: "PUT", + body: "Hello world" + }); + expect(req.status).toEqual(200); + expect(await req.json()).toEqual({ ok: true }); + expect((await expectedMsg).data.content).toEqual({ + msgtype: 'm.notice', + body: 'Received webhook data: Hello world', + formatted_body: '

Received webhook data: Hello world

', + format: 'org.matrix.custom.html', + 'uk.half-shot.hookshot.webhook_data': 'Hello world' + }); + }); + + it('should be able to create a new expiring webhook and handle valid requests.', async () => { + jest.useFakeTimers(); + const user = testEnv.getUser('user'); + const roomId = await user.createRoom({ name: 'My Test Webhooks room'}); + const okMsg = user.waitForRoomEvent({ eventType: "m.room.message", sender: testEnv.botMxid, roomId }); + const url = await createInboundConnection(user, testEnv.botMxid, roomId, '2h'); + expect((await okMsg).data.content.body).toEqual('Room configured to bridge webhooks. See admin room for secret url.'); + + const expectedMsg = user.waitForRoomEvent({ eventType: "m.room.message", sender: testEnv.botMxid, roomId }); + const req = await fetch(url, { + method: "PUT", + body: "Hello world" + }); + expect(req.status).toEqual(200); + expect(await req.json()).toEqual({ ok: true }); + expect((await expectedMsg).data.content).toEqual({ + msgtype: 'm.notice', + body: 'Received webhook data: Hello world', + formatted_body: '

Received webhook data: Hello world

', + format: 'org.matrix.custom.html', + 'uk.half-shot.hookshot.webhook_data': 'Hello world' + }); + jest.setSystemTime(add(new Date(), { hours: 3 })); + const expiredReq = await fetch(url, { + method: "PUT", + body: "Hello world" + }); + expect(expiredReq.status).toEqual(404); + expect(await expiredReq.json()).toEqual({ + ok: false, + error: "This hook has expired", + }); + }); +}); diff --git a/src/Bridge.ts b/src/Bridge.ts index f968c8047..099e05fe0 100644 --- a/src/Bridge.ts +++ b/src/Bridge.ts @@ -10,7 +10,7 @@ import { GetIssueResponse, GetIssueOpts } from "./Gitlab/Types" import { GithubInstance } from "./github/GithubInstance"; import { IBridgeStorageProvider } from "./Stores/StorageProvider"; import { IConnection, GitHubDiscussionSpace, GitHubDiscussionConnection, GitHubUserSpace, JiraProjectConnection, GitLabRepoConnection, - GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitLabIssueConnection, FigmaFileConnection, FeedConnection, GenericHookConnection, WebhookResponse } from "./Connections"; + GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitLabIssueConnection, FigmaFileConnection, FeedConnection, GenericHookConnection } from "./Connections"; import { IGitLabWebhookIssueStateEvent, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "./Gitlab/WebhookTypes"; import { JiraIssueEvent, JiraIssueUpdatedEvent, JiraVersionEvent } from "./jira/WebhookTypes"; import { JiraOAuthResult } from "./jira/Types"; @@ -606,7 +606,7 @@ export class Bridge { if (!connections.length) { await this.queue.push({ - data: {notFound: true}, + data: {successful: true, notFound: true}, sender: "Bridge", messageId: messageId, eventName: "response.generic-webhook.event", @@ -621,21 +621,19 @@ export class Bridge { await c.onGenericHook(data.hookData); return; } - let successful: boolean|null = null; - let response: WebhookResponse|undefined; if (this.config.generic?.waitForComplete || c.waitForComplete) { const result = await c.onGenericHook(data.hookData); - successful = result.successful; - response = result.response; await this.queue.push({ - data: {successful, response}, + data: result, sender: "Bridge", messageId, eventName: "response.generic-webhook.event", }); } else { await this.queue.push({ - data: {}, + data: { + successful: null, + }, sender: "Bridge", messageId, eventName: "response.generic-webhook.event", diff --git a/src/Connections/GenericHook.ts b/src/Connections/GenericHook.ts index 80cee3adc..2f0e831f7 100644 --- a/src/Connections/GenericHook.ts +++ b/src/Connections/GenericHook.ts @@ -8,9 +8,13 @@ import { Appservice, Intent, StateEvent } from "matrix-bot-sdk"; import { ApiError, ErrCode } from "../api"; import { BaseConnection } from "./BaseConnection"; import { GetConnectionsResponseItem } from "../provisioning/api"; -import { BridgeConfigGenericWebhooks } from "../config/Config"; +import { BridgeConfigGenericWebhooks } from "../config/sections"; import { ensureUserIsInRoom } from "../IntentUtils"; import { randomUUID } from 'node:crypto'; +import { GenericWebhookEventResult } from "../generic/types"; +import { StatusCodes } from "http-status-codes"; +import { IBridgeStorageProvider } from "../Stores/StorageProvider"; +import { formatDuration, isMatch, millisecondsToHours } from "date-fns"; export interface GenericHookConnectionState extends IConnectionState { /** @@ -21,11 +25,17 @@ export interface GenericHookConnectionState extends IConnectionState { * The name given in the provisioning UI and displaynames. */ name: string; - transformationFunction: string|undefined; + transformationFunction?: string; /** * Should the webhook only respond on completion. */ - waitForComplete: boolean|undefined; + waitForComplete?: boolean|undefined; + + /** + * If the webhook has an expriation date, then the date at which the webhook is no longer value + * (in UTC) time. + */ + expirationDate?: string; } export interface GenericHookSecrets { @@ -37,6 +47,10 @@ export interface GenericHookSecrets { * The hookId of the webhook. */ hookId: string; + /** + * How long remains until the webhook expires. + */ + timeRemainingMs?: number } export type GenericHookResponseItem = GetConnectionsResponseItem; @@ -64,6 +78,14 @@ interface WebhookTransformationResult { webhookResponse?: WebhookResponse; } +export interface GenericHookServiceConfig { + userIdPrefix?: string; + allowJsTransformationFunctions?: boolean, + waitForComplete?: boolean, + maxExpiryTime?: number, + requireExpiryTime: boolean, +} + const log = new Logger("GenericHookConnection"); const md = new markdownit(); @@ -71,6 +93,12 @@ const TRANSFORMATION_TIMEOUT_MS = 500; const SANITIZE_MAX_DEPTH = 10; const SANITIZE_MAX_BREADTH = 50; +const WARN_AT_EXPIRY_MS = 3 * 24 * 60 * 60 * 1000; +const MIN_EXPIRY_MS = 60 * 60 * 1000; +const CHECK_EXPIRY_MS = 15 * 60 * 1000; + +const EXPIRY_NOTICE_MESSAGE = "The webhook **%NAME** will be expiring in %TIME." + /** * Handles rooms connected to a generic webhook. */ @@ -123,8 +151,8 @@ export class GenericHookConnection extends BaseConnection implements IConnection return obj; } - static validateState(state: Record): GenericHookConnectionState { - const {name, transformationFunction, waitForComplete} = state; + static validateState(state: Partial>): GenericHookConnectionState { + const {name, transformationFunction, waitForComplete, expirationDate: expirationDateStr} = state; if (!name) { throw new ApiError('Missing name', ErrCode.BadValue); } @@ -143,14 +171,26 @@ export class GenericHookConnection extends BaseConnection implements IConnection throw new ApiError('Transformation functions must be a string', ErrCode.BadValue); } } + let expirationDate: string|undefined; + if (expirationDateStr != undefined) { + if (typeof expirationDateStr !== "string" || !expirationDateStr) { + throw new ApiError("'expirationDate' must be a non-empty string", ErrCode.BadValue); + } + if (!isMatch(expirationDateStr, "yyyy-MM-dd'T'HH:mm:ss.SSSXX")) { + throw new ApiError("'expirationDate' must be a valid date", ErrCode.BadValue); + } + expirationDate = expirationDateStr; + } + return { name, transformationFunction: transformationFunction || undefined, waitForComplete, + expirationDate, }; } - static async createConnectionForState(roomId: string, event: StateEvent>, {as, intent, config, messageClient}: InstantiateConnectionOpts) { + static async createConnectionForState(roomId: string, event: StateEvent>, {as, intent, config, messageClient, storage}: InstantiateConnectionOpts) { if (!config.generic) { throw Error('Generic webhooks are not configured'); } @@ -162,6 +202,10 @@ export class GenericHookConnection extends BaseConnection implements IConnection if (!hookId) { hookId = randomUUID(); log.warn(`hookId for ${roomId} not set in accountData, setting to ${hookId}`); + // If this is a new hook... + if (config.generic.requireExpiryTime && !state.expirationDate) { + throw new Error('Expiration date must be set'); + } await GenericHookConnection.ensureRoomAccountData(roomId, intent, hookId, event.stateKey); } @@ -174,18 +218,41 @@ export class GenericHookConnection extends BaseConnection implements IConnection config.generic, as, intent, + storage, ); } - static async provisionConnection(roomId: string, userId: string, data: Record = {}, {as, intent, config, messageClient}: ProvisionConnectionOpts) { + static async provisionConnection(roomId: string, userId: string, data: Partial> = {}, {as, intent, config, messageClient, storage}: ProvisionConnectionOpts) { if (!config.generic) { throw Error('Generic Webhooks are not configured'); } const hookId = randomUUID(); const validState = GenericHookConnection.validateState(data); + if (validState.expirationDate) { + const durationRemaining = new Date(validState.expirationDate).getTime() - Date.now(); + if (config.generic.maxExpiryTimeMs) { + if (durationRemaining > config.generic.maxExpiryTimeMs) { + throw new ApiError('Expiration date cannot exceed the configured max expiry time', ErrCode.BadValue); + } + } + if (durationRemaining < MIN_EXPIRY_MS) { + // If the webhook is actually created with a shorter expiry time than + // our warning period, then just mark it as warned. + throw new ApiError('Expiration date must at least be a hour in the future', ErrCode.BadValue); + } + if (durationRemaining < WARN_AT_EXPIRY_MS) { + // If the webhook is actually created with a shorter expiry time than + // our warning period, then just mark it as warned. + await storage.setHasGenericHookWarnedExpiry(hookId, true); + } + } else if (config.generic.requireExpiryTime) { + throw new ApiError('Expiration date must be set', ErrCode.BadValue); + } + + await GenericHookConnection.ensureRoomAccountData(roomId, intent, hookId, validState.name); await intent.underlyingClient.sendStateEvent(roomId, this.CanonicalEventType, validState.name, validState); - const connection = new GenericHookConnection(roomId, validState, hookId, validState.name, messageClient, config.generic, as, intent); + const connection = new GenericHookConnection(roomId, validState, hookId, validState.name, messageClient, config.generic, as, intent, storage); return { connection, stateEventContent: validState, @@ -218,6 +285,8 @@ export class GenericHookConnection extends BaseConnection implements IConnection private transformationFunction?: string; private cachedDisplayname?: string; + private warnOnExpiryInterval?: NodeJS.Timeout; + /** * @param state Should be a pre-validated state object returned by {@link validateState} */ @@ -230,11 +299,19 @@ export class GenericHookConnection extends BaseConnection implements IConnection private readonly config: BridgeConfigGenericWebhooks, private readonly as: Appservice, private readonly intent: Intent, + private readonly storage: IBridgeStorageProvider, ) { super(roomId, stateKey, GenericHookConnection.CanonicalEventType); if (state.transformationFunction && GenericHookConnection.quickModule) { this.transformationFunction = state.transformationFunction; } + this.handleExpiryTimeUpdate(false).catch(ex => { + log.warn("Failed to configure expiry time warning for hook", ex); + }); + } + + public get expiresAt(): Date|undefined { + return this.state.expirationDate ? new Date(this.state.expirationDate) : undefined; } /** @@ -313,7 +390,49 @@ export class GenericHookConnection extends BaseConnection implements IConnection } else { this.transformationFunction = undefined; } + + const prevDate = this.state.expirationDate; this.state = validatedConfig; + if (prevDate !== validatedConfig.expirationDate) { + await this.handleExpiryTimeUpdate(true); + } + } + + /** + * Called when the expiry time has been updated for the connection. If the connection + * no longer has an expiry time. This voids the interval. + * @returns + */ + private async handleExpiryTimeUpdate(shouldWrite: boolean) { + if (!this.config.sendExpiryNotice) { + return; + } + if (this.warnOnExpiryInterval) { + clearInterval(this.warnOnExpiryInterval); + this.warnOnExpiryInterval = undefined; + } + if (!this.state.expirationDate) { + return; + } + + const durationRemaining = new Date(this.state.expirationDate).getTime() - Date.now(); + if (durationRemaining < WARN_AT_EXPIRY_MS) { + // If the webhook is actually created with a shorter expiry time than + // our warning period, then just mark it as warned. + if (shouldWrite) { + await this.storage.setHasGenericHookWarnedExpiry(this.hookId, true); + } + } else { + const fuzzCheckTimeMs = Math.round((Math.random() * CHECK_EXPIRY_MS)); + this.warnOnExpiryInterval = setInterval(() => { + this.checkAndWarnExpiry().catch(ex => { + log.warn("Failed to check expiry time for hook", ex); + }) + }, CHECK_EXPIRY_MS + fuzzCheckTimeMs); + if (shouldWrite) { + await this.storage.setHasGenericHookWarnedExpiry(this.hookId, false); + } + } } public transformHookData(data: unknown): {plain: string, html?: string} { @@ -424,8 +543,18 @@ export class GenericHookConnection extends BaseConnection implements IConnection * @param data Structured data. This may either be a string, or an object. * @returns `true` if the webhook completed, or `false` if it failed to complete */ - public async onGenericHook(data: unknown): Promise<{successful: boolean, response?: WebhookResponse}> { + public async onGenericHook(data: unknown): Promise { log.info(`onGenericHook ${this.roomId} ${this.hookId}`); + + if (this.expiresAt && new Date() >= this.expiresAt) { + log.warn("Ignoring incoming webhook. This hook has expired"); + return { + successful: false, + statusCode: StatusCodes.NOT_FOUND, + error: 'This hook has expired', + }; + } + let content: {plain: string, html?: string, msgtype?: string}|undefined; let webhookResponse: WebhookResponse|undefined; let successful = true; @@ -487,16 +616,19 @@ export class GenericHookConnection extends BaseConnection implements IConnection transformationFunction: this.state.transformationFunction, waitForComplete: this.waitForComplete, name: this.state.name, + expirationDate: this.state.expirationDate, }, ...(showSecrets ? { secrets: { url: new URL(this.hookId, this.config.parsedUrlPrefix), hookId: this.hookId, + timeRemainingMs: this.expiresAt ? this.expiresAt.getTime() - Date.now() : undefined, } satisfies GenericHookSecrets} : undefined) } } public async onRemove() { log.info(`Removing ${this.toString()} for ${this.roomId}`); + clearInterval(this.warnOnExpiryInterval); // Do a sanity check that the event exists. try { await this.intent.underlyingClient.getRoomStateEvent(this.roomId, GenericHookConnection.CanonicalEventType, this.stateKey); @@ -508,8 +640,9 @@ export class GenericHookConnection extends BaseConnection implements IConnection await GenericHookConnection.ensureRoomAccountData(this.roomId, this.intent, this.hookId, this.stateKey, true); } - public async provisionerUpdateConfig(userId: string, config: Record) { + public async provisionerUpdateConfig(_userId: string, config: Record) { // Apply previous state to the current config, as provisioners might not return "unknown" keys. + config.expirationDate = config.expirationDate ?? undefined; config = { ...this.state, ...config }; const validatedConfig = GenericHookConnection.validateState(config); await this.intent.underlyingClient.sendStateEvent(this.roomId, GenericHookConnection.CanonicalEventType, this.stateKey, @@ -521,6 +654,35 @@ export class GenericHookConnection extends BaseConnection implements IConnection this.state = validatedConfig; } + private async checkAndWarnExpiry() { + const remainingMs = this.expiresAt ? this.expiresAt.getTime() - Date.now() : undefined; + if (!remainingMs) { + return; + } + if (remainingMs < CHECK_EXPIRY_MS) { + // Nearly expired + return; + } + if (remainingMs > WARN_AT_EXPIRY_MS) { + return; + } + if (await this.storage.getHasGenericHookWarnedExpiry(this.hookId)) { + return; + } + // Warn + const markdownStr = EXPIRY_NOTICE_MESSAGE.replace('%NAME', this.state.name).replace('%TIME', formatDuration({ + hours: millisecondsToHours(remainingMs) + })); + await this.messageClient.sendMatrixMessage(this.roomId, { + msgtype: "m.notice", + body: markdownStr, + // render can output redundant trailing newlines, so trim it. + formatted_body: md.render(markdownStr).trim(), + format: "org.matrix.custom.html", + }, 'm.room.message', this.getUserId()); + await this.storage.setHasGenericHookWarnedExpiry(this.hookId, true); + } + public toString() { return `GenericHookConnection ${this.hookId}`; } diff --git a/src/Connections/SetupConnection.ts b/src/Connections/SetupConnection.ts index c642b3e4f..176a11b79 100644 --- a/src/Connections/SetupConnection.ts +++ b/src/Connections/SetupConnection.ts @@ -14,6 +14,7 @@ import { IConnection, IConnectionState, ProvisionConnectionOpts } from "./IConne import { ApiError, Logger } from "matrix-appservice-bridge"; import { Intent } from "matrix-bot-sdk"; import YAML from 'yaml'; +import parseDuration from 'parse-duration'; import { HoundConnection } from "./HoundConnection"; const md = new markdown(); const log = new Logger("SetupConnection"); @@ -209,18 +210,27 @@ export class SetupConnection extends CommandConnection { return this.client.sendHtmlNotice(this.roomId, md.renderInline(`Room no longer bridged to Jira project \`${safeUrl}\`.`)); } - @botCommand("webhook", { help: "Create an inbound webhook.", requiredArgs: ["name"], includeUserId: true, category: GenericHookConnection.ServiceCategory}) - public async onWebhook(userId: string, name: string) { + @botCommand("webhook", { help: "Create an inbound webhook. The liveDuration must be specified as a duration string (e.g. 30d).", requiredArgs: ["name"], includeUserId: true, optionalArgs: ['liveDuration'], category: GenericHookConnection.ServiceCategory}) + public async onWebhook(userId: string, name: string, liveDuration?: string) { if (!this.config.generic?.enabled) { throw new CommandError("not-configured", "The bridge is not configured to support webhooks."); } + let expirationDate: string|undefined = undefined; + if (liveDuration) { + const expirationDuration = parseDuration(liveDuration); + if (!expirationDuration) { + throw new CommandError("Bad webhook duration", "A webhook name must be between 3-64 characters."); + } + expirationDate = new Date(expirationDuration + Date.now()).toISOString(); + } + await this.checkUserPermissions(userId, "webhooks", GitHubRepoConnection.CanonicalEventType); if (!name || name.length < 3 || name.length > 64) { throw new CommandError("Bad webhook name", "A webhook name must be between 3-64 characters."); } - const c = await GenericHookConnection.provisionConnection(this.roomId, userId, {name}, this.provisionOpts); + const c = await GenericHookConnection.provisionConnection(this.roomId, userId, {name, expirationDate}, this.provisionOpts); this.pushConnections(c.connection); const url = new URL(c.connection.hookId, this.config.generic.parsedUrlPrefix); const adminRoom = await this.getOrCreateAdminRoom(this.intent, userId); diff --git a/src/MatrixSender.ts b/src/MatrixSender.ts index 924d1f9e8..36b59e127 100644 --- a/src/MatrixSender.ts +++ b/src/MatrixSender.ts @@ -34,7 +34,7 @@ export class MatrixSender { try { await this.sendMatrixMessage(msg.messageId || randomUUID(), msg.data); } catch (ex) { - log.error(`Failed to send message (${msg.data.roomId}, ${msg.data.sender}, ${msg.data.type})`); + log.error(`Failed to send message (${msg.data.roomId}, ${msg.data.sender}, ${msg.data.type})`, ex); } }); } diff --git a/src/Stores/MemoryStorageProvider.ts b/src/Stores/MemoryStorageProvider.ts index e2d65be11..89f3bc58c 100644 --- a/src/Stores/MemoryStorageProvider.ts +++ b/src/Stores/MemoryStorageProvider.ts @@ -16,6 +16,7 @@ export class MemoryStorageProvider extends MSP implements IBridgeStorageProvider private feedGuids = new Map>(); private houndActivityIds = new Map>(); private houndActivityIdToEvent = new Map(); + private hasGenericHookWarnedExpiry = new Set(); constructor() { super(); @@ -139,4 +140,12 @@ export class MemoryStorageProvider extends MSP implements IBridgeStorageProvider public async getHoundActivity(challengeId: string, activityId: string): Promise { return this.houndActivityIdToEvent.get(`${challengeId}.${activityId}`) ?? null; } + + public async getHasGenericHookWarnedExpiry(hookId: string): Promise { + return this.hasGenericHookWarnedExpiry.has(hookId); + } + + public async setHasGenericHookWarnedExpiry(hookId: string, hasWarned: boolean): Promise { + this.hasGenericHookWarnedExpiry[hasWarned ? "add" : "delete"](hookId); + } } diff --git a/src/Stores/RedisStorageProvider.ts b/src/Stores/RedisStorageProvider.ts index bd045cd54..b4ed8ac22 100644 --- a/src/Stores/RedisStorageProvider.ts +++ b/src/Stores/RedisStorageProvider.ts @@ -33,6 +33,8 @@ const FEED_GUIDS = "feeds.guids."; const HOUND_GUIDS = "hound.guids."; const HOUND_EVENTS = "hound.events."; +const GENERIC_HOOK_HAS_WARNED = "generichook.haswarned"; + const log = new Logger("RedisASProvider"); export class RedisStorageContextualProvider implements IStorageProvider { @@ -284,4 +286,12 @@ export class RedisStorageProvider extends RedisStorageContextualProvider impleme public async getHoundActivity(challengeId: string, activityId: string): Promise { return this.redis.get(`${HOUND_EVENTS}${challengeId}.${activityId}`); } + + public async getHasGenericHookWarnedExpiry(hookId: string): Promise { + return await this.redis.sismember(GENERIC_HOOK_HAS_WARNED, hookId) === 1; + } + + public async setHasGenericHookWarnedExpiry(hookId: string, hasWarned: boolean): Promise { + await this.redis[hasWarned ? "sadd" : "srem"](GENERIC_HOOK_HAS_WARNED, hookId); + } } diff --git a/src/Stores/StorageProvider.ts b/src/Stores/StorageProvider.ts index 74a0f345a..0ec10dddd 100644 --- a/src/Stores/StorageProvider.ts +++ b/src/Stores/StorageProvider.ts @@ -36,4 +36,7 @@ export interface IBridgeStorageProvider extends IAppserviceStorageProvider, ISto hasSeenHoundActivity(challengeId: string, ...activityHashes: string[]): Promise; storeHoundActivityEvent(challengeId: string, activityId: string, eventId: string): Promise; getHoundActivity(challengeId: string, activityId: string): Promise; + + getHasGenericHookWarnedExpiry(hookId: string): Promise; + setHasGenericHookWarnedExpiry(hookId: string, hasWarned: boolean): Promise; } \ No newline at end of file diff --git a/src/Widgets/BridgeWidgetApi.ts b/src/Widgets/BridgeWidgetApi.ts index 16ff84fd9..f17d62903 100644 --- a/src/Widgets/BridgeWidgetApi.ts +++ b/src/Widgets/BridgeWidgetApi.ts @@ -108,7 +108,7 @@ export class BridgeWidgetApi extends ProvisioningApi { }); } - private async getServiceConfig(req: ProvisioningRequest, res: Response>) { + private async getServiceConfig(req: ProvisioningRequest, res: Response) { // GitHub is a special case because it depends on live config. if (req.params.service === 'github') { res.send(this.config.github?.publicConfig(this.github)); diff --git a/src/config/Config.ts b/src/config/Config.ts index 3884a0c71..2a3aa502d 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -9,12 +9,14 @@ import { BridgeConfigActorPermission, BridgePermissions } from "../libRs"; import { ConfigError } from "../errors"; import { ApiError, ErrCode } from "../api"; import { GithubInstance, GITHUB_CLOUD_URL } from "../github/GithubInstance"; -import { DefaultDisallowedIpRanges, Logger } from "matrix-appservice-bridge"; +import { Logger } from "matrix-appservice-bridge"; import { BridgeConfigCache } from "./sections/cache"; -import { BridgeConfigQueue } from "./sections"; +import { BridgeConfigGenericWebhooks, BridgeConfigQueue, BridgeGenericWebhooksConfigYAML } from "./sections"; +import { GenericHookServiceConfig } from "../Connections"; const log = new Logger("Config"); + function makePrefixedUrl(urlString: string): URL { return new URL(urlString.endsWith("/") ? urlString : urlString + "/"); } @@ -288,56 +290,6 @@ export interface BridgeConfigFigma { }}; } -export interface BridgeGenericWebhooksConfigYAML { - enabled: boolean; - urlPrefix: string; - userIdPrefix?: string; - allowJsTransformationFunctions?: boolean; - waitForComplete?: boolean; - enableHttpGet?: boolean; - outbound?: boolean; - disallowedIpRanges?: string[]; -} - -export class BridgeConfigGenericWebhooks { - public readonly enabled: boolean; - public readonly outbound: boolean; - - @hideKey() - public readonly parsedUrlPrefix: URL; - public readonly urlPrefix: () => string; - - public readonly userIdPrefix?: string; - public readonly allowJsTransformationFunctions?: boolean; - public readonly waitForComplete?: boolean; - public readonly enableHttpGet: boolean; - constructor(yaml: BridgeGenericWebhooksConfigYAML) { - this.enabled = yaml.enabled || false; - this.outbound = yaml.outbound || false; - this.enableHttpGet = yaml.enableHttpGet || false; - try { - this.parsedUrlPrefix = makePrefixedUrl(yaml.urlPrefix); - this.urlPrefix = () => { return this.parsedUrlPrefix.href; } - } catch (err) { - throw new ConfigError("generic.urlPrefix", "is not defined or not a valid URL"); - } - this.userIdPrefix = yaml.userIdPrefix; - this.allowJsTransformationFunctions = yaml.allowJsTransformationFunctions; - this.waitForComplete = yaml.waitForComplete; - } - - @hideKey() - public get publicConfig() { - return { - userIdPrefix: this.userIdPrefix, - allowJsTransformationFunctions: this.allowJsTransformationFunctions, - waitForComplete: this.waitForComplete, - } - } - -} - - interface BridgeWidgetConfigYAML { publicUrl: string; /** @@ -779,8 +731,8 @@ remove "useLegacySledStore" from your configuration file, and restart Hookshot. return services; } - public getPublicConfigForService(serviceName: string): Record { - let config: undefined|Record; + public getPublicConfigForService(serviceName: string): Record|GenericHookServiceConfig { + let config: undefined|Record|GenericHookServiceConfig; switch (serviceName) { case "feeds": config = this.feeds?.publicConfig; diff --git a/src/config/Defaults.ts b/src/config/Defaults.ts index 2ea9e1f5b..876870ea2 100644 --- a/src/config/Defaults.ts +++ b/src/config/Defaults.ts @@ -109,6 +109,8 @@ export const DefaultConfigRoot: BridgeConfigRoot = { urlPrefix: `${hookshotWebhooksUrl}/webhook/`, userIdPrefix: "_webhooks_", waitForComplete: false, + maxExpiryTime: "30d", + sendExpiryNotice: false, }, figma: { publicUrl: `${hookshotWebhooksUrl}/hookshot/`, diff --git a/src/config/sections/generichooks.ts b/src/config/sections/generichooks.ts new file mode 100644 index 000000000..f78d62ce3 --- /dev/null +++ b/src/config/sections/generichooks.ts @@ -0,0 +1,74 @@ +import { GenericHookServiceConfig } from "../../Connections"; +import { ConfigError } from "../../errors"; +import { hideKey } from "../Decorators"; +import parseDuration from "parse-duration"; + +function makePrefixedUrl(urlString: string): URL { + return new URL(urlString.endsWith("/") ? urlString : urlString + "/"); +} + +export interface BridgeGenericWebhooksConfigYAML { + enabled: boolean; + urlPrefix: string; + userIdPrefix?: string; + allowJsTransformationFunctions?: boolean; + waitForComplete?: boolean; + enableHttpGet?: boolean; + outbound?: boolean; + disallowedIpRanges?: string[]; + maxExpiryTime?: string; + sendExpiryNotice?: boolean; + requireExpiryTime?: boolean; +} + +export class BridgeConfigGenericWebhooks { + public readonly enabled: boolean; + public readonly outbound: boolean; + + @hideKey() + public readonly parsedUrlPrefix: URL; + public readonly urlPrefix: () => string; + + public readonly userIdPrefix?: string; + public readonly allowJsTransformationFunctions?: boolean; + public readonly waitForComplete?: boolean; + public readonly enableHttpGet: boolean; + + @hideKey() + public readonly maxExpiryTimeMs?: number; + public readonly sendExpiryNotice: boolean; + public readonly requireExpiryTime: boolean; + // Public facing value for config generator + public readonly maxExpiryTime?: string; + + constructor(yaml: BridgeGenericWebhooksConfigYAML) { + this.enabled = yaml.enabled || false; + this.outbound = yaml.outbound || false; + this.enableHttpGet = yaml.enableHttpGet || false; + this.sendExpiryNotice = yaml.sendExpiryNotice || false; + this.requireExpiryTime = yaml.requireExpiryTime || false; + try { + this.parsedUrlPrefix = makePrefixedUrl(yaml.urlPrefix); + this.urlPrefix = () => { return this.parsedUrlPrefix.href; } + } catch (err) { + throw new ConfigError("generic.urlPrefix", "is not defined or not a valid URL"); + } + this.userIdPrefix = yaml.userIdPrefix; + this.allowJsTransformationFunctions = yaml.allowJsTransformationFunctions; + this.waitForComplete = yaml.waitForComplete; + this.maxExpiryTimeMs = yaml.maxExpiryTime ? parseDuration(yaml.maxExpiryTime) : undefined; + this.maxExpiryTime = yaml.maxExpiryTime; + } + + @hideKey() + public get publicConfig(): GenericHookServiceConfig { + return { + userIdPrefix: this.userIdPrefix, + allowJsTransformationFunctions: this.allowJsTransformationFunctions, + waitForComplete: this.waitForComplete, + maxExpiryTime: this.maxExpiryTimeMs, + requireExpiryTime: this.requireExpiryTime, + } + } + +} diff --git a/src/config/sections/index.ts b/src/config/sections/index.ts index e4fb02833..8a6b91e70 100644 --- a/src/config/sections/index.ts +++ b/src/config/sections/index.ts @@ -1,2 +1,3 @@ export * from "./cache"; -export * from "./queue"; \ No newline at end of file +export * from "./queue"; +export * from "./generichooks"; \ No newline at end of file diff --git a/src/generic/Router.ts b/src/generic/Router.ts index 53731e7d4..6645afbdb 100644 --- a/src/generic/Router.ts +++ b/src/generic/Router.ts @@ -4,7 +4,8 @@ import { Logger } from "matrix-appservice-bridge"; import { ApiError, ErrCode } from "../api"; import { GenericWebhookEvent, GenericWebhookEventResult } from "./types"; import * as xml from "xml2js"; -import helmet, { crossOriginOpenerPolicy } from "helmet"; +import helmet from "helmet"; +import { StatusCodes } from "http-status-codes"; const WEBHOOK_RESPONSE_TIMEOUT = 5000; @@ -42,21 +43,21 @@ export class GenericWebhooksRouter { next(); return; } - res.status(404).send({ok: false, error: "Webhook not found"}); + res.status(StatusCodes.NOT_FOUND).send({ok: false, error: "Webhook not found"}); } else if (response.successful) { const body = response.response?.body ?? {ok: true}; if (response.response?.contentType) { res.contentType(response.response.contentType); } - res.status(response.response?.statusCode ?? 200).send(body); + res.status(response.response?.statusCode ?? StatusCodes.OK).send(body); } else if (response.successful === false) { - res.status(500).send({ok: false, error: "Failed to process webhook"}); + res.status(response.statusCode ?? StatusCodes.INTERNAL_SERVER_ERROR).send({ok: false, error: response.error || "Failed to process webhook"}); } else { - res.status(202).send({ok: true}); + res.status(StatusCodes.ACCEPTED).send({ok: true}); } }).catch((err) => { log.error(`Failed to emit payload: ${err}`); - res.status(500).send({ok: false, error: "Failed to handle webhook"}); + res.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ok: false, error: "Failed to handle webhook"}); }); } diff --git a/src/generic/types.ts b/src/generic/types.ts index f028a0465..611c490fe 100644 --- a/src/generic/types.ts +++ b/src/generic/types.ts @@ -5,8 +5,18 @@ export interface GenericWebhookEvent { hookId: string; } -export interface GenericWebhookEventResult { - successful?: boolean|null; + +export type GenericWebhookEventResult = GenericWebhookEventResultSuccess | GenericWebhookEventResultFailure; + + +export interface GenericWebhookEventResultSuccess { + successful: true|null; response?: WebhookResponse, notFound?: boolean; +} +export interface GenericWebhookEventResultFailure { + successful: false; + statusCode?: number; + error?: string; + notFound?: boolean; } \ No newline at end of file diff --git a/tests/connections/GenericHookTest.ts b/tests/connections/GenericHookTest.ts index 007c880d6..b683d6a6f 100644 --- a/tests/connections/GenericHookTest.ts +++ b/tests/connections/GenericHookTest.ts @@ -1,11 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { expect } from "chai"; -import { MatrixError } from "matrix-bot-sdk"; -import { BridgeConfigGenericWebhooks, BridgeGenericWebhooksConfigYAML } from "../../src/config/Config"; +import { assert, expect } from "chai"; +import { Appservice, Intent, MatrixError } from "matrix-bot-sdk"; +import { BridgeConfigGenericWebhooks, BridgeGenericWebhooksConfigYAML } from "../../src/config/sections"; import { GenericHookConnection, GenericHookConnectionState } from "../../src/Connections/GenericHook"; import { MessageSenderClient, IMatrixSendMessage } from "../../src/MatrixSender"; import { LocalMQ } from "../../src/MessageQueue/LocalMQ"; import { AppserviceMock } from "../utils/AppserviceMock"; +import { MemoryStorageProvider } from "../../src/Stores/MemoryStorageProvider"; +import { BridgeConfig } from "../../src/config/Config"; +import { ProvisionConnectionOpts } from "../../src/Connections"; +import { add } from "date-fns"; const ROOM_ID = "!foo:bar"; @@ -31,12 +35,15 @@ async function testSimpleWebhook(connection: GenericHookConnection, mq: LocalMQ, }); } +const ConfigDefaults = {enabled: true, urlPrefix: "https://example.com/webhookurl"}; + function createGenericHook( state: Partial = { }, - config: BridgeGenericWebhooksConfigYAML = { enabled: true, urlPrefix: "https://example.com/webhookurl"} -) { + config: Partial = { } +): [GenericHookConnection, LocalMQ, Appservice, Intent] { const mq = new LocalMQ(); mq.subscribe('*'); + const storage = new MemoryStorageProvider(); const messageClient = new MessageSenderClient(mq); const as = AppserviceMock.create(); const intent = as.getIntentForUserId('@webhooks:example.test'); @@ -45,7 +52,10 @@ function createGenericHook( transformationFunction: undefined, waitForComplete: undefined, ...state, - }, "foobar", "foobar", messageClient, new BridgeConfigGenericWebhooks(config), as, intent); + }, "foobar", "foobar", messageClient, new BridgeConfigGenericWebhooks({ + ...ConfigDefaults, + ...config, + }), as, intent, storage); return [connection, mq, as, intent]; } @@ -162,8 +172,6 @@ describe("GenericHookConnection", () => { it("will handle a hook event with a v1 transformation function", async () => { const webhookData = {question: 'What is the meaning of life?', answer: 42}; const [connection, mq] = createGenericHook({name: 'test', transformationFunction: V1TFFunction}, { - enabled: true, - urlPrefix: "https://example.com/webhookurl", allowJsTransformationFunctions: true, } ); @@ -185,8 +193,6 @@ describe("GenericHookConnection", () => { it("will handle a hook event with a v2 transformation function", async () => { const webhookData = {question: 'What is the meaning of life?', answer: 42}; const [connection, mq] = createGenericHook({name: 'test', transformationFunction: V2TFFunction}, { - enabled: true, - urlPrefix: "https://example.com/webhookurl", allowJsTransformationFunctions: true, } ); @@ -208,8 +214,6 @@ describe("GenericHookConnection", () => { it("will handle a hook event with a top-level return", async () => { const webhookData = {question: 'What is the meaning of life?', answer: 42}; const [connection, mq] = createGenericHook({name: 'test', transformationFunction: V2TFFunctionWithReturn}, { - enabled: true, - urlPrefix: "https://example.com/webhookurl", allowJsTransformationFunctions: true, } ); @@ -231,8 +235,6 @@ describe("GenericHookConnection", () => { it("will fail to handle a webhook with an invalid script", async () => { const webhookData = {question: 'What is the meaning of life?', answer: 42}; const [connection, mq] = createGenericHook({name: 'test', transformationFunction: "bibble bobble"}, { - enabled: true, - urlPrefix: "https://example.com/webhookurl", allowJsTransformationFunctions: true, } ); @@ -290,8 +292,7 @@ describe("GenericHookConnection", () => { }); it("should handle simple hook events with user Id prefix", async () => { - const config = { enabled: true, urlPrefix: "https://example.com/webhookurl", userIdPrefix: "_webhooks_"}; - const [connection, mq] = createGenericHook(undefined, config); + const [connection, mq] = createGenericHook(undefined, { userIdPrefix: "_webhooks_"}); await testSimpleWebhook(connection, mq, "data1"); // regression test covering https://github.com/matrix-org/matrix-hookshot/issues/625 await testSimpleWebhook(connection, mq, "data2"); @@ -299,22 +300,21 @@ describe("GenericHookConnection", () => { it("should invite a configured puppet to the room if it's unable to join", async () => { const senderUserId = "@_webhooks_some-name:example.test"; - const config = { enabled: true, urlPrefix: "https://example.com/webhookurl", userIdPrefix: "_webhooks_"}; - const [connection, mq, as, botIntent] = createGenericHook(undefined, config); + const [connection, mq, as, botIntent] = createGenericHook(undefined, { userIdPrefix: "_webhooks_"}); const intent = as.getIntentForUserId(senderUserId); let hasInvited = false; // This should fail the first time, then pass once we've tried to invite the user - intent.ensureJoined = (roomId: string) => { + intent.ensureJoined = async (roomId: string) => { if (hasInvited) { - return; + return roomId; } expect(roomId).to.equal(ROOM_ID); throw new MatrixError({ errcode: "M_FORBIDDEN", error: "Test forced error"}, 401) }; // This should invite the puppet user. - botIntent.underlyingClient.inviteUser = (userId: string, roomId: string) => { + botIntent.underlyingClient.inviteUser = async (userId: string, roomId: string) => { expect(userId).to.equal(senderUserId); expect(roomId).to.equal(ROOM_ID); hasInvited = true; @@ -328,8 +328,7 @@ describe("GenericHookConnection", () => { it("should fail a message if a bot cannot join a room", async () => { const senderUserId = "@_webhooks_some-name:example.test"; - const config = { enabled: true, urlPrefix: "https://example.com/webhookurl", userIdPrefix: "_webhooks_"}; - const [connection, mq, as] = createGenericHook(undefined, config); + const [connection, mq, as] = createGenericHook(undefined, { userIdPrefix: "_webhooks_"}); const intent = as.getIntentForUserId(senderUserId); // This should fail the first time, then pass once we've tried to invite the user @@ -343,4 +342,95 @@ describe("GenericHookConnection", () => { expect(ex.message).to.contain(`Could not ensure that ${senderUserId} is in ${ROOM_ID}`) } }); + + it('should fail to create a hook with an invalid expiry time', () => { + for (const expirationDate of [0, 1, -1, false, true, {}, [], new Date(), ""]) { + expect(() => GenericHookConnection.validateState({ + name: "beep", + expirationDate, + })).to.throw("'expirationDate' must be a non-empty string"); + } + for (const expirationDate of ["no", "\0", "true", " 2024", "2024-01-01", "15:56", "2024-01-01 15:16"]) { + expect(() => GenericHookConnection.validateState({ + name: "beep", + expirationDate, + })).to.throw("'expirationDate' must be a valid date"); + } + }); + it('should fail to create a hook with a too short expiry time', async () => { + const as = AppserviceMock.create(); + try { + await GenericHookConnection.provisionConnection(ROOM_ID, "@some:user", { + name: "foo", + expirationDate: new Date().toISOString(), + }, { + as: as, + intent: as.botIntent, + config: { generic: new BridgeConfigGenericWebhooks(ConfigDefaults) } as unknown as BridgeConfig, + messageClient: new MessageSenderClient(new LocalMQ()), + storage: new MemoryStorageProvider(), + } as unknown as ProvisionConnectionOpts); + assert.fail('Expected function to throw'); + } catch (ex) { + expect(ex.message).to.contain('Expiration date must at least be a hour in the future'); + } + }); + it('should fail to create a hook with a too long expiry time', async () => { + const as = AppserviceMock.create(); + try { + await GenericHookConnection.provisionConnection(ROOM_ID, "@some:user", { + name: "foo", + expirationDate: add(new Date(), { days: 1, seconds: 1}).toISOString(), + }, { + as: as, + intent: as.botIntent, + config: { generic: new BridgeConfigGenericWebhooks({ + ...ConfigDefaults, + maxExpiryTime: '1d' + }) } as unknown as BridgeConfig, + messageClient: new MessageSenderClient(new LocalMQ()), + storage: new MemoryStorageProvider(), + } as unknown as ProvisionConnectionOpts); + assert.fail('Expected function to throw'); + } catch (ex) { + expect(ex.message).to.contain('Expiration date cannot exceed the configured max expiry time'); + } + }); + it('should fail to create a hook without an expiry time when required by config', async () => { + const as = AppserviceMock.create(); + try { + await GenericHookConnection.provisionConnection(ROOM_ID, "@some:user", { + name: "foo", + }, { + as: as, + intent: as.botIntent, + config: { generic: new BridgeConfigGenericWebhooks({ + ...ConfigDefaults, + maxExpiryTime: '1d', + requireExpiryTime: true, + }) } as unknown as BridgeConfig, + messageClient: new MessageSenderClient(new LocalMQ()), + storage: new MemoryStorageProvider(), + } as unknown as ProvisionConnectionOpts); + assert.fail('Expected function to throw'); + } catch (ex) { + expect(ex.message).to.contain('Expiration date must be set'); + } + }); + it('should create a hook and handle a request within the expiry time', async () => { + const [connection, mq] = createGenericHook({ + expirationDate: add(new Date(), { seconds: 30 }).toISOString(), + }); + await testSimpleWebhook(connection, mq, "test"); + }); + it('should reject requests to an expired hook', async () => { + const [connection] = createGenericHook({ + expirationDate: new Date().toISOString(), + }); + expect(await connection.onGenericHook({test: "value"})).to.deep.equal({ + error: "This hook has expired", + statusCode: 404, + successful: false, + }); + }); }) diff --git a/web/components/RoomConfigView.tsx b/web/components/RoomConfigView.tsx index 3ce4af351..9a0f25dd2 100644 --- a/web/components/RoomConfigView.tsx +++ b/web/components/RoomConfigView.tsx @@ -14,6 +14,7 @@ import GitHubIcon from "../icons/github.png"; import GitLabIcon from "../icons/gitlab.png"; import JiraIcon from "../icons/jira.png"; import WebhookIcon from "../icons/webhook.png"; +import { ChevronLeftIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; interface IProps { @@ -118,7 +119,7 @@ export default function RoomConfigView(props: IProps) { {!serviceScope && activeConnectionType &&
setActiveConnectionType(null)}> - Browse integrations + Browse integrations
} diff --git a/web/components/elements/Button.tsx b/web/components/elements/Button.tsx index 6f0c87382..9c1622994 100644 --- a/web/components/elements/Button.tsx +++ b/web/components/elements/Button.tsx @@ -2,10 +2,10 @@ import { FunctionComponent, h } from "preact"; import style from "./Button.module.scss"; interface ButtonProps extends h.JSX.HTMLAttributes { - intent?: string; + intent?: "remove"; } -export const Button: FunctionComponent = (props: ButtonProps) => { +export const Button: FunctionComponent = (props) => { let className = style.button; if (props.intent === "remove") { className += ` ${style.remove}`; diff --git a/web/components/elements/InputField.module.scss b/web/components/elements/InputField.module.scss index d3aa901c4..9f9e4bf72 100644 --- a/web/components/elements/InputField.module.scss +++ b/web/components/elements/InputField.module.scss @@ -35,7 +35,7 @@ margin: auto; } - input[type="text"],input[type="search"] { + input[type="text"],input[type="search"],input[type="datetime-local"] { border: 1px solid var(--cpd-color-border-interactive-primary); box-sizing: border-box; border-radius: 8px; @@ -57,3 +57,12 @@ background: var(--cpd-color-bg-subtle-primary); } } + +.container { + display: flex; + gap: 4px; + + button { + min-width: 0; + } +} diff --git a/web/components/elements/InputField.tsx b/web/components/elements/InputField.tsx index 7a11f2a11..1a8aebf06 100644 --- a/web/components/elements/InputField.tsx +++ b/web/components/elements/InputField.tsx @@ -1,22 +1,22 @@ import { FunctionComponent } from "preact"; -import style from "./InputField.module.scss"; - +import styles from "./InputField.module.scss"; +import clsx from 'clsx'; interface Props { className?: string; visible?: boolean; - label?: string; + label: string; noPadding: boolean; - innerChild?: boolean; } -export const InputField: FunctionComponent = ({ className, children, visible = true, label, noPadding, innerChild = false }) => { - const inputClassName = [ - className, - style.inputField, - noPadding && style.nopad - ].filter(a => !!a).join(' '); - return visible ?
- {label && } - {(!label || !innerChild) && children} -
: <> +export const InputField: FunctionComponent = ({ className, children, visible = true, label, noPadding }) => { + if (!visible) { + return null; + } + + return
+ +
+ {children} +
+
; }; \ No newline at end of file diff --git a/web/components/elements/ListItem.tsx b/web/components/elements/ListItem.tsx index 5b826424a..4fa33a785 100644 --- a/web/components/elements/ListItem.tsx +++ b/web/components/elements/ListItem.tsx @@ -1,14 +1,15 @@ -import { FunctionComponent } from "preact" +import { ComponentChild, FunctionComponent } from "preact" import { useState } from "preact/hooks" import style from "./ListItem.module.scss"; +import { ChevronDownIcon, ChevronUpIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; -export const ListItem: FunctionComponent<{text: string}> = ({ text, children }) => { +export const ListItem: FunctionComponent<{text: ComponentChild}> = ({ text, children }) => { const [expand, setExpand] = useState(false); return
-
setExpand(!expand)}> - {text} -
+

setExpand(!expand)}> + {text} {expand ? : } +

{expand && children}
diff --git a/web/components/roomConfig/GenericWebhookConfig.tsx b/web/components/roomConfig/GenericWebhookConfig.tsx index ec583f416..d8a999d83 100644 --- a/web/components/roomConfig/GenericWebhookConfig.tsx +++ b/web/components/roomConfig/GenericWebhookConfig.tsx @@ -2,11 +2,14 @@ import { FunctionComponent, createRef } from "preact"; import { useCallback, useEffect, useState } from "preact/hooks" import CodeMirror from '@uiw/react-codemirror'; import { javascript } from '@codemirror/lang-javascript'; +import { add, format } from "date-fns"; import { BridgeConfig } from "../../BridgeAPI"; -import { GenericHookConnectionState, GenericHookResponseItem } from "../../../src/Connections/GenericHook"; +import type { GenericHookConnectionState, GenericHookResponseItem, GenericHookServiceConfig } from "../../../src/Connections/GenericHook"; import { ConnectionConfigurationProps, RoomConfig } from "./RoomConfig"; import { InputField, ButtonSet, Button } from "../elements"; import WebhookIcon from "../../icons/webhook.png"; +import { Alert, ToggleInput } from "@vector-im/compound-web"; +import { InfoIcon, WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons" const EXAMPLE_SCRIPT = `if (data.counter === undefined) { result = { @@ -28,14 +31,20 @@ const EXAMPLE_SCRIPT = `if (data.counter === undefined) { const DOCUMENTATION_LINK = "https://matrix-org.github.io/matrix-hookshot/latest/setup/webhooks.html#script-api"; const CODE_MIRROR_EXTENSIONS = [javascript({})]; -const ConnectionConfiguration: FunctionComponent> = ({serviceConfig, existingConnection, onSave, onRemove, isUpdating}) => { +const EXPIRY_WARN_AT_MS = 3 * 24 * 60 * 60 * 1000; + +const ConnectionConfiguration: FunctionComponent> = ({serviceConfig, existingConnection, onSave, onRemove, isUpdating}) => { const [transFn, setTransFn] = useState(existingConnection?.config.transformationFunction as string || EXAMPLE_SCRIPT); const [transFnEnabled, setTransFnEnabled] = useState(serviceConfig.allowJsTransformationFunctions && !!existingConnection?.config.transformationFunction); const [waitForComplete, setWaitForComplete] = useState(existingConnection?.config.waitForComplete ?? false); + const minExpiryTime = format(add(new Date(), { hours: 1 }), "yyyy-MM-dd'T'HH:mm"); + const maxExpiryTime = serviceConfig.maxExpiryTime ? format(Date.now() + serviceConfig.maxExpiryTime, "yyyy-MM-dd'T'HH:mm") : undefined; + const nameRef = createRef(); + const expiryRef = createRef(); - const canEdit = !existingConnection || (existingConnection?.canEdit ?? false); + const canEdit = !existingConnection || existingConnection?.canEdit || false; const handleSave = useCallback((evt: Event) => { evt.preventDefault(); if (!canEdit) { @@ -43,10 +52,11 @@ const ConnectionConfiguration: FunctionComponent("light"); useEffect(() => { @@ -55,7 +65,6 @@ const ConnectionConfiguration: FunctionComponent { - console.log('media change!'); setCodeMirrorTheme(event.matches ? "dark" : "light"); }; mm.addEventListener('change', fn); @@ -63,56 +72,96 @@ const ConnectionConfiguration: FunctionComponent mm.removeEventListener('change', fn); }, [transFnEnabled]); + + const hasExpired = existingConnection?.secrets?.timeRemainingMs ? existingConnection?.secrets?.timeRemainingMs <= 0 : false; + const willExpireSoon = !hasExpired && existingConnection?.secrets?.timeRemainingMs ? existingConnection?.secrets?.timeRemainingMs <= EXPIRY_WARN_AT_MS : false; + + useEffect(() => { + if (!expiryRef.current || !existingConnection?.config.expirationDate) { + return; + } + console.log('Setting date', new Date(existingConnection.config.expirationDate)); + expiryRef.current.valueAsDate = new Date(existingConnection.config.expirationDate); + }, [existingConnection, expiryRef]); + return
+ {hasExpired && + This Webhook has expired and will no longer handle any incoming requests. Please set a new expiry date or remove the Webhook. + } + {willExpireSoon && + This Webhook will expired soon will no longer handle any incoming requests. To extend the Webhook lifetime, set a new expiry date below. + } - + - + + + + + + - setTransFnEnabled(v => !v), [])} /> + setTransFnEnabled(v => !v), [])} /> - setWaitForComplete(v => !v), [])} /> + setWaitForComplete(v => !v), [])} /> - - -

See the documentation for help writing transformation functions

-
+ {transFnEnabled && <> +

See the documentation for help writing transformation functions

+ } - { canEdit && } - { canEdit && existingConnection && } + { canEdit && } + { canEdit && existingConnection && } ; }; -interface ServiceConfig { - allowJsTransformationFunctions: boolean, - waitForComplete: boolean, -} const RoomConfigText = { header: 'Inbound (Generic) Webhooks', - createNew: 'Create new webhook', - listCanEdit: 'Your webhooks', - listCantEdit: 'Configured webhooks', + createNew: 'Create new Webhook', + listCanEdit: 'Your Webhooks', + listCantEdit: 'Configured Webhooks', }; -const RoomConfigListItemFunc = (c: GenericHookResponseItem) => c.config.name; +const RoomConfigListItemFunc = (c: GenericHookResponseItem) => { + const hasExpired = c.secrets?.timeRemainingMs ? c.secrets.timeRemainingMs <= 0 : false; + const willExpireSoon = !hasExpired && c.secrets?.timeRemainingMs ? c.secrets?.timeRemainingMs <= EXPIRY_WARN_AT_MS : false; + + return <> + {c.config.name} + + {hasExpired && } + {willExpireSoon && } + + +}; export const GenericWebhookConfig: BridgeConfig = ({ roomId, showHeader }) => { - return + return headerImg={WebhookIcon} darkHeaderImg={true} showHeader={showHeader} diff --git a/web/components/roomConfig/GitlabRepoConfig.tsx b/web/components/roomConfig/GitlabRepoConfig.tsx index 7b6922258..573c56c9e 100644 --- a/web/components/roomConfig/GitlabRepoConfig.tsx +++ b/web/components/roomConfig/GitlabRepoConfig.tsx @@ -88,7 +88,7 @@ const ConnectionConfiguration: FunctionComponent - + diff --git a/web/components/roomConfig/RoomConfig.tsx b/web/components/roomConfig/RoomConfig.tsx index a42e8d9ca..bbc76fba2 100644 --- a/web/components/roomConfig/RoomConfig.tsx +++ b/web/components/roomConfig/RoomConfig.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent } from "preact"; +import { ComponentChild, FunctionComponent } from "preact"; import { useCallback, useContext, useEffect, useReducer, useState } from "preact/hooks" import { BridgeAPIError } from "../../BridgeAPI"; import { ListItem, Card } from "../elements"; @@ -39,7 +39,7 @@ interface IRoomConfigProps string, + listItemName: (c: ConnectionType) => ComponentChild, connectionConfigComponent: FunctionComponent>; } diff --git a/web/styling.scss b/web/styling.scss index 028d9d229..51d1aebbc 100644 --- a/web/styling.scss +++ b/web/styling.scss @@ -37,33 +37,3 @@ button { font-weight: 600; margin-right: 8px; } - - -.chevron::before { - border-style: solid; - border-width: 0.25em 0.25em 0 0; - border-radius: 3px; - content: ''; - display: inline-block; - height: 10px; - position: relative; - top: 0.15em; - vertical-align: top; - width: 10px; - left: 0.25em; - transform: rotate(-135deg); - margin-right: 5px; -} - -.chevron.down::before { - transform: rotate(-225deg); - border-color: var(--cpd-color-text-primary); - float: right; -} - - -.chevron.up::before { - transform: rotate(-45deg); - border-color: var(--cpd-color-text-primary); - float: right; -} diff --git a/yarn.lock b/yarn.lock index b9a4d6a06..4a08c2ebb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -309,7 +309,7 @@ "@babel/plugin-syntax-jsx" "^7.23.3" "@babel/types" "^7.23.4" -"@babel/runtime@^7.13.10", "@babel/runtime@^7.18.6", "@babel/runtime@^7.6.0": +"@babel/runtime@^7.18.6", "@babel/runtime@^7.6.0": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d" integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ== @@ -647,20 +647,20 @@ dependencies: "@floating-ui/dom" "^1.5.1" -"@floating-ui/react-dom@^2.0.8", "@floating-ui/react-dom@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.0.tgz#4f0e5e9920137874b2405f7d6c862873baf4beff" - integrity sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA== +"@floating-ui/react-dom@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31" + integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A== dependencies: "@floating-ui/dom" "^1.0.0" -"@floating-ui/react@^0.26.9": - version "0.26.17" - resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.17.tgz#efa2e1a0dea3d9d308965c5ccd49756bb64a883d" - integrity sha512-ESD+jYWwqwVzaIgIhExrArdsCL1rOAzryG/Sjlu8yaD3Mtqi3uVyhbE2V7jD58Mo52qbzKz2eUY/Xgh5I86FCQ== +"@floating-ui/react@^0.26.24": + version "0.26.28" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.28.tgz#93f44ebaeb02409312e9df9507e83aab4a8c0dc7" + integrity sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw== dependencies: - "@floating-ui/react-dom" "^2.1.0" - "@floating-ui/utils" "^0.2.0" + "@floating-ui/react-dom" "^2.1.2" + "@floating-ui/utils" "^0.2.8" tabbable "^6.0.0" "@floating-ui/utils@^0.1.3": @@ -673,6 +673,11 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5" integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw== +"@floating-ui/utils@^0.2.8": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" + integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== + "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -1328,337 +1333,286 @@ "@prefresh/utils" "^1.2.0" "@rollup/pluginutils" "^4.2.1" -"@radix-ui/primitive@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd" - integrity sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw== - dependencies: - "@babel/runtime" "^7.13.10" +"@radix-ui/primitive@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2" + integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA== -"@radix-ui/react-arrow@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" - integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA== +"@radix-ui/react-arrow@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a" + integrity sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-primitive" "2.0.0" -"@radix-ui/react-collection@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159" - integrity sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA== +"@radix-ui/react-collection@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed" + integrity sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" -"@radix-ui/react-compose-refs@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989" - integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw== - dependencies: - "@babel/runtime" "^7.13.10" +"@radix-ui/react-compose-refs@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" + integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== -"@radix-ui/react-context-menu@^2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.1.5.tgz#1bdbd72761439f9166f75dc4598f276265785c83" - integrity sha512-R5XaDj06Xul1KGb+WP8qiOh7tKJNz2durpLBXAGZjSVtctcRFCuEvy2gtMwRJGePwQQE5nV77gs4FwRi8T+r2g== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-menu" "2.0.6" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" - "@radix-ui/react-use-controllable-state" "1.0.1" - -"@radix-ui/react-context@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c" - integrity sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg== +"@radix-ui/react-context-menu@^2.2.1": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.2.tgz#efcddc559fc3011721b65148f062d04027f76c7a" + integrity sha512-99EatSTpW+hRYHt7m8wdDlLtkmTovEe8Z/hnxUPV+SKuuNL5HWNhQI4QSdjZqNSgXHay2z4M3Dym73j9p2Gx5Q== dependencies: - "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-menu" "2.1.2" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" -"@radix-ui/react-dialog@^1.0.4": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" - integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-dismissable-layer" "1.0.5" - "@radix-ui/react-focus-guards" "1.0.1" - "@radix-ui/react-focus-scope" "1.0.4" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-portal" "1.0.4" - "@radix-ui/react-presence" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-slot" "1.0.2" - "@radix-ui/react-use-controllable-state" "1.0.1" +"@radix-ui/react-context@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8" + integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A== + +"@radix-ui/react-context@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a" + integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q== + +"@radix-ui/react-dialog@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz#d9345575211d6f2d13e209e84aec9a8584b54d6c" + integrity sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.1" + "@radix-ui/react-focus-guards" "1.1.1" + "@radix-ui/react-focus-scope" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-portal" "1.1.2" + "@radix-ui/react-presence" "1.1.1" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" aria-hidden "^1.1.1" - react-remove-scroll "2.5.5" + react-remove-scroll "2.6.0" -"@radix-ui/react-direction@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" - integrity sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA== +"@radix-ui/react-direction@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc" + integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg== + +"@radix-ui/react-dismissable-layer@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz#cbdcb739c5403382bdde5f9243042ba643883396" + integrity sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ== dependencies: - "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-escape-keydown" "1.1.0" -"@radix-ui/react-dismissable-layer@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4" - integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g== +"@radix-ui/react-dropdown-menu@^2.1.1": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz#acc49577130e3c875ef0133bd1e271ea3392d924" + integrity sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-menu" "2.1.2" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + +"@radix-ui/react-focus-guards@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe" + integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg== + +"@radix-ui/react-focus-scope@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2" + integrity sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" - "@radix-ui/react-use-escape-keydown" "1.0.3" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" -"@radix-ui/react-dropdown-menu@^2.0.6": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz#cdf13c956c5e263afe4e5f3587b3071a25755b63" - integrity sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-menu" "2.0.6" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-controllable-state" "1.0.1" - -"@radix-ui/react-focus-guards@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" - integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA== +"@radix-ui/react-form@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-form/-/react-form-0.1.0.tgz#7111a6aa54a2bde0d11fb72643f9ffc871ac58ad" + integrity sha512-1/oVYPDjbFILOLIarcGcMKo+y6SbTVT/iUKVEw59CF4offwZgBgC3ZOeSBewjqU0vdA6FWTPWTN63obj55S/tQ== dependencies: - "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-label" "2.1.0" + "@radix-ui/react-primitive" "2.0.0" -"@radix-ui/react-focus-scope@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" - integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" - -"@radix-ui/react-form@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-form/-/react-form-0.0.3.tgz#328e7163e723ccc748459d66a2d685d7b4f85d5a" - integrity sha512-kgE+Z/haV6fxE5WqIXj05KkaXa3OkZASoTDy25yX2EIp/x0c54rOH/vFr5nOZTg7n7T1z8bSyXmiVIFP9bbhPQ== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-label" "2.0.2" - "@radix-ui/react-primitive" "1.0.3" - -"@radix-ui/react-id@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" - integrity sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ== +"@radix-ui/react-id@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed" + integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.1.0" -"@radix-ui/react-label@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.0.2.tgz#9c72f1d334aac996fdc27b48a8bdddd82108fb6d" - integrity sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ== +"@radix-ui/react-label@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.1.0.tgz#3aa2418d70bb242be37c51ff5e51a2adcbc372e3" + integrity sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-primitive" "2.0.0" -"@radix-ui/react-menu@2.0.6": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.6.tgz#2c9e093c1a5d5daa87304b2a2f884e32288ae79e" - integrity sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-collection" "1.0.3" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-direction" "1.0.1" - "@radix-ui/react-dismissable-layer" "1.0.5" - "@radix-ui/react-focus-guards" "1.0.1" - "@radix-ui/react-focus-scope" "1.0.4" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-popper" "1.1.3" - "@radix-ui/react-portal" "1.0.4" - "@radix-ui/react-presence" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-roving-focus" "1.0.4" - "@radix-ui/react-slot" "1.0.2" - "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-menu@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.2.tgz#91f6815845a4298dde775563ed2d80b7ad667899" + integrity sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-collection" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.1" + "@radix-ui/react-focus-guards" "1.1.1" + "@radix-ui/react-focus-scope" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.0" + "@radix-ui/react-portal" "1.1.2" + "@radix-ui/react-presence" "1.1.1" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-roving-focus" "1.1.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-callback-ref" "1.1.0" aria-hidden "^1.1.1" - react-remove-scroll "2.5.5" + react-remove-scroll "2.6.0" -"@radix-ui/react-popper@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42" - integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w== +"@radix-ui/react-popper@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a" + integrity sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg== dependencies: - "@babel/runtime" "^7.13.10" "@floating-ui/react-dom" "^2.0.0" - "@radix-ui/react-arrow" "1.0.3" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" - "@radix-ui/react-use-layout-effect" "1.0.1" - "@radix-ui/react-use-rect" "1.0.1" - "@radix-ui/react-use-size" "1.0.1" - "@radix-ui/rect" "1.0.1" - -"@radix-ui/react-portal@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15" - integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q== + "@radix-ui/react-arrow" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-rect" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/rect" "1.1.0" + +"@radix-ui/react-portal@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.2.tgz#51eb46dae7505074b306ebcb985bf65cc547d74e" + integrity sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-layout-effect" "1.1.0" -"@radix-ui/react-presence@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" - integrity sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg== +"@radix-ui/react-presence@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz#98aba423dba5e0c687a782c0669dcd99de17f9b1" + integrity sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" -"@radix-ui/react-primitive@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0" - integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g== +"@radix-ui/react-primitive@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884" + integrity sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-slot" "1.1.0" -"@radix-ui/react-roving-focus@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974" - integrity sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-collection" "1.0.3" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-direction" "1.0.1" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" - "@radix-ui/react-use-controllable-state" "1.0.1" - -"@radix-ui/react-separator@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.0.3.tgz#be5a931a543d5726336b112f465f58585c04c8aa" - integrity sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw== +"@radix-ui/react-progress@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.0.tgz#28c267885ec154fc557ec7a66cb462787312f7e2" + integrity sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" -"@radix-ui/react-slot@1.0.2", "@radix-ui/react-slot@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" - integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg== +"@radix-ui/react-roving-focus@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e" + integrity sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-collection" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + +"@radix-ui/react-separator@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.0.tgz#ee0f4d86003b0e3ea7bc6ccab01ea0adee32663e" + integrity sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "2.0.0" -"@radix-ui/react-tooltip@^1.0.6": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz#8f55070f852e7e7450cc1d9210b793d2e5a7686e" - integrity sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-dismissable-layer" "1.0.5" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-popper" "1.1.3" - "@radix-ui/react-portal" "1.0.4" - "@radix-ui/react-presence" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-slot" "1.0.2" - "@radix-ui/react-use-controllable-state" "1.0.1" - "@radix-ui/react-visually-hidden" "1.0.3" - -"@radix-ui/react-use-callback-ref@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a" - integrity sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ== +"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84" + integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw== dependencies: - "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.1.0" -"@radix-ui/react-use-controllable-state@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286" - integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-use-callback-ref@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" + integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw== -"@radix-ui/react-use-escape-keydown@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755" - integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg== +"@radix-ui/react-use-controllable-state@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0" + integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" -"@radix-ui/react-use-layout-effect@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399" - integrity sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ== +"@radix-ui/react-use-escape-keydown@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754" + integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw== dependencies: - "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.1.0" -"@radix-ui/react-use-rect@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2" - integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/rect" "1.0.1" +"@radix-ui/react-use-layout-effect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" + integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w== -"@radix-ui/react-use-size@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2" - integrity sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g== +"@radix-ui/react-use-rect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88" + integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/rect" "1.1.0" -"@radix-ui/react-visually-hidden@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz#51aed9dd0fe5abcad7dee2a234ad36106a6984ac" - integrity sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA== +"@radix-ui/react-use-size@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b" + integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-layout-effect" "1.1.0" -"@radix-ui/rect@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f" - integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ== - dependencies: - "@babel/runtime" "^7.13.10" +"@radix-ui/rect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438" + integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg== "@rollup/plugin-alias@^5.1.0": version "5.1.0" @@ -2097,11 +2051,6 @@ dependencies: undici-types "~5.26.4" -"@types/q@^1.5.1": - version "1.5.8" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.8.tgz#95f6c6a08f2ad868ba230ead1d2d7f7be3db3837" - integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw== - "@types/qs@*": version "6.9.11" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda" @@ -2349,39 +2298,32 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vector-im/compound-design-tokens@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.3.0.tgz#1d04f006a9e56b920432095d08d7c84c0933ebc7" - integrity sha512-RXcyEAdxNzekMhVuvxtLPt9zb6yT2N+5cnb2Hul9zwRiF7+XEHpD36+IF6V0QOXk2pkN0wOr3jCvc9eOWOq9SQ== - dependencies: - svg2vectordrawable "^2.9.1" - -"@vector-im/compound-web@^4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-4.8.0.tgz#1fe11d78549694f8d91b40065994bad19a7cebf2" - integrity sha512-kyB8wQPbdTUFWIzAbb4HcZ4iisUUpbm0xwmEjV9ZNN1/EIodidW6nLeYATh3Vc1fBvTGTgbFiPc1DiAcBuudiw== - dependencies: - "@floating-ui/react" "^0.26.9" - "@floating-ui/react-dom" "^2.0.8" - "@radix-ui/react-context-menu" "^2.1.5" - "@radix-ui/react-dropdown-menu" "^2.0.6" - "@radix-ui/react-form" "^0.0.3" - "@radix-ui/react-separator" "^1.0.3" - "@radix-ui/react-slot" "^1.0.2" - "@radix-ui/react-tooltip" "^1.0.6" - classnames "^2.3.2" - vaul "^0.7.0" +"@vector-im/compound-design-tokens@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.0.1.tgz#add14494caab16cdbe98f2bdabe726908739def4" + integrity sha512-4nkPcrPII+sejispn+UkWZYFN7LecN39e4WGBupdceiMq0NJrfXrnVtJ9/6BDLgSqHInb6R/IWQkIbPbzfqRMg== + +"@vector-im/compound-web@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.3.0.tgz#9594113ac50bff4794715104a30a60c52d15517d" + integrity sha512-gDppQUtpk5LvNHUg+Zlv9qzo1iBAag0s3g8Ec0qS5q4zGBKG6ruXXrNUKg1aK8rpbo2hYQsGaHM6dD8NqLoq3Q== + dependencies: + "@floating-ui/react" "^0.26.24" + "@radix-ui/react-context-menu" "^2.2.1" + "@radix-ui/react-dropdown-menu" "^2.1.1" + "@radix-ui/react-form" "^0.1.0" + "@radix-ui/react-progress" "^1.1.0" + "@radix-ui/react-separator" "^1.1.0" + "@radix-ui/react-slot" "^1.1.0" + classnames "^2.5.1" + ts-xor "^1.3.0" + vaul "^1.0.0" abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abs-svg-path@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/abs-svg-path/-/abs-svg-path-0.1.1.tgz#df601c8e8d2ba10d4a76d625e236a9a39c2723bf" - integrity sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA== - accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -3008,7 +2950,7 @@ chalk@4, chalk@^4, chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -3054,10 +2996,10 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== -classnames@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" - integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== +classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== clean-stack@^2.0.0: version "2.2.0" @@ -3091,6 +3033,11 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + cluster-key-slot@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" @@ -3101,15 +3048,6 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== -coa@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" - integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== - dependencies: - "@types/q" "^1.5.1" - chalk "^2.4.1" - q "^1.1.2" - codemirror@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29" @@ -3280,17 +3218,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -css-select@^4.1.3: - version "4.3.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" - integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== - dependencies: - boolbase "^1.0.0" - css-what "^6.0.1" - domhandler "^4.3.1" - domutils "^2.8.0" - nth-check "^2.0.1" - css-select@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" @@ -3302,14 +3229,6 @@ css-select@^5.1.0: domutils "^3.0.1" nth-check "^2.0.1" -css-tree@^1.1.2, css-tree@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - css-tree@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" @@ -3326,18 +3245,11 @@ css-tree@~2.2.0: mdn-data "2.0.28" source-map-js "^1.0.1" -css-what@^6.0.1, css-what@^6.1.0: +css-what@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== -csso@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - csso@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" @@ -3352,6 +3264,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14" + integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -3531,7 +3448,7 @@ domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== -domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: +domhandler@^4.0.0, domhandler@^4.2.0: version "4.3.1" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== @@ -3545,7 +3462,7 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -domutils@^2.5.2, domutils@^2.8.0: +domutils@^2.5.2: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== @@ -5034,11 +4951,6 @@ is-string@^1.0.5, is-string@^1.0.7: dependencies: has-tostringtag "^1.0.0" -is-svg-path@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-svg-path/-/is-svg-path-1.0.2.tgz#77ab590c12b3d20348e5c7a13d0040c87784dda0" - integrity sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg== - is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" @@ -6064,11 +5976,6 @@ matrix-widget-api@^1.6.0: "@types/events" "^3.0.0" events "^3.2.0" -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - mdn-data@2.0.28: version "2.0.28" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" @@ -6350,13 +6257,6 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-svg-path@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz#0e614eca23c39f0cffe821d6be6cd17e569a766c" - integrity sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg== - dependencies: - svg-arc-to-cubic-bezier "^3.0.0" - npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -6603,6 +6503,11 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-duration@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-1.1.0.tgz#5192084c5d8f2a3fd676d04a451dbd2e05a1819c" + integrity sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ== + parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -6618,11 +6523,6 @@ parse-srcset@^1.0.2: resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== -parse-svg-path@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz#7a7ec0d1eb06fa5325c7d3e009b859a09b5d49eb" - integrity sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ== - parseley@^0.12.0: version "0.12.1" resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.12.1.tgz#4afd561d50215ebe259e3e7a853e62f600683aef" @@ -6879,11 +6779,6 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.4.tgz#50b737f6a925468679bff00ad20eade53f37d5c7" integrity sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA== -q@^1.1.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== - qs@6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" @@ -6981,20 +6876,20 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-remove-scroll-bar@^2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9" - integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A== +react-remove-scroll-bar@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" + integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== dependencies: react-style-singleton "^2.2.1" tslib "^2.0.0" -react-remove-scroll@2.5.5: - version "2.5.5" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" - integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw== +react-remove-scroll@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz#fb03a0845d7768a4f1519a99fdb84983b793dc07" + integrity sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ== dependencies: - react-remove-scroll-bar "^2.3.3" + react-remove-scroll-bar "^2.3.6" react-style-singleton "^2.2.1" tslib "^2.1.0" use-callback-ref "^1.3.0" @@ -7559,11 +7454,6 @@ sshpk@^1.14.1, sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -7766,45 +7656,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -svg-arc-to-cubic-bezier@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz#390c450035ae1c4a0104d90650304c3bc814abe6" - integrity sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g== - -svg-path-bounds@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/svg-path-bounds/-/svg-path-bounds-1.0.2.tgz#00312f672b08afc432a66ddfbd06db40cec8d0d0" - integrity sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ== - dependencies: - abs-svg-path "^0.1.1" - is-svg-path "^1.0.1" - normalize-svg-path "^1.0.0" - parse-svg-path "^0.1.2" - -svg2vectordrawable@^2.9.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/svg2vectordrawable/-/svg2vectordrawable-2.9.1.tgz#23186ff7ace7038d09c031176dbca04063a97e5d" - integrity sha512-7WJIh4SzZLyEJtn45y+f8rREkgBiQMWfb0FoYkXuioywESjDWfbSuP0FQEmIiHP2zOi0oOO8pTG4VkeWJyidWw== - dependencies: - coa "^2.0.2" - mkdirp "^1.0.4" - svg-path-bounds "^1.0.1" - svgo "^2.8.0" - svgpath "^2.5.0" - -svgo@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" - integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== - dependencies: - "@trysound/sax" "0.2.0" - commander "^7.2.0" - css-select "^4.1.3" - css-tree "^1.1.3" - csso "^4.2.0" - picocolors "^1.0.0" - stable "^0.1.8" - svgo@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.2.0.tgz#7a5dff2938d8c6096e00295c2390e8e652fa805d" @@ -7818,11 +7669,6 @@ svgo@^3.1.0: csso "^5.0.5" picocolors "^1.0.0" -svgpath@^2.5.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/svgpath/-/svgpath-2.6.0.tgz#5b160ef3d742b7dfd2d721bf90588d3450d7a90d" - integrity sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg== - tabbable@^6.0.0: version "6.2.0" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" @@ -7932,6 +7778,11 @@ ts-node@^10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +ts-xor@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-xor/-/ts-xor-1.3.0.tgz#3e59f24f0321f9f10f350e0cee3b534b89a2c70b" + integrity sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA== + tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -8177,12 +8028,12 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -vaul@^0.7.0: - version "0.7.9" - resolved "https://registry.yarnpkg.com/vaul/-/vaul-0.7.9.tgz#365dfe8f6c1df3a81a26508474db0e0ceb98ac8c" - integrity sha512-RrcnGOHOq/cEU3YpyyZrnjh0H79xMpF3IrHZs9ichvHlpKjLDc4Vwjn4VkuGzeUGrmQ3wamfm/cpdKWpvBIgQw== +vaul@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.1.tgz#93aceaad16f7c53aacf28a2609b2dd43b5a91fa0" + integrity sha512-+ejzF6ffQKPcfgS7uOrGn017g39F8SO4yLPXbBhpC7a0H+oPqPna8f1BUfXaz8eU4+pxbQcmjxW+jWBSbxjaFg== dependencies: - "@radix-ui/react-dialog" "^1.0.4" + "@radix-ui/react-dialog" "^1.1.1" verror@1.10.0: version "1.10.0"