@ai-matrx/vault 0.1.27 → 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";
@@ -2637,9 +2653,12 @@ function normalizeLoginUrl(input) {
2637
2653
  return null;
2638
2654
  }
2639
2655
  }
2656
+ function normalizeLoginUrls(inputs) {
2657
+ return inputs.map((u) => u.trim()).filter(Boolean).map((u) => normalizeLoginUrl(u) ?? u);
2658
+ }
2640
2659
  function describeDestination(r) {
2641
2660
  if (r.state === "reachable") return { url: r.normalizedUrl, state: r.state, tone: "success", message: "Website confirmed" };
2642
- if (r.state === "invalid") return { url: r.normalizedUrl, state: r.state, tone: "destructive", message: "Not a public website" };
2661
+ if (r.state === "invalid") return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Not a public website. You can still save it." };
2643
2662
  if (r.state === "not_found") return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Site answered, but this page was not found" };
2644
2663
  return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Couldn\u2019t confirm it. You can still save it." };
2645
2664
  }
@@ -3123,6 +3142,27 @@ function importLimitsFrom(resolved) {
3123
3142
  return out;
3124
3143
  }
3125
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
+
3126
3166
  // src/lock.ts
3127
3167
  import { useSyncExternalStore as useSyncExternalStore2 } from "react";
3128
3168
  var LOCKED_MESSAGE = "The vault is locked. Unlock it first.";
@@ -3318,7 +3358,8 @@ function Omissions({ omissions, accepted, onAccepted, verb }) {
3318
3358
  /* @__PURE__ */ jsx17(VaultRow, { label: "Accept", hint: `${verb} without these`, children: /* @__PURE__ */ jsx17(Switch3, { checked: accepted, onCheckedChange: onAccepted, "aria-label": `${verb} without these` }) })
3319
3359
  ] });
3320
3360
  }
3321
- function BackupPanel({ port, items, onClose, onChanged }) {
3361
+ function BackupPanel({ port, host, items, onClose, onChanged }) {
3362
+ const minLength = passphraseMinimum(host.backupPassphraseMinLength);
3322
3363
  const confirmed = useConfirmed();
3323
3364
  const fileInput = useRef11(null);
3324
3365
  const abort = useRef11(null);
@@ -3373,7 +3414,7 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3373
3414
  setAccepted(false);
3374
3415
  });
3375
3416
  const downloadBackup = () => guarded(async () => {
3376
- if (!backup) return;
3417
+ if (!backup || tooShort(passphrase, minLength)) return;
3377
3418
  const bytes2 = await confirmed(() => port.io.downloadBackup({ itemIds: [...selected], revision: backup.revision, passphrase, acceptOmissions: accepted }));
3378
3419
  downloadFile(BACKUP_FILENAME, bytes2, "application/octet-stream");
3379
3420
  setPassphrase("");
@@ -3420,10 +3461,11 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3420
3461
  };
3421
3462
  const retryable = (result?.retryable_failure ?? 0) > 0;
3422
3463
  const mismatch = repeat !== "" && repeat !== passphrase;
3464
+ const short = passphrase !== "" && tooShort(passphrase, minLength);
3423
3465
  const names = new Map(restorePreview?.records.map((r) => [r.source_item_id, r.display_name]) ?? []);
3424
3466
  const footer = mode === "backup" ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
3425
3467
  /* @__PURE__ */ jsx17(Button13, { variant: "quiet", onClick: onClose, children: "Close" }),
3426
- 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: [
3427
3469
  "Review ",
3428
3470
  selected.size
3429
3471
  ] })
@@ -3470,7 +3512,14 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3470
3512
  /* @__PURE__ */ jsx17(Summary, { value: backup }),
3471
3513
  /* @__PURE__ */ jsx17(Omissions, { omissions: backup.omissions, accepted, onAccepted: setAccepted, verb: "Back up" }),
3472
3514
  /* @__PURE__ */ jsxs16(VaultSection, { children: [
3473
- /* @__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,
3474
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) }) })
3475
3524
  ] })
3476
3525
  ] }) : null
@@ -3503,6 +3552,12 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3503
3552
  /* @__PURE__ */ jsx17(ErrorLine, { message: error })
3504
3553
  ] });
3505
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
+ }
3506
3561
 
3507
3562
  // src/io/ExportPanel.tsx
3508
3563
  import { useMemo, useState as useState18 } from "react";
@@ -5345,6 +5400,40 @@ function AssignResult({ sent, onAnother, onDone }) {
5345
5400
  ] });
5346
5401
  }
5347
5402
 
5403
+ // src/lists/turn-back-on.ts
5404
+ async function turnBackOn(port, item, fields, options = {}) {
5405
+ const run = options.run ?? ((op) => op());
5406
+ const done = [];
5407
+ if (item.turnedOff) {
5408
+ await run(() => port.fields.updateItem(item, { active: true }));
5409
+ done.push("the item");
5410
+ }
5411
+ let n = 0;
5412
+ for (const f of fields) {
5413
+ if (f.active) continue;
5414
+ await run(() => port.fields.updateField(item, f.id, { active: true }));
5415
+ n += 1;
5416
+ }
5417
+ if (n) done.push(n === 1 ? "1 field" : `${n} fields`);
5418
+ if (fields.some((f) => f.key === OTP_SEED_KEY)) {
5419
+ const codes = await port.authenticator.get(item);
5420
+ if (codes && !codes.enabled) {
5421
+ await run(() => port.authenticator.setEnabled(item, true));
5422
+ done.push("one-time codes");
5423
+ }
5424
+ }
5425
+ if (options.browserFill && !item.browserFillEnabled && item.loginUrls.length > 0) {
5426
+ await run(() => port.fields.updateItem(item, { browserFillEnabled: true }));
5427
+ done.push("browser fill");
5428
+ }
5429
+ return done;
5430
+ }
5431
+ function turnedOnSentence(done) {
5432
+ if (done.length === 0) return "Nothing here was off.";
5433
+ const list = done.length === 1 ? done[0] : `${done.slice(0, -1).join(", ")} and ${done[done.length - 1]}`;
5434
+ return `Turned on ${list}.`;
5435
+ }
5436
+
5348
5437
  // src/ItemPane.tsx
