@captainsafia/burrow 1.0.0-preview.0a24dbc → 1.0.0-preview.2be53e0

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
 
@@ -44,8 +44,14 @@ burrow list --format json
44
44
  ### Export to your shell
45
45
 
46
46
  ```bash
47
+ # Auto-detects your shell (bash, fish, powershell, cmd)
47
48
  eval "$(burrow export)"
48
- eval "$(burrow export --format shell)" && npm start
49
+
50
+ # Or specify a format explicitly
51
+ burrow export --format fish
52
+ burrow export --format powershell
53
+ burrow export --format dotenv
54
+ burrow export --format json
49
55
  ```
50
56
 
51
57
  ### Block inheritance
@@ -54,6 +60,14 @@ eval "$(burrow export --format shell)" && npm start
54
60
  burrow unset API_KEY --path ~/projects/app/tests
55
61
  ```
56
62
 
63
+ ### Remove a secret
64
+
65
+ ```bash
66
+ burrow remove API_KEY --path ~/projects/app
67
+ ```
68
+
69
+ Unlike `unset` which blocks inheritance, `remove` deletes the entry entirely, restoring inheritance from parent directories.
70
+
57
71
  ## How It Works
58
72
 
59
73
  Secrets are stored in your user profile:
@@ -76,13 +90,26 @@ import { BurrowClient } from '@captainsafia/burrow';
76
90
 
77
91
  const client = new BurrowClient();
78
92
 
79
- await client.set('API_KEY', 'secret123', { path: '/my/project' });
93
+ try {
94
+ await client.set('API_KEY', 'secret123', { path: '/my/project' });
95
+
96
+ const secret = await client.get('API_KEY', { cwd: '/my/project/subdir' });
97
+ console.log(secret?.value); // 'secret123'
98
+ console.log(secret?.sourcePath); // '/my/project'
80
99
 
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'
100
+ const allSecrets = await client.list({ cwd: '/my/project' });
101
+ } finally {
102
+ client.close(); // Clean up database connection
103
+ }
104
+ ```
84
105
 
85
- const allSecrets = await client.list({ cwd: '/my/project' });
106
+ Or with TypeScript's `using` declarations for automatic cleanup:
107
+
108
+ ```typescript
109
+ {
110
+ using client = new BurrowClient();
111
+ await client.set('API_KEY', 'secret123');
112
+ } // Automatically cleaned up
86
113
  ```
87
114
 
88
115
  ## Contributing
package/dist/api.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface ResolvedSecret {
5
5
  value: string;
6
6
  sourcePath: string;
7
7
  }
8
- export type ExportFormat = "shell" | "dotenv" | "json";
8
+ export type ExportFormat = "shell" | "bash" | "fish" | "powershell" | "cmd" | "dotenv" | "json";
9
9
  /**
10
10
  * Configuration options for creating a BurrowClient instance.
11
11
  */
@@ -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, readFile, 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,68 +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 content = await readFile(this.storePath, "utf-8");
69
- const store = JSON.parse(content);
70
- if (store.version !== STORE_VERSION) {
71
- throw new Error(`Unsupported store version: ${store.version}. Expected: ${STORE_VERSION}`);
72
- }
73
- return store;
74
- } catch (error) {
75
- if (error.code === "ENOENT") {
76
- return createEmptyStore();
77
- }
78
- throw error;
60
+ async ensureDb() {
61
+ if (this.db) {
62
+ return this.db;
79
63
  }
80
- }
81
- async write(store) {
82
64
  await mkdir(this.configDir, { recursive: true });
83
- const tempFileName = `.store-${randomBytes(8).toString("hex")}.tmp`;
84
- const tempPath = join2(this.configDir, tempFileName);
85
- const content = JSON.stringify(store, null, 2);
86
- try {
87
- const file = Bun.file(tempPath);
88
- await Bun.write(file, content);
89
- await rename(tempPath, this.storePath);
90
- } catch (error) {
91
- try {
92
- await unlink(tempPath);
93
- } catch {}
94
- 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);
95
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;
96
91
  }
97
92
  async setSecret(canonicalPath, key, value) {
98
- const store = await this.read();
99
- if (!store.paths[canonicalPath]) {
100
- store.paths[canonicalPath] = {};
101
- }
102
- store.paths[canonicalPath][key] = {
103
- value,
104
- updatedAt: new Date().toISOString()
105
- };
106
- 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);
107
102
  }
108
103
  async getPathSecrets(canonicalPath) {
109
- const store = await this.read();
110
- 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;
111
117
  }
112
118
  async getAllPaths() {
113
- const store = await this.read();
114
- 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);
115
132
  }
116
133
  async removeKey(canonicalPath, key) {
117
- const store = await this.read();
118
- 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) {
119
137
  return false;
120
138
  }
121
- delete store.paths[canonicalPath][key];
122
- if (Object.keys(store.paths[canonicalPath]).length === 0) {
123
- delete store.paths[canonicalPath];
124
- }
125
- await this.write(store);
139
+ db.query("DELETE FROM secrets WHERE path = ? AND key = ?").run(canonicalPath, key);
126
140
  return true;
127
141
  }
142
+ close() {
143
+ if (this.db) {
144
+ this.db.close();
145
+ this.db = null;
146
+ }
147
+ }
148
+ [Symbol.dispose]() {
149
+ this.close();
150
+ }
128
151
  }
129
152
 
130
153
  // src/core/path.ts
@@ -161,16 +184,6 @@ function normalizePath(path) {
161
184
  }
162
185
  return normalized;
163
186
  }
