@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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.29
4
+
5
+ - **Lock and Open on the web at every width.** The three-pane sidebar's foot offers **Lock** whenever the host gives `lock.lock`, and **Open on the web** whenever it gives `openWebVault` (before, both lived only in the narrow layout's More menu, so a desktop window 1120px or wider had no manual lock). Guard: `src/no-feature-lost.test.tsx` § D1. Consumer action: none.
6
+ - **"This page" has the list's search row and New.** A "Search the vault" box (so "/" finds), the compact actions and a **New item** button sit above the site on every page, https or not; typing shows the matching list with the cursor kept in the search, clearing it brings the page back. Guard: § E1. Consumer action: none.
7
+ - **Sign me in is a visible button beside Fill** (primary when there is no Fill), never inside a More menu. Guard: § E2. Consumer action: none.
8
+ - **"Save this login?" and "Turn on filling" show above the list in every view**, not only on "This page" (`host.capture`, `host.fillSetup`; `PageView` no longer receives them from `VaultApp`). Guard: § E3. Consumer action: none.
9
+ - **The copied line says when the clipboard clears.** `VaultClipboard.copy` may answer `{ clearsAtMs }` (`VaultCopied`); the vault then says "Password copied. Clears at 3:42 PM" (`copiedLine`). Answering nothing keeps "Password copied". Guard: § D3. **Consumer action:** a host whose clipboard clears a secret later (the desktop) returns main's `clearsAtMs` from `copy`.
10
+ - **Activity, Share and Delete on every row's menu** (`RowMenuEntryKind` `manage`, `ItemList` `onItemAction`, `manageEntries`): Activity and Share open the item's own panels, Delete asks the item's own question (`confirmDelete`) and moves it to Trash; Share and Delete show off when the item says why it cannot be managed. A host's right-click menu reading `rowMenuEntriesAt` may take the `manage` entries too. Guard: § D4. Consumer action: none.
11
+ - **The organization's backup passphrase minimum is enforced.** `host.backupPassphraseMinLength` (else the organization's `desktop.vault/backup_passphrase_min_length`, `BACKUP_PASSPHRASE_MIN_KNOB`, read through `host.knobs`): the Passphrase row says "At least N characters", a shorter one is refused by name and Encrypt and download stays off. Guard: § D5. **Consumer action:** a host that reads the setting itself (the desktop) passes `backupPassphraseMinLength`.
12
+ - **Every host obeys the organization's reveal time.** Without `host.revealSeconds` the vault reads `desktop.vault/reveal_hide_seconds` (`REVEAL_SECONDS_KNOB`) through `host.knobs` (the extension and the website: 30s by default, was the package's 20s). `positiveKnob` reads either. Guard: § E5. Consumer action: none (a host with `knobs` inherits it).
13
+ - **`useDisposable(create, deps)`**: a host object the screen owns, made and disposed in the same effect, so StrictMode's mount → cleanup → mount leaves a live one (a `useMemo` + cleanup-only effect left the first, disposed copy). Guard: § E4/D2. **Consumer action:** extension: make the new-password-fields host with it; desktop: make the vault lock with it.
14
+
3
15
  ## 0.1.28
4
16
 
5
17
  - **A website typed as a bare domain always saves.** Every bare domain is sent as `https://…` on create, edit, assign and the item settings call, whether or not the server could reach it (an intranet, a site not live yet) (`normalizeLoginUrls`); the Website box shows the `https://` form after leaving it, and "Not a public website. You can still save it." is a warning, never a block. Guard: `src/review-0-1-28.test.tsx` § 1. Consumer action: none.