5349
5438
  import { HistoryIcon, SettingsIcon, UsersIcon as UsersIcon2 } from "@ai-matrx/design-system/icons";
5350
5439
  import { dropKeptDraft, keepDraft, readKeptDraft } from "@ai-matrx/kit/drafts";
@@ -5926,6 +6015,9 @@ import { Fragment as Fragment18, jsx as jsx27, jsxs as jsxs25 } from "react/jsx-
5926
6015
  var ENV_VALUE_KEY = "env_value";
5927
6016
  var secureNoteDraft = () => ({ key: SECURE_NOTE_KEY, label: "Secure note", value: "", multiline: true, description: "Encrypted notes" });
5928
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
+ }
5929
6021
  function ItemPane(props) {
5930
6022
  const { port, item, definition, mode, host, organizations, defaultOwner, seed, onBack, onMode, onSaved, onDeleted, onDirty, onCancelNew, onPanel, onChanged, favorite, notice, refreshKey } = props;
5931
6023
  const confirmed = useConfirmed();
@@ -5958,7 +6050,8 @@ function ItemPane(props) {
5958
6050
  const [showBrowser, setShowBrowser] = useState27(false);
5959
6051
  const [error, setError] = useState27(null);
5960
6052
  const [destinations, setDestinations] = useState27({});
5961
- const dirty = editing && isDirty(base, draft);
6053
+ const [savedNew, setSavedNew] = useState27(false);
6054
+ const dirty = editing && !savedNew && isDirty(base, draft);
5962
6055
  const declared = definition?.fields ?? [];
5963
6056
  const prev = useRef14(null);
5964
6057
  const paneRef = useRef14(null);
@@ -5981,8 +6074,11 @@ function ItemPane(props) {
5981
6074
  setShowTags(false);
5982
6075
  setShowDescription(false);
5983
6076
  setShowRecovery(false);
6077
+ setSavedNew(false);
5984
6078
  }, [item?.id, mode]);
5985
- useEffect16(() => onDirty(dirty), [dirty, onDirty]);
6079
+ const onDirtyRef = useRef14(onDirty);
6080
+ onDirtyRef.current = onDirty;
6081
+ useEffect16(() => onDirtyRef.current(dirty), [dirty]);
5986
6082
  const keepKey = draftKey(item?.id ?? null, definition?.key ?? item?.definitionKey ?? null);
5987
6083
  useEffect16(() => {
5988
6084
  if (editing && dirty) keepDraft(keepKey, keptFieldsOf(draft), { basedOn: item?.updatedAt ?? "new" });
@@ -6039,6 +6135,8 @@ function ItemPane(props) {
6039
6135
  const [withCode, n0, enrolled] = await withOtp(r.item, r.notice);
6040
6136
  const [next, n] = await storeQueuedFiles(withCode, n0);
6041
6137
  dropKeptDraft(keepKey);
6138
+ setSavedNew(true);
6139
+ onDirty(false);
6042
6140
  onSaved(next, true, n, enrolled);
6043
6141
  }
6044
6142
  } catch (e) {
@@ -6072,8 +6170,7 @@ function ItemPane(props) {
6072
6170
  }
6073
6171
  async function remove() {
6074
6172
  if (!item) return;
6075
- 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" });
6076
- if (!ok) return;
6173
+ if (!await confirmDelete(item)) return;
6077
6174
  setBusy(true);
6078
6175
  try {
6079
6176
  await port.remove(item);
@@ -6101,12 +6198,7 @@ function ItemPane(props) {
6101
6198
  if (!item) return;
6102
6199
  setError(null);
6103
6200
  try {
6104
- if (item.turnedOff) await confirmed(() => port.fields.updateItem(item, { active: true }));
6105
- for (const f of fields) await confirmed(() => port.fields.updateField(item, f.id, { active: true }));
6106
- if (fields.some((f) => f.key === OTP_SEED_KEY)) {
6107
- const codes = await port.authenticator.get(item);
6108
- if (codes && !codes.enabled) await confirmed(() => port.authenticator.setEnabled(item, true));
6109
- }
6201
+ await turnBackOn(port, item, fields, { run: confirmed });
6110
6202
  onChanged();
6111
6203
  } catch (e) {
6112
6204
  if (!wasCancelled(e)) setError(e instanceof Error ? e.message : "It could not be turned on.");
@@ -6161,7 +6253,7 @@ function ItemPane(props) {
6161
6253
  "Unsaved"
6162
6254
  ] }) : null,
6163
6255
  /* @__PURE__ */ jsx27(Button22, { variant: "quiet", disabled: busy, onClick: () => mode === "new" ? onCancelNew() : onMode("view"), children: dirty ? "Discard" : "Cancel" }),
6164
- /* @__PURE__ */ jsx27(Button22, { variant: "primary", disabled: busy || sent !== null || draft.name.trim() === "" || mode === "edit" && !dirty || mode === "new" && owner === "person" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(recipient.trim()), onClick: () => void save(), children: mode === "new" && owner === "person" ? "Send" : "Save" })
6256
+ /* @__PURE__ */ jsx27(Button22, { variant: "primary", disabled: busy || savedNew || sent !== null || draft.name.trim() === "" || mode === "edit" && !dirty || mode === "new" && owner === "person" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(recipient.trim()), onClick: () => void save(), children: mode === "new" && owner === "person" ? "Send" : "Save" })
6165
6257
  ] }) : item ? /* @__PURE__ */ jsxs25("div", { className: "flex shrink-0 items-center gap-1", children: [
6166
6258
  host.page && canUseOnSite(item, host.page.url) ? /* @__PURE__ */ jsx27(UseOnSiteButton, { port, item, pageUrl: host.page.url, onChanged: (next) => onChanged(next) }) : null,
6167
6259
  host.fill && item.capabilities.canUse ? /* @__PURE__ */ jsx27(
@@ -6329,7 +6421,8 @@ function ItemPane(props) {
6329
6421
  else delete next[i];
6330
6422
  return next;
6331
6423
  });
6332
- if (r?.url && r.url !== u) setDraft((d) => ({ ...d, loginUrls: d.loginUrls.map((x, j) => j === i && x === u ? r.url : x) }));
6424
+ const stored = r?.url || normalizeLoginUrl(u);
6425
+ if (stored && stored !== u) setDraft((d) => ({ ...d, loginUrls: d.loginUrls.map((x, j) => j === i && x === u ? stored : x) }));
6333
6426
  }),
6334
6427
  onRemove: () => patch({ loginUrls: draft.loginUrls.filter((_, j) => j !== i) }),
6335
6428
  removeLabel: "Remove website"
@@ -6488,7 +6581,7 @@ function subtitleOf(item, withOwner) {
6488
6581
  const what = loginSiteOf(item) || item.definitionLabel;
6489
6582
  return withOwner ? `${what} \xB7 ${item.owner.label}` : what;
6490
6583
  }
6491
- 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 }) {
6492
6585
  const sorts = SORTS2.filter((s) => canSortViewed || s.value !== "viewed");
6493
6586
  const pinned = matchingIds ? items.filter((i) => matchingIds.has(i.id)) : [];
6494
6587
  const rest = matchingIds ? items.filter((i) => !matchingIds.has(i.id)) : items;
@@ -6509,7 +6602,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6509
6602
  }
6510
6603
  };
