Compare commits

...
Sign in to create a new pull request.

20 commits
master ... GCP

Author SHA1 Message Date
ItzCrazyKns
35a3eda213
Merge pull request #155 from notedsource/hristo/gcp-deploy-vertexai-models-embeddings
Hristo/gcp deploy vertexai models embeddings
2024-06-01 10:49:38 +05:30
Hristo
dfed6a0ad8 Use container restart policy from main 2024-05-30 17:21:40 -04:00
Hristo
e0d9522435 Merge branch 'master' of github.com:notedsource/Perplexica into hristo/gcp-deploy-vertexai-models-embeddings 2024-05-30 11:19:50 -04:00
Hristo
f7c3bc2823 No auth on root route for health checks, fix suggestions request 2024-05-30 11:18:31 -04:00
Hristo
0ac971e6b4 Merge branch 'hristo/deploy-on-gcp-gke' of github.com:notedsource/Perplexica into hristo/vertexai-embeddings 2024-05-22 15:05:45 -04:00
Hristo
4ff6502dae Restore searxng dockerfile to enable remote builds 2024-05-22 15:04:25 -04:00
Hristo
795309cfe2 Private searxng instance 2024-05-22 14:52:47 -04:00
Hristo
8bf4269208 Add vertexai text embeddings capability 2024-05-21 16:23:34 -04:00
Hristo
4c7942d2e8 Merge branch 'master' of github.com:notedsource/Perplexica into hristo/deploy-on-gcp-gke 2024-05-21 15:41:23 -04:00
Hristo
aa55206a30 Add VertexAI deps using yarn not npm 2024-05-21 15:15:19 -04:00
Hristo
27d7b000d0 Add AI/ML infrence scope to OAuth credentials requested for cluster IAM account 2024-05-21 14:31:14 -04:00
Hristo
8b9b4085ea Fix query appearing twice in chat history
The initial query appears twice in the prompt, this is ignored by OpenAI
models, however it breaks with Gemini models are they fail with an error
stating that AI and User prompts need to alternate.

Tested all search modes with both OpenAI GTP3 turbo and Vertex Gemini
1.0 and this changes appears to now function correctly with both.
2024-05-17 14:10:11 -04:00
Hristo
2e58dab30a Additional changes for VertexAI 2024-05-17 14:08:57 -04:00
Hristo
48018990be Ensure containers are brought backup when exiting on error
This is esp. important for the NodeJS (backend) container as  Node will
exit on any unhandled error, it is best practice to let the errored
process crash and start a new one in its place. It this case we use
docker to do that for us (`restart: always` policy)
2024-05-16 09:53:33 -04:00
Hristo
ebbe18ab45 Adds Google VertexAI as model provider 2024-05-14 15:05:17 -04:00
Hristo
cef75279c5 Add Google VertexAI deps. 2024-05-14 14:51:26 -04:00
Hristo
c56a058a74 Websocket auth, pass access token in gke configs 2024-05-10 19:32:35 -04:00
Hristo
4e20c4ac56 Finalizes option to secure backend http endpoints with a token
- Also fixes to build commands in makefile
2024-05-10 18:11:23 -04:00
Hristo
e6c2042df6 Backend GKE Deploy, access key for backend
- Configs and automation for deploying backend to GKE
- First steps to adding an optional token check for requests to backend
- First steps frontend sending optional token to backend when configured
2024-05-10 16:07:58 -04:00
Hristo
0fedaef537 First pass at setting up GCP deploy config as infrastructure
- Terraform config files to setup cluster, deployments and services
  - Adds only Searxng deployment and test service in this commit

- Makefile to:
  - Build and push images
  - Run terraform with correct project configuration

- Env file template to help setting .env file with project configs
2024-05-08 18:19:59 -04:00
32 changed files with 902 additions and 55 deletions

20
Makefile Normal file
View file

@ -0,0 +1,20 @@
.PHONY: run
run:
docker compose -f docker-compose.yaml up
.PHONY: rebuild-run
rebuild-run:
docker compose -f docker-compose.yaml build --no-cache \
&& docker compose -f docker-compose.yaml up
.PHONY: run-app-only
run-app-only:
docker compose -f app-docker-compose.yaml up
.PHONY: rebuild-run-app-only
rebuild-run-app-only:
docker compose -f app-docker-compose.yaml build --no-cache \
&& docker compose -f app-docker-compose.yaml up

View file

@ -118,6 +118,27 @@ If you wish to use Perplexica as an alternative to traditional search engines li
[![Deploy to RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploylobe.svg)](https://repocloud.io/details/?app_id=267) [![Deploy to RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploylobe.svg)](https://repocloud.io/details/?app_id=267)
## Deploy Perplexica backend to Google GKE
0: Install `docker` and `terraform` (Process specific to your system)
1a: Copy the `sample.env` file to `.env`
1b: Copy the `deploy/gcp/sample.env` file to `deploy/gcp/.env`
2a: Fillout desired LLM provider access keys etc. in `.env`
- Note: you will have to comeback and edit this file again once you have the address of the K8s backend deploy
2b: Fillout the GCP info in `deploy/gcp/.env`
3: Edit `GCP_REPO` to the correct docker image repo path if you are using something other than Container registry
4: Edit the `PREFIX` if you would like images and GKE entities to be prefixed with something else
5: In `deploy/gcp` run `make init` to initialize terraform
6: Follow the normal Preplexica configuration steps outlined in the project readme
7: Auth docker with the appropriate credential for repo Ex. for `gcr.io` -> `gcloud auth configure-docker`
8: In `deploy/gcp` run `make build-deplpy` to build and push the project images to the repo, create a GKE cluster and deploy the app
9: Once deployed successfully edit the `.env` file in the root project folder and update the `REMOTE_BACKEND_ADDRESS` with the remote k8s deployment address and port
10: In root project folder run `make rebuild-run-app-only`
If you configured everything correctly frontend app will run locally and provide you with a local url to open it.
Now you can run queries against the remotely deployed backend from your local machine. :celebrate:
## Upcoming Features ## Upcoming Features
- [ ] Finalizing Copilot Mode - [ ] Finalizing Copilot Mode

13
app-docker-compose.yaml Normal file
View file

@ -0,0 +1,13 @@
services:
perplexica-frontend:
build:
context: .
dockerfile: app.dockerfile
args:
- NEXT_PUBLIC_SUPER_SECRET_KEY=${SUPER_SECRET_KEY}
- NEXT_PUBLIC_API_URL=https://${REMOTE_BACKEND_ADDRESS}/api
- NEXT_PUBLIC_WS_URL=wss://${REMOTE_BACKEND_ADDRESS}
expose:
- 3000
ports:
- 3000:3000

View file

@ -2,8 +2,11 @@ FROM node:alpine
ARG NEXT_PUBLIC_WS_URL ARG NEXT_PUBLIC_WS_URL
ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_SUPER_SECRET_KEY
ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL} ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL}
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_SUPER_SECRET_KEY=${NEXT_PUBLIC_SUPER_SECRET_KEY}
WORKDIR /home/perplexica WORKDIR /home/perplexica