164
- function isAncestorOf(ancestorPath, descendantPath) {
165
- if (ancestorPath === descendantPath) {
166
- return true;
167
- }
168
- const ancestorWithSep = ancestorPath.endsWith(sep) ? ancestorPath : ancestorPath + sep;
169
- if (isWindows()) {
170
- return descendantPath.toLowerCase().startsWith(ancestorWithSep.toLowerCase());
171
- }
172
- return descendantPath.startsWith(ancestorWithSep);
173
- }
174
187
  // src/core/resolver.ts
175
188
  class Resolver {
176
189
  storage;
@@ -184,8 +197,7 @@ class Resolver {
184
197
  async resolve(cwd) {
185
198
  const workingDir = cwd ?? process.cwd();
186
199
  const canonicalCwd = await canonicalize(workingDir, this.pathOptions);
187
- const allPaths = await this.storage.getAllPaths();
188
- const ancestorPaths = allPaths.filter((storedPath) => isAncestorOf(storedPath, canonicalCwd));
200
+ const ancestorPaths = await this.storage.getAncestorPaths(canonicalCwd);
189
201
  ancestorPaths.sort((a, b) => {
190
202
  if (isWindows()) {
191
203
  return a.toLowerCase().localeCompare(b.toLowerCase());
@@ -224,7 +236,7 @@ class Resolver {
224
236
  }
225
237
  }
226
238
  // src/core/formatter.ts
227
- var ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
239
+ var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
228
240
  function validateEnvKey(key) {
229
241
  return ENV_KEY_PATTERN.test(key);
230
242
  }
@@ -239,6 +251,9 @@ function escapeShellValue(value) {
239
251
  function escapeDoubleQuotes(value) {
240
252
  return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
241
253
  }
254
+ function escapePowerShellValue(value) {
255
+ return value.replace(/'/g, "''");
256
+ }
242
257
  function formatShell(secrets) {
243
258
  const lines = [];
244
259
  const sortedKeys = Array.from(secrets.keys()).sort();
@@ -251,6 +266,42 @@ function formatShell(secrets) {
251
266
  return lines.join(`
252
267
  `);
253
268
  }
269
+ function formatFish(secrets) {
270
+ const lines = [];
271
+ const sortedKeys = Array.from(secrets.keys()).sort();
272
+ for (const key of sortedKeys) {
273
+ const secret = secrets.get(key);
274
+ assertValidEnvKey(key);
275
+ const escapedValue = escapeShellValue(secret.value);
276
+ lines.push(`set -gx ${key} '${escapedValue}'`);
277
+ }
278
+ return lines.join(`
279
+ `);
280
+ }
281
+ function formatPowerShell(secrets) {
282
+ const lines = [];
283
+ const sortedKeys = Array.from(secrets.keys()).sort();
284
+ for (const key of sortedKeys) {
285
+ const secret = secrets.get(key);
286
+ assertValidEnvKey(key);
287
+ const escapedValue = escapePowerShellValue(secret.value);
288
+ lines.push(`$env:${key} = '${escapedValue}'`);
289
+ }
290
+ return lines.join(`
291
+ `);
292
+ }
293
+ function formatCmd(secrets) {
294
+ const lines = [];
295
+ const sortedKeys = Array.from(secrets.keys()).sort();
296
+ for (const key of sortedKeys) {
297
+ const secret = secrets.get(key);
298
+ assertValidEnvKey(key);
299
+ const escapedValue = secret.value.replace(/([&|<>^])/g, "^$1");
300
+ lines.push(`set ${key}=${escapedValue}`);
301
+ }
302
+ return lines.join(`
303
+ `);
304
+ }
254
305
  function formatDotenv(secrets) {
255
306
  const lines = [];
256
307
  const sortedKeys = Array.from(secrets.keys()).sort();
@@ -288,7 +339,14 @@ function formatJson(secrets, includeSources = false) {
288
339
  function format(secrets, fmt, options = {}) {
289
340
  switch (fmt) {
290
341
  case "shell":
342
+ case "bash":
291
343
  return formatShell(secrets);
344
+ case "fish":
345
+ return formatFish(secrets);
346
+ case "powershell":
347
+ return formatPowerShell(secrets);
348
+ case "cmd":
349
+ return formatCmd(secrets);
292
350
  case "dotenv":
293
351
  return formatDotenv(secrets);
294
352
  case "json":
@@ -333,6 +391,12 @@ class BurrowClient {
333
391
  const canonicalPath = await canonicalize(targetPath, this.pathOptions);
334
392
  await this.storage.setSecret(canonicalPath, key, null);
335
393
  }
394
+ async remove(key, options = {}) {
395
+ assertValidEnvKey(key);
396
+ const targetPath = options.path ?? process.cwd();
397
+ const canonicalPath = await canonicalize(targetPath, this.pathOptions);
398
+ return this.storage.removeKey(canonicalPath, key);
399
+ }
336
400
  async export(options = {}) {
337
401
  const secrets = await this.resolver.resolve(options.cwd);
338
402
  const fmt = options.format ?? "shell";
@@ -343,6 +407,12 @@ class BurrowClient {
343
407
  async resolve(cwd) {
344
408
  return this.resolver.resolve(cwd);
345
409
  }
410
+ close() {
411
+ this.storage.close();
412
+ }
413
+ [Symbol.dispose]() {
414
+ this.close();
415
+ }
346
416
  }
347
417
  function createClient(options) {
348
418
  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.0a24dbc",
3
+ "version": "1.0.0-preview.2be53e0",
4
4
  "description": "Platform-agnostic, directory-scoped secrets manager",
5
5
  "type": "module",
6
6
  "main": "dist/api.js",