gogle secrete setup added
This commit is contained in:
parent
47077552cf
commit
f69814ce98
277
docs/GOOGLE_SECRET_MANAGER_SETUP.md
Normal file
277
docs/GOOGLE_SECRET_MANAGER_SETUP.md
Normal file
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
395
package-lock.json
generated
395
package-lock.json
generated
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
11
secret-map.example.json
Normal file
11
secret-map.example.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
15
src/app.ts
15
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<void> {
|
||||
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();
|
||||
|
||||
|
||||
@ -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<void> => {
|
||||
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);
|
||||
|
||||
|
||||
390
src/services/googleSecretManager.service.ts
Normal file
390
src/services/googleSecretManager.service.ts
Normal file
@ -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<string, string> = {};
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<string, string> = {};
|
||||
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<string | null> {
|
||||
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<string[]> {
|
||||
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<void> {
|
||||
await googleSecretManager.loadSecrets(secretNames);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user