@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 +8 -0
- package/README.md +117 -4
- package/dist/api.d.ts +318 -0
- package/dist/api.js +97 -77
- package/package.json +6 -3
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
|
-
|
|
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
|
-
|
|
110
|
+
### Development
|
|
10
111
|
|
|
11
112
|
```bash
|
|
12
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
105
|
-
|
|
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
|
|
109
|
-
|
|
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
|
|
113
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|