@ai-matrx/vault 0.1.19 → 0.1.21

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,10 +1,39 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.19
3
+ ## 0.1.21
4
4
 
5
5
  UNVERIFIED: this release has no Unreleased entry and the source comparison could not be made (the caller supplied no source comparison). Whether source changed is unknown; consumer action is unknown.
6
6
  Check the package's gitHead on npm against this version's gitHead: `git diff <previous gitHead> <this gitHead> -- apps/shared/vault/src`.
7
7
 
8
+ ## 0.1.20
9
+
10
+ - **A multi-line secret edits as several lines.** A field whose type says multi-line, any field under a notes key (an old **Secure note**, `secure_notes`) and any value that already holds a line break now edit in a multi-line box — a one-line box dropped the line breaks on save. Shown values keep their lines too. Guard: `src/vault-gaps-2026-10-19.test.tsx`. Consumer action: none.
11
+ - **Secure note is back** in the item form's Add menu: an encrypted multi-line field stored as `secure_notes`, hidden until shown (`secureNoteDraft`, `NewSecretField.multiline`/`key`/`description`). Consumer action: none.
12
+ - **A new item's declared secrets carry env key, who can see it and sandbox use to the create call** (`ItemDraft.fieldSettings`, `declaredFieldIn`, `FieldSettingsRows`): behind each secret's sliders, open from the start on an Environment value. Consumer action: none.
13
+ - **Trash rows say each item's type** ("Website login"), read from the item through the state door's `schema("users")` (`VaultUserStateDoor.schema`, `withTypes`), instead of "Credential". Consumer action: none — every host already passes its supabase client as `userState`.
14
+ - **A code added in the Authenticator shows in its list at once**, before the vault's re-read answers (or when an older re-read answers last) (`AddOtpPanel onAdded`, `withAdded`). Consumer action: none.
15
+ - **The list stays current.** A **Refresh** button beside the sort re-reads the list; the list also re-reads itself when the window or panel comes back into focus, and every `host.refreshSeconds` (else the organization's `vault.list.refresh_seconds` knob) while shown — never while hidden, never over an edit or a new item, never two reads at once. A refresh that fails keeps the loaded list and says "Couldn’t refresh. Showing what was loaded." with Try again; the partial-read line's Try again no longer drops to the "couldn't open" screen. Guard: `src/parity-gaps.test.tsx`. **Consumer action:** desktop: pass `refreshSeconds` from its refresh knob. Web app and extension: none (focus refresh is on by itself).
16
+ - **The row menu copies any field** — API key, token, account number, any plain custom field — besides username, password and code (never a sealed field, the authenticator's key or recovery codes). **Right-click on a row opens that menu** in every host, with a host's extra entries (`host.rowMenuItems`). **Consumer action:** web app: pass `rowContextMenu: "host"` so its own v3 right-click menu (agents, Copy, Export) keeps answering on rows. Extension and desktop: none.
17
+ - **After an import**, one **Set up authenticator for <name>** link per imported login that came with a one-time password; it opens the setup for that login over the result (`ImportRunResult.needOtp`). Consumer action: none.
18
+ - **The authenticator setup screen starts the camera by itself** where this browser can scan (`OtpSetupRow autoStartCamera`, used by `AddOtpPanel`; a setup row inside an edit never does). Consumer action: none.
19
+ - **An opener can name the new item**: `VaultLocation.name` (`?name=`) prefills the new item's name over the one guessed from the address. Consumer action: none.
20
+
21
+ ## 0.1.19
22
+
23
+ SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
24
+ so they may include breaking API changes. Consumer action is UNKNOWN: read the diff before upgrading.
25
+ Diff: `git diff ff560b1be4 05488c8d62 -- apps/shared/vault/src`
26
+
27
+ Changed shipped source files (8):
28
+ - `apps/shared/vault/src/ItemList.tsx`
29
+ - `apps/shared/vault/src/VaultApp.tsx`
30
+ - `apps/shared/vault/src/authenticator/AddOtpPanel.tsx`
31
+ - `apps/shared/vault/src/authenticator/Setup.tsx`
32
+ - `apps/shared/vault/src/io/ImportPanel.tsx`
33
+ - `apps/shared/vault/src/io/import-run.ts`
34
+ - `apps/shared/vault/src/model.ts`
35
+ - `apps/shared/vault/src/port.ts`
36
+
8
37
  ## 0.1.18
9
38
 
10
39
  - The Authenticator list keeps the code and its countdown ring apart at every width (the code column never squeezes), the list header lines up with the list on wide screens, and the Add authenticator panel's camera button is icon-only so the setup-key box has room on a phone. Consumer action: none.
package/dist/index.cjs CHANGED
@@ -622,6 +622,8 @@ function ConfirmIdentityDialog({ open, identity, onDone, onCancel }) {
622
622
  var import_react5 = require("react");
623
623
  var import_controls2 = require("@ai-matrx/design-system/controls");
624
624
  var import_design_system5 = require("@ai-matrx/design-system");
625
+ var import_outbound = require("@ai-matrx/kit/outbound");
626
+ var import_outbound_react = require("@ai-matrx/kit/outbound-react");
625
627
  var import_icons2 = require("@ai-matrx/design-system/icons");
626
628
 
627
629
  // src/hooks.ts
@@ -740,7 +742,7 @@ var DAY_FORMAT = { year: "numeric", month: "short", day: "numeric" };
740
742
  var import_jsx_runtime5 = require("react/jsx-runtime");
741
743
  var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
742
744
  function SecretText({ value, multiline }) {
743
- if (multiline || value.length > 200) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "whitespace-pre-wrap break-all font-mono text-sm", children: value });
745
+ if (multiline || value.length > 200 || value.includes("\n")) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "whitespace-pre-wrap break-all font-mono text-sm", children: value });
744
746
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "break-all font-mono text-sm tracking-wide", children: [...value].map((ch, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: /[0-9]/.test(ch) ? "text-primary" : /[^A-Za-z0-9]/.test(ch) ? "text-warning" : void 0, children: ch }, i)) });
745
747
  }
