@captainsafia/burrow 1.2.0 → 1.3.0-preview.319e6c8

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.
Files changed (3) hide show
  1. package/dist/api.d.ts +213 -0
  2. package/dist/api.js +251 -0
  3. package/package.json +1 -1
package/dist/api.d.ts CHANGED
@@ -1,11 +1,32 @@
1
1
  // Generated by dts-bundle-generator v9.5.1
2
2
 
3
+ export interface TrustedPath {
4
+ path: string;
5
+ inode: string;
6
+ trustedAt: string;
7
+ }
3
8
  export interface ResolvedSecret {
4
9
  key: string;
5
10
  value: string;
6
11
  sourcePath: string;
7
12
  }
8
13
  export type ExportFormat = "shell" | "bash" | "fish" | "powershell" | "cmd" | "dotenv" | "json";
14
+ export interface TrustCheckResult {
15
+ trusted: boolean;
16
+ trustedPath?: string;
17
+ reason?: "not-trusted" | "inode-mismatch" | "path-not-found";
18
+ }
19
+ export interface LoadedSecret {
20
+ key: string;
21
+ value: string;
22
+ sourcePath: string;
23
+ }
24
+ export interface HookDiff {
25
+ /** Keys to unset from the environment */
26
+ toUnset: string[];
27
+ /** Secrets to set in the environment */
28
+ toSet: LoadedSecret[];
29
+ }
9
30
  /**
10
31
  * Configuration options for creating a BurrowClient instance.
11
32
  */
@@ -82,6 +103,97 @@ export interface RemoveOptions {
82
103
  */
83
104
  path?: string;
84
105
  }
106
+ /**
107
+ * Options for the `trust` method.
108
+ */
109
+ export interface TrustOptions {
110
+ /**
111
+ * Directory path to trust.
112
+ * Defaults to the current working directory.
113
+ */
114
+ path?: string;
115
+ }
116
+ /**
117
+ * Options for the `untrust` method.
118
+ */
119
+ export interface UntrustOptions {
120
+ /**
121
+ * Directory path to untrust.
122
+ * Defaults to the current working directory.
123
+ */
124
+ path?: string;
125
+ }
126
+ /**
127
+ * Options for the `isTrusted` method.
128
+ */
129
+ export interface IsTrustedOptions {
130
+ /**
131
+ * Directory path to check for trust.
132
+ * Defaults to the current working directory.
133
+ */
134
+ path?: string;
135
+ }
136
+ /**
137
+ * Result of the `trust` method.
138
+ */
139
+ export interface TrustResult {
140
+ /**
141
+ * The canonicalized path that was trusted.
142
+ */
143
+ path: string;
144
+ /**
145
+ * The filesystem inode/file ID for the trusted path.
146
+ */
147
+ inode: string;
148
+ }
149
+ /**
150
+ * Options for the `hook` method.
151
+ */
152
+ export interface HookOptions {
153
+ /**
154
+ * The shell to generate hook commands for.
155
+ */
156
+ shell: "bash" | "zsh" | "fish";
157
+ /**
158
+ * Whether to use colored output.
159
+ * Defaults to respecting NO_COLOR environment variable.
160
+ */
161
+ useColor?: boolean;
162
+ /**
163
+ * Keys that were previously loaded by the hook.
164
+ * Used to compute which keys need to be unset.
165
+ */
166
+ previousKeys?: string[];
167
+ }
168
+ /**
169
+ * Result of the `hook` method.
170
+ */
171
+ export interface HookResult {
172
+ /**
173
+ * Shell commands to execute for environment updates.
174
+ */
175
+ commands: string[];
176
+ /**
177
+ * Optional message to display to the user.
178
+ */
179
+ message?: string;
180
+ /**
181
+ * The secrets that were loaded.
182
+ */
183
+ secrets: LoadedSecret[];
184
+ /**
185
+ * The keys that were unloaded.
186
+ */
187
+ unloadedKeys: string[];
188
+ /**
189
+ * Whether the directory is trusted.
190
+ */
191
+ trusted: boolean;
192
+ /**
193
+ * Reason if directory is not trusted.
194
+ */
195
+ notTrustedReason?: "not-trusted" | "inode-mismatch" | "path-not-found" | "autoload-disabled";
196
+ }
85
197
  /**
86
198
  * Options for the `export` method.
87
199
  */
