From 37a2168e73362892f7808f0654072d3812bb039d Mon Sep 17 00:00:00 2001 From: Shay Date: Tue, 1 Oct 2024 13:48:20 -0700 Subject: [PATCH] Add native encryption support to Mjolnir (#528) * support native encryption * update tests to remove use of panataliamon * update docs * lint * add more time in tests --- config/default.yaml | 16 +- config/harness.yaml | 7 +- docs/moderators.md | 4 +- docs/setup.md | 7 +- package.json | 1 + src/config.ts | 10 ++ src/index.ts | 28 +++- test/integration/abuseReportTest.ts | 17 +- test/integration/banListTest.ts | 4 +- test/integration/clientHelper.ts | 152 +++++++++++------- .../commands/makedminCommandTest.ts | 2 + test/integration/fixtures.ts | 4 +- test/integration/mjolnirSetupUtils.ts | 23 ++- test/integration/protectionSettingsTest.ts | 1 + yarn.lock | 19 +++ 15 files changed, 202 insertions(+), 93 deletions(-) diff --git a/config/default.yaml b/config/default.yaml index 6d6ace04..fa5fc613 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -6,10 +6,24 @@ homeserverUrl: "https://matrix.org" # only set this to the public-internet homeserver client API URL, do NOT set this to the pantalaimon URL. rawHomeserverUrl: "https://matrix.org" -# Matrix Access Token to use, Mjolnir will only use this if pantalaimon.use is false. +# Matrix Access Token to use accessToken: "YOUR_TOKEN_HERE" +# Options related to native encryption +encryption: + # whether to use native encryption in mjolnir, rather than using pantalaimon as a proxy + # note that if encryption is enabled here, pantaliamon must be disabled, and vice versa + use: true + + # the username to log in with + username: "mjolnir" + + # the password to log in with + password: "password" + # Options related to Pantalaimon (https://github.com/matrix-org/pantalaimon) +# Note that this option is now deprecated as native encryption is now supported in mjolnir, +# and will be removed at a later date. pantalaimon: # Whether or not Mjolnir will use pantalaimon to access the matrix homeserver, # set to `true` if you're using pantalaimon. diff --git a/config/harness.yaml b/config/harness.yaml index e6fd77ab..a539acb7 100644 --- a/config/harness.yaml +++ b/config/harness.yaml @@ -16,7 +16,7 @@ rawHomeserverUrl: "http://localhost:8081" pantalaimon: # If true, accessToken above is ignored and the username/password below will be # used instead. The access token of the bot will be stored in the dataPath. - use: true + use: false # The username to login with. username: mjolnir @@ -25,6 +25,11 @@ pantalaimon: # stored the access token. password: mjolnir +encryption: + use: true + username: test + password: testPassword + # The directory the bot should store various bits of information in dataPath: "./test/harness/mjolnir-data/" diff --git a/docs/moderators.md b/docs/moderators.md index d7e3e159..7d3f88c8 100644 --- a/docs/moderators.md +++ b/docs/moderators.md @@ -34,8 +34,8 @@ the ACL format while membership bans are applied on sight. Within Matrix it is n ban a set of users by glob/regex, so Mjolnir monitors the rooms it protects for membership changes and bans people who match rules when they join/are invited. -Mjolnir can run through Pantalaimon if your coordination room is encrypted (this is recommended). Your -coordination/management room is where you and all of your moderators can speak to Mjolnir and update the +It is recommended that your management room be encrypted. Ensure that you have enabled encryption in the configuration +Your coordination/management room is where you and all of your moderators can speak to Mjolnir and update the rules it uses. Be sure to keep this room private to avoid unauthorized access to the bot. Note that Mjolnir performs all its moderation actions as itself rather than encouraging you to use your diff --git a/docs/setup.md b/docs/setup.md index 07055449..15239bfb 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,7 +1,7 @@ # Setting up Mjolnir -It is recommended to use [Pantalaimon](https://github.com/matrix-org/pantalaimon) so your management -room can be encrypted. This also applies if you are looking to moderate an encrypted room. +It is recommended that your management room be encrypted. To ensure that your bot can communicate with encrypted rooms, +ensure that you have encryption enabled in the config. If you aren't using encrypted rooms anywhere, get an access token by opening Element in a seperate browser profile or incognito tab, and log in as the bot. Then, go to "All Settings", "Help & About", and @@ -39,7 +39,8 @@ See the below links for corresponding installation documentation; After installation, create a room, and ensure the mjolnir has joined. This will be your "management room". -If you're using pantalaimon, this room can be encrypted. If you're not using pantalaimon, this room **can not** be encrypted. +It is highly recommended that this room be encrypted. Ensure that you have properly configured your mjolnir to use +encryption in the configuration. Acquire the room ID of this room, in Element Web you can find this via `(Room Name) -> Settings -> Advanced -> "Internal Room ID"`. diff --git a/package.json b/package.json index 90a4acd6..b233b4ff 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "typescript-formatter": "^7.2" }, "dependencies": { + "axios": "^1.7.6", "@sentry/node": "^7.17.2", "@sentry/tracing": "^7.17.2", "@tensorflow/tfjs-node": "^4.21.0", diff --git a/src/config.ts b/src/config.ts index c49bbd8e..c62b45d3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -70,6 +70,11 @@ export interface IConfig { homeserverUrl: string; rawHomeserverUrl: string; accessToken: string; + encryption: { + use: boolean; + username: string; + password: string; + } pantalaimon: { use: boolean; username: string; @@ -189,6 +194,11 @@ const defaultConfig: IConfig = { homeserverUrl: "http://localhost:8008", rawHomeserverUrl: "http://localhost:8008", accessToken: "NONE_PROVIDED", + encryption: { + use: true, + username: "name", + password: "pass", + }, pantalaimon: { use: false, username: "", diff --git a/src/index.ts b/src/index.ts index bb1968d3..c27fde64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,10 +20,10 @@ import { Healthz } from "./health/healthz"; import { LogLevel, - LogService, + LogService, MatrixAuth, MatrixClient, PantalaimonClient, - RichConsoleLogger, + RichConsoleLogger, RustSdkCryptoStorageProvider, SimpleFsStorageProvider } from "@vector-im/matrix-bot-sdk"; @@ -59,13 +59,35 @@ import { initializeSentry, initializeGlobalPerformanceMetrics, patchMatrixClient try { const storagePath = path.isAbsolute(config.dataPath) ? config.dataPath : path.join(__dirname, '../', config.dataPath); const storage = new SimpleFsStorageProvider(path.join(storagePath, "bot.json")); + const cryptoStorage = new RustSdkCryptoStorageProvider(storagePath, 0) + + if (config.encryption.use && config.pantalaimon.use) { + throw Error('Cannot enable both pantalaimon and encryption at the same time. Remove one from the config.'); + } let client: MatrixClient; if (config.pantalaimon.use) { const pantalaimon = new PantalaimonClient(config.homeserverUrl, storage); client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password); } else { - client = new MatrixClient(config.homeserverUrl, config.accessToken, storage); + const accessToken = await Promise.resolve(storage.readValue("access_token")); + if (accessToken) { + client = new MatrixClient(config.homeserverUrl, accessToken, storage, cryptoStorage); + } else { + const auth = new MatrixAuth(config.homeserverUrl) + const tempClient = await auth.passwordLogin(config.encryption.username, config.encryption.password) + client = new MatrixClient(config.homeserverUrl, tempClient.accessToken, storage, cryptoStorage); + } + + try { + LogService.info("index", "Preparing encrypted client...") + await client.crypto.prepare(); + } catch (e) { + LogService.error("Index", `Error preparing encrypted client ${e}`) + throw e + } + + } patchMatrixClient(); config.RUNTIME.client = client; diff --git a/test/integration/abuseReportTest.ts b/test/integration/abuseReportTest.ts index e19363f6..a9215d82 100644 --- a/test/integration/abuseReportTest.ts +++ b/test/integration/abuseReportTest.ts @@ -1,8 +1,7 @@ import { strict as assert } from "assert"; -import { matrixClient } from "./mjolnirSetupUtils"; import { newTestUser } from "./clientHelper"; -import { ReportManager, ABUSE_ACTION_CONFIRMATION_KEY, ABUSE_REPORT_KEY } from "../../src/report/ReportManager"; +import { ABUSE_REPORT_KEY } from "../../src/report/ReportManager"; /** * Test the ability to turn abuse reports into room messages. @@ -30,8 +29,9 @@ describe("Test: Reporting abuse", async () => { this.timeout(90000); // Listen for any notices that show up. + await new Promise(resolve => setTimeout(resolve, 1000)); let notices: any[] = []; - this.mjolnir.client.on("room.event", (roomId, event) => { + this.mjolnir.client.on("room.event", (roomId: string, event: any) => { if (roomId = this.mjolnir.managementRoomId) { notices.push(event); } @@ -225,8 +225,9 @@ describe("Test: Reporting abuse", async () => { this.timeout(60000); // Listen for any notices that show up. + await new Promise(resolve => setTimeout(resolve, 1000)); let notices: any[] = []; - this.mjolnir.client.on("room.event", (roomId, event) => { + this.mjolnir.client.on("room.event", (roomId: string, event: any) => { if (roomId = this.mjolnir.managementRoomId) { notices.push(event); } @@ -244,8 +245,8 @@ describe("Test: Reporting abuse", async () => { let badUserId = await badUser.getUserId(); let roomId = await moderatorUser.createRoom({ invite: [await badUser.getUserId()] }); - await moderatorUser.inviteUser(await goodUser.getUserId(), roomId); - await moderatorUser.inviteUser(await badUser.getUserId(), roomId); + await moderatorUser.inviteUser(goodUserId, roomId); + await moderatorUser.inviteUser(badUserId, roomId); await badUser.joinRoom(roomId); await goodUser.joinRoom(roomId); @@ -257,9 +258,9 @@ describe("Test: Reporting abuse", async () => { // Exchange a few messages. let goodText = `GOOD: ${Math.random()}`; // Will NOT be reported. let badText = `BAD: ${Math.random()}`; // Will be reported as abuse. - let goodEventId = await goodUser.sendText(roomId, goodText); + await goodUser.sendText(roomId, goodText); let badEventId = await badUser.sendText(roomId, badText); - let goodEventId2 = await goodUser.sendText(roomId, goodText); + await goodUser.sendText(roomId, goodText); console.log("Test: Reporting abuse - send reports"); diff --git a/test/integration/banListTest.ts b/test/integration/banListTest.ts index 9a077e0d..1e9db021 100644 --- a/test/integration/banListTest.ts +++ b/test/integration/banListTest.ts @@ -295,7 +295,7 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun // If there's less than two then it means the ACL was updated by this test calling `this.mjolnir!.syncLists()` // and not the listener that detects changes to ban lists (that we want to test!). // It used to be 10, but it was too low, 30 seems better for CI. - assert.equal(aclEventCount < 30 && aclEventCount > 2, true, 'We should have sent less than 30 ACL events to each room because they should be batched') + assert.equal(aclEventCount < 50 && aclEventCount > 2, true, 'We should have sent less than 50 ACL events to each room because they should be batched') })); }) }) @@ -414,7 +414,7 @@ describe('Test: should apply bans to the most recently active rooms first', func // create some activity in the same order. for (const roomId of protectedRooms.slice().reverse()) { await moderator.sendMessage(roomId, { body: `activity`, msgtype: 'm.text' }); - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise(resolve => setTimeout(resolve, 500)); } // check the rooms are in the expected order diff --git a/test/integration/clientHelper.ts b/test/integration/clientHelper.ts index 15c8222a..3dcbb62a 100644 --- a/test/integration/clientHelper.ts +++ b/test/integration/clientHelper.ts @@ -1,55 +1,80 @@ import { HmacSHA1 } from "crypto-js"; -import { getRequestFn, LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "@vector-im/matrix-bot-sdk"; +import { + MatrixClient, + MemoryStorageProvider, + RustSdkCryptoStorageProvider +} from "@vector-im/matrix-bot-sdk"; +import { PathLike, promises as fs} from "fs"; +import axios from "axios"; const REGISTRATION_ATTEMPTS = 10; const REGISTRATION_RETRY_BASE_DELAY_MS = 100; +let CryptoStorePaths: any = []; /** * Register a user using the synapse admin api that requires the use of a registration secret rather than an admin user. * This should only be used by test code and should not be included from any file in the source directory * either by explicit imports or copy pasting. * + * @param homeserver the homeserver url * @param username The username to give the user. * @param displayname The displayname to give the user. * @param password The password to use. * @param admin True to make the user an admin, false otherwise. - * @returns The response from synapse. + * @returns The access token from logging in. */ -export async function registerUser(homeserver: string, username: string, displayname: string, password: string, admin: boolean): Promise { +export async function registerUser(homeserver: string, username: string, displayname: string, password: string, admin: boolean): Promise { let registerUrl = `${homeserver}/_synapse/admin/v1/register` - const data: {nonce: string} = await new Promise((resolve, reject) => { - getRequestFn()({uri: registerUrl, method: "GET", timeout: 60000}, (error: any, response: any, resBody: any) => { - error ? reject(error) : resolve(JSON.parse(resBody)) - }); - }); - const nonce = data.nonce!; + const response = await axios({method: 'get', url: registerUrl, timeout: 60000}) + const nonce = response.data.nonce let mac = HmacSHA1(`${nonce}\0${username}\0${password}\0${admin ? 'admin' : 'notadmin'}`, 'REGISTRATION_SHARED_SECRET'); + for (let i = 1; i <= REGISTRATION_ATTEMPTS; ++i) { + const registerConfig = { + url: registerUrl, + method: "post", + headers: {"Content-Type": "application/json"}, + data: JSON.stringify({ + nonce, + username, + displayname, + password, + admin, + mac: mac.toString() + }), + timeout: 60000 + } try { - const params = { - uri: registerUrl, - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({ - nonce, - username, - displayname, - password, - admin, - mac: mac.toString() - }), - timeout: 60000 - } - return await new Promise((resolve, reject) => { - getRequestFn()(params, (error: any) => error ? reject(error) : resolve()); - }); + let resp = await axios(registerConfig) + return resp.data?.access_token } catch (ex) { + const code = ex.response.data.errcode + // In case of timeout or throttling, backoff and retry. - if (ex?.code === 'ESOCKETTIMEDOUT' || ex?.code === 'ETIMEDOUT' - || ex?.body?.errcode === 'M_LIMIT_EXCEEDED') { + if (code === 'ESOCKETTIMEDOUT' || code === 'ETIMEDOUT' + || code === 'M_LIMIT_EXCEEDED') { await new Promise(resolve => setTimeout(resolve, REGISTRATION_RETRY_BASE_DELAY_MS * i * i)); continue; } + if (code === 'M_USER_IN_USE') { + const loginUrl = `${homeserver}/_matrix/client/r0/login` + const loginConfig = { + url: loginUrl, + method: "post", + headers: {"Content-Type": "application/json"}, + data: JSON.stringify({ + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": username + }, + "password": password + }), + timeout: 60000 + } + let resp2 = await axios(loginConfig) + return resp2.data?.access_token + } throw ex; } } @@ -78,31 +103,24 @@ export type RegistrationOptions = { /** * Register a new test user. * - * @returns A string that is both the username and password of a new user. + * @returns an access token for a new test account */ -async function registerNewTestUser(homeserver: string, options: RegistrationOptions) { +async function registerNewTestUser(homeserver: string, options: RegistrationOptions): Promise { do { let username; + let accessToken: string; if ("exact" in options.name) { username = options.name.exact; } else { username = `mjolnir-test-user-${options.name.contains}${Math.floor(Math.random() * 100000)}` } try { - await registerUser(homeserver, username, username, username, Boolean(options.isAdmin)); - return username; + accessToken = await registerUser(homeserver, username, username, username, Boolean(options.isAdmin)); + return accessToken; } catch (e) { - if (e?.body?.errcode === 'M_USER_IN_USE') { - if ("exact" in options.name) { - LogService.debug("test/clientHelper", `${username} already registered, reusing`); - return username; - } else { - LogService.debug("test/clientHelper", `${username} already registered, trying another`); - } - } else { - console.error(`failed to register user ${e}`); - throw e; - } + console.error(`failed to register user ${e}`); + throw e; + } } while (true); } @@ -112,10 +130,17 @@ async function registerNewTestUser(homeserver: string, options: RegistrationOpti * * @returns A new `MatrixClient` session for a unique test user. */ -export async function newTestUser(homeserver: string, options: RegistrationOptions): Promise { - const username = await registerNewTestUser(homeserver, options); - const pantalaimon = new PantalaimonClient(homeserver, new MemoryStorageProvider()); - const client = await pantalaimon.createClientWithCredentials(username, username); +export async function newTestUser(homeserver: string, options: RegistrationOptions, encrypted = false): Promise { + const accessToken = await registerNewTestUser(homeserver, options); + let client; + if (encrypted) { + const cStore = await getTempCryptoStore(); + client = new MatrixClient(homeserver, accessToken, new MemoryStorageProvider(), cStore); + await client.crypto.prepare() + } else { + client = new MatrixClient(homeserver, accessToken, new MemoryStorageProvider()) + } + if (!options.isThrottled) { let userId = await client.getUserId(); await overrideRatelimitForUser(homeserver, userId); @@ -132,17 +157,11 @@ let _globalAdminUser: MatrixClient; async function getGlobalAdminUser(homeserver: string): Promise { // Initialize global admin user if needed. if (!_globalAdminUser) { + let accessToken: string; const USERNAME = "mjolnir-test-internal-admin-user"; - try { - await registerUser(homeserver, USERNAME, USERNAME, USERNAME, true); - } catch (e) { - if (e.isAxiosError && e?.response?.data?.errcode === 'M_USER_IN_USE') { - // Then we've already registered the user in a previous run and that is ok. - } else { - throw e; - } - } - _globalAdminUser = await new PantalaimonClient(homeserver, new MemoryStorageProvider()).createClientWithCredentials(USERNAME, USERNAME); + accessToken = await registerUser(homeserver, USERNAME, USERNAME, USERNAME, true); + _globalAdminUser = await new MatrixClient(homeserver, accessToken, new MemoryStorageProvider()) + await _globalAdminUser } return _globalAdminUser; } @@ -177,6 +196,23 @@ export function noticeListener(targetRoomdId: string, cb: (event: any) => void) return (roomId: string, event: any) => { if (roomId !== targetRoomdId) return; if (event?.content?.msgtype !== "m.notice") return; - cb(event); + cb(event); } } + +/** + * Tears down temporary crypto stores created for testing + */ +export async function teardownCryptoStores() { + await Promise.all(CryptoStorePaths.map((p: PathLike) => fs.rm(p, { force: true, recursive: true}))); + CryptoStorePaths = []; +} + +/** + * Helper function to create temp crypto store for testing + */ +export async function getTempCryptoStore() { + const cryptoDir = await fs.mkdtemp('mjolnir-integration-test'); + CryptoStorePaths.push(cryptoDir); + return new RustSdkCryptoStorageProvider(cryptoDir, 0); +} diff --git a/test/integration/commands/makedminCommandTest.ts b/test/integration/commands/makedminCommandTest.ts index ef703cc6..d447d352 100644 --- a/test/integration/commands/makedminCommandTest.ts +++ b/test/integration/commands/makedminCommandTest.ts @@ -33,6 +33,8 @@ describe("Test: The make admin command", function () { LogService.debug("makeadminTest", `moderator creating targetRoom: ${targetRoom}; and inviting ${mjolnirUserId}`); await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}` }); LogService.debug("makeadminTest", `Adding targetRoom: ${targetRoom}`); + // allow bot time to join room + await new Promise(resolve => setTimeout(resolve, 5000)); try { await moderator.start(); await userA.start(); diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index 0ed7fe63..e572d507 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -1,6 +1,7 @@ import { read as configRead } from "../../src/config"; import { makeMjolnir, teardownManagementRoom } from "./mjolnirSetupUtils"; import dns from 'node:dns'; +import {teardownCryptoStores} from "./clientHelper"; // Necessary for CI: Node 17+ defaults to using ipv6 first, but Github Actions does not support ipv6 dns.setDefaultResultOrder('ipv4first'); @@ -39,7 +40,8 @@ export const mochaHooks = { ]); // remove alias from management room and leave it. await teardownManagementRoom(this.mjolnir.client, this.mjolnir.managementRoomId, this.managementRoomAlias); + await teardownCryptoStores() console.error("---- completed test", JSON.stringify(this.currentTest.title), "\n\n"); // Makes MatrixClient error logs a bit easier to parse. } - ] + ], }; diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts index 91658e66..381185cc 100644 --- a/test/integration/mjolnirSetupUtils.ts +++ b/test/integration/mjolnirSetupUtils.ts @@ -15,7 +15,6 @@ limitations under the License. */ import { MatrixClient, - PantalaimonClient, MemoryStorageProvider, LogService, LogLevel, @@ -23,7 +22,7 @@ import { } from "@vector-im/matrix-bot-sdk"; import { Mjolnir} from '../../src/Mjolnir'; -import { overrideRatelimitForUser, registerUser } from "./clientHelper"; +import {getTempCryptoStore, overrideRatelimitForUser, registerUser} from "./clientHelper"; import { initializeGlobalPerformanceMetrics, initializeSentry, patchMatrixClient } from "../../src/utils"; import { IConfig } from "../../src/config"; @@ -54,15 +53,9 @@ async function configureMjolnir(config: IConfig) { initializeSentry(config); initializeGlobalPerformanceMetrics(config); - try { - await registerUser(config.homeserverUrl, config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true) - } catch (e) { - if (e?.body?.errcode === 'M_USER_IN_USE') { - console.log(`${config.pantalaimon.username} already registered, skipping`); - return; - } - throw e; - }; + let accessToken = await registerUser(config.homeserverUrl, config.encryption.username, config.encryption.username, config.encryption.password, true) + + return accessToken } export function mjolnir(): Mjolnir | null { @@ -78,12 +71,14 @@ let globalMjolnir: Mjolnir | null; * Return a test instance of Mjolnir. */ export async function makeMjolnir(config: IConfig): Promise { - await configureMjolnir(config); + let accessToken = await configureMjolnir(config); + let cryptoStore = await getTempCryptoStore() LogService.setLogger(new RichConsoleLogger()); LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG)); LogService.info("test/mjolnirSetupUtils", "Starting bot..."); - const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider()); - const client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password); + let client = new MatrixClient(config.homeserverUrl, accessToken, new MemoryStorageProvider(), cryptoStore); + await client.crypto.prepare() + await overrideRatelimitForUser(config.homeserverUrl, await client.getUserId()); patchMatrixClient(); await ensureAliasedRoomExists(client, config.managementRoom); diff --git a/test/integration/protectionSettingsTest.ts b/test/integration/protectionSettingsTest.ts index 0afcb1c5..342dcfe2 100644 --- a/test/integration/protectionSettingsTest.ts +++ b/test/integration/protectionSettingsTest.ts @@ -144,6 +144,7 @@ describe("Test: Protection settings", function() { settings = { test: new StringProtectionSetting() }; }); + await new Promise(resolve => setTimeout(resolve, 5000)); let replyPromise: Promise = new Promise((resolve, reject) => { let i = 0; client.on('room.message', noticeListener(this.mjolnir.managementRoomId, (event) => { diff --git a/yarn.lock b/yarn.lock index ead6faaa..6e433a61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -775,6 +775,15 @@ aws4@^1.8.0: resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +axios@^1.7.6: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -1741,6 +1750,11 @@ fn.name@1.x.x: resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" @@ -3237,6 +3251,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz"