@ai-matrx/vault 0.1.28 → 0.1.29

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/index.js CHANGED
@@ -101,8 +101,8 @@ var init_keepass_xml_worker_client = __esm({
101
101
  });
102
102
 
103
103
  // src/VaultApp.tsx
104
- import { useCallback as useCallback5, useEffect as useEffect23, useMemo as useMemo6, useRef as useRef19, useState as useState41 } from "react";
105
- import { DropdownMenu as DropdownMenu6, DropdownMenuContent as DropdownMenuContent6, DropdownMenuItem as DropdownMenuItem6, DropdownMenuTrigger as DropdownMenuTrigger6, LookScope } from "@ai-matrx/design-system";
104
+ import { useCallback as useCallback5, useEffect as useEffect23, useMemo as useMemo6, useRef as useRef19, useState as useState41, useSyncExternalStore as useSyncExternalStore5 } from "react";
105
+ import { DropdownMenu as DropdownMenu5, DropdownMenuContent as DropdownMenuContent5, DropdownMenuItem as DropdownMenuItem5, DropdownMenuTrigger as DropdownMenuTrigger5, LookScope } from "@ai-matrx/design-system";
106
106
  import { Button as Button34, SearchField as SearchField4, Select as Select12 } from "@ai-matrx/design-system/controls";
107
107
  import { confirm as confirm14 } from "@ai-matrx/kit/confirm-opener";
108
108
 
@@ -119,8 +119,8 @@ async function copyValue(text, label, options, deps) {
119
119
  const secret = options.secret ?? true;
120
120
  if (deps.clipboard) {
121
121
  try {
122
- await deps.clipboard.copy(text, { secret });
123
- deps.notify(`${label} copied`, "success");
122
+ const copied = await deps.clipboard.copy(text, { secret });
123
+ deps.notify(copiedLine(label, copied?.clearsAtMs ?? null), "success");
124
124
  return true;
125
125
  } catch (e) {
126
126
  deps.notify(e instanceof Error && e.message ? e.message : `${label} could not be copied`, "error");
@@ -129,6 +129,11 @@ async function copyValue(text, label, options, deps) {
129
129
  }
130
130
  return copyText(text, { successMessage: `${label} copied`, failureMessage: `${label} could not be copied`, notify: (m, k) => deps.notify(m, k === "error" ? "error" : "success") });
131
131
  }
132
+ function copiedLine(label, clearsAtMs) {
133
+ if (clearsAtMs === null || !Number.isFinite(clearsAtMs)) return `${label} copied`;
134
+ const at = new Date(clearsAtMs).toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
135
+ return `${label} copied. Clears at ${at}`;
136
+ }
132
137
  var ClipboardCtx = createContext(void 0);
133
138
  function ClipboardProvider({ clipboard, children }) {
134
139
  return /* @__PURE__ */ jsx(ClipboardCtx.Provider, { value: clipboard, children });
@@ -576,6 +581,17 @@ function useElementWidth(ref) {
576
581
  }, [ref]);
577
582
  return width;
578
583
  }
584
+ function useDisposable(create, deps) {
585
+ const [held, setHeld] = useState3(null);
586
+ const make = useRef3(create);
587
+ make.current = create;
588
+ useEffect3(() => {
589
+ const made = make.current();
590
+ setHeld(made);
591
+ return () => made.dispose();
592
+ }, deps);
593
+ return held;
594
+ }
579
595
 
580
596
  // src/ui.tsx
581
597
  import { createContext as createContext4, useContext as useContext4, useEffect as useEffect4, useState as useState4 } from "react";
@@ -3126,6 +3142,27 @@ function importLimitsFrom(resolved) {
3126
3142
  return out;
3127
3143
  }
3128
3144
 
3145
+ // src/knobs.ts
3146
+ import { createKnobReader } from "@ai-matrx/data/knobs";
3147
+ var FRESH_MS = 6e4;
3148
+ function createVaultKnobs(client, address) {
3149
+ const reader = createKnobReader(client);
3150
+ let kept = null;
3151
+ return async () => {
3152
+ if (kept && Date.now() - kept.at < FRESH_MS) return kept.resolved;
3153
+ const snap = await reader.snapshot(address);
3154
+ kept = { at: Date.now(), resolved: snap.resolved };
3155
+ return snap.resolved;
3156
+ };
3157
+ }
3158
+ var REVEAL_SECONDS_KNOB = "desktop.vault.reveal_hide_seconds";
3159
+ var BACKUP_PASSPHRASE_MIN_KNOB = "desktop.vault.backup_passphrase_min_length";
3160
+ function positiveKnob(resolved, key) {
3161
+ const v = resolved[key];
3162
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
3163
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
3164
+ }
3165
+
3129
3166
  // src/lock.ts
3130
3167
  import { useSyncExternalStore as useSyncExternalStore2 } from "react";
3131
3168
  var LOCKED_MESSAGE = "The vault is locked. Unlock it first.";
@@ -3321,7 +3358,8 @@ function Omissions({ omissions, accepted, onAccepted, verb }) {
3321
3358
  /* @__PURE__ */ jsx17(VaultRow, { label: "Accept", hint: `${verb} without these`, children: /* @__PURE__ */ jsx17(Switch3, { checked: accepted, onCheckedChange: onAccepted, "aria-label": `${verb} without these` }) })
3322
3359
  ] });
3323
3360
  }
3324
- function BackupPanel({ port, items, onClose, onChanged }) {
3361
+ function BackupPanel({ port, host, items, onClose, onChanged }) {
3362
+ const minLength = passphraseMinimum(host.backupPassphraseMinLength);
3325
3363
  const confirmed = useConfirmed();
3326
3364
  const fileInput = useRef11(null);
3327
3365
  const abort = useRef11(null);
@@ -3376,7 +3414,7 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3376
3414
  setAccepted(false);
3377
3415
  });
3378
3416
  const downloadBackup = () => guarded(async () => {
3379
- if (!backup) return;
3417
+ if (!backup || tooShort(passphrase, minLength)) return;
3380
3418
  const bytes2 = await confirmed(() => port.io.downloadBackup({ itemIds: [...selected], revision: backup.revision, passphrase, acceptOmissions: accepted }));
3381
3419
  downloadFile(BACKUP_FILENAME, bytes2, "application/octet-stream");
3382
3420
  setPassphrase("");
@@ -3423,10 +3461,11 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3423
3461
  };
3424
3462
  const retryable = (result?.retryable_failure ?? 0) > 0;
3425
3463
  const mismatch = repeat !== "" && repeat !== passphrase;
3464
+ const short = passphrase !== "" && tooShort(passphrase, minLength);
3426
3465
  const names = new Map(restorePreview?.records.map((r) => [r.source_item_id, r.display_name]) ?? []);
3427
3466
  const footer = mode === "backup" ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
3428
3467
  /* @__PURE__ */ jsx17(Button13, { variant: "quiet", onClick: onClose, children: "Close" }),
3429
- backup ? /* @__PURE__ */ jsx17(Button13, { variant: "primary", disabled: busy || !passphrase || passphrase !== repeat || backup.omissions.length > 0 && !accepted, onClick: () => void downloadBackup(), children: "Encrypt and download" }) : /* @__PURE__ */ jsxs16(Button13, { variant: "primary", disabled: busy || selected.size === 0, onClick: () => void reviewBackup(), children: [
3468
+ backup ? /* @__PURE__ */ jsx17(Button13, { variant: "primary", disabled: busy || !passphrase || short || passphrase !== repeat || backup.omissions.length > 0 && !accepted, onClick: () => void downloadBackup(), children: "Encrypt and download" }) : /* @__PURE__ */ jsxs16(Button13, { variant: "primary", disabled: busy || selected.size === 0, onClick: () => void reviewBackup(), children: [
3430
3469
  "Review ",
3431
3470
  selected.size
3432
3471
  ] })
@@ -3473,7 +3512,14 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3473
3512
  /* @__PURE__ */ jsx17(Summary, { value: backup }),
3474
3513
  /* @__PURE__ */ jsx17(Omissions, { omissions: backup.omissions, accepted, onAccepted: setAccepted, verb: "Back up" }),
3475
3514
  /* @__PURE__ */ jsxs16(VaultSection, { children: [
3476
- /* @__PURE__ */ jsx17(VaultRow, { label: "Passphrase", hint: "Not saved. It can't be recovered.", children: /* @__PURE__ */ jsx17(Field6, { type: "password", value: passphrase, autoComplete: "new-password", "aria-label": "Backup passphrase", onChange: (e) => setPassphrase(e.target.value) }) }),
3515
+ /* @__PURE__ */ jsx17(VaultRow, { label: "Passphrase", hint: minLength ? `At least ${minLength} characters. Not saved.` : "Not saved. It can't be recovered.", children: /* @__PURE__ */ jsx17(Field6, { type: "password", value: passphrase, autoComplete: "new-password", "aria-label": "Backup passphrase", "aria-invalid": short || void 0, onChange: (e) => setPassphrase(e.target.value) }) }),
3516
+ short ? /* @__PURE__ */ jsxs16("p", { role: "alert", className: "px-1 text-xs text-destructive", "data-passphrase-too-short": "", children: [
3517
+ "Use at least ",
3518
+ minLength,
3519
+ " characters. This one has ",
3520
+ passphrase.length,
3521
+ "."
3522
+ ] }) : null,
3477
3523
  /* @__PURE__ */ jsx17(VaultRow, { label: "Repeat", ...mismatch ? { hint: "Doesn't match" } : {}, children: /* @__PURE__ */ jsx17(Field6, { type: "password", value: repeat, autoComplete: "new-password", "aria-label": "Repeat the passphrase", onChange: (e) => setRepeat(e.target.value) }) })
3478
3524
  ] })
3479
3525
  ] }) : null
@@ -3506,6 +3552,12 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3506
3552
  /* @__PURE__ */ jsx17(ErrorLine, { message: error })
3507
3553
  ] });
3508
3554
  }
3555
+ function passphraseMinimum(given) {
3556
+ return given !== void 0 && Number.isFinite(given) && given > 1 ? Math.floor(given) : null;
3557
+ }
3558
+ function tooShort(passphrase, minLength) {
3559
+ return minLength !== null && [...passphrase].length < minLength;
3560
+ }
3509
3561
 
3510
3562
  // src/io/ExportPanel.tsx
3511
3563
  import { useMemo, useState as useState18 } from "react";
@@ -5963,6 +6015,9 @@ import { Fragment as Fragment18, jsx as jsx27, jsxs as jsxs25 } from "react/jsx-
5963
6015
  var ENV_VALUE_KEY = "env_value";
5964
6016
  var secureNoteDraft = () => ({ key: SECURE_NOTE_KEY, label: "Secure note", value: "", multiline: true, description: "Encrypted notes" });
5965
6017
  var USERNAME_KEYS = /^(username|user_name|email|login|username_or_email|account_email)$/;
6018
+ function confirmDelete(item) {
6019
+ return confirm10({ title: `Delete \u201C${item.name}\u201D?`, description: "It moves to Trash. You can restore it from there.", confirmLabel: "Delete", variant: "destructive" });
6020
+ }
5966
6021
  function ItemPane(props) {
5967
6022
  const { port, item, definition, mode, host, organizations, defaultOwner, seed, onBack, onMode, onSaved, onDeleted, onDirty, onCancelNew, onPanel, onChanged, favorite, notice, refreshKey } = props;
5968
6023
  const confirmed = useConfirmed();
@@ -6115,8 +6170,7 @@ function ItemPane(props) {
6115
6170
  }
6116
6171
  async function remove() {
6117
6172
  if (!item) return;
6118
- const ok = await confirm10({ title: `Delete \u201C${item.name}\u201D?`, description: "It moves to Trash. You can restore it from there.", confirmLabel: "Delete", variant: "destructive" });
6119
- if (!ok) return;
6173
+ if (!await confirmDelete(item)) return;
6120
6174
  setBusy(true);
6121
6175
  try {
6122
6176
  await port.remove(item);
@@ -6527,7 +6581,7 @@ function subtitleOf(item, withOwner) {
6527
6581
  const what = loginSiteOf(item) || item.definitionLabel;
6528
6582
  return withOwner ? `${what} \xB7 ${item.owner.label}` : what;
6529
6583
  }
6530
- function ItemList({ title, items, definitions, port, selectedId, onSelect, query, onQuery, sort, onSort, onNew, matchingIds, header, actions, showOwner, canSortViewed = false, onRefresh, refreshing = false, rowContextMenu = "package", rowMenuItems, stillLoading, favorites, onToggleFavorite }) {
6584
+ function ItemList({ title, items, definitions, port, selectedId, onSelect, query, onQuery, sort, onSort, onNew, matchingIds, header, actions, showOwner, canSortViewed = false, onRefresh, refreshing = false, rowContextMenu = "package", rowMenuItems, stillLoading, favorites, onToggleFavorite, onItemAction }) {
6531
6585
  const sorts = SORTS2.filter((s) => canSortViewed || s.value !== "viewed");
6532
6586
  const pinned = matchingIds ? items.filter((i) => matchingIds.has(i.id)) : [];
6533
6587
  const rest = matchingIds ? items.filter((i) => !matchingIds.has(i.id)) : items;
@@ -6548,7 +6602,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6548
6602
  }
6549
6603
  };
6550
6604
  const mixedOwners = showOwner ?? new Set(items.map((i) => i.owner.organizationId ?? i.owner.kind)).size > 1;
6551
- const row = (i) => /* @__PURE__ */ jsx28(ListRow, { item: i, family: familyOf2(i.definitionKey), port, on: i.id === selectedId, onSelect, withOwner: mixedOwners, ownContextMenu: rowContextMenu === "package", extra: rowMenuItems, favorite: onToggleFavorite ? { on: favorites?.has(i.id) ?? false, toggle: () => onToggleFavorite(i.id) } : void 0 }, i.id);
6605
+ const row = (i) => /* @__PURE__ */ jsx28(ListRow, { item: i, family: familyOf2(i.definitionKey), port, on: i.id === selectedId, onSelect, withOwner: mixedOwners, ownContextMenu: rowContextMenu === "package", extra: rowMenuItems, onAction: onItemAction, favorite: onToggleFavorite ? { on: favorites?.has(i.id) ?? false, toggle: () => onToggleFavorite(i.id) } : void 0 }, i.id);
6552
6606
  const sortLabel = sorts.find((s) => s.value === sort)?.label ?? "Name A\u2013Z";
6553
6607
  return /* @__PURE__ */ jsxs26("section", { "aria-label": title, className: "flex h-full min-h-0 flex-col border-r border-border bg-background", children: [
6554
6608
  /* @__PURE__ */ jsxs26("div", { className: "flex flex-col gap-2 px-3 pb-2 pt-3", children: [
@@ -6592,7 +6646,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6592
6646
  ] }) })
6593
6647
  ] });
6594
6648
  }
6595
- function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu, extra, favorite }) {
6649
+ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu, extra, onAction, favorite }) {
6596
6650
  const confirmed = useConfirmed();
6597
6651
  const copy = useCopy();
6598
6652
  const notify = useNotify();
@@ -6633,11 +6687,12 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6633
6687
  ...favoriteEntry ? [favoriteEntry] : [],
6634
6688
  { id: "open", kind: "open", label: "Open", onSelect: () => onSelect(item.id) },
6635
6689
  { id: "copy-name", kind: "copy-name", label: "Copy name", onSelect: copyName },
6690
+ ...manageEntries(item, onAction),
6636
6691
  ...hostEntries.map((h) => ({ ...h, kind: h.kind ?? "host" }))
6637
6692
  ];
6638
6693
  const entriesRef = useRef15(entries);
6639
6694
  entriesRef.current = entries;