package/dist/index.cjs CHANGED
@@ -135,6 +135,7 @@ var init_keepass_xml_worker_client = __esm({
135
135
  // src/index.ts
136
136
  var src_exports = {};
137
137
  __export(src_exports, {
138
+ BACKUP_PASSPHRASE_MIN_KNOB: () => BACKUP_PASSPHRASE_MIN_KNOB,
138
139
  CATEGORIES: () => CATEGORIES,
139
140
  CONFIRM_RETURN_KEY: () => CONFIRM_RETURN_KEY,
140
141
  DEFAULT_LOCATION: () => DEFAULT_LOCATION,
@@ -143,9 +144,11 @@ __export(src_exports, {
143
144
  HOLDINGS_DOOR_MIGRATION: () => HOLDINGS_DOOR_MIGRATION,
144
145
  IdentityGate: () => IdentityGate,
145
146
  LOCKED_MESSAGE: () => LOCKED_MESSAGE,
147
+ REVEAL_SECONDS_KNOB: () => REVEAL_SECONDS_KNOB,
146
148
  VaultApp: () => VaultApp,
147
149
  VaultLockedError: () => VaultLockedError,
148
150
  categoryOf: () => categoryOf,
151
+ copiedLine: () => copiedLine,
149
152
  copyValue: () => copyValue,
150
153
  createApiVaultPort: () => createApiVaultPort,
151
154
  createSupabaseIdentity: () => createSupabaseIdentity,
@@ -162,11 +165,13 @@ __export(src_exports, {
162
165
  matchesQuery: () => matchesQuery,
163
166
  needsRecentSignIn: () => needsRecentSignIn,
164
167
  parseDefinitions: () => parseDefinitions,
168
+ positiveKnob: () => positiveKnob,
165
169
  providerOf: () => providerOf,
166
170
  rowMenuEntriesAt: () => rowMenuEntriesAt,
167
171
  siteDomainOf: () => siteDomainOf,
168
172
  sortItems: () => sortItems,
169
173
  useConfirmed: () => useConfirmed,
174
+ useDisposable: () => useDisposable,
170
175
  vaultInventoryScope: () => vaultInventoryScope,
171
176
  vaultItemFacts: () => vaultItemFacts,
172
177
  wasCancelled: () => wasCancelled
@@ -192,8 +197,8 @@ async function copyValue(text, label, options, deps) {
192
197
  const secret = options.secret ?? true;
193
198
  if (deps.clipboard) {
194
199
  try {
195
- await deps.clipboard.copy(text, { secret });
196
- deps.notify(`${label} copied`, "success");
200
+ const copied = await deps.clipboard.copy(text, { secret });
201
+ deps.notify(copiedLine(label, copied?.clearsAtMs ?? null), "success");
197
202
  return true;
198
203
  } catch (e) {
199
204
  deps.notify(e instanceof Error && e.message ? e.message : `${label} could not be copied`, "error");
@@ -202,6 +207,11 @@ async function copyValue(text, label, options, deps) {
202
207
  }
203
208
  return (0, import_clipboard.copyText)(text, { successMessage: `${label} copied`, failureMessage: `${label} could not be copied`, notify: (m, k) => deps.notify(m, k === "error" ? "error" : "success") });
204
209
  }
210
+ function copiedLine(label, clearsAtMs) {
211
+ if (clearsAtMs === null || !Number.isFinite(clearsAtMs)) return `${label} copied`;
212
+ const at = new Date(clearsAtMs).toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
213
+ return `${label} copied. Clears at ${at}`;
214
+ }
205
215
  var ClipboardCtx = (0, import_react.createContext)(void 0);
206
216
  function ClipboardProvider({ clipboard, children }) {
207
217
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ClipboardCtx.Provider, { value: clipboard, children });
@@ -602,6 +612,17 @@ function useElementWidth(ref) {
602
612
  }, [ref]);
603
613
  return width;
604
614
  }
615
+ function useDisposable(create, deps) {
616
+ const [held, setHeld] = (0, import_react3.useState)(null);
617
+ const make = (0, import_react3.useRef)(create);
618
+ make.current = create;
619
+ (0, import_react3.useEffect)(() => {
620
+ const made = make.current();
621
+ setHeld(made);
622
+ return () => made.dispose();
623
+ }, deps);
624
+ return held;
625
+ }
605
626
 
606
627
  // src/ui.tsx
607
628
  var import_react4 = require("react");
@@ -3149,6 +3170,27 @@ function importLimitsFrom(resolved) {
3149
3170
  return out;
3150
3171
  }
3151
3172
 
3173
+ // src/knobs.ts
3174
+ var import_knobs = require("@ai-matrx/data/knobs");
3175
+ var FRESH_MS = 6e4;
3176
+ function createVaultKnobs(client, address) {
3177
+ const reader = (0, import_knobs.createKnobReader)(client);
3178
+ let kept = null;
3179
+ return async () => {
3180
+ if (kept && Date.now() - kept.at < FRESH_MS) return kept.resolved;
3181
+ const snap = await reader.snapshot(address);
3182
+ kept = { at: Date.now(), resolved: snap.resolved };
3183
+ return snap.resolved;
3184
+ };
3185
+ }
3186
+ var REVEAL_SECONDS_KNOB = "desktop.vault.reveal_hide_seconds";
3187
+ var BACKUP_PASSPHRASE_MIN_KNOB = "desktop.vault.backup_passphrase_min_length";
3188
+ function positiveKnob(resolved, key) {
3189
+ const v = resolved[key];
3190
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
3191
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
3192
+ }
3193
+
3152
3194
  // src/lock.ts
3153
3195
  var import_react16 = require("react");
3154
3196
  var LOCKED_MESSAGE = "The vault is locked. Unlock it first.";
@@ -3344,7 +3386,8 @@ function Omissions({ omissions, accepted, onAccepted, verb }) {
3344
3386
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(VaultRow, { label: "Accept", hint: `${verb} without these`, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_controls13.Switch, { checked: accepted, onCheckedChange: onAccepted, "aria-label": `${verb} without these` }) })
3345
3387
  ] });
3346
3388
  }
3347
- function BackupPanel({ port, items, onClose, onChanged }) {
3389
+ function BackupPanel({ port, host, items, onClose, onChanged }) {
3390
+ const minLength = passphraseMinimum(host.backupPassphraseMinLength);
3348
3391
  const confirmed = useConfirmed();
3349
3392
  const fileInput = (0, import_react18.useRef)(null);
3350
3393
  const abort = (0, import_react18.useRef)(null);
@@ -3399,7 +3442,7 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3399
3442
  setAccepted(false);
3400
3443
  });
3401
3444
  const downloadBackup = () => guarded(async () => {
3402
- if (!backup) return;
3445
+ if (!backup || tooShort(passphrase, minLength)) return;
3403
3446
  const bytes2 = await confirmed(() => port.io.downloadBackup({ itemIds: [...selected], revision: backup.revision, passphrase, acceptOmissions: accepted }));
3404
3447
  (0, import_download.downloadFile)(BACKUP_FILENAME, bytes2, "application/octet-stream");
3405
3448
  setPassphrase("");
@@ -3446,10 +3489,11 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3446
3489
  };
3447
3490
  const retryable = (result?.retryable_failure ?? 0) > 0;
3448
3491
  const mismatch = repeat !== "" && repeat !== passphrase;
3492
+ const short = passphrase !== "" && tooShort(passphrase, minLength);
3449
3493
  const names = new Map(restorePreview?.records.map((r) => [r.source_item_id, r.display_name]) ?? []);
3450
3494
  const footer = mode === "backup" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
3451
3495
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_controls13.Button, { variant: "quiet", onClick: onClose, children: "Close" }),
3452
- backup ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_controls13.Button, { variant: "primary", disabled: busy || !passphrase || passphrase !== repeat || backup.omissions.length > 0 && !accepted, onClick: () => void downloadBackup(), children: "Encrypt and download" }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_controls13.Button, { variant: "primary", disabled: busy || selected.size === 0, onClick: () => void reviewBackup(), children: [
3496
+ backup ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_controls13.Button, { variant: "primary", disabled: busy || !passphrase || short || passphrase !== repeat || backup.omissions.length > 0 && !accepted, onClick: () => void downloadBackup(), children: "Encrypt and download" }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_controls13.Button, { variant: "primary", disabled: busy || selected.size === 0, onClick: () => void reviewBackup(), children: [
3453
3497
  "Review ",
3454
3498
  selected.size
3455
3499
  ] })
@@ -3496,7 +3540,14 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3496
3540
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Summary, { value: backup }),
3497
3541
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Omissions, { omissions: backup.omissions, accepted, onAccepted: setAccepted, verb: "Back up" }),
3498
3542
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(VaultSection, { children: [
3499
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(VaultRow, { label: "Passphrase", hint: "Not saved. It can't be recovered.", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_controls13.Field, { type: "password", value: passphrase, autoComplete: "new-password", "aria-label": "Backup passphrase", onChange: (e) => setPassphrase(e.target.value) }) }),
3543
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(VaultRow, { label: "Passphrase", hint: minLength ? `At least ${minLength} characters. Not saved.` : "Not saved. It can't be recovered.", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_controls13.Field, { type: "password", value: passphrase, autoComplete: "new-password", "aria-label": "Backup passphrase", "aria-invalid": short || void 0, onChange: (e) => setPassphrase(e.target.value) }) }),
3544
+ short ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { role: "alert", className: "px-1 text-xs text-destructive", "data-passphrase-too-short": "", children: [
3545
+ "Use at least ",
3546
+ minLength,
3547
+ " characters. This one has ",
3548
+ passphrase.length,
3549
+ "."
3550
+ ] }) : null,
3500
3551
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(VaultRow, { label: "Repeat", ...mismatch ? { hint: "Doesn't match" } : {}, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_controls13.Field, { type: "password", value: repeat, autoComplete: "new-password", "aria-label": "Repeat the passphrase", onChange: (e) => setRepeat(e.target.value) }) })
3501
3552
  ] })
