Compare commits
43 Commits
f570b17dfb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11d41be89e | ||
|
|
3a072a7165 | ||
|
|
0668660e85 | ||
|
|
17d8580387 | ||
|
|
11f3812dbb | ||
|
|
a00649584b | ||
|
|
c8af4e895a | ||
|
|
bea31fe8f2 | ||
|
|
85430fa3fb | ||
|
|
4aea10b195 | ||
|
|
7deedcb276 | ||
|
|
7973c44705 | ||
|
|
41e189655a | ||
|
|
8050104ad3 | ||
|
|
c8c248c4af | ||
|
|
4d5aa433e8 | ||
|
|
9384a42e39 | ||
|
|
66dfadef2e | ||
|
|
8943eeb0d7 | ||
|
|
0244f64df7 | ||
|
|
fff0d7af11 | ||
|
|
6471d2ffcf | ||
|
|
387a504801 | ||
|
|
de0e230632 | ||
|
|
71561724a5 | ||
|
|
a0627f6f2c | ||
|
|
11470cd086 | ||
|
|
ddb4e26e78 | ||
|
|
fe8a69d9b8 | ||
|
|
fbe961c358 | ||
|
|
359a4237c3 | ||
|
|
203d37f3c7 | ||
|
|
e55a7f1f37 | ||
|
|
f956148141 | ||
|
|
bba9c98c82 | ||
|
|
7ca503667f | ||
|
|
aeb40eb692 | ||
|
|
d180d2416b | ||
|
|
0767907fb7 | ||
|
|
bb8aa86115 | ||
|
|
3d61d80824 | ||
|
|
79baab1738 | ||
|
|
504a798c4b |
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
.git
|
||||||
60
.gitea/workflows/build-and-deploy.yml
Normal file
60
.gitea/workflows/build-and-deploy.yml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Build and deploy Backend
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: builder
|
||||||
|
env:
|
||||||
|
SERVICE_NAME: backend
|
||||||
|
IMAGE_SHA: gitea.dsrptlab.com/mapflow/backend:${{ github.sha }}
|
||||||
|
IMAGE_LATEST: gitea.dsrptlab.com/mapflow/backend:latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configure Gitea registry auth
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p ~/.docker
|
||||||
|
auth="$(printf '%s:%s' "${{ secrets.REGISTRY_USERNAME }}" "${{ secrets.REGISTRY_TOKEN }}" | base64 | tr -d '\n')"
|
||||||
|
printf '{"auths":{"gitea.dsrptlab.com":{"auth":"%s"}}}\n' "$auth" > ~/.docker/config.json
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if docker buildx build --push --provenance=false --tag "$IMAGE_SHA" --tag "$IMAGE_LATEST" .; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep "$((attempt * 10))"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Skip stale deployment
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
latest_sha="$(git ls-remote origin refs/heads/main | awk '{print $1}')"
|
||||||
|
if [ "$latest_sha" = "${GITHUB_SHA}" ]; then
|
||||||
|
touch .deploy-current
|
||||||
|
else
|
||||||
|
echo "A newer main commit exists: $latest_sha. Skipping deploy for ${GITHUB_SHA}."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Trigger Dokploy deploy webhook
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
[ -f .deploy-current ] || exit 0
|
||||||
|
payload=$(cat <<JSON
|
||||||
|
{"ref":"refs/heads/main","after":"$GITHUB_SHA","commits":[{"id":"$GITHUB_SHA","message":"$SERVICE_NAME #${GITHUB_RUN_NUMBER:-0} ${GITHUB_SHA:0:7}","modified":["Dockerfile"]}]}
|
||||||
|
JSON
|
||||||
|
)
|
||||||
|
response_file="$(mktemp)"
|
||||||
|
status_code="$(curl -sS -o "$response_file" -w "%{http_code}" -X POST "${{ secrets.DOKPLOY_DEPLOY_WEBHOOK }}" \
|
||||||
|
-H "x-gitea-event: push" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload")"
|
||||||
|
cat "$response_file"
|
||||||
|
[ "$status_code" = "200" ]
|
||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN DATABASE_URL="postgresql://mapflow:mapflow@localhost:5432/mapflow" npm run prisma:generate
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY --from=build /app/src/generated ./dist/generated
|
||||||
|
COPY --from=build /app/prisma ./prisma
|
||||||
|
COPY --from=build /app/prisma.config.ts ./prisma.config.ts
|
||||||
|
EXPOSE 4000
|
||||||
|
CMD ["node", "dist/entrypoint.js"]
|
||||||
91
package-lock.json
generated
91
package-lock.json
generated
@@ -16,12 +16,12 @@
|
|||||||
"fastify": "^5.8.5",
|
"fastify": "^5.8.5",
|
||||||
"graphql": "^16.13.2",
|
"graphql": "^16.13.2",
|
||||||
"mercurius": "^16.9.0",
|
"mercurius": "^16.9.0",
|
||||||
"pg": "^8.20.0"
|
"pg": "^8.20.0",
|
||||||
|
"prisma": "^7.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@types/pg": "^8.20.0",
|
"@types/pg": "^8.20.0",
|
||||||
"prisma": "^7.8.0",
|
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
@@ -198,14 +198,12 @@
|
|||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz",
|
||||||
"integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==",
|
"integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@electric-sql/pglite-socket": {
|
"node_modules/@electric-sql/pglite-socket": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz",
|
||||||
"integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==",
|
"integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"pglite-server": "dist/scripts/server.js"
|
"pglite-server": "dist/scripts/server.js"
|
||||||
@@ -218,7 +216,6 @@
|
|||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz",
|
||||||
"integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==",
|
"integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@electric-sql/pglite": "0.4.1"
|
"@electric-sql/pglite": "0.4.1"
|
||||||
@@ -964,7 +961,6 @@
|
|||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@lukeed/ms": {
|
"node_modules/@lukeed/ms": {
|
||||||
@@ -1537,7 +1533,6 @@
|
|||||||
"version": "7.8.0",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz",
|
||||||
"integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==",
|
"integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"c12": "3.3.4",
|
"c12": "3.3.4",
|
||||||
@@ -1556,7 +1551,6 @@
|
|||||||
"version": "0.24.3",
|
"version": "0.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz",
|
||||||
"integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==",
|
"integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electric-sql/pglite": "0.4.1",
|
"@electric-sql/pglite": "0.4.1",
|
||||||
@@ -1582,7 +1576,6 @@
|
|||||||
"version": "1.19.11",
|
"version": "1.19.11",
|
||||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
|
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
|
||||||
"integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
|
"integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.14.1"
|
"node": ">=18.14.1"
|
||||||
@@ -1604,7 +1597,6 @@
|
|||||||
"version": "7.8.0",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz",
|
||||||
"integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==",
|
"integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==",
|
||||||
"devOptional": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1618,14 +1610,12 @@
|
|||||||
"version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a",
|
"version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz",
|
||||||
"integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==",
|
"integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
|
||||||
"version": "7.8.0",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz",
|
||||||
"integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==",
|
"integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "7.8.0"
|
"@prisma/debug": "7.8.0"
|
||||||
@@ -1635,7 +1625,6 @@
|
|||||||
"version": "7.8.0",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz",
|
||||||
"integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==",
|
"integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "7.8.0",
|
"@prisma/debug": "7.8.0",
|
||||||
@@ -1647,7 +1636,6 @@
|
|||||||
"version": "7.8.0",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz",
|
||||||
"integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==",
|
"integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "7.8.0"
|
"@prisma/debug": "7.8.0"
|
||||||
@@ -1657,7 +1645,6 @@
|
|||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz",
|
||||||
"integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==",
|
"integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "7.2.0"
|
"@prisma/debug": "7.2.0"
|
||||||
@@ -1667,21 +1654,18 @@
|
|||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz",
|
||||||
"integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==",
|
"integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/query-plan-executor": {
|
"node_modules/@prisma/query-plan-executor": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz",
|
||||||
"integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==",
|
"integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/streams-local": {
|
"node_modules/@prisma/streams-local": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz",
|
||||||
"integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==",
|
"integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
@@ -1698,7 +1682,6 @@
|
|||||||
"version": "0.27.3",
|
"version": "0.27.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz",
|
||||||
"integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==",
|
"integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-toggle": "1.1.10",
|
"@radix-ui/react-toggle": "1.1.10",
|
||||||
@@ -1782,14 +1765,12 @@
|
|||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1805,7 +1786,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "1.2.3"
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
@@ -1829,7 +1809,6 @@
|
|||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.2"
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
@@ -1848,7 +1827,6 @@
|
|||||||
"version": "1.1.10",
|
"version": "1.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
|
||||||
"integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
|
"integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.3",
|
"@radix-ui/primitive": "1.1.3",
|
||||||
@@ -1874,7 +1852,6 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||||
@@ -1894,7 +1871,6 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||||
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
@@ -1913,7 +1889,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1929,7 +1904,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
@@ -1962,7 +1936,6 @@
|
|||||||
"version": "19.2.14",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2149,7 +2122,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||||
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6.0.0"
|
"node": ">= 6.0.0"
|
||||||
@@ -2199,7 +2171,6 @@
|
|||||||
"version": "2.9.2",
|
"version": "2.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz",
|
||||||
"integrity": "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==",
|
"integrity": "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/bintrees": {
|
"node_modules/bintrees": {
|
||||||
@@ -2284,7 +2255,6 @@
|
|||||||
"version": "3.3.4",
|
"version": "3.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz",
|
||||||
"integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==",
|
"integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
@@ -2342,7 +2312,6 @@
|
|||||||
"version": "4.5.1",
|
"version": "4.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kurkle/color": "^0.3.0"
|
"@kurkle/color": "^0.3.0"
|
||||||
@@ -2355,7 +2324,6 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readdirp": "^5.0.0"
|
"readdirp": "^5.0.0"
|
||||||
@@ -2431,7 +2399,6 @@
|
|||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||||
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
@@ -2499,7 +2466,6 @@
|
|||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-key": "^3.1.0",
|
"path-key": "^3.1.0",
|
||||||
@@ -2514,7 +2480,6 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
@@ -2540,7 +2505,6 @@
|
|||||||
"version": "7.1.5",
|
"version": "7.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||||
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
@@ -2550,7 +2514,6 @@
|
|||||||
"version": "6.1.7",
|
"version": "6.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||||
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
@@ -2566,7 +2529,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
@@ -2594,7 +2556,6 @@
|
|||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||||
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
@@ -2660,7 +2621,6 @@
|
|||||||
"version": "3.20.0",
|
"version": "3.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz",
|
||||||
"integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==",
|
"integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
@@ -2677,7 +2637,6 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
||||||
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@@ -2706,7 +2665,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
|
||||||
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
|
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
@@ -2935,14 +2893,12 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-check": {
|
"node_modules/fast-check": {
|
||||||
"version": "3.23.2",
|
"version": "3.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
||||||
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
|
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
|
||||||
"devOptional": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -3150,7 +3106,6 @@
|
|||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "^7.0.6",
|
"cross-spawn": "^7.0.6",
|
||||||
@@ -3290,7 +3245,6 @@
|
|||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
|
||||||
"integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==",
|
"integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/get-proto": {
|
"node_modules/get-proto": {
|
||||||
@@ -3323,7 +3277,6 @@
|
|||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz",
|
||||||
"integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==",
|
"integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"giget": "dist/cli.mjs"
|
"giget": "dist/cli.mjs"
|
||||||
@@ -3362,21 +3315,18 @@
|
|||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/grammex": {
|
"node_modules/grammex": {
|
||||||
"version": "3.1.12",
|
"version": "3.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz",
|
||||||
"integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==",
|
"integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/graphmatch": {
|
"node_modules/graphmatch": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz",
|
||||||
"integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==",
|
"integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/graphql": {
|
"node_modules/graphql": {
|
||||||
@@ -3487,7 +3437,6 @@
|
|||||||
"version": "4.12.16",
|
"version": "4.12.16",
|
||||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz",
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz",
|
||||||
"integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==",
|
"integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.9.0"
|
"node": ">=16.9.0"
|
||||||
@@ -3517,14 +3466,12 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
|
||||||
"integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
|
"integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.7.2",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
@@ -3633,14 +3580,12 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
@@ -3795,7 +3740,6 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
|
||||||
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
|
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.0.0",
|
"bun": ">=1.0.0",
|
||||||
@@ -3961,7 +3905,6 @@
|
|||||||
"version": "3.15.3",
|
"version": "3.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz",
|
||||||
"integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==",
|
"integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws-ssl-profiles": "^1.1.1",
|
"aws-ssl-profiles": "^1.1.1",
|
||||||
@@ -3982,7 +3925,6 @@
|
|||||||
"version": "1.1.6",
|
"version": "1.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
|
||||||
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
|
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lru.min": "^1.1.0"
|
"lru.min": "^1.1.0"
|
||||||
@@ -4047,7 +3989,6 @@
|
|||||||
"version": "2.0.11",
|
"version": "2.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
||||||
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/on-exit-leak-free": {
|
"node_modules/on-exit-leak-free": {
|
||||||
@@ -4132,7 +4073,6 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -4169,14 +4109,12 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/perfect-debounce": {
|
"node_modules/perfect-debounce": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
|
||||||
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
|
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pg": {
|
"node_modules/pg": {
|
||||||
@@ -4328,7 +4266,6 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
|
||||||
"integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
|
"integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"confbox": "^0.2.4",
|
"confbox": "^0.2.4",
|
||||||
@@ -4340,7 +4277,6 @@
|
|||||||
"version": "3.4.7",
|
"version": "3.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
|
||||||
"integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==",
|
"integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -4393,7 +4329,6 @@
|
|||||||
"version": "7.8.0",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz",
|
||||||
"integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==",
|
"integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==",
|
||||||
"devOptional": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4466,7 +4401,6 @@
|
|||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||||
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.4",
|
"graceful-fs": "^4.2.4",
|
||||||
@@ -4478,7 +4412,6 @@
|
|||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/protobufjs": {
|
"node_modules/protobufjs": {
|
||||||
@@ -4542,7 +4475,6 @@
|
|||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||||
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
||||||
"devOptional": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -4627,7 +4559,6 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz",
|
||||||
"integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==",
|
"integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"defu": "^6.1.6",
|
"defu": "^6.1.6",
|
||||||
@@ -4638,7 +4569,6 @@
|
|||||||
"version": "19.2.5",
|
"version": "19.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
||||||
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
|
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4649,7 +4579,6 @@
|
|||||||
"version": "19.2.5",
|
"version": "19.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
|
||||||
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
|
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4679,7 +4608,6 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 20.19.0"
|
"node": ">= 20.19.0"
|
||||||
@@ -4702,7 +4630,6 @@
|
|||||||
"version": "2.33.4",
|
"version": "2.33.4",
|
||||||
"resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz",
|
"resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz",
|
||||||
"integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==",
|
"integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/remeda"
|
"url": "https://github.com/sponsors/remeda"
|
||||||
@@ -4763,7 +4690,6 @@
|
|||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||||
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
@@ -4857,14 +4783,12 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
@@ -4926,8 +4850,7 @@
|
|||||||
"node_modules/seq-queue": {
|
"node_modules/seq-queue": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
||||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==",
|
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/serve-static": {
|
"node_modules/serve-static": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
@@ -4965,7 +4888,6 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"shebang-regex": "^3.0.0"
|
"shebang-regex": "^3.0.0"
|
||||||
@@ -4978,7 +4900,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -5060,7 +4981,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@@ -5100,7 +5020,6 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||||
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
|
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@@ -5119,7 +5038,6 @@
|
|||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/stream-shift": {
|
"node_modules/stream-shift": {
|
||||||
@@ -5300,7 +5218,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
|
||||||
"integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
|
"integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=5"
|
"typescript": ">=5"
|
||||||
@@ -5325,7 +5242,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
@@ -5445,7 +5361,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz",
|
||||||
"integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==",
|
"integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grammex": "^3.1.11",
|
"grammex": "^3.1.11",
|
||||||
|
|||||||
@@ -26,12 +26,12 @@
|
|||||||
"fastify": "^5.8.5",
|
"fastify": "^5.8.5",
|
||||||
"graphql": "^16.13.2",
|
"graphql": "^16.13.2",
|
||||||
"mercurius": "^16.9.0",
|
"mercurius": "^16.9.0",
|
||||||
"pg": "^8.20.0"
|
"pg": "^8.20.0",
|
||||||
|
"prisma": "^7.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@types/pg": "^8.20.0",
|
"@types/pg": "^8.20.0",
|
||||||
"prisma": "^7.8.0",
|
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
|
|||||||
40
prisma/migrations/1_init_schema/migration.sql
Normal file
40
prisma/migrations/1_init_schema/migration.sql
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
-- CreateSchema
|
||||||
|
CREATE SCHEMA IF NOT EXISTS "public";
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "VoiceExperienceStatus" AS ENUM ('UPLOADED', 'TRANSCRIBING', 'TRANSCRIBED', 'ANALYZING', 'ANALYZED', 'FAILED');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Place" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"googlePlaceId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"latitude" DOUBLE PRECISION NOT NULL,
|
||||||
|
"longitude" DOUBLE PRECISION NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Place_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "VoiceExperience" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"placeId" TEXT NOT NULL,
|
||||||
|
"durationSeconds" INTEGER NOT NULL,
|
||||||
|
"audioObjectKey" TEXT NOT NULL,
|
||||||
|
"status" "VoiceExperienceStatus" NOT NULL DEFAULT 'UPLOADED',
|
||||||
|
"transcript" TEXT,
|
||||||
|
"analysis" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "VoiceExperience_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Place_googlePlaceId_key" ON "Place"("googlePlaceId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "VoiceExperience" ADD CONSTRAINT "VoiceExperience_placeId_fkey" FOREIGN KEY ("placeId") REFERENCES "Place"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
24
prisma/migrations/2_add_telegram_users/migration.sql
Normal file
24
prisma/migrations/2_add_telegram_users/migration.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "VoiceExperience" ADD COLUMN "userId" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"telegramId" TEXT NOT NULL,
|
||||||
|
"username" TEXT,
|
||||||
|
"firstName" TEXT,
|
||||||
|
"lastName" TEXT,
|
||||||
|
"photoUrl" TEXT,
|
||||||
|
"languageCode" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_telegramId_key" ON "User"("telegramId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "VoiceExperience" ADD CONSTRAINT "VoiceExperience_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
37
prisma/migrations/3_add_telegram_bot_sessions/migration.sql
Normal file
37
prisma/migrations/3_add_telegram_bot_sessions/migration.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserSession" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"tokenHash" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "UserSession_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "TelegramLoginRequest" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"tokenHash" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
||||||
|
"sessionToken" TEXT,
|
||||||
|
"userId" TEXT,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "TelegramLoginRequest_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UserSession_tokenHash_key" ON "UserSession"("tokenHash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "TelegramLoginRequest_tokenHash_key" ON "TelegramLoginRequest"("tokenHash");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserSession" ADD CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TelegramLoginRequest" ADD CONSTRAINT "TelegramLoginRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
4
prisma/migrations/4_add_google_place_types/migration.sql
Normal file
4
prisma/migrations/4_add_google_place_types/migration.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Place" ADD COLUMN "googlePrimaryType" TEXT,
|
||||||
|
ADD COLUMN "googleTypes" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TelegramLoginRequest" ADD COLUMN "telegramChatId" TEXT,
|
||||||
|
ADD COLUMN "telegramMessageId" INTEGER;
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "isAdmin" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "VoiceExperience" ADD COLUMN "audioAccessToken" TEXT,
|
||||||
|
ADD COLUMN "audioContentBase64" TEXT,
|
||||||
|
ADD COLUMN "audioMimeType" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "VoiceExperience_audioAccessToken_key" ON "VoiceExperience"("audioAccessToken");
|
||||||
@@ -22,17 +22,63 @@ model Place {
|
|||||||
name String
|
name String
|
||||||
latitude Float
|
latitude Float
|
||||||
longitude Float
|
longitude Float
|
||||||
|
googlePrimaryType String?
|
||||||
|
googleTypes String[] @default([])
|
||||||
experiences VoiceExperience[]
|
experiences VoiceExperience[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
telegramId String @unique
|
||||||
|
username String?
|
||||||
|
firstName String?
|
||||||
|
lastName String?
|
||||||
|
photoUrl String?
|
||||||
|
languageCode String?
|
||||||
|
isAdmin Boolean @default(false)
|
||||||
|
sessions UserSession[]
|
||||||
|
loginRequests TelegramLoginRequest[]
|
||||||
|
voiceExperiences VoiceExperience[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserSession {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
tokenHash String @unique
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model TelegramLoginRequest {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
tokenHash String @unique
|
||||||
|
status String @default("PENDING")
|
||||||
|
sessionToken String?
|
||||||
|
telegramChatId String?
|
||||||
|
telegramMessageId Int?
|
||||||
|
userId String?
|
||||||
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
model VoiceExperience {
|
model VoiceExperience {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
placeId String
|
placeId String
|
||||||
place Place @relation(fields: [placeId], references: [id])
|
place Place @relation(fields: [placeId], references: [id])
|
||||||
|
userId String?
|
||||||
|
user User? @relation(fields: [userId], references: [id])
|
||||||
durationSeconds Int
|
durationSeconds Int
|
||||||
audioObjectKey String
|
audioObjectKey String
|
||||||
|
audioContentBase64 String?
|
||||||
|
audioMimeType String?
|
||||||
|
audioAccessToken String? @unique
|
||||||
status VoiceExperienceStatus @default(UPLOADED)
|
status VoiceExperienceStatus @default(UPLOADED)
|
||||||
transcript String?
|
transcript String?
|
||||||
analysis Json?
|
analysis Json?
|
||||||
|
|||||||
377
src/auth/telegram-bot-login.ts
Normal file
377
src/auth/telegram-bot-login.ts
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
|
import { config } from '../config.js';
|
||||||
|
import { prisma } from '../prisma.js';
|
||||||
|
import {
|
||||||
|
sha256Hex,
|
||||||
|
upsertTelegramUser,
|
||||||
|
type TelegramUserPayload,
|
||||||
|
} from './telegram.js';
|
||||||
|
|
||||||
|
type TelegramUpdate = {
|
||||||
|
message?: {
|
||||||
|
chat: { id: number };
|
||||||
|
text?: string;
|
||||||
|
from?: TelegramBotUser;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type TelegramBotUser = {
|
||||||
|
id: number;
|
||||||
|
is_bot?: boolean;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
username?: string;
|
||||||
|
language_code?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TelegramProfilePhotos = {
|
||||||
|
photos: { file_id: string }[][];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TelegramFile = {
|
||||||
|
file_path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TelegramSentMessage = {
|
||||||
|
message_id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginPrefix = 'login_';
|
||||||
|
const loginExpirationHandles = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
function randomToken() {
|
||||||
|
return randomBytes(32).toString('base64url');
|
||||||
|
}
|
||||||
|
|
||||||
|
function expiresIn(seconds: number) {
|
||||||
|
return new Date(Date.now() + seconds * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function botApiUrl(method: string) {
|
||||||
|
if (!config.telegramMiniAppBotToken) {
|
||||||
|
throw new Error('TELEGRAM_MINI_APP_BOT_TOKEN is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `https://api.telegram.org/bot${config.telegramMiniAppBotToken}/${method}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callTelegram<T>(method: string, body: Record<string, unknown>) {
|
||||||
|
const response = await fetch(botApiUrl(method), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Telegram ${method} failed with ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = (await response.json()) as {
|
||||||
|
ok: boolean;
|
||||||
|
description?: string;
|
||||||
|
result?: T;
|
||||||
|
};
|
||||||
|
if (!payload.ok) {
|
||||||
|
throw new Error(payload.description ?? `Telegram ${method} failed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload.result as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function userPayload(from: TelegramBotUser): TelegramUserPayload {
|
||||||
|
return {
|
||||||
|
id: from.id,
|
||||||
|
username: from.username,
|
||||||
|
firstName: from.first_name,
|
||||||
|
lastName: from.last_name,
|
||||||
|
languageCode: from.language_code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function telegramUserPhotoUrl(userId: number) {
|
||||||
|
const photos = await callTelegram<TelegramProfilePhotos>('getUserProfilePhotos', {
|
||||||
|
user_id: userId,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
const variants = photos.photos[0];
|
||||||
|
const fileId = variants?.[variants.length - 1]?.file_id;
|
||||||
|
return fileId ? `${config.publicApiUrl}/telegram/photo/${fileId}` : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTelegramPhoto(fileId: string) {
|
||||||
|
const file = await callTelegram<TelegramFile>('getFile', { file_id: fileId });
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.telegram.org/file/bot${config.telegramMiniAppBotToken}/${file.file_path}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Telegram file fetch failed with ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentType: file.file_path.toLowerCase().endsWith('.png')
|
||||||
|
? 'image/png'
|
||||||
|
: 'image/jpeg',
|
||||||
|
bytes: Buffer.from(await response.arrayBuffer()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRemaining(expiresAt: Date) {
|
||||||
|
const seconds = Math.max(0, Math.ceil((expiresAt.getTime() - Date.now()) / 1000));
|
||||||
|
const minutes = Math.floor(seconds / 60).toString();
|
||||||
|
const rest = (seconds % 60).toString().padStart(2, '0');
|
||||||
|
return `${minutes}:${rest}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loginReplyMarkup(token: string, expiresAt: Date) {
|
||||||
|
return {
|
||||||
|
inline_keyboard: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: `${formatRemaining(expiresAt)} Войти`,
|
||||||
|
url: `${config.webAppUrl}?telegram_login=${encodeURIComponent(token)}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loginMessageText(expiresAt: Date) {
|
||||||
|
return 'Чтобы войти на сайт map.craftee.vn, воспользуйтесь ссылкой ниже.';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendLoginMessage(
|
||||||
|
chatId: number,
|
||||||
|
text: string,
|
||||||
|
token?: string,
|
||||||
|
expiresAt?: Date,
|
||||||
|
) {
|
||||||
|
const replyMarkup = token && expiresAt ? loginReplyMarkup(token, expiresAt) : undefined;
|
||||||
|
|
||||||
|
return callTelegram<TelegramSentMessage>('sendMessage', {
|
||||||
|
chat_id: chatId,
|
||||||
|
text,
|
||||||
|
reply_markup: replyMarkup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editLoginMessage(
|
||||||
|
chatId: string,
|
||||||
|
messageId: number,
|
||||||
|
text: string,
|
||||||
|
) {
|
||||||
|
await callTelegram('editMessageText', {
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: messageId,
|
||||||
|
text,
|
||||||
|
reply_markup: { inline_keyboard: [] },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleLoginExpiration(
|
||||||
|
requestId: string,
|
||||||
|
chatId: string,
|
||||||
|
messageId: number,
|
||||||
|
expiresAt: Date,
|
||||||
|
) {
|
||||||
|
const existingHandle = loginExpirationHandles.get(requestId);
|
||||||
|
if (existingHandle) {
|
||||||
|
clearTimeout(existingHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle = setTimeout(() => {
|
||||||
|
void (async () => {
|
||||||
|
const updated = await prisma.telegramLoginRequest.updateMany({
|
||||||
|
where: { id: requestId, status: 'CONFIRMED' },
|
||||||
|
data: { status: 'EXPIRED' },
|
||||||
|
});
|
||||||
|
|
||||||
|
loginExpirationHandles.delete(requestId);
|
||||||
|
if (updated.count > 0) {
|
||||||
|
await editLoginMessage(chatId, messageId, 'Ссылка входа устарела.');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, Math.max(0, expiresAt.getTime() - Date.now()));
|
||||||
|
loginExpirationHandles.set(requestId, handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelLoginExpiration(requestId: string) {
|
||||||
|
const handle = loginExpirationHandles.get(requestId);
|
||||||
|
if (handle) {
|
||||||
|
clearTimeout(handle);
|
||||||
|
loginExpirationHandles.delete(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expireLoginMessageNow(
|
||||||
|
requestId: string,
|
||||||
|
chatId: string,
|
||||||
|
messageId: number,
|
||||||
|
) {
|
||||||
|
cancelLoginExpiration(requestId);
|
||||||
|
const updated = await prisma.telegramLoginRequest.updateMany({
|
||||||
|
where: { id: requestId, status: 'CONFIRMED' },
|
||||||
|
data: { status: 'EXPIRED' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updated.count > 0) {
|
||||||
|
await editLoginMessage(chatId, messageId, 'Ссылка входа устарела.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshExpiredLoginMessage(
|
||||||
|
requestId: string,
|
||||||
|
chatId: string,
|
||||||
|
messageId: number,
|
||||||
|
) {
|
||||||
|
const request = await prisma.telegramLoginRequest.findUnique({
|
||||||
|
where: { id: requestId },
|
||||||
|
select: { status: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (request?.status === 'CONFIRMED') {
|
||||||
|
await expireLoginMessageNow(requestId, chatId, messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTelegramBotLogin() {
|
||||||
|
const token = randomToken();
|
||||||
|
const expiresAt = expiresIn(config.botLoginMaxAgeSeconds);
|
||||||
|
|
||||||
|
await prisma.telegramLoginRequest.create({
|
||||||
|
data: {
|
||||||
|
tokenHash: sha256Hex(token),
|
||||||
|
expiresAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
botUrl: `https://t.me/${config.telegramBotUsername}?start=${loginPrefix}${token}`,
|
||||||
|
expiresAt: expiresAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function completeTelegramBotLogin(token: string) {
|
||||||
|
const request = await prisma.telegramLoginRequest.findUnique({
|
||||||
|
where: { tokenHash: sha256Hex(token) },
|
||||||
|
include: { user: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
throw new Error('Telegram login token is invalid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.expiresAt <= new Date()) {
|
||||||
|
if (request.telegramChatId && request.telegramMessageId) {
|
||||||
|
await refreshExpiredLoginMessage(
|
||||||
|
request.id,
|
||||||
|
request.telegramChatId,
|
||||||
|
request.telegramMessageId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await prisma.telegramLoginRequest.update({
|
||||||
|
where: { id: request.id },
|
||||||
|
data: { status: 'EXPIRED' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new Error('Telegram login token is expired.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.status !== 'CONFIRMED' || !request.sessionToken || !request.user) {
|
||||||
|
throw new Error('Telegram login is not confirmed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelLoginExpiration(request.id);
|
||||||
|
await prisma.telegramLoginRequest.update({
|
||||||
|
where: { id: request.id },
|
||||||
|
data: { status: 'USED' },
|
||||||
|
});
|
||||||
|
if (request.telegramChatId && request.telegramMessageId) {
|
||||||
|
await editLoginMessage(
|
||||||
|
request.telegramChatId,
|
||||||
|
request.telegramMessageId,
|
||||||
|
'Вход выполнен.\nМожно вернуться в MapFlow.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionToken: request.sessionToken,
|
||||||
|
user: request.user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleTelegramBotWebhook(
|
||||||
|
update: TelegramUpdate,
|
||||||
|
secretToken?: string,
|
||||||
|
) {
|
||||||
|
if (config.telegramWebhookSecret && secretToken !== config.telegramWebhookSecret) {
|
||||||
|
throw new Error('Telegram webhook secret is invalid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = update.message;
|
||||||
|
const text = message?.text;
|
||||||
|
const from = message?.from;
|
||||||
|
const chatId = message?.chat.id;
|
||||||
|
if (!message || !text || !from || !chatId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [command, payload] = text.split(' ');
|
||||||
|
if (command !== '/start' || !payload?.startsWith(loginPrefix)) {
|
||||||
|
await sendLoginMessage(chatId, 'Начните вход с сайта MapFlow.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = payload.slice(loginPrefix.length);
|
||||||
|
const request = await prisma.telegramLoginRequest.findUnique({
|
||||||
|
where: { tokenHash: sha256Hex(token) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!request || request.status !== 'PENDING' || request.expiresAt <= new Date()) {
|
||||||
|
await sendLoginMessage(
|
||||||
|
chatId,
|
||||||
|
'Ссылка входа устарела.\nВернитесь на сайт и начните вход заново.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await upsertTelegramUser({
|
||||||
|
...userPayload(from),
|
||||||
|
photoUrl: await telegramUserPhotoUrl(from.id),
|
||||||
|
});
|
||||||
|
const sessionToken = randomToken();
|
||||||
|
await prisma.userSession.create({
|
||||||
|
data: {
|
||||||
|
tokenHash: sha256Hex(sessionToken),
|
||||||
|
userId: user.id,
|
||||||
|
expiresAt: expiresIn(config.sessionMaxAgeSeconds),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sentMessage = await sendLoginMessage(
|
||||||
|
chatId,
|
||||||
|
loginMessageText(request.expiresAt),
|
||||||
|
token,
|
||||||
|
request.expiresAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
await prisma.telegramLoginRequest.update({
|
||||||
|
where: { id: request.id },
|
||||||
|
data: {
|
||||||
|
status: 'CONFIRMED',
|
||||||
|
sessionToken,
|
||||||
|
telegramChatId: chatId.toString(),
|
||||||
|
telegramMessageId: sentMessage.message_id,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
scheduleLoginExpiration(
|
||||||
|
request.id,
|
||||||
|
chatId.toString(),
|
||||||
|
sentMessage.message_id,
|
||||||
|
request.expiresAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
207
src/auth/telegram.ts
Normal file
207
src/auth/telegram.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
||||||
|
|
||||||
|
import { config } from '../config.js';
|
||||||
|
import { prisma } from '../prisma.js';
|
||||||
|
|
||||||
|
type TelegramInitDataUser = {
|
||||||
|
id: number;
|
||||||
|
username?: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
photo_url?: string;
|
||||||
|
language_code?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TelegramLoginData = {
|
||||||
|
id: number;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
username?: string;
|
||||||
|
photo_url?: string;
|
||||||
|
auth_date: number;
|
||||||
|
hash: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TelegramUserPayload = {
|
||||||
|
id: number;
|
||||||
|
username?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
photoUrl?: string;
|
||||||
|
languageCode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TelegramAuthData = {
|
||||||
|
telegramInitData?: string;
|
||||||
|
telegramLoginData?: string;
|
||||||
|
mapflowSessionToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function hmacSha256(key: string | Buffer, value: string) {
|
||||||
|
return createHmac('sha256', key).update(value).digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sha256(value: string) {
|
||||||
|
return createHash('sha256').update(value).digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sha256Hex(value: string) {
|
||||||
|
return createHash('sha256').update(value).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertValidHash(receivedHash: string, expectedHash: Buffer, dataType: string) {
|
||||||
|
const received = Buffer.from(receivedHash, 'hex');
|
||||||
|
if (received.length !== expectedHash.length) {
|
||||||
|
throw new Error(`Telegram ${dataType} hash is invalid.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timingSafeEqual(received, expectedHash)) {
|
||||||
|
throw new Error(`Telegram ${dataType} hash is invalid.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertConfiguredBotToken() {
|
||||||
|
if (!config.telegramMiniAppBotToken) {
|
||||||
|
throw new Error('TELEGRAM_MINI_APP_BOT_TOKEN is required.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertFreshAuthDate(authDate: number, dataType: string) {
|
||||||
|
if (!Number.isFinite(authDate)) {
|
||||||
|
throw new Error(`Telegram ${dataType} auth_date is required.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ageSeconds = Math.floor(Date.now() / 1000) - authDate;
|
||||||
|
if (ageSeconds > config.telegramAuthMaxAgeSeconds) {
|
||||||
|
throw new Error(`Telegram ${dataType} is expired.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTelegramInitData(initData: string): TelegramUserPayload {
|
||||||
|
assertConfiguredBotToken();
|
||||||
|
const params = new URLSearchParams(initData);
|
||||||
|
const receivedHash = params.get('hash');
|
||||||
|
if (!receivedHash) {
|
||||||
|
throw new Error('Telegram init data hash is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFreshAuthDate(Number(params.get('auth_date')), 'init data');
|
||||||
|
|
||||||
|
const dataCheckString = [...params.entries()]
|
||||||
|
.filter(([key]) => key !== 'hash')
|
||||||
|
.sort(([left], [right]) => left.localeCompare(right))
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const secretKey = hmacSha256('WebAppData', config.telegramMiniAppBotToken);
|
||||||
|
const expectedHash = hmacSha256(secretKey, dataCheckString);
|
||||||
|
assertValidHash(receivedHash, expectedHash, 'init data');
|
||||||
|
|
||||||
|
const rawUser = params.get('user');
|
||||||
|
if (!rawUser) {
|
||||||
|
throw new Error('Telegram user is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = JSON.parse(rawUser) as TelegramInitDataUser;
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
firstName: user.first_name,
|
||||||
|
lastName: user.last_name,
|
||||||
|
photoUrl: user.photo_url,
|
||||||
|
languageCode: user.language_code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTelegramLoginData(loginData: TelegramLoginData): TelegramUserPayload {
|
||||||
|
assertConfiguredBotToken();
|
||||||
|
assertFreshAuthDate(Number(loginData.auth_date), 'login data');
|
||||||
|
|
||||||
|
const dataCheckString = Object.entries(loginData)
|
||||||
|
.filter(([key, value]) => key !== 'hash' && value !== undefined && value !== null)
|
||||||
|
.sort(([left], [right]) => left.localeCompare(right))
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const expectedHash = hmacSha256(sha256(config.telegramMiniAppBotToken), dataCheckString);
|
||||||
|
assertValidHash(loginData.hash, expectedHash, 'login data');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: Number(loginData.id),
|
||||||
|
username: loginData.username,
|
||||||
|
firstName: loginData.first_name,
|
||||||
|
lastName: loginData.last_name,
|
||||||
|
photoUrl: loginData.photo_url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTelegramLoginJson(loginData: string) {
|
||||||
|
return parseTelegramLoginData(JSON.parse(loginData) as TelegramLoginData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertTelegramUser(user: TelegramUserPayload) {
|
||||||
|
return prisma.user.upsert({
|
||||||
|
where: { telegramId: String(user.id) },
|
||||||
|
create: {
|
||||||
|
telegramId: String(user.id),
|
||||||
|
username: user.username,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
|
languageCode: user.languageCode,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
username: user.username,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
|
languageCode: user.languageCode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrCreateTelegramUser(initData: string) {
|
||||||
|
return upsertTelegramUser(parseTelegramInitData(initData));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrCreateTelegramLoginUser(loginData: TelegramLoginData) {
|
||||||
|
return upsertTelegramUser(parseTelegramLoginData(loginData));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserBySessionToken(sessionToken: string) {
|
||||||
|
const session = await prisma.userSession.findUnique({
|
||||||
|
where: { tokenHash: sha256Hex(sessionToken) },
|
||||||
|
include: { user: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session || session.expiresAt <= new Date()) {
|
||||||
|
throw new Error('Telegram authorization is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requireTelegramUser(authData: TelegramAuthData) {
|
||||||
|
if (authData.mapflowSessionToken) {
|
||||||
|
return getUserBySessionToken(authData.mapflowSessionToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.telegramInitData) {
|
||||||
|
return upsertTelegramUser(parseTelegramInitData(authData.telegramInitData));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.telegramLoginData) {
|
||||||
|
return upsertTelegramUser(parseTelegramLoginJson(authData.telegramLoginData));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Telegram authorization is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requireAdminTelegramUser(authData: TelegramAuthData) {
|
||||||
|
const user = await requireTelegramUser(authData);
|
||||||
|
if (!user.isAdmin) {
|
||||||
|
throw new Error('MapFlow admin access is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
@@ -1,8 +1,23 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
import { loadVaultEnvironment } from './vault/env.js';
|
||||||
|
|
||||||
|
await loadVaultEnvironment();
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
host: process.env.HOST ?? '0.0.0.0',
|
host: process.env.HOST ?? '0.0.0.0',
|
||||||
port: Number(process.env.PORT ?? '4000'),
|
port: Number(process.env.PORT ?? '4000'),
|
||||||
databaseUrl: process.env.DATABASE_URL ?? '',
|
databaseUrl: process.env.DATABASE_URL ?? '',
|
||||||
hatchetToken: process.env.HATCHET_CLIENT_TOKEN ?? '',
|
hatchetToken: process.env.HATCHET_CLIENT_TOKEN ?? '',
|
||||||
|
telegramMiniAppBotToken: process.env.TELEGRAM_MINI_APP_BOT_TOKEN ?? '',
|
||||||
|
telegramBotUsername: process.env.TELEGRAM_BOT_USERNAME ?? 'carfteebot',
|
||||||
|
telegramWebhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET ?? '',
|
||||||
|
googlePlacesApiKey: process.env.GOOGLE_PLACES_API_KEY ?? '',
|
||||||
|
webAppUrl: process.env.WEB_APP_URL ?? 'https://map.craftee.vn',
|
||||||
|
publicApiUrl: process.env.PUBLIC_API_URL ?? 'https://api.map.craftee.vn',
|
||||||
|
telegramAuthMaxAgeSeconds: Number(
|
||||||
|
process.env.TELEGRAM_AUTH_MAX_AGE_SECONDS ?? '86400',
|
||||||
|
),
|
||||||
|
sessionMaxAgeSeconds: Number(process.env.SESSION_MAX_AGE_SECONDS ?? '2592000'),
|
||||||
|
botLoginMaxAgeSeconds: Number(process.env.BOT_LOGIN_MAX_AGE_SECONDS ?? '300'),
|
||||||
};
|
};
|
||||||
|
|||||||
26
src/entrypoint.ts
Normal file
26
src/entrypoint.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
|
||||||
|
import { loadVaultEnvironment } from './vault/env.js';
|
||||||
|
|
||||||
|
function run(command: string, args: string[]): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
env: process.env,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', reject);
|
||||||
|
child.on('exit', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new Error(`${command} ${args.join(' ')} exited with ${code}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadVaultEnvironment();
|
||||||
|
await run('npm', ['run', 'prisma:migrate:deploy']);
|
||||||
|
await import('./server.js');
|
||||||
File diff suppressed because one or more lines are too long
@@ -126,6 +126,42 @@ exports.Prisma.PlaceScalarFieldEnum = {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
latitude: 'latitude',
|
latitude: 'latitude',
|
||||||
longitude: 'longitude',
|
longitude: 'longitude',
|
||||||
|
googlePrimaryType: 'googlePrimaryType',
|
||||||
|
googleTypes: 'googleTypes',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.UserScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
telegramId: 'telegramId',
|
||||||
|
username: 'username',
|
||||||
|
firstName: 'firstName',
|
||||||
|
lastName: 'lastName',
|
||||||
|
photoUrl: 'photoUrl',
|
||||||
|
languageCode: 'languageCode',
|
||||||
|
isAdmin: 'isAdmin',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.UserSessionScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
tokenHash: 'tokenHash',
|
||||||
|
userId: 'userId',
|
||||||
|
expiresAt: 'expiresAt',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.TelegramLoginRequestScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
tokenHash: 'tokenHash',
|
||||||
|
status: 'status',
|
||||||
|
sessionToken: 'sessionToken',
|
||||||
|
telegramChatId: 'telegramChatId',
|
||||||
|
telegramMessageId: 'telegramMessageId',
|
||||||
|
userId: 'userId',
|
||||||
|
expiresAt: 'expiresAt',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
@@ -133,8 +169,12 @@ exports.Prisma.PlaceScalarFieldEnum = {
|
|||||||
exports.Prisma.VoiceExperienceScalarFieldEnum = {
|
exports.Prisma.VoiceExperienceScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
placeId: 'placeId',
|
placeId: 'placeId',
|
||||||
|
userId: 'userId',
|
||||||
durationSeconds: 'durationSeconds',
|
durationSeconds: 'durationSeconds',
|
||||||
audioObjectKey: 'audioObjectKey',
|
audioObjectKey: 'audioObjectKey',
|
||||||
|
audioContentBase64: 'audioContentBase64',
|
||||||
|
audioMimeType: 'audioMimeType',
|
||||||
|
audioAccessToken: 'audioAccessToken',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
transcript: 'transcript',
|
transcript: 'transcript',
|
||||||
analysis: 'analysis',
|
analysis: 'analysis',
|
||||||
@@ -157,16 +197,16 @@ exports.Prisma.QueryMode = {
|
|||||||
insensitive: 'insensitive'
|
insensitive: 'insensitive'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.Prisma.NullsOrder = {
|
||||||
|
first: 'first',
|
||||||
|
last: 'last'
|
||||||
|
};
|
||||||
|
|
||||||
exports.Prisma.JsonNullValueFilter = {
|
exports.Prisma.JsonNullValueFilter = {
|
||||||
DbNull: Prisma.DbNull,
|
DbNull: Prisma.DbNull,
|
||||||
JsonNull: Prisma.JsonNull,
|
JsonNull: Prisma.JsonNull,
|
||||||
AnyNull: Prisma.AnyNull
|
AnyNull: Prisma.AnyNull
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.Prisma.NullsOrder = {
|
|
||||||
first: 'first',
|
|
||||||
last: 'last'
|
|
||||||
};
|
|
||||||
exports.VoiceExperienceStatus = exports.$Enums.VoiceExperienceStatus = {
|
exports.VoiceExperienceStatus = exports.$Enums.VoiceExperienceStatus = {
|
||||||
UPLOADED: 'UPLOADED',
|
UPLOADED: 'UPLOADED',
|
||||||
TRANSCRIBING: 'TRANSCRIBING',
|
TRANSCRIBING: 'TRANSCRIBING',
|
||||||
@@ -178,6 +218,9 @@ exports.VoiceExperienceStatus = exports.$Enums.VoiceExperienceStatus = {
|
|||||||
|
|
||||||
exports.Prisma.ModelName = {
|
exports.Prisma.ModelName = {
|
||||||
Place: 'Place',
|
Place: 'Place',
|
||||||
|
User: 'User',
|
||||||
|
UserSession: 'UserSession',
|
||||||
|
TelegramLoginRequest: 'TelegramLoginRequest',
|
||||||
VoiceExperience: 'VoiceExperience'
|
VoiceExperience: 'VoiceExperience'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
5704
src/generated/prisma/index.d.ts
vendored
5704
src/generated/prisma/index.d.ts
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-5316d8e66293a4954be6e26169692366997bf25fe9186e98c87e62eed3dc691e",
|
"name": "prisma-client-af7d1a24a80c81a94e157e22f4f12e91cca6099374f7a2346452663b0168688a",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "default.js",
|
"browser": "default.js",
|
||||||
|
|||||||
@@ -22,17 +22,63 @@ model Place {
|
|||||||
name String
|
name String
|
||||||
latitude Float
|
latitude Float
|
||||||
longitude Float
|
longitude Float
|
||||||
|
googlePrimaryType String?
|
||||||
|
googleTypes String[] @default([])
|
||||||
experiences VoiceExperience[]
|
experiences VoiceExperience[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
telegramId String @unique
|
||||||
|
username String?
|
||||||
|
firstName String?
|
||||||
|
lastName String?
|
||||||
|
photoUrl String?
|
||||||
|
languageCode String?
|
||||||
|
isAdmin Boolean @default(false)
|
||||||
|
sessions UserSession[]
|
||||||
|
loginRequests TelegramLoginRequest[]
|
||||||
|
voiceExperiences VoiceExperience[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserSession {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
tokenHash String @unique
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model TelegramLoginRequest {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
tokenHash String @unique
|
||||||
|
status String @default("PENDING")
|
||||||
|
sessionToken String?
|
||||||
|
telegramChatId String?
|
||||||
|
telegramMessageId Int?
|
||||||
|
userId String?
|
||||||
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
model VoiceExperience {
|
model VoiceExperience {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
placeId String
|
placeId String
|
||||||
place Place @relation(fields: [placeId], references: [id])
|
place Place @relation(fields: [placeId], references: [id])
|
||||||
|
userId String?
|
||||||
|
user User? @relation(fields: [userId], references: [id])
|
||||||
durationSeconds Int
|
durationSeconds Int
|
||||||
audioObjectKey String
|
audioObjectKey String
|
||||||
|
audioContentBase64 String?
|
||||||
|
audioMimeType String?
|
||||||
|
audioAccessToken String? @unique
|
||||||
status VoiceExperienceStatus @default(UPLOADED)
|
status VoiceExperienceStatus @default(UPLOADED)
|
||||||
transcript String?
|
transcript String?
|
||||||
analysis Json?
|
analysis Json?
|
||||||
|
|||||||
234
src/graphql/places.ts
Normal file
234
src/graphql/places.ts
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import { prisma } from '../prisma.js';
|
||||||
|
import { config } from '../config.js';
|
||||||
|
import type { Prisma } from '../generated/prisma/client.js';
|
||||||
|
|
||||||
|
const googleNearbySearchUrl =
|
||||||
|
'https://places.googleapis.com/v1/places:searchNearby';
|
||||||
|
const maxNearbyRadiusMeters = 500;
|
||||||
|
const nearbyPlaceTypes = [
|
||||||
|
'restaurant',
|
||||||
|
'cafe',
|
||||||
|
'bar',
|
||||||
|
'bakery',
|
||||||
|
'meal_takeaway',
|
||||||
|
'meal_delivery',
|
||||||
|
];
|
||||||
|
|
||||||
|
type NearbyPlacesInput = {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
radiusMeters: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GoogleNearbyPlace = {
|
||||||
|
id?: string;
|
||||||
|
displayName?: {
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
primaryType?: string;
|
||||||
|
types?: string[];
|
||||||
|
location?: {
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type GoogleNearbyResponse = {
|
||||||
|
places?: GoogleNearbyPlace[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type PersistableGooglePlace = {
|
||||||
|
googlePlaceId: string;
|
||||||
|
name: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
googlePrimaryType: string | null;
|
||||||
|
googleTypes: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlaceWithRecentExperiences = Prisma.PlaceGetPayload<{
|
||||||
|
include: {
|
||||||
|
experiences: {
|
||||||
|
include: {
|
||||||
|
user: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function serializeVoiceExperience(
|
||||||
|
experience: Awaited<ReturnType<typeof prisma.voiceExperience.findMany>>[number],
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...experience,
|
||||||
|
createdAt: experience.createdAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializePlace(place: PlaceWithRecentExperiences) {
|
||||||
|
return {
|
||||||
|
...place,
|
||||||
|
experiences: place.experiences.map(serializeVoiceExperience),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listPlaces() {
|
||||||
|
const places = await prisma.place.findMany({
|
||||||
|
where: {
|
||||||
|
experiences: { some: {} },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
experiences: {
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 10,
|
||||||
|
include: { user: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { updatedAt: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return places.map(serializePlace);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listNearbyPlaces(input: NearbyPlacesInput) {
|
||||||
|
if (!Number.isFinite(input.latitude) || !Number.isFinite(input.longitude)) {
|
||||||
|
throw new Error('Nearby place search requires a valid coordinate.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!Number.isInteger(input.radiusMeters) ||
|
||||||
|
input.radiusMeters <= 0 ||
|
||||||
|
input.radiusMeters > maxNearbyRadiusMeters
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Nearby place radius must be from 1 to ${maxNearbyRadiusMeters} meters.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (config.googlePlacesApiKey === '') {
|
||||||
|
throw new Error('GOOGLE_PLACES_API_KEY is required for nearby place search.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(googleNearbySearchUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Goog-Api-Key': config.googlePlacesApiKey,
|
||||||
|
'X-Goog-FieldMask': [
|
||||||
|
'places.id',
|
||||||
|
'places.displayName',
|
||||||
|
'places.location',
|
||||||
|
'places.primaryType',
|
||||||
|
'places.types',
|
||||||
|
].join(','),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
includedTypes: nearbyPlaceTypes,
|
||||||
|
maxResultCount: 20,
|
||||||
|
rankPreference: 'DISTANCE',
|
||||||
|
locationRestriction: {
|
||||||
|
circle: {
|
||||||
|
center: {
|
||||||
|
latitude: input.latitude,
|
||||||
|
longitude: input.longitude,
|
||||||
|
},
|
||||||
|
radius: input.radiusMeters,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text();
|
||||||
|
throw new Error(`Google Nearby Search failed: ${response.status} ${body}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = (await response.json()) as GoogleNearbyResponse;
|
||||||
|
return parseGoogleNearbyPlaces(input, payload).map((place) =>
|
||||||
|
serializeGoogleNearbyPlace(place),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listVoiceExperiences() {
|
||||||
|
const experiences = await prisma.voiceExperience.findMany({
|
||||||
|
include: { place: true, user: true },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
return experiences.map(serializeVoiceExperience);
|
||||||
|
}
|
||||||
|
|
||||||
|
function distanceMeters(
|
||||||
|
fromLatitude: number,
|
||||||
|
fromLongitude: number,
|
||||||
|
toLatitude: number,
|
||||||
|
toLongitude: number,
|
||||||
|
) {
|
||||||
|
const earthRadiusMeters = 6371000;
|
||||||
|
const fromLat = degreesToRadians(fromLatitude);
|
||||||
|
const toLat = degreesToRadians(toLatitude);
|
||||||
|
const deltaLat = degreesToRadians(toLatitude - fromLatitude);
|
||||||
|
const deltaLon = degreesToRadians(toLongitude - fromLongitude);
|
||||||
|
const a =
|
||||||
|
Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
|
||||||
|
Math.cos(fromLat) *
|
||||||
|
Math.cos(toLat) *
|
||||||
|
Math.sin(deltaLon / 2) *
|
||||||
|
Math.sin(deltaLon / 2);
|
||||||
|
return earthRadiusMeters * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
}
|
||||||
|
|
||||||
|
function degreesToRadians(value: number) {
|
||||||
|
return (value * Math.PI) / 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGoogleNearbyPlaces(
|
||||||
|
input: NearbyPlacesInput,
|
||||||
|
payload: GoogleNearbyResponse,
|
||||||
|
) {
|
||||||
|
const placesByGoogleId = new Map<string, PersistableGooglePlace>();
|
||||||
|
for (const place of payload.places ?? []) {
|
||||||
|
const googlePlaceId = place.id;
|
||||||
|
const name = place.displayName?.text;
|
||||||
|
const latitude = place.location?.latitude;
|
||||||
|
const longitude = place.location?.longitude;
|
||||||
|
const googleTypes = place.types ?? [];
|
||||||
|
if (
|
||||||
|
!googlePlaceId ||
|
||||||
|
!name ||
|
||||||
|
latitude === undefined ||
|
||||||
|
longitude === undefined
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
distanceMeters(input.latitude, input.longitude, latitude, longitude) >
|
||||||
|
input.radiusMeters
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
placesByGoogleId.set(googlePlaceId, {
|
||||||
|
googlePlaceId,
|
||||||
|
name,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
googlePrimaryType: place.primaryType ?? null,
|
||||||
|
googleTypes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...placesByGoogleId.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeGoogleNearbyPlace(place: PersistableGooglePlace) {
|
||||||
|
return {
|
||||||
|
id: place.googlePlaceId,
|
||||||
|
googlePlaceId: place.googlePlaceId,
|
||||||
|
name: place.name,
|
||||||
|
latitude: place.latitude,
|
||||||
|
longitude: place.longitude,
|
||||||
|
googlePrimaryType: place.googlePrimaryType,
|
||||||
|
googleTypes: place.googleTypes,
|
||||||
|
experiences: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,28 @@
|
|||||||
import { GraphQLJSONObject } from './scalars.js';
|
import { GraphQLJSONObject } from './scalars.js';
|
||||||
|
import {
|
||||||
|
getOrCreateTelegramLoginUser,
|
||||||
|
getOrCreateTelegramUser,
|
||||||
|
requireAdminTelegramUser,
|
||||||
|
requireTelegramUser,
|
||||||
|
type TelegramLoginData,
|
||||||
|
} from '../auth/telegram.js';
|
||||||
|
import {
|
||||||
|
completeTelegramBotLogin,
|
||||||
|
createTelegramBotLogin,
|
||||||
|
} from '../auth/telegram-bot-login.js';
|
||||||
|
import {
|
||||||
|
listNearbyPlaces,
|
||||||
|
listPlaces,
|
||||||
|
listVoiceExperiences,
|
||||||
|
} from './places.js';
|
||||||
import { createVoiceExperience } from './voice-experiences.js';
|
import { createVoiceExperience } from './voice-experiences.js';
|
||||||
|
|
||||||
|
export type GraphqlContext = {
|
||||||
|
telegramInitData?: string;
|
||||||
|
telegramLoginData?: string;
|
||||||
|
mapflowSessionToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const schema = /* GraphQL */ `
|
export const schema = /* GraphQL */ `
|
||||||
scalar JSON
|
scalar JSON
|
||||||
|
|
||||||
@@ -19,11 +41,26 @@ export const schema = /* GraphQL */ `
|
|||||||
name: String!
|
name: String!
|
||||||
latitude: Float!
|
latitude: Float!
|
||||||
longitude: Float!
|
longitude: Float!
|
||||||
|
googlePrimaryType: String
|
||||||
|
googleTypes: [String!]!
|
||||||
|
experiences: [VoiceExperience!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
telegramId: String!
|
||||||
|
username: String
|
||||||
|
firstName: String
|
||||||
|
lastName: String
|
||||||
|
photoUrl: String
|
||||||
|
languageCode: String
|
||||||
|
isAdmin: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type VoiceExperience {
|
type VoiceExperience {
|
||||||
id: ID!
|
id: ID!
|
||||||
place: Place!
|
place: Place!
|
||||||
|
user: User
|
||||||
status: VoiceExperienceStatus!
|
status: VoiceExperienceStatus!
|
||||||
durationSeconds: Int!
|
durationSeconds: Int!
|
||||||
audioObjectKey: String!
|
audioObjectKey: String!
|
||||||
@@ -39,13 +76,58 @@ export const schema = /* GraphQL */ `
|
|||||||
longitude: Float!
|
longitude: Float!
|
||||||
durationSeconds: Int!
|
durationSeconds: Int!
|
||||||
audioObjectKey: String!
|
audioObjectKey: String!
|
||||||
|
audioContentBase64: String!
|
||||||
|
audioMimeType: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input NearbyPlacesInput {
|
||||||
|
latitude: Float!
|
||||||
|
longitude: Float!
|
||||||
|
radiusMeters: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
input AuthenticateTelegramInput {
|
||||||
|
initData: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input AuthenticateTelegramLoginInput {
|
||||||
|
id: Float!
|
||||||
|
first_name: String
|
||||||
|
last_name: String
|
||||||
|
username: String
|
||||||
|
photo_url: String
|
||||||
|
auth_date: Float!
|
||||||
|
hash: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthPayload {
|
||||||
|
user: User!
|
||||||
|
}
|
||||||
|
|
||||||
|
type TelegramBotLoginPayload {
|
||||||
|
token: String!
|
||||||
|
botUrl: String!
|
||||||
|
expiresAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type TelegramBotLoginSession {
|
||||||
|
sessionToken: String!
|
||||||
|
user: User!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
health: String!
|
health: String!
|
||||||
|
me: User!
|
||||||
|
places: [Place!]!
|
||||||
|
nearbyPlaces(input: NearbyPlacesInput!): [Place!]!
|
||||||
|
voiceExperiences: [VoiceExperience!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
startTelegramBotLogin: TelegramBotLoginPayload!
|
||||||
|
completeTelegramBotLogin(token: String!): TelegramBotLoginSession!
|
||||||
|
authenticateTelegram(input: AuthenticateTelegramInput!): AuthPayload!
|
||||||
|
authenticateTelegramLogin(input: AuthenticateTelegramLoginInput!): AuthPayload!
|
||||||
createVoiceExperience(input: CreateVoiceExperienceInput!): VoiceExperience!
|
createVoiceExperience(input: CreateVoiceExperienceInput!): VoiceExperience!
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -54,11 +136,50 @@ export const resolvers = {
|
|||||||
JSON: GraphQLJSONObject,
|
JSON: GraphQLJSONObject,
|
||||||
Query: {
|
Query: {
|
||||||
health: () => 'ok',
|
health: () => 'ok',
|
||||||
|
me: async (_: unknown, __: unknown, context: unknown) => {
|
||||||
|
const graphqlContext = context as GraphqlContext;
|
||||||
|
return requireTelegramUser(graphqlContext);
|
||||||
|
},
|
||||||
|
places: async (_: unknown, __: unknown, context: unknown) => {
|
||||||
|
const graphqlContext = context as GraphqlContext;
|
||||||
|
await requireTelegramUser(graphqlContext);
|
||||||
|
return listPlaces();
|
||||||
|
},
|
||||||
|
nearbyPlaces: async (
|
||||||
|
_: unknown,
|
||||||
|
args: { input: Parameters<typeof listNearbyPlaces>[0] },
|
||||||
|
context: unknown,
|
||||||
|
) => {
|
||||||
|
const graphqlContext = context as GraphqlContext;
|
||||||
|
await requireTelegramUser(graphqlContext);
|
||||||
|
return listNearbyPlaces(args.input);
|
||||||
|
},
|
||||||
|
voiceExperiences: async (_: unknown, __: unknown, context: unknown) => {
|
||||||
|
const graphqlContext = context as GraphqlContext;
|
||||||
|
await requireAdminTelegramUser(graphqlContext);
|
||||||
|
return listVoiceExperiences();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
|
startTelegramBotLogin: async () => createTelegramBotLogin(),
|
||||||
|
completeTelegramBotLogin: async (_: unknown, args: { token: string }) =>
|
||||||
|
completeTelegramBotLogin(args.token),
|
||||||
|
authenticateTelegram: async (
|
||||||
|
_: unknown,
|
||||||
|
args: { input: { initData: string } },
|
||||||
|
) => ({ user: await getOrCreateTelegramUser(args.input.initData) }),
|
||||||
|
authenticateTelegramLogin: async (
|
||||||
|
_: unknown,
|
||||||
|
args: { input: TelegramLoginData },
|
||||||
|
) => ({ user: await getOrCreateTelegramLoginUser(args.input) }),
|
||||||
createVoiceExperience: async (
|
createVoiceExperience: async (
|
||||||
_: unknown,
|
_: unknown,
|
||||||
args: { input: Parameters<typeof createVoiceExperience>[0] },
|
args: { input: Parameters<typeof createVoiceExperience>[0] },
|
||||||
) => createVoiceExperience(args.input),
|
context: unknown,
|
||||||
|
) => {
|
||||||
|
const graphqlContext = context as GraphqlContext;
|
||||||
|
const user = await requireTelegramUser(graphqlContext);
|
||||||
|
return createVoiceExperience(args.input, user.id);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
import { enqueueVoiceExperience } from '../hatchet/enqueue-voice-experience.js';
|
import { enqueueVoiceExperience } from '../hatchet/enqueue-voice-experience.js';
|
||||||
import { prisma } from '../prisma.js';
|
import { prisma } from '../prisma.js';
|
||||||
|
|
||||||
|
const forbiddenGeneratedPlaceIdPrefix = 'manual-';
|
||||||
|
|
||||||
export type CreateVoiceExperienceInput = {
|
export type CreateVoiceExperienceInput = {
|
||||||
googlePlaceId: string;
|
googlePlaceId: string;
|
||||||
googleName: string;
|
googleName: string;
|
||||||
@@ -8,23 +12,46 @@ export type CreateVoiceExperienceInput = {
|
|||||||
longitude: number;
|
longitude: number;
|
||||||
durationSeconds: number;
|
durationSeconds: number;
|
||||||
audioObjectKey: string;
|
audioObjectKey: string;
|
||||||
|
audioContentBase64: string;
|
||||||
|
audioMimeType: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function createVoiceExperience(input: CreateVoiceExperienceInput) {
|
function randomAudioAccessToken() {
|
||||||
if (input.durationSeconds < 60) {
|
return randomBytes(32).toString('base64url');
|
||||||
throw new Error('Voice experience must be at least 60 seconds.');
|
}
|
||||||
|
|
||||||
|
export async function createVoiceExperience(
|
||||||
|
input: CreateVoiceExperienceInput,
|
||||||
|
userId: string,
|
||||||
|
) {
|
||||||
|
const googlePlaceId = input.googlePlaceId.trim();
|
||||||
|
const googleName = input.googleName.trim();
|
||||||
|
if (googlePlaceId === '') {
|
||||||
|
throw new Error('Google place id is required.');
|
||||||
|
}
|
||||||
|
if (googlePlaceId.startsWith(forbiddenGeneratedPlaceIdPrefix)) {
|
||||||
|
throw new Error('Voice experience must be linked to a Google place.');
|
||||||
|
}
|
||||||
|
if (googleName === '') {
|
||||||
|
throw new Error('Google place name is required.');
|
||||||
|
}
|
||||||
|
if (input.audioContentBase64.trim() === '') {
|
||||||
|
throw new Error('Voice experience audio is required.');
|
||||||
|
}
|
||||||
|
if (input.audioMimeType.trim() === '') {
|
||||||
|
throw new Error('Voice experience audio MIME type is required.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const place = await prisma.place.upsert({
|
const place = await prisma.place.upsert({
|
||||||
where: { googlePlaceId: input.googlePlaceId },
|
where: { googlePlaceId },
|
||||||
create: {
|
create: {
|
||||||
googlePlaceId: input.googlePlaceId,
|
googlePlaceId,
|
||||||
name: input.googleName,
|
name: googleName,
|
||||||
latitude: input.latitude,
|
latitude: input.latitude,
|
||||||
longitude: input.longitude,
|
longitude: input.longitude,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
name: input.googleName,
|
name: googleName,
|
||||||
latitude: input.latitude,
|
latitude: input.latitude,
|
||||||
longitude: input.longitude,
|
longitude: input.longitude,
|
||||||
},
|
},
|
||||||
@@ -33,11 +60,15 @@ export async function createVoiceExperience(input: CreateVoiceExperienceInput) {
|
|||||||
const experience = await prisma.voiceExperience.create({
|
const experience = await prisma.voiceExperience.create({
|
||||||
data: {
|
data: {
|
||||||
placeId: place.id,
|
placeId: place.id,
|
||||||
|
userId,
|
||||||
durationSeconds: input.durationSeconds,
|
durationSeconds: input.durationSeconds,
|
||||||
audioObjectKey: input.audioObjectKey,
|
audioObjectKey: input.audioObjectKey,
|
||||||
|
audioContentBase64: input.audioContentBase64,
|
||||||
|
audioMimeType: input.audioMimeType,
|
||||||
|
audioAccessToken: randomAudioAccessToken(),
|
||||||
status: 'UPLOADED',
|
status: 'UPLOADED',
|
||||||
},
|
},
|
||||||
include: { place: true },
|
include: { place: true, user: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
await enqueueVoiceExperience({ experienceId: experience.id });
|
await enqueueVoiceExperience({ experienceId: experience.id });
|
||||||
|
|||||||
@@ -4,17 +4,81 @@ import mercurius from 'mercurius';
|
|||||||
import { config } from './config.js';
|
import { config } from './config.js';
|
||||||
import { prisma } from './prisma.js';
|
import { prisma } from './prisma.js';
|
||||||
import { resolvers, schema } from './graphql/schema.js';
|
import { resolvers, schema } from './graphql/schema.js';
|
||||||
|
import {
|
||||||
|
fetchTelegramPhoto,
|
||||||
|
handleTelegramBotWebhook,
|
||||||
|
} from './auth/telegram-bot-login.js';
|
||||||
|
|
||||||
const app = Fastify({ logger: true });
|
const app = Fastify({
|
||||||
|
logger: true,
|
||||||
|
bodyLimit: 25 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
|
||||||
app.register(mercurius, {
|
app.register(mercurius, {
|
||||||
schema,
|
schema,
|
||||||
resolvers,
|
resolvers,
|
||||||
graphiql: true,
|
graphiql: true,
|
||||||
|
context: async (request) => {
|
||||||
|
const initDataHeader = request.headers['x-telegram-init-data'];
|
||||||
|
const loginDataHeader = request.headers['x-telegram-login-data'];
|
||||||
|
const sessionTokenHeader = request.headers['x-mapflow-session-token'];
|
||||||
|
return {
|
||||||
|
telegramInitData: Array.isArray(initDataHeader) ? initDataHeader[0] : initDataHeader,
|
||||||
|
telegramLoginData: Array.isArray(loginDataHeader) ? loginDataHeader[0] : loginDataHeader,
|
||||||
|
mapflowSessionToken: Array.isArray(sessionTokenHeader) ? sessionTokenHeader[0] : sessionTokenHeader,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/health', async () => ({ ok: true }));
|
app.get('/health', async () => ({ ok: true }));
|
||||||
|
|
||||||
|
app.post('/telegram/webhook', async (request, reply) => {
|
||||||
|
const secretHeader = request.headers['x-telegram-bot-api-secret-token'];
|
||||||
|
const secretToken = Array.isArray(secretHeader) ? secretHeader[0] : secretHeader;
|
||||||
|
await handleTelegramBotWebhook(request.body as never, secretToken);
|
||||||
|
return reply.send({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/telegram/photo/:fileId', async (request, reply) => {
|
||||||
|
const params = request.params as { fileId: string };
|
||||||
|
const photo = await fetchTelegramPhoto(params.fileId);
|
||||||
|
reply.header('content-type', photo.contentType);
|
||||||
|
reply.header('cache-control', 'public, max-age=86400');
|
||||||
|
reply.header('access-control-allow-origin', '*');
|
||||||
|
reply.header('cross-origin-resource-policy', 'cross-origin');
|
||||||
|
return reply.send(photo.bytes);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/audio/voice-experiences/:experienceId', async (request, reply) => {
|
||||||
|
const params = request.params as { experienceId: string };
|
||||||
|
const query = request.query as { token?: string };
|
||||||
|
if (!query.token) {
|
||||||
|
return reply.code(401).send({ error: 'Audio token is required.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const experience = await prisma.voiceExperience.findUnique({
|
||||||
|
where: { id: params.experienceId },
|
||||||
|
select: {
|
||||||
|
audioAccessToken: true,
|
||||||
|
audioContentBase64: true,
|
||||||
|
audioMimeType: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!experience ||
|
||||||
|
experience.audioAccessToken !== query.token ||
|
||||||
|
!experience.audioContentBase64 ||
|
||||||
|
!experience.audioMimeType
|
||||||
|
) {
|
||||||
|
return reply.code(404).send({ error: 'Audio was not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.header('content-type', experience.audioMimeType);
|
||||||
|
reply.header('cache-control', 'no-store');
|
||||||
|
return reply.send(Buffer.from(experience.audioContentBase64, 'base64'));
|
||||||
|
});
|
||||||
|
|
||||||
app.addHook('onClose', async () => {
|
app.addHook('onClose', async () => {
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
});
|
});
|
||||||
|
|||||||
65
src/vault/env.ts
Normal file
65
src/vault/env.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
type VaultConfig = {
|
||||||
|
address: string;
|
||||||
|
token: string;
|
||||||
|
mount: string;
|
||||||
|
sharedPath: string;
|
||||||
|
projectPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function requireEnv(name: string) {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`${name} is required when VAULT_ENABLED=true.`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vaultConfig(): VaultConfig {
|
||||||
|
return {
|
||||||
|
address: requireEnv('VAULT_ADDR').replace(/\/$/, ''),
|
||||||
|
token: requireEnv('VAULT_TOKEN'),
|
||||||
|
mount: requireEnv('VAULT_KV_MOUNT'),
|
||||||
|
sharedPath: requireEnv('VAULT_SHARED_PATH'),
|
||||||
|
projectPath: requireEnv('VAULT_PROJECT_PATH'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readVaultPath(config: VaultConfig, path: string) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.address}/v1/${config.mount}/data/${path}`,
|
||||||
|
{ headers: { 'X-Vault-Token': config.token } },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Vault read failed for ${path}: ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = (await response.json()) as {
|
||||||
|
data?: { data?: Record<string, unknown> };
|
||||||
|
};
|
||||||
|
const data = payload.data?.data;
|
||||||
|
if (!data) {
|
||||||
|
throw new Error(`Vault path ${path} has no KV v2 data.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyEnvironment(values: Record<string, unknown>) {
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new Error(`Vault value ${key} must be a string.`);
|
||||||
|
}
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadVaultEnvironment() {
|
||||||
|
if (process.env.VAULT_ENABLED !== 'true') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = vaultConfig();
|
||||||
|
applyEnvironment(await readVaultPath(config, config.sharedPath));
|
||||||
|
applyEnvironment(await readVaultPath(config, config.projectPath));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user