@ai-matrx/vault 0.1.10 → 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,8 +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,
142
+ HIDDEN_ITEM_TAG: () => HIDDEN_ITEM_TAG,
143
+ IdentityGate: () => IdentityGate,
141
144
  LOCKED_MESSAGE: () => LOCKED_MESSAGE,
142
145
  VaultApp: () => VaultApp,
143
146
  VaultLockedError: () => VaultLockedError,
@@ -148,13 +151,17 @@ __export(src_exports, {
148
151
  createVaultKnobs: () => createVaultKnobs,
149
152
  guardPort: () => guardPort,
150
153
  inferFromUrl: () => inferFromUrl,
154
+ isHiddenItem: () => isHiddenItem,
151
155
  locationFromParams: () => locationFromParams,
152
156
  locationToParams: () => locationToParams,
153
157
  loginSiteOf: () => loginSiteOf,
154
158
  matchesQuery: () => matchesQuery,
155
159
  needsRecentSignIn: () => needsRecentSignIn,
156
160
  parseDefinitions: () => parseDefinitions,
157
- sortItems: () => sortItems
161
+ siteDomainOf: () => siteDomainOf,
162
+ sortItems: () => sortItems,
163
+ useConfirmed: () => useConfirmed,
164
+ wasCancelled: () => wasCancelled
158
165
  });
159
166
  module.exports = __toCommonJS(src_exports);
160
167
 
@@ -382,9 +389,20 @@ var FingerprintGlyph = glyph(/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(impor
382
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]);
383
390
 
384
391
  // src/auth/identity.ts
392
+ var CONFIRM_RETURN_KEY = "matrx-vault:confirming-as";
393
+ var RETURN_WINDOW_MS = 15 * 6e4;
385
394
  var SAME_PERSON = "That signed in a different account. Use the account this vault belongs to.";
386
395
  function createSupabaseIdentity(auth, options = {}) {
387
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
+ };
388
406
  const who = async () => {
389
407
  const { data } = await auth.getUser();
390
408
  if (!data.user) throw new Error("You are signed out. Sign in again.");
@@ -401,10 +419,35 @@ function createSupabaseIdentity(auth, options = {}) {
401
419
  return { email: u.email ?? null, providers: [...names].filter((p) => p !== "email" && p !== "phone"), hasPassword: names.has("email") };
402
420
  },
403
421
  async withProvider(provider) {
404
- await who();
422
+ const u = await who();
423
+ store()?.setItem(CONFIRM_RETURN_KEY, JSON.stringify({ id: u.id, email: u.email ?? null, at: now() }));
405
424
  const redirectTo = options.redirectTo?.() ?? globalThis.location?.href;
406
425
  const { error } = await auth.signInWithOAuth({ provider, ...redirectTo ? { options: { redirectTo } } : {} });
407
- 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;
408
451
  },
409
452
  async withPassword(password) {
410
453
  const u = await who();
@@ -450,6 +493,18 @@ var providerLabel = (p) => PROVIDER_LABEL[p] ?? p.charAt(0).toUpperCase() + p.sl
450
493
  function IdentityGate({ identity, children }) {
451
494
  const [asking, setAsking] = (0, import_react2.useState)(false);
452
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]);
453
508
  const confirmed = async (run) => {
454
509
  try {
455
510
  return await run();
@@ -470,7 +525,14 @@ function IdentityGate({ identity, children }) {
470
525
  };
471
526
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Ctx2.Provider, { value: confirmed, children: [
472
527
  children,
473
- 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
+ ] }) })
474
536
  ] });
475
537
  }
476
538
  var wasCancelled = (e) => e instanceof Error && e.message === CANCELLED;