6
deploy/gcp/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.env
.auto.tfvars
.terraform
terraform.tfstate
terraform.tfstate.*
.terraform.lock.hcl

103
deploy/gcp/Makefile Normal file
View file

@ -0,0 +1,103 @@
# Adds all the deployment relevant sensitive information about project
include .env
# Adds secrets/ keys we have define for the project locally and deployment
include ../../.env
# Use `location-id-docker.pkg` for artifact registry Ex. west-1-docker.pkg
GCP_REPO=gcr.io
PREFIX=perplexica
SEARCH_PORT=8080
BACKEND_PORT=3001
SEARCH_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-searxng:latest
BACKEND_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-backend:latest
APP_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-app:latest
CLUSTER_NAME=$(PREFIX)-cluster
.PHONY: build-deploy
build-deploy: docker-build-all deploy
.PHONY: docker-build-all
docker-build-all: docker-build-push-searxng docker-build-push-backend docker-build-push-app
.PHONY: show_config
show_config:
@echo $(GCP_PROJECT_ID) \
&& echo $(CLUSTER_NAME) \
&& echo $(GCP_REGION) \
&& echo $(GCP_SERVICE_ACCOUNT_KEY_FILE) \
&& echo $(SEARCH_IMAGE_TAG) \
&& echo $(BACKEND_IMAGE_TAG) \
&& echo $(APP_IMAGE_TAG) \
&& echo $(SEARCH_PORT) \
&& echo $(BACKEND_PORT) \
&& echo $(OPENAI) \
&& echo $(SUPER_SECRET_KEY)
.PHONY: docker-build-push-searxng
docker-build-push-searxng:
cd ../../ && docker build -f ./deploy/gcp/searxng.dockerfile -t $(SEARCH_IMAGE_TAG) . --platform="linux/amd64"
docker push $(SEARCH_IMAGE_TAG)
.PHONY: docker-build-push-backend
docker-build-push-backend:
cd ../../ && docker build -f ./backend.dockerfile -t $(BACKEND_IMAGE_TAG) . --platform="linux/amd64"
docker push $(BACKEND_IMAGE_TAG)
.PHONY: docker-build-push-app
docker-build-push-app:
#
# cd ../../ && docker build -f ./app.dockerfile -t $(APP_IMAGE_TAG) . --platform="linux/amd64"
# docker push $(APP_IMAGE_TAG)
.PHONY: init
init:
terraform init
.PHONY: deploy
deploy:
export TF_VAR_project_id=$(GCP_PROJECT_ID) \
&& export TF_VAR_cluster_name=$(CLUSTER_NAME) \
&& export TF_VAR_region=$(GCP_REGION) \
&& export TF_VAR_key_file=$(GCP_SERVICE_ACCOUNT_KEY_FILE) \
&& export TF_VAR_search_image=$(SEARCH_IMAGE_TAG) \
&& export TF_VAR_backend_image=$(BACKEND_IMAGE_TAG) \
&& export TF_VAR_app_image=$(APP_IMAGE_TAG) \
&& export TF_VAR_search_port=$(SEARCH_PORT) \
&& export TF_VAR_backend_port=$(BACKEND_PORT) \
&& export TF_VAR_open_ai=$(OPENAI) \
&& export TF_VAR_secret_key=$(SUPER_SECRET_KEY) \
&& terraform apply
.PHONY: teardown
teardown:
export TF_VAR_project_id=$(GCP_PROJECT_ID) \
&& export TF_VAR_cluster_name=$(CLUSTER_NAME) \
&& export TF_VAR_region=$(GCP_REGION) \
&& export TF_VAR_key_file=$(GCP_SERVICE_ACCOUNT_KEY_FILE) \
&& export TF_VAR_search_image=$(SEARCH_IMAGE_TAG) \
&& export TF_VAR_backend_image=$(BACKEND_IMAGE_TAG) \
&& export TF_VAR_app_image=$(APP_IMAGE_TAG) \
&& export TF_VAR_search_port=$(SEARCH_PORT) \
&& export TF_VAR_backend_port=$(BACKEND_PORT) \
&& export TF_VAR_open_ai=$(OPENAI) \
&& export TF_VAR_secret_key=$(SUPER_SECRET_KEY) \
&& terraform destroy
.PHONY: auth-kubectl
auth-kubectl:
gcloud container clusters get-credentials $(CLUSTER_NAME) --region=$(GCP_REGION)
.PHONY: rollout-new-version-backend
rollout-new-version-backend: auth-kubectl
kubectl rollout restart deploy backend

View file

