@ai-matrx/vault 0.1.11 → 0.1.13

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
@@ -136,9 +136,11 @@ var init_keepass_xml_worker_client = __esm({
136
136
  var src_exports = {};
137
137
  __export(src_exports, {
138
138
  CATEGORIES: () => CATEGORIES,
139
+ CONFIRM_RETURN_KEY: () => CONFIRM_RETURN_KEY,
139
140
  DEFAULT_LOCATION: () => DEFAULT_LOCATION,
140
141
  GenerateButton: () => GenerateButton,
141
142
  HIDDEN_ITEM_TAG: () => HIDDEN_ITEM_TAG,
143
+ IdentityGate: () => IdentityGate,
142
144
  LOCKED_MESSAGE: () => LOCKED_MESSAGE,
143
145
  VaultApp: () => VaultApp,
144
146
  VaultLockedError: () => VaultLockedError,
@@ -156,7 +158,10 @@ __export(src_exports, {
156
158
  matchesQuery: () => matchesQuery,
157
159
  needsRecentSignIn: () => needsRecentSignIn,
158
160
  parseDefinitions: () => parseDefinitions,
159
- sortItems: () => sortItems
161
+ siteDomainOf: () => siteDomainOf,
162
+ sortItems: () => sortItems,
163
+ useConfirmed: () => useConfirmed,
164
+ wasCancelled: () => wasCancelled
160
165
  });
161
166
  module.exports = __toCommonJS(src_exports);
162
167
 
@@ -384,9 +389,20 @@ var FingerprintGlyph = glyph(/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(impor
384
389
  var SendGlyph = glyph(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M14.5 21.7a.5.5 0 0 0 .9 0L22 3a.5.5 0 0 0-.6-.6L2.3 9.1a.5.5 0 0 0 0 .9l7.9 3.2a2 2 0 0 1 1.1 1.1zM21.9 2.1 10.9 13.1" }), [1, 1]);
385
390
 
386
391
  // src/auth/identity.ts
392
+ var CONFIRM_RETURN_KEY = "matrx-vault:confirming-as";
393
+ var RETURN_WINDOW_MS = 15 * 6e4;
387
394
  var SAME_PERSON = "That signed in a different account. Use the account this vault belongs to.";
388
395
  function createSupabaseIdentity(auth, options = {}) {
389
396
  let me = null;
397
+ const now = options.now ?? Date.now;
398
+ const store = () => {
399
+ if (options.store) return options.store;
400
+ try {
401
+ return globalThis.sessionStorage ?? null;
402
+ } catch {
403
+ return null;
404
+ }
405
+ };
390
406
  const who = async () => {
391
407
  const { data } = await auth.getUser();
392
408
  if (!data.user) throw new Error("You are signed out. Sign in again.");
@@ -403,10 +419,35 @@ function createSupabaseIdentity(auth, options = {}) {
403
419
  return { email: u.email ?? null, providers: [...names].filter((p) => p !== "email" && p !== "phone"), hasPassword: names.has("email") };
404
420
  },
405
421
  async withProvider(provider) {
406
- await who();
422
+ const u = await who();
423
+ store()?.setItem(CONFIRM_RETURN_KEY, JSON.stringify({ id: u.id, email: u.email ?? null, at: now() }));
407
424
  const redirectTo = options.redirectTo?.() ?? globalThis.location?.href;
408
425
  const { error } = await auth.signInWithOAuth({ provider, ...redirectTo ? { options: { redirectTo } } : {} });
409
- if (error) throw new Error(error.message);
426
+ if (error) {
427
+ store()?.removeItem(CONFIRM_RETURN_KEY);
428
+ throw new Error(error.message);
429
+ }
430
+ },
431
+ async checkReturn() {
432
+ const s = store();
433
+ const raw = s?.getItem(CONFIRM_RETURN_KEY) ?? null;
434
+ if (!s || raw === null) return null;
435
+ s.removeItem(CONFIRM_RETURN_KEY);
436
+ let expected;
437
+ try {
438
+ expected = JSON.parse(raw);
439
+ } catch {
440
+ return null;
441
+ }
442
+ if (typeof expected.id !== "string" || typeof expected.at !== "number" || now() - expected.at > RETURN_WINDOW_MS) return null;
443
+ const { data } = await auth.getUser();
444
+ if (!data.user || data.user.id === expected.id) return null;
445
+ const email = typeof expected.email === "string" && expected.email ? expected.email : null;
446
+ const message2 = `That signed in a different account, so it was signed out here.${email ? ` Sign in again as ${email}.` : " Sign in again."}`;
447
+ me = null;
448
+ if (options.onDifferentPerson) await options.onDifferentPerson(message2);
449
+ else await auth.signOut?.({ scope: "local" });
450
+ return message2;
410
451
  },
411
452
  async withPassword(password) {
412
453
  const u = await who();
@@ -452,6 +493,18 @@ var providerLabel = (p) => PROVIDER_LABEL[p] ?? p.charAt(0).toUpperCase() + p.sl
452
493
  function IdentityGate({ identity, children }) {
453
494
  const [asking, setAsking] = (0, import_react2.useState)(false);
454
495
  const waiters = (0, import_react2.useRef)([]);
496
+ const [returnProblem, setReturnProblem] = (0, import_react2.useState)(null);
497
+ (0, import_react2.useEffect)(() => {
498
+ if (!identity?.checkReturn) return;
499
+ let live = true;
500
+ identity.checkReturn().then(
501
+ (problem) => live && problem && setReturnProblem(problem),
502
+ (e) => live && setReturnProblem(e instanceof Error ? e.message : "The account that came back could not be checked.")
503
+ );
504
+ return () => {
505
+ live = false;
506
+ };
507
+ }, [identity]);
455
508
  const confirmed = async (run) => {
456
509
  try {
457
510
  return await run();
@@ -472,7 +525,14 @@ function IdentityGate({ identity, children }) {
472
525
  };
473
526
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Ctx2.Provider, { value: confirmed, children: [
474
527
  children,
475
- identity ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ConfirmIdentityDialog, { open: asking, identity, onDone: () => settle(true), onCancel: () => settle(false) }) : null
528
+ identity ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ConfirmIdentityDialog, { open: asking, identity, onDone: () => settle(true), onCancel: () => settle(false) }) : null,
529
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_design_system2.Dialog, { open: returnProblem !== null, onOpenChange: (o) => o ? void 0 : setReturnProblem(null), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_design_system2.DialogContent, { size: "sm", children: [
530
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_design_system2.DialogHeader, { children: [
531
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_design_system2.DialogTitle, { children: "Different account" }),
532
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_design_system2.DialogDescription, { children: returnProblem })
533
+ ] }),
534
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Button, { variant: "primary", onClick: () => setReturnProblem(null), children: "OK" }) })
535
+ ] }) })
476
536
  ] });
477
537
  }
478
538
  var wasCancelled = (e) => e instanceof Error && e.message === CANCELLED;
@@ -536,7 +596,7 @@ function ConfirmIdentityDialog({ open, identity, onDone, onCancel }) {
536
596
  e.preventDefault();
537
597
  if (password) void attempt(() => identity.withPassword(password));
538
598
  }, children: [
539
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Field, { className: "min-w-0 flex-1", type: "password", autoComplete: "current-password", placeholder: "Your password", "aria-label": "Your password", value: password, onChange: (e) => setPassword(e.target.value) }),
599
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Field, { className: "min-w-0 flex-1", type: "password", autoComplete: "current-password", placeholder: "Your password", "aria-label": "Your password", value: password, onChange: (e) => setPassword(e.target.value), autoFocus: true }),
540
600
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Button, { variant: "outline", type: "submit", disabled: busy || !password, children: "Confirm" })
541
601
  ] }) : null,