6511
6604
  const mixedOwners = showOwner ?? new Set(items.map((i) => i.owner.organizationId ?? i.owner.kind)).size > 1;
6512
- 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);
6513
6606
  const sortLabel = sorts.find((s) => s.value === sort)?.label ?? "Name A\u2013Z";
6514
6607
  return /* @__PURE__ */ jsxs26("section", { "aria-label": title, className: "flex h-full min-h-0 flex-col border-r border-border bg-background", children: [
6515
6608
  /* @__PURE__ */ jsxs26("div", { className: "flex flex-col gap-2 px-3 pb-2 pt-3", children: [
@@ -6553,7 +6646,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6553
6646
  ] }) })
6554
6647
  ] });
6555
6648
  }
6556
- function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu, extra, favorite }) {
6649
+ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu, extra, onAction, favorite }) {
6557
6650
  const confirmed = useConfirmed();
6558
6651
  const copy = useCopy();
6559
6652
  const notify = useNotify();
@@ -6594,11 +6687,12 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6594
6687
  ...favoriteEntry ? [favoriteEntry] : [],
6595
6688
  { id: "open", kind: "open", label: "Open", onSelect: () => onSelect(item.id) },
6596
6689
  { id: "copy-name", kind: "copy-name", label: "Copy name", onSelect: copyName },
6690
+ ...manageEntries(item, onAction),
6597
6691
  ...hostEntries.map((h) => ({ ...h, kind: h.kind ?? "host" }))
6598
6692
  ];
6599
6693
  const entriesRef = useRef15(entries);
6600
6694
  entriesRef.current = entries;
6601
- 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" });
6602
6696
  const menuItem = (e) => /* @__PURE__ */ jsxs26(DropdownMenuItem4, { disabled: e.disabled ?? false, onSelect: e.onSelect, children: [
6603
6697
  glyphOf(e),
6604
6698
  e.label
@@ -6652,6 +6746,8 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6652
6746
  copyEntries.map(menuItem),
6653
6747
  copyEntries.length > 0 ? /* @__PURE__ */ jsx28(DropdownMenuSeparator4, {}) : null,
6654
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),
6655
6751
  hostEntries.length > 0 ? /* @__PURE__ */ jsx28(DropdownMenuSeparator4, {}) : null,
6656
6752
  entries.filter((e) => e.kind === "host").map(menuItem)
6657
6753
  ] })
@@ -6661,6 +6757,17 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6661
6757
  }
6662
6758
  );
6663
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
+ }
6664
6771
 
6665
6772
  // src/lists/TrashView.tsx
6666
6773
  import { useState as useState30 } from "react";
@@ -6741,8 +6848,16 @@ import { useEffect as useEffect18, useState as useState29 } from "react";
6741
6848
  import { Button as Button24 } from "@ai-matrx/design-system/controls";
6742
6849
  import { Fragment as Fragment19, jsx as jsx29, jsxs as jsxs27 } from "react/jsx-runtime";
6743
6850
  var count = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
