diff --git a/.dockerignore b/.dockerignore
index b99e7de969338ea4dab5eedd05299349aa8c7049..f5a5e249bfe45608d8d00a85d90918a0d9062ffc 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,3 +1,4 @@
 node_modules
 .git
 .gitignore
+data
diff --git a/.eslintignore b/.eslintignore
index ed4598e7fabda7e5f6950645c5570c66ac5cfd28..cff71ae1a6ce1e94e00f0ec28298bb3a56d89fc2 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,2 +1,3 @@
 node_modules
 bin
+data
diff --git a/.gitignore b/.gitignore
index 349f2b1937a1bcf134e2e996fd86b8945de07636..c9156895f6e72132faecaf379c297f032f3c43f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,6 @@ coverage
 .netlify
 
 kong.yml
+
+
+data
diff --git a/.prettierignore b/.prettierignore
index 6895bf07a960c02734f68bf0ef353f557ec1a7e1..161501f965cbb072852ab334a693a67d0a7e6aa4 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,3 +1,3 @@
 node_modules
 coverage
-
+data
diff --git a/README.md b/README.md
index 40e017d78154d9569ff37fb1ff4ec42c51d99bcc..bc73802b541ec12a8f6d72ce7308359449343498 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# RESTful API Node Server Boilerplate
+# RESTful API Node Server Boilerplate - te
 
 [![Build Status](https://travis-ci.org/hagopj13/node-express-boilerplate.svg?branch=master)](https://travis-ci.org/hagopj13/node-express-boilerplate)
 [![Coverage Status](https://coveralls.io/repos/github/hagopj13/node-express-boilerplate/badge.svg?branch=master)](https://coveralls.io/github/hagopj13/node-express-boilerplate?branch=master)
diff --git a/docker-compose.yml b/docker-compose.yml
index 65a108e4be87a423e686e612d6e40081f376af1b..26dcb0a5dcc1c133cc1bdeea7776efd2596f6240 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -36,8 +36,6 @@ services:
   playground-db:
     container_name: ${DB_CONTAINER_NAME:-playground-db}
     image: mongo:4.4.6-bionic
-    ports:
-      - '${MONGO_EXPOSE_PORT}:27017'
     volumes:
       - dbdata:/data/db
     networks:
@@ -47,8 +45,9 @@ services:
   upload:
     platform: linux/amd64
     container_name: ${ENV:-dev}-upload
-    build: ./upload/
-    image: boschvn/upload:${IMAGE_TAG:-latest}
+    image: boschvn/upload:latest
+    env_file:
+      - .env
     volumes:
       - '${UPLOAD_PATH}:/usr/src/upload/data'
     networks:
diff --git a/kong.yml.template b/kong.yml.template
index 657cfb6d9104855585501412acd00f42b7739d15..1a60994422ecfd383608083258acce58aa5dcb3d 100644
--- a/kong.yml.template
+++ b/kong.yml.template
@@ -18,6 +18,7 @@ services:
           - HEAD
           - CONNECT
           - TRACE
+    read_timeout: 180000
 plugins:
   - name: rate-limiting
     enabled: true
@@ -29,7 +30,7 @@ plugins:
     config:
       fault_tolerant: true
       hide_client_headers: false
-      minute: 60
+      minute: 120
       limit_by: header
       header_name: x-forwarded-for
       policy: local
diff --git a/package.json b/package.json
index 9aecb60654f97dd31c7862df8d2d5f38675ac212..ba4e2736e19b01b6dbb250d91913e0c34ecc1334 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
   },
   "scripts": {
     "start": "pm2 start ecosystem.config.json --no-daemon",
-    "dev": "cross-env NODE_ENV=development nodemon src/index.js",
+    "dev": "cross-env NODE_ENV=development nodemon --ignore 'data/*' src/index.js",
     "test": "jest -i --colors --verbose --detectOpenHandles",
     "test:watch": "jest -i --watchAll",
     "coverage": "jest -i --coverage",
@@ -69,6 +69,7 @@
     "jimp": "^0.22.12",
     "joi": "^17.3.0",
     "jsonwebtoken": "^8.5.1",
+    "lodash": "^4.17.21",
     "moment": "^2.24.0",
     "mongoose": "^5.7.7",
     "morgan": "^1.9.1",
@@ -76,15 +77,16 @@
     "passport": "^0.4.0",
     "passport-jwt": "^4.0.0",
     "pm2": "^5.1.0",
-    "socket.io": "^4.7.5",
+    "socket.io": "^4.8.0",
     "swagger-jsdoc": "^6.0.8",
     "swagger-ui-express": "^4.1.6",
     "utf-8-validate": "^6.0.4",
     "validator": "^13.0.0",
-    "winston": "^3.2.1",
-    "xss-clean": "^0.1.1"
+    "websocket": "^1.0.35",
+    "winston": "^3.2.1"
   },
   "devDependencies": {
+    "@types/lodash": "^4.17.12",
     "@types/mongoose": "^5.11.97",
     "coveralls": "^3.0.7",
     "eslint": "^7.0.0",
diff --git a/src/app.js b/src/app.js
index 97a3c4eae5a320a6f18d0419e22287d3158a5ec5..947ff0cdec5bfdf1b966cbaff08fce44927e0138 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1,6 +1,5 @@
 const express = require('express');
 const helmet = require('helmet');
-const xss = require('xss-clean');
 const cookies = require('cookie-parser');
 const mongoSanitize = require('express-mongo-sanitize');
 const compression = require('compression');
@@ -16,6 +15,7 @@ const routesV2 = require('./routes/v2');
 const { errorConverter, errorHandler } = require('./middlewares/error');
 const ApiError = require('./utils/ApiError');
 const setupProxy = require('./config/proxyHandler');
+const { init: initSocketIO } = require('./config/socket');
 
 const app = express();
 
@@ -37,7 +37,6 @@ app.use(express.json({ limit: '50mb', strict: false }));
 app.use(express.urlencoded({ extended: true, limit: '50mb', parameterLimit: 10000 }));
 
 // sanitize request data
-app.use(xss());
 app.use(mongoSanitize());
 
 // gzip compression
@@ -46,14 +45,7 @@ app.use(compression());
 // enable cors
 app.use(
   cors({
-    origin: [
-      /localhost:\d+/,
-      /\.digitalauto\.tech$/,
-      /\.digitalauto\.asia$/,
-      /\.digital\.auto$/,
-      'https://digitalauto.netlify.app',
-      /127\.0\.0\.1:\d+/,
-    ],
+    origin: config.cors.regex,
     credentials: true,
   })
 );
@@ -63,18 +55,14 @@ app.options('*', cors());
 app.use(passport.initialize());
 passport.use('jwt', jwtStrategy);
 
-// limit repeated failed requests to auth endpoints
-// if (config.env === 'production') {
-//  app.use('/v1/auth', authLimiter);
-//  app.use('/v2/auth', authLimiter);
-// }
-
 // v1 api routes
 app.use('/v1', routes);
 app.use('/v2', routesV2);
 
 // Setup proxy to other services
 setupProxy(app);
+const server = require('http').createServer(app);
+initSocketIO(server);
 
 // send back a 404 error for any unknown api request
 app.use((req, res, next) => {
diff --git a/src/config/config.js b/src/config/config.js
index eddc37db4241d2307c176f9be4f104ce7713f67d..49739f25bc61d7d17b3820749fb3ed38ab1b726d 100644
--- a/src/config/config.js
+++ b/src/config/config.js
@@ -18,6 +18,8 @@ const envVarsSchema = Joi.object()
     JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: Joi.number()
       .default(10)
       .description('minutes after which verify email token expires'),
+    JWT_COOKIE_NAME: Joi.string().required().description('JWT cookie name'),
+    JWT_COOKIE_DOMAIN: Joi.string().required().description('JWT cookie domain'),
     SMTP_HOST: Joi.string().description('server that will send the emails'),
     SMTP_PORT: Joi.number().description('port to connect to the email server'),
     SMTP_USERNAME: Joi.string().description('username for email server'),
@@ -45,6 +47,7 @@ const envVarsSchema = Joi.object()
     ETAS_CLIENT_SECRET: Joi.string().description('ETAS client secret'),
     ETAS_SCOPE: Joi.string().description('ETAS scope'),
     ETAS_INSTANCE_ENDPOINT: Joi.string().description('ETAS instance endpoint'),
+    ETAS_DEV_INSTANCE_ENDPOINT: Joi.string().description('ETAS dev instance endpoint'),
     // Certivity
     CERTIVITY_CLIENT_ID: Joi.string().required().description('Certivity client id'),
     CERTIVITY_CLIENT_SECRET: Joi.string().required().description('Certivity client secret'),
@@ -62,6 +65,16 @@ const config = {
   env: envVars.NODE_ENV,
   port: envVars.PORT,
   strictAuth: envVars.STRICT_AUTH,
+  cors: {
+    regex: [
+      /localhost:\d+/,
+      /\.digitalauto\.tech$/,
+      /\.digitalauto\.asia$/,
+      /\.digital\.auto$/,
+      'https://digitalauto.netlify.app',
+      /127\.0\.0\.1:\d+/,
+    ],
+  },
   mongoose: {
     url: envVars.MONGODB_URL + (envVars.NODE_ENV === 'test' ? '-test' : ''),
     options: {
@@ -77,10 +90,14 @@ const config = {
     refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,
     resetPasswordExpirationMinutes: envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,
     verifyEmailExpirationMinutes: envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,
-    cookieRefreshOptions: {
-      secure: true,
-      httpOnly: true,
-      sameSite: 'None',
+    cookie: {
+      name: envVars.JWT_COOKIE_NAME,
+      options: {
+        secure: true,
+        httpOnly: true,
+        sameSite: 'None',
+        ...(envVars.NODE_ENV === 'production' && { domain: envVars.JWT_COOKIE_DOMAIN }),
+      },
     },
   },
   email: {
@@ -140,6 +157,7 @@ const config = {
     clientSecret: envVars.ETAS_CLIENT_SECRET,
     scope: envVars.ETAS_SCOPE,
     instanceEndpoint: envVars.ETAS_INSTANCE_ENDPOINT,
+    developmentEndpoint: envVars.ETAS_DEV_INSTANCE_ENDPOINT,
   },
   githubIssueSubmitUrl: 'https://api.github.com/repos/digital-auto/vehicle_signal_specification/issues',
   certivity: {
diff --git a/src/config/passport.js b/src/config/passport.js
index d92ebf346bf726f3ea2187f77cc586589bb42b03..73feed0d02550af82781b415828e24de3c1906d8 100644
--- a/src/config/passport.js
+++ b/src/config/passport.js
@@ -31,4 +31,5 @@ const jwtStrategy = new JwtStrategy(jwtOptions, jwtVerify);
 
 module.exports = {
   jwtStrategy,
+  jwtVerify,
 };
diff --git a/src/config/socket.js b/src/config/socket.js
index 5b9790ad575f2077840a42221fdc3de663adf1f6..0e6f87c839ef65d3bcb08d874d605846e6277e78 100644
--- a/src/config/socket.js
+++ b/src/config/socket.js
@@ -1,22 +1,50 @@
 const { Server } = require('socket.io');
 const logger = require('./logger');
+const ApiError = require('../utils/ApiError');
+const httpStatus = require('http-status');
+const jwt = require('jsonwebtoken');
+const config = require('./config');
+const { jwtVerify } = require('./passport');
+const { tokenTypes } = require('./tokens');
+const permissionService = require('../services/permission.service');
+const { PERMISSIONS } = require('./roles');
 
 let io = null;
 
 const init = (server) => {
   io = new Server(server, {
     cors: {
-      origin: [
-        /localhost:\d+/,
-        /\.digitalauto\.tech$/,
-        /\.digitalauto\.asia$/,
-        /\.digital\.auto$/,
-        'https://digitalauto.netlify.app',
-      ],
+      origin: config.cors.regex,
       credentials: true,
     },
   });
 
+  io.use(function (socket, next) {
+    if (socket.handshake.query && socket.handshake.query.access_token) {
+      jwt.verify(socket.handshake.query.access_token, config.jwt.secret, async function (err, decoded) {
+        if (err) return next(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
+        await jwtVerify(
+          {
+            type: tokenTypes.ACCESS,
+            sub: decoded.sub,
+          },
+          async (error, user) => {
+            if (error || !user) {
+              return next(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
+            }
+            // if (!(await permissionService.hasPermission(user.id, PERMISSIONS.GENERATIVE_AI)))
+            //   return next(new ApiError(httpStatus.UNAUTHORIZED, 'You are not authorized to access this'));
+            // }
+            socket.user = user;
+            next();
+          }
+        );
+      });
+    } else {
+      next(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
+    }
+  });
+
   io.on('connection', () => {
     logger.info('a user connected');
   });
diff --git a/src/controllers/api.controller.js b/src/controllers/api.controller.js
index 8cf7522b27f73c63d05a1bd943235e2cff8a40f1..498268eec70b18249727c43334245cb0cc39f2a7 100644
--- a/src/controllers/api.controller.js
+++ b/src/controllers/api.controller.js
@@ -31,10 +31,22 @@ const deleteApi = catchAsync(async (req, res) => {
   res.status(httpStatus.NO_CONTENT).send();
 });
 
+const listVSSVersions = catchAsync(async (req, res) => {
+  const versions = await apiService.listVSSVersions();
+  res.send(versions);
+});
+
+const getVSSVersion = catchAsync(async (req, res) => {
+  const version = await apiService.getVSSVersion(req.params.name);
+  res.send(version);
+});
+
 module.exports = {
   createApi,
   getApiByModelId,
   getApi,
   updateApi,
   deleteApi,
+  listVSSVersions,
+  getVSSVersion,
 };
diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js
index 27ab779059045a7a4a7e6092a7f6fd131a67c041..40a3aceebfd758e43bd8f17455cf31ee435fc958 100644
--- a/src/controllers/auth.controller.js
+++ b/src/controllers/auth.controller.js
@@ -1,10 +1,9 @@
 const httpStatus = require('http-status');
 const catchAsync = require('../utils/catchAsync');
-const { authService, userService, tokenService, emailService, fileService } = require('../services');
+const { authService, userService, tokenService, emailService } = require('../services');
 const config = require('../config/config');
 const ApiError = require('../utils/ApiError');
 const logger = require('../config/logger');
-const image = require('../utils/image');
 
 const register = catchAsync(async (req, res) => {
   const user = await userService.createUser({
@@ -12,10 +11,11 @@ const register = catchAsync(async (req, res) => {
     provider: req.body?.provider || 'Email',
   });
   const tokens = await tokenService.generateAuthTokens(user);
-  res.cookie('token', tokens.refresh.token, {
+  res.cookie(config.jwt.cookie.name, tokens.refresh.token, {
     expires: tokens.refresh.expires,
-    ...config.jwt.cookieRefreshOptions,
+    ...config.jwt.cookie.options,
   });
+
   delete tokens.refresh;
   res.status(httpStatus.CREATED).send({ user, tokens });
 });
@@ -24,25 +24,28 @@ const login = catchAsync(async (req, res) => {
   const { email, password } = req.body;
   const user = await authService.loginUserWithEmailAndPassword(email, password);
   const tokens = await tokenService.generateAuthTokens(user);
-  res.cookie('token', tokens.refresh.token, {
+  res.cookie(config.jwt.cookie.name, tokens.refresh.token, {
     expires: tokens.refresh.expires,
-    ...config.jwt.cookieRefreshOptions,
+    ...config.jwt.cookie.options,
   });
   delete tokens.refresh;
   res.send({ user, tokens });
 });
 
 const logout = catchAsync(async (req, res) => {
-  await authService.logout(req.cookies.token);
-  res.clearCookie('token');
+  await authService.logout(req.cookies[config.jwt.cookie.name]);
+  res.clearCookie(config.jwt.cookie.name);
+  res.clearCookie(config.jwt.cookie.name, {
+    ...config.jwt.cookie.options,
+  });
   res.status(httpStatus.NO_CONTENT).send();
 });
 
 const refreshTokens = catchAsync(async (req, res) => {
-  const tokens = await authService.refreshAuth(req.cookies.token);
-  res.cookie('token', tokens.refresh.token, {
+  const tokens = await authService.refreshAuth(req.cookies[config.jwt.cookie.name]);
+  res.cookie(config.jwt.cookie.name, tokens.refresh.token, {
     expires: tokens.refresh.expires,
-    ...config.jwt.cookieRefreshOptions,
+    ...config.jwt.cookie.options,
   });
   delete tokens.refresh;
 
@@ -77,6 +80,7 @@ const githubCallback = catchAsync(async (req, res) => {
     await authService.githubCallback(code, userId);
     res.redirect(`${origin || 'http://127.0.0.1:3000'}/auth/github/success`);
   } catch (error) {
+    logger.error(error);
     res.status(httpStatus.UNAUTHORIZED).send('Unauthorized. Please try again.');
   }
 });
@@ -100,9 +104,9 @@ const sso = catchAsync(async (req, res) => {
   }
 
   const tokens = await tokenService.generateAuthTokens(user);
-  res.cookie('token', tokens.refresh.token, {
+  res.cookie(config.jwt.cookie.name, tokens.refresh.token, {
     expires: tokens.refresh.expires,
-    ...config.jwt.cookieRefreshOptions,
+    ...config.jwt.cookie.options,
   });
   delete tokens.refresh;
 
diff --git a/src/controllers/extendedApi.controller.js b/src/controllers/extendedApi.controller.js
index 90fda16fd8eecaf2b59ae125f33bec2703b19e6f..fefe6a52f867cd047d8e173ec9abe52e76ff13ee 100644
--- a/src/controllers/extendedApi.controller.js
+++ b/src/controllers/extendedApi.controller.js
@@ -1,15 +1,23 @@
 const httpStatus = require('http-status');
 const catchAsync = require('../utils/catchAsync');
-const { extendedApiService } = require('../services');
+const { extendedApiService, permissionService } = require('../services');
 const pick = require('../utils/pick');
 const ApiError = require('../utils/ApiError');
+const { PERMISSIONS } = require('../config/roles');
 
 const createExtendedApi = catchAsync(async (req, res) => {
+  if (!(await permissionService.hasPermission(req.user?.id, PERMISSIONS.WRITE_MODEL, req.body.model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden. You do not have permission to update extended API for this model');
+  }
   const extendedApi = await extendedApiService.createExtendedApi(req.body);
   res.status(httpStatus.CREATED).send(extendedApi);
 });
 
 const getExtendedApis = catchAsync(async (req, res) => {
+  const { model } = req.query;
+  if (!(await permissionService.canAccessModel(req.user?.id, model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden');
+  }
   const filter = pick(req.query, ['apiName', 'model', 'skeleton']);
   const options = pick(req.query, ['sortBy', 'limit', 'page']);
   const result = await extendedApiService.queryExtendedApis(filter, options);
@@ -18,6 +26,10 @@ const getExtendedApis = catchAsync(async (req, res) => {
 
 const getExtendedApi = catchAsync(async (req, res) => {
   const extendedApi = await extendedApiService.getExtendedApiById(req.params.id);
+  const { model } = extendedApi;
+  if (!(await permissionService.canAccessModel(req.user?.id, model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden');
+  }
   if (!extendedApi) {
     throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found');
   }
@@ -26,6 +38,9 @@ const getExtendedApi = catchAsync(async (req, res) => {
 
 const getExtendedApiByApiNameAndModel = catchAsync(async (req, res) => {
   const { apiName, model } = req.query;
+  if (!(await permissionService.canAccessModel(req.user?.id, model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden');
+  }
   const extendedApi = await extendedApiService.getExtendedApiByApiNameAndModel(apiName, model);
   if (!extendedApi) {
     throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found');
@@ -34,12 +49,12 @@ const getExtendedApiByApiNameAndModel = catchAsync(async (req, res) => {
 });
 
 const updateExtendedApi = catchAsync(async (req, res) => {
-  const extendedApi = await extendedApiService.updateExtendedApiById(req.params.id, req.body);
+  const extendedApi = await extendedApiService.updateExtendedApiById(req.params.id, req.body, req.user.id);
   res.send(extendedApi);
 });
 
 const deleteExtendedApi = catchAsync(async (req, res) => {
-  await extendedApiService.deleteExtendedApiById(req.params.id);
+  await extendedApiService.deleteExtendedApiById(req.params.id, req.user.id);
   res.status(httpStatus.NO_CONTENT).send();
 });
 
diff --git a/src/controllers/genai.controller.js b/src/controllers/genai.controller.js
index 45b616f7a1f62ec87bcece3733835a4ebd3b6373..c2c62c71dc322e7bf4d10741b06afc52901c403a 100644
--- a/src/controllers/genai.controller.js
+++ b/src/controllers/genai.controller.js
@@ -10,6 +10,7 @@ const config = require('../config/config');
 const axios = require('axios');
 const etasAuthorizationData = require('../states/etasAuthorization');
 const moment = require('moment');
+const { setupClient } = require('../utils/setupEtasStream');
 
 dotenv.config();
 
@@ -169,6 +170,8 @@ async function BedrockGenCode({ endpointURL, publicKey, secretKey, inputPrompt,
       }
     }
   } catch (error) {
+    console.log('Error in BedrockGenCode');
+    console.log(error);
     try {
       const bedrockResponse = await bedrock.send(new InvokeModelCommand(input));
       const response = JSON.parse(new TextDecoder().decode(bedrockResponse.body));
@@ -176,7 +179,7 @@ async function BedrockGenCode({ endpointURL, publicKey, secretKey, inputPrompt,
         generation = response.completions[0].data.text;
       }
     } catch (err) {
-      throw new Error('Failed to generate response from Bedrock, please check your endpoint URL, region, keys');
+      throw err;
     }
   }
   return generation;
@@ -251,8 +254,20 @@ const getAccessToken = async () => {
   }
 };
 
+const getInstance = (environment = 'prod') => {
+  switch (environment) {
+    case 'prod':
+      return config.etas.instanceEndpoint;
+    case 'dev':
+      return config.etas.developmentEndpoint;
+    default:
+      return config.etas.instanceEndpoint;
+  }
+};
+
 const generateAIContent = async (req, res) => {
   try {
+    const { environment } = req.params;
     const { prompt } = req.body;
     const authorizationData = etasAuthorizationData.getAuthorizationData();
     let token = authorizationData.accessToken;
@@ -265,9 +280,9 @@ const generateAIContent = async (req, res) => {
       });
     }
 
-    const instance = config.etas.instanceEndpoint;
+    const instance = getInstance(environment);
 
-    // console.log('ETAS_INSTANCE_ENDPOINT', instance);
+    setupClient(token);
 
     const response = await axios.post(
       `https://${instance}/r2mm/GENERATE_AI`,
diff --git a/src/controllers/model.controller.js b/src/controllers/model.controller.js
index 978f373e2cb0de3eb80553023a0f4fd7ab667d9a..ca0c0b5b99dceef0cf39d67bd182dc160c30f0e8 100644
--- a/src/controllers/model.controller.js
+++ b/src/controllers/model.controller.js
@@ -1,22 +1,69 @@
 const httpStatus = require('http-status');
-const { modelService, apiService, permissionService } = require('../services');
+const { modelService, apiService, permissionService, extendedApiService } = require('../services');
 const catchAsync = require('../utils/catchAsync');
 const pick = require('../utils/pick');
 const ApiError = require('../utils/ApiError');
 const { PERMISSIONS } = require('../config/roles');
 
 const createModel = catchAsync(async (req, res) => {
-  const { cvi, custom_apis, ...reqBody } = req.body;
+  const { cvi, custom_apis, extended_apis, ...reqBody } = req.body;
   const model = await modelService.createModel(req.user.id, {
     ...reqBody,
-    ...(reqBody.custom_apis && { custom_apis: JSON.parse(reqBody.custom_apis) }),
-  });
-  await apiService.createApi({
-    model: model._id,
-    cvi: JSON.parse(cvi),
-    created_by: req.user.id,
   });
 
+  // if (cvi) {
+  //   // await apiService.createApi(model._id, cvi);
+  // }
+
+  try {
+    if (extended_apis) {
+      await Promise.all(
+        extended_apis.map((api) =>
+          extendedApiService.createExtendedApi({
+            model: model._id,
+            apiName: api.apiName,
+            description: api.description,
+            skeleton: api.skeleton,
+            tags: api.tags,
+            type: api.type,
+            datatype: api.datatype,
+          })
+        )
+      );
+    }
+  } catch (error) {
+    console.warn('Error in creating model with extended_apis', error);
+  }
+
+  try {
+    if (custom_apis) {
+      let apis = custom_apis;
+      try {
+        apis = JSON.parse(custom_apis);
+      } catch (error) {
+        // Do nothing
+      }
+
+      if (Array.isArray(apis)) {
+        await Promise.all(
+          apis.map((api) =>
+            extendedApiService.createExtendedApi({
+              model: model._id,
+              apiName: api.name || api.apiName || 'Vehicle',
+              description: api.description || '',
+              skeleton: api.skeleton || '{}',
+              tags: api.tags || [],
+              type: api.type || 'branch',
+              datatype: api.datatype || (api.type !== 'branch' ? 'string' : null),
+            })
+          )
+        );
+      }
+    }
+  } catch (error) {
+    console.warn('Error in creating model with custom_apis', error);
+  }
+
   res.status(httpStatus.CREATED).send(model);
 });
 
@@ -94,6 +141,14 @@ const deleteAuthorizedUser = catchAsync(async (req, res) => {
   res.status(httpStatus.NO_CONTENT).send();
 });
 
+const getComputedVSSApi = catchAsync(async (req, res) => {
+  if (!(await permissionService.canAccessModel(req.user?.id, req.params.id))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden');
+  }
+  const data = await apiService.computeVSSApi(req.params.id);
+  res.send(data);
+});
+
 module.exports = {
   createModel,
   listModels,
@@ -102,4 +157,5 @@ module.exports = {
   deleteModel,
   addAuthorizedUser,
   deleteAuthorizedUser,
+  getComputedVSSApi,
 };
diff --git a/src/controllers/prototype.controller.js b/src/controllers/prototype.controller.js
index e61540142322f348b04ee248ae245a9cdc224b14..cfdf3d9bc9cc2b58ffb825686f03f8b1d458f6b3 100644
--- a/src/controllers/prototype.controller.js
+++ b/src/controllers/prototype.controller.js
@@ -65,7 +65,7 @@ const executeCode = catchAsync(async (req, res) => {
 });
 
 const listPopularPrototypes = catchAsync(async (req, res) => {
-  const prototypes = await prototypeService.listPopularPrototypes(req.user.id);
+  const prototypes = await prototypeService.listPopularPrototypes(req.user?.id);
   res.send(prototypes);
 });
 
diff --git a/src/index.js b/src/index.js
index dd72d881158a6d32839455687d6c89894fa8b92b..54664d15eb7690b8596688159e08309f6daca44f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,6 +4,7 @@ const config = require('./config/config');
 const logger = require('./config/logger');
 const initializeRoles = require('./utils/initializeRoles');
 const { init } = require('./config/socket');
+const setupScheduledCheck = require('./scripts/checkVSSUpdate');
 
 let server;
 mongoose.connect(config.mongoose.url, config.mongoose.options).then(() => {
@@ -40,3 +41,5 @@ process.on('SIGTERM', () => {
     server.close();
   }
 });
+
+setupScheduledCheck();
diff --git a/src/models/extendedApi.model.js b/src/models/extendedApi.model.js
index 19da0b11b5966632b0888a443c8d3712c752de99..13a4cf02560055d1a102287a68bdd062c91fa206 100644
--- a/src/models/extendedApi.model.js
+++ b/src/models/extendedApi.model.js
@@ -3,9 +3,15 @@ const { toJSON, paginate } = require('./plugins');
 
 const tagSchema = mongoose.Schema(
   {
-    tagCategoryId: String,
-    tagCategoryName: String,
-    tag: String,
+    title: {
+      type: String,
+      required: true,
+      trim: true,
+    },
+    description: {
+      type: String,
+      trim: true,
+    },
   },
   {
     _id: false,
@@ -29,11 +35,15 @@ const extendedApiSchema = mongoose.Schema(
     type: {
       type: String,
     },
-    data_type: {
+    datatype: {
       type: String,
     },
     description: String,
     tags: [tagSchema],
+    isWishlist: {
+      type: Boolean,
+      default: false
+    }
   },
   {
     timestamps: true,
diff --git a/src/models/model.model.js b/src/models/model.model.js
index a427b7938f9ba1bd65a63174a76dea13e6be5f77..6cbeea2df5197f94235ffe868b0ccc9a8323f739 100644
--- a/src/models/model.model.js
+++ b/src/models/model.model.js
@@ -4,17 +4,14 @@ const { visibilityTypes } = require('../config/enums');
 
 const tagSchema = mongoose.Schema(
   {
-    tag: {
-      type: String,
-      required: true,
-    },
-    tagCategoryId: {
+    title: {
       type: String,
       required: true,
+      trim: true,
     },
-    tagCategoryName: {
+    description: {
       type: String,
-      required: true,
+      trim: true,
     },
   },
   {
@@ -73,6 +70,11 @@ const modelSchema = mongoose.Schema(
     extend: {
       type: mongoose.SchemaTypes.Mixed,
     },
+    api_version: {
+      type: String,
+      trim: true,
+      lowercase: true,
+    },
   },
   {
     timestamps: true,
diff --git a/src/models/plugins/toJSON.plugin.js b/src/models/plugins/toJSON.plugin.js
index d511c9db59253303a2f9b73b3986b1ce1f0636a4..97a7df2e74b560a6ade0c28ebea4517f27d5c701 100644
--- a/src/models/plugins/toJSON.plugin.js
+++ b/src/models/plugins/toJSON.plugin.js
@@ -31,7 +31,7 @@ const toJSON = (schema) => {
       ret.id = ret._id.toString();
       delete ret._id;
       delete ret.__v;
-      // ret.created_at = ret.createdAt;
+      ret.created_at = ret.createdAt;
       delete ret.createdAt;
       delete ret.updatedAt;
       if (transform) {
diff --git a/src/models/prototype.model.js b/src/models/prototype.model.js
index 878efebdfe594c5869df15c397bc4cbebba44cc2..79173a6274411ae097cfca618ab7bf817bb71ad8 100644
--- a/src/models/prototype.model.js
+++ b/src/models/prototype.model.js
@@ -32,6 +32,9 @@ const descriptionSchema = mongoose.Schema(
       type: String,
       max: 255,
     },
+    text: {
+      type: String,
+    },
   },
   {
     _id: false,
@@ -51,17 +54,14 @@ const portfolioSchema = mongoose.Schema(
 
 const tagSchema = mongoose.Schema(
   {
-    tag: {
-      type: String,
-      required: true,
-    },
-    tagCategoryId: {
+    title: {
       type: String,
       required: true,
+      trim: true,
     },
-    tagCategoryName: {
+    description: {
       type: String,
-      required: true,
+      trim: true,
     },
   },
   {
@@ -81,116 +81,132 @@ const ratingSchema = mongoose.Schema({
   },
 });
 
-const prototypeSchema = mongoose.Schema({
-  apis: {
-    type: apiSchema,
-    default: {
-      VSC: [],
-      VSS: [],
+const prototypeSchema = mongoose.Schema(
+  {
+    apis: {
+      type: apiSchema,
+      default: {
+        VSC: [],
+        VSS: [],
+      },
     },
-  },
-  code: {
-    type: String,
-    required: true,
-    default:
-      'from sdv_model import Vehicle import plugins from browser import aio vehicle = Vehicle() # write your code here',
-  },
-  extend: {
-    type: mongoose.SchemaTypes.Mixed,
-  },
-  complexity_level: {
-    type: Number,
-    required: true,
-    min: 1,
-    max: 5,
-    default: 3,
-  },
-  customer_journey: {
-    type: String,
-    required: true,
-    default:
-      ' #Step 1 Who: Driver What: Wipers turned on manually Customer TouchPoints: Windshield wiper switch #Step 2 Who: User What: User opens the car door/trunk and the open status of door/trunk is set to true Customer TouchPoints: Door/trunk handle #Step 3 Who: System What: The wiping is immediately turned off by the software and user is notified Customer TouchPoints: Notification on car dashboard and mobile app ',
-  },
-  description: {
-    type: descriptionSchema,
-    default: {
-      problem: '',
-      says_who: '',
-      solution: '',
-      status: '',
+    code: {
+      type: String,
+      required: true,
+      default:
+        'from sdv_model import Vehicle import plugins from browser import aio vehicle = Vehicle() # write your code here',
     },
-  },
-  image_file: {
-    type: String,
-  },
-  journey_image_file: {
-    type: String,
-  },
-  analysis_image_file: {
-    type: String,
-  },
-  model_id: {
-    type: mongoose.SchemaTypes.ObjectId,
-    ref: 'Model',
-    required: true,
-  },
-  name: {
-    type: String,
-    required: true,
-    trim: true,
-    maxLength: 255,
-  },
-  portfolio: {
-    type: portfolioSchema,
-    default: {
-      effort_estimation: 0,
-      needs_addressed: 0,
-      relevance: 0,
+    extend: {
+      type: mongoose.SchemaTypes.Mixed,
+    },
+    complexity_level: {
+      type: Number,
+      required: true,
+      min: 1,
+      max: 5,
+      default: 3,
+    },
+    customer_journey: {
+      type: String,
+      required: true,
+      default:
+        ' #Step 1 Who: Driver What: Wipers turned on manually Customer TouchPoints: Windshield wiper switch #Step 2 Who: User What: User opens the car door/trunk and the open status of door/trunk is set to true Customer TouchPoints: Door/trunk handle #Step 3 Who: System What: The wiping is immediately turned off by the software and user is notified Customer TouchPoints: Notification on car dashboard and mobile app ',
+    },
+    description: {
+      type: descriptionSchema,
+      default: {
+        problem: '',
+        says_who: '',
+        solution: '',
+        status: '',
+      },
+    },
+    image_file: {
+      type: String,
+    },
+    journey_image_file: {
+      type: String,
+    },
+    analysis_image_file: {
+      type: String,
+    },
+    model_id: {
+      type: mongoose.SchemaTypes.ObjectId,
+      ref: 'Model',
+      required: true,
+    },
+    name: {
+      type: String,
+      required: true,
+      trim: true,
+      maxLength: 255,
+    },
+    portfolio: {
+      type: portfolioSchema,
+      default: {
+        effort_estimation: 0,
+        needs_addressed: 0,
+        relevance: 0,
+      },
+    },
+    skeleton: {
+      type: String,
+    },
+    state: {
+      type: String,
+      required: true,
+      default: stateTypes.DEVELOPMENT,
+      enums: Object.values(stateTypes),
+    },
+    tags: {
+      type: [tagSchema],
+    },
+    widget_config: {
+      type: String,
+    },
+    last_viewed: {
+      type: Date,
+    },
+    rated_by: {
+      type: Map,
+      of: ratingSchema,
+      default: {},
+    },
+    autorun: {
+      type: Boolean,
+      default: false,
+    },
+    related_ea_components: {
+      type: String,
+    },
+    partner_logo: {
+      type: String,
+    },
+    created_by: {
+      type: mongoose.SchemaTypes.ObjectId,
+      ref: 'User',
+      required: true,
+    },
+    executed_turns: {
+      type: Number,
+      default: 0,
+    },
+    language: {
+      type: String,
+      default: 'python',
+      maxLength: 20,
+    },
+    requirements: {
+      type: String,
+    },
+    flow: {
+      type: mongoose.SchemaTypes.Mixed,
     },
   },
-  skeleton: {
-    type: String,
-  },
-  state: {
-    type: String,
-    required: true,
-    default: stateTypes.DEVELOPMENT,
-    enums: Object.values(stateTypes),
-  },
-  tags: {
-    type: [tagSchema],
-  },
-  widget_config: {
-    type: String,
-  },
-  last_viewed: {
-    type: Date,
-  },
-  rated_by: {
-    type: Map,
-    of: ratingSchema,
-    default: {},
-  },
-  autorun: {
-    type: Boolean,
-    default: false,
-  },
-  related_ea_components: {
-    type: String,
-  },
-  partner_logo: {
-    type: String,
-  },
-  created_by: {
-    type: mongoose.SchemaTypes.ObjectId,
-    ref: 'User',
-    required: true,
-  },
-  executed_turns: {
-    type: Number,
-    default: 0,
-  },
-});
+  {
+    timestamps: true,
+  }
+);
 
 // add plugin that converts mongoose to json
 prototypeSchema.plugin(toJSON);
diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js
index 585ad5e096fa5160944a8cfe15c40446bd9d1526..467c64c16084a315b5889ff8de8563a4bb1830cd 100644
--- a/src/routes/v1/index.js
+++ b/src/routes/v1/index.js
@@ -56,10 +56,10 @@ const defaultRoutes = [
     path: '/plugins',
     route: pluginsRoute,
   },
-  {
-    path: '/tags',
-    route: tagsRoute,
-  },
+  // {
+  //   path: '/tags',
+  //   route: tagsRoute,
+  // },
   {
     path: '/medias',
     route: mediasRoute,
diff --git a/src/routes/v1/tag.route.js b/src/routes/v1/tag.route.js
index ab174899e36d961fa36cbac367b8a398db9f5612..327a1f4dcdbe2194f0937b6d5fe47f2e0e0b7cde 100644
--- a/src/routes/v1/tag.route.js
+++ b/src/routes/v1/tag.route.js
@@ -6,12 +6,12 @@ const validate = require('../../middlewares/validate');
 
 const router = express.Router();
 
-router.route('/').post(validate(tagValidation.createTag), tagController.createTag);
-router
-  .route('/categories')
-  .get(validate(tagValidation.listTagCategories), tagController.listTagCategories)
-  .post(validate(tagValidation.createTagCategory), tagController.createTagCategory);
-router.route('/categories/:id').put(validate(tagValidation.updateTagCategory), tagController.updateTagCategory);
+// router.route('/').post(validate(tagValidation.createTag), tagController.createTag);
+// router
+//   .route('/categories')
+//   .get(validate(tagValidation.listTagCategories), tagController.listTagCategories)
+//   .post(validate(tagValidation.createTagCategory), tagController.createTagCategory);
+// router.route('/categories/:id').put(validate(tagValidation.updateTagCategory), tagController.updateTagCategory);
 
 // router.get('/categories/:tenantId', async (req, res) => {
 //   try {
diff --git a/src/routes/v2/api.route.js b/src/routes/v2/api.route.js
index b9bd30b50f954b26f6175ba7408dfd9affe79736..b0874c84a4a4e36112840c71fb1419545059466b 100644
--- a/src/routes/v2/api.route.js
+++ b/src/routes/v2/api.route.js
@@ -7,6 +7,9 @@ const config = require('../../config/config');
 
 const router = express.Router();
 
+router.route('/vss').get(apiController.listVSSVersions);
+router.route('/vss/:name').get(validate(apiValidation.getVSSVersion), apiController.getVSSVersion);
+
 router.route('/').post(auth(), validate(apiValidation.createApi), apiController.createApi);
 router
   .route('/:id')
diff --git a/src/routes/v2/auth.route.js b/src/routes/v2/auth.route.js
index b4a9908b4f205fe7cdd4703ff0f102fa9d8d89b1..e5598ec0f25d76115ecc30bb4885801ac6ec7b1a 100644
--- a/src/routes/v2/auth.route.js
+++ b/src/routes/v2/auth.route.js
@@ -11,8 +11,8 @@ router.get('/github/callback', authController.githubCallback);
 router.post('/sso', validate(authValidation.sso), authController.sso);
 if (!config.strictAuth) {
   router.post('/register', validate(authValidation.register), authController.register);
-  router.post('/login', validate(authValidation.login), authController.login);
 }
+router.post('/login', validate(authValidation.login), authController.login);
 router.post('/logout', authController.logout);
 router.post('/refresh-tokens', authController.refreshTokens);
 router.post('/forgot-password', validate(authValidation.forgotPassword), authController.forgotPassword);
diff --git a/src/routes/v2/email.route.js b/src/routes/v2/email.route.js
index 71a2bd454b160b43e3fe479e976e4ecfc480389f..a0dc24ed54b0cb323b7a3198ec208b4b03ec307d 100644
--- a/src/routes/v2/email.route.js
+++ b/src/routes/v2/email.route.js
@@ -7,21 +7,21 @@ const config = require('../../config/config');
 
 const router = express.Router();
 
-if (config.env === 'development') {
-  router.route('').post(
-    validate(emailValidation.sendEmail),
-    catchAsync(async (req, res) => {
-      const { to, subject, content } = req.body;
-      let html;
-      try {
-        html = decodeURIComponent(content);
-      } catch (error) {
-        html = content;
-      }
-      await emailService.sendEmail(to, subject, html);
-      res.status(200).send();
-    })
-  );
-}
+// if (config.env === 'development') {
+router.route('').post(
+  validate(emailValidation.sendEmail),
+  catchAsync(async (req, res) => {
+    const { to, subject, content } = req.body;
+    let html;
+    try {
+      html = decodeURIComponent(content);
+    } catch (error) {
+      html = content;
+    }
+    await emailService.sendEmail(to, subject, html);
+    res.status(200).send();
+  })
+);
+// }
 
 module.exports = router;
diff --git a/src/routes/v2/genai.route.js b/src/routes/v2/genai.route.js
index d2af9a09bfb9385db632c67e028b2c3da64f3497..991d05c5432a52036802ef898f8a1ec690116120 100644
--- a/src/routes/v2/genai.route.js
+++ b/src/routes/v2/genai.route.js
@@ -33,6 +33,16 @@ router.post(
   auth({
     optional: !config.strictAuth,
   }),
+  genaiPermission,
+  genaiController.generateAIContent
+);
+
+router.post(
+  '/etas/:environment',
+  auth({
+    optional: !config.strictAuth,
+  }),
+  genaiPermission,
   genaiController.generateAIContent
 );
 
diff --git a/src/routes/v2/model.route.js b/src/routes/v2/model.route.js
index e64fcc3329c2a043d20405122649d39b0fd49536..322016f5348e2db77dfbdc0e9008742f6f7060bc 100644
--- a/src/routes/v2/model.route.js
+++ b/src/routes/v2/model.route.js
@@ -42,6 +42,14 @@ router
     modelController.deleteModel
   );
 
+router.route('/:id/api').get(
+  auth({
+    optional: !config.strictAuth,
+  }),
+  validate(modelValidation.getApiByModelId),
+  modelController.getComputedVSSApi
+);
+
 router
   .route('/:id/permissions')
   .post(
diff --git a/src/scripts/checkVSSUpdate.js b/src/scripts/checkVSSUpdate.js
new file mode 100644
index 0000000000000000000000000000000000000000..b3d8c537f6413f29f08ca4d413772efa96ed3dc7
--- /dev/null
+++ b/src/scripts/checkVSSUpdate.js
@@ -0,0 +1,92 @@
+const axios = require('axios');
+const fs = require('fs');
+const path = require('path');
+const moment = require('moment');
+const _ = require('lodash');
+const logger = require('../config/logger');
+const { fileService } = require('../services');
+
+const setLastCheckTime = () => {
+  fs.writeFileSync(path.join(__dirname, '../../data/clock.txt'), moment().format());
+};
+
+const getLastCheckTime = () => {
+  try {
+    const lastCheckTime = fs.readFileSync(path.join(__dirname, '../../data/clock.txt'), 'utf8');
+    return moment(lastCheckTime);
+  } catch (error) {
+    return moment().subtract(1, 'hour');
+  }
+};
+
+/**
+ *
+ * @param {{name: string; published_at: string; browser_download_url: string}[]} releases
+ */
+const updateVSS = async (releases) => {
+  try {
+    fs.writeFileSync(path.join(__dirname, '../../data/vss.json'), JSON.stringify(releases, null, 2));
+    logger.info('Updated VSS version list');
+    logger.info('Downloading VSS versions data');
+    const promises = releases.map((release) =>
+      fileService.downloadFile(release.browser_download_url, path.join(__dirname, `../../data/${release.name}.json`))
+    );
+    await Promise.all(promises);
+    logger.info('Downloaded VSS versions data');
+  } catch (error) {
+    logger.error(error);
+  }
+};
+
+const checkUpdateVSS = async () => {
+  try {
+    const vssReleases = (await axios.get('https://api.github.com/repos/COVESA/vehicle_signal_specification/releases')).data;
+    setLastCheckTime();
+    const regex = /v(\d+\.\d+)/;
+
+    const filtered = vssReleases
+      ?.filter((release) => {
+        const match = release.name.match(regex);
+        if (match) {
+          return Number(match[1]) >= 3.0;
+        }
+      })
+      ?.map((release) => {
+        return {
+          name: release.name,
+          published_at: release.published_at,
+          browser_download_url: release.assets?.find((asset) => asset.name.endsWith('.json'))?.browser_download_url,
+        };
+      });
+
+    const vssFilePath = path.join(__dirname, '../../data/vss.json');
+
+    try {
+      if (fs.existsSync(vssFilePath) && _.isEqual(filtered, JSON.parse(fs.readFileSync(vssFilePath, 'utf8')))) return;
+    } catch (error) {
+      logger.warn(error);
+    }
+
+    await updateVSS(filtered);
+  } catch (error) {
+    logger.error(error);
+  }
+};
+
+let interval = null;
+
+const setupScheduledCheck = () => {
+  const dataDirExist = fs.existsSync(path.join(__dirname, '../../data'));
+  if (!dataDirExist) {
+    fs.mkdirSync(path.join(__dirname, '../../data'));
+  }
+  const lastCheckTime = getLastCheckTime();
+  if (moment().diff(lastCheckTime, 'seconds') > 120) {
+    checkUpdateVSS();
+  }
+  if (!interval) {
+    interval = setInterval(checkUpdateVSS, 1000 * 60 * 60 * 24);
+  }
+};
+
+module.exports = setupScheduledCheck;
diff --git a/src/scripts/migrate-api.js b/src/scripts/migrate-api.js
new file mode 100644
index 0000000000000000000000000000000000000000..44b868864a051be80abe3e3744bbe1e610089630
--- /dev/null
+++ b/src/scripts/migrate-api.js
@@ -0,0 +1,48 @@
+const Client = require('mongodb').MongoClient;
+
+const connect = async (url) => {
+  const client = new Client(url, { useUnifiedTopology: true });
+  await client.connect();
+  return client.db();
+};
+
+const main = async () => {
+  db = await connect('mongodb://etas-prod-playground-db:27017/playground-be');
+  const models = await db
+    .collection('models')
+    .find({
+      custom_apis: {
+        $exists: true,
+      },
+    })
+    .toArray();
+
+  await db.collection('models').updateMany({ api_version: { $exists: false } }, { $set: { api_version: 'v4.1' } });
+
+  const before = await db.collection('extendedapis').count();
+  console.log('Before:', before);
+  const promises = [];
+
+  models.forEach(async (model) => {
+    const custom_apis = model.custom_apis;
+    custom_apis.forEach(async (custom_api) => {
+      const newData = {
+        apiName: custom_api.name,
+        model: model._id,
+        skeleton: custom_api.skeleton || '{}',
+        tags: custom_api.tags || [],
+        type: custom_api.type || 'branch',
+        datatype: custom_api.datatype || (custom_api.type !== 'branch' ? 'string' : null),
+        description: custom_api.description || '',
+        isWishlist: true
+      };
+      promises.push(db.collection('extendedapis').insertOne(newData));
+    });
+  });
+
+  await Promise.allSettled(promises).catch((err) => console.error(err));
+  const after = await db.collection('extendedapis').count();
+  console.log('after:', after);
+};
+
+main();
diff --git a/src/services/api.service.js b/src/services/api.service.js
index b7da797f53e09b15c84600c73156956f3bef86fb..1978507f0d541501772c916f85d31e30464ff4e8 100644
--- a/src/services/api.service.js
+++ b/src/services/api.service.js
@@ -1,7 +1,12 @@
 const httpStatus = require('http-status');
 const { userService } = require('.');
-const { Api } = require('../models');
+const { Api, Model, ExtendedApi } = require('../models');
 const ApiError = require('../utils/ApiError');
+const fs = require('fs');
+const path = require('path');
+const logger = require('../config/logger');
+const { isArray } = require('lodash');
+const { sortObject } = require('../utils/sort');
 
 /**
  *
@@ -78,10 +83,92 @@ const deleteApi = async (apiId, userId) => {
   await api.remove();
 };
 
+const listVSSVersions = async () => {
+  let versions;
+  try {
+    const rawData = fs.readFileSync(path.join(__dirname, '../../data/vss.json'));
+    versions = rawData ? JSON.parse(rawData, 'utf8') : [];
+  } catch (error) {
+    logger.error(error);
+    versions = [];
+  }
+  return versions;
+};
+
+/**
+ *
+ * @param {string} name
+ * @returns {Promise<object>}
+ */
+const getVSSVersion = async (name) => {
+  const filePath = path.join(__dirname, `../../data/${name}.json`);
+  if (!fs.existsSync(filePath)) {
+    throw new ApiError(httpStatus.NOT_FOUND, 'VSS version not found');
+  }
+  const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
+  return data;
+};
+
+/**
+ *
+ * @param {string} modelId
+ */
+const computeVSSApi = async (modelId) => {
+  const model = await Model.findById(modelId);
+  if (!model) {
+    throw new ApiError(httpStatus.NOT_FOUND, 'Model not found');
+  }
+  let ret = null;
+
+  const apiVersion = model.api_version;
+  if (!apiVersion) {
+    ret = {
+      Vehicle: {
+        description: 'Vehicle',
+        type: 'branch',
+        children: {},
+      },
+    };
+  } else {
+    ret = await getVSSVersion(apiVersion);
+  }
+
+  const extendedApis = await ExtendedApi.find({
+    model: modelId,
+    isWishlist: true
+  });
+  extendedApis.forEach((extendedApi) => {
+    try {
+      const name = extendedApi.apiName.split('.').slice(1).join('.');
+      if (!name) return;
+      ret['Vehicle'].children[name] = {
+        description: extendedApi.description,
+        type: extendedApi.type || 'branch',
+        id: extendedApi._id,
+        datatype: extendedApi.datatype,
+        name: extendedApi.apiName,
+        isWishlist: extendedApi.isWishlist
+      };
+    } catch (error) {
+      logger.warn(`Error while processing extended API ${extendedApi._id} with name ${extendedApi.apiName}: ${error}`);
+    }
+  });
+
+  try {
+    ret['Vehicle'].children = sortObject(ret['Vehicle'].children);
+  } catch (error) {
+    logger.warn(`Error while sorting object: ${error}`);
+  }
+  return ret;
+};
+
 module.exports = {
   createApi,
   getApi,
   getApiByModelId,
   updateApi,
   deleteApi,
+  listVSSVersions,
+  getVSSVersion,
+  computeVSSApi,
 };
diff --git a/src/services/extendedApi.service.js b/src/services/extendedApi.service.js
index 95ea869fc7683a62f791076318f6cc199012da52..264bb5c1f4016eace709720b70670253d2287df5 100644
--- a/src/services/extendedApi.service.js
+++ b/src/services/extendedApi.service.js
@@ -1,6 +1,8 @@
 const httpStatus = require('http-status');
 const { ExtendedApi } = require('../models');
 const ApiError = require('../utils/ApiError');
+const { permissionService } = require('.');
+const { PERMISSIONS } = require('../config/roles');
 
 /**
  * Create a new ExtendedApi
@@ -8,6 +10,10 @@ const ApiError = require('../utils/ApiError');
  * @returns {Promise<ExtendedApi>}
  */
 const createExtendedApi = async (extendedApiBody) => {
+  const existingExtendedApi = await ExtendedApi.findOne({ apiName: extendedApiBody.apiName, model: extendedApiBody.model });
+  if (existingExtendedApi) {
+    throw new ApiError(httpStatus.BAD_REQUEST, 'An extended API with the same name already exists for this model');
+  }
   return ExtendedApi.create(extendedApiBody);
 };
 
@@ -38,13 +44,17 @@ const getExtendedApiById = async (id) => {
  * Update ExtendedApi by id
  * @param {ObjectId} extendedApiId
  * @param {Object} updateBody
+ * @param {string} userId
  * @returns {Promise<ExtendedApi>}
  */
-const updateExtendedApiById = async (extendedApiId, updateBody) => {
+const updateExtendedApiById = async (extendedApiId, updateBody, userId) => {
   const extendedApi = await getExtendedApiById(extendedApiId);
   if (!extendedApi) {
     throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found');
   }
+  if (!(await permissionService.hasPermission(userId, PERMISSIONS.WRITE_MODEL, extendedApi.model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden. You do not have permission to update extended API for this model');
+  }
   Object.assign(extendedApi, updateBody);
   await extendedApi.save();
   return extendedApi;
@@ -53,13 +63,17 @@ const updateExtendedApiById = async (extendedApiId, updateBody) => {
 /**
  * Delete ExtendedApi by id
  * @param {ObjectId} extendedApiId
+ * @param {string} userId
  * @returns {Promise<ExtendedApi>}
  */
-const deleteExtendedApiById = async (extendedApiId) => {
+const deleteExtendedApiById = async (extendedApiId, userId) => {
   const extendedApi = await getExtendedApiById(extendedApiId);
   if (!extendedApi) {
     throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found');
   }
+  if (!(await permissionService.hasPermission(userId, PERMISSIONS.WRITE_MODEL, extendedApi.model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden. You do not have permission to update extended API for this model');
+  }
   await extendedApi.remove();
   return extendedApi;
 };
diff --git a/src/services/file.service.js b/src/services/file.service.js
index 3c49c71c3a55f21344510c4eac06f2f37e4f4fdb..fdcc13f86a542f027f54176d5aad39181aed8049 100644
--- a/src/services/file.service.js
+++ b/src/services/file.service.js
@@ -3,6 +3,7 @@ const ApiError = require('../utils/ApiError');
 const httpStatus = require('http-status');
 const logger = require('../config/logger');
 const config = require('../config/config');
+const fs = require('fs');
 
 /**
  *
@@ -47,6 +48,16 @@ const getFileFromURL = async (url, encoding = 'File') => {
   }
 };
 
+const downloadFile = async (url, path) => {
+  try {
+    const arrayBuffer = await (await fetch(url)).arrayBuffer();
+    fs.writeFileSync(path, new Uint8Array(arrayBuffer));
+  } catch (error) {
+    logger.error(`Failed to download file from ${url}`);
+    logger.error(error);
+  }
+};
+
 /**
  *
  * @param {File} file1
@@ -57,4 +68,5 @@ const compareImages = async (file1, file2) => {};
 module.exports = {
   upload,
   getFileFromURL,
+  downloadFile,
 };
diff --git a/src/services/listener.service.js b/src/services/listener.service.js
index ef9d8529a7776bd9cf756916503d4319e6869814..eec861b4a3226c9a59dbb21e1cf27763b2f802a4 100644
--- a/src/services/listener.service.js
+++ b/src/services/listener.service.js
@@ -4,7 +4,11 @@ const findSocketByUser = (userId) => {
   const io = getIO();
   let socket = null;
   io.sockets.sockets.forEach((value) => {
-    if (String(value.request._query.userId) === String(userId)) {
+    const user = value.user;
+    if (!user) {
+      return;
+    }
+    if (String(user._id) === String(userId)) {
       socket = value;
     }
   });
diff --git a/src/services/permission.service.js b/src/services/permission.service.js
index d9e914c3105d402e4648664259f7711c964427af..499246387ebac11fc5b4dd2ce9147f2e30a1a9d1 100644
--- a/src/services/permission.service.js
+++ b/src/services/permission.service.js
@@ -121,12 +121,12 @@ const getMappedRoles = (roles) => {
       if (!Array.isArray(existingRolePermissions)) {
         existingRolePermissions = [];
       } else {
-        const newArray = existingRolePermissions.concat(role.role.permissions);
+        const newArray = existingRolePermissions.concat(role?.role?.permissions || []);
         existingRolePermissions = Array.from(new Set(newArray));
       }
       map.set(roleRef, existingRolePermissions);
     } else {
-      map.set(roleRef, role.role.permissions);
+      map.set(roleRef, role?.role?.permissions || []);
     }
   });
   return map;
@@ -135,7 +135,9 @@ const getMappedRoles = (roles) => {
 // Check if the role map contains the permission
 const containsPermission = (roleMap, permission, id) => {
   const stringId = String(id);
+  // In case user is admin, have access to all type of resources
   const firstCondition = roleMap.has('*') && roleMap.get('*').includes(permission);
+  // In case user has access to specific resource
   const secondCondition = roleMap.has(stringId) && roleMap.get(stringId).includes(permission);
   return firstCondition || secondCondition;
 };
@@ -154,7 +156,7 @@ const checkModelPermission = (model, userId, permission) => {
 };
 
 const checkPrototypePermission = (prototype, userId, permission) => {
-  if (String(prototype.created_by) === String(userId)) {
+  if (String(prototype.created_by) === String(userId) || String(prototype.model_id?.created_by) === String(userId)) {
     return true;
   }
 
@@ -256,6 +258,21 @@ const listReadableModelIds = async (userId) => {
   return Array.from(results);
 };
 
+/**
+ *
+ * @param {string} userId
+ * @param {string} modelId
+ * @returns {Promise<boolean>}
+ */
+const canAccessModel = async (userId, modelId) => {
+  const model = await Model.findById(modelId);
+  if (!model) {
+    throw new ApiError(httpStatus.NOT_FOUND, 'Model not found');
+  }
+  if (model.visibility === 'public') return true;
+  return hasPermission(userId, PERMISSIONS.READ_MODEL, modelId);
+};
+
 module.exports = {
   listAuthorizedUser,
   assignRoleToUser,
@@ -268,4 +285,5 @@ module.exports = {
   getRoles,
   getPermissions,
   listReadableModelIds,
+  canAccessModel,
 };
diff --git a/src/services/prototype.service.js b/src/services/prototype.service.js
index 6014f45c20c1266219a26b48348e6bb4aa2e5bba..8d405163850f1fb96581b2dd89b01aeef12347bb 100644
--- a/src/services/prototype.service.js
+++ b/src/services/prototype.service.js
@@ -149,7 +149,8 @@ const listRecentPrototypes = async (userId) => {
 
   const prototypes = await Prototype.find({ _id: { $in: Array.from(prototypeMap.keys()) } })
     .select('name model_id description image_file executed_turns')
-    .populate('model', 'name visibility');
+    .populate('model', 'name visibility')
+    .populate('created_by', 'name image_file');
 
   const results = [];
   recentData.forEach((data) => {
@@ -189,11 +190,13 @@ const listPopularPrototypes = async () => {
   ).map((model) => String(model._id));
   return Prototype.find({
     model_id: { $in: publicModelIds },
+    state: 'Released',
   })
     .sort({ executed_turns: -1 })
     .limit(8)
     .select('name model_id description image_file executed_turns')
-    .populate('model', 'name visibility');
+    .populate('model', 'name visibility')
+    .populate('created_by', 'name image_file');
 };
 
 module.exports = {
diff --git a/src/services/user.service.js b/src/services/user.service.js
index 4493a5c32080675da40e54e53b0ff81b9da379f4..7787878edb187e61ecc8bc80c4603d749a7fdc4a 100644
--- a/src/services/user.service.js
+++ b/src/services/user.service.js
@@ -3,6 +3,7 @@ const { User } = require('../models');
 const ApiError = require('../utils/ApiError');
 const image = require('../utils/image');
 const fileService = require('./file.service');
+const logger = require('../config/logger');
 
 /**
  * Create a user
@@ -140,13 +141,18 @@ const updateSSOUser = async (user, graphData) => {
     updateBody.name = graphData.displayName;
   }
 
-  if (userPhoto) {
-    const photoBuffer = await userPhoto.arrayBuffer();
-    const diff = await image.diff(user?.image_file, photoBuffer);
-    if (diff > 0.1 || diff === -1) {
-      const { url } = await fileService.upload(userPhoto);
-      updateBody.image_file = url;
+  try {
+    if (userPhoto) {
+      const photoBuffer = await userPhoto.arrayBuffer();
+      const diff = await image.diff(user?.image_file, photoBuffer);
+      if (diff > 0.1 || diff === -1) {
+        const { url } = await fileService.upload(userPhoto);
+        updateBody.image_file = url;
+      }
     }
+  } catch (error) {
+    logger.error('Error updating user photo');
+    logger.error(error);
   }
 
   if (Object.keys(updateBody).length === 0) {
@@ -170,9 +176,14 @@ const createSSOUser = async (graphData) => {
     provider_user_id: graphData.id,
   };
 
-  if (userPhoto) {
-    const { url } = await fileService.upload(userPhoto);
-    userBody.image_file = url;
+  try {
+    if (userPhoto) {
+      const { url } = await fileService.upload(userPhoto);
+      userBody.image_file = url;
+    }
+  } catch (error) {
+    logger.error('Error creating user photo');
+    logger.error(error);
   }
 
   return createUser(userBody);
diff --git a/src/utils/setupEtasStream.js b/src/utils/setupEtasStream.js
new file mode 100644
index 0000000000000000000000000000000000000000..4e3e1942965098cde9fbfa3480c9ffe52130348c
--- /dev/null
+++ b/src/utils/setupEtasStream.js
@@ -0,0 +1,36 @@
+const WebSocketClient = require('websocket').client;
+const { getIO } = require('../config/socket');
+const config = require('../config/config.js');
+let client = null;
+
+const setupClient = (token) => {
+  if (!client) {
+    const client = new WebSocketClient();
+
+    client.on('connectFailed', function (error) {
+      console.log('Connect etas websocket error: ' + error.toString());
+    });
+    client.on('connect', function (connection) {
+      console.log('Etas WebSocket Client Connected');
+      connection.on('error', function (error) {
+        console.log('Connection Error: ' + error.toString());
+      });
+      connection.on('close', function () {
+        console.log('echo-protocol Connection Closed');
+      });
+      connection.on('message', function (message) {
+        const io = getIO();
+        console.log(message.utf8Data);
+        io?.sockets?.emit('etas-stream', message.utf8Data);
+      });
+    });
+
+    client.connect(`wss://${config.etas.instanceEndpoint}/ws`, null, null, {
+      authorization: `Bearer ${token}`,
+    });
+  }
+};
+
+module.exports = {
+  setupClient,
+};
diff --git a/src/utils/sort.js b/src/utils/sort.js
new file mode 100644
index 0000000000000000000000000000000000000000..a5e37d4873cf595c0c72384e6ea3b5b2aa728454
--- /dev/null
+++ b/src/utils/sort.js
@@ -0,0 +1,12 @@
+const sortObject = (obj) => {
+  return Object.keys(obj)
+    .sort()
+    .reduce((acc, key) => {
+      acc[key] = obj[key];
+      return acc;
+    }, {});
+};
+
+module.exports = {
+  sortObject,
+};
diff --git a/src/validations/api.validation.js b/src/validations/api.validation.js
index 862cd5db6ef865650167f2861c8d98839469c887..be0edfe190ecd27763cd12b567df31170a72f8a8 100644
--- a/src/validations/api.validation.js
+++ b/src/validations/api.validation.js
@@ -38,10 +38,17 @@ const deleteApi = {
   }),
 };
 
+const getVSSVersion = {
+  params: Joi.object().keys({
+    name: Joi.string().required(),
+  }),
+};
+
 module.exports = {
   createApi,
   getApi,
   getApiByModelId,
   updateApi,
   deleteApi,
+  getVSSVersion,
 };
diff --git a/src/validations/discussion.validation.js b/src/validations/discussion.validation.js
index 1fc48db1d5844fb3527e89a39e8da046625a1ae7..5315e98262f716c1664910bd7fdbf4ee557af34f 100644
--- a/src/validations/discussion.validation.js
+++ b/src/validations/discussion.validation.js
@@ -1,5 +1,6 @@
 const Joi = require('joi');
 const { objectId } = require('./custom.validation');
+const { populate } = require('../models/token.model');
 
 const createDiscussion = {
   body: Joi.object().keys({
@@ -20,6 +21,7 @@ const listDiscussions = {
     limit: Joi.number().integer(),
     page: Joi.number().integer(),
     fields: Joi.string(),
+    populate: Joi.any(),
   }),
 };
 
diff --git a/src/validations/extendedApi.validation.js b/src/validations/extendedApi.validation.js
index e589b7ed6e2fa51206f911d64f0ce5b508b940cd..b4f3477caa8c722ea5d423d3b5a94bbfc081a7c6 100644
--- a/src/validations/extendedApi.validation.js
+++ b/src/validations/extendedApi.validation.js
@@ -7,24 +7,22 @@ const createExtendedApi = {
     model: Joi.string().custom(objectId).required(),
     skeleton: Joi.string().optional(),
     type: Joi.string(),
-    data_type: Joi.string(),
-    description: Joi.string(),
-    tags: Joi.array()
-      .items(
-        Joi.object({
-          tagCategoryId: Joi.string(),
-          tagCategoryName: Joi.string(),
-          tag: Joi.string(),
-        })
-      )
-      .optional(),
+    datatype: Joi.string(),
+    description: Joi.string().allow('').default(''),
+    tags: Joi.array().items(
+      Joi.object().keys({
+        title: Joi.string().required(),
+        description: Joi.string().allow(''),
+      })
+    ),
+    isWishlist: Joi.boolean().default(false),
   }),
 };
 
 const getExtendedApis = {
   query: Joi.object().keys({
     apiName: Joi.string(),
-    model: Joi.string().custom(objectId),
+    model: Joi.string().custom(objectId).required(),
     sortBy: Joi.string(),
     limit: Joi.number().integer(),
     page: Joi.number().integer(),
@@ -46,20 +44,17 @@ const updateExtendedApi = {
       apiName: Joi.string()
         .regex(/^Vehicle\./)
         .message('apiName must start with Vehicle.'),
-      model: Joi.string().custom(objectId),
       skeleton: Joi.string().optional(),
       type: Joi.string(),
-      data_type: Joi.string(),
-      description: Joi.string(),
-      tags: Joi.array()
-        .items(
-          Joi.object({
-            tagCategoryId: Joi.string(),
-            tagCategoryName: Joi.string(),
-            tag: Joi.string(),
-          })
-        )
-        .optional(),
+      datatype: Joi.string(),
+      description: Joi.string().allow(''),
+      tags: Joi.array().items(
+        Joi.object().keys({
+          title: Joi.string().required(),
+          description: Joi.string().allow(''),
+        })
+      ),
+      isWishlist: Joi.boolean(),
     })
     .min(1),
 };
diff --git a/src/validations/model.validation.js b/src/validations/model.validation.js
index adac0ac8e5a170228308854145feeb6bb23ea23a..d2fd0b036d63841dbef986b9fbf22bff9100eea6 100644
--- a/src/validations/model.validation.js
+++ b/src/validations/model.validation.js
@@ -6,7 +6,9 @@ const createModel = {
   body: Joi.object().keys({
     extend: Joi.any(),
     custom_apis: Joi.string().custom(jsonString),
-    cvi: Joi.string().required().custom(jsonString),
+    api_version: Joi.string(),
+    cvi: Joi.string().custom(jsonString),
+    extended_apis: Joi.array().items(Joi.any()),
     main_api: Joi.string().required().max(255),
     model_home_image_file: Joi.string()
       .allow('')
@@ -23,9 +25,8 @@ const createModel = {
     skeleton: Joi.string().custom(jsonString),
     tags: Joi.array().items(
       Joi.object().keys({
-        tag: Joi.string().required(),
-        tagCategoryId: Joi.string().required().custom(slug),
-        tagCategoryName: Joi.string().required(),
+        title: Joi.string().required(),
+        description: Joi.string().allow(''),
       })
     ),
   }),
@@ -53,6 +54,7 @@ const updateModel = {
     .keys({
       extend: Joi.any(),
       custom_apis: Joi.string().custom(jsonString),
+      api_version: Joi.string(),
       cvi: Joi.string().custom(jsonString),
       main_api: Joi.string().max(255),
       model_home_image_file: Joi.string().allow(''),
@@ -64,9 +66,8 @@ const updateModel = {
       skeleton: Joi.string().custom(jsonString),
       tags: Joi.array().items(
         Joi.object().keys({
-          tag: Joi.string().required(),
-          tagCategoryId: Joi.string().required().custom(slug),
-          tagCategoryName: Joi.string().required(),
+          title: Joi.string().required(),
+          description: Joi.string().allow(''),
         })
       ),
     })
@@ -108,6 +109,12 @@ const deleteAuthorizedUser = {
   }),
 };
 
+const getApiByModelId = {
+  params: Joi.object().keys({
+    id: Joi.string().custom(objectId),
+  }),
+};
+
 module.exports = {
   createModel,
   listModels,
@@ -116,4 +123,5 @@ module.exports = {
   deleteModel,
   addAuthorizedUser,
   deleteAuthorizedUser,
+  getApiByModelId,
 };
diff --git a/src/validations/prototype.validation.js b/src/validations/prototype.validation.js
index e8f5bee45d1f69704cf9395bae9453842d34bfe4..521f8e220344584ddd79483338d578972ee64459 100644
--- a/src/validations/prototype.validation.js
+++ b/src/validations/prototype.validation.js
@@ -5,6 +5,7 @@ const { objectId, jsonString, slug } = require('./custom.validation');
 const createPrototype = {
   body: Joi.object().keys({
     extend: Joi.any(),
+    flow: Joi.any(),
     state: Joi.string().allow(...Object.values(stateTypes)),
     apis: Joi.object().keys({
       VSC: Joi.array().items(Joi.string()),
@@ -18,6 +19,7 @@ const createPrototype = {
       says_who: Joi.string().allow('').max(4095),
       solution: Joi.string().allow('').max(4095),
       status: Joi.string().allow('').max(255),
+      text: Joi.string().allow(''),
     }),
     image_file: Joi.string().allow(''),
     journey_image_file: Joi.string().allow(''),
@@ -32,15 +34,16 @@ const createPrototype = {
     skeleton: Joi.string().custom(jsonString),
     tags: Joi.array().items(
       Joi.object().keys({
-        tag: Joi.string().required(),
-        tagCategoryId: Joi.string().required().custom(slug),
-        tagCategoryName: Joi.string().required(),
+        title: Joi.string().required(),
+        description: Joi.string().allow(''),
       })
     ),
     widget_config: Joi.string().custom(jsonString),
     autorun: Joi.boolean(),
     related_ea_components: Joi.string().allow(''),
     partner_logo: Joi.string().allow(''),
+    language: Joi.string().default('python'),
+    requirements: Joi.string().allow(''),
     // rated_by: Joi.object().pattern(
     //   /^[0-9a-fA-F]{24}$/,
     //   Joi.object()
@@ -76,6 +79,7 @@ const getPrototype = {
 
 const updatePrototype = {
   body: Joi.object().keys({
+    flow: Joi.any(),
     extend: Joi.any(),
     state: Joi.string().allow(...Object.values(stateTypes)),
     apis: Joi.object().keys({
@@ -90,6 +94,7 @@ const updatePrototype = {
       says_who: Joi.string().allow('').max(4095),
       solution: Joi.string().allow('').max(4095),
       status: Joi.string().allow('').max(255),
+      text: Joi.string().allow(''),
     }),
     image_file: Joi.string().allow(''),
     journey_image_file: Joi.string().allow(''),
@@ -103,15 +108,16 @@ const updatePrototype = {
     skeleton: Joi.string().custom(jsonString),
     tags: Joi.array().items(
       Joi.object().keys({
-        tag: Joi.string().required(),
-        tagCategoryId: Joi.string().required().custom(slug),
-        tagCategoryName: Joi.string().required(),
+        title: Joi.string().required(),
+        description: Joi.string().allow(''),
       })
     ),
     widget_config: Joi.string().custom(jsonString),
     autorun: Joi.boolean(),
     related_ea_components: Joi.string().allow(''),
     partner_logo: Joi.string().allow(''),
+    requirements: Joi.string().allow(''),
+    language: Joi.string(),
     // rated_by: Joi.object().pattern(
     //   /^[0-9a-fA-F]{24}$/,
     //   Joi.object()
diff --git a/yarn.lock b/yarn.lock
index 70731548a8ccb3cbde22be8128d9e37b660ce4c8..ac02d3d2e64fe50a078406689cd6f256a5104431 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2245,6 +2245,11 @@
   dependencies:
     "@types/node" "*"
 
+"@types/lodash@^4.17.12":
+  version "4.17.12"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.12.tgz#25d71312bf66512105d71e55d42e22c36bcfc689"
+  integrity sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==
+
 "@types/long@^4.0.0":
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
@@ -3058,7 +3063,7 @@ buffer@^5.2.0, buffer@^5.5.0:
     base64-js "^1.3.1"
     ieee754 "^1.1.13"
 
-bufferutil@^4.0.8:
+bufferutil@^4.0.1, bufferutil@^4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea"
   integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==
@@ -3598,6 +3603,14 @@ culvert@^0.1.2:
   resolved "https://registry.yarnpkg.com/culvert/-/culvert-0.1.2.tgz#9502f5f0154a2d5a22a023e79f71cc936fa6ef6f"
   integrity sha1-lQL18BVKLVoioCPnn3HMk2+m728=
 
+d@1, d@^1.0.1, d@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de"
+  integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==
+  dependencies:
+    es5-ext "^0.10.64"
+    type "^2.7.2"
+
 dashdash@^1.12.0:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -3939,10 +3952,10 @@ engine.io-parser@~5.2.1:
   resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49"
   integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==
 
-engine.io@~6.5.2:
-  version "6.5.5"
-  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.5.tgz#430b80d8840caab91a50e9e23cb551455195fc93"
-  integrity sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==
+engine.io@~6.6.0:
+  version "6.6.1"
+  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.1.tgz#a82b1e5511239a0e95fac14516870ee9138febc8"
+  integrity sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og==
   dependencies:
     "@types/cookie" "^0.4.1"
     "@types/cors" "^2.8.12"
@@ -4000,6 +4013,33 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
+es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.63, es5-ext@^0.10.64, es5-ext@~0.10.14:
+  version "0.10.64"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714"
+  integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==
+  dependencies:
+    es6-iterator "^2.0.3"
+    es6-symbol "^3.1.3"
+    esniff "^2.0.1"
+    next-tick "^1.1.0"
+
+es6-iterator@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+  integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==
+  dependencies:
+    d "1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@^3.1.3:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c"
+  integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==
+  dependencies:
+    d "^1.0.2"
+    ext "^1.7.0"
+
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -4204,6 +4244,16 @@ eslint@^7.0.0:
     text-table "^0.2.0"
     v8-compile-cache "^2.0.3"
 
+esniff@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308"
+  integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==
+  dependencies:
+    d "^1.0.1"
+    es5-ext "^0.10.62"
+    event-emitter "^0.3.5"
+    type "^2.7.2"
+
 espree@^7.3.0, espree@^7.3.1:
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
@@ -4252,6 +4302,14 @@ etag@~1.8.1:
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
   integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
 
+event-emitter@^0.3.5:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+  integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+
 event-target-shim@^5.0.0:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
@@ -4416,6 +4474,13 @@ express@^4.17.1:
     utils-merge "1.0.1"
     vary "~1.1.2"
 
+ext@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
+  integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==
+  dependencies:
+    type "^2.7.2"
+
 extend-shallow@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -6605,7 +6670,7 @@ lodash.truncate@^4.4.2:
   resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
   integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
 
-lodash@^4.17.14, lodash@^4.7.0:
+lodash@^4.17.14, lodash@^4.17.21, lodash@^4.7.0:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -7071,6 +7136,11 @@ netmask@^2.0.1:
   resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
   integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
 
+next-tick@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
+  integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
+
 nice-try@^1.0.4:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -8668,16 +8738,16 @@ socket.io-parser@~4.2.4:
     "@socket.io/component-emitter" "~3.1.0"
     debug "~4.3.1"
 
-socket.io@^4.7.5:
-  version "4.7.5"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.5.tgz#56eb2d976aef9d1445f373a62d781a41c7add8f8"
-  integrity sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==
+socket.io@^4.8.0:
+  version "4.8.0"
+  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.0.tgz#33d05ae0915fad1670bd0c4efcc07ccfabebe3b1"
+  integrity sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==
   dependencies:
     accepts "~1.3.4"
     base64id "~2.0.0"
     cors "~2.8.5"
     debug "~4.3.2"
-    engine.io "~6.5.2"
+    engine.io "~6.6.0"
     socket.io-adapter "~2.5.2"
     socket.io-parser "~4.2.4"
 
@@ -9392,6 +9462,11 @@ type-is@^1.6.18, type-is@~1.6.17, type-is@~1.6.18:
     media-typer "0.3.0"
     mime-types "~2.1.24"
 
+type@^2.7.2:
+  version "2.7.3"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486"
+  integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==
+
 typedarray-to-buffer@^3.1.5:
   version "3.1.5"
   resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@@ -9499,6 +9574,13 @@ use@^3.1.0:
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
+utf-8-validate@^5.0.2:
+  version "5.0.10"
+  resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2"
+  integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==
+  dependencies:
+    node-gyp-build "^4.3.0"
+
 utf-8-validate@^6.0.4:
   version "6.0.4"
   resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.4.tgz#1305a1bfd94cecb5a866e6fc74fd07f3ed7292e5"
@@ -9649,6 +9731,18 @@ websocket-extensions@>=0.1.1:
   resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
   integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
 
+websocket@^1.0.35:
+  version "1.0.35"
+  resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.35.tgz#374197207d7d4cc4c36cbf8a1bb886ee52a07885"
+  integrity sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==
+  dependencies:
+    bufferutil "^4.0.1"
+    debug "^2.2.0"
+    es5-ext "^0.10.63"
+    typedarray-to-buffer "^3.1.5"
+    utf-8-validate "^5.0.2"
+    yaeti "^0.0.6"
+
 whatwg-encoding@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
@@ -9852,18 +9946,6 @@ xregexp@2.0.0:
   resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
   integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=
 
-xss-clean@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/xss-clean/-/xss-clean-0.1.1.tgz#d3ba684d85ccd52054963d01ad6ab36d662db1a5"
-  integrity sha1-07poTYXM1SBUlj0BrWqzbWYtsaU=
-  dependencies:
-    xss-filters "1.2.6"
-
-xss-filters@1.2.6:
-  version "1.2.6"
-  resolved "https://registry.yarnpkg.com/xss-filters/-/xss-filters-1.2.6.tgz#68b39089cb1dff8b9dbc889484839b2f507f5c55"
-  integrity sha1-aLOQicsd/4udvIiUhIObL1B/XFU=
-
 xtend@^4.0.0:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
@@ -9879,6 +9961,11 @@ y18n@^5.0.5:
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
   integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
 
+yaeti@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
+  integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==
+
 yallist@^2.0.0:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"