@@ -135,6 +247,7 @@ export declare class BurrowClient {
135
247
  private readonly storage;
136
248
  private readonly resolver;
137
249
  private readonly pathOptions;
250
+ private readonly trustManager;
138
251
  /**
139
252
  * Creates a new BurrowClient instance.
140
253
  *
@@ -307,6 +420,106 @@ export declare class BurrowClient {
307
420
  * ```
308
421
  */
309
422
  resolve(cwd?: string): Promise<Map<string, ResolvedSecret>>;
423
+ /**
424
+ * Trusts a directory for auto-loading secrets.
425
+ *
426
+ * When a directory is trusted, navigating into it (or its subdirectories)
427
+ * will automatically load secrets into the shell environment via the hook.
428
+ *
429
+ * @param options - Trust options including target path
430
+ * @returns The trusted path info with canonicalized path and inode
431
+ *
432
+ * @example
433
+ * ```typescript
434
+ * // Trust current directory
435
+ * await client.trust();
436
+ *
437
+ * // Trust specific path
438
+ * const result = await client.trust({ path: '/projects/myapp' });
439
+ * console.log(`Trusted: ${result.path}`);
440
+ * ```
441
+ */
442
+ trust(options?: TrustOptions): Promise<TrustResult>;
443
+ /**
444
+ * Removes trust from a directory.
445
+ *
446
+ * After untrusting, the directory (and its subdirectories) will no longer
447
+ * auto-load secrets. Secrets currently loaded remain until the next directory change.
448
+ *
449
+ * @param options - Untrust options including target path
450
+ * @returns true if the path was trusted and is now untrusted, false if it wasn't trusted
451
+ *
452
+ * @example
453
+ * ```typescript
454
+ * const removed = await client.untrust({ path: '/projects/myapp' });
455
+ * if (removed) {
456
+ * console.log('Directory is no longer trusted');
457
+ * }
458
+ * ```
459
+ */
460
+ untrust(options?: UntrustOptions): Promise<boolean>;
461
+ /**
462
+ * Checks if a directory is trusted for auto-loading.
463
+ *
464
+ * Trust can be inherited from ancestor directories. Also validates that
465
+ * the inode matches to detect directory replacements.
466
+ *
467
+ * @param options - Options including path to check
468
+ * @returns Trust check result with status and reason if not trusted
469
+ *
470
+ * @example
471
+ * ```typescript
472
+ * const result = await client.isTrusted({ path: '/projects/myapp/src' });
473
+ * if (result.trusted) {
474
+ * console.log(`Trusted via: ${result.trustedPath}`);
475
+ * } else {
476
+ * console.log(`Not trusted: ${result.reason}`);
477
+ * }
478
+ * ```
479
+ */
480
+ isTrusted(options?: IsTrustedOptions): Promise<TrustCheckResult>;
481
+ /**
482
+ * Lists all trusted directories.
483
+ *
484
+ * @returns Array of all trusted path entries
485
+ *
486
+ * @example
487
+ * ```typescript
488
+ * const trusted = await client.listTrusted();
489
+ * for (const entry of trusted) {
490
+ * console.log(`${entry.path} (trusted at ${entry.trustedAt})`);
491
+ * }
492
+ * ```
493
+ */
494
+ listTrusted(): Promise<TrustedPath[]>;
495
+ /**
496
+ * Processes a directory change for the shell hook.
497
+ *
498
+ * This is the main method called by shell hooks on directory change.
499
+ * It checks trust, resolves secrets, computes the diff from previous
500
+ * state, and returns the commands needed to update the environment.
501
+ *
502
+ * @param cwd - The new working directory
503
+ * @param options - Hook options including shell type and previous keys
504
+ * @returns Hook result with commands to execute and optional message
505
+ *
506
+ * @example
507
+ * ```typescript
508
+ * const result = await client.hook('/projects/myapp', {
509
+ * shell: 'bash',
510
+ * previousKeys: ['OLD_KEY'] // Keys from last hook call
511
+ * });
512
+ * if (result.trusted) {
513
+ * for (const cmd of result.commands) {
514
+ * console.log(cmd); // Execute in shell
515
+ * }
516
+ * if (result.message) {
517
+ * console.error(result.message);
518
+ * }
519
+ * }
520
+ * ```
521
+ */
522
+ hook(cwd: string, options: HookOptions): Promise<HookResult>;
310
523
  /**
311
524
  * Closes the database connection and releases resources.
312
525
  * After calling this method, the client instance should not be used.
package/dist/api.js CHANGED
@@ -80,6 +80,14 @@ class Storage {
80
80
  )
81
81
  `);
82
82
  this.db.run("CREATE INDEX IF NOT EXISTS idx_secrets_path ON secrets (path)");
83
+ this.db.run(`
84
+ CREATE TABLE IF NOT EXISTS trusted_paths (
85
+ path TEXT PRIMARY KEY,
86
+ inode TEXT NOT NULL,
87
+ trusted_at TEXT NOT NULL
88
+ )
89
+ `);
90
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_trusted_paths_inode ON trusted_paths (inode)");
83
91
  const versionResult = this.db.query("PRAGMA user_version").get();
84
92
  const currentVersion = versionResult?.user_version ?? 0;
85
93
  if (currentVersion === 0) {
@@ -139,6 +147,61 @@ class Storage {
139
147
  db.query("DELETE FROM secrets WHERE path = ? AND key = ?").run(canonicalPath, key);
140
148
  return true;
141
149
  }
150
+ async addTrustedPath(canonicalPath, inode) {
151
+ const db = await this.ensureDb();
152
+ const trustedAt = new Date().toISOString();
153
+ db.query(`
154
+ INSERT INTO trusted_paths (path, inode, trusted_at)
155
+ VALUES (?, ?, ?)
156
+ ON CONFLICT(path) DO UPDATE SET
157
+ inode = excluded.inode,
158
+ trusted_at = excluded.trusted_at
159
+ `).run(canonicalPath, inode, trustedAt);
160
+ }
161
+ async removeTrustedPath(canonicalPath) {
162
+ const db = await this.ensureDb();
163
+ const existing = db.query("SELECT path FROM trusted_paths WHERE path = ?").get(canonicalPath);
164
+ if (!existing) {
165
+ return false;
166
+ }
167
+ db.query("DELETE FROM trusted_paths WHERE path = ?").run(canonicalPath);
168
+ return true;
169
+ }
170
+ async getTrustedPath(canonicalPath) {
171
+ const db = await this.ensureDb();
172
+ const row = db.query("SELECT path, inode, trusted_at FROM trusted_paths WHERE path = ?").get(canonicalPath);
173
+ if (!row) {
174
+ return;
175
+ }
176
+ return {
177
+ path: row.path,
178
+ inode: row.inode,
179
+ trustedAt: row.trusted_at
180
+ };
181
+ }
182
+ async getAllTrustedPaths() {
183
+ const db = await this.ensureDb();
184
+ const rows = db.query("SELECT path, inode, trusted_at FROM trusted_paths ORDER BY path").all();
185
+ return rows.map((row) => ({
186
+ path: row.path,
187
+ inode: row.inode,
188
+ trustedAt: row.trusted_at
189
+ }));
190
+ }
191
+ async getTrustedAncestorPaths(canonicalPath) {
192
+ const db = await this.ensureDb();
193
+ let rows;
194
+ if (isWindows()) {
195
+ rows = db.query("SELECT path, inode, trusted_at FROM trusted_paths WHERE ? = path OR ? LIKE path || '\\' || '%' OR (length(path) = 3 AND path LIKE '_:\\' AND ? LIKE path || '%')").all(canonicalPath, canonicalPath, canonicalPath);
196
+ } else {
197
+ rows = db.query("SELECT path, inode, trusted_at FROM trusted_paths WHERE ? = path OR ? LIKE path || '/' || '%' OR path = '/'").all(canonicalPath, canonicalPath);
198
+ }
199
+ return rows.map((row) => ({
200
+ path: row.path,
201
+ inode: row.inode,
202
+ trustedAt: row.trusted_at
203
+ }));
204
+ }
142
205
  close() {
143
206
  if (this.db) {
144
207
  this.db.close();
@@ -355,11 +418,135 @@ function format(secrets, fmt, options = {}) {
355
418
  throw new Error(`Unknown format: ${fmt}`);
356
419
  }
357
420
  }
421
+ // src/core/trust.ts
422
+ import { stat } from "node:fs/promises";
423
+ async function getInode(path) {
424
+ const stats = await stat(path);
425
+ return stats.ino.toString();
426
+ }
427
+
428
+ class TrustManager {
429
+ storage;
430
+ pathOptions;
431
+ constructor(options) {
432
+ this.storage = options.storage;
433
+ this.pathOptions = {
434
+ followSymlinks: options.followSymlinks
435
+ };
436
+ }
437
+ async trust(targetPath) {
438
+ const canonicalPath = await canonicalize(targetPath, this.pathOptions);
439
+ const inode = await getInode(canonicalPath);
440
+ await this.storage.addTrustedPath(canonicalPath, inode);
441
+ return { path: canonicalPath, inode };
442
+ }
443
+ async untrust(targetPath) {
444
+ const canonicalPath = await canonicalize(targetPath, this.pathOptions);
445
+ return this.storage.removeTrustedPath(canonicalPath);
446
+ }
447
+ async isTrusted(targetPath) {
448
+ let canonicalPath;
449
+ try {
450
+ canonicalPath = await canonicalize(targetPath, this.pathOptions);
451
+ } catch {
452
+ return { trusted: false, reason: "path-not-found" };
453
+ }
454
+ const trustedAncestors = await this.storage.getTrustedAncestorPaths(canonicalPath);
455
+ if (trustedAncestors.length === 0) {
456
+ return { trusted: false, reason: "not-trusted" };
457
+ }
458
+ trustedAncestors.sort((a, b) => b.path.length - a.path.length);
459
+ for (const trusted of trustedAncestors) {
460
+ try {
461
+ const currentInode = await getInode(trusted.path);
462
+ if (currentInode === trusted.inode) {
463
+ return { trusted: true, trustedPath: trusted.path };
464
+ }
465
+ } catch {}
466
+ }
467
+ return { trusted: false, reason: "inode-mismatch" };
468
+ }
469
+ async list() {
470
+ return this.storage.getAllTrustedPaths();
471
+ }
472
+ }
473
+ // src/core/hook.ts
474
+ function resolveToLoadedSecrets(secrets) {
475
+ const result = [];
476
+ for (const [, secret] of secrets) {
477
+ result.push({
478
+ key: secret.key,
479
+ value: secret.value,
480
+ sourcePath: secret.sourcePath
481
+ });
482
+ }
483
+ return result;
484
+ }
485
+ function computeHookDiff(previousKeys, newSecrets) {
486
+ const newKeys = new Set(newSecrets.map((s) => s.key));
487
+ const toUnset = [];
488
+ for (const key of previousKeys) {
489
+ if (!newKeys.has(key)) {
490
+ toUnset.push(key);
491
+ }
492
+ }
493
+ return {
494
+ toUnset,
495
+ toSet: newSecrets
496
+ };
497
+ }
498
+ function formatHookMessage(diff, useColor = true) {
499
+ const loaded = diff.toSet.length;
500
+ const unloaded = diff.toUnset.length;
501
+ if (loaded === 0 && unloaded === 0) {
502
+ return;
503
+ }
504
+ const dim = useColor ? "\x1B[2m" : "";
505
+ const reset = useColor ? "\x1B[0m" : "";
506
+ let message = `${dim}burrow: `;
507
+ if (unloaded > 0 && loaded > 0) {
508
+ message += `unloaded ${unloaded}, loaded ${loaded}`;
509
+ } else if (loaded > 0) {
510
+ message += `loaded ${loaded} secret${loaded === 1 ? "" : "s"}`;
511
+ } else {
512
+ message += `unloaded ${unloaded} secret${unloaded === 1 ? "" : "s"}`;
513
+ }
514
+ message += reset;
515
+ return message;
516
+ }
517
+ function generateShellCommands(diff, shell) {
518
+ const commands = [];
519
+ for (const key of diff.toUnset) {
520
+ if (shell === "fish") {
521
+ commands.push(`set -e ${key}`);
522
+ } else {
523
+ commands.push(`unset ${key}`);
524
+ }
525
+ }
526
+ for (const secret of diff.toSet) {
527
+ const escapedValue = escapeShellValue2(secret.value, shell);
528
+ if (shell === "fish") {
529
+ commands.push(`set -gx ${secret.key} ${escapedValue}`);
530
+ } else {
531
+ commands.push(`export ${secret.key}=${escapedValue}`);
532
+ }
533
+ }
534
+ return commands;
535
+ }
536
+ function escapeShellValue2(value, shell) {
537
+ if (shell === "fish") {
538
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
539
+ } else {
540
+ const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
541
+ return `$'${escaped}'`;
542
+ }
543
+ }
358
544
  // src/api.ts
359
545
  class BurrowClient {
360
546
  storage;
361
547
  resolver;
362
548
  pathOptions;
549
+ trustManager;
363
550
  constructor(options = {}) {
364
551
  this.storage = new Storage({
365
552
  configDir: options.configDir,
@@ -372,6 +559,10 @@ class BurrowClient {
372
559
  this.pathOptions = {
373
560
  followSymlinks: options.followSymlinks
374
561
  };
562
+ this.trustManager = new TrustManager({
563
+ storage: this.storage,
564
+ followSymlinks: options.followSymlinks
565
+ });
375
566
  }
376
567
  async set(key, value, options = {}) {
377
568
  assertValidEnvKey(key);
@@ -407,6 +598,66 @@ class BurrowClient {
407
598
  async resolve(cwd) {
408
599
  return this.resolver.resolve(cwd);
409
600
  }
601
+ async trust(options = {}) {
602
+ const targetPath = options.path ?? process.cwd();
603
+ return this.trustManager.trust(targetPath);
604
+ }
605
+ async untrust(options = {}) {
606
+ const targetPath = options.path ?? process.cwd();
607
+ return this.trustManager.untrust(targetPath);
608
+ }
609
+ async isTrusted(options = {}) {
610
+ const targetPath = options.path ?? process.cwd();
611
+ return this.trustManager.isTrusted(targetPath);
612
+ }
613
+ async listTrusted() {
614
+ return this.trustManager.list();
615
+ }
616
+ async hook(cwd, options) {
617
+ const previousKeys = new Set(options.previousKeys ?? []);
618
+ if (process.env["BURROW_AUTOLOAD"] === "0") {
619
+ const diff2 = computeHookDiff(previousKeys, []);
620
+ const commands2 = generateShellCommands(diff2, options.shell);
621
+ const useColor2 = options.useColor ?? !process.env["NO_COLOR"];
622
+ const message2 = diff2.toUnset.length > 0 ? formatHookMessage(diff2, useColor2) : undefined;
623
+ return {
624
+ commands: commands2,
625
+ message: message2,
626
+ secrets: [],
627
+ unloadedKeys: diff2.toUnset,
628
+ trusted: false,
629
+ notTrustedReason: "autoload-disabled"
630
+ };
631
+ }
632
+ const trustResult = await this.isTrusted({ path: cwd });
633
+ if (!trustResult.trusted) {
634
+ const diff2 = computeHookDiff(previousKeys, []);
635
+ const commands2 = generateShellCommands(diff2, options.shell);
636
+ const useColor2 = options.useColor ?? !process.env["NO_COLOR"];
637
+ const message2 = diff2.toUnset.length > 0 ? formatHookMessage(diff2, useColor2) : undefined;
638
+ return {
639
+ commands: commands2,
640
+ message: message2,
641
+ secrets: [],
642
+ unloadedKeys: diff2.toUnset,
643
+ trusted: false,
644
+ notTrustedReason: trustResult.reason
645
+ };
646
+ }
647
+ const resolvedSecrets = await this.resolve(cwd);
648
+ const secrets = resolveToLoadedSecrets(resolvedSecrets);
649
+ const diff = computeHookDiff(previousKeys, secrets);
650
+ const commands = generateShellCommands(diff, options.shell);
651
+ const useColor = options.useColor ?? !process.env["NO_COLOR"];
652
+ const message = formatHookMessage(diff, useColor);
653
+ return {
654
+ commands,
655
+ message,
656
+ secrets,
657
+ unloadedKeys: diff.toUnset,
658
+ trusted: true
659
+ };
660
+ }
410
661
  close() {
411
662
  this.storage.close();
412
663
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@captainsafia/burrow",
3
- "version": "1.2.0",
3
+ "version": "1.3.0-preview.319e6c8",
4
4
  "description": "Platform-agnostic, directory-scoped secrets manager",
5
5
  "type": "module",
6
6
  "main": "dist/api.js",