6744
- function RestorePanel({ lists, item, restore, onClose }) {
6851
+ function RestorePanel({
6852
+ lists,
6853
+ item,
6854
+ restore,
6855
+ turnOn,
6856
+ onClose
6857
+ }) {
6745
6858
  const [preview, setPreview] = useState29(null);
6859
+ const [restored, setRestored] = useState29(null);
6860
+ const [turnedOn, setTurnedOn] = useState29(null);
6746
6861
  const [error, setError] = useState29(null);
6747
6862
  const [busy, setBusy] = useState29(false);
6748
6863
  const [loading, setLoading] = useState29(Boolean(lists.preview));
@@ -6771,13 +6886,54 @@ function RestorePanel({ lists, item, restore, onClose }) {
6771
6886
  setError(null);
6772
6887
  try {
6773
6888
  const result = await restore(preview?.supported ? preview.deletionId : void 0);
6774
- if (result) onClose();
6889
+ if (!result) return;
6890
+ if (turnOn && !result.alreadyRestored && preview?.supported && !preview.priorWasDisabled) setRestored(result);
6891
+ else onClose();
6775
6892
  } catch (e) {
6776
6893
  setError(e instanceof Error && e.message ? e.message : "Restore did not complete. Try again.");
6777
6894
  } finally {
6778
6895
  setBusy(false);
6779
6896
  }
6780
6897
  };
6898
+ const runTurnOn = async () => {
6899
+ if (!restored || !turnOn) return;
6900
+ setBusy(true);
6901
+ setError(null);
6902
+ try {
6903
+ const said = await turnOn(restored.item);
6904
+ if (said !== null) setTurnedOn(said);
6905
+ } catch (e) {
6906
+ setError(e instanceof Error && e.message ? e.message : "It could not be turned on. Turn it on from the item.");
6907
+ } finally {
6908
+ setBusy(false);
6909
+ }
6910
+ };
6911
+ if (restored) {
6912
+ return /* @__PURE__ */ jsxs27(
6913
+ Panel,
6914
+ {
6915
+ title: `Restored "${item.name}"`,
6916
+ size: "md",
6917
+ onClose,
6918
+ footer: turnedOn ? /* @__PURE__ */ jsx29(Button24, { variant: "primary", onClick: onClose, children: "Done" }) : /* @__PURE__ */ jsxs27(Fragment19, { children: [
6919
+ /* @__PURE__ */ jsx29(Button24, { variant: "quiet", onClick: onClose, children: "Leave off" }),
6920
+ /* @__PURE__ */ jsx29(Button24, { variant: "primary", icon: /* @__PURE__ */ jsx29(RestoreGlyph, {}), disabled: busy, onClick: () => void runTurnOn(), children: busy ? "Turning on\u2026" : "Turn these back on" })
6921
+ ] }),
6922
+ children: [
6923
+ /* @__PURE__ */ jsxs27(VaultSection, { title: turnedOn ? "Turned on" : "Back, turned off", children: [
6924
+ /* @__PURE__ */ jsx29(VaultRow, { label: "Came back", children: /* @__PURE__ */ jsxs27("span", { className: "text-foreground", children: [
6925
+ count(restored.restoredFields, "field"),
6926
+ ", ",
6927
+ count(restored.restoredAttachments, "file")
6928
+ ] }) }),
6929
+ turnedOn ? /* @__PURE__ */ jsx29(VaultRow, { label: "Now on", children: /* @__PURE__ */ jsx29("span", { role: "status", className: "text-foreground", children: turnedOn }) }) : /* @__PURE__ */ jsx29(VaultRow, { label: "Turns on", hint: "One press, nothing else changes", children: /* @__PURE__ */ jsx29("span", { className: "text-muted-foreground", children: "The item, its fields, codes and browser fill" }) }),
6930
+ /* @__PURE__ */ jsx29(VaultRow, { label: "Stays off", hint: "Choose these again yourself", children: /* @__PURE__ */ jsx29("span", { className: "text-muted-foreground", children: "Sharing, sandbox use" }) })
6931
+ ] }),
6932
+ error ? /* @__PURE__ */ jsx29("p", { role: "alert", className: "px-1 text-sm text-destructive", children: error }) : null
6933
+ ]
6934
+ }
6935
+ );
6936
+ }
6781
6937
  return /* @__PURE__ */ jsxs27(
6782
6938
  Panel,
6783
6939
  {
@@ -6818,7 +6974,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6818
6974
  setMessage(null);
6819
6975
  try {
6820
6976
  const r = await confirmed(() => port.lists.restore(item, deletionId));
6821
- setMessage({ tone: "ok", text: r.alreadyRestored ? `"${item.name}" was already restored.` : `Restored "${item.name}" with ${r.restoredFields} fields and ${r.restoredAttachments} files. Turn its fields on from the item.` });
6977
+ setMessage({ tone: "ok", text: r.alreadyRestored ? `"${item.name}" was already restored.` : `Restored "${item.name}" with ${r.restoredFields} fields and ${r.restoredAttachments} files, turned off.` });
6822
6978
  onChanged();
6823
6979
  return r;
6824
6980
  } catch (e) {
@@ -6828,6 +6984,19 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6828
6984
  setBusyId(null);
6829
6985
  }
6830
6986
  };
6987
+ const turnOn = async (item) => {
6988
+ try {
6989
+ const done = await turnBackOn(port, item, item.fields, { browserFill: true, run: confirmed });
6990
+ const said = turnedOnSentence(done);
6991
+ setMessage({ tone: "ok", text: `"${item.name}": ${said}` });
6992
+ onChanged();
6993
+ return said;
6994
+ } catch (e) {
6995
+ onChanged();
6996
+ if (wasCancelled(e)) return null;
6997
+ throw e;
6998
+ }
6999
+ };
6831
7000
  return /* @__PURE__ */ jsxs28("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3", children: [
6832
7001
  header,
6833
7002
  /* @__PURE__ */ jsx30(VaultSection, { title: "Trash", children: shown.length === 0 ? /* @__PURE__ */ jsx30(Empty, { children: deleted.length === 0 ? "The trash is empty." : "Nothing matches." }) : shown.map((item) => (
@@ -6841,7 +7010,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6841
7010
  /* @__PURE__ */ jsx30(Button25, { variant: "outline", icon: /* @__PURE__ */ jsx30(RestoreGlyph, {}), disabled: busyId !== null || !item.deletion, onClick: () => setRestoring(item), children: "Restore" })
6842
7011
  ] }, item.id)
6843
7012
  )) }),
6844
- restoring ? /* @__PURE__ */ jsx30(RestorePanel, { lists: port.lists, item: restoring, restore: (deletionId) => restore(restoring, deletionId), onClose: () => setRestoring(null) }) : null,
7013
+ restoring ? /* @__PURE__ */ jsx30(RestorePanel, { lists: port.lists, item: restoring, restore: (deletionId) => restore(restoring, deletionId), turnOn, onClose: () => setRestoring(null) }) : null,
6845
7014
  message2 ? /* @__PURE__ */ jsx30("p", { role: "status", className: message2.tone === "error" ? "px-1 text-sm text-destructive" : "px-1 text-sm text-muted-foreground", children: message2.text }) : null
6846
7015
  ] });
