@captainsafia/burrow 0.1.0 → 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/LICENSE ADDED
@@ -0,0 +1,8 @@
1
+ Copyright 2025 Safia Abdalla
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
package/README.md CHANGED
@@ -1,15 +1,128 @@
1
1
  # burrow
2
2
 
3
- To install dependencies:
3
+ A platform-agnostic, directory-scoped secrets manager. Store secrets outside your repos, inherit them through directory ancestry.
4
+
5
+ ```
6
+ ~/projects/ # DATABASE_URL, API_KEY defined here
7
+ ├── app-a/ # inherits both secrets
8
+ ├── app-b/ # inherits both, overrides API_KEY
9
+ │ └── tests/ # blocks API_KEY (uses none)
10
+ └── app-c/ # inherits both secrets
11
+ ```
12
+
13
+ ## Installation
14
+
15
+ **Linux/macOS:**
16
+
17
+ ```bash
18
+ curl -fsSL https://safia.rocks/burrow/install.sh | sh
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### Set a secret
24
+
25
+ ```bash
26
+ burrow set API_KEY=sk-live-abc123
27
+ burrow set DATABASE_URL=postgres://localhost/mydb --path ~/projects
28
+ ```
29
+
30
+ ### Get a secret
31
+
32
+ ```bash
33
+ burrow get API_KEY --show
34
+ burrow get API_KEY --format json
35
+ ```
36
+
37
+ ### List all secrets
38
+
39
+ ```bash
40
+ burrow list
41
+ burrow list --format json
42
+ ```
43
+
44
+ ### Export to your shell
45
+
46
+ ```bash
47
+ eval "$(burrow export)"
48
+ eval "$(burrow export --format shell)" && npm start
49
+ ```
50
+
51
+ ### Block inheritance
52
+
53
+ ```bash
54
+ burrow unset API_KEY --path ~/projects/app/tests
55
+ ```
56
+
57
+ ### Remove a secret
4
58
 
5
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
+
65
+ ## How It Works
66
+
67
+ Secrets are stored in your user profile:
68
+ - **Linux/macOS:** `$XDG_CONFIG_HOME/burrow` or `~/.config/burrow`
69
+ - **Windows:** `%APPDATA%\burrow`
70
+
71
+ When you request secrets for a directory, burrow:
72
+
73
+ 1. Finds all ancestor paths with stored secrets
74
+ 2. Merges them from shallowest to deepest
75
+ 3. Deeper scopes override shallower ones
76
+ 4. Tombstones (from `unset`) block inheritance
77
+
78
+ ## Library Usage
79
+
80
+ Burrow also works as a TypeScript/JavaScript library:
81
+
82
+ ```typescript
83
+ import { BurrowClient } from '@captainsafia/burrow';
84
+
85
+ const client = new BurrowClient();
86
+
87
+ await client.set('API_KEY', 'secret123', { path: '/my/project' });
88
+
89
+ const secret = await client.get('API_KEY', { cwd: '/my/project/subdir' });
90
+ console.log(secret?.value); // 'secret123'
91
+ console.log(secret?.sourcePath); // '/my/project'
92
+
93
+ const allSecrets = await client.list({ cwd: '/my/project' });
94
+ ```
95
+
96
+ ## Contributing
97
+
98
+ ### Prerequisites
99
+
100
+ - [Bun](https://bun.sh) v1.0 or later
101
+
102
+ ### Setup
103
+
104
+ ```bash
105
+ git clone https://github.com/captainsafia/burrow.git
106
+ cd burrow
6
107
  bun install
7
108
  ```
8
109
 
9
- To run:
110
+ ### Development
10
111
 
11
112
  ```bash
