@ai-matrx/vault 0.1.11 → 0.1.12

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();
@@ -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,21 @@ 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 seedField = item?.fields.find((f) => f.key === OTP_SEED_KEY) ?? null;
5836
+ const canTurnOn = Boolean(item && canEdit);
5837
+ async function turnOn(fields) {
5838
+ if (!item || fields.length === 0) return;
5839
+ setError(null);
5840
+ try {
5841
+ for (const f of fields) await confirmed(() => port.fields.updateField(item, f.id, { active: true }));
5842
+ onChanged();
5843
+ } catch (e) {
5844
+ if (!wasCancelled(e)) setError(e instanceof Error ? e.message : "It could not be turned on.");
5845
+ onChanged();
5846
+ }
5847
+ }
5848
+ const turnOnOne = canTurnOn ? (f) => () => turnOn([f]) : () => void 0;
5737
5849
  const declaredKeys = new Set(declared.map((f) => f.key));
5738
5850
  const extraFields = (item?.fields ?? []).filter((f) => !declaredKeys.has(f.key) && !draft.removed.includes(f.id) && !isAuthenticatorStorage(f.key));
5739
5851
  const metaValue = (key) => draft.values[key] ?? item?.custom.find((c) => c.key === key)?.value ?? "";
@@ -5742,6 +5854,10 @@ function ItemPane(props) {
5742
5854
  const isLogin = (item?.definitionKey ?? definition?.key) === "website_login" || (item?.definitionKey ?? definition?.key) === "username_password";
5743
5855
  const typeLabel = definition?.label ?? item?.definitionLabel ?? "";
5744
5856
  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;
5857
+ const sameNamed = (id) => {
5858
+ const name = organizations.find((o) => o.id === id)?.name.trim().toLowerCase();
5859
+ return Boolean(name) && organizations.filter((o) => o.name.trim().toLowerCase() === name).length > 1;
5860
+ };
5745
5861
  const tagsVisible = showTags || draft.tags.length > 0 || tagsText !== "";
5746
5862
  const descriptionVisible = showDescription || draft.description !== "";
5747
5863
  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 +5941,10 @@ function ItemPane(props) {
5825
5941
  header,
5826
5942
  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
5943
  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,
5944
+ item && !editing && 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: [
5945
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "min-w-0 flex-1 truncate text-warning", children: offFields.length === 1 ? "1 field is turned off" : `${offFields.length} fields are turned off` }),
5946
+ canTurnOn ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TurnOnButton, { label: "all fields", onTurnOn: () => turnOn(offFields) }) : null
5947
+ ] }) : null,
5828
5948
  /* @__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
5949
  /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(VaultSection, { children: [
5830
5950
  mode === "new" ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VaultRow, { label: "Save to", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
@@ -5837,11 +5957,11 @@ function ItemPane(props) {
5837
5957
  searchPlaceholder: "Find an organization",
5838
5958
  groups: [
5839
5959
  { heading: "Just you", options: ["me"] },
5840
- ...organizations.length > 0 ? [{ heading: "Organizations", options: organizations.map((o) => o.id) }] : [],
5960
+ ...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
5961
  { heading: "Send", options: ["person"] }
5842
5962
  ],
5843
5963
  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,
5964
+ getHint: (v) => v === "me" ? "Only you" : v === "person" ? "Sent by email" : sameNamed(v) ? `ID ${v.slice(0, 8)}` : null,
5845
5965
  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
5966
  }
5847
5967
  ) }) : null,
@@ -5916,7 +6036,7 @@ function ItemPane(props) {
5916
6036
  i
5917
6037
  )),
5918
6038
  /* @__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 }),
6039
+ /* @__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
6040
  item && isLogin && !item.authenticator ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TwoFactorMethodRow, { port, item, onChanged }) : null
5921
6041
  ] }),
5922
6042
  /* @__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 +6069,7 @@ function ItemPane(props) {
5949
6069
  ) : null,
5950
6070
  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
6071
  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
6072
+ !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
6073
  ]
5954
6074
  }
5955
6075
  ),
@@ -5963,11 +6083,15 @@ function ItemPane(props) {
5963
6083
  }
5964
6084
  const field = item.fields.find((x) => x.key === f.key);
5965
6085
  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;
6086
+ return field ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SecretRow, { port, item, field, onTurnOn: turnOnOne(field) }, field.id) : null;
5967
6087
  }),
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)),
6088
+ 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
6089
  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" }),
6090
+ item.authenticator ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TotpRow, { port, item, onChanged }) : seedField && !seedField.active ? (
6091
+ // The secret is still there, only off: turning it on brings the codes back. Never "Set up",
6092
+ // which would invite replacing a working two-factor secret.
6093
+ /* @__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" }) })
6094
+ ) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AddRow, { label: "One-time password", onAdd: canEdit ? () => onMode("edit") : null, children: "Set up" }),
5971
6095
  !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
6096
  ] }),
5973
6097
  /* @__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 +6123,7 @@ function subtitleOf(item, withOwner) {
5999
6123
  const what = loginSiteOf(item) || item.definitionLabel;
6000
6124
  return withOwner ? `${what} \xB7 ${item.owner.label}` : what;
6001
6125
  }
6002
- function ItemList({ title, items, definitions, port, selectedId, onSelect, query, onQuery, sort, onSort, onNew, matchingIds, header }) {
6126
+ function ItemList({ title, items, definitions, port, selectedId, onSelect, query, onQuery, sort, onSort, onNew, matchingIds, header, showOwner }) {
6003
6127
  const pinned = matchingIds ? items.filter((i) => matchingIds.has(i.id)) : [];
6004
6128
  const rest = matchingIds ? items.filter((i) => !matchingIds.has(i.id)) : items;
6005
6129
  const ordered = [...pinned, ...rest];
@@ -6018,7 +6142,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6018
6142
  listRef.current?.querySelector(`[data-item="${next.id}"]`)?.scrollIntoView({ block: "nearest" });
6019
6143
  }
6020
6144
  };
6021
- const mixedOwners = new Set(items.map((i) => i.owner.organizationId ?? i.owner.kind)).size > 1;
6145
+ const mixedOwners = showOwner ?? new Set(items.map((i) => i.owner.organizationId ?? i.owner.kind)).size > 1;
6022
6146
  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
6147
  const sortLabel = SORTS2.find((s) => s.value === sort)?.label ?? "Name";
6024
6148
  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 +6360,9 @@ function RestorePanel({ lists, item, restore, onClose }) {
6236
6360
 
6237
6361
  // src/lists/TrashView.tsx
6238
6362
  var import_jsx_runtime29 = require("react/jsx-runtime");
6363
+ function trashLineOf(item) {
6364
+ 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 ");
6365
+ }
6239
6366
  function TrashView({ port, deleted, query, header, onChanged }) {
6240
6367
  const confirmed = useConfirmed();
6241
6368
  const [busyId, setBusyId] = (0, import_react30.useState)(null);
@@ -6247,7 +6374,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6247
6374
  setMessage(null);
6248
6375
  try {
6249
6376
  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.` });
6377
+ 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
6378
  onChanged();
6252
6379
  return r;
6253
6380
  } catch (e) {
@@ -6265,7 +6392,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6265
6392
  /* @__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
6393
  /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [
6267
6394
  /* @__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 ") })
6395
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "truncate text-xs text-muted-foreground", children: trashLineOf(item) })
6269
6396
  ] }),
6270
6397
  /* @__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
6398
  ] }, item.id)
@@ -6556,10 +6683,9 @@ function SecureLinkPanel({ item, port, onClose }) {
6556
6683
  title: "Send securely",
6557
6684
  size: "md",
6558
6685
  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 }) }),
6686
+ footer: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(PanelFooter, { status: error || notice ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(StatusLine, { error, notice }) : null, children: [
6561
6687
  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" }),
6688
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_controls26.Button, { variant: "quiet", onClick: onClose, children: "Close" }),
6563
6689
  /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_controls26.Button, { variant: "primary", disabled: !canSend, onClick: () => void send(), children: busy === "send" ? "Sending\u2026" : "Send link" })
6564
6690
  ] }),
6565
6691
  children: [
@@ -7815,8 +7941,33 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7815
7941
  },
7816
7942
  [loc, move]
7817
7943
  );
7944
+ const keyState = (0, import_react41.useRef)({ loc, go });
7945
+ keyState.current = { loc, go };
7946
+ (0, import_react41.useEffect)(() => {
7947
+ const onKey = (e) => {
7948
+ if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey) return;
7949
+ const root = rootRef.current;
7950
+ const at = document.activeElement;
7951
+ if (!root || at && at !== document.body && !root.contains(at)) return;
7952
+ if (at instanceof HTMLElement && (at.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(at.tagName))) return;
7953
+ if (document.querySelector('[role="dialog"], [role="alertdialog"]')) return;
7954
+ const { loc: here, go: to } = keyState.current;
7955
+ if (e.key === "/") {
7956
+ const search = root.querySelector('input[aria-label^="Search"]');
7957
+ if (!search) return;
7958
+ e.preventDefault();
7959
+ search.focus();
7960
+ } else if (e.key === "Escape" && here.itemId && here.mode === "view" && !here.panel) {
7961
+ e.preventDefault();
7962
+ to({ itemId: null, mode: "view", panel: null });
7963
+ }
7964
+ };
7965
+ document.addEventListener("keydown", onKey);
7966
+ return () => document.removeEventListener("keydown", onKey);
7967
+ }, []);
7818
7968
  const knobsRef = (0, import_react41.useRef)(host.knobs);
7819
7969
  knobsRef.current = host.knobs;
7970
+ const [signedOut, setSignedOut] = (0, import_react41.useState)(false);
7820
7971
  const load = (0, import_react41.useCallback)(() => {
7821
7972
  setError(null);
7822
7973
  port.load().then((s) => {
@@ -7827,7 +7978,10 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7827
7978
  (rows) => setDeleted([...s.deleted, ...rows.filter((r) => !s.deleted.some((d) => d.id === r.id))]),
7828
7979
  () => void 0
7829
7980
  );
7830
- }).catch((e) => setError(e instanceof Error ? e.message : "The vault could not be loaded."));
7981
+ }).catch((e) => {
7982
+ setSignedOut(e?.status === 401);
7983
+ setError(e instanceof Error ? e.message : "The vault could not be loaded.");
7984
+ });
7831
7985
  port.lists.favorites?.list().then((ids) => setFavorites(new Set(ids)), () => void 0);
7832
7986
  knobsRef.current?.().catch(() => void 0);
7833
7987
  port.lists.recents?.list().then(setRecents, () => void 0);
@@ -7841,6 +7995,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7841
7995
  const orgs = snap?.organizations ?? [];
7842
7996
  const definitions = snap?.definitions ?? [];
7843
7997
  const inThisVault = (0, import_react41.useMemo)(() => items.filter((i) => inVault(i, loc.vault)), [items, loc.vault]);
7998
+ const vaultMixesOwners = new Set(inThisVault.map((i) => i.owner.organizationId ?? i.owner.kind)).size > 1;
7844
7999
  const views = (0, import_react41.useMemo)(() => buildViews(inThisVault, definitions, favorites, recents), [inThisVault, definitions, favorites, recents]);
7845
8000
  const visible = (0, import_react41.useMemo)(() => {
7846
8001
  const filtered = inThisVault.filter((i) => {
@@ -7881,8 +8036,11 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7881
8036
  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
8037
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SafeGlyph, { className: "size-8 text-muted-foreground" }),
7883
8038
  /* @__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" })
8039
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "max-w-xs text-muted-foreground", children: signedOut ? "You\u2019ve been signed out." : plainLoadError(error) }),
8040
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex gap-2", children: [
8041
+ signedOut && host.signIn ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_controls34.Button, { variant: "primary", onClick: host.signIn, children: "Sign in" }) : null,
8042
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_controls34.Button, { variant: signedOut && host.signIn ? "outline" : "primary", onClick: load, children: "Try again" })
8043
+ ] })
7886
8044
  ] });
7887
8045
  }
7888
8046
  if (!snap) {
@@ -7962,6 +8120,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7962
8120
  {
7963
8121
  title,
7964
8122
  items: visible,
8123
+ showOwner: vaultMixesOwners,
7965
8124
  definitions,
7966
8125
  port,
7967
8126
  selectedId: loc.itemId,
@@ -8734,6 +8893,7 @@ async function readNativeImportLimits(api, organizationId) {
8734
8893
  var recoveryOf = (w) => w.lifecycle?.vault_recovery;
8735
8894
  var isDeleted = (w) => recoveryOf(w)?.state === "deleted";
8736
8895
  var MATCH_MODES2 = ["host", "domain", "exact", "never"];
8896
+ var FIELD_WORDS = { totp_seed: "One-time password key", authenticator_last_time_step: "Last code used" };
8737
8897
  var handlingOf = (v) => v === "visible" || v === "sealed" ? v : "revealable";
8738
8898
  function fieldOf(w, def) {
8739
8899
  const declared = def?.fields.find((f) => f.key === w.field_key);
@@ -8741,7 +8901,7 @@ function fieldOf(w, def) {
8741
8901
  return {
8742
8902
  id: w.id,
8743
8903
  key: w.field_key,
8744
- label: declared?.label ?? humanize(w.field_key),
8904
+ label: declared?.label ?? FIELD_WORDS[w.field_key] ?? humanize(w.field_key),
8745
8905
  kind: declared?.kind ?? (handling === "visible" ? "text" : "secret"),
8746
8906
  handling,
8747
8907
  editable: w.editable ?? true,