@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/dist/index.d.cts CHANGED
@@ -1420,7 +1420,7 @@ interface ApiVaultPortOptions {
1420
1420
  /** The host's `supabase.auth`: turns on approving a browser with a passkey. */
1421
1421
  passkeys?: PasskeyAuthLike;
1422
1422
  }
1423
- declare function createApiVaultPort(api: Api, options?: ApiVaultPortOptions): VaultPort;
1423
+ declare function createApiVaultPort(hostApi: Api, options?: ApiVaultPortOptions): VaultPort;
1424
1424
 
1425
1425
  /**
1426
1426
  * Wrap any call that shows, copies or exports a secret. When the server answers "sign in again",
@@ -1471,4 +1471,48 @@ interface RowMenuEntry {
1471
1471
  /** The entries of the row a right-click landed on (`[data-item]`), else none. */
1472
1472
  declare function rowMenuEntriesAt(target: Element | null | undefined): readonly RowMenuEntry[];
1473
1473
 
1474
- export { type ApiVaultPortOptions, CATEGORIES, CONFIRM_RETURN_KEY, type CategoryId, type Confirmed, type CopyOptions, DEFAULT_LOCATION, type DefinitionField, type FieldHandling, type FieldKind, GenerateButton, HIDDEN_ITEM_TAG, type IdentityAccount, IdentityGate, type IdentityMethod, type ItemDraft, LOCKED_MESSAGE, type NoticeKind, type Notify, type ReturnStore, type RowMenuEntry, type RowMenuEntryKind, type SaveResult, type SupabaseAuthLike, type SupabaseIdentityOptions, type TotpReading, type UriMatchMode, type UrlGuess, type VaultAgentScope, VaultApp, type VaultAppProps, type VaultAttachment, type VaultAuthenticator, type VaultClipboard, type VaultCustomField, type VaultDefinition, type VaultField, type VaultHostCapabilities, type VaultIdentity, type VaultInventory, type VaultItem, type VaultItemCapabilities, type VaultKnobs, type VaultLayout, type VaultLocation, type VaultLock, VaultLockedError, type VaultOrganizationRef, type VaultOwner, type VaultPort, type VaultSelection, type VaultSnapshot, type VaultSort, type VaultUserStateDoor, categoryOf, copyValue, createApiVaultPort, createSupabaseIdentity, createVaultKnobs, guardPort, inferFromUrl, isHiddenItem, itemIdAt, locationFromParams, locationToParams, loginSiteOf, matchesQuery, needsRecentSignIn, parseDefinitions, providerOf, rowMenuEntriesAt, siteDomainOf, sortItems, useConfirmed, vaultInventoryScope, vaultItemFacts, wasCancelled };
1474
+ /**
1475
+ * AN EMPTY CREDENTIAL IS A FACT; AN EMPTY READ OF A FULL ONE IS A FAILURE (DD-160, lane
1476
+ * ERRORS-HONEST 2026-09-24).
1477
+ *
1478
+ * RLS refuses a row by returning nothing, so a list that reads a credential's fields and gets `[]`
1479
+ * cannot tell "there is nothing in it" from "the database filtered the read" — from 2026-07 to
1480
+ * 2026-09-12 a restrictive policy hid every field from its own owner and the screen reported
1481
+ * success. The list used to guess from the whole scope ("no field and no file anywhere" = alarm),
1482
+ * which blanked an organization whose only credential was simply empty (Pinecrest Records'
1483
+ * Bandcamp label login).
1484
+ *
1485
+ * The guess is replaced by the database's own answer: `users.credential_item_holdings(ids)` counts
1486
+ * the live field and file rows of each credential the caller may read (counts only, never a value).
1487
+ * The alarm fires only for a credential that HOLDS something the read did not return.
1488
+ */
1489
+ type HoldingRow = {
1490
+ credential_item_id: string;
1491
+ field_count: number;
1492
+ file_count: number;
1493
+ };
1494
+ type Holdings = {
1495
+ state: "answered";
1496
+ rows: HoldingRow[];
1497
+ }
1498
+ /** The door is not on this database yet — the migration below has not been applied. */
1499
+ | {
1500
+ state: "absent";
1501
+ why: string;
1502
+ } | {
1503
+ state: "unavailable";
1504
+ why: string;
1505
+ };
1506
+ declare const HOLDINGS_DOOR_MIGRATION = "migrations/campaign/errorshonest_s8_a_credential_says_how_much_it_holds.sql";
1507
+ /** The error codes that mean "no such function on this database", not "the read failed". */
1508
+ declare function holdingsDoorIsAbsent(code: string | undefined | null): boolean;
1509
+ /**
1510
+ * The alarm sentence, or null when every credential that read empty is genuinely empty.
1511
+ *
1512
+ * @param total how many credentials the list read
1513
+ * @param emptyIds the ones whose field AND file reads came back empty
1514
+ * @param holdings the database's answer for `emptyIds`
1515
+ */
1516
+ declare function emptyReadAlarm(total: number, emptyIds: readonly string[], holdings: Holdings): string | null;
1517
+
1518
+ export { type ApiVaultPortOptions, CATEGORIES, CONFIRM_RETURN_KEY, type CategoryId, type Confirmed, type CopyOptions, DEFAULT_LOCATION, type DefinitionField, type FieldHandling, type FieldKind, GenerateButton, HIDDEN_ITEM_TAG, HOLDINGS_DOOR_MIGRATION, type HoldingRow, type Holdings, type IdentityAccount, IdentityGate, type IdentityMethod, type ItemDraft, LOCKED_MESSAGE, type NoticeKind, type Notify, type ReturnStore, type RowMenuEntry, type RowMenuEntryKind, type SaveResult, type SupabaseAuthLike, type SupabaseIdentityOptions, type TotpReading, type UriMatchMode, type UrlGuess, type VaultAgentScope, VaultApp, type VaultAppProps, type VaultAttachment, type VaultAuthenticator, type VaultClipboard, type VaultCustomField, type VaultDefinition, type VaultField, type VaultHostCapabilities, type VaultIdentity, type VaultInventory, type VaultItem, type VaultItemCapabilities, type VaultKnobs, type VaultLayout, type VaultLocation, type VaultLock, VaultLockedError, type VaultOrganizationRef, type VaultOwner, type VaultPort, type VaultSelection, type VaultSnapshot, type VaultSort, type VaultUserStateDoor, categoryOf, copyValue, createApiVaultPort, createSupabaseIdentity, createVaultKnobs, emptyReadAlarm, guardPort, holdingsDoorIsAbsent, inferFromUrl, isHiddenItem, itemIdAt, locationFromParams, locationToParams, loginSiteOf, matchesQuery, needsRecentSignIn, parseDefinitions, providerOf, rowMenuEntriesAt, siteDomainOf, sortItems, useConfirmed, vaultInventoryScope, vaultItemFacts, wasCancelled };
package/dist/index.d.ts CHANGED
@@ -1420,7 +1420,7 @@ interface ApiVaultPortOptions {
1420
1420
  /** The host's `supabase.auth`: turns on approving a browser with a passkey. */
1421
1421
  passkeys?: PasskeyAuthLike;
1422
1422
  }
