@captainsafia/burrow 1.0.0-preview.d773ac0 → 1.0.0-preview.e3ae96a

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
@@ -30,7 +30,7 @@ burrow set DATABASE_URL=postgres://localhost/mydb --path ~/projects
30
30
  ### Get a secret
31
31
 
32
32
  ```bash
33
- burrow get API_KEY --show
33
+ burrow get API_KEY
34
34
  burrow get API_KEY --format json
35
35
  ```
36
36
 
@@ -54,6 +54,14 @@ eval "$(burrow export --format shell)" && npm start
54
54
  burrow unset API_KEY --path ~/projects/app/tests
55
55
  ```
56
56
 
57
+ ### Remove a secret
58
+
59
+ ```bash
60
+ burrow remove API_KEY --path ~/projects/app
61
+ ```
62
+
63
+ Unlike `unset` which blocks inheritance, `remove` deletes the entry entirely, restoring inheritance from parent directories.
64
+
57
65
  ## How It Works
58
66
 
59
67
  Secrets are stored in your user profile:
@@ -76,13 +84,26 @@ import { BurrowClient } from '@captainsafia/burrow';
76
84
 
77
85
  const client = new BurrowClient();
78
86
 
79
- await client.set('API_KEY', 'secret123', { path: '/my/project' });
87
+ try {
88
+ await client.set('API_KEY', 'secret123', { path: '/my/project' });
80
89
 
81
- const secret = await client.get('API_KEY', { cwd: '/my/project/subdir' });
82
- console.log(secret?.value); // 'secret123'
83
- console.log(secret?.sourcePath); // '/my/project'
90
+ const secret = await client.get('API_KEY', { cwd: '/my/project/subdir' });
91
+ console.log(secret?.value); // 'secret123'
92
+ console.log(secret?.sourcePath); // '/my/project'
84
93
 
85
- const allSecrets = await client.list({ cwd: '/my/project' });
94
+ const allSecrets = await client.list({ cwd: '/my/project' });
95
+ } finally {
96
+ client.close(); // Clean up database connection
97
+ }
98
+ ```
99
+
100
+ Or with TypeScript's `using` declarations for automatic cleanup:
101
+
102
+ ```typescript
103
+ {
104
+ using client = new BurrowClient();
105
+ await client.set('API_KEY', 'secret123');
106
+ } // Automatically cleaned up
86
107
  ```
87
108
 
88
109
  ## Contributing
package/dist/api.d.ts CHANGED
@@ -21,7 +21,7 @@ export interface BurrowClientOptions {
21
21
  configDir?: string;
22
22
  /**
23
23
  * Custom filename for the secrets store.
24
- * Defaults to `store.json`.
24
+ * Defaults to `store.db`.
25
25
  */
26
26
  storeFileName?: string;
27
27
  /**
@@ -72,6 +72,16 @@ export interface BlockOptions {
72
72
  */
73
73
  path?: string;
74
74
  }
75
+ /**
76
+ * Options for the `remove` method.
77
+ */
78
+ export interface RemoveOptions {
79
+ /**
80
+ * Directory path to remove the secret from.
81
+ * Defaults to the current working directory.
82
+ */
83
+ path?: string;
84
+ }
75
85
  /**
76
86
  * Options for the `export` method.
77
87
  */
