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
This commit is contained in:
Will Hunt 2024-11-18 17:08:52 +00:00 committed by GitHub
parent 6571b9f710
commit 80c7d35a18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 991 additions and 646 deletions

1
changelog.d/985.feature Normal file
View File

@ -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.

View File

@ -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

View File

@ -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
<section class="notice">

View File

@ -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",

119
spec/generic-hooks.spec.ts Normal file
View File

@ -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: '<p>Received webhook data: Hello world</p>',
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: '<p>Received webhook data: Hello world</p>',
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",
});
});
});

View File

@ -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<GenericWebhookEventResult>({
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<GenericWebhookEventResult>({
data: {successful, response},
data: result,
sender: "Bridge",
messageId,
eventName: "response.generic-webhook.event",
});
} else {
await this.queue.push<GenericWebhookEventResult>({
data: {},
data: {
successful: null,
},
sender: "Bridge",
messageId,
eventName: "response.generic-webhook.event",

View File

@ -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<GenericHookConnectionState, GenericHookSecrets>;
@ -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<string, unknown>): GenericHookConnectionState {
const {name, transformationFunction, waitForComplete} = state;
static validateState(state: Partial<Record<keyof GenericHookConnectionState, unknown>>): 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<Record<string, unknown>>, {as, intent, config, messageClient}: InstantiateConnectionOpts) {
static async createConnectionForState(roomId: string, event: StateEvent<Record<string, unknown>>, {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<string, unknown> = {}, {as, intent, config, messageClient}: ProvisionConnectionOpts) {
static async provisionConnection(roomId: string, userId: string, data: Partial<Record<keyof GenericHookConnectionState, unknown>> = {}, {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<GenericWebhookEventResult> {
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<string, unknown>) {
public async provisionerUpdateConfig(_userId: string, config: Record<keyof GenericHookConnectionState, unknown>) {
// 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}`;
}

View File

@ -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);

View File

@ -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);
}
});
}

View File

@ -16,6 +16,7 @@ export class MemoryStorageProvider extends MSP implements IBridgeStorageProvider
private feedGuids = new Map<string, Array<string>>();
private houndActivityIds = new Map<string, Array<string>>();
private houndActivityIdToEvent = new Map<string, string>();
private hasGenericHookWarnedExpiry = new Set<string>();
constructor() {
super();
@ -139,4 +140,12 @@ export class MemoryStorageProvider extends MSP implements IBridgeStorageProvider
public async getHoundActivity(challengeId: string, activityId: string): Promise<string|null> {
return this.houndActivityIdToEvent.get(`${challengeId}.${activityId}`) ?? null;
}
public async getHasGenericHookWarnedExpiry(hookId: string): Promise<boolean> {
return this.hasGenericHookWarnedExpiry.has(hookId);
}
public async setHasGenericHookWarnedExpiry(hookId: string, hasWarned: boolean): Promise<void> {
this.hasGenericHookWarnedExpiry[hasWarned ? "add" : "delete"](hookId);
}
}

View File

@ -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<string|null> {
return this.redis.get(`${HOUND_EVENTS}${challengeId}.${activityId}`);
}
public async getHasGenericHookWarnedExpiry(hookId: string): Promise<boolean> {
return await this.redis.sismember(GENERIC_HOOK_HAS_WARNED, hookId) === 1;
}
public async setHasGenericHookWarnedExpiry(hookId: string, hasWarned: boolean): Promise<void> {
await this.redis[hasWarned ? "sadd" : "srem"](GENERIC_HOOK_HAS_WARNED, hookId);
}
}

View File

@ -36,4 +36,7 @@ export interface IBridgeStorageProvider extends IAppserviceStorageProvider, ISto
hasSeenHoundActivity(challengeId: string, ...activityHashes: string[]): Promise<string[]>;
storeHoundActivityEvent(challengeId: string, activityId: string, eventId: string): Promise<void>;
getHoundActivity(challengeId: string, activityId: string): Promise<string|null>;
getHasGenericHookWarnedExpiry(hookId: string): Promise<boolean>;
setHasGenericHookWarnedExpiry(hookId: string, hasWarned: boolean): Promise<void>;
}

View File

@ -108,7 +108,7 @@ export class BridgeWidgetApi extends ProvisioningApi {
});
}
private async getServiceConfig(req: ProvisioningRequest, res: Response<Record<string, unknown>>) {
private async getServiceConfig(req: ProvisioningRequest, res: Response<object>) {
// GitHub is a special case because it depends on live config.
if (req.params.service === 'github') {
res.send(this.config.github?.publicConfig(this.github));

View File

@ -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<string, unknown> {
let config: undefined|Record<string, unknown>;
public getPublicConfigForService(serviceName: string): Record<string, unknown>|GenericHookServiceConfig {
let config: undefined|Record<string, unknown>|GenericHookServiceConfig;
switch (serviceName) {
case "feeds":
config = this.feeds?.publicConfig;

View File

@ -109,6 +109,8 @@ export const DefaultConfigRoot: BridgeConfigRoot = {
urlPrefix: `${hookshotWebhooksUrl}/webhook/`,
userIdPrefix: "_webhooks_",
waitForComplete: false,
maxExpiryTime: "30d",
sendExpiryNotice: false,
},
figma: {
publicUrl: `${hookshotWebhooksUrl}/hookshot/`,

View File

@ -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,
}
}
}

View File

@ -1,2 +1,3 @@
export * from "./cache";
export * from "./queue";
export * from "./queue";
export * from "./generichooks";

View File

@ -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"});
});
}

View File

@ -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;
}