746
748
  var quietError = (e, fallback) => wasCancelled(e) ? null : e instanceof Error ? e.message : fallback;
@@ -833,13 +835,13 @@ function PlainRow({ label, value, hint: hint2, openUrl, extra, multiline = false
833
835
  top: multiline,
834
836
  actions: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
835
837
  extra,
836
- isUrl && openUrl ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ExternalGlyph, {}), "aria-label": `Open ${value}`, onClick: () => openUrl(value) }) : null,
838
+ isUrl && openUrl ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ExternalGlyph, {}), "aria-label": `Open ${value}`, onClick: () => openUrl((0, import_outbound.withReferral)(value)) }) : null,
837
839
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_icons2.CopyIcon, {}), "aria-label": `Copy ${typeof label === "string" ? label : "value"}`, onClick: () => void copyValue2(value, typeof label === "string" && label ? label : "Value", { secret: false }) })
838
840
  ] }),
839
841
  children: isUrl && openUrl ? (
840
842
  // A real link (right-click, copy link address, open in a new tab); a click goes through the host's opener.
841
843
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
842
- "a",
844
+ import_outbound_react.OutboundLink,
843
845
  {
844
846
  href: value,
845
847
  target: "_blank",
@@ -847,7 +849,7 @@ function PlainRow({ label, value, hint: hint2, openUrl, extra, multiline = false
847
849
  className: "block max-w-full truncate text-primary hover:underline",
848
850
  onClick: (e) => {
849
851
  e.preventDefault();
850
- openUrl(value);
852
+ openUrl((0, import_outbound.withReferral)(value));
851
853
  },
852
854
  children: value.replace(/^https?:\/\//i, "").replace(/\/$/, "")
853
855
  }
@@ -876,18 +878,19 @@ function EditRow({ label, hint: hint2, value, onChange, placeholder, secret = fa
876
878
  }
877
879
  );
878
880
  }
879
- function CustomEditRow({ label, value, onLabel, onValue, secret, onRemove, extra, onPasteValue }) {
881
+ function CustomEditRow({ label, value, onLabel, onValue, secret, onRemove, extra, onPasteValue, multiline = false }) {
880
882
  const [shown, setShown] = (0, import_react5.useState)(false);
881
883
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
882
884
  VaultRow,
883
885
  {
884
886
  label: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Field, { className: "w-full", value: label, onChange: (e) => onLabel(e.target.value), placeholder: "Name", "aria-label": "Field name" }),
887
+ top: multiline,
885
888
  actions: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
886
- secret && value ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Button, { variant: "quiet", icon: shown ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(EyeOffGlyph, {}) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(EyeGlyph, {}), "aria-label": shown ? "Hide" : "Show", onClick: () => setShown((x) => !x) }) : null,
889
+ secret && value && !multiline ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Button, { variant: "quiet", icon: shown ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(EyeOffGlyph, {}) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(EyeGlyph, {}), "aria-label": shown ? "Hide" : "Show", onClick: () => setShown((x) => !x) }) : null,
887
890
  extra,
888
891
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Button, { variant: "quiet", removes: true, icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(XGlyph, {}), "aria-label": "Remove field", onClick: onRemove })
889
892
  ] }),
