From c0340ecd12dec4eaf85faa53e110083968860496 Mon Sep 17 00:00:00 2001 From: David Gamez Diaz <1192523+davidgamez@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:55:57 -0500 Subject: [PATCH 01/11] add working ssr authentication --- .gitignore | 2 +- scripts/iap-test/.gitignore | 2 + scripts/iap-test/README.md | 92 +++ scripts/iap-test/fetch-gcip.js | 105 +++ scripts/iap-test/fetch-iap.js | 110 +++ scripts/iap-test/package.json | 19 + scripts/iap-test/yarn.lock | 1119 ++++++++++++++++++++++++++ src/app/actions/auth-actions.ts | 27 - src/app/components/AuthTokenSync.tsx | 26 - src/app/providers.tsx | 2 - src/app/utils/auth-server.ts | 136 +++- src/app/utils/config.ts | 16 + src/lib/firebase-admin.ts | 73 +- 13 files changed, 1604 insertions(+), 125 deletions(-) create mode 100644 scripts/iap-test/.gitignore create mode 100644 scripts/iap-test/README.md create mode 100644 scripts/iap-test/fetch-gcip.js create mode 100644 scripts/iap-test/fetch-iap.js create mode 100644 scripts/iap-test/package.json create mode 100644 scripts/iap-test/yarn.lock delete mode 100644 src/app/actions/auth-actions.ts delete mode 100644 src/app/components/AuthTokenSync.tsx diff --git a/.gitignore b/.gitignore index feeb469f..34adcd87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules +node_modules /.pnp .pnp.js diff --git a/scripts/iap-test/.gitignore b/scripts/iap-test/.gitignore new file mode 100644 index 00000000..f510bc35 --- /dev/null +++ b/scripts/iap-test/.gitignore @@ -0,0 +1,2 @@ +.env +sa-key.json \ No newline at end of file diff --git a/scripts/iap-test/README.md b/scripts/iap-test/README.md new file mode 100644 index 00000000..68851578 --- /dev/null +++ b/scripts/iap-test/README.md @@ -0,0 +1,92 @@ +# IAP Test Scripts (IAP + GCIP) + +This folder contains two minimal scripts to test calling an IAP-protected API: + +- `fetch-iap.js`: Uses a Google OIDC ID token (audience = IAP OAuth Client ID). Works when IAP uses Google accounts. +- `fetch-gcip.js`: Uses an Identity Platform (GCIP) ID token obtained via custom token flow. Use this when IAP is configured with GCIP. + +## Environment +Create `.env` based on `.env.sample`: + +```env +# Common +API_URL=https:/// +GOOGLE_SA_JSON_PATH=./sa-key.json + +# IAP (Google accounts) +IAP_AUDIENCE= + +# GCIP (Identity Platform) +GCIP_PROJECT_ID= +GCIP_API_KEY= +# optional if using multi-tenant +GCIP_TENANT_ID= +# optional synthetic UID for the service caller +GCIP_SERVICE_UID=iap-service-caller +``` + +## Install & Run +```bash +# Install local-only deps +npm i google-auth-library firebase-admin node-fetch dotenv + +# IAP (Google accounts) flow +node fetch-iap.js + +# GCIP (Identity Platform) flow +node fetch-gcip.js +``` + +### Using Yarn +```bash +# Install +yarn add google-auth-library firebase-admin node-fetch dotenv + +# IAP flow +yarn run test:iap + +# GCIP flow (default) +yarn test +``` + +## Notes +- `GCIP_API_KEY` is the Web API key for Identity Platform (Firebase/GCIP). Find it in Google Cloud Console → APIs & Services → Credentials. +- For GCIP multi-tenant, set `GCIP_TENANT_ID` and ensure the IAP resource is linked to the same tenant. +- The Service Account must have access to mint custom tokens and (optionally) act as a service principal.# IAP ID Token Test Script + +Minimal Node.js script to fetch a Google-signed OIDC ID token for an IAP-protected HTTPS resource and perform a request to your API. + +## Prerequisites +- Node.js 18+ installed +- Service Account with role `roles/iap.httpsResourceAccessor` +- IAP OAuth 2.0 Client ID (the audience) + +## Setup +1. Create a service account key locally (for testing only; rotate/secure it): +```bash +PROJECT_ID= +gcloud iam service-accounts keys create sa-key.json \ + --iam-account=vercel-iap-caller@${PROJECT_ID}.iam.gserviceaccount.com +``` + +2. Copy `.env.sample` to `.env` and fill values: +```bash +cp .env.sample .env +``` + +3. Install dependencies and run: +```bash +npm ci +npm run test +``` + +## Environment Variables +- `IAP_AUDIENCE`: The IAP OAuth client ID (…apps.googleusercontent.com) +- `API_URL`: Full URL to a reachable endpoint (e.g., https:///health) +- `GOOGLE_SA_JSON_PATH` (preferred): Path to SA key JSON file (e.g., `./sa-key.json`). +- `GOOGLE_SA_JSON` (optional): Inline JSON string of SA key (used if `GOOGLE_SA_JSON_PATH` not provided). +- `LOG_JWT_CLAIMS` (optional): `true|false` print decoded JWT claims for verification. + +## Notes +- For production (Vercel), store secrets in environment variables and avoid files. +- The audience must exactly match the IAP OAuth client ID configured on the protected resource. \ No newline at end of file diff --git a/scripts/iap-test/fetch-gcip.js b/scripts/iap-test/fetch-gcip.js new file mode 100644 index 00000000..21e2e850 --- /dev/null +++ b/scripts/iap-test/fetch-gcip.js @@ -0,0 +1,105 @@ +/* + * GCIP (Identity Platform) token flow for IAP when IAP is configured with GCIP. + * 1) Use Firebase Admin to mint a custom token for a synthetic service user. + * 2) Exchange the custom token for a GCIP ID token via Identity Toolkit API. + * 3) Call the IAP-protected API with Authorization: Bearer . + */ + +const fs = require('fs'); +const path = require('path'); +const fetch = require('node-fetch'); +const admin = require('firebase-admin'); +require('dotenv').config(); + +function ensureEnv(name) { + const v = process.env[name]; + if (!v) { + console.error(`Missing ${name}`); + process.exit(2); + } + return v; +} + +async function main() { + const apiUrl = ensureEnv('API_URL'); + const saJsonPath = process.env.GOOGLE_SA_JSON_PATH; + const saJsonInline = process.env.GOOGLE_SA_JSON; + const projectId = ensureEnv('GCIP_PROJECT_ID'); + const apiKey = ensureEnv('GCIP_API_KEY'); + const tenantId = process.env.GCIP_TENANT_ID || undefined; // optional if default tenant + const serviceUid = process.env.GCIP_SERVICE_UID || 'iap-service-caller'; + + let credentials; + if (saJsonPath && fs.existsSync(path.resolve(saJsonPath))) { + credentials = JSON.parse(fs.readFileSync(path.resolve(saJsonPath), 'utf8')); + } else if (saJsonInline) { + credentials = JSON.parse(saJsonInline); + } else { + console.error('Provide GOOGLE_SA_JSON_PATH or GOOGLE_SA_JSON'); + process.exit(2); + } + + if (!credentials.client_email || !credentials.private_key) { + console.error('Service Account JSON must include client_email and private_key'); + process.exit(2); + } + + if (!admin.apps.length) { + admin.initializeApp({ + credential: admin.credential.cert(credentials), + projectId, + }); + } + + // Mint a custom token for a synthetic service user UID. + const customToken = await admin.auth().createCustomToken(serviceUid, { + service: true, + }); + + // Exchange the custom token for an ID token. + const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`; + const body = { + token: customToken, + returnSecureToken: true, + }; + if (tenantId) body.tenantId = tenantId; + + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!resp.ok) { + const errText = await resp.text(); + console.error('Failed to exchange custom token:', resp.status, errText); + process.exit(1); + } + const tokens = await resp.json(); + const idToken = tokens.idToken; + if (!idToken) { + console.error('No idToken in response'); + process.exit(1); + } + + // Call the IAP-protected API with GCIP ID token. + console.log(`Calling ${apiUrl} with GCIP ID token (tenant: ${tenantId || 'default'})...`); + const apiResp = await fetch(apiUrl, { + headers: { Authorization: `Bearer ${idToken}` }, + }); + console.log('Status:', apiResp.status); + const data = await apiResp.text(); + if (apiResp.status >= 400) { + console.error('Error body:', data); + process.exit(1); + } + try { + console.log(JSON.stringify(JSON.parse(data), null, 2)); + } catch { + console.log(data); + } +} + +main().catch((e) => { + console.error('Unhandled error:', e); + process.exit(1); +}); diff --git a/scripts/iap-test/fetch-iap.js b/scripts/iap-test/fetch-iap.js new file mode 100644 index 00000000..d379c41c --- /dev/null +++ b/scripts/iap-test/fetch-iap.js @@ -0,0 +1,110 @@ +/* + * Minimal script to acquire an IAP OIDC ID token and call the API. + * Reads configuration from environment variables (see .env.sample). + */ + +const fs = require('fs'); +const path = require('path'); +const { GoogleAuth } = require('google-auth-library'); +require('dotenv').config(); + +function base64UrlDecode(input) { + // Pad input to multiple of 4 and replace URL-safe chars + input = input.replace(/-/g, '+').replace(/_/g, '/'); + const pad = input.length % 4; + if (pad) input += '='.repeat(4 - pad); + return Buffer.from(input, 'base64').toString('utf8'); +} + +function decodeJwt(jwt) { + try { + const [header, payload] = jwt.split('.'); + const h = JSON.parse(base64UrlDecode(header)); + const p = JSON.parse(base64UrlDecode(payload)); + return { header: h, payload: p }; + } catch (e) { + return null; + } +} + +async function main() { + const audience = process.env.IAP_AUDIENCE; + const apiUrl = process.env.API_URL; + const saJsonPath = process.env.GOOGLE_SA_JSON_PATH; + const saJsonInline = process.env.GOOGLE_SA_JSON; + const logClaims = String(process.env.LOG_JWT_CLAIMS || 'false').toLowerCase() === 'true'; + + if (!audience) { + console.error('Missing IAP_AUDIENCE'); + process.exit(2); + } + if (!apiUrl) { + console.error('Missing API_URL'); + process.exit(2); + } + + let credentials; + if (saJsonPath && fs.existsSync(path.resolve(saJsonPath))) { + credentials = JSON.parse(fs.readFileSync(path.resolve(saJsonPath), 'utf8')); + } else if (saJsonInline) { + credentials = JSON.parse(saJsonInline); + } else { + console.error('Provide GOOGLE_SA_JSON_PATH or GOOGLE_SA_JSON'); + process.exit(2); + } + + const auth = new GoogleAuth({ credentials }); + const client = await auth.getIdTokenClient(audience); + + // Intercept to print the ID token (and optionally decode claims) + const origRequest = client.request.bind(client); + client.request = async (opts) => { + const res = await origRequest(opts); + return res; + }; + + // The getRequestHeaders() includes Authorization header; we can decode claims for sanity + const headers = await client.getRequestHeaders(); + const token = (headers.Authorization || headers.authorization || '').replace(/^Bearer\s+/i, ''); + if (!token) { + console.error('Failed to obtain ID token'); + process.exit(1); + } + if (logClaims) { + const decoded = decodeJwt(token); + if (decoded) { + console.log('JWT header:', JSON.stringify(decoded.header, null, 2)); + console.log('JWT payload:', JSON.stringify(decoded.payload, null, 2)); + if (decoded.payload && decoded.payload.iss) { + console.log('JWT issuer:', decoded.payload.iss); + } + if (decoded.payload && decoded.payload.aud !== audience) { + console.warn('Warning: token aud does not match IAP_AUDIENCE'); + } + } + } + + console.log(`Calling ${apiUrl} with IAP audience ${audience}...`); + try { + const res = await client.request({ url: apiUrl }); + console.log('Status:', res.status); + if (res.status >= 400) { + console.error('Error body:', res.data); + process.exit(1); + } + console.log(typeof res.data === 'string' ? res.data : JSON.stringify(res.data)); + } catch (err) { + if (err.response) { + console.error('Request failed:', err.response.status, err.response.data); + const body = typeof err.response.data === 'string' ? err.response.data : JSON.stringify(err.response.data); + if (err.response.status === 401 && /Invalid GCIP ID token/i.test(body)) { + console.error('Hint: IAP is configured with Identity Platform (GCIP). Use fetch-gcip.js instead of fetch-iap.js.'); + } + } else { + console.error('Error:', err.message); + } + process.exit(1); + } +} + +main(); diff --git a/scripts/iap-test/package.json b/scripts/iap-test/package.json new file mode 100644 index 00000000..362d13bd --- /dev/null +++ b/scripts/iap-test/package.json @@ -0,0 +1,19 @@ +{ + "name": "iap-test", + "private": true, + "version": "0.1.0", + "description": "IAP + GCIP token test scripts for IAP-protected API calls", + "license": "UNLICENSED", + "type": "commonjs", + "scripts": { + "test:iap": "node fetch-iap.js", + "test:gcip": "node fetch-gcip.js", + "test": "node fetch-gcip.js" + }, + "dependencies": { + "dotenv": "^16.4.5", + "firebase-admin": "^12.3.0", + "google-auth-library": "^9.14.2", + "node-fetch": "^2.6.9" + } +} \ No newline at end of file diff --git a/scripts/iap-test/yarn.lock b/scripts/iap-test/yarn.lock new file mode 100644 index 00000000..b1b9c1b4 --- /dev/null +++ b/scripts/iap-test/yarn.lock @@ -0,0 +1,1119 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@fastify/busboy@^3.0.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-3.2.0.tgz#13ed8212f3b9ba697611529d15347f8528058cea" + integrity sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA== + +"@firebase/app-check-interop-types@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz#455b6562c7a3de3ef75ea51f72dfec5829ad6997" + integrity sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ== + +"@firebase/app-types@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.2.tgz#8cbcceba784753a7c0066a4809bc22f93adee080" + integrity sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ== + +"@firebase/auth-interop-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz#927f1f2139a680b55fef0bddbff2c982b08587e8" + integrity sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ== + +"@firebase/component@0.6.9": + version "0.6.9" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.9.tgz#4248cfeab222245ada0d7f78ece95a87574532b4" + integrity sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q== + dependencies: + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/database-compat@1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-1.0.8.tgz#69ab03d00e27a89f65486896ea219094aa38c27f" + integrity sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/database" "1.0.8" + "@firebase/database-types" "1.0.5" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/database-types@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.5.tgz#2d923f42e3d9911b9eec537ed8b5ecaa0ce95c37" + integrity sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ== + dependencies: + "@firebase/app-types" "0.9.2" + "@firebase/util" "1.10.0" + +"@firebase/database@1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.8.tgz#01bb0d0cb5653ae6a6641523f6f085b4c1be9c2f" + integrity sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg== + dependencies: + "@firebase/app-check-interop-types" "0.3.2" + "@firebase/auth-interop-types" "0.2.3" + "@firebase/component" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/logger@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.2.tgz#74dfcfeedee810deb8a7080d5b7eba56aa16ffa2" + integrity sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A== + dependencies: + tslib "^2.1.0" + +"@firebase/util@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.10.0.tgz#9ec8ab54da82bfc31baff0c43cb281998cbeddab" + integrity sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ== + dependencies: + tslib "^2.1.0" + +"@google-cloud/firestore@^7.7.0": + version "7.11.6" + resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-7.11.6.tgz#0a2b26e215aa4f903267f82370450753b84db16a" + integrity sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw== + dependencies: + "@opentelemetry/api" "^1.3.0" + fast-deep-equal "^3.1.1" + functional-red-black-tree "^1.0.1" + google-gax "^4.3.3" + protobufjs "^7.2.6" + +"@google-cloud/paginator@^5.0.0": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-5.0.2.tgz#86ad773266ce9f3b82955a8f75e22cd012ccc889" + integrity sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg== + dependencies: + arrify "^2.0.0" + extend "^3.0.2" + +"@google-cloud/projectify@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-4.0.0.tgz#d600e0433daf51b88c1fa95ac7f02e38e80a07be" + integrity sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA== + +"@google-cloud/promisify@<4.1.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-4.0.0.tgz#a906e533ebdd0f754dca2509933334ce58b8c8b1" + integrity sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g== + +"@google-cloud/storage@^7.7.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-7.18.0.tgz#1b0e7415633e97b8ce0364c3e3aac033a52cb318" + integrity sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ== + dependencies: + "@google-cloud/paginator" "^5.0.0" + "@google-cloud/projectify" "^4.0.0" + "@google-cloud/promisify" "<4.1.0" + abort-controller "^3.0.0" + async-retry "^1.3.3" + duplexify "^4.1.3" + fast-xml-parser "^4.4.1" + gaxios "^6.0.2" + google-auth-library "^9.6.3" + html-entities "^2.5.2" + mime "^3.0.0" + p-limit "^3.0.1" + retry-request "^7.0.0" + teeny-request "^9.0.0" + uuid "^8.0.0" + +"@grpc/grpc-js@^1.10.9": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.3.tgz#4c9b817a900ae4020ddc28515ae4b52c78cfb8da" + integrity sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA== + dependencies: + "@grpc/proto-loader" "^0.8.0" + "@js-sdsl/ordered-map" "^4.4.2" + +"@grpc/proto-loader@^0.7.13": + version "0.7.15" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz#4cdfbf35a35461fc843abe8b9e2c0770b5095e60" + integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + +"@grpc/proto-loader@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz#b6c324dd909c458a0e4aa9bfd3d69cf78a4b9bd8" + integrity sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.5.3" + yargs "^17.7.2" + +"@js-sdsl/ordered-map@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" + integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== + +"@opentelemetry/api@^1.3.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" + integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@types/caseless@*": + version "0.12.5" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.5.tgz#db9468cb1b1b5a925b8f34822f1669df0c5472f5" + integrity sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg== + +"@types/jsonwebtoken@^9.0.4": + version "9.0.10" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz#a7932a47177dcd4283b6146f3bd5c26d82647f09" + integrity sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA== + dependencies: + "@types/ms" "*" + "@types/node" "*" + +"@types/long@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + +"@types/node@*", "@types/node@>=13.7.0": + version "25.1.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.1.0.tgz#95cc584f1f478301efc86de4f1867e5875e83571" + integrity sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA== + dependencies: + undici-types "~7.16.0" + +"@types/node@^22.0.1": + version "22.19.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.7.tgz#434094ee1731ae76c16083008590a5835a8c39c1" + integrity sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw== + dependencies: + undici-types "~6.21.0" + +"@types/request@^2.48.8": + version "2.48.13" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.13.tgz#abdf4256524e801ea8fdda54320f083edb5a6b80" + integrity sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.5" + +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +arrify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +async-retry@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +base64-js@^1.3.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bignumber.js@^9.0.0: + version "9.3.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" + integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== + +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +debug@4, debug@^4.3.4: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +dotenv@^16.4.5: + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +duplexify@^4.0.0, duplexify@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" + integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.2" + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +end-of-stream@^1.4.1: + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== + dependencies: + once "^1.4.0" + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +farmhash-modern@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/farmhash-modern/-/farmhash-modern-1.1.0.tgz#c36b34ad196290d57b0b482dc89e637d0b59835f" + integrity sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-xml-parser@^4.4.1: + version "4.5.3" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz#c54d6b35aa0f23dc1ea60b6c884340c006dc6efb" + integrity sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig== + dependencies: + strnum "^1.1.1" + +faye-websocket@0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +firebase-admin@^12.3.0: + version "12.7.0" + resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-12.7.0.tgz#586c9ed852c4bb2d4d72f0d52c1c48a2b6dee793" + integrity sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA== + dependencies: + "@fastify/busboy" "^3.0.0" + "@firebase/database-compat" "1.0.8" + "@firebase/database-types" "1.0.5" + "@types/node" "^22.0.1" + farmhash-modern "^1.1.0" + jsonwebtoken "^9.0.0" + jwks-rsa "^3.1.0" + node-forge "^1.3.1" + uuid "^10.0.0" + optionalDependencies: + "@google-cloud/firestore" "^7.7.0" + "@google-cloud/storage" "^7.7.0" + +form-data@^2.5.5: + version "2.5.5" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.5.tgz#a5f6364ad7e4e67e95b4a07e2d8c6f711c74f624" + integrity sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.35" + safe-buffer "^5.2.1" + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== + +gaxios@^6.0.0, gaxios@^6.0.2, gaxios@^6.1.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb" + integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + is-stream "^2.0.0" + node-fetch "^2.6.9" + uuid "^9.0.1" + +gcp-metadata@^6.1.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.1.tgz#f65aa69f546bc56e116061d137d3f5f90bdec494" + integrity sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A== + dependencies: + gaxios "^6.1.1" + google-logging-utils "^0.0.2" + json-bigint "^1.0.0" + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +google-auth-library@^9.14.2, google-auth-library@^9.3.0, google-auth-library@^9.6.3: + version "9.15.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.15.1.tgz#0c5d84ed1890b2375f1cd74f03ac7b806b392928" + integrity sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^6.1.1" + gcp-metadata "^6.1.0" + gtoken "^7.0.0" + jws "^4.0.0" + +google-gax@^4.3.3: + version "4.6.1" + resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-4.6.1.tgz#57f8e3d893d4c708a71167cdcf47eb3afab95929" + integrity sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ== + dependencies: + "@grpc/grpc-js" "^1.10.9" + "@grpc/proto-loader" "^0.7.13" + "@types/long" "^4.0.0" + abort-controller "^3.0.0" + duplexify "^4.0.0" + google-auth-library "^9.3.0" + node-fetch "^2.7.0" + object-hash "^3.0.0" + proto3-json-serializer "^2.0.2" + protobufjs "^7.3.2" + retry-request "^7.0.0" + uuid "^9.0.1" + +google-logging-utils@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz#5fd837e06fa334da450433b9e3e1870c1594466a" + integrity sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ== + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +gtoken@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" + integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== + dependencies: + gaxios "^6.0.0" + jws "^4.0.0" + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +html-entities@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.6.0.tgz#7c64f1ea3b36818ccae3d3fb48b6974208e984f8" + integrity sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ== + +http-parser-js@>=0.5.1: + version "0.5.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" + integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +https-proxy-agent@^7.0.1: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + +inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +jose@^4.15.4: + version "4.15.9" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.9.tgz#9b68eda29e9a0614c042fa29387196c7dd800100" + integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA== + +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + +jsonwebtoken@^9.0.0: + version "9.0.3" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2" + integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== + 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" + +jwa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jwks-rsa@^3.1.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-3.2.2.tgz#f6d528306befacdbc62c8c0272761faac55ffbb3" + integrity sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w== + dependencies: + "@types/jsonwebtoken" "^9.0.4" + debug "^4.3.4" + jose "^4.15.4" + limiter "^1.1.5" + lru-memoizer "^2.2.0" + +jws@^4.0.0, jws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" + integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== + dependencies: + jwa "^2.0.1" + safe-buffer "^5.0.1" + +limiter@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" + integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +long@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== + +lru-cache@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lru-memoizer@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.3.0.tgz#ef0fbc021bceb666794b145eefac6be49dc47f31" + integrity sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug== + dependencies: + lodash.clonedeep "^4.5.0" + lru-cache "6.0.0" + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.35: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + +ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +node-fetch@^2.6.9, node-fetch@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751" + integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg== + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +proto3-json-serializer@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz#5b705203b4d58f3880596c95fad64902617529dd" + integrity sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ== + dependencies: + protobufjs "^7.2.5" + +protobufjs@^7.2.5, protobufjs@^7.2.6, protobufjs@^7.3.2, protobufjs@^7.5.3: + version "7.5.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" + integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + +readable-stream@^3.1.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +retry-request@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-7.0.2.tgz#60bf48cfb424ec01b03fca6665dee91d06dd95f3" + integrity sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w== + dependencies: + "@types/request" "^2.48.8" + extend "^3.0.2" + teeny-request "^9.0.0" + +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +semver@^7.5.4: + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + +stream-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== + dependencies: + stubs "^3.0.0" + +stream-shift@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strnum@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.1.2.tgz#57bca4fbaa6f271081715dbc9ed7cee5493e28e4" + integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA== + +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== + +teeny-request@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-9.0.0.tgz#18140de2eb6595771b1b02203312dfad79a4716d" + integrity sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.9" + stream-events "^1.0.5" + uuid "^9.0.0" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tslib@^2.1.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + +uuid@^8.0.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +uuid@^9.0.0, uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + 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" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/src/app/actions/auth-actions.ts b/src/app/actions/auth-actions.ts deleted file mode 100644 index 7b569801..00000000 --- a/src/app/actions/auth-actions.ts +++ /dev/null @@ -1,27 +0,0 @@ -'use server'; - -import { cookies } from 'next/headers'; - -export async function setAuthTokenAction(token: string | null): Promise { - const cookieStore = await cookies(); - - if (token != null && token.length > 0) { - // We assume the toke is valid -> could add extra layer of security but will make an extra call to Firebase - // If bad token is provided, it will be rejected by external API - cookieStore.set('firebase_token', token, { - path: '/', - maxAge: 3600, - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - }); - } else { - cookieStore.set('firebase_token', '', { - path: '/', - maxAge: 0, - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - }); - } -} diff --git a/src/app/components/AuthTokenSync.tsx b/src/app/components/AuthTokenSync.tsx deleted file mode 100644 index 1489a167..00000000 --- a/src/app/components/AuthTokenSync.tsx +++ /dev/null @@ -1,26 +0,0 @@ -'use client'; - -import { useEffect, type ReactElement } from 'react'; -import { app } from '../../firebase'; -import { setAuthTokenAction } from '../actions/auth-actions'; - -export default function AuthTokenSync(): ReactElement | null { - useEffect(() => { - // Sets the auth token in a httpOnly cookie for server side requests - // Uses Next.js const cookieStore = await cookies(); for cookie management - const unsubscribe = app.auth().onIdTokenChanged(async (user) => { - if (user != null) { - const token = await user.getIdToken(); - await setAuthTokenAction(token); - } else { - await setAuthTokenAction(null); - } - }); - - return () => { - unsubscribe(); - }; - }, []); - - return null; -} diff --git a/src/app/providers.tsx b/src/app/providers.tsx index f780ffad..4ab03097 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -9,7 +9,6 @@ import { type RemoteConfigValues } from './interface/RemoteConfig'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import AuthTokenSync from './components/AuthTokenSync'; interface ProvidersProps { children: React.ReactNode; @@ -23,7 +22,6 @@ export function Providers({ }: ProvidersProps): React.ReactElement { return ( - {children} diff --git a/src/app/utils/auth-server.ts b/src/app/utils/auth-server.ts index c29225ce..bba4b057 100644 --- a/src/app/utils/auth-server.ts +++ b/src/app/utils/auth-server.ts @@ -1,45 +1,103 @@ -import { cookies } from 'next/headers'; -import { app } from '../../firebase'; -import firebase from 'firebase/compat/app'; -import 'firebase/compat/auth'; -// TODO: for anonymous auth use fireabse admin +import 'server-only'; +import { getAuth } from 'firebase-admin/auth'; +import { getEnvConfig, nonEmpty } from './config'; +import { getFirebaseAdminApp } from '../../lib/firebase-admin'; + +interface CachedToken { + token: string; + expiresAt: number; // epoch ms +} + +let cached: CachedToken | undefined; +function now(): number { + return Date.now(); +} + +async function exchangeCustomTokenForIdToken( + customToken: string, +): Promise<{ idToken: string; expiresInSec?: number }> { + // Accept multiple env var names for dev/prod convenience + const apiKey = + nonEmpty(getEnvConfig('GCIP_API_KEY')) ?? + nonEmpty(getEnvConfig('FIREBASE_API_KEY')) ?? + nonEmpty(getEnvConfig('NEXT_PUBLIC_FIREBASE_API_KEY')); + if (apiKey == undefined) { + throw new Error('GCIP/Firebase API key is not set'); + } + + const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`; + const body: Record = { + token: customToken, + returnSecureToken: true, + }; + + const tenantId = + nonEmpty(getEnvConfig('GCIP_TENANT_ID')) ?? + nonEmpty(getEnvConfig('NEXT_PUBLIC_GCIP_TENANT_ID')); + if (tenantId != undefined) { + body.tenantId = tenantId; + } + + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + cache: 'no-store', + }); + if (!resp.ok) { + const text = await resp.text(); + throw new Error( + `GCIP signInWithCustomToken failed: ${resp.status} ${text}`, + ); + } + const data = (await resp.json()) as { + idToken: string; + expiresIn?: string; // seconds as string + }; + const expiresInSec = data.expiresIn ? Number(data.expiresIn) : undefined; + return { idToken: data.idToken, expiresInSec }; +} /** - * Retrieves the Firebase access token from the 'firebase_token' cookie. - * If the cookie is missing, performs a server-side anonymous login to generate a token. - * This ensures SSR pages can access the API even for direct, unauthenticated visits. + * Returns a GCIP ID token suitable for calling an IAP-protected API configured with Identity Platform. + * Caches the token until near expiry to minimize exchanges. */ -export async function getSSRAccessToken(): Promise { - const cookieStore = await cookies(); - const tokenCookie = cookieStore.get('firebase_token'); - - if (tokenCookie?.value != null && tokenCookie.value.length > 0) { - try { - // Basic JWT decoding to check expiry - const token = tokenCookie.value; - const payloadBase64 = token.split('.')[1]; - const payload = JSON.parse( - Buffer.from(payloadBase64, 'base64').toString(), - ); - const now = Math.floor(Date.now() / 1000); - - if (payload.exp != null && payload.exp > now) { - return token; - } - } catch (error) {} +export async function getGcipIdToken(): Promise { + // Use cached token if still valid for at least 60 seconds + if (cached != undefined && cached.expiresAt - now() > 60_000) { + return cached.token; } - // Fallback: Server-side Anonymous Login - // We use NONE persistence to verify we don't store this session in any shared environment storage - try { - const auth = app.auth(); - await auth.setPersistence(firebase.auth.Auth.Persistence.NONE); - const userCredential = await auth.signInAnonymously(); - if (userCredential.user != null) { - const token = await userCredential.user.getIdToken(); - return token; - } - } catch (error) {} - - return ''; + // Ensure Admin app is initialized centrally + const adminApp = getFirebaseAdminApp(); + const serviceUid = + nonEmpty(getEnvConfig('GCIP_SERVICE_UID')) ?? + nonEmpty(getEnvConfig('NEXT_GCIP_SERVICE_UID')) ?? + 'iap-service-caller'; + const customToken = await getAuth(adminApp).createCustomToken(serviceUid, { + service: true, + }); + const { idToken, expiresInSec } = + await exchangeCustomTokenForIdToken(customToken); + // Default TTL ~ 55 minutes if expiresIn not present + const ttlMs = (expiresInSec ?? 3600) * 1000; + const safetyMs = 300_000; // refresh 5 minutes early + cached = { + token: idToken, + expiresAt: now() + ttlMs - safetyMs, + }; + return idToken; +} + +// export interface EndUserIdentity { +// subject?: string; +// email?: string; +// } + +/** + * Returns a GCIP ID token suitable for IAP-protected API calls. + * This avoids trusting client tokens and keeps credentials server-side only. + */ +export async function getSSRAccessToken(): Promise { + return await getGcipIdToken(); } diff --git a/src/app/utils/config.ts b/src/app/utils/config.ts index d419a3d2..6e33c526 100644 --- a/src/app/utils/config.ts +++ b/src/app/utils/config.ts @@ -38,3 +38,19 @@ export const getFeedFilesBaseUrl = (): string => { } return `https://${prefix}files.mobilitydatabase.org`; }; + +/** + * Treat empty or whitespace-only strings as "not set". + * + * @param value Raw string. + * @returns The trimmed string when non-empty; otherwise undefined. + */ +export const nonEmpty = ( + value: string | undefined | null, +): string | undefined => { + if (value == undefined) { + return undefined; + } + const trimmed = value.trim(); + return trimmed === '' ? undefined : trimmed; +}; diff --git a/src/lib/firebase-admin.ts b/src/lib/firebase-admin.ts index 54196444..35e2c8cb 100644 --- a/src/lib/firebase-admin.ts +++ b/src/lib/firebase-admin.ts @@ -1,4 +1,13 @@ import { initializeApp, getApps, cert, type App } from 'firebase-admin/app'; +import fs from 'node:fs'; +import path from 'node:path'; +import { getEnvConfig, nonEmpty } from '../app/utils/config'; + +/** + * Centralized Firebase Admin initialization. + * Prefers explicit service account credentials via env. + * Fallback to ADC only when GOOGLE_APPLICATION_CREDENTIALS is set. + */ /** * Server-only Firebase Admin SDK initialization. @@ -10,44 +19,48 @@ import { initializeApp, getApps, cert, type App } from 'firebase-admin/app'; let adminApp: App | undefined; -export function getFirebaseAdminApp(): App { - if (adminApp != undefined) { - return adminApp; - } - +function ensureAdminInitialized(): App { + // Reuse already initialized app const existingApps = getApps(); + const projectId = getEnvConfig('NEXT_PUBLIC_FIREBASE_PROJECT_ID'); if (existingApps.length > 0) { - adminApp = existingApps.find( - (existingApp) => - existingApp.options.projectId === process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, - ); - if (adminApp != undefined) { - return adminApp; - } + const matchedApp = + existingApps.find((app) => app.options?.projectId === projectId) ?? + existingApps[0]; + return matchedApp; } - // Check if we have explicit credentials via environment variable - const serviceAccountJson = process.env.FIREBASE_SERVICE_ACCOUNT_JSON; - - if (serviceAccountJson != null && serviceAccountJson.length > 0) { - try { - const serviceAccount = JSON.parse(serviceAccountJson); - adminApp = initializeApp({ - credential: cert(serviceAccount), - projectId: serviceAccount.project_id, - }); - } catch (error) { - console.error('Failed to parse FIREBASE_SERVICE_ACCOUNT_JSON:', error); - // Fall through to ADC - } + // Prefer inline service account JSON (server-only) + const inlineJson = nonEmpty(getEnvConfig('GOOGLE_SA_JSON')); + if (inlineJson != undefined) { + const serviceAccount = JSON.parse(inlineJson); + return initializeApp({ + credential: cert(serviceAccount), + projectId: serviceAccount.project_id, + }); } - if (adminApp == undefined) { - // Use Application Default Credentials (works on Cloud Run automatically) - adminApp = initializeApp({ - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + // Or load from file path + const filePath = nonEmpty(getEnvConfig('GOOGLE_SA_JSON_PATH')); + if (filePath != undefined) { + const raw = fs.readFileSync(path.resolve(filePath), 'utf8'); + const serviceAccount = JSON.parse(raw); + return initializeApp({ + credential: cert(serviceAccount), + projectId: serviceAccount.project_id ?? projectId, }); } + // No credentials provided: fail fast instead of attempting metadata server + throw new Error( + 'Missing server-side credentials. Set GOOGLE_SA_JSON (inline), or GOOGLE_SA_JSON_PATH(file path).', + ); +} + +export function getFirebaseAdminApp(): App { + if (adminApp != undefined) { + return adminApp; + } + adminApp = ensureAdminInitialized(); return adminApp; } From e0152db2b601f0d27d68715c14ff6cfeac6351df Mon Sep 17 00:00:00 2001 From: David Gamez Diaz <1192523+davidgamez@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:19:47 -0500 Subject: [PATCH 02/11] fix lint --- src/app/providers.tsx | 1 - src/app/utils/auth-server.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 4ab03097..33c30d7e 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -9,7 +9,6 @@ import { type RemoteConfigValues } from './interface/RemoteConfig'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; - interface ProvidersProps { children: React.ReactNode; remoteConfig: RemoteConfigValues; diff --git a/src/app/utils/auth-server.ts b/src/app/utils/auth-server.ts index bba4b057..c00ee90d 100644 --- a/src/app/utils/auth-server.ts +++ b/src/app/utils/auth-server.ts @@ -54,7 +54,8 @@ async function exchangeCustomTokenForIdToken( idToken: string; expiresIn?: string; // seconds as string }; - const expiresInSec = data.expiresIn ? Number(data.expiresIn) : undefined; + const expiresInSec = + data.expiresIn != null ? Number(data.expiresIn) : undefined; return { idToken: data.idToken, expiresInSec }; } From 2f395e17eb4cfd3d6e0b10c212f6e633d4b9edef Mon Sep 17 00:00:00 2001 From: David Gamez Diaz <1192523+davidgamez@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:59:34 -0500 Subject: [PATCH 03/11] add mock server for lcal dev and documentation --- cypress/fixtures/feed_datasets_mdb-2947.json | 29 +++++++++++ cypress/fixtures/feed_mdb-2947.json | 21 ++++++++ cypress/fixtures/gtfs_feed_mdb-2947.json | 53 ++++++++++++++++++++ src/app/providers.tsx | 12 +++++ src/app/utils/auth-server.ts | 12 +++-- src/lib/firebase-admin.ts | 39 +++++++++----- src/lib/remote-config.server.ts | 15 ++++++ src/mocks/browser.ts | 5 ++ src/mocks/data/gtfs_feed_mdb-2947.json | 14 ++++++ src/mocks/data/gtfs_feed_test-516.json | 14 ++++++ src/mocks/handlers.ts | 43 ++++++++++++++++ 11 files changed, 239 insertions(+), 18 deletions(-) create mode 100644 cypress/fixtures/feed_datasets_mdb-2947.json create mode 100644 cypress/fixtures/feed_mdb-2947.json create mode 100644 cypress/fixtures/gtfs_feed_mdb-2947.json create mode 100644 src/mocks/browser.ts create mode 100644 src/mocks/data/gtfs_feed_mdb-2947.json create mode 100644 src/mocks/data/gtfs_feed_test-516.json diff --git a/cypress/fixtures/feed_datasets_mdb-2947.json b/cypress/fixtures/feed_datasets_mdb-2947.json new file mode 100644 index 00000000..12684689 --- /dev/null +++ b/cypress/fixtures/feed_datasets_mdb-2947.json @@ -0,0 +1,29 @@ +[ + { + "id": "mdb-2947-202410010000", + "feed_id": "mdb-2947", + "hosted_url": "https://files.example.com/mdb-2947/mdb-2947-202410010000.zip", + "note": null, + "downloaded_at": "2024-10-01T00:00:15Z", + "hash": "mockhash2947", + "bounding_box": { + "minimum_latitude": 40.0, + "maximum_latitude": 41.0, + "minimum_longitude": -75.0, + "maximum_longitude": -74.0 + }, + "validation_report": { + "validated_at": "2024-10-01T00:10:00Z", + "features": ["Transfers", "Shapes"], + "validator_version": "5.0.2-SNAPSHOT", + "total_error": 0, + "total_warning": 10, + "total_info": 0, + "unique_error_count": 0, + "unique_warning_count": 2, + "unique_info_count": 0, + "url_json": "https://files.example.com/mdb-2947/report.json", + "url_html": "https://files.example.com/mdb-2947/report.html" + } + } +] \ No newline at end of file diff --git a/cypress/fixtures/feed_mdb-2947.json b/cypress/fixtures/feed_mdb-2947.json new file mode 100644 index 00000000..2236a916 --- /dev/null +++ b/cypress/fixtures/feed_mdb-2947.json @@ -0,0 +1,21 @@ +{ + "id": "mdb-2947", + "data_type": "gtfs", + "status": "active", + "created_at": "2024-10-01T00:00:00Z", + "external_ids": [ + { "external_id": "2947", "source": "mdb" } + ], + "provider": "Example Transit Agency", + "feed_name": "Example City Transit", + "note": "Mock fixture for local dev", + "feed_contact_email": "", + "source_info": { + "producer_url": "https://example.com/gtfs.zip", + "authentication_type": 0, + "authentication_info_url": "", + "api_key_parameter_name": "", + "license_url": "" + }, + "redirects": [] +} \ No newline at end of file diff --git a/cypress/fixtures/gtfs_feed_mdb-2947.json b/cypress/fixtures/gtfs_feed_mdb-2947.json new file mode 100644 index 00000000..ecbdd30e --- /dev/null +++ b/cypress/fixtures/gtfs_feed_mdb-2947.json @@ -0,0 +1,53 @@ +{ + "id": "mdb-2947", + "data_type": "gtfs", + "status": "active", + "created_at": "2024-10-01T00:00:00Z", + "external_ids": [ + { "external_id": "2947", "source": "mdb" } + ], + "provider": "Example Transit Agency", + "feed_name": "Example City Transit", + "note": "Mock fixture for local dev", + "feed_contact_email": "", + "source_info": { + "producer_url": "https://example.com/gtfs.zip", + "authentication_type": 0, + "authentication_info_url": "", + "api_key_parameter_name": "", + "license_url": "" + }, + "redirects": [], + "locations": [ + { + "country_code": "US", + "subdivision_name": "Example State", + "municipality": "Example City" + } + ], + "latest_dataset": { + "id": "mdb-2947-202410010000", + "hosted_url": "https://files.example.com/mdb-2947/mdb-2947-202410010000.zip", + "bounding_box": { + "minimum_latitude": 40.0, + "maximum_latitude": 41.0, + "minimum_longitude": -75.0, + "maximum_longitude": -74.0 + }, + "downloaded_at": "2024-10-01T00:00:15Z", + "hash": "mockhash2947", + "validation_report": { + "validated_at": "2024-10-01T00:10:00Z", + "features": ["Transfers", "Shapes"], + "validator_version": "5.0.2-SNAPSHOT", + "total_error": 0, + "total_warning": 10, + "total_info": 0, + "unique_error_count": 0, + "unique_warning_count": 2, + "unique_info_count": 0, + "url_json": "https://files.example.com/mdb-2947/report.json", + "url_html": "https://files.example.com/mdb-2947/report.html" + } + } +} \ No newline at end of file diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 33c30d7e..b3cf8808 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -19,6 +19,18 @@ export function Providers({ children, remoteConfig, }: ProvidersProps): React.ReactElement { + // Start MSW in mock mode to intercept API calls client-side + React.useEffect(() => { + if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') { + // Lazy-load the worker to avoid bundling in prod + import('../mocks/browser') + .then(async ({ worker }) => await worker.start()) + .catch((err) => { + console.warn('MSW mock worker failed to start:', err); + }); + } + }, []); + return ( diff --git a/src/app/utils/auth-server.ts b/src/app/utils/auth-server.ts index c00ee90d..06dac163 100644 --- a/src/app/utils/auth-server.ts +++ b/src/app/utils/auth-server.ts @@ -64,6 +64,13 @@ async function exchangeCustomTokenForIdToken( * Caches the token until near expiry to minimize exchanges. */ export async function getGcipIdToken(): Promise { + // Dev/mock bypass: allow local runs without Firebase Admin/service accounts + const isMock = + getEnvConfig('NEXT_PUBLIC_API_MOCKING') === 'enabled' || + getEnvConfig('LOCAL_DEV_NO_ADMIN') === '1'; + if (isMock) { + return 'dev-mock-token'; + } // Use cached token if still valid for at least 60 seconds if (cached != undefined && cached.expiresAt - now() > 60_000) { return cached.token; @@ -90,11 +97,6 @@ export async function getGcipIdToken(): Promise { return idToken; } -// export interface EndUserIdentity { -// subject?: string; -// email?: string; -// } - /** * Returns a GCIP ID token suitable for IAP-protected API calls. * This avoids trusting client tokens and keeps credentials server-side only. diff --git a/src/lib/firebase-admin.ts b/src/lib/firebase-admin.ts index 35e2c8cb..07eeee33 100644 --- a/src/lib/firebase-admin.ts +++ b/src/lib/firebase-admin.ts @@ -3,22 +3,35 @@ import fs from 'node:fs'; import path from 'node:path'; import { getEnvConfig, nonEmpty } from '../app/utils/config'; -/** - * Centralized Firebase Admin initialization. - * Prefers explicit service account credentials via env. - * Fallback to ADC only when GOOGLE_APPLICATION_CREDENTIALS is set. - */ +let adminApp: App | undefined; /** - * Server-only Firebase Admin SDK initialization. - * Uses Application Default Credentials (ADC) which works automatically on Cloud Run. - * For local development, you can either: - * 1. Run `gcloud auth application-default login` - * 2. Set GOOGLE_APPLICATION_CREDENTIALS env var to a service account JSON path + * ensureAdminInitialized + * Creates or reuses a singleton Firebase Admin App. + * + * Selection order: + * 1) Reuse an existing Admin app from `getApps()`, preferring the one whose + * `options.projectId` matches `NEXT_PUBLIC_FIREBASE_PROJECT_ID`; falls back + * to the first app if no match. + * 2) If `GOOGLE_SA_JSON` is set (server-only inline JSON), parse and initialize + * with `cert(serviceAccount)`. + * 3) If `GOOGLE_SA_JSON_PATH` is set, read and parse the JSON file and + * initialize with `cert(serviceAccount)`. + * 4) If none of the above are provided, throws an error to avoid implicit ADC + * behavior (metadata server lookups) in serverless environments. + * + * Environment variables accessed: + * - NEXT_PUBLIC_FIREBASE_PROJECT_ID: Used to match existing apps and as fallback + * when the service account JSON lacks `project_id`. + * - GOOGLE_SA_JSON: Server-only inline service account JSON string + * (must include `project_id`, `client_email`, and `private_key`). + * - GOOGLE_SA_JSON_PATH: Path to a service account JSON file containing the + * same required fields. + * + * Notes: + * - Uses `getEnvConfig` and `nonEmpty` to read configuration consistently. + * - Keep credentials server-only; do not expose inline JSON to client code. */ - -let adminApp: App | undefined; - function ensureAdminInitialized(): App { // Reuse already initialized app const existingApps = getApps(); diff --git a/src/lib/remote-config.server.ts b/src/lib/remote-config.server.ts index d170446f..fa79af3a 100644 --- a/src/lib/remote-config.server.ts +++ b/src/lib/remote-config.server.ts @@ -3,6 +3,7 @@ import 'server-only'; import { cache } from 'react'; import { getRemoteConfig } from 'firebase-admin/remote-config'; import { getFirebaseAdminApp } from './firebase-admin'; +import { getEnvConfig } from '../app/utils/config'; import { defaultRemoteConfigValues, type RemoteConfigValues, @@ -51,6 +52,13 @@ function parseConfigValue( * Returns the template parameters merged with defaults. */ async function fetchRemoteConfigFromFirebase(): Promise { + // Dev/mock bypass: return defaults without touching Admin SDK + const isMock = + getEnvConfig('NEXT_PUBLIC_API_MOCKING') === 'enabled' || + getEnvConfig('LOCAL_DEV_NO_ADMIN') === '1'; + if (isMock) { + return defaultRemoteConfigValues; + } const app = getFirebaseAdminApp(); const remoteConfigAdmin = getRemoteConfig(app); @@ -95,6 +103,13 @@ async function fetchRemoteConfigFromFirebase(): Promise { */ export const getRemoteConfigValues = cache( async (): Promise => { + // Dev/mock bypass: use defaults immediately + const isMock = + getEnvConfig('NEXT_PUBLIC_API_MOCKING') === 'enabled' || + getEnvConfig('LOCAL_DEV_NO_ADMIN') === '1'; + if (isMock) { + return defaultRemoteConfigValues; + } const now = Date.now(); const cacheAge = (now - cacheTimestamp) / 1000; diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts new file mode 100644 index 00000000..245f7605 --- /dev/null +++ b/src/mocks/browser.ts @@ -0,0 +1,5 @@ +import { setupWorker } from 'msw/browser'; +import { handlers } from './handlers'; + +// MSW browser worker to intercept client-side fetch/XHR +export const worker = setupWorker(...handlers); diff --git a/src/mocks/data/gtfs_feed_mdb-2947.json b/src/mocks/data/gtfs_feed_mdb-2947.json new file mode 100644 index 00000000..131d294b --- /dev/null +++ b/src/mocks/data/gtfs_feed_mdb-2947.json @@ -0,0 +1,14 @@ +{ + "id": "mdb-2947", + "data_type": "gtfs", + "provider": "Example Transit Agency", + "feed_name": "Example City Transit", + "locations": [ + { + "country_code": "US", + "subdivision_name": "Example State", + "municipality": "Example City", + "country": "United States" + } + ] +} \ No newline at end of file diff --git a/src/mocks/data/gtfs_feed_test-516.json b/src/mocks/data/gtfs_feed_test-516.json new file mode 100644 index 00000000..c96deb6d --- /dev/null +++ b/src/mocks/data/gtfs_feed_test-516.json @@ -0,0 +1,14 @@ +{ + "id": "test-516", + "data_type": "gtfs", + "provider": "Metropolitan Transit Authority (MTA)", + "feed_name": "NYC Subway", + "locations": [ + { + "country_code": "US", + "subdivision_name": "New York", + "municipality": "New York City", + "country": "United States" + } + ] +} \ No newline at end of file diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index e1cc895e..80f87426 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -4,8 +4,37 @@ import { http, HttpResponse } from 'msw'; import feedJson from '../../cypress/fixtures/feed_test-516.json'; import gtfsFeedJson from '../../cypress/fixtures/gtfs_feed_test-516.json'; import datasetsFeedJson from '../../cypress/fixtures/feed_datasets_test-516.json'; +import feed2947 from '../../cypress/fixtures/feed_mdb-2947.json'; +import gtfsFeed2947 from '../../cypress/fixtures/gtfs_feed_mdb-2947.json'; +import datasets2947 from '../../cypress/fixtures/feed_datasets_mdb-2947.json'; export const handlers = [ + // Mock search endpoint: return a list including test-516 and mdb-2947 + http.get(`*/v1/search`, () => { + return HttpResponse.json({ + total: 2, + results: [ + { + id: 'test-516', + data_type: 'gtfs', + feed_name: gtfsFeedJson.feed_name, + provider: gtfsFeedJson.provider, + official: false, + locations: gtfsFeedJson.locations ?? [], + entity_types: [], + }, + { + id: 'mdb-2947', + data_type: 'gtfs', + feed_name: gtfsFeed2947.feed_name, + provider: gtfsFeed2947.provider, + official: false, + locations: gtfsFeed2947.locations ?? [], + entity_types: [], + }, + ], + }); + }), // Mock GET /v1/feeds/{id} - basic feed info http.get(`*/v1/feeds/test-516`, () => { return HttpResponse.json(feedJson); @@ -30,4 +59,18 @@ export const handlers = [ http.get(/.*test-516.*routes\.json$/, () => { return HttpResponse.json([]); }), + + // --- mdb-2947 --- + http.get(`*/v1/feeds/mdb-2947`, () => { + return HttpResponse.json(feed2947); + }), + http.get(`*/v1/gtfs_feeds/mdb-2947`, () => { + return HttpResponse.json(gtfsFeed2947); + }), + http.get(`*/v1/gtfs_feeds/mdb-2947/datasets`, () => { + return HttpResponse.json(datasets2947); + }), + http.get(`*/v1/gtfs_feeds/mdb-2947/gtfs_rt_feeds`, () => { + return HttpResponse.json([]); + }), ]; From 026b5b3a291708aac58ff22896bbaab01ee6ff31 Mon Sep 17 00:00:00 2001 From: David Gamez Diaz <1192523+davidgamez@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:01:31 -0500 Subject: [PATCH 04/11] add auth documentation --- docs/Authentication.md | 107 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 docs/Authentication.md diff --git a/docs/Authentication.md b/docs/Authentication.md new file mode 100644 index 00000000..7eed59e9 --- /dev/null +++ b/docs/Authentication.md @@ -0,0 +1,107 @@ +# Authentication Architecture (SSR + IAP via GCIP) + +This document explains how server-side authentication works in the Mobility Database Web app when calling the Mobility Feed API behind Google Cloud IAP with Identity Platform (GCIP). It also covers local development without Firebase access. + +## Overview +- Server components/actions call the Mobility Feed API using a server‑minted GCIP ID token. +- The client never forwards its token to the server; all API calls use server credentials only. +- Firebase Admin is initialized once via a centralized helper and used for both Remote Config and token minting. +- Mock mode (MSW) enables local development without hitting the real API or Firebase. + +## Server‑Side Token Flow (IAP + Identity Platform) +1. Firebase Admin creates a **custom token** for a service UID (synthetic user identity). +2. The custom token is exchanged with **Identity Toolkit** (`accounts:signInWithCustomToken`) to obtain a **GCIP ID token**. +3. The GCIP ID token is added as `Authorization: Bearer ` for calls to the IAP‑protected Mobility Feed API. +4. Tokens are cached server‑side and refreshed a few minutes before expiry. + +Key code paths: +- `src/lib/firebase-admin.ts`: centralized Admin initialization (`getFirebaseAdminApp()`), backed by `ensureAdminInitialized()`. +- `src/app/utils/auth-server.ts`: token functions `getGcipIdToken()` and `getSSRAccessToken()` (the canonical token provider for SSR calls). +- `src/app/services/feeds/index.ts`: OpenAPI client; injects `Authorization` header using the token returned by `getSSRAccessToken()`. + +## Firebase Admin Initialization +Centralized in `getFirebaseAdminApp()`: +- Reuse existing Admin app if already initialized (matching `NEXT_PUBLIC_FIREBASE_PROJECT_ID`). +- Prefer **inline service account JSON**: `GOOGLE_SA_JSON` (or `FIREBASE_SERVICE_ACCOUNT_JSON` if enabled in your environment). +- Or load **from file path**: `GOOGLE_SA_JSON_PATH` (or `GOOGLE_APPLICATION_CREDENTIALS`). +- Fail fast if no credentials are provided to avoid unpredictable ADC/metadata lookups in serverless/dev environments. + +Required service account fields: `project_id`, `client_email`, `private_key`. + +## Environment Variables +Server‑side credentials and config (server‑only): +- `GOOGLE_SA_JSON`: Inline service account JSON string. +- `GOOGLE_SA_JSON_PATH`: Absolute/relative path to service account JSON file. +- `NEXT_PUBLIC_FIREBASE_PROJECT_ID`: Project ID; used to match Admin apps and as fallback when JSON lacks `project_id`. + +GCIP / Identity Toolkit: +- `GCIP_API_KEY` (or `FIREBASE_API_KEY` or `NEXT_PUBLIC_FIREBASE_API_KEY`): API key for `accounts:signInWithCustomToken`. +- `GCIP_TENANT_ID` (optional): Tenant ID when using multi‑tenant Identity Platform. +- `GCIP_SERVICE_UID` (optional): Synthetic UID for the server caller (default: `iap-service-caller`). + +Mock/dev: +- `NEXT_PUBLIC_API_MOCKING=enabled`: Enables MSW mock service worker in the browser. +- `LOCAL_DEV_NO_ADMIN=1` (optional): Bypass Admin initialization for Remote Config/token code paths during local experimentation. + +## Remote Config +- Server‑side code fetches Firebase Remote Config via Admin SDK. +- In mock mode, Remote Config returns defaults to avoid Admin calls. +- Entry points: `src/lib/remote-config.server.ts` and the `Providers` wrapper in `src/app/providers.tsx`. + +## Mock Mode (No Real API or Firebase) +Use **MSW** (Mock Service Worker) so mocks behave like a real API without changing app logic. + +Setup: +1. Initialize the worker (one‑time): + ```bash + npx msw init public/ + ``` +2. Start dev in mock mode: + ```bash + NEXT_PUBLIC_API_MOCKING=enabled yarn start:dev:mock + ``` +3. Mock handlers live in `src/mocks/handlers.ts`. + - The browser worker starts from `src/mocks/browser.ts` via `src/app/providers.tsx` when mock mode is enabled. + +## Usage in Code +- SSR/API calls: Always obtain `accessToken = await getSSRAccessToken()` in server components/actions, then pass to `openapi-fetch` client. +- Do **not** forward client tokens to the server. +- Keep all credentials server‑only and never expose service account JSON to client code. + +## Security Considerations +- **No client token pass‑through**: Prevents elevation of privilege and token replay. +- **Server‑only credentials**: Service account material must never be sent to the client. +- **Optional auditing**: You may forward end‑user identity headers (e.g., `X-User-Subject`, `X-User-Email`) for backend audit trails if required. + +## Troubleshooting +Common issues and fixes: +- "Firebase app already exists": Multiple Admin initializations. Use centralized `getFirebaseAdminApp()` and reuse existing apps. +- "Invalid GCIP ID token": IAP configured with Identity Platform requires **GCIP tokens**, not Google OIDC tokens. +- Metadata errors (ENOTFOUND): ADC fallback on local/dev without credentials. Provide explicit service account JSON/path and avoid metadata server. +- Missing fields: Ensure service account JSON contains `project_id`, `client_email`, `private_key`. +- MSW not intercepting: Confirm `NEXT_PUBLIC_API_MOCKING=enabled`, `public/mockServiceWorker.js` exists, and the worker starts in `providers.tsx`. + +## Quick Start (Local Dev) +1. Provide service account (either inline or file path): + - Inline (recommended for Vercel server env): + - Set `GOOGLE_SA_JSON` to a single‑line JSON string. + - File path: + - Set `GOOGLE_SA_JSON_PATH` to the JSON file path. +2. Start dev: + ```bash + yarn start:dev + ``` +3. Optional mock mode: + ```bash + npx msw init public/ + NEXT_PUBLIC_API_MOCKING=enabled yarn start:dev:mock + ``` + +## Deployment (Vercel) +- Set server‑only env vars in Vercel: + - `GOOGLE_SA_JSON`, `GCIP_API_KEY`, and optionally `GCIP_TENANT_ID`, `GCIP_SERVICE_UID`. +- Ensure `NEXT_PUBLIC_FIREBASE_PROJECT_ID` matches your Firebase project. +- Use Node runtime for server components/actions (default in Next.js 16 App Router). + +--- +For questions or improvements, see `src/lib/firebase-admin.ts`, `src/app/utils/auth-server.ts`, and `src/mocks/handlers.ts` for practical references. From 23871e40aa9bcea7f0d4805d87e12fa287e73cf1 Mon Sep 17 00:00:00 2001 From: David Gamez Diaz <1192523+davidgamez@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:17:55 -0500 Subject: [PATCH 05/11] add jwt session cookie --- src/app/api/session/route.ts | 89 +++++++ src/app/context/api-auth-middleware.ts | 43 ++++ .../feeds/[feedDataType]/[feedId]/page.tsx | 31 ++- src/app/services/feeds/index.ts | 228 ++++++++---------- src/app/services/session-service.ts | 26 ++ src/app/store/saga/auth-saga.ts | 8 + src/app/utils/auth-server.ts | 131 +++++++++- src/app/utils/session-jwt.ts | 150 ++++++++++++ 8 files changed, 560 insertions(+), 146 deletions(-) create mode 100644 src/app/api/session/route.ts create mode 100644 src/app/context/api-auth-middleware.ts create mode 100644 src/app/services/session-service.ts create mode 100644 src/app/utils/session-jwt.ts diff --git a/src/app/api/session/route.ts b/src/app/api/session/route.ts new file mode 100644 index 00000000..4feded04 --- /dev/null +++ b/src/app/api/session/route.ts @@ -0,0 +1,89 @@ +import { type NextRequest, NextResponse } from 'next/server'; +import { getAuth } from 'firebase-admin/auth'; +import { getFirebaseAdminApp } from '../../../lib/firebase-admin'; +import { signSessionToken, verifySessionToken } from '../../utils/session-jwt'; + +const COOKIE_NAME = 'md_session'; + +function isProduction(): boolean { + return process.env.NODE_ENV === 'production'; +} + +export async function POST(req: NextRequest): Promise { + try { + const body = (await req.json()) as { idToken?: string }; + const idToken = body?.idToken; + + if (idToken == null || typeof idToken !== 'string') { + return NextResponse.json({ error: 'Missing idToken' }, { status: 400 }); + } + + const app = getFirebaseAdminApp(); + const decoded = await getAuth(app).verifyIdToken(idToken); + + // Log the user id server-side for auditing/debugging + console.info('Session established for user', { + uid: decoded.uid, + provider: decoded.firebase?.sign_in_provider, + }); + + const response = NextResponse.json({ status: 'ok' }); + + // Create a short-lived server-signed session token that only contains + // the user id and expiry, not the Firebase ID token itself. + const sessionMaxAgeSec = 60 * 60; // 1 hour + const isGuest = decoded.firebase?.sign_in_provider === 'anonymous'; + const sessionToken = signSessionToken( + decoded.uid, + decoded.email ?? undefined, + { + ttlSeconds: sessionMaxAgeSec, + isGuest, + }, + ); + console.info(decoded); + response.cookies.set({ + name: COOKIE_NAME, + value: sessionToken, + httpOnly: true, + secure: isProduction(), + sameSite: 'lax', + path: '/', + maxAge: sessionMaxAgeSec, + }); + + return response; + } catch (error) { + console.error('Error establishing session', error); + return NextResponse.json( + { error: 'Invalid or expired token' }, + { status: 401 }, + ); + } +} + +export async function GET(req: NextRequest): Promise { + try { + const cookie = req.cookies.get(COOKIE_NAME)?.value; + if (cookie == null) { + return NextResponse.json({ authenticated: false }, { status: 200 }); + } + + const session = verifySessionToken(cookie); + if (session == null) { + return NextResponse.json({ authenticated: false }, { status: 200 }); + } + + return NextResponse.json( + { + authenticated: true, + uid: session.uid, + isGuest: session.isGuest ?? false, + }, + { status: 200 }, + ); + } catch (error) { + console.error('Error validating session', error); + return NextResponse.json({ authenticated: false }, { status: 200 }); + } +} diff --git a/src/app/context/api-auth-middleware.ts b/src/app/context/api-auth-middleware.ts new file mode 100644 index 00000000..ab350b4f --- /dev/null +++ b/src/app/context/api-auth-middleware.ts @@ -0,0 +1,43 @@ +import type { Middleware } from 'openapi-fetch'; + +/** + * Internal header used to propagate end-user context from the web app + * to backend services. The value is expected to be a compact JWT that + * the backend (Python) can verify and decode. + */ +export const USER_CONTEXT_HEADER = 'x-mdb-user-context'; + +/** + * Builds an OpenAPI client middleware that attaches both the GCIP/IAP + * access token and, optionally, a user-context JWT header to outgoing + * requests. + * + * This helper lives outside the generated API client so that future + * OpenAPI-generated services can all share the same auth wiring. + * + * - `accessToken` is the IAP/GCIP token used for Authorization. + * - `userContextJwt`, when provided, is a server-signed JWT carrying + * minimal user identity (uid/email/isGuest, etc.) that the Python + * service can decode. + */ +export const generateAuthMiddlewareWithToken = ( + accessToken: string, + userContextJwt?: string, +): Middleware => { + return { + async onRequest(req) { + // Always attach the bearer token for IAP/GCIP. + req.headers.set('Authorization', `Bearer ${accessToken}`); + + // When available (typically in server-side code), also attach a + // compact user-context JWT so the backend can attribute calls to + // an end-user without relying on IAP to forward custom claims. + if (userContextJwt != null && userContextJwt !== '') { + console.log('user contet ' + userContextJwt); + req.headers.set(USER_CONTEXT_HEADER, userContextJwt); + } + + return req; + }, + }; +}; diff --git a/src/app/feeds/[feedDataType]/[feedId]/page.tsx b/src/app/feeds/[feedDataType]/[feedId]/page.tsx index d7774e13..715b780f 100644 --- a/src/app/feeds/[feedDataType]/[feedId]/page.tsx +++ b/src/app/feeds/[feedDataType]/[feedId]/page.tsx @@ -12,7 +12,10 @@ import { } from '../../../services/feeds'; import { notFound } from 'next/navigation'; import type { Metadata, ResolvingMetadata } from 'next'; -import { getSSRAccessToken } from '../../../utils/auth-server'; +import { + getSSRAccessToken, + getUserContextJwtFromCookie, +} from '../../../utils/auth-server'; import { type GTFSFeedType, type GTFSRTFeedType, @@ -32,15 +35,16 @@ interface Props { const fetchFeedData = cache( async (feedDataType: string, feedId: string, accessToken: string) => { try { + const userContextJwt = await getUserContextJwtFromCookie(); let feed; if (feedDataType === 'gtfs') { - feed = await getGtfsFeed(feedId, accessToken); + feed = await getGtfsFeed(feedId, accessToken, userContextJwt); } else if (feedDataType === 'gtfs_rt') { - feed = await getGtfsRtFeed(feedId, accessToken); + feed = await getGtfsRtFeed(feedId, accessToken, userContextJwt); } else if (feedDataType === 'gbfs') { - feed = await getGbfsFeed(feedId, accessToken); + feed = await getGbfsFeed(feedId, accessToken, userContextJwt); } else { - feed = await getFeed(feedId, accessToken); + feed = await getFeed(feedId, accessToken, userContextJwt); } return feed; } catch (e) { @@ -52,9 +56,15 @@ const fetchFeedData = cache( const fetchInitialDatasets = cache( async (feedId: string, accessToken: string) => { try { - const datasets = await getGtfsFeedDatasets(feedId, accessToken, { - limit: 10, - }); + const userContextJwt = await getUserContextJwtFromCookie(); + const datasets = await getGtfsFeedDatasets( + feedId, + accessToken, + userContextJwt, + { + limit: 10, + }, + ); return datasets; } catch (e) { return []; @@ -65,9 +75,10 @@ const fetchInitialDatasets = cache( const fetchRelatedFeeds = cache( async (feedReferences: string[], accessToken: string) => { try { + const userContextJwt = await getUserContextJwtFromCookie(); const feedPromises = feedReferences.map( async (feedId) => - await getFeed(feedId, accessToken).catch((e) => { + await getFeed(feedId, accessToken, userContextJwt).catch((e) => { return undefined; }), ); @@ -218,12 +229,14 @@ export default async function FeedPage({ accessToken, ); + const userContextJwt = await getUserContextJwtFromCookie(); const associatedGtfsRtFeedsArrays = await Promise.all( gtfsFeeds.map( async (gtfsFeed) => await getGtfsFeedAssociatedGtfsRtFeeds( gtfsFeed?.id ?? '', accessToken, + userContextJwt, ), ), ); diff --git a/src/app/services/feeds/index.ts b/src/app/services/feeds/index.ts index 7e4eb384..baa5d4dd 100644 --- a/src/app/services/feeds/index.ts +++ b/src/app/services/feeds/index.ts @@ -3,6 +3,7 @@ import type { paths } from './types'; import { type AllFeedsParams, type AllFeedType } from './utils'; import { type GtfsRoute } from '../../types'; import { getFeedFilesBaseUrl } from '../../utils/config'; +import { generateAuthMiddlewareWithToken } from '../../context/api-auth-middleware'; const client = createClient({ baseUrl: String(process.env.NEXT_PUBLIC_FEED_API_BASE_URL), @@ -33,15 +34,17 @@ const throwOnError: Middleware = { client.use(throwOnError); -const generateAuthMiddlewareWithToken = (accessToken: string): Middleware => { - return { - async onRequest(req) { - // add Authorization header to every request - req.headers.set('Authorization', `Bearer ${accessToken}`); - return req; - }, - }; -}; +async function withAuthMiddleware( + authMiddleware: Middleware, + fn: () => Promise, +): Promise { + client.use(authMiddleware); + try { + return await fn(); + } finally { + client.eject(authMiddleware); + } +} export const getFeeds = async (): Promise< | paths['/v1/feeds']['get']['responses'][200]['content']['application/json'] @@ -61,24 +64,21 @@ export const getFeeds = async (): Promise< export const getFeed = async ( feedId: string, accessToken: string, + userContextJwt?: string, ): Promise< | paths['/v1/feeds/{id}']['get']['responses'][200]['content']['application/json'] | undefined > => { - const authMiddleware = generateAuthMiddlewareWithToken(accessToken); - client.use(authMiddleware); - return await client - .GET('/v1/feeds/{id}', { params: { path: { id: feedId } } }) - .then((response) => { - const data = response.data; - return data; - }) - .catch(function (error) { - throw error; - }) - .finally(() => { - client.eject(authMiddleware); + const authMiddleware = generateAuthMiddlewareWithToken( + accessToken, + userContextJwt, + ); + return await withAuthMiddleware(authMiddleware, async () => { + const response = await client.GET('/v1/feeds/{id}', { + params: { path: { id: feedId } }, }); + return response.data; + }); }; export const getGtfsFeeds = async (): Promise< @@ -114,141 +114,119 @@ export const getGtfsRtFeeds = async (): Promise< export const getGtfsFeed = async ( id: string, accessToken: string, + userContextJwt?: string, ): Promise => { - const authMiddleware = generateAuthMiddlewareWithToken(accessToken); - client.use(authMiddleware); - return await client - .GET('/v1/gtfs_feeds/{id}', { params: { path: { id } } }) - .then((response) => { - const data = response.data; - return data; - }) - .catch(function (error) { - throw error; - }) - .finally(() => { - client.eject(authMiddleware); + const authMiddleware = generateAuthMiddlewareWithToken( + accessToken, + userContextJwt, + ); + return await withAuthMiddleware(authMiddleware, async () => { + const response = await client.GET('/v1/gtfs_feeds/{id}', { + params: { path: { id } }, }); + return response.data as AllFeedType; + }); }; export const getGtfsRtFeed = async ( id: string, accessToken: string, + userContextJwt?: string, ): Promise< | paths['/v1/gtfs_rt_feeds/{id}']['get']['responses'][200]['content']['application/json'] | undefined > => { - const authMiddleware = generateAuthMiddlewareWithToken(accessToken); - client.use(authMiddleware); - return await client - .GET('/v1/gtfs_rt_feeds/{id}', { params: { path: { id } } }) - .then((response) => { - const data = response.data; - return data; - }) - .catch(function (error) { - throw error; - }) - .finally(() => { - client.eject(authMiddleware); + const authMiddleware = generateAuthMiddlewareWithToken( + accessToken, + userContextJwt, + ); + return await withAuthMiddleware(authMiddleware, async () => { + const response = await client.GET('/v1/gtfs_rt_feeds/{id}', { + params: { path: { id } }, }); + return response.data; + }); }; export const getGbfsFeed = async ( id: string, accessToken: string, + userContextJwt?: string, ): Promise< | paths['/v1/gbfs_feeds/{id}']['get']['responses'][200]['content']['application/json'] | undefined > => { - const authMiddleware = generateAuthMiddlewareWithToken(accessToken); - client.use(authMiddleware); - return await client - .GET('/v1/gbfs_feeds/{id}', { params: { path: { id } } }) - .then((response) => { - const data = response.data; - return data; - }) - .catch(function (error) { - throw error; - }) - .finally(() => { - client.eject(authMiddleware); + const authMiddleware = generateAuthMiddlewareWithToken( + accessToken, + userContextJwt, + ); + return await withAuthMiddleware(authMiddleware, async () => { + const response = await client.GET('/v1/gbfs_feeds/{id}', { + params: { path: { id } }, }); + return response.data; + }); }; export const getGtfsFeedAssociatedGtfsRtFeeds = async ( id: string, accessToken: string, + userContextJwt?: string, ): Promise< | paths['/v1/gtfs_feeds/{id}/gtfs_rt_feeds']['get']['responses'][200]['content']['application/json'] | undefined > => { - const authMiddleware = generateAuthMiddlewareWithToken(accessToken); - client.use(authMiddleware); - return await client - .GET('/v1/gtfs_feeds/{id}/gtfs_rt_feeds', { + const authMiddleware = generateAuthMiddlewareWithToken( + accessToken, + userContextJwt, + ); + return await withAuthMiddleware(authMiddleware, async () => { + const response = await client.GET('/v1/gtfs_feeds/{id}/gtfs_rt_feeds', { params: { path: { id } }, - }) - .then((response) => { - const data = response.data; - return data; - }) - .catch(function (error) { - throw error; - }) - .finally(() => { - client.eject(authMiddleware); }); + return response.data; + }); }; export const getGtfsFeedDatasets = async ( id: string, accessToken: string, + userContextJwt?: string, queryParams?: paths['/v1/gtfs_feeds/{id}/datasets']['get']['parameters']['query'], ): Promise< | paths['/v1/gtfs_feeds/{id}/datasets']['get']['responses'][200]['content']['application/json'] | undefined > => { - const authMiddleware = generateAuthMiddlewareWithToken(accessToken); - client.use(authMiddleware); - return await client - .GET('/v1/gtfs_feeds/{id}/datasets', { + const authMiddleware = generateAuthMiddlewareWithToken( + accessToken, + userContextJwt, + ); + return await withAuthMiddleware(authMiddleware, async () => { + const response = await client.GET('/v1/gtfs_feeds/{id}/datasets', { params: { query: queryParams, path: { id } }, - }) - .then((response) => { - const data = response.data; - return data; - }) - .catch(function (error) { - throw error; - }) - .finally(() => { - client.eject(authMiddleware); }); + return response.data; + }); }; export const getDatasetGtfs = async ( id: string, accessToken: string, + userContextJwt?: string, ): Promise< | paths['/v1/datasets/gtfs/{id}']['get']['responses'][200]['content']['application/json'] | undefined > => { - const authMiddleware = generateAuthMiddlewareWithToken(accessToken); - client.use(authMiddleware); - return await client - .GET('/v1/datasets/gtfs/{id}', { params: { path: { id } } }) - .then((response) => { - const data = response.data; - return data; - }) - .catch(function (error) { - throw error; - }) - .finally(() => { - client.eject(authMiddleware); + const authMiddleware = generateAuthMiddlewareWithToken( + accessToken, + userContextJwt, + ); + return await withAuthMiddleware(authMiddleware, async () => { + const response = await client.GET('/v1/datasets/gtfs/{id}', { + params: { path: { id } }, }); + return response.data; + }); }; export const getMetadata = async (): Promise< @@ -269,47 +247,39 @@ export const getMetadata = async (): Promise< export const searchFeeds = async ( params: AllFeedsParams, accessToken: string, + userContextJwt?: string, ): Promise< | paths['/v1/search']['get']['responses'][200]['content']['application/json'] | undefined > => { - const authMiddleware = generateAuthMiddlewareWithToken(accessToken); - client.use(authMiddleware); - return await client - .GET('/v1/search', { params }) - .then((response) => { - const data = response.data; - return data; - }) - .catch(function (error) { - throw error; - }) - .finally(() => { - client.eject(authMiddleware); - }); + const authMiddleware = generateAuthMiddlewareWithToken( + accessToken, + userContextJwt, + ); + return await withAuthMiddleware(authMiddleware, async () => { + const response = await client.GET('/v1/search', { params }); + return response.data; + }); }; export const getLicense = async ( id: string, accessToken: string, + userContextJwt?: string, ): Promise< | paths['/v1/licenses/{id}']['get']['responses'][200]['content']['application/json'] | undefined > => { - const authMiddleware = generateAuthMiddlewareWithToken(accessToken); - client.use(authMiddleware); - return await client - .GET('/v1/licenses/{id}', { params: { path: { id } } }) - .then((response) => { - const data = response.data; - return data; - }) - .catch(function (error) { - throw error; - }) - .finally(() => { - client.eject(authMiddleware); + const authMiddleware = generateAuthMiddlewareWithToken( + accessToken, + userContextJwt, + ); + return await withAuthMiddleware(authMiddleware, async () => { + const response = await client.GET('/v1/licenses/{id}', { + params: { path: { id } }, }); + return response.data; + }); }; /** diff --git a/src/app/services/session-service.ts b/src/app/services/session-service.ts new file mode 100644 index 00000000..9707c0b7 --- /dev/null +++ b/src/app/services/session-service.ts @@ -0,0 +1,26 @@ +import { app } from '../../firebase'; + +/** + * After Firebase login on the client, call this to establish + * a server-side session via the /api/session endpoint. + */ +export const setUserCookieSession = async (): Promise => { + console.log('setting id into the cookie--'); + // Ensure this only runs in the browser + if (typeof window === 'undefined') { + return; + } + + const user = app.auth().currentUser; + if (user == null) { + return; + } + + const idToken = await user.getIdToken(); + console.log('setting id into the cookie' + idToken); + await fetch('/api/session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ idToken }), + }); +}; diff --git a/src/app/store/saga/auth-saga.ts b/src/app/store/saga/auth-saga.ts index 8859ce76..922f5d76 100644 --- a/src/app/store/saga/auth-saga.ts +++ b/src/app/store/saga/auth-saga.ts @@ -39,6 +39,7 @@ import { retrieveUserInformation, sendEmailVerification, } from '../../services'; +import { setUserCookieSession } from '../../services/session-service'; import { type AdditionalUserInfo, type UserCredential, @@ -262,6 +263,11 @@ function* anonymousLoginSaga(): Generator { } } +function* sessionCookieAfterLoginSaga(): Generator { + // Establish server-side HTTP-only session cookie after any loginSuccess. + yield call(setUserCookieSession); +} + export function* watchAuth(): Generator { yield takeLatest(USER_PROFILE_LOGIN, emailLoginSaga); yield takeLatest(USER_PROFILE_LOGOUT, logoutSaga); @@ -274,4 +280,6 @@ export function* watchAuth(): Generator { yield takeLatest(USER_PROFILE_CHANGE_PASSWORD, changePasswordSaga); yield takeLatest(USER_PROFILE_RESET_PASSWORD, resetPasswordSaga); yield takeLatest(USER_PROFILE_ANONYMOUS_LOGIN, anonymousLoginSaga); + // When loginSuccess is dispatched (any login flow), set the session cookie. + yield takeLatest(loginSuccess.type, sessionCookieAfterLoginSaga); } diff --git a/src/app/utils/auth-server.ts b/src/app/utils/auth-server.ts index 06dac163..3b39bf33 100644 --- a/src/app/utils/auth-server.ts +++ b/src/app/utils/auth-server.ts @@ -1,14 +1,18 @@ import 'server-only'; +import { cookies } from 'next/headers'; import { getAuth } from 'firebase-admin/auth'; import { getEnvConfig, nonEmpty } from './config'; import { getFirebaseAdminApp } from '../../lib/firebase-admin'; +import { verifySessionToken, type SessionPayload } from './session-jwt'; interface CachedToken { token: string; expiresAt: number; // epoch ms } -let cached: CachedToken | undefined; +// Per-cache-key token cache. The cache key is typically the user uid, +// or a fallback key like 'service' when no user is associated. +const cachedByKey = new Map(); function now(): number { return Date.now(); } @@ -61,9 +65,18 @@ async function exchangeCustomTokenForIdToken( /** * Returns a GCIP ID token suitable for calling an IAP-protected API configured with Identity Platform. - * Caches the token until near expiry to minimize exchanges. + * + * If a user uid is provided, the uid is embedded as a custom claim in the + * underlying Firebase custom token, and the resulting GCIP ID token is cached + * per-user. This ensures that user-specific tokens are not shared across + * different users. + * + * When no uid is provided, a shared "service" token is used and cached + * under a common key. */ -export async function getGcipIdToken(): Promise { +export async function getGcipIdToken( + userInfo: SessionPayload | undefined, +): Promise { // Dev/mock bypass: allow local runs without Firebase Admin/service accounts const isMock = getEnvConfig('NEXT_PUBLIC_API_MOCKING') === 'enabled' || @@ -71,7 +84,10 @@ export async function getGcipIdToken(): Promise { if (isMock) { return 'dev-mock-token'; } + const cacheKey = userInfo?.uid ?? 'service'; + // Use cached token if still valid for at least 60 seconds + const cached = cachedByKey.get(cacheKey); if (cached != undefined && cached.expiresAt - now() > 60_000) { return cached.token; } @@ -82,18 +98,30 @@ export async function getGcipIdToken(): Promise { nonEmpty(getEnvConfig('GCIP_SERVICE_UID')) ?? nonEmpty(getEnvConfig('NEXT_GCIP_SERVICE_UID')) ?? 'iap-service-caller'; - const customToken = await getAuth(adminApp).createCustomToken(serviceUid, { - service: true, - }); + const customClaims: Record = { service: true }; + if (userInfo?.uid != undefined) { + // Attach the end-user session information as metadata so downstream + // services can attribute calls without changing API signatures. + customClaims.userUid = userInfo.uid; + customClaims.email = userInfo.email; + customClaims.sessionIat = userInfo.iat; + customClaims.sessionExp = userInfo.exp; + customClaims.isGuest = userInfo.isGuest === true; + } + const customToken = await getAuth(adminApp).createCustomToken( + serviceUid, + customClaims, + ); const { idToken, expiresInSec } = await exchangeCustomTokenForIdToken(customToken); // Default TTL ~ 55 minutes if expiresIn not present const ttlMs = (expiresInSec ?? 3600) * 1000; const safetyMs = 300_000; // refresh 5 minutes early - cached = { + const entry: CachedToken = { token: idToken, expiresAt: now() + ttlMs - safetyMs, }; + cachedByKey.set(cacheKey, entry); return idToken; } @@ -102,5 +130,92 @@ export async function getGcipIdToken(): Promise { * This avoids trusting client tokens and keeps credentials server-side only. */ export async function getSSRAccessToken(): Promise { - return await getGcipIdToken(); + // If a user session exists, embed the user uid as a custom claim so + // downstream services can attribute the call. Otherwise, fall back to a + // shared service token. + let userInfo: SessionPayload | undefined; + try { + userInfo = await getCurrentUserFromCookie(); + } catch { + console.warn('No cookie found'); + } + console.log(userInfo); + return await getGcipIdToken(userInfo); +} + +/** + * Reads the HTTP-only session cookie set by /api/session, verifies the + * server-signed session JWT, and returns basic user info. + * + * Server-only: do not import this helper from client components. + */ +export async function getCurrentUserFromCookie(): Promise< + SessionPayload | undefined +> { + try { + // In newer Next.js versions, cookies() can be async and must be awaited. + let cookieStore; + try { + cookieStore = await cookies(); + } catch { + cookieStore = undefined; + } + + if (cookieStore == undefined || typeof cookieStore.get !== 'function') { + return undefined; + } + + const token = cookieStore.get('md_session')?.value; + if (token == null) { + return undefined; + } + + const session = verifySessionToken(token); + if (session == null) { + return undefined; + } + + return session; + } catch (error) { + // Swallow errors and treat as unauthenticated; callers already handle + // the undefined case and attach a service-level token instead. + return undefined; + } +} + +/** + * Returns the raw session JWT from the md_session cookie, but only if it + * verifies successfully. This is intended for forwarding to backend services + * via a header (e.g. x-mdb-user-context) so they can decode user identity + * without directly accessing browser cookies. + */ +export async function getUserContextJwtFromCookie(): Promise< + string | undefined +> { + try { + let cookieStore; + try { + cookieStore = await cookies(); + } catch { + cookieStore = undefined; + } + + if (cookieStore == undefined || typeof cookieStore.get !== 'function') { + return undefined; + } + + const token = cookieStore.get('md_session')?.value; + if (token == null) { + return undefined; + } + + const verified = verifySessionToken(token); + if (verified == null) { + return undefined; + } + + return token; + } catch { + return undefined; + } } diff --git a/src/app/utils/session-jwt.ts b/src/app/utils/session-jwt.ts new file mode 100644 index 00000000..5904984e --- /dev/null +++ b/src/app/utils/session-jwt.ts @@ -0,0 +1,150 @@ +import crypto from 'node:crypto'; +import { getEnvConfig, nonEmpty } from './config'; + +/** + * Session JWT payload shape used internally when signing and verifying tokens. + * + * - uid: stable Firebase user identifier + * - email: optional email when available + * - isGuest: true when the user is an anonymous/guest session + * - iat/exp: issued-at and expiry timestamps (seconds since epoch) + */ +export interface SessionPayload { + uid: string; + email?: string; + isGuest?: boolean; + iat: number; + exp: number; +} + +/** + * Encode a Buffer or string to a URL-safe Base64 string (base64url) with padding removed. + * + * This follows the common "base64url" format used by JWTs: + * - '+' => '-' + * - '/' => '_' + * - '=' padding removed + * + * @param input - The input Buffer or string to encode. + * @returns The URL-safe Base64-encoded string without padding. + */ +function base64url(input: Buffer | string): string { + return Buffer.from(input) + .toString('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); +} + +/** + * Default time-to-live (TTL) for session JWTs, in seconds. + * + * Value: 60 * 60 (1 hour) + */ +const DEFAULT_TTL_SECONDS = 60 * 60; // 1 hour + +/** + * Read the JWT secret from the environment. + * + * The secret is required to be present in the SESSION_JWT_SECRET environment variable + * and must be at least 32 characters long to ensure sufficient HMAC key strength. + * + * @returns The raw secret string. + * @throws {Error} If SESSION_JWT_SECRET is not set or is shorter than 32 characters. + * + * @internal + */ +function getSecret(): string { + const secret = nonEmpty(getEnvConfig('NEXT_SESSION_JWT_SECRET')); + if (secret == null || secret.length < 32) { + throw new Error( + 'SESSION_JWT_SECRET must be set and at least 32 characters long', + ); + } + return secret; +} + +/** + * Create and sign a session JWT for a given user ID (uid). + * + * The token uses HS256 (HMAC-SHA256) with a base64url-encoded header and payload: + * - Header: { alg: "HS256", typ: "JWT" } + * - Payload: { uid, email?, isGuest?, iat, exp } + * + * The issued-at time (iat) and expiration time (exp) are expressed as UNIX timestamps + * (seconds since epoch). If ttlSeconds is omitted, DEFAULT_TTL_SECONDS (1 hour) is used. + * + * @param uid - The unique identifier for the user (required). + * @param email - Optional email address to include in the token payload. + * @param options - Optional settings: TTL (seconds) and guest flag. + * @returns A compact JWT string in the format: "..". + * @throws {Error} If the signing secret is missing or invalid. + */ +export function signSessionToken( + uid: string, + email?: string, + options?: { ttlSeconds?: number; isGuest?: boolean }, +): string { + const now = Math.floor(Date.now() / 1000); + const exp = now + (options?.ttlSeconds ?? DEFAULT_TTL_SECONDS); + + const header = { alg: 'HS256', typ: 'JWT' }; + const payload: SessionPayload = { + uid, + email, + isGuest: options?.isGuest, + iat: now, + exp, + }; + + const encodedHeader = base64url(JSON.stringify(header)); + const encodedPayload = base64url(JSON.stringify(payload)); + const data = `${encodedHeader}.${encodedPayload}`; + + const secret = getSecret(); + const signature = crypto.createHmac('sha256', secret).update(data).digest(); + + return `${data}.${base64url(signature)}`; +} + +/** + * Verify a session JWT previously created by signSessionToken. + * + * Verification steps: + * 1. Ensure the token has three dot-separated parts. + * 2. Recompute the HMAC-SHA256 signature using the same secret and compare. + * 3. Decode and parse the payload, validating required fields (uid and exp). + * 4. Ensure the token has not expired (payload.exp > current time). + * + * On success, returns the minimal payload { uid, email? }. On any failure (malformed token, + * signature mismatch, missing/invalid claims, expired token, or runtime error), returns undefined. + * + * @param token - The compact JWT string to verify. + * @returns The session identity { uid, email? } if verification succeeds, otherwise undefined. + */ +export function verifySessionToken(token: string): SessionPayload | undefined { + try { + const parts = token.split('.'); + if (parts.length !== 3) return undefined; + const [encodedHeader, encodedPayload, signature] = parts; + const data = `${encodedHeader}.${encodedPayload}`; + + const secret = getSecret(); + const expectedSig = base64url( + crypto.createHmac('sha256', secret).update(data).digest(), + ); + if (signature !== expectedSig) return undefined; + + const json = Buffer.from(encodedPayload, 'base64').toString('utf8'); + const payload = JSON.parse(json) as Partial; + if (typeof payload.uid !== 'string') return undefined; + if (typeof payload.exp !== 'number') return undefined; + + const now = Math.floor(Date.now() / 1000); + if (payload.exp <= now) return undefined; + + return payload as SessionPayload; + } catch { + return undefined; + } +} From b4c439547dd2f4bfbaa03019ed91d1d139310ca2 Mon Sep 17 00:00:00 2001 From: David Gamez Diaz <1192523+davidgamez@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:33:48 -0500 Subject: [PATCH 06/11] delete cookie after logout --- src/app/api/session/route.ts | 8 ++++++++ src/app/context/api-auth-middleware.ts | 1 - src/app/services/session-service.ts | 15 +++++++++++++-- src/app/store/saga/auth-saga.ts | 8 +++++++- src/app/utils/auth-server.ts | 1 - 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/app/api/session/route.ts b/src/app/api/session/route.ts index 4feded04..c7cc109b 100644 --- a/src/app/api/session/route.ts +++ b/src/app/api/session/route.ts @@ -87,3 +87,11 @@ export async function GET(req: NextRequest): Promise { return NextResponse.json({ authenticated: false }, { status: 200 }); } } + +export async function DELETE(req: NextRequest): Promise { + // Clear the session cookie so that subsequent requests have no session. + const response = NextResponse.json({ status: 'logged_out' }); + // Use the built-in delete helper to ensure the cookie is removed. + response.cookies.delete(COOKIE_NAME); + return response; +} diff --git a/src/app/context/api-auth-middleware.ts b/src/app/context/api-auth-middleware.ts index ab350b4f..d83ba77b 100644 --- a/src/app/context/api-auth-middleware.ts +++ b/src/app/context/api-auth-middleware.ts @@ -33,7 +33,6 @@ export const generateAuthMiddlewareWithToken = ( // compact user-context JWT so the backend can attribute calls to // an end-user without relying on IAP to forward custom claims. if (userContextJwt != null && userContextJwt !== '') { - console.log('user contet ' + userContextJwt); req.headers.set(USER_CONTEXT_HEADER, userContextJwt); } diff --git a/src/app/services/session-service.ts b/src/app/services/session-service.ts index 9707c0b7..5f8298a9 100644 --- a/src/app/services/session-service.ts +++ b/src/app/services/session-service.ts @@ -5,7 +5,6 @@ import { app } from '../../firebase'; * a server-side session via the /api/session endpoint. */ export const setUserCookieSession = async (): Promise => { - console.log('setting id into the cookie--'); // Ensure this only runs in the browser if (typeof window === 'undefined') { return; @@ -17,10 +16,22 @@ export const setUserCookieSession = async (): Promise => { } const idToken = await user.getIdToken(); - console.log('setting id into the cookie' + idToken); await fetch('/api/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ idToken }), }); }; + +/** + * Clear the server-side session cookie on logout. + */ +export const clearUserCookieSession = async (): Promise => { + if (typeof window === 'undefined') { + return; + } + + await fetch('/api/session', { + method: 'DELETE', + }); +}; diff --git a/src/app/store/saga/auth-saga.ts b/src/app/store/saga/auth-saga.ts index 922f5d76..deb9ba13 100644 --- a/src/app/store/saga/auth-saga.ts +++ b/src/app/store/saga/auth-saga.ts @@ -39,7 +39,10 @@ import { retrieveUserInformation, sendEmailVerification, } from '../../services'; -import { setUserCookieSession } from '../../services/session-service'; +import { + setUserCookieSession, + clearUserCookieSession, +} from '../../services/session-service'; import { type AdditionalUserInfo, type UserCredential, @@ -91,6 +94,9 @@ function* logoutSaga({ try { navigateTo(redirectScreen); yield app.auth().signOut(); + // Clear the HTTP-only md_session cookie on logout so that + // server-side requests immediately see the user as logged out. + yield call(clearUserCookieSession); yield put(logoutSuccess()); if (propagate) { broadcastMessage(LOGOUT_CHANNEL); diff --git a/src/app/utils/auth-server.ts b/src/app/utils/auth-server.ts index 3b39bf33..1ec4b051 100644 --- a/src/app/utils/auth-server.ts +++ b/src/app/utils/auth-server.ts @@ -139,7 +139,6 @@ export async function getSSRAccessToken(): Promise { } catch { console.warn('No cookie found'); } - console.log(userInfo); return await getGcipIdToken(userInfo); } From 73f72f2a45d1af3cf6b1f1741c8d9021b4b5f85f Mon Sep 17 00:00:00 2001 From: David Gamez Diaz <1192523+davidgamez@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:36:32 -0500 Subject: [PATCH 07/11] update documentation --- docs/Authentication.md | 52 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/docs/Authentication.md b/docs/Authentication.md index 7eed59e9..2fcbc5be 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -4,7 +4,8 @@ This document explains how server-side authentication works in the Mobility Data ## Overview - Server components/actions call the Mobility Feed API using a server‑minted GCIP ID token. -- The client never forwards its token to the server; all API calls use server credentials only. +- The client never forwards its Firebase ID token to the server; all API calls use server credentials only. +- A short‑lived HTTP‑only session cookie (`md_session`) stores a server‑signed JWT with the current user's identity (including guest/anonymous flag). - Firebase Admin is initialized once via a centralized helper and used for both Remote Config and token minting. - Mock mode (MSW) enables local development without hitting the real API or Firebase. @@ -16,8 +17,31 @@ This document explains how server-side authentication works in the Mobility Data Key code paths: - `src/lib/firebase-admin.ts`: centralized Admin initialization (`getFirebaseAdminApp()`), backed by `ensureAdminInitialized()`. -- `src/app/utils/auth-server.ts`: token functions `getGcipIdToken()` and `getSSRAccessToken()` (the canonical token provider for SSR calls). -- `src/app/services/feeds/index.ts`: OpenAPI client; injects `Authorization` header using the token returned by `getSSRAccessToken()`. +- `src/app/utils/auth-server.ts`: token functions `getGcipIdToken()` and `getSSRAccessToken()` (the canonical token provider for SSR calls) and helpers for reading the session cookie. +- `src/app/services/feeds/index.ts`: OpenAPI client; injects `Authorization` and user‑context headers using the token returned by `getSSRAccessToken()`. + +## Session Cookie & SSR User Identity + +To let server components know "who" the current user is (including guests) without ever trusting client tokens directly, the web app uses a short‑lived, server‑signed session JWT stored in the `md_session` HTTP‑only cookie. + +Flow: +1. The user signs in on the client with Firebase Auth (email/password, provider, or anonymous). +2. After any successful login, a Redux saga calls `setUserCookieSession()` from [src/app/services/session-service.ts](src/app/services/session-service.ts). +3. `setUserCookieSession()` reads the current Firebase ID token from the client SDK and POSTs it to `/api/session`. +4. The `/api/session` `POST` handler in [src/app/api/session/route.ts](src/app/api/session/route.ts): + - Verifies the ID token with Firebase Admin. + - Derives an `isGuest` flag from the sign‑in provider (`anonymous` → guest). + - Issues a short‑lived session JWT (1 hour) signed with `NEXT_SESSION_JWT_SECRET` containing: + - `uid`, optional `email`, `isGuest`, `iat`, and `exp`. + - Sets the `md_session` cookie (HTTP‑only, `sameSite=lax`, `secure` in production). + +On the server side: +- [src/app/utils/session-jwt.ts](src/app/utils/session-jwt.ts) defines the `SessionPayload` type and helpers to sign/verify the JWT used in `md_session`. +- [src/app/utils/auth-server.ts](src/app/utils/auth-server.ts) exposes: + - `getCurrentUserFromCookie()` to decode the session cookie into `SessionPayload` for SSR. + - `getUserContextJwtFromCookie()` to obtain the raw, verified session JWT for forwarding to the backend. + +The GCIP ID token used for IAP remains a server‑minted token that does not depend on the client token; the session cookie is only used for identifying the current end‑user (including guests) and for per‑user attribution. ## Firebase Admin Initialization Centralized in `getFirebaseAdminApp()`: @@ -33,6 +57,7 @@ Server‑side credentials and config (server‑only): - `GOOGLE_SA_JSON`: Inline service account JSON string. - `GOOGLE_SA_JSON_PATH`: Absolute/relative path to service account JSON file. - `NEXT_PUBLIC_FIREBASE_PROJECT_ID`: Project ID; used to match Admin apps and as fallback when JSON lacks `project_id`. +- `NEXT_SESSION_JWT_SECRET`: Secret used to sign and verify the `md_session` session JWT on the web side. GCIP / Identity Toolkit: - `GCIP_API_KEY` (or `FIREBASE_API_KEY` or `NEXT_PUBLIC_FIREBASE_API_KEY`): API key for `accounts:signInWithCustomToken`. @@ -43,6 +68,8 @@ Mock/dev: - `NEXT_PUBLIC_API_MOCKING=enabled`: Enables MSW mock service worker in the browser. - `LOCAL_DEV_NO_ADMIN=1` (optional): Bypass Admin initialization for Remote Config/token code paths during local experimentation. +> Note: On the Mobility Feed API (Python) side, a matching secret (e.g. `S2S_JWT_SECRET`) is used to validate the user‑context JWT forwarded from the web app. + ## Remote Config - Server‑side code fetches Firebase Remote Config via Admin SDK. - In mock mode, Remote Config returns defaults to avoid Admin calls. @@ -68,10 +95,27 @@ Setup: - Do **not** forward client tokens to the server. - Keep all credentials server‑only and never expose service account JSON to client code. +### End‑User Context Propagation to the Mobility Feed API + +In addition to the GCIP ID token for IAP, SSR API calls also propagate a compact, server‑signed user‑context JWT so the backend can attribute requests to an end‑user without trusting any client tokens: + +- The `md_session` cookie's JWT is reused as this user‑context token. +- On the server, [src/app/utils/auth-server.ts](src/app/utils/auth-server.ts) reads the cookie via `getUserContextJwtFromCookie()`. +- [src/app/context/api-auth-middleware.ts](src/app/context/api-auth-middleware.ts) provides `generateAuthMiddlewareWithToken(accessToken, userContextJwt?)`, which: + - Sets `Authorization: Bearer ` for IAP. + - When `userContextJwt` is present, also sets `x-mdb-user-context: `. +- All server‑side feeds service functions in [src/app/services/feeds/index.ts](src/app/services/feeds/index.ts) accept an optional `userContextJwt` and pass it into this middleware. + +On the Mobility Feed API side (see [mobility-feed-api/api/src/middleware/request_context.py](mobility-feed-api/api/src/middleware/request_context.py)): +- The `RequestContext` middleware reads `x-mdb-user-context`. +- It verifies the HS256 signature using a shared secret and decodes the payload. +- It populates `user_id`, `user_email`, and an `is_guest` flag for auditing and per‑user behavior. + ## Security Considerations - **No client token pass‑through**: Prevents elevation of privilege and token replay. - **Server‑only credentials**: Service account material must never be sent to the client. -- **Optional auditing**: You may forward end‑user identity headers (e.g., `X-User-Subject`, `X-User-Email`) for backend audit trails if required. +- **End‑user attribution without client tokens**: The backend receives only a server‑signed, minimal user‑context JWT via `x-mdb-user-context`, never the raw client Firebase ID token. +- **Guest users**: Anonymous sign‑ins are explicitly flagged via `isGuest` in the session JWT so both the web app and backend can distinguish guest from authenticated accounts. ## Troubleshooting Common issues and fixes: From c520790eaecc7f603f5057dd733c0f874b77da8d Mon Sep 17 00:00:00 2001 From: David Gamez Diaz <1192523+davidgamez@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:37:29 -0500 Subject: [PATCH 08/11] update gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d9568296..9d2dfa1c 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,4 @@ cypress/videos next-env.d.ts .vercel -.vscode/settings.json +.vscode/ From 19fe007d451df36d3423a73c9780dfcccf588c15 Mon Sep 17 00:00:00 2001 From: David Gamez Diaz <1192523+davidgamez@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:57:26 -0500 Subject: [PATCH 09/11] fix get datasets call --- src/app/feeds/[feedDataType]/[feedId]/page.tsx | 2 +- src/app/services/feeds/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/feeds/[feedDataType]/[feedId]/page.tsx b/src/app/feeds/[feedDataType]/[feedId]/page.tsx index 715b780f..146e37c8 100644 --- a/src/app/feeds/[feedDataType]/[feedId]/page.tsx +++ b/src/app/feeds/[feedDataType]/[feedId]/page.tsx @@ -60,10 +60,10 @@ const fetchInitialDatasets = cache( const datasets = await getGtfsFeedDatasets( feedId, accessToken, - userContextJwt, { limit: 10, }, + userContextJwt, ); return datasets; } catch (e) { diff --git a/src/app/services/feeds/index.ts b/src/app/services/feeds/index.ts index baa5d4dd..dd393853 100644 --- a/src/app/services/feeds/index.ts +++ b/src/app/services/feeds/index.ts @@ -191,8 +191,8 @@ export const getGtfsFeedAssociatedGtfsRtFeeds = async ( export const getGtfsFeedDatasets = async ( id: string, accessToken: string, - userContextJwt?: string, queryParams?: paths['/v1/gtfs_feeds/{id}/datasets']['get']['parameters']['query'], + userContextJwt?: string, ): Promise< | paths['/v1/gtfs_feeds/{id}/datasets']['get']['responses'][200]['content']['application/json'] | undefined From 7540c5ddfe5e12451fc0fc27b05ab125467dd115 Mon Sep 17 00:00:00 2001 From: David Gamez Diaz <1192523+davidgamez@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:59:44 -0500 Subject: [PATCH 10/11] remove iap testing script --- scripts/iap-test/.gitignore | 2 - scripts/iap-test/README.md | 92 --- scripts/iap-test/fetch-gcip.js | 105 --- scripts/iap-test/fetch-iap.js | 110 ---- scripts/iap-test/package.json | 19 - scripts/iap-test/yarn.lock | 1119 -------------------------------- 6 files changed, 1447 deletions(-) delete mode 100644 scripts/iap-test/.gitignore delete mode 100644 scripts/iap-test/README.md delete mode 100644 scripts/iap-test/fetch-gcip.js delete mode 100644 scripts/iap-test/fetch-iap.js delete mode 100644 scripts/iap-test/package.json delete mode 100644 scripts/iap-test/yarn.lock diff --git a/scripts/iap-test/.gitignore b/scripts/iap-test/.gitignore deleted file mode 100644 index f510bc35..00000000 --- a/scripts/iap-test/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.env -sa-key.json \ No newline at end of file diff --git a/scripts/iap-test/README.md b/scripts/iap-test/README.md deleted file mode 100644 index 68851578..00000000 --- a/scripts/iap-test/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# IAP Test Scripts (IAP + GCIP) - -This folder contains two minimal scripts to test calling an IAP-protected API: - -- `fetch-iap.js`: Uses a Google OIDC ID token (audience = IAP OAuth Client ID). Works when IAP uses Google accounts. -- `fetch-gcip.js`: Uses an Identity Platform (GCIP) ID token obtained via custom token flow. Use this when IAP is configured with GCIP. - -## Environment -Create `.env` based on `.env.sample`: - -```env -# Common -API_URL=https:/// -GOOGLE_SA_JSON_PATH=./sa-key.json - -# IAP (Google accounts) -IAP_AUDIENCE= - -# GCIP (Identity Platform) -GCIP_PROJECT_ID= -GCIP_API_KEY= -# optional if using multi-tenant -GCIP_TENANT_ID= -# optional synthetic UID for the service caller -GCIP_SERVICE_UID=iap-service-caller -``` - -## Install & Run -```bash -# Install local-only deps -npm i google-auth-library firebase-admin node-fetch dotenv - -# IAP (Google accounts) flow -node fetch-iap.js - -# GCIP (Identity Platform) flow -node fetch-gcip.js -``` - -### Using Yarn -```bash -# Install -yarn add google-auth-library firebase-admin node-fetch dotenv - -# IAP flow -yarn run test:iap - -# GCIP flow (default) -yarn test -``` - -## Notes -- `GCIP_API_KEY` is the Web API key for Identity Platform (Firebase/GCIP). Find it in Google Cloud Console → APIs & Services → Credentials. -- For GCIP multi-tenant, set `GCIP_TENANT_ID` and ensure the IAP resource is linked to the same tenant. -- The Service Account must have access to mint custom tokens and (optionally) act as a service principal.# IAP ID Token Test Script - -Minimal Node.js script to fetch a Google-signed OIDC ID token for an IAP-protected HTTPS resource and perform a request to your API. - -## Prerequisites -- Node.js 18+ installed -- Service Account with role `roles/iap.httpsResourceAccessor` -- IAP OAuth 2.0 Client ID (the audience) - -## Setup -1. Create a service account key locally (for testing only; rotate/secure it): -```bash -PROJECT_ID= -gcloud iam service-accounts keys create sa-key.json \ - --iam-account=vercel-iap-caller@${PROJECT_ID}.iam.gserviceaccount.com -``` - -2. Copy `.env.sample` to `.env` and fill values: -```bash -cp .env.sample .env -``` - -3. Install dependencies and run: -```bash -npm ci -npm run test -``` - -## Environment Variables -- `IAP_AUDIENCE`: The IAP OAuth client ID (…apps.googleusercontent.com) -- `API_URL`: Full URL to a reachable endpoint (e.g., https:///health) -- `GOOGLE_SA_JSON_PATH` (preferred): Path to SA key JSON file (e.g., `./sa-key.json`). -- `GOOGLE_SA_JSON` (optional): Inline JSON string of SA key (used if `GOOGLE_SA_JSON_PATH` not provided). -- `LOG_JWT_CLAIMS` (optional): `true|false` print decoded JWT claims for verification. - -## Notes -- For production (Vercel), store secrets in environment variables and avoid files. -- The audience must exactly match the IAP OAuth client ID configured on the protected resource. \ No newline at end of file diff --git a/scripts/iap-test/fetch-gcip.js b/scripts/iap-test/fetch-gcip.js deleted file mode 100644 index 21e2e850..00000000 --- a/scripts/iap-test/fetch-gcip.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * GCIP (Identity Platform) token flow for IAP when IAP is configured with GCIP. - * 1) Use Firebase Admin to mint a custom token for a synthetic service user. - * 2) Exchange the custom token for a GCIP ID token via Identity Toolkit API. - * 3) Call the IAP-protected API with Authorization: Bearer . - */ - -const fs = require('fs'); -const path = require('path'); -const fetch = require('node-fetch'); -const admin = require('firebase-admin'); -require('dotenv').config(); - -function ensureEnv(name) { - const v = process.env[name]; - if (!v) { - console.error(`Missing ${name}`); - process.exit(2); - } - return v; -} - -async function main() { - const apiUrl = ensureEnv('API_URL'); - const saJsonPath = process.env.GOOGLE_SA_JSON_PATH; - const saJsonInline = process.env.GOOGLE_SA_JSON; - const projectId = ensureEnv('GCIP_PROJECT_ID'); - const apiKey = ensureEnv('GCIP_API_KEY'); - const tenantId = process.env.GCIP_TENANT_ID || undefined; // optional if default tenant - const serviceUid = process.env.GCIP_SERVICE_UID || 'iap-service-caller'; - - let credentials; - if (saJsonPath && fs.existsSync(path.resolve(saJsonPath))) { - credentials = JSON.parse(fs.readFileSync(path.resolve(saJsonPath), 'utf8')); - } else if (saJsonInline) { - credentials = JSON.parse(saJsonInline); - } else { - console.error('Provide GOOGLE_SA_JSON_PATH or GOOGLE_SA_JSON'); - process.exit(2); - } - - if (!credentials.client_email || !credentials.private_key) { - console.error('Service Account JSON must include client_email and private_key'); - process.exit(2); - } - - if (!admin.apps.length) { - admin.initializeApp({ - credential: admin.credential.cert(credentials), - projectId, - }); - } - - // Mint a custom token for a synthetic service user UID. - const customToken = await admin.auth().createCustomToken(serviceUid, { - service: true, - }); - - // Exchange the custom token for an ID token. - const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`; - const body = { - token: customToken, - returnSecureToken: true, - }; - if (tenantId) body.tenantId = tenantId; - - const resp = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - if (!resp.ok) { - const errText = await resp.text(); - console.error('Failed to exchange custom token:', resp.status, errText); - process.exit(1); - } - const tokens = await resp.json(); - const idToken = tokens.idToken; - if (!idToken) { - console.error('No idToken in response'); - process.exit(1); - } - - // Call the IAP-protected API with GCIP ID token. - console.log(`Calling ${apiUrl} with GCIP ID token (tenant: ${tenantId || 'default'})...`); - const apiResp = await fetch(apiUrl, { - headers: { Authorization: `Bearer ${idToken}` }, - }); - console.log('Status:', apiResp.status); - const data = await apiResp.text(); - if (apiResp.status >= 400) { - console.error('Error body:', data); - process.exit(1); - } - try { - console.log(JSON.stringify(JSON.parse(data), null, 2)); - } catch { - console.log(data); - } -} - -main().catch((e) => { - console.error('Unhandled error:', e); - process.exit(1); -}); diff --git a/scripts/iap-test/fetch-iap.js b/scripts/iap-test/fetch-iap.js deleted file mode 100644 index d379c41c..00000000 --- a/scripts/iap-test/fetch-iap.js +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Minimal script to acquire an IAP OIDC ID token and call the API. - * Reads configuration from environment variables (see .env.sample). - */ - -const fs = require('fs'); -const path = require('path'); -const { GoogleAuth } = require('google-auth-library'); -require('dotenv').config(); - -function base64UrlDecode(input) { - // Pad input to multiple of 4 and replace URL-safe chars - input = input.replace(/-/g, '+').replace(/_/g, '/'); - const pad = input.length % 4; - if (pad) input += '='.repeat(4 - pad); - return Buffer.from(input, 'base64').toString('utf8'); -} - -function decodeJwt(jwt) { - try { - const [header, payload] = jwt.split('.'); - const h = JSON.parse(base64UrlDecode(header)); - const p = JSON.parse(base64UrlDecode(payload)); - return { header: h, payload: p }; - } catch (e) { - return null; - } -} - -async function main() { - const audience = process.env.IAP_AUDIENCE; - const apiUrl = process.env.API_URL; - const saJsonPath = process.env.GOOGLE_SA_JSON_PATH; - const saJsonInline = process.env.GOOGLE_SA_JSON; - const logClaims = String(process.env.LOG_JWT_CLAIMS || 'false').toLowerCase() === 'true'; - - if (!audience) { - console.error('Missing IAP_AUDIENCE'); - process.exit(2); - } - if (!apiUrl) { - console.error('Missing API_URL'); - process.exit(2); - } - - let credentials; - if (saJsonPath && fs.existsSync(path.resolve(saJsonPath))) { - credentials = JSON.parse(fs.readFileSync(path.resolve(saJsonPath), 'utf8')); - } else if (saJsonInline) { - credentials = JSON.parse(saJsonInline); - } else { - console.error('Provide GOOGLE_SA_JSON_PATH or GOOGLE_SA_JSON'); - process.exit(2); - } - - const auth = new GoogleAuth({ credentials }); - const client = await auth.getIdTokenClient(audience); - - // Intercept to print the ID token (and optionally decode claims) - const origRequest = client.request.bind(client); - client.request = async (opts) => { - const res = await origRequest(opts); - return res; - }; - - // The getRequestHeaders() includes Authorization header; we can decode claims for sanity - const headers = await client.getRequestHeaders(); - const token = (headers.Authorization || headers.authorization || '').replace(/^Bearer\s+/i, ''); - if (!token) { - console.error('Failed to obtain ID token'); - process.exit(1); - } - if (logClaims) { - const decoded = decodeJwt(token); - if (decoded) { - console.log('JWT header:', JSON.stringify(decoded.header, null, 2)); - console.log('JWT payload:', JSON.stringify(decoded.payload, null, 2)); - if (decoded.payload && decoded.payload.iss) { - console.log('JWT issuer:', decoded.payload.iss); - } - if (decoded.payload && decoded.payload.aud !== audience) { - console.warn('Warning: token aud does not match IAP_AUDIENCE'); - } - } - } - - console.log(`Calling ${apiUrl} with IAP audience ${audience}...`); - try { - const res = await client.request({ url: apiUrl }); - console.log('Status:', res.status); - if (res.status >= 400) { - console.error('Error body:', res.data); - process.exit(1); - } - console.log(typeof res.data === 'string' ? res.data : JSON.stringify(res.data)); - } catch (err) { - if (err.response) { - console.error('Request failed:', err.response.status, err.response.data); - const body = typeof err.response.data === 'string' ? err.response.data : JSON.stringify(err.response.data); - if (err.response.status === 401 && /Invalid GCIP ID token/i.test(body)) { - console.error('Hint: IAP is configured with Identity Platform (GCIP). Use fetch-gcip.js instead of fetch-iap.js.'); - } - } else { - console.error('Error:', err.message); - } - process.exit(1); - } -} - -main(); diff --git a/scripts/iap-test/package.json b/scripts/iap-test/package.json deleted file mode 100644 index 362d13bd..00000000 --- a/scripts/iap-test/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "iap-test", - "private": true, - "version": "0.1.0", - "description": "IAP + GCIP token test scripts for IAP-protected API calls", - "license": "UNLICENSED", - "type": "commonjs", - "scripts": { - "test:iap": "node fetch-iap.js", - "test:gcip": "node fetch-gcip.js", - "test": "node fetch-gcip.js" - }, - "dependencies": { - "dotenv": "^16.4.5", - "firebase-admin": "^12.3.0", - "google-auth-library": "^9.14.2", - "node-fetch": "^2.6.9" - } -} \ No newline at end of file diff --git a/scripts/iap-test/yarn.lock b/scripts/iap-test/yarn.lock deleted file mode 100644 index b1b9c1b4..00000000 --- a/scripts/iap-test/yarn.lock +++ /dev/null @@ -1,1119 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@fastify/busboy@^3.0.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-3.2.0.tgz#13ed8212f3b9ba697611529d15347f8528058cea" - integrity sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA== - -"@firebase/app-check-interop-types@0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz#455b6562c7a3de3ef75ea51f72dfec5829ad6997" - integrity sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ== - -"@firebase/app-types@0.9.2": - version "0.9.2" - resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.2.tgz#8cbcceba784753a7c0066a4809bc22f93adee080" - integrity sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ== - -"@firebase/auth-interop-types@0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz#927f1f2139a680b55fef0bddbff2c982b08587e8" - integrity sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ== - -"@firebase/component@0.6.9": - version "0.6.9" - resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.9.tgz#4248cfeab222245ada0d7f78ece95a87574532b4" - integrity sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q== - dependencies: - "@firebase/util" "1.10.0" - tslib "^2.1.0" - -"@firebase/database-compat@1.0.8": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-1.0.8.tgz#69ab03d00e27a89f65486896ea219094aa38c27f" - integrity sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg== - dependencies: - "@firebase/component" "0.6.9" - "@firebase/database" "1.0.8" - "@firebase/database-types" "1.0.5" - "@firebase/logger" "0.4.2" - "@firebase/util" "1.10.0" - tslib "^2.1.0" - -"@firebase/database-types@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.5.tgz#2d923f42e3d9911b9eec537ed8b5ecaa0ce95c37" - integrity sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ== - dependencies: - "@firebase/app-types" "0.9.2" - "@firebase/util" "1.10.0" - -"@firebase/database@1.0.8": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.8.tgz#01bb0d0cb5653ae6a6641523f6f085b4c1be9c2f" - integrity sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg== - dependencies: - "@firebase/app-check-interop-types" "0.3.2" - "@firebase/auth-interop-types" "0.2.3" - "@firebase/component" "0.6.9" - "@firebase/logger" "0.4.2" - "@firebase/util" "1.10.0" - faye-websocket "0.11.4" - tslib "^2.1.0" - -"@firebase/logger@0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.2.tgz#74dfcfeedee810deb8a7080d5b7eba56aa16ffa2" - integrity sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A== - dependencies: - tslib "^2.1.0" - -"@firebase/util@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.10.0.tgz#9ec8ab54da82bfc31baff0c43cb281998cbeddab" - integrity sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ== - dependencies: - tslib "^2.1.0" - -"@google-cloud/firestore@^7.7.0": - version "7.11.6" - resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-7.11.6.tgz#0a2b26e215aa4f903267f82370450753b84db16a" - integrity sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw== - dependencies: - "@opentelemetry/api" "^1.3.0" - fast-deep-equal "^3.1.1" - functional-red-black-tree "^1.0.1" - google-gax "^4.3.3" - protobufjs "^7.2.6" - -"@google-cloud/paginator@^5.0.0": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-5.0.2.tgz#86ad773266ce9f3b82955a8f75e22cd012ccc889" - integrity sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg== - dependencies: - arrify "^2.0.0" - extend "^3.0.2" - -"@google-cloud/projectify@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-4.0.0.tgz#d600e0433daf51b88c1fa95ac7f02e38e80a07be" - integrity sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA== - -"@google-cloud/promisify@<4.1.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-4.0.0.tgz#a906e533ebdd0f754dca2509933334ce58b8c8b1" - integrity sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g== - -"@google-cloud/storage@^7.7.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-7.18.0.tgz#1b0e7415633e97b8ce0364c3e3aac033a52cb318" - integrity sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ== - dependencies: - "@google-cloud/paginator" "^5.0.0" - "@google-cloud/projectify" "^4.0.0" - "@google-cloud/promisify" "<4.1.0" - abort-controller "^3.0.0" - async-retry "^1.3.3" - duplexify "^4.1.3" - fast-xml-parser "^4.4.1" - gaxios "^6.0.2" - google-auth-library "^9.6.3" - html-entities "^2.5.2" - mime "^3.0.0" - p-limit "^3.0.1" - retry-request "^7.0.0" - teeny-request "^9.0.0" - uuid "^8.0.0" - -"@grpc/grpc-js@^1.10.9": - version "1.14.3" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.3.tgz#4c9b817a900ae4020ddc28515ae4b52c78cfb8da" - integrity sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA== - dependencies: - "@grpc/proto-loader" "^0.8.0" - "@js-sdsl/ordered-map" "^4.4.2" - -"@grpc/proto-loader@^0.7.13": - version "0.7.15" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz#4cdfbf35a35461fc843abe8b9e2c0770b5095e60" - integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ== - dependencies: - lodash.camelcase "^4.3.0" - long "^5.0.0" - protobufjs "^7.2.5" - yargs "^17.7.2" - -"@grpc/proto-loader@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz#b6c324dd909c458a0e4aa9bfd3d69cf78a4b9bd8" - integrity sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ== - dependencies: - lodash.camelcase "^4.3.0" - long "^5.0.0" - protobufjs "^7.5.3" - yargs "^17.7.2" - -"@js-sdsl/ordered-map@^4.4.2": - version "4.4.2" - resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" - integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== - -"@opentelemetry/api@^1.3.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" - integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== - -"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" - integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== - -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== - -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" - integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== - -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" - integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== - dependencies: - "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" - -"@protobufjs/float@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" - integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== - -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== - -"@protobufjs/path@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" - integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== - -"@protobufjs/pool@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" - integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== - -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== - -"@tootallnate/once@2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" - integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== - -"@types/caseless@*": - version "0.12.5" - resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.5.tgz#db9468cb1b1b5a925b8f34822f1669df0c5472f5" - integrity sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg== - -"@types/jsonwebtoken@^9.0.4": - version "9.0.10" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz#a7932a47177dcd4283b6146f3bd5c26d82647f09" - integrity sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA== - dependencies: - "@types/ms" "*" - "@types/node" "*" - -"@types/long@^4.0.0": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" - integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== - -"@types/ms@*": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" - integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== - -"@types/node@*", "@types/node@>=13.7.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.1.0.tgz#95cc584f1f478301efc86de4f1867e5875e83571" - integrity sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA== - dependencies: - undici-types "~7.16.0" - -"@types/node@^22.0.1": - version "22.19.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.7.tgz#434094ee1731ae76c16083008590a5835a8c39c1" - integrity sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw== - dependencies: - undici-types "~6.21.0" - -"@types/request@^2.48.8": - version "2.48.13" - resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.13.tgz#abdf4256524e801ea8fdda54320f083edb5a6b80" - integrity sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg== - dependencies: - "@types/caseless" "*" - "@types/node" "*" - "@types/tough-cookie" "*" - form-data "^2.5.5" - -"@types/tough-cookie@*": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" - integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== - -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -agent-base@^7.1.2: - version "7.1.4" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" - integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -arrify@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" - integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== - -async-retry@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" - integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== - dependencies: - retry "0.13.1" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -base64-js@^1.3.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -bignumber.js@^9.0.0: - version "9.3.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" - integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== - -buffer-equal-constant-time@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== - -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -debug@4, debug@^4.3.4: - version "4.4.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" - integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== - dependencies: - ms "^2.1.3" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -dotenv@^16.4.5: - version "16.6.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" - integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== - -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -duplexify@^4.0.0, duplexify@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" - integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== - dependencies: - end-of-stream "^1.4.1" - inherits "^2.0.3" - readable-stream "^3.1.1" - stream-shift "^1.0.2" - -ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -end-of-stream@^1.4.1: - version "1.4.5" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" - integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== - dependencies: - once "^1.4.0" - -es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -es-set-tostringtag@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" - integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== - dependencies: - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - -escalade@^3.1.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -extend@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -farmhash-modern@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/farmhash-modern/-/farmhash-modern-1.1.0.tgz#c36b34ad196290d57b0b482dc89e637d0b59835f" - integrity sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA== - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-xml-parser@^4.4.1: - version "4.5.3" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz#c54d6b35aa0f23dc1ea60b6c884340c006dc6efb" - integrity sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig== - dependencies: - strnum "^1.1.1" - -faye-websocket@0.11.4: - version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" - integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== - dependencies: - websocket-driver ">=0.5.1" - -firebase-admin@^12.3.0: - version "12.7.0" - resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-12.7.0.tgz#586c9ed852c4bb2d4d72f0d52c1c48a2b6dee793" - integrity sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA== - dependencies: - "@fastify/busboy" "^3.0.0" - "@firebase/database-compat" "1.0.8" - "@firebase/database-types" "1.0.5" - "@types/node" "^22.0.1" - farmhash-modern "^1.1.0" - jsonwebtoken "^9.0.0" - jwks-rsa "^3.1.0" - node-forge "^1.3.1" - uuid "^10.0.0" - optionalDependencies: - "@google-cloud/firestore" "^7.7.0" - "@google-cloud/storage" "^7.7.0" - -form-data@^2.5.5: - version "2.5.5" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.5.tgz#a5f6364ad7e4e67e95b4a07e2d8c6f711c74f624" - integrity sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.35" - safe-buffer "^5.2.1" - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - -gaxios@^6.0.0, gaxios@^6.0.2, gaxios@^6.1.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb" - integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== - dependencies: - extend "^3.0.2" - https-proxy-agent "^7.0.1" - is-stream "^2.0.0" - node-fetch "^2.6.9" - uuid "^9.0.1" - -gcp-metadata@^6.1.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.1.tgz#f65aa69f546bc56e116061d137d3f5f90bdec494" - integrity sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A== - dependencies: - gaxios "^6.1.1" - google-logging-utils "^0.0.2" - json-bigint "^1.0.0" - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.2.6: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -google-auth-library@^9.14.2, google-auth-library@^9.3.0, google-auth-library@^9.6.3: - version "9.15.1" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.15.1.tgz#0c5d84ed1890b2375f1cd74f03ac7b806b392928" - integrity sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng== - dependencies: - base64-js "^1.3.0" - ecdsa-sig-formatter "^1.0.11" - gaxios "^6.1.1" - gcp-metadata "^6.1.0" - gtoken "^7.0.0" - jws "^4.0.0" - -google-gax@^4.3.3: - version "4.6.1" - resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-4.6.1.tgz#57f8e3d893d4c708a71167cdcf47eb3afab95929" - integrity sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ== - dependencies: - "@grpc/grpc-js" "^1.10.9" - "@grpc/proto-loader" "^0.7.13" - "@types/long" "^4.0.0" - abort-controller "^3.0.0" - duplexify "^4.0.0" - google-auth-library "^9.3.0" - node-fetch "^2.7.0" - object-hash "^3.0.0" - proto3-json-serializer "^2.0.2" - protobufjs "^7.3.2" - retry-request "^7.0.0" - uuid "^9.0.1" - -google-logging-utils@^0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz#5fd837e06fa334da450433b9e3e1870c1594466a" - integrity sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ== - -gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -gtoken@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" - integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== - dependencies: - gaxios "^6.0.0" - jws "^4.0.0" - -has-symbols@^1.0.3, has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -html-entities@^2.5.2: - version "2.6.0" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.6.0.tgz#7c64f1ea3b36818ccae3d3fb48b6974208e984f8" - integrity sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ== - -http-parser-js@>=0.5.1: - version "0.5.10" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" - integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== - -http-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" - integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== - dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" - -https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -https-proxy-agent@^7.0.1: - version "7.0.6" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" - integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== - dependencies: - agent-base "^7.1.2" - debug "4" - -inherits@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -jose@^4.15.4: - version "4.15.9" - resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.9.tgz#9b68eda29e9a0614c042fa29387196c7dd800100" - integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA== - -json-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" - integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== - dependencies: - bignumber.js "^9.0.0" - -jsonwebtoken@^9.0.0: - version "9.0.3" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2" - integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== - 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" - -jwa@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" - integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== - dependencies: - buffer-equal-constant-time "^1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jwks-rsa@^3.1.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-3.2.2.tgz#f6d528306befacdbc62c8c0272761faac55ffbb3" - integrity sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w== - dependencies: - "@types/jsonwebtoken" "^9.0.4" - debug "^4.3.4" - jose "^4.15.4" - limiter "^1.1.5" - lru-memoizer "^2.2.0" - -jws@^4.0.0, jws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" - integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== - dependencies: - jwa "^2.0.1" - safe-buffer "^5.0.1" - -limiter@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" - integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== - -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== - -long@^5.0.0: - version "5.3.2" - resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" - integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== - -lru-cache@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -lru-memoizer@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.3.0.tgz#ef0fbc021bceb666794b145eefac6be49dc47f31" - integrity sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug== - dependencies: - lodash.clonedeep "^4.5.0" - lru-cache "6.0.0" - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.35: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" - integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== - -ms@^2.1.1, ms@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -node-fetch@^2.6.9, node-fetch@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - -node-forge@^1.3.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751" - integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg== - -object-hash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" - integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== - -once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -p-limit@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -proto3-json-serializer@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz#5b705203b4d58f3880596c95fad64902617529dd" - integrity sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ== - dependencies: - protobufjs "^7.2.5" - -protobufjs@^7.2.5, protobufjs@^7.2.6, protobufjs@^7.3.2, protobufjs@^7.5.3: - version "7.5.4" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" - integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/node" ">=13.7.0" - long "^5.0.0" - -readable-stream@^3.1.1: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -retry-request@^7.0.0: - version "7.0.2" - resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-7.0.2.tgz#60bf48cfb424ec01b03fca6665dee91d06dd95f3" - integrity sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w== - dependencies: - "@types/request" "^2.48.8" - extend "^3.0.2" - teeny-request "^9.0.0" - -retry@0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.2.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -semver@^7.5.4: - version "7.7.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" - integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== - -stream-events@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" - integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== - dependencies: - stubs "^3.0.0" - -stream-shift@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" - integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strnum@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.1.2.tgz#57bca4fbaa6f271081715dbc9ed7cee5493e28e4" - integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA== - -stubs@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" - integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== - -teeny-request@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-9.0.0.tgz#18140de2eb6595771b1b02203312dfad79a4716d" - integrity sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g== - dependencies: - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - node-fetch "^2.6.9" - stream-events "^1.0.5" - uuid "^9.0.0" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -tslib@^2.1.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - -undici-types@~6.21.0: - version "6.21.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" - integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== - -undici-types@~7.16.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" - integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== - -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -uuid@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" - integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== - -uuid@^8.0.0: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -uuid@^9.0.0, uuid@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -websocket-driver@>=0.5.1: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^17.7.2: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - 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" - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 2c3e7ab3e81657d3336f7c136763061c6fadd1f3 Mon Sep 17 00:00:00 2001 From: David Gamez Diaz <1192523+davidgamez@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:08:27 -0500 Subject: [PATCH 11/11] change middleware file location --- docs/Authentication.md | 2 +- src/app/{context => services}/api-auth-middleware.ts | 0 src/app/services/feeds/index.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/app/{context => services}/api-auth-middleware.ts (100%) diff --git a/docs/Authentication.md b/docs/Authentication.md index 2fcbc5be..1df94572 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -101,7 +101,7 @@ In addition to the GCIP ID token for IAP, SSR API calls also propagate a compact - The `md_session` cookie's JWT is reused as this user‑context token. - On the server, [src/app/utils/auth-server.ts](src/app/utils/auth-server.ts) reads the cookie via `getUserContextJwtFromCookie()`. -- [src/app/context/api-auth-middleware.ts](src/app/context/api-auth-middleware.ts) provides `generateAuthMiddlewareWithToken(accessToken, userContextJwt?)`, which: +- [src/app/services/api-auth-middleware.ts](src/app/services/api-auth-middleware.ts) provides `generateAuthMiddlewareWithToken(accessToken, userContextJwt?)`, which: - Sets `Authorization: Bearer ` for IAP. - When `userContextJwt` is present, also sets `x-mdb-user-context: `. - All server‑side feeds service functions in [src/app/services/feeds/index.ts](src/app/services/feeds/index.ts) accept an optional `userContextJwt` and pass it into this middleware. diff --git a/src/app/context/api-auth-middleware.ts b/src/app/services/api-auth-middleware.ts similarity index 100% rename from src/app/context/api-auth-middleware.ts rename to src/app/services/api-auth-middleware.ts diff --git a/src/app/services/feeds/index.ts b/src/app/services/feeds/index.ts index dd393853..acfb115c 100644 --- a/src/app/services/feeds/index.ts +++ b/src/app/services/feeds/index.ts @@ -3,7 +3,7 @@ import type { paths } from './types'; import { type AllFeedsParams, type AllFeedType } from './utils'; import { type GtfsRoute } from '../../types'; import { getFeedFilesBaseUrl } from '../../utils/config'; -import { generateAuthMiddlewareWithToken } from '../../context/api-auth-middleware'; +import { generateAuthMiddlewareWithToken } from '../api-auth-middleware'; const client = createClient({ baseUrl: String(process.env.NEXT_PUBLIC_FEED_API_BASE_URL),