From 5bb4eee5135e1e88067ec6f1d2093df271b3fcd9 Mon Sep 17 00:00:00 2001 From: Oscar Grande Date: Wed, 11 Nov 2026 21:11:44 +0100 Subject: [PATCH 1/8] feat: integrate infrastructure for Firebase authentication --- frontend/package-lock.json | 2140 ++++++++++++++++++++++--- frontend/package.json | 2 + frontend/src/context/AuthContext.ts | 21 + frontend/src/context/AuthProvider.tsx | 37 + frontend/src/lib/firebase.ts | 11 + frontend/src/main.tsx | 11 +- 6 files changed, 2038 insertions(+), 184 deletions(-) create mode 100644 frontend/src/context/AuthContext.ts create mode 100644 frontend/src/context/AuthProvider.tsx create mode 100644 frontend/src/lib/firebase.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b260a60..70a02c7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.3.3", + "backend": "^0.0.0", + "firebase": "^13.2.0", "lucide-react": "^1.64.0", "react": "^19.2.8", "react-dom": "^19.2.8", @@ -669,261 +671,1744 @@ } } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, + "node_modules/@firebase/ai": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-3.0.0.tgz", + "integrity": "sha512-66jJAi6JUmyXna89xaDONmfvupO1wNc3f2BegPwUImagbbpKdVeycE3kMqbU4jiDLL14N5PHzadovvxpaqD2hw==", "license": "Apache-2.0", "dependencies": { - "@humanfs/types": "^0.15.0" + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" }, "engines": { - "node": ">=18.18.0" + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" } }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, + "node_modules/@firebase/analytics": { + "version": "0.10.25", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.25.tgz", + "integrity": "sha512-oDagV3PHeNXBdxemksZCdj+GmUT19eCnTOcLdQePIIovFXlm3zWy3Gjeaokt9Nqx24kc4ljK8lU0XGcGmmKZ1A==", "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.31", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.31.tgz", + "integrity": "sha512-msTlG9REujk5rjtOHNLngiuPmvaWGQte8ugL4/F6ZCMb+92Ue7SgjSEpNxINkM8ZtTrbGS83j5SZkt0r+M1Tjw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.25", + "@firebase/analytics-types": "0.8.5", + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.5.tgz", + "integrity": "sha512-kdnooE7Bis2jEnsqcerRwn/UQpH5D3uvHpku7OdUM9TJN3omlu6iYtbyAQ6XkAJRgJtO0aNf9OOkW8J6D59oCA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.16.5.tgz", + "integrity": "sha512-hVFsDVHCD86RjS+e2IDujnB5oZ57DVKcvhZLs7GVBnFk3SuGA4XkGUma85bFyQkxJ1XZI60Zfj+JRkIkos7omg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" }, "engines": { - "node": ">=18.18.0" + "node": ">=24.12.0" } }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, + "node_modules/@firebase/app-check": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.13.1.tgz", + "integrity": "sha512-l8y3dmnhodXks/APAwx4tWqRl3tk8b9874KF1FJyKg1DIU+kMD0l52iz7S9/MW+eK8k6cjJg0G0iJ5KQAsQpow==", "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, "engines": { - "node": ">=18.18.0" + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.7.tgz", + "integrity": "sha512-fBb/xSMyIKv1nDmccfzz3IAGBzx/cQOxmZai66rJzifb1hMMkztf824Xx7LhpTUe7HNUbXwY90IvJ66fi8q6yg==", "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.13.1", + "@firebase/app-check-types": "0.5.5", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, "engines": { - "node": ">=12.22" + "node": ">=20.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.5.tgz", + "integrity": "sha512-qId34pVZ2CXTmtu4ofW5leiI93DvnOvIcr/5GT7MZPw5WXAi6nZoU+g9cNRgl7K90UJSbK0cXxten4meYMPyZg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.5.tgz", + "integrity": "sha512-+DF4gzFrlwGFyky38o4T/YN/r51l70yCtJ2HTkwmRj8FCMwPjm4hLH4fQbj6BUvzkiFqliBkitFsRpf1/YZkWA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.21.tgz", + "integrity": "sha512-TM5ROeRhbOXj+n1umENLZkjxm5f3myWonM+O7Na4VvCfFKWTH0cmNxjfMjrNoMkvoit4XtewMt4M7jaCAy4enQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.16.5", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=24.12.0" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, + "node_modules/@firebase/app-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.6.tgz", + "integrity": "sha512-yPLahy7Esfu2w/yme3msVK4xTkDXQqq6szfQn8yVOQpCKiT5GVFjqNpLbuz6NkX0WuTwUidkmPewW3r4xkvpeg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/logger": "0.5.2" + } + }, + "node_modules/@firebase/auth": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.13.9.tgz", + "integrity": "sha512-skKWiI33P138PTCl6NDvLecoy/ho6C31/2t/MdKMaWLLN1/ZFT5FaWgQUtif4HWIonk/JMQp8jYmlmUugFvETA==", "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, "engines": { - "node": ">=18.18" + "node": ">=24.12.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } } }, - "node_modules/@inquirer/ansi": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.10.tgz", - "integrity": "sha512-dwrc+1aqVZpHNkNDswXGtn1typ+Pxpo5FHZRyBw1le7onEJUkPUkYOY1FIFC1LvFMYUqqLuWBwElhAmencL7MQ==", - "dev": true, - "license": "MIT", + "node_modules/@firebase/auth-compat": { + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.14.tgz", + "integrity": "sha512-sc6HHIbAt8Ah///K5ufZEpEBBPvstXnqde1VQUME7o6Nh+OKYYniuCxqEvPwtLiwuyyQ8rC4fljymedwvTDtGw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.13.9", + "@firebase/auth-types": "0.13.2", + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" } }, - "node_modules/@inquirer/confirm": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.5.tgz", - "integrity": "sha512-BJ59BOyURiZiAxdr9AuRu38/k+rV+DcTIM2fUF/c/BXrab6H8zMZTTOl3LgkSJH96Xob2vOJxDiBkHiz32Xx2w==", - "dev": true, - "license": "MIT", + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.6.tgz", + "integrity": "sha512-FgwZqDrBqgK0BHI70QTv1v/5wmuDF9f8fsJFvKSxoRlK2PPfSQtZAk2jhXu4pe9BZzFi/e4UYusU/bEQHdf6Qg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.2.tgz", + "integrity": "sha512-OU+miuoSxIWYN7GT291d2ylVJBL3k/OePo7/JDSoQtkFQoEiGBc4AHuMjj/seqFIsHrqnjrAfRCk4pfufoJolQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.5.tgz", + "integrity": "sha512-vuFDcL91Q+2ZuBJkyOh86T4q0B4ffNTDjc/A38tybO56odQABxRTFLTIowCWqAKeIcgo37GowWMVgFF73gD8Qw==", + "license": "Apache-2.0", "dependencies": { - "@inquirer/core": "^12.0.6", - "@inquirer/type": "4.2.1" + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/crashlytics": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@firebase/crashlytics/-/crashlytics-0.1.0.tgz", + "integrity": "sha512-OiFx/LeHbocsQ5+6yMru0l6uMup6jmoeVDQCPYlXZ/u+xDGbO5dY/yh983CmscTPTvUGiON5ytvxHpH7vL8KqA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/util": "1.15.3", + "@opentelemetry/api": "1.9.1", + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/browser-instrumentation": "0.6.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/exporter-logs-otlp-http": "0.220.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.220.0", + "@opentelemetry/instrumentation": "0.220.0", + "@opentelemetry/instrumentation-document-load": "0.65.0", + "@opentelemetry/instrumentation-fetch": "0.220.0", + "@opentelemetry/instrumentation-xml-http-request": "0.220.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.220.0", + "@opentelemetry/sdk-trace-base": "2.10.0", + "@opentelemetry/sdk-trace-web": "2.10.0", + "@opentelemetry/semantic-conventions": "1.43.0", + "tslib": "^2.1.0" + }, + "bin": { + "crashlytics-setconstants": "setconstants.js" }, "peerDependencies": { - "@types/node": ">=18" + "@angular/core": ">= 14.0.0", + "@angular/router": ">= 14.0.0", + "@firebase/app": "0.x", + "@firebase/app-types": "0.x", + "@types/react": ">= 16.8.0", + "next": ">= 13.0.0", + "react": ">= 16.8.0", + "react-router-dom": ">= 6.0.0" }, "peerDependenciesMeta": { - "@types/node": { + "@angular/core": { + "optional": true + }, + "@angular/router": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "react-router-dom": { "optional": true } } }, - "node_modules/@inquirer/core": { - "version": "12.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.6.tgz", - "integrity": "sha512-5yxn2LsJ8Tu4ei2a+yNkmFRhM/cb+JMEsEs/AW6CCOnBnpMy4IjYiPMFyHtYq8weOmlTyqqCx40y0DsD7OsEPw==", - "dev": true, - "license": "MIT", + "node_modules/@firebase/data-connect": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.7.4.tgz", + "integrity": "sha512-su1aGWlzhxb+xtggCUSsufJn1FDa06SBDK71y+fpQ+g2zMhMExik6FUv/odkKzOF/xfWVjabCbWBcjJY3MceDA==", + "license": "Apache-2.0", "dependencies": { - "@inquirer/ansi": "^2.0.10", - "@inquirer/figures": "^2.0.10", - "@inquirer/type": "4.2.1", - "cli-width": "^4.1.0", - "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0" + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.5.tgz", + "integrity": "sha512-/JGpvszLoNXNgzilRXocigGfFF4hbcPA9wN1i1kjJx6oKkXgkHteZYl3lQs1lJX3ETDf6bv7zI15lPMVUp4sAQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.7.tgz", + "integrity": "sha512-lBq9sJm8MnJINKJnkAKSOj2MbC66xGoSVCcGkPtstl+lkoKEBtD46qHLbuDgW/WbLPoKVm17RIN8BxHj+Z2F2w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/database": "1.1.5", + "@firebase/database-types": "1.0.22", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" }, "peerDependencies": { - "@types/node": ">=18" + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" }, "peerDependenciesMeta": { - "@types/node": { + "@firebase/app": { + "optional": true + }, + "@firebase/app-compat": { "optional": true } } }, - "node_modules/@inquirer/figures": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.10.tgz", - "integrity": "sha512-51elrUJV3jZpvD0ygz8+U10Kc6+Lzqln+/tYmnbhy6jo5oXpE6hMCcv+OTBC0enwJXsb3JIuT3VtEinRNCSK2A==", - "dev": true, - "license": "MIT", + "node_modules/@firebase/database-types": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.22.tgz", + "integrity": "sha512-YAZNXsjY9EQQ+pKw/3ax8n5FgolHC7Qew7EY5RceYdl0R2ZP+kCv1O0DkKlcceue8uQCOreHCNN7PWVFpY1Nug==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.6", + "@firebase/util": "1.15.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.19.1.tgz", + "integrity": "sha512-W14adbYBDKvvLNnVIeUjjANvvDNOJiPIc1xq5GfRDBO0y/3At3cJ3CLqY71YMVF88y2eZIrOvlz8qrQUX5y1aA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "@firebase/webchannel-wrapper": "1.0.7", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "re2js": "^2.8.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.17.tgz", + "integrity": "sha512-3DfKwSg4e/mhoMaS965GjEG0+CvEbKZD3UW9wKufIW2LmWglEhXaKyrlSJQJLqEnLBd0+6eNXeeuCF5GKq1kAQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/firestore": "4.19.1", + "@firebase/firestore-types": "3.0.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.5.tgz", + "integrity": "sha512-dbdMAQkMd5dwWc48eupz/Y6/E9ruat3+gY5lhVKscvvT/HnDBEEMzJW38zdKhgdnglZHGk2vUsJMOHAphMHGMA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.14.0.tgz", + "integrity": "sha512-DhuYFr0eMhp+s/PNEk6SiMsYkc00+XVOUeKbrl8MOzQNZI3SKQpqoDR1d+keNDAutwmHRWTUAdXBoVPD+xxVUw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/messaging-interop-types": "0.2.6", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.5.0.tgz", + "integrity": "sha512-T3BDIToESZUHt7438wyKYMkRjKg3m1xAIux5fVpNvTRQlDOFLukWniQWBDhJ2znpU9kxMTR6+e31TVo8P15PZw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/functions": "0.14.0", + "@firebase/functions-types": "0.6.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.5.tgz", + "integrity": "sha512-Zc0pURjthHXzSj54ZivCkzKDSV1r/wIpnmHdhq82q2yFFPVoncG/ZJjnVMiANvQfeww/QnElhzODpfwUucVwdA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.24", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.24.tgz", + "integrity": "sha512-Ui52ey8wHoWqkBbXRKJEKYWylI0JZogZmoLS+o8Anh1bxWtK27ZYjLla1UJAAdC5DCKLJ2qAp0VpJOjg8VOX1g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.24.tgz", + "integrity": "sha512-8M5nlcWwYt881x83COP2odq5vgf+NgwJh+RMd4LRSv8JI1pxwDfgrDJOERrjTgfC9J5Z0vnQX4b1pdN914e0Zw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/installations-types": "0.5.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.5.tgz", + "integrity": "sha512-e9UYcju3puDl1vdrcKIi5dExzHLameOT/Tc61Q48PYwxtsM1NzZh/ikGbdBQTsbRgg0EMZqdPr0/m5ODUBobrg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.2.tgz", + "integrity": "sha512-J2VO4NFTc0xQFrxV1B/lm5balicm9cwuX2acR9Yn41fN8KgUeQFo+VJV222IqW2FPSXKDu9uo5WdQFWf9TPbYg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.13.3.tgz", + "integrity": "sha512-nRXPAp+z0kA3KziJtRY9kktHECvRwaaONzHvislFgllhCwJMscW7icWNQA5IjyP8Uc2+GS/4px8Uz6rx6ftTag==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/messaging-interop-types": "0.2.6", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.30", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.30.tgz", + "integrity": "sha512-m1DPT2ET5x0qYqXLoHwKMAyf1mgZZOhuoE6H6/H4OPsp8zSxMIDEFPaW7MaffPJsCA5J7kHzo+BHVNXK4ZoV+w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/messaging": "0.13.3", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.6.tgz", + "integrity": "sha512-MVzvkKe2V4H2dHu5oOxRfeKQcfTwWmCgnzsC4V1q3ixun5iiL8riCZ2qI35rDhCL4glPGiu0jHxDbpVNuTKfow==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.14.tgz", + "integrity": "sha512-9PH1XEZVHErxGdbXluvGz4Uyjw4W955H8v7Mv9rHIybRrg5F2Ac2tvf5+mSf5EtnxWNmxrD2WLnvMFaZP4sMrQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.27.tgz", + "integrity": "sha512-O/ozTf/EbChN94Pk7bd1eUC0PAC36726AwsaiJyC0bZzSBWfLGSWASGPH5pebUWXQPJtGxIJYRkkbX5l0Qa+Hw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/performance": "0.7.14", + "@firebase/performance-types": "0.2.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.5.tgz", + "integrity": "sha512-PRzOgB+/M+6AKlEkY8a9xy5Ff5SfZPIh4iShXhn2WAcL/euSbK7bdLqWysHduunNJhGFsXa22Ag3ixqL6rQtYg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.9.3.tgz", + "integrity": "sha512-9DbuFNAFeUbVJQZmwLZfdoLRNFWV43V4UqaRpDkaSIdTmVWhzctPeISGkWe4sKTsqpoyldRumquS0gqWZg05fQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.30", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.30.tgz", + "integrity": "sha512-Q4sQKhQ8TPEMOcCUvUBfGeAP1DfcG7FGBfA9yFUn/XRr4vSToY0DTmK28Nh7lgLQYzs6D5Ng6knSoXH+aGn1Tg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/remote-config": "0.9.3", + "@firebase/remote-config-types": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.2.tgz", + "integrity": "sha512-i8k1omVfoAnaT1ZPv2FFjxMZrATYsO/GnFgHEj5+7fAJLzi8wLnGGv1WK06oEAD6ltrWXSO1HzeNyajn/NjMWw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.6.tgz", + "integrity": "sha512-9WjohxpjbfcySjCp8kIbd1ZhtmCQFQLqPtDQ+wmZ5PVXimTDjDsSx/RZjwwK89MjCTXB9fCdmvZkJxplZn4W1A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.6.tgz", + "integrity": "sha512-te0IsqX1bM+OZCJF7jNSKXcMqCA2yYmpshRHjX4axy0lI/P9ZP9GLjEZMVL8wrjUWF4e3cZxMZTbck3Zd5E1hA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/storage": "0.14.6", + "@firebase/storage-types": "0.8.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.5.tgz", + "integrity": "sha512-GEDs5P+rNUfNcS+wxIdOLAHficife2YXtvTJnyi3ssrX11AAtBg+nDUdbYh8vuBzWehVQZPmztGUUnud4L+4yg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.3.tgz", + "integrity": "sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.7.tgz", + "integrity": "sha512-phBFwieDLvkZGYN9CE9ZFNEIoBVksprzsnCzQejCmCHtgwCXReeuRpoEGN9C4EbhONztv8NRV1tau6Rb9pONwQ==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.16.tgz", + "integrity": "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/@grpc/proto-loader/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/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==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.10.tgz", + "integrity": "sha512-dwrc+1aqVZpHNkNDswXGtn1typ+Pxpo5FHZRyBw1le7onEJUkPUkYOY1FIFC1LvFMYUqqLuWBwElhAmencL7MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.5.tgz", + "integrity": "sha512-BJ59BOyURiZiAxdr9AuRu38/k+rV+DcTIM2fUF/c/BXrab6H8zMZTTOl3LgkSJH96Xob2vOJxDiBkHiz32Xx2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.6.tgz", + "integrity": "sha512-5yxn2LsJ8Tu4ei2a+yNkmFRhM/cb+JMEsEs/AW6CCOnBnpMy4IjYiPMFyHtYq8weOmlTyqqCx40y0DsD7OsEPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.10.tgz", + "integrity": "sha512-51elrUJV3jZpvD0ygz8+U10Kc6+Lzqln+/tYmnbhy6jo5oXpE6hMCcv+OTBC0enwJXsb3JIuT3VtEinRNCSK2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@msw/url": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@msw/url/-/url-0.1.2.tgz", + "integrity": "sha512-DdeBLQxQ+ZrtmwEmaj7GQ7lbzPCgbbbyAqOz/I4ulo68hwBpfdNvQJ5bmihJUkAAKOeCmqCIG3OFZGosqFN4mA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mswjs/interceptors": { + "version": "0.45.10", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.45.10.tgz", + "integrity": "sha512-pGZkvxzv6PZAQCpZHuu+TRs/4U8GWlClRToQETz4MHwwnq0dSxS279xRQ2StBVPpwXxTKtXpLHeK6MGSuxh8uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/until": "^3.0.1", + "@types/debug": "^4.1.13", + "debug": "^4.4.3", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "rettime": "^0.11.12" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@open-draft/until": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-3.0.1.tgz", + "integrity": "sha512-s7/9ELP4aP9YZtW7RJaa0Xf3RISaRH9+EFN18FtykJYRHGNrl8f9ymvoNXzjhrjmftFvkQudtuDU9RYhk0xs3A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/browser-instrumentation": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/browser-instrumentation/-/browser-instrumentation-0.6.0.tgz", + "integrity": "sha512-ZYi3WaiCix1D8K6SY5y+i9zvo0lWsnd/tKKnk5PaqWLLxUhVl0gud8G79RJZ3NApDZvQ57PAvNC7povMHIj7jw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.220.0", + "@opentelemetry/core": "^2.9.0", + "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/semantic-conventions": "^1.43.0", + "web-vitals": "^5.3.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.1" + } + }, + "node_modules/@opentelemetry/browser-instrumentation/node_modules/web-vitals": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", + "integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==", + "license": "Apache-2.0" + }, + "node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.220.0.tgz", + "integrity": "sha512-8186thl+pTw64iz/qEEen5oJZoZ/gO73XruChdaGlYdWOdBIQ42r+vHLf6a7vIDqTD4b8ZOoMlyxptanECaI9A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/sdk-logs": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.220.0.tgz", + "integrity": "sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", + "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-document-load": { + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.65.0.tgz", + "integrity": "sha512-xAhNXUIywjqmaSkpimsuUpRr0QYebMyTn0IJS9wFWYVLyH+2Q5IYWYys4IrT60foeyf2gyFmRBQyHupblNdaMA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/sdk-trace-web": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.23.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fetch/-/instrumentation-fetch-0.220.0.tgz", + "integrity": "sha512-9LW+zgvKK6kqojB0GVqbOCRGBUWiatHPmRTusjSVdwYCImIgJRrJTKu6WhjHdejJSjRS/mGsw/w4ETZBqav3kQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/instrumentation": "0.220.0", + "@opentelemetry/sdk-trace-web": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", + "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.9.0.tgz", + "integrity": "sha512-LS4XlzOK3e6YYdt84m15AmRR04121rKipmifi4XFZToH1h75f7F2bzHLbB1NMgIEYJ0jSKYm4VsK5mws/5kfTQ==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/sdk-trace-base": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-xml-http-request": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-xml-http-request/-/instrumentation-xml-http-request-0.220.0.tgz", + "integrity": "sha512-9FTY+kT3z5D8f7dC+Tubd4oJTu6tOGlQmws5J8e7DLMeIUwpWmLquRFV2BuUShiAPxTMGqqsZnuT3CVVbTu0JA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/instrumentation": "0.220.0", + "@opentelemetry/sdk-trace-web": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-xml-http-request/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-xml-http-request/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-xml-http-request/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", + "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-xml-http-request/node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.9.0.tgz", + "integrity": "sha512-LS4XlzOK3e6YYdt84m15AmRR04121rKipmifi4XFZToH1h75f7F2bzHLbB1NMgIEYJ0jSKYm4VsK5mws/5kfTQ==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/sdk-trace-base": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", + "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-transformer": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", + "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-logs": "0.220.0", + "@opentelemetry/sdk-metrics": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", + "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, - "node_modules/@inquirer/type": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", - "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", - "dev": true, - "license": "MIT", + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", + "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, "engines": { - "node": ">=6.0.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@keyv/bigmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", - "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", - "dev": true, - "license": "MIT", + "node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.10.0.tgz", + "integrity": "sha512-6WqXdWSlBH0GNjDhv1gg6SLVtMtIBVhLaWqd1hiKSXS8meXXP13sVPQoYFHz1uVo0feUTUCdP9vMS54KflisAw==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0" }, "engines": { - "node": ">= 18" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "keyv": "^5.6.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@msw/url": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@msw/url/-/url-0.1.2.tgz", - "integrity": "sha512-DdeBLQxQ+ZrtmwEmaj7GQ7lbzPCgbbbyAqOz/I4ulo68hwBpfdNvQJ5bmihJUkAAKOeCmqCIG3OFZGosqFN4mA==", - "dev": true, - "license": "MIT" + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } }, - "node_modules/@mswjs/interceptors": { - "version": "0.45.10", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.45.10.tgz", - "integrity": "sha512-pGZkvxzv6PZAQCpZHuu+TRs/4U8GWlClRToQETz4MHwwnq0dSxS279xRQ2StBVPpwXxTKtXpLHeK6MGSuxh8uA==", - "dev": true, - "license": "MIT", + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", "dependencies": { - "@open-draft/until": "^3.0.1", - "@types/debug": "^4.1.13", - "debug": "^4.4.3", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "rettime": "^0.11.12" + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=22" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@open-draft/until": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-3.0.1.tgz", - "integrity": "sha512-s7/9ELP4aP9YZtW7RJaa0Xf3RISaRH9+EFN18FtykJYRHGNrl8f9ymvoNXzjhrjmftFvkQudtuDU9RYhk0xs3A==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/kettanaito" + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" } }, "node_modules/@oxc-project/types": { @@ -935,6 +2420,63 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.15", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", @@ -1872,7 +3414,6 @@ "version": "24.19.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz", "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": ">=7.24.0 <7.24.7" @@ -2273,9 +3814,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -2314,6 +3853,12 @@ "node": ">=12" } }, + "node_modules/backend": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/backend/-/backend-0.0.0.tgz", + "integrity": "sha512-Fq2aG5+zmmsKv2Dhm3ijAU5spnKOb5ldJlnnC/Vhk6n8In6zaq9eCPBMRiz2j94P/r84QEaBmtwh9tjaDPiQqg==", + "license": "BSD" + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2439,6 +3984,12 @@ "node": ">=18" } }, + "node_modules/cjs-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.3.0.tgz", + "integrity": "sha512-lmNyBzi6iYiqyrExTctfmjd8tInx+dHu9PBomTfH5RXS53c27JMFJgzNv5TyAnm3PgGf+MLy0+Ws41/HlVxdKQ==", + "license": "MIT" + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -2482,6 +4033,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2564,7 +4133,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2670,7 +4238,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2942,6 +4509,18 @@ "fast-string-width": "^3.0.2" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2986,6 +4565,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-13.2.0.tgz", + "integrity": "sha512-29ye5F+viPTv48mcljy84ZnwnrYmTTIULrWf3P113SfeTB/USIUZTs8gREreN5PmndQdKs3D06KQUPJIrFeTGw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "3.0.0", + "@firebase/analytics": "0.10.25", + "@firebase/analytics-compat": "0.2.31", + "@firebase/app": "0.16.5", + "@firebase/app-check": "0.13.1", + "@firebase/app-check-compat": "0.4.7", + "@firebase/app-compat": "0.5.21", + "@firebase/app-types": "0.9.6", + "@firebase/auth": "1.13.9", + "@firebase/auth-compat": "0.6.14", + "@firebase/crashlytics": "0.1.0", + "@firebase/data-connect": "0.7.4", + "@firebase/database": "1.1.5", + "@firebase/database-compat": "2.1.7", + "@firebase/firestore": "4.19.1", + "@firebase/firestore-compat": "0.4.17", + "@firebase/functions": "0.14.0", + "@firebase/functions-compat": "0.5.0", + "@firebase/installations": "0.6.24", + "@firebase/installations-compat": "0.2.24", + "@firebase/messaging": "0.13.3", + "@firebase/messaging-compat": "0.2.30", + "@firebase/performance": "0.7.14", + "@firebase/performance-compat": "0.2.27", + "@firebase/remote-config": "0.9.3", + "@firebase/remote-config-compat": "0.2.30", + "@firebase/storage": "0.14.6", + "@firebase/storage-compat": "0.4.6", + "@firebase/util": "1.15.3" + } + }, "node_modules/flat-cache": { "version": "6.1.23", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", @@ -3033,7 +4649,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -3145,6 +4760,18 @@ "node": "^22.13.0 || >=24.0.0" } }, + "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==", + "license": "MIT" + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3155,6 +4782,26 @@ "node": ">= 4" } }, + "node_modules/import-in-the-middle": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.6.0.tgz", + "integrity": "sha512-xRU7hI24S2FhucXtuZXn8EpcOjfVP6iiEQyR0USayPeaOroEmSsNlqnl95866iRdRtkHrd6Q/Z4D14R60au0+A==", + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^3.0.4", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/import-in-the-middle/node_modules/es-module-lexer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", + "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", + "license": "MIT" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3185,6 +4832,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3614,6 +5270,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3692,11 +5360,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/msw": { @@ -3966,6 +5639,29 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3996,6 +5692,15 @@ "dev": true, "license": "MIT" }, + "node_modules/re2js": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-2.10.0.tgz", + "integrity": "sha512-/P6FNnLbHjXJMRF1dPoZoVTyHvFcuVhbDpzml9DPMlbeErYlZ18GueAaG9GhYhAm/9AN7i7Y6crmDrLX0DeJ4Q==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", @@ -4039,6 +5744,15 @@ "node": ">=8" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "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", @@ -4049,6 +5763,19 @@ "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", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/rettime": { "version": "0.11.12", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.12.tgz", @@ -4089,6 +5816,26 @@ "@rolldown/binding-win32-x64-msvc": "1.2.15" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -4373,6 +6120,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz", + "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4454,7 +6207,6 @@ "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "devOptional": true, "license": "MIT" }, "node_modules/until-async": { @@ -4691,6 +6443,12 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -4701,6 +6459,29 @@ "node": ">=20" } }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-mimetype": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", @@ -4835,7 +6616,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" diff --git a/frontend/package.json b/frontend/package.json index ac94c40..4f8a4d8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,8 @@ }, "dependencies": { "@tailwindcss/vite": "^4.3.3", + "backend": "^0.0.0", + "firebase": "^13.2.0", "lucide-react": "^1.64.0", "react": "^19.2.8", "react-dom": "^19.2.8", diff --git a/frontend/src/context/AuthContext.ts b/frontend/src/context/AuthContext.ts new file mode 100644 index 0000000..ade226e --- /dev/null +++ b/frontend/src/context/AuthContext.ts @@ -0,0 +1,21 @@ +import { createContext, useContext } from "react"; +import type { User } from "firebase/auth"; + +type AuthContextValue = { + user: User | null; + loading: boolean; + login: () => Promise; + logout: () => Promise; +}; + +export const AuthContext = createContext(null); + +export function useAuth() { + const context = useContext(AuthContext); + + if (!context) { + throw new Error("useAuth must be used inside AuthProvider"); + } + + return context; +} \ No newline at end of file diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx new file mode 100644 index 0000000..2f763fa --- /dev/null +++ b/frontend/src/context/AuthProvider.tsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from "react"; +import type { ReactNode } from "react"; +import { + GoogleAuthProvider, + onAuthStateChanged, + signInWithPopup, + signOut, +} from "firebase/auth"; +import type { User } from "firebase/auth"; +import { auth } from "@/lib/firebase"; +import { AuthContext } from "./AuthContext"; + +export default function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + return onAuthStateChanged(auth, (currentUser) => { + setUser(currentUser); + setLoading(false); + }); + }, []); + + async function login() { + await signInWithPopup(auth, new GoogleAuthProvider()); + } + + async function logout() { + await signOut(auth); + } + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/frontend/src/lib/firebase.ts b/frontend/src/lib/firebase.ts new file mode 100644 index 0000000..ef086d0 --- /dev/null +++ b/frontend/src/lib/firebase.ts @@ -0,0 +1,11 @@ +import { initializeApp } from "firebase/app"; +import { getAuth } from "firebase/auth"; + +const app = initializeApp({ + apiKey: import.meta.env.VITE_FIREBASE_API_KEY, + authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, + projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, + appId: import.meta.env.VITE_FIREBASE_APP_ID, +}); + +export const auth = getAuth(app); \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index d1778d6..77613fc 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,12 +2,15 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./App.tsx"; -import BuildProvider from './context/BuildProvider' +import BuildProvider from "./context/BuildProvider"; +import AuthProvider from "./context/AuthProvider"; createRoot(document.getElementById("root")!).render( - - - + + + + + , ); From 70c7863dd17ed54e81fd00c25d6cef0d0bd782dd Mon Sep 17 00:00:00 2001 From: Oscar Grande Date: Fri, 13 Nov 2026 19:53:39 +0100 Subject: [PATCH 2/8] Add Firebase token verification to GraphQL --- backend/server/package-lock.json | 1355 +++++++++++++++++++++++++++++- backend/server/package.json | 1 + backend/server/src/auth.ts | 31 + backend/server/src/firebase.ts | 13 + backend/server/src/index.ts | 13 +- backend/server/src/resolvers.ts | 2 + backend/server/src/schema.ts | 1 + 7 files changed, 1402 insertions(+), 14 deletions(-) create mode 100644 backend/server/src/auth.ts create mode 100644 backend/server/src/firebase.ts diff --git a/backend/server/package-lock.json b/backend/server/package-lock.json index 9b56ec3..4ebd591 100644 --- a/backend/server/package-lock.json +++ b/backend/server/package-lock.json @@ -14,6 +14,7 @@ "cors": "^2.8.6", "dotenv": "^18.0.7", "express": "^5.3.0", + "firebase-admin": "^14.5.0", "graphql": "^16.14.2", "pg": "^8.23.1" }, @@ -875,6 +876,226 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@fastify/busboy": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.5.tgz", + "integrity": "sha512-y8MTAeBlEQUNVnskmZQwpseZN3EgCksQNOULlfxWY6o6VEqh01+oExMgbM7gN6YLQ8pNDYeweNPKJRt8jQm2ZQ==", + "license": "MIT" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.5.tgz", + "integrity": "sha512-qId34pVZ2CXTmtu4ofW5leiI93DvnOvIcr/5GT7MZPw5WXAi6nZoU+g9cNRgl7K90UJSbK0cXxten4meYMPyZg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.6.tgz", + "integrity": "sha512-yPLahy7Esfu2w/yme3msVK4xTkDXQqq6szfQn8yVOQpCKiT5GVFjqNpLbuz6NkX0WuTwUidkmPewW3r4xkvpeg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/logger": "0.5.2" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.6.tgz", + "integrity": "sha512-FgwZqDrBqgK0BHI70QTv1v/5wmuDF9f8fsJFvKSxoRlK2PPfSQtZAk2jhXu4pe9BZzFi/e4UYusU/bEQHdf6Qg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/component": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.5.tgz", + "integrity": "sha512-vuFDcL91Q+2ZuBJkyOh86T4q0B4ffNTDjc/A38tybO56odQABxRTFLTIowCWqAKeIcgo37GowWMVgFF73gD8Qw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.5.tgz", + "integrity": "sha512-/JGpvszLoNXNgzilRXocigGfFF4hbcPA9wN1i1kjJx6oKkXgkHteZYl3lQs1lJX3ETDf6bv7zI15lPMVUp4sAQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.7.tgz", + "integrity": "sha512-lBq9sJm8MnJINKJnkAKSOj2MbC66xGoSVCcGkPtstl+lkoKEBtD46qHLbuDgW/WbLPoKVm17RIN8BxHj+Z2F2w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/database": "1.1.5", + "@firebase/database-types": "1.0.22", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + }, + "peerDependenciesMeta": { + "@firebase/app": { + "optional": true + }, + "@firebase/app-compat": { + "optional": true + } + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.22.tgz", + "integrity": "sha512-YAZNXsjY9EQQ+pKw/3ax8n5FgolHC7Qew7EY5RceYdl0R2ZP+kCv1O0DkKlcceue8uQCOreHCNN7PWVFpY1Nug==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.6", + "@firebase/util": "1.15.3" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.2.tgz", + "integrity": "sha512-J2VO4NFTc0xQFrxV1B/lm5balicm9cwuX2acR9Yn41fN8KgUeQFo+VJV222IqW2FPSXKDu9uo5WdQFWf9TPbYg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.3.tgz", + "integrity": "sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-9.4.1.tgz", + "integrity": "sha512-arD6dY7H8G1647OqxHmgxb3m1IYAAoVDm9ev/MW1meQRl1XuaNP8j/aGT5HPHTeaV/8AHZVGeXwOGTovWvIKIA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@google-cloud/firestore-api": "^0.7.0", + "@opentelemetry/api": "^1.9.0", + "fast-deep-equal": "^3.1.3", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^6.12.1", + "protobufjs": "^7.5.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/firestore-api": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore-api/-/firestore-api-0.7.1.tgz", + "integrity": "sha512-CM6Xm8P/eU59AgH94whcJxA4jaUUgH8Lj4Ki425yFlICEsOaw7Sc6XuezvrZGoXec+W5iVsQgPrZHnwF5g98Kg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "google-gax": "^6.0.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@google-cloud/paginator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-7.2.0.tgz", + "integrity": "sha512-l+x6YObHGTuqBCMvC0SXo3Cnyqg4FkWWKYRptf46PACHJ5GAUuhsd2b3wphgGrg1ybs/LAb+q5MnUTuG/3+v7w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-6.2.1.tgz", + "integrity": "sha512-PV9oy7VpQRXLuqKuU5RSOHFQyWgvw2sT9MoTGykja9j6c9pv5+ttKTJiFsccu500YYIGiz8JCOsb60PfKmiV/w==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-6.2.1.tgz", + "integrity": "sha512-/+RvLJ5u+WhGay6Z3aY6OApDQmNqq1427Mmt7Eov9kYp69gdUiGSwFnygeTxfz69XBtYZLAnWikGMIoFXtKt+A==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/storage": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-8.4.0.tgz", + "integrity": "sha512-M3kA6UA2Tjp5aznCrgKcm9CzOjiQyRMnquqalqXAeS5wvkqqZxN8wpNjjHafOSYs6lQ82U7f2i8we4eTfIrpkA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^7.0.1", + "@google-cloud/projectify": "^6.0.1", + "@google-cloud/promisify": "^6.0.1", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^5.3.4", + "gaxios": "^7.1.4", + "google-auth-library": "^11.0.0", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^9.0.1", + "teeny-request": "^11.0.1" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@graphql-tools/merge": { "version": "9.2.7", "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.2.7.tgz", @@ -935,6 +1156,46 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.7.tgz", + "integrity": "sha512-W4W/fcBMGRVtG6dW2c7cw2mnpS3hvbBd+oQXjh2iYFTZDk1JJmcNqkM5KSaPubQpamlUUurrlb7G1s9NJeERmQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1001,6 +1262,17 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@keyv/bigmap": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", @@ -1025,6 +1297,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@nodable/entities": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.1.0.tgz", + "integrity": "sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/@opentelemetry/api": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.10.0.tgz", + "integrity": "sha512-lMkyes8iHrG+mk6WH7X70MdCLI2RS0FwNBY1PyQ8UemaijE2m5mv5A0Niy3HtbYTFrgkUEQjFMXmSAk2hQDuPQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1172,17 +1467,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.6.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~8.9.0" @@ -1477,6 +1787,19 @@ "node": ">=22.15.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1513,6 +1836,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -1530,6 +1862,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -1564,6 +1935,35 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -1614,6 +2014,12 @@ "node": "20 || >=22" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1684,6 +2090,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true + }, "node_modules/content-disposition": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz", @@ -1768,6 +2209,15 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1847,12 +2297,41 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1862,6 +2341,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==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1934,6 +2423,16 @@ "@esbuild/win32-x64": "0.28.2" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2133,6 +2632,16 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/express/-/express-5.3.0.tgz", @@ -2189,11 +2698,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "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", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { @@ -2210,6 +2724,59 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-builder": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", + "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.12.0.tgz", + "integrity": "sha512-/pXxRBtwxY9w8z7YbgsvIPPcPvFDsTbIkWwTQJhzNJPokj+TbN6ftmcv4j/pEQDvOCNnKGDzuk5Bo2oogIOiFg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@nodable/entities": "^3.1.0", + "fast-xml-builder": "^1.3.1", + "is-unsafe": "^2.0.3", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.5.1", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2228,6 +2795,29 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "11.1.5", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", @@ -2276,6 +2866,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase-admin": { + "version": "14.5.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-14.5.0.tgz", + "integrity": "sha512-DElmgmaH9aPXu2dWXGQiDoan33UfTnbfAAKaue4fwEZxmpdaNrTk5DmuFn+vVDki2KZ6TiaC0IHRWcEzcDxB3Q==", + "license": "Apache-2.0", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^2.1.4", + "@firebase/database-types": "^1.0.20", + "fast-deep-equal": "^3.1.1", + "google-auth-library": "^11.1.0", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^4.0.1" + }, + "engines": { + "node": ">=22" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^9.1.0", + "@google-cloud/storage": "^8.1.0" + } + }, "node_modules/flat-cache": { "version": "6.1.23", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", @@ -2310,6 +2922,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2352,6 +2976,66 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT", + "optional": true + }, + "node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.2.3.tgz", + "integrity": "sha512-d2OI/dJJ3IIaeOz3c9jvr5j5G5beI5fQzbgilNiWhDKGwvJN9o10Qub70NkbF6PNJSUoNKZ77JKYGz0Fs4+9Lg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^8.3.1", + "google-logging-utils": "^2.1.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.2.tgz", + "integrity": "sha512-7VsaTExxjbDv2ZK1YuC4jbgfN33NI/6/7zdYWUHB6mcg51v4tCBZe+xzjqPzOpVzI+C5k1Kk9OQ80WVrXAHOqw==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "optional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2402,6 +3086,69 @@ "node": ">=10.13.0" } }, + "node_modules/google-auth-library": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.3.4.tgz", + "integrity": "sha512-RaYCDoeYIIFTRgH8hesSxqLrmtC2l+DB3cSCS/SN1TPGjI2VpPdJmF3bfRLUn8VvP/4I2Af9fsktw2f9SvVSVg==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^8.3.1", + "gcp-metadata": "^9.2.1", + "google-logging-utils": "^2.1.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-auth-library/node_modules/gaxios": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.2.tgz", + "integrity": "sha512-7VsaTExxjbDv2ZK1YuC4jbgfN33NI/6/7zdYWUHB6mcg51v4tCBZe+xzjqPzOpVzI+C5k1Kk9OQ80WVrXAHOqw==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-gax": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-6.17.0.tgz", + "integrity": "sha512-qitOd0t2mqRFaP2KGRVL+/refKafCVwbXQVodICQX93x/WbKyopMz0r5mdBsEjwctQ22ITZyrQeabRddpFeHXQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "@opentelemetry/api": "^1.9.0", + "duplexify": "^4.1.3", + "google-auth-library": "^11.3.1", + "google-logging-utils": "^2.1.0", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^4.1.0", + "protobufjs": "^7.5.4", + "retry-request": "^9.1.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-logging-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.2.0.tgz", + "integrity": "sha512-Q6HuSEyoaxi0AgqUrOYb2gc77fDl4QugRoHpreI8VpW/VzPsoKyxjL8tGKLmZpWlvC1uVBXM85tykoQpCGBF5g==", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2494,6 +3241,23 @@ "dev": true, "license": "MIT" }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -2514,6 +3278,39 @@ "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==", + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", @@ -2587,6 +3384,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2621,6 +3428,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unsafe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.3.tgz", + "integrity": "sha512-ZBllczCCO267vgdFb6Tq3pv+LhHaCiGMdr+ZsuL+OopruAg7S+3ZDs1vD0RPC0uSrkjzbraxaGjtX1YBxjPeFQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -2634,6 +3454,24 @@ "dev": true, "license": "ISC" }, + "node_modules/jose": { + "version": "6.2.17", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.17.tgz", + "integrity": "sha512-wOuK9V1yujQFH5S0ZWmLcDZN7+IKh9bUCC2ZPCf2HDlol3WWHy+PPyNjEI75FXRLMp3vDKQcupP59xhVU95hcg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2641,12 +3479,72 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-4.1.0.tgz", + "integrity": "sha512-sbkByqyATKYJP5F4RXj03N5TUNC0QLTjCAZvwTzC4BwJZ8e0/cWxN8YROnyUth2g1/ONWi4eSFHeu6oYalrc3Q==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^6.1.3", + "limiter": "^1.1.5", + "lru-cache": "^11.0.0", + "lru-memoizer": "^3.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >= 23.0.0" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } }, "node_modules/keyv": { "version": "5.6.0", @@ -2672,6 +3570,11 @@ "node": ">= 0.8.0" } }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2688,6 +3591,61 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", + "optional": true + }, + "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==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -2722,6 +3680,16 @@ "node": "20 || >=22" } }, + "node_modules/lru-memoizer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-3.0.0.tgz", + "integrity": "sha512-m83w/cYXLdUIboKSPxzPAGfYnk+vqeDYXuoSrQRw1q+yVEd8IXhvMufN8Q5TIPe7e2jyX4SRNrDJI2Skw1yznQ==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^11.0.1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2756,6 +3724,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -2839,6 +3820,44 @@ "url": "https://opencollective.com/express" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2848,6 +3867,16 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -2903,7 +3932,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -2950,6 +3979,22 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3146,6 +4191,50 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/proto3-json-serializer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-4.1.0.tgz", + "integrity": "sha512-Z0Blk2BTZTPp9ZXOWnRG/P3bkEs53FtMv59sl4xU44xcDdYhKlVO/AqAHTYXAlW9HdislqQxjSornP3457wkag==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "protobufjs": "^7.5.4" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true + }, "node_modules/proxy-addr": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", @@ -3237,6 +4326,31 @@ "node": ">= 0.10" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -3246,6 +4360,19 @@ "node": ">= 4" } }, + "node_modules/retry-request": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-9.1.0.tgz", + "integrity": "sha512-FILyVbis+DTo3KRbtKceqpj1595wIF+XQFW/+AVOBdCQmxwNDgcNXXr2FNM0Ej2qD7zVXbJGfH7lo4jB0wwhKA==", + "license": "MIT", + "optional": true, + "dependencies": { + "extend": "^3.0.2" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -3292,7 +4419,6 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3502,6 +4628,100 @@ "node": ">= 0.8" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.5.1.tgz", + "integrity": "sha512-UM4nTLWzQiDBseVxUGxN0wJzIk0UU89Qi7pKXz9qISo+lhviJikTuikBsK/oFRlpvYHvSdz0TVoLyQ+sEpYwxA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true + }, + "node_modules/teeny-request": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-11.1.1.tgz", + "integrity": "sha512-I5OLOkPN3QDN2l1H/eGueuKaq32CdAcXt91WOF/bOfadtZD1WQ8/Fk28WsqDcGjIyhAW/GQDagYhVEjhuZUFyQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -3680,7 +4900,6 @@ "version": "8.9.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -3702,6 +4921,13 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3711,6 +4937,38 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", @@ -3767,12 +5025,46 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "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==", "license": "ISC" }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -3782,11 +5074,50 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/backend/server/package.json b/backend/server/package.json index aa8dbed..4e2952f 100644 --- a/backend/server/package.json +++ b/backend/server/package.json @@ -34,6 +34,7 @@ "cors": "^2.8.6", "dotenv": "^18.0.7", "express": "^5.3.0", + "firebase-admin": "^14.5.0", "graphql": "^16.14.2", "pg": "^8.23.1" } diff --git a/backend/server/src/auth.ts b/backend/server/src/auth.ts new file mode 100644 index 0000000..80e2db9 --- /dev/null +++ b/backend/server/src/auth.ts @@ -0,0 +1,31 @@ +import { GraphQLError } from "graphql"; +import { firebaseAuth } from "./firebase.js"; + +export type AuthContext = { + uid: string | null; +}; + +export async function createAuthContext( + authorization?: string, +): Promise { + if (!authorization) { + return { uid: null }; + } + + const match = /^Bearer (\S+)$/i.exec(authorization); + + if (!match) { + throw new GraphQLError("Invalid authorization header", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } + + try { + const token = await firebaseAuth.verifyIdToken(match[1]!); + return { uid: token.uid }; + } catch { + throw new GraphQLError("Invalid or expired login token", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } +} \ No newline at end of file diff --git a/backend/server/src/firebase.ts b/backend/server/src/firebase.ts new file mode 100644 index 0000000..0f56fa2 --- /dev/null +++ b/backend/server/src/firebase.ts @@ -0,0 +1,13 @@ +import "dotenv/config"; +import { initializeApp } from "firebase-admin/app"; +import { getAuth } from "firebase-admin/auth"; + +const projectId = process.env.FIREBASE_PROJECT_ID; + +if (!projectId) { + throw new Error("Missing FIREBASE_PROJECT_ID in backend/server/.env"); +} + +const app = initializeApp({ projectId }); + +export const firebaseAuth = getAuth(app); \ No newline at end of file diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 52eee5f..9b64a9f 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -7,9 +7,11 @@ import { config } from "./config.js"; import { pool } from "./db.js"; import { resolvers } from "./resolvers.js"; import { typeDefs } from "./schema.js"; +import { createAuthContext } from "./auth.js"; +import type { AuthContext } from "./auth.js"; const app = express(); -const apollo = new ApolloServer({ typeDefs, resolvers }); +const apollo = new ApolloServer({ typeDefs, resolvers }); await apollo.start(); app.use( @@ -32,7 +34,14 @@ app.get("/health", async (_req, res) => { } }); -app.use("/graphql", express.json(), expressMiddleware(apollo)); +app.use( + "/graphql", + express.json(), + expressMiddleware(apollo, { + context: async ({ req }) => + createAuthContext(req.headers.authorization), + }), +); app.listen(config.port, () => { console.log( diff --git a/backend/server/src/resolvers.ts b/backend/server/src/resolvers.ts index 85d351d..f760ee8 100644 --- a/backend/server/src/resolvers.ts +++ b/backend/server/src/resolvers.ts @@ -1,6 +1,7 @@ import { GraphQLError, GraphQLScalarType } from "graphql"; import path from "node:path"; import { pool } from "./db.js"; +import type { AuthContext } from "./auth.js"; const MAX_PAGE_SIZE = 100; @@ -421,6 +422,7 @@ export const resolvers = { JSON: JSONScalar, Mutation: { publishBuild }, Query: { + me: (_: unknown, __: unknown, context: AuthContext) => context.uid, parts, part, manufacturers, builds: async (_: unknown, { limit = 20, offset = 0 }: { limit?: number | null; offset?: number | null }) => { if (limit == null || offset == null || limit < 1 || limit > 100 || offset < 0) { diff --git a/backend/server/src/schema.ts b/backend/server/src/schema.ts index 728803a..77ff736 100644 --- a/backend/server/src/schema.ts +++ b/backend/server/src/schema.ts @@ -116,6 +116,7 @@ export const typeDefs = /* GraphQL */ ` builds(limit: Int = 20, offset: Int = 0): [Build!]! build(id: ID!): Build + me: ID "Distinct manufacturers, optionally only those that have parts in the given category" manufacturers(category: Category): [String!]! From b3bdd26e8577c0a692abf87a0fbc77a04d1dd4d7 Mon Sep 17 00:00:00 2001 From: Oscar Grande Date: Fri, 13 Nov 2026 19:54:01 +0100 Subject: [PATCH 3/8] Connect login UI and authenticated GraphQL requests --- frontend/src/api/graphql.ts | 33 ++++++++++++++ frontend/src/components/AuthButton.tsx | 59 ++++++++++++++++++++++++++ frontend/src/components/NavBar.tsx | 4 ++ 3 files changed, 96 insertions(+) create mode 100644 frontend/src/api/graphql.ts create mode 100644 frontend/src/components/AuthButton.tsx diff --git a/frontend/src/api/graphql.ts b/frontend/src/api/graphql.ts new file mode 100644 index 0000000..e699333 --- /dev/null +++ b/frontend/src/api/graphql.ts @@ -0,0 +1,33 @@ +import { auth } from "@/lib/firebase"; + +export async function graphql( + query: string, + variables: Record = {}, +): Promise { + await auth.authStateReady(); + const token = await auth.currentUser?.getIdToken(); + + const response = await fetch("/api/graphql", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ query, variables }), + }); + + const result: { + data?: T; + errors?: { message: string }[]; + } = await response.json(); + + if (result.errors?.length) { + throw new Error(result.errors.map((error) => error.message).join(", ")); + } + + if (!response.ok || !result.data) { + throw new Error(`Request failed (HTTP ${response.status}).`); + } + + return result.data; +} \ No newline at end of file diff --git a/frontend/src/components/AuthButton.tsx b/frontend/src/components/AuthButton.tsx new file mode 100644 index 0000000..613bec1 --- /dev/null +++ b/frontend/src/components/AuthButton.tsx @@ -0,0 +1,59 @@ +import { useState } from "react"; +import { useAuth } from "@/context/AuthContext"; +import { graphql } from "@/api/graphql"; + +export default function AuthButton() { + const { user, loading, login, logout } = useAuth(); + const [pending, setPending] = useState(false); + const [error, setError] = useState(""); + + async function handleClick() { + setPending(true); + setError(""); + + try { + if (user) await logout(); + else await login(); + } catch { + setError("Could not complete sign-in or sign-out. Please try again."); + } finally { + setPending(false); + } + } + async function testBackend() { + try { + const result = await graphql<{ me: string | null }>("{ me }"); + alert(`Backend user: ${result.me ?? "Not signed in"}`); + } catch (error) { + alert(error instanceof Error ? error.message : "Request failed"); + } + } + + return ( +
+ {user && {user.displayName}} + + + + {error &&

{error}

} +
+ ); +} diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index 012edd1..3d10899 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { useBuild } from "@/context/BuildContext"; +import AuthButton from "@/components/AuthButton"; type NavBarProps = { search?: string; @@ -136,6 +137,9 @@ export default function NavBar({ {selectedCount} +
+ +
); From ff5792065fb62d07f308f1fd8e6eb1162d44fdb1 Mon Sep 17 00:00:00 2001 From: Oscar Grande Date: Fri, 13 Nov 2026 20:13:24 +0100 Subject: [PATCH 4/8] Schema setup for users --- backend/db/schema.sql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/db/schema.sql b/backend/db/schema.sql index 7612d12..d020e41 100644 --- a/backend/db/schema.sql +++ b/backend/db/schema.sql @@ -53,6 +53,12 @@ CREATE INDEX IF NOT EXISTS parts_cooler_height_idx WHERE category = 'CPUCooler'; +CREATE TABLE IF NOT EXISTS users ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + firebase_uid varchar(128) NOT NULL UNIQUE, + display_name text +); + /* Saved builds */ CREATE TABLE builds ( From c1da046bc2465e398e5a8d092d357ecf502c1e81 Mon Sep 17 00:00:00 2001 From: Oscar Grande Date: Fri, 13 Nov 2026 20:46:00 +0100 Subject: [PATCH 5/8] Link Firebase authentication to PostgreSQL users --- backend/server/src/auth.ts | 20 ++++++++++++------ backend/server/src/resolvers.ts | 9 +++++++- backend/server/src/schema.ts | 9 ++++++-- backend/server/src/users.ts | 29 ++++++++++++++++++++++++++ frontend/src/components/AuthButton.tsx | 11 ++++++++-- 5 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 backend/server/src/users.ts diff --git a/backend/server/src/auth.ts b/backend/server/src/auth.ts index 80e2db9..ccfc6b1 100644 --- a/backend/server/src/auth.ts +++ b/backend/server/src/auth.ts @@ -1,15 +1,18 @@ import { GraphQLError } from "graphql"; import { firebaseAuth } from "./firebase.js"; +import { findOrCreateUser } from "./users.js"; +import type { DatabaseUser } from "./users.js"; export type AuthContext = { uid: string | null; + user: DatabaseUser | null; }; export async function createAuthContext( authorization?: string, ): Promise { if (!authorization) { - return { uid: null }; + return { uid: null, user: null }; } const match = /^Bearer (\S+)$/i.exec(authorization); @@ -20,12 +23,17 @@ export async function createAuthContext( }); } - try { - const token = await firebaseAuth.verifyIdToken(match[1]!); - return { uid: token.uid }; - } catch { + // Keep database errors separate from authentication errors. + const token = await firebaseAuth.verifyIdToken(match[1]!).catch(() => { throw new GraphQLError("Invalid or expired login token", { extensions: { code: "UNAUTHENTICATED" }, }); - } + }); + + const displayName = + typeof token.name === "string" ? token.name : null; + + const user = await findOrCreateUser(token.uid, displayName); + + return { uid: token.uid, user }; } \ No newline at end of file diff --git a/backend/server/src/resolvers.ts b/backend/server/src/resolvers.ts index f760ee8..db2b370 100644 --- a/backend/server/src/resolvers.ts +++ b/backend/server/src/resolvers.ts @@ -422,7 +422,14 @@ export const resolvers = { JSON: JSONScalar, Mutation: { publishBuild }, Query: { - me: (_: unknown, __: unknown, context: AuthContext) => context.uid, + me: (_: unknown, __: unknown, context: AuthContext) => { + if (!context.user) return null; + + return { + id: context.user.id, + displayName: context.user.display_name, + }; +}, parts, part, manufacturers, builds: async (_: unknown, { limit = 20, offset = 0 }: { limit?: number | null; offset?: number | null }) => { if (limit == null || offset == null || limit < 1 || limit > 100 || offset < 0) { diff --git a/backend/server/src/schema.ts b/backend/server/src/schema.ts index 77ff736..25b464d 100644 --- a/backend/server/src/schema.ts +++ b/backend/server/src/schema.ts @@ -93,7 +93,12 @@ export const typeDefs = /* GraphQL */ ` type Mutation { publishBuild(input: PublishBuildInput!): Build! -} + } + + type User { + id: ID! + displayName: String + } type Query { """ @@ -116,7 +121,7 @@ export const typeDefs = /* GraphQL */ ` builds(limit: Int = 20, offset: Int = 0): [Build!]! build(id: ID!): Build - me: ID + me: User "Distinct manufacturers, optionally only those that have parts in the given category" manufacturers(category: Category): [String!]! diff --git a/backend/server/src/users.ts b/backend/server/src/users.ts new file mode 100644 index 0000000..4ad341d --- /dev/null +++ b/backend/server/src/users.ts @@ -0,0 +1,29 @@ +import { pool } from "./db.js"; + +export type DatabaseUser = { + id: string; + firebase_uid: string; + display_name: string | null; +}; + +export async function findOrCreateUser( + uid: string, + displayName: string | null, +): Promise { + const { rows } = await pool.query( + `INSERT INTO users (firebase_uid, display_name) + VALUES ($1, $2) + ON CONFLICT (firebase_uid) + DO UPDATE SET display_name = EXCLUDED.display_name + RETURNING id::text, firebase_uid, display_name`, + [uid, displayName], + ); + + const user = rows[0]; + + if (!user) { + throw new Error("Could not create or retrieve user."); + } + + return user; +} \ No newline at end of file diff --git a/frontend/src/components/AuthButton.tsx b/frontend/src/components/AuthButton.tsx index 613bec1..1dcbb05 100644 --- a/frontend/src/components/AuthButton.tsx +++ b/frontend/src/components/AuthButton.tsx @@ -22,8 +22,15 @@ export default function AuthButton() { } async function testBackend() { try { - const result = await graphql<{ me: string | null }>("{ me }"); - alert(`Backend user: ${result.me ?? "Not signed in"}`); + const result = await graphql<{ + me: { id: string; displayName: string | null } | null; + }>("{ me { id displayName } }"); + + alert( + result.me + ? `Database ID: ${result.me.id}\nName: ${result.me.displayName ?? "No name"}` + : "Not signed in", + ); } catch (error) { alert(error instanceof Error ? error.message : "Request failed"); } From 6e546614f34d5b0877f525cfefe5051bc170a7ea Mon Sep 17 00:00:00 2001 From: Oscar Grande Date: Fri, 13 Nov 2026 22:13:40 +0100 Subject: [PATCH 6/8] Add email login and authenticated build publishing --- backend/server/src/firebase.ts | 6 +- backend/server/src/resolvers.ts | 131 ++++++++++++++++++++++++-------- 2 files changed, 102 insertions(+), 35 deletions(-) diff --git a/backend/server/src/firebase.ts b/backend/server/src/firebase.ts index 0f56fa2..09002db 100644 --- a/backend/server/src/firebase.ts +++ b/backend/server/src/firebase.ts @@ -2,11 +2,7 @@ import "dotenv/config"; import { initializeApp } from "firebase-admin/app"; import { getAuth } from "firebase-admin/auth"; -const projectId = process.env.FIREBASE_PROJECT_ID; - -if (!projectId) { - throw new Error("Missing FIREBASE_PROJECT_ID in backend/server/.env"); -} +const projectId = "full-circuit"; const app = initializeApp({ projectId }); diff --git a/backend/server/src/resolvers.ts b/backend/server/src/resolvers.ts index db2b370..944d52e 100644 --- a/backend/server/src/resolvers.ts +++ b/backend/server/src/resolvers.ts @@ -380,33 +380,79 @@ async function manufacturers( return rows.map((row) => row.manufacturer); } -async function publishBuild(_: unknown, { input }: { - input: { tittel: string; comment?: string | null; partIds: string[] }; -}) { +async function publishBuild( + _: unknown, + { + input, + }: { + input: { + tittel: string; + comment?: string | null; + partIds: string[]; + }; + }, + context: AuthContext, +) { + if (!context.user) { + throw new GraphQLError("Sign in to publish a build.", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } const tittel = input.tittel.trim(); const comment = input.comment?.trim() || null; - const invalid = (message: string) => new GraphQLError(message, { - extensions: { code: "BAD_USER_INPUT" }, - }); - if (!tittel || [...tittel].length > 50) throw invalid("Enter a title of 1–50 characters."); - if (comment && [...comment].length > 200) throw invalid("Comment must be at most 200 characters."); - if (input.partIds.length < 1 || input.partIds.length > 8 || input.partIds.some(id => !UUID_RE.test(id))) { + const invalid = (message: string) => + new GraphQLError(message, { + extensions: { code: "BAD_USER_INPUT" }, + }); + if (!tittel || [...tittel].length > 50) + throw invalid("Enter a title of 1–50 characters."); + if (comment && [...comment].length > 200) + throw invalid("Comment must be at most 200 characters."); + if ( + input.partIds.length < 1 || + input.partIds.length > 8 || + input.partIds.some((id) => !UUID_RE.test(id)) + ) { throw invalid("Choose between 1 and 8 valid components."); } const { rows: selected } = await pool.query<{ id: string; category: string }>( - "SELECT id, category FROM parts WHERE id = ANY($1::uuid[])", [input.partIds], + "SELECT id, category FROM parts WHERE id = ANY($1::uuid[])", + [input.partIds], ); - const categories = ["CPU", "GPU", "RAM", "Storage", "Motherboard", "PSU", "PCCase", "CPUCooler"]; - if (selected.length !== input.partIds.length || - new Set(selected.map(part => part.category)).size !== selected.length || - selected.some(part => !categories.includes(part.category))) { + const categories = [ + "CPU", + "GPU", + "RAM", + "Storage", + "Motherboard", + "PSU", + "PCCase", + "CPUCooler", + ]; + if ( + selected.length !== input.partIds.length || + new Set(selected.map((part) => part.category)).size !== selected.length || + selected.some((part) => !categories.includes(part.category)) + ) { throw invalid("Choose existing components, with only one per category."); } - const ids = categories.map(category => selected.find(part => part.category === category)?.id ?? null); + const ids = categories.map( + (category) => + selected.find((part) => part.category === category)?.id ?? null, + ); const { rows } = await pool.query( - `INSERT INTO builds (tittel, comment, cpu_id, gpu_id, ram_id, storage_id, motherboard_id, psu_id, case_id, cooler_id) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *`, - [tittel, comment, ...ids], + `INSERT INTO builds ( + tittel, comment, + cpu_id, gpu_id, ram_id, storage_id, + motherboard_id, psu_id, case_id, cooler_id, + owner_id, is_published + ) + VALUES ( + $1, $2, $3, $4, $5, $6, + $7, $8, $9, $10, $11, true + ) + RETURNING *`, + [tittel, comment, ...ids, context.user.id], ); return rows[0]; } @@ -423,22 +469,39 @@ export const resolvers = { Mutation: { publishBuild }, Query: { me: (_: unknown, __: unknown, context: AuthContext) => { - if (!context.user) return null; + if (!context.user) return null; - return { - id: context.user.id, - displayName: context.user.display_name, - }; -}, - parts, part, manufacturers, - builds: async (_: unknown, { limit = 20, offset = 0 }: { limit?: number | null; offset?: number | null }) => { - if (limit == null || offset == null || limit < 1 || limit > 100 || offset < 0) { + return { + id: context.user.id, + displayName: context.user.display_name, + }; + }, + parts, + part, + manufacturers, + builds: async ( + _: unknown, + { + limit = 20, + offset = 0, + }: { limit?: number | null; offset?: number | null }, + ) => { + if ( + limit == null || + offset == null || + limit < 1 || + limit > 100 || + offset < 0 + ) { throw new GraphQLError("Invalid page size or offset", { extensions: { code: "BAD_USER_INPUT" }, }); } const { rows } = await pool.query( - `SELECT * FROM builds ORDER BY created_at DESC, id DESC LIMIT $1 OFFSET $2`, + `SELECT * FROM builds + WHERE is_published = true + ORDER BY created_at DESC, id DESC + LIMIT $1 OFFSET $2`, [limit, offset], ); return rows; @@ -447,8 +510,16 @@ export const resolvers = { Build: { createdAt: (row: { created_at: Date }) => row.created_at.toISOString(), parts: async (row: Record) => { - const ids = [row.cpu_id, row.gpu_id, row.ram_id, row.storage_id, - row.motherboard_id, row.psu_id, row.case_id, row.cooler_id].filter(Boolean); + const ids = [ + row.cpu_id, + row.gpu_id, + row.ram_id, + row.storage_id, + row.motherboard_id, + row.psu_id, + row.case_id, + row.cooler_id, + ].filter(Boolean); const { rows } = await pool.query( `SELECT ${PART_COLUMNS} FROM parts WHERE id = ANY($1::uuid[]) ORDER BY category`, [ids], From 53d0bffabafe5477f58d16dbe5a002d598341499 Mon Sep 17 00:00:00 2001 From: Oscar Grande Date: Fri, 13 Nov 2026 22:14:36 +0100 Subject: [PATCH 7/8] Add email login and authenticated build publishing --- backend/db/schema.sql | 6 +- frontend/src/App.tsx | 5 +- frontend/src/api/builds.ts | 41 ++-- frontend/src/components/AuthButton.tsx | 73 ++----- frontend/src/context/AuthContext.ts | 2 + frontend/src/context/AuthProvider.tsx | 23 ++- frontend/src/index.css | 21 +++ frontend/src/lib/firebase.ts | 8 +- frontend/src/pages/loginPage/loginPage.tsx | 210 +++++++++++++++++++++ 9 files changed, 302 insertions(+), 87 deletions(-) create mode 100644 frontend/src/pages/loginPage/loginPage.tsx diff --git a/backend/db/schema.sql b/backend/db/schema.sql index d020e41..e250507 100644 --- a/backend/db/schema.sql +++ b/backend/db/schema.sql @@ -62,6 +62,8 @@ CREATE TABLE IF NOT EXISTS users ( /* Saved builds */ CREATE TABLE builds ( + owner_id bigint REFERENCES users(id), + is_published boolean NOT NULL DEFAULT false, id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tittel varchar(50) NOT NULL CHECK (length(trim(tittel)) > 0), created_at timestamptz NOT NULL DEFAULT now(), @@ -75,4 +77,6 @@ CREATE TABLE builds ( psu_id uuid REFERENCES parts(id), case_id uuid REFERENCES parts(id), cooler_id uuid REFERENCES parts(id) -); \ No newline at end of file +); +CREATE INDEX IF NOT EXISTS builds_owner_id_idx + ON builds(owner_id); \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3e0db5d..f2eb009 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import CommunityPage from "./pages/communityPage/communityPage"; import NavBar from "./components/NavBar"; import Footer from "./components/Footer"; import BackToTopButton from "./components/BackToTopButton"; +import LoginPage from "./pages/loginPage/loginPage"; function App() { const params = new URLSearchParams(window.location.search); @@ -15,7 +16,9 @@ function App() { let content = ; - if (page === "builder") { + if (page === "login") { + content = ; + } else if (page === "builder") { content = ; } else if (page === "community") { content = ; diff --git a/frontend/src/api/builds.ts b/frontend/src/api/builds.ts index 45f6952..0f3f651 100644 --- a/frontend/src/api/builds.ts +++ b/frontend/src/api/builds.ts @@ -1,3 +1,5 @@ +import { graphql } from "./graphql"; + export type SavedBuild = { id: string tittel: string @@ -6,28 +8,23 @@ export type SavedBuild = { parts: { id: string; name: string; category: string; priceEur: number | null }[] } -export async function publishBuild(input: { tittel: string; comment: string; partIds: string[] }): Promise { - const response = await fetch('/api/graphql', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query: `mutation PublishBuild($input: PublishBuildInput!) { - publishBuild(input: $input) { id } - }`, - variables: { input }, - }), - }) - const result: { - data?: { publishBuild: { id: string } } - errors?: { message: string }[] - } = await response.json().catch(() => ({})) - if (result.errors?.length) { - throw new Error(result.errors.map(error => error.message).join(', ')) - } - if (!response.ok || !result.data?.publishBuild) { - throw new Error(`Could not publish build (HTTP ${response.status}). Check that the backend is updated and running.`) - } - return result.data.publishBuild.id +export async function publishBuild(input: { + tittel: string; + comment: string; + partIds: string[]; +}): Promise { + const result = await graphql<{ + publishBuild: { id: string }; + }>( + `mutation PublishBuild($input: PublishBuildInput!) { + publishBuild(input: $input) { + id + } + }`, + { input }, + ); + + return result.publishBuild.id; } export async function getBuilds(offset = 0, signal?: AbortSignal): Promise { diff --git a/frontend/src/components/AuthButton.tsx b/frontend/src/components/AuthButton.tsx index 1dcbb05..2d074f0 100644 --- a/frontend/src/components/AuthButton.tsx +++ b/frontend/src/components/AuthButton.tsx @@ -1,66 +1,25 @@ -import { useState } from "react"; +import { UserRound } from "lucide-react"; import { useAuth } from "@/context/AuthContext"; -import { graphql } from "@/api/graphql"; export default function AuthButton() { - const { user, loading, login, logout } = useAuth(); - const [pending, setPending] = useState(false); - const [error, setError] = useState(""); + const { user, loading } = useAuth(); - async function handleClick() { - setPending(true); - setError(""); - - try { - if (user) await logout(); - else await login(); - } catch { - setError("Could not complete sign-in or sign-out. Please try again."); - } finally { - setPending(false); - } - } - async function testBackend() { - try { - const result = await graphql<{ - me: { id: string; displayName: string | null } | null; - }>("{ me { id displayName } }"); - - alert( - result.me - ? `Database ID: ${result.me.id}\nName: ${result.me.displayName ?? "No name"}` - : "Not signed in", - ); - } catch (error) { - alert(error instanceof Error ? error.message : "Request failed"); - } + if (loading) { + return ( + + Loading... + + ); } return ( -
- {user && {user.displayName}} - - - - {error &&

{error}

} -
+ + ); } diff --git a/frontend/src/context/AuthContext.ts b/frontend/src/context/AuthContext.ts index ade226e..e6f8682 100644 --- a/frontend/src/context/AuthContext.ts +++ b/frontend/src/context/AuthContext.ts @@ -5,6 +5,8 @@ type AuthContextValue = { user: User | null; loading: boolean; login: () => Promise; + loginWithEmail: (email: string, password: string) => Promise; + registerWithEmail: (email: string, password: string) => Promise; logout: () => Promise; }; diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx index 2f763fa..92be236 100644 --- a/frontend/src/context/AuthProvider.tsx +++ b/frontend/src/context/AuthProvider.tsx @@ -3,6 +3,8 @@ import type { ReactNode } from "react"; import { GoogleAuthProvider, onAuthStateChanged, + createUserWithEmailAndPassword, + signInWithEmailAndPassword, signInWithPopup, signOut, } from "firebase/auth"; @@ -29,9 +31,26 @@ export default function AuthProvider({ children }: { children: ReactNode }) { await signOut(auth); } + async function loginWithEmail(email: string, password: string) { + await signInWithEmailAndPassword(auth, email.trim(), password); + } + + async function registerWithEmail(email: string, password: string) { + await createUserWithEmailAndPassword(auth, email.trim(), password); + } + return ( - + {children} ); -} \ No newline at end of file +} diff --git a/frontend/src/index.css b/frontend/src/index.css index c3fbb7b..47500ac 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -31,3 +31,24 @@ body { background-color: #166534; } } + +@keyframes auth-form-enter { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.auth-form-enter { + animation: auth-form-enter 200ms ease-out; +} + +@media (prefers-reduced-motion: reduce) { + .auth-form-enter { + animation: none; + } +} diff --git a/frontend/src/lib/firebase.ts b/frontend/src/lib/firebase.ts index ef086d0..ce206ff 100644 --- a/frontend/src/lib/firebase.ts +++ b/frontend/src/lib/firebase.ts @@ -2,10 +2,10 @@ import { initializeApp } from "firebase/app"; import { getAuth } from "firebase/auth"; const app = initializeApp({ - apiKey: import.meta.env.VITE_FIREBASE_API_KEY, - authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, - projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, - appId: import.meta.env.VITE_FIREBASE_APP_ID, + apiKey: "AIzaSyBa12YFnOadd1lkieNJAfMfebAnxRG57FI", + authDomain: "full-circuit.firebaseapp.com", + projectId: "full-circuit", + appId: "1:10539610170:web:d1241a25a1b0cd9e1ac503" }); export const auth = getAuth(app); \ No newline at end of file diff --git a/frontend/src/pages/loginPage/loginPage.tsx b/frontend/src/pages/loginPage/loginPage.tsx new file mode 100644 index 0000000..c164472 --- /dev/null +++ b/frontend/src/pages/loginPage/loginPage.tsx @@ -0,0 +1,210 @@ +import { useState } from "react"; +import { FirebaseError } from "firebase/app"; +import { useAuth } from "@/context/AuthContext"; +import { LoaderCircle } from "lucide-react"; + +export default function LoginPage() { + const { user, loading, login, loginWithEmail, registerWithEmail } = useAuth(); + + const [registering, setRegistering] = useState(false); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [pending, setPending] = useState(false); + const [googlePending, setGooglePending] = useState(false); + const [error, setError] = useState(""); + + async function handleEmailLogin() { + if (pending || googlePending) return; + + setPending(true); + setError(""); + + try { + if (registering) { + await registerWithEmail(email, password); + } else { + await loginWithEmail(email, password); + } + + setPassword(""); + window.location.assign(import.meta.env.BASE_URL); + } catch { + setError( + registering + ? "Could not create your account. Try another email or a stronger password." + : "Could not sign in. Check your details and try again.", + ); + } finally { + setPending(false); + } + } + + async function handleGoogleLogin() { + if (pending || googlePending) return; + + setGooglePending(true); + setError(""); + + try { + await login(); + window.location.assign(import.meta.env.BASE_URL); + } catch (error) { + const cancelled = + error instanceof FirebaseError && + (error.code === "auth/popup-closed-by-user" || + error.code === "auth/cancelled-popup-request"); + + if (!cancelled) { + setError("Google sign-in failed. Please try again."); + } + } finally { + setGooglePending(false); + } + } + + if (loading) { + return

Loading...

; + } + + if (user) { + return ( +
+

You’re signed in.

+ + Go to PC builder + +
+ ); + } + + return ( +
+
+

+ {registering ? "Create account" : "Welcome back"} +

+ +

+ {registering + ? "Save your builds and share them with the community." + : "Sign in to access your saved builds."} +

+ +
{ + event.preventDefault(); + void handleEmailLogin(); + }} + > +
+ + + + + +
+
+ + + + + + {error && ( +

+ {error} +

+ )} + +
+ + {registering ? "Already have an account? " : "Need an account? "} + + +
+ + {(pending || googlePending) && ( +

+ Sign-in request in progress. +

+ )} +
+
+ ); +} From 12528044bad978dcf901d972ef675c764f219f9d Mon Sep 17 00:00:00 2001 From: Oscar Grande Date: Fri, 13 Nov 2026 23:15:06 +0100 Subject: [PATCH 8/8] feat: implement saved build functionality and account management - Added OpenSavedBuild component to handle loading saved builds in the PC Builder. - Updated PcBuilderPage to integrate OpenSavedBuild and display editing status for saved builds. - Created AccountPage to manage user accounts and display their builds. - Implemented DeleteBuildButton for deleting builds with confirmation dialog. - Enhanced backend tests for build ownership and authorization. - Expanded frontend tests for saved build workflows and delete functionality. - Updated README with new test results and coverage. --- README.md | 54 +++- backend/server/package.json | 2 +- backend/server/src/resolvers.ts | 142 ++++++++- backend/server/src/schema.ts | 9 +- backend/server/test/buildOwnership.test.mjs | 245 +++++++++++++++ frontend/src/App.tsx | 5 +- frontend/src/api/builds.ts | 173 +++++++++-- frontend/src/context/BuildContext.ts | 41 +-- frontend/src/context/BuildProvider.tsx | 119 ++++++-- .../src/pages/accountPage/accountPage.tsx | 285 ++++++++++++++++++ .../components/DeleteBuildButton.tsx | 108 +++++++ .../src/pages/communityPage/communityPage.tsx | 74 +++-- .../components/OpenSavedBuild.tsx | 67 ++++ .../pcBuilderPage/components/PublishBuild.tsx | 202 +++++++++---- .../src/pages/pcBuilderPage/pcBuilderPage.tsx | 21 +- frontend/src/test/BuildProvider.test.tsx | 178 +++++++++++ frontend/src/test/DeleteBuildButton.test.tsx | 74 +++++ frontend/src/test/README.md | 4 +- 18 files changed, 1633 insertions(+), 170 deletions(-) create mode 100644 backend/server/test/buildOwnership.test.mjs create mode 100644 frontend/src/pages/accountPage/accountPage.tsx create mode 100644 frontend/src/pages/accountPage/components/DeleteBuildButton.tsx create mode 100644 frontend/src/pages/pcBuilderPage/components/OpenSavedBuild.tsx create mode 100644 frontend/src/test/BuildProvider.test.tsx create mode 100644 frontend/src/test/DeleteBuildButton.test.tsx diff --git a/README.md b/README.md index 599f109..7c87757 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Full Circuit is a web application for exploring PC components and planning a cus ## Project status -The app displays a component catalog backed by PostgreSQL and GraphQL, with search, filtering, sorting, component details, and a load-more button. The frontend loads 40 components per request. The PC builder lets users select parts, view compatibility checks, and publish a build with a name and optional comment. Current selections are saved in the browser, while published builds are stored in the database and shown on the Community page. +The app displays a component catalog backed by PostgreSQL and GraphQL, with search, filtering, sorting, component details, and a load-more button. The frontend loads 40 components per request. The PC builder lets users select parts, view compatibility checks, and publish a build with a name and optional comment. Current selections are saved in the browser. Firebase authenticates users, and PostgreSQL stores their private and published builds. Users can reopen, update, publish, copy, and delete their own builds from My builds. Community lists published builds only. ## Accessibility @@ -67,7 +67,7 @@ Files in `backend/`: - `data/parts.csv` – the dataset that is loaded into the database. - `data/part_images/` – one image per part, named `.webp`. -- `db/schema.sql` – creates the `parts` and `builds` tables and the parts indexes. +- `db/schema.sql` – creates the `parts`, `users`, and `builds` tables and their indexes. - `db/seed.sql` – empties `parts` and loads `parts.csv`. - `db/seed.sh` – runs `schema.sql` and `seed.sql`. @@ -146,7 +146,7 @@ Open the builder through the navigation bar or by visiting `?page=builder`. Use - The summary shows the selected component count, progress, and total price in EUR. Missing prices currently contribute zero to the total, so the displayed sum may be incomplete. - **Clear build** removes all selections. -Selections are stored as JSON under the `pc-builder` key in `localStorage` and restored when the app loads. This preserves the current build across refreshes and page navigation in the same browser and site. Localhost and the deployed site have separate storage. The compatibility panel checks selected parts and shows results such as compatible, incompatible, warning, or unknown. The checks may not cover every possible combination. Users can also publish their builds to the database, but private builds are not synchronized between browsers. +Selections are stored as JSON under the `pc-builder` key in `localStorage` and restored when the app loads. This preserves the current build across refreshes and page navigation in the same browser and site. Localhost and the deployed site have separate storage. The compatibility panel checks selected parts and shows results such as compatible, incompatible, warning, or unknown. The checks may not cover every possible combination. Signed-in users can save private builds or publish to Community, and retrieve them across sessions and devices through My builds. ### Navigation and shared build state @@ -171,6 +171,45 @@ The builder takes precedence if both `page=builder` and a category are supplied. Use `AddToBuildButton` for actions that add a component to this shared state. The generic `Button` remains available for other click handlers and form submissions. +### Authentication decision and ownership + +We evaluated three providers for our React, GraphQL, and existing PostgreSQL architecture: + +| Provider | Fit and decision | +| --- | --- | +| [Clerk](https://clerk.com/docs/guides/sessions/manual-jwt-verification) | Provides authentication UI and verifiable session tokens. Suitable, but we chose to keep the small login UI in our own React application. | +| [Firebase Authentication](https://firebase.google.com/docs/auth/admin/verify-id-tokens) | Supports Google and email/password sign-in with ID tokens our GraphQL backend can verify. Selected: it fits our existing backend without moving application data. | +| [Supabase Auth](https://supabase.com/docs/guides/auth/architecture) | Supports token-based authentication and integrates with Supabase PostgreSQL. Suitable, but its integrated database features provide less benefit while we retain our VM database. | + +Firebase manages accounts and passwords. The frontend sends an ID token with authenticated GraphQL requests; Firebase Admin verifies its signature, expiry, issuer, and project. Basic token verification uses Google's public signing keys and the public project ID; no service-account private key is needed. We do not perform token-revocation checks or Firebase administrative account operations. Firebase public web configuration is in `frontend/src/lib/firebase.ts`, and the backend project ID is in `backend/server/src/firebase.ts`. Private database credentials remain in the ignored backend `.env`. + +The backend finds or creates a PostgreSQL user by the verified Firebase UID. Ownership always comes from this authenticated context. `myBuild` and `myBuilds` return only owned builds; updates and deletion include both the build ID and owner ID in the SQL condition. Community only returns published builds. Email addresses and passwords are not stored in our PostgreSQL user table. + +Existing builds are preserved as published with `owner_id = NULL`; users cannot claim, update, or delete them. The VM upgrade was applied separately from `schema.sql`. On another existing database, run the following once (not `seed.sh`): + +```sql +BEGIN; +CREATE TABLE IF NOT EXISTS users ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + firebase_uid varchar(128) NOT NULL UNIQUE, + display_name text +); +ALTER TABLE builds + ADD COLUMN IF NOT EXISTS owner_id bigint REFERENCES users(id), + ADD COLUMN IF NOT EXISTS is_published boolean NOT NULL DEFAULT true; +ALTER TABLE builds ALTER COLUMN is_published SET DEFAULT false; +CREATE INDEX IF NOT EXISTS builds_owner_id_idx ON builds(owner_id); +COMMIT; +``` + +The upgrade's initial `true` default preserves pre-existing public builds; subsequent new builds default to private. If the columns already exist, the upgrade preserves their values. New databases use `schema.sql`, whose default is private from the start. + +### Account builds + +Sign in using Google or email/password, then use the account icon to open My builds. The PC builder supports **Save privately** and **Publish build**. Private builds appear only in their owner's account; published builds also appear in Community. Each save creates a build until an existing build is opened or saved; subsequent saves update that build. Select **Save as new** in the save dialog to create a separate copy. + +Use **Open in builder** on an account build to restore its selected components, name, comment, and visibility. Opening a saved build replaces the current browser draft immediately; it does not modify any build in the database until Save or Publish is used. Drafts and the active build ID are stored separately for each account in the browser and survive navigation and refresh. **Clear build** starts a new draft. Saving an opened published build privately removes it from Community. The GraphQL API checks ownership when reading, updating, or deleting account builds. The Delete action asks for confirmation and removes the build from PostgreSQL and Community. If that build is open in the browser, its selected parts remain as a new local draft. + ### Published builds Users can publish their current build from the PC builder by entering a name and an optional comment. Publishing saves the chosen parts in PostgreSQL through GraphQL. Empty builds cannot be published, and the page shows an error if publishing fails. @@ -181,8 +220,8 @@ The Community page at `?page=community` lists published builds with their names, Features we still plan to add: -- A separate page and shareable link for each published build. -- Saving and reopening private builds across devices. +- Publishing abuse protection (F-002), including rate limiting. Authentication alone does not prevent publishing abuse. +- Firebase account deletion is outside the build ownership issue; build deletion is implemented. ## Use of AI @@ -200,6 +239,11 @@ npm run test See [frontend/src/test/README.md](frontend/src/test/README.md) for the test setup and covered scenarios. +Run backend authentication/build authorization tests from `backend/server/` with `npm test`. These execute the actual GraphQL schema and resolvers with isolated database fixtures, covering two user identities, anonymous requests, private reads, owner-only updates/deletion, private/public saving, copies, and ownerless legacy builds. They do not contact the VM, use real Google accounts, or prove the live database configuration. Frontend tests also cover reopening, account separation, draft persistence, and deletion confirmation and failure states. + +For a live deployment smoke test, use two accounts: save a private build as A, confirm it is absent from Community and B's My builds, and verify B cannot retrieve/update/delete its ID through authenticated GraphQL. Reopen/update it as A, publish it, confirm it appears publicly, then delete it as A. Keep this live check separate from the automated fixture tests. + + ## Run the prototype locally You need Node.js and npm installed. From the repository root, run: diff --git a/backend/server/package.json b/backend/server/package.json index 4e2952f..d7a3de8 100644 --- a/backend/server/package.json +++ b/backend/server/package.json @@ -7,7 +7,7 @@ "type": "module", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "npm run build && node --test test/*.test.mjs", "dev": "tsx watch src/index.ts", "build": "tsc", "start": "node dist/index.js", diff --git a/backend/server/src/resolvers.ts b/backend/server/src/resolvers.ts index 944d52e..c93dc39 100644 --- a/backend/server/src/resolvers.ts +++ b/backend/server/src/resolvers.ts @@ -380,23 +380,28 @@ async function manufacturers( return rows.map((row) => row.manufacturer); } -async function publishBuild( +async function createBuild( _: unknown, { input, }: { input: { + id?: string | null; tittel: string; comment?: string | null; partIds: string[]; }; }, context: AuthContext, + isPublished: boolean, ) { if (!context.user) { - throw new GraphQLError("Sign in to publish a build.", { - extensions: { code: "UNAUTHENTICATED" }, - }); + throw new GraphQLError( + isPublished ? "Sign in to publish a build." : "Sign in to save a build.", + { + extensions: { code: "UNAUTHENTICATED" }, + }, + ); } const tittel = input.tittel.trim(); const comment = input.comment?.trim() || null; @@ -404,6 +409,12 @@ async function publishBuild( new GraphQLError(message, { extensions: { code: "BAD_USER_INPUT" }, }); + if ( + input.id != null && + (!/^[1-9]\d*$/.test(input.id) || BigInt(input.id) > 9223372036854775807n) + ) { + throw invalid("Invalid build ID."); + } if (!tittel || [...tittel].length > 50) throw invalid("Enter a title of 1–50 characters."); if (comment && [...comment].length > 200) @@ -441,7 +452,15 @@ async function publishBuild( selected.find((part) => part.category === category)?.id ?? null, ); const { rows } = await pool.query( - `INSERT INTO builds ( + input.id != null + ? `UPDATE builds SET + tittel = $1, comment = $2, + cpu_id = $3, gpu_id = $4, ram_id = $5, storage_id = $6, + motherboard_id = $7, psu_id = $8, case_id = $9, cooler_id = $10, + is_published = $12 + WHERE id = $13 AND owner_id = $11 + RETURNING *` + : `INSERT INTO builds ( tittel, comment, cpu_id, gpu_id, ram_id, storage_id, motherboard_id, psu_id, case_id, cooler_id, @@ -449,11 +468,23 @@ async function publishBuild( ) VALUES ( $1, $2, $3, $4, $5, $6, - $7, $8, $9, $10, $11, true + $7, $8, $9, $10, $11, $12 ) RETURNING *`, - [tittel, comment, ...ids, context.user.id], + [ + tittel, + comment, + ...ids, + context.user.id, + isPublished, + ...(input.id != null ? [input.id] : []), + ], ); + if (!rows[0]) { + throw new GraphQLError("Build not found or not owned by you.", { + extensions: { code: "NOT_FOUND" }, + }); + } return rows[0]; } @@ -466,8 +497,102 @@ const JSONScalar = new GraphQLScalarType({ export const resolvers = { JSON: JSONScalar, - Mutation: { publishBuild }, + Mutation: { + deleteBuild: async ( + _: unknown, + { id }: { id: string }, + context: AuthContext, + ) => { + if (!context.user) { + throw new GraphQLError("Sign in to delete a build.", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } + if (!/^[1-9]\d*$/.test(id) || BigInt(id) > 9223372036854775807n) { + throw new GraphQLError("Invalid build ID.", { + extensions: { code: "BAD_USER_INPUT" }, + }); + } + const { rowCount } = await pool.query( + "DELETE FROM builds WHERE id = $1 AND owner_id = $2", + [id, context.user.id], + ); + if (!rowCount) { + throw new GraphQLError("Build not found or not owned by you.", { + extensions: { code: "NOT_FOUND" }, + }); + } + return true; + }, + publishBuild: ( + _: unknown, + args: Parameters[1], + context: AuthContext, + ) => createBuild(_, args, context, true), + savePrivateBuild: ( + _: unknown, + args: Parameters[1], + context: AuthContext, + ) => createBuild(_, args, context, false), + }, Query: { + myBuild: async ( + _: unknown, + { id }: { id: string }, + context: AuthContext, + ) => { + if (!context.user) { + throw new GraphQLError("Sign in to open a saved build.", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } + if (!/^[1-9]\d*$/.test(id) || BigInt(id) > 9223372036854775807n) + return null; + const { rows } = await pool.query( + "SELECT * FROM builds WHERE id = $1 AND owner_id = $2", + [id, context.user.id], + ); + return rows[0] ?? null; + }, + myBuilds: async ( + _: unknown, + { + limit = 21, + offset = 0, + }: { + limit?: number | null; + offset?: number | null; + }, + context: AuthContext, + ) => { + if (!context.user) { + throw new GraphQLError("Sign in to view your builds.", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } + + if ( + limit == null || + offset == null || + limit < 1 || + limit > 100 || + offset < 0 + ) { + throw new GraphQLError("Invalid page size or offset", { + extensions: { code: "BAD_USER_INPUT" }, + }); + } + + const { rows } = await pool.query( + `SELECT * FROM builds + WHERE owner_id = $1 + ORDER BY created_at DESC, id DESC + LIMIT $2 OFFSET $3`, + [context.user.id, limit, offset], + ); + + return rows; + }, me: (_: unknown, __: unknown, context: AuthContext) => { if (!context.user) return null; @@ -509,6 +634,7 @@ export const resolvers = { }, Build: { createdAt: (row: { created_at: Date }) => row.created_at.toISOString(), + isPublished: (row: { is_published: boolean }) => row.is_published, parts: async (row: Record) => { const ids = [ row.cpu_id, diff --git a/backend/server/src/schema.ts b/backend/server/src/schema.ts index 25b464d..2b1ab5d 100644 --- a/backend/server/src/schema.ts +++ b/backend/server/src/schema.ts @@ -83,16 +83,20 @@ export const typeDefs = /* GraphQL */ ` comment: String createdAt: String! parts: [Part!]! + isPublished: Boolean! } input PublishBuildInput { + id: ID tittel: String! comment: String partIds: [ID!]! -} + } type Mutation { publishBuild(input: PublishBuildInput!): Build! + savePrivateBuild(input: PublishBuildInput!): Build! + deleteBuild(id: ID!): Boolean! } type User { @@ -120,8 +124,9 @@ export const typeDefs = /* GraphQL */ ` part(id: ID!): Part builds(limit: Int = 20, offset: Int = 0): [Build!]! - build(id: ID!): Build me: User + myBuilds(limit: Int = 21, offset: Int = 0): [Build!]! + myBuild(id: ID!): Build "Distinct manufacturers, optionally only those that have parts in the given category" manufacturers(category: Category): [String!]! diff --git a/backend/server/test/buildOwnership.test.mjs b/backend/server/test/buildOwnership.test.mjs new file mode 100644 index 0000000..0046891 --- /dev/null +++ b/backend/server/test/buildOwnership.test.mjs @@ -0,0 +1,245 @@ +import assert from "node:assert/strict"; +import process from "node:process"; +import { after, beforeEach, test } from "node:test"; +import { ApolloServer } from "@apollo/server"; + +// Exercise the actual GraphQL schema/resolvers with isolated database fixtures. +// No connection to the VM or real user data is made. +process.env.DATABASE_URL = "postgresql://test:test@localhost:5432/test"; +const { pool } = await import("../dist/db.js"); +const { resolvers } = await import("../dist/resolvers.js"); +const { typeDefs } = await import("../dist/schema.js"); +const { createAuthContext } = await import("../dist/auth.js"); +const server = new ApolloServer({ typeDefs, resolvers }); +const originalQuery = pool.query; +const cpuId = "11111111-1111-4111-8111-111111111111"; +const contexts = { + a: { + uid: "firebase-a", + user: { id: "10", firebase_uid: "firebase-a", display_name: "A" }, + }, + b: { + uid: "firebase-b", + user: { id: "20", firebase_uid: "firebase-b", display_name: "B" }, + }, + guest: { uid: null, user: null }, +}; +let builds; +const calls = []; + +beforeEach(() => { + calls.length = 0; + builds = new Map( + [ + ["1", { id: "1", owner_id: "10", is_published: false }], + ["2", { id: "2", owner_id: "10", is_published: true }], + ["3", { id: "3", owner_id: "20", is_published: false }], + ["4", { id: "4", owner_id: null, is_published: true }], + ].map(([id, row]) => [ + id, + { + ...row, + tittel: `Build ${id}`, + comment: null, + created_at: new Date("2026-11-13T12:00:00Z"), + cpu_id: cpuId, + }, + ]), + ); +}); + +pool.query = async (sql, params = []) => { + calls.push({ sql, params }); + if (sql.includes("FROM parts")) { + return { + rows: [ + { + id: cpuId, + category: "CPU", + name: "Test CPU", + manufacturer: "Test", + price_eur: 10, + image_path: null, + specs: {}, + }, + ], + }; + } + if (sql.startsWith("DELETE FROM builds")) { + assert.match(sql, /WHERE id = \$1 AND owner_id = \$2/); + const row = builds.get(params[0]); + if (!row || row.owner_id !== params[1]) return { rowCount: 0, rows: [] }; + builds.delete(row.id); + return { rowCount: 1, rows: [] }; + } + if (sql.includes("UPDATE builds SET")) { + assert.match(sql, /WHERE id = \$13 AND owner_id = \$11/); + const row = builds.get(params[12]); + if (!row || row.owner_id !== params[10]) return { rows: [] }; + Object.assign(row, { + tittel: params[0], + comment: params[1], + is_published: params[11], + }); + return { rows: [row] }; + } + if (sql.includes("INSERT INTO builds")) { + const id = String(builds.size + 10); + const row = { + id, + owner_id: params[10], + is_published: params[11], + tittel: params[0], + comment: params[1], + created_at: new Date(), + cpu_id: cpuId, + }; + builds.set(id, row); + return { rows: [row] }; + } + if (sql.includes("FROM builds WHERE id")) { + assert.match(sql, /id = \$1 AND owner_id = \$2/); + const row = builds.get(params[0]); + return { rows: row?.owner_id === params[1] ? [row] : [] }; + } + if (sql.includes("WHERE owner_id = $1")) { + return { + rows: [...builds.values()] + .filter((row) => row.owner_id === params[0]) + .slice(params[2], params[2] + params[1]), + }; + } + if (sql.includes("WHERE is_published = true")) { + return { + rows: [...builds.values()] + .filter((row) => row.is_published) + .slice(params[1], params[1] + params[0]), + }; + } + throw new Error("Unexpected database operation in ownership test"); +}; + +after(async () => { + pool.query = originalQuery; + await server.stop(); + await pool.end(); +}); + +async function request(query, variables = {}, account = "a") { + const result = await server.executeOperation( + { query, variables }, + { contextValue: contexts[account] }, + ); + assert.equal(result.body.kind, "single"); + return result.body.singleResult; +} +const input = { tittel: "Changed", comment: "", partIds: [cpuId] }; +const save = (mutation, id, account = "a") => + request( + `mutation($input: PublishBuildInput!) { ${mutation}(input: $input) { id isPublished } }`, + { input: { ...input, ...(id ? { id } : {}) } }, + account, + ); + +test("anonymous callers cannot list, reopen, save, publish or delete account builds", async () => { + const results = [ + await request("{ myBuilds { id } }", {}, "guest"), + await request('{ myBuild(id: "1") { id } }', {}, "guest"), + await save("savePrivateBuild", null, "guest"), + await save("publishBuild", null, "guest"), + await request('mutation { deleteBuild(id: "1") }', {}, "guest"), + ]; + for (const result of results) + assert.equal(result.errors[0].extensions.code, "UNAUTHENTICATED"); + assert.equal(calls.length, 0); +}); + +test("two accounts see only their own saved builds; private data is inaccessible to the other", async () => { + assert.deepEqual( + (await request("{ myBuilds { id } }")).data.myBuilds.map((row) => row.id), + ["1", "2"], + ); + assert.deepEqual( + (await request("{ myBuilds { id } }", {}, "b")).data.myBuilds.map( + (row) => row.id, + ), + ["3"], + ); + assert.equal( + (await request('{ myBuild(id: "1") { id } }', {}, "b")).data.myBuild, + null, + ); + const own = await request('{ myBuild(id: "1") { id parts { name specs } } }'); + assert.equal(own.data.myBuild.id, "1"); + assert.equal(own.data.myBuild.parts[0].name, "Test CPU"); +}); + +test("Community includes public legacy builds and excludes private builds", async () => { + const result = await request("{ builds { id } }", {}, "guest"); + assert.deepEqual( + result.data.builds.map((row) => row.id), + ["2", "4"], + ); +}); + +test("another account cannot modify, publish or delete an owned build", async () => { + for (const mutation of ["savePrivateBuild", "publishBuild"]) { + const result = await save(mutation, "1", "b"); + assert.equal(result.errors[0].extensions.code, "NOT_FOUND"); + } + const result = await request('mutation { deleteBuild(id: "1") }', {}, "b"); + assert.equal(result.errors[0].extensions.code, "NOT_FOUND"); + assert.equal(builds.get("1").tittel, "Build 1"); +}); + +test("owner updates the same build and can publish or make it private", async () => { + const published = await save("publishBuild", "1"); + assert.equal(published.data.publishBuild.id, "1"); + assert.equal(builds.get("1").is_published, true); + await save("savePrivateBuild", "1"); + assert.equal(builds.get("1").is_published, false); + assert.equal(builds.size, 4); +}); + +test("saving as new creates a separate owned build and preserves the original", async () => { + const result = await save("savePrivateBuild"); + assert.equal(result.errors, undefined); + const copy = builds.get(result.data.savePrivateBuild.id); + assert.equal(copy.owner_id, "10"); + assert.equal(copy.is_published, false); + assert.equal(builds.get("1").tittel, "Build 1"); + assert.equal(builds.size, 5); +}); + +test("owner can delete their build; ownerless legacy builds cannot be claimed or deleted", async () => { + const deleted = await request('mutation { deleteBuild(id: "1") }'); + assert.equal(deleted.data.deleteBuild, true); + assert.equal(builds.has("1"), false); + const legacyDelete = await request('mutation { deleteBuild(id: "4") }'); + assert.equal(legacyDelete.errors[0].extensions.code, "NOT_FOUND"); + assert.equal( + (await save("publishBuild", "4")).errors[0].extensions.code, + "NOT_FOUND", + ); + assert.equal(builds.has("4"), true); +}); + +test("invalid IDs and empty builds are rejected before database mutations", async () => { + const invalid = await request('mutation { deleteBuild(id: "invalid") }'); + assert.equal(invalid.errors[0].extensions.code, "BAD_USER_INPUT"); + const empty = await request( + "mutation($input: PublishBuildInput!) { savePrivateBuild(input: $input) { id } }", + { input: { ...input, partIds: [] } }, + ); + assert.equal(empty.errors[0].extensions.code, "BAD_USER_INPUT"); + assert.equal(calls.length, 0); +}); + +test("authentication accepts absent credentials as anonymous and rejects malformed tokens", async () => { + assert.deepEqual(await createAuthContext(), contexts.guest); + await assert.rejects( + createAuthContext("Bearer broken-token"), + (error) => error.extensions.code === "UNAUTHENTICATED", + ); + assert.equal(calls.length, 0); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f2eb009..f898d93 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import NavBar from "./components/NavBar"; import Footer from "./components/Footer"; import BackToTopButton from "./components/BackToTopButton"; import LoginPage from "./pages/loginPage/loginPage"; +import AccountPage from "./pages/accountPage/accountPage"; function App() { const params = new URLSearchParams(window.location.search); @@ -16,7 +17,9 @@ function App() { let content = ; - if (page === "login") { + if (page === "account") { + content = ; + } else if (page === "login") { content = ; } else if (page === "builder") { content = ; diff --git a/frontend/src/api/builds.ts b/frontend/src/api/builds.ts index 0f3f651..d136f8a 100644 --- a/frontend/src/api/builds.ts +++ b/frontend/src/api/builds.ts @@ -1,14 +1,21 @@ +import type { PcPart } from "@/types/types"; import { graphql } from "./graphql"; export type SavedBuild = { - id: string - tittel: string - comment: string | null - createdAt: string - parts: { id: string; name: string; category: string; priceEur: number | null }[] -} + id: string; + tittel: string; + comment: string | null; + createdAt: string; + parts: { + id: string; + name: string; + category: string; + priceEur: number | null; + }[]; +}; export async function publishBuild(input: { + id?: string; tittel: string; comment: string; partIds: string[]; @@ -16,21 +23,26 @@ export async function publishBuild(input: { const result = await graphql<{ publishBuild: { id: string }; }>( - `mutation PublishBuild($input: PublishBuildInput!) { - publishBuild(input: $input) { - id + ` + mutation PublishBuild($input: PublishBuildInput!) { + publishBuild(input: $input) { + id + } } - }`, + `, { input }, ); return result.publishBuild.id; } -export async function getBuilds(offset = 0, signal?: AbortSignal): Promise { - const response = await fetch('/api/graphql', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, +export async function getBuilds( + offset = 0, + signal?: AbortSignal, +): Promise { + const response = await fetch("/api/graphql", { + method: "POST", + headers: { "Content-Type": "application/json" }, signal, body: JSON.stringify({ query: `query GetBuilds($offset: Int!) { @@ -41,14 +53,133 @@ export async function getBuilds(offset = 0, signal?: AbortSignal): Promise { + const result = await graphql<{ savePrivateBuild: { id: string } }>( + ` + mutation SavePrivateBuild($input: PublishBuildInput!) { + savePrivateBuild(input: $input) { + id + } + } + `, + { input }, + ); + + return result.savePrivateBuild.id; +} + +export type AccountBuild = SavedBuild & { + isPublished: boolean; +}; + +export async function getMyBuilds(offset = 0): Promise { + const result = await graphql<{ myBuilds: AccountBuild[] }>( + ` + query MyBuilds($offset: Int!) { + myBuilds(limit: 21, offset: $offset) { + id + tittel + comment + createdAt + isPublished + parts { + id + name + category + priceEur + } + } + } + `, + { offset }, + ); + + return result.myBuilds; +} + +export async function deleteBuild(id: string): Promise { + await graphql<{ deleteBuild: boolean }>( + ` + mutation DeleteBuild($id: ID!) { + deleteBuild(id: $id) + } + `, + { id }, + ); +} + +export type OpenedBuild = Omit & { parts: PcPart[] }; + +export async function getMyBuild(id: string): Promise { + const result = await graphql<{ + myBuild: + | (Omit & { + parts: { + id: string; + name: string; + category: PcPart["category"]; + manufacturer: string | null; + priceEur: number | null; + imageUrl: string | null; + specs: unknown; + }[]; + }) + | null; + }>( + ` + query OpenMyBuild($id: ID!) { + myBuild(id: $id) { + id + tittel + comment + createdAt + isPublished + parts { + id + name + category + manufacturer + priceEur + imageUrl + specs + } + } + } + `, + { id }, + ); + if (!result.myBuild) return null; + return { + ...result.myBuild, + parts: result.myBuild.parts.map( + (part) => + ({ + id: part.id, + name: part.name, + category: part.category, + manufacturer: part.manufacturer ?? "Unknown", + price_eur: part.priceEur, + image_url: part.imageUrl ? `/api${part.imageUrl}` : "", + details: part.specs ?? {}, + }) as PcPart, + ), + }; } diff --git a/frontend/src/context/BuildContext.ts b/frontend/src/context/BuildContext.ts index 8408224..b7dc6a8 100644 --- a/frontend/src/context/BuildContext.ts +++ b/frontend/src/context/BuildContext.ts @@ -1,23 +1,30 @@ -import {createContext, useContext} from 'react' -import type { PcPart} from '@/types/types' +import { createContext, useContext } from "react"; +import type { PcPart } from "@/types/types"; -export type BuildParts = Partial> +export type BuildParts = Partial>; +export type SavedBuildInfo = { + id: string; + title: string; + comment: string; + isPublished: boolean; +}; type BuildContextValue = { - parts : BuildParts - addPart : (part: PcPart) => void - removePart : (category: PcPart['category']) => void - resetBuild: () => void -} + parts: BuildParts; + savedBuild: SavedBuildInfo | null; + isDirty: boolean; + addPart: (part: PcPart) => void; + removePart: (category: PcPart["category"]) => void; + resetBuild: () => void; + loadBuild: (parts: PcPart[], info: SavedBuildInfo) => void; + markSaved: (info: SavedBuildInfo) => void; + forgetSavedBuild: (id: string) => void; +}; -export const BuildContext = createContext(null) +export const BuildContext = createContext(null); export function useBuild() { - const context = useContext(BuildContext) - - if (!context) { - throw new Error('useBuild must be used inside BuildProvider') - } - - return context -} \ No newline at end of file + const context = useContext(BuildContext); + if (!context) throw new Error("useBuild must be used inside BuildProvider"); + return context; +} diff --git a/frontend/src/context/BuildProvider.tsx b/frontend/src/context/BuildProvider.tsx index 911c3e2..d9df3fd 100644 --- a/frontend/src/context/BuildProvider.tsx +++ b/frontend/src/context/BuildProvider.tsx @@ -1,51 +1,116 @@ -import { useEffect, useState } from 'react' -import type { ReactNode } from 'react' -import type { PcPart } from '@/types/types' -import { BuildContext } from './BuildContext' -import type { BuildParts } from './BuildContext' +import { useCallback, useEffect, useState } from "react"; +import type { ReactNode } from "react"; +import type { PcPart } from "@/types/types"; +import { useAuth } from "./AuthContext"; +import { BuildContext } from "./BuildContext"; +import type { BuildParts, SavedBuildInfo } from "./BuildContext"; -const STORAGE_KEY = 'pc-builder' +type Draft = { + parts: BuildParts; + savedBuild: SavedBuildInfo | null; + isDirty: boolean; +}; export default function BuildProvider({ children }: { children: ReactNode }) { - const [parts, setParts] = useState(() => { + const { user, loading } = useAuth(); + if (loading) return