542
602
  account10?.email ? codeSent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { className: "flex gap-2", onSubmit: (e) => {
@@ -664,6 +724,12 @@ function Panel({ title, open = true, onClose, children, footer, size = "lg" }) {
664
724
  footer ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_design_system3.DialogFooter, { children: footer }) : null
665
725
  ] }) });
666
726
  }
727
+ function PanelFooter({ status, children }) {
728
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex w-full flex-wrap items-center justify-end gap-2", children: [
729
+ status ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "min-w-0 flex-1 basis-full sm:basis-0", children: status }) : null,
730
+ children
731
+ ] });
732
+ }
667
733
  var DAY_FORMAT = { year: "numeric", month: "short", day: "numeric" };
668
734
 
669
735
  // src/FieldRows.tsx
@@ -674,7 +740,7 @@ function SecretText({ value, multiline }) {
674
740
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "break-all font-mono text-sm tracking-wide", children: [...value].map((ch, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: /[0-9]/.test(ch) ? "text-primary" : /[^A-Za-z0-9]/.test(ch) ? "text-warning" : void 0, children: ch }, i)) });
675
741
  }
676
742
  var quietError = (e, fallback) => wasCancelled(e) ? null : e instanceof Error ? e.message : fallback;
677
- function SecretRow({ port, item, field, extra }) {
743
+ function SecretRow({ port, item, field, extra, onTurnOn }) {
678
744
  const confirmed = useConfirmed();
679
745
  const copyValue2 = useCopy();
680
746
  const secret = useTransientSecret();
@@ -683,7 +749,7 @@ function SecretRow({ port, item, field, extra }) {
683
749
  const [error, setError] = (0, import_react5.useState)(null);
684
750
  const sealed = field.handling === "sealed";
685
751
  const visible = field.handling === "visible";
686
- const off = field.active === false;
752
+ const off = field.active === false || item.turnedOff === true;
687
753
  const canSee = !sealed && !off && (visible ? item.capabilities.canUse : item.capabilities.canReveal);
688
754
  (0, import_react5.useEffect)(() => {
689
755
  if (!visible || !canSee) return;
@@ -728,10 +794,27 @@ function SecretRow({ port, item, field, extra }) {
728
794
  top: multiline && shown !== null,
729
795
  actions: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
730
796
  extra,
797
+ off && onTurnOn ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TurnOnButton, { label: field.label, onTurnOn }) : null,
731
798
  canSee && !visible ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Button, { variant: "quiet", icon: secret.value !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(EyeOffGlyph, {}) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(EyeGlyph, {}), "aria-label": secret.value !== null ? `Hide ${field.label}` : `Show ${field.label}`, disabled: busy, onClick: () => void toggle2() }) : null,
732
799
  canSee ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_icons2.CopyIcon, {}), "aria-label": `Copy ${field.label}`, disabled: busy, onClick: () => void copy() }) : null
733
800
  ] }),
734
- children: sealed ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Badge, { icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LockGlyph, {}), children: "Sealed" }) : off ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-muted-foreground", children: "Turned off" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "truncate text-destructive", title: error, children: error }) : shown !== null ? visible ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: (0, import_design_system5.cn)("block", multiline ? "whitespace-pre-wrap break-words" : "truncate"), children: shown || "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SecretText, { value: shown, multiline }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: (0, import_design_system5.cn)("block truncate text-muted-foreground", !visible && "tracking-widest"), children: visible ? "\u2026" : MASK })
801
+ children: sealed ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Badge, { icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LockGlyph, {}), children: "Never shown" }) : off ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-muted-foreground", children: "Turned off" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "truncate text-destructive", title: error, children: error }) : shown !== null ? visible ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: (0, import_design_system5.cn)("block", multiline ? "whitespace-pre-wrap break-words" : "truncate"), children: shown || "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SecretText, { value: shown, multiline }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: (0, import_design_system5.cn)("block truncate text-muted-foreground", !visible && "tracking-widest"), children: visible ? "\u2026" : MASK })
802
+ }
803
+ );
804
+ }
805
+ function TurnOnButton({ label, onTurnOn }) {
806
+ const [busy, setBusy] = (0, import_react5.useState)(false);
807
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
808
+ import_controls2.Button,
809
+ {
810
+ variant: "outline",
811
+ "aria-label": `Turn on ${label}`,
812
+ disabled: busy,
813
+ onClick: () => {
814
+ setBusy(true);
815
+ void onTurnOn().finally(() => setBusy(false));
816
+ },
817
+ children: "Turn on"
735
818
  }
736
819
  );
737
820
  }
@@ -870,14 +953,27 @@ function loginSiteOf(item) {
870
953
  return raw.replace(/^https?:\/\//, "").replace(/^www\./, "").split("/")[0] ?? "";
871
954
  }
872
955
  }
