@ai-matrx/vault 0.1.56 → 0.1.57

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
@@ -2621,10 +2621,22 @@ function TotpRow({ port, item, onChanged }) {
2621
2621
  function TotpEditRow({ value, onChange, hasExisting, item, port, onEnrolled, onRecognized }) {
2622
2622
  const confirmed = useConfirmed();
2623
2623
  const [status, setStatus] = (0, import_react12.useState)(null);
2624
+ const [retryImage, setRetryImage] = (0, import_react12.useState)(null);
2625
+ const latestItem = (0, import_react12.useRef)(item);
2626
+ latestItem.current = item;
2624
2627
  const uploadImage = item && port ? async (file) => {
2628
+ setRetryImage(file);
2625
2629
  setStatus("Adding from image\u2026");
2626
2630
  try {
2627
- const entry = await confirmed(() => port.authenticator.enrolQrImage(item, file, file.name));
2631
+ const entry = await confirmed(async () => {
2632
+ const current = await port.get(item);
2633
+ const visible = latestItem.current;
2634
+ if (!visible || visible.id !== item.id || visible.authenticatorState !== "known" || current.authenticatorState !== "known" || !item.authenticator && (visible.authenticator || current.authenticator)) {
2635
+ throw new Error("Authenticator status unavailable. Retry after refreshing.");
2636
+ }
2637
+ return port.authenticator.enrolQrImage(current, file, file.name);
2638
+ });
2639
+ setRetryImage(null);
2628
2640
  setStatus(entry.issuer ? `Added \xB7 ${entry.issuer}` : "Added");
2629
2641
  onChange("");
2630
2642
  onEnrolled?.({ ...item, authenticator: { digits: entry.digits, period: entry.period, enabled: entry.enabled } });
@@ -2641,7 +2653,11 @@ function TotpEditRow({ value, onChange, hasExisting, item, port, onEnrolled, onR
2641
2653
  onChange(v);
2642
2654
  },
2643
2655
  placeholder: hasExisting ? "Paste a new key to replace" : SETUP_KEY_PLACEHOLDER,
2644
- status,
2656
+ status: status && retryImage && uploadImage ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
2657
+ status,
2658
+ " ",
2659
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_controls8.Button, { variant: "quiet", onClick: () => void uploadImage(retryImage), children: "Retry" })
2660
+ ] }) : status,
2645
2661
  ...uploadImage ? { onImage: (f) => void uploadImage(f) } : {},
2646
2662
  ...onRecognized ? { onRecognized } : {}
2647
2663
  }
@@ -2702,6 +2718,8 @@ function normalizeWebsite(raw) {
2702
2718
  }
2703
2719
  function AddOtpPanel({ port, items, definitions, itemId = null, onClose, onChanged, onAdded, onDone }) {
2704
2720
  const confirmed = useConfirmed();
2721
+ const itemsRef = (0, import_react13.useRef)(items);
2722
+ itemsRef.current = items;
2705
2723
  const candidates = items.filter((i) => i.authenticatorState === "known" && !i.authenticator && !i.deletion && (i.capabilities.canEdit || i.capabilities.canManage)).sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" }));
2706
2724
  const [input, setInput] = (0, import_react13.useState)("");
2707
2725
  const [image, setImage] = (0, import_react13.useState)(null);
@@ -2730,17 +2748,25 @@ function AddOtpPanel({ port, items, definitions, itemId = null, onClose, onChang
2730
2748
  setBusy(true);
2731
2749
  setError(null);
2732
2750
  try {
2733
- let item = isNew ? created : items.find((i) => i.id === target) ?? null;
2751
+ let item = isNew ? created : itemsRef.current.find((i) => i.id === target) ?? null;
2752
+ let freshlyCreated = false;
2753
+ if (!isNew && (!item || item.authenticatorState !== "known" || item.authenticator)) throw new Error("Authenticator status unavailable. Retry after refreshing.");
2734
2754
  if (!item) {
2735
2755
  const def = LOGIN_KEYS.map((k) => definitions.find((d) => d.key === k)).find((d) => Boolean(d));
2736
2756
  if (!def) throw new Error("No login type is available.");
2737
2757
  const r = await port.create(def, null, { ...draftOf(null), name: name.trim(), loginUrls: websiteUrl ? [websiteUrl] : [] });
2738
2758
  item = r.item;
2759
+ freshlyCreated = true;
2739
2760
  setCreated(item);
2740
2761
  onChanged();
2741
2762
  }
2742
2763
  const it = item;
2743
- const entry = await confirmed(() => image ? port.authenticator.enrolQrImage(it, image, image.name) : port.authenticator.enrol(it, input));
2764
+ const entry = await confirmed(async () => {
2765
+ const latest = freshlyCreated ? it : await port.get(it);
2766
+ const listed = itemsRef.current.find((i) => i.id === it.id);
2767
+ if (!freshlyCreated && (latest.authenticatorState !== "known" || latest.authenticator || !isNew && !listed) || listed && (listed.authenticatorState !== "known" || listed.authenticator)) throw new Error("Authenticator status unavailable. Retry after refreshing.");
2768
+ return image ? port.authenticator.enrolQrImage(latest, image, image.name) : port.authenticator.enrol(latest, input);
2769
+ });
2744
2770
  let next = { ...it, authenticatorState: "known", authenticator: { digits: entry.digits, period: entry.period, enabled: entry.enabled } };
2745
2771
  const better = authenticatorName(it.name, { issuer: entry.issuer, account: entry.account });
2746
2772
  if (!isNew && better !== it.name) {
@@ -2827,7 +2853,11 @@ function AddOtpPanel({ port, items, definitions, itemId = null, onClose, onChang
2827
2853
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(VaultRow, { label: "Website", hint: websiteBad ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-destructive", children: "Not a web address" }) : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_controls9.Field, { className: "w-full", type: "url", value: website, disabled: busy || created !== null, onChange: (e) => setWebsite(e.target.value), placeholder: "example.com", "aria-label": "Website", "aria-invalid": websiteBad || void 0 }) })
2828
2854
  ] }) : null
2829
2855
  ] }),
2830
- error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "px-1 text-sm text-destructive", role: "alert", children: error }) : null
2856
+ error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("p", { className: "px-1 text-sm text-destructive", role: "alert", children: [
2857
+ error,
2858
+ " ",
2859
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_controls9.Button, { variant: "quiet", onClick: onChanged, children: "Retry" })
2860
+ ] }) : null
2831
2861
  ]
