diff --git a/docs/GOOGLE_SECRET_MANAGER_SETUP.md b/docs/GOOGLE_SECRET_MANAGER_SETUP.md new file mode 100644 index 0000000..f02853c --- /dev/null +++ b/docs/GOOGLE_SECRET_MANAGER_SETUP.md @@ -0,0 +1,277 @@ +# Google Secret Manager Integration Guide + +This guide explains how to integrate Google Cloud Secret Manager with your Node.js application to securely manage environment variables. + +## Overview + +The Google Secret Manager integration allows you to: +- Store sensitive configuration values (passwords, API keys, tokens) in Google Cloud Secret Manager +- Load secrets at application startup and merge them with your existing environment variables +- Maintain backward compatibility with `.env` files for local development +- Use minimal code changes - existing `process.env.VARIABLE_NAME` access continues to work + +## Prerequisites + +1. **Google Cloud Project** with Secret Manager API enabled +2. **Service Account** with Secret Manager Secret Accessor role +3. **Authentication** - Service account credentials configured (via `GCP_KEY_FILE` or default credentials) + +## Setup Instructions + +### 1. Enable Secret Manager API + +```bash +gcloud services enable secretmanager.googleapis.com --project=YOUR_PROJECT_ID +``` + +### 2. Create Secrets in Google Secret Manager + +Create secrets using the Google Cloud Console or gcloud CLI: + +```bash +# Example: Create a database password secret +echo -n "your-secure-password" | gcloud secrets create DB_PASSWORD \ + --project=YOUR_PROJECT_ID \ + --data-file=- + +# Example: Create a JWT secret +echo -n "your-jwt-secret-key" | gcloud secrets create JWT_SECRET \ + --project=YOUR_PROJECT_ID \ + --data-file=- + +# Grant service account access to secrets +gcloud secrets add-iam-policy-binding DB_PASSWORD \ + --member="serviceAccount:YOUR_SERVICE_ACCOUNT@YOUR_PROJECT.iam.gserviceaccount.com" \ + --role="roles/secretmanager.secretAccessor" \ + --project=YOUR_PROJECT_ID +``` + +### 3. Configure Environment Variables + +Add the following to your `.env` file: + +```env +# Google Secret Manager Configuration +USE_GOOGLE_SECRET_MANAGER=true +GCP_PROJECT_ID=your-project-id + +# Optional: Prefix for all secret names (e.g., "prod" -> looks for "prod-DB_PASSWORD") +GCP_SECRET_PREFIX= + +# Optional: JSON file mapping secret names to env var names +GCP_SECRET_MAP_FILE=./secret-map.json +``` + +**Important Notes:** +- Set `USE_GOOGLE_SECRET_MANAGER=true` to enable the integration +- `GCP_PROJECT_ID` must be set (same as used for GCS/Vertex AI) +- `GCP_KEY_FILE` should already be configured for other GCP services +- When `USE_GOOGLE_SECRET_MANAGER=false` or not set, the app uses `.env` file only + +### 4. Secret Name Mapping + +By default, secrets in Google Secret Manager are automatically mapped to environment variables: +- Secret name: `DB_PASSWORD` → Environment variable: `DB_PASSWORD` +- Secret name: `db-password` → Environment variable: `DB_PASSWORD` (hyphens converted to underscores, uppercase) +- Secret name: `jwt-secret-key` → Environment variable: `JWT_SECRET_KEY` + +#### Custom Mapping (Optional) + +If you need custom mappings, create a JSON file (e.g., `secret-map.json`): + +```json +{ + "db-password-prod": "DB_PASSWORD", + "jwt-secret-key": "JWT_SECRET", + "okta-client-secret-prod": "OKTA_CLIENT_SECRET" +} +``` + +Then set in `.env`: +```env +GCP_SECRET_MAP_FILE=./secret-map.json +``` + +### 5. Secret Prefix (Optional) + +If all your secrets share a common prefix: + +```env +GCP_SECRET_PREFIX=prod +``` + +This will look for secrets named `prod-DB_PASSWORD`, `prod-JWT_SECRET`, etc. + +## How It Works + +1. **Application Startup:** + - `.env` file is loaded first (provides fallback values) + - If `USE_GOOGLE_SECRET_MANAGER=true`, secrets are fetched from Google Secret Manager + - Secrets are merged into `process.env`, overriding `.env` values if they exist + - Application continues with merged environment variables + +2. **Fallback Behavior:** + - If Secret Manager is disabled or fails, the app falls back to `.env` file + - No errors are thrown - the app continues with available configuration + - Logs indicate whether secrets were loaded successfully + +3. **Existing Code Compatibility:** + - No changes needed to existing code + - Continue using `process.env.VARIABLE_NAME` as before + - Secrets from GCS automatically populate `process.env` + +## Default Secrets Loaded + +The service automatically attempts to load these common secrets (if they exist in Secret Manager): + +**Database:** +- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` + +**Authentication:** +- `JWT_SECRET`, `REFRESH_TOKEN_SECRET`, `SESSION_SECRET` + +**SSO/Okta:** +- `OKTA_DOMAIN`, `OKTA_CLIENT_ID`, `OKTA_CLIENT_SECRET`, `OKTA_API_TOKEN` + +**Email:** +- `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD` + +**Web Push (VAPID):** +- `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY` + +**Logging:** +- `LOKI_HOST`, `LOKI_USER`, `LOKI_PASSWORD` + +### Loading Custom Secrets + +To load additional secrets, modify the code: + +```typescript +// In server.ts or app.ts +import { googleSecretManager } from './services/googleSecretManager.service'; + +// Load default secrets + custom ones +await googleSecretManager.loadSecrets([ + 'DB_PASSWORD', + 'JWT_SECRET', + 'CUSTOM_API_KEY', // Your custom secret + 'CUSTOM_SECRET_2' +]); +``` + +Or load a single secret on-demand: + +```typescript +import { googleSecretManager } from './services/googleSecretManager.service'; + +const apiKey = await googleSecretManager.getSecretValue('CUSTOM_API_KEY', 'API_KEY'); +``` + +## Security Best Practices + +1. **Service Account Permissions:** + - Grant only `roles/secretmanager.secretAccessor` role + - Use separate service accounts for different environments + - Never grant `roles/owner` or `roles/editor` to service accounts + +2. **Secret Rotation:** + - Rotate secrets regularly in Google Secret Manager + - The app automatically uses the `latest` version of each secret + - No code changes needed when secrets are rotated + +3. **Environment Separation:** + - Use different Google Cloud projects for dev/staging/prod + - Use `GCP_SECRET_PREFIX` to namespace secrets by environment + - Never commit `.env` files with production secrets to version control + +4. **Access Control:** + - Use IAM policies to control who can read secrets + - Enable audit logging for secret access + - Regularly review secret access logs + +## Troubleshooting + +### Secrets Not Loading + +**Check logs for:** +``` +[Secret Manager] Google Secret Manager is disabled (USE_GOOGLE_SECRET_MANAGER != true) +[Secret Manager] GCP_PROJECT_ID not set, skipping Google Secret Manager +[Secret Manager] Failed to load secrets: [error message] +``` + +**Common issues:** +1. `USE_GOOGLE_SECRET_MANAGER` not set to `true` +2. `GCP_PROJECT_ID` not configured +3. Service account lacks Secret Manager permissions +4. Secrets don't exist in Secret Manager +5. Incorrect secret names (check case sensitivity) + +### Service Account Authentication + +Ensure service account credentials are available: +- Set `GCP_KEY_FILE` to point to service account JSON file +- Or configure Application Default Credentials (ADC) +- Test with: `gcloud auth application-default login` + +### Secret Not Found + +If a secret doesn't exist in Secret Manager: +- The app logs a debug message and continues +- Falls back to `.env` file value +- This is expected behavior - not all secrets need to be in GCS + +### Debugging + +Enable debug logging by setting: +```env +LOG_LEVEL=debug +``` + +This will show detailed logs about which secrets are being loaded. + +## Example Configuration + +**Local Development (.env):** +```env +USE_GOOGLE_SECRET_MANAGER=false +DB_PASSWORD=local-dev-password +JWT_SECRET=local-jwt-secret +``` + +**Production (.env):** +```env +USE_GOOGLE_SECRET_MANAGER=true +GCP_PROJECT_ID=re-platform-workflow-dealer +GCP_SECRET_PREFIX=prod +GCP_KEY_FILE=./credentials/service-account.json +# DB_PASSWORD and other secrets loaded from GCS +``` + +## Migration Strategy + +1. **Phase 1: Setup** + - Create secrets in Google Secret Manager + - Keep `.env` file with current values (as backup) + +2. **Phase 2: Test** + - Set `USE_GOOGLE_SECRET_MANAGER=true` in development + - Verify secrets are loaded correctly + - Test application functionality + +3. **Phase 3: Production** + - Deploy with `USE_GOOGLE_SECRET_MANAGER=true` + - Monitor logs for secret loading success + - Remove sensitive values from `.env` file (keep placeholders) + +4. **Phase 4: Cleanup** + - Remove production secrets from `.env` file + - Ensure all secrets are in Secret Manager + - Document secret names and mappings + +## Additional Resources + +- [Google Secret Manager Documentation](https://cloud.google.com/secret-manager/docs) +- [Secret Manager Client Library](https://cloud.google.com/nodejs/docs/reference/secret-manager/latest) +- [Service Account Best Practices](https://cloud.google.com/iam/docs/best-practices-service-accounts) + diff --git a/env.example b/env.example index 06fea52..c229c80 100644 --- a/env.example +++ b/env.example @@ -30,6 +30,13 @@ GCP_PROJECT_ID=re-workflow-project GCP_BUCKET_NAME=re-workflow-documents GCP_KEY_FILE=./config/gcp-key.json +# Google Secret Manager (Optional - for production) +# Set USE_GOOGLE_SECRET_MANAGER=true to enable loading secrets from Google Secret Manager +# Secrets from GCS will override .env file values +USE_GOOGLE_SECRET_MANAGER=false +# GCP_SECRET_PREFIX=optional-prefix-for-secret-names (e.g., "prod" -> looks for "prod-DB_PASSWORD") +# GCP_SECRET_MAP_FILE=./secret-map.json (optional JSON file to map secret names to env var names) + # Email Service (Optional) SMTP_HOST=smtp.gmail.com SMTP_PORT=587 diff --git a/package-lock.json b/package-lock.json index e94c366..de90345 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "re-workflow-backend", "version": "1.0.0", "dependencies": { + "@google-cloud/secret-manager": "^6.1.1", "@google-cloud/storage": "^7.18.0", "@google-cloud/vertexai": "^1.10.0", "@types/nodemailer": "^7.0.4", @@ -1625,6 +1626,18 @@ "node": ">=14" } }, + "node_modules/@google-cloud/secret-manager": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/secret-manager/-/secret-manager-6.1.1.tgz", + "integrity": "sha512-dwSuxJ9RNmAW46FjK1StiNIeOiSHHQs/XIy4VArJ6bBMR+WsIvR+zhPh2pa40aFa9uTty67j38Rl268TVV62EA==", + "license": "Apache-2.0", + "dependencies": { + "google-gax": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@google-cloud/storage": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.18.0.tgz", @@ -1663,6 +1676,37 @@ "node": ">=18.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1725,7 +1769,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -1743,7 +1786,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1756,7 +1798,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1769,14 +1810,12 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -1794,7 +1833,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -1810,7 +1848,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -2283,6 +2320,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -2742,7 +2789,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -4329,7 +4375,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4339,7 +4384,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4610,7 +4654,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -4766,7 +4809,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -5076,7 +5118,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -5131,7 +5172,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5144,7 +5184,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/color-string": { @@ -5420,7 +5459,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5431,6 +5469,15 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", @@ -5645,7 +5692,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/ecdsa-sig-formatter": { @@ -5722,7 +5768,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/enabled": { @@ -5884,7 +5929,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6389,6 +6433,29 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6516,7 +6583,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -6533,7 +6599,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -6558,6 +6623,18 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/formidable": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", @@ -6698,7 +6775,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6897,6 +6973,203 @@ "node": ">=14" } }, + "node_modules/google-gax": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.6.tgz", + "integrity": "sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "^10.1.0", + "google-logging-utils": "^1.1.1", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^3.0.0", + "protobufjs": "^7.5.3", + "retry-request": "^8.0.0", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/google-gax/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/google-gax/node_modules/retry-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.2.tgz", + "integrity": "sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==", + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/teeny-request": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.0.tgz", + "integrity": "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/google-logging-utils": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", @@ -7315,7 +7588,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7376,7 +7648,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -7454,7 +7725,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -8341,6 +8611,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -8604,7 +8880,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -8629,7 +8904,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -8822,6 +9096,26 @@ "node": ">=6.0.0" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -9004,6 +9298,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -9155,7 +9458,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -9259,7 +9561,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9276,7 +9577,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -9293,7 +9593,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, "license": "ISC" }, "node_modules/path-to-regexp": { @@ -9663,6 +9962,18 @@ "dev": true, "license": "ISC" }, + "node_modules/proto3-json-serializer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", + "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -9869,7 +10180,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10294,7 +10604,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -10307,7 +10616,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10703,7 +11011,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -10719,7 +11026,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -10734,7 +11040,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10748,7 +11053,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11601,6 +11905,15 @@ "node": ">= 16" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -11621,7 +11934,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -11715,7 +12027,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -11734,7 +12045,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -11804,7 +12114,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -11821,7 +12130,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -11840,7 +12148,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 8852283..b345113 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "seed:config": "ts-node -r tsconfig-paths/register src/scripts/seed-admin-config.ts" }, "dependencies": { + "@google-cloud/secret-manager": "^6.1.1", "@google-cloud/storage": "^7.18.0", "@google-cloud/vertexai": "^1.10.0", "@types/nodemailer": "^7.0.4", diff --git a/secret-map.example.json b/secret-map.example.json new file mode 100644 index 0000000..7475dbc --- /dev/null +++ b/secret-map.example.json @@ -0,0 +1,11 @@ +{ + "_comment": "Optional: Map Google Secret Manager secret names to environment variable names", + "_comment2": "If not provided, secrets are mapped automatically: secret-name -> SECRET_NAME (uppercase)", + + "examples": { + "db-password": "DB_PASSWORD", + "jwt-secret-key": "JWT_SECRET", + "okta-client-secret": "OKTA_CLIENT_SECRET" + } +} + diff --git a/src/app.ts b/src/app.ts index e221aae..2f4ebe0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,11 +10,24 @@ import { corsMiddleware } from './middlewares/cors.middleware'; import { metricsMiddleware, createMetricsRouter } from './middlewares/metrics.middleware'; import routes from './routes/index'; import { ensureUploadDir, UPLOAD_DIR } from './config/storage'; +import { initializeGoogleSecretManager } from './services/googleSecretManager.service'; import path from 'path'; -// Load environment variables +// Load environment variables from .env file first dotenv.config(); +// Initialize Google Secret Manager (async, but we'll wait for it in server.ts) +// This will merge secrets from GCS into process.env if USE_GOOGLE_SECRET_MANAGER=true +// Export initialization function so server.ts can await it before starting +export async function initializeSecrets(): Promise { + try { + await initializeGoogleSecretManager(); + } catch (error) { + // Log error but don't throw - allow fallback to .env + console.error('⚠️ Failed to initialize Google Secret Manager, using .env file:', error); + } +} + const app: express.Application = express(); const userService = new UserService(); diff --git a/src/server.ts b/src/server.ts index ff4d10a..8645a99 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,6 @@ -import app from './app'; import http from 'http'; +import { initializeSecrets } from './app'; // Import initialization function +import app from './app'; import { initSocket } from './realtime/socket'; import './queues/tatWorker'; // Initialize TAT worker import { logTatConfig } from './config/tat.config'; @@ -15,6 +16,10 @@ const PORT: number = parseInt(process.env.PORT || '5000', 10); // Start server const startServer = async (): Promise => { try { + // Initialize Google Secret Manager before starting server + // This will merge secrets from GCS into process.env if enabled + await initializeSecrets(); + const server = http.createServer(app); initSocket(server); diff --git a/src/services/googleSecretManager.service.ts b/src/services/googleSecretManager.service.ts new file mode 100644 index 0000000..5d6022f --- /dev/null +++ b/src/services/googleSecretManager.service.ts @@ -0,0 +1,390 @@ +import { SecretManagerServiceClient } from '@google-cloud/secret-manager'; +import logger from '@utils/logger'; +import path from 'path'; +import fs from 'fs'; + +/** + * Google Secret Manager Service + * + * This service loads secrets from Google Cloud Secret Manager and merges them + * with the current process.env, allowing for minimal changes to existing code. + * + * Configuration: + * - Set USE_GOOGLE_SECRET_MANAGER=true to enable (default: false) + * - Set GCP_PROJECT_ID to your Google Cloud Project ID + * - Set GCP_SECRET_PREFIX to prefix all secret names (optional, default: empty) + * - Set GCP_SECRET_MAP_FILE to map secret names to env vars (optional, see format below) + * + * Secret Name Mapping: + * - By default, secrets are mapped to env vars with the same name (uppercase) + * - Example: secret "db_password" -> process.env.DB_PASSWORD + * - Use GCP_SECRET_MAP_FILE to provide a JSON mapping file for custom mappings + * + * Fallback: + * - If Google Secret Manager is disabled or fails, falls back to .env file + * - Existing environment variables take precedence unless explicitly overridden + */ +class GoogleSecretManagerService { + private client: SecretManagerServiceClient | null = null; + private projectId: string; + private secretPrefix: string; + private secretMap: Record = {}; + private isInitialized: boolean = false; + + constructor() { + this.projectId = process.env.GCP_PROJECT_ID || ''; + this.secretPrefix = process.env.GCP_SECRET_PREFIX || ''; + + // Load secret mapping file if provided + const mapFile = process.env.GCP_SECRET_MAP_FILE; + if (mapFile) { + try { + const fs = require('fs'); + const path = require('path'); + const mapFilePath = path.resolve(mapFile); + if (fs.existsSync(mapFilePath)) { + const mapContent = fs.readFileSync(mapFilePath, 'utf8'); + this.secretMap = JSON.parse(mapContent); + logger.info(`[Secret Manager] Loaded secret mapping from ${mapFilePath}`); + } else { + logger.warn(`[Secret Manager] Secret mapping file not found: ${mapFilePath}`); + } + } catch (error: any) { + logger.warn(`[Secret Manager] Failed to load secret mapping file: ${error.message}`); + } + } + } + + /** + * Initialize Google Secret Manager client + */ + private async initializeClient(): Promise { + if (this.client) { + return; + } + + try { + const keyFilePath = process.env.GCP_KEY_FILE || ''; + let originalCredentialsEnv: string | undefined; + + // If GCP_KEY_FILE is specified, set GOOGLE_APPLICATION_CREDENTIALS temporarily + if (keyFilePath) { + const resolvedKeyPath = path.isAbsolute(keyFilePath) + ? keyFilePath + : path.resolve(process.cwd(), keyFilePath); + + if (fs.existsSync(resolvedKeyPath)) { + // Save original value if it exists + originalCredentialsEnv = process.env.GOOGLE_APPLICATION_CREDENTIALS; + // Set it to use the key file + process.env.GOOGLE_APPLICATION_CREDENTIALS = resolvedKeyPath; + logger.debug(`[Secret Manager] Using key file: ${resolvedKeyPath}`); + } else { + logger.warn(`[Secret Manager] Key file not found at: ${resolvedKeyPath}`); + logger.warn('[Secret Manager] Will attempt to use Application Default Credentials'); + } + } + + try { + // Create client - it will use GOOGLE_APPLICATION_CREDENTIALS if set + this.client = new SecretManagerServiceClient({ + projectId: this.projectId, + }); + logger.info('[Secret Manager] ✅ Google Secret Manager client initialized'); + } finally { + // Restore original GOOGLE_APPLICATION_CREDENTIALS if we changed it + if (keyFilePath && originalCredentialsEnv !== undefined) { + if (originalCredentialsEnv) { + process.env.GOOGLE_APPLICATION_CREDENTIALS = originalCredentialsEnv; + } else { + delete process.env.GOOGLE_APPLICATION_CREDENTIALS; + } + } + } + } catch (error: any) { + logger.error('[Secret Manager] Failed to initialize client:', error); + if (error.message?.includes('Could not load the default credentials')) { + logger.error('[Secret Manager] Authentication failed. Please check:'); + logger.error('[Secret Manager] 1. GCP_KEY_FILE points to a valid service account JSON file'); + logger.error('[Secret Manager] 2. Service account has Secret Manager Secret Accessor role'); + logger.error('[Secret Manager] 3. The key file is readable and not corrupted'); + } + throw error; + } + } + + /** + * Get secret value from Google Secret Manager + */ + private async getSecret(secretName: string): Promise { + if (!this.client || !this.projectId) { + return null; + } + + const fullSecretName = this.secretPrefix + ? `${this.secretPrefix}-${secretName}` + : secretName; + + const name = `projects/${this.projectId}/secrets/${fullSecretName}/versions/latest`; + + try { + const [version] = await this.client.accessSecretVersion({ name }); + + if (version.payload?.data) { + const secretValue = version.payload.data.toString(); + logger.debug(`[Secret Manager] ✅ Fetched secret: ${fullSecretName}`); + return secretValue; + } + + logger.debug(`[Secret Manager] Secret ${fullSecretName} exists but payload is empty`); + return null; + } catch (error: any) { + // Handle "not found" errors (code 5 = NOT_FOUND) + if (error.code === 5 || error.code === 'NOT_FOUND' || error.message?.includes('not found')) { + logger.debug(`[Secret Manager] Secret not found: ${fullSecretName} (project: ${this.projectId})`); + return null; + } + + // Handle permission errors (code 7 = PERMISSION_DENIED) + if (error.code === 7 || error.code === 'PERMISSION_DENIED' || error.message?.includes('Permission denied')) { + logger.warn(`[Secret Manager] ❌ Permission denied for secret '${fullSecretName}'`); + logger.warn(`[Secret Manager] Service account needs 'Secret Manager Secret Accessor' role`); + logger.warn(`[Secret Manager] To grant access, run:`); + logger.warn(`[Secret Manager] gcloud secrets add-iam-policy-binding ${fullSecretName} \\`); + logger.warn(`[Secret Manager] --member="serviceAccount:YOUR_SERVICE_ACCOUNT@${this.projectId}.iam.gserviceaccount.com" \\`); + logger.warn(`[Secret Manager] --role="roles/secretmanager.secretAccessor" \\`); + logger.warn(`[Secret Manager] --project=${this.projectId}`); + return null; + } + + // Log full error details for debugging + logger.warn(`[Secret Manager] Failed to fetch secret '${fullSecretName}' (project: ${this.projectId})`); + logger.warn(`[Secret Manager] Error code: ${error.code || 'unknown'}, Message: ${error.message || 'no message'}`); + if (error.details) { + logger.warn(`[Secret Manager] Error details: ${JSON.stringify(error.details)}`); + } + + return null; + } + } + + /** + * Map secret name to environment variable name + */ + private getEnvVarName(secretName: string): string { + // Check if there's a custom mapping + if (this.secretMap[secretName]) { + return this.secretMap[secretName]; + } + + // Default: convert secret name to uppercase and replace hyphens with underscores + // Example: "db-password" -> "DB_PASSWORD", "JWT_SECRET" -> "JWT_SECRET" + return secretName.toUpperCase().replace(/-/g, '_'); + } + + /** + * Load all secrets from Google Secret Manager and merge with process.env + * + * @param secretNames - Array of secret names to load. If not provided, + * will attempt to load common secret names based on env.example + */ + async loadSecrets(secretNames?: string[]): Promise { + const useSecretManager = process.env.USE_GOOGLE_SECRET_MANAGER === 'true'; + + if (!useSecretManager) { + logger.debug('[Secret Manager] Google Secret Manager is disabled (USE_GOOGLE_SECRET_MANAGER != true)'); + return; + } + + if (!this.projectId) { + logger.warn('[Secret Manager] GCP_PROJECT_ID not set, skipping Google Secret Manager'); + return; + } + + try { + await this.initializeClient(); + + // Default list of secrets to load if not provided + const secretsToLoad = secretNames || this.getDefaultSecretNames(); + + logger.info(`[Secret Manager] Loading ${secretsToLoad.length} secrets from Google Secret Manager (project: ${this.projectId})...`); + if (this.secretPrefix) { + logger.info(`[Secret Manager] Using secret prefix: ${this.secretPrefix}`); + } + + const loadedSecrets: Record = {}; + const notFoundSecrets: string[] = []; + let loadedCount = 0; + + // Load each secret + for (const secretName of secretsToLoad) { + const secretValue = await this.getSecret(secretName); + + if (secretValue !== null) { + const envVarName = this.getEnvVarName(secretName); + loadedSecrets[envVarName] = secretValue; + loadedCount++; + } else { + // Track which secrets weren't found for better logging + const fullSecretName = this.secretPrefix + ? `${this.secretPrefix}-${secretName}` + : secretName; + notFoundSecrets.push(fullSecretName); + } + } + + // Merge secrets into process.env (only override if secret exists) + for (const [envVar, value] of Object.entries(loadedSecrets)) { + process.env[envVar] = value; + } + + logger.info(`[Secret Manager] ✅ Successfully loaded ${loadedCount}/${secretsToLoad.length} secrets`); + + if (loadedCount > 0) { + const loadedVars = Object.keys(loadedSecrets); + logger.info(`[Secret Manager] Loaded env vars: ${loadedVars.join(', ')}`); + } else { + logger.warn(`[Secret Manager] ⚠️ No secrets were loaded. This might be normal if secrets don't exist yet.`); + logger.info(`[Secret Manager] To create secrets, use: gcloud secrets create SECRET_NAME --data-file=- --project=${this.projectId}`); + if (notFoundSecrets.length > 0 && notFoundSecrets.length <= 5) { + logger.info(`[Secret Manager] Example secrets to create: ${notFoundSecrets.slice(0, 3).join(', ')}`); + } + } + + this.isInitialized = true; + } catch (error: any) { + logger.error('[Secret Manager] Failed to load secrets:', error); + // Don't throw - allow fallback to .env file + logger.warn('[Secret Manager] Falling back to .env file and existing environment variables'); + } + } + + /** + * Get default list of secret names based on common environment variables + */ + private getDefaultSecretNames(): string[] { + return [ + // Database + 'DB_HOST', + 'DB_PORT', + 'DB_NAME', + 'DB_USER', + 'DB_PASSWORD', + + // JWT & Session + 'JWT_SECRET', + 'REFRESH_TOKEN_SECRET', + 'SESSION_SECRET', + + // Okta/SSO + 'OKTA_DOMAIN', + 'OKTA_CLIENT_ID', + 'OKTA_CLIENT_SECRET', + 'OKTA_API_TOKEN', + + // Email + 'SMTP_HOST', + 'SMTP_PORT', + 'SMTP_USER', + 'SMTP_PASSWORD', + + // VAPID (Web Push) + 'VAPID_PUBLIC_KEY', + 'VAPID_PRIVATE_KEY', + + // Loki + 'LOKI_HOST', + 'LOKI_USER', + 'LOKI_PASSWORD', + ]; + } + + /** + * Load a single secret value + * Useful for on-demand secret retrieval + */ + async getSecretValue(secretName: string, envVarName?: string): Promise { + const useSecretManager = process.env.USE_GOOGLE_SECRET_MANAGER === 'true'; + + if (!useSecretManager || !this.projectId) { + return null; + } + + try { + if (!this.client) { + await this.initializeClient(); + } + + const secretValue = await this.getSecret(secretName); + + if (secretValue !== null) { + const envVar = envVarName || this.getEnvVarName(secretName); + process.env[envVar] = secretValue; + logger.debug(`[Secret Manager] ✅ Loaded secret ${secretName} -> ${envVar}`); + } + + return secretValue; + } catch (error: any) { + logger.error(`[Secret Manager] Failed to get secret ${secretName}:`, error); + return null; + } + } + + /** + * Check if secret manager is initialized and ready + */ + isReady(): boolean { + return this.isInitialized; + } + + /** + * List all secrets in the project (for debugging/setup verification) + * Returns array of secret names + */ + async listSecrets(): Promise { + const useSecretManager = process.env.USE_GOOGLE_SECRET_MANAGER === 'true'; + + if (!useSecretManager || !this.projectId) { + logger.warn('[Secret Manager] Cannot list secrets: Secret Manager not enabled or project ID not set'); + return []; + } + + try { + if (!this.client) { + await this.initializeClient(); + } + + if (!this.client) { + return []; + } + + const parent = `projects/${this.projectId}`; + const [secrets] = await this.client.listSecrets({ parent }); + + const secretNames = secrets.map(secret => { + // Extract secret name from full path: projects/PROJECT/secrets/NAME -> NAME + const nameParts = secret.name?.split('/') || []; + return nameParts[nameParts.length - 1] || ''; + }).filter(Boolean); + + logger.info(`[Secret Manager] Found ${secretNames.length} secrets in project ${this.projectId}`); + if (secretNames.length > 0) { + logger.info(`[Secret Manager] Available secrets: ${secretNames.slice(0, 10).join(', ')}${secretNames.length > 10 ? '...' : ''}`); + } + + return secretNames; + } catch (error: any) { + logger.error(`[Secret Manager] Failed to list secrets: ${error.message || JSON.stringify(error)}`); + return []; + } + } +} + +// Export singleton instance +export const googleSecretManager = new GoogleSecretManagerService(); + +// Export for easy initialization +export async function initializeGoogleSecretManager(secretNames?: string[]): Promise { + await googleSecretManager.loadSecrets(secretNames); +} +