From 6d68351a46409138eea72b8f1094eb6c2da5c7e1 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Sun, 21 Sep 2025 19:35:41 +0100 Subject: [PATCH] feat: end to end api type safety with redis rate limiting (#573) * feat: support hono rpc * add wrangler file * update name of wrangler file * add wrangler as a dependency * add compatibility_flags * trigger ci * core: add health check to docker file * expose port 80 * update custom healthcheck * feat: implement openapi * install dependencies in root * debug yml * update yml * update port * update dockerfile with envs * completely move away from apiClient * rename backend to api * refactor: update server descriptions and comment out local spec saving * update lock file * use api.classroomio.com * add support for redis * fix: add missing line break in WelcomeModal.svelte * refactor: remove wrangler configuration and update Vite config for API integration --- .github/workflows/deploy-backend-on-push.yml | 18 - .github/workflows/upload-openapi-spec.yml | 61 + README.md | 8 +- apps/{backend => api}/.dockerignore | 0 apps/{backend => api}/.env.example | 4 +- apps/api/.gitignore | 33 +- apps/{backend => api}/Dockerfile | 18 +- apps/api/README.md | 16 +- apps/{backend => api}/certificate.html | 0 apps/{backend => api}/course.html | 0 apps/{backend => api}/fly.toml | 0 apps/{backend => api}/lesson.html | 0 apps/api/package.json | 79 +- apps/api/scripts/README.md | 74 + apps/api/scripts/upload-openapi-spec.ts | 191 +++ apps/api/src/app.ts | 44 + apps/{backend => api}/src/config/env.ts | 21 +- apps/{backend => api}/src/constants/index.ts | 8 +- apps/api/src/constants/rate-limiter.ts | 32 + apps/{backend => api}/src/constants/upload.ts | 0 apps/api/src/index.ts | 25 +- apps/{backend => api}/src/middlewares/auth.ts | 0 apps/api/src/middlewares/rate-limiter.ts | 79 + apps/api/src/routes/course/course.ts | 50 + .../src/routes/course/katex.ts | 6 +- apps/api/src/routes/course/lesson.ts | 24 + apps/api/src/routes/course/presign.ts | 253 ++++ apps/{backend => api}/src/routes/mail.ts | 38 +- apps/api/src/rpc-types.ts | 9 + apps/{backend => api}/src/services/mail.ts | 0 .../src/types/course/index.ts | 10 +- .../src/types/course/lesson.ts | 0 apps/{backend => api}/src/types/mail.ts | 0 .../src/utils/auth/validate-user.ts | 2 +- .../{backend => api}/src/utils/certificate.ts | 0 apps/{backend => api}/src/utils/cloudflare.ts | 5 +- apps/{backend => api}/src/utils/course.ts | 3 +- apps/{backend => api}/src/utils/email.ts | 0 .../{backend => api}/src/utils/genUniqueId.ts | 0 apps/{backend => api}/src/utils/lesson.ts | 0 apps/{backend => api}/src/utils/mail.ts | 0 apps/api/src/utils/openapi/index.ts | 9 + apps/api/src/utils/redis/key-generators.ts | 105 ++ apps/api/src/utils/redis/limiter.ts | 71 + apps/api/src/utils/redis/redis.ts | 4 + apps/{backend => api}/src/utils/s3.ts | 0 apps/{backend => api}/src/utils/supabase.ts | 2 +- apps/{backend => api}/src/utils/upload.ts | 0 apps/api/tsconfig.json | 33 +- apps/{backend => api}/vitest.config.ts | 0 apps/api/wrangler.toml | 28 - apps/backend/.gitignore | 4 - apps/backend/README.md | 10 - apps/backend/package.json | 62 - apps/backend/src/index.ts | 66 - apps/backend/src/routes/course/course.ts | 91 -- apps/backend/src/routes/course/lesson.ts | 35 - apps/backend/src/routes/course/presign.ts | 199 --- apps/backend/tsconfig.json | 26 - apps/classroomio-com/src/blog/self-host.md | 2 +- .../src/routes/api/contact/+server.ts | 2 +- apps/dashboard/.env.example | 2 +- apps/dashboard/classroomio.code-workspace | 17 - apps/dashboard/package.json | 2 + apps/dashboard/postcss.config.cjs | 13 - .../UnlockedCertificate.svelte | 20 +- ...ndex.svelte => AddDocumentToLesson.svelte} | 160 +- .../Lesson/Materials/Document/store.ts | 11 +- .../{Index.svelte => AddVideoToLesson.svelte} | 0 .../Lesson/Materials/Video/UploadVideo.svelte | 140 +- .../components/ComponentDocument.svelte | 95 +- .../components/Lesson/Materials/index.svelte | 44 +- .../Course/components/Lesson/store/lessons.ts | 32 +- .../WelcomeModal/WelcomeModal.svelte | 5 +- .../lib/utils/functions/tinymce/plugins.ts | 2 +- apps/dashboard/src/lib/utils/services/api.ts | 19 - .../src/lib/utils/services/api/README.md | 80 + .../src/lib/utils/services/api/constants.ts | 24 + .../src/lib/utils/services/api/index.ts | 212 +++ .../src/lib/utils/services/api/types.ts | 46 + .../src/lib/utils/services/api/utils.ts | 125 ++ .../src/lib/utils/services/courses/index.ts | 35 +- .../src/lib/utils/services/courses/presign.ts | 134 ++ .../src/lib/utils/translations/de.json | 6 +- .../src/lib/utils/translations/en.json | 4 +- .../src/lib/utils/translations/es.json | 6 +- .../src/lib/utils/translations/fr.json | 6 +- .../src/lib/utils/translations/hi.json | 6 +- .../src/lib/utils/translations/pl.json | 6 +- .../src/lib/utils/translations/pt.json | 6 +- .../src/lib/utils/translations/ru.json | 6 +- .../src/lib/utils/translations/vi.json | 6 +- apps/dashboard/src/lib/utils/types/index.ts | 2 +- apps/dashboard/src/mail/sendEmail.ts | 13 +- .../lessons/[...lessonParams]/+page.svelte | 15 +- apps/dashboard/tailwind.config.cjs | 15 - apps/dashboard/tsconfig.json | 2 +- apps/dashboard/vite.config.ts | 14 +- .../contributor-guides/cloudflare-setup.mdx | 2 +- apps/docs/pages/quickstart/self-hosting.mdx | 20 +- docker-compose.yaml | 16 +- pnpm-lock.yaml | 1342 ++++++++++------- 102 files changed, 2938 insertions(+), 1633 deletions(-) delete mode 100644 .github/workflows/deploy-backend-on-push.yml create mode 100644 .github/workflows/upload-openapi-spec.yml rename apps/{backend => api}/.dockerignore (100%) rename apps/{backend => api}/.env.example (92%) rename apps/{backend => api}/Dockerfile (82%) rename apps/{backend => api}/certificate.html (100%) rename apps/{backend => api}/course.html (100%) rename apps/{backend => api}/fly.toml (100%) rename apps/{backend => api}/lesson.html (100%) create mode 100644 apps/api/scripts/README.md create mode 100644 apps/api/scripts/upload-openapi-spec.ts create mode 100644 apps/api/src/app.ts rename apps/{backend => api}/src/config/env.ts (76%) rename apps/{backend => api}/src/constants/index.ts (76%) create mode 100644 apps/api/src/constants/rate-limiter.ts rename apps/{backend => api}/src/constants/upload.ts (100%) rename apps/{backend => api}/src/middlewares/auth.ts (100%) create mode 100644 apps/api/src/middlewares/rate-limiter.ts create mode 100644 apps/api/src/routes/course/course.ts rename apps/{backend => api}/src/routes/course/katex.ts (87%) create mode 100644 apps/api/src/routes/course/lesson.ts create mode 100644 apps/api/src/routes/course/presign.ts rename apps/{backend => api}/src/routes/mail.ts (59%) create mode 100644 apps/api/src/rpc-types.ts rename apps/{backend => api}/src/services/mail.ts (100%) rename apps/{backend => api}/src/types/course/index.ts (90%) rename apps/{backend => api}/src/types/course/lesson.ts (100%) rename apps/{backend => api}/src/types/mail.ts (100%) rename apps/{backend => api}/src/utils/auth/validate-user.ts (89%) rename apps/{backend => api}/src/utils/certificate.ts (100%) rename apps/{backend => api}/src/utils/cloudflare.ts (82%) rename apps/{backend => api}/src/utils/course.ts (97%) rename apps/{backend => api}/src/utils/email.ts (100%) rename apps/{backend => api}/src/utils/genUniqueId.ts (100%) rename apps/{backend => api}/src/utils/lesson.ts (100%) rename apps/{backend => api}/src/utils/mail.ts (100%) create mode 100644 apps/api/src/utils/openapi/index.ts create mode 100644 apps/api/src/utils/redis/key-generators.ts create mode 100644 apps/api/src/utils/redis/limiter.ts create mode 100644 apps/api/src/utils/redis/redis.ts rename apps/{backend => api}/src/utils/s3.ts (100%) rename apps/{backend => api}/src/utils/supabase.ts (93%) rename apps/{backend => api}/src/utils/upload.ts (100%) rename apps/{backend => api}/vitest.config.ts (100%) delete mode 100644 apps/api/wrangler.toml delete mode 100644 apps/backend/.gitignore delete mode 100644 apps/backend/README.md delete mode 100644 apps/backend/package.json delete mode 100644 apps/backend/src/index.ts delete mode 100644 apps/backend/src/routes/course/course.ts delete mode 100644 apps/backend/src/routes/course/lesson.ts delete mode 100644 apps/backend/src/routes/course/presign.ts delete mode 100644 apps/backend/tsconfig.json delete mode 100644 apps/dashboard/classroomio.code-workspace delete mode 100644 apps/dashboard/postcss.config.cjs rename apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Document/{Index.svelte => AddDocumentToLesson.svelte} (66%) rename apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/{Index.svelte => AddVideoToLesson.svelte} (100%) delete mode 100644 apps/dashboard/src/lib/utils/services/api.ts create mode 100644 apps/dashboard/src/lib/utils/services/api/README.md create mode 100644 apps/dashboard/src/lib/utils/services/api/constants.ts create mode 100644 apps/dashboard/src/lib/utils/services/api/index.ts create mode 100644 apps/dashboard/src/lib/utils/services/api/types.ts create mode 100644 apps/dashboard/src/lib/utils/services/api/utils.ts create mode 100644 apps/dashboard/src/lib/utils/services/courses/presign.ts delete mode 100644 apps/dashboard/tailwind.config.cjs diff --git a/.github/workflows/deploy-backend-on-push.yml b/.github/workflows/deploy-backend-on-push.yml deleted file mode 100644 index 6c33335b..00000000 --- a/.github/workflows/deploy-backend-on-push.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Deploy backend on push - -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - deploy: - runs-on: ubuntu-latest - concurrency: deploy-group # optional: ensure only one action runs at a time - steps: - - uses: actions/checkout@v4 - - uses: superfly/flyctl-actions/setup-flyctl@master - - run: cd apps/backend && flyctl deploy --remote-only - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/.github/workflows/upload-openapi-spec.yml b/.github/workflows/upload-openapi-spec.yml new file mode 100644 index 00000000..79d846dc --- /dev/null +++ b/.github/workflows/upload-openapi-spec.yml @@ -0,0 +1,61 @@ +name: Upload OpenAPI Spec to R2 + +on: + push: + branches: [main] + paths: + - 'apps/api/src/**' + - 'apps/api/scripts/**' + - 'apps/api/package.json' + workflow_dispatch: + +env: + NODE_VERSION: '20' + PNPM_VERSION: '8' + +jobs: + generate-and-upload-openapi: + runs-on: ubuntu-latest + environment: production + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Get pnpm store directory + shell: bash + run: | + ls -la && echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v3 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm i + + - name: Upload to Cloudflare R2 + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + run: | + cd apps/api + pnpm run upload:openapi + env: + CLOUDFLARE_ACCESS_KEY: ${{ secrets.CLOUDFLARE_ACCESS_KEY }} + CLOUDFLARE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_SECRET_ACCESS_KEY }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + PUBLIC_SUPABASE_URL: ${{ secrets.PUBLIC_SUPABASE_URL }} + PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.PUBLIC_SUPABASE_ANON_KEY }} diff --git a/README.md b/README.md index 844f5c02..7d4dcec1 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Here is what you need to be able to run ClassroomIO.com This repo is a mono repo that consists of 3 projects: 1. `classroomio-com`: The landing page of ClassroomIO hosted [here](https://classroomio.com) -2. `backend`: The backend service that handles PDF, video processing, Emailing and Notifications. +2. `api`: The api service that handles PDF, video processing, Emailing and Notifications. 3. `dashboard`: The web application that runs the learning management system hosted [here](https://app.classroomio.com). 4. `docs`: Official documentation of ClassroomIO hosted [here](https://classroomio.com/docs) @@ -118,7 +118,7 @@ This repo is a mono repo that consists of 3 projects: 4. Set up your `.env` file - - Go to `apps/classroomio-com`, `apps/dashboard` and `apps/backend`. + - Go to `apps/classroomio-com`, `apps/dashboard` and `apps/api`. - Duplicate the `.env.example` file and rename it to `.env` - Populate your .env files with the neccessary variables @@ -173,14 +173,14 @@ To get the environmental variables for supabase continue to step(5) 8. All projects should start running - `classroomio-com`: [http://localhost:5174](http://localhost:5174) - - `backend`: [http://localhost:3002](http://localhost:3002) + - `api`: [http://localhost:3002](http://localhost:3002) - `dashboard`: [http://localhost:5173](http://localhost:5173) - `docs`: [http://localhost:3000](http://localhost:3000) 9. Running a specific project - **classroomio-com**: `pnpm dev --filter=classroomio-com` - - **backend**: `pnpm dev --filter=backend` + - **api**: `pnpm dev --filter=api` - **dashboard**: `pnpm dev --filter=dashboard` - **docs**: `pnpm dev --filter=docs` diff --git a/apps/backend/.dockerignore b/apps/api/.dockerignore similarity index 100% rename from apps/backend/.dockerignore rename to apps/api/.dockerignore diff --git a/apps/backend/.env.example b/apps/api/.env.example similarity index 92% rename from apps/backend/.env.example rename to apps/api/.env.example index 0f24be11..ca67045a 100644 --- a/apps/backend/.env.example +++ b/apps/api/.env.example @@ -17,4 +17,6 @@ SMTP_PASSWORD= SMTP_PORT= # Sentry DNS -SENTRY_DNS= \ No newline at end of file +SENTRY_DNS= + +OPENAPI_URL= \ No newline at end of file diff --git a/apps/api/.gitignore b/apps/api/.gitignore index e319e063..80647510 100644 --- a/apps/api/.gitignore +++ b/apps/api/.gitignore @@ -1,33 +1,4 @@ -# prod -dist/ - -# dev -.yarn/ -!.yarn/releases -.vscode/* -!.vscode/launch.json -!.vscode/*.code-snippets -.idea/workspace.xml -.idea/usage.statistics.xml -.idea/shelf - -# deps -node_modules/ -.wrangler - -# env +node_modules .env -.env.production -.dev.vars - -# logs -logs/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -# misc .DS_Store +dist \ No newline at end of file diff --git a/apps/backend/Dockerfile b/apps/api/Dockerfile similarity index 82% rename from apps/backend/Dockerfile rename to apps/api/Dockerfile index dff48ecc..fb59e5c3 100644 --- a/apps/backend/Dockerfile +++ b/apps/api/Dockerfile @@ -13,12 +13,18 @@ ARG SMTP_USER ARG SMTP_PASSWORD ARG SMTP_PORT ARG SMTP_SENDER +ARG ZOHO_TOKEN +ARG CLOUDFLARE_RENDERING_API_KEY +ARG OPENAPI_URL RUN mkdir /app WORKDIR /app -# Install pnpm -RUN npm install -g pnpm +# Install pnpm and healthcheck tools +RUN npm install -g pnpm && \ + apt-get update && \ + apt-get install -y curl wget && \ + rm -rf /var/lib/apt/lists/* # Install chrome stable from sources, then remove it. Why? The # npm install of `puppeteer` brings its own bundle chromium build, @@ -58,14 +64,22 @@ ENV NODE_ENV=production \ CLOUDFLARE_SECRET_ACCESS_KEY=$CLOUDFLARE_SECRET_ACCESS_KEY \ CLOUDFLARE_ACCOUNT_ID=$CLOUDFLARE_ACCOUNT_ID \ CLOUDFLARE_BUCKET_ID=$CLOUDFLARE_BUCKET_ID \ + CLOUDFLARE_RENDERING_API_KEY=$CLOUDFLARE_RENDERING_API_KEY \ SMTP_HOST=$SMTP_HOST \ SMTP_USER=$SMTP_USER \ SMTP_PASSWORD=$SMTP_PASSWORD \ SMTP_PORT=$SMTP_PORT \ + SMTP_SENDER=$SMTP_SENDER \ + ZOHO_TOKEN=$ZOHO_TOKEN \ + OPENAPI_URL=$OPENAPI_URL \ PORT=3002 RUN pnpm build EXPOSE 3002 +# Add healthcheck +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3002 || exit 1 + CMD ["pnpm", "start"] diff --git a/apps/api/README.md b/apps/api/README.md index cc58e962..92072dbc 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -1,8 +1,10 @@ -``` -npm install -npm run dev -``` +# ClassroomIO Backend -``` -npm run deploy -``` +## Supported Features + +1. Certificate download for students. +2. Uploading videos to cloudflare (for storage) and Muse.ai (for transcription) + +## Debugging Fly.io + +View logs from fly.io in real time diff --git a/apps/backend/certificate.html b/apps/api/certificate.html similarity index 100% rename from apps/backend/certificate.html rename to apps/api/certificate.html diff --git a/apps/backend/course.html b/apps/api/course.html similarity index 100% rename from apps/backend/course.html rename to apps/api/course.html diff --git a/apps/backend/fly.toml b/apps/api/fly.toml similarity index 100% rename from apps/backend/fly.toml rename to apps/api/fly.toml diff --git a/apps/backend/lesson.html b/apps/api/lesson.html similarity index 100% rename from apps/backend/lesson.html rename to apps/api/lesson.html diff --git a/apps/api/package.json b/apps/api/package.json index 388bf737..5aed6318 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,14 +1,79 @@ { - "name": "api", + "name": "@cio/api", + "version": "0.0.1", + "description": "ClassroomIO Backend for long processes", + "main": "dist/index.js", + "type": "module", "scripts": { - "dev": "wrangler dev", - "deploy": "wrangler deploy --minify" + "build": "tsc && tsc-alias", + "dev": "pnpm --filter @cio/api --parallel \"/^dev:.*/\"", + "dev:server": "tsx watch src/index.ts", + "dev:tsc": "tsc -p tsconfig.json --watch", + "deploy": "wrangler deploy", + "start": "node dist/index.js", + "test": "vitest NODE_ENV=test", + "test:coverage": "vitest run --coverage", + "lint": "eslint . --ext .ts", + "format": "prettier --write \"src/**/*.ts\"", + "upload:openapi": "tsx scripts/upload-openapi-spec.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/classroomio/classroomio.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/classroomio/classroomio/issues" + }, + "homepage": "https://github.com/classroomio/classroomio#readme", + "exports": { + "./rpc-types": { + "types": "./dist/rpc-types.d.ts", + "default": "./dist/rpc-types.js" + } }, "dependencies": { - "hono": "^4.6.16" + "@aws-sdk/client-s3": "^3.388.0", + "@aws-sdk/s3-request-presigner": "^3.675.0", + "@hono/node-server": "^1.8.2", + "@hono/standard-validator": "^0.1.5", + "@hono/zod-validator": "^0.7.2", + "@scalar/hono-api-reference": "^0.9.18", + "@sentry/node": "^7.110.1", + "@sentry/profiling-node": "^7.110.1", + "@supabase/supabase-js": "^2.32.0", + "axios": "^1.4.0", + "dotenv": "^16.3.1", + "hono": "^4.9.7", + "hono-openapi": "^1.0.8", + "hono-rate-limiter": "^0.4.2", + "ioredis": "^5.7.0", + "katex": "^0.16.10", + "marked": "^7.0.3", + "nanoid": "^5.1.5", + "nodemailer": "^6.9.5", + "zeptomail": "^6.0.0", + "zod": "^4.1.8", + "zod-openapi": "^5.4.1" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20250109.0", - "wrangler": "^3.101.0" + "@types/katex": "^0.16.7", + "@types/node": "^20.11.19", + "@types/nodemailer": "^6.4.17", + "@typescript-eslint/eslint-plugin": "7.0.1", + "@typescript-eslint/parser": "7.0.1", + "@vitest/coverage-v8": "^1.2.2", + "eslint": "^9.31.0", + "prettier": "^3.2.5", + "ts-node": "^10.9.2", + "tsc-alias": "^1.8.16", + "tsx": "^4.7.1", + "typescript": "^5.5.2", + "vitest": "^1.2.2", + "wrangler": "4.21.x" + }, + "_moduleAliases": { + "$src": "src" } -} \ No newline at end of file +} diff --git a/apps/api/scripts/README.md b/apps/api/scripts/README.md new file mode 100644 index 00000000..ccd54105 --- /dev/null +++ b/apps/api/scripts/README.md @@ -0,0 +1,74 @@ +# OpenAPI Spec Generator Script + +This script generates the OpenAPI specification from your Hono application and optionally uploads it to a Cloudflare R2 bucket. + +## Usage + +### Generate and Upload OpenAPI spec + +```bash +pnpm run upload:openapi +``` + +### Direct script usage +```bash +tsx scripts/upload-openapi-spec.ts +``` + +## Examples + +```bash +# Generate spec locally only +pnpm upload:openapi +``` + +## Environment Variables + +For R2 upload functionality, set these environment variables: + +```bash +CLOUDFLARE_ACCESS_KEY=your_access_key +CLOUDFLARE_SECRET_ACCESS_KEY=your_secret_key +CLOUDFLARE_ACCOUNT_ID=your_account_id +``` + +## Output + +The script will: + +1. **Generate** the OpenAPI specification from your Hono app routes +2. **Save locally** to `dist/openapi/openapi.json` +3. **Upload to R2** (if credentials provided): + - `openapi/openapi-YYYY-MM-DD.json` (dated version) + - `openapi/openapi-latest.json` (latest version) + +## GitHub Action Integration + +This script is integrated with a GitHub Action that automatically generates and uploads the OpenAPI spec on code changes. + +### Automatic Workflow + +The GitHub Action runs on: +- **Push to main** - Automatically uploads to R2 +- **Pull Requests** - Generates spec for review (no upload) +- **Manual dispatch** - Allows manual triggering with custom parameters + +### Setup + +1. **Required secrets:** + - `CLOUDFLARE_ACCESS_KEY` + - `CLOUDFLARE_SECRET_ACCESS_KEY` + - `CLOUDFLARE_ACCOUNT_ID` + - `PUBLIC_SUPABASE_URL` + - `PUBLIC_SUPABASE_ANON_KEY` + +## Features + +- ✅ Automatic OpenAPI spec generation from Hono routes +- ✅ Local file generation for development +- ✅ Cloudflare R2 upload with versioning +- ✅ GitHub Action integration +- ✅ Environment-specific deployments +- ✅ PR previews and artifacts +- ✅ Error handling and logging +- ✅ CLI help with `--help` flag diff --git a/apps/api/scripts/upload-openapi-spec.ts b/apps/api/scripts/upload-openapi-spec.ts new file mode 100644 index 00000000..36280a78 --- /dev/null +++ b/apps/api/scripts/upload-openapi-spec.ts @@ -0,0 +1,191 @@ +#!/usr/bin/env tsx + +import 'dotenv/config'; + +import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { mkdirSync, writeFileSync } from 'fs'; + +import { app } from '../src/app'; +import { env } from '../src/config/env'; +import { generateSpecs } from 'hono-openapi'; +import { join } from 'path'; + +interface UploadOptions { + key: string; + content: string; + contentType?: string; +} + +class OpenAPISpecGenerator { + private s3Client: S3Client | null = null; + + constructor() { + this.initializeS3Client(); + } + + private initializeS3Client() { + if ( + env.CLOUDFLARE_ACCESS_KEY && + env.CLOUDFLARE_SECRET_ACCESS_KEY && + env.CLOUDFLARE_ACCOUNT_ID + ) { + this.s3Client = new S3Client({ + region: 'auto', + endpoint: `https://${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`, + credentials: { + accessKeyId: env.CLOUDFLARE_ACCESS_KEY, + secretAccessKey: env.CLOUDFLARE_SECRET_ACCESS_KEY + } + }); + console.log('✅ S3 client initialized for Cloudflare R2'); + } else { + console.warn('⚠️ Cloudflare R2 credentials not found. Will only generate local spec file.'); + } + } + + async generateSpec(): Promise { + try { + console.log('🔄 Generating OpenAPI specification...'); + + // Generate the OpenAPI spec from the Hono app + const spec = await generateSpecs(app, { + documentation: { + info: { + title: 'ClassroomIO API', + version: '1.0.0', + description: 'Manage your organization on classroomio via the API', + contact: { + name: 'ClassroomIO', + url: 'https://classroomio.com' + } + }, + servers: [ + { + url: 'https://api.classroomio.com', + description: 'Production environment' + }, + { + url: 'http://localhost:3002', + description: 'Development environment' + } + ] + } + }); + + const specString = JSON.stringify(spec, null, 2); + console.log('✅ OpenAPI specification generated successfully'); + + return specString; + } catch (error) { + console.error('❌ Error generating OpenAPI spec:', error); + throw error; + } + } + + async saveLocalSpec(spec: string): Promise { + try { + const outputDir = join(process.cwd(), 'dist', 'openapi'); + mkdirSync(outputDir, { recursive: true }); + + const filePath = join(outputDir, 'openapi.json'); + writeFileSync(filePath, spec, 'utf8'); + + console.log(`✅ OpenAPI spec saved locally: ${filePath}`); + return filePath; + } catch (error) { + console.error('❌ Error saving local spec:', error); + throw error; + } + } + + async uploadToR2(options: UploadOptions): Promise { + if (!this.s3Client) { + throw new Error('S3 client not initialized. Check your Cloudflare R2 credentials.'); + } + + try { + console.log(`🔄 Uploading to R2 bucket`); + + const command = new PutObjectCommand({ + Bucket: 'api', + Key: options.key, + Body: options.content, + ContentType: options.contentType || 'application/json', + CacheControl: 'public, max-age=3600' // Cache for 1 hour + }); + + await this.s3Client.send(command); + console.log(`✅ Successfully uploaded to R2`); + } catch (error) { + console.error('❌ Error uploading to R2:', error); + throw error; + } + } + + async generateAndUpload(): Promise { + try { + const spec = await this.generateSpec(); + + // await this.saveLocalSpec(spec); + + if (this.s3Client) { + const uploadKey = `openapi/openapi-${new Date().toISOString().split('T')[0]}.json`; + await this.uploadToR2({ + key: uploadKey, + content: spec, + contentType: 'application/json' + }); + + // Also upload as latest version + await this.uploadToR2({ + key: 'openapi/openapi-latest.json', + content: spec, + contentType: 'application/json' + }); + } + + console.log('🎉 OpenAPI spec generation and upload completed successfully!'); + } catch (error) { + console.error('❌ Failed to generate and upload OpenAPI spec:', error); + process.exit(1); + } + } +} + +// CLI interface +async function main() { + const args = process.argv.slice(2); + + if (args.includes('--help') || args.includes('-h')) { + console.log(` +Usage: tsx scripts/upload-openapi-spec.ts + +Examples: + tsx scripts/upload-openapi-spec.ts + +Environment Variables Required for R2 Upload: + CLOUDFLARE_ACCESS_KEY + CLOUDFLARE_SECRET_ACCESS_KEY + CLOUDFLARE_ACCOUNT_ID + `); + return; + } + + const generator = new OpenAPISpecGenerator(); + await generator.generateAndUpload(); +} + +// Run the script if called directly +if (process.argv[1] && process.argv[1].endsWith('upload-openapi-spec.ts')) { + main() + .then(() => { + console.log('Script completed successfully'); + process.exit(0); + }) + .catch((error) => { + console.error('Script failed:', error); + process.exit(1); + }); +} + +export { OpenAPISpecGenerator }; diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts new file mode 100644 index 00000000..3e52486a --- /dev/null +++ b/apps/api/src/app.ts @@ -0,0 +1,44 @@ +import 'dotenv/config'; + +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { courseRouter } from '$src/routes/course/course'; +import { logger } from 'hono/logger'; +import { mailRouter } from '$src/routes/mail'; +import { prettyJSON } from 'hono/pretty-json'; +import { rateLimiterMiddleware } from '$src/middlewares/rate-limiter'; +import { secureHeaders } from 'hono/secure-headers'; + +// Create Hono app with chaining for RPC support +export const app = new Hono() + // Middleware + .use('*', logger()) + .use('*', prettyJSON()) + .use('*', secureHeaders()) + .use( + '*', + cors({ + origin: '*', + allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization'], + exposeHeaders: ['Content-Length', 'X-Kuma-Revision'], + maxAge: 600, + credentials: true + }) + ) + .use('*', rateLimiterMiddleware) + + // Routes + .get('/', (c) => + c.json({ + message: '"Welcome to Classroomio.com API - docs are at https://api.classroomio.com/docs"' + }) + ) + .route('/course', courseRouter) + .route('/mail', mailRouter) + + // Error handling + .onError((err, c) => { + console.error('Error:', err); + return c.json({ error: 'Internal Server Error' }, 500); + }); diff --git a/apps/backend/src/config/env.ts b/apps/api/src/config/env.ts similarity index 76% rename from apps/backend/src/config/env.ts rename to apps/api/src/config/env.ts index 0e18d88c..84d8864c 100644 --- a/apps/backend/src/config/env.ts +++ b/apps/api/src/config/env.ts @@ -1,20 +1,23 @@ import { z } from 'zod'; const envSchema = z.object({ - PORT: z.string().optional(), - NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), - CLOUDFLARE_BUCKET_DOMAIN: z.string().optional(), CLOUDFLARE_ACCESS_KEY: z.string().optional(), - CLOUDFLARE_SECRET_ACCESS_KEY: z.string().optional(), CLOUDFLARE_ACCOUNT_ID: z.string().optional(), - PUBLIC_SUPABASE_URL: z.string(), + CLOUDFLARE_BUCKET_DOMAIN: z.string().optional(), + CLOUDFLARE_RENDERING_API_KEY: z.string().optional(), + CLOUDFLARE_SECRET_ACCESS_KEY: z.string().optional(), + NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), + OPENAPI_URL: z.string().optional(), + PORT: z.string().optional(), PUBLIC_SUPABASE_ANON_KEY: z.string(), - ZOHO_TOKEN: z.string().optional(), + PUBLIC_SUPABASE_URL: z.string(), + REDIS_URL: z.string().optional(), SMTP_HOST: z.string().optional(), - SMTP_PORT: z.string().optional(), - SMTP_USER: z.string().optional(), SMTP_PASSWORD: z.string().optional(), - SMTP_SENDER: z.string().optional() + SMTP_PORT: z.string().optional(), + SMTP_SENDER: z.string().optional(), + SMTP_USER: z.string().optional(), + ZOHO_TOKEN: z.string().optional() }); export const env = envSchema.parse(process.env); diff --git a/apps/backend/src/constants/index.ts b/apps/api/src/constants/index.ts similarity index 76% rename from apps/backend/src/constants/index.ts rename to apps/api/src/constants/index.ts index 9db00bc9..f719cb76 100644 --- a/apps/backend/src/constants/index.ts +++ b/apps/api/src/constants/index.ts @@ -1,16 +1,20 @@ import { env } from '$src/config/env'; +export * from './rate-limiter'; + const { CLOUDFLARE_BUCKET_DOMAIN, CLOUDFLARE_ACCESS_KEY, CLOUDFLARE_SECRET_ACCESS_KEY, - CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_ACCOUNT_ID, + CLOUDFLARE_RENDERING_API_KEY } = env; export const CLOUDFLARE = { CONFIGS: { BUCKET_DOMAIN: CLOUDFLARE_BUCKET_DOMAIN!, ACCESS_KEY: CLOUDFLARE_ACCESS_KEY!, + RENDERING_API_KEY: CLOUDFLARE_RENDERING_API_KEY!, SECRET_ACCESS_KEY: CLOUDFLARE_SECRET_ACCESS_KEY!, ACCOUNT_ID: CLOUDFLARE_ACCOUNT_ID! }, @@ -20,6 +24,6 @@ export const CLOUDFLARE = { ? `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com` : CLOUDFLARE_BUCKET_DOMAIN!, PRESIGN_EXPIRATION_TIME: 60 * 60, // 1 hour - DOWNLOAD_EXPIRATION_TIME: 60 * 60 * 3 // 3 hours + DOWNLOAD_EXPIRATION_TIME: 60 * 60 // 1 hour } } as const; diff --git a/apps/api/src/constants/rate-limiter.ts b/apps/api/src/constants/rate-limiter.ts new file mode 100644 index 00000000..e49f2b15 --- /dev/null +++ b/apps/api/src/constants/rate-limiter.ts @@ -0,0 +1,32 @@ +// Default time window for rate limiting (1 minute in milliseconds) +export const DEFAULT_WINDOW_MS = 60 * 1000; + +export const DEFAULT_MAX_REQUESTS = 100; + +export const RATE_LIMIT_KEY_PREFIX = 'rate_limit:'; + +export const DEFAULT_RATE_LIMITER_OPTIONS = { + windowMs: DEFAULT_WINDOW_MS, + maxRequests: DEFAULT_MAX_REQUESTS, + skipSuccessfulRequests: false, + skipFailedRequests: false, + message: 'Rate limit exceeded. Please try again later.', + standardHeaders: true, + legacyHeaders: false +} as const; + +export const HTTP_STATUS = { + TOO_MANY_REQUESTS: 429 +} as const; + +export const ERROR_MESSAGES = { + RATE_LIMIT_EXCEEDED: 'Too Many Requests', + REDIS_PIPELINE_FAILED: 'Redis pipeline execution failed' +} as const; + +export const RATE_LIMIT_HEADERS = { + LIMIT: 'X-RateLimit-Limit', + REMAINING: 'X-RateLimit-Remaining', + RESET: 'X-RateLimit-Reset', + RETRY_AFTER: 'Retry-After' +} as const; diff --git a/apps/backend/src/constants/upload.ts b/apps/api/src/constants/upload.ts similarity index 100% rename from apps/backend/src/constants/upload.ts rename to apps/api/src/constants/upload.ts diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 3191383b..2efb1044 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,9 +1,22 @@ -import { Hono } from 'hono' +import 'dotenv/config'; -const app = new Hono() +import { app } from '$src/app'; +import { configureOpenAPI } from '$src/utils/openapi'; +import { env } from '$src/config/env'; +import { serve } from '@hono/node-server'; +import { showRoutes } from 'hono/dev'; -app.get('/', (c) => { - return c.text('Hello Hono!') -}) +// Start server +const port = env.PORT ? parseInt(env.PORT) : 3002; -export default app +function startServer() { + console.log('Starting server...'); + + serve({ fetch: app.fetch, port }); + + showRoutes(app, { colorize: true }); +} + +configureOpenAPI(app); + +startServer(); diff --git a/apps/backend/src/middlewares/auth.ts b/apps/api/src/middlewares/auth.ts similarity index 100% rename from apps/backend/src/middlewares/auth.ts rename to apps/api/src/middlewares/auth.ts diff --git a/apps/api/src/middlewares/rate-limiter.ts b/apps/api/src/middlewares/rate-limiter.ts new file mode 100644 index 00000000..e4efabb2 --- /dev/null +++ b/apps/api/src/middlewares/rate-limiter.ts @@ -0,0 +1,79 @@ +import { + DEFAULT_RATE_LIMITER_OPTIONS, + ERROR_MESSAGES, + HTTP_STATUS, + RATE_LIMIT_HEADERS +} from '$src/constants/rate-limiter'; + +import type { MiddlewareHandler } from 'hono'; +import { rateLimiter } from '$src/utils/redis/limiter'; +import { userKeyGenerator } from '../utils/redis/key-generators'; + +export interface RateLimiterOptions { + windowMs?: number; + maxRequests?: number; + keyGenerator?: (c: any) => string; + skipSuccessfulRequests?: boolean; + skipFailedRequests?: boolean; + message?: string; + standardHeaders?: boolean; + legacyHeaders?: boolean; +} + +const defaultOptions: Required = { + ...DEFAULT_RATE_LIMITER_OPTIONS, + keyGenerator: userKeyGenerator +}; + +export const createRateLimiter = (options: RateLimiterOptions = {}): MiddlewareHandler => { + const opts = { ...defaultOptions, ...options }; + + return async (c, next) => { + try { + // Generate rate limit key + const key = opts.keyGenerator(c); + + // Check rate limit + const result = await rateLimiter.isAllowed(key); + + // Set rate limit headers + if (opts.standardHeaders) { + c.header(RATE_LIMIT_HEADERS.LIMIT, opts.maxRequests.toString()); + c.header(RATE_LIMIT_HEADERS.REMAINING, result.remaining.toString()); + c.header(RATE_LIMIT_HEADERS.RESET, new Date(result.resetTime).toISOString()); + } + + if (opts.legacyHeaders) { + c.header(RATE_LIMIT_HEADERS.LIMIT, opts.maxRequests.toString()); + c.header(RATE_LIMIT_HEADERS.REMAINING, result.remaining.toString()); + c.header(RATE_LIMIT_HEADERS.RESET, Math.ceil(result.resetTime / 1000).toString()); + } + + if (!result.allowed) { + const retryAfter = Math.ceil((result.resetTime - Date.now()) / 1000); + + if (opts.standardHeaders) { + c.header(RATE_LIMIT_HEADERS.RETRY_AFTER, retryAfter.toString()); + } + + return c.json( + { + error: ERROR_MESSAGES.RATE_LIMIT_EXCEEDED, + message: opts.message, + retryAfter + }, + HTTP_STATUS.TOO_MANY_REQUESTS + ); + } + + await next(); + } catch (error) { + console.error('Rate limiter error:', error); + // If Redis is down, allow the request but log the error + await next(); + } + }; +}; + +// Default rate limiter middleware with standard configuration +export const rateLimiterMiddleware = createRateLimiter(); diff --git a/apps/api/src/routes/course/course.ts b/apps/api/src/routes/course/course.ts new file mode 100644 index 00000000..5c0dc830 --- /dev/null +++ b/apps/api/src/routes/course/course.ts @@ -0,0 +1,50 @@ +import { ZCertificateDownload, ZCourseDownloadContent } from '$src/types/course'; + +import { Hono } from 'hono'; +import { generateCertificate } from '$src/utils/certificate'; +import { generateCoursePdf } from '$src/utils/course'; +import { katexRouter } from '$src/routes/course/katex'; +import { lessonRouter } from '$src/routes/course/lesson'; +import { presignRouter } from '$src/routes/course/presign'; +import { zValidator } from '@hono/zod-validator'; + +export const courseRouter = new Hono() + .post('/download/certificate', zValidator('json', ZCertificateDownload), async (c) => { + const validatedData = c.req.valid('json'); + + const result = await generateCertificate(validatedData); + + c.header('Content-Type', 'application/pdf'); + c.header( + 'Content-Disposition', + `attachment; filename="certificate-${validatedData.courseName}.pdf"` + ); + + return c.body( + new ReadableStream({ + start(controller) { + controller.enqueue(result); + controller.close(); + } + }) + ); + }) + .post('/download/content', zValidator('json', ZCourseDownloadContent), async (c) => { + const validatedData = c.req.valid('json'); + + const pdfBuffer = await generateCoursePdf(validatedData); + + c.header('Content-Type', 'application/pdf'); + + return c.body( + new ReadableStream({ + start(controller) { + controller.enqueue(pdfBuffer); + controller.close(); + } + }) + ); + }) + .route('/katex', katexRouter) + .route('/lesson', lessonRouter) + .route('/presign', presignRouter); diff --git a/apps/backend/src/routes/course/katex.ts b/apps/api/src/routes/course/katex.ts similarity index 87% rename from apps/backend/src/routes/course/katex.ts rename to apps/api/src/routes/course/katex.ts index 49f8d70e..d5b65ad0 100644 --- a/apps/backend/src/routes/course/katex.ts +++ b/apps/api/src/routes/course/katex.ts @@ -3,9 +3,7 @@ import { Context, Hono } from 'hono'; import katex from 'katex'; import { z } from 'zod'; -export const katexRouter = new Hono(); - -katexRouter.get('/', async (c: Context) => { +export const katexRouter = new Hono().get('/', async (c: Context) => { try { const original = c.req.raw.url; @@ -23,7 +21,7 @@ katexRouter.get('/', async (c: Context) => { { success: false, error: 'Validation error', - details: error.errors + details: error.message }, 400 ); diff --git a/apps/api/src/routes/course/lesson.ts b/apps/api/src/routes/course/lesson.ts new file mode 100644 index 00000000..e380a627 --- /dev/null +++ b/apps/api/src/routes/course/lesson.ts @@ -0,0 +1,24 @@ +import { Hono } from 'hono'; +import { ZLessonDownloadContent } from '$src/types/course/lesson'; +import { generateLessonPdf } from '$src/utils/lesson'; +import { zValidator } from '@hono/zod-validator'; + +export const lessonRouter = new Hono().post( + '/download/pdf', + zValidator('json', ZLessonDownloadContent), + async (c) => { + const validatedData = c.req.valid('json'); + + const pdfBuffer = await generateLessonPdf(validatedData); + + c.header('Content-Type', 'application/pdf'); + return c.body( + new ReadableStream({ + start(controller) { + controller.enqueue(pdfBuffer); + controller.close(); + } + }) + ); + } +); diff --git a/apps/api/src/routes/course/presign.ts b/apps/api/src/routes/course/presign.ts new file mode 100644 index 00000000..20519806 --- /dev/null +++ b/apps/api/src/routes/course/presign.ts @@ -0,0 +1,253 @@ +import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; +import { + ZCourseDocumentPresignUrlUpload, + ZCourseDownloadPresignedUrl, + ZCoursePresignUrlUpload +} from '$src/types/course'; +import { describeRoute, validator } from 'hono-openapi'; + +import { BUCKET_NAME } from '$src/constants/upload'; +import { CLOUDFLARE } from '$src/constants'; +import type { GetSignedUrlParameters } from '$src/utils/s3'; +import { Hono } from 'hono'; +import { authMiddleware } from '$src/middlewares/auth'; +import { generateFileKey } from '$src/utils/upload'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { s3Client } from '$src/utils/s3'; + +// Response schemas for OpenAPI documentation +const PresignUploadResponse = { + type: 'object' as const, + properties: { + success: { type: 'boolean' as const }, + url: { type: 'string' as const }, + fileKey: { type: 'string' as const }, + message: { type: 'string' as const } + }, + required: ['success', 'url', 'fileKey', 'message'] +}; + +const PresignDownloadResponse = { + type: 'object' as const, + properties: { + success: { type: 'boolean' as const }, + urls: { + type: 'object' as const, + additionalProperties: { type: 'string' as const } + }, + message: { type: 'string' as const } + }, + required: ['success', 'urls', 'message'] +}; + +export const presignRouter = new Hono() + .post( + '/video/upload', + authMiddleware, + describeRoute({ + description: 'Generate a pre-signed URL for video upload', + responses: { + 200: { + description: 'Pre-signed URL generated successfully', + content: { + 'application/json': { + schema: PresignUploadResponse + } + } + }, + 400: { + description: 'Invalid request body' + }, + 401: { + description: 'Unauthorized' + } + }, + tags: ['Presign'] + }), + validator('json', ZCoursePresignUrlUpload), + async (c) => { + const body = c.req.valid('json'); + + const { fileName, fileType } = body; + const fileKey = generateFileKey(fileName); + + const command = new PutObjectCommand({ + Bucket: BUCKET_NAME.VIDEOS, + Key: fileKey, + ContentType: fileType + }) as GetSignedUrlParameters[1]; + + const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, { + expiresIn: CLOUDFLARE.R2.PRESIGN_EXPIRATION_TIME + }); + + return c.json({ + success: true, + url: presignedUrl, + fileKey, + message: 'Pre-signed URL generated successfully' + }); + } + ) + .post( + '/document/upload', + authMiddleware, + describeRoute({ + description: 'Generate a pre-signed URL for document upload', + responses: { + 200: { + description: 'Document pre-signed URL generated successfully', + content: { + 'application/json': { + schema: PresignUploadResponse + } + } + }, + 400: { + description: 'Invalid request body' + }, + 401: { + description: 'Unauthorized' + } + }, + tags: ['Presign'] + }), + validator('json', ZCourseDocumentPresignUrlUpload), + async (c) => { + const body = c.req.valid('json'); + + const { fileName, fileType } = body; + const fileKey = generateFileKey(fileName); + + const command = new PutObjectCommand({ + Bucket: BUCKET_NAME.DOCUMENTS, + Key: fileKey, + ContentType: fileType + }) as GetSignedUrlParameters[1]; + + const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, { + expiresIn: CLOUDFLARE.R2.PRESIGN_EXPIRATION_TIME + }); + + return c.json({ + success: true, + url: presignedUrl, + fileKey, + message: 'Document pre-signed URL generated successfully' + }); + } + ) + .post( + '/video/download', + authMiddleware, + describeRoute({ + description: 'Generate pre-signed URLs for video download', + responses: { + 200: { + description: 'Video URLs retrieved successfully', + content: { + 'application/json': { + schema: PresignDownloadResponse + } + } + }, + 400: { + description: 'Invalid request body' + }, + 401: { + description: 'Unauthorized' + } + }, + tags: ['Presign'] + }), + validator('json', ZCourseDownloadPresignedUrl), + async (c) => { + const body = c.req.valid('json'); + + const { keys } = body; + + const signedUrls: Record = {}; + + const urlPromises = keys.map(async (key) => { + const command = new GetObjectCommand({ + Bucket: BUCKET_NAME.VIDEOS, + Key: key + }) as GetSignedUrlParameters[1]; + + const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, { + expiresIn: CLOUDFLARE.R2.DOWNLOAD_EXPIRATION_TIME + }); + + return { key, presignedUrl }; + }); + + const results = await Promise.all(urlPromises); + + results.forEach(({ key, presignedUrl }) => { + signedUrls[key] = presignedUrl; + }); + + return c.json({ + success: true, + urls: signedUrls, + message: 'Video URLs retrieved successfully' + }); + } + ) + .post( + '/document/download', + authMiddleware, + describeRoute({ + description: 'Generate pre-signed URLs for document download', + responses: { + 200: { + description: 'Document URLs retrieved successfully', + content: { + 'application/json': { + schema: PresignDownloadResponse + } + } + }, + 400: { + description: 'Invalid request body' + }, + 401: { + description: 'Unauthorized' + } + }, + tags: ['Presign'] + }), + validator('json', ZCourseDownloadPresignedUrl), + async (c) => { + const body = c.req.valid('json'); + + const { keys } = body; + + const signedUrls: Record = {}; + + const urlPromises = keys.map(async (key) => { + const command = new GetObjectCommand({ + Bucket: BUCKET_NAME.DOCUMENTS, + Key: key + }) as GetSignedUrlParameters[1]; + + const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, { + expiresIn: CLOUDFLARE.R2.DOWNLOAD_EXPIRATION_TIME + }); + + return { key, presignedUrl }; + }); + + const results = await Promise.all(urlPromises); + + results.forEach(({ key, presignedUrl }) => { + signedUrls[key] = presignedUrl; + }); + + return c.json({ + success: true, + urls: signedUrls, + message: 'Document URLs retrieved successfully' + }); + } + ); diff --git a/apps/backend/src/routes/mail.ts b/apps/api/src/routes/mail.ts similarity index 59% rename from apps/backend/src/routes/mail.ts rename to apps/api/src/routes/mail.ts index 539886db..53deeb88 100644 --- a/apps/backend/src/routes/mail.ts +++ b/apps/api/src/routes/mail.ts @@ -1,16 +1,15 @@ -import { Context, Hono } from 'hono'; import { sendWithNodemailer, sendWithZoho } from '$src/services/mail'; +import { Hono } from 'hono'; import { ZSendEmailValidation } from '$src/types/mail'; import { env } from '$src/config/env'; -import { z } from 'zod'; +import { zValidator } from '@hono/zod-validator'; -export const mailRouter = new Hono(); - -mailRouter.post('/send', async (c: Context) => { - try { - const data = await c.req.json(); - const validatedData = ZSendEmailValidation.parse(data); +export const mailRouter = new Hono().post( + '/send', + zValidator('json', ZSendEmailValidation), + async (c) => { + const validatedData = c.req.valid('json'); const results = await Promise.all( validatedData.map(async (emailData) => { @@ -45,26 +44,5 @@ mailRouter.post('/send', async (c: Context) => { } return c.json({ success: true, details: results }); - } catch (error) { - if (error instanceof z.ZodError) { - return c.json( - { - success: false, - error: 'Validation error', - details: error.errors - }, - 400 - ); - } - - console.error('Error sending emails:', error); - return c.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - details: error - }, - 500 - ); } -}); +); diff --git a/apps/api/src/rpc-types.ts b/apps/api/src/rpc-types.ts new file mode 100644 index 00000000..0e526f63 --- /dev/null +++ b/apps/api/src/rpc-types.ts @@ -0,0 +1,9 @@ +import { app } from './app'; +import { hc } from 'hono/client'; +import type { InferRequestType, InferResponseType } from 'hono/client'; + +export type Client = ReturnType>; + +export const hcWithType = (...args: Parameters): Client => hc(...args); + +export type { InferRequestType, InferResponseType }; diff --git a/apps/backend/src/services/mail.ts b/apps/api/src/services/mail.ts similarity index 100% rename from apps/backend/src/services/mail.ts rename to apps/api/src/services/mail.ts diff --git a/apps/backend/src/types/course/index.ts b/apps/api/src/types/course/index.ts similarity index 90% rename from apps/backend/src/types/course/index.ts rename to apps/api/src/types/course/index.ts index 5fb6c1ec..f26086a7 100644 --- a/apps/backend/src/types/course/index.ts +++ b/apps/api/src/types/course/index.ts @@ -1,8 +1,4 @@ -import { - ALLOWED_CONTENT_TYPES, - ALLOWED_DOCUMENT_TYPES, - MAX_FILE_SIZE -} from '$src/constants/upload'; +import { ALLOWED_CONTENT_TYPES, ALLOWED_DOCUMENT_TYPES } from '$src/constants/upload'; import { z } from 'zod'; @@ -28,7 +24,7 @@ export type TCourseDownloadContent = z.infer; export const ZCoursePresignUrlUpload = z.object({ fileName: z.string().min(1, 'File name is required'), fileType: z.enum(ALLOWED_CONTENT_TYPES, { - errorMap: () => ({ + error: () => ({ message: `Invalid content type. Allowed types: ${ALLOWED_CONTENT_TYPES.join(', ')}` }) }) @@ -39,7 +35,7 @@ export type TCoursePresignUrlUpload = z.infer; export const ZCourseDocumentPresignUrlUpload = z.object({ fileName: z.string().min(1, 'File name is required'), fileType: z.enum(ALLOWED_DOCUMENT_TYPES, { - errorMap: () => ({ + error: () => ({ message: `Invalid content type. Allowed types: ${ALLOWED_DOCUMENT_TYPES.join(', ')}` }) }) diff --git a/apps/backend/src/types/course/lesson.ts b/apps/api/src/types/course/lesson.ts similarity index 100% rename from apps/backend/src/types/course/lesson.ts rename to apps/api/src/types/course/lesson.ts diff --git a/apps/backend/src/types/mail.ts b/apps/api/src/types/mail.ts similarity index 100% rename from apps/backend/src/types/mail.ts rename to apps/api/src/types/mail.ts diff --git a/apps/backend/src/utils/auth/validate-user.ts b/apps/api/src/utils/auth/validate-user.ts similarity index 89% rename from apps/backend/src/utils/auth/validate-user.ts rename to apps/api/src/utils/auth/validate-user.ts index 9ea20f2f..a96cc85f 100644 --- a/apps/backend/src/utils/auth/validate-user.ts +++ b/apps/api/src/utils/auth/validate-user.ts @@ -1,5 +1,5 @@ import { User } from '@supabase/supabase-js'; -import { getSupabase } from '../supabase.js'; +import { getSupabase } from '../supabase'; const supabase = getSupabase(); diff --git a/apps/backend/src/utils/certificate.ts b/apps/api/src/utils/certificate.ts similarity index 100% rename from apps/backend/src/utils/certificate.ts rename to apps/api/src/utils/certificate.ts diff --git a/apps/backend/src/utils/cloudflare.ts b/apps/api/src/utils/cloudflare.ts similarity index 82% rename from apps/backend/src/utils/cloudflare.ts rename to apps/api/src/utils/cloudflare.ts index 9d480803..6a1e55ed 100644 --- a/apps/backend/src/utils/cloudflare.ts +++ b/apps/api/src/utils/cloudflare.ts @@ -9,7 +9,7 @@ export const getCloudflarePdfBuffer = async (html: string, styles?: string) => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${CLOUDFLARE.CONFIGS.ACCESS_KEY}` + Authorization: `Bearer ${CLOUDFLARE.CONFIGS.RENDERING_API_KEY}` }, body: JSON.stringify({ html: html, @@ -19,7 +19,8 @@ export const getCloudflarePdfBuffer = async (html: string, styles?: string) => { ); console.log('PDF response status:', pdfResponse.status); - return pdfResponse; + const arrayBuffer = await pdfResponse.arrayBuffer(); + return Buffer.from(arrayBuffer); } catch (error) { console.error('Error generating PDF:', error); throw new Error(error instanceof Error ? error.message : 'Failed to generate PDF'); diff --git a/apps/backend/src/utils/course.ts b/apps/api/src/utils/course.ts similarity index 97% rename from apps/backend/src/utils/course.ts rename to apps/api/src/utils/course.ts index 200b2261..52edcd8a 100644 --- a/apps/backend/src/utils/course.ts +++ b/apps/api/src/utils/course.ts @@ -1,4 +1,5 @@ import type { TCourseDownloadContent } from '$src/types/course'; +import { getCloudflarePdfBuffer } from '$src/utils/cloudflare'; import { marked } from 'marked'; function getHtmlTemplate(body: string): string { @@ -159,7 +160,7 @@ async function generateSinglePdfFromHtml( courseTitle: string, orgTheme: string ): Promise { - throw 'Download disabled'; + return await getCloudflarePdfBuffer(htmlContent); } export async function generateCoursePdf(params: TCourseDownloadContent): Promise { diff --git a/apps/backend/src/utils/email.ts b/apps/api/src/utils/email.ts similarity index 100% rename from apps/backend/src/utils/email.ts rename to apps/api/src/utils/email.ts diff --git a/apps/backend/src/utils/genUniqueId.ts b/apps/api/src/utils/genUniqueId.ts similarity index 100% rename from apps/backend/src/utils/genUniqueId.ts rename to apps/api/src/utils/genUniqueId.ts diff --git a/apps/backend/src/utils/lesson.ts b/apps/api/src/utils/lesson.ts similarity index 100% rename from apps/backend/src/utils/lesson.ts rename to apps/api/src/utils/lesson.ts diff --git a/apps/backend/src/utils/mail.ts b/apps/api/src/utils/mail.ts similarity index 100% rename from apps/backend/src/utils/mail.ts rename to apps/api/src/utils/mail.ts diff --git a/apps/api/src/utils/openapi/index.ts b/apps/api/src/utils/openapi/index.ts new file mode 100644 index 00000000..323a635a --- /dev/null +++ b/apps/api/src/utils/openapi/index.ts @@ -0,0 +1,9 @@ +import { Hono } from 'hono'; +import { Scalar } from '@scalar/hono-api-reference'; +import { env } from '$src/config/env'; + +export function configureOpenAPI(app: Hono) { + if (env.OPENAPI_URL) { + app.get('/docs', Scalar({ url: env.OPENAPI_URL, theme: 'none' })); + } +} diff --git a/apps/api/src/utils/redis/key-generators.ts b/apps/api/src/utils/redis/key-generators.ts new file mode 100644 index 00000000..c18669b7 --- /dev/null +++ b/apps/api/src/utils/redis/key-generators.ts @@ -0,0 +1,105 @@ +/** + * Key generation utilities for rate limiting + * These functions help create consistent and secure rate limit keys + */ + +import type { Context } from 'hono'; +import { supabase } from '$src/utils/supabase'; + +/** + * Extract user ID from JWT token + * In a real implementation, you'd decode and verify the JWT + */ +export const extractUserIdFromToken = async (token: string): Promise => { + try { + const { data } = await supabase.auth.getUser(token); + return data.user?.id || null; + } catch { + return null; + } +}; + +/** + * Extract real client IP address considering various proxy headers + */ +export const extractClientIp = (c: Context): string => { + // Priority order (most trusted first): + // 1. CF-Connecting-IP (Cloudflare) + // 2. X-Real-IP (nginx) + // 3. X-Forwarded-For (standard, but can be spoofed) + // 4. Connection IP (least reliable) + + const cfConnectingIp = c.req.header('cf-connecting-ip'); + const realIp = c.req.header('x-real-ip'); + const forwardedFor = c.req.header('x-forwarded-for'); + + if (cfConnectingIp) { + return cfConnectingIp; + } + + if (realIp) { + return realIp; + } + + if (forwardedFor) { + // x-forwarded-for can contain multiple IPs: "client, proxy1, proxy2" + // The first IP is usually the original client + return forwardedFor.split(',')[0].trim(); + } + + // Fallback - this might be a proxy IP + return 'unknown'; +}; + +/** + * Generate rate limit key for authenticated users + */ +export const userKeyGenerator = (c: Context): string => { + const authHeader = c.req.header('authorization'); + + if (authHeader) { + const token = authHeader.split(' ')[1]; + if (token && token !== 'null' && token !== 'undefined') { + const userId = extractUserIdFromToken(token); + if (userId) { + return `user:${userId}`; + } + // Fallback to token if user ID extraction fails + return `token:${token}`; + } + } + + // No valid auth, fall back to IP + return `ip:${extractClientIp(c)}`; +}; + +/** + * Generate rate limit key for API keys + */ +export const apiKeyGenerator = (c: Context): string => { + const apiKey = c.req.header('X-API-Key'); + + if (apiKey) { + return `api:${apiKey}`; + } + + // Fallback to IP if no API key + return `ip:${extractClientIp(c)}`; +}; + +/** + * Generate rate limit key based on IP only + */ +export const ipKeyGenerator = (c: Context): string => { + return `ip:${extractClientIp(c)}`; +}; + +/** + * Generate rate limit key for specific endpoints + */ +export const endpointKeyGenerator = + (endpoint: string) => + (c: Context): string => { + const baseKey = userKeyGenerator(c); + return `${baseKey}:${endpoint}`; + }; diff --git a/apps/api/src/utils/redis/limiter.ts b/apps/api/src/utils/redis/limiter.ts new file mode 100644 index 00000000..63c02c6f --- /dev/null +++ b/apps/api/src/utils/redis/limiter.ts @@ -0,0 +1,71 @@ +import { + DEFAULT_MAX_REQUESTS, + DEFAULT_WINDOW_MS, + ERROR_MESSAGES, + RATE_LIMIT_KEY_PREFIX +} from '$src/constants/rate-limiter'; + +import { Redis } from 'ioredis'; +import { redis } from './redis'; + +export class RedisRateLimiter { + private redis: Redis; + private windowMs: number; + private maxRequests: number; + + constructor( + redis: Redis, + windowMs: number = DEFAULT_WINDOW_MS, + maxRequests: number = DEFAULT_MAX_REQUESTS + ) { + this.redis = redis; + this.windowMs = windowMs; + this.maxRequests = maxRequests; + } + + async isAllowed( + key: string + ): Promise<{ allowed: boolean; remaining: number; resetTime: number }> { + const now = Date.now(); + const windowStart = now - this.windowMs; + const keyWithPrefix = `${RATE_LIMIT_KEY_PREFIX}${key}`; + + // Use Redis pipeline for atomic operations + const pipeline = this.redis.pipeline(); + + // Remove expired entries + pipeline.zremrangebyscore(keyWithPrefix, '-inf', windowStart); + + // Count current requests in window + pipeline.zcard(keyWithPrefix); + + // Add current request + pipeline.zadd(keyWithPrefix, now, `${now}-${Math.random()}`); + + // Set expiration for the key + pipeline.expire(keyWithPrefix, Math.ceil(this.windowMs / 1000)); + + const results = await pipeline.exec(); + + if (!results || results.some((result) => result[0] !== null)) { + throw new Error(ERROR_MESSAGES.REDIS_PIPELINE_FAILED); + } + + const currentCount = results[1][1] as number; + const remaining = Math.max(0, this.maxRequests - currentCount - 1); + const resetTime = now + this.windowMs; + + return { + allowed: currentCount < this.maxRequests, + remaining, + resetTime + }; + } + + async reset(key: string): Promise { + const keyWithPrefix = `${RATE_LIMIT_KEY_PREFIX}${key}`; + await this.redis.del(keyWithPrefix); + } +} + +export const rateLimiter = new RedisRateLimiter(redis); diff --git a/apps/api/src/utils/redis/redis.ts b/apps/api/src/utils/redis/redis.ts new file mode 100644 index 00000000..785e4be6 --- /dev/null +++ b/apps/api/src/utils/redis/redis.ts @@ -0,0 +1,4 @@ +import { Redis } from 'ioredis'; +import { env } from '$src/config/env'; + +export const redis = new Redis(env.REDIS_URL); diff --git a/apps/backend/src/utils/s3.ts b/apps/api/src/utils/s3.ts similarity index 100% rename from apps/backend/src/utils/s3.ts rename to apps/api/src/utils/s3.ts diff --git a/apps/backend/src/utils/supabase.ts b/apps/api/src/utils/supabase.ts similarity index 93% rename from apps/backend/src/utils/supabase.ts rename to apps/api/src/utils/supabase.ts index cb0e461f..1789b49c 100644 --- a/apps/backend/src/utils/supabase.ts +++ b/apps/api/src/utils/supabase.ts @@ -4,7 +4,7 @@ import { env } from '$src/config/env'; const { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } = env; -let supabase: SupabaseClient | null = null; +export let supabase: SupabaseClient | null = null; interface SupabaseConfig { supabaseConfig: { diff --git a/apps/backend/src/utils/upload.ts b/apps/api/src/utils/upload.ts similarity index 100% rename from apps/backend/src/utils/upload.ts rename to apps/api/src/utils/upload.ts diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 934f03cc..0dfe20e6 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -1,17 +1,28 @@ { "compilerOptions": { - "target": "ESNext", + "allowSyntheticDefaultImports": true, + "baseUrl": "./", + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, "module": "ESNext", "moduleResolution": "Bundler", - "strict": true, + "outDir": "./dist", + "paths": { + "$src": ["./src"], + "$src/*": ["./src/*"] + }, + "resolveJsonModule": false, "skipLibCheck": true, - "lib": [ - "ESNext" - ], - "types": [ - "@cloudflare/workers-types/2023-07-01" - ], - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx" + "sourceMap": true, + "target": "ESNext", + "typeRoots": ["./node_modules/@types", "./src/types"], + "types": ["node"] }, -} \ No newline at end of file + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "tsc-alias": { + "resolveFullPaths": true, + "verbose": false + } +} diff --git a/apps/backend/vitest.config.ts b/apps/api/vitest.config.ts similarity index 100% rename from apps/backend/vitest.config.ts rename to apps/api/vitest.config.ts diff --git a/apps/api/wrangler.toml b/apps/api/wrangler.toml deleted file mode 100644 index 9043a2d6..00000000 --- a/apps/api/wrangler.toml +++ /dev/null @@ -1,28 +0,0 @@ -name = "api" -main = "src/index.ts" -compatibility_date = "2025-01-16" - -# compatibility_flags = [ "nodejs_compat" ] - -# [vars] -# MY_VAR = "my-variable" - -# [[kv_namespaces]] -# binding = "MY_KV_NAMESPACE" -# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - -# [[r2_buckets]] -# binding = "MY_BUCKET" -# bucket_name = "my-bucket" - -# [[d1_databases]] -# binding = "DB" -# database_name = "my-database" -# database_id = "" - -# [ai] -# binding = "AI" - -# [observability] -# enabled = true -# head_sampling_rate = 1 \ No newline at end of file diff --git a/apps/backend/.gitignore b/apps/backend/.gitignore deleted file mode 100644 index 80647510..00000000 --- a/apps/backend/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -.env -.DS_Store -dist \ No newline at end of file diff --git a/apps/backend/README.md b/apps/backend/README.md deleted file mode 100644 index 92072dbc..00000000 --- a/apps/backend/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# ClassroomIO Backend - -## Supported Features - -1. Certificate download for students. -2. Uploading videos to cloudflare (for storage) and Muse.ai (for transcription) - -## Debugging Fly.io - -View logs from fly.io in real time diff --git a/apps/backend/package.json b/apps/backend/package.json deleted file mode 100644 index 5e97a0d3..00000000 --- a/apps/backend/package.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "backend", - "version": "0.0.1", - "description": "ClassroomIO Backend for long processes", - "main": "dist/index.js", - "type": "module", - "scripts": { - "build": "tsc && tsc-alias", - "dev": "tsx watch src/index.ts", - "start": "node dist/index.js", - "test": "vitest", - "test:coverage": "vitest run --coverage", - "lint": "eslint . --ext .ts", - "format": "prettier --write \"src/**/*.ts\"" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/classroomio/classroomio.git" - }, - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/classroomio/classroomio/issues" - }, - "homepage": "https://github.com/classroomio/classroomio#readme", - "dependencies": { - "@aws-sdk/client-s3": "^3.388.0", - "@aws-sdk/s3-request-presigner": "^3.675.0", - "@hono/node-server": "^1.8.2", - "@sentry/node": "^7.110.1", - "@sentry/profiling-node": "^7.110.1", - "@supabase/supabase-js": "^2.32.0", - "axios": "^1.4.0", - "dotenv": "^16.3.1", - "hono": "^4.0.5", - "hono-rate-limiter": "^0.4.2", - "katex": "^0.16.10", - "marked": "^7.0.3", - "nanoid": "^5.1.5", - "nodemailer": "^6.9.5", - "zeptomail": "^6.0.0", - "zod": "^3.21.4" - }, - "devDependencies": { - "@types/katex": "^0.16.7", - "@types/node": "^20.11.19", - "@types/nodemailer": "^6.4.17", - "@typescript-eslint/eslint-plugin": "7.0.1", - "@typescript-eslint/parser": "7.0.1", - "@vitest/coverage-v8": "^1.2.2", - "eslint": "^9.31.0", - "prettier": "^3.2.5", - "ts-node": "^10.9.2", - "tsc-alias": "^1.8.16", - "tsx": "^4.7.1", - "typescript": "^5.5.2", - "vitest": "^1.2.2" - }, - "_moduleAliases": { - "$src": "src" - } -} diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts deleted file mode 100644 index e30e06c6..00000000 --- a/apps/backend/src/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -import 'dotenv/config'; - -import { Hono } from 'hono'; -import { cors } from 'hono/cors'; -import { courseRouter } from '$src/routes/course/course'; -import { env } from '$src/config/env'; -import { logger } from 'hono/logger'; -import { mailRouter } from '$src/routes/mail'; -import { prettyJSON } from 'hono/pretty-json'; -import { rateLimiter } from 'hono-rate-limiter'; -import { secureHeaders } from 'hono/secure-headers'; -import { serve } from '@hono/node-server'; -import { showRoutes } from 'hono/dev'; - -// Create Hono app -const app = new Hono(); - -// Middleware -app.use('*', logger()); -app.use('*', prettyJSON()); -app.use('*', secureHeaders()); -app.use( - '*', - cors({ - origin: '*', - allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowHeaders: ['Content-Type', 'Authorization'], - exposeHeaders: ['Content-Length', 'X-Kuma-Revision'], - maxAge: 600, - credentials: true - }) -); - -app.use( - rateLimiter({ - windowMs: 15 * 60 * 1000, // 15 minutes - limit: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes). - standardHeaders: 'draft-6', // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header - keyGenerator: (c) => c.req.header('Authorization')?.split(' ')[1] ?? 'unknown' // Method to generate custom identifiers for clients. - }) -); - -// Routes -app.get('/', (c) => c.json({ message: 'Welcome to ClassroomIO' })); - -app.route('/course', courseRouter); -app.route('/mail', mailRouter); - -// Error handling -app.onError((err, c) => { - console.error('Error:', err); - return c.json({ error: 'Internal Server Error' }, 500); -}); - -// Start server -const port = env.PORT ? parseInt(env.PORT) : 3002; - -function startServer() { - console.log('Starting server...'); - - serve({ fetch: app.fetch, port }); - - showRoutes(app, { colorize: true }); -} - -startServer(); diff --git a/apps/backend/src/routes/course/course.ts b/apps/backend/src/routes/course/course.ts deleted file mode 100644 index e078d9e3..00000000 --- a/apps/backend/src/routes/course/course.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Context, Hono } from 'hono'; -import { ZCertificateDownload, ZCourseDownloadContent } from '$src/types/course'; - -import { generateCertificate } from '$src/utils/certificate'; -import { generateCoursePdf } from '$src/utils/course'; -import { katexRouter } from '$src/routes/course/katex'; -import { lessonRouter } from '$src/routes/course/lesson'; -import { presignRouter } from '$src/routes/course/presign'; -import { z } from 'zod'; - -export const courseRouter = new Hono(); - -courseRouter.route('/katex', katexRouter); -courseRouter.route('/lesson', lessonRouter); -courseRouter.route('/presign', presignRouter); - -courseRouter.post('/download/certificate', async (c: Context) => { - try { - const data = await c.req.json(); - - const validatedData = ZCertificateDownload.parse(data); - - const result = await generateCertificate(validatedData); - - c.header('Content-Type', 'application/pdf'); - c.header( - 'Content-Disposition', - `attachment; filename="certificate-${validatedData.courseName}.pdf"` - ); - - return c.body( - new ReadableStream({ - start(controller) { - controller.enqueue(result); - controller.close(); - } - }) - ); - } catch (error) { - if (error instanceof z.ZodError) { - return c.json( - { - success: false, - error: 'Validation error', - details: error.errors - }, - 400 - ); - } - - console.error('Error generating certificate:', error); - return c.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - details: error - }, - 500 - ); - } -}); - -courseRouter.post('/download/content', async (c: Context) => { - try { - const data = await c.req.json(); - const validatedData = ZCourseDownloadContent.parse(data); - - const pdfBuffer = await generateCoursePdf(validatedData); - - c.header('Content-Type', 'application/pdf'); - - return c.body( - new ReadableStream({ - start(controller) { - controller.enqueue(pdfBuffer); - controller.close(); - } - }) - ); - } catch (error) { - console.error('Error generating course PDF:', error); - return c.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - details: error - }, - 400 - ); - } -}); diff --git a/apps/backend/src/routes/course/lesson.ts b/apps/backend/src/routes/course/lesson.ts deleted file mode 100644 index 0638f85e..00000000 --- a/apps/backend/src/routes/course/lesson.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Context, Hono } from 'hono'; -import { ZCertificateDownload, ZLessonDownloadContent } from '$src/types/course/lesson'; - -import { generateLessonPdf } from '$src/utils/lesson'; - -export const lessonRouter = new Hono(); - -lessonRouter.post('/download/pdf', async (c: Context) => { - try { - const data = await c.req.json(); - const validatedData = ZLessonDownloadContent.parse(data); - - const pdfBuffer = await generateLessonPdf(validatedData); - - c.header('Content-Type', 'application/pdf'); - return c.body( - new ReadableStream({ - start(controller) { - controller.enqueue(pdfBuffer); - controller.close(); - } - }) - ); - } catch (error) { - console.error('Error generating lesson PDF:', error); - return c.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - details: error - }, - 400 - ); - } -}); diff --git a/apps/backend/src/routes/course/presign.ts b/apps/backend/src/routes/course/presign.ts deleted file mode 100644 index 15523e47..00000000 --- a/apps/backend/src/routes/course/presign.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; -import { - ZCourseDocumentPresignUrlUpload, - ZCourseDownloadPresignedUrl, - ZCoursePresignUrlUpload -} from '$src/types/course'; - -import { BUCKET_NAME } from '$src/constants/upload'; -import { CLOUDFLARE } from '$src/constants'; -import type { GetSignedUrlParameters } from '$src/utils/s3'; -import { Hono } from 'hono'; -import { authMiddleware } from '$src/middlewares/auth'; -import { generateFileKey } from '$src/utils/upload'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { s3Client } from '$src/utils/s3'; - -export const presignRouter = new Hono(); - -presignRouter.post('/upload', authMiddleware, async (c) => { - const body = await c.req.json(); - const result = ZCoursePresignUrlUpload.parse(body); - - const { fileName, fileType } = result; - const fileKey = generateFileKey(fileName); - - try { - const command = new PutObjectCommand({ - Bucket: BUCKET_NAME.VIDEOS, - Key: fileKey, - ContentType: fileType - }) as GetSignedUrlParameters[1]; - - const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, { - expiresIn: CLOUDFLARE.R2.PRESIGN_EXPIRATION_TIME - }); - - return c.json({ - success: true, - url: presignedUrl, - fileKey, - message: 'Pre-signed URL generated successfully' - }); - } catch (error) { - console.error('Error generating pre-signed URL:', error); - - if (error instanceof Error && error.name === 'SignatureDoesNotMatch') { - return c.json( - { - success: false, - type: 'SIGNATURE_ERROR', - message: 'Invalid signature for the request' - }, - 403 - ); - } - - return c.json( - { - success: false, - type: 'INTERNAL_ERROR', - message: 'Error generating pre-signed URL' - }, - 500 - ); - } -}); - -presignRouter.post('/document/upload', authMiddleware, async (c) => { - const body = await c.req.json(); - const result = ZCourseDocumentPresignUrlUpload.parse(body); - - const { fileName, fileType } = result; - const fileKey = generateFileKey(fileName); - - try { - const command = new PutObjectCommand({ - Bucket: BUCKET_NAME.DOCUMENTS, - Key: fileKey, - ContentType: fileType - }) as GetSignedUrlParameters[1]; - - const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, { - expiresIn: CLOUDFLARE.R2.PRESIGN_EXPIRATION_TIME - }); - - return c.json({ - success: true, - url: presignedUrl, - fileKey, - message: 'Document pre-signed URL generated successfully' - }); - } catch (error) { - console.error('Error generating document pre-signed URL:', error); - - if (error instanceof Error && error.name === 'SignatureDoesNotMatch') { - return c.json( - { - success: false, - type: 'SIGNATURE_ERROR', - message: 'Invalid signature for the request' - }, - 403 - ); - } - - return c.json( - { - success: false, - type: 'INTERNAL_ERROR', - message: 'Error generating document pre-signed URL' - }, - 500 - ); - } -}); - -presignRouter.post('/download', authMiddleware, async (c) => { - const body = await c.req.json(); - const result = ZCourseDownloadPresignedUrl.parse(body); - - const { keys } = result; - - try { - const signedUrls: Record = {}; - - await Promise.all( - keys.map(async (key) => { - const command = new GetObjectCommand({ - Bucket: BUCKET_NAME.VIDEOS, - Key: key - }) as GetSignedUrlParameters[1]; - - const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, { - expiresIn: CLOUDFLARE.R2.DOWNLOAD_EXPIRATION_TIME - }); - - signedUrls[key] = presignedUrl; - }) - ); - - return c.json({ - success: true, - urls: signedUrls, - message: 'Video URLs retrieved successfully' - }); - } catch (error) { - console.error('Error Retrieving Video:', error); - return c.json( - { - success: false, - type: 'INTERNAL_ERROR', - message: 'Error Retrieving Video' - }, - 500 - ); - } -}); - -presignRouter.post('/download/document', authMiddleware, async (c) => { - const body = await c.req.json(); - const result = ZCourseDownloadPresignedUrl.parse(body); - - const { keys } = result; - - try { - const signedUrls: Record = {}; - - await Promise.all( - keys.map(async (key) => { - const command = new GetObjectCommand({ - Bucket: BUCKET_NAME.DOCUMENTS, - Key: key - }) as GetSignedUrlParameters[1]; - - const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, { - expiresIn: CLOUDFLARE.R2.DOWNLOAD_EXPIRATION_TIME - }); - - signedUrls[key] = presignedUrl; - }) - ); - - return c.json({ - success: true, - urls: signedUrls, - message: 'Document URLs retrieved successfully' - }); - } catch (error) { - console.error('Error Retrieving Document:', error); - return c.json( - { - success: false, - type: 'INTERNAL_ERROR', - message: 'Error Retrieving Document' - }, - 500 - ); - } -}); diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json deleted file mode 100644 index eb9e8baf..00000000 --- a/apps/backend/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "baseUrl": "./", - "esModuleInterop": true, - "module": "ESNext", - "moduleResolution": "Node", - "outDir": "./dist", - "paths": { - "$src": ["./src"], - "$src/*": ["./src/*"] - }, - "resolveJsonModule": false, - "skipLibCheck": true, - "sourceMap": true, - "target": "ESNext", - "typeRoots": ["./node_modules/@types", "./src/types"], - "types": ["node"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"], - "tsc-alias": { - "resolveFullPaths": true, - "verbose": false - } -} diff --git a/apps/classroomio-com/src/blog/self-host.md b/apps/classroomio-com/src/blog/self-host.md index 0a5ada7d..7ebd3ff3 100644 --- a/apps/classroomio-com/src/blog/self-host.md +++ b/apps/classroomio-com/src/blog/self-host.md @@ -25,6 +25,6 @@ Below is a demo showing you step by step how to self host the project ### Next steps -We want to support self-hosting using a Coolify template or just using a Dockerfile. We currently have Dockerfiles for both our backend and dashboard projects however contributions from the open source community would help us in properly testing and improving them. +We want to support self-hosting using a Coolify template or just using a Dockerfile. We currently have Dockerfiles for both our api and dashboard projects however contributions from the open source community would help us in properly testing and improving them. If you prefer deployment with Docker, do checkout our [docker-compose.yaml](https://github.com/classroomio/classroomio/blob/main/docker-compose.yaml) file to get started and we can kick off a conversation on Github. diff --git a/apps/classroomio-com/src/routes/api/contact/+server.ts b/apps/classroomio-com/src/routes/api/contact/+server.ts index fa74261a..400ff9ff 100644 --- a/apps/classroomio-com/src/routes/api/contact/+server.ts +++ b/apps/classroomio-com/src/routes/api/contact/+server.ts @@ -47,7 +47,7 @@ export async function POST({ request }) { } ); } catch (error) { - console.log('errors from backend', error); + console.log('errors from api', error); return new Response( JSON.stringify({ diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example index e7020732..638d2779 100644 --- a/apps/dashboard/.env.example +++ b/apps/dashboard/.env.example @@ -14,7 +14,7 @@ UNSPLASH_API_KEY= OPENAI_API_KEY= # Sever URL - Optional if you want to use video upload and pdf downloads -# This is the deployment URL of apps/backend +# This is the deployment URL of apps/api PUBLIC_SERVER_URL= # For getting metadata for signup users. diff --git a/apps/dashboard/classroomio.code-workspace b/apps/dashboard/classroomio.code-workspace deleted file mode 100644 index a9ad457d..00000000 --- a/apps/dashboard/classroomio.code-workspace +++ /dev/null @@ -1,17 +0,0 @@ -{ - "folders": [ - { - "name": "project-root", - "path": "./" - }, - { - "name": "supabase-functions", - "path": "supabase/functions" - } - ], - "settings": { - "files.exclude": { - "supabase/functions/": true - } - } -} diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index d37f7d1e..bff240ea 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@carbon/charts-svelte": "^1.13.6", + "@cio/api": "workspace:*", "@lemonsqueezy/lemonsqueezy.js": "^2.2.0", "@polar-sh/sveltekit": "^0.2.1", "@supabase/supabase-js": "^2.31.0", @@ -51,6 +52,7 @@ "js-yaml": "^4.1.0", "jspdf": "^2.5.1", "jspdf-autotable": "^3.8.2", + "ky": "^1.10.0", "lodash": "^4.17.21", "openai-edge": "^1.2.2", "papaparse": "^5.4.1", diff --git a/apps/dashboard/postcss.config.cjs b/apps/dashboard/postcss.config.cjs deleted file mode 100644 index 3a310c7e..00000000 --- a/apps/dashboard/postcss.config.cjs +++ /dev/null @@ -1,13 +0,0 @@ -const tailwindcss = require('tailwindcss'); -const autoprefixer = require('autoprefixer'); - -const config = { - plugins: [ - //Some plugins, like tailwindcss/nesting, need to run before Tailwind, - tailwindcss(), - //But others, like autoprefixer, need to run after, - autoprefixer - ] -}; - -module.exports = config; diff --git a/apps/dashboard/src/lib/components/Course/components/Ceritficate/StudentCertificate/UnlockedCertificate.svelte b/apps/dashboard/src/lib/components/Course/components/Ceritficate/StudentCertificate/UnlockedCertificate.svelte index febe70cd..a54019e2 100644 --- a/apps/dashboard/src/lib/components/Course/components/Ceritficate/StudentCertificate/UnlockedCertificate.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Ceritficate/StudentCertificate/UnlockedCertificate.svelte @@ -4,7 +4,7 @@ import { env } from '$env/dynamic/public'; import { course } from '$lib/components/Course/store'; - import { currentOrg } from '$lib/utils/store/org'; + import { currentOrg, currentOrgDomain } from '$lib/utils/store/org'; import { profile } from '$lib/utils/store/user'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import { VARIANTS } from '$lib/components/PrimaryButton/constants'; @@ -13,7 +13,7 @@ import { fetchProfileCourseProgress } from '$lib/utils/services/courses'; import type { ProfileCourseProgress } from '$lib/utils/types'; import { snackbar } from '$lib/components/Snackbar/store'; - import { apiClient } from '$lib/utils/services/api'; + import { classroomio } from '$lib/utils/services/api'; let isLoading = false; let isCourseComplete = false; @@ -24,22 +24,20 @@ isLoading = true; try { - const response = await apiClient.post( - '/course/download/certificate', - { + const response = await classroomio.course.download.certificate.$post({ + json: { theme: `${$course.certificate_theme}`, studentName: `${$profile.fullname}`, courseName: `${$course.title}`, courseDescription: `${$course.description}`, - orgLogoUrl: `${$currentOrg.avatar_url}`, + orgLogoUrl: $currentOrg.avatar_url || `${$currentOrgDomain}/logo-512.png`, orgName: `${$currentOrg.name}` - }, - { - responseType: 'blob' } - ); + }); - const file = new Blob([response.data], { type: 'application/pdf' }); + const blobResponse = await response.blob(); + + const file = new Blob([blobResponse], { type: 'application/pdf' }); const fileURL = URL.createObjectURL(file); let a = document.createElement('a'); diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Document/Index.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Document/AddDocumentToLesson.svelte similarity index 66% rename from apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Document/Index.svelte rename to apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Document/AddDocumentToLesson.svelte index 59a94eb9..a0bd9d7d 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Document/Index.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Document/AddDocumentToLesson.svelte @@ -1,9 +1,5 @@ + ($lessonDocUpload.isModalOpen = false)}> + {$t('course.navItem.lessons.materials.tabs.document.upgrade')} + +

@@ -261,7 +220,8 @@
fileInput?.click()} />
@@ -305,23 +267,23 @@

