@purpleschool/infisical-env 0.1.0 → 0.1.2

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.
Files changed (2) hide show
  1. package/package.json +5 -2
  2. package/src/cli.js +267 -267
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@purpleschool/infisical-env",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "bin": {
6
- "purple-env": "./src/cli.js"
6
+ "infisical-env": "src/cli.js"
7
7
  },
8
+ "files": [
9
+ "src/cli.js"
10
+ ],
8
11
  "scripts": {
9
12
  "start": "node ./src/cli.js"
10
13
  },
package/src/cli.js CHANGED
@@ -16,348 +16,348 @@ const DEFAULT_INFISICAL_BASE_URL = "https://secret.purplecode.ru";
16
16
  const DEFAULT_INFISICAL_CLIENT_ID = "f3e0d60c-a602-4296-bd03-3be228e89165";
17
17
 
18
18
  async function main() {
19
- try {
20
- const args = process.argv.slice(2);
21
- if (args.includes("--help") || args.includes("-h")) {
22
- printHelp();
23
- return;
24
- }
25
-
26
- const repoRoot = findRepoRoot(process.cwd());
27
- const packageJson = readJson(path.join(repoRoot, "package.json"));
28
-
29
- const projectId = resolveInfisicalProject(packageJson);
30
- const envName = await resolveEnvName(args);
19
+ try {
20
+ const args = process.argv.slice(2);
21
+ if (args.includes("--help") || args.includes("-h")) {
22
+ printHelp();
23
+ return;
24
+ }
31
25
 
32
- const deploymentPath = resolveDeploymentPath(repoRoot);
33
- const envRefs = collectEnvRefs(deploymentPath);
26
+ const repoRoot = findRepoRoot(process.cwd());
27
+ const packageJson = readJson(path.join(repoRoot, "package.json"));
34
28
 
35
- if (envRefs.length === 0) {
36
- throw new Error(`No env refs found in ${deploymentPath}`);
37
- }
29
+ const projectId = resolveInfisicalProject(packageJson);
30
+ const envName = await resolveEnvName(args);
38
31
 
39
- const values = await resolveEnvValues({
40
- projectId,
41
- envName,
42
- envRefs,
43
- repoRoot,
44
- });
32
+ const deploymentPath = resolveDeploymentPath(repoRoot);
33
+ const envRefs = collectEnvRefs(deploymentPath);
45
34
 
46
- const envFilePath = path.join(repoRoot, ".env");
47
- const lines = values.map((item) => `${item.name}=${item.value}`);
48
- fs.writeFileSync(envFilePath, lines.join(os.EOL), "utf8");
35
+ if (envRefs.length === 0) {
36
+ throw new Error(`No env refs found in ${deploymentPath}`);
37
+ }
49
38
 
50
- console.log(pc.green(`.env written to ${envFilePath}`));
51
- } catch (err) {
52
- console.error(pc.red(formatError(err)));
53
- console.error(pc.yellow("Run with --help for usage."));
54
- process.exit(1);
55
- }
39
+ const values = await resolveEnvValues({
40
+ projectId,
41
+ envName,
42
+ envRefs,
43
+ repoRoot,
44
+ });
45
+
46
+ const envFilePath = path.join(repoRoot, ".env");
47
+ const lines = values.map((item) => `${item.name}=${item.value}`);
48
+ fs.writeFileSync(envFilePath, lines.join(os.EOL), "utf8");
49
+
50
+ console.log(pc.green(`.env written to ${envFilePath}`));
51
+ } catch (err) {
52
+ console.error(pc.red(formatError(err)));
53
+ console.error(pc.yellow("Run with --help for usage."));
54
+ process.exit(1);
55
+ }
56
56
  }
57
57
 
58
58
  function printHelp() {
59
- console.log(
60
- [
61
- pc.bold("Usage:"),
62
- " purple-env [--env stage|stage-2]",
63
- "",
64
- pc.bold("Options:"),
65
- " --env <name> Environment name (stage or stage-2)",
66
- " -h, --help Show this help",
67
- "",
68
- pc.bold("Environment variables:"),
69
- " INFISICAL_CLIENT_SECRET Required (machine identity secret)",
70
- " INFISICAL_CLIENT_ID Optional override",
71
- " INFISICAL_BASE_URL Optional override (default https://secret.purplecode.ru)",
72
- " INFISICAL_MOCK_PATH Optional mock json path for offline testing",
73
- ].join(os.EOL)
74
- );
59
+ console.log(
60
+ [
61
+ pc.bold("Usage:"),
62
+ " infisical-env [--env stage|stage-2]",
63
+ "",
64
+ pc.bold("Options:"),
65
+ " --env <name> Environment name (stage or stage-2)",
66
+ " -h, --help Show this help",
67
+ "",
68
+ pc.bold("Environment variables:"),
69
+ " INFISICAL_CLIENT_SECRET Required (machine identity secret)",
70
+ " INFISICAL_CLIENT_ID Optional override",
71
+ " INFISICAL_BASE_URL Optional override (default https://secret.purplecode.ru)",
72
+ " INFISICAL_MOCK_PATH Optional mock json path for offline testing",
73
+ ].join(os.EOL)
74
+ );
75
75
  }
76
76
 
77
77
  function findRepoRoot(startDir) {
78
- let current = startDir;
79
- while (true) {
80
- const candidate = path.join(current, "package.json");
81
- if (fs.existsSync(candidate)) {
82
- return current;
83
- }
84
- const parent = path.dirname(current);
85
- if (parent === current) {
86
- throw new Error("package.json not found in any parent directory");
78
+ let current = startDir;
79
+ while (true) {
80
+ const candidate = path.join(current, "package.json");
81
+ if (fs.existsSync(candidate)) {
82
+ return current;
83
+ }
84
+ const parent = path.dirname(current);
85
+ if (parent === current) {
86
+ throw new Error("package.json not found in any parent directory");
87
+ }
88
+ current = parent;
87
89
  }
88
- current = parent;
89
- }
90
90
  }
91
91
 
92
92
  function readJson(filePath) {
93
- const raw = fs.readFileSync(filePath, "utf8");
94
- return JSON.parse(raw);
93
+ const raw = fs.readFileSync(filePath, "utf8");
94
+ return JSON.parse(raw);
95
95
  }
96
96
 
97
97
  function resolveInfisicalProject(packageJson) {
98
- const name = typeof packageJson.name === "string" ? packageJson.name.toLowerCase() : "";
99
- if (name.includes("rugpt")) {
100
- return "48854e39-7129-4786-87ac-cfcce296991c";
101
- }
102
- if (name.includes("studdy")) {
103
- return "b4b0abbd-f8e6-428a-a296-6289369ca8dd";
104
- }
105
- if (name.includes("multisite")) {
106
- return "6531c08b-45e0-4d75-a8ab-08d14d0387bf";
107
- }
108
- throw new Error(
109
- "Unable to resolve Infisical project from package.json name. Expected name to include rugpt, studdy, or multisite."
110
- );
98
+ const name = typeof packageJson.name === "string" ? packageJson.name.toLowerCase() : "";
99
+ if (name.includes("rugpt")) {
100
+ return "48854e39-7129-4786-87ac-cfcce296991c";
101
+ }
102
+ if (name.includes("studdy") || name.includes('student-works')) {
103
+ return "b4b0abbd-f8e6-428a-a296-6289369ca8dd";
104
+ }
105
+ if (name.includes("multisite")) {
106
+ return "6531c08b-45e0-4d75-a8ab-08d14d0387bf";
107
+ }
108
+ throw new Error(
109
+ "Unable to resolve Infisical project from package.json name. Expected name to include rugpt, studdy, or multisite."
110
+ );
111
111
  }
112
112
 
113
113
  async function resolveEnvName(args) {
114
- const flagValue = parseEnvFlag(args);
115
- if (flagValue) {
116
- return validateEnv(flagValue);
117
- }
118
- return promptEnv();
114
+ const flagValue = parseEnvFlag(args);
115
+ if (flagValue) {
116
+ return validateEnv(flagValue);
117
+ }
118
+ return promptEnv();
119
119
  }
120
120
 
121
121
  function parseEnvFlag(args) {
122
- for (let i = 0; i < args.length; i += 1) {
123
- const arg = args[i];
124
- if (arg === "--env") {
125
- return args[i + 1] || "";
126
- }
127
- if (arg.startsWith("--env=")) {
128
- return arg.slice("--env=".length);
122
+ for (let i = 0; i < args.length; i += 1) {
123
+ const arg = args[i];
124
+ if (arg === "--env") {
125
+ return args[i + 1] || "";
126
+ }
127
+ if (arg.startsWith("--env=")) {
128
+ return arg.slice("--env=".length);
129
+ }
129
130
  }
130
- }
131
- return "";
131
+ return "";
132
132
  }
133
133
 
134
134
  function validateEnv(value) {
135
- if (!ALLOWED_ENVS.includes(value)) {
136
- throw new Error(`Invalid environment \"${value}\". Allowed: ${ALLOWED_ENVS.join(", ")}`);
137
- }
138
- return value;
135
+ if (!ALLOWED_ENVS.includes(value)) {
136
+ throw new Error(`Invalid environment \"${value}\". Allowed: ${ALLOWED_ENVS.join(", ")}`);
137
+ }
138
+ return value;
139
139
  }
140
140
 
141
141
  async function promptEnv() {
142
- const rl = readline.createInterface({
143
- input: process.stdin,
144
- output: process.stdout,
145
- });
146
-
147
- try {
148
- console.log(pc.bold("Select environment:"));
149
- ALLOWED_ENVS.forEach((name, idx) => {
150
- console.log(`${idx + 1}. ${name}`);
142
+ const rl = readline.createInterface({
143
+ input: process.stdin,
144
+ output: process.stdout,
151
145
  });
152
- const answer = await rl.question("Enter number: ");
153
- const index = Number(answer.trim()) - 1;
154
- if (!Number.isInteger(index) || index < 0 || index >= ALLOWED_ENVS.length) {
155
- throw new Error("Invalid environment selection");
146
+
147
+ try {
148
+ console.log(pc.bold("Select environment:"));
149
+ ALLOWED_ENVS.forEach((name, idx) => {
150
+ console.log(`${idx + 1}. ${name}`);
151
+ });
152
+ const answer = await rl.question("Enter number: ");
153
+ const index = Number(answer.trim()) - 1;
154
+ if (!Number.isInteger(index) || index < 0 || index >= ALLOWED_ENVS.length) {
155
+ throw new Error("Invalid environment selection");
156
+ }
157
+ return ALLOWED_ENVS[index];
158
+ } finally {
159
+ rl.close();
156
160
  }
157
- return ALLOWED_ENVS[index];
158
- } finally {
159
- rl.close();
160
- }
161
161
  }
162
162
 
163
163
  function resolveDeploymentPath(repoRoot) {
164
- const candidates = [
165
- path.join(repoRoot, ".kube", "deployment.yml"),
166
- path.join(repoRoot, ".kube", "deployment.yaml"),
167
- path.join(repoRoot, "deployment.yml"),
168
- path.join(repoRoot, "deployment.yaml"),
169
- ];
170
-
171
- for (const candidate of candidates) {
172
- if (fs.existsSync(candidate)) {
173
- return candidate;
164
+ const candidates = [
165
+ path.join(repoRoot, ".kube", "deployment.yml"),
166
+ path.join(repoRoot, ".kube", "deployment.yaml"),
167
+ path.join(repoRoot, "deployment.yml"),
168
+ path.join(repoRoot, "deployment.yaml"),
169
+ ];
170
+
171
+ for (const candidate of candidates) {
172
+ if (fs.existsSync(candidate)) {
173
+ return candidate;
174
+ }
174
175
  }
175
- }
176
176
 
177
- throw new Error("Deployment file not found. Expected .kube/deployment.yml or .kube/deployment.yaml");
177
+ throw new Error("Deployment file not found. Expected .kube/deployment.yml or .kube/deployment.yaml");
178
178
  }
179
179
 
180
180
  function collectEnvRefs(deploymentPath) {
181
- const raw = fs.readFileSync(deploymentPath, "utf8");
182
- const docs = YAML.parseAllDocuments(raw);
183
- const refs = [];
184
-
185
- for (const doc of docs) {
186
- const data = doc.toJSON();
187
- const containers = data?.spec?.template?.spec?.containers || [];
188
- for (const container of containers) {
189
- const envList = container?.env || [];
190
- for (const envItem of envList) {
191
- const name = envItem?.name;
192
- const secretKeyRef = envItem?.valueFrom?.secretKeyRef;
193
- const folder = secretKeyRef?.name;
194
- const key = secretKeyRef?.key;
195
- if (name && folder && key) {
196
- refs.push({ name, folder, key });
181
+ const raw = fs.readFileSync(deploymentPath, "utf8");
182
+ const docs = YAML.parseAllDocuments(raw);
183
+ const refs = [];
184
+
185
+ for (const doc of docs) {
186
+ const data = doc.toJSON();
187
+ const containers = data?.spec?.template?.spec?.containers || [];
188
+ for (const container of containers) {
189
+ const envList = container?.env || [];
190
+ for (const envItem of envList) {
191
+ const name = envItem?.name;
192
+ const secretKeyRef = envItem?.valueFrom?.secretKeyRef;
193
+ const folder = secretKeyRef?.name;
194
+ const key = secretKeyRef?.key;
195
+ if (name && folder && key) {
196
+ refs.push({ name, folder, key });
197
+ }
198
+ }
197
199
  }
198
- }
199
200
  }
200
- }
201
201
 
202
- return refs;
202
+ return refs;
203
203
  }
204
204
 
205
205
  async function resolveEnvValues({ projectId, envName, envRefs, repoRoot }) {
206
- const mockPath = process.env.INFISICAL_MOCK_PATH;
207
- if (mockPath) {
208
- return resolveFromMock({ projectId, envName, envRefs, mockPath, repoRoot });
209
- }
210
-
211
- const clientId = process.env.INFISICAL_CLIENT_ID || DEFAULT_INFISICAL_CLIENT_ID;
212
- const clientSecret = process.env.INFISICAL_CLIENT_SECRET;
213
- if (!clientId || !clientSecret) {
214
- throw new Error(
215
- "Missing Infisical credentials. Set INFISICAL_CLIENT_SECRET or use INFISICAL_MOCK_PATH."
216
- );
217
- }
206
+ const mockPath = process.env.INFISICAL_MOCK_PATH;
207
+ if (mockPath) {
208
+ return resolveFromMock({ projectId, envName, envRefs, mockPath, repoRoot });
209
+ }
218
210
 
219
- const baseUrl = normalizeBaseUrl(process.env.INFISICAL_BASE_URL || DEFAULT_INFISICAL_BASE_URL);
220
- const accessToken = await loginUniversalAuth({ baseUrl, clientId, clientSecret });
211
+ const clientId = process.env.INFISICAL_CLIENT_ID || DEFAULT_INFISICAL_CLIENT_ID;
212
+ const clientSecret = process.env.INFISICAL_CLIENT_SECRET;
213
+ if (!clientId || !clientSecret) {
214
+ throw new Error(
215
+ "Missing Infisical credentials. Set INFISICAL_CLIENT_SECRET or use INFISICAL_MOCK_PATH."
216
+ );
217
+ }
218
+
219
+ const baseUrl = normalizeBaseUrl(process.env.INFISICAL_BASE_URL || DEFAULT_INFISICAL_BASE_URL);
220
+ const accessToken = await loginUniversalAuth({ baseUrl, clientId, clientSecret });
221
221
 
222
- return fetchSecretsFromInfisical({ baseUrl, accessToken, projectId, envName, envRefs });
222
+ return fetchSecretsFromInfisical({ baseUrl, accessToken, projectId, envName, envRefs });
223
223
  }
224
224
 
225
225
  function resolveFromMock({ projectId, envName, envRefs, mockPath, repoRoot }) {
226
- const absPath = path.isAbsolute(mockPath) ? mockPath : path.join(repoRoot, mockPath);
227
- if (!fs.existsSync(absPath)) {
228
- throw new Error(`Mock file not found: ${absPath}`);
229
- }
230
- const data = readJson(absPath);
231
- const projectData = data?.projects?.[projectId];
232
- if (!projectData) {
233
- throw new Error(`Project not found in mock: ${projectId}`);
234
- }
235
- const envData = projectData?.[envName];
236
- if (!envData) {
237
- throw new Error(`Environment not found in mock: ${envName}`);
238
- }
239
-
240
- return envRefs.map((ref) => {
241
- const folderData = envData?.[ref.folder];
242
- const value = folderData?.[ref.key];
243
- if (value === undefined || value === null) {
244
- throw new Error(`Missing value for ${ref.folder}.${ref.key}`);
226
+ const absPath = path.isAbsolute(mockPath) ? mockPath : path.join(repoRoot, mockPath);
227
+ if (!fs.existsSync(absPath)) {
228
+ throw new Error(`Mock file not found: ${absPath}`);
229
+ }
230
+ const data = readJson(absPath);
231
+ const projectData = data?.projects?.[projectId];
232
+ if (!projectData) {
233
+ throw new Error(`Project not found in mock: ${projectId}`);
234
+ }
235
+ const envData = projectData?.[envName];
236
+ if (!envData) {
237
+ throw new Error(`Environment not found in mock: ${envName}`);
245
238
  }
246
- return { name: ref.name, value: String(value) };
247
- });
239
+
240
+ return envRefs.map((ref) => {
241
+ const folderData = envData?.[ref.folder];
242
+ const value = folderData?.[ref.key];
243
+ if (value === undefined || value === null) {
244
+ throw new Error(`Missing value for ${ref.folder}.${ref.key}`);
245
+ }
246
+ return { name: ref.name, value: String(value) };
247
+ });
248
248
  }
249
249
 
250
250
  function formatError(err) {
251
- if (err instanceof Error) {
252
- return err.message;
253
- }
254
- return String(err);
251
+ if (err instanceof Error) {
252
+ return err.message;
253
+ }
254
+ return String(err);
255
255
  }
256
256
 
257
257
  function normalizeBaseUrl(value) {
258
- return value.replace(/\/+$/, "");
258
+ return value.replace(/\/+$/, "");
259
259
  }
260
260
 
261
261
  async function loginUniversalAuth({ baseUrl, clientId, clientSecret }) {
262
- const url = `${baseUrl}/api/v1/auth/universal-auth/login`;
263
- const body = new URLSearchParams();
264
- body.set("clientId", clientId);
265
- body.set("clientSecret", clientSecret);
266
-
267
- const res = await fetch(url, {
268
- method: "POST",
269
- headers: {
270
- "Content-Type": "application/x-www-form-urlencoded",
271
- },
272
- body,
273
- });
274
-
275
- if (!res.ok) {
276
- const text = await safeReadText(res);
277
- throw new Error(`Infisical login failed (${res.status}). ${text}`);
278
- }
279
-
280
- const data = await res.json();
281
- if (!data?.accessToken) {
282
- throw new Error("Infisical login response missing accessToken.");
283
- }
284
- return data.accessToken;
262
+ const url = `${baseUrl}/api/v1/auth/universal-auth/login`;
263
+ const body = new URLSearchParams();
264
+ body.set("clientId", clientId);
265
+ body.set("clientSecret", clientSecret);
266
+
267
+ const res = await fetch(url, {
268
+ method: "POST",
269
+ headers: {
270
+ "Content-Type": "application/x-www-form-urlencoded",
271
+ },
272
+ body,
273
+ });
274
+
275
+ if (!res.ok) {
276
+ const text = await safeReadText(res);
277
+ throw new Error(`Infisical login failed (${res.status}). ${text}`);
278
+ }
279
+
280
+ const data = await res.json();
281
+ if (!data?.accessToken) {
282
+ throw new Error("Infisical login response missing accessToken.");
283
+ }
284
+ return data.accessToken;
285
285
  }
286
286
 
287
287
  async function fetchSecretsFromInfisical({ baseUrl, accessToken, projectId, envName, envRefs }) {
288
- const refsByFolder = new Map();
289
- for (const ref of envRefs) {
290
- if (!refsByFolder.has(ref.folder)) {
291
- refsByFolder.set(ref.folder, []);
292
- }
293
- refsByFolder.get(ref.folder).push(ref);
294
- }
295
-
296
- const secretCache = new Map();
297
- for (const [folder, refs] of refsByFolder.entries()) {
298
- const secretPath = folder.startsWith("/") ? folder : `/${folder}`;
299
- const secrets = await listSecrets({
300
- baseUrl,
301
- accessToken,
302
- projectId,
303
- envName,
304
- secretPath,
305
- });
306
- const map = new Map();
307
- for (const item of secrets) {
308
- if (item?.secretKey != null) {
309
- map.set(item.secretKey, item.secretValue);
310
- }
288
+ const refsByFolder = new Map();
289
+ for (const ref of envRefs) {
290
+ if (!refsByFolder.has(ref.folder)) {
291
+ refsByFolder.set(ref.folder, []);
292
+ }
293
+ refsByFolder.get(ref.folder).push(ref);
311
294
  }
312
- secretCache.set(folder, map);
313
295
 
314
- for (const ref of refs) {
315
- if (!map.has(ref.key)) {
316
- throw new Error(`Missing value for ${folder}.${ref.key} in environment ${envName}`);
317
- }
296
+ const secretCache = new Map();
297
+ for (const [folder, refs] of refsByFolder.entries()) {
298
+ const secretPath = folder.startsWith("/") ? folder : `/${folder}`;
299
+ const secrets = await listSecrets({
300
+ baseUrl,
301
+ accessToken,
302
+ projectId,
303
+ envName,
304
+ secretPath,
305
+ });
306
+ const map = new Map();
307
+ for (const item of secrets) {
308
+ if (item?.secretKey != null) {
309
+ map.set(item.secretKey, item.secretValue);
310
+ }
311
+ }
312
+ secretCache.set(folder, map);
313
+
314
+ for (const ref of refs) {
315
+ if (!map.has(ref.key)) {
316
+ throw new Error(`Missing value for ${folder}.${ref.key} in environment ${envName}`);
317
+ }
318
+ }
318
319
  }
319
- }
320
320
 
321
- return envRefs.map((ref) => {
322
- const folderMap = secretCache.get(ref.folder);
323
- const value = folderMap?.get(ref.key);
324
- return { name: ref.name, value: String(value) };
325
- });
321
+ return envRefs.map((ref) => {
322
+ const folderMap = secretCache.get(ref.folder);
323
+ const value = folderMap?.get(ref.key);
324
+ return { name: ref.name, value: String(value) };
325
+ });
326
326
  }
327
327
 
328
328
  async function listSecrets({ baseUrl, accessToken, projectId, envName, secretPath }) {
329
- const params = new URLSearchParams({
330
- projectId,
331
- environment: envName,
332
- secretPath,
333
- viewSecretValue: "true",
334
- expandSecretReferences: "true",
335
- recursive: "false",
336
- includeImports: "true",
337
- });
338
-
339
- const url = `${baseUrl}/api/v4/secrets?${params.toString()}`;
340
- const res = await fetch(url, {
341
- headers: {
342
- Authorization: `Bearer ${accessToken}`,
343
- },
344
- });
345
-
346
- if (!res.ok) {
347
- const text = await safeReadText(res);
348
- throw new Error(`Infisical secrets list failed (${res.status}) for ${secretPath}. ${text}`);
349
- }
350
-
351
- const data = await res.json();
352
- return Array.isArray(data?.secrets) ? data.secrets : [];
329
+ const params = new URLSearchParams({
330
+ projectId,
331
+ environment: envName,
332
+ secretPath,
333
+ viewSecretValue: "true",
334
+ expandSecretReferences: "true",
335
+ recursive: "false",
336
+ includeImports: "true",
337
+ });
338
+
339
+ const url = `${baseUrl}/api/v4/secrets?${params.toString()}`;
340
+ const res = await fetch(url, {
341
+ headers: {
342
+ Authorization: `Bearer ${accessToken}`,
343
+ },
344
+ });
345
+
346
+ if (!res.ok) {
347
+ const text = await safeReadText(res);
348
+ throw new Error(`Infisical secrets list failed (${res.status}) for ${secretPath}. ${text}`);
349
+ }
350
+
351
+ const data = await res.json();
352
+ return Array.isArray(data?.secrets) ? data.secrets : [];
353
353
  }
354
354
 
355
355
  async function safeReadText(res) {
356
- try {
357
- return await res.text();
358
- } catch {
359
- return "";
360
- }
356
+ try {
357
+ return await res.text();
358
+ } catch {
359
+ return "";
360
+ }
361
361
  }
362
362
 
363
363
  await main();