890
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
893
+ children: multiline ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Textarea, { className: "w-full", value, onChange: (e) => onValue(e.target.value), rows: 4, placeholder: secret ? "Encrypted, hidden until shown" : "Value", autoComplete: "off", spellCheck: !secret, "aria-label": label ? `${label} value` : "Field value" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
891
894
  import_controls2.Field,
892
895
  {
893
896
  className: "w-full",
@@ -1006,6 +1009,7 @@ function sortItems(items, sort, recents) {
1006
1009
  if (sort === "updated") return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1007
1010
  return out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
1008
1011
  }
1012
+ var SECURE_NOTE_KEY = "secure_notes";
1009
1013
  function draftOf(item) {
1010
1014
  return {
1011
1015
  name: item?.name ?? "",
@@ -1056,7 +1060,9 @@ function rebaseDraft(oldBase, newBase, draft) {
1056
1060
  secrets: draft.secrets,
1057
1061
  custom,
1058
1062
  removed: draft.removed,
1059
- authenticator: draft.authenticator
1063
+ authenticator: draft.authenticator,
1064
+ ...draft.browser ? { browser: draft.browser } : {},
1065
+ ...draft.fieldSettings ? { fieldSettings: draft.fieldSettings } : {}
1060
1066
  };
1061
1067
  }
1062
1068
  function panesShown(mode, specialView, showingDetail) {
@@ -1084,7 +1090,8 @@ var FAMILY_LABELS = {
1084
1090
  signing_files: "Signing & files"
1085
1091
  };
1086
1092
  var COMMON_DEFINITION_KEYS = ["website_login", "username_password", "api_key", "env_value", "database_login", "custom"];
1087
- var MULTILINE = /(json|pem|private_key|certificate|cert|bundle|document|recovery_codes)/;
1093
+ var MULTILINE = /(json|pem|private_key|certificate|cert|bundle|document|recovery_codes|(^|_)notes?($|_))/;
1094
+ var isMultilineKey = (key) => MULTILINE.test(key);
1088
1095
  var SECRET = /(password|secret|token|api_key|private|passphrase|credential|seed|key$|_key)/;
1089
1096
  var URLISH = /(url|uri|endpoint|webhook|hook)/;
1090
1097
  function humanize(key) {
@@ -2298,7 +2305,7 @@ function normalizeWebsite(raw) {
2298
2305
  return null;
2299
2306
  }
2300
2307
  }
2301
- function AddOtpPanel({ port, items, definitions, itemId = null, onClose, onChanged, onDone }) {
2308
+ function AddOtpPanel({ port, items, definitions, itemId = null, onClose, onChanged, onAdded, onDone }) {
2302
2309
  const confirmed = useConfirmed();
2303
2310
  const candidates = items.filter((i) => !i.authenticator && !i.deletion && (i.capabilities.canEdit || i.capabilities.canManage)).sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" }));
2304
2311
  const [input, setInput] = (0, import_react12.useState)("");
@@ -2351,6 +2358,7 @@ function AddOtpPanel({ port, items, definitions, itemId = null, onClose, onChang
2351
2358
  setInput("");
2352
2359
  setImage(null);
2353
2360
  setSaved(next);
2361
+ onAdded?.(next);
2354
2362
  onChanged();
2355
2363
  } catch (e) {
2356
2364
  setError(wasCancelled(e) ? "Not confirmed, so nothing was added." : otpActionError(e, "add it"));
@@ -2459,7 +2467,23 @@ function otpGroups(items, entries, query, hidden = /* @__PURE__ */ new Set()) {
2459
2467
  const rank = (k) => k === "me" ? 0 : k === "shared" ? 2 : 1;
2460
2468
  return [...groups.entries()].sort(([a, x], [b, y]) => rank(a) - rank(b) || x.label.localeCompare(y.label)).map(([key, g]) => ({ key, ...g }));
2461
2469
  }
2462
- function AuthenticatorView({ port, host, items, definitions, query, onOpen, onChanged, header }) {
2470
+ function withAdded(items, added) {
2471
+ if (added.size === 0) return items;
2472
+ const seen = /* @__PURE__ */ new Set();
2473
+ const out = items.map((i) => {
2474
+ seen.add(i.id);
2475
+ const mine = added.get(i.id);
2476
+ return mine && !i.authenticator ? { ...i, authenticator: mine.authenticator, name: mine.name } : i;
2477
+ });
2478
+ for (const [id, a] of added) if (!seen.has(id)) out.push(a);
2479
+ return out;
2480
+ }
2481
+ function AuthenticatorView({ port, host, items: listed, definitions, query, onOpen, onChanged, header }) {
2482
+ const [added, setAdded] = (0, import_react13.useState)(/* @__PURE__ */ new Map());
2483
+ const items = withAdded(listed, added);
2484
+ (0, import_react13.useEffect)(() => {
2485
+ if (added.size > 0 && [...added.keys()].every((id) => listed.some((i) => i.id === id && i.authenticator))) setAdded(/* @__PURE__ */ new Map());
2486
+ }, [listed, added]);
2463
2487
  const [entries, setEntries] = (0, import_react13.useState)(/* @__PURE__ */ new Map());
2464
2488
  const [hidden, setHidden] = (0, import_react13.useState)(/* @__PURE__ */ new Set());
2465
2489
  const [adding, setAdding] = (0, import_react13.useState)(false);
@@ -2475,7 +2499,7 @@ function AuthenticatorView({ port, host, items, definitions, query, onOpen, onCh
2475
2499
  return () => {
2476
2500
  live = false;
2477
2501
  };
2478
- }, [port, items]);
2502
+ }, [port, listed, added]);
2479
2503
  const groups = otpGroups(items, entries, query, hidden);
2480
2504
  const total = items.filter((i) => i.authenticator && !i.deletion && !hidden.has(i.id)).length;
2481
2505
  const familyOf2 = (key) => definitions.find((d) => d.key === key)?.family ?? "generic";
@@ -2521,6 +2545,7 @@ function AuthenticatorView({ port, host, items, definitions, query, onOpen, onCh
2521
2545
  definitions,
2522
2546
  onClose: () => setAdding(false),
2523
2547
  onChanged,
2548
+ onAdded: (item) => setAdded((cur) => new Map(cur).set(item.id, item)),
2524
2549
  onDone: (id) => {
2525
2550
  setAdding(false);
2526
2551
  onOpen(id);
@@ -5245,14 +5270,12 @@ var import_confirm_opener7 = require("@ai-matrx/kit/confirm-opener");
5245
5270
  var import_jsx_runtime22 = require("react/jsx-runtime");
5246
5271
  var HANDLINGS = ["visible", "revealable", "sealed"];
5247
5272
  var hint = (text, tone = "muted") => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: tone === "destructive" ? "text-destructive" : void 0, title: text, children: text });
5248
- var hasSecretSettings = (s) => Boolean(s.envKey?.trim() || s.injectIntoSandbox || s.handling && s.handling !== "revealable");
5249
- function SecretBuilderRow({ field, onChange, onRemove }) {
5250
- const [open, setOpen] = (0, import_react23.useState)(hasSecretSettings(field));
5251
- const env = field.envKey ?? "";
5273
+ var saysMore = (s, defaultHandling) => Boolean(s.envKey?.trim() || s.injectIntoSandbox || s.handling && s.handling !== defaultHandling);
5274
+ function FieldSettingsRows({ name, value, defaultHandling = "revealable", onChange }) {
5275
+ const env = value.envKey ?? "";
5252
5276
  const envProblem = envKeyProblem(env);
5253
5277
  const envOk = env.trim() !== "" && !envProblem;
5254
- const handling = field.handling ?? "revealable";
5255
- const name = field.label.trim() || "Secret";
5278
+ const handling = value.handling ?? defaultHandling;
5256
5279
  const chooseHandling = async (next) => {
5257
5280
  if (next === handling) return;
5258
5281
  if (next === "sealed") {
@@ -5266,6 +5289,38 @@ function SecretBuilderRow({ field, onChange, onRemove }) {
5266
5289
  }
5267
5290
  onChange({ handling: next });
5268
5291
  };
5292
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
5293
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(VaultRow, { label: "Env key", hint: envProblem ? hint(envProblem, "destructive") : hint("The name sandboxes and workflows use"), children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5294
+ import_controls18.Field,
5295
+ {
5296
+ className: "w-full font-mono",
5297
+ "aria-label": `${name} env key`,
5298
+ "aria-invalid": Boolean(envProblem),
5299
+ placeholder: "e.g. GITHUB_TOKEN",
5300
+ autoComplete: "off",
5301
+ spellCheck: false,
5302
+ value: env,
5303
+ onChange: (e) => onChange({ envKey: e.target.value, ...e.target.value.trim() ? {} : { injectIntoSandbox: false } })
5304
+ }
5305
+ ) }),
5306
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(VaultRow, { label: "Who can see it", hint: hint(HANDLING_WORDS[handling].hint), children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5307
+ import_controls18.SegmentedControl,
5308
+ {
5309
+ "aria-label": `Who can see ${name}`,
5310
+ value: handling,
5311
+ onValueChange: (v) => void chooseHandling(v),
5312
+ data: HANDLINGS.map((h) => ({ value: h, label: HANDLING_WORDS[h].label, title: HANDLING_WORDS[h].hint }))
5313
+ }
5314
+ ) }),
5315
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(VaultRow, { label: "Use in sandbox", hint: envOk ? hint("Sandboxes get it as an env variable") : hint("Needs an env key"), children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_controls18.Switch, { "aria-label": `Use ${name} in sandbox`, checked: envOk && value.injectIntoSandbox === true, disabled: !envOk, onCheckedChange: (v) => onChange({ injectIntoSandbox: v }) }) })
5316
+ ] });
5317
+ }
5318
+ var hasSecretSettings = (s) => saysMore(s, "revealable");
5319
+ var hasDeclaredSettings = (s, defaultHandling) => Boolean(s && saysMore(s, defaultHandling));
5320
+ function SecretBuilderRow({ field, onChange, onRemove }) {
5321
+ const [open, setOpen] = (0, import_react23.useState)(hasSecretSettings(field));
5322
+ const handling = field.handling ?? "revealable";
5323
+ const name = field.label.trim() || "Secret";
5269
5324
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
5270
5325
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5271
5326
  CustomEditRow,
@@ -5273,47 +5328,26 @@ function SecretBuilderRow({ field, onChange, onRemove }) {
5273
5328
  label: field.label,
5274
5329
  value: field.value,
5275
5330
  secret: true,
5331
+ multiline: field.multiline === true,
5276
5332
  onLabel: (v) => onChange({ label: v }),
5277
5333
  onValue: (v) => onChange({ value: v }),
5278
5334
  onRemove,
5279
- onPasteValue: (text) => {
5280
- const a = parseEnvAssignment(text);
5281
- if (!a) return false;
5282
- onChange({ envKey: a.key, value: a.value, ...field.label.trim() ? {} : { label: labelFromEnvKey(a.key) } });
5283
- setOpen(true);
5284
- return true;
5335
+ ...field.multiline ? {} : {
5336
+ onPasteValue: (text) => {
5337
+ const a = parseEnvAssignment(text);
5338
+ if (!a) return false;
5339
+ onChange({ envKey: a.key, value: a.value, ...field.label.trim() ? {} : { label: labelFromEnvKey(a.key) } });
5340
+ setOpen(true);
5341
+ return true;
5342
+ }
5285
5343
  },
5286
5344
  extra: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
5287
- handling !== "sealed" ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(GenerateButton, { onUse: (v) => onChange({ value: v }) }) : null,
5345
+ handling !== "sealed" && !field.multiline ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(GenerateButton, { onUse: (v) => onChange({ value: v }) }) : null,
5288
5346
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_controls18.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(SlidersGlyph, {}), "aria-label": `${name} settings`, pressed: open, onClick: () => setOpen((o) => !o) })
5289
5347
  ] })
5290
5348
  }
5291
5349
  ),
5292
- open ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
5293
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(VaultRow, { label: "Env key", hint: envProblem ? hint(envProblem, "destructive") : hint("The name sandboxes and workflows use"), children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5294
- import_controls18.Field,
5295
- {
5296
- className: "w-full font-mono",
5297
- "aria-label": `${name} env key`,
5298
- "aria-invalid": Boolean(envProblem),
5299
- placeholder: "e.g. GITHUB_TOKEN",
5300
- autoComplete: "off",
5301
- spellCheck: false,
5302
- value: env,
5303
- onChange: (e) => onChange({ envKey: e.target.value, ...e.target.value.trim() ? {} : { injectIntoSandbox: false } })
5304
- }
5305
- ) }),
5306
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(VaultRow, { label: "Who can see it", hint: hint(HANDLING_WORDS[handling].hint), children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5307
- import_controls18.SegmentedControl,
5308
- {
5309
- "aria-label": `Who can see ${name}`,
5310
- value: handling,
5311
- onValueChange: (v) => void chooseHandling(v),
5312
- data: HANDLINGS.map((h) => ({ value: h, label: HANDLING_WORDS[h].label, title: HANDLING_WORDS[h].hint }))
5313
- }
5314
- ) }),
5315
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(VaultRow, { label: "Use in sandbox", hint: envOk ? hint("Sandboxes get it as an env variable") : hint("Needs an env key"), children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_controls18.Switch, { "aria-label": `Use ${name} in sandbox`, checked: envOk && field.injectIntoSandbox === true, disabled: !envOk, onCheckedChange: (v) => onChange({ injectIntoSandbox: v }) }) })
5316
- ] }) : null
5350
+ open ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(FieldSettingsRows, { name, value: field, onChange }) : null
5317
5351
  ] });