@@ -89,10 +99,6 @@ export interface ExportOptions {
89
99
  * - `json`: Exports as a JSON object
90
100
  */
91
101
  format?: ExportFormat;
92
- /**
93
- * Whether to show actual values (currently unused, reserved for future use).
94
- */
95
- showValues?: boolean;
96
102
  /**
97
103
  * Whether to include source paths in JSON output.
98
104
  * When true, JSON output includes `{ key: { value, sourcePath } }` format.
@@ -141,7 +147,7 @@ export declare class BurrowClient {
141
147
  * The secret will be available to the specified directory and all its
142
148
  * subdirectories, unless overridden or blocked at a deeper level.
143
149
  *
144
- * @param key - Environment variable name. Must match `^[A-Z_][A-Z0-9_]*$`
150
+ * @param key - Environment variable name. Must match `^[A-Za-z_][A-Za-z0-9_]*$`
145
151
  * @param value - Secret value to store
146
152
  * @param options - Set options including target path
147
153
  * @throws Error if the key format is invalid
@@ -205,7 +211,7 @@ export declare class BurrowClient {
205
211
  *
206
212
  * A blocked key can be re-enabled by calling `set` at the same or deeper path.
207
213
  *
208
- * @param key - Environment variable name to block. Must match `^[A-Z_][A-Z0-9_]*$`
214
+ * @param key - Environment variable name to block. Must match `^[A-Za-z_][A-Za-z0-9_]*$`
209
215
  * @param options - Block options including target path
210
216
  * @throws Error if the key format is invalid
211
217
  *
@@ -223,6 +229,33 @@ export declare class BurrowClient {
223
229
  * ```
224
230
  */
225
231
  block(key: string, options?: BlockOptions): Promise<void>;
232
+ /**
233
+ * Removes a secret entry entirely from the specified path.
234
+ *
235
+ * Unlike `block`, which creates a tombstone to prevent inheritance,
236
+ * `remove` completely deletes the secret entry. After removal, the key
237
+ * may still be inherited from parent directories if defined there.
238
+ *
239
+ * @param key - Environment variable name to remove. Must match `^[A-Za-z_][A-Za-z0-9_]*$`
240
+ * @param options - Remove options including target path
241
+ * @returns true if the secret was found and removed, false if it didn't exist
242
+ * @throws Error if the key format is invalid
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * // Set a secret
247
+ * await client.set('API_KEY', 'secret', { path: '/projects/myapp' });
248
+ *
249
+ * // Remove it entirely
250
+ * const removed = await client.remove('API_KEY', { path: '/projects/myapp' });
251
+ * console.log(removed); // true
252
+ *
253
+ * // Trying to remove again returns false
254
+ * const removedAgain = await client.remove('API_KEY', { path: '/projects/myapp' });
255
+ * console.log(removedAgain); // false
256
+ * ```
257
+ */
258
+ remove(key: string, options?: RemoveOptions): Promise<boolean>;
226
259
  /**
227
260
  * Exports resolved secrets in various formats.
228
261
  *
@@ -274,6 +307,36 @@ export declare class BurrowClient {
274
307
  * ```
275
308
  */
276
309
  resolve(cwd?: string): Promise<Map<string, ResolvedSecret>>;
310
+ /**
311
+ * Closes the database connection and releases resources.
312
+ * After calling this method, the client instance should not be used.
313
+ *
314
+ * This method is safe to call multiple times.
315
+ *
316
+ * @example
317
+ * ```typescript
318
+ * const client = new BurrowClient();
319
+ * try {
320
+ * await client.set('API_KEY', 'value');
321
+ * // ... do work
322
+ * } finally {
323
+ * client.close();
324
+ * }
325
+ * ```
326
+ */
327
+ close(): void;
328
+ /**
329
+ * Allows using the BurrowClient with `using` declarations for automatic cleanup.
330
+ *
331
+ * @example
332
+ * ```typescript
333
+ * {
334
+ * using client = new BurrowClient();
335
+ * await client.set('API_KEY', 'value');
336
+ * } // client.close() is called automatically
337
+ * ```
338
+ */
339
+ [Symbol.dispose](): void;
277
340
  }