956
+ var TWO_PART_SUFFIX = /* @__PURE__ */ new Set(["co", "com", "org", "net", "gov", "edu", "ac", "ne", "or"]);
957
+ function siteDomainOf(host) {
958
+ const h = host.toLowerCase().replace(/\.$/, "");
959
+ if (!h || /^[\d.]+$/.test(h) || h.includes(":")) return h;
960
+ const labels = h.split(".").filter(Boolean);
961
+ if (labels.length <= 2) return labels.join(".");
962
+ const sld = labels[labels.length - 2] ?? "";
963
+ const tld = labels[labels.length - 1] ?? "";
964
+ const keep = TWO_PART_SUFFIX.has(sld) && tld.length <= 3 ? 3 : 2;
965
+ return labels.slice(-keep).join(".");
966
+ }
873
967
  function sortItems(items, sort) {
874
968
  const out = [...items];
875
969
  const byName = (a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
876
970
  if (sort === "name") return out.sort(byName);
877
- if (sort === "website") return out.sort((a, b) => {
878
- const x = loginSiteOf(a), y = loginSiteOf(b);
879
- return x && y ? x.localeCompare(y) || byName(a, b) : x ? -1 : y ? 1 : byName(a, b);
880
- });
971
+ if (sort === "website")
972
+ return out.sort((a, b) => {
973
+ const x = loginSiteOf(a), y = loginSiteOf(b);
974
+ if (!x || !y) return x ? -1 : y ? 1 : byName(a, b);
975
+ return siteDomainOf(x).localeCompare(siteDomainOf(y)) || x.localeCompare(y) || byName(a, b);
976
+ });
881
977
  if (sort === "updated") return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
882
978
  return out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
883
979
  }
@@ -1297,7 +1393,10 @@ function ActivityPanel({ item, port, onClose }) {
1297
1393
  {
1298
1394
  title: "Activity",
1299
1395
  onClose,
1300
- footer: hasMore ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_controls3.Button, { variant: "outline", disabled: loading, onClick: () => setLimit((n) => Math.min(MAX, n * 2)), children: loading ? "Loading\u2026" : "Show more" }) : void 0,
1396
+ footer: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1397
+ hasMore ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_controls3.Button, { variant: "outline", disabled: loading, onClick: () => setLimit((n) => Math.min(MAX, n * 2)), children: loading ? "Loading\u2026" : "Show more" }) : null,
1398
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_controls3.Button, { variant: "primary", onClick: onClose, children: "Done" })
1399
+ ] }),
1301
1400
  children: [
1302
1401
  error ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { role: "alert", className: "px-1 text-sm text-destructive", children: error }) : null,
1303
1402
  entries === null ? loading ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Empty, { children: "Reading activity\u2026" }) : null : entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Empty, { children: "Nothing has happened yet." }) : groupByDay(entries, now).map((day) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(VaultSection, { title: day.label, children: collapseRuns(
@@ -1380,7 +1479,10 @@ function ItemHistoryPanel({ item, port, arg, onClose, onChanged }) {
1380
1479
  {
1381
1480
  title: "Password history",
1382
1481
  onClose,
1383
- footer: page?.nextBefore ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_controls4.Button, { variant: "outline", disabled: more, onClick: () => void loadMore(), children: more ? "Loading\u2026" : "Show earlier" }) : void 0,
1482
+ footer: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1483
+ page?.nextBefore ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_controls4.Button, { variant: "outline", disabled: more, onClick: () => void loadMore(), children: more ? "Loading\u2026" : "Show earlier" }) : null,
1484
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_controls4.Button, { variant: "primary", onClick: onClose, children: "Done" })
1485
+ ] }),
1384
1486
  children: !eligible ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Empty, { children: "History is kept only for your own items." }) : loading && !page ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Empty, { children: "Reading history\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1385
1487
  error ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "alert", className: "px-1 text-sm text-destructive", children: error }) : null,
1386
1488
  notice ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "status", className: "px-1 text-sm text-success", children: notice }) : null,
@@ -1985,6 +2087,7 @@ var import_confirm_opener2 = require("@ai-matrx/kit/confirm-opener");
1985
2087
 
1986
2088
  // src/authenticator/port.ts
1987
2089
  var MFA_METHOD_KEY = "mfa_method";
2090
+ var OTP_SEED_KEY = "totp_seed";
1988
2091
  function mfaMethodOf(item) {
1989
2092
  const v = item.custom.find((c) => c.key === MFA_METHOD_KEY)?.value;
1990
2093
  return v === "sms" ? "sms" : v ? "push_or_other" : null;
@@ -2343,7 +2446,7 @@ function AuthenticatorView({ port, items, definitions, query, onOpen, onChanged,
2343
2446
  const total = items.filter((i) => i.authenticator && !i.deletion && !hidden.has(i.id)).length;
2344
2447
  const familyOf2 = (key) => definitions.find((d) => d.key === key)?.family ?? "generic";
2345
2448
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("section", { "aria-label": "One-time passwords", className: "flex h-full min-h-0 flex-col bg-background", children: [
2346
- header,
2449
+ header ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mx-auto w-full max-w-3xl px-3 pt-3 sm:px-4", children: header }) : null,
2347
2450
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-h-12 items-center justify-between gap-2 px-4 pb-1 pt-3", children: [
2348
2451
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-w-0 items-baseline gap-2", children: [
2349
2452
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h2", { className: "truncate text-sm font-semibold text-foreground", children: "One-time passwords" }),
@@ -2355,7 +2458,7 @@ function AuthenticatorView({ port, items, definitions, query, onOpen, onChanged,
2355
2458
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ShieldGlyph, { className: "size-6" }) }),
2356
2459
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm font-medium text-foreground", children: "No one-time passwords yet" }),
2357
2460
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_controls9.Button, { variant: "primary", icon: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PlusGlyph, {}), onClick: () => setAdding(true), children: "Add one-time password" })
2358
- ] }) : groups.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "px-4 py-10 text-center text-sm text-muted-foreground", children: "Nothing matches." }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 pb-6 pt-1 sm:px-4", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mx-auto flex w-full max-w-3xl flex-col gap-5", children: groups.map((g) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(VaultSection, { ...groups.length > 1 ? { title: g.label } : {}, className: "[&>div]:px-1", children: g.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2461
+ ] }) : groups.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "px-4 py-10 text-center text-sm text-muted-foreground", children: "Nothing matches." }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 pb-6 pt-1 sm:px-4", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mx-auto flex w-full max-w-3xl flex-col gap-5", children: groups.map((g) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(VaultSection, { title: g.label, className: "[&>div]:px-1", children: g.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2359
2462
  OtpListRow,
2360
2463
  {
2361
2464
  port,
@@ -2492,9 +2595,9 @@ function sandboxBlocker(envKeyDraft, savedEnvKey) {
2492
2595
  return null;
2493
2596
  }
2494
2597
  var HANDLING_WORDS = {
2495
- visible: { label: "Anyone", hint: "Anyone who can use it can show or copy it" },
2496
- revealable: { label: "On request", hint: "Showing or copying it needs reveal access" },
2497
- sealed: { label: "Sealed", hint: "No one can see it. Automation can still use it." }
2598
+ visible: { label: "Always shown", hint: "Anyone who can use it can show or copy it" },
2599
+ revealable: { label: "Hidden", hint: "Showing or copying it needs reveal access" },
2600
+ sealed: { label: "Never shown", hint: "No one can see it. Automation can still use it." }
2498
2601
  };
2499
2602
  function handlingChoices(current) {
2500
2603
  return current === "sealed" ? ["sealed"] : ["visible", "revealable", "sealed"];
@@ -2676,7 +2779,7 @@ function FieldSettings({ item, port, field, onClose, onChanged }) {
2676
2779
  }
2677
2780
  ) }),
2678
2781
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(VaultRow, { label: "Editable", hint: quietHint(field.editable ? "The value can be changed" : "The value is locked"), children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_controls10.Switch, { "aria-label": "Editable", checked: field.editable, disabled: locked, onCheckedChange: (v) => void update("editable", { editable: v }) }) }),
2679
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(VaultRow, { label: "Active", hint: quietHint(field.active ? "In use" : "Kept, but nothing uses it"), children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_controls10.Switch, { "aria-label": "Active", checked: field.active, disabled: locked, onCheckedChange: (v) => void update("active", { active: v }) }) })
2782
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(VaultRow, { label: "Turned on", hint: quietHint(field.active ? "In use" : "Kept, but nothing uses it"), children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_controls10.Switch, { "aria-label": "Turned on", checked: field.active, disabled: locked, onCheckedChange: (v) => void update("active", { active: v }) }) })
2680
2783
  ] }),
