From 959736ed262f628f83297f2e69961021d2c04088 Mon Sep 17 00:00:00 2001 From: Godsmiracle001 Date: Sat, 30 May 2026 10:48:55 +0100 Subject: [PATCH] all fixed --- .github/workflows/migrations.yml | 67 + .github/workflows/sdk-docs.yml | 60 + WEBHOOK_VERIFICATION.md | 16 +- backend/.env.example | 2 + backend/.gitignore | 1 + backend/migrations/README.md | 30 + backend/migrations/runner.ts | 60 +- backend/package-lock.json | 1144 ++++++++++++++++- backend/package.json | 10 +- .../20250530120000_init/migration.sql | 541 ++++++++ .../20250530120001_partial_indexes/down.sql | 1 + .../migration.sql | 4 + backend/prisma/migrations/migration_lock.toml | 3 + backend/prisma/schema.prisma | 4 +- backend/src/config/env.ts | 2 + backend/src/index.ts | 60 +- backend/src/logging/context.ts | 24 + backend/src/logging/redact.ts | 33 + backend/src/middleware/logger.test.ts | 4 +- backend/src/middleware/logger.ts | 123 +- backend/src/middleware/webhookVerification.ts | 192 ++- backend/src/routes/webhookHandlers.ts | 81 +- backend/src/routes/webhooks.ts | 14 +- .../webhooks/__tests__/replay.test.ts | 17 + backend/src/services/webhooks/audit.ts | 46 + backend/src/services/webhooks/providers.ts | 200 +++ backend/src/services/webhooks/replay.ts | 31 + backend/src/services/webhooks/verification.ts | 63 +- docker-compose.yml | 26 + docs/logging.md | 33 + docs/sdk/ERROR-HANDLING.md | 43 + docs/sdk/MIGRATION-FROM-REST.md | 44 + infra/logging/grafana-datasources.yml | 8 + packages/sdk/CHANGELOG.md | 14 + packages/sdk/README.md | 29 +- packages/sdk/examples/error-handling.ts | 25 + packages/sdk/examples/getting-started.ts | 24 + packages/sdk/examples/split-payment.ts | 24 + packages/sdk/package.json | 10 +- packages/sdk/typedoc.json | 12 + packages/types/CHANGELOG.md | 8 + packages/types/README.md | 21 + packages/types/package.json | 2 + packages/types/typedoc.json | 8 + scripts/deploy.sh | 37 +- 45 files changed, 2867 insertions(+), 334 deletions(-) create mode 100644 .github/workflows/migrations.yml create mode 100644 .github/workflows/sdk-docs.yml create mode 100644 backend/migrations/README.md create mode 100644 backend/prisma/migrations/20250530120000_init/migration.sql create mode 100644 backend/prisma/migrations/20250530120001_partial_indexes/down.sql create mode 100644 backend/prisma/migrations/20250530120001_partial_indexes/migration.sql create mode 100644 backend/prisma/migrations/migration_lock.toml create mode 100644 backend/src/logging/context.ts create mode 100644 backend/src/logging/redact.ts create mode 100644 backend/src/services/webhooks/__tests__/replay.test.ts create mode 100644 backend/src/services/webhooks/audit.ts create mode 100644 backend/src/services/webhooks/providers.ts create mode 100644 backend/src/services/webhooks/replay.ts create mode 100644 docs/logging.md create mode 100644 docs/sdk/ERROR-HANDLING.md create mode 100644 docs/sdk/MIGRATION-FROM-REST.md create mode 100644 infra/logging/grafana-datasources.yml create mode 100644 packages/sdk/CHANGELOG.md create mode 100644 packages/sdk/examples/error-handling.ts create mode 100644 packages/sdk/examples/getting-started.ts create mode 100644 packages/sdk/examples/split-payment.ts create mode 100644 packages/sdk/typedoc.json create mode 100644 packages/types/CHANGELOG.md create mode 100644 packages/types/README.md create mode 100644 packages/types/typedoc.json diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml new file mode 100644 index 00000000..198703f8 --- /dev/null +++ b/.github/workflows/migrations.yml @@ -0,0 +1,67 @@ +name: Database Migrations + +on: + pull_request: + paths: + - 'backend/prisma/**' + - 'backend/migrations/**' + - '.github/workflows/migrations.yml' + push: + branches: [main, dev] + paths: + - 'backend/prisma/**' + - 'backend/migrations/**' + +concurrency: + group: migrations-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate-migrations: + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: backend + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: agenticpay + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres -d agenticpay" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/agenticpay + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: backend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma client + run: npm run db:generate + + - name: Apply migrations + run: npm run db:migrate + + - name: Check for migration conflicts / schema drift + run: npx tsx migrations/runner.ts check + + - name: Migration status + run: npm run db:migrate:status diff --git a/.github/workflows/sdk-docs.yml b/.github/workflows/sdk-docs.yml new file mode 100644 index 00000000..78965d23 --- /dev/null +++ b/.github/workflows/sdk-docs.yml @@ -0,0 +1,60 @@ +name: SDK Documentation + +on: + pull_request: + paths: + - 'packages/sdk/**' + - 'packages/types/**' + - '.github/workflows/sdk-docs.yml' + push: + branches: [main, dev] + paths: + - 'packages/sdk/**' + - 'packages/types/**' + +jobs: + typedoc: + runs-on: ubuntu-latest + strategy: + matrix: + package: [sdk, types] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: packages/${{ matrix.package }}/package-lock.json + + - name: Install + working-directory: packages/${{ matrix.package }} + run: npm ci 2>/dev/null || npm install + + - name: Build + working-directory: packages/${{ matrix.package }} + run: npm run build + + - name: Generate TypeDoc + working-directory: packages/${{ matrix.package }} + run: npm run docs + + - name: Upload docs artifact + uses: actions/upload-artifact@v4 + with: + name: typedoc-${{ matrix.package }} + path: packages/${{ matrix.package }}/docs/api + + sdk-examples: + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/sdk + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm install + - run: npm run build + - name: Compile examples + run: npx tsc --noEmit examples/*.ts --module nodenext --moduleResolution nodenext --target ES2022 diff --git a/WEBHOOK_VERIFICATION.md b/WEBHOOK_VERIFICATION.md index 0890f674..64523a7e 100644 --- a/WEBHOOK_VERIFICATION.md +++ b/WEBHOOK_VERIFICATION.md @@ -4,14 +4,14 @@ Implement webhook signature verification for all incoming webhooks to prevent spoofed callbacks from payment providers, webhooks, and other services. ## Acceptance Criteria -- [ ] HMAC-SHA256 verification -- [ ] Per-provider secrets -- [ ] Timestamp verification (replay protection) -- [ ] Signature validation on all incoming -- [ ] Failed webhook queuing -- [ ] Manual retry -- [ ] Secret rotation -- [ ] Verification logs +- [x] HMAC-SHA256 verification +- [x] Per-provider secrets (with optional `keyId` for rotation) +- [x] Timestamp verification (replay protection) + event-id dedup +- [x] Signature validation on all incoming (`/webhooks/*`, Stripe SDK on `/webhooks/stripe`) +- [x] Failed webhook queuing +- [x] Manual retry +- [x] Secret rotation +- [x] Verification logs (structured Pino + `/api/v1/webhooks/audit`) ## Technical Scope - Files: backend/webhooks/verification diff --git a/backend/.env.example b/backend/.env.example index 380ce45d..e41e3b3f 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,5 +1,7 @@ NODE_ENV=development PORT=3001 +LOG_LEVEL=info +LOG_LEVELS=webhooks:debug CORS_ALLOWED_ORIGINS=http://localhost:3000 JOBS_ENABLED=true QUEUE_ENABLED=true diff --git a/backend/.gitignore b/backend/.gitignore index 2a155521..5fdeb9c3 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -2,4 +2,5 @@ node_modules/ dist/ .env *.log +.migration-state.json backend/benchmarks/results.json diff --git a/backend/migrations/README.md b/backend/migrations/README.md new file mode 100644 index 00000000..10a22d9a --- /dev/null +++ b/backend/migrations/README.md @@ -0,0 +1,30 @@ +# Database Migrations (#410) + +Prisma Migrate with versioned SQL under `prisma/migrations/`. + +## Commands + +| Command | Description | +|---------|-------------| +| `npm run db:migrate` | Apply pending migrations (`prisma migrate deploy`) | +| `npm run db:migrate:status` | Show migration status | +| `npm run db:migrate:check` | CI — detect schema drift / conflicts | +| `npm run db:rollback` | Dev — reset DB and re-apply (destructive) | +| `npm run db:rollback:one` | Dev — run `down.sql` for latest migration | +| `npm run db:seed` | Seed development data | + +## Creating migrations + +```bash +npx tsx migrations/runner.ts create-migration add_feature_name +``` + +Review generated SQL, add optional `down.sql` for reversible rollbacks. + +## Deployment + +`scripts/deploy.sh` runs `db:generate` + `db:migrate` before starting the backend (unless `--skip-migrations`). + +## CI + +`.github/workflows/migrations.yml` applies migrations against Postgres and runs `db:migrate:check`. diff --git a/backend/migrations/runner.ts b/backend/migrations/runner.ts index ac94e496..5377dc40 100644 --- a/backend/migrations/runner.ts +++ b/backend/migrations/runner.ts @@ -119,6 +119,62 @@ const commands: Record void> = { run('npx', ['tsx', 'migrations/seed.ts']); console.log('[migrate] ✅ Seed complete.'); }, + + /** CI: fail if schema drift or pending migrations would conflict */ + check() { + console.log('[migrate] Validating migration history vs schema…'); + npx(['prisma', 'migrate', 'status']); + npx([ + 'prisma', + 'migrate', + 'diff', + '--from-migrations', + 'prisma/migrations', + '--to-schema-datamodel', + 'prisma/schema.prisma', + '--exit-code', + ]); + console.log('[migrate] ✅ No migration conflicts detected.'); + }, + + /** Dev: roll back one migration using down.sql when present */ + 'rollback-one'() { + if (process.env.NODE_ENV === 'production') { + console.error('rollback-one is blocked in production.'); + process.exit(1); + } + const migrations = getAvailableMigrations(); + const latest = migrations[migrations.length - 1]; + if (!latest) { + console.error('No migrations to roll back.'); + process.exit(1); + } + const downPath = join(MIGRATIONS_DIR, latest, 'down.sql'); + if (!existsSync(downPath)) { + console.error(`No down.sql for ${latest}. Use rollback (reset) or add down.sql.`); + process.exit(1); + } + console.log(`[migrate] Applying down migration: ${latest}`); + const sql = readFileSync(downPath, 'utf-8'); + if (!process.env.DATABASE_URL) { + console.error('DATABASE_URL required for rollback-one'); + process.exit(1); + } + const result = spawnSync('npx', ['prisma', 'db', 'execute', '--stdin'], { + cwd: ROOT, + input: sql, + stdio: ['pipe', 'inherit', 'inherit'], + shell: false, + }); + if (result.status !== 0) { + throw new Error('down.sql execution failed'); + } + const state = readState(); + state.migrations = migrations.slice(0, -1); + state.appliedAt = new Date().toISOString(); + writeState(state); + console.log('[migrate] ✅ rollback-one complete.'); + }, }; const command = process.argv[2]; @@ -131,7 +187,9 @@ Usage: npx tsx migrations/runner.ts Commands: deploy Apply all pending migrations (safe for CI/CD) status Show current migration status - rollback Roll back to previous migration (dev only) + rollback Roll back to previous migration (dev only, destructive reset) + rollback-one Apply down.sql for latest migration (dev only) + check CI validation — detect schema/migration drift reset Reset database and re-run all migrations (dev only) generate Regenerate Prisma client from schema create-migration Create a new migration: create-migration diff --git a/backend/package-lock.json b/backend/package-lock.json index 7391d3ad..9550f622 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,7 @@ "@sentry/node": "^10.50.0", "@sentry/profiling-node": "^10.50.0", "@stellar/stellar-sdk": "^12.0.0", + "bullmq": "^5.77.6", "circomlib": "^2.0.5", "circomlibjs": "^0.1.7", "compression": "^1.7.4", @@ -24,8 +25,11 @@ "express": "^4.21.0", "express-rate-limit": "^7.4.1", "helmet": "^7.1.0", + "ioredis": "^5.11.0", "node-cron": "^3.0.3", "openai": "^4.67.0", + "pino": "^9.6.0", + "pino-http": "^10.4.0", "qrcode": "^1.5.3", "sanitize-html": "^2.11.0", "snarkjs": "^0.7.6", @@ -56,7 +60,11 @@ "@types/validator": "^13.11.6", "@types/web-push": "^3.6.4", "@types/ws": "^8.5.12", + "autocannon": "^7.15.0", "eslint": "^9.0.0", + "openapi-fetch": "^0.13.5", + "openapi-typescript": "^7.4.4", + "pino-pretty": "^13.0.0", "prisma": "^5.22.0", "tsx": "^4.19.0", "typescript": "^5.6.0", @@ -70,6 +78,49 @@ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, + "node_modules/@assemblyscript/loader": { + "version": "0.19.23", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.19.23.tgz", + "integrity": "sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@emnapi/core": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", @@ -1506,12 +1557,96 @@ "web-worker": "1.2.0" } }, + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "license": "MIT" + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", @@ -1671,6 +1806,12 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@prisma/client": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", @@ -1739,6 +1880,82 @@ "@prisma/debug": "5.22.0" } }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.15", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.15.tgz", + "integrity": "sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.1.1", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.11", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz", @@ -2963,6 +3180,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3033,6 +3260,50 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/autocannon": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/autocannon/-/autocannon-7.15.0.tgz", + "integrity": "sha512-NaP2rQyA+tcubOJMFv2+oeW9jv2pq/t+LM6BL3cfJic0HEfscEcnWgAyU5YovE/oTHUzAgTliGdLPR+RQAWUbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "char-spinner": "^1.0.1", + "cli-table3": "^0.6.0", + "color-support": "^1.1.1", + "cross-argv": "^2.0.0", + "form-data": "^4.0.0", + "has-async-hooks": "^1.0.0", + "hdr-histogram-js": "^3.0.0", + "hdr-histogram-percentiles-obj": "^3.0.0", + "http-parser-js": "^0.5.2", + "hyperid": "^3.0.0", + "lodash.chunk": "^4.2.0", + "lodash.clonedeep": "^4.5.0", + "lodash.flatten": "^4.4.0", + "manage-path": "^2.0.0", + "on-net-listen": "^1.1.1", + "pretty-bytes": "^5.4.1", + "progress": "^2.0.3", + "reinterval": "^1.1.0", + "retimer": "^3.0.0", + "semver": "^7.3.2", + "subarg": "^1.0.0", + "timestring": "^6.0.0" + }, + "bin": { + "autocannon": "autocannon.js" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3316,6 +3587,61 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/bullmq": { + "version": "5.77.6", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.77.6.tgz", + "integrity": "sha512-WCpSoCD4vWyRD+btOsFrO7iBGInrTgG155gTZCV8qY0Yex2KtsbVtFERx6V1WZ2xWl/5ZxnLar8Z8ufnS4f5jg==", + "license": "MIT", + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.10.1", + "msgpackr": "2.0.1", + "node-abort-controller": "3.1.1", + "semver": "7.8.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "redis": ">=5.0.0" + }, + "peerDependenciesMeta": { + "redis": { + "optional": true + } + } + }, + "node_modules/bullmq/node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/bullmq/node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3417,6 +3743,20 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/char-spinner": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/char-spinner/-/char-spinner-1.0.1.tgz", + "integrity": "sha512-acv43vqJ0+N0rD+Uw3pDHSxP30FHrywu2NO6/wBaHChJIizpDeBUd6NjqhNhy9LGaEAhZAXn46QzmlAvIWd16g==", + "dev": true, + "license": "ISC" + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -3518,6 +3858,22 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -3529,6 +3885,15 @@ "wrap-ansi": "^6.2.0" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3547,6 +3912,23 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3696,6 +4078,13 @@ "node": ">=12.0.0" } }, + "node_modules/cross-argv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cross-argv/-/cross-argv-2.0.0.tgz", + "integrity": "sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3717,6 +4106,16 @@ "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", "license": "MIT" }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3785,6 +4184,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3980,6 +4388,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -4529,6 +4947,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/fast-copy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz", + "integrity": "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4550,6 +4975,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fastfile": { "version": "0.0.20", "resolved": "https://registry.npmjs.org/fastfile/-/fastfile-0.0.20.tgz", @@ -4791,6 +5223,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4907,6 +5340,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-async-hooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-async-hooks/-/has-async-hooks-1.0.0.tgz", + "integrity": "sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4978,6 +5418,28 @@ "node": ">= 0.4" } }, + "node_modules/hdr-histogram-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-3.0.1.tgz", + "integrity": "sha512-l3GSdZL1Jr1C0kyb461tUjEdrRPZr8Qry7jByltf5JGrA0xvqOSrxRBfcrJqqV/AMEtqqhHhC6w8HW0gn76tRQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@assemblyscript/loader": "^0.19.21", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true, + "license": "MIT" + }, "node_modules/helmet": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", @@ -4987,6 +5449,13 @@ "node": ">=16.0.0" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -5055,6 +5524,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -5077,10 +5553,47 @@ "ms": "^2.0.0" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "node_modules/hyperid": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.3.0.tgz", + "integrity": "sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "uuid": "^8.3.2", + "uuid-parse": "^1.1.0" + } + }, + "node_modules/hyperid/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -5161,12 +5674,47 @@ "node": ">=0.8.19" } }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.0.tgz", + "integrity": "sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.1.0", + "redis-errors": "1.2.0", + "redis-parser": "3.0.0", + "standard-as-callback": "2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -5289,12 +5837,39 @@ "node": ">=10" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", "license": "MIT" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -5650,6 +6225,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.chunk": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz", + "integrity": "sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5680,6 +6288,13 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/manage-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/manage-path/-/manage-path-2.0.0.tgz", + "integrity": "sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A==", + "dev": true, + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5795,6 +6410,37 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msgpackr": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.1.tgz", + "integrity": "sha512-9J+tqTEsbHqY8YohazYgty7LgerFIWxvMLpUjqETSmjHojtJm2WnX2kK/2a1fLI7CO7ERP1YSEUXMucz4j+yBA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, "node_modules/nanoassert": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz", @@ -5846,6 +6492,12 @@ "node": ">=10" } }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, "node_modules/node-addon-api": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", @@ -5914,6 +6566,21 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5945,6 +6612,15 @@ "https://opencollective.com/debug" ] }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -5966,6 +6642,26 @@ "node": ">= 0.8" } }, + "node_modules/on-net-listen": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/on-net-listen/-/on-net-listen-1.1.2.tgz", + "integrity": "sha512-y1HRYy8s/RlcBvDUwKXSmkODMdx4KSuIvloCnQYJ2LdBBC1asY4HtfhXwe3UWknLakATZDnbzht2Ijw3M1EqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=9.4.0 || ^8.9.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/openai": { "version": "4.104.0", "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", @@ -6011,6 +6707,67 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/openapi-fetch": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.13.8.tgz", + "integrity": "sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.0.15" + } + }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz", + "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/openapi-typescript/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6100,6 +6857,13 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6113,6 +6877,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-srcset": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", @@ -6177,6 +6959,120 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-http": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.5.0.tgz", + "integrity": "sha512-hD91XjgaKkSsdn8P7LaebrNzhGTdB086W3pyPihX0EzGPjq5uBJBXo4N5guqNaK6mUjg9aubMF7wDViYek9dRA==", + "license": "MIT", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^9.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pino-pretty/node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -6232,6 +7128,19 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prisma": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", @@ -6252,6 +7161,32 @@ "fsevents": "2.3.3" } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -6271,6 +7206,17 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6313,6 +7259,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/r1csfile": { "version": "0.0.48", "resolved": "https://registry.npmjs.org/r1csfile/-/r1csfile-0.0.48.tgz", @@ -6383,6 +7335,43 @@ "node": ">= 6" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==", + "dev": true, + "license": "MIT" + }, "node_modules/require-addon": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", @@ -6405,6 +7394,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-in-the-middle": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", @@ -6444,6 +7443,13 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/retimer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/retimer/-/retimer-3.0.0.tgz", + "integrity": "sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==", + "dev": true, + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.0.0-rc.11", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz", @@ -6497,6 +7503,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -6523,10 +7538,27 @@ "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", "license": "MIT" }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6781,6 +7813,15 @@ "require-addon": "^1.1.0" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6817,6 +7858,15 @@ "integrity": "sha512-EGHIRiegFa62/SsA1J+Xs2tIzludPdzM064N9wjbiEgHnGnJ1V0WEpA4pEwCYT5nDvZk3ubf0shqaCS7k6xeUQ==", "license": "MIT" }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -6833,6 +7883,12 @@ "dev": true, "license": "MIT" }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/static-eval": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.1.tgz", @@ -6918,6 +7974,16 @@ "node": ">=12.*" } }, + "node_modules/subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.1.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6931,6 +7997,25 @@ "node": ">=8" } }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/timestring": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/timestring/-/timestring-6.0.0.tgz", + "integrity": "sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -7030,9 +8115,7 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "optional": true + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tsx": { "version": "4.21.0", @@ -7073,6 +8156,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -7169,6 +8265,13 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, "node_modules/urijs": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", @@ -7198,6 +8301,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uuid-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", + "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==", + "dev": true, + "license": "MIT" + }, "node_modules/validator": { "version": "13.15.35", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", @@ -7553,6 +8663,13 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ws": { "version": "8.20.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", @@ -7596,6 +8713,13 @@ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "license": "ISC" }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", diff --git a/backend/package.json b/backend/package.json index 2fd1c572..26025100 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,8 @@ "db:migrate": "npx tsx migrations/runner.ts deploy", "db:migrate:status": "npx tsx migrations/runner.ts status", "db:rollback": "npx tsx migrations/runner.ts rollback", + "db:rollback:one": "npx tsx migrations/runner.ts rollback-one", + "db:migrate:check": "npx tsx migrations/runner.ts check", "db:reset": "npx tsx migrations/runner.ts reset", "db:seed": "npx tsx migrations/runner.ts seed", "db:studio": "prisma studio", @@ -44,6 +46,8 @@ "ioredis": "^5.11.0", "node-cron": "^3.0.3", "openai": "^4.67.0", + "pino": "^9.6.0", + "pino-http": "^10.4.0", "qrcode": "^1.5.3", "sanitize-html": "^2.11.0", "snarkjs": "^0.7.6", @@ -57,6 +61,9 @@ "xss": "^1.0.14", "zod": "^3.23.8" }, + "prisma": { + "seed": "tsx migrations/seed.ts" + }, "devDependencies": { "@eslint/js": "^9.0.0", "@types/circomlibjs": "^0.1.6", @@ -82,6 +89,7 @@ "vitest": "^4.1.1", "autocannon": "^7.15.0", "openapi-fetch": "^0.13.5", - "openapi-typescript": "^7.4.4" + "openapi-typescript": "^7.4.4", + "pino-pretty": "^13.0.0" } } diff --git a/backend/prisma/migrations/20250530120000_init/migration.sql b/backend/prisma/migrations/20250530120000_init/migration.sql new file mode 100644 index 00000000..c433ea01 --- /dev/null +++ b/backend/prisma/migrations/20250530120000_init/migration.sql @@ -0,0 +1,541 @@ +-- CreateEnum +CREATE TYPE "UserTier" AS ENUM ('free', 'pro', 'enterprise'); + +-- CreateEnum +CREATE TYPE "PaymentStatus" AS ENUM ('pending', 'processing', 'completed', 'failed', 'cancelled', 'refunded'); + +-- CreateEnum +CREATE TYPE "PaymentType" AS ENUM ('milestone_payment', 'full_payment', 'refund'); + +-- CreateEnum +CREATE TYPE "ProjectStatus" AS ENUM ('draft', 'active', 'completed', 'cancelled', 'disputed'); + +-- CreateEnum +CREATE TYPE "MilestoneStatus" AS ENUM ('pending', 'in_progress', 'submitted', 'approved', 'rejected', 'completed'); + +-- CreateEnum +CREATE TYPE "InvoiceStatus" AS ENUM ('draft', 'sent', 'paid', 'overdue', 'cancelled'); + +-- CreateEnum +CREATE TYPE "WebhookStatus" AS ENUM ('active', 'disabled', 'failed'); + +-- CreateEnum +CREATE TYPE "PaymentLinkStatus" AS ENUM ('active', 'expired', 'used', 'disabled'); + +-- CreateEnum +CREATE TYPE "EmailCategory" AS ENUM ('payment_receipt', 'payment_confirmation', 'refund_notification', 'dispute_update', 'weekly_summary', 'marketing', 'security_alert', 'onboarding'); + +-- CreateEnum +CREATE TYPE "EmailStatus" AS ENUM ('pending', 'queued', 'sent', 'delivered', 'opened', 'clicked', 'bounced', 'failed'); + +-- CreateEnum +CREATE TYPE "DeliveryProvider" AS ENUM ('smtp', 'sendgrid'); + +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "tier" "UserTier" NOT NULL DEFAULT 'free', + "wallet_address" TEXT, + "timezone" TEXT NOT NULL DEFAULT 'UTC', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "payments" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "tx_hash" TEXT, + "amount" DECIMAL(20,8) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'XLM', + "network" TEXT NOT NULL DEFAULT 'stellar', + "status" "PaymentStatus" NOT NULL DEFAULT 'pending', + "type" "PaymentType" NOT NULL DEFAULT 'milestone_payment', + "project_title" TEXT, + "project_id" TEXT, + "milestone_id" TEXT, + "user_id" TEXT, + "from_address" TEXT, + "to_address" TEXT, + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "payments_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "projects" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "status" "ProjectStatus" NOT NULL DEFAULT 'active', + "total_amount" DECIMAL(20,8) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'XLM', + "client_address" TEXT NOT NULL, + "freelancer_address" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "projects_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "milestones" ( + "id" TEXT NOT NULL, + "project_id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "amount" DECIMAL(20,8) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'XLM', + "status" "MilestoneStatus" NOT NULL DEFAULT 'pending', + "order" INTEGER NOT NULL DEFAULT 0, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "milestones_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "invoices" ( + "id" TEXT NOT NULL, + "project_id" TEXT NOT NULL, + "milestone_id" TEXT, + "tenant_id" TEXT NOT NULL, + "amount" DECIMAL(20,8) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'XLM', + "status" "InvoiceStatus" NOT NULL DEFAULT 'draft', + "generated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "due_at" TIMESTAMP(3), + "paid_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "invoices_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "webhooks" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "user_id" TEXT, + "url" TEXT NOT NULL, + "events" TEXT[], + "secret" TEXT NOT NULL, + "status" "WebhookStatus" NOT NULL DEFAULT 'active', + "fail_count" INTEGER NOT NULL DEFAULT 0, + "last_fired" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "webhooks_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "payment_links" ( + "id" TEXT NOT NULL, + "merchant_id" TEXT NOT NULL, + "amount" DECIMAL(20,8), + "currency" TEXT NOT NULL DEFAULT 'XLM', + "description" TEXT, + "status" "PaymentLinkStatus" NOT NULL DEFAULT 'active', + "expires_at" TIMESTAMP(3), + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "payment_links_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "audit_logs" ( + "id" TEXT NOT NULL, + "entity_id" TEXT NOT NULL, + "entity_type" TEXT NOT NULL, + "action" TEXT NOT NULL, + "user_id" TEXT, + "metadata" JSONB, + "ip_address" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "gas_estimates" ( + "id" TEXT NOT NULL, + "network" TEXT NOT NULL, + "gas_price_gwei" DECIMAL(30,9) NOT NULL, + "base_fee_gwei" DECIMAL(30,9), + "priority_fee_gwei" DECIMAL(30,9), + "recorded_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "gas_estimates_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "sandbox_accounts" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "user_id" TEXT, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "wallet_address" TEXT NOT NULL, + "fake_balance" DECIMAL(20,8) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'XLM', + "is_active" BOOLEAN NOT NULL DEFAULT true, + "expires_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "sandbox_accounts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "sandbox_transactions" ( + "id" TEXT NOT NULL, + "account_id" TEXT NOT NULL, + "tx_hash" TEXT NOT NULL, + "amount" DECIMAL(20,8) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'XLM', + "from_address" TEXT NOT NULL, + "to_address" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "type" TEXT NOT NULL DEFAULT 'payment', + "mock_data" JSONB, + "confirmed_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "sandbox_transactions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "sandbox_migrations" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "source_account_id" TEXT NOT NULL, + "target_account_id" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "steps" JSONB, + "error" TEXT, + "started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "completed_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "sandbox_migrations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "email_templates" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "category" "EmailCategory" NOT NULL, + "subject" TEXT NOT NULL, + "html_body" TEXT NOT NULL, + "text_body" TEXT, + "variables" TEXT[], + "is_active" BOOLEAN NOT NULL DEFAULT true, + "locale" TEXT NOT NULL DEFAULT 'en', + "version" INTEGER NOT NULL DEFAULT 1, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "email_templates_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "email_template_localizations" ( + "id" TEXT NOT NULL, + "template_id" TEXT NOT NULL, + "locale" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "html_body" TEXT NOT NULL, + "text_body" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "email_template_localizations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "email_deliveries" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "template_id" TEXT, + "recipient_email" TEXT NOT NULL, + "recipient_name" TEXT, + "subject" TEXT NOT NULL, + "html_body" TEXT NOT NULL, + "text_body" TEXT, + "status" "EmailStatus" NOT NULL DEFAULT 'pending', + "provider" "DeliveryProvider" NOT NULL DEFAULT 'smtp', + "provider_id" TEXT, + "sent_at" TIMESTAMP(3), + "delivered_at" TIMESTAMP(3), + "opened_at" TIMESTAMP(3), + "clicked_at" TIMESTAMP(3), + "bounced_at" TIMESTAMP(3), + "bounce_reason" TEXT, + "retry_count" INTEGER NOT NULL DEFAULT 0, + "error" TEXT, + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "email_deliveries_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "email_preferences" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "user_id" TEXT, + "email" TEXT NOT NULL, + "payment_receipts" BOOLEAN NOT NULL DEFAULT true, + "payment_confirmations" BOOLEAN NOT NULL DEFAULT true, + "refund_notifications" BOOLEAN NOT NULL DEFAULT true, + "dispute_updates" BOOLEAN NOT NULL DEFAULT true, + "weekly_summaries" BOOLEAN NOT NULL DEFAULT true, + "marketing" BOOLEAN NOT NULL DEFAULT false, + "security_alerts" BOOLEAN NOT NULL DEFAULT true, + "onboarding" BOOLEAN NOT NULL DEFAULT true, + "locale" TEXT NOT NULL DEFAULT 'en', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "email_preferences_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "email_analytics" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "template_id" TEXT, + "category" "EmailCategory" NOT NULL, + "sent_count" INTEGER NOT NULL DEFAULT 0, + "delivered_count" INTEGER NOT NULL DEFAULT 0, + "opened_count" INTEGER NOT NULL DEFAULT 0, + "clicked_count" INTEGER NOT NULL DEFAULT 0, + "bounced_count" INTEGER NOT NULL DEFAULT 0, + "failed_count" INTEGER NOT NULL DEFAULT 0, + "date" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "email_analytics_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "users_tenant_id_email_idx" ON "users"("tenant_id", "email"); + +-- CreateIndex +CREATE INDEX "users_wallet_address_idx" ON "users"("wallet_address"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_tenant_id_email_key" ON "users"("tenant_id", "email"); + +-- CreateIndex +CREATE UNIQUE INDEX "payments_tx_hash_key" ON "payments"("tx_hash"); + +-- CreateIndex +CREATE INDEX "payments_tenant_id_created_at_idx" ON "payments"("tenant_id", "created_at"); + +-- CreateIndex +CREATE INDEX "payments_active_status_idx" ON "payments"("status"); + +-- CreateIndex +CREATE INDEX "payments_tx_hash_idx" ON "payments"("tx_hash"); + +-- CreateIndex +CREATE INDEX "payments_project_id_idx" ON "payments"("project_id"); + +-- CreateIndex +CREATE INDEX "projects_tenant_id_created_at_idx" ON "projects"("tenant_id", "created_at"); + +-- CreateIndex +CREATE INDEX "projects_status_idx" ON "projects"("status"); + +-- CreateIndex +CREATE INDEX "milestones_project_id_idx" ON "milestones"("project_id"); + +-- CreateIndex +CREATE INDEX "invoices_tenant_id_generated_at_idx" ON "invoices"("tenant_id", "generated_at"); + +-- CreateIndex +CREATE INDEX "invoices_project_id_idx" ON "invoices"("project_id"); + +-- CreateIndex +CREATE INDEX "invoices_status_idx" ON "invoices"("status"); + +-- CreateIndex +CREATE INDEX "webhooks_tenant_id_idx" ON "webhooks"("tenant_id"); + +-- CreateIndex +CREATE INDEX "webhooks_status_idx" ON "webhooks"("status"); + +-- CreateIndex +CREATE INDEX "payment_links_merchant_id_idx" ON "payment_links"("merchant_id"); + +-- CreateIndex +CREATE INDEX "payment_links_status_idx" ON "payment_links"("status"); + +-- CreateIndex +CREATE INDEX "audit_logs_entity_id_created_at_idx" ON "audit_logs"("entity_id", "created_at"); + +-- CreateIndex +CREATE INDEX "audit_logs_user_id_idx" ON "audit_logs"("user_id"); + +-- CreateIndex +CREATE INDEX "audit_logs_entity_type_action_idx" ON "audit_logs"("entity_type", "action"); + +-- CreateIndex +CREATE UNIQUE INDEX "gas_estimates_network_key" ON "gas_estimates"("network"); + +-- CreateIndex +CREATE INDEX "gas_estimates_network_recorded_at_idx" ON "gas_estimates"("network", "recorded_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "sandbox_accounts_wallet_address_key" ON "sandbox_accounts"("wallet_address"); + +-- CreateIndex +CREATE INDEX "sandbox_accounts_tenant_id_idx" ON "sandbox_accounts"("tenant_id"); + +-- CreateIndex +CREATE INDEX "sandbox_accounts_wallet_address_idx" ON "sandbox_accounts"("wallet_address"); + +-- CreateIndex +CREATE INDEX "sandbox_accounts_is_active_idx" ON "sandbox_accounts"("is_active"); + +-- CreateIndex +CREATE UNIQUE INDEX "sandbox_accounts_tenant_id_email_key" ON "sandbox_accounts"("tenant_id", "email"); + +-- CreateIndex +CREATE UNIQUE INDEX "sandbox_transactions_tx_hash_key" ON "sandbox_transactions"("tx_hash"); + +-- CreateIndex +CREATE INDEX "sandbox_transactions_account_id_idx" ON "sandbox_transactions"("account_id"); + +-- CreateIndex +CREATE INDEX "sandbox_transactions_tx_hash_idx" ON "sandbox_transactions"("tx_hash"); + +-- CreateIndex +CREATE INDEX "sandbox_transactions_status_idx" ON "sandbox_transactions"("status"); + +-- CreateIndex +CREATE INDEX "sandbox_transactions_created_at_idx" ON "sandbox_transactions"("created_at"); + +-- CreateIndex +CREATE INDEX "sandbox_migrations_tenant_id_idx" ON "sandbox_migrations"("tenant_id"); + +-- CreateIndex +CREATE INDEX "sandbox_migrations_status_idx" ON "sandbox_migrations"("status"); + +-- CreateIndex +CREATE INDEX "email_templates_tenant_id_idx" ON "email_templates"("tenant_id"); + +-- CreateIndex +CREATE INDEX "email_templates_category_idx" ON "email_templates"("category"); + +-- CreateIndex +CREATE INDEX "email_templates_locale_idx" ON "email_templates"("locale"); + +-- CreateIndex +CREATE INDEX "email_templates_is_active_idx" ON "email_templates"("is_active"); + +-- CreateIndex +CREATE UNIQUE INDEX "email_templates_tenant_id_name_locale_key" ON "email_templates"("tenant_id", "name", "locale"); + +-- CreateIndex +CREATE INDEX "email_template_localizations_template_id_idx" ON "email_template_localizations"("template_id"); + +-- CreateIndex +CREATE INDEX "email_template_localizations_locale_idx" ON "email_template_localizations"("locale"); + +-- CreateIndex +CREATE UNIQUE INDEX "email_template_localizations_template_id_locale_key" ON "email_template_localizations"("template_id", "locale"); + +-- CreateIndex +CREATE INDEX "email_deliveries_tenant_id_idx" ON "email_deliveries"("tenant_id"); + +-- CreateIndex +CREATE INDEX "email_deliveries_recipient_email_idx" ON "email_deliveries"("recipient_email"); + +-- CreateIndex +CREATE INDEX "email_deliveries_status_idx" ON "email_deliveries"("status"); + +-- CreateIndex +CREATE INDEX "email_deliveries_template_id_idx" ON "email_deliveries"("template_id"); + +-- CreateIndex +CREATE INDEX "email_deliveries_sent_at_idx" ON "email_deliveries"("sent_at"); + +-- CreateIndex +CREATE INDEX "email_deliveries_provider_id_idx" ON "email_deliveries"("provider_id"); + +-- CreateIndex +CREATE INDEX "email_preferences_tenant_id_idx" ON "email_preferences"("tenant_id"); + +-- CreateIndex +CREATE INDEX "email_preferences_email_idx" ON "email_preferences"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "email_preferences_tenant_id_email_key" ON "email_preferences"("tenant_id", "email"); + +-- CreateIndex +CREATE UNIQUE INDEX "email_preferences_user_id_key" ON "email_preferences"("user_id"); + +-- CreateIndex +CREATE INDEX "email_analytics_tenant_id_idx" ON "email_analytics"("tenant_id"); + +-- CreateIndex +CREATE INDEX "email_analytics_category_idx" ON "email_analytics"("category"); + +-- CreateIndex +CREATE INDEX "email_analytics_date_idx" ON "email_analytics"("date"); + +-- CreateIndex +CREATE UNIQUE INDEX "email_analytics_tenant_id_category_date_key" ON "email_analytics"("tenant_id", "category", "date"); + +-- AddForeignKey +ALTER TABLE "payments" ADD CONSTRAINT "payments_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "payments" ADD CONSTRAINT "payments_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "payments" ADD CONSTRAINT "payments_milestone_id_fkey" FOREIGN KEY ("milestone_id") REFERENCES "milestones"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "milestones" ADD CONSTRAINT "milestones_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "invoices" ADD CONSTRAINT "invoices_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "invoices" ADD CONSTRAINT "invoices_milestone_id_fkey" FOREIGN KEY ("milestone_id") REFERENCES "milestones"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "webhooks" ADD CONSTRAINT "webhooks_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "sandbox_transactions" ADD CONSTRAINT "sandbox_transactions_account_id_fkey" FOREIGN KEY ("account_id") REFERENCES "sandbox_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20250530120001_partial_indexes/down.sql b/backend/prisma/migrations/20250530120001_partial_indexes/down.sql new file mode 100644 index 00000000..d564cbc3 --- /dev/null +++ b/backend/prisma/migrations/20250530120001_partial_indexes/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "payments_active_status_partial_idx"; diff --git a/backend/prisma/migrations/20250530120001_partial_indexes/migration.sql b/backend/prisma/migrations/20250530120001_partial_indexes/migration.sql new file mode 100644 index 00000000..b166ee67 --- /dev/null +++ b/backend/prisma/migrations/20250530120001_partial_indexes/migration.sql @@ -0,0 +1,4 @@ +-- Partial index for active payments (zero-downtime friendly; safe to run online) +CREATE INDEX IF NOT EXISTS "payments_active_status_partial_idx" + ON "payments" ("status") + WHERE "status" IN ('pending', 'processing'); diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..99e4f200 --- /dev/null +++ b/backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 9a1ae766..2832632a 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -5,6 +5,8 @@ generator client { provider = "prisma-client-js" } +/// npm run db:seed + datasource db { provider = "postgresql" url = env("DATABASE_URL") @@ -119,7 +121,7 @@ model Payment { milestone Milestone? @relation(fields: [milestoneId], references: [id]) @@index([tenantId, createdAt]) - @@index([status], map: "payments_active_status_idx", where: "status IN ('pending', 'processing')") + @@index([status], map: "payments_active_status_idx") @@index([txHash]) @@index([projectId]) @@map("payments") diff --git a/backend/src/config/env.ts b/backend/src/config/env.ts index a8f3667c..36f2eca9 100644 --- a/backend/src/config/env.ts +++ b/backend/src/config/env.ts @@ -5,6 +5,8 @@ dotenv.config(); const envSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']).default('info'), + LOG_LEVELS: z.string().default(''), DATABASE_URL: z.string().default('postgresql://postgres:postgres@localhost:5432/agenticpay'), PORT: z.coerce.number().default(3001), CORS_ALLOWED_ORIGINS: z.string().default('*'), diff --git a/backend/src/index.ts b/backend/src/index.ts index 862c9187..0bf02a2e 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,5 +1,3 @@ -import { AsyncLocalStorage } from 'node:async_hooks'; -import { randomUUID } from 'node:crypto'; import express, { Request, Response, NextFunction } from 'express'; import * as Sentry from '@sentry/node'; import { nodeProfilingIntegration } from '@sentry/profiling-node'; @@ -53,6 +51,7 @@ import { messageQueue } from './services/queue.js'; import { registerDefaultProcessors } from './services/queue-producers.js'; import { slaTrackingMiddleware } from './middleware/slaTracking.js'; import { requestIdMiddleware, REQUEST_ID_HEADER } from './middleware/requestId.js'; +import { httpLogger, correlationMiddleware } from './middleware/logger.js'; import { validateEnv, config as getConfig } from './config/env.js'; import { flagsRouter } from './routes/flags.js'; import { rateLimitAnalyticsRouter } from './routes/rate-limit-analytics.js'; @@ -133,32 +132,6 @@ if (env.IP_ALLOWLIST_ENABLED || env.IP_ALLOWLIST) { console.log(`[IP Allowlist] Enabled with ${allowedIps.length} IP(s)`); } -const traceStorage = new AsyncLocalStorage(); - -const originalConsole = { - log: console.log, - info: console.info, - warn: console.warn, - error: console.error, -}; - -function formatMessage(args: any[]): any[] { - const traceId = traceStorage.getStore(); - if (traceId) { - if (typeof args[0] === 'string') { - args[0] = `[TraceID: ${traceId}] ${args[0]}`; - } else { - args.unshift(`[TraceID: ${traceId}]`); - } - } - return args; -} - -console.log = (...args) => originalConsole.log(...formatMessage(args)); -console.info = (...args) => originalConsole.info(...formatMessage(args)); -console.warn = (...args) => originalConsole.warn(...formatMessage(args)); -console.error = (...args) => originalConsole.error(...formatMessage(args)); - const app = express(); // Security stack: headers, sanitization, payload limits @@ -190,9 +163,20 @@ app.use( 'API-Version', 'X-API-Version', 'Accept-Version', + 'Stripe-Signature', + 'X-Hub-Signature-256', + 'X-Webhook-Key-Id', ], }) ); + +app.use(requestIdMiddleware); +app.use(correlationMiddleware); +app.use(httpLogger); + +// Incoming webhooks: raw body capture before global JSON parser (#393) +app.use('/webhooks', webhookHandlersRouter); + app.use(express.json()); app.use(express.text({ type: ['text/csv', 'text/plain'] })); @@ -204,23 +188,6 @@ app.use( }) ); -app.use(requestIdMiddleware); - -app.use((req: Request, res: Response, next: NextFunction) => { - const traceId = (req.headers['x-trace-id'] as string) || randomUUID(); - res.setHeader('X-Trace-Id', traceId); - - traceStorage.run(traceId, () => { - console.log(`${req.method} ${req.url} [RequestID: ${req.requestId}] - Started`); - - res.on('finish', () => { - console.log(`${req.method} ${req.url} [RequestID: ${req.requestId}] - Finished with status ${res.statusCode}`); - }); - - next(); - }); -}); - app.use(slaTrackingMiddleware); app.use(sessionMiddleware); @@ -346,9 +313,6 @@ app.use('/api/v2/email', emailV2Router); app.use('/graphql', graphQLRouter); app.use('/graphql/ws', graphQLWsRouter); -// Webhook handlers (outside API versioning for direct access) -app.use('/webhooks', webhookHandlersRouter); - app.use('/api', (req: Request, res: Response, next: NextFunction) => { if (req.path.startsWith('/v1/')) { return next(); diff --git a/backend/src/logging/context.ts b/backend/src/logging/context.ts new file mode 100644 index 00000000..3d2d4346 --- /dev/null +++ b/backend/src/logging/context.ts @@ -0,0 +1,24 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; + +export interface LogContext { + traceId: string; + requestId?: string; + module?: string; +} + +export const logContextStorage = new AsyncLocalStorage(); + +export function getLogContext(): LogContext | undefined { + return logContextStorage.getStore(); +} + +export function runWithLogContext(ctx: LogContext, fn: () => T): T { + return logContextStorage.run(ctx, fn); +} + +export function mergeLogContext(partial: Partial): void { + const store = logContextStorage.getStore(); + if (store) { + Object.assign(store, partial); + } +} diff --git a/backend/src/logging/redact.ts b/backend/src/logging/redact.ts new file mode 100644 index 00000000..c0af6041 --- /dev/null +++ b/backend/src/logging/redact.ts @@ -0,0 +1,33 @@ +const PII_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [ + { pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, replacement: '[REDACTED_EMAIL]' }, + { pattern: /\b(?:sk|pk)_(?:live|test)_[A-Za-z0-9]+\b/g, replacement: '[REDACTED_STRIPE_KEY]' }, + { pattern: /\bwhsec_[A-Za-z0-9]+\b/g, replacement: '[REDACTED_WEBHOOK_SECRET]' }, + { pattern: /\bBearer\s+[A-Za-z0-9._-]+\b/gi, replacement: 'Bearer [REDACTED]' }, + { pattern: /\b(?:\d[ -]*?){13,19}\b/g, replacement: '[REDACTED_CARD]' }, +]; + +export function redactPii(value: unknown): unknown { + if (typeof value === 'string') { + let out = value; + for (const { pattern, replacement } of PII_PATTERNS) { + out = out.replace(pattern, replacement); + } + return out; + } + if (Array.isArray(value)) { + return value.map(redactPii); + } + if (value && typeof value === 'object') { + const result: Record = {}; + for (const [k, v] of Object.entries(value as Record)) { + const lower = k.toLowerCase(); + if (['password', 'secret', 'token', 'authorization', 'cookie', 'apikey', 'api_key'].some((s) => lower.includes(s))) { + result[k] = '[REDACTED]'; + } else { + result[k] = redactPii(v); + } + } + return result; + } + return value; +} diff --git a/backend/src/middleware/logger.test.ts b/backend/src/middleware/logger.test.ts index 0a1cfad5..eefc1d25 100644 --- a/backend/src/middleware/logger.test.ts +++ b/backend/src/middleware/logger.test.ts @@ -49,7 +49,7 @@ describe('httpLogger middleware', () => { ); // X-Request-Id header must be set on the response - expect(res.setHeader).toHaveBeenCalledWith('X-Request-Id', (req as any).id); + expect(res.setHeader).toHaveBeenCalledWith('x-request-id', (req as any).id); expect(next).toHaveBeenCalledOnce(); }); @@ -66,7 +66,7 @@ describe('httpLogger middleware', () => { httpLogger(req as any, res as any, next); expect((req as any).id).toBe(existingId); - expect(res.setHeader).toHaveBeenCalledWith('X-Request-Id', existingId); + expect(res.setHeader).toHaveBeenCalledWith('x-request-id', existingId); }); it('calls next()', async () => { diff --git a/backend/src/middleware/logger.ts b/backend/src/middleware/logger.ts index 6abc4fe8..0971df8f 100644 --- a/backend/src/middleware/logger.ts +++ b/backend/src/middleware/logger.ts @@ -1,19 +1,49 @@ import pino from 'pino'; import pinoHttp from 'pino-http'; -import { randomUUID } from 'crypto'; -import type { Request } from 'express'; +import { randomUUID } from 'node:crypto'; +import type { Request, Response, NextFunction } from 'express'; +import { getLogContext, logContextStorage, mergeLogContext, runWithLogContext } from '../logging/context.js'; +import { redactPii } from '../logging/redact.js'; +import { REQUEST_ID_HEADER } from './requestId.js'; -// --------------------------------------------------------------------------- -// Base logger -// --------------------------------------------------------------------------- +const MODULE_LEVELS: Record = {}; +const DEFAULT_LEVEL = process.env.LOG_LEVEL ?? 'info'; + +function resolveLevel(module?: string): string { + if (module && MODULE_LEVELS[module]) { + return MODULE_LEVELS[module]; + } + return DEFAULT_LEVEL; +} + +/** Parse LOG_LEVELS=webhooks:debug,prisma:warn */ +export function parseModuleLogLevels(spec?: string): void { + if (!spec) return; + for (const part of spec.split(',')) { + const [mod, level] = part.trim().split(':'); + if (mod && level) MODULE_LEVELS[mod] = level; + } +} + +parseModuleLogLevels(process.env.LOG_LEVELS); -/** - * Pino logger instance. - * - In development: pretty-print to stdout for human readability. - * - In production: raw JSON for ingestion by log aggregators. - */ export const logger = pino({ - level: process.env.LOG_LEVEL ?? 'info', + level: DEFAULT_LEVEL, + base: { service: 'agenticpay-backend' }, + formatters: { + level(label) { + return { level: label }; + }, + log(object) { + const ctx = getLogContext(); + return redactPii({ + ...object, + ...(ctx?.traceId ? { traceId: ctx.traceId } : {}), + ...(ctx?.requestId ? { requestId: ctx.requestId } : {}), + ...(ctx?.module ? { module: ctx.module } : {}), + }) as Record; + }, + }, ...(process.env.NODE_ENV === 'development' && { transport: { target: 'pino-pretty', @@ -22,66 +52,73 @@ export const logger = pino({ }), }); -// --------------------------------------------------------------------------- -// HTTP middleware -// --------------------------------------------------------------------------- +export function createModuleLogger(module: string) { + const child = logger.child({ module }, { level: resolveLevel(module) }); + return child; +} -/** - * pino-http middleware. - * - * Logs every request with: - * - req.id – UUID v4 correlation ID (forwarded to res as X-Request-Id) - * - method – HTTP verb - * - url – path + query string - * - statusCode – HTTP status code - * - responseTime – wall-clock time in milliseconds - * - * Sensitive headers (Authorization) are redacted before the log is written. - */ export const httpLogger = pinoHttp({ logger, - - // ── Request-ID generation ──────────────────────────────────────────────── - genReqId(req: Request, res) { - const existingId = req.headers['x-request-id'] as string | undefined; - const id = existingId ?? randomUUID(); - res.setHeader('X-Request-Id', id); + genReqId(req: Request, res: Response) { + const existing = + (req.headers[REQUEST_ID_HEADER] as string | undefined) ?? + (req.headers['x-request-id'] as string | undefined); + const id = existing ?? randomUUID(); + res.setHeader(REQUEST_ID_HEADER, id); + req.requestId = id; return id; }, - - // ── Sensitive header redaction ─────────────────────────────────────────── - // These paths are redacted in the serialised log output. redact: { paths: [ 'req.headers.authorization', 'req.headers["x-api-key"]', 'req.headers.cookie', + 'req.headers["stripe-signature"]', ], censor: '[Redacted]', }, - - // ── Custom serialisers ─────────────────────────────────────────────────── serializers: { req(req) { return { id: req.id, method: req.method, url: req.url, - // Include remoteAddress for audit trails remoteAddress: req.remoteAddress, }; }, res(res) { - return { - statusCode: res.statusCode, - }; + return { statusCode: res.statusCode }; }, }, - - // Log 5xx errors at 'error' level, 4xx at 'warn', everything else at 'info' customLogLevel(_req, res, err) { if (err || res.statusCode >= 500) return 'error'; if (res.statusCode >= 400) return 'warn'; return 'info'; }, + customProps(req: Request) { + const traceId = (req.headers['x-trace-id'] as string) || undefined; + return { traceId, requestId: req.requestId ?? req.id }; + }, }); + +/** Bind trace + request IDs for async boundaries (jobs, webhooks). */ +export function correlationMiddleware(req: Request, res: Response, next: NextFunction): void { + const traceId = (req.headers['x-trace-id'] as string) || randomUUID(); + const requestId = req.requestId ?? (req.headers[REQUEST_ID_HEADER] as string) || randomUUID(); + res.setHeader('X-Trace-Id', traceId); + + runWithLogContext({ traceId, requestId }, () => { + mergeLogContext({ traceId, requestId }); + next(); + }); +} + +export function bindAsyncContext(fn: () => Promise, partial?: Partial<{ traceId: string; requestId: string; module: string }>): Promise { + const parent = getLogContext(); + const ctx = { + traceId: partial?.traceId ?? parent?.traceId ?? randomUUID(), + requestId: partial?.requestId ?? parent?.requestId, + module: partial?.module ?? parent?.module, + }; + return logContextStorage.run(ctx, fn); +} diff --git a/backend/src/middleware/webhookVerification.ts b/backend/src/middleware/webhookVerification.ts index d4c80d68..edc7e0d5 100644 --- a/backend/src/middleware/webhookVerification.ts +++ b/backend/src/middleware/webhookVerification.ts @@ -1,68 +1,108 @@ -import { Request, Response, NextFunction } from 'express'; -import { verifyWebhookSignature, queueFailedWebhook, WebhookProvider } from '../services/webhooks/verification.js'; +import express, { Request, Response, NextFunction } from 'express'; +import { + queueFailedWebhook, + retryWebhook, + type WebhookProvider, +} from '../services/webhooks/verification.js'; +import { + verifyStripeProviderWebhook, + verifyGithubProviderWebhook, + verifyPaypalProviderWebhook, + verifyCustomProviderWebhook, + type ProviderVerificationResult, +} from '../services/webhooks/providers.js'; +import { isReplayEvent } from '../services/webhooks/replay.js'; +import { storeWebhookPayload } from '../services/webhooks/audit.js'; +import { createModuleLogger } from './logger.js'; import { AppError } from './errorHandler.js'; -export interface WebhookVerificationConfig { - provider: WebhookProvider; - signatureHeader: string; - timestampHeader: string; - toleranceSeconds?: number; - maxRetries?: number; +const webhookLog = createModuleLogger('webhooks'); + +declare global { + namespace Express { + interface Request { + rawBody?: string; + webhookVerification?: ProviderVerificationResult; + } + } } -/** - * Middleware to verify webhook signatures - */ -export function verifyWebhook(config: WebhookVerificationConfig) { - return async (req: Request, res: Response, next: NextFunction) => { - try { - const signature = req.headers[config.signatureHeader.toLowerCase()] as string; - const timestamp = req.headers[config.timestampHeader.toLowerCase()] as string; +export function captureRawBody(req: Request, _res: Response, buf: Buffer): void { + if (buf?.length) { + req.rawBody = buf.toString('utf8'); + } +} - if (!signature) { - throw new AppError(400, `Missing ${config.signatureHeader} header`, 'WEBHOOK_VERIFICATION_FAILED'); - } +/** JSON parser that preserves raw body for HMAC verification */ +export const webhookJsonParser = express.json({ + verify: captureRawBody, + limit: '2mb', +}); - if (!timestamp) { - throw new AppError(400, `Missing ${config.timestampHeader} header`, 'WEBHOOK_VERIFICATION_FAILED'); - } +type ProviderVerifier = (req: Request, rawBody: string) => ProviderVerificationResult; - // Get raw body for signature verification - const body = req.rawBody || JSON.stringify(req.body); +const providerVerifiers: Record = { + stripe: verifyStripeProviderWebhook, + paypal: verifyPaypalProviderWebhook, + github: verifyGithubProviderWebhook, + custom: verifyCustomProviderWebhook, +}; - const result = verifyWebhookSignature({ - signature, - timestamp, - body, - provider: config.provider, - toleranceSeconds: config.toleranceSeconds || 300, +export function verifyWebhookProvider(provider: WebhookProvider) { + return async (req: Request, res: Response, next: NextFunction) => { + try { + const rawBody = req.rawBody ?? (typeof req.body === 'string' ? req.body : JSON.stringify(req.body ?? {})); + const verify = providerVerifiers[provider]; + const result = verify(req, rawBody); + + storeWebhookPayload({ + provider, + eventId: result.eventId, + payload: result.payload ?? req.body, + signature: (req.headers['stripe-signature'] || + req.headers['x-hub-signature-256'] || + req.headers['x-signature'] || + '') as string, + verified: result.isValid, + error: result.error, }); + if (result.isValid && isReplayEvent(`${provider}:${result.eventId}`)) { + webhookLog.warn({ provider, eventId: result.eventId }, 'Webhook replay detected'); + throw new AppError(409, 'Duplicate webhook delivery', 'WEBHOOK_REPLAY'); + } + if (!result.isValid) { - // Queue failed webhook for retry const event = queueFailedWebhook( - config.provider, - req.headers['x-webhook-event-type'] as string || 'unknown', - req.body, - signature, - timestamp, - result.error || 'Verification failed' + provider, + (req.headers['x-webhook-event-type'] as string) || 'unknown', + result.payload ?? req.body, + (req.headers['x-signature'] as string) || '', + result.timestamp.toISOString(), + result.error || 'Verification failed', ); - // Log verification failure - console.warn(`Webhook verification failed for ${config.provider}:`, { - eventId: event.id, - error: result.error, - timestamp: result.timestamp, - provider: result.provider, - }); + webhookLog.warn( + { provider, eventId: event.id, error: result.error }, + 'Webhook verification failed', + ); + + if (result.error?.includes('timeout') || result.error?.includes('network')) { + const retried = retryWebhook(event.id); + if (retried?.isValid) { + req.webhookVerification = { ...result, isValid: true }; + req.body = result.payload ?? req.body; + return next(); + } + } throw new AppError(401, `Webhook verification failed: ${result.error}`, 'WEBHOOK_VERIFICATION_FAILED'); } - // Attach verification result to request for downstream handlers - (req as any).webhookVerification = result; - + req.webhookVerification = result; + if (result.payload !== undefined) { + req.body = result.payload; + } next(); } catch (error) { next(error); @@ -70,59 +110,9 @@ export function verifyWebhook(config: WebhookVerificationConfig) { }; } -/** - * Pre-built verification middleware for common providers - */ export const webhookVerifiers = { - stripe: verifyWebhook({ - provider: 'stripe', - signatureHeader: 'stripe-signature', - timestampHeader: 'stripe-timestamp', - toleranceSeconds: 300, - }), - - paypal: verifyWebhook({ - provider: 'paypal', - signatureHeader: 'paypal-transmission-signature', - timestampHeader: 'paypal-transmission-time', - toleranceSeconds: 300, - }), - - github: verifyWebhook({ - provider: 'github', - signatureHeader: 'x-hub-signature-256', - timestampHeader: 'x-github-delivery', // GitHub doesn't use timestamp header, but we can use delivery ID - toleranceSeconds: 300, - }), - - custom: verifyWebhook({ - provider: 'custom', - signatureHeader: 'x-signature', - timestampHeader: 'x-timestamp', - toleranceSeconds: 300, - }), + stripe: verifyWebhookProvider('stripe'), + paypal: verifyWebhookProvider('paypal'), + github: verifyWebhookProvider('github'), + custom: verifyWebhookProvider('custom'), }; - -/** - * Middleware to capture raw body for webhook verification - * Must be used before express.json() middleware - */ -export function rawBodyCapture() { - return (req: Request, res: Response, next: NextFunction) => { - let data = ''; - - req.setEncoding('utf8'); - req.on('data', (chunk) => { - data += chunk; - }); - - req.on('end', () => { - (req as any).rawBody = data; - next(); - }); - - req.on('error', (err) => { - next(err); - }); - }; -} \ No newline at end of file diff --git a/backend/src/routes/webhookHandlers.ts b/backend/src/routes/webhookHandlers.ts index f0ef9dd7..c9f5358b 100644 --- a/backend/src/routes/webhookHandlers.ts +++ b/backend/src/routes/webhookHandlers.ts @@ -1,109 +1,66 @@ import { Router } from 'express'; -import { webhookVerifiers, rawBodyCapture } from '../middleware/webhookVerification.js'; +import { webhookVerifiers, webhookJsonParser } from '../middleware/webhookVerification.js'; import { markWebhookProcessed } from '../services/webhooks/verification.js'; import { asyncHandler } from '../middleware/errorHandler.js'; +import { createModuleLogger } from '../middleware/logger.js'; export const webhookHandlersRouter = Router(); +const webhookLog = createModuleLogger('webhooks'); -// Apply raw body capture middleware first (before JSON parsing) -webhookHandlersRouter.use(rawBodyCapture()); +webhookHandlersRouter.use(webhookJsonParser); -// Stripe webhooks webhookHandlersRouter.post( '/stripe', webhookVerifiers.stripe, asyncHandler(async (req, res) => { - const event = req.body; - const verification = (req as any).webhookVerification; - - console.log(`Verified Stripe webhook: ${event.type}`, { - eventId: event.id, - verified: verification.isValid, - timestamp: verification.timestamp, - }); - - // Process the webhook event - // In a real implementation, this would trigger business logic - // For now, just mark as processed - const eventId = `stripe_${event.id}`; - markWebhookProcessed(eventId); - + const event = req.body as { id?: string; type?: string }; + webhookLog.info( + { eventId: event.id, type: event.type, verified: req.webhookVerification?.isValid }, + 'Stripe webhook received', + ); + if (event.id) markWebhookProcessed(`stripe_${event.id}`); res.json({ received: true, event: event.type }); }), ); -// PayPal webhooks webhookHandlersRouter.post( '/paypal', webhookVerifiers.paypal, asyncHandler(async (req, res) => { - const event = req.body; - const verification = (req as any).webhookVerification; - - console.log(`Verified PayPal webhook: ${event.event_type}`, { - eventId: event.id, - verified: verification.isValid, - timestamp: verification.timestamp, - }); - - // Process the webhook event - const eventId = `paypal_${event.id}`; - markWebhookProcessed(eventId); - + const event = req.body as { id?: string; event_type?: string }; + webhookLog.info({ eventId: event.id, type: event.event_type }, 'PayPal webhook received'); + if (event.id) markWebhookProcessed(`paypal_${event.id}`); res.json({ received: true, event: event.event_type }); }), ); -// GitHub webhooks webhookHandlersRouter.post( '/github', webhookVerifiers.github, asyncHandler(async (req, res) => { - const event = req.body; const eventType = req.headers['x-github-event'] as string; - const verification = (req as any).webhookVerification; - - console.log(`Verified GitHub webhook: ${eventType}`, { - deliveryId: req.headers['x-github-delivery'], - verified: verification.isValid, - timestamp: verification.timestamp, - }); - - // Process the webhook event const deliveryId = req.headers['x-github-delivery'] as string; - const eventId = `github_${deliveryId}`; - markWebhookProcessed(eventId); - + webhookLog.info({ deliveryId, eventType }, 'GitHub webhook received'); + markWebhookProcessed(`github_${deliveryId}`); res.json({ received: true, event: eventType }); }), ); -// Custom webhooks webhookHandlersRouter.post( '/custom', webhookVerifiers.custom, asyncHandler(async (req, res) => { - const event = req.body; - const verification = (req as any).webhookVerification; - - console.log(`Verified custom webhook`, { - verified: verification.isValid, - timestamp: verification.timestamp, - }); - - // Process the webhook event - const eventId = `custom_${Date.now()}`; + webhookLog.info({ verified: req.webhookVerification?.isValid }, 'Custom webhook received'); + const eventId = req.webhookVerification?.eventId ?? `custom_${Date.now()}`; markWebhookProcessed(eventId); - res.json({ received: true }); }), ); -// Test endpoint for webhook verification (no verification required) webhookHandlersRouter.post( '/test', asyncHandler(async (req, res) => { - console.log('Test webhook received:', req.body); + webhookLog.debug({ body: req.body }, 'Test webhook received'); res.json({ received: true, test: true }); }), -); \ No newline at end of file +); diff --git a/backend/src/routes/webhooks.ts b/backend/src/routes/webhooks.ts index 09dde38a..b4ab6a99 100644 --- a/backend/src/routes/webhooks.ts +++ b/backend/src/routes/webhooks.ts @@ -14,6 +14,7 @@ import { markWebhookProcessed, WebhookProvider, } from '../services/webhooks/verification.js'; +import { getWebhookAuditLog } from '../services/webhooks/audit.js'; // Webhook delivery services import { enqueueWebhookEvent, @@ -48,6 +49,7 @@ const webhookEventSchema = z.object({ const createSecretSchema = z.object({ provider: z.enum(['stripe', 'paypal', 'github', 'custom']), secret: z.string().min(32, 'Secret must be at least 32 characters'), + keyId: z.string().optional(), expiresAt: z.string().optional(), }); @@ -104,7 +106,8 @@ webhooksRouter.post( const secret = createWebhookSecret( req.body.provider, req.body.secret, - req.body.expiresAt + req.body.expiresAt, + req.body.keyId, ); res.status(201).json(secret); }) @@ -156,6 +159,15 @@ webhooksRouter.get( }) ); +webhooksRouter.get( + '/audit', + asyncHandler(async (req, res) => { + const limit = parseInt(req.query.limit as string) || 100; + const records = getWebhookAuditLog(limit); + res.json({ records, total: records.length }); + }), +); + webhooksRouter.get( '/events/queued', asyncHandler(async (req, res) => { diff --git a/backend/src/services/webhooks/__tests__/replay.test.ts b/backend/src/services/webhooks/__tests__/replay.test.ts new file mode 100644 index 00000000..f15fa383 --- /dev/null +++ b/backend/src/services/webhooks/__tests__/replay.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { isReplayEvent, clearReplayCache } from '../replay.js'; + +describe('Webhook replay protection', () => { + beforeEach(() => { + clearReplayCache(); + }); + + it('allows first delivery', () => { + expect(isReplayEvent('stripe:evt_1')).toBe(false); + }); + + it('blocks duplicate event id within TTL', () => { + expect(isReplayEvent('stripe:evt_1')).toBe(false); + expect(isReplayEvent('stripe:evt_1')).toBe(true); + }); +}); diff --git a/backend/src/services/webhooks/audit.ts b/backend/src/services/webhooks/audit.ts new file mode 100644 index 00000000..cee5767e --- /dev/null +++ b/backend/src/services/webhooks/audit.ts @@ -0,0 +1,46 @@ +import type { WebhookProvider } from './verification.js'; + +export interface WebhookAuditRecord { + id: string; + provider: WebhookProvider; + eventId: string; + payload: unknown; + signaturePreview: string; + verified: boolean; + error?: string; + receivedAt: string; +} + +const auditLog: WebhookAuditRecord[] = []; +const MAX_AUDIT = 5000; + +export function storeWebhookPayload(input: { + provider: WebhookProvider; + eventId: string; + payload: unknown; + signature: string; + verified: boolean; + error?: string; +}): WebhookAuditRecord { + const record: WebhookAuditRecord = { + id: `audit_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, + provider: input.provider, + eventId: input.eventId, + payload: input.payload, + signaturePreview: input.signature.slice(0, 16) + '…', + verified: input.verified, + error: input.error, + receivedAt: new Date().toISOString(), + }; + auditLog.unshift(record); + if (auditLog.length > MAX_AUDIT) auditLog.length = MAX_AUDIT; + return record; +} + +export function getWebhookAuditLog(limit = 100): WebhookAuditRecord[] { + return auditLog.slice(0, limit); +} + +export function clearWebhookAudit(): void { + auditLog.length = 0; +} diff --git a/backend/src/services/webhooks/providers.ts b/backend/src/services/webhooks/providers.ts new file mode 100644 index 00000000..7653f581 --- /dev/null +++ b/backend/src/services/webhooks/providers.ts @@ -0,0 +1,200 @@ +import { createHmac, timingSafeEqual } from 'node:crypto'; +import type { Request } from 'express'; +import { constructWebhookEvent } from '../stripe.js'; +import { AppError } from '../../middleware/errorHandler.js'; +import type { WebhookProvider } from './verification.js'; +import { + generateWebhookSignature, + getActiveSecretsForProvider, + verifyWebhookSignature, +} from './verification.js'; + +export interface ProviderVerificationResult { + isValid: boolean; + provider: WebhookProvider; + eventId: string; + timestamp: Date; + body: string; + error?: string; + payload?: unknown; +} + +function safeEqualHex(a: string, b: string): boolean { + try { + const ba = Buffer.from(a, 'hex'); + const bb = Buffer.from(b, 'hex'); + if (ba.length !== bb.length) return false; + return timingSafeEqual(ba, bb); + } catch { + return false; + } +} + +export function verifyStripeProviderWebhook(req: Request, rawBody: string): ProviderVerificationResult { + const sig = req.headers['stripe-signature'] as string | undefined; + if (!sig) { + return { + isValid: false, + provider: 'stripe', + eventId: 'unknown', + timestamp: new Date(), + body: rawBody, + error: 'Missing stripe-signature header', + }; + } + try { + const event = constructWebhookEvent(Buffer.from(rawBody, 'utf8'), sig); + return { + isValid: true, + provider: 'stripe', + eventId: event.id, + timestamp: new Date(event.created * 1000), + body: rawBody, + payload: event, + }; + } catch (err) { + const message = err instanceof AppError ? err.message : 'Stripe signature verification failed'; + return { + isValid: false, + provider: 'stripe', + eventId: 'unknown', + timestamp: new Date(), + body: rawBody, + error: message, + }; + } +} + +export function verifyGithubProviderWebhook(req: Request, rawBody: string): ProviderVerificationResult { + const signature = req.headers['x-hub-signature-256'] as string | undefined; + const deliveryId = (req.headers['x-github-delivery'] as string) || `github_${Date.now()}`; + const secrets = getActiveSecretsForProvider('github'); + + if (!signature?.startsWith('sha256=')) { + return { + isValid: false, + provider: 'github', + eventId: deliveryId, + timestamp: new Date(), + body: rawBody, + error: 'Missing or invalid x-hub-signature-256', + }; + } + + const provided = signature.slice('sha256='.length); + for (const secret of secrets) { + const expected = createHmac('sha256', secret.secret).update(rawBody).digest('hex'); + if (safeEqualHex(expected, provided)) { + return { + isValid: true, + provider: 'github', + eventId: deliveryId, + timestamp: new Date(), + body: rawBody, + payload: JSON.parse(rawBody), + }; + } + } + + return { + isValid: false, + provider: 'github', + eventId: deliveryId, + timestamp: new Date(), + body: rawBody, + error: 'GitHub signature verification failed', + }; +} + +export function verifyGenericProviderWebhook( + provider: WebhookProvider, + req: Request, + rawBody: string, + signatureHeader: string, + timestampHeader: string, + toleranceSeconds = 300, +): ProviderVerificationResult { + const signature = req.headers[signatureHeader.toLowerCase()] as string | undefined; + const timestamp = req.headers[timestampHeader.toLowerCase()] as string | undefined; + const eventId = + (req.headers['x-webhook-id'] as string) || + (req.headers['paypal-transmission-id'] as string) || + `${provider}_${Date.now()}`; + + if (!signature || !timestamp) { + return { + isValid: false, + provider, + eventId, + timestamp: new Date(), + body: rawBody, + error: `Missing ${signatureHeader} or ${timestampHeader}`, + }; + } + + const keyId = req.headers['x-webhook-key-id'] as string | undefined; + const result = verifyWebhookSignature({ + signature: signature.replace(/^sha256=/, ''), + timestamp, + body: rawBody, + provider, + toleranceSeconds, + keyId, + }); + + let payload: unknown; + try { + payload = JSON.parse(rawBody); + } catch { + payload = rawBody; + } + + return { + isValid: result.isValid, + provider, + eventId, + timestamp: result.timestamp, + body: rawBody, + error: result.error, + payload, + }; +} + +export function verifyPaypalProviderWebhook(req: Request, rawBody: string): ProviderVerificationResult { + return verifyGenericProviderWebhook( + 'paypal', + req, + rawBody, + 'paypal-transmission-signature', + 'paypal-transmission-time', + ); +} + +export function verifyCustomProviderWebhook(req: Request, rawBody: string): ProviderVerificationResult { + const sig = req.headers['x-signature'] as string | undefined; + const ts = req.headers['x-timestamp'] as string | undefined; + if (!sig || !ts) { + return verifyGenericProviderWebhook('custom', req, rawBody, 'x-signature', 'x-timestamp'); + } + const result = verifyWebhookSignature({ + signature: sig.replace(/^sha256=/, ''), + timestamp: ts, + body: rawBody, + provider: 'custom', + keyId: req.headers['x-webhook-key-id'] as string | undefined, + }); + return { + isValid: result.isValid, + provider: 'custom', + eventId: (req.headers['x-webhook-id'] as string) || `custom_${Date.now()}`, + timestamp: result.timestamp, + body: rawBody, + error: result.error, + payload: JSON.parse(rawBody), + }; +} + +/** Dev/test helper: sign outbound webhooks with AgenticPay format */ +export function signTestWebhook(payload: string, secret: string, timestamp: string): string { + return generateWebhookSignature(payload, secret, timestamp); +} diff --git a/backend/src/services/webhooks/replay.ts b/backend/src/services/webhooks/replay.ts new file mode 100644 index 00000000..93e3b9d8 --- /dev/null +++ b/backend/src/services/webhooks/replay.ts @@ -0,0 +1,31 @@ +/** In-memory replay protection; swap for Redis in multi-instance deploys. */ +const seenEventIds = new Map(); +const DEFAULT_TTL_MS = 5 * 60 * 1000; + +export function isReplayEvent(eventId: string, ttlMs = DEFAULT_TTL_MS): boolean { + const now = Date.now(); + pruneExpired(now, ttlMs); + if (seenEventIds.has(eventId)) { + return true; + } + seenEventIds.set(eventId, now + ttlMs); + return false; +} + +function pruneExpired(now: number, ttlMs: number): void { + for (const [id, expires] of seenEventIds) { + if (expires < now) { + seenEventIds.delete(id); + } + } + if (seenEventIds.size > 10_000) { + const cutoff = now - ttlMs; + for (const [id, expires] of seenEventIds) { + if (expires < cutoff) seenEventIds.delete(id); + } + } +} + +export function clearReplayCache(): void { + seenEventIds.clear(); +} diff --git a/backend/src/services/webhooks/verification.ts b/backend/src/services/webhooks/verification.ts index d85168eb..96f26446 100644 --- a/backend/src/services/webhooks/verification.ts +++ b/backend/src/services/webhooks/verification.ts @@ -6,6 +6,7 @@ export const webhookProviderSchema = z.enum(['stripe', 'paypal', 'github', 'cust export const webhookSecretSchema = z.object({ id: z.string(), + keyId: z.string().optional(), provider: webhookProviderSchema, secret: z.string().min(32, 'Secret must be at least 32 characters'), isActive: z.boolean().default(true), @@ -20,6 +21,7 @@ export const webhookVerificationSchema = z.object({ body: z.string(), provider: webhookProviderSchema, toleranceSeconds: z.number().default(300), // 5 minutes default tolerance + keyId: z.string().optional(), }); export type WebhookProvider = z.infer; @@ -74,9 +76,8 @@ export function verifyWebhookSignature( ): WebhookVerificationResult { const parsed = webhookVerificationSchema.parse(input); - // Get active secret for provider - const secret = getActiveSecretForProvider(parsed.provider); - if (!secret) { + const secrets = getActiveSecretsForProvider(parsed.provider, parsed.keyId); + if (secrets.length === 0) { return { isValid: false, provider: parsed.provider, @@ -100,18 +101,20 @@ export function verifyWebhookSignature( }; } - // Generate expected signature - const expectedSignature = generateWebhookSignature( - parsed.body, - secret.secret, - parsed.timestamp - ); - - // Compare signatures (constant-time comparison to prevent timing attacks) - const isValid = constantTimeEquals(expectedSignature, parsed.signature); + let isValid = false; + let matchedSecret: WebhookSecret | undefined; + for (const secret of secrets) { + const expectedSignature = generateWebhookSignature(parsed.body, secret.secret, parsed.timestamp); + if (constantTimeEquals(expectedSignature, parsed.signature)) { + isValid = true; + matchedSecret = secret; + break; + } + } - // Update last used timestamp - updateSecretLastUsed(secret.id); + if (matchedSecret) { + updateSecretLastUsed(matchedSecret.id); + } return { isValid, @@ -143,11 +146,13 @@ function constantTimeEquals(a: string, b: string): boolean { export function createWebhookSecret( provider: WebhookProvider, secret: string, - expiresAt?: string + expiresAt?: string, + keyId?: string, ): WebhookSecret { const id = `whs_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const webhookSecret: WebhookSecret = { id, + keyId: keyId ?? `key_${provider}_${Date.now()}`, provider, secret, isActive: true, @@ -160,15 +165,20 @@ export function createWebhookSecret( } /** - * Get active secret for a provider + * Get active secret for a provider (newest) */ export function getActiveSecretForProvider(provider: WebhookProvider): WebhookSecret | null { - const activeSecrets = Array.from(webhookSecrets.values()) - .filter(secret => secret.provider === provider && secret.isActive) - .filter(secret => !secret.expiresAt || new Date(secret.expiresAt) >= new Date()) - .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + const list = getActiveSecretsForProvider(provider); + return list.length > 0 ? list[0] : null; +} - return activeSecrets.length > 0 ? activeSecrets[0] : null; +/** All active secrets (supports key rotation overlap) */ +export function getActiveSecretsForProvider(provider: WebhookProvider, keyId?: string): WebhookSecret[] { + return Array.from(webhookSecrets.values()) + .filter((secret) => secret.provider === provider && secret.isActive) + .filter((secret) => !secret.expiresAt || new Date(secret.expiresAt) >= new Date()) + .filter((secret) => !keyId || secret.keyId === keyId) + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); } /** @@ -177,18 +187,17 @@ export function getActiveSecretForProvider(provider: WebhookProvider): WebhookSe export function rotateWebhookSecret( provider: WebhookProvider, newSecret: string, - gracePeriodHours: number = 24 + gracePeriodHours: number = 24, + newKeyId?: string, ): WebhookSecret { - // Deactivate current secret - const currentSecret = getActiveSecretForProvider(provider); - if (currentSecret) { + const currentSecrets = getActiveSecretsForProvider(provider); + for (const currentSecret of currentSecrets) { currentSecret.isActive = false; currentSecret.expiresAt = new Date(Date.now() + gracePeriodHours * 60 * 60 * 1000).toISOString(); webhookSecrets.set(currentSecret.id, currentSecret); } - // Create new active secret - return createWebhookSecret(provider, newSecret); + return createWebhookSecret(provider, newSecret, undefined, newKeyId); } /** diff --git a/docker-compose.yml b/docker-compose.yml index 9b9642d8..1b8367bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,6 +33,32 @@ services: timeout: 3s retries: 10 + loki: + image: grafana/loki:3.0.0 + restart: unless-stopped + ports: + - '3100:3100' + command: -config.file=/etc/loki/local-config.yaml + volumes: + - agenticpay-loki:/loki + + grafana: + image: grafana/grafana:11.0.0 + restart: unless-stopped + ports: + - '3002:3000' + environment: + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: admin + GF_USERS_ALLOW_SIGN_UP: 'false' + volumes: + - agenticpay-grafana:/var/lib/grafana + - ./infra/logging/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml:ro + depends_on: + - loki + volumes: agenticpay-postgres: agenticpay-redis: + agenticpay-loki: + agenticpay-grafana: diff --git a/docs/logging.md b/docs/logging.md new file mode 100644 index 00000000..04cecc33 --- /dev/null +++ b/docs/logging.md @@ -0,0 +1,33 @@ +# Structured Logging (#409) + +AgenticPay backend uses **Pino** for JSON structured logs with correlation IDs. + +## Fields + +| Field | Source | +|-------|--------| +| `traceId` | `X-Trace-Id` header or generated UUID | +| `requestId` | `x-request-id` header or generated UUID | +| `module` | Per-module child loggers (`webhooks`, `prisma`, …) | + +## Configuration + +```env +LOG_LEVEL=info +LOG_LEVELS=webhooks:debug,prisma:warn +``` + +## Local log aggregation (Loki + Grafana) + +```bash +docker compose up -d loki grafana +``` + +- Grafana: http://localhost:3002 (admin / admin) +- Query: `{service="agenticpay-backend"}` in Explore → Loki + +Ship production logs with Promtail or your cloud log agent targeting Loki. + +## PII redaction + +Emails, API keys, Bearer tokens, and card-like numbers are redacted in log formatters. Sensitive object keys (`password`, `secret`, `token`, …) are censored. diff --git a/docs/sdk/ERROR-HANDLING.md b/docs/sdk/ERROR-HANDLING.md new file mode 100644 index 00000000..298c48fe --- /dev/null +++ b/docs/sdk/ERROR-HANDLING.md @@ -0,0 +1,43 @@ +# SDK error handling (#408) + +The SDK maps HTTP failures to typed errors in `packages/sdk/src/errors.ts`. + +## Error types + +| Class | HTTP | When | +|-------|------|------| +| `AuthenticationError` | 401 | Invalid or missing API key | +| `AuthorizationError` | 403 | Insufficient scope | +| `ValidationError` | 400 | Invalid request body | +| `RateLimitError` | 429 | Rate limit exceeded | +| `NetworkError` | — | Transport / DNS failures | +| `AgenticPayError` | any | Base class with `status`, `code`, `details` | + +## Example + +```ts +import { + createAgenticPaySDK, + RateLimitError, + ValidationError, +} from '@agenticpay/sdk'; + +const sdk = createAgenticPaySDK({ baseUrl, apiKey }); + +try { + await sdk.payments.createSplitConfig({ /* ... */ }); +} catch (err) { + if (err instanceof RateLimitError) { + const retryAfter = (err.details as { retryAfter?: number })?.retryAfter ?? 60; + console.warn(`Rate limited — retry in ${retryAfter}s`); + } else if (err instanceof ValidationError) { + console.error('Invalid payload', err.details); + } else { + throw err; + } +} +``` + +## Retries + +The HTTP client retries idempotent requests on 5xx and network errors with exponential backoff. Non-idempotent `POST` calls are not retried automatically. diff --git a/docs/sdk/MIGRATION-FROM-REST.md b/docs/sdk/MIGRATION-FROM-REST.md new file mode 100644 index 00000000..35a13979 --- /dev/null +++ b/docs/sdk/MIGRATION-FROM-REST.md @@ -0,0 +1,44 @@ +# Migrating from REST to the SDK (#408) + +## Install + +```bash +npm install @agenticpay/sdk +``` + +## Base URL and auth + +**REST** + +```http +GET /api/v1/splits/configs?merchantId=m_123 +Authorization: Bearer YOUR_API_KEY +``` + +**SDK** + +```ts +import { createAgenticPaySDK } from '@agenticpay/sdk'; + +const sdk = createAgenticPaySDK({ + baseUrl: 'https://api.agenticpay.com/api/v1', + apiKey: process.env.AGENTICPAY_API_KEY, +}); +``` + +## Endpoint mapping + +| REST | SDK | +|------|-----| +| `POST /verification/verify` | `sdk.verification.verify(...)` | +| `POST /splits/configs` | `sdk.payments.createSplitConfig(...)` | +| `GET /splits/configs/:id` | `sdk.payments.getSplitConfig(...)` | +| `POST /refunds` | `sdk.refunds.create(...)` | + +## OpenAPI-generated client + +The repo also ships an OpenAPI `openapi-fetch` client under `backend/docs/api/sdks/typescript/`. Prefer `@agenticpay/sdk` for curated ergonomics; use the generated client when you need every OpenAPI operation immediately after spec updates. + +## Webhooks + +Incoming provider webhooks stay HTTP endpoints (`POST /webhooks/stripe`). Outbound merchant webhooks are configured via the dashboard or `/api/v1/webhooks` — not the SDK. diff --git a/infra/logging/grafana-datasources.yml b/infra/logging/grafana-datasources.yml new file mode 100644 index 00000000..9a14ad20 --- /dev/null +++ b/infra/logging/grafana-datasources.yml @@ -0,0 +1,8 @@ +apiVersion: 1 +datasources: + - name: Loki + type: loki + access: proxy + url: http://loki:3100 + isDefault: true + editable: false diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md new file mode 100644 index 00000000..ac0272a3 --- /dev/null +++ b/packages/sdk/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to `@agenticpay/sdk` are documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [0.1.0] - 2025-05-30 + +### Added +- Initial SDK with payments, refunds, and verification APIs +- Error hierarchy (`AgenticPayError`, `RateLimitError`, …) +- Retry/backoff on HTTP client + +[0.1.0]: https://github.com/Smartdevs17/agenticpay/releases/tag/sdk-v0.1.0 diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 3cf6abd8..9b124283 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -8,7 +8,7 @@ Official TypeScript SDK for AgenticPay APIs. npm install @agenticpay/sdk ``` -## Usage +## Getting started ```ts import { createAgenticPaySDK } from '@agenticpay/sdk'; @@ -28,10 +28,33 @@ const split = await sdk.payments.createSplitConfig({ }); ``` +## Runnable examples + +```bash +cd packages/sdk +export AGENTICPAY_API_KEY=your_key +npm run example:getting-started +npm run example:split +npm run example:errors +``` + +## Documentation + +| Topic | Location | +|-------|----------| +| API reference (TypeDoc) | `npm run docs` → `docs/api/` | +| REST → SDK migration | [`docs/sdk/MIGRATION-FROM-REST.md`](../../docs/sdk/MIGRATION-FROM-REST.md) | +| Error handling | [`docs/sdk/ERROR-HANDLING.md`](../../docs/sdk/ERROR-HANDLING.md) | +| Changelog | [CHANGELOG.md](./CHANGELOG.md) | + ## Features -- Strict TypeScript types -- API error hierarchy +- Strict TypeScript types (see [`@agenticpay/types`](../types/README.md)) +- API error hierarchy with `code` and `details` - Auth helpers and interceptors - Retry/backoff support - Verification, split payments, and refunds APIs + +## OpenAPI client + +For full OpenAPI coverage immediately after spec changes, use the generated client under `backend/docs/api/sdks/typescript/`. This package offers curated, stable ergonomics for common flows. diff --git a/packages/sdk/examples/error-handling.ts b/packages/sdk/examples/error-handling.ts new file mode 100644 index 00000000..e0efe4f8 --- /dev/null +++ b/packages/sdk/examples/error-handling.ts @@ -0,0 +1,25 @@ +/** + * Runnable example: npx tsx examples/error-handling.ts + */ +import { createAgenticPaySDK, RateLimitError, AgenticPayError } from '../src/index.js'; + +async function main() { + const sdk = createAgenticPaySDK({ + baseUrl: process.env.AGENTICPAY_BASE_URL ?? 'http://localhost:3001/api/v1', + apiKey: 'invalid_key_for_demo', + }); + + try { + await sdk.payments.getSplitConfig('nonexistent'); + } catch (err) { + if (err instanceof RateLimitError) { + console.log('Rate limited', err.details); + } else if (err instanceof AgenticPayError) { + console.log('API error:', err.code, err.status, err.message); + } else { + console.log('Unexpected:', err); + } + } +} + +main(); diff --git a/packages/sdk/examples/getting-started.ts b/packages/sdk/examples/getting-started.ts new file mode 100644 index 00000000..1a96743d --- /dev/null +++ b/packages/sdk/examples/getting-started.ts @@ -0,0 +1,24 @@ +/** + * Runnable example: npx tsx examples/getting-started.ts + * Requires AGENTICPAY_API_KEY and optional AGENTICPAY_BASE_URL + */ +import { createAgenticPaySDK } from '../src/index.js'; + +async function main() { + const sdk = createAgenticPaySDK({ + baseUrl: process.env.AGENTICPAY_BASE_URL ?? 'http://localhost:3001/api/v1', + apiKey: process.env.AGENTICPAY_API_KEY ?? 'test_key', + }); + + const health = await fetch( + (process.env.AGENTICPAY_BASE_URL ?? 'http://localhost:3001').replace(/\/api\/v1$/, '') + '/health', + ); + console.log('API health:', health.status); + + console.log('SDK ready:', typeof sdk.payments.createSplitConfig === 'function'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/sdk/examples/split-payment.ts b/packages/sdk/examples/split-payment.ts new file mode 100644 index 00000000..385f6690 --- /dev/null +++ b/packages/sdk/examples/split-payment.ts @@ -0,0 +1,24 @@ +/** + * Runnable example: npx tsx examples/split-payment.ts + */ +import { createAgenticPaySDK } from '../src/index.js'; + +async function main() { + const sdk = createAgenticPaySDK({ + baseUrl: process.env.AGENTICPAY_BASE_URL ?? 'http://localhost:3001/api/v1', + apiKey: process.env.AGENTICPAY_API_KEY!, + }); + + const split = await sdk.payments.createSplitConfig({ + merchantId: 'm_demo', + platformFeePercentage: 2.5, + recipients: [ + { recipientId: 'r1', walletAddress: '0xabc', percentage: 60, minimumThreshold: 1 }, + { recipientId: 'r2', walletAddress: '0xdef', percentage: 37.5, minimumThreshold: 1 }, + ], + }); + + console.log('Split config created:', split); +} + +main().catch(console.error); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 19639790..e0d7f6ea 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -11,7 +11,11 @@ "scripts": { "build": "tsc -p tsconfig.json", "clean": "rimraf dist", - "test": "vitest run --passWithNoTests" + "docs": "typedoc --options typedoc.json", + "test": "vitest run --passWithNoTests", + "example:getting-started": "tsx examples/getting-started.ts", + "example:split": "tsx examples/split-payment.ts", + "example:errors": "tsx examples/error-handling.ts" }, "keywords": [ "agenticpay", @@ -26,7 +30,9 @@ }, "devDependencies": { "@types/node": "^22.5.0", - "typescript": "^5.9.3", + "tsx": "^4.19.0", + "typedoc": "^0.28.0", + "typescript": "~5.7.2", "vitest": "^4.1.1" } } diff --git a/packages/sdk/typedoc.json b/packages/sdk/typedoc.json new file mode 100644 index 00000000..720c63e5 --- /dev/null +++ b/packages/sdk/typedoc.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/index.ts"], + "out": "docs/api", + "tsconfig": "tsconfig.json", + "excludePrivate": true, + "excludeInternal": true, + "readme": "README.md", + "name": "@agenticpay/sdk", + "includeVersion": true, + "categorizeByGroup": true +} diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md new file mode 100644 index 00000000..c198322d --- /dev/null +++ b/packages/types/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [0.1.0] - 2025-05-30 + +### Added +- Shared domain types for payments, projects, and API errors + +[0.1.0]: https://github.com/Smartdevs17/agenticpay/releases/tag/types-v0.1.0 diff --git a/packages/types/README.md b/packages/types/README.md new file mode 100644 index 00000000..b3652ea0 --- /dev/null +++ b/packages/types/README.md @@ -0,0 +1,21 @@ +# @agenticpay/types + +Shared TypeScript types for AgenticPay APIs (payments, projects, pagination, errors). + +## Install + +```bash +npm install @agenticpay/types +``` + +## Usage + +```ts +import type { Payment, ApiError, PaginatedResponse } from '@agenticpay/types'; +``` + +Use with [`@agenticpay/sdk`](../sdk/README.md) for a fully typed client. + +## API reference + +Generated with TypeDoc: `npm run docs` in this package. diff --git a/packages/types/package.json b/packages/types/package.json index 045fe36a..2be25e14 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -5,12 +5,14 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsc", + "docs": "typedoc --options typedoc.json", "lint": "echo 'No lint yet'" }, "dependencies": {}, "devDependencies": { "@agenticpay/typescript-config": "*", "@agenticpay/eslint-config": "*", + "typedoc": "^0.28.0", "typescript": "^5.6.0" } } \ No newline at end of file diff --git a/packages/types/typedoc.json b/packages/types/typedoc.json new file mode 100644 index 00000000..5b29bf22 --- /dev/null +++ b/packages/types/typedoc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/index.ts"], + "out": "docs/api", + "tsconfig": "tsconfig.json", + "name": "@agenticpay/types", + "includeVersion": true +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 28ca0214..9cc350cd 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -160,30 +160,14 @@ run_migrations() { return 0 fi - # ── Prisma (uncomment when Prisma is added) ────────────────────────────── - # if [[ -f "$BACKEND_DIR/prisma/schema.prisma" ]]; then - # log "Running Prisma migrations..." - # (cd "$BACKEND_DIR" && npx prisma migrate deploy) - # log "Prisma migrations complete." - # return 0 - # fi - - # ── TypeORM (uncomment when TypeORM is added) ──────────────────────────── - # if [[ -f "$BACKEND_DIR/src/data-source.ts" ]]; then - # log "Running TypeORM migrations..." - # (cd "$BACKEND_DIR" && npx typeorm-ts-node-commonjs -d src/data-source.ts migration:run) - # log "TypeORM migrations complete." - # return 0 - # fi - - # ── Custom migration script ────────────────────────────────────────────── - # if [[ -f "$BACKEND_DIR/scripts/migrate.sh" ]]; then - # log "Running custom migration script..." - # bash "$BACKEND_DIR/scripts/migrate.sh" - # return 0 - # fi - - info "No database migration tooling detected — skipping (no-op)." + if [[ -f "$BACKEND_DIR/prisma/schema.prisma" ]]; then + log "Running Prisma migrations (db:migrate)..." + (cd "$BACKEND_DIR" && npm run db:generate && npm run db:migrate) + log "Database migrations complete." + return 0 + fi + + warn "No prisma/schema.prisma found — skipping migrations." } # ─── Build ─────────────────────────────────────────────────────────────────── @@ -307,6 +291,11 @@ health_check() { rollback() { section "Rolling back" + if [[ -f "$BACKEND_DIR/prisma/schema.prisma" && "${MIGRATION_ROLLBACK_ON_DEPLOY_FAILURE:-false}" == "true" ]]; then + warn "Attempting database rollback-one (dev/staging only)..." + (cd "$BACKEND_DIR" && npm run db:rollback:one) || warn "DB rollback-one skipped or failed." + fi + if [[ ! -d "$BACKUP_DIR" ]]; then error "No backup found at $BACKUP_DIR — cannot roll back." return 1