1423
- declare function createApiVaultPort(api: Api, options?: ApiVaultPortOptions): VaultPort;
1423
+ declare function createApiVaultPort(hostApi: Api, options?: ApiVaultPortOptions): VaultPort;
1424
1424
 
1425
1425
  /**
1426
1426
  * Wrap any call that shows, copies or exports a secret. When the server answers "sign in again",
@@ -1471,4 +1471,48 @@ interface RowMenuEntry {
1471
1471
  /** The entries of the row a right-click landed on (`[data-item]`), else none. */
1472
1472
  declare function rowMenuEntriesAt(target: Element | null | undefined): readonly RowMenuEntry[];
1473
1473
 
1474
- export { type ApiVaultPortOptions, CATEGORIES, CONFIRM_RETURN_KEY, type CategoryId, type Confirmed, type CopyOptions, DEFAULT_LOCATION, type DefinitionField, type FieldHandling, type FieldKind, GenerateButton, HIDDEN_ITEM_TAG, type IdentityAccount, IdentityGate, type IdentityMethod, type ItemDraft, LOCKED_MESSAGE, type NoticeKind, type Notify, type ReturnStore, type RowMenuEntry, type RowMenuEntryKind, type SaveResult, type SupabaseAuthLike, type SupabaseIdentityOptions, type TotpReading, type UriMatchMode, type UrlGuess, type VaultAgentScope, VaultApp, type VaultAppProps, type VaultAttachment, type VaultAuthenticator, type VaultClipboard, type VaultCustomField, type VaultDefinition, type VaultField, type VaultHostCapabilities, type VaultIdentity, type VaultInventory, type VaultItem, type VaultItemCapabilities, type VaultKnobs, type VaultLayout, type VaultLocation, type VaultLock, VaultLockedError, type VaultOrganizationRef, type VaultOwner, type VaultPort, type VaultSelection, type VaultSnapshot, type VaultSort, type VaultUserStateDoor, categoryOf, copyValue, createApiVaultPort, createSupabaseIdentity, createVaultKnobs, guardPort, inferFromUrl, isHiddenItem, itemIdAt, locationFromParams, locationToParams, loginSiteOf, matchesQuery, needsRecentSignIn, parseDefinitions, providerOf, rowMenuEntriesAt, siteDomainOf, sortItems, useConfirmed, vaultInventoryScope, vaultItemFacts, wasCancelled };
1474
+ /**
1475
+ * AN EMPTY CREDENTIAL IS A FACT; AN EMPTY READ OF A FULL ONE IS A FAILURE (DD-160, lane
1476
+ * ERRORS-HONEST 2026-09-24).
1477
+ *
1478
+ * RLS refuses a row by returning nothing, so a list that reads a credential's fields and gets `[]`
1479
+ * cannot tell "there is nothing in it" from "the database filtered the read" — from 2026-07 to
1480
+ * 2026-09-12 a restrictive policy hid every field from its own owner and the screen reported
1481
+ * success. The list used to guess from the whole scope ("no field and no file anywhere" = alarm),
1482
+ * which blanked an organization whose only credential was simply empty (Pinecrest Records'
1483
+ * Bandcamp label login).
1484
+ *
1485
+ * The guess is replaced by the database's own answer: `users.credential_item_holdings(ids)` counts
1486
+ * the live field and file rows of each credential the caller may read (counts only, never a value).
1487
+ * The alarm fires only for a credential that HOLDS something the read did not return.
1488
+ */
1489
+ type HoldingRow = {
1490
+ credential_item_id: string;
1491
+ field_count: number;
1492
+ file_count: number;
1493
+ };
1494
+ type Holdings = {
1495
+ state: "answered";
1496
+ rows: HoldingRow[];
1497
+ }
1498
+ /** The door is not on this database yet — the migration below has not been applied. */
1499
+ | {
1500
+ state: "absent";
1501
+ why: string;
1502
+ } | {
1503
+ state: "unavailable";
1504
+ why: string;
1505
+ };
1506
+ declare const HOLDINGS_DOOR_MIGRATION = "migrations/campaign/errorshonest_s8_a_credential_says_how_much_it_holds.sql";
1507
+ /** The error codes that mean "no such function on this database", not "the read failed". */
1508
+ declare function holdingsDoorIsAbsent(code: string | undefined | null): boolean;
1509
+ /**
1510
+ * The alarm sentence, or null when every credential that read empty is genuinely empty.
1511
+ *
1512
+ * @param total how many credentials the list read
1513
+ * @param emptyIds the ones whose field AND file reads came back empty
1514
+ * @param holdings the database's answer for `emptyIds`
1515
+ */
1516
+ declare function emptyReadAlarm(total: number, emptyIds: readonly string[], holdings: Holdings): string | null;
1517
+
1518
+ export { type ApiVaultPortOptions, CATEGORIES, CONFIRM_RETURN_KEY, type CategoryId, type Confirmed, type CopyOptions, DEFAULT_LOCATION, type DefinitionField, type FieldHandling, type FieldKind, GenerateButton, HIDDEN_ITEM_TAG, HOLDINGS_DOOR_MIGRATION, type HoldingRow, type Holdings, type IdentityAccount, IdentityGate, type IdentityMethod, type ItemDraft, LOCKED_MESSAGE, type NoticeKind, type Notify, type ReturnStore, type RowMenuEntry, type RowMenuEntryKind, type SaveResult, type SupabaseAuthLike, type SupabaseIdentityOptions, type TotpReading, type UriMatchMode, type UrlGuess, type VaultAgentScope, VaultApp, type VaultAppProps, type VaultAttachment, type VaultAuthenticator, type VaultClipboard, type VaultCustomField, type VaultDefinition, type VaultField, type VaultHostCapabilities, type VaultIdentity, type VaultInventory, type VaultItem, type VaultItemCapabilities, type VaultKnobs, type VaultLayout, type VaultLocation, type VaultLock, VaultLockedError, type VaultOrganizationRef, type VaultOwner, type VaultPort, type VaultSelection, type VaultSnapshot, type VaultSort, type VaultUserStateDoor, categoryOf, copyValue, createApiVaultPort, createSupabaseIdentity, createVaultKnobs, emptyReadAlarm, guardPort, holdingsDoorIsAbsent, inferFromUrl, isHiddenItem, itemIdAt, locationFromParams, locationToParams, loginSiteOf, matchesQuery, needsRecentSignIn, parseDefinitions, providerOf, rowMenuEntriesAt, siteDomainOf, sortItems, useConfirmed, vaultInventoryScope, vaultItemFacts, wasCancelled };
package/dist/index.js CHANGED
@@ -2637,9 +2637,12 @@ function normalizeLoginUrl(input) {
2637
2637
  return null;
2638
2638
  }