12
- bun run index.ts
113
+ # Run tests
114
+ bun test
115
+
116
+ # Type check
117
+ bun run typecheck
118
+
119
+ # Build npm package
120
+ bun run build
121
+
122
+ # Compile binary
123
+ bun run compile
13
124
  ```
14
125
 
15
- This project was created using `bun init` in bun v1.3.4. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
126
+ ## License
127
+
128
+ MIT
package/dist/api.d.ts CHANGED
@@ -6,41 +6,359 @@ export interface ResolvedSecret {
6
6
  sourcePath: string;
7
7
  }
8
8
  export type ExportFormat = "shell" | "dotenv" | "json";
9
+ /**
10
+ * Configuration options for creating a BurrowClient instance.
11
+ */
9
12
  export interface BurrowClientOptions {
13
+ /**
14
+ * Custom directory for storing the secrets database.
15
+ * Defaults to platform-specific user config directory:
16
+ * - Linux/macOS: `$XDG_CONFIG_HOME/burrow` or `~/.config/burrow`
17
+ * - Windows: `%APPDATA%\burrow`
18
+ *
19
+ * Can also be set via the `BURROW_CONFIG_DIR` environment variable.
20
+ */
10
21
  configDir?: string;
22
+ /**
23
+ * Custom filename for the secrets store.
24
+ * Defaults to `store.json`.
25
+ */
11
26
  storeFileName?: string;
27
+ /**
28
+ * Whether to follow symlinks when canonicalizing paths.
29
+ * Defaults to `true`.
30
+ */
12
31
  followSymlinks?: boolean;
13
32
  }
33
+ /**
34
+ * Options for the `set` method.
35
+ */
14
36
  export interface SetOptions {
37
+ /**
38
+ * Directory path to scope the secret to.
39
+ * Defaults to the current working directory.
40
+ */
15
41
  path?: string;
16
42
  }
43
+ /**
44
+ * Options for the `get` method.
45
+ */
17
46
  export interface GetOptions {
47
+ /**
48
+ * Directory to resolve secrets from.
49
+ * Secrets are inherited from ancestor directories.
50
+ * Defaults to the current working directory.
51
+ */
18
52
  cwd?: string;
19
53
  }
54
+ /**
55
+ * Options for the `list` method.
56
+ */
20
57
  export interface ListOptions {
58
+ /**
59
+ * Directory to resolve secrets from.
60
+ * Secrets are inherited from ancestor directories.
61
+ * Defaults to the current working directory.
62
+ */
21
63
  cwd?: string;
22
64
  }
65
+ /**
66
+ * Options for the `block` method.
67
+ */
23
68
  export interface BlockOptions {
69
+ /**
70
+ * Directory path to scope the tombstone to.
71
+ * Defaults to the current working directory.
72
+ */
24
73
  path?: string;
25
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
+ }
85
+ /**
86
+ * Options for the `export` method.
87
+ */
26
88
  export interface ExportOptions {
89
+ /**
90
+ * Directory to resolve secrets from.
91
+ * Secrets are inherited from ancestor directories.
92
+ * Defaults to the current working directory.
93
+ */
27
94
  cwd?: string;
95
+ /**
96
+ * Output format for the exported secrets.
97
+ * - `shell`: Exports as `export KEY='value'` statements (default)
98
+ * - `dotenv`: Exports as `KEY="value"` lines
99
+ * - `json`: Exports as a JSON object
100
+ */
28
101
  format?: ExportFormat;
102
+ /**
103
+ * Whether to show actual values (currently unused, reserved for future use).
104
+ */
29
105
  showValues?: boolean;
106
+ /**
107
+ * Whether to include source paths in JSON output.
108
+ * When true, JSON output includes `{ key: { value, sourcePath } }` format.
109
+ * Only applies when format is `json`.
110
+ */
30
111
  includeSources?: boolean;
31
112
  }
113
+ /**
114
+ * Client for managing directory-scoped secrets.
115
+ *
116
+ * Secrets are stored outside your repository in the user's config directory
117
+ * and are scoped to filesystem paths. Child directories automatically inherit
118
+ * secrets from parent directories, with deeper scopes overriding shallower ones.
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * import { BurrowClient } from '@captainsafia/burrow';
123
+ *
124
+ * const client = new BurrowClient();
125
+ *
126
+ * // Set a secret scoped to a directory
127
+ * await client.set('API_KEY', 'sk-live-abc123', { path: '/projects/myapp' });
128
+ *
129
+ * // Get a secret (inherits from parent directories)
130
+ * const secret = await client.get('API_KEY', { cwd: '/projects/myapp/src' });
131
+ * console.log(secret?.value); // 'sk-live-abc123'
132
+ *
133
+ * // Export secrets for shell usage
134
+ * const shellExport = await client.export({ format: 'shell' });
135
+ * // Returns: export API_KEY='sk-live-abc123'
136
+ * ```
137
+ */
32
138
  export declare class BurrowClient {
33
139
  private readonly storage;
34
140
  private readonly resolver;
35
141
  private readonly pathOptions;
142
+ /**
143
+ * Creates a new BurrowClient instance.
144
+ *
145
+ * @param options - Configuration options for the client
146
+ */
36
147
  constructor(options?: BurrowClientOptions);
148
+ /**
149
+ * Sets a secret at the specified path scope.
150
+ *
151
+ * The secret will be available to the specified directory and all its
152
+ * subdirectories, unless overridden or blocked at a deeper level.
153
+ *
154
+ * @param key - Environment variable name. Must match `^[A-Z_][A-Z0-9_]*$`
155
+ * @param value - Secret value to store
156
+ * @param options - Set options including target path
157
+ * @throws Error if the key format is invalid
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * // Set at current directory
162
+ * await client.set('DATABASE_URL', 'postgres://localhost/mydb');
163
+ *
164
+ * // Set at specific path
165
+ * await client.set('API_KEY', 'secret', { path: '/projects/myapp' });
166
+ * ```
167
+ */
37
168
  set(key: string, value: string, options?: SetOptions): Promise<void>;
169
+ /**
170
+ * Gets a secret resolved through directory ancestry.
171
+ *
172
+ * Starting from the specified directory (or cwd), walks up the directory
173
+ * tree to find the nearest scope that defines the key. Deeper scopes
174
+ * override shallower ones.
175
+ *
176
+ * @param key - Environment variable name to retrieve
177
+ * @param options - Get options including working directory
178
+ * @returns The resolved secret with its source path, or undefined if not found
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * const secret = await client.get('API_KEY', { cwd: '/projects/myapp/src' });
183
+ * if (secret) {
184
+ * console.log(secret.value); // The secret value
185
+ * console.log(secret.sourcePath); // Path where it was defined
186
+ * }
187
+ * ```
188
+ */
38
189
  get(key: string, options?: GetOptions): Promise<ResolvedSecret | undefined>;
190
+ /**
191
+ * Lists all secrets resolved for a directory.
192
+ *
193
+ * Returns all secrets that would be available in the specified directory,
194
+ * including those inherited from parent directories. Each secret includes
195
+ * its source path indicating where it was defined.
196
+ *
197
+ * @param options - List options including working directory
198
+ * @returns Array of resolved secrets sorted by key name
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * const secrets = await client.list({ cwd: '/projects/myapp' });
203
+ * for (const secret of secrets) {
204
+ * console.log(`${secret.key} from ${secret.sourcePath}`);
205
+ * }
206
+ * ```
207
+ */
39
208
  list(options?: ListOptions): Promise<ResolvedSecret[]>;
209
+ /**
210
+ * Blocks a secret from being inherited at the specified path.
211
+ *
212
+ * Creates a "tombstone" that prevents the key from being inherited from
213
+ * parent directories. The block only affects the specified directory and
214
+ * its subdirectories. The secret remains available in parent directories.
215
+ *
216
+ * A blocked key can be re-enabled by calling `set` at the same or deeper path.
217
+ *
218
+ * @param key - Environment variable name to block. Must match `^[A-Z_][A-Z0-9_]*$`
219
+ * @param options - Block options including target path
220
+ * @throws Error if the key format is invalid
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * // Parent has API_KEY defined
225
+ * await client.set('API_KEY', 'prod-key', { path: '/projects' });
226
+ *
227
+ * // Block it in the test directory
228
+ * await client.block('API_KEY', { path: '/projects/myapp/tests' });
229
+ *
230
+ * // Now API_KEY won't resolve in /projects/myapp/tests or below
231
+ * const secret = await client.get('API_KEY', { cwd: '/projects/myapp/tests' });
232
+ * console.log(secret); // undefined
233
+ * ```
234
+ */
40
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>;
263
+ /**
264
+ * Exports resolved secrets in various formats.
265
+ *
266
+ * Generates a formatted string of all secrets resolved for the specified
267
+ * directory, suitable for shell evaluation or configuration files.
268
+ *
269
+ * @param options - Export options including format and working directory
270
+ * @returns Formatted string of secrets
271
+ *
272
+ * @example
273
+ * ```typescript
274
+ * // Shell format (default) - use with eval
275
+ * const shell = await client.export({ format: 'shell' });
276
+ * // Returns: export API_KEY='value'\nexport DB_URL='...'
277
+ *
278
+ * // Dotenv format - save to .env file
279
+ * const dotenv = await client.export({ format: 'dotenv' });
280
+ * // Returns: API_KEY="value"\nDB_URL="..."
281
+ *
282
+ * // JSON format - for programmatic use
283
+ * const json = await client.export({ format: 'json' });
284
+ * // Returns: { "API_KEY": "value", "DB_URL": "..." }
285
+ *
286
+ * // JSON with source paths
287
+ * const jsonWithSources = await client.export({
288
+ * format: 'json',
289
+ * includeSources: true
290
+ * });
291
+ * // Returns: { "API_KEY": { "value": "...", "sourcePath": "/..." } }
292
+ * ```
293
+ */
41
294
  export(options?: ExportOptions): Promise<string>;
295
+ /**
296
+ * Resolves all secrets for a directory as a Map.
297
+ *
298
+ * Lower-level method that returns the raw resolution result. Useful for
299
+ * programmatic access when you need to iterate over secrets or perform
300
+ * custom processing.
301
+ *
302
+ * @param cwd - Directory to resolve secrets from. Defaults to current working directory.
303
+ * @returns Map of key names to resolved secrets
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * const secrets = await client.resolve('/projects/myapp');
308
+ * for (const [key, secret] of secrets) {
309
+ * console.log(`${key}=${secret.value} (from ${secret.sourcePath})`);
310
+ * }
311
+ * ```
312
+ */
42
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;
43
344
  }