2681
2784
  error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Problem, { children: error }) : null
2682
2785
  ] });
@@ -2744,7 +2847,7 @@ function ItemSettingsPanel({ item, port, onClose, onChanged, onPanel }) {
2744
2847
  f.envKey
2745
2848
  ] }) : null,
2746
2849
  f.injectIntoSandbox ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-muted-foreground", children: " \xB7 Sandbox" }) : null,
2747
- !f.active ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-disabled-foreground", children: " \xB7 Off" }) : null
2850
+ !f.active ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-warning", children: " \xB7 Turned off" }) : null
2748
2851
  ] })
2749
2852
  },
2750
2853
  f.id
@@ -4686,7 +4789,7 @@ var import_jsx_runtime19 = require("react/jsx-runtime");
4686
4789
  function ItemIcon({ definitionKey, family, site, size = "md", inverted = false }) {
4687
4790
  const box = size === "lg" ? "size-10 rounded-xl text-base" : size === "md" ? "size-8 rounded-lg text-sm" : "size-6 rounded-md text-xs";
4688
4791
  const tone = inverted ? "bg-primary-foreground text-primary" : "bg-muted text-foreground";
4689
- const initial = site.replace(/^(app|my|login|accounts|signin|auth|console|portal|admin|www)\./, "").charAt(0).toUpperCase();
4792
+ const initial = siteDomainOf(site).charAt(0).toUpperCase();
4690
4793
  if (initial && /[A-Z0-9]/.test(initial)) {
4691
4794
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { "aria-hidden": true, className: (0, import_design_system12.cn)("flex shrink-0 items-center justify-center font-semibold", box, tone), children: initial });
4692
4795
  }
@@ -4715,7 +4818,7 @@ var TOO_BIG = "Files over 25 MB can't be stored.";
4715
4818
  var PROTECTION = [
4716
4819
  { value: "visible", label: "Open", title: "Anyone who can use the item can download it" },
4717
4820
  { value: "revealable", label: "Protected", title: "Downloading needs a recent sign-in" },
4718
- { value: "sealed", label: "Sealed", title: "Only automation can use it, never a person" }
4821
+ { value: "sealed", label: "Never shown", title: "Only automation can use it, never a person" }
4719
4822
  ];
4720
4823
  function FilesSection({ item, port, editing, onChanged }) {
4721
4824
  const confirmed = useConfirmed();
@@ -4782,7 +4885,7 @@ function FilesSection({ item, port, editing, onChanged }) {
4782
4885
  {
4783
4886
  label: a.label,
4784
4887
  hint: a.description || void 0,
4785
- actions: a.handling === "sealed" ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_controls16.Badge, { icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(LockGlyph, {}), children: "Sealed" }) : canDownload(item, a) ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_controls16.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_icons8.DownloadIcon, {}), "aria-label": `Download ${a.fileName}`, disabled: busy !== null, onClick: () => void download(a) }) : null,
4888
+ actions: a.handling === "sealed" ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_controls16.Badge, { icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(LockGlyph, {}), children: "Never shown" }) : canDownload(item, a) ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_controls16.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_icons8.DownloadIcon, {}), "aria-label": `Download ${a.fileName}`, disabled: busy !== null, onClick: () => void download(a) }) : null,
4786
4889
  children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(FileName, { file: a })
4787
4890
  },
4788
4891
  a.id
@@ -5004,7 +5107,7 @@ function RecoveryCodesRow({ item, field, port, onChanged }) {
5004
5107
  canSee ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_controls17.Button, { variant: "quiet", icon: codes ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(EyeOffGlyph, {}) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(EyeGlyph, {}), "aria-label": codes ? "Hide codes" : "Show codes", disabled: busy, onClick: () => void toggle2() }) : null,
5005
5108
  canEdit && !sealed ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_controls17.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PencilGlyph, {}), "aria-label": "Replace all codes", disabled: busy, onClick: () => setDraft("") }) : null
5006
5109
  ] }),