278
341
  /**
279
342
  * Creates a new BurrowClient instance.
package/dist/api.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/storage/index.ts
2
- import { mkdir, rename, unlink } from "node:fs/promises";
2
+ import { Database } from "bun:sqlite";
3
+ import { chmod, mkdir } from "node:fs/promises";
3
4
  import { join as join2 } from "node:path";
4
- import { randomBytes } from "node:crypto";
5
5
 
6
6
  // src/platform/index.ts
7
7
  import { homedir } from "node:os";
@@ -44,18 +44,12 @@ function isWindows() {
44
44
  }
45
45
 
46
46
  // src/storage/index.ts
47
- var STORE_VERSION = 1;
48
- var DEFAULT_STORE_FILE = "store.json";
49
- function createEmptyStore() {
50
- return {
51
- version: STORE_VERSION,
52
- paths: {}
53
- };
54
- }
47
+ var DEFAULT_STORE_FILE = "store.db";
55
48
 
56
49
  class Storage {
57
50
  configDir;
58
51
  storeFileName;
52
+ db = null;
59
53
  constructor(options = {}) {
60
54
  this.configDir = options.configDir ?? getConfigDir();
61
55
  this.storeFileName = options.storeFileName ?? DEFAULT_STORE_FILE;
@@ -63,69 +57,97 @@ class Storage {
63
57
  get storePath() {
64
58
  return join2(this.configDir, this.storeFileName);
65
59
  }
66
- async read() {
67
- try {
68
- const file = Bun.file(this.storePath);
69
- const content = await file.text();
70
- const store = JSON.parse(content);
71
- if (store.version !== STORE_VERSION) {
72
- throw new Error(`Unsupported store version: ${store.version}. Expected: ${STORE_VERSION}`);
73
- }
74
- return store;
75
- } catch (error) {
76
- if (error.code === "ENOENT") {
77
- return createEmptyStore();
78
- }
79
- throw error;
60
+ async ensureDb() {
61
+ if (this.db) {
62
+ return this.db;
80
63
  }
81
- }
82
- async write(store) {
83
64
  await mkdir(this.configDir, { recursive: true });
84
- const tempFileName = `.store-${randomBytes(8).toString("hex")}.tmp`;
85
- const tempPath = join2(this.configDir, tempFileName);
86
- const content = JSON.stringify(store, null, 2);
87
- try {
88
- const file = Bun.file(tempPath);
89
- await Bun.write(file, content);
90
- await rename(tempPath, this.storePath);
91
- } catch (error) {
92
- try {
93
- await unlink(tempPath);
94
- } catch {}
95
- throw error;
65
+ if (!isWindows()) {
66
+ await chmod(this.configDir, 448);
67
+ }
68
+ this.db = new Database(this.storePath);
69
+ if (!isWindows()) {
70
+ await chmod(this.storePath, 384);
96
71
  }
72
+ this.db.run("PRAGMA journal_mode = WAL");
73
+ this.db.run(`
74
+ CREATE TABLE IF NOT EXISTS secrets (
75
+ path TEXT NOT NULL,
76
+ key TEXT NOT NULL,
77
+ value TEXT,
78
+ updated_at TEXT NOT NULL,
79
+ PRIMARY KEY (path, key)
80
+ )
81
+ `);
82
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_secrets_path ON secrets (path)");
83
+ const versionResult = this.db.query("PRAGMA user_version").get();
84
+ const currentVersion = versionResult?.user_version ?? 0;
85
+ if (currentVersion === 0) {
86
+ this.db.run("PRAGMA user_version = 1");
87
+ } else if (currentVersion !== 1) {
88
+ throw new Error(`Unsupported store version: ${currentVersion}. Expected: 1`);
89
+ }
90
+ return this.db;
97
91
  }
98
92
  async setSecret(canonicalPath, key, value) {
99
- const store = await this.read();
100
- if (!store.paths[canonicalPath]) {
101
- store.paths[canonicalPath] = {};
102
- }
103
- store.paths[canonicalPath][key] = {
104
- value,
105
- updatedAt: new Date().toISOString()
106
- };
107
- await this.write(store);
93
+ const db = await this.ensureDb();
94
+ const updatedAt = new Date().toISOString();
95
+ db.query(`
96
+ INSERT INTO secrets (path, key, value, updated_at)
97
+ VALUES (?, ?, ?, ?)
98
+ ON CONFLICT(path, key) DO UPDATE SET
99
+ value = excluded.value,
100
+ updated_at = excluded.updated_at
101
+ `).run(canonicalPath, key, value, updatedAt);
108
102
  }
109
103
  async getPathSecrets(canonicalPath) {
110
- const store = await this.read();
111
- return store.paths[canonicalPath];
104
+ const db = await this.ensureDb();
105
+ const rows = db.query("SELECT key, value, updated_at FROM secrets WHERE path = ?").all(canonicalPath);
106
+ if (rows.length === 0) {
107
+ return;
108
+ }
109
+ const secrets = {};
110
+ for (const row of rows) {
111
+ secrets[row.key] = {
112
+ value: row.value,
113
+ updatedAt: row.updated_at
114
+ };
115
+ }
116
+ return secrets;
112
117
  }
113
118
  async getAllPaths() {
114
- const store = await this.read();
115
- return Object.keys(store.paths);
119
+ const db = await this.ensureDb();
120
+ const rows = db.query("SELECT DISTINCT path FROM secrets").all();
121
+ return rows.map((row) => row.path);
122
+ }
123
+ async getAncestorPaths(canonicalPath) {
124
+ const db = await this.ensureDb();
125
+ let rows;
126
+ if (isWindows()) {
127
+ rows = db.query("SELECT DISTINCT path FROM secrets WHERE ? = path OR ? LIKE path || '\\' || '%' OR (length(path) = 3 AND path LIKE '_:\\' AND ? LIKE path || '%')").all(canonicalPath, canonicalPath, canonicalPath);
128
+ } else {
129
+ rows = db.query("SELECT DISTINCT path FROM secrets WHERE ? = path OR ? LIKE path || '/' || '%' OR path = '/'").all(canonicalPath, canonicalPath);
130
+ }
131
+ return rows.map((row) => row.path);
116
132
  }
117
133
  async removeKey(canonicalPath, key) {
118
- const store = await this.read();
119
- if (!store.paths[canonicalPath]?.[key]) {
134
+ const db = await this.ensureDb();
135
+ const existing = db.query("SELECT path FROM secrets WHERE path = ? AND key = ?").get(canonicalPath, key);
136
+ if (!existing) {
120
137
  return false;
121
138
  }
122
- delete store.paths[canonicalPath][key];
123
- if (Object.keys(store.paths[canonicalPath]).length === 0) {
124
- delete store.paths[canonicalPath];
125
- }
126
- await this.write(store);
139
+ db.query("DELETE FROM secrets WHERE path = ? AND key = ?").run(canonicalPath, key);
127
140
  return true;
128
141
  }
142
+ close() {
143
+ if (this.db) {
144
+ this.db.close();
145
+ this.db = null;
146
+ }
147
+ }
148
+ [Symbol.dispose]() {
149
+ this.close();
150
+ }
129
151
  }
130
152
 
131
153
  // src/core/path.ts
@@ -162,16 +184,6 @@ function normalizePath(path) {
162
184
  }
163
185
  return normalized;
164
186
  }
165
- function isAncestorOf(ancestorPath, descendantPath) {
166
- if (ancestorPath === descendantPath) {
167
- return true;
168
- }
169
- const ancestorWithSep = ancestorPath.endsWith(sep) ? ancestorPath : ancestorPath + sep;
170
- if (isWindows()) {
171
- return descendantPath.toLowerCase().startsWith(ancestorWithSep.toLowerCase());
172
- }
173
- return descendantPath.startsWith(ancestorWithSep);
174
- }
175
187
  // src/core/resolver.ts
176
188
  class Resolver {
177
189
  storage;
@@ -185,8 +197,7 @@ class Resolver {
185
197
  async resolve(cwd) {
186
198
  const workingDir = cwd ?? process.cwd();
187
199
  const canonicalCwd = await canonicalize(workingDir, this.pathOptions);
188
- const allPaths = await this.storage.getAllPaths();
189
- const ancestorPaths = allPaths.filter((storedPath) => isAncestorOf(storedPath, canonicalCwd));
200
+ const ancestorPaths = await this.storage.getAncestorPaths(canonicalCwd);
190
201
  ancestorPaths.sort((a, b) => {
191
202
  if (isWindows()) {
192
203
  return a.toLowerCase().localeCompare(b.toLowerCase());
@@ -225,7 +236,7 @@ class Resolver {
225
236
  }
226
237
  }
227
238
  // src/core/formatter.ts
228
- var ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
239
+ var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
229
240
  function validateEnvKey(key) {
230
241
  return ENV_KEY_PATTERN.test(key);
231
242
  }
@@ -334,6 +345,12 @@ class BurrowClient {
334
345
  const canonicalPath = await canonicalize(targetPath, this.pathOptions);
335
346
  await this.storage.setSecret(canonicalPath, key, null);
336
347
  }
348
+ async remove(key, options = {}) {
349
+ assertValidEnvKey(key);
350
+ const targetPath = options.path ?? process.cwd();
351
+ const canonicalPath = await canonicalize(targetPath, this.pathOptions);
352
+ return this.storage.removeKey(canonicalPath, key);
353
+ }
337
354
  async export(options = {}) {
338
355
  const secrets = await this.resolver.resolve(options.cwd);
339
356
  const fmt = options.format ?? "shell";
@@ -344,6 +361,12 @@ class BurrowClient {
344
361
  async resolve(cwd) {
345
362
  return this.resolver.resolve(cwd);
346
363
  }
364
+ close() {
365
+ this.storage.close();
366
+ }
367
+ [Symbol.dispose]() {
368
+ this.close();
369
+ }
347
370
  }
348
371
  function createClient(options) {
349
372
  return new BurrowClient(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@captainsafia/burrow",
3
- "version": "1.0.0-preview.d773ac0",
3
+ "version": "1.0.0-preview.e3ae96a",
4
4
  "description": "Platform-agnostic, directory-scoped secrets manager",
5
5
  "type": "module",
6
6
  "main": "dist/api.js",