diff --git a/packages/api/package.json b/packages/api/package.json index 614d4e8..7e583d8 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -12,19 +12,20 @@ "dependencies": { "@fastify/cors": "^10.0.1", "@fastify/helmet": "^13.0.1", + "@fastify/multipart": "^7.7.3", "@fastify/rate-limit": "^9.0.0", "@fastify/sensible": "^6.0.1", - "@shieldai/db": "workspace:*", - "@shieldai/types": "workspace:*", "@shieldai/correlation": "workspace:*", - "@shieldai/report": "workspace:*", - "fastify": "^5.2.0", "@shieldai/darkwatch": "workspace:*", + "@shieldai/db": "workspace:*", + "@shieldai/monitoring": "workspace:*", + "@shieldai/report": "workspace:*", + "@shieldai/types": "workspace:*", "@shieldai/voiceprint": "workspace:*", - "@shieldai/monitoring": "workspace:*" + "fastify": "^5.2.0" }, "devDependencies": { - "vitest": "^4.1.5", - "@vitest/coverage-v8": "^4.1.5" + "@vitest/coverage-v8": "^4.1.5", + "vitest": "^4.1.5" } } diff --git a/packages/api/src/routes/voiceprint.routes.ts b/packages/api/src/routes/voiceprint.routes.ts index dcdd483..f8df23a 100644 --- a/packages/api/src/routes/voiceprint.routes.ts +++ b/packages/api/src/routes/voiceprint.routes.ts @@ -1,36 +1,65 @@ import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import fastifyMultipart from '@fastify/multipart'; import { voiceEnrollmentService, analysisService, batchAnalysisService, voicePrintEnv, - AnalysisJobStatus, } from '../services/voiceprint'; +interface AuthenticatedRequest extends FastifyRequest { + user?: { id: string; email: string; role: string }; + authType?: 'jwt' | 'api-key' | 'anonymous'; +} + export async function voiceprintRoutes(fastify: FastifyInstance) { + // P1-2 fix: Require authentication on all VoicePrint routes + fastify.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => { + const authReq = request as AuthenticatedRequest; + if (authReq.authType === 'anonymous' || !authReq.user?.id || authReq.user.id === 'anonymous') { + return reply.code(401).send({ error: 'Authentication required' }); + } + }); + + // P1-3 fix: Register multipart for audio file uploads + await fastify.register(fastifyMultipart, { + limits: { + fileSize: voicePrintEnv.ENROLLMENT_MAX_DURATION_SEC > 0 + ? 50 * 1024 * 1024 // 50MB max file size for audio + : 50 * 1024 * 1024, + }, + }); // Enroll a new voice profile fastify.post('/enroll', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; + const authReq = request as AuthenticatedRequest; const userId = authReq.user?.id; if (!userId) { return reply.code(401).send({ error: 'User ID required' }); } - const body = request.body as { - name: string; - audio: Buffer; - }; + // P1-3 fix: Parse multipart form-data for audio upload + let name: string | undefined; + let audioBuffer: Buffer | undefined; - if (!body.name || !body.audio) { - return reply.code(400).send({ error: 'name and audio are required' }); + for await (const part of request.files()) { + if (part.type === 'file') { + audioBuffer = await part.toBuffer(); + name = name || part.filename || 'voice_enrollment'; + } else if (part.fieldname === 'name') { + name = part.value; + } + } + + if (!audioBuffer || audioBuffer.length === 0) { + return reply.code(400).send({ error: 'audio file is required' }); } try { const enrollment = await voiceEnrollmentService.enroll( userId, - body.name, - body.audio + name || 'voice_enrollment', + audioBuffer ); return reply.code(201).send({ enrollment: { @@ -48,7 +77,7 @@ export async function voiceprintRoutes(fastify: FastifyInstance) { // List user's voice enrollments fastify.get('/enrollments', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; + const authReq = request as AuthenticatedRequest; const userId = authReq.user?.id; if (!userId) { @@ -79,7 +108,7 @@ export async function voiceprintRoutes(fastify: FastifyInstance) { // Remove an enrollment fastify.delete('/enrollments/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; + const authReq = request as AuthenticatedRequest; const userId = authReq.user?.id; if (!userId) { @@ -108,27 +137,36 @@ export async function voiceprintRoutes(fastify: FastifyInstance) { // Analyze a single audio file fastify.post('/analyze', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; + const authReq = request as AuthenticatedRequest; const userId = authReq.user?.id; if (!userId) { return reply.code(401).send({ error: 'User ID required' }); } - const body = request.body as { - audio: Buffer; - enrollmentId?: string; - audioUrl?: string; - }; + // P1-3 fix: Parse multipart form-data for audio upload + let audioBuffer: Buffer | undefined; + let enrollmentId: string | undefined; + let audioUrl: string | undefined; - if (!body.audio) { - return reply.code(400).send({ error: 'audio is required' }); + for await (const part of request.files()) { + if (part.type === 'file') { + audioBuffer = await part.toBuffer(); + } else if (part.fieldname === 'enrollmentId') { + enrollmentId = part.value; + } else if (part.fieldname === 'audioUrl') { + audioUrl = part.value; + } + } + + if (!audioBuffer || audioBuffer.length === 0) { + return reply.code(400).send({ error: 'audio file is required' }); } try { - const result = await analysisService.analyze(userId, body.audio, { - enrollmentId: body.enrollmentId, - audioUrl: body.audioUrl, + const result = await analysisService.analyze(userId, audioBuffer, { + enrollmentId, + audioUrl, }); return reply.code(201).send({ analysis: { @@ -147,7 +185,7 @@ export async function voiceprintRoutes(fastify: FastifyInstance) { // Get analysis result by ID fastify.get('/results/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; + const authReq = request as AuthenticatedRequest; const userId = authReq.user?.id; if (!userId) { @@ -174,7 +212,7 @@ export async function voiceprintRoutes(fastify: FastifyInstance) { // Get analysis history fastify.get('/history', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; + const authReq = request as AuthenticatedRequest; const userId = authReq.user?.id; if (!userId) { @@ -207,37 +245,42 @@ export async function voiceprintRoutes(fastify: FastifyInstance) { // Batch analyze multiple audio files fastify.post('/batch', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; + const authReq = request as AuthenticatedRequest; const userId = authReq.user?.id; if (!userId) { return reply.code(401).send({ error: 'User ID required' }); } - const body = request.body as { - files: Array<{ - name: string; - audio: Buffer; - audioUrl?: string; - }>; - enrollmentId?: string; - }; + // P1-3 fix: Parse multipart form-data for multiple audio uploads + const files: Array<{ name: string; buffer: Buffer; audioUrl?: string }> = []; + let enrollmentId: string | undefined; - if (!body.files || body.files.length === 0) { - return reply.code(400).send({ error: 'files array is required' }); + for await (const part of request.files()) { + if (part.type === 'file') { + const buffer = await part.toBuffer(); + files.push({ + name: part.filename || `file_${files.length}`, + buffer, + }); + } else if (part.fieldname === 'enrollmentId') { + enrollmentId = part.value; + } else if (part.fieldname === 'audioUrl') { + if (files.length > 0) { + files[files.length - 1].audioUrl = part.value; + } + } + } + + if (files.length === 0) { + return reply.code(400).send({ error: 'at least one audio file is required' }); } try { const result = await batchAnalysisService.analyzeBatch( userId, - body.files.map((f) => ({ - name: f.name, - buffer: f.audio, - audioUrl: f.audioUrl, - })), - { - enrollmentId: body.enrollmentId, - } + files, + { enrollmentId } ); return reply.code(201).send({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8c7e45..1f3da4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,7 +29,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)) packages/api: dependencies: @@ -39,6 +39,9 @@ importers: '@fastify/helmet': specifier: ^13.0.1 version: 13.0.2 + '@fastify/multipart': + specifier: ^7.7.3 + version: 7.7.3 '@fastify/rate-limit': specifier: ^9.0.0 version: 9.1.0 @@ -75,7 +78,7 @@ importers: version: 4.1.5(vitest@4.1.5) vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)) packages/correlation: dependencies: @@ -187,7 +190,7 @@ importers: version: 4.1.5(vitest@4.1.5) vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)) packages/mobile: dependencies: @@ -262,7 +265,7 @@ importers: version: 4.1.5(vitest@4.1.5) vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)) packages/shared-analytics: dependencies: @@ -360,7 +363,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)) packages/shared-ui: dependencies: @@ -382,7 +385,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)) packages/types: {} @@ -434,7 +437,7 @@ importers: version: 4.1.5(vitest@4.1.5) vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)) services/spamshield: dependencies: @@ -474,7 +477,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)) services/voiceprint: dependencies: @@ -496,7 +499,7 @@ importers: version: 4.1.5(vitest@4.1.5) vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)) packages: @@ -1158,15 +1161,29 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@fastify/accept-negotiator@1.1.0': + resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} + engines: {node: '>=14'} + '@fastify/ajv-compiler@4.0.5': resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + '@fastify/busboy@1.2.1': + resolution: {integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==} + engines: {node: '>=14'} + '@fastify/busboy@3.2.0': resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} '@fastify/cors@10.1.0': resolution: {integrity: sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==} + '@fastify/deepmerge@1.3.0': + resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} + + '@fastify/error@3.4.1': + resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} + '@fastify/error@4.2.0': resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} @@ -1182,15 +1199,30 @@ packages: '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + '@fastify/multipart@7.7.3': + resolution: {integrity: sha512-MG4Gd9FNEXc8qx0OgqoXM10EGO/dN/0iVQ8SrpFMU3d6F6KUfcqD2ZyoQhkm9LWrbiMgdHv5a43x78lASdn5GA==} + '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} '@fastify/rate-limit@9.1.0': resolution: {integrity: sha512-h5dZWCkuZXN0PxwqaFQLxeln8/LNwQwH9popywmDCFdKfgpi4b/HoMH1lluy6P+30CG9yzzpSpwTCIPNB9T1JA==} + '@fastify/send@2.1.0': + resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==} + '@fastify/sensible@6.0.4': resolution: {integrity: sha512-1vxcCUlPMew6WroK8fq+LVOwbsLtX+lmuRuqpcp6eYqu6vmkLwbKTdBWAZwbeaSgCfW4tzUpTIHLLvTiQQ1BwQ==} + '@fastify/static@6.12.0': + resolution: {integrity: sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==} + + '@fastify/swagger-ui@1.10.2': + resolution: {integrity: sha512-f2mRqtblm6eRAFQ3e8zSngxVNEtiYY7rISKQVjPA++ZsWc5WYlPVTb6Bx0G/zy0BIoucNqDr/Q2Vb/kTYkOq1A==} + + '@fastify/swagger@8.15.0': + resolution: {integrity: sha512-zy+HEEKFqPMS2sFUsQU5X0MHplhKJvWeohBwTCkBAJA/GDYGLGUWQaETEhptiqxK7Hs0fQB9B4MDb3pbwIiCwA==} + '@firebase/app-check-interop-types@0.3.2': resolution: {integrity: sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==} @@ -3691,6 +3723,11 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -3777,6 +3814,10 @@ packages: htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -4169,6 +4210,10 @@ packages: json-schema-ref-resolver@3.0.0: resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-resolver@2.0.0: + resolution: {integrity: sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==} + engines: {node: '>=10'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -4454,6 +4499,10 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -4651,6 +4700,9 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + openid-client@5.7.1: resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} @@ -5069,6 +5121,9 @@ packages: resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==} deprecated: Just use Node.js's crypto.timingSafeEqual() + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} @@ -5210,6 +5265,10 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -5227,6 +5286,10 @@ packages: stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + stream-wormhole@1.1.0: + resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==} + engines: {node: '>=4.0.0'} + string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -5305,6 +5368,9 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + text-decoding@1.0.0: + resolution: {integrity: sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -5731,6 +5797,11 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.8.4: + resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -6548,12 +6619,18 @@ snapshots: '@eslint/js@8.57.1': {} + '@fastify/accept-negotiator@1.1.0': {} + '@fastify/ajv-compiler@4.0.5': dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) fast-uri: 3.1.0 + '@fastify/busboy@1.2.1': + dependencies: + text-decoding: 1.0.0 + '@fastify/busboy@3.2.0': {} '@fastify/cors@10.1.0': @@ -6561,6 +6638,10 @@ snapshots: fastify-plugin: 5.1.0 mnemonist: 0.40.0 + '@fastify/deepmerge@1.3.0': {} + + '@fastify/error@3.4.1': {} + '@fastify/error@4.2.0': {} '@fastify/fast-json-stringify-compiler@5.0.3': @@ -6578,6 +6659,20 @@ snapshots: dependencies: dequal: 2.0.3 + '@fastify/multipart@7.7.3': + dependencies: + '@fastify/busboy': 1.2.1 + '@fastify/deepmerge': 1.3.0 + '@fastify/error': 3.4.1 + '@fastify/swagger': 8.15.0 + '@fastify/swagger-ui': 1.10.2 + end-of-stream: 1.4.5 + fastify-plugin: 4.5.1 + secure-json-parse: 2.7.0 + stream-wormhole: 1.1.0 + transitivePeerDependencies: + - supports-color + '@fastify/proxy-addr@5.1.0': dependencies: '@fastify/forwarded': 3.0.1 @@ -6589,6 +6684,14 @@ snapshots: fastify-plugin: 4.5.1 toad-cache: 3.7.0 + '@fastify/send@2.1.0': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.0 + mime: 3.0.0 + '@fastify/sensible@6.0.4': dependencies: '@lukeed/ms': 2.0.2 @@ -6599,6 +6702,33 @@ snapshots: type-is: 2.0.1 vary: 1.1.2 + '@fastify/static@6.12.0': + dependencies: + '@fastify/accept-negotiator': 1.1.0 + '@fastify/send': 2.1.0 + content-disposition: 0.5.4 + fastify-plugin: 4.5.1 + glob: 8.1.0 + p-limit: 3.1.0 + + '@fastify/swagger-ui@1.10.2': + dependencies: + '@fastify/static': 6.12.0 + fastify-plugin: 4.5.1 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.8.4 + + '@fastify/swagger@8.15.0': + dependencies: + fastify-plugin: 4.5.1 + json-schema-resolver: 2.0.0 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.8.4 + transitivePeerDependencies: + - supports-color + '@firebase/app-check-interop-types@0.3.2': {} '@firebase/app-types@0.9.2': {} @@ -8344,7 +8474,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)) '@vitest/expect@4.1.5': dependencies: @@ -8363,13 +8493,13 @@ snapshots: optionalDependencies: vite: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0) - '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0))': + '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4) '@vitest/pretty-format@4.1.5': dependencies: @@ -9000,7 +9130,6 @@ snapshots: end-of-stream@1.4.5: dependencies: once: 1.4.0 - optional: true entities@4.5.0: {} @@ -9529,6 +9658,14 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.9 + once: 1.4.0 + globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -9651,6 +9788,14 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -10276,6 +10421,14 @@ snapshots: dependencies: dequal: 2.0.3 + json-schema-resolver@2.0.0: + dependencies: + debug: 4.4.3 + rfdc: 1.4.1 + uri-js: 4.4.1 + transitivePeerDependencies: + - supports-color + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -10516,8 +10669,7 @@ snapshots: mime@1.6.0: {} - mime@3.0.0: - optional: true + mime@3.0.0: {} mimic-fn@2.1.0: {} @@ -10525,6 +10677,10 @@ snapshots: dependencies: brace-expansion: 1.1.14 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.0 + minimatch@9.0.9: dependencies: brace-expansion: 2.1.0 @@ -10702,6 +10858,8 @@ snapshots: dependencies: mimic-fn: 2.1.0 + openapi-types@12.1.3: {} + openid-client@5.7.1: dependencies: jose: 4.15.9 @@ -11190,6 +11348,8 @@ snapshots: scmp@2.1.0: {} + secure-json-parse@2.7.0: {} + secure-json-parse@4.1.0: {} selderee@0.11.0: @@ -11377,6 +11537,8 @@ snapshots: standard-as-callback@2.1.0: {} + statuses@2.0.1: {} + statuses@2.0.2: {} std-env@4.1.0: {} @@ -11394,6 +11556,8 @@ snapshots: stream-shift@1.0.3: optional: true + stream-wormhole@1.1.0: {} + string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -11473,6 +11637,8 @@ snapshots: glob: 7.2.3 minimatch: 3.1.5 + text-decoding@1.0.0: {} + text-table@0.2.0: {} thread-stream@4.0.0: @@ -11680,7 +11846,7 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.32.0 - vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0): + vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -11693,6 +11859,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 tsx: 4.21.0 + yaml: 2.8.4 vitefu@1.1.3(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)): optionalDependencies: @@ -11727,10 +11894,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)): + vitest@4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -11747,7 +11914,7 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -11844,6 +12011,8 @@ snapshots: yallist@4.0.0: {} + yaml@2.8.4: {} + yargs-parser@21.1.1: {} yargs@17.7.2: