@dotenc/cli 0.3.4 → 0.4.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dotenc/cli",
3
- "version": "0.3.4",
4
- "description": "🔐 Secure, encrypted environment variables that live in your codebase",
3
+ "version": "0.4.2",
4
+ "description": "🔐 Git-native encrypted environments powered by your SSH keys",
5
5
  "author": "Ivan Filho <i@ivanfilho.com>",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -11,18 +11,29 @@
11
11
  "files": [
12
12
  "dist"
13
13
  ],
14
+ "scripts": {
15
+ "dev": "bun src/cli.ts",
16
+ "start": "node dist/cli.js",
17
+ "build": "bun build src/cli.ts --outdir dist --target node --packages external && { echo '#!/usr/bin/env node'; cat dist/cli.js; } > dist/cli.tmp && mv dist/cli.tmp dist/cli.js",
18
+ "build:binary": "bun run build:binary:darwin-arm64 && bun run build:binary:darwin-x64 && bun run build:binary:linux-x64 && bun run build:binary:linux-arm64 && bun run build:binary:windows-x64",
19
+ "build:binary:darwin-arm64": "bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/dotenc-darwin-arm64",
20
+ "build:binary:darwin-x64": "bun build src/cli.ts --compile --target=bun-darwin-x64 --outfile dist/dotenc-darwin-x64",
21
+ "build:binary:linux-x64": "bun build src/cli.ts --compile --target=bun-linux-x64 --outfile dist/dotenc-linux-x64",
22
+ "build:binary:linux-arm64": "bun build src/cli.ts --compile --target=bun-linux-arm64 --outfile dist/dotenc-linux-arm64",
23
+ "build:binary:windows-x64": "bun build src/cli.ts --compile --target=bun-windows-x64 --outfile dist/dotenc-windows-x64",
24
+ "test": "bun test",
25
+ "test:e2e": "docker build -t dotenc-e2e -f e2e/Dockerfile .. && docker run --rm dotenc-e2e"
26
+ },
14
27
  "devDependencies": {
15
- "@biomejs/biome": "^1.9.4",
16
- "@types/node": "^22.13.7",
17
- "tsc-alias": "^1.8.11",
18
- "tsx": "^4.19.3",
19
- "typescript": "^5.8.2",
20
- "vitest": "^3.0.9"
28
+ "@biomejs/biome": "^2.1.2",
29
+ "@types/bun": "^1.3.9",
30
+ "typescript": "^5.8.2"
21
31
  },
22
32
  "dependencies": {
23
33
  "@paralleldrive/cuid2": "^2.2.2",
24
34
  "chalk": "^5.4.1",
25
35
  "commander": "^13.1.0",
36
+ "eciesjs": "^0.4.15",
26
37
  "inquirer": "^12.4.2",
27
38
  "zod": "^3.24.2"
28
39
  },
@@ -35,26 +46,25 @@
35
46
  },
36
47
  "homepage": "https://github.com/ivanfilhoz/dotenc#readme",
