envsec 0.1.2 → 0.1.5

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/README.md CHANGED
@@ -1,15 +1,101 @@
1
1
  # secenv
2
2
 
3
- To install dependencies:
3
+ Secure environment secrets management for macOS using the native Keychain.
4
+
5
+ ## Features
6
+
7
+ - Store secrets in macOS Keychain (not plain text files)
8
+ - Organize secrets by environment (dev, staging, prod, etc.)
9
+ - Track secret types (string, number, boolean) and metadata via SQLite
10
+ - Search secrets with glob patterns
11
+ - Run commands with secret interpolation
12
+ - Export secrets to `.env` files
13
+
14
+ ## Requirements
15
+
16
+ - macOS
17
+ - Node.js >= 18
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install -g secenv
23
+ ```
24
+
25
+ ```bash
26
+ npx secenv
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ All commands require an environment specified with `--env` (or `-e`):
32
+
33
+ ### Add a secret
4
34
 
5
35
  ```bash
6
- bun install
36
+ # Store a string
37
+ secenv -e dev add api.key --word "sk-abc123"
38
+
39
+ # Store a number
40
+ secenv -e dev add server.port --digit 3000
41
+
42
+ # Store a boolean
43
+ secenv -e dev add feature.enabled --bool
7
44
  ```
8
45
 
9
- To run:
46
+ ### Get a secret
10
47
 
11
48
  ```bash
12
- bun run index.ts
49
+ secenv -e dev get api.key
13
50
  ```
14
51
 
15
- This project was created using `bun init` in bun v1.3.10. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
52
+ ### List all secrets
53
+
54
+ ```bash
55
+ secenv -e dev list
56
+ ```
57
+
58
+ ### Search secrets
59
+
60
+ ```bash
61
+ secenv -e dev search "api.*"
62
+ ```
63
+
64
+ ### Generate a .env file
65
+
66
+ ```bash
67
+ # Creates .env with all secrets from the environment
68
+ secenv -e dev env-file
69
+
70
+ # Specify a custom output path
71
+ secenv -e dev env-file --output .env.local
72
+ ```
73
+
74
+ Keys are converted to `UPPER_SNAKE_CASE` (e.g. `api.token` → `API_TOKEN`).
75
+
76
+ ### Run a command with secrets
77
+
78
+ ```bash
79
+ # Placeholders {key} are resolved with secret values before execution
80
+ secenv -e dev run 'curl {api.url} -H "Authorization: Bearer {api.token}"'
81
+
82
+ # Any {dotted.key} in the command string is replaced with its value
83
+ secenv -e prod run 'psql {db.connection_string}'
84
+ ```
85
+
86
+ ### Delete a secret
87
+
88
+ ```bash
89
+ secenv -e dev delete api.key
90
+
91
+ # or use the alias
92
+ secenv -e dev del api.key
93
+ ```
94
+
95
+ ## How it works
96
+
97
+ Secrets are stored in the macOS Keychain using the `security` command-line tool. Metadata (key names, types, timestamps) is kept in a SQLite database at `~/.secenv/store.sqlite`. Keys must contain at least one dot separator (e.g., `service.account`) which maps to the Keychain service/account structure.
98
+
99
+ ## License
100
+
101
+ MIT
@@ -0,0 +1,22 @@
1
+ import { Command, Options } from "@effect/cli";
2
+ import { Effect } from "effect";
3
+ import { SecretStore } from "../services/SecretStore.js";
4
+ import { rootCommand } from "./root.js";
5
+ import { writeFileSync } from "node:fs";
6
+ const output = Options.text("output").pipe(Options.withAlias("o"), Options.withDescription("Output file path (default: .env)"), Options.withDefault(".env"));
7
+ export const envFileCommand = Command.make("env-file", { output }, ({ output }) => Effect.gen(function* () {
8
+ const { env } = yield* rootCommand;
9
+ const secrets = yield* SecretStore.list(env);
10
+ if (secrets.length === 0) {
11
+ yield* Effect.log(`No secrets found for env "${env}"`);
12
+ return;
13
+ }
14
+ const lines = [];
15
+ for (const item of secrets) {
16
+ const value = yield* SecretStore.get(env, item.key);
17
+ const envKey = item.key.toUpperCase().replaceAll(".", "_");
18
+ lines.push(`${envKey}=${String(value)}`);
19
+ }
20
+ writeFileSync(output, lines.join("\n") + "\n", "utf-8");
21
+ yield* Effect.log(`Written ${secrets.length} secret(s) to ${output}`);
22
+ }));
@@ -0,0 +1,26 @@
1
+ import { Command, Args } from "@effect/cli";
2
+ import { Effect } from "effect";
3
+ import { SecretStore } from "../services/SecretStore.js";
4
+ import { rootCommand } from "./root.js";
5
+ import { execSync } from "node:child_process";
6
+ const cmd = Args.text({ name: "command" }).pipe(Args.withDescription("Command to execute. Use {key} placeholders for secret interpolation"));
7
+ export const runCommand = Command.make("run", { cmd }, ({ cmd }) => Effect.gen(function* () {
8
+ const { env } = yield* rootCommand;
9
+ // Find all {key} placeholders
10
+ const placeholders = [...cmd.matchAll(/\{([^}]+)\}/g)];
11
+ let resolved = cmd;
12
+ for (const match of placeholders) {
13
+ const key = match[1];
14
+ const value = yield* SecretStore.get(env, key);
15
+ resolved = resolved.replaceAll(`{${key}}`, String(value));
16
+ }
17
+ if (placeholders.length > 0) {
18
+ yield* Effect.log(`Resolved ${placeholders.length} secret(s)`);
19
+ }
20
+ try {
21
+ execSync(resolved, { stdio: "inherit", shell: "/bin/bash" });
22
+ }
23
+ catch (e) {
24
+ yield* Effect.fail(new Error(`Command exited with code ${e.status ?? 1}`));
25
+ }
26
+ }));
@@ -1,15 +1,19 @@
1
1
  import { Effect, Layer } from "effect";