5318
5352
  }
5319
5353
  function withMfaMethod(custom, v) {
@@ -5943,6 +5977,8 @@ function SharingSummary({ item, port, className }) {
5943
5977
  // src/ItemPane.tsx
5944
5978
  var import_format8 = require("@ai-matrx/kit/format");
5945
5979
  var import_jsx_runtime27 = require("react/jsx-runtime");
5980
+ var ENV_VALUE_KEY = "env_value";
5981
+ var secureNoteDraft = () => ({ key: SECURE_NOTE_KEY, label: "Secure note", value: "", multiline: true, description: "Encrypted notes" });
5946
5982
  var USERNAME_KEYS = /^(username|user_name|email|login|username_or_email|account_email)$/;
5947
5983
  function ItemPane(props) {
5948
5984
  const { port, item, definition, mode, host, organizations, defaultOwner, seed, onBack, onMode, onSaved, onDeleted, onDirty, onCancelNew, onPanel, onChanged, favorite, notice, refreshKey } = props;
@@ -6133,6 +6169,16 @@ function ItemPane(props) {
6133
6169
  const fieldSettings = (f) => canEdit ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(SlidersGlyph, {}), "aria-label": `${f.label} settings`, title: `${f.label} settings`, onClick: () => onPanel(`field:${f.id}`) }) : null;
6134
6170
  const turnOnOne = canTurnOn ? (f) => () => turnOn([f]) : () => void 0;
6135
6171
  const declaredKeys = new Set(declared.map((f) => f.key));
6172
+ const hasSecureNote = declaredKeys.has(SECURE_NOTE_KEY) || Boolean(item?.fields.some((f) => f.key === SECURE_NOTE_KEY)) || draft.secrets.some((x) => x.key === SECURE_NOTE_KEY);
6173
+ const [settingsToggled, setSettingsToggled] = (0, import_react28.useState)(/* @__PURE__ */ new Set());
6174
+ const settingsOpen = (f) => settingsToggled.has(f.key) !== (definition?.key === ENV_VALUE_KEY || hasDeclaredSettings(draft.fieldSettings?.[f.key], f.handling));
6175
+ const toggleSettings = (key) => setSettingsToggled((cur) => {
6176
+ const next = new Set(cur);
6177
+ if (next.has(key)) next.delete(key);
6178
+ else next.add(key);
6179
+ return next;
6180
+ });
6181
+ const patchFieldSettings = (key, p) => setDraft((d) => ({ ...d, fieldSettings: { ...d.fieldSettings, [key]: { ...d.fieldSettings?.[key], ...p } } }));
6136
6182
  const extraFields = (item?.fields ?? []).filter((f) => !declaredKeys.has(f.key) && !draft.removed.includes(f.id) && !isAuthenticatorStorage(f.key));
6137
6183
  const metaValue = (key) => draft.values[key] ?? item?.custom.find((c) => c.key === key)?.value ?? "";
6138
6184
  const extraCustom = draft.custom.filter((c) => !declaredKeys.has(c.key) && c.key !== MFA_METHOD_KEY && !isAuthenticatorStorage(c.key));
@@ -6265,24 +6311,29 @@ function ItemPane(props) {
6265
6311
  const secretKind = f.storage === "encrypted" && f.kind === "secret";
6266
6312
  const value = f.storage === "metadata" ? metaValue(f.key) : draft.values[f.key] ?? "";
6267
6313
  const hidden = existing && existing.handling !== "visible" && !(f.key in loaded);
6268
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
6269
- EditRow,
6270
- {
6271
- label: f.label,
6272
- value,
6273
- placeholder: hidden ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : f.required ? "Required" : isRecoveryCodesField(f) ? "Backup codes, one per line" : "",
6274
- secret: secretKind,
6275
- multiline: f.kind === "multiline",
6276
- inputMode: f.kind === "url" ? "url" : USERNAME_KEYS.test(f.key) ? "email" : "text",
6277
- onChange: (v) => setValue(f.key, v),
6278
- actions: /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
6279
- hidden && existing?.handling === "revealable" && item?.capabilities.canReveal ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EyeGlyph, {}), "aria-label": `Show current ${f.label}`, onClick: () => void loadForEdit(f.key) }) : null,
6280
- secretKind ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(GenerateButton, { onUse: (v) => setValue(f.key, v) }) : null,
6281
- existing ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_icons11.SettingsIcon, {}), "aria-label": `${f.label} settings`, onClick: () => onPanel(`field:${existing.id}`) }) : null
6282
- ] })
6283
- },
6284
- f.key
6285
- );
6314
+ const newSecret = mode === "new" && f.storage === "encrypted" && owner !== "person";
6315
+ const open = newSecret && settingsOpen(f);
6316
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_react28.Fragment, { children: [
6317
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
6318
+ EditRow,
6319
+ {
6320
+ label: f.label,
6321
+ value,
6322
+ placeholder: hidden ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : f.required ? "Required" : isRecoveryCodesField(f) ? "Backup codes, one per line" : "",
6323
+ secret: secretKind,
6324
+ multiline: f.kind === "multiline" || value.includes("\n"),
6325
+ inputMode: f.kind === "url" ? "url" : USERNAME_KEYS.test(f.key) ? "email" : "text",
6326
+ onChange: (v) => setValue(f.key, v),
6327
+ actions: /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
6328
+ hidden && existing?.handling === "revealable" && item?.capabilities.canReveal ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EyeGlyph, {}), "aria-label": `Show current ${f.label}`, onClick: () => void loadForEdit(f.key) }) : null,
6329
+ secretKind ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(GenerateButton, { onUse: (v) => setValue(f.key, v) }) : null,
6330
+ existing ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_icons11.SettingsIcon, {}), "aria-label": `${f.label} settings`, onClick: () => onPanel(`field:${existing.id}`) }) : null,
6331
+ newSecret ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(SlidersGlyph, {}), "aria-label": `${f.label} settings`, pressed: open, onClick: () => toggleSettings(f.key) }) : null
6332
+ ] })
6333
+ }
6334
+ ),
6335
+ open ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(FieldSettingsRows, { name: f.label, value: draft.fieldSettings?.[f.key] ?? {}, defaultHandling: f.handling, onChange: (p) => patchFieldSettings(f.key, p) }) : null
6336
+ ] }, f.key);
6286
6337
  }),