6847
7016
  }
@@ -7806,7 +7975,7 @@ function Importer({ bridge, readLimits, activeOrganizationId, organizations, onC
7806
7975
 
7807
7976
  // src/extension/PageView.tsx
7808
7977
  import { useState as useState39 } from "react";
7809
- 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";
7810
7979
  import { Button as Button33 } from "@ai-matrx/design-system/controls";
7811
7980
  import { GlobeIcon as GlobeIcon4 } from "@ai-matrx/design-system/icons";
7812
7981
 
@@ -8135,22 +8304,10 @@ function MatchRow({ itemId, item, tag, family, port, busy, running, onOpen, onFi
8135
8304
  ] })
8136
8305
  ] }),
8137
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,
8138
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,
8139
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,
8140
- 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,
8141
- onSignIn ? /* @__PURE__ */ jsxs36(DropdownMenu5, { children: [
8142
- /* @__PURE__ */ jsx38(DropdownMenuTrigger5, { asChild: true, children: /* @__PURE__ */ jsx38(Button33, { variant: "quiet", icon: /* @__PURE__ */ jsx38(MoreGlyph, {}), "aria-label": "More", disabled: busy }) }),
8143
- /* @__PURE__ */ jsxs36(DropdownMenuContent5, { align: "end", children: [
8144
- /* @__PURE__ */ jsxs36(DropdownMenuItem5, { onSelect: onSignIn, children: [
8145
- /* @__PURE__ */ jsx38(ShieldGlyph, { className: "size-4" }),
8146
- "Sign me in"
8147
- ] }),
8148
- /* @__PURE__ */ jsxs36(DropdownMenuItem5, { onSelect: () => onOpen(itemId), children: [
8149
- /* @__PURE__ */ jsx38(ListGlyph, { className: "size-4" }),
8150
- "Open item"
8151
- ] })
8152
- ] })
8153
- ] }) : 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
8154
8311
  ] });
8155
8312
  }
8156
8313
 
@@ -8262,7 +8419,7 @@ function Group({ title, children }) {
8262
8419
  children
8263
8420
  ] });
8264
8421
  }
8265
- 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 }) {
8266
8423
  const [tagsOpen, setTagsOpen] = useState40(view.startsWith("tag:"));
8267
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: [
8268
8425
  /* @__PURE__ */ jsxs37("div", { className: "flex items-center gap-2 px-3 pb-2 pt-3", children: [
@@ -8301,7 +8458,9 @@ function VaultSidebar({ vault, choices, onVault, view, allCount, deletedCount, v
8301
8458
  /* @__PURE__ */ jsx39(ShieldGlyph, { className: "size-4 shrink-0" }),
8302
8459
  /* @__PURE__ */ jsx39("span", { className: "min-w-0 flex-1 truncate", children: "System AutoFill" }),
8303
8460
  /* @__PURE__ */ jsx39("span", { className: cn15("text-xs", nativeProvider.status === "ready" ? "text-success" : "text-warning"), children: nativeProvider.status === "ready" ? "On" : "Set up" })
8304
- ] }) : 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
8305
8464
  ] })
8306
8465
  ] });
8307
8466
  }
@@ -8344,12 +8503,42 @@ function ImportWithLimits(props) {
8344
8503
  if (state && "limits" in state) return /* @__PURE__ */ jsx40(ImportPanel, { ...rest, limits: state.limits });
8345
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" }) });
8346
8505
  }
8347
- function VaultApp(props) {
8506
+ function VaultApp(given) {
8507
+ const props = { ...given, host: useOrganizationSettings(given.host ?? NO_HOST) };
8348
8508
  const lock = props.host?.lock;
8349
8509
  const locked = useLocked(lock);
8350
8510
  const port = useMemo6(() => lock ? guardPort(props.port, () => lock.locked()) : props.port, [props.port, lock]);
8351
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 }) }) }) }) }) }) }) });
8352
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
+ }
8353
8542
  function LockedScreen({ lock }) {
8354
8543
  const [busy, setBusy] = useState41(false);
8355
8544
  const [error, setError] = useState41(null);
@@ -8437,6 +8626,16 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8437
8626
  document.addEventListener("keydown", onKey);
8438
8627
  return () => document.removeEventListener("keydown", onKey);
8439
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);
8440
8639
  const knobsRef = useRef19(host.knobs);