3502
3553
  ] }) : null
@@ -3529,6 +3580,12 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3529
3580
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ErrorLine, { message: error })
3530
3581
  ] });
3531
3582
  }
3583
+ function passphraseMinimum(given) {
3584
+ return given !== void 0 && Number.isFinite(given) && given > 1 ? Math.floor(given) : null;
3585
+ }
3586
+ function tooShort(passphrase, minLength) {
3587
+ return minLength !== null && [...passphrase].length < minLength;
3588
+ }
3532
3589
 
3533
3590
  // src/io/ExportPanel.tsx
3534
3591
  var import_react19 = require("react");
@@ -5986,6 +6043,9 @@ var import_jsx_runtime27 = require("react/jsx-runtime");
5986
6043
  var ENV_VALUE_KEY = "env_value";
5987
6044
  var secureNoteDraft = () => ({ key: SECURE_NOTE_KEY, label: "Secure note", value: "", multiline: true, description: "Encrypted notes" });
5988
6045
  var USERNAME_KEYS = /^(username|user_name|email|login|username_or_email|account_email)$/;
6046
+ function confirmDelete(item) {
6047
+ return (0, import_confirm_opener10.confirm)({ title: `Delete \u201C${item.name}\u201D?`, description: "It moves to Trash. You can restore it from there.", confirmLabel: "Delete", variant: "destructive" });
6048
+ }
5989
6049
  function ItemPane(props) {
5990
6050
  const { port, item, definition, mode, host, organizations, defaultOwner, seed, onBack, onMode, onSaved, onDeleted, onDirty, onCancelNew, onPanel, onChanged, favorite, notice, refreshKey } = props;
5991
6051
  const confirmed = useConfirmed();
@@ -6138,8 +6198,7 @@ function ItemPane(props) {
6138
6198
  }
6139
6199
  async function remove() {
6140
6200
  if (!item) return;
6141
- const ok = await (0, import_confirm_opener10.confirm)({ title: `Delete \u201C${item.name}\u201D?`, description: "It moves to Trash. You can restore it from there.", confirmLabel: "Delete", variant: "destructive" });
6142
- if (!ok) return;
6201
+ if (!await confirmDelete(item)) return;
6143
6202
  setBusy(true);
6144
6203
  try {
6145
6204
  await port.remove(item);
@@ -6550,7 +6609,7 @@ function subtitleOf(item, withOwner) {
6550
6609
  const what = loginSiteOf(item) || item.definitionLabel;
6551
6610
  return withOwner ? `${what} \xB7 ${item.owner.label}` : what;
6552
6611
  }
6553
- 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 }) {
6612
+ 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 }) {
6554
6613
  const sorts = SORTS2.filter((s) => canSortViewed || s.value !== "viewed");
6555
6614
  const pinned = matchingIds ? items.filter((i) => matchingIds.has(i.id)) : [];
6556
6615
  const rest = matchingIds ? items.filter((i) => !matchingIds.has(i.id)) : items;
@@ -6571,7 +6630,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6571
6630
  }