5007
- children: sealed ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_controls17.Badge, { icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(LockGlyph, {}), children: "Sealed" }) : codes ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("ul", { className: "flex flex-col gap-0.5", "aria-label": "Recovery codes", children: [
5110
+ children: sealed ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_controls17.Badge, { icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(LockGlyph, {}), children: "Never shown" }) : codes ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("ul", { className: "flex flex-col gap-0.5", "aria-label": "Recovery codes", children: [
5008
5111
  codes.map((code, i) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("li", { className: "flex min-h-7 items-center gap-1", children: [
5009
5112
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("code", { className: "min-w-0 flex-1 break-all font-mono text-sm text-foreground", children: code }),
5010
5113
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_controls17.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_icons9.CopyIcon, {}), "aria-label": `Copy code ${i + 1}`, onClick: () => void copyValue2(code, "Recovery code") }),
@@ -5327,10 +5430,7 @@ function MovePanel({ item, port, organizations, onClose, onChanged }) {
5327
5430
  title: "Move or copy",
5328
5431
  size: "md",
5329
5432
  onClose,
5330
- footer: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex w-full items-center gap-2", children: [
5331
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(StatusLine, { error, notice }),
5332
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_controls19.Button, { variant: "outline", className: "ml-auto", onClick: onClose, children: "Close" })
5333
- ] }),
5433
+ footer: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(PanelFooter, { status: error || notice ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(StatusLine, { error, notice }) : null, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_controls19.Button, { variant: "primary", onClick: onClose, children: "Done" }) }),
5334
5434
  children: nothing ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(VaultSection, { children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Empty, { children: passkey ? "A passkey stays where it was created." : "Only the owner can move or give this item." }) }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(VaultSection, { children: [
5335
5435
  canMove && moves.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5336
5436
  VaultRow,
@@ -5445,10 +5545,7 @@ function SharePanel({ item, port, organizations, onClose, onChanged }) {
5445
5545
  title: "Share",
5446
5546
  size: "md",
5447
5547
  onClose,
5448
- footer: /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex w-full items-center gap-2", children: [
5449
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(StatusLine, { error, notice }),
5450
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_controls20.Button, { variant: "primary", className: "ml-auto", onClick: onClose, children: "Done" })
5451
- ] }),
5548
+ footer: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(PanelFooter, { status: error || notice ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(StatusLine, { error, notice }) : null, children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_controls20.Button, { variant: "primary", onClick: onClose, children: "Done" }) }),
5452
5549
  children: [
5453
5550
  /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(VaultSection, { title: "Who has access", children: [
5454
5551
  isOrg ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(AccessLine, { glyph: BuildingGlyph, who: `${orgName} admins`, children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "px-2 text-sm text-muted-foreground", children: "Full access" }) }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(AccessLine, { glyph: PersonGlyph, who: "You", meta: "Owner", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "px-2 text-sm text-muted-foreground", children: "Full access" }) }),
@@ -5734,6 +5831,27 @@ function ItemPane(props) {
5734
5831
  if (!wasCancelled(e)) setError(e instanceof Error ? e.message : "The value could not be read.");
5735
5832
  }
5736
5833
  }
5834
+ const offFields = item ? item.fields.filter((f) => !f.active) : [];
5835
+ const itemOff = item?.turnedOff === true;
5836
+ const seedField = item?.fields.find((f) => f.key === OTP_SEED_KEY) ?? null;
5837
+ const canTurnOn = Boolean(item && canEdit);
5838
+ async function turnOn(fields) {
5839
+ if (!item) return;
5840
+ setError(null);
5841
+ try {
5842
+ if (item.turnedOff) await confirmed(() => port.fields.updateItem(item, { active: true }));
5843
+ for (const f of fields) await confirmed(() => port.fields.updateField(item, f.id, { active: true }));
5844
+ if (fields.some((f) => f.key === OTP_SEED_KEY)) {
5845
+ const codes = await port.authenticator.get(item);
5846
+ if (codes && !codes.enabled) await confirmed(() => port.authenticator.setEnabled(item, true));
5847
+ }
5848
+ onChanged();
5849
+ } catch (e) {
5850
+ if (!wasCancelled(e)) setError(e instanceof Error ? e.message : "It could not be turned on.");
5851
+ onChanged();
5852
+ }
5853
+ }
5854
+ const turnOnOne = canTurnOn ? (f) => () => turnOn([f]) : () => void 0;
5737
5855
  const declaredKeys = new Set(declared.map((f) => f.key));
5738
5856
  const extraFields = (item?.fields ?? []).filter((f) => !declaredKeys.has(f.key) && !draft.removed.includes(f.id) && !isAuthenticatorStorage(f.key));
5739
5857
  const metaValue = (key) => draft.values[key] ?? item?.custom.find((c) => c.key === key)?.value ?? "";
@@ -5742,6 +5860,10 @@ function ItemPane(props) {
5742
5860
  const isLogin = (item?.definitionKey ?? definition?.key) === "website_login" || (item?.definitionKey ?? definition?.key) === "username_password";
5743
5861
  const typeLabel = definition?.label ?? item?.definitionLabel ?? "";
5744
5862
  const shownOwner = mode === "new" ? owner === "me" ? { label: "Personal", glyph: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(PersonGlyph, {}) } : owner === "person" ? { label: "Someone else", glyph: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SendGlyph, {}) } : { label: organizations.find((o) => o.id === owner)?.name ?? "Organization", glyph: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(BuildingGlyph, {}) } : item ? { label: item.owner.label, glyph: item.owner.kind === "me" ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(PersonGlyph, {}) : item.owner.kind === "organization" ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(BuildingGlyph, {}) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_icons11.UsersIcon, {}) } : null;
5863
+ const sameNamed = (id) => {
5864
+ const name = organizations.find((o) => o.id === id)?.name.trim().toLowerCase();
5865
+ return Boolean(name) && organizations.filter((o) => o.name.trim().toLowerCase() === name).length > 1;
5866
+ };
5745
5867
  const tagsVisible = showTags || draft.tags.length > 0 || tagsText !== "";
5746
5868
  const descriptionVisible = showDescription || draft.description !== "";
5747
5869
  const header = /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("header", { className: "flex min-h-14 items-center gap-2 border-b border-border px-3 py-2 sm:gap-3 sm:px-5", children: [
@@ -5825,6 +5947,10 @@ function ItemPane(props) {
5825
5947
  header,
5826
5948
  error ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { role: "alert", className: "border-b border-border bg-muted px-5 py-2 text-sm text-destructive", children: error }) : null,
5827
5949
  notice && !editing ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { role: "status", className: "border-b border-border bg-muted px-5 py-2 text-sm text-warning", children: notice }) : null,
5950
+ item && !editing && (itemOff || offFields.length > 0) ? /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { role: "status", className: "flex items-center gap-3 border-b border-border bg-muted px-5 py-2 text-sm", children: [
5951
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "min-w-0 flex-1 truncate text-warning", children: offFields.length === 0 ? "This item is turned off" : offFields.length === 1 ? "1 field is turned off" : `${offFields.length} fields are turned off` }),
5952
+ canTurnOn ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TurnOnButton, { label: "all fields", onTurnOn: () => turnOn(offFields) }) : null
5953
+ ] }) : null,
5828
5954
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "min-h-0 flex-1 overflow-y-auto", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "mx-auto flex w-full max-w-3xl flex-col gap-5 px-3 py-4 sm:px-6 sm:py-6", children: editing ? /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
5829
5955
  /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(VaultSection, { children: [
5830
5956
  mode === "new" ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VaultRow, { label: "Save to", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
@@ -5837,11 +5963,11 @@ function ItemPane(props) {
5837
5963
  searchPlaceholder: "Find an organization",
5838
5964
  groups: [
5839
5965
  { heading: "Just you", options: ["me"] },
5840
- ...organizations.length > 0 ? [{ heading: "Organizations", options: organizations.map((o) => o.id) }] : [],
5966
+ ...organizations.length > 0 ? [{ heading: "Organizations", options: [...organizations].sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" }) || a.id.localeCompare(b.id)).map((o) => o.id) }] : [],
5841
5967
  { heading: "Send", options: ["person"] }
5842
5968
  ],
5843
5969
  getLabel: (v) => v === "me" ? "Personal" : v === "person" ? "Someone else" : organizations.find((o) => o.id === v)?.name ?? v,
5844
- getHint: (v) => v === "me" ? "Only you" : v === "person" ? "Sent by email" : null,
5970
+ getHint: (v) => v === "me" ? "Only you" : v === "person" ? "Sent by email" : sameNamed(v) ? `ID ${v.slice(0, 8)}` : null,
5845
5971
  renderIcon: (v) => v === "me" ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(PersonGlyph, { className: "size-4" }) : v === "person" ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SendGlyph, { className: "size-4" }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(BuildingGlyph, { className: "size-4" })
5846
5972
  }
5847
5973
  ) }) : null,
@@ -5916,7 +6042,7 @@ function ItemPane(props) {
5916
6042
  i
5917
6043
  )),
5918
6044
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AddRow, { label: draft.loginUrls.length === 0 ? "Website" : "", onAdd: () => patch({ loginUrls: [...draft.loginUrls, ""] }), children: draft.loginUrls.length === 0 ? "Add website" : "Add another website" }),
5919
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TotpEditRow, { value: draft.authenticator, onChange: (v) => patch({ authenticator: v }), hasExisting: Boolean(item?.authenticator), item, port, onEnrolled: onChanged }),
6045
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TotpEditRow, { value: draft.authenticator, onChange: (v) => patch({ authenticator: v }), hasExisting: Boolean(item?.authenticator || seedField), item, port, onEnrolled: onChanged }),
5920
6046
  item && isLogin && !item.authenticator ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TwoFactorMethodRow, { port, item, onChanged }) : null