345
+ /**
346
+ * Creates a new BurrowClient instance.
347
+ *
348
+ * Convenience function equivalent to `new BurrowClient(options)`.
349
+ *
350
+ * @param options - Configuration options for the client
351
+ * @returns A new BurrowClient instance
352
+ *
353
+ * @example
354
+ * ```typescript
355
+ * import { createClient } from '@captainsafia/burrow';
356
+ *
357
+ * const client = createClient({
358
+ * configDir: '/custom/config/path'
359
+ * });
360
+ * ```
361
+ */
44
362
  export declare function createClient(options?: BurrowClientOptions): BurrowClient;
45
363
 
46
364
  export {};
package/dist/api.js CHANGED
@@ -1,13 +1,18 @@
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";
8
8
  import { join } from "node:path";
9
9
  var APP_NAME = "burrow";
10
+ var CONFIG_DIR_ENV = "BURROW_CONFIG_DIR";
10
11
  function getConfigDir() {
12
+ const envOverride = process.env[CONFIG_DIR_ENV];
13
+ if (envOverride) {
14
+ return envOverride;
15
+ }
11
16
  const platform = process.platform;
12
17
  if (platform === "win32") {
13
18
  return getWindowsConfigDir();
@@ -39,18 +44,12 @@ function isWindows() {
39
44
  }
40
45
 
41
46
  // src/storage/index.ts
42
- var STORE_VERSION = 1;
43
- var DEFAULT_STORE_FILE = "store.json";
44
- function createEmptyStore() {
45
- return {
46
- version: STORE_VERSION,
47
- paths: {}
48
- };
49
- }
47
+ var DEFAULT_STORE_FILE = "store.db";
50
48
 
51
49
  class Storage {
52
50
  configDir;
53
51
  storeFileName;
52
+ db = null;
54
53
  constructor(options = {}) {
55
54
  this.configDir = options.configDir ?? getConfigDir();
56
55
  this.storeFileName = options.storeFileName ?? DEFAULT_STORE_FILE;
@@ -58,68 +57,92 @@ class Storage {
58
57
  get storePath() {
59
58
  return join2(this.configDir, this.storeFileName);
60
59
  }
61
- async read() {
62
- try {
63
- const content = await readFile(this.storePath, "utf-8");
64
- const store = JSON.parse(content);
65
- if (store.version !== STORE_VERSION) {
66
- throw new Error(`Unsupported store version: ${store.version}. Expected: ${STORE_VERSION}`);
67
- }
68
- return store;
69
- } catch (error) {
70
- if (error.code === "ENOENT") {
71
- return createEmptyStore();
72
- }
73
- throw error;
60
+ async ensureDb() {
61
+ if (this.db) {
62
+ return this.db;
74
63
  }
75
- }
76
- async write(store) {
77
64
  await mkdir(this.configDir, { recursive: true });
78
- const tempFileName = `.store-${randomBytes(8).toString("hex")}.tmp`;
79
- const tempPath = join2(this.configDir, tempFileName);
80
- const content = JSON.stringify(store, null, 2);
81
- try {
82
- const file = Bun.file(tempPath);
83
- await Bun.write(file, content);
84
- await rename(tempPath, this.storePath);
85
- } catch (error) {
86
- try {
87
- await unlink(tempPath);
88
- } catch {}
89
- throw error;
65
+ if (!isWindows()) {
66
+ await chmod(this.configDir, 448);
90
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);
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;
91
91
  }
92
92
  async setSecret(canonicalPath, key, value) {
93
- const store = await this.read();
94
- if (!store.paths[canonicalPath]) {
95
- store.paths[canonicalPath] = {};
96
- }
97
- store.paths[canonicalPath][key] = {
98
- value,
99
- updatedAt: new Date().toISOString()
100
- };
101
- 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);
102
102
  }
103
103
  async getPathSecrets(canonicalPath) {
104
- const store = await this.read();
105
- 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;
106
117
  }
107
118
  async getAllPaths() {
108
- const store = await this.read();
109
- 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);
110
127
  }
111
128
  async removeKey(canonicalPath, key) {
112
- const store = await this.read();
113
- 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) {
114
132
  return false;
115
133
  }
116
- delete store.paths[canonicalPath][key];
117
- if (Object.keys(store.paths[canonicalPath]).length === 0) {
118
- delete store.paths[canonicalPath];
119
- }
120
- await this.write(store);
134
+ db.query("DELETE FROM secrets WHERE path = ? AND key = ?").run(canonicalPath, key);
121
135
  return true;
122
136
  }
137
+ close() {
138
+ if (this.db) {
139
+ this.db.close();
140
+ this.db = null;
141
+ }
142
+ }
143
+ [Symbol.dispose]() {
144
+ this.close();
145
+ }
123
146
  }
124
147
 
125
148
  // src/core/path.ts
@@ -156,25 +179,12 @@ function normalizePath(path) {
156
179
  }
157
180
  return normalized;
158
181
  }
159
- function isAncestorOf(ancestorPath, descendantPath) {
160
- if (ancestorPath === descendantPath) {
161
- return true;
162
- }
163
- const ancestorWithSep = ancestorPath.endsWith(sep) ? ancestorPath : ancestorPath + sep;
164
- if (isWindows()) {
165
- return descendantPath.toLowerCase().startsWith(ancestorWithSep.toLowerCase());
166
- }
167
- return descendantPath.startsWith(ancestorWithSep);
168
- }
169
182
  // src/core/resolver.ts
170
183
  class Resolver {
171
184
  storage;
172
185
  pathOptions;
173
- constructor(options = {}) {
174
- this.storage = new Storage({
175
- configDir: options.configDir,
176
- storeFileName: options.storeFileName
177
- });
186
+ constructor(options) {
187
+ this.storage = options.storage;
178
188
  this.pathOptions = {
179
189
  followSymlinks: options.followSymlinks
180
190
  };
@@ -182,8 +192,7 @@ class Resolver {
182
192
  async resolve(cwd) {
183
193
  const workingDir = cwd ?? process.cwd();
184
194
  const canonicalCwd = await canonicalize(workingDir, this.pathOptions);
185
- const allPaths = await this.storage.getAllPaths();
186
- const ancestorPaths = allPaths.filter((storedPath) => isAncestorOf(storedPath, canonicalCwd));
195
+ const ancestorPaths = await this.storage.getAncestorPaths(canonicalCwd);
187
196
  ancestorPaths.sort((a, b) => {
188
197
  if (isWindows()) {
189
198
  return a.toLowerCase().localeCompare(b.toLowerCase());
@@ -222,7 +231,7 @@ class Resolver {
222
231
  }
223
232
  }
224
233
  // src/core/formatter.ts
225
- var ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
234
+ var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
226
235
  function validateEnvKey(key) {
227
236
  return ENV_KEY_PATTERN.test(key);
228
237
  }
@@ -306,8 +315,7 @@ class BurrowClient {
306
315
  storeFileName: options.storeFileName
307
316
  });
308
317
  this.resolver = new Resolver({
309
- configDir: options.configDir,
310
- storeFileName: options.storeFileName,
318
+ storage: this.storage,
311
319
  followSymlinks: options.followSymlinks
312
320
  });
313
321
  this.pathOptions = {
@@ -332,6 +340,12 @@ class BurrowClient {
332
340
  const canonicalPath = await canonicalize(targetPath, this.pathOptions);
333
341
  await this.storage.setSecret(canonicalPath, key, null);
334
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
+ }
335
349
  async export(options = {}) {
336
350
  const secrets = await this.resolver.resolve(options.cwd);
337
351
  const fmt = options.format ?? "shell";
@@ -342,6 +356,12 @@ class BurrowClient {
342
356
  async resolve(cwd) {
343
357
  return this.resolver.resolve(cwd);
344
358
  }
359
+ close() {
360
+ this.storage.close();
361
+ }
362
+ [Symbol.dispose]() {
363
+ this.close();
364
+ }
345
365
  }
346
366
  function createClient(options) {
347
367
  return new BurrowClient(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@captainsafia/burrow",
3
- "version": "0.1.0",
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",
@@ -49,5 +49,8 @@
49
49
  "direnv",
50
50
  "configuration"
51
51
  ],
52
- "license": "MIT"
53
- }
52
+ "license": "MIT",
53
+ "dependencies": {
54
+ "commander": "^14.0.2"
55
+ }
56
+ }