autho 2.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 +2566 -125
  3. package/package.json +8 -2
package/dist/autho.js CHANGED
@@ -1,15 +1,21 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
-
4
- // apps/cli/src/index.ts
5
- import { spawn } from "child_process";
6
- import { existsSync as existsSync4 } from "fs";
7
- import { createInterface } from "readline/promises";
8
- import { resolve as resolve3 } from "path";
9
-
10
- // packages/core/src/daemon.ts
11
- import { createHash, timingSafeEqual } from "crypto";
12
- import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
3
+ var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true,
13
+ configurable: true,
14
+ set: __exportSetter.bind(all, name)
15
+ });
16
+ };
17
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = import.meta.require;
13
19
 
14
20
  // packages/storage/src/index.ts
15
21
  import { Database } from "bun:sqlite";
@@ -100,6 +106,13 @@ class AuthoDatabase {
100
106
  setVaultConfig(config) {
101
107
  this.db.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)").run("vault.config", JSON.stringify(config));
102
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
+ }
103
116
  countSecrets() {
104
117
  const row = this.db.query("SELECT COUNT(*) AS count FROM secrets").get();
105
118
  return row.count;
@@ -144,6 +157,31 @@ class AuthoDatabase {
144
157
  LIMIT 1`).get(ref);
145
158
  return row ?? null;
146
159
  }
160
+ updateSecret(id, updates) {
161
+ const parts = [];
162
+ const values = [];
163
+ let idx = 1;
164
+ if (updates.name !== undefined) {
165
+ parts.push(`name = ?${idx}`);
166
+ values.push(updates.name);
167
+ idx++;
168
+ }
169
+ if (updates.type !== undefined) {
170
+ parts.push(`type = ?${idx}`);
171
+ values.push(updates.type);
172
+ idx++;
173
+ }
174
+ if (updates.payload !== undefined) {
175
+ parts.push(`payload = ?${idx}`);
176
+ values.push(updates.payload);
177
+ idx++;
178
+ }
179
+ parts.push(`updated_at = ?${idx}`);
180
+ values.push(updates.updatedAt);
181
+ idx++;
182
+ values.push(id);
183
+ this.db.query(`UPDATE secrets SET ${parts.join(", ")} WHERE id = ?${idx}`).run(...values);
184
+ }
147
185
  deleteSecret(id) {
148
186
  this.db.query("DELETE FROM secrets WHERE id = ?1").run(id);
149
187
  }
@@ -185,22 +223,16 @@ class AuthoDatabase {
185
223
  LIMIT ?1`).all(limit);
186
224
  }
187
225
  }
226
+ var init_src = () => {};
188
227
 
189
228
  // packages/crypto/src/index.ts
190
229
  import {
191
230
  createCipheriv,
192
231
  createDecipheriv,
232
+ createHmac,
193
233
  randomBytes,
194
234
  scryptSync
195
235
  } from "crypto";
196
- var DEFAULT_KDF = {
197
- keyLength: 32,
198
- name: "scrypt",
199
- salt: "",
200
- N: 1 << 17,
201
- p: 1,
202
- r: 8
203
- };
204
236
  function toBuffer(value) {
205
237
  return Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
206
238
  }
@@ -257,24 +289,89 @@ function unlockRootKey(password, config) {
257
289
  const key = deriveKeyFromPassword(password, config.kdf);
258
290
  return decryptWithKey(config.wrappedRootKey, key, "autho:vault-root");
259
291
  }
260
-
261
- // packages/core/src/index.ts
262
- import { spawnSync } from "child_process";
263
- import { createHmac, randomBytes as randomBytes3 } from "crypto";
264
- import {
265
- existsSync as existsSync2,
266
- readFileSync as readFileSync2
267
- } from "fs";
268
- import { basename as basename2 } from "path";
269
-
270
- // packages/core/src/artifacts.ts
271
- import { randomBytes as randomBytes2 } from "crypto";
272
- import {
273
- readdirSync,
274
- readFileSync,
275
- statSync
276
- } from "fs";
277
- import { basename, join as join2, relative, resolve as resolve2, sep } from "path";
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
+ }
364
+ var DEFAULT_KDF;
365
+ var init_src2 = __esm(() => {
366
+ DEFAULT_KDF = {
367
+ keyLength: 32,
368
+ name: "scrypt",
369
+ salt: "",
370
+ N: 1 << 17,
371
+ p: 1,
372
+ r: 8
373
+ };
374
+ });
278
375
 
279
376
  // packages/core/src/paths.ts
280
377
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, writeFileSync } from "fs";
@@ -323,8 +420,16 @@ function writeBinaryFileSecure(path, content) {
323
420
  writeFileSync(path, content, { mode: 384 });
324
421
  hardenFilePermissions(path);
325
422
  }
423
+ var init_paths = () => {};
326
424
 
327
425
  // packages/core/src/artifacts.ts
426
+ import { randomBytes as randomBytes2 } from "crypto";
427
+ import {
428
+ readdirSync,
429
+ readFileSync,
430
+ statSync
431
+ } from "fs";
432
+ import { basename, join as join2, relative, resolve as resolve2, sep } from "path";
328
433
  function normalizeRelativePath(input) {
329
434
  return input.replace(/\\/g, "/");
330
435
  }
@@ -434,15 +539,26 @@ function assertPathIsFile(path) {
434
539
  throw new Error(`Expected file: ${path}`);
435
540
  }
436
541
  }
542
+ var init_artifacts = __esm(() => {
543
+ init_src2();
544
+ init_paths();
545
+ });
437
546
 
438
547
  // packages/core/src/index.ts