8441
8640
  knobsRef.current = host.knobs;
8442
8641
  const [signedOut, setSignedOut] = useState41(false);
@@ -8579,6 +8778,25 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8579
8778
  notify(e instanceof Error ? e.message : "Favorites could not be saved.", "error");
8580
8779
  });
8581
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
+ };
8582
8800
  if (error) {
8583
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: [
8584
8802
  /* @__PURE__ */ jsx40(SafeGlyph, { className: "size-8 text-muted-foreground" }),
@@ -8617,38 +8835,38 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8617
8835
  onClick: () => go({ view: loc.view === "authenticator" ? "all" : "authenticator", itemId: null, mode: "view", panel: null })
8618
8836
  }
8619
8837
  ),
8620
- /* @__PURE__ */ jsxs38(DropdownMenu6, { children: [
8621
- /* @__PURE__ */ jsx40(DropdownMenuTrigger6, { asChild: true, children: /* @__PURE__ */ jsx40(Button34, { variant: "quiet", icon: /* @__PURE__ */ jsx40(MoreGlyph, {}), "aria-label": "Vault actions" }) }),
8622
- /* @__PURE__ */ jsxs38(DropdownMenuContent6, { align: "end", children: [
8623
- /* @__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: [
8624
8842
  /* @__PURE__ */ jsx40(ImportGlyph, { className: "size-4" }),
8625
8843
  "Import"
8626
8844
  ] }),
8627
- /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "export" }), children: [
8845
+ /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: () => go({ panel: "export" }), children: [
8628
8846
  /* @__PURE__ */ jsx40(ExportGlyph, { className: "size-4" }),
8629
8847
  "Export"
8630
8848
  ] }),
8631
- /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "backup" }), children: [
8849
+ /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: () => go({ panel: "backup" }), children: [
8632
8850
  /* @__PURE__ */ jsx40(SafeGlyph, { className: "size-4" }),
8633
8851
  "Backup"
8634
8852
  ] }),
8635
- /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "devices" }), children: [
8853
+ /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: () => go({ panel: "devices" }), children: [
8636
8854
  /* @__PURE__ */ jsx40(ShieldGlyph, { className: "size-4" }),
8637
8855
  "Browsers that fill"
8638
8856
  ] }),
8639
- host.nativeImport ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "import-passkeys" }), children: [
8857
+ host.nativeImport ? /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: () => go({ panel: "import-passkeys" }), children: [
8640
8858
  /* @__PURE__ */ jsx40(ImportGlyph, { className: "size-4" }),
8641
8859
  "Import passkeys"
8642
8860
  ] }) : null,
8643
- 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: [
8644
8862
  /* @__PURE__ */ jsx40(ShieldGlyph, { className: "size-4" }),
8645
8863
  "System AutoFill"
8646
8864
  ] }) : null,
8647
- 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: [
8648
8866
  /* @__PURE__ */ jsx40(LockGlyph, { className: "size-4" }),
8649
8867
  "Lock"
8650
8868
  ] }) : null,
8651
- host.openWebVault ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: host.openWebVault, children: [
8869
+ host.openWebVault ? /* @__PURE__ */ jsxs38(DropdownMenuItem5, { onSelect: host.openWebVault, children: [
8652
8870
  /* @__PURE__ */ jsx40(ExternalGlyph, { className: "size-4" }),
8653
8871
  "Open on the web"
8654
8872
  ] }) : null
@@ -8662,6 +8880,27 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8662
8880
  compactActions
8663
8881
  ] })
8664
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
+ ] });
8665
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 };
8666
8905
  const pageView = loc.view === "page" && host.page && !loc.q ? /* @__PURE__ */ jsx40(
8667
8906
  PageView,
@@ -8669,8 +8908,6 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8669
8908
  port,
8670
8909
  page: host.page,
8671
8910
  fill: host.fill,
8672
- capture: host.capture,
8673
- fillSetup: host.fillSetup,
8674
8911
  newPasswordFields: host.newPasswordFields,
8675
8912
  items: inThisVault,
8676
8913
  definitions,
@@ -8678,10 +8915,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8678
8915
  onNewForPage: (url) => go({ mode: "new", kind: null, url, itemId: null, panel: null }),
8679
8916
  onBrowse: () => go({ view: "all", itemId: null }),
8680
8917
  onChanged: load,
8681
- ...compactHeader ? { header: /* @__PURE__ */ jsxs38("div", { className: "flex flex-col gap-2", children: [
8682
- compactHeader,
8683
- /* @__PURE__ */ jsx40("div", { className: "flex justify-end", children: compactActions })
8684
- ] }) } : {}
8918
+ header: pageHeader
8685
8919
  }
8686
8920
  ) : null;
8687
8921
  const specialView = pageView ?? (loc.view === "authenticator" ? /* @__PURE__ */ jsx40(AuthenticatorView, { ...viewProps }) : loc.view === "trash" ? /* @__PURE__ */ jsx40(TrashView, { ...viewProps }) : null);
@@ -8696,11 +8930,15 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8696
8930
  selectedId: loc.itemId,
8697
8931
  onSelect: (id) => go({ itemId: id, mode: "view", kind: null, panel: null }),
8698
8932
  query: loc.q,