6640
- const glyphOf = (e) => e.id === "copy:username" ? /* @__PURE__ */ jsx28(UserGlyph, { className: "size-4" }) : e.id === "copy:password" ? /* @__PURE__ */ jsx28(KeyGlyph, { className: "size-4" }) : e.id === "copy:code" ? /* @__PURE__ */ jsx28(ClockGlyph, { className: "size-4" }) : e.kind === "favorite" ? /* @__PURE__ */ jsx28(StarGlyph, { className: cn13("size-4", favorite?.on && "fill-current text-warning") }) : e.kind === "open" ? /* @__PURE__ */ jsx28(ChevronRightGlyph, { className: "size-4" }) : e.kind === "host" ? null : /* @__PURE__ */ jsx28(CopyIcon7, { className: "size-4" });
6695
+ const glyphOf = (e) => e.id === "copy:username" ? /* @__PURE__ */ jsx28(UserGlyph, { className: "size-4" }) : e.id === "copy:password" ? /* @__PURE__ */ jsx28(KeyGlyph, { className: "size-4" }) : e.id === "copy:code" ? /* @__PURE__ */ jsx28(ClockGlyph, { className: "size-4" }) : e.kind === "favorite" ? /* @__PURE__ */ jsx28(StarGlyph, { className: cn13("size-4", favorite?.on && "fill-current text-warning") }) : e.kind === "open" ? /* @__PURE__ */ jsx28(ChevronRightGlyph, { className: "size-4" }) : e.kind === "host" || e.kind === "manage" ? null : /* @__PURE__ */ jsx28(CopyIcon7, { className: "size-4" });
6641
6696
  const menuItem = (e) => /* @__PURE__ */ jsxs26(DropdownMenuItem4, { disabled: e.disabled ?? false, onSelect: e.onSelect, children: [
6642
6697
  glyphOf(e),
6643
6698
  e.label
@@ -6691,6 +6746,8 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6691
6746
  copyEntries.map(menuItem),
6692
6747
  copyEntries.length > 0 ? /* @__PURE__ */ jsx28(DropdownMenuSeparator4, {}) : null,
6693
6748
  entries.filter((e) => e.kind === "favorite" || e.kind === "open" || e.kind === "copy-name").map(menuItem),
6749
+ entries.some((e) => e.kind === "manage") ? /* @__PURE__ */ jsx28(DropdownMenuSeparator4, {}) : null,
6750
+ entries.filter((e) => e.kind === "manage").map(menuItem),
6694
6751
  hostEntries.length > 0 ? /* @__PURE__ */ jsx28(DropdownMenuSeparator4, {}) : null,
6695
6752
  entries.filter((e) => e.kind === "host").map(menuItem)
6696
6753
  ] })
@@ -6700,6 +6757,17 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6700
6757
  }
6701
6758
  );
6702
6759
  }
6760
+ function manageEntries(item, onAction) {
6761
+ if (!onAction) return [];
6762
+ const canManage = item.capabilities.canManage;
6763
+ const offManage = !canManage && manageUnavailableReason(item) !== null;
6764
+ const run = (action) => () => onAction(item, action);
6765
+ return [
6766
+ { id: "activity", kind: "manage", label: "Activity", onSelect: run("activity") },
6767
+ ...canManage || offManage ? [{ id: "share", kind: "manage", label: "Share", onSelect: run("share"), disabled: !canManage }] : [],
6768
+ ...canManage || offManage ? [{ id: "delete", kind: "manage", label: "Delete", onSelect: run("delete"), disabled: !canManage }] : []
6769
+ ];
6770
+ }
6703
6771
 
6704
6772
  // src/lists/TrashView.tsx
6705
6773
  import { useState as useState30 } from "react";
@@ -7907,7 +7975,7 @@ function Importer({ bridge, readLimits, activeOrganizationId, organizations, onC
7907
7975
 
7908
7976
  // src/extension/PageView.tsx
7909
7977
  import { useState as useState39 } from "react";
7910
- import { DropdownMenu as DropdownMenu5, DropdownMenuContent as DropdownMenuContent5, DropdownMenuItem as DropdownMenuItem5, DropdownMenuTrigger as DropdownMenuTrigger5, SiteIcon } from "@ai-matrx/design-system";
7978
+ import { SiteIcon } from "@ai-matrx/design-system";
7911
7979
  import { Button as Button33 } from "@ai-matrx/design-system/controls";
7912
7980
  import { GlobeIcon as GlobeIcon4 } from "@ai-matrx/design-system/icons";
7913
7981
 
@@ -8236,22 +8304,10 @@ function MatchRow({ itemId, item, tag, family, port, busy, running, onOpen, onFi
8236
8304
  ] })
8237
8305
  ] }),
8238
8306
  onFill ? /* @__PURE__ */ jsx38(Button33, { variant: "primary", disabled: busy, "aria-busy": running || void 0, onClick: onFill, "aria-label": `Fill ${item?.name ?? "this login"}`, children: running ? "Filling\u2026" : "Fill" }) : null,
8307
+ onSignIn ? /* @__PURE__ */ jsx38(Button33, { variant: onFill ? "outline" : "primary", icon: /* @__PURE__ */ jsx38(ShieldGlyph, {}), disabled: busy, onClick: onSignIn, "aria-label": `Sign me in with ${item?.name ?? "this login"}`, title: "Sign me in", children: "Sign me in" }) : null,
8239
8308
  user && item ? /* @__PURE__ */ jsx38(Button33, { variant: "quiet", icon: /* @__PURE__ */ jsx38(UserGlyph, {}), "aria-label": `Copy ${user.label}`, title: `Copy ${user.label}`, disabled: reading, onClick: () => void read(user.label, () => port.reveal(item, user.key)) }) : null,
8240
8309
  secret && item ? /* @__PURE__ */ jsx38(Button33, { variant: "quiet", icon: /* @__PURE__ */ jsx38(KeyGlyph, {}), "aria-label": "Copy password", title: "Copy password", disabled: reading, onClick: () => void read("Password", () => port.reveal(item, secret.key)) }) : null,