2832
2862
  }
2833
2863
  );
@@ -2862,23 +2892,7 @@ function otpGroups(items, entries, query, hidden = /* @__PURE__ */ new Set()) {
2862
2892
  const rank = (k) => k === "me" ? 0 : k === "shared" ? 2 : 1;
2863
2893
  return [...groups.entries()].sort(([a, x], [b, y]) => rank(a) - rank(b) || x.label.localeCompare(y.label)).map(([key, g]) => ({ key, ...g }));
2864
2894
  }
2865
- function withAdded(items, added) {
2866
- if (added.size === 0) return items;
2867
- const seen = /* @__PURE__ */ new Set();
2868
- const out = items.map((i) => {
2869
- seen.add(i.id);
2870
- const mine = added.get(i.id);
2871
- return mine && !i.authenticator ? { ...i, authenticator: mine.authenticator, name: mine.name } : i;
2872
- });
2873
- for (const [id, a] of added) if (!seen.has(id)) out.push(a);
2874
- return out;
2875
- }
2876
- function AuthenticatorView({ port, host, items: listed, definitions, query, onOpen, onChanged, header }) {
2877
- const [added, setAdded] = (0, import_react14.useState)(/* @__PURE__ */ new Map());
2878
- const items = withAdded(listed, added);
2879
- (0, import_react14.useEffect)(() => {
2880
- if (added.size > 0 && [...added.keys()].every((id) => listed.some((i) => i.id === id && i.authenticator))) setAdded(/* @__PURE__ */ new Map());
2881
- }, [listed, added]);
2895
+ function AuthenticatorView({ port, host, items, definitions, query, onOpen, onChanged, header }) {
2882
2896
  const [incomplete, setIncomplete] = (0, import_react14.useState)(true);
2883
2897
  const [entries, setEntries] = (0, import_react14.useState)(/* @__PURE__ */ new Map());
2884
2898
  const [hidden, setHidden] = (0, import_react14.useState)(/* @__PURE__ */ new Set());
@@ -2902,7 +2916,7 @@ function AuthenticatorView({ port, host, items: listed, definitions, query, onOp
2902
2916
  return () => {
2903
2917
  live = false;
2904
2918
  };
2905
- }, [port, listed, added]);
2919
+ }, [port, items]);
2906
2920
  const groups = otpGroups(items, entries, query, hidden);
2907
2921
  const total = items.filter((i) => i.authenticator && !i.deletion && !hidden.has(i.id)).length;
2908
2922
  const familyOf2 = (key) => definitions.find((d) => d.key === key)?.family ?? "generic";
@@ -2915,7 +2929,7 @@ function AuthenticatorView({ port, host, items: listed, definitions, query, onOp
2915
2929
  ] }),
2916
2930
  total > 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_controls10.Button, { variant: "outline", icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PlusGlyph, {}), onClick: () => setAdding(true), children: "Add" }) : null
2917
2931
  ] }),
2918
- total > 0 && incomplete ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "px-4 text-sm text-muted-foreground", children: [
2932
+ total > 0 && (incomplete || items.some((i) => i.authenticatorState !== "known")) ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "px-4 text-sm text-muted-foreground", children: [
2919
2933
  "Some codes unavailable ",
2920
2934
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_controls10.Button, { variant: "quiet", onClick: onChanged, children: "Retry" })
2921
2935
  ] }) : null,
