@captainsafia/burrow 0.1.0

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 ADDED
@@ -0,0 +1,15 @@
1
+ # burrow
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
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.
package/dist/api.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ export interface ResolvedSecret {
4
+ key: string;
5
+ value: string;
6
+ sourcePath: string;
7
+ }
8
+ export type ExportFormat = "shell" | "dotenv" | "json";
9
+ export interface BurrowClientOptions {
10
+ configDir?: string;
11
+ storeFileName?: string;
12
+ followSymlinks?: boolean;
13
+ }
14
+ export interface SetOptions {
15
+ path?: string;
16
+ }
17
+ export interface GetOptions {
18
+ cwd?: string;
19
+ }
20
+ export interface ListOptions {
21
+ cwd?: string;
22
+ }
23
+ export interface BlockOptions {
24
+ path?: string;
25
+ }
26
+ export interface ExportOptions {
27
+ cwd?: string;
28
+ format?: ExportFormat;
29
+ showValues?: boolean;
30
+ includeSources?: boolean;
31
+ }
32
+ export declare class BurrowClient {
33
+ private readonly storage;
34
+ private readonly resolver;
35
+ private readonly pathOptions;
36
+ constructor(options?: BurrowClientOptions);
37
+ set(key: string, value: string, options?: SetOptions): Promise<void>;
38
+ get(key: string, options?: GetOptions): Promise<ResolvedSecret | undefined>;
39
+ list(options?: ListOptions): Promise<ResolvedSecret[]>;
40
+ block(key: string, options?: BlockOptions): Promise<void>;
41
+ export(options?: ExportOptions): Promise<string>;
42
+ resolve(cwd?: string): Promise<Map<string, ResolvedSecret>>;
43
+ }
44
+ export declare function createClient(options?: BurrowClientOptions): BurrowClient;
45
+
46
+ export {};
package/dist/api.js ADDED
@@ -0,0 +1,352 @@
1
+ // src/storage/index.ts
2
+ import { mkdir, rename, readFile, unlink } from "node:fs/promises";
3
+ import { join as join2 } from "node:path";
4
+ import { randomBytes } from "node:crypto";
5
+
6
+ // src/platform/index.ts
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ var APP_NAME = "burrow";
10
+ function getConfigDir() {
11
+ const platform = process.platform;
12
+ if (platform === "win32") {
13
+ return getWindowsConfigDir();
14
+ }
15
+ return getUnixConfigDir();
16
+ }
17
+ function getUnixConfigDir() {
18
+ const xdgConfigHome = process.env["XDG_CONFIG_HOME"];
19
+ if (xdgConfigHome) {
20
+ return join(xdgConfigHome, APP_NAME);
21
+ }
22
+ const home = homedir();
23
+ return join(home, ".config", APP_NAME);
24
+ }
25
+ function getWindowsConfigDir() {
26
+ const appData = process.env["APPDATA"];
27
+ if (appData) {
28
+ return join(appData, APP_NAME);
29
+ }
30
+ const localAppData = process.env["LOCALAPPDATA"];
31
+ if (localAppData) {
32
+ return join(localAppData, APP_NAME);
33
+ }
34
+ const home = homedir();
35
+ return join(home, APP_NAME);
36
+ }
37
+ function isWindows() {
38
+ return process.platform === "win32";
39
+ }
40
+
41
+ // 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
+ }
50
+
51
+ class Storage {
52
+ configDir;
53
+ storeFileName;
54
+ constructor(options = {}) {
55
+ this.configDir = options.configDir ?? getConfigDir();
56
+ this.storeFileName = options.storeFileName ?? DEFAULT_STORE_FILE;
57
+ }
58
+ get storePath() {
59
+ return join2(this.configDir, this.storeFileName);
60
+ }
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;
74
+ }
75
+ }
76
+ async write(store) {
77
+ 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;
90
+ }
91
+ }
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);
102
+ }
103
+ async getPathSecrets(canonicalPath) {
104
+ const store = await this.read();
105
+ return store.paths[canonicalPath];
106
+ }
107
+ async getAllPaths() {
108
+ const store = await this.read();
109
+ return Object.keys(store.paths);
110
+ }
111
+ async removeKey(canonicalPath, key) {
112
+ const store = await this.read();
113
+ if (!store.paths[canonicalPath]?.[key]) {
114
+ return false;
115
+ }
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);
121
+ return true;
122
+ }
123
+ }
124
+
125
+ // src/core/path.ts
126
+ import { realpath } from "node:fs/promises";
127
+ import { resolve, sep, normalize } from "node:path";
128
+ async function canonicalize(inputPath, options = {}) {
129
+ const { followSymlinks = true } = options;
130
+ let canonical;
131
+ if (followSymlinks) {
132
+ try {
133
+ canonical = await realpath(inputPath);
134
+ } catch (error) {
135
+ if (error.code === "ENOENT") {
136
+ canonical = resolve(inputPath);
137
+ } else {
138
+ throw error;
139
+ }
140
+ }
141
+ } else {
142
+ canonical = resolve(inputPath);
143
+ }
144
+ if (isWindows()) {
145
+ canonical = normalizePath(canonical);
146
+ }
147
+ return canonical;
148
+ }
149
+ function normalizePath(path) {
150
+ let normalized = normalize(path);
151
+ if (isWindows()) {
152
+ normalized = normalized.replace(/\//g, "\\");
153
+ if (normalized.length >= 2 && normalized[1] === ":") {
154
+ normalized = normalized[0].toUpperCase() + normalized.slice(1);
155
+ }
156
+ }
157
+ return normalized;
158
+ }
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
+ // src/core/resolver.ts
170
+ class Resolver {
171
+ storage;
172
+ pathOptions;
173
+ constructor(options = {}) {
174
+ this.storage = new Storage({
175
+ configDir: options.configDir,
176
+ storeFileName: options.storeFileName
177
+ });
178
+ this.pathOptions = {
179
+ followSymlinks: options.followSymlinks
180
+ };
181
+ }
182
+ async resolve(cwd) {
183
+ const workingDir = cwd ?? process.cwd();
184
+ const canonicalCwd = await canonicalize(workingDir, this.pathOptions);
185
+ const allPaths = await this.storage.getAllPaths();
186
+ const ancestorPaths = allPaths.filter((storedPath) => isAncestorOf(storedPath, canonicalCwd));
187
+ ancestorPaths.sort((a, b) => {
188
+ if (isWindows()) {
189
+ return a.toLowerCase().localeCompare(b.toLowerCase());
190
+ }
191
+ return a.localeCompare(b);
192
+ });
193
+ const resolved = new Map;
194
+ for (const scopePath of ancestorPaths) {
195
+ const secrets = await this.storage.getPathSecrets(scopePath);
196
+ if (!secrets)
197
+ continue;
198
+ for (const [key, entry] of Object.entries(secrets)) {
199
+ if (entry.value === null) {
200
+ resolved.delete(key);
201
+ } else {
202
+ resolved.set(key, {
203
+ key,
204
+ value: entry.value,
205
+ sourcePath: scopePath
206
+ });
207
+ }
208
+ }
209
+ }
210
+ return resolved;
211
+ }
212
+ async get(key, cwd) {
213
+ const resolved = await this.resolve(cwd);
214
+ return resolved.get(key);
215
+ }
216
+ async list(cwd) {
217
+ const resolved = await this.resolve(cwd);
218
+ return Array.from(resolved.values()).sort((a, b) => a.key.localeCompare(b.key));
219
+ }
220
+ get storageInstance() {
221
+ return this.storage;
222
+ }
223
+ }
224
+ // src/core/formatter.ts
225
+ var ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
226
+ function validateEnvKey(key) {
227
+ return ENV_KEY_PATTERN.test(key);
228
+ }
229
+ function assertValidEnvKey(key) {
230
+ if (!validateEnvKey(key)) {
231
+ throw new Error(`Invalid environment variable key: "${key}". ` + `Keys must match ${ENV_KEY_PATTERN.toString()}`);
232
+ }
233
+ }
234
+ function escapeShellValue(value) {
235
+ return value.replace(/'/g, `'"'"'`);
236
+ }
237
+ function escapeDoubleQuotes(value) {
238
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
239
+ }
240
+ function formatShell(secrets) {
241
+ const lines = [];
242
+ const sortedKeys = Array.from(secrets.keys()).sort();
243
+ for (const key of sortedKeys) {
244
+ const secret = secrets.get(key);
245
+ assertValidEnvKey(key);
246
+ const escapedValue = escapeShellValue(secret.value);
247
+ lines.push(`export ${key}='${escapedValue}'`);
248
+ }
249
+ return lines.join(`
250
+ `);
251
+ }
252
+ function formatDotenv(secrets) {
253
+ const lines = [];
254
+ const sortedKeys = Array.from(secrets.keys()).sort();
255
+ for (const key of sortedKeys) {
256
+ const secret = secrets.get(key);
257
+ assertValidEnvKey(key);
258
+ if (secret.value.includes(`
259
+ `)) {
260
+ throw new Error(`Cannot export key "${key}" to dotenv format: value contains newlines. ` + `Use --format json or --format shell instead.`);
261
+ }
262
+ const escapedValue = escapeDoubleQuotes(secret.value);
263
+ lines.push(`${key}="${escapedValue}"`);
264
+ }
265
+ return lines.join(`
266
+ `);
267
+ }
268
+ function formatJson(secrets, includeSources = false) {
269
+ if (includeSources) {
270
+ const result = {};
271
+ for (const [key, secret] of secrets) {
272
+ result[key] = {
273
+ value: secret.value,
274
+ sourcePath: secret.sourcePath
275
+ };
276
+ }
277
+ return JSON.stringify(result, null, 2);
278
+ } else {
279
+ const result = {};
280
+ for (const [key, secret] of secrets) {
281
+ result[key] = secret.value;
282
+ }
283
+ return JSON.stringify(result, null, 2);
284
+ }
285
+ }
286
+ function format(secrets, fmt, options = {}) {
287
+ switch (fmt) {
288
+ case "shell":
289
+ return formatShell(secrets);
290
+ case "dotenv":
291
+ return formatDotenv(secrets);
292
+ case "json":
293
+ return formatJson(secrets, options.includeSources);
294
+ default:
295
+ throw new Error(`Unknown format: ${fmt}`);
296
+ }
297
+ }
298
+ // src/api.ts
299
+ class BurrowClient {
300
+ storage;
301
+ resolver;
302
+ pathOptions;
303
+ constructor(options = {}) {
304
+ this.storage = new Storage({
305
+ configDir: options.configDir,
306
+ storeFileName: options.storeFileName
307
+ });
308
+ this.resolver = new Resolver({
309
+ configDir: options.configDir,
310
+ storeFileName: options.storeFileName,
311
+ followSymlinks: options.followSymlinks
312
+ });
313
+ this.pathOptions = {
314
+ followSymlinks: options.followSymlinks
315
+ };
316
+ }
317
+ async set(key, value, options = {}) {
318
+ assertValidEnvKey(key);
319
+ const targetPath = options.path ?? process.cwd();
320
+ const canonicalPath = await canonicalize(targetPath, this.pathOptions);
321
+ await this.storage.setSecret(canonicalPath, key, value);
322
+ }
323
+ async get(key, options = {}) {
324
+ return this.resolver.get(key, options.cwd);
325
+ }
326
+ async list(options = {}) {
327
+ return this.resolver.list(options.cwd);
328
+ }
329
+ async block(key, options = {}) {
330
+ assertValidEnvKey(key);
331
+ const targetPath = options.path ?? process.cwd();
332
+ const canonicalPath = await canonicalize(targetPath, this.pathOptions);
333
+ await this.storage.setSecret(canonicalPath, key, null);
334
+ }
335
+ async export(options = {}) {
336
+ const secrets = await this.resolver.resolve(options.cwd);
337
+ const fmt = options.format ?? "shell";
338
+ return format(secrets, fmt, {
339
+ includeSources: options.includeSources
340
+ });
341
+ }
342
+ async resolve(cwd) {
343
+ return this.resolver.resolve(cwd);
344
+ }
345
+ }
346
+ function createClient(options) {
347
+ return new BurrowClient(options);
348
+ }
349
+ export {
350
+ createClient,
351
+ BurrowClient
352
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@captainsafia/burrow",
3
+ "version": "0.1.0",
4
+ "description": "Platform-agnostic, directory-scoped secrets manager",
5
+ "type": "module",
6
+ "main": "dist/api.js",
7
+ "types": "dist/api.d.ts",
8
+ "bin": {
9
+ "burrow": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/captainsafia/burrow.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/captainsafia/burrow/issues"
20
+ },
21
+ "homepage": "https://github.com/captainsafia/burrow#readme",
22
+ "scripts": {
23
+ "build": "bun build src/api.ts --outdir dist --target node && dts-bundle-generator -o dist/api.d.ts src/api.ts",
24
+ "build:cli": "bun build src/cli.ts --outdir dist --target bun",
25
+ "compile": "bun build src/cli.ts --compile --outfile burrow",
26
+ "compile:linux-x64": "bun build src/cli.ts --compile --target=bun-linux-x64 --outfile burrow-linux-x64",
27
+ "compile:linux-arm64": "bun build src/cli.ts --compile --target=bun-linux-arm64 --outfile burrow-linux-arm64",
28
+ "compile:darwin-x64": "bun build src/cli.ts --compile --target=bun-darwin-x64 --outfile burrow-darwin-x64",
29
+ "compile:darwin-arm64": "bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile burrow-darwin-arm64",
30
+ "compile:windows-x64": "bun build src/cli.ts --compile --target=bun-windows-x64 --outfile burrow-windows-x64.exe",
31
+ "test": "bun test",
32
+ "typecheck": "tsc --noEmit"
33
+ },
34
+ "devDependencies": {
35
+ "@types/bun": "latest",
36
+ "dts-bundle-generator": "^9.5.1",
37
+ "typescript": "^5"
38
+ },
39
+ "exports": {
40
+ ".": {
41
+ "types": "./dist/api.d.ts",
42
+ "import": "./dist/api.js"
43
+ }
44
+ },
45
+ "keywords": [
46
+ "secrets",
47
+ "environment",
48
+ "dotenv",
49
+ "direnv",
50
+ "configuration"
51
+ ],
52
+ "license": "MIT"
53
+ }