envsec 0.1.1 → 0.1.4
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 +68 -6
- package/dist/cli/delete.js +12 -0
- package/dist/cli/read.js +1 -1
- package/dist/implementations/SqliteMetadataStore.js +52 -20
- package/dist/main.js +3 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,15 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# envsec
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- macOS
|
|
15
|
+
- Node.js >= 18
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g envsec
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx envsec
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
All commands require an environment specified with `--env` (or `-e`):
|
|
30
|
+
|
|
31
|
+
### Add a secret
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Store a string
|
|
35
|
+
secenv -e dev add api.key --word "sk-abc123"
|
|
36
|
+
|
|
37
|
+
# Store a number
|
|
38
|
+
secenv -e dev add server.port --digit 3000
|
|
39
|
+
|
|
40
|
+
# Store a boolean
|
|
41
|
+
secenv -e dev add feature.enabled --bool
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Get a secret
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
secenv -e dev get api.key
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### List all secrets
|
|
4
51
|
|
|
5
52
|
```bash
|
|
6
|
-
|
|
53
|
+
secenv -e dev list
|
|
7
54
|
```
|
|
8
55
|
|
|
9
|
-
|
|
56
|
+
### Search secrets
|
|
10
57
|
|
|
11
58
|
```bash
|
|
12
|
-
|
|
59
|
+
secenv -e dev search "api.*"
|
|
13
60
|
```
|
|
14
61
|
|
|
15
|
-
|
|
62
|
+
### Delete a secret
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
secenv -e dev delete api.key
|
|
66
|
+
|
|
67
|
+
# or use the alias
|
|
68
|
+
secenv -e dev del api.key
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## How it works
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
const key = Args.text({ name: "key" });
|
|
6
|
+
const handler = ({ key }) => Effect.gen(function* () {
|
|
7
|
+
const { env } = yield* rootCommand;
|
|
8
|
+
yield* SecretStore.remove(env, key);
|
|
9
|
+
yield* Effect.log(`Secret "${key}" removed from env "${env}"`);
|
|
10
|
+
});
|
|
11
|
+
export const deleteCommand = Command.make("delete", { key }, handler);
|
|
12
|
+
export const delCommand = Command.make("del", { key }, handler);
|
package/dist/cli/read.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Effect, Console } from "effect";
|
|
|
3
3
|
import { SecretStore } from "../services/SecretStore.js";
|
|
4
4
|
import { rootCommand } from "./root.js";
|
|
5
5
|
const key = Args.text({ name: "key" });
|
|
6
|
-
export const
|
|
6
|
+
export const getCommand = Command.make("get", { key }, ({ key }) => Effect.gen(function* () {
|
|
7
7
|
const { env } = yield* rootCommand;
|
|
8
8
|
const value = yield* SecretStore.get(env, key);
|
|
9
9
|
yield* Console.log(String(value));
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { Effect, Layer } from "effect";
|
|
2
|
-
import
|
|
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
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
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.
|
|
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')
|
|
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: () =>
|
|
52
|
-
.prepare(`SELECT key, type, created_at, updated_at FROM secrets WHERE env = ? AND key = ?`)
|
|
53
|
-
.
|
|
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.
|
|
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: () =>
|
|
82
|
-
|
|
83
|
-
.
|
|
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: () =>
|
|
93
|
-
|
|
94
|
-
.
|
|
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
|
@@ -5,13 +5,14 @@ import { Effect } from "effect";
|
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { rootCommand } from "./cli/root.js";
|
|
7
7
|
import { addCommand } from "./cli/add.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getCommand } from "./cli/read.js";
|
|
9
|
+
import { deleteCommand, delCommand } from "./cli/delete.js";
|
|
9
10
|
import { searchCommand } from "./cli/search.js";
|
|
10
11
|
import { listCommand } from "./cli/list.js";
|
|
11
12
|
import { SecretStore } from "./services/SecretStore.js";
|
|
12
13
|
const require = createRequire(import.meta.url);
|
|
13
14
|
const pkg = require("../package.json");
|
|
14
|
-
const command = rootCommand.pipe(Command.withSubcommands([addCommand,
|
|
15
|
+
const command = rootCommand.pipe(Command.withSubcommands([addCommand, getCommand, deleteCommand, delCommand, searchCommand, listCommand]));
|
|
15
16
|
const cli = Command.run(command, {
|
|
16
17
|
name: "secenv",
|
|
17
18
|
version: pkg.version,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envsec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Secure environment secrets management using macOS Keychain",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/davidnussio/
|
|
34
|
+
"url": "git+https://github.com/davidnussio/envsec.git"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|
|
@@ -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
|
-
"
|
|
44
|
-
"
|
|
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
|
}
|