nsarrazin HF staff rtrm HF staff victor HF staff commited on
Commit
98b1c51
1 Parent(s): 1b24189

Move vars to dynamic, add metrics (#1085)

Browse files

* Add healthcheck route

* Add prom client with basic metrics

* wip: serve metrics on a different port

* exclude server from tsconfig

* latest

* refacto(all): use class & singleton

* refacto(all): use class & singleton

* refacto(all): add request logs

* Make all the env vars dynamic (#1092)

* Add truncate to task model (#1090)

* retry text area height (#1091)

* Make all the env vars dynamic

* only check for db at runtime

* remove secret from build step

* improve dockerfile

* Wrap db in singleton

* add .env.local to dockerignore

* changes to dockerfile

* lint

* aborted generations

* move collections build check

* Use a single dockerfile

* lint

* fixes

* lint

* don't return db during building

* remove dev

---------

Co-authored-by: Victor Muštar <victor.mustar@gmail.com>

* refacto(all): update default metrics port

* fix recursion error

* add version check

* revert vite preview

* Move request logs to debug level and add an env var to filter by log level in dev mode

* Set package version if needed

* Set log level for prod and dev

---------

Co-authored-by: rtrompier <remy.trompier@gmail.com>
Co-authored-by: Victor Muštar <victor.mustar@gmail.com>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +1 -1
  2. .env +2 -0
  3. .github/workflows/build-image.yml +3 -3
  4. Dockerfile +56 -10
  5. Dockerfile.local +0 -28
  6. entrypoint.sh +5 -15
  7. package-lock.json +642 -12
  8. package.json +3 -0
  9. scripts/populate.ts +9 -4
  10. src/hooks.server.ts +36 -26
  11. src/lib/assistantStats/refresh-assistants-counts.ts +40 -35
  12. src/lib/components/AnnouncementBanner.svelte +1 -1
  13. src/lib/components/DisclaimerModal.svelte +6 -10
  14. src/lib/components/LoginModal.svelte +5 -5
  15. src/lib/components/NavMenu.svelte +7 -4
  16. src/lib/components/chat/AssistantIntroduction.svelte +3 -2
  17. src/lib/components/chat/ChatIntroduction.svelte +6 -8
  18. src/lib/components/chat/ChatWindow.svelte +1 -1
  19. src/lib/components/icons/Logo.svelte +4 -4
  20. src/lib/migrations/migrations.ts +29 -22
  21. src/lib/migrations/routines/01-update-search-assistants.ts +5 -5
  22. src/lib/migrations/routines/02-update-assistants-models.ts +3 -3
  23. src/lib/migrations/routines/index.ts +4 -3
  24. src/lib/server/abortedGenerations.ts +25 -13
  25. src/lib/server/auth.ts +13 -24
  26. src/lib/server/database.ts +188 -141
  27. src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts +2 -2
  28. src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts +2 -2
  29. src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts +2 -2
  30. src/lib/server/embeddingModels.ts +2 -2
  31. src/lib/server/endpoints/anthropic/endpointAnthropic.ts +2 -2
  32. src/lib/server/endpoints/cloudflare/endpointCloudflare.ts +3 -3
  33. src/lib/server/endpoints/cohere/endpointCohere.ts +2 -2
  34. src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts +2 -2
  35. src/lib/server/endpoints/openai/endpointOai.ts +2 -2
  36. src/lib/server/endpoints/tgi/endpointTgi.ts +2 -2
  37. src/lib/server/files/downloadFile.ts +1 -1
  38. src/lib/server/files/uploadFile.ts +1 -1
  39. src/lib/server/logger.ts +2 -2
  40. src/lib/server/metrics.ts +43 -0
  41. src/lib/server/models.ts +9 -16
  42. src/lib/server/summarize.ts +2 -2
  43. src/lib/server/usageLimits.ts +3 -3
  44. src/lib/server/websearch/runWebSearch.ts +4 -4
  45. src/lib/server/websearch/searchSearxng.ts +2 -2
  46. src/lib/server/websearch/searchWeb.ts +13 -20
  47. src/lib/utils/getShareUrl.ts +4 -2
  48. src/lib/utils/isHuggingChat.ts +2 -2
  49. src/routes/+layout.server.ts +10 -20
  50. src/routes/+layout.svelte +20 -21
.dockerignore CHANGED
@@ -8,4 +8,4 @@ node_modules/
8
  .svelte-kit/
9
  .env*
10
  !.env
11
- !.env.local
 
8
  .svelte-kit/
9
  .env*
10
  !.env
11
+ .env.local
.env CHANGED
@@ -154,3 +154,5 @@ ALLOWED_USER_EMAILS=`[]` # if it's defined, only these emails will be allowed to
154
 
155
  USAGE_LIMITS=`{}`
156
  ALLOW_INSECURE_COOKIES=false # recommended to keep this to false but set to true if you need to run over http without tls
 
 
 
154
 
155
  USAGE_LIMITS=`{}`
156
  ALLOW_INSECURE_COOKIES=false # recommended to keep this to false but set to true if you need to run over http without tls
157
+ METRICS_PORT=
158
+ LOG_LEVEL=info
.github/workflows/build-image.yml CHANGED
@@ -8,7 +8,7 @@ on:
8
  branches:
9
  - "*"
10
  paths:
11
- - "Dockerfile.local"
12
  - "entrypoint.sh"
13
  workflow_dispatch:
14
  release:
@@ -62,7 +62,7 @@ jobs:
62
  uses: docker/build-push-action@v5
63
  with:
64
  context: .
65
- file: Dockerfile.local
66
  push: ${{ github.event_name != 'pull_request' }}
67
  tags: ${{ steps.meta.outputs.tags }}
68
  labels: ${{ steps.meta.outputs.labels }}
@@ -116,7 +116,7 @@ jobs:
116
  uses: docker/build-push-action@v5
117
  with:
118
  context: .
119
- file: Dockerfile.local
120
  push: ${{ github.event_name != 'pull_request' }}
121
  tags: ${{ steps.meta.outputs.tags }}
122
  labels: ${{ steps.meta.outputs.labels }}
 
8
  branches:
9
  - "*"
10
  paths:
11
+ - "Dockerfile"
12
  - "entrypoint.sh"
13
  workflow_dispatch:
14
  release:
 
62
  uses: docker/build-push-action@v5
63
  with:
64
  context: .
65
+ file: Dockerfile
66
  push: ${{ github.event_name != 'pull_request' }}
67
  tags: ${{ steps.meta.outputs.tags }}
68
  labels: ${{ steps.meta.outputs.labels }}
 
116
  uses: docker/build-push-action@v5
117
  with:
118
  context: .
119
+ file: Dockerfile
120
  push: ${{ github.event_name != 'pull_request' }}
121
  tags: ${{ steps.meta.outputs.tags }}
122
  labels: ${{ steps.meta.outputs.labels }}
Dockerfile CHANGED
@@ -1,6 +1,9 @@
1
  # syntax=docker/dockerfile:1
2
  # read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
3
  # you will also find guides on how best to write your Dockerfile
 
 
 
4
  FROM node:20 as builder-production
5
 
6
  WORKDIR /app
@@ -12,31 +15,74 @@ RUN --mount=type=cache,target=/app/.npm \
12
 
13
  FROM builder-production as builder
14
 
 
 
 
15
  RUN --mount=type=cache,target=/app/.npm \
16
  npm set cache /app/.npm && \
17
  npm ci
18
 
19
  COPY --link --chown=1000 . .
20
 
21
- RUN --mount=type=secret,id=DOTENV_LOCAL,dst=.env.local \
22
- npm run build
23
 
24
- FROM node:20-slim
25
- RUN npm install -g pm2
26
 
27
- RUN userdel -r node
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- RUN useradd -m -u 1000 user
30
 
 
 
 
 
 
 
31
  USER user
32
 
33
  ENV HOME=/home/user \
34
  PATH=/home/user/.local/bin:$PATH
35
 
 
36
 
37
- COPY --from=builder-production --chown=1000 /app/node_modules /app/node_modules
38
- COPY --link --chown=1000 package.json /app/package.json
39
- COPY --from=builder --chown=1000 /app/build /app/build
 
 
 
 
40
  COPY --chown=1000 gcp-*.json /app/
41
 
42
- CMD pm2 start /app/build/index.js -i $CPU_CORES --no-daemon
 
 
 
 
 
 
 
1
  # syntax=docker/dockerfile:1
2
  # read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
3
  # you will also find guides on how best to write your Dockerfile
4
+ ARG INCLUDE_DB=false
5
+
6
+ # stage that install the dependencies
7
  FROM node:20 as builder-production
8
 
9
  WORKDIR /app
 
15
 
16
  FROM builder-production as builder
17
 
18
+ ARG APP_BASE=
19
+ ARG PUBLIC_APP_COLOR=blue
20
+
21
  RUN --mount=type=cache,target=/app/.npm \
22
  npm set cache /app/.npm && \
23
  npm ci
24
 
25
  COPY --link --chown=1000 . .
26
 
27
+ RUN npm run build
 
28
 
29
+ # mongo image
30
+ FROM mongo:latest as mongo
31
 
32
+ # image to be used if INCLUDE_DB is false
33
+ FROM node:20-slim as local_db_false
34
+
35
+ # image to be used if INCLUDE_DB is true
36
+ FROM node:20-slim as local_db_true
37
+
38
+ RUN apt-get update
39
+ RUN apt-get install gnupg curl -y
40
+ # copy mongo from the other stage
41
+ COPY --from=mongo /usr/bin/mongo* /usr/bin/
42
+
43
+ ENV MONGODB_URL=mongodb://localhost:27017
44
+ RUN mkdir -p /data/db
45
+ RUN chown -R 1000:1000 /data/db
46
+
47
+ # final image
48
+ FROM local_db_${INCLUDE_DB} as final
49
+
50
+ # build arg to determine if the database should be included
51
+ ARG INCLUDE_DB=false
52
+ ENV INCLUDE_DB=${INCLUDE_DB}
53
+
54
+ # svelte requires APP_BASE at build time so it must be passed as a build arg
55
+ ARG APP_BASE=
56
+ # tailwind requires the primary theme to be known at build time so it must be passed as a build arg
57
+ ARG PUBLIC_APP_COLOR=blue
58
 
 
59
 
60
+ # install dotenv-cli
61
+ RUN npm install -g dotenv-cli
62
+
63
+ # switch to a user that works for spaces
64
+ RUN userdel -r node
65
+ RUN useradd -m -u 1000 user
66
  USER user
67
 
68
  ENV HOME=/home/user \
69
  PATH=/home/user/.local/bin:$PATH
70
 
71
+ WORKDIR /app
72
 
73
+ # add a .env.local if the user doesn't bind a volume to it
74
+ RUN touch /app/.env.local
75
+
76
+ # get the default config, the entrypoint script and the server script
77
+ COPY --chown=1000 package.json /app/package.json
78
+ COPY --chown=1000 .env /app/.env
79
+ COPY --chown=1000 entrypoint.sh /app/entrypoint.sh
80
  COPY --chown=1000 gcp-*.json /app/
81
 
82
+ #import the build & dependencies
83
+ COPY --from=builder --chown=1000 /app/build /app/build
84
+ COPY --from=builder --chown=1000 /app/node_modules /app/node_modules
85
+
86
+ RUN chmod +x /app/entrypoint.sh
87
+
88
+ CMD ["/bin/bash", "-c", "/app/entrypoint.sh"]
Dockerfile.local DELETED
@@ -1,28 +0,0 @@
1
- ARG INCLUDE_DB=false
2
- FROM mongo:latest as mongo
3
-
4
- FROM node:20-slim as local_db_false
5
-
6
- FROM node:20-slim as local_db_true
7
-
8
- RUN apt-get update
9
- RUN apt-get install gnupg curl -y
10
-
11
- COPY --from=mongo /usr/bin/mongo* /usr/bin/
12
-
13
- FROM local_db_${INCLUDE_DB} as final
14
- ARG INCLUDE_DB=false
15
- ENV INCLUDE_DB=${INCLUDE_DB}
16
-
17
- WORKDIR /app
18
-
19
- COPY --link --chown=1000 package-lock.json package.json ./
20
- RUN --mount=type=cache,target=/app/.npm \
21
- npm set cache /app/.npm && \
22
- npm ci
23
-
24
- # copy the rest of the files, run regardless of
25
- COPY --chown=1000 --link . .
26
- RUN chmod +x /app/entrypoint.sh
27
-
28
- CMD ["/bin/bash", "-c", "/app/entrypoint.sh"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
entrypoint.sh CHANGED
@@ -2,7 +2,7 @@ ENV_LOCAL_PATH=/app/.env.local
2
 
3
  if test -z "${DOTENV_LOCAL}" ; then
4
  if ! test -f "${ENV_LOCAL_PATH}" ; then
5
- echo "DOTENV_LOCAL was not found in the ENV variables and .env.local is not set using a bind volume. We are using the default .env config."
6
  fi;
7
  else
8
  echo "DOTENV_LOCAL was found in the ENV variables. Creating .env.local file."
@@ -10,20 +10,10 @@ else
10
  fi;
11
 
12
  if [ "$INCLUDE_DB" = "true" ] ; then
13
- echo "INCLUDE_DB is set to true."
14
-
15
- MONGODB_CONFIG="MONGODB_URL=mongodb://localhost:27017"
16
- if ! grep -q "^${MONGODB_CONFIG}$" ${ENV_LOCAL_PATH}; then
17
- echo "Appending MONGODB_URL"
18
- touch /app/.env.local
19
- echo -e "\n${MONGODB_CONFIG}" >> ${ENV_LOCAL_PATH}
20
- fi
21
-
22
- mkdir -p /data/db
23
- mongod &
24
  echo "Starting local MongoDB instance"
25
-
26
  fi;
27
 
28
- npm run build
29
- npm run preview -- --host 0.0.0.0 --port 3000
 
 
2
 
3
  if test -z "${DOTENV_LOCAL}" ; then
4
  if ! test -f "${ENV_LOCAL_PATH}" ; then
5
+ echo "DOTENV_LOCAL was not found in the ENV variables and .env.local is not set using a bind volume. Make sure to set environment variables properly. "
6
  fi;
7
  else
8
  echo "DOTENV_LOCAL was found in the ENV variables. Creating .env.local file."
 
10
  fi;
11
 
12
  if [ "$INCLUDE_DB" = "true" ] ; then
 
 
 
 
 
 
 
 
 
 
 
13
  echo "Starting local MongoDB instance"
14
+ nohup mongod &
15
  fi;
16
 
17
+ export PUBLIC_VERSION=$(node -p "require('./package.json').version")
18
+
19
+ dotenv -e /app/.env -c -- node /app/build/index.js -- --host 0.0.0.0 --port 3000
package-lock.json CHANGED
@@ -17,6 +17,7 @@
17
  "browser-image-resizer": "^2.4.1",
18
  "date-fns": "^2.29.3",
19
  "dotenv": "^16.0.3",
 
20
  "handlebars": "^4.7.8",
21
  "highlight.js": "^11.7.0",
22
  "image-size": "^1.0.2",
@@ -49,6 +50,7 @@
49
  "@sveltejs/adapter-node": "^1.3.1",
50
  "@sveltejs/kit": "^1.30.4",
51
  "@tailwindcss/typography": "^0.5.9",
 
52
  "@types/jsdom": "^21.1.1",
53
  "@types/minimist": "^1.2.5",
54
  "@types/parquetjs": "^0.10.3",
@@ -62,6 +64,7 @@
62
  "prettier": "^2.8.0",
63
  "prettier-plugin-svelte": "^2.10.1",
64
  "prettier-plugin-tailwindcss": "^0.2.7",
 
65
  "svelte": "^4.2.8",
66
  "svelte-check": "^3.6.2",
67
  "ts-node": "^10.9.1",
@@ -1263,6 +1266,15 @@
1263
  "node": ">= 8"
1264
  }
1265
  },
 
 
 
 
 
 
 
 
 
1266
  "node_modules/@polka/url": {
1267
  "version": "1.0.0-next.21",
1268
  "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
@@ -1987,6 +1999,16 @@
1987
  "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
1988
  "devOptional": true
1989
  },
 
 
 
 
 
 
 
 
 
 
1990
  "node_modules/@types/chai": {
1991
  "version": "4.3.5",
1992
  "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz",
@@ -2002,6 +2024,15 @@
2002
  "@types/chai": "*"
2003
  }
2004
  },
 
 
 
 
 
 
 
 
 
2005
  "node_modules/@types/cookie": {
2006
  "version": "0.5.1",
2007
  "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
@@ -2014,6 +2045,36 @@
2014
  "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
2015
  "dev": true
2016
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2017
  "node_modules/@types/jsdom": {
2018
  "version": "21.1.1",
2019
  "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.1.tgz",
@@ -2041,6 +2102,12 @@
2041
  "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
2042
  "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
2043
  },
 
 
 
 
 
 
2044
  "node_modules/@types/minimist": {
2045
  "version": "1.2.5",
2046
  "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
@@ -2086,6 +2153,18 @@
2086
  "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
2087
  "dev": true
2088
  },
 
 
 
 
 
 
 
 
 
 
 
 
2089
  "node_modules/@types/resolve": {
2090
  "version": "1.20.2",
2091
  "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -2098,6 +2177,27 @@
2098
  "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==",
2099
  "dev": true
2100
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2101
  "node_modules/@types/tough-cookie": {
2102
  "version": "4.0.2",
2103
  "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
@@ -2460,6 +2560,18 @@
2460
  "node": ">=6.5"
2461
  }
2462
  },
 
 
 
 
 
 
 
 
 
 
 
 
2463
  "node_modules/acorn": {
2464
  "version": "8.11.3",
2465
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
@@ -2590,6 +2702,11 @@
2590
  "dequal": "^2.0.3"
2591
  }
2592
  },
 
 
 
 
 
2593
  "node_modules/array-union": {
2594
  "version": "2.1.0",
2595
  "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -2726,6 +2843,12 @@
2726
  "integrity": "sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g==",
2727
  "optional": true
2728
  },
 
 
 
 
 
 
2729
  "node_modules/bl": {
2730
  "version": "4.1.0",
2731
  "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -2742,6 +2865,67 @@
2742
  "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==",
2743
  "dev": true
2744
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2745
  "node_modules/brace-expansion": {
2746
  "version": "1.1.11",
2747
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -2874,6 +3058,14 @@
2874
  "url": "https://github.com/sponsors/sindresorhus"
2875
  }
2876
  },
 
 
 
 
 
 
 
 
2877
  "node_modules/cac": {
2878
  "version": "6.7.14",
2879
  "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -2887,7 +3079,6 @@
2887
  "version": "1.0.7",
2888
  "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
2889
  "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
2890
- "optional": true,
2891
  "dependencies": {
2892
  "es-define-property": "^1.0.0",
2893
  "es-errors": "^1.3.0",
@@ -3166,6 +3357,25 @@
3166
  "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14"
3167
  }
3168
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3169
  "node_modules/cookie": {
3170
  "version": "0.5.0",
3171
  "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
@@ -3175,6 +3385,11 @@
3175
  "node": ">= 0.6"
3176
  }
3177
  },
 
 
 
 
 
3178
  "node_modules/create-require": {
3179
  "version": "1.1.1",
3180
  "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -3409,7 +3624,6 @@
3409
  "version": "1.1.4",
3410
  "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
3411
  "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
3412
- "optional": true,
3413
  "dependencies": {
3414
  "es-define-property": "^1.0.0",
3415
  "es-errors": "^1.3.0",
@@ -3430,6 +3644,14 @@
3430
  "node": ">=0.4.0"
3431
  }
3432
  },
 
 
 
 
 
 
 
 
3433
  "node_modules/dequal": {
3434
  "version": "2.0.3",
3435
  "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -3439,6 +3661,15 @@
3439
  "node": ">=6"
3440
  }
3441
  },
 
 
 
 
 
 
 
 
 
3442
  "node_modules/detect-indent": {
3443
  "version": "6.1.0",
3444
  "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
@@ -3543,6 +3774,11 @@
3543
  "safe-buffer": "^5.0.1"
3544
  }
3545
  },
 
 
 
 
 
3546
  "node_modules/electron-to-chromium": {
3547
  "version": "1.4.359",
3548
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz",
@@ -3553,6 +3789,14 @@
3553
  "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
3554
  "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
3555
  },
 
 
 
 
 
 
 
 