@ -0,0 +1,60 @@
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "5.28.0"
}
}
}
variable "project_id" {
description = "The ID of the project in which resources will be deployed."
type = string
}
variable "name" {
description = "The GKE Cluster name"
type = string
}
variable "region" {
description = "The GCP region to deploy to."
type = string
}
variable "key_file" {
description = "The path to the GCP service account key file."
type = string
}
provider "google" {
credentials = file(var.key_file)
project = var.project_id
region = var.region
}
resource "google_container_cluster" "cluster" {
name = var.name
location = var.region
initial_node_count = 1
remove_default_node_pool = true
}
resource "google_container_node_pool" "primary_preemptible_nodes" {
name = "${google_container_cluster.cluster.name}-node-pool"
location = var.region
cluster = google_container_cluster.cluster.name
node_count = 1
node_config {
machine_type = "n1-standard-4"
disk_size_gb = 25
spot = true
oauth_scopes = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring",
]
}
}

238
deploy/gcp/main.tf Normal file
View file

@ -0,0 +1,238 @@
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "5.28.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
provider "google" {
credentials = file(var.key_file)
project = var.project_id
region = var.region
}
data "google_client_config" "default" {
depends_on = [module.gke-cluster]
}
# Defer reading the cluster data until the GKE cluster exists.
data "google_container_cluster" "default" {
name = var.cluster_name
depends_on = [module.gke-cluster]
location = var.region
}
provider "kubernetes" {
host = "https://${data.google_container_cluster.default.endpoint}"
token = data.google_client_config.default.access_token
cluster_ca_certificate = base64decode(
data.google_container_cluster.default.master_auth[0].cluster_ca_certificate,
)
}
#####################################################################################################
# SearXNG - Search engine deployment and service
#####################################################################################################
resource "kubernetes_deployment" "searxng" {
metadata {
name = "searxng"
labels = {
app = "searxng"
}
}
spec {
replicas = 1
selector {
match_labels = {
component = "searxng"
}
}
template {
metadata {
labels = {
component = "searxng"
}
}
spec {
container {
image = var.search_image
name = "searxng-container"
port {
container_port = var.search_port
}
}
}
}
}
}
resource "kubernetes_service" "searxng_service" {
metadata {
name = "searxng-service"
namespace = "default"
annotations = {
"networking.gke.io/load-balancer-type" = "Internal" # Remove to create an external loadbalancer
}
}
spec {
selector = {
component = "searxng"
}
port {
port = var.search_port
target_port = var.search_port
}
type = "LoadBalancer"
}
}
#####################################################################################################
# Perplexica - backend deployment and service
#####################################################################################################
resource "kubernetes_deployment" "backend" {
metadata {
name = "backend"
labels = {
app = "backend"
}
}
spec {
replicas = 1
selector {
match_labels = {
component = "backend"
}
}
template {
metadata {
labels = {
component = "backend"
}
}
spec {
container {
image = var.backend_image
name = "backend-container"
port {
container_port = var.backend_port
}
env {
# searxng service ip
name = "SEARXNG_API_URL"
value = "http://${kubernetes_service.searxng_service.status[0].load_balancer[0].ingress[0].ip}:${var.search_port}"
}
env {
# openai key
name = "OPENAI"
value = var.open_ai
}
env {
# port
name = "PORT"
value = var.backend_port
}
env {
# Access key for backend
name = "SUPER_SECRET_KEY"
value = var.secret_key
}
}
}
}
}
}
resource "kubernetes_service" "backend_service" {
metadata {
name = "backend-service"
namespace = "default"
}
spec {
selector = {
component = "backend"
}
port {
port = var.backend_port
target_port = var.backend_port
}
type = "LoadBalancer"
}
}
#####################################################################################################
# Variable and module definitions
#####################################################################################################
variable "project_id" {
description = "The ID of the project in which the resources will be deployed."
type = string
}
variable "key_file" {
description = "The path to the GCP service account key file."
type = string
}
variable "region" {
description = "The GCP region to deploy to."
type = string
}
variable "cluster_name" {
description = "The GCP region to deploy to."
type = string
}
variable "search_image" {
description = "Tag for the searxng image"
type = string
}
variable "backend_image" {
description = "Tag for the Perplexica backend image"
type = string
}
variable "app_image" {
description = "Tag for the app image"
type = string
}
variable "open_ai" {
description = "OPENAI access key"
type = string
}
variable "secret_key" {
description = "Access key to secure backend endpoints"
type = string
}
variable "search_port" {
description = "Port for searxng service"
type = number
}
variable "backend_port" {
description = "Port for backend service"
type = number
}
module "gke-cluster" {
source = "./gke-cluster"
project_id = var.project_id
name = var.cluster_name
region = var.region
key_file = var.key_file
}

7
deploy/gcp/sample.env Normal file
View file

@ -0,0 +1,7 @@
# Rename this file to .env
# 0: Update to your GCP project id
# 1: Update to the path where the GCP service account credential file is kept
# 2: Update the region to your desired GCP region
GCP_PROJECT_ID=name-of-your-gcp-project
GCP_SERVICE_ACCOUNT_KEY_FILE=/Path/to/your/gcp-service-account-key-file.json
GCP_REGION=us-east1

View file

@ -0,0 +1,3 @@
FROM searxng/searxng
COPY searxng/ /etc/searxng/

View file

@ -14,7 +14,18 @@ services:
context: . context: .
dockerfile: backend.dockerfile dockerfile: backend.dockerfile
args: args:
- SEARXNG_API_URL=http://searxng:8080 - SEARXNG_API_URL=null
volumes:
- "/Volumes/keys/headllamp/keys/:/var/keys/"
- "${GOOGLE_APPLICATION_CREDENTIALS}:/var/keys/gcp_service_account.json"
environment:
SEARXNG_API_URL: 'http://searxng:8080'
SUPER_SECRET_KEY: ${SUPER_SECRET_KEY}
OPENAI: ${OPENAI}
GROQ: ${GROQ}
OLLAMA_API_URL: ${OLLAMA_API_URL}
GOOGLE_APPLICATION_CREDENTIALS: /var/keys/gcp_service_account.json
USE_JWT: ${USE_JWT}
depends_on: depends_on:
- searxng - searxng
ports: ports:
@ -28,6 +39,7 @@ services:
context: . context: .
dockerfile: app.dockerfile dockerfile: app.dockerfile
args: args:
- NEXT_PUBLIC_SUPER_SECRET_KEY=${SUPER_SECRET_KEY}
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api - NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001 - NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
depends_on: depends_on:

