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