- {#if $uploadCourseDocumentStore.error} + {#if $lessonDocUpload.error}
-

{$uploadCourseDocumentStore.error}

+

{$lessonDocUpload.error}

{/if} - {#if $uploadCourseDocumentStore.isUploading} + {#if $lessonDocUpload.isUploading}
{$t('course.navItem.lessons.materials.tabs.document.uploading')} - {$uploadCourseDocumentStore.uploadProgress}% + {$lessonDocUpload.uploadProgress}%
@@ -335,7 +297,7 @@ {/if} - {#if selectedFile && !$uploadCourseDocumentStore.isUploading} + {#if selectedFile && !$lessonDocUpload.isUploading}
{/if} - {#if $uploadCourseDocumentStore.uploadedDocument} + {#if $lessonDocUpload.uploadedDocument}

{$t('course.navItem.lessons.materials.tabs.document.upload_success')} diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Document/store.ts b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Document/store.ts index c74d996f..bd66ed6d 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Document/store.ts +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Document/store.ts @@ -11,22 +11,25 @@ export interface DocumentUploadState { size: number; } | null; error: string | null; + isCancelled: boolean; } -export const uploadCourseDocumentStore = writable({ +export const lessonDocUpload = writable({ isUploading: false, isModalOpen: false, uploadProgress: 0, uploadedDocument: null, - error: null + error: null, + isCancelled: false }); export function resetDocumentUploadStore() { - uploadCourseDocumentStore.set({ + lessonDocUpload.set({ isUploading: false, isModalOpen: false, uploadProgress: 0, uploadedDocument: null, - error: null + error: null, + isCancelled: false }); } diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/Index.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/AddVideoToLesson.svelte similarity index 100% rename from apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/Index.svelte rename to apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/AddVideoToLesson.svelte diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/UploadVideo.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/UploadVideo.svelte index e2fb82e8..b5a04757 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/UploadVideo.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/Video/UploadVideo.svelte @@ -1,26 +1,21 @@ - ($uploadCourseVideoStore.isModalOpen = false)}> + ($lessonVideoUpload.isModalOpen = false)}> {$t('course.navItem.lessons.materials.tabs.video.add_video.upgrade')} {#if !isLoaded} +

+
+
+ +

{fileInput?.files?.[0]?.name || fileName}

($uploadCourseVideoStore.isModalOpen = false)} + onClick={() => ($lessonVideoUpload.isModalOpen = false)} />
diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/components/ComponentDocument.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/components/ComponentDocument.svelte index e646cd19..4d7eb5e0 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/components/ComponentDocument.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/components/ComponentDocument.svelte @@ -3,10 +3,7 @@ lesson, deleteLessonDocument } from '$lib/components/Course/components/Lesson/store/lessons'; - import { - uploadCourseDocumentStore, - presignedDocUrls - } from '$lib/components/Course/components/Lesson/store/lessons'; + import { lessonDocUpload } from '$lib/components/Course/components/Lesson/store/lessons'; import MODES from '$lib/utils/constants/mode'; import IconButton from '$lib/components/IconButton/index.svelte'; import CloseIcon from 'carbon-icons-svelte/lib/Close.svelte'; @@ -17,7 +14,6 @@ import { onMount } from 'svelte'; import DocumentList from '../Document/DocumentList.svelte'; import { t } from '$lib/utils/functions/translations'; - import { apiClient } from '$lib/utils/services/api'; import type { LessonDocument } from '$lib/utils/types'; import { snackbar } from '$lib/components/Snackbar/store'; @@ -56,7 +52,7 @@ }); function openDocumentUploadModal() { - $uploadCourseDocumentStore.isModalOpen = true; + $lessonDocUpload.isModalOpen = true; } function deleteDocument(index: number) { @@ -71,36 +67,6 @@ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } - async function getPresignedDocs(docs: LessonDocument[]) { - if (!docs.length) return {}; - if (docs.every((d) => $presignedDocUrls[d.key || ''])) return; - - isLoading = true; - - try { - const { data } = await apiClient.post('/course/presign/download/document', { - keys: docs.map((d) => d.key) - }); - - if (!data.success) { - throw new Error(data.message); - } - - for (const key in data.urls) { - presignedDocUrls.update((urls) => ({ - ...urls, - [key]: data.urls[key] - })); - } - } catch (error) { - isLoading = false; - console.error('Error getting presigned docs:', error); - snackbar.error('Error getting presigned docs'); - } finally { - isLoading = false; - } - } - async function downloadDocument(doc: LessonDocument) { downloadingDocuments.add(doc.name); downloadingDocuments = downloadingDocuments; @@ -111,7 +77,7 @@ } try { - const response = await fetch($presignedDocUrls[doc.key]); + const response = await fetch(doc.link); if (!response.ok) { throw new Error('Failed to fetch document'); } @@ -142,6 +108,19 @@ renderTimeout = null; }, 150); // 150ms debounce } + // Wait for PDF.js to load + const checkPDFJS = () => { + return new Promise((resolve) => { + const check = () => { + if (pdfjsLib) { + resolve(true); + } else { + setTimeout(check, 100); + } + }; + check(); + }); + }; async function viewPDF(document: LessonDocument) { // Wait for PDF.js to be fully loaded @@ -152,20 +131,6 @@ isLoading = true; error = 'Loading PDF viewer...'; - // Wait for PDF.js to load - const checkPDFJS = () => { - return new Promise((resolve) => { - const check = () => { - if (pdfjsLib) { - resolve(true); - } else { - setTimeout(check, 100); - } - }; - check(); - }); - }; - await checkPDFJS(); } @@ -180,7 +145,7 @@ return; } - const response = await fetch($presignedDocUrls[document.key]); + const response = await fetch(document.link); if (!response.ok) { throw new Error('Failed to fetch PDF'); @@ -335,23 +300,19 @@ } $: displayDocuments = $lesson?.materials?.documents || []; - - $: getPresignedDocs(displayDocuments); -
- -
+ {#if pdfViewerOpen} diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/index.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/index.svelte index 51f8de48..400e8c9c 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/index.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/index.svelte @@ -1,7 +1,7 @@ @@ -29,7 +30,7 @@ maxWidth="w-[800px]" modalHeading="Welcome" > -

+

{$t('welcome_modal.we_at')} ClassroomIO{$t('welcome_modal.thank_you')}; {$t('welcome_modal.deeply_appreciate')}

- A welcome banner + A welcome banner diff --git a/apps/dashboard/src/lib/utils/functions/tinymce/plugins.ts b/apps/dashboard/src/lib/utils/functions/tinymce/plugins.ts index df1141da..a5b955c6 100644 --- a/apps/dashboard/src/lib/utils/functions/tinymce/plugins.ts +++ b/apps/dashboard/src/lib/utils/functions/tinymce/plugins.ts @@ -80,7 +80,7 @@ export const addMathPlugin = (tinymce: any) => { onSubmit: async function (w) { let content = output.exportAs('html'); - // The generated content always points to https://latex.codecogs.com/svg.image, this means we have to always rely on their server to serve formulas. We have a dedicated endpoint for generating formulas on our backend service: GET `/katex`. We will need to manually update the URL in the generate HTML to point to our server: + // The generated content always points to https://latex.codecogs.com/svg.image, this means we have to always rely on their server to serve formulas. We have a dedicated endpoint for generating formulas on our api service: GET `/katex`. We will need to manually update the URL in the generate HTML to point to our server: // https://latex.codecogs.com/svg.image // 1. Get only the URL in the img tag generated by codecogs library diff --git a/apps/dashboard/src/lib/utils/services/api.ts b/apps/dashboard/src/lib/utils/services/api.ts deleted file mode 100644 index ff7741ab..00000000 --- a/apps/dashboard/src/lib/utils/services/api.ts +++ /dev/null @@ -1,19 +0,0 @@ -import axios from 'axios'; -import { env } from '$env/dynamic/public'; -import { getAccessToken } from '$lib/utils/functions/supabase'; - -export const apiClient = axios.create({ - baseURL: `${env.PUBLIC_SERVER_URL}`, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - } -}); - -apiClient.interceptors.request.use(async (config) => { - const token = await getAccessToken(); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); diff --git a/apps/dashboard/src/lib/utils/services/api/README.md b/apps/dashboard/src/lib/utils/services/api/README.md new file mode 100644 index 00000000..50ed79e0 --- /dev/null +++ b/apps/dashboard/src/lib/utils/services/api/README.md @@ -0,0 +1,80 @@ +# API Client + +## Basic Usage + +```typescript +import { apiClient, safeRequest, classroomio } from '$lib/utils/services/api'; + +// GET request +const result = await safeRequest(async () => { + return apiClient.request('/api/users', { method: 'GET' }); +}); + +// POST request +const result = await safeRequest(async () => { + return apiClient.request('/api/users', { + method: 'POST', + body: { name: 'John', email: 'john@example.com' } + }); +}); + +// Handle response +if (result.success) { + console.log('Data:', result.data); +} else { + console.error('Error:', result.error.message); +} +``` + +## Error Handling + +```typescript +if (result.success) { + // Success + console.log(result.data); +} else { + // Check error type + if (result.error.error_code === 'auth') { + window.location.href = '/login'; + } else if (result.error.error_code === 'client') { + showError('Please check your input'); + } else if (result.error.error_code === 'server') { + showError('Server error, please try again'); + } +} +``` + +## Custom Fetch + +```typescript +import { createApiClient } from '$lib/utils/services/api'; + +const customClient = createApiClient({ + customFetch: (input, init) => { + // Your custom fetch logic + return fetch(input, init); + } +}); + +const result = await safeRequest(async () => { + return customClient.request('/api/data', { method: 'GET' }); +}); +``` + +## RPC Usage + +```typescript +import { classroomio } from '$lib/utils/services/api'; + +// Use with Hono RPC +const data = await classroomio.users.$get(); +const user = await classroomio.users.$post({ json: { name: 'John' } }); +``` + +## Error Codes + +- `'auth'` - 401 Unauthorized +- `'client'` - 400-499 Client errors +- `'server'` - 500-599 Server errors +- `'timeout'` - 408 Request timeout +- `'network'` - 0 Network issues diff --git a/apps/dashboard/src/lib/utils/services/api/constants.ts b/apps/dashboard/src/lib/utils/services/api/constants.ts new file mode 100644 index 00000000..29f7b240 --- /dev/null +++ b/apps/dashboard/src/lib/utils/services/api/constants.ts @@ -0,0 +1,24 @@ +import type { ApiClientConfig } from './types'; +import { env } from '$env/dynamic/public'; + +// Default configuration +export const DEFAULT_CONFIG: Required = { + baseURL: env.PUBLIC_SERVER_URL || '', + timeout: 30000, // 30 seconds + retries: 1, + retryDelay: 1000, // 1 second + customFetch: fetch, + onAuthError: async () => { + // Default: redirect to login or refresh token + console.warn('Authentication error occurred. Consider redirecting to login.'); + }, + onNetworkError: async (error: Error) => { + console.error('Network error:', error); + }, + onResponse: async (response: Response) => { + // Default: log response for debugging + if (process.env.NODE_ENV === 'development') { + console.log(`API Response: ${response.status} ${response.statusText}`); + } + } +}; diff --git a/apps/dashboard/src/lib/utils/services/api/index.ts b/apps/dashboard/src/lib/utils/services/api/index.ts new file mode 100644 index 00000000..dbeca8f7 --- /dev/null +++ b/apps/dashboard/src/lib/utils/services/api/index.ts @@ -0,0 +1,212 @@ +import { type ApiClientConfig, ApiError, type RequestConfig } from './types'; + +import { DEFAULT_CONFIG } from './constants'; +import { delay } from './utils'; +import { env } from '$env/dynamic/public'; +import { getAccessToken } from '$lib/utils/functions/supabase'; +import { hcWithType } from '@cio/api/rpc-types'; + +class ApiClient { + private config: Required; + + constructor(config: ApiClientConfig = {}) { + this.config = { ...DEFAULT_CONFIG, ...config }; + } + + private async makeRequest( + input: RequestInfo | URL, + requestConfig: RequestConfig = {} + ): Promise { + console.log('makeRequest', input, requestConfig); + const { + timeout = this.config.timeout, + retries = this.config.retries, + skipAuth = false, + ...fetchConfig + } = requestConfig; + + const url = typeof input === 'string' ? input : input.toString(); + const fullUrl = url.startsWith('http') ? url : `${this.config.baseURL}${url}`; + + // Prepare headers + const headers = new Headers(fetchConfig.headers); + + // Add default headers + if (!headers.has('Accept')) { + headers.set('Accept', 'application/json'); + } + + // Handle body serialization and content-type + let requestBody = fetchConfig.body; + if ( + fetchConfig.body && + typeof fetchConfig.body === 'object' && + !(fetchConfig.body instanceof FormData) + ) { + // Auto-stringify objects (except FormData) + requestBody = JSON.stringify(fetchConfig.body); + if (!headers.has('Content-Type')) { + headers.set('Content-Type', 'application/json'); + } + } else if (fetchConfig.body && !headers.has('Content-Type')) { + // Set content-type for other body types if not already set + if (typeof fetchConfig.body === 'string') { + headers.set('Content-Type', 'text/plain'); + } + } + + // Add authentication if not skipped + if (!skipAuth) { + try { + const token = await getAccessToken(); + if (token) { + headers.set('Authorization', `Bearer ${token}`); + } + } catch (error) { + console.warn('Failed to get access token:', error); + // Continue without auth if token retrieval fails + } + } + + // Create abort controller for timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const requestInit: RequestInit = { + ...fetchConfig, + headers, + body: requestBody, + signal: controller.signal + }; + + let lastError: Error | null = null; + + // Retry logic with exponential backoff + for (let attempt = 0; attempt <= retries; attempt++) { + try { + const response = await this.config.customFetch(fullUrl, requestInit); + + clearTimeout(timeoutId); + + await this.config.onResponse(response); + + if (response.ok) { + return response; + } + + if (response.status === 401) { + await this.config.onAuthError(); + throw new ApiError( + 'Authentication failed', + response.status, + response.statusText, + response + ); + } + + if (response.status >= 400 && response.status < 500) { + const errorText = await response.text().catch(() => 'Unknown error'); + throw new ApiError( + `Client error: ${errorText}`, + response.status, + response.statusText, + response + ); + } + + if (response.status >= 500) { + const error = new ApiError( + `Server error: ${response.statusText}`, + response.status, + response.statusText, + response + ); + + if (attempt === retries) { + throw error; + } + + lastError = error; + await delay(this.config.retryDelay * Math.pow(2, attempt)); + continue; + } + + // Other status codes + throw new ApiError( + `Unexpected status: ${response.statusText}`, + response.status, + response.statusText, + response + ); + } catch (error) { + clearTimeout(timeoutId); + + // Handle abort (timeout) + if (error instanceof Error && error.name === 'AbortError') { + throw new ApiError('Request timeout', 408, 'Request Timeout'); + } + + // Handle network errors - retryable + if (error instanceof TypeError && error.message.includes('fetch')) { + const networkError = new ApiError('Network error', 0, 'Network Error'); + + if (attempt === retries) { + await this.config.onNetworkError(networkError); + throw networkError; + } + + lastError = networkError; + await delay(this.config.retryDelay * Math.pow(2, attempt)); + continue; + } + + // Re-throw non-retryable errors + throw error; + } + } + + // If we get here, all retries failed + throw lastError || new ApiError('Request failed after all retries'); + } + + // Generic request method - handles all HTTP methods + async request(input: RequestInfo | URL, config: RequestConfig = {}): Promise { + return this.makeRequest(input, config); + } + + // Update configuration + updateConfig(newConfig: Partial): void { + this.config = { ...this.config, ...newConfig }; + } +} + +// Create default instance +export const apiClient = new ApiClient(); + +// RPC client using the new fetch wrapper +export const classroomio = hcWithType(env.PUBLIC_SERVER_URL, { + fetch: async (input: RequestInfo | URL, requestInit?: RequestInit) => { + return apiClient.request(input, requestInit); + } +}); + +// Utility functions for common use cases +export const createApiClient = (config: ApiClientConfig) => new ApiClient(config); + +// Re-export utility functions for convenience +export { + handleJsonResponse, + handleTextResponse, + isApiError, + isNetworkError, + isAuthError, + isClientError, + isServerError, + isTimeoutError, + safeRequest, + safeRequestText, + safeRequestRaw, + type Result +} from './utils'; + +export type { InferResponseType, InferRequestType } from '@cio/api/rpc-types'; diff --git a/apps/dashboard/src/lib/utils/services/api/types.ts b/apps/dashboard/src/lib/utils/services/api/types.ts new file mode 100644 index 00000000..495afe72 --- /dev/null +++ b/apps/dashboard/src/lib/utils/services/api/types.ts @@ -0,0 +1,46 @@ +// Types for better type safety +export interface ApiClientConfig { + baseURL?: string; + timeout?: number; + retries?: number; + retryDelay?: number; + customFetch?: typeof fetch; + onAuthError?: () => Promise | void; + onNetworkError?: (error: Error) => Promise | void; + onResponse?: (response: Response) => Promise | void; +} + +export interface RequestConfig extends RequestInit { + timeout?: number; + retries?: number; + skipAuth?: boolean; +} + +export class ApiError extends Error { + public readonly error_code: string; + + constructor( + message: string, + public status?: number, + public statusText?: string, + public response?: Response + ) { + super(message); + this.name = 'ApiError'; + + // Determine error code based on status + if (status === 401) { + this.error_code = 'auth'; + } else if (status && status >= 400 && status < 500) { + this.error_code = 'client'; + } else if (status && status >= 500) { + this.error_code = 'server'; + } else if (status === 408) { + this.error_code = 'timeout'; + } else if (status === 0 || !status) { + this.error_code = 'network'; + } else { + this.error_code = 'unknown'; + } + } +} diff --git a/apps/dashboard/src/lib/utils/services/api/utils.ts b/apps/dashboard/src/lib/utils/services/api/utils.ts new file mode 100644 index 00000000..c7ee525a --- /dev/null +++ b/apps/dashboard/src/lib/utils/services/api/utils.ts @@ -0,0 +1,125 @@ +import { ApiError } from './types'; + +// Utility function for exponential backoff +export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +// Helper function to handle JSON responses +export const handleJsonResponse = async (response: Response): Promise => { + if (!response.ok) { + throw new ApiError( + `HTTP ${response.status}: ${response.statusText}`, + response.status, + response.statusText, + response + ); + } + + try { + return await response.json(); + } catch (error) { + throw new ApiError( + 'Failed to parse JSON response', + response.status, + response.statusText, + response + ); + } +}; + +// Helper function to handle text responses +export const handleTextResponse = async (response: Response): Promise => { + if (!response.ok) { + throw new ApiError( + `HTTP ${response.status}: ${response.statusText}`, + response.status, + response.statusText, + response + ); + } + + return response.text(); +}; + +// Error handling utilities +export const isApiError = (error: unknown): error is ApiError => { + return error instanceof ApiError; +}; + +export const isNetworkError = (error: unknown): boolean => { + return isApiError(error) && error.status === 0; +}; + +export const isAuthError = (error: unknown): boolean => { + return isApiError(error) && error.status === 401; +}; + +export const isClientError = (error: unknown): boolean => { + return isApiError(error) && (error.status ?? 0) >= 400 && (error.status ?? 0) < 500; +}; + +export const isServerError = (error: unknown): boolean => { + return isApiError(error) && (error.status ?? 0) >= 500; +}; + +export const isTimeoutError = (error: unknown): boolean => { + return isApiError(error) && error.status === 408; +}; + +// Safe request wrapper that returns Result pattern +export type Result = { success: true; data: T } | { success: false; error: E }; + +// Generic safe request wrapper +export const safeRequest = async ( + requestFn: () => Promise +): Promise> => { + try { + const response = await requestFn(); + const data = await handleJsonResponse(response); + return { success: true, data }; + } catch (error) { + if (isApiError(error)) { + return { success: false, error }; + } + return { + success: false, + error: new ApiError('Unexpected error', 0, 'Unknown Error') + }; + } +}; + +// Safe request wrapper for text responses +export const safeRequestText = async ( + requestFn: () => Promise +): Promise> => { + try { + const response = await requestFn(); + const data = await handleTextResponse(response); + return { success: true, data }; + } catch (error) { + if (isApiError(error)) { + return { success: false, error }; + } + return { + success: false, + error: new ApiError('Unexpected error', 0, 'Unknown Error') + }; + } +}; + +// Safe request wrapper that returns the raw response +export const safeRequestRaw = async ( + requestFn: () => Promise +): Promise> => { + try { + const response = await requestFn(); + return { success: true, data: response }; + } catch (error) { + if (isApiError(error)) { + return { success: false, error }; + } + return { + success: false, + error: new ApiError('Unexpected error', 0, 'Unknown Error') + }; + } +}; diff --git a/apps/dashboard/src/lib/utils/services/courses/index.ts b/apps/dashboard/src/lib/utils/services/courses/index.ts index fac5a7e6..6b625bd4 100644 --- a/apps/dashboard/src/lib/utils/services/courses/index.ts +++ b/apps/dashboard/src/lib/utils/services/courses/index.ts @@ -11,9 +11,9 @@ import type { } from '$lib/utils/types'; import type { PostgrestError, PostgrestSingleResponse } from '@supabase/supabase-js'; +import { GenericUploader } from './presign'; import { QUESTION_TYPE } from '$lib/components/Question/constants'; import { STATUS } from '$lib/utils/constants/course'; -import { apiClient } from '$lib/utils/services/api'; import { get } from 'svelte/store'; import { isOrgAdmin } from '$lib/utils/store/org'; import { isUUID } from '$lib/utils/functions/isUUID'; @@ -53,12 +53,10 @@ export async function fetchProfileCourseProgress( data: ProfileCourseProgress[] | null; error: PostgrestError | null; }> { - const { data, error } = await supabase - .rpc('get_course_progress', { - course_id_arg: courseId, - profile_id_arg: profileId - }) - .returns(); + const { data, error } = await supabase.rpc('get_course_progress', { + course_id_arg: courseId, + profile_id_arg: profileId + }); return { data, error }; } @@ -286,23 +284,30 @@ export async function fetchLesson(lessonId: Lesson['id']) { .single(); if (data) { - const keys = data.videos.filter((video) => video.type === 'upload').map((video) => video.key); + const videoKeys = + data.videos?.filter((video) => video.type === 'upload')?.map((video) => video.key) || []; - if (!keys.length) { - return { data, error }; - } + const docKeys = data.documents?.map((doc) => doc.key) || []; try { - const response = await apiClient.post('/course/presign/download', { keys }); + // Get presigned URLs for videos and documents + const genericUploader = new GenericUploader('generic'); + + const urls = await genericUploader.getAllDownloadPresignedUrl(videoKeys, docKeys); data.videos = data.videos.map((video) => { - if (video.type === 'upload') { - video.link = response.data.urls[video.key]; + if (urls.videos[video.key]) { + video.link = urls.videos[video.key]; } return video; }); + + data.documents = data.documents.map((doc) => { + doc.link = urls.documents[doc.key]; + return doc; + }); } catch (error) { - console.error('Error retrieving videos:', error); + console.error('Error retrieving presigned assets (videos and documents):', error); } } diff --git a/apps/dashboard/src/lib/utils/services/courses/presign.ts b/apps/dashboard/src/lib/utils/services/courses/presign.ts new file mode 100644 index 00000000..4151ef0f --- /dev/null +++ b/apps/dashboard/src/lib/utils/services/courses/presign.ts @@ -0,0 +1,134 @@ +import { type Writable, get } from 'svelte/store'; +import { + lessonDocUpload, + lessonVideoUpload +} from '$lib/components/Course/components/Lesson/store/lessons'; + +import axios from 'axios'; +import { classroomio } from '$lib/utils/services/api'; + +export type UploadType = 'document' | 'video' | 'generic'; + +export class GenericUploader { + public abortController: AbortController | null = null; + private uploadType: UploadType; + private uploadStore: Writable; + + constructor(uploadType: UploadType) { + this.uploadType = uploadType; + this.uploadStore = uploadType === 'document' ? lessonDocUpload : lessonVideoUpload; + this.abortController = new AbortController(); + } + + async getDownloadPresignedUrl(keys: string[], type = this.uploadType) { + const endpoint = + type === 'document' + ? classroomio.course.presign.document.download + : classroomio.course.presign.video.download; + + const response = await endpoint.$post({ + json: { + keys + } + }); + + return response.json(); + } + + async getAllDownloadPresignedUrl(videoKeys: string[], docKeys: string[]) { + const urls = { + videos: {}, + documents: {} + }; + + try { + if (videoKeys.length) { + const videoResponse = await this.getDownloadPresignedUrl(videoKeys, 'video'); + urls.videos = videoResponse?.urls || {}; + } + + if (docKeys.length) { + const docResponse = await this.getDownloadPresignedUrl(docKeys, 'document'); + urls.documents = docResponse?.urls || {}; + } + } catch (error) { + console.error('Error getting download presigned url:', error); + } + + return urls; + } + + async getPresignedUrl(file: File) { + const endpoint = + this.uploadType === 'document' + ? classroomio.course.presign.document.upload + : classroomio.course.presign.video.upload; + + const response = await endpoint.$post({ + json: { + fileName: file?.name, + fileType: file?.type + } + }); + + return response.json(); + } + + async uploadFile(params: { url: string; file: File }) { + await axios.put(params.url, params.file, { + headers: { + 'Content-Type': params.file.type + }, + maxContentLength: Infinity, + maxBodyLength: Infinity, + signal: this.abortController?.signal, + onUploadProgress: (progressEvent) => { + if (get(this.uploadStore).isCancelled) { + this.abortController?.abort(); + return; + } + + const progress = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1)); + this.uploadStore.update((state) => ({ + ...state, + uploadProgress: progress + })); + } + }); + } + + initUpload() { + this.uploadStore.update((state) => ({ + ...state, + isUploading: true, + uploadProgress: 0, + error: null, + isCancelled: false + })); + + this.abortController = new AbortController(); + } + + cancelUpload() { + this.uploadStore.update((store) => ({ + ...store, + isCancelled: true, + isUploading: false + })); + + this.abortController?.abort(); + this.abortController = null; + } +} + +export class DocumentUploader extends GenericUploader { + constructor() { + super('document'); + } +} + +export class VideoUploader extends GenericUploader { + constructor() { + super('video'); + } +} diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index 3298271e..499adedd 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -789,7 +789,8 @@ "loading_pdf": "PDF laden ...", "failed_to_load_pdf": "Das PDF -Dokument konnte nicht geladen werden", "failed_to_render_pdf": "Die PDF -Seite versäumte es, PDF -Seite zu rendern", - "use_arrow_keys": "Verwenden Sie Pfeiltasten, um zu navigieren, +/-, um zu zoomen, oder klicken Sie auf die obigen Steuerelemente. Mit der rechten Maustaste ist deaktiviert." + "use_arrow_keys": "Verwenden Sie Pfeiltasten, um zu navigieren, +/-, um zu zoomen, oder klicken Sie auf die obigen Steuerelemente. Mit der rechten Maustaste ist deaktiviert.", + "upgrade": "Aktualisieren Sie Ihren Plan, um Dokumente hochzuladen" } } }, @@ -1409,7 +1410,8 @@ }, "generic": { "locked": "Gesperrt", - "unlocked": "Entsperrt" + "unlocked": "Entsperrt", + "loading": "Laden" }, "specialization": { "course_tag": "Spezialisierung" diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index e7ae78eb..7704bbf5 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -693,6 +693,7 @@ "upload_error_message": "Unsupported file format or upload failed", "upload_error_try_again": "Please try again with a valid document file", "try_again": "Try Again", + "upgrade": "Upgrade your plan to upload documents", "cancel_upload": "Cancel Upload", "upload_cancelled": "Upload cancelled by user", @@ -1521,7 +1522,8 @@ "generic": { "locked": "Locked", - "unlocked": "Unlocked" + "unlocked": "Unlocked", + "loading": "Loading" }, "validations": { diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index ed788d2a..95e36efb 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -789,7 +789,8 @@ "loading_pdf": "Cargando PDF ...", "failed_to_load_pdf": "No se pudo cargar el documento PDF", "failed_to_render_pdf": "No se pudo renderizar la página de PDF", - "use_arrow_keys": "Use las teclas de flecha para navegar, +/- para hacer zoom o hacer clic en los controles anteriores. El clic derecho está deshabilitado." + "use_arrow_keys": "Use las teclas de flecha para navegar, +/- para hacer zoom o hacer clic en los controles anteriores. El clic derecho está deshabilitado.", + "upgrade": "Actualice su plan para cargar documentos" } } }, @@ -1409,7 +1410,8 @@ }, "generic": { "locked": "bloqueado", - "unlocked": "desbloqueado" + "unlocked": "desbloqueado", + "loading": "Cargando" }, "specialization": { "course_tag": "Especialización" diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index 77766598..f177385d 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -789,7 +789,8 @@ "loading_pdf": "Chargement du PDF ...", "failed_to_load_pdf": "Échec du chargement du document PDF", "failed_to_render_pdf": "Échec de la page PDF", - "use_arrow_keys": "Utilisez des touches flèches pour naviguer, +/- pour zoomer ou cliquez sur les commandes ci-dessus. Le clic droit est désactivé." + "use_arrow_keys": "Utilisez des touches flèches pour naviguer, +/- pour zoomer ou cliquez sur les commandes ci-dessus. Le clic droit est désactivé.", + "upgrade": "Mettez à niveau votre plan pour télécharger des documents" } } }, @@ -1409,7 +1410,8 @@ }, "generic": { "locked": "Fermé à clé", - "unlocked": "Débloqué" + "unlocked": "Débloqué", + "loading": "Chargement" }, "specialization": { "course_tag": "Spécialisation" diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index eff32c8c..8414bb40 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -789,7 +789,8 @@ "loading_pdf": "लोडिंग पीडीएफ ...", "failed_to_load_pdf": "पीडीएफ दस्तावेज़ लोड करने में विफल", "failed_to_render_pdf": "पीडीएफ पेज को प्रस्तुत करने में विफल", - "use_arrow_keys": "ज़ूम करने के लिए नेविगेट करने के लिए तीर कुंजियों का उपयोग करें, या ऊपर दिए गए नियंत्रणों पर क्लिक करें। राइट-क्लिक अक्षम है।" + "use_arrow_keys": "ज़ूम करने के लिए नेविगेट करने के लिए तीर कुंजियों का उपयोग करें, या ऊपर दिए गए नियंत्रणों पर क्लिक करें। राइट-क्लिक अक्षम है।", + "upgrade": "दस्तावेज़ अपलोड करने के लिए अपनी योजना को अपग्रेड करें" } } }, @@ -1409,7 +1410,8 @@ }, "generic": { "locked": "बंद", - "unlocked": "अनलॉक किया" + "unlocked": "अनलॉक किया", + "loading": "लोड करना" }, "specialization": { "course_tag": "विशेषज्ञता" diff --git a/apps/dashboard/src/lib/utils/translations/pl.json b/apps/dashboard/src/lib/utils/translations/pl.json index 584774e5..6584a33a 100644 --- a/apps/dashboard/src/lib/utils/translations/pl.json +++ b/apps/dashboard/src/lib/utils/translations/pl.json @@ -99,7 +99,8 @@ }, "generic": { "locked": "Zamknięty", - "unlocked": "Odblokowany" + "unlocked": "Odblokowany", + "loading": "Załadunek" }, "content": { "heading": "Internacjonalizacja w SvelTekit", @@ -957,7 +958,8 @@ "loading_pdf": "Ładowanie PDF ...", "failed_to_load_pdf": "Nie udało się załadować dokumentu PDF", "failed_to_render_pdf": "Nie udało się renderować strony PDF", - "use_arrow_keys": "Użyj klawiszy strzałek do nawigacji, +/-, aby powiększyć lub kliknąć powyższe elementy sterujące. Kliknięcie prawym przyciskiem myszy jest wyłączone." + "use_arrow_keys": "Użyj klawiszy strzałek do nawigacji, +/-, aby powiększyć lub kliknąć powyższe elementy sterujące. Kliknięcie prawym przyciskiem myszy jest wyłączone.", + "upgrade": "Uaktualnij swój plan przesyłania dokumentów" } } }, diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index 1be3b7b0..44701894 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -789,7 +789,8 @@ "loading_pdf": "Carregando PDF ...", "failed_to_load_pdf": "Falha ao carregar o documento PDF", "failed_to_render_pdf": "Falhou em renderizar a página em PDF", - "use_arrow_keys": "Use as teclas de seta para navegar, +/- zoom ou clique nos controles acima. Clique com o botão direito do mouse está desativado." + "use_arrow_keys": "Use as teclas de seta para navegar, +/- zoom ou clique nos controles acima. Clique com o botão direito do mouse está desativado.", + "upgrade": "Atualize seu plano para fazer upload de documentos" } } }, @@ -1409,7 +1410,8 @@ }, "generic": { "locked": "Bloqueado", - "unlocked": "Desbloqueado" + "unlocked": "Desbloqueado", + "loading": "Carregando" }, "specialization": { "course_tag": "Especialização" diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index c0a13375..934c253e 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -789,7 +789,8 @@ "loading_pdf": "Загрузка PDF ...", "failed_to_load_pdf": "Не удалось загрузить документ PDF", "failed_to_render_pdf": "Не удалось отобразить страницу PDF", - "use_arrow_keys": "Используйте клавиши со стрелками, чтобы перемещаться, +/- до масштабирования или нажмите кнопку элементов управления выше. Щелкнуть правой кнопкой мыши отключено." + "use_arrow_keys": "Используйте клавиши со стрелками, чтобы перемещаться, +/- до масштабирования или нажмите кнопку элементов управления выше. Щелкнуть правой кнопкой мыши отключено.", + "upgrade": "Обновите свой план для загрузки документов" } } }, @@ -1409,7 +1410,8 @@ }, "generic": { "locked": "Заблокировано", - "unlocked": "Разблокировано" + "unlocked": "Разблокировано", + "loading": "Загрузка" }, "specialization": { "course_tag": "Специализация" diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index fdde8929..c9382a69 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -789,7 +789,8 @@ "loading_pdf": "Tải PDF ...", "failed_to_load_pdf": "Không tải tài liệu PDF", "failed_to_render_pdf": "Không thể hiển thị trang PDF", - "use_arrow_keys": "Sử dụng các phím mũi tên để điều hướng, +/- đến phóng to hoặc nhấp vào các điều khiển ở trên. Nhấp chuột phải bị vô hiệu hóa." + "use_arrow_keys": "Sử dụng các phím mũi tên để điều hướng, +/- đến phóng to hoặc nhấp vào các điều khiển ở trên. Nhấp chuột phải bị vô hiệu hóa.", + "upgrade": "Nâng cấp kế hoạch của bạn để tải lên tài liệu" } } }, @@ -1409,7 +1410,8 @@ }, "generic": { "locked": "Đã khóa", - "unlocked": "Đã mở khóa" + "unlocked": "Đã mở khóa", + "loading": "Đang tải" }, "specialization": { "course_tag": "Chuyên môn" diff --git a/apps/dashboard/src/lib/utils/types/index.ts b/apps/dashboard/src/lib/utils/types/index.ts index 472c4a85..8f620559 100644 --- a/apps/dashboard/src/lib/utils/types/index.ts +++ b/apps/dashboard/src/lib/utils/types/index.ts @@ -295,7 +295,7 @@ export type LessonDocument = { name: string; link: string; size?: number; - key?: string; + key: string; }; export interface LessonPage { diff --git a/apps/dashboard/src/mail/sendEmail.ts b/apps/dashboard/src/mail/sendEmail.ts index ca5ed25e..ad1c5179 100644 --- a/apps/dashboard/src/mail/sendEmail.ts +++ b/apps/dashboard/src/mail/sendEmail.ts @@ -1,4 +1,4 @@ -import { env } from '$env/dynamic/public'; +import { classroomio } from '$lib/utils/services/api'; const sendEmail = (sFetch: typeof fetch) => { return async ( @@ -12,13 +12,10 @@ const sendEmail = (sFetch: typeof fetch) => { }[] ) => { try { - const response = await sFetch(`${env.PUBLIC_SERVER_URL}/mail/send`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(emailDataArray) - }); + const response = await classroomio.mail.send.$post( + { json: emailDataArray }, + { fetch: sFetch } + ); if (!response.ok) { console.log('Failed to send emails'); diff --git a/apps/dashboard/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte b/apps/dashboard/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte index 4e6d36c1..18789573 100644 --- a/apps/dashboard/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte +++ b/apps/dashboard/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte @@ -41,7 +41,7 @@ import { ChevronLeft, ChevronRight, Edit, Save } from 'carbon-icons-svelte'; import OverflowMenuVertical from 'carbon-icons-svelte/lib/OverflowMenuVertical.svelte'; import ResultOld from 'carbon-icons-svelte/lib/ResultOld.svelte'; - import { apiClient } from '$lib/utils/services/api'; + import { classroomio } from '$lib/utils/services/api'; export let data; @@ -160,9 +160,8 @@ const lessonNumber = getLessonOrder(currentLesson.id); const slideUrl = $lesson.materials.slide_url || ''; - const response = await apiClient.post( - '/course/lesson/download/pdf', - { + const response = await classroomio.course.lesson.download.pdf.$post({ + json: { title: currentLesson.title, number: lessonNumber, orgName: $currentOrg.name, @@ -170,13 +169,11 @@ slideUrl: slideUrl, video: lessonVideo, courseTitle: $course.title - }, - { - responseType: 'blob' } - ); + }); + const blob = await response.blob(); - const file = new Blob([response.data], { type: 'application/pdf' }); + const file = new Blob([blob], { type: 'application/pdf' }); const fileURL = URL.createObjectURL(file); let a = document.createElement('a'); diff --git a/apps/dashboard/tailwind.config.cjs b/apps/dashboard/tailwind.config.cjs deleted file mode 100644 index 25365d3f..00000000 --- a/apps/dashboard/tailwind.config.cjs +++ /dev/null @@ -1,15 +0,0 @@ -/** @type {import('tailwindcss').Config}*/ -const config = { - content: ['./src/**/*.{html,js,svelte,ts}'], - - theme: { - extend: {} - }, - - plugins: [] -}; - -module.exports = config; -// generic name for component -// add theme-name dynamically on body -// have globaly style for theme-name .bg-primary-100 diff --git a/apps/dashboard/tsconfig.json b/apps/dashboard/tsconfig.json index 7f93e12b..cadf2d46 100644 --- a/apps/dashboard/tsconfig.json +++ b/apps/dashboard/tsconfig.json @@ -11,7 +11,7 @@ "strict": true, "target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, "module": "ESNext", - "moduleResolution": "node", + "moduleResolution": "bundler", "noImplicitAny": false, "paths": { "$lib": ["./src/lib"], diff --git a/apps/dashboard/vite.config.ts b/apps/dashboard/vite.config.ts index 71a3e793..13f7b584 100644 --- a/apps/dashboard/vite.config.ts +++ b/apps/dashboard/vite.config.ts @@ -1,7 +1,9 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import fs from 'fs'; import { defineConfig, loadEnv } from 'vite'; +import fs from 'fs'; +import path from 'path'; +import { sveltekit } from '@sveltejs/kit/vite'; + export default ({ mode }) => { process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }; @@ -19,10 +21,14 @@ export default ({ mode }) => { sourcemap: false }, optimizeDeps: { - entries: ['src/routes/**/+*.{js,ts,svelte}'] + entries: ['src/routes/**/+*.{js,ts,svelte}'], + include: ['@cio/api/rpc-types'] }, resolve: { - mainFields: ['browser'] + mainFields: ['browser'], + alias: { + '@cio/api': path.resolve(__dirname, '../api/dist') + } } }); }; diff --git a/apps/docs/pages/contributor-guides/cloudflare-setup.mdx b/apps/docs/pages/contributor-guides/cloudflare-setup.mdx index 180e5705..6052c6ba 100644 --- a/apps/docs/pages/contributor-guides/cloudflare-setup.mdx +++ b/apps/docs/pages/contributor-guides/cloudflare-setup.mdx @@ -4,4 +4,4 @@ import { Callout } from 'nextra/components'; # Cloudflare Setup -In this guide, you will learn how to get your cloudflare environment variables. We use Cloudflare R2 for video uploads and the aim of this guide is to help you get the right variables to properly setup our backend. +In this guide, you will learn how to get your cloudflare environment variables. We use Cloudflare R2 for video uploads and the aim of this guide is to help you get the right variables to properly setup our api. diff --git a/apps/docs/pages/quickstart/self-hosting.mdx b/apps/docs/pages/quickstart/self-hosting.mdx index 08295351..0c82065f 100644 --- a/apps/docs/pages/quickstart/self-hosting.mdx +++ b/apps/docs/pages/quickstart/self-hosting.mdx @@ -10,19 +10,19 @@ This guide will help you quickly selfhost ClassroomIO ClassroomIO is made up of the following applications -- [apps/backend](https://github.com/rotimi-best/classroomio/tree/main/apps/backend) - an Express server +- [apps/api](https://github.com/rotimi-best/classroomio/tree/main/apps/api) - an Express server - [apps/dashboard](https://github.com/rotimi-best/classroomio/tree/main/apps/dashboard) - the admin dashboard + LMS - [supabase](https://github.com/rotimi-best/classroomio/tree/main/supabase) - the database + REST API. -The quickest and easiest way to deploy both the backend and dashboard is by using our [Railway Template](https://railway.app/template/dl9RG7?referralCode=8OyIj5). However there are a couple of things that you'd need first before heading over to the template, which are: +The quickest and easiest way to deploy both the api and dashboard is by using our [Railway Template](https://railway.app/template/dl9RG7?referralCode=8OyIj5). However there are a couple of things that you'd need first before heading over to the template, which are: -1. SMTP values - For sending emails on the backend +1. SMTP values - For sending emails on the api 2. Supabase - For the database used on the dashboard -3. Cloudflare R2 (optional) - Video uploads on the backend +3. Cloudflare R2 (optional) - Video uploads on the api ## SMTP Setup -Both for the supabase and backend setup, you'd need a custom SMTP for email sending. Most importantly you need these values: +Both for the supabase and api setup, you'd need a custom SMTP for email sending. Most importantly you need these values: ```bash SMTP_HOST= @@ -63,7 +63,7 @@ We use Cloudflare R2 for video uploading within the course builder. It is not re ## Deploy on Railway -Now you have everything you need and you are ready to deploy the backend and dashboard. +Now you have everything you need and you are ready to deploy the api and dashboard. Go ahead and click this button to get started. @@ -71,7 +71,7 @@ Go ahead and click this button to get started. ### Environment Variables. -#### cio-backend +#### cio-api ```bash # Required @@ -94,7 +94,7 @@ SENTRY_DNS="" Learn more about the environment variables - [here](https://github.com/rotimi-best/classroomio/blob/main/apps/backend/.env.example) + [here](https://github.com/rotimi-best/classroomio/blob/main/apps/api/.env.example) #### cio-dashboard @@ -121,7 +121,7 @@ PRIVATE_APP_SUBDOMAINS="" ## Using Docker -We provide a docker-compose configuration to deploy both the backend and dashboard services. Here's how to use it: +We provide a docker-compose configuration to deploy both the api and dashboard services. Here's how to use it: 1. Clone the repository: @@ -151,7 +151,7 @@ The docker-compose file sets up two services: - Runs on port 3002 - Uses Node.js slim image -- Mounts the backend code directory +- Mounts the api code directory - Requires environment variables for: - Supabase configuration - Cloudflare R2 for video uploads diff --git a/docker-compose.yaml b/docker-compose.yaml index 975cccb1..2cb60df6 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,15 +1,15 @@ # ClassroomIO -# Project: Self-hosted Backend and Dashboard -# Description: This docker-compose file sets up the backend and dashboard services for self-hosting. +# Project: Self-hosted API and Dashboard +# Description: This docker-compose file sets up the api and dashboard services for self-hosting. # Author: rotimi-best # Date: 2024-09-18 version: '3.8' services: - backend: + api: build: - context: ./apps/backend + context: ./apps/api dockerfile: Dockerfile args: - PUBLIC_SUPABASE_ANON_KEY=${PUBLIC_SUPABASE_ANON_KEY} @@ -27,9 +27,9 @@ services: - '3002:3002' volumes: # Node modules (optional - for faster development) - - ./apps/backend/node_modules:/app/node_modules + - ./apps/api/node_modules:/app/node_modules # Build output directory (if you want to access built files) - - ./apps/backend/dist:/app/dist + - ./apps/api/dist:/app/dist environment: - NODE_ENV=production # Supabase - Database @@ -78,7 +78,7 @@ services: - PRIVATE_APP_SUBDOMAINS=${PRIVATE_APP_SUBDOMAINS} # app (if you are using app.yourdomain.com for the dashboard) - UNSPLASH_API_KEY=${UNSPLASH_API_KEY} - OPENAI_API_KEY=${OPENAI_API_KEY} - - PUBLIC_SERVER_URL=${PUBLIC_SERVER_URL} # this is the url to the backend service + - PUBLIC_SERVER_URL=${PUBLIC_SERVER_URL} # this is the url to the api service - PUBLIC_IP_REGISTRY_KEY=${PUBLIC_IP_REGISTRY_KEY} depends_on: - - backend + - api diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d520ad88..2e065f33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,19 +38,6 @@ importers: version: 5.8.3 apps/api: - dependencies: - hono: - specifier: ^4.6.16 - version: 4.7.11 - devDependencies: - '@cloudflare/workers-types': - specifier: ^4.20250109.0 - version: 4.20250613.0 - wrangler: - specifier: ^3.101.0 - version: 3.114.9(@cloudflare/workers-types@4.20250613.0)(bufferutil@4.0.9)(utf-8-validate@6.0.5) - - apps/backend: dependencies: '@aws-sdk/client-s3': specifier: ^3.388.0 @@ -60,7 +47,16 @@ importers: version: 3.828.0 '@hono/node-server': specifier: ^1.8.2 - version: 1.14.4(hono@4.7.11) + version: 1.14.4(hono@4.9.7) + '@hono/standard-validator': + specifier: ^0.1.5 + version: 0.1.5(@standard-schema/spec@1.0.0)(hono@4.9.7) + '@hono/zod-validator': + specifier: ^0.7.2 + version: 0.7.2(hono@4.9.7)(zod@4.1.8) + '@scalar/hono-api-reference': + specifier: ^0.9.18 + version: 0.9.18(hono@4.9.7) '@sentry/node': specifier: ^7.110.1 version: 7.120.3 @@ -77,11 +73,17 @@ importers: specifier: ^16.3.1 version: 16.5.0 hono: - specifier: ^4.0.5 - version: 4.7.11 + specifier: ^4.9.7 + version: 4.9.7 + hono-openapi: + specifier: ^1.0.8 + version: 1.0.8(@hono/standard-validator@0.1.5(@standard-schema/spec@1.0.0)(hono@4.9.7))(@standard-community/standard-json@0.3.3(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(valibot@1.1.0(typescript@5.8.3))(zod@4.1.8))(@standard-community/standard-openapi@0.2.4(@standard-community/standard-json@0.3.3(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(valibot@1.1.0(typescript@5.8.3))(zod@4.1.8))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(valibot@1.1.0(typescript@5.8.3))(zod-openapi@5.4.1(zod@4.1.8))(zod@4.1.8))(@types/json-schema@7.0.15)(hono@4.9.7)(openapi-types@12.1.3) hono-rate-limiter: specifier: ^0.4.2 - version: 0.4.2(hono@4.7.11) + version: 0.4.2(hono@4.9.7) + ioredis: + specifier: ^5.7.0 + version: 5.7.0 katex: specifier: ^0.16.10 version: 0.16.22 @@ -98,8 +100,11 @@ importers: specifier: ^6.0.0 version: 6.2.1(encoding@0.1.13) zod: - specifier: ^3.21.4 - version: 3.25.64 + specifier: ^4.1.8 + version: 4.1.8 + zod-openapi: + specifier: ^5.4.1 + version: 5.4.1(zod@4.1.8) devDependencies: '@types/katex': specifier: ^0.16.7 @@ -140,6 +145,9 @@ importers: vitest: specifier: ^1.2.2 version: 1.6.1(@types/node@20.19.0)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass@1.89.2) + wrangler: + specifier: 4.21.x + version: 4.21.2(bufferutil@4.0.9)(utf-8-validate@6.0.5) apps/classroomio-com: dependencies: @@ -173,13 +181,13 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^3.0.0 - version: 3.3.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2))) + version: 3.3.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2))) '@sveltejs/kit': specifier: ^2.0.0 - version: 2.21.5(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)) + version: 2.21.5(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)) '@sveltejs/vite-plugin-svelte': specifier: ^3.0.0 - version: 3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)) + version: 3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)) autoprefixer: specifier: ^10.4.15 version: 10.4.21(postcss@8.5.5) @@ -197,31 +205,31 @@ importers: version: 4.2.20 svelte-check: specifier: ^3.4.3 - version: 3.8.6(@babel/core@7.27.4)(postcss-load-config@4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)))(postcss@8.5.5)(sass@1.89.2)(svelte@4.2.20) + version: 3.8.6(@babel/core@7.27.4)(postcss-load-config@4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)))(postcss@8.5.5)(sass@1.89.2)(svelte@4.2.20) svelte-sequential-preprocessor: specifier: ^2.0.1 version: 2.0.2 tailwindcss: specifier: ^3.3.3 - version: 3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) + version: 3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) typescript: specifier: ^5.0.0 version: 5.8.3 vite: specifier: ^5.4.4 - version: 5.4.19(@types/node@22.15.31)(sass@1.89.2) + version: 5.4.19(@types/node@24.0.1)(sass@1.89.2) apps/course-app: dependencies: '@tailwindcss/container-queries': specifier: ^0.1.1 - version: 0.1.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3))) + version: 0.1.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3))) '@tailwindcss/forms': specifier: ^0.5.9 - version: 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3))) + version: 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3))) '@tailwindcss/typography': specifier: ^0.5.15 - version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3))) + version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3))) lucide-svelte: specifier: ^0.468.0 version: 0.468.0(svelte@5.34.1) @@ -240,13 +248,13 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^3.0.0 - version: 3.3.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))) + version: 3.3.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))) '@sveltejs/kit': specifier: ^2.9.0 - version: 2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) + version: 2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 - version: 5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) + version: 5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) autoprefixer: specifier: ^10.4.20 version: 10.4.21(postcss@8.5.5) @@ -282,43 +290,46 @@ importers: version: 2.6.0 tailwind-variants: specifier: ^0.3.0 - version: 0.3.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3))) + version: 0.3.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3))) tailwindcss: specifier: ^3.4.9 - version: 3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) + version: 3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) typescript: specifier: ^5.0.0 version: 5.8.3 vite: specifier: ^6.0.0 - version: 6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) + version: 6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vitest: specifier: ^2.0.4 - version: 2.1.9(@types/node@22.15.31)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass@1.89.2) + version: 2.1.9(@types/node@24.0.1)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass@1.89.2) apps/dashboard: dependencies: '@carbon/charts-svelte': specifier: ^1.13.6 version: 1.22.18(svelte@4.2.20) + '@cio/api': + specifier: workspace:* + version: link:../api '@lemonsqueezy/lemonsqueezy.js': specifier: ^2.2.0 version: 2.2.0 '@polar-sh/sveltekit': specifier: ^0.2.1 - version: 0.2.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)))(zod@3.25.64) + version: 0.2.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)))(zod@3.25.64) '@supabase/supabase-js': specifier: ^2.31.0 version: 2.50.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) '@sveltejs/adapter-auto': specifier: ^2.1.0 - version: 2.1.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2))) + version: 2.1.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2))) '@sveltejs/adapter-node': specifier: ^1.3.1 - version: 1.3.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2))) + version: 1.3.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2))) '@sveltejs/adapter-vercel': specifier: ^3.1.0 - version: 3.1.0(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)))(encoding@0.1.13) + version: 3.1.0(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)))(encoding@0.1.13) '@sveltekit-i18n/base': specifier: ^1.3.7 version: 1.3.7(svelte@4.2.20) @@ -327,7 +338,7 @@ importers: version: 1.0.8 '@tailwindcss/forms': specifier: ^0.5.4 - version: 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3))) + version: 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3))) '@types/pluralize': specifier: 0.0.30 version: 0.0.30 @@ -391,6 +402,9 @@ importers: jspdf-autotable: specifier: ^3.8.2 version: 3.8.4(jspdf@2.5.2) + ky: + specifier: ^1.10.0 + version: 1.10.0 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -466,10 +480,10 @@ importers: version: 7.27.6 '@sveltejs/kit': specifier: ^1.29.0 - version: 1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)) + version: 1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)) '@tailwindcss/typography': specifier: ^0.5.9 - version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3))) + version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3))) '@testing-library/jest-dom': specifier: ^6.1.5 version: 6.6.3 @@ -508,13 +522,13 @@ importers: version: 3.1.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + version: 29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) postcss: specifier: ^8.4.24 version: 8.5.5 postcss-load-config: specifier: ^4.0.1 - version: 4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + version: 4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) qrcode: specifier: ^1.5.3 version: 1.5.4 @@ -532,22 +546,22 @@ importers: version: 0.9.61(svelte@4.2.20) svelte-jester: specifier: ^3.0.0 - version: 3.0.0(jest@29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)))(svelte@4.2.20) + version: 3.0.0(jest@29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)))(svelte@4.2.20) svelte-loading-spinners: specifier: ^0.3.4 version: 0.3.6 svelte-preprocess: specifier: ^5.0.4 - version: 5.1.4(@babel/core@7.27.4)(postcss-load-config@4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)))(postcss@8.5.5)(sass@1.89.2)(svelte@4.2.20)(typescript@5.8.3) + version: 5.1.4(@babel/core@7.27.4)(postcss-load-config@4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)))(postcss@8.5.5)(sass@1.89.2)(svelte@4.2.20)(typescript@5.8.3) tailwindcss: specifier: ^3.3.2 - version: 3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + version: 3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) ts-jest: specifier: ^29.1.1 - version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)))(typescript@5.8.3) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@24.0.1)(typescript@5.8.3) + version: 10.9.2(@types/node@22.15.31)(typescript@5.8.3) tsconfig: specifier: workspace:* version: link:../../packages/tsconfig @@ -559,7 +573,7 @@ importers: version: 5.8.3 vite: specifier: ^4.4.8 - version: 4.5.14(@types/node@24.0.1)(sass@1.89.2) + version: 4.5.14(@types/node@22.15.31)(sass@1.89.2) apps/docs: dependencies: @@ -568,13 +582,13 @@ importers: version: 3.1.11(rollup@4.43.0) next: specifier: ^13.0.6 - version: 13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2) + version: 13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2) nextra: specifier: 2.13.2 - version: 2.13.2(next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.13.2(next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nextra-theme-docs: specifier: 2.13.2 - version: 2.13.2(next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(nextra@2.13.2(next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.13.2(next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(nextra@2.13.2(next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.2.0 version: 18.3.1 @@ -627,7 +641,7 @@ importers: version: 5.8.3 vite: specifier: ^5.0.3 - version: 5.4.19(@types/node@22.15.31)(sass@1.89.2) + version: 5.4.19(@types/node@24.0.1)(sass@1.89.2) packages/course-app/src/template: dependencies: @@ -1516,52 +1530,49 @@ packages: '@carbon/utils-position@1.3.0': resolution: {integrity: sha512-bfar2dV+fQ15syIrH3n9ujY4PXd1Q+AF2VcTLJIC04IDe2f80zOnJlLNPc/RktHcWTZ7WSQm80cQo3abGcsfTA==} - '@cloudflare/kv-asset-handler@0.3.4': - resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} - engines: {node: '>=16.13'} + '@cloudflare/kv-asset-handler@0.4.0': + resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} + engines: {node: '>=18.0.0'} - '@cloudflare/unenv-preset@2.0.2': - resolution: {integrity: sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==} + '@cloudflare/unenv-preset@2.3.3': + resolution: {integrity: sha512-/M3MEcj3V2WHIRSW1eAQBPRJ6JnGQHc6JKMAPLkDb7pLs3m6X9ES/+K3ceGqxI6TKeF32AWAi7ls0AYzVxCP0A==} peerDependencies: - unenv: 2.0.0-rc.14 - workerd: ^1.20250124.0 + unenv: 2.0.0-rc.17 + workerd: ^1.20250508.0 peerDependenciesMeta: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20250408.0': - resolution: {integrity: sha512-bxhIwBWxaNItZLXDNOKY2dCv0FHjDiDkfJFpwv4HvtvU5MKcrivZHVmmfDzLW85rqzfcDOmKbZeMPVfiKxdBZw==} + '@cloudflare/workerd-darwin-64@1.20250617.0': + resolution: {integrity: sha512-toG8JUKVLIks4oOJLe9FeuixE84pDpMZ32ip7mCpE7JaFc5BqGFvevk0YC/db3T71AQlialjRwioH3jS/dzItA==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20250408.0': - resolution: {integrity: sha512-5XZ2Oykr8bSo7zBmERtHh18h5BZYC/6H1YFWVxEj3PtalF3+6SHsO4KZsbGvDml9Pu7sHV277jiZE5eny8Hlyw==} + '@cloudflare/workerd-darwin-arm64@1.20250617.0': + resolution: {integrity: sha512-JTX0exbC9/ZtMmQQA8tDZEZFMXZrxOpTUj2hHnsUkErWYkr5SSZH04RBhPg6dU4VL8bXuB5/eJAh7+P9cZAp7g==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20250408.0': - resolution: {integrity: sha512-WbgItXWln6G5d7GvYLWcuOzAVwafysZaWunH3UEfsm95wPuRofpYnlDD861gdWJX10IHSVgMStGESUcs7FLerQ==} + '@cloudflare/workerd-linux-64@1.20250617.0': + resolution: {integrity: sha512-8jkSoVRJ+1bOx3tuWlZCGaGCV2ew7/jFMl6V3CPXOoEtERUHsZBQLVkQIGKcmC/LKSj7f/mpyBUeu2EPTo2HEg==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20250408.0': - resolution: {integrity: sha512-pAhEywPPvr92SLylnQfZEPgXz+9pOG9G9haAPLpEatncZwYiYd9yiR6HYWhKp2erzCoNrOqKg9IlQwU3z1IDiw==} + '@cloudflare/workerd-linux-arm64@1.20250617.0': + resolution: {integrity: sha512-YAzcOyu897z5dQKFzme1oujGWMGEJCR7/Wrrm1nSP6dqutxFPTubRADM8BHn2CV3ij//vaPnAeLmZE3jVwOwig==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20250408.0': - resolution: {integrity: sha512-nJ3RjMKGae2aF2rZ/CNeBvQPM+W5V1SUK0FYWG/uomyr7uQ2l4IayHna1ODg/OHHTEgIjwom0Mbn58iXb0WOcQ==} + '@cloudflare/workerd-windows-64@1.20250617.0': + resolution: {integrity: sha512-XWM/6sagDrO0CYDKhXhPjM23qusvIN1ju9ZEml6gOQs8tNOFnq6Cn6X9FAmnyapRFCGUSEC3HZYJAm7zwVKaMA==} engines: {node: '>=16'} cpu: [x64] os: [win32] - '@cloudflare/workers-types@4.20250613.0': - resolution: {integrity: sha512-upH6pxh43ph9293aXVgEp/0ODhyT3deRdlL0NEe4eJJdgoSAJxWj0c2ASZHs3awOaItRTxTxo7GZk1d6wolPKQ==} - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1597,34 +1608,24 @@ packages: '@emnapi/runtime@1.4.3': resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} - '@esbuild-plugins/node-globals-polyfill@0.2.3': - resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} - peerDependencies: - esbuild: '*' - - '@esbuild-plugins/node-modules-polyfill@0.2.2': - resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} - peerDependencies: - esbuild: '*' - '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.5': resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.17.19': - resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -1637,16 +1638,16 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.5': - resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.17.19': - resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} - engines: {node: '>=12'} - cpu: [arm] + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] os: [android] '@esbuild/android-arm@0.18.20': @@ -1661,16 +1662,16 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.5': - resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.17.19': - resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] os: [android] '@esbuild/android-x64@0.18.20': @@ -1685,18 +1686,18 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.5': resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.17.19': - resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -1709,16 +1710,16 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.5': - resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.17.19': - resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.18.20': @@ -1733,18 +1734,18 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.5': resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.17.19': - resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -1757,16 +1758,16 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.5': - resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.17.19': - resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.18.20': @@ -1781,18 +1782,18 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.5': resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.17.19': - resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -1805,16 +1806,16 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.5': - resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.17.19': - resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} - engines: {node: '>=12'} - cpu: [arm] + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.18.20': @@ -1829,16 +1830,16 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.5': - resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.17.19': - resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} - engines: {node: '>=12'} - cpu: [ia32] + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.18.20': @@ -1853,16 +1854,16 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.5': - resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.17.19': - resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} - engines: {node: '>=12'} - cpu: [loong64] + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.18.20': @@ -1877,16 +1878,16 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.5': - resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.17.19': - resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} - engines: {node: '>=12'} - cpu: [mips64el] + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.18.20': @@ -1901,16 +1902,16 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.5': - resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.17.19': - resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} - engines: {node: '>=12'} - cpu: [ppc64] + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.18.20': @@ -1925,16 +1926,16 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.5': - resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.17.19': - resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} - engines: {node: '>=12'} - cpu: [riscv64] + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.18.20': @@ -1949,16 +1950,16 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.5': - resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.17.19': - resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} - engines: {node: '>=12'} - cpu: [s390x] + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.18.20': @@ -1973,16 +1974,16 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.5': - resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.17.19': - resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.18.20': @@ -1997,22 +1998,28 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.5': resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.5': - resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.17.19': - resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] os: [netbsd] '@esbuild/netbsd-x64@0.18.20': @@ -2027,22 +2034,28 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.5': resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.5': - resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.17.19': - resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] os: [openbsd] '@esbuild/openbsd-x64@0.18.20': @@ -2057,18 +2070,18 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.5': resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.17.19': - resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -2081,18 +2094,18 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.5': resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.17.19': - resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -2105,16 +2118,16 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.5': - resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.17.19': - resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} - engines: {node: '>=12'} - cpu: [ia32] + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.18.20': @@ -2129,16 +2142,16 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.5': - resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.17.19': - resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.18.20': @@ -2153,6 +2166,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.5': resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} engines: {node: '>=18'} @@ -2252,6 +2271,18 @@ packages: peerDependencies: hono: ^4 + '@hono/standard-validator@0.1.5': + resolution: {integrity: sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w==} + peerDependencies: + '@standard-schema/spec': 1.0.0 + hono: '>=3.9.0' + + '@hono/zod-validator@0.7.2': + resolution: {integrity: sha512-ub5eL/NeZ4eLZawu78JpW/J+dugDAYhwqUIdp9KYScI6PZECij4Hx4UsrthlEUutqDDhPwRI0MscUfNkvn/mqQ==} + peerDependencies: + hono: '>=3.9.0' + zod: ^3.25.0 || ^4.0.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2453,6 +2484,9 @@ packages: '@internationalized/date@3.8.2': resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==} + '@ioredis/commands@1.4.0': + resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2981,6 +3015,24 @@ packages: cpu: [x64] os: [win32] + '@scalar/core@0.3.16': + resolution: {integrity: sha512-gZLKu6yzc9Md8X29OX7hN/Cgl95ReHK+t/HXbaT3O8hsbCc9JyhEZiKFpTfCH9vcgyYQ91fcq9w8tT4BVpFJUw==} + engines: {node: '>=20'} + + '@scalar/hono-api-reference@0.9.18': + resolution: {integrity: sha512-YYD0HY4ru3RRA8DQcIQiF5OBJntZSSfFu14KbU/Pdd5W5x57+BQH3cAa9aDJ7M1+wOD9a2iJUSCjTh8K3GOhHw==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4.0.0 + + '@scalar/openapi-types@0.3.7': + resolution: {integrity: sha512-QHSvHBVDze3+dUwAhIGq6l1iOev4jdoqdBK7QpfeN1Q4h+6qpVEw3EEqBiH0AXUSh/iWwObBv4uMgfIx0aNZ5g==} + engines: {node: '>=20'} + + '@scalar/types@0.2.15': + resolution: {integrity: sha512-x2aCNmkDqr3VXUHjw7wPXK9KZwHbGGMs4NuxJIzy9MbAxUS9li8HXGG0K82Q5fDm47SAM+68z0/tnWkJpu+kzg==} + engines: {node: '>=20'} + '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} @@ -3267,6 +3319,55 @@ packages: '@stablelib/base64@1.0.1': resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@standard-community/standard-json@0.3.3': + resolution: {integrity: sha512-oIdzYrYFe5hUxcAgnNcDuaP59dn1LRPHVv16ZNIONPF2OF9r5O8zGGhBHfrNEmldLcljmDBTnGz1gwmaK0iczQ==} + peerDependencies: + '@standard-schema/spec': ^1.0.0 + '@types/json-schema': ^7.0.15 + '@valibot/to-json-schema': ^1.3.0 + arktype: ^2.1.20 + effect: ^3.16.8 + quansync: ^0.2.11 + valibot: ^1.1.0 + zod: ^3.25.0 || ^4.0.0 + zod-to-json-schema: ^3.24.5 + peerDependenciesMeta: + '@valibot/to-json-schema': + optional: true + arktype: + optional: true + effect: + optional: true + valibot: + optional: true + zod: + optional: true + zod-to-json-schema: + optional: true + + '@standard-community/standard-openapi@0.2.4': + resolution: {integrity: sha512-guPU+9Y+Y9JN0gpBQbZMlIYzRSaRyTe7f+g6JCV3d0rrMQ5JFngLQKRyg3MP07xIts8nGim167Y9ePfdlkJp0Q==} + peerDependencies: + '@standard-community/standard-json': ^0.3.1 + '@standard-schema/spec': ^1.0.0 + arktype: ^2.1.20 + openapi-types: ^12.1.3 + valibot: ^1.1.0 + zod: ^3.25.0 || ^4.0.0 + zod-openapi: ^4 + peerDependenciesMeta: + arktype: + optional: true + valibot: + optional: true + zod: + optional: true + zod-openapi: + optional: true + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@supabase/auth-js@2.70.0': resolution: {integrity: sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==} @@ -4393,6 +4494,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -4909,6 +5014,10 @@ packages: delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -5097,11 +5206,6 @@ packages: es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} - esbuild@0.17.19: - resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -5112,6 +5216,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.25.5: resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} engines: {node: '>=18'} @@ -5218,9 +5327,6 @@ packages: estree-util-visit@1.2.1: resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==} - estree-walker@0.6.1: - resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -5682,13 +5788,28 @@ packages: hey-listen@1.0.8: resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} + hono-openapi@1.0.8: + resolution: {integrity: sha512-JjSdT4sNUgxQGgwO90boRLfnrVYp3ge+Y/vHqPMJrAZuaIhKekAVipoeJ8AgpTyK+ZaxPzqdcmDBA9L+Ce3X9Q==} + peerDependencies: + '@hono/standard-validator': ^0.1.2 + '@standard-community/standard-json': ^0.3.1 + '@standard-community/standard-openapi': ^0.2.4 + '@types/json-schema': ^7.0.15 + hono: ^4.8.3 + openapi-types: ^12.1.3 + peerDependenciesMeta: + '@hono/standard-validator': + optional: true + hono: + optional: true + hono-rate-limiter@0.4.2: resolution: {integrity: sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw==} peerDependencies: hono: ^4.1.1 - hono@4.7.11: - resolution: {integrity: sha512-rv0JMwC0KALbbmwJDEnxvQCeJh+xbS3KEWW5PC9cMJ08Ur9xgatI0HmtgYZfOdOSOeYsp5LO2cOhdI8cLEbDEQ==} + hono@4.9.7: + resolution: {integrity: sha512-t4Te6ERzIaC48W3x4hJmBwgNlLhmiEdEE5ViYb02ffw4ignHNHa5IBtPjmbKstmtKa8X6C35iWwK4HaqvrzG9w==} engines: {node: '>=16.9.0'} hosted-git-info@2.8.9: @@ -5821,6 +5942,10 @@ packages: intl-messageformat@10.7.16: resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==} + ioredis@5.7.0: + resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==} + engines: {node: '>=12.22.0'} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -6308,6 +6433,10 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + ky@1.10.0: + resolution: {integrity: sha512-YRPCzHEWZffbfvmRrfwa+5nwBHwZuYiTrfDX0wuhGBPV0pA/zCqcOq93MDssON/baIkpYbvehIX5aLpMxrRhaA==} + engines: {node: '>=18'} + layout-base@1.0.2: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} @@ -6371,10 +6500,16 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.ismatch@4.4.0: resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} @@ -6429,9 +6564,6 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true - magic-string@0.25.9: - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -6732,9 +6864,9 @@ packages: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true - miniflare@3.20250408.2: - resolution: {integrity: sha512-uTs7cGWFErgJTKtBdmtctwhuoxniuCQqDT8+xaEiJdEC8d+HsaZVYfZwIX2NuSmdAiHMe7NtbdZYjFMbIXtJsQ==} - engines: {node: '>=16.13'} + miniflare@4.20250617.4: + resolution: {integrity: sha512-IAoApFKxOJlaaFkym5ETstVX3qWzVt3xyqCDj6vSSTgEH3zxZJ5417jZGg8iQfMHosKCcQH1doPPqqnOZm/yrw==} + engines: {node: '>=18.0.0'} hasBin: true minimatch@3.1.2: @@ -7041,6 +7173,9 @@ packages: engines: {node: '>=18'} deprecated: The official OpenAI SDK now supports edge runtimes, please use that instead. + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -7520,6 +7655,9 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + queue-lit@1.5.2: resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} engines: {node: '>=12'} @@ -7594,6 +7732,14 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + regenerate-unicode-properties@10.2.0: resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} engines: {node: '>=4'} @@ -7729,16 +7875,6 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rollup-plugin-inject@3.0.2: - resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} - deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. - - rollup-plugin-node-polyfills@0.2.1: - resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} - - rollup-pluginutils@2.8.2: - resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} - rollup@3.29.5: resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -7963,10 +8099,6 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} - sourcemap-codec@1.4.8: - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - deprecated: Please use @jridgewell/sourcemap-codec instead - space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -8013,6 +8145,9 @@ packages: stacktracey@2.1.8: resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + standard-version@9.5.0: resolution: {integrity: sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q==} engines: {node: '>=10'} @@ -8694,8 +8829,8 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} - unenv@2.0.0-rc.14: - resolution: {integrity: sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==} + unenv@2.0.0-rc.17: + resolution: {integrity: sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==} unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} @@ -8829,6 +8964,14 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + valibot@1.1.0: + resolution: {integrity: sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -9131,17 +9274,17 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - workerd@1.20250408.0: - resolution: {integrity: sha512-bBUX+UsvpzAqiWFNeZrlZmDGddiGZdBBbftZJz2wE6iUg/cIAJeVQYTtS/3ahaicguoLBz4nJiDo8luqM9fx1A==} + workerd@1.20250617.0: + resolution: {integrity: sha512-Uv6p0PYUHp/W/aWfUPLkZVAoAjapisM27JJlwcX9wCPTfCfnuegGOxFMvvlYpmNaX4YCwEdLCwuNn3xkpSkuZw==} engines: {node: '>=16'} hasBin: true - wrangler@3.114.9: - resolution: {integrity: sha512-1e0gL+rxLF04kM9bW4sxoDGLXpJ1x53Rx1t18JuUm6F67qadKKPISyUAXuBeIQudWrCWEBXaTVnSdLHz0yBXbA==} - engines: {node: '>=16.17.0'} + wrangler@4.21.2: + resolution: {integrity: sha512-POC8gGIAsJIYISxVe/oWIjSNwCqfaHMcDPzo6zuGTGvqYC33UM5WI82nULse1bNpXBC0L0XpqtHysW3sDqa8DQ==} + engines: {node: '>=18.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20250408.0 + '@cloudflare/workers-types': ^4.20250617.0 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -9270,12 +9413,24 @@ packages: zimmerframe@1.1.2: resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zod-openapi@5.4.1: + resolution: {integrity: sha512-deZCmezGJhi2urw7l8LWYCJEUaAb/Azo14c0wncCM7L7u7hz+1d4v3r4Uav7x4+zVkJj/ZYlCu6lwT0QQttW6g==} + engines: {node: '>=20'} + peerDependencies: + zod: ^3.25.74 || ^4.0.0 + zod@3.22.3: resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + zod@3.24.1: + resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zod@3.25.64: resolution: {integrity: sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==} + zod@4.1.8: + resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -10570,33 +10725,31 @@ snapshots: dependencies: '@ibm/telemetry-js': 1.9.1 - '@cloudflare/kv-asset-handler@0.3.4': + '@cloudflare/kv-asset-handler@0.4.0': dependencies: mime: 3.0.0 - '@cloudflare/unenv-preset@2.0.2(unenv@2.0.0-rc.14)(workerd@1.20250408.0)': + '@cloudflare/unenv-preset@2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0)': dependencies: - unenv: 2.0.0-rc.14 + unenv: 2.0.0-rc.17 optionalDependencies: - workerd: 1.20250408.0 + workerd: 1.20250617.0 - '@cloudflare/workerd-darwin-64@1.20250408.0': + '@cloudflare/workerd-darwin-64@1.20250617.0': optional: true - '@cloudflare/workerd-darwin-arm64@1.20250408.0': + '@cloudflare/workerd-darwin-arm64@1.20250617.0': optional: true - '@cloudflare/workerd-linux-64@1.20250408.0': + '@cloudflare/workerd-linux-64@1.20250617.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20250408.0': + '@cloudflare/workerd-linux-arm64@1.20250617.0': optional: true - '@cloudflare/workerd-windows-64@1.20250408.0': + '@cloudflare/workerd-windows-64@1.20250617.0': optional: true - '@cloudflare/workers-types@4.20250613.0': {} - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -10626,23 +10779,13 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)': - dependencies: - esbuild: 0.17.19 - - '@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19)': - dependencies: - esbuild: 0.17.19 - escape-string-regexp: 4.0.0 - rollup-plugin-node-polyfills: 0.2.1 - '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.25.5': + '@esbuild/aix-ppc64@0.25.4': optional: true - '@esbuild/android-arm64@0.17.19': + '@esbuild/aix-ppc64@0.25.5': optional: true '@esbuild/android-arm64@0.18.20': @@ -10651,10 +10794,10 @@ snapshots: '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.25.5': + '@esbuild/android-arm64@0.25.4': optional: true - '@esbuild/android-arm@0.17.19': + '@esbuild/android-arm64@0.25.5': optional: true '@esbuild/android-arm@0.18.20': @@ -10663,10 +10806,10 @@ snapshots: '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.25.5': + '@esbuild/android-arm@0.25.4': optional: true - '@esbuild/android-x64@0.17.19': + '@esbuild/android-arm@0.25.5': optional: true '@esbuild/android-x64@0.18.20': @@ -10675,10 +10818,10 @@ snapshots: '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.25.5': + '@esbuild/android-x64@0.25.4': optional: true - '@esbuild/darwin-arm64@0.17.19': + '@esbuild/android-x64@0.25.5': optional: true '@esbuild/darwin-arm64@0.18.20': @@ -10687,10 +10830,10 @@ snapshots: '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.25.5': + '@esbuild/darwin-arm64@0.25.4': optional: true - '@esbuild/darwin-x64@0.17.19': + '@esbuild/darwin-arm64@0.25.5': optional: true '@esbuild/darwin-x64@0.18.20': @@ -10699,10 +10842,10 @@ snapshots: '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.25.5': + '@esbuild/darwin-x64@0.25.4': optional: true - '@esbuild/freebsd-arm64@0.17.19': + '@esbuild/darwin-x64@0.25.5': optional: true '@esbuild/freebsd-arm64@0.18.20': @@ -10711,10 +10854,10 @@ snapshots: '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.25.5': + '@esbuild/freebsd-arm64@0.25.4': optional: true - '@esbuild/freebsd-x64@0.17.19': + '@esbuild/freebsd-arm64@0.25.5': optional: true '@esbuild/freebsd-x64@0.18.20': @@ -10723,10 +10866,10 @@ snapshots: '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.25.5': + '@esbuild/freebsd-x64@0.25.4': optional: true - '@esbuild/linux-arm64@0.17.19': + '@esbuild/freebsd-x64@0.25.5': optional: true '@esbuild/linux-arm64@0.18.20': @@ -10735,10 +10878,10 @@ snapshots: '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.25.5': + '@esbuild/linux-arm64@0.25.4': optional: true - '@esbuild/linux-arm@0.17.19': + '@esbuild/linux-arm64@0.25.5': optional: true '@esbuild/linux-arm@0.18.20': @@ -10747,10 +10890,10 @@ snapshots: '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.25.5': + '@esbuild/linux-arm@0.25.4': optional: true - '@esbuild/linux-ia32@0.17.19': + '@esbuild/linux-arm@0.25.5': optional: true '@esbuild/linux-ia32@0.18.20': @@ -10759,10 +10902,10 @@ snapshots: '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.25.5': + '@esbuild/linux-ia32@0.25.4': optional: true - '@esbuild/linux-loong64@0.17.19': + '@esbuild/linux-ia32@0.25.5': optional: true '@esbuild/linux-loong64@0.18.20': @@ -10771,10 +10914,10 @@ snapshots: '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.25.5': + '@esbuild/linux-loong64@0.25.4': optional: true - '@esbuild/linux-mips64el@0.17.19': + '@esbuild/linux-loong64@0.25.5': optional: true '@esbuild/linux-mips64el@0.18.20': @@ -10783,10 +10926,10 @@ snapshots: '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.25.5': + '@esbuild/linux-mips64el@0.25.4': optional: true - '@esbuild/linux-ppc64@0.17.19': + '@esbuild/linux-mips64el@0.25.5': optional: true '@esbuild/linux-ppc64@0.18.20': @@ -10795,10 +10938,10 @@ snapshots: '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.25.5': + '@esbuild/linux-ppc64@0.25.4': optional: true - '@esbuild/linux-riscv64@0.17.19': + '@esbuild/linux-ppc64@0.25.5': optional: true '@esbuild/linux-riscv64@0.18.20': @@ -10807,10 +10950,10 @@ snapshots: '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.25.5': + '@esbuild/linux-riscv64@0.25.4': optional: true - '@esbuild/linux-s390x@0.17.19': + '@esbuild/linux-riscv64@0.25.5': optional: true '@esbuild/linux-s390x@0.18.20': @@ -10819,10 +10962,10 @@ snapshots: '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.25.5': + '@esbuild/linux-s390x@0.25.4': optional: true - '@esbuild/linux-x64@0.17.19': + '@esbuild/linux-s390x@0.25.5': optional: true '@esbuild/linux-x64@0.18.20': @@ -10831,13 +10974,16 @@ snapshots: '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.25.4': + optional: true + '@esbuild/linux-x64@0.25.5': optional: true - '@esbuild/netbsd-arm64@0.25.5': + '@esbuild/netbsd-arm64@0.25.4': optional: true - '@esbuild/netbsd-x64@0.17.19': + '@esbuild/netbsd-arm64@0.25.5': optional: true '@esbuild/netbsd-x64@0.18.20': @@ -10846,13 +10992,16 @@ snapshots: '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.25.4': + optional: true + '@esbuild/netbsd-x64@0.25.5': optional: true - '@esbuild/openbsd-arm64@0.25.5': + '@esbuild/openbsd-arm64@0.25.4': optional: true - '@esbuild/openbsd-x64@0.17.19': + '@esbuild/openbsd-arm64@0.25.5': optional: true '@esbuild/openbsd-x64@0.18.20': @@ -10861,10 +11010,10 @@ snapshots: '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.25.5': + '@esbuild/openbsd-x64@0.25.4': optional: true - '@esbuild/sunos-x64@0.17.19': + '@esbuild/openbsd-x64@0.25.5': optional: true '@esbuild/sunos-x64@0.18.20': @@ -10873,10 +11022,10 @@ snapshots: '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.25.5': + '@esbuild/sunos-x64@0.25.4': optional: true - '@esbuild/win32-arm64@0.17.19': + '@esbuild/sunos-x64@0.25.5': optional: true '@esbuild/win32-arm64@0.18.20': @@ -10885,10 +11034,10 @@ snapshots: '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.25.5': + '@esbuild/win32-arm64@0.25.4': optional: true - '@esbuild/win32-ia32@0.17.19': + '@esbuild/win32-arm64@0.25.5': optional: true '@esbuild/win32-ia32@0.18.20': @@ -10897,10 +11046,10 @@ snapshots: '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.25.5': + '@esbuild/win32-ia32@0.25.4': optional: true - '@esbuild/win32-x64@0.17.19': + '@esbuild/win32-ia32@0.25.5': optional: true '@esbuild/win32-x64@0.18.20': @@ -10909,6 +11058,9 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.25.4': + optional: true + '@esbuild/win32-x64@0.25.5': optional: true @@ -11029,9 +11181,19 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@hono/node-server@1.14.4(hono@4.7.11)': + '@hono/node-server@1.14.4(hono@4.9.7)': dependencies: - hono: 4.7.11 + hono: 4.9.7 + + '@hono/standard-validator@0.1.5(@standard-schema/spec@1.0.0)(hono@4.9.7)': + dependencies: + '@standard-schema/spec': 1.0.0 + hono: 4.9.7 + + '@hono/zod-validator@0.7.2(hono@4.9.7)(zod@4.1.8)': + dependencies: + hono: 4.9.7 + zod: 4.1.8 '@humanfs/core@0.19.1': {} @@ -11235,6 +11397,8 @@ snapshots: dependencies: '@swc/helpers': 0.5.17 + '@ioredis/commands@1.4.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -11257,27 +11421,27 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 24.0.1 + '@types/node': 22.15.31 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.0.1 + '@types/node': 22.15.31 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -11302,7 +11466,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.0.1 + '@types/node': 22.15.31 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -11320,7 +11484,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 24.0.1 + '@types/node': 22.15.31 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -11342,7 +11506,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 24.0.1 + '@types/node': 22.15.31 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -11412,7 +11576,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.0.1 + '@types/node': 22.15.31 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -11668,10 +11832,10 @@ snapshots: standardwebhooks: 1.0.0 zod: 3.25.64 - '@polar-sh/sveltekit@0.2.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)))(zod@3.25.64)': + '@polar-sh/sveltekit@0.2.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)))(zod@3.25.64)': dependencies: '@polar-sh/sdk': 0.20.2(zod@3.25.64) - '@sveltejs/kit': 1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)) + '@sveltejs/kit': 1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)) transitivePeerDependencies: - zod @@ -11787,6 +11951,25 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.43.0': optional: true + '@scalar/core@0.3.16': + dependencies: + '@scalar/types': 0.2.15 + + '@scalar/hono-api-reference@0.9.18(hono@4.9.7)': + dependencies: + '@scalar/core': 0.3.16 + hono: 4.9.7 + + '@scalar/openapi-types@0.3.7': + dependencies: + zod: 3.24.1 + + '@scalar/types@0.2.15': + dependencies: + '@scalar/openapi-types': 0.3.7 + nanoid: 5.1.5 + zod: 3.24.1 + '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 @@ -12217,6 +12400,27 @@ snapshots: '@stablelib/base64@1.0.1': {} + '@standard-community/standard-json@0.3.3(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(valibot@1.1.0(typescript@5.8.3))(zod@4.1.8)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@types/json-schema': 7.0.15 + quansync: 0.2.11 + optionalDependencies: + valibot: 1.1.0(typescript@5.8.3) + zod: 4.1.8 + + '@standard-community/standard-openapi@0.2.4(@standard-community/standard-json@0.3.3(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(valibot@1.1.0(typescript@5.8.3))(zod@4.1.8))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(valibot@1.1.0(typescript@5.8.3))(zod-openapi@5.4.1(zod@4.1.8))(zod@4.1.8)': + dependencies: + '@standard-community/standard-json': 0.3.3(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(valibot@1.1.0(typescript@5.8.3))(zod@4.1.8) + '@standard-schema/spec': 1.0.0 + openapi-types: 12.1.3 + optionalDependencies: + valibot: 1.1.0(typescript@5.8.3) + zod: 4.1.8 + zod-openapi: 5.4.1(zod@4.1.8) + + '@standard-schema/spec@1.0.0': {} + '@supabase/auth-js@2.70.0': dependencies: '@supabase/node-fetch': 2.6.15 @@ -12263,14 +12467,14 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-auto@2.1.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)))': + '@sveltejs/adapter-auto@2.1.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)))': dependencies: - '@sveltejs/kit': 1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)) + '@sveltejs/kit': 1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)) import-meta-resolve: 4.1.0 - '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))': + '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)))': dependencies: - '@sveltejs/kit': 2.21.5(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)) + '@sveltejs/kit': 2.21.5(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)) import-meta-resolve: 4.1.0 '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.34.1)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))(svelte@5.34.1)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))': @@ -12278,31 +12482,31 @@ snapshots: '@sveltejs/kit': 2.21.5(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.34.1)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))(svelte@5.34.1)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)) import-meta-resolve: 4.1.0 - '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))': + '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))': dependencies: - '@sveltejs/kit': 2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) + '@sveltejs/kit': 2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) import-meta-resolve: 4.1.0 - '@sveltejs/adapter-node@1.3.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)))': + '@sveltejs/adapter-node@1.3.1(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)))': dependencies: '@rollup/plugin-commonjs': 25.0.8(rollup@3.29.5) '@rollup/plugin-json': 6.1.0(rollup@3.29.5) '@rollup/plugin-node-resolve': 15.3.1(rollup@3.29.5) - '@sveltejs/kit': 1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)) + '@sveltejs/kit': 1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)) rollup: 3.29.5 - '@sveltejs/adapter-vercel@3.1.0(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)))(encoding@0.1.13)': + '@sveltejs/adapter-vercel@3.1.0(@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)))(encoding@0.1.13)': dependencies: - '@sveltejs/kit': 1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)) + '@sveltejs/kit': 1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)) '@vercel/nft': 0.24.4(encoding@0.1.13) esbuild: 0.18.20 transitivePeerDependencies: - encoding - supports-color - '@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2))': + '@sveltejs/kit@1.30.4(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 2.5.3(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)) + '@sveltejs/vite-plugin-svelte': 2.5.3(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)) '@types/cookie': 0.5.4 cookie: 0.5.0 devalue: 4.3.3 @@ -12316,14 +12520,14 @@ snapshots: svelte: 4.2.20 tiny-glob: 0.2.9 undici: 5.29.0 - vite: 4.5.14(@types/node@24.0.1)(sass@1.89.2) + vite: 4.5.14(@types/node@22.15.31)(sass@1.89.2) transitivePeerDependencies: - supports-color - '@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2))': + '@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2))': dependencies: '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)) + '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -12336,8 +12540,8 @@ snapshots: set-cookie-parser: 2.7.1 sirv: 3.0.1 svelte: 4.2.20 - vite: 5.4.19(@types/node@22.15.31)(sass@1.89.2) - vitefu: 1.0.6(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)) + vite: 5.4.19(@types/node@24.0.1)(sass@1.89.2) + vitefu: 1.0.6(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)) '@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.34.1)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))(svelte@5.34.1)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2))': dependencies: @@ -12358,10 +12562,10 @@ snapshots: vite: 5.4.19(@types/node@22.15.31)(sass@1.89.2) vitefu: 1.0.6(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)) - '@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))': + '@sveltejs/kit@2.21.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) + '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -12374,24 +12578,24 @@ snapshots: set-cookie-parser: 2.7.1 sirv: 3.0.1 svelte: 5.34.1 - vite: 6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) - vitefu: 1.0.6(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) + vite: 6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) + vitefu: 1.0.6(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) - '@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.5.3(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)))(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2))': + '@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.5.3(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)))(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 2.5.3(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)) + '@sveltejs/vite-plugin-svelte': 2.5.3(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)) debug: 4.4.1 svelte: 4.2.20 - vite: 4.5.14(@types/node@24.0.1)(sass@1.89.2) + vite: 4.5.14(@types/node@22.15.31)(sass@1.89.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2))': + '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)) + '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)) debug: 4.4.1 svelte: 4.2.20 - vite: 5.4.19(@types/node@22.15.31)(sass@1.89.2) + vite: 5.4.19(@types/node@24.0.1)(sass@1.89.2) transitivePeerDependencies: - supports-color @@ -12404,40 +12608,40 @@ snapshots: transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) + '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) debug: 4.4.1 svelte: 5.34.1 - vite: 6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@2.5.3(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2))': + '@sveltejs/vite-plugin-svelte@2.5.3(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/vite-plugin-svelte@2.5.3(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)))(svelte@4.2.20)(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)) + '@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/vite-plugin-svelte@2.5.3(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)))(svelte@4.2.20)(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)) debug: 4.4.1 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 svelte: 4.2.20 svelte-hmr: 0.15.3(svelte@4.2.20) - vite: 4.5.14(@types/node@24.0.1)(sass@1.89.2) - vitefu: 0.2.5(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)) + vite: 4.5.14(@types/node@22.15.31)(sass@1.89.2) + vitefu: 0.2.5(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2))': + '@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)) + '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)) debug: 4.4.1 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 svelte: 4.2.20 svelte-hmr: 0.16.0(svelte@4.2.20) - vite: 5.4.19(@types/node@22.15.31)(sass@1.89.2) - vitefu: 0.2.5(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)) + vite: 5.4.19(@types/node@24.0.1)(sass@1.89.2) + vitefu: 0.2.5(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)) transitivePeerDependencies: - supports-color @@ -12454,16 +12658,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))': + '@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)))(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) debug: 4.4.1 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 svelte: 5.34.1 - vite: 6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) - vitefu: 1.0.6(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) + vite: 6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) + vitefu: 1.0.6(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) transitivePeerDependencies: - supports-color @@ -12485,9 +12689,9 @@ snapshots: dependencies: tslib: 2.8.1 - '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)))': + '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)))': dependencies: - tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) + tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) '@tailwindcss/forms@0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)))': dependencies: @@ -12756,13 +12960,13 @@ snapshots: '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 24.0.1 + '@types/node': 22.15.31 '@types/geojson@7946.0.16': {} '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 24.0.1 + '@types/node': 22.15.31 '@types/hast@2.3.10': dependencies: @@ -12793,7 +12997,7 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 24.0.1 + '@types/node': 22.15.31 '@types/katex@0.16.7': {} @@ -12815,7 +13019,7 @@ snapshots: '@types/mute-stream@0.0.4': dependencies: - '@types/node': 24.0.1 + '@types/node': 22.15.31 '@types/node@18.11.10': {} @@ -12830,10 +13034,11 @@ snapshots: '@types/node@24.0.1': dependencies: undici-types: 7.8.0 + optional: true '@types/nodemailer@6.4.17': dependencies: - '@types/node': 20.19.0 + '@types/node': 22.15.31 '@types/normalize-package-data@2.4.4': {} @@ -12903,7 +13108,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.0.1 + '@types/node': 22.15.31 '@types/yargs-parser@21.0.3': {} @@ -13056,6 +13261,14 @@ snapshots: optionalDependencies: vite: 5.4.19(@types/node@22.15.31)(sass@1.89.2) + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@24.0.1)(sass@1.89.2) + '@vitest/pretty-format@2.1.9': dependencies: tinyrainbow: 1.2.0 @@ -13668,6 +13881,8 @@ snapshots: clsx@2.1.1: {} + cluster-key-slot@1.1.2: {} + co@4.6.0: {} code-red@1.0.4: @@ -13696,7 +13911,6 @@ snapshots: dependencies: color-name: 1.1.4 simple-swizzle: 0.2.2 - optional: true color-support@1.1.3: {} @@ -13706,7 +13920,6 @@ snapshots: dependencies: color-convert: 2.0.1 color-string: 1.9.1 - optional: true colord@2.9.3: {} @@ -13920,13 +14133,13 @@ snapshots: dependencies: layout-base: 1.0.2 - create-jest@29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)): + create-jest@29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -14250,6 +14463,8 @@ snapshots: delegates@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} dequal@2.0.3: {} @@ -14413,31 +14628,6 @@ snapshots: es6-promise@3.3.1: {} - esbuild@0.17.19: - optionalDependencies: - '@esbuild/android-arm': 0.17.19 - '@esbuild/android-arm64': 0.17.19 - '@esbuild/android-x64': 0.17.19 - '@esbuild/darwin-arm64': 0.17.19 - '@esbuild/darwin-x64': 0.17.19 - '@esbuild/freebsd-arm64': 0.17.19 - '@esbuild/freebsd-x64': 0.17.19 - '@esbuild/linux-arm': 0.17.19 - '@esbuild/linux-arm64': 0.17.19 - '@esbuild/linux-ia32': 0.17.19 - '@esbuild/linux-loong64': 0.17.19 - '@esbuild/linux-mips64el': 0.17.19 - '@esbuild/linux-ppc64': 0.17.19 - '@esbuild/linux-riscv64': 0.17.19 - '@esbuild/linux-s390x': 0.17.19 - '@esbuild/linux-x64': 0.17.19 - '@esbuild/netbsd-x64': 0.17.19 - '@esbuild/openbsd-x64': 0.17.19 - '@esbuild/sunos-x64': 0.17.19 - '@esbuild/win32-arm64': 0.17.19 - '@esbuild/win32-ia32': 0.17.19 - '@esbuild/win32-x64': 0.17.19 - esbuild@0.18.20: optionalDependencies: '@esbuild/android-arm': 0.18.20 @@ -14489,6 +14679,34 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.4 + '@esbuild/android-arm': 0.25.4 + '@esbuild/android-arm64': 0.25.4 + '@esbuild/android-x64': 0.25.4 + '@esbuild/darwin-arm64': 0.25.4 + '@esbuild/darwin-x64': 0.25.4 + '@esbuild/freebsd-arm64': 0.25.4 + '@esbuild/freebsd-x64': 0.25.4 + '@esbuild/linux-arm': 0.25.4 + '@esbuild/linux-arm64': 0.25.4 + '@esbuild/linux-ia32': 0.25.4 + '@esbuild/linux-loong64': 0.25.4 + '@esbuild/linux-mips64el': 0.25.4 + '@esbuild/linux-ppc64': 0.25.4 + '@esbuild/linux-riscv64': 0.25.4 + '@esbuild/linux-s390x': 0.25.4 + '@esbuild/linux-x64': 0.25.4 + '@esbuild/netbsd-arm64': 0.25.4 + '@esbuild/netbsd-x64': 0.25.4 + '@esbuild/openbsd-arm64': 0.25.4 + '@esbuild/openbsd-x64': 0.25.4 + '@esbuild/sunos-x64': 0.25.4 + '@esbuild/win32-arm64': 0.25.4 + '@esbuild/win32-ia32': 0.25.4 + '@esbuild/win32-x64': 0.25.4 + esbuild@0.25.5: optionalDependencies: '@esbuild/aix-ppc64': 0.25.5 @@ -14683,8 +14901,6 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/unist': 2.0.11 - estree-walker@0.6.1: {} - estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -15252,11 +15468,21 @@ snapshots: hey-listen@1.0.8: {} - hono-rate-limiter@0.4.2(hono@4.7.11): + hono-openapi@1.0.8(@hono/standard-validator@0.1.5(@standard-schema/spec@1.0.0)(hono@4.9.7))(@standard-community/standard-json@0.3.3(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(valibot@1.1.0(typescript@5.8.3))(zod@4.1.8))(@standard-community/standard-openapi@0.2.4(@standard-community/standard-json@0.3.3(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(valibot@1.1.0(typescript@5.8.3))(zod@4.1.8))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(valibot@1.1.0(typescript@5.8.3))(zod-openapi@5.4.1(zod@4.1.8))(zod@4.1.8))(@types/json-schema@7.0.15)(hono@4.9.7)(openapi-types@12.1.3): dependencies: - hono: 4.7.11 + '@standard-community/standard-json': 0.3.3(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(valibot@1.1.0(typescript@5.8.3))(zod@4.1.8) + '@standard-community/standard-openapi': 0.2.4(@standard-community/standard-json@0.3.3(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(valibot@1.1.0(typescript@5.8.3))(zod@4.1.8))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(valibot@1.1.0(typescript@5.8.3))(zod-openapi@5.4.1(zod@4.1.8))(zod@4.1.8) + '@types/json-schema': 7.0.15 + openapi-types: 12.1.3 + optionalDependencies: + '@hono/standard-validator': 0.1.5(@standard-schema/spec@1.0.0)(hono@4.9.7) + hono: 4.9.7 - hono@4.7.11: {} + hono-rate-limiter@0.4.2(hono@4.9.7): + dependencies: + hono: 4.9.7 + + hono@4.9.7: {} hosted-git-info@2.8.9: {} @@ -15401,6 +15627,20 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.2 tslib: 2.8.1 + ioredis@5.7.0: + dependencies: + '@ioredis/commands': 1.4.0 + cluster-key-slot: 1.1.2 + debug: 4.4.1 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -15421,8 +15661,7 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: - optional: true + is-arrayish@0.3.2: {} is-bigint@1.1.0: dependencies: @@ -15638,7 +15877,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.0.1 + '@types/node': 22.15.31 chalk: 4.1.2 co: 4.6.0 dedent: 1.6.0 @@ -15658,16 +15897,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)): + jest-cli@29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + create-jest: 29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -15677,7 +15916,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)): + jest-config@29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)): dependencies: '@babel/core': 7.27.4 '@jest/test-sequencer': 29.7.0 @@ -15702,8 +15941,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 24.0.1 - ts-node: 10.9.2(@types/node@24.0.1)(typescript@5.8.3) + '@types/node': 22.15.31 + ts-node: 10.9.2(@types/node@22.15.31)(typescript@5.8.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -15732,7 +15971,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.0.1 + '@types/node': 22.15.31 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -15742,7 +15981,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 24.0.1 + '@types/node': 22.15.31 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -15781,7 +16020,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.0.1 + '@types/node': 22.15.31 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -15816,7 +16055,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.0.1 + '@types/node': 22.15.31 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -15844,7 +16083,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.0.1 + '@types/node': 22.15.31 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.2 @@ -15890,7 +16129,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.0.1 + '@types/node': 22.15.31 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -15909,7 +16148,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.0.1 + '@types/node': 22.15.31 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -15918,17 +16157,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 24.0.1 + '@types/node': 22.15.31 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)): + jest@29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + jest-cli: 29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -16064,6 +16303,8 @@ snapshots: kleur@4.1.5: {} + ky@1.10.0: {} + layout-base@1.0.2: {} leac@0.6.0: {} @@ -16125,8 +16366,12 @@ snapshots: lodash.debounce@4.0.8: {} + lodash.defaults@4.2.0: {} + lodash.get@4.4.2: {} + lodash.isarguments@3.1.0: {} + lodash.ismatch@4.4.0: {} lodash.isplainobject@4.0.6: {} @@ -16175,10 +16420,6 @@ snapshots: lz-string@1.5.0: {} - magic-string@0.25.9: - dependencies: - sourcemap-codec: 1.4.8 - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -16793,16 +17034,17 @@ snapshots: mini-svg-data-uri@1.4.4: {} - miniflare@3.20250408.2(bufferutil@4.0.9)(utf-8-validate@6.0.5): + miniflare@4.20250617.4(bufferutil@4.0.9)(utf-8-validate@6.0.5): dependencies: '@cspotcode/source-map-support': 0.8.1 acorn: 8.14.0 acorn-walk: 8.3.2 exit-hook: 2.2.1 glob-to-regexp: 0.4.1 + sharp: 0.33.5 stoppable: 1.1.0 undici: 5.29.0 - workerd: 1.20250408.0 + workerd: 1.20250617.0 ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) youch: 3.3.4 zod: 3.22.3 @@ -16913,19 +17155,19 @@ snapshots: transitivePeerDependencies: - supports-color - next-seo@6.8.0(next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-seo@6.8.0(next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - next: 13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2) + next: 13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next-themes@0.2.1(next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-themes@0.2.1(next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - next: 13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2) + next: 13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2): + next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2): dependencies: '@next/env': 13.5.11 '@swc/helpers': 0.5.2 @@ -16934,7 +17176,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(@babel/core@7.27.4)(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) watchpack: 2.4.0 optionalDependencies: '@next/swc-darwin-arm64': 13.5.9 @@ -16951,7 +17193,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@2.13.2(next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(nextra@2.13.2(next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextra-theme-docs@2.13.2(next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(nextra@2.13.2(next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@popperjs/core': 2.11.8 @@ -16962,16 +17204,16 @@ snapshots: git-url-parse: 13.1.1 intersection-observer: 0.12.2 match-sorter: 6.3.4 - next: 13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2) - next-seo: 6.8.0(next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next-themes: 0.2.1(next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nextra: 2.13.2(next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2) + next-seo: 6.8.0(next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-themes: 0.2.1(next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nextra: 2.13.2(next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) scroll-into-view-if-needed: 3.1.0 zod: 3.25.64 - nextra@2.13.2(next@13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextra@2.13.2(next@13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdx-js/mdx': 2.3.0 @@ -16985,7 +17227,7 @@ snapshots: gray-matter: 4.0.3 katex: 0.16.22 lodash.get: 4.4.2 - next: 13.5.11(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2) + next: 13.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2) next-mdx-remote: 4.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) p-limit: 3.1.0 react: 18.3.1 @@ -17129,6 +17371,8 @@ snapshots: openai-edge@1.2.3: {} + openapi-types@12.1.3: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -17497,6 +17741,8 @@ snapshots: dependencies: side-channel: 1.1.0 + quansync@0.2.11: {} + queue-lit@1.5.2: {} queue-microtask@1.2.3: {} @@ -17586,6 +17832,12 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + regenerate-unicode-properties@10.2.0: dependencies: regenerate: 1.4.2 @@ -17768,20 +18020,6 @@ snapshots: robust-predicates@3.0.2: {} - rollup-plugin-inject@3.0.2: - dependencies: - estree-walker: 0.6.1 - magic-string: 0.25.9 - rollup-pluginutils: 2.8.2 - - rollup-plugin-node-polyfills@0.2.1: - dependencies: - rollup-plugin-inject: 3.0.2 - - rollup-pluginutils@2.8.2: - dependencies: - estree-walker: 0.6.1 - rollup@3.29.5: optionalDependencies: fsevents: 2.3.3 @@ -17943,7 +18181,6 @@ snapshots: '@img/sharp-wasm32': 0.33.5 '@img/sharp-win32-ia32': 0.33.5 '@img/sharp-win32-x64': 0.33.5 - optional: true shebang-command@1.2.0: dependencies: @@ -18014,7 +18251,6 @@ snapshots: simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 - optional: true sirv@2.0.4: dependencies: @@ -18065,8 +18301,6 @@ snapshots: source-map@0.7.4: {} - sourcemap-codec@1.4.8: {} - space-separated-tokens@2.0.2: {} spawn-command@0.0.2: {} @@ -18114,6 +18348,8 @@ snapshots: as-table: 1.0.55 get-source: 2.0.12 + standard-as-callback@2.1.0: {} + standard-version@9.5.0: dependencies: chalk: 2.4.2 @@ -18221,7 +18457,7 @@ snapshots: stripe@14.25.0: dependencies: - '@types/node': 24.0.1 + '@types/node': 22.15.31 qs: 6.14.0 strnum@1.1.2: {} @@ -18235,12 +18471,10 @@ snapshots: hey-listen: 1.0.8 tslib: 2.4.0 - styled-jsx@5.1.1(@babel/core@7.27.4)(react@18.3.1): + styled-jsx@5.1.1(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 - optionalDependencies: - '@babel/core': 7.27.4 stylis@4.3.6: {} @@ -18287,14 +18521,14 @@ snapshots: dayjs: 1.11.13 just-throttle: 2.3.1 - svelte-check@3.8.6(@babel/core@7.27.4)(postcss-load-config@4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)))(postcss@8.5.5)(sass@1.89.2)(svelte@4.2.20): + svelte-check@3.8.6(@babel/core@7.27.4)(postcss-load-config@4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)))(postcss@8.5.5)(sass@1.89.2)(svelte@4.2.20): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 3.6.0 picocolors: 1.1.1 sade: 1.8.1 svelte: 4.2.20 - svelte-preprocess: 5.1.4(@babel/core@7.27.4)(postcss-load-config@4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)))(postcss@8.5.5)(sass@1.89.2)(svelte@4.2.20)(typescript@5.8.3) + svelte-preprocess: 5.1.4(@babel/core@7.27.4)(postcss-load-config@4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)))(postcss@8.5.5)(sass@1.89.2)(svelte@4.2.20)(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - '@babel/core' @@ -18341,9 +18575,9 @@ snapshots: dependencies: svelte: 5.34.1 - svelte-jester@3.0.0(jest@29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)))(svelte@4.2.20): + svelte-jester@3.0.0(jest@29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)))(svelte@4.2.20): dependencies: - jest: 29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + jest: 29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) svelte: 4.2.20 svelte-loading-spinners@0.3.6: {} @@ -18483,10 +18717,10 @@ snapshots: tailwind-merge: 2.6.0 tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) - tailwind-variants@0.3.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3))): + tailwind-variants@0.3.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3))): dependencies: tailwind-merge: 2.5.4 - tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) + tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) tailwindcss-animate@1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3))): dependencies: @@ -18703,12 +18937,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@24.0.1)(ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3)) + jest: 29.7.0(@types/node@22.15.31)(ts-node@10.9.2(@types/node@22.15.31)(typescript@5.8.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -18777,7 +19011,6 @@ snapshots: typescript: 5.8.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - optional: true ts-node@10.9.2(@types/node@24.0.1)(typescript@5.8.3): dependencies: @@ -18796,6 +19029,7 @@ snapshots: typescript: 5.8.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optional: true tsc-alias@1.8.16: dependencies: @@ -18887,7 +19121,8 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.8.0: {} + undici-types@7.8.0: + optional: true undici@5.29.0: dependencies: @@ -18895,7 +19130,7 @@ snapshots: undici@6.21.3: {} - unenv@2.0.0-rc.14: + unenv@2.0.0-rc.17: dependencies: defu: 6.1.4 exsolve: 1.0.5 @@ -19065,6 +19300,11 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + valibot@1.1.0(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + optional: true + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 @@ -19144,13 +19384,31 @@ snapshots: - supports-color - terser - vite@4.5.14(@types/node@24.0.1)(sass@1.89.2): + vite-node@2.1.9(@types/node@24.0.1)(sass@1.89.2): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@24.0.1)(sass@1.89.2) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@4.5.14(@types/node@22.15.31)(sass@1.89.2): dependencies: esbuild: 0.18.20 postcss: 8.5.5 rollup: 3.29.5 optionalDependencies: - '@types/node': 24.0.1 + '@types/node': 22.15.31 fsevents: 2.3.3 sass: 1.89.2 @@ -19174,7 +19432,17 @@ snapshots: fsevents: 2.3.3 sass: 1.89.2 - vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0): + vite@5.4.19(@types/node@24.0.1)(sass@1.89.2): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.5 + rollup: 4.43.0 + optionalDependencies: + '@types/node': 24.0.1 + fsevents: 2.3.3 + sass: 1.89.2 + + vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.5 fdir: 6.4.6(picomatch@4.0.2) @@ -19183,28 +19451,32 @@ snapshots: rollup: 4.43.0 tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 22.15.31 + '@types/node': 24.0.1 fsevents: 2.3.3 jiti: 1.21.7 sass: 1.89.2 tsx: 4.20.3 yaml: 2.8.0 - vitefu@0.2.5(vite@4.5.14(@types/node@24.0.1)(sass@1.89.2)): + vitefu@0.2.5(vite@4.5.14(@types/node@22.15.31)(sass@1.89.2)): optionalDependencies: - vite: 4.5.14(@types/node@24.0.1)(sass@1.89.2) + vite: 4.5.14(@types/node@22.15.31)(sass@1.89.2) - vitefu@0.2.5(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)): + vitefu@0.2.5(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)): optionalDependencies: - vite: 5.4.19(@types/node@22.15.31)(sass@1.89.2) + vite: 5.4.19(@types/node@24.0.1)(sass@1.89.2) vitefu@1.0.6(vite@5.4.19(@types/node@22.15.31)(sass@1.89.2)): optionalDependencies: vite: 5.4.19(@types/node@22.15.31)(sass@1.89.2) - vitefu@1.0.6(vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)): + vitefu@1.0.6(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)): optionalDependencies: - vite: 6.3.5(@types/node@22.15.31)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) + vite: 5.4.19(@types/node@24.0.1)(sass@1.89.2) + + vitefu@1.0.6(vite@6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)): + optionalDependencies: + vite: 6.3.5(@types/node@24.0.1)(jiti@1.21.7)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vitest@1.6.1(@types/node@20.19.0)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass@1.89.2): dependencies: @@ -19277,6 +19549,42 @@ snapshots: - supports-color - terser + vitest@2.1.9(@types/node@24.0.1)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(sass@1.89.2): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@24.0.1)(sass@1.89.2)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + debug: 4.4.1 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.0 + tinyrainbow: 1.2.0 + vite: 5.4.19(@types/node@24.0.1)(sass@1.89.2) + vite-node: 2.1.9(@types/node@24.0.1)(sass@1.89.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.0.1 + jsdom: 25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vscode-oniguruma@1.7.0: {} vscode-textmate@8.0.0: {} @@ -19390,30 +19698,26 @@ snapshots: wordwrap@1.0.0: {} - workerd@1.20250408.0: + workerd@1.20250617.0: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20250408.0 - '@cloudflare/workerd-darwin-arm64': 1.20250408.0 - '@cloudflare/workerd-linux-64': 1.20250408.0 - '@cloudflare/workerd-linux-arm64': 1.20250408.0 - '@cloudflare/workerd-windows-64': 1.20250408.0 + '@cloudflare/workerd-darwin-64': 1.20250617.0 + '@cloudflare/workerd-darwin-arm64': 1.20250617.0 + '@cloudflare/workerd-linux-64': 1.20250617.0 + '@cloudflare/workerd-linux-arm64': 1.20250617.0 + '@cloudflare/workerd-windows-64': 1.20250617.0 - wrangler@3.114.9(@cloudflare/workers-types@4.20250613.0)(bufferutil@4.0.9)(utf-8-validate@6.0.5): + wrangler@4.21.2(bufferutil@4.0.9)(utf-8-validate@6.0.5): dependencies: - '@cloudflare/kv-asset-handler': 0.3.4 - '@cloudflare/unenv-preset': 2.0.2(unenv@2.0.0-rc.14)(workerd@1.20250408.0) - '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) - '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) + '@cloudflare/kv-asset-handler': 0.4.0 + '@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0) blake3-wasm: 2.1.5 - esbuild: 0.17.19 - miniflare: 3.20250408.2(bufferutil@4.0.9)(utf-8-validate@6.0.5) + esbuild: 0.25.4 + miniflare: 4.20250617.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.14 - workerd: 1.20250408.0 + unenv: 2.0.0-rc.17 + workerd: 1.20250617.0 optionalDependencies: - '@cloudflare/workers-types': 4.20250613.0 fsevents: 2.3.3 - sharp: 0.33.5 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -19536,8 +19840,16 @@ snapshots: zimmerframe@1.1.2: {} + zod-openapi@5.4.1(zod@4.1.8): + dependencies: + zod: 4.1.8 + zod@3.22.3: {} + zod@3.24.1: {} + zod@3.25.64: {} + zod@4.1.8: {} + zwitch@2.0.4: {}