@ai-matrx/vault 0.1.26 → 0.1.28

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.28
4
+
5
+ - **A website typed as a bare domain always saves.** Every bare domain is sent as `https://…` on create, edit, assign and the item settings call, whether or not the server could reach it (an intranet, a site not live yet) (`normalizeLoginUrls`); the Website box shows the `https://` form after leaving it, and "Not a public website. You can still save it." is a warning, never a block. Guard: `src/review-0-1-28.test.tsx` § 1. Consumer action: none.
6
+ - **Every vault refusal shows the server's reason.** `createApiVaultPort` wraps the host's `Api` once (`withServerReasons`): a 4xx whose sentence is generic ("Invalid request. Please check your input and try again.") is re-worded, on the same error object, with the server's own reason ("Login URL must start with https:// or http://…"), so every action inherits it. Guard: § 1. Consumer action: none.
7
+ - **A restored item turns back on in one press.** Restore still comes back as the server's quarantine (item, fields, codes, browser fill and sandbox use off; shares never recreated). The restore result now offers **Turn these back on**: it turns on the item, its fields, its one-time codes and browser fill (when it has a website), then says what it turned on; sharing and sandbox use stay the person's choice (`RestorePanel turnOn`, `turnBackOn`, `turnedOnSentence`; the item's own Turn on uses the same `turnBackOn`). Not offered when the item was already off before it was deleted. Guard: § 2. Consumer action: none.
8
+ - **The first save of a new item leaves the form clean.** Once create resolves the pane reports clean and Save is off, even while a host's address has not moved to the saved item yet (it used to keep "Unsaved" up and ask "Discard your changes?"); `onDirty` is said when it changes, never re-said for a new callback. Guard: § 3. Consumer action: none.
9
+ - Guard for every row copy ending in "<label> copied" or a failure line (refused clipboard, unreadable value): § 4. Consumer action: none.
10
+
11
+ ## 0.1.27
12
+
13
+ - **`emptyReadAlarm`, `holdingsDoorIsAbsent`, `HOLDINGS_DOOR_MIGRATION` and the `HoldingRow`/`Holdings` types are exported from the root** so the web app can drop its whole-file copy (`features/secrets/empty-read.ts`). Consumer action: none.
14
+
3
15
  ## 0.1.26
4
16
 
5
17
  - **`glyphs.tsx` draws only `SafeGlyph` itself.** The other 46 vault glyphs (`KeyGlyph`, `CardGlyph`, `EyeGlyph`, `QrGlyph`, ...) are aliases of the matching `@ai-matrx/design-system/icons` glyphs, so they now render the exact lucide 1.48 drawing and carry the `lucide-<name>` optical-trim class instead of a hand-set `--matrx-glyph-air-*` style. Every export name and the `Glyph` type are unchanged. Consumer action: none.
package/dist/index.cjs CHANGED
@@ -140,6 +140,7 @@ __export(src_exports, {
140
140
  DEFAULT_LOCATION: () => DEFAULT_LOCATION,
141
141
  GenerateButton: () => GenerateButton,
142
142
  HIDDEN_ITEM_TAG: () => HIDDEN_ITEM_TAG,
143
+ HOLDINGS_DOOR_MIGRATION: () => HOLDINGS_DOOR_MIGRATION,
143
144
  IdentityGate: () => IdentityGate,
144
145
  LOCKED_MESSAGE: () => LOCKED_MESSAGE,
145
146
  VaultApp: () => VaultApp,
@@ -149,7 +150,9 @@ __export(src_exports, {
149
150
  createApiVaultPort: () => createApiVaultPort,
150
151
  createSupabaseIdentity: () => createSupabaseIdentity,
151
152
  createVaultKnobs: () => createVaultKnobs,
153
+ emptyReadAlarm: () => emptyReadAlarm,
152
154
  guardPort: () => guardPort,
155
+ holdingsDoorIsAbsent: () => holdingsDoorIsAbsent,
153
156
  inferFromUrl: () => inferFromUrl,
154
157
  isHiddenItem: () => isHiddenItem,
155
158
  itemIdAt: () => itemIdAt,
@@ -2660,9 +2663,12 @@ function normalizeLoginUrl(input) {
2660
2663
  return null;
2661
2664
  }
2662
2665
  }
2666
+ function normalizeLoginUrls(inputs) {
2667
+ return inputs.map((u) => u.trim()).filter(Boolean).map((u) => normalizeLoginUrl(u) ?? u);
2668
+ }
2663
2669
  function describeDestination(r) {
2664
2670
  if (r.state === "reachable") return { url: r.normalizedUrl, state: r.state, tone: "success", message: "Website confirmed" };
2665
- if (r.state === "invalid") return { url: r.normalizedUrl, state: r.state, tone: "destructive", message: "Not a public website" };
2671
+ if (r.state === "invalid") return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Not a public website. You can still save it." };
2666
2672
  if (r.state === "not_found") return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Site answered, but this page was not found" };
2667
2673
  return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Couldn\u2019t confirm it. You can still save it." };
2668
2674
  }
@@ -5365,6 +5371,40 @@ function AssignResult({ sent, onAnother, onDone }) {
5365
5371
  ] });
5366
5372
  }
5367
5373
 
5374
+ // src/lists/turn-back-on.ts
5375
+ async function turnBackOn(port, item, fields, options = {}) {
5376
+ const run = options.run ?? ((op) => op());
5377
+ const done = [];
5378
+ if (item.turnedOff) {
5379
+ await run(() => port.fields.updateItem(item, { active: true }));
5380
+ done.push("the item");
5381
+ }
5382
+ let n = 0;
5383
+ for (const f of fields) {
5384
+ if (f.active) continue;
5385
+ await run(() => port.fields.updateField(item, f.id, { active: true }));
5386
+ n += 1;
5387
+ }
5388
+ if (n) done.push(n === 1 ? "1 field" : `${n} fields`);
5389
+ if (fields.some((f) => f.key === OTP_SEED_KEY)) {
5390
+ const codes = await port.authenticator.get(item);
5391
+ if (codes && !codes.enabled) {
5392
+ await run(() => port.authenticator.setEnabled(item, true));
5393
+ done.push("one-time codes");
5394
+ }
5395
+ }
5396
+ if (options.browserFill && !item.browserFillEnabled && item.loginUrls.length > 0) {
5397
+ await run(() => port.fields.updateItem(item, { browserFillEnabled: true }));
5398
+ done.push("browser fill");
5399
+ }
5400
+ return done;
5401
+ }
5402
+ function turnedOnSentence(done) {
5403
+ if (done.length === 0) return "Nothing here was off.";
5404
+ const list = done.length === 1 ? done[0] : `${done.slice(0, -1).join(", ")} and ${done[done.length - 1]}`;
5405
+ return `Turned on ${list}.`;
5406
+ }
5407
+
5368
5408
  // src/ItemPane.tsx
5369
5409
  var import_icons11 = require("@ai-matrx/design-system/icons");
5370
5410
  var import_drafts = require("@ai-matrx/kit/drafts");
@@ -5978,7 +6018,8 @@ function ItemPane(props) {
5978
6018
  const [showBrowser, setShowBrowser] = (0, import_react28.useState)(false);
5979
6019
  const [error, setError] = (0, import_react28.useState)(null);
5980
6020
  const [destinations, setDestinations] = (0, import_react28.useState)({});
5981
- const dirty = editing && isDirty(base, draft);
6021
+ const [savedNew, setSavedNew] = (0, import_react28.useState)(false);
6022
+ const dirty = editing && !savedNew && isDirty(base, draft);
5982
6023
  const declared = definition?.fields ?? [];
5983
6024
  const prev = (0, import_react28.useRef)(null);
5984
6025
  const paneRef = (0, import_react28.useRef)(null);
@@ -6001,8 +6042,11 @@ function ItemPane(props) {
6001
6042
  setShowTags(false);
6002
6043
  setShowDescription(false);
6003
6044
  setShowRecovery(false);
6045
+ setSavedNew(false);
6004
6046
  }, [item?.id, mode]);
6005
- (0, import_react28.useEffect)(() => onDirty(dirty), [dirty, onDirty]);
6047
+ const onDirtyRef = (0, import_react28.useRef)(onDirty);
6048
+ onDirtyRef.current = onDirty;
6049
+ (0, import_react28.useEffect)(() => onDirtyRef.current(dirty), [dirty]);
6006
6050
  const keepKey = draftKey(item?.id ?? null, definition?.key ?? item?.definitionKey ?? null);
6007
6051
  (0, import_react28.useEffect)(() => {
6008
6052
  if (editing && dirty) (0, import_drafts.keepDraft)(keepKey, keptFieldsOf(draft), { basedOn: item?.updatedAt ?? "new" });
@@ -6059,6 +6103,8 @@ function ItemPane(props) {
6059
6103
  const [withCode, n0, enrolled] = await withOtp(r.item, r.notice);
6060
6104
  const [next, n] = await storeQueuedFiles(withCode, n0);
6061
6105
  (0, import_drafts.dropKeptDraft)(keepKey);
6106
+ setSavedNew(true);
6107
+ onDirty(false);
6062
6108
  onSaved(next, true, n, enrolled);
6063
6109
  }
6064
6110
  } catch (e) {
@@ -6121,12 +6167,7 @@ function ItemPane(props) {
6121
6167
  if (!item) return;
6122
6168
  setError(null);
6123
6169
  try {
6124
- if (item.turnedOff) await confirmed(() => port.fields.updateItem(item, { active: true }));
6125
- for (const f of fields) await confirmed(() => port.fields.updateField(item, f.id, { active: true }));
6126
- if (fields.some((f) => f.key === OTP_SEED_KEY)) {
6127
- const codes = await port.authenticator.get(item);
6128
- if (codes && !codes.enabled) await confirmed(() => port.authenticator.setEnabled(item, true));
6129
- }
6170
+ await turnBackOn(port, item, fields, { run: confirmed });
6130
6171
  onChanged();
6131
6172
  } catch (e) {
6132
6173
  if (!wasCancelled(e)) setError(e instanceof Error ? e.message : "It could not be turned on.");
@@ -6181,7 +6222,7 @@ function ItemPane(props) {
6181
6222
  "Unsaved"
6182
6223
  ] }) : null,
6183
6224
  /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { variant: "quiet", disabled: busy, onClick: () => mode === "new" ? onCancelNew() : onMode("view"), children: dirty ? "Discard" : "Cancel" }),
6184
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { variant: "primary", disabled: busy || sent !== null || draft.name.trim() === "" || mode === "edit" && !dirty || mode === "new" && owner === "person" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(recipient.trim()), onClick: () => void save(), children: mode === "new" && owner === "person" ? "Send" : "Save" })
6225
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { variant: "primary", disabled: busy || savedNew || sent !== null || draft.name.trim() === "" || mode === "edit" && !dirty || mode === "new" && owner === "person" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(recipient.trim()), onClick: () => void save(), children: mode === "new" && owner === "person" ? "Send" : "Save" })
6185
6226
  ] }) : item ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "flex shrink-0 items-center gap-1", children: [
6186
6227
  host.page && canUseOnSite(item, host.page.url) ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(UseOnSiteButton, { port, item, pageUrl: host.page.url, onChanged: (next) => onChanged(next) }) : null,
6187
6228
  host.fill && item.capabilities.canUse ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
@@ -6349,7 +6390,8 @@ function ItemPane(props) {
6349
6390
  else delete next[i];
6350
6391
  return next;
6351
6392
  });