8699
- onQuery: (q) => go({ q }, "replace"),
8933
+ onQuery: (q) => {
8934
+ if (loc.view === "page" && host.page && !q) keepSearchFocus.current = true;
8935
+ go({ q }, "replace");
8936
+ },
8700
8937
  sort: loc.sort,
8701
8938
  onSort: (sort) => go({ sort }, "replace"),
8702
8939
  canSortViewed: port.lists.recents !== void 0,
8703
- onNew: () => go({ itemId: null, mode: "new", kind: null, panel: null }),
8940
+ onNew: newItem,
8941
+ onItemAction: rowAction,
8704
8942
  onRefresh: refresh,
8705
8943
  refreshing,
8706
8944
  ...snap.pending && snap.pending.length > 0 ? { stillLoading: snap.pending } : {},
@@ -8775,13 +9013,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8775
9013
  setNotice(n);
8776
9014
  notify("Saved");
8777
9015
  },
8778
- onDeleted: (id) => {
8779
- setItems((cur) => cur.filter((i) => i.id !== id));
8780
- dirty.current = false;
8781
- move({ ...loc, itemId: null, mode: "view", panel: null }, "push");
8782
- notify("Moved to Trash");
8783
- load();
8784
- },
9016
+ onDeleted: deletedItem,
8785
9017
  onDirty: (d) => {
8786
9018
  dirty.current = d;
8787
9019
  },
@@ -8842,6 +9074,13 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8842
9074
  /* @__PURE__ */ jsx40("span", { className: "min-w-0 flex-1 truncate text-warning", title: refreshError, children: "Couldn\u2019t refresh. Showing what was loaded." }),
8843
9075
  /* @__PURE__ */ jsx40("button", { type: "button", className: "shrink-0 text-primary hover:underline", onClick: refresh, children: "Try again" })