8241
- otp ? /* @__PURE__ */ jsx38(Button33, { variant: "quiet", icon: /* @__PURE__ */ jsx38(ClockGlyph, {}), "aria-label": "Copy one-time code", title: "Copy one-time code", disabled: reading, onClick: () => void read("One-time code", async () => (await port.authenticator.code(otp)).code) }) : null,
8242
- onSignIn ? /* @__PURE__ */ jsxs36(DropdownMenu5, { children: [
8243
- /* @__PURE__ */ jsx38(DropdownMenuTrigger5, { asChild: true, children: /* @__PURE__ */ jsx38(Button33, { variant: "quiet", icon: /* @__PURE__ */ jsx38(MoreGlyph, {}), "aria-label": "More", disabled: busy }) }),
8244
- /* @__PURE__ */ jsxs36(DropdownMenuContent5, { align: "end", children: [
8245
- /* @__PURE__ */ jsxs36(DropdownMenuItem5, { onSelect: onSignIn, children: [
8246
- /* @__PURE__ */ jsx38(ShieldGlyph, { className: "size-4" }),
8247
- "Sign me in"
8248
- ] }),
8249
- /* @__PURE__ */ jsxs36(DropdownMenuItem5, { onSelect: () => onOpen(itemId), children: [
8250
- /* @__PURE__ */ jsx38(ListGlyph, { className: "size-4" }),
8251
- "Open item"
8252
- ] })
8253
- ] })
8254
- ] }) : null
8310
+ otp ? /* @__PURE__ */ jsx38(Button33, { variant: "quiet", icon: /* @__PURE__ */ jsx38(ClockGlyph, {}), "aria-label": "Copy one-time code", title: "Copy one-time code", disabled: reading, onClick: () => void read("One-time code", async () => (await port.authenticator.code(otp)).code) }) : null
8255
8311
  ] });
8256
8312
  }
8257
8313
 
@@ -8363,7 +8419,7 @@ function Group({ title, children }) {
8363
8419
  children
8364
8420
  ] });
8365
8421
  }
8366
- function VaultSidebar({ vault, choices, onVault, view, allCount, deletedCount, views, hasFavorites, hasRecents, onView, onPanel, nativeProvider, hasNativeImport = false, hasPage = false }) {
8422
+ function VaultSidebar({ vault, choices, onVault, view, allCount, deletedCount, views, hasFavorites, hasRecents, onView, onPanel, nativeProvider, hasNativeImport = false, hasPage = false, onLock, onOpenWeb }) {
8367
8423
  const [tagsOpen, setTagsOpen] = useState40(view.startsWith("tag:"));
8368
8424
  return /* @__PURE__ */ jsxs37("nav", { "aria-label": "Vault", className: "flex h-full min-h-0 flex-col border-r border-sidebar-border bg-sidebar-background text-sidebar-foreground", children: [
8369
8425
  /* @__PURE__ */ jsxs37("div", { className: "flex items-center gap-2 px-3 pb-2 pt-3", children: [
@@ -8402,7 +8458,9 @@ function VaultSidebar({ vault, choices, onVault, view, allCount, deletedCount, v
8402
8458
  /* @__PURE__ */ jsx39(ShieldGlyph, { className: "size-4 shrink-0" }),
8403
8459
  /* @__PURE__ */ jsx39("span", { className: "min-w-0 flex-1 truncate", children: "System AutoFill" }),
8404
8460
  /* @__PURE__ */ jsx39("span", { className: cn15("text-xs", nativeProvider.status === "ready" ? "text-success" : "text-warning"), children: nativeProvider.status === "ready" ? "On" : "Set up" })
8405
- ] }) : null
8461
+ ] }) : null,
8462
+ onOpenWeb ? /* @__PURE__ */ jsx39(Entry, { on: false, glyph: ExternalGlyph, label: "Open on the web", onClick: onOpenWeb }) : null,
8463
+ onLock ? /* @__PURE__ */ jsx39(Entry, { on: false, glyph: LockGlyph, label: "Lock", onClick: onLock }) : null
8406
8464
  ] })
8407
8465
  ] });
8408
8466
  }
@@ -8445,12 +8503,42 @@ function ImportWithLimits(props) {
8445
8503
  if (state && "limits" in state) return /* @__PURE__ */ jsx40(ImportPanel, { ...rest, limits: state.limits });
8446
8504
  return /* @__PURE__ */ jsx40(Panel, { title: "Import", onClose: rest.onClose, children: /* @__PURE__ */ jsx40(Empty, { children: state ? state.error : "Reading this organization\u2019s import settings\u2026" }) });
8447
8505
  }
8448
- function VaultApp(props) {
8506
+ function VaultApp(given) {
8507
+ const props = { ...given, host: useOrganizationSettings(given.host ?? NO_HOST) };
8449
8508
  const lock = props.host?.lock;
8450
8509
  const locked = useLocked(lock);
8451
8510
  const port = useMemo6(() => lock ? guardPort(props.port, () => lock.locked()) : props.port, [props.port, lock]);
8452
8511
  return /* @__PURE__ */ jsx40(LookScope, { look: props.look ?? "web", className: "h-full min-h-0 w-full", children: /* @__PURE__ */ jsx40("div", { className: "relative h-full min-h-0 w-full", children: /* @__PURE__ */ jsx40(NoticeProvider, { notify: props.notify, children: lock && locked ? /* @__PURE__ */ jsx40(LockedScreen, { lock }) : /* @__PURE__ */ jsx40(ClipboardProvider, { clipboard: props.host?.clipboard, children: /* @__PURE__ */ jsx40(RevealSecondsProvider, { seconds: props.host?.revealSeconds, children: /* @__PURE__ */ jsx40(IdentityGate, { identity: props.host?.identity, children: /* @__PURE__ */ jsx40(GeneratorLimitsProvider, { resolve: generatorLimitsFrom(props.host?.knobs), children: /* @__PURE__ */ jsx40(Workspace, { ...props, port }) }) }) }) }) }) }) });
8453
8512
  }
8513
+ function useOrganizationSettings(host) {
8514
+ const knobs = host.knobs;
8515
+ const needs = knobs !== void 0 && (host.revealSeconds === void 0 || host.backupPassphraseMinLength === void 0);
8516
+ const [read, setRead] = useState41(null);
8517
+ useEffect23(() => {
8518
+ if (!needs || !knobs) return;
8519
+ let live = true;
8520
+ knobs().then(
8521
+ (r) => live && setRead({ reveal: positiveKnob(r, REVEAL_SECONDS_KNOB), backupMin: positiveKnob(r, BACKUP_PASSPHRASE_MIN_KNOB) }),
8522
+ () => void 0
8523
+ );
8524
+ return () => {
8525
+ live = false;
8526
+ };
8527
+ }, [needs, knobs]);
8528
+ if (!read) return host;
8529
+ return {
8530
+ ...host,
8531
+ ...host.revealSeconds === void 0 && read.reveal !== null ? { revealSeconds: read.reveal } : {},
8532
+ ...host.backupPassphraseMinLength === void 0 && read.backupMin !== null ? { backupPassphraseMinLength: read.backupMin } : {}
8533
+ };
8534
+ }
8535
+ var NO_STORE = () => () => void 0;
8536
+ var NOTHING = () => null;
8537
+ function useHostPrompts(capture, fillSetup) {
8538
+ const prompt = useSyncExternalStore5(capture ? capture.subscribe : NO_STORE, capture ? capture.get : NOTHING, capture ? capture.get : NOTHING);
8539
+ const setup = useSyncExternalStore5(fillSetup ? fillSetup.subscribe : NO_STORE, fillSetup ? fillSetup.get : NOTHING, fillSetup ? fillSetup.get : NOTHING);
8540
+ return prompt !== null || setup?.status === "off";
8541
+ }
8454
8542
  function LockedScreen({ lock }) {
8455
8543
  const [busy, setBusy] = useState41(false);
8456
8544
  const [error, setError] = useState41(null);
@@ -8538,6 +8626,16 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8538
8626
  document.addEventListener("keydown", onKey);
8539
8627
  return () => document.removeEventListener("keydown", onKey);
8540
8628
  }, []);