6287
6338
  extraFields.map((f) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
6288
6339
  EditRow,
@@ -6291,7 +6342,7 @@ function ItemPane(props) {
6291
6342
  value: draft.values[f.key] ?? "",
6292
6343
  placeholder: f.handling === "visible" || f.key in loaded ? "" : "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
6293
6344
  secret: f.handling !== "visible" && !isRecoveryCodesField(f),
6294
- multiline: isRecoveryCodesField(f),
6345
+ multiline: f.kind === "multiline" || isRecoveryCodesField(f) || (draft.values[f.key] ?? "").includes("\n"),
6295
6346
  onChange: (v) => setValue(f.key, v),
6296
6347
  onRemove: () => patch({ removed: [...draft.removed, f.id] }),
6297
6348
  removeLabel: `Delete ${f.label}`,
@@ -6360,6 +6411,7 @@ function ItemPane(props) {
6360
6411
  /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_design_system17.DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_controls22.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(PlusGlyph, {}), children: "Add" }) }),
6361
6412
  /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_design_system17.DropdownMenuContent, { align: "end", children: [
6362
6413
  /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_design_system17.DropdownMenuItem, { onSelect: () => patch({ secrets: [...draft.secrets, { key: "", label: "", value: "" }] }), children: "Secret field" }),
6414
+ !hasSecureNote ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_design_system17.DropdownMenuItem, { onSelect: () => patch({ secrets: [...draft.secrets, secureNoteDraft()] }), children: "Secure note" }) : null,
6363
6415
  /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_design_system17.DropdownMenuItem, { onSelect: () => patch({ custom: [...draft.custom, { key: `custom_${draft.custom.length + 1}_${Date.now() % 1e5}`, label: "", value: "" }] }), children: "Text field" }),
