@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.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");
@@ -2663,9 +2684,12 @@ function normalizeLoginUrl(input) {
2663
2684
  return null;
2664
2685
  }
2665
2686
  }
2687
+ function normalizeLoginUrls(inputs) {
2688
+ return inputs.map((u) => u.trim()).filter(Boolean).map((u) => normalizeLoginUrl(u) ?? u);
2689
+ }
2666
2690
  function describeDestination(r) {
2667
2691
  if (r.state === "reachable") return { url: r.normalizedUrl, state: r.state, tone: "success", message: "Website confirmed" };
2668
- if (r.state === "invalid") return { url: r.normalizedUrl, state: r.state, tone: "destructive", message: "Not a public website" };
2692
+ if (r.state === "invalid") return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Not a public website. You can still save it." };
2669
2693
  if (r.state === "not_found") return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Site answered, but this page was not found" };
2670
2694
  return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Couldn\u2019t confirm it. You can still save it." };
2671
2695
  }
@@ -3146,6 +3170,27 @@ function importLimitsFrom(resolved) {
3146
3170
  return out;
3147
3171
  }
3148
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
+
3149
3194
  // src/lock.ts
3150
3195
  var import_react16 = require("react");
3151
3196
  var LOCKED_MESSAGE = "The vault is locked. Unlock it first.";