6572
6631
  };
6573
6632
  const mixedOwners = showOwner ?? new Set(items.map((i) => i.owner.organizationId ?? i.owner.kind)).size > 1;
6574
- const row = (i) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(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);
6633
+ const row = (i) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(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);
6575
6634
  const sortLabel = sorts.find((s) => s.value === sort)?.label ?? "Name A\u2013Z";
6576
6635
  return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("section", { "aria-label": title, className: "flex h-full min-h-0 flex-col border-r border-border bg-background", children: [
6577
6636
  /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-col gap-2 px-3 pb-2 pt-3", children: [
@@ -6615,7 +6674,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6615
6674
  ] }) })
6616
6675
  ] });
6617
6676
  }
6618
- function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu, extra, favorite }) {
6677
+ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu, extra, onAction, favorite }) {
6619
6678
  const confirmed = useConfirmed();
6620
6679
  const copy = useCopy();
6621
6680
  const notify = useNotify();
@@ -6656,11 +6715,12 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6656
6715
  ...favoriteEntry ? [favoriteEntry] : [],
6657
6716
  { id: "open", kind: "open", label: "Open", onSelect: () => onSelect(item.id) },
6658
6717
  { id: "copy-name", kind: "copy-name", label: "Copy name", onSelect: copyName },
6718
+ ...manageEntries(item, onAction),
6659
6719
  ...hostEntries.map((h) => ({ ...h, kind: h.kind ?? "host" }))
6660
6720
  ];
6661
6721
  const entriesRef = (0, import_react29.useRef)(entries);
6662
6722
  entriesRef.current = entries;
