diff --git a/src/components/ui/__tests__/password-input.test.tsx b/src/components/ui/__tests__/password-input.test.tsx new file mode 100644 index 000000000..dadb1389d --- /dev/null +++ b/src/components/ui/__tests__/password-input.test.tsx @@ -0,0 +1,44 @@ +import { describe, it, expect } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { I18nProvider } from "@/lib/i18n/context"; +import { PasswordInput } from "../password-input"; + +/** + * PasswordInput bakes the password-manager autofill guard in by default so + * BYOK API keys / webhook secrets / ntfy tokens (config secrets, not + * credentials) are never offered up to Bitwarden / 1Password / LastPass. + * Credential fields that pass a real autocomplete token keep manager + * integration. + */ +function render(node: React.ReactNode) { + return renderToStaticMarkup( + {node}, + ); +} + +describe("PasswordInput", () => { + it("suppresses password managers by default for config secrets", () => { + const html = render(); + expect(html).toMatch(/autocomplete="off"/i); + expect(html).toContain("data-bwignore"); + expect(html).toContain("data-1p-ignore"); + expect(html).toContain('data-lpignore="true"'); + }); + + it("treats an explicit autoComplete=off as a config secret too", () => { + const html = render(); + expect(html).toContain("data-bwignore"); + expect(html).toContain("data-1p-ignore"); + }); + + it("keeps manager integration for credential fields", () => { + const html = render( + , + ); + expect(html).toMatch(/autocomplete="current-password"/i); + expect(html).not.toContain("data-bwignore"); + expect(html).not.toContain("data-1p-ignore"); + expect(html).not.toContain("data-lpignore"); + }); +}); diff --git a/src/components/ui/password-input.tsx b/src/components/ui/password-input.tsx index 7e1ac7625..e9149c3ce 100644 --- a/src/components/ui/password-input.tsx +++ b/src/components/ui/password-input.tsx @@ -22,14 +22,28 @@ import { useTranslations } from "@/lib/i18n/context"; */ export function PasswordInput({ className, + autoComplete, ...props }: React.ComponentProps) { const [visible, setVisible] = useState(false); const { t } = useTranslations(); + + // Config/secret fields (BYOK API keys, webhook secrets, ntfy tokens) pass no + // `autoComplete` — they are not credentials, so a password manager offering + // to fill or *save* them is noise and risks clobbering the field. Suppress + // Bitwarden / 1Password / LastPass by default for those. Credential fields + // (login, register, password change) pass a real token + // (`current-password` / `new-password` / `username`) and keep manager + // integration; only an explicit `"off"` opts back into suppression. + const suppressManagers = autoComplete == null || autoComplete === "off"; return (