8629
+ const keepSearchFocus = useRef19(false);
8630
+ useEffect23(() => {
8631
+ if (!keepSearchFocus.current) return;
8632
+ keepSearchFocus.current = false;
8633
+ const search = rootRef.current?.querySelector('input[aria-label^="Search"]');
8634
+ if (!search || document.activeElement === search) return;
8635
+ search.focus();
8636
+ search.setSelectionRange(search.value.length, search.value.length);
8637
+ }, [loc.q]);
8638
+ const hasPrompts = useHostPrompts(host.capture, host.fillSetup);
8541
8639
  const knobsRef = useRef19(host.knobs);
8542
8640
  knobsRef.current = host.knobs;
8543
8641
  const [signedOut, setSignedOut] = useState41(false);
@@ -8680,6 +8778,25 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8680
8778
  notify(e instanceof Error ? e.message : "Favorites could not be saved.", "error");
8681
8779
  });
8682
8780
  };
8781
+ const deletedItem = (id) => {
8782
+ setItems((cur) => cur.filter((i) => i.id !== id));
8783
+ dirty.current = false;
8784
+ if (loc.itemId === id) move({ ...loc, itemId: null, mode: "view", panel: null }, "push");
8785
+ notify("Moved to Trash");
8786
+ load();
8787
+ };
8788
+ const rowAction = (item, action) => {
8789
+ if (action !== "delete") return go({ view: loc.view === "page" ? "all" : loc.view, itemId: item.id, mode: "view", panel: action });
8790
+ void confirmDelete(item).then(async (ok) => {
8791
+ if (!ok) return;
8792
+ try {
8793
+ await port.remove(item);
8794
+ deletedItem(item.id);
8795
+ } catch (e) {
8796
+ notify(e instanceof Error && e.message ? e.message : "Deleting failed.", "error");
8797
+ }
8798
+ });
8799
+ };
8683
8800
  if (error) {
8684
8801
  return /* @__PURE__ */ jsxs38("div", { ref: rootRef, role: "alert", className: "flex h-full flex-col items-center justify-center gap-3 bg-background p-6 text-center text-sm", children: [
8685
8802
  /* @__PURE__ */ jsx40(SafeGlyph, { className: "size-8 text-muted-foreground" }),
@@ -8718,38 +8835,38 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8718
8835
  onClick: () => go({ view: loc.view === "authenticator" ? "all" : "authenticator", itemId: null, mode: "view", panel: null })
8719
8836
  }
8720
8837
  ),
8721
- /* @__PURE__ */ jsxs38(DropdownMenu6, { children: [
8722
- /* @__PURE__ */ jsx40(DropdownMenuTrigger6, { asChild: true, children: /* @__PURE__ */ jsx40(Button34, { variant: "quiet", icon: /* @__PURE__ */ jsx40(MoreGlyph, {}), "aria-label": "Vault actions" }) }),
8723
- /* @__PURE__ */ jsxs38(DropdownMenuContent6, { align: "end", children: [
8724
- /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "import" }), children: [
8838
+ /* @__PURE__ */ jsxs38(DropdownMenu5, { children: [
8839
+ /* @__PURE__ */ jsx40(DropdownMenuTrigger5, { asChild: true, children: /* @__PURE__ */ jsx40(Button34, { variant: "quiet", icon: /* @__PURE__ */ jsx40(MoreGlyph, {}), "aria-label": "Vault actions" }) }),
8840
+ /* @__PURE__ */ jsxs38(DropdownMenuContent5, { align: "end", children: [
8841
+ /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: () => go({ panel: "import" }), children: [
8725
8842
  /* @__PURE__ */ jsx40(ImportGlyph, { className: "size-4" }),
8726
8843
  "Import"
8727
8844
  ] }),
8728
- /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "export" }), children: [
8845
+ /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: () => go({ panel: "export" }), children: [
8729
8846
  /* @__PURE__ */ jsx40(ExportGlyph, { className: "size-4" }),
8730
8847
  "Export"
8731
8848
  ] }),
8732
- /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "backup" }), children: [
8849
+ /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: () => go({ panel: "backup" }), children: [
8733
8850
  /* @__PURE__ */ jsx40(SafeGlyph, { className: "size-4" }),
8734
8851
  "Backup"
8735
8852
  ] }),
8736
- /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "devices" }), children: [
8853
+ /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: () => go({ panel: "devices" }), children: [
8737
8854
  /* @__PURE__ */ jsx40(ShieldGlyph, { className: "size-4" }),
8738
8855
  "Browsers that fill"
8739
8856
  ] }),
8740
- host.nativeImport ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "import-passkeys" }), children: [
8857
+ host.nativeImport ? /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: () => go({ panel: "import-passkeys" }), children: [
8741
8858
  /* @__PURE__ */ jsx40(ImportGlyph, { className: "size-4" }),
8742
8859
  "Import passkeys"
8743
8860
  ] }) : null,