5921
6047
  ] }),
5922
6048
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VaultSection, { children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(EditRow, { label: "Notes", value: draft.notes, multiline: true, placeholder: "Anything worth remembering", onChange: (v) => patch({ notes: v }) }) }),
@@ -5949,7 +6075,7 @@ function ItemPane(props) {
5949
6075
  ) : null,
5950
6076
  draft.secrets.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(CustomEditRow, { label: s.label, value: s.value, secret: true, onLabel: (v) => patch({ secrets: draft.secrets.map((x, j) => j === i ? { ...x, label: v } : x) }), onValue: (v) => patch({ secrets: draft.secrets.map((x, j) => j === i ? { ...x, value: v } : x) }), onRemove: () => patch({ secrets: draft.secrets.filter((_, j) => j !== i) }) }, `s${i}`)),
5951
6077
  extraCustom.map((c) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(CustomEditRow, { label: c.label, value: c.value, secret: false, onLabel: (v) => patch({ custom: draft.custom.map((x) => x.key === c.key ? { ...x, label: v } : x) }), onValue: (v) => patch({ custom: draft.custom.map((x) => x.key === c.key ? { ...x, value: v } : x) }), onRemove: () => patch({ custom: draft.custom.filter((x) => x.key !== c.key) }) }, c.key)),
5952
- !descriptionVisible && !tagsVisible && draft.secrets.length === 0 && extraCustom.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AddRow, { label: "Fields, tags", onAdd: null, children: "None" }) : null
6078
+ !descriptionVisible && !tagsVisible && draft.secrets.length === 0 && extraCustom.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AddRow, { label: "Tags", onAdd: () => setShowTags(true), children: "Add tags" }) : null
5953
6079
  ]
5954
6080
  }
5955
6081
  ),
@@ -5963,11 +6089,15 @@ function ItemPane(props) {
5963
6089
  }
5964
6090
  const field = item.fields.find((x) => x.key === f.key);
5965
6091
  if (isRecoveryCodesField(f)) return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(RecoveryCodesRow, { item, field, port, onChanged }, f.key);
5966
- return field ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SecretRow, { port, item, field }, field.id) : null;
6092
+ return field ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SecretRow, { port, item, field, onTurnOn: turnOnOne(field) }, field.id) : null;
5967
6093
  }),
5968
- extraFields.map((f) => isRecoveryCodesField(f) ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(RecoveryCodesRow, { item, field: f, port, onChanged }, f.id) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SecretRow, { port, item, field: f }, f.id)),
6094
+ extraFields.map((f) => isRecoveryCodesField(f) ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(RecoveryCodesRow, { item, field: f, port, onChanged }, f.id) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SecretRow, { port, item, field: f, onTurnOn: turnOnOne(f) }, f.id)),
5969
6095
  item.loginUrls.length > 0 ? item.loginUrls.map((u, i) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(PlainRow, { label: i === 0 ? "Website" : `Website ${i + 1}`, value: u, openUrl: host.openUrl }, `${u}-${i}`)) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AddRow, { label: "Website", onAdd: canEdit ? () => onMode("edit") : null, children: "Add website" }),
5970
- item.authenticator ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TotpRow, { port, item, onChanged }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AddRow, { label: "One-time password", onAdd: canEdit ? () => onMode("edit") : null, children: "Set up" }),
6096
+ item.authenticator ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TotpRow, { port, item, onChanged }) : seedField && !seedField.active ? (
6097
+ // The secret is still there, only off: turning it on brings the codes back. Never "Set up",
6098
+ // which would invite replacing a working two-factor secret.
6099
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VaultRow, { label: "One-time password", actions: canTurnOn ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TurnOnButton, { label: "One-time password", onTurnOn: () => turnOn([seedField]) }) : null, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "text-muted-foreground", children: "Turned off" }) })
6100
+ ) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AddRow, { label: "One-time password", onAdd: canEdit ? () => onMode("edit") : null, children: "Set up" }),
5971
6101
  !item.authenticator && item.custom.some((c) => c.key === MFA_METHOD_KEY && c.value) ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TwoFactorMethodRow, { port, item, onChanged }) : null
5972
6102
  ] }),
5973
6103
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VaultSection, { children: item.notes ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(PlainRow, { label: "Notes", value: item.notes, multiline: true }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AddRow, { label: "Notes", onAdd: canEdit ? () => onMode("edit") : null, children: "Add notes" }) }),
@@ -5999,7 +6129,7 @@ function subtitleOf(item, withOwner) {
5999
6129
  const what = loginSiteOf(item) || item.definitionLabel;
6000
6130
  return withOwner ? `${what} \xB7 ${item.owner.label}` : what;
6001
6131
  }