6663
- const glyphOf = (e) => e.id === "copy:username" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(UserGlyph, { className: "size-4" }) : e.id === "copy:password" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(KeyGlyph, { className: "size-4" }) : e.id === "copy:code" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ClockGlyph, { className: "size-4" }) : e.kind === "favorite" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(StarGlyph, { className: (0, import_design_system20.cn)("size-4", favorite?.on && "fill-current text-warning") }) : e.kind === "open" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ChevronRightGlyph, { className: "size-4" }) : e.kind === "host" ? null : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_icons12.CopyIcon, { className: "size-4" });
6723
+ const glyphOf = (e) => e.id === "copy:username" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(UserGlyph, { className: "size-4" }) : e.id === "copy:password" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(KeyGlyph, { className: "size-4" }) : e.id === "copy:code" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ClockGlyph, { className: "size-4" }) : e.kind === "favorite" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(StarGlyph, { className: (0, import_design_system20.cn)("size-4", favorite?.on && "fill-current text-warning") }) : e.kind === "open" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ChevronRightGlyph, { className: "size-4" }) : e.kind === "host" || e.kind === "manage" ? null : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_icons12.CopyIcon, { className: "size-4" });
6664
6724
  const menuItem = (e) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_design_system19.DropdownMenuItem, { disabled: e.disabled ?? false, onSelect: e.onSelect, children: [
6665
6725
  glyphOf(e),
6666
6726
  e.label
@@ -6714,6 +6774,8 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6714
6774
  copyEntries.map(menuItem),
6715
6775
  copyEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_design_system19.DropdownMenuSeparator, {}) : null,
6716
6776
  entries.filter((e) => e.kind === "favorite" || e.kind === "open" || e.kind === "copy-name").map(menuItem),
6777
+ entries.some((e) => e.kind === "manage") ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_design_system19.DropdownMenuSeparator, {}) : null,
6778
+ entries.filter((e) => e.kind === "manage").map(menuItem),
6717
6779
  hostEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_design_system19.DropdownMenuSeparator, {}) : null,
6718
6780
  entries.filter((e) => e.kind === "host").map(menuItem)
6719
6781
  ] })
@@ -6723,6 +6785,17 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6723
6785
  }
6724
6786
  );
6725
6787
  }
6788
+ function manageEntries(item, onAction) {
6789
+ if (!onAction) return [];
6790
+ const canManage = item.capabilities.canManage;
6791
+ const offManage = !canManage && manageUnavailableReason(item) !== null;
6792
+ const run = (action) => () => onAction(item, action);
6793
+ return [
6794
+ { id: "activity", kind: "manage", label: "Activity", onSelect: run("activity") },
6795
+ ...canManage || offManage ? [{ id: "share", kind: "manage", label: "Share", onSelect: run("share"), disabled: !canManage }] : [],
6796
+ ...canManage || offManage ? [{ id: "delete", kind: "manage", label: "Delete", onSelect: run("delete"), disabled: !canManage }] : []
6797
+ ];
6798
+ }
6726
6799
 
6727
6800
  // src/lists/TrashView.tsx
6728
6801
  var import_react31 = require("react");
@@ -8259,22 +8332,10 @@ function MatchRow({ itemId, item, tag, family, port, busy, running, onOpen, onFi
8259
8332
  ] })
8260
8333
  ] }),
8261
8334
  onFill ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_controls33.Button, { variant: "primary", disabled: busy, "aria-busy": running || void 0, onClick: onFill, "aria-label": `Fill ${item?.name ?? "this login"}`, children: running ? "Filling\u2026" : "Fill" }) : null,
8335
+ onSignIn ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_controls33.Button, { variant: onFill ? "outline" : "primary", icon: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ShieldGlyph, {}), disabled: busy, onClick: onSignIn, "aria-label": `Sign me in with ${item?.name ?? "this login"}`, title: "Sign me in", children: "Sign me in" }) : null,
8262
8336
  user && item ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_controls33.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(UserGlyph, {}), "aria-label": `Copy ${user.label}`, title: `Copy ${user.label}`, disabled: reading, onClick: () => void read(user.label, () => port.reveal(item, user.key)) }) : null,
8263
8337
  secret && item ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_controls33.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(KeyGlyph, {}), "aria-label": "Copy password", title: "Copy password", disabled: reading, onClick: () => void read("Password", () => port.reveal(item, secret.key)) }) : null,
8264
- otp ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_controls33.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(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,
8265
- onSignIn ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_design_system24.DropdownMenu, { children: [
8266
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_design_system24.DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_controls33.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(MoreGlyph, {}), "aria-label": "More", disabled: busy }) }),
8267
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_design_system24.DropdownMenuContent, { align: "end", children: [
8268
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_design_system24.DropdownMenuItem, { onSelect: onSignIn, children: [
8269
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ShieldGlyph, { className: "size-4" }),
8270
- "Sign me in"
8271
- ] }),
8272
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_design_system24.DropdownMenuItem, { onSelect: () => onOpen(itemId), children: [
8273
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ListGlyph, { className: "size-4" }),
8274
- "Open item"
8275
- ] })
8276
- ] })
8277
- ] }) : null
8338
+ otp ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_controls33.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(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
8278
8339
  ] });
8279
8340
  }
8280
8341
 
@@ -8386,7 +8447,7 @@ function Group({ title, children }) {
8386
8447
  children
8387
8448
  ] });
