@dataformer/env-service 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/index.js +109 -18
- package/package.json +2 -3
- package/src/index.ts +137 -20
package/CHANGELOG.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,24 +1,60 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import readline from "readline";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
function getGcpProjectIdFromGcloud() {
|
|
7
|
+
try {
|
|
8
|
+
const cmd = "gcloud config get-value core/project";
|
|
9
|
+
const projectId2 = execSync(cmd, { encoding: "utf8", stdio: "pipe" }).trim();
|
|
10
|
+
if (projectId2 && projectId2 !== "(unset)") {
|
|
11
|
+
return projectId2;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function getProjectId() {
|
|
19
|
+
if (process.env.GCP_PROJECT_ID) {
|
|
20
|
+
return process.env.GCP_PROJECT_ID;
|
|
21
|
+
}
|
|
22
|
+
const gcloudProjectId = getGcpProjectIdFromGcloud();
|
|
23
|
+
if (gcloudProjectId) {
|
|
24
|
+
return gcloudProjectId;
|
|
25
|
+
}
|
|
26
|
+
throw new Error(
|
|
27
|
+
"\u{1F6A8} GCP project ID is required for Secret Manager.\n\nCI/CD DEPLOYMENT (REQUIRED):\n Set GCP_PROJECT_ID as environment variable\n Example: GCP_PROJECT_ID=dataformer-prod\n\nLOCAL DEVELOPMENT:\n Set your gcloud project: gcloud config set project YOUR_PROJECT_ID\n Verify with: gcloud config get-value project"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
var secretManagerClient = null;
|
|
6
31
|
var secretManagerInitialized = false;
|
|
7
|
-
|
|
32
|
+
var projectId = null;
|
|
33
|
+
async function getSecretManagerClient() {
|
|
8
34
|
if (secretManagerInitialized) {
|
|
9
|
-
return
|
|
35
|
+
return secretManagerClient && projectId ? { client: secretManagerClient, projectId } : null;
|
|
10
36
|
}
|
|
11
37
|
try {
|
|
12
|
-
|
|
38
|
+
projectId = getProjectId();
|
|
39
|
+
secretManagerClient = new SecretManagerServiceClient({ projectId });
|
|
13
40
|
secretManagerInitialized = true;
|
|
14
|
-
return
|
|
41
|
+
return { client: secretManagerClient, projectId };
|
|
15
42
|
} catch (error) {
|
|
16
|
-
console.error("[EnvService.ts] Failed to initialize
|
|
43
|
+
console.error("[EnvService.ts] Failed to initialize SecretManagerServiceClient:", error.message);
|
|
17
44
|
console.warn("[EnvService.ts] Secret Manager functionality will be unavailable.");
|
|
18
45
|
secretManagerInitialized = true;
|
|
19
46
|
return null;
|
|
20
47
|
}
|
|
21
48
|
}
|
|
49
|
+
function buildSecretName(projectId2, baseSecretId) {
|
|
50
|
+
return `${projectId2}_${baseSecretId}`;
|
|
51
|
+
}
|
|
52
|
+
function buildSecretPath(projectId2, secretName) {
|
|
53
|
+
return `projects/${projectId2}/secrets/${secretName}`;
|
|
54
|
+
}
|
|
55
|
+
function buildSecretVersionPath(projectId2, secretName, version = "latest") {
|
|
56
|
+
return `projects/${projectId2}/secrets/${secretName}/versions/${version}`;
|
|
57
|
+
}
|
|
22
58
|
var rl = readline.createInterface({
|
|
23
59
|
input: process.stdin,
|
|
24
60
|
output: process.stdout
|
|
@@ -40,12 +76,26 @@ function obfuscateCred(cred) {
|
|
|
40
76
|
}
|
|
41
77
|
}
|
|
42
78
|
async function getEnvVar(baseSecretKey) {
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
79
|
+
const smClient = await getSecretManagerClient();
|
|
80
|
+
if (!smClient) {
|
|
45
81
|
console.warn(`[EnvService.ts] SecretManager not available for getEnvVar (key: ${baseSecretKey})`);
|
|
46
82
|
return null;
|
|
47
83
|
}
|
|
48
|
-
|
|
84
|
+
try {
|
|
85
|
+
const secretName = buildSecretName(smClient.projectId, baseSecretKey);
|
|
86
|
+
const secretVersionPath = buildSecretVersionPath(smClient.projectId, secretName);
|
|
87
|
+
const [response] = await smClient.client.accessSecretVersion({
|
|
88
|
+
name: secretVersionPath
|
|
89
|
+
});
|
|
90
|
+
const secretValue = response.payload?.data?.toString();
|
|
91
|
+
return secretValue || null;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error.code === 5) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
console.warn(`[EnvService.ts] Failed to get secret ${baseSecretKey}: ${error.message}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
49
99
|
}
|
|
50
100
|
async function getCustomSearchApiKey() {
|
|
51
101
|
const apiKey = await getEnvVar("CUSTOM_SEARCH_API_KEY");
|
|
@@ -67,15 +117,15 @@ async function getGcpProjectId() {
|
|
|
67
117
|
return envProjectId;
|
|
68
118
|
}
|
|
69
119
|
try {
|
|
70
|
-
const { execSync } = await import("child_process");
|
|
71
|
-
const
|
|
120
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
121
|
+
const projectId2 = execSync2("gcloud config get-value project", {
|
|
72
122
|
encoding: "utf8",
|
|
73
123
|
stdio: ["ignore", "pipe", "ignore"]
|
|
74
124
|
}).trim();
|
|
75
|
-
if (!
|
|
125
|
+
if (!projectId2 || projectId2 === "(unset)") {
|
|
76
126
|
throw new Error("No GCP project configured. Run `gcloud config set project YOUR_PROJECT_ID`");
|
|
77
127
|
}
|
|
78
|
-
return
|
|
128
|
+
return projectId2;
|
|
79
129
|
} catch (error) {
|
|
80
130
|
throw new Error(
|
|
81
131
|
"GCP_PROJECT_ID not found. Either:\n1. Set GCP_PROJECT_ID environment variable (for deployed environments), or\n2. Configure gcloud: `gcloud config set project YOUR_PROJECT_ID` (for local development)"
|
|
@@ -83,17 +133,58 @@ async function getGcpProjectId() {
|
|
|
83
133
|
}
|
|
84
134
|
}
|
|
85
135
|
async function setEnvVar(baseSecretKey, value) {
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
136
|
+
const smClient = await getSecretManagerClient();
|
|
137
|
+
if (!smClient) {
|
|
88
138
|
console.warn(`[EnvService.ts] SecretManager not available for setEnvVar (key: ${baseSecretKey})`);
|
|
89
139
|
return false;
|
|
90
140
|
}
|
|
91
|
-
|
|
141
|
+
try {
|
|
142
|
+
const secretName = buildSecretName(smClient.projectId, baseSecretKey);
|
|
143
|
+
const addVersion = async () => {
|
|
144
|
+
const secretPath = buildSecretPath(smClient.projectId, secretName);
|
|
145
|
+
await smClient.client.addSecretVersion({
|
|
146
|
+
parent: secretPath,
|
|
147
|
+
payload: {
|
|
148
|
+
data: Buffer.from(value, "utf8")
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
console.log(`\u2705 Secret '${baseSecretKey}' saved in GCP project: ${smClient.projectId}`);
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
await addVersion();
|
|
155
|
+
return true;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err.code === 5) {
|
|
158
|
+
await smClient.client.createSecret({
|
|
159
|
+
parent: `projects/${smClient.projectId}`,
|
|
160
|
+
secretId: secretName,
|
|
161
|
+
secret: {
|
|
162
|
+
replication: {
|
|
163
|
+
automatic: {}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
console.log(`[EnvService.ts] Created secret resource: ${secretName}`);
|
|
168
|
+
await addVersion();
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
console.warn(`[EnvService.ts] setEnvVar failed: ${err.message}`);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.warn(`[EnvService.ts] Failed to set secret ${baseSecretKey}: ${error.message}`);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
92
178
|
}
|
|
93
179
|
var setLogToConsole = async () => {
|
|
94
180
|
const logToConsole = await askQuestion("Use consoleLogger to log to console? (yes, no)", "no");
|
|
95
181
|
if (logToConsole) {
|
|
96
|
-
|
|
182
|
+
const normalizedValue = logToConsole.toLowerCase();
|
|
183
|
+
if (normalizedValue === "yes" || normalizedValue === "no") {
|
|
184
|
+
await setEnvVar("LOG_TO_CONSOLE", normalizedValue);
|
|
185
|
+
} else {
|
|
186
|
+
console.warn(`Invalid LOG_TO_CONSOLE value: '${logToConsole}'. Only 'yes' or 'no' are valid.`);
|
|
187
|
+
}
|
|
97
188
|
}
|
|
98
189
|
};
|
|
99
190
|
var setPostgresUser = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dataformer/env-service",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Environment service for Dataformer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@google-cloud/secret-manager": "^5.4.0"
|
|
16
|
-
"@dataformer/secret-manager-client": "^2.3.0"
|
|
15
|
+
"@google-cloud/secret-manager": "^5.4.0"
|
|
17
16
|
},
|
|
18
17
|
"devDependencies": {
|
|
19
18
|
"tsup": "^8.0.0",
|
package/src/index.ts
CHANGED
|
@@ -39,29 +39,83 @@
|
|
|
39
39
|
|
|
40
40
|
import readline from 'readline';
|
|
41
41
|
import path from 'path';
|
|
42
|
-
import {
|
|
42
|
+
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
|
|
43
|
+
import { execSync } from 'child_process';
|
|
43
44
|
|
|
44
|
-
//
|
|
45
|
-
|
|
45
|
+
// Helper functions for GCP project ID detection
|
|
46
|
+
function getGcpProjectIdFromGcloud(): string | null {
|
|
47
|
+
try {
|
|
48
|
+
const cmd = 'gcloud config get-value core/project';
|
|
49
|
+
const projectId = execSync(cmd, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
50
|
+
if (projectId && projectId !== '(unset)') {
|
|
51
|
+
return projectId;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getProjectId(): string {
|
|
60
|
+
// Project ID resolution priority:
|
|
61
|
+
// 1. GCP_PROJECT_ID env var (REQUIRED for CI/CD)
|
|
62
|
+
// 2. gcloud config project (for local development)
|
|
63
|
+
if (process.env.GCP_PROJECT_ID) {
|
|
64
|
+
return process.env.GCP_PROJECT_ID;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const gcloudProjectId = getGcpProjectIdFromGcloud();
|
|
68
|
+
if (gcloudProjectId) {
|
|
69
|
+
return gcloudProjectId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error(
|
|
73
|
+
'🚨 GCP project ID is required for Secret Manager.\n\n' +
|
|
74
|
+
'CI/CD DEPLOYMENT (REQUIRED):\n' +
|
|
75
|
+
' Set GCP_PROJECT_ID as environment variable\n' +
|
|
76
|
+
' Example: GCP_PROJECT_ID=dataformer-prod\n\n' +
|
|
77
|
+
'LOCAL DEVELOPMENT:\n' +
|
|
78
|
+
' Set your gcloud project: gcloud config set project YOUR_PROJECT_ID\n' +
|
|
79
|
+
' Verify with: gcloud config get-value project'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Secret Manager direct implementation
|
|
84
|
+
let secretManagerClient: SecretManagerServiceClient | null = null;
|
|
46
85
|
let secretManagerInitialized = false;
|
|
86
|
+
let projectId: string | null = null;
|
|
47
87
|
|
|
48
|
-
async function
|
|
88
|
+
async function getSecretManagerClient(): Promise<{ client: SecretManagerServiceClient; projectId: string } | null> {
|
|
49
89
|
if (secretManagerInitialized) {
|
|
50
|
-
return
|
|
90
|
+
return secretManagerClient && projectId ? { client: secretManagerClient, projectId } : null;
|
|
51
91
|
}
|
|
52
92
|
|
|
53
93
|
try {
|
|
54
|
-
|
|
94
|
+
projectId = getProjectId();
|
|
95
|
+
secretManagerClient = new SecretManagerServiceClient({ projectId });
|
|
55
96
|
secretManagerInitialized = true;
|
|
56
|
-
return
|
|
97
|
+
return { client: secretManagerClient, projectId };
|
|
57
98
|
} catch (error) {
|
|
58
|
-
console.error('[EnvService.ts] Failed to initialize
|
|
99
|
+
console.error('[EnvService.ts] Failed to initialize SecretManagerServiceClient:', (error as Error).message);
|
|
59
100
|
console.warn('[EnvService.ts] Secret Manager functionality will be unavailable.');
|
|
60
101
|
secretManagerInitialized = true; // Prevent retrying
|
|
61
102
|
return null;
|
|
62
103
|
}
|
|
63
104
|
}
|
|
64
105
|
|
|
106
|
+
// Helper functions for secret operations
|
|
107
|
+
function buildSecretName(projectId: string, baseSecretId: string): string {
|
|
108
|
+
return `${projectId}_${baseSecretId}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildSecretPath(projectId: string, secretName: string): string {
|
|
112
|
+
return `projects/${projectId}/secrets/${secretName}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildSecretVersionPath(projectId: string, secretName: string, version: string = 'latest'): string {
|
|
116
|
+
return `projects/${projectId}/secrets/${secretName}/versions/${version}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
65
119
|
const rl = readline.createInterface({
|
|
66
120
|
input: process.stdin,
|
|
67
121
|
output: process.stdout,
|
|
@@ -87,12 +141,29 @@ function obfuscateCred(cred: string | null): string {
|
|
|
87
141
|
|
|
88
142
|
// --- Generic Environment Variable Accessors ---
|
|
89
143
|
async function getEnvVar(baseSecretKey: string): Promise<string | null> {
|
|
90
|
-
const
|
|
91
|
-
if (!
|
|
144
|
+
const smClient = await getSecretManagerClient();
|
|
145
|
+
if (!smClient) {
|
|
92
146
|
console.warn(`[EnvService.ts] SecretManager not available for getEnvVar (key: ${baseSecretKey})`);
|
|
93
147
|
return null;
|
|
94
148
|
}
|
|
95
|
-
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const secretName = buildSecretName(smClient.projectId, baseSecretKey);
|
|
152
|
+
const secretVersionPath = buildSecretVersionPath(smClient.projectId, secretName);
|
|
153
|
+
|
|
154
|
+
const [response] = await smClient.client.accessSecretVersion({
|
|
155
|
+
name: secretVersionPath,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const secretValue = response.payload?.data?.toString();
|
|
159
|
+
return secretValue || null;
|
|
160
|
+
} catch (error: any) {
|
|
161
|
+
if (error.code === 5) { // NOT_FOUND
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
console.warn(`[EnvService.ts] Failed to get secret ${baseSecretKey}: ${error.message}`);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
96
167
|
}
|
|
97
168
|
|
|
98
169
|
export async function getCustomSearchApiKey(): Promise<string> {
|
|
@@ -141,21 +212,67 @@ export async function getGcpProjectId(): Promise<string> {
|
|
|
141
212
|
}
|
|
142
213
|
|
|
143
214
|
async function setEnvVar(baseSecretKey: string, value: string): Promise<boolean> {
|
|
144
|
-
const
|
|
145
|
-
if (!
|
|
215
|
+
const smClient = await getSecretManagerClient();
|
|
216
|
+
if (!smClient) {
|
|
146
217
|
console.warn(`[EnvService.ts] SecretManager not available for setEnvVar (key: ${baseSecretKey})`);
|
|
147
218
|
return false; // Indicate failure
|
|
148
219
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const secretName = buildSecretName(smClient.projectId, baseSecretKey);
|
|
223
|
+
|
|
224
|
+
// Try to add a version first
|
|
225
|
+
const addVersion = async (): Promise<void> => {
|
|
226
|
+
const secretPath = buildSecretPath(smClient.projectId, secretName);
|
|
227
|
+
await smClient.client.addSecretVersion({
|
|
228
|
+
parent: secretPath,
|
|
229
|
+
payload: {
|
|
230
|
+
data: Buffer.from(value, 'utf8'),
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
console.log(`✅ Secret '${baseSecretKey}' saved in GCP project: ${smClient.projectId}`);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
await addVersion();
|
|
238
|
+
return true;
|
|
239
|
+
} catch (err: any) {
|
|
240
|
+
if (err.code === 5) { // NOT_FOUND → create then retry
|
|
241
|
+
// Create the secret resource first
|
|
242
|
+
await smClient.client.createSecret({
|
|
243
|
+
parent: `projects/${smClient.projectId}`,
|
|
244
|
+
secretId: secretName,
|
|
245
|
+
secret: {
|
|
246
|
+
replication: {
|
|
247
|
+
automatic: {},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
console.log(`[EnvService.ts] Created secret resource: ${secretName}`);
|
|
252
|
+
|
|
253
|
+
await addVersion();
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
console.warn(`[EnvService.ts] setEnvVar failed: ${err.message}`);
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
} catch (error: any) {
|
|
260
|
+
console.warn(`[EnvService.ts] Failed to set secret ${baseSecretKey}: ${error.message}`);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
152
263
|
}
|
|
153
264
|
|
|
154
265
|
// --- Specific Setters (Now use generic setEnvVar) ---
|
|
155
266
|
const setLogToConsole = async (): Promise<void> => {
|
|
156
267
|
const logToConsole = await askQuestion('Use consoleLogger to log to console? (yes, no)', 'no');
|
|
157
268
|
if (logToConsole) {
|
|
158
|
-
|
|
269
|
+
// Normalize to lowercase and validate - only 'yes' or 'no' are valid
|
|
270
|
+
const normalizedValue = logToConsole.toLowerCase();
|
|
271
|
+
if (normalizedValue === 'yes' || normalizedValue === 'no') {
|
|
272
|
+
await setEnvVar('LOG_TO_CONSOLE', normalizedValue);
|
|
273
|
+
} else {
|
|
274
|
+
console.warn(`Invalid LOG_TO_CONSOLE value: '${logToConsole}'. Only 'yes' or 'no' are valid.`);
|
|
275
|
+
}
|
|
159
276
|
}
|
|
160
277
|
};
|
|
161
278
|
|
|
@@ -315,12 +432,12 @@ const getProjectRoot = (): string => {
|
|
|
315
432
|
};
|
|
316
433
|
|
|
317
434
|
const _getResolvedGcpProjectId = async (): Promise<string | null> => {
|
|
318
|
-
const
|
|
319
|
-
if (!
|
|
435
|
+
const smClient = await getSecretManagerClient();
|
|
436
|
+
if (!smClient) {
|
|
320
437
|
console.warn('[EnvService.ts] SecretManager not available for _getResolvedGcpProjectId');
|
|
321
438
|
return null;
|
|
322
439
|
}
|
|
323
|
-
return
|
|
440
|
+
return smClient.projectId;
|
|
324
441
|
};
|
|
325
442
|
|
|
326
443
|
const printEnv = async (): Promise<void> => {
|