6352
- if (r?.url && r.url !== u) setDraft((d) => ({ ...d, loginUrls: d.loginUrls.map((x, j) => j === i && x === u ? r.url : x) }));
6393
+ const stored = r?.url || normalizeLoginUrl(u);
6394
+ if (stored && stored !== u) setDraft((d) => ({ ...d, loginUrls: d.loginUrls.map((x, j) => j === i && x === u ? stored : x) }));
6353
6395
  }),
6354
6396
  onRemove: () => patch({ loginUrls: draft.loginUrls.filter((_, j) => j !== i) }),
6355
6397
  removeLabel: "Remove website"
@@ -6761,8 +6803,16 @@ var import_react30 = require("react");
6761
6803
  var import_controls24 = require("@ai-matrx/design-system/controls");
6762
6804
  var import_jsx_runtime29 = require("react/jsx-runtime");
6763
6805
  var count = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
6764
- function RestorePanel({ lists, item, restore, onClose }) {
6806
+ function RestorePanel({
6807
+ lists,
6808
+ item,
6809
+ restore,
6810
+ turnOn,
6811
+ onClose
6812
+ }) {
6765
6813
  const [preview, setPreview] = (0, import_react30.useState)(null);
6814
+ const [restored, setRestored] = (0, import_react30.useState)(null);
6815
+ const [turnedOn, setTurnedOn] = (0, import_react30.useState)(null);
6766
6816
  const [error, setError] = (0, import_react30.useState)(null);
6767
6817
  const [busy, setBusy] = (0, import_react30.useState)(false);
6768
6818
  const [loading, setLoading] = (0, import_react30.useState)(Boolean(lists.preview));
@@ -6791,13 +6841,54 @@ function RestorePanel({ lists, item, restore, onClose }) {
6791
6841
  setError(null);
6792
6842
  try {
6793
6843
  const result = await restore(preview?.supported ? preview.deletionId : void 0);
6794
- if (result) onClose();
6844
+ if (!result) return;
6845
+ if (turnOn && !result.alreadyRestored && preview?.supported && !preview.priorWasDisabled) setRestored(result);
6846
+ else onClose();
6795
6847
  } catch (e) {
6796
6848
  setError(e instanceof Error && e.message ? e.message : "Restore did not complete. Try again.");
6797
6849
  } finally {
6798
6850
  setBusy(false);
6799
6851
  }
6800
6852
  };
6853
+ const runTurnOn = async () => {
6854
+ if (!restored || !turnOn) return;
6855
+ setBusy(true);
6856
+ setError(null);
6857
+ try {
6858
+ const said = await turnOn(restored.item);
6859
+ if (said !== null) setTurnedOn(said);
6860
+ } catch (e) {
6861
+ setError(e instanceof Error && e.message ? e.message : "It could not be turned on. Turn it on from the item.");
6862
+ } finally {
6863
+ setBusy(false);
6864
+ }
6865
+ };
6866
+ if (restored) {
6867
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
6868
+ Panel,
6869
+ {
6870
+ title: `Restored "${item.name}"`,
6871
+ size: "md",
6872
+ onClose,
6873
+ footer: turnedOn ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_controls24.Button, { variant: "primary", onClick: onClose, children: "Done" }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
6874
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_controls24.Button, { variant: "quiet", onClick: onClose, children: "Leave off" }),
6875
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_controls24.Button, { variant: "primary", icon: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(RestoreGlyph, {}), disabled: busy, onClick: () => void runTurnOn(), children: busy ? "Turning on\u2026" : "Turn these back on" })
6876
+ ] }),
6877
+ children: [
6878
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(VaultSection, { title: turnedOn ? "Turned on" : "Back, turned off", children: [
6879
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(VaultRow, { label: "Came back", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: "text-foreground", children: [
6880
+ count(restored.restoredFields, "field"),
6881
+ ", ",
6882
+ count(restored.restoredAttachments, "file")
6883
+ ] }) }),
6884
+ turnedOn ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(VaultRow, { label: "Now on", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { role: "status", className: "text-foreground", children: turnedOn }) }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(VaultRow, { label: "Turns on", hint: "One press, nothing else changes", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "text-muted-foreground", children: "The item, its fields, codes and browser fill" }) }),
6885
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(VaultRow, { label: "Stays off", hint: "Choose these again yourself", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "text-muted-foreground", children: "Sharing, sandbox use" }) })
6886
+ ] }),
6887
+ error ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { role: "alert", className: "px-1 text-sm text-destructive", children: error }) : null
6888
+ ]
6889
+ }
6890
+ );
6891
+ }
6801
6892
  return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
