From 5a51bfb9847a72a34784b8ba5831628e1b86cc88 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Fri, 4 Apr 2025 10:46:00 +0200 Subject: [PATCH 01/15] update codecov yml --- codecov.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/codecov.yml b/codecov.yml index 8a484e684..5f6e2ef12 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,7 +8,6 @@ ignore: - "**/*/build/**" - "**/*/.test.*" - "**/*/version.ts" -review: - poem: false - review_status: false - collapse_walkthrough: true \ No newline at end of file +comment: + layout: "condensed_header, condensed_files, condensed_footer" + hide_project_coverage: TRUE From c1d51b98a245be47c6111b0034a77859b5dd970d Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 12:05:36 +0200 Subject: [PATCH 02/15] feat(assist-server): added a first part of the assist v2 (#3269) --- .github/workflows/assist-server-ee.yaml | 121 ++ ee/assist-server/.gitignore | 5 + ee/assist-server/Dockerfile | 24 + ee/assist-server/app/assist.js | 168 ++ ee/assist-server/app/cache.js | 106 + ee/assist-server/app/geoIP.js | 21 + ee/assist-server/app/logger.js | 23 + ee/assist-server/app/socket.js | 254 +++ ee/assist-server/build.sh | 65 + ee/assist-server/package-lock.json | 1761 +++++++++++++++++ ee/assist-server/package.json | 24 + ee/assist-server/server.js | 75 + .../charts/assist-server/.helmignore | 23 + .../charts/assist-server/Chart.yaml | 24 + .../charts/assist-server/templates/NOTES.txt | 22 + .../assist-server/templates/_helpers.tpl | 65 + .../assist-server/templates/deployment.yaml | 113 ++ .../charts/assist-server/templates/hpa.yaml | 33 + .../assist-server/templates/ingress.yaml | 56 + .../assist-server/templates/service.yaml | 18 + .../templates/serviceMonitor.yaml | 18 + .../templates/serviceaccount.yaml | 13 + .../charts/assist-server/values.yaml | 134 ++ 23 files changed, 3166 insertions(+) create mode 100644 .github/workflows/assist-server-ee.yaml create mode 100644 ee/assist-server/.gitignore create mode 100644 ee/assist-server/Dockerfile create mode 100644 ee/assist-server/app/assist.js create mode 100644 ee/assist-server/app/cache.js create mode 100644 ee/assist-server/app/geoIP.js create mode 100644 ee/assist-server/app/logger.js create mode 100644 ee/assist-server/app/socket.js create mode 100644 ee/assist-server/build.sh create mode 100644 ee/assist-server/package-lock.json create mode 100644 ee/assist-server/package.json create mode 100644 ee/assist-server/server.js create mode 100644 scripts/helmcharts/openreplay/charts/assist-server/.helmignore create mode 100644 scripts/helmcharts/openreplay/charts/assist-server/Chart.yaml create mode 100644 scripts/helmcharts/openreplay/charts/assist-server/templates/NOTES.txt create mode 100644 scripts/helmcharts/openreplay/charts/assist-server/templates/_helpers.tpl create mode 100644 scripts/helmcharts/openreplay/charts/assist-server/templates/deployment.yaml create mode 100644 scripts/helmcharts/openreplay/charts/assist-server/templates/hpa.yaml create mode 100644 scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml create mode 100644 scripts/helmcharts/openreplay/charts/assist-server/templates/service.yaml create mode 100644 scripts/helmcharts/openreplay/charts/assist-server/templates/serviceMonitor.yaml create mode 100644 scripts/helmcharts/openreplay/charts/assist-server/templates/serviceaccount.yaml create mode 100644 scripts/helmcharts/openreplay/charts/assist-server/values.yaml diff --git a/.github/workflows/assist-server-ee.yaml b/.github/workflows/assist-server-ee.yaml new file mode 100644 index 000000000..653495cc6 --- /dev/null +++ b/.github/workflows/assist-server-ee.yaml @@ -0,0 +1,121 @@ +# This action will push the assist changes to aws +on: + workflow_dispatch: + inputs: + skip_security_checks: + description: "Skip Security checks if there is a unfixable vuln or error. Value: true/false" + required: false + default: "false" + push: + branches: + - dev + paths: + - "ee/assist-server/**" + +name: Build and Deploy Assist-Server EE + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + # We need to diff with old commit + # to see which workers got changed. + fetch-depth: 2 + + - uses: ./.github/composite-actions/update-keys + with: + assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }} + assist_key: ${{ secrets.ASSIST_KEY }} + domain_name: ${{ secrets.EE_DOMAIN_NAME }} + jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }} + jwt_secret: ${{ secrets.EE_JWT_SECRET }} + jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }} + jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }} + license_key: ${{ secrets.EE_LICENSE_KEY }} + minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }} + minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }} + pg_password: ${{ secrets.EE_PG_PASSWORD }} + registry_url: ${{ secrets.OSS_REGISTRY_URL }} + name: Update Keys + + - name: Docker login + run: | + docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}" + + - uses: azure/k8s-set-context@v1 + with: + method: kubeconfig + kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret. + id: setcontext + + - name: Building and Pushing Assist-Server image + id: build-image + env: + DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }} + IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee + ENVIRONMENT: staging + run: | + skip_security_checks=${{ github.event.inputs.skip_security_checks }} + cd ee/assist-server + PUSH_IMAGE=0 bash -x ./build.sh ee + [[ "x$skip_security_checks" == "xtrue" ]] || { + curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./ + images=("assist-server") + for image in ${images[*]};do + ./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG + done + err_code=$? + [[ $err_code -ne 0 ]] && { + exit $err_code + } + } && { + echo "Skipping Security Checks" + } + images=("assist-server") + for image in ${images[*]};do + docker push $DOCKER_REPO/$image:$IMAGE_TAG + done + - name: Creating old image input + run: | + # + # Create yaml with existing image tags + # + kubectl get pods -n app -o jsonpath="{.items[*].spec.containers[*].image}" |\ + tr -s '[[:space:]]' '\n' | sort | uniq -c | grep '/foss/' | cut -d '/' -f3 > /tmp/image_tag.txt + + echo > /tmp/image_override.yaml + + for line in `cat /tmp/image_tag.txt`; + do + image_array=($(echo "$line" | tr ':' '\n')) + cat <> /tmp/image_override.yaml + ${image_array[0]}: + image: + # We've to strip off the -ee, as helm will append it. + tag: `echo ${image_array[1]} | cut -d '-' -f 1` + EOF + done + - name: Deploy to kubernetes + run: | + cd ../../scripts/helmcharts/ + + # Update changed image tag + sed -i "/assist-server/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml + + cat /tmp/image_override.yaml + # Deploy command + mkdir -p /tmp/charts + mv openreplay/charts/{ingress-nginx,assist-server,quickwit,connector} /tmp/charts/ + rm -rf openreplay/charts/* + mv /tmp/charts/* openreplay/charts/ + helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f - + env: + DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }} + # We're not passing -ee flag, because helm will add that. + IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }} + ENVIRONMENT: staging diff --git a/ee/assist-server/.gitignore b/ee/assist-server/.gitignore new file mode 100644 index 000000000..e0fd21e8f --- /dev/null +++ b/ee/assist-server/.gitignore @@ -0,0 +1,5 @@ +.idea +node_modules +npm-debug.log +.cache +*.mmdb \ No newline at end of file diff --git a/ee/assist-server/Dockerfile b/ee/assist-server/Dockerfile new file mode 100644 index 000000000..f30f5dc1d --- /dev/null +++ b/ee/assist-server/Dockerfile @@ -0,0 +1,24 @@ +ARG ARCH=amd64 + +FROM --platform=linux/$ARCH node:23-alpine +LABEL Maintainer="Zavorotynskiy Alexander " +RUN apk add --no-cache tini git libc6-compat +ARG envarg +ENV ENTERPRISE_BUILD=${envarg} \ + MAXMINDDB_FILE=/home/openreplay/geoip.mmdb \ + PRIVATE_ENDPOINTS=false \ + LISTEN_PORT=9001 \ + ERROR=1 \ + NODE_ENV=production +WORKDIR /work +COPY package.json . +COPY package-lock.json . +RUN npm install +COPY . . + +RUN adduser -u 1001 openreplay -D +USER 1001 +ADD --chown=1001 https://static.openreplay.com/geoip/GeoLite2-City.mmdb $MAXMINDDB_FILE + +ENTRYPOINT ["/sbin/tini", "--"] +CMD npm start diff --git a/ee/assist-server/app/assist.js b/ee/assist-server/app/assist.js new file mode 100644 index 000000000..a394f4908 --- /dev/null +++ b/ee/assist-server/app/assist.js @@ -0,0 +1,168 @@ +const jwt = require('jsonwebtoken'); +const uaParser = require('ua-parser-js'); +const {geoip} = require('./geoIP'); +const {logger} = require('./logger'); + +let PROJECT_KEY_LENGTH = parseInt(process.env.PROJECT_KEY_LENGTH) || 20; + +const IDENTITIES = {agent: 'agent', session: 'session'}; +const EVENTS_DEFINITION = { + listen: { + UPDATE_EVENT: "UPDATE_SESSION", // tab become active/inactive, page title change, changed session object (rare case), call start/end + CONNECT_ERROR: "connect_error", + CONNECT_FAILED: "connect_failed", + ERROR: "error" + }, + //The following list of events will be only emitted by the server + server: { + UPDATE_SESSION: "SERVER_UPDATE_SESSION" + } +}; +EVENTS_DEFINITION.emit = { + NEW_AGENT: "NEW_AGENT", + NO_AGENTS: "NO_AGENT", + AGENT_DISCONNECT: "AGENT_DISCONNECTED", + AGENTS_CONNECTED: "AGENTS_CONNECTED", + NO_SESSIONS: "SESSION_DISCONNECTED", + SESSION_ALREADY_CONNECTED: "SESSION_ALREADY_CONNECTED", + SESSION_RECONNECTED: "SESSION_RECONNECTED", + UPDATE_EVENT: EVENTS_DEFINITION.listen.UPDATE_EVENT +}; + +const BASE_sessionInfo = { + "pageTitle": "Page", + "active": false, + "live": true, + "sessionID": "0", + "metadata": {}, + "userID": "", + "userUUID": "", + "projectKey": "", + "revID": "", + "timestamp": 0, + "trackerVersion": "", + "isSnippet": true, + "userOs": "", + "userBrowser": "", + "userBrowserVersion": "", + "userDevice": "", + "userDeviceType": "", + "userCountry": "", + "userState": "", + "userCity": "", + "projectId": 0 +}; + +const extractPeerId = (peerId) => { + const parts = peerId.split("-"); + if (parts.length < 2 || parts.length > 3) { + logger.debug(`Invalid peerId format: ${peerId}`); + return {}; + } + if (PROJECT_KEY_LENGTH > 0 && parts[0].length !== PROJECT_KEY_LENGTH) { + logger.debug(`Invalid project key length in peerId: ${peerId}`); + return {}; + } + const [projectKey, sessionId, tabId = generateRandomTabId()] = parts; + return { projectKey, sessionId, tabId }; +}; + +const generateRandomTabId = () => (Math.random() + 1).toString(36).substring(2); + +function processPeerInfo(socket) { + socket._connectedAt = new Date(); + const { projectKey, sessionId, tabId } = extractPeerId(socket.handshake.query.peerId || ""); + Object.assign(socket.handshake.query, { + roomId: projectKey && sessionId ? `${projectKey}-${sessionId}` : null, + projectKey, + sessId: sessionId, + tabId + }); + logger.debug(`Connection details: projectKey:${projectKey}, sessionId:${sessionId}, tabId:${tabId}, roomId:${socket.handshake.query.roomId}`); +} + +/** + * extracts and populate socket with information + * @Param {socket} used socket + * */ +const extractSessionInfo = function (socket) { + if (socket.handshake.query.sessionInfo !== undefined) { + logger.debug(`received headers: ${socket.handshake.headers}`); + + socket.handshake.query.sessionInfo = JSON.parse(socket.handshake.query.sessionInfo); + socket.handshake.query.sessionInfo = {...BASE_sessionInfo, ...socket.handshake.query.sessionInfo}; + + let ua = uaParser(socket.handshake.headers['user-agent']); + socket.handshake.query.sessionInfo.userOs = ua.os.name || null; + socket.handshake.query.sessionInfo.userBrowser = ua.browser.name || null; + socket.handshake.query.sessionInfo.userBrowserVersion = ua.browser.version || null; + socket.handshake.query.sessionInfo.userDevice = ua.device.model || null; + socket.handshake.query.sessionInfo.userDeviceType = ua.device.type || 'desktop'; + socket.handshake.query.sessionInfo.userCountry = null; + socket.handshake.query.sessionInfo.userState = null; + socket.handshake.query.sessionInfo.userCity = null; + if (geoip() !== null) { + logger.debug(`looking for location of ${socket.handshake.headers['x-forwarded-for'] || socket.handshake.address}`); + try { + let ip = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address; + ip = ip.split(",")[0]; + let info = geoip().city(ip); + socket.handshake.query.sessionInfo.userCountry = info.country.isoCode; + socket.handshake.query.sessionInfo.userCity = info.city.names.en; + socket.handshake.query.sessionInfo.userState = info.subdivisions.length > 0 ? info.subdivisions[0].names.en : null; + } catch (e) { + logger.debug(`geoip-country failed: ${e}`); + } + } + } +} + +function errorHandler(listenerName, error) { + logger.error(`Error detected from ${listenerName}\n${error}`); +} + +const JWT_TOKEN_PREFIX = "Bearer "; + +function check(socket, next) { + if (socket.handshake.query.identity === IDENTITIES.session) { + return next(); + } + if (socket.handshake.query.peerId && socket.handshake.auth && socket.handshake.auth.token) { + let token = socket.handshake.auth.token; + if (token.startsWith(JWT_TOKEN_PREFIX)) { + token = token.substring(JWT_TOKEN_PREFIX.length); + } + jwt.verify(token, process.env.ASSIST_JWT_SECRET, (err, decoded) => { + logger.debug(`JWT payload: ${decoded}`); + if (err) { + logger.debug(err); + return next(new Error('Authentication error')); + } + const {projectKey, sessionId} = extractPeerId(socket.handshake.query.peerId); + if (!projectKey || !sessionId) { + logger.debug(`Missing attribute: projectKey:${projectKey}, sessionId:${sessionId}`); + return next(new Error('Authentication error')); + } + if (String(projectKey) !== String(decoded.projectKey) || String(sessionId) !== String(decoded.sessionId)) { + logger.debug(`Trying to access projectKey:${projectKey} instead of ${decoded.projectKey} or + to sessionId:${sessionId} instead of ${decoded.sessionId}`); + return next(new Error('Authorization error')); + } + socket.decoded = decoded; + return next(); + }); + } else { + logger.debug(`something missing in handshake: ${socket.handshake}`); + return next(new Error('Authentication error')); + } +} + +module.exports = { + processPeerInfo, + extractPeerId, + extractSessionInfo, + EVENTS_DEFINITION, + IDENTITIES, + errorHandler, + authorizer: {check} +}; \ No newline at end of file diff --git a/ee/assist-server/app/cache.js b/ee/assist-server/app/cache.js new file mode 100644 index 000000000..decb487d0 --- /dev/null +++ b/ee/assist-server/app/cache.js @@ -0,0 +1,106 @@ +const {logger} = require('./logger'); +const {createClient} = require("redis"); +const crypto = require("crypto"); + +let redisClient; +const REDIS_URL = (process.env.REDIS_URL || "localhost:6379").replace(/((^\w+:|^)\/\/|^)/, 'redis://'); +redisClient = createClient({url: REDIS_URL}); +redisClient.on("error", (error) => logger.error(`Redis cache error : ${error}`)); +void redisClient.connect(); + +function generateNodeID() { + const buffer = crypto.randomBytes(8); + return "node_"+buffer.readBigUInt64BE(0).toString(); +} + +const pingInterval = parseInt(process.env.PING_INTERVAL) || 25000; +const CACHE_REFRESH_INTERVAL = parseInt(process.env.cacheRefreshInterval) || 10000; +let lastCacheUpdateTime = 0; +let cacheRefresher = null; +const nodeID = process.env.HOSTNAME || generateNodeID(); + +const addSessionToCache = async function (sessionID, sessionData) { + try { + await redisClient.set(`active_sessions:${sessionID}`, JSON.stringify(sessionData), 'EX', pingInterval*2); + logger.debug(`Session ${sessionID} stored in Redis`); + } catch (error) { + logger.error(error); + } +} + +const renewSession = async function (sessionID){ + try { + await redisClient.expire(`active_sessions:${sessionID}`, pingInterval*2); + logger.debug(`Session ${sessionID} renewed in Redis`); + } catch (error) { + logger.error(error); + } +} + +const getSessionFromCache = async function (sessionID) { + try { + const sessionData = await redisClient.get(`active_sessions:${sessionID}`); + if (sessionData) { + logger.debug(`Session ${sessionID} retrieved from Redis`); + return JSON.parse(sessionData); + } + return null; + } catch (error) { + logger.error(error); + return null; + } +} + +const removeSessionFromCache = async function (sessionID) { + try { + await redisClient.del(`active_sessions:${sessionID}`); + logger.debug(`Session ${sessionID} removed from Redis`); + } catch (error) { + logger.error(error); + } +} + +const setNodeSessions = async function (nodeID, sessionIDs) { + try { + await redisClient.set(`node:${nodeID}:sessions`, JSON.stringify(sessionIDs), 'EX', CACHE_REFRESH_INTERVAL*2); + logger.debug(`Node ${nodeID} sessions stored in Redis`); + } catch (error) { + logger.error(error); + } +} + +function startCacheRefresher(io) { + if (cacheRefresher) clearInterval(cacheRefresher); + + cacheRefresher = setInterval(async () => { + const now = Date.now(); + if (now - lastCacheUpdateTime < CACHE_REFRESH_INTERVAL) { + return; + } + logger.debug('Background refresh triggered'); + try { + const startTime = performance.now(); + const sessionIDs = new Set(); + const result = await io.fetchSockets(); + result.forEach((r) => { + if (r.handshake.query.sessionID) { + sessionIDs.add(r.handshake.query.sessionID); + } + }) + await setNodeSessions(nodeID, Array.from(sessionIDs)); + lastCacheUpdateTime = now; + const duration = performance.now() - startTime; + logger.info(`Background refresh complete: ${duration}ms, ${result.length} sockets`); + } catch (error) { + logger.error(`Background refresh error: ${error}`); + } + }, CACHE_REFRESH_INTERVAL / 2); +} + +module.exports = { + addSessionToCache, + renewSession, + getSessionFromCache, + removeSessionFromCache, + startCacheRefresher, +} \ No newline at end of file diff --git a/ee/assist-server/app/geoIP.js b/ee/assist-server/app/geoIP.js new file mode 100644 index 000000000..69445c4fe --- /dev/null +++ b/ee/assist-server/app/geoIP.js @@ -0,0 +1,21 @@ +const geoip2Reader = require('@maxmind/geoip2-node').Reader; +const {logger} = require('./logger'); + +let geoip = null; +if (process.env.MAXMINDDB_FILE !== undefined) { + geoip2Reader.open(process.env.MAXMINDDB_FILE, {}) + .then(reader => { + geoip = reader; + }) + .catch(error => { + logger.error(`Error while opening the MAXMINDDB_FILE, err: ${error}`); + }); +} else { + logger.error("!!! please provide a valid value for MAXMINDDB_FILE env var."); +} + +module.exports = { + geoip: () => { + return geoip; + } +} \ No newline at end of file diff --git a/ee/assist-server/app/logger.js b/ee/assist-server/app/logger.js new file mode 100644 index 000000000..c7a541dfd --- /dev/null +++ b/ee/assist-server/app/logger.js @@ -0,0 +1,23 @@ +const winston = require('winston'); + +const isDebugMode = process.env.debug === "1"; +const logLevel = isDebugMode ? 'debug' : 'info'; + +const logger = winston.createLogger({ + level: logLevel, + format: winston.format.combine( + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss.SSS' // The same format as in backend services + }), + winston.format.errors({stack: true}), + winston.format.json() + ), + defaultMeta: {service: process.env.SERVICE_NAME || 'assist'}, + transports: [ + new winston.transports.Console(), + ], +}); + +module.exports = { + logger, +} diff --git a/ee/assist-server/app/socket.js b/ee/assist-server/app/socket.js new file mode 100644 index 000000000..28005a2fc --- /dev/null +++ b/ee/assist-server/app/socket.js @@ -0,0 +1,254 @@ +const { + processPeerInfo, + IDENTITIES, + EVENTS_DEFINITION, + extractSessionInfo, + errorHandler +} = require("./assist"); +const { + addSessionToCache, + renewSession, + removeSessionFromCache +} = require('./cache'); +const { + logger +} = require('./logger'); +const deepMerge = require('@fastify/deepmerge')({all: true}); + +let io; + +const setSocketIOServer = function (server) { + io = server; +} + +function sendFrom(from, to, eventName, ...data) { + from.to(to).emit(eventName, ...data); +} + +function sendTo(to, eventName, ...data) { + sendFrom(io, to, eventName, ...data); +} + +const fetchSockets = async function (roomID) { + if (!io) { + return []; + } + try { + if (roomID) { + return await io.in(roomID).fetchSockets(); + } else { + return await io.fetchSockets(); + } + } catch (error) { + logger.error('Error fetching sockets:', error); + return []; + } +} + +const findSessionSocketId = async (roomId, tabId) => { + let pickFirstSession = tabId === undefined; + const connected_sockets = await fetchSockets(roomId); + for (let socket of connected_sockets) { + if (socket.handshake.query.identity === IDENTITIES.session) { + if (pickFirstSession) { + return socket.id; + } else if (socket.handshake.query.tabId === tabId) { + return socket.id; + } + } + } + return null; +}; + +async function getRoomData(roomID) { + let tabsCount = 0, agentsCount = 0, tabIDs = [], agentIDs = []; + const connected_sockets = await fetchSockets(roomID); + if (connected_sockets.length > 0) { + for (let socket of connected_sockets) { + if (socket.handshake.query.identity === IDENTITIES.session) { + tabsCount++; + tabIDs.push(socket.handshake.query.tabId); + } else { + agentsCount++; + agentIDs.push(socket.id); + } + } + } else { + tabsCount = -1; + agentsCount = -1; + } + return {tabsCount, agentsCount, tabIDs, agentIDs}; +} + +async function onConnect(socket) { + logger.debug(`A new client:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`); + // Drop unknown socket.io connections + if (socket.handshake.query.identity === undefined || socket.handshake.query.peerId === undefined || socket.handshake.query.sessionInfo === undefined) { + logger.debug(`something is undefined, refusing connexion`); + return socket.disconnect(); + } + processPeerInfo(socket); + + const {tabsCount, agentsCount, tabIDs, agentIDs} = await getRoomData(socket.handshake.query.roomId); + + if (socket.handshake.query.identity === IDENTITIES.session) { + // Check if session with the same tabID already connected, if so, refuse new connexion + if (tabsCount > 0) { + for (let tab of tabIDs) { + if (tab === socket.handshake.query.tabId) { + logger.debug(`session already connected, refusing new connexion, peerId: ${socket.handshake.query.peerId}`); + sendTo(socket.id, EVENTS_DEFINITION.emit.SESSION_ALREADY_CONNECTED); + return socket.disconnect(); + } + } + } + extractSessionInfo(socket); + if (tabsCount < 0) { + // New session creates new room + } + // Inform all connected agents about reconnected session + if (agentsCount > 0) { + logger.debug(`notifying new session about agent-existence`); + sendTo(socket.id, EVENTS_DEFINITION.emit.AGENTS_CONNECTED, agentIDs); + sendFrom(socket, socket.handshake.query.roomId, EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id); + } + } else if (tabsCount <= 0) { + logger.debug(`notifying new agent about no SESSIONS with peerId:${socket.handshake.query.peerId}`); + sendTo(socket.id, EVENTS_DEFINITION.emit.NO_SESSIONS); + } + + await socket.join(socket.handshake.query.roomId); + logger.debug(`${socket.id} joined room:${socket.handshake.query.roomId}, as:${socket.handshake.query.identity}, connections:${agentsCount + tabsCount + 1}`) + + // Add session to cache + if (socket.handshake.query.identity === IDENTITIES.session) { + await addSessionToCache(socket.handshake.query.sessId, socket.handshake.query.sessionInfo); + } + + if (socket.handshake.query.identity === IDENTITIES.agent) { + if (socket.handshake.query.agentInfo !== undefined) { + socket.handshake.query.agentInfo = JSON.parse(socket.handshake.query.agentInfo); + socket.handshake.query.agentID = socket.handshake.query.agentInfo.id; + } + sendFrom(socket, socket.handshake.query.roomId, EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, socket.handshake.query.agentInfo); + } + + socket.conn.on("packet", (packet) => { + if (packet.type === 'pong') { + renewSession(socket.handshake.query.sessId); + } + }); + + // Set disconnect handler + socket.on('disconnect', () => onDisconnect(socket)); + + // Handle update event + socket.on(EVENTS_DEFINITION.listen.UPDATE_EVENT, (...args) => onUpdateEvent(socket, ...args)); + + // Handle webrtc events + socket.on(EVENTS_DEFINITION.listen.WEBRTC_AGENT_CALL, (...args) => onWebrtcAgentHandler(socket, ...args)); + + // Handle errors + socket.on(EVENTS_DEFINITION.listen.ERROR, err => errorHandler(EVENTS_DEFINITION.listen.ERROR, err)); + socket.on(EVENTS_DEFINITION.listen.CONNECT_ERROR, err => errorHandler(EVENTS_DEFINITION.listen.CONNECT_ERROR, err)); + socket.on(EVENTS_DEFINITION.listen.CONNECT_FAILED, err => errorHandler(EVENTS_DEFINITION.listen.CONNECT_FAILED, err)); + + // Handle all other events (usually dom's mutations and user's actions) + socket.onAny((eventName, ...args) => onAny(socket, eventName, ...args)); +} + +async function onDisconnect(socket) { + logger.debug(`${socket.id} disconnected from ${socket.handshake.query.roomId}`); + + if (socket.handshake.query.identity === IDENTITIES.agent) { + sendFrom(socket, socket.handshake.query.roomId, EVENTS_DEFINITION.emit.AGENT_DISCONNECT, socket.id); + } + logger.debug("checking for number of connected agents and sessions"); + let {tabsCount, agentsCount, tabIDs, agentIDs} = await getRoomData(socket.handshake.query.roomId); + + if (tabsCount <= 0) { + await removeSessionFromCache(socket.handshake.query.sessId); + } + + if (tabsCount === -1 && agentsCount === -1) { + logger.debug(`room not found: ${socket.handshake.query.roomId}`); + return; + } + if (tabsCount === 0) { + logger.debug(`notifying everyone in ${socket.handshake.query.roomId} about no SESSIONS`); + sendFrom(socket, socket.handshake.query.roomId, EVENTS_DEFINITION.emit.NO_SESSIONS); + } + if (agentsCount === 0) { + logger.debug(`notifying everyone in ${socket.handshake.query.roomId} about no AGENTS`); + sendFrom(socket, socket.handshake.query.roomId, EVENTS_DEFINITION.emit.NO_AGENTS); + } +} + +async function onUpdateEvent(socket, ...args) { + logger.debug(`${socket.id} sent update event.`); + if (socket.handshake.query.identity !== IDENTITIES.session) { + logger.debug('Ignoring update event.'); + return + } + + args[0] = updateSessionData(socket, args[0]) + socket.handshake.query.sessionInfo = deepMerge(socket.handshake.query.sessionInfo, args[0]?.data, {tabId: args[0]?.meta?.tabId}); + + // update session cache + await addSessionToCache(socket.handshake.query.sessId, socket.handshake.query.sessionInfo); + + // Update sessionInfo for all agents in the room + const connected_sockets = await fetchSockets(socket.handshake.query.roomId); + for (let item of connected_sockets) { + if (item.handshake.query.identity === IDENTITIES.session && item.handshake.query.sessionInfo) { + item.handshake.query.sessionInfo = deepMerge(item.handshake.query.sessionInfo, args[0]?.data, {tabId: args[0]?.meta?.tabId}); + } else if (item.handshake.query.identity === IDENTITIES.agent) { + sendFrom(socket, item.id, EVENTS_DEFINITION.emit.UPDATE_EVENT, args[0]); + } + } +} + +async function onWebrtcAgentHandler(socket, ...args) { + if (socket.handshake.query.identity === IDENTITIES.agent) { + const agentIdToConnect = args[0]?.data?.toAgentId; + logger.debug(`${socket.id} sent webrtc event to agent:${agentIdToConnect}`); + if (agentIdToConnect && socket.handshake.sessionData.AGENTS_CONNECTED.includes(agentIdToConnect)) { + sendFrom(socket, agentIdToConnect, EVENTS_DEFINITION.listen.WEBRTC_AGENT_CALL, args[0]); + } + } +} + +async function onAny(socket, eventName, ...args) { + if (Object.values(EVENTS_DEFINITION.listen).indexOf(eventName) >= 0) { + logger.debug(`received event:${eventName}, should be handled by another listener, stopping onAny.`); + return + } + args[0] = updateSessionData(socket, args[0]) + if (socket.handshake.query.identity === IDENTITIES.session) { + logger.debug(`received event:${eventName}, from:${socket.handshake.query.identity}, sending message to room:${socket.handshake.query.roomId}`); + sendFrom(socket, socket.handshake.query.roomId, eventName, args[0]); + } else { + logger.debug(`received event:${eventName}, from:${socket.handshake.query.identity}, sending message to session of room:${socket.handshake.query.roomId}`); + let socketId = await findSessionSocketId(socket.handshake.query.roomId, args[0]?.meta?.tabId); + if (socketId === null) { + logger.debug(`session not found for:${socket.handshake.query.roomId}`); + sendTo(socket.id, EVENTS_DEFINITION.emit.NO_SESSIONS); + } else { + logger.debug("message sent"); + sendTo(socket.id, eventName, socket.id, args[0]); + } + } +} + +// Back compatibility (add top layer with meta information) +function updateSessionData(socket, sessionData) { + if (sessionData?.meta === undefined && socket.handshake.query.identity === IDENTITIES.session) { + sessionData = {meta: {tabId: socket.handshake.query.tabId, version: 1}, data: sessionData}; + } + return sessionData +} + +module.exports = { + onConnect, + setSocketIOServer, +} diff --git a/ee/assist-server/build.sh b/ee/assist-server/build.sh new file mode 100644 index 000000000..0aaaaaadd --- /dev/null +++ b/ee/assist-server/build.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Usage: IMAGE_TAG=latest DOCKER_REPO=myDockerHubID bash build.sh + +git_sha=$(git rev-parse --short HEAD) +image_tag=${IMAGE_TAG:-git_sha} +check_prereq() { + which docker || { + echo "Docker not installed, please install docker." + exit 1 + } +} +source ../scripts/lib/_docker.sh + +[[ $1 == ee ]] && ee=true +[[ $PATCH -eq 1 ]] && { + image_tag="$(grep -ER ^.ppVersion ../scripts/helmcharts/openreplay/charts/$chart | xargs | awk '{print $2}' | awk -F. -v OFS=. '{$NF += 1 ; print}')" + [[ $ee == "true" ]] && { + image_tag="${image_tag}-ee" + } +} +update_helm_release() { + chart=$1 + HELM_TAG="$(grep -iER ^version ../scripts/helmcharts/openreplay/charts/$chart | awk '{print $2}' | awk -F. -v OFS=. '{$NF += 1 ; print}')" + # Update the chart version + sed -i "s#^version.*#version: $HELM_TAG# g" ../scripts/helmcharts/openreplay/charts/$chart/Chart.yaml + # Update image tags + sed -i "s#ppVersion.*#ppVersion: \"$image_tag\"#g" ../scripts/helmcharts/openreplay/charts/$chart/Chart.yaml + # Commit the changes + git add ../scripts/helmcharts/openreplay/charts/$chart/Chart.yaml + git commit -m "chore(helm): Updating $chart image release" +} + +function build_api() { + destination="_assist-server" + [[ $1 == "ee" ]] && { + destination="_assist-server_ee" + } + [[ -d ../${destination} ]] && { + echo "Removing previous build cache" + rm -rf ../${destination} + } + cp -R ../ee/assist-server ../${destination} + cd ../${destination} + + docker build -f ./Dockerfile --build-arg GIT_SHA=$git_sha -t ${DOCKER_REPO:-'local'}/assist-server:${image_tag} . + + cd ../assist + rm -rf ../${destination} + [[ $PUSH_IMAGE -eq 1 ]] && { + docker push ${DOCKER_REPO:-'local'}/assist-server:${image_tag} + docker tag ${DOCKER_REPO:-'local'}/assist-server:${image_tag} ${DOCKER_REPO:-'local'}/assist-server:latest + docker push ${DOCKER_REPO:-'local'}/assist-server:latest + } + [[ $SIGN_IMAGE -eq 1 ]] && { + cosign sign --key $SIGN_KEY ${DOCKER_REPO:-'local'}/assist-server:${image_tag} + } + echo "build completed for assist-server" +} + +check_prereq +build_api $1 +if [[ $PATCH -eq 1 ]]; then + update_helm_release assist-server +fi diff --git a/ee/assist-server/package-lock.json b/ee/assist-server/package-lock.json new file mode 100644 index 000000000..8d8f17e93 --- /dev/null +++ b/ee/assist-server/package-lock.json @@ -0,0 +1,1761 @@ +{ + "name": "assist-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "assist-server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@fastify/deepmerge": "^3.0.0", + "@maxmind/geoip2-node": "^6.0.0", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.2", + "redis": "^4.7.0", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", + "ua-parser-js": "^2.0.3", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.51.0", + "winston": "^3.17.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@fastify/deepmerge": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.0.0.tgz", + "integrity": "sha512-VW7srTEkCzbfqoRBn+k7s/nP9WVFmKSfeHBbMZLLrOYDQoShHWuEi5V6Jgz1Q0fm3pMYsiC+EVMvhZyI3hKzkw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/@maxmind/geoip2-node": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-6.0.0.tgz", + "integrity": "sha512-b1zcdxRm13HDgNfHnRpET7E3r1NlSK+VpQhN9dKn8aUUH71Ug6rNmgASYh7WrHDZLDqSXMHryfs5Aw0ufMtBYw==", + "dependencies": { + "maxmind": "^4.2.0" + } + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.13.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", + "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-europe-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", + "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ] + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/is-standalone-pwa": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", + "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ] + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/maxmind": { + "version": "4.3.24", + "resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.24.tgz", + "integrity": "sha512-dexrLcjfS2xDGOvdV8XcfQYmyQVpGidMwEG2ld19lXlsB+i+lXRWPzQi81HfwRXR4hxzFr5gT0oAIFyqAAb/Ww==", + "dependencies": { + "mmdb-lib": "2.1.1", + "tiny-lru": "11.2.11" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mmdb-lib": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-2.1.1.tgz", + "integrity": "sha512-yx8H/1H5AfnufiLnzzPqPf4yr/dKU9IFT1rPVwSkrKWHsQEeVVd6+X+L0nUbXhlEFTu3y/7hu38CFmEVgzvyeg==", + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/redis": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.0", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/tiny-lru": { + "version": "11.2.11", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.2.11.tgz", + "integrity": "sha512-27BIW0dIWTYYoWNnqSmoNMKe5WIbkXsc0xaCQHd3/3xT2XMuMJrzHdrO9QBFR14emBz1Bu0dOAs2sCBBrvgPQA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ua-is-frozen": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz", + "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ] + }, + "node_modules/ua-parser-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.3.tgz", + "integrity": "sha512-LZyXZdNttONW8LjzEH3Z8+6TE7RfrEiJqDKyh0R11p/kxvrV2o9DrT2FGZO+KVNs3k+drcIQ6C3En6wLnzJGpw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "dependencies": { + "@types/node-fetch": "^2.6.12", + "detect-europe-js": "^0.1.2", + "is-standalone-pwa": "^0.1.1", + "node-fetch": "^2.7.0", + "ua-is-frozen": "^0.1.2" + }, + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uWebSockets.js": { + "version": "20.51.0", + "resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#6609a88ffa9a16ac5158046761356ce03250a0df", + "license": "Apache-2.0" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/ee/assist-server/package.json b/ee/assist-server/package.json new file mode 100644 index 000000000..50c1fbf95 --- /dev/null +++ b/ee/assist-server/package.json @@ -0,0 +1,24 @@ +{ + "name": "assist-server", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@fastify/deepmerge": "^3.0.0", + "@maxmind/geoip2-node": "^6.0.0", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.2", + "redis": "^4.7.0", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", + "ua-parser-js": "^2.0.3", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.51.0", + "winston": "^3.17.0" + } +} diff --git a/ee/assist-server/server.js b/ee/assist-server/server.js new file mode 100644 index 000000000..b6e5dc34a --- /dev/null +++ b/ee/assist-server/server.js @@ -0,0 +1,75 @@ +const { App } = require('uWebSockets.js'); +const { Server } = require('socket.io'); +const { logger } = require("./app/logger"); +const { authorizer } = require("./app/assist"); +const { onConnect, setSocketIOServer } = require("./app/socket"); +const { startCacheRefresher } = require("./app/cache"); + +// Create uWebSockets.js app +const app = App(); +const prefix = process.env.PREFIX || process.env.prefix || `/assist`; +const pingInterval = parseInt(process.env.PING_INTERVAL) || 5000; + +const getCompressionConfig = function () { + // WS: The theoretical overhead per socket is 19KB (11KB for compressor and 8KB for decompressor) + let perMessageDeflate = false; + if (process.env.COMPRESSION === "true") { + logger.info(`WS compression: enabled`); + perMessageDeflate = { + zlibDeflateOptions: { + windowBits: 10, + memLevel: 1 + }, + zlibInflateOptions: { + windowBits: 10 + } + } + } else { + logger.info(`WS compression: disabled`); + } + return { + perMessageDeflate: perMessageDeflate, + clientNoContextTakeover: true + }; +} + +// Create a Socket.IO server with uWebSockets.js adapter +const io = new Server({ + maxHttpBufferSize: (parseFloat(process.env.maxHttpBufferSize) || 5) * 1e6, + pingInterval: pingInterval, // Will use it for cache invalidation + cors: { + origin: "*", // Allow connections from any origin (for development) + methods: ["GET", "POST"], + credentials: true + }, + path: (prefix ? prefix : '') + '/socket', + ...getCompressionConfig() +}); + +// Middleware for Socket.IO to check authorization +io.use(async (socket, next) => await authorizer.check(socket, next)); +// Socket.IO connection handler +io.on('connection', (socket) => onConnect(socket)); +// Attach Socket.IO to uWebSockets.js +io.attachApp(app); +io.engine.on("headers", (headers) => { + headers["x-host-id"] = process.env.HOSTNAME || "unknown"; +}); +setSocketIOServer(io); + +// Start the server +const PORT = process.env.PORT || 3000; +app.listen(PORT, (token) => { + if (token) { + console.log(`Server running at http://localhost:${PORT}`); + } else { + console.log(`Failed to listen on port ${PORT}`); + } +}); + +startCacheRefresher(io); + +// Error handling for uncaught exceptions +process.on('uncaughtException', err => { + logger.error(`Uncaught Exception: ${err}`); +}); \ No newline at end of file diff --git a/scripts/helmcharts/openreplay/charts/assist-server/.helmignore b/scripts/helmcharts/openreplay/charts/assist-server/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/scripts/helmcharts/openreplay/charts/assist-server/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/scripts/helmcharts/openreplay/charts/assist-server/Chart.yaml b/scripts/helmcharts/openreplay/charts/assist-server/Chart.yaml new file mode 100644 index 000000000..57014cf36 --- /dev/null +++ b/scripts/helmcharts/openreplay/charts/assist-server/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: assist-server +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful assist-server or functions for the chart developer. They're included as +# a dependency of application charts to inject those assist-server and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.1 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +AppVersion: "v1.22.0" diff --git a/scripts/helmcharts/openreplay/charts/assist-server/templates/NOTES.txt b/scripts/helmcharts/openreplay/charts/assist-server/templates/NOTES.txt new file mode 100644 index 000000000..e23331c6d --- /dev/null +++ b/scripts/helmcharts/openreplay/charts/assist-server/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "assist-server.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "assist-server.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "assist-server.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "assist-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/scripts/helmcharts/openreplay/charts/assist-server/templates/_helpers.tpl b/scripts/helmcharts/openreplay/charts/assist-server/templates/_helpers.tpl new file mode 100644 index 000000000..1b621eb3d --- /dev/null +++ b/scripts/helmcharts/openreplay/charts/assist-server/templates/_helpers.tpl @@ -0,0 +1,65 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "assist-server.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "assist-server.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "assist-server.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "assist-server.labels" -}} +helm.sh/chart: {{ include "assist-server.chart" . }} +{{ include "assist-server.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- if .Values.global.appLabels }} +{{- .Values.global.appLabels | toYaml | nindent 0}} +{{- end}} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "assist-server.selectorLabels" -}} +app.kubernetes.io/name: {{ include "assist-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "assist-server.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "assist-server.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/scripts/helmcharts/openreplay/charts/assist-server/templates/deployment.yaml b/scripts/helmcharts/openreplay/charts/assist-server/templates/deployment.yaml new file mode 100644 index 000000000..2c65cc188 --- /dev/null +++ b/scripts/helmcharts/openreplay/charts/assist-server/templates/deployment.yaml @@ -0,0 +1,113 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "assist-server.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "assist-server.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "assist-server.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "assist-server.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "assist-server.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + shareProcessNamespace: true + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + {{- if .Values.global.enterpriseEditionLicense }} + image: "{{ tpl .Values.image.repository . }}:{{ .Values.image.tag | default .Chart.AppVersion }}-ee" + {{- else }} + image: "{{ tpl .Values.image.repository . }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + {{- end }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.healthCheck}} + {{- .Values.healthCheck | toYaml | nindent 10}} + {{- end}} + env: + - name: ASSIST_JWT_SECRET + value: {{ .Values.global.assist-serverJWTSecret }} + - name: ASSIST_KEY + value: {{ .Values.global.assist-serverKey }} + - name: AWS_DEFAULT_REGION + value: "{{ .Values.global.s3.region }}" + - name: S3_HOST + {{- if contains "minio" .Values.global.s3.endpoint }} + value: '{{ ternary "https" "http" .Values.global.ORSecureAccess}}://{{ .Values.global.domainName }}:{{ ternary .Values.global.ingress.controller.service.ports.https .Values.global.ingress.controller.service.ports.http .Values.global.ORSecureAccess }}' + {{- else}} + value: '{{ .Values.global.s3.endpoint }}' + {{- end}} + - name: S3_KEY + {{- if .Values.global.s3.existingSecret }} + valueFrom: + secretKeyRef: + name: {{ .Values.global.s3.existingSecret }} + key: access-key + {{- else }} + value: {{ .Values.global.s3.accessKey }} + {{- end }} + - name: S3_SECRET + {{- if .Values.global.s3.existingSecret }} + valueFrom: + secretKeyRef: + name: {{ .Values.global.s3.existingSecret }} + key: secret-key + {{- else }} + value: {{ .Values.global.s3.secretKey }} + {{- end }} + - name: REDIS_URL + value: {{ .Values.global.redis.redisHost }} + {{- range $key, $val := .Values.global.env }} + - name: {{ $key }} + value: '{{ $val }}' + {{- end }} + {{- range $key, $val := .Values.env }} + - name: {{ $key }} + value: '{{ $val }}' + {{- end}} + ports: + {{- range $key, $val := .Values.service.ports }} + - name: {{ $key }} + containerPort: {{ $val }} + {{- end }} + protocol: TCP + {{- with .Values.persistence.mounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.persistence.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/scripts/helmcharts/openreplay/charts/assist-server/templates/hpa.yaml b/scripts/helmcharts/openreplay/charts/assist-server/templates/hpa.yaml new file mode 100644 index 000000000..80f270bc4 --- /dev/null +++ b/scripts/helmcharts/openreplay/charts/assist-server/templates/hpa.yaml @@ -0,0 +1,33 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "assist-server.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "assist-server.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "assist-server.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml b/scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml new file mode 100644 index 000000000..7811676c1 --- /dev/null +++ b/scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml @@ -0,0 +1,56 @@ +{{- if .Values.ingress.enabled }} +{{- $fullName := include "assist-server.fullname" . -}} +{{- $socketioSvcPort := .Values.service.ports.socketio -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "assist-server.labels" . | nindent 4 }} + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$1 + nginx.ingress.kubernetes.io/configuration-snippet: | + #set $sticky_used "no"; + #if ($sessionid != "") { + # set $sticky_used "yes"; + #} + + #add_header X-Debug-Session-ID $sessionid; + #add_header X-Debug-Session-Type "wss"; + #add_header X-Sticky-Session-Used $sticky_used; + #add_header X-Upstream-Server $upstream_addr; + + proxy_hide_header access-control-allow-headers; + proxy_hide_header Access-Control-Allow-Origin; + add_header 'Access-Control-Allow-Origin' $http_origin always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'sessionid, Content-Type, Authorization' always; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + + nginx.ingress.kubernetes.io/upstream-hash-by: $sessionid + + {{- with .Values.ingress.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + ingressClassName: "{{ tpl .Values.ingress.className . }}" + tls: + - hosts: + - {{ .Values.global.domainName }} + {{- if .Values.ingress.tls.secretName}} + secretName: {{ .Values.ingress.tls.secretName }} + {{- end}} + rules: + - host: {{ .Values.global.domainName }} + http: + paths: + - pathType: Prefix + backend: + service: + name: {{ $fullName }} + port: + number: {{ $socketioSvcPort }} + path: /ws-assist-server/(.*) +{{- end }} diff --git a/scripts/helmcharts/openreplay/charts/assist-server/templates/service.yaml b/scripts/helmcharts/openreplay/charts/assist-server/templates/service.yaml new file mode 100644 index 000000000..cf23f4676 --- /dev/null +++ b/scripts/helmcharts/openreplay/charts/assist-server/templates/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "assist-server.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "assist-server.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + {{- range $key, $val := .Values.service.ports }} + - port: {{ $val }} + targetPort: {{ $key }} + protocol: TCP + name: {{ $key }} + {{- end}} + selector: + {{- include "assist-server.selectorLabels" . | nindent 4 }} diff --git a/scripts/helmcharts/openreplay/charts/assist-server/templates/serviceMonitor.yaml b/scripts/helmcharts/openreplay/charts/assist-server/templates/serviceMonitor.yaml new file mode 100644 index 000000000..6fa157aff --- /dev/null +++ b/scripts/helmcharts/openreplay/charts/assist-server/templates/serviceMonitor.yaml @@ -0,0 +1,18 @@ +{{- if and ( .Capabilities.APIVersions.Has "monitoring.coreos.com/v1" ) ( .Values.serviceMonitor.enabled ) }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "assist-server.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "assist-server.labels" . | nindent 4 }} + {{- if .Values.serviceMonitor.additionalLabels }} + {{- toYaml .Values.serviceMonitor.additionalLabels | nindent 4 }} + {{- end }} +spec: + endpoints: + {{- .Values.serviceMonitor.scrapeConfigs | toYaml | nindent 4 }} + selector: + matchLabels: + {{- include "assist-server.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/scripts/helmcharts/openreplay/charts/assist-server/templates/serviceaccount.yaml b/scripts/helmcharts/openreplay/charts/assist-server/templates/serviceaccount.yaml new file mode 100644 index 000000000..eb5d75858 --- /dev/null +++ b/scripts/helmcharts/openreplay/charts/assist-server/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "assist-server.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "assist-server.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/scripts/helmcharts/openreplay/charts/assist-server/values.yaml b/scripts/helmcharts/openreplay/charts/assist-server/values.yaml new file mode 100644 index 000000000..a2db60b69 --- /dev/null +++ b/scripts/helmcharts/openreplay/charts/assist-server/values.yaml @@ -0,0 +1,134 @@ +# Default values for openreplay. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: "{{ .Values.global.openReplayContainerRegistry }}/assist-server" + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "assist-server" +fullnameOverride: "assist-server-openreplay" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +securityContext: + runAsUser: 1001 + runAsGroup: 1001 +podSecurityContext: + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + fsGroupChangePolicy: "OnRootMismatch" +# podSecurityContext: {} + # fsGroup: 2000 + +# securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +#service: +# type: ClusterIP +# port: 9000 + +serviceMonitor: + enabled: false + additionalLabels: + release: observability + scrapeConfigs: + - port: metrics + honorLabels: true + interval: 15s + path: /metrics + scheme: http + scrapeTimeout: 10s + +service: + type: ClusterIP + ports: + socketio: 9001 + metrics: 8888 + +ingress: + enabled: true + className: "{{ .Values.global.ingress.controller.ingressClassResource.name }}" + annotations: + nginx.ingress.kubernetes.io/configuration-snippet: | + add_header X-Debug-Session-ID $http_sessionid; + add_header X-Debug-Session-Type "wss"; + + # CORS configuration + # We don't need the upstream header + proxy_hide_header Access-Control-Allow-Origin; + add_header 'Access-Control-Allow-Origin' $http_origin always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'sessionid, Content-Type, Authorization' always; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + tls: + secretName: openreplay-ssl + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +env: + debug: 0 + uws: false + redis: false + CLEAR_SOCKET_TIME: 720 + + +nodeSelector: {} + +tolerations: [] + +affinity: {} + + +persistence: {} + # # Spec of spec.template.spec.containers[*].volumeMounts + # mounts: + # - name: kafka-ssl + # mountPath: /opt/kafka/ssl + # # Spec of spec.template.spec.volumes + # volumes: + # - name: kafka-ssl + # secret: + # secretName: kafka-ssl From dc975bc19ad440bfd7e83bfbafe5eeb4fd8cf121 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 12:11:48 +0200 Subject: [PATCH 03/15] feat(actions): small fix in assist-server action --- .github/workflows/assist-server-ee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/assist-server-ee.yaml b/.github/workflows/assist-server-ee.yaml index 653495cc6..184a0d8ca 100644 --- a/.github/workflows/assist-server-ee.yaml +++ b/.github/workflows/assist-server-ee.yaml @@ -102,7 +102,7 @@ jobs: done - name: Deploy to kubernetes run: | - cd ../../scripts/helmcharts/ + cd ../scripts/helmcharts/ # Update changed image tag sed -i "/assist-server/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml From 8b3be469b666839c9197151b3d99876142a371ca Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 15:09:37 +0200 Subject: [PATCH 04/15] feat(assist-server): added host configuration --- ee/assist-server/server.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ee/assist-server/server.js b/ee/assist-server/server.js index b6e5dc34a..891b74f22 100644 --- a/ee/assist-server/server.js +++ b/ee/assist-server/server.js @@ -33,7 +33,6 @@ const getCompressionConfig = function () { }; } -// Create a Socket.IO server with uWebSockets.js adapter const io = new Server({ maxHttpBufferSize: (parseFloat(process.env.maxHttpBufferSize) || 5) * 1e6, pingInterval: pingInterval, // Will use it for cache invalidation @@ -46,22 +45,19 @@ const io = new Server({ ...getCompressionConfig() }); -// Middleware for Socket.IO to check authorization io.use(async (socket, next) => await authorizer.check(socket, next)); -// Socket.IO connection handler io.on('connection', (socket) => onConnect(socket)); -// Attach Socket.IO to uWebSockets.js io.attachApp(app); io.engine.on("headers", (headers) => { headers["x-host-id"] = process.env.HOSTNAME || "unknown"; }); setSocketIOServer(io); -// Start the server +const HOST = process.env.LISTEN_HOST || '0.0.0.0'; const PORT = process.env.PORT || 3000; app.listen(PORT, (token) => { if (token) { - console.log(`Server running at http://localhost:${PORT}`); + console.log(`Server running at http://${HOST}:${PORT}`); } else { console.log(`Failed to listen on port ${PORT}`); } From f70cce7e2348c772b679bd3abfde5a859b8ec688 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 15:13:45 +0200 Subject: [PATCH 05/15] feat(assist-server): removed unnecessary comments --- .github/workflows/assist-server-ee.yaml | 1 + ee/assist-server/server.js | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/assist-server-ee.yaml b/.github/workflows/assist-server-ee.yaml index 184a0d8ca..b098dec9f 100644 --- a/.github/workflows/assist-server-ee.yaml +++ b/.github/workflows/assist-server-ee.yaml @@ -102,6 +102,7 @@ jobs: done - name: Deploy to kubernetes run: | + pwd cd ../scripts/helmcharts/ # Update changed image tag diff --git a/ee/assist-server/server.js b/ee/assist-server/server.js index 891b74f22..e7425027e 100644 --- a/ee/assist-server/server.js +++ b/ee/assist-server/server.js @@ -5,7 +5,6 @@ const { authorizer } = require("./app/assist"); const { onConnect, setSocketIOServer } = require("./app/socket"); const { startCacheRefresher } = require("./app/cache"); -// Create uWebSockets.js app const app = App(); const prefix = process.env.PREFIX || process.env.prefix || `/assist`; const pingInterval = parseInt(process.env.PING_INTERVAL) || 5000; @@ -62,10 +61,8 @@ app.listen(PORT, (token) => { console.log(`Failed to listen on port ${PORT}`); } }); - startCacheRefresher(io); -// Error handling for uncaught exceptions process.on('uncaughtException', err => { logger.error(`Uncaught Exception: ${err}`); }); \ No newline at end of file From b55e44d45007806225acfbcdaf00b27d4644c1db Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 15:44:19 +0200 Subject: [PATCH 06/15] feat(assist-server): moved the build.sh script to the root --- .github/workflows/assist-server-ee.yaml | 4 ++-- {ee/assist-server => assist-server}/build.sh | 18 +++++++----------- ee/assist-server/server.js | 2 ++ 3 files changed, 11 insertions(+), 13 deletions(-) rename {ee/assist-server => assist-server}/build.sh (87%) diff --git a/.github/workflows/assist-server-ee.yaml b/.github/workflows/assist-server-ee.yaml index b098dec9f..62f857f9d 100644 --- a/.github/workflows/assist-server-ee.yaml +++ b/.github/workflows/assist-server-ee.yaml @@ -61,7 +61,7 @@ jobs: ENVIRONMENT: staging run: | skip_security_checks=${{ github.event.inputs.skip_security_checks }} - cd ee/assist-server + cd assist-server PUSH_IMAGE=0 bash -x ./build.sh ee [[ "x$skip_security_checks" == "xtrue" ]] || { curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./ @@ -103,7 +103,7 @@ jobs: - name: Deploy to kubernetes run: | pwd - cd ../scripts/helmcharts/ + cd scripts/helmcharts/ # Update changed image tag sed -i "/assist-server/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml diff --git a/ee/assist-server/build.sh b/assist-server/build.sh similarity index 87% rename from ee/assist-server/build.sh rename to assist-server/build.sh index 0aaaaaadd..abd26d5bc 100644 --- a/ee/assist-server/build.sh +++ b/assist-server/build.sh @@ -2,6 +2,7 @@ # Usage: IMAGE_TAG=latest DOCKER_REPO=myDockerHubID bash build.sh +ARCH=${ARCH:-amd64} git_sha=$(git rev-parse --short HEAD) image_tag=${IMAGE_TAG:-git_sha} check_prereq() { @@ -12,12 +13,9 @@ check_prereq() { } source ../scripts/lib/_docker.sh -[[ $1 == ee ]] && ee=true [[ $PATCH -eq 1 ]] && { image_tag="$(grep -ER ^.ppVersion ../scripts/helmcharts/openreplay/charts/$chart | xargs | awk '{print $2}' | awk -F. -v OFS=. '{$NF += 1 ; print}')" - [[ $ee == "true" ]] && { - image_tag="${image_tag}-ee" - } + image_tag="${image_tag}-ee" } update_helm_release() { chart=$1 @@ -32,20 +30,18 @@ update_helm_release() { } function build_api() { - destination="_assist-server" - [[ $1 == "ee" ]] && { - destination="_assist-server_ee" - } + destination="_assist-server_ee" [[ -d ../${destination} ]] && { echo "Removing previous build cache" rm -rf ../${destination} } - cp -R ../ee/assist-server ../${destination} - cd ../${destination} + cp -R ../assist-server ../${destination} + cd ../${destination} || exit 1 + cp -rf ../ee/assist-server/* ./ docker build -f ./Dockerfile --build-arg GIT_SHA=$git_sha -t ${DOCKER_REPO:-'local'}/assist-server:${image_tag} . - cd ../assist + cd ../assist-server || exit 1 rm -rf ../${destination} [[ $PUSH_IMAGE -eq 1 ]] && { docker push ${DOCKER_REPO:-'local'}/assist-server:${image_tag} diff --git a/ee/assist-server/server.js b/ee/assist-server/server.js index e7425027e..76d635e0f 100644 --- a/ee/assist-server/server.js +++ b/ee/assist-server/server.js @@ -50,6 +50,7 @@ io.attachApp(app); io.engine.on("headers", (headers) => { headers["x-host-id"] = process.env.HOSTNAME || "unknown"; }); + setSocketIOServer(io); const HOST = process.env.LISTEN_HOST || '0.0.0.0'; @@ -61,6 +62,7 @@ app.listen(PORT, (token) => { console.log(`Failed to listen on port ${PORT}`); } }); + startCacheRefresher(io); process.on('uncaughtException', err => { From 0141a429115c2f121a990d32a015300f08f76db2 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 15:48:31 +0200 Subject: [PATCH 07/15] feat(assist-server): fixed the helm chart --- ee/assist-server/server.js | 1 - .../openreplay/charts/assist-server/templates/deployment.yaml | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ee/assist-server/server.js b/ee/assist-server/server.js index 76d635e0f..bf05b959f 100644 --- a/ee/assist-server/server.js +++ b/ee/assist-server/server.js @@ -50,7 +50,6 @@ io.attachApp(app); io.engine.on("headers", (headers) => { headers["x-host-id"] = process.env.HOSTNAME || "unknown"; }); - setSocketIOServer(io); const HOST = process.env.LISTEN_HOST || '0.0.0.0'; diff --git a/scripts/helmcharts/openreplay/charts/assist-server/templates/deployment.yaml b/scripts/helmcharts/openreplay/charts/assist-server/templates/deployment.yaml index 2c65cc188..102a6a8d8 100644 --- a/scripts/helmcharts/openreplay/charts/assist-server/templates/deployment.yaml +++ b/scripts/helmcharts/openreplay/charts/assist-server/templates/deployment.yaml @@ -44,9 +44,9 @@ spec: {{- end}} env: - name: ASSIST_JWT_SECRET - value: {{ .Values.global.assist-serverJWTSecret }} + value: {{ .Values.global.assistJWTSecret }} - name: ASSIST_KEY - value: {{ .Values.global.assist-serverKey }} + value: {{ .Values.global.assistKey }} - name: AWS_DEFAULT_REGION value: "{{ .Values.global.s3.region }}" - name: S3_HOST From 09c2ce097619bcf05e8885dd0cd4e8eb9b9f523e Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Fri, 4 Apr 2025 12:40:29 +0200 Subject: [PATCH 08/15] ci(action): Build and patch github tags feat(workflow): update commit timestamp for patching Add a step to set the commit timestamp of the HEAD commit to be 1 second newer than the oldest of the last 3 commits. This ensures proper chronological order while preserving the commit content. - Fetch deeper history to access commit history - Get oldest timestamp from recent commits - Set new commit date with BSD-compatible date command - Verify timestamp change with git log The workflow was previously checking out 'main' branch with a comment indicating it needed to be fixed. This change makes it properly checkout the tag specified by the workflow input. Signed-off-by: rjshrjndrn --- .github/workflows/patch-build-old.yaml | 185 +++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 .github/workflows/patch-build-old.yaml diff --git a/.github/workflows/patch-build-old.yaml b/.github/workflows/patch-build-old.yaml new file mode 100644 index 000000000..90a1fc1cb --- /dev/null +++ b/.github/workflows/patch-build-old.yaml @@ -0,0 +1,185 @@ +# Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions + +on: + workflow_dispatch: + inputs: + services: + description: 'Comma separated names of services to build(in small letters).' + required: true + default: 'chalice,frontend' + tag: + description: 'Tag to build patches from.' + required: true + type: string + +name: Build patches from tag, rewrite commit HEAD to older timestamp, and Push the tag + +jobs: + deploy: + name: Build Patch from old tag + runs-on: ubuntu-latest + env: + DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }} + DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }} + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 4 + ref: ${{ github.event.inputs.tag }} + + - name: Set Remote with GITHUB_TOKEN + run: | + git config --unset http.https://github.com/.extraheader + git remote set-url origin https://x-access-token:${{ secrets.ACTIONS_COMMMIT_TOKEN }}@github.com/${{ github.repository }}.git + + - name: Create backup tag with timestamp + run: | + set -e # Exit immediately if a command exits with a non-zero status + TIMESTAMP=$(date +%Y%m%d%H%M%S) + BACKUP_TAG="${{ github.event.inputs.tag }}-backup-${TIMESTAMP}" + echo "BACKUP_TAG=${BACKUP_TAG}" >> $GITHUB_ENV + echo "INPUT_TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV + git tag $BACKUP_TAG || { echo "Failed to create backup tag"; exit 1; } + git push origin $BACKUP_TAG || { echo "Failed to push backup tag"; exit 1; } + echo "Created backup tag: $BACKUP_TAG" + + # Get the oldest commit date from the last 3 commits in raw format + OLDEST_COMMIT_TIMESTAMP=$(git log -3 --pretty=format:"%at" | tail -1) + echo "Oldest commit timestamp: $OLDEST_COMMIT_TIMESTAMP" + # Add 1 second to the timestamp + NEW_TIMESTAMP=$((OLDEST_COMMIT_TIMESTAMP + 1)) + echo "NEW_TIMESTAMP=$NEW_TIMESTAMP" >> $GITHUB_ENV + + + - name: Setup yq + uses: mikefarah/yq@master + + # Configure AWS credentials for the first registry + - name: Configure AWS credentials for RELEASE_ARM_REGISTRY + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_DEPOT_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_DEPOT_SECRET_KEY }} + aws-region: ${{ secrets.AWS_DEPOT_DEFAULT_REGION }} + + - name: Login to Amazon ECR for RELEASE_ARM_REGISTRY + id: login-ecr-arm + run: | + aws ecr get-login-password --region ${{ secrets.AWS_DEPOT_DEFAULT_REGION }} | docker login --username AWS --password-stdin ${{ secrets.RELEASE_ARM_REGISTRY }} + aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.RELEASE_OSS_REGISTRY }} + + - uses: depot/setup-action@v1 + - name: Get HEAD Commit ID + run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV + - name: Define Branch Name + run: echo "BRANCH_NAME=patch/main/${HEAD_COMMIT_ID}" >> $GITHUB_ENV + + - name: Build + id: build-image + env: + DOCKER_REPO_ARM: ${{ secrets.RELEASE_ARM_REGISTRY }} + DOCKER_REPO_OSS: ${{ secrets.RELEASE_OSS_REGISTRY }} + MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }} + MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }} + MSAAS_REPO_FOLDER: /tmp/msaas + run: | + set -exo pipefail + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git checkout -b $BRANCH_NAME + working_dir=$(pwd) + function image_version(){ + local service=$1 + chart_path="$working_dir/scripts/helmcharts/openreplay/charts/$service/Chart.yaml" + current_version=$(yq eval '.AppVersion' $chart_path) + new_version=$(echo $current_version | awk -F. '{$NF += 1 ; print $1"."$2"."$3}') + echo $new_version + # yq eval ".AppVersion = \"$new_version\"" -i $chart_path + } + function clone_msaas() { + [ -d $MSAAS_REPO_FOLDER ] || { + git clone -b dev --recursive https://x-access-token:$MSAAS_REPO_CLONE_TOKEN@$MSAAS_REPO_URL $MSAAS_REPO_FOLDER + cd $MSAAS_REPO_FOLDER + cd openreplay && git fetch origin && git checkout $INPUT_TAG + git log -1 + cd $MSAAS_REPO_FOLDER + bash git-init.sh + git checkout + } + } + function build_managed() { + local service=$1 + local version=$2 + echo building managed + clone_msaas + if [[ $service == 'chalice' ]]; then + cd $MSAAS_REPO_FOLDER/openreplay/api + else + cd $MSAAS_REPO_FOLDER/openreplay/$service + fi + IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash build.sh >> /tmp/arm.txt + } + # Checking for backend images + ls backend/cmd >> /tmp/backend.txt + echo Services: "${{ github.event.inputs.services }}" + IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}" + BUILD_SCRIPT_NAME="build.sh" + # Build FOSS + for SERVICE in "${SERVICES[@]}"; do + # Check if service is backend + if grep -q $SERVICE /tmp/backend.txt; then + cd backend + foss_build_args="nil $SERVICE" + ee_build_args="ee $SERVICE" + else + [[ $SERVICE == 'chalice' || $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && cd $working_dir/api || cd $SERVICE + [[ $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && BUILD_SCRIPT_NAME="build_${SERVICE}.sh" + ee_build_args="ee" + fi + version=$(image_version $SERVICE) + echo IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args + IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args + echo IMAGE_TAG=$version-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args + IMAGE_TAG=$version-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args + if [[ "$SERVICE" != "chalice" && "$SERVICE" != "frontend" ]]; then + IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args + echo IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args + else + build_managed $SERVICE $version + fi + cd $working_dir + chart_path="$working_dir/scripts/helmcharts/openreplay/charts/$SERVICE/Chart.yaml" + yq eval ".AppVersion = \"$version\"" -i $chart_path + git add $chart_path + git commit -m "Increment $SERVICE chart version" + done + + - name: Change commit timestamp + run: | + # Convert the timestamp to a date format git can understand + NEW_DATE=$(perl -le 'print scalar gmtime($ARGV[0])." +0000"' $NEW_TIMESTAMP) + echo "Setting commit date to: $NEW_DATE" + + # Amend the commit with the new date + GIT_COMMITTER_DATE="$NEW_DATE" git commit --amend --no-edit --date="$NEW_DATE" + + # Verify the change + git log -1 --pretty=format:"Commit now dated: %cD" + + # git tag and push + git tag $INPUT_TAG -f + git push origin $INPUT_TAG -f + + + # - name: Debug Job + # if: ${{ failure() }} + # uses: mxschmitt/action-tmate@v3 + # env: + # DOCKER_REPO_ARM: ${{ secrets.RELEASE_ARM_REGISTRY }} + # DOCKER_REPO_OSS: ${{ secrets.RELEASE_OSS_REGISTRY }} + # MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }} + # MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }} + # MSAAS_REPO_FOLDER: /tmp/msaas + # with: + # limit-access-to-actor: true From d9ff3f469179717d61cde0d3d8e355fdf4a215d6 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 16:08:09 +0200 Subject: [PATCH 09/15] feat(assist-server): use the default prefix url --- ee/assist-server/server.js | 1 - .../openreplay/charts/assist-server/templates/ingress.yaml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ee/assist-server/server.js b/ee/assist-server/server.js index bf05b959f..e7425027e 100644 --- a/ee/assist-server/server.js +++ b/ee/assist-server/server.js @@ -61,7 +61,6 @@ app.listen(PORT, (token) => { console.log(`Failed to listen on port ${PORT}`); } }); - startCacheRefresher(io); process.on('uncaughtException', err => { diff --git a/scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml b/scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml index 7811676c1..222115b0a 100644 --- a/scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml +++ b/scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml @@ -52,5 +52,5 @@ spec: name: {{ $fullName }} port: number: {{ $socketioSvcPort }} - path: /ws-assist-server/(.*) + path: /ws-assist/(.*) {{- end }} From 57713238005ce9c202c78e486dc2b75b99333f8f Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 16:13:03 +0200 Subject: [PATCH 10/15] feat(assist): temporary changed the default assist path --- .../helmcharts/openreplay/charts/assist/templates/ingress.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/helmcharts/openreplay/charts/assist/templates/ingress.yaml b/scripts/helmcharts/openreplay/charts/assist/templates/ingress.yaml index 41436c679..5b7feef65 100644 --- a/scripts/helmcharts/openreplay/charts/assist/templates/ingress.yaml +++ b/scripts/helmcharts/openreplay/charts/assist/templates/ingress.yaml @@ -52,5 +52,5 @@ spec: name: {{ $fullName }} port: number: {{ $socketioSvcPort }} - path: /ws-assist/(.*) + path: /ws-assist-old/(.*) {{- end }} From 77ae0cac0e6d845960710c7176bce73d61b8f99e Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 16:18:19 +0200 Subject: [PATCH 11/15] Revert "feat(assist): temporary changed the default assist path" This reverts commit 57713238005ce9c202c78e486dc2b75b99333f8f. --- .../helmcharts/openreplay/charts/assist/templates/ingress.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/helmcharts/openreplay/charts/assist/templates/ingress.yaml b/scripts/helmcharts/openreplay/charts/assist/templates/ingress.yaml index 5b7feef65..41436c679 100644 --- a/scripts/helmcharts/openreplay/charts/assist/templates/ingress.yaml +++ b/scripts/helmcharts/openreplay/charts/assist/templates/ingress.yaml @@ -52,5 +52,5 @@ spec: name: {{ $fullName }} port: number: {{ $socketioSvcPort }} - path: /ws-assist-old/(.*) + path: /ws-assist/(.*) {{- end }} From cbbd480cca651686193c329bcc76dde0295197f9 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 16:23:16 +0200 Subject: [PATCH 12/15] feat(assist-server): changed default port --- ee/assist-server/server.js | 2 +- .../openreplay/charts/assist-server/templates/ingress.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/assist-server/server.js b/ee/assist-server/server.js index e7425027e..0cc2a2b13 100644 --- a/ee/assist-server/server.js +++ b/ee/assist-server/server.js @@ -53,7 +53,7 @@ io.engine.on("headers", (headers) => { setSocketIOServer(io); const HOST = process.env.LISTEN_HOST || '0.0.0.0'; -const PORT = process.env.PORT || 3000; +const PORT = parseInt(process.env.PORT) || 9001; app.listen(PORT, (token) => { if (token) { console.log(`Server running at http://${HOST}:${PORT}`); diff --git a/scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml b/scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml index 222115b0a..7811676c1 100644 --- a/scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml +++ b/scripts/helmcharts/openreplay/charts/assist-server/templates/ingress.yaml @@ -52,5 +52,5 @@ spec: name: {{ $fullName }} port: number: {{ $socketioSvcPort }} - path: /ws-assist/(.*) + path: /ws-assist-server/(.*) {{- end }} From 36e1a2fca269780689820f32be47bc7f303bd632 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 16:34:45 +0200 Subject: [PATCH 13/15] feat(assist-server): removed unnecessary prefix for ws connections --- ee/assist-server/server.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ee/assist-server/server.js b/ee/assist-server/server.js index 0cc2a2b13..00b0e2f29 100644 --- a/ee/assist-server/server.js +++ b/ee/assist-server/server.js @@ -6,7 +6,6 @@ const { onConnect, setSocketIOServer } = require("./app/socket"); const { startCacheRefresher } = require("./app/cache"); const app = App(); -const prefix = process.env.PREFIX || process.env.prefix || `/assist`; const pingInterval = parseInt(process.env.PING_INTERVAL) || 5000; const getCompressionConfig = function () { @@ -40,7 +39,7 @@ const io = new Server({ methods: ["GET", "POST"], credentials: true }, - path: (prefix ? prefix : '') + '/socket', + path: '/socket', ...getCompressionConfig() }); From 9160b4211342f10b265416dfe48e8d8c27a8914a Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 4 Apr 2025 17:53:19 +0200 Subject: [PATCH 14/15] feat(assist-server): fixed an issue with sessionIDs collector --- ee/assist-server/app/cache.js | 23 +++++++++++++---------- ee/assist-server/server.js | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/ee/assist-server/app/cache.js b/ee/assist-server/app/cache.js index decb487d0..8a07e08dc 100644 --- a/ee/assist-server/app/cache.js +++ b/ee/assist-server/app/cache.js @@ -13,15 +13,18 @@ function generateNodeID() { return "node_"+buffer.readBigUInt64BE(0).toString(); } -const pingInterval = parseInt(process.env.PING_INTERVAL) || 25000; -const CACHE_REFRESH_INTERVAL = parseInt(process.env.cacheRefreshInterval) || 10000; +const PING_INTERVAL = parseInt(process.env.PING_INTERVAL_SECONDS) || 25; +const CACHE_REFRESH_INTERVAL = parseInt(process.env.CACHE_REFRESH_INTERVAL_SECONDS) || 10; +const pingInterval = PING_INTERVAL + PING_INTERVAL/2; +const cacheRefreshInterval = CACHE_REFRESH_INTERVAL + CACHE_REFRESH_INTERVAL/2; +const cacheRefreshIntervalMs = CACHE_REFRESH_INTERVAL * 1000; let lastCacheUpdateTime = 0; let cacheRefresher = null; const nodeID = process.env.HOSTNAME || generateNodeID(); const addSessionToCache = async function (sessionID, sessionData) { try { - await redisClient.set(`active_sessions:${sessionID}`, JSON.stringify(sessionData), 'EX', pingInterval*2); + await redisClient.set(`active_sessions:${sessionID}`, JSON.stringify(sessionData), 'EX', pingInterval); logger.debug(`Session ${sessionID} stored in Redis`); } catch (error) { logger.error(error); @@ -30,7 +33,7 @@ const addSessionToCache = async function (sessionID, sessionData) { const renewSession = async function (sessionID){ try { - await redisClient.expire(`active_sessions:${sessionID}`, pingInterval*2); + await redisClient.expire(`active_sessions:${sessionID}`, pingInterval); logger.debug(`Session ${sessionID} renewed in Redis`); } catch (error) { logger.error(error); @@ -62,7 +65,7 @@ const removeSessionFromCache = async function (sessionID) { const setNodeSessions = async function (nodeID, sessionIDs) { try { - await redisClient.set(`node:${nodeID}:sessions`, JSON.stringify(sessionIDs), 'EX', CACHE_REFRESH_INTERVAL*2); + await redisClient.set(`node:${nodeID}:sessions`, JSON.stringify(sessionIDs), 'EX', cacheRefreshInterval); logger.debug(`Node ${nodeID} sessions stored in Redis`); } catch (error) { logger.error(error); @@ -74,7 +77,7 @@ function startCacheRefresher(io) { cacheRefresher = setInterval(async () => { const now = Date.now(); - if (now - lastCacheUpdateTime < CACHE_REFRESH_INTERVAL) { + if (now - lastCacheUpdateTime < cacheRefreshIntervalMs) { return; } logger.debug('Background refresh triggered'); @@ -82,9 +85,9 @@ function startCacheRefresher(io) { const startTime = performance.now(); const sessionIDs = new Set(); const result = await io.fetchSockets(); - result.forEach((r) => { - if (r.handshake.query.sessionID) { - sessionIDs.add(r.handshake.query.sessionID); + result.forEach((socket) => { + if (socket.handshake.query.sessId) { + sessionIDs.add(socket.handshake.query.sessId); } }) await setNodeSessions(nodeID, Array.from(sessionIDs)); @@ -94,7 +97,7 @@ function startCacheRefresher(io) { } catch (error) { logger.error(`Background refresh error: ${error}`); } - }, CACHE_REFRESH_INTERVAL / 2); + }, cacheRefreshIntervalMs / 2); } module.exports = { diff --git a/ee/assist-server/server.js b/ee/assist-server/server.js index 00b0e2f29..c143ab0d4 100644 --- a/ee/assist-server/server.js +++ b/ee/assist-server/server.js @@ -6,7 +6,7 @@ const { onConnect, setSocketIOServer } = require("./app/socket"); const { startCacheRefresher } = require("./app/cache"); const app = App(); -const pingInterval = parseInt(process.env.PING_INTERVAL) || 5000; +const pingInterval = parseInt(process.env.PING_INTERVAL) || 25000; const getCompressionConfig = function () { // WS: The theoretical overhead per socket is 19KB (11KB for compressor and 8KB for decompressor) From 3f73bae22f1b33cfd3bf5c1b577899bedbeaebee Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Fri, 4 Apr 2025 23:37:59 +0200 Subject: [PATCH 15/15] fix(helm): proper aws endpoint detection Signed-off-by: rjshrjndrn --- .../openreplay/charts/assets/templates/deployment.yaml | 2 +- .../openreplay/charts/canvases/templates/deployment.yaml | 2 +- .../openreplay/charts/connector/templates/deployment.yaml | 2 +- .../openreplay/charts/images/templates/deployment.yaml | 2 +- .../openreplay/charts/storage/templates/deployment.yaml | 2 +- scripts/helmcharts/openreplay/templates/_helpers.tpl | 7 +++++-- scripts/helmcharts/openreplay/templates/job.yaml | 2 +- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/scripts/helmcharts/openreplay/charts/assets/templates/deployment.yaml b/scripts/helmcharts/openreplay/charts/assets/templates/deployment.yaml index ad45f0e70..825228e88 100644 --- a/scripts/helmcharts/openreplay/charts/assets/templates/deployment.yaml +++ b/scripts/helmcharts/openreplay/charts/assets/templates/deployment.yaml @@ -74,7 +74,7 @@ spec: - name: LICENSE_KEY value: '{{ .Values.global.enterpriseEditionLicense }}' - name: AWS_ENDPOINT - value: '{{ .Values.global.s3.endpoint }}' + value: '{{- include "openreplay.s3Endpoint" . }}' - name: AWS_REGION value: '{{ .Values.global.s3.region }}' - name: KAFKA_SERVERS diff --git a/scripts/helmcharts/openreplay/charts/canvases/templates/deployment.yaml b/scripts/helmcharts/openreplay/charts/canvases/templates/deployment.yaml index 428d58897..f160e50a9 100644 --- a/scripts/helmcharts/openreplay/charts/canvases/templates/deployment.yaml +++ b/scripts/helmcharts/openreplay/charts/canvases/templates/deployment.yaml @@ -62,7 +62,7 @@ spec: value: {{ .Values.global.s3.secretKey }} {{- end }} - name: AWS_ENDPOINT - value: '{{ .Values.global.s3.endpoint }}' + value: '{{- include "openreplay.s3Endpoint" . }}' - name: AWS_REGION value: '{{ .Values.global.s3.region }}' - name: BUCKET_NAME diff --git a/scripts/helmcharts/openreplay/charts/connector/templates/deployment.yaml b/scripts/helmcharts/openreplay/charts/connector/templates/deployment.yaml index d46405502..065ce1f09 100644 --- a/scripts/helmcharts/openreplay/charts/connector/templates/deployment.yaml +++ b/scripts/helmcharts/openreplay/charts/connector/templates/deployment.yaml @@ -83,7 +83,7 @@ spec: value: {{ .Values.global.s3.secretKey }} {{- end }} - name: AWS_ENDPOINT - value: '{{ .Values.global.s3.endpoint }}' + value: '{{- include "openreplay.s3Endpoint" . }}' - name: AWS_REGION value: '{{ .Values.global.s3.region }}' {{- include "openreplay.env.redis_string" .Values.global.redis | nindent 12 }} diff --git a/scripts/helmcharts/openreplay/charts/images/templates/deployment.yaml b/scripts/helmcharts/openreplay/charts/images/templates/deployment.yaml index 2a93d495d..38f4f24c4 100644 --- a/scripts/helmcharts/openreplay/charts/images/templates/deployment.yaml +++ b/scripts/helmcharts/openreplay/charts/images/templates/deployment.yaml @@ -62,7 +62,7 @@ spec: value: {{ .Values.global.s3.secretKey }} {{- end }} - name: AWS_ENDPOINT - value: '{{ .Values.global.s3.endpoint }}' + value: '{{- include "openreplay.s3Endpoint" . }}' - name: AWS_REGION value: '{{ .Values.global.s3.region }}' - name: BUCKET_NAME diff --git a/scripts/helmcharts/openreplay/charts/storage/templates/deployment.yaml b/scripts/helmcharts/openreplay/charts/storage/templates/deployment.yaml index bc5ea5a9c..8868c77fb 100644 --- a/scripts/helmcharts/openreplay/charts/storage/templates/deployment.yaml +++ b/scripts/helmcharts/openreplay/charts/storage/templates/deployment.yaml @@ -62,7 +62,7 @@ spec: value: {{ .Values.global.s3.secretKey }} {{- end }} - name: AWS_ENDPOINT - value: '{{ .Values.global.s3.endpoint }}' + value: '{{- include "openreplay.s3Endpoint" . }}' - name: AWS_REGION value: '{{ .Values.global.s3.region }}' - name: BUCKET_NAME diff --git a/scripts/helmcharts/openreplay/templates/_helpers.tpl b/scripts/helmcharts/openreplay/templates/_helpers.tpl index be8ef934e..de12340a9 100644 --- a/scripts/helmcharts/openreplay/templates/_helpers.tpl +++ b/scripts/helmcharts/openreplay/templates/_helpers.tpl @@ -33,10 +33,13 @@ ingress-nginx: &ingress-nginx {{- if contains "minio" .Values.global.s3.endpoint -}} {{- include "openreplay.domainURL" . -}} {{- else -}} - {{- .Values.global.s3.endpoint -}} + {{- .Values.global.s3.endpoint -}} {{- end -}} -{{- else -}} +{{/* Endpoint wil be empty if used with aws iam roles*/}} +{{- else if and .Values.global.s3.accessKey .Values.global.s3.secretKey -}} {{- printf "https://s3.%s.amazonaws.com" .Values.global.s3.region -}} +{{- else -}} + {{- .Values.global.s3.endpoint -}} {{- end -}} {{- end -}} diff --git a/scripts/helmcharts/openreplay/templates/job.yaml b/scripts/helmcharts/openreplay/templates/job.yaml index 1cf314514..e4d5463a8 100644 --- a/scripts/helmcharts/openreplay/templates/job.yaml +++ b/scripts/helmcharts/openreplay/templates/job.yaml @@ -404,7 +404,7 @@ spec: - name: AWS_DEFAULT_REGION value: "{{ .Values.global.s3.region }}" - name: AWS_ENDPOINT - value: "{{ .Values.global.s3.endpoint }}" + value: "{{- include "openreplay.s3Endpoint" . }}" - name: VAULT_BUCKET value: "{{ .Values.global.s3.vaultBucket }}" image: amazon/aws-cli