@captainsafia/burrow 1.0.0-preview.ddd1e9d → 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,6 +1,6 @@
1
1
  // src/storage/index.ts
2
2
  import { Database } from "bun:sqlite";
3
- import { mkdir } from "node:fs/promises";
3
+ import { chmod, mkdir } from "node:fs/promises";
4
4
  import { join as join2 } from "node:path";
5
5
 
6
6
  // src/platform/index.ts
@@ -62,7 +62,13 @@ class Storage {
62
62
  return this.db;
63
63
  }
64
64
  await mkdir(this.configDir, { recursive: true });
65
+ if (!isWindows()) {
66
+ await chmod(this.configDir, 448);
67
+ }
65
68
  this.db = new Database(this.storePath);
69
+ if (!isWindows()) {
70
+ await chmod(this.storePath, 384);
71
+ }
66
72
  this.db.run("PRAGMA journal_mode = WAL");
67
73
  this.db.run(`
68
74
  CREATE TABLE IF NOT EXISTS secrets (
@@ -114,6 +120,16 @@ class Storage {
114
120
  const rows = db.query("SELECT DISTINCT path FROM secrets").all();
115
121
  return rows.map((row) => row.path);
116
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);
132
+ }
117
133
  async removeKey(canonicalPath, key) {
118
134
  const db = await this.ensureDb();
119
135
  const existing = db.query("SELECT path FROM secrets WHERE path = ? AND key = ?").get(canonicalPath, key);
@@ -123,6 +139,15 @@ class Storage {
123
139
  db.query("DELETE FROM secrets WHERE path = ? AND key = ?").run(canonicalPath, key);
124
140
  return true;
125
141
  }
142
+ close() {
143
+ if (this.db) {
144
+ this.db.close();
145
+ this.db = null;
146
+ }
147
+ }
148
+ [Symbol.dispose]() {
149
+ this.close();
150
+ }
126
151
  }
127
152
 
128
153
  // src/core/path.ts
@@ -159,16 +184,6 @@ function normalizePath(path) {
159
184
  }
160
185
  return normalized;
161
186
  }
162
- function isAncestorOf(ancestorPath, descendantPath) {
163
- if (ancestorPath === descendantPath) {
164
- return true;
165
- }
166
- const ancestorWithSep = ancestorPath.endsWith(sep) ? ancestorPath : ancestorPath + sep;
167
- if (isWindows()) {
168
- return descendantPath.toLowerCase().startsWith(ancestorWithSep.toLowerCase());
169
- }
170
- return descendantPath.startsWith(ancestorWithSep);
171
- }
172
187
  // src/core/resolver.ts
173
188
  class Resolver {
174
189
  storage;
@@ -182,8 +197,7 @@ class Resolver {
182
197
  async resolve(cwd) {
183
198
  const workingDir = cwd ?? process.cwd();
184
199
  const canonicalCwd = await canonicalize(workingDir, this.pathOptions);
185
- const allPaths = await this.storage.getAllPaths();
186
- const ancestorPaths = allPaths.filter((storedPath) => isAncestorOf(storedPath, canonicalCwd));
200
+ const ancestorPaths = await this.storage.getAncestorPaths(canonicalCwd);
187
201
  ancestorPaths.sort((a, b) => {
188
202
  if (isWindows()) {
189
203
  return a.toLowerCase().localeCompare(b.toLowerCase());
@@ -222,7 +236,7 @@ class Resolver {
222
236
  }
223
237
  }
224
238
  // src/core/formatter.ts
225
- var ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
239
+ var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
226
240
  function validateEnvKey(key) {
227
241
  return ENV_KEY_PATTERN.test(key);
228
242
  }
@@ -331,6 +345,12 @@ class BurrowClient {
331
345
  const canonicalPath = await canonicalize(targetPath, this.pathOptions);
332
346
  await this.storage.setSecret(canonicalPath, key, null);
333
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
+ }
334
354
  async export(options = {}) {
335
355
  const secrets = await this.resolver.resolve(options.cwd);
336
356
  const fmt = options.format ?? "shell";
@@ -341,6 +361,12 @@ class BurrowClient {
341
361
  async resolve(cwd) {
342
362
  return this.resolver.resolve(cwd);
343
363
  }
364
+ close() {
365
+ this.storage.close();
366
+ }
367
+ [Symbol.dispose]() {
368
+ this.close();
369
+ }
344
370
  }
345
371
  function createClient(options) {
346
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.ddd1e9d",
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",