autho 3.0.3 → 3.0.4
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 +1107 -249
- 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.4",
|
|
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 };
|
|
1983
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
|
+
}
|
|
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 };
|
|
2125
2640
|
});
|
|
2126
|
-
return { id: secret.id, name: secret.name };
|
|
2127
2641
|
}
|
|
2128
|
-
|
|
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 };
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
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
|
" ",
|
|
@@ -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(["", ""]);
|
|
@@ -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
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
5577
|
+
}
|
|
5578
|
+
} else {
|
|
5579
|
+
if (key.name === "right" || key.name === "l") {
|
|
5580
|
+
focusPane("actions");
|
|
5581
|
+
return;
|
|
5582
|
+
}
|
|
5583
|
+
if (isEnter(key)) {
|
|
5584
|
+
if (showDetail)
|
|
5585
|
+
copyPrimary(selected);
|
|
5586
|
+
else
|
|
5587
|
+
focusPane("actions");
|
|
4799
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();
|
|
@@ -5544,13 +6324,14 @@ async function readPasswordMasked(prompt = "Master password: ") {
|
|
|
5544
6324
|
init_src();
|
|
5545
6325
|
init_src2();
|
|
5546
6326
|
init_src3();
|
|
6327
|
+
init_os_secrets();
|
|
5547
6328
|
init_paths();
|
|
5548
6329
|
init_paths();
|
|
5549
|
-
import { createHash as
|
|
6330
|
+
import { createHash as createHash3, timingSafeEqual as timingSafeEqual4 } from "crypto";
|
|
5550
6331
|
import { existsSync as existsSync4, readFileSync as readFileSync4, rmSync } from "fs";
|
|
5551
6332
|
var DAEMON_TOKEN_SERVICE = "autho.daemon";
|
|
5552
6333
|
function daemonTokenName(statePath) {
|
|
5553
|
-
return
|
|
6334
|
+
return createHash3("sha256").update(statePath).digest("hex");
|
|
5554
6335
|
}
|
|
5555
6336
|
async function storeDaemonToken(statePath, token) {
|
|
5556
6337
|
const tokenName = daemonTokenName(statePath);
|
|
@@ -5641,20 +6422,11 @@ async function deleteDaemonState(statePath) {
|
|
|
5641
6422
|
rmSync(statePath, { force: true });
|
|
5642
6423
|
}
|
|
5643
6424
|
}
|
|
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
|
-
}
|
|
6425
|
+
async function openSessionFromRootKey(session) {
|
|
6426
|
+
return new VaultSession(new AuthoDatabase(session.vaultPath), session.rootKey, {
|
|
6427
|
+
sealBound: session.sealBound,
|
|
6428
|
+
sealRecord: await loadSealRecord(session.vaultPath)
|
|
6429
|
+
});
|
|
5658
6430
|
}
|
|
5659
6431
|
async function readJson(request) {
|
|
5660
6432
|
return await request.json();
|
|
@@ -5698,7 +6470,7 @@ async function startDaemonServer(options) {
|
|
|
5698
6470
|
};
|
|
5699
6471
|
const auth = (request) => {
|
|
5700
6472
|
const provided = getBearerToken(request);
|
|
5701
|
-
if (!provided || provided.length !== token.length || !
|
|
6473
|
+
if (!provided || provided.length !== token.length || !timingSafeEqual4(Buffer.from(provided), Buffer.from(token))) {
|
|
5702
6474
|
return unauthorized();
|
|
5703
6475
|
}
|
|
5704
6476
|
return null;
|
|
@@ -5748,13 +6520,18 @@ async function startDaemonServer(options) {
|
|
|
5748
6520
|
if (ttlSeconds <= 0 || ttlSeconds > 86400) {
|
|
5749
6521
|
return json({ error: "Unlock ttl must be between 1 and 86400 seconds" }, 400);
|
|
5750
6522
|
}
|
|
5751
|
-
const rootKey =
|
|
6523
|
+
const { rootKey, sealBound } = VaultService.unlockRootKey(options.vaultPath, {
|
|
6524
|
+
password: body.password,
|
|
6525
|
+
sealRecord: await loadSealRecord(options.vaultPath),
|
|
6526
|
+
totp: body.totp
|
|
6527
|
+
});
|
|
5752
6528
|
const sessionId = randomId();
|
|
5753
6529
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
5754
6530
|
sessions.set(sessionId, {
|
|
5755
6531
|
expiresAt,
|
|
5756
6532
|
id: sessionId,
|
|
5757
6533
|
rootKey,
|
|
6534
|
+
sealBound,
|
|
5758
6535
|
vaultPath: options.vaultPath
|
|
5759
6536
|
});
|
|
5760
6537
|
return json({
|
|
@@ -5773,11 +6550,11 @@ async function startDaemonServer(options) {
|
|
|
5773
6550
|
if (!session) {
|
|
5774
6551
|
return json({ error: `Unknown daemon session: ${body.sessionId}` }, 404);
|
|
5775
6552
|
}
|
|
5776
|
-
const vaultSession = openSessionFromRootKey(session
|
|
6553
|
+
const vaultSession = await openSessionFromRootKey(session);
|
|
5777
6554
|
try {
|
|
5778
6555
|
return json(vaultSession.renderEnv(body.mappings, body.leaseId));
|
|
5779
6556
|
} finally {
|
|
5780
|
-
vaultSession
|
|
6557
|
+
await closeSessionAndRecordSeal(session.vaultPath, vaultSession);
|
|
5781
6558
|
}
|
|
5782
6559
|
}
|
|
5783
6560
|
if (request.method === "POST" && url.pathname === "/exec") {
|
|
@@ -5786,11 +6563,11 @@ async function startDaemonServer(options) {
|
|
|
5786
6563
|
if (!session) {
|
|
5787
6564
|
return json({ error: `Unknown daemon session: ${body.sessionId}` }, 404);
|
|
5788
6565
|
}
|
|
5789
|
-
const vaultSession = openSessionFromRootKey(session
|
|
6566
|
+
const vaultSession = await openSessionFromRootKey(session);
|
|
5790
6567
|
try {
|
|
5791
6568
|
return json(vaultSession.runExec(body));
|
|
5792
6569
|
} finally {
|
|
5793
|
-
vaultSession
|
|
6570
|
+
await closeSessionAndRecordSeal(session.vaultPath, vaultSession);
|
|
5794
6571
|
}
|
|
5795
6572
|
}
|
|
5796
6573
|
if (request.method === "POST" && url.pathname === "/shutdown") {
|
|
@@ -5861,6 +6638,7 @@ async function daemonUnlock(options) {
|
|
|
5861
6638
|
}
|
|
5862
6639
|
return daemonRequest(state, "/unlock", {
|
|
5863
6640
|
password: options.password,
|
|
6641
|
+
totp: options.totp,
|
|
5864
6642
|
ttlSeconds: options.ttlSeconds
|
|
5865
6643
|
});
|
|
5866
6644
|
}
|
|
@@ -5989,6 +6767,14 @@ function output(value, jsonMode = false) {
|
|
|
5989
6767
|
}
|
|
5990
6768
|
console.log(value);
|
|
5991
6769
|
}
|
|
6770
|
+
function printSealChanges(changes, diffTrusted) {
|
|
6771
|
+
if (!diffTrusted && changes.length > 0) {
|
|
6772
|
+
console.log("The stored row signatures were edited too, so the list below may be incomplete.");
|
|
6773
|
+
}
|
|
6774
|
+
for (const change of changes) {
|
|
6775
|
+
console.log(` ${change.change.padEnd(8)} ${change.table} ${change.rowId}`);
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
5992
6778
|
function absolutePath(path) {
|
|
5993
6779
|
return resolve5(path);
|
|
5994
6780
|
}
|
|
@@ -6046,7 +6832,7 @@ async function runPromptMode(vaultPath, initialPassword) {
|
|
|
6046
6832
|
const prompt = await createPromptAdapter();
|
|
6047
6833
|
try {
|
|
6048
6834
|
const password = await askPassword(prompt, initialPassword);
|
|
6049
|
-
const session = VaultService.unlock(vaultPath, password);
|
|
6835
|
+
const session = VaultService.unlock(vaultPath, { password, sealRecord: await loadSealRecord(vaultPath) });
|
|
6050
6836
|
try {
|
|
6051
6837
|
const action = (await prompt.ask("Action [create/read/list/delete/otp/exit]: ")).toLowerCase();
|
|
6052
6838
|
if (action === "exit" /* Exit */) {
|
|
@@ -6096,7 +6882,7 @@ async function runPromptMode(vaultPath, initialPassword) {
|
|
|
6096
6882
|
}
|
|
6097
6883
|
throw new Error(`Unknown prompt action: ${action}`);
|
|
6098
6884
|
} finally {
|
|
6099
|
-
session
|
|
6885
|
+
await closeSessionAndRecordSeal(vaultPath, session);
|
|
6100
6886
|
}
|
|
6101
6887
|
} finally {
|
|
6102
6888
|
prompt.close();
|
|
@@ -6119,8 +6905,9 @@ async function passkeyCredentials(vaultPath) {
|
|
|
6119
6905
|
return { password: "", passkey: { credentialId: result.credentialId, prfOutput: result.prfOutput } };
|
|
6120
6906
|
}
|
|
6121
6907
|
async function resolveUnlockCredentials(vaultPath, args, existingPassword) {
|
|
6908
|
+
const sealRecord = await loadSealRecord(vaultPath);
|
|
6122
6909
|
if (wantsPasskey(args)) {
|
|
6123
|
-
return passkeyCredentials(vaultPath);
|
|
6910
|
+
return { ...await passkeyCredentials(vaultPath), sealRecord };
|
|
6124
6911
|
}
|
|
6125
6912
|
let password = existingPassword ?? getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
|
|
6126
6913
|
if (!password) {
|
|
@@ -6129,7 +6916,7 @@ async function resolveUnlockCredentials(vaultPath, args, existingPassword) {
|
|
|
6129
6916
|
if (!password && process.stdin.isTTY) {
|
|
6130
6917
|
password = await readPasswordMasked("Master password: ");
|
|
6131
6918
|
}
|
|
6132
|
-
const creds = { password: required(password, "--password") };
|
|
6919
|
+
const creds = { password: required(password, "--password"), sealRecord };
|
|
6133
6920
|
if (await hasPinSet(vaultPath)) {
|
|
6134
6921
|
const pin = process.env.AUTHO_PIN ?? getString(args, "pin") ?? (process.stdin.isTTY ? await readPasswordMasked("PIN: ") : undefined);
|
|
6135
6922
|
if (!pin)
|
|
@@ -6302,12 +7089,17 @@ function help() {
|
|
|
6302
7089
|
" import legacy --file <path> [--skip-existing] [--vault <path>] [--json]",
|
|
6303
7090
|
" secrets add --name <name> --type <password|note|otp> --value <value> [--username <v>] [--url <v>] [--description <v>] [--digits <v>] [--algorithm <v>] [--vault <path>]",
|
|
6304
7091
|
" 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]",
|
|
7092
|
+
" secrets get --ref <name-or-id> [--links] [--vault <path>] [--json]",
|
|
7093
|
+
" secrets rm --ref <name-or-id> [--with-linked] [--vault <path>] [--json]",
|
|
7094
|
+
" link <primary> <secret> [--vault <path>] [--json] Show <secret> on <primary>'s page",
|
|
7095
|
+
" unlink <primary> <secret> [--vault <path>] [--json]",
|
|
6307
7096
|
" secrets edit --ref <name-or-id> [--new-name <v>] [--value <v>] [--username <v>] [--url <v>] [--description <v>] [--vault <path>] [--json]",
|
|
6308
7097
|
" otp code --ref <name-or-id> [--vault <path>] [--json]",
|
|
6309
|
-
" lease create --secret <ref> [--secret <ref>] [--ttl <seconds>] [--name <v>] [--vault <path>] [--json]",
|
|
7098
|
+
" lease create --secret <ref> [--secret <ref>] [--ttl <seconds>] [--name <v>] [--no-links] [--vault <path>] [--json]",
|
|
7099
|
+
" lease show --lease <id> [--vault <path>] [--json]",
|
|
6310
7100
|
" lease revoke --lease <id> [--vault <path>] [--json]",
|
|
7101
|
+
" vault verify [--vault <path>] [--json]",
|
|
7102
|
+
" vault reseal [--accept] [--vault <path>] [--json]",
|
|
6311
7103
|
" env render --map <ENV=ref> [--project-file <path>] [--lease <id>] [--vault <path>] [--json]",
|
|
6312
7104
|
" env sync --map <ENV=ref> [--project-file <path>] [--lease <id>] [--ttl <seconds>] [--output <path>] [--force] [--vault <path>] [--json]",
|
|
6313
7105
|
" exec --map <ENV=ref> [--project-file <path>] [--lease <id>] [--vault <path>] -- <cmd>",
|
|
@@ -6358,6 +7150,9 @@ function help() {
|
|
|
6358
7150
|
" Config keys: vaultDir, revealSeconds, clipboardClearSeconds, idleLockSeconds, passwordLength,",
|
|
6359
7151
|
" passwordSymbols, mouse, defaultLeaseTtl, daemonTtlSeconds, webPort (see `autho config show`).",
|
|
6360
7152
|
" lease create uses the defaultLeaseTtl setting when --ttl is omitted (0 = --ttl required).",
|
|
7153
|
+
" A lease on a secret also covers the secrets linked to it, read live (--no-links to opt out).",
|
|
7154
|
+
" env and exec inject an OTP secret's current code, never its seed.",
|
|
7155
|
+
" The vault is sealed: edits made outside autho stop env, exec and leases until `autho vault reseal`.",
|
|
6361
7156
|
" In the TUI, press , for Settings. `autho --version` prints the installed version.",
|
|
6362
7157
|
" The default vault path is ~/.autho/vault.db (override with config or AUTHO_HOME).",
|
|
6363
7158
|
" The default project file is <vaultDir>/project.json.",
|
|
@@ -6419,7 +7214,10 @@ async function main() {
|
|
|
6419
7214
|
"audit" /* Audit */,
|
|
6420
7215
|
"import" /* Import */,
|
|
6421
7216
|
"recovery" /* Recovery */,
|
|
6422
|
-
"unlock" /* Unlock
|
|
7217
|
+
"unlock" /* Unlock */,
|
|
7218
|
+
"link" /* Link */,
|
|
7219
|
+
"unlink" /* Unlink */,
|
|
7220
|
+
"vault" /* Vault */
|
|
6423
7221
|
].includes(scope ?? "");
|
|
6424
7222
|
const daemonNeedsPassword = scope === "daemon" /* Daemon */ && action === "unlock" /* Unlock */;
|
|
6425
7223
|
if (needsPassword || daemonNeedsPassword) {
|
|
@@ -6595,9 +7393,13 @@ async function main() {
|
|
|
6595
7393
|
return;
|
|
6596
7394
|
}
|
|
6597
7395
|
if (scope === "daemon" /* Daemon */ && action === "unlock" /* Unlock */) {
|
|
7396
|
+
const creds = await resolveUnlockCredentials(vaultPath, args, password);
|
|
7397
|
+
if (creds.passkey || creds.recovery)
|
|
7398
|
+
throw new Error("daemon unlock needs the master password");
|
|
6598
7399
|
output(await daemonUnlock({
|
|
6599
|
-
password:
|
|
7400
|
+
password: creds.password,
|
|
6600
7401
|
statePath,
|
|
7402
|
+
totp: creds.totp,
|
|
6601
7403
|
ttlSeconds: getString(args, "ttl") ? Number(getString(args, "ttl")) : numberPreference("daemonTtlSeconds")
|
|
6602
7404
|
}), jsonMode);
|
|
6603
7405
|
return;
|
|
@@ -6683,6 +7485,7 @@ async function main() {
|
|
|
6683
7485
|
info = admin.addPasskey({ ...result, label, prfSalt });
|
|
6684
7486
|
} finally {
|
|
6685
7487
|
admin.close();
|
|
7488
|
+
await saveSealRecord(vaultPath, admin.sealRecord());
|
|
6686
7489
|
}
|
|
6687
7490
|
if (jsonMode) {
|
|
6688
7491
|
output({ added: true, id: info.id, label: info.label }, true);
|
|
@@ -6701,6 +7504,31 @@ async function main() {
|
|
|
6701
7504
|
if (scope === "passkey" /* Passkey */) {
|
|
6702
7505
|
throw new Error(`Unknown passkey action: ${action}. Use add, list or remove.`);
|
|
6703
7506
|
}
|
|
7507
|
+
if (scope === "vault" /* Vault */ && action === "reseal" /* Reseal */) {
|
|
7508
|
+
const mode = getBoolean(args, "accept") ? "accept" : "quarantine";
|
|
7509
|
+
const result = VaultService.reseal(vaultPath, await resolveUnlockCredentials(vaultPath, args, password), mode);
|
|
7510
|
+
await saveSealRecord(vaultPath, result.sealRecord);
|
|
7511
|
+
if (jsonMode) {
|
|
7512
|
+
output(result, true);
|
|
7513
|
+
} else if (result.wasIntact) {
|
|
7514
|
+
console.log("The vault seal is intact; nothing to do.");
|
|
7515
|
+
} else {
|
|
7516
|
+
if (result.reason)
|
|
7517
|
+
console.log(`Seal problem: ${result.reason}.`);
|
|
7518
|
+
printSealChanges(result.changes, result.diffTrusted);
|
|
7519
|
+
if (mode === "quarantine") {
|
|
7520
|
+
console.log(`Revoked ${result.revokedLeases} active lease(s).`);
|
|
7521
|
+
if (result.undone.length)
|
|
7522
|
+
console.log(`Undid ${result.undone.length} change(s).`);
|
|
7523
|
+
if (result.kept.length) {
|
|
7524
|
+
console.log(`Couldn't undo ${result.kept.length} change(s); check these by hand:`);
|
|
7525
|
+
printSealChanges(result.kept, true);
|
|
7526
|
+
}
|
|
7527
|
+
}
|
|
7528
|
+
console.log(mode === "accept" ? "Vault re-sealed as it is now." : "Vault re-sealed.");
|
|
7529
|
+
}
|
|
7530
|
+
return;
|
|
7531
|
+
}
|
|
6704
7532
|
if (scope === "password" /* Password */ && action === "change" /* Change */) {
|
|
6705
7533
|
const admin = VaultService.authorize(vaultPath, await resolveUnlockCredentials(vaultPath, args, password));
|
|
6706
7534
|
try {
|
|
@@ -6723,6 +7551,7 @@ async function main() {
|
|
|
6723
7551
|
}
|
|
6724
7552
|
} finally {
|
|
6725
7553
|
admin.close();
|
|
7554
|
+
await saveSealRecord(vaultPath, admin.sealRecord());
|
|
6726
7555
|
}
|
|
6727
7556
|
return;
|
|
6728
7557
|
}
|
|
@@ -6770,13 +7599,41 @@ async function main() {
|
|
|
6770
7599
|
return;
|
|
6771
7600
|
}
|
|
6772
7601
|
if (scope === "secrets" /* Secrets */ && action === "get" /* Get */) {
|
|
6773
|
-
const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
|
|
6774
|
-
|
|
7602
|
+
const ref = required(getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id"), "--ref");
|
|
7603
|
+
const secret = session.getSecret(ref);
|
|
7604
|
+
output(getBoolean(args, "links") ? { ...secret, ...session.getLinks(secret.id) } : secret, jsonMode);
|
|
6775
7605
|
return;
|
|
6776
7606
|
}
|
|
6777
7607
|
if (scope === "secrets" /* Secrets */ && action === "rm" /* Rm */) {
|
|
6778
7608
|
const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
|
|
6779
|
-
output(session.removeSecret(required(ref, "--ref")), jsonMode);
|
|
7609
|
+
output(session.removeSecret(required(ref, "--ref"), { withLinked: getBoolean(args, "with-linked") }), jsonMode);
|
|
7610
|
+
return;
|
|
7611
|
+
}
|
|
7612
|
+
if (scope === "link" /* Link */ || scope === "unlink" /* Unlink */) {
|
|
7613
|
+
const primary = required(action ?? getString(args, "primary"), "<primary>");
|
|
7614
|
+
const linked = required(subaction ?? getString(args, "secret"), "<secret>");
|
|
7615
|
+
output(scope === "link" /* Link */ ? session.linkSecrets(primary, linked) : session.unlinkSecrets(primary, linked), jsonMode);
|
|
7616
|
+
return;
|
|
7617
|
+
}
|
|
7618
|
+
if (scope === "vault" /* Vault */ && action === "verify" /* Verify */) {
|
|
7619
|
+
const seal = session.verifySeal();
|
|
7620
|
+
if (jsonMode) {
|
|
7621
|
+
output(seal, true);
|
|
7622
|
+
} else if (seal.state === "intact") {
|
|
7623
|
+
console.log("Vault seal is intact.");
|
|
7624
|
+
} else if (seal.state === "unsealed") {
|
|
7625
|
+
console.log("Vault isn't sealed yet. Unlock it with the master password to seal it.");
|
|
7626
|
+
} else {
|
|
7627
|
+
console.log(`Vault was modified outside autho: ${seal.reason}.`);
|
|
7628
|
+
printSealChanges(seal.changes, seal.diffTrusted);
|
|
7629
|
+
console.log("Run `autho vault reseal` to undo the changes and revoke leases, or `autho vault reseal --accept` if you made the change.");
|
|
7630
|
+
}
|
|
7631
|
+
if (seal.state === "tampered")
|
|
7632
|
+
process.exitCode = 2;
|
|
7633
|
+
return;
|
|
7634
|
+
}
|
|
7635
|
+
if (scope === "lease" /* Lease */ && action === "show" /* Show */) {
|
|
7636
|
+
output(session.showLease(required(getString(args, "lease"), "--lease")), jsonMode);
|
|
6780
7637
|
return;
|
|
6781
7638
|
}
|
|
6782
7639
|
if (scope === "secrets" /* Secrets */ && action === "edit" /* Edit */) {
|
|
@@ -6804,6 +7661,7 @@ async function main() {
|
|
|
6804
7661
|
if (scope === "lease" /* Lease */ && action === "create" /* Create */) {
|
|
6805
7662
|
output(session.createLease({
|
|
6806
7663
|
name: getString(args, "name") ?? "session",
|
|
7664
|
+
noLinks: getBoolean(args, "no-links"),
|
|
6807
7665
|
secretRefs: getStrings(args, "secret"),
|
|
6808
7666
|
ttlSeconds: leaseTtl(args)
|
|
6809
7667
|
}), jsonMode);
|
|
@@ -6868,7 +7726,7 @@ async function main() {
|
|
|
6868
7726
|
}
|
|
6869
7727
|
throw new Error(`Unknown command: ${[scope, action, subaction].filter(Boolean).join(" ")}`);
|
|
6870
7728
|
} finally {
|
|
6871
|
-
session
|
|
7729
|
+
await closeSessionAndRecordSeal(vaultPath, session);
|
|
6872
7730
|
}
|
|
6873
7731
|
}
|
|
6874
7732
|
main().catch((error) => {
|