View file

@ -21,6 +21,7 @@
}, },
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@langchain/google-vertexai": "^0.0.16",
"@langchain/openai": "^0.0.25", "@langchain/openai": "^0.0.25",
"@xenova/transformers": "^2.17.1", "@xenova/transformers": "^2.17.1",
"axios": "^1.6.8", "axios": "^1.6.8",

24
sample.env Normal file
View file

@ -0,0 +1,24 @@
# Copy this file over to .env and fill in the desired config.
# .env will become available to docker compose and these values will be
# used when running docker compose up
# Edit to set OpenAI access key
OPENAI=ADD OPENAI KEY HERE
# Uncomment and edit to set GROQ access key
# GROQ: ${GROQ}
# Uncomment and edit to set OLLAMA Url
# OLLAMA_API_URL: ${OLLAMA_API_URL}
# Address and port of the remotely deployed Perplexica backend
REMOTE_BACKEND_ADDRESS=111.111.111.111:0000
# Uncomment and edit to configure backend to reject requests without token
# leave commented to have open access to all endpoints
# Secret key to "secure" backend
# SUPER_SECRET_KEY=THISISASUPERSECRETKEYSERIOUSLY
# Uncomment and edit to configure a specific service account key file to use to
# auth with VertexAI when running (backend) full Perplexica stack locally
# GOOGLE_APPLICATION_CREDENTIALS=/absolute/path/to/gcp-service-account-key-file.json

View file

