diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..2ac56d80e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.github +.husky +node_modules +dist +coverage +playwright-report +test-results +.vscode +.idea +*.log +.env +.env.* +!.env.example +Dockerfile* +docker-compose*.yml +README.md +docs/auditorias diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f174a0581..a26358a5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,12 +16,32 @@ jobs: - name: Install dependencies run: bun install + - name: Typecheck + run: bun run typecheck + + - name: Strict core typecheck + run: bun run typecheck:strict:core + - name: Lint run: bun run lint - + + - name: Domain boundary checks + run: | + bun run check:domain + bun run check:barrels + + - name: API contract validation + run: bun run api:validate + + - name: VPS readiness checks + run: bun run vps:check + - name: Build run: bun run build - + + - name: Performance budget + run: bun run perf:budget + - name: Vitest (Unit & Fuzz) run: bunx vitest run --coverage diff --git a/.github/workflows/deploy-vps.yml b/.github/workflows/deploy-vps.yml new file mode 100644 index 000000000..19f754261 --- /dev/null +++ b/.github/workflows/deploy-vps.yml @@ -0,0 +1,102 @@ +name: Deploy VPS + +on: + workflow_dispatch: + inputs: + environment: + description: "Target environment" + required: true + default: "staging" + type: choice + options: + - staging + - production + image_tag: + description: "Optional image tag override" + required: false + type: string + +permissions: + contents: read + packages: write + +concurrency: + group: deploy-vps-${{ inputs.environment }} + cancel-in-progress: false + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/zapp-web + +jobs: + build-and-push: + name: Build and push image + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + outputs: + image: ${{ steps.meta.outputs.image }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute image metadata + id: meta + run: | + TAG="${{ inputs.image_tag }}" + if [ -z "$TAG" ]; then TAG="${{ inputs.environment }}-${GITHUB_SHA::12}"; fi + IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}" + echo "image=$IMAGE" >> "$GITHUB_OUTPUT" + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.image }} + build-args: | + VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }} + VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }} + VITE_EXTERNAL_SUPABASE_URL=${{ secrets.VITE_EXTERNAL_SUPABASE_URL }} + VITE_EXTERNAL_SUPABASE_ANON_KEY=${{ secrets.VITE_EXTERNAL_SUPABASE_ANON_KEY }} + VITE_SENTRY_DSN=${{ secrets.VITE_SENTRY_DSN }} + VITE_SENTRY_ENVIRONMENT=${{ inputs.environment }} + VITE_APP_ENV=${{ inputs.environment }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + name: Restart on VPS + runs-on: ubuntu-latest + needs: build-and-push + environment: ${{ inputs.environment }} + if: ${{ vars.VPS_DEPLOY_ENABLED == 'true' }} + steps: + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.2.0 + env: + ZAPP_WEB_IMAGE: ${{ needs.build-and-push.outputs.image }} + ZAPP_WEB_HOST: ${{ vars.ZAPP_WEB_HOST }} + ZAPP_WEB_DEPLOY_DIR: ${{ vars.ZAPP_WEB_DEPLOY_DIR }} + with: + host: ${{ secrets.VPS_HOST }} + username: ${{ secrets.VPS_USER }} + key: ${{ secrets.VPS_SSH_KEY }} + envs: ZAPP_WEB_IMAGE,ZAPP_WEB_HOST,ZAPP_WEB_DEPLOY_DIR + script: | + set -euo pipefail + cd "/opt/zapp-web" + export ZAPP_WEB_IMAGE="$ZAPP_WEB_IMAGE" + export ZAPP_WEB_HOST="${ZAPP_WEB_HOST:-zapp.atomicabr.com.br}" + docker compose pull zapp-web || true + docker compose up -d zapp-web + docker image prune -f diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 8adb200a8..c20268590 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -35,25 +35,33 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITLEAKS_ENABLE_SUMMARY: "false" - rls-audit: - name: RLS & Compliance Weekly Report + dependency-review: + name: Dependency Review runs-on: ubuntu-latest - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + if: github.event_name == 'pull_request' steps: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Dependency Review + uses: actions/dependency-review-action@v4 with: - node-version: '20' + fail-on-severity: high + + rls-audit: + name: RLS & Compliance Report + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v1 - name: Install dependencies - run: npm install --no-audit --no-fund + run: bun install --frozen-lockfile - name: Generate RLS Report - run: bun scripts/verify_rls_compliance.ts > rls-compliance-report.md - - name: Publish Weekly Compliance + run: bun run security:rls | tee rls-compliance-report.md + - name: Publish RLS Compliance uses: actions/upload-artifact@v4 + if: always() with: - name: weekly-security-compliance-report + name: rls-compliance-report path: rls-compliance-report.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 241eb6109..71dc3ad64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # 📜 Changelog — ZAPP WEB +## [Unreleased] - 2026-05-13 +### Adicionado +- Gate estático de compliance RLS (`scripts/verify_rls_compliance.ts`) integrado ao workflow de segurança. +- Budget de performance pós-build (`scripts/check-performance-budget.mjs`) integrado ao CI. +- Baseline de autenticação privilegiada e SLOs operacionais iniciais. +- Redaction central de PII/secrets para o logger do frontend. +- Contrato OpenAPI mínimo para Edge Functions críticas com validação automatizada. +- Idempotência por `x-idempotency-key` no endpoint `public-api` para evitar envios duplicados em retries. + +### Alterado +- Pipeline de CI agora executa typecheck, typecheck strict do core, boundaries de domínio, validação de barrels, contrato OpenAPI e performance budget. +- Workflow de segurança agora publica relatório RLS em PR/push/schedule e adiciona dependency review em PRs. + +### Corrigido +- Dependência `lint-staged` ausente no Husky pre-commit. +- Gaps de RLS/policies em tabelas operacionais detectadas pela auditoria estática. + ## [2.0.1] - 2026-05-06 ### Adicionado - Schemas de validação **Zod** para contatos e boundaries. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..dbf97757d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# syntax=docker/dockerfile:1.7 + +FROM oven/bun:1.2-alpine AS deps +WORKDIR /app +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile + +FROM deps AS builder +WORKDIR /app +COPY . . + +ARG VITE_SUPABASE_URL +ARG VITE_SUPABASE_ANON_KEY +ARG VITE_EXTERNAL_SUPABASE_URL +ARG VITE_EXTERNAL_SUPABASE_ANON_KEY +ARG VITE_SENTRY_DSN +ARG VITE_SENTRY_ENVIRONMENT=production +ARG VITE_APP_ENV=production + +ENV VITE_SUPABASE_URL=${VITE_SUPABASE_URL} +ENV VITE_SUPABASE_ANON_KEY=${VITE_SUPABASE_ANON_KEY} +ENV VITE_EXTERNAL_SUPABASE_URL=${VITE_EXTERNAL_SUPABASE_URL} +ENV VITE_EXTERNAL_SUPABASE_ANON_KEY=${VITE_EXTERNAL_SUPABASE_ANON_KEY} +ENV VITE_SENTRY_DSN=${VITE_SENTRY_DSN} +ENV VITE_SENTRY_ENVIRONMENT=${VITE_SENTRY_ENVIRONMENT} +ENV VITE_APP_ENV=${VITE_APP_ENV} + +RUN bun run build + +FROM nginx:1.27-alpine AS runtime +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /usr/share/nginx/html +EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD wget -q -O - http://127.0.0.1/healthz >/dev/null || exit 1 +CMD ["nginx", "-g", "daemon off;"] diff --git a/bun.lock b/bun.lock index e28e5e5ee..86155447c 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "vite_react_shadcn_ts", @@ -93,6 +92,7 @@ "globals": "^15.15.0", "happy-dom": "^20.9.0", "husky": "^9.1.7", + "lint-staged": "^17.0.4", "playwright": "^1.59.1", "postcss": "^8.5.6", "prettier": "^3.8.3", @@ -630,6 +630,8 @@ "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -694,6 +696,10 @@ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cmd-shim": ["cmd-shim@7.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/cmd-shim/-/cmd-shim-7.0.0.tgz", {}, "sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw=="], @@ -782,12 +788,14 @@ "electron-to-chromium": ["electron-to-chromium@1.5.330", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/electron-to-chromium/-/electron-to-chromium-1.5.330.tgz", {}, "sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA=="], - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "enhanced-resolve": ["enhanced-resolve@5.21.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], "entities": ["entities@7.0.1", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/entities/-/entities-7.0.1.tgz", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], "es-toolkit": ["es-toolkit@1.46.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/es-toolkit/-/es-toolkit-1.46.1.tgz", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="], @@ -878,6 +886,8 @@ "geojson-vt": ["geojson-vt@4.0.2", "", {}, "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "gl-matrix": ["gl-matrix@3.4.4", "", {}, "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="], @@ -934,7 +944,7 @@ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], @@ -982,6 +992,10 @@ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "lint-staged": ["lint-staged@17.0.4", "", { "dependencies": { "listr2": "^10.2.1", "picomatch": "^4.0.4", "string-argv": "^0.3.2", "tinyexec": "^1.1.2" }, "optionalDependencies": { "yaml": "^2.8.4" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-+rU9lSUyVOZ/hDUmRLVGzyS2v73cDdQjX+XQz1AaOdIE4RysLq0HoPW2HrrgeNCLklkhi904VBU1bmgWLHVnkA=="], + + "listr2": ["listr2@10.2.1", "", { "dependencies": { "cli-truncate": "^5.2.0", "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^10.0.0" } }, "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q=="], + "livekit-client": ["livekit-client@2.16.1", "", { "dependencies": { "@livekit/mutex": "1.1.1", "@livekit/protocol": "1.42.2", "events": "^3.3.0", "jose": "^6.1.0", "loglevel": "^1.9.2", "sdp-transform": "^2.15.0", "ts-debounce": "^4.0.0", "tslib": "2.8.1", "typed-emitter": "^2.1.0", "webrtc-adapter": "^9.0.1" } }, "sha512-PmOHiGdkzw2P/t0vLbJRwHU59+T+JcFlGTYDV7PObWmzvypIij1Vlj0WhZKDZ/Ac7CvwWinbiGCVw/Pb/OiYYw=="], "local-pkg": ["local-pkg@1.1.2", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/local-pkg/-/local-pkg-1.1.2.tgz", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], @@ -990,6 +1004,8 @@ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + "loglevel": ["loglevel@1.9.2", "", {}, "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -1016,6 +1032,8 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -1064,6 +1082,8 @@ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -1092,7 +1112,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], @@ -1208,8 +1228,12 @@ "resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="], "robust-predicates": ["robust-predicates@2.0.4", "", {}, "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg=="], @@ -1240,6 +1264,8 @@ "sirv": ["sirv@3.0.2", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/sirv/-/sirv-3.0.2.tgz", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + "sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="], "source-map": ["source-map@0.6.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -1258,11 +1284,13 @@ "std-env": ["std-env@4.1.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/std-env/-/std-env-4.1.0.tgz", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], - "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1308,7 +1336,7 @@ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], @@ -1382,7 +1410,7 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -1394,7 +1422,7 @@ "yallist": ["yallist@5.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/yallist/-/yallist-5.0.0.tgz", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "yaml": ["yaml@2.6.0", "", { "bin": "bin.mjs" }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -1406,6 +1434,12 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -1470,6 +1504,10 @@ "happy-dom/@types/node": ["@types/node@22.16.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ=="], + "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "lru-cache/yallist": ["yallist@3.1.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "make-dir/semver": ["semver@7.7.4", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -1486,27 +1524,39 @@ "path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "postcss-load-config/yaml": ["yaml@2.6.0", "", { "bin": "bin.mjs" }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="], + "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "tailwind-api-utils/jiti": ["jiti@2.7.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/jiti/-/jiti-2.7.0.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "terser/commander": ["commander@2.20.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/commander/-/commander-2.20.3.tgz", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "vitest/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "vitest/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "tsx"], "bin": "bin/vite.js" }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -1514,6 +1564,12 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "@types/ws/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -1522,6 +1578,12 @@ "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "mlly/pkg-types/confbox": ["confbox@0.1.8", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/confbox/-/confbox-0.1.8.tgz", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "vitest/vite/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": "bin/esbuild" }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], @@ -1530,6 +1592,8 @@ "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..bfc4091e7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + zapp-web: + build: + context: . + args: + VITE_SUPABASE_URL: ${VITE_SUPABASE_URL:?required} + VITE_SUPABASE_ANON_KEY: ${VITE_SUPABASE_ANON_KEY:?required} + VITE_EXTERNAL_SUPABASE_URL: ${VITE_EXTERNAL_SUPABASE_URL:-${VITE_SUPABASE_URL}} + VITE_EXTERNAL_SUPABASE_ANON_KEY: ${VITE_EXTERNAL_SUPABASE_ANON_KEY:-${VITE_SUPABASE_ANON_KEY}} + VITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-} + VITE_SENTRY_ENVIRONMENT: ${VITE_SENTRY_ENVIRONMENT:-production} + VITE_APP_ENV: ${VITE_APP_ENV:-production} + image: ${ZAPP_WEB_IMAGE:-zapp-web:local} + container_name: zapp-web + restart: unless-stopped + networks: + - atomicabr + labels: + - traefik.enable=true + - traefik.http.routers.zapp-web.rule=Host(`${ZAPP_WEB_HOST:-zapp.atomicabr.com.br}`) + - traefik.http.routers.zapp-web.entrypoints=websecure + - traefik.http.routers.zapp-web.tls=true + - traefik.http.routers.zapp-web.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt} + - traefik.http.services.zapp-web.loadbalancer.server.port=80 + healthcheck: + test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1/healthz"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 10s + +networks: + atomicabr: + external: true diff --git a/docs/api/openapi.json b/docs/api/openapi.json new file mode 100644 index 000000000..e7b9017d1 --- /dev/null +++ b/docs/api/openapi.json @@ -0,0 +1,508 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "ZAPP-WEB Edge Functions API", + "version": "2.0.1", + "description": "Contrato operacional mínimo das Edge Functions expostas/integradas. Endpoints internos continuam protegidos por Supabase Auth, RLS, HMAC e service-role conforme o caso." + }, + "servers": [ + { + "url": "https://{projectRef}.supabase.co/functions/v1", + "variables": { + "projectRef": { + "default": "allrjhkpuscmgbsnmjlv" + } + } + } + ], + "tags": [ + { + "name": "Health" + }, + { + "name": "Webhooks" + }, + { + "name": "Public API" + }, + { + "name": "Observability" + }, + { + "name": "Security" + } + ], + "paths": { + "/health-check": { + "get": { + "tags": [ + "Health" + ], + "operationId": "getHealthCheck", + "summary": "Retorna status básico da plataforma Edge Functions.", + "responses": { + "200": { + "description": "Serviço saudável.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + } + } + } + } + }, + "/proxy-health": { + "get": { + "tags": [ + "Health", + "Observability" + ], + "operationId": "getProxyHealth", + "summary": "Retorna saúde de providers/proxies monitorados.", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Snapshot de saúde dos providers.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, + "/proxy-metrics": { + "get": { + "tags": [ + "Observability" + ], + "operationId": "getProxyMetrics", + "summary": "Consulta métricas RED/operacionais de providers e proxy.", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Métricas agregadas.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetricsResponse" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/public-api": { + "post": { + "tags": [ + "Public API" + ], + "operationId": "postPublicApiCommand", + "summary": "Executa comando público autenticado por API key escopada.", + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicApiCommand" + } + } + } + }, + "responses": { + "200": { + "description": "Comando aceito/processado.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/IdempotencyKeyHeader" + } + ] + } + }, + "/evolution-webhook": { + "post": { + "tags": [ + "Webhooks" + ], + "operationId": "postEvolutionWebhook", + "summary": "Recebe eventos da Evolution API com validação HMAC/idempotência.", + "security": [ + { + "hmacSignature": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/RequestIdHeader" + }, + { + "$ref": "#/components/parameters/IdempotencyKeyHeader" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/WebhookAccepted" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, + "/whatsapp-cloud-webhook": { + "post": { + "tags": [ + "Webhooks" + ], + "operationId": "postWhatsAppCloudWebhook", + "summary": "Recebe eventos do WhatsApp Cloud API com verificação de assinatura.", + "security": [ + { + "hmacSignature": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/RequestIdHeader" + }, + { + "$ref": "#/components/parameters/IdempotencyKeyHeader" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/WebhookAccepted" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, + "/webhook-secret-status": { + "get": { + "tags": [ + "Security" + ], + "operationId": "getWebhookSecretStatus", + "summary": "Audita presença/rotação de secrets de webhook sem expor valores.", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Status mascarado dos secrets.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretStatusResponse" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "x-api-key" + }, + "hmacSignature": { + "type": "apiKey", + "in": "header", + "name": "x-signature" + } + }, + "parameters": { + "RequestIdHeader": { + "name": "x-request-id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "maxLength": 64 + }, + "description": "Correlation id propagado pelo chamador. Se ausente, a Edge Function deve gerar um novo id." + }, + "IdempotencyKeyHeader": { + "name": "x-idempotency-key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "minLength": 8, + "maxLength": 200 + }, + "description": "Chave de idempotência para retries seguros quando aplicável." + } + }, + "responses": { + "BadRequest": { + "description": "Payload inválido.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "Unauthorized": { + "description": "Credencial ausente ou inválida.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "Forbidden": { + "description": "Usuário autenticado sem permissão para a operação.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "RateLimited": { + "description": "Rate limit excedido.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "WebhookAccepted": { + "description": "Evento aceito, ignorado por idempotência ou rejeitado de forma controlada.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookResponse" + } + } + } + } + }, + "schemas": { + "HealthResponse": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + }, + "status": { + "type": "string" + }, + "requestId": { + "type": "string" + } + }, + "additionalProperties": true + }, + "MetricsResponse": { + "type": "object", + "properties": { + "requestId": { + "type": "string" + }, + "metrics": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "PublicApiCommand": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string", + "minLength": 1 + }, + "payload": { + "type": "object", + "additionalProperties": true + }, + "requestId": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CommandResponse": { + "type": "object", + "required": [ + "success" + ], + "properties": { + "success": { + "type": "boolean" + }, + "requestId": { + "type": "string" + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "WebhookResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "ok": { + "type": "boolean" + }, + "duplicate": { + "type": "boolean" + }, + "requestId": { + "type": "string" + } + }, + "additionalProperties": true + }, + "SecretStatusResponse": { + "type": "object", + "properties": { + "requestId": { + "type": "string" + }, + "secrets": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + }, + "additionalProperties": true + }, + "ErrorResponse": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "requestId": { + "type": "string" + } + }, + "additionalProperties": true + } + } + } +} diff --git a/docs/auditorias/2026-05-13-auditoria-tecnica-exaustiva.md b/docs/auditorias/2026-05-13-auditoria-tecnica-exaustiva.md new file mode 100644 index 000000000..61e483aed --- /dev/null +++ b/docs/auditorias/2026-05-13-auditoria-tecnica-exaustiva.md @@ -0,0 +1,615 @@ +# Auditoria Técnica Exaustiva — ZAPP-WEB / Pronto Talk Suite + +**Data:** 2026-05-13 +**Escopo auditado:** repositório local `/workspace/zapp-web`, branch `work` +**Prompt base:** Auditoria Técnica Exaustiva de Sistema — Rumo ao 10/10 v2.0 FINAL +**Observação metodológica:** o prompt solicita “22 dimensões”, mas a lista detalhada e o scorecard fornecem **20 dimensões**. Esta auditoria cobre integralmente as 20 dimensões explicitamente especificadas; nenhuma dimensão extra foi inferida para evitar achismo. + +## 1. Inventário do Sistema + +| Item | Evidência auditada | +|---|---| +| Repositório / branch | Git local em `/workspace/zapp-web`, branch `work`. | +| Stack tecnológico | React 18, TypeScript 5, Vite 5, TailwindCSS, TanStack Query, Supabase Auth/DB/Storage/Edge Functions/Realtime, Evolution API, Bitrix24, ElevenLabs, Mapbox, Resend e SIP.js, conforme `README.md` e `package.json`. | +| Frontend | SPA React/Vite em `src/`, com `AppProviders`, `AppRoutes`, módulos em `features/`, `components/`, `hooks/`, `services/`, `lib/` e `integrations/`. | +| Backend | Supabase Edge Functions em `supabase/functions/`; foram localizadas 100+ functions com `index.ts`, incluindo Evolution, Bitrix, Gmail, WhatsApp Cloud, IA, observabilidade e health checks. | +| Banco de dados | SQL versionado em `supabase/migrations/` e `supabase/migrations-from-lovable/`; contagem local: 460 arquivos SQL e 460 ocorrências de `CREATE TABLE`. | +| Integrações externas | Supabase, Evolution API, Bitrix24, Gmail/Outlook, WhatsApp Cloud, ElevenLabs, Mapbox, Resend, Sicoob, VirusTotal e n8n via UI/documentação; não foram encontrados exports de workflows n8n no repositório. | +| Ambientes | `.env.example`, documentação de deploy/preview/produção Lovable e Supabase local config; isolamento real dev/staging/prod não é totalmente auditável sem acesso aos painéis. | +| Arquivos / LOC aproximados | `rg --files` sem `node_modules`, `dist`, `build`: 2.362 arquivos. `wc -l` em arquivos texto relevantes: ~332.333 linhas. | +| Endpoints/rotas | Frontend: rotas declaradas em `src/components/routing/AppRoutes.tsx`. Backend: Edge Functions por diretório em `supabase/functions/*/index.ts`. | +| Workflows de automação | GitHub Actions: CI, CodeQL, Security, branch protection. n8n: **não auditável** — não há export JSON/YAML de workflows, apenas tela de integração. | +| Último deploy em produção | **Não auditável** pelo código local. Seria necessário acesso a Lovable/Supabase/GitHub deployments. | + +## 2. Análise das Dimensões + +### 1. Arquitetura — **7/10** + +**Justificativa:** há documentação arquitetural, ADRs e separação parcial por `features/`, mas ainda existe mistura forte entre componentes globais, hooks, serviços e lógica de domínio fora de boundaries estritos. + +**Evidências** +- README documenta arquitetura macro, stack e integrações. +- Existem ADRs em `docs/decisions/`, incluindo React Query, RLS, lazy loading, two-backend boundary e error tracking. +- `tsconfig.app.json` define aliases por feature (`@/admin`, `@/auth`, `@/connections`, `@/inbox`, `@/sla`). +- Estrutura ainda tem muitos hooks e componentes no nível global (`src/hooks`, `src/components`) além de `src/features`, indicando coexistência de arquitetura feature-based e legacy/global. +- Há script de boundary (`scripts/check-domain-boundaries.ts`) e comando `check:domain`, mas ele não está no `check` principal do `package.json`. + +**Gaps para 10/10** +- Boundary feature-based ainda não é aplicado a 100% do frontend. +- Falta enforcement de dependências circulares no CI. +- Falta regra mandatória de importação por camada/domínio no pipeline principal. +- Falta dicionário claro de domínios e owners por módulo. + +**Ações corretivas** +- Incluir `bun run check:domain` no `npm run check`/CI. +- Migrar hooks/componentes globais para features quando pertencerem a domínio específico. +- Adotar ferramenta como `madge`/`dependency-cruiser` para dependências circulares. +- Criar `docs/architecture/domain-map.md` com owners, APIs internas e dependências permitidas. + +### 2. Autenticação — **6/10** + +**Justificativa:** Supabase Auth está integrado com sessão, refresh automático e logout, mas tokens ficam em `localStorage`, não há evidência local de MFA obrigatório, rate limiting/brute force no fluxo de login e lockout testado. + +**Evidências** +- Cliente Supabase usa `persistSession: true` e `autoRefreshToken: true`. +- AuthProvider escuta `onAuthStateChange`, trata `TOKEN_REFRESHED`, carrega perfil e expõe `signIn`, `signUp`, `signOut`. +- AuthService usa `signInWithPassword`, `signUp`, `signOut` e consulta `profiles`. +- O storage do Supabase é `window.localStorage`, aumentando impacto de XSS em comparação com cookies HttpOnly. +- Há Edge Function `webauthn`, mas não foi evidenciado que MFA/WebAuthn é obrigatório no fluxo principal. + +**Gaps para 10/10** +- MFA/WebAuthn não obrigatório nem coberto por política de acesso. +- Tokens não protegidos por HttpOnly/SameSite/Secure cookies. +- Rate limiting e lockout do login não foram verificados como enforcement central. +- Password policy e recuperação de senha dependem de configuração Supabase externa não auditável pelo repo. +- Testes automatizados do refresh concorrente não evidenciados. + +**Ações corretivas** +- Documentar e versionar configuração Supabase Auth esperada em `docs/security/auth-baseline.md`. +- Exigir MFA para admin/supervisor e adicionar E2E de login com step-up. +- Avaliar BFF/cookie HttpOnly para sessões de alto privilégio. +- Criar Edge Function/RPC para registrar tentativas e bloquear login quando aplicável, com testes. + +### 3. Autorização — **7/10** + +**Justificativa:** há RBAC no frontend, RLS e policies em volume expressivo, mas coverage real por tabela e testes negativos por role não são comprovados automaticamente em CI para todas as tabelas. + +**Evidências** +- Rotas protegidas usam `ProtectedRoute` e `requiredRoles` em páginas admin/dev/supervisor. +- Foram encontradas 341 ocorrências de `enable row level security` e 1.224 ocorrências de `create policy` nas migrations. +- Security workflow agenda relatório de RLS com `scripts/verify_rls_compliance.ts` apenas em schedule/workflow_dispatch. +- Existem E2E de escopo/autorização, por exemplo `e2e/inbox-scope.spec.ts`, `e2e/teams-security-integration.spec.ts` e `e2e/audit-enterprise.spec.ts`. +- Algumas Edge Functions validam admin; outras usam service role e exigem revisão individual. + +**Gaps para 10/10** +- CI de PR não executa auditoria RLS completa. +- Cobertura RLS por tabela não está anexada como artefato obrigatório em todo PR. +- Autorização field-level e proteção contra alteração do próprio role não foram comprovadas para 100% do schema. +- Required roles no frontend não substituem autorização server-side; precisa matriz endpoint × role. + +**Ações corretivas** +- Rodar `bun scripts/verify_rls_compliance.ts` em PRs com artefato e falha em regressões críticas. +- Criar testes SQL para policies por role (positivo/negativo) com fixtures. +- Criar matriz `docs/security/rbac-matrix.md` e mapear Edge Functions/RPCs. +- Revisar functions service-role e exigir `assertAdmin`/`assertRole` compartilhado quando mutarem dados sensíveis. + +### 4. Banco de Dados — **7/10** + +**Justificativa:** há alto volume de migrations, RLS, constraints e documentação de arquitetura, mas o repositório mistura migrations principais e `migrations-from-lovable`, não há evidência local de restore testado, EXPLAIN das top queries ou reversibilidade sistemática. + +**Evidências** +- 460 arquivos SQL locais e 460 ocorrências de `CREATE TABLE`. +- Documentos como `docs/DATABASE_ARCHITECTURE.md`, `docs/ER_DIAGRAM.md` e `docs/BACKUP-RECOVERY-STRATEGY.md` existem. +- Há muitas policies e triggers; isso aumenta capacidade de integridade mas também risco de chains complexas. +- `supabase/config.toml` existe, mas backup/pooler/restore real são configurações de plataforma não totalmente auditáveis pelo repo. + +**Gaps para 10/10** +- Falta pipeline que aplique migrations do zero em banco efêmero e execute smoke SQL. +- Falta catálogo de índices/top queries com `EXPLAIN ANALYZE` versionado. +- Falta política consistente de down migrations/rollback de schema. +- Falta inventário automático de tabelas mortas/colunas não usadas. + +**Ações corretivas** +- Adicionar job CI `supabase db reset` ou Postgres efêmero com migrations completas. +- Criar `docs/db/top-queries.md` com query, índice esperado e plano. +- Criar script `scripts/audit-db-objects.ts` para listar tabelas sem acesso, sem policies ou sem uso. +- Separar migrations importadas/legacy de migrations oficiais aplicáveis. + +### 5. CI/CD — **7/10** + +**Justificativa:** CI executa lint, build, Vitest com coverage e Deno tests; há CodeQL, gitleaks e Dependabot. Porém E2E está desativado no CI, deploy/preview/rollback não são automatizados pelo pipeline do repo e há TODO explícito no workflow. + +**Evidências** +- `.github/workflows/ci.yml` roda install, lint, build, Vitest com coverage e Deno tests. +- Playwright E2E está comentado com TODO e dependências de secrets/seed. +- `.github/workflows/security.yml` roda Gitleaks em PR/push e relatório RLS semanal/manual. +- `.github/dependabot.yml`, `.github/CODEOWNERS` e PR template existem. +- Documentação de branch protection existe, mas configuração real do GitHub não é auditável localmente. + +**Gaps para 10/10** +- E2E crítico fora do PR pipeline. +- Sem preview automático validado por PR no workflow local. +- Sem rollback automatizado ou teste de rollback no CI. +- Sem `npm audit`/SCA completo no CI de PR além de gitleaks/CodeQL. + +**Ações corretivas** +- Reativar Playwright em CI com Supabase de teste e fixture auth. +- Adicionar `bun audit`/`npm audit --audit-level=high` ou Snyk/OSV. +- Adicionar workflow de preview/staging e checklist de rollback. +- Fazer branch protection exigir CI + security + E2E smoke. + +### 6. Data Integrity — **7/10** + +**Justificativa:** há migrations, RLS, idempotência em helpers e DLQ/retry, mas não há prova de transações e idempotência para todas as mutações críticas nem de optimistic locking generalizado. + +**Evidências** +- Existem helpers de idempotência e DLQ em `supabase/functions/_shared/`. +- Há E2E específico de `dlq-idempotency`. +- Banco contém constraints/policies em volume relevante. +- Algumas operações Edge Functions usam service role e precisam de validação/transação revisada função a função. + +**Gaps para 10/10** +- Sem padrão obrigatório de idempotency key em todos os webhooks/mutations externas. +- Transações multi-tabela não são auditadas automaticamente. +- Optimistic locking/version column não evidenciado como padrão. +- Política soft delete vs hard delete não está consolidada por entidade. + +**Ações corretivas** +- Criar helper obrigatório `withIdempotency` para webhooks e commands. +- Adicionar constraints unique para chaves externas de dedupe onde faltarem. +- Criar `docs/data-integrity/entity-lifecycle.md` para delete/versioning. +- Testar idempotência e concorrência em endpoints críticos com Vitest/SQL fixtures. + +### 7. Documentação — **8/10** + +**Justificativa:** documentação é um ponto forte: README completo, ADRs, runbooks, ER diagram, deploy, onboarding, segurança e integrações. Ainda faltam OpenAPI/contratos formais e evidências operacionais atualizadas como deploy real e changelog disciplinado por release. + +**Evidências** +- `README.md` cobre stack, setup, arquitetura, integrações, segurança, testes, deploy e contribuição. +- Existem ADRs em `docs/decisions/`. +- Existem runbooks e docs operacionais: deploy, DR, incidentes, backup, key rotation, troubleshooting, onboarding. +- Há documentação de webhook security, Evolution API, integrações e arquitetura. + +**Gaps para 10/10** +- Falta OpenAPI/Swagger ou spec equivalente para Edge Functions públicas/internas. +- Falta dicionário de dados completo ligando negócio ↔ tabelas ↔ eventos. +- Falta validação automatizada de freshness da documentação. +- Falta registro de último deploy/versão em produção gerado automaticamente. + +**Ações corretivas** +- Criar `openapi.yaml` para public API e Edge Functions expostas. +- Criar `docs/data-dictionary.md` com owner, PII, retenção e RLS por entidade. +- Adicionar CI que valida links de docs e docs obrigatórias por feature. +- Gerar release notes/changelog em workflow. + +### 8. Infraestrutura / DevOps — **5/10** + +**Justificativa:** há documentação e configs de deploy, mas pouco IaC real. Muitos aspectos de SSL, DNS, backups, network segmentation e sizing dependem de painéis externos não auditáveis pelo repo. + +**Evidências** +- `supabase/config.toml`, deploy docs e templates de nginx/security headers em `supabase/migrations-from-lovable/_deploy/`. +- README aponta deploy público e Lovable. +- Há `.env.example`, docs de env e key rotation. +- Não foram encontrados Terraform/Pulumi/Helm/Kubernetes ou IaC equivalente. + +**Gaps para 10/10** +- Infra não declarada como código de ponta a ponta. +- Isolamento dev/staging/prod não comprovado por config versionada. +- Health checks existem como functions, mas não há evidência de monitor externo configurado. +- DR/backup documentados, porém restore testado não comprovado. + +**Ações corretivas** +- Introduzir IaC mínimo: Supabase projects/config, DNS/CDN, alerting e secrets references. +- Criar checklist de ambiente com variáveis, URLs, owners e matriz de acesso. +- Automatizar teste periódico de restore e registrar resultado em `docs/ops/restore-tests/`. +- Documentar Cloudflare/CDN/security headers ativos em produção. + +### 9. Logging / Monitoring — **7/10** + +**Justificativa:** há logger central com correlação e Sentry, health/proxy metrics e docs de dashboards, porém frontend logger usa console formatado em texto, há muitos `console.*` espalhados e não há garantia de mascaramento de PII em todos os logs. + +**Evidências** +- `src/lib/logger.ts` centraliza logs com session id, correlation id e Sentry breadcrumbs/capture em produção. +- Contagem local: 259 ocorrências de `console.*` em `src` e `supabase/functions`. +- Edge Functions possuem helper de validation/logger estruturado, mas nem todas parecem usá-lo. +- Existem docs e dashboard Grafana para proxy metrics. + +**Gaps para 10/10** +- Logs não são JSON estruturado de forma uniforme em frontend e Edge Functions. +- Correlation ID não é propagado obrigatoriamente entre frontend → Edge Function → integrações. +- Sem scanner CI para impedir PII/tokens em logs. +- Retenção/alertas reais não auditáveis localmente. + +**Ações corretivas** +- Padronizar logger compartilhado para Edge Functions e banir `console.*` direto via ESLint. +- Implementar `x-request-id`/correlation propagation em clients e functions. +- Adicionar redaction helper obrigatório para email, telefone, tokens, JID e CPF/CNPJ. +- Criar alertas documentados para error rate, latency, DLQ e webhooks rejeitados. + +### 10. Observabilidade — **6/10** + +**Justificativa:** existem Sentry, client observability, proxy metrics, health checks e dashboards, mas tracing distribuído, RED/USE formal e SLOs linkados a runbooks ainda são parciais. + +**Evidências** +- Sentry é dependência de runtime e usado no logger. +- Existem Edge Functions `client-observability`, `proxy-metrics`, `proxy-health`, `provider-healthcheck`, `connection-health-check`. +- Existem docs de Grafana proxy metrics e runbooks. +- Não foi encontrada instrumentação OpenTelemetry/distributed tracing cross-service. + +**Gaps para 10/10** +- Falta tracing distribuído n8n/Evolution/Supabase/Bitrix. +- Falta definição formal de SLI/SLO por serviço crítico. +- Falta RED por endpoint e USE por recurso de infra como padrão. +- Falta feature flags/remoto config para debug seguro em produção. + +**Ações corretivas** +- Adicionar OpenTelemetry ou padrão de trace id nos Edge Functions e integrações. +- Criar `docs/observability/slos.md` com SLIs/SLOs, alertas e runbooks. +- Instrumentar métricas RED para functions críticas. +- Adicionar painel de DLQ, webhook rejection rate, queue lag e provider latency. + +### 11. Lógica de Negócio — **6/10** + +**Justificativa:** há services/hooks e algumas separações por feature, mas parte relevante da lógica parece residir em hooks/componentes e Edge Functions, com baixa garantia de regras críticas testadas isoladamente. + +**Evidências** +- `features/*/services`, `features/*/data-access` e hooks indicam tentativa de isolamento. +- Muitas regras aparecem em hooks globais (`useQueues`, `useAutomations`, `useBusinessHours`, `useConversation*`, etc.). +- Testes existem, mas cobertura e foco em regras críticas não foram comprovados por relatório atualizado no repo. +- Cálculos financeiros e estados de ciclo de vida não têm state machines formais evidentes. + +**Gaps para 10/10** +- Falta camada domain explícita para regras críticas. +- Falta suite unitária focada em pricing/SLA/routing/queue/business hours. +- Falta state machine formal para conversa, mensagem, fila, conexão e ticket. +- Falta single source of truth backend para regras replicadas no frontend. + +**Ações corretivas** +- Criar `src/domain/` ou `src/features/*/domain/` com funções puras e testes. +- Modelar transições de status com unions/Zod e validators server-side. +- Criar tests parametrizados para edge cases de SLA, datas/timezones e filas. +- Remover regras críticas de componentes, deixando UI apenas orquestrar. + +### 12. Manutenibilidade — **6/10** + +**Justificativa:** há scripts de design system, barrel validation e lint, mas o tamanho do codebase, coexistência de padrões, 906 ocorrências de `any`, 21 TODO/FIXME/HACK e muitos arquivos globais reduzem previsibilidade. + +**Evidências** +- `package.json` inclui lint, typecheck, build, check, design system check e barrel validation. +- TypeScript strict está desabilitado e `noUnusedLocals`/`noUnusedParameters` também. +- Contagem local: 906 ocorrências de `any` e 21 ocorrências TODO/FIXME/HACK em código/docs relevantes. +- Há muitos componentes e hooks globais, aumentando risco de acoplamento implícito. + +**Gaps para 10/10** +- Reduzir `any` e ativar strict gradualmente. +- Medir complexidade ciclomática e tamanho de arquivos no CI. +- Registrar débito técnico em backlog/labels rastreáveis. +- Consolidar padrões de módulo, nomenclatura e imports. + +**Ações corretivas** +- Criar plano `strictness` por diretório com metas semanais. +- Adicionar ESLint rules para `no-explicit-any`, unused imports/vars e max complexity por etapas. +- Criar relatório de maiores arquivos/funções e quebrar God components. +- Adicionar `docs/engineering/code-review.md` com critérios objetivos. + +### 13. Operacionalidade — **6/10** + +**Justificativa:** há runbooks e procedimento de incidentes, mas rollback, zero-downtime, feature flags e circuit breakers padronizados ainda não são comprovados. + +**Evidências** +- `docs/runbooks/deploy.md` descreve deploy Lovable, pré-deploy, monitoramento e incident response. +- Existem `docs/INCIDENT_RUNBOOK.md`, `docs/DR_RUNBOOK.md`, `docs/DISTRIBUTION-FALLBACKS.md` e docs de fallback. +- Edge Functions e helpers de fallback/retry existem para mensagens e providers. +- Deploy real depende de Lovable UI, não de workflow reproduzível do repo. + +**Gaps para 10/10** +- Rollback <5 min não testado/versionado. +- Feature flags não aparecem como sistema padronizado. +- Migrations reversíveis não são garantidas. +- Circuit breaker/desligamento por integração é parcial e não uniformizado. + +**Ações corretivas** +- Criar runbook de rollback com simulação trimestral. +- Introduzir feature flags por integração e kill switches operacionais. +- Exigir migration plan com rollback/forward-fix para mudanças de schema. +- Criar matriz de degradação: qual módulo funciona quando cada provider cai. + +### 14. Performance — **7/10** + +**Justificativa:** há lazy loading, React Query cache, virtualização e scripts de orçamento de performance; faltam dados atuais de Web Vitals reais, bundle budget integrado ao CI principal e auditoria de queries top. + +**Evidências** +- `App.tsx` usa lazy imports e deferred providers após first paint. +- React Query tem stale/gc/retry configurados em `AppProviders`. +- `package.json` possui `perf:budget` e `perf:budget:baseline`. +- Dependências incluem virtualização (`@tanstack/react-virtual`, `react-window`) e Web Vitals são documentados em runbook. +- Não há evidência local de execução Lighthouse/Web Vitals ou bundle analysis recente. + +**Gaps para 10/10** +- `perf:budget` não faz parte de `check`/CI principal. +- Bundle inicial e LCP reais não estão anexados como artefatos por PR. +- Falta EXPLAIN/index coverage das top queries. +- Falta rate limiting uniforme em endpoints públicos e debounce auditado por regra. + +**Ações corretivas** +- Adicionar `bun run perf:budget` ao CI. +- Gerar bundle analyzer artifact e budget por chunk. +- Executar Lighthouse CI ou Playwright Web Vitals nos fluxos críticos. +- Criar dashboard de queries lentas e índices top 20. + +### 15. Qualidade de Código — **6/10** + +**Justificativa:** ESLint, Prettier, Husky e scripts de checks existem, mas strictness baixa, E2E desligado, `console.*` direto e TODOs mantêm risco. + +**Evidências** +- `package.json` define `lint`, `typecheck`, `check`, `verify`, `prepare: husky` e Prettier como dev dependency. +- `eslint.config.js` existe. +- `.github/PULL_REQUEST_TEMPLATE.md` e CODEOWNERS existem. +- Contagens locais: 259 `console.*`, 21 TODO/FIXME/HACK e 906 `any`. + +**Gaps para 10/10** +- ESLint ainda permite padrões legados ou exceções amplas. +- Pre-commit depende de Husky instalado localmente; não garante CI para tudo. +- Sem regra clara de zero console em produção para functions/frontend. +- Sem conventional commits/changelog automatizado obrigatório. + +**Ações corretivas** +- Ativar `@typescript-eslint/no-explicit-any` como warn e depois error. +- Banir `console.*` direto fora de logger compartilhado. +- Reforçar `noUnusedLocals`/`noUnusedParameters` por diretório. +- Adicionar release/changelog automation com conventional commits. + +### 16. Segurança — **7/10** + +**Justificativa:** há gitleaks, CodeQL/security workflow, HMAC webhooks, secret status, scanner de upload e docs de segurança, mas há riscos relevantes: token em localStorage, CORS/headers dependem de deploy externo, CVEs não foram auditados nesta execução e nem todos os webhooks/functions foram validados individualmente. + +**Evidências** +- Security workflow roda Gitleaks em PR/push e relatório RLS semanal/manual. +- Existem docs `WEBHOOK_SECURITY.md`, `SECURITY_ALERTS.md`, `KEY_ROTATION_PROCEDURE.md` e templates de security headers. +- Existem helpers/functions de HMAC, webhook selftest, secret status, secure upload e file-security-scanner. +- Supabase client usa localStorage para sessão. +- Não foi executado audit de dependências com sucesso como parte desta auditoria antes da correção; deve entrar no pipeline. + +**Gaps para 10/10** +- Sessões de alto privilégio não protegidas por cookies HttpOnly. +- CSP/HSTS/CORS de produção não são comprováveis pelo repo. +- SCA/dependency audit deve ser obrigatório e com política de severidade. +- LGPD precisa de matriz PII/retenção/direito ao esquecimento por entidade. +- Pentest/security audit externo não evidenciado. + +**Ações corretivas** +- Adicionar SCA no CI e bloquear high/critical exploráveis. +- Validar headers reais de produção com smoke test (`curl -I`) em pipeline pós-deploy. +- Criar `docs/lgpd/data-map.md` e workflows de retenção/anonimização. +- Tornar HMAC strict por padrão para webhooks externos e testar rotação. + +### 17. Testes — **7/10** + +**Justificativa:** há volume bom de testes unitários/E2E e CI executa Vitest/Deno, mas E2E está desativado no CI e cobertura por lógica crítica/RLS não é comprovada >70/80%. + +**Evidências** +- Contagem local: 101 arquivos `.test.*`/`.spec.*` em `src`, `tests`, `e2e`, `supabase/functions` e `scripts`. +- CI roda `bunx vitest run --coverage` e `deno test` em Edge Functions. +- Playwright está configurado e há muitos specs em `e2e/`, mas o CI comenta a execução. +- Existem testes de fuzz, visual, reliability, webhooks e RLS/segurança em nomes de specs. + +**Gaps para 10/10** +- Playwright não roda em PR. +- Cobertura real não anexada nem usada como gate por domínio crítico. +- Testes de carga/stress não aparecem como gate regular. +- Testes de contrato para Bitrix/Evolution/Gmail não evidenciados. + +**Ações corretivas** +- Criar ambiente E2E isolado e reativar Playwright smoke em PR. +- Definir coverage thresholds por `src/domain`, `features/auth`, functions críticas e helpers. +- Adicionar contrato/mock server para Evolution/Bitrix/Gmail. +- Rodar k6/Artillery em nightly para webhooks/send-message. + +### 18. Tipagem / Type Safety — **5/10** + +**Justificativa:** o projeto usa TypeScript e tipos Supabase gerados, mas `strict` está desligado, `noImplicitAny` e `strictNullChecks` estão desligados e há 906 ocorrências de `any`. + +**Evidências** +- `tsconfig.app.json` define `strict: false`, `strictNullChecks: false`, `noImplicitAny: false`, `noUnusedLocals: false` e `skipLibCheck: true`. +- `package.json` possui `types:gen`, `types:check` e `typecheck` com validação de tipos Supabase. +- `src/integrations/supabase/client.ts` usa `createClient`. +- Zod é dependência de runtime e há schemas compartilhados em Edge Functions. + +**Gaps para 10/10** +- Strict mode desligado. +- Muitos `any` e assertions não justificadas. +- Null safety desligado. +- Validação runtime não é comprovada em todas as respostas de API. + +**Ações corretivas** +- Criar `tsconfig.strict.json` incremental por diretório e aumentar coverage semanal. +- Trocar `any` por `unknown`, tipos gerados ou type guards. +- Ativar `strictNullChecks` primeiro em módulos de domínio/helpers. +- Exigir Zod parse nas boundaries externas. + +### 19. Validação — **7/10** + +**Justificativa:** Zod e helpers de schemas existem, React Hook Form está disponível e há validações em functions; porém não há prova de schema compartilhado em 100% das escritas nem validação robusta de uploads em todos os caminhos. + +**Evidências** +- Zod é dependência e há `supabase/functions/_shared/schemas.ts`. +- Edge Functions possuem `_shared/validation.ts` e secure upload/file scanner. +- Frontend usa React Hook Form e há diretório `src/schemas`. +- Validações específicas de CPF/CNPJ/telefone/CEP e magic bytes não foram auditadas como cobertura total. + +**Gaps para 10/10** +- Backend validation não é garantia em todas as Edge Functions e RPCs. +- Schemas frontend/backend não parecem compartilhar pacote único obrigatório. +- Mensagens de erro e limites por campo não estão centralizados. +- Upload validation precisa matriz MIME/magic bytes/size/malware por fluxo. + +**Ações corretivas** +- Criar pacote/pasta `src/shared/schemas` ou `packages/schemas` e importar nos dois lados quando possível. +- Adicionar lint/check para Edge Function sem `validateBody`/schema. +- Criar `docs/validation/input-contracts.md` com limites e mensagens. +- Adicionar testes de fuzz para uploads e formatos BR. + +### 20. Operações (Processos) — **7/10** + +**Justificativa:** processos estão bem documentados para branch protection, deploy, incidentes, onboarding, troubleshooting e dependabot. Ainda faltam evidências de SLA real de review, hotfix exercitado, backlog técnico visível e cadência comprovada de segurança/deps. + +**Evidências** +- `docs/BRANCH_PROTECTION.md` define PR obrigatório, approval, checks e sem force push. +- `docs/runbooks/deploy.md`, `docs/INCIDENT_RUNBOOK.md`, `docs/ONBOARDING.md`, `docs/TROUBLESHOOTING.md` existem. +- Dependabot está configurado. +- PR template e CODEOWNERS existem. + +**Gaps para 10/10** +- Configuração real de branch protection não é auditável sem GitHub Admin/API. +- SLA de code review e ownership por domínio não estão comprovados. +- Processo de hotfix/rollback precisa teste prático. +- Backlog técnico priorizado não está no repo. + +**Ações corretivas** +- Criar `docs/engineering/review-sla.md` com SLA, owners e critérios. +- Adicionar rotina mensal de dependency updates/security review no calendário operacional. +- Registrar exercícios de incident/hotfix/rollback. +- Manter `TECH_DEBT.md` ou issues exportadas com prioridade/ROI. + +## 3. Scorecard Consolidado + +| Dimensão | Nota | Gap principal para 10/10 | +|---|---:|---| +| 1. Arquitetura | 7/10 | Boundaries feature/domain ainda não são obrigatórios em todo o codebase. | +| 2. Autenticação | 6/10 | Tokens em localStorage e MFA/rate limit/lockout não comprovados como padrão. | +| 3. Autorização | 7/10 | RLS/RBAC não têm gate completo por PR e matriz endpoint × role. | +| 4. Banco de Dados | 7/10 | Falta replay de migrations + EXPLAIN top queries + rollback de schema. | +| 5. CI/CD | 7/10 | E2E está desativado no CI e deploy/rollback não são automatizados pelo repo. | +| 6. Data Integrity | 7/10 | Idempotência/transações/locking não são padrão comprovado em todas as mutações críticas. | +| 7. Documentação | 8/10 | Falta OpenAPI e dicionário de dados completo/automatizado. | +| 8. Infraestrutura / DevOps | 5/10 | Ausência de IaC ponta a ponta e evidência de ambientes/restore reais. | +| 9. Logging / Monitoring | 7/10 | Logging não é JSON/correlation/redaction uniforme; muitos `console.*`. | +| 10. Observabilidade | 6/10 | Sem tracing distribuído e SLOs formais por serviço crítico. | +| 11. Lógica de Negócio | 6/10 | Regras críticas ainda dispersas em hooks/components/functions. | +| 12. Manutenibilidade | 6/10 | Strictness baixa, muitos `any` e padrões globais/legacy coexistentes. | +| 13. Operacionalidade | 6/10 | Rollback, feature flags e circuit breakers não padronizados/testados. | +| 14. Performance | 7/10 | Budgets/Web Vitals/EXPLAIN não são gates regulares. | +| 15. Qualidade de Código | 6/10 | ESLint/TS ainda permitem `any`, console direto e strict off. | +| 16. Segurança | 7/10 | Sessão localStorage, SCA/headers/LGPD/pentest não comprovados integralmente. | +| 17. Testes | 7/10 | E2E fora do CI e cobertura crítica/RLS sem gate. | +| 18. Tipagem / Type Safety | 5/10 | `strict`, `strictNullChecks` e `noImplicitAny` estão desligados. | +| 19. Validação | 7/10 | Schemas não são boundary obrigatório em 100% das escritas externas. | +| 20. Operações (Processos) | 7/10 | SLA de review/hotfix/backlog técnico não comprovados. | + +**Nota geral ponderada:** **6,6/10** +Cálculo: dimensões críticas ×3 (Segurança 7, Autenticação 6, Autorização 7, Data Integrity 7), altas ×2 (Banco 7, Tipagem 5, Validação 7, Testes 7, Arquitetura 7) e demais ×1. Soma ponderada 250 / peso total 38 = 6,58. + +## 4. Top 10 Ações de Maior Impacto (ROI) + +1. **[P0] [Tipagem] — Criar plano de strict mode incremental** + - Impacto: Alto + - Esforço: Médio + - Tipo: Config / Código + - Arquivos afetados: `tsconfig.app.json`, `eslint.config.js`, módulos priorizados + - Descrição técnica: criar `tsconfig.strict.json`, ativar `strictNullChecks`/`noImplicitAny` por diretório e reduzir `any` por waves. + - Critério de aceite: `bun run typecheck` passa e novos módulos críticos não aceitam `any`/null unsafe. + +2. **[P0] [CI/CD/Testes] — Reativar Playwright smoke no CI** + - Impacto: Alto + - Esforço: Médio + - Tipo: Config / Processo + - Arquivos afetados: `.github/workflows/ci.yml`, `playwright.config.ts`, `e2e/fixtures/*` + - Descrição técnica: provisionar Supabase test/staging, secrets e seed determinístico para smoke auth/inbox/send-message. + - Critério de aceite: PR falha quando fluxo crítico E2E quebra. + +3. **[P0] [Segurança] — Adicionar SCA obrigatório no pipeline** + - Impacto: Alto + - Esforço: Baixo + - Tipo: Config + - Arquivos afetados: `.github/workflows/security.yml` ou `.github/workflows/ci.yml` + - Descrição técnica: rodar `bun audit`/OSV/Snyk e bloquear high/critical conforme política. + - Critério de aceite: relatório de dependências anexado em PR e falha em CVE crítica. + +4. **[P0] [Autorização] — Tornar auditoria RLS gate de PR** + - Impacto: Alto + - Esforço: Baixo/Médio + - Tipo: Config / Teste + - Arquivos afetados: `.github/workflows/security.yml`, `scripts/verify_rls_compliance.ts` + - Descrição técnica: executar compliance RLS em pull requests e publicar artefato. + - Critério de aceite: tabela nova sem RLS/policy falha o PR. + +5. **[P0] [Logging] — Banir `console.*` direto e padronizar logger/redaction** + - Impacto: Alto + - Esforço: Médio + - Tipo: Código / Config + - Arquivos afetados: `src/lib/logger.ts`, `supabase/functions/_shared/`, `eslint.config.js` + - Descrição técnica: criar logger edge compartilhado, redaction helper e ESLint no-console com allowlist controlada. + - Critério de aceite: zero `console.*` fora de logger/helpers aprovados. + +6. **[P1] [Autenticação] — Política MFA/step-up para roles privilegiadas** + - Impacto: Alto + - Esforço: Médio + - Tipo: Código / Config / Documentação + - Arquivos afetados: `src/features/auth/*`, Supabase Auth config, docs de auth + - Descrição técnica: exigir WebAuthn/MFA para admin/supervisor e registrar baseline Supabase Auth. + - Critério de aceite: admin sem MFA não acessa rotas/functions sensíveis. + +7. **[P1] [Banco] — Replay de migrations em banco efêmero** + - Impacto: Alto + - Esforço: Médio + - Tipo: Config / Migration + - Arquivos afetados: `.github/workflows/ci.yml`, `supabase/migrations/*` + - Descrição técnica: subir Postgres/Supabase local no CI e aplicar migrations do zero. + - Critério de aceite: migration quebrada ou não reprodutível falha PR. + +8. **[P1] [Observabilidade] — Definir SLOs e RED metrics para functions críticas** + - Impacto: Alto + - Esforço: Médio + - Tipo: Código / Documentação + - Arquivos afetados: `supabase/functions/_shared/`, `docs/observability/slos.md` + - Descrição técnica: instrumentar rate/errors/duration e associar alertas/runbooks. + - Critério de aceite: dashboard mostra RED por webhook/send-message/provider. + +9. **[P1] [Data Integrity] — Helper obrigatório de idempotência em webhooks/mutations externas** + - Impacto: Alto + - Esforço: Médio + - Tipo: Código / Migration + - Arquivos afetados: `supabase/functions/_shared/`, functions de webhook, migrations de dedupe + - Descrição técnica: padronizar idempotency key, unique constraints e replay-safe handlers. + - Critério de aceite: retries duplicados não duplicam dados em testes automatizados. + +10. **[P1] [Documentação/Validação] — OpenAPI + contratos de input/output** + - Impacto: Médio/Alto + - Esforço: Médio + - Tipo: Documentação / Código + - Arquivos afetados: `openapi.yaml`, `supabase/functions/_shared/schemas.ts`, docs de API + - Descrição técnica: documentar endpoints públicos e gerar testes/schema validation. + - Critério de aceite: divergência entre schema e handler falha CI. + +## 5. Roadmap em 3 Ondas + +### 🔴 Quick Wins (1–3 dias) + +- Rodar SCA no CI e bloquear CVEs high/critical. +- Executar auditoria RLS em PRs, não só schedule/manual. +- Adicionar `perf:budget`, `check:domain` e `check:barrels` ao `check` principal ou workflow. +- Criar `docs/security/auth-baseline.md` com MFA, password policy, recovery e lockout esperados. +- Criar `docs/observability/slos.md` inicial com serviços críticos, owners, SLIs e alertas. +- Adicionar regra ESLint gradual para `no-console` e relatório de `any` por diretório. + +### 🟠 Sprint 1 (1–2 semanas) + +- Reativar Playwright smoke no CI com ambiente/seed isolado. +- Criar tsconfig strict incremental e migrar `src/lib`, `src/features/auth`, helpers críticos e schemas. +- Padronizar logger/redaction em frontend e Edge Functions. +- Implementar RLS tests por role para tabelas críticas. +- Instrumentar RED metrics nas Edge Functions de webhook/send-message/provider. +- Criar replay de migrations em banco efêmero no CI. + +### 🟡 Sprint 2 (2–4 semanas) + +- Migrar lógica crítica para `domain/` com testes unitários parametrizados. +- Formalizar state machines de conversa/mensagem/fila/conexão/ticket. +- Criar OpenAPI/contratos de Edge Functions e testes de contrato para Evolution/Bitrix/Gmail. +- Implementar feature flags/kill switches por integração. +- Criar plano LGPD completo: data map, retenção, anonimização e direito ao esquecimento. +- Introduzir tracing/correlation end-to-end e dashboards operacionais. + +## 6. Nota Final + +A maturidade geral do sistema é **intermediária-alta para um produto em evolução rápida**: há documentação robusta, muita automação, RLS, testes, Edge Functions, observabilidade inicial e preocupação real com segurança. O principal bloqueio para chegar a 9–10/10 não é ausência de funcionalidades, mas **falta de enforcement sistemático**: strict TypeScript desligado, E2E fora do CI, RLS/compliance parcial no PR, logs não uniformes, infra pouco declarada como código e regras de negócio ainda dispersas. O caminho recomendado é transformar as práticas já documentadas em gates automáticos e reduzir variação arquitetural/typagem por ondas incrementais. diff --git a/docs/observability/slos.md b/docs/observability/slos.md new file mode 100644 index 000000000..f7e487424 --- /dev/null +++ b/docs/observability/slos.md @@ -0,0 +1,45 @@ +# SLIs, SLOs e Alertas — ZAPP-WEB + +## Objetivo + +Definir metas operacionais mínimas para transformar monitoramento em decisões acionáveis. Cada alerta deve apontar para um runbook e ter owner claro. + +## Serviços críticos + +| Serviço | SLI | SLO inicial | Alerta | Runbook | +|---|---|---:|---|---| +| SPA Web | LCP p75 | < 2,5s | LCP p75 > 3s por 15min | `docs/runbooks/deploy.md` | +| Supabase Edge Functions | Error rate | < 1% em 30min | 5xx > 2% por 10min | `docs/INCIDENT_RUNBOOK.md` | +| Evolution webhook | Taxa de processamento | > 99% eventos válidos | DLQ/rejeição > 1% por 10min | `docs/WEBHOOK_SECURITY.md` | +| Envio de mensagens | Latência p95 | < 5s | p95 > 8s por 15min | `docs/DISTRIBUTION-FALLBACKS.md` | +| Vault/secrets | Healthcheck | 100% secrets decifráveis ou deferidos | qualquer fail não deferido | `docs/DR_RUNBOOK.md` | +| Banco/RLS | Compliance | 100% tabelas com RLS + policy | `security:rls` falha | `docs/decisions/ADR-002-supabase-rls-security.md` | + +## Métricas RED obrigatórias para Edge Functions críticas + +- **Rate:** requisições por minuto por função, provider e status. +- **Errors:** 4xx/5xx por função, provider, tipo de erro e correlação. +- **Duration:** p50/p95/p99 por função e provider externo. + +## Métricas de negócio mínimas + +- Mensagens recebidas/enviadas por hora. +- Tamanho e idade da fila/DLQ. +- Tempo médio até primeira resposta. +- Taxa de falha por provider (Evolution, WhatsApp Cloud, Gmail/Outlook, Bitrix). + +## Padrão de correlação + +Todo fluxo cross-service deve carregar `x-request-id` ou correlation id equivalente: + +```text +frontend → edge function → provider externo → persistência/audit log +``` + +Se o request não trouxer id, a boundary de entrada deve gerar um id e propagá-lo para logs e respostas. + +## Política de alerta + +1. Alertas P1 devem ter runbook e owner antes de entrarem em produção. +2. Alertas sem ação clara devem virar dashboard, não pager. +3. Todo incidente com impacto externo gera post-mortem em até 2 dias úteis. diff --git a/docs/operations/supabase-backup-and-drift-runbook.md b/docs/operations/supabase-backup-and-drift-runbook.md new file mode 100644 index 000000000..e6d7f9185 --- /dev/null +++ b/docs/operations/supabase-backup-and-drift-runbook.md @@ -0,0 +1,125 @@ +# Supabase self-hosted backup and drift runbook + +Este runbook transforma o handoff de 2026-05-07 em procedimentos reexecutáveis e auditáveis para a VPS AtomicaBR. Ele evita os dois problemas encontrados na investigação: backup amarrado a IP efêmero de container e drift entre compose declarado e imagens realmente em execução. + +> Nunca cole senhas neste arquivo. Use secrets, variáveis de ambiente ou o container operacional que já possui as credenciais necessárias. + +## Estado conhecido que motivou o runbook + +- `supabase-backup` usava `PGHOST` com IP hardcoded de container. IP de task Swarm muda em redeploy. +- O host correto deve ser DNS interno estável, por exemplo `supabase_db`. +- O backup antigo usava filtro de schema e não cobria schemas Supabase críticos como `auth`, `storage`, `vault`, `cron`, `realtime` e `supabase_functions`. +- Dumps de 20 bytes são falha crítica: gzip pode existir mesmo quando `pg_dump` falhou. +- Drift de imagem deve ser detectado antes de redeploy do stack Supabase. + +## Princípios obrigatórios + +1. Backup completo antes de qualquer alteração operacional no stack Supabase. +2. Não usar IP de container como `PGHOST`; usar DNS/VIP do Swarm. +3. Não usar `--schema=public` para backup de desastre do Supabase self-hosted. +4. Validar artifact por tamanho mínimo, `gunzip -t` e SHA256. +5. Fazer restore test antes de confiar no backup. +6. Corrigir drift via source of truth versionado, não por atualização manual invisível. + +## B2 — backup manual completo + +Execute dentro de um ambiente que tenha `pg_dump`, `gzip`, `sha256sum` e acesso ao Postgres. O script falha se `PGHOST` for IP. + +```bash +PGHOST=supabase_db \ +PGUSER=postgres \ +PGDATABASE=postgres \ +PGPASSWORD="$POSTGRES_PASSWORD" \ +BACKUP_DIR=/backups \ +BACKUP_COPY_DIR=/workspace/notes/backups \ +scripts/manual-supabase-backup.sh +``` + +Validações automáticas do script: + +- `pg_dump --no-owner --no-acl --verbose` sem filtro de schema. +- stderr em arquivo `.stderr.log` ao lado do backup. +- tamanho mínimo configurável por `MIN_BACKUP_BYTES`. +- `gunzip -t`. +- sidecar `.sha256`. +- cópia opcional para segundo diretório. + +Para validar novamente um artifact existente: + +```bash +MIN_BACKUP_BYTES=104857600 node scripts/validate-supabase-backup-artifact.mjs /backups/arquivo.sql.gz +``` + +## B3 — restore test obrigatório + +Fluxo recomendado: + +1. Criar database temporário com nome explícito, por exemplo `_restore_test_drop_after`. +2. Restaurar o dump validado no database temporário. +3. Comparar pelo menos estas contagens com produção: + - `auth.users` + - tabela principal de mensagens/conversas usada pela operação + - tabela de conexões WhatsApp +4. Validar que schemas Supabase existem no restore (`auth`, `storage`, `vault`, `cron`, `realtime`). +5. Dropar o database temporário. + +Não marque backup como confiável sem restore test. + +## B4 — correção do serviço de backup + +A configuração permanente do serviço precisa conter: + +```diff +- PGHOST=10.x.x.x ++ PGHOST=supabase_db +``` + +E o comando do backup precisa seguir estas regras: + +```diff +- pg_dump --schema=public ... 2>/dev/null ++ pg_dump --no-owner --no-acl --verbose ... 2>>/backups/error.log +``` + +Adicione sentinel de tamanho mínimo. Qualquer dump menor que 100 MiB deve ser tratado como falha até prova em contrário. + +## B5 — force update e monitoramento + +Depois de atualizar o serviço, force uma nova task e monitore: + +```bash +docker service update --force supabase-backup_backup +docker service ps supabase-backup_backup +``` + +Em seguida, dispare ou aguarde um novo backup e valide com: + +```bash +node scripts/validate-supabase-backup-artifact.mjs /backups/novo-arquivo.sql.gz +``` + +## V9 — detecção de drift Supabase + +Para comparar imagens declaradas no compose com imagens em execução: + +```bash +node scripts/check-supabase-drift.mjs --compose /root/supabase.yaml --stack supabase +``` + +O script usa `docker service inspect` e falha se qualquer imagem em runtime diferir do compose. Para CI/offline, capture o inspect em JSON e rode: + +```bash +docker service inspect supabase_db supabase_auth supabase_rest > /tmp/supabase-inspect.json +node scripts/check-supabase-drift.mjs \ + --compose /root/supabase.yaml \ + --stack supabase \ + --inspect-json /tmp/supabase-inspect.json +``` + +## Critério de encerramento + +- Backup manual completo validado por artifact e restore test. +- Serviço `supabase-backup` corrigido para DNS estável. +- Próximo backup automático validado e maior que o mínimo esperado. +- Drift report executado e anexado ao postmortem. +- Política de backup/offsite decidida e documentada. diff --git a/docs/security/auth-baseline.md b/docs/security/auth-baseline.md new file mode 100644 index 000000000..747e67481 --- /dev/null +++ b/docs/security/auth-baseline.md @@ -0,0 +1,40 @@ +# Auth Baseline — ZAPP-WEB + +## Objetivo + +Este baseline torna explícitos os controles mínimos para autenticação e sessões em produção. Ele deve ser usado como checklist para Supabase Auth, Edge Functions e fluxos privilegiados. + +## Controles obrigatórios + +| Controle | Baseline de produção | Critério de aceite | +|---|---|---| +| MFA / step-up | Admins, supervisores e devs devem ter MFA/WebAuthn antes de acessar rotas e funções sensíveis. | Usuário privilegiado sem MFA não consegue acessar painel admin nem executar mutation sensível. | +| Sessão | Refresh automático habilitado; sessões inativas expiram conforme política Supabase Auth. | Sessão expirada redireciona para login sem loop de 401 silencioso. | +| Storage de token | O SPA ainda usa Supabase localStorage; operações privilegiadas devem exigir step-up e RLS server-side. Evolução recomendada: BFF/cookie HttpOnly para roles privilegiadas. | Threat model documentado e backlog aberto para cookie HttpOnly/BFF. | +| Brute force | Tentativas de login/reset devem ser registradas e bloqueadas por rate limit/lockout. | Após N tentativas falhas, novo login é bloqueado temporariamente e auditado. | +| Password policy | Mínimo 12 caracteres, verificação contra senhas comuns e confirmação de email obrigatória. | Configuração Supabase revisada e evidenciada a cada release de segurança. | +| Recovery | Tokens de recuperação com expiração curta e uso único; reset auditado. | Link usado duas vezes falha; reset gera trilha auditável. | +| Logout | Logout limpa sessão local e invalida refresh quando suportado pela plataforma. | Após logout, refresh/token antigo não reabre sessão privilegiada. | +| Service role | Chaves service-role nunca no frontend; Edge Functions validam role antes de mutações sensíveis. | Secret scan + code review impedem exposição e bypass. | + +## Matriz de acesso privilegiado + +| Role | MFA obrigatório | Rotas/ações com step-up | +|---|---:|---| +| admin | Sim | `/admin/*`, gestão de usuários, RLS recheck, secrets, webhooks, auditoria, stress tests. | +| supervisor | Sim | filas, relatórios operacionais, automações, painéis de monitoramento. | +| dev | Sim | diagnósticos, external DB explorer, health/proxy internals. | +| agent/viewer | Não obrigatório no baseline inicial | Fluxos padrão protegidos por Supabase Auth + RLS. | + +## Evidências esperadas por release + +1. Screenshot/export da configuração Supabase Auth relevante. +2. Resultado dos testes E2E de login/logout/reset/MFA quando disponíveis. +3. Relatório de RLS (`bun run security:rls`). +4. Resultado de secret scan e dependency review no PR. + +## Próximas evoluções + +- Implementar enforcement de MFA/step-up no `ProtectedRoute` e em Edge Functions críticas. +- Criar testes E2E para bloqueio de admin sem MFA. +- Avaliar arquitetura BFF/cookie HttpOnly para sessões privilegiadas. diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 000000000..20152ed5f --- /dev/null +++ b/nginx.conf @@ -0,0 +1,51 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + server_tokens off; + + gzip on; + gzip_comp_level 6; + gzip_min_length 1000; + gzip_vary on; + gzip_types + text/plain + text/css + text/xml + application/json + application/javascript + application/xml + application/rss+xml + image/svg+xml; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; + + location = /healthz { + access_log off; + add_header Content-Type text/plain; + add_header Cache-Control "no-store"; + return 200 "ok\n"; + } + + location = /version.json { + add_header Cache-Control "no-store"; + try_files $uri =404; + } + + location /assets/ { + try_files $uri =404; + expires 1y; + add_header Cache-Control "public, immutable" always; + } + + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache" always; + } +} diff --git a/package.json b/package.json index 7fb509c73..4c2c1dd08 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "typecheck": "npm run types:check && tsc --noEmit -p tsconfig.app.json", "types:gen": "bash scripts/validate-supabase-types.sh", "types:check": "bash scripts/validate-supabase-types.sh --check --summary", - "check": "npm run typecheck && npm run lint && npm run build", + "check": "npm run typecheck && npm run typecheck:strict:core && npm run lint && npm run check:domain && npm run check:barrels && npm run api:validate && npm run vps:check && npm run build && npm run perf:budget", "verify": "npm run check", "prebuild": "npm run types:gen && bun run scripts/generate-component-registry.ts", "lint": "eslint . && bun run scripts/check-design-system.ts --ci", @@ -32,7 +32,14 @@ "ds:fix": "bun run scripts/check-design-system.ts --apply-patch", "ds:check": "bun run scripts/check-design-system.ts --ci", "ds:test": "bun test scripts/check-design-system.test.ts", - "prepare": "husky" + "prepare": "husky", + "security:rls": "bun run scripts/verify_rls_compliance.ts", + "lint-staged": "lint-staged", + "typecheck:strict:core": "tsc --noEmit -p tsconfig.strict.core.json", + "api:validate": "node scripts/validate-openapi.mjs", + "vps:check": "node scripts/check-vps-readiness.mjs", + "ops:backup:validate": "node scripts/validate-supabase-backup-artifact.mjs", + "ops:drift:check": "node scripts/check-supabase-drift.mjs" }, "dependencies": { "@axe-core/react": "^4.11.1", @@ -123,6 +130,7 @@ "globals": "^15.15.0", "happy-dom": "^20.9.0", "husky": "^9.1.7", + "lint-staged": "^17.0.4", "playwright": "^1.59.1", "postcss": "^8.5.6", "prettier": "^3.8.3", diff --git a/public/healthz b/public/healthz new file mode 100644 index 000000000..9766475a4 --- /dev/null +++ b/public/healthz @@ -0,0 +1 @@ +ok diff --git a/public/version.json b/public/version.json new file mode 100644 index 000000000..cfab65c13 --- /dev/null +++ b/public/version.json @@ -0,0 +1 @@ +{"name":"zapp-web","version":"2.0.1"} diff --git a/scripts/check-performance-budget.mjs b/scripts/check-performance-budget.mjs new file mode 100755 index 000000000..510b46acf --- /dev/null +++ b/scripts/check-performance-budget.mjs @@ -0,0 +1,87 @@ +#!/usr/bin/env node +import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { gzipSync } from 'node:zlib'; + +const root = process.cwd(); +const distDir = join(root, 'dist'); +const assetsDir = join(distDir, 'assets'); + +const budgets = { + initialJsGzipKb: Number(process.env.PERF_BUDGET_INITIAL_JS_GZIP_KB ?? 1500), + chunkJsGzipKb: Number(process.env.PERF_BUDGET_CHUNK_JS_GZIP_KB ?? 1500), + totalJsGzipKb: Number(process.env.PERF_BUDGET_TOTAL_JS_GZIP_KB ?? 2700), + totalCssGzipKb: Number(process.env.PERF_BUDGET_TOTAL_CSS_GZIP_KB ?? 250), +}; + +function walk(dir, files = []) { + for (const entry of readdirSync(dir)) { + const path = join(dir, entry); + const stat = statSync(path); + if (stat.isDirectory()) walk(path, files); + else files.push(path); + } + return files; +} + +function gzipKb(path) { + return gzipSync(readFileSync(path)).length / 1024; +} + +function formatKb(value) { + return `${value.toFixed(1)} KiB`; +} + +if (!existsSync(distDir) || !existsSync(assetsDir)) { + console.error('❌ dist/assets não encontrado. Execute `bun run build` antes de `bun run perf:budget`.'); + process.exit(1); +} + +const files = walk(assetsDir); +const jsFiles = files.filter((file) => file.endsWith('.js')); +const cssFiles = files.filter((file) => file.endsWith('.css')); + +if (jsFiles.length === 0) { + console.error('❌ Nenhum bundle JavaScript encontrado em dist/assets.'); + process.exit(1); +} + +const jsMetrics = jsFiles + .map((file) => ({ file, gzipKb: gzipKb(file) })) + .sort((a, b) => b.gzipKb - a.gzipKb); +const cssMetrics = cssFiles.map((file) => ({ file, gzipKb: gzipKb(file) })); + +const initialCandidates = jsMetrics.filter(({ file }) => /index-[\w-]+\.js$/.test(file)); +const initial = initialCandidates[0] ?? jsMetrics[0]; +const totalJsGzipKb = jsMetrics.reduce((sum, metric) => sum + metric.gzipKb, 0); +const totalCssGzipKb = cssMetrics.reduce((sum, metric) => sum + metric.gzipKb, 0); + +const failures = []; +if (initial.gzipKb > budgets.initialJsGzipKb) { + failures.push(`Bundle inicial ${relative(root, initial.file)} = ${formatKb(initial.gzipKb)} > ${formatKb(budgets.initialJsGzipKb)}`); +} +for (const metric of jsMetrics) { + if (metric.gzipKb > budgets.chunkJsGzipKb) { + failures.push(`Chunk JS ${relative(root, metric.file)} = ${formatKb(metric.gzipKb)} > ${formatKb(budgets.chunkJsGzipKb)}`); + } +} +if (totalJsGzipKb > budgets.totalJsGzipKb) { + failures.push(`Total JS gzip = ${formatKb(totalJsGzipKb)} > ${formatKb(budgets.totalJsGzipKb)}`); +} +if (totalCssGzipKb > budgets.totalCssGzipKb) { + failures.push(`Total CSS gzip = ${formatKb(totalCssGzipKb)} > ${formatKb(budgets.totalCssGzipKb)}`); +} + +console.log('📦 Performance budget'); +console.log(`- Initial JS: ${relative(root, initial.file)} (${formatKb(initial.gzipKb)} / ${formatKb(budgets.initialJsGzipKb)})`); +console.log(`- Largest JS chunk: ${relative(root, jsMetrics[0].file)} (${formatKb(jsMetrics[0].gzipKb)} / ${formatKb(budgets.chunkJsGzipKb)})`); +console.log(`- Total JS gzip: ${formatKb(totalJsGzipKb)} / ${formatKb(budgets.totalJsGzipKb)}`); +console.log(`- Total CSS gzip: ${formatKb(totalCssGzipKb)} / ${formatKb(budgets.totalCssGzipKb)}`); + +if (failures.length > 0) { + console.error('\n🚨 Performance budget excedido:'); + for (const failure of failures) console.error(`- ${failure}`); + process.exit(1); +} + +console.log('✅ Performance budget dentro dos limites configurados.'); diff --git a/scripts/check-supabase-drift.mjs b/scripts/check-supabase-drift.mjs new file mode 100755 index 000000000..869f0bb04 --- /dev/null +++ b/scripts/check-supabase-drift.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node +import { existsSync, readFileSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; + +const args = new Map(); +for (let index = 2; index < process.argv.length; index += 1) { + const arg = process.argv[index]; + if (arg.startsWith('--')) args.set(arg, process.argv[index + 1] ?? ''); +} + +const composeFile = args.get('--compose') ?? process.env.SUPABASE_COMPOSE_FILE; +const stack = args.get('--stack') ?? process.env.SUPABASE_STACK ?? 'supabase'; +const inspectFile = args.get('--inspect-json') ?? process.env.SUPABASE_SERVICE_INSPECT_JSON; + +function usage(exitCode = 0) { + console.log(`Usage: node scripts/check-supabase-drift.mjs --compose /path/supabase.yaml [--stack supabase]\n\nOptions:\n --compose Docker Compose/Stack YAML containing service image declarations.\n --stack Docker stack name used to inspect services. Default: supabase.\n --inspect-json Optional JSON file with docker service inspect output for offline checks.\n\nThe script fails when a running Swarm service image differs from the compose image.`); + process.exit(exitCode); +} + +if (process.argv.includes('--help') || process.argv.includes('-h')) usage(0); +if (!composeFile) usage(2); +if (!existsSync(composeFile)) { + console.error(`❌ Compose file not found: ${composeFile}`); + process.exit(1); +} + +function parseComposeImages(text) { + const images = new Map(); + let inServices = false; + let currentService = null; + let serviceIndent = null; + + for (const rawLine of text.split('\n')) { + const line = rawLine.replace(/#.*$/, ''); + if (!line.trim()) continue; + const indent = line.match(/^\s*/)?.[0].length ?? 0; + + if (/^services:\s*$/.test(line)) { + inServices = true; + currentService = null; + serviceIndent = null; + continue; + } + if (!inServices) continue; + if (indent === 0 && !/^services:\s*$/.test(line)) break; + + const serviceMatch = line.match(/^(\s{2,})([A-Za-z0-9_.-]+):\s*$/); + if (serviceMatch) { + currentService = serviceMatch[2]; + serviceIndent = serviceMatch[1].length; + continue; + } + + if (currentService && serviceIndent !== null && indent > serviceIndent) { + const imageMatch = line.match(/^\s+image:\s*["']?([^"'\s]+)["']?\s*$/); + if (imageMatch) images.set(currentService, imageMatch[1]); + } + } + + return images; +} + +function readRuntimeImages() { + if (inspectFile) { + return JSON.parse(readFileSync(inspectFile, 'utf8')); + } + + const serviceNames = [...expected.keys()].map((service) => `${stack}_${service}`); + const inspect = spawnSync('docker', ['service', 'inspect', ...serviceNames], { encoding: 'utf8' }); + if (inspect.status !== 0) { + console.error(inspect.stderr || inspect.stdout); + process.exit(inspect.status ?? 1); + } + return JSON.parse(inspect.stdout); +} + +const expected = parseComposeImages(readFileSync(composeFile, 'utf8')); +if (expected.size === 0) { + console.error(`❌ No service image declarations found in ${composeFile}`); + process.exit(1); +} + +const inspected = readRuntimeImages(); +const runtime = new Map(); +for (const service of inspected) { + const name = service?.Spec?.Name?.startsWith(`${stack}_`) + ? service.Spec.Name.slice(stack.length + 1) + : service?.Spec?.Name; + const image = service?.Spec?.TaskTemplate?.ContainerSpec?.Image?.split('@')[0]; + if (name && image) runtime.set(name, image); +} + +const rows = []; +for (const [service, expectedImage] of expected) { + const runtimeImage = runtime.get(service); + rows.push({ service, expectedImage, runtimeImage, pass: runtimeImage === expectedImage }); +} + +const failures = rows.filter((row) => !row.pass); +console.log('# Supabase Image Drift Report'); +console.log(''); +for (const row of rows) { + console.log(`- ${row.pass ? '✅' : '❌'} ${row.service}: compose=${row.expectedImage} runtime=${row.runtimeImage ?? 'missing'}`); +} +console.log(''); +console.log(`Summary: ${rows.length - failures.length}/${rows.length} service images match compose.`); + +if (failures.length > 0) { + console.error(`🚨 Supabase drift detected: ${failures.length} service(s) differ from compose.`); + process.exit(1); +} diff --git a/scripts/check-vps-readiness.mjs b/scripts/check-vps-readiness.mjs new file mode 100755 index 000000000..9d29c91ed --- /dev/null +++ b/scripts/check-vps-readiness.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +const checks = []; +const add = (name, pass, detail) => checks.push({ name, pass, detail }); +const read = (path) => existsSync(path) ? readFileSync(path, 'utf8') : ''; +const pkg = JSON.parse(read('package.json')); + +function walkFiles(dir, files = []) { + if (!existsSync(dir)) return files; + for (const entry of readdirSync(dir)) { + const path = join(dir, entry); + const stat = statSync(path); + if (stat.isDirectory()) walkFiles(path, files); + else files.push(path); + } + return files; +} + +function sourceContains(pattern, roots) { + return roots + .flatMap((root) => walkFiles(root)) + .filter((file) => /\.(ts|tsx|js|jsx)$/.test(file)) + .some((file) => pattern.test(read(file))); +} + +add('Dockerfile exists', existsSync('Dockerfile'), 'multi-stage Bun build + nginx runtime required'); +add('docker-compose.yml exists', existsSync('docker-compose.yml'), 'Traefik/VPS stack descriptor required'); +add('nginx.conf exists', existsSync('nginx.conf'), 'SPA fallback, healthcheck, cache and security headers required'); +add('.dockerignore exists', existsSync('.dockerignore'), 'Docker context must exclude node_modules/dist/secrets'); +add('frontend healthz exists', existsSync('public/healthz'), 'nginx and container healthcheck must return 200'); +add('deploy-vps workflow exists', existsSync('.github/workflows/deploy-vps.yml'), 'manual staging/production deploy workflow required'); + +const vite = read('vite.config.ts'); +add('Vite target configured', /target:\s*["']es2020["']/.test(vite), 'build.target must be explicit'); +add('Vite manualChunks configured', /manualChunks/.test(vite), 'vendor chunk splitting must be explicit'); +add('Vite sourcemap policy configured', /sourcemap/.test(vite), 'source maps must be intentional per mode'); +add('Vite chunk warning limit configured', /chunkSizeWarningLimit/.test(vite), 'large chunks must be visible'); + +const supabaseConfig = read('supabase/config.toml'); +const activeSupabaseConfig = supabaseConfig + .split('\n') + .filter((line) => !line.trim().startsWith('#')) + .join('\n'); +add('Supabase Cloud project_id removed', !/project_id\s*=\s*["']allrjhkpuscmgbsnmjlv["']/.test(activeSupabaseConfig), 'self-hosted flow must not deploy via Cloud project_id'); +add('Supabase self-hosted documented', /supabase\.atomicabr\.com\.br/.test(supabaseConfig), 'config must document canonical self-hosted endpoint'); + +const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }; +add('No Lovable Cloud Auth runtime dependency', !('@lovable.dev/cloud-auth-js' in deps), 'Lovable cloud auth must not be bundled for VPS'); +const sourceUsesLovableAuth = sourceContains(/@lovable\.dev\/cloud-auth-js/, ['src', 'supabase/functions']); +add('Lovable tagger not in Vite config', !/lovable-tagger|componentTagger/.test(vite), 'development-only Lovable tagging must not be in production config'); +add('No direct Lovable auth source import', !sourceUsesLovableAuth, 'source must not import @lovable.dev/cloud-auth-js'); + +const failures = checks.filter((check) => !check.pass); +console.log('# VPS Readiness Report'); +console.log(''); +for (const check of checks) { + console.log(`- ${check.pass ? '✅' : '❌'} ${check.name} — ${check.detail}`); +} +console.log(''); +console.log(`Summary: ${checks.length - failures.length}/${checks.length} checks passing.`); + +if (failures.length > 0) { + console.error(`🚨 VPS readiness failed: ${failures.length} blocker(s) remain.`); + process.exit(1); +} + +console.log('✅ VPS readiness blockers covered by repository artifacts.'); diff --git a/scripts/manual-supabase-backup.sh b/scripts/manual-supabase-backup.sh new file mode 100755 index 000000000..c8c77d15c --- /dev/null +++ b/scripts/manual-supabase-backup.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/manual-supabase-backup.sh + +Creates a full logical PostgreSQL backup, validates gzip integrity, writes a +SHA256 sidecar and optionally copies the artifact to a second directory. + +Required env: + PGHOST DNS/host for Postgres. Use Swarm DNS, never container IPs. + PGUSER Postgres user. + PGDATABASE Database name. + PGPASSWORD Postgres password. Prefer passing as a secret/env, not CLI. + +Optional env: + BACKUP_DIR Destination directory. Default: /backups + BACKUP_PREFIX File prefix. Default: supabase-full + BACKUP_COPY_DIR Optional second destination directory. + MIN_BACKUP_BYTES Minimum accepted gzip size. Default: 104857600 (100 MiB) + PGDUMP_EXTRA_ARGS Extra pg_dump args. Default: empty + +Example inside the backup container: + PGHOST=supabase_db PGUSER=postgres PGDATABASE=postgres PGPASSWORD=... \ + BACKUP_COPY_DIR=/workspace/notes/backups \ + scripts/manual-supabase-backup.sh +EOF +} + +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + usage + exit 0 +fi + +for required in PGHOST PGUSER PGDATABASE PGPASSWORD; do + if [[ -z "${!required:-}" ]]; then + echo "❌ Missing required env: ${required}" >&2 + usage >&2 + exit 2 + fi +done + +BACKUP_DIR="${BACKUP_DIR:-/backups}" +BACKUP_PREFIX="${BACKUP_PREFIX:-supabase-full}" +MIN_BACKUP_BYTES="${MIN_BACKUP_BYTES:-104857600}" +PGDUMP_EXTRA_ARGS="${PGDUMP_EXTRA_ARGS:-}" +TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)" +BACKUP_FILE="${BACKUP_DIR%/}/${BACKUP_PREFIX}-${TIMESTAMP}.sql.gz" +ERROR_LOG="${BACKUP_FILE%.sql.gz}.stderr.log" +SHA_FILE="${BACKUP_FILE}.sha256" + +mkdir -p "$BACKUP_DIR" +if [[ -n "${BACKUP_COPY_DIR:-}" ]]; then + mkdir -p "$BACKUP_COPY_DIR" +fi + +cleanup_partial() { + local status=$? + if [[ $status -ne 0 ]]; then + rm -f "$BACKUP_FILE" "$SHA_FILE" + echo "❌ Backup failed. See stderr log: ${ERROR_LOG}" >&2 + fi + exit $status +} +trap cleanup_partial EXIT + +if [[ "$PGHOST" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "❌ PGHOST is an IP address (${PGHOST}). Use stable DNS such as supabase_db." >&2 + exit 2 +fi + +if ! [[ "$MIN_BACKUP_BYTES" =~ ^[0-9]+$ ]]; then + echo "❌ MIN_BACKUP_BYTES must be numeric." >&2 + exit 2 +fi + +export PGHOST PGUSER PGDATABASE PGPASSWORD + +echo "🔄 Starting full logical backup to ${BACKUP_FILE}" +# Intentionally no --schema filter: backup all schemas, including auth/storage/vault/cron/etc. +# shellcheck disable=SC2086 +pg_dump --no-owner --no-acl --verbose ${PGDUMP_EXTRA_ARGS} 2>"$ERROR_LOG" | gzip -c >"$BACKUP_FILE" + +actual_bytes="$(stat -c%s "$BACKUP_FILE")" +if (( actual_bytes < MIN_BACKUP_BYTES )); then + echo "❌ Backup too small: ${actual_bytes} bytes < ${MIN_BACKUP_BYTES} bytes" >&2 + exit 1 +fi + +gunzip -t "$BACKUP_FILE" +sha256sum "$BACKUP_FILE" | tee "$SHA_FILE" >/dev/null + +if [[ -n "${BACKUP_COPY_DIR:-}" ]]; then + cp -p "$BACKUP_FILE" "$SHA_FILE" "$ERROR_LOG" "$BACKUP_COPY_DIR/" + echo "📦 Copied artifact, checksum and stderr log to ${BACKUP_COPY_DIR}" +fi + +trap - EXIT + +echo "✅ Backup complete" +echo "- file: ${BACKUP_FILE}" +echo "- bytes: ${actual_bytes}" +echo "- sha256: ${SHA_FILE}" +echo "- stderr_log: ${ERROR_LOG}" diff --git a/scripts/validate-openapi.mjs b/scripts/validate-openapi.mjs new file mode 100755 index 000000000..25a226a6c --- /dev/null +++ b/scripts/validate-openapi.mjs @@ -0,0 +1,58 @@ +#!/usr/bin/env node +import { existsSync, readFileSync } from 'node:fs'; + +const specPath = 'docs/api/openapi.json'; +const spec = JSON.parse(readFileSync(specPath, 'utf8')); +const failures = []; +const operationIds = new Set(); +const EDGE_FUNCTIONS_DIR = 'supabase/functions'; + +function fail(message) { + failures.push(message); +} + +if (!spec.openapi?.startsWith('3.')) fail('openapi must be 3.x'); +if (!spec.info?.title) fail('info.title is required'); +if (!spec.info?.version) fail('info.version is required'); +if (!spec.paths || Object.keys(spec.paths).length === 0) fail('paths must not be empty'); +if (!spec.components?.securitySchemes?.bearerAuth) fail('bearerAuth security scheme is required'); +if (!spec.components?.securitySchemes?.hmacSignature) fail('hmacSignature security scheme is required'); + +for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { + if (!path.startsWith('/')) fail(`path must start with /: ${path}`); + const functionName = path.split('/').filter(Boolean)[0]; + if (functionName && !existsSync(`${EDGE_FUNCTIONS_DIR}/${functionName}/index.ts`)) { + fail(`${path} does not map to an Edge Function directory (${EDGE_FUNCTIONS_DIR}/${functionName}/index.ts)`); + } + for (const [method, operation] of Object.entries(pathItem)) { + if (!['get', 'post', 'put', 'patch', 'delete', 'options', 'head'].includes(method)) continue; + const label = `${method.toUpperCase()} ${path}`; + if (!operation.operationId) fail(`${label} missing operationId`); + if (operation.operationId) { + if (operationIds.has(operation.operationId)) fail(`${label} duplicate operationId: ${operation.operationId}`); + operationIds.add(operation.operationId); + } + if (!operation.summary) fail(`${label} missing summary`); + if (!operation.responses || Object.keys(operation.responses).length === 0) fail(`${label} missing responses`); + const successResponse = Object.keys(operation.responses ?? {}).find((status) => /^2\d\d$/.test(status)); + if (!successResponse) fail(`${label} missing 2xx response`); + if (method !== 'get' && !operation.requestBody && !path.includes('webhook')) { + fail(`${label} non-GET operation should declare requestBody`); + } + if (path.includes('webhook') && method === 'post') { + const securityNames = (operation.security ?? []).flatMap((entry) => Object.keys(entry)); + if (!securityNames.includes('hmacSignature')) fail(`${label} webhook POST must require hmacSignature`); + const parameterRefs = (operation.parameters ?? []).map((param) => param.$ref ?? param.name); + if (!parameterRefs.some((ref) => String(ref).includes('RequestIdHeader'))) fail(`${label} webhook POST must document x-request-id`); + if (!parameterRefs.some((ref) => String(ref).includes('IdempotencyKeyHeader'))) fail(`${label} webhook POST must document idempotency header`); + } + } +} + +if (failures.length > 0) { + console.error(`🚨 OpenAPI contract validation failed (${failures.length}):`); + for (const failure of failures) console.error(`- ${failure}`); + process.exit(1); +} + +console.log(`✅ OpenAPI contract valid: ${operationIds.size} operations in ${specPath}.`); diff --git a/scripts/validate-supabase-backup-artifact.mjs b/scripts/validate-supabase-backup-artifact.mjs new file mode 100755 index 000000000..501fe4bca --- /dev/null +++ b/scripts/validate-supabase-backup-artifact.mjs @@ -0,0 +1,42 @@ +#!/usr/bin/env node +import { createHash } from 'node:crypto'; +import { createReadStream, existsSync, statSync } from 'node:fs'; +import { basename } from 'node:path'; +import { spawnSync } from 'node:child_process'; + +const file = process.argv[2]; +const minBytes = Number(process.env.MIN_BACKUP_BYTES ?? 104_857_600); + +function fail(message) { + console.error(`❌ ${message}`); + process.exit(1); +} + +if (!file || ['-h', '--help'].includes(file)) { + console.log(`Usage: node scripts/validate-supabase-backup-artifact.mjs \n\nEnv:\n MIN_BACKUP_BYTES Minimum accepted gzip size. Default: 104857600.`); + process.exit(file ? 0 : 2); +} + +if (!existsSync(file)) fail(`Backup artifact not found: ${file}`); +if (!Number.isFinite(minBytes) || minBytes <= 0) fail('MIN_BACKUP_BYTES must be a positive number.'); + +const { size } = statSync(file); +if (size < minBytes) fail(`${basename(file)} is too small: ${size} bytes < ${minBytes} bytes`); + +const gzip = spawnSync('gunzip', ['-t', file], { encoding: 'utf8' }); +if (gzip.status !== 0) fail(`gzip integrity check failed: ${gzip.stderr || gzip.stdout}`); + +const hash = createHash('sha256'); +await new Promise((resolve, reject) => { + createReadStream(file) + .on('data', (chunk) => hash.update(chunk)) + .on('error', reject) + .on('end', resolve); +}); + +console.log('# Supabase Backup Artifact Validation'); +console.log(''); +console.log(`- ✅ File exists: ${file}`); +console.log(`- ✅ Size: ${size} bytes`); +console.log('- ✅ gzip integrity: ok'); +console.log(`- ✅ sha256: ${hash.digest('hex')}`); diff --git a/scripts/verify_rls_compliance.ts b/scripts/verify_rls_compliance.ts new file mode 100644 index 000000000..1c28abd3e --- /dev/null +++ b/scripts/verify_rls_compliance.ts @@ -0,0 +1,98 @@ +import { readdirSync, readFileSync, statSync } from 'fs'; +import { join, relative } from 'path'; + +type TableKey = `${string}.${string}`; + +const ROOTS = ['supabase/migrations', 'supabase/migrations-from-lovable']; +const IGNORED_SCHEMAS = new Set(['auth', 'extensions', 'graphql', 'graphql_public', 'net', 'pgbouncer', 'pgsodium', 'realtime', 'storage', 'supabase_functions', 'vault']); +const IGNORED_TABLES = new Set([ + // SQL parser guardrails for procedural snippets, not real application tables. + 'public.if', + 'public.for', +]); + +function walk(dir: string, files: string[] = []): string[] { + for (const entry of readdirSync(dir)) { + const path = join(dir, entry); + if (statSync(path).isDirectory()) walk(path, files); + else if (path.endsWith('.sql')) files.push(path); + } + return files; +} + +function stripComments(sql: string): string { + return sql + .replace(/\/\*[\s\S]*?\*\//g, ' ') + .replace(/--.*$/gm, ' '); +} + +function normalizeIdentifier(value: string | undefined): string | undefined { + if (!value) return undefined; + return value.replace(/^"|"$/g, '').toLowerCase(); +} + +function tableKey(schema: string | undefined, table: string | undefined): TableKey | undefined { + const normalizedTable = normalizeIdentifier(table); + if (!normalizedTable) return undefined; + const normalizedSchema = normalizeIdentifier(schema) ?? 'public'; + if (IGNORED_SCHEMAS.has(normalizedSchema)) return undefined; + return `${normalizedSchema}.${normalizedTable}`; +} + +function addMatch(set: Set, schema: string | undefined, table: string | undefined) { + const key = tableKey(schema, table); + if (key && !IGNORED_TABLES.has(key)) set.add(key); +} + +const createdTables = new Set(); +const rlsTables = new Set(); +const policyTables = new Set(); +const sourceByTable = new Map(); + +for (const file of ROOTS.flatMap((root) => walk(root).sort())) { + const sql = stripComments(readFileSync(file, 'utf8')); + const source = relative(process.cwd(), file); + + for (const match of sql.matchAll(/\bcreate\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?:(?:"([^"]+)"|([a-zA-Z_][\w$]*))\s*\.\s*)?(?:"([^"]+)"|([a-zA-Z_][\w$]*))/gi)) { + const schema = match[1] ?? match[2]; + const table = match[3] ?? match[4]; + const key = tableKey(schema, table); + if (key && !IGNORED_TABLES.has(key)) { + createdTables.add(key); + if (!sourceByTable.has(key)) sourceByTable.set(key, source); + } + } + + for (const match of sql.matchAll(/\balter\s+table\s+(?:if\s+exists\s+)?(?:(?:"([^"]+)"|([a-zA-Z_][\w$]*))\s*\.\s*)?(?:"([^"]+)"|([a-zA-Z_][\w$]*))\s+enable\s+row\s+level\s+security/gi)) { + addMatch(rlsTables, match[1] ?? match[2], match[3] ?? match[4]); + } + + for (const match of sql.matchAll(/\bcreate\s+policy\b[\s\S]*?\bon\s+(?:(?:"([^"]+)"|([a-zA-Z_][\w$]*))\s*\.\s*)?(?:"([^"]+)"|([a-zA-Z_][\w$]*))\b[\s\S]*?;/gi)) { + addMatch(policyTables, match[1] ?? match[2], match[3] ?? match[4]); + } +} + +const missingRls = [...createdTables].filter((table) => !rlsTables.has(table)).sort(); +const missingPolicies = [...createdTables].filter((table) => !policyTables.has(table)).sort(); +const nonCompliant = [...new Set([...missingRls, ...missingPolicies])].sort(); + +console.log('# RLS Compliance Report'); +console.log(''); +console.log(`- Tables discovered: ${createdTables.size}`); +console.log(`- Tables with RLS enabled in migrations: ${[...createdTables].filter((table) => rlsTables.has(table)).length}`); +console.log(`- Tables with at least one policy in migrations: ${[...createdTables].filter((table) => policyTables.has(table)).length}`); +console.log(`- Non-compliant tables: ${nonCompliant.length}`); +console.log(''); + +if (nonCompliant.length > 0) { + console.log('| Table | Missing RLS | Missing Policy | First seen |'); + console.log('|---|---:|---:|---|'); + for (const table of nonCompliant) { + console.log(`| ${table} | ${missingRls.includes(table) ? 'yes' : 'no'} | ${missingPolicies.includes(table) ? 'yes' : 'no'} | ${sourceByTable.get(table) ?? 'unknown'} |`); + } + console.log(''); + console.error(`🚨 RLS compliance failed: ${nonCompliant.length} table(s) need RLS and/or policies.`); + process.exit(1); +} + +console.log('✅ All discovered application tables have RLS enabled and at least one policy declared in migrations.'); diff --git a/src/lib/logger.ts b/src/lib/logger.ts index a8ec37f36..6c9b8bcb3 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -1,19 +1,15 @@ // Centralized logging utility with correlation IDs and structured output // Logs are automatically filtered in production builds -import * as Sentry from "@sentry/react"; +import * as Sentry from '@sentry/react'; +import { redactLogArgs, stringifyRedacted } from '@/lib/redaction'; const isDev = import.meta.env.DEV; type LogLevel = 'debug' | 'info' | 'warn' | 'error'; -interface LogContext { - module?: string; - correlationId?: string; - [key: string]: unknown; -} - // Session-level correlation ID for tracing across the app lifetime -const sessionId = crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; +const sessionId = + crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; // Per-request correlation ID generator let requestCounter = 0; @@ -43,7 +39,7 @@ class Logger { category: 'log', message: `${this.module}: ${message}`, level: level === 'error' ? 'error' : level === 'warn' ? 'warning' : 'info', - data: args.length > 0 ? { args: JSON.stringify(args) } : undefined, + data: args.length > 0 ? { args: stringifyRedacted(args) } : undefined, }); } @@ -55,27 +51,35 @@ class Logger { } debug(message: string, ...args: unknown[]): void { - // eslint-disable-next-line no-console - if (this.shouldLog('debug')) console.debug(this.formatMessage('debug', message), ...args); + if (this.shouldLog('debug')) { + // eslint-disable-next-line no-console + console.debug(this.formatMessage('debug', message), ...redactLogArgs(args)); + } this.addToSentryBreadcrumb('debug', message, ...args); } info(message: string, ...args: unknown[]): void { - // eslint-disable-next-line no-console - if (this.shouldLog('info')) console.info(this.formatMessage('info', message), ...args); + if (this.shouldLog('info')) { + // eslint-disable-next-line no-console + console.info(this.formatMessage('info', message), ...redactLogArgs(args)); + } this.addToSentryBreadcrumb('info', message, ...args); } warn(message: string, ...args: unknown[]): void { - if (this.shouldLog('warn')) console.warn(this.formatMessage('warn', message), ...args); + if (this.shouldLog('warn')) + console.warn(this.formatMessage('warn', message), ...redactLogArgs(args)); this.addToSentryBreadcrumb('warn', message, ...args); } error(message: string, ...args: unknown[]): void { - if (this.shouldLog('error')) console.error(this.formatMessage('error', message), ...args); + if (this.shouldLog('error')) + console.error(this.formatMessage('error', message), ...redactLogArgs(args)); this.addToSentryBreadcrumb('error', message, ...args); if (import.meta.env.PROD) { - Sentry.captureException(new Error(`${this.module}: ${message}`), { extra: { args } }); + Sentry.captureException(new Error(`${this.module}: ${message}`), { + extra: { args: redactLogArgs(args) }, + }); } } @@ -115,7 +119,7 @@ export function logPerformance(label: string, fn: () => void): void { fn(); return; } - + const start = performance.now(); fn(); const end = performance.now(); @@ -128,7 +132,7 @@ export async function logAsyncPerformance(label: string, fn: () => Promise if (!isDev) { return fn(); } - + const start = performance.now(); const result = await fn(); const end = performance.now(); diff --git a/src/lib/redaction.test.ts b/src/lib/redaction.test.ts new file mode 100644 index 000000000..5c409045d --- /dev/null +++ b/src/lib/redaction.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import { redactSensitiveData, stringifyRedacted } from './redaction'; + +describe('redaction', () => { + it('redacts sensitive keys recursively', () => { + const result = redactSensitiveData({ + email: 'cliente@example.com', + nested: { + access_token: 'secret-token', + password: '123456', + }, + }); + + expect(result).toEqual({ + email: '[REDACTED]', + nested: { + access_token: '[REDACTED]', + password: '[REDACTED]', + }, + }); + }); + + it('redacts tokens, Brazilian documents, phones and WhatsApp JIDs in strings', () => { + const input = + 'Bearer abc.def.ghi CPF 123.456.789-10 tel +55 11 98765-4321 jid 5511999999999@s.whatsapp.net'; + + expect(redactSensitiveData(input)).toBe( + 'Bearer [REDACTED] CPF [REDACTED] tel [REDACTED] jid [REDACTED]' + ); + }); + + it('does not redact bare operational numbers as Brazilian phones', () => { + expect(redactSensitiveData('ticket 12345678 protocolo 987654321')).toBe( + 'ticket 12345678 protocolo 987654321' + ); + }); + + it('stringifies redacted circular-safe errors', () => { + const error = new Error('token abc@example.com'); + const output = stringifyRedacted({ error }); + + expect(output).toContain('[REDACTED]'); + expect(output).not.toContain('abc@example.com'); + }); +}); diff --git a/src/lib/redaction.ts b/src/lib/redaction.ts new file mode 100644 index 000000000..8d1663dd7 --- /dev/null +++ b/src/lib/redaction.ts @@ -0,0 +1,66 @@ +const REDACTED = '[REDACTED]'; + +const SENSITIVE_KEY_PATTERN = + /(?:password|passwd|pwd|token|secret|api[_-]?key|authorization|cookie|refresh[_-]?token|access[_-]?token|jwt|credential|private[_-]?key)/i; +const EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi; +const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi; +const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g; +const UUID_TOKEN_PATTERN = + /\b(?:[A-F0-9]{8}-[A-F0-9]{4}-[1-5][A-F0-9]{3}-[89AB][A-F0-9]{3}-[A-F0-9]{12})\b/gi; +const BRAZIL_PHONE_PATTERN = /(?): unknown[] { + return value.map((item) => redactSensitiveData(item, seen)); +} + +function redactObject( + value: Record, + seen: WeakSet +): Record { + if (seen.has(value)) return { circular: true }; + seen.add(value); + + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => [ + key, + SENSITIVE_KEY_PATTERN.test(key) ? REDACTED : redactSensitiveData(entry, seen), + ]) + ); +} + +export function redactSensitiveData(value: unknown, seen = new WeakSet()): unknown { + if (typeof value === 'string') return redactString(value); + if (typeof value !== 'object' || value === null) return value; + if (value instanceof Error) { + return { + name: value.name, + message: redactString(value.message), + stack: value.stack ? redactString(value.stack) : undefined, + }; + } + if (value instanceof Date) return value.toISOString(); + if (Array.isArray(value)) return redactArray(value, seen); + return redactObject(value as Record, seen); +} + +export function redactLogArgs(args: unknown[]): unknown[] { + return args.map((arg) => redactSensitiveData(arg)); +} + +export function stringifyRedacted(value: unknown): string { + return JSON.stringify(redactSensitiveData(value)); +} diff --git a/supabase/functions/public-api/index.ts b/supabase/functions/public-api/index.ts index f6d188fcd..76b230cfc 100644 --- a/supabase/functions/public-api/index.ts +++ b/supabase/functions/public-api/index.ts @@ -6,12 +6,61 @@ import { createCriticalPayloadSchemas, mapValidationIssuesToContractError } from const { publicApiSendSchema } = createCriticalPayloadSchemas(z); + +type PublicApiMessageReplay = { + id: string; + contact_id: string | null; + status: string | null; + external_id: string | null; +}; + +const SUCCESSFUL_REPLAY_STATUSES = new Set(['sending', 'sent', 'delivered', 'read']); + +function isSuccessfulReplayStatus(status: string | null): boolean { + return status ? SUCCESSFUL_REPLAY_STATUSES.has(status) : false; +} + +function buildReplayPayload(previousMessage: PublicApiMessageReplay, requestId: string) { + return { + success: isSuccessfulReplayStatus(previousMessage.status), + duplicate: true, + messageId: previousMessage.id, + contactId: previousMessage.contact_id, + status: previousMessage.status, + externalId: previousMessage.external_id, + requestId, + }; +} + +async function findMessageByIdempotencyKey(supabase: ReturnType, idempotencyKey: string) { + const { data } = await supabase + .from('messages') + .select('id, contact_id, status, external_id') + .eq('request_id', idempotencyKey) + .maybeSingle(); + + return data as PublicApiMessageReplay | null; +} + +function isUniqueViolation(error: { code?: string; message?: string } | null): boolean { + return error?.code === '23505' || /duplicate key value violates unique constraint/i.test(error?.message ?? ''); +} + +function getIdempotencyKey(req: Request): string | null { + const raw = req.headers.get('x-idempotency-key') ?? req.headers.get('idempotency-key'); + const key = raw?.trim(); + if (!key) return null; + return key.length >= 8 && key.length <= 200 ? key : null; +} + Deno.serve(async (req) => { const cors = handleCors(req); if (cors) return cors; const log = new Logger("public-api", req); const requestId = log.getRequestId(); + const idempotencyKey = getIdempotencyKey(req); + const traceId = idempotencyKey ?? requestId; const ip = getClientIP(req); const rl = checkRateLimit(`public-api:${ip}`, 60, 60_000); @@ -60,6 +109,15 @@ Deno.serve(async (req) => { const { number, message, connectionId } = parsed.data; const phone = number.replace(/\D/g, ''); + if (idempotencyKey) { + const previousMessage = await findMessageByIdempotencyKey(supabase, idempotencyKey); + + if (previousMessage) { + log.info('Idempotency replay', { idempotencyKey, messageId: previousMessage.id }); + return jsonResponse(buildReplayPayload(previousMessage, requestId), 200, req); + } + } + // Find connection let connection; if (connectionId) { @@ -114,12 +172,20 @@ Deno.serve(async (req) => { message_type: 'text', status: 'sending', whatsapp_connection_id: connection.id, - request_id: requestId, + request_id: traceId, }) .select() .single(); if (msgError) { + if (idempotencyKey && isUniqueViolation(msgError)) { + const previousMessage = await findMessageByIdempotencyKey(supabase, idempotencyKey); + if (previousMessage) { + log.info('Idempotency replay after unique conflict', { idempotencyKey, messageId: previousMessage.id }); + return jsonResponse(buildReplayPayload(previousMessage, requestId), 200, req); + } + } + log.error('Failed to save message', { error: msgError.message }); return errorResponse('Failed to save message', 500, req); } @@ -159,7 +225,7 @@ Deno.serve(async (req) => { await supabase.from('messages').update({ status: 'failed' }).eq('id', msg.id); } - log.done(200, { messageId: msg.id, requestId }); + log.done(200, { messageId: msg.id, requestId, idempotencyKey }); return jsonResponse({ success: true, messageId: msg.id, contactId: contact.id, requestId }, 200, req); } catch (err) { const msg = err instanceof Error ? err.message : 'Internal server error'; diff --git a/supabase/migrations/20260513093000_rls_compliance_gap_closure.sql b/supabase/migrations/20260513093000_rls_compliance_gap_closure.sql new file mode 100644 index 000000000..fd0a42f70 --- /dev/null +++ b/supabase/migrations/20260513093000_rls_compliance_gap_closure.sql @@ -0,0 +1,115 @@ +-- ============================================================================ +-- 20260513093000_rls_compliance_gap_closure.sql +-- Fecha gaps estáticos de RLS detectados por scripts/verify_rls_compliance.ts. +-- Idempotente: usa ALTER TABLE IF EXISTS + DROP/CREATE POLICY. +-- ============================================================================ + +-- 1) Habilita RLS nas tabelas que ainda não tinham enforcement explícito. +ALTER TABLE IF EXISTS public.avatars ENABLE ROW LEVEL SECURITY; +ALTER TABLE IF EXISTS public.channel_connections_safe ENABLE ROW LEVEL SECURITY; +ALTER TABLE IF EXISTS public.conversation_summaries ENABLE ROW LEVEL SECURITY; +ALTER TABLE IF EXISTS public.email_templates ENABLE ROW LEVEL SECURITY; +ALTER TABLE IF EXISTS public.message_queue ENABLE ROW LEVEL SECURITY; +ALTER TABLE IF EXISTS public.messages_whatsapp ENABLE ROW LEVEL SECURITY; +ALTER TABLE IF EXISTS public.salespeople ENABLE ROW LEVEL SECURITY; +ALTER TABLE IF EXISTS public.system_logs ENABLE ROW LEVEL SECURITY; +ALTER TABLE IF EXISTS public.vault_healthcheck_log ENABLE ROW LEVEL SECURITY; + +-- 2) Policies default para tabelas operacionais/sensíveis: leitura admin+supervisor, escrita admin. +DROP POLICY IF EXISTS rls_select_admin_supervisor ON public.channel_connections_safe; +CREATE POLICY rls_select_admin_supervisor ON public.channel_connections_safe FOR SELECT TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); +DROP POLICY IF EXISTS rls_admin_manage ON public.channel_connections_safe; +CREATE POLICY rls_admin_manage ON public.channel_connections_safe FOR ALL TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role)) +WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role)); + +DROP POLICY IF EXISTS rls_select_admin_supervisor ON public.conversation_summaries; +CREATE POLICY rls_select_admin_supervisor ON public.conversation_summaries FOR SELECT TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); +DROP POLICY IF EXISTS rls_admin_manage ON public.conversation_summaries; +CREATE POLICY rls_admin_manage ON public.conversation_summaries FOR ALL TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role)) +WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role)); + +DROP POLICY IF EXISTS rls_select_admin_supervisor ON public.message_queue; +CREATE POLICY rls_select_admin_supervisor ON public.message_queue FOR SELECT TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); +DROP POLICY IF EXISTS rls_admin_manage ON public.message_queue; +CREATE POLICY rls_admin_manage ON public.message_queue FOR ALL TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role)) +WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role)); + +DROP POLICY IF EXISTS rls_select_admin_supervisor ON public.messages_whatsapp; +CREATE POLICY rls_select_admin_supervisor ON public.messages_whatsapp FOR SELECT TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); +DROP POLICY IF EXISTS rls_admin_manage ON public.messages_whatsapp; +CREATE POLICY rls_admin_manage ON public.messages_whatsapp FOR ALL TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role)) +WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role)); + +DROP POLICY IF EXISTS rls_select_admin_supervisor ON public.scheduled_job_log; +CREATE POLICY rls_select_admin_supervisor ON public.scheduled_job_log FOR SELECT TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); +DROP POLICY IF EXISTS rls_admin_manage ON public.scheduled_job_log; +CREATE POLICY rls_admin_manage ON public.scheduled_job_log FOR ALL TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role)) +WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role)); + +DROP POLICY IF EXISTS rls_select_admin_supervisor ON public.system_logs; +CREATE POLICY rls_select_admin_supervisor ON public.system_logs FOR SELECT TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); +DROP POLICY IF EXISTS rls_admin_manage ON public.system_logs; +CREATE POLICY rls_admin_manage ON public.system_logs FOR ALL TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role)) +WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role)); + +DROP POLICY IF EXISTS rls_select_admin_supervisor ON public.vault_healthcheck_log; +CREATE POLICY rls_select_admin_supervisor ON public.vault_healthcheck_log FOR SELECT TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); +DROP POLICY IF EXISTS rls_admin_manage ON public.vault_healthcheck_log; +CREATE POLICY rls_admin_manage ON public.vault_healthcheck_log FOR ALL TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role)) +WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role)); + +-- 3) Avatars: leitura autenticada; usuários gerenciam seus próprios metadados; admins gerenciam tudo. +DROP POLICY IF EXISTS rls_avatars_select_authenticated ON public.avatars; +CREATE POLICY rls_avatars_select_authenticated ON public.avatars FOR SELECT TO authenticated USING (true); +DROP POLICY IF EXISTS rls_avatars_manage_own ON public.avatars; +CREATE POLICY rls_avatars_manage_own ON public.avatars FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()); +DROP POLICY IF EXISTS rls_avatars_admin_manage ON public.avatars; +CREATE POLICY rls_avatars_admin_manage ON public.avatars FOR ALL TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role)) +WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role)); + +-- 4) Templates de e-mail: leitura para autenticados; criador/admin gerenciam. +DROP POLICY IF EXISTS rls_email_templates_select_authenticated ON public.email_templates; +CREATE POLICY rls_email_templates_select_authenticated ON public.email_templates FOR SELECT TO authenticated USING (true); +DROP POLICY IF EXISTS rls_email_templates_manage_creator_or_admin ON public.email_templates; +CREATE POLICY rls_email_templates_manage_creator_or_admin ON public.email_templates FOR ALL TO authenticated +USING (created_by = auth.uid() OR public.has_role(auth.uid(), 'admin'::public.app_role)) +WITH CHECK (created_by = auth.uid() OR public.has_role(auth.uid(), 'admin'::public.app_role)); + +-- 5) Contatos multi-telefone: leitura autenticada; escrita admin/supervisor. +DROP POLICY IF EXISTS rls_contact_phones_select_authenticated ON public.contact_phones; +CREATE POLICY rls_contact_phones_select_authenticated ON public.contact_phones FOR SELECT TO authenticated USING (true); +DROP POLICY IF EXISTS rls_contact_phones_admin_manage ON public.contact_phones; +CREATE POLICY rls_contact_phones_admin_manage ON public.contact_phones FOR ALL TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)) +WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); + +-- 6) IMAP/SMTP: usuário gerencia suas próprias credenciais; admins podem auditar/gerenciar. +DROP POLICY IF EXISTS rls_imap_smtp_accounts_manage_own ON public.imap_smtp_accounts; +CREATE POLICY rls_imap_smtp_accounts_manage_own ON public.imap_smtp_accounts FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()); +DROP POLICY IF EXISTS rls_imap_smtp_accounts_admin_manage ON public.imap_smtp_accounts; +CREATE POLICY rls_imap_smtp_accounts_admin_manage ON public.imap_smtp_accounts FOR ALL TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role)) +WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role)); + +-- 7) Salespeople: leitura autenticada para roteamento/CRM; escrita restrita a admin/supervisor. +DROP POLICY IF EXISTS rls_salespeople_select_authenticated ON public.salespeople; +CREATE POLICY rls_salespeople_select_authenticated ON public.salespeople FOR SELECT TO authenticated USING (true); +DROP POLICY IF EXISTS rls_salespeople_admin_supervisor_manage ON public.salespeople; +CREATE POLICY rls_salespeople_admin_supervisor_manage ON public.salespeople FOR ALL TO authenticated +USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)) +WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); diff --git a/supabase/migrations/20260513113000_pr132_review_hardening.sql b/supabase/migrations/20260513113000_pr132_review_hardening.sql new file mode 100644 index 000000000..099f1cde9 --- /dev/null +++ b/supabase/migrations/20260513113000_pr132_review_hardening.sql @@ -0,0 +1,55 @@ +-- PR #132 review hardening: make public API idempotency enforceable and +-- tighten formerly broad authenticated-read policies introduced for static RLS coverage. + +-- 1) Public API idempotency must be enforced by the database, not only by +-- check-then-insert application logic. Existing plain lookup index is kept for +-- compatibility; this partial unique index prevents concurrent duplicate sends +-- for the same non-empty request_id/idempotency key. +CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_request_id_unique +ON public.messages (request_id) +WHERE request_id IS NOT NULL AND request_id <> ''; + +-- 2) Avatars: users can read/manage their own avatar metadata; supervisors and +-- admins can read all for support/audit; admins can manage all. +DROP POLICY IF EXISTS rls_avatars_select_authenticated ON public.avatars; +DROP POLICY IF EXISTS rls_avatars_select_own_or_staff ON public.avatars; +CREATE POLICY rls_avatars_select_own_or_staff ON public.avatars FOR SELECT TO authenticated +USING ( + user_id = auth.uid() + OR public.has_role(auth.uid(), 'admin'::public.app_role) + OR public.has_role(auth.uid(), 'supervisor'::public.app_role) +); + +-- 3) Email templates: remove global authenticated reads. Creators can read their +-- own templates; admins/supervisors can read all; creator/admin manage writes. +DROP POLICY IF EXISTS rls_email_templates_select_authenticated ON public.email_templates; +DROP POLICY IF EXISTS rls_email_templates_select_creator_or_staff ON public.email_templates; +CREATE POLICY rls_email_templates_select_creator_or_staff ON public.email_templates FOR SELECT TO authenticated +USING ( + created_by = auth.uid() + OR public.has_role(auth.uid(), 'admin'::public.app_role) + OR public.has_role(auth.uid(), 'supervisor'::public.app_role) +); + +-- 4) Contact phone numbers are PII. Without a tenant/ownership column on this +-- legacy table, authenticated-wide reads are too broad; restrict reads/writes to +-- admin/supervisor until a contact ownership model is available for this table. +DROP POLICY IF EXISTS rls_contact_phones_select_authenticated ON public.contact_phones; +DROP POLICY IF EXISTS rls_contact_phones_select_admin_supervisor ON public.contact_phones; +CREATE POLICY rls_contact_phones_select_admin_supervisor ON public.contact_phones FOR SELECT TO authenticated +USING ( + public.has_role(auth.uid(), 'admin'::public.app_role) + OR public.has_role(auth.uid(), 'supervisor'::public.app_role) +); + +-- 5) Salespeople: users can read their own salesperson record; supervisors/admins +-- can read and manage the roster. This removes the previous global authenticated +-- read while preserving operational routing visibility for staff roles. +DROP POLICY IF EXISTS rls_salespeople_select_authenticated ON public.salespeople; +DROP POLICY IF EXISTS rls_salespeople_select_own_or_staff ON public.salespeople; +CREATE POLICY rls_salespeople_select_own_or_staff ON public.salespeople FOR SELECT TO authenticated +USING ( + user_id = auth.uid() + OR public.has_role(auth.uid(), 'admin'::public.app_role) + OR public.has_role(auth.uid(), 'supervisor'::public.app_role) +); diff --git a/tsconfig.strict.core.json b/tsconfig.strict.core.json new file mode 100644 index 000000000..335da9760 --- /dev/null +++ b/tsconfig.strict.core.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true + }, + "include": [ + "src/vite-env.d.ts", + "src/lib/redaction.ts", + "src/lib/logger.ts" + ] +} diff --git a/vite.config.ts b/vite.config.ts index 804020d9b..61144294f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,17 +2,46 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; +const manualChunks = (id: string): string | undefined => { + if (!id.includes("node_modules")) return undefined; + + if (/[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/.test(id)) { + return "vendor-react"; + } + if (id.includes("node_modules/@supabase")) return "vendor-supabase"; + if (id.includes("node_modules/@radix-ui")) return "vendor-radix"; + if (id.includes("node_modules/@tanstack")) return "vendor-tanstack"; + if (id.includes("node_modules/framer-motion")) return "vendor-motion"; + if (id.includes("node_modules/recharts") || id.includes("node_modules/d3-")) return "vendor-charts"; + if (id.includes("node_modules/jspdf") || id.includes("node_modules/xlsx") || id.includes("node_modules/html2canvas")) { + return "vendor-export"; + } + if (id.includes("node_modules/@sentry")) return "vendor-observability"; + + return undefined; +}; + export default defineConfig(({ mode }) => ({ server: { host: "::", port: 8080, }, - plugins: [ - react(), - ].filter(Boolean), + plugins: [react()], resolve: { alias: { "@": path.resolve(__dirname, "./src"), }, }, -})); \ No newline at end of file + build: { + target: "es2020", + sourcemap: mode !== "production" ? true : "hidden", + cssCodeSplit: true, + assetsInlineLimit: 4096, + chunkSizeWarningLimit: 900, + rollupOptions: { + output: { + manualChunks, + }, + }, + }, +}));