37
48
  "keywords": [
49
+ "dotenv",
50
+ "env",
38
51
  "environment",
39
52
  "variables",
40
- "safe",
41
- "secure",
42
- "codebase",
43
- "cli",
44
- "command",
45
- "line",
46
- "tool",
47
- "utility",
48
- "env",
49
- "box",
50
- "dotenv",
53
+ "secrets",
51
54
  "encrypted",
52
- "codebase"
53
- ],
54
- "scripts": {
55
- "dev": "tsx src/cli.ts",
56
- "start": "node dist/cli.js",
57
- "build": "tsc && tsc-alias",
58
- "test": "vitest"
59
- }
60
- }
55
+ "encryption",
56
+ "aes-256-gcm",
57
+ "ssh",
58
+ "ssh-keys",
59
+ "ed25519",
60
+ "rsa",
61
+ "git",
62
+ "cli",
63
+ "secure",
64
+ "security",
65
+ "devops",
66
+ "cicd",
67
+ "configuration",
68
+ "config"
69
+ ]
70
+ }
@@ -1,15 +0,0 @@
1
- import { getHomeConfig, setHomeConfig } from "../helpers/homeConfig.js";
2
- export const configCommand = async (key, value, options) => {
3
- const config = await getHomeConfig();
4
- if (options.remove) {
5
- delete config[key];
6
- await setHomeConfig(config);
7
- return;
8
- }
9
- if (value) {
10
- config[key] = value;
11
- await setHomeConfig(config);
12
- return;
13
- }
14
- console.log(config[key]);
15
- };
@@ -1,16 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { decrypt, encrypt } from "../helpers/crypto.js";
6
- export const debugCommand = async () => {
7
- const key = crypto.randomBytes(32).toString("base64");
8
- const encryptedFilePath = path.join(os.tmpdir(), "dotenc.enc");
9
- await encrypt(key, "Test", encryptedFilePath);
10
- const content = await decrypt(key, encryptedFilePath);
11
- await fs.unlink(encryptedFilePath);
12
- if (content !== "Test") {
13
- throw new Error("Decrypted content is not equal to the original content");
14
- }
15
- console.log("Decryption successful");
16
- };
@@ -1,51 +0,0 @@
1
- import chalk from "chalk";
2
- import { execSync } from "node:child_process";
3
- import { existsSync } from "node:fs";
4
- import fs from "node:fs/promises";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import { createHash } from "../helpers/createHash.js";
8
- import { decrypt, encrypt } from "../helpers/crypto.js";
9
- import { getDefaultEditor } from "../helpers/getDefaultEditor.js";
10
- import { getKey } from "../helpers/key.js";
11
- import { chooseEnvironmentPrompt } from "../prompts/chooseEnvironment.js";
12
- export const editCommand = async (environmentArg) => {
13
- let environment = environmentArg;
14
- if (!environment) {
15
- environment = await chooseEnvironmentPrompt("What environment do you want to edit?");
16
- }
17
- const environmentFile = `.env.${environment}.enc`;
18
- const environmentFilePath = path.join(process.cwd(), environmentFile);
19
- if (!existsSync(environmentFilePath)) {
20
- console.error(`Environment file not found: ${environmentFilePath}`);
21
- return;
22
- }
23
- const key = await getKey(environment);
24
- if (!key) {
25
- console.error(`\n${chalk.red("Error:")} no key found for the ${chalk.cyan(environment)} environment.`);
26
- return;
27
- }
28
- const tempFilePath = path.join(os.tmpdir(), `.env.${environment}`);
29
- const content = await decrypt(key, environmentFilePath);
30
- await fs.writeFile(tempFilePath, content);
31
- const initialHash = createHash(content);
32
- const editor = await getDefaultEditor();
33
- try {
34
- // This will block until the editor process is closed
35
- execSync(`${editor} ${tempFilePath}`, { stdio: "inherit" });
36
- }
37
- catch (error) {
38
- console.error(`\nFailed to open editor: ${editor}`);
39
- return;
40
- }
41
- const newContent = await fs.readFile(tempFilePath, "utf-8");
42
- const finalHash = createHash(newContent);
43
- if (initialHash === finalHash) {
44
- console.log(`\nNo changes were made to the ${chalk.cyan(environment)} environment.`);
45
- }
46
- else {
47
- await encrypt(key, newContent, environmentFilePath);
48
- console.log(`\nEncrypted ${chalk.cyan(environment)} environment and saved it to ${chalk.gray(environmentFile)}.`);
49
- }
50
- await fs.unlink(tempFilePath);
51
- };
@@ -1,44 +0,0 @@
1
- import chalk from "chalk";
2
- import crypto from "node:crypto";
3
- import { createEnvironment } from "../helpers/createEnvironment.js";
4
- import { createLocalEnvironment } from "../helpers/createLocalEnvironment.js";
5
- import { createProject } from "../helpers/createProject.js";
6
- import { environmentExists } from "../helpers/environmentExists.js";
7
- import { getEnvironmentNameSuggestion } from "../helpers/getEnvironmentNameSuggestion.js";
8
- import { addKey } from "../helpers/key.js";
9
- import { createEnvironmentPrompt } from "../prompts/createEnvironment.js";
10
- export const initCommand = async (environmentArg) => {
11
- // Generate a unique project ID
12
- const { projectId } = await createProject();
13
- // Setup local environment
14
- await createLocalEnvironment();
15
- // Generate a random key
16
- const key = crypto.randomBytes(32).toString("base64");
17
- // Prompt for the environment name
18
- let environment = environmentArg;
19
- if (!environment) {
20
- environment = await createEnvironmentPrompt("What should the environment be named?", getEnvironmentNameSuggestion());
21
- }
22
- if (!environment) {
23
- console.log(`${chalk.red("Error:")} no environment name provided`);
24
- return;
25
- }
26
- if (environmentExists(environment)) {
27
- console.log(`${chalk.red("Error:")} environment ${environment} already exists. To edit it, use ${chalk.gray(`dotenc edit ${environment}`)}`);
28
- return;
29
- }
30
- await createEnvironment(environment, key);
31
- // Store the key
32
- await addKey(projectId, environment, key);
33
- // Output success message
34
- console.log(`${chalk.green("✔")} Initialization complete!`);
35
- console.log("\nSome useful tips:");
36
- const editCommand = chalk.gray(`dotenc edit ${environment}`);
37
- console.log(`\n- To securely edit your environment:\t${editCommand}`);
38
- const runCommand = chalk.gray(`dotenc run -e ${environment} <command> [args...]`);
39
- const runCommandWithEnv = chalk.gray(`DOTENC_ENV=${environment} dotenc run <command> [args...]`);
40
- console.log(`- To run your application:\t\t${runCommand} or ${runCommandWithEnv}`);
41
- const initCommand = chalk.gray("dotenc init [environment]");
42
- console.log(`- To initialize a new environment:\t${initCommand}`);
43
- console.log(`- Use the git-ignored ${chalk.gray(".env")} file for local development. It will have priority over any encrypted environments.`);
44
- };
@@ -1,21 +0,0 @@
1
- import chalk from "chalk";
2
- import { getKey } from "../../helpers/key.js";
3
- import { getProjectConfig } from "../../helpers/projectConfig.js";
4
- import { chooseEnvironmentPrompt } from "../../prompts/chooseEnvironment.js";
5
- export const keyExportCommand = async (environmentArg) => {
6
- const { projectId } = await getProjectConfig();
7
- if (!projectId) {
8
- console.error('No project found. Run "dotenc init" to create one.');
9
- return;
10
- }
11
- let environment = environmentArg;
12
- if (!environment) {
13
- environment = await chooseEnvironmentPrompt("What environment do you want to export the key from?");
14
- }
15
- const key = await getKey(environment);
16
- if (!key) {
17
- console.error(`\nNo key found for the ${chalk.cyan(environment)} environment.`);
18
- return;
19
- }
20
- console.log(`\nKey for the ${chalk.cyan(environment)} environment: ${chalk.gray(key)}`);
21
- };
@@ -1,22 +0,0 @@
1
- import chalk from "chalk";
2
- import { addKey } from "../../helpers/key.js";
3
- import { getProjectConfig } from "../../helpers/projectConfig.js";
4
- import { chooseEnvironmentPrompt } from "../../prompts/chooseEnvironment.js";
5
- import { inputKeyPrompt } from "../../prompts/inputKey.js";
6
- export const keyImportCommand = async (environmentArg, keyArg) => {
7
- const { projectId } = await getProjectConfig();
8
- if (!projectId) {
9
- console.error('No project found. Run "dotenc init" to create one.');
10
- return;
11
- }
12
- let environment = environmentArg;
13
- if (!environment) {
14
- environment = await chooseEnvironmentPrompt("What environment do you want to import the key to?");
15
- }
16
- let key = keyArg;
17
- if (!key) {
18
- key = await inputKeyPrompt("Paste the key here:");
19
- }
20
- await addKey(projectId, environment, key);
21
- console.log(`\nKey imported to the ${chalk.cyan(environment)} environment.`);
22
- };
@@ -1,35 +0,0 @@
1
- import chalk from "chalk";
2
- import crypto from "node:crypto";
3
- import { existsSync } from "node:fs";
4
- import path from "node:path";
5
- import { decrypt, encrypt } from "../../helpers/crypto.js";
6
- import { addKey, getKey } from "../../helpers/key.js";
7
- import { getProjectConfig } from "../../helpers/projectConfig.js";
8
- import { chooseEnvironmentPrompt } from "../../prompts/chooseEnvironment.js";
9
- export const keyRotateCommand = async (environmentArg) => {
10
- const { projectId } = await getProjectConfig();
11
- if (!projectId) {
12
- console.error('No project found. Run "dotenc init" to create one.');
13
- return;
14
- }
15
- let environment = environmentArg;
16
- if (!environment) {
17
- environment = await chooseEnvironmentPrompt("What environment do you want to rotate the key for?");
18
- }
19
- const environmentFile = `.env.${environment}.enc`;
20
- const environmentFilePath = path.join(process.cwd(), environmentFile);
21
- if (!existsSync(environmentFilePath)) {
22
- console.error(`Environment file not found: ${environmentFilePath}`);
23
- return;
24
- }
25
- const key = await getKey(environment);
26
- if (!key) {
27
- console.error(`\nNo key found for the ${chalk.cyan(environment)} environment.`);
28
- return;
29
- }
30
- const content = await decrypt(key, environmentFilePath);
31
- const newKey = crypto.randomBytes(32).toString("base64");
32
- await encrypt(newKey, content, environmentFilePath);
33
- await addKey(projectId, environment, newKey);
34
- console.log(`\nKey rotated for the ${chalk.cyan(environment)} environment.`);
35
- };
@@ -1,46 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import fs from "node:fs/promises";
4
- import path from "node:path";
5
- import { decrypt } from "../helpers/crypto.js";
6
- import { getKey } from "../helpers/key.js";
7
- import { parseEnv } from "../helpers/parseEnv.js";
8
- export const runCommand = async (command, args, options) => {
9
- // Get the environment
10
- const environmentArg = options.env || process.env.DOTENC_ENV;
11
- if (!environmentArg) {
12
- console.error('No environment provided. Use -e or set DOTENC_ENV to the environment you want to run the command in.\nTo start a new environment, use "dotenc init [environment]".');
13
- return;
14
- }
15
- const environments = environmentArg.split(",");
16
- const decryptedEnvs = await Promise.all(environments.map(async (environment, index) => {
17
- const environmentFilePath = path.join(process.cwd(), `.env.${environment}.enc`);
18
- if (!existsSync(environmentFilePath)) {
19
- console.error(`Environment file not found: ${environmentFilePath}`);
20
- return;
21
- }
22
- const key = await getKey(environment, index);
23
- const content = await decrypt(key, environmentFilePath);
24
- const decryptedEnv = parseEnv(content);
25
- return decryptedEnv;
26
- }));
27
- const decryptedEnv = decryptedEnvs.reduce((acc, env) => {
28
- return { ...acc, ...env };
29
- }, {});
30
- // Get the local environment
31
- let localEnv = {};
32
- const localEnvironmentFilePath = path.join(process.cwd(), ".env");
33
- if (existsSync(localEnvironmentFilePath)) {
34
- const localEnvContent = await fs.readFile(localEnvironmentFilePath, "utf-8");
35
- localEnv = parseEnv(localEnvContent);
36
- }
37
- // Merge the environment variables and run the command
38
- const mergedEnv = { ...process.env, ...decryptedEnv, ...localEnv };
39
- const child = spawn(command, args, {
40
- env: mergedEnv,
41
- stdio: "inherit",
42
- });
43
- child.on("exit", (code) => {
44
- process.exit(code);
45
- });
46
- };
@@ -1,10 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import path from "node:path";
3
- import { encrypt } from "./crypto.js";
4
- export const createEnvironment = async (name, key) => {
5
- const filePath = path.join(process.cwd(), `.env.${name}.enc`);
6
- if (existsSync(filePath)) {
7
- throw new Error(`Environment "${name}" already exists.`);
8
- }
9
- await encrypt(key, `# ${name} environment\n`, filePath);
10
- };
@@ -1,7 +0,0 @@
1
- import crypto from "node:crypto";
2
- /**
3
- * Computes a hash of the input string.
4
- */
5
- export const createHash = (input) => {
6
- return crypto.createHash("sha256").update(input).digest("hex");
7
- };
@@ -1,21 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- export const createLocalEnvironment = async () => {
5
- const gitignorePath = path.join(process.cwd(), ".gitignore");
6
- const envEntry = ".env";
7
- let gitignoreContent = [];
8
- if (existsSync(gitignorePath)) {
9
- gitignoreContent = (await fs.readFile(gitignorePath, "utf8")).split("\n");
10
- }
11
- // Check if the .env entry already exists (ignoring comments and whitespace)
12
- const isEnvIgnored = gitignoreContent.some((line) => line.trim() === envEntry);
13
- if (!isEnvIgnored) {
14
- // Append the .env entry to the .gitignore file
15
- await fs.appendFile(gitignorePath, `\n# Ignore local environment file\n${envEntry}\n`);
16
- }
17
- const envPath = path.join(process.cwd(), ".env");
18
- if (!existsSync(envPath)) {
19
- await fs.writeFile(envPath, "# Local environment variables\n");
20
- }
21
- };
@@ -1,13 +0,0 @@
1
- import { createId } from "@paralleldrive/cuid2";
2
- import { getProjectConfig, setProjectConfig } from "./projectConfig.js";
3
- export const createProject = async () => {
4
- const config = await getProjectConfig();
5
- if (config.projectId) {
6
- return config;
7
- }
8
- const newConfig = {
9
- projectId: createId(),
10
- };
11
- await setProjectConfig(newConfig);
12
- return newConfig;
13
- };
@@ -1,70 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs/promises";
3
- // AES-256-GCM constants
4
- const ALGORITHM = "aes-256-gcm";
5
- const IV_LENGTH = 12; // 96 bits, recommended for GCM
6
- const AUTH_TAG_LENGTH = 16; // 128 bits, standard for GCM
7
- /**
8
- * Encrypts a file using AES-256-GCM.
9
- * @param {string} key - The encryption key (must be 32 bytes for AES-256).
10
- * @param {string} input - The input string to encrypt.
11
- * @param {string} outputFile - Path to the output encrypted file.
12
- */
13
- export async function encrypt(key, input, outputFile) {
14
- const keyBuffer = Buffer.from(key, "base64");
15
- if (keyBuffer.length !== 32) {
16
- throw new Error("Key must be 32 bytes (256 bits) for AES-256-GCM.");
17
- }
18
- // Generate a random IV
19
- const iv = crypto.randomBytes(IV_LENGTH);
20
- // Create the cipher
21
- const cipher = crypto.createCipheriv(ALGORITHM, keyBuffer, iv);
22
- // Encrypt the data
23
- const encrypted = Buffer.concat([cipher.update(input), cipher.final()]);
24
- // Get the auth tag
25
- const authTag = cipher.getAuthTag();
26
- // Combine IV + encrypted content + auth tag
27
- const result = Buffer.concat([iv, encrypted, authTag]);
28
- // Write the encrypted file
29
- await fs.writeFile(outputFile, result);
30
- }
31
- /**
32
- * Decrypts a file using AES-256-GCM.
33
- * @param {string} key - The decryption key (must be 32 bytes for AES-256).
34
- * @param {string} inputFile - The input file to decrypt.
35
- */
36
- export async function decrypt(key, inputFile) {
37
- const keyBuffer = Buffer.from(key, "base64");
38
- if (keyBuffer.length !== 32) {
39
- throw new Error("Key must be 32 bytes (256 bits) for AES-256-GCM.");
40
- }
41
- // Read the encrypted file
42
- const encryptedData = await fs.readFile(inputFile);
43
- // Extract the IV from the start of the file
44
- const iv = encryptedData.subarray(0, IV_LENGTH);
45
- // Extract the auth tag from the end of the file
46
- const authTag = encryptedData.subarray(encryptedData.length - AUTH_TAG_LENGTH);
47
- // Extract the ciphertext (everything between IV and auth tag)
48
- const ciphertext = encryptedData.subarray(IV_LENGTH, encryptedData.length - AUTH_TAG_LENGTH);
49
- try {
50
- // Create the decipher
51
- const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv);
52
- decipher.setAuthTag(authTag);
53
- // Decrypt the ciphertext
54
- const decrypted = Buffer.concat([
55
- decipher.update(ciphertext),
56
- decipher.final(),
57
- ]);
58
- return decrypted.toString();
59
- }
60
- catch (error) {
61
- if (error instanceof Error &&
62
- error.message.includes("unable to authenticate")) {
63
- throw new Error("Failed to decrypt file. This could be because:\n" +
64
- "1. The encryption key may be incorrect\n" +
65
- "2. The encrypted file may be corrupted\n" +
66
- "3. The encrypted file may have been tampered with");
67
- }
68
- throw error;
69
- }
70
- }
@@ -1,6 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import path from "node:path";
3
- export const environmentExists = (environment) => {
4
- const envPath = path.join(process.cwd(), `.env.${environment}.enc`);
5
- return existsSync(envPath);
6
- };
@@ -1,41 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import { getHomeConfig } from "./homeConfig.js";
3
- /**
4
- * Determines the default text editor for the system.
5
- * @returns {string} The command to launch the default text editor.
6
- */
7
- export const getDefaultEditor = async () => {
8
- const config = await getHomeConfig();
9
- // Check the editor field in the config file
10
- if (config.editor) {
11
- return config.editor;
12
- }
13
- // Check the EDITOR environment variable
14
- if (process.env.EDITOR) {
15
- return process.env.EDITOR;
16
- }
17
- // Check the VISUAL environment variable
18
- if (process.env.VISUAL) {
19
- return process.env.VISUAL;
20
- }
21
- // Platform-specific defaults
22
- const platform = process.platform;
23
- if (platform === "win32") {
24
- // Windows: Use notepad as the fallback editor
25
- return "notepad";
26
- }
27
- // Linux/macOS: Try nano, vim, or vi
28
- const editors = ["nano", "vim", "vi"];
29
- for (const editor of editors) {
30
- try {
31
- // Check if the editor is available
32
- execSync(`command -v ${editor}`, { stdio: "ignore" });
33
- return editor; // Return the first available editor
34
- }
35
- catch {
36
- // Ignore errors and try the next editor
37
- }
38
- }
39
- // If no editor is found, throw an error
40
- throw new Error('No text editor found. Please set the EDITOR environment variable, configure an editor using "dotenc config editor <command>", or install a text editor (e.g., nano, vim, or notepad).');
41
- };
@@ -1,5 +0,0 @@
1
- import { environmentExists } from "./environmentExists.js";
2
- export const getEnvironmentNameSuggestion = () => {
3
- const suggestions = ["development", "staging", "production", "test"].find((env) => !environmentExists(env)) ?? "";
4
- return suggestions;
5
- };
@@ -1,22 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import fs from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { z } from "zod";
6
- const homeConfigSchema = z.object({
7
- editor: z.string().nullish(),
8
- });
9
- const configPath = path.join(os.homedir(), ".dotenc", "config.json");
10
- export const setHomeConfig = async (config) => {
11
- const parsedConfig = homeConfigSchema.parse(config);
12
- await fs.writeFile(configPath, JSON.stringify(parsedConfig, null, 2), {
13
- mode: 0o600,
14
- });
15
- };
16
- export const getHomeConfig = async () => {
17
- if (existsSync(configPath)) {
18
- const config = JSON.parse(await fs.readFile(configPath, "utf-8"));
19
- return homeConfigSchema.parse(config);
20
- }
21
- return {};
22
- };
@@ -1,39 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import fs from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { getProjectConfig } from "./projectConfig.js";
6
- const keysFile = path.join(os.homedir(), ".dotenc", "keys.json");
7
- export const getKey = async (environment, index = 0) => {
8
- if (process.env.DOTENC_KEY) {
9
- const keys = process.env.DOTENC_KEY.split(",");
10
- return keys[index];
11
- }
12
- const { projectId } = await getProjectConfig();
13
- if (existsSync(keysFile)) {
14
- const keys = JSON.parse(await fs.readFile(keysFile, "utf-8"));
15
- return keys[projectId][environment];
16
- }
17
- throw new Error("No key found. Please set the DOTENC_KEY environment variable or import the key using `dotenc key import <environment> <key>`.");
18
- };
19
- /**
20
- * Adds or updates a key for a specific project and environment.
21
- */
22
- export const addKey = async (projectId, environment, key) => {
23
- // Ensure the keys file exists
24
- if (!existsSync(keysFile)) {
25
- await fs.mkdir(path.dirname(keysFile), { recursive: true });
26
- await fs.writeFile(keysFile, "{}", { mode: 0o600 }); // Create an empty JSON file with secure permissions
27
- }
28
- // Read the existing keys
29
- const keys = JSON.parse(await fs.readFile(keysFile, "utf8"));
30
- // Add or update the key
31
- if (!keys[projectId]) {
32
- keys[projectId] = {};
33
- }
34
- keys[projectId][environment] = key;
35
- // Write the updated keys back to the file
36
- await fs.writeFile(keysFile, JSON.stringify(keys, null, 2), {
37
- mode: 0o600,
38
- });
39
- };