diff --git a/docker/.env.example b/docker/.env.example index 6368a1900b..97cf22ca2d 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -235,4 +235,10 @@ GID='1000' # AGENT_SERPER_DEV_KEY= #------ Bing Search ----------- https://portal.azure.com/ -# AGENT_BING_SEARCH_API_KEY= \ No newline at end of file +# AGENT_BING_SEARCH_API_KEY= + +########################################### +######## SOCIAL PROVIDERS KEYS ############### +########################################### + +# GOOGLE_AUTH_CLIENT_ID= diff --git a/frontend/package.json b/frontend/package.json index 11e612fcdf..800075a1ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@metamask/jazzicon": "^2.0.0", "@microsoft/fetch-event-source": "^2.0.1", "@phosphor-icons/react": "^2.0.13", + "@react-oauth/google": "^0.12.1", "@tremor/react": "^3.15.1", "dompurify": "^3.0.8", "file-saver": "^2.0.5", diff --git a/frontend/src/AuthContext.jsx b/frontend/src/AuthContext.jsx index 34ec0cff7e..27d00b3285 100644 --- a/frontend/src/AuthContext.jsx +++ b/frontend/src/AuthContext.jsx @@ -1,10 +1,13 @@ -import React, { useState, createContext } from "react"; +import React, { useState, createContext, useEffect } from "react"; import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants"; +import { GoogleOAuthProvider } from "@react-oauth/google"; +import System from "./models/system"; export const AuthContext = createContext(null); export function ContextWrapper(props) { const localUser = localStorage.getItem(AUTH_USER); const localAuthToken = localStorage.getItem(AUTH_TOKEN); + const [googleAuthClientId, setGoogleAuthClientId] = useState(null); const [store, setStore] = useState({ user: localUser ? JSON.parse(localUser) : null, authToken: localAuthToken ? localAuthToken : null, @@ -24,9 +27,19 @@ export function ContextWrapper(props) { }, }); + useEffect(() => { + async function fetchSettings() { + const _settings = await System.keys(); + setGoogleAuthClientId(_settings?.GoogleAuthClientId); + } + fetchSettings(); + }, []); + return ( - - {props.children} - + + + {props.children} + + ); } diff --git a/frontend/src/components/Modals/Password/MultiUserAuth.jsx b/frontend/src/components/Modals/Password/MultiUserAuth.jsx index 04625b9507..b33394a9a0 100644 --- a/frontend/src/components/Modals/Password/MultiUserAuth.jsx +++ b/frontend/src/components/Modals/Password/MultiUserAuth.jsx @@ -6,6 +6,7 @@ import showToast from "@/utils/toast"; import ModalWrapper from "@/components/ModalWrapper"; import { useModal } from "@/hooks/useModal"; import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal"; +import SocialProviders from "./SocialProviders"; const RecoveryForm = ({ onSubmit, setShowRecoveryForm }) => { const [username, setUsername] = useState(""); @@ -275,7 +276,7 @@ export default function MultiUserAuth() { <>
-
+

