autho 3.0.0 → 3.0.1

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 +987 -60
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -28,7 +28,9 @@ autho secrets get --password "..." --ref github --json
28
28
  autho otp code --password "..." --ref my-totp --json
29
29
  ```
30
30
 
31
- You can also set `AUTHO_MASTER_PASSWORD` to avoid passing `--password` on every call.
31
+ Run `autho init` to save your master password to the native OS secret store (macOS Keychain, Linux Secret Service, Windows Credential Manager). After that, all commands unlock silently without prompting.
32
+
33
+ You can also set `AUTHO_MASTER_PASSWORD` to avoid passing `--password` on every call, or set `AUTHO_DISABLE_OS_SECRETS=1` to opt out of OS secret storage.
32
34
 
33
35
  ## Features
34
36
 
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 = {
@@ -465,7 +546,7 @@ var init_artifacts = __esm(() => {
465
546
 
466
547
  // packages/core/src/index.ts
467
548
  import { spawnSync } from "child_process";
468
- import { createHmac, randomBytes as randomBytes3 } from "crypto";
549
+ import { createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
469
550
  import {
470
551
  existsSync as existsSync2,
471
552
  readFileSync as readFileSync2
@@ -477,7 +558,7 @@ function requireValue(value, label) {
477
558
  }
478
559
  return value;
479
560
  }
480
- function decodeBase32(input) {
561
+ function decodeBase322(input) {
481
562
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
482
563
  const normalized = input.toUpperCase().replace(/=+$/g, "").replace(/\s+/g, "");
483
564
  let bits = 0;
@@ -500,7 +581,7 @@ function decodeBase32(input) {
500
581
  function generateTotp(secret, options, now = Date.now()) {
501
582
  const algorithm = (options?.algorithm ?? "sha1").toLowerCase();
502
583
  const digits = options?.digits ?? 6;
503
- const key = decodeBase32(secret);
584
+ const key = decodeBase322(secret);
504
585
  const counter = Math.floor(now / 30000);
505
586
  const message = Buffer.alloc(8);
506
587
  let cursor = counter;
@@ -508,7 +589,7 @@ function generateTotp(secret, options, now = Date.now()) {
508
589
  message[index] = cursor & 255;
509
590
  cursor >>= 8;
510
591
  }
511
- const hash = createHmac(algorithm, Buffer.from(key)).update(message).digest();
592
+ const hash = createHmac2(algorithm, Buffer.from(key)).update(message).digest();
512
593
  const offset = hash[hash.length - 1] & 15;
513
594
  const binary = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
514
595
  const mod = 10 ** digits;
@@ -710,21 +791,212 @@ class VaultService {
710
791
  db.close();
711
792
  }
712
793
  }
713
- static unlock(vaultPath, password) {
794
+ static unlock(vaultPath, credentials) {
795
+ const creds = typeof credentials === "string" ? { password: credentials } : credentials;
714
796
  const db = new AuthoDatabase(vaultPath);
715
797
  const config = db.getVaultConfig();
716
798
  if (!config) {
717
799
  db.close();
718
800
  throw new Error(`Vault is not initialized at ${vaultPath}`);
719
801
  }
802
+ const vaultAuth = db.getVaultAuth();
803
+ if (creds.recovery) {
804
+ if (!vaultAuth?.recovery) {
805
+ db.close();
806
+ throw new Error("No recovery token configured for this vault");
807
+ }
808
+ try {
809
+ const recoveryKEK = deriveKeyFromPassword(creds.recovery, vaultAuth.recovery.kdf);
810
+ const rootKey = decryptWithKey(vaultAuth.recovery.wrappedRootKey, recoveryKEK, "autho:vault-recovery");
811
+ db.insertAudit({
812
+ createdAt: new Date().toISOString(),
813
+ eventType: "auth.unlock.recovery",
814
+ id: randomId(),
815
+ message: "Vault unlocked via recovery file",
816
+ metadata: JSON.stringify({}),
817
+ subjectRef: null,
818
+ subjectType: "vault"
819
+ });
820
+ return new VaultSession(db, rootKey);
821
+ } catch (error) {
822
+ db.close();
823
+ throw new Error("Invalid recovery token", { cause: error });
824
+ }
825
+ }
720
826
  try {
721
- const rootKey = unlockRootKey(password, config);
827
+ const passwordKEK = deriveKeyFromPassword(creds.password, config.kdf);
828
+ if (vaultAuth?.totp) {
829
+ const totpSecret = decryptWithKey(vaultAuth.totp.encryptedSecret, passwordKEK, "autho:vault-totp").toString("utf8");
830
+ if (!creds.totp || !verifyTotpCode(totpSecret, creds.totp)) {
831
+ throw new Error("Invalid or missing TOTP code");
832
+ }
833
+ }
834
+ const rootKey = decryptWithKey(config.wrappedRootKey, passwordKEK, "autho:vault-root");
722
835
  return new VaultSession(db, rootKey);
723
836
  } catch (error) {
724
837
  db.close();
838
+ if (error instanceof Error && error.message === "Invalid or missing TOTP code") {
839
+ throw error;
840
+ }
725
841
  throw new Error("Invalid vault password", { cause: error });
726
842
  }
727
843
  }
844
+ static getAuthConfig(vaultPath) {
845
+ const db = new AuthoDatabase(vaultPath);
846
+ try {
847
+ return db.getVaultAuth();
848
+ } finally {
849
+ db.close();
850
+ }
851
+ }
852
+ static setupTotp(vaultPath) {
853
+ const secret = generateTotpSecret();
854
+ const uri = totpUri(secret, "autho", vaultPath);
855
+ return { secret, uri };
856
+ }
857
+ static enableTotp(vaultPath, credentials, secret, code) {
858
+ if (!verifyTotpCode(secret, code)) {
859
+ throw new Error("Invalid TOTP code \u2014 make sure your authenticator app is showing the right code");
860
+ }
861
+ const session = VaultService.unlock(vaultPath, credentials);
862
+ session.close();
863
+ const db = new AuthoDatabase(vaultPath);
864
+ try {
865
+ const config = db.getVaultConfig();
866
+ const passwordKEK = deriveKeyFromPassword(credentials.password, config.kdf);
867
+ const encryptedSecret = encryptWithKey(Buffer.from(secret, "utf8"), passwordKEK, "autho:vault-totp");
868
+ const existing = db.getVaultAuth();
869
+ db.setVaultAuth({
870
+ version: 1,
871
+ ...existing,
872
+ totp: {
873
+ algorithm: "SHA1",
874
+ digits: 6,
875
+ encryptedSecret,
876
+ period: 30
877
+ }
878
+ });
879
+ db.insertAudit({
880
+ createdAt: new Date().toISOString(),
881
+ eventType: "auth.totp.enabled",
882
+ id: randomId(),
883
+ message: "TOTP vault unlock enabled",
884
+ metadata: JSON.stringify({}),
885
+ subjectRef: null,
886
+ subjectType: "vault"
887
+ });
888
+ } finally {
889
+ db.close();
890
+ }
891
+ }
892
+ static removeTotp(vaultPath, credentials) {
893
+ const session = VaultService.unlock(vaultPath, credentials);
894
+ session.close();
895
+ const db = new AuthoDatabase(vaultPath);
896
+ try {
897
+ const existing = db.getVaultAuth();
898
+ if (existing) {
899
+ const { totp: _removed, ...rest } = existing;
900
+ db.setVaultAuth({ ...rest, version: 1 });
901
+ }
902
+ db.insertAudit({
903
+ createdAt: new Date().toISOString(),
904
+ eventType: "auth.totp.removed",
905
+ id: randomId(),
906
+ message: "TOTP vault unlock removed",
907
+ metadata: JSON.stringify({}),
908
+ subjectRef: null,
909
+ subjectType: "vault"
910
+ });
911
+ } finally {
912
+ db.close();
913
+ }
914
+ }
915
+ static generateRecovery(vaultPath, credentials) {
916
+ const session = VaultService.unlock(vaultPath, credentials);
917
+ const rootKey = session.getRootKey();
918
+ session.close();
919
+ const db = new AuthoDatabase(vaultPath);
920
+ try {
921
+ const tokenBytes = randomBytes3(32);
922
+ const token = tokenBytes.toString("hex");
923
+ const recoverySalt = randomBytes3(16).toString("base64");
924
+ const recoveryKdf = {
925
+ keyLength: 32,
926
+ name: "scrypt",
927
+ salt: recoverySalt,
928
+ N: 1 << 17,
929
+ p: 1,
930
+ r: 8
931
+ };
932
+ const recoveryKEK = deriveKeyFromPassword(token, recoveryKdf);
933
+ const wrappedRootKey = encryptWithKey(rootKey, recoveryKEK, "autho:vault-recovery");
934
+ const existing = db.getVaultAuth();
935
+ db.setVaultAuth({
936
+ version: 1,
937
+ ...existing,
938
+ recovery: {
939
+ createdAt: new Date().toISOString(),
940
+ kdf: recoveryKdf,
941
+ wrappedRootKey
942
+ }
943
+ });
944
+ const formattedToken = token.toUpperCase().match(/.{1,8}/g).join("-");
945
+ const fileContent = [
946
+ "================================================================================",
947
+ "AUTHO VAULT RECOVERY FILE",
948
+ "================================================================================",
949
+ `Generated : ${new Date().toISOString()}`,
950
+ `Vault : ${vaultPath}`,
951
+ "",
952
+ "WARNING: Anyone with this file can open your vault regardless of password,",
953
+ "PIN, or authenticator app. Store it offline (printed paper, encrypted USB,",
954
+ "safety deposit box). Revoke with: autho recovery revoke",
955
+ "",
956
+ "RECOVERY TOKEN:",
957
+ formattedToken,
958
+ "",
959
+ "To use: autho unlock --recovery-file <path-to-this-file>",
960
+ "================================================================================"
961
+ ].join(`
962
+ `);
963
+ db.insertAudit({
964
+ createdAt: new Date().toISOString(),
965
+ eventType: "auth.recovery.generated",
966
+ id: randomId(),
967
+ message: "Recovery file generated",
968
+ metadata: JSON.stringify({}),
969
+ subjectRef: null,
970
+ subjectType: "vault"
971
+ });
972
+ return { fileContent };
973
+ } finally {
974
+ db.close();
975
+ }
976
+ }
977
+ static revokeRecovery(vaultPath, credentials) {
978
+ const session = VaultService.unlock(vaultPath, credentials);
979
+ session.close();
980
+ const db = new AuthoDatabase(vaultPath);
981
+ try {
982
+ const existing = db.getVaultAuth();
983
+ if (existing) {
984
+ const { recovery: _removed, ...rest } = existing;
985
+ db.setVaultAuth({ ...rest, version: 1 });
986
+ }
987
+ db.insertAudit({
988
+ createdAt: new Date().toISOString(),
989
+ eventType: "auth.recovery.revoked",
990
+ id: randomId(),
991
+ message: "Recovery file revoked",
992
+ metadata: JSON.stringify({}),
993
+ subjectRef: null,
994
+ subjectType: "vault"
995
+ });
996
+ } finally {
997
+ db.close();
998
+ }
999
+ }
728
1000
  }
729
1001
 
730
1002
  class VaultSession {
@@ -734,6 +1006,9 @@ class VaultSession {
734
1006
  this.db = db;
735
1007
  this.rootKey = rootKey;
736
1008
  }
1009
+ getRootKey() {
1010
+ return Buffer.from(this.rootKey);
1011
+ }
737
1012
  close() {
738
1013
  this.db.close();
739
1014
  }
@@ -1126,6 +1401,146 @@ var init_src3 = __esm(() => {
1126
1401
  init_paths();
1127
1402
  });
1128
1403
 
1404
+ // packages/core/src/os-secrets.ts
1405
+ import { createHash as createHash2, randomBytes as randomBytes4, scryptSync as scryptSync2, timingSafeEqual as timingSafeEqual2 } from "crypto";
1406
+ import { resolve as resolve3 } from "path";
1407
+ function vaultPasswordName(vaultPath) {
1408
+ return createHash2("sha256").update(resolve3(vaultPath)).digest("hex");
1409
+ }
1410
+ function osSecretsDisabled() {
1411
+ return process.env.AUTHO_DISABLE_OS_SECRETS === "1";
1412
+ }
1413
+ async function storeVaultPassword(vaultPath, password) {
1414
+ if (osSecretsDisabled()) {
1415
+ return false;
1416
+ }
1417
+ try {
1418
+ await Bun.secrets.set({
1419
+ name: vaultPasswordName(vaultPath),
1420
+ service: VAULT_PASSWORD_SERVICE,
1421
+ value: password
1422
+ });
1423
+ return true;
1424
+ } catch {
1425
+ return false;
1426
+ }
1427
+ }
1428
+ async function loadVaultPassword(vaultPath) {
1429
+ if (osSecretsDisabled()) {
1430
+ return null;
1431
+ }
1432
+ try {
1433
+ const password = await Bun.secrets.get({
1434
+ name: vaultPasswordName(vaultPath),
1435
+ service: VAULT_PASSWORD_SERVICE
1436
+ });
1437
+ return password ?? null;
1438
+ } catch {
1439
+ return null;
1440
+ }
1441
+ }
1442
+ async function deleteVaultPassword(vaultPath) {
1443
+ if (osSecretsDisabled()) {
1444
+ return false;
1445
+ }
1446
+ try {
1447
+ return await Bun.secrets.delete({
1448
+ name: vaultPasswordName(vaultPath),
1449
+ service: VAULT_PASSWORD_SERVICE
1450
+ });
1451
+ } catch {
1452
+ return false;
1453
+ }
1454
+ }
1455
+ async function setOsSecret(name, value) {
1456
+ if (osSecretsDisabled()) {
1457
+ return false;
1458
+ }
1459
+ try {
1460
+ await Bun.secrets.set({ name, service: USER_SECRETS_SERVICE, value });
1461
+ return true;
1462
+ } catch {
1463
+ return false;
1464
+ }
1465
+ }
1466
+ async function getOsSecret(name) {
1467
+ if (osSecretsDisabled()) {
1468
+ return null;
1469
+ }
1470
+ try {
1471
+ const value = await Bun.secrets.get({ name, service: USER_SECRETS_SERVICE });
1472
+ return value ?? null;
1473
+ } catch {
1474
+ return null;
1475
+ }
1476
+ }
1477
+ async function deleteOsSecret(name) {
1478
+ if (osSecretsDisabled()) {
1479
+ return false;
1480
+ }
1481
+ try {
1482
+ return await Bun.secrets.delete({ name, service: USER_SECRETS_SERVICE });
1483
+ } catch {
1484
+ return false;
1485
+ }
1486
+ }
1487
+ async function storePinHash(vaultPath, pin) {
1488
+ if (osSecretsDisabled()) {
1489
+ return false;
1490
+ }
1491
+ try {
1492
+ const salt = randomBytes4(16);
1493
+ const hash = scryptSync2(pin, salt, 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
1494
+ const value = `${salt.toString("base64")}:${hash.toString("base64")}`;
1495
+ await Bun.secrets.set({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE, value });
1496
+ return true;
1497
+ } catch {
1498
+ return false;
1499
+ }
1500
+ }
1501
+ async function verifyPin(vaultPath, pin) {
1502
+ if (osSecretsDisabled()) {
1503
+ return false;
1504
+ }
1505
+ try {
1506
+ const stored = await Bun.secrets.get({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE });
1507
+ if (!stored)
1508
+ return false;
1509
+ const colonIdx = stored.indexOf(":");
1510
+ if (colonIdx === -1)
1511
+ return false;
1512
+ const salt = Buffer.from(stored.slice(0, colonIdx), "base64");
1513
+ const expectedHash = Buffer.from(stored.slice(colonIdx + 1), "base64");
1514
+ const actualHash = scryptSync2(pin, salt, 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
1515
+ return timingSafeEqual2(actualHash, expectedHash);
1516
+ } catch {
1517
+ return false;
1518
+ }
1519
+ }
1520
+ async function hasPinSet(vaultPath) {
1521
+ if (osSecretsDisabled()) {
1522
+ return false;
1523
+ }
1524
+ try {
1525
+ const stored = await Bun.secrets.get({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE });
1526
+ return stored != null;
1527
+ } catch {
1528
+ return false;
1529
+ }
1530
+ }
1531
+ async function deletePin(vaultPath) {
1532
+ if (osSecretsDisabled()) {
1533
+ return false;
1534
+ }
1535
+ try {
1536
+ return await Bun.secrets.delete({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE });
1537
+ } catch {
1538
+ return false;
1539
+ }
1540
+ }
1541
+ var VAULT_PASSWORD_SERVICE = "autho.vault", USER_SECRETS_SERVICE = "autho", PIN_SERVICE = "autho.pin";
1542
+ var init_os_secrets = () => {};
1543
+
1129
1544
  // apps/cli/src/tui.tsx
1130
1545
  var exports_tui = {};
1131
1546
  __export(exports_tui, {
@@ -1240,28 +1655,33 @@ function useToast() {
1240
1655
  }, []);
1241
1656
  return { toast, show };
1242
1657
  }
1243
- function PasswordScreen({ onUnlock, vaultPath }) {
1658
+ function tryUnlockWithPassword(vaultPath, password) {
1659
+ try {
1660
+ return VaultService.unlock(vaultPath, { password });
1661
+ } catch {
1662
+ return null;
1663
+ }
1664
+ }
1665
+ function tryUnlockWithRecovery(vaultPath, token) {
1666
+ try {
1667
+ return VaultService.unlock(vaultPath, { password: "", recovery: token });
1668
+ } catch {
1669
+ return null;
1670
+ }
1671
+ }
1672
+ function PasswordMethod({ onSubmit, vaultPath: _vaultPath }) {
1244
1673
  const [password, setPassword] = useState("");
1245
- const [error, setError] = useState("");
1246
- const [unlocking, setUnlocking] = useState(false);
1674
+ const [submitting, setSubmitting] = useState(false);
1247
1675
  useKeyboard((key) => {
1248
1676
  if (key.eventType !== "press" && key.eventType !== "repeat")
1249
1677
  return;
1250
- if (unlocking)
1678
+ if (submitting)
1251
1679
  return;
1252
1680
  if (key.name === "enter" || key.name === "return") {
1253
1681
  if (!password)
1254
1682
  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
- }
1683
+ setSubmitting(true);
1684
+ onSubmit(password);
1265
1685
  return;
1266
1686
  }
1267
1687
  if (key.name === "backspace") {
@@ -1274,6 +1694,284 @@ function PasswordScreen({ onUnlock, vaultPath }) {
1274
1694
  if (char && char.length === 1 && char.charCodeAt(0) >= 32)
1275
1695
  setPassword((p) => p + char);
1276
1696
  });
1697
+ return /* @__PURE__ */ jsxDEV("box", {
1698
+ flexDirection: "row",
1699
+ gap: 1,
1700
+ marginTop: 1,
1701
+ children: [
1702
+ /* @__PURE__ */ jsxDEV("text", {
1703
+ fg: "#AAAAAA",
1704
+ children: "Password "
1705
+ }, undefined, false, undefined, this),
1706
+ /* @__PURE__ */ jsxDEV("box", {
1707
+ backgroundColor: "#111111",
1708
+ flexGrow: 1,
1709
+ paddingX: 1,
1710
+ height: 1,
1711
+ children: /* @__PURE__ */ jsxDEV("text", {
1712
+ fg: "#FFD700",
1713
+ children: password ? "*".repeat(password.length) : ""
1714
+ }, undefined, false, undefined, this)
1715
+ }, undefined, false, undefined, this),
1716
+ submitting ? /* @__PURE__ */ jsxDEV("text", {
1717
+ fg: "#FFD700",
1718
+ children: " ..."
1719
+ }, undefined, false, undefined, this) : null
1720
+ ]
1721
+ }, undefined, true, undefined, this);
1722
+ }
1723
+ function PinMethod({ vaultPath, password, onVerified, onError }) {
1724
+ const [pin, setPin] = useState("");
1725
+ const [verifying, setVerifying] = useState(false);
1726
+ useKeyboard((key) => {
1727
+ if (key.eventType !== "press" && key.eventType !== "repeat")
1728
+ return;
1729
+ if (verifying)
1730
+ return;
1731
+ if (key.name === "enter" || key.name === "return") {
1732
+ if (!pin)
1733
+ return;
1734
+ setVerifying(true);
1735
+ verifyPin(vaultPath, pin).then((ok) => {
1736
+ if (ok) {
1737
+ onVerified(password);
1738
+ } else {
1739
+ onError("Wrong PIN");
1740
+ setPin("");
1741
+ setVerifying(false);
1742
+ }
1743
+ });
1744
+ return;
1745
+ }
1746
+ if (key.name === "backspace") {
1747
+ setPin((p) => p.slice(0, -1));
1748
+ return;
1749
+ }
1750
+ 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))
1751
+ return;
1752
+ const char = key.sequence;
1753
+ if (char && char.length === 1 && char.charCodeAt(0) >= 32)
1754
+ setPin((p) => p + char);
1755
+ });
1756
+ return /* @__PURE__ */ jsxDEV("box", {
1757
+ flexDirection: "row",
1758
+ gap: 1,
1759
+ marginTop: 1,
1760
+ children: [
1761
+ /* @__PURE__ */ jsxDEV("text", {
1762
+ fg: "#AAAAAA",
1763
+ children: "PIN "
1764
+ }, undefined, false, undefined, this),
1765
+ /* @__PURE__ */ jsxDEV("box", {
1766
+ backgroundColor: "#111111",
1767
+ flexGrow: 1,
1768
+ paddingX: 1,
1769
+ height: 1,
1770
+ children: /* @__PURE__ */ jsxDEV("text", {
1771
+ fg: "#FFD700",
1772
+ children: pin ? "*".repeat(pin.length) : ""
1773
+ }, undefined, false, undefined, this)
1774
+ }, undefined, false, undefined, this),
1775
+ verifying ? /* @__PURE__ */ jsxDEV("text", {
1776
+ fg: "#FFD700",
1777
+ children: " ..."
1778
+ }, undefined, false, undefined, this) : null
1779
+ ]
1780
+ }, undefined, true, undefined, this);
1781
+ }
1782
+ function TotpMethod({ onSubmit, onError: _onError }) {
1783
+ const [code, setCode] = useState("");
1784
+ const [remaining, setRemaining] = useState(0);
1785
+ useEffect(() => {
1786
+ const update = () => {
1787
+ const secs = Math.ceil((30000 - Date.now() % 30000) / 1000);
1788
+ setRemaining(secs);
1789
+ };
1790
+ update();
1791
+ const interval = setInterval(update, 1000);
1792
+ return () => clearInterval(interval);
1793
+ }, []);
1794
+ useKeyboard((key) => {
1795
+ if (key.eventType !== "press" && key.eventType !== "repeat")
1796
+ return;
1797
+ if (key.name === "enter" || key.name === "return") {
1798
+ if (code.length !== 6)
1799
+ return;
1800
+ onSubmit(code);
1801
+ return;
1802
+ }
1803
+ if (key.name === "backspace") {
1804
+ setCode((c) => c.slice(0, -1));
1805
+ return;
1806
+ }
1807
+ 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))
1808
+ return;
1809
+ const char = key.sequence;
1810
+ if (char && /^\d$/.test(char) && code.length < 6) {
1811
+ const next = code + char;
1812
+ setCode(next);
1813
+ if (next.length === 6) {
1814
+ onSubmit(next);
1815
+ }
1816
+ }
1817
+ });
1818
+ return /* @__PURE__ */ jsxDEV("box", {
1819
+ flexDirection: "column",
1820
+ gap: 1,
1821
+ marginTop: 1,
1822
+ children: [
1823
+ /* @__PURE__ */ jsxDEV("box", {
1824
+ flexDirection: "row",
1825
+ gap: 1,
1826
+ children: [
1827
+ /* @__PURE__ */ jsxDEV("text", {
1828
+ fg: "#AAAAAA",
1829
+ children: "Auth code "
1830
+ }, undefined, false, undefined, this),
1831
+ /* @__PURE__ */ jsxDEV("box", {
1832
+ backgroundColor: "#111111",
1833
+ flexGrow: 1,
1834
+ paddingX: 1,
1835
+ height: 1,
1836
+ children: /* @__PURE__ */ jsxDEV("text", {
1837
+ fg: "#FFD700",
1838
+ children: code || "_".repeat(6)
1839
+ }, undefined, false, undefined, this)
1840
+ }, undefined, false, undefined, this),
1841
+ /* @__PURE__ */ jsxDEV("text", {
1842
+ fg: remaining <= 5 ? "#FF4444" : "#555555",
1843
+ children: [
1844
+ " ",
1845
+ remaining,
1846
+ "s"
1847
+ ]
1848
+ }, undefined, true, undefined, this)
1849
+ ]
1850
+ }, undefined, true, undefined, this),
1851
+ /* @__PURE__ */ jsxDEV("text", {
1852
+ fg: "#555555",
1853
+ children: "Enter 6-digit authenticator code"
1854
+ }, undefined, false, undefined, this)
1855
+ ]
1856
+ }, undefined, true, undefined, this);
1857
+ }
1858
+ function UnlockScreen({ onUnlock, vaultPath }) {
1859
+ const [step, setStep] = useState("loading");
1860
+ const [error, setError] = useState("");
1861
+ const [password, setPasswordState] = useState("");
1862
+ const [pinNeeded, setPinNeeded] = useState(false);
1863
+ const [totpNeeded, setTotpNeeded] = useState(false);
1864
+ useEffect(() => {
1865
+ let cancelled = false;
1866
+ (async () => {
1867
+ const recoveryFile = process.env.AUTHO_RECOVERY_FILE;
1868
+ if (recoveryFile) {
1869
+ try {
1870
+ const { readFileSync: readFileSync4 } = await import("fs");
1871
+ const content = readFileSync4(recoveryFile, "utf8");
1872
+ const lines = content.split(`
1873
+ `);
1874
+ const idx = lines.findIndex((l) => l.trim() === "RECOVERY TOKEN:");
1875
+ if (idx !== -1) {
1876
+ const rawToken = lines.slice(idx + 1).find((l) => l.trim() !== "") ?? "";
1877
+ const token = rawToken.replace(/-/g, "").toLowerCase();
1878
+ if (token) {
1879
+ const session = tryUnlockWithRecovery(vaultPath, token);
1880
+ if (!cancelled && session) {
1881
+ onUnlock(session);
1882
+ return;
1883
+ }
1884
+ if (!cancelled)
1885
+ setError("Recovery file failed \u2014 enter password instead");
1886
+ } else {
1887
+ if (!cancelled)
1888
+ setError("Recovery file is malformed \u2014 enter password instead");
1889
+ }
1890
+ } else {
1891
+ if (!cancelled)
1892
+ setError("Recovery file is malformed \u2014 enter password instead");
1893
+ }
1894
+ } catch {
1895
+ if (!cancelled)
1896
+ setError("Recovery file could not be read \u2014 enter password instead");
1897
+ }
1898
+ }
1899
+ const pinSet = await hasPinSet(vaultPath);
1900
+ if (!cancelled)
1901
+ setPinNeeded(pinSet);
1902
+ const authConfig = VaultService.getAuthConfig(vaultPath);
1903
+ if (!cancelled)
1904
+ setTotpNeeded(authConfig?.totp !== undefined);
1905
+ if (!pinSet) {
1906
+ const pw = await loadVaultPassword(vaultPath);
1907
+ if (!cancelled && pw && !authConfig?.totp) {
1908
+ const session = tryUnlockWithPassword(vaultPath, pw);
1909
+ if (!cancelled && session) {
1910
+ onUnlock(session);
1911
+ return;
1912
+ }
1913
+ }
1914
+ if (!cancelled && pw) {
1915
+ setPasswordState(pw);
1916
+ if (!cancelled)
1917
+ setStep(authConfig?.totp ? "totp" : "password");
1918
+ return;
1919
+ }
1920
+ }
1921
+ if (!cancelled)
1922
+ setStep("password");
1923
+ })();
1924
+ return () => {
1925
+ cancelled = true;
1926
+ };
1927
+ }, [vaultPath, onUnlock]);
1928
+ const doUnlock = useCallback((pw, totp) => {
1929
+ const creds = { password: pw, totp };
1930
+ try {
1931
+ const session = VaultService.unlock(vaultPath, creds);
1932
+ onUnlock(session);
1933
+ } catch (e) {
1934
+ setError(e instanceof Error ? e.message : String(e));
1935
+ setStep("password");
1936
+ setPasswordState("");
1937
+ }
1938
+ }, [vaultPath, onUnlock]);
1939
+ const handlePasswordSubmit = useCallback((pw) => {
1940
+ setPasswordState(pw);
1941
+ if (pinNeeded) {
1942
+ setStep("pin");
1943
+ } else if (totpNeeded) {
1944
+ setStep("totp");
1945
+ } else {
1946
+ setStep("unlocking");
1947
+ doUnlock(pw, undefined);
1948
+ }
1949
+ }, [pinNeeded, totpNeeded, doUnlock]);
1950
+ const handlePinVerified = useCallback((pw) => {
1951
+ if (totpNeeded) {
1952
+ setStep("totp");
1953
+ } else {
1954
+ setStep("unlocking");
1955
+ doUnlock(pw, undefined);
1956
+ }
1957
+ }, [totpNeeded, doUnlock]);
1958
+ const handleTotpSubmit = useCallback((totp) => {
1959
+ setStep("unlocking");
1960
+ doUnlock(password, totp);
1961
+ }, [password, doUnlock]);
1962
+ if (step === "loading" || step === "unlocking") {
1963
+ return /* @__PURE__ */ jsxDEV("box", {
1964
+ flexDirection: "column",
1965
+ alignItems: "center",
1966
+ justifyContent: "center",
1967
+ width: "100%",
1968
+ height: "100%",
1969
+ children: /* @__PURE__ */ jsxDEV("text", {
1970
+ fg: "#FFD700",
1971
+ children: step === "loading" ? "Unlocking..." : "Verifying..."
1972
+ }, undefined, false, undefined, this)
1973
+ }, undefined, false, undefined, this);
1974
+ }
1277
1975
  return /* @__PURE__ */ jsxDEV("box", {
1278
1976
  flexDirection: "column",
1279
1977
  alignItems: "center",
@@ -1306,31 +2004,21 @@ function PasswordScreen({ onUnlock, vaultPath }) {
1306
2004
  children: error
1307
2005
  }, undefined, false, undefined, this)
1308
2006
  }, 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", {
2007
+ step === "password" && /* @__PURE__ */ jsxDEV(PasswordMethod, {
2008
+ vaultPath,
2009
+ onSubmit: handlePasswordSubmit
2010
+ }, undefined, false, undefined, this),
2011
+ step === "pin" && /* @__PURE__ */ jsxDEV(PinMethod, {
2012
+ vaultPath,
2013
+ password,
2014
+ onVerified: handlePinVerified,
2015
+ onError: setError
2016
+ }, undefined, false, undefined, this),
2017
+ step === "totp" && /* @__PURE__ */ jsxDEV(TotpMethod, {
2018
+ onSubmit: handleTotpSubmit,
2019
+ onError: setError
2020
+ }, undefined, false, undefined, this),
2021
+ /* @__PURE__ */ jsxDEV("text", {
1334
2022
  fg: "#444444",
1335
2023
  children: "Enter to unlock | Ctrl+C to exit"
1336
2024
  }, undefined, false, undefined, this)
@@ -2322,7 +3010,7 @@ function EditScreen({ session, secret, onDone, onBack }) {
2322
3010
  }, undefined, true, undefined, this);
2323
3011
  }
2324
3012
  function App({ vaultPath }) {
2325
- const [screen, setScreen] = useState("password");
3013
+ const [screen, setScreen] = useState("unlock");
2326
3014
  const [session, setSession] = useState(null);
2327
3015
  const [selectedSecret, setSelectedSecret] = useState(null);
2328
3016
  const { toast, show: showToast } = useToast();
@@ -2336,8 +3024,8 @@ function App({ vaultPath }) {
2336
3024
  goHome();
2337
3025
  showToast(msg, msg.startsWith("Error:") ? "error" : "success");
2338
3026
  }, [goHome, showToast]);
2339
- if (screen === "password") {
2340
- return /* @__PURE__ */ jsxDEV(PasswordScreen, {
3027
+ if (screen === "unlock") {
3028
+ return /* @__PURE__ */ jsxDEV(UnlockScreen, {
2341
3029
  vaultPath,
2342
3030
  onUnlock: (s) => {
2343
3031
  setSession(s);
@@ -2423,6 +3111,7 @@ async function runTui(vaultPath) {
2423
3111
  var EDIT_FIELDS;
2424
3112
  var init_tui = __esm(() => {
2425
3113
  init_src3();
3114
+ init_os_secrets();
2426
3115
  EDIT_FIELDS = [
2427
3116
  { key: "name", label: "Name", forTypes: ["password", "note", "otp"] },
2428
3117
  { key: "value", label: "Value", forTypes: ["password", "note", "otp"] },
@@ -2434,11 +3123,30 @@ var init_tui = __esm(() => {
2434
3123
 
2435
3124
  // apps/cli/src/index.ts
2436
3125
  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";
3126
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
3127
+ import { createInterface as createInterface2 } from "readline/promises";
3128
+ import { resolve as resolve4 } from "path";
2440
3129
 
2441
3130
  // apps/cli/src/password.ts
3131
+ import { createInterface } from "readline/promises";
3132
+ async function readLine(prompt) {
3133
+ if (!process.stdin.isTTY) {
3134
+ throw new Error("Cannot read input: stdin is not a TTY");
3135
+ }
3136
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3137
+ try {
3138
+ return await rl.question(prompt);
3139
+ } finally {
3140
+ rl.close();
3141
+ }
3142
+ }
3143
+ async function confirm(question, defaultYes = true) {
3144
+ const hint = defaultYes ? "Y/n" : "y/N";
3145
+ const answer = (await readLine(`${question} (${hint}) `)).trim().toLowerCase();
3146
+ if (answer === "")
3147
+ return defaultYes;
3148
+ return answer === "y" || answer === "yes";
3149
+ }
2442
3150
  async function readPasswordMasked(prompt = "Master password: ") {
2443
3151
  if (!process.stdin.isTTY) {
2444
3152
  throw new Error("Cannot read password: stdin is not a TTY");
@@ -2480,9 +3188,8 @@ async function readPasswordMasked(prompt = "Master password: ") {
2480
3188
  }
2481
3189
  return;
2482
3190
  }
2483
- if (code === 27) {
3191
+ if (code === 27)
2484
3192
  return;
2485
- }
2486
3193
  if (code >= 32) {
2487
3194
  password += char;
2488
3195
  process.stdout.write("*");
@@ -2861,6 +3568,7 @@ async function daemonStop(options) {
2861
3568
 
2862
3569
  // apps/cli/src/index.ts
2863
3570
  init_src3();
3571
+ init_os_secrets();
2864
3572
  function parseArgs(argv) {
2865
3573
  const dashDashIndex = argv.indexOf("--");
2866
3574
  const main = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
@@ -2937,7 +3645,7 @@ function output(value, jsonMode = false) {
2937
3645
  console.log(value);
2938
3646
  }
2939
3647
  function absolutePath(path) {
2940
- return resolve3(path);
3648
+ return resolve4(path);
2941
3649
  }
2942
3650
  function buildSecretMetadata(args) {
2943
3651
  return Object.fromEntries(Object.entries({
@@ -2957,7 +3665,7 @@ async function readBufferedStdin() {
2957
3665
  }
2958
3666
  async function createPromptAdapter() {
2959
3667
  if (process.stdin.isTTY) {
2960
- const rl = createInterface({
3668
+ const rl = createInterface2({
2961
3669
  input: process.stdin,
2962
3670
  output: process.stdout
2963
3671
  });
@@ -3049,6 +3757,128 @@ async function runPromptMode(vaultPath, initialPassword) {
3049
3757
  prompt.close();
3050
3758
  }
3051
3759
  }
3760
+ async function resolveUnlockCredentials(vaultPath, args, existingPassword) {
3761
+ let password = existingPassword ?? getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
3762
+ if (!password) {
3763
+ password = await loadVaultPassword(vaultPath) ?? undefined;
3764
+ }
3765
+ if (!password && process.stdin.isTTY) {
3766
+ password = await readPasswordMasked("Master password: ");
3767
+ }
3768
+ const creds = { password: required(password, "--password") };
3769
+ if (await hasPinSet(vaultPath)) {
3770
+ const pin = process.env.AUTHO_PIN ?? getString(args, "pin") ?? (process.stdin.isTTY ? await readPasswordMasked("PIN: ") : undefined);
3771
+ if (!pin)
3772
+ throw new Error("PIN is set on this vault \u2014 provide it with AUTHO_PIN, --pin, or interactively");
3773
+ const ok = await verifyPin(vaultPath, pin);
3774
+ if (!ok)
3775
+ throw new Error("Wrong PIN");
3776
+ }
3777
+ const authConfig = VaultService.getAuthConfig(vaultPath);
3778
+ if (authConfig?.totp) {
3779
+ const totp = process.env.AUTHO_TOTP_CODE ?? getString(args, "totp") ?? (process.stdin.isTTY ? await readPasswordMasked("Authenticator code: ") : undefined);
3780
+ if (!totp)
3781
+ throw new Error("TOTP is enabled \u2014 provide a 6-digit code with AUTHO_TOTP_CODE, --totp, or interactively");
3782
+ creds.totp = totp;
3783
+ }
3784
+ return creds;
3785
+ }
3786
+ function osKeychainName() {
3787
+ const p = process.platform;
3788
+ if (p === "darwin")
3789
+ return "macOS Keychain";
3790
+ if (p === "win32")
3791
+ return "Windows Credential Manager";
3792
+ return "Secret Service (libsecret)";
3793
+ }
3794
+ async function runInitWizard(vaultPath, password, existingCreds) {
3795
+ const creds = existingCreds ?? { password };
3796
+ console.log("");
3797
+ if (!osSecretsDisabled()) {
3798
+ const passwordStored = await loadVaultPassword(vaultPath) !== null;
3799
+ if (passwordStored) {
3800
+ console.log(`\x1B[32m\u2713\x1B[0m Master password is saved in ${osKeychainName()}.`);
3801
+ if (await confirm(" Remove it?", false)) {
3802
+ const deleted = await deleteVaultPassword(vaultPath);
3803
+ console.log(deleted ? " Removed." : " Could not remove.");
3804
+ }
3805
+ } else {
3806
+ console.log(`\x1B[33m?\x1B[0m Save master password to ${osKeychainName()}?`);
3807
+ console.log(" This lets all vault commands unlock without prompting on this machine.");
3808
+ if (await confirm(" Save to keychain?")) {
3809
+ const stored = await storeVaultPassword(vaultPath, password);
3810
+ console.log(stored ? " \x1B[32m\u2713\x1B[0m Saved." : " Could not save \u2014 OS secret store may be unavailable.");
3811
+ } else {
3812
+ console.log(" Skipped.");
3813
+ }
3814
+ }
3815
+ console.log("");
3816
+ }
3817
+ if (!osSecretsDisabled()) {
3818
+ const pinSet = await hasPinSet(vaultPath);
3819
+ if (pinSet) {
3820
+ console.log("\x1B[32m\u2713\x1B[0m PIN is set on this machine.");
3821
+ if (await confirm(" Remove PIN?", false)) {
3822
+ const pin = await readPasswordMasked(" Enter current PIN: ");
3823
+ const ok = await verifyPin(vaultPath, pin);
3824
+ if (ok) {
3825
+ await deletePin(vaultPath);
3826
+ console.log(" Removed.");
3827
+ } else {
3828
+ console.log(" Wrong PIN, not removed.");
3829
+ }
3830
+ }
3831
+ } else {
3832
+ if (await confirm("Set up a PIN for quick unlock on this machine?", false)) {
3833
+ const newPin = await readPasswordMasked(" New PIN: ");
3834
+ if (!newPin) {
3835
+ console.log(" PIN cannot be empty, skipped.");
3836
+ } else {
3837
+ const confirmPin = await readPasswordMasked(" Confirm PIN: ");
3838
+ if (newPin !== confirmPin) {
3839
+ console.log(" PINs do not match, skipped.");
3840
+ } else {
3841
+ await storePinHash(vaultPath, newPin);
3842
+ console.log(" \x1B[32m\u2713\x1B[0m PIN set.");
3843
+ }
3844
+ }
3845
+ }
3846
+ }
3847
+ console.log("");
3848
+ }
3849
+ const authConfig = VaultService.getAuthConfig(vaultPath);
3850
+ const totpEnabled = authConfig?.totp !== undefined;
3851
+ if (totpEnabled) {
3852
+ console.log("\x1B[32m\u2713\x1B[0m TOTP is enabled (authenticator app required to unlock).");
3853
+ if (await confirm(" Disable TOTP?", false)) {
3854
+ const code = await readPasswordMasked(" Enter current TOTP code: ");
3855
+ try {
3856
+ VaultService.removeTotp(vaultPath, { ...creds, totp: code });
3857
+ console.log(" Disabled.");
3858
+ } catch (e) {
3859
+ console.log(` Error: ${e instanceof Error ? e.message : String(e)}`);
3860
+ }
3861
+ }
3862
+ } else {
3863
+ if (await confirm("Enable TOTP (authenticator app verification)?", false)) {
3864
+ const { secret, uri } = VaultService.setupTotp(vaultPath);
3865
+ console.log("");
3866
+ console.log(` Secret: ${secret}`);
3867
+ console.log(` URI: ${uri}`);
3868
+ console.log("");
3869
+ console.log(" Add this to your authenticator app, then enter the 6-digit code.");
3870
+ const code = await readPasswordMasked(" Code: ");
3871
+ try {
3872
+ VaultService.enableTotp(vaultPath, creds, secret, code);
3873
+ console.log(" \x1B[32m\u2713\x1B[0m TOTP enabled.");
3874
+ } catch (e) {
3875
+ console.log(` Error: ${e instanceof Error ? e.message : String(e)}`);
3876
+ }
3877
+ }
3878
+ }
3879
+ console.log("");
3880
+ console.log("Setup complete. Run \x1B[1mautho init\x1B[0m anytime to change these settings.");
3881
+ }
3052
3882
  async function runWebServer(vaultPath, args) {
3053
3883
  const commandArgs = [
3054
3884
  "run",
@@ -3118,15 +3948,32 @@ function help() {
3118
3948
  " files encrypt --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
3119
3949
  " files decrypt --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
3120
3950
  " audit list [--limit <number>] [--vault <path>] [--json]",
3951
+ " recovery generate --output <path> [--vault <path>] [--json]",
3952
+ " recovery revoke [--vault <path>] [--json]",
3953
+ " unlock --recovery-file <path> [--vault <path>] [--json]",
3954
+ " os-secrets set --name <name> [--value <value>] [--json]",
3955
+ " os-secrets get --name <name> [--json]",
3956
+ " os-secrets delete --name <name> [--json]",
3121
3957
  "",
3122
3958
  "Authentication:",
3123
3959
  " When running interactively (TTY), you will be securely prompted for your",
3124
3960
  " master password with masked input (no --password flag needed).",
3125
3961
  "",
3126
3962
  " For automation and coding agents, use one of:",
3127
- " AUTHO_MASTER_PASSWORD=<value> Environment variable (recommended for agents)",
3963
+ " autho init Setup wizard \u2014 choose to save password in OS keychain, set PIN, enable TOTP",
3964
+ " AUTHO_MASTER_PASSWORD=<value> Environment variable (master password)",
3965
+ " AUTHO_PIN=<value> Environment variable (PIN, if set on this machine)",
3966
+ " AUTHO_TOTP_CODE=<value> Environment variable (TOTP code, if enabled)",
3128
3967
  " --password <value> CLI flag (visible in shell history - avoid!)",
3129
3968
  "",
3969
+ " Native OS secret store support (via Bun.secrets):",
3970
+ " macOS \u2192 Keychain Services",
3971
+ " Linux \u2192 libsecret / GNOME Keyring / KWallet",
3972
+ " Windows \u2192 Windows Credential Manager",
3973
+ "",
3974
+ " The OS secret store is checked automatically before prompting.",
3975
+ " Set AUTHO_DISABLE_OS_SECRETS=1 to opt out.",
3976
+ "",
3130
3977
  "Notes:",
3131
3978
  " Running `autho` with no arguments opens the interactive TUI.",
3132
3979
  " The default vault path is ~/.autho/vault.db (override with AUTHO_HOME).",
@@ -3145,6 +3992,9 @@ async function main() {
3145
3992
  const fallbackProjectFile = defaultProjectFilePath();
3146
3993
  const projectFile = explicitProjectFile ?? (existsSync4(fallbackProjectFile) ? fallbackProjectFile : undefined);
3147
3994
  let password = getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
3995
+ if (!password) {
3996
+ password = await loadVaultPassword(vaultPath) ?? undefined;
3997
+ }
3148
3998
  if (!scope) {
3149
3999
  if (process.stdin.isTTY) {
3150
4000
  const { runTui: runTui2 } = await Promise.resolve().then(() => (init_tui(), exports_tui));
@@ -3164,7 +4014,6 @@ async function main() {
3164
4014
  }
3165
4015
  if (!password && process.stdin.isTTY) {
3166
4016
  const needsPassword = [
3167
- "init",
3168
4017
  "secrets",
3169
4018
  "otp",
3170
4019
  "lease",
@@ -3173,7 +4022,9 @@ async function main() {
3173
4022
  "file",
3174
4023
  "files",
3175
4024
  "audit",
3176
- "import"
4025
+ "import",
4026
+ "recovery",
4027
+ "unlock"
3177
4028
  ].includes(scope);
3178
4029
  const daemonNeedsPassword = scope === "daemon" && action === "unlock";
3179
4030
  if (needsPassword || daemonNeedsPassword) {
@@ -3181,7 +4032,21 @@ async function main() {
3181
4032
  }
3182
4033
  }
3183
4034
  if (scope === "init") {
3184
- output(VaultService.initialize(vaultPath, required(password, "--password")), jsonMode);
4035
+ const existingStatus = VaultService.status(vaultPath);
4036
+ if (!existingStatus.initialized) {
4037
+ const pw = required(password, "--password");
4038
+ output(VaultService.initialize(vaultPath, pw), jsonMode);
4039
+ if (process.stdin.isTTY && !jsonMode) {
4040
+ await runInitWizard(vaultPath, pw);
4041
+ }
4042
+ } else {
4043
+ const creds2 = await resolveUnlockCredentials(vaultPath, args, password);
4044
+ if (process.stdin.isTTY && !jsonMode) {
4045
+ await runInitWizard(vaultPath, creds2.password, creds2);
4046
+ } else {
4047
+ console.log("Vault already initialized. Use interactive mode (TTY) to reconfigure.");
4048
+ }
4049
+ }
3185
4050
  return;
3186
4051
  }
3187
4052
  if (scope === "status") {
@@ -3199,6 +4064,31 @@ async function main() {
3199
4064
  }), jsonMode);
3200
4065
  return;
3201
4066
  }
4067
+ if (scope === "os-secrets" && action === "set") {
4068
+ const name = required(getString(args, "name"), "--name");
4069
+ const value = getString(args, "value") ?? await readPasswordMasked(`Value for "${name}": `);
4070
+ const stored = await setOsSecret(name, value);
4071
+ if (!stored) {
4072
+ 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)");
4073
+ }
4074
+ output({ name, stored: true }, jsonMode);
4075
+ return;
4076
+ }
4077
+ if (scope === "os-secrets" && action === "get") {
4078
+ const name = required(getString(args, "name"), "--name");
4079
+ const value = await getOsSecret(name);
4080
+ if (value === null) {
4081
+ throw new Error(`Secret "${name}" not found in OS secret store`);
4082
+ }
4083
+ output({ name, value }, jsonMode);
4084
+ return;
4085
+ }
4086
+ if (scope === "os-secrets" && action === "delete") {
4087
+ const name = required(getString(args, "name"), "--name");
4088
+ const deleted = await deleteOsSecret(name);
4089
+ output({ deleted, name }, jsonMode);
4090
+ return;
4091
+ }
3202
4092
  if (scope === "web" && action === "serve") {
3203
4093
  await runWebServer(vaultPath, args);
3204
4094
  return;
@@ -3256,7 +4146,44 @@ async function main() {
3256
4146
  process.stderr.write(result.stderr);
3257
4147
  process.exit(result.exitCode);
3258
4148
  }
3259
- const session = VaultService.unlock(vaultPath, required(password, "--password"));
4149
+ if (scope === "recovery" && action === "generate") {
4150
+ const outputPath = absolutePath(required(getString(args, "output"), "--output"));
4151
+ const creds2 = await resolveUnlockCredentials(vaultPath, args, password);
4152
+ const { fileContent } = VaultService.generateRecovery(vaultPath, creds2);
4153
+ writeFileSync2(outputPath, fileContent, { encoding: "utf8", mode: 384 });
4154
+ if (!jsonMode) {
4155
+ console.log(`Recovery file written to ${outputPath}`);
4156
+ console.log("WARNING: Anyone with this file can open your vault. Store it offline.");
4157
+ } else {
4158
+ output({ outputPath, written: true }, jsonMode);
4159
+ }
4160
+ return;
4161
+ }
4162
+ if (scope === "recovery" && action === "revoke") {
4163
+ const creds2 = await resolveUnlockCredentials(vaultPath, args, password);
4164
+ VaultService.revokeRecovery(vaultPath, creds2);
4165
+ output({ revoked: true }, jsonMode);
4166
+ return;
4167
+ }
4168
+ if (scope === "unlock" && getString(args, "recovery-file")) {
4169
+ const recoveryFilePath = absolutePath(getString(args, "recovery-file"));
4170
+ const content = readFileSync4(recoveryFilePath, "utf8");
4171
+ const lines = content.split(`
4172
+ `);
4173
+ const tokenLineIdx = lines.findIndex((l) => l.trim() === "RECOVERY TOKEN:");
4174
+ if (tokenLineIdx === -1)
4175
+ throw new Error("Invalid recovery file format");
4176
+ const rawTokenLine = lines.slice(tokenLineIdx + 1).find((l) => l.trim() !== "") ?? "";
4177
+ const token = rawTokenLine.replace(/-/g, "").toLowerCase();
4178
+ if (!token)
4179
+ throw new Error("Invalid recovery file format: missing token");
4180
+ const recoverySession = VaultService.unlock(vaultPath, { password: "", recovery: token });
4181
+ output({ unlocked: true, vaultPath }, jsonMode);
4182
+ recoverySession.close();
4183
+ return;
4184
+ }
4185
+ const creds = await resolveUnlockCredentials(vaultPath, args, password);
4186
+ const session = VaultService.unlock(vaultPath, creds);
3260
4187
  try {
3261
4188
  if (scope === "import" && action === "legacy") {
3262
4189
  output(session.importLegacyFile(absolutePath(required(getString(args, "file"), "--file")), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autho",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Local-first secret manager for humans and coding agents, rebuilt on Bun",
5
5
  "type": "module",
6
6
  "bin": {