6002
- function ItemList({ title, items, definitions, port, selectedId, onSelect, query, onQuery, sort, onSort, onNew, matchingIds, header }) {
6132
+ function ItemList({ title, items, definitions, port, selectedId, onSelect, query, onQuery, sort, onSort, onNew, matchingIds, header, showOwner }) {
6003
6133
  const pinned = matchingIds ? items.filter((i) => matchingIds.has(i.id)) : [];
6004
6134
  const rest = matchingIds ? items.filter((i) => !matchingIds.has(i.id)) : items;
6005
6135
  const ordered = [...pinned, ...rest];
@@ -6018,7 +6148,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6018
6148
  listRef.current?.querySelector(`[data-item="${next.id}"]`)?.scrollIntoView({ block: "nearest" });
6019
6149
  }
6020
6150
  };
6021
- const mixedOwners = new Set(items.map((i) => i.owner.organizationId ?? i.owner.kind)).size > 1;
6151
+ const mixedOwners = showOwner ?? new Set(items.map((i) => i.owner.organizationId ?? i.owner.kind)).size > 1;
6022
6152
  const row = (i) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ListRow, { item: i, family: familyOf2(i.definitionKey), port, on: i.id === selectedId, onSelect, withOwner: mixedOwners }, i.id);
6023
6153
  const sortLabel = SORTS2.find((s) => s.value === sort)?.label ?? "Name";