3556
  "node_modules/end-of-stream": {
3557
  "version": "1.4.4",
3558
  "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -3576,7 +3820,6 @@
3576
  "version": "1.0.0",
3577
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
3578
  "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
3579
- "optional": true,
3580
  "dependencies": {
3581
  "get-intrinsic": "^1.2.4"
3582
  },
@@ -3588,7 +3831,6 @@
3588
  "version": "1.3.0",
3589
  "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
3590
  "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
3591
- "optional": true,
3592
  "engines": {
3593
  "node": ">= 0.4"
3594
  }
@@ -3878,6 +4120,14 @@
3878
  "node": ">=0.10.0"
3879
  }
3880
  },
 
 
 
 
 
 
 
 
3881
  "node_modules/event-target-shim": {
3882
  "version": "5.0.1",
3883
  "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
@@ -3925,6 +4175,82 @@
3925
  "node": ">=6"
3926
  }
3927
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3928
  "node_modules/extend": {
3929
  "version": "3.0.2",
3930
  "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -4040,6 +4366,36 @@
4040
  "node": ">=8"
4041
  }
4042
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4043
  "node_modules/find-up": {
4044
  "version": "5.0.0",
4045
  "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -4112,6 +4468,14 @@
4112
  "node": ">= 12.20"
4113
  }
4114
  },
 
 
 
 
 
 
 
 
4115
  "node_modules/fraction.js": {
4116
  "version": "4.2.0",
4117
  "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
@@ -4124,6 +4488,14 @@
4124
  "url": "https://www.patreon.com/infusion"
4125
  }
4126
  },
 
 
 
 
 
 
 
 
4127
  "node_modules/fs-constants": {
4128
  "version": "1.0.0",
4129
  "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -4221,7 +4593,6 @@
4221
  "version": "1.2.4",
4222
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
4223
  "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
4224
- "optional": true,
4225
  "dependencies": {
4226
  "es-errors": "^1.3.0",
4227
  "function-bind": "^1.1.2",
@@ -4352,7 +4723,6 @@
4352
  "version": "1.0.1",
4353
  "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
4354
  "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
4355
- "optional": true,
4356
  "dependencies": {
4357
  "get-intrinsic": "^1.1.3"
4358
  },
@@ -4429,7 +4799,6 @@
4429
  "version": "1.0.2",
4430
  "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
4431
  "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
4432
- "optional": true,
4433
  "dependencies": {
4434
  "es-define-property": "^1.0.0"
4435
  },
@@ -4441,7 +4810,6 @@
4441
  "version": "1.0.3",
4442
  "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
4443
  "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
4444
- "optional": true,
4445
  "engines": {
4446
  "node": ">= 0.4"
4447
  },
@@ -4453,7 +4821,6 @@
4453
  "version": "1.0.3",
4454
  "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
4455
  "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
4456
- "optional": true,
4457
  "engines": {
4458
  "node": ">= 0.4"
4459
  },
@@ -4512,6 +4879,21 @@
4512
  "node": ">=12"
4513
  }
4514
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4515
  "node_modules/http-proxy-agent": {
4516
  "version": "5.0.0",
4517
  "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
@@ -4669,6 +5051,14 @@
4669
  "node": ">= 12"
4670
  }
4671
  },
 
 
 
 
 
 
 
 
4672
  "node_modules/is-arrayish": {
4673
  "version": "0.3.2",
4674
  "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@@ -5231,11 +5621,24 @@
5231
  "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
5232
  "dev": true
5233
  },
 
 
 
 
 
 
 
 
5234
  "node_modules/memory-pager": {
5235
  "version": "1.5.0",
5236
  "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
5237
  "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
5238
  },
 
 
 
 
 
5239
  "node_modules/merge-stream": {
5240
  "version": "2.0.0",
5241
  "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -5250,6 +5653,14 @@
5250
  "node": ">= 8"
5251
  }
5252
  },
 
 
 
 
 
 
 
 
5253
  "node_modules/micromatch": {
5254
  "version": "4.0.5",
5255
  "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
@@ -5262,6 +5673,17 @@
5262
  "node": ">=8.6"
5263
  }
5264
  },
 
 
 
 
 
 
 
 
 
 
 
5265
  "node_modules/mime-db": {
5266
  "version": "1.52.0",
5267
  "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -5468,6 +5890,14 @@
5468
  "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
5469
  "dev": true
5470
  },
 
 
 
 
 
 
 
 
5471
  "node_modules/neo-async": {
5472
  "version": "2.6.2",
5473
  "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
@@ -5625,7 +6055,6 @@
5625
  "version": "1.13.1",
5626
  "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
5627
  "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
5628
- "optional": true,
5629
  "funding": {
5630
  "url": "https://github.com/sponsors/ljharb"
5631
  }
@@ -5654,6 +6083,17 @@
5654
  "node": ">=14.0.0"
5655
  }
5656
  },
 
 
 
 
 
 
 
 
 
 
 
5657
  "node_modules/once": {
5658
  "version": "1.4.0",
5659
  "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -5880,6 +6320,14 @@
5880
  "url": "https://github.com/inikulin/parse5?sponsor=1"
5881
  }
5882
  },
 
 
 
 
 
 
 
 
5883
  "node_modules/path-exists": {
5884
  "version": "4.0.0",
5885
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5911,6 +6359,11 @@
5911
  "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
5912
  "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
5913
  },
 
 
 
 
 
5914
  "node_modules/path-type": {
5915
  "version": "4.0.0",
5916
  "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -6529,6 +6982,19 @@
6529
  "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz",
6530
  "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="
6531
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
6532
  "node_modules/protobufjs": {
6533
  "version": "6.11.4",
6534
  "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
@@ -6554,6 +7020,18 @@
6554
  "pbts": "bin/pbts"
6555
  }
6556
  },
 
 
 
 
 
 
 
 
 
 
 
 
6557
  "node_modules/psl": {
6558
  "version": "1.9.0",
6559
  "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -6642,6 +7120,39 @@
6642
  "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
6643
  "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
6644
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6645
  "node_modules/rc": {
6646
  "version": "1.2.8",
6647
  "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -6944,6 +7455,47 @@
6944
  "node": ">=10"
6945
  }
6946
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6947
  "node_modules/serpapi": {
6948
  "version": "1.1.1",
6949
  "resolved": "https://registry.npmjs.org/serpapi/-/serpapi-1.1.1.tgz",
@@ -6952,6 +7504,20 @@
6952
  "undici": "^5.12.0"
6953
  }
6954
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6955
  "node_modules/set-cookie-parser": {
6956
  "version": "2.6.0",
6957
  "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
@@ -6962,7 +7528,6 @@
6962
  "version": "1.2.2",
6963
  "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
6964
  "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
6965
- "optional": true,
6966
  "dependencies": {
6967
  "define-data-property": "^1.1.4",
6968
  "es-errors": "^1.3.0",
@@ -6975,6 +7540,11 @@
6975
  "node": ">= 0.4"
6976
  }
6977
  },
 
 
 
 
 
6978
  "node_modules/sharp": {
6979
  "version": "0.33.2",
6980
  "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz",
@@ -7039,7 +7609,6 @@
7039
  "version": "1.0.6",
7040
  "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
7041
  "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
7042
- "optional": true,
7043
  "dependencies": {
7044
  "call-bind": "^1.0.7",
7045
  "es-errors": "^1.3.0",
@@ -7232,6 +7801,14 @@
7232
  "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
7233
  "dev": true
7234
  },
 
 
 
 
 
 
 
 
7235
  "node_modules/std-env": {
7236
  "version": "3.3.3",
7237
  "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz",
@@ -7703,6 +8280,15 @@
7703
  "streamx": "^2.15.0"
7704
  }
7705
  },
 
 
 
 
 
 
 
 
 
7706
  "node_modules/text-table": {
7707
  "version": "0.2.0",
7708
  "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -7808,6 +8394,14 @@
7808
  "node": ">=8.0"
7809
  }
7810
  },
 
 
 
 
 
 
 
 
7811
  "node_modules/totalist": {
7812
  "version": "3.0.0",
7813
  "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz",
@@ -7958,6 +8552,18 @@
7958
  "url": "https://github.com/sponsors/sindresorhus"
7959
  }
7960
  },
 
 
 
 
 
 
 
 
 
 
 
 
7961
  "node_modules/typescript": {
7962
  "version": "5.2.2",
7963
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
@@ -8022,6 +8628,14 @@
8022
  "node": ">= 4.0.0"
8023
  }
8024
  },
 
 
 
 
 
 
 
 
8025
  "node_modules/unplugin": {
8026
  "version": "1.3.1",
8027
  "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
@@ -8126,6 +8740,14 @@
8126
  "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
8127
  "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
8128
  },
 
 
 
 
 
 
 
 
8129
  "node_modules/uuid": {
8130
  "version": "9.0.1",
8131
  "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
@@ -8149,6 +8771,14 @@
8149
  "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz",
8150
  "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="
8151
  },
 
 
 
 
 
 
 
 
8152
  "node_modules/vite": {
8153
  "version": "4.5.3",
8154
  "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
 
17
  "browser-image-resizer": "^2.4.1",
18
  "date-fns": "^2.29.3",
19
  "dotenv": "^16.0.3",
20
+ "express": "^4.19.2",
21
  "handlebars": "^4.7.8",
22
  "highlight.js": "^11.7.0",
23
  "image-size": "^1.0.2",
 
50
  "@sveltejs/adapter-node": "^1.3.1",
51
  "@sveltejs/kit": "^1.30.4",
52
  "@tailwindcss/typography": "^0.5.9",
53
+ "@types/express": "^4.17.21",
54
  "@types/jsdom": "^21.1.1",
55
  "@types/minimist": "^1.2.5",
56
  "@types/parquetjs": "^0.10.3",
 
64
  "prettier": "^2.8.0",
65
  "prettier-plugin-svelte": "^2.10.1",
66
  "prettier-plugin-tailwindcss": "^0.2.7",
67
+ "prom-client": "^15.1.2",
68
  "svelte": "^4.2.8",
69
  "svelte-check": "^3.6.2",
70
  "ts-node": "^10.9.1",
 
1266
  "node": ">= 8"
1267
  }
1268
  },
1269
+ "node_modules/@opentelemetry/api": {
1270
+ "version": "1.8.0",
1271
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz",
1272
+ "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==",
1273
+ "dev": true,
1274
+ "engines": {
1275
+ "node": ">=8.0.0"
1276
+ }
1277
+ },
1278
  "node_modules/@polka/url": {
1279
  "version": "1.0.0-next.21",
1280
  "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
 
1999
  "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
2000
  "devOptional": true
2001
  },
2002
+ "node_modules/@types/body-parser": {
2003
+ "version": "1.19.5",
2004
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
2005
+ "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
2006
+ "dev": true,
2007
+ "dependencies": {
2008
+ "@types/connect": "*",
2009
+ "@types/node": "*"
2010
+ }
2011
+ },
2012
  "node_modules/@types/chai": {
2013
  "version": "4.3.5",
2014
  "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz",
 
2024
  "@types/chai": "*"
2025
  }
2026
  },
2027
+ "node_modules/@types/connect": {
2028
+ "version": "3.4.38",
2029
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
2030
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
2031
+ "dev": true,
2032
+ "dependencies": {
2033
+ "@types/node": "*"
2034
+ }
2035
+ },
2036
  "node_modules/@types/cookie": {
2037
  "version": "0.5.1",
2038
  "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
 
2045
  "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
2046
  "dev": true
2047
  },
2048
+ "node_modules/@types/express": {
2049
+ "version": "4.17.21",
2050
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
2051
+ "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
2052
+ "dev": true,
2053
+ "dependencies": {
2054
+ "@types/body-parser": "*",
2055
+ "@types/express-serve-static-core": "^4.17.33",
2056
+ "@types/qs": "*",
2057
+ "@types/serve-static": "*"
2058
+ }
2059
+ },
2060
+ "node_modules/@types/express-serve-static-core": {
2061
+ "version": "4.19.0",
2062
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz",
2063
+ "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==",
2064
+ "dev": true,
2065
+ "dependencies": {
2066
+ "@types/node": "*",
2067
+ "@types/qs": "*",
2068
+ "@types/range-parser": "*",
2069
+ "@types/send": "*"
2070
+ }
2071
+ },
2072
+ "node_modules/@types/http-errors": {
2073
+ "version": "2.0.4",
2074
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
2075
+ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
2076
+ "dev": true
2077
+ },
2078
  "node_modules/@types/jsdom": {
2079
  "version": "21.1.1",
2080
  "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.1.tgz",
 
2102
  "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
2103
  "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
2104
  },
2105
+ "node_modules/@types/mime": {
2106
+ "version": "1.3.5",
2107
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
2108
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
2109
+ "dev": true
2110
+ },
2111
  "node_modules/@types/minimist": {
2112
  "version": "1.2.5",
2113
  "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
 
2153
  "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
2154
  "dev": true
2155
  },
2156
+ "node_modules/@types/qs": {
2157
+ "version": "6.9.15",
2158
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
2159
+ "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==",
2160
+ "dev": true
2161
+ },
2162
+ "node_modules/@types/range-parser": {
2163
+ "version": "1.2.7",
2164
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
2165
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
2166
+ "dev": true
2167
+ },
2168
  "node_modules/@types/resolve": {
2169
  "version": "1.20.2",
2170
  "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
 
2177
  "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==",
2178
  "dev": true
2179
  },
2180
+ "node_modules/@types/send": {
2181
+ "version": "0.17.4",
2182
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
2183
+ "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
2184
+ "dev": true,
2185
+ "dependencies": {
2186
+ "@types/mime": "^1",
2187
+ "@types/node": "*"
2188
+ }
2189
+ },
2190
+ "node_modules/@types/serve-static": {
2191
+ "version": "1.15.7",
2192
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
2193
+ "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
2194
+ "dev": true,
2195
+ "dependencies": {
2196
+ "@types/http-errors": "*",
2197
+ "@types/node": "*",
2198
+ "@types/send": "*"
2199
+ }
2200
+ },
2201
  "node_modules/@types/tough-cookie": {
2202
  "version": "4.0.2",
2203
  "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
 
2560
  "node": ">=6.5"
2561
  }
2562
  },
2563
+ "node_modules/accepts": {
2564
+ "version": "1.3.8",
2565
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
2566
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
2567
+ "dependencies": {
2568
+ "mime-types": "~2.1.34",
2569
+ "negotiator": "0.6.3"
2570
+ },
2571
+ "engines": {
2572
+ "node": ">= 0.6"
2573
+ }
2574
+ },
2575
  "node_modules/acorn": {
2576
  "version": "8.11.3",
2577
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
 
2702
  "dequal": "^2.0.3"
2703
  }
2704
  },
2705
+ "node_modules/array-flatten": {
2706
+ "version": "1.1.1",
2707
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
2708
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
2709
+ },
2710
  "node_modules/array-union": {
2711
  "version": "2.1.0",
2712
  "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
 
2843
  "integrity": "sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g==",
2844
  "optional": true
2845
  },
2846
+ "node_modules/bintrees": {
2847
+ "version": "1.0.2",
2848
+ "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
2849
+ "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
2850
+ "dev": true
2851
+ },
2852
  "node_modules/bl": {
2853
  "version": "4.1.0",
2854
  "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
 
2865
  "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==",
2866
  "dev": true
2867
  },
2868
+ "node_modules/body-parser": {
2869
+ "version": "1.20.2",
2870
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
2871
+ "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
2872
+ "dependencies": {
2873
+ "bytes": "3.1.2",
2874
+ "content-type": "~1.0.5",
2875
+ "debug": "2.6.9",
2876
+ "depd": "2.0.0",
2877
+ "destroy": "1.2.0",
2878
+ "http-errors": "2.0.0",
2879
+ "iconv-lite": "0.4.24",
2880
+ "on-finished": "2.4.1",
2881
+ "qs": "6.11.0",
2882
+ "raw-body": "2.5.2",
2883
+ "type-is": "~1.6.18",
2884
+ "unpipe": "1.0.0"
2885
+ },
2886
+ "engines": {
2887
+ "node": ">= 0.8",
2888
+ "npm": "1.2.8000 || >= 1.4.16"
2889
+ }
2890
+ },
2891
+ "node_modules/body-parser/node_modules/debug": {
2892
+ "version": "2.6.9",
2893
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
2894
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
2895
+ "dependencies": {
2896
+ "ms": "2.0.0"
2897
+ }
2898
+ },
2899
+ "node_modules/body-parser/node_modules/iconv-lite": {
2900
+ "version": "0.4.24",
2901
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
2902
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
2903
+ "dependencies": {
2904
+ "safer-buffer": ">= 2.1.2 < 3"
2905
+ },
2906
+ "engines": {
2907
+ "node": ">=0.10.0"
2908
+ }
2909
+ },
2910
+ "node_modules/body-parser/node_modules/ms": {
2911
+ "version": "2.0.0",
2912
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
2913
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
2914
+ },
2915
+ "node_modules/body-parser/node_modules/qs": {
2916
+ "version": "6.11.0",
2917
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
2918
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
2919
+ "dependencies": {
2920
+ "side-channel": "^1.0.4"
2921
+ },
2922
+ "engines": {
2923
+ "node": ">=0.6"
2924
+ },
2925
+ "funding": {
2926
+ "url": "https://github.com/sponsors/ljharb"
2927
+ }
2928
+ },
2929
  "node_modules/brace-expansion": {
2930
  "version": "1.1.11",
2931
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
 
3058
  "url": "https://github.com/sponsors/sindresorhus"
3059
  }
3060
  },
3061
+ "node_modules/bytes": {
3062
+ "version": "3.1.2",
3063
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
3064
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
3065
+ "engines": {
3066
+ "node": ">= 0.8"
3067
+ }
3068
+ },
3069
  "node_modules/cac": {
3070
  "version": "6.7.14",
3071
  "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
 
3079
  "version": "1.0.7",
3080
  "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
3081
  "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
 
3082
  "dependencies": {
3083
  "es-define-property": "^1.0.0",
3084
  "es-errors": "^1.3.0",
 
3357
  "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14"
3358
  }
3359
  },
3360
+ "node_modules/content-disposition": {
3361
+ "version": "0.5.4",
3362
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
3363
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
3364
+ "dependencies": {
3365
+ "safe-buffer": "5.2.1"
3366
+ },
3367
+ "engines": {
3368
+ "node": ">= 0.6"
3369
+ }
3370
+ },
3371
+ "node_modules/content-type": {
3372
+ "version": "1.0.5",
3373
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
3374
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
3375
+ "engines": {
3376
+ "node": ">= 0.6"
3377
+ }
3378
+ },
3379
  "node_modules/cookie": {
3380
  "version": "0.5.0",
3381
  "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
 
3385
  "node": ">= 0.6"
3386
  }
3387
  },
3388
+ "node_modules/cookie-signature": {
3389
+ "version": "1.0.6",
3390
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
3391
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
3392
+ },
3393
  "node_modules/create-require": {
3394
  "version": "1.1.1",
3395
  "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
 
3624
  "version": "1.1.4",
3625
  "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
3626
  "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
 
3627
  "dependencies": {
3628
  "es-define-property": "^1.0.0",
3629
  "es-errors": "^1.3.0",
 
3644
  "node": ">=0.4.0"
3645
  }
3646
  },
3647
+ "node_modules/depd": {
3648
+ "version": "2.0.0",
3649
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
3650
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
3651
+ "engines": {
3652
+ "node": ">= 0.8"
3653
+ }
3654
+ },
3655
  "node_modules/dequal": {
3656
  "version": "2.0.3",
3657
  "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
 
3661
  "node": ">=6"
3662
  }
3663
  },
3664
+ "node_modules/destroy": {
3665
+ "version": "1.2.0",
3666
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
3667
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
3668
+ "engines": {
3669
+ "node": ">= 0.8",
3670
+ "npm": "1.2.8000 || >= 1.4.16"
3671
+ }
3672
+ },
3673
  "node_modules/detect-indent": {
3674
  "version": "6.1.0",
3675
  "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
 
3774
  "safe-buffer": "^5.0.1"
3775
  }
3776
  },
3777
+ "node_modules/ee-first": {
3778
+ "version": "1.1.1",
3779
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
3780
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
3781
+ },
3782
  "node_modules/electron-to-chromium": {
3783
  "version": "1.4.359",
3784
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz",
 
3789
  "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
3790
  "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
3791
  },
3792
+ "node_modules/encodeurl": {
3793
+ "version": "1.0.2",
3794
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
3795
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
3796
+ "engines": {
3797
+ "node": ">= 0.8"
3798
+ }
3799
+ },
3800
  "node_modules/end-of-stream": {
3801
  "version": "1.4.4",
3802
  "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
 
3820
  "version": "1.0.0",
3821
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
3822
  "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
 
3823
  "dependencies": {
3824
  "get-intrinsic": "^1.2.4"
3825
  },
 
3831
  "version": "1.3.0",
3832
  "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
3833
  "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
 
3834
  "engines": {
3835
  "node": ">= 0.4"
3836
  }
 
4120
  "node": ">=0.10.0"
4121
  }
