autho 3.0.4 → 3.0.6
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 +775 -146
- 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.6",
|
|
594
594
|
description: "Local-first secret manager for humans and coding agents, rebuilt on Bun",
|
|
595
595
|
type: "module",
|
|
596
596
|
bin: {
|
|
@@ -4105,7 +4105,7 @@ var init_palette = __esm(() => {
|
|
|
4105
4105
|
// apps/cli/src/tui/settings.tsx
|
|
4106
4106
|
import { existsSync as existsSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
4107
4107
|
import { homedir as homedir2 } from "os";
|
|
4108
|
-
import { dirname as dirname3, join as join3, resolve as
|
|
4108
|
+
import { dirname as dirname3, join as join3, resolve as resolve5 } from "path";
|
|
4109
4109
|
import { useKeyboard as useKeyboard4, useTerminalDimensions as useTerminalDimensions3 } from "@opentui/react";
|
|
4110
4110
|
import { useCallback, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef5, useState as useState4 } from "react";
|
|
4111
4111
|
import { jsx as jsx4, jsxs as jsxs4, Fragment } from "@opentui/react/jsx-runtime";
|
|
@@ -4372,7 +4372,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
4372
4372
|
return;
|
|
4373
4373
|
case "vaultDir": {
|
|
4374
4374
|
const config = { ...loadConfig() };
|
|
4375
|
-
const target = first.trim() ?
|
|
4375
|
+
const target = first.trim() ? resolve5(first.trim()).replace(/\\/g, "/") : null;
|
|
4376
4376
|
if (target)
|
|
4377
4377
|
config.vaultDir = target;
|
|
4378
4378
|
else
|
|
@@ -4440,7 +4440,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
|
|
|
4440
4440
|
registerPasskey(first.trim());
|
|
4441
4441
|
return;
|
|
4442
4442
|
case "recoveryPath": {
|
|
4443
|
-
const target =
|
|
4443
|
+
const target = resolve5(first.trim() || join3(homedir2(), "autho-recovery.txt"));
|
|
4444
4444
|
if (existsSync5(target)) {
|
|
4445
4445
|
setError("That file already exists \u2014 pick another name");
|
|
4446
4446
|
return;
|
|
@@ -5305,24 +5305,203 @@ var init_unlock = __esm(() => {
|
|
|
5305
5305
|
init_theme();
|
|
5306
5306
|
});
|
|
5307
5307
|
|
|
5308
|
-
// apps/cli/src/tui/
|
|
5308
|
+
// apps/cli/src/tui/links.tsx
|
|
5309
5309
|
import { useKeyboard as useKeyboard6, useTerminalDimensions as useTerminalDimensions4 } from "@opentui/react";
|
|
5310
|
-
import {
|
|
5311
|
-
import { jsx as jsx6, jsxs as jsxs6
|
|
5310
|
+
import { useMemo as useMemo5, useRef as useRef7, useState as useState6 } from "react";
|
|
5311
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "@opentui/react/jsx-runtime";
|
|
5312
|
+
function LinkPicker({ primary, entries, linkedIds, onToggle, onClose }) {
|
|
5313
|
+
const { width, height } = useTerminalDimensions4();
|
|
5314
|
+
const [query, setQuery] = useState6("");
|
|
5315
|
+
const [index, setIndex] = useState6(0);
|
|
5316
|
+
const latestQuery = useRef7("");
|
|
5317
|
+
const candidates = useMemo5(() => entries.filter((e) => e.id !== primary.id), [entries, primary.id]);
|
|
5318
|
+
const search = (q) => fuzzyFilter(candidates, q, (e) => [e.name, e.username, e.url, typeLabel(e.type)]);
|
|
5319
|
+
const results = useMemo5(() => search(query), [candidates, query]);
|
|
5320
|
+
const current = Math.min(index, Math.max(0, results.length - 1));
|
|
5321
|
+
useKeyboard6((key) => {
|
|
5322
|
+
if (key.name === "escape") {
|
|
5323
|
+
key.preventDefault();
|
|
5324
|
+
onClose();
|
|
5325
|
+
return;
|
|
5326
|
+
}
|
|
5327
|
+
if (key.name === "up") {
|
|
5328
|
+
key.preventDefault();
|
|
5329
|
+
setIndex(Math.max(0, current - 1));
|
|
5330
|
+
return;
|
|
5331
|
+
}
|
|
5332
|
+
if (key.name === "down") {
|
|
5333
|
+
key.preventDefault();
|
|
5334
|
+
setIndex(Math.min(results.length - 1, current + 1));
|
|
5335
|
+
return;
|
|
5336
|
+
}
|
|
5337
|
+
if (key.name === "pageup") {
|
|
5338
|
+
key.preventDefault();
|
|
5339
|
+
setIndex(Math.max(0, current - 10));
|
|
5340
|
+
return;
|
|
5341
|
+
}
|
|
5342
|
+
if (key.name === "pagedown") {
|
|
5343
|
+
key.preventDefault();
|
|
5344
|
+
setIndex(Math.min(results.length - 1, current + 10));
|
|
5345
|
+
return;
|
|
5346
|
+
}
|
|
5347
|
+
if (isEnter(key)) {
|
|
5348
|
+
key.preventDefault();
|
|
5349
|
+
const fresh = latestQuery.current === query ? results : search(latestQuery.current);
|
|
5350
|
+
const entry = fresh[latestQuery.current === query ? current : 0];
|
|
5351
|
+
if (entry)
|
|
5352
|
+
onToggle(entry);
|
|
5353
|
+
}
|
|
5354
|
+
});
|
|
5355
|
+
const boxWidth = Math.min(72, Math.max(30, width - 4));
|
|
5356
|
+
const rows = Math.max(3, Math.min(14, height - 8));
|
|
5357
|
+
const offset = Math.max(0, Math.min(current - Math.floor(rows / 2), results.length - rows));
|
|
5358
|
+
const visible = results.slice(offset, offset + rows);
|
|
5359
|
+
const inner = boxWidth - 4;
|
|
5360
|
+
return /* @__PURE__ */ jsx6("box", {
|
|
5361
|
+
position: "absolute",
|
|
5362
|
+
top: 2,
|
|
5363
|
+
left: Math.max(0, Math.floor((width - boxWidth) / 2)),
|
|
5364
|
+
width: boxWidth,
|
|
5365
|
+
zIndex: 100,
|
|
5366
|
+
children: /* @__PURE__ */ jsxs6("box", {
|
|
5367
|
+
flexDirection: "column",
|
|
5368
|
+
border: true,
|
|
5369
|
+
borderStyle: "rounded",
|
|
5370
|
+
borderColor: theme.borderFocus,
|
|
5371
|
+
backgroundColor: theme.panel,
|
|
5372
|
+
paddingX: 1,
|
|
5373
|
+
title: ` Link to ${truncate(primary.name, 40)} `,
|
|
5374
|
+
titleColor: theme.accent,
|
|
5375
|
+
titleAlignment: "center",
|
|
5376
|
+
children: [
|
|
5377
|
+
/* @__PURE__ */ jsxs6("box", {
|
|
5378
|
+
flexDirection: "row",
|
|
5379
|
+
height: 1,
|
|
5380
|
+
gap: 1,
|
|
5381
|
+
children: [
|
|
5382
|
+
/* @__PURE__ */ jsx6("text", {
|
|
5383
|
+
fg: theme.accent,
|
|
5384
|
+
children: /* @__PURE__ */ jsx6("strong", {
|
|
5385
|
+
children: "\u203A"
|
|
5386
|
+
})
|
|
5387
|
+
}),
|
|
5388
|
+
/* @__PURE__ */ jsx6(TextField, {
|
|
5389
|
+
value: query,
|
|
5390
|
+
onChange: (v) => {
|
|
5391
|
+
latestQuery.current = v;
|
|
5392
|
+
setQuery(v);
|
|
5393
|
+
setIndex(0);
|
|
5394
|
+
},
|
|
5395
|
+
focused: true,
|
|
5396
|
+
placeholder: "search secrets to link"
|
|
5397
|
+
})
|
|
5398
|
+
]
|
|
5399
|
+
}),
|
|
5400
|
+
/* @__PURE__ */ jsxs6("box", {
|
|
5401
|
+
flexDirection: "column",
|
|
5402
|
+
marginTop: 1,
|
|
5403
|
+
children: [
|
|
5404
|
+
visible.length === 0 ? /* @__PURE__ */ jsx6("text", {
|
|
5405
|
+
fg: theme.dim,
|
|
5406
|
+
children: "No matching secrets"
|
|
5407
|
+
}) : null,
|
|
5408
|
+
visible.map((entry, i) => {
|
|
5409
|
+
const on = offset + i === current;
|
|
5410
|
+
const linked = linkedIds.has(entry.id);
|
|
5411
|
+
const badge = typeBadge(entry.type);
|
|
5412
|
+
const name = truncate(entry.name, Math.max(4, inner - 6));
|
|
5413
|
+
return /* @__PURE__ */ jsx6("box", {
|
|
5414
|
+
flexDirection: "row",
|
|
5415
|
+
height: 1,
|
|
5416
|
+
backgroundColor: on ? theme.selection : undefined,
|
|
5417
|
+
onMouseDown: (event) => {
|
|
5418
|
+
event.preventDefault();
|
|
5419
|
+
onToggle(entry);
|
|
5420
|
+
},
|
|
5421
|
+
onMouseOver: () => setIndex(offset + i),
|
|
5422
|
+
children: /* @__PURE__ */ jsxs6("text", {
|
|
5423
|
+
children: [
|
|
5424
|
+
/* @__PURE__ */ jsx6("span", {
|
|
5425
|
+
fg: linked ? theme.success : theme.dim,
|
|
5426
|
+
children: linked ? " \u2713 " : " "
|
|
5427
|
+
}),
|
|
5428
|
+
/* @__PURE__ */ jsxs6("span", {
|
|
5429
|
+
fg: badge.color,
|
|
5430
|
+
children: [
|
|
5431
|
+
badge.glyph,
|
|
5432
|
+
" "
|
|
5433
|
+
]
|
|
5434
|
+
}),
|
|
5435
|
+
on ? /* @__PURE__ */ jsx6("strong", {
|
|
5436
|
+
fg: theme.accent,
|
|
5437
|
+
children: name
|
|
5438
|
+
}) : /* @__PURE__ */ jsx6("span", {
|
|
5439
|
+
fg: theme.text,
|
|
5440
|
+
children: name
|
|
5441
|
+
})
|
|
5442
|
+
]
|
|
5443
|
+
})
|
|
5444
|
+
}, entry.id);
|
|
5445
|
+
})
|
|
5446
|
+
]
|
|
5447
|
+
}),
|
|
5448
|
+
/* @__PURE__ */ jsxs6("text", {
|
|
5449
|
+
marginTop: 1,
|
|
5450
|
+
children: [
|
|
5451
|
+
/* @__PURE__ */ jsx6(Chip, {
|
|
5452
|
+
label: "\u2191\u2193"
|
|
5453
|
+
}),
|
|
5454
|
+
/* @__PURE__ */ jsx6("span", {
|
|
5455
|
+
fg: theme.muted,
|
|
5456
|
+
children: " move "
|
|
5457
|
+
}),
|
|
5458
|
+
/* @__PURE__ */ jsx6(Chip, {
|
|
5459
|
+
label: "enter"
|
|
5460
|
+
}),
|
|
5461
|
+
/* @__PURE__ */ jsx6("span", {
|
|
5462
|
+
fg: theme.muted,
|
|
5463
|
+
children: " link/unlink "
|
|
5464
|
+
}),
|
|
5465
|
+
/* @__PURE__ */ jsx6(Chip, {
|
|
5466
|
+
label: "esc"
|
|
5467
|
+
}),
|
|
5468
|
+
/* @__PURE__ */ jsx6("span", {
|
|
5469
|
+
fg: theme.muted,
|
|
5470
|
+
children: " done"
|
|
5471
|
+
})
|
|
5472
|
+
]
|
|
5473
|
+
})
|
|
5474
|
+
]
|
|
5475
|
+
})
|
|
5476
|
+
});
|
|
5477
|
+
}
|
|
5478
|
+
var init_links = __esm(() => {
|
|
5479
|
+
init_components();
|
|
5480
|
+
init_theme();
|
|
5481
|
+
init_util();
|
|
5482
|
+
});
|
|
5483
|
+
|
|
5484
|
+
// apps/cli/src/tui/vault.tsx
|
|
5485
|
+
import { useKeyboard as useKeyboard7, useTerminalDimensions as useTerminalDimensions5 } from "@opentui/react";
|
|
5486
|
+
import { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo6, useState as useState7 } from "react";
|
|
5487
|
+
import { jsx as jsx7, jsxs as jsxs7, Fragment as Fragment2 } from "@opentui/react/jsx-runtime";
|
|
5312
5488
|
function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings, setHints, revealMs }) {
|
|
5313
5489
|
const app = useApp();
|
|
5314
|
-
const { width, height } =
|
|
5490
|
+
const { width, height } = useTerminalDimensions5();
|
|
5315
5491
|
const wide = width >= WIDE_MIN_COLUMNS;
|
|
5316
|
-
const [entries, setEntries] =
|
|
5317
|
-
const [query, setQuery] =
|
|
5318
|
-
const [pane, setPane] =
|
|
5319
|
-
const [actionIndex, setActionIndex] =
|
|
5492
|
+
const [entries, setEntries] = useState7([]);
|
|
5493
|
+
const [query, setQuery] = useState7("");
|
|
5494
|
+
const [pane, setPane] = useState7("list");
|
|
5495
|
+
const [actionIndex, setActionIndex] = useState7(0);
|
|
5320
5496
|
const searchFocused = pane === "search";
|
|
5321
|
-
const [selectedId, setSelectedId] =
|
|
5322
|
-
const [detailOpen, setDetailOpen] =
|
|
5323
|
-
const [revealed, setRevealed] =
|
|
5324
|
-
const [otp, setOtp] =
|
|
5325
|
-
const [confirmDelete, setConfirmDelete] =
|
|
5497
|
+
const [selectedId, setSelectedId] = useState7(initialSelectedId ?? null);
|
|
5498
|
+
const [detailOpen, setDetailOpen] = useState7(false);
|
|
5499
|
+
const [revealed, setRevealed] = useState7(null);
|
|
5500
|
+
const [otp, setOtp] = useState7(null);
|
|
5501
|
+
const [confirmDelete, setConfirmDelete] = useState7(false);
|
|
5502
|
+
const [links, setLinks] = useState7(null);
|
|
5503
|
+
const [linkPickerOpen, setLinkPickerOpen] = useState7(false);
|
|
5504
|
+
const [linkedOtp, setLinkedOtp] = useState7(null);
|
|
5326
5505
|
const reload = useCallback3(() => {
|
|
5327
5506
|
try {
|
|
5328
5507
|
setEntries(session.listSecretIndex());
|
|
@@ -5332,7 +5511,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5332
5511
|
}
|
|
5333
5512
|
}, [session, app]);
|
|
5334
5513
|
useEffect6(reload, [reload]);
|
|
5335
|
-
const filtered =
|
|
5514
|
+
const filtered = useMemo6(() => fuzzyFilter(entries, query, (e) => [e.name, e.username, e.url, e.description, typeLabel(e.type)]), [entries, query]);
|
|
5336
5515
|
const selectedIndex = Math.max(0, filtered.findIndex((e) => e.id === selectedId));
|
|
5337
5516
|
const selected = filtered[selectedIndex] ?? null;
|
|
5338
5517
|
useEffect6(() => {
|
|
@@ -5343,10 +5522,25 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5343
5522
|
useEffect6(() => {
|
|
5344
5523
|
setRevealed((r) => r && r.id !== selected?.id ? null : r);
|
|
5345
5524
|
setOtp((o) => o && o.id !== selected?.id ? null : o);
|
|
5525
|
+
setLinkedOtp(null);
|
|
5346
5526
|
setConfirmDelete(false);
|
|
5347
5527
|
setActionIndex(0);
|
|
5348
5528
|
}, [selected?.id]);
|
|
5349
|
-
const
|
|
5529
|
+
const loadLinks = useCallback3((id) => {
|
|
5530
|
+
if (!id) {
|
|
5531
|
+
setLinks(null);
|
|
5532
|
+
return;
|
|
5533
|
+
}
|
|
5534
|
+
try {
|
|
5535
|
+
setLinks(session.getLinks(id));
|
|
5536
|
+
} catch (e) {
|
|
5537
|
+
app.toast(e instanceof Error ? e.message : String(e), "error");
|
|
5538
|
+
setLinks(null);
|
|
5539
|
+
}
|
|
5540
|
+
}, [session, app]);
|
|
5541
|
+
useEffect6(() => loadLinks(selected?.id), [loadLinks, selected?.id, selected?.linkCount]);
|
|
5542
|
+
const linkedOtpSecret = links?.links.find((l) => l.type === "otp") ?? null;
|
|
5543
|
+
const now = useNow(1000, Boolean(revealed || otp || linkedOtp));
|
|
5350
5544
|
useEffect6(() => {
|
|
5351
5545
|
if (revealed && now >= revealed.until) {
|
|
5352
5546
|
setRevealed(null);
|
|
@@ -5360,22 +5554,56 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5360
5554
|
return null;
|
|
5361
5555
|
}
|
|
5362
5556
|
}, [session, app]);
|
|
5363
|
-
const
|
|
5557
|
+
const makeOtp = useCallback3((id, options) => {
|
|
5364
5558
|
try {
|
|
5365
|
-
const result = session.generateOtp(
|
|
5366
|
-
|
|
5367
|
-
setOtp(next);
|
|
5368
|
-
return next;
|
|
5559
|
+
const result = session.generateOtp(id, options);
|
|
5560
|
+
return { id, code: result.code, expiresAt: Date.parse(result.expiresAt) };
|
|
5369
5561
|
} catch (e) {
|
|
5370
5562
|
app.toast(e instanceof Error ? e.message : String(e), "error");
|
|
5371
5563
|
return null;
|
|
5372
5564
|
}
|
|
5373
5565
|
}, [session, app]);
|
|
5566
|
+
const generateOtp = useCallback3((entry, options) => {
|
|
5567
|
+
const next = makeOtp(entry.id, options);
|
|
5568
|
+
if (next)
|
|
5569
|
+
setOtp(next);
|
|
5570
|
+
return next;
|
|
5571
|
+
}, [makeOtp]);
|
|
5374
5572
|
useEffect6(() => {
|
|
5375
5573
|
if (otp && selected && otp.id === selected.id && now >= otp.expiresAt) {
|
|
5376
5574
|
generateOtp(selected, { audit: false });
|
|
5377
5575
|
}
|
|
5378
5576
|
}, [now, otp, selected, generateOtp]);
|
|
5577
|
+
useEffect6(() => {
|
|
5578
|
+
if (linkedOtp && linkedOtp.id === linkedOtpSecret?.id && now >= linkedOtp.expiresAt) {
|
|
5579
|
+
setLinkedOtp(makeOtp(linkedOtp.id, { audit: false }));
|
|
5580
|
+
}
|
|
5581
|
+
}, [now, linkedOtp, linkedOtpSecret?.id, makeOtp]);
|
|
5582
|
+
const copyLinkedOtp = useCallback3(() => {
|
|
5583
|
+
if (!linkedOtpSecret) {
|
|
5584
|
+
app.toast("No OTP linked to this secret", "info");
|
|
5585
|
+
return;
|
|
5586
|
+
}
|
|
5587
|
+
const code = makeOtp(linkedOtpSecret.id);
|
|
5588
|
+
if (!code)
|
|
5589
|
+
return;
|
|
5590
|
+
setLinkedOtp(code);
|
|
5591
|
+
app.copy("OTP code", code.code, { secret: true });
|
|
5592
|
+
}, [app, linkedOtpSecret, makeOtp]);
|
|
5593
|
+
const toggleLink = useCallback3((primary, target) => {
|
|
5594
|
+
const linked = links?.links.some((l) => l.id === target.id) ?? false;
|
|
5595
|
+
try {
|
|
5596
|
+
if (linked)
|
|
5597
|
+
session.unlinkSecrets(primary.id, target.id);
|
|
5598
|
+
else
|
|
5599
|
+
session.linkSecrets(primary.id, target.id);
|
|
5600
|
+
app.toast(linked ? `Unlinked ${target.name}` : `Linked ${target.name}`, "info");
|
|
5601
|
+
} catch (e) {
|
|
5602
|
+
app.toast(e instanceof Error ? e.message : String(e), "error");
|
|
5603
|
+
}
|
|
5604
|
+
reload();
|
|
5605
|
+
loadLinks(primary.id);
|
|
5606
|
+
}, [links, session, app, reload, loadLinks]);
|
|
5379
5607
|
const copyPrimary = useCallback3((entry) => {
|
|
5380
5608
|
if (!entry)
|
|
5381
5609
|
return;
|
|
@@ -5418,10 +5646,12 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5418
5646
|
setRevealed({ id: entry.id, value, until: Date.now() + revealMs });
|
|
5419
5647
|
}
|
|
5420
5648
|
}, [revealed, readValue, revealMs]);
|
|
5421
|
-
const remove = useCallback3((entry) => {
|
|
5649
|
+
const remove = useCallback3((entry, withLinked = false) => {
|
|
5422
5650
|
try {
|
|
5423
|
-
session.removeSecret(entry.id);
|
|
5424
|
-
|
|
5651
|
+
const { removedLinked, keptLinked } = session.removeSecret(entry.id, { withLinked });
|
|
5652
|
+
const also = removedLinked.length ? ` and ${removedLinked.map((l) => l.name).join(", ")}` : "";
|
|
5653
|
+
const kept = keptLinked.length ? ` \xB7 kept ${keptLinked.map((l) => l.name).join(", ")} (linked elsewhere)` : "";
|
|
5654
|
+
app.toast(`Deleted "${entry.name}"${also}${kept}`);
|
|
5425
5655
|
setConfirmDelete(false);
|
|
5426
5656
|
setDetailOpen(false);
|
|
5427
5657
|
setPane("list");
|
|
@@ -5451,6 +5681,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5451
5681
|
...selected.type !== "otp" ? [{ id: "reveal", hotkey: "s", label: revealed?.id === selected.id ? "hide" : "reveal", color: theme.revealed, run: () => toggleReveal(selected) }] : [],
|
|
5452
5682
|
...selected.url && selected.type !== "otp" ? [{ id: "open", hotkey: "o", label: "open", color: theme.info, run: () => open(selected) }] : [],
|
|
5453
5683
|
{ id: "edit", hotkey: "e", label: "edit", color: theme.accent, run: () => onEdit(selected.id) },
|
|
5684
|
+
{ id: "link", hotkey: "L", label: "link", color: theme.info, run: () => setLinkPickerOpen(true) },
|
|
5454
5685
|
{ id: "delete", hotkey: "d", label: "delete", color: theme.danger, run: () => setConfirmDelete(true) }
|
|
5455
5686
|
] : [];
|
|
5456
5687
|
const currentAction = Math.min(actionIndex, Math.max(0, actions.length - 1));
|
|
@@ -5472,13 +5703,15 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5472
5703
|
while (PANES[index] === "actions" && !selected);
|
|
5473
5704
|
focusPane(PANES[index]);
|
|
5474
5705
|
};
|
|
5475
|
-
|
|
5476
|
-
if (!app.active || key.ctrl || key.meta)
|
|
5706
|
+
useKeyboard7((key) => {
|
|
5707
|
+
if (!app.active || key.ctrl || key.meta || linkPickerOpen)
|
|
5477
5708
|
return;
|
|
5478
5709
|
if (confirmDelete) {
|
|
5479
5710
|
key.preventDefault();
|
|
5480
5711
|
if ((key.name === "y" || isEnter(key)) && selected)
|
|
5481
5712
|
remove(selected);
|
|
5713
|
+
else if (key.name === "a" && selected && links?.links.length)
|
|
5714
|
+
remove(selected, true);
|
|
5482
5715
|
else if (key.name === "n" || key.name === "escape")
|
|
5483
5716
|
setConfirmDelete(false);
|
|
5484
5717
|
return;
|
|
@@ -5498,6 +5731,10 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5498
5731
|
}
|
|
5499
5732
|
return;
|
|
5500
5733
|
}
|
|
5734
|
+
if (key.name === "l" && key.shift && selected) {
|
|
5735
|
+
setLinkPickerOpen(true);
|
|
5736
|
+
return;
|
|
5737
|
+
}
|
|
5501
5738
|
switch (key.name) {
|
|
5502
5739
|
case "up":
|
|
5503
5740
|
case "k":
|
|
@@ -5611,13 +5848,16 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5611
5848
|
case "e":
|
|
5612
5849
|
onEdit(selected.id);
|
|
5613
5850
|
return;
|
|
5851
|
+
case "t":
|
|
5852
|
+
copyLinkedOtp();
|
|
5853
|
+
return;
|
|
5614
5854
|
case "d":
|
|
5615
5855
|
case "delete":
|
|
5616
5856
|
setConfirmDelete(true);
|
|
5617
5857
|
return;
|
|
5618
5858
|
}
|
|
5619
5859
|
});
|
|
5620
|
-
const commands =
|
|
5860
|
+
const commands = useMemo6(() => {
|
|
5621
5861
|
const list = [
|
|
5622
5862
|
{ id: "new", title: "New secret", hint: "n", group: "Vault", run: onCreate },
|
|
5623
5863
|
{ id: "search", title: "Search secrets", hint: "/", group: "Vault", run: () => setPane("search") },
|
|
@@ -5625,11 +5865,26 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5625
5865
|
];
|
|
5626
5866
|
if (selected) {
|
|
5627
5867
|
const name = selected.name;
|
|
5628
|
-
list.unshift({ id: "copy", title: `Copy ${selected.type === "otp" ? "OTP code" : selected.type === "note" ? "note" : "password"} \u2014 ${name}`, hint: "c / enter", group: "Selected", run: () => copyPrimary(selected) }, { id: "copy-user", title: `Copy username \u2014 ${name}`, hint: "u", group: "Selected", run: () => copyUsername(selected) }, { id: "reveal", title: `${revealed ? "Hide" : "Reveal"} value \u2014 ${name}`, hint: "s", group: "Selected", run: () => toggleReveal(selected) }, { id: "edit", title: `Edit \u2014 ${name}`, hint: "e", group: "Selected", run: () => onEdit(selected.id) }, { id: "delete", title: `Delete \u2014 ${name}`, hint: "d", group: "Selected", run: () => setConfirmDelete(true) });
|
|
5868
|
+
list.unshift({ id: "copy", title: `Copy ${selected.type === "otp" ? "OTP code" : selected.type === "note" ? "note" : "password"} \u2014 ${name}`, hint: "c / enter", group: "Selected", run: () => copyPrimary(selected) }, { id: "copy-user", title: `Copy username \u2014 ${name}`, hint: "u", group: "Selected", run: () => copyUsername(selected) }, { id: "reveal", title: `${revealed ? "Hide" : "Reveal"} value \u2014 ${name}`, hint: "s", group: "Selected", run: () => toggleReveal(selected) }, { id: "edit", title: `Edit \u2014 ${name}`, hint: "e", group: "Selected", run: () => onEdit(selected.id) }, { id: "link", title: `Link secrets \u2014 ${name}`, hint: "L", group: "Selected", run: () => setLinkPickerOpen(true) }, { id: "delete", title: `Delete \u2014 ${name}`, hint: "d", group: "Selected", run: () => setConfirmDelete(true) });
|
|
5869
|
+
if (linkedOtpSecret) {
|
|
5870
|
+
list.splice(1, 0, { id: "copy-linked-otp", title: `Copy ${linkedOtpSecret.name} code \u2014 ${name}`, hint: "t", group: "Selected", run: copyLinkedOtp });
|
|
5871
|
+
}
|
|
5629
5872
|
if (selected.url) {
|
|
5630
5873
|
list.splice(2, 0, { id: "copy-url", title: `Copy URL \u2014 ${name}`, hint: "w", group: "Selected", run: () => copyUrl(selected) }, { id: "open-url", title: `Open ${hostOf(selected.url)} in browser`, hint: selected.type === "otp" ? undefined : "o", group: "Selected", run: () => open(selected) });
|
|
5631
5874
|
}
|
|
5632
5875
|
}
|
|
5876
|
+
for (const link of [...links?.links ?? [], ...links?.linkedFrom ?? []]) {
|
|
5877
|
+
list.push({
|
|
5878
|
+
id: `goto-linked:${link.id}`,
|
|
5879
|
+
title: `Go to ${link.name}`,
|
|
5880
|
+
hint: typeLabel(link.type),
|
|
5881
|
+
group: "Linked",
|
|
5882
|
+
run: () => {
|
|
5883
|
+
setQuery("");
|
|
5884
|
+
setSelectedId(link.id);
|
|
5885
|
+
}
|
|
5886
|
+
});
|
|
5887
|
+
}
|
|
5633
5888
|
for (const entry of entries) {
|
|
5634
5889
|
list.push({
|
|
5635
5890
|
id: `goto:${entry.id}`,
|
|
@@ -5645,7 +5900,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5645
5900
|
});
|
|
5646
5901
|
}
|
|
5647
5902
|
return list;
|
|
5648
|
-
}, [entries, selected, revealed, onCreate, onEdit, reload, copyPrimary, copyUsername, copyUrl, toggleReveal]);
|
|
5903
|
+
}, [entries, selected, revealed, links, linkedOtpSecret, onCreate, onEdit, reload, copyPrimary, copyUsername, copyUrl, toggleReveal, copyLinkedOtp]);
|
|
5649
5904
|
useScreenContext(commands, selected ? {
|
|
5650
5905
|
label: selected.type === "otp" ? "OTP code" : selected.type === "note" ? "Note" : "Password",
|
|
5651
5906
|
read: () => selected.type === "otp" ? generateOtp(selected)?.code ?? null : readValue(selected),
|
|
@@ -5654,7 +5909,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5654
5909
|
useEffect6(() => {
|
|
5655
5910
|
let hints;
|
|
5656
5911
|
if (confirmDelete)
|
|
5657
|
-
hints = [["y", "delete"], ["n/esc", "cancel"]];
|
|
5912
|
+
hints = links?.links.length ? [["y", "delete"], ["a", "with linked"], ["n/esc", "cancel"]] : [["y", "delete"], ["n/esc", "cancel"]];
|
|
5658
5913
|
else if (searchFocused)
|
|
5659
5914
|
hints = [["type", "filter"], ["enter/\u2193", "results"], ["esc", "clear"]];
|
|
5660
5915
|
else if (pane === "actions")
|
|
@@ -5664,11 +5919,11 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5664
5919
|
else
|
|
5665
5920
|
hints = [["\u2191\u2193", "move"], ["enter", "copy"], ["\u2192/tab", "actions"], ["/", "search"], ["n", "new"], ["q", "quit"]];
|
|
5666
5921
|
setHints(hints);
|
|
5667
|
-
}, [confirmDelete, searchFocused, pane, showDetail, setHints]);
|
|
5922
|
+
}, [confirmDelete, links, searchFocused, pane, showDetail, setHints]);
|
|
5668
5923
|
const listWidth = wide ? Math.max(32, Math.floor(width * 0.38)) : width;
|
|
5669
5924
|
const offset = Math.max(0, Math.min(selectedIndex - Math.floor(listHeight / 2), filtered.length - listHeight));
|
|
5670
5925
|
const visible = filtered.slice(offset, offset + listHeight);
|
|
5671
|
-
const list = /* @__PURE__ */
|
|
5926
|
+
const list = /* @__PURE__ */ jsx7("box", {
|
|
5672
5927
|
flexDirection: "column",
|
|
5673
5928
|
width: wide ? listWidth : "100%",
|
|
5674
5929
|
flexShrink: 0,
|
|
@@ -5678,9 +5933,9 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5678
5933
|
title: ` ${filtered.length}/${entries.length} secrets `,
|
|
5679
5934
|
titleColor: theme.muted,
|
|
5680
5935
|
onMouseScroll: (event) => move(event.scroll?.direction === "up" ? -3 : 3),
|
|
5681
|
-
children: filtered.length === 0 ? /* @__PURE__ */
|
|
5936
|
+
children: filtered.length === 0 ? /* @__PURE__ */ jsx7("box", {
|
|
5682
5937
|
padding: 1,
|
|
5683
|
-
children: /* @__PURE__ */
|
|
5938
|
+
children: /* @__PURE__ */ jsx7("text", {
|
|
5684
5939
|
fg: theme.dim,
|
|
5685
5940
|
children: entries.length === 0 ? "No secrets yet \u2014 press n to add one." : `No match for "${query}".`
|
|
5686
5941
|
})
|
|
@@ -5690,7 +5945,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5690
5945
|
const sub = entry.username ?? (entry.url ? hostOf(entry.url) : "");
|
|
5691
5946
|
const room = listWidth - 6;
|
|
5692
5947
|
const name = truncate(entry.name, Math.max(8, Math.min(room, Math.ceil(room * 0.55))));
|
|
5693
|
-
return /* @__PURE__ */
|
|
5948
|
+
return /* @__PURE__ */ jsx7("box", {
|
|
5694
5949
|
flexDirection: "row",
|
|
5695
5950
|
height: 1,
|
|
5696
5951
|
paddingX: 1,
|
|
@@ -5704,66 +5959,75 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5704
5959
|
setPane("actions");
|
|
5705
5960
|
}
|
|
5706
5961
|
},
|
|
5707
|
-
children: /* @__PURE__ */
|
|
5962
|
+
children: /* @__PURE__ */ jsxs7("text", {
|
|
5708
5963
|
children: [
|
|
5709
|
-
/* @__PURE__ */
|
|
5964
|
+
/* @__PURE__ */ jsxs7("span", {
|
|
5710
5965
|
fg: badge.color,
|
|
5711
5966
|
children: [
|
|
5712
5967
|
badge.glyph,
|
|
5713
5968
|
" "
|
|
5714
5969
|
]
|
|
5715
5970
|
}),
|
|
5716
|
-
isSelected ? /* @__PURE__ */
|
|
5971
|
+
isSelected ? /* @__PURE__ */ jsx7("strong", {
|
|
5717
5972
|
fg: theme.accent,
|
|
5718
5973
|
children: name
|
|
5719
|
-
}) : /* @__PURE__ */
|
|
5974
|
+
}) : /* @__PURE__ */ jsx7("span", {
|
|
5720
5975
|
fg: theme.text,
|
|
5721
5976
|
children: name
|
|
5722
5977
|
}),
|
|
5723
|
-
/* @__PURE__ */
|
|
5978
|
+
entry.linkCount ? /* @__PURE__ */ jsxs7("span", {
|
|
5979
|
+
fg: theme.muted,
|
|
5980
|
+
children: [
|
|
5981
|
+
" \xB7",
|
|
5982
|
+
entry.linkCount
|
|
5983
|
+
]
|
|
5984
|
+
}) : null,
|
|
5985
|
+
/* @__PURE__ */ jsx7("span", {
|
|
5724
5986
|
fg: theme.dim,
|
|
5725
|
-
children: sub ? ` ${truncate(sub, Math.max(0, room - name.length - 2))}` : ""
|
|
5987
|
+
children: sub ? ` ${truncate(sub, Math.max(0, room - name.length - 2 - (entry.linkCount ? 3 : 0)))}` : ""
|
|
5726
5988
|
})
|
|
5727
5989
|
]
|
|
5728
5990
|
})
|
|
5729
5991
|
}, entry.id);
|
|
5730
5992
|
})
|
|
5731
5993
|
});
|
|
5732
|
-
const detail = selected ? /* @__PURE__ */
|
|
5994
|
+
const detail = selected ? /* @__PURE__ */ jsx7(Detail, {
|
|
5733
5995
|
entry: selected,
|
|
5734
5996
|
revealed: revealed?.id === selected.id ? revealed : null,
|
|
5735
5997
|
otp: otp?.id === selected.id ? otp : null,
|
|
5736
5998
|
now,
|
|
5737
5999
|
confirmDelete,
|
|
6000
|
+
links,
|
|
6001
|
+
linkedOtp,
|
|
5738
6002
|
actions,
|
|
5739
6003
|
focusedAction: pane === "actions" ? currentAction : null
|
|
5740
|
-
}) : /* @__PURE__ */
|
|
6004
|
+
}) : /* @__PURE__ */ jsx7("box", {
|
|
5741
6005
|
flexGrow: 1,
|
|
5742
6006
|
border: true,
|
|
5743
6007
|
borderStyle: "rounded",
|
|
5744
6008
|
borderColor: theme.border,
|
|
5745
6009
|
padding: 1,
|
|
5746
|
-
children: /* @__PURE__ */
|
|
6010
|
+
children: /* @__PURE__ */ jsx7("text", {
|
|
5747
6011
|
fg: theme.dim,
|
|
5748
6012
|
children: "Nothing selected."
|
|
5749
6013
|
})
|
|
5750
6014
|
});
|
|
5751
|
-
return /* @__PURE__ */
|
|
6015
|
+
return /* @__PURE__ */ jsxs7("box", {
|
|
5752
6016
|
flexDirection: "column",
|
|
5753
6017
|
flexGrow: 1,
|
|
5754
6018
|
width: "100%",
|
|
5755
6019
|
children: [
|
|
5756
|
-
/* @__PURE__ */
|
|
6020
|
+
/* @__PURE__ */ jsxs7("box", {
|
|
5757
6021
|
flexDirection: "row",
|
|
5758
6022
|
paddingX: 1,
|
|
5759
6023
|
height: 1,
|
|
5760
6024
|
children: [
|
|
5761
|
-
/* @__PURE__ */
|
|
6025
|
+
/* @__PURE__ */ jsx7("text", {
|
|
5762
6026
|
bg: searchFocused ? theme.accent : theme.chip,
|
|
5763
6027
|
fg: searchFocused ? theme.onAccent : theme.muted,
|
|
5764
6028
|
children: " / "
|
|
5765
6029
|
}),
|
|
5766
|
-
/* @__PURE__ */
|
|
6030
|
+
/* @__PURE__ */ jsx7(TextField, {
|
|
5767
6031
|
value: query,
|
|
5768
6032
|
onChange: setQuery,
|
|
5769
6033
|
focused: searchFocused && app.active,
|
|
@@ -5771,7 +6035,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5771
6035
|
})
|
|
5772
6036
|
]
|
|
5773
6037
|
}),
|
|
5774
|
-
/* @__PURE__ */
|
|
6038
|
+
/* @__PURE__ */ jsxs7("box", {
|
|
5775
6039
|
flexDirection: "row",
|
|
5776
6040
|
flexGrow: 1,
|
|
5777
6041
|
gap: 1,
|
|
@@ -5779,21 +6043,69 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
|
|
|
5779
6043
|
wide || !detailOpen ? list : null,
|
|
5780
6044
|
showDetail ? detail : null
|
|
5781
6045
|
]
|
|
5782
|
-
})
|
|
6046
|
+
}),
|
|
6047
|
+
linkPickerOpen && selected ? /* @__PURE__ */ jsx7(LinkPicker, {
|
|
6048
|
+
primary: selected,
|
|
6049
|
+
entries,
|
|
6050
|
+
linkedIds: new Set(links?.links.map((l) => l.id) ?? []),
|
|
6051
|
+
onToggle: (target) => toggleLink(selected, target),
|
|
6052
|
+
onClose: () => setLinkPickerOpen(false)
|
|
6053
|
+
}) : null
|
|
6054
|
+
]
|
|
6055
|
+
});
|
|
6056
|
+
}
|
|
6057
|
+
function LinkedRow({ link, otp, now }) {
|
|
6058
|
+
const badge = typeBadge(link.type);
|
|
6059
|
+
const seconds = otp ? Math.max(0, Math.ceil((otp.expiresAt - now) / 1000)) : 0;
|
|
6060
|
+
return /* @__PURE__ */ jsxs7("text", {
|
|
6061
|
+
children: [
|
|
6062
|
+
/* @__PURE__ */ jsxs7("span", {
|
|
6063
|
+
fg: badge.color,
|
|
6064
|
+
children: [
|
|
6065
|
+
" ",
|
|
6066
|
+
badge.glyph,
|
|
6067
|
+
" "
|
|
6068
|
+
]
|
|
6069
|
+
}),
|
|
6070
|
+
/* @__PURE__ */ jsx7("span", {
|
|
6071
|
+
fg: theme.text,
|
|
6072
|
+
children: link.name
|
|
6073
|
+
}),
|
|
6074
|
+
link.type === "otp" ? otp ? /* @__PURE__ */ jsxs7(Fragment2, {
|
|
6075
|
+
children: [
|
|
6076
|
+
/* @__PURE__ */ jsx7("span", {
|
|
6077
|
+
fg: theme.success,
|
|
6078
|
+
children: /* @__PURE__ */ jsx7("strong", {
|
|
6079
|
+
children: ` ${otp.code.slice(0, 3)} ${otp.code.slice(3)}`
|
|
6080
|
+
})
|
|
6081
|
+
}),
|
|
6082
|
+
/* @__PURE__ */ jsxs7("span", {
|
|
6083
|
+
fg: seconds <= 5 ? theme.danger : theme.muted,
|
|
6084
|
+
children: [
|
|
6085
|
+
" ",
|
|
6086
|
+
seconds,
|
|
6087
|
+
"s"
|
|
6088
|
+
]
|
|
6089
|
+
})
|
|
6090
|
+
]
|
|
6091
|
+
}) : /* @__PURE__ */ jsx7("span", {
|
|
6092
|
+
fg: theme.dim,
|
|
6093
|
+
children: " press t for the code"
|
|
6094
|
+
}) : null
|
|
5783
6095
|
]
|
|
5784
6096
|
});
|
|
5785
6097
|
}
|
|
5786
6098
|
function valueName(type) {
|
|
5787
6099
|
return type === "note" ? "Note" : type === "otp" ? "Secret key" : "Password";
|
|
5788
6100
|
}
|
|
5789
|
-
function Detail({ entry, revealed, otp, now, confirmDelete, actions, focusedAction }) {
|
|
6101
|
+
function Detail({ entry, revealed, otp, now, confirmDelete, links, linkedOtp, actions, focusedAction }) {
|
|
5790
6102
|
const badge = typeBadge(entry.type);
|
|
5791
6103
|
const valueLabel = valueName(entry.type);
|
|
5792
6104
|
const revealSeconds = revealed ? Math.max(0, Math.ceil((revealed.until - now) / 1000)) : 0;
|
|
5793
6105
|
const otpSeconds = otp ? Math.max(0, Math.ceil((otp.expiresAt - now) / 1000)) : 0;
|
|
5794
6106
|
const otpBar = 10;
|
|
5795
6107
|
const otpFilled = Math.round(otpSeconds / 30 * otpBar);
|
|
5796
|
-
return /* @__PURE__ */
|
|
6108
|
+
return /* @__PURE__ */ jsxs7("box", {
|
|
5797
6109
|
flexDirection: "column",
|
|
5798
6110
|
flexGrow: 1,
|
|
5799
6111
|
border: true,
|
|
@@ -5804,51 +6116,51 @@ function Detail({ entry, revealed, otp, now, confirmDelete, actions, focusedActi
|
|
|
5804
6116
|
title: ` ${typeLabel(entry.type)} `,
|
|
5805
6117
|
titleColor: badge.color,
|
|
5806
6118
|
children: [
|
|
5807
|
-
/* @__PURE__ */
|
|
6119
|
+
/* @__PURE__ */ jsxs7("text", {
|
|
5808
6120
|
children: [
|
|
5809
|
-
/* @__PURE__ */
|
|
6121
|
+
/* @__PURE__ */ jsxs7("span", {
|
|
5810
6122
|
fg: badge.color,
|
|
5811
6123
|
children: [
|
|
5812
6124
|
badge.glyph,
|
|
5813
6125
|
" "
|
|
5814
6126
|
]
|
|
5815
6127
|
}),
|
|
5816
|
-
/* @__PURE__ */
|
|
6128
|
+
/* @__PURE__ */ jsx7("strong", {
|
|
5817
6129
|
fg: theme.accent,
|
|
5818
6130
|
children: entry.name
|
|
5819
6131
|
})
|
|
5820
6132
|
]
|
|
5821
6133
|
}),
|
|
5822
|
-
/* @__PURE__ */
|
|
6134
|
+
/* @__PURE__ */ jsxs7("box", {
|
|
5823
6135
|
flexDirection: "column",
|
|
5824
6136
|
children: [
|
|
5825
|
-
entry.type !== "note" ? /* @__PURE__ */
|
|
6137
|
+
entry.type !== "note" ? /* @__PURE__ */ jsx7(FieldRow, {
|
|
5826
6138
|
label: "Username",
|
|
5827
6139
|
value: entry.username ?? "\u2014",
|
|
5828
6140
|
color: entry.username ? theme.text : theme.dim
|
|
5829
6141
|
}) : null,
|
|
5830
|
-
/* @__PURE__ */
|
|
6142
|
+
/* @__PURE__ */ jsx7(FieldRow, {
|
|
5831
6143
|
label: valueLabel,
|
|
5832
6144
|
value: revealed ? revealed.value : MASK,
|
|
5833
6145
|
color: revealed ? theme.revealed : theme.dim
|
|
5834
6146
|
}),
|
|
5835
|
-
entry.url ? /* @__PURE__ */
|
|
6147
|
+
entry.url ? /* @__PURE__ */ jsx7(FieldRow, {
|
|
5836
6148
|
label: "URL",
|
|
5837
6149
|
value: entry.url,
|
|
5838
6150
|
color: theme.info
|
|
5839
6151
|
}) : null,
|
|
5840
|
-
entry.description ? /* @__PURE__ */
|
|
6152
|
+
entry.description ? /* @__PURE__ */ jsx7(FieldRow, {
|
|
5841
6153
|
label: "Notes",
|
|
5842
6154
|
value: entry.description
|
|
5843
6155
|
}) : null,
|
|
5844
|
-
/* @__PURE__ */
|
|
6156
|
+
/* @__PURE__ */ jsx7(FieldRow, {
|
|
5845
6157
|
label: "Updated",
|
|
5846
6158
|
value: entry.updatedAt.slice(0, 16).replace("T", " "),
|
|
5847
6159
|
color: theme.dim
|
|
5848
6160
|
})
|
|
5849
6161
|
]
|
|
5850
6162
|
}),
|
|
5851
|
-
entry.type === "otp" ? /* @__PURE__ */
|
|
6163
|
+
entry.type === "otp" ? /* @__PURE__ */ jsx7("box", {
|
|
5852
6164
|
flexDirection: "row",
|
|
5853
6165
|
border: true,
|
|
5854
6166
|
borderStyle: "rounded",
|
|
@@ -5856,26 +6168,26 @@ function Detail({ entry, revealed, otp, now, confirmDelete, actions, focusedActi
|
|
|
5856
6168
|
paddingX: 1,
|
|
5857
6169
|
gap: 2,
|
|
5858
6170
|
alignSelf: "flex-start",
|
|
5859
|
-
children: otp ? /* @__PURE__ */
|
|
6171
|
+
children: otp ? /* @__PURE__ */ jsxs7(Fragment2, {
|
|
5860
6172
|
children: [
|
|
5861
|
-
/* @__PURE__ */
|
|
6173
|
+
/* @__PURE__ */ jsx7("text", {
|
|
5862
6174
|
fg: theme.success,
|
|
5863
6175
|
selectable: true,
|
|
5864
|
-
children: /* @__PURE__ */
|
|
6176
|
+
children: /* @__PURE__ */ jsx7("strong", {
|
|
5865
6177
|
children: `${otp.code.slice(0, 3)} ${otp.code.slice(3)}`
|
|
5866
6178
|
})
|
|
5867
6179
|
}),
|
|
5868
|
-
/* @__PURE__ */
|
|
6180
|
+
/* @__PURE__ */ jsxs7("text", {
|
|
5869
6181
|
children: [
|
|
5870
|
-
/* @__PURE__ */
|
|
6182
|
+
/* @__PURE__ */ jsx7("span", {
|
|
5871
6183
|
fg: otpSeconds <= 5 ? theme.danger : theme.success,
|
|
5872
6184
|
children: "\u2588".repeat(otpFilled)
|
|
5873
6185
|
}),
|
|
5874
|
-
/* @__PURE__ */
|
|
6186
|
+
/* @__PURE__ */ jsx7("span", {
|
|
5875
6187
|
fg: theme.chip,
|
|
5876
6188
|
children: "\u2588".repeat(otpBar - otpFilled)
|
|
5877
6189
|
}),
|
|
5878
|
-
/* @__PURE__ */
|
|
6190
|
+
/* @__PURE__ */ jsxs7("span", {
|
|
5879
6191
|
fg: otpSeconds <= 5 ? theme.danger : theme.muted,
|
|
5880
6192
|
children: [
|
|
5881
6193
|
" ",
|
|
@@ -5886,12 +6198,38 @@ function Detail({ entry, revealed, otp, now, confirmDelete, actions, focusedActi
|
|
|
5886
6198
|
]
|
|
5887
6199
|
})
|
|
5888
6200
|
]
|
|
5889
|
-
}) : /* @__PURE__ */
|
|
6201
|
+
}) : /* @__PURE__ */ jsx7("text", {
|
|
5890
6202
|
fg: theme.dim,
|
|
5891
6203
|
children: "press o to show the code"
|
|
5892
6204
|
})
|
|
5893
6205
|
}) : null,
|
|
5894
|
-
|
|
6206
|
+
links && (links.links.length > 0 || links.linkedFrom.length > 0) ? /* @__PURE__ */ jsxs7("box", {
|
|
6207
|
+
flexDirection: "column",
|
|
6208
|
+
children: [
|
|
6209
|
+
links.links.length > 0 ? /* @__PURE__ */ jsx7("text", {
|
|
6210
|
+
fg: theme.muted,
|
|
6211
|
+
children: "Linked"
|
|
6212
|
+
}) : null,
|
|
6213
|
+
links.links.map((link) => /* @__PURE__ */ jsx7(LinkedRow, {
|
|
6214
|
+
link,
|
|
6215
|
+
otp: linkedOtp?.id === link.id ? linkedOtp : null,
|
|
6216
|
+
now
|
|
6217
|
+
}, link.id)),
|
|
6218
|
+
links.linkedFrom.length > 0 ? /* @__PURE__ */ jsxs7("text", {
|
|
6219
|
+
children: [
|
|
6220
|
+
/* @__PURE__ */ jsx7("span", {
|
|
6221
|
+
fg: theme.muted,
|
|
6222
|
+
children: "Linked from "
|
|
6223
|
+
}),
|
|
6224
|
+
/* @__PURE__ */ jsx7("span", {
|
|
6225
|
+
fg: theme.text,
|
|
6226
|
+
children: links.linkedFrom.map((l) => l.name).join(", ")
|
|
6227
|
+
})
|
|
6228
|
+
]
|
|
6229
|
+
}) : null
|
|
6230
|
+
]
|
|
6231
|
+
}) : null,
|
|
6232
|
+
revealed ? /* @__PURE__ */ jsxs7("text", {
|
|
5895
6233
|
fg: theme.dim,
|
|
5896
6234
|
children: [
|
|
5897
6235
|
"Visible for ",
|
|
@@ -5899,30 +6237,30 @@ function Detail({ entry, revealed, otp, now, confirmDelete, actions, focusedActi
|
|
|
5899
6237
|
"s \u2014 drag to select, ctrl+c to copy the selection."
|
|
5900
6238
|
]
|
|
5901
6239
|
}) : null,
|
|
5902
|
-
confirmDelete ? /* @__PURE__ */
|
|
6240
|
+
confirmDelete ? /* @__PURE__ */ jsx7("box", {
|
|
5903
6241
|
backgroundColor: theme.dangerBg,
|
|
5904
6242
|
paddingX: 1,
|
|
5905
|
-
children: /* @__PURE__ */
|
|
6243
|
+
children: /* @__PURE__ */ jsxs7("text", {
|
|
5906
6244
|
fg: theme.danger,
|
|
5907
6245
|
children: [
|
|
5908
|
-
/* @__PURE__ */
|
|
6246
|
+
/* @__PURE__ */ jsxs7("strong", {
|
|
5909
6247
|
children: [
|
|
5910
6248
|
'Delete "',
|
|
5911
6249
|
entry.name,
|
|
5912
6250
|
'" permanently?'
|
|
5913
6251
|
]
|
|
5914
6252
|
}),
|
|
5915
|
-
/* @__PURE__ */
|
|
6253
|
+
/* @__PURE__ */ jsx7("span", {
|
|
5916
6254
|
fg: theme.muted,
|
|
5917
|
-
children: " y = delete \xB7 n = cancel"
|
|
6255
|
+
children: links?.links.length ? ` y = only this \xB7 a = also its ${links.links.length} linked \xB7 n = cancel` : " y = delete \xB7 n = cancel"
|
|
5918
6256
|
})
|
|
5919
6257
|
]
|
|
5920
6258
|
})
|
|
5921
|
-
}) : /* @__PURE__ */
|
|
6259
|
+
}) : /* @__PURE__ */ jsx7("box", {
|
|
5922
6260
|
flexDirection: "row",
|
|
5923
6261
|
gap: 1,
|
|
5924
6262
|
flexWrap: "wrap",
|
|
5925
|
-
children: actions.map((action, i) => /* @__PURE__ */
|
|
6263
|
+
children: actions.map((action, i) => /* @__PURE__ */ jsx7(Button, {
|
|
5926
6264
|
hotkey: action.hotkey,
|
|
5927
6265
|
label: action.label,
|
|
5928
6266
|
onPress: action.run,
|
|
@@ -5937,6 +6275,7 @@ var PANES, WIDE_MIN_COLUMNS = 96;
|
|
|
5937
6275
|
var init_vault = __esm(() => {
|
|
5938
6276
|
init_components();
|
|
5939
6277
|
init_context();
|
|
6278
|
+
init_links();
|
|
5940
6279
|
init_theme();
|
|
5941
6280
|
init_util();
|
|
5942
6281
|
PANES = ["search", "list", "actions"];
|
|
@@ -5945,29 +6284,29 @@ var init_vault = __esm(() => {
|
|
|
5945
6284
|
// apps/cli/src/tui.tsx
|
|
5946
6285
|
import { basename as basename3 } from "path";
|
|
5947
6286
|
import { createCliRenderer } from "@opentui/core";
|
|
5948
|
-
import { createRoot, useKeyboard as
|
|
5949
|
-
import { useCallback as useCallback4, useEffect as useEffect7, useMemo as
|
|
5950
|
-
import { jsx as
|
|
6287
|
+
import { createRoot, useKeyboard as useKeyboard8, useRenderer, useSelectionHandler } from "@opentui/react";
|
|
6288
|
+
import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo7, useRef as useRef8, useState as useState8 } from "react";
|
|
6289
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "@opentui/react/jsx-runtime";
|
|
5951
6290
|
function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs: initialClipboardClearMs, idleLockMs: initialIdleLockMs, onQuit, sessionSink, passkeyRunner }) {
|
|
5952
6291
|
const renderer = useRenderer();
|
|
5953
|
-
const [timings, setTimingsState] =
|
|
6292
|
+
const [timings, setTimingsState] = useState8({ revealMs: initialRevealMs, clipboardClearMs: initialClipboardClearMs, idleLockMs: initialIdleLockMs });
|
|
5954
6293
|
const { revealMs, clipboardClearMs, idleLockMs } = timings;
|
|
5955
6294
|
const setTimings = useCallback4((next) => setTimingsState((prev) => ({ ...prev, ...next })), []);
|
|
5956
|
-
const [route, setRoute] =
|
|
5957
|
-
const [session, setSession] =
|
|
5958
|
-
const [toastState, setToastState] =
|
|
5959
|
-
const [hints, setHints] =
|
|
5960
|
-
const [paletteOpen, setPaletteOpen] =
|
|
5961
|
-
const commandsRef =
|
|
5962
|
-
const copyTargetRef =
|
|
5963
|
-
const sessionRef =
|
|
6295
|
+
const [route, setRoute] = useState8({ name: "unlock" });
|
|
6296
|
+
const [session, setSession] = useState8(null);
|
|
6297
|
+
const [toastState, setToastState] = useState8(null);
|
|
6298
|
+
const [hints, setHints] = useState8([]);
|
|
6299
|
+
const [paletteOpen, setPaletteOpen] = useState8(false);
|
|
6300
|
+
const commandsRef = useRef8([]);
|
|
6301
|
+
const copyTargetRef = useRef8(null);
|
|
6302
|
+
const sessionRef = useRef8(null);
|
|
5964
6303
|
sessionRef.current = session;
|
|
5965
6304
|
if (sessionSink)
|
|
5966
6305
|
sessionSink.current = session;
|
|
5967
|
-
const lastCtrlC =
|
|
5968
|
-
const lastActivity =
|
|
5969
|
-
const toastTimer =
|
|
5970
|
-
const quitting =
|
|
6306
|
+
const lastCtrlC = useRef8(0);
|
|
6307
|
+
const lastActivity = useRef8(Date.now());
|
|
6308
|
+
const toastTimer = useRef8(null);
|
|
6309
|
+
const quitting = useRef8(false);
|
|
5971
6310
|
const toast = useCallback4((message, kind = "success") => {
|
|
5972
6311
|
if (toastTimer.current)
|
|
5973
6312
|
clearTimeout(toastTimer.current);
|
|
@@ -6001,7 +6340,7 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
6001
6340
|
onQuit();
|
|
6002
6341
|
}, [onQuit]);
|
|
6003
6342
|
const openPalette = useCallback4(() => setPaletteOpen(true), []);
|
|
6004
|
-
const api =
|
|
6343
|
+
const api = useMemo7(() => ({
|
|
6005
6344
|
active: !paletteOpen,
|
|
6006
6345
|
toast,
|
|
6007
6346
|
copy,
|
|
@@ -6039,7 +6378,7 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
6039
6378
|
}
|
|
6040
6379
|
toast("Press ctrl+c again to quit", "info");
|
|
6041
6380
|
}, [renderer, clipboard, clipboardClearMs, quit, toast]);
|
|
6042
|
-
|
|
6381
|
+
useKeyboard8((key) => {
|
|
6043
6382
|
lastActivity.current = Date.now();
|
|
6044
6383
|
if (!key.ctrl || key.meta)
|
|
6045
6384
|
return;
|
|
@@ -6105,7 +6444,7 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
6105
6444
|
const vaultName = basename3(vaultPath);
|
|
6106
6445
|
let screen;
|
|
6107
6446
|
if (route.name === "unlock" || !session) {
|
|
6108
|
-
screen = /* @__PURE__ */
|
|
6447
|
+
screen = /* @__PURE__ */ jsx8(UnlockScreen, {
|
|
6109
6448
|
vaultPath,
|
|
6110
6449
|
onUnlock,
|
|
6111
6450
|
setHints,
|
|
@@ -6113,7 +6452,7 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
6113
6452
|
passkeyRunner
|
|
6114
6453
|
}, `unlock:${route.name === "unlock" ? route.notice ?? "" : ""}`);
|
|
6115
6454
|
} else if (route.name === "form") {
|
|
6116
|
-
screen = /* @__PURE__ */
|
|
6455
|
+
screen = /* @__PURE__ */ jsx8(SecretForm, {
|
|
6117
6456
|
session,
|
|
6118
6457
|
editing: route.editing,
|
|
6119
6458
|
setHints,
|
|
@@ -6125,14 +6464,14 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
6125
6464
|
}
|
|
6126
6465
|
});
|
|
6127
6466
|
} else if (route.name === "settings") {
|
|
6128
|
-
screen = /* @__PURE__ */
|
|
6467
|
+
screen = /* @__PURE__ */ jsx8(SettingsScreen, {
|
|
6129
6468
|
vaultPath,
|
|
6130
6469
|
setHints,
|
|
6131
6470
|
onBack: goVault,
|
|
6132
6471
|
passkeyRunner
|
|
6133
6472
|
});
|
|
6134
6473
|
} else {
|
|
6135
|
-
screen = /* @__PURE__ */
|
|
6474
|
+
screen = /* @__PURE__ */ jsx8(VaultScreen, {
|
|
6136
6475
|
session,
|
|
6137
6476
|
initialSelectedId: route.focusId,
|
|
6138
6477
|
onCreate: () => setRoute({ name: "form", editing: null }),
|
|
@@ -6142,9 +6481,9 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
6142
6481
|
revealMs
|
|
6143
6482
|
}, route.focusId ?? "vault");
|
|
6144
6483
|
}
|
|
6145
|
-
return /* @__PURE__ */
|
|
6484
|
+
return /* @__PURE__ */ jsx8(AppContext.Provider, {
|
|
6146
6485
|
value: api,
|
|
6147
|
-
children: /* @__PURE__ */
|
|
6486
|
+
children: /* @__PURE__ */ jsxs8("box", {
|
|
6148
6487
|
flexDirection: "column",
|
|
6149
6488
|
width: "100%",
|
|
6150
6489
|
height: "100%",
|
|
@@ -6155,15 +6494,15 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
6155
6494
|
lastActivity.current = Date.now();
|
|
6156
6495
|
},
|
|
6157
6496
|
children: [
|
|
6158
|
-
/* @__PURE__ */
|
|
6497
|
+
/* @__PURE__ */ jsx8(Header, {
|
|
6159
6498
|
title,
|
|
6160
|
-
right: /* @__PURE__ */
|
|
6499
|
+
right: /* @__PURE__ */ jsxs8("text", {
|
|
6161
6500
|
fg: theme.dim,
|
|
6162
6501
|
children: [
|
|
6163
|
-
session ? /* @__PURE__ */
|
|
6502
|
+
session ? /* @__PURE__ */ jsx8("span", {
|
|
6164
6503
|
fg: theme.success,
|
|
6165
6504
|
children: "\u25CF unlocked"
|
|
6166
|
-
}) : /* @__PURE__ */
|
|
6505
|
+
}) : /* @__PURE__ */ jsx8("span", {
|
|
6167
6506
|
fg: theme.muted,
|
|
6168
6507
|
children: "\u25CB locked"
|
|
6169
6508
|
}),
|
|
@@ -6172,17 +6511,17 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
|
|
|
6172
6511
|
]
|
|
6173
6512
|
})
|
|
6174
6513
|
}),
|
|
6175
|
-
/* @__PURE__ */
|
|
6514
|
+
/* @__PURE__ */ jsx8("box", {
|
|
6176
6515
|
flexDirection: "column",
|
|
6177
6516
|
flexGrow: 1,
|
|
6178
6517
|
children: screen
|
|
6179
6518
|
}),
|
|
6180
|
-
/* @__PURE__ */
|
|
6519
|
+
/* @__PURE__ */ jsx8(StatusBar, {
|
|
6181
6520
|
hints,
|
|
6182
6521
|
pinned: globalHints,
|
|
6183
6522
|
toast: toastState
|
|
6184
6523
|
}),
|
|
6185
|
-
paletteOpen ? /* @__PURE__ */
|
|
6524
|
+
paletteOpen ? /* @__PURE__ */ jsx8(CommandPalette, {
|
|
6186
6525
|
commands: [...commandsRef.current, ...globalCommands],
|
|
6187
6526
|
onClose: () => setPaletteOpen(false)
|
|
6188
6527
|
}) : null
|
|
@@ -6210,7 +6549,7 @@ async function runTui(vaultPath) {
|
|
|
6210
6549
|
closeSessionAndRecordSeal(vault, sessionSink.current).catch(() => {});
|
|
6211
6550
|
sessionSink.current = null;
|
|
6212
6551
|
});
|
|
6213
|
-
createRoot(renderer).render(/* @__PURE__ */
|
|
6552
|
+
createRoot(renderer).render(/* @__PURE__ */ jsx8(App, {
|
|
6214
6553
|
vaultPath: vault,
|
|
6215
6554
|
clipboard,
|
|
6216
6555
|
revealMs: numberPreference("revealSeconds") * 1000,
|
|
@@ -6238,10 +6577,9 @@ var init_tui = __esm(() => {
|
|
|
6238
6577
|
init_clipboard();
|
|
6239
6578
|
init_passkey();
|
|
6240
6579
|
init_package();
|
|
6241
|
-
import { spawn as spawn3 } from "child_process";
|
|
6242
6580
|
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
6243
6581
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
6244
|
-
import { basename as basename4, isAbsolute, resolve as
|
|
6582
|
+
import { basename as basename4, isAbsolute, resolve as resolve6 } from "path";
|
|
6245
6583
|
|
|
6246
6584
|
// apps/cli/src/password.ts
|
|
6247
6585
|
import { createInterface } from "readline/promises";
|
|
@@ -6683,6 +7021,321 @@ async function daemonStop(options) {
|
|
|
6683
7021
|
// apps/cli/src/index.ts
|
|
6684
7022
|
init_src3();
|
|
6685
7023
|
init_os_secrets();
|
|
7024
|
+
|
|
7025
|
+
// apps/web/src/server.ts
|
|
7026
|
+
init_src();
|
|
7027
|
+
init_src3();
|
|
7028
|
+
init_os_secrets();
|
|
7029
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
7030
|
+
import { resolve as resolve4 } from "path";
|
|
7031
|
+
function json2(data, status = 200, headers) {
|
|
7032
|
+
return new Response(JSON.stringify(data, null, 2), {
|
|
7033
|
+
headers: {
|
|
7034
|
+
"content-type": "application/json; charset=utf-8",
|
|
7035
|
+
...headers ?? {}
|
|
7036
|
+
},
|
|
7037
|
+
status
|
|
7038
|
+
});
|
|
7039
|
+
}
|
|
7040
|
+
function html(body) {
|
|
7041
|
+
return new Response(body, {
|
|
7042
|
+
headers: {
|
|
7043
|
+
"content-type": "text/html; charset=utf-8"
|
|
7044
|
+
}
|
|
7045
|
+
});
|
|
7046
|
+
}
|
|
7047
|
+
function parseCookies(request) {
|
|
7048
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
7049
|
+
return Object.fromEntries(cookieHeader.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
7050
|
+
const index = part.indexOf("=");
|
|
7051
|
+
return [part.slice(0, index), decodeURIComponent(part.slice(index + 1))];
|
|
7052
|
+
}));
|
|
7053
|
+
}
|
|
7054
|
+
async function openSession(vaultPath, state) {
|
|
7055
|
+
return new VaultSession(new AuthoDatabase(vaultPath), state.rootKey, {
|
|
7056
|
+
sealBound: state.sealBound,
|
|
7057
|
+
sealRecord: await loadSealRecord(vaultPath)
|
|
7058
|
+
});
|
|
7059
|
+
}
|
|
7060
|
+
function requireSession(request, sessions) {
|
|
7061
|
+
const cookies = parseCookies(request);
|
|
7062
|
+
const sessionId = cookies.autho_session;
|
|
7063
|
+
if (!sessionId) {
|
|
7064
|
+
throw new Error("Missing web session");
|
|
7065
|
+
}
|
|
7066
|
+
const session = sessions.get(sessionId);
|
|
7067
|
+
if (!session) {
|
|
7068
|
+
throw new Error("Unknown web session");
|
|
7069
|
+
}
|
|
7070
|
+
if (Date.parse(session.expiresAt) <= Date.now()) {
|
|
7071
|
+
sessions.delete(sessionId);
|
|
7072
|
+
throw new Error("Expired web session");
|
|
7073
|
+
}
|
|
7074
|
+
return { ...session, id: sessionId };
|
|
7075
|
+
}
|
|
7076
|
+
async function readJson2(request) {
|
|
7077
|
+
return await request.json();
|
|
7078
|
+
}
|
|
7079
|
+
function page2() {
|
|
7080
|
+
return `<!doctype html>
|
|
7081
|
+
<html lang="en">
|
|
7082
|
+
<head>
|
|
7083
|
+
<meta charset="utf-8" />
|
|
7084
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7085
|
+
<title>Autho</title>
|
|
7086
|
+
<style>
|
|
7087
|
+
:root { color-scheme: light; font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
7088
|
+
body { margin: 0; background: #f5f1e8; color: #1f2937; }
|
|
7089
|
+
main { max-width: 760px; margin: 0 auto; padding: 32px 20px 64px; }
|
|
7090
|
+
section { background: #fffdf8; border: 1px solid #d6cbb8; border-radius: 16px; padding: 20px; margin-bottom: 16px; box-shadow: 0 8px 30px rgba(83,58,18,0.08); }
|
|
7091
|
+
h1 { margin-top: 0; font-size: 2rem; }
|
|
7092
|
+
input, select, button { font: inherit; padding: 10px 12px; border-radius: 10px; border: 1px solid #bca889; }
|
|
7093
|
+
button { background: #1f4d3a; color: white; border: none; cursor: pointer; }
|
|
7094
|
+
form { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
7095
|
+
pre { background: #1f2937; color: #f9fafb; padding: 12px; border-radius: 12px; overflow: auto; }
|
|
7096
|
+
ul { padding-left: 18px; }
|
|
7097
|
+
</style>
|
|
7098
|
+
</head>
|
|
7099
|
+
<body>
|
|
7100
|
+
<main>
|
|
7101
|
+
<section>
|
|
7102
|
+
<h1>Autho Local Web</h1>
|
|
7103
|
+
<p>Local-only Bun web UI for vault unlock and secret browsing.</p>
|
|
7104
|
+
<form id="unlock-form">
|
|
7105
|
+
<input id="password" type="password" placeholder="Master password" required />
|
|
7106
|
+
<input id="totp" inputmode="numeric" autocomplete="one-time-code" placeholder="Authenticator code (if enabled)" />
|
|
7107
|
+
<button type="submit">Unlock</button>
|
|
7108
|
+
</form>
|
|
7109
|
+
</section>
|
|
7110
|
+
<section>
|
|
7111
|
+
<h2>Secrets</h2>
|
|
7112
|
+
<button id="refresh">Refresh</button>
|
|
7113
|
+
<ul id="secret-list"></ul>
|
|
7114
|
+
<pre id="output"></pre>
|
|
7115
|
+
</section>
|
|
7116
|
+
<section id="linked" hidden>
|
|
7117
|
+
<h2>Linked to <span id="linked-name"></span></h2>
|
|
7118
|
+
<ul id="linked-list"></ul>
|
|
7119
|
+
<p id="linked-from"></p>
|
|
7120
|
+
<form id="link-form">
|
|
7121
|
+
<input id="link-ref" placeholder="Secret name or id to link" required />
|
|
7122
|
+
<button type="submit">Link</button>
|
|
7123
|
+
</form>
|
|
7124
|
+
</section>
|
|
7125
|
+
</main>
|
|
7126
|
+
<script>
|
|
7127
|
+
const output = document.getElementById('output');
|
|
7128
|
+
const secretList = document.getElementById('secret-list');
|
|
7129
|
+
async function refresh() {
|
|
7130
|
+
const response = await fetch('/api/secrets');
|
|
7131
|
+
if (!response.ok) {
|
|
7132
|
+
output.textContent = await response.text();
|
|
7133
|
+
return;
|
|
7134
|
+
}
|
|
7135
|
+
const data = await response.json();
|
|
7136
|
+
secretList.innerHTML = '';
|
|
7137
|
+
data.data.forEach((secret) => {
|
|
7138
|
+
const item = document.createElement('li');
|
|
7139
|
+
const open = document.createElement('a');
|
|
7140
|
+
open.href = '#';
|
|
7141
|
+
open.textContent = secret.name + " (" + secret.type + ")" + (secret.linkCount ? " \xB7 " + secret.linkCount + " linked" : "");
|
|
7142
|
+
open.addEventListener('click', (event) => { event.preventDefault(); showSecret(secret.id); });
|
|
7143
|
+
item.appendChild(open);
|
|
7144
|
+
secretList.appendChild(item);
|
|
7145
|
+
});
|
|
7146
|
+
output.textContent = JSON.stringify(data, null, 2);
|
|
7147
|
+
}
|
|
7148
|
+
let current = null;
|
|
7149
|
+
async function showSecret(ref) {
|
|
7150
|
+
const response = await fetch('/api/secrets/' + encodeURIComponent(ref));
|
|
7151
|
+
if (!response.ok) {
|
|
7152
|
+
output.textContent = await response.text();
|
|
7153
|
+
return;
|
|
7154
|
+
}
|
|
7155
|
+
const secret = (await response.json()).data;
|
|
7156
|
+
current = secret;
|
|
7157
|
+
const { links, linkedFrom, ...rest } = secret;
|
|
7158
|
+
output.textContent = JSON.stringify(rest, null, 2);
|
|
7159
|
+
document.getElementById('linked').hidden = false;
|
|
7160
|
+
document.getElementById('linked-name').textContent = secret.name;
|
|
7161
|
+
const list = document.getElementById('linked-list');
|
|
7162
|
+
list.innerHTML = '';
|
|
7163
|
+
if (!links.length) {
|
|
7164
|
+
list.innerHTML = '<li>No linked secrets</li>';
|
|
7165
|
+
}
|
|
7166
|
+
links.forEach((linked) => {
|
|
7167
|
+
const item = document.createElement('li');
|
|
7168
|
+
const open = document.createElement('a');
|
|
7169
|
+
open.href = '#';
|
|
7170
|
+
open.textContent = linked.name + " (" + linked.type + ") ";
|
|
7171
|
+
open.addEventListener('click', (event) => { event.preventDefault(); showSecret(linked.id); });
|
|
7172
|
+
const unlink = document.createElement('button');
|
|
7173
|
+
unlink.textContent = 'Unlink';
|
|
7174
|
+
unlink.addEventListener('click', () => changeLink('DELETE', linked.id));
|
|
7175
|
+
item.append(open, unlink);
|
|
7176
|
+
list.appendChild(item);
|
|
7177
|
+
});
|
|
7178
|
+
document.getElementById('linked-from').textContent = linkedFrom.length
|
|
7179
|
+
? 'Linked from: ' + linkedFrom.map((primary) => primary.name).join(', ')
|
|
7180
|
+
: '';
|
|
7181
|
+
}
|
|
7182
|
+
async function changeLink(method, secret) {
|
|
7183
|
+
const response = await fetch('/api/links', {
|
|
7184
|
+
method,
|
|
7185
|
+
headers: { 'content-type': 'application/json' },
|
|
7186
|
+
body: JSON.stringify({ primary: current.id, secret })
|
|
7187
|
+
});
|
|
7188
|
+
if (!response.ok) {
|
|
7189
|
+
output.textContent = await response.text();
|
|
7190
|
+
return;
|
|
7191
|
+
}
|
|
7192
|
+
await showSecret(current.id);
|
|
7193
|
+
await refresh();
|
|
7194
|
+
}
|
|
7195
|
+
document.getElementById('link-form').addEventListener('submit', async (event) => {
|
|
7196
|
+
event.preventDefault();
|
|
7197
|
+
const input = document.getElementById('link-ref');
|
|
7198
|
+
await changeLink('POST', input.value);
|
|
7199
|
+
input.value = '';
|
|
7200
|
+
});
|
|
7201
|
+
document.getElementById('unlock-form').addEventListener('submit', async (event) => {
|
|
7202
|
+
event.preventDefault();
|
|
7203
|
+
const response = await fetch('/api/session/unlock', {
|
|
7204
|
+
method: 'POST',
|
|
7205
|
+
headers: { 'content-type': 'application/json' },
|
|
7206
|
+
body: JSON.stringify({
|
|
7207
|
+
password: document.getElementById('password').value,
|
|
7208
|
+
totp: document.getElementById('totp').value || undefined
|
|
7209
|
+
})
|
|
7210
|
+
});
|
|
7211
|
+
output.textContent = await response.text();
|
|
7212
|
+
if (response.ok) {
|
|
7213
|
+
await refresh();
|
|
7214
|
+
}
|
|
7215
|
+
});
|
|
7216
|
+
document.getElementById('refresh').addEventListener('click', refresh);
|
|
7217
|
+
</script>
|
|
7218
|
+
</body>
|
|
7219
|
+
</html>`;
|
|
7220
|
+
}
|
|
7221
|
+
async function startWebServer(options = {}) {
|
|
7222
|
+
const vaultPath = resolve4(options.vaultPath ?? defaultVaultPath());
|
|
7223
|
+
const host = options.host ?? "127.0.0.1";
|
|
7224
|
+
const port = options.port ?? 8061;
|
|
7225
|
+
const sessions = new Map;
|
|
7226
|
+
const cleanup = () => {
|
|
7227
|
+
for (const [id, session] of sessions.entries()) {
|
|
7228
|
+
if (Date.parse(session.expiresAt) <= Date.now()) {
|
|
7229
|
+
sessions.delete(id);
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
7232
|
+
};
|
|
7233
|
+
const server = Bun.serve({
|
|
7234
|
+
fetch: async (request) => {
|
|
7235
|
+
cleanup();
|
|
7236
|
+
const url = new URL(request.url);
|
|
7237
|
+
try {
|
|
7238
|
+
if (request.method === "GET" && url.pathname === "/health") {
|
|
7239
|
+
return json2({ ok: true, running: true, vaultPath });
|
|
7240
|
+
}
|
|
7241
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
7242
|
+
return html(page2());
|
|
7243
|
+
}
|
|
7244
|
+
if (request.method === "POST" && url.pathname === "/api/session/unlock") {
|
|
7245
|
+
const body = await readJson2(request);
|
|
7246
|
+
const ttlSeconds = body.ttlSeconds ?? 3600;
|
|
7247
|
+
if (ttlSeconds <= 0 || ttlSeconds > 86400) {
|
|
7248
|
+
return json2({ error: "Session TTL must be between 1 and 86400 seconds" }, 400);
|
|
7249
|
+
}
|
|
7250
|
+
const { rootKey, sealBound } = VaultService.unlockRootKey(vaultPath, {
|
|
7251
|
+
password: body.password,
|
|
7252
|
+
sealRecord: await loadSealRecord(vaultPath),
|
|
7253
|
+
totp: body.totp
|
|
7254
|
+
});
|
|
7255
|
+
const sessionId = randomBytes6(16).toString("hex");
|
|
7256
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
7257
|
+
sessions.set(sessionId, { expiresAt, rootKey, sealBound });
|
|
7258
|
+
return json2({ expiresAt, sessionId }, 200, { "set-cookie": `autho_session=${sessionId}; HttpOnly; SameSite=Strict; Path=/; Secure` });
|
|
7259
|
+
}
|
|
7260
|
+
if (request.method === "POST" && url.pathname === "/api/session/lock") {
|
|
7261
|
+
const cookies = parseCookies(request);
|
|
7262
|
+
if (cookies.autho_session) {
|
|
7263
|
+
sessions.delete(cookies.autho_session);
|
|
7264
|
+
}
|
|
7265
|
+
return json2({ locked: true }, 200, { "set-cookie": "autho_session=; Max-Age=0; Path=/; Secure" });
|
|
7266
|
+
}
|
|
7267
|
+
if (url.pathname.startsWith("/api/")) {
|
|
7268
|
+
const webSession = requireSession(request, sessions);
|
|
7269
|
+
const session = await openSession(vaultPath, webSession);
|
|
7270
|
+
try {
|
|
7271
|
+
if (request.method === "GET" && url.pathname === "/api/status") {
|
|
7272
|
+
return json2({
|
|
7273
|
+
initialized: true,
|
|
7274
|
+
secretCount: session.listSecrets().length,
|
|
7275
|
+
unlocked: true,
|
|
7276
|
+
vaultPath
|
|
7277
|
+
});
|
|
7278
|
+
}
|
|
7279
|
+
if (request.method === "GET" && url.pathname === "/api/secrets") {
|
|
7280
|
+
return json2({ data: session.listSecretIndex() });
|
|
7281
|
+
}
|
|
7282
|
+
if (request.method === "GET" && url.pathname.startsWith("/api/secrets/")) {
|
|
7283
|
+
const ref = decodeURIComponent(url.pathname.slice("/api/secrets/".length));
|
|
7284
|
+
const { linkedFrom, links } = session.getLinks(ref);
|
|
7285
|
+
return json2({ data: { ...session.getSecret(ref), linkedFrom, links } });
|
|
7286
|
+
}
|
|
7287
|
+
if ((request.method === "POST" || request.method === "DELETE") && url.pathname === "/api/links") {
|
|
7288
|
+
const body = await readJson2(request);
|
|
7289
|
+
return json2({
|
|
7290
|
+
data: request.method === "POST" ? session.linkSecrets(body.primary, body.secret) : session.unlinkSecrets(body.primary, body.secret)
|
|
7291
|
+
});
|
|
7292
|
+
}
|
|
7293
|
+
if (request.method === "POST" && url.pathname === "/api/secrets") {
|
|
7294
|
+
const body = await readJson2(request);
|
|
7295
|
+
return json2({
|
|
7296
|
+
data: session.addSecret({
|
|
7297
|
+
metadata: Object.fromEntries(Object.entries({
|
|
7298
|
+
algorithm: body.algorithm,
|
|
7299
|
+
description: body.description,
|
|
7300
|
+
digits: body.digits,
|
|
7301
|
+
url: body.url
|
|
7302
|
+
}).filter(([, value]) => value !== undefined)),
|
|
7303
|
+
name: body.name,
|
|
7304
|
+
type: body.type,
|
|
7305
|
+
username: body.username,
|
|
7306
|
+
value: body.value
|
|
7307
|
+
})
|
|
7308
|
+
}, 201);
|
|
7309
|
+
}
|
|
7310
|
+
if (request.method === "DELETE" && url.pathname.startsWith("/api/secrets/")) {
|
|
7311
|
+
const ref = decodeURIComponent(url.pathname.slice("/api/secrets/".length));
|
|
7312
|
+
return json2({ data: session.removeSecret(ref, { withLinked: url.searchParams.get("withLinked") === "1" }) });
|
|
7313
|
+
}
|
|
7314
|
+
if (request.method === "GET" && url.pathname.startsWith("/api/otp/")) {
|
|
7315
|
+
const ref = decodeURIComponent(url.pathname.slice("/api/otp/".length));
|
|
7316
|
+
return json2({ data: session.generateOtp(ref) });
|
|
7317
|
+
}
|
|
7318
|
+
} finally {
|
|
7319
|
+
await closeSessionAndRecordSeal(vaultPath, session);
|
|
7320
|
+
}
|
|
7321
|
+
}
|
|
7322
|
+
} catch (error) {
|
|
7323
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7324
|
+
const status = /session/i.test(message) ? 401 : 400;
|
|
7325
|
+
return json2({ error: message }, status);
|
|
7326
|
+
}
|
|
7327
|
+
return json2({ error: "Not found" }, 404);
|
|
7328
|
+
},
|
|
7329
|
+
hostname: host,
|
|
7330
|
+
port
|
|
7331
|
+
});
|
|
7332
|
+
process.on("SIGINT", () => process.exit(0));
|
|
7333
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
7334
|
+
console.log(`Autho web server listening on http://${host}:${server.port}`);
|
|
7335
|
+
await new Promise(() => {});
|
|
7336
|
+
}
|
|
7337
|
+
|
|
7338
|
+
// apps/cli/src/index.ts
|
|
6686
7339
|
function parseArgs(argv) {
|
|
6687
7340
|
const dashDashIndex = argv.indexOf("--");
|
|
6688
7341
|
const main = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
|
|
@@ -6776,7 +7429,7 @@ function printSealChanges(changes, diffTrusted) {
|
|
|
6776
7429
|
}
|
|
6777
7430
|
}
|
|
6778
7431
|
function absolutePath(path) {
|
|
6779
|
-
return
|
|
7432
|
+
return resolve6(path);
|
|
6780
7433
|
}
|
|
6781
7434
|
function buildSecretMetadata(args) {
|
|
6782
7435
|
return Object.fromEntries(Object.entries({
|
|
@@ -7031,34 +7684,10 @@ async function runInitWizard(vaultPath, password, existingCreds) {
|
|
|
7031
7684
|
console.log("Setup complete. Run \x1B[1mautho init\x1B[0m anytime to change these settings.");
|
|
7032
7685
|
}
|
|
7033
7686
|
async function runWebServer(vaultPath, args) {
|
|
7034
|
-
|
|
7035
|
-
"
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
"--vault",
|
|
7039
|
-
absolutePath(vaultPath)
|
|
7040
|
-
];
|
|
7041
|
-
const host = getString(args, "host");
|
|
7042
|
-
const port = getString(args, "port") ?? String(numberPreference("webPort"));
|
|
7043
|
-
if (host) {
|
|
7044
|
-
commandArgs.push("--host", host);
|
|
7045
|
-
}
|
|
7046
|
-
if (port) {
|
|
7047
|
-
commandArgs.push("--port", port);
|
|
7048
|
-
}
|
|
7049
|
-
await new Promise((resolvePromise, rejectPromise) => {
|
|
7050
|
-
const child = spawn3(process.execPath, commandArgs, {
|
|
7051
|
-
cwd: process.cwd(),
|
|
7052
|
-
stdio: "inherit"
|
|
7053
|
-
});
|
|
7054
|
-
child.on("exit", (code) => {
|
|
7055
|
-
if (code === 0) {
|
|
7056
|
-
resolvePromise();
|
|
7057
|
-
return;
|
|
7058
|
-
}
|
|
7059
|
-
rejectPromise(new Error(`Web server exited with code ${code ?? 1}`));
|
|
7060
|
-
});
|
|
7061
|
-
child.on("error", rejectPromise);
|
|
7687
|
+
await startWebServer({
|
|
7688
|
+
host: getString(args, "host"),
|
|
7689
|
+
port: Number(getString(args, "port") ?? numberPreference("webPort")),
|
|
7690
|
+
vaultPath: absolutePath(vaultPath)
|
|
7062
7691
|
});
|
|
7063
7692
|
}
|
|
7064
7693
|
function help() {
|
|
@@ -7235,7 +7864,7 @@ async function main() {
|
|
|
7235
7864
|
console.log(" Tip: Use a cloud-synced folder (e.g. Google Drive) to share the vault across machines.");
|
|
7236
7865
|
const customDir = (await readLine(` Path (Enter to keep default): `)).trim();
|
|
7237
7866
|
if (customDir && customDir !== currentDir) {
|
|
7238
|
-
const resolved =
|
|
7867
|
+
const resolved = resolve6(customDir).replace(/\\/g, "/");
|
|
7239
7868
|
saveConfig({ ...config, vaultDir: resolved });
|
|
7240
7869
|
effectiveVaultPath = resolved + "/vault.db";
|
|
7241
7870
|
console.log(` \x1B[32m\u2713\x1B[0m Vault directory set to ${resolved}`);
|
|
@@ -7303,7 +7932,7 @@ async function main() {
|
|
|
7303
7932
|
if (key === "autoLock") {
|
|
7304
7933
|
updated.autoLock = value === "true";
|
|
7305
7934
|
} else if (key === "vaultDir") {
|
|
7306
|
-
updated.vaultDir = isAbsolute(value) ? value :
|
|
7935
|
+
updated.vaultDir = isAbsolute(value) ? value : resolve6(value);
|
|
7307
7936
|
} else if (key === "editor") {
|
|
7308
7937
|
updated.editor = value;
|
|
7309
7938
|
} else if (key === "autoLockTimeout") {
|