@captainsafia/burrow 1.0.0-preview.0a24dbc → 1.0.0-preview.0d335f8

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
@@ -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:
package/dist/api.d.ts CHANGED
@@ -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
  */
@@ -223,6 +233,33 @@ export declare class BurrowClient {
223
233
  * ```
224
234
  */
225
235
  block(key: string, options?: BlockOptions): Promise<void>;
236
+ /**
237
+ * Removes a secret entry entirely from the specified path.
238
+ *
239
+ * Unlike `block`, which creates a tombstone to prevent inheritance,
240
+ * `remove` completely deletes the secret entry. After removal, the key
241
+ * may still be inherited from parent directories if defined there.
242
+ *
243
+ * @param key - Environment variable name to remove. Must match `^[A-Z_][A-Z0-9_]*$`
244
+ * @param options - Remove options including target path
245
+ * @returns true if the secret was found and removed, false if it didn't exist
246
+ * @throws Error if the key format is invalid
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * // Set a secret
251
+ * await client.set('API_KEY', 'secret', { path: '/projects/myapp' });
252
+ *
253
+ * // Remove it entirely
254
+ * const removed = await client.remove('API_KEY', { path: '/projects/myapp' });
255
+ * console.log(removed); // true
256
+ *
257
+ * // Trying to remove again returns false
258
+ * const removedAgain = await client.remove('API_KEY', { path: '/projects/myapp' });
259
+ * console.log(removedAgain); // false
260
+ * ```
261
+ */
262
+ remove(key: string, options?: RemoveOptions): Promise<boolean>;
226
263
  /**
227
264
  * Exports resolved secrets in various formats.
228
265
  *
@@ -274,6 +311,36 @@ export declare class BurrowClient {
274
311
  * ```
275
312
  */
276
313
  resolve(cwd?: string): Promise<Map<string, ResolvedSecret>>;
314
+ /**
315
+ * Closes the database connection and releases resources.
316
+ * After calling this method, the client instance should not be used.
317
+ *
318
+ * This method is safe to call multiple times.
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * const client = new BurrowClient();
323
+ * try {
324
+ * await client.set('API_KEY', 'value');
325
+ * // ... do work
326
+ * } finally {
327
+ * client.close();
328
+ * }
329
+ * ```
330
+ */
331
+ close(): void;
332
+ /**
333
+ * Allows using the BurrowClient with `using` declarations for automatic cleanup.
334
+ *
335
+ * @example
336
+ * ```typescript
337
+ * {
338
+ * using client = new BurrowClient();
339
+ * await client.set('API_KEY', 'value');
340
+ * } // client.close() is called automatically
341
+ * ```
342
+ */
343
+ [Symbol.dispose](): void;
277
344
  }
278
345
  /**
279
346
  * 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,92 @@ 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
+ this.db.run("PRAGMA journal_mode = WAL");
70
+ if (!isWindows()) {
71
+ await chmod(this.storePath, 384);
95
72
  }
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
+ const rows = db.query("SELECT DISTINCT path FROM secrets WHERE ? = path OR ? LIKE path || '/' || '%' OR path = '/'").all(canonicalPath, canonicalPath);
126
+ return rows.map((row) => row.path);
115
127
  }
116
128
  async removeKey(canonicalPath, key) {
117
- const store = await this.read();
118
- if (!store.paths[canonicalPath]?.[key]) {
129
+ const db = await this.ensureDb();
130
+ const existing = db.query("SELECT path FROM secrets WHERE path = ? AND key = ?").get(canonicalPath, key);
131
+ if (!existing) {
119
132
  return false;
120
133
  }
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);
134
+ db.query("DELETE FROM secrets WHERE path = ? AND key = ?").run(canonicalPath, key);
126
135
  return true;
127
136
  }
137
+ close() {
138
+ if (this.db) {
139
+ this.db.close();
140
+ this.db = null;
141
+ }
142
+ }
143
+ [Symbol.dispose]() {
144
+ this.close();
145
+ }
128
146
  }
129
147
 
130
148
  // src/core/path.ts
@@ -161,16 +179,6 @@ function normalizePath(path) {
161
179
  }
162
180
  return normalized;
163
181
  }
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
182
  // src/core/resolver.ts
175
183
  class Resolver {
176
184
  storage;
@@ -184,8 +192,7 @@ class Resolver {
184
192
  async resolve(cwd) {
185
193
  const workingDir = cwd ?? process.cwd();
186
194
  const canonicalCwd = await canonicalize(workingDir, this.pathOptions);
187
- const allPaths = await this.storage.getAllPaths();
188
- const ancestorPaths = allPaths.filter((storedPath) => isAncestorOf(storedPath, canonicalCwd));
195
+ const ancestorPaths = await this.storage.getAncestorPaths(canonicalCwd);
189
196
  ancestorPaths.sort((a, b) => {
190
197
  if (isWindows()) {
191
198
  return a.toLowerCase().localeCompare(b.toLowerCase());
@@ -224,7 +231,7 @@ class Resolver {
224
231
  }
225
232
  }
226
233
  // src/core/formatter.ts
227
- var ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
234
+ var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
228
235
  function validateEnvKey(key) {
229
236
  return ENV_KEY_PATTERN.test(key);
230
237
  }
@@ -333,6 +340,12 @@ class BurrowClient {
333
340
  const canonicalPath = await canonicalize(targetPath, this.pathOptions);
334
341
  await this.storage.setSecret(canonicalPath, key, null);
335
342
  }
343
+ async remove(key, options = {}) {
344
+ assertValidEnvKey(key);
345
+ const targetPath = options.path ?? process.cwd();
346
+ const canonicalPath = await canonicalize(targetPath, this.pathOptions);
347
+ return this.storage.removeKey(canonicalPath, key);
348
+ }
336
349
  async export(options = {}) {
337
350
  const secrets = await this.resolver.resolve(options.cwd);
338
351
  const fmt = options.format ?? "shell";
@@ -343,6 +356,12 @@ class BurrowClient {
343
356
  async resolve(cwd) {
344
357
  return this.resolver.resolve(cwd);
345
358
  }
359
+ close() {
360
+ this.storage.close();
361
+ }
362
+ [Symbol.dispose]() {
363
+ this.close();
364
+ }
346
365
  }
347
366
  function createClient(options) {
348
367
  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.0d335f8",
4
4
  "description": "Platform-agnostic, directory-scoped secrets manager",
5
5
  "type": "module",
6
6
  "main": "dist/api.js",