6364
6416
  !tagsVisible ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_design_system17.DropdownMenuItem, { onSelect: () => setShowTags(true), children: "Tags" }) : null,
6365
6417
  !descriptionVisible ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_design_system17.DropdownMenuItem, { onSelect: () => setShowDescription(true), children: "Description" }) : null,
@@ -9128,6 +9180,28 @@ function trashItem(r, ctx) {
9128
9180
  createdAt: ""
9129
9181
  };
9130
9182
  }
9183
+ async function withTypes(items, door, ctx) {
9184
+ if (!door.schema || items.length === 0) return items;
9185
+ const keys = /* @__PURE__ */ new Map();
9186
+ const CHUNK = 100;
9187
+ try {
9188
+ for (let i = 0; i < items.length; i += CHUNK) {
9189
+ const ids = items.slice(i, i + CHUNK).map((x) => x.id);
9190
+ const { data, error } = await door.schema("users").from("credential_items").select("id,definition_key").in("id", ids);
9191
+ if (error) throw new Error(error.message || "credential_items read failed");
9192
+ for (const r of Array.isArray(data) ? data : []) {
9193
+ if (typeof r.id === "string" && typeof r.definition_key === "string" && r.definition_key) keys.set(r.id, r.definition_key);
9194
+ }
9195
+ }
9196
+ } catch (e) {
9197
+ console.warn("[vault] Trash item types could not be read; rows say Credential", e);
9198
+ return items;
9199
+ }
9200
+ return items.map((it) => {
9201
+ const key = keys.get(it.id);
9202
+ return key ? { ...it, definitionKey: key, definitionLabel: ctx.definition(key)?.label ?? humanize(key) } : it;
9203
+ });
9204
+ }
9131
9205
  var previewOf = async (door, id) => parseRecoveryPreview(await call(door, "vault_recovery_preview", { p_id: id }));
