diff --git a/changelog.d/1805.feature b/changelog.d/1805.feature new file mode 100644 index 000000000..6fe874292 --- /dev/null +++ b/changelog.d/1805.feature @@ -0,0 +1 @@ +Use MediaProxy to serve authenticated Matrix media. diff --git a/config.sample.yaml b/config.sample.yaml index 2ce18b8f2..ea2fbb686 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -2,15 +2,8 @@ # are *REQUIRED*. # Unless otherwise specified, these keys CANNOT be hot-reloaded. homeserver: - # The URL to the home server for client-server API calls, also used to form the - # media URLs as displayed in bridged IRC channels: + # The URL to the home server for client-server API calls url: "http://localhost:8008" - # - # The URL of the homeserver hosting media files. This is only used to transform - # mxc URIs to http URIs when bridging m.room.[file|image] events. Optional. By - # default, this is the homeserver URL, specified above. - # This key CAN be hot-reloaded. - # media_url: "http://media.repo:8008" # Drop Matrix messages which are older than this number of seconds, according to # the event's origin_server_ts. @@ -607,6 +600,18 @@ ircService: # Ignore users mentioned in a io.element.functional_members state event when checking admin room membership ignoreFunctionalMembersInAdminRooms: false + # Config for the media proxy, required to serve publically accessible URLs to authenticated Matrix media + mediaProxy: + # To generate a .jwk file: + # $ node src/generate-signing-key.js > signingkey.jwk + signingKeyPath: "signingkey.jwk" + # How long should the generated URLs be valid for + ttlSeconds: 3600 + # The port for the media proxy to listen on + bindPort: 11111 + # The publically accessible URL to the media proxy + publicUrl: "https://irc.bridge/media" + # Maximum number of montly active users, beyond which the bridge gets blocked (both ways) # RMAUlimit: 100 diff --git a/config.schema.yml b/config.schema.yml index 17d78fe2a..74bd389a1 100644 --- a/config.schema.yml +++ b/config.schema.yml @@ -44,8 +44,6 @@ properties: properties: url: type: "string" - media_url: - type: "string" domain: type: "string" dropMatrixMessagesAfterSecs: @@ -173,6 +171,18 @@ properties: type: "integer" ignoreFunctionalMembersInAdminRooms: type: "boolean" + mediaProxy: + type: "object" + properties: + signingKeyPath: + type: "string" + ttlSeconds: + type: "integer" + bindPort: + type: "integer" + publicUrl: + type: "string" + required: ["signingKeyPath", "ttlSeconds", "bindPort", "publicUrl"] ircHandler: type: "object" properties: @@ -485,4 +495,4 @@ properties: kickReason: type: "string" required: ["regex"] - required: ["servers"] + required: ["servers", "mediaProxy"] diff --git a/package.json b/package.json index b398ea4c4..3db886b6d 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "he": "^1.2.0", "ioredis": "^5.3.1", "logform": "^2.4.2", - "matrix-appservice-bridge": "^10.1.0", + "matrix-appservice-bridge": "^10.2.0", "matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.7.0-element.1", "matrix-org-irc": "^3.0.0", "nopt": "^7.2.0", diff --git a/spec/integ/matrix-to-irc.spec.js b/spec/integ/matrix-to-irc.spec.js index 9aff5ede5..5393e0cb5 100644 --- a/spec/integ/matrix-to-irc.spec.js +++ b/spec/integ/matrix-to-irc.spec.js @@ -632,10 +632,10 @@ describe("Matrix-to-IRC message bridging", function() { expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); // don't be too brittle when checking this, but I expect to see the - // start of the first line and the mxc fragment + // start of the first line and the media proxy url expect(text.indexOf(tBody[0])).toEqual(0); expect(text.indexOf(tBody[1])).not.toEqual(0); - expect(text.indexOf('deadbeefcafe')).not.toEqual(-1); + expect(text.includes(config.ircService.mediaProxy.publicUrl)).toEqual(true); done(); }); @@ -667,9 +667,9 @@ describe("Matrix-to-IRC message bridging", function() { expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); // don't be too brittle when checking this, but I expect to see the - // code type and the mxc fragment. + // code type and the media proxy url expect(text.indexOf('javascript')).not.toEqual(-1); - expect(text.indexOf('deadbeefcafe')).not.toEqual(-1); + expect(text.includes(config.ircService.mediaProxy.publicUrl)).toEqual(true); done(); }); @@ -695,10 +695,10 @@ describe("Matrix-to-IRC message bridging", function() { expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); // don't be too brittle when checking this, but I expect to see the - // start of the first line and the mxc fragment + // start of the first line and the media proxy URL expect(text.indexOf(tBody[0])).toEqual(0); expect(text.indexOf(tBody[1])).not.toEqual(0); - expect(text.indexOf('deadbeefcafe')).not.toEqual(-1); + expect(text.includes(config.ircService.mediaProxy.publicUrl)).toEqual(true); done(); }); @@ -716,17 +716,12 @@ describe("Matrix-to-IRC message bridging", function() { it("should bridge matrix images as IRC action with a URL", function(done) { const tBody = "the_image.jpg"; const tMxcSegment = "/somecontentid"; - const tHsUrl = "https://some.home.server.goeshere/"; env.ircMock._whenClient(roomMapping.server, testUser.nick, "action", (client, channel, text) => { expect(client.nick).toEqual(testUser.nick); expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); - // don't be too brittle when checking this, but I expect to see the - // filename (body) and the http url. - expect(text.indexOf(tBody)).not.toEqual(-1); - expect(text.indexOf(tHsUrl)).not.toEqual(-1); - expect(text.indexOf(tMxcSegment)).not.toEqual(-1); + expect(text.includes(config.ircService.mediaProxy.publicUrl)).toEqual(true); done(); }); @@ -745,17 +740,12 @@ describe("Matrix-to-IRC message bridging", function() { it("should bridge matrix files as IRC action with a URL", function(done) { const tBody = "a_file.apk"; const tMxcSegment = "/somecontentid"; - const tHsUrl = "https://some.home.server.goeshere/"; env.ircMock._whenClient(roomMapping.server, testUser.nick, "action", (client, channel, text) => { expect(client.nick).toEqual(testUser.nick); expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); - // don't be too brittle when checking this, but I expect to see the - // filename (body) and the http url. - expect(text.indexOf(tBody)).not.toEqual(-1); - expect(text.indexOf(tHsUrl)).not.toEqual(-1); - expect(text.indexOf(tMxcSegment)).not.toEqual(-1); + expect(text.includes(config.ircService.mediaProxy.publicUrl)).toEqual(true); done(); }); @@ -1087,21 +1077,13 @@ describe("Matrix-to-IRC message bridging with media URL and drop time", function it("should bridge matrix files as IRC action with a configured media URL", function(done) { let tBody = "a_file.apk"; let tMxcSegment = "/somecontentid"; - let tMediaUrl = mediaUrl; - let tHsUrl = "http://somedomain.com"; - const sdk = env.clientMock._client(config._botUserId); env.ircMock._whenClient(roomMapping.server, testUser.nick, "action", function(client, channel, text) { expect(client.nick).toEqual(testUser.nick); expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); - // don't be too brittle when checking this, but I expect to see the - // filename (body) and the http url. - expect(text.indexOf(tBody)).not.toEqual(-1, "File name not present"); - expect(text.indexOf(tHsUrl)).toEqual(-1, "HS URL present instead of media URL"); - expect(text.indexOf(tMediaUrl)).not.toEqual(-1, "No media URL"); - expect(text.indexOf(tMxcSegment)).not.toEqual(-1, "No Mxc segment"); + expect(text.includes(config.ircService.mediaProxy.publicUrl)).toEqual(true); done(); }); diff --git a/spec/support/signingkey.jwk b/spec/support/signingkey.jwk new file mode 100644 index 000000000..783d02f96 --- /dev/null +++ b/spec/support/signingkey.jwk @@ -0,0 +1,10 @@ +{ + "key_ops": [ + "sign", + "verify" + ], + "ext": true, + "kty": "oct", + "k": "1bWvvQPL7rVtRYMmesyR5z5LDDMZCktGgdiDwTtuYZARd_OIDtxUpRZP8zh46uPy06CaPpJQrCl84BzSKpSzMSsbYcvYM1Zn25VsEzH_OYJP2B0if-mAblVDMNoXva2ZobKXMz0dXrxeE0I-fwrORBKOcuZzveqH4VYGaeFPDkk", + "alg": "HS512" +} diff --git a/spec/util/e2e-test.ts b/spec/util/e2e-test.ts index ccf306dfa..5dae30035 100644 --- a/spec/util/e2e-test.ts +++ b/spec/util/e2e-test.ts @@ -242,6 +242,12 @@ export class IrcBridgeE2ETest { ...MatrixHandlerDefaults, shortReplyTresholdSeconds: opts.shortReplyTresholdSeconds ?? 0, }, + "mediaProxy": { + "signingKeyPath": "./spec/support/signingkey.jwk", + "ttlSeconds": 5, + "bindPort": 0, + "publicUrl": "https://irc.bridge/media" + }, servers: { localhost: { ...IrcServer.DEFAULT_CONFIG, diff --git a/spec/util/test-config.json b/spec/util/test-config.json index 870da20e3..883714a58 100644 --- a/spec/util/test-config.json +++ b/spec/util/test-config.json @@ -46,6 +46,12 @@ "matrixHandler": { "ignoreFunctionalMembersInAdminRooms": true }, + "mediaProxy": { + "signingKeyPath": "./spec/support/signingkey.jwk", + "ttlSeconds": 5, + "bindPort": 0, + "publicUrl": "https://irc.bridge/media" + }, "servers": { "irc.example": { "port": 6667, diff --git a/src/bridge/IrcBridge.ts b/src/bridge/IrcBridge.ts index 365711f43..0aa94780f 100644 --- a/src/bridge/IrcBridge.ts +++ b/src/bridge/IrcBridge.ts @@ -1,5 +1,6 @@ import Bluebird from "bluebird"; import extend from "extend"; +import * as fs from "fs"; import * as promiseutil from "../promiseutil"; import { IrcHandler, MatrixMembership } from "./IrcHandler"; import { MatrixHandler, MatrixEventInvite, OnMemberEventData, MatrixEventKick } from "./MatrixHandler"; @@ -41,6 +42,7 @@ import { UserActivityTracker, UserActivityTrackerConfig, WeakStateEvent, + MediaProxy, } from "matrix-appservice-bridge"; import { IrcAction } from "../models/IrcAction"; import { DataStore } from "../datastore/DataStore"; @@ -56,6 +58,7 @@ import { TestingOptions } from "../config/TestOpts"; import { MatrixBanSync } from "./MatrixBanSync"; import { configure } from "../logging"; import { IrcPoolClient } from "../pool-service/IrcPoolClient"; +import { webcrypto } from "node:crypto"; const log = getLogger("IrcBridge"); const DEFAULT_PORT = 8090; @@ -88,6 +91,7 @@ export class IrcBridge { public activityTracker: ActivityTracker|null = null; public readonly roomConfigs: RoomConfig; public readonly matrixBanSyncer?: MatrixBanSync; + private _mediaProxy?: MediaProxy; private clientPool!: ClientPool; // This gets defined in the `run` function private ircServers: IrcServer[] = []; private memberListSyncers: {[domain: string]: MemberListSyncer} = {}; @@ -255,9 +259,10 @@ export class IrcBridge { log.info(`Adjusted dropMatrixMessagesAfterSecs to ${newConfig.homeserver.dropMatrixMessagesAfterSecs}`); } - if (oldConfig.homeserver.media_url !== newConfig.homeserver.media_url) { - oldConfig.homeserver.media_url = newConfig.homeserver.media_url; - log.info(`Adjusted media_url to ${newConfig.homeserver.media_url}`); + if (JSON.stringify(oldConfig.ircService.mediaProxy) !== JSON.stringify(newConfig.ircService.mediaProxy)) { + await this._mediaProxy?.close(); + this._mediaProxy = await this.initialiseMediaProxy(); + log.info(`Media proxy reinitialized`); } await this.setupStateSyncer(newConfig); @@ -308,6 +313,25 @@ export class IrcBridge { await this.clientPool.checkForBannedConnectedUsers(); } + private async initialiseMediaProxy(): Promise { + const config = this.config.ircService.mediaProxy; + const jwk = JSON.parse(fs.readFileSync(config.signingKeyPath, "utf8").toString()); + const signingKey = await webcrypto.subtle.importKey('jwk', jwk, { + name: 'HMAC', + hash: 'SHA-512', + }, true, ['sign', 'verify']); + const publicUrl = new URL(config.publicUrl); + + const mediaProxy = new MediaProxy({ + publicUrl, + signingKey, + ttl: config.ttlSeconds * 1000 + }, this.bridge.getIntent().matrixClient); + await mediaProxy.start(config.bindPort); + + return mediaProxy; + } + private initialiseMetrics(bindPort: number) { const zeroAge = new AgeCounters(); const registry = new Registry(); @@ -523,6 +547,13 @@ export class IrcBridge { return `@${this.registration.getSenderLocalpart()}:${this.domain}`; } + public get mediaProxy(): MediaProxy { + if (!this._mediaProxy) { + throw new Error(`Bridge not yet initialized`); + } + return this._mediaProxy; + } + public getStore() { return this.dataStore; } @@ -654,6 +685,8 @@ export class IrcBridge { await this.dataStore.removeConfigMappings(); + this._mediaProxy = await this.initialiseMediaProxy(); + if (this.activityTracker) { log.info("Restoring last active times from DB"); const users = await this.dataStore.getLastSeenTimeForUsers(); @@ -958,6 +991,16 @@ export class IrcBridge { public async kill(reason?: string) { log.info("Killing bridge"); this.bridgeState = "killed"; + if (this._mediaProxy) { + log.info("Killing media proxy"); + // We don't care about the outcome since we don't really care + // about the state of the bridge after a kill(). + // Awaiting this tripped up the BOTS-70 unit test for reasons I couldn't figure out, + // and I spent a not-worth-it amount of time on it, + // so this is me giving up and techdebting it. + // Please let me know, future programmer, if you do fix this properly. -- tadzik + void this._mediaProxy.close().catch(err => log.warn(`Failed to close MediaProxy: ${err}`)); + } log.info("Killing all clients"); if (!this.config.connectionPool?.persistConnectionsOnShutdown) { this.clientPool.killAllClients(reason); diff --git a/src/bridge/MatrixHandler.ts b/src/bridge/MatrixHandler.ts index 89a0885b7..4e4616732 100644 --- a/src/bridge/MatrixHandler.ts +++ b/src/bridge/MatrixHandler.ts @@ -1,13 +1,13 @@ import { IrcBridge } from "./IrcBridge"; import { BridgeRequest, BridgeRequestErr } from "../models/BridgeRequest"; import { - ContentRepo, MatrixUser, MatrixRoom, MembershipQueue, StateLookup, StateLookupEvent, Intent, + MediaProxy, } from "matrix-appservice-bridge"; import { IrcUser } from "../models/IrcUser"; import { ActionType, MatrixAction, MatrixMessageEvent } from "../models/MatrixAction"; @@ -142,7 +142,6 @@ export class MatrixHandler { private readonly metrics: {[domain: string]: { [metricName: string]: number; };} = {}; - private readonly mediaUrl: string; private memberTracker?: StateLookup; private adminHandler: AdminRoomHandler; private config: MatrixHandlerConfig = DEFAULTS; @@ -155,15 +154,16 @@ export class MatrixHandler { constructor( private readonly ircBridge: IrcBridge, config: MatrixHandlerConfig|undefined, - private readonly membershipQueue: MembershipQueue + private readonly membershipQueue: MembershipQueue, ) { this.onConfigChanged(config); - - // The media URL to use to transform mxc:// URLs when handling m.room.[file|image]s - this.mediaUrl = ircBridge.config.homeserver.media_url || ircBridge.config.homeserver.url; this.adminHandler = new AdminRoomHandler(ircBridge, this); } + private get mediaProxy(): MediaProxy { + return this.ircBridge.mediaProxy; + } + public initialise() { this.memberTracker = new StateLookup({ intent: this.ircBridge.getAppServiceBridge().getIntent(), @@ -909,9 +909,7 @@ export class MatrixHandler { req.log.debug("Message body: %s", event.content.body); } - const mxAction = MatrixAction.fromEvent( - event, this.mediaUrl - ); + const mxAction = await MatrixAction.fromEvent(event, this.mediaProxy); // check if this message is from one of our virtual users const servers = this.ircBridge.getServers(); @@ -1171,7 +1169,7 @@ export class MatrixHandler { // This is true if the upload was a success if (contentUri) { - const httpUrl = ContentRepo.getHttpUriForMxc(this.mediaUrl, contentUri); + const httpUrl = await this.mediaProxy.generateMediaUrl(contentUri); // we check event.content.body since ircAction already has the markers stripped const codeBlockMatch = event.content.body.match(/^```(\w+)?/); if (codeBlockMatch) { @@ -1182,7 +1180,7 @@ export class MatrixHandler { }; } else { - const explanation = renderTemplate(this.config.truncatedMessageTemplate, { url: httpUrl }); + const explanation = renderTemplate(this.config.truncatedMessageTemplate, { url: httpUrl.toString() }); let messagePreview = trimString( potentialMessages[0], ircClient.getMaxLineLength() - 4 /* "... " */ - explanation.length - ircRoom.channel.length @@ -1198,7 +1196,7 @@ export class MatrixHandler { } const truncatedIrcAction = IrcAction.fromMatrixAction( - MatrixAction.fromEvent(event, this.mediaUrl) + await MatrixAction.fromEvent(event, this.mediaProxy) ); if (truncatedIrcAction) { await this.ircBridge.sendIrcAction(ircRoom, ircClient, truncatedIrcAction); @@ -1220,9 +1218,9 @@ export class MatrixHandler { // Recreate action from modified event const truncatedIrcAction = IrcAction.fromMatrixAction( - MatrixAction.fromEvent( + await MatrixAction.fromEvent( sendingEvent, - this.mediaUrl, + this.mediaProxy, ) ); if (truncatedIrcAction) { diff --git a/src/config/BridgeConfig.ts b/src/config/BridgeConfig.ts index 0ae31069a..a6e7f905b 100644 --- a/src/config/BridgeConfig.ts +++ b/src/config/BridgeConfig.ts @@ -13,7 +13,6 @@ export interface BridgeConfig { }; homeserver: { url: string; - media_url?: string; domain: string; enablePresence?: boolean; dropMatrixMessagesAfterSecs?: number; @@ -23,6 +22,12 @@ export interface BridgeConfig { ircService: { servers: {[domain: string]: IrcServerConfig}; matrixHandler?: MatrixHandlerConfig; + mediaProxy: { + signingKeyPath: string; + ttlSeconds: number; + bindPort: number; + publicUrl: string; + }; ircHandler?: IrcHandlerConfig; provisioning: ProvisionerConfig; logging: LoggerConfig; diff --git a/src/generate-signing-key.js b/src/generate-signing-key.js new file mode 100644 index 000000000..cb032b43c --- /dev/null +++ b/src/generate-signing-key.js @@ -0,0 +1,11 @@ +const webcrypto = require('node:crypto'); + +async function main() { + const key = await webcrypto.subtle.generateKey({ + name: 'HMAC', + hash: 'SHA-512', + }, true, ['sign', 'verify']); + console.log(JSON.stringify(await webcrypto.subtle.exportKey('jwk', key), undefined, 4)); +} + +main().then(() => process.exit(0)).catch(err => { throw err }); diff --git a/src/models/MatrixAction.ts b/src/models/MatrixAction.ts index e213aeb46..6f211b45d 100644 --- a/src/models/MatrixAction.ts +++ b/src/models/MatrixAction.ts @@ -17,7 +17,7 @@ limitations under the License. import { IrcAction } from "./IrcAction"; import ircFormatting = require("../irc/formatting"); -import { ContentRepo, Intent } from "matrix-appservice-bridge"; +import { Intent, MediaProxy } from "matrix-appservice-bridge"; import escapeStringRegexp from "escape-string-regexp"; import logging from "../logging"; const log = logging("MatrixAction"); @@ -110,6 +110,7 @@ export class MatrixAction { public htmlText: string|null = null, public readonly ts: number = 0, public replyEvent?: string, + private mediaProxy?: MediaProxy, ) { } public get msgType() { @@ -183,7 +184,7 @@ export class MatrixAction { } } - public static fromEvent(event: MatrixMessageEvent, mediaUrl: string, filename?: string) { + public static async fromEvent(event: MatrixMessageEvent, mediaProxy: MediaProxy, filename?: string) { event.content = event.content || {}; let type = EVENT_TO_TYPE[event.type] || "message"; // mx event type to action type let text = event.content.body; @@ -212,14 +213,13 @@ export class MatrixAction { fileSize = "(" + Math.round(event.content.info.size / 1024) + "KiB)"; } - let url = ContentRepo.getHttpUriForMxc(mediaUrl, event.content.url); + const url = await mediaProxy.generateMediaUrl(event.content.url); if (!filename && event.content.body && /\S*\.[\w\d]{2,4}$/.test(event.content.body)) { // Add filename to url if body is a filename. filename = event.content.body; } if (filename) { - url += `/${encodeURIComponent(filename)}`; text = `${fileSize} < ${url} >`; } else { diff --git a/yarn.lock b/yarn.lock index 62abf1486..2e241cc33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1406,7 +1406,7 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vector-im/matrix-bot-sdk@^0.7.0-element.0", "matrix-bot-sdk@npm:@vector-im/matrix-bot-sdk@^0.7.0-element.1": +"@vector-im/matrix-bot-sdk@^0.7.0-element.0": version "0.7.0-element.1" resolved "https://registry.yarnpkg.com/@vector-im/matrix-bot-sdk/-/matrix-bot-sdk-0.7.0-element.1.tgz#94704e6abda3e5098d40e5b626ab7288645bb090" integrity sha512-klP0F9YQZyjuvWOtfHnJ4ftBOzQ9jn0TSTSHv/UuGUnQuvWeSQytZAu1zLgHHQ9fs7Li16QwpuTPQ0/MNre+lw== @@ -4677,10 +4677,10 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -matrix-appservice-bridge@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/matrix-appservice-bridge/-/matrix-appservice-bridge-10.1.0.tgz#8d23bb4c671822684e3121224f1df339f0615b9d" - integrity sha512-WiyovdQv3WfguffCTTycZ+tM+gwc4DvxSS6em0q5kjdCYiw+ogXezTTkUZZ2QBsIqmgVzF96UQC8PuFS7iWoBA== +matrix-appservice-bridge@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/matrix-appservice-bridge/-/matrix-appservice-bridge-10.2.0.tgz#79b6f53593ab21cf6211a3ba1051a0439b2f2824" + integrity sha512-GYpIBPgQnc0/p93KoqzrOYwh6JJEWZc+t9N0GxMO7EXb0IEkfKMji6lOOEqk9QdNs2+fpMBKIS2mjT3X5iuTUg== dependencies: "@alloc/quick-lru" "^5.2.0" "@types/nedb" "^1.8.16" @@ -4711,6 +4711,30 @@ matrix-appservice@^2.0.0: js-yaml "^4.1.0" morgan "^1.10.0" +"matrix-bot-sdk@npm:@vector-im/matrix-bot-sdk@^0.7.0-element.1": + version "0.7.0-element.1" + resolved "https://registry.yarnpkg.com/@vector-im/matrix-bot-sdk/-/matrix-bot-sdk-0.7.0-element.1.tgz#94704e6abda3e5098d40e5b626ab7288645bb090" + integrity sha512-klP0F9YQZyjuvWOtfHnJ4ftBOzQ9jn0TSTSHv/UuGUnQuvWeSQytZAu1zLgHHQ9fs7Li16QwpuTPQ0/MNre+lw== + dependencies: + "@matrix-org/matrix-sdk-crypto-nodejs" "0.1.0-beta.11" + "@types/express" "^4.17.21" + another-json "^0.2.0" + async-lock "^1.4.0" + chalk "4" + express "^4.18.2" + glob-to-regexp "^0.4.1" + hash.js "^1.1.7" + html-to-text "^9.0.5" + htmlencode "^0.0.4" + lowdb "1" + lru-cache "^10.0.1" + mkdirp "^3.0.1" + morgan "^1.10.0" + postgres "^3.4.1" + request "^2.88.2" + request-promise "^4.2.6" + sanitize-html "^2.11.0" + matrix-org-irc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matrix-org-irc/-/matrix-org-irc-3.0.0.tgz#de7c23cc6b69e12574704bd4aa43a4d89976d589" @@ -6216,7 +6240,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6284,7 +6317,14 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6895,7 +6935,7 @@ winston@^3.8.2: triple-beam "^1.3.0" winston-transport "^4.7.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -6913,6 +6953,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"