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.
- package/README.md +3 -1
- package/dist/autho.js +2566 -125
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
|
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/
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
1055
|
-
|
|
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
|
|
1077
|
-
if (
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
name:
|
|
1083
|
-
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
return token;
|
|
1423
|
+
return true;
|
|
1424
|
+
} catch {
|
|
1425
|
+
return false;
|
|
1089
1426
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
|
1096
|
-
if (
|
|
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:
|
|
1102
|
-
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
|
|
1107
|
-
if (
|
|
1108
|
-
return
|
|
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
|
|
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 =
|
|
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
|
|
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 [--
|
|
1627
|
-
" init
|
|
1628
|
-
" status [--
|
|
1629
|
-
" project init --map <
|
|
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
|
|
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 <
|
|
1637
|
-
" daemon exec --session <id> --map <
|
|
1638
|
-
" import legacy --
|
|
1639
|
-
" secrets add --
|
|
1640
|
-
" secrets list
|
|
1641
|
-
" secrets get --
|
|
1642
|
-
" secrets rm --
|
|
1643
|
-
"
|
|
1644
|
-
"
|
|
1645
|
-
" lease
|
|
1646
|
-
"
|
|
1647
|
-
" env
|
|
1648
|
-
"
|
|
1649
|
-
"
|
|
1650
|
-
" file
|
|
1651
|
-
"
|
|
1652
|
-
" files
|
|
1653
|
-
"
|
|
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
|
|
1657
|
-
" The default vault path is ~/.autho/vault.db (
|
|
1658
|
-
" The default project file is ~/.autho/project.json
|
|
1659
|
-
" The default daemon state file is ~/.autho/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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|