548
+ import { spawnSync } from "child_process";
549
+ import { createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
550
+ import {
551
+ existsSync as existsSync2,
552
+ readFileSync as readFileSync2
553
+ } from "fs";
554
+ import { basename as basename2 } from "path";
439
555
  function requireValue(value, label) {
440
556
  if (!value) {
441
557
  throw new Error(`Missing required option: ${label}`);
442
558
  }
443
559
  return value;
444
560
  }
445
- function decodeBase32(input) {
561
+ function decodeBase322(input) {
446
562
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
447
563
  const normalized = input.toUpperCase().replace(/=+$/g, "").replace(/\s+/g, "");
448
564
  let bits = 0;
@@ -465,7 +581,7 @@ function decodeBase32(input) {
465
581
  function generateTotp(secret, options, now = Date.now()) {
466
582
  const algorithm = (options?.algorithm ?? "sha1").toLowerCase();
467
583
  const digits = options?.digits ?? 6;
468
- const key = decodeBase32(secret);
584
+ const key = decodeBase322(secret);
469
585
  const counter = Math.floor(now / 30000);
470
586
  const message = Buffer.alloc(8);
471
587
  let cursor = counter;
@@ -473,7 +589,7 @@ function generateTotp(secret, options, now = Date.now()) {
473
589
  message[index] = cursor & 255;
474
590
  cursor >>= 8;
475
591
  }
476
- const hash = createHmac(algorithm, Buffer.from(key)).update(message).digest();
592
+ const hash = createHmac2(algorithm, Buffer.from(key)).update(message).digest();
477
593
  const offset = hash[hash.length - 1] & 15;
478
594
  const binary = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
479
595
  const mod = 10 ** digits;
@@ -675,21 +791,212 @@ class VaultService {
675
791
  db.close();
676
792
  }
677
793
  }
678
- static unlock(vaultPath, password) {
794
+ static unlock(vaultPath, credentials) {
795
+ const creds = typeof credentials === "string" ? { password: credentials } : credentials;
679
796
  const db = new AuthoDatabase(vaultPath);
680
797
  const config = db.getVaultConfig();
681
798
  if (!config) {
682
799
  db.close();
683
800
  throw new Error(`Vault is not initialized at ${vaultPath}`);
684
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
+ }
685
826
  try {
686
- 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");
687
835
  return new VaultSession(db, rootKey);
688
836
  } catch (error) {
689
837
  db.close();
838
+ if (error instanceof Error && error.message === "Invalid or missing TOTP code") {
839
+ throw error;
840
+ }
690
841
  throw new Error("Invalid vault password", { cause: error });
691
842
  }
692
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
+ }
693
1000
  }
694
1001
 
695
1002
  class VaultSession {
@@ -699,6 +1006,9 @@ class VaultSession {
699
1006
  this.db = db;
700
1007
  this.rootKey = rootKey;
701
1008
  }
1009
+ getRootKey() {
1010
+ return Buffer.from(this.rootKey);
1011
+ }
702
1012
  close() {
703
1013
  this.db.close();
704
1014
  }
@@ -808,6 +1118,44 @@ class VaultSession {
808
1118
  updatedAt: now
809
1119
  };
810
1120
  }
1121
+ updateSecret(ref, updates) {
1122
+ const existing = this.getSecretOrThrow(ref);
1123
+ if (updates.name && updates.name !== existing.name) {
1124
+ const conflict = this.db.findSecret(updates.name);
1125
+ if (conflict && conflict.id !== existing.id) {
1126
+ throw new Error(`Secret already exists: ${updates.name}`);
1127
+ }
1128
+ }
1129
+ const newName = updates.name ?? existing.name;
1130
+ const newType = updates.type ? normalizeSecretType(updates.type) : existing.type;
1131
+ const newValue = updates.value ?? existing.value;
1132
+ const newUsername = updates.username !== undefined ? updates.username || null : existing.username;
1133
+ const newMetadata = updates.metadata ? normalizeMetadata({ ...existing.metadata, ...updates.metadata }) : existing.metadata;
1134
+ const now = new Date().toISOString();
1135
+ const wrappedKeyBlob = JSON.parse(this.db.findSecret(existing.id).wrappedKey);
1136
+ const dek = decryptWithKey(wrappedKeyBlob, this.rootKey, `autho:secret:${existing.id}:dek`);
1137
+ const payload = encryptWithKey(JSON.stringify({
1138
+ metadata: newMetadata,
1139
+ username: newUsername,
1140
+ value: newValue
1141
+ }), dek, `autho:secret:${existing.id}:payload`);
1142
+ this.db.updateSecret(existing.id, {
1143
+ name: newName,
1144
+ payload: JSON.stringify(payload),
1145
+ type: newType,
1146
+ updatedAt: now
1147
+ });
1148
+ this.audit("secret.updated", "secret", existing.id, "Secret updated", {
1149
+ fields: Object.keys(updates).filter((k) => updates[k] !== undefined)
1150
+ });
1151
+ return {
1152
+ createdAt: existing.createdAt,
1153
+ id: existing.id,
1154
+ name: newName,
1155
+ type: newType,
1156
+ updatedAt: now
1157
+ };
1158
+ }
811
1159
  importLegacyFile(filePath, options) {
812
1160
  const raw = JSON.parse(readFileSync2(filePath, "utf8"));
813
1161
  let imported = 0;
@@ -1045,67 +1393,1884 @@ class VaultSession {
1045
1393
  return this.db.listAudit(limit).map(toAuditEvent);
1046
1394
  }
1047
1395
  }
1396
+ var init_src3 = __esm(() => {
1397
+ init_src2();
1398
+ init_artifacts();
1399
+ init_src();
1400
+ init_paths();
1401
+ init_paths();
1402
+ });
1048
1403
 
1049
- // packages/core/src/daemon.ts
1050
- var DAEMON_TOKEN_SERVICE = "autho.daemon";
1051
- function daemonTokenName(statePath) {
1052
- return createHash("sha256").update(statePath).digest("hex");
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");
1053
1409
  }
1054
- async function storeDaemonToken(statePath, token) {
1055
- const tokenName = daemonTokenName(statePath);
1056
- if (process.env.AUTHO_DISABLE_OS_SECRETS !== "1") {
1057
- try {
1058
- await Bun.secrets.set({
1059
- name: tokenName,
1060
- service: DAEMON_TOKEN_SERVICE,
1061
- value: token
1062
- });
1063
- return {
1064
- token: null,
1065
- tokenName,
1066
- tokenStorage: "os"
1067
- };
1068
- } catch {}
1069
- }
1070
- return {
1071
- token,
1072
- tokenName: null,
1073
- tokenStorage: "file"
1074
- };
1410
+ function osSecretsDisabled() {
1411
+ return process.env.AUTHO_DISABLE_OS_SECRETS === "1";
1075
1412
  }
1076
- async function resolveDaemonToken(state) {
1077
- if (state.tokenStorage === "os") {
1078
- if (!state.tokenName) {
1079
- throw new Error("Daemon state is missing tokenName for OS secret storage");
1080
- }
1081
- const token = await Bun.secrets.get({
1082
- name: state.tokenName,
1083
- service: DAEMON_TOKEN_SERVICE
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
1084
1422
  });
1085
- if (!token) {
1086
- throw new Error("Daemon token not found in OS secret storage");
1087
- }
1088
- return token;
1423
+ return true;
1424
+ } catch {
1425
+ return false;
1089
1426
  }
1090
- if (!state.token) {
1091
- throw new Error("Daemon state is missing token");
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;
1092
1440
  }
1093
- return state.token;
1094
1441
  }
1095
- async function deleteStoredDaemonToken(state) {
1096
- if (!state || state.tokenStorage !== "os" || !state.tokenName) {
1097
- return;
1442
+ async function deleteVaultPassword(vaultPath) {
1443
+ if (osSecretsDisabled()) {
1444
+ return false;
1098
1445
  }
1099
1446
  try {
1100
- await Bun.secrets.delete({
1101
- name: state.tokenName,
1102
- service: DAEMON_TOKEN_SERVICE
1447
+ return await Bun.secrets.delete({
1448
+ name: vaultPasswordName(vaultPath),
1449
+ service: VAULT_PASSWORD_SERVICE
1103
1450
  });
1104
- } catch {}
1451
+ } catch {
1452
+ return false;
1453
+ }
1105
1454
  }
1106
- function readDaemonState(statePath) {
1107
- if (!existsSync3(statePath)) {
1108
- return null;
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
+
1544
+ // apps/cli/src/tui.tsx
1545
+ var exports_tui = {};
1546
+ __export(exports_tui, {
1547
+ runTui: () => runTui
1548
+ });
1549
+ import { createCliRenderer } from "@opentui/core";
1550
+ import { createRoot, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
1551
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1552
+ import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
1553
+ function typeLabel(type) {
1554
+ switch (type) {
1555
+ case "password":
1556
+ return "Login";
1557
+ case "note":
1558
+ return "Secure Note";
1559
+ case "otp":
1560
+ return "OTP Secret";
1561
+ default:
1562
+ return type;
1563
+ }
1564
+ }
1565
+ function Header({ title, subtitle }) {
1566
+ return /* @__PURE__ */ jsxDEV("box", {
1567
+ width: "100%",
1568
+ paddingLeft: 2,
1569
+ paddingTop: 1,
1570
+ paddingBottom: 1,
1571
+ borderBottom: true,
1572
+ borderColor: "#333333",
1573
+ children: [
1574
+ /* @__PURE__ */ jsxDEV("text", {
1575
+ fg: "#FFD700",
1576
+ children: /* @__PURE__ */ jsxDEV("strong", {
1577
+ children: title
1578
+ }, undefined, false, undefined, this)
1579
+ }, undefined, false, undefined, this),
1580
+ subtitle ? /* @__PURE__ */ jsxDEV("text", {
1581
+ fg: "#555555",
1582
+ children: [
1583
+ " ",
1584
+ subtitle
1585
+ ]
1586
+ }, undefined, true, undefined, this) : null
1587
+ ]
1588
+ }, undefined, true, undefined, this);
1589
+ }
1590
+ function StatusBar({ message }) {
1591
+ return /* @__PURE__ */ jsxDEV("box", {
1592
+ width: "100%",
1593
+ height: 1,
1594
+ backgroundColor: "#1a1a1a",
1595
+ paddingLeft: 1,
1596
+ paddingRight: 1,
1597
+ children: /* @__PURE__ */ jsxDEV("text", {
1598
+ fg: "#666666",
1599
+ children: message
1600
+ }, undefined, false, undefined, this)
1601
+ }, undefined, false, undefined, this);
1602
+ }
1603
+ function ToastBar({ toast }) {
1604
+ if (!toast)
1605
+ return null;
1606
+ const color = toast.type === "success" ? "#00CC66" : "#FF4444";
1607
+ const icon = toast.type === "success" ? "+" : "!";
1608
+ return /* @__PURE__ */ jsxDEV("box", {
1609
+ width: "100%",
1610
+ height: 1,
1611
+ backgroundColor: color,
1612
+ paddingLeft: 1,
1613
+ children: /* @__PURE__ */ jsxDEV("text", {
1614
+ fg: "#000000",
1615
+ children: /* @__PURE__ */ jsxDEV("strong", {
1616
+ children: [
1617
+ " ",
1618
+ icon,
1619
+ " ",
1620
+ toast.message
1621
+ ]
1622
+ }, undefined, true, undefined, this)
1623
+ }, undefined, false, undefined, this)
1624
+ }, undefined, false, undefined, this);
1625
+ }
1626
+ function KeyValue({ label, value, labelColor = "#888888", valueColor = "#FFFFFF" }) {
1627
+ return /* @__PURE__ */ jsxDEV("box", {
1628
+ flexDirection: "row",
1629
+ gap: 1,
1630
+ children: [
1631
+ /* @__PURE__ */ jsxDEV("text", {
1632
+ fg: labelColor,
1633
+ width: 14,
1634
+ children: label
1635
+ }, undefined, false, undefined, this),
1636
+ /* @__PURE__ */ jsxDEV("text", {
1637
+ fg: valueColor,
1638
+ children: value
1639
+ }, undefined, false, undefined, this)
1640
+ ]
1641
+ }, undefined, true, undefined, this);
1642
+ }
1643
+ function useToast() {
1644
+ const [toast, setToast] = useState(null);
1645
+ const timerRef = useRef(null);
1646
+ const show = useCallback((message, type = "success") => {
1647
+ if (timerRef.current)
1648
+ clearTimeout(timerRef.current);
1649
+ setToast({ message, type });
1650
+ timerRef.current = setTimeout(() => setToast(null), 3000);
1651
+ }, []);
1652
+ useEffect(() => () => {
1653
+ if (timerRef.current)
1654
+ clearTimeout(timerRef.current);
1655
+ }, []);
1656
+ return { toast, show };
1657
+ }
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 }) {
1673
+ const [password, setPassword] = useState("");
1674
+ const [submitting, setSubmitting] = useState(false);
1675
+ useKeyboard((key) => {
1676
+ if (key.eventType !== "press" && key.eventType !== "repeat")
1677
+ return;
1678
+ if (submitting)
1679
+ return;
1680
+ if (key.name === "enter" || key.name === "return") {
1681
+ if (!password)
1682
+ return;
1683
+ setSubmitting(true);
1684
+ onSubmit(password);
1685
+ return;
1686
+ }
1687
+ if (key.name === "backspace") {
1688
+ setPassword((p) => p.slice(0, -1));
1689
+ return;
1690
+ }
1691
+ 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))
1692
+ return;
1693
+ const char = key.sequence;
1694
+ if (char && char.length === 1 && char.charCodeAt(0) >= 32)
1695
+ setPassword((p) => p + char);
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
+ }
1975
+ return /* @__PURE__ */ jsxDEV("box", {
1976
+ flexDirection: "column",
1977
+ alignItems: "center",
1978
+ justifyContent: "center",
1979
+ width: "100%",
1980
+ height: "100%",
1981
+ children: /* @__PURE__ */ jsxDEV("box", {
1982
+ flexDirection: "column",
1983
+ border: true,
1984
+ borderStyle: "rounded",
1985
+ padding: 2,
1986
+ width: 52,
1987
+ gap: 1,
1988
+ children: [
1989
+ /* @__PURE__ */ jsxDEV("ascii-font", {
1990
+ text: "autho",
1991
+ font: "tiny",
1992
+ color: "#FFD700"
1993
+ }, undefined, false, undefined, this),
1994
+ /* @__PURE__ */ jsxDEV("text", {
1995
+ fg: "#888888",
1996
+ children: "Unlock your vault"
1997
+ }, undefined, false, undefined, this),
1998
+ error ? /* @__PURE__ */ jsxDEV("box", {
1999
+ backgroundColor: "#331111",
2000
+ paddingX: 1,
2001
+ width: "100%",
2002
+ children: /* @__PURE__ */ jsxDEV("text", {
2003
+ fg: "#FF4444",
2004
+ children: error
2005
+ }, undefined, false, undefined, this)
2006
+ }, undefined, false, undefined, this) : null,
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", {
2022
+ fg: "#444444",
2023
+ children: "Enter to unlock | Ctrl+C to exit"
2024
+ }, undefined, false, undefined, this)
2025
+ ]
2026
+ }, undefined, true, undefined, this)
2027
+ }, undefined, false, undefined, this);
2028
+ }
2029
+ function buildDescription(s, availableWidth) {
2030
+ const label = typeLabel(s.type);
2031
+ let desc = label;
2032
+ if (s.username) {
2033
+ const withUser = `${label} \xB7 ${s.username}`;
2034
+ if (withUser.length <= availableWidth) {
2035
+ desc = withUser;
2036
+ } else {
2037
+ return desc;
2038
+ }
2039
+ }
2040
+ const url = s.metadata?.url ? String(s.metadata.url) : "";
2041
+ if (url) {
2042
+ const shortUrl = url.replace(/^https?:\/\//, "");
2043
+ const withUrl = `${desc} \xB7 ${shortUrl}`;
2044
+ if (withUrl.length <= availableWidth) {
2045
+ desc = withUrl;
2046
+ } else {
2047
+ return desc;
2048
+ }
2049
+ }
2050
+ const note = s.metadata?.description ? String(s.metadata.description) : "";
2051
+ if (note) {
2052
+ const withNote = `${desc} \xB7 ${note}`;
2053
+ if (withNote.length <= availableWidth) {
2054
+ desc = withNote;
2055
+ } else {
2056
+ const budget = availableWidth - desc.length - 5;
2057
+ if (budget > 5) {
2058
+ desc = `${desc} \u2014 ${note.slice(0, budget)}\u2026`;
2059
+ }
2060
+ }
2061
+ }
2062
+ return desc;
2063
+ }
2064
+ function HomeScreen({ session, onSelect, onCreate, toast }) {
2065
+ const renderer = useRenderer();
2066
+ const { width: termWidth } = useTerminalDimensions();
2067
+ const [secrets, setSecrets] = useState([]);
2068
+ const [query, setQuery] = useState("");
2069
+ const [searchFocused, setSearchFocused] = useState(false);
2070
+ const reload = useCallback(() => {
2071
+ try {
2072
+ const summaries = session.listSecrets();
2073
+ const enriched = summaries.map((s) => {
2074
+ try {
2075
+ return session.getSecret(s.id);
2076
+ } catch {
2077
+ return s;
2078
+ }
2079
+ });
2080
+ setSecrets(enriched);
2081
+ } catch {
2082
+ setSecrets([]);
2083
+ }
2084
+ }, [session]);
2085
+ useEffect(reload, [reload]);
2086
+ const filtered = useMemo(() => {
2087
+ if (!query)
2088
+ return secrets;
2089
+ const q = query.toLowerCase();
2090
+ return secrets.filter((s) => s.name.toLowerCase().includes(q) || s.type.toLowerCase().includes(q) || s.username && s.username.toLowerCase().includes(q) || s.metadata?.url && String(s.metadata.url).toLowerCase().includes(q) || s.metadata?.description && String(s.metadata.description).toLowerCase().includes(q));
2091
+ }, [secrets, query]);
2092
+ const descWidth = Math.max(20, termWidth - 30);
2093
+ const options = useMemo(() => filtered.map((s) => ({
2094
+ name: s.name,
2095
+ description: buildDescription(s, descWidth),
2096
+ value: s.id
2097
+ })), [filtered, descWidth]);
2098
+ useKeyboard((key) => {
2099
+ if (key.eventType !== "press")
2100
+ return;
2101
+ if (key.name === "q" && !searchFocused) {
2102
+ session.close();
2103
+ renderer.destroy();
2104
+ return;
2105
+ }
2106
+ if (key.name === "n" && !searchFocused) {
2107
+ onCreate();
2108
+ return;
2109
+ }
2110
+ if (key.name === "/" && !searchFocused) {
2111
+ setSearchFocused(true);
2112
+ return;
2113
+ }
2114
+ if (key.name === "escape" && searchFocused) {
2115
+ setSearchFocused(false);
2116
+ setQuery("");
2117
+ return;
2118
+ }
2119
+ if (key.name === "escape" && !searchFocused) {
2120
+ session.close();
2121
+ renderer.destroy();
2122
+ return;
2123
+ }
2124
+ if (key.name === "tab") {
2125
+ setSearchFocused((f) => !f);
2126
+ return;
2127
+ }
2128
+ });
2129
+ return /* @__PURE__ */ jsxDEV("box", {
2130
+ flexDirection: "column",
2131
+ width: "100%",
2132
+ height: "100%",
2133
+ children: [
2134
+ /* @__PURE__ */ jsxDEV(Header, {
2135
+ title: "Autho",
2136
+ subtitle: `${secrets.length} secrets`
2137
+ }, undefined, false, undefined, this),
2138
+ /* @__PURE__ */ jsxDEV("box", {
2139
+ flexDirection: "row",
2140
+ paddingX: 2,
2141
+ paddingTop: 1,
2142
+ gap: 2,
2143
+ alignItems: "center",
2144
+ children: [
2145
+ /* @__PURE__ */ jsxDEV("input", {
2146
+ value: query,
2147
+ onChange: setQuery,
2148
+ placeholder: "/ search...",
2149
+ focused: searchFocused,
2150
+ flexGrow: 1,
2151
+ backgroundColor: "#111111",
2152
+ focusedBackgroundColor: "#1a1a1a",
2153
+ textColor: "#FFFFFF",
2154
+ placeholderColor: "#444444"
2155
+ }, undefined, false, undefined, this),
2156
+ /* @__PURE__ */ jsxDEV("text", {
2157
+ fg: "#555555",
2158
+ children: "[n] new"
2159
+ }, undefined, false, undefined, this)
2160
+ ]
2161
+ }, undefined, true, undefined, this),
2162
+ /* @__PURE__ */ jsxDEV("box", {
2163
+ flexGrow: 1,
2164
+ paddingX: 2,
2165
+ paddingTop: 1,
2166
+ children: filtered.length === 0 ? /* @__PURE__ */ jsxDEV("box", {
2167
+ padding: 1,
2168
+ children: /* @__PURE__ */ jsxDEV("text", {
2169
+ fg: "#555555",
2170
+ children: secrets.length === 0 ? "No secrets yet. Press [n] to create one." : "No matches."
2171
+ }, undefined, false, undefined, this)
2172
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV("select", {
2173
+ options,
2174
+ onSelect: (index) => onSelect(filtered[index]),
2175
+ focused: !searchFocused,
2176
+ height: Math.min(filtered.length * 2 + 1, 18),
2177
+ selectedBackgroundColor: "#222222",
2178
+ selectedTextColor: "#FFD700"
2179
+ }, undefined, false, undefined, this)
2180
+ }, undefined, false, undefined, this),
2181
+ /* @__PURE__ */ jsxDEV(ToastBar, {
2182
+ toast
2183
+ }, undefined, false, undefined, this),
2184
+ /* @__PURE__ */ jsxDEV(StatusBar, {
2185
+ message: "\u2191\u2193 navigate Enter open / search n new q quit"
2186
+ }, undefined, false, undefined, this)
2187
+ ]
2188
+ }, undefined, true, undefined, this);
2189
+ }
2190
+ function DetailScreen({ session, secret, onBack, onDeleted, onEdit }) {
2191
+ const renderer = useRenderer();
2192
+ const [revealing, setRevealing] = useState(false);
2193
+ const [copied, setCopied] = useState("");
2194
+ const [confirmDelete, setConfirmDelete] = useState(false);
2195
+ const [otpResult, setOtpResult] = useState(null);
2196
+ const [otpCountdown, setOtpCountdown] = useState(0);
2197
+ const [error, setError] = useState("");
2198
+ const hideTimerRef = useRef(null);
2199
+ const countdownRef = useRef(null);
2200
+ let detail;
2201
+ try {
2202
+ detail = session.getSecret(secret.name ?? secret.id);
2203
+ } catch {
2204
+ detail = secret;
2205
+ }
2206
+ const isOtp = detail.type === "otp";
2207
+ useEffect(() => () => {
2208
+ if (hideTimerRef.current)
2209
+ clearTimeout(hideTimerRef.current);
2210
+ if (countdownRef.current)
2211
+ clearInterval(countdownRef.current);
2212
+ }, []);
2213
+ useEffect(() => {
2214
+ if (!otpResult) {
2215
+ if (countdownRef.current)
2216
+ clearInterval(countdownRef.current);
2217
+ return;
2218
+ }
2219
+ const update = () => {
2220
+ const remaining = Math.max(0, Math.ceil((new Date(otpResult.expiresAt).getTime() - Date.now()) / 1000));
2221
+ setOtpCountdown(remaining);
2222
+ if (remaining <= 0 && countdownRef.current) {
2223
+ clearInterval(countdownRef.current);
2224
+ }
2225
+ };
2226
+ update();
2227
+ countdownRef.current = setInterval(update, 1000);
2228
+ return () => {
2229
+ if (countdownRef.current)
2230
+ clearInterval(countdownRef.current);
2231
+ };
2232
+ }, [otpResult]);
2233
+ useKeyboard((key) => {
2234
+ if (key.name === "s" && !confirmDelete && (key.eventType === "press" || key.eventType === "repeat")) {
2235
+ setRevealing(true);
2236
+ if (hideTimerRef.current)
2237
+ clearTimeout(hideTimerRef.current);
2238
+ hideTimerRef.current = setTimeout(() => setRevealing(false), 600);
2239
+ return;
2240
+ }
2241
+ if (key.eventType !== "press")
2242
+ return;
2243
+ if (key.name === "escape") {
2244
+ if (confirmDelete) {
2245
+ setConfirmDelete(false);
2246
+ return;
2247
+ }
2248
+ if (otpResult) {
2249
+ setOtpResult(null);
2250
+ return;
2251
+ }
2252
+ onBack();
2253
+ return;
2254
+ }
2255
+ if (key.name === "c" && !confirmDelete) {
2256
+ const toCopy = otpResult ? otpResult.code : detail.value ?? "";
2257
+ if (toCopy) {
2258
+ renderer.copyToClipboardOSC52(toCopy);
2259
+ setCopied(otpResult ? "OTP code" : "Value");
2260
+ setTimeout(() => setCopied(""), 2000);
2261
+ }
2262
+ return;
2263
+ }
2264
+ if (key.name === "e" && !confirmDelete && !otpResult) {
2265
+ onEdit();
2266
+ return;
2267
+ }
2268
+ if (key.name === "d" && !confirmDelete && !otpResult) {
2269
+ setConfirmDelete(true);
2270
+ return;
2271
+ }
2272
+ if (key.name === "o" && isOtp && !confirmDelete) {
2273
+ try {
2274
+ const result = session.generateOtp(detail.name);
2275
+ setOtpResult(result);
2276
+ setError("");
2277
+ } catch (e) {
2278
+ setError(e instanceof Error ? e.message : String(e));
2279
+ }
2280
+ return;
2281
+ }
2282
+ if (key.name === "r" && otpResult) {
2283
+ try {
2284
+ const result = session.generateOtp(detail.name);
2285
+ setOtpResult(result);
2286
+ setError("");
2287
+ } catch (e) {
2288
+ setError(e instanceof Error ? e.message : String(e));
2289
+ }
2290
+ return;
2291
+ }
2292
+ });
2293
+ if (confirmDelete) {
2294
+ return /* @__PURE__ */ jsxDEV("box", {
2295
+ flexDirection: "column",
2296
+ width: "100%",
2297
+ height: "100%",
2298
+ children: [
2299
+ /* @__PURE__ */ jsxDEV(Header, {
2300
+ title: "Delete",
2301
+ subtitle: detail.name
2302
+ }, undefined, false, undefined, this),
2303
+ /* @__PURE__ */ jsxDEV("box", {
2304
+ padding: 2,
2305
+ flexDirection: "column",
2306
+ gap: 1,
2307
+ children: [
2308
+ /* @__PURE__ */ jsxDEV("box", {
2309
+ border: true,
2310
+ borderStyle: "rounded",
2311
+ borderColor: "#FF4444",
2312
+ padding: 1,
2313
+ flexDirection: "column",
2314
+ gap: 0,
2315
+ children: [
2316
+ /* @__PURE__ */ jsxDEV("text", {
2317
+ fg: "#FF4444",
2318
+ children: /* @__PURE__ */ jsxDEV("strong", {
2319
+ children: "Permanently delete this secret?"
2320
+ }, undefined, false, undefined, this)
2321
+ }, undefined, false, undefined, this),
2322
+ /* @__PURE__ */ jsxDEV(KeyValue, {
2323
+ label: "Name",
2324
+ value: detail.name,
2325
+ valueColor: "#FFD700"
2326
+ }, undefined, false, undefined, this),
2327
+ /* @__PURE__ */ jsxDEV(KeyValue, {
2328
+ label: "Type",
2329
+ value: detail.type
2330
+ }, undefined, false, undefined, this)
2331
+ ]
2332
+ }, undefined, true, undefined, this),
2333
+ /* @__PURE__ */ jsxDEV("select", {
2334
+ options: [
2335
+ { name: "Cancel", description: "Keep the secret", value: "cancel" },
2336
+ { name: "Delete forever", description: "Cannot be undone", value: "delete" }
2337
+ ],
2338
+ onSelect: (_i, opt) => {
2339
+ if (opt.value === "delete") {
2340
+ try {
2341
+ session.removeSecret(detail.name ?? detail.id);
2342
+ onDeleted(`"${detail.name}" deleted`);
2343
+ } catch (e) {
2344
+ onDeleted(`Error: ${e instanceof Error ? e.message : String(e)}`);
2345
+ }
2346
+ } else {
2347
+ setConfirmDelete(false);
2348
+ }
2349
+ },
2350
+ focused: true,
2351
+ height: 4,
2352
+ selectedBackgroundColor: "#222222",
2353
+ selectedTextColor: "#FF4444"
2354
+ }, undefined, false, undefined, this)
2355
+ ]
2356
+ }, undefined, true, undefined, this),
2357
+ /* @__PURE__ */ jsxDEV(StatusBar, {
2358
+ message: "\u2191\u2193 navigate Enter confirm Esc cancel"
2359
+ }, undefined, false, undefined, this)
2360
+ ]
2361
+ }, undefined, true, undefined, this);
2362
+ }
2363
+ let maskedValue = "\u2014";
2364
+ if (detail.value) {
2365
+ maskedValue = revealing ? detail.value : "*".repeat(Math.min(detail.value.length, 32));
2366
+ }
2367
+ const hints = ["Hold s reveal", "c copy", "e edit", "d delete"];
2368
+ if (isOtp)
2369
+ hints.push(otpResult ? "r regen" : "o OTP");
2370
+ hints.push("Esc back");
2371
+ return /* @__PURE__ */ jsxDEV("box", {
2372
+ flexDirection: "column",
2373
+ width: "100%",
2374
+ height: "100%",
2375
+ children: [
2376
+ /* @__PURE__ */ jsxDEV(Header, {
2377
+ title: detail.name,
2378
+ subtitle: typeLabel(detail.type)
2379
+ }, undefined, false, undefined, this),
2380
+ /* @__PURE__ */ jsxDEV("box", {
2381
+ flexDirection: "column",
2382
+ flexGrow: 1,
2383
+ padding: 2,
2384
+ gap: 1,
2385
+ children: [
2386
+ /* @__PURE__ */ jsxDEV("box", {
2387
+ flexDirection: "column",
2388
+ border: true,
2389
+ borderStyle: "rounded",
2390
+ borderColor: "#333333",
2391
+ padding: 1,
2392
+ gap: 0,
2393
+ width: "100%",
2394
+ children: [
2395
+ /* @__PURE__ */ jsxDEV(KeyValue, {
2396
+ label: "Name",
2397
+ value: detail.name,
2398
+ valueColor: "#FFD700"
2399
+ }, undefined, false, undefined, this),
2400
+ /* @__PURE__ */ jsxDEV(KeyValue, {
2401
+ label: "Type",
2402
+ value: typeLabel(detail.type)
2403
+ }, undefined, false, undefined, this),
2404
+ /* @__PURE__ */ jsxDEV(KeyValue, {
2405
+ label: "ID",
2406
+ value: detail.id,
2407
+ valueColor: "#555555"
2408
+ }, undefined, false, undefined, this),
2409
+ detail.username ? /* @__PURE__ */ jsxDEV(KeyValue, {
2410
+ label: "Username",
2411
+ value: detail.username
2412
+ }, undefined, false, undefined, this) : null,
2413
+ /* @__PURE__ */ jsxDEV(KeyValue, {
2414
+ label: "Value",
2415
+ value: maskedValue,
2416
+ valueColor: revealing ? "#00FF00" : "#FF4444"
2417
+ }, undefined, false, undefined, this),
2418
+ detail.metadata?.url ? /* @__PURE__ */ jsxDEV(KeyValue, {
2419
+ label: "URL",
2420
+ value: String(detail.metadata.url)
2421
+ }, undefined, false, undefined, this) : null,
2422
+ detail.metadata?.description ? /* @__PURE__ */ jsxDEV(KeyValue, {
2423
+ label: "Description",
2424
+ value: String(detail.metadata.description)
2425
+ }, undefined, false, undefined, this) : null
2426
+ ]
2427
+ }, undefined, true, undefined, this),
2428
+ /* @__PURE__ */ jsxDEV("text", {
2429
+ fg: revealing ? "#00FF00" : "#666666",
2430
+ children: revealing ? "Revealing \u2014 release [s] to hide" : "Hold [s] to reveal secret value"
2431
+ }, undefined, false, undefined, this),
2432
+ otpResult ? /* @__PURE__ */ jsxDEV("box", {
2433
+ border: true,
2434
+ borderStyle: "rounded",
2435
+ borderColor: "#00CC66",
2436
+ paddingX: 2,
2437
+ height: 3,
2438
+ flexDirection: "row",
2439
+ alignItems: "center",
2440
+ gap: 2,
2441
+ children: [
2442
+ /* @__PURE__ */ jsxDEV("text", {
2443
+ fg: "#888888",
2444
+ children: "OTP"
2445
+ }, undefined, false, undefined, this),
2446
+ /* @__PURE__ */ jsxDEV("text", {
2447
+ fg: "#00FF00",
2448
+ children: /* @__PURE__ */ jsxDEV("strong", {
2449
+ children: otpResult.code
2450
+ }, undefined, false, undefined, this)
2451
+ }, undefined, false, undefined, this),
2452
+ /* @__PURE__ */ jsxDEV("text", {
2453
+ fg: otpCountdown <= 5 ? "#FF4444" : "#888888",
2454
+ children: otpCountdown > 0 ? `${otpCountdown}s` : "expired"
2455
+ }, undefined, false, undefined, this),
2456
+ /* @__PURE__ */ jsxDEV("text", {
2457
+ fg: "#555555",
2458
+ children: "[r] refresh"
2459
+ }, undefined, false, undefined, this)
2460
+ ]
2461
+ }, undefined, true, undefined, this) : null,
2462
+ error ? /* @__PURE__ */ jsxDEV("text", {
2463
+ fg: "#FF4444",
2464
+ children: error
2465
+ }, undefined, false, undefined, this) : null,
2466
+ copied ? /* @__PURE__ */ jsxDEV("text", {
2467
+ fg: "#00CC66",
2468
+ children: [
2469
+ copied,
2470
+ " copied to clipboard"
2471
+ ]
2472
+ }, undefined, true, undefined, this) : null,
2473
+ /* @__PURE__ */ jsxDEV("box", {
2474
+ flexDirection: "row",
2475
+ gap: 3,
2476
+ marginTop: 1,
2477
+ children: [
2478
+ /* @__PURE__ */ jsxDEV("text", {
2479
+ fg: "#00CC66",
2480
+ children: "[c] Copy"
2481
+ }, undefined, false, undefined, this),
2482
+ /* @__PURE__ */ jsxDEV("text", {
2483
+ fg: "#FFD700",
2484
+ children: "[e] Edit"
2485
+ }, undefined, false, undefined, this),
2486
+ /* @__PURE__ */ jsxDEV("text", {
2487
+ fg: "#FF4444",
2488
+ children: "[d] Delete"
2489
+ }, undefined, false, undefined, this),
2490
+ isOtp ? /* @__PURE__ */ jsxDEV("text", {
2491
+ fg: "#00CCFF",
2492
+ children: "[o] Generate OTP"
2493
+ }, undefined, false, undefined, this) : null
2494
+ ]
2495
+ }, undefined, true, undefined, this)
2496
+ ]
2497
+ }, undefined, true, undefined, this),
2498
+ /* @__PURE__ */ jsxDEV(StatusBar, {
2499
+ message: hints.join(" ")
2500
+ }, undefined, false, undefined, this)
2501
+ ]
2502
+ }, undefined, true, undefined, this);
2503
+ }
2504
+ function CreateScreen({ session, onDone, onBack }) {
2505
+ const [step, setStep] = useState(0);
2506
+ const [secretType, setSecretType] = useState("password");
2507
+ const [name, setName] = useState("");
2508
+ const [value, setValue] = useState("");
2509
+ const [username, setUsername] = useState("");
2510
+ const [url, setUrl] = useState("");
2511
+ const [description, setDescription] = useState("");
2512
+ const typeOptions = [
2513
+ { name: "Login", description: "Username, password, and URL", value: "password" },
2514
+ { name: "Secure Note", description: "Encrypted text note", value: "note" },
2515
+ { name: "OTP Secret", description: "TOTP key for 2FA codes", value: "otp" }
2516
+ ];
2517
+ const steps = (() => {
2518
+ switch (secretType) {
2519
+ case "password":
2520
+ return ["type", "name", "value", "username", "url", "description"];
2521
+ case "note":
2522
+ return ["type", "name", "value", "description"];
2523
+ case "otp":
2524
+ return ["type", "name", "value", "username", "description"];
2525
+ default:
2526
+ return ["type", "name", "value", "description"];
2527
+ }
2528
+ })();
2529
+ const currentField = steps[step] ?? "description";
2530
+ const totalSteps = steps.length;
2531
+ const label = typeLabel(secretType);
2532
+ const doCreate = useCallback(() => {
2533
+ try {
2534
+ const metadata = Object.fromEntries(Object.entries({
2535
+ description: description || undefined,
2536
+ url: url || undefined
2537
+ }).filter(([, v]) => v !== undefined));
2538
+ session.addSecret({
2539
+ metadata,
2540
+ name,
2541
+ type: secretType,
2542
+ username: username || undefined,
2543
+ value
2544
+ });
2545
+ onDone(`"${name}" created`);
2546
+ } catch (e) {
2547
+ onDone(`Error: ${e instanceof Error ? e.message : String(e)}`);
2548
+ }
2549
+ }, [session, name, value, secretType, username, url, description, onDone]);
2550
+ useKeyboard((key) => {
2551
+ if (key.eventType !== "press")
2552
+ return;
2553
+ if (key.name === "escape") {
2554
+ if (step > 0)
2555
+ setStep((s) => s - 1);
2556
+ else
2557
+ onBack();
2558
+ }
2559
+ });
2560
+ const progress = `${step + 1}/${totalSteps}`;
2561
+ const nextStep = () => setStep((s) => s + 1);
2562
+ const inputRow = (label2, val, onChange, onSubmit, placeholder) => /* @__PURE__ */ jsxDEV("box", {
2563
+ flexDirection: "row",
2564
+ gap: 1,
2565
+ children: [
2566
+ /* @__PURE__ */ jsxDEV("text", {
2567
+ fg: "#AAAAAA",
2568
+ width: 12,
2569
+ children: label2
2570
+ }, undefined, false, undefined, this),
2571
+ /* @__PURE__ */ jsxDEV("input", {
2572
+ value: val,
2573
+ onChange,
2574
+ onSubmit,
2575
+ placeholder,
2576
+ focused: true,
2577
+ flexGrow: 1,
2578
+ backgroundColor: "#111111",
2579
+ focusedBackgroundColor: "#1a1a1a",
2580
+ textColor: "#FFFFFF"
2581
+ }, undefined, false, undefined, this)
2582
+ ]
2583
+ }, undefined, true, undefined, this);
2584
+ if (step === 0) {
2585
+ return /* @__PURE__ */ jsxDEV("box", {
2586
+ flexDirection: "column",
2587
+ width: "100%",
2588
+ height: "100%",
2589
+ children: [
2590
+ /* @__PURE__ */ jsxDEV(Header, {
2591
+ title: "New Secret",
2592
+ subtitle: progress
2593
+ }, undefined, false, undefined, this),
2594
+ /* @__PURE__ */ jsxDEV("box", {
2595
+ paddingLeft: 2,
2596
+ paddingTop: 1,
2597
+ children: /* @__PURE__ */ jsxDEV("text", {
2598
+ fg: "#888888",
2599
+ children: "What type of secret?"
2600
+ }, undefined, false, undefined, this)
2601
+ }, undefined, false, undefined, this),
2602
+ /* @__PURE__ */ jsxDEV("box", {
2603
+ flexGrow: 1,
2604
+ paddingLeft: 2,
2605
+ paddingTop: 1,
2606
+ children: /* @__PURE__ */ jsxDEV("select", {
2607
+ options: typeOptions,
2608
+ onSelect: (_i, opt) => {
2609
+ setSecretType(opt.value ?? "password");
2610
+ nextStep();
2611
+ },
2612
+ focused: true,
2613
+ height: 6,
2614
+ selectedBackgroundColor: "#222222",
2615
+ selectedTextColor: "#FFD700"
2616
+ }, undefined, false, undefined, this)
2617
+ }, undefined, false, undefined, this),
2618
+ /* @__PURE__ */ jsxDEV(StatusBar, {
2619
+ message: "Enter select Esc cancel"
2620
+ }, undefined, false, undefined, this)
2621
+ ]
2622
+ }, undefined, true, undefined, this);
2623
+ }
2624
+ if (currentField === "name") {
2625
+ return /* @__PURE__ */ jsxDEV("box", {
2626
+ flexDirection: "column",
2627
+ width: "100%",
2628
+ height: "100%",
2629
+ children: [
2630
+ /* @__PURE__ */ jsxDEV(Header, {
2631
+ title: "New Secret",
2632
+ subtitle: `${progress} \xB7 ${label}`
2633
+ }, undefined, false, undefined, this),
2634
+ /* @__PURE__ */ jsxDEV("box", {
2635
+ padding: 2,
2636
+ flexDirection: "column",
2637
+ gap: 1,
2638
+ children: [
2639
+ /* @__PURE__ */ jsxDEV("text", {
2640
+ fg: "#888888",
2641
+ children: "Give it a name"
2642
+ }, undefined, false, undefined, this),
2643
+ inputRow("Name", name, setName, () => {
2644
+ if (name.trim())
2645
+ nextStep();
2646
+ }, "e.g. github-login")
2647
+ ]
2648
+ }, undefined, true, undefined, this),
2649
+ /* @__PURE__ */ jsxDEV(StatusBar, {
2650
+ message: "Enter next Esc back"
2651
+ }, undefined, false, undefined, this)
2652
+ ]
2653
+ }, undefined, true, undefined, this);
2654
+ }
2655
+ if (currentField === "value") {
2656
+ const hint = secretType === "password" ? "Enter the password" : secretType === "otp" ? "Enter the TOTP base32 secret key" : "Enter the note content";
2657
+ const placeholder = secretType === "password" ? "password..." : secretType === "otp" ? "e.g. JBSWY3DPEHPK3PXP" : "note content...";
2658
+ return /* @__PURE__ */ jsxDEV("box", {
2659
+ flexDirection: "column",
2660
+ width: "100%",
2661
+ height: "100%",
2662
+ children: [
2663
+ /* @__PURE__ */ jsxDEV(Header, {
2664
+ title: "New Secret",
2665
+ subtitle: `${progress} \xB7 ${name}`
2666
+ }, undefined, false, undefined, this),
2667
+ /* @__PURE__ */ jsxDEV("box", {
2668
+ padding: 2,
2669
+ flexDirection: "column",
2670
+ gap: 1,
2671
+ children: [
2672
+ /* @__PURE__ */ jsxDEV("text", {
2673
+ fg: "#888888",
2674
+ children: hint
2675
+ }, undefined, false, undefined, this),
2676
+ inputRow(secretType === "note" ? "Note" : "Value", value, setValue, () => {
2677
+ if (value.trim())
2678
+ nextStep();
2679
+ }, placeholder)
2680
+ ]
2681
+ }, undefined, true, undefined, this),
2682
+ /* @__PURE__ */ jsxDEV(StatusBar, {
2683
+ message: "Enter next Esc back"
2684
+ }, undefined, false, undefined, this)
2685
+ ]
2686
+ }, undefined, true, undefined, this);
2687
+ }
2688
+ if (currentField === "username") {
2689
+ return /* @__PURE__ */ jsxDEV("box", {
2690
+ flexDirection: "column",
2691
+ width: "100%",
2692
+ height: "100%",
2693
+ children: [
2694
+ /* @__PURE__ */ jsxDEV(Header, {
2695
+ title: "New Secret",
2696
+ subtitle: `${progress} \xB7 optional`
2697
+ }, undefined, false, undefined, this),
2698
+ /* @__PURE__ */ jsxDEV("box", {
2699
+ padding: 2,
2700
+ flexDirection: "column",
2701
+ gap: 1,
2702
+ children: [
2703
+ /* @__PURE__ */ jsxDEV("text", {
2704
+ fg: "#888888",
2705
+ children: "Username or email (optional)"
2706
+ }, undefined, false, undefined, this),
2707
+ inputRow("Username", username, setUsername, nextStep, "skip with Enter")
2708
+ ]
2709
+ }, undefined, true, undefined, this),
2710
+ /* @__PURE__ */ jsxDEV(StatusBar, {
2711
+ message: "Enter next (empty = skip) Esc back"
2712
+ }, undefined, false, undefined, this)
2713
+ ]
2714
+ }, undefined, true, undefined, this);
2715
+ }
2716
+ if (currentField === "url") {
2717
+ return /* @__PURE__ */ jsxDEV("box", {
2718
+ flexDirection: "column",
2719
+ width: "100%",
2720
+ height: "100%",
2721
+ children: [
2722
+ /* @__PURE__ */ jsxDEV(Header, {
2723
+ title: "New Secret",
2724
+ subtitle: `${progress} \xB7 optional`
2725
+ }, undefined, false, undefined, this),
2726
+ /* @__PURE__ */ jsxDEV("box", {
2727
+ padding: 2,
2728
+ flexDirection: "column",
2729
+ gap: 1,
2730
+ children: [
2731
+ /* @__PURE__ */ jsxDEV("text", {
2732
+ fg: "#888888",
2733
+ children: "Website URL (optional)"
2734
+ }, undefined, false, undefined, this),
2735
+ inputRow("URL", url, setUrl, nextStep, "e.g. https://github.com")
2736
+ ]
2737
+ }, undefined, true, undefined, this),
2738
+ /* @__PURE__ */ jsxDEV(StatusBar, {
2739
+ message: "Enter next (empty = skip) Esc back"
2740
+ }, undefined, false, undefined, this)
2741
+ ]
2742
+ }, undefined, true, undefined, this);
2743
+ }
2744
+ if (currentField === "description") {
2745
+ return /* @__PURE__ */ jsxDEV("box", {
2746
+ flexDirection: "column",
2747
+ width: "100%",
2748
+ height: "100%",
2749
+ children: [
2750
+ /* @__PURE__ */ jsxDEV(Header, {
2751
+ title: "New Secret",
2752
+ subtitle: `${progress} \xB7 save`
2753
+ }, undefined, false, undefined, this),
2754
+ /* @__PURE__ */ jsxDEV("box", {
2755
+ padding: 2,
2756
+ flexDirection: "column",
2757
+ gap: 1,
2758
+ children: [
2759
+ /* @__PURE__ */ jsxDEV("text", {
2760
+ fg: "#888888",
2761
+ children: "Description (optional) \u2014 Enter to save"
2762
+ }, undefined, false, undefined, this),
2763
+ inputRow("Description", description, setDescription, doCreate, "skip with Enter"),
2764
+ /* @__PURE__ */ jsxDEV("box", {
2765
+ flexDirection: "column",
2766
+ border: true,
2767
+ borderStyle: "rounded",
2768
+ borderColor: "#333333",
2769
+ padding: 1,
2770
+ marginTop: 1,
2771
+ gap: 0,
2772
+ children: [
2773
+ /* @__PURE__ */ jsxDEV("text", {
2774
+ fg: "#555555",
2775
+ children: /* @__PURE__ */ jsxDEV("strong", {
2776
+ children: "Summary"
2777
+ }, undefined, false, undefined, this)
2778
+ }, undefined, false, undefined, this),
2779
+ /* @__PURE__ */ jsxDEV(KeyValue, {
2780
+ label: "Type",
2781
+ value: label
2782
+ }, undefined, false, undefined, this),
2783
+ /* @__PURE__ */ jsxDEV(KeyValue, {
2784
+ label: "Name",
2785
+ value: name,
2786
+ valueColor: "#FFD700"
2787
+ }, undefined, false, undefined, this),
2788
+ /* @__PURE__ */ jsxDEV(KeyValue, {
2789
+ label: "Value",
2790
+ value: "*".repeat(Math.min(value.length, 20)),
2791
+ valueColor: "#FF4444"
2792
+ }, undefined, false, undefined, this),
2793
+ username ? /* @__PURE__ */ jsxDEV(KeyValue, {
2794
+ label: "Username",
2795
+ value: username
2796
+ }, undefined, false, undefined, this) : null,
2797
+ url ? /* @__PURE__ */ jsxDEV(KeyValue, {
2798
+ label: "URL",
2799
+ value: url
2800
+ }, undefined, false, undefined, this) : null
2801
+ ]
2802
+ }, undefined, true, undefined, this)
2803
+ ]
2804
+ }, undefined, true, undefined, this),
2805
+ /* @__PURE__ */ jsxDEV(StatusBar, {
2806
+ message: "Enter save Esc back"
2807
+ }, undefined, false, undefined, this)
2808
+ ]
2809
+ }, undefined, true, undefined, this);
2810
+ }
2811
+ return null;
2812
+ }
2813
+ function EditScreen({ session, secret, onDone, onBack }) {
2814
+ let detail;
2815
+ try {
2816
+ detail = session.getSecret(secret.id);
2817
+ } catch {
2818
+ detail = secret;
2819
+ }
2820
+ const secretType = detail.type;
2821
+ const fields = EDIT_FIELDS.filter((f) => f.forTypes.includes(secretType));
2822
+ const [editing, setEditing] = useState(null);
2823
+ const [values, setValues] = useState({
2824
+ name: detail.name,
2825
+ value: detail.value ?? "",
2826
+ username: detail.username ?? "",
2827
+ url: detail.metadata?.url ? String(detail.metadata.url) : "",
2828
+ description: detail.metadata?.description ? String(detail.metadata.description) : ""
2829
+ });
2830
+ const [inputVal, setInputVal] = useState("");
2831
+ const inputValRef = useRef("");
2832
+ const [editGeneration, setEditGeneration] = useState(0);
2833
+ const doSave = useCallback(() => {
2834
+ try {
2835
+ const updates = {};
2836
+ if (values.name !== detail.name)
2837
+ updates.name = values.name;
2838
+ if (values.value !== (detail.value ?? ""))
2839
+ updates.value = values.value;
2840
+ if (values.username !== (detail.username ?? ""))
2841
+ updates.username = values.username;
2842
+ const metaUpdates = {};
2843
+ const oldUrl = detail.metadata?.url ? String(detail.metadata.url) : "";
2844
+ const oldDesc = detail.metadata?.description ? String(detail.metadata.description) : "";
2845
+ if (values.url !== oldUrl)
2846
+ metaUpdates.url = values.url || undefined;
2847
+ if (values.description !== oldDesc)
2848
+ metaUpdates.description = values.description || undefined;
2849
+ if (Object.keys(metaUpdates).length > 0)
2850
+ updates.metadata = metaUpdates;
2851
+ if (Object.keys(updates).length === 0) {
2852
+ onDone("No changes");
2853
+ return;
2854
+ }
2855
+ session.updateSecret(detail.id, updates);
2856
+ onDone(`"${values.name}" updated`);
2857
+ } catch (e) {
2858
+ onDone(`Error: ${e instanceof Error ? e.message : String(e)}`);
2859
+ }
2860
+ }, [values, detail, session, onDone]);
2861
+ useKeyboard((key) => {
2862
+ if (key.eventType !== "press")
2863
+ return;
2864
+ if (key.name === "escape") {
2865
+ if (editing) {
2866
+ setEditing(null);
2867
+ } else {
2868
+ onBack();
2869
+ }
2870
+ }
2871
+ if (key.ctrl && key.name === "s" && !editing) {
2872
+ doSave();
2873
+ }
2874
+ });
2875
+ if (editing) {
2876
+ const fieldDef = fields.find((f) => f.key === editing);
2877
+ return /* @__PURE__ */ jsxDEV("box", {
2878
+ flexDirection: "column",
2879
+ width: "100%",
2880
+ height: "100%",
2881
+ children: [
2882
+ /* @__PURE__ */ jsxDEV(Header, {
2883
+ title: `Edit ${fieldDef?.label ?? editing}`,
2884
+ subtitle: detail.name
2885
+ }, undefined, false, undefined, this),
2886
+ /* @__PURE__ */ jsxDEV("box", {
2887
+ padding: 2,
2888
+ flexDirection: "column",
2889
+ gap: 1,
2890
+ children: [
2891
+ /* @__PURE__ */ jsxDEV("text", {
2892
+ fg: "#888888",
2893
+ children: [
2894
+ "Current: ",
2895
+ /* @__PURE__ */ jsxDEV("span", {
2896
+ fg: "#555555",
2897
+ children: values[editing] || "(empty)"
2898
+ }, undefined, false, undefined, this)
2899
+ ]
2900
+ }, undefined, true, undefined, this),
2901
+ /* @__PURE__ */ jsxDEV("box", {
2902
+ flexDirection: "row",
2903
+ gap: 1,
2904
+ children: [
2905
+ /* @__PURE__ */ jsxDEV("text", {
2906
+ fg: "#AAAAAA",
2907
+ width: 12,
2908
+ children: "New value"
2909
+ }, undefined, false, undefined, this),
2910
+ /* @__PURE__ */ jsxDEV("input", {
2911
+ value: inputVal,
2912
+ onChange: (v) => {
2913
+ setInputVal(v);
2914
+ inputValRef.current = v;
2915
+ },
2916
+ onSubmit: () => {
2917
+ const saved = inputValRef.current;
2918
+ const field = editing;
2919
+ if (field) {
2920
+ setValues((prev) => ({ ...prev, [field]: saved }));
2921
+ }
2922
+ setEditing(null);
2923
+ setInputVal("");
2924
+ inputValRef.current = "";
2925
+ setEditGeneration((g) => g + 1);
2926
+ },
2927
+ placeholder: values[editing] || "enter new value...",
2928
+ focused: true,
2929
+ flexGrow: 1,
2930
+ backgroundColor: "#111111",
2931
+ focusedBackgroundColor: "#1a1a1a",
2932
+ textColor: "#FFFFFF"
2933
+ }, undefined, false, undefined, this)
2934
+ ]
2935
+ }, undefined, true, undefined, this)
2936
+ ]
2937
+ }, undefined, true, undefined, this),
2938
+ /* @__PURE__ */ jsxDEV(StatusBar, {
2939
+ message: "Enter save field Esc cancel"
2940
+ }, undefined, false, undefined, this)
2941
+ ]
2942
+ }, undefined, true, undefined, this);
2943
+ }
2944
+ const options = fields.map((f) => {
2945
+ let display;
2946
+ if (f.key === "value") {
2947
+ display = values[f.key] ? "***" : "(empty)";
2948
+ } else {
2949
+ display = values[f.key] || "(empty)";
2950
+ }
2951
+ return {
2952
+ name: `${f.label}: ${display}`,
2953
+ description: "Enter to edit",
2954
+ value: f.key
2955
+ };
2956
+ });
2957
+ return /* @__PURE__ */ jsxDEV("box", {
2958
+ flexDirection: "column",
2959
+ width: "100%",
2960
+ height: "100%",
2961
+ children: [
2962
+ /* @__PURE__ */ jsxDEV(Header, {
2963
+ title: "Edit Secret",
2964
+ subtitle: detail.name
2965
+ }, undefined, false, undefined, this),
2966
+ /* @__PURE__ */ jsxDEV("box", {
2967
+ flexGrow: 1,
2968
+ paddingX: 2,
2969
+ paddingTop: 1,
2970
+ children: /* @__PURE__ */ jsxDEV("select", {
2971
+ options,
2972
+ onSelect: (_i, opt) => {
2973
+ const val = opt.value ?? "";
2974
+ const current = values[val] ?? "";
2975
+ setEditing(val);
2976
+ setInputVal(current);
2977
+ inputValRef.current = current;
2978
+ },
2979
+ focused: true,
2980
+ height: Math.min(options.length * 2 + 1, 14),
2981
+ selectedBackgroundColor: "#222222",
2982
+ selectedTextColor: "#FFD700"
2983
+ }, editGeneration, false, undefined, this)
2984
+ }, undefined, false, undefined, this),
2985
+ /* @__PURE__ */ jsxDEV("box", {
2986
+ paddingX: 2,
2987
+ paddingBottom: 1,
2988
+ children: /* @__PURE__ */ jsxDEV("box", {
2989
+ border: true,
2990
+ borderStyle: "rounded",
2991
+ borderColor: "#FFD700",
2992
+ paddingX: 2,
2993
+ height: 3,
2994
+ flexDirection: "row",
2995
+ alignItems: "center",
2996
+ gap: 2,
2997
+ onMouseDown: doSave,
2998
+ children: /* @__PURE__ */ jsxDEV("text", {
2999
+ fg: "#FFD700",
3000
+ children: /* @__PURE__ */ jsxDEV("strong", {
3001
+ children: "Ctrl+S Save"
3002
+ }, undefined, false, undefined, this)
3003
+ }, undefined, false, undefined, this)
3004
+ }, undefined, false, undefined, this)
3005
+ }, undefined, false, undefined, this),
3006
+ /* @__PURE__ */ jsxDEV(StatusBar, {
3007
+ message: "Enter edit field Ctrl+S save Esc back"
3008
+ }, undefined, false, undefined, this)
3009
+ ]
3010
+ }, undefined, true, undefined, this);
3011
+ }
3012
+ function App({ vaultPath }) {
3013
+ const [screen, setScreen] = useState("unlock");
3014
+ const [session, setSession] = useState(null);
3015
+ const [selectedSecret, setSelectedSecret] = useState(null);
3016
+ const { toast, show: showToast } = useToast();
3017
+ const [refreshKey, setRefreshKey] = useState(0);
3018
+ const goHome = useCallback(() => {
3019
+ setScreen("home");
3020
+ setSelectedSecret(null);
3021
+ setRefreshKey((k) => k + 1);
3022
+ }, []);
3023
+ const handleDone = useCallback((msg) => {
3024
+ goHome();
3025
+ showToast(msg, msg.startsWith("Error:") ? "error" : "success");
3026
+ }, [goHome, showToast]);
3027
+ if (screen === "unlock") {
3028
+ return /* @__PURE__ */ jsxDEV(UnlockScreen, {
3029
+ vaultPath,
3030
+ onUnlock: (s) => {
3031
+ setSession(s);
3032
+ setScreen("home");
3033
+ }
3034
+ }, undefined, false, undefined, this);
3035
+ }
3036
+ if (!session)
3037
+ return /* @__PURE__ */ jsxDEV("box", {
3038
+ padding: 2,
3039
+ children: /* @__PURE__ */ jsxDEV("text", {
3040
+ fg: "#FF4444",
3041
+ children: "No session."
3042
+ }, undefined, false, undefined, this)
3043
+ }, undefined, false, undefined, this);
3044
+ switch (screen) {
3045
+ case "home":
3046
+ return /* @__PURE__ */ jsxDEV(HomeScreen, {
3047
+ session,
3048
+ onSelect: (s) => {
3049
+ setSelectedSecret(s);
3050
+ setScreen("detail");
3051
+ },
3052
+ onCreate: () => setScreen("create"),
3053
+ toast
3054
+ }, refreshKey, false, undefined, this);
3055
+ case "detail":
3056
+ return selectedSecret ? /* @__PURE__ */ jsxDEV(DetailScreen, {
3057
+ session,
3058
+ secret: selectedSecret,
3059
+ onBack: goHome,
3060
+ onDeleted: handleDone,
3061
+ onEdit: () => setScreen("edit")
3062
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(HomeScreen, {
3063
+ session,
3064
+ onSelect: (s) => {
3065
+ setSelectedSecret(s);
3066
+ setScreen("detail");
3067
+ },
3068
+ onCreate: () => setScreen("create"),
3069
+ toast
3070
+ }, refreshKey, false, undefined, this);
3071
+ case "edit":
3072
+ return selectedSecret ? /* @__PURE__ */ jsxDEV(EditScreen, {
3073
+ session,
3074
+ secret: selectedSecret,
3075
+ onDone: handleDone,
3076
+ onBack: () => setScreen("detail")
3077
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(HomeScreen, {
3078
+ session,
3079
+ onSelect: (s) => {
3080
+ setSelectedSecret(s);
3081
+ setScreen("detail");
3082
+ },
3083
+ onCreate: () => setScreen("create"),
3084
+ toast
3085
+ }, refreshKey, false, undefined, this);
3086
+ case "create":
3087
+ return /* @__PURE__ */ jsxDEV(CreateScreen, {
3088
+ session,
3089
+ onDone: handleDone,
3090
+ onBack: goHome
3091
+ }, undefined, false, undefined, this);
3092
+ default:
3093
+ return /* @__PURE__ */ jsxDEV(HomeScreen, {
3094
+ session,
3095
+ onSelect: (s) => {
3096
+ setSelectedSecret(s);
3097
+ setScreen("detail");
3098
+ },
3099
+ onCreate: () => setScreen("create"),
3100
+ toast
3101
+ }, refreshKey, false, undefined, this);
3102
+ }
3103
+ }
3104
+ async function runTui(vaultPath) {
3105
+ const vault = vaultPath ?? defaultVaultPath();
3106
+ const renderer = await createCliRenderer({ exitOnCtrlC: true });
3107
+ createRoot(renderer).render(/* @__PURE__ */ jsxDEV(App, {
3108
+ vaultPath: vault
3109
+ }, undefined, false, undefined, this));
3110
+ }
3111
+ var EDIT_FIELDS;
3112
+ var init_tui = __esm(() => {
3113
+ init_src3();
3114
+ init_os_secrets();
3115
+ EDIT_FIELDS = [
3116
+ { key: "name", label: "Name", forTypes: ["password", "note", "otp"] },
3117
+ { key: "value", label: "Value", forTypes: ["password", "note", "otp"] },
3118
+ { key: "username", label: "Username", forTypes: ["password", "otp"] },
3119
+ { key: "url", label: "URL", forTypes: ["password"] },
3120
+ { key: "description", label: "Description", forTypes: ["password", "note", "otp"] }
3121
+ ];
3122
+ });
3123
+
3124
+ // apps/cli/src/index.ts
3125
+ import { spawn } from "child_process";
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";
3129
+
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
+ }
3150
+ async function readPasswordMasked(prompt = "Master password: ") {
3151
+ if (!process.stdin.isTTY) {
3152
+ throw new Error("Cannot read password: stdin is not a TTY");
3153
+ }
3154
+ process.stdout.write(prompt);
3155
+ process.stdin.setRawMode(true);
3156
+ process.stdin.resume();
3157
+ process.stdin.setEncoding("utf8");
3158
+ let password = "";
3159
+ return new Promise((resolve, reject) => {
3160
+ const onData = (char) => {
3161
+ const code = char.charCodeAt(0);
3162
+ if (char === "\r" || char === `
3163
+ `) {
3164
+ cleanup();
3165
+ process.stdout.write(`
3166
+ `);
3167
+ resolve(password);
3168
+ return;
3169
+ }
3170
+ if (code === 3) {
3171
+ cleanup();
3172
+ process.stdout.write(`
3173
+ `);
3174
+ reject(new Error("Password input cancelled"));
3175
+ return;
3176
+ }
3177
+ if (code === 4) {
3178
+ cleanup();
3179
+ process.stdout.write(`
3180
+ `);
3181
+ resolve(password);
3182
+ return;
3183
+ }
3184
+ if (code === 127 || code === 8) {
3185
+ if (password.length > 0) {
3186
+ password = password.slice(0, -1);
3187
+ process.stdout.write("\b \b");
3188
+ }
3189
+ return;
3190
+ }
3191
+ if (code === 27)
3192
+ return;
3193
+ if (code >= 32) {
3194
+ password += char;
3195
+ process.stdout.write("*");
3196
+ }
3197
+ };
3198
+ const cleanup = () => {
3199
+ process.stdin.setRawMode(false);
3200
+ process.stdin.pause();
3201
+ process.stdin.removeListener("data", onData);
3202
+ };
3203
+ process.stdin.on("data", onData);
3204
+ });
3205
+ }
3206
+
3207
+ // packages/core/src/daemon.ts
3208
+ init_src();
3209
+ init_src2();
3210
+ init_src3();
3211
+ init_paths();
3212
+ init_paths();
3213
+ import { createHash, timingSafeEqual } from "crypto";
3214
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
3215
+ var DAEMON_TOKEN_SERVICE = "autho.daemon";
3216
+ function daemonTokenName(statePath) {
3217
+ return createHash("sha256").update(statePath).digest("hex");
3218
+ }
3219
+ async function storeDaemonToken(statePath, token) {
3220
+ const tokenName = daemonTokenName(statePath);
3221
+ if (process.env.AUTHO_DISABLE_OS_SECRETS !== "1") {
3222
+ try {
3223
+ await Bun.secrets.set({
3224
+ name: tokenName,
3225
+ service: DAEMON_TOKEN_SERVICE,
3226
+ value: token
3227
+ });
3228
+ return {
3229
+ token: null,
3230
+ tokenName,
3231
+ tokenStorage: "os"
3232
+ };
3233
+ } catch {}
3234
+ }
3235
+ return {
3236
+ token,
3237
+ tokenName: null,
3238
+ tokenStorage: "file"
3239
+ };
3240
+ }
3241
+ async function resolveDaemonToken(state) {
3242
+ if (state.tokenStorage === "os") {
3243
+ if (!state.tokenName) {
3244
+ throw new Error("Daemon state is missing tokenName for OS secret storage");
3245
+ }
3246
+ const token = await Bun.secrets.get({
3247
+ name: state.tokenName,
3248
+ service: DAEMON_TOKEN_SERVICE
3249
+ });
3250
+ if (!token) {
3251
+ throw new Error("Daemon token not found in OS secret storage");
3252
+ }
3253
+ return token;
3254
+ }
3255
+ if (!state.token) {
3256
+ throw new Error("Daemon state is missing token");
3257
+ }
3258
+ return state.token;
3259
+ }
3260
+ async function deleteStoredDaemonToken(state) {
3261
+ if (!state || state.tokenStorage !== "os" || !state.tokenName) {
3262
+ return;
3263
+ }
3264
+ try {
3265
+ await Bun.secrets.delete({
3266
+ name: state.tokenName,
3267
+ service: DAEMON_TOKEN_SERVICE
3268
+ });
3269
+ } catch {}
3270
+ }
3271
+ function readDaemonState(statePath) {
3272
+ if (!existsSync3(statePath)) {
3273
+ return null;
1109
3274
  }
1110
3275
  const stored = JSON.parse(readFileSync3(statePath, "utf8"));
1111
3276
  return {
@@ -1402,6 +3567,8 @@ async function daemonStop(options) {
1402
3567
  }
1403
3568
 
1404
3569
  // apps/cli/src/index.ts
3570
+ init_src3();
3571
+ init_os_secrets();
1405
3572
  function parseArgs(argv) {
1406
3573
  const dashDashIndex = argv.indexOf("--");
1407
3574
  const main = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
@@ -1478,7 +3645,7 @@ function output(value, jsonMode = false) {
1478
3645
  console.log(value);
1479
3646
  }
1480
3647
  function absolutePath(path) {
1481
- return resolve3(path);
3648
+ return resolve4(path);
1482
3649
  }
1483
3650
  function buildSecretMetadata(args) {
1484
3651
  return Object.fromEntries(Object.entries({
@@ -1498,7 +3665,7 @@ async function readBufferedStdin() {
1498
3665
  }
1499
3666
  async function createPromptAdapter() {
1500
3667
  if (process.stdin.isTTY) {
1501
- const rl = createInterface({
3668
+ const rl = createInterface2({
1502
3669
  input: process.stdin,
1503
3670
  output: process.stdout
1504
3671
  });
@@ -1525,6 +3692,9 @@ async function askPassword(prompt, initial) {
1525
3692
  if (initial) {
1526
3693
  return initial;
1527
3694
  }
3695
+ if (process.stdin.isTTY) {
3696
+ return readPasswordMasked("Master password: ");
3697
+ }
1528
3698
  return prompt.ask("Master password: ");
1529
3699
  }
1530
3700
  async function runPromptMode(vaultPath, initialPassword) {
@@ -1587,6 +3757,128 @@ async function runPromptMode(vaultPath, initialPassword) {
1587
3757
  prompt.close();
1588
3758
  }
1589
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
+ }
1590
3882
  async function runWebServer(vaultPath, args) {
1591
3883
  const commandArgs = [
1592
3884
  "run",
@@ -1620,44 +3912,73 @@ async function runWebServer(vaultPath, args) {
1620
3912
  }
1621
3913
  function help() {
1622
3914
  return [
1623
- "Autho Bun CLI",
3915
+ "Autho \u2013 Local-first secret manager for humans and coding agents",
3916
+ "",
3917
+ "Usage:",
3918
+ " autho Open interactive TUI (terminal UI)",
3919
+ " autho <command> Run a CLI command (see below)",
1624
3920
  "",
1625
3921
  "Commands:",
1626
- " prompt [--password <value>] [--vault <path>]",
1627
- " init --password <value> [--vault <path>]",
1628
- " status [--password <value>] [--vault <path>] [--project-file <path>] [--json]",
1629
- " project init --map <ENV_NAME=secretRef> [--map <ENV_NAME=secretRef>] [--output <path>] [--force] [--json]",
3922
+ " prompt [--vault <path>]",
3923
+ " init [--vault <path>]",
3924
+ " status [--vault <path>] [--project-file <path>] [--json]",
3925
+ " project init --map <ENV=ref> [--output <path>] [--force] [--json]",
1630
3926
  " web serve [--vault <path>] [--host <value>] [--port <value>]",
1631
3927
  " daemon serve [--vault <path>] [--state-file <path>] [--host <value>] [--port <value>]",
1632
3928
  " daemon status [--state-file <path>] [--json]",
1633
- " daemon unlock --password <value> [--ttl <seconds>] [--state-file <path>] [--json]",
3929
+ " daemon unlock [--ttl <seconds>] [--state-file <path>] [--json]",
1634
3930
  " daemon lock --session <id> [--state-file <path>] [--json]",
1635
3931
  " daemon stop [--state-file <path>] [--json]",
1636
- " daemon env render --session <id> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--state-file <path>] [--json]",
1637
- " daemon exec --session <id> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--state-file <path>] -- <command>",
1638
- " import legacy --password <value> --file <path> [--skip-existing] [--vault <path>] [--json]",
1639
- " secrets add --password <value> --name <name> --type <password|note|otp> --value <value> [--username <value>] [--url <value>] [--description <value>] [--digits <value>] [--algorithm <value>] [--vault <path>]",
1640
- " secrets list --password <value> [--vault <path>] [--json]",
1641
- " secrets get --password <value> --ref <name-or-id> [--vault <path>] [--json]",
1642
- " secrets rm --password <value> --ref <name-or-id> [--vault <path>] [--json]",
1643
- " otp code --password <value> --ref <name-or-id> [--vault <path>] [--json]",
1644
- " lease create --password <value> --secret <name-or-id> [--secret <name-or-id>] --ttl <seconds> [--name <value>] [--vault <path>] [--json]",
1645
- " lease revoke --password <value> --lease <lease-id> [--vault <path>] [--json]",
1646
- " env render --password <value> --map <ENV_NAME=secretRef> [--map <ENV_NAME=secretRef>] [--project-file <path>] [--lease <lease-id>] [--vault <path>] [--json]",
1647
- " env sync --password <value> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--ttl <seconds>] [--output <path>] [--force] [--vault <path>] [--json]",
1648
- " exec --password <value> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--vault <path>] -- <command>",
1649
- " file encrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
1650
- " file decrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
1651
- " files encrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
1652
- " files decrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
1653
- " audit list --password <value> [--limit <number>] [--vault <path>] [--json]",
3932
+ " daemon env render --session <id> --map <ENV=ref> [--project-file <path>] [--lease <id>] [--state-file <path>] [--json]",
3933
+ " daemon exec --session <id> --map <ENV=ref> [--project-file <path>] [--lease <id>] [--state-file <path>] -- <cmd>",
3934
+ " import legacy --file <path> [--skip-existing] [--vault <path>] [--json]",
3935
+ " secrets add --name <name> --type <password|note|otp> --value <value> [--username <v>] [--url <v>] [--description <v>] [--digits <v>] [--algorithm <v>] [--vault <path>]",
3936
+ " secrets list [--vault <path>] [--json]",
3937
+ " secrets get --ref <name-or-id> [--vault <path>] [--json]",
3938
+ " secrets rm --ref <name-or-id> [--vault <path>] [--json]",
3939
+ " secrets edit --ref <name-or-id> [--new-name <v>] [--value <v>] [--username <v>] [--url <v>] [--description <v>] [--vault <path>] [--json]",
3940
+ " otp code --ref <name-or-id> [--vault <path>] [--json]",
3941
+ " lease create --secret <ref> [--secret <ref>] --ttl <seconds> [--name <v>] [--vault <path>] [--json]",
3942
+ " lease revoke --lease <id> [--vault <path>] [--json]",
3943
+ " env render --map <ENV=ref> [--project-file <path>] [--lease <id>] [--vault <path>] [--json]",
3944
+ " env sync --map <ENV=ref> [--project-file <path>] [--lease <id>] [--ttl <seconds>] [--output <path>] [--force] [--vault <path>] [--json]",
3945
+ " exec --map <ENV=ref> [--project-file <path>] [--lease <id>] [--vault <path>] -- <cmd>",
3946
+ " file encrypt --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
3947
+ " file decrypt --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
3948
+ " files encrypt --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
3949
+ " files decrypt --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
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]",
3957
+ "",
3958
+ "Authentication:",
3959
+ " When running interactively (TTY), you will be securely prompted for your",
3960
+ " master password with masked input (no --password flag needed).",
3961
+ "",
3962
+ " For automation and coding agents, use one of:",
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)",
3967
+ " --password <value> CLI flag (visible in shell history - avoid!)",
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.",
1654
3976
  "",
1655
3977
  "Notes:",
1656
- " Running `autho` with no command enters interactive prompt mode.",
1657
- " The default vault path is ~/.autho/vault.db (or AUTHO_HOME/vault.db).",
1658
- " The default project file is ~/.autho/project.json when it exists (or AUTHO_HOME/project.json).",
1659
- " The default daemon state file is ~/.autho/daemon.json (or AUTHO_HOME/daemon.json).",
1660
- " AUTHO_MASTER_PASSWORD can be used instead of --password."
3978
+ " Running `autho` with no arguments opens the interactive TUI.",
3979
+ " The default vault path is ~/.autho/vault.db (override with AUTHO_HOME).",
3980
+ " The default project file is ~/.autho/project.json.",
3981
+ " The default daemon state file is ~/.autho/daemon.json."
1661
3982
  ].join(`
1662
3983
  `);
1663
3984
  }
@@ -1670,8 +3991,16 @@ async function main() {
1670
3991
  const explicitProjectFile = getString(args, "project-file");
1671
3992
  const fallbackProjectFile = defaultProjectFilePath();
1672
3993
  const projectFile = explicitProjectFile ?? (existsSync4(fallbackProjectFile) ? fallbackProjectFile : undefined);
1673
- const password = getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
3994
+ let password = getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
3995
+ if (!password) {
3996
+ password = await loadVaultPassword(vaultPath) ?? undefined;
3997
+ }
1674
3998
  if (!scope) {
3999
+ if (process.stdin.isTTY) {
4000
+ const { runTui: runTui2 } = await Promise.resolve().then(() => (init_tui(), exports_tui));
4001
+ await runTui2(vaultPath);
4002
+ return;
4003
+ }
1675
4004
  await runPromptMode(vaultPath, password);
1676
4005
  return;
1677
4006
  }
@@ -1683,8 +4012,41 @@ async function main() {
1683
4012
  await runPromptMode(vaultPath, password);
1684
4013
  return;
1685
4014
  }
4015
+ if (!password && process.stdin.isTTY) {
4016
+ const needsPassword = [
4017
+ "secrets",
4018
+ "otp",
4019
+ "lease",
4020
+ "env",
4021
+ "exec",
4022
+ "file",
4023
+ "files",
4024
+ "audit",
4025
+ "import",
4026
+ "recovery",
4027
+ "unlock"
4028
+ ].includes(scope);
4029
+ const daemonNeedsPassword = scope === "daemon" && action === "unlock";
4030
+ if (needsPassword || daemonNeedsPassword) {
4031
+ password = await readPasswordMasked("Master password: ");
4032
+ }
4033
+ }
1686
4034
  if (scope === "init") {
1687
- 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
+ }
1688
4050
  return;
1689
4051
  }
1690
4052
  if (scope === "status") {
@@ -1702,6 +4064,31 @@ async function main() {
1702
4064
  }), jsonMode);
1703
4065
  return;
1704
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
+ }
1705
4092
  if (scope === "web" && action === "serve") {
1706
4093
  await runWebServer(vaultPath, args);
1707
4094
  return;
@@ -1759,7 +4146,44 @@ async function main() {
1759
4146
  process.stderr.write(result.stderr);
1760
4147
  process.exit(result.exitCode);
1761
4148
  }
1762
- 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);
1763
4187
  try {
1764
4188
  if (scope === "import" && action === "legacy") {
1765
4189
  output(session.importLegacyFile(absolutePath(required(getString(args, "file"), "--file")), {
@@ -1791,6 +4215,23 @@ async function main() {
1791
4215
  output(session.removeSecret(required(ref, "--ref")), jsonMode);
1792
4216
  return;
1793
4217
  }
4218
+ if (scope === "secrets" && action === "edit") {
4219
+ const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
4220
+ const updates = {};
4221
+ if (getString(args, "new-name"))
4222
+ updates.name = getString(args, "new-name");
4223
+ if (getString(args, "value"))
4224
+ updates.value = getString(args, "value");
4225
+ if (getString(args, "username"))
4226
+ updates.username = getString(args, "username");
4227
+ if (getString(args, "type"))
4228
+ updates.type = getString(args, "type");
4229
+ const meta = buildSecretMetadata(args);
4230
+ if (Object.keys(meta).length > 0)
4231
+ updates.metadata = meta;
4232
+ output(session.updateSecret(required(ref, "--ref"), updates), jsonMode);
4233
+ return;
4234
+ }
1794
4235
  if (scope === "otp" && action === "code") {
1795
4236
  const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
1796
4237
  output(session.generateOtp(required(ref, "--ref")), jsonMode);