@@ -2955,7 +2969,6 @@ function AuthenticatorView({ port, host, items: listed, definitions, query, onOp
2955
2969
  definitions,
2956
2970
  onClose: () => setAdding(false),
2957
2971
  onChanged,
2958
- onAdded: (item) => setAdded((cur) => new Map(cur).set(item.id, item)),
2959
2972
  onDone: (id) => {
2960
2973
  setAdding(false);
2961
2974
  onOpen(id);
@@ -6560,6 +6573,10 @@ function confirmDelete(item) {
6560
6573
  function ItemPane(props) {
6561
6574
  const { port, item, definition, mode, host, organizations, defaultOwner, seed, onBack, onMode, onSaved, onDeleted, onDirty, onCancelNew, onPanel, onChanged, favorite, notice, refreshKey } = props;
6562
6575
  const confirmed = useConfirmed();
6576
+ const latestItem = (0, import_react30.useRef)(item);
6577
+ latestItem.current = item;
6578
+ const [createdForRetry, setCreatedForRetry] = (0, import_react30.useState)(null);
6579
+ const enrolledForRetry = (0, import_react30.useRef)(null);
6563
6580
  const notify = useNotify();
6564
6581
  const editing = mode !== "view";
6565
6582
  const seedName = seed?.name ?? "";
@@ -6645,23 +6662,39 @@ function ItemPane(props) {
6645
6662
  setError(null);
6646
6663
  const out = { ...draft, values: Object.fromEntries(changedValues(base, draft)) };
6647
6664
  if (out.values[RECOVERY_CODES_KEY] !== void 0) out.values[RECOVERY_CODES_KEY] = parseRecoveryCodes(out.values[RECOVERY_CODES_KEY]).join("\n");
6648
- const withOtp = async (saved, notice2) => {
6665
+ const withOtp = async (saved, notice2, freshlyCreated = false) => {
6649
6666
  const setup = out.authenticator.trim();
6650
6667
  if (!setup) return [saved, notice2, false];
6651
6668
  try {
6652
- await confirmed(() => port.authenticator.enrol(saved, setup));
6669
+ if (enrolledForRetry.current?.itemId !== saved.id || enrolledForRetry.current.setup !== setup) await confirmed(async () => {
6670
+ const current = freshlyCreated ? saved : await port.get(saved);
6671
+ const visible = latestItem.current?.id === saved.id ? latestItem.current : null;
6672
+ const ownEnrollment = enrolledForRetry.current?.itemId === saved.id;
6673
+ if (!freshlyCreated && (current.authenticatorState !== "known" || !item?.authenticator && current.authenticator && !ownEnrollment || item && !visible) || visible && (visible.authenticatorState !== "known" || !item?.authenticator && visible.authenticator && !ownEnrollment)) throw new Error("Authenticator status unavailable. Retry after refreshing");
6674
+ await port.authenticator.enrol(current, setup);
6675
+ enrolledForRetry.current = { itemId: saved.id, setup };
6676
+ });
6653
6677
  return [await port.get(saved), notice2, true];
6654
6678
  } catch (e) {
6679
+ if (enrolledForRetry.current?.itemId === saved.id && enrolledForRetry.current.setup === setup) return [saved, `Saved and authenticator set up, but refresh failed: ${e instanceof Error ? e.message : "Retry"}.`, false];
6655
6680
  const why = wasCancelled(e) ? "it was not confirmed" : e instanceof Error ? e.message : "it was refused";
6656
6681
  return [saved, `Saved, but the authenticator was not set up: ${why}. Add it with Edit.`, false];
6657
6682
  }
6658
6683
  };
6659
6684
  try {
6660
- if (item) {
6661
- const r = await port.save(item, definition, out);
6685
+ if (item || createdForRetry) {
6686
+ const r = await port.save(item ?? createdForRetry, definition, out);
6662
6687
  const [next, n, enrolled] = await withOtp(r.item, r.notice);
6663
- (0, import_drafts.dropKeptDraft)(keepKey);
6664
- onSaved(next, false, n, enrolled);
6688
+ if (out.authenticator.trim() && !enrolled) {
6689
+ setError(n);
6690
+ onChanged();
6691
+ return;
6692
+ }
6693
+ if (createdForRetry) await finishNew(next, n, enrolled);
6694
+ else {
6695
+ (0, import_drafts.dropKeptDraft)(keepKey);
6696
+ onSaved(next, false, n, enrolled);
6697
+ }
6665
6698
  } else if (definition && owner === "person") {
6666
6699
  const secretKey = declared.find((f) => f.storage === "encrypted" && f.kind === "secret")?.key ?? null;
6667
6700
  const r = await port.sharing.assign({ recipientEmail: recipient.trim(), definition, draft: out, generateFieldKey: generatePrivately ? secretKey : null });
@@ -6671,12 +6704,14 @@ function ItemPane(props) {
6671
6704
  setSent(r);
6672
6705
  } else if (definition) {
6673
6706
  const r = await port.create(definition, owner === "me" ? null : owner, out);
6674
- const [withCode, n0, enrolled] = await withOtp(r.item, r.notice);
6675
- const [next, n] = await storeQueuedFiles(withCode, n0);
6676
- (0, import_drafts.dropKeptDraft)(keepKey);
6677
- setSavedNew(true);
6678
- onDirty(false);
6679
- onSaved(next, true, n, enrolled);
6707
+ setCreatedForRetry(r.item);
6708
+ const [withCode, n0, enrolled] = await withOtp(r.item, r.notice, true);
6709
+ if (out.authenticator.trim() && !enrolled) {
6710
+ setError(n0);
6711
+ onChanged();
6712
+ return;
6713
+ }
6714
+ await finishNew(withCode, n0, enrolled);
6680
6715
  }
6681
6716
  } catch (e) {
6682
6717
  setError(e instanceof Error ? e.message : "Saving failed. Nothing was changed.");
@@ -6684,6 +6719,13 @@ function ItemPane(props) {
6684
6719
  setBusy(false);
6685
6720
  }
6686
6721
  }
6722
+ async function finishNew(saved, notice2, enrolled) {
6723
+ const [next, finalNotice] = await storeQueuedFiles(saved, notice2);
6724
+ (0, import_drafts.dropKeptDraft)(keepKey);
6725
+ setSavedNew(true);
6726
+ onDirty(false);
6727
+ onSaved(next, true, finalNotice, enrolled);
6728
+ }
6687
6729
  async function storeQueuedFiles(saved, notice2) {
6688
6730
  let next = saved;
6689
6731
  const failed = [];
@@ -6865,7 +6907,11 @@ function ItemPane(props) {
6865
6907
  const filesProps = item ? { item, port, host, definition, organizations, arg: null, onClose: () => void 0, onChanged, onPanel } : null;
6866
6908
  return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(RoomProvider, { value: room, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("article", { ref: paneRef, "aria-label": item?.name ?? "New item", className: (0, import_design_system19.cn)(import_controls23.CONTROLS_CONTAINER_CLASS, "flex h-full min-h-0 flex-col bg-background"), children: [
6867
6909
  header,
6868
- error ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { role: "alert", className: "border-b border-border bg-muted px-5 py-2 text-sm text-destructive", children: error }) : null,
6910
+ error ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { role: "alert", className: "border-b border-border bg-muted px-5 py-2 text-sm text-destructive", children: [
6911
+ error,
6912
+ " ",
6913
+ error.includes("Retry after refreshing") ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_controls23.Button, { variant: "quiet", onClick: () => onChanged(), children: "Retry" }) : null
6914
+ ] }) : null,
6869
6915
  notice && !editing ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { role: "status", className: "border-b border-border bg-muted px-5 py-2 text-sm text-warning", children: notice }) : null,
6870
6916
  item && !editing && (itemOff || offFields.length > 0) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { role: "status", className: "flex items-center gap-3 border-b border-border bg-muted px-5 py-2 text-sm", children: [
6871
6917
  /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "min-w-0 flex-1 truncate text-warning", children: offFields.length === 0 ? "This item is turned off" : offFields.length === 1 ? "1 field is turned off" : `${offFields.length} fields are turned off` }),
@@ -6969,10 +7015,11 @@ function ItemPane(props) {
6969
7015
  i
6970
7016
  )),
6971
7017
  /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(AddRow, { label: draft.loginUrls.length === 0 ? "Website" : "", onAdd: () => patch({ loginUrls: [...draft.loginUrls, ""] }), children: draft.loginUrls.length === 0 ? "Add website" : "Add another website" }),
6972
- !item || item.authenticatorState === "known" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(TotpEditRow, { value: draft.authenticator, onChange: (v) => patch({ authenticator: v }), hasExisting: Boolean(item?.authenticator || seedField), item, port, onEnrolled: onChanged }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(VaultRow, { label: "Authenticator", children: [
7018
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { hidden: Boolean(item && item.authenticatorState !== "known"), children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(TotpEditRow, { value: draft.authenticator, onChange: (v) => patch({ authenticator: v }), hasExisting: Boolean(item?.authenticator || seedField), item, port, onEnrolled: onChanged }) }),
7019
+ item && item.authenticatorState !== "known" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(VaultRow, { label: "Authenticator", children: [
6973
7020
  /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "text-muted-foreground", children: item.authenticatorState === "pending" ? "Loading" : "Unavailable" }),
6974
7021
  item.authenticatorState === "unavailable" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_controls23.Button, { variant: "quiet", onClick: () => onChanged?.(), children: "Retry" }) : null
6975
- ] }),
7022
+ ] }) : null,
6976
7023
  item && isLogin && !item.authenticator ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(TwoFactorMethodRow, { port, item, onChanged }) : null,
6977
7024
  !item && isLogin && draft.authenticator.trim() === "" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(DraftTwoFactorRow, { custom: draft.custom, onCustom: (custom) => patch({ custom }) }) : null,
6978
7025
  recoveryEditing ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
@@ -9225,6 +9272,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
9225
9272
  const [refreshing, setRefreshing] = (0, import_react44.useState)(false);
9226
9273
  const loadedAt = (0, import_react44.useRef)(0);
9227
9274
  const inFlight = (0, import_react44.useRef)(false);
9275
+ const loadGeneration = (0, import_react44.useRef)(0);
9228
9276
  const apply = (0, import_react44.useCallback)(
9229
9277
  (s) => {
9230
9278
  loadedAt.current = Date.now();
@@ -9241,11 +9289,15 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
9241
9289
  const refresh = (0, import_react44.useCallback)(() => {
9242
9290
  if (inFlight.current) return;
9243
9291
  inFlight.current = true;
9292
+ const generation = ++loadGeneration.current;
9244
9293
  setRefreshing(true);
9245
9294
  port.load().then((s) => {
9295
+ if (generation !== loadGeneration.current) return;
9246
9296
  setRefreshError(null);
9247
9297
  apply(s);
9248
- }).catch((e) => setRefreshError(plainLoadError(e instanceof Error && e.message ? e.message : "The vault could not be read."))).finally(() => {
9298
+ }).catch((e) => {
9299
+ if (generation === loadGeneration.current) setRefreshError(plainLoadError(e instanceof Error && e.message ? e.message : "The vault could not be read."));
9300
+ }).finally(() => {
9249
9301
  inFlight.current = false;
9250
9302
  setRefreshing(false);
9251
9303
  });
@@ -9253,18 +9305,20 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
9253
9305
  port.lists.recents?.list().then(setRecents, () => void 0);
9254
9306
  }, [port, apply]);
9255
9307
  const load = (0, import_react44.useCallback)(() => {
9308
+ const generation = ++loadGeneration.current;
9256
9309
  setError(null);
9257
9310
  setRefreshError(null);
9258
9311
  let finished = false;
9259
9312
  port.load((partial) => {
9260
- if (finished) return;
9313
+ if (finished || generation !== loadGeneration.current) return;
9261
9314
  setSnap(partial);
9262
9315
  setItems(partial.items);
9263
9316
  setDeleted(partial.deleted);
9264
9317
  }).then((s) => {
9265
9318
  finished = true;
9266
- apply(s);
9319
+ if (generation === loadGeneration.current) apply(s);
9267
9320
  }).catch((e) => {
9321
+ if (generation !== loadGeneration.current) return;
9268
9322
  setSignedOut(e?.status === 401);
9269
9323
  setError(e instanceof Error ? e.message : "The vault could not be loaded.");
9270
9324
  });
@@ -10473,7 +10527,7 @@ function itemOf(w, owner, defs, auth, state = "known", unavailable = [], invento
10473
10527
  fields: (w.fields ?? []).filter((f) => f.execution_purpose !== "passkey_private").map((f) => fieldOf(f, def)),
10474
10528
  custom: (w.non_secret_fields ?? []).map((c) => ({ key: c["key"] ?? "", label: c["label"] ?? humanize(c["key"] ?? ""), value: c["value"] ?? "" })),
10475
10529
  authenticator: a ? { digits: a.digits, period: a.period, enabled: a.enabled } : null,
10476
- authenticatorState: a ? "known" : state === "pending" ? "pending" : state === "unavailable" || scopeUnavailable(inventoryOwner, unavailable) ? "unavailable" : "known",
10530
+ authenticatorState: a ? "known" : state === "pending" ? "pending" : state === "unavailable" || scopeUnavailable(inventoryOwner, unavailable) || (w.fields ?? []).some((f) => f.field_key === OTP_SEED_KEY && f.is_active !== false) ? "unavailable" : "known",
10477
10531
  capabilities: { canUse: cap.can_use ?? false, canEdit: cap.can_edit ?? false, canReveal: cap.can_reveal ?? false, canManage: cap.can_manage ?? false },
10478
10532
  attachments: (w.attachments ?? []).map((a2) => ({ id: a2.id, label: a2.label, description: a2.description ?? "", fileName: a2.file_name, mediaType: a2.media_type ?? "application/octet-stream", sizeBytes: a2.size_bytes, handling: handlingOf(a2.handling), createdAt: a2.created_at, updatedAt: a2.updated_at })),
10479
10533
  uriMatchMode: MATCH_MODES2.find((m) => m === w.uri_match_mode) ?? "host",
@@ -10514,16 +10568,12 @@ function createApiVaultPort(hostApi, options = {}) {
10514
10568
  let authUnavailable = [];
10515
10569
  let inventoryOwners = /* @__PURE__ */ new Map();
10516
10570
  let orgs = [];
10571
+ let loadGeneration = 0;
10572
+ let authMutation = 0;
10517
10573
  const orgOf = (id, userId, mine) => {
10518
10574
  if (id) return { kind: "organization", label: orgs.find((o) => o.id === id)?.name ?? "Organization", organizationId: id };
10519
10575
  return mine || userId ? { kind: "me", label: "Personal", organizationId: null } : { kind: "shared", label: "Shared with me", organizationId: null };
10520
10576
  };
10521
- const ownerInOrgList = (w, via, meId) => {
10522
- if (w.organization_id) return orgOf(w.organization_id, null, false);
10523
- if (w.user_id && meId && w.user_id === meId) return { kind: "me", label: "Personal", organizationId: null };
10524
- if (via) return { kind: "organization", label: via.name, organizationId: via.id, memberShared: { userId: w.user_id ?? null } };
10525
- return { kind: "shared", label: "Shared with me", organizationId: null };
10526
- };
10527
10577
  async function refetch(id, owner) {
10528
10578
  const w = await account9.getItem(api, { path: { item_id: id } });
10529
10579
  return itemOf(w, owner, defs, auth, authState, authUnavailable, inventoryOwners.get(id) ?? owner);
@@ -10536,6 +10586,7 @@ function createApiVaultPort(hostApi, options = {}) {
10536
10586
  definition: (key) => defs.get(key),
10537
10587
  organizations: () => orgs,
10538
10588
  setAuthenticator: (itemId, entry) => {
10589
+ authMutation++;
10539
10590
  const next = new Map(auth);
10540
10591
  if (entry) next.set(itemId, entry);
10541
10592
  else next.delete(itemId);
@@ -10553,134 +10604,155 @@ function createApiVaultPort(hostApi, options = {}) {
10553
10604
  fields: createFieldsPort(ctx),
10554
10605
  lists: createListsPort(ctx, options.userState),
10555
10606
  async load(onProgress) {
10556
- authState = "pending";
10557
- auth = /* @__PURE__ */ new Map();
10558
- authUnavailable = [];
10559
- inventoryOwners = /* @__PURE__ */ new Map();
10560
- const catalogP = retrying(() => apps2.getAppCatalogKind(api, { path: { app: "matrx", kind: "credential_definition" } }));
10561
- const authP = retrying(() => apps2.listAuthenticators(api));
10562
- const userP = retrying(() => account9.listItems(api, { query: { principal_type: "user" } }));
10563
- const sharedP = retrying(() => account9.sharedWithMe(api));
10564
- const quiet = (p) => void p.catch(() => void 0);
10565
- [catalogP, authP, userP, sharedP].forEach(quiet);
10566
- const orgItemsP = retrying(() => account9.organizationItems(api));
10567
- quiet(orgItemsP);
10568
- const raw = {
10569
- orgLists: [],
10570
- orgsKnown: options.organizations !== void 0,
10571
- meId: null
10572
- };
10573
- const unavailable = [];
10574
- let done = false;
10575
- const build = () => {
10576
- const catalog2 = raw.catalog ?? [];
10577
- defs = new Map(parseDefinitions(catalog2.filter((e) => e.is_active).map((e) => ({ key: e.key, payload: e.payload, sort_order: e.sort_order }))).map((d) => [d.key, d]));
10578
- const mineItems = raw.mine?.items ?? [];
10579
- const meId = raw.meId ?? mineItems.find((w) => w.user_id)?.user_id ?? null;
10580
- const seen = /* @__PURE__ */ new Map();
10581
- const deleted = /* @__PURE__ */ new Map();
10582
- const take = (w, owner, inventoryOwner = owner) => {
10583
- if (seen.has(w.id) || deleted.has(w.id) || isHiddenItem(w)) return;
10584
- inventoryOwners.set(w.id, inventoryOwner);
10585
- const item = itemOf(w, owner, defs, auth, authState, authUnavailable, inventoryOwner);
10586
- (isDeleted(w) ? deleted : seen).set(w.id, item);
10607
+ const run = async (restarts) => {
10608
+ const generation = ++loadGeneration;
10609
+ const mutationAtStart = authMutation;
10610
+ let localAuth = /* @__PURE__ */ new Map();
10611
+ let localState = "pending";
10612
+ let localUnavailable = [];
10613
+ let localDefs = /* @__PURE__ */ new Map();
10614
+ let localOrgs = [];
10615
+ const localOwners = /* @__PURE__ */ new Map();
10616
+ const catalogP = retrying(() => apps2.getAppCatalogKind(api, { path: { app: "matrx", kind: "credential_definition" } }));
10617
+ const authP = retrying(() => apps2.listAuthenticators(api));
10618
+ const userP = retrying(() => account9.listItems(api, { query: { principal_type: "user" } }));
10619
+ const sharedP = retrying(() => account9.sharedWithMe(api));
10620
+ const quiet = (p) => void p.catch(() => void 0);
10621
+ [catalogP, authP, userP, sharedP].forEach(quiet);
10622
+ const orgItemsP = retrying(() => account9.organizationItems(api));
10623
+ quiet(orgItemsP);
10624
+ const raw = {
10625
+ orgLists: [],
10626
+ orgsKnown: options.organizations !== void 0,
10627
+ meId: null
10587
10628
  };
10588
- for (const w of mineItems) take(w, orgOf(w.organization_id, null, true));
10589
- raw.orgLists.forEach((l, i) => {
10590
- const via = orgs[i];
10591
- for (const w of l?.items ?? []) take(w, ownerInOrgList(w, via, meId));
10592
- });
10593
- for (const w of raw.shared?.items ?? []) take(w, w.organization_id ? orgOf(w.organization_id, null, false) : { kind: "shared", label: "Shared with me", organizationId: null }, { kind: "shared", label: "Shared with me", organizationId: null });
10594
- const pending = done ? [] : [...raw.orgsKnown ? orgs.filter((_, i) => raw.orgLists[i] === void 0).map((o) => o.name) : ["your organizations"], ...raw.shared ? [] : ["Shared with me"]];
10595
- return {
10596
- unavailable: [...unavailable],
10597
- ...pending.length > 0 ? { pending } : {},
10598
- items: [...seen.values()],
10599
- deleted: [...deleted.values()],
10600
- organizations: orgs,
10601
- definitions: [...defs.values()].sort((a, b) => a.sortOrder - b.sortOrder || a.label.localeCompare(b.label))
10629
+ const unavailable = [];
10630
+ let done = false;
10631
+ const build = () => {
10632
+ const catalog2 = raw.catalog ?? [];
10633
+ localDefs = new Map(parseDefinitions(catalog2.filter((e) => e.is_active).map((e) => ({ key: e.key, payload: e.payload, sort_order: e.sort_order }))).map((d) => [d.key, d]));
10634
+ const mineItems = raw.mine?.items ?? [];
10635
+ const meId = raw.meId ?? mineItems.find((w) => w.user_id)?.user_id ?? null;
10636
+ const seen = /* @__PURE__ */ new Map();
10637
+ const deleted = /* @__PURE__ */ new Map();
10638
+ const localOrgOf = (id, mine2) => id ? { kind: "organization", label: localOrgs.find((o) => o.id === id)?.name ?? "Organization", organizationId: id } : mine2 ? { kind: "me", label: "Personal", organizationId: null } : { kind: "shared", label: "Shared with me", organizationId: null };
10639
+ const take = (w, owner, inventoryOwner = owner) => {
10640
+ if (seen.has(w.id) || deleted.has(w.id) || isHiddenItem(w)) return;
10641
+ localOwners.set(w.id, inventoryOwner);
10642
+ const item = itemOf(w, owner, localDefs, localAuth, localState, localUnavailable, inventoryOwner);
10643
+ (isDeleted(w) ? deleted : seen).set(w.id, item);
10644
+ };
10645
+ for (const w of mineItems) take(w, localOrgOf(w.organization_id, true));
10646
+ raw.orgLists.forEach((l, i) => {
10647
+ const via = localOrgs[i];
10648
+ for (const w of l?.items ?? []) take(w, w.organization_id ? localOrgOf(w.organization_id, false) : w.user_id && meId && w.user_id === meId ? localOrgOf(null, true) : via ? { kind: "organization", label: via.name, organizationId: via.id, memberShared: { userId: w.user_id ?? null } } : localOrgOf(null, false));
10649
+ });
10650
+ for (const w of raw.shared?.items ?? []) take(w, w.organization_id ? localOrgOf(w.organization_id, false) : { kind: "shared", label: "Shared with me", organizationId: null }, { kind: "shared", label: "Shared with me", organizationId: null });
10651
+ const pending = done ? [] : [...raw.orgsKnown ? localOrgs.filter((_, i) => raw.orgLists[i] === void 0).map((o) => o.name) : ["your organizations"], ...raw.shared ? [] : ["Shared with me"]];
10652
+ return {
10653
+ unavailable: [...unavailable],
10654
+ ...pending.length > 0 ? { pending } : {},
10655
+ items: [...seen.values()],
10656
+ deleted: [...deleted.values()],
10657
+ organizations: localOrgs,
10658
+ definitions: [...localDefs.values()].sort((a, b) => a.sortOrder - b.sortOrder || a.label.localeCompare(b.label))
10659
+ };
10602
10660
  };
10603
- };
10604
- const progress = () => {
10605
- if (!done && onProgress && raw.catalog && raw.mine) onProgress(build());
10606
- };
10607
- const orgReportP = options.organizations ? null : retrying(() => platform.listMyOrganizations(api));
10608
- if (orgReportP) quiet(orgReportP);
10609
- const orgListsReady = (async () => {
10610
- const report = orgReportP ? await orgReportP : null;
10611
- raw.meId = report?.user_id ?? null;
10612
- orgs = [...new Map((options.organizations ?? (report?.organizations ?? []).filter((o) => !o.archived_at).map((o) => ({ id: o.id, name: o.name }))).map((o) => [o.id, o])).values()];
10613
- raw.orgLists = orgs.map(() => void 0);
10614
- raw.orgsKnown = true;
10615
- progress();
10616
- const one = await orgItemsP.then(
10617
- (r) => r,
10618
- (e) => {
10619
- if (e?.status === 404) {
10620
- hostProblem("GET /vault/organization-items answered 404; reading each organization vault separately", e);
10621
- return null;
10661
+ const progress = () => {
10662
+ if (!done && generation === loadGeneration && mutationAtStart === authMutation && onProgress && raw.catalog && raw.mine) onProgress(build());
10663
+ };
10664
+ const orgReportP = options.organizations ? null : retrying(() => platform.listMyOrganizations(api));
10665
+ if (orgReportP) quiet(orgReportP);
10666
+ const orgListsReady = (async () => {
10667
+ const report = orgReportP ? await orgReportP : null;
10668
+ raw.meId = report?.user_id ?? null;
10669
+ localOrgs = [...new Map((options.organizations ?? (report?.organizations ?? []).filter((o) => !o.archived_at).map((o) => ({ id: o.id, name: o.name }))).map((o) => [o.id, o])).values()];
10670
+ raw.orgLists = localOrgs.map(() => void 0);
10671
+ raw.orgsKnown = true;
10672
+ progress();
10673
+ const one = await orgItemsP.then(
10674
+ (r) => r,
10675
+ (e) => {
10676
+ if (e?.status === 404) {
10677
+ hostProblem("GET /vault/organization-items answered 404; reading each organization vault separately", e);
10678
+ return null;
10679
+ }
10680
+ return "failed";
10622
10681
  }
10623
- return "failed";
10682
+ );
10683
+ if (one) {
10684
+ const byOrg = one === "failed" ? /* @__PURE__ */ new Map() : new Map(one.views.map((v) => [v.organization_id, v.items]));
10685
+ const failed = one === "failed" ? new Set(localOrgs.map((o) => o.id)) : new Set(one.unavailable);
10686
+ const settled = localOrgs.map((o, i) => {
10687
+ const items = byOrg.get(o.id);
10688
+ if (failed.has(o.id) || items === void 0) return Promise.reject(new Error(`${o.name} is unavailable`));
10689
+ raw.orgLists[i] = { items, count: items.length };
10690
+ return Promise.resolve(raw.orgLists[i]);
10691
+ });
10692
+ settled.forEach(quiet);
10693
+ progress();
10694
+ return settled;
10624
10695
  }
10625
- );
10626
- if (one) {
10627
- const byOrg = one === "failed" ? /* @__PURE__ */ new Map() : new Map(one.views.map((v) => [v.organization_id, v.items]));
10628
- const failed = one === "failed" ? new Set(orgs.map((o) => o.id)) : new Set(one.unavailable);
10629
- const settled = orgs.map((o, i) => {
10630
- const items = byOrg.get(o.id);
10631
- if (failed.has(o.id) || items === void 0) return Promise.reject(new Error(`${o.name} is unavailable`));
10632
- raw.orgLists[i] = { items, count: items.length };
10633
- return Promise.resolve(raw.orgLists[i]);
10696
+ return localOrgs.map((o, i) => {
10697
+ const p = retrying(() => account9.listItems(api, { query: { principal_type: "organization", organization_id: o.id } }, { organizationId: o.id }));
10698
+ const arrived = p.then((l) => {
10699
+ raw.orgLists[i] = l;
10700
+ progress();
10701
+ return l;
10702
+ });
10703
+ quiet(arrived);
10704
+ return arrived;
10634
10705
  });
10635
- settled.forEach(quiet);
10706
+ })();
10707
+ quiet(orgListsReady);
10708
+ const authDone = authP.then((a) => {
10709
+ localUnavailable = a.unavailable_scopes ?? [];
10710
+ const fresh = new Map((a.entries ?? []).map((e) => [e.credential_item_id, e]));
10711
+ localAuth = fresh;
10712
+ localState = "known";
10713
+ if (localUnavailable.length) unavailable.push("One-time codes");
10636
10714
  progress();
10637
- return settled;
10638
- }
10639
- return orgs.map((o, i) => {
10640
- const p = retrying(() => account9.listItems(api, { query: { principal_type: "organization", organization_id: o.id } }, { organizationId: o.id }));
10641
- const arrived = p.then((l) => {
10642
- raw.orgLists[i] = l;
10643
- progress();
10644
- return l;
10645
- });
10646
- quiet(arrived);
10647
- return arrived;
10715
+ }, () => {
10716
+ localState = "unavailable";
10717
+ unavailable.push("One-time codes");
10648
10718
  });
10649
- })();
10650
- quiet(orgListsReady);
10651
- const authDone = authP.then((a) => {
10652
- authUnavailable = a.unavailable_scopes ?? [];
10653
- const fresh = new Map((a.entries ?? []).map((e) => [e.credential_item_id, e]));
10654
- auth = fresh;
10655
- authState = "known";
10656
- if (authUnavailable.length) unavailable.push("One-time codes");
10657
- progress();
10658
- }, () => {
10659
- authState = "unavailable";
10660
- unavailable.push("One-time codes");
10661
- });
10662
- const sharedDone = sharedP.then((sh) => {
10663
- raw.shared = sh;
10719
+ const sharedDone = sharedP.then((sh) => {
10720
+ raw.shared = sh;
10721
+ progress();
10722
+ }, () => {
10723
+ raw.shared = { items: [], count: 0 };
10724
+ unavailable.push("Shared with me");
10725
+ });
10726
+ const [catalog, mine] = await Promise.all([catalogP, userP]);
10727
+ raw.catalog = catalog;
10728
+ raw.mine = mine;
10664
10729
  progress();
10665
- }, () => {
10666
- raw.shared = { items: [], count: 0 };
10667
- unavailable.push("Shared with me");
10668
- });
10669
- const [catalog, mine] = await Promise.all([catalogP, userP]);
10670
- raw.catalog = catalog;
10671
- raw.mine = mine;
10672
- progress();
10673
- const orgPs = await orgListsReady;
10674
- const orgSettled = await Promise.allSettled(orgPs);
10675
- orgSettled.forEach((r, i) => {
10676
- if (r.status === "rejected") {
10677
- raw.orgLists[i] = { items: [], count: 0 };
10678
- unavailable.push(orgs[i]?.name ?? "An organization");
10730
+ const orgPs = await orgListsReady;
10731
+ const orgSettled = await Promise.allSettled(orgPs);
10732
+ orgSettled.forEach((r, i) => {
10733
+ if (r.status === "rejected") {
10734
+ raw.orgLists[i] = { items: [], count: 0 };
10735
+ unavailable.push(localOrgs[i]?.name ?? "An organization");
10736
+ }
10737
+ });
10738
+ await Promise.all([authDone, sharedDone]);
10739
+ done = true;
10740
+ if (generation === loadGeneration && mutationAtStart !== authMutation) {
10741
+ if (restarts >= 2) throw new Error("Authenticator changed while loading. Retry");
10742
+ return run(restarts + 1);
10679
10743
  }
10680
- });
10681
- await Promise.all([authDone, sharedDone]);
10682
- done = true;
10683
- return build();
10744
+ const snapshot = build();
10745
+ if (generation === loadGeneration) {
10746
+ defs = localDefs;
10747
+ auth = localAuth;
10748
+ authState = localState;
10749
+ authUnavailable = localUnavailable;
10750
+ inventoryOwners = localOwners;
10751
+ orgs = localOrgs;
10752
+ }
10753
+ return snapshot;
10754
+ };
10755
+ return run(0);
10684
10756
  },
10685
10757
  async reveal(item, fieldKey2) {
10686
10758
  const field = item.fields.find((f) => f.key === fieldKey2);