Loading...

; + + // Saved build IDs and drafts must not carry over when switching accounts. + const storageKey = `pc-builder-v2:${user?.uid ?? "guest"}`; + return ( + + {children} + + ); +} + +function BuildState({ + children, + storageKey, +}: { + children: ReactNode; + storageKey: string; +}) { + const [draft, setDraft] = useState(() => { try { - const saved = localStorage.getItem(STORAGE_KEY) - return saved ? JSON.parse(saved) ?? {} : {} + const saved = localStorage.getItem(storageKey); + if (saved) return JSON.parse(saved) as Draft; + // Preserve the old browser draft without claiming ownership of a saved build. + const legacy = localStorage.getItem("pc-builder"); + const parts: BuildParts = legacy ? (JSON.parse(legacy) ?? {}) : {}; + return { + parts, + savedBuild: null, + isDirty: Object.keys(parts).length > 0, + }; } catch { - return {} + return { parts: {}, savedBuild: null, isDirty: false }; } - }) + }); useEffect(() => { try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(parts)) + localStorage.setItem(storageKey, JSON.stringify(draft)); } catch { - console.warn('Could not save the build in this browser.') + console.warn("Could not save the build in this browser."); } - }, [parts]) + }, [draft, storageKey]); + + const loadBuild = useCallback((parts: PcPart[], info: SavedBuildInfo) => { + const selected: BuildParts = {}; + for (const part of parts) selected[part.category] = part; + setDraft({ parts: selected, savedBuild: info, isDirty: false }); + }, []); + + const markSaved = useCallback((info: SavedBuildInfo) => { + setDraft((previous) => ({ ...previous, savedBuild: info, isDirty: false })); + }, []); + + const forgetSavedBuild = useCallback((id: string) => { + setDraft((previous) => + previous.savedBuild?.id === id + ? { + ...previous, + savedBuild: null, + isDirty: Object.keys(previous.parts).length > 0, + } + : previous, + ); + }, []); function addPart(part: PcPart) { - setParts((previous) => ({ + setDraft((previous) => ({ ...previous, - [part.category]: part, - })) + parts: { ...previous.parts, [part.category]: part }, + isDirty: true, + })); } - function removePart(category: PcPart['category']) { - setParts((previous) => { - const updated = { ...previous } - delete updated[category] - return updated - }) + function removePart(category: PcPart["category"]) { + setDraft((previous) => { + const parts = { ...previous.parts }; + delete parts[category]; + return { ...previous, parts, isDirty: true }; + }); } function resetBuild() { - setParts({}) -} + setDraft({ parts: {}, savedBuild: null, isDirty: false }); + } return ( - + {children} - ) + ); } diff --git a/frontend/src/pages/accountPage/accountPage.tsx b/frontend/src/pages/accountPage/accountPage.tsx new file mode 100644 index 0000000..f8da3fa --- /dev/null +++ b/frontend/src/pages/accountPage/accountPage.tsx @@ -0,0 +1,285 @@ +import { useEffect, useState } from "react"; +import { LoaderCircle, LogOut, Plus, UserRound } from "lucide-react"; +import { useAuth } from "@/context/AuthContext"; +import { useBuild } from "@/context/BuildContext"; +import { getMyBuilds } from "@/api/builds"; +import type { AccountBuild } from "@/api/builds"; +import DeleteBuildButton from "./components/DeleteBuildButton"; + +const buttonStyle = + "inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition duration-200 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-accent disabled:cursor-wait disabled:opacity-50"; + +export default function AccountPage() { + const { user, loading } = useAuth(); + + if (loading) { + return ( +