@ -209,7 +209,6 @@ const createBasicAcademicSearchAnsweringChain = (
ChatPromptTemplate.fromMessages([ ChatPromptTemplate.fromMessages([
['system', basicAcademicSearchResponsePrompt], ['system', basicAcademicSearchResponsePrompt],
new MessagesPlaceholder('chat_history'), new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]), ]),
llm, llm,
strParser, strParser,

View file

@ -205,7 +205,6 @@ const createBasicRedditSearchAnsweringChain = (
ChatPromptTemplate.fromMessages([ ChatPromptTemplate.fromMessages([
['system', basicRedditSearchResponsePrompt], ['system', basicRedditSearchResponsePrompt],
new MessagesPlaceholder('chat_history'), new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]), ]),
llm, llm,
strParser, strParser,

View file

@ -203,7 +203,6 @@ const createBasicWebSearchAnsweringChain = (
ChatPromptTemplate.fromMessages([ ChatPromptTemplate.fromMessages([
['system', basicWebSearchResponsePrompt], ['system', basicWebSearchResponsePrompt],
new MessagesPlaceholder('chat_history'), new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]), ]),
llm, llm,
strParser, strParser,

View file

@ -165,7 +165,6 @@ const createBasicWolframAlphaSearchAnsweringChain = (llm: BaseChatModel) => {
ChatPromptTemplate.fromMessages([ ChatPromptTemplate.fromMessages([
['system', basicWolframAlphaSearchResponsePrompt], ['system', basicWolframAlphaSearchResponsePrompt],
new MessagesPlaceholder('chat_history'), new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]), ]),
llm, llm,
strParser, strParser,

View file

@ -46,7 +46,6 @@ const createWritingAssistantChain = (llm: BaseChatModel) => {
ChatPromptTemplate.fromMessages([ ChatPromptTemplate.fromMessages([
['system', writingAssistantPrompt], ['system', writingAssistantPrompt],
new MessagesPlaceholder('chat_history'), new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]), ]),
llm, llm,
strParser, strParser,

View file

@ -205,7 +205,6 @@ const createBasicYoutubeSearchAnsweringChain = (
ChatPromptTemplate.fromMessages([ ChatPromptTemplate.fromMessages([
['system', basicYoutubeSearchResponsePrompt], ['system', basicYoutubeSearchResponsePrompt],
new MessagesPlaceholder('chat_history'), new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]), ]),
llm, llm,
strParser, strParser,

View file

@ -3,7 +3,8 @@ import express from 'express';
import cors from 'cors'; import cors from 'cors';
import http from 'http'; import http from 'http';
import routes from './routes'; import routes from './routes';
import { getPort } from './config'; import { requireAccessKey } from './auth';
import { getAccessKey, getPort } from './config';
import logger from './utils/logger'; import logger from './utils/logger';
const port = getPort(); const port = getPort();
@ -13,11 +14,21 @@ const server = http.createServer(app);
const corsOptions = { const corsOptions = {
origin: '*', origin: '*',
allowedHeaders: ['Authorization', 'Content-Type'],
}; };
app.use(cors(corsOptions)); app.use(cors(corsOptions));
if (getAccessKey()) {
app.all('/api/*', requireAccessKey);
}
app.use(express.json()); app.use(express.json());
app.get('/', (_, res) => {
res.status(200).json({ status: 'ok' });
});
app.use('/api', routes); app.use('/api', routes);
app.get('/api', (_, res) => { app.get('/api', (_, res) => {
res.status(200).json({ status: 'ok' }); res.status(200).json({ status: 'ok' });

29
src/auth.ts Normal file
View file

@ -0,0 +1,29 @@
import { auth } from 'google-auth-library';
import { getAccessKey } from './config';
export const requireAccessKey = (req, res, next) => {
const authHeader = req.headers.authorization;
if (authHeader) {
if (!checkAccessKey(authHeader)) {
return res.sendStatus(403);
}
next();
} else {
res.sendStatus(401);
}
};
export const checkAccessKey = (authHeader) => {
const token = authHeader.split(' ')[1];
return Boolean(authHeader && token === getAccessKey());
};
export const hasGCPCredentials = async () => {
try {
const credentials = await auth.getCredentials();
return Object.keys(credentials).length > 0;
} catch (e) {
return false;
}
};

View file

@ -8,6 +8,7 @@ interface Config {
GENERAL: { GENERAL: {
PORT: number; PORT: number;
SIMILARITY_MEASURE: string; SIMILARITY_MEASURE: string;
SUPER_SECRET_KEY: string;
}; };
API_KEYS: { API_KEYS: {
OPENAI: string; OPENAI: string;
@ -28,18 +29,43 @@ const loadConfig = () =>
fs.readFileSync(path.join(__dirname, `../${configFileName}`), 'utf-8'), fs.readFileSync(path.join(__dirname, `../${configFileName}`), 'utf-8'),
) as any as Config; ) as any as Config;
const loadEnv = () => {
return {
GENERAL: {
PORT: Number(process.env.PORT),
SIMILARITY_MEASURE: process.env.SIMILARITY_MEASURE,
SUPER_SECRET_KEY: process.env.SUPER_SECRET_KEY,
},
API_KEYS: {
OPENAI: process.env.OPENAI,
GROQ: process.env.GROQ,
},
API_ENDPOINTS: {
SEARXNG: process.env.SEARXNG_API_URL,
OLLAMA: process.env.OLLAMA_API_URL,
},
} as Config;
};
export const getPort = () => loadConfig().GENERAL.PORT; export const getPort = () => loadConfig().GENERAL.PORT;
export const getAccessKey = () =>
loadEnv().GENERAL.SUPER_SECRET_KEY || loadConfig().GENERAL.SUPER_SECRET_KEY;
export const getSimilarityMeasure = () => export const getSimilarityMeasure = () =>
loadConfig().GENERAL.SIMILARITY_MEASURE; loadConfig().GENERAL.SIMILARITY_MEASURE;
export const getOpenaiApiKey = () => loadConfig().API_KEYS.OPENAI; export const getOpenaiApiKey = () =>
loadEnv().API_KEYS.OPENAI || loadConfig().API_KEYS.OPENAI;
export const getGroqApiKey = () => loadConfig().API_KEYS.GROQ; export const getGroqApiKey = () =>
loadEnv().API_KEYS.GROQ || loadConfig().API_KEYS.GROQ;
export const getSearxngApiEndpoint = () => loadConfig().API_ENDPOINTS.SEARXNG; export const getSearxngApiEndpoint = () =>
loadEnv().API_ENDPOINTS.SEARXNG || loadConfig().API_ENDPOINTS.SEARXNG;
export const getOllamaApiEndpoint = () => loadConfig().API_ENDPOINTS.OLLAMA; export const getOllamaApiEndpoint = () =>
loadEnv().API_ENDPOINTS.OLLAMA || loadConfig().API_ENDPOINTS.OLLAMA;
export const updateConfig = (config: RecursivePartial<Config>) => { export const updateConfig = (config: RecursivePartial<Config>) => {
const currentConfig = loadConfig(); const currentConfig = loadConfig();

View file

@ -1,7 +1,10 @@
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'; import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
import { ChatOllama } from '@langchain/community/chat_models/ollama'; import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { VertexAI } from "@langchain/google-vertexai";
import { GoogleVertexAIEmbeddings } from "@langchain/community/embeddings/googlevertexai";
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama'; import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
import { HuggingFaceTransformersEmbeddings } from './huggingfaceTransformer'; import { HuggingFaceTransformersEmbeddings } from './huggingfaceTransformer';
import { hasGCPCredentials } from '../auth';
import { import {
getGroqApiKey, getGroqApiKey,
getOllamaApiEndpoint, getOllamaApiEndpoint,
@ -117,6 +120,23 @@ export const getAvailableChatModelProviders = async () => {
} }
} }
if (await hasGCPCredentials()) {
try {
models['vertexai'] = {
'gemini-1.5-pro (preview-0409)': new VertexAI({
temperature: 0.7,
modelName: 'gemini-1.5-pro-preview-0409',
}),
'gemini-1.0-pro (Latest)': new VertexAI({
temperature: 0.7,
modelName: 'gemini-1.0-pro',
}),
};
} catch (err) {
logger.error(`Error loading VertexAI models: ${err}`);
}
}
models['custom_openai'] = {}; models['custom_openai'] = {};
return models; return models;
@ -167,6 +187,16 @@ export const getAvailableEmbeddingModelProviders = async () => {
} }
} }
if (await hasGCPCredentials()) {
try {
models['vertexai'] = {
'Text Gecko default': new GoogleVertexAIEmbeddings(),
}
} catch (err) {
logger.error(`Error loading VertexAI embeddings: ${err}`);
}
}
try { try {
models['local'] = { models['local'] = {
'BGE Small': new HuggingFaceTransformersEmbeddings({ 'BGE Small': new HuggingFaceTransformersEmbeddings({

View file

@ -9,6 +9,8 @@ import type { Embeddings } from '@langchain/core/embeddings';
import type { IncomingMessage } from 'http'; import type { IncomingMessage } from 'http';
import logger from '../utils/logger'; import logger from '../utils/logger';
import { ChatOpenAI } from '@langchain/openai'; import { ChatOpenAI } from '@langchain/openai';
import { getAccessKey } from '../config';
import { checkAccessKey } from '../auth';
export const handleConnection = async ( export const handleConnection = async (
ws: WebSocket, ws: WebSocket,
@ -18,6 +20,20 @@ export const handleConnection = async (
const searchParams = new URL(request.url, `http://${request.headers.host}`) const searchParams = new URL(request.url, `http://${request.headers.host}`)
.searchParams; .searchParams;
if (getAccessKey()) {
const securtyProtocolHeader = request.headers['sec-websocket-protocol'];
if (!checkAccessKey(securtyProtocolHeader)) {
ws.send(
JSON.stringify({
type: 'error',
data: 'Incorrect or missing authentication token.',
key: 'FAILED_AUTHORIZATION',
}),
);
ws.close();
}
}
const [chatModelProviders, embeddingModelProviders] = await Promise.all([ const [chatModelProviders, embeddingModelProviders] = await Promise.all([
getAvailableChatModelProviders(), getAvailableChatModelProviders(),
getAvailableEmbeddingModelProviders(), getAvailableEmbeddingModelProviders(),

View file

@ -8,6 +8,8 @@ import EmptyChat from './EmptyChat';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { getSuggestions } from '@/lib/actions'; import { getSuggestions } from '@/lib/actions';
import { clientFetch } from '@/lib/utils';
import { getAccessKey } from '@/lib/config';
export type Message = { export type Message = {
id: string; id: string;
@ -37,14 +39,11 @@ const useSocket = (url: string, setIsReady: (ready: boolean) => void) => {
!embeddingModel || !embeddingModel ||
!embeddingModelProvider !embeddingModelProvider
) { ) {
const providers = await fetch( const providers = await clientFetch('/models', {
`${process.env.NEXT_PUBLIC_API_URL}/models`,
{
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}, }).then(async (res) => await res.json());
).then(async (res) => await res.json());
const chatModelProviders = providers.chatModelProviders; const chatModelProviders = providers.chatModelProviders;
const embeddingModelProviders = providers.embeddingModelProviders; const embeddingModelProviders = providers.embeddingModelProviders;
@ -100,7 +99,14 @@ const useSocket = (url: string, setIsReady: (ready: boolean) => void) => {
wsURL.search = searchParams.toString(); wsURL.search = searchParams.toString();
const ws = new WebSocket(wsURL.toString()); let protocols: any[] = [];
const secretToken = getAccessKey();
if (secretToken) {
protocols = ['Authorization', `${secretToken}`];
}
const ws = new WebSocket(wsURL.toString(), protocols);
ws.onopen = () => { ws.onopen = () => {
console.log('[DEBUG] open'); console.log('[DEBUG] open');

View file

@ -4,6 +4,7 @@ import { useState } from 'react';
import Lightbox from 'yet-another-react-lightbox'; import Lightbox from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css'; import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow'; import { Message } from './ChatWindow';
import { clientFetch } from '@/lib/utils';
type Image = { type Image = {
url: string; url: string;
@ -33,9 +34,7 @@ const SearchImages = ({
const chatModelProvider = localStorage.getItem('chatModelProvider'); const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel'); const chatModel = localStorage.getItem('chatModel');
const res = await fetch( const res = await clientFetch('/images', {
`${process.env.NEXT_PUBLIC_API_URL}/images`,
{
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -46,8 +45,7 @@ const SearchImages = ({
chat_model_provider: chatModelProvider, chat_model_provider: chatModelProvider,
chat_model: chatModel, chat_model: chatModel,
}), }),
}, });
);
const data = await res.json(); const data = await res.json();

View file

@ -4,6 +4,7 @@ import { useState } from 'react';
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox'; import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css'; import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow'; import { Message } from './ChatWindow';
import { clientFetch } from '@/lib/utils';
type Video = { type Video = {
url: string; url: string;
@ -46,9 +47,7 @@ const Searchvideos = ({
const chatModelProvider = localStorage.getItem('chatModelProvider'); const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel'); const chatModel = localStorage.getItem('chatModel');
const res = await fetch( const res = await clientFetch('/videos', {
`${process.env.NEXT_PUBLIC_API_URL}/videos`,
{
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -59,8 +58,7 @@ const Searchvideos = ({
chat_model_provider: chatModelProvider, chat_model_provider: chatModelProvider,
chat_model: chatModel, chat_model: chatModel,
}), }),
}, });
);
const data = await res.json(); const data = await res.json();

View file

@ -1,6 +1,7 @@
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from '@headlessui/react';
import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react'; import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react';
import React, { Fragment, useEffect, useState } from 'react'; import React, { Fragment, useEffect, useState } from 'react';
import { clientFetch } from '@/lib/utils';
interface SettingsType { interface SettingsType {
chatModelProviders: { chatModelProviders: {
@ -42,7 +43,7 @@ const SettingsDialog = ({
if (isOpen) { if (isOpen) {
const fetchConfig = async () => { const fetchConfig = async () => {
setIsLoading(true); setIsLoading(true);
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, { const res = await clientFetch('/config', {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -102,7 +103,7 @@ const SettingsDialog = ({
setIsUpdating(true); setIsUpdating(true);
try { try {
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, { await clientFetch('/config', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View file

@ -1,10 +1,11 @@
import { Message } from '@/components/ChatWindow'; import { Message } from '@/components/ChatWindow';
import { clientFetch } from '@/lib/utils';
export const getSuggestions = async (chatHisory: Message[]) => { export const getSuggestions = async (chatHisory: Message[]) => {
const chatModel = localStorage.getItem('chatModel'); const chatModel = localStorage.getItem('chatModel');
const chatModelProvider = localStorage.getItem('chatModelProvider'); const chatModelProvider = localStorage.getItem('chatModelProvider');
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/suggestions`, { const res = await clientFetch('/suggestions', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

22
ui/lib/config.ts Normal file
View file

@ -0,0 +1,22 @@
interface Config {
GENERAL: {
NEXT_PUBLIC_SUPER_SECRET_KEY: string;
NEXT_PUBLIC_API_URL: string;
NEXT_PUBLIC_WS_URL: string;
};
}
const loadEnv = () => {
return {
GENERAL: {
NEXT_PUBLIC_SUPER_SECRET_KEY: process.env.NEXT_PUBLIC_SUPER_SECRET_KEY!,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL!,
NEXT_PUBLIC_WS_URL: process.env.NEXT_PUBLIC_WS_URL!,
},
} as Config;
};
export const getAccessKey = () =>
loadEnv().GENERAL.NEXT_PUBLIC_SUPER_SECRET_KEY;
export const getBackendURL = () => loadEnv().GENERAL.NEXT_PUBLIC_API_URL;

View file

@ -1,5 +1,6 @@
import clsx, { ClassValue } from 'clsx'; import clsx, { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { getAccessKey, getBackendURL } from './config';
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes)); export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes));
@ -19,3 +20,20 @@ export const formatTimeDifference = (date1: Date, date2: Date): string => {
else else
return `${Math.floor(diffInSeconds / 31536000)} year${Math.floor(diffInSeconds / 31536000) !== 1 ? 's' : ''}`; return `${Math.floor(diffInSeconds / 31536000)} year${Math.floor(diffInSeconds / 31536000) !== 1 ? 's' : ''}`;
}; };
export const clientFetch = async (path: string, payload: any): Promise<any> => {
let headers = payload.headers;
const url = `${getBackendURL()}${path}`;
const secretToken = getAccessKey();
if (secretToken) {
if (headers == null) {
headers = {};
}
headers['Authorization'] = `Bearer ${secretToken}`;
payload.headers = headers;
}
return await fetch(url, payload);
};

192
yarn.lock
View file

@ -79,6 +79,24 @@
uuid "^9.0.0" uuid "^9.0.0"
zod "^3.22.3" zod "^3.22.3"
"@langchain/core@>0.1.56 <0.3.0", "@langchain/core@>0.1.56 <0.3.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.2.0.tgz#19c6374a5ad80daf8e14cb58582bc988109a1403"
integrity sha512-UbCJUp9eh2JXd9AW/vhPbTgtZoMgTqJgSan5Wf/EP27X8JM65lWdCOpJW+gHyBXvabbyrZz3/EGaptTUL5gutw==
dependencies:
ansi-styles "^5.0.0"
camelcase "6"
decamelize "1.2.0"
js-tiktoken "^1.0.12"
langsmith "~0.1.7"
ml-distance "^4.0.0"
mustache "^4.2.0"
p-queue "^6.6.2"
p-retry "4"
uuid "^9.0.0"
zod "^3.22.4"
zod-to-json-schema "^3.22.3"
"@langchain/core@~0.1.44", "@langchain/core@~0.1.45": "@langchain/core@~0.1.44", "@langchain/core@~0.1.45":
version "0.1.52" version "0.1.52"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.1.52.tgz#7619310b83ffa841628efe2e1eda873ca714d068" resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.1.52.tgz#7619310b83ffa841628efe2e1eda873ca714d068"
@ -96,6 +114,32 @@
zod "^3.22.4" zod "^3.22.4"
zod-to-json-schema "^3.22.3" zod-to-json-schema "^3.22.3"
"@langchain/google-common@~0.0.15":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@langchain/google-common/-/google-common-0.0.16.tgz#e2ff43eaebcf7bea84a067f8bdaf7f01e23bc1c0"
integrity sha512-eQMdqEYfzcavkE5Cpk7LCUlFx2Gb+skNZci/DlS2zot4XCSVg8QDIYOkL+PrXtTZBsp36SyOnNfzHUzdbU8cPA==
dependencies:
"@langchain/core" ">0.1.56 <0.3.0"
uuid "^9.0.0"
zod-to-json-schema "^3.22.4"
"@langchain/google-gauth@~0.0.16":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@langchain/google-gauth/-/google-gauth-0.0.16.tgz#164c865c0d6363385f3375e54e2ed66c6ed06cfd"
integrity sha512-mp68iw/XA/lbBwh8+6vV7FFsibP595mt+OZdEFU9QewpUv99YVHH1FT+mNoQAI9p6uZpSHYQ3Iip70nIU976sw==
dependencies:
"@langchain/core" ">0.1.56 <0.3.0"
"@langchain/google-common" "~0.0.15"
google-auth-library "^8.9.0"
"@langchain/google-vertexai@^0.0.16":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@langchain/google-vertexai/-/google-vertexai-0.0.16.tgz#388ddf21dc9537d4632acc5c046583fe9ac8022a"
integrity sha512-tJTyPxg3vYSqhNyqx6/UViPNdn3NPeZL29JqNen26x/w4JYYMpde0Dm20KCd5TCsbdUfrkk7tMyJZjr2e30jMg==
dependencies:
"@langchain/core" ">0.1.56 <0.3.0"
"@langchain/google-gauth" "~0.0.16"
"@langchain/openai@^0.0.25", "@langchain/openai@~0.0.19": "@langchain/openai@^0.0.25", "@langchain/openai@~0.0.19":
version "0.0.25" version "0.0.25"
resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-0.0.25.tgz#8332abea1e3acb9b1169f90636e518c0ee90622e" resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-0.0.25.tgz#8332abea1e3acb9b1169f90636e518c0ee90622e"
@ -357,6 +401,13 @@ acorn@^8.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
agentkeepalive@^4.2.1: agentkeepalive@^4.2.1:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923"
@ -392,6 +443,11 @@ array-flatten@1.1.1:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
arrify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
async@^3.2.3: async@^3.2.3:
version "3.2.5" version "3.2.5"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
@ -459,11 +515,16 @@ base-64@^0.1.0:
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
base64-js@^1.3.1, base64-js@^1.5.1: base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
bignumber.js@^9.0.0:
version "9.1.2"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c"
integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==
binary-extensions@^2.0.0, binary-extensions@^2.2.0: binary-extensions@^2.0.0, binary-extensions@^2.2.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
@ -516,6 +577,11 @@ braces@~3.0.2:
dependencies: dependencies:
fill-range "^7.0.1" fill-range "^7.0.1"
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@^5.5.0: buffer@^5.5.0:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
@ -716,7 +782,7 @@ debug@2.6.9:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
debug@^4: debug@4, debug@^4:
version "4.3.4" version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -787,6 +853,13 @@ dotenv@^16.4.5:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
ecdsa-sig-formatter@1.0.11, 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: ee-first@1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@ -888,11 +961,21 @@ express@^4.19.2:
utils-merge "1.0.1" utils-merge "1.0.1"
vary "~1.1.2" vary "~1.1.2"
extend@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
fast-fifo@^1.1.0, fast-fifo@^1.2.0: fast-fifo@^1.1.0, fast-fifo@^1.2.0:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==
fast-text-encoding@^1.0.0:
version "1.0.6"
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867"
integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==
fecha@^4.2.0: fecha@^4.2.0:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
@ -985,6 +1068,24 @@ function-bind@^1.1.2:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
gaxios@^5.0.0, gaxios@^5.0.1:
version "5.1.3"
resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-5.1.3.tgz#f7fa92da0fe197c846441e5ead2573d4979e9013"
integrity sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==
dependencies:
extend "^3.0.2"
https-proxy-agent "^5.0.0"
is-stream "^2.0.0"
node-fetch "^2.6.9"
gcp-metadata@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-5.3.0.tgz#6f45eb473d0cb47d15001476b48b663744d25408"
integrity sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==
dependencies:
gaxios "^5.0.0"
json-bigint "^1.0.0"
get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
@ -1008,6 +1109,28 @@ glob-parent@~5.1.2:
dependencies: dependencies:
is-glob "^4.0.1" is-glob "^4.0.1"
google-auth-library@^8.9.0:
version "8.9.0"
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-8.9.0.tgz#15a271eb2ec35d43b81deb72211bd61b1ef14dd0"
integrity sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==
dependencies:
arrify "^2.0.0"
base64-js "^1.3.0"
ecdsa-sig-formatter "^1.0.11"
fast-text-encoding "^1.0.0"
gaxios "^5.0.0"
gcp-metadata "^5.3.0"
gtoken "^6.1.0"
jws "^4.0.0"
lru-cache "^6.0.0"
google-p12-pem@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-4.0.1.tgz#82841798253c65b7dc2a4e5fe9df141db670172a"
integrity sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==
dependencies:
node-forge "^1.3.1"
gopd@^1.0.1: gopd@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@ -1015,6 +1138,15 @@ gopd@^1.0.1:
dependencies: dependencies:
get-intrinsic "^1.1.3" get-intrinsic "^1.1.3"
gtoken@^6.1.0:
version "6.1.2"
resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-6.1.2.tgz#aeb7bdb019ff4c3ba3ac100bbe7b6e74dce0e8bc"
integrity sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==
dependencies:
gaxios "^5.0.1"
google-p12-pem "^4.0.0"
jws "^4.0.0"
guid-typescript@^1.0.9: guid-typescript@^1.0.9:
version "1.0.9" version "1.0.9"
resolved "https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc" resolved "https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc"
@ -1060,6 +1192,14 @@ http-errors@2.0.0:
statuses "2.0.1" statuses "2.0.1"
toidentifier "1.0.1" toidentifier "1.0.1"
https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"
humanize-ms@^1.2.1: humanize-ms@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
@ -1143,6 +1283,13 @@ is-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
js-tiktoken@^1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.12.tgz#af0f5cf58e5e7318240d050c8413234019424211"
integrity sha512-L7wURW1fH9Qaext0VzaUDpFGVQgjkdE3Dgsy9/+yXyGEpBKnylTd0mU0bfbNkKDlXRb6TEsZkwuflu1B8uQbJQ==
dependencies:
base64-js "^1.5.1"
js-tiktoken@^1.0.7, js-tiktoken@^1.0.8: js-tiktoken@^1.0.7, js-tiktoken@^1.0.8:
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.10.tgz#2b343ec169399dcee8f9ef9807dbd4fafd3b30dc" resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.10.tgz#2b343ec169399dcee8f9ef9807dbd4fafd3b30dc"
@ -1157,11 +1304,35 @@ js-yaml@^4.1.0:
dependencies: dependencies:
argparse "^2.0.1" argparse "^2.0.1"
json-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"
integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==
dependencies:
bignumber.js "^9.0.0"
jsonpointer@^5.0.1: jsonpointer@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
jwa@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
dependencies:
jwa "^2.0.0"
safe-buffer "^5.0.1"
kuler@^2.0.0: kuler@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
@ -1349,6 +1520,11 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
mustache@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
napi-build-utils@^1.0.1: napi-build-utils@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
@ -1376,13 +1552,18 @@ node-domexception@1.0.0:
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch@^2.6.7: node-fetch@^2.6.7, node-fetch@^2.6.9:
version "2.7.0" version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
dependencies: dependencies:
whatwg-url "^5.0.0" whatwg-url "^5.0.0"
node-forge@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
nodemon@^3.1.0: nodemon@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.0.tgz#ff7394f2450eb6a5e96fe4180acd5176b29799c9" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.0.tgz#ff7394f2450eb6a5e96fe4180acd5176b29799c9"
@ -2079,6 +2260,11 @@ zod-to-json-schema@^3.22.3:
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.22.5.tgz#3646e81cfc318dbad2a22519e5ce661615418673" resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.22.5.tgz#3646e81cfc318dbad2a22519e5ce661615418673"
integrity sha512-+akaPo6a0zpVCCseDed504KBJUQpEW5QZw7RMneNmKw+fGaML1Z9tUNLnHHAC8x6dzVRO1eB2oEMyZRnuBZg7Q== integrity sha512-+akaPo6a0zpVCCseDed504KBJUQpEW5QZw7RMneNmKw+fGaML1Z9tUNLnHHAC8x6dzVRO1eB2oEMyZRnuBZg7Q==
zod-to-json-schema@^3.22.4:
version "3.23.0"
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.23.0.tgz#4fc60e88d3c709eedbfaae3f92f8a7bf786469f2"
integrity sha512-az0uJ243PxsRIa2x1WmNE/pnuA05gUq/JB8Lwe1EDCCL/Fz9MgjYQ0fPlyc2Tcv6aF2ZA7WM5TWaRZVEFaAIag==
zod@^3.22.3, zod@^3.22.4: zod@^3.22.3, zod@^3.22.4:
version "3.22.4" version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"