2
- import Database from "better-sqlite3";
3
- import { mkdirSync } from "node:fs";
2
+ import initSqlJs from "sql.js";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { MetadataStore } from "../services/MetadataStore.js";
7
7
  import { MetadataStoreError, SecretNotFoundError } from "../errors.js";
8
- const dbPath = join(homedir(), ".secenv", "store.sqlite");
9
- const initDb = () => {
10
- mkdirSync(join(homedir(), ".secenv"), { recursive: true });
11
- const db = new Database(dbPath);
12
- db.exec(`
8
+ const dbDir = join(homedir(), ".secenv");
9
+ const dbPath = join(dbDir, "store.sqlite");
10
+ const initDb = async () => {
11
+ mkdirSync(dbDir, { recursive: true });
12
+ const SQL = await initSqlJs();
13
+ const db = existsSync(dbPath)
14
+ ? new SQL.Database(readFileSync(dbPath))
15
+ : new SQL.Database();
16
+ db.run(`
13
17
  CREATE TABLE IF NOT EXISTS secrets (
14
18
  id INTEGER PRIMARY KEY AUTOINCREMENT,
15
19
  env TEXT NOT NULL,
@@ -20,10 +24,14 @@ const initDb = () => {
20
24
  UNIQUE(env, key)
21
25
  )
22
26
  `);
27
+ persist(db);
23
28
  return db;
24
29
  };
30
+ const persist = (db) => {
31
+ writeFileSync(dbPath, Buffer.from(db.export()));
32
+ };
25
33
  const make = Effect.gen(function* () {
26
- const db = yield* Effect.try({
34
+ const db = yield* Effect.tryPromise({
27
35
  try: () => initDb(),
28
36
  catch: (error) => new MetadataStoreError({
29
37
  operation: "init",
@@ -34,11 +42,12 @@ const make = Effect.gen(function* () {
34
42
  upsert: Effect.fn("SqliteMetadataStore.upsert")(function* (env, key, type) {
35
43
  yield* Effect.try({
36
44
  try: () => {
37
- db.prepare(`INSERT INTO secrets (env, key, type)
45
+ db.run(`INSERT INTO secrets (env, key, type)
38
46
  VALUES (?, ?, ?)
39
47
  ON CONFLICT(env, key) DO UPDATE SET
40
48
  type = excluded.type,
41
- updated_at = datetime('now')`).run(env, key, type);
49
+ updated_at = datetime('now')`, [env, key, type]);
50
+ persist(db);
42
51
  },
43
52
  catch: (error) => new MetadataStoreError({
44
53
  operation: "upsert",
@@ -48,9 +57,17 @@ const make = Effect.gen(function* () {
48
57
  }),
49
58
  get: Effect.fn("SqliteMetadataStore.get")(function* (env, key) {
50
59
  const row = yield* Effect.try({
51
- try: () => db
52
- .prepare(`SELECT key, type, created_at, updated_at FROM secrets WHERE env = ? AND key = ?`)
53
- .get(env, key),
60
+ try: () => {
61
+ const stmt = db.prepare(`SELECT key, type, created_at, updated_at FROM secrets WHERE env = ? AND key = ?`);
62
+ stmt.bind([env, key]);
63
+ if (!stmt.step()) {
64
+ stmt.free();
65
+ return null;
66
+ }
67
+ const result = stmt.getAsObject();
68
+ stmt.free();
69
+ return result;
70
+ },
54
71
  catch: (error) => new MetadataStoreError({
55
72
  operation: "get",
56
73
  message: `Failed to get metadata for ${env}/${key}: ${error}`,
@@ -68,7 +85,8 @@ const make = Effect.gen(function* () {
68
85
  remove: Effect.fn("SqliteMetadataStore.remove")(function* (env, key) {
69
86
  yield* Effect.try({
70
87
  try: () => {
71
- db.prepare(`DELETE FROM secrets WHERE env = ? AND key = ?`).run(env, key);
88
+ db.run(`DELETE FROM secrets WHERE env = ? AND key = ?`, [env, key]);
89
+ persist(db);
72
90
  },
73
91
  catch: (error) => new MetadataStoreError({
74
92
  operation: "remove",
@@ -78,9 +96,16 @@ const make = Effect.gen(function* () {
78
96
  }),
79
97
  search: Effect.fn("SqliteMetadataStore.search")(function* (env, pattern) {
80
98
  return yield* Effect.try({
81
- try: () => db
82
- .prepare(`SELECT key, type FROM secrets WHERE env = ? AND key GLOB ?`)
83
- .all(env, pattern),
99
+ try: () => {
100
+ const results = [];
101
+ const stmt = db.prepare(`SELECT key, type FROM secrets WHERE env = ? AND key GLOB ?`);
102
+ stmt.bind([env, pattern]);
103
+ while (stmt.step()) {
104
+ results.push(stmt.getAsObject());
105
+ }
106
+ stmt.free();
107
+ return results;
108
+ },
84
109
  catch: (error) => new MetadataStoreError({
85
110
  operation: "search",
86
111
  message: `Failed to search metadata for ${env}/${pattern}: ${error}`,
@@ -89,9 +114,16 @@ const make = Effect.gen(function* () {
89
114
  }),
90
115
  list: Effect.fn("SqliteMetadataStore.list")(function* (env) {
91
116
  return yield* Effect.try({
92
- try: () => db
93
- .prepare(`SELECT key, type, updated_at FROM secrets WHERE env = ? ORDER BY key`)
94
- .all(env),
117
+ try: () => {
118
+ const results = [];
119
+ const stmt = db.prepare(`SELECT key, type, updated_at FROM secrets WHERE env = ? ORDER BY key`);
120
+ stmt.bind([env]);
121
+ while (stmt.step()) {
122
+ results.push(stmt.getAsObject());
123
+ }
124
+ stmt.free();
125
+ return results;
126
+ },
95
127
  catch: (error) => new MetadataStoreError({
96
128
  operation: "list",
97
129
  message: `Failed to list metadata for ${env}: ${error}`,
package/dist/main.js CHANGED
@@ -9,10 +9,12 @@ import { getCommand } from "./cli/read.js";
9
9
  import { deleteCommand, delCommand } from "./cli/delete.js";
10
10
  import { searchCommand } from "./cli/search.js";
11
11
  import { listCommand } from "./cli/list.js";
12
+ import { runCommand } from "./cli/run.js";
13
+ import { envFileCommand } from "./cli/env-file.js";
12
14
  import { SecretStore } from "./services/SecretStore.js";
13
15
  const require = createRequire(import.meta.url);
14
16
  const pkg = require("../package.json");
15
- const command = rootCommand.pipe(Command.withSubcommands([addCommand, getCommand, deleteCommand, delCommand, searchCommand, listCommand]));
17
+ const command = rootCommand.pipe(Command.withSubcommands([addCommand, getCommand, deleteCommand, delCommand, searchCommand, listCommand, runCommand, envFileCommand]));
16
18
  const cli = Command.run(command, {
17
19
  name: "secenv",
18
20
  version: pkg.version,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envsec",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "Secure environment secrets management using macOS Keychain",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,12 +40,12 @@
40
40
  "@effect/cli": "^0.73.2",
41
41
  "@effect/platform": "^0.94.5",
42
42
  "@effect/platform-node": "^0.104.1",
43
- "better-sqlite3": "^12.8.0",
44
- "effect": "^3.19.19"
43
+ "effect": "^3.19.19",
44
+ "sql.js": "^1.14.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@types/better-sqlite3": "^7.6.13",
48
47
  "@types/node": "^22",
48
+ "@types/sql.js": "^1.4.9",
49
49
  "typescript": "^5"
50
50
  }
51
51
  }