8744
- host.nativeProvider && host.nativeProvider.status !== "unavailable" ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: host.nativeProvider.openSetup, children: [
8861
+ host.nativeProvider && host.nativeProvider.status !== "unavailable" ? /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: host.nativeProvider.openSetup, children: [
8745
8862
  /* @__PURE__ */ jsx40(ShieldGlyph, { className: "size-4" }),
8746
8863
  "System AutoFill"
8747
8864
  ] }) : null,
8748
- host.lock?.lock ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => void host.lock?.lock?.(), children: [
8865
+ host.lock?.lock ? /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: () => void host.lock?.lock?.(), children: [
8749
8866
  /* @__PURE__ */ jsx40(LockGlyph, { className: "size-4" }),
8750
8867
  "Lock"
8751
8868
  ] }) : null,
8752
- host.openWebVault ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: host.openWebVault, children: [
8869
+ host.openWebVault ? /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: host.openWebVault, children: [
8753
8870
  /* @__PURE__ */ jsx40(ExternalGlyph, { className: "size-4" }),
8754
8871
  "Open on the web"
8755
8872
  ] }) : null
@@ -8763,6 +8880,27 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8763
8880
  compactActions
8764
8881
  ] })
8765
8882
  ] });
8883
+ const newItem = () => go({ itemId: null, mode: "new", kind: null, panel: null });
8884
+ const pageHeader = /* @__PURE__ */ jsxs38("div", { className: "flex flex-col gap-2", "data-vault-page-header": "", children: [
8885
+ compactHeader,
8886
+ /* @__PURE__ */ jsxs38("div", { className: "flex items-center gap-2", children: [
8887
+ /* @__PURE__ */ jsx40(
8888
+ SearchField4,
8889
+ {
8890
+ value: loc.q,
8891
+ onChange: (e) => {
8892
+ keepSearchFocus.current = true;
8893
+ go({ q: e.target.value }, "replace");
8894
+ },
8895
+ placeholder: "Search the vault",
8896
+ "aria-label": "Search the vault",
8897
+ className: "min-w-0 flex-1"
8898
+ }
8899
+ ),
8900
+ compactActions,
8901
+ /* @__PURE__ */ jsx40(Button34, { variant: "primary", icon: /* @__PURE__ */ jsx40(PlusGlyph, {}), "aria-label": "New item", onClick: newItem })
8902
+ ] })
8903
+ ] });
8766
8904
  const viewProps = { port, host, items: inThisVault, deleted, definitions, query: loc.q, onOpen: (id) => go({ view: "all", itemId: id, mode: "view", panel: null }), onChanged: load, header: viewHeader };
8767
8905
  const pageView = loc.view === "page" && host.page && !loc.q ? /* @__PURE__ */ jsx40(
8768
8906
  PageView,
@@ -8770,8 +8908,6 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8770
8908
  port,
8771
8909
  page: host.page,
8772
8910
  fill: host.fill,
8773
- capture: host.capture,
8774
- fillSetup: host.fillSetup,
8775
8911
  newPasswordFields: host.newPasswordFields,
8776
8912
  items: inThisVault,
8777
8913
  definitions,
@@ -8779,10 +8915,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8779
8915
  onNewForPage: (url) => go({ mode: "new", kind: null, url, itemId: null, panel: null }),
8780
8916
  onBrowse: () => go({ view: "all", itemId: null }),
8781
8917
  onChanged: load,
8782
- ...compactHeader ? { header: /* @__PURE__ */ jsxs38("div", { className: "flex flex-col gap-2", children: [
8783
- compactHeader,
8784
- /* @__PURE__ */ jsx40("div", { className: "flex justify-end", children: compactActions })
8785
- ] }) } : {}
8918
+ header: pageHeader
8786
8919
  }
8787
8920
  ) : null;
8788
8921
  const specialView = pageView ?? (loc.view === "authenticator" ? /* @__PURE__ */ jsx40(AuthenticatorView, { ...viewProps }) : loc.view === "trash" ? /* @__PURE__ */ jsx40(TrashView, { ...viewProps }) : null);
@@ -8797,11 +8930,15 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8797
8930
  selectedId: loc.itemId,
8798
8931
  onSelect: (id) => go({ itemId: id, mode: "view", kind: null, panel: null }),
8799
8932
  query: loc.q,
8800
- onQuery: (q) => go({ q }, "replace"),
8933
+ onQuery: (q) => {
8934
+ if (loc.view === "page" && host.page && !q) keepSearchFocus.current = true;
8935
+ go({ q }, "replace");
8936
+ },
8801
8937
  sort: loc.sort,
8802
8938
  onSort: (sort) => go({ sort }, "replace"),