@@ -3341,7 +3386,8 @@ function Omissions({ omissions, accepted, onAccepted, verb }) {
3341
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` }) })
3342
3387
  ] });
3343
3388
  }
3344
- function BackupPanel({ port, items, onClose, onChanged }) {
3389
+ function BackupPanel({ port, host, items, onClose, onChanged }) {
3390
+ const minLength = passphraseMinimum(host.backupPassphraseMinLength);
3345
3391
  const confirmed = useConfirmed();
3346
3392
  const fileInput = (0, import_react18.useRef)(null);
3347
3393
  const abort = (0, import_react18.useRef)(null);
@@ -3396,7 +3442,7 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3396
3442
  setAccepted(false);
3397
3443
  });
3398
3444
  const downloadBackup = () => guarded(async () => {
3399
- if (!backup) return;
3445
+ if (!backup || tooShort(passphrase, minLength)) return;
3400
3446
  const bytes2 = await confirmed(() => port.io.downloadBackup({ itemIds: [...selected], revision: backup.revision, passphrase, acceptOmissions: accepted }));
3401
3447
  (0, import_download.downloadFile)(BACKUP_FILENAME, bytes2, "application/octet-stream");
3402
3448
  setPassphrase("");
@@ -3443,10 +3489,11 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3443
3489
  };
3444
3490
  const retryable = (result?.retryable_failure ?? 0) > 0;
3445
3491
  const mismatch = repeat !== "" && repeat !== passphrase;
3492
+ const short = passphrase !== "" && tooShort(passphrase, minLength);
3446
3493
  const names = new Map(restorePreview?.records.map((r) => [r.source_item_id, r.display_name]) ?? []);
3447
3494
  const footer = mode === "backup" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
3448
3495
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_controls13.Button, { variant: "quiet", onClick: onClose, children: "Close" }),
3449
- 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: [
3450
3497
  "Review ",
3451
3498
  selected.size
3452
3499
  ] })
@@ -3493,7 +3540,14 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3493
3540
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Summary, { value: backup }),
3494
3541
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Omissions, { omissions: backup.omissions, accepted, onAccepted: setAccepted, verb: "Back up" }),
3495
3542
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(VaultSection, { children: [
3496
- /* @__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,
3497
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) }) })
3498
3552
  ] })
3499
3553
  ] }) : null
@@ -3526,6 +3580,12 @@ function BackupPanel({ port, items, onClose, onChanged }) {
3526
3580
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ErrorLine, { message: error })
3527
3581
  ] });
3528
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
+ }
3529
3589
 
3530
3590
  // src/io/ExportPanel.tsx
3531
3591
  var import_react19 = require("react");
@@ -5368,6 +5428,40 @@ function AssignResult({ sent, onAnother, onDone }) {
5368
5428
  ] });
5369
5429
  }
5370
5430
 
5431
+ // src/lists/turn-back-on.ts
5432
+ async function turnBackOn(port, item, fields, options = {}) {
5433
+ const run = options.run ?? ((op) => op());
5434
+ const done = [];
5435
+ if (item.turnedOff) {
5436
+ await run(() => port.fields.updateItem(item, { active: true }));
5437
+ done.push("the item");
5438
+ }
5439
+ let n = 0;
5440
+ for (const f of fields) {
5441
+ if (f.active) continue;
5442
+ await run(() => port.fields.updateField(item, f.id, { active: true }));
5443
+ n += 1;
5444
+ }
5445
+ if (n) done.push(n === 1 ? "1 field" : `${n} fields`);
5446
+ if (fields.some((f) => f.key === OTP_SEED_KEY)) {
5447
+ const codes = await port.authenticator.get(item);
5448
+ if (codes && !codes.enabled) {
5449
+ await run(() => port.authenticator.setEnabled(item, true));
5450
+ done.push("one-time codes");
5451
+ }
5452
+ }
5453
+ if (options.browserFill && !item.browserFillEnabled && item.loginUrls.length > 0) {
5454
+ await run(() => port.fields.updateItem(item, { browserFillEnabled: true }));
5455
+ done.push("browser fill");
5456
+ }
5457
+ return done;
5458
+ }
5459
+ function turnedOnSentence(done) {
5460
+ if (done.length === 0) return "Nothing here was off.";
5461
+ const list = done.length === 1 ? done[0] : `${done.slice(0, -1).join(", ")} and ${done[done.length - 1]}`;
5462
+ return `Turned on ${list}.`;
5463
+ }
5464
+
5371
5465
  // src/ItemPane.tsx
5372
5466
  var import_icons11 = require("@ai-matrx/design-system/icons");
5373
5467
  var import_drafts = require("@ai-matrx/kit/drafts");
@@ -5949,6 +6043,9 @@ var import_jsx_runtime27 = require("react/jsx-runtime");
5949
6043
  var ENV_VALUE_KEY = "env_value";
5950
6044
  var secureNoteDraft = () => ({ key: SECURE_NOTE_KEY, label: "Secure note", value: "", multiline: true, description: "Encrypted notes" });
5951
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
+ }
5952
6049
  function ItemPane(props) {
5953
6050
  const { port, item, definition, mode, host, organizations, defaultOwner, seed, onBack, onMode, onSaved, onDeleted, onDirty, onCancelNew, onPanel, onChanged, favorite, notice, refreshKey } = props;
5954
6051
  const confirmed = useConfirmed();
@@ -5981,7 +6078,8 @@ function ItemPane(props) {
5981
6078
  const [showBrowser, setShowBrowser] = (0, import_react28.useState)(false);
5982
6079
  const [error, setError] = (0, import_react28.useState)(null);
5983
6080
  const [destinations, setDestinations] = (0, import_react28.useState)({});
5984
- const dirty = editing && isDirty(base, draft);
6081
+ const [savedNew, setSavedNew] = (0, import_react28.useState)(false);
6082
+ const dirty = editing && !savedNew && isDirty(base, draft);
5985
6083
  const declared = definition?.fields ?? [];
5986
6084
  const prev = (0, import_react28.useRef)(null);
5987
6085
  const paneRef = (0, import_react28.useRef)(null);
@@ -6004,8 +6102,11 @@ function ItemPane(props) {
6004
6102
  setShowTags(false);
6005
6103
  setShowDescription(false);
6006
6104
  setShowRecovery(false);
6105
+ setSavedNew(false);
6007
6106
  }, [item?.id, mode]);
6008
- (0, import_react28.useEffect)(() => onDirty(dirty), [dirty, onDirty]);
6107
+ const onDirtyRef = (0, import_react28.useRef)(onDirty);
6108
+ onDirtyRef.current = onDirty;
6109
+ (0, import_react28.useEffect)(() => onDirtyRef.current(dirty), [dirty]);
6009
6110
  const keepKey = draftKey(item?.id ?? null, definition?.key ?? item?.definitionKey ?? null);
6010
6111
  (0, import_react28.useEffect)(() => {
6011
6112
  if (editing && dirty) (0, import_drafts.keepDraft)(keepKey, keptFieldsOf(draft), { basedOn: item?.updatedAt ?? "new" });
@@ -6062,6 +6163,8 @@ function ItemPane(props) {
6062
6163
  const [withCode, n0, enrolled] = await withOtp(r.item, r.notice);
6063
6164
  const [next, n] = await storeQueuedFiles(withCode, n0);
6064
6165
  (0, import_drafts.dropKeptDraft)(keepKey);
6166
+ setSavedNew(true);
6167
+ onDirty(false);
6065
6168
  onSaved(next, true, n, enrolled);
6066
6169
  }
6067
6170
  } catch (e) {
@@ -6095,8 +6198,7 @@ function ItemPane(props) {
6095
6198
  }
6096
6199
  async function remove() {
6097
6200
  if (!item) return;
6098
- 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" });
6099
- if (!ok) return;
6201
+ if (!await confirmDelete(item)) return;
6100
6202
  setBusy(true);
6101
6203
  try {
6102
6204
  await port.remove(item);
@@ -6124,12 +6226,7 @@ function ItemPane(props) {
6124
6226
  if (!item) return;
6125
6227
  setError(null);
6126
6228
  try {
6127
- if (item.turnedOff) await confirmed(() => port.fields.updateItem(item, { active: true }));
6128
- for (const f of fields) await confirmed(() => port.fields.updateField(item, f.id, { active: true }));
6129
- if (fields.some((f) => f.key === OTP_SEED_KEY)) {
6130
- const codes = await port.authenticator.get(item);
6131
- if (codes && !codes.enabled) await confirmed(() => port.authenticator.setEnabled(item, true));
6132
- }
6229
+ await turnBackOn(port, item, fields, { run: confirmed });
6133
6230
  onChanged();
6134
6231
  } catch (e) {
6135
6232
  if (!wasCancelled(e)) setError(e instanceof Error ? e.message : "It could not be turned on.");
@@ -6184,7 +6281,7 @@ function ItemPane(props) {
6184
6281
  "Unsaved"
6185
6282
  ] }) : null,
6186
6283
  /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { variant: "quiet", disabled: busy, onClick: () => mode === "new" ? onCancelNew() : onMode("view"), children: dirty ? "Discard" : "Cancel" }),
6187
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { 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" })
6284
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { 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" })
6188
6285
  ] }) : item ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "flex shrink-0 items-center gap-1", children: [
6189
6286
  host.page && canUseOnSite(item, host.page.url) ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(UseOnSiteButton, { port, item, pageUrl: host.page.url, onChanged: (next) => onChanged(next) }) : null,
6190
6287
  host.fill && item.capabilities.canUse ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
@@ -6352,7 +6449,8 @@ function ItemPane(props) {
6352
6449
  else delete next[i];
6353
6450
  return next;
6354
6451
  });
6355
- if (r?.url && r.url !== u) setDraft((d) => ({ ...d, loginUrls: d.loginUrls.map((x, j) => j === i && x === u ? r.url : x) }));
6452
+ const stored = r?.url || normalizeLoginUrl(u);
6453
+ if (stored && stored !== u) setDraft((d) => ({ ...d, loginUrls: d.loginUrls.map((x, j) => j === i && x === u ? stored : x) }));
6356
6454
  }),
6357
6455
  onRemove: () => patch({ loginUrls: draft.loginUrls.filter((_, j) => j !== i) }),
6358
6456
  removeLabel: "Remove website"
@@ -6511,7 +6609,7 @@ function subtitleOf(item, withOwner) {
6511
6609
  const what = loginSiteOf(item) || item.definitionLabel;
6512
6610
  return withOwner ? `${what} \xB7 ${item.owner.label}` : what;
6513
6611
  }
6514
- 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 }) {
6515
6613
  const sorts = SORTS2.filter((s) => canSortViewed || s.value !== "viewed");
6516
6614
  const pinned = matchingIds ? items.filter((i) => matchingIds.has(i.id)) : [];
6517
6615
  const rest = matchingIds ? items.filter((i) => !matchingIds.has(i.id)) : items;
@@ -6532,7 +6630,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6532
6630
  }
6533
6631
  };
6534
6632
  const mixedOwners = showOwner ?? new Set(items.map((i) => i.owner.organizationId ?? i.owner.kind)).size > 1;
6535
- 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);
6536
6634
  const sortLabel = sorts.find((s) => s.value === sort)?.label ?? "Name A\u2013Z";
6537
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: [
6538
6636
  /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-col gap-2 px-3 pb-2 pt-3", children: [
@@ -6576,7 +6674,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6576
6674
  ] }) })
6577
6675
  ] });
6578
6676
  }
6579
- function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu, extra, favorite }) {
6677
+ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu, extra, onAction, favorite }) {
6580
6678
  const confirmed = useConfirmed();
6581
6679
  const copy = useCopy();
6582
6680
  const notify = useNotify();
@@ -6617,11 +6715,12 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6617
6715
  ...favoriteEntry ? [favoriteEntry] : [],
6618
6716
  { id: "open", kind: "open", label: "Open", onSelect: () => onSelect(item.id) },
6619
6717
  { id: "copy-name", kind: "copy-name", label: "Copy name", onSelect: copyName },
6718
+ ...manageEntries(item, onAction),
6620
6719
  ...hostEntries.map((h) => ({ ...h, kind: h.kind ?? "host" }))
6621
6720
  ];
6622
6721
  const entriesRef = (0, import_react29.useRef)(entries);
6623
6722
  entriesRef.current = entries;
6624
- 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" });
6625
6724
  const menuItem = (e) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_design_system19.DropdownMenuItem, { disabled: e.disabled ?? false, onSelect: e.onSelect, children: [
6626
6725
  glyphOf(e),
6627
6726
  e.label
@@ -6675,6 +6774,8 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6675
6774
  copyEntries.map(menuItem),
6676
6775
  copyEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_design_system19.DropdownMenuSeparator, {}) : null,
6677
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),
6678
6779
  hostEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_design_system19.DropdownMenuSeparator, {}) : null,
6679
6780
  entries.filter((e) => e.kind === "host").map(menuItem)
6680
6781
  ] })
@@ -6684,6 +6785,17 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6684
6785
  }
6685
6786
  );
6686
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
+ }
6687
6799
 
6688
6800
  // src/lists/TrashView.tsx
6689
6801
  var import_react31 = require("react");
@@ -6764,8 +6876,16 @@ var import_react30 = require("react");
6764
6876
  var import_controls24 = require("@ai-matrx/design-system/controls");
6765
6877
  var import_jsx_runtime29 = require("react/jsx-runtime");
6766
6878
  var count = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
6767
- function RestorePanel({ lists, item, restore, onClose }) {
6879
+ function RestorePanel({
6880
+ lists,
6881
+ item,
6882
+ restore,
6883
+ turnOn,
6884
+ onClose
6885
+ }) {
6768
6886
  const [preview, setPreview] = (0, import_react30.useState)(null);
6887
+ const [restored, setRestored] = (0, import_react30.useState)(null);
6888
+ const [turnedOn, setTurnedOn] = (0, import_react30.useState)(null);
6769
6889
  const [error, setError] = (0, import_react30.useState)(null);
6770
6890
  const [busy, setBusy] = (0, import_react30.useState)(false);
6771
6891
  const [loading, setLoading] = (0, import_react30.useState)(Boolean(lists.preview));
@@ -6794,13 +6914,54 @@ function RestorePanel({ lists, item, restore, onClose }) {
6794
6914
  setError(null);
6795
6915
  try {
6796
6916
  const result = await restore(preview?.supported ? preview.deletionId : void 0);
6797
- if (result) onClose();
6917
+ if (!result) return;
6918
+ if (turnOn && !result.alreadyRestored && preview?.supported && !preview.priorWasDisabled) setRestored(result);
6919
+ else onClose();
6798
6920
  } catch (e) {
6799
6921
  setError(e instanceof Error && e.message ? e.message : "Restore did not complete. Try again.");
6800
6922
  } finally {
6801
6923
  setBusy(false);
6802
6924
  }
6803
6925
  };
6926
+ const runTurnOn = async () => {
6927
+ if (!restored || !turnOn) return;
6928
+ setBusy(true);
6929
+ setError(null);
6930
+ try {
6931
+ const said = await turnOn(restored.item);
6932
+ if (said !== null) setTurnedOn(said);
6933
+ } catch (e) {
6934
+ setError(e instanceof Error && e.message ? e.message : "It could not be turned on. Turn it on from the item.");
6935
+ } finally {
6936
+ setBusy(false);
6937
+ }
6938
+ };
6939
+ if (restored) {
6940
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
6941
+ Panel,
6942
+ {
6943
+ title: `Restored "${item.name}"`,
6944
+ size: "md",
6945
+ onClose,
6946
+ footer: turnedOn ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_controls24.Button, { variant: "primary", onClick: onClose, children: "Done" }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
6947
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_controls24.Button, { variant: "quiet", onClick: onClose, children: "Leave off" }),
6948
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_controls24.Button, { variant: "primary", icon: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(RestoreGlyph, {}), disabled: busy, onClick: () => void runTurnOn(), children: busy ? "Turning on\u2026" : "Turn these back on" })
6949
+ ] }),
6950
+ children: [
6951
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(VaultSection, { title: turnedOn ? "Turned on" : "Back, turned off", children: [
6952
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(VaultRow, { label: "Came back", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: "text-foreground", children: [
6953
+ count(restored.restoredFields, "field"),
6954
+ ", ",
6955
+ count(restored.restoredAttachments, "file")
6956
+ ] }) }),
6957
+ turnedOn ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(VaultRow, { label: "Now on", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { role: "status", className: "text-foreground", children: turnedOn }) }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(VaultRow, { label: "Turns on", hint: "One press, nothing else changes", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "text-muted-foreground", children: "The item, its fields, codes and browser fill" }) }),
6958
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(VaultRow, { label: "Stays off", hint: "Choose these again yourself", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "text-muted-foreground", children: "Sharing, sandbox use" }) })
6959
+ ] }),
6960
+ error ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { role: "alert", className: "px-1 text-sm text-destructive", children: error }) : null
6961
+ ]
6962
+ }
6963
+ );
6964
+ }
6804
6965
  return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
6805
6966
  Panel,
6806
6967
  {
@@ -6841,7 +7002,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6841
7002
  setMessage(null);
6842
7003
  try {
6843
7004
  const r = await confirmed(() => port.lists.restore(item, deletionId));
6844
- 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.` });
7005
+ setMessage({ tone: "ok", text: r.alreadyRestored ? `"${item.name}" was already restored.` : `Restored "${item.name}" with ${r.restoredFields} fields and ${r.restoredAttachments} files, turned off.` });
6845
7006
  onChanged();