9132
9206
  function createListsPort(ctx, door) {
9133
9207
  return {
@@ -9160,7 +9234,8 @@ function createListsPort(ctx, door) {
9160
9234
  if (rows.length < PAGE2) break;
9161
9235
  }
9162
9236
  const items = all.map((r) => trashItem(r, ctx)).filter((i) => i !== null);
9163
- return [...new Map(items.map((i) => [i.id, i])).values()];
9237
+ const unique = [...new Map(items.map((i) => [i.id, i])).values()];
9238
+ return withTypes(unique, door, ctx);
9164
9239
  }
9165
9240
  },
9166
9241
  favorites: {
@@ -9183,16 +9258,25 @@ function createListsPort(ctx, door) {
9183
9258
  var account6 = __toESM(require("@ai-matrx/api/account"), 1);
9184
9259
 
9185
9260
  // src/secret-field-in.ts
9186
- function secretFieldIn(s) {
9261
+ function settingsIn(s) {
9187
9262
  const envKey = s.envKey?.trim() || null;
9188
9263
  return {
9189
- field_key: fieldKeyOf(s.label),
9190
- value: s.value,
9191
- handling: s.handling ?? "revealable",
9192
9264
  ...envKey ? { env_key: envKey } : {},
9193
9265
  ...envKey && s.injectIntoSandbox ? { inject_into_sandbox: true } : {}
9194
9266
  };
9195
9267
  }
9268
+ function secretFieldIn(s) {
9269
+ return {
9270
+ field_key: s.key.trim() || fieldKeyOf(s.label),
9271
+ value: s.value,
9272
+ handling: s.handling ?? "revealable",
9273
+ ...s.description ? { description: s.description } : {},
9274
+ ...settingsIn(s)
9275
+ };
9276
+ }
9277
+ function declaredFieldIn(f, value, settings) {
9278
+ return { field_key: f.key, value, handling: settings?.handling ?? f.handling, ...settingsIn(settings ?? {}) };
9279
+ }
9196
9280
 
9197
9281
  // src/sharing/api.ts
9198
9282
  function grantOf(w) {
@@ -9293,7 +9377,7 @@ function createSharingPort(ctx, options = {}) {
9293
9377
  initiation: "user",
9294
9378
  non_secret_fields: [...meta.map((f) => ({ key: f.key, label: f.label, value: draft.values[f.key] ?? "" })), ...draft.custom.filter((c) => c.label.trim() !== "").map((c) => ({ key: c.key || fieldKeyOf(c.label), label: c.label, value: c.value }))],
9295
9379
  fields: [
9296
- ...encrypted.map((f) => ({ field_key: f.key, value: draft.values[f.key] ?? "", handling: f.handling })),
9380
+ ...encrypted.map((f) => declaredFieldIn(f, draft.values[f.key] ?? "", draft.fieldSettings?.[f.key])),
9297
9381
  ...draft.secrets.filter((s) => s.label.trim() !== "" && s.value !== "").map(secretFieldIn)
9298
9382
  ]
9299
9383
  }
@@ -9422,7 +9506,7 @@ async function readNativeImportLimits(api, organizationId) {
9422
9506
  var recoveryOf = (w) => w.lifecycle?.vault_recovery;
9423
9507
  var isDeleted = (w) => recoveryOf(w)?.state === "deleted";
9424
9508
  var MATCH_MODES2 = ["host", "domain", "exact", "never"];
9425
- var FIELD_WORDS = { totp_seed: "One-time password key", authenticator_last_time_step: "Last code used" };
9509
+ var FIELD_WORDS = { secure_notes: "Secure note", totp_seed: "One-time password key", authenticator_last_time_step: "Last code used" };
9426
9510
  var handlingOf = (v) => v === "visible" || v === "sealed" ? v : "revealable";
9427
9511
  function fieldOf(w, def) {
9428
9512
  const declared = def?.fields.find((f) => f.key === w.field_key);
@@ -9431,7 +9515,8 @@ function fieldOf(w, def) {
9431
9515
  id: w.id,
9432
9516
  key: w.field_key,
9433
9517
  label: declared?.label ?? FIELD_WORDS[w.field_key] ?? humanize(w.field_key),
9434
- kind: declared?.kind ?? (handling === "visible" ? "text" : "secret"),
9518
+ // A field no type declares still edits as several lines when its key says so (an old "Secure note").
9519
+ kind: declared?.kind ?? (isMultilineKey(w.field_key) ? "multiline" : handling === "visible" ? "text" : "secret"),
9435
9520
  handling,
9436
9521
  editable: w.editable ?? true,
9437
9522
  envKey: w.env_key ?? null,
@@ -9586,7 +9671,7 @@ function createApiVaultPort(api, options = {}) {
9586
9671
  initiation: "user",
9587
9672
  non_secret_fields: [...meta.map((f) => ({ key: f.key, label: f.label, value: draft.values[f.key] ?? "" })), ...draft.custom.filter((c) => c.label.trim() !== "").map((c) => ({ key: c.key || fieldKeyOf(c.label), label: c.label, value: c.value }))],
9588
9673
  fields: [
9589
- ...encrypted.map((f) => ({ field_key: f.key, value: draft.values[f.key] ?? "", handling: f.handling })),
9674
+ ...encrypted.map((f) => declaredFieldIn(f, draft.values[f.key] ?? "", draft.fieldSettings?.[f.key])),
9590
9675
  ...draft.secrets.filter((s) => s.label.trim() !== "" && s.value !== "").map(secretFieldIn),
9591
9676
  // Backup codes on a type that does not declare them (a website login): their own encrypted field.
9592
9677
  ...!declared.some((f) => f.key === RECOVERY_CODES_KEY) && (draft.values[RECOVERY_CODES_KEY] ?? "").trim() !== "" ? [{ field_key: RECOVERY_CODES_KEY, value: parseRecoveryCodes(draft.values[RECOVERY_CODES_KEY] ?? "").join("\n"), handling: "revealable", description: "One-time account recovery codes" }] : []