@@ -290,6 +291,12 @@ export default function MultiUserAuth() {

+
diff --git a/frontend/src/components/Modals/Password/SocialProviders.jsx b/frontend/src/components/Modals/Password/SocialProviders.jsx new file mode 100644 index 0000000000..c8e20d832e --- /dev/null +++ b/frontend/src/components/Modals/Password/SocialProviders.jsx @@ -0,0 +1,63 @@ +import System from "@/models/system"; +import { AUTH_TOKEN, AUTH_USER } from "@/utils/constants"; +import paths from "@/utils/paths"; +import { GoogleLogin } from "@react-oauth/google"; +import { useEffect, useState } from "react"; + +export default function SocialProviders({ + setError, + setLoading, + setUser, + setToken, +}) { + const [settings, setSettings] = useState(null); + + useEffect(() => { + async function fetchSettings() { + const _settings = await System.keys(); + setSettings(_settings); + } + fetchSettings(); + }, []); + + const handleGoogleLogin = async (data) => { + setError(null); + setLoading(true); + const { valid, user, token, message } = await System.socialLogin( + data, + "google" + ); + if (valid && !!token && !!user) { + setUser(user); + setToken(token); + + window.localStorage.setItem(AUTH_USER, JSON.stringify(user)); + window.localStorage.setItem(AUTH_TOKEN, token); + window.location = paths.home(); + } else { + setError(message); + setLoading(false); + } + setLoading(false); + }; + + return ( + <> + {settings?.GoogleAuthClientId && ( + <> + { + handleGoogleLogin(credentialResponse); + }} + onError={() => { + setError("Something went wrong"); + }} + theme={"filled_black"} + /> + {/* Add here other social providers */} +

or

+ + )} + + ); +} diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index d2252be16f..93370ff60f 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -78,6 +78,20 @@ const System = { return { valid: false, message: e.message }; }); }, + socialLogin: async function (body, provider) { + return await fetch(`${API_BASE}/social-login/${provider}`, { + method: "POST", + body: JSON.stringify({ ...body }), + }) + .then((res) => { + if (!res.ok) throw new Error("Could not validate login."); + return res.json(); + }) + .then((res) => res) + .catch((e) => { + return { valid: false, message: e.message }; + }); + }, recoverAccount: async function (username, recoveryCodes) { return await fetch(`${API_BASE}/system/recover-account`, { method: "POST", diff --git a/frontend/src/pages/Admin/System/index.jsx b/frontend/src/pages/Admin/System/index.jsx index bdab765a5a..57c590f6ad 100644 --- a/frontend/src/pages/Admin/System/index.jsx +++ b/frontend/src/pages/Admin/System/index.jsx @@ -4,6 +4,7 @@ import { isMobile } from "react-device-detect"; import Admin from "@/models/admin"; import showToast from "@/utils/toast"; import CTAButton from "@/components/lib/CTAButton"; +import System from "@/models/system"; export default function AdminSystem() { const [saving, setSaving] = useState(false); @@ -13,6 +14,11 @@ export default function AdminSystem() { enabled: false, limit: 10, }); + const [canLoginWithGoogle, setCanLoginWithGoogle] = useState({ + enabled: false, + clientId: null, + allowedDomain: null, + }); const handleSubmit = async (e) => { e.preventDefault(); @@ -21,7 +27,14 @@ export default function AdminSystem() { users_can_delete_workspaces: canDelete, limit_user_messages: messageLimit.enabled, message_limit: messageLimit.limit, + users_can_login_with_google: canLoginWithGoogle.enabled, + allowed_domain: canLoginWithGoogle.allowedDomain, }); + if (canLoginWithGoogle.enabled && canLoginWithGoogle.clientId) { + await System.updateSystem({ + GoogleAuthClientId: canLoginWithGoogle.clientId, + }); + } setSaving(false); setHasChanges(false); showToast("System preferences updated successfully.", "success"); @@ -36,6 +49,12 @@ export default function AdminSystem() { enabled: settings.limit_user_messages, limit: settings.message_limit, }); + setCanLoginWithGoogle({ + ...canLoginWithGoogle, + enabled: settings.users_can_login_with_google, + clientId: settings.users_can_login_with_google ? "*".repeat(20) : "", + allowedDomain: settings.allowed_domain, + }); } fetchSettings(); }, []); @@ -148,6 +167,87 @@ export default function AdminSystem() {
)}
+ +
+
+

+ Users can login with Google +

+

+ Enable this option if you want users to be able to log in using + their Google accounts. You can restrict access to users with + emails from your organization's domain. +

