@dataformer/env-service 2.2.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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @dataformer/env-service
2
2
 
3
+ ## 2.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fix circular dependency with LoggerService, EnvService, and SecretManagerClient
8
+
9
+ ## 2.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - LoggerService upgraded
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies
18
+ - @dataformer/secret-manager-client@2.3.0
19
+
3
20
  ## 2.2.0
4
21
 
5
22
  ### Minor Changes
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 { createSecretManagerClient } from "@dataformer/secret-manager-client";
5
- var secretManager = null;
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
- async function getSecretManager() {
32
+ var projectId = null;
33
+ async function getSecretManagerClient() {
8
34
  if (secretManagerInitialized) {
9
- return secretManager;
35
+ return secretManagerClient && projectId ? { client: secretManagerClient, projectId } : null;
10
36
  }
11
37
  try {
12
- secretManager = await createSecretManagerClient();
38
+ projectId = getProjectId();
39
+ secretManagerClient = new SecretManagerServiceClient({ projectId });
13
40
  secretManagerInitialized = true;
14
- return secretManager;
41
+ return { client: secretManagerClient, projectId };
15
42
  } catch (error) {
16
- console.error("[EnvService.ts] Failed to initialize SecretManagerClient:", error.message);
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 sm = await getSecretManager();
44
- if (!sm) {
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
- return sm.getSecret(baseSecretKey);
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 projectId = execSync("gcloud config get-value project", {
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 (!projectId || projectId === "(unset)") {
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 projectId;
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 sm = await getSecretManager();
87
- if (!sm) {
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
- return sm.setSecret(baseSecretKey, value);
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
- await setEnvVar("LOG_TO_CONSOLE", logToConsole);
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.2.0",
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.2.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
@@ -1,12 +1,16 @@
1
1
  // EnvService.ts - Manages environment variables and credentials using Google Secret Manager.
2
2
  //
3
3
  // HOW TO USE:
4
- // This script is typically invoked via the wrapper `env.js` in the project root.
4
+ // This script can be invoked via the wrapper `env.js` in the project root (after building):
5
5
  // Commands are passed as arguments, e.g.:
6
6
  // node env.js printEnv - Prints all current environment variable values.
7
- // node env.js setApiKey - Prompts to set the API key.
7
+ // node env.js setGeminiApiKey - Prompts to set the Gemini API key.
8
+ // node env.js setPostgresUser - Prompts to set the PostgreSQL user.
8
9
  // ... and so on for other specific setters listed in the `run` function's switch statement.
9
10
  //
11
+ // Alternatively, you can run it directly from the compiled output:
12
+ // node packages/services/env-service/dist/index.js printEnv
13
+ //
10
14
  // GCLOUD AUTHENTICATION REQUIREMENT:
11
15
  // This script requires you to be authenticated with Google Cloud CLI (`gcloud`).
12
16
  // The credentials are used by the underlying SecretManagerClient to access Google Secret Manager.
@@ -35,29 +39,83 @@
35
39
 
36
40
  import readline from 'readline';
37
41
  import path from 'path';
38
- import { createSecretManagerClient, SecretManagerClient } from '@dataformer/secret-manager-client';
42
+ import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
43
+ import { execSync } from 'child_process';
39
44
 
40
- // SecretManager client will be initialized lazily on first use
41
- let secretManager: SecretManagerClient | null = null;
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;
42
85
  let secretManagerInitialized = false;
86
+ let projectId: string | null = null;
43
87
 
44
- async function getSecretManager(): Promise<SecretManagerClient | null> {
88
+ async function getSecretManagerClient(): Promise<{ client: SecretManagerServiceClient; projectId: string } | null> {
45
89
  if (secretManagerInitialized) {
46
- return secretManager;
90
+ return secretManagerClient && projectId ? { client: secretManagerClient, projectId } : null;
47
91
  }
48
92
 
49
93
  try {
50
- secretManager = await createSecretManagerClient();
94
+ projectId = getProjectId();
95
+ secretManagerClient = new SecretManagerServiceClient({ projectId });
51
96
  secretManagerInitialized = true;
52
- return secretManager;
97
+ return { client: secretManagerClient, projectId };
53
98
  } catch (error) {
54
- console.error('[EnvService.ts] Failed to initialize SecretManagerClient:', (error as Error).message);
99
+ console.error('[EnvService.ts] Failed to initialize SecretManagerServiceClient:', (error as Error).message);
55
100
  console.warn('[EnvService.ts] Secret Manager functionality will be unavailable.');
56
101
  secretManagerInitialized = true; // Prevent retrying
57
102
  return null;
58
103
  }
59
104
  }
60
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
+
61
119
  const rl = readline.createInterface({
62
120
  input: process.stdin,
63
121
  output: process.stdout,
@@ -83,12 +141,29 @@ function obfuscateCred(cred: string | null): string {
83
141
 
84
142
  // --- Generic Environment Variable Accessors ---
85
143
  async function getEnvVar(baseSecretKey: string): Promise<string | null> {
86
- const sm = await getSecretManager();
87
- if (!sm) {
144
+ const smClient = await getSecretManagerClient();
145
+ if (!smClient) {
88
146
  console.warn(`[EnvService.ts] SecretManager not available for getEnvVar (key: ${baseSecretKey})`);
89
147
  return null;
90
148
  }
91
- return sm.getSecret(baseSecretKey);
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
+ }
92
167
  }
93
168
 
94
169
  export async function getCustomSearchApiKey(): Promise<string> {
@@ -137,21 +212,67 @@ export async function getGcpProjectId(): Promise<string> {
137
212
  }
138
213
 
139
214
  async function setEnvVar(baseSecretKey: string, value: string): Promise<boolean> {
140
- const sm = await getSecretManager();
141
- if (!sm) {
215
+ const smClient = await getSecretManagerClient();
216
+ if (!smClient) {
142
217
  console.warn(`[EnvService.ts] SecretManager not available for setEnvVar (key: ${baseSecretKey})`);
143
218
  return false; // Indicate failure
144
219
  }
145
- // TODO: need to get change iamPolicy permissions
146
- // const currentUserOnly = true;
147
- return sm.setSecret(baseSecretKey, value);
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
+ }
148
263
  }
149
264
 
150
265
  // --- Specific Setters (Now use generic setEnvVar) ---
151
266
  const setLogToConsole = async (): Promise<void> => {
152
267
  const logToConsole = await askQuestion('Use consoleLogger to log to console? (yes, no)', 'no');
153
268
  if (logToConsole) {
154
- await setEnvVar('LOG_TO_CONSOLE', logToConsole);
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
+ }
155
276
  }
156
277
  };
157
278
 
@@ -311,12 +432,12 @@ const getProjectRoot = (): string => {
311
432
  };
312
433
 
313
434
  const _getResolvedGcpProjectId = async (): Promise<string | null> => {
314
- const sm = await getSecretManager();
315
- if (!sm) {
435
+ const smClient = await getSecretManagerClient();
436
+ if (!smClient) {
316
437
  console.warn('[EnvService.ts] SecretManager not available for _getResolvedGcpProjectId');
317
438
  return null;
318
439
  }
319
- return sm.getResolvedProjectId();
440
+ return smClient.projectId;
320
441
  };
321
442
 
322
443
  const printEnv = async (): Promise<void> => {