2639
2639
  }
2640
+ function normalizeLoginUrls(inputs) {
2641
+ return inputs.map((u) => u.trim()).filter(Boolean).map((u) => normalizeLoginUrl(u) ?? u);
2642
+ }
2640
2643
  function describeDestination(r) {
2641
2644
  if (r.state === "reachable") return { url: r.normalizedUrl, state: r.state, tone: "success", message: "Website confirmed" };
2642
- if (r.state === "invalid") return { url: r.normalizedUrl, state: r.state, tone: "destructive", message: "Not a public website" };
2645
+ if (r.state === "invalid") return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Not a public website. You can still save it." };
2643
2646
  if (r.state === "not_found") return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Site answered, but this page was not found" };
2644
2647
  return { url: r.normalizedUrl, state: r.state, tone: "warning", message: "Couldn\u2019t confirm it. You can still save it." };
2645
2648
  }
@@ -5345,6 +5348,40 @@ function AssignResult({ sent, onAnother, onDone }) {
5345
5348
  ] });
5346
5349
  }
5347
5350
 
5351
+ // src/lists/turn-back-on.ts
5352
+ async function turnBackOn(port, item, fields, options = {}) {
5353
+ const run = options.run ?? ((op) => op());
5354
+ const done = [];
5355
+ if (item.turnedOff) {
5356
+ await run(() => port.fields.updateItem(item, { active: true }));
5357
+ done.push("the item");
5358
+ }
5359
+ let n = 0;
5360
+ for (const f of fields) {
5361
+ if (f.active) continue;
5362
+ await run(() => port.fields.updateField(item, f.id, { active: true }));
5363
+ n += 1;
5364
+ }
5365
+ if (n) done.push(n === 1 ? "1 field" : `${n} fields`);
5366
+ if (fields.some((f) => f.key === OTP_SEED_KEY)) {
5367
+ const codes = await port.authenticator.get(item);
5368
+ if (codes && !codes.enabled) {
5369
+ await run(() => port.authenticator.setEnabled(item, true));
5370
+ done.push("one-time codes");
5371
+ }
5372
+ }
5373
+ if (options.browserFill && !item.browserFillEnabled && item.loginUrls.length > 0) {
5374
+ await run(() => port.fields.updateItem(item, { browserFillEnabled: true }));
5375
+ done.push("browser fill");
5376
+ }
5377
+ return done;
5378
+ }
5379
+ function turnedOnSentence(done) {
5380
+ if (done.length === 0) return "Nothing here was off.";
5381
+ const list = done.length === 1 ? done[0] : `${done.slice(0, -1).join(", ")} and ${done[done.length - 1]}`;
5382
+ return `Turned on ${list}.`;
5383
+ }
5384
+
5348
5385
  // src/ItemPane.tsx
