autho 3.0.0 → 3.0.2

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/README.md +3 -1
  2. package/dist/autho.js +1153 -90
  3. package/package.json +1 -1
package/dist/autho.js CHANGED
@@ -15,6 +15,7 @@ var __export = (target, all) => {
15
15
  });
16
16
  };
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = import.meta.require;
18
19
 
19
20
  // packages/storage/src/index.ts
20
21
  import { Database } from "bun:sqlite";
@@ -105,6 +106,13 @@ class AuthoDatabase {
105
106
  setVaultConfig(config) {
106
107
  this.db.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)").run("vault.config", JSON.stringify(config));
107
108
  }
109
+ getVaultAuth() {
110
+ const row = this.db.query("SELECT value FROM meta WHERE key = ?1").get("vault.auth");
111
+ return row ? parseJson(row.value) : null;
112
+ }
113
+ setVaultAuth(auth) {
114
+ this.db.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)").run("vault.auth", JSON.stringify(auth));
115
+ }
108
116
  countSecrets() {
109
117
  const row = this.db.query("SELECT COUNT(*) AS count FROM secrets").get();
110
118
  return row.count;
@@ -221,6 +229,7 @@ var init_src = () => {};
221
229
  import {
222
230
  createCipheriv,
223
231
  createDecipheriv,
232
+ createHmac,
224
233
  randomBytes,
225
234
  scryptSync
226
235
  } from "crypto";
@@ -280,6 +289,78 @@ function unlockRootKey(password, config) {
280
289
  const key = deriveKeyFromPassword(password, config.kdf);
281
290
  return decryptWithKey(config.wrappedRootKey, key, "autho:vault-root");
282
291
  }
292
+ function decodeBase32(input) {
293
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
294
+ const normalized = input.toUpperCase().replace(/=+$/g, "").replace(/\s+/g, "");
295
+ let bits = 0;
296
+ let value = 0;
297
+ const output = [];
298
+ for (const char of normalized) {
299
+ const index = alphabet.indexOf(char);
300
+ if (index === -1) {
301
+ throw new Error("OTP secret must be valid base32");
302
+ }
303
+ value = value << 5 | index;
304
+ bits += 5;
305
+ if (bits >= 8) {
306
+ output.push(value >>> bits - 8 & 255);
307
+ bits -= 8;
308
+ }
309
+ }
310
+ return Uint8Array.from(output);
311
+ }
312
+ function generateTotpCode(secret, options, now) {
313
+ const algorithm = (options?.algorithm ?? "sha1").toLowerCase();
314
+ const digits = options?.digits ?? 6;
315
+ const key = decodeBase32(secret);
316
+ const counter = Math.floor(now / 30000);
317
+ const message = Buffer.alloc(8);
318
+ let cursor = counter;
319
+ for (let index = 7;index >= 0; index -= 1) {
320
+ message[index] = cursor & 255;
321
+ cursor >>= 8;
322
+ }
323
+ const hash = createHmac(algorithm, Buffer.from(key)).update(message).digest();
324
+ const offset = hash[hash.length - 1] & 15;
325
+ const binary = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
326
+ const mod = 10 ** digits;
327
+ return String(binary % mod).padStart(digits, "0");
328
+ }
329
+ function generateTotpSecret() {
330
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
331
+ const bytes = randomBytes(20);
332
+ let bits = 0;
333
+ let value = 0;
334
+ let result = "";
335
+ for (const byte of bytes) {
336
+ value = value << 8 | byte;
337
+ bits += 8;
338
+ while (bits >= 5) {
339
+ result += alphabet[value >>> bits - 5 & 31];
340
+ bits -= 5;
341
+ }
342
+ }
343
+ if (bits > 0) {
344
+ result += alphabet[value << 5 - bits & 31];
345
+ }
346
+ return result;
347
+ }
348
+ function totpUri(secret, issuer, account) {
349
+ return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(account)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`;
350
+ }
351
+ function verifyTotpCode(secret, code, opts) {
352
+ const digits = opts?.digits ?? 6;
353
+ if (code.length !== digits) {
354
+ return false;
355
+ }
356
+ const now = Date.now();
357
+ for (const offset of [-30000, 0, 30000]) {
358
+ if (generateTotpCode(secret, opts, now + offset) === code) {
359
+ return true;
360
+ }
361
+ }
362
+ return false;
363
+ }
283
364
  var DEFAULT_KDF;
284
365
  var init_src2 = __esm(() => {
285
366
  DEFAULT_KDF = {
@@ -293,7 +374,7 @@ var init_src2 = __esm(() => {
293
374
  });
294
375
 
295
376
  // packages/core/src/paths.ts
296
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, writeFileSync } from "fs";
377
+ import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
297
378
  import { homedir } from "os";
298
379
  import { dirname as dirname2, join, resolve } from "path";
299
380
  function normalizePath(path) {
@@ -307,9 +388,51 @@ function tryChmod2(path, mode) {
307
388
  chmodSync2(path, mode);
308
389
  } catch {}
309
390
  }
310
- function authoHomeDir() {
391
+ function authoConfigDir() {
311
392
  return normalizePath(process.env.AUTHO_HOME ?? join(homedir(), ".autho"));
312
393
  }
394
+ function configFilePath() {
395
+ return normalizePath(join(authoConfigDir(), "config.json"));
396
+ }
397
+ function loadConfig() {
398
+ if (configCache.loaded)
399
+ return configCache.value ?? {};
400
+ const path = configFilePath();
401
+ if (!existsSync2(path)) {
402
+ configCache.loaded = true;
403
+ configCache.value = {};
404
+ return {};
405
+ }
406
+ try {
407
+ const raw = JSON.parse(readFileSync(path, "utf8"));
408
+ configCache.loaded = true;
409
+ configCache.value = raw;
410
+ return raw;
411
+ } catch {
412
+ configCache.loaded = true;
413
+ configCache.value = {};
414
+ return {};
415
+ }
416
+ }
417
+ function saveConfig(config) {
418
+ const path = configFilePath();
419
+ ensurePrivateParent(path);
420
+ writeFileSync(path, JSON.stringify(config, null, 2) + `
421
+ `, { encoding: "utf8", mode: 384 });
422
+ hardenFilePermissions(path);
423
+ configCache.value = config;
424
+ configCache.loaded = true;
425
+ }
426
+ function authoHomeDir() {
427
+ if (process.env.AUTHO_HOME) {
428
+ return normalizePath(process.env.AUTHO_HOME);
429
+ }
430
+ const config = loadConfig();
431
+ if (config.vaultDir) {
432
+ return normalizePath(config.vaultDir);
433
+ }
434
+ return normalizePath(join(homedir(), ".autho"));
435
+ }
313
436
  function defaultVaultPath() {
314
437
  return normalizePath(join(authoHomeDir(), "vault.db"));
315
438
  }
@@ -339,13 +462,16 @@ function writeBinaryFileSecure(path, content) {
339
462
  writeFileSync(path, content, { mode: 384 });
340
463
  hardenFilePermissions(path);
341
464
  }
342
- var init_paths = () => {};
465
+ var configCache;
466
+ var init_paths = __esm(() => {
467
+ configCache = { value: null, loaded: false };
468
+ });
343
469
 
344
470
  // packages/core/src/artifacts.ts
345
471
  import { randomBytes as randomBytes2 } from "crypto";
346
472
  import {
347
473
  readdirSync,
348
- readFileSync,
474
+ readFileSync as readFileSync2,
349
475
  statSync
350
476
  } from "fs";
351
477
  import { basename, join as join2, relative, resolve as resolve2, sep } from "path";
@@ -381,7 +507,7 @@ function defaultDecryptedFolderPath(inputPath) {
381
507
  }
382
508
  function encryptFileArtifact(inputPath, outputPath, rootKey) {
383
509
  const fileKey = randomBytes2(32);
384
- const payload = encryptWithKey(readFileSync(inputPath), fileKey, `autho:file:${basename(inputPath)}`);
510
+ const payload = encryptWithKey(readFileSync2(inputPath), fileKey, `autho:file:${basename(inputPath)}`);
385
511
  const envelope = {
386
512
  kind: "file",
387
513
  originalName: basename(inputPath),
@@ -393,7 +519,7 @@ function encryptFileArtifact(inputPath, outputPath, rootKey) {
393
519
  return { outputPath };
394
520
  }
395
521
  function decryptFileArtifact(inputPath, outputPath, rootKey) {
396
- const envelope = JSON.parse(readFileSync(inputPath, "utf8"));
522
+ const envelope = JSON.parse(readFileSync2(inputPath, "utf8"));
397
523
  if (envelope.kind !== "file" || envelope.version !== 1) {
398
524
  throw new Error(`Unsupported file artifact: ${inputPath}`);
399
525
  }
@@ -411,7 +537,7 @@ function encryptFolderArtifact(inputPath, outputPath, rootKey) {
411
537
  const relativePath = normalizeRelativePath(relative(inputPath, filePath));
412
538
  return {
413
539
  path: relativePath,
414
- payload: encryptWithKey(readFileSync(filePath), folderKey, `autho:folder:${relativePath}`)
540
+ payload: encryptWithKey(readFileSync2(filePath), folderKey, `autho:folder:${relativePath}`)
415
541
  };
416
542
  }),
417
543
  kind: "folder",
@@ -426,7 +552,7 @@ function encryptFolderArtifact(inputPath, outputPath, rootKey) {
426
552
  };
427
553
  }
428
554
  function decryptFolderArtifact(inputPath, outputPath, rootKey) {
429
- const envelope = JSON.parse(readFileSync(inputPath, "utf8"));
555
+ const envelope = JSON.parse(readFileSync2(inputPath, "utf8"));
430
556
  if (envelope.kind !== "folder" || envelope.version !== 1) {
431
557
  throw new Error(`Unsupported folder artifact: ${inputPath}`);
432
558
  }
@@ -465,10 +591,10 @@ var init_artifacts = __esm(() => {
465
591
 
466
592
  // packages/core/src/index.ts
467
593
  import { spawnSync } from "child_process";
468
- import { createHmac, randomBytes as randomBytes3 } from "crypto";
594
+ import { createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
469
595
  import {
470
- existsSync as existsSync2,
471
- readFileSync as readFileSync2
596
+ existsSync as existsSync3,
597
+ readFileSync as readFileSync3
472
598
  } from "fs";
473
599
  import { basename as basename2 } from "path";
474
600
  function requireValue(value, label) {
@@ -477,7 +603,7 @@ function requireValue(value, label) {
477
603
  }
478
604
  return value;
479
605
  }
480
- function decodeBase32(input) {
606
+ function decodeBase322(input) {
481
607
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
482
608
  const normalized = input.toUpperCase().replace(/=+$/g, "").replace(/\s+/g, "");
483
609
  let bits = 0;
@@ -500,7 +626,7 @@ function decodeBase32(input) {
500
626
  function generateTotp(secret, options, now = Date.now()) {
501
627
  const algorithm = (options?.algorithm ?? "sha1").toLowerCase();
502
628
  const digits = options?.digits ?? 6;
503
- const key = decodeBase32(secret);
629
+ const key = decodeBase322(secret);
504
630
  const counter = Math.floor(now / 30000);
505
631
  const message = Buffer.alloc(8);
506
632
  let cursor = counter;
@@ -508,7 +634,7 @@ function generateTotp(secret, options, now = Date.now()) {
508
634
  message[index] = cursor & 255;
509
635
  cursor >>= 8;
510
636
  }
511
- const hash = createHmac(algorithm, Buffer.from(key)).update(message).digest();
637
+ const hash = createHmac2(algorithm, Buffer.from(key)).update(message).digest();
512
638
  const offset = hash[hash.length - 1] & 15;
513
639
  const binary = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
514
640
  const mod = 10 ** digits;
@@ -523,7 +649,7 @@ function normalizeSecretType(type) {
523
649
  throw new Error(`Unsupported secret type: ${type}`);
524
650
  }
525
651
  function parseProjectMappings(projectFile) {
526
- const raw = JSON.parse(readFileSync2(projectFile, "utf8"));
652
+ const raw = JSON.parse(readFileSync3(projectFile, "utf8"));
527
653
  return Object.entries(raw.env ?? {}).map(([envName, secretRef]) => ({
528
654
  envName,
529
655
  secretRef
@@ -601,7 +727,7 @@ function summarizeCommand(cmd) {
601
727
  };
602
728
  }
603
729
  function projectMappingsForStatus(projectFile) {
604
- if (!projectFile || !existsSync2(projectFile)) {
730
+ if (!projectFile || !existsSync3(projectFile)) {
605
731
  return {
606
732
  mappings: [],
607
733
  path: projectFile ?? null
@@ -624,7 +750,7 @@ function resolveMappings(options) {
624
750
  };
625
751
  });
626
752
  if (options.projectFile) {
627
- if (!existsSync2(options.projectFile)) {
753
+ if (!existsSync3(options.projectFile)) {
628
754
  throw new Error(`Project mapping file not found: ${options.projectFile}`);
629
755
  }
630
756
  return [...parseProjectMappings(options.projectFile), ...fromMaps];
@@ -635,7 +761,7 @@ function writeProjectConfig(input) {
635
761
  if (input.mappings.length === 0) {
636
762
  throw new Error("Provide at least one env mapping");
637
763
  }
638
- if (!input.force && existsSync2(input.outputPath)) {
764
+ if (!input.force && existsSync3(input.outputPath)) {
639
765
  throw new Error(`Project config already exists: ${input.outputPath}`);
640
766
  }
641
767
  const env = Object.fromEntries(input.mappings.map((mapping) => [mapping.envName, mapping.secretRef]));
@@ -710,21 +836,212 @@ class VaultService {
710
836
  db.close();
711
837
  }
712
838
  }
713
- static unlock(vaultPath, password) {
839
+ static unlock(vaultPath, credentials) {
840
+ const creds = typeof credentials === "string" ? { password: credentials } : credentials;
714
841
  const db = new AuthoDatabase(vaultPath);
715
842
  const config = db.getVaultConfig();
716
843
  if (!config) {
717
844
  db.close();
718
845
  throw new Error(`Vault is not initialized at ${vaultPath}`);
719
846
  }
847
+ const vaultAuth = db.getVaultAuth();
848
+ if (creds.recovery) {
849
+ if (!vaultAuth?.recovery) {
850
+ db.close();
851
+ throw new Error("No recovery token configured for this vault");
852
+ }
853
+ try {
854
+ const recoveryKEK = deriveKeyFromPassword(creds.recovery, vaultAuth.recovery.kdf);
855
+ const rootKey = decryptWithKey(vaultAuth.recovery.wrappedRootKey, recoveryKEK, "autho:vault-recovery");
856
+ db.insertAudit({
857
+ createdAt: new Date().toISOString(),
858
+ eventType: "auth.unlock.recovery",
859
+ id: randomId(),
860
+ message: "Vault unlocked via recovery file",
861
+ metadata: JSON.stringify({}),
862
+ subjectRef: null,
863
+ subjectType: "vault"
864
+ });
865
+ return new VaultSession(db, rootKey);
866
+ } catch (error) {
867
+ db.close();
868
+ throw new Error("Invalid recovery token", { cause: error });
869
+ }
870
+ }
720
871
  try {
721
- const rootKey = unlockRootKey(password, config);
872
+ const passwordKEK = deriveKeyFromPassword(creds.password, config.kdf);
873
+ if (vaultAuth?.totp) {
874
+ const totpSecret = decryptWithKey(vaultAuth.totp.encryptedSecret, passwordKEK, "autho:vault-totp").toString("utf8");
875
+ if (!creds.totp || !verifyTotpCode(totpSecret, creds.totp)) {
876
+ throw new Error("Invalid or missing TOTP code");
877
+ }
878
+ }
879
+ const rootKey = decryptWithKey(config.wrappedRootKey, passwordKEK, "autho:vault-root");
722
880
  return new VaultSession(db, rootKey);
723
881
  } catch (error) {
724
882
  db.close();
883
+ if (error instanceof Error && error.message === "Invalid or missing TOTP code") {
884
+ throw error;
885
+ }
725
886
  throw new Error("Invalid vault password", { cause: error });
726
887
  }
727
888
  }
889
+ static getAuthConfig(vaultPath) {
890
+ const db = new AuthoDatabase(vaultPath);
891
+ try {
892
+ return db.getVaultAuth();
893
+ } finally {
894
+ db.close();
895
+ }
896
+ }
897
+ static setupTotp(vaultPath) {
898
+ const secret = generateTotpSecret();
899
+ const uri = totpUri(secret, "autho", vaultPath);
900
+ return { secret, uri };
901
+ }
902
+ static enableTotp(vaultPath, credentials, secret, code) {
903
+ if (!verifyTotpCode(secret, code)) {
904
+ throw new Error("Invalid TOTP code \u2014 make sure your authenticator app is showing the right code");
905
+ }
906
+ const session = VaultService.unlock(vaultPath, credentials);
907
+ session.close();
908
+ const db = new AuthoDatabase(vaultPath);
909
+ try {
910
+ const config = db.getVaultConfig();
911
+ const passwordKEK = deriveKeyFromPassword(credentials.password, config.kdf);
912
+ const encryptedSecret = encryptWithKey(Buffer.from(secret, "utf8"), passwordKEK, "autho:vault-totp");
913
+ const existing = db.getVaultAuth();
914
+ db.setVaultAuth({
915
+ version: 1,
916
+ ...existing,
917
+ totp: {
918
+ algorithm: "SHA1",
919
+ digits: 6,
920
+ encryptedSecret,
921
+ period: 30
922
+ }
923
+ });
924
+ db.insertAudit({
925
+ createdAt: new Date().toISOString(),
926
+ eventType: "auth.totp.enabled",
927
+ id: randomId(),
928
+ message: "TOTP vault unlock enabled",
929
+ metadata: JSON.stringify({}),
930
+ subjectRef: null,
931
+ subjectType: "vault"
932
+ });
933
+ } finally {
934
+ db.close();
935
+ }
936
+ }
937
+ static removeTotp(vaultPath, credentials) {
938
+ const session = VaultService.unlock(vaultPath, credentials);
939
+ session.close();
940
+ const db = new AuthoDatabase(vaultPath);
941
+ try {
942
+ const existing = db.getVaultAuth();
943
+ if (existing) {
944
+ const { totp: _removed, ...rest } = existing;
945
+ db.setVaultAuth({ ...rest, version: 1 });
946
+ }
947
+ db.insertAudit({
948
+ createdAt: new Date().toISOString(),
949
+ eventType: "auth.totp.removed",
950
+ id: randomId(),
951
+ message: "TOTP vault unlock removed",
952
+ metadata: JSON.stringify({}),
953
+ subjectRef: null,
954
+ subjectType: "vault"
955
+ });
956
+ } finally {
957
+ db.close();
958
+ }
959
+ }
960
+ static generateRecovery(vaultPath, credentials) {
961
+ const session = VaultService.unlock(vaultPath, credentials);
962
+ const rootKey = session.getRootKey();
963
+ session.close();
964
+ const db = new AuthoDatabase(vaultPath);
965
+ try {
966
+ const tokenBytes = randomBytes3(32);
967
+ const token = tokenBytes.toString("hex");
968
+ const recoverySalt = randomBytes3(16).toString("base64");
969
+ const recoveryKdf = {
970
+ keyLength: 32,
971
+ name: "scrypt",
972
+ salt: recoverySalt,
973
+ N: 1 << 17,
974
+ p: 1,
975
+ r: 8
976
+ };
977
+ const recoveryKEK = deriveKeyFromPassword(token, recoveryKdf);
978
+ const wrappedRootKey = encryptWithKey(rootKey, recoveryKEK, "autho:vault-recovery");
979
+ const existing = db.getVaultAuth();
980
+ db.setVaultAuth({
981
+ version: 1,
982
+ ...existing,
983
+ recovery: {
984
+ createdAt: new Date().toISOString(),
985
+ kdf: recoveryKdf,
986
+ wrappedRootKey
987
+ }
988
+ });
989
+ const formattedToken = token.toUpperCase().match(/.{1,8}/g).join("-");
990
+ const fileContent = [
991
+ "================================================================================",
992
+ "AUTHO VAULT RECOVERY FILE",
993
+ "================================================================================",
994
+ `Generated : ${new Date().toISOString()}`,
995
+ `Vault : ${vaultPath}`,
996
+ "",
997
+ "WARNING: Anyone with this file can open your vault regardless of password,",
998
+ "PIN, or authenticator app. Store it offline (printed paper, encrypted USB,",
999
+ "safety deposit box). Revoke with: autho recovery revoke",
1000
+ "",
1001
+ "RECOVERY TOKEN:",
1002
+ formattedToken,
1003
+ "",
1004
+ "To use: autho unlock --recovery-file <path-to-this-file>",
1005
+ "================================================================================"
1006
+ ].join(`
1007
+ `);
1008
+ db.insertAudit({
1009
+ createdAt: new Date().toISOString(),
1010
+ eventType: "auth.recovery.generated",
1011
+ id: randomId(),
1012
+ message: "Recovery file generated",
1013
+ metadata: JSON.stringify({}),
1014
+ subjectRef: null,
1015
+ subjectType: "vault"
1016
+ });
1017
+ return { fileContent };
1018
+ } finally {
1019
+ db.close();
1020
+ }
1021
+ }
1022
+ static revokeRecovery(vaultPath, credentials) {
1023
+ const session = VaultService.unlock(vaultPath, credentials);
1024
+ session.close();
1025
+ const db = new AuthoDatabase(vaultPath);
1026
+ try {
1027
+ const existing = db.getVaultAuth();
1028
+ if (existing) {
1029
+ const { recovery: _removed, ...rest } = existing;
1030
+ db.setVaultAuth({ ...rest, version: 1 });
1031
+ }
1032
+ db.insertAudit({
1033
+ createdAt: new Date().toISOString(),
1034
+ eventType: "auth.recovery.revoked",
1035
+ id: randomId(),
1036
+ message: "Recovery file revoked",
1037
+ metadata: JSON.stringify({}),
1038
+ subjectRef: null,
1039
+ subjectType: "vault"
1040
+ });
1041
+ } finally {
1042
+ db.close();
1043
+ }
1044
+ }
728
1045
  }
729
1046
 
730
1047
  class VaultSession {
@@ -734,6 +1051,9 @@ class VaultSession {
734
1051
  this.db = db;
735
1052
  this.rootKey = rootKey;
736
1053
  }
1054
+ getRootKey() {
1055
+ return Buffer.from(this.rootKey);
1056
+ }
737
1057
  close() {
738
1058
  this.db.close();
739
1059
  }
@@ -882,7 +1202,7 @@ class VaultSession {
882
1202
  };
883
1203
  }
884
1204
  importLegacyFile(filePath, options) {
885
- const raw = JSON.parse(readFileSync2(filePath, "utf8"));
1205
+ const raw = JSON.parse(readFileSync3(filePath, "utf8"));
886
1206
  let imported = 0;
887
1207
  let skipped = 0;
888
1208
  for (const entry of raw) {
@@ -1015,7 +1335,7 @@ class VaultSession {
1015
1335
  }
1016
1336
  syncEnvFile(input) {
1017
1337
  const env = this.buildEnv(input.mappings, input.leaseId);
1018
- if (!input.force && existsSync2(input.outputPath)) {
1338
+ if (!input.force && existsSync3(input.outputPath)) {
1019
1339
  throw new Error(`Env file already exists: ${input.outputPath}`);
1020
1340
  }
1021
1341
  const createdAt = new Date().toISOString();
@@ -1067,7 +1387,7 @@ class VaultSession {
1067
1387
  encryptFile(inputPath, outputPath, options) {
1068
1388
  assertPathIsFile(inputPath);
1069
1389
  const resolvedOutput = outputPath ?? defaultEncryptedFilePath(inputPath);
1070
- if (!options?.force && existsSync2(resolvedOutput)) {
1390
+ if (!options?.force && existsSync3(resolvedOutput)) {
1071
1391
  throw new Error(`Output file already exists: ${resolvedOutput}`);
1072
1392
  }
1073
1393
  const result = encryptFileArtifact(inputPath, resolvedOutput, this.rootKey);
@@ -1079,7 +1399,7 @@ class VaultSession {
1079
1399
  decryptFile(inputPath, outputPath, options) {
1080
1400
  assertPathIsFile(inputPath);
1081
1401
  const resolvedOutput = outputPath ?? defaultDecryptedFilePath(inputPath);
1082
- if (!options?.force && existsSync2(resolvedOutput)) {
1402
+ if (!options?.force && existsSync3(resolvedOutput)) {
1083
1403
  throw new Error(`Output file already exists: ${resolvedOutput}`);
1084
1404
  }
1085
1405
  const result = decryptFileArtifact(inputPath, resolvedOutput, this.rootKey);
@@ -1091,7 +1411,7 @@ class VaultSession {
1091
1411
  encryptFolder(inputPath, outputPath, options) {
1092
1412
  assertPathIsDirectory(inputPath);
1093
1413
  const resolvedOutput = outputPath ?? defaultEncryptedFolderPath(inputPath);
1094
- if (!options?.force && existsSync2(resolvedOutput)) {
1414
+ if (!options?.force && existsSync3(resolvedOutput)) {
1095
1415
  throw new Error(`Output file already exists: ${resolvedOutput}`);
1096
1416
  }
1097
1417
  const result = encryptFolderArtifact(inputPath, resolvedOutput, this.rootKey);
@@ -1104,7 +1424,7 @@ class VaultSession {
1104
1424
  decryptFolder(inputPath, outputPath, options) {
1105
1425
  assertPathIsFile(inputPath);
1106
1426
  const resolvedOutput = outputPath ?? defaultDecryptedFolderPath(inputPath);
1107
- if (!options?.force && existsSync2(resolvedOutput)) {
1427
+ if (!options?.force && existsSync3(resolvedOutput)) {
1108
1428
  throw new Error(`Output path already exists: ${resolvedOutput}`);
1109
1429
  }
1110
1430
  const result = decryptFolderArtifact(inputPath, resolvedOutput, this.rootKey);
@@ -1126,6 +1446,146 @@ var init_src3 = __esm(() => {
1126
1446
  init_paths();
1127
1447
  });
1128
1448
 
1449
+ // packages/core/src/os-secrets.ts
1450
+ import { createHash as createHash2, randomBytes as randomBytes4, scryptSync as scryptSync2, timingSafeEqual as timingSafeEqual2 } from "crypto";
1451
+ import { resolve as resolve3 } from "path";
1452
+ function vaultPasswordName(vaultPath) {
1453
+ return createHash2("sha256").update(resolve3(vaultPath)).digest("hex");
1454
+ }
1455
+ function osSecretsDisabled() {
1456
+ return process.env.AUTHO_DISABLE_OS_SECRETS === "1";
1457
+ }
1458
+ async function storeVaultPassword(vaultPath, password) {
1459
+ if (osSecretsDisabled()) {
1460
+ return false;
1461
+ }
1462
+ try {
1463
+ await Bun.secrets.set({
1464
+ name: vaultPasswordName(vaultPath),
1465
+ service: VAULT_PASSWORD_SERVICE,
1466
+ value: password
1467
+ });
1468
+ return true;
1469
+ } catch {
1470
+ return false;
1471
+ }
1472
+ }
1473
+ async function loadVaultPassword(vaultPath) {
1474
+ if (osSecretsDisabled()) {
1475
+ return null;
1476
+ }
1477
+ try {
1478
+ const password = await Bun.secrets.get({
1479
+ name: vaultPasswordName(vaultPath),
1480
+ service: VAULT_PASSWORD_SERVICE
1481
+ });
1482
+ return password ?? null;
1483
+ } catch {
1484
+ return null;
1485
+ }
1486
+ }
1487
+ async function deleteVaultPassword(vaultPath) {
1488
+ if (osSecretsDisabled()) {
1489
+ return false;
1490
+ }
1491
+ try {
1492
+ return await Bun.secrets.delete({
1493
+ name: vaultPasswordName(vaultPath),
1494
+ service: VAULT_PASSWORD_SERVICE
1495
+ });
1496
+ } catch {
1497
+ return false;
1498
+ }
1499
+ }
1500
+ async function setOsSecret(name, value) {
1501
+ if (osSecretsDisabled()) {
1502
+ return false;
1503
+ }
1504
+ try {
1505
+ await Bun.secrets.set({ name, service: USER_SECRETS_SERVICE, value });
1506
+ return true;
1507
+ } catch {
1508
+ return false;
1509
+ }
1510
+ }
1511
+ async function getOsSecret(name) {
1512
+ if (osSecretsDisabled()) {
1513
+ return null;
1514
+ }
1515
+ try {
1516
+ const value = await Bun.secrets.get({ name, service: USER_SECRETS_SERVICE });
1517
+ return value ?? null;
1518
+ } catch {
1519
+ return null;
1520
+ }
1521
+ }
1522
+ async function deleteOsSecret(name) {
1523
+ if (osSecretsDisabled()) {
1524
+ return false;
1525
+ }
1526
+ try {
1527
+ return await Bun.secrets.delete({ name, service: USER_SECRETS_SERVICE });
1528
+ } catch {
1529
+ return false;
1530
+ }
1531
+ }
1532
+ async function storePinHash(vaultPath, pin) {
1533
+ if (osSecretsDisabled()) {
1534
+ return false;
1535
+ }
1536
+ try {
1537
+ const salt = randomBytes4(16);
1538
+ const hash = scryptSync2(pin, salt, 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
1539
+ const value = `${salt.toString("base64")}:${hash.toString("base64")}`;
1540
+ await Bun.secrets.set({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE, value });
1541
+ return true;
1542
+ } catch {
1543
+ return false;
1544
+ }
1545
+ }
1546
+ async function verifyPin(vaultPath, pin) {
1547
+ if (osSecretsDisabled()) {
1548
+ return false;
1549
+ }
1550
+ try {
1551
+ const stored = await Bun.secrets.get({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE });
1552
+ if (!stored)
1553
+ return false;
1554
+ const colonIdx = stored.indexOf(":");
1555
+ if (colonIdx === -1)
1556
+ return false;
1557
+ const salt = Buffer.from(stored.slice(0, colonIdx), "base64");
1558
+ const expectedHash = Buffer.from(stored.slice(colonIdx + 1), "base64");
1559
+ const actualHash = scryptSync2(pin, salt, 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
1560
+ return timingSafeEqual2(actualHash, expectedHash);
1561
+ } catch {
1562
+ return false;
1563
+ }
1564
+ }
1565
+ async function hasPinSet(vaultPath) {
1566
+ if (osSecretsDisabled()) {
1567
+ return false;
1568
+ }
1569
+ try {
1570
+ const stored = await Bun.secrets.get({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE });
1571
+ return stored != null;
1572
+ } catch {
1573
+ return false;
1574
+ }
1575
+ }
1576
+ async function deletePin(vaultPath) {
1577
+ if (osSecretsDisabled()) {
1578
+ return false;
1579
+ }
1580
+ try {
1581
+ return await Bun.secrets.delete({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE });
1582
+ } catch {
1583
+ return false;
1584
+ }
1585
+ }
1586
+ var VAULT_PASSWORD_SERVICE = "autho.vault", USER_SECRETS_SERVICE = "autho", PIN_SERVICE = "autho.pin";
1587
+ var init_os_secrets = () => {};
1588
+
1129
1589
  // apps/cli/src/tui.tsx
1130
1590
  var exports_tui = {};
1131
1591
  __export(exports_tui, {
@@ -1240,28 +1700,33 @@ function useToast() {
1240
1700
  }, []);
1241
1701
  return { toast, show };
1242
1702
  }
1243
- function PasswordScreen({ onUnlock, vaultPath }) {
1703
+ function tryUnlockWithPassword(vaultPath, password) {
1704
+ try {
1705
+ return VaultService.unlock(vaultPath, { password });
1706
+ } catch {
1707
+ return null;
1708
+ }
1709
+ }
1710
+ function tryUnlockWithRecovery(vaultPath, token) {
1711
+ try {
1712
+ return VaultService.unlock(vaultPath, { password: "", recovery: token });
1713
+ } catch {
1714
+ return null;
1715
+ }
1716
+ }
1717
+ function PasswordMethod({ onSubmit, vaultPath: _vaultPath }) {
1244
1718
  const [password, setPassword] = useState("");
1245
- const [error, setError] = useState("");
1246
- const [unlocking, setUnlocking] = useState(false);
1719
+ const [submitting, setSubmitting] = useState(false);
1247
1720
  useKeyboard((key) => {
1248
1721
  if (key.eventType !== "press" && key.eventType !== "repeat")
1249
1722
  return;
1250
- if (unlocking)
1723
+ if (submitting)
1251
1724
  return;
1252
1725
  if (key.name === "enter" || key.name === "return") {
1253
1726
  if (!password)
1254
1727
  return;
1255
- setUnlocking(true);
1256
- setError("");
1257
- try {
1258
- const session = VaultService.unlock(vaultPath, password);
1259
- onUnlock(session);
1260
- } catch {
1261
- setError("Wrong password");
1262
- setPassword("");
1263
- setUnlocking(false);
1264
- }
1728
+ setSubmitting(true);
1729
+ onSubmit(password);
1265
1730
  return;
1266
1731
  }
1267
1732
  if (key.name === "backspace") {
@@ -1274,6 +1739,284 @@ function PasswordScreen({ onUnlock, vaultPath }) {
1274
1739
  if (char && char.length === 1 && char.charCodeAt(0) >= 32)
1275
1740
  setPassword((p) => p + char);
1276
1741
  });
1742
+ return /* @__PURE__ */ jsxDEV("box", {
1743
+ flexDirection: "row",
1744
+ gap: 1,
1745
+ marginTop: 1,
1746
+ children: [
1747
+ /* @__PURE__ */ jsxDEV("text", {
1748
+ fg: "#AAAAAA",
1749
+ children: "Password "
1750
+ }, undefined, false, undefined, this),
1751
+ /* @__PURE__ */ jsxDEV("box", {
1752
+ backgroundColor: "#111111",
1753
+ flexGrow: 1,
1754
+ paddingX: 1,
1755
+ height: 1,
1756
+ children: /* @__PURE__ */ jsxDEV("text", {
1757
+ fg: "#FFD700",
1758
+ children: password ? "*".repeat(password.length) : ""
1759
+ }, undefined, false, undefined, this)
1760
+ }, undefined, false, undefined, this),
1761
+ submitting ? /* @__PURE__ */ jsxDEV("text", {
1762
+ fg: "#FFD700",
1763
+ children: " ..."
1764
+ }, undefined, false, undefined, this) : null
1765
+ ]
1766
+ }, undefined, true, undefined, this);
1767
+ }
1768
+ function PinMethod({ vaultPath, password, onVerified, onError }) {
1769
+ const [pin, setPin] = useState("");
1770
+ const [verifying, setVerifying] = useState(false);
1771
+ useKeyboard((key) => {
1772
+ if (key.eventType !== "press" && key.eventType !== "repeat")
1773
+ return;
1774
+ if (verifying)
1775
+ return;
1776
+ if (key.name === "enter" || key.name === "return") {
1777
+ if (!pin)
1778
+ return;
1779
+ setVerifying(true);
1780
+ verifyPin(vaultPath, pin).then((ok) => {
1781
+ if (ok) {
1782
+ onVerified(password);
1783
+ } else {
1784
+ onError("Wrong PIN");
1785
+ setPin("");
1786
+ setVerifying(false);
1787
+ }
1788
+ });
1789
+ return;
1790
+ }
1791
+ if (key.name === "backspace") {
1792
+ setPin((p) => p.slice(0, -1));
1793
+ return;
1794
+ }
1795
+ if (key.ctrl || key.meta || key.name === "escape" || key.name === "tab" || key.name === "up" || key.name === "down" || key.name === "left" || key.name === "right" || key.name === "home" || key.name === "end" || key.name === "delete" || key.name === "insert" || key.name === "pageup" || key.name === "pagedown" || key.name.startsWith("f") && /^f\d+$/.test(key.name))
1796
+ return;
1797
+ const char = key.sequence;
1798
+ if (char && char.length === 1 && char.charCodeAt(0) >= 32)
1799
+ setPin((p) => p + char);
1800
+ });
1801
+ return /* @__PURE__ */ jsxDEV("box", {
1802
+ flexDirection: "row",
1803
+ gap: 1,
1804
+ marginTop: 1,
1805
+ children: [
1806
+ /* @__PURE__ */ jsxDEV("text", {
1807
+ fg: "#AAAAAA",
1808
+ children: "PIN "
1809
+ }, undefined, false, undefined, this),
1810
+ /* @__PURE__ */ jsxDEV("box", {
1811
+ backgroundColor: "#111111",
1812
+ flexGrow: 1,
1813
+ paddingX: 1,
1814
+ height: 1,
1815
+ children: /* @__PURE__ */ jsxDEV("text", {
1816
+ fg: "#FFD700",
1817
+ children: pin ? "*".repeat(pin.length) : ""
1818
+ }, undefined, false, undefined, this)
1819
+ }, undefined, false, undefined, this),
1820
+ verifying ? /* @__PURE__ */ jsxDEV("text", {
1821
+ fg: "#FFD700",
1822
+ children: " ..."
1823
+ }, undefined, false, undefined, this) : null
1824
+ ]
1825
+ }, undefined, true, undefined, this);
1826
+ }
1827
+ function TotpMethod({ onSubmit, onError: _onError }) {
1828
+ const [code, setCode] = useState("");
1829
+ const [remaining, setRemaining] = useState(0);
1830
+ useEffect(() => {
1831
+ const update = () => {
1832
+ const secs = Math.ceil((30000 - Date.now() % 30000) / 1000);
1833
+ setRemaining(secs);
1834
+ };
1835
+ update();
1836
+ const interval = setInterval(update, 1000);
1837
+ return () => clearInterval(interval);
1838
+ }, []);
1839
+ useKeyboard((key) => {
1840
+ if (key.eventType !== "press" && key.eventType !== "repeat")
1841
+ return;
1842
+ if (key.name === "enter" || key.name === "return") {
1843
+ if (code.length !== 6)
1844
+ return;
1845
+ onSubmit(code);
1846
+ return;
1847
+ }
1848
+ if (key.name === "backspace") {
1849
+ setCode((c) => c.slice(0, -1));
1850
+ return;
1851
+ }
1852
+ if (key.ctrl || key.meta || key.name === "escape" || key.name === "tab" || key.name === "up" || key.name === "down" || key.name === "left" || key.name === "right" || key.name === "home" || key.name === "end" || key.name === "delete" || key.name === "insert" || key.name === "pageup" || key.name === "pagedown" || key.name.startsWith("f") && /^f\d+$/.test(key.name))
1853
+ return;
1854
+ const char = key.sequence;
1855
+ if (char && /^\d$/.test(char) && code.length < 6) {
1856
+ const next = code + char;
1857
+ setCode(next);
1858
+ if (next.length === 6) {
1859
+ onSubmit(next);
1860
+ }
1861
+ }
1862
+ });
1863
+ return /* @__PURE__ */ jsxDEV("box", {
1864
+ flexDirection: "column",
1865
+ gap: 1,
1866
+ marginTop: 1,
1867
+ children: [
1868
+ /* @__PURE__ */ jsxDEV("box", {
1869
+ flexDirection: "row",
1870
+ gap: 1,
1871
+ children: [
1872
+ /* @__PURE__ */ jsxDEV("text", {
1873
+ fg: "#AAAAAA",
1874
+ children: "Auth code "
1875
+ }, undefined, false, undefined, this),
1876
+ /* @__PURE__ */ jsxDEV("box", {
1877
+ backgroundColor: "#111111",
1878
+ flexGrow: 1,
1879
+ paddingX: 1,
1880
+ height: 1,
1881
+ children: /* @__PURE__ */ jsxDEV("text", {
1882
+ fg: "#FFD700",
1883
+ children: code || "_".repeat(6)
1884
+ }, undefined, false, undefined, this)
1885
+ }, undefined, false, undefined, this),
1886
+ /* @__PURE__ */ jsxDEV("text", {
1887
+ fg: remaining <= 5 ? "#FF4444" : "#555555",
1888
+ children: [
1889
+ " ",
1890
+ remaining,
1891
+ "s"
1892
+ ]
1893
+ }, undefined, true, undefined, this)
1894
+ ]
1895
+ }, undefined, true, undefined, this),
1896
+ /* @__PURE__ */ jsxDEV("text", {
1897
+ fg: "#555555",
1898
+ children: "Enter 6-digit authenticator code"
1899
+ }, undefined, false, undefined, this)
1900
+ ]
1901
+ }, undefined, true, undefined, this);
1902
+ }
1903
+ function UnlockScreen({ onUnlock, vaultPath }) {
1904
+ const [step, setStep] = useState("loading");
1905
+ const [error, setError] = useState("");
1906
+ const [password, setPasswordState] = useState("");
1907
+ const [pinNeeded, setPinNeeded] = useState(false);
1908
+ const [totpNeeded, setTotpNeeded] = useState(false);
1909
+ useEffect(() => {
1910
+ let cancelled = false;
1911
+ (async () => {
1912
+ const recoveryFile = process.env.AUTHO_RECOVERY_FILE;
1913
+ if (recoveryFile) {
1914
+ try {
1915
+ const { readFileSync: readFileSync5 } = await import("fs");
1916
+ const content = readFileSync5(recoveryFile, "utf8");
1917
+ const lines = content.split(`
1918
+ `);
1919
+ const idx = lines.findIndex((l) => l.trim() === "RECOVERY TOKEN:");
1920
+ if (idx !== -1) {
1921
+ const rawToken = lines.slice(idx + 1).find((l) => l.trim() !== "") ?? "";
1922
+ const token = rawToken.replace(/-/g, "").toLowerCase();
1923
+ if (token) {
1924
+ const session = tryUnlockWithRecovery(vaultPath, token);
1925
+ if (!cancelled && session) {
1926
+ onUnlock(session);
1927
+ return;
1928
+ }
1929
+ if (!cancelled)
1930
+ setError("Recovery file failed \u2014 enter password instead");
1931
+ } else {
1932
+ if (!cancelled)
1933
+ setError("Recovery file is malformed \u2014 enter password instead");
1934
+ }
1935
+ } else {
1936
+ if (!cancelled)
1937
+ setError("Recovery file is malformed \u2014 enter password instead");
1938
+ }
1939
+ } catch {
1940
+ if (!cancelled)
1941
+ setError("Recovery file could not be read \u2014 enter password instead");
1942
+ }
1943
+ }
1944
+ const pinSet = await hasPinSet(vaultPath);
1945
+ if (!cancelled)
1946
+ setPinNeeded(pinSet);
1947
+ const authConfig = VaultService.getAuthConfig(vaultPath);
1948
+ if (!cancelled)
1949
+ setTotpNeeded(authConfig?.totp !== undefined);
1950
+ if (!pinSet) {
1951
+ const pw = await loadVaultPassword(vaultPath);
1952
+ if (!cancelled && pw && !authConfig?.totp) {
1953
+ const session = tryUnlockWithPassword(vaultPath, pw);
1954
+ if (!cancelled && session) {
1955
+ onUnlock(session);
1956
+ return;
1957
+ }
1958
+ }
1959
+ if (!cancelled && pw) {
1960
+ setPasswordState(pw);
1961
+ if (!cancelled)
1962
+ setStep(authConfig?.totp ? "totp" : "password");
1963
+ return;
1964
+ }
1965
+ }
1966
+ if (!cancelled)
1967
+ setStep("password");
1968
+ })();
1969
+ return () => {
1970
+ cancelled = true;
1971
+ };
1972
+ }, [vaultPath, onUnlock]);
1973
+ const doUnlock = useCallback((pw, totp) => {
1974
+ const creds = { password: pw, totp };
1975
+ try {
1976
+ const session = VaultService.unlock(vaultPath, creds);
1977
+ onUnlock(session);
1978
+ } catch (e) {
1979
+ setError(e instanceof Error ? e.message : String(e));
1980
+ setStep("password");
1981
+ setPasswordState("");
1982
+ }
1983
+ }, [vaultPath, onUnlock]);
1984
+ const handlePasswordSubmit = useCallback((pw) => {
1985
+ setPasswordState(pw);
1986
+ if (pinNeeded) {
1987
+ setStep("pin");
1988
+ } else if (totpNeeded) {
1989
+ setStep("totp");
1990
+ } else {
1991
+ setStep("unlocking");
1992
+ doUnlock(pw, undefined);
1993
+ }
1994
+ }, [pinNeeded, totpNeeded, doUnlock]);
1995
+ const handlePinVerified = useCallback((pw) => {
1996
+ if (totpNeeded) {
1997
+ setStep("totp");
1998
+ } else {
1999
+ setStep("unlocking");
2000
+ doUnlock(pw, undefined);
2001
+ }
2002
+ }, [totpNeeded, doUnlock]);
2003
+ const handleTotpSubmit = useCallback((totp) => {
2004
+ setStep("unlocking");
2005
+ doUnlock(password, totp);
2006
+ }, [password, doUnlock]);
2007
+ if (step === "loading" || step === "unlocking") {
2008
+ return /* @__PURE__ */ jsxDEV("box", {
2009
+ flexDirection: "column",
2010
+ alignItems: "center",
2011
+ justifyContent: "center",
2012
+ width: "100%",
2013
+ height: "100%",
2014
+ children: /* @__PURE__ */ jsxDEV("text", {
2015
+ fg: "#FFD700",
2016
+ children: step === "loading" ? "Unlocking..." : "Verifying..."
2017
+ }, undefined, false, undefined, this)
2018
+ }, undefined, false, undefined, this);
2019
+ }
1277
2020
  return /* @__PURE__ */ jsxDEV("box", {
1278
2021
  flexDirection: "column",
1279
2022
  alignItems: "center",
@@ -1306,31 +2049,21 @@ function PasswordScreen({ onUnlock, vaultPath }) {
1306
2049
  children: error
1307
2050
  }, undefined, false, undefined, this)
1308
2051
  }, undefined, false, undefined, this) : null,
1309
- /* @__PURE__ */ jsxDEV("box", {
1310
- flexDirection: "row",
1311
- gap: 1,
1312
- marginTop: 1,
1313
- children: [
1314
- /* @__PURE__ */ jsxDEV("text", {
1315
- fg: "#AAAAAA",
1316
- children: "Password "
1317
- }, undefined, false, undefined, this),
1318
- /* @__PURE__ */ jsxDEV("box", {
1319
- backgroundColor: "#111111",
1320
- flexGrow: 1,
1321
- paddingX: 1,
1322
- height: 1,
1323
- children: /* @__PURE__ */ jsxDEV("text", {
1324
- fg: "#FFD700",
1325
- children: password ? "*".repeat(password.length) : ""
1326
- }, undefined, false, undefined, this)
1327
- }, undefined, false, undefined, this)
1328
- ]
1329
- }, undefined, true, undefined, this),
1330
- unlocking ? /* @__PURE__ */ jsxDEV("text", {
1331
- fg: "#FFD700",
1332
- children: "Unlocking..."
1333
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV("text", {
2052
+ step === "password" && /* @__PURE__ */ jsxDEV(PasswordMethod, {
2053
+ vaultPath,
2054
+ onSubmit: handlePasswordSubmit
2055
+ }, undefined, false, undefined, this),
2056
+ step === "pin" && /* @__PURE__ */ jsxDEV(PinMethod, {
2057
+ vaultPath,
2058
+ password,
2059
+ onVerified: handlePinVerified,
2060
+ onError: setError
2061
+ }, undefined, false, undefined, this),
2062
+ step === "totp" && /* @__PURE__ */ jsxDEV(TotpMethod, {
2063
+ onSubmit: handleTotpSubmit,
2064
+ onError: setError
2065
+ }, undefined, false, undefined, this),
2066
+ /* @__PURE__ */ jsxDEV("text", {
1334
2067
  fg: "#444444",
1335
2068
  children: "Enter to unlock | Ctrl+C to exit"
1336
2069
  }, undefined, false, undefined, this)
@@ -2322,7 +3055,7 @@ function EditScreen({ session, secret, onDone, onBack }) {
2322
3055
  }, undefined, true, undefined, this);
2323
3056
  }
2324
3057
  function App({ vaultPath }) {
2325
- const [screen, setScreen] = useState("password");
3058
+ const [screen, setScreen] = useState("unlock");
2326
3059
  const [session, setSession] = useState(null);
2327
3060
  const [selectedSecret, setSelectedSecret] = useState(null);
2328
3061
  const { toast, show: showToast } = useToast();
@@ -2336,8 +3069,8 @@ function App({ vaultPath }) {
2336
3069
  goHome();
2337
3070
  showToast(msg, msg.startsWith("Error:") ? "error" : "success");
2338
3071
  }, [goHome, showToast]);
2339
- if (screen === "password") {
2340
- return /* @__PURE__ */ jsxDEV(PasswordScreen, {
3072
+ if (screen === "unlock") {
3073
+ return /* @__PURE__ */ jsxDEV(UnlockScreen, {
2341
3074
  vaultPath,
2342
3075
  onUnlock: (s) => {
2343
3076
  setSession(s);
@@ -2423,6 +3156,7 @@ async function runTui(vaultPath) {
2423
3156
  var EDIT_FIELDS;
2424
3157
  var init_tui = __esm(() => {
2425
3158
  init_src3();
3159
+ init_os_secrets();
2426
3160
  EDIT_FIELDS = [
2427
3161
  { key: "name", label: "Name", forTypes: ["password", "note", "otp"] },
2428
3162
  { key: "value", label: "Value", forTypes: ["password", "note", "otp"] },
@@ -2434,11 +3168,30 @@ var init_tui = __esm(() => {
2434
3168
 
2435
3169
  // apps/cli/src/index.ts
2436
3170
  import { spawn } from "child_process";
2437
- import { existsSync as existsSync4 } from "fs";
2438
- import { createInterface } from "readline/promises";
2439
- import { resolve as resolve3 } from "path";
3171
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
3172
+ import { createInterface as createInterface2 } from "readline/promises";
3173
+ import { resolve as resolve4 } from "path";
2440
3174
 
2441
3175
  // apps/cli/src/password.ts
3176
+ import { createInterface } from "readline/promises";
3177
+ async function readLine(prompt) {
3178
+ if (!process.stdin.isTTY) {
3179
+ throw new Error("Cannot read input: stdin is not a TTY");
3180
+ }
3181
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3182
+ try {
3183
+ return await rl.question(prompt);
3184
+ } finally {
3185
+ rl.close();
3186
+ }
3187
+ }
3188
+ async function confirm(question, defaultYes = true) {
3189
+ const hint = defaultYes ? "Y/n" : "y/N";
3190
+ const answer = (await readLine(`${question} (${hint}) `)).trim().toLowerCase();
3191
+ if (answer === "")
3192
+ return defaultYes;
3193
+ return answer === "y" || answer === "yes";
3194
+ }
2442
3195
  async function readPasswordMasked(prompt = "Master password: ") {
2443
3196
  if (!process.stdin.isTTY) {
2444
3197
  throw new Error("Cannot read password: stdin is not a TTY");
@@ -2480,9 +3233,8 @@ async function readPasswordMasked(prompt = "Master password: ") {
2480
3233
  }
2481
3234
  return;
2482
3235
  }
2483
- if (code === 27) {
3236
+ if (code === 27)
2484
3237
  return;
2485
- }
2486
3238
  if (code >= 32) {
2487
3239
  password += char;
2488
3240
  process.stdout.write("*");
@@ -2504,7 +3256,7 @@ init_src3();
2504
3256
  init_paths();
2505
3257
  init_paths();
2506
3258
  import { createHash, timingSafeEqual } from "crypto";
2507
- import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
3259
+ import { existsSync as existsSync4, readFileSync as readFileSync4, rmSync } from "fs";
2508
3260
  var DAEMON_TOKEN_SERVICE = "autho.daemon";
2509
3261
  function daemonTokenName(statePath) {
2510
3262
  return createHash("sha256").update(statePath).digest("hex");
@@ -2562,10 +3314,10 @@ async function deleteStoredDaemonToken(state) {
2562
3314
  } catch {}
2563
3315
  }
2564
3316
  function readDaemonState(statePath) {
2565
- if (!existsSync3(statePath)) {
3317
+ if (!existsSync4(statePath)) {
2566
3318
  return null;
2567
3319
  }
2568
- const stored = JSON.parse(readFileSync3(statePath, "utf8"));
3320
+ const stored = JSON.parse(readFileSync4(statePath, "utf8"));
2569
3321
  return {
2570
3322
  pid: stored.pid,
2571
3323
  port: stored.port,
@@ -2594,7 +3346,7 @@ async function writeDaemonState(statePath, state) {
2594
3346
  async function deleteDaemonState(statePath) {
2595
3347
  const state = readDaemonState(statePath);
2596
3348
  await deleteStoredDaemonToken(state);
2597
- if (existsSync3(statePath)) {
3349
+ if (existsSync4(statePath)) {
2598
3350
  rmSync(statePath, { force: true });
2599
3351
  }
2600
3352
  }
@@ -2781,12 +3533,12 @@ async function startDaemonServer(options) {
2781
3533
  }
2782
3534
  async function waitForDaemonStateDeletion(statePath) {
2783
3535
  for (let attempt = 0;attempt < 20; attempt += 1) {
2784
- if (!existsSync3(statePath)) {
3536
+ if (!existsSync4(statePath)) {
2785
3537
  return true;
2786
3538
  }
2787
3539
  await Bun.sleep(25);
2788
3540
  }
2789
- return !existsSync3(statePath);
3541
+ return !existsSync4(statePath);
2790
3542
  }
2791
3543
  async function daemonRequest(state, path, body) {
2792
3544
  const token = await resolveDaemonToken(state);
@@ -2861,6 +3613,7 @@ async function daemonStop(options) {
2861
3613
 
2862
3614
  // apps/cli/src/index.ts
2863
3615
  init_src3();
3616
+ init_os_secrets();
2864
3617
  function parseArgs(argv) {
2865
3618
  const dashDashIndex = argv.indexOf("--");
2866
3619
  const main = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
@@ -2937,7 +3690,7 @@ function output(value, jsonMode = false) {
2937
3690
  console.log(value);
2938
3691
  }
2939
3692
  function absolutePath(path) {
2940
- return resolve3(path);
3693
+ return resolve4(path);
2941
3694
  }
2942
3695
  function buildSecretMetadata(args) {
2943
3696
  return Object.fromEntries(Object.entries({
@@ -2957,7 +3710,7 @@ async function readBufferedStdin() {
2957
3710
  }
2958
3711
  async function createPromptAdapter() {
2959
3712
  if (process.stdin.isTTY) {
2960
- const rl = createInterface({
3713
+ const rl = createInterface2({
2961
3714
  input: process.stdin,
2962
3715
  output: process.stdout
2963
3716
  });
@@ -3049,6 +3802,128 @@ async function runPromptMode(vaultPath, initialPassword) {
3049
3802
  prompt.close();
3050
3803
  }
3051
3804
  }
3805
+ async function resolveUnlockCredentials(vaultPath, args, existingPassword) {
3806
+ let password = existingPassword ?? getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
3807
+ if (!password) {
3808
+ password = await loadVaultPassword(vaultPath) ?? undefined;
3809
+ }
3810
+ if (!password && process.stdin.isTTY) {
3811
+ password = await readPasswordMasked("Master password: ");
3812
+ }
3813
+ const creds = { password: required(password, "--password") };
3814
+ if (await hasPinSet(vaultPath)) {
3815
+ const pin = process.env.AUTHO_PIN ?? getString(args, "pin") ?? (process.stdin.isTTY ? await readPasswordMasked("PIN: ") : undefined);
3816
+ if (!pin)
3817
+ throw new Error("PIN is set on this vault \u2014 provide it with AUTHO_PIN, --pin, or interactively");
3818
+ const ok = await verifyPin(vaultPath, pin);
3819
+ if (!ok)
3820
+ throw new Error("Wrong PIN");
3821
+ }
3822
+ const authConfig = VaultService.getAuthConfig(vaultPath);
3823
+ if (authConfig?.totp) {
3824
+ const totp = process.env.AUTHO_TOTP_CODE ?? getString(args, "totp") ?? (process.stdin.isTTY ? await readPasswordMasked("Authenticator code: ") : undefined);
3825
+ if (!totp)
3826
+ throw new Error("TOTP is enabled \u2014 provide a 6-digit code with AUTHO_TOTP_CODE, --totp, or interactively");
3827
+ creds.totp = totp;
3828
+ }
3829
+ return creds;
3830
+ }
3831
+ function osKeychainName() {
3832
+ const p = process.platform;
3833
+ if (p === "darwin")
3834
+ return "macOS Keychain";
3835
+ if (p === "win32")
3836
+ return "Windows Credential Manager";
3837
+ return "Secret Service (libsecret)";
3838
+ }
3839
+ async function runInitWizard(vaultPath, password, existingCreds) {
3840
+ const creds = existingCreds ?? { password };
3841
+ console.log("");
3842
+ if (!osSecretsDisabled()) {
3843
+ const passwordStored = await loadVaultPassword(vaultPath) !== null;
3844
+ if (passwordStored) {
3845
+ console.log(`\x1B[32m\u2713\x1B[0m Master password is saved in ${osKeychainName()}.`);
3846
+ if (await confirm(" Remove it?", false)) {
3847
+ const deleted = await deleteVaultPassword(vaultPath);
3848
+ console.log(deleted ? " Removed." : " Could not remove.");
3849
+ }
3850
+ } else {
3851
+ console.log(`\x1B[33m?\x1B[0m Save master password to ${osKeychainName()}?`);
3852
+ console.log(" This lets all vault commands unlock without prompting on this machine.");
3853
+ if (await confirm(" Save to keychain?")) {
3854
+ const stored = await storeVaultPassword(vaultPath, password);
3855
+ console.log(stored ? " \x1B[32m\u2713\x1B[0m Saved." : " Could not save \u2014 OS secret store may be unavailable.");
3856
+ } else {
3857
+ console.log(" Skipped.");
3858
+ }
3859
+ }
3860
+ console.log("");
3861
+ }
3862
+ if (!osSecretsDisabled()) {
3863
+ const pinSet = await hasPinSet(vaultPath);
3864
+ if (pinSet) {
3865
+ console.log("\x1B[32m\u2713\x1B[0m PIN is set on this machine.");
3866
+ if (await confirm(" Remove PIN?", false)) {
3867
+ const pin = await readPasswordMasked(" Enter current PIN: ");
3868
+ const ok = await verifyPin(vaultPath, pin);
3869
+ if (ok) {
3870
+ await deletePin(vaultPath);
3871
+ console.log(" Removed.");
3872
+ } else {
3873
+ console.log(" Wrong PIN, not removed.");
3874
+ }
3875
+ }
3876
+ } else {
3877
+ if (await confirm("Set up a PIN for quick unlock on this machine?", false)) {
3878
+ const newPin = await readPasswordMasked(" New PIN: ");
3879
+ if (!newPin) {
3880
+ console.log(" PIN cannot be empty, skipped.");
3881
+ } else {
3882
+ const confirmPin = await readPasswordMasked(" Confirm PIN: ");
3883
+ if (newPin !== confirmPin) {
3884
+ console.log(" PINs do not match, skipped.");
3885
+ } else {
3886
+ await storePinHash(vaultPath, newPin);
3887
+ console.log(" \x1B[32m\u2713\x1B[0m PIN set.");
3888
+ }
3889
+ }
3890
+ }
3891
+ }
3892
+ console.log("");
3893
+ }
3894
+ const authConfig = VaultService.getAuthConfig(vaultPath);
3895
+ const totpEnabled = authConfig?.totp !== undefined;
3896
+ if (totpEnabled) {
3897
+ console.log("\x1B[32m\u2713\x1B[0m TOTP is enabled (authenticator app required to unlock).");
3898
+ if (await confirm(" Disable TOTP?", false)) {
3899
+ const code = await readPasswordMasked(" Enter current TOTP code: ");
3900
+ try {
3901
+ VaultService.removeTotp(vaultPath, { ...creds, totp: code });
3902
+ console.log(" Disabled.");
3903
+ } catch (e) {
3904
+ console.log(` Error: ${e instanceof Error ? e.message : String(e)}`);
3905
+ }
3906
+ }
3907
+ } else {
3908
+ if (await confirm("Enable TOTP (authenticator app verification)?", false)) {
3909
+ const { secret, uri } = VaultService.setupTotp(vaultPath);
3910
+ console.log("");
3911
+ console.log(` Secret: ${secret}`);
3912
+ console.log(` URI: ${uri}`);
3913
+ console.log("");
3914
+ console.log(" Add this to your authenticator app, then enter the 6-digit code.");
3915
+ const code = await readPasswordMasked(" Code: ");
3916
+ try {
3917
+ VaultService.enableTotp(vaultPath, creds, secret, code);
3918
+ console.log(" \x1B[32m\u2713\x1B[0m TOTP enabled.");
3919
+ } catch (e) {
3920
+ console.log(` Error: ${e instanceof Error ? e.message : String(e)}`);
3921
+ }
3922
+ }
3923
+ }
3924
+ console.log("");
3925
+ console.log("Setup complete. Run \x1B[1mautho init\x1B[0m anytime to change these settings.");
3926
+ }
3052
3927
  async function runWebServer(vaultPath, args) {
3053
3928
  const commandArgs = [
3054
3929
  "run",
@@ -3092,6 +3967,10 @@ function help() {
3092
3967
  " prompt [--vault <path>]",
3093
3968
  " init [--vault <path>]",
3094
3969
  " status [--vault <path>] [--project-file <path>] [--json]",
3970
+ " config [show] Show current configuration",
3971
+ " config set <key> <value> Set a config value",
3972
+ " config unset <key> Remove a config value",
3973
+ " config path Print config file path",
3095
3974
  " project init --map <ENV=ref> [--output <path>] [--force] [--json]",
3096
3975
  " web serve [--vault <path>] [--host <value>] [--port <value>]",
3097
3976
  " daemon serve [--vault <path>] [--state-file <path>] [--host <value>] [--port <value>]",
@@ -3118,20 +3997,40 @@ function help() {
3118
3997
  " files encrypt --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
3119
3998
  " files decrypt --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
3120
3999
  " audit list [--limit <number>] [--vault <path>] [--json]",
4000
+ " recovery generate --output <path> [--vault <path>] [--json]",
4001
+ " recovery revoke [--vault <path>] [--json]",
4002
+ " unlock --recovery-file <path> [--vault <path>] [--json]",
4003
+ " os-secrets set --name <name> [--value <value>] [--json]",
4004
+ " os-secrets get --name <name> [--json]",
4005
+ " os-secrets delete --name <name> [--json]",
3121
4006
  "",
3122
4007
  "Authentication:",
3123
4008
  " When running interactively (TTY), you will be securely prompted for your",
3124
4009
  " master password with masked input (no --password flag needed).",
3125
4010
  "",
3126
4011
  " For automation and coding agents, use one of:",
3127
- " AUTHO_MASTER_PASSWORD=<value> Environment variable (recommended for agents)",
4012
+ " autho init Setup wizard \u2014 choose to save password in OS keychain, set PIN, enable TOTP",
4013
+ " AUTHO_MASTER_PASSWORD=<value> Environment variable (master password)",
4014
+ " AUTHO_PIN=<value> Environment variable (PIN, if set on this machine)",
4015
+ " AUTHO_TOTP_CODE=<value> Environment variable (TOTP code, if enabled)",
3128
4016
  " --password <value> CLI flag (visible in shell history - avoid!)",
3129
4017
  "",
4018
+ " Native OS secret store support (via Bun.secrets):",
4019
+ " macOS \u2192 Keychain Services",
4020
+ " Linux \u2192 libsecret / GNOME Keyring / KWallet",
4021
+ " Windows \u2192 Windows Credential Manager",
4022
+ "",
4023
+ " The OS secret store is checked automatically before prompting.",
4024
+ " Set AUTHO_DISABLE_OS_SECRETS=1 to opt out.",
4025
+ "",
3130
4026
  "Notes:",
3131
4027
  " Running `autho` with no arguments opens the interactive TUI.",
3132
- " The default vault path is ~/.autho/vault.db (override with AUTHO_HOME).",
3133
- " The default project file is ~/.autho/project.json.",
3134
- " The default daemon state file is ~/.autho/daemon.json."
4028
+ " Config is stored in ~/.autho/config.json (always at ~/.autho).",
4029
+ " The vault directory can be customized via `autho init` or `autho config set vaultDir <path>`.",
4030
+ " Config keys: vaultDir, defaultLeaseTtl, editor, autoLock, autoLockTimeout",
4031
+ " The default vault path is ~/.autho/vault.db (override with config or AUTHO_HOME).",
4032
+ " The default project file is <vaultDir>/project.json.",
4033
+ " The default daemon state file is <vaultDir>/daemon.json."
3135
4034
  ].join(`
3136
4035
  `);
3137
4036
  }
@@ -3143,8 +4042,11 @@ async function main() {
3143
4042
  const statePath = absolutePath(getString(args, "state-file") ?? defaultDaemonStatePath());
3144
4043
  const explicitProjectFile = getString(args, "project-file");
3145
4044
  const fallbackProjectFile = defaultProjectFilePath();
3146
- const projectFile = explicitProjectFile ?? (existsSync4(fallbackProjectFile) ? fallbackProjectFile : undefined);
4045
+ const projectFile = explicitProjectFile ?? (existsSync5(fallbackProjectFile) ? fallbackProjectFile : undefined);
3147
4046
  let password = getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
4047
+ if (!password) {
4048
+ password = await loadVaultPassword(vaultPath) ?? undefined;
4049
+ }
3148
4050
  if (!scope) {
3149
4051
  if (process.stdin.isTTY) {
3150
4052
  const { runTui: runTui2 } = await Promise.resolve().then(() => (init_tui(), exports_tui));
@@ -3164,7 +4066,6 @@ async function main() {
3164
4066
  }
3165
4067
  if (!password && process.stdin.isTTY) {
3166
4068
  const needsPassword = [
3167
- "init",
3168
4069
  "secrets",
3169
4070
  "otp",
3170
4071
  "lease",
@@ -3173,7 +4074,9 @@ async function main() {
3173
4074
  "file",
3174
4075
  "files",
3175
4076
  "audit",
3176
- "import"
4077
+ "import",
4078
+ "recovery",
4079
+ "unlock"
3177
4080
  ].includes(scope);
3178
4081
  const daemonNeedsPassword = scope === "daemon" && action === "unlock";
3179
4082
  if (needsPassword || daemonNeedsPassword) {
@@ -3181,7 +4084,39 @@ async function main() {
3181
4084
  }
3182
4085
  }
3183
4086
  if (scope === "init") {
3184
- output(VaultService.initialize(vaultPath, required(password, "--password")), jsonMode);
4087
+ let effectiveVaultPath = vaultPath;
4088
+ if (!VaultService.status(vaultPath).initialized && process.stdin.isTTY && !jsonMode) {
4089
+ const config = loadConfig();
4090
+ const currentDir = config.vaultDir ?? authoConfigDir();
4091
+ console.log(`
4092
+ \x1B[1mVault directory\x1B[0m`);
4093
+ console.log(` Default: ${currentDir}`);
4094
+ console.log(" Tip: Use a cloud-synced folder (e.g. Google Drive) to share the vault across machines.");
4095
+ const customDir = (await readLine(` Path (Enter to keep default): `)).trim();
4096
+ if (customDir && customDir !== currentDir) {
4097
+ const resolved = resolve4(customDir).replace(/\\/g, "/");
4098
+ saveConfig({ ...config, vaultDir: resolved });
4099
+ effectiveVaultPath = resolved + "/vault.db";
4100
+ console.log(` \x1B[32m\u2713\x1B[0m Vault directory set to ${resolved}`);
4101
+ console.log(` Config saved to ${authoConfigDir()}/config.json`);
4102
+ }
4103
+ console.log("");
4104
+ }
4105
+ const existingStatus = VaultService.status(effectiveVaultPath);
4106
+ if (!existingStatus.initialized) {
4107
+ const pw = required(password, "--password");
4108
+ output(VaultService.initialize(effectiveVaultPath, pw), jsonMode);
4109
+ if (process.stdin.isTTY && !jsonMode) {
4110
+ await runInitWizard(effectiveVaultPath, pw);
4111
+ }
4112
+ } else {
4113
+ const creds2 = await resolveUnlockCredentials(effectiveVaultPath, args, password);
4114
+ if (process.stdin.isTTY && !jsonMode) {
4115
+ await runInitWizard(effectiveVaultPath, creds2.password, creds2);
4116
+ } else {
4117
+ console.log("Vault already initialized. Use interactive mode (TTY) to reconfigure.");
4118
+ }
4119
+ }
3185
4120
  return;
3186
4121
  }
3187
4122
  if (scope === "status") {
@@ -3191,6 +4126,72 @@ async function main() {
3191
4126
  }), jsonMode);
3192
4127
  return;
3193
4128
  }
4129
+ if (scope === "config") {
4130
+ const config = loadConfig();
4131
+ if (!action || action === "show") {
4132
+ if (jsonMode) {
4133
+ output({ configDir: authoConfigDir(), config }, jsonMode);
4134
+ } else {
4135
+ console.log(`Config: ${authoConfigDir()}/config.json`);
4136
+ console.log(`Vault dir: ${config.vaultDir ?? authoConfigDir()} ${config.vaultDir ? "(custom)" : "(default)"}`);
4137
+ if (config.defaultLeaseTtl)
4138
+ console.log(`Default lease TTL: ${config.defaultLeaseTtl}`);
4139
+ if (config.editor)
4140
+ console.log(`Editor: ${config.editor}`);
4141
+ if (config.autoLock !== undefined)
4142
+ console.log(`Auto-lock: ${config.autoLock}`);
4143
+ if (config.autoLockTimeout)
4144
+ console.log(`Auto-lock timeout: ${config.autoLockTimeout}`);
4145
+ }
4146
+ return;
4147
+ }
4148
+ if (action === "set") {
4149
+ const key = subaction;
4150
+ const value = process.argv[process.argv.indexOf("set") + 2];
4151
+ if (!key || value === undefined) {
4152
+ console.error("Usage: autho config set <key> <value>");
4153
+ console.error("Keys: vaultDir, defaultLeaseTtl, editor, autoLock, autoLockTimeout");
4154
+ process.exit(1);
4155
+ }
4156
+ const updated = { ...config };
4157
+ if (key === "autoLock") {
4158
+ updated.autoLock = value === "true";
4159
+ } else if (key === "vaultDir") {
4160
+ updated.vaultDir = value;
4161
+ } else if (key === "defaultLeaseTtl") {
4162
+ updated.defaultLeaseTtl = value;
4163
+ } else if (key === "editor") {
4164
+ updated.editor = value;
4165
+ } else if (key === "autoLockTimeout") {
4166
+ updated.autoLockTimeout = value;
4167
+ } else {
4168
+ console.error(`Unknown config key: ${key}`);
4169
+ console.error("Keys: vaultDir, defaultLeaseTtl, editor, autoLock, autoLockTimeout");
4170
+ process.exit(1);
4171
+ }
4172
+ saveConfig(updated);
4173
+ console.log(`Set ${key} = ${value}`);
4174
+ return;
4175
+ }
4176
+ if (action === "unset") {
4177
+ const key = subaction;
4178
+ if (!key) {
4179
+ console.error("Usage: autho config unset <key>");
4180
+ process.exit(1);
4181
+ }
4182
+ const updated = { ...config };
4183
+ delete updated[key];
4184
+ saveConfig(updated);
4185
+ console.log(`Removed ${key}`);
4186
+ return;
4187
+ }
4188
+ if (action === "path") {
4189
+ console.log(authoConfigDir() + "/config.json");
4190
+ return;
4191
+ }
4192
+ console.error(`Unknown config action: ${action}. Use: show, set, unset, path`);
4193
+ process.exit(1);
4194
+ }
3194
4195
  if (scope === "project" && action === "init") {
3195
4196
  output(writeProjectConfig({
3196
4197
  force: getBoolean(args, "force"),
@@ -3199,6 +4200,31 @@ async function main() {
3199
4200
  }), jsonMode);
3200
4201
  return;
3201
4202
  }
4203
+ if (scope === "os-secrets" && action === "set") {
4204
+ const name = required(getString(args, "name"), "--name");
4205
+ const value = getString(args, "value") ?? await readPasswordMasked(`Value for "${name}": `);
4206
+ const stored = await setOsSecret(name, value);
4207
+ if (!stored) {
4208
+ throw new Error("OS secret store is unavailable on this system (try AUTHO_DISABLE_OS_SECRETS=1 to confirm, or check that a secret service daemon is running on Linux)");
4209
+ }
4210
+ output({ name, stored: true }, jsonMode);
4211
+ return;
4212
+ }
4213
+ if (scope === "os-secrets" && action === "get") {
4214
+ const name = required(getString(args, "name"), "--name");
4215
+ const value = await getOsSecret(name);
4216
+ if (value === null) {
4217
+ throw new Error(`Secret "${name}" not found in OS secret store`);
4218
+ }
4219
+ output({ name, value }, jsonMode);
4220
+ return;
4221
+ }
4222
+ if (scope === "os-secrets" && action === "delete") {
4223
+ const name = required(getString(args, "name"), "--name");
4224
+ const deleted = await deleteOsSecret(name);
4225
+ output({ deleted, name }, jsonMode);
4226
+ return;
4227
+ }
3202
4228
  if (scope === "web" && action === "serve") {
3203
4229
  await runWebServer(vaultPath, args);
3204
4230
  return;
@@ -3256,7 +4282,44 @@ async function main() {
3256
4282
  process.stderr.write(result.stderr);
3257
4283
  process.exit(result.exitCode);
3258
4284
  }
3259
- const session = VaultService.unlock(vaultPath, required(password, "--password"));
4285
+ if (scope === "recovery" && action === "generate") {
4286
+ const outputPath = absolutePath(required(getString(args, "output"), "--output"));
4287
+ const creds2 = await resolveUnlockCredentials(vaultPath, args, password);
4288
+ const { fileContent } = VaultService.generateRecovery(vaultPath, creds2);
4289
+ writeFileSync2(outputPath, fileContent, { encoding: "utf8", mode: 384 });
4290
+ if (!jsonMode) {
4291
+ console.log(`Recovery file written to ${outputPath}`);
4292
+ console.log("WARNING: Anyone with this file can open your vault. Store it offline.");
4293
+ } else {
4294
+ output({ outputPath, written: true }, jsonMode);
4295
+ }
4296
+ return;
4297
+ }
4298
+ if (scope === "recovery" && action === "revoke") {
4299
+ const creds2 = await resolveUnlockCredentials(vaultPath, args, password);
4300
+ VaultService.revokeRecovery(vaultPath, creds2);
4301
+ output({ revoked: true }, jsonMode);
4302
+ return;
4303
+ }
4304
+ if (scope === "unlock" && getString(args, "recovery-file")) {
4305
+ const recoveryFilePath = absolutePath(getString(args, "recovery-file"));
4306
+ const content = readFileSync5(recoveryFilePath, "utf8");
4307
+ const lines = content.split(`
4308
+ `);
4309
+ const tokenLineIdx = lines.findIndex((l) => l.trim() === "RECOVERY TOKEN:");
4310
+ if (tokenLineIdx === -1)
4311
+ throw new Error("Invalid recovery file format");
4312
+ const rawTokenLine = lines.slice(tokenLineIdx + 1).find((l) => l.trim() !== "") ?? "";
4313
+ const token = rawTokenLine.replace(/-/g, "").toLowerCase();
4314
+ if (!token)
4315
+ throw new Error("Invalid recovery file format: missing token");
4316
+ const recoverySession = VaultService.unlock(vaultPath, { password: "", recovery: token });
4317
+ output({ unlocked: true, vaultPath }, jsonMode);
4318
+ recoverySession.close();
4319
+ return;
4320
+ }
4321
+ const creds = await resolveUnlockCredentials(vaultPath, args, password);
4322
+ const session = VaultService.unlock(vaultPath, creds);
3260
4323
  try {
3261
4324
  if (scope === "import" && action === "legacy") {
3262
4325
  output(session.importLegacyFile(absolutePath(required(getString(args, "file"), "--file")), {