4122
  },
4123
+ "node_modules/etag": {
4124
+ "version": "1.8.1",
4125
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
4126
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
4127
+ "engines": {
4128
+ "node": ">= 0.6"
4129
+ }
4130
+ },
4131
  "node_modules/event-target-shim": {
4132
  "version": "5.0.1",
4133
  "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
 
4175
  "node": ">=6"
4176
  }
4177
  },
4178
+ "node_modules/express": {
4179
+ "version": "4.19.2",
4180
+ "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
4181
+ "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
4182
+ "dependencies": {
4183
+ "accepts": "~1.3.8",
4184
+ "array-flatten": "1.1.1",
4185
+ "body-parser": "1.20.2",
4186
+ "content-disposition": "0.5.4",
4187
+ "content-type": "~1.0.4",
4188
+ "cookie": "0.6.0",
4189
+ "cookie-signature": "1.0.6",
4190
+ "debug": "2.6.9",
4191
+ "depd": "2.0.0",
4192
+ "encodeurl": "~1.0.2",
4193
+ "escape-html": "~1.0.3",
4194
+ "etag": "~1.8.1",
4195
+ "finalhandler": "1.2.0",
4196
+ "fresh": "0.5.2",
4197
+ "http-errors": "2.0.0",
4198
+ "merge-descriptors": "1.0.1",
4199
+ "methods": "~1.1.2",
4200
+ "on-finished": "2.4.1",
4201
+ "parseurl": "~1.3.3",
4202
+ "path-to-regexp": "0.1.7",
4203
+ "proxy-addr": "~2.0.7",
4204
+ "qs": "6.11.0",
4205
+ "range-parser": "~1.2.1",
4206
+ "safe-buffer": "5.2.1",
4207
+ "send": "0.18.0",
4208
+ "serve-static": "1.15.0",
4209
+ "setprototypeof": "1.2.0",
4210
+ "statuses": "2.0.1",
4211
+ "type-is": "~1.6.18",
4212
+ "utils-merge": "1.0.1",
4213
+ "vary": "~1.1.2"
4214
+ },
4215
+ "engines": {
4216
+ "node": ">= 0.10.0"
4217
+ }
4218
+ },
4219
+ "node_modules/express/node_modules/cookie": {
4220
+ "version": "0.6.0",
4221
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
4222
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
4223
+ "engines": {
4224
+ "node": ">= 0.6"
4225
+ }
4226
+ },
4227
+ "node_modules/express/node_modules/debug": {
4228
+ "version": "2.6.9",
4229
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
4230
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
4231
+ "dependencies": {
4232
+ "ms": "2.0.0"
4233
+ }
4234
+ },
4235
+ "node_modules/express/node_modules/ms": {
4236
+ "version": "2.0.0",
4237
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
4238
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
4239
+ },
4240
+ "node_modules/express/node_modules/qs": {
4241
+ "version": "6.11.0",
4242
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
4243
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
4244
+ "dependencies": {
4245
+ "side-channel": "^1.0.4"
4246
+ },
4247
+ "engines": {
4248
+ "node": ">=0.6"
4249
+ },
4250
+ "funding": {
4251
+ "url": "https://github.com/sponsors/ljharb"
4252
+ }
4253
+ },
4254
  "node_modules/extend": {
4255
  "version": "3.0.2",
4256
  "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
 
4366
  "node": ">=8"
4367
  }
4368
  },
4369
+ "node_modules/finalhandler": {
4370
+ "version": "1.2.0",
4371
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
4372
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
4373
+ "dependencies": {
4374
+ "debug": "2.6.9",
4375
+ "encodeurl": "~1.0.2",
4376
+ "escape-html": "~1.0.3",
4377
+ "on-finished": "2.4.1",
4378
+ "parseurl": "~1.3.3",
4379
+ "statuses": "2.0.1",
4380
+ "unpipe": "~1.0.0"
4381
+ },
4382
+ "engines": {
4383
+ "node": ">= 0.8"
4384
+ }
4385
+ },
4386
+ "node_modules/finalhandler/node_modules/debug": {
4387
+ "version": "2.6.9",
4388
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
4389
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
4390
+ "dependencies": {
4391
+ "ms": "2.0.0"
4392
+ }
4393
+ },
4394
+ "node_modules/finalhandler/node_modules/ms": {
4395
+ "version": "2.0.0",
4396
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
4397
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
4398
+ },
4399
  "node_modules/find-up": {
4400
  "version": "5.0.0",
4401
  "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
 
4468
  "node": ">= 12.20"
4469
  }
4470
  },
4471
+ "node_modules/forwarded": {
4472
+ "version": "0.2.0",
4473
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
4474
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
4475
+ "engines": {
4476
+ "node": ">= 0.6"
4477
+ }
4478
+ },
4479
  "node_modules/fraction.js": {
4480
  "version": "4.2.0",
4481
  "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
 
4488
  "url": "https://www.patreon.com/infusion"
4489
  }
4490
  },
4491
+ "node_modules/fresh": {
4492
+ "version": "0.5.2",
4493
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
4494
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
4495
+ "engines": {
4496
+ "node": ">= 0.6"
4497
+ }
4498
+ },
4499
  "node_modules/fs-constants": {
4500
  "version": "1.0.0",
4501
  "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
 
4593
  "version": "1.2.4",
4594
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
4595
  "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
 
4596
  "dependencies": {
4597
  "es-errors": "^1.3.0",
4598
  "function-bind": "^1.1.2",
 
4723
  "version": "1.0.1",
4724
  "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
4725
  "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
 
4726
  "dependencies": {
4727
  "get-intrinsic": "^1.1.3"
4728
  },
 
4799
  "version": "1.0.2",
4800
  "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
4801
  "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
 
4802
  "dependencies": {
4803
  "es-define-property": "^1.0.0"
4804
  },
 
4810
  "version": "1.0.3",
4811
  "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
4812
  "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
 
4813
  "engines": {
4814
  "node": ">= 0.4"
4815
  },
 
4821
  "version": "1.0.3",
4822
  "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
4823
  "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
 
4824
  "engines": {
4825
  "node": ">= 0.4"
4826
  },
 
4879
  "node": ">=12"
4880
  }
4881
  },
4882
+ "node_modules/http-errors": {
4883
+ "version": "2.0.0",
4884
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
4885
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
4886
+ "dependencies": {
4887
+ "depd": "2.0.0",
4888
+ "inherits": "2.0.4",
4889
+ "setprototypeof": "1.2.0",
4890
+ "statuses": "2.0.1",
4891
+ "toidentifier": "1.0.1"
4892
+ },
4893
+ "engines": {
4894
+ "node": ">= 0.8"
4895
+ }
4896
+ },
4897
  "node_modules/http-proxy-agent": {
4898
  "version": "5.0.0",
4899
  "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
 
5051
  "node": ">= 12"
5052
  }
5053
  },
5054
+ "node_modules/ipaddr.js": {
5055
+ "version": "1.9.1",
5056
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
5057
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
5058
+ "engines": {
5059
+ "node": ">= 0.10"
5060
+ }
5061
+ },
5062
  "node_modules/is-arrayish": {
5063
  "version": "0.3.2",
5064
  "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
 
5621
  "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
5622
  "dev": true
5623
  },
5624
+ "node_modules/media-typer": {
5625
+ "version": "0.3.0",
5626
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
5627
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
5628
+ "engines": {
5629
+ "node": ">= 0.6"
5630
+ }
5631
+ },
5632
  "node_modules/memory-pager": {
5633
  "version": "1.5.0",
5634
  "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
5635
  "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
5636
  },
5637
+ "node_modules/merge-descriptors": {
5638
+ "version": "1.0.1",
5639
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
5640
+ "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
5641
+ },
5642
  "node_modules/merge-stream": {
5643
  "version": "2.0.0",
5644
  "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
 
5653
  "node": ">= 8"
5654
  }
5655
  },
5656
+ "node_modules/methods": {
5657
+ "version": "1.1.2",
5658
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
5659
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
5660
+ "engines": {
5661
+ "node": ">= 0.6"
5662
+ }
5663
+ },
5664
  "node_modules/micromatch": {
5665
  "version": "4.0.5",
5666
  "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
 
5673
  "node": ">=8.6"
5674
  }
5675
  },
5676
+ "node_modules/mime": {
5677
+ "version": "1.6.0",
5678
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
5679
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
5680
+ "bin": {
5681
+ "mime": "cli.js"
5682
+ },
5683
+ "engines": {
5684
+ "node": ">=4"
5685
+ }
5686
+ },
5687
  "node_modules/mime-db": {
5688
  "version": "1.52.0",
5689
  "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
 
5890
  "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
5891
  "dev": true
5892
  },
5893
+ "node_modules/negotiator": {
5894
+ "version": "0.6.3",
5895
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
5896
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
5897
+ "engines": {
5898
+ "node": ">= 0.6"
5899
+ }
5900
+ },
5901
  "node_modules/neo-async": {
5902
  "version": "2.6.2",
5903
  "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
 
6055
  "version": "1.13.1",
6056
  "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
6057
  "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
 
6058
  "funding": {
6059
  "url": "https://github.com/sponsors/ljharb"
6060
  }
 
6083
  "node": ">=14.0.0"
6084
  }
6085
  },
6086
+ "node_modules/on-finished": {
6087
+ "version": "2.4.1",
6088
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
6089
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
6090
+ "dependencies": {
6091
+ "ee-first": "1.1.1"
6092
+ },
6093
+ "engines": {
6094
+ "node": ">= 0.8"
6095
+ }
6096
+ },
6097
  "node_modules/once": {
6098
  "version": "1.4.0",
6099
  "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
 
6320
  "url": "https://github.com/inikulin/parse5?sponsor=1"
6321
  }
6322
  },
6323
+ "node_modules/parseurl": {
6324
+ "version": "1.3.3",
6325
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
6326
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
6327
+ "engines": {
6328
+ "node": ">= 0.8"
6329
+ }
6330
+ },
6331
  "node_modules/path-exists": {
6332
  "version": "4.0.0",
6333
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
 
6359
  "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
6360
  "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
6361
  },
6362
+ "node_modules/path-to-regexp": {
6363
+ "version": "0.1.7",
6364
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
6365
+ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
6366
+ },
6367
  "node_modules/path-type": {
6368
  "version": "4.0.0",
6369
  "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
 
6982
  "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz",
6983
  "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="
6984
  },
6985
+ "node_modules/prom-client": {
6986
+ "version": "15.1.2",
6987
+ "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.2.tgz",
6988
+ "integrity": "sha512-on3h1iXb04QFLLThrmVYg1SChBQ9N1c+nKAjebBjokBqipddH3uxmOUcEkTnzmJ8Jh/5TSUnUqS40i2QB2dJHQ==",
6989
+ "dev": true,
6990
+ "dependencies": {
6991
+ "@opentelemetry/api": "^1.4.0",
6992
+ "tdigest": "^0.1.1"
6993
+ },
6994
+ "engines": {
6995
+ "node": "^16 || ^18 || >=20"
6996
+ }
6997
+ },
6998
  "node_modules/protobufjs": {
6999
  "version": "6.11.4",
7000
  "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
 
7020
  "pbts": "bin/pbts"
7021
  }
7022
  },
7023
+ "node_modules/proxy-addr": {
7024
+ "version": "2.0.7",
7025
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
7026
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
7027
+ "dependencies": {
7028
+ "forwarded": "0.2.0",
7029
+ "ipaddr.js": "1.9.1"
7030
+ },
7031
+ "engines": {
7032
+ "node": ">= 0.10"
7033
+ }
7034
+ },
7035
  "node_modules/psl": {
7036
  "version": "1.9.0",
7037
  "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
 
7120
  "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
7121
  "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
7122
  },
7123
+ "node_modules/range-parser": {
7124
+ "version": "1.2.1",
7125
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
7126
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
7127
+ "engines": {
7128
+ "node": ">= 0.6"
7129
+ }
7130
+ },
7131
+ "node_modules/raw-body": {
7132
+ "version": "2.5.2",
7133
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
7134
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
7135
+ "dependencies": {
7136
+ "bytes": "3.1.2",
7137
+ "http-errors": "2.0.0",
7138
+ "iconv-lite": "0.4.24",
7139
+ "unpipe": "1.0.0"
7140
+ },
7141
+ "engines": {
7142
+ "node": ">= 0.8"
7143
+ }
7144
+ },
7145
+ "node_modules/raw-body/node_modules/iconv-lite": {
7146
+ "version": "0.4.24",
7147
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
7148
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
7149
+ "dependencies": {
7150
+ "safer-buffer": ">= 2.1.2 < 3"
7151
+ },
7152
+ "engines": {
7153
+ "node": ">=0.10.0"
7154
+ }
7155
+ },
7156
  "node_modules/rc": {
7157
  "version": "1.2.8",
7158
  "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
 
7455
  "node": ">=10"
7456
  }
7457
  },
7458
+ "node_modules/send": {
7459
+ "version": "0.18.0",
7460
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
7461
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
7462
+ "dependencies": {
7463
+ "debug": "2.6.9",
7464
+ "depd": "2.0.0",
7465
+ "destroy": "1.2.0",
7466
+ "encodeurl": "~1.0.2",
7467
+ "escape-html": "~1.0.3",
7468
+ "etag": "~1.8.1",
7469
+ "fresh": "0.5.2",
7470
+ "http-errors": "2.0.0",
7471
+ "mime": "1.6.0",
7472
+ "ms": "2.1.3",
7473
+ "on-finished": "2.4.1",
7474
+ "range-parser": "~1.2.1",
7475
+ "statuses": "2.0.1"
7476
+ },
7477
+ "engines": {
7478
+ "node": ">= 0.8.0"
7479
+ }
7480
+ },
7481
+ "node_modules/send/node_modules/debug": {
7482
+ "version": "2.6.9",
7483
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
7484
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
7485
+ "dependencies": {
7486
+ "ms": "2.0.0"
7487
+ }
7488
+ },
7489
+ "node_modules/send/node_modules/debug/node_modules/ms": {
7490
+ "version": "2.0.0",
7491
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
7492
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
7493
+ },
7494
+ "node_modules/send/node_modules/ms": {
7495
+ "version": "2.1.3",
7496
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
7497
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
7498
+ },
7499
  "node_modules/serpapi": {
7500
  "version": "1.1.1",
7501
  "resolved": "https://registry.npmjs.org/serpapi/-/serpapi-1.1.1.tgz",
 
7504
  "undici": "^5.12.0"
7505
  }
7506
  },
7507
+ "node_modules/serve-static": {
7508
+ "version": "1.15.0",
7509
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
7510
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
7511
+ "dependencies": {
7512
+ "encodeurl": "~1.0.2",
7513
+ "escape-html": "~1.0.3",
7514
+ "parseurl": "~1.3.3",
7515
+ "send": "0.18.0"
7516
+ },
7517
+ "engines": {
7518
+ "node": ">= 0.8.0"
7519
+ }
7520
+ },
7521
  "node_modules/set-cookie-parser": {
7522
  "version": "2.6.0",
7523
  "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
 
7528
  "version": "1.2.2",
7529
  "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
7530
  "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
 
7531
  "dependencies": {
7532
  "define-data-property": "^1.1.4",
7533
  "es-errors": "^1.3.0",
 
7540
  "node": ">= 0.4"
7541
  }
7542
  },
7543
+ "node_modules/setprototypeof": {
7544
+ "version": "1.2.0",
7545
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
7546
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
7547
+ },
7548
  "node_modules/sharp": {
7549
  "version": "0.33.2",
7550
  "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz",
 
7609
  "version": "1.0.6",
7610
  "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
7611
  "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
 
7612
  "dependencies": {
7613
  "call-bind": "^1.0.7",
7614
  "es-errors": "^1.3.0",
 
7801
  "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
7802
  "dev": true
7803
  },
7804
+ "node_modules/statuses": {
7805
+ "version": "2.0.1",
7806
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
7807
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
7808
+ "engines": {
7809
+ "node": ">= 0.8"
7810
+ }
7811
+ },
7812
  "node_modules/std-env": {
7813
  "version": "3.3.3",
7814
  "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz",
 
8280
  "streamx": "^2.15.0"
8281
  }
8282
  },
8283
+ "node_modules/tdigest": {
8284
+ "version": "0.1.2",
8285
+ "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
8286
+ "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
8287
+ "dev": true,
8288
+ "dependencies": {
8289
+ "bintrees": "1.0.2"
8290
+ }
8291
+ },
8292
  "node_modules/text-table": {
8293
  "version": "0.2.0",
8294
  "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
 
8394
  "node": ">=8.0"
8395
  }
8396
  },
8397
+ "node_modules/toidentifier": {
8398
+ "version": "1.0.1",
8399
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
8400
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
8401
+ "engines": {
8402
+ "node": ">=0.6"
8403
+ }
8404
+ },
8405
  "node_modules/totalist": {
8406
  "version": "3.0.0",
8407
  "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz",
 
8552
  "url": "https://github.com/sponsors/sindresorhus"
8553
  }
8554
  },
8555
+ "node_modules/type-is": {
8556
+ "version": "1.6.18",
8557
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
8558
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
8559
+ "dependencies": {
8560
+ "media-typer": "0.3.0",
8561
+ "mime-types": "~2.1.24"
8562
+ },
8563
+ "engines": {
8564
+ "node": ">= 0.6"
8565
+ }
8566
+ },
8567
  "node_modules/typescript": {
8568
  "version": "5.2.2",
8569
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
 
8628
  "node": ">= 4.0.0"
8629
  }
8630
  },
8631
+ "node_modules/unpipe": {
8632
+ "version": "1.0.0",
8633
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
8634
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
8635
+ "engines": {
8636
+ "node": ">= 0.8"
8637
+ }
8638
+ },
8639
  "node_modules/unplugin": {
8640
  "version": "1.3.1",
8641
  "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
 
8740
  "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
8741
  "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
8742
  },
8743
+ "node_modules/utils-merge": {
8744
+ "version": "1.0.1",
8745
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
8746
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
8747
+ "engines": {
8748
+ "node": ">= 0.4.0"
8749
+ }
8750
+ },
8751
  "node_modules/uuid": {
8752
  "version": "9.0.1",
8753
  "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
 
8771
  "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz",
8772
  "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="
8773
  },
