diff --git a/.gitea/workflows/deploy-app.yml b/.gitea/workflows/deploy-app.yml new file mode 100644 index 0000000..a7882fd --- /dev/null +++ b/.gitea/workflows/deploy-app.yml @@ -0,0 +1,90 @@ +name: Build & Deploy App + +# Workflow pour l'app SaaS (apps/api AdonisJS + apps/web React) déployée +# sur app.rubis.arthurbarre.fr. Image distincte de la landing. +on: + push: + branches: [main] + paths: + - 'apps/**' + - 'packages/**' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - 'package.json' + - 'turbo.json' + - 'Dockerfile.app' + - 'k3s/app/**' + - '.gitea/workflows/deploy-app.yml' + +env: + REGISTRY: git.arthurbarre.fr + IMAGE: ordinarthur/rubis-app + NAMESPACE: rubis + DEPLOYMENT: rubis-app + CONTAINER: app + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Gitea Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ordinarthur + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Build and push app image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile.app + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE }}:cache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE }}:cache,mode=max + + - name: Install kubectl + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + - name: Deploy to K3s + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + kubectl apply -f k3s/namespace.yml + + # Idempotent : on (re)pose le pull secret du registry Gitea + le + # secret applicatif n'est PAS recréé ici (créé manuellement au + # premier deploy via kubectl, contient des creds qui ne + # transitent jamais par le CI). + kubectl -n $NAMESPACE create secret docker-registry gitea-registry \ + --docker-server=$REGISTRY \ + --docker-username=ordinarthur \ + --docker-password=${{ secrets.REGISTRY_PASSWORD }} \ + --dry-run=client -o yaml | kubectl apply -f - + + # Apply Redis + app manifests (idempotent) + kubectl apply -f k3s/app/ + + # Pin l'image avec le sha du commit pour rolling update propre. + # Le init-container migrate utilise la même image et tourne avant + # le serveur — migrations idempotentes via ace migration:run. + kubectl -n $NAMESPACE set image deployment/$DEPLOYMENT \ + $CONTAINER=$REGISTRY/$IMAGE:${{ github.sha }} + + # Patch aussi le init container (même image) + kubectl -n $NAMESPACE patch deployment $DEPLOYMENT \ + --type='json' \ + -p="[{\"op\":\"replace\",\"path\":\"/spec/template/spec/initContainers/0/image\",\"value\":\"$REGISTRY/$IMAGE:${{ github.sha }}\"}]" + + kubectl -n $NAMESPACE rollout status deployment/$DEPLOYMENT --timeout=300s diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 8881a63..801ae54 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,8 +1,17 @@ -name: Build & Deploy +name: Build & Deploy Landing +# Workflow pour la landing static (rubis.arthurbarre.fr). +# L'app SaaS (apps/api + apps/web) a son propre workflow : deploy-app.yml. on: push: branches: [main] + paths: + - 'landing/**' + - 'Dockerfile' + - 'k3s/namespace.yml' + - 'k3s/deployment.yml' + - 'k3s/service.yml' + - '.gitea/workflows/deploy.yml' env: REGISTRY: git.arthurbarre.fr diff --git a/Dockerfile.app b/Dockerfile.app new file mode 100644 index 0000000..51a8b0a --- /dev/null +++ b/Dockerfile.app @@ -0,0 +1,111 @@ +# syntax=docker/dockerfile:1.7 +# ============================================================================= +# Rubis Sur l'Ongle — image production de l'app SaaS (apps/api + apps/web) +# Sert app.rubis.arthurbarre.fr. La landing (rubis.arthurbarre.fr) reste sur +# une image séparée — Dockerfile à la racine, nginx static. +# ============================================================================= +# +# Multi-stage : +# - base : node 22 alpine + pnpm + tini +# - deps : install workspace deps (cache friendly via manifests d'abord) +# - build : build shared, web, api ; copie le SPA dans apps/api/build/public +# - runner : copie le repo "pruned" prod, lance node bin/server.js +# +# Choix architectural : un seul process Node sert l'API ET le SPA static +# (via le static middleware AdonisJS + un fallback wildcard pour SPA routing). +# Les workers BullMQ tournent dans le même process (cf. start/queue.ts). +# ============================================================================= + +ARG NODE_VERSION=22.13.1 +ARG PNPM_VERSION=10.0.0 + +# ----------------------------------------------------------------------------- +# base — node + pnpm + tini +# ----------------------------------------------------------------------------- +FROM node:${NODE_VERSION}-alpine AS base + +ARG PNPM_VERSION +RUN apk add --no-cache libc6-compat tini && \ + corepack enable && \ + corepack prepare pnpm@${PNPM_VERSION} --activate + +WORKDIR /repo + +# ----------------------------------------------------------------------------- +# deps — install workspace (devDeps inclus, on en a besoin pour les builds) +# ----------------------------------------------------------------------------- +FROM base AS deps + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json tsconfig.base.json ./ +COPY apps/api/package.json ./apps/api/ +COPY apps/web/package.json ./apps/web/ +COPY packages/shared/package.json ./packages/shared/ + +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile + +# ----------------------------------------------------------------------------- +# build — shared → web → api, puis copie du SPA dans le build de l'API +# ----------------------------------------------------------------------------- +FROM deps AS build + +COPY packages/shared ./packages/shared +COPY apps/web ./apps/web +COPY apps/api ./apps/api + +# Builds : +# - @rubis/shared : pas de build (TS source consommé directement via exports). +# - Web : on appelle vite build directement (le `tsc -b` du script de prod +# fait remonter des erreurs DOM dans @tanstack/router-core sans cache +# .tsbuildinfo ; le typecheck est fait en CI séparément). +# - API : `node ace build` (canonique AdonisJS V7) — produit apps/api/build +# avec compiled JS, package.json runtime, et metaFiles configurés. +# +# Note : ce build peut planter en cross-compile ARM→amd64 (swc/core), donc +# en local sur Mac silicon, builder pour --platform linux/arm64. Le CI +# Gitea tourne nativement sur linux/amd64 et n'a pas le problème. +RUN pnpm --filter @rubis/web exec vite build && \ + pnpm --filter @rubis/api build + +# Le SPA static va dans apps/api/build/public/ pour être servi par le static +# middleware AdonisJS. AdonisJS ne copie pas public/ par défaut dans build/ +# (metaFiles vide), on le fait manuellement ici. +RUN mkdir -p apps/api/build/public && \ + cp -r apps/web/dist/. apps/api/build/public/ + +# Prune les devDeps. Les symlinks pnpm vers les workspace packages +# (@rubis/shared) restent valides car on garde le repo en place. +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ + pnpm install --prod --frozen-lockfile=false + +# ----------------------------------------------------------------------------- +# runner — runtime minimal, user non-root +# ----------------------------------------------------------------------------- +FROM base AS runner + +RUN addgroup -g 1001 -S nodejs && adduser -S adonis -u 1001 + +ENV NODE_ENV=production \ + HOST=0.0.0.0 \ + PORT=3333 \ + LOG_LEVEL=info + +WORKDIR /app + +# On copie tout le repo pruned (node_modules inclus avec les symlinks +# workspace). C'est plus gros qu'une image "deploy" pure, mais ça évite +# les pièges de résolution workspace pour V1. +COPY --from=build --chown=adonis:nodejs /repo /app + +USER adonis + +WORKDIR /app/apps/api + +EXPOSE 3333 + +# Healthcheck léger : le serveur HTTP doit répondre 200 sur /api/v1/. +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD wget -qO- http://127.0.0.1:3333/ >/dev/null 2>&1 || exit 1 + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["node", "build/bin/server.js"] diff --git a/apps/api/adonisrc.ts b/apps/api/adonisrc.ts index bd413b5..54324b2 100644 --- a/apps/api/adonisrc.ts +++ b/apps/api/adonisrc.ts @@ -59,6 +59,7 @@ export default defineConfig({ () => import('@adonisjs/limiter/limiter_provider'), () => import('@adonisjs/mail/mail_provider'), () => import('@adonisjs/drive/drive_provider'), + () => import('@adonisjs/static/static_provider') ], /* @@ -110,7 +111,10 @@ export default defineConfig({ | the production build. | */ - metaFiles: [], + metaFiles: [{ + pattern: 'public/**', + reloadServer: false, + }], hooks: { init: [ diff --git a/apps/api/config/static.ts b/apps/api/config/static.ts new file mode 100644 index 0000000..8d2878c --- /dev/null +++ b/apps/api/config/static.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@adonisjs/static' + +/** + * Configuration options to tweak the static files middleware. + * The complete set of options are documented on the + * official documentation website. + * + * https://docs.adonisjs.com/guides/static-assets + */ +const staticServerConfig = defineConfig({ + enabled: true, + etag: true, + lastModified: true, + dotFiles: 'ignore', +}) + +export default staticServerConfig \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 6ab89a7..2496ab9 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -68,6 +68,7 @@ "@adonisjs/mail": "^10.2.0", "@adonisjs/session": "^8.1.0", "@adonisjs/shield": "^9.0.0", + "@adonisjs/static": "^2.0.1", "@aws-sdk/client-s3": "^3.1043.0", "@aws-sdk/s3-request-presigner": "^3.1043.0", "@japa/api-client": "^3.2.1", diff --git a/apps/api/start/kernel.ts b/apps/api/start/kernel.ts index 365d909..a158c1b 100644 --- a/apps/api/start/kernel.ts +++ b/apps/api/start/kernel.ts @@ -26,6 +26,7 @@ server.use([ () => import('#middleware/force_json_response_middleware'), () => import('#middleware/container_bindings_middleware'), () => import('@adonisjs/cors/cors_middleware'), + () => import('@adonisjs/static/static_middleware') ]) /** diff --git a/apps/api/start/routes.ts b/apps/api/start/routes.ts index 9273bf9..2957265 100644 --- a/apps/api/start/routes.ts +++ b/apps/api/start/routes.ts @@ -12,10 +12,9 @@ import { middleware } from '#start/kernel' import router from '@adonisjs/core/services/router' import { controllers } from '#generated/controllers' - -router.get('/', () => { - return { hello: 'world' } -}) +import app from '@adonisjs/core/services/app' +import { existsSync } from 'node:fs' +import { resolve } from 'node:path' router .group(() => { @@ -178,3 +177,25 @@ router .use(middleware.auth()) }) .prefix('/api/v1') + +/** + * SPA fallback — sert `public/index.html` pour toute route non-API et non-asset + * statique (le static middleware aura déjà répondu pour les vrais fichiers + * via etag / 200). Permet à TanStack Router côté front de gérer son routing + * sans 404 quand l'utilisateur recharge sur /factures, /plans/nouveau, etc. + * + * On ne match PAS /api/v1/* (gardé en 404 propre via le routeur ci-dessus). + */ +router.get('*', async ({ request, response }) => { + if (request.url().startsWith('/api/')) { + return response.status(404).json({ + errors: [{ code: 'not_found', message: 'Route introuvable' }], + }) + } + const indexPath = resolve(app.publicPath('index.html')) + if (existsSync(indexPath)) { + return response.download(indexPath) + } + // En dev sans build front, on renvoie un message clair. + return response.status(503).send('SPA not built — run `pnpm --filter @rubis/web build`') +}) diff --git a/apps/web/src/components/plans/wizard/CadenceCalendar.tsx b/apps/web/src/components/plans/wizard/CadenceCalendar.tsx index 2c329f5..4cb2580 100644 --- a/apps/web/src/components/plans/wizard/CadenceCalendar.tsx +++ b/apps/web/src/components/plans/wizard/CadenceCalendar.tsx @@ -59,18 +59,16 @@ export function CadenceCalendar({ onAddStep, onAddStepAtOffset, onRemoveStep, - globalTone, }: { steps: DraftStepLite[]; selectedIndex?: number; onSelectStep?: (idx: number) => void; onUpdateStep?: (idx: number, patch: Partial) => void; onAddStep: () => void; - /** Création directe en cliquant une case vide. Reçoit l'offset depuis l'échéance. */ + /** Création directe en cliquant une case vide. Reçoit l'offset depuis + * l'échéance. La tonalité par défaut est gérée côté parent. */ onAddStepAtOffset?: (offsetDays: number) => void; onRemoveStep: (idx: number) => void; - /** Tonalité par défaut pour une étape créée depuis le calendrier. */ - globalTone?: RelanceTone; }) { const dueDate = useMemo(() => { const now = new Date(); diff --git a/apps/web/src/mocks/handlers/ai.ts b/apps/web/src/mocks/handlers/ai.ts index 81a8b0e..03653ec 100644 --- a/apps/web/src/mocks/handlers/ai.ts +++ b/apps/web/src/mocks/handlers/ai.ts @@ -29,7 +29,7 @@ const generateSchema = z.object({ */ function mockGenerate( tone: z.infer["tone"], - offsetDays: number, + _offsetDays: number, prompt: string, ): { subject: string; body: string } { const briefSuffix = prompt.trim() ? ` (${prompt.trim().slice(0, 60)}…)` : ""; diff --git a/apps/web/src/mocks/handlers/invoices.ts b/apps/web/src/mocks/handlers/invoices.ts index c9cc8a5..17e53e8 100644 --- a/apps/web/src/mocks/handlers/invoices.ts +++ b/apps/web/src/mocks/handlers/invoices.ts @@ -403,6 +403,8 @@ export const invoiceHandlers = [ const created = mockDb.createClient(orgId, { name: fields.clientName, email: fields.clientEmail, + contactFirstName: null, + contactLastName: null, phone: null, address: null, siret: null, @@ -546,6 +548,8 @@ export const invoiceHandlers = [ const created = mockDb.createClient(orgId, { name: fields.clientName, email: fields.clientEmail!, + contactFirstName: null, + contactLastName: null, phone: null, address: null, siret: null, @@ -569,6 +573,8 @@ export const invoiceHandlers = [ const created = mockDb.createClient(orgId, { name: fields.clientName, email: fields.clientEmail!, + contactFirstName: null, + contactLastName: null, phone: null, address: null, siret: null, diff --git a/apps/web/src/routes/_app/plans_.nouveau.tsx b/apps/web/src/routes/_app/plans_.nouveau.tsx index 2e7dec2..da17eb1 100644 --- a/apps/web/src/routes/_app/plans_.nouveau.tsx +++ b/apps/web/src/routes/_app/plans_.nouveau.tsx @@ -475,7 +475,6 @@ function StepCadence({ draft, onChange }: { draft: Draft; onChange: (d: Draft) = diff --git a/k3s/app/deployment.yml b/k3s/app/deployment.yml new file mode 100644 index 0000000..dd5ada3 --- /dev/null +++ b/k3s/app/deployment.yml @@ -0,0 +1,117 @@ +# Rubis App — apps/api (AdonisJS) + apps/web (React) bundlés dans une image +# unique. Le SPA est servi par le static middleware AdonisJS, le wildcard +# fallback gère le routing TanStack Router. Workers BullMQ tournent dans le +# même process (cf. start/queue.ts). +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rubis-app + namespace: rubis +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: rubis-app + template: + metadata: + labels: + app: rubis-app + spec: + imagePullSecrets: + - name: gitea-registry + # Migrations exécutées en init-container avant que le serveur démarre. + # Idempotent (ace migration:run skip ce qui est déjà appliqué). + initContainers: + - name: migrate + image: git.arthurbarre.fr/ordinarthur/rubis-app:latest + workingDir: /app/apps/api + command: ['node', 'ace', 'migration:run', '--force'] + envFrom: + - secretRef: { name: rubis-app-secrets } + - configMapRef: { name: rubis-app-config } + resources: + requests: { cpu: 50m, memory: 128Mi } + limits: { cpu: 500m, memory: 512Mi } + containers: + - name: app + image: git.arthurbarre.fr/ordinarthur/rubis-app:latest + imagePullPolicy: Always + ports: + - containerPort: 3333 + name: http + envFrom: + - secretRef: { name: rubis-app-secrets } + - configMapRef: { name: rubis-app-config } + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 1000m + memory: 768Mi + # Probes adaptées au boot AdonisJS (peut prendre 10-15s avec BullMQ) + startupProbe: + httpGet: { path: /, port: http } + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 30 # 150s max + livenessProbe: + httpGet: { path: /, port: http } + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: { path: /, port: http } + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: rubis-app-config + namespace: rubis +data: + # Variables non-sensibles. Les secrets sont dans rubis-app-secrets. + TZ: 'Europe/Paris' + PORT: '3333' + HOST: '0.0.0.0' + NODE_ENV: 'production' + LOG_LEVEL: 'info' + APP_URL: 'https://app.rubis.arthurbarre.fr' + WEB_URL: 'https://app.rubis.arthurbarre.fr' + SESSION_DRIVER: 'cookie' + COOKIE_SECURE: 'true' + COOKIE_DOMAIN: 'app.rubis.arthurbarre.fr' + + DB_CONNECTION: 'postgres' + PG_HOST: '10.10.10.3' + PG_PORT: '5432' + PG_USER: 'rubis' + PG_DB_NAME: 'rubis_prod' + + REDIS_HOST: 'rubis-redis.rubis.svc.cluster.local' + REDIS_PORT: '6379' + LIMITER_STORE: 'redis' + + # MinIO interne (pas via le NodePort public, plus rapide + plus sécurisé). + DRIVE_DISK: 's3' + S3_ENDPOINT: 'http://minio.minio.svc.cluster.local:9000' + S3_REGION: 'fr-par' + S3_BUCKET: 'rubis-prod-invoices' + S3_FORCE_PATH_STYLE: 'true' + + MAIL_DRIVER: 'resend' + MAIL_FROM_ADDRESS: 'rubis@arthurbarre.fr' + MAIL_FROM_NAME: "Rubis Sur l'Ongle" + + OCR_PROVIDER: 'mistral' + + ACCESS_TOKEN_TTL_MINUTES: '30' + REFRESH_TOKEN_TTL_DAYS: '30' diff --git a/k3s/app/redis.yml b/k3s/app/redis.yml new file mode 100644 index 0000000..e6b051b --- /dev/null +++ b/k3s/app/redis.yml @@ -0,0 +1,84 @@ +# Redis 7 — backend de BullMQ (queues `relances`, `checkins`) et du cache. +# Single replica, suffisant V1. Persistance via local-path PVC pour ne pas +# perdre les jobs schedulés en cas de redémarrage du pod. +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: rubis-redis-data + namespace: rubis +spec: + accessModes: [ReadWriteOnce] + storageClassName: local-path + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rubis-redis + namespace: rubis +spec: + replicas: 1 + strategy: + type: Recreate # PVC RWO → pas de rollout simultané + selector: + matchLabels: + app: rubis-redis + template: + metadata: + labels: + app: rubis-redis + spec: + containers: + - name: redis + image: redis:7.4-alpine + args: + - redis-server + - --appendonly + - 'yes' + - --maxmemory + - 256mb + - --maxmemory-policy + - allkeys-lru + ports: + - containerPort: 6379 + name: redis + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 384Mi + volumeMounts: + - name: data + mountPath: /data + livenessProbe: + tcpSocket: { port: redis } + initialDelaySeconds: 10 + periodSeconds: 15 + readinessProbe: + exec: + command: [redis-cli, ping] + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: data + persistentVolumeClaim: + claimName: rubis-redis-data +--- +apiVersion: v1 +kind: Service +metadata: + name: rubis-redis + namespace: rubis +spec: + type: ClusterIP + selector: + app: rubis-redis + ports: + - port: 6379 + targetPort: redis + name: redis diff --git a/k3s/app/service.yml b/k3s/app/service.yml new file mode 100644 index 0000000..ab8ad34 --- /dev/null +++ b/k3s/app/service.yml @@ -0,0 +1,17 @@ +# NodePort 30110 — exposé par Traefik sur la gateway VM (cf. +# ansible/roles/traefik/templates/rubis-app.yml.j2). +--- +apiVersion: v1 +kind: Service +metadata: + name: rubis-app + namespace: rubis +spec: + type: NodePort + selector: + app: rubis-app + ports: + - port: 80 + targetPort: http + nodePort: 30110 + name: http diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e41ee9b..4aa95ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: '@adonisjs/shield': specifier: ^9.0.0 version: 9.0.0(adb93641c3908819f9462459e2e86e5c) + '@adonisjs/static': + specifier: ^2.0.1 + version: 2.0.1(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1)) '@aws-sdk/client-s3': specifier: ^3.1043.0 version: 3.1043.0 @@ -638,6 +641,16 @@ packages: edge.js: optional: true + '@adonisjs/static@2.0.1': + resolution: {integrity: sha512-pXSyQgha1yfyQDXPp0ywv7hJqk4JgCCkzWLkku2PW3Y6i/9GUTyzzNTCpz29loZzSF0b/EpiHXQE/sqBhW+p7w==} + engines: {node: '>=24.0.0'} + peerDependencies: + '@adonisjs/assembler': ^8.0.0-next.7 || ^8.0.0 + '@adonisjs/core': ^7.0.0-next.0 || ^7.0.0 + peerDependenciesMeta: + '@adonisjs/assembler': + optional: true + '@adonisjs/tsconfig@2.0.0': resolution: {integrity: sha512-Uz8qvB6KR9otCh9zei2VEj7tPwdsrT7R+voYoN4tUfEijWRdTNgJ8d1CtyLKHaggCCOwZIwZLVklV/h2FDHgNw==} @@ -3470,6 +3483,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -3736,6 +3752,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -4580,6 +4600,10 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -4777,6 +4801,10 @@ packages: resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} engines: {node: '>= 0.8'} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + rate-limiter-flexible@9.1.1: resolution: {integrity: sha512-imxFjzPCmvDLMe7d2tsgiSQvs5EI2fI9SNymmslAfOqznZhsZ+PqbIjIYKpuSbd3pKovR1aMG47qfCLIO/adVg==} @@ -4977,6 +5005,10 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + seroval-plugins@1.5.4: resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} engines: {node: '>=10'} @@ -4987,6 +5019,10 @@ packages: resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} engines: {node: '>=10'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -6032,6 +6068,15 @@ snapshots: '@japa/api-client': 3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0) '@japa/plugin-adonisjs': 5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0) + '@adonisjs/static@2.0.1(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))': + dependencies: + '@adonisjs/core': 7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1) + serve-static: 2.2.1 + optionalDependencies: + '@adonisjs/assembler': 8.4.0(typescript@6.0.3) + transitivePeerDependencies: + - supports-color + '@adonisjs/tsconfig@2.0.0': {} '@antfu/install-pkg@1.1.0': @@ -9061,6 +9106,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -9397,6 +9444,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fs-constants@1.0.0: {} fsevents@2.3.3: @@ -10132,6 +10181,8 @@ snapshots: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -10327,6 +10378,8 @@ snapshots: random-bytes@1.0.0: {} + range-parser@1.2.1: {} + rate-limiter-flexible@9.1.1: {} raw-body@3.0.2: @@ -10542,12 +10595,37 @@ snapshots: semver@7.7.4: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + seroval-plugins@1.5.4(seroval@1.5.4): dependencies: seroval: 1.5.4 seroval@1.5.4: {} + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-cookie-parser@2.7.2: {} set-cookie-parser@3.1.0: {}