+
+ +
+
+ {canLoginWithGoogle.enabled && ( +
+ +
+ e.target.blur()} + onChange={(e) => { + setCanLoginWithGoogle({ + ...canLoginWithGoogle, + clientId: e.target.value, + }); + }} + value={canLoginWithGoogle.clientId} + min={1} + max={300} + className="w-1/3 rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-slate-200 dark:text-slate-200 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary" + /> +
+ + +

+ Restrict access to a specific domain, or leave empty to allow + login with any Google account. +

+
+ e.target.blur()} + onChange={(e) => { + setCanLoginWithGoogle({ + ...canLoginWithGoogle, + allowedDomain: e.target.value, + }); + }} + value={canLoginWithGoogle.allowedDomain} + min={1} + max={300} + className="w-1/3 rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-slate-200 dark:text-slate-200 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary" + /> +
+
+ )} +
diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 93bdc0884f..2a9afae295 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -538,6 +538,11 @@ picocolors "^1.0.0" tslib "^2.6.0" +"@react-oauth/google@^0.12.1": + version "0.12.1" + resolved "https://registry.yarnpkg.com/@react-oauth/google/-/google-0.12.1.tgz#b76432c3a525e9afe076f787d2ded003fcc1bee9" + integrity sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg== + "@remix-run/router@1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.14.1.tgz#6d2dd03d52e604279c38911afc1079d58c50a755" diff --git a/server/.env.example b/server/.env.example index f51d617718..d9f387e0fb 100644 --- a/server/.env.example +++ b/server/.env.example @@ -232,3 +232,9 @@ TTS_PROVIDER="native" #------ Bing Search ----------- https://portal.azure.com/ # AGENT_BING_SEARCH_API_KEY= + +########################################### +######## SOCIAL PROVIDERS KEYS ############### +########################################### + +# GOOGLE_AUTH_CLIENT_ID= diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index 59d645447e..79f4f7b923 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -355,6 +355,12 @@ function adminEndpoints(app) { ?.value, [] ) || [], + users_can_login_with_google: + (await SystemSettings.get({ label: "users_can_login_with_google" })) + ?.value === "true", + allowed_domain: ( + await SystemSettings.get({ label: "allowed_domain" }) + )?.value, custom_app_name: (await SystemSettings.get({ label: "custom_app_name" }))?.value || null, diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 6ab30c5c10..d0a717f89a 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -50,6 +50,7 @@ const { resetPassword, generateRecoveryCodes, } = require("../utils/PasswordRecovery"); +const { SocialProvider } = require("../utils/socialProviders"); const { SlashCommandPresets } = require("../models/slashCommandsPresets"); function systemEndpoints(app) { @@ -130,6 +131,24 @@ function systemEndpoints(app) { return; } + // Users who created their account with a social provider do not have a password. So they can only log in with the social provider. + if (existingUser.use_social_provider) { + await EventLogs.logEvent( + "failed_login_user_use_social_provider", + { + ip: request.ip || "Unknown IP", + username: username || "Unknown user", + }, + existingUser?.id + ); + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[005] Invalid login credentials.", + }); + return; + } if (!bcrypt.compareSync(String(password), existingUser.password)) { await EventLogs.logEvent( "failed_login_invalid_password", @@ -246,6 +265,122 @@ function systemEndpoints(app) { } }); + app.post("/social-login/:provider", async (request, response) => { + try { + if (!(await SystemSettings.isMultiUserMode())) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "Social login is available on multi-user mode", + }); + return; + } + + const { provider } = request.params; + const data = reqBody(request); + const socialProvider = new SocialProvider(provider); + const { username } = await socialProvider.login(data); + let user = await User.get({ username: String(username) }); + + const allowedDomain = ( + await SystemSettings.get({ label: "allowed_domain" }) + )?.value; + if (allowedDomain && allowedDomain !== username.split("@")[1]) { + await EventLogs.logEvent( + "failed_login_domain_not_allowed", + { + ip: request.ip || "Unknown IP", + username: username || "Unknown user", + }, + user?.id + ); + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[006] Domain not allowed by admin.", + }); + return; + } + + if (!user) { + const { user: newUser, error } = await User.createWithSocialProvider({ + username: String(username), + }); + if (!newUser) { + await EventLogs.logEvent( + "failed_login_error_creating_user", + { + ip: request.ip || "Unknown IP", + username: username || "Unknown user", + }, + existingUser?.id + ); + response.status(200).json({ + user: null, + valid: false, + token: null, + message: error, + }); + return; + } + await EventLogs.logEvent( + "user_created", + { + userName: newUser.username, + createdBy: newUser.username, + }, + newUser.id + ); + user = newUser; + } + + if (user.suspended) { + await EventLogs.logEvent( + "failed_login_account_suspended", + { + ip: request.ip || "Unknown IP", + username: username || "Unknown user", + }, + user?.id + ); + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[004] Account suspended by admin.", + }); + return; + } + + await Telemetry.sendTelemetry( + "login_event", + { multiUserMode: false }, + user?.id + ); + + await EventLogs.logEvent( + "login_event", + { + ip: request.ip || "Unknown IP", + username: user.username || "Unknown user", + }, + user?.id + ); + + response.status(200).json({ + valid: true, + user: user, + token: makeJWT({ id: user.id, username: user.username }, "30d"), + message: null, + }); + return; + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); app.post( "/system/recover-account", [isMultiUserSetup], diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index ac05231984..4cf3c1e9df 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -26,6 +26,8 @@ const SystemSettings = { "text_splitter_chunk_overlap", "agent_search_provider", "default_agent_skills", + "users_can_login_with_google", + "allowed_domain", "agent_sql_connections", "custom_app_name", ], @@ -176,6 +178,15 @@ const SystemSettings = { AgentGoogleSearchEngineKey: process.env.AGENT_GSE_KEY || null, AgentSerperApiKey: process.env.AGENT_SERPER_DEV_KEY || null, AgentBingSearchApiKey: process.env.AGENT_BING_SEARCH_API_KEY || null, + + // -------------------------------------------------------- + // Social Providers + // -------------------------------------------------------- + GoogleAuthClientId: + (await this.get({ label: "users_can_login_with_google" }))?.value === + "true" + ? process.env.GOOGLE_AUTH_CLIENT_ID + : null, }; }, diff --git a/server/models/user.js b/server/models/user.js index f08548afb7..3062072e22 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -62,6 +62,21 @@ const User = { } }, + createWithSocialProvider: async function ({ username }) { + try { + const user = await prisma.users.create({ + data: { + username, + use_social_provider: true, + }, + }); + return { user, error: null }; + } catch (error) { + console.error("FAILED TO CREATE USER.", error.message); + return { user: null, error: error.message }; + } + }, + // Log the changes to a user object, but omit sensitive fields // that are not meant to be logged. loggedChanges: function (updates, prev = {}) { diff --git a/server/package.json b/server/package.json index 4f9954700e..8e178bcb3c 100644 --- a/server/package.json +++ b/server/package.json @@ -49,6 +49,7 @@ "express-ws": "^5.0.2", "extract-json-from-string": "^1.0.1", "extract-zip": "^2.0.1", + "google-auth-library": "^9.9.0", "graphql": "^16.7.1", "joi": "^17.11.0", "joi-password-complexity": "^5.2.0", diff --git a/server/prisma/migrations/20240509170128_init/migration.sql b/server/prisma/migrations/20240509170128_init/migration.sql new file mode 100644 index 0000000000..74fd5b302b --- /dev/null +++ b/server/prisma/migrations/20240509170128_init/migration.sql @@ -0,0 +1,20 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_users" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "username" TEXT, + "password" TEXT, + "use_social_provider" BOOLEAN NOT NULL DEFAULT false, + "pfpFilename" TEXT, + "role" TEXT NOT NULL DEFAULT 'default', + "suspended" INTEGER NOT NULL DEFAULT 0, + "seen_recovery_codes" BOOLEAN DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO "new_users" ("createdAt", "id", "lastUpdatedAt", "password", "pfpFilename", "role", "seen_recovery_codes", "suspended", "username") SELECT "createdAt", "id", "lastUpdatedAt", "password", "pfpFilename", "role", "seen_recovery_codes", "suspended", "username" FROM "users"; +DROP TABLE "users"; +ALTER TABLE "new_users" RENAME TO "users"; +CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 0ded65be63..52152d4e97 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -58,7 +58,8 @@ model system_settings { model users { id Int @id @default(autoincrement()) username String? @unique - password String + password String? + use_social_provider Boolean @default(false) pfpFilename String? role String @default("default") suspended Int @default(0) diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index d5cdc68f2e..ab5f9293b6 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -404,6 +404,12 @@ const KEY_MAPPING = { checks: [], }, + // Social Providers + GoogleAuthClientId: { + envKey: "GOOGLE_AUTH_CLIENT_ID", + checks: [isNotEmpty], + }, + // TTS/STT Integration ENVS TextToSpeechProvider: { envKey: "TTS_PROVIDER", diff --git a/server/utils/socialProviders/index.js b/server/utils/socialProviders/index.js new file mode 100644 index 0000000000..a12e68bd9e --- /dev/null +++ b/server/utils/socialProviders/index.js @@ -0,0 +1,56 @@ +const { OAuth2Client } = require("google-auth-library"); + +class SocialProviderAbstract { + constructor() { + if (this.constructor === SocialProviderAbstract) { + throw new Error("Cannot instantiate abstract class"); + } + } + async login(data) { + throw new Error("Method 'login' must be implemented"); + } +} + +class GoogleProvider extends SocialProviderAbstract { + constructor() { + super(); + this.client = new OAuth2Client(); + } + async login({ credential }) { + const { email, name, picture, error } = await this._verify(credential); + if (error) { + throw new Error("Google Sign-In failed"); + } + return { username: email }; + } + + async _verify(token) { + try { + const ticket = await this.client.verifyIdToken({ + idToken: token, + audience: process.env.GOOGLE_AUTH_CLIENT_ID, + }); + const userData = ticket.getPayload(); + return { ...userData, error: null }; + } catch (error) { + return { error: error.message }; + } + } +} + +const providers = { + google: new GoogleProvider(), + // Add here new providers +}; + +class SocialProvider { + constructor(provider) { + this.instance = providers[provider]; + } + + async login(data) { + return await this.instance.login(data); + } +} + +module.exports.SocialProvider = SocialProvider; diff --git a/server/yarn.lock b/server/yarn.lock index d274e574f3..dd64250c32 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -3200,6 +3200,17 @@ gaxios@^5.0.0, gaxios@^5.0.1: is-stream "^2.0.0" node-fetch "^2.6.9" +gaxios@^6.0.0, gaxios@^6.1.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.5.0.tgz#21bc20e24f21189ce8907079b56205ff9fd2c0d7" + integrity sha512-R9QGdv8j4/dlNoQbX3hSaK/S0rkMijqjVvW3YM06CoBdbU/VdKd159j4hePpng0KuE6Lh6JJ7UdmVGJZFcAG1w== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + is-stream "^2.0.0" + node-fetch "^2.6.9" + uuid "^9.0.1" + gcp-metadata@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-5.3.0.tgz#6f45eb473d0cb47d15001476b48b663744d25408" @@ -3208,6 +3219,13 @@ gcp-metadata@^5.3.0: gaxios "^5.0.0" json-bigint "^1.0.0" +gcp-metadata@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.0.tgz#9b0dd2b2445258e7597f2024332d20611cbd6b8c" + integrity sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg== + dependencies: + gaxios "^6.0.0" + json-bigint "^1.0.0" generate-function@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" @@ -3318,6 +3336,18 @@ google-auth-library@^8.0.2: jws "^4.0.0" lru-cache "^6.0.0" +google-auth-library@^9.9.0: + version "9.9.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.9.0.tgz#71488ef444335ff4ea91611729b88c0f57625fdf" + integrity sha512-9l+zO07h1tDJdIHN74SpnWIlNR+OuOemXlWJlLP9pXy6vFtizgpEzMuwJa4lqY9UAdiAv5DVd5ql0Am916I+aA== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^6.1.1" + gcp-metadata "^6.1.0" + gtoken "^7.0.0" + jws "^4.0.0" + google-p12-pem@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-4.0.1.tgz#82841798253c65b7dc2a4e5fe9df141db670172a" @@ -3378,6 +3408,14 @@ gtoken@^6.1.0: google-p12-pem "^4.0.0" jws "^4.0.0" +gtoken@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" + integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== + dependencies: + gaxios "^6.0.0" + jws "^4.0.0" + guid-typescript@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc" @@ -3513,7 +3551,7 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -https-proxy-agent@^7.0.0: +https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1: version "7.0.4" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168" integrity sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==