5349
5386
  import { HistoryIcon, SettingsIcon, UsersIcon as UsersIcon2 } from "@ai-matrx/design-system/icons";
5350
5387
  import { dropKeptDraft, keepDraft, readKeptDraft } from "@ai-matrx/kit/drafts";
@@ -5958,7 +5995,8 @@ function ItemPane(props) {
5958
5995
  const [showBrowser, setShowBrowser] = useState27(false);
5959
5996
  const [error, setError] = useState27(null);
5960
5997
  const [destinations, setDestinations] = useState27({});
5961
- const dirty = editing && isDirty(base, draft);
5998
+ const [savedNew, setSavedNew] = useState27(false);
5999
+ const dirty = editing && !savedNew && isDirty(base, draft);
5962
6000
  const declared = definition?.fields ?? [];
5963
6001
  const prev = useRef14(null);
5964
6002
  const paneRef = useRef14(null);
@@ -5981,8 +6019,11 @@ function ItemPane(props) {
5981
6019
  setShowTags(false);
5982
6020
  setShowDescription(false);
5983
6021
  setShowRecovery(false);
6022
+ setSavedNew(false);
5984
6023
  }, [item?.id, mode]);
5985
- useEffect16(() => onDirty(dirty), [dirty, onDirty]);
6024
+ const onDirtyRef = useRef14(onDirty);
6025
+ onDirtyRef.current = onDirty;
6026
+ useEffect16(() => onDirtyRef.current(dirty), [dirty]);
5986
6027
  const keepKey = draftKey(item?.id ?? null, definition?.key ?? item?.definitionKey ?? null);