8803
8939
  canSortViewed: port.lists.recents !== void 0,
8804
- onNew: () => go({ itemId: null, mode: "new", kind: null, panel: null }),
8940
+ onNew: newItem,
8941
+ onItemAction: rowAction,
8805
8942
  onRefresh: refresh,
8806
8943
  refreshing,
8807
8944
  ...snap.pending && snap.pending.length > 0 ? { stillLoading: snap.pending } : {},
@@ -8876,13 +9013,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8876
9013
  setNotice(n);
8877
9014
  notify("Saved");
8878
9015
  },
8879
- onDeleted: (id) => {
8880
- setItems((cur) => cur.filter((i) => i.id !== id));
8881
- dirty.current = false;
8882
- move({ ...loc, itemId: null, mode: "view", panel: null }, "push");
8883
- notify("Moved to Trash");
8884
- load();
8885
- },
9016
+ onDeleted: deletedItem,
8886
9017
  onDirty: (d) => {
8887
9018
  dirty.current = d;
8888
9019
  },
@@ -8943,6 +9074,13 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8943
9074
  /* @__PURE__ */ jsx40("span", { className: "min-w-0 flex-1 truncate text-warning", title: refreshError, children: "Couldn\u2019t refresh. Showing what was loaded." }),
8944
9075
  /* @__PURE__ */ jsx40("button", { type: "button", className: "shrink-0 text-primary hover:underline", onClick: refresh, children: "Try again" })
8945
9076
  ] }) : null,
9077
+ hasPrompts ? (
9078
+ // "Save this login?" and "Turn on filling" show above the list in every view, not only "This page".
9079
+ /* @__PURE__ */ jsxs38("div", { className: "flex flex-col gap-3 border-b border-border px-3 py-3", "data-vault-host-prompts": "", children: [
9080
+ host.fillSetup ? /* @__PURE__ */ jsx40(FillSetupCard, { fillSetup: host.fillSetup }) : null,
9081
+ host.capture ? /* @__PURE__ */ jsx40(CaptureCard, { capture: host.capture, onChanged: load }) : null
9082
+ ] })
9083
+ ) : null,
8946
9084
  snap.unavailable.length > 0 ? /* @__PURE__ */ jsxs38("div", { role: "status", className: "flex items-center gap-3 border-b border-border bg-muted px-4 py-2 text-sm", children: [
8947
9085
  /* @__PURE__ */ jsxs38("span", { className: "min-w-0 flex-1 truncate text-warning", children: [
8948
9086
  "Couldn\u2019t load ",
@@ -8968,7 +9106,9 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8968
9106
  onPanel: (p) => go({ panel: p }),
8969
9107
  nativeProvider: host.nativeProvider,
8970
9108
  hasNativeImport: host.nativeImport !== void 0,
8971
- hasPage: host.page !== void 0
9109
+ hasPage: host.page !== void 0,
9110
+ onLock: host.lock?.lock ? () => void host.lock?.lock?.() : void 0,
9111
+ onOpenWeb: host.openWebVault
8972
9112
  }
8973
9113
  ) : null,
8974
9114
  shown.list ? /* @__PURE__ */ jsx40("div", { className: specialView && mode !== "stack" ? "col-span-2 min-h-0" : "min-h-0", children: list }) : null,
@@ -9941,20 +10081,6 @@ function createApiVaultPort(hostApi, options = {}) {
9941
10081
  };
9942
10082
  }
9943
10083
 
9944
- // src/knobs.ts
9945
- import { createKnobReader } from "@ai-matrx/data/knobs";
9946
- var FRESH_MS = 6e4;
9947
- function createVaultKnobs(client, address) {
9948
- const reader = createKnobReader(client);
9949
- let kept = null;
9950
- return async () => {
9951
- if (kept && Date.now() - kept.at < FRESH_MS) return kept.resolved;
9952
- const snap = await reader.snapshot(address);
9953
- kept = { at: Date.now(), resolved: snap.resolved };
9954
- return snap.resolved;
9955
- };
9956
- }
9957
-
9958
10084
  // src/agent-context.ts
9959
10085
  var hostOf = (url) => {
9960
10086
  try {
@@ -10022,6 +10148,7 @@ function emptyReadAlarm(total, emptyIds, holdings) {
10022
10148
  return `${staleAlarm(total)} (The check that tells an empty credential from an unreadable one did not answer either: ${holdings.why})`;
10023
10149
  }
10024
10150
  export {
10151
+ BACKUP_PASSPHRASE_MIN_KNOB,
10025
10152
  CATEGORIES,
10026
10153
  CONFIRM_RETURN_KEY,
10027
10154
  DEFAULT_LOCATION,
@@ -10030,9 +10157,11 @@ export {
10030
10157
  HOLDINGS_DOOR_MIGRATION,
10031
10158
  IdentityGate,
10032
10159
  LOCKED_MESSAGE,
10160
+ REVEAL_SECONDS_KNOB,
10033
10161
  VaultApp,
10034
10162
  VaultLockedError,
10035
10163
  categoryOf,
10164
+ copiedLine,
10036
10165
  copyValue,
10037
10166
  createApiVaultPort,
10038
10167
  createSupabaseIdentity,
@@ -10049,11 +10178,13 @@ export {
10049
10178
  matchesQuery,
10050
10179
  needsRecentSignIn,
10051
10180
  parseDefinitions,
10181
+ positiveKnob,
10052
10182
  providerOf,
10053
10183
  rowMenuEntriesAt,
10054
10184
  siteDomainOf,
10055
10185
  sortItems,
10056
10186
  useConfirmed,
10187
+ useDisposable,
10057
10188
  vaultInventoryScope,
10058
10189
  vaultItemFacts,
10059
10190
  wasCancelled