8388
8449
  }
8389
- function VaultSidebar({ vault, choices, onVault, view, allCount, deletedCount, views, hasFavorites, hasRecents, onView, onPanel, nativeProvider, hasNativeImport = false, hasPage = false }) {
8450
+ function VaultSidebar({ vault, choices, onVault, view, allCount, deletedCount, views, hasFavorites, hasRecents, onView, onPanel, nativeProvider, hasNativeImport = false, hasPage = false, onLock, onOpenWeb }) {
8390
8451
  const [tagsOpen, setTagsOpen] = (0, import_react41.useState)(view.startsWith("tag:"));
8391
8452
  return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("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: [
8392
8453
  /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2 px-3 pb-2 pt-3", children: [
@@ -8425,7 +8486,9 @@ function VaultSidebar({ vault, choices, onVault, view, allCount, deletedCount, v
8425
8486
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ShieldGlyph, { className: "size-4 shrink-0" }),
8426
8487
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "min-w-0 flex-1 truncate", children: "System AutoFill" }),
8427
8488
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: (0, import_design_system25.cn)("text-xs", nativeProvider.status === "ready" ? "text-success" : "text-warning"), children: nativeProvider.status === "ready" ? "On" : "Set up" })
8428
- ] }) : null
8489
+ ] }) : null,
8490
+ onOpenWeb ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Entry, { on: false, glyph: ExternalGlyph, label: "Open on the web", onClick: onOpenWeb }) : null,
8491
+ onLock ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Entry, { on: false, glyph: LockGlyph, label: "Lock", onClick: onLock }) : null
8429
8492
  ] })
8430
8493
  ] });
8431
8494
  }
@@ -8468,12 +8531,42 @@ function ImportWithLimits(props) {
8468
8531
  if (state && "limits" in state) return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ImportPanel, { ...rest, limits: state.limits });
8469
8532
  return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(Panel, { title: "Import", onClose: rest.onClose, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(Empty, { children: state ? state.error : "Reading this organization\u2019s import settings\u2026" }) });
8470
8533
  }
8471
- function VaultApp(props) {
8534
+ function VaultApp(given) {
8535
+ const props = { ...given, host: useOrganizationSettings(given.host ?? NO_HOST) };
8472
8536
  const lock = props.host?.lock;
8473
8537
  const locked = useLocked(lock);
8474
8538
  const port = (0, import_react42.useMemo)(() => lock ? guardPort(props.port, () => lock.locked()) : props.port, [props.port, lock]);
8475
8539
  return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_design_system26.LookScope, { look: props.look ?? "web", className: "h-full min-h-0 w-full", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "relative h-full min-h-0 w-full", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(NoticeProvider, { notify: props.notify, children: lock && locked ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(LockedScreen, { lock }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ClipboardProvider, { clipboard: props.host?.clipboard, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(RevealSecondsProvider, { seconds: props.host?.revealSeconds, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(IdentityGate, { identity: props.host?.identity, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(GeneratorLimitsProvider, { resolve: generatorLimitsFrom(props.host?.knobs), children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(Workspace, { ...props, port }) }) }) }) }) }) }) });
8476
8540
  }
8541
+ function useOrganizationSettings(host) {
8542
+ const knobs = host.knobs;
8543
+ const needs = knobs !== void 0 && (host.revealSeconds === void 0 || host.backupPassphraseMinLength === void 0);
8544
+ const [read, setRead] = (0, import_react42.useState)(null);
8545
+ (0, import_react42.useEffect)(() => {
8546
+ if (!needs || !knobs) return;
8547
+ let live = true;
8548
+ knobs().then(
8549
+ (r) => live && setRead({ reveal: positiveKnob(r, REVEAL_SECONDS_KNOB), backupMin: positiveKnob(r, BACKUP_PASSPHRASE_MIN_KNOB) }),
8550
+ () => void 0
8551
+ );
8552
+ return () => {
8553
+ live = false;
8554
+ };
8555
+ }, [needs, knobs]);
8556
+ if (!read) return host;
8557
+ return {
8558
+ ...host,
8559
+ ...host.revealSeconds === void 0 && read.reveal !== null ? { revealSeconds: read.reveal } : {},
8560
+ ...host.backupPassphraseMinLength === void 0 && read.backupMin !== null ? { backupPassphraseMinLength: read.backupMin } : {}
8561
+ };
8562
+ }
8563
+ var NO_STORE = () => () => void 0;
8564
+ var NOTHING = () => null;
8565
+ function useHostPrompts(capture, fillSetup) {
8566
+ const prompt = (0, import_react42.useSyncExternalStore)(capture ? capture.subscribe : NO_STORE, capture ? capture.get : NOTHING, capture ? capture.get : NOTHING);
8567
+ const setup = (0, import_react42.useSyncExternalStore)(fillSetup ? fillSetup.subscribe : NO_STORE, fillSetup ? fillSetup.get : NOTHING, fillSetup ? fillSetup.get : NOTHING);
8568
+ return prompt !== null || setup?.status === "off";
8569
+ }
8477
8570
  function LockedScreen({ lock }) {
8478
8571
  const [busy, setBusy] = (0, import_react42.useState)(false);
8479
8572
  const [error, setError] = (0, import_react42.useState)(null);
@@ -8561,6 +8654,16 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8561
8654
  document.addEventListener("keydown", onKey);
8562
8655
  return () => document.removeEventListener("keydown", onKey);
8563
8656
  }, []);