5987
6028
  useEffect16(() => {
5988
6029
  if (editing && dirty) keepDraft(keepKey, keptFieldsOf(draft), { basedOn: item?.updatedAt ?? "new" });
@@ -6039,6 +6080,8 @@ function ItemPane(props) {
6039
6080
  const [withCode, n0, enrolled] = await withOtp(r.item, r.notice);
6040
6081
  const [next, n] = await storeQueuedFiles(withCode, n0);
6041
6082
  dropKeptDraft(keepKey);
6083
+ setSavedNew(true);
6084
+ onDirty(false);
6042
6085
  onSaved(next, true, n, enrolled);
6043
6086
  }
6044
6087
  } catch (e) {
@@ -6101,12 +6144,7 @@ function ItemPane(props) {
6101
6144
  if (!item) return;
6102
6145
  setError(null);
6103
6146
  try {
6104
- if (item.turnedOff) await confirmed(() => port.fields.updateItem(item, { active: true }));
6105
- for (const f of fields) await confirmed(() => port.fields.updateField(item, f.id, { active: true }));
6106
- if (fields.some((f) => f.key === OTP_SEED_KEY)) {
6107
- const codes = await port.authenticator.get(item);
6108
- if (codes && !codes.enabled) await confirmed(() => port.authenticator.setEnabled(item, true));
6109
- }
6147
+ await turnBackOn(port, item, fields, { run: confirmed });
6110
6148
  onChanged();
6111
6149
  } catch (e) {
6112
6150
  if (!wasCancelled(e)) setError(e instanceof Error ? e.message : "It could not be turned on.");
@@ -6161,7 +6199,7 @@ function ItemPane(props) {
6161
6199
  "Unsaved"
6162
6200
  ] }) : null,
6163
6201
  /* @__PURE__ */ jsx27(Button22, { variant: "quiet", disabled: busy, onClick: () => mode === "new" ? onCancelNew() : onMode("view"), children: dirty ? "Discard" : "Cancel" }),
6164
- /* @__PURE__ */ jsx27(Button22, { 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" })
6202
+ /* @__PURE__ */ jsx27(Button22, { 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" })
6165
6203
  ] }) : item ? /* @__PURE__ */ jsxs25("div", { className: "flex shrink-0 items-center gap-1", children: [
6166
6204
  host.page && canUseOnSite(item, host.page.url) ? /* @__PURE__ */ jsx27(UseOnSiteButton, { port, item, pageUrl: host.page.url, onChanged: (next) => onChanged(next) }) : null,
6167
6205
  host.fill && item.capabilities.canUse ? /* @__PURE__ */ jsx27(
@@ -6329,7 +6367,8 @@ function ItemPane(props) {
6329
6367
  else delete next[i];
6330
6368
  return next;
6331
6369
  });
6332
- if (r?.url && r.url !== u) setDraft((d) => ({ ...d, loginUrls: d.loginUrls.map((x, j) => j === i && x === u ? r.url : x) }));
6370
+ const stored = r?.url || normalizeLoginUrl(u);
6371
+ if (stored && stored !== u) setDraft((d) => ({ ...d, loginUrls: d.loginUrls.map((x, j) => j === i && x === u ? stored : x) }));
6333
6372
  }),
6334
6373
  onRemove: () => patch({ loginUrls: draft.loginUrls.filter((_, j) => j !== i) }),
6335
6374
  removeLabel: "Remove website"
@@ -6741,8 +6780,16 @@ import { useEffect as useEffect18, useState as useState29 } from "react";
6741
6780
  import { Button as Button24 } from "@ai-matrx/design-system/controls";
6742
6781
  import { Fragment as Fragment19, jsx as jsx29, jsxs as jsxs27 } from "react/jsx-runtime";
6743
6782
  var count = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
6744
- function RestorePanel({ lists, item, restore, onClose }) {
6783
+ function RestorePanel({
6784
+ lists,
6785
+ item,
6786
+ restore,
6787
+ turnOn,
6788
+ onClose
6789
+ }) {
6745
6790
  const [preview, setPreview] = useState29(null);
6791
+ const [restored, setRestored] = useState29(null);
6792
+ const [turnedOn, setTurnedOn] = useState29(null);
6746
6793
  const [error, setError] = useState29(null);
6747
6794
  const [busy, setBusy] = useState29(false);
6748
6795
  const [loading, setLoading] = useState29(Boolean(lists.preview));
@@ -6771,13 +6818,54 @@ function RestorePanel({ lists, item, restore, onClose }) {
6771
6818
  setError(null);
6772
6819
  try {
6773
6820
  const result = await restore(preview?.supported ? preview.deletionId : void 0);
6774
- if (result) onClose();
6821
+ if (!result) return;
6822
+ if (turnOn && !result.alreadyRestored && preview?.supported && !preview.priorWasDisabled) setRestored(result);
6823
+ else onClose();
6775
6824
  } catch (e) {
6776
6825
  setError(e instanceof Error && e.message ? e.message : "Restore did not complete. Try again.");
6777
6826
  } finally {
6778
6827
  setBusy(false);
6779
6828
  }
6780
6829
  };
6830
+ const runTurnOn = async () => {
6831
+ if (!restored || !turnOn) return;
6832
+ setBusy(true);
6833
+ setError(null);
6834
+ try {
6835
+ const said = await turnOn(restored.item);
6836
+ if (said !== null) setTurnedOn(said);
6837
+ } catch (e) {
6838
+ setError(e instanceof Error && e.message ? e.message : "It could not be turned on. Turn it on from the item.");
6839
+ } finally {
6840
+ setBusy(false);
6841
+ }
6842
+ };
6843
+ if (restored) {
6844
+ return /* @__PURE__ */ jsxs27(
6845
+ Panel,
6846
+ {
6847
+ title: `Restored "${item.name}"`,
6848
+ size: "md",
6849
+ onClose,
6850
+ footer: turnedOn ? /* @__PURE__ */ jsx29(Button24, { variant: "primary", onClick: onClose, children: "Done" }) : /* @__PURE__ */ jsxs27(Fragment19, { children: [
6851
+ /* @__PURE__ */ jsx29(Button24, { variant: "quiet", onClick: onClose, children: "Leave off" }),
6852
+ /* @__PURE__ */ jsx29(Button24, { variant: "primary", icon: /* @__PURE__ */ jsx29(RestoreGlyph, {}), disabled: busy, onClick: () => void runTurnOn(), children: busy ? "Turning on\u2026" : "Turn these back on" })
6853
+ ] }),
6854
+ children: [
6855
+ /* @__PURE__ */ jsxs27(VaultSection, { title: turnedOn ? "Turned on" : "Back, turned off", children: [
6856
+ /* @__PURE__ */ jsx29(VaultRow, { label: "Came back", children: /* @__PURE__ */ jsxs27("span", { className: "text-foreground", children: [
6857
+ count(restored.restoredFields, "field"),
6858
+ ", ",
6859
+ count(restored.restoredAttachments, "file")
6860
+ ] }) }),
6861
+ turnedOn ? /* @__PURE__ */ jsx29(VaultRow, { label: "Now on", children: /* @__PURE__ */ jsx29("span", { role: "status", className: "text-foreground", children: turnedOn }) }) : /* @__PURE__ */ jsx29(VaultRow, { label: "Turns on", hint: "One press, nothing else changes", children: /* @__PURE__ */ jsx29("span", { className: "text-muted-foreground", children: "The item, its fields, codes and browser fill" }) }),
6862
+ /* @__PURE__ */ jsx29(VaultRow, { label: "Stays off", hint: "Choose these again yourself", children: /* @__PURE__ */ jsx29("span", { className: "text-muted-foreground", children: "Sharing, sandbox use" }) })
6863
+ ] }),
6864
+ error ? /* @__PURE__ */ jsx29("p", { role: "alert", className: "px-1 text-sm text-destructive", children: error }) : null
6865
+ ]
6866
+ }
6867
+ );
6868
+ }
6781
6869
  return /* @__PURE__ */ jsxs27(
6782
6870
  Panel,
6783
6871
  {
@@ -6818,7 +6906,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6818
6906
  setMessage(null);
6819
6907
  try {
6820
6908
  const r = await confirmed(() => port.lists.restore(item, deletionId));
6821
- 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.` });
6909
+ setMessage({ tone: "ok", text: r.alreadyRestored ? `"${item.name}" was already restored.` : `Restored "${item.name}" with ${r.restoredFields} fields and ${r.restoredAttachments} files, turned off.` });
6822
6910
  onChanged();
6823
6911
  return r;
6824
6912
  } catch (e) {
@@ -6828,6 +6916,19 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6828
6916
  setBusyId(null);
6829
6917
  }
6830
6918
  };
6919
+ const turnOn = async (item) => {
6920
+ try {
6921
+ const done = await turnBackOn(port, item, item.fields, { browserFill: true, run: confirmed });
6922
+ const said = turnedOnSentence(done);
6923
+ setMessage({ tone: "ok", text: `"${item.name}": ${said}` });
6924
+ onChanged();
6925
+ return said;
6926
+ } catch (e) {
6927
+ onChanged();
6928
+ if (wasCancelled(e)) return null;
6929
+ throw e;
6930
+ }
6931
+ };
6831
6932
  return /* @__PURE__ */ jsxs28("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3", children: [
6832
6933
  header,
6833
6934
  /* @__PURE__ */ jsx30(VaultSection, { title: "Trash", children: shown.length === 0 ? /* @__PURE__ */ jsx30(Empty, { children: deleted.length === 0 ? "The trash is empty." : "Nothing matches." }) : shown.map((item) => (
@@ -6841,7 +6942,7 @@ function TrashView({ port, deleted, query, header, onChanged }) {
6841
6942
  /* @__PURE__ */ jsx30(Button25, { variant: "outline", icon: /* @__PURE__ */ jsx30(RestoreGlyph, {}), disabled: busyId !== null || !item.deletion, onClick: () => setRestoring(item), children: "Restore" })
6842
6943
  ] }, item.id)
6843
6944
  )) }),
6844
- restoring ? /* @__PURE__ */ jsx30(RestorePanel, { lists: port.lists, item: restoring, restore: (deletionId) => restore(restoring, deletionId), onClose: () => setRestoring(null) }) : null,
6945
+ restoring ? /* @__PURE__ */ jsx30(RestorePanel, { lists: port.lists, item: restoring, restore: (deletionId) => restore(restoring, deletionId), turnOn, onClose: () => setRestoring(null) }) : null,
6845
6946
  message2 ? /* @__PURE__ */ jsx30("p", { role: "status", className: message2.tone === "error" ? "px-1 text-sm text-destructive" : "px-1 text-sm text-muted-foreground", children: message2.text }) : null
6846
6947
  ] });
6847
6948
  }
@@ -9106,7 +9207,7 @@ function createFieldsPort(ctx) {
9106
9207
  const body = { initiation: "user" };
9107
9208
  if (patch.uriMatchMode !== void 0) body.uri_match_mode = patch.uriMatchMode;
9108
9209
  if (patch.browserFillEnabled !== void 0) body.browser_fill_enabled = patch.browserFillEnabled;
9109
- if (patch.loginUrls !== void 0) body.login_urls = patch.loginUrls;
9210
+ if (patch.loginUrls !== void 0) body.login_urls = normalizeLoginUrls(patch.loginUrls);
9110
9211
  if (patch.active !== void 0) body.status = patch.active ? "active" : "disabled";
9111
9212
  await account3.updateItem(api, { path: { item_id: item.id }, body }, ctx.optsFor(item));
9112
9213
  return reread(item);
@@ -9118,6 +9219,33 @@ function createFieldsPort(ctx) {
9118
9219
  };
9119
9220
  }
9120
9221
 
9222
+ // src/server-reasons.ts
9223
+ var GENERIC = [/please check your input/i, /^\s*request failed\s*\(\d+\)\s*$/i, /^\s*bad request\.?\s*$/i, /^\s*HTTP \d+\s*$/i];
9224
+ var isGeneric = (s) => GENERIC.some((re) => re.test(s));
9225
+ function cleanReason(detail) {
9226
+ const s = detail.replace(/^\s*validation error:\s*/i, "").replace(/^\s*value error,\s*/i, "").trim();
9227
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
9228
+ }
9229
+ function serverReason(e) {
9230
+ if (!e || typeof e !== "object") return null;
9231
+ const { status, detail, message: message2 } = e;
9232
+ if (typeof status !== "number" || status < 400 || status >= 500) return null;
9233
+ if (typeof detail !== "string" || !detail.trim() || isGeneric(detail)) return null;
9234
+ const shown = typeof message2 === "string" ? message2 : "";
9235
+ if (shown && !isGeneric(shown)) return null;
9236
+ return cleanReason(detail);
9237
+ }
9238
+ function withReason(e) {
9239
+ const reason = serverReason(e);
9240
+ if (reason) Object.assign(e, { message: reason, userMessage: reason });
9241
+ return e;
9242
+ }
9243
+ function withServerReasons(api) {
9244
+ const wrapped = Object.create(api);
9245
+ wrapped.call = (...args) => api.call(...args).catch((e) => Promise.reject(withReason(e)));
9246
+ return wrapped;
9247
+ }
9248
+
9121
9249
  // src/io/api.ts
9122
9250
  import * as account4 from "@ai-matrx/api/account";
9123
9251
  var orgOpts = (organizationId) => organizationId ? { organizationId } : void 0;
@@ -9390,7 +9518,7 @@ function createSharingPort(ctx, options = {}) {
9390
9518
  definition_key: definition.key,
9391
9519
  provider_key: definition.providerKey,
9392
9520
  tags: draft.tags,
9393
- login_urls: draft.loginUrls,
9521
+ login_urls: normalizeLoginUrls(draft.loginUrls),
9394
9522
  notes: draft.notes || null,
9395
9523
  generate_field_key: generateFieldKey,
9396
9524
  initiation: "user",
@@ -9587,7 +9715,8 @@ async function retrying(run, tries = 3) {
9587
9715
  }
9588
9716
  }
9589
9717
  }
9590
- function createApiVaultPort(api, options = {}) {
9718
+ function createApiVaultPort(hostApi, options = {}) {
9719
+ const api = withServerReasons(hostApi);
9591
9720
  let defs = /* @__PURE__ */ new Map();
9592
9721
  let auth = /* @__PURE__ */ new Map();
9593
9722
  let orgs = [];
@@ -9742,6 +9871,7 @@ function createApiVaultPort(api, options = {}) {
9742
9871
  const declared = definition.fields;
9743
9872
  const encrypted = declared.filter((f) => f.storage === "encrypted" && (draft.values[f.key] ?? "") !== "");
9744
9873
  const meta = declared.filter((f) => f.storage === "metadata" && (draft.values[f.key] ?? "") !== "");
9874
+ const loginUrls = normalizeLoginUrls(draft.loginUrls);
9745
9875
  const w = await account9.createItem(api, {
9746
9876
  body: {
9747
9877
  display_name: draft.name.trim(),
@@ -9749,10 +9879,10 @@ function createApiVaultPort(api, options = {}) {
9749
9879
  definition_key: definition.key,
9750
9880
  provider_key: definition.providerKey,
9751
9881
  tags: draft.tags,
9752
- login_urls: draft.loginUrls,
9882
+ login_urls: loginUrls,
9753
9883
  // A login a person saves with its website is offered on that website (the server's own
9754
9884
  // default is off, which made "Save a login" on a page answer "No saved login" there).
9755
- ...draft.browser ? { uri_match_mode: draft.browser.uriMatchMode, browser_fill_enabled: draft.browser.browserFillEnabled } : draft.loginUrls.length > 0 ? { browser_fill_enabled: true } : {},
9885
+ ...draft.browser ? { uri_match_mode: draft.browser.uriMatchMode, browser_fill_enabled: draft.browser.browserFillEnabled } : loginUrls.length > 0 ? { browser_fill_enabled: true } : {},
9756
9886
  notes: draft.notes || null,
9757
9887
  principal: organizationId ? { type: "organization", organization_id: organizationId } : { type: "user" },
9758
9888
  initiation: "user",
@@ -9779,7 +9909,7 @@ function createApiVaultPort(api, options = {}) {
9779
9909
  else body.description = draft.description.trim();
9780
9910
  }
9781
9911
  if (JSON.stringify(draft.tags) !== JSON.stringify(item.tags)) body.tags = draft.tags;
9782
- if (JSON.stringify(draft.loginUrls) !== JSON.stringify(item.loginUrls)) body.login_urls = draft.loginUrls;
9912
+ if (JSON.stringify(draft.loginUrls) !== JSON.stringify(item.loginUrls)) body.login_urls = normalizeLoginUrls(draft.loginUrls);
9783
9913
  if (draft.notes !== item.notes) {
9784
9914
  if (draft.notes === "") body.clear_notes = true;
9785
9915
  else body.notes = draft.notes;
@@ -9862,12 +9992,42 @@ function vaultInventoryScope(inventory, clicked = null) {
9862
9992
  function itemIdAt(target) {
9863
9993
  return target?.closest("[data-item]")?.getAttribute("data-item") ?? null;
9864
9994
  }
9995
+
9996
+ // src/io/empty-read.ts
9997
+ var HOLDINGS_DOOR_MIGRATION = "migrations/campaign/errorshonest_s8_a_credential_says_how_much_it_holds.sql";
9998
+ function holdingsDoorIsAbsent(code) {
9999
+ return code === "PGRST202" || code === "42883";
10000
+ }
10001
+ function staleAlarm(total) {
10002
+ 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.`;
10003
+ }
10004
+ function emptyReadAlarm(total, emptyIds, holdings) {
10005
+ if (emptyIds.length === 0) return null;
10006
+ if (holdings.state === "answered") {
10007
+ const asked = new Set(emptyIds);
10008
+ const unread = holdings.rows.filter(
10009
+ (r) => asked.has(r.credential_item_id) && (r.field_count > 0 || r.file_count > 0)
10010
+ );
10011
+ if (unread.length === 0) return null;
10012
+ const n = unread.length;
10013
+ 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.`;
10014
+ }
10015
+ if (emptyIds.length < total) return null;
10016
+ if (holdings.state === "absent") {
10017
+ console.warn(
10018
+ `[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.`
10019
+ );
10020
+ return staleAlarm(total);
10021
+ }
10022
+ return `${staleAlarm(total)} (The check that tells an empty credential from an unreadable one did not answer either: ${holdings.why})`;
10023
+ }
9865
10024
  export {
9866
10025
  CATEGORIES,
9867
10026
  CONFIRM_RETURN_KEY,
9868
10027
  DEFAULT_LOCATION,
9869
10028
  GenerateButton,
9870
10029
  HIDDEN_ITEM_TAG,
10030
+ HOLDINGS_DOOR_MIGRATION,
9871
10031
  IdentityGate,
9872
10032
  LOCKED_MESSAGE,
9873
10033
  VaultApp,
@@ -9877,7 +10037,9 @@ export {
9877
10037
  createApiVaultPort,
9878
10038
  createSupabaseIdentity,
9879
10039
  createVaultKnobs,
10040
+ emptyReadAlarm,
9880
10041
  guardPort,
10042
+ holdingsDoorIsAbsent,
9881
10043
  inferFromUrl,
9882
10044
  isHiddenItem,
9883
10045
  itemIdAt,