diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d992b0f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +.git \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 55c1e3b..7172e54 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ vite.config.js .gitignore -data \ No newline at end of file +data +mongo-init.js \ No newline at end of file diff --git a/.gitignore b/.gitignore index 42d9320..a389d25 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* +.bash_history +.viminfo +*.swp # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..43b79ff --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,21 @@ +FROM node + +WORKDIR /app +COPY package.json yarn.lock ./ +COPY server/package.json ./server/ +RUN yarn install --frozen-lockfile + +WORKDIR /app/server + +ARG DOCKER_GID=140 + +RUN groupadd -g $DOCKER_GID docker && usermod -aG docker node + +USER node + +ENV NODE_ENV=development +ENV HOST=0.0.0.0 +ENV PORT=3000 +EXPOSE 3000 + +CMD [ "yarn", "dev" ] diff --git a/cleanup.sh b/cleanup.sh new file mode 100644 index 0000000..09c66ad --- /dev/null +++ b/cleanup.sh @@ -0,0 +1,5 @@ +#!/bin/sh +containers=$(docker ps -a | grep 'quest-' | awk '{print $1}') +echo $containers + +docker rm -f $containers \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..4644ef6 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,42 @@ +version: '3' +services: + app: + image: lancatlin/linux-odyssey + restart: on-failure + build: + context: . + dockerfile: Dockerfile + networks: + - internal + - players + ports: + - '3000:3000' + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - MONGO_URL=mongodb://user:user-password@db:27017/odyssey-test + - DOCKER_NETWORK=linux-odyssey-players + db: + image: mongo + restart: always + networks: + - internal + ports: + - '27017:27017' + volumes: + - mongo:/data/db + - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro + environment: + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=admin-password + - MONGO_USER=user + - MONGO_PASS=user-password + - MONGO_DB=odyssey-test + +networks: + internal: + players: + name: linux-odyssey-players + +volumes: + mongo: diff --git a/docker-compose.yml b/docker-compose.yml index 8900c1a..8a03ed9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,32 @@ version: '3' services: app: - image: lancatlin/linux-odyssey-server - build: . + restart: on-failure + build: + context: . + dockerfile: Dockerfile.dev + networks: + - internal + - players ports: - '3000:3000' - links: - - db volumes: - /var/run/docker.sock:/var/run/docker.sock + - ./server:/app/server environment: - MONGO_URL=mongodb://db:27017/odyssey-test + - DOCKER_NETWORK=linux-odyssey-players db: image: mongo restart: always + networks: + - internal ports: - '27017:27017' volumes: - ./data:/data/db + +networks: + internal: + players: + name: linux-odyssey-players diff --git a/inspect.sh b/inspect.sh new file mode 100755 index 0000000..7f3c41d --- /dev/null +++ b/inspect.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker compose logs app -f --no-log-prefix diff --git a/mongo-init.js b/mongo-init.js new file mode 100644 index 0000000..9e4f467 --- /dev/null +++ b/mongo-init.js @@ -0,0 +1,10 @@ +const user = process.env.MONGO_USER +const password = process.env.MONGO_PASS +const dbName = process.env.MONGO_DB + +db = db.getSiblingDB(dbName) +db.createUser({ + user: user, + pwd: password, + roles: [{ role: 'readWrite', db: dbName }], +}) diff --git a/server/package.json b/server/package.json index 2c70405..6a40895 100644 --- a/server/package.json +++ b/server/package.json @@ -10,8 +10,10 @@ "test": "yarn node --experimental-vm-modules $(yarn bin jest)" }, "dependencies": { + "commander": "^11.0.0", "dockerode": "^3.3.5", "express": "^4.18.2", + "jsonwebtoken": "^9.0.1", "marked": "^5.1.1", "mongoose": "^7.4.0", "socket.io": "^4.7.1", diff --git a/server/quests/helloworld/Dockerfile b/server/quests/helloworld/Dockerfile new file mode 100644 index 0000000..01f3814 --- /dev/null +++ b/server/quests/helloworld/Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:latest + +RUN apt-get update && apt-get install -y \ + curl \ + vim \ + jq + +RUN adduser --disabled-password --gecos '' rudeus + +USER rudeus + +WORKDIR /home/rudeus + +COPY ./home /home/rudeus/ + +CMD [ "/bin/bash" ] diff --git a/server/quests/helloworld/home/.bashrc b/server/quests/helloworld/home/.bashrc new file mode 100644 index 0000000..3a0355f --- /dev/null +++ b/server/quests/helloworld/home/.bashrc @@ -0,0 +1,30 @@ +# This function will be called before each command is executed +preexec() { + [ -n "$COMP_LINE" ] && return # do nothing if completing + [ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return # do nothing if completing + local command="$BASH_COMMAND" + local pwd="$PWD" + + if [[ $command =~ ^(vim|top|less|more|nano) ]]; then + local payload=$(printf '{"token": "%s", "pwd": "%s", "command": "%s"}' "$TOKEN" "$pwd" "$command") + curl -X POST $API_ENDPOINT/api/v1/commands -H 'accept: */*' -H 'Content-Type: application/json' -d "$payload" + return + fi + + # Execute the command, capture output and exit code + local output=$( { eval $command 2>&1; echo $? > /tmp/exit_code; }) + local exit_code=$(cat /tmp/exit_code) + + local output_esc=$(echo $output | jq -Rs .) + + # Store command in variable + local payload=$(printf '{"token": "%s", "pwd": "%s", "command": "%s", "exit_code": "%s", "output": %s}' "$TOKEN" "$pwd" "$command" "$exit_code" "$output_esc") + + # Send command to the API + curl -X POST $API_ENDPOINT/api/v1/commands -H 'accept: */*' -H 'Content-Type: application/json' -d "$payload" || echo $payload + + BASH_COMMAND="" +} + +# Install the trap +trap 'preexec' DEBUG diff --git a/server/src/api/controllers/commandController.js b/server/src/api/controllers/commandController.js new file mode 100644 index 0000000..a886d23 --- /dev/null +++ b/server/src/api/controllers/commandController.js @@ -0,0 +1,37 @@ +/* eslint-disable import/prefer-default-export */ +import Command from '../../models/command.js' +import Session from '../../models/session.js' +import { verifySessionJWT } from '../../utils/auth.js' + +export async function newCommand(req, res) { + console.log('new command:', req.body) + const { token, command, pwd, exit_code, output } = req.body + + let sessionId + try { + const decoded = await verifySessionJWT(token) + sessionId = decoded.session_id + } catch (err) { + res.status(401).json({ message: 'token not valid' }) + return + } + + const session = await Session.findById(sessionId) + + if (!session) { + res.status(401).json({ message: 'session not found' }) + return + } + + const c = new Command({ + session, + command, + pwd, + exit_code, + output, + }) + + await c.save() + + res.status(201).end() +} diff --git a/server/src/api/controllers/sessionController.js b/server/src/api/controllers/sessionController.js index 19d431d..81e163f 100644 --- a/server/src/api/controllers/sessionController.js +++ b/server/src/api/controllers/sessionController.js @@ -22,13 +22,13 @@ export async function createSession(req, res) { return } - const newSession = new Session({ - user: req.user, - quest, - containerId: `quest-${quest.id}-${req.user.username}-${Date.now()}`, - }) - try { + const newSession = new Session({ + user: req.user, + quest, + containerId: `quest-${quest.id}-${req.user.username}-${Date.now()}`, + }) + const session = await newSession.save() res.status(201).json(session) } catch (err) { diff --git a/server/src/api/routes/commands.js b/server/src/api/routes/commands.js new file mode 100644 index 0000000..5eeef05 --- /dev/null +++ b/server/src/api/routes/commands.js @@ -0,0 +1,8 @@ +import { Router } from 'express' +import * as commandController from '../controllers/commandController.js' + +const router = Router() + +router.post('/', commandController.newCommand) + +export default router diff --git a/server/src/api/routes/index.js b/server/src/api/routes/index.js index bc3eae8..e7c6084 100644 --- a/server/src/api/routes/index.js +++ b/server/src/api/routes/index.js @@ -1,6 +1,7 @@ import { Router } from 'express' import sessions from './sessions.js' import quests from './quests.js' +import commands from './commands.js' import { authMiddleware } from '../../middleware/auth.js' const router = Router() @@ -11,5 +12,6 @@ router.get('/', (req, res) => { router.use('/sessions', authMiddleware, sessions) router.use('/quests', quests) +router.use('/commands', commands) export default router diff --git a/server/src/api/socket.js b/server/src/api/socket.js index 5f08973..2eea301 100644 --- a/server/src/api/socket.js +++ b/server/src/api/socket.js @@ -1,7 +1,7 @@ import { Server } from 'socket.io' import { getOrCreateContainer, attachContainer } from '../utils/docker.js' import Session from '../models/session.js' -import { defaultUser } from '../utils/auth.js' +import { defaultUser, genSessionJWT } from '../utils/auth.js' export default (server) => { const io = new Server(server) @@ -21,26 +21,39 @@ export default (server) => { io.on('connection', async (socket) => { console.log('Connected to the client.') - const session = await Session.findOne({ - _id: socket.handshake.query.session_id, - user: socket.user, - }) - - if (!session) { - socket.send('Session not found.') + const sessionId = socket.handshake.query.session_id + if (!sessionId) { + socket.send('Session ID not found.') + socket.disconnect() + return + } + let session + try { + session = await Session.findOne({ + _id: sessionId, + user: socket.user, + }) + if (!session) { + socket.send('Session not found.') + socket.disconnect() + return + } + } catch (err) { + socket.send(err.message) socket.disconnect() return } + const token = await genSessionJWT(session) + const container = await getOrCreateContainer(session.containerId) - const stream = await attachContainer(container) + const stream = await attachContainer(container, { token }) stream.on('data', (chunk) => { socket.send(chunk) }) socket.on('message', function incoming(message) { - console.log(`From client: ${message}`) stream.write(message) }) diff --git a/server/src/client.js b/server/src/client.js index 40e16cd..557b88d 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -1,37 +1,115 @@ +/* eslint-disable no-underscore-dangle */ import { stdin, stdout, exit } from 'process' import { io } from 'socket.io-client' +import { program } from 'commander' +import { get } from './utils/env.js' stdin.setRawMode(true) stdin.resume() stdin.setEncoding('utf8') -const sessionId = process.argv[2] -console.log(`Session ID: ${sessionId}`) +async function createdSession() { + console.log('Creating a new session...') + const res = await fetch(`${program.opts().host}/api/v1/sessions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + quest_id: 'helloworld', + }), + }) + const data = await res.json() + console.log('Created Session ID:', data._id) + return data +} -const socket = io('ws://localhost:3000', { - query: { - session_id: sessionId, - }, -}) +async function getSessionList() { + const res = await fetch(`${program.opts().host}/api/v1/sessions`) + const data = await res.json() + return data +} -socket.on('open', function open() { - console.log('Connected to the server.') -}) - -socket.on('message', function incoming(data) { - stdout.write(data) -}) - -socket.on('close', function close() { - console.log('Disconnected from the server.') - exit() -}) - -stdin.on('data', (key) => { - if (key === '\u0003') { - socket.close() - exit() - } else { - socket.send(key) +async function lastSession() { + const sessions = await getSessionList() + if (sessions.length === 0) { + console.log( + 'No session found. Please use --create to create a new session.' + ) + exit(1) } -}) + const session = sessions[sessions.length - 1] + console.log( + `Last Session ID: ${session._id}, Quest: ${session.quest}, Created At: ${session.createdAt}` + ) + return session +} + +async function connect(sessionId) { + console.log(`Session ID: ${sessionId}`) + console.log('Use Ctrl + D to exit.') + if (!sessionId) exit() + + const socket = io(program.opts().host, { + query: { + session_id: sessionId, + }, + }) + + socket.on('open', function open() { + console.log('Connected to the server.') + }) + + socket.on('message', function incoming(data) { + stdout.write(data) + }) + + socket.on('close', function close() { + console.log('Disconnected from the server.') + exit() + }) + + stdin.on('data', (key) => { + if (key === '\u0004') { + socket.close() + exit() + } else { + socket.send(key) + } + }) +} + +async function main() { + console.log(program.opts().session) + // main() + + const sessionId = + program.opts().session || + (program.opts().create ? (await createdSession())._id : null) || + (await lastSession())._id + + await connect(sessionId) +} + +program + .option('-s, --session ', 'Session ID') + .option('-c, --create', 'Create a new session') + .option( + '-h, --host ', + 'Server host', + get('API_ENDPOINT', 'http://localhost:3000') + ) + .action(main) + +program + .command('list') + .description('List all sessions') + .action(async () => { + const sessions = await getSessionList() + sessions.forEach(({ _id, quest, createdAt }) => { + console.log(`${_id} ${quest} ${createdAt}`) + }) + exit() + }) + +program.parse() diff --git a/server/src/config.js b/server/src/config.js index 0aaef88..6b3abf0 100644 --- a/server/src/config.js +++ b/server/src/config.js @@ -1,13 +1,10 @@ -function get(key, defaultValue) { - const value = process.env[key] - if (value != null) { - return value - } - return defaultValue -} +import { get } from './utils/env.js' export default { host: get('HOST', 'localhost'), port: get('PORT', 3000), db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'), + jwtSecret: get('JWT_SECRET', 'secret'), + dockerNetwork: get('DOCKER_NETWORK', 'linux-odyssey-players'), + isProduction: process.env.NODE_ENV === 'production', } diff --git a/server/src/db.js b/server/src/db.js index fb575c9..842ce6f 100644 --- a/server/src/db.js +++ b/server/src/db.js @@ -6,6 +6,7 @@ export default async function connectDB() { await mongoose.connect(config.db) console.log('Connected to MongoDB') } catch (err) { - console.log(err) + console.error(err) + process.exit(1) } } diff --git a/server/src/index.js b/server/src/index.js index 80a424e..4b5ad47 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -9,6 +9,7 @@ import connectDB from './db.js' import apiRouter from './api/routes/index.js' import loadAndUpdateQuests from './utils/quest.js' import config from './config.js' +import errorHandler from './middleware/error.js' async function main() { const app = express() @@ -28,6 +29,8 @@ async function main() { app.use('/api/v1', apiRouter) + app.use(errorHandler) + server.listen(config.port, config.host, () => { console.log(`Server listening at http://${config.host}:${config.port}`) }) diff --git a/server/src/middleware/error.js b/server/src/middleware/error.js index e69de29..fc1be7e 100644 --- a/server/src/middleware/error.js +++ b/server/src/middleware/error.js @@ -0,0 +1,5 @@ +// eslint-disable-next-line no-unused-vars +export default function errorHandler(err, req, res, next) { + console.error(err.stack) // Log error stack trace to the console + res.status(500).send({ message: err.message }) // Send error response +} diff --git a/server/src/models/command.js b/server/src/models/command.js new file mode 100644 index 0000000..ce64e7c --- /dev/null +++ b/server/src/models/command.js @@ -0,0 +1,33 @@ +import { model, Schema } from 'mongoose' + +const Command = model( + 'Command', + new Schema( + { + session: { + type: Schema.ObjectId, + ref: 'Session', + required: true, + }, + pwd: { + type: String, + required: true, + }, + command: { + type: String, + required: true, + }, + exit_code: { + type: Number, + required: false, + }, + output: { + type: String, + required: false, + }, + }, + { timestamps: true } + ) +) + +export default Command diff --git a/server/src/utils/auth.js b/server/src/utils/auth.js index e33cf5f..d97de3a 100644 --- a/server/src/utils/auth.js +++ b/server/src/utils/auth.js @@ -1,4 +1,5 @@ -/* eslint-disable import/prefer-default-export */ +import jwt from 'jsonwebtoken' +import config from '../config.js' import User from '../models/user.js' export async function defaultUser() { @@ -12,3 +13,32 @@ export async function defaultUser() { } return user } + +export function genSessionJWT(session) { + return new Promise((resolve, reject) => { + jwt.sign( + { session_id: session.id }, + config.jwtSecret, + { expiresIn: '1h' }, + (err, token) => { + if (err) { + reject(err) + } else { + resolve(token) + } + } + ) + }) +} + +export function verifySessionJWT(token) { + return new Promise((resolve, reject) => { + jwt.verify(token, config.jwtSecret, (err, decoded) => { + if (err) { + reject(err) + } else { + resolve(decoded) + } + }) + }) +} diff --git a/server/src/utils/docker.js b/server/src/utils/docker.js index a78b738..50fe918 100644 --- a/server/src/utils/docker.js +++ b/server/src/utils/docker.js @@ -1,9 +1,12 @@ import Docker from 'dockerode' +import config from '../config.js' const engine = new Docker() +const questPath = '/home/zeko/src/linux-odyssey/server/quests' + const containerOptions = { - Image: 'ubuntu', + Image: 'lancatlin/linux-odyssey:helloworld', AttachStdin: true, AttachStdout: true, AttachStderr: true, @@ -11,42 +14,43 @@ const containerOptions = { Cmd: ['/bin/bash'], OpenStdin: true, StdinOnce: false, + HostConfig: { + Binds: config.isProduction + ? [] + : [`${questPath}/helloworld/home:/home/rudeus`], + }, } -export async function createContainer() { - const container = await engine.createContainer({ - ...containerOptions, - }) - await container.start() - return container -} - -export async function getContainer(id) { - const container = await engine.getContainer(id) - await container.start() - return container -} +const network = engine.getNetwork(config.dockerNetwork) export async function getOrCreateContainer(id) { console.log(`Getting container: ${id}`) + let container try { - const container = engine.getContainer(id) + container = engine.getContainer(id) if (!(await container.inspect()).State.Running) { await container.start() } return container } catch (error) { - const container = await engine.createContainer({ - ...containerOptions, - name: id, - }) + try { + container = await engine.createContainer({ + ...containerOptions, + name: id, + }) + } catch (err) { + console.log(err) + throw err + } + await network.connect({ Container: container.id }) + await container.start() console.log(container.id) return container } } -export async function attachContainer(container) { +export async function attachContainer(container, { token }) { // Create an exec instance with bash shell const exec = await container.exec({ AttachStdin: true, @@ -54,6 +58,7 @@ export async function attachContainer(container) { AttachStderr: true, Cmd: ['/bin/bash'], Tty: true, + Env: [`TOKEN=${token}`, `API_ENDPOINT=http://app:3000`], }) const execOutput = await exec.start({ diff --git a/server/src/utils/env.js b/server/src/utils/env.js new file mode 100644 index 0000000..3195d31 --- /dev/null +++ b/server/src/utils/env.js @@ -0,0 +1,8 @@ +/* eslint-disable import/prefer-default-export */ +export function get(key, defaultValue) { + const value = process.env[key] + if (value != null) { + return value + } + return defaultValue +} diff --git a/server/swagger.yaml b/server/swagger.yaml index d33f72a..2527225 100644 --- a/server/swagger.yaml +++ b/server/swagger.yaml @@ -291,6 +291,24 @@ paths: properties: message: type: string + + /commands: + post: + summary: Record a command that was executed in a session + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Command' + responses: + '201': + description: Command recorded successfully + '400': + description: Invalid request body + '401': + description: Unauthorized + components: schemas: User: @@ -422,6 +440,28 @@ components: source: type: string + Command: + type: object + required: + - token + - pwd + - command + properties: + token: + type: string + description: The JWT token of the session + pwd: + type: string + example: '/root' + description: The current working directory + command: + type: string + example: 'ls -al /' + description: The command that was executed + output: + type: string + description: The output of the command + securitySchemes: BearerAuth: type: http diff --git a/yarn.lock b/yarn.lock index b19df92..c888fdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1323,6 +1323,11 @@ bson@^5.4.0: resolved "https://registry.yarnpkg.com/bson/-/bson-5.4.0.tgz#0eea77276d490953ad8616b483298dbff07384c6" integrity sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA== +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1481,6 +1486,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +commander@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" + integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== + commander@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -1698,6 +1708,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -3218,6 +3235,33 @@ json5@^2.2.2: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonwebtoken@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#81d8c901c112c24e497a55daf6b2be1225b40145" + integrity sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + 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" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + kareem@2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.5.1.tgz#7b8203e11819a8e77a34b3517d3ead206764d15d" @@ -4010,7 +4054,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4041,7 +4085,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.5, semver@^7.3.6, semver@^7.5.3: +semver@^7.3.5, semver@^7.3.6, semver@^7.3.8, semver@^7.5.3: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==