8657
+ const keepSearchFocus = (0, import_react42.useRef)(false);
8658
+ (0, import_react42.useEffect)(() => {
8659
+ if (!keepSearchFocus.current) return;
8660
+ keepSearchFocus.current = false;
8661
+ const search = rootRef.current?.querySelector('input[aria-label^="Search"]');
8662
+ if (!search || document.activeElement === search) return;
8663
+ search.focus();
8664
+ search.setSelectionRange(search.value.length, search.value.length);
8665
+ }, [loc.q]);
8666
+ const hasPrompts = useHostPrompts(host.capture, host.fillSetup);
8564
8667
  const knobsRef = (0, import_react42.useRef)(host.knobs);
8565
8668
  knobsRef.current = host.knobs;
8566
8669
  const [signedOut, setSignedOut] = (0, import_react42.useState)(false);
@@ -8703,6 +8806,25 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8703
8806
  notify(e instanceof Error ? e.message : "Favorites could not be saved.", "error");
8704
8807
  });
8705
8808
  };
8809
+ const deletedItem = (id) => {
8810
+ setItems((cur) => cur.filter((i) => i.id !== id));
8811
+ dirty.current = false;
8812
+ if (loc.itemId === id) move({ ...loc, itemId: null, mode: "view", panel: null }, "push");
8813
+ notify("Moved to Trash");
8814
+ load();
8815
+ };
8816
+ const rowAction = (item, action) => {
8817
+ if (action !== "delete") return go({ view: loc.view === "page" ? "all" : loc.view, itemId: item.id, mode: "view", panel: action });
8818
+ void confirmDelete(item).then(async (ok) => {
8819
+ if (!ok) return;
8820
+ try {
8821
+ await port.remove(item);
8822
+ deletedItem(item.id);
8823
+ } catch (e) {
8824
+ notify(e instanceof Error && e.message ? e.message : "Deleting failed.", "error");
8825
+ }
8826
+ });
8827
+ };
8706
8828
  if (error) {
8707
8829
  return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("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: [
8708
8830
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(SafeGlyph, { className: "size-8 text-muted-foreground" }),
@@ -8786,6 +8908,27 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8786
8908
  compactActions
8787
8909
  ] })
8788
8910
  ] });
8911
+ const newItem = () => go({ itemId: null, mode: "new", kind: null, panel: null });
8912
+ const pageHeader = /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-col gap-2", "data-vault-page-header": "", children: [
8913
+ compactHeader,
8914
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex items-center gap-2", children: [
8915
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
8916
+ import_controls35.SearchField,
8917
+ {
8918
+ value: loc.q,
8919
+ onChange: (e) => {
8920
+ keepSearchFocus.current = true;
8921
+ go({ q: e.target.value }, "replace");
8922
+ },
8923
+ placeholder: "Search the vault",
8924
+ "aria-label": "Search the vault",
8925
+ className: "min-w-0 flex-1"
8926
+ }
8927
+ ),
8928
+ compactActions,
8929
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_controls35.Button, { variant: "primary", icon: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(PlusGlyph, {}), "aria-label": "New item", onClick: newItem })
8930
+ ] })
8931
+ ] });
8789
8932
  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 };
8790
8933
  const pageView = loc.view === "page" && host.page && !loc.q ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
8791
8934
  PageView,
@@ -8793,8 +8936,6 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8793
8936
  port,
8794
8937
  page: host.page,
8795
8938
  fill: host.fill,
8796
- capture: host.capture,
8797
- fillSetup: host.fillSetup,
8798
8939
  newPasswordFields: host.newPasswordFields,
8799
8940
  items: inThisVault,
8800
8941
  definitions,
@@ -8802,10 +8943,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8802
8943
  onNewForPage: (url) => go({ mode: "new", kind: null, url, itemId: null, panel: null }),
8803
8944
  onBrowse: () => go({ view: "all", itemId: null }),
8804
8945
  onChanged: load,