8844
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,
8845
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: [
8846
9085
  /* @__PURE__ */ jsxs38("span", { className: "min-w-0 flex-1 truncate text-warning", children: [
8847
9086
  "Couldn\u2019t load ",
@@ -8867,7 +9106,9 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8867
9106
  onPanel: (p) => go({ panel: p }),
8868
9107
  nativeProvider: host.nativeProvider,
8869
9108
  hasNativeImport: host.nativeImport !== void 0,
8870
- 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
8871
9112
  }
8872
9113
  ) : null,
8873
9114
  shown.list ? /* @__PURE__ */ jsx40("div", { className: specialView && mode !== "stack" ? "col-span-2 min-h-0" : "min-h-0", children: list }) : null,
@@ -9106,7 +9347,7 @@ function createFieldsPort(ctx) {
9106
9347
  const body = { initiation: "user" };
9107
9348
  if (patch.uriMatchMode !== void 0) body.uri_match_mode = patch.uriMatchMode;
9108
9349
  if (patch.browserFillEnabled !== void 0) body.browser_fill_enabled = patch.browserFillEnabled;
9109
- if (patch.loginUrls !== void 0) body.login_urls = patch.loginUrls;
9350
+ if (patch.loginUrls !== void 0) body.login_urls = normalizeLoginUrls(patch.loginUrls);
9110
9351
  if (patch.active !== void 0) body.status = patch.active ? "active" : "disabled";
9111
9352
  await account3.updateItem(api, { path: { item_id: item.id }, body }, ctx.optsFor(item));
9112
9353
  return reread(item);
@@ -9118,6 +9359,33 @@ function createFieldsPort(ctx) {
9118
9359
  };
9119
9360
  }
9120
9361
 
9362
+ // src/server-reasons.ts
9363
+ var GENERIC = [/please check your input/i, /^\s*request failed\s*\(\d+\)\s*$/i, /^\s*bad request\.?\s*$/i, /^\s*HTTP \d+\s*$/i];
9364
+ var isGeneric = (s) => GENERIC.some((re) => re.test(s));
9365
+ function cleanReason(detail) {
9366
+ const s = detail.replace(/^\s*validation error:\s*/i, "").replace(/^\s*value error,\s*/i, "").trim();
9367
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
9368
+ }
9369
+ function serverReason(e) {
9370
+ if (!e || typeof e !== "object") return null;
9371
+ const { status, detail, message: message2 } = e;
9372
+ if (typeof status !== "number" || status < 400 || status >= 500) return null;
9373
+ if (typeof detail !== "string" || !detail.trim() || isGeneric(detail)) return null;
9374
+ const shown = typeof message2 === "string" ? message2 : "";
9375
+ if (shown && !isGeneric(shown)) return null;
9376
+ return cleanReason(detail);
9377
+ }
9378
+ function withReason(e) {
9379
+ const reason = serverReason(e);
9380
+ if (reason) Object.assign(e, { message: reason, userMessage: reason });
9381
+ return e;
9382
+ }
9383
+ function withServerReasons(api) {
9384
+ const wrapped = Object.create(api);
9385
+ wrapped.call = (...args) => api.call(...args).catch((e) => Promise.reject(withReason(e)));
9386
+ return wrapped;
9387
+ }
9388
+
9121
9389
  // src/io/api.ts
9122
9390
  import * as account4 from "@ai-matrx/api/account";
9123
9391
  var orgOpts = (organizationId) => organizationId ? { organizationId } : void 0;
@@ -9390,7 +9658,7 @@ function createSharingPort(ctx, options = {}) {
9390
9658
  definition_key: definition.key,
9391
9659
  provider_key: definition.providerKey,
9392
9660
  tags: draft.tags,
9393
- login_urls: draft.loginUrls,
9661
+ login_urls: normalizeLoginUrls(draft.loginUrls),
9394
9662
  notes: draft.notes || null,
9395
9663
  generate_field_key: generateFieldKey,
9396
9664
  initiation: "user",
@@ -9587,7 +9855,8 @@ async function retrying(run, tries = 3) {
9587
9855
  }
9588
9856
  }
9589
9857
  }
9590
- function createApiVaultPort(api, options = {}) {
9858
+ function createApiVaultPort(hostApi, options = {}) {
9859
+ const api = withServerReasons(hostApi);
9591
9860
  let defs = /* @__PURE__ */ new Map();
9592
9861
  let auth = /* @__PURE__ */ new Map();
9593
9862
  let orgs = [];
@@ -9742,6 +10011,7 @@ function createApiVaultPort(api, options = {}) {
9742
10011
  const declared = definition.fields;
9743
10012
  const encrypted = declared.filter((f) => f.storage === "encrypted" && (draft.values[f.key] ?? "") !== "");
9744
10013
  const meta = declared.filter((f) => f.storage === "metadata" && (draft.values[f.key] ?? "") !== "");
10014
+ const loginUrls = normalizeLoginUrls(draft.loginUrls);
9745
10015
  const w = await account9.createItem(api, {
9746
10016
  body: {
9747
10017
  display_name: draft.name.trim(),
@@ -9749,10 +10019,10 @@ function createApiVaultPort(api, options = {}) {
9749
10019
  definition_key: definition.key,
9750
10020
  provider_key: definition.providerKey,
9751
10021
  tags: draft.tags,
9752
- login_urls: draft.loginUrls,
10022
+ login_urls: loginUrls,
9753
10023
  // A login a person saves with its website is offered on that website (the server's own
9754
10024
  // default is off, which made "Save a login" on a page answer "No saved login" there).
9755
- ...draft.browser ? { uri_match_mode: draft.browser.uriMatchMode, browser_fill_enabled: draft.browser.browserFillEnabled } : draft.loginUrls.length > 0 ? { browser_fill_enabled: true } : {},
10025
+ ...draft.browser ? { uri_match_mode: draft.browser.uriMatchMode, browser_fill_enabled: draft.browser.browserFillEnabled } : loginUrls.length > 0 ? { browser_fill_enabled: true } : {},
9756
10026
  notes: draft.notes || null,
9757
10027
  principal: organizationId ? { type: "organization", organization_id: organizationId } : { type: "user" },
9758
10028
  initiation: "user",
@@ -9779,7 +10049,7 @@ function createApiVaultPort(api, options = {}) {
9779
10049
  else body.description = draft.description.trim();
9780
10050
  }
9781
10051
  if (JSON.stringify(draft.tags) !== JSON.stringify(item.tags)) body.tags = draft.tags;
9782
- if (JSON.stringify(draft.loginUrls) !== JSON.stringify(item.loginUrls)) body.login_urls = draft.loginUrls;
10052
+ if (JSON.stringify(draft.loginUrls) !== JSON.stringify(item.loginUrls)) body.login_urls = normalizeLoginUrls(draft.loginUrls);
9783
10053
  if (draft.notes !== item.notes) {
9784
10054
  if (draft.notes === "") body.clear_notes = true;
9785
10055
  else body.notes = draft.notes;
@@ -9811,20 +10081,6 @@ function createApiVaultPort(api, options = {}) {
9811
10081
  };
9812
10082
  }
9813
10083
 
9814
- // src/knobs.ts
9815
- import { createKnobReader } from "@ai-matrx/data/knobs";
9816
- var FRESH_MS = 6e4;
9817
- function createVaultKnobs(client, address) {
9818
- const reader = createKnobReader(client);
9819
- let kept = null;
9820
- return async () => {
9821
- if (kept && Date.now() - kept.at < FRESH_MS) return kept.resolved;
9822
- const snap = await reader.snapshot(address);
9823
- kept = { at: Date.now(), resolved: snap.resolved };
9824
- return snap.resolved;
9825
- };
9826
- }
9827
-
9828
10084
  // src/agent-context.ts
9829
10085
  var hostOf = (url) => {
9830
10086
  try {
@@ -9892,6 +10148,7 @@ function emptyReadAlarm(total, emptyIds, holdings) {
9892
10148
  return `${staleAlarm(total)} (The check that tells an empty credential from an unreadable one did not answer either: ${holdings.why})`;
9893
10149
  }
9894
10150
  export {
10151
+ BACKUP_PASSPHRASE_MIN_KNOB,
9895
10152
  CATEGORIES,
9896
10153
  CONFIRM_RETURN_KEY,
9897
10154
  DEFAULT_LOCATION,
@@ -9900,9 +10157,11 @@ export {
9900
10157
  HOLDINGS_DOOR_MIGRATION,
9901
10158
  IdentityGate,
9902
10159
  LOCKED_MESSAGE,
10160
+ REVEAL_SECONDS_KNOB,
9903
10161
  VaultApp,
9904
10162
  VaultLockedError,
9905
10163
  categoryOf,
10164
+ copiedLine,
9906
10165
  copyValue,
9907
10166
  createApiVaultPort,
9908
10167
  createSupabaseIdentity,
@@ -9919,11 +10178,13 @@ export {
9919
10178
  matchesQuery,
9920
10179
  needsRecentSignIn,
9921
10180
  parseDefinitions,
10181
+ positiveKnob,
9922
10182
  providerOf,
9923
10183
  rowMenuEntriesAt,
9924
10184
  siteDomainOf,
9925
10185
  sortItems,
9926
10186
  useConfirmed,
10187
+ useDisposable,
9927
10188
  vaultInventoryScope,
9928
10189
  vaultItemFacts,
9929
10190
  wasCancelled