@captainsafia/burrow 1.0.0-preview.ddd1e9d → 1.0.0-preview.f26ef28
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 +34 -7
- package/dist/api.d.ts +71 -8
- package/dist/api.js +86 -14
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
const allSecrets = await client.list({ cwd: '/my/project' });
|
|
101
|
+
} finally {
|
|
102
|
+
client.close(); // Clean up database connection
|
|
103
|
+
}
|
|
104
|
+
```
|
|
84
105
|
|
|
85
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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
|
}
|
|
@@ -237,6 +251,9 @@ function escapeShellValue(value) {
|
|
|
237
251
|
function escapeDoubleQuotes(value) {
|
|
238
252
|
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
239
253
|
}
|
|
254
|
+
function escapePowerShellValue(value) {
|
|
255
|
+
return value.replace(/'/g, "''");
|
|
256
|
+
}
|
|
240
257
|
function formatShell(secrets) {
|
|
241
258
|
const lines = [];
|
|
242
259
|
const sortedKeys = Array.from(secrets.keys()).sort();
|
|
@@ -249,6 +266,42 @@ function formatShell(secrets) {
|
|
|
249
266
|
return lines.join(`
|
|
250
267
|
`);
|
|
251
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
|
+
}
|
|
252
305
|
function formatDotenv(secrets) {
|
|
253
306
|
const lines = [];
|
|
254
307
|
const sortedKeys = Array.from(secrets.keys()).sort();
|
|
@@ -286,7 +339,14 @@ function formatJson(secrets, includeSources = false) {
|
|
|
286
339
|
function format(secrets, fmt, options = {}) {
|
|
287
340
|
switch (fmt) {
|
|
288
341
|
case "shell":
|
|
342
|
+
case "bash":
|
|
289
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);
|
|
290
350
|
case "dotenv":
|
|
291
351
|
return formatDotenv(secrets);
|
|
292
352
|
case "json":
|
|
@@ -331,6 +391,12 @@ class BurrowClient {
|
|
|
331
391
|
const canonicalPath = await canonicalize(targetPath, this.pathOptions);
|
|
332
392
|
await this.storage.setSecret(canonicalPath, key, null);
|
|
333
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
|
+
}
|
|
334
400
|
async export(options = {}) {
|
|
335
401
|
const secrets = await this.resolver.resolve(options.cwd);
|
|
336
402
|
const fmt = options.format ?? "shell";
|
|
@@ -341,6 +407,12 @@ class BurrowClient {
|
|
|
341
407
|
async resolve(cwd) {
|
|
342
408
|
return this.resolver.resolve(cwd);
|
|
343
409
|
}
|
|
410
|
+
close() {
|
|
411
|
+
this.storage.close();
|
|
412
|
+
}
|
|
413
|
+
[Symbol.dispose]() {
|
|
414
|
+
this.close();
|
|
415
|
+
}
|
|
344
416
|
}
|
|
345
417
|
function createClient(options) {
|
|
346
418
|
return new BurrowClient(options);
|