6846
7007
  return r;
6847
7008
  } catch (e) {
@@ -6851,6 +7012,19 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6851
7012
  setBusyId(null);
6852
7013
  }
6853
7014
  };
7015
+ const turnOn = async (item) => {
7016
+ try {
7017
+ const done = await turnBackOn(port, item, item.fields, { browserFill: true, run: confirmed });
7018
+ const said = turnedOnSentence(done);
7019
+ setMessage({ tone: "ok", text: `"${item.name}": ${said}` });
7020
+ onChanged();
7021
+ return said;
7022
+ } catch (e) {
7023
+ onChanged();
7024
+ if (wasCancelled(e)) return null;
7025
+ throw e;
7026
+ }
7027
+ };
6854
7028
  return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3", children: [
6855
7029
  header,
6856
7030
  /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(VaultSection, { title: "Trash", children: shown.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(Empty, { children: deleted.length === 0 ? "The trash is empty." : "Nothing matches." }) : shown.map((item) => (
@@ -6864,7 +7038,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6864
7038
  /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_controls25.Button, { variant: "outline", icon: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(RestoreGlyph, {}), disabled: busyId !== null || !item.deletion, onClick: () => setRestoring(item), children: "Restore" })
6865
7039
  ] }, item.id)
6866
7040
  )) }),