@@ -534,7 +596,7 @@ function ConfirmIdentityDialog({ open, identity, onDone, onCancel }) {
534
596
  e.preventDefault();
535
597
  if (password) void attempt(() => identity.withPassword(password));
536
598
  }, children: [
537
- /* @__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 }),
538
600
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Button, { variant: "outline", type: "submit", disabled: busy || !password, children: "Confirm" })
539
601
  ] }) : null,
540
602
  account10?.email ? codeSent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { className: "flex gap-2", onSubmit: (e) => {
@@ -662,6 +724,12 @@ function Panel({ title, open = true, onClose, children, footer, size = "lg" }) {
662
724
  footer ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_design_system3.DialogFooter, { children: footer }) : null
663
725
  ] }) });
664
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
+ }
665
733
  var DAY_FORMAT = { year: "numeric", month: "short", day: "numeric" };
666
734
 
667
735
  // src/FieldRows.tsx
@@ -672,7 +740,7 @@ function SecretText({ value, multiline }) {
672
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)) });
673
741
  }
674
742
  var quietError = (e, fallback) => wasCancelled(e) ? null : e instanceof Error ? e.message : fallback;
675
- function SecretRow({ port, item, field, extra }) {
743
+ function SecretRow({ port, item, field, extra, onTurnOn }) {
676
744
  const confirmed = useConfirmed();
677
745
  const copyValue2 = useCopy();
678
746
  const secret = useTransientSecret();
@@ -726,10 +794,27 @@ function SecretRow({ port, item, field, extra }) {
726
794
  top: multiline && shown !== null,
727
795
  actions: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
728
796
  extra,
797
+ off && onTurnOn ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TurnOnButton, { label: field.label, onTurnOn }) : null,
729
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,
730
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
731
800
  ] }),
732
- 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"
733
818
  }
734
819
  );
735
820
  }
@@ -856,6 +941,8 @@ function matchesQuery(item, query) {
856
941
  if (!q) return true;
857
942
  return [item.name, item.definitionLabel, ...item.tags, ...item.loginUrls, ...item.custom.map((c) => `${c.label} ${c.value}`)].some((h) => h.toLowerCase().includes(q));
858
943
  }
944
+ var HIDDEN_ITEM_TAG = "matrx:hidden";
945
+ var isHiddenItem = (item) => (item.tags ?? []).includes(HIDDEN_ITEM_TAG);
859
946
  var isAuthenticatorStorage = (key) => key === "totp_seed" || key.startsWith("totp_") || key === "authenticator_last_time_step";
860
947
  function loginSiteOf(item) {
861
948
  const raw = item.loginUrls[0];
@@ -866,14 +953,27 @@ function loginSiteOf(item) {
866
953
  return raw.replace(/^https?:\/\//, "").replace(/^www\./, "").split("/")[0] ?? "";
867
954
  }
868
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
+ }
869
967
  function sortItems(items, sort) {
870
968
  const out = [...items];
871
969
  const byName = (a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
872
970
  if (sort === "name") return out.sort(byName);
873
- if (sort === "website") return out.sort((a, b) => {
874
- const x = loginSiteOf(a), y = loginSiteOf(b);
875
- return x && y ? x.localeCompare(y) || byName(a, b) : x ? -1 : y ? 1 : byName(a, b);
876
- });
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
+ });
877
977
  if (sort === "updated") return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
878
978
  return out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
879
979
  }
@@ -1293,7 +1393,10 @@ function ActivityPanel({ item, port, onClose }) {
1293
1393
  {
1294
1394
  title: "Activity",
1295
1395
  onClose,
1296
- 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
+ ] }),
1297
1400
  children: [
1298
1401
  error ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { role: "alert", className: "px-1 text-sm text-destructive", children: error }) : null,
1299
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(
@@ -1376,7 +1479,10 @@ function ItemHistoryPanel({ item, port, arg, onClose, onChanged }) {
1376
1479
  {
1377
1480
  title: "Password history",
1378
1481
  onClose,
1379
- 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
+ ] }),
1380
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: [
1381
1487
  error ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "alert", className: "px-1 text-sm text-destructive", children: error }) : null,
1382
1488
  notice ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "status", className: "px-1 text-sm text-success", children: notice }) : null,
@@ -1981,6 +2087,7 @@ var import_confirm_opener2 = require("@ai-matrx/kit/confirm-opener");
1981
2087
 
1982
2088
  // src/authenticator/port.ts
1983
2089
  var MFA_METHOD_KEY = "mfa_method";
2090
+ var OTP_SEED_KEY = "totp_seed";
1984
2091
  function mfaMethodOf(item) {
1985
2092
  const v = item.custom.find((c) => c.key === MFA_METHOD_KEY)?.value;
1986
2093
  return v === "sms" ? "sms" : v ? "push_or_other" : null;
@@ -2339,7 +2446,7 @@ function AuthenticatorView({ port, items, definitions, query, onOpen, onChanged,
2339
2446
  const total = items.filter((i) => i.authenticator && !i.deletion && !hidden.has(i.id)).length;
2340
2447
  const familyOf2 = (key) => definitions.find((d) => d.key === key)?.family ?? "generic";
2341
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: [
2342
- 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,
2343
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: [
2344
2451
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-w-0 items-baseline gap-2", children: [
2345
2452
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h2", { className: "truncate text-sm font-semibold text-foreground", children: "One-time passwords" }),
@@ -2351,7 +2458,7 @@ function AuthenticatorView({ port, items, definitions, query, onOpen, onChanged,
2351
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" }) }),
2352
2459
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm font-medium text-foreground", children: "No one-time passwords yet" }),
2353
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" })
2354
- ] }) : 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)(
2355
2462
  OtpListRow,
2356
2463
  {
2357
2464
  port,
@@ -2488,9 +2595,9 @@ function sandboxBlocker(envKeyDraft, savedEnvKey) {
2488
2595
  return null;
2489
2596
  }
2490
2597
  var HANDLING_WORDS = {
2491
- visible: { label: "Anyone", hint: "Anyone who can use it can show or copy it" },
2492
- revealable: { label: "On request", hint: "Showing or copying it needs reveal access" },
2493
- 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." }
2494
2601
  };
2495
2602
  function handlingChoices(current) {
2496
2603
  return current === "sealed" ? ["sealed"] : ["visible", "revealable", "sealed"];
@@ -2672,7 +2779,7 @@ function FieldSettings({ item, port, field, onClose, onChanged }) {
2672
2779
  }
2673
2780
  ) }),
2674
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 }) }) }),
2675
- /* @__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 }) }) })
2676
2783
  ] }),
2677
2784
  error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Problem, { children: error }) : null
2678
2785
  ] });
@@ -2740,7 +2847,7 @@ function ItemSettingsPanel({ item, port, onClose, onChanged, onPanel }) {
2740
2847
  f.envKey
2741
2848
  ] }) : null,
2742
2849
  f.injectIntoSandbox ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-muted-foreground", children: " \xB7 Sandbox" }) : null,
2743
- !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
2744
2851
  ] })
2745
2852
  },
2746
2853
  f.id
@@ -4682,7 +4789,7 @@ var import_jsx_runtime19 = require("react/jsx-runtime");
4682
4789
  function ItemIcon({ definitionKey, family, site, size = "md", inverted = false }) {
4683
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";
4684
4791
  const tone = inverted ? "bg-primary-foreground text-primary" : "bg-muted text-foreground";
4685
- 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();
4686
4793
  if (initial && /[A-Z0-9]/.test(initial)) {
4687
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 });
4688
4795
  }
@@ -4711,7 +4818,7 @@ var TOO_BIG = "Files over 25 MB can't be stored.";
4711
4818
  var PROTECTION = [
4712
4819
  { value: "visible", label: "Open", title: "Anyone who can use the item can download it" },
4713
4820
  { value: "revealable", label: "Protected", title: "Downloading needs a recent sign-in" },
4714
- { 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" }
4715
4822
  ];
4716
4823
  function FilesSection({ item, port, editing, onChanged }) {
4717
4824
  const confirmed = useConfirmed();
@@ -4778,7 +4885,7 @@ function FilesSection({ item, port, editing, onChanged }) {
4778
4885
  {
4779
4886
  label: a.label,
4780
4887
  hint: a.description || void 0,
4781
- 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,
4782
4889
  children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(FileName, { file: a })
4783
4890
  },
4784
4891
  a.id
@@ -5000,7 +5107,7 @@ function RecoveryCodesRow({ item, field, port, onChanged }) {
5000
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,
5001
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
5002
5109
  ] }),
5003
- 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: [
5004
5111
  codes.map((code, i) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("li", { className: "flex min-h-7 items-center gap-1", children: [
5005
5112
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("code", { className: "min-w-0 flex-1 break-all font-mono text-sm text-foreground", children: code }),
5006
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") }),
@@ -5323,10 +5430,7 @@ function MovePanel({ item, port, organizations, onClose, onChanged }) {
5323
5430
  title: "Move or copy",
5324
5431
  size: "md",
5325
5432
  onClose,
5326
- footer: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex w-full items-center gap-2", children: [
5327
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(StatusLine, { error, notice }),
5328
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_controls19.Button, { variant: "outline", className: "ml-auto", onClick: onClose, children: "Close" })
5329
- ] }),
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" }) }),
5330
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: [
5331
5435
  canMove && moves.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5332
5436
  VaultRow,
@@ -5441,10 +5545,7 @@ function SharePanel({ item, port, organizations, onClose, onChanged }) {
5441
5545
  title: "Share",
5442
5546
  size: "md",
5443
5547
  onClose,
5444
- footer: /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex w-full items-center gap-2", children: [
5445
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(StatusLine, { error, notice }),
5446
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_controls20.Button, { variant: "primary", className: "ml-auto", onClick: onClose, children: "Done" })
5447
- ] }),
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" }) }),
5448
5549
  children: [
5449
5550
  /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(VaultSection, { title: "Who has access", children: [
5450
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" }) }),
@@ -5730,6 +5831,21 @@ function ItemPane(props) {
5730
5831
  if (!wasCancelled(e)) setError(e instanceof Error ? e.message : "The value could not be read.");
5731
5832
  }
5732
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;
5733
5849
  const declaredKeys = new Set(declared.map((f) => f.key));
5734
5850
  const extraFields = (item?.fields ?? []).filter((f) => !declaredKeys.has(f.key) && !draft.removed.includes(f.id) && !isAuthenticatorStorage(f.key));
5735
5851
  const metaValue = (key) => draft.values[key] ?? item?.custom.find((c) => c.key === key)?.value ?? "";
@@ -5738,6 +5854,10 @@ function ItemPane(props) {
5738
5854
  const isLogin = (item?.definitionKey ?? definition?.key) === "website_login" || (item?.definitionKey ?? definition?.key) === "username_password";
5739
5855
  const typeLabel = definition?.label ?? item?.definitionLabel ?? "";
5740
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
+ };
5741
5861
  const tagsVisible = showTags || draft.tags.length > 0 || tagsText !== "";
5742
5862
  const descriptionVisible = showDescription || draft.description !== "";
5743
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: [
@@ -5821,6 +5941,10 @@ function ItemPane(props) {
5821
5941
  header,
5822
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,
5823
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,
5824
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: [
5825
5949
  /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(VaultSection, { children: [
5826
5950
  mode === "new" ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VaultRow, { label: "Save to", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
@@ -5833,11 +5957,11 @@ function ItemPane(props) {
5833
5957
  searchPlaceholder: "Find an organization",
5834
5958
  groups: [
5835
5959
  { heading: "Just you", options: ["me"] },
5836
- ...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) }] : [],
5837
5961
  { heading: "Send", options: ["person"] }
5838
5962
  ],
5839
5963
  getLabel: (v) => v === "me" ? "Personal" : v === "person" ? "Someone else" : organizations.find((o) => o.id === v)?.name ?? v,
5840
- 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,
5841
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" })
5842
5966
  }
5843
5967
  ) }) : null,
@@ -5912,7 +6036,7 @@ function ItemPane(props) {
5912
6036
  i
5913
6037
  )),
5914
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" }),
5915
- /* @__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 }),
5916
6040
  item && isLogin && !item.authenticator ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TwoFactorMethodRow, { port, item, onChanged }) : null
5917
6041
  ] }),
5918
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 }) }) }),
@@ -5945,7 +6069,7 @@ function ItemPane(props) {
5945
6069
  ) : null,
5946
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}`)),
5947
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)),
5948
- !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
5949
6073
  ]
5950
6074
  }
5951
6075
  ),
@@ -5959,11 +6083,15 @@ function ItemPane(props) {
5959
6083
  }
5960
6084
  const field = item.fields.find((x) => x.key === f.key);
5961
6085
  if (isRecoveryCodesField(f)) return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(RecoveryCodesRow, { item, field, port, onChanged }, f.key);
5962
- 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;
5963
6087
  }),
5964
- 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)),
5965
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" }),
5966
- 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" }),
5967
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
5968
6096
  ] }),
5969
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" }) }),
@@ -5995,7 +6123,7 @@ function subtitleOf(item, withOwner) {
5995
6123
  const what = loginSiteOf(item) || item.definitionLabel;
5996
6124
  return withOwner ? `${what} \xB7 ${item.owner.label}` : what;
5997
6125
  }
5998
- 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 }) {
5999
6127
  const pinned = matchingIds ? items.filter((i) => matchingIds.has(i.id)) : [];
6000
6128
  const rest = matchingIds ? items.filter((i) => !matchingIds.has(i.id)) : items;
6001
6129
  const ordered = [...pinned, ...rest];
@@ -6014,7 +6142,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6014
6142
  listRef.current?.querySelector(`[data-item="${next.id}"]`)?.scrollIntoView({ block: "nearest" });
6015
6143
  }
6016
6144
  };
6017
- 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;
6018
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);
6019
6147
  const sortLabel = SORTS2.find((s) => s.value === sort)?.label ?? "Name";
6020
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: [
@@ -6232,6 +6360,9 @@ function RestorePanel({ lists, item, restore, onClose }) {
6232
6360
 
6233
6361
  // src/lists/TrashView.tsx
6234
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
+ }
6235
6366
  function TrashView({ port, deleted, query, header, onChanged }) {
6236
6367
  const confirmed = useConfirmed();
6237
6368
  const [busyId, setBusyId] = (0, import_react30.useState)(null);
@@ -6243,7 +6374,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6243
6374
  setMessage(null);
6244
6375
  try {
6245
6376
  const r = await confirmed(() => port.lists.restore(item, deletionId));
6246
- 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.` });
6247
6378
  onChanged();
6248
6379
  return r;
6249
6380
  } catch (e) {
@@ -6261,7 +6392,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6261
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: [
6262
6393
  /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [
6263
6394
  /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "truncate text-sm text-foreground", children: item.name }),
6264
- /* @__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) })
6265
6396
  ] }),
6266
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" })
6267
6398
  ] }, item.id)
@@ -6552,10 +6683,9 @@ function SecureLinkPanel({ item, port, onClose }) {
6552
6683
  title: "Send securely",
6553
6684
  size: "md",
6554
6685
  onClose,
6555
- footer: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "flex w-full flex-wrap items-center gap-2", children: [
6556
- /* @__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: [
6557
6687
  sameChannelSent && !error ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_controls26.Badge, { tone: "warning", children: "Both by email" }) : null,
6558
- /* @__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" }),
6559
6689
  /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_controls26.Button, { variant: "primary", disabled: !canSend, onClick: () => void send(), children: busy === "send" ? "Sending\u2026" : "Send link" })
6560
6690
  ] }),
6561
6691
  children: [
@@ -7811,8 +7941,33 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7811
7941
  },
7812
7942
  [loc, move]
7813
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
+ }, []);
7814
7968
  const knobsRef = (0, import_react41.useRef)(host.knobs);
7815
7969
  knobsRef.current = host.knobs;
7970
+ const [signedOut, setSignedOut] = (0, import_react41.useState)(false);
7816
7971
  const load = (0, import_react41.useCallback)(() => {
7817
7972
  setError(null);
7818
7973
  port.load().then((s) => {
@@ -7823,7 +7978,10 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7823
7978
  (rows) => setDeleted([...s.deleted, ...rows.filter((r) => !s.deleted.some((d) => d.id === r.id))]),
7824
7979
  () => void 0
7825
7980
  );
7826
- }).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
+ });
7827
7985
  port.lists.favorites?.list().then((ids) => setFavorites(new Set(ids)), () => void 0);
7828
7986
  knobsRef.current?.().catch(() => void 0);
7829
7987
  port.lists.recents?.list().then(setRecents, () => void 0);
@@ -7837,6 +7995,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7837
7995
  const orgs = snap?.organizations ?? [];
7838
7996
  const definitions = snap?.definitions ?? [];
7839
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;
7840
7999
  const views = (0, import_react41.useMemo)(() => buildViews(inThisVault, definitions, favorites, recents), [inThisVault, definitions, favorites, recents]);
7841
8000
  const visible = (0, import_react41.useMemo)(() => {
7842
8001
  const filtered = inThisVault.filter((i) => {
@@ -7877,8 +8036,11 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7877
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: [
7878
8037
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SafeGlyph, { className: "size-8 text-muted-foreground" }),
7879
8038
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "font-medium", children: "The vault couldn\u2019t open" }),
7880
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "max-w-xs text-muted-foreground", children: plainLoadError(error) }),
7881
- /* @__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
+ ] })
7882
8044
  ] });
7883
8045
  }
7884
8046
  if (!snap) {
@@ -7958,6 +8120,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7958
8120
  {
7959
8121
  title,
7960
8122
  items: visible,
8123
+ showOwner: vaultMixesOwners,
7961
8124
  definitions,
7962
8125
  port,
7963
8126
  selectedId: loc.itemId,
@@ -8730,6 +8893,7 @@ async function readNativeImportLimits(api, organizationId) {
8730
8893
  var recoveryOf = (w) => w.lifecycle?.vault_recovery;
8731
8894
  var isDeleted = (w) => recoveryOf(w)?.state === "deleted";
8732
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" };
8733
8897
  var handlingOf = (v) => v === "visible" || v === "sealed" ? v : "revealable";
8734
8898
  function fieldOf(w, def) {
8735
8899
  const declared = def?.fields.find((f) => f.key === w.field_key);
@@ -8737,7 +8901,7 @@ function fieldOf(w, def) {
8737
8901
  return {
8738
8902
  id: w.id,
8739
8903
  key: w.field_key,
8740
- label: declared?.label ?? humanize(w.field_key),
8904
+ label: declared?.label ?? FIELD_WORDS[w.field_key] ?? humanize(w.field_key),
8741
8905
  kind: declared?.kind ?? (handling === "visible" ? "text" : "secret"),
8742
8906
  handling,
8743
8907
  editable: w.editable ?? true,
@@ -8846,7 +9010,7 @@ function createApiVaultPort(api, options = {}) {
8846
9010
  lists.forEach((l, i) => {
8847
9011
  const sharedList = i === lists.length - 1;
8848
9012
  for (const w of l.items) {
8849
- if (seen.has(w.id) || deleted.has(w.id)) continue;
9013
+ if (seen.has(w.id) || deleted.has(w.id) || isHiddenItem(w)) continue;
8850
9014
  const owner = sharedList ? orgOf(w.organization_id, null, false) : orgOf(w.organization_id, w.user_id, i === 0);
8851
9015
  const item = itemOf(w, sharedList && !w.organization_id ? { kind: "shared", label: "Shared with me", organizationId: null } : owner, defs, auth);
8852
9016
  (isDeleted(w) ? deleted : seen).set(w.id, item);