6024
6154
  return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("section", { "aria-label": title, className: "flex h-full min-h-0 flex-col border-r border-border bg-background", children: [
@@ -6236,6 +6366,9 @@ function RestorePanel({ lists, item, restore, onClose }) {
6236
6366
 
6237
6367
  // src/lists/TrashView.tsx
6238
6368
  var import_jsx_runtime29 = require("react/jsx-runtime");
6369
+ function trashLineOf(item) {
6370
+ return [item.definitionLabel || null, item.owner.label, item.deletion?.deletedAt ? `Deleted ${(0, import_format9.formatAbsoluteDate)(item.deletion.deletedAt, DAY_FORMAT, "")}` : null].filter(Boolean).join(" \xB7 ");
6371
+ }
6239
6372
  function TrashView({ port, deleted, query, header, onChanged }) {
6240
6373
  const confirmed = useConfirmed();
6241
6374
  const [busyId, setBusyId] = (0, import_react30.useState)(null);
@@ -6247,7 +6380,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6247
6380
  setMessage(null);
6248
6381
  try {
6249
6382
  const r = await confirmed(() => port.lists.restore(item, deletionId));
6250
- setMessage({ tone: "ok", text: r.alreadyRestored ? `"${item.name}" was already restored.` : `Restored "${item.name}" with ${r.restoredFields} fields and ${r.restoredAttachments} files. It is turned off.` });
6383
+ 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.` });
6251
6384
  onChanged();
6252
6385
  return r;
6253
6386
  } catch (e) {
@@ -6265,7 +6398,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6265
6398
  /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex min-h-12 items-center gap-3 px-3 py-1.5 sm:px-4", children: [
6266
6399
  /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [
6267
6400
  /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "truncate text-sm text-foreground", children: item.name }),
6268
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "truncate text-xs text-muted-foreground", children: [item.deletion?.deletedAt ? `Deleted ${(0, import_format9.formatAbsoluteDate)(item.deletion.deletedAt, DAY_FORMAT, "")}` : null, item.owner.kind !== "me" ? item.owner.label : null].filter(Boolean).join(" \xB7 ") })
6401
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "truncate text-xs text-muted-foreground", children: trashLineOf(item) })
6269
6402
  ] }),
6270
6403
  /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_controls24.Button, { variant: "outline", icon: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(RestoreGlyph, {}), disabled: busyId !== null || !item.deletion, onClick: () => setRestoring(item), children: "Restore" })
6271
6404
  ] }, item.id)
@@ -6556,10 +6689,9 @@ function SecureLinkPanel({ item, port, onClose }) {
6556
6689
  title: "Send securely",
6557
6690
  size: "md",
6558
6691
  onClose,
6559
- footer: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "flex w-full flex-wrap items-center gap-2", children: [
6560
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "w-full sm:w-auto sm:min-w-0 sm:flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(StatusLine, { error, notice }) }),
6692
+ footer: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(PanelFooter, { status: error || notice ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(StatusLine, { error, notice }) : null, children: [
6561
6693
  sameChannelSent && !error ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_controls26.Badge, { tone: "warning", children: "Both by email" }) : null,
6562
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_controls26.Button, { variant: "quiet", className: "ml-auto", onClick: onClose, children: "Close" }),
6694
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_controls26.Button, { variant: "quiet", onClick: onClose, children: "Close" }),
6563
6695
  /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_controls26.Button, { variant: "primary", disabled: !canSend, onClick: () => void send(), children: busy === "send" ? "Sending\u2026" : "Send link" })
6564
6696
  ] }),
6565
6697
  children: [
@@ -7815,8 +7947,33 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7815
7947
  },
7816
7948
  [loc, move]
7817
7949
  );
7950
+ const keyState = (0, import_react41.useRef)({ loc, go });
7951
+ keyState.current = { loc, go };
7952
+ (0, import_react41.useEffect)(() => {
7953
+ const onKey = (e) => {
7954
+ if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey) return;
7955
+ const root = rootRef.current;
7956
+ const at = document.activeElement;
7957
+ if (!root || at && at !== document.body && !root.contains(at)) return;
7958
+ if (at instanceof HTMLElement && (at.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(at.tagName))) return;
7959
+ if (document.querySelector('[role="dialog"], [role="alertdialog"]')) return;
7960
+ const { loc: here, go: to } = keyState.current;
7961
+ if (e.key === "/") {
7962
+ const search = root.querySelector('input[aria-label^="Search"]');
7963
+ if (!search) return;
7964
+ e.preventDefault();
7965
+ search.focus();
7966
+ } else if (e.key === "Escape" && here.itemId && here.mode === "view" && !here.panel) {
7967
+ e.preventDefault();
7968
+ to({ itemId: null, mode: "view", panel: null });
7969
+ }
7970
+ };
7971
+ document.addEventListener("keydown", onKey);
7972
+ return () => document.removeEventListener("keydown", onKey);
7973
+ }, []);
7818
7974
  const knobsRef = (0, import_react41.useRef)(host.knobs);
7819
7975
  knobsRef.current = host.knobs;
7976
+ const [signedOut, setSignedOut] = (0, import_react41.useState)(false);
7820
7977
  const load = (0, import_react41.useCallback)(() => {
7821
7978
  setError(null);
7822
7979
  port.load().then((s) => {
@@ -7827,7 +7984,10 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7827
7984
  (rows) => setDeleted([...s.deleted, ...rows.filter((r) => !s.deleted.some((d) => d.id === r.id))]),
7828
7985
  () => void 0
7829
7986
  );
7830
- }).catch((e) => setError(e instanceof Error ? e.message : "The vault could not be loaded."));
7987
+ }).catch((e) => {
7988
+ setSignedOut(e?.status === 401);
7989
+ setError(e instanceof Error ? e.message : "The vault could not be loaded.");
7990
+ });
7831
7991
  port.lists.favorites?.list().then((ids) => setFavorites(new Set(ids)), () => void 0);
7832
7992
  knobsRef.current?.().catch(() => void 0);
7833
7993
  port.lists.recents?.list().then(setRecents, () => void 0);
@@ -7841,6 +8001,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7841
8001
  const orgs = snap?.organizations ?? [];
7842
8002
  const definitions = snap?.definitions ?? [];
7843
8003
  const inThisVault = (0, import_react41.useMemo)(() => items.filter((i) => inVault(i, loc.vault)), [items, loc.vault]);
8004
+ const vaultMixesOwners = new Set(inThisVault.map((i) => i.owner.organizationId ?? i.owner.kind)).size > 1;
7844
8005
  const views = (0, import_react41.useMemo)(() => buildViews(inThisVault, definitions, favorites, recents), [inThisVault, definitions, favorites, recents]);
7845
8006
  const visible = (0, import_react41.useMemo)(() => {
7846
8007
  const filtered = inThisVault.filter((i) => {
@@ -7881,8 +8042,11 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7881
8042
  return /* @__PURE__ */ (0, import_jsx_runtime39.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: [
7882
8043
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SafeGlyph, { className: "size-8 text-muted-foreground" }),
7883
8044
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "font-medium", children: "The vault couldn\u2019t open" }),
7884
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "max-w-xs text-muted-foreground", children: plainLoadError(error) }),
7885
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { type: "button", className: "text-primary hover:underline", onClick: load, children: "Try again" })
8045
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "max-w-xs text-muted-foreground", children: signedOut ? "You\u2019ve been signed out." : plainLoadError(error) }),
8046
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex gap-2", children: [
8047
+ signedOut && host.signIn ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_controls34.Button, { variant: "primary", onClick: host.signIn, children: "Sign in" }) : null,
8048
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_controls34.Button, { variant: signedOut && host.signIn ? "outline" : "primary", onClick: load, children: "Try again" })
8049
+ ] })
7886
8050
  ] });
7887
8051
  }
7888
8052
  if (!snap) {
@@ -7962,6 +8126,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7962
8126
  {
7963
8127
  title,
7964
8128
  items: visible,
8129
+ showOwner: vaultMixesOwners,
7965
8130
  definitions,
7966
8131
  port,
7967
8132
  selectedId: loc.itemId,
@@ -8363,6 +8528,7 @@ function createFieldsPort(ctx) {
8363
8528
  if (patch.uriMatchMode !== void 0) body.uri_match_mode = patch.uriMatchMode;
8364
8529
  if (patch.browserFillEnabled !== void 0) body.browser_fill_enabled = patch.browserFillEnabled;
8365
8530
  if (patch.loginUrls !== void 0) body.login_urls = patch.loginUrls;
8531
+ if (patch.active !== void 0) body.status = patch.active ? "active" : "disabled";
8366
8532
  await account3.updateItem(api, { path: { item_id: item.id }, body }, ctx.optsFor(item));
8367
8533
  return reread(item);
8368
8534
  },
@@ -8734,6 +8900,7 @@ async function readNativeImportLimits(api, organizationId) {
8734
8900
  var recoveryOf = (w) => w.lifecycle?.vault_recovery;
8735
8901
  var isDeleted = (w) => recoveryOf(w)?.state === "deleted";
8736
8902
  var MATCH_MODES2 = ["host", "domain", "exact", "never"];
8903
+ var FIELD_WORDS = { totp_seed: "One-time password key", authenticator_last_time_step: "Last code used" };
8737
8904
  var handlingOf = (v) => v === "visible" || v === "sealed" ? v : "revealable";
8738
8905
  function fieldOf(w, def) {
8739
8906
  const declared = def?.fields.find((f) => f.key === w.field_key);
@@ -8741,7 +8908,7 @@ function fieldOf(w, def) {
8741
8908
  return {
8742
8909
  id: w.id,
8743
8910
  key: w.field_key,
8744
- label: declared?.label ?? humanize(w.field_key),
8911
+ label: declared?.label ?? FIELD_WORDS[w.field_key] ?? humanize(w.field_key),
8745
8912
  kind: declared?.kind ?? (handling === "visible" ? "text" : "secret"),
8746
8913
  handling,
8747
8914
  editable: w.editable ?? true,
@@ -8774,6 +8941,7 @@ function itemOf(w, owner, defs, auth) {
8774
8941
  uriMatchMode: MATCH_MODES2.find((m) => m === w.uri_match_mode) ?? "host",
8775
8942
  browserFillEnabled: w.browser_fill_enabled ?? true,
8776
8943
  accessMode: w.access_mode ?? null,
8944
+ turnedOff: w.status === "disabled",
8777
8945
  ownerUserId: w.user_id ?? null,
8778
8946
  deletion: recovery?.state === "deleted" ? { deletionId: typeof recovery.deletion_id === "string" ? recovery.deletion_id : null, deletedAt: typeof recovery.deleted_at === "string" ? recovery.deleted_at : null } : null,
8779
8947
  updatedAt: w.updated_at,
@@ -8833,9 +9001,12 @@ function createApiVaultPort(api, options = {}) {
8833
9001
  const authP = retrying(() => apps2.listAuthenticators(api));
8834
9002
  const userP = retrying(() => account9.listItems(api, { query: { principal_type: "user" } }));
8835
9003
  const sharedP = retrying(() => account9.sharedWithMe(api));
9004
+ const quiet = (p) => void p.catch(() => void 0);
9005
+ [catalogP, authP, userP, sharedP].forEach(quiet);
8836
9006
  const org = options.organizations ? null : await retrying(() => platform.listMyOrganizations(api));
8837
9007
  orgs = [...new Map((options.organizations ?? (org?.organizations ?? []).filter((o) => !o.archived_at).map((o) => ({ id: o.id, name: o.name }))).map((o) => [o.id, o])).values()];
8838
9008
  const orgListsP = orgs.map((o) => retrying(() => account9.listItems(api, { query: { principal_type: "organization", organization_id: o.id } }, { organizationId: o.id })));
9009
+ orgListsP.forEach(quiet);
8839
9010
  const [catalog, mine] = await Promise.all([catalogP, userP]);
8840
9011
  const [authSettled, sharedSettled, ...orgSettled] = await Promise.allSettled([authP, sharedP, ...orgListsP]);
8841
9012
  const unavailable = [];