6867
- restoring ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(RestorePanel, { lists: port.lists, item: restoring, restore: (deletionId) => restore(restoring, deletionId), onClose: () => setRestoring(null) }) : null,
7041
+ restoring ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(RestorePanel, { lists: port.lists, item: restoring, restore: (deletionId) => restore(restoring, deletionId), turnOn, onClose: () => setRestoring(null) }) : null,
6868
7042
  message2 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { role: "status", className: message2.tone === "error" ? "px-1 text-sm text-destructive" : "px-1 text-sm text-muted-foreground", children: message2.text }) : null
6869
7043
  ] });
6870
7044
  }
@@ -8158,22 +8332,10 @@ function MatchRow({ itemId, item, tag, family, port, busy, running, onOpen, onFi
8158
8332
  ] })
8159
8333
  ] }),
8160
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,
8161
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,
8162
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,
8163
- 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,
8164
- onSignIn ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_design_system24.DropdownMenu, { children: [
8165
- /* @__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 }) }),
8166
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_design_system24.DropdownMenuContent, { align: "end", children: [
8167
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_design_system24.DropdownMenuItem, { onSelect: onSignIn, children: [
8168
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ShieldGlyph, { className: "size-4" }),
8169
- "Sign me in"
8170
- ] }),
8171
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_design_system24.DropdownMenuItem, { onSelect: () => onOpen(itemId), children: [
8172
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ListGlyph, { className: "size-4" }),
8173
- "Open item"
8174
- ] })
8175
- ] })
8176
- ] }) : 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
8177
8339
  ] });
8178
8340
  }
8179
8341
 
@@ -8285,7 +8447,7 @@ function Group({ title, children }) {
8285
8447
  children
8286
8448
  ] });
8287
8449
  }
8288
- 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 }) {
8289
8451
  const [tagsOpen, setTagsOpen] = (0, import_react41.useState)(view.startsWith("tag:"));
8290
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: [
8291
8453
  /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2 px-3 pb-2 pt-3", children: [
@@ -8324,7 +8486,9 @@ function VaultSidebar({ vault, choices, onVault, view, allCount, deletedCount, v
8324
8486
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ShieldGlyph, { className: "size-4 shrink-0" }),
8325
8487
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "min-w-0 flex-1 truncate", children: "System AutoFill" }),
8326
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" })
8327
- ] }) : 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
8328
8492
  ] })
8329
8493
  ] });
8330
8494
  }
@@ -8367,12 +8531,42 @@ function ImportWithLimits(props) {
8367
8531
  if (state && "limits" in state) return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ImportPanel, { ...rest, limits: state.limits });
8368
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" }) });
8369
8533
  }
8370
- function VaultApp(props) {
8534
+ function VaultApp(given) {
8535
+ const props = { ...given, host: useOrganizationSettings(given.host ?? NO_HOST) };
8371
8536
  const lock = props.host?.lock;
8372
8537
  const locked = useLocked(lock);
8373
8538
  const port = (0, import_react42.useMemo)(() => lock ? guardPort(props.port, () => lock.locked()) : props.port, [props.port, lock]);
8374
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 }) }) }) }) }) }) }) });
8375
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
+ }
8376
8570
  function LockedScreen({ lock }) {
8377
8571
  const [busy, setBusy] = (0, import_react42.useState)(false);
8378
8572
  const [error, setError] = (0, import_react42.useState)(null);
@@ -8460,6 +8654,16 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8460
8654
  document.addEventListener("keydown", onKey);
8461
8655
  return () => document.removeEventListener("keydown", onKey);
8462
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);
8463
8667
  const knobsRef = (0, import_react42.useRef)(host.knobs);
8464
8668
  knobsRef.current = host.knobs;
8465
8669
  const [signedOut, setSignedOut] = (0, import_react42.useState)(false);
@@ -8602,6 +8806,25 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8602
8806
  notify(e instanceof Error ? e.message : "Favorites could not be saved.", "error");
8603
8807
  });
8604
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
+ };
8605
8828
  if (error) {
8606
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: [
8607
8830
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(SafeGlyph, { className: "size-8 text-muted-foreground" }),
@@ -8685,6 +8908,27 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8685
8908
  compactActions
8686
8909
  ] })
8687
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
+ ] });
8688
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 };
8689
8933
  const pageView = loc.view === "page" && host.page && !loc.q ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
8690
8934
  PageView,
@@ -8692,8 +8936,6 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8692
8936
  port,
8693
8937
  page: host.page,
8694
8938
  fill: host.fill,
8695
- capture: host.capture,
8696
- fillSetup: host.fillSetup,
8697
8939
  newPasswordFields: host.newPasswordFields,
8698
8940
  items: inThisVault,
8699
8941
  definitions,
@@ -8701,10 +8943,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8701
8943
  onNewForPage: (url) => go({ mode: "new", kind: null, url, itemId: null, panel: null }),
8702
8944
  onBrowse: () => go({ view: "all", itemId: null }),
8703
8945
  onChanged: load,
8704
- ...compactHeader ? { header: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-col gap-2", children: [
8705
- compactHeader,
8706
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex justify-end", children: compactActions })
8707
- ] }) } : {}
8946
+ header: pageHeader
8708
8947
  }
8709
8948
  ) : null;
8710
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);
@@ -8719,11 +8958,15 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8719
8958
  selectedId: loc.itemId,
8720
8959
  onSelect: (id) => go({ itemId: id, mode: "view", kind: null, panel: null }),
8721
8960
  query: loc.q,
8722
- onQuery: (q) => go({ q }, "replace"),
8961
+ onQuery: (q) => {
8962
+ if (loc.view === "page" && host.page && !q) keepSearchFocus.current = true;
8963
+ go({ q }, "replace");
8964
+ },
8723
8965
  sort: loc.sort,
8724
8966
  onSort: (sort) => go({ sort }, "replace"),
8725
8967
  canSortViewed: port.lists.recents !== void 0,
8726
- onNew: () => go({ itemId: null, mode: "new", kind: null, panel: null }),
8968
+ onNew: newItem,
8969
+ onItemAction: rowAction,
8727
8970
  onRefresh: refresh,
8728
8971
  refreshing,
8729
8972
  ...snap.pending && snap.pending.length > 0 ? { stillLoading: snap.pending } : {},
@@ -8798,13 +9041,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8798
9041
  setNotice(n);
8799
9042
  notify("Saved");
8800
9043
  },
8801
- onDeleted: (id) => {
8802
- setItems((cur) => cur.filter((i) => i.id !== id));
8803
- dirty.current = false;
8804
- move({ ...loc, itemId: null, mode: "view", panel: null }, "push");
8805
- notify("Moved to Trash");
8806
- load();
8807
- },
9044
+ onDeleted: deletedItem,
8808
9045
  onDirty: (d) => {
8809
9046
  dirty.current = d;
8810
9047
  },
@@ -8865,6 +9102,13 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8865
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." }),
8866
9103
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("button", { type: "button", className: "shrink-0 text-primary hover:underline", onClick: refresh, children: "Try again" })
8867
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,
8868
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: [
8869
9113
  /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "min-w-0 flex-1 truncate text-warning", children: [
8870
9114
  "Couldn\u2019t load ",
@@ -8890,7 +9134,9 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8890
9134
  onPanel: (p) => go({ panel: p }),
8891
9135
  nativeProvider: host.nativeProvider,
8892
9136
  hasNativeImport: host.nativeImport !== void 0,
8893
- 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
8894
9140
  }
8895
9141
  ) : null,
8896
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,
@@ -9129,7 +9375,7 @@ function createFieldsPort(ctx) {
9129
9375
  const body = { initiation: "user" };
9130
9376
  if (patch.uriMatchMode !== void 0) body.uri_match_mode = patch.uriMatchMode;
9131
9377
  if (patch.browserFillEnabled !== void 0) body.browser_fill_enabled = patch.browserFillEnabled;
9132
- if (patch.loginUrls !== void 0) body.login_urls = patch.loginUrls;
9378
+ if (patch.loginUrls !== void 0) body.login_urls = normalizeLoginUrls(patch.loginUrls);
9133
9379
  if (patch.active !== void 0) body.status = patch.active ? "active" : "disabled";
9134
9380
  await account3.updateItem(api, { path: { item_id: item.id }, body }, ctx.optsFor(item));
9135
9381
  return reread(item);
@@ -9141,6 +9387,33 @@ function createFieldsPort(ctx) {
9141
9387
  };
9142
9388
  }
9143
9389
 
9390
+ // src/server-reasons.ts
9391
+ var GENERIC = [/please check your input/i, /^\s*request failed\s*\(\d+\)\s*$/i, /^\s*bad request\.?\s*$/i, /^\s*HTTP \d+\s*$/i];
9392
+ var isGeneric = (s) => GENERIC.some((re) => re.test(s));
9393
+ function cleanReason(detail) {
9394
+ const s = detail.replace(/^\s*validation error:\s*/i, "").replace(/^\s*value error,\s*/i, "").trim();
9395
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
9396
+ }
9397
+ function serverReason(e) {
9398
+ if (!e || typeof e !== "object") return null;
9399
+ const { status, detail, message: message2 } = e;
9400
+ if (typeof status !== "number" || status < 400 || status >= 500) return null;
9401
+ if (typeof detail !== "string" || !detail.trim() || isGeneric(detail)) return null;
9402
+ const shown = typeof message2 === "string" ? message2 : "";
9403
+ if (shown && !isGeneric(shown)) return null;
9404
+ return cleanReason(detail);
9405
+ }
9406
+ function withReason(e) {
9407
+ const reason = serverReason(e);
9408
+ if (reason) Object.assign(e, { message: reason, userMessage: reason });
9409
+ return e;
9410
+ }
9411
+ function withServerReasons(api) {
9412
+ const wrapped = Object.create(api);
9413
+ wrapped.call = (...args) => api.call(...args).catch((e) => Promise.reject(withReason(e)));
9414
+ return wrapped;
9415
+ }
9416
+
9144
9417
  // src/io/api.ts