+ Loading account... +

+ ); + } + + if (!user) { + return ( +
+
+ ); + } + + return ; +} + +function AccountContent() { + const { user, logout } = useAuth(); + const { resetBuild } = useBuild(); + const [builds, setBuilds] = useState([]); + const [offset, setOffset] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [signingOut, setSigningOut] = useState(false); + const [logoutError, setLogoutError] = useState(""); + const [retry, setRetry] = useState(0); + const [notice, setNotice] = useState(""); + + useEffect(() => { + let cancelled = false; + + getMyBuilds(offset) + .then((data) => { + if (!cancelled) setBuilds(data); + }) + .catch(() => { + if (!cancelled) + setError("Could not load your builds. Please try again."); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [offset, retry]); + + function changePage(nextOffset: number) { + setLoading(true); + setError(""); + setOffset(nextOffset); + } + + function handleDeleted() { + setNotice("Build deleted."); + setLoading(true); + setError(""); + if (builds.length === 1 && offset > 0) setOffset(offset - 20); + else setRetry((value) => value + 1); + } + + async function handleLogout() { + setSigningOut(true); + setLogoutError(""); + + try { + await logout(); + window.location.assign(import.meta.env.BASE_URL); + } catch { + setLogoutError("Could not sign out. Please try again."); + setSigningOut(false); + } + } + + return ( +
+
+
+
+
+
+

My account

+

+ {user?.displayName || "Welcome, builder"} +

+
+
+ + +
+ + {logoutError && ( +

+ {logoutError} +

+ )} + +
+
+

My builds

+

+ Your private builds and the builds you’ve shared. +

+
+ + + +
+ + {notice && ( +

+ {notice} +

+ )} + + {loading && ( +

+

+ )} + + {!loading && error && ( +
+

+ {error} +

+ +
+ )} + + {!loading && !error && ( + <> + {builds.length === 0 ? ( +
+

+ {offset === 0 + ? "Your next build starts here" + : "No more builds"} +

+

+ {offset === 0 + ? "Builds you save or publish will appear here." + : "Go back to see your earlier results."} +

+
+ ) : ( + + )} + +
+ {offset > 0 && ( + + )} + {builds.length > 20 && ( + + )} +
+ + )} +
+ ); +} diff --git a/frontend/src/pages/accountPage/components/DeleteBuildButton.tsx b/frontend/src/pages/accountPage/components/DeleteBuildButton.tsx new file mode 100644 index 0000000..0b21d1c --- /dev/null +++ b/frontend/src/pages/accountPage/components/DeleteBuildButton.tsx @@ -0,0 +1,108 @@ +import { useId, useRef, useState } from "react"; +import { LoaderCircle, Trash2 } from "lucide-react"; +import { deleteBuild } from "@/api/builds"; +import { useBuild } from "@/context/BuildContext"; + +type Props = { id: string; title: string; onDeleted: () => void }; + +export default function DeleteBuildButton({ id, title, onDeleted }: Props) { + const dialogRef = useRef(null); + const cancelRef = useRef(null); + const inFlight = useRef(false); + const headingId = useId(); + const descriptionId = useId(); + const { forgetSavedBuild } = useBuild(); + const [deleting, setDeleting] = useState(false); + const [error, setError] = useState(""); + + async function confirmDelete() { + if (inFlight.current) return; + inFlight.current = true; + setDeleting(true); + setError(""); + try { + await deleteBuild(id); + forgetSavedBuild(id); + dialogRef.current?.close(); + onDeleted(); + } catch (error) { + setError( + error instanceof Error + ? error.message + : "Could not delete build. Please try again.", + ); + } finally { + inFlight.current = false; + setDeleting(false); + } + } + + return ( + <> + + { + if (inFlight.current) event.preventDefault(); + }} + className="m-auto w-[calc(100%-2rem)] max-w-md rounded-xl border border-gray-600 bg-surface p-6 text-white shadow-xl backdrop:bg-black/60" + > +

+ Delete build? +

+

+ Permanently delete{" "} + {title} from + your account and Community? This cannot be undone. +

+ {error && ( +

+ {error} +

+ )} +
+ + +
+
+ + ); +} diff --git a/frontend/src/pages/communityPage/communityPage.tsx b/frontend/src/pages/communityPage/communityPage.tsx index f7a5a18..d48ece5 100644 --- a/frontend/src/pages/communityPage/communityPage.tsx +++ b/frontend/src/pages/communityPage/communityPage.tsx @@ -1,62 +1,78 @@ -import { useEffect, useState } from 'react' -import { getBuilds, type SavedBuild } from '@/api/builds' -import Button from '@/components/Button' -import BuildParts from './components/BuildParts' +import { useEffect, useState } from "react"; +import { getBuilds, type SavedBuild } from "@/api/builds"; +import Button from "@/components/Button"; +import BuildParts from "./components/BuildParts"; export default function CommunityPage() { - const [builds, setBuilds] = useState([]) - const [offset, setOffset] = useState(0) - const [loading, setLoading] = useState(true) - const [error, setError] = useState('') + const [builds, setBuilds] = useState([]); + const [offset, setOffset] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); useEffect(() => { - const controller = new AbortController() + const controller = new AbortController(); getBuilds(offset, controller.signal) .then((data) => { - if (!controller.signal.aborted) setBuilds(data) + if (!controller.signal.aborted) setBuilds(data); }) .catch(() => { - if (!controller.signal.aborted) { - setError('Could not load community builds.') - } + if (!controller.signal.aborted) + setError("Could not load community builds."); }) .finally(() => { - if (!controller.signal.aborted) setLoading(false) - }) - return () => controller.abort() - }, [offset]) + if (!controller.signal.aborted) setLoading(false); + }); + return () => controller.abort(); + }, [offset]); function changePage(nextOffset: number) { - setLoading(true) - setError('') - setOffset(nextOffset) + setLoading(true); + setError(""); + setOffset(nextOffset); } return ( -
-

Community builds

- +
+

Community builds

{loading &&

Loading builds...

} {error &&

{error}

} - {!loading && !error && ( <> {builds.length === 0 &&

No builds published yet.

}
{builds.slice(0, 20).map((build) => ( -
+

{build.tittel}

- {build.comment &&

{build.comment}

} + {build.comment && ( +

{build.comment}

+ )}
))}
- {offset > 0 && } - {builds.length > 20 && } + {offset > 0 && ( + + )} + {builds.length > 20 && ( + + )}
)}
- ) + ); } diff --git a/frontend/src/pages/pcBuilderPage/components/OpenSavedBuild.tsx b/frontend/src/pages/pcBuilderPage/components/OpenSavedBuild.tsx new file mode 100644 index 0000000..c436084 --- /dev/null +++ b/frontend/src/pages/pcBuilderPage/components/OpenSavedBuild.tsx @@ -0,0 +1,67 @@ +import { useEffect, useState } from "react"; +import type { ReactNode } from "react"; +import { getMyBuild } from "@/api/builds"; +import { useBuild } from "@/context/BuildContext"; + +export default function OpenSavedBuild({ children }: { children: ReactNode }) { + const { loadBuild } = useBuild(); + const [id] = useState(() => + new URLSearchParams(window.location.search).get("build"), + ); + const [ready, setReady] = useState(!id); + const [error, setError] = useState(""); + + useEffect(() => { + if (!id) return; + let cancelled = false; + + getMyBuild(id) + .then((build) => { + if (cancelled) return; + if (!build) throw new Error("Build not found or not owned by you."); + loadBuild(build.parts, { + id: build.id, + title: build.tittel, + comment: build.comment ?? "", + isPublished: build.isPublished, + }); + // Refresh restores the current draft rather than fetching over edits. + const url = new URL(window.location.href); + url.searchParams.delete("build"); + window.history.replaceState(null, "", url); + setReady(true); + }) + .catch((error: unknown) => { + if (!cancelled) { + setError( + error instanceof Error ? error.message : "Could not open build.", + ); + } + }); + + return () => { + cancelled = true; + }; + }, [id, loadBuild]); + + if (error) { + return ( +
+

{error}

+ + Back to My builds + +
+ ); + } + if (!ready) + return ( +

+ Opening saved build... +

+ ); + return children; +} diff --git a/frontend/src/pages/pcBuilderPage/components/PublishBuild.tsx b/frontend/src/pages/pcBuilderPage/components/PublishBuild.tsx index 5d36536..a3f6712 100644 --- a/frontend/src/pages/pcBuilderPage/components/PublishBuild.tsx +++ b/frontend/src/pages/pcBuilderPage/components/PublishBuild.tsx @@ -1,84 +1,176 @@ -import { useRef, useState } from 'react' -import { useBuild } from '@/context/BuildContext' -import { publishBuild } from '@/api/builds' -import Button from '@/components/Button' +import { useId, useRef, useState } from "react"; +import { useBuild } from "@/context/BuildContext"; +import { publishBuild, savePrivateBuild } from "@/api/builds"; +import Button from "@/components/Button"; -export default function PublishBuild() { - const { parts } = useBuild() - const dialogRef = useRef(null) - const pending = useRef(false) - const [title, setTitle] = useState('') - const [comment, setComment] = useState('') - const [saving, setSaving] = useState(false) - const [published, setPublished] = useState(false) - const [error, setError] = useState('') +export default function PublishBuild({ + privately = false, +}: { + privately?: boolean; +}) { + const headingId = useId(); + const actionLabel = privately ? "Save privately" : "Publish build"; + const save = privately ? savePrivateBuild : publishBuild; + const { parts, savedBuild, markSaved } = useBuild(); + const dialogRef = useRef(null); + const pending = useRef(false); + const [title, setTitle] = useState(""); + const [comment, setComment] = useState(""); + const [saving, setSaving] = useState(false); + const [saveAsNew, setSaveAsNew] = useState(false); + const [published, setPublished] = useState(false); + const [error, setError] = useState(""); return ( <> -
- +
+
{ if (pending.current) event.preventDefault() }} + onCancel={(event) => { + if (pending.current) event.preventDefault(); + }} > -

Publish build

+

+ {actionLabel} +

{published ? (
-

Your build is published!

- View community builds +

+ {privately + ? "Your build is saved privately!" + : "Your build is published!"} +

+ + {privately ? "View my builds" : "View community builds"} +
) : ( -
{ - event.preventDefault() - if (pending.current) return - if (!title.trim()) { setError('Enter a build name.'); return } - pending.current = true - setSaving(true) - setError('') - try { - await publishBuild({ - tittel: title.trim(), comment: comment.trim(), - partIds: Object.values(parts).map(part => part.id), - }) - setPublished(true) - setTitle('') - setComment('') - } catch (error) { - setError(error instanceof Error ? error.message : 'Could not publish build.') - } finally { - pending.current = false - setSaving(false) - } - }}> -

Share this build with everyone in the community.

+ { + event.preventDefault(); + if (pending.current) return; + if (!title.trim()) { + setError("Enter a build name."); + return; + } + pending.current = true; + setSaving(true); + setError(""); + try { + const id = await save({ + ...(savedBuild && !saveAsNew ? { id: savedBuild.id } : {}), + tittel: title.trim(), + comment: comment.trim(), + partIds: Object.values(parts).map((part) => part.id), + }); + markSaved({ + id, + title: title.trim(), + comment: comment.trim(), + isPublished: !privately, + }); + setPublished(true); + } catch (error) { + setError( + error instanceof Error + ? error.message + : "Could not save build.", + ); + } finally { + pending.current = false; + setSaving(false); + } + }} + > +

+ {privately + ? savedBuild && !saveAsNew + ? "Update this build and make it private. It will not appear in Community." + : "Save a new private build to your account. Only you can see it." + : "Save and share this build with everyone in the community."} +