@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 +8 -0
- package/dist/api.d.ts +67 -0
- package/dist/api.js +89 -70
- package/package.json +1 -1
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 {
|
|
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,68 +57,92 @@ class Storage {
|
|
|
63
57
|
get storePath() {
|
|
64
58
|
return join2(this.configDir, this.storeFileName);
|
|
65
59
|
}
|
|
66
|
-
async
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
await
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
110
|
-
|
|
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
|
|
114
|
-
|
|
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
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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);
|