9145
9418
  var account4 = __toESM(require("@ai-matrx/api/account"), 1);
9146
9419
  var orgOpts = (organizationId) => organizationId ? { organizationId } : void 0;
@@ -9413,7 +9686,7 @@ function createSharingPort(ctx, options = {}) {
9413
9686
  definition_key: definition.key,
9414
9687
  provider_key: definition.providerKey,
9415
9688
  tags: draft.tags,
9416
- login_urls: draft.loginUrls,
9689
+ login_urls: normalizeLoginUrls(draft.loginUrls),
9417
9690
  notes: draft.notes || null,
9418
9691
  generate_field_key: generateFieldKey,
9419
9692
  initiation: "user",
@@ -9610,7 +9883,8 @@ async function retrying(run, tries = 3) {
9610
9883
  }
9611
9884
  }
9612
9885
  }
9613
- function createApiVaultPort(api, options = {}) {
9886
+ function createApiVaultPort(hostApi, options = {}) {
9887
+ const api = withServerReasons(hostApi);
9614
9888
  let defs = /* @__PURE__ */ new Map();
9615
9889
  let auth = /* @__PURE__ */ new Map();
9616
9890
  let orgs = [];
@@ -9765,6 +10039,7 @@ function createApiVaultPort(api, options = {}) {
9765
10039
  const declared = definition.fields;
9766
10040
  const encrypted = declared.filter((f) => f.storage === "encrypted" && (draft.values[f.key] ?? "") !== "");
9767
10041
  const meta = declared.filter((f) => f.storage === "metadata" && (draft.values[f.key] ?? "") !== "");
10042
+ const loginUrls = normalizeLoginUrls(draft.loginUrls);
9768
10043
  const w = await account9.createItem(api, {
9769
10044
  body: {
9770
10045
  display_name: draft.name.trim(),
@@ -9772,10 +10047,10 @@ function createApiVaultPort(api, options = {}) {
9772
10047
  definition_key: definition.key,
9773
10048
  provider_key: definition.providerKey,
9774
10049
  tags: draft.tags,
9775
- login_urls: draft.loginUrls,
10050
+ login_urls: loginUrls,
9776
10051
  // A login a person saves with its website is offered on that website (the server's own
9777
10052
  // default is off, which made "Save a login" on a page answer "No saved login" there).
9778
- ...draft.browser ? { uri_match_mode: draft.browser.uriMatchMode, browser_fill_enabled: draft.browser.browserFillEnabled } : draft.loginUrls.length > 0 ? { browser_fill_enabled: true } : {},
10053
+ ...draft.browser ? { uri_match_mode: draft.browser.uriMatchMode, browser_fill_enabled: draft.browser.browserFillEnabled } : loginUrls.length > 0 ? { browser_fill_enabled: true } : {},
9779
10054
  notes: draft.notes || null,
9780
10055
  principal: organizationId ? { type: "organization", organization_id: organizationId } : { type: "user" },
9781
10056
  initiation: "user",
@@ -9802,7 +10077,7 @@ function createApiVaultPort(api, options = {}) {
9802
10077
  else body.description = draft.description.trim();
9803
10078
  }
9804
10079
  if (JSON.stringify(draft.tags) !== JSON.stringify(item.tags)) body.tags = draft.tags;
9805
- if (JSON.stringify(draft.loginUrls) !== JSON.stringify(item.loginUrls)) body.login_urls = draft.loginUrls;
10080
+ if (JSON.stringify(draft.loginUrls) !== JSON.stringify(item.loginUrls)) body.login_urls = normalizeLoginUrls(draft.loginUrls);
9806
10081
  if (draft.notes !== item.notes) {
9807
10082
  if (draft.notes === "") body.clear_notes = true;
9808
10083
  else body.notes = draft.notes;
@@ -9834,20 +10109,6 @@ function createApiVaultPort(api, options = {}) {
9834
10109
  };
9835
10110
  }
9836
10111
 
9837
- // src/knobs.ts
9838
- var import_knobs = require("@ai-matrx/data/knobs");
9839
- var FRESH_MS = 6e4;
9840
- function createVaultKnobs(client, address) {
9841
- const reader = (0, import_knobs.createKnobReader)(client);
9842
- let kept = null;
9843
- return async () => {
9844
- if (kept && Date.now() - kept.at < FRESH_MS) return kept.resolved;
9845
- const snap = await reader.snapshot(address);
9846
- kept = { at: Date.now(), resolved: snap.resolved };
9847
- return snap.resolved;
9848
- };
9849
- }
9850
-
9851
10112
  // src/agent-context.ts
9852
10113
  var hostOf = (url) => {
9853
10114
  try {