@ai-matrx/vault 0.1.5 → 0.1.6

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
@@ -147,9 +147,6 @@ interface ItemDraft {
147
147
  authenticator: string;
148
148
  }
149
149
 
150
- type NoticeKind = "success" | "error" | "info";
151
- type Notify = (message: string, kind?: NoticeKind) => void;
152
-
153
150
  /** One recorded earlier value of a secret field. Value-free: the value comes only from `revealHistory`. */
154
151
  interface HistoryEntry {
155
152
  fieldId: string;
@@ -1096,7 +1093,8 @@ interface VaultPort {
1096
1093
  * What only some hosts can do. A missing capability hides its control; it never shows a dead one.
1097
1094
  * - Every host with a signed-in person supplies `identity` (confirm it's you before a secret shows).
1098
1095
  * - The browser extension supplies `page` (the tab's address, the logins that match it) and `fill`.
1099
- * - The desktop app supplies `nativeProvider` (the system credential provider's state).
1096
+ * - The desktop app supplies `nativeProvider` (the system credential provider's state), and its own
1097
+ * `lock`, `clipboard` custody and `revealSeconds`.
1100
1098
  */
1101
1099
  interface VaultHostCapabilities {
1102
1100
  identity?: VaultIdentity;
@@ -1114,12 +1112,71 @@ interface VaultHostCapabilities {
1114
1112
  /** Desktop: the native passkey importer (file chooser, parsing and keys stay native). */
1115
1113
  nativeImport?: NativeImportBridge;
1116
1114
  openUrl?: (url: string) => void;
1115
+ /** A lock the host holds (`lock.ts`): while locked the vault shows Unlock and refuses every port call. */
1116
+ lock?: VaultLock;
1117
+ /** The host's clipboard: every copy goes through it; a secret is the host's to clear (`lock.ts`). */
1118
+ clipboard?: VaultClipboard;
1119
+ /** How long a shown value stays shown, in seconds (an organization knob the host reads); default 20. */
1120
+ revealSeconds?: number;
1117
1121
  nativeProvider?: {
1118
1122
  status: "ready" | "needs_setup" | "unavailable";
1119
1123
  openSetup: () => void;
1120
1124
  };
1121
1125
  }
1122
1126
 
1127
+ /**
1128
+ * A HOST-HELD LOCK. Some hosts lock the vault themselves (the desktop app: Touch ID, starts locked,
1129
+ * locks after idle, on sleep and on screen lock). The host owns the lock and enforces it on its own
1130
+ * side; the vault shows a locked screen with Unlock, and while locked refuses every port call — a
1131
+ * value never reaches a screen while the host says locked. Without `lock` nothing changes.
1132
+ */
1133
+ interface VaultLock {
1134
+ /** Locked right now. */
1135
+ locked(): boolean;
1136
+ /** Told on every change; returns the unsubscribe. */
1137
+ subscribe(listener: () => void): () => void;
1138
+ /** Ask the host to unlock (Touch ID, a click…). Rejects with a sentence a person can act on. */
1139
+ unlock(): Promise<void>;
1140
+ /** Lock now. When given, the vault offers "Lock". */
1141
+ lock?: () => void | Promise<void>;
1142
+ /** The unlock button's words ("Unlock with Touch ID"); default "Unlock". */
1143
+ unlockLabel?: string;
1144
+ }
1145
+ /**
1146
+ * HOST CLIPBOARD CUSTODY. When given, every copy in the vault goes through the host. `secret` asks
1147
+ * the host to take custody: clear it after a while if it is still there, and on lock or quit.
1148
+ */
1149
+ interface VaultClipboard {
1150
+ copy(text: string, how: {
1151
+ secret: boolean;
1152
+ }): Promise<void>;
1153
+ }
1154
+ declare const LOCKED_MESSAGE = "The vault is locked. Unlock it first.";
1155
+ declare class VaultLockedError extends Error {
1156
+ readonly code = "vault_locked";
1157
+ constructor();
1158
+ }
1159
+ /**
1160
+ * The port with every method (areas included) refused while `locked()` says so. The check runs at
1161
+ * call time, so a screen still mounted when the lock lands cannot read a value.
1162
+ */
1163
+ declare function guardPort(port: VaultPort, locked: () => boolean): VaultPort;
1164
+
1165
+ type NoticeKind = "success" | "error" | "info";
1166
+ type Notify = (message: string, kind?: NoticeKind) => void;
1167
+ interface CopyOptions {
1168
+ /** A secret (a password, a code, a key): the host's clipboard takes custody. Default true. */
1169
+ secret?: boolean;
1170
+ }
1171
+ /**
1172
+ * Writes `text` and says so (or says why not). Through the host's clipboard when it has one (custody:
1173
+ * the host clears a secret later), else the browser's. Pure over its inputs, so it is tested alone.
1174
+ */
1175
+ declare function copyValue(text: string, label: string, options: CopyOptions, deps: {
1176
+ clipboard?: VaultClipboard | undefined;
1177
+ notify: Notify;
1178
+ }): Promise<boolean>;
1179
+
1123
1180
  type VaultLayout = "auto" | "full" | "split" | "stack";
1124
1181
  interface VaultAppProps {
1125
1182
  port: VaultPort;
@@ -1183,4 +1240,4 @@ declare function GenerateButton({ onUse, label, limits, disabled }: {
1183
1240
  disabled?: boolean;
1184
1241
  }): react.JSX.Element | null;
1185
1242
 
1186
- export { type ApiVaultPortOptions, CATEGORIES, type CategoryId, DEFAULT_LOCATION, type DefinitionField, type FieldHandling, type FieldKind, GenerateButton, type IdentityAccount, type IdentityMethod, type ItemDraft, type NoticeKind, type Notify, type SaveResult, type SupabaseAuthLike, type TotpReading, type UriMatchMode, type UrlGuess, VaultApp, type VaultAppProps, type VaultAttachment, type VaultAuthenticator, type VaultCustomField, type VaultDefinition, type VaultField, type VaultHostCapabilities, type VaultIdentity, type VaultItem, type VaultItemCapabilities, type VaultKnobs, type VaultLayout, type VaultLocation, type VaultOrganizationRef, type VaultOwner, type VaultPort, type VaultSelection, type VaultSnapshot, type VaultSort, type VaultUserStateDoor, categoryOf, createApiVaultPort, createSupabaseIdentity, createVaultKnobs, inferFromUrl, locationFromParams, locationToParams, matchesQuery, needsRecentSignIn, parseDefinitions, siteOf, sortItems };
1243
+ export { type ApiVaultPortOptions, CATEGORIES, type CategoryId, type CopyOptions, DEFAULT_LOCATION, type DefinitionField, type FieldHandling, type FieldKind, GenerateButton, type IdentityAccount, type IdentityMethod, type ItemDraft, LOCKED_MESSAGE, type NoticeKind, type Notify, type SaveResult, type SupabaseAuthLike, type TotpReading, type UriMatchMode, type UrlGuess, VaultApp, type VaultAppProps, type VaultAttachment, type VaultAuthenticator, type VaultClipboard, type VaultCustomField, type VaultDefinition, type VaultField, type VaultHostCapabilities, type VaultIdentity, 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, locationFromParams, locationToParams, matchesQuery, needsRecentSignIn, parseDefinitions, siteOf, sortItems };
package/dist/index.d.ts CHANGED
@@ -147,9 +147,6 @@ interface ItemDraft {
147
147
  authenticator: string;
148
148
  }
149
149
 
150
- type NoticeKind = "success" | "error" | "info";
151
- type Notify = (message: string, kind?: NoticeKind) => void;
152
-
153
150
  /** One recorded earlier value of a secret field. Value-free: the value comes only from `revealHistory`. */
154
151
  interface HistoryEntry {
155
152
  fieldId: string;
@@ -1096,7 +1093,8 @@ interface VaultPort {
1096
1093
  * What only some hosts can do. A missing capability hides its control; it never shows a dead one.
1097
1094
  * - Every host with a signed-in person supplies `identity` (confirm it's you before a secret shows).
1098
1095
  * - The browser extension supplies `page` (the tab's address, the logins that match it) and `fill`.
1099
- * - The desktop app supplies `nativeProvider` (the system credential provider's state).
1096
+ * - The desktop app supplies `nativeProvider` (the system credential provider's state), and its own
1097
+ * `lock`, `clipboard` custody and `revealSeconds`.
1100
1098
  */
1101
1099
  interface VaultHostCapabilities {
1102
1100
  identity?: VaultIdentity;
@@ -1114,12 +1112,71 @@ interface VaultHostCapabilities {
1114
1112
  /** Desktop: the native passkey importer (file chooser, parsing and keys stay native). */
1115
1113
  nativeImport?: NativeImportBridge;
1116
1114
  openUrl?: (url: string) => void;
1115
+ /** A lock the host holds (`lock.ts`): while locked the vault shows Unlock and refuses every port call. */
1116
+ lock?: VaultLock;
1117
+ /** The host's clipboard: every copy goes through it; a secret is the host's to clear (`lock.ts`). */
1118
+ clipboard?: VaultClipboard;
1119
+ /** How long a shown value stays shown, in seconds (an organization knob the host reads); default 20. */
1120
+ revealSeconds?: number;
1117
1121
  nativeProvider?: {
1118
1122
  status: "ready" | "needs_setup" | "unavailable";
1119
1123
  openSetup: () => void;
1120
1124
  };
1121
1125
  }
1122
1126
 
1127
+ /**
1128
+ * A HOST-HELD LOCK. Some hosts lock the vault themselves (the desktop app: Touch ID, starts locked,
1129
+ * locks after idle, on sleep and on screen lock). The host owns the lock and enforces it on its own
1130
+ * side; the vault shows a locked screen with Unlock, and while locked refuses every port call — a
1131
+ * value never reaches a screen while the host says locked. Without `lock` nothing changes.
1132
+ */
1133
+ interface VaultLock {
1134
+ /** Locked right now. */
1135
+ locked(): boolean;
1136
+ /** Told on every change; returns the unsubscribe. */
1137
+ subscribe(listener: () => void): () => void;
1138
+ /** Ask the host to unlock (Touch ID, a click…). Rejects with a sentence a person can act on. */
1139
+ unlock(): Promise<void>;
1140
+ /** Lock now. When given, the vault offers "Lock". */
1141
+ lock?: () => void | Promise<void>;
1142
+ /** The unlock button's words ("Unlock with Touch ID"); default "Unlock". */
1143
+ unlockLabel?: string;
1144
+ }
1145
+ /**
1146
+ * HOST CLIPBOARD CUSTODY. When given, every copy in the vault goes through the host. `secret` asks
1147
+ * the host to take custody: clear it after a while if it is still there, and on lock or quit.
1148
+ */
1149
+ interface VaultClipboard {
1150
+ copy(text: string, how: {
1151
+ secret: boolean;
1152
+ }): Promise<void>;
1153
+ }
1154
+ declare const LOCKED_MESSAGE = "The vault is locked. Unlock it first.";
1155
+ declare class VaultLockedError extends Error {
1156
+ readonly code = "vault_locked";
1157
+ constructor();
1158
+ }
1159
+ /**
1160
+ * The port with every method (areas included) refused while `locked()` says so. The check runs at
1161
+ * call time, so a screen still mounted when the lock lands cannot read a value.
1162
+ */
1163
+ declare function guardPort(port: VaultPort, locked: () => boolean): VaultPort;
1164
+
1165
+ type NoticeKind = "success" | "error" | "info";
1166
+ type Notify = (message: string, kind?: NoticeKind) => void;
1167
+ interface CopyOptions {
1168
+ /** A secret (a password, a code, a key): the host's clipboard takes custody. Default true. */
1169
+ secret?: boolean;
1170
+ }
1171
+ /**
1172
+ * Writes `text` and says so (or says why not). Through the host's clipboard when it has one (custody:
1173
+ * the host clears a secret later), else the browser's. Pure over its inputs, so it is tested alone.
1174
+ */
1175
+ declare function copyValue(text: string, label: string, options: CopyOptions, deps: {
1176
+ clipboard?: VaultClipboard | undefined;
1177
+ notify: Notify;
1178
+ }): Promise<boolean>;
1179
+
1123
1180
  type VaultLayout = "auto" | "full" | "split" | "stack";
1124
1181
  interface VaultAppProps {
1125
1182
  port: VaultPort;
@@ -1183,4 +1240,4 @@ declare function GenerateButton({ onUse, label, limits, disabled }: {
1183
1240
  disabled?: boolean;
1184
1241
  }): react.JSX.Element | null;
1185
1242
 
1186
- export { type ApiVaultPortOptions, CATEGORIES, type CategoryId, DEFAULT_LOCATION, type DefinitionField, type FieldHandling, type FieldKind, GenerateButton, type IdentityAccount, type IdentityMethod, type ItemDraft, type NoticeKind, type Notify, type SaveResult, type SupabaseAuthLike, type TotpReading, type UriMatchMode, type UrlGuess, VaultApp, type VaultAppProps, type VaultAttachment, type VaultAuthenticator, type VaultCustomField, type VaultDefinition, type VaultField, type VaultHostCapabilities, type VaultIdentity, type VaultItem, type VaultItemCapabilities, type VaultKnobs, type VaultLayout, type VaultLocation, type VaultOrganizationRef, type VaultOwner, type VaultPort, type VaultSelection, type VaultSnapshot, type VaultSort, type VaultUserStateDoor, categoryOf, createApiVaultPort, createSupabaseIdentity, createVaultKnobs, inferFromUrl, locationFromParams, locationToParams, matchesQuery, needsRecentSignIn, parseDefinitions, siteOf, sortItems };
1243
+ export { type ApiVaultPortOptions, CATEGORIES, type CategoryId, type CopyOptions, DEFAULT_LOCATION, type DefinitionField, type FieldHandling, type FieldKind, GenerateButton, type IdentityAccount, type IdentityMethod, type ItemDraft, LOCKED_MESSAGE, type NoticeKind, type Notify, type SaveResult, type SupabaseAuthLike, type TotpReading, type UriMatchMode, type UrlGuess, VaultApp, type VaultAppProps, type VaultAttachment, type VaultAuthenticator, type VaultClipboard, type VaultCustomField, type VaultDefinition, type VaultField, type VaultHostCapabilities, type VaultIdentity, 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, locationFromParams, locationToParams, matchesQuery, needsRecentSignIn, parseDefinitions, siteOf, sortItems };
package/dist/index.js CHANGED
@@ -115,9 +115,28 @@ var Ctx = createContext(() => void 0);
115
115
  function useNotify() {
116
116
  return useContext(Ctx);
117
117
  }
118
+ async function copyValue(text, label, options, deps) {
119
+ const secret = options.secret ?? true;
120
+ if (deps.clipboard) {
121
+ try {
122
+ await deps.clipboard.copy(text, { secret });
123
+ deps.notify(`${label} copied`, "success");
124
+ return true;
125
+ } catch (e) {
126
+ deps.notify(e instanceof Error && e.message ? e.message : `${label} could not be copied`, "error");
127
+ return false;
128
+ }
129
+ }
130
+ return copyText(text, { successMessage: `${label} copied`, failureMessage: `${label} could not be copied`, notify: (m, k) => deps.notify(m, k === "error" ? "error" : "success") });
131
+ }
132
+ var ClipboardCtx = createContext(void 0);
133
+ function ClipboardProvider({ clipboard, children }) {
134
+ return /* @__PURE__ */ jsx(ClipboardCtx.Provider, { value: clipboard, children });
135
+ }
118
136
  function useCopy() {
119
137
  const notify = useNotify();
120
- return useCallback((text, label) => copyText(text, { successMessage: `${label} copied`, failureMessage: `${label} could not be copied`, notify: (m, k) => notify(m, k === "error" ? "error" : "success") }), [notify]);
138
+ const clipboard = useContext(ClipboardCtx);
139
+ return useCallback((text, label, options = {}) => copyValue(text, label, options, { clipboard, notify }), [notify, clipboard]);
121
140
  }
122
141
  function NoticeProvider({ notify, children }) {
123
142
  const [shown, setShown] = useState(null);
@@ -494,8 +513,16 @@ import { Badge, Button as Button2, Field as Field2, Textarea } from "@ai-matrx/d
494
513
  import { cn as cn3 } from "@ai-matrx/design-system";
495
514
 
496
515
  // src/hooks.ts
497
- import { useCallback as useCallback2, useEffect as useEffect3, useLayoutEffect, useRef as useRef3, useState as useState3 } from "react";
498
- function useTransientSecret(ms = 2e4) {
516
+ import { createContext as createContext3, createElement, useCallback as useCallback2, useContext as useContext3, useEffect as useEffect3, useLayoutEffect, useRef as useRef3, useState as useState3 } from "react";
517
+ var DEFAULT_REVEAL_MS = 2e4;
518
+ var RevealMs = createContext3(DEFAULT_REVEAL_MS);
519
+ function RevealSecondsProvider({ seconds, children }) {
520
+ const ms = seconds !== void 0 && Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : DEFAULT_REVEAL_MS;
521
+ return createElement(RevealMs.Provider, { value: ms }, children);
522
+ }
523
+ function useTransientSecret(given) {
524
+ const fromHost = useContext3(RevealMs);
525
+ const ms = given ?? fromHost;
499
526
  const [value, setValue] = useState3(null);
500
527
  const timer2 = useRef3(null);
501
528
  const clear = useCallback2(() => {
@@ -531,12 +558,12 @@ function useElementWidth(ref) {
531
558
  }
532
559
 
533
560
  // src/ui.tsx
534
- import { createContext as createContext3, useContext as useContext3, useEffect as useEffect4, useState as useState4 } from "react";
561
+ import { createContext as createContext4, useContext as useContext4, useEffect as useEffect4, useState as useState4 } from "react";
535
562
  import { Dialog as Dialog2, DialogContent as DialogContent2, DialogFooter, DialogHeader as DialogHeader2, DialogTitle as DialogTitle2 } from "@ai-matrx/design-system";
536
563
  import { cn as cn2 } from "@ai-matrx/design-system";
537
564
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
538
- var InPanel = createContext3(false);
539
- var Room = createContext3(null);
565
+ var InPanel = createContext4(false);
566
+ var Room = createContext4(null);
540
567
  var ROOMY_AT = 560;
541
568
  function useRoomOf(ref) {
542
569
  const [room, setRoom] = useState4(null);
@@ -553,7 +580,7 @@ function useRoomOf(ref) {
553
580
  }
554
581
  var RoomProvider = Room.Provider;
555
582
  function Section({ title, actions, children, className }) {
556
- const flat = useContext3(InPanel);
583
+ const flat = useContext4(InPanel);
557
584
  return /* @__PURE__ */ jsxs4("section", { className: cn2("flex flex-col gap-1.5", className), children: [
558
585
  title || actions ? /* @__PURE__ */ jsxs4("div", { className: "flex min-h-7 items-center justify-between gap-2 px-1", children: [
559
586
  title ? /* @__PURE__ */ jsx4("h3", { className: "text-xs font-medium text-muted-foreground", children: title }) : /* @__PURE__ */ jsx4("span", {}),
@@ -568,7 +595,7 @@ var ROW_GRID = {
568
595
  viewport: "grid-cols-[5.75rem_minmax(0,1fr)_auto] gap-x-2 px-3 sm:grid-cols-[9rem_minmax(0,1fr)_auto] sm:gap-x-3 sm:px-4"
569
596
  };
570
597
  function Row({ label, hint, children, actions, top = false, className }) {
571
- const room = useContext3(Room);
598
+ const room = useContext4(Room);
572
599
  return /* @__PURE__ */ jsxs4("div", { className: cn2("grid min-h-11 py-1", ROW_GRID[room ?? "viewport"], top ? "items-start" : "items-center", className), children: [
573
600
  /* @__PURE__ */ jsxs4("div", { className: cn2("flex min-w-0 flex-col text-xs leading-tight text-muted-foreground", top && "pt-2.5"), children: [
574
601
  /* @__PURE__ */ jsx4("span", { className: room === "roomy" ? "truncate" : room === "compact" ? "line-clamp-2 break-words" : "line-clamp-2 break-words sm:truncate", children: label }),
@@ -610,7 +637,7 @@ function SecretText({ value, multiline }) {
610
637
  var quietError = (e, fallback) => wasCancelled(e) ? null : e instanceof Error ? e.message : fallback;
611
638
  function SecretRow({ port, item, field, extra }) {
612
639
  const confirmed = useConfirmed();
613
- const copyValue = useCopy();
640
+ const copyValue2 = useCopy();
614
641
  const secret = useTransientSecret();
615
642
  const [plain, setPlain] = useState5(null);
616
643
  const [busy, setBusy] = useState5(false);
@@ -651,7 +678,7 @@ function SecretRow({ port, item, field, extra }) {
651
678
  }
652
679
  async function copy() {
653
680
  const v = await fetchValue();
654
- if (v !== null) await copyValue(v, field.label);
681
+ if (v !== null) await copyValue2(v, field.label, { secret: !visible });
655
682
  }
656
683
  const multiline = field.kind === "multiline";
657
684
  const shown = visible ? plain : secret.value;
@@ -670,7 +697,7 @@ function SecretRow({ port, item, field, extra }) {
670
697
  );
671
698
  }
672
699
  function PlainRow({ label, value, hint, openUrl, extra, multiline = false }) {
673
- const copyValue = useCopy();
700
+ const copyValue2 = useCopy();
674
701
  const isUrl = /^https?:\/\//i.test(value);
675
702
  return /* @__PURE__ */ jsx5(
676
703
  Row,
@@ -681,7 +708,7 @@ function PlainRow({ label, value, hint, openUrl, extra, multiline = false }) {
681
708
  actions: /* @__PURE__ */ jsxs5(Fragment2, { children: [
682
709
  extra,
683
710
  isUrl && openUrl ? /* @__PURE__ */ jsx5(Button2, { variant: "quiet", icon: /* @__PURE__ */ jsx5(ExternalGlyph, {}), "aria-label": `Open ${value}`, onClick: () => openUrl(value) }) : null,
684
- /* @__PURE__ */ jsx5(Button2, { variant: "quiet", icon: /* @__PURE__ */ jsx5(CopyGlyph, {}), "aria-label": `Copy ${typeof label === "string" ? label : "value"}`, onClick: () => void copyValue(value, typeof label === "string" && label ? label : "Value") })
711
+ /* @__PURE__ */ jsx5(Button2, { variant: "quiet", icon: /* @__PURE__ */ jsx5(CopyGlyph, {}), "aria-label": `Copy ${typeof label === "string" ? label : "value"}`, onClick: () => void copyValue2(value, typeof label === "string" && label ? label : "Value", { secret: false }) })
685
712
  ] }),
686
713
  children: isUrl && openUrl ? (
687
714
  // A real link (right-click, copy link address, open in a new tab); a click goes through the host's opener.
@@ -1327,7 +1354,7 @@ function HistoryPanel({ item, port, arg, onClose, onChanged }) {
1327
1354
  function HistoryRow({ item, port, page, entry, label, onRestored, onConflict, onError }) {
1328
1355
  const confirmed = useConfirmed();
1329
1356
  const secret = useTransientSecret();
1330
- const copyValue = useCopy();
1357
+ const copyValue2 = useCopy();
1331
1358
  const [busy, setBusy] = useState7(false);
1332
1359
  const when = formatDate(entry.recordedAt, true);
1333
1360
  async function read() {
@@ -1350,7 +1377,7 @@ function HistoryRow({ item, port, page, entry, label, onRestored, onConflict, on
1350
1377
  }
1351
1378
  async function copy() {
1352
1379
  const v = await read();
1353
- if (v !== null) await copyValue(v, `Earlier ${label.toLowerCase()}`);
1380
+ if (v !== null) await copyValue2(v, `Earlier ${label.toLowerCase()}`);
1354
1381
  }
1355
1382
  async function restore() {
1356
1383
  const yes = await confirm({
@@ -1535,13 +1562,13 @@ function CountdownRing({ remaining, period, loading = false, className }) {
1535
1562
  }
1536
1563
  function useCopied() {
1537
1564
  const [copied, setCopied] = useState8(false);
1538
- const copyValue = useCopy();
1565
+ const copyValue2 = useCopy();
1539
1566
  const t = useRef6(null);
1540
1567
  useEffect8(() => () => void (t.current && clearTimeout(t.current)), []);
1541
1568
  return {
1542
1569
  copied,
1543
1570
  copy: async (code) => {
1544
- const ok = await copyValue(code, "One-time code");
1571
+ const ok = await copyValue2(code, "One-time code");
1545
1572
  if (!ok) return;
1546
1573
  setCopied(true);
1547
1574
  if (t.current) clearTimeout(t.current);
@@ -2683,7 +2710,7 @@ function ItemSettingsPanel({ item, port, onClose, onChanged, onPanel }) {
2683
2710
  }
2684
2711
 
2685
2712
  // src/generator/PasswordGenerator.tsx
2686
- import { createContext as createContext4, useContext as useContext4, useEffect as useEffect12, useRef as useRef10, useState as useState15 } from "react";
2713
+ import { createContext as createContext5, useContext as useContext5, useEffect as useEffect12, useRef as useRef10, useState as useState15 } from "react";
2687
2714
  import { Popover, PopoverContent, PopoverTrigger, cn as cn7 } from "@ai-matrx/design-system";
2688
2715
  import { Button as Button11, NumberStepper, SegmentedControl as SegmentedControl2, Select as Select4, Switch as Switch2 } from "@ai-matrx/design-system/controls";
2689
2716
 
@@ -2744,12 +2771,12 @@ function strengthOf(bits) {
2744
2771
 
2745
2772
  // src/generator/PasswordGenerator.tsx
2746
2773
  import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2747
- var LimitsCtx = createContext4(null);
2774
+ var LimitsCtx = createContext5(null);
2748
2775
  function GeneratorLimitsProvider({ resolve: resolve2, children }) {
2749
2776
  return /* @__PURE__ */ jsx14(LimitsCtx.Provider, { value: resolve2 ?? null, children });
2750
2777
  }
2751
2778
  function useGeneratorLimitsSource() {
2752
- return useContext4(LimitsCtx);
2779
+ return useContext5(LimitsCtx);
2753
2780
  }
2754
2781
  var MASK3 = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
2755
2782
  function PasswordGenerator({ limits: source, onUse, useLabel = "Use" }) {
@@ -2759,7 +2786,7 @@ function PasswordGenerator({ limits: source, onUse, useLabel = "Use" }) {
2759
2786
  const [candidate, setCandidate] = useState15(null);
2760
2787
  const [revealed, setRevealed] = useState15(false);
2761
2788
  const [note, setNote] = useState15(null);
2762
- const copyValue = useCopy();
2789
+ const copyValue2 = useCopy();
2763
2790
  const expiry = useRef10(null);
2764
2791
  const drop = () => {
2765
2792
  if (expiry.current) clearTimeout(expiry.current);
@@ -2819,7 +2846,7 @@ function PasswordGenerator({ limits: source, onUse, useLabel = "Use" }) {
2819
2846
  const strength = candidate ? strengthOf(candidate.bits) : null;
2820
2847
  const copy = async () => {
2821
2848
  if (!candidate) return;
2822
- const ok = await copyValue(candidate.value, "Generated password");
2849
+ const ok = await copyValue2(candidate.value, "Generated password");
2823
2850
  setNote(ok ? { tone: "success", text: "Copied. Your clipboard keeps it." } : { tone: "destructive", text: "Couldn\u2019t copy. Reveal it and copy by hand." });
2824
2851
  };
2825
2852
  const use = () => {
@@ -2930,6 +2957,45 @@ function importLimitsFrom(resolved) {
2930
2957
  return out;
2931
2958
  }
2932
2959
 
2960
+ // src/lock.ts
2961
+ import { useSyncExternalStore as useSyncExternalStore2 } from "react";
2962
+ var LOCKED_MESSAGE = "The vault is locked. Unlock it first.";
2963
+ var VaultLockedError = class extends Error {
2964
+ code = "vault_locked";
2965
+ constructor() {
2966
+ super(LOCKED_MESSAGE);
2967
+ this.name = "VaultLockedError";
2968
+ }
2969
+ };
2970
+ function guardPort(port, locked) {
2971
+ const cache = /* @__PURE__ */ new WeakMap();
2972
+ const wrap = (target) => {
2973
+ const hit = cache.get(target);
2974
+ if (hit) return hit;
2975
+ const proxy = new Proxy(target, {
2976
+ get(obj2, key, receiver) {
2977
+ const value = Reflect.get(obj2, key, receiver);
2978
+ if (typeof value === "function") {
2979
+ return (...args) => {
2980
+ if (locked()) return Promise.reject(new VaultLockedError());
2981
+ return value.apply(obj2, args);
2982
+ };
2983
+ }
2984
+ if (value && typeof value === "object") return wrap(value);
2985
+ return value;
2986
+ }
2987
+ });
2988
+ cache.set(target, proxy);
2989
+ return proxy;
2990
+ };
2991
+ return wrap(port);
2992
+ }
2993
+ var NEVER = () => () => void 0;
2994
+ var UNLOCKED = () => false;
2995
+ function useLocked(lock) {
2996
+ return useSyncExternalStore2(lock ? lock.subscribe : NEVER, lock ? () => lock.locked() : UNLOCKED, UNLOCKED);
2997
+ }
2998
+
2933
2999
  // src/io/BackupPanel.tsx
2934
3000
  import { useEffect as useEffect13, useRef as useRef11, useState as useState17 } from "react";
2935
3001
  import { Button as Button13, Field as Field6, SegmentedControl as SegmentedControl3, Switch as Switch3 } from "@ai-matrx/design-system/controls";
@@ -4806,7 +4872,7 @@ var MASK4 = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\
4806
4872
  var say2 = (e, fallback) => e instanceof Error && e.message ? e.message : fallback;
4807
4873
  function RecoveryCodesRow({ item, field, port, onChanged }) {
4808
4874
  const confirmed = useConfirmed();
4809
- const copyValue = useCopy();
4875
+ const copyValue2 = useCopy();
4810
4876
  const secret = useTransientSecret();
4811
4877
  const [busy, setBusy] = useState21(false);
4812
4878
  const [error, setError] = useState21(null);
@@ -4894,7 +4960,7 @@ function RecoveryCodesRow({ item, field, port, onChanged }) {
4894
4960
  children: sealed ? /* @__PURE__ */ jsx21(Badge6, { icon: /* @__PURE__ */ jsx21(LockGlyph, {}), children: "Sealed" }) : codes ? /* @__PURE__ */ jsxs20("ul", { className: "flex flex-col gap-0.5", "aria-label": "Recovery codes", children: [
4895
4961
  codes.map((code, i) => /* @__PURE__ */ jsxs20("li", { className: "flex min-h-7 items-center gap-1", children: [
4896
4962
  /* @__PURE__ */ jsx21("code", { className: "min-w-0 flex-1 break-all font-mono text-sm text-foreground", children: code }),
4897
- /* @__PURE__ */ jsx21(Button17, { variant: "quiet", icon: /* @__PURE__ */ jsx21(CopyGlyph, {}), "aria-label": `Copy code ${i + 1}`, onClick: () => void copyValue(code, "Recovery code") }),
4963
+ /* @__PURE__ */ jsx21(Button17, { variant: "quiet", icon: /* @__PURE__ */ jsx21(CopyGlyph, {}), "aria-label": `Copy code ${i + 1}`, onClick: () => void copyValue2(code, "Recovery code") }),
4898
4964
  canEdit ? /* @__PURE__ */ jsx21(Button17, { variant: "quiet", icon: /* @__PURE__ */ jsx21(CheckGlyph, {}), disabled: busy, onClick: () => void markUsed(i), children: "Mark used" }) : null
4899
4965
  ] }, `${i}-${code}`)),
4900
4966
  error ? /* @__PURE__ */ jsx21("li", { role: "alert", className: "text-xs text-destructive", children: error }) : null
@@ -5963,7 +6029,7 @@ function ListRow({ item, family, port, on, onSelect, withOwner }) {
5963
6029
  setBusy(true);
5964
6030
  try {
5965
6031
  const v = await confirmed(() => port.reveal(item, key));
5966
- await copy(v, label);
6032
+ await copy(v, label, { secret: key !== user?.key || user.handling !== "visible" });
5967
6033
  } catch (e) {
5968
6034
  if (!wasCancelled(e)) notify(e instanceof Error ? e.message : `${label} could not be read`, "error");
5969
6035
  } finally {
@@ -7137,12 +7203,12 @@ import { DropdownMenu as DropdownMenu5, DropdownMenuContent as DropdownMenuConte
7137
7203
  import { Button as Button32 } from "@ai-matrx/design-system/controls";
7138
7204
 
7139
7205
  // src/extension/CaptureCard.tsx
7140
- import { useState as useState35, useSyncExternalStore as useSyncExternalStore2 } from "react";
7206
+ import { useState as useState35, useSyncExternalStore as useSyncExternalStore3 } from "react";
7141
7207
  import { Button as Button29, SearchField as SearchField3 } from "@ai-matrx/design-system/controls";
7142
7208
  import { Fragment as Fragment19, jsx as jsx34, jsxs as jsxs32 } from "react/jsx-runtime";
7143
7209
  var SEARCH_FROM = 4;
7144
7210
  function CaptureCard({ capture, onChanged }) {
7145
- const prompt = useSyncExternalStore2(capture.subscribe, capture.get, capture.get);
7211
+ const prompt = useSyncExternalStore3(capture.subscribe, capture.get, capture.get);
7146
7212
  const notify = useNotify();
7147
7213
  const [busy, setBusy] = useState35(false);
7148
7214
  const [q, setQ] = useState35("");
@@ -7201,12 +7267,12 @@ function CaptureCard({ capture, onChanged }) {
7201
7267
  }
7202
7268
 
7203
7269
  // src/extension/FillSetupCard.tsx
7204
- import { useRef as useRef18, useState as useState36, useSyncExternalStore as useSyncExternalStore3 } from "react";
7270
+ import { useRef as useRef18, useState as useState36, useSyncExternalStore as useSyncExternalStore4 } from "react";
7205
7271
  import { Button as Button30, Field as Field12 } from "@ai-matrx/design-system/controls";
7206
7272
  import { jsx as jsx35, jsxs as jsxs33 } from "react/jsx-runtime";
7207
7273
  function FillSetupCard({ fillSetup }) {
7208
7274
  const notify = useNotify();
7209
- const state = useSyncExternalStore3(fillSetup.subscribe, fillSetup.get, fillSetup.get);
7275
+ const state = useSyncExternalStore4(fillSetup.subscribe, fillSetup.get, fillSetup.get);
7210
7276
  const box = useRef18(null);
7211
7277
  const [busy, setBusy] = useState36(false);
7212
7278
  const [error, setError] = useState36(null);
@@ -7641,7 +7707,31 @@ function ImportWithLimits(props) {
7641
7707
  return /* @__PURE__ */ jsx39(Panel, { title: "Import", onClose: rest.onClose, children: /* @__PURE__ */ jsx39(Empty, { children: state ? state.error : "Reading this organization\u2019s import settings\u2026" }) });
7642
7708
  }
7643
7709
  function VaultApp(props) {
7644
- return /* @__PURE__ */ jsx39("div", { className: "relative h-full min-h-0 w-full", children: /* @__PURE__ */ jsx39(NoticeProvider, { notify: props.notify, children: /* @__PURE__ */ jsx39(IdentityGate, { identity: props.host?.identity, children: /* @__PURE__ */ jsx39(GeneratorLimitsProvider, { resolve: generatorLimitsFrom(props.host?.knobs), children: /* @__PURE__ */ jsx39(Workspace, { ...props }) }) }) }) });
7710
+ const lock = props.host?.lock;
7711
+ const locked = useLocked(lock);
7712
+ const port = useMemo6(() => lock ? guardPort(props.port, () => lock.locked()) : props.port, [props.port, lock]);
7713
+ return /* @__PURE__ */ jsx39("div", { className: "relative h-full min-h-0 w-full", children: /* @__PURE__ */ jsx39(NoticeProvider, { notify: props.notify, children: lock && locked ? /* @__PURE__ */ jsx39(LockedScreen, { lock }) : /* @__PURE__ */ jsx39(ClipboardProvider, { clipboard: props.host?.clipboard, children: /* @__PURE__ */ jsx39(RevealSecondsProvider, { seconds: props.host?.revealSeconds, children: /* @__PURE__ */ jsx39(IdentityGate, { identity: props.host?.identity, children: /* @__PURE__ */ jsx39(GeneratorLimitsProvider, { resolve: generatorLimitsFrom(props.host?.knobs), children: /* @__PURE__ */ jsx39(Workspace, { ...props, port }) }) }) }) }) }) });
7714
+ }
7715
+ function LockedScreen({ lock }) {
7716
+ const [busy, setBusy] = useState40(false);
7717
+ const [error, setError] = useState40(null);
7718
+ const unlock = async () => {
7719
+ setBusy(true);
7720
+ setError(null);
7721
+ try {
7722
+ await lock.unlock();
7723
+ } catch (e) {
7724
+ setError(e instanceof Error && e.message ? e.message : "The vault stayed locked.");
7725
+ } finally {
7726
+ setBusy(false);
7727
+ }
7728
+ };
7729
+ return /* @__PURE__ */ jsxs37("div", { role: "region", "aria-label": "Vault locked", className: "flex h-full min-h-0 flex-col items-center justify-center gap-3 p-6 text-center", children: [
7730
+ /* @__PURE__ */ jsx39(LockGlyph, { className: "size-8 text-muted-foreground" }),
7731
+ /* @__PURE__ */ jsx39("div", { className: "text-base font-medium text-foreground", children: "Vault locked" }),
7732
+ /* @__PURE__ */ jsx39(Button33, { variant: "primary", disabled: busy, onClick: () => void unlock(), children: lock.unlockLabel ?? "Unlock" }),
7733
+ error ? /* @__PURE__ */ jsx39("div", { role: "alert", className: "max-w-sm text-sm text-destructive", children: error }) : null
7734
+ ] });
7645
7735
  }
7646
7736
  function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocationChange }) {
7647
7737
  const rootRef = useRef19(null);
@@ -7794,6 +7884,10 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
7794
7884
  host.nativeProvider && host.nativeProvider.status !== "unavailable" ? /* @__PURE__ */ jsxs37(DropdownMenuItem5, { onSelect: host.nativeProvider.openSetup, children: [
7795
7885
  /* @__PURE__ */ jsx39(ShieldGlyph, { className: "size-4" }),
7796
7886
  "System AutoFill"
7887
+ ] }) : null,
7888
+ host.lock?.lock ? /* @__PURE__ */ jsxs37(DropdownMenuItem5, { onSelect: () => void host.lock?.lock?.(), children: [
7889
+ /* @__PURE__ */ jsx39(LockGlyph, { className: "size-4" }),
7890
+ "Lock"
7797
7891
  ] }) : null
7798
7892
  ] })
7799
7893
  ] })
@@ -8826,11 +8920,15 @@ export {
8826
8920
  CATEGORIES,
8827
8921
  DEFAULT_LOCATION,
8828
8922
  GenerateButton,
8923
+ LOCKED_MESSAGE,
8829
8924
  VaultApp,
8925
+ VaultLockedError,
8830
8926
  categoryOf,
8927
+ copyValue,
8831
8928
  createApiVaultPort,
8832
8929
  createSupabaseIdentity,
8833
8930
  createVaultKnobs,
8931
+ guardPort,
8834
8932
  inferFromUrl,
8835
8933
  locationFromParams,
8836
8934
  locationToParams,