autho 1.0.0 → 2.0.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/dist/autho.js ADDED
@@ -0,0 +1,1873 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // apps/cli/src/index.ts
5
+ import { spawn } from "child_process";
6
+ import { existsSync as existsSync4 } from "fs";
7
+ import { createInterface } from "readline/promises";
8
+ import { resolve as resolve3 } from "path";
9
+
10
+ // packages/core/src/daemon.ts
11
+ import { createHash, timingSafeEqual } from "crypto";
12
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
13
+
14
+ // packages/storage/src/index.ts
15
+ import { Database } from "bun:sqlite";
16
+ import { chmodSync, existsSync, mkdirSync } from "fs";
17
+ import { dirname } from "path";
18
+ function parseJson(value) {
19
+ return JSON.parse(value);
20
+ }
21
+ function tryChmod(path, mode) {
22
+ if (process.platform === "win32") {
23
+ return;
24
+ }
25
+ try {
26
+ chmodSync(path, mode);
27
+ } catch {}
28
+ }
29
+
30
+ class AuthoDatabase {
31
+ vaultPath;
32
+ db;
33
+ constructor(vaultPath) {
34
+ this.vaultPath = vaultPath;
35
+ if (vaultPath !== ":memory:") {
36
+ mkdirSync(dirname(vaultPath), { mode: 448, recursive: true });
37
+ tryChmod(dirname(vaultPath), 448);
38
+ }
39
+ this.db = new Database(vaultPath, { create: true, strict: true });
40
+ this.migrate();
41
+ this.hardenStorageFiles();
42
+ }
43
+ close() {
44
+ this.hardenStorageFiles();
45
+ this.db.close();
46
+ }
47
+ hardenStorageFiles() {
48
+ if (this.vaultPath === ":memory:") {
49
+ return;
50
+ }
51
+ for (const path of [this.vaultPath, `${this.vaultPath}-shm`, `${this.vaultPath}-wal`]) {
52
+ if (existsSync(path)) {
53
+ tryChmod(path, 384);
54
+ }
55
+ }
56
+ }
57
+ migrate() {
58
+ this.db.exec(`
59
+ PRAGMA journal_mode = WAL;
60
+
61
+ CREATE TABLE IF NOT EXISTS meta (
62
+ key TEXT PRIMARY KEY,
63
+ value TEXT NOT NULL
64
+ );
65
+
66
+ CREATE TABLE IF NOT EXISTS secrets (
67
+ id TEXT PRIMARY KEY,
68
+ name TEXT NOT NULL UNIQUE,
69
+ type TEXT NOT NULL,
70
+ payload TEXT NOT NULL,
71
+ wrapped_key TEXT NOT NULL,
72
+ created_at TEXT NOT NULL,
73
+ updated_at TEXT NOT NULL
74
+ );
75
+
76
+ CREATE TABLE IF NOT EXISTS leases (
77
+ id TEXT PRIMARY KEY,
78
+ name TEXT NOT NULL,
79
+ secret_refs TEXT NOT NULL,
80
+ expires_at TEXT NOT NULL,
81
+ revoked_at TEXT,
82
+ created_at TEXT NOT NULL
83
+ );
84
+
85
+ CREATE TABLE IF NOT EXISTS audit_events (
86
+ id TEXT PRIMARY KEY,
87
+ event_type TEXT NOT NULL,
88
+ subject_type TEXT NOT NULL,
89
+ subject_ref TEXT,
90
+ message TEXT NOT NULL,
91
+ metadata TEXT NOT NULL,
92
+ created_at TEXT NOT NULL
93
+ );
94
+ `);
95
+ }
96
+ getVaultConfig() {
97
+ const row = this.db.query("SELECT value FROM meta WHERE key = ?1").get("vault.config");
98
+ return row ? parseJson(row.value) : null;
99
+ }
100
+ setVaultConfig(config) {
101
+ this.db.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)").run("vault.config", JSON.stringify(config));
102
+ }
103
+ countSecrets() {
104
+ const row = this.db.query("SELECT COUNT(*) AS count FROM secrets").get();
105
+ return row.count;
106
+ }
107
+ countActiveLeases(nowIso) {
108
+ const row = this.db.query(`SELECT COUNT(*) AS count
109
+ FROM leases
110
+ WHERE revoked_at IS NULL AND expires_at > ?1`).get(nowIso);
111
+ return row.count;
112
+ }
113
+ countAuditEvents() {
114
+ const row = this.db.query("SELECT COUNT(*) AS count FROM audit_events").get();
115
+ return row.count;
116
+ }
117
+ insertSecret(secret) {
118
+ this.db.query(`INSERT INTO secrets (id, name, type, payload, wrapped_key, created_at, updated_at)
119
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)`).run(secret.id, secret.name, secret.type, secret.payload, secret.wrappedKey, secret.createdAt, secret.updatedAt);
120
+ }
121
+ listSecrets() {
122
+ return this.db.query(`SELECT
123
+ id,
124
+ name,
125
+ type,
126
+ payload,
127
+ wrapped_key AS wrappedKey,
128
+ created_at AS createdAt,
129
+ updated_at AS updatedAt
130
+ FROM secrets
131
+ ORDER BY created_at ASC`).all();
132
+ }
133
+ findSecret(ref) {
134
+ const row = this.db.query(`SELECT
135
+ id,
136
+ name,
137
+ type,
138
+ payload,
139
+ wrapped_key AS wrappedKey,
140
+ created_at AS createdAt,
141
+ updated_at AS updatedAt
142
+ FROM secrets
143
+ WHERE id = ?1 OR name = ?1
144
+ LIMIT 1`).get(ref);
145
+ return row ?? null;
146
+ }
147
+ deleteSecret(id) {
148
+ this.db.query("DELETE FROM secrets WHERE id = ?1").run(id);
149
+ }
150
+ insertLease(lease) {
151
+ this.db.query(`INSERT INTO leases (id, name, secret_refs, expires_at, revoked_at, created_at)
152
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6)`).run(lease.id, lease.name, lease.secretRefs, lease.expiresAt, lease.revokedAt, lease.createdAt);
153
+ }
154
+ findLease(id) {
155
+ const row = this.db.query(`SELECT
156
+ id,
157
+ name,
158
+ secret_refs AS secretRefs,
159
+ expires_at AS expiresAt,
160
+ revoked_at AS revokedAt,
161
+ created_at AS createdAt
162
+ FROM leases
163
+ WHERE id = ?1
164
+ LIMIT 1`).get(id);
165
+ return row ?? null;
166
+ }
167
+ revokeLease(id, revokedAt) {
168
+ this.db.query("UPDATE leases SET revoked_at = ?2 WHERE id = ?1").run(id, revokedAt);
169
+ }
170
+ insertAudit(event) {
171
+ this.db.query(`INSERT INTO audit_events (id, event_type, subject_type, subject_ref, message, metadata, created_at)
172
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)`).run(event.id, event.eventType, event.subjectType, event.subjectRef, event.message, event.metadata, event.createdAt);
173
+ }
174
+ listAudit(limit) {
175
+ return this.db.query(`SELECT
176
+ id,
177
+ event_type AS eventType,
178
+ subject_type AS subjectType,
179
+ subject_ref AS subjectRef,
180
+ message,
181
+ metadata,
182
+ created_at AS createdAt
183
+ FROM audit_events
184
+ ORDER BY created_at DESC
185
+ LIMIT ?1`).all(limit);
186
+ }
187
+ }
188
+
189
+ // packages/crypto/src/index.ts
190
+ import {
191
+ createCipheriv,
192
+ createDecipheriv,
193
+ randomBytes,
194
+ scryptSync
195
+ } from "crypto";
196
+ var DEFAULT_KDF = {
197
+ keyLength: 32,
198
+ name: "scrypt",
199
+ salt: "",
200
+ N: 1 << 17,
201
+ p: 1,
202
+ r: 8
203
+ };
204
+ function toBuffer(value) {
205
+ return Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
206
+ }
207
+ function randomId(size = 16) {
208
+ return randomBytes(size).toString("hex");
209
+ }
210
+ function deriveKeyFromPassword(password, config) {
211
+ return scryptSync(password, Buffer.from(config.salt, "base64"), config.keyLength, {
212
+ maxmem: 256 * 1024 * 1024,
213
+ N: config.N,
214
+ p: config.p,
215
+ r: config.r
216
+ });
217
+ }
218
+ function encryptWithKey(value, key, aad) {
219
+ const iv = randomBytes(12);
220
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
221
+ cipher.setAAD(Buffer.from(aad, "utf8"));
222
+ const ciphertext = Buffer.concat([cipher.update(toBuffer(value)), cipher.final()]);
223
+ return {
224
+ algorithm: "aes-256-gcm",
225
+ ciphertext: ciphertext.toString("base64"),
226
+ iv: iv.toString("base64"),
227
+ tag: cipher.getAuthTag().toString("base64")
228
+ };
229
+ }
230
+ function decryptWithKey(blob, key, aad) {
231
+ const decipher = createDecipheriv(blob.algorithm, key, Buffer.from(blob.iv, "base64"));
232
+ decipher.setAAD(Buffer.from(aad, "utf8"));
233
+ decipher.setAuthTag(Buffer.from(blob.tag, "base64"));
234
+ return Buffer.concat([
235
+ decipher.update(Buffer.from(blob.ciphertext, "base64")),
236
+ decipher.final()
237
+ ]);
238
+ }
239
+ function createVaultConfig(password) {
240
+ const rootKey = randomBytes(32);
241
+ const kdf = {
242
+ ...DEFAULT_KDF,
243
+ salt: randomBytes(16).toString("base64")
244
+ };
245
+ const key = deriveKeyFromPassword(password, kdf);
246
+ return {
247
+ config: {
248
+ createdAt: new Date().toISOString(),
249
+ kdf,
250
+ version: 1,
251
+ wrappedRootKey: encryptWithKey(rootKey, key, "autho:vault-root")
252
+ },
253
+ rootKey
254
+ };
255
+ }
256
+ function unlockRootKey(password, config) {
257
+ const key = deriveKeyFromPassword(password, config.kdf);
258
+ return decryptWithKey(config.wrappedRootKey, key, "autho:vault-root");
259
+ }
260
+
261
+ // packages/core/src/index.ts
262
+ import { spawnSync } from "child_process";
263
+ import { createHmac, randomBytes as randomBytes3 } from "crypto";
264
+ import {
265
+ existsSync as existsSync2,
266
+ readFileSync as readFileSync2
267
+ } from "fs";
268
+ import { basename as basename2 } from "path";
269
+
270
+ // packages/core/src/artifacts.ts
271
+ import { randomBytes as randomBytes2 } from "crypto";
272
+ import {
273
+ readdirSync,
274
+ readFileSync,
275
+ statSync
276
+ } from "fs";
277
+ import { basename, join as join2, relative, resolve as resolve2, sep } from "path";
278
+
279
+ // packages/core/src/paths.ts
280
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, writeFileSync } from "fs";
281
+ import { homedir } from "os";
282
+ import { dirname as dirname2, join, resolve } from "path";
283
+ function normalizePath(path) {
284
+ return resolve(path).replace(/\\/g, "/");
285
+ }
286
+ function tryChmod2(path, mode) {
287
+ if (process.platform === "win32") {
288
+ return;
289
+ }
290
+ try {
291
+ chmodSync2(path, mode);
292
+ } catch {}
293
+ }
294
+ function authoHomeDir() {
295
+ return normalizePath(process.env.AUTHO_HOME ?? join(homedir(), ".autho"));
296
+ }
297
+ function defaultVaultPath() {
298
+ return normalizePath(join(authoHomeDir(), "vault.db"));
299
+ }
300
+ function defaultProjectFilePath() {
301
+ return normalizePath(join(authoHomeDir(), "project.json"));
302
+ }
303
+ function defaultDaemonStatePath() {
304
+ return normalizePath(join(authoHomeDir(), "daemon.json"));
305
+ }
306
+ function ensurePrivateDir(path) {
307
+ mkdirSync2(path, { mode: 448, recursive: true });
308
+ tryChmod2(path, 448);
309
+ }
310
+ function ensurePrivateParent(path) {
311
+ ensurePrivateDir(dirname2(path));
312
+ }
313
+ function hardenFilePermissions(path) {
314
+ tryChmod2(path, 384);
315
+ }
316
+ function writeTextFileSecure(path, content) {
317
+ ensurePrivateParent(path);
318
+ writeFileSync(path, content, { encoding: "utf8", mode: 384 });
319
+ hardenFilePermissions(path);
320
+ }
321
+ function writeBinaryFileSecure(path, content) {
322
+ ensurePrivateParent(path);
323
+ writeFileSync(path, content, { mode: 384 });
324
+ hardenFilePermissions(path);
325
+ }
326
+
327
+ // packages/core/src/artifacts.ts
328
+ function normalizeRelativePath(input) {
329
+ return input.replace(/\\/g, "/");
330
+ }
331
+ function walkFiles(rootPath) {
332
+ const entries = readdirSync(rootPath, { withFileTypes: true });
333
+ const files = [];
334
+ for (const entry of entries) {
335
+ const entryPath = join2(rootPath, entry.name);
336
+ if (entry.isDirectory()) {
337
+ files.push(...walkFiles(entryPath));
338
+ continue;
339
+ }
340
+ if (entry.isFile()) {
341
+ files.push(entryPath);
342
+ }
343
+ }
344
+ return files;
345
+ }
346
+ function defaultEncryptedFilePath(inputPath) {
347
+ return `${inputPath}.autho`;
348
+ }
349
+ function defaultDecryptedFilePath(inputPath) {
350
+ return inputPath.endsWith(".autho") ? inputPath.slice(0, -".autho".length) : `${inputPath}.decrypted`;
351
+ }
352
+ function defaultEncryptedFolderPath(inputPath) {
353
+ return `${inputPath}.autho-folder`;
354
+ }
355
+ function defaultDecryptedFolderPath(inputPath) {
356
+ return inputPath.endsWith(".autho-folder") ? inputPath.slice(0, -".autho-folder".length) : `${inputPath}.folder`;
357
+ }
358
+ function encryptFileArtifact(inputPath, outputPath, rootKey) {
359
+ const fileKey = randomBytes2(32);
360
+ const payload = encryptWithKey(readFileSync(inputPath), fileKey, `autho:file:${basename(inputPath)}`);
361
+ const envelope = {
362
+ kind: "file",
363
+ originalName: basename(inputPath),
364
+ payload,
365
+ version: 1,
366
+ wrappedKey: encryptWithKey(fileKey, rootKey, "autho:file:dek")
367
+ };
368
+ writeTextFileSecure(outputPath, JSON.stringify(envelope, null, 2));
369
+ return { outputPath };
370
+ }
371
+ function decryptFileArtifact(inputPath, outputPath, rootKey) {
372
+ const envelope = JSON.parse(readFileSync(inputPath, "utf8"));
373
+ if (envelope.kind !== "file" || envelope.version !== 1) {
374
+ throw new Error(`Unsupported file artifact: ${inputPath}`);
375
+ }
376
+ const fileKey = decryptWithKey(envelope.wrappedKey, rootKey, "autho:file:dek");
377
+ const content = decryptWithKey(envelope.payload, fileKey, `autho:file:${envelope.originalName}`);
378
+ writeBinaryFileSecure(outputPath, content);
379
+ return { outputPath };
380
+ }
381
+ function encryptFolderArtifact(inputPath, outputPath, rootKey) {
382
+ const folderKey = randomBytes2(32);
383
+ const files = walkFiles(inputPath);
384
+ const rootName = basename(inputPath);
385
+ const envelope = {
386
+ entries: files.map((filePath) => {
387
+ const relativePath = normalizeRelativePath(relative(inputPath, filePath));
388
+ return {
389
+ path: relativePath,
390
+ payload: encryptWithKey(readFileSync(filePath), folderKey, `autho:folder:${relativePath}`)
391
+ };
392
+ }),
393
+ kind: "folder",
394
+ rootName,
395
+ version: 1,
396
+ wrappedKey: encryptWithKey(folderKey, rootKey, `autho:folder:dek:${rootName}`)
397
+ };
398
+ writeTextFileSecure(outputPath, JSON.stringify(envelope, null, 2));
399
+ return {
400
+ fileCount: envelope.entries.length,
401
+ outputPath
402
+ };
403
+ }
404
+ function decryptFolderArtifact(inputPath, outputPath, rootKey) {
405
+ const envelope = JSON.parse(readFileSync(inputPath, "utf8"));
406
+ if (envelope.kind !== "folder" || envelope.version !== 1) {
407
+ throw new Error(`Unsupported folder artifact: ${inputPath}`);
408
+ }
409
+ const folderKey = decryptWithKey(envelope.wrappedKey, rootKey, `autho:folder:dek:${envelope.rootName}`);
410
+ const resolvedOutput = resolve2(outputPath);
411
+ ensurePrivateDir(outputPath);
412
+ for (const entry of envelope.entries) {
413
+ const destination = join2(outputPath, entry.path);
414
+ const resolvedDest = resolve2(destination);
415
+ if (!resolvedDest.startsWith(resolvedOutput + sep) && resolvedDest !== resolvedOutput) {
416
+ throw new Error(`Path traversal detected in folder artifact: ${entry.path}`);
417
+ }
418
+ ensurePrivateParent(destination);
419
+ const content = decryptWithKey(entry.payload, folderKey, `autho:folder:${entry.path}`);
420
+ writeBinaryFileSecure(destination, content);
421
+ }
422
+ return {
423
+ fileCount: envelope.entries.length,
424
+ outputPath
425
+ };
426
+ }
427
+ function assertPathIsDirectory(path) {
428
+ if (!statSync(path).isDirectory()) {
429
+ throw new Error(`Expected directory: ${path}`);
430
+ }
431
+ }
432
+ function assertPathIsFile(path) {
433
+ if (!statSync(path).isFile()) {
434
+ throw new Error(`Expected file: ${path}`);
435
+ }
436
+ }
437
+
438
+ // packages/core/src/index.ts
439
+ function requireValue(value, label) {
440
+ if (!value) {
441
+ throw new Error(`Missing required option: ${label}`);
442
+ }
443
+ return value;
444
+ }
445
+ function decodeBase32(input) {
446
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
447
+ const normalized = input.toUpperCase().replace(/=+$/g, "").replace(/\s+/g, "");
448
+ let bits = 0;
449
+ let value = 0;
450
+ const output = [];
451
+ for (const char of normalized) {
452
+ const index = alphabet.indexOf(char);
453
+ if (index === -1) {
454
+ throw new Error("OTP secret must be valid base32");
455
+ }
456
+ value = value << 5 | index;
457
+ bits += 5;
458
+ if (bits >= 8) {
459
+ output.push(value >>> bits - 8 & 255);
460
+ bits -= 8;
461
+ }
462
+ }
463
+ return Uint8Array.from(output);
464
+ }
465
+ function generateTotp(secret, options, now = Date.now()) {
466
+ const algorithm = (options?.algorithm ?? "sha1").toLowerCase();
467
+ const digits = options?.digits ?? 6;
468
+ const key = decodeBase32(secret);
469
+ const counter = Math.floor(now / 30000);
470
+ const message = Buffer.alloc(8);
471
+ let cursor = counter;
472
+ for (let index = 7;index >= 0; index -= 1) {
473
+ message[index] = cursor & 255;
474
+ cursor >>= 8;
475
+ }
476
+ const hash = createHmac(algorithm, Buffer.from(key)).update(message).digest();
477
+ const offset = hash[hash.length - 1] & 15;
478
+ const binary = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
479
+ const mod = 10 ** digits;
480
+ const code = String(binary % mod).padStart(digits, "0");
481
+ const expiresAt = new Date((counter + 1) * 30000).toISOString();
482
+ return { code, expiresAt };
483
+ }
484
+ function normalizeSecretType(type) {
485
+ if (type === "password" || type === "note" || type === "otp") {
486
+ return type;
487
+ }
488
+ throw new Error(`Unsupported secret type: ${type}`);
489
+ }
490
+ function parseProjectMappings(projectFile) {
491
+ const raw = JSON.parse(readFileSync2(projectFile, "utf8"));
492
+ return Object.entries(raw.env ?? {}).map(([envName, secretRef]) => ({
493
+ envName,
494
+ secretRef
495
+ }));
496
+ }
497
+ function parseLease(row) {
498
+ return {
499
+ createdAt: row.createdAt,
500
+ expiresAt: row.expiresAt,
501
+ id: row.id,
502
+ name: row.name,
503
+ revokedAt: row.revokedAt,
504
+ secretRefs: JSON.parse(row.secretRefs)
505
+ };
506
+ }
507
+ function toAuditEvent(row) {
508
+ return {
509
+ createdAt: row.createdAt,
510
+ eventType: row.eventType,
511
+ id: row.id,
512
+ message: row.message,
513
+ metadata: JSON.parse(row.metadata),
514
+ subjectRef: row.subjectRef,
515
+ subjectType: row.subjectType
516
+ };
517
+ }
518
+ function normalizeMetadata(input) {
519
+ return Object.fromEntries(Object.entries(input ?? {}).filter(([, value]) => value !== undefined && value !== null && value !== ""));
520
+ }
521
+ function parseLegacySecret(secret) {
522
+ const type = normalizeSecretType(requireValue(secret.type, "legacy.type"));
523
+ const name = requireValue(secret.name, "legacy.name");
524
+ const value = requireValue(secret.secret ?? secret.value, "legacy.secret");
525
+ if (type === "password") {
526
+ return {
527
+ metadata: normalizeMetadata({
528
+ description: secret.description,
529
+ url: secret.url
530
+ }),
531
+ name,
532
+ type,
533
+ username: secret.username,
534
+ value
535
+ };
536
+ }
537
+ if (type === "otp") {
538
+ return {
539
+ metadata: normalizeMetadata({
540
+ algorithm: secret.algorithm ?? "SHA1",
541
+ description: secret.description,
542
+ digits: secret.digits ?? 6
543
+ }),
544
+ name,
545
+ type,
546
+ username: secret.username,
547
+ value
548
+ };
549
+ }
550
+ return {
551
+ metadata: normalizeMetadata({
552
+ description: secret.description
553
+ }),
554
+ name,
555
+ type,
556
+ value
557
+ };
558
+ }
559
+ function quoteEnvValue(value) {
560
+ return JSON.stringify(value);
561
+ }
562
+ function summarizeCommand(cmd) {
563
+ return {
564
+ argCount: Math.max(0, cmd.length - 1),
565
+ executable: basename2(cmd[0] ?? "unknown")
566
+ };
567
+ }
568
+ function projectMappingsForStatus(projectFile) {
569
+ if (!projectFile || !existsSync2(projectFile)) {
570
+ return {
571
+ mappings: [],
572
+ path: projectFile ?? null
573
+ };
574
+ }
575
+ return {
576
+ mappings: parseProjectMappings(projectFile).map((mapping) => mapping.envName),
577
+ path: projectFile
578
+ };
579
+ }
580
+ function resolveMappings(options) {
581
+ const fromMaps = (options.maps ?? []).map((mapping) => {
582
+ const splitIndex = mapping.indexOf("=");
583
+ if (splitIndex === -1) {
584
+ throw new Error(`Invalid env mapping: ${mapping}`);
585
+ }
586
+ return {
587
+ envName: mapping.slice(0, splitIndex),
588
+ secretRef: mapping.slice(splitIndex + 1)
589
+ };
590
+ });
591
+ if (options.projectFile) {
592
+ if (!existsSync2(options.projectFile)) {
593
+ throw new Error(`Project mapping file not found: ${options.projectFile}`);
594
+ }
595
+ return [...parseProjectMappings(options.projectFile), ...fromMaps];
596
+ }
597
+ return fromMaps;
598
+ }
599
+ function writeProjectConfig(input) {
600
+ if (input.mappings.length === 0) {
601
+ throw new Error("Provide at least one env mapping");
602
+ }
603
+ if (!input.force && existsSync2(input.outputPath)) {
604
+ throw new Error(`Project config already exists: ${input.outputPath}`);
605
+ }
606
+ const env = Object.fromEntries(input.mappings.map((mapping) => [mapping.envName, mapping.secretRef]));
607
+ writeTextFileSecure(input.outputPath, JSON.stringify({
608
+ env,
609
+ generatedAt: new Date().toISOString(),
610
+ version: 1
611
+ }, null, 2) + `
612
+ `);
613
+ return {
614
+ mappingCount: input.mappings.length,
615
+ outputPath: input.outputPath
616
+ };
617
+ }
618
+
619
+ class VaultService {
620
+ static initialize(vaultPath, password) {
621
+ const db = new AuthoDatabase(vaultPath);
622
+ try {
623
+ if (db.getVaultConfig()) {
624
+ throw new Error(`Vault already initialized at ${vaultPath}`);
625
+ }
626
+ const { config } = createVaultConfig(password);
627
+ db.setVaultConfig(config);
628
+ db.insertAudit({
629
+ createdAt: new Date().toISOString(),
630
+ eventType: "vault.initialized",
631
+ id: randomId(),
632
+ message: "Vault initialized",
633
+ metadata: JSON.stringify({ version: config.version }),
634
+ subjectRef: null,
635
+ subjectType: "vault"
636
+ });
637
+ return { vaultPath };
638
+ } finally {
639
+ db.close();
640
+ }
641
+ }
642
+ static status(vaultPath, options) {
643
+ const db = new AuthoDatabase(vaultPath);
644
+ try {
645
+ const config = db.getVaultConfig();
646
+ const project = projectMappingsForStatus(options?.projectFile);
647
+ if (!config) {
648
+ return {
649
+ activeLeaseCount: 0,
650
+ auditEventCount: 0,
651
+ initialized: false,
652
+ projectFile: project.path,
653
+ projectMappings: project.mappings,
654
+ secretCount: 0,
655
+ unlocked: false,
656
+ vaultPath
657
+ };
658
+ }
659
+ if (!options?.password) {
660
+ return {
661
+ activeLeaseCount: 0,
662
+ auditEventCount: 0,
663
+ initialized: true,
664
+ projectFile: project.path,
665
+ projectMappings: project.mappings,
666
+ secretCount: 0,
667
+ unlocked: false,
668
+ vaultPath
669
+ };
670
+ }
671
+ const rootKey = unlockRootKey(options.password, config);
672
+ const session = new VaultSession(db, rootKey);
673
+ return session.status(vaultPath, project.path, project.mappings);
674
+ } finally {
675
+ db.close();
676
+ }
677
+ }
678
+ static unlock(vaultPath, password) {
679
+ const db = new AuthoDatabase(vaultPath);
680
+ const config = db.getVaultConfig();
681
+ if (!config) {
682
+ db.close();
683
+ throw new Error(`Vault is not initialized at ${vaultPath}`);
684
+ }
685
+ try {
686
+ const rootKey = unlockRootKey(password, config);
687
+ return new VaultSession(db, rootKey);
688
+ } catch (error) {
689
+ db.close();
690
+ throw new Error("Invalid vault password", { cause: error });
691
+ }
692
+ }
693
+ }
694
+
695
+ class VaultSession {
696
+ db;
697
+ rootKey;
698
+ constructor(db, rootKey) {
699
+ this.db = db;
700
+ this.rootKey = rootKey;
701
+ }
702
+ close() {
703
+ this.db.close();
704
+ }
705
+ status(vaultPath, projectFile, projectMappings = []) {
706
+ return {
707
+ activeLeaseCount: this.db.countActiveLeases(new Date().toISOString()),
708
+ auditEventCount: this.db.countAuditEvents(),
709
+ initialized: true,
710
+ projectFile: projectFile ?? null,
711
+ projectMappings,
712
+ secretCount: this.db.countSecrets(),
713
+ unlocked: true,
714
+ vaultPath
715
+ };
716
+ }
717
+ audit(eventType, subjectType, subjectRef, message, metadata) {
718
+ this.db.insertAudit({
719
+ createdAt: new Date().toISOString(),
720
+ eventType,
721
+ id: randomId(),
722
+ message,
723
+ metadata: JSON.stringify(metadata),
724
+ subjectRef,
725
+ subjectType
726
+ });
727
+ }
728
+ unwrapSecret(row) {
729
+ const wrappedKey = JSON.parse(row.wrappedKey);
730
+ const payload = JSON.parse(row.payload);
731
+ const dek = decryptWithKey(wrappedKey, this.rootKey, `autho:secret:${row.id}:dek`);
732
+ const secret = JSON.parse(decryptWithKey(payload, dek, `autho:secret:${row.id}:payload`).toString("utf8"));
733
+ return {
734
+ createdAt: row.createdAt,
735
+ id: row.id,
736
+ metadata: normalizeMetadata(secret.metadata),
737
+ name: row.name,
738
+ type: normalizeSecretType(row.type),
739
+ updatedAt: row.updatedAt,
740
+ username: secret.username,
741
+ value: secret.value
742
+ };
743
+ }
744
+ getSecretOrThrow(ref) {
745
+ const row = this.db.findSecret(ref);
746
+ if (!row) {
747
+ throw new Error(`Secret not found: ${ref}`);
748
+ }
749
+ return this.unwrapSecret(row);
750
+ }
751
+ getLeaseOrThrow(id) {
752
+ const row = this.db.findLease(id);
753
+ if (!row) {
754
+ throw new Error(`Lease not found: ${id}`);
755
+ }
756
+ return parseLease(row);
757
+ }
758
+ assertLeaseAllows(leaseId, secretRef) {
759
+ if (!leaseId) {
760
+ return;
761
+ }
762
+ const lease = this.getLeaseOrThrow(leaseId);
763
+ if (lease.revokedAt) {
764
+ throw new Error(`Lease revoked: ${lease.id}`);
765
+ }
766
+ if (Date.parse(lease.expiresAt) <= Date.now()) {
767
+ throw new Error(`Lease expired: ${lease.id}`);
768
+ }
769
+ const secret = this.getSecretOrThrow(secretRef);
770
+ if (!lease.secretRefs.includes(secret.id) && !lease.secretRefs.includes(secret.name)) {
771
+ throw new Error(`Lease ${lease.id} does not allow secret ${secretRef}`);
772
+ }
773
+ }
774
+ addSecret(input) {
775
+ requireValue(input.name, "--name");
776
+ requireValue(input.value, "--value");
777
+ const type = normalizeSecretType(input.type);
778
+ if (this.db.findSecret(input.name)) {
779
+ throw new Error(`Secret already exists: ${input.name}`);
780
+ }
781
+ const now = new Date().toISOString();
782
+ const id = randomId();
783
+ const dek = randomBytes3(32);
784
+ const payload = encryptWithKey(JSON.stringify({
785
+ metadata: normalizeMetadata(input.metadata),
786
+ username: input.username ?? null,
787
+ value: input.value
788
+ }), dek, `autho:secret:${id}:payload`);
789
+ const wrappedKey = encryptWithKey(dek, this.rootKey, `autho:secret:${id}:dek`);
790
+ this.db.insertSecret({
791
+ createdAt: now,
792
+ id,
793
+ name: input.name,
794
+ payload: JSON.stringify(payload),
795
+ type,
796
+ updatedAt: now,
797
+ wrappedKey: JSON.stringify(wrappedKey)
798
+ });
799
+ this.audit("secret.created", "secret", id, "Secret created", {
800
+ metadataKeyCount: Object.keys(normalizeMetadata(input.metadata)).length,
801
+ type
802
+ });
803
+ return {
804
+ createdAt: now,
805
+ id,
806
+ name: input.name,
807
+ type,
808
+ updatedAt: now
809
+ };
810
+ }
811
+ importLegacyFile(filePath, options) {
812
+ const raw = JSON.parse(readFileSync2(filePath, "utf8"));
813
+ let imported = 0;
814
+ let skipped = 0;
815
+ for (const entry of raw) {
816
+ if (!entry) {
817
+ continue;
818
+ }
819
+ const parsed = parseLegacySecret(entry);
820
+ if (this.db.findSecret(parsed.name)) {
821
+ if (options?.skipExisting ?? true) {
822
+ skipped += 1;
823
+ continue;
824
+ }
825
+ throw new Error(`Secret already exists: ${parsed.name}`);
826
+ }
827
+ this.addSecret(parsed);
828
+ imported += 1;
829
+ }
830
+ this.audit("import.legacy", "vault", null, "Legacy backup imported", {
831
+ imported,
832
+ skipped
833
+ });
834
+ return { imported, skipped };
835
+ }
836
+ listSecrets() {
837
+ return this.db.listSecrets().map((row) => ({
838
+ createdAt: row.createdAt,
839
+ id: row.id,
840
+ name: row.name,
841
+ type: normalizeSecretType(row.type),
842
+ updatedAt: row.updatedAt
843
+ }));
844
+ }
845
+ getSecret(ref) {
846
+ const secret = this.getSecretOrThrow(ref);
847
+ this.audit("secret.read", "secret", secret.id, "Secret read", {
848
+ metadataKeyCount: Object.keys(secret.metadata).length,
849
+ type: secret.type
850
+ });
851
+ return secret;
852
+ }
853
+ removeSecret(ref) {
854
+ const secret = this.getSecretOrThrow(ref);
855
+ this.db.deleteSecret(secret.id);
856
+ this.audit("secret.deleted", "secret", secret.id, "Secret deleted", {
857
+ type: secret.type
858
+ });
859
+ return { id: secret.id, name: secret.name };
860
+ }
861
+ generateOtp(ref) {
862
+ const secret = this.getSecretOrThrow(ref);
863
+ if (secret.type !== "otp") {
864
+ throw new Error(`Secret is not an OTP secret: ${ref}`);
865
+ }
866
+ const result = generateTotp(secret.value, {
867
+ algorithm: secret.metadata.algorithm,
868
+ digits: secret.metadata.digits
869
+ });
870
+ this.audit("otp.generated", "secret", secret.id, "OTP code generated", {
871
+ expiresAt: result.expiresAt
872
+ });
873
+ return {
874
+ code: result.code,
875
+ expiresAt: result.expiresAt,
876
+ secret: secret.name
877
+ };
878
+ }
879
+ createLease(input) {
880
+ if (input.secretRefs.length === 0) {
881
+ throw new Error("Lease requires at least one --secret");
882
+ }
883
+ if (input.ttlSeconds <= 0) {
884
+ throw new Error("Lease ttl must be greater than zero");
885
+ }
886
+ const resolved = input.secretRefs.map((ref) => this.getSecretOrThrow(ref));
887
+ const now = new Date().toISOString();
888
+ const expiresAt = new Date(Date.now() + input.ttlSeconds * 1000).toISOString();
889
+ const lease = {
890
+ createdAt: now,
891
+ expiresAt,
892
+ id: randomId(),
893
+ name: input.name || "session",
894
+ revokedAt: null,
895
+ secretRefs: resolved.map((secret) => secret.id)
896
+ };
897
+ this.db.insertLease({
898
+ createdAt: lease.createdAt,
899
+ expiresAt: lease.expiresAt,
900
+ id: lease.id,
901
+ name: lease.name,
902
+ revokedAt: lease.revokedAt,
903
+ secretRefs: JSON.stringify(lease.secretRefs)
904
+ });
905
+ this.audit("lease.created", "lease", lease.id, "Lease created", {
906
+ expiresAt,
907
+ secretCount: lease.secretRefs.length
908
+ });
909
+ return lease;
910
+ }
911
+ revokeLease(id) {
912
+ const lease = this.getLeaseOrThrow(id);
913
+ const revokedAt = new Date().toISOString();
914
+ this.db.revokeLease(id, revokedAt);
915
+ this.audit("lease.revoked", "lease", id, "Lease revoked", {
916
+ id
917
+ });
918
+ return {
919
+ ...lease,
920
+ revokedAt
921
+ };
922
+ }
923
+ buildEnv(mappings, leaseId) {
924
+ if (mappings.length === 0) {
925
+ throw new Error("Provide at least one env mapping");
926
+ }
927
+ const output = {};
928
+ for (const mapping of mappings) {
929
+ this.assertLeaseAllows(leaseId, mapping.secretRef);
930
+ const secret = this.getSecretOrThrow(mapping.secretRef);
931
+ output[mapping.envName] = secret.value;
932
+ }
933
+ return output;
934
+ }
935
+ renderEnv(mappings, leaseId) {
936
+ const env = this.buildEnv(mappings, leaseId);
937
+ this.audit("env.rendered", "lease", leaseId ?? null, "Environment rendered", {
938
+ leaseId: leaseId ?? null,
939
+ varCount: Object.keys(env).length
940
+ });
941
+ return env;
942
+ }
943
+ syncEnvFile(input) {
944
+ const env = this.buildEnv(input.mappings, input.leaseId);
945
+ if (!input.force && existsSync2(input.outputPath)) {
946
+ throw new Error(`Env file already exists: ${input.outputPath}`);
947
+ }
948
+ const createdAt = new Date().toISOString();
949
+ const expiresAt = input.ttlSeconds ? new Date(Date.now() + input.ttlSeconds * 1000).toISOString() : null;
950
+ const lines = [
951
+ "# autho-generated=true",
952
+ `# autho-created-at=${createdAt}`,
953
+ `# autho-expires-at=${expiresAt ?? ""}`,
954
+ ...Object.entries(env).map(([key, value]) => `${key}=${quoteEnvValue(value)}`),
955
+ ""
956
+ ];
957
+ writeTextFileSecure(input.outputPath, lines.join(`
958
+ `));
959
+ this.audit("env.synced", "lease", input.leaseId ?? null, "Environment file written", {
960
+ expiresAt,
961
+ leaseId: input.leaseId ?? null,
962
+ varCount: Object.keys(env).length
963
+ });
964
+ return {
965
+ expiresAt,
966
+ outputPath: input.outputPath,
967
+ varCount: Object.keys(env).length
968
+ };
969
+ }
970
+ runExec(input) {
971
+ if (input.cmd.length === 0) {
972
+ throw new Error("Missing command after --");
973
+ }
974
+ const injectedEnv = this.buildEnv(input.mappings, input.leaseId);
975
+ const result = spawnSync(input.cmd[0], input.cmd.slice(1), {
976
+ env: {
977
+ ...process.env,
978
+ ...injectedEnv
979
+ },
980
+ stdio: "pipe"
981
+ });
982
+ this.audit("exec.run", "lease", input.leaseId ?? null, "Injected command executed", {
983
+ ...summarizeCommand(input.cmd),
984
+ envCount: Object.keys(injectedEnv).length,
985
+ exitCode: result.status ?? 1,
986
+ leaseId: input.leaseId ?? null
987
+ });
988
+ return {
989
+ exitCode: result.status ?? 1,
990
+ stderr: (result.stderr ?? Buffer.from("")).toString("utf8"),
991
+ stdout: (result.stdout ?? Buffer.from("")).toString("utf8")
992
+ };
993
+ }
994
+ encryptFile(inputPath, outputPath, options) {
995
+ assertPathIsFile(inputPath);
996
+ const resolvedOutput = outputPath ?? defaultEncryptedFilePath(inputPath);
997
+ if (!options?.force && existsSync2(resolvedOutput)) {
998
+ throw new Error(`Output file already exists: ${resolvedOutput}`);
999
+ }
1000
+ const result = encryptFileArtifact(inputPath, resolvedOutput, this.rootKey);
1001
+ this.audit("file.encrypted", "artifact", null, "File encrypted", {
1002
+ kind: "file"
1003
+ });
1004
+ return result;
1005
+ }
1006
+ decryptFile(inputPath, outputPath, options) {
1007
+ assertPathIsFile(inputPath);
1008
+ const resolvedOutput = outputPath ?? defaultDecryptedFilePath(inputPath);
1009
+ if (!options?.force && existsSync2(resolvedOutput)) {
1010
+ throw new Error(`Output file already exists: ${resolvedOutput}`);
1011
+ }
1012
+ const result = decryptFileArtifact(inputPath, resolvedOutput, this.rootKey);
1013
+ this.audit("file.decrypted", "artifact", null, "File decrypted", {
1014
+ kind: "file"
1015
+ });
1016
+ return result;
1017
+ }
1018
+ encryptFolder(inputPath, outputPath, options) {
1019
+ assertPathIsDirectory(inputPath);
1020
+ const resolvedOutput = outputPath ?? defaultEncryptedFolderPath(inputPath);
1021
+ if (!options?.force && existsSync2(resolvedOutput)) {
1022
+ throw new Error(`Output file already exists: ${resolvedOutput}`);
1023
+ }
1024
+ const result = encryptFolderArtifact(inputPath, resolvedOutput, this.rootKey);
1025
+ this.audit("folder.encrypted", "artifact", null, "Folder encrypted", {
1026
+ fileCount: result.fileCount,
1027
+ kind: "folder"
1028
+ });
1029
+ return result;
1030
+ }
1031
+ decryptFolder(inputPath, outputPath, options) {
1032
+ assertPathIsFile(inputPath);
1033
+ const resolvedOutput = outputPath ?? defaultDecryptedFolderPath(inputPath);
1034
+ if (!options?.force && existsSync2(resolvedOutput)) {
1035
+ throw new Error(`Output path already exists: ${resolvedOutput}`);
1036
+ }
1037
+ const result = decryptFolderArtifact(inputPath, resolvedOutput, this.rootKey);
1038
+ this.audit("folder.decrypted", "artifact", null, "Folder decrypted", {
1039
+ fileCount: result.fileCount,
1040
+ kind: "folder"
1041
+ });
1042
+ return result;
1043
+ }
1044
+ listAudit(limit = 50) {
1045
+ return this.db.listAudit(limit).map(toAuditEvent);
1046
+ }
1047
+ }
1048
+
1049
+ // packages/core/src/daemon.ts
1050
+ var DAEMON_TOKEN_SERVICE = "autho.daemon";
1051
+ function daemonTokenName(statePath) {
1052
+ return createHash("sha256").update(statePath).digest("hex");
1053
+ }
1054
+ async function storeDaemonToken(statePath, token) {
1055
+ const tokenName = daemonTokenName(statePath);
1056
+ if (process.env.AUTHO_DISABLE_OS_SECRETS !== "1") {
1057
+ try {
1058
+ await Bun.secrets.set({
1059
+ name: tokenName,
1060
+ service: DAEMON_TOKEN_SERVICE,
1061
+ value: token
1062
+ });
1063
+ return {
1064
+ token: null,
1065
+ tokenName,
1066
+ tokenStorage: "os"
1067
+ };
1068
+ } catch {}
1069
+ }
1070
+ return {
1071
+ token,
1072
+ tokenName: null,
1073
+ tokenStorage: "file"
1074
+ };
1075
+ }
1076
+ async function resolveDaemonToken(state) {
1077
+ if (state.tokenStorage === "os") {
1078
+ if (!state.tokenName) {
1079
+ throw new Error("Daemon state is missing tokenName for OS secret storage");
1080
+ }
1081
+ const token = await Bun.secrets.get({
1082
+ name: state.tokenName,
1083
+ service: DAEMON_TOKEN_SERVICE
1084
+ });
1085
+ if (!token) {
1086
+ throw new Error("Daemon token not found in OS secret storage");
1087
+ }
1088
+ return token;
1089
+ }
1090
+ if (!state.token) {
1091
+ throw new Error("Daemon state is missing token");
1092
+ }
1093
+ return state.token;
1094
+ }
1095
+ async function deleteStoredDaemonToken(state) {
1096
+ if (!state || state.tokenStorage !== "os" || !state.tokenName) {
1097
+ return;
1098
+ }
1099
+ try {
1100
+ await Bun.secrets.delete({
1101
+ name: state.tokenName,
1102
+ service: DAEMON_TOKEN_SERVICE
1103
+ });
1104
+ } catch {}
1105
+ }
1106
+ function readDaemonState(statePath) {
1107
+ if (!existsSync3(statePath)) {
1108
+ return null;
1109
+ }
1110
+ const stored = JSON.parse(readFileSync3(statePath, "utf8"));
1111
+ return {
1112
+ pid: stored.pid,
1113
+ port: stored.port,
1114
+ startedAt: stored.startedAt,
1115
+ token: stored.token ?? null,
1116
+ tokenName: stored.tokenName ?? null,
1117
+ tokenStorage: stored.tokenStorage ?? (stored.token ? "file" : "os"),
1118
+ vaultPath: stored.vaultPath,
1119
+ version: stored.version
1120
+ };
1121
+ }
1122
+ async function writeDaemonState(statePath, state) {
1123
+ const tokenState = await storeDaemonToken(statePath, state.token);
1124
+ writeTextFileSecure(statePath, JSON.stringify({
1125
+ pid: state.pid,
1126
+ port: state.port,
1127
+ startedAt: state.startedAt,
1128
+ token: tokenState.token ?? undefined,
1129
+ tokenName: tokenState.tokenName ?? undefined,
1130
+ tokenStorage: tokenState.tokenStorage,
1131
+ vaultPath: state.vaultPath,
1132
+ version: state.version
1133
+ }, null, 2) + `
1134
+ `);
1135
+ }
1136
+ async function deleteDaemonState(statePath) {
1137
+ const state = readDaemonState(statePath);
1138
+ await deleteStoredDaemonToken(state);
1139
+ if (existsSync3(statePath)) {
1140
+ rmSync(statePath, { force: true });
1141
+ }
1142
+ }
1143
+ function openSessionFromRootKey(rootKey, vaultPath) {
1144
+ return new VaultSession(new AuthoDatabase(vaultPath), rootKey);
1145
+ }
1146
+ function unlockVaultRootKey(vaultPath, password) {
1147
+ const db = new AuthoDatabase(vaultPath);
1148
+ try {
1149
+ const config = db.getVaultConfig();
1150
+ if (!config) {
1151
+ throw new Error(`Vault is not initialized at ${vaultPath}`);
1152
+ }
1153
+ return unlockRootKey(password, config);
1154
+ } finally {
1155
+ db.close();
1156
+ }
1157
+ }
1158
+ async function readJson(request) {
1159
+ return await request.json();
1160
+ }
1161
+ function json(data, status = 200) {
1162
+ return new Response(JSON.stringify(data, null, 2), {
1163
+ headers: {
1164
+ "content-type": "application/json; charset=utf-8"
1165
+ },
1166
+ status
1167
+ });
1168
+ }
1169
+ function unauthorized() {
1170
+ return json({ error: "Unauthorized daemon request" }, 401);
1171
+ }
1172
+ function getBearerToken(request) {
1173
+ const header = request.headers.get("authorization");
1174
+ if (!header?.startsWith("Bearer ")) {
1175
+ return null;
1176
+ }
1177
+ return header.slice("Bearer ".length);
1178
+ }
1179
+ async function startDaemonServer(options) {
1180
+ const sessions = new Map;
1181
+ const startedAt = new Date().toISOString();
1182
+ const token = randomId(24);
1183
+ const cleanupExpiredSessions = () => {
1184
+ const now = Date.now();
1185
+ for (const [id, session] of sessions.entries()) {
1186
+ if (Date.parse(session.expiresAt) <= now) {
1187
+ sessions.delete(id);
1188
+ }
1189
+ }
1190
+ };
1191
+ let server = null;
1192
+ const shutdown = () => {
1193
+ deleteDaemonState(options.statePath).then(() => {
1194
+ server?.stop(true);
1195
+ process.exit(0);
1196
+ });
1197
+ };
1198
+ const auth = (request) => {
1199
+ const provided = getBearerToken(request);
1200
+ if (!provided || provided.length !== token.length || !timingSafeEqual(Buffer.from(provided), Buffer.from(token))) {
1201
+ return unauthorized();
1202
+ }
1203
+ return null;
1204
+ };
1205
+ server = Bun.serve({
1206
+ async fetch(request) {
1207
+ cleanupExpiredSessions();
1208
+ const url = new URL(request.url);
1209
+ if (request.method === "GET" && url.pathname === "/health") {
1210
+ return json({
1211
+ ok: true,
1212
+ running: true,
1213
+ startedAt
1214
+ });
1215
+ }
1216
+ const authResponse = auth(request);
1217
+ if (authResponse) {
1218
+ return authResponse;
1219
+ }
1220
+ try {
1221
+ if (request.method === "POST" && url.pathname === "/status") {
1222
+ const db = new AuthoDatabase(options.vaultPath);
1223
+ try {
1224
+ const config = db.getVaultConfig();
1225
+ const status = {
1226
+ activeLeaseCount: db.countActiveLeases(new Date().toISOString()),
1227
+ auditEventCount: db.countAuditEvents(),
1228
+ initialized: config !== null,
1229
+ projectFile: null,
1230
+ projectMappings: [],
1231
+ secretCount: db.countSecrets(),
1232
+ unlocked: sessions.size > 0,
1233
+ vaultPath: options.vaultPath
1234
+ };
1235
+ return json({
1236
+ activeSessions: sessions.size,
1237
+ daemonStartedAt: startedAt,
1238
+ status
1239
+ });
1240
+ } finally {
1241
+ db.close();
1242
+ }
1243
+ }
1244
+ if (request.method === "POST" && url.pathname === "/unlock") {
1245
+ const body = await readJson(request);
1246
+ const ttlSeconds = body.ttlSeconds ?? 900;
1247
+ if (ttlSeconds <= 0 || ttlSeconds > 86400) {
1248
+ return json({ error: "Unlock ttl must be between 1 and 86400 seconds" }, 400);
1249
+ }
1250
+ const rootKey = unlockVaultRootKey(options.vaultPath, body.password);
1251
+ const sessionId = randomId();
1252
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1253
+ sessions.set(sessionId, {
1254
+ expiresAt,
1255
+ id: sessionId,
1256
+ rootKey,
1257
+ vaultPath: options.vaultPath
1258
+ });
1259
+ return json({
1260
+ expiresAt,
1261
+ sessionId
1262
+ });
1263
+ }
1264
+ if (request.method === "POST" && url.pathname === "/lock") {
1265
+ const body = await readJson(request);
1266
+ sessions.delete(body.sessionId);
1267
+ return json({ locked: true, sessionId: body.sessionId });
1268
+ }
1269
+ if (request.method === "POST" && url.pathname === "/env/render") {
1270
+ const body = await readJson(request);
1271
+ const session = sessions.get(body.sessionId);
1272
+ if (!session) {
1273
+ return json({ error: `Unknown daemon session: ${body.sessionId}` }, 404);
1274
+ }
1275
+ const vaultSession = openSessionFromRootKey(session.rootKey, session.vaultPath);
1276
+ try {
1277
+ return json(vaultSession.renderEnv(body.mappings, body.leaseId));
1278
+ } finally {
1279
+ vaultSession.close();
1280
+ }
1281
+ }
1282
+ if (request.method === "POST" && url.pathname === "/exec") {
1283
+ const body = await readJson(request);
1284
+ const session = sessions.get(body.sessionId);
1285
+ if (!session) {
1286
+ return json({ error: `Unknown daemon session: ${body.sessionId}` }, 404);
1287
+ }
1288
+ const vaultSession = openSessionFromRootKey(session.rootKey, session.vaultPath);
1289
+ try {
1290
+ return json(vaultSession.runExec(body));
1291
+ } finally {
1292
+ vaultSession.close();
1293
+ }
1294
+ }
1295
+ if (request.method === "POST" && url.pathname === "/shutdown") {
1296
+ shutdown();
1297
+ return json({ ok: true, stopped: true });
1298
+ }
1299
+ } catch (error) {
1300
+ const message = error instanceof Error ? error.message : String(error);
1301
+ return json({ error: message }, 400);
1302
+ }
1303
+ return json({ error: "Not found" }, 404);
1304
+ },
1305
+ hostname: options.host,
1306
+ port: options.port
1307
+ });
1308
+ await writeDaemonState(options.statePath, {
1309
+ pid: process.pid,
1310
+ port: server.port,
1311
+ startedAt,
1312
+ token,
1313
+ vaultPath: options.vaultPath,
1314
+ version: 1
1315
+ });
1316
+ const onSignal = () => {
1317
+ shutdown();
1318
+ process.exit(0);
1319
+ };
1320
+ process.on("SIGINT", onSignal);
1321
+ process.on("SIGTERM", onSignal);
1322
+ await new Promise(() => {});
1323
+ }
1324
+ async function waitForDaemonStateDeletion(statePath) {
1325
+ for (let attempt = 0;attempt < 20; attempt += 1) {
1326
+ if (!existsSync3(statePath)) {
1327
+ return true;
1328
+ }
1329
+ await Bun.sleep(25);
1330
+ }
1331
+ return !existsSync3(statePath);
1332
+ }
1333
+ async function daemonRequest(state, path, body) {
1334
+ const token = await resolveDaemonToken(state);
1335
+ const response = await fetch(`http://127.0.0.1:${state.port}${path}`, {
1336
+ body: JSON.stringify(body ?? {}),
1337
+ headers: {
1338
+ authorization: `Bearer ${token}`,
1339
+ "content-type": "application/json"
1340
+ },
1341
+ method: "POST"
1342
+ });
1343
+ const data = await response.json();
1344
+ if (!response.ok) {
1345
+ throw new Error(data.error ?? `Daemon request failed: ${response.status}`);
1346
+ }
1347
+ return data;
1348
+ }
1349
+ async function daemonStatus(options) {
1350
+ const state = readDaemonState(options.statePath);
1351
+ if (!state) {
1352
+ throw new Error(`Daemon state not found: ${options.statePath}`);
1353
+ }
1354
+ return daemonRequest(state, "/status", {});
1355
+ }
1356
+ async function daemonUnlock(options) {
1357
+ const state = readDaemonState(options.statePath);
1358
+ if (!state) {
1359
+ throw new Error(`Daemon state not found: ${options.statePath}`);
1360
+ }
1361
+ return daemonRequest(state, "/unlock", {
1362
+ password: options.password,
1363
+ ttlSeconds: options.ttlSeconds
1364
+ });
1365
+ }
1366
+ async function daemonLock(options) {
1367
+ const state = readDaemonState(options.statePath);
1368
+ if (!state) {
1369
+ throw new Error(`Daemon state not found: ${options.statePath}`);
1370
+ }
1371
+ return daemonRequest(state, "/lock", { sessionId: options.sessionId });
1372
+ }
1373
+ async function daemonRenderEnv(options) {
1374
+ const state = readDaemonState(options.statePath);
1375
+ if (!state) {
1376
+ throw new Error(`Daemon state not found: ${options.statePath}`);
1377
+ }
1378
+ return daemonRequest(state, "/env/render", options);
1379
+ }
1380
+ async function daemonExec(options) {
1381
+ const state = readDaemonState(options.statePath);
1382
+ if (!state) {
1383
+ throw new Error(`Daemon state not found: ${options.statePath}`);
1384
+ }
1385
+ return daemonRequest(state, "/exec", options);
1386
+ }
1387
+ async function daemonStop(options) {
1388
+ const state = readDaemonState(options.statePath);
1389
+ if (!state) {
1390
+ throw new Error(`Daemon state not found: ${options.statePath}`);
1391
+ }
1392
+ try {
1393
+ const result = await daemonRequest(state, "/shutdown", {});
1394
+ await waitForDaemonStateDeletion(options.statePath);
1395
+ return result;
1396
+ } catch (error) {
1397
+ if (await waitForDaemonStateDeletion(options.statePath)) {
1398
+ return { ok: true, stopped: true };
1399
+ }
1400
+ throw error;
1401
+ }
1402
+ }
1403
+
1404
+ // apps/cli/src/index.ts
1405
+ function parseArgs(argv) {
1406
+ const dashDashIndex = argv.indexOf("--");
1407
+ const main = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
1408
+ const passthrough = dashDashIndex === -1 ? [] : argv.slice(dashDashIndex + 1);
1409
+ const positionals = [];
1410
+ const options = {};
1411
+ for (let index = 0;index < main.length; index += 1) {
1412
+ const token = main[index];
1413
+ if (!token.startsWith("--")) {
1414
+ positionals.push(token);
1415
+ continue;
1416
+ }
1417
+ const key = token.slice(2);
1418
+ const next = main[index + 1];
1419
+ if (!next || next.startsWith("--")) {
1420
+ options[key] = true;
1421
+ continue;
1422
+ }
1423
+ const current = options[key];
1424
+ if (current === undefined) {
1425
+ options[key] = next;
1426
+ } else if (Array.isArray(current)) {
1427
+ current.push(next);
1428
+ } else {
1429
+ options[key] = [current, next];
1430
+ }
1431
+ index += 1;
1432
+ }
1433
+ return { options, passthrough, positionals };
1434
+ }
1435
+ function getString(args, key) {
1436
+ const value = args.options[key];
1437
+ return typeof value === "string" ? value : undefined;
1438
+ }
1439
+ function getStrings(args, key) {
1440
+ const value = args.options[key];
1441
+ if (Array.isArray(value)) {
1442
+ return value.filter((entry) => typeof entry === "string");
1443
+ }
1444
+ if (typeof value === "string") {
1445
+ return [value];
1446
+ }
1447
+ return [];
1448
+ }
1449
+ function getBoolean(args, key) {
1450
+ return args.options[key] === true;
1451
+ }
1452
+ function required(value, label) {
1453
+ if (!value) {
1454
+ throw new Error(`Missing required option: ${label}`);
1455
+ }
1456
+ return value;
1457
+ }
1458
+ function requirePositiveInt(value, label) {
1459
+ const num = Number(value);
1460
+ if (!Number.isFinite(num) || num <= 0 || Math.floor(num) !== num) {
1461
+ throw new Error(`${label} must be a positive integer`);
1462
+ }
1463
+ return num;
1464
+ }
1465
+ function output(value, jsonMode = false) {
1466
+ if (jsonMode) {
1467
+ console.log(JSON.stringify(value, null, 2));
1468
+ return;
1469
+ }
1470
+ if (Array.isArray(value)) {
1471
+ console.table(value);
1472
+ return;
1473
+ }
1474
+ if (typeof value === "object" && value !== null) {
1475
+ console.log(JSON.stringify(value, null, 2));
1476
+ return;
1477
+ }
1478
+ console.log(value);
1479
+ }
1480
+ function absolutePath(path) {
1481
+ return resolve3(path);
1482
+ }
1483
+ function buildSecretMetadata(args) {
1484
+ return Object.fromEntries(Object.entries({
1485
+ algorithm: getString(args, "algorithm"),
1486
+ description: getString(args, "description"),
1487
+ digits: getString(args, "digits") ? Number(getString(args, "digits")) : undefined,
1488
+ url: getString(args, "url")
1489
+ }).filter(([, value]) => value !== undefined));
1490
+ }
1491
+ async function readBufferedStdin() {
1492
+ process.stdin.setEncoding("utf8");
1493
+ let input = "";
1494
+ for await (const chunk of process.stdin) {
1495
+ input += chunk;
1496
+ }
1497
+ return input.split(/\r?\n/);
1498
+ }
1499
+ async function createPromptAdapter() {
1500
+ if (process.stdin.isTTY) {
1501
+ const rl = createInterface({
1502
+ input: process.stdin,
1503
+ output: process.stdout
1504
+ });
1505
+ return {
1506
+ ask: async (prompt) => (await rl.question(prompt)).trim(),
1507
+ close: () => rl.close()
1508
+ };
1509
+ }
1510
+ const answers = await readBufferedStdin();
1511
+ let index = 0;
1512
+ return {
1513
+ ask: async (prompt) => {
1514
+ process.stdout.write(prompt);
1515
+ const value = answers[index] ?? "";
1516
+ index += 1;
1517
+ return value.trim();
1518
+ },
1519
+ close: () => {
1520
+ return;
1521
+ }
1522
+ };
1523
+ }
1524
+ async function askPassword(prompt, initial) {
1525
+ if (initial) {
1526
+ return initial;
1527
+ }
1528
+ return prompt.ask("Master password: ");
1529
+ }
1530
+ async function runPromptMode(vaultPath, initialPassword) {
1531
+ const prompt = await createPromptAdapter();
1532
+ try {
1533
+ const password = await askPassword(prompt, initialPassword);
1534
+ const session = VaultService.unlock(vaultPath, password);
1535
+ try {
1536
+ const action = (await prompt.ask("Action [create/read/list/delete/otp/exit]: ")).toLowerCase();
1537
+ if (action === "exit") {
1538
+ return;
1539
+ }
1540
+ if (action === "list") {
1541
+ output(session.listSecrets());
1542
+ return;
1543
+ }
1544
+ if (action === "read") {
1545
+ const ref = await prompt.ask("Secret ref: ");
1546
+ output(session.getSecret(ref));
1547
+ return;
1548
+ }
1549
+ if (action === "delete") {
1550
+ const ref = await prompt.ask("Secret ref: ");
1551
+ output(session.removeSecret(ref));
1552
+ return;
1553
+ }
1554
+ if (action === "otp") {
1555
+ const ref = await prompt.ask("OTP ref: ");
1556
+ output(session.generateOtp(ref));
1557
+ return;
1558
+ }
1559
+ if (action === "create") {
1560
+ const name = await prompt.ask("Name: ");
1561
+ const type = (await prompt.ask("Type [password/note/otp]: ")).toLowerCase();
1562
+ const value = await prompt.ask("Value: ");
1563
+ const username = type !== "note" ? await prompt.ask("Username (optional): ") : "";
1564
+ const url = type === "password" ? await prompt.ask("URL (optional): ") : "";
1565
+ const description = await prompt.ask("Description (optional): ");
1566
+ const digits = type === "otp" ? await prompt.ask("Digits [6]: ") : "";
1567
+ const algorithm = type === "otp" ? await prompt.ask("Algorithm [SHA1]: ") : "";
1568
+ output(session.addSecret({
1569
+ metadata: Object.fromEntries(Object.entries({
1570
+ algorithm: algorithm || undefined,
1571
+ description: description || undefined,
1572
+ digits: digits ? Number(digits) : undefined,
1573
+ url: url || undefined
1574
+ }).filter(([, entry]) => entry !== undefined)),
1575
+ name,
1576
+ type,
1577
+ username: username || undefined,
1578
+ value
1579
+ }));
1580
+ return;
1581
+ }
1582
+ throw new Error(`Unknown prompt action: ${action}`);
1583
+ } finally {
1584
+ session.close();
1585
+ }
1586
+ } finally {
1587
+ prompt.close();
1588
+ }
1589
+ }
1590
+ async function runWebServer(vaultPath, args) {
1591
+ const commandArgs = [
1592
+ "run",
1593
+ absolutePath("./apps/web/src/index.ts"),
1594
+ "serve",
1595
+ "--vault",
1596
+ absolutePath(vaultPath)
1597
+ ];
1598
+ const host = getString(args, "host");
1599
+ const port = getString(args, "port");
1600
+ if (host) {
1601
+ commandArgs.push("--host", host);
1602
+ }
1603
+ if (port) {
1604
+ commandArgs.push("--port", port);
1605
+ }
1606
+ await new Promise((resolvePromise, rejectPromise) => {
1607
+ const child = spawn(process.execPath, commandArgs, {
1608
+ cwd: process.cwd(),
1609
+ stdio: "inherit"
1610
+ });
1611
+ child.on("exit", (code) => {
1612
+ if (code === 0) {
1613
+ resolvePromise();
1614
+ return;
1615
+ }
1616
+ rejectPromise(new Error(`Web server exited with code ${code ?? 1}`));
1617
+ });
1618
+ child.on("error", rejectPromise);
1619
+ });
1620
+ }
1621
+ function help() {
1622
+ return [
1623
+ "Autho Bun CLI",
1624
+ "",
1625
+ "Commands:",
1626
+ " prompt [--password <value>] [--vault <path>]",
1627
+ " init --password <value> [--vault <path>]",
1628
+ " status [--password <value>] [--vault <path>] [--project-file <path>] [--json]",
1629
+ " project init --map <ENV_NAME=secretRef> [--map <ENV_NAME=secretRef>] [--output <path>] [--force] [--json]",
1630
+ " web serve [--vault <path>] [--host <value>] [--port <value>]",
1631
+ " daemon serve [--vault <path>] [--state-file <path>] [--host <value>] [--port <value>]",
1632
+ " daemon status [--state-file <path>] [--json]",
1633
+ " daemon unlock --password <value> [--ttl <seconds>] [--state-file <path>] [--json]",
1634
+ " daemon lock --session <id> [--state-file <path>] [--json]",
1635
+ " daemon stop [--state-file <path>] [--json]",
1636
+ " daemon env render --session <id> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--state-file <path>] [--json]",
1637
+ " daemon exec --session <id> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--state-file <path>] -- <command>",
1638
+ " import legacy --password <value> --file <path> [--skip-existing] [--vault <path>] [--json]",
1639
+ " secrets add --password <value> --name <name> --type <password|note|otp> --value <value> [--username <value>] [--url <value>] [--description <value>] [--digits <value>] [--algorithm <value>] [--vault <path>]",
1640
+ " secrets list --password <value> [--vault <path>] [--json]",
1641
+ " secrets get --password <value> --ref <name-or-id> [--vault <path>] [--json]",
1642
+ " secrets rm --password <value> --ref <name-or-id> [--vault <path>] [--json]",
1643
+ " otp code --password <value> --ref <name-or-id> [--vault <path>] [--json]",
1644
+ " lease create --password <value> --secret <name-or-id> [--secret <name-or-id>] --ttl <seconds> [--name <value>] [--vault <path>] [--json]",
1645
+ " lease revoke --password <value> --lease <lease-id> [--vault <path>] [--json]",
1646
+ " env render --password <value> --map <ENV_NAME=secretRef> [--map <ENV_NAME=secretRef>] [--project-file <path>] [--lease <lease-id>] [--vault <path>] [--json]",
1647
+ " env sync --password <value> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--ttl <seconds>] [--output <path>] [--force] [--vault <path>] [--json]",
1648
+ " exec --password <value> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--vault <path>] -- <command>",
1649
+ " file encrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
1650
+ " file decrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
1651
+ " files encrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
1652
+ " files decrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
1653
+ " audit list --password <value> [--limit <number>] [--vault <path>] [--json]",
1654
+ "",
1655
+ "Notes:",
1656
+ " Running `autho` with no command enters interactive prompt mode.",
1657
+ " The default vault path is ~/.autho/vault.db (or AUTHO_HOME/vault.db).",
1658
+ " The default project file is ~/.autho/project.json when it exists (or AUTHO_HOME/project.json).",
1659
+ " The default daemon state file is ~/.autho/daemon.json (or AUTHO_HOME/daemon.json).",
1660
+ " AUTHO_MASTER_PASSWORD can be used instead of --password."
1661
+ ].join(`
1662
+ `);
1663
+ }
1664
+ async function main() {
1665
+ const args = parseArgs(process.argv.slice(2));
1666
+ const [scope, action, subaction] = args.positionals;
1667
+ const jsonMode = getBoolean(args, "json");
1668
+ const vaultPath = getString(args, "vault") ?? defaultVaultPath();
1669
+ const statePath = absolutePath(getString(args, "state-file") ?? defaultDaemonStatePath());
1670
+ const explicitProjectFile = getString(args, "project-file");
1671
+ const fallbackProjectFile = defaultProjectFilePath();
1672
+ const projectFile = explicitProjectFile ?? (existsSync4(fallbackProjectFile) ? fallbackProjectFile : undefined);
1673
+ const password = getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
1674
+ if (!scope) {
1675
+ await runPromptMode(vaultPath, password);
1676
+ return;
1677
+ }
1678
+ if (scope === "help" || scope === "--help") {
1679
+ console.log(help());
1680
+ return;
1681
+ }
1682
+ if (scope === "prompt") {
1683
+ await runPromptMode(vaultPath, password);
1684
+ return;
1685
+ }
1686
+ if (scope === "init") {
1687
+ output(VaultService.initialize(vaultPath, required(password, "--password")), jsonMode);
1688
+ return;
1689
+ }
1690
+ if (scope === "status") {
1691
+ output(VaultService.status(vaultPath, {
1692
+ password,
1693
+ projectFile
1694
+ }), jsonMode);
1695
+ return;
1696
+ }
1697
+ if (scope === "project" && action === "init") {
1698
+ output(writeProjectConfig({
1699
+ force: getBoolean(args, "force"),
1700
+ mappings: resolveMappings({ maps: getStrings(args, "map") }),
1701
+ outputPath: absolutePath(getString(args, "output") ?? projectFile ?? defaultProjectFilePath())
1702
+ }), jsonMode);
1703
+ return;
1704
+ }
1705
+ if (scope === "web" && action === "serve") {
1706
+ await runWebServer(vaultPath, args);
1707
+ return;
1708
+ }
1709
+ if (scope === "daemon" && action === "serve") {
1710
+ await startDaemonServer({
1711
+ host: getString(args, "host") ?? "127.0.0.1",
1712
+ port: Number(getString(args, "port") ?? "0"),
1713
+ statePath,
1714
+ vaultPath: absolutePath(vaultPath)
1715
+ });
1716
+ return;
1717
+ }
1718
+ if (scope === "daemon" && action === "status") {
1719
+ output(await daemonStatus({ statePath }), jsonMode);
1720
+ return;
1721
+ }
1722
+ if (scope === "daemon" && action === "unlock") {
1723
+ output(await daemonUnlock({
1724
+ password: required(password, "--password"),
1725
+ statePath,
1726
+ ttlSeconds: getString(args, "ttl") ? Number(getString(args, "ttl")) : undefined
1727
+ }), jsonMode);
1728
+ return;
1729
+ }
1730
+ if (scope === "daemon" && action === "lock") {
1731
+ output(await daemonLock({
1732
+ sessionId: required(getString(args, "session"), "--session"),
1733
+ statePath
1734
+ }), jsonMode);
1735
+ return;
1736
+ }
1737
+ if (scope === "daemon" && action === "stop") {
1738
+ output(await daemonStop({ statePath }), jsonMode);
1739
+ return;
1740
+ }
1741
+ if (scope === "daemon" && action === "env" && subaction === "render") {
1742
+ output(await daemonRenderEnv({
1743
+ leaseId: getString(args, "lease"),
1744
+ mappings: resolveMappings({ maps: getStrings(args, "map"), projectFile }),
1745
+ sessionId: required(getString(args, "session"), "--session"),
1746
+ statePath
1747
+ }), jsonMode);
1748
+ return;
1749
+ }
1750
+ if (scope === "daemon" && action === "exec") {
1751
+ const result = await daemonExec({
1752
+ cmd: args.passthrough,
1753
+ leaseId: getString(args, "lease"),
1754
+ mappings: resolveMappings({ maps: getStrings(args, "map"), projectFile }),
1755
+ sessionId: required(getString(args, "session"), "--session"),
1756
+ statePath
1757
+ });
1758
+ process.stdout.write(result.stdout);
1759
+ process.stderr.write(result.stderr);
1760
+ process.exit(result.exitCode);
1761
+ }
1762
+ const session = VaultService.unlock(vaultPath, required(password, "--password"));
1763
+ try {
1764
+ if (scope === "import" && action === "legacy") {
1765
+ output(session.importLegacyFile(absolutePath(required(getString(args, "file"), "--file")), {
1766
+ skipExisting: !getBoolean(args, "no-skip-existing")
1767
+ }), jsonMode);
1768
+ return;
1769
+ }
1770
+ if (scope === "secrets" && action === "add") {
1771
+ output(session.addSecret({
1772
+ metadata: buildSecretMetadata(args),
1773
+ name: required(getString(args, "name"), "--name"),
1774
+ type: required(getString(args, "type"), "--type"),
1775
+ username: getString(args, "username"),
1776
+ value: required(getString(args, "value"), "--value")
1777
+ }), jsonMode);
1778
+ return;
1779
+ }
1780
+ if (scope === "secrets" && action === "list") {
1781
+ output(session.listSecrets(), jsonMode);
1782
+ return;
1783
+ }
1784
+ if (scope === "secrets" && action === "get") {
1785
+ const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
1786
+ output(session.getSecret(required(ref, "--ref")), jsonMode);
1787
+ return;
1788
+ }
1789
+ if (scope === "secrets" && action === "rm") {
1790
+ const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
1791
+ output(session.removeSecret(required(ref, "--ref")), jsonMode);
1792
+ return;
1793
+ }
1794
+ if (scope === "otp" && action === "code") {
1795
+ const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
1796
+ output(session.generateOtp(required(ref, "--ref")), jsonMode);
1797
+ return;
1798
+ }
1799
+ if (scope === "lease" && action === "create") {
1800
+ output(session.createLease({
1801
+ name: getString(args, "name") ?? "session",
1802
+ secretRefs: getStrings(args, "secret"),
1803
+ ttlSeconds: requirePositiveInt(required(getString(args, "ttl"), "--ttl"), "--ttl")
1804
+ }), jsonMode);
1805
+ return;
1806
+ }
1807
+ if (scope === "lease" && action === "revoke") {
1808
+ output(session.revokeLease(required(getString(args, "lease"), "--lease")), jsonMode);
1809
+ return;
1810
+ }
1811
+ if (scope === "env" && action === "render") {
1812
+ output(session.renderEnv(resolveMappings({
1813
+ maps: getStrings(args, "map"),
1814
+ projectFile
1815
+ }), getString(args, "lease")), jsonMode);
1816
+ return;
1817
+ }
1818
+ if (scope === "env" && action === "sync") {
1819
+ output(session.syncEnvFile({
1820
+ force: getBoolean(args, "force"),
1821
+ leaseId: getString(args, "lease"),
1822
+ mappings: resolveMappings({
1823
+ maps: getStrings(args, "map"),
1824
+ projectFile
1825
+ }),
1826
+ outputPath: absolutePath(getString(args, "output") ?? ".env.autho"),
1827
+ ttlSeconds: getString(args, "ttl") ? requirePositiveInt(getString(args, "ttl"), "--ttl") : undefined
1828
+ }), jsonMode);
1829
+ return;
1830
+ }
1831
+ if (scope === "exec") {
1832
+ const result = session.runExec({
1833
+ cmd: args.passthrough,
1834
+ leaseId: getString(args, "lease"),
1835
+ mappings: resolveMappings({
1836
+ maps: getStrings(args, "map"),
1837
+ projectFile
1838
+ })
1839
+ });
1840
+ process.stdout.write(result.stdout);
1841
+ process.stderr.write(result.stderr);
1842
+ process.exit(result.exitCode);
1843
+ }
1844
+ if (scope === "file" && action === "encrypt") {
1845
+ output(session.encryptFile(absolutePath(required(getString(args, "input"), "--input")), getString(args, "output") ? absolutePath(getString(args, "output")) : undefined, { force: getBoolean(args, "force") }), jsonMode);
1846
+ return;
1847
+ }
1848
+ if (scope === "file" && action === "decrypt") {
1849
+ output(session.decryptFile(absolutePath(required(getString(args, "input"), "--input")), getString(args, "output") ? absolutePath(getString(args, "output")) : undefined, { force: getBoolean(args, "force") }), jsonMode);
1850
+ return;
1851
+ }
1852
+ if (scope === "files" && action === "encrypt") {
1853
+ output(session.encryptFolder(absolutePath(required(getString(args, "input"), "--input")), getString(args, "output") ? absolutePath(getString(args, "output")) : undefined, { force: getBoolean(args, "force") }), jsonMode);
1854
+ return;
1855
+ }
1856
+ if (scope === "files" && action === "decrypt") {
1857
+ output(session.decryptFolder(absolutePath(required(getString(args, "input"), "--input")), getString(args, "output") ? absolutePath(getString(args, "output")) : undefined, { force: getBoolean(args, "force") }), jsonMode);
1858
+ return;
1859
+ }
1860
+ if (scope === "audit" && action === "list") {
1861
+ output(session.listAudit(Number(getString(args, "limit") ?? "50")), jsonMode);
1862
+ return;
1863
+ }
1864
+ throw new Error(`Unknown command: ${[scope, action, subaction].filter(Boolean).join(" ")}`);
1865
+ } finally {
1866
+ session.close();
1867
+ }
1868
+ }
1869
+ main().catch((error) => {
1870
+ const message = error instanceof Error ? error.message : String(error);
1871
+ console.error(message);
1872
+ process.exit(1);
1873
+ });