View File

@ -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<GenericHookConnectionState> = { },
config: BridgeGenericWebhooksConfigYAML = { enabled: true, urlPrefix: "https://example.com/webhookurl"}
) {
config: Partial<BridgeGenericWebhooksConfigYAML> = { }
): [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,
});
});
})

View File

@ -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 &&
<header>
<span className={style.backButton} onClick={() => setActiveConnectionType(null)}>
<span className="chevron" /> Browse integrations
<ChevronLeftIcon /> Browse integrations
</span>
</header>
}

View File

@ -2,10 +2,10 @@ import { FunctionComponent, h } from "preact";
import style from "./Button.module.scss";
interface ButtonProps extends h.JSX.HTMLAttributes<HTMLButtonElement> {
intent?: string;
intent?: "remove";
}
export const Button: FunctionComponent = (props: ButtonProps) => {
export const Button: FunctionComponent<ButtonProps> = (props) => {
let className = style.button;
if (props.intent === "remove") {
className += ` ${style.remove}`;

View File

@ -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;
}
}

View File

@ -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<Props> = ({ className, children, visible = true, label, noPadding, innerChild = false }) => {
const inputClassName = [
className,
style.inputField,
noPadding && style.nopad
].filter(a => !!a).join(' ');
return visible ? <div className={inputClassName}>
{label && <label>{innerChild && children}{label}</label>}
{(!label || !innerChild) && children}
</div> : <></>
export const InputField: FunctionComponent<Props> = ({ className, children, visible = true, label, noPadding }) => {
if (!visible) {
return null;
}
return <div className={clsx(className, styles.inputField, noPadding && styles.nopad)}>
<label>{label}</label>
<div className={styles.container}>
{children}
</div>
</div>;
};

View File

@ -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 <div className={style.root}>
<div className={style.header} onClick={() => setExpand(!expand)}>
<span>{text}</span><span className={`chevron ${expand ? "up" : "down"}`} />
</div>
<h3 className={style.header} onClick={() => setExpand(!expand)}>
{text} {expand ? <ChevronUpIcon /> : <ChevronDownIcon />}
</h3>
<div className={style.contents}>
{expand && children}
</div>

View File

@ -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<ConnectionConfigurationProps<ServiceConfig, GenericHookResponseItem, GenericHookConnectionState>> = ({serviceConfig, existingConnection, onSave, onRemove, isUpdating}) => {
const EXPIRY_WARN_AT_MS = 3 * 24 * 60 * 60 * 1000;
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<GenericHookServiceConfig, GenericHookResponseItem, GenericHookConnectionState>> = ({serviceConfig, existingConnection, onSave, onRemove, isUpdating}) => {
const [transFn, setTransFn] = useState<string>(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 nameRef = createRef<HTMLInputElement>();
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 canEdit = !existingConnection || (existingConnection?.canEdit ?? false);
const nameRef = createRef<HTMLInputElement>();
const expiryRef = createRef<HTMLInputElement>();
const canEdit = !existingConnection || existingConnection?.canEdit || false;
const handleSave = useCallback((evt: Event) => {
evt.preventDefault();
if (!canEdit) {
@ -43,10 +52,11 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<Se
}
onSave({
name: nameRef?.current?.value || existingConnection?.config.name || "Generic Webhook",
expirationDate: expiryRef?.current?.value ? new Date(expiryRef?.current?.value).toISOString() : undefined,
waitForComplete,
...(transFnEnabled ? { transformationFunction: transFn } : undefined),
});
}, [canEdit, onSave, nameRef, transFn, existingConnection, transFnEnabled, waitForComplete]);
}, [expiryRef, canEdit, onSave, nameRef, transFn, existingConnection, transFnEnabled, waitForComplete]);
const [codeMirrorTheme, setCodeMirrorTheme] = useState<"light"|"dark">("light");
useEffect(() => {
@ -55,7 +65,6 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<Se
}
const mm = window.matchMedia('(prefers-color-scheme: dark)');
const fn = (event: MediaQueryListEvent) => {
console.log('media change!');
setCodeMirrorTheme(event.matches ? "dark" : "light");
};
mm.addEventListener('change', fn);
@ -63,56 +72,96 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<Se
return () => 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 <form onSubmit={handleSave}>
{hasExpired && <Alert type="critical" title="This Webhook has expired">
This Webhook has expired and will no longer handle any incoming requests. Please set a new expiry date or <strong>remove</strong> the Webhook.
</Alert>}
{willExpireSoon && <Alert type="info" title="This Webhook is expiring soon">
This Webhook will expired soon will no longer handle any incoming requests. To extend the Webhook lifetime, set a new expiry date below.
</Alert>}
<InputField visible={!existingConnection} label="Friendly name" noPadding={true}>
<input ref={nameRef} disabled={!canEdit} placeholder="My webhook" type="text" value={existingConnection?.config.name} />
<input ref={nameRef} disabled={!canEdit} placeholder="My Webhook" type="text" value={existingConnection?.config.name} />
</InputField>
<InputField visible={!!existingConnection} label="URL" noPadding={true}>
<input disabled={true} placeholder="URL hidden" type="text" value={existingConnection?.secrets?.url || ""} />
<input disabled={true} placeholder="URL hidden" type="text" value={existingConnection?.secrets?.url?.toString() || ""} />
</InputField>
<InputField label="Expiration date" noPadding={true}>
<input
type="datetime-local"
required={serviceConfig.requireExpiryTime}
disabled={!canEdit}
ref={expiryRef}
min={minExpiryTime}
max={maxExpiryTime} />
<Button intent="remove" onClick={(ev) => {
ev.preventDefault();
if (expiryRef.current?.value) {
expiryRef.current.value = "";
}
}} disabled={!!expiryRef.current?.value}>Clear</Button>
</InputField>
<InputField visible={serviceConfig.allowJsTransformationFunctions} label="Enable Transformation JavaScript" noPadding={true}>
<input disabled={!canEdit} type="checkbox" checked={transFnEnabled} onChange={useCallback(() => setTransFnEnabled(v => !v), [])} />
<ToggleInput disabled={!canEdit} type="checkbox" checked={transFnEnabled} onChange={useCallback(() => setTransFnEnabled(v => !v), [])} />
</InputField>
<InputField visible={serviceConfig.allowJsTransformationFunctions && transFnEnabled} label="Respond after function completes" noPadding={true}>
<input disabled={!canEdit || serviceConfig.waitForComplete} type="checkbox" checked={waitForComplete || serviceConfig.waitForComplete} onChange={useCallback(() => setWaitForComplete(v => !v), [])} />
<ToggleInput disabled={!canEdit || serviceConfig.waitForComplete} type="checkbox" checked={waitForComplete || serviceConfig.waitForComplete} onChange={useCallback(() => setWaitForComplete(v => !v), [])} />
</InputField>
<InputField visible={transFnEnabled} noPadding={true}>
<CodeMirror
value={transFn}
theme={codeMirrorTheme}
extensions={CODE_MIRROR_EXTENSIONS}
onChange={setTransFn}
/>
<p> See the <a target="_blank" rel="noopener noreferrer" href={DOCUMENTATION_LINK}>documentation</a> for help writing transformation functions </p>
</InputField>
{transFnEnabled && <><CodeMirror
value={transFn}
theme={codeMirrorTheme}
extensions={CODE_MIRROR_EXTENSIONS}
onChange={setTransFn}
/>
<p> See the <a target="_blank" rel="noopener noreferrer" href={DOCUMENTATION_LINK}>documentation</a> for help writing transformation functions </p>
</>}
<ButtonSet>
{ canEdit && <Button disabled={isUpdating} type="submit">{ existingConnection ? "Save" : "Add webhook" }</Button>}
{ canEdit && existingConnection && <Button disabled={isUpdating} intent="remove" onClick={onRemove}>Remove webhook</Button>}
{ canEdit && <Button disabled={isUpdating} type="submit">{ existingConnection ? "Save" : "Add Webhook" }</Button>}
{ canEdit && existingConnection && <Button disabled={isUpdating} intent="remove" onClick={onRemove}>Remove Webhook</Button>}
</ButtonSet>
</form>;
};
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 <>
<span style={{verticalAlign: "middle"}}>{c.config.name}</span>
<span style={{marginLeft: "8px", verticalAlign: "middle"}}>
{hasExpired && <WarningIcon />}
{willExpireSoon && <InfoIcon />}
</span>
</>
};
export const GenericWebhookConfig: BridgeConfig = ({ roomId, showHeader }) => {
return <RoomConfig<ServiceConfig, GenericHookResponseItem, GenericHookConnectionState>
return <RoomConfig<GenericHookServiceConfig, GenericHookResponseItem, GenericHookConnectionState>
headerImg={WebhookIcon}
darkHeaderImg={true}
showHeader={showHeader}

View File

@ -88,7 +88,7 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
<InputField visible={!!existingConnection || !!newConnectionState} label="Command Prefix" noPadding={true}>
<input ref={commandPrefixRef} type="text" value={existingConnection?.config.commandPrefix} placeholder="!gl" />
</InputField>
<InputField visible={!!existingConnection || !!newConnectionState} label="Include comment bodies" noPadding={true} innerChild={true}>
<InputField visible={!!existingConnection || !!newConnectionState} label="Include comment bodies" noPadding={true}>
<input ref={includeBodyRef} disabled={!canEdit} type="checkbox" checked={!!existingConnection?.config.includeCommentBody} />
</InputField>
<InputField visible={!!existingConnection || !!newConnectionState} label="Events" noPadding={true}>

View File

@ -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<SConfig, ConnectionType extends GetConnectionsRespons
headerImg: string;
text: IRoomConfigText;
connectionEventType: string;
listItemName: (c: ConnectionType) => string,
listItemName: (c: ConnectionType) => ComponentChild,
connectionConfigComponent: FunctionComponent<ConnectionConfigurationProps<SConfig, ConnectionType, ConnectionState>>;
}

View File

@ -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;
}

749
yarn.lock
View File

@ -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==
"@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.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/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-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==
dependencies:
"@babel/runtime" "^7.13.10"
"@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-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==
"@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:
"@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/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==
dependencies:
"@babel/runtime" "^7.13.10"
"@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.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-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.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/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-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==
"@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:
"@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/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.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==
dependencies:
"@babel/runtime" "^7.13.10"
"@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.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==
"@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/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-compose-refs" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@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==
"@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.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/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-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==
"@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:
"@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/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-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.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-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==
"@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/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-context" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@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-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:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@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-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-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==
"@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/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-compose-refs" "1.1.0"
"@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==
dependencies:
"@babel/runtime" "^7.13.10"
"@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-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==
"@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-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-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.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==
dependencies:
"@babel/runtime" "^7.13.10"
"@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-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==
"@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/rect" "1.0.1"
"@radix-ui/rect" "1.1.0"
"@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-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-use-layout-effect" "1.0.1"
"@radix-ui/react-use-layout-effect" "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==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@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-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@^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==
"@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.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"
"@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"