6802
6893
  Panel,
6803
6894
  {
@@ -6838,7 +6929,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6838
6929
  setMessage(null);
6839
6930
  try {
6840
6931
  const r = await confirmed(() => port.lists.restore(item, deletionId));
6841
- 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.` });
6932
+ setMessage({ tone: "ok", text: r.alreadyRestored ? `"${item.name}" was already restored.` : `Restored "${item.name}" with ${r.restoredFields} fields and ${r.restoredAttachments} files, turned off.` });
6842
6933
  onChanged();
6843
6934
  return r;
6844
6935
  } catch (e) {
@@ -6848,6 +6939,19 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6848
6939
  setBusyId(null);
6849
6940
  }
6850
6941
  };
6942
+ const turnOn = async (item) => {
6943
+ try {
6944
+ const done = await turnBackOn(port, item, item.fields, { browserFill: true, run: confirmed });
6945
+ const said = turnedOnSentence(done);
6946
+ setMessage({ tone: "ok", text: `"${item.name}": ${said}` });
6947
+ onChanged();
6948
+ return said;
6949
+ } catch (e) {
6950
+ onChanged();
6951
+ if (wasCancelled(e)) return null;
6952
+ throw e;
6953
+ }
6954
+ };
6851
6955
  return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3", children: [
6852
6956
  header,
6853
6957
  /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(VaultSection, { title: "Trash", children: shown.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(Empty, { children: deleted.length === 0 ? "The trash is empty." : "Nothing matches." }) : shown.map((item) => (
@@ -6861,7 +6965,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6861
6965
  /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_controls25.Button, { variant: "outline", icon: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(RestoreGlyph, {}), disabled: busyId !== null || !item.deletion, onClick: () => setRestoring(item), children: "Restore" })
6862
6966
  ] }, item.id)
6863
6967
  )) }),
6864
- restoring ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(RestorePanel, { lists: port.lists, item: restoring, restore: (deletionId) => restore(restoring, deletionId), onClose: () => setRestoring(null) }) : null,
6968
+ restoring ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(RestorePanel, { lists: port.lists, item: restoring, restore: (deletionId) => restore(restoring, deletionId), turnOn, onClose: () => setRestoring(null) }) : null,
6865
6969
  message2 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { role: "status", className: message2.tone === "error" ? "px-1 text-sm text-destructive" : "px-1 text-sm text-muted-foreground", children: message2.text }) : null
6866
6970
  ] });
6867
6971
  }
@@ -9126,7 +9230,7 @@ function createFieldsPort(ctx) {
9126
9230
  const body = { initiation: "user" };
9127
9231
  if (patch.uriMatchMode !== void 0) body.uri_match_mode = patch.uriMatchMode;
9128
9232
  if (patch.browserFillEnabled !== void 0) body.browser_fill_enabled = patch.browserFillEnabled;
9129
- if (patch.loginUrls !== void 0) body.login_urls = patch.loginUrls;
9233
+ if (patch.loginUrls !== void 0) body.login_urls = normalizeLoginUrls(patch.loginUrls);
9130
9234
  if (patch.active !== void 0) body.status = patch.active ? "active" : "disabled";
9131
9235
  await account3.updateItem(api, { path: { item_id: item.id }, body }, ctx.optsFor(item));
9132
9236
  return reread(item);
@@ -9138,6 +9242,33 @@ function createFieldsPort(ctx) {
9138
9242
  };
9139
9243
  }
9140
9244
 
9245
+ // src/server-reasons.ts
9246
+ var GENERIC = [/please check your input/i, /^\s*request failed\s*\(\d+\)\s*$/i, /^\s*bad request\.?\s*$/i, /^\s*HTTP \d+\s*$/i];
9247
+ var isGeneric = (s) => GENERIC.some((re) => re.test(s));
9248
+ function cleanReason(detail) {
9249
+ const s = detail.replace(/^\s*validation error:\s*/i, "").replace(/^\s*value error,\s*/i, "").trim();
9250
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
9251
+ }
9252
+ function serverReason(e) {
9253
+ if (!e || typeof e !== "object") return null;
9254
+ const { status, detail, message: message2 } = e;
9255
+ if (typeof status !== "number" || status < 400 || status >= 500) return null;
9256
+ if (typeof detail !== "string" || !detail.trim() || isGeneric(detail)) return null;
9257
+ const shown = typeof message2 === "string" ? message2 : "";
9258
+ if (shown && !isGeneric(shown)) return null;
9259
+ return cleanReason(detail);
9260
+ }
9261
+ function withReason(e) {
9262
+ const reason = serverReason(e);
9263
+ if (reason) Object.assign(e, { message: reason, userMessage: reason });
9264
+ return e;
9265
+ }
9266
+ function withServerReasons(api) {
9267
+ const wrapped = Object.create(api);
9268
+ wrapped.call = (...args) => api.call(...args).catch((e) => Promise.reject(withReason(e)));
9269
+ return wrapped;
9270
+ }
9271
+
9141
9272
  // src/io/api.ts
9142
9273
  var account4 = __toESM(require("@ai-matrx/api/account"), 1);
9143
9274
  var orgOpts = (organizationId) => organizationId ? { organizationId } : void 0;
@@ -9410,7 +9541,7 @@ function createSharingPort(ctx, options = {}) {
9410
9541
  definition_key: definition.key,
9411
9542
  provider_key: definition.providerKey,
9412
9543
  tags: draft.tags,
9413
- login_urls: draft.loginUrls,
9544
+ login_urls: normalizeLoginUrls(draft.loginUrls),
9414
9545
  notes: draft.notes || null,
9415
9546
  generate_field_key: generateFieldKey,
9416
9547
  initiation: "user",
@@ -9607,7 +9738,8 @@ async function retrying(run, tries = 3) {
9607
9738
  }
9608
9739
  }
9609
9740
  }
9610
- function createApiVaultPort(api, options = {}) {
9741
+ function createApiVaultPort(hostApi, options = {}) {
9742
+ const api = withServerReasons(hostApi);
9611
9743
  let defs = /* @__PURE__ */ new Map();
9612
9744
  let auth = /* @__PURE__ */ new Map();
9613
9745
  let orgs = [];
@@ -9762,6 +9894,7 @@ function createApiVaultPort(api, options = {}) {
9762
9894
  const declared = definition.fields;
9763
9895
  const encrypted = declared.filter((f) => f.storage === "encrypted" && (draft.values[f.key] ?? "") !== "");
9764
9896
  const meta = declared.filter((f) => f.storage === "metadata" && (draft.values[f.key] ?? "") !== "");
9897
+ const loginUrls = normalizeLoginUrls(draft.loginUrls);
9765
9898
  const w = await account9.createItem(api, {
9766
9899
  body: {
9767
9900
  display_name: draft.name.trim(),
@@ -9769,10 +9902,10 @@ function createApiVaultPort(api, options = {}) {
9769
9902
  definition_key: definition.key,
9770
9903
  provider_key: definition.providerKey,
9771
9904
  tags: draft.tags,
9772
- login_urls: draft.loginUrls,
9905
+ login_urls: loginUrls,
9773
9906
  // A login a person saves with its website is offered on that website (the server's own
9774
9907
  // default is off, which made "Save a login" on a page answer "No saved login" there).
9775
- ...draft.browser ? { uri_match_mode: draft.browser.uriMatchMode, browser_fill_enabled: draft.browser.browserFillEnabled } : draft.loginUrls.length > 0 ? { browser_fill_enabled: true } : {},
9908
+ ...draft.browser ? { uri_match_mode: draft.browser.uriMatchMode, browser_fill_enabled: draft.browser.browserFillEnabled } : loginUrls.length > 0 ? { browser_fill_enabled: true } : {},
9776
9909
  notes: draft.notes || null,
9777
9910
  principal: organizationId ? { type: "organization", organization_id: organizationId } : { type: "user" },
9778
9911
  initiation: "user",
@@ -9799,7 +9932,7 @@ function createApiVaultPort(api, options = {}) {
9799
9932
  else body.description = draft.description.trim();
9800
9933
  }
9801
9934
  if (JSON.stringify(draft.tags) !== JSON.stringify(item.tags)) body.tags = draft.tags;
9802
- if (JSON.stringify(draft.loginUrls) !== JSON.stringify(item.loginUrls)) body.login_urls = draft.loginUrls;
9935
+ if (JSON.stringify(draft.loginUrls) !== JSON.stringify(item.loginUrls)) body.login_urls = normalizeLoginUrls(draft.loginUrls);
9803
9936
  if (draft.notes !== item.notes) {
9804
9937
  if (draft.notes === "") body.clear_notes = true;
9805
9938
  else body.notes = draft.notes;
@@ -9882,4 +10015,33 @@ function vaultInventoryScope(inventory, clicked = null) {
9882
10015
  function itemIdAt(target) {
9883
10016
  return target?.closest("[data-item]")?.getAttribute("data-item") ?? null;
9884
10017
  }
10018
+
10019
+ // src/io/empty-read.ts
10020
+ var HOLDINGS_DOOR_MIGRATION = "migrations/campaign/errorshonest_s8_a_credential_says_how_much_it_holds.sql";
10021
+ function holdingsDoorIsAbsent(code) {
10022
+ return code === "PGRST202" || code === "42883";
10023
+ }
10024
+ function staleAlarm(total) {
10025
+ return `Your vault has ${total} ${total === 1 ? "item" : "items"} but none of their fields or files could be read. Either every one of these items is genuinely still empty, or the database refused the read without reporting an error \u2014 the second is a known failure mode of this screen (DD-160). Nothing has been lost: reload, and if the items are still blank, report it rather than re-entering the credentials.`;
10026
+ }
10027
+ function emptyReadAlarm(total, emptyIds, holdings) {
10028
+ if (emptyIds.length === 0) return null;
10029
+ if (holdings.state === "answered") {
10030
+ const asked = new Set(emptyIds);
10031
+ const unread = holdings.rows.filter(
10032
+ (r) => asked.has(r.credential_item_id) && (r.field_count > 0 || r.file_count > 0)
10033
+ );
10034
+ if (unread.length === 0) return null;
10035
+ const n = unread.length;
10036
+ return `${n === 1 ? "One credential" : `${n} credentials`} in this vault ${n === 1 ? "holds" : "hold"} fields or files this screen could not read \u2014 the database refused the read without reporting an error, a known failure mode of this screen (DD-160). Nothing has been lost: reload, and if ${n === 1 ? "it is" : "they are"} still blank, report it rather than re-entering the credentials.`;
10037
+ }
10038
+ if (emptyIds.length < total) return null;
10039
+ if (holdings.state === "absent") {
10040
+ console.warn(
10041
+ `[vault] users.credential_item_holdings is not on this database (${holdings.why}); the empty-read alarm falls back to the whole-vault rule until ${HOLDINGS_DOOR_MIGRATION} is applied.`
10042
+ );
10043
+ return staleAlarm(total);
10044
+ }
10045
+ return `${staleAlarm(total)} (The check that tells an empty credential from an unreadable one did not answer either: ${holdings.why})`;
10046
+ }
9885
10047
  //# sourceMappingURL=index.cjs.map