8774
+ "node_modules/vary": {
8775
+ "version": "1.1.2",
8776
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
8777
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
8778
+ "engines": {
8779
+ "node": ">= 0.8"
8780
+ }
8781
+ },
8782
  "node_modules/vite": {
8783
  "version": "4.5.3",
8784
  "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
package.json CHANGED
@@ -23,6 +23,7 @@
23
  "@sveltejs/adapter-node": "^1.3.1",
24
  "@sveltejs/kit": "^1.30.4",
25
  "@tailwindcss/typography": "^0.5.9",
 
26
  "@types/jsdom": "^21.1.1",
27
  "@types/minimist": "^1.2.5",
28
  "@types/parquetjs": "^0.10.3",
@@ -36,6 +37,7 @@
36
  "prettier": "^2.8.0",
37
  "prettier-plugin-svelte": "^2.10.1",
38
  "prettier-plugin-tailwindcss": "^0.2.7",
 
39
  "svelte": "^4.2.8",
40
  "svelte-check": "^3.6.2",
41
  "ts-node": "^10.9.1",
@@ -57,6 +59,7 @@
57
  "browser-image-resizer": "^2.4.1",
58
  "date-fns": "^2.29.3",
59
  "dotenv": "^16.0.3",
 
60
  "handlebars": "^4.7.8",
61
  "highlight.js": "^11.7.0",
62
  "image-size": "^1.0.2",
 
23
  "@sveltejs/adapter-node": "^1.3.1",
24
  "@sveltejs/kit": "^1.30.4",
25
  "@tailwindcss/typography": "^0.5.9",
26
+ "@types/express": "^4.17.21",
27
  "@types/jsdom": "^21.1.1",
28
  "@types/minimist": "^1.2.5",
29
  "@types/parquetjs": "^0.10.3",
 
37
  "prettier": "^2.8.0",
38
  "prettier-plugin-svelte": "^2.10.1",
39
  "prettier-plugin-tailwindcss": "^0.2.7",
40
+ "prom-client": "^15.1.2",
41
  "svelte": "^4.2.8",
42
  "svelte-check": "^3.6.2",
43
  "ts-node": "^10.9.1",
 
59
  "browser-image-resizer": "^2.4.1",
60
  "date-fns": "^2.29.3",
61
  "dotenv": "^16.0.3",
62
+ "express": "^4.19.2",
63
  "handlebars": "^4.7.8",
64
  "highlight.js": "^11.7.0",
65
  "image-size": "^1.0.2",
scripts/populate.ts CHANGED
@@ -2,12 +2,13 @@ import readline from "readline";
2
  import minimist from "minimist";
3
 
4
  // @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them
5
- import { MONGODB_URL } from "$env/static/private";
6
 
7
  import { faker } from "@faker-js/faker";
8
  import { ObjectId } from "mongodb";
9
 
10
- import { collections } from "../src/lib/server/database.ts";
 
11
  import { models } from "../src/lib/server/models.ts";
12
  import type { User } from "../src/lib/types/User";
13
  import type { Assistant } from "../src/lib/types/Assistant";
@@ -17,6 +18,7 @@ import { defaultEmbeddingModel } from "../src/lib/server/embeddingModels.ts";
17
  import { Message } from "../src/lib/types/Message.ts";
18
 
19
  import { addChildren } from "../src/lib/utils/tree/addChildren.ts";
 
20
 
21
  const rl = readline.createInterface({
22
  input: process.stdin,
@@ -158,10 +160,11 @@ async function seed() {
158
  console.log("Creating assistants for all users");
159
  await Promise.all(
160
  users.map(async (user) => {
 
161
  const assistants = faker.helpers.multiple<Assistant>(
162
  () => ({
163
  _id: new ObjectId(),
164
- name: faker.animal.insect(),
165
  createdById: user._id,
166
  createdByName: user.username,
167
  createdAt: faker.date.recent({ days: 30 }),
@@ -174,6 +177,8 @@ async function seed() {
174
  exampleInputs: faker.helpers.multiple(() => faker.lorem.sentence(), {
175
  count: faker.number.int({ min: 0, max: 4 }),
176
  }),
 
 
177
  }),
178
  { count: faker.number.int({ min: 3, max: 10 }) }
179
  );
@@ -241,7 +246,7 @@ async function seed() {
241
  try {
242
  rl.question(
243
  "You're about to run a seeding script on the following MONGODB_URL: \x1b[31m" +
244
- MONGODB_URL +
245
  "\x1b[0m\n\n With the following flags: \x1b[31m" +
246
  flags.join("\x1b[0m , \x1b[31m") +
247
  "\x1b[0m\n \n\n Are you sure you want to continue? (yes/no): ",
 
2
  import minimist from "minimist";
3
 
4
  // @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them
5
+ import { env } from "$env/dynamic/private";
6
 
7
  import { faker } from "@faker-js/faker";
8
  import { ObjectId } from "mongodb";
9
 
10
+ // @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them
11
+ import { collections } from "$lib/server/database";
12
  import { models } from "../src/lib/server/models.ts";
13
  import type { User } from "../src/lib/types/User";
14
  import type { Assistant } from "../src/lib/types/Assistant";
 
18
  import { Message } from "../src/lib/types/Message.ts";
19
 
20
  import { addChildren } from "../src/lib/utils/tree/addChildren.ts";
21
+ import { generateSearchTokens } from "../src/lib/utils/searchTokens.ts";
22
 
23
  const rl = readline.createInterface({
24
  input: process.stdin,
 
160
  console.log("Creating assistants for all users");
161
  await Promise.all(
162
  users.map(async (user) => {
163
+ const name = faker.animal.insect();
164
  const assistants = faker.helpers.multiple<Assistant>(
165
  () => ({
166
  _id: new ObjectId(),
167
+ name,
168
  createdById: user._id,
169
  createdByName: user.username,
170
  createdAt: faker.date.recent({ days: 30 }),
 
177
  exampleInputs: faker.helpers.multiple(() => faker.lorem.sentence(), {
178
  count: faker.number.int({ min: 0, max: 4 }),
179
  }),
180
+ searchTokens: generateSearchTokens(name),
181
+ last24HoursCount: faker.number.int({ min: 0, max: 1000 }),
182
  }),
183
  { count: faker.number.int({ min: 3, max: 10 }) }
184
  );
 
246
  try {
247
  rl.question(
248
  "You're about to run a seeding script on the following MONGODB_URL: \x1b[31m" +
249
+ env.MONGODB_URL +
250
  "\x1b[0m\n\n With the following flags: \x1b[31m" +
251
  flags.join("\x1b[0m , \x1b[31m") +
252
  "\x1b[0m\n \n\n Are you sure you want to continue? (yes/no): ",
src/hooks.server.ts CHANGED
@@ -1,17 +1,6 @@
1
- import {
2
- ADMIN_API_SECRET,
3
- COOKIE_NAME,
4
- ENABLE_ASSISTANTS,
5
- EXPOSE_API,
6
- MESSAGES_BEFORE_LOGIN,
7
- PARQUET_EXPORT_SECRET,
8
- } from "$env/static/private";
9
  import type { Handle, HandleServerError } from "@sveltejs/kit";
10
- import {
11
- PUBLIC_GOOGLE_ANALYTICS_ID,
12
- PUBLIC_ORIGIN,
13
- PUBLIC_APP_DISCLAIMER,
14
- } from "$env/static/public";
15
  import { collections } from "$lib/server/database";
16
  import { base } from "$app/paths";
17
  import { findUser, refreshSessionCookie, requiresUser } from "$lib/server/auth";
@@ -22,16 +11,30 @@ import { checkAndRunMigrations } from "$lib/migrations/migrations";
22
  import { building } from "$app/environment";
23
  import { refreshAssistantsCounts } from "$lib/assistantStats/refresh-assistants-counts";
24
  import { logger } from "$lib/server/logger";
 
 
25
 
 
26
  if (!building) {
27
  await checkAndRunMigrations();
28
- if (ENABLE_ASSISTANTS) {
29
  refreshAssistantsCounts();
30
  }
 
 
 
 
 
 
31
  }
32
 
33
  export const handleError: HandleServerError = async ({ error, event }) => {
34
  // handle 404
 
 
 
 
 
35
  if (event.route.id === null) {
36
  return {
37
  message: `Page ${event.url.pathname} not found`,
@@ -56,7 +59,14 @@ export const handleError: HandleServerError = async ({ error, event }) => {
56
  };
57
 
58
  export const handle: Handle = async ({ event, resolve }) => {
59
- if (event.url.pathname.startsWith(`${base}/api/`) && EXPOSE_API !== "true") {
 
 
 
 
 
 
 
60
  return new Response("API is disabled", { status: 403 });
61
  }
62
 
@@ -73,7 +83,7 @@ export const handle: Handle = async ({ event, resolve }) => {
73
  }
74
 
75
  if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) {
76
- const ADMIN_SECRET = ADMIN_API_SECRET || PARQUET_EXPORT_SECRET;
77
 
78
  if (!ADMIN_SECRET) {
79
  return errorResponse(500, "Admin API is not configured");
@@ -84,7 +94,7 @@ export const handle: Handle = async ({ event, resolve }) => {
84
  }
85
  }
86
 
87
- const token = event.cookies.get(COOKIE_NAME);
88
 
89
  let secretSessionId: string;
90
  let sessionId: string;
@@ -123,18 +133,18 @@ export const handle: Handle = async ({ event, resolve }) => {
123
  refreshSessionCookie(event.cookies, event.locals.sessionId);
124
 
125
  if (nativeFormContentTypes.includes(requestContentType)) {
126
- const referer = event.request.headers.get("referer");
127
 
128
- if (!referer) {
129
- return errorResponse(403, "Non-JSON form requests need to have a referer");
130
  }
131
 
132
  const validOrigins = [
133
- new URL(event.request.url).origin,
134
- ...(PUBLIC_ORIGIN ? [new URL(PUBLIC_ORIGIN).origin] : []),
135
  ];
136
 
137
- if (!validOrigins.includes(new URL(referer).origin)) {
138
  return errorResponse(403, "Invalid referer for POST request");
139
  }
140
  }
@@ -158,7 +168,7 @@ export const handle: Handle = async ({ event, resolve }) => {
158
  if (
159
  !event.locals.user &&
160
  requiresUser &&
161
- !((MESSAGES_BEFORE_LOGIN ? parseInt(MESSAGES_BEFORE_LOGIN) : 0) > 0)
162
  ) {
163
  return errorResponse(401, ERROR_MESSAGES.authOnly);
164
  }
@@ -169,7 +179,7 @@ export const handle: Handle = async ({ event, resolve }) => {
169
  if (
170
  !requiresUser &&
171
  !event.url.pathname.startsWith(`${base}/settings`) &&
172
- !!PUBLIC_APP_DISCLAIMER
173
  ) {
174
  const hasAcceptedEthicsModal = await collections.settings.countDocuments({
175
  sessionId: event.locals.sessionId,
@@ -192,7 +202,7 @@ export const handle: Handle = async ({ event, resolve }) => {
192
  }
193
  replaced = true;
194
 
195
- return chunk.html.replace("%gaId%", PUBLIC_GOOGLE_ANALYTICS_ID);
196
  },
197
  });
198
 
 
1
+ import { env } from "$env/dynamic/private";
2
+ import { env as envPublic } from "$env/dynamic/public";
 
 
 
 
 
 
3
  import type { Handle, HandleServerError } from "@sveltejs/kit";
 
 
 
 
 
4
  import { collections } from "$lib/server/database";
5
  import { base } from "$app/paths";
6
  import { findUser, refreshSessionCookie, requiresUser } from "$lib/server/auth";
 
11
  import { building } from "$app/environment";
12
  import { refreshAssistantsCounts } from "$lib/assistantStats/refresh-assistants-counts";
13
  import { logger } from "$lib/server/logger";
14
+ import { AbortedGenerations } from "$lib/server/abortedGenerations";
15
+ import { MetricsServer } from "$lib/server/metrics";
16
 
17
+ // TODO: move this code on a started server hook, instead of using a "building" flag
18
  if (!building) {
19
  await checkAndRunMigrations();
20
+ if (env.ENABLE_ASSISTANTS) {
21
  refreshAssistantsCounts();
22
  }
23
+
24
+ // Init metrics server
25
+ MetricsServer.getInstance();
26
+
27
+ // Init AbortedGenerations refresh process
28
+ AbortedGenerations.getInstance();
29
  }
30
 
31
  export const handleError: HandleServerError = async ({ error, event }) => {
32
  // handle 404
33
+
34
+ if (building) {
35
+ throw error;
36
+ }
37
+
38
  if (event.route.id === null) {
39
  return {
40
  message: `Page ${event.url.pathname} not found`,
 
59
  };
60
 
61
  export const handle: Handle = async ({ event, resolve }) => {
62
+ logger.debug({
63
+ locals: event.locals,
64
+ url: event.url.pathname,
65
+ params: event.params,
66
+ request: event.request,
67
+ });
68
+
69
+ if (event.url.pathname.startsWith(`${base}/api/`) && env.EXPOSE_API !== "true") {
70
  return new Response("API is disabled", { status: 403 });
71
  }
72
 
 
83
  }
84
 
85
  if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) {
86
+ const ADMIN_SECRET = env.ADMIN_API_SECRET || env.PARQUET_EXPORT_SECRET;
87
 
88
  if (!ADMIN_SECRET) {
89
  return errorResponse(500, "Admin API is not configured");
 
94
  }
95
  }
96
 
97
+ const token = event.cookies.get(env.COOKIE_NAME);
98
 
99
  let secretSessionId: string;
100
  let sessionId: string;
 
133
  refreshSessionCookie(event.cookies, event.locals.sessionId);
134
 
135
  if (nativeFormContentTypes.includes(requestContentType)) {
136
+ const origin = event.request.headers.get("origin");
137
 
138
+ if (!origin) {
139
+ return errorResponse(403, "Non-JSON form requests need to have an origin");
140
  }
141
 
142
  const validOrigins = [
143
+ new URL(event.request.url).host,
144
+ ...(envPublic.PUBLIC_ORIGIN ? [new URL(envPublic.PUBLIC_ORIGIN).host] : []),
145
  ];
146
 
147
+ if (!validOrigins.includes(new URL(origin).host)) {
148
  return errorResponse(403, "Invalid referer for POST request");
149
  }
150
  }
 
168
  if (
169
  !event.locals.user &&
170
  requiresUser &&
171
+ !((env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0) > 0)
172
  ) {
173
  return errorResponse(401, ERROR_MESSAGES.authOnly);
174
  }
 
179
  if (
180
  !requiresUser &&
181
  !event.url.pathname.startsWith(`${base}/settings`) &&
182
+ !!envPublic.PUBLIC_APP_DISCLAIMER
183
  ) {
184
  const hasAcceptedEthicsModal = await collections.settings.countDocuments({
185
  sessionId: event.locals.sessionId,
 
202
  }
203
  replaced = true;
204
 
205
+ return chunk.html.replace("%gaId%", envPublic.PUBLIC_GOOGLE_ANALYTICS_ID);
206
  },
207
  });
208
 
src/lib/assistantStats/refresh-assistants-counts.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { client, collections } from "$lib/server/database";
2
  import { acquireLock, refreshLock } from "$lib/migrations/lock";
3
  import type { ObjectId } from "mongodb";
4
  import { subDays } from "date-fns";
@@ -15,44 +15,49 @@ async function refreshAssistantsCountsHelper() {
15
  }
16
 
17
  try {
18
- await client.withSession((session) =>
19
- session.withTransaction(async () => {
20
- await collections.assistants
21
- .aggregate([
22
- { $project: { _id: 1 } },
23
- { $set: { last24HoursCount: 0 } },
24
- {
25
- $unionWith: {
26
- coll: "assistants.stats",
27
- pipeline: [
28
- { $match: { "date.at": { $gte: subDays(new Date(), 1) }, "date.span": "hour" } },
29
- {
30
- $group: {
31
- _id: "$assistantId",
32
- last24HoursCount: { $sum: "$count" },
33
  },
34
- },
35
- ],
 
 
 
 
 
 
36
  },
37
- },
38
- {
39
- $group: {
40
- _id: "$_id",
41
- last24HoursCount: { $sum: "$last24HoursCount" },
42
  },
43
- },
44
- {
45
- $merge: {
46
- into: "assistants",
47
- on: "_id",
48
- whenMatched: "merge",
49
- whenNotMatched: "discard",
50
  },
51
- },
52
- ])
53
- .next();
54
- })
55
- );
56
  } catch (e) {
57
  logger.error("Refresh assistants counter failed!");
58
  logger.error(e);
 
1
+ import { Database } from "$lib/server/database";
2
  import { acquireLock, refreshLock } from "$lib/migrations/lock";
3
  import type { ObjectId } from "mongodb";
4
  import { subDays } from "date-fns";
 
15
  }
16
 
17
  try {
18
+ await Database.getInstance()
19
+ .getClient()
20
+ .withSession((session) =>
21
+ session.withTransaction(async () => {
22
+ await Database.getInstance()
23
+ .getCollections()
24
+ .assistants.aggregate([
25
+ { $project: { _id: 1 } },
26
+ { $set: { last24HoursCount: 0 } },
27
+ {
28
+ $unionWith: {
29
+ coll: "assistants.stats",
30
+ pipeline: [
31
+ {
32
+ $match: { "date.at": { $gte: subDays(new Date(), 1) }, "date.span": "hour" },
33
  },
34
+ {
35
+ $group: {
36
+ _id: "$assistantId",
37
+ last24HoursCount: { $sum: "$count" },
38
+ },
39
+ },
40
+ ],
41
+ },
42
  },
43
+ {
44
+ $group: {
45
+ _id: "$_id",
46
+ last24HoursCount: { $sum: "$last24HoursCount" },
47
+ },
48
  },
49
+ {
50
+ $merge: {
51
+ into: "assistants",
52
+ on: "_id",
53
+ whenMatched: "merge",
54
+ whenNotMatched: "discard",
55
+ },
56
  },
57
+ ])
58
+ .next();
59
+ })
60
+ );
 
61
  } catch (e) {
62
  logger.error("Refresh assistants counter failed!");
63
  logger.error(e);
src/lib/components/AnnouncementBanner.svelte CHANGED
@@ -5,7 +5,7 @@
5
 
6
  <div class="flex items-center rounded-xl bg-gray-100 p-1 text-sm dark:bg-gray-800 {classNames}">
7
  <span
8
- class="mr-2 inline-flex items-center rounded-lg bg-gradient-to-br from-primary-300 px-2 py-1 text-xxs font-medium uppercase leading-3 text-primary-700 dark:from-primary-900 dark:text-primary-400"
9
  >New</span
10
  >
11
  {title}
 
5
 
6
  <div class="flex items-center rounded-xl bg-gray-100 p-1 text-sm dark:bg-gray-800 {classNames}">
7
  <span
8
+ class="from-primary-300 text-primary-700 dark:from-primary-900 dark:text-primary-400 mr-2 inline-flex items-center rounded-lg bg-gradient-to-br px-2 py-1 text-xxs font-medium uppercase leading-3"
9
  >New</span
10
  >
11
  {title}
src/lib/components/DisclaimerModal.svelte CHANGED
@@ -1,11 +1,7 @@
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { page } from "$app/stores";
4
- import {
5
- PUBLIC_APP_DESCRIPTION,
6
- PUBLIC_APP_NAME,
7
- PUBLIC_APP_DISCLAIMER_MESSAGE,
8
- } from "$env/static/public";
9
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
10
  import Modal from "$lib/components/Modal.svelte";
11
  import { useSettingsStore } from "$lib/stores/settings";
@@ -17,19 +13,19 @@
17
 
18
  <Modal>
19
  <div
20
- class="flex w-full flex-col items-center gap-6 bg-gradient-to-b from-primary-500/40 via-primary-500/10 to-primary-500/0 px-5 pb-8 pt-9 text-center sm:px-6"
21
  >
22
  <h2 class="flex items-center text-2xl font-semibold text-gray-800">
23
  <Logo classNames="mr-1" />
24
- {PUBLIC_APP_NAME}
25
  </h2>
26
 
27
  <p class="text-lg font-semibold leading-snug text-gray-800" style="text-wrap: balance;">
28
- {PUBLIC_APP_DESCRIPTION}
29
  </p>
30
 
31
  <p class="text-sm text-gray-500">
32
- {PUBLIC_APP_DISCLAIMER_MESSAGE}
33
  </p>
34
 
35
  <div class="flex w-full flex-col items-center gap-2">
@@ -61,7 +57,7 @@
61
  class="flex w-full items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
62
  >
63
  Sign in
64
- {#if PUBLIC_APP_NAME === "HuggingChat"}
65
  with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5 flex-none" /> Hugging Face
66
  {/if}
67
  </button>
 
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { page } from "$app/stores";
4
+ import { env as envPublic } from "$env/dynamic/public";
 
 
 
 
5
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
6
  import Modal from "$lib/components/Modal.svelte";
7
  import { useSettingsStore } from "$lib/stores/settings";
 
13
 
14
  <Modal>
15
  <div
16
+ class="from-primary-500/40 via-primary-500/10 to-primary-500/0 flex w-full flex-col items-center gap-6 bg-gradient-to-b px-5 pb-8 pt-9 text-center sm:px-6"
17
  >
18
  <h2 class="flex items-center text-2xl font-semibold text-gray-800">
19
  <Logo classNames="mr-1" />
20
+ {envPublic.PUBLIC_APP_NAME}
21
  </h2>
22
 
23
  <p class="text-lg font-semibold leading-snug text-gray-800" style="text-wrap: balance;">
24
+ {envPublic.PUBLIC_APP_DESCRIPTION}
25
  </p>
26
 
27
  <p class="text-sm text-gray-500">
28
+ {envPublic.PUBLIC_APP_DISCLAIMER_MESSAGE}
29
  </p>
30
 
31
  <div class="flex w-full flex-col items-center gap-2">
 
57
  class="flex w-full items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
58
  >
59
  Sign in
60
+ {#if envPublic.PUBLIC_APP_NAME === "HuggingChat"}
61
  with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5 flex-none" /> Hugging Face
62
  {/if}
63
  </button>
src/lib/components/LoginModal.svelte CHANGED
@@ -1,7 +1,7 @@
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { page } from "$app/stores";
4
- import { PUBLIC_APP_DESCRIPTION, PUBLIC_APP_NAME } from "$env/static/public";
5
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
6
  import Modal from "$lib/components/Modal.svelte";
7
  import { useSettingsStore } from "$lib/stores/settings";
@@ -13,14 +13,14 @@
13
 
14
  <Modal on:close>
15
  <div
16
- class="flex w-full flex-col items-center gap-6 bg-gradient-to-b from-primary-500/40 via-primary-500/10 to-primary-500/0 px-5 pb-8 pt-9 text-center"
17
  >
18
  <h2 class="flex items-center text-2xl font-semibold text-gray-800">
19
  <Logo classNames="mr-1" />
20
- {PUBLIC_APP_NAME}
21
  </h2>
22
  <p class="text-balance text-lg font-semibold leading-snug text-gray-800">
23
- {PUBLIC_APP_DESCRIPTION}
24
  </p>
25
  <p class="text-balance rounded-xl border bg-white/80 p-2 text-base text-gray-800">
26
  You have reached the guest message limit, <strong class="font-semibold"
@@ -40,7 +40,7 @@
40
  class="flex w-full items-center justify-center whitespace-nowrap rounded-full bg-black px-5 py-2 text-center text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
41
  >
42
  Sign in
43
- {#if PUBLIC_APP_NAME === "HuggingChat"}
44
  with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5" /> Hugging Face
45
  {/if}
46
  </button>
 
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { page } from "$app/stores";
4
+ import { env as envPublic } from "$env/dynamic/public";
5
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
6
  import Modal from "$lib/components/Modal.svelte";
7
  import { useSettingsStore } from "$lib/stores/settings";
 
13
 
14
  <Modal on:close>
15
  <div
16
+ class="from-primary-500/40 via-primary-500/10 to-primary-500/0 flex w-full flex-col items-center gap-6 bg-gradient-to-b px-5 pb-8 pt-9 text-center"
17
  >
18
  <h2 class="flex items-center text-2xl font-semibold text-gray-800">
19
  <Logo classNames="mr-1" />
20
+ {envPublic.PUBLIC_APP_NAME}
21
  </h2>
22
  <p class="text-balance text-lg font-semibold leading-snug text-gray-800">
23
+ {envPublic.PUBLIC_APP_DESCRIPTION}
24
  </p>
25
  <p class="text-balance rounded-xl border bg-white/80 p-2 text-base text-gray-800">
26
  You have reached the guest message limit, <strong class="font-semibold"
 
40
  class="flex w-full items-center justify-center whitespace-nowrap rounded-full bg-black px-5 py-2 text-center text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
41
  >
42
  Sign in
43
+ {#if envPublic.PUBLIC_APP_NAME === "HuggingChat"}
44
  with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5" /> Hugging Face
45
  {/if}
46
  </button>
src/lib/components/NavMenu.svelte CHANGED
@@ -4,7 +4,7 @@
4
  import Logo from "$lib/components/icons/Logo.svelte";
5
  import { switchTheme } from "$lib/switchTheme";
6
  import { isAborted } from "$lib/stores/isAborted";
7
- import { PUBLIC_APP_NAME, PUBLIC_ORIGIN } from "$env/static/public";
8
  import NavConversationItem from "./NavConversationItem.svelte";
9
  import type { LayoutData } from "../../routes/$types";
10
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
@@ -47,9 +47,12 @@
47
  </script>
48
 
49
  <div class="sticky top-0 flex flex-none items-center justify-between px-3 py-3.5 max-sm:pt-0">
50
- <a class="flex items-center rounded-xl text-lg font-semibold" href="{PUBLIC_ORIGIN}{base}/">
 
 
 
51
  <Logo classNames="mr-1" />
52
- {PUBLIC_APP_NAME}
53
  </a>
54
  <a
55
  href={`${base}/`}
@@ -142,7 +145,7 @@
142
  >
143
  Settings
144
  </a>
145
- {#if PUBLIC_APP_NAME === "HuggingChat"}
146
  <a
147
  href="{base}/privacy"
148
  class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
 
4
  import Logo from "$lib/components/icons/Logo.svelte";
5
  import { switchTheme } from "$lib/switchTheme";
6
  import { isAborted } from "$lib/stores/isAborted";
7
+ import { env as envPublic } from "$env/dynamic/public";
8
  import NavConversationItem from "./NavConversationItem.svelte";
9
  import type { LayoutData } from "../../routes/$types";
10
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
 
47
  </script>
48
 
49
  <div class="sticky top-0 flex flex-none items-center justify-between px-3 py-3.5 max-sm:pt-0">
50
+ <a
51
+ class="flex items-center rounded-xl text-lg font-semibold"
52
+ href="{envPublic.PUBLIC_ORIGIN}{base}/"
53
+ >
54
  <Logo classNames="mr-1" />
55
+ {envPublic.PUBLIC_APP_NAME}
56
  </a>
57
  <a
58
  href={`${base}/`}
 
145
  >
146
  Settings
147
  </a>
148
+ {#if envPublic.PUBLIC_APP_NAME === "HuggingChat"}
149
  <a
150
  href="{base}/privacy"
151
  class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
src/lib/components/chat/AssistantIntroduction.svelte CHANGED
@@ -10,7 +10,7 @@
10
  import CarbonUserMultiple from "~icons/carbon/user-multiple";
11
 
12
  import { share } from "$lib/utils/share";
13
- import { PUBLIC_ORIGIN, PUBLIC_SHARE_PREFIX } from "$env/static/public";
14
  import { page } from "$app/stores";
15
 
16
  export let assistant: Pick<
@@ -35,7 +35,8 @@
35
  (assistant?.rag?.allowedLinks?.length ?? 0) > 0 ||
36
  assistant?.dynamicPrompt;
37
 
38
- const prefix = PUBLIC_SHARE_PREFIX || `${PUBLIC_ORIGIN || $page.url.origin}${base}`;
 
39
 
40
  $: shareUrl = `${prefix}/assistant/${assistant?._id}`;
41
 
 
10
  import CarbonUserMultiple from "~icons/carbon/user-multiple";
11
 
12
  import { share } from "$lib/utils/share";
13
+ import { env as envPublic } from "$env/dynamic/public";
14
  import { page } from "$app/stores";
15
 
16
  export let assistant: Pick<
 
35
  (assistant?.rag?.allowedLinks?.length ?? 0) > 0 ||
36
  assistant?.dynamicPrompt;
37
 
38
+ const prefix =
39
+ envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || $page.url.origin}${base}`;
40
 
41
  $: shareUrl = `${prefix}/assistant/${assistant?._id}`;
42
 
src/lib/components/chat/ChatIntroduction.svelte CHANGED
@@ -1,7 +1,5 @@
1
  <script lang="ts">
2
- import { PUBLIC_APP_NAME, PUBLIC_VERSION } from "$env/static/public";
3
- import { PUBLIC_ANNOUNCEMENT_BANNERS } from "$env/static/public";
4
- import { PUBLIC_APP_DESCRIPTION } from "$env/static/public";
5
  import Logo from "$lib/components/icons/Logo.svelte";
6
  import { createEventDispatcher } from "svelte";
7
  import IconGear from "~icons/bi/gear-fill";
@@ -20,8 +18,8 @@
20
 
21
  $: currentModelMetadata = findCurrentModel(models, $settings.activeModel);
22
 
23
- const announcementBanners = PUBLIC_ANNOUNCEMENT_BANNERS
24
- ? JSON5.parse(PUBLIC_ANNOUNCEMENT_BANNERS)
25
  : [];
26
 
27
  const dispatch = createEventDispatcher<{ message: string }>();
@@ -32,15 +30,15 @@
32
  <div>
33
  <div class="mb-3 flex items-center text-2xl font-semibold">
34
  <Logo classNames="mr-1 flex-none" />
35
- {PUBLIC_APP_NAME}
36
  <div
37
  class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400 dark:border-gray-700/60 dark:bg-gray-800"
38
  >
39
- v{PUBLIC_VERSION}
40
  </div>
41
  </div>
42
  <p class="text-base text-gray-600 dark:text-gray-400">
43
- {PUBLIC_APP_DESCRIPTION ||
44
  "Making the community's best AI chat models available to everyone."}
45
  </p>
46
  </div>
 
1
  <script lang="ts">
2
+ import { env as envPublic } from "$env/dynamic/public";
 
 
3
  import Logo from "$lib/components/icons/Logo.svelte";
4
  import { createEventDispatcher } from "svelte";
5
  import IconGear from "~icons/bi/gear-fill";
 
18
 
19
  $: currentModelMetadata = findCurrentModel(models, $settings.activeModel);
20
 
21
+ const announcementBanners = envPublic.PUBLIC_ANNOUNCEMENT_BANNERS
22
+ ? JSON5.parse(envPublic.PUBLIC_ANNOUNCEMENT_BANNERS)
23
  : [];
24
 
25
  const dispatch = createEventDispatcher<{ message: string }>();
 
30
  <div>
31
  <div class="mb-3 flex items-center text-2xl font-semibold">
32
  <Logo classNames="mr-1 flex-none" />
33
+ {envPublic.PUBLIC_APP_NAME}
34
  <div
35
  class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400 dark:border-gray-700/60 dark:bg-gray-800"
36
  >
37
+ v{envPublic.PUBLIC_VERSION}
38
  </div>
39
  </div>
40
  <p class="text-base text-gray-600 dark:text-gray-400">
41
+ {envPublic.PUBLIC_APP_DESCRIPTION ||
42
  "Making the community's best AI chat models available to everyone."}
43
  </p>
44
  </div>
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -395,7 +395,7 @@
395
  <CarbonCheckmark class="text-[.6rem] sm:mr-1.5 sm:text-green-600" />
396
  <div class="text-green-600 max-sm:hidden">Link copied to clipboard</div>
397
  {:else}
398
- <CarbonExport class="text-[.6rem] sm:mr-1.5 sm:text-primary-500" />
399
  <div class="max-sm:hidden">Share this conversation</div>
400
  {/if}
401
  </button>
 
395
  <CarbonCheckmark class="text-[.6rem] sm:mr-1.5 sm:text-green-600" />
396
  <div class="text-green-600 max-sm:hidden">Link copied to clipboard</div>
397
  {:else}
398
+ <CarbonExport class="sm:text-primary-500 text-[.6rem] sm:mr-1.5" />
399
  <div class="max-sm:hidden">Share this conversation</div>
400
  {/if}
401
  </button>
src/lib/components/icons/Logo.svelte CHANGED
@@ -1,12 +1,12 @@
1
  <script lang="ts">
2
  import { page } from "$app/stores";
3
- import { PUBLIC_APP_ASSETS, PUBLIC_APP_NAME, PUBLIC_ORIGIN } from "$env/static/public";
4
  import { base } from "$app/paths";
5
 
6
  export let classNames = "";
7
  </script>
8
 
9
- {#if PUBLIC_APP_ASSETS === "chatui"}
10
  <svg
11
  height="30"
12
  width="30"
@@ -22,7 +22,7 @@
22
  {:else}
23
  <img
24
  class={classNames}
25
- alt="{PUBLIC_APP_NAME} logo"
26
- src="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/logo.svg"
27
  />
28
  {/if}
 
1
  <script lang="ts">
2
  import { page } from "$app/stores";
3
+ import { env as envPublic } from "$env/dynamic/public";
4
  import { base } from "$app/paths";
5
 
6
  export let classNames = "";
7
  </script>
8
 
9
+ {#if envPublic.PUBLIC_APP_ASSETS === "chatui"}
10
  <svg
11
  height="30"
12
  width="30"
 
22
  {:else}
23
  <img
24
  class={classNames}
25
+ alt="{envPublic.PUBLIC_APP_NAME} logo"
26
+ src="{envPublic.PUBLIC_ORIGIN || $page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/logo.svg"
27
  />
28
  {/if}
src/lib/migrations/migrations.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { client, collections } from "$lib/server/database";
2
  import { migrations } from "./routines";
3
  import { acquireLock, releaseLock, isDBLocked, refreshLock } from "./lock";
4
  import { isHuggingChat } from "$lib/utils/isHuggingChat";
@@ -13,12 +13,15 @@ export async function checkAndRunMigrations() {
13
  }
14
 
15
  // check if all migrations have already been run
16
- const migrationResults = await collections.migrationResults.find().toArray();
 
 
 
17
 
18
  logger.info("[MIGRATIONS] Begin check...");
19
 
20
  // connect to the database
21
- const connectedClient = await client.connect();
22
 
23
  const lockId = await acquireLock(LOCK_KEY);
24
 
@@ -71,23 +74,25 @@ export async function checkAndRunMigrations() {
71
  }. Applying...`
72
  );
73
 
74
- await collections.migrationResults.updateOne(
75
- { _id: migration._id },
76
- {
77
- $set: {
78
- name: migration.name,
79
- status: "ongoing",
 
 
 
80
  },
81
- },
82
- { upsert: true }
83
- );
84
 
85
  const session = connectedClient.startSession();
86
  let result = false;
87
 
88
  try {
89
  await session.withTransaction(async () => {
90
- result = await migration.up(connectedClient);
91
  });
92
  } catch (e) {
93
  logger.info(`[MIGRATIONS] "${migration.name}" failed!`);
@@ -96,16 +101,18 @@ export async function checkAndRunMigrations() {
96
  await session.endSession();
97
  }
98
 
99
- await collections.migrationResults.updateOne(
100
- { _id: migration._id },
101
- {
102
- $set: {
103
- name: migration.name,
104
- status: result ? "success" : "failure",
 
 
 
105
  },
106
- },
107
- { upsert: true }
108
- );
109
  }
110
  }
111
 
 
1
+ import { Database } from "$lib/server/database";
2
  import { migrations } from "./routines";
3
  import { acquireLock, releaseLock, isDBLocked, refreshLock } from "./lock";
4
  import { isHuggingChat } from "$lib/utils/isHuggingChat";
 
13
  }
14
 
15
  // check if all migrations have already been run
16
+ const migrationResults = await Database.getInstance()
17
+ .getCollections()
18
+ .migrationResults.find()
19
+ .toArray();
20
 
21
  logger.info("[MIGRATIONS] Begin check...");
22
 
23
  // connect to the database
24
+ const connectedClient = await Database.getInstance().getClient().connect();
25
 
26
  const lockId = await acquireLock(LOCK_KEY);
27
 
 
74
  }. Applying...`
75
  );
76
 
77
+ await Database.getInstance()
78
+ .getCollections()
79
+ .migrationResults.updateOne(
80
+ { _id: migration._id },
81
+ {
82
+ $set: {
83
+ name: migration.name,
84
+ status: "ongoing",
85
+ },
86
  },
87
+ { upsert: true }
88
+ );
 
89
 
90
  const session = connectedClient.startSession();
91
  let result = false;
92
 
93
  try {
94
  await session.withTransaction(async () => {
95
+ result = await migration.up(Database.getInstance());
96
  });
97
  } catch (e) {
98
  logger.info(`[MIGRATIONS] "${migration.name}" failed!`);
 
101
  await session.endSession();
102
  }
103
 
104
+ await Database.getInstance()
105
+ .getCollections()
106
+ .migrationResults.updateOne(
107
+ { _id: migration._id },
108
+ {
109
+ $set: {
110
+ name: migration.name,
111
+ status: result ? "success" : "failure",
112
+ },
113
  },
114
+ { upsert: true }
115
+ );
 
116
  }
117
  }
118
 
src/lib/migrations/routines/01-update-search-assistants.ts CHANGED
@@ -1,5 +1,5 @@
1
  import type { Migration } from ".";
2
- import { getCollections } from "$lib/server/database";
3
  import { ObjectId, type AnyBulkWriteOperation } from "mongodb";
4
  import type { Assistant } from "$lib/types/Assistant";
5
  import { generateSearchTokens } from "$lib/utils/searchTokens";
@@ -7,8 +7,8 @@ import { generateSearchTokens } from "$lib/utils/searchTokens";
7
  const migration: Migration = {
8
  _id: new ObjectId("5f9f3e3e3e3e3e3e3e3e3e3e"),
9
  name: "Update search assistants",
10
- up: async (client) => {
11
- const { assistants } = getCollections(client);
12
  let ops: AnyBulkWriteOperation<Assistant>[] = [];
13
 
14
  for await (const assistant of assistants
@@ -40,8 +40,8 @@ const migration: Migration = {
40
 
41
  return true;
42
  },
43
- down: async (client) => {
44
- const { assistants } = getCollections(client);
45
  await assistants.updateMany({}, { $unset: { searchTokens: "" } });
46
  return true;
47
  },
 
1
  import type { Migration } from ".";
2
+ import { collections } from "$lib/server/database";
3
  import { ObjectId, type AnyBulkWriteOperation } from "mongodb";
4
  import type { Assistant } from "$lib/types/Assistant";
5
  import { generateSearchTokens } from "$lib/utils/searchTokens";
 
7
  const migration: Migration = {
8
  _id: new ObjectId("5f9f3e3e3e3e3e3e3e3e3e3e"),
9
  name: "Update search assistants",
10
+ up: async () => {
11
+ const { assistants } = collections;
12
  let ops: AnyBulkWriteOperation<Assistant>[] = [];
13
 
14
  for await (const assistant of assistants
 
40
 
41
  return true;
42
  },
43
+ down: async () => {
44
+ const { assistants } = collections;
45
  await assistants.updateMany({}, { $unset: { searchTokens: "" } });
46
  return true;
47
  },
src/lib/migrations/routines/02-update-assistants-models.ts CHANGED
@@ -1,14 +1,14 @@
1
  import type { Migration } from ".";
2
- import { getCollections } from "$lib/server/database";
3
  import { ObjectId } from "mongodb";
4
 
5
  const updateAssistantsModels: Migration = {
6
  _id: new ObjectId("5f9f3f3f3f3f3f3f3f3f3f3f"),
7
  name: "Update deprecated models in assistants with the default model",
8
- up: async (client) => {
9
  const models = (await import("$lib/server/models")).models;
10
 
11
- const { assistants } = getCollections(client);
12
 
13
  const modelIds = models.map((el) => el.id); // string[]
14
  const defaultModelId = models[0].id;
 
1
  import type { Migration } from ".";
2
+ import { collections } from "$lib/server/database";
3
  import { ObjectId } from "mongodb";
4
 
5
  const updateAssistantsModels: Migration = {
6
  _id: new ObjectId("5f9f3f3f3f3f3f3f3f3f3f3f"),
7
  name: "Update deprecated models in assistants with the default model",
8
+ up: async () => {
9
  const models = (await import("$lib/server/models")).models;
10
 
11
+ const { assistants } = collections;
12
 
13
  const modelIds = models.map((el) => el.id); // string[]
14
  const defaultModelId = models[0].id;
src/lib/migrations/routines/index.ts CHANGED
@@ -1,13 +1,14 @@
1
- import type { MongoClient, ObjectId } from "mongodb";
2
 
3
  import updateSearchAssistant from "./01-update-search-assistants";
4
  import updateAssistantsModels from "./02-update-assistants-models";
 
5
 
6
  export interface Migration {
7
  _id: ObjectId;
8
  name: string;
9
- up: (client: MongoClient) => Promise<boolean>;
10
- down?: (client: MongoClient) => Promise<boolean>;
11
  runForFreshInstall?: "only" | "never"; // leave unspecified to run for both
12
  runForHuggingChat?: "only" | "never"; // leave unspecified to run for both
13
  runEveryTime?: boolean;
 
1
+ import type { ObjectId } from "mongodb";
2
 
3
  import updateSearchAssistant from "./01-update-search-assistants";
4
  import updateAssistantsModels from "./02-update-assistants-models";
5
+ import type { Database } from "$lib/server/database";
6
 
7
  export interface Migration {
8
  _id: ObjectId;
9
  name: string;
10
+ up: (client: Database) => Promise<boolean>;
11
+ down?: (client: Database) => Promise<boolean>;
12
  runForFreshInstall?: "only" | "never"; // leave unspecified to run for both
13
  runForHuggingChat?: "only" | "never"; // leave unspecified to run for both
14
  runEveryTime?: boolean;
src/lib/server/abortedGenerations.ts CHANGED
@@ -1,24 +1,38 @@
1
  // Shouldn't be needed if we dove into sveltekit internals, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850
2
 
3
- import { setTimeout } from "node:timers/promises";
4
- import { collections } from "./database";
5
  import { logger } from "$lib/server/logger";
 
6
 
7
- let closed = false;
8
- process.on("SIGINT", () => {
9
- closed = true;
10
- });
11
 
12
- export let abortedGenerations: Map<string, Date> = new Map();
13
 
14
- async function maintainAbortedGenerations() {
15
- while (!closed) {
16
- await setTimeout(1000);
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  try {
19
  const aborts = await collections.abortedGenerations.find({}).sort({ createdAt: 1 }).toArray();
20
 
21
- abortedGenerations = new Map(
22
  aborts.map(({ conversationId, createdAt }) => [conversationId.toString(), createdAt])
23
  );
24
  } catch (err) {
@@ -26,5 +40,3 @@ async function maintainAbortedGenerations() {
26
  }
27
  }
28
  }
29
-
30
- maintainAbortedGenerations();
 
1
  // Shouldn't be needed if we dove into sveltekit internals, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850
2
 
 
 
3
  import { logger } from "$lib/server/logger";
4
+ import { collections } from "$lib/server/database";
5
 
6
+ export class AbortedGenerations {
7
+ private static instance: AbortedGenerations;
 
 
8
 
9
+ private abortedGenerations: Map<string, Date> = new Map();
10
 
11
+ private constructor() {
12
+ const interval = setInterval(this.updateList, 1000);
 
13
 
14
+ process.on("SIGINT", () => {
15
+ clearInterval(interval);
16
+ });
17
+ }
18
+
19
+ public static getInstance(): AbortedGenerations {
20
+ if (!AbortedGenerations.instance) {
21
+ AbortedGenerations.instance = new AbortedGenerations();
22
+ }
23
+
24
+ return AbortedGenerations.instance;
25
+ }
26
+
27
+ public getList(): Map<string, Date> {
28
+ return this.abortedGenerations;
29
+ }
30
+
31
+ private async updateList() {
32
  try {
33
  const aborts = await collections.abortedGenerations.find({}).sort({ createdAt: 1 }).toArray();
34
 
35
+ this.abortedGenerations = new Map(
36
  aborts.map(({ conversationId, createdAt }) => [conversationId.toString(), createdAt])
37
  );
38
  } catch (err) {
 
40
  }
41
  }
42
  }
 
 
src/lib/server/auth.ts CHANGED
@@ -1,22 +1,11 @@
1
  import { Issuer, BaseClient, type UserinfoResponse, TokenSet, custom } from "openid-client";
2
  import { addHours, addWeeks } from "date-fns";
3
- import {
4
- COOKIE_NAME,
5
- OPENID_CLIENT_ID,
6
- OPENID_CLIENT_SECRET,
7
- OPENID_PROVIDER_URL,
8
- OPENID_SCOPES,
9
- OPENID_NAME_CLAIM,
10
- OPENID_TOLERANCE,
11
- OPENID_RESOURCE,
12
- OPENID_CONFIG,
13
- ALLOW_INSECURE_COOKIES,
14
- } from "$env/static/private";
15
  import { sha256 } from "$lib/utils/sha256";
16
  import { z } from "zod";
17
  import { dev } from "$app/environment";
18
  import type { Cookies } from "@sveltejs/kit";
19
- import { collections } from "./database";
20
  import JSON5 from "json5";
21
  import { logger } from "$lib/server/logger";
22
 
@@ -37,27 +26,27 @@ const stringWithDefault = (value: string) =>
37
 
38
  export const OIDConfig = z
39
  .object({
40
- CLIENT_ID: stringWithDefault(OPENID_CLIENT_ID),
41
- CLIENT_SECRET: stringWithDefault(OPENID_CLIENT_SECRET),
42
- PROVIDER_URL: stringWithDefault(OPENID_PROVIDER_URL),
43
- SCOPES: stringWithDefault(OPENID_SCOPES),
44
- NAME_CLAIM: stringWithDefault(OPENID_NAME_CLAIM).refine(
45
  (el) => !["preferred_username", "email", "picture", "sub"].includes(el),
46
  { message: "nameClaim cannot be one of the restricted keys." }
47
  ),
48
- TOLERANCE: stringWithDefault(OPENID_TOLERANCE),
49
- RESOURCE: stringWithDefault(OPENID_RESOURCE),
50
  })
51
- .parse(JSON5.parse(OPENID_CONFIG));
52
 
53
  export const requiresUser = !!OIDConfig.CLIENT_ID && !!OIDConfig.CLIENT_SECRET;
54
 
55
  export function refreshSessionCookie(cookies: Cookies, sessionId: string) {
56
- cookies.set(COOKIE_NAME, sessionId, {
57
  path: "/",
58
  // So that it works inside the space's iframe
59
- sameSite: dev || ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none",
60
- secure: !dev && !(ALLOW_INSECURE_COOKIES === "true"),
61
  httpOnly: true,
62
  expires: addWeeks(new Date(), 2),
63
  });
 
1
  import { Issuer, BaseClient, type UserinfoResponse, TokenSet, custom } from "openid-client";
2
  import { addHours, addWeeks } from "date-fns";
3
+ import { env } from "$env/dynamic/private";
 
 
 
 
 
 
 
 
 
 
 
4
  import { sha256 } from "$lib/utils/sha256";
5
  import { z } from "zod";
6
  import { dev } from "$app/environment";
7
  import type { Cookies } from "@sveltejs/kit";
8
+ import { collections } from "$lib/server/database";
9
  import JSON5 from "json5";
10
  import { logger } from "$lib/server/logger";
11
 
 
26
 
27
  export const OIDConfig = z
28
  .object({
29
+ CLIENT_ID: stringWithDefault(env.OPENID_CLIENT_ID),
30
+ CLIENT_SECRET: stringWithDefault(env.OPENID_CLIENT_SECRET),
31
+ PROVIDER_URL: stringWithDefault(env.OPENID_PROVIDER_URL),
32
+ SCOPES: stringWithDefault(env.OPENID_SCOPES),
33
+ NAME_CLAIM: stringWithDefault(env.OPENID_NAME_CLAIM).refine(
34
  (el) => !["preferred_username", "email", "picture", "sub"].includes(el),
35
  { message: "nameClaim cannot be one of the restricted keys." }
36
  ),
37
+ TOLERANCE: stringWithDefault(env.OPENID_TOLERANCE),
38
+ RESOURCE: stringWithDefault(env.OPENID_RESOURCE),
39
  })
40
+ .parse(JSON5.parse(env.OPENID_CONFIG));
41
 
42
  export const requiresUser = !!OIDConfig.CLIENT_ID && !!OIDConfig.CLIENT_SECRET;
43
 
44
  export function refreshSessionCookie(cookies: Cookies, sessionId: string) {
45
+ cookies.set(env.COOKIE_NAME, sessionId, {
46
  path: "/",
47
  // So that it works inside the space's iframe
48
+ sameSite: dev || env.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none",
49
+ secure: !dev && !(env.ALLOW_INSECURE_COOKIES === "true"),
50
  httpOnly: true,
51
  expires: addWeeks(new Date(), 2),
52
  });
src/lib/server/database.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { MONGODB_URL, MONGODB_DB_NAME, MONGODB_DIRECT_CONNECTION } from "$env/static/private";
2
  import { GridFSBucket, MongoClient } from "mongodb";
3
  import type { Conversation } from "$lib/types/Conversation";
4
  import type { SharedConversation } from "$lib/types/SharedConversation";
@@ -14,149 +14,196 @@ import type { MigrationResult } from "$lib/types/MigrationResult";
14
  import type { Semaphore } from "$lib/types/Semaphore";
15
  import type { AssistantStats } from "$lib/types/AssistantStats";
16
  import { logger } from "$lib/server/logger";
 
17
 
18
- if (!MONGODB_URL) {
19
- throw new Error(
20
- "Please specify the MONGODB_URL environment variable inside .env.local. Set it to mongodb://localhost:27017 if you are running MongoDB locally, or to a MongoDB Atlas free instance for example."
21
- );
22
- }
23
  export const CONVERSATION_STATS_COLLECTION = "conversations.stats";
24
 
25
- const client = new MongoClient(MONGODB_URL, {
26
- directConnection: MONGODB_DIRECT_CONNECTION === "true",
27
- });
28
-
29
- export const connectPromise = client.connect().catch(logger.error);
30
-
31
- export function getCollections(mongoClient: MongoClient) {
32
- const db = mongoClient.db(MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""));
33
-
34
- const conversations = db.collection<Conversation>("conversations");
35
- const conversationStats = db.collection<ConversationStats>(CONVERSATION_STATS_COLLECTION);
36
- const assistants = db.collection<Assistant>("assistants");
37
- const assistantStats = db.collection<AssistantStats>("assistants.stats");
38
- const reports = db.collection<Report>("reports");
39
- const sharedConversations = db.collection<SharedConversation>("sharedConversations");
40
- const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
41
- const settings = db.collection<Settings>("settings");
42
- const users = db.collection<User>("users");
43
- const sessions = db.collection<Session>("sessions");
44
- const messageEvents = db.collection<MessageEvent>("messageEvents");
45
- const bucket = new GridFSBucket(db, { bucketName: "files" });
46
- const migrationResults = db.collection<MigrationResult>("migrationResults");
47
- const semaphores = db.collection<Semaphore>("semaphores");
48
-
49
- return {
50
- conversations,
51
- conversationStats,
52
- assistants,
53
- assistantStats,
54
- reports,
55
- sharedConversations,
56
- abortedGenerations,
57
- settings,
58
- users,
59
- sessions,
60
- messageEvents,
61
- bucket,
62
- migrationResults,
63
- semaphores,
64
- };
65
- }
66
- const db = client.db(MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""));
67
-
68
- const collections = getCollections(client);
69
-
70
- const {
71
- conversations,
72
- conversationStats,
73
- assistants,
74
- assistantStats,
75
- reports,
76
- sharedConversations,
77
- abortedGenerations,
78
- settings,
79
- users,
80
- sessions,
81
- messageEvents,
82
- semaphores,
83
- } = collections;
84
-
85
- export { client, db, collections };
86
-
87
- client.on("open", () => {
88
- conversations
89
- .createIndex(
90
- { sessionId: 1, updatedAt: -1 },
91
- { partialFilterExpression: { sessionId: { $exists: true } } }
92
- )
93
- .catch(logger.error);
94
- conversations
95
- .createIndex(
96
- { userId: 1, updatedAt: -1 },
97
- { partialFilterExpression: { userId: { $exists: true } } }
98
- )
99
- .catch(logger.error);
100
- conversations
101
- .createIndex(
102
- { "message.id": 1, "message.ancestors": 1 },
103
- { partialFilterExpression: { userId: { $exists: true } } }
104
- )
105
- .catch(logger.error);
106
- // To do stats on conversations
107
- conversations.createIndex({ updatedAt: 1 }).catch(logger.error);
108
- // Not strictly necessary, could use _id, but more convenient. Also for stats
109
- conversations.createIndex({ createdAt: 1 }).catch(logger.error);
110
- // To do stats on conversation messages
111
- conversations.createIndex({ "messages.createdAt": 1 }, { sparse: true }).catch(logger.error);
112
- // Unique index for stats
113
- conversationStats
114
- .createIndex(
115
- {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  type: 1,
117
  "date.field": 1,
118
- "date.span": 1,
119
  "date.at": 1,
120
- distinct: 1,
121
- },
122
- { unique: true }
123
- )
124
- .catch(logger.error);
125
- // Allow easy check of last computed stat for given type/dateField
126
- conversationStats
127
- .createIndex({
128
- type: 1,
129
- "date.field": 1,
130
- "date.at": 1,
131
- })
132
- .catch(logger.error);
133
- abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(logger.error);
134
- abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(logger.error);
135
- sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(logger.error);
136
- settings.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(logger.error);
137
- settings.createIndex({ userId: 1 }, { unique: true, sparse: true }).catch(logger.error);
138
- settings.createIndex({ assistants: 1 }).catch(logger.error);
139
- users.createIndex({ hfUserId: 1 }, { unique: true }).catch(logger.error);
140
- users.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(logger.error);
141
- // No unicity because due to renames & outdated info from oauth provider, there may be the same username on different users
142
- users.createIndex({ username: 1 }).catch(logger.error);
143
- messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(logger.error);
144
- sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(logger.error);
145
- sessions.createIndex({ sessionId: 1 }, { unique: true }).catch(logger.error);
146
- assistants.createIndex({ createdById: 1, userCount: -1 }).catch(logger.error);
147
- assistants.createIndex({ userCount: 1 }).catch(logger.error);
148
- assistants.createIndex({ featured: 1, userCount: -1 }).catch(logger.error);
149
- assistants.createIndex({ modelId: 1, userCount: -1 }).catch(logger.error);
150
- assistants.createIndex({ searchTokens: 1 }).catch(logger.error);
151
- assistants.createIndex({ last24HoursCount: 1 }).catch(logger.error);
152
- assistantStats
153
- // Order of keys is important for the queries
154
- .createIndex({ "date.span": 1, "date.at": 1, assistantId: 1 }, { unique: true })
155
- .catch(logger.error);
156
- reports.createIndex({ assistantId: 1 }).catch(logger.error);
157
- reports.createIndex({ createdBy: 1, assistantId: 1 }).catch(logger.error);
158
-
159
- // Unique index for semaphore and migration results
160
- semaphores.createIndex({ key: 1 }, { unique: true }).catch(logger.error);
161
- semaphores.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(logger.error);
162
- });
 
1
+ import { env } from "$env/dynamic/private";
2
  import { GridFSBucket, MongoClient } from "mongodb";
3
  import type { Conversation } from "$lib/types/Conversation";
4
  import type { SharedConversation } from "$lib/types/SharedConversation";
 
14
  import type { Semaphore } from "$lib/types/Semaphore";
15
  import type { AssistantStats } from "$lib/types/AssistantStats";
16
  import { logger } from "$lib/server/logger";
17
+ import { building } from "$app/environment";
18
 
 
 
 
 
 
19
  export const CONVERSATION_STATS_COLLECTION = "conversations.stats";
20
 
21
+ export class Database {
22
+ private client: MongoClient;
23
+
24
+ private static instance: Database;
25
+
26
+ private constructor() {
27
+ if (!env.MONGODB_URL) {
28
+ throw new Error(
29
+ "Please specify the MONGODB_URL environment variable inside .env.local. Set it to mongodb://localhost:27017 if you are running MongoDB locally, or to a MongoDB Atlas free instance for example."
30
+ );
31
+ }
32
+
33
+ this.client = new MongoClient(env.MONGODB_URL, {
34
+ directConnection: env.MONGODB_DIRECT_CONNECTION === "true",
35
+ });
36
+
37
+ this.client.connect().catch((err) => {
38
+ logger.error("Connection error", err);
39
+ process.exit(1);
40
+ });
41
+ this.client.db(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""));
42
+ this.client.on("open", () => this.initDatabase());
43
+
44
+ // Disconnect DB on process kill
45
+ process.on("SIGINT", async () => {
46
+ await this.client.close(true);
47
+
48
+ // https://github.com/sveltejs/kit/issues/9540
49
+ setTimeout(() => {
50
+ process.exit(0);
51
+ }, 100);
52
+ });
53
+ }
54
+
55
+ public static getInstance(): Database {
56
+ if (!Database.instance) {
57
+ Database.instance = new Database();
58
+ }
59
+
60
+ return Database.instance;
61
+ }
62
+
63
+ /**
64
+ * Return mongoClient
65
+ */
66
+ public getClient(): MongoClient {
67
+ return this.client;
68
+ }
69
+
70
+ /**
71
+ * Return map of database's collections
72
+ */
73
+ public getCollections() {
74
+ const db = this.client.db(
75
+ env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")
76
+ );
77
+
78
+ const conversations = db.collection<Conversation>("conversations");
79
+ const conversationStats = db.collection<ConversationStats>(CONVERSATION_STATS_COLLECTION);
80
+ const assistants = db.collection<Assistant>("assistants");
81
+ const assistantStats = db.collection<AssistantStats>("assistants.stats");
82
+ const reports = db.collection<Report>("reports");
83
+ const sharedConversations = db.collection<SharedConversation>("sharedConversations");
84
+ const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
85
+ const settings = db.collection<Settings>("settings");
86
+ const users = db.collection<User>("users");
87
+ const sessions = db.collection<Session>("sessions");
88
+ const messageEvents = db.collection<MessageEvent>("messageEvents");
89
+ const bucket = new GridFSBucket(db, { bucketName: "files" });
90
+ const migrationResults = db.collection<MigrationResult>("migrationResults");
91
+ const semaphores = db.collection<Semaphore>("semaphores");
92
+
93
+ return {
94
+ conversations,
95
+ conversationStats,
96
+ assistants,
97
+ assistantStats,
98
+ reports,
99
+ sharedConversations,
100
+ abortedGenerations,
101
+ settings,
102
+ users,
103
+ sessions,
104
+ messageEvents,
105
+ bucket,
106
+ migrationResults,
107
+ semaphores,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Init database once connected: Index creation
113
+ * @private
114
+ */
115
+ private initDatabase() {
116
+ const {
117
+ conversations,
118
+ conversationStats,
119
+ assistants,
120
+ assistantStats,
121
+ reports,
122
+ sharedConversations,
123
+ abortedGenerations,
124
+ settings,
125
+ users,
126
+ sessions,
127
+ messageEvents,
128
+ semaphores,
129
+ } = this.getCollections();
130
+
131
+ conversations
132
+ .createIndex(
133
+ { sessionId: 1, updatedAt: -1 },
134
+ { partialFilterExpression: { sessionId: { $exists: true } } }
135
+ )
136
+ .catch(logger.error);
137
+ conversations
138
+ .createIndex(
139
+ { userId: 1, updatedAt: -1 },
140
+ { partialFilterExpression: { userId: { $exists: true } } }
141
+ )
142
+ .catch(logger.error);
143
+ conversations
144
+ .createIndex(
145
+ { "message.id": 1, "message.ancestors": 1 },
146
+ { partialFilterExpression: { userId: { $exists: true } } }
147
+ )
148
+ .catch(logger.error);
149
+ // Not strictly necessary, could use _id, but more convenient. Also for stats
150
+ // To do stats on conversation messages
151
+ conversations.createIndex({ "messages.createdAt": 1 }, { sparse: true }).catch(logger.error);
152
+ // Unique index for stats
153
+ conversationStats
154
+ .createIndex(
155
+ {
156
+ type: 1,
157
+ "date.field": 1,
158
+ "date.span": 1,
159
+ "date.at": 1,
160
+ distinct: 1,
161
+ },
162
+ { unique: true }
163
+ )
164
+ .catch(logger.error);
165
+ // Allow easy check of last computed stat for given type/dateField
166
+ conversationStats
167
+ .createIndex({
168
  type: 1,
169
  "date.field": 1,
 
170
  "date.at": 1,
171
+ })
172
+ .catch(logger.error);
173
+ abortedGenerations
174
+ .createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 })
175
+ .catch(logger.error);
176
+ abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(logger.error);
177
+ sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(logger.error);
178
+ settings.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(logger.error);
179
+ settings.createIndex({ userId: 1 }, { unique: true, sparse: true }).catch(logger.error);
180
+ settings.createIndex({ assistants: 1 }).catch(logger.error);
181
+ users.createIndex({ hfUserId: 1 }, { unique: true }).catch(logger.error);
182
+ users.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(logger.error);
183
+ // No unicity because due to renames & outdated info from oauth provider, there may be the same username on different users
184
+ users.createIndex({ username: 1 }).catch(logger.error);
185
+ messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(logger.error);
186
+ sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(logger.error);
187
+ sessions.createIndex({ sessionId: 1 }, { unique: true }).catch(logger.error);
188
+ assistants.createIndex({ createdById: 1, userCount: -1 }).catch(logger.error);
189
+ assistants.createIndex({ userCount: 1 }).catch(logger.error);
190
+ assistants.createIndex({ featured: 1, userCount: -1 }).catch(logger.error);
191
+ assistants.createIndex({ modelId: 1, userCount: -1 }).catch(logger.error);
192
+ assistants.createIndex({ searchTokens: 1 }).catch(logger.error);
193
+ assistants.createIndex({ last24HoursCount: 1 }).catch(logger.error);
194
+ assistantStats
195
+ // Order of keys is important for the queries
196
+ .createIndex({ "date.span": 1, "date.at": 1, assistantId: 1 }, { unique: true })
197
+ .catch(logger.error);
198
+ reports.createIndex({ assistantId: 1 }).catch(logger.error);
199
+ reports.createIndex({ createdBy: 1, assistantId: 1 }).catch(logger.error);
200
+
201
+ // Unique index for semaphore and migration results
202
+ semaphores.createIndex({ key: 1 }, { unique: true }).catch(logger.error);
203
+ semaphores.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(logger.error);
204
+ }
205
+ }
206
+
207
+ export const collections = building
208
+ ? ({} as unknown as ReturnType<typeof Database.prototype.getCollections>)
209
+ : Database.getInstance().getCollections();
 
 
 
 
src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { z } from "zod";
2
  import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
3
  import { chunk } from "$lib/utils/chunk";
4
- import { HF_TOKEN } from "$env/static/private";
5
  import { logger } from "$lib/server/logger";
6
 
7
  export const embeddingEndpointHfApiSchema = z.object({
@@ -11,7 +11,7 @@ export const embeddingEndpointHfApiSchema = z.object({
11
  authorization: z
12
  .string()
13
  .optional()
14
- .transform((v) => (!v && HF_TOKEN ? "Bearer " + HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header
15
  });
16
 
17
  export async function embeddingEndpointHfApi(
 
1
  import { z } from "zod";
2
  import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
3
  import { chunk } from "$lib/utils/chunk";
4
+ import { env } from "$env/dynamic/private";
5
  import { logger } from "$lib/server/logger";
6
 
7
  export const embeddingEndpointHfApiSchema = z.object({
 
11
  authorization: z
12
  .string()
13
  .optional()
14
+ .transform((v) => (!v && env.HF_TOKEN ? "Bearer " + env.HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header
15
  });
16
 
17
  export async function embeddingEndpointHfApi(
src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts CHANGED
@@ -1,14 +1,14 @@
1
  import { z } from "zod";
2
  import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
3
  import { chunk } from "$lib/utils/chunk";
4
- import { OPENAI_API_KEY } from "$env/static/private";
5
 
6
  export const embeddingEndpointOpenAIParametersSchema = z.object({
7
  weight: z.number().int().positive().default(1),
8
  model: z.any(),
9
  type: z.literal("openai"),
10
  url: z.string().url().default("https://api.openai.com/v1/embeddings"),
11
- apiKey: z.string().default(OPENAI_API_KEY),
12
  });
13
 
14
  export async function embeddingEndpointOpenAI(
 
1
  import { z } from "zod";
2
  import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
3
  import { chunk } from "$lib/utils/chunk";
4
+ import { env } from "$env/dynamic/private";
5
 
6
  export const embeddingEndpointOpenAIParametersSchema = z.object({
7
  weight: z.number().int().positive().default(1),
8
  model: z.any(),
9
  type: z.literal("openai"),
10
  url: z.string().url().default("https://api.openai.com/v1/embeddings"),
11
+ apiKey: z.string().default(env.OPENAI_API_KEY),
12
  });
13
 
14
  export async function embeddingEndpointOpenAI(
src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { z } from "zod";
2
  import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
3
  import { chunk } from "$lib/utils/chunk";
4
- import { HF_TOKEN } from "$env/static/private";
5
  import { logger } from "$lib/server/logger";
6
 
7
  export const embeddingEndpointTeiParametersSchema = z.object({
@@ -12,7 +12,7 @@ export const embeddingEndpointTeiParametersSchema = z.object({
12
  authorization: z
13
  .string()
14
  .optional()
15
- .transform((v) => (!v && HF_TOKEN ? "Bearer " + HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header
16
  });
17
 
18
  const getModelInfoByUrl = async (url: string, authorization?: string) => {
 
1
  import { z } from "zod";
2
  import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
3
  import { chunk } from "$lib/utils/chunk";
4
+ import { env } from "$env/dynamic/private";
5
  import { logger } from "$lib/server/logger";
6
 
7
  export const embeddingEndpointTeiParametersSchema = z.object({
 
12
  authorization: z
13
  .string()
14
  .optional()
15
+ .transform((v) => (!v && env.HF_TOKEN ? "Bearer " + env.HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header
16
  });
17
 
18
  const getModelInfoByUrl = async (url: string, authorization?: string) => {
src/lib/server/embeddingModels.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { TEXT_EMBEDDING_MODELS } from "$env/static/private";
2
 
3
  import { z } from "zod";
4
  import { sum } from "$lib/utils/sum";
@@ -29,7 +29,7 @@ const modelConfig = z.object({
29
 
30
  // Default embedding model for backward compatibility
31
  const rawEmbeddingModelJSON =
32
- TEXT_EMBEDDING_MODELS ||
33
  `[
34
  {
35
  "name": "Xenova/gte-small",
 
1
+ import { env } from "$env/dynamic/private";
2
 
3
  import { z } from "zod";
4
  import { sum } from "$lib/utils/sum";
 
29
 
30
  // Default embedding model for backward compatibility
31
  const rawEmbeddingModelJSON =
32
+ env.TEXT_EMBEDDING_MODELS ||
33
  `[
34
  {
35
  "name": "Xenova/gte-small",
src/lib/server/endpoints/anthropic/endpointAnthropic.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { z } from "zod";
2
- import { ANTHROPIC_API_KEY } from "$env/static/private";
3
  import type { Endpoint } from "../endpoints";
4
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
5
 
@@ -8,7 +8,7 @@ export const endpointAnthropicParametersSchema = z.object({
8
  model: z.any(),
9
  type: z.literal("anthropic"),
10
  baseURL: z.string().url().default("https://api.anthropic.com"),
11
- apiKey: z.string().default(ANTHROPIC_API_KEY ?? "sk-"),
12
  defaultHeaders: z.record(z.string()).optional(),
13
  defaultQuery: z.record(z.string()).optional(),
14
  });
 
1
  import { z } from "zod";
2
+ import { env } from "$env/dynamic/private";
3
  import type { Endpoint } from "../endpoints";
4
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
5
 
 
8
  model: z.any(),
9
  type: z.literal("anthropic"),
10
  baseURL: z.string().url().default("https://api.anthropic.com"),
11
+ apiKey: z.string().default(env.ANTHROPIC_API_KEY ?? "sk-"),
12
  defaultHeaders: z.record(z.string()).optional(),
13
  defaultQuery: z.record(z.string()).optional(),
14
  });
src/lib/server/endpoints/cloudflare/endpointCloudflare.ts CHANGED
@@ -1,15 +1,15 @@
1
  import { z } from "zod";
2
  import type { Endpoint } from "../endpoints";
3
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
4
- import { CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN } from "$env/static/private";
5
  import { logger } from "$lib/server/logger";
6
 
7
  export const endpointCloudflareParametersSchema = z.object({
8
  weight: z.number().int().positive().default(1),
9
  model: z.any(),
10
  type: z.literal("cloudflare"),
11
- accountId: z.string().default(CLOUDFLARE_ACCOUNT_ID),
12
- apiToken: z.string().default(CLOUDFLARE_API_TOKEN),
13
  });
14
 
15
  export async function endpointCloudflare(
 
1
  import { z } from "zod";
2
  import type { Endpoint } from "../endpoints";
3
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
4
+ import { env } from "$env/dynamic/private";
5
  import { logger } from "$lib/server/logger";
6
 
7
  export const endpointCloudflareParametersSchema = z.object({
8
  weight: z.number().int().positive().default(1),
9
  model: z.any(),
10
  type: z.literal("cloudflare"),
11
+ accountId: z.string().default(env.CLOUDFLARE_ACCOUNT_ID),
12
+ apiToken: z.string().default(env.CLOUDFLARE_API_TOKEN),
13
  });
14
 
15
  export async function endpointCloudflare(
src/lib/server/endpoints/cohere/endpointCohere.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { z } from "zod";
2
- import { COHERE_API_TOKEN } from "$env/static/private";
3
  import type { Endpoint } from "../endpoints";
4
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
5
  import type { Cohere, CohereClient } from "cohere-ai";
@@ -9,7 +9,7 @@ export const endpointCohereParametersSchema = z.object({
9
  weight: z.number().int().positive().default(1),
10
  model: z.any(),
11
  type: z.literal("cohere"),
12
- apiKey: z.string().default(COHERE_API_TOKEN),
13
  raw: z.boolean().default(false),
14
  });
15
 
 
1
  import { z } from "zod";
2
+ import { env } from "$env/dynamic/private";
3
  import type { Endpoint } from "../endpoints";
4
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
5
  import type { Cohere, CohereClient } from "cohere-ai";
 
9
  weight: z.number().int().positive().default(1),
10
  model: z.any(),
11
  type: z.literal("cohere"),
12
+ apiKey: z.string().default(env.COHERE_API_TOKEN),
13
  raw: z.boolean().default(false),
14
  });
15
 
src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { HF_ACCESS_TOKEN, HF_TOKEN } from "$env/static/private";
2
  import { buildPrompt } from "$lib/buildPrompt";
3
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
4
  import type { Endpoint } from "../endpoints";
@@ -13,7 +13,7 @@ export const endpointLlamacppParametersSchema = z.object({
13
  accessToken: z
14
  .string()
15
  .min(1)
16
- .default(HF_TOKEN ?? HF_ACCESS_TOKEN),
17
  });
18
 
19
  export function endpointLlamacpp(
 
1
+ import { env } from "$env/dynamic/private";
2
  import { buildPrompt } from "$lib/buildPrompt";
3
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
4
  import type { Endpoint } from "../endpoints";
 
13
  accessToken: z
14
  .string()
15
  .min(1)
16
+ .default(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN),
17
  });
18
 
19
  export function endpointLlamacpp(
src/lib/server/endpoints/openai/endpointOai.ts CHANGED
@@ -2,7 +2,7 @@ import { z } from "zod";
2
  import { openAICompletionToTextGenerationStream } from "./openAICompletionToTextGenerationStream";
3
  import { openAIChatToTextGenerationStream } from "./openAIChatToTextGenerationStream";
4
  import { buildPrompt } from "$lib/buildPrompt";
5
- import { OPENAI_API_KEY } from "$env/static/private";
6
  import type { Endpoint } from "../endpoints";
7
 
8
  export const endpointOAIParametersSchema = z.object({
@@ -10,7 +10,7 @@ export const endpointOAIParametersSchema = z.object({
10
  model: z.any(),
11
  type: z.literal("openai"),
12
  baseURL: z.string().url().default("https://api.openai.com/v1"),
13
- apiKey: z.string().default(OPENAI_API_KEY ?? "sk-"),
14
  completion: z
15
  .union([z.literal("completions"), z.literal("chat_completions")])
16
  .default("chat_completions"),
 
2
  import { openAICompletionToTextGenerationStream } from "./openAICompletionToTextGenerationStream";
3
  import { openAIChatToTextGenerationStream } from "./openAIChatToTextGenerationStream";
4
  import { buildPrompt } from "$lib/buildPrompt";
5
+ import { env } from "$env/dynamic/private";
6
  import type { Endpoint } from "../endpoints";
7
 
8
  export const endpointOAIParametersSchema = z.object({
 
10
  model: z.any(),
11
  type: z.literal("openai"),
12
  baseURL: z.string().url().default("https://api.openai.com/v1"),
13
+ apiKey: z.string().default(env.OPENAI_API_KEY ?? "sk-"),
14
  completion: z
15
  .union([z.literal("completions"), z.literal("chat_completions")])
16
  .default("chat_completions"),
src/lib/server/endpoints/tgi/endpointTgi.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { HF_ACCESS_TOKEN, HF_TOKEN } from "$env/static/private";
2
  import { buildPrompt } from "$lib/buildPrompt";
3
  import { textGenerationStream } from "@huggingface/inference";
4
  import type { Endpoint } from "../endpoints";
@@ -9,7 +9,7 @@ export const endpointTgiParametersSchema = z.object({
9
  model: z.any(),
10
  type: z.literal("tgi"),
11
  url: z.string().url(),
12
- accessToken: z.string().default(HF_TOKEN ?? HF_ACCESS_TOKEN),
13
  authorization: z.string().optional(),
14
  });
15
 
 
1
+ import { env } from "$env/dynamic/private";
2
  import { buildPrompt } from "$lib/buildPrompt";
3
  import { textGenerationStream } from "@huggingface/inference";
4
  import type { Endpoint } from "../endpoints";
 
9
  model: z.any(),
10
  type: z.literal("tgi"),
11
  url: z.string().url(),
12
+ accessToken: z.string().default(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN),
13
  authorization: z.string().optional(),
14
  });
15
 
src/lib/server/files/downloadFile.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { error } from "@sveltejs/kit";
2
- import { collections } from "../database";
3
  import type { Conversation } from "$lib/types/Conversation";
4
  import type { SharedConversation } from "$lib/types/SharedConversation";
5
 
 
1
  import { error } from "@sveltejs/kit";
2
+ import { collections } from "$lib/server/database";
3
  import type { Conversation } from "$lib/types/Conversation";
4
  import type { SharedConversation } from "$lib/types/SharedConversation";
5
 
src/lib/server/files/uploadFile.ts CHANGED
@@ -1,6 +1,6 @@
1
  import type { Conversation } from "$lib/types/Conversation";
2
  import { sha256 } from "$lib/utils/sha256";
3
- import { collections } from "../database";
4
 
5
  export async function uploadFile(file: Blob, conv: Conversation): Promise<string> {
6
  const sha = await sha256(await file.text());
 
1
  import type { Conversation } from "$lib/types/Conversation";
2
  import { sha256 } from "$lib/utils/sha256";
3
+ import { collections } from "$lib/server/database";
4
 
5
  export async function uploadFile(file: Blob, conv: Conversation): Promise<string> {
6
  const sha = await sha256(await file.text());
src/lib/server/logger.ts CHANGED
@@ -1,11 +1,11 @@
1
  import pino from "pino";
2
  import { dev } from "$app/environment";
 
3
 
4
  let options: pino.LoggerOptions = {};
5
 
6
  if (dev) {
7
  options = {
8
- level: "debug",
9
  transport: {
10
  target: "pino-pretty",
11
  options: {
@@ -15,4 +15,4 @@ if (dev) {
15
  };
16
  }
17
 
18
- export const logger = pino(options);
 
1
  import pino from "pino";
2
  import { dev } from "$app/environment";
3
+ import { env } from "$env/dynamic/private";
4
 
5
  let options: pino.LoggerOptions = {};
6
 
7
  if (dev) {
8
  options = {
 
9
  transport: {
10
  target: "pino-pretty",
11
  options: {
 
15
  };
16
  }
17
 
18
+ export const logger = pino({ ...options, level: env.LOG_LEVEL ?? "info" });
src/lib/server/metrics.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { collectDefaultMetrics, Registry } from "prom-client";
2
+ import express from "express";
3
+ import { logger } from "$lib/server/logger";
4
+ import { env } from "$env/dynamic/private";
5
+
6
+ export class MetricsServer {
7
+ private static instance: MetricsServer;
8
+
9
+ private constructor() {
10
+ const app = express();
11
+ const port = env.METRICS_PORT || "5565";
12
+
13
+ const server = app.listen(port, () => {
14
+ logger.info(`Metrics server listening on port ${port}`);
15
+ });
16
+
17
+ const register = new Registry();
18
+ collectDefaultMetrics({ register });
19
+
20
+ app.get("/metrics", (req, res) => {
21
+ register.metrics().then((metrics) => {
22
+ res.set("Content-Type", "text/plain");
23
+ res.send(metrics);
24
+ });
25
+ });
26
+
27
+ process.on("SIGINT", async () => {
28
+ logger.info("Sigint received, disconnect metrics server ...");
29
+ server.close(() => {
30
+ logger.info("Server stopped ...");
31
+ });
32
+ process.exit();
33
+ });
34
+ }
35
+
36
+ public static getInstance(): MetricsServer {
37
+ if (!MetricsServer.instance) {
38
+ MetricsServer.instance = new MetricsServer();
39
+ }
40
+
41
+ return MetricsServer.instance;
42
+ }
43
+ }
src/lib/server/models.ts CHANGED
@@ -1,11 +1,4 @@
1
- import {
2
- HF_TOKEN,
3
- HF_API_ROOT,
4
- MODELS,
5
- OLD_MODELS,
6
- TASK_MODEL,
7
- HF_ACCESS_TOKEN,
8
- } from "$env/static/private";
9
  import type { ChatTemplateInput } from "$lib/types/Template";
10
  import { compileTemplate } from "$lib/utils/template";
11
  import { z } from "zod";
@@ -72,7 +65,7 @@ const modelConfig = z.object({
72
  embeddingModel: validateEmbeddingModelByName(embeddingModels).optional(),
73
  });
74
 
75
- const modelsRaw = z.array(modelConfig).parse(JSON5.parse(MODELS));
76
 
77
  async function getChatPromptRender(
78
  m: z.infer<typeof modelConfig>
@@ -147,8 +140,8 @@ const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
147
  if (!m.endpoints) {
148
  return endpointTgi({
149
  type: "tgi",
150
- url: `${HF_API_ROOT}/${m.name}`,
151
- accessToken: HF_TOKEN ?? HF_ACCESS_TOKEN,
152
  weight: 1,
153
  model: m,
154
  });
@@ -199,7 +192,7 @@ export const models = await Promise.all(modelsRaw.map((e) => processModel(e).the
199
  export const defaultModel = models[0];
200
 
201
  // Models that have been deprecated
202
- export const oldModels = OLD_MODELS
203
  ? z
204
  .array(
205
  z.object({
@@ -208,7 +201,7 @@ export const oldModels = OLD_MODELS
208
  displayName: z.string().min(1).optional(),
209
  })
210
  )
211
- .parse(JSON5.parse(OLD_MODELS))
212
  .map((m) => ({ ...m, id: m.id || m.name, displayName: m.displayName || m.name }))
213
  : [];
214
 
@@ -219,9 +212,9 @@ export const validateModel = (_models: BackendModel[]) => {
219
 
220
  // if `TASK_MODEL` is string & name of a model in `MODELS`, then we use `MODELS[TASK_MODEL]`, else we try to parse `TASK_MODEL` as a model config itself
221
 
222
- export const smallModel = TASK_MODEL
223
- ? (models.find((m) => m.name === TASK_MODEL) ||
224
- (await processModel(modelConfig.parse(JSON5.parse(TASK_MODEL))).then((m) =>
225
  addEndpoint(m)
226
  ))) ??
227
  defaultModel
 
1
+ import { env } from "$env/dynamic/private";
 
 
 
 
 
 
 
2
  import type { ChatTemplateInput } from "$lib/types/Template";
3
  import { compileTemplate } from "$lib/utils/template";
4
  import { z } from "zod";
 
65
  embeddingModel: validateEmbeddingModelByName(embeddingModels).optional(),
66
  });
67
 
68
+ const modelsRaw = z.array(modelConfig).parse(JSON5.parse(env.MODELS));
69
 
70
  async function getChatPromptRender(
71
  m: z.infer<typeof modelConfig>
 
140
  if (!m.endpoints) {
141
  return endpointTgi({
142
  type: "tgi",
143
+ url: `${env.HF_API_ROOT}/${m.name}`,
144
+ accessToken: env.HF_TOKEN ?? env.HF_ACCESS_TOKEN,
145
  weight: 1,
146
  model: m,
147
  });
 
192
  export const defaultModel = models[0];
193
 
194
  // Models that have been deprecated
195
+ export const oldModels = env.OLD_MODELS
196
  ? z
197
  .array(
198
  z.object({
 
201
  displayName: z.string().min(1).optional(),
202
  })
203
  )
204
+ .parse(JSON5.parse(env.OLD_MODELS))
205
  .map((m) => ({ ...m, id: m.id || m.name, displayName: m.displayName || m.name }))
206
  : [];
207
 
 
212
 
213
  // if `TASK_MODEL` is string & name of a model in `MODELS`, then we use `MODELS[TASK_MODEL]`, else we try to parse `TASK_MODEL` as a model config itself
214
 
215
+ export const smallModel = env.TASK_MODEL
216
+ ? (models.find((m) => m.name === env.TASK_MODEL) ||
217
+ (await processModel(modelConfig.parse(JSON5.parse(env.TASK_MODEL))).then((m) =>
218
  addEndpoint(m)
219
  ))) ??
220
  defaultModel
src/lib/server/summarize.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { LLM_SUMMERIZATION } from "$env/static/private";
2
  import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint";
3
  import type { Message } from "$lib/types/Message";
4
  import { logger } from "$lib/server/logger";
5
 
6
  export async function summarize(prompt: string) {
7
- if (!LLM_SUMMERIZATION) {
8
  return prompt.split(/\s+/g).slice(0, 5).join(" ");
9
  }
10
 
 
1
+ import { env } from "$env/dynamic/private";
2
  import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint";
3
  import type { Message } from "$lib/types/Message";
4
  import { logger } from "$lib/server/logger";
5
 
6
  export async function summarize(prompt: string) {
7
+ if (!env.LLM_SUMMERIZATION) {
8
  return prompt.split(/\s+/g).slice(0, 5).join(" ");
9
  }
10
 
src/lib/server/usageLimits.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { z } from "zod";
2
- import { USAGE_LIMITS, RATE_LIMIT } from "$env/static/private";
3
  import JSON5 from "json5";
4
 
5
  // RATE_LIMIT is the legacy way to define messages per minute limit
@@ -12,7 +12,7 @@ export const usageLimitsSchema = z
12
  messagesPerMinute: z
13
  .preprocess((val) => {
14
  if (val === undefined) {
15
- return RATE_LIMIT;
16
  }
17
  return val;
18
  }, z.coerce.number().optional())
@@ -20,4 +20,4 @@ export const usageLimitsSchema = z
20
  })
21
  .optional();
22
 
23
- export const usageLimits = usageLimitsSchema.parse(JSON5.parse(USAGE_LIMITS));
 
1
  import { z } from "zod";
2
+ import { env } from "$env/dynamic/private";
3
  import JSON5 from "json5";
4
 
5
  // RATE_LIMIT is the legacy way to define messages per minute limit
 
12
  messagesPerMinute: z
13
  .preprocess((val) => {
14
  if (val === undefined) {
15
+ return env.RATE_LIMIT;
16
  }
17
  return val;
18
  }, z.coerce.number().optional())
 
20
  })
21
  .optional();
22
 
23
+ export const usageLimits = usageLimitsSchema.parse(JSON5.parse(env.USAGE_LIMITS));
src/lib/server/websearch/runWebSearch.ts CHANGED
@@ -5,7 +5,7 @@ import { chunk } from "$lib/utils/chunk";
5
  import { findSimilarSentences } from "$lib/server/sentenceSimilarity";
6
  import { getWebSearchProvider } from "./searchWeb";
7
  import { defaultEmbeddingModel, embeddingModels } from "$lib/server/embeddingModels";
8
- import { WEBSEARCH_ALLOWLIST, WEBSEARCH_BLOCKLIST, ENABLE_LOCAL_FETCH } from "$env/static/private";
9
 
10
  import type { Conversation } from "$lib/types/Conversation";
11
  import type { MessageUpdate } from "$lib/types/MessageUpdate";
@@ -22,8 +22,8 @@ const MAX_N_PAGES_EMBED = 5 as const;
22
 
23
  const listSchema = z.array(z.string()).default([]);
24
 
25
- const allowList = listSchema.parse(JSON5.parse(WEBSEARCH_ALLOWLIST));
26
- const blockList = listSchema.parse(JSON5.parse(WEBSEARCH_BLOCKLIST));
27
 
28
  export async function runWebSearch(
29
  conv: Conversation,
@@ -52,7 +52,7 @@ export async function runWebSearch(
52
 
53
  let linksToUse = [...ragSettings.allowedLinks];
54
 
55
- if (ENABLE_LOCAL_FETCH !== "true") {
56
  const localLinks = await Promise.all(
57
  linksToUse.map(async (link) => {
58
  try {
 
5
  import { findSimilarSentences } from "$lib/server/sentenceSimilarity";
6
  import { getWebSearchProvider } from "./searchWeb";
7
  import { defaultEmbeddingModel, embeddingModels } from "$lib/server/embeddingModels";
8
+ import { env } from "$env/dynamic/private";
9
 
10
  import type { Conversation } from "$lib/types/Conversation";
11
  import type { MessageUpdate } from "$lib/types/MessageUpdate";
 
22
 
23
  const listSchema = z.array(z.string()).default([]);
24
 
25
+ const allowList = listSchema.parse(JSON5.parse(env.WEBSEARCH_ALLOWLIST));
26
+ const blockList = listSchema.parse(JSON5.parse(env.WEBSEARCH_BLOCKLIST));
27
 
28
  export async function runWebSearch(
29
  conv: Conversation,
 
52
 
53
  let linksToUse = [...ragSettings.allowedLinks];
54
 
55
+ if (env.ENABLE_LOCAL_FETCH !== "true") {
56
  const localLinks = await Promise.all(
57
  linksToUse.map(async (link) => {
58
  try {
src/lib/server/websearch/searchSearxng.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SEARXNG_QUERY_URL } from "$env/static/private";
2
  import { logger } from "$lib/server/logger";
3
 
4
  export async function searchSearxng(query: string) {
@@ -6,7 +6,7 @@ export async function searchSearxng(query: string) {
6
  setTimeout(() => abortController.abort(), 10000);
7
 
8
  // Insert the query into the URL template
9
- let url = SEARXNG_QUERY_URL.replace("<query>", query);
10
 
11
  // Check if "&format=json" already exists in the URL
12
  if (!url.includes("&format=json")) {
 
1
+ import { env } from "$env/dynamic/private";
2
  import { logger } from "$lib/server/logger";
3
 
4
  export async function searchSearxng(query: string) {
 
6
  setTimeout(() => abortController.abort(), 10000);
7
 
8
  // Insert the query into the URL template
9
+ let url = env.SEARXNG_QUERY_URL.replace("<query>", query);
10
 
11
  // Check if "&format=json" already exists in the URL
12
  if (!url.includes("&format=json")) {
src/lib/server/websearch/searchWeb.ts CHANGED
@@ -1,13 +1,6 @@
1
  import type { YouWebSearch } from "../../types/WebSearch";
2
  import { WebSearchProvider } from "../../types/WebSearch";
3
- import {
4
- SERPAPI_KEY,
5
- SERPER_API_KEY,
6
- SERPSTACK_API_KEY,
7
- USE_LOCAL_WEBSEARCH,
8
- SEARXNG_QUERY_URL,
9
- YDC_API_KEY,
10
- } from "$env/static/private";
11
  import { getJson } from "serpapi";
12
  import type { GoogleParameters } from "serpapi";
13
  import { searchWebLocal } from "./searchWebLocal";
@@ -15,9 +8,9 @@ import { searchSearxng } from "./searchSearxng";
15
 
16
  // get which SERP api is providing web results
17
  export function getWebSearchProvider() {
18
- if (YDC_API_KEY) {
19
  return WebSearchProvider.YOU;
20
- } else if (SEARXNG_QUERY_URL) {
21
  return WebSearchProvider.SEARXNG;
22
  } else {
23
  return WebSearchProvider.GOOGLE;
@@ -26,22 +19,22 @@ export function getWebSearchProvider() {
26
 
27
  // Show result as JSON
28
  export async function searchWeb(query: string) {
29
- if (USE_LOCAL_WEBSEARCH) {
30
  return await searchWebLocal(query);
31
  }
32
- if (SEARXNG_QUERY_URL) {
33
  return await searchSearxng(query);
34
  }
35
- if (SERPER_API_KEY) {
36
  return await searchWebSerper(query);
37
  }
38
- if (YDC_API_KEY) {
39
  return await searchWebYouApi(query);
40
  }
41
- if (SERPAPI_KEY) {
42
  return await searchWebSerpApi(query);
43
  }
44
- if (SERPSTACK_API_KEY) {
45
  return await searchSerpStack(query);
46
  }
47
  throw new Error("No You.com or Serper.dev or SerpAPI key found");
@@ -58,7 +51,7 @@ export async function searchWebSerper(query: string) {
58
  method: "POST",
59
  body: JSON.stringify(params),
60
  headers: {
61
- "x-api-key": SERPER_API_KEY,
62
  "Content-type": "application/json; charset=UTF-8",
63
  },
64
  });
@@ -84,7 +77,7 @@ export async function searchWebSerpApi(query: string) {
84
  hl: "en",
85
  gl: "us",
86
  google_domain: "google.com",
87
- api_key: SERPAPI_KEY,
88
  } satisfies GoogleParameters;
89
 
90
  // Show result as JSON
@@ -97,7 +90,7 @@ export async function searchWebYouApi(query: string) {
97
  const response = await fetch(`https://api.ydc-index.io/search?query=${query}`, {
98
  method: "GET",
99
  headers: {
100
- "X-API-Key": YDC_API_KEY,
101
  "Content-type": "application/json; charset=UTF-8",
102
  },
103
  });
@@ -123,7 +116,7 @@ export async function searchWebYouApi(query: string) {
123
 
124
  export async function searchSerpStack(query: string) {
125
  const response = await fetch(
126
- `http://api.serpstack.com/search?access_key=${SERPSTACK_API_KEY}&query=${query}&hl=en&gl=us`,
127
  {
128
  method: "GET",
129
  headers: {
 
1
  import type { YouWebSearch } from "../../types/WebSearch";
2
  import { WebSearchProvider } from "../../types/WebSearch";
3
+ import { env } from "$env/dynamic/private";
 
 
 
 
 
 
 
4
  import { getJson } from "serpapi";
5
  import type { GoogleParameters } from "serpapi";
6
  import { searchWebLocal } from "./searchWebLocal";
 
8
 
9
  // get which SERP api is providing web results
10
  export function getWebSearchProvider() {
11
+ if (env.YDC_API_KEY) {
12
  return WebSearchProvider.YOU;
13
+ } else if (env.SEARXNG_QUERY_URL) {
14
  return WebSearchProvider.SEARXNG;
15
  } else {
16
  return WebSearchProvider.GOOGLE;
 
19
 
20
  // Show result as JSON
21
  export async function searchWeb(query: string) {
22
+ if (env.USE_LOCAL_WEBSEARCH) {
23
  return await searchWebLocal(query);
24
  }
25
+ if (env.SEARXNG_QUERY_URL) {
26
  return await searchSearxng(query);
27
  }
28
+ if (env.SERPER_API_KEY) {
29
  return await searchWebSerper(query);
30
  }
31
+ if (env.YDC_API_KEY) {
32
  return await searchWebYouApi(query);
33
  }
34
+ if (env.SERPAPI_KEY) {
35
  return await searchWebSerpApi(query);
36
  }
37
+ if (env.SERPSTACK_API_KEY) {
38
  return await searchSerpStack(query);
39
  }
40
  throw new Error("No You.com or Serper.dev or SerpAPI key found");
 
51
  method: "POST",
52
  body: JSON.stringify(params),
53
  headers: {
54
+ "x-api-key": env.SERPER_API_KEY,
55
  "Content-type": "application/json; charset=UTF-8",
56
  },
57
  });
 
77
  hl: "en",
78
  gl: "us",
79
  google_domain: "google.com",
80
+ api_key: env.SERPAPI_KEY,
81
  } satisfies GoogleParameters;
82
 
83
  // Show result as JSON
 
90
  const response = await fetch(`https://api.ydc-index.io/search?query=${query}`, {
91
  method: "GET",
92
  headers: {
93
+ "X-API-Key": env.YDC_API_KEY,
94
  "Content-type": "application/json; charset=UTF-8",
95
  },
96
  });
 
116
 
117
  export async function searchSerpStack(query: string) {
118
  const response = await fetch(
119
+ `http://api.serpstack.com/search?access_key=${env.SERPSTACK_API_KEY}&query=${query}&hl=en&gl=us`,
120
  {
121
  method: "GET",
122
  headers: {
src/lib/utils/getShareUrl.ts CHANGED
@@ -1,6 +1,8 @@
1
  import { base } from "$app/paths";
2
- import { PUBLIC_ORIGIN, PUBLIC_SHARE_PREFIX } from "$env/static/public";
3
 
4
  export function getShareUrl(url: URL, shareId: string): string {
5
- return `${PUBLIC_SHARE_PREFIX || `${PUBLIC_ORIGIN || url.origin}${base}`}/r/${shareId}`;
 
 
6
  }
 
1
  import { base } from "$app/paths";
2
+ import { env as envPublic } from "$env/dynamic/public";
3
 
4
  export function getShareUrl(url: URL, shareId: string): string {
5
+ return `${
6
+ envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`
7
+ }/r/${shareId}`;
8
  }
src/lib/utils/isHuggingChat.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { PUBLIC_APP_ASSETS } from "$env/static/public";
2
 
3
- export const isHuggingChat = PUBLIC_APP_ASSETS === "huggingchat";
 
1
+ import { env as envPublic } from "$env/dynamic/public";
2
 
3
+ export const isHuggingChat = envPublic.PUBLIC_APP_ASSETS === "huggingchat";
src/routes/+layout.server.ts CHANGED
@@ -5,17 +5,7 @@ import { UrlDependency } from "$lib/types/UrlDependency";
5
  import { defaultModel, models, oldModels, validateModel } from "$lib/server/models";
6
  import { authCondition, requiresUser } from "$lib/server/auth";
7
  import { DEFAULT_SETTINGS } from "$lib/types/Settings";
8
- import {
9
- SERPAPI_KEY,
10
- SERPER_API_KEY,
11
- SERPSTACK_API_KEY,
12
- MESSAGES_BEFORE_LOGIN,
13
- YDC_API_KEY,
14
- USE_LOCAL_WEBSEARCH,
15
- SEARXNG_QUERY_URL,
16
- ENABLE_ASSISTANTS,
17
- ENABLE_ASSISTANTS_RAG,
18
- } from "$env/static/private";
19
  import { ObjectId } from "mongodb";
20
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
21
 
@@ -47,7 +37,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
47
  });
48
  }
49
 
50
- const enableAssistants = ENABLE_ASSISTANTS === "true";
51
 
52
  const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? "");
53
 
@@ -87,7 +77,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
87
 
88
  const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray();
89
 
90
- const messagesBeforeLogin = MESSAGES_BEFORE_LOGIN ? parseInt(MESSAGES_BEFORE_LOGIN) : 0;
91
 
92
  let loginRequired = false;
93
 
@@ -136,12 +126,12 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
136
  }) satisfies ConvSidebar[],
137
  settings: {
138
  searchEnabled: !!(
139
- SERPAPI_KEY ||
140
- SERPER_API_KEY ||
141
- SERPSTACK_API_KEY ||
142
- YDC_API_KEY ||
143
- USE_LOCAL_WEBSEARCH ||
144
- SEARXNG_QUERY_URL
145
  ),
146
  ethicsModalAccepted: !!settings?.ethicsModalAcceptedAt,
147
  ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,
@@ -188,7 +178,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
188
  },
189
  assistant,
190
  enableAssistants,
191
- enableAssistantsRAG: ENABLE_ASSISTANTS_RAG === "true",
192
  loginRequired,
193
  loginEnabled: requiresUser,
194
  guestMode: requiresUser && messagesBeforeLogin > 0,
 
5
  import { defaultModel, models, oldModels, validateModel } from "$lib/server/models";
6
  import { authCondition, requiresUser } from "$lib/server/auth";
7
  import { DEFAULT_SETTINGS } from "$lib/types/Settings";
8
+ import { env } from "$env/dynamic/private";
 
 
 
 
 
 
 
 
 
 
9
  import { ObjectId } from "mongodb";
10
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
11
 
 
37
  });
38
  }
39
 
40
+ const enableAssistants = env.ENABLE_ASSISTANTS === "true";
41
 
42
  const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? "");
43
 
 
77
 
78
  const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray();
79
 
80
+ const messagesBeforeLogin = env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0;
81
 
82
  let loginRequired = false;
83
 
 
126
  }) satisfies ConvSidebar[],
127
  settings: {
128
  searchEnabled: !!(
129
+ env.SERPAPI_KEY ||
130
+ env.SERPER_API_KEY ||
131
+ env.SERPSTACK_API_KEY ||
132
+ env.YDC_API_KEY ||
133
+ env.USE_LOCAL_WEBSEARCH ||
134
+ env.SEARXNG_QUERY_URL
135
  ),
136
  ethicsModalAccepted: !!settings?.ethicsModalAcceptedAt,
137
  ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,
 
178
  },
179
  assistant,
180
  enableAssistants,
181
+ enableAssistantsRAG: env.ENABLE_ASSISTANTS_RAG === "true",
182
  loginRequired,
183
  loginEnabled: requiresUser,
184
  guestMode: requiresUser && messagesBeforeLogin > 0,
src/routes/+layout.svelte CHANGED
@@ -7,13 +7,7 @@
7
  import { page } from "$app/stores";
8
  import { browser } from "$app/environment";
9
 
10
- import {
11
- PUBLIC_APPLE_APP_ID,
12
- PUBLIC_APP_DESCRIPTION,
13
- PUBLIC_ORIGIN,
14
- PUBLIC_PLAUSIBLE_SCRIPT_URL,
15
- } from "$env/static/public";
16
- import { PUBLIC_APP_ASSETS, PUBLIC_APP_NAME } from "$env/static/public";
17
 
18
  import { error } from "$lib/stores/errors";
19
  import { createSettingsStore } from "$lib/stores/settings";
@@ -134,7 +128,7 @@
134
  </script>
135
 
136
  <svelte:head>
137
- <title>{PUBLIC_APP_NAME}</title>
138
  <meta name="description" content="The first open source alternative to ChatGPT. 💪" />
139
  <meta name="twitter:card" content="summary_large_image" />
140
  <meta name="twitter:site" content="@huggingface" />
@@ -142,44 +136,49 @@
142
  <!-- use those meta tags everywhere except on the share assistant page -->
143
  <!-- feel free to refacto if there's a better way -->
144
  {#if !$page.url.pathname.includes("/assistant/") && $page.route.id !== "/assistants" && !$page.url.pathname.includes("/models/")}
145
- <meta property="og:title" content={PUBLIC_APP_NAME} />
146
  <meta property="og:type" content="website" />
147
- <meta property="og:url" content="{PUBLIC_ORIGIN || $page.url.origin}{base}" />
148
  <meta
149
  property="og:image"
150
- content="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/thumbnail.png"
 
151
  />
152
- <meta property="og:description" content={PUBLIC_APP_DESCRIPTION} />
153
  {/if}
154
  <link
155
  rel="icon"
156
- href="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/favicon.ico"
 
157
  sizes="32x32"
158
  />
159
  <link
160
  rel="icon"
161
- href="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/icon.svg"
 
162
  type="image/svg+xml"
163
  />
164
  <link
165
  rel="apple-touch-icon"
166
- href="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/apple-touch-icon.png"
 
167
  />
168
  <link
169
  rel="manifest"
170
- href="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/manifest.json"
 
171
  />
172
 
173
- {#if PUBLIC_PLAUSIBLE_SCRIPT_URL && PUBLIC_ORIGIN}
174
  <script
175
  defer
176
- data-domain={new URL(PUBLIC_ORIGIN).hostname}
177
- src={PUBLIC_PLAUSIBLE_SCRIPT_URL}
178
  ></script>
179
  {/if}
180
 
181
- {#if PUBLIC_APPLE_APP_ID}
182
- <meta name="apple-itunes-app" content={`app-id=${PUBLIC_APPLE_APP_ID}`} />
183
  {/if}
184
  </svelte:head>
185
 
 
7
  import { page } from "$app/stores";
8
  import { browser } from "$app/environment";
9
 
10
+ import { env as envPublic } from "$env/dynamic/public";
 
 
 
 
 
 
11
 
12
  import { error } from "$lib/stores/errors";
13
  import { createSettingsStore } from "$lib/stores/settings";
 
128
  </script>
129
 
130
  <svelte:head>
131
+ <title>{envPublic.PUBLIC_APP_NAME}</title>
132
  <meta name="description" content="The first open source alternative to ChatGPT. 💪" />
133
  <meta name="twitter:card" content="summary_large_image" />
134
  <meta name="twitter:site" content="@huggingface" />
 
136
  <!-- use those meta tags everywhere except on the share assistant page -->
137
  <!-- feel free to refacto if there's a better way -->
138
  {#if !$page.url.pathname.includes("/assistant/") && $page.route.id !== "/assistants" && !$page.url.pathname.includes("/models/")}
139
+ <meta property="og:title" content={envPublic.PUBLIC_APP_NAME} />
140
  <meta property="og:type" content="website" />
141
+ <meta property="og:url" content="{envPublic.PUBLIC_ORIGIN || $page.url.origin}{base}" />
142
  <meta
143
  property="og:image"
144
+ content="{envPublic.PUBLIC_ORIGIN ||
145
+ $page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/thumbnail.png"
146
  />
147
+ <meta property="og:description" content={envPublic.PUBLIC_APP_DESCRIPTION} />
148
  {/if}
149
  <link
150
  rel="icon"
151
+ href="{envPublic.PUBLIC_ORIGIN ||
152
+ $page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/favicon.ico"
153
  sizes="32x32"
154
  />
155
  <link
156
  rel="icon"
157
+ href="{envPublic.PUBLIC_ORIGIN ||
158
+ $page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/icon.svg"
159
  type="image/svg+xml"
160
  />
161
  <link
162
  rel="apple-touch-icon"
163
+ href="{envPublic.PUBLIC_ORIGIN ||
164
+ $page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/apple-touch-icon.png"
165
  />
166
  <link
167
  rel="manifest"
168
+ href="{envPublic.PUBLIC_ORIGIN ||
169
+ $page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/manifest.json"
170
  />
171
 
172
+ {#if envPublic.PUBLIC_PLAUSIBLE_SCRIPT_URL && envPublic.PUBLIC_ORIGIN}
173
  <script
174
  defer
175
+ data-domain={new URL(envPublic.PUBLIC_ORIGIN).hostname}
176
+ src={envPublic.PUBLIC_PLAUSIBLE_SCRIPT_URL}
177
  ></script>
178
  {/if}
179
 
180
+ {#if envPublic.PUBLIC_APPLE_APP_ID}
181
+ <meta name="apple-itunes-app" content={`app-id=${envPublic.PUBLIC_APPLE_APP_ID}`} />
182
  {/if}
183
  </svelte:head>
184