@captainsafia/burrow 1.0.0-preview.d773ac0 → 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 +27 -6
- package/dist/api.d.ts +70 -7
- package/dist/api.js +94 -71
- 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
|
|
|
@@ -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
|
-
|
|
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.
|
|
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,7 +1,7 @@
|
|
|
1
1
|
// src/storage/index.ts
|
|
2
|
-
import {
|
|
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
|
|
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,69 +57,97 @@ class Storage {
|
|
|
63
57
|
get storePath() {
|
|
64
58
|
return join2(this.configDir, this.storeFileName);
|
|
65
59
|
}
|
|
66
|
-
async
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const content = await file.text();
|
|
70
|
-
const store = JSON.parse(content);
|
|
71
|
-
if (store.version !== STORE_VERSION) {
|
|
72
|
-
throw new Error(`Unsupported store version: ${store.version}. Expected: ${STORE_VERSION}`);
|
|
73
|
-
}
|
|
74
|
-
return store;
|
|
75
|
-
} catch (error) {
|
|
76
|
-
if (error.code === "ENOENT") {
|
|
77
|
-
return createEmptyStore();
|
|
78
|
-
}
|
|
79
|
-
throw error;
|
|
60
|
+
async ensureDb() {
|
|
61
|
+
if (this.db) {
|
|
62
|
+
return this.db;
|
|
80
63
|
}
|
|
81
|
-
}
|
|
82
|
-
async write(store) {
|
|
83
64
|
await mkdir(this.configDir, { recursive: true });
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
await
|
|
90
|
-
await rename(tempPath, this.storePath);
|
|
91
|
-
} catch (error) {
|
|
92
|
-
try {
|
|
93
|
-
await unlink(tempPath);
|
|
94
|
-
} catch {}
|
|
95
|
-
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);
|
|
96
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;
|
|
97
91
|
}
|
|
98
92
|
async setSecret(canonicalPath, key, value) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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);
|
|
108
102
|
}
|
|
109
103
|
async getPathSecrets(canonicalPath) {
|
|
110
|
-
const
|
|
111
|
-
|
|
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;
|
|
112
117
|
}
|
|
113
118
|
async getAllPaths() {
|
|
114
|
-
const
|
|
115
|
-
|
|
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);
|
|
116
132
|
}
|
|
117
133
|
async removeKey(canonicalPath, key) {
|
|
118
|
-
const
|
|
119
|
-
|
|
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) {
|
|
120
137
|
return false;
|
|
121
138
|
}
|
|
122
|
-
|
|
123
|
-
if (Object.keys(store.paths[canonicalPath]).length === 0) {
|
|
124
|
-
delete store.paths[canonicalPath];
|
|
125
|
-
}
|
|
126
|
-
await this.write(store);
|
|
139
|
+
db.query("DELETE FROM secrets WHERE path = ? AND key = ?").run(canonicalPath, key);
|
|
127
140
|
return true;
|
|
128
141
|
}
|
|
142
|
+
close() {
|
|
143
|
+
if (this.db) {
|
|
144
|
+
this.db.close();
|
|
145
|
+
this.db = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
[Symbol.dispose]() {
|
|
149
|
+
this.close();
|
|
150
|
+
}
|
|
129
151
|
}
|
|
130
152
|
|
|
131
153
|
// src/core/path.ts
|
|
@@ -162,16 +184,6 @@ function normalizePath(path) {
|
|
|
162
184
|
}
|
|
163
185
|
return normalized;
|
|
164
186
|
}
|
|
165
|
-
function isAncestorOf(ancestorPath, descendantPath) {
|
|
166
|
-
if (ancestorPath === descendantPath) {
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
const ancestorWithSep = ancestorPath.endsWith(sep) ? ancestorPath : ancestorPath + sep;
|
|
170
|
-
if (isWindows()) {
|
|
171
|
-
return descendantPath.toLowerCase().startsWith(ancestorWithSep.toLowerCase());
|
|
172
|
-
}
|
|
173
|
-
return descendantPath.startsWith(ancestorWithSep);
|
|
174
|
-
}
|
|
175
187
|
// src/core/resolver.ts
|
|
176
188
|
class Resolver {
|
|
177
189
|
storage;
|
|
@@ -185,8 +197,7 @@ class Resolver {
|
|
|
185
197
|
async resolve(cwd) {
|
|
186
198
|
const workingDir = cwd ?? process.cwd();
|
|
187
199
|
const canonicalCwd = await canonicalize(workingDir, this.pathOptions);
|
|
188
|
-
const
|
|
189
|
-
const ancestorPaths = allPaths.filter((storedPath) => isAncestorOf(storedPath, canonicalCwd));
|
|
200
|
+
const ancestorPaths = await this.storage.getAncestorPaths(canonicalCwd);
|
|
190
201
|
ancestorPaths.sort((a, b) => {
|
|
191
202
|
if (isWindows()) {
|
|
192
203
|
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
@@ -225,7 +236,7 @@ class Resolver {
|
|
|
225
236
|
}
|
|
226
237
|
}
|
|
227
238
|
// src/core/formatter.ts
|
|
228
|
-
var ENV_KEY_PATTERN = /^[A-
|
|
239
|
+
var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
229
240
|
function validateEnvKey(key) {
|
|
230
241
|
return ENV_KEY_PATTERN.test(key);
|
|
231
242
|
}
|
|
@@ -334,6 +345,12 @@ class BurrowClient {
|
|
|
334
345
|
const canonicalPath = await canonicalize(targetPath, this.pathOptions);
|
|
335
346
|
await this.storage.setSecret(canonicalPath, key, null);
|
|
336
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
|
+
}
|
|
337
354
|
async export(options = {}) {
|
|
338
355
|
const secrets = await this.resolver.resolve(options.cwd);
|
|
339
356
|
const fmt = options.format ?? "shell";
|
|
@@ -344,6 +361,12 @@ class BurrowClient {
|
|
|
344
361
|
async resolve(cwd) {
|
|
345
362
|
return this.resolver.resolve(cwd);
|
|
346
363
|
}
|
|
364
|
+
close() {
|
|
365
|
+
this.storage.close();
|
|
366
|
+
}
|
|
367
|
+
[Symbol.dispose]() {
|
|
368
|
+
this.close();
|
|
369
|
+
}
|
|
347
370
|
}
|
|
348
371
|
function createClient(options) {
|
|
349
372
|
return new BurrowClient(options);
|