8805
- ...compactHeader ? { header: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-col gap-2", children: [
8806
- compactHeader,
8807
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex justify-end", children: compactActions })
8808
- ] }) } : {}
8946
+ header: pageHeader
8809
8947
  }
8810
8948
  ) : null;
8811
8949
  const specialView = pageView ?? (loc.view === "authenticator" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(AuthenticatorView, { ...viewProps }) : loc.view === "trash" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(TrashView, { ...viewProps }) : null);
@@ -8820,11 +8958,15 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8820
8958
  selectedId: loc.itemId,
8821
8959
  onSelect: (id) => go({ itemId: id, mode: "view", kind: null, panel: null }),
8822
8960
  query: loc.q,
8823
- onQuery: (q) => go({ q }, "replace"),
8961
+ onQuery: (q) => {
8962
+ if (loc.view === "page" && host.page && !q) keepSearchFocus.current = true;
8963
+ go({ q }, "replace");
8964
+ },
8824
8965
  sort: loc.sort,
8825
8966
  onSort: (sort) => go({ sort }, "replace"),
8826
8967
  canSortViewed: port.lists.recents !== void 0,
8827
- onNew: () => go({ itemId: null, mode: "new", kind: null, panel: null }),
8968
+ onNew: newItem,
8969
+ onItemAction: rowAction,
8828
8970
  onRefresh: refresh,
8829
8971
  refreshing,
8830
8972
  ...snap.pending && snap.pending.length > 0 ? { stillLoading: snap.pending } : {},
@@ -8899,13 +9041,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8899
9041
  setNotice(n);
8900
9042
  notify("Saved");
8901
9043
  },
8902
- onDeleted: (id) => {
8903
- setItems((cur) => cur.filter((i) => i.id !== id));
8904
- dirty.current = false;
8905
- move({ ...loc, itemId: null, mode: "view", panel: null }, "push");
8906
- notify("Moved to Trash");
8907
- load();
8908
- },
9044
+ onDeleted: deletedItem,
8909
9045
  onDirty: (d) => {
8910
9046
  dirty.current = d;
8911
9047
  },
@@ -8966,6 +9102,13 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8966
9102
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "min-w-0 flex-1 truncate text-warning", title: refreshError, children: "Couldn\u2019t refresh. Showing what was loaded." }),
8967
9103
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", className: "shrink-0 text-primary hover:underline", onClick: refresh, children: "Try again" })
8968
9104
  ] }) : null,
9105
+ hasPrompts ? (
9106
+ // "Save this login?" and "Turn on filling" show above the list in every view, not only "This page".
9107
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-col gap-3 border-b border-border px-3 py-3", "data-vault-host-prompts": "", children: [
9108
+ host.fillSetup ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(FillSetupCard, { fillSetup: host.fillSetup }) : null,
9109
+ host.capture ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(CaptureCard, { capture: host.capture, onChanged: load }) : null
9110
+ ] })
9111
+ ) : null,
8969
9112
  snap.unavailable.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { role: "status", className: "flex items-center gap-3 border-b border-border bg-muted px-4 py-2 text-sm", children: [
8970
9113
  /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "min-w-0 flex-1 truncate text-warning", children: [
8971
9114
  "Couldn\u2019t load ",
@@ -8991,7 +9134,9 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8991
9134
  onPanel: (p) => go({ panel: p }),
8992
9135
  nativeProvider: host.nativeProvider,
8993
9136
  hasNativeImport: host.nativeImport !== void 0,
8994
- hasPage: host.page !== void 0
9137
+ hasPage: host.page !== void 0,
9138
+ onLock: host.lock?.lock ? () => void host.lock?.lock?.() : void 0,
9139
+ onOpenWeb: host.openWebVault
8995
9140
  }
8996
9141
  ) : null,
8997
9142
  shown.list ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: specialView && mode !== "stack" ? "col-span-2 min-h-0" : "min-h-0", children: list }) : null,
@@ -9964,20 +10109,6 @@ function createApiVaultPort(hostApi, options = {}) {
9964
10109
  };
9965
10110
  }
9966
10111
 
9967
- // src/knobs.ts
9968
- var import_knobs = require("@ai-matrx/data/knobs");
9969
- var FRESH_MS = 6e4;
9970
- function createVaultKnobs(client, address) {
9971
- const reader = (0, import_knobs.createKnobReader)(client);
9972
- let kept = null;
9973
- return async () => {
9974
- if (kept && Date.now() - kept.at < FRESH_MS) return kept.resolved;
9975
- const snap = await reader.snapshot(address);
9976
- kept = { at: Date.now(), resolved: snap.resolved };
9977
- return snap.resolved;
9978
- };
9979
- }
9980
-
9981
10112
  // src/agent-context.ts
9982
10113
  var hostOf = (url) => {
9983
10114
  try {