autho 3.0.3 → 3.0.5
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/dist/autho.js +1433 -285
- package/package.json +1 -1
package/dist/autho.js
CHANGED
|
@@ -590,7 +590,7 @@ var package_default;
|
|
|
590
590
|
var init_package = __esm(() => {
|
|
591
591
|
package_default = {
|
|
592
592
|
name: "autho",
|
|
593
|
-
version: "3.0.
|
|
593
|
+
version: "3.0.5",
|
|
594
594
|
description: "Local-first secret manager for humans and coding agents, rebuilt on Bun",
|
|
595
595
|
type: "module",
|
|
596
596
|
bin: {
|
|
@@ -632,6 +632,19 @@ var init_package = __esm(() => {
|
|
|
632
632
|
import { Database } from "bun:sqlite";
|
|
633
633
|
import { chmodSync, existsSync, mkdirSync } from "fs";
|
|
634
634
|
import { dirname } from "path";
|
|
635
|
+
function protectedTable(tableName) {
|
|
636
|
+
if (!Object.hasOwn(PROTECTED_COLUMNS, tableName)) {
|
|
637
|
+
throw new Error(`Not a sealed table: ${tableName}`);
|
|
638
|
+
}
|
|
639
|
+
return tableName;
|
|
640
|
+
}
|
|
641
|
+
function protectedRowWhere(table, rowId) {
|
|
642
|
+
if (table === "secret_links") {
|
|
643
|
+
const [primaryId, linkedId] = rowId.split(">");
|
|
644
|
+
return { sql: "primary_id = ?1 AND linked_id = ?2", values: [primaryId ?? "", linkedId ?? ""] };
|
|
645
|
+
}
|
|
646
|
+
return { sql: `${table === "meta" ? "key" : "id"} = ?1`, values: [rowId] };
|
|
647
|
+
}
|
|
635
648
|
function parseJson(value) {
|
|
636
649
|
return JSON.parse(value);
|
|
637
650
|
}
|
|
@@ -660,6 +673,12 @@ class AuthoDatabase {
|
|
|
660
673
|
transaction(fn) {
|
|
661
674
|
return this.db.transaction(fn)();
|
|
662
675
|
}
|
|
676
|
+
immediateTransaction(fn) {
|
|
677
|
+
return this.db.transaction(fn).immediate();
|
|
678
|
+
}
|
|
679
|
+
get inTransaction() {
|
|
680
|
+
return this.db.inTransaction;
|
|
681
|
+
}
|
|
663
682
|
close() {
|
|
664
683
|
this.hardenStorageFiles();
|
|
665
684
|
this.db.close();
|
|
@@ -711,7 +730,30 @@ class AuthoDatabase {
|
|
|
711
730
|
metadata TEXT NOT NULL,
|
|
712
731
|
created_at TEXT NOT NULL
|
|
713
732
|
);
|
|
733
|
+
|
|
734
|
+
CREATE TABLE IF NOT EXISTS secret_links (
|
|
735
|
+
primary_id TEXT NOT NULL,
|
|
736
|
+
linked_id TEXT NOT NULL,
|
|
737
|
+
created_at TEXT NOT NULL,
|
|
738
|
+
PRIMARY KEY (primary_id, linked_id)
|
|
739
|
+
);
|
|
740
|
+
CREATE INDEX IF NOT EXISTS secret_links_linked ON secret_links (linked_id);
|
|
741
|
+
|
|
742
|
+
CREATE TABLE IF NOT EXISTS seal_rows (
|
|
743
|
+
table_name TEXT NOT NULL,
|
|
744
|
+
row_id TEXT NOT NULL,
|
|
745
|
+
mac TEXT NOT NULL,
|
|
746
|
+
PRIMARY KEY (table_name, row_id)
|
|
747
|
+
);
|
|
714
748
|
`);
|
|
749
|
+
const leaseColumns = this.db.query("PRAGMA table_info(leases)").all();
|
|
750
|
+
if (!leaseColumns.some((column) => column.name === "include_links")) {
|
|
751
|
+
this.db.exec("ALTER TABLE leases ADD COLUMN include_links INTEGER NOT NULL DEFAULT 1");
|
|
752
|
+
}
|
|
753
|
+
const sealRowColumns = this.db.query("PRAGMA table_info(seal_rows)").all();
|
|
754
|
+
if (!sealRowColumns.some((column) => column.name === "row_json")) {
|
|
755
|
+
this.db.exec("ALTER TABLE seal_rows ADD COLUMN row_json TEXT");
|
|
756
|
+
}
|
|
715
757
|
}
|
|
716
758
|
getVaultConfig() {
|
|
717
759
|
const row = this.db.query("SELECT value FROM meta WHERE key = ?1").get("vault.config");
|
|
@@ -797,11 +839,32 @@ class AuthoDatabase {
|
|
|
797
839
|
this.db.query(`UPDATE secrets SET ${parts.join(", ")} WHERE id = ?${idx}`).run(...values);
|
|
798
840
|
}
|
|
799
841
|
deleteSecret(id) {
|
|
842
|
+
this.db.query("DELETE FROM secret_links WHERE primary_id = ?1 OR linked_id = ?1").run(id);
|
|
800
843
|
this.db.query("DELETE FROM secrets WHERE id = ?1").run(id);
|
|
801
844
|
}
|
|
845
|
+
insertLink(link) {
|
|
846
|
+
this.db.query("INSERT OR IGNORE INTO secret_links (primary_id, linked_id, created_at) VALUES (?1, ?2, ?3)").run(link.primaryId, link.linkedId, link.createdAt);
|
|
847
|
+
}
|
|
848
|
+
deleteLink(primaryId, linkedId) {
|
|
849
|
+
return this.db.query("DELETE FROM secret_links WHERE primary_id = ?1 AND linked_id = ?2").run(primaryId, linkedId).changes > 0;
|
|
850
|
+
}
|
|
851
|
+
hasLink(primaryId, linkedId) {
|
|
852
|
+
return this.db.query("SELECT 1 FROM secret_links WHERE primary_id = ?1 AND linked_id = ?2").get(primaryId, linkedId) !== null;
|
|
853
|
+
}
|
|
854
|
+
listLinksFrom(id) {
|
|
855
|
+
return this.db.query(`SELECT primary_id AS primaryId, linked_id AS linkedId, created_at AS createdAt
|
|
856
|
+
FROM secret_links WHERE primary_id = ?1 ORDER BY created_at ASC`).all(id);
|
|
857
|
+
}
|
|
858
|
+
listLinksTo(id) {
|
|
859
|
+
return this.db.query(`SELECT primary_id AS primaryId, linked_id AS linkedId, created_at AS createdAt
|
|
860
|
+
FROM secret_links WHERE linked_id = ?1 ORDER BY created_at ASC`).all(id);
|
|
861
|
+
}
|
|
862
|
+
listLinks() {
|
|
863
|
+
return this.db.query("SELECT primary_id AS primaryId, linked_id AS linkedId, created_at AS createdAt FROM secret_links").all();
|
|
864
|
+
}
|
|
802
865
|
insertLease(lease) {
|
|
803
|
-
this.db.query(`INSERT INTO leases (id, name, secret_refs, expires_at, revoked_at, created_at)
|
|
804
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6)`).run(lease.id, lease.name, lease.secretRefs, lease.expiresAt, lease.revokedAt, lease.createdAt);
|
|
866
|
+
this.db.query(`INSERT INTO leases (id, name, secret_refs, expires_at, revoked_at, created_at, include_links)
|
|
867
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)`).run(lease.id, lease.name, lease.secretRefs, lease.expiresAt, lease.revokedAt, lease.createdAt, lease.includeLinks);
|
|
805
868
|
}
|
|
806
869
|
findLease(id) {
|
|
807
870
|
const row = this.db.query(`SELECT
|
|
@@ -810,7 +873,8 @@ class AuthoDatabase {
|
|
|
810
873
|
secret_refs AS secretRefs,
|
|
811
874
|
expires_at AS expiresAt,
|
|
812
875
|
revoked_at AS revokedAt,
|
|
813
|
-
created_at AS createdAt
|
|
876
|
+
created_at AS createdAt,
|
|
877
|
+
include_links AS includeLinks
|
|
814
878
|
FROM leases
|
|
815
879
|
WHERE id = ?1
|
|
816
880
|
LIMIT 1`).get(id);
|
|
@@ -819,6 +883,55 @@ class AuthoDatabase {
|
|
|
819
883
|
revokeLease(id, revokedAt) {
|
|
820
884
|
this.db.query("UPDATE leases SET revoked_at = ?2 WHERE id = ?1").run(id, revokedAt);
|
|
821
885
|
}
|
|
886
|
+
revokeActiveLeases(nowIso) {
|
|
887
|
+
return this.db.query("UPDATE leases SET revoked_at = ?1 WHERE revoked_at IS NULL AND expires_at > ?1").run(nowIso).changes;
|
|
888
|
+
}
|
|
889
|
+
listProtectedRows() {
|
|
890
|
+
const select = (table) => `SELECT ${PROTECTED_COLUMNS[table].join(", ")} FROM ${table}`;
|
|
891
|
+
const secrets = this.db.query(select("secrets")).all();
|
|
892
|
+
const links = this.db.query(select("secret_links")).all();
|
|
893
|
+
const leases = this.db.query(select("leases")).all();
|
|
894
|
+
const meta = this.db.query(`${select("meta")} WHERE key != ?1`).all(SEAL_META_KEY);
|
|
895
|
+
return [
|
|
896
|
+
...secrets.map((row) => ({ row, rowId: String(row.id), tableName: "secrets" })),
|
|
897
|
+
...links.map((row) => ({ row, rowId: `${row.primary_id}>${row.linked_id}`, tableName: "secret_links" })),
|
|
898
|
+
...leases.map((row) => ({ row, rowId: String(row.id), tableName: "leases" })),
|
|
899
|
+
...meta.map((row) => ({ row, rowId: String(row.key), tableName: "meta" }))
|
|
900
|
+
];
|
|
901
|
+
}
|
|
902
|
+
getSeal() {
|
|
903
|
+
const row = this.db.query("SELECT value FROM meta WHERE key = ?1").get(SEAL_META_KEY);
|
|
904
|
+
return row?.value ?? null;
|
|
905
|
+
}
|
|
906
|
+
setSeal(seal, rows) {
|
|
907
|
+
this.db.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)").run(SEAL_META_KEY, seal);
|
|
908
|
+
this.db.exec("DELETE FROM seal_rows");
|
|
909
|
+
const insert = this.db.query("INSERT INTO seal_rows (table_name, row_id, mac, row_json) VALUES (?1, ?2, ?3, ?4)");
|
|
910
|
+
for (const row of rows) {
|
|
911
|
+
insert.run(row.tableName, row.rowId, row.mac, row.snapshot);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
listSealRows() {
|
|
915
|
+
return this.db.query("SELECT table_name AS tableName, row_id AS rowId, mac, row_json AS snapshot FROM seal_rows").all();
|
|
916
|
+
}
|
|
917
|
+
deleteProtectedRow(tableName, rowId) {
|
|
918
|
+
const table = protectedTable(tableName);
|
|
919
|
+
if (table === "meta" && rowId === SEAL_META_KEY) {
|
|
920
|
+
throw new Error("The seal is not a protected row");
|
|
921
|
+
}
|
|
922
|
+
const where = protectedRowWhere(table, rowId);
|
|
923
|
+
this.db.query(`DELETE FROM ${table} WHERE ${where.sql}`).run(...where.values);
|
|
924
|
+
}
|
|
925
|
+
insertProtectedRow(tableName, row) {
|
|
926
|
+
const table = protectedTable(tableName);
|
|
927
|
+
const columns = PROTECTED_COLUMNS[table];
|
|
928
|
+
if (table === "meta" && row.key === SEAL_META_KEY) {
|
|
929
|
+
throw new Error("The seal is not a protected row");
|
|
930
|
+
}
|
|
931
|
+
const placeholders = columns.map((_, index) => `?${index + 1}`).join(", ");
|
|
932
|
+
const values = columns.map((column) => row[column] ?? null);
|
|
933
|
+
this.db.query(`INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders})`).run(...values);
|
|
934
|
+
}
|
|
822
935
|
insertAudit(event) {
|
|
823
936
|
this.db.query(`INSERT INTO audit_events (id, event_type, subject_type, subject_ref, message, metadata, created_at)
|
|
824
937
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)`).run(event.id, event.eventType, event.subjectType, event.subjectRef, event.message, event.metadata, event.createdAt);
|
|
@@ -837,7 +950,15 @@ class AuthoDatabase {
|
|
|
837
950
|
LIMIT ?1`).all(limit);
|
|
838
951
|
}
|
|
839
952
|
}
|
|
840
|
-
var
|
|
953
|
+
var PROTECTED_COLUMNS, SEAL_META_KEY = "vault.seal";
|
|
954
|
+
var init_src = __esm(() => {
|
|
955
|
+
PROTECTED_COLUMNS = {
|
|
956
|
+
leases: ["id", "name", "secret_refs", "expires_at", "revoked_at", "created_at", "include_links"],
|
|
957
|
+
meta: ["key", "value"],
|
|
958
|
+
secret_links: ["primary_id", "linked_id", "created_at"],
|
|
959
|
+
secrets: ["id", "name", "type", "payload", "wrapped_key", "created_at", "updated_at"]
|
|
960
|
+
};
|
|
961
|
+
});
|
|
841
962
|
|
|
842
963
|
// packages/crypto/src/index.ts
|
|
843
964
|
import {
|
|
@@ -889,7 +1010,10 @@ function decryptWithKey(blob, key, aad) {
|
|
|
889
1010
|
decipher.final()
|
|
890
1011
|
]);
|
|
891
1012
|
}
|
|
892
|
-
function
|
|
1013
|
+
function wrapRootKey(rootKey, passwordKey, sealed) {
|
|
1014
|
+
return encryptWithKey(rootKey, passwordKey, sealed ? SEALED_ROOT_AAD : ROOT_AAD);
|
|
1015
|
+
}
|
|
1016
|
+
function createVaultConfig(password, rootKey = randomBytes2(32), options) {
|
|
893
1017
|
const kdf = {
|
|
894
1018
|
...DEFAULT_KDF,
|
|
895
1019
|
salt: randomBytes2(16).toString("base64")
|
|
@@ -900,14 +1024,20 @@ function createVaultConfig(password, rootKey = randomBytes2(32)) {
|
|
|
900
1024
|
createdAt: new Date().toISOString(),
|
|
901
1025
|
kdf,
|
|
902
1026
|
version: 1,
|
|
903
|
-
wrappedRootKey:
|
|
1027
|
+
wrappedRootKey: wrapRootKey(rootKey, key, options?.sealed ?? false)
|
|
904
1028
|
},
|
|
905
1029
|
rootKey
|
|
906
1030
|
};
|
|
907
1031
|
}
|
|
1032
|
+
function unwrapRootKey(config, passwordKey) {
|
|
1033
|
+
try {
|
|
1034
|
+
return { rootKey: decryptWithKey(config.wrappedRootKey, passwordKey, SEALED_ROOT_AAD), sealed: true };
|
|
1035
|
+
} catch {
|
|
1036
|
+
return { rootKey: decryptWithKey(config.wrappedRootKey, passwordKey, ROOT_AAD), sealed: false };
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
908
1039
|
function unlockRootKey(password, config) {
|
|
909
|
-
|
|
910
|
-
return decryptWithKey(config.wrappedRootKey, key, "autho:vault-root");
|
|
1040
|
+
return unwrapRootKey(config, deriveKeyFromPassword(password, config.kdf)).rootKey;
|
|
911
1041
|
}
|
|
912
1042
|
function decodeBase32(input) {
|
|
913
1043
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
@@ -981,7 +1111,7 @@ function verifyTotpCode(secret, code, opts) {
|
|
|
981
1111
|
}
|
|
982
1112
|
return false;
|
|
983
1113
|
}
|
|
984
|
-
var DEFAULT_KDF;
|
|
1114
|
+
var DEFAULT_KDF, ROOT_AAD = "autho:vault-root", SEALED_ROOT_AAD = "autho:vault-root:sealed";
|
|
985
1115
|
var init_src2 = __esm(() => {
|
|
986
1116
|
DEFAULT_KDF = {
|
|
987
1117
|
keyLength: 32,
|
|
@@ -1209,6 +1339,182 @@ var init_artifacts = __esm(() => {
|
|
|
1209
1339
|
init_paths();
|
|
1210
1340
|
});
|
|
1211
1341
|
|
|
1342
|
+
// packages/core/src/seal.ts
|
|
1343
|
+
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1344
|
+
function sealKey(rootKey) {
|
|
1345
|
+
return deriveKeyFromSecret(rootKey, Buffer.from(`${SEAL_INFO}:v1`, "utf8"), SEAL_INFO);
|
|
1346
|
+
}
|
|
1347
|
+
function canonical(row) {
|
|
1348
|
+
return JSON.stringify(Object.keys(row).sort().map((key) => [key, row[key] ?? null]));
|
|
1349
|
+
}
|
|
1350
|
+
function computeRowMac(key, row) {
|
|
1351
|
+
return createHmac2("sha256", key).update(`${ROW_MAC_VERSION}\x00${row.tableName}\x00${row.rowId}\x00${canonical(row.row)}`).digest("base64");
|
|
1352
|
+
}
|
|
1353
|
+
function rowMac(key, row) {
|
|
1354
|
+
return { mac: computeRowMac(key, row), rowId: row.rowId, snapshot: JSON.stringify(row.row), tableName: row.tableName };
|
|
1355
|
+
}
|
|
1356
|
+
function rowKey(row) {
|
|
1357
|
+
return `${row.tableName}\x00${row.rowId}`;
|
|
1358
|
+
}
|
|
1359
|
+
function rootMac(key, seal, rows) {
|
|
1360
|
+
const lines = rows.map((row) => `${rowKey(row)}\x00${row.mac}`).sort();
|
|
1361
|
+
const header = seal.version === 1 ? "autho-seal:v1" : `autho-seal:v2
|
|
1362
|
+
${seal.vaultId}
|
|
1363
|
+
${seal.counter}`;
|
|
1364
|
+
return createHmac2("sha256", key).update(`${header}
|
|
1365
|
+
${lines.join(`
|
|
1366
|
+
`)}`).digest("base64");
|
|
1367
|
+
}
|
|
1368
|
+
function validSeal(seal) {
|
|
1369
|
+
if (seal.version === 1)
|
|
1370
|
+
return true;
|
|
1371
|
+
return seal.version === 2 && Number.isSafeInteger(seal.counter) && typeof seal.vaultId === "string" && seal.vaultId !== "";
|
|
1372
|
+
}
|
|
1373
|
+
function sameMac(a, b) {
|
|
1374
|
+
const left = Buffer.from(a, "utf8");
|
|
1375
|
+
const right = Buffer.from(b, "utf8");
|
|
1376
|
+
return left.length === right.length && timingSafeEqual2(left, right);
|
|
1377
|
+
}
|
|
1378
|
+
function readSeal(db) {
|
|
1379
|
+
const raw = db.getSeal();
|
|
1380
|
+
if (raw === null)
|
|
1381
|
+
return null;
|
|
1382
|
+
try {
|
|
1383
|
+
return JSON.parse(raw);
|
|
1384
|
+
} catch {
|
|
1385
|
+
return { mac: "", sealedAt: "", version: -1 };
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
function diffRows(stored, current) {
|
|
1389
|
+
const before = new Map(stored.map((row) => [rowKey(row), row]));
|
|
1390
|
+
const after = new Map(current.map((row) => [rowKey(row), row]));
|
|
1391
|
+
const changes = [];
|
|
1392
|
+
for (const [id, row] of after) {
|
|
1393
|
+
const old = before.get(id);
|
|
1394
|
+
if (!old)
|
|
1395
|
+
changes.push({ change: "added", rowId: row.rowId, table: row.tableName });
|
|
1396
|
+
else if (!sameMac(old.mac, row.mac))
|
|
1397
|
+
changes.push({ change: "modified", rowId: row.rowId, table: row.tableName });
|
|
1398
|
+
}
|
|
1399
|
+
for (const [id, row] of before) {
|
|
1400
|
+
if (!after.has(id))
|
|
1401
|
+
changes.push({ change: "removed", rowId: row.rowId, table: row.tableName });
|
|
1402
|
+
}
|
|
1403
|
+
return changes;
|
|
1404
|
+
}
|
|
1405
|
+
function verifySeal(db, key, expectSealed, record) {
|
|
1406
|
+
const stored = readSeal(db);
|
|
1407
|
+
if (!stored) {
|
|
1408
|
+
if (!expectSealed && !record)
|
|
1409
|
+
return { state: "unsealed" };
|
|
1410
|
+
return { changes: [], diffTrusted: false, reason: "the seal was removed", state: "tampered" };
|
|
1411
|
+
}
|
|
1412
|
+
if (!validSeal(stored)) {
|
|
1413
|
+
return { changes: [], diffTrusted: false, reason: "unknown seal format", state: "tampered" };
|
|
1414
|
+
}
|
|
1415
|
+
const current = db.listProtectedRows().map((row) => rowMac(key, row));
|
|
1416
|
+
if (sameMac(rootMac(key, stored, current), stored.mac)) {
|
|
1417
|
+
return rolledBack(stored, record) ? ROLLED_BACK : { state: "intact" };
|
|
1418
|
+
}
|
|
1419
|
+
const storedRows = db.listSealRows();
|
|
1420
|
+
const diffTrusted = sameMac(rootMac(key, stored, storedRows), stored.mac);
|
|
1421
|
+
return {
|
|
1422
|
+
changes: diffRows(storedRows, current),
|
|
1423
|
+
diffTrusted,
|
|
1424
|
+
reason: "rows changed without the vault key",
|
|
1425
|
+
state: "tampered"
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
function rolledBack(stored, record) {
|
|
1429
|
+
if (!record)
|
|
1430
|
+
return false;
|
|
1431
|
+
if (stored.vaultId !== undefined && stored.vaultId !== record.vaultId)
|
|
1432
|
+
return false;
|
|
1433
|
+
return (stored.counter ?? 0) < record.counter;
|
|
1434
|
+
}
|
|
1435
|
+
function currentSealRecord(db) {
|
|
1436
|
+
const stored = readSeal(db);
|
|
1437
|
+
if (!stored || stored.version !== 2 || !validSeal(stored))
|
|
1438
|
+
return null;
|
|
1439
|
+
return { counter: stored.counter, vaultId: stored.vaultId };
|
|
1440
|
+
}
|
|
1441
|
+
function writeSeal(db, key, options) {
|
|
1442
|
+
const previous = readSeal(db);
|
|
1443
|
+
const previousCounter = Number.isSafeInteger(previous?.counter) ? previous.counter : 0;
|
|
1444
|
+
const vaultId = typeof previous?.vaultId === "string" && previous.vaultId !== "" ? previous.vaultId : randomId();
|
|
1445
|
+
const counter = Math.max(previousCounter + 1, options?.minCounter ?? 0);
|
|
1446
|
+
const rows = db.listProtectedRows().map((row) => rowMac(key, row));
|
|
1447
|
+
const header = { counter, vaultId, version: SEAL_VERSION };
|
|
1448
|
+
const seal = { ...header, mac: rootMac(key, header, rows), sealedAt: new Date().toISOString() };
|
|
1449
|
+
db.setSeal(JSON.stringify(seal), rows);
|
|
1450
|
+
return { counter, vaultId };
|
|
1451
|
+
}
|
|
1452
|
+
function sealedSnapshot(key, sealRow) {
|
|
1453
|
+
if (!sealRow.snapshot)
|
|
1454
|
+
return null;
|
|
1455
|
+
let row;
|
|
1456
|
+
try {
|
|
1457
|
+
row = JSON.parse(sealRow.snapshot);
|
|
1458
|
+
} catch {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
if (typeof row !== "object" || row === null || Array.isArray(row))
|
|
1462
|
+
return null;
|
|
1463
|
+
const record = row;
|
|
1464
|
+
const mac = computeRowMac(key, { row: record, rowId: sealRow.rowId, tableName: sealRow.tableName });
|
|
1465
|
+
return sameMac(mac, sealRow.mac) ? record : null;
|
|
1466
|
+
}
|
|
1467
|
+
function unlockWithPassword(db, password, beforeUnwrap, record) {
|
|
1468
|
+
const config = db.getVaultConfig();
|
|
1469
|
+
if (!config) {
|
|
1470
|
+
throw new Error("Vault is not initialized");
|
|
1471
|
+
}
|
|
1472
|
+
const passwordKey = deriveKeyFromPassword(password, config.kdf);
|
|
1473
|
+
beforeUnwrap?.(passwordKey);
|
|
1474
|
+
const { rootKey, sealed } = unwrapRootKey(config, passwordKey);
|
|
1475
|
+
if (sealed) {
|
|
1476
|
+
return { rootKey, sealBound: true };
|
|
1477
|
+
}
|
|
1478
|
+
if (record) {
|
|
1479
|
+
return { rootKey, sealBound: false };
|
|
1480
|
+
}
|
|
1481
|
+
const sealedNow = db.immediateTransaction(() => {
|
|
1482
|
+
if (db.getSeal() !== null)
|
|
1483
|
+
return false;
|
|
1484
|
+
db.setVaultConfig({ ...config, wrappedRootKey: wrapRootKey(rootKey, passwordKey, true) });
|
|
1485
|
+
writeSeal(db, sealKey(rootKey));
|
|
1486
|
+
db.insertAudit({
|
|
1487
|
+
createdAt: new Date().toISOString(),
|
|
1488
|
+
eventType: "vault.sealed.initial",
|
|
1489
|
+
id: randomId(),
|
|
1490
|
+
message: "Vault sealed for the first time",
|
|
1491
|
+
metadata: JSON.stringify({}),
|
|
1492
|
+
subjectRef: null,
|
|
1493
|
+
subjectType: "vault"
|
|
1494
|
+
});
|
|
1495
|
+
return true;
|
|
1496
|
+
});
|
|
1497
|
+
return { rootKey, sealBound: sealedNow };
|
|
1498
|
+
}
|
|
1499
|
+
var ROW_MAC_VERSION = 1, SEAL_VERSION = 2, SEAL_INFO = "autho:vault-seal", VaultTamperedError, ROLLED_BACK;
|
|
1500
|
+
var init_seal = __esm(() => {
|
|
1501
|
+
init_src2();
|
|
1502
|
+
VaultTamperedError = class VaultTamperedError extends Error {
|
|
1503
|
+
seal;
|
|
1504
|
+
constructor(seal) {
|
|
1505
|
+
super(`Vault was modified outside autho (${seal.reason}). Run \`autho vault verify\` to see what changed, then \`autho vault reseal\`.`);
|
|
1506
|
+
this.seal = seal;
|
|
1507
|
+
this.name = "VaultTamperedError";
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
ROLLED_BACK = {
|
|
1511
|
+
changes: [],
|
|
1512
|
+
diffTrusted: false,
|
|
1513
|
+
reason: "the vault file was replaced with an older copy",
|
|
1514
|
+
state: "tampered"
|
|
1515
|
+
};
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1212
1518
|
// packages/core/src/settings.ts
|
|
1213
1519
|
function preferenceSpec(key) {
|
|
1214
1520
|
return PREFERENCES.find((spec) => spec.key === key);
|
|
@@ -1309,7 +1615,7 @@ var init_settings = __esm(() => {
|
|
|
1309
1615
|
|
|
1310
1616
|
// packages/core/src/index.ts
|
|
1311
1617
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
1312
|
-
import { createHmac as
|
|
1618
|
+
import { createHmac as createHmac3, randomBytes as randomBytes4 } from "crypto";
|
|
1313
1619
|
import {
|
|
1314
1620
|
existsSync as existsSync3,
|
|
1315
1621
|
readFileSync as readFileSync3
|
|
@@ -1365,7 +1671,7 @@ function generateTotp(secret, options, now = Date.now()) {
|
|
|
1365
1671
|
message[index] = cursor & 255;
|
|
1366
1672
|
cursor >>= 8;
|
|
1367
1673
|
}
|
|
1368
|
-
const hash =
|
|
1674
|
+
const hash = createHmac3(algorithm, Buffer.from(key)).update(message).digest();
|
|
1369
1675
|
const offset = hash[hash.length - 1] & 15;
|
|
1370
1676
|
const binary = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
|
|
1371
1677
|
const mod = 10 ** digits;
|
|
@@ -1391,6 +1697,7 @@ function parseLease(row) {
|
|
|
1391
1697
|
createdAt: row.createdAt,
|
|
1392
1698
|
expiresAt: row.expiresAt,
|
|
1393
1699
|
id: row.id,
|
|
1700
|
+
includeLinks: row.includeLinks !== 0,
|
|
1394
1701
|
name: row.name,
|
|
1395
1702
|
revokedAt: row.revokedAt,
|
|
1396
1703
|
secretRefs: JSON.parse(row.secretRefs)
|
|
@@ -1515,17 +1822,21 @@ class VaultService {
|
|
|
1515
1822
|
if (db.getVaultConfig()) {
|
|
1516
1823
|
throw new Error(`Vault already initialized at ${vaultPath}`);
|
|
1517
1824
|
}
|
|
1518
|
-
const { config } = createVaultConfig(password);
|
|
1519
|
-
db.
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1825
|
+
const { config, rootKey } = createVaultConfig(password, undefined, { sealed: true });
|
|
1826
|
+
db.immediateTransaction(() => {
|
|
1827
|
+
db.setVaultConfig(config);
|
|
1828
|
+
writeSeal(db, sealKey(rootKey));
|
|
1829
|
+
db.insertAudit({
|
|
1830
|
+
createdAt: new Date().toISOString(),
|
|
1831
|
+
eventType: "vault.initialized",
|
|
1832
|
+
id: randomId(),
|
|
1833
|
+
message: "Vault initialized",
|
|
1834
|
+
metadata: JSON.stringify({ version: config.version }),
|
|
1835
|
+
subjectRef: null,
|
|
1836
|
+
subjectType: "vault"
|
|
1837
|
+
});
|
|
1528
1838
|
});
|
|
1839
|
+
rootKey.fill(0);
|
|
1529
1840
|
return { vaultPath };
|
|
1530
1841
|
} finally {
|
|
1531
1842
|
db.close();
|
|
@@ -1593,7 +1904,7 @@ class VaultService {
|
|
|
1593
1904
|
subjectRef: null,
|
|
1594
1905
|
subjectType: "vault"
|
|
1595
1906
|
});
|
|
1596
|
-
return new VaultSession(db, rootKey);
|
|
1907
|
+
return new VaultSession(db, rootKey, { sealRecord: creds.sealRecord });
|
|
1597
1908
|
} catch (error) {
|
|
1598
1909
|
db.close();
|
|
1599
1910
|
throw new Error("Invalid recovery token", { cause: error });
|
|
@@ -1617,22 +1928,15 @@ class VaultService {
|
|
|
1617
1928
|
subjectRef: passkey.id,
|
|
1618
1929
|
subjectType: "vault"
|
|
1619
1930
|
});
|
|
1620
|
-
return new VaultSession(db, rootKey);
|
|
1931
|
+
return new VaultSession(db, rootKey, { sealRecord: creds.sealRecord });
|
|
1621
1932
|
} catch (error) {
|
|
1622
1933
|
db.close();
|
|
1623
1934
|
throw new Error("Passkey did not unlock the vault", { cause: error });
|
|
1624
1935
|
}
|
|
1625
1936
|
}
|
|
1626
1937
|
try {
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1629
|
-
const totpSecret = decryptWithKey(vaultAuth.totp.encryptedSecret, passwordKEK, "autho:vault-totp").toString("utf8");
|
|
1630
|
-
if (!creds.totp || !verifyTotpCode(totpSecret, creds.totp)) {
|
|
1631
|
-
throw new Error("Invalid or missing TOTP code");
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
const rootKey = decryptWithKey(config.wrappedRootKey, passwordKEK, "autho:vault-root");
|
|
1635
|
-
return new VaultSession(db, rootKey);
|
|
1938
|
+
const { rootKey, sealBound } = unlockWithPassword(db, creds.password, (passwordKEK) => checkVaultTotp(vaultAuth, passwordKEK, creds.totp), creds.sealRecord);
|
|
1939
|
+
return new VaultSession(db, rootKey, { sealBound, sealRecord: creds.sealRecord });
|
|
1636
1940
|
} catch (error) {
|
|
1637
1941
|
db.close();
|
|
1638
1942
|
if (error instanceof Error && error.message === "Invalid or missing TOTP code") {
|
|
@@ -1657,9 +1961,25 @@ class VaultService {
|
|
|
1657
1961
|
static authorize(vaultPath, credentials) {
|
|
1658
1962
|
const session = VaultService.unlock(vaultPath, credentials);
|
|
1659
1963
|
const rootKey = session.getRootKey();
|
|
1964
|
+
const sealBound = session.sealBound;
|
|
1660
1965
|
session.close();
|
|
1661
1966
|
const viaPassword = !credentials.passkey && !credentials.recovery;
|
|
1662
|
-
return new VaultAdmin(vaultPath, rootKey, viaPassword ? credentials.password : null);
|
|
1967
|
+
return new VaultAdmin(vaultPath, rootKey, viaPassword ? credentials.password : null, sealBound, credentials.sealRecord);
|
|
1968
|
+
}
|
|
1969
|
+
static unlockRootKey(vaultPath, credentials) {
|
|
1970
|
+
const db = new AuthoDatabase(vaultPath);
|
|
1971
|
+
try {
|
|
1972
|
+
if (!db.getVaultConfig()) {
|
|
1973
|
+
throw new Error(`Vault is not initialized at ${vaultPath}`);
|
|
1974
|
+
}
|
|
1975
|
+
const vaultAuth = db.getVaultAuth();
|
|
1976
|
+
return unlockWithPassword(db, credentials.password, (passwordKEK) => checkVaultTotp(vaultAuth, passwordKEK, credentials.totp), credentials.sealRecord);
|
|
1977
|
+
} finally {
|
|
1978
|
+
db.close();
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
static reseal(vaultPath, credentials, mode) {
|
|
1982
|
+
return VaultService.withAdmin(vaultPath, credentials, (admin) => admin.reseal(mode));
|
|
1663
1983
|
}
|
|
1664
1984
|
static withAdmin(vaultPath, credentials, fn) {
|
|
1665
1985
|
const admin = VaultService.authorize(vaultPath, credentials);
|
|
@@ -1695,6 +2015,14 @@ class VaultService {
|
|
|
1695
2015
|
return VaultService.withAdmin(vaultPath, credentials, (admin) => admin.removePasskey(ref));
|
|
1696
2016
|
}
|
|
1697
2017
|
}
|
|
2018
|
+
function checkVaultTotp(vaultAuth, passwordKEK, totp) {
|
|
2019
|
+
if (!vaultAuth?.totp)
|
|
2020
|
+
return;
|
|
2021
|
+
const totpSecret = decryptWithKey(vaultAuth.totp.encryptedSecret, passwordKEK, "autho:vault-totp").toString("utf8");
|
|
2022
|
+
if (!totp || !verifyTotpCode(totpSecret, totp)) {
|
|
2023
|
+
throw new Error("Invalid or missing TOTP code");
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
1698
2026
|
function assertTotpCode(secret, code) {
|
|
1699
2027
|
if (!verifyTotpCode(secret, code)) {
|
|
1700
2028
|
throw new Error("Invalid TOTP code \u2014 make sure your authenticator app is showing the right code");
|
|
@@ -1705,11 +2033,18 @@ class VaultAdmin {
|
|
|
1705
2033
|
vaultPath;
|
|
1706
2034
|
rootKey;
|
|
1707
2035
|
password;
|
|
2036
|
+
sealBound;
|
|
2037
|
+
record;
|
|
1708
2038
|
closed = false;
|
|
1709
|
-
constructor(vaultPath, rootKey, password) {
|
|
2039
|
+
constructor(vaultPath, rootKey, password, sealBound = false, record = null) {
|
|
1710
2040
|
this.vaultPath = vaultPath;
|
|
1711
2041
|
this.rootKey = rootKey;
|
|
1712
2042
|
this.password = password;
|
|
2043
|
+
this.sealBound = sealBound;
|
|
2044
|
+
this.record = record;
|
|
2045
|
+
}
|
|
2046
|
+
sealRecord() {
|
|
2047
|
+
return this.record;
|
|
1713
2048
|
}
|
|
1714
2049
|
get hasPassword() {
|
|
1715
2050
|
return this.password !== null;
|
|
@@ -1752,9 +2087,9 @@ class VaultAdmin {
|
|
|
1752
2087
|
if (newPassword.length < MIN_PASSWORD_LENGTH) {
|
|
1753
2088
|
throw new Error(`The new password must be at least ${MIN_PASSWORD_LENGTH} characters`);
|
|
1754
2089
|
}
|
|
1755
|
-
this.write((db) => {
|
|
2090
|
+
this.write((db, sealed) => {
|
|
1756
2091
|
const auth = db.getVaultAuth();
|
|
1757
|
-
const { config } = createVaultConfig(newPassword, this.liveRootKey());
|
|
2092
|
+
const { config } = createVaultConfig(newPassword, this.liveRootKey(), { sealed });
|
|
1758
2093
|
if (auth?.totp) {
|
|
1759
2094
|
const oldPassword = this.requirePassword("Two-factor codes are on: authorize with the current master password to change it");
|
|
1760
2095
|
const oldKEK = deriveKeyFromPassword(oldPassword, db.getVaultConfig().kdf);
|
|
@@ -1857,16 +2192,116 @@ class VaultAdmin {
|
|
|
1857
2192
|
}
|
|
1858
2193
|
return this.password;
|
|
1859
2194
|
}
|
|
2195
|
+
reseal(mode) {
|
|
2196
|
+
const rootKey = this.liveRootKey();
|
|
2197
|
+
const key = sealKey(rootKey);
|
|
2198
|
+
const db = new AuthoDatabase(this.vaultPath);
|
|
2199
|
+
try {
|
|
2200
|
+
return db.immediateTransaction(() => {
|
|
2201
|
+
const seal = verifySeal(db, key, true, this.record);
|
|
2202
|
+
const result = {
|
|
2203
|
+
changes: seal.state === "tampered" ? seal.changes : [],
|
|
2204
|
+
diffTrusted: seal.state === "tampered" ? seal.diffTrusted : true,
|
|
2205
|
+
kept: [],
|
|
2206
|
+
mode,
|
|
2207
|
+
reason: seal.state === "tampered" ? seal.reason : seal.state === "unsealed" ? "unsealed" : null,
|
|
2208
|
+
rebound: false,
|
|
2209
|
+
revokedLeases: 0,
|
|
2210
|
+
sealRecord: null,
|
|
2211
|
+
undone: [],
|
|
2212
|
+
wasIntact: seal.state === "intact"
|
|
2213
|
+
};
|
|
2214
|
+
if (seal.state === "intact")
|
|
2215
|
+
return result;
|
|
2216
|
+
if (mode === "quarantine") {
|
|
2217
|
+
Object.assign(result, quarantineChanges(db, key, result.changes, result.diffTrusted));
|
|
2218
|
+
result.revokedLeases = db.revokeActiveLeases(new Date().toISOString());
|
|
2219
|
+
} else {
|
|
2220
|
+
result.kept = result.changes;
|
|
2221
|
+
}
|
|
2222
|
+
if (this.password !== null) {
|
|
2223
|
+
const config = db.getVaultConfig();
|
|
2224
|
+
const passwordKey = deriveKeyFromPassword(this.password, config.kdf);
|
|
2225
|
+
if (!unwrapRootKey(config, passwordKey).sealed) {
|
|
2226
|
+
db.setVaultConfig({ ...config, wrappedRootKey: wrapRootKey(rootKey, passwordKey, true) });
|
|
2227
|
+
result.rebound = true;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
result.sealRecord = writeSeal(db, key, { minCounter: this.record ? this.record.counter + 1 : 0 });
|
|
2231
|
+
this.record = result.sealRecord;
|
|
2232
|
+
audit(db, `vault.reseal.${mode}`, mode === "accept" ? "Vault re-sealed as is" : "Vault re-sealed after quarantine", {
|
|
2233
|
+
changes: result.changes.slice(0, 200),
|
|
2234
|
+
diffTrusted: result.diffTrusted,
|
|
2235
|
+
kept: result.kept.length,
|
|
2236
|
+
reason: result.reason,
|
|
2237
|
+
revokedLeases: result.revokedLeases,
|
|
2238
|
+
undone: result.undone.length
|
|
2239
|
+
});
|
|
2240
|
+
return result;
|
|
2241
|
+
});
|
|
2242
|
+
} finally {
|
|
2243
|
+
key.fill(0);
|
|
2244
|
+
db.close();
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
1860
2247
|
write(fn) {
|
|
1861
|
-
this.liveRootKey();
|
|
2248
|
+
const key = sealKey(this.liveRootKey());
|
|
1862
2249
|
const db = new AuthoDatabase(this.vaultPath);
|
|
1863
2250
|
try {
|
|
1864
|
-
return db.
|
|
2251
|
+
return db.immediateTransaction(() => {
|
|
2252
|
+
const seal = verifySeal(db, key, this.sealBound, this.record);
|
|
2253
|
+
if (seal.state === "tampered")
|
|
2254
|
+
throw new VaultTamperedError(seal);
|
|
2255
|
+
const result = fn(db, seal.state === "intact");
|
|
2256
|
+
if (seal.state === "intact")
|
|
2257
|
+
this.record = writeSeal(db, key);
|
|
2258
|
+
return result;
|
|
2259
|
+
});
|
|
1865
2260
|
} finally {
|
|
2261
|
+
key.fill(0);
|
|
1866
2262
|
db.close();
|
|
1867
2263
|
}
|
|
1868
2264
|
}
|
|
1869
2265
|
}
|
|
2266
|
+
function quarantineChanges(db, key, changes, diffTrusted) {
|
|
2267
|
+
const sealed = new Map(db.listSealRows().map((row) => [`${row.tableName}\x00${row.rowId}`, row]));
|
|
2268
|
+
const current = new Map(db.listProtectedRows().map((row) => [`${row.tableName}\x00${row.rowId}`, row.row]));
|
|
2269
|
+
const undone = [];
|
|
2270
|
+
const kept = [];
|
|
2271
|
+
const restores = [];
|
|
2272
|
+
const deletes = [];
|
|
2273
|
+
for (const change of changes) {
|
|
2274
|
+
const sealedRow = sealed.get(`${change.table}\x00${change.rowId}`);
|
|
2275
|
+
const snapshot = diffTrusted && change.change !== "added" && sealedRow ? sealedSnapshot(key, sealedRow) : null;
|
|
2276
|
+
if (snapshot) {
|
|
2277
|
+
if (change.change === "modified")
|
|
2278
|
+
deletes.push(change);
|
|
2279
|
+
restores.push({ change, row: snapshot });
|
|
2280
|
+
} else if (change.change === "added" && diffTrusted) {
|
|
2281
|
+
deletes.push(change);
|
|
2282
|
+
undone.push(change);
|
|
2283
|
+
} else if (change.change !== "removed" && ACCESS_TABLES.has(change.table)) {
|
|
2284
|
+
deletes.push(change);
|
|
2285
|
+
undone.push(change);
|
|
2286
|
+
} else {
|
|
2287
|
+
kept.push(change);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
for (const change of deletes)
|
|
2291
|
+
db.deleteProtectedRow(change.table, change.rowId);
|
|
2292
|
+
for (const { change, row } of restores) {
|
|
2293
|
+
try {
|
|
2294
|
+
db.insertProtectedRow(change.table, row);
|
|
2295
|
+
undone.push(change);
|
|
2296
|
+
} catch {
|
|
2297
|
+
const modified = change.change === "modified" ? current.get(`${change.table}\x00${change.rowId}`) : undefined;
|
|
2298
|
+
if (modified)
|
|
2299
|
+
db.insertProtectedRow(change.table, modified);
|
|
2300
|
+
kept.push(change);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return { kept, undone };
|
|
2304
|
+
}
|
|
1870
2305
|
function audit(db, eventType, message, metadata = {}, subjectRef = null) {
|
|
1871
2306
|
db.insertAudit({
|
|
1872
2307
|
createdAt: new Date().toISOString(),
|
|
@@ -1903,16 +2338,54 @@ function recoveryFileContent(vaultPath, token) {
|
|
|
1903
2338
|
class VaultSession {
|
|
1904
2339
|
db;
|
|
1905
2340
|
rootKey;
|
|
1906
|
-
|
|
2341
|
+
sealKey;
|
|
2342
|
+
sealBound;
|
|
2343
|
+
sealRecord;
|
|
2344
|
+
constructor(db, rootKey, options = {}) {
|
|
1907
2345
|
this.db = db;
|
|
1908
2346
|
this.rootKey = rootKey;
|
|
2347
|
+
this.sealKey = sealKey(rootKey);
|
|
2348
|
+
this.sealBound = options.sealBound ?? false;
|
|
2349
|
+
this.sealRecord = options.sealRecord ?? null;
|
|
2350
|
+
}
|
|
2351
|
+
sealCheckpoint() {
|
|
2352
|
+
return this.verifySeal().state === "intact" ? currentSealRecord(this.db) : null;
|
|
1909
2353
|
}
|
|
1910
2354
|
getRootKey() {
|
|
1911
2355
|
return Buffer.from(this.rootKey);
|
|
1912
2356
|
}
|
|
1913
2357
|
close() {
|
|
2358
|
+
this.sealKey.fill(0);
|
|
1914
2359
|
this.db.close();
|
|
1915
2360
|
}
|
|
2361
|
+
verifySeal() {
|
|
2362
|
+
return verifySeal(this.db, this.sealKey, this.sealBound, this.sealRecord);
|
|
2363
|
+
}
|
|
2364
|
+
sealedWrite(fn, options) {
|
|
2365
|
+
if (this.db.inTransaction) {
|
|
2366
|
+
return fn();
|
|
2367
|
+
}
|
|
2368
|
+
return this.db.immediateTransaction(() => {
|
|
2369
|
+
const seal = this.verifySeal();
|
|
2370
|
+
if (seal.state === "tampered" && !options?.allowTampered) {
|
|
2371
|
+
throw new VaultTamperedError(seal);
|
|
2372
|
+
}
|
|
2373
|
+
const result = fn();
|
|
2374
|
+
if (seal.state === "intact") {
|
|
2375
|
+
writeSeal(this.db, this.sealKey);
|
|
2376
|
+
}
|
|
2377
|
+
return result;
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
requireIntactSeal() {
|
|
2381
|
+
const seal = this.verifySeal();
|
|
2382
|
+
if (seal.state === "tampered") {
|
|
2383
|
+
throw new VaultTamperedError(seal);
|
|
2384
|
+
}
|
|
2385
|
+
if (seal.state === "unsealed") {
|
|
2386
|
+
throw new Error("This vault isn't sealed yet: unlock it once with the master password, then retry");
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
1916
2389
|
status(vaultPath, projectFile, projectMappings = []) {
|
|
1917
2390
|
return {
|
|
1918
2391
|
activeLeaseCount: this.db.countActiveLeases(new Date().toISOString()),
|
|
@@ -1966,26 +2439,40 @@ class VaultSession {
|
|
|
1966
2439
|
}
|
|
1967
2440
|
return parseLease(row);
|
|
1968
2441
|
}
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
return;
|
|
1972
|
-
}
|
|
1973
|
-
const lease = this.getLeaseOrThrow(leaseId);
|
|
2442
|
+
getActiveLeaseOrThrow(id) {
|
|
2443
|
+
const lease = this.getLeaseOrThrow(id);
|
|
1974
2444
|
if (lease.revokedAt) {
|
|
1975
2445
|
throw new Error(`Lease revoked: ${lease.id}`);
|
|
1976
2446
|
}
|
|
1977
2447
|
if (Date.parse(lease.expiresAt) <= Date.now()) {
|
|
1978
2448
|
throw new Error(`Lease expired: ${lease.id}`);
|
|
1979
2449
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2450
|
+
return lease;
|
|
2451
|
+
}
|
|
2452
|
+
linkedSecret(id) {
|
|
2453
|
+
const row = this.db.findSecret(id);
|
|
2454
|
+
return row ? { id: row.id, name: row.name, type: normalizeSecretType(row.type) } : null;
|
|
2455
|
+
}
|
|
2456
|
+
leaseAccess(lease, secret) {
|
|
2457
|
+
if (lease.secretRefs.includes(secret.id) || lease.secretRefs.includes(secret.name)) {
|
|
2458
|
+
return { via: null };
|
|
2459
|
+
}
|
|
2460
|
+
if (lease.includeLinks) {
|
|
2461
|
+
for (const link of this.db.listLinksTo(secret.id)) {
|
|
2462
|
+
if (lease.secretRefs.includes(link.primaryId)) {
|
|
2463
|
+
return { via: this.linkedSecret(link.primaryId) };
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
1983
2466
|
}
|
|
2467
|
+
throw new Error(`Lease ${lease.id} does not allow secret ${secret.name}`);
|
|
1984
2468
|
}
|
|
1985
2469
|
addSecret(input) {
|
|
1986
2470
|
requireValue(input.name, "--name");
|
|
1987
2471
|
requireValue(input.value, "--value");
|
|
1988
2472
|
const type = normalizeSecretType(input.type);
|
|
2473
|
+
return this.sealedWrite(() => this.insertNewSecret(input, type));
|
|
2474
|
+
}
|
|
2475
|
+
insertNewSecret(input, type) {
|
|
1989
2476
|
if (this.db.findSecret(input.name)) {
|
|
1990
2477
|
throw new Error(`Secret already exists: ${input.name}`);
|
|
1991
2478
|
}
|
|
@@ -2020,6 +2507,9 @@ class VaultSession {
|
|
|
2020
2507
|
};
|
|
2021
2508
|
}
|
|
2022
2509
|
updateSecret(ref, updates) {
|
|
2510
|
+
return this.sealedWrite(() => this.applySecretUpdate(ref, updates));
|
|
2511
|
+
}
|
|
2512
|
+
applySecretUpdate(ref, updates) {
|
|
2023
2513
|
const existing = this.getSecretOrThrow(ref);
|
|
2024
2514
|
if (updates.name && updates.name !== existing.name) {
|
|
2025
2515
|
const conflict = this.db.findSecret(updates.name);
|
|
@@ -2059,6 +2549,9 @@ class VaultSession {
|
|
|
2059
2549
|
}
|
|
2060
2550
|
importLegacyFile(filePath, options) {
|
|
2061
2551
|
const raw = JSON.parse(readFileSync3(filePath, "utf8"));
|
|
2552
|
+
return this.sealedWrite(() => this.importLegacyEntries(raw, options));
|
|
2553
|
+
}
|
|
2554
|
+
importLegacyEntries(raw, options) {
|
|
2062
2555
|
let imported = 0;
|
|
2063
2556
|
let skipped = 0;
|
|
2064
2557
|
for (const entry of raw) {
|
|
@@ -2092,6 +2585,10 @@ class VaultSession {
|
|
|
2092
2585
|
}));
|
|
2093
2586
|
}
|
|
2094
2587
|
listSecretIndex() {
|
|
2588
|
+
const linkCounts = new Map;
|
|
2589
|
+
for (const link of this.db.listLinks()) {
|
|
2590
|
+
linkCounts.set(link.primaryId, (linkCounts.get(link.primaryId) ?? 0) + 1);
|
|
2591
|
+
}
|
|
2095
2592
|
const entries = this.db.listSecrets().map((row) => {
|
|
2096
2593
|
const secret = this.unwrapSecret(row);
|
|
2097
2594
|
const { url, description } = secret.metadata;
|
|
@@ -2099,6 +2596,7 @@ class VaultSession {
|
|
|
2099
2596
|
createdAt: secret.createdAt,
|
|
2100
2597
|
description: typeof description === "string" && description ? description : null,
|
|
2101
2598
|
id: secret.id,
|
|
2599
|
+
linkCount: linkCounts.get(secret.id) ?? 0,
|
|
2102
2600
|
name: secret.name,
|
|
2103
2601
|
type: secret.type,
|
|
2104
2602
|
updatedAt: secret.updatedAt,
|
|
@@ -2117,33 +2615,128 @@ class VaultSession {
|
|
|
2117
2615
|
});
|
|
2118
2616
|
return secret;
|
|
2119
2617
|
}
|
|
2120
|
-
removeSecret(ref) {
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2618
|
+
removeSecret(ref, options) {
|
|
2619
|
+
return this.sealedWrite(() => {
|
|
2620
|
+
const secret = this.getSecretOrThrow(ref);
|
|
2621
|
+
const removedLinked = [];
|
|
2622
|
+
const keptLinked = [];
|
|
2623
|
+
if (options?.withLinked) {
|
|
2624
|
+
for (const link of this.db.listLinksFrom(secret.id)) {
|
|
2625
|
+
const linked = this.linkedSecret(link.linkedId);
|
|
2626
|
+
if (!linked)
|
|
2627
|
+
continue;
|
|
2628
|
+
const otherPrimaries = this.db.listLinksTo(linked.id).filter((l) => l.primaryId !== secret.id);
|
|
2629
|
+
(otherPrimaries.length > 0 ? keptLinked : removedLinked).push(linked);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
for (const target of [{ id: secret.id, type: secret.type }, ...removedLinked]) {
|
|
2633
|
+
this.db.deleteSecret(target.id);
|
|
2634
|
+
this.audit("secret.deleted", "secret", target.id, "Secret deleted", {
|
|
2635
|
+
type: target.type,
|
|
2636
|
+
...target.id === secret.id ? {} : { withPrimary: secret.id }
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
return { id: secret.id, keptLinked, name: secret.name, removedLinked };
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
linkSecrets(primaryRef, linkedRef) {
|
|
2643
|
+
return this.sealedWrite(() => {
|
|
2644
|
+
const primary = this.requireLinkedSecret(primaryRef);
|
|
2645
|
+
const linked = this.requireLinkedSecret(linkedRef);
|
|
2646
|
+
if (primary.id === linked.id) {
|
|
2647
|
+
throw new Error("A secret can't be linked to itself");
|
|
2648
|
+
}
|
|
2649
|
+
if (this.db.hasLink(primary.id, linked.id)) {
|
|
2650
|
+
return { created: false, linked, primary };
|
|
2651
|
+
}
|
|
2652
|
+
this.db.insertLink({ createdAt: new Date().toISOString(), linkedId: linked.id, primaryId: primary.id });
|
|
2653
|
+
this.audit("secret.linked", "secret", primary.id, `Linked ${linked.name} under ${primary.name}`, {
|
|
2654
|
+
linkedId: linked.id,
|
|
2655
|
+
primaryId: primary.id
|
|
2656
|
+
});
|
|
2657
|
+
return { created: true, linked, primary };
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
unlinkSecrets(primaryRef, linkedRef) {
|
|
2661
|
+
return this.sealedWrite(() => {
|
|
2662
|
+
const primary = this.requireLinkedSecret(primaryRef);
|
|
2663
|
+
const linked = this.requireLinkedSecret(linkedRef);
|
|
2664
|
+
if (!this.db.deleteLink(primary.id, linked.id)) {
|
|
2665
|
+
throw new Error(`${linked.name} is not linked under ${primary.name}`);
|
|
2666
|
+
}
|
|
2667
|
+
this.audit("secret.unlinked", "secret", primary.id, `Unlinked ${linked.name} from ${primary.name}`, {
|
|
2668
|
+
linkedId: linked.id,
|
|
2669
|
+
primaryId: primary.id
|
|
2670
|
+
});
|
|
2671
|
+
return { linked, primary };
|
|
2125
2672
|
});
|
|
2126
|
-
return { id: secret.id, name: secret.name };
|
|
2127
2673
|
}
|
|
2128
|
-
|
|
2674
|
+
getLinks(ref) {
|
|
2675
|
+
const secret = this.requireLinkedSecret(ref);
|
|
2676
|
+
const resolve = (ids) => ids.map((id) => this.linkedSecret(id)).filter((entry) => entry !== null);
|
|
2677
|
+
return {
|
|
2678
|
+
linkedFrom: resolve(this.db.listLinksTo(secret.id).map((link) => link.primaryId)),
|
|
2679
|
+
links: resolve(this.db.listLinksFrom(secret.id).map((link) => link.linkedId))
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
requireLinkedSecret(ref) {
|
|
2683
|
+
const row = this.db.findSecret(ref);
|
|
2684
|
+
if (!row) {
|
|
2685
|
+
throw new Error(`Secret not found: ${ref}`);
|
|
2686
|
+
}
|
|
2687
|
+
return { id: row.id, name: row.name, type: normalizeSecretType(row.type) };
|
|
2688
|
+
}
|
|
2689
|
+
generateOtp(ref, options) {
|
|
2129
2690
|
const secret = this.getSecretOrThrow(ref);
|
|
2130
2691
|
if (secret.type !== "otp") {
|
|
2131
2692
|
throw new Error(`Secret is not an OTP secret: ${ref}`);
|
|
2132
2693
|
}
|
|
2133
|
-
const result =
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
});
|
|
2694
|
+
const result = this.otpCode(secret);
|
|
2695
|
+
if (options?.audit ?? true) {
|
|
2696
|
+
this.audit("otp.generated", "secret", secret.id, "OTP code generated", {
|
|
2697
|
+
expiresAt: result.expiresAt
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2140
2700
|
return {
|
|
2141
2701
|
code: result.code,
|
|
2142
2702
|
expiresAt: result.expiresAt,
|
|
2143
2703
|
secret: secret.name
|
|
2144
2704
|
};
|
|
2145
2705
|
}
|
|
2706
|
+
otpCode(secret) {
|
|
2707
|
+
return generateTotp(secret.value, {
|
|
2708
|
+
algorithm: secret.metadata.algorithm,
|
|
2709
|
+
digits: secret.metadata.digits
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
showLease(id) {
|
|
2713
|
+
const lease = this.getLeaseOrThrow(id);
|
|
2714
|
+
const coverage = [];
|
|
2715
|
+
const seen = new Set;
|
|
2716
|
+
for (const ref of lease.secretRefs) {
|
|
2717
|
+
const direct = this.linkedSecret(ref);
|
|
2718
|
+
if (!direct || seen.has(direct.id))
|
|
2719
|
+
continue;
|
|
2720
|
+
seen.add(direct.id);
|
|
2721
|
+
coverage.push({ ...direct, via: null });
|
|
2722
|
+
}
|
|
2723
|
+
if (lease.includeLinks) {
|
|
2724
|
+
for (const primary of [...coverage]) {
|
|
2725
|
+
for (const link of this.db.listLinksFrom(primary.id)) {
|
|
2726
|
+
const linked = this.linkedSecret(link.linkedId);
|
|
2727
|
+
if (!linked || seen.has(linked.id))
|
|
2728
|
+
continue;
|
|
2729
|
+
seen.add(linked.id);
|
|
2730
|
+
coverage.push({ ...linked, via: { id: primary.id, name: primary.name, type: primary.type } });
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
return { coverage, lease };
|
|
2735
|
+
}
|
|
2146
2736
|
createLease(input) {
|
|
2737
|
+
return this.sealedWrite(() => this.insertLease(input));
|
|
2738
|
+
}
|
|
2739
|
+
insertLease(input) {
|
|
2147
2740
|
if (input.secretRefs.length === 0) {
|
|
2148
2741
|
throw new Error("Lease requires at least one --secret");
|
|
2149
2742
|
}
|
|
@@ -2157,6 +2750,7 @@ class VaultSession {
|
|
|
2157
2750
|
createdAt: now,
|
|
2158
2751
|
expiresAt,
|
|
2159
2752
|
id: randomId(),
|
|
2753
|
+
includeLinks: !input.noLinks,
|
|
2160
2754
|
name: input.name || "session",
|
|
2161
2755
|
revokedAt: null,
|
|
2162
2756
|
secretRefs: resolved.map((secret) => secret.id)
|
|
@@ -2165,39 +2759,55 @@ class VaultSession {
|
|
|
2165
2759
|
createdAt: lease.createdAt,
|
|
2166
2760
|
expiresAt: lease.expiresAt,
|
|
2167
2761
|
id: lease.id,
|
|
2762
|
+
includeLinks: lease.includeLinks ? 1 : 0,
|
|
2168
2763
|
name: lease.name,
|
|
2169
2764
|
revokedAt: lease.revokedAt,
|
|
2170
2765
|
secretRefs: JSON.stringify(lease.secretRefs)
|
|
2171
2766
|
});
|
|
2172
2767
|
this.audit("lease.created", "lease", lease.id, "Lease created", {
|
|
2173
2768
|
expiresAt,
|
|
2769
|
+
includeLinks: lease.includeLinks,
|
|
2174
2770
|
secretCount: lease.secretRefs.length
|
|
2175
2771
|
});
|
|
2176
2772
|
return lease;
|
|
2177
2773
|
}
|
|
2178
2774
|
revokeLease(id) {
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
id
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2775
|
+
return this.sealedWrite(() => {
|
|
2776
|
+
const lease = this.getLeaseOrThrow(id);
|
|
2777
|
+
const revokedAt = new Date().toISOString();
|
|
2778
|
+
this.db.revokeLease(id, revokedAt);
|
|
2779
|
+
this.audit("lease.revoked", "lease", id, "Lease revoked", {
|
|
2780
|
+
id
|
|
2781
|
+
});
|
|
2782
|
+
return {
|
|
2783
|
+
...lease,
|
|
2784
|
+
revokedAt
|
|
2785
|
+
};
|
|
2786
|
+
}, { allowTampered: true });
|
|
2189
2787
|
}
|
|
2190
2788
|
buildEnv(mappings, leaseId) {
|
|
2191
2789
|
if (mappings.length === 0) {
|
|
2192
2790
|
throw new Error("Provide at least one env mapping");
|
|
2193
2791
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
this.
|
|
2197
|
-
const
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2792
|
+
return this.db.immediateTransaction(() => {
|
|
2793
|
+
this.requireIntactSeal();
|
|
2794
|
+
const lease = leaseId ? this.getActiveLeaseOrThrow(leaseId) : null;
|
|
2795
|
+
const output = {};
|
|
2796
|
+
for (const mapping of mappings) {
|
|
2797
|
+
const secret = this.getSecretOrThrow(mapping.secretRef);
|
|
2798
|
+
if (lease) {
|
|
2799
|
+
const { via } = this.leaseAccess(lease, secret);
|
|
2800
|
+
if (via) {
|
|
2801
|
+
this.audit("lease.link_access", "secret", secret.id, `Secret read via link from ${via.name}`, {
|
|
2802
|
+
leaseId: lease.id,
|
|
2803
|
+
primaryId: via.id
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
output[mapping.envName] = secret.type === "otp" ? this.otpCode(secret).code : secret.value;
|
|
2808
|
+
}
|
|
2809
|
+
return output;
|
|
2810
|
+
});
|
|
2201
2811
|
}
|
|
2202
2812
|
renderEnv(mappings, leaseId) {
|
|
2203
2813
|
const env = this.buildEnv(mappings, leaseId);
|
|
@@ -2312,21 +2922,24 @@ class VaultSession {
|
|
|
2312
2922
|
return this.db.listAudit(limit).map(toAuditEvent);
|
|
2313
2923
|
}
|
|
2314
2924
|
}
|
|
2315
|
-
var PASSKEY_INFO = "autho:vault-passkey", MIN_PASSWORD_LENGTH = 8;
|
|
2925
|
+
var PASSKEY_INFO = "autho:vault-passkey", MIN_PASSWORD_LENGTH = 8, ACCESS_TABLES;
|
|
2316
2926
|
var init_src3 = __esm(() => {
|
|
2317
2927
|
init_src2();
|
|
2318
2928
|
init_artifacts();
|
|
2319
2929
|
init_src();
|
|
2930
|
+
init_seal();
|
|
2320
2931
|
init_paths();
|
|
2932
|
+
init_seal();
|
|
2321
2933
|
init_paths();
|
|
2322
2934
|
init_settings();
|
|
2935
|
+
ACCESS_TABLES = new Set(["leases", "secret_links"]);
|
|
2323
2936
|
});
|
|
2324
2937
|
|
|
2325
2938
|
// packages/core/src/os-secrets.ts
|
|
2326
|
-
import { createHash as
|
|
2939
|
+
import { createHash as createHash2, randomBytes as randomBytes5, scryptSync as scryptSync2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
2327
2940
|
import { resolve as resolve3 } from "path";
|
|
2328
2941
|
function vaultPasswordName(vaultPath) {
|
|
2329
|
-
return
|
|
2942
|
+
return createHash2("sha256").update(resolve3(vaultPath)).digest("hex");
|
|
2330
2943
|
}
|
|
2331
2944
|
function osSecretsDisabled() {
|
|
2332
2945
|
return process.env.AUTHO_DISABLE_OS_SECRETS === "1";
|
|
@@ -2373,6 +2986,41 @@ async function deleteVaultPassword(vaultPath) {
|
|
|
2373
2986
|
return false;
|
|
2374
2987
|
}
|
|
2375
2988
|
}
|
|
2989
|
+
async function loadSealRecord(vaultPath) {
|
|
2990
|
+
if (osSecretsDisabled()) {
|
|
2991
|
+
return null;
|
|
2992
|
+
}
|
|
2993
|
+
try {
|
|
2994
|
+
const raw = await Bun.secrets.get({ name: vaultPasswordName(vaultPath), service: SEAL_SERVICE });
|
|
2995
|
+
if (!raw)
|
|
2996
|
+
return null;
|
|
2997
|
+
const parsed = JSON.parse(raw);
|
|
2998
|
+
if (!Number.isSafeInteger(parsed.counter) || typeof parsed.vaultId !== "string")
|
|
2999
|
+
return null;
|
|
3000
|
+
return { counter: parsed.counter, vaultId: parsed.vaultId };
|
|
3001
|
+
} catch {
|
|
3002
|
+
return null;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
async function saveSealRecord(vaultPath, record) {
|
|
3006
|
+
if (!record || osSecretsDisabled()) {
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
try {
|
|
3010
|
+
const stored = await loadSealRecord(vaultPath);
|
|
3011
|
+
if (stored && stored.vaultId === record.vaultId && stored.counter >= record.counter)
|
|
3012
|
+
return;
|
|
3013
|
+
await Bun.secrets.set({ name: vaultPasswordName(vaultPath), service: SEAL_SERVICE, value: JSON.stringify(record) });
|
|
3014
|
+
} catch {}
|
|
3015
|
+
}
|
|
3016
|
+
async function closeSessionAndRecordSeal(vaultPath, session) {
|
|
3017
|
+
let record = null;
|
|
3018
|
+
try {
|
|
3019
|
+
record = session.sealCheckpoint();
|
|
3020
|
+
} catch {}
|
|
3021
|
+
session.close();
|
|
3022
|
+
await saveSealRecord(vaultPath, record);
|
|
3023
|
+
}
|
|
2376
3024
|
async function setOsSecret(name, value) {
|
|
2377
3025
|
if (osSecretsDisabled()) {
|
|
2378
3026
|
return false;
|
|
@@ -2459,7 +3107,7 @@ async function deletePin(vaultPath) {
|
|
|
2459
3107
|
return false;
|
|
2460
3108
|
}
|
|
2461
3109
|
}
|
|
2462
|
-
var VAULT_PASSWORD_SERVICE = "autho.vault", USER_SECRETS_SERVICE = "autho", PIN_SERVICE = "autho.pin";
|
|
3110
|
+
var VAULT_PASSWORD_SERVICE = "autho.vault", USER_SECRETS_SERVICE = "autho", PIN_SERVICE = "autho.pin", SEAL_SERVICE = "autho.seal";
|
|
2463
3111
|
var init_os_secrets = () => {};
|
|
2464
3112
|
|
|
2465
3113
|
// apps/cli/src/tui/theme.ts
|
|
@@ -2568,7 +3216,7 @@ function Chip({ label, color = theme.accent }) {
|
|
|
2568
3216
|
]
|
|
2569
3217
|
});
|
|
2570
3218
|
}
|
|
2571
|
-
function StatusBar({ hints, toast }) {
|
|
3219
|
+
function StatusBar({ hints, pinned = [], toast }) {
|
|
2572
3220
|
const { width } = useTerminalDimensions();
|
|
2573
3221
|
if (toast) {
|
|
2574
3222
|
const color = toast.kind === "error" ? theme.danger : toast.kind === "info" ? theme.info : theme.success;
|
|
@@ -2590,31 +3238,62 @@ function StatusBar({ hints, toast }) {
|
|
|
2590
3238
|
})
|
|
2591
3239
|
});
|
|
2592
3240
|
}
|
|
2593
|
-
return /* @__PURE__ */
|
|
3241
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
2594
3242
|
width: "100%",
|
|
2595
3243
|
height: 1,
|
|
2596
3244
|
backgroundColor: theme.panel,
|
|
2597
3245
|
paddingX: 1,
|
|
2598
3246
|
flexDirection: "row",
|
|
2599
|
-
|
|
3247
|
+
justifyContent: "space-between",
|
|
2600
3248
|
overflow: "hidden",
|
|
2601
|
-
children:
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
/* @__PURE__ */ jsxs("
|
|
2608
|
-
|
|
3249
|
+
children: [
|
|
3250
|
+
/* @__PURE__ */ jsx("box", {
|
|
3251
|
+
flexDirection: "row",
|
|
3252
|
+
gap: 2,
|
|
3253
|
+
flexShrink: 1,
|
|
3254
|
+
overflow: "hidden",
|
|
3255
|
+
children: fitHints(hints, width - 2 - hintsWidth(pinned) - (pinned.length ? 3 : 0)).map(([key, label]) => /* @__PURE__ */ jsxs("text", {
|
|
3256
|
+
flexShrink: 0,
|
|
2609
3257
|
children: [
|
|
2610
|
-
|
|
2611
|
-
|
|
3258
|
+
/* @__PURE__ */ jsx(Chip, {
|
|
3259
|
+
label: key
|
|
3260
|
+
}),
|
|
3261
|
+
/* @__PURE__ */ jsxs("span", {
|
|
3262
|
+
fg: theme.muted,
|
|
3263
|
+
children: [
|
|
3264
|
+
" ",
|
|
3265
|
+
label
|
|
3266
|
+
]
|
|
3267
|
+
})
|
|
2612
3268
|
]
|
|
2613
|
-
})
|
|
2614
|
-
|
|
2615
|
-
|
|
3269
|
+
}, `${key}-${label}`))
|
|
3270
|
+
}),
|
|
3271
|
+
pinned.length ? /* @__PURE__ */ jsx("box", {
|
|
3272
|
+
flexDirection: "row",
|
|
3273
|
+
gap: 2,
|
|
3274
|
+
flexShrink: 0,
|
|
3275
|
+
children: pinned.map(([key, label]) => /* @__PURE__ */ jsxs("text", {
|
|
3276
|
+
flexShrink: 0,
|
|
3277
|
+
children: [
|
|
3278
|
+
/* @__PURE__ */ jsx(Chip, {
|
|
3279
|
+
label: key
|
|
3280
|
+
}),
|
|
3281
|
+
/* @__PURE__ */ jsxs("span", {
|
|
3282
|
+
fg: theme.muted,
|
|
3283
|
+
children: [
|
|
3284
|
+
" ",
|
|
3285
|
+
label
|
|
3286
|
+
]
|
|
3287
|
+
})
|
|
3288
|
+
]
|
|
3289
|
+
}, `${key}-${label}`))
|
|
3290
|
+
}) : null
|
|
3291
|
+
]
|
|
2616
3292
|
});
|
|
2617
3293
|
}
|
|
3294
|
+
function hintsWidth(hints) {
|
|
3295
|
+
return hints.reduce((sum, [key, label], i) => sum + key.length + 2 + 1 + label.length + (i ? 2 : 0), 0);
|
|
3296
|
+
}
|
|
2618
3297
|
function fitHints(hints, width) {
|
|
2619
3298
|
const shown = [];
|
|
2620
3299
|
let used = 0;
|
|
@@ -2627,7 +3306,7 @@ function fitHints(hints, width) {
|
|
|
2627
3306
|
}
|
|
2628
3307
|
return shown;
|
|
2629
3308
|
}
|
|
2630
|
-
function Button({ label, hotkey, onPress, color = theme.accent }) {
|
|
3309
|
+
function Button({ label, hotkey, onPress, color = theme.accent, focused = false }) {
|
|
2631
3310
|
return /* @__PURE__ */ jsx("box", {
|
|
2632
3311
|
onMouseDown: (event) => {
|
|
2633
3312
|
event.preventDefault();
|
|
@@ -2635,6 +3314,7 @@ function Button({ label, hotkey, onPress, color = theme.accent }) {
|
|
|
2635
3314
|
},
|
|
2636
3315
|
height: 1,
|
|
2637
3316
|
flexShrink: 0,
|
|
3317
|
+
backgroundColor: focused ? theme.selection : undefined,
|
|
2638
3318
|
children: /* @__PURE__ */ jsxs("text", {
|
|
2639
3319
|
children: [
|
|
2640
3320
|
hotkey ? /* @__PURE__ */ jsx("span", {
|
|
@@ -2648,7 +3328,14 @@ function Button({ label, hotkey, onPress, color = theme.accent }) {
|
|
|
2648
3328
|
]
|
|
2649
3329
|
})
|
|
2650
3330
|
}) : null,
|
|
2651
|
-
/* @__PURE__ */ jsxs("
|
|
3331
|
+
focused ? /* @__PURE__ */ jsxs("strong", {
|
|
3332
|
+
fg: color,
|
|
3333
|
+
children: [
|
|
3334
|
+
" ",
|
|
3335
|
+
label,
|
|
3336
|
+
" \u2039"
|
|
3337
|
+
]
|
|
3338
|
+
}) : /* @__PURE__ */ jsxs("span", {
|
|
2652
3339
|
fg: color,
|
|
2653
3340
|
children: [
|
|
2654
3341
|
" ",
|
|
@@ -3418,7 +4105,7 @@ var init_palette = __esm(() => {
|
|
|
3418
4105
|
// apps/cli/src/tui/settings.tsx
|
|
3419
4106
|
import { existsSync as existsSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
3420
4107
|
import { homedir as homedir2 } from "os";
|
|
3421
|
-
import { dirname as dirname3, join as join3, resolve as
|
|
4108
|
+
import { dirname as dirname3, join as join3, resolve as resolve5 } from "path";
|
|
3422
4109
|
import { useKeyboard as useKeyboard4, useTerminalDimensions as useTerminalDimensions3 } from "@opentui/react";
|
|
3423
4110
|
import { useCallback, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef5, useState as useState4 } from "react";
|
|
3424
4111
|
import { jsx as jsx4, jsxs as jsxs4, Fragment } from "@opentui/react/jsx-runtime";
|
|
@@ -3426,6 +4113,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3426
4113
|
const app = useApp();
|
|
3427
4114
|
const { width, height } = useTerminalDimensions3();
|
|
3428
4115
|
const [section, setSection] = useState4("Storage" /* Storage */);
|
|
4116
|
+
const [focus, setFocus] = useState4("nav");
|
|
3429
4117
|
const [rowIndex, setRowIndex] = useState4(0);
|
|
3430
4118
|
const [dialog, setDialog] = useState4(null);
|
|
3431
4119
|
const [error, setError] = useState4("");
|
|
@@ -3445,11 +4133,21 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3445
4133
|
useEffect4(() => {
|
|
3446
4134
|
refreshStatus();
|
|
3447
4135
|
}, [refreshStatus]);
|
|
4136
|
+
const sealRecord = useRef5(null);
|
|
4137
|
+
useEffect4(() => {
|
|
4138
|
+
loadSealRecord(vaultPath).then((record) => {
|
|
4139
|
+
sealRecord.current = record;
|
|
4140
|
+
});
|
|
4141
|
+
}, [vaultPath]);
|
|
3448
4142
|
useEffect4(() => () => {
|
|
3449
4143
|
passkeyAbort.current?.abort();
|
|
3450
|
-
admin.current
|
|
4144
|
+
const current = admin.current;
|
|
3451
4145
|
admin.current = null;
|
|
3452
|
-
|
|
4146
|
+
if (current) {
|
|
4147
|
+
current.close();
|
|
4148
|
+
saveSealRecord(vaultPath, current.sealRecord());
|
|
4149
|
+
}
|
|
4150
|
+
}, [vaultPath]);
|
|
3453
4151
|
const writeFields = (next) => {
|
|
3454
4152
|
fieldsRef.current = next;
|
|
3455
4153
|
setFields(next);
|
|
@@ -3653,7 +4351,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3653
4351
|
return;
|
|
3654
4352
|
}
|
|
3655
4353
|
try {
|
|
3656
|
-
admin.current = VaultService.authorize(vaultPath, { password: first, totp: status?.totp ? second : undefined });
|
|
4354
|
+
admin.current = VaultService.authorize(vaultPath, { password: first, sealRecord: sealRecord.current, totp: status?.totp ? second : undefined });
|
|
3657
4355
|
} catch (e) {
|
|
3658
4356
|
setError(message(e));
|
|
3659
4357
|
writeFields(["", ""]);
|
|
@@ -3674,7 +4372,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3674
4372
|
return;
|
|
3675
4373
|
case "vaultDir": {
|
|
3676
4374
|
const config = { ...loadConfig() };
|
|
3677
|
-
const target = first.trim() ?
|
|
4375
|
+
const target = first.trim() ? resolve5(first.trim()).replace(/\\/g, "/") : null;
|
|
3678
4376
|
if (target)
|
|
3679
4377
|
config.vaultDir = target;
|
|
3680
4378
|
else
|
|
@@ -3742,7 +4440,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3742
4440
|
registerPasskey(first.trim());
|
|
3743
4441
|
return;
|
|
3744
4442
|
case "recoveryPath": {
|
|
3745
|
-
const target =
|
|
4443
|
+
const target = resolve5(first.trim() || join3(homedir2(), "autho-recovery.txt"));
|
|
3746
4444
|
if (existsSync5(target)) {
|
|
3747
4445
|
setError("That file already exists \u2014 pick another name");
|
|
3748
4446
|
return;
|
|
@@ -3795,6 +4493,12 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3795
4493
|
setSection(next);
|
|
3796
4494
|
setRowIndex(0);
|
|
3797
4495
|
};
|
|
4496
|
+
const pickSection = (next) => {
|
|
4497
|
+
close();
|
|
4498
|
+
setSection(next);
|
|
4499
|
+
setRowIndex(0);
|
|
4500
|
+
setFocus("nav");
|
|
4501
|
+
};
|
|
3798
4502
|
const moveRow = (delta) => setRowIndex(Math.min(rows.length - 1, Math.max(0, current + delta)));
|
|
3799
4503
|
useKeyboard4((key) => {
|
|
3800
4504
|
if (!app.active || key.ctrl || key.meta)
|
|
@@ -3819,6 +4523,44 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3819
4523
|
}
|
|
3820
4524
|
return;
|
|
3821
4525
|
}
|
|
4526
|
+
if (key.name === "q") {
|
|
4527
|
+
onBack();
|
|
4528
|
+
return;
|
|
4529
|
+
}
|
|
4530
|
+
if (key.name === "tab") {
|
|
4531
|
+
key.preventDefault();
|
|
4532
|
+
setFocus(focus === "nav" ? "rows" : "nav");
|
|
4533
|
+
return;
|
|
4534
|
+
}
|
|
4535
|
+
if (focus === "nav") {
|
|
4536
|
+
switch (key.name) {
|
|
4537
|
+
case "up":
|
|
4538
|
+
case "k":
|
|
4539
|
+
switchSection(-1);
|
|
4540
|
+
return;
|
|
4541
|
+
case "down":
|
|
4542
|
+
case "j":
|
|
4543
|
+
switchSection(1);
|
|
4544
|
+
return;
|
|
4545
|
+
case "home":
|
|
4546
|
+
pickSection(SECTIONS[0]);
|
|
4547
|
+
return;
|
|
4548
|
+
case "end":
|
|
4549
|
+
pickSection(SECTIONS[SECTIONS.length - 1]);
|
|
4550
|
+
return;
|
|
4551
|
+
case "right":
|
|
4552
|
+
case "l":
|
|
4553
|
+
case "space":
|
|
4554
|
+
setFocus("rows");
|
|
4555
|
+
return;
|
|
4556
|
+
case "escape":
|
|
4557
|
+
onBack();
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
if (isEnter(key))
|
|
4561
|
+
setFocus("rows");
|
|
4562
|
+
return;
|
|
4563
|
+
}
|
|
3822
4564
|
switch (key.name) {
|
|
3823
4565
|
case "up":
|
|
3824
4566
|
case "k":
|
|
@@ -3828,21 +4570,16 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3828
4570
|
case "j":
|
|
3829
4571
|
moveRow(1);
|
|
3830
4572
|
return;
|
|
3831
|
-
case "
|
|
3832
|
-
|
|
3833
|
-
switchSection(-1);
|
|
3834
|
-
return;
|
|
3835
|
-
case "right":
|
|
3836
|
-
case "l":
|
|
3837
|
-
switchSection(1);
|
|
4573
|
+
case "home":
|
|
4574
|
+
setRowIndex(0);
|
|
3838
4575
|
return;
|
|
3839
|
-
case "
|
|
3840
|
-
|
|
3841
|
-
switchSection(key.shift ? -1 : 1);
|
|
4576
|
+
case "end":
|
|
4577
|
+
setRowIndex(rows.length - 1);
|
|
3842
4578
|
return;
|
|
4579
|
+
case "left":
|
|
4580
|
+
case "h":
|
|
3843
4581
|
case "escape":
|
|
3844
|
-
|
|
3845
|
-
onBack();
|
|
4582
|
+
setFocus("nav");
|
|
3846
4583
|
return;
|
|
3847
4584
|
case "space":
|
|
3848
4585
|
row?.run?.();
|
|
@@ -3852,14 +4589,10 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3852
4589
|
row?.run?.();
|
|
3853
4590
|
});
|
|
3854
4591
|
const commands = useMemo3(() => [
|
|
3855
|
-
...SECTIONS.map((s) => ({ id: `settings:${s}`, title: `Settings \u2014 ${s}`, group: "Settings", run: () =>
|
|
3856
|
-
close();
|
|
3857
|
-
setSection(s);
|
|
3858
|
-
setRowIndex(0);
|
|
3859
|
-
} })),
|
|
4592
|
+
...SECTIONS.map((s) => ({ id: `settings:${s}`, title: `Settings \u2014 ${s}`, group: "Settings", run: () => pickSection(s) })),
|
|
3860
4593
|
{ id: "settings:back", title: "Back to the vault", hint: "esc", group: "Settings", run: onBack }
|
|
3861
4594
|
], [onBack]);
|
|
3862
|
-
const copyTarget = row?.copy ? { label: row.label, read: () => row.copy ?? null, secret: false } : null;
|
|
4595
|
+
const copyTarget = focus === "rows" && row?.copy ? { label: row.label, read: () => row.copy ?? null, secret: false } : null;
|
|
3863
4596
|
useScreenContext(commands, copyTarget);
|
|
3864
4597
|
useEffect4(() => {
|
|
3865
4598
|
if (dialog?.kind === "confirm")
|
|
@@ -3868,13 +4601,16 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3868
4601
|
setHints([["esc", "cancel"]]);
|
|
3869
4602
|
else if (dialog)
|
|
3870
4603
|
setHints([["enter", "save"], ...fieldCount(dialog, status) > 1 ? [["tab", "field"]] : [], ["esc", "cancel"]]);
|
|
4604
|
+
else if (focus === "nav")
|
|
4605
|
+
setHints([["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["esc", "back to vault"]]);
|
|
3871
4606
|
else
|
|
3872
|
-
setHints([["\u2191\u2193", "move"],
|
|
3873
|
-
}, [dialog, status, row?.verb, row?.copy, setHints]);
|
|
4607
|
+
setHints([["\u2191\u2193", "move"], ...row?.verb ? [["enter", row.verb]] : [], ...row?.copy && row.verb !== "copy" ? [["ctrl+c", "copy"]] : [], ["\u2190/esc", "sections"]]);
|
|
4608
|
+
}, [dialog, status, focus, row?.verb, row?.copy, setHints]);
|
|
3874
4609
|
const listHeight = Math.max(3, height - 12);
|
|
3875
4610
|
const offset = Math.max(0, Math.min(current - Math.floor(listHeight / 2), rows.length - listHeight));
|
|
3876
4611
|
const visible = rows.slice(offset, offset + listHeight);
|
|
3877
4612
|
const inputsFocused = app.active && dialog !== null;
|
|
4613
|
+
const rowsFocused = focus === "rows" || dialog !== null;
|
|
3878
4614
|
const valueWidth = Math.max(12, width - 64);
|
|
3879
4615
|
return /* @__PURE__ */ jsxs4("box", {
|
|
3880
4616
|
flexDirection: "row",
|
|
@@ -3895,14 +4631,10 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3895
4631
|
height: 1,
|
|
3896
4632
|
paddingX: 1,
|
|
3897
4633
|
backgroundColor: s === section ? theme.selection : undefined,
|
|
3898
|
-
onMouseDown: () =>
|
|
3899
|
-
close();
|
|
3900
|
-
setSection(s);
|
|
3901
|
-
setRowIndex(0);
|
|
3902
|
-
},
|
|
4634
|
+
onMouseDown: () => pickSection(s),
|
|
3903
4635
|
children: /* @__PURE__ */ jsx4("text", {
|
|
3904
4636
|
children: s === section ? /* @__PURE__ */ jsxs4("strong", {
|
|
3905
|
-
fg: theme.accent,
|
|
4637
|
+
fg: rowsFocused ? theme.muted : theme.accent,
|
|
3906
4638
|
children: [
|
|
3907
4639
|
"\u203A ",
|
|
3908
4640
|
s
|
|
@@ -3932,7 +4664,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3932
4664
|
children: admin.current ? "Authorized for this visit \u2014 changes apply right away." : "Changes ask for your master password once per visit."
|
|
3933
4665
|
}) : null,
|
|
3934
4666
|
visible.map((r, i) => {
|
|
3935
|
-
const on = offset + i === current;
|
|
4667
|
+
const on = rowsFocused && offset + i === current;
|
|
3936
4668
|
return /* @__PURE__ */ jsxs4("box", {
|
|
3937
4669
|
flexDirection: "row",
|
|
3938
4670
|
height: 1,
|
|
@@ -3940,6 +4672,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3940
4672
|
onMouseDown: () => {
|
|
3941
4673
|
if (dialog)
|
|
3942
4674
|
return;
|
|
4675
|
+
setFocus("rows");
|
|
3943
4676
|
if (on)
|
|
3944
4677
|
r.run?.();
|
|
3945
4678
|
else
|
|
@@ -3977,7 +4710,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
3977
4710
|
]
|
|
3978
4711
|
}, r.id);
|
|
3979
4712
|
}),
|
|
3980
|
-
row?.help && !dialog ? /* @__PURE__ */ jsx4("text", {
|
|
4713
|
+
focus === "rows" && row?.help && !dialog ? /* @__PURE__ */ jsx4("text", {
|
|
3981
4714
|
marginTop: 1,
|
|
3982
4715
|
fg: theme.dim,
|
|
3983
4716
|
wrapMode: "word",
|
|
@@ -4242,10 +4975,11 @@ function UnlockScreen({ vaultPath, onUnlock, setHints, notice, passkeyRunner = r
|
|
|
4242
4975
|
const passkeys = useMemo4(() => VaultService.listPasskeys(vaultPath), [vaultPath]);
|
|
4243
4976
|
const passkeyAbort = useRef6(null);
|
|
4244
4977
|
const stepBeforePasskey = useRef6("password");
|
|
4978
|
+
const sealRecord = useRef6(null);
|
|
4245
4979
|
const finish = useCallback2((creds) => {
|
|
4246
4980
|
setStep("unlocking");
|
|
4247
4981
|
try {
|
|
4248
|
-
onUnlock(VaultService.unlock(vaultPath, creds));
|
|
4982
|
+
onUnlock(VaultService.unlock(vaultPath, { ...creds, sealRecord: sealRecord.current }));
|
|
4249
4983
|
} catch (e) {
|
|
4250
4984
|
setError(e instanceof Error ? e.message : String(e));
|
|
4251
4985
|
setPassword("");
|
|
@@ -4257,13 +4991,14 @@ function UnlockScreen({ vaultPath, onUnlock, setHints, notice, passkeyRunner = r
|
|
|
4257
4991
|
useEffect5(() => {
|
|
4258
4992
|
let cancelled = false;
|
|
4259
4993
|
(async () => {
|
|
4994
|
+
sealRecord.current = await loadSealRecord(vaultPath);
|
|
4260
4995
|
const recoveryFile = process.env.AUTHO_RECOVERY_FILE;
|
|
4261
4996
|
if (recoveryFile) {
|
|
4262
4997
|
try {
|
|
4263
4998
|
const token = readRecoveryToken(recoveryFile);
|
|
4264
4999
|
if (token) {
|
|
4265
5000
|
try {
|
|
4266
|
-
const session = VaultService.unlock(vaultPath, { password: "", recovery: token });
|
|
5001
|
+
const session = VaultService.unlock(vaultPath, { password: "", recovery: token, sealRecord: sealRecord.current });
|
|
4267
5002
|
if (!cancelled)
|
|
4268
5003
|
onUnlock(session);
|
|
4269
5004
|
return;
|
|
@@ -4580,7 +5315,9 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4580
5315
|
const wide = width >= WIDE_MIN_COLUMNS;
|
|
4581
5316
|
const [entries, setEntries] = useState6([]);
|
|
4582
5317
|
const [query, setQuery] = useState6("");
|
|
4583
|
-
const [
|
|
5318
|
+
const [pane, setPane] = useState6("list");
|
|
5319
|
+
const [actionIndex, setActionIndex] = useState6(0);
|
|
5320
|
+
const searchFocused = pane === "search";
|
|
4584
5321
|
const [selectedId, setSelectedId] = useState6(initialSelectedId ?? null);
|
|
4585
5322
|
const [detailOpen, setDetailOpen] = useState6(false);
|
|
4586
5323
|
const [revealed, setRevealed] = useState6(null);
|
|
@@ -4607,6 +5344,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4607
5344
|
setRevealed((r) => r && r.id !== selected?.id ? null : r);
|
|
4608
5345
|
setOtp((o) => o && o.id !== selected?.id ? null : o);
|
|
4609
5346
|
setConfirmDelete(false);
|
|
5347
|
+
setActionIndex(0);
|
|
4610
5348
|
}, [selected?.id]);
|
|
4611
5349
|
const now = useNow(1000, Boolean(revealed || otp));
|
|
4612
5350
|
useEffect6(() => {
|
|
@@ -4622,9 +5360,9 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4622
5360
|
return null;
|
|
4623
5361
|
}
|
|
4624
5362
|
}, [session, app]);
|
|
4625
|
-
const generateOtp = useCallback3((entry) => {
|
|
5363
|
+
const generateOtp = useCallback3((entry, options) => {
|
|
4626
5364
|
try {
|
|
4627
|
-
const result = session.generateOtp(entry.id);
|
|
5365
|
+
const result = session.generateOtp(entry.id, options);
|
|
4628
5366
|
const next = { id: entry.id, code: result.code, expiresAt: Date.parse(result.expiresAt) };
|
|
4629
5367
|
setOtp(next);
|
|
4630
5368
|
return next;
|
|
@@ -4635,7 +5373,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4635
5373
|
}, [session, app]);
|
|
4636
5374
|
useEffect6(() => {
|
|
4637
5375
|
if (otp && selected && otp.id === selected.id && now >= otp.expiresAt) {
|
|
4638
|
-
generateOtp(selected);
|
|
5376
|
+
generateOtp(selected, { audit: false });
|
|
4639
5377
|
}
|
|
4640
5378
|
}, [now, otp, selected, generateOtp]);
|
|
4641
5379
|
const copyPrimary = useCallback3((entry) => {
|
|
@@ -4686,6 +5424,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4686
5424
|
app.toast(`Deleted "${entry.name}"`);
|
|
4687
5425
|
setConfirmDelete(false);
|
|
4688
5426
|
setDetailOpen(false);
|
|
5427
|
+
setPane("list");
|
|
4689
5428
|
reload();
|
|
4690
5429
|
} catch (e) {
|
|
4691
5430
|
app.toast(e instanceof Error ? e.message : String(e), "error");
|
|
@@ -4707,6 +5446,32 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4707
5446
|
};
|
|
4708
5447
|
const showDetail = wide || detailOpen;
|
|
4709
5448
|
const listHeight = Math.max(3, height - 6);
|
|
5449
|
+
const actions = selected ? [
|
|
5450
|
+
selected.type === "otp" ? { id: "copy", hotkey: "o", label: "copy code", color: theme.success, run: () => copyPrimary(selected) } : { id: "copy", hotkey: "c", label: `copy ${valueName(selected.type).toLowerCase()}`, color: theme.success, run: () => copyPrimary(selected) },
|
|
5451
|
+
...selected.type !== "otp" ? [{ id: "reveal", hotkey: "s", label: revealed?.id === selected.id ? "hide" : "reveal", color: theme.revealed, run: () => toggleReveal(selected) }] : [],
|
|
5452
|
+
...selected.url && selected.type !== "otp" ? [{ id: "open", hotkey: "o", label: "open", color: theme.info, run: () => open(selected) }] : [],
|
|
5453
|
+
{ id: "edit", hotkey: "e", label: "edit", color: theme.accent, run: () => onEdit(selected.id) },
|
|
5454
|
+
{ id: "delete", hotkey: "d", label: "delete", color: theme.danger, run: () => setConfirmDelete(true) }
|
|
5455
|
+
] : [];
|
|
5456
|
+
const currentAction = Math.min(actionIndex, Math.max(0, actions.length - 1));
|
|
5457
|
+
const focusPane = (next) => {
|
|
5458
|
+
if (next === "actions") {
|
|
5459
|
+
if (!selected)
|
|
5460
|
+
return;
|
|
5461
|
+
setDetailOpen(true);
|
|
5462
|
+
setActionIndex(0);
|
|
5463
|
+
}
|
|
5464
|
+
if (next !== "actions" && !wide)
|
|
5465
|
+
setDetailOpen(false);
|
|
5466
|
+
setPane(next);
|
|
5467
|
+
};
|
|
5468
|
+
const cyclePane = (delta) => {
|
|
5469
|
+
let index = PANES.indexOf(pane);
|
|
5470
|
+
do
|
|
5471
|
+
index = (index + delta + PANES.length) % PANES.length;
|
|
5472
|
+
while (PANES[index] === "actions" && !selected);
|
|
5473
|
+
focusPane(PANES[index]);
|
|
5474
|
+
};
|
|
4710
5475
|
useKeyboard6((key) => {
|
|
4711
5476
|
if (!app.active || key.ctrl || key.meta)
|
|
4712
5477
|
return;
|
|
@@ -4718,13 +5483,18 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4718
5483
|
setConfirmDelete(false);
|
|
4719
5484
|
return;
|
|
4720
5485
|
}
|
|
5486
|
+
if (key.name === "tab") {
|
|
5487
|
+
key.preventDefault();
|
|
5488
|
+
cyclePane(key.shift ? -1 : 1);
|
|
5489
|
+
return;
|
|
5490
|
+
}
|
|
4721
5491
|
if (searchFocused) {
|
|
4722
5492
|
if (key.name === "escape") {
|
|
4723
5493
|
setQuery("");
|
|
4724
|
-
|
|
4725
|
-
} else if (isEnter(key) || key.name === "down"
|
|
5494
|
+
setPane("list");
|
|
5495
|
+
} else if (isEnter(key) || key.name === "down") {
|
|
4726
5496
|
key.preventDefault();
|
|
4727
|
-
|
|
5497
|
+
setPane("list");
|
|
4728
5498
|
}
|
|
4729
5499
|
return;
|
|
4730
5500
|
}
|
|
@@ -4754,7 +5524,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4754
5524
|
return;
|
|
4755
5525
|
case "/":
|
|
4756
5526
|
key.preventDefault();
|
|
4757
|
-
|
|
5527
|
+
focusPane("search");
|
|
4758
5528
|
return;
|
|
4759
5529
|
case "n":
|
|
4760
5530
|
onCreate();
|
|
@@ -4773,30 +5543,52 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4773
5543
|
app.toast("Reloaded", "info");
|
|
4774
5544
|
return;
|
|
4775
5545
|
}
|
|
4776
|
-
if (
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
if (
|
|
4780
|
-
|
|
4781
|
-
else
|
|
4782
|
-
|
|
5546
|
+
if (key.name === "escape") {
|
|
5547
|
+
if (pane === "actions")
|
|
5548
|
+
focusPane("list");
|
|
5549
|
+
else if (revealed)
|
|
5550
|
+
setRevealed(null);
|
|
5551
|
+
else if (query)
|
|
5552
|
+
setQuery("");
|
|
4783
5553
|
return;
|
|
4784
5554
|
}
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
5555
|
+
if (!selected)
|
|
5556
|
+
return;
|
|
5557
|
+
if (pane === "actions") {
|
|
5558
|
+
switch (key.name) {
|
|
5559
|
+
case "left":
|
|
5560
|
+
case "h":
|
|
5561
|
+
if (currentAction === 0)
|
|
5562
|
+
focusPane("list");
|
|
5563
|
+
else
|
|
5564
|
+
setActionIndex((i) => Math.max(0, Math.min(i, actions.length - 1) - 1));
|
|
5565
|
+
return;
|
|
5566
|
+
case "right":
|
|
5567
|
+
case "l":
|
|
5568
|
+
setActionIndex((i) => Math.min(actions.length - 1, i + 1));
|
|
5569
|
+
return;
|
|
5570
|
+
case "space":
|
|
5571
|
+
actions[currentAction]?.run();
|
|
5572
|
+
return;
|
|
5573
|
+
}
|
|
5574
|
+
if (isEnter(key)) {
|
|
5575
|
+
actions[currentAction]?.run();
|
|
4789
5576
|
return;
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
setDetailOpen(false);
|
|
4795
|
-
else if (revealed)
|
|
4796
|
-
setRevealed(null);
|
|
4797
|
-
else if (query)
|
|
4798
|
-
setQuery("");
|
|
5577
|
+
}
|
|
5578
|
+
} else {
|
|
5579
|
+
if (key.name === "right" || key.name === "l") {
|
|
5580
|
+
focusPane("actions");
|
|
4799
5581
|
return;
|
|
5582
|
+
}
|
|
5583
|
+
if (isEnter(key)) {
|
|
5584
|
+
if (showDetail)
|
|
5585
|
+
copyPrimary(selected);
|
|
5586
|
+
else
|
|
5587
|
+
focusPane("actions");
|
|
5588
|
+
return;
|
|
5589
|
+
}
|
|
5590
|
+
}
|
|
5591
|
+
switch (key.name) {
|
|
4800
5592
|
case "c":
|
|
4801
5593
|
copyPrimary(selected);
|
|
4802
5594
|
return;
|
|
@@ -4828,7 +5620,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4828
5620
|
const commands = useMemo5(() => {
|
|
4829
5621
|
const list = [
|
|
4830
5622
|
{ id: "new", title: "New secret", hint: "n", group: "Vault", run: onCreate },
|
|
4831
|
-
{ id: "search", title: "Search secrets", hint: "/", group: "Vault", run: () =>
|
|
5623
|
+
{ id: "search", title: "Search secrets", hint: "/", group: "Vault", run: () => setPane("search") },
|
|
4832
5624
|
{ id: "reload", title: "Reload list", hint: "r", group: "Vault", run: reload }
|
|
4833
5625
|
];
|
|
4834
5626
|
if (selected) {
|
|
@@ -4848,6 +5640,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4848
5640
|
setQuery("");
|
|
4849
5641
|
setSelectedId(entry.id);
|
|
4850
5642
|
setDetailOpen(true);
|
|
5643
|
+
setPane("actions");
|
|
4851
5644
|
}
|
|
4852
5645
|
});
|
|
4853
5646
|
}
|
|
@@ -4864,12 +5657,14 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4864
5657
|
hints = [["y", "delete"], ["n/esc", "cancel"]];
|
|
4865
5658
|
else if (searchFocused)
|
|
4866
5659
|
hints = [["type", "filter"], ["enter/\u2193", "results"], ["esc", "clear"]];
|
|
5660
|
+
else if (pane === "actions")
|
|
5661
|
+
hints = [["\u2190\u2192", "choose"], ["enter", "run"], ["\u2191\u2193", "secret"], ["esc", "back"]];
|
|
4867
5662
|
else if (!showDetail)
|
|
4868
|
-
hints = [["\u2191\u2193", "move"], ["enter", "open"], ["c", "copy"], ["
|
|
5663
|
+
hints = [["\u2191\u2193", "move"], ["enter", "open"], ["c", "copy"], ["/", "search"], ["n", "new"], ["q", "quit"]];
|
|
4869
5664
|
else
|
|
4870
|
-
hints = [["
|
|
5665
|
+
hints = [["\u2191\u2193", "move"], ["enter", "copy"], ["\u2192/tab", "actions"], ["/", "search"], ["n", "new"], ["q", "quit"]];
|
|
4871
5666
|
setHints(hints);
|
|
4872
|
-
}, [confirmDelete, searchFocused,
|
|
5667
|
+
}, [confirmDelete, searchFocused, pane, showDetail, setHints]);
|
|
4873
5668
|
const listWidth = wide ? Math.max(32, Math.floor(width * 0.38)) : width;
|
|
4874
5669
|
const offset = Math.max(0, Math.min(selectedIndex - Math.floor(listHeight / 2), filtered.length - listHeight));
|
|
4875
5670
|
const visible = filtered.slice(offset, offset + listHeight);
|
|
@@ -4902,8 +5697,12 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4902
5697
|
backgroundColor: isSelected ? theme.selection : undefined,
|
|
4903
5698
|
onMouseDown: () => {
|
|
4904
5699
|
setSelectedId(entry.id);
|
|
4905
|
-
if (
|
|
5700
|
+
if (wide)
|
|
5701
|
+
setPane("list");
|
|
5702
|
+
else {
|
|
4906
5703
|
setDetailOpen(true);
|
|
5704
|
+
setPane("actions");
|
|
5705
|
+
}
|
|
4907
5706
|
},
|
|
4908
5707
|
children: /* @__PURE__ */ jsxs6("text", {
|
|
4909
5708
|
children: [
|
|
@@ -4936,11 +5735,8 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4936
5735
|
otp: otp?.id === selected.id ? otp : null,
|
|
4937
5736
|
now,
|
|
4938
5737
|
confirmDelete,
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
onOpen: () => open(selected),
|
|
4942
|
-
onEdit: () => onEdit(selected.id),
|
|
4943
|
-
onDelete: () => setConfirmDelete(true)
|
|
5738
|
+
actions,
|
|
5739
|
+
focusedAction: pane === "actions" ? currentAction : null
|
|
4944
5740
|
}) : /* @__PURE__ */ jsx6("box", {
|
|
4945
5741
|
flexGrow: 1,
|
|
4946
5742
|
border: true,
|
|
@@ -4987,9 +5783,12 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
4987
5783
|
]
|
|
4988
5784
|
});
|
|
4989
5785
|
}
|
|
4990
|
-
function
|
|
5786
|
+
function valueName(type) {
|
|
5787
|
+
return type === "note" ? "Note" : type === "otp" ? "Secret key" : "Password";
|
|
5788
|
+
}
|
|
5789
|
+
function Detail({ entry, revealed, otp, now, confirmDelete, actions, focusedAction }) {
|
|
4991
5790
|
const badge = typeBadge(entry.type);
|
|
4992
|
-
const valueLabel = entry.type
|
|
5791
|
+
const valueLabel = valueName(entry.type);
|
|
4993
5792
|
const revealSeconds = revealed ? Math.max(0, Math.ceil((revealed.until - now) / 1000)) : 0;
|
|
4994
5793
|
const otpSeconds = otp ? Math.max(0, Math.ceil((otp.expiresAt - now) / 1000)) : 0;
|
|
4995
5794
|
const otpBar = 10;
|
|
@@ -5119,51 +5918,28 @@ function Detail({ entry, revealed, otp, now, confirmDelete, onCopy, onOtp, onOpe
|
|
|
5119
5918
|
})
|
|
5120
5919
|
]
|
|
5121
5920
|
})
|
|
5122
|
-
}) : /* @__PURE__ */
|
|
5921
|
+
}) : /* @__PURE__ */ jsx6("box", {
|
|
5123
5922
|
flexDirection: "row",
|
|
5124
5923
|
gap: 1,
|
|
5125
5924
|
flexWrap: "wrap",
|
|
5126
|
-
children:
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
hotkey: "c",
|
|
5134
|
-
label: `copy ${valueLabel.toLowerCase()}`,
|
|
5135
|
-
onPress: onCopy,
|
|
5136
|
-
color: theme.success
|
|
5137
|
-
}),
|
|
5138
|
-
entry.url && entry.type !== "otp" ? /* @__PURE__ */ jsx6(Button, {
|
|
5139
|
-
hotkey: "o",
|
|
5140
|
-
label: "open",
|
|
5141
|
-
onPress: onOpen,
|
|
5142
|
-
color: theme.info
|
|
5143
|
-
}) : null,
|
|
5144
|
-
/* @__PURE__ */ jsx6(Button, {
|
|
5145
|
-
hotkey: "e",
|
|
5146
|
-
label: "edit",
|
|
5147
|
-
onPress: onEdit,
|
|
5148
|
-
color: theme.accent
|
|
5149
|
-
}),
|
|
5150
|
-
/* @__PURE__ */ jsx6(Button, {
|
|
5151
|
-
hotkey: "d",
|
|
5152
|
-
label: "delete",
|
|
5153
|
-
onPress: onDelete,
|
|
5154
|
-
color: theme.danger
|
|
5155
|
-
})
|
|
5156
|
-
]
|
|
5925
|
+
children: actions.map((action, i) => /* @__PURE__ */ jsx6(Button, {
|
|
5926
|
+
hotkey: action.hotkey,
|
|
5927
|
+
label: action.label,
|
|
5928
|
+
onPress: action.run,
|
|
5929
|
+
color: action.color,
|
|
5930
|
+
focused: focusedAction === i
|
|
5931
|
+
}, action.id))
|
|
5157
5932
|
})
|
|
5158
5933
|
]
|
|
5159
5934
|
});
|
|
5160
5935
|
}
|
|
5161
|
-
var WIDE_MIN_COLUMNS = 96;
|
|
5936
|
+
var PANES, WIDE_MIN_COLUMNS = 96;
|
|
5162
5937
|
var init_vault = __esm(() => {
|
|
5163
5938
|
init_components();
|
|
5164
5939
|
init_context();
|
|
5165
5940
|
init_theme();
|
|
5166
5941
|
init_util();
|
|
5942
|
+
PANES = ["search", "list", "actions"];
|
|
5167
5943
|
});
|
|
5168
5944
|
|
|
5169
5945
|
// apps/cli/src/tui.tsx
|
|
@@ -5213,11 +5989,11 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
5213
5989
|
const current = sessionRef.current;
|
|
5214
5990
|
if (!current)
|
|
5215
5991
|
return;
|
|
5216
|
-
current
|
|
5992
|
+
closeSessionAndRecordSeal(vaultPath, current);
|
|
5217
5993
|
setSession(null);
|
|
5218
5994
|
setPaletteOpen(false);
|
|
5219
5995
|
setRoute({ name: "unlock", notice });
|
|
5220
|
-
}, []);
|
|
5996
|
+
}, [vaultPath]);
|
|
5221
5997
|
const quit = useCallback4(() => {
|
|
5222
5998
|
if (quitting.current)
|
|
5223
5999
|
return;
|
|
@@ -5322,6 +6098,9 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
5322
6098
|
] : [],
|
|
5323
6099
|
{ id: "quit", title: "Quit", hint: "ctrl+c \xD72", group: "App", run: quit }
|
|
5324
6100
|
];
|
|
6101
|
+
const goVault = useCallback4(() => setRoute({ name: "vault" }), []);
|
|
6102
|
+
const goSettings = useCallback4(() => setRoute({ name: "settings" }), []);
|
|
6103
|
+
const globalHints = !session || route.name === "unlock" ? [] : route.name === "vault" ? [["ctrl+k", "commands"], [",", "settings"]] : [["ctrl+k", "commands"]];
|
|
5325
6104
|
const title = route.name === "unlock" ? "Locked" : route.name === "form" ? route.editing ? "Edit secret" : "New secret" : route.name === "settings" ? "Settings" : "Vault";
|
|
5326
6105
|
const vaultName = basename3(vaultPath);
|
|
5327
6106
|
let screen;
|
|
@@ -5349,7 +6128,7 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
5349
6128
|
screen = /* @__PURE__ */ jsx7(SettingsScreen, {
|
|
5350
6129
|
vaultPath,
|
|
5351
6130
|
setHints,
|
|
5352
|
-
onBack:
|
|
6131
|
+
onBack: goVault,
|
|
5353
6132
|
passkeyRunner
|
|
5354
6133
|
});
|
|
5355
6134
|
} else {
|
|
@@ -5358,7 +6137,7 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
5358
6137
|
initialSelectedId: route.focusId,
|
|
5359
6138
|
onCreate: () => setRoute({ name: "form", editing: null }),
|
|
5360
6139
|
onEdit,
|
|
5361
|
-
onSettings:
|
|
6140
|
+
onSettings: goSettings,
|
|
5362
6141
|
setHints,
|
|
5363
6142
|
revealMs
|
|
5364
6143
|
}, route.focusId ?? "vault");
|
|
@@ -5400,6 +6179,7 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
5400
6179
|
}),
|
|
5401
6180
|
/* @__PURE__ */ jsx7(StatusBar, {
|
|
5402
6181
|
hints,
|
|
6182
|
+
pinned: globalHints,
|
|
5403
6183
|
toast: toastState
|
|
5404
6184
|
}),
|
|
5405
6185
|
paletteOpen ? /* @__PURE__ */ jsx7(CommandPalette, {
|
|
@@ -5426,9 +6206,8 @@ async function runTui(vaultPath) {
|
|
|
5426
6206
|
const sessionSink = { current: null };
|
|
5427
6207
|
renderer.once("destroy", () => {
|
|
5428
6208
|
clipboard.handOffPendingClear();
|
|
5429
|
-
|
|
5430
|
-
sessionSink.current
|
|
5431
|
-
} catch {}
|
|
6209
|
+
if (sessionSink.current)
|
|
6210
|
+
closeSessionAndRecordSeal(vault, sessionSink.current).catch(() => {});
|
|
5432
6211
|
sessionSink.current = null;
|
|
5433
6212
|
});
|
|
5434
6213
|
createRoot(renderer).render(/* @__PURE__ */ jsx7(App, {
|
|
@@ -5443,6 +6222,7 @@ async function runTui(vaultPath) {
|
|
|
5443
6222
|
}
|
|
5444
6223
|
var init_tui = __esm(() => {
|
|
5445
6224
|
init_src3();
|
|
6225
|
+
init_os_secrets();
|
|
5446
6226
|
init_clipboard();
|
|
5447
6227
|
init_components();
|
|
5448
6228
|
init_context();
|
|
@@ -5458,10 +6238,9 @@ var init_tui = __esm(() => {
|
|
|
5458
6238
|
init_clipboard();
|
|
5459
6239
|
init_passkey();
|
|
5460
6240
|
init_package();
|
|
5461
|
-
import { spawn as spawn3 } from "child_process";
|
|
5462
6241
|
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
5463
6242
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
5464
|
-
import { basename as basename4, isAbsolute, resolve as
|
|
6243
|
+
import { basename as basename4, isAbsolute, resolve as resolve6 } from "path";
|
|
5465
6244
|
|
|
5466
6245
|
// apps/cli/src/password.ts
|
|
5467
6246
|
import { createInterface } from "readline/promises";
|
|
@@ -5544,13 +6323,14 @@ async function readPasswordMasked(prompt = "Master password: ") {
|
|
|
5544
6323
|
init_src();
|
|
5545
6324
|
init_src2();
|
|
5546
6325
|
init_src3();
|
|
6326
|
+
init_os_secrets();
|
|
5547
6327
|
init_paths();
|
|
5548
6328
|
init_paths();
|
|
5549
|
-
import { createHash as
|
|
6329
|
+
import { createHash as createHash3, timingSafeEqual as timingSafeEqual4 } from "crypto";
|
|
5550
6330
|
import { existsSync as existsSync4, readFileSync as readFileSync4, rmSync } from "fs";
|
|
5551
6331
|
var DAEMON_TOKEN_SERVICE = "autho.daemon";
|
|
5552
6332
|
function daemonTokenName(statePath) {
|
|
5553
|
-
return
|
|
6333
|
+
return createHash3("sha256").update(statePath).digest("hex");
|
|
5554
6334
|
}
|
|
5555
6335
|
async function storeDaemonToken(statePath, token) {
|
|
5556
6336
|
const tokenName = daemonTokenName(statePath);
|
|
@@ -5641,20 +6421,11 @@ async function deleteDaemonState(statePath) {
|
|
|
5641
6421
|
rmSync(statePath, { force: true });
|
|
5642
6422
|
}
|
|
5643
6423
|
}
|
|
5644
|
-
function openSessionFromRootKey(
|
|
5645
|
-
return new VaultSession(new AuthoDatabase(vaultPath), rootKey
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
try {
|
|
5650
|
-
const config = db.getVaultConfig();
|
|
5651
|
-
if (!config) {
|
|
5652
|
-
throw new Error(`Vault is not initialized at ${vaultPath}`);
|
|
5653
|
-
}
|
|
5654
|
-
return unlockRootKey(password, config);
|
|
5655
|
-
} finally {
|
|
5656
|
-
db.close();
|
|
5657
|
-
}
|
|
6424
|
+
async function openSessionFromRootKey(session) {
|
|
6425
|
+
return new VaultSession(new AuthoDatabase(session.vaultPath), session.rootKey, {
|
|
6426
|
+
sealBound: session.sealBound,
|
|
6427
|
+
sealRecord: await loadSealRecord(session.vaultPath)
|
|
6428
|
+
});
|
|
5658
6429
|
}
|
|
5659
6430
|
async function readJson(request) {
|
|
5660
6431
|
return await request.json();
|
|
@@ -5698,7 +6469,7 @@ async function startDaemonServer(options) {
|
|
|
5698
6469
|
};
|
|
5699
6470
|
const auth = (request) => {
|
|
5700
6471
|
const provided = getBearerToken(request);
|
|
5701
|
-
if (!provided || provided.length !== token.length || !
|
|
6472
|
+
if (!provided || provided.length !== token.length || !timingSafeEqual4(Buffer.from(provided), Buffer.from(token))) {
|
|
5702
6473
|
return unauthorized();
|
|
5703
6474
|
}
|
|
5704
6475
|
return null;
|
|
@@ -5748,13 +6519,18 @@ async function startDaemonServer(options) {
|
|
|
5748
6519
|
if (ttlSeconds <= 0 || ttlSeconds > 86400) {
|
|
5749
6520
|
return json({ error: "Unlock ttl must be between 1 and 86400 seconds" }, 400);
|
|
5750
6521
|
}
|
|
5751
|
-
const rootKey =
|
|
6522
|
+
const { rootKey, sealBound } = VaultService.unlockRootKey(options.vaultPath, {
|
|
6523
|
+
password: body.password,
|
|
6524
|
+
sealRecord: await loadSealRecord(options.vaultPath),
|
|
6525
|
+
totp: body.totp
|
|
6526
|
+
});
|
|
5752
6527
|
const sessionId = randomId();
|
|
5753
6528
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
5754
6529
|
sessions.set(sessionId, {
|
|
5755
6530
|
expiresAt,
|
|
5756
6531
|
id: sessionId,
|
|
5757
6532
|
rootKey,
|
|
6533
|
+
sealBound,
|
|
5758
6534
|
vaultPath: options.vaultPath
|
|
5759
6535
|
});
|
|
5760
6536
|
return json({
|
|
@@ -5773,11 +6549,11 @@ async function startDaemonServer(options) {
|
|
|
5773
6549
|
if (!session) {
|
|
5774
6550
|
return json({ error: `Unknown daemon session: ${body.sessionId}` }, 404);
|
|
5775
6551
|
}
|
|
5776
|
-
const vaultSession = openSessionFromRootKey(session
|
|
6552
|
+
const vaultSession = await openSessionFromRootKey(session);
|
|
5777
6553
|
try {
|
|
5778
6554
|
return json(vaultSession.renderEnv(body.mappings, body.leaseId));
|
|
5779
6555
|
} finally {
|
|
5780
|
-
vaultSession
|
|
6556
|
+
await closeSessionAndRecordSeal(session.vaultPath, vaultSession);
|
|
5781
6557
|
}
|
|
5782
6558
|
}
|
|
5783
6559
|
if (request.method === "POST" && url.pathname === "/exec") {
|
|
@@ -5786,11 +6562,11 @@ async function startDaemonServer(options) {
|
|
|
5786
6562
|
if (!session) {
|
|
5787
6563
|
return json({ error: `Unknown daemon session: ${body.sessionId}` }, 404);
|
|
5788
6564
|
}
|
|
5789
|
-
const vaultSession = openSessionFromRootKey(session
|
|
6565
|
+
const vaultSession = await openSessionFromRootKey(session);
|
|
5790
6566
|
try {
|
|
5791
6567
|
return json(vaultSession.runExec(body));
|
|
5792
6568
|
} finally {
|
|
5793
|
-
vaultSession
|
|
6569
|
+
await closeSessionAndRecordSeal(session.vaultPath, vaultSession);
|
|
5794
6570
|
}
|
|
5795
6571
|
}
|
|
5796
6572
|
if (request.method === "POST" && url.pathname === "/shutdown") {
|
|
@@ -5861,6 +6637,7 @@ async function daemonUnlock(options) {
|
|
|
5861
6637
|
}
|
|
5862
6638
|
return daemonRequest(state, "/unlock", {
|
|
5863
6639
|
password: options.password,
|
|
6640
|
+
totp: options.totp,
|
|
5864
6641
|
ttlSeconds: options.ttlSeconds
|
|
5865
6642
|
});
|
|
5866
6643
|
}
|
|
@@ -5905,6 +6682,321 @@ async function daemonStop(options) {
|
|
|
5905
6682
|
// apps/cli/src/index.ts
|
|
5906
6683
|
init_src3();
|
|
5907
6684
|
init_os_secrets();
|
|
6685
|
+
|
|
6686
|
+
// apps/web/src/server.ts
|
|
6687
|
+
init_src();
|
|
6688
|
+
init_src3();
|
|
6689
|
+
init_os_secrets();
|
|
6690
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
6691
|
+
import { resolve as resolve4 } from "path";
|
|
6692
|
+
function json2(data, status = 200, headers) {
|
|
6693
|
+
return new Response(JSON.stringify(data, null, 2), {
|
|
6694
|
+
headers: {
|
|
6695
|
+
"content-type": "application/json; charset=utf-8",
|
|
6696
|
+
...headers ?? {}
|
|
6697
|
+
},
|
|
6698
|
+
status
|
|
6699
|
+
});
|
|
6700
|
+
}
|
|
6701
|
+
function html(body) {
|
|
6702
|
+
return new Response(body, {
|
|
6703
|
+
headers: {
|
|
6704
|
+
"content-type": "text/html; charset=utf-8"
|
|
6705
|
+
}
|
|
6706
|
+
});
|
|
6707
|
+
}
|
|
6708
|
+
function parseCookies(request) {
|
|
6709
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
6710
|
+
return Object.fromEntries(cookieHeader.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
6711
|
+
const index = part.indexOf("=");
|
|
6712
|
+
return [part.slice(0, index), decodeURIComponent(part.slice(index + 1))];
|
|
6713
|
+
}));
|
|
6714
|
+
}
|
|
6715
|
+
async function openSession(vaultPath, state) {
|
|
6716
|
+
return new VaultSession(new AuthoDatabase(vaultPath), state.rootKey, {
|
|
6717
|
+
sealBound: state.sealBound,
|
|
6718
|
+
sealRecord: await loadSealRecord(vaultPath)
|
|
6719
|
+
});
|
|
6720
|
+
}
|
|
6721
|
+
function requireSession(request, sessions) {
|
|
6722
|
+
const cookies = parseCookies(request);
|
|
6723
|
+
const sessionId = cookies.autho_session;
|
|
6724
|
+
if (!sessionId) {
|
|
6725
|
+
throw new Error("Missing web session");
|
|
6726
|
+
}
|
|
6727
|
+
const session = sessions.get(sessionId);
|
|
6728
|
+
if (!session) {
|
|
6729
|
+
throw new Error("Unknown web session");
|
|
6730
|
+
}
|
|
6731
|
+
if (Date.parse(session.expiresAt) <= Date.now()) {
|
|
6732
|
+
sessions.delete(sessionId);
|
|
6733
|
+
throw new Error("Expired web session");
|
|
6734
|
+
}
|
|
6735
|
+
return { ...session, id: sessionId };
|
|
6736
|
+
}
|
|
6737
|
+
async function readJson2(request) {
|
|
6738
|
+
return await request.json();
|
|
6739
|
+
}
|
|
6740
|
+
function page2() {
|
|
6741
|
+
return `<!doctype html>
|
|
6742
|
+
<html lang="en">
|
|
6743
|
+
<head>
|
|
6744
|
+
<meta charset="utf-8" />
|
|
6745
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6746
|
+
<title>Autho</title>
|
|
6747
|
+
<style>
|
|
6748
|
+
:root { color-scheme: light; font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
6749
|
+
body { margin: 0; background: #f5f1e8; color: #1f2937; }
|
|
6750
|
+
main { max-width: 760px; margin: 0 auto; padding: 32px 20px 64px; }
|
|
6751
|
+
section { background: #fffdf8; border: 1px solid #d6cbb8; border-radius: 16px; padding: 20px; margin-bottom: 16px; box-shadow: 0 8px 30px rgba(83,58,18,0.08); }
|
|
6752
|
+
h1 { margin-top: 0; font-size: 2rem; }
|
|
6753
|
+
input, select, button { font: inherit; padding: 10px 12px; border-radius: 10px; border: 1px solid #bca889; }
|
|
6754
|
+
button { background: #1f4d3a; color: white; border: none; cursor: pointer; }
|
|
6755
|
+
form { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
6756
|
+
pre { background: #1f2937; color: #f9fafb; padding: 12px; border-radius: 12px; overflow: auto; }
|
|
6757
|
+
ul { padding-left: 18px; }
|
|
6758
|
+
</style>
|
|
6759
|
+
</head>
|
|
6760
|
+
<body>
|
|
6761
|
+
<main>
|
|
6762
|
+
<section>
|
|
6763
|
+
<h1>Autho Local Web</h1>
|
|
6764
|
+
<p>Local-only Bun web UI for vault unlock and secret browsing.</p>
|
|
6765
|
+
<form id="unlock-form">
|
|
6766
|
+
<input id="password" type="password" placeholder="Master password" required />
|
|
6767
|
+
<input id="totp" inputmode="numeric" autocomplete="one-time-code" placeholder="Authenticator code (if enabled)" />
|
|
6768
|
+
<button type="submit">Unlock</button>
|
|
6769
|
+
</form>
|
|
6770
|
+
</section>
|
|
6771
|
+
<section>
|
|
6772
|
+
<h2>Secrets</h2>
|
|
6773
|
+
<button id="refresh">Refresh</button>
|
|
6774
|
+
<ul id="secret-list"></ul>
|
|
6775
|
+
<pre id="output"></pre>
|
|
6776
|
+
</section>
|
|
6777
|
+
<section id="linked" hidden>
|
|
6778
|
+
<h2>Linked to <span id="linked-name"></span></h2>
|
|
6779
|
+
<ul id="linked-list"></ul>
|
|
6780
|
+
<p id="linked-from"></p>
|
|
6781
|
+
<form id="link-form">
|
|
6782
|
+
<input id="link-ref" placeholder="Secret name or id to link" required />
|
|
6783
|
+
<button type="submit">Link</button>
|
|
6784
|
+
</form>
|
|
6785
|
+
</section>
|
|
6786
|
+
</main>
|
|
6787
|
+
<script>
|
|
6788
|
+
const output = document.getElementById('output');
|
|
6789
|
+
const secretList = document.getElementById('secret-list');
|
|
6790
|
+
async function refresh() {
|
|
6791
|
+
const response = await fetch('/api/secrets');
|
|
6792
|
+
if (!response.ok) {
|
|
6793
|
+
output.textContent = await response.text();
|
|
6794
|
+
return;
|
|
6795
|
+
}
|
|
6796
|
+
const data = await response.json();
|
|
6797
|
+
secretList.innerHTML = '';
|
|
6798
|
+
data.data.forEach((secret) => {
|
|
6799
|
+
const item = document.createElement('li');
|
|
6800
|
+
const open = document.createElement('a');
|
|
6801
|
+
open.href = '#';
|
|
6802
|
+
open.textContent = secret.name + " (" + secret.type + ")" + (secret.linkCount ? " \xB7 " + secret.linkCount + " linked" : "");
|
|
6803
|
+
open.addEventListener('click', (event) => { event.preventDefault(); showSecret(secret.id); });
|
|
6804
|
+
item.appendChild(open);
|
|
6805
|
+
secretList.appendChild(item);
|
|
6806
|
+
});
|
|
6807
|
+
output.textContent = JSON.stringify(data, null, 2);
|
|
6808
|
+
}
|
|
6809
|
+
let current = null;
|
|
6810
|
+
async function showSecret(ref) {
|
|
6811
|
+
const response = await fetch('/api/secrets/' + encodeURIComponent(ref));
|
|
6812
|
+
if (!response.ok) {
|
|
6813
|
+
output.textContent = await response.text();
|
|
6814
|
+
return;
|
|
6815
|
+
}
|
|
6816
|
+
const secret = (await response.json()).data;
|
|
6817
|
+
current = secret;
|
|
6818
|
+
const { links, linkedFrom, ...rest } = secret;
|
|
6819
|
+
output.textContent = JSON.stringify(rest, null, 2);
|
|
6820
|
+
document.getElementById('linked').hidden = false;
|
|
6821
|
+
document.getElementById('linked-name').textContent = secret.name;
|
|
6822
|
+
const list = document.getElementById('linked-list');
|
|
6823
|
+
list.innerHTML = '';
|
|
6824
|
+
if (!links.length) {
|
|
6825
|
+
list.innerHTML = '<li>No linked secrets</li>';
|
|
6826
|
+
}
|
|
6827
|
+
links.forEach((linked) => {
|
|
6828
|
+
const item = document.createElement('li');
|
|
6829
|
+
const open = document.createElement('a');
|
|
6830
|
+
open.href = '#';
|
|
6831
|
+
open.textContent = linked.name + " (" + linked.type + ") ";
|
|
6832
|
+
open.addEventListener('click', (event) => { event.preventDefault(); showSecret(linked.id); });
|
|
6833
|
+
const unlink = document.createElement('button');
|
|
6834
|
+
unlink.textContent = 'Unlink';
|
|
6835
|
+
unlink.addEventListener('click', () => changeLink('DELETE', linked.id));
|
|
6836
|
+
item.append(open, unlink);
|
|
6837
|
+
list.appendChild(item);
|
|
6838
|
+
});
|
|
6839
|
+
document.getElementById('linked-from').textContent = linkedFrom.length
|
|
6840
|
+
? 'Linked from: ' + linkedFrom.map((primary) => primary.name).join(', ')
|
|
6841
|
+
: '';
|
|
6842
|
+
}
|
|
6843
|
+
async function changeLink(method, secret) {
|
|
6844
|
+
const response = await fetch('/api/links', {
|
|
6845
|
+
method,
|
|
6846
|
+
headers: { 'content-type': 'application/json' },
|
|
6847
|
+
body: JSON.stringify({ primary: current.id, secret })
|
|
6848
|
+
});
|
|
6849
|
+
if (!response.ok) {
|
|
6850
|
+
output.textContent = await response.text();
|
|
6851
|
+
return;
|
|
6852
|
+
}
|
|
6853
|
+
await showSecret(current.id);
|
|
6854
|
+
await refresh();
|
|
6855
|
+
}
|
|
6856
|
+
document.getElementById('link-form').addEventListener('submit', async (event) => {
|
|
6857
|
+
event.preventDefault();
|
|
6858
|
+
const input = document.getElementById('link-ref');
|
|
6859
|
+
await changeLink('POST', input.value);
|
|
6860
|
+
input.value = '';
|
|
6861
|
+
});
|
|
6862
|
+
document.getElementById('unlock-form').addEventListener('submit', async (event) => {
|
|
6863
|
+
event.preventDefault();
|
|
6864
|
+
const response = await fetch('/api/session/unlock', {
|
|
6865
|
+
method: 'POST',
|
|
6866
|
+
headers: { 'content-type': 'application/json' },
|
|
6867
|
+
body: JSON.stringify({
|
|
6868
|
+
password: document.getElementById('password').value,
|
|
6869
|
+
totp: document.getElementById('totp').value || undefined
|
|
6870
|
+
})
|
|
6871
|
+
});
|
|
6872
|
+
output.textContent = await response.text();
|
|
6873
|
+
if (response.ok) {
|
|
6874
|
+
await refresh();
|
|
6875
|
+
}
|
|
6876
|
+
});
|
|
6877
|
+
document.getElementById('refresh').addEventListener('click', refresh);
|
|
6878
|
+
</script>
|
|
6879
|
+
</body>
|
|
6880
|
+
</html>`;
|
|
6881
|
+
}
|
|
6882
|
+
async function startWebServer(options = {}) {
|
|
6883
|
+
const vaultPath = resolve4(options.vaultPath ?? defaultVaultPath());
|
|
6884
|
+
const host = options.host ?? "127.0.0.1";
|
|
6885
|
+
const port = options.port ?? 8061;
|
|
6886
|
+
const sessions = new Map;
|
|
6887
|
+
const cleanup = () => {
|
|
6888
|
+
for (const [id, session] of sessions.entries()) {
|
|
6889
|
+
if (Date.parse(session.expiresAt) <= Date.now()) {
|
|
6890
|
+
sessions.delete(id);
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6893
|
+
};
|
|
6894
|
+
const server = Bun.serve({
|
|
6895
|
+
fetch: async (request) => {
|
|
6896
|
+
cleanup();
|
|
6897
|
+
const url = new URL(request.url);
|
|
6898
|
+
try {
|
|
6899
|
+
if (request.method === "GET" && url.pathname === "/health") {
|
|
6900
|
+
return json2({ ok: true, running: true, vaultPath });
|
|
6901
|
+
}
|
|
6902
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
6903
|
+
return html(page2());
|
|
6904
|
+
}
|
|
6905
|
+
if (request.method === "POST" && url.pathname === "/api/session/unlock") {
|
|
6906
|
+
const body = await readJson2(request);
|
|
6907
|
+
const ttlSeconds = body.ttlSeconds ?? 3600;
|
|
6908
|
+
if (ttlSeconds <= 0 || ttlSeconds > 86400) {
|
|
6909
|
+
return json2({ error: "Session TTL must be between 1 and 86400 seconds" }, 400);
|
|
6910
|
+
}
|
|
6911
|
+
const { rootKey, sealBound } = VaultService.unlockRootKey(vaultPath, {
|
|
6912
|
+
password: body.password,
|
|
6913
|
+
sealRecord: await loadSealRecord(vaultPath),
|
|
6914
|
+
totp: body.totp
|
|
6915
|
+
});
|
|
6916
|
+
const sessionId = randomBytes6(16).toString("hex");
|
|
6917
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
6918
|
+
sessions.set(sessionId, { expiresAt, rootKey, sealBound });
|
|
6919
|
+
return json2({ expiresAt, sessionId }, 200, { "set-cookie": `autho_session=${sessionId}; HttpOnly; SameSite=Strict; Path=/; Secure` });
|
|
6920
|
+
}
|
|
6921
|
+
if (request.method === "POST" && url.pathname === "/api/session/lock") {
|
|
6922
|
+
const cookies = parseCookies(request);
|
|
6923
|
+
if (cookies.autho_session) {
|
|
6924
|
+
sessions.delete(cookies.autho_session);
|
|
6925
|
+
}
|
|
6926
|
+
return json2({ locked: true }, 200, { "set-cookie": "autho_session=; Max-Age=0; Path=/; Secure" });
|
|
6927
|
+
}
|
|
6928
|
+
if (url.pathname.startsWith("/api/")) {
|
|
6929
|
+
const webSession = requireSession(request, sessions);
|
|
6930
|
+
const session = await openSession(vaultPath, webSession);
|
|
6931
|
+
try {
|
|
6932
|
+
if (request.method === "GET" && url.pathname === "/api/status") {
|
|
6933
|
+
return json2({
|
|
6934
|
+
initialized: true,
|
|
6935
|
+
secretCount: session.listSecrets().length,
|
|
6936
|
+
unlocked: true,
|
|
6937
|
+
vaultPath
|
|
6938
|
+
});
|
|
6939
|
+
}
|
|
6940
|
+
if (request.method === "GET" && url.pathname === "/api/secrets") {
|
|
6941
|
+
return json2({ data: session.listSecretIndex() });
|
|
6942
|
+
}
|
|
6943
|
+
if (request.method === "GET" && url.pathname.startsWith("/api/secrets/")) {
|
|
6944
|
+
const ref = decodeURIComponent(url.pathname.slice("/api/secrets/".length));
|
|
6945
|
+
const { linkedFrom, links } = session.getLinks(ref);
|
|
6946
|
+
return json2({ data: { ...session.getSecret(ref), linkedFrom, links } });
|
|
6947
|
+
}
|
|
6948
|
+
if ((request.method === "POST" || request.method === "DELETE") && url.pathname === "/api/links") {
|
|
6949
|
+
const body = await readJson2(request);
|
|
6950
|
+
return json2({
|
|
6951
|
+
data: request.method === "POST" ? session.linkSecrets(body.primary, body.secret) : session.unlinkSecrets(body.primary, body.secret)
|
|
6952
|
+
});
|
|
6953
|
+
}
|
|
6954
|
+
if (request.method === "POST" && url.pathname === "/api/secrets") {
|
|
6955
|
+
const body = await readJson2(request);
|
|
6956
|
+
return json2({
|
|
6957
|
+
data: session.addSecret({
|
|
6958
|
+
metadata: Object.fromEntries(Object.entries({
|
|
6959
|
+
algorithm: body.algorithm,
|
|
6960
|
+
description: body.description,
|
|
6961
|
+
digits: body.digits,
|
|
6962
|
+
url: body.url
|
|
6963
|
+
}).filter(([, value]) => value !== undefined)),
|
|
6964
|
+
name: body.name,
|
|
6965
|
+
type: body.type,
|
|
6966
|
+
username: body.username,
|
|
6967
|
+
value: body.value
|
|
6968
|
+
})
|
|
6969
|
+
}, 201);
|
|
6970
|
+
}
|
|
6971
|
+
if (request.method === "DELETE" && url.pathname.startsWith("/api/secrets/")) {
|
|
6972
|
+
const ref = decodeURIComponent(url.pathname.slice("/api/secrets/".length));
|
|
6973
|
+
return json2({ data: session.removeSecret(ref, { withLinked: url.searchParams.get("withLinked") === "1" }) });
|
|
6974
|
+
}
|
|
6975
|
+
if (request.method === "GET" && url.pathname.startsWith("/api/otp/")) {
|
|
6976
|
+
const ref = decodeURIComponent(url.pathname.slice("/api/otp/".length));
|
|
6977
|
+
return json2({ data: session.generateOtp(ref) });
|
|
6978
|
+
}
|
|
6979
|
+
} finally {
|
|
6980
|
+
await closeSessionAndRecordSeal(vaultPath, session);
|
|
6981
|
+
}
|
|
6982
|
+
}
|
|
6983
|
+
} catch (error) {
|
|
6984
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6985
|
+
const status = /session/i.test(message) ? 401 : 400;
|
|
6986
|
+
return json2({ error: message }, status);
|
|
6987
|
+
}
|
|
6988
|
+
return json2({ error: "Not found" }, 404);
|
|
6989
|
+
},
|
|
6990
|
+
hostname: host,
|
|
6991
|
+
port
|
|
6992
|
+
});
|
|
6993
|
+
process.on("SIGINT", () => process.exit(0));
|
|
6994
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
6995
|
+
console.log(`Autho web server listening on http://${host}:${server.port}`);
|
|
6996
|
+
await new Promise(() => {});
|
|
6997
|
+
}
|
|
6998
|
+
|
|
6999
|
+
// apps/cli/src/index.ts
|
|
5908
7000
|
function parseArgs(argv) {
|
|
5909
7001
|
const dashDashIndex = argv.indexOf("--");
|
|
5910
7002
|
const main = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
|
|
@@ -5989,8 +7081,16 @@ function output(value, jsonMode = false) {
|
|
|
5989
7081
|
}
|
|
5990
7082
|
console.log(value);
|
|
5991
7083
|
}
|
|
7084
|
+
function printSealChanges(changes, diffTrusted) {
|
|
7085
|
+
if (!diffTrusted && changes.length > 0) {
|
|
7086
|
+
console.log("The stored row signatures were edited too, so the list below may be incomplete.");
|
|
7087
|
+
}
|
|
7088
|
+
for (const change of changes) {
|
|
7089
|
+
console.log(` ${change.change.padEnd(8)} ${change.table} ${change.rowId}`);
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
5992
7092
|
function absolutePath(path) {
|
|
5993
|
-
return
|
|
7093
|
+
return resolve6(path);
|
|
5994
7094
|
}
|
|
5995
7095
|
function buildSecretMetadata(args) {
|
|
5996
7096
|
return Object.fromEntries(Object.entries({
|
|
@@ -6046,7 +7146,7 @@ async function runPromptMode(vaultPath, initialPassword) {
|
|
|
6046
7146
|
const prompt = await createPromptAdapter();
|
|
6047
7147
|
try {
|
|
6048
7148
|
const password = await askPassword(prompt, initialPassword);
|
|
6049
|
-
const session = VaultService.unlock(vaultPath, password);
|
|
7149
|
+
const session = VaultService.unlock(vaultPath, { password, sealRecord: await loadSealRecord(vaultPath) });
|
|
6050
7150
|
try {
|
|
6051
7151
|
const action = (await prompt.ask("Action [create/read/list/delete/otp/exit]: ")).toLowerCase();
|
|
6052
7152
|
if (action === "exit" /* Exit */) {
|
|
@@ -6096,7 +7196,7 @@ async function runPromptMode(vaultPath, initialPassword) {
|
|
|
6096
7196
|
}
|
|
6097
7197
|
throw new Error(`Unknown prompt action: ${action}`);
|
|
6098
7198
|
} finally {
|
|
6099
|
-
session
|
|
7199
|
+
await closeSessionAndRecordSeal(vaultPath, session);
|
|
6100
7200
|
}
|
|
6101
7201
|
} finally {
|
|
6102
7202
|
prompt.close();
|
|
@@ -6119,8 +7219,9 @@ async function passkeyCredentials(vaultPath) {
|
|
|
6119
7219
|
return { password: "", passkey: { credentialId: result.credentialId, prfOutput: result.prfOutput } };
|
|
6120
7220
|
}
|
|
6121
7221
|
async function resolveUnlockCredentials(vaultPath, args, existingPassword) {
|
|
7222
|
+
const sealRecord = await loadSealRecord(vaultPath);
|
|
6122
7223
|
if (wantsPasskey(args)) {
|
|
6123
|
-
return passkeyCredentials(vaultPath);
|
|
7224
|
+
return { ...await passkeyCredentials(vaultPath), sealRecord };
|
|
6124
7225
|
}
|
|
6125
7226
|
let password = existingPassword ?? getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
|
|
6126
7227
|
if (!password) {
|
|
@@ -6129,7 +7230,7 @@ async function resolveUnlockCredentials(vaultPath, args, existingPassword) {
|
|
|
6129
7230
|
if (!password && process.stdin.isTTY) {
|
|
6130
7231
|
password = await readPasswordMasked("Master password: ");
|
|
6131
7232
|
}
|
|
6132
|
-
const creds = { password: required(password, "--password") };
|
|
7233
|
+
const creds = { password: required(password, "--password"), sealRecord };
|
|
6133
7234
|
if (await hasPinSet(vaultPath)) {
|
|
6134
7235
|
const pin = process.env.AUTHO_PIN ?? getString(args, "pin") ?? (process.stdin.isTTY ? await readPasswordMasked("PIN: ") : undefined);
|
|
6135
7236
|
if (!pin)
|
|
@@ -6244,34 +7345,10 @@ async function runInitWizard(vaultPath, password, existingCreds) {
|
|
|
6244
7345
|
console.log("Setup complete. Run \x1B[1mautho init\x1B[0m anytime to change these settings.");
|
|
6245
7346
|
}
|
|
6246
7347
|
async function runWebServer(vaultPath, args) {
|
|
6247
|
-
|
|
6248
|
-
"
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
"--vault",
|
|
6252
|
-
absolutePath(vaultPath)
|
|
6253
|
-
];
|
|
6254
|
-
const host = getString(args, "host");
|
|
6255
|
-
const port = getString(args, "port") ?? String(numberPreference("webPort"));
|
|
6256
|
-
if (host) {
|
|
6257
|
-
commandArgs.push("--host", host);
|
|
6258
|
-
}
|
|
6259
|
-
if (port) {
|
|
6260
|
-
commandArgs.push("--port", port);
|
|
6261
|
-
}
|
|
6262
|
-
await new Promise((resolvePromise, rejectPromise) => {
|
|
6263
|
-
const child = spawn3(process.execPath, commandArgs, {
|
|
6264
|
-
cwd: process.cwd(),
|
|
6265
|
-
stdio: "inherit"
|
|
6266
|
-
});
|
|
6267
|
-
child.on("exit", (code) => {
|
|
6268
|
-
if (code === 0) {
|
|
6269
|
-
resolvePromise();
|
|
6270
|
-
return;
|
|
6271
|
-
}
|
|
6272
|
-
rejectPromise(new Error(`Web server exited with code ${code ?? 1}`));
|
|
6273
|
-
});
|
|
6274
|
-
child.on("error", rejectPromise);
|
|
7348
|
+
await startWebServer({
|
|
7349
|
+
host: getString(args, "host"),
|
|
7350
|
+
port: Number(getString(args, "port") ?? numberPreference("webPort")),
|
|
7351
|
+
vaultPath: absolutePath(vaultPath)
|
|
6275
7352
|
});
|
|
6276
7353
|
}
|
|
6277
7354
|
function help() {
|
|
@@ -6302,12 +7379,17 @@ function help() {
|
|
|
6302
7379
|
" import legacy --file <path> [--skip-existing] [--vault <path>] [--json]",
|
|
6303
7380
|
" secrets add --name <name> --type <password|note|otp> --value <value> [--username <v>] [--url <v>] [--description <v>] [--digits <v>] [--algorithm <v>] [--vault <path>]",
|
|
6304
7381
|
" secrets list [--vault <path>] [--json]",
|
|
6305
|
-
" secrets get --ref <name-or-id> [--vault <path>] [--json]",
|
|
6306
|
-
" secrets rm --ref <name-or-id> [--vault <path>] [--json]",
|
|
7382
|
+
" secrets get --ref <name-or-id> [--links] [--vault <path>] [--json]",
|
|
7383
|
+
" secrets rm --ref <name-or-id> [--with-linked] [--vault <path>] [--json]",
|
|
7384
|
+
" link <primary> <secret> [--vault <path>] [--json] Show <secret> on <primary>'s page",
|
|
7385
|
+
" unlink <primary> <secret> [--vault <path>] [--json]",
|
|
6307
7386
|
" secrets edit --ref <name-or-id> [--new-name <v>] [--value <v>] [--username <v>] [--url <v>] [--description <v>] [--vault <path>] [--json]",
|
|
6308
7387
|
" otp code --ref <name-or-id> [--vault <path>] [--json]",
|
|
6309
|
-
" lease create --secret <ref> [--secret <ref>] [--ttl <seconds>] [--name <v>] [--vault <path>] [--json]",
|
|
7388
|
+
" lease create --secret <ref> [--secret <ref>] [--ttl <seconds>] [--name <v>] [--no-links] [--vault <path>] [--json]",
|
|
7389
|
+
" lease show --lease <id> [--vault <path>] [--json]",
|
|
6310
7390
|
" lease revoke --lease <id> [--vault <path>] [--json]",
|
|
7391
|
+
" vault verify [--vault <path>] [--json]",
|
|
7392
|
+
" vault reseal [--accept] [--vault <path>] [--json]",
|
|
6311
7393
|
" env render --map <ENV=ref> [--project-file <path>] [--lease <id>] [--vault <path>] [--json]",
|
|
6312
7394
|
" env sync --map <ENV=ref> [--project-file <path>] [--lease <id>] [--ttl <seconds>] [--output <path>] [--force] [--vault <path>] [--json]",
|
|
6313
7395
|
" exec --map <ENV=ref> [--project-file <path>] [--lease <id>] [--vault <path>] -- <cmd>",
|
|
@@ -6358,6 +7440,9 @@ function help() {
|
|
|
6358
7440
|
" Config keys: vaultDir, revealSeconds, clipboardClearSeconds, idleLockSeconds, passwordLength,",
|
|
6359
7441
|
" passwordSymbols, mouse, defaultLeaseTtl, daemonTtlSeconds, webPort (see `autho config show`).",
|
|
6360
7442
|
" lease create uses the defaultLeaseTtl setting when --ttl is omitted (0 = --ttl required).",
|
|
7443
|
+
" A lease on a secret also covers the secrets linked to it, read live (--no-links to opt out).",
|
|
7444
|
+
" env and exec inject an OTP secret's current code, never its seed.",
|
|
7445
|
+
" The vault is sealed: edits made outside autho stop env, exec and leases until `autho vault reseal`.",
|
|
6361
7446
|
" In the TUI, press , for Settings. `autho --version` prints the installed version.",
|
|
6362
7447
|
" The default vault path is ~/.autho/vault.db (override with config or AUTHO_HOME).",
|
|
6363
7448
|
" The default project file is <vaultDir>/project.json.",
|
|
@@ -6419,7 +7504,10 @@ async function main() {
|
|
|
6419
7504
|
"audit" /* Audit */,
|
|
6420
7505
|
"import" /* Import */,
|
|
6421
7506
|
"recovery" /* Recovery */,
|
|
6422
|
-
"unlock" /* Unlock
|
|
7507
|
+
"unlock" /* Unlock */,
|
|
7508
|
+
"link" /* Link */,
|
|
7509
|
+
"unlink" /* Unlink */,
|
|
7510
|
+
"vault" /* Vault */
|
|
6423
7511
|
].includes(scope ?? "");
|
|
6424
7512
|
const daemonNeedsPassword = scope === "daemon" /* Daemon */ && action === "unlock" /* Unlock */;
|
|
6425
7513
|
if (needsPassword || daemonNeedsPassword) {
|
|
@@ -6437,7 +7525,7 @@ async function main() {
|
|
|
6437
7525
|
console.log(" Tip: Use a cloud-synced folder (e.g. Google Drive) to share the vault across machines.");
|
|
6438
7526
|
const customDir = (await readLine(` Path (Enter to keep default): `)).trim();
|
|
6439
7527
|
if (customDir && customDir !== currentDir) {
|
|
6440
|
-
const resolved =
|
|
7528
|
+
const resolved = resolve6(customDir).replace(/\\/g, "/");
|
|
6441
7529
|
saveConfig({ ...config, vaultDir: resolved });
|
|
6442
7530
|
effectiveVaultPath = resolved + "/vault.db";
|
|
6443
7531
|
console.log(` \x1B[32m\u2713\x1B[0m Vault directory set to ${resolved}`);
|
|
@@ -6505,7 +7593,7 @@ async function main() {
|
|
|
6505
7593
|
if (key === "autoLock") {
|
|
6506
7594
|
updated.autoLock = value === "true";
|
|
6507
7595
|
} else if (key === "vaultDir") {
|
|
6508
|
-
updated.vaultDir = isAbsolute(value) ? value :
|
|
7596
|
+
updated.vaultDir = isAbsolute(value) ? value : resolve6(value);
|
|
6509
7597
|
} else if (key === "editor") {
|
|
6510
7598
|
updated.editor = value;
|
|
6511
7599
|
} else if (key === "autoLockTimeout") {
|
|
@@ -6595,9 +7683,13 @@ async function main() {
|
|
|
6595
7683
|
return;
|
|
6596
7684
|
}
|
|
6597
7685
|
if (scope === "daemon" /* Daemon */ && action === "unlock" /* Unlock */) {
|
|
7686
|
+
const creds = await resolveUnlockCredentials(vaultPath, args, password);
|
|
7687
|
+
if (creds.passkey || creds.recovery)
|
|
7688
|
+
throw new Error("daemon unlock needs the master password");
|
|
6598
7689
|
output(await daemonUnlock({
|
|
6599
|
-
password:
|
|
7690
|
+
password: creds.password,
|
|
6600
7691
|
statePath,
|
|
7692
|
+
totp: creds.totp,
|
|
6601
7693
|
ttlSeconds: getString(args, "ttl") ? Number(getString(args, "ttl")) : numberPreference("daemonTtlSeconds")
|
|
6602
7694
|
}), jsonMode);
|
|
6603
7695
|
return;
|
|
@@ -6683,6 +7775,7 @@ async function main() {
|
|
|
6683
7775
|
info = admin.addPasskey({ ...result, label, prfSalt });
|
|
6684
7776
|
} finally {
|
|
6685
7777
|
admin.close();
|
|
7778
|
+
await saveSealRecord(vaultPath, admin.sealRecord());
|
|
6686
7779
|
}
|
|
6687
7780
|
if (jsonMode) {
|
|
6688
7781
|
output({ added: true, id: info.id, label: info.label }, true);
|
|
@@ -6701,6 +7794,31 @@ async function main() {
|
|
|
6701
7794
|
if (scope === "passkey" /* Passkey */) {
|
|
6702
7795
|
throw new Error(`Unknown passkey action: ${action}. Use add, list or remove.`);
|
|
6703
7796
|
}
|
|
7797
|
+
if (scope === "vault" /* Vault */ && action === "reseal" /* Reseal */) {
|
|
7798
|
+
const mode = getBoolean(args, "accept") ? "accept" : "quarantine";
|
|
7799
|
+
const result = VaultService.reseal(vaultPath, await resolveUnlockCredentials(vaultPath, args, password), mode);
|
|
7800
|
+
await saveSealRecord(vaultPath, result.sealRecord);
|
|
7801
|
+
if (jsonMode) {
|
|
7802
|
+
output(result, true);
|
|
7803
|
+
} else if (result.wasIntact) {
|
|
7804
|
+
console.log("The vault seal is intact; nothing to do.");
|
|
7805
|
+
} else {
|
|
7806
|
+
if (result.reason)
|
|
7807
|
+
console.log(`Seal problem: ${result.reason}.`);
|
|
7808
|
+
printSealChanges(result.changes, result.diffTrusted);
|
|
7809
|
+
if (mode === "quarantine") {
|
|
7810
|
+
console.log(`Revoked ${result.revokedLeases} active lease(s).`);
|
|
7811
|
+
if (result.undone.length)
|
|
7812
|
+
console.log(`Undid ${result.undone.length} change(s).`);
|
|
7813
|
+
if (result.kept.length) {
|
|
7814
|
+
console.log(`Couldn't undo ${result.kept.length} change(s); check these by hand:`);
|
|
7815
|
+
printSealChanges(result.kept, true);
|
|
7816
|
+
}
|
|
7817
|
+
}
|
|
7818
|
+
console.log(mode === "accept" ? "Vault re-sealed as it is now." : "Vault re-sealed.");
|
|
7819
|
+
}
|
|
7820
|
+
return;
|
|
7821
|
+
}
|
|
6704
7822
|
if (scope === "password" /* Password */ && action === "change" /* Change */) {
|
|
6705
7823
|
const admin = VaultService.authorize(vaultPath, await resolveUnlockCredentials(vaultPath, args, password));
|
|
6706
7824
|
try {
|
|
@@ -6723,6 +7841,7 @@ async function main() {
|
|
|
6723
7841
|
}
|
|
6724
7842
|
} finally {
|
|
6725
7843
|
admin.close();
|
|
7844
|
+
await saveSealRecord(vaultPath, admin.sealRecord());
|
|
6726
7845
|
}
|
|
6727
7846
|
return;
|
|
6728
7847
|
}
|
|
@@ -6770,13 +7889,41 @@ async function main() {
|
|
|
6770
7889
|
return;
|
|
6771
7890
|
}
|
|
6772
7891
|
if (scope === "secrets" /* Secrets */ && action === "get" /* Get */) {
|
|
6773
|
-
const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
|
|
6774
|
-
|
|
7892
|
+
const ref = required(getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id"), "--ref");
|
|
7893
|
+
const secret = session.getSecret(ref);
|
|
7894
|
+
output(getBoolean(args, "links") ? { ...secret, ...session.getLinks(secret.id) } : secret, jsonMode);
|
|
6775
7895
|
return;
|
|
6776
7896
|
}
|
|
6777
7897
|
if (scope === "secrets" /* Secrets */ && action === "rm" /* Rm */) {
|
|
6778
7898
|
const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
|
|
6779
|
-
output(session.removeSecret(required(ref, "--ref")), jsonMode);
|
|
7899
|
+
output(session.removeSecret(required(ref, "--ref"), { withLinked: getBoolean(args, "with-linked") }), jsonMode);
|
|
7900
|
+
return;
|
|
7901
|
+
}
|
|
7902
|
+
if (scope === "link" /* Link */ || scope === "unlink" /* Unlink */) {
|
|
7903
|
+
const primary = required(action ?? getString(args, "primary"), "<primary>");
|
|
7904
|
+
const linked = required(subaction ?? getString(args, "secret"), "<secret>");
|
|
7905
|
+
output(scope === "link" /* Link */ ? session.linkSecrets(primary, linked) : session.unlinkSecrets(primary, linked), jsonMode);
|
|
7906
|
+
return;
|
|
7907
|
+
}
|
|
7908
|
+
if (scope === "vault" /* Vault */ && action === "verify" /* Verify */) {
|
|
7909
|
+
const seal = session.verifySeal();
|
|
7910
|
+
if (jsonMode) {
|
|
7911
|
+
output(seal, true);
|
|
7912
|
+
} else if (seal.state === "intact") {
|
|
7913
|
+
console.log("Vault seal is intact.");
|
|
7914
|
+
} else if (seal.state === "unsealed") {
|
|
7915
|
+
console.log("Vault isn't sealed yet. Unlock it with the master password to seal it.");
|
|
7916
|
+
} else {
|
|
7917
|
+
console.log(`Vault was modified outside autho: ${seal.reason}.`);
|
|
7918
|
+
printSealChanges(seal.changes, seal.diffTrusted);
|
|
7919
|
+
console.log("Run `autho vault reseal` to undo the changes and revoke leases, or `autho vault reseal --accept` if you made the change.");
|
|
7920
|
+
}
|
|
7921
|
+
if (seal.state === "tampered")
|
|
7922
|
+
process.exitCode = 2;
|
|
7923
|
+
return;
|
|
7924
|
+
}
|
|
7925
|
+
if (scope === "lease" /* Lease */ && action === "show" /* Show */) {
|
|
7926
|
+
output(session.showLease(required(getString(args, "lease"), "--lease")), jsonMode);
|
|
6780
7927
|
return;
|
|
6781
7928
|
}
|
|
6782
7929
|
if (scope === "secrets" /* Secrets */ && action === "edit" /* Edit */) {
|
|
@@ -6804,6 +7951,7 @@ async function main() {
|
|
|
6804
7951
|
if (scope === "lease" /* Lease */ && action === "create" /* Create */) {
|
|
6805
7952
|
output(session.createLease({
|
|
6806
7953
|
name: getString(args, "name") ?? "session",
|
|
7954
|
+
noLinks: getBoolean(args, "no-links"),
|
|
6807
7955
|
secretRefs: getStrings(args, "secret"),
|
|
6808
7956
|
ttlSeconds: leaseTtl(args)
|
|
6809
7957
|
}), jsonMode);
|
|
@@ -6868,7 +8016,7 @@ async function main() {
|
|
|
6868
8016
|
}
|
|
6869
8017
|
throw new Error(`Unknown command: ${[scope, action, subaction].filter(Boolean).join(" ")}`);
|
|
6870
8018
|
} finally {
|
|
6871
|
-
session
|
|
8019
|
+
await closeSessionAndRecordSeal(vaultPath, session);
|
|
6872
8020
|
}
|
|
6873
8021
|
}
|
|
6874
8022
|
main().catch((error) => {
|