/** * @file the password-manager save — ids reach the browser's storage only by * this gesture-gated path (or copy), never localStorage/IndexedDB, which * are ambient and XSS-reachable. It runs in the *browser*: pages embed it * via `pmSave.toString()`, so it must be self-contained (no imports, no * closure, no inner named functions — wrangler's keepNames would wrap * those in an `__name` helper the browser doesn't have). Its powers * (the petname field, the credential store, the console) are passed in, * so tests can stand in for the browser. */ /** the browser powers pmSave uses */ export type PmPowers = { petname: { value: string }; credentials?: { store(credential: unknown): Promise }; PasswordCredential?: new (init: { id: string; password: string }) => unknown; log: Pick; }; /** the button pmSave reports on */ export type PmButton = { textContent: string | null; disabled: boolean }; /** * gesture-gated save of `value` under the holder's petname (the PM * username). * * @see https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/store */ export function pmSave(btn: PmButton, value: string, powers: PmPowers): Promise { const label = powers.petname.value.trim(); if (!label) { btn.textContent = "give it a petname first"; return Promise.resolve(); } btn.disabled = true; if (!powers.PasswordCredential || !powers.credentials) { powers.log.warn({ event: "pm-save", outcome: "unsupported", detail: "no PasswordCredential / navigator.credentials" }); btn.textContent = "browser has no credential store — copy instead"; btn.disabled = false; return Promise.resolve(); } return powers.credentials .store(new powers.PasswordCredential({ id: label, password: value })) .then(() => { // store() resolves with undefined per spec, whether the holder // accepted the browser's prompt or not (or no prompt showed): // we can only say it was offered — the PM's own UI is the truth powers.log.info({ event: "pm-save", outcome: "offered", label }); btn.textContent = "offered to the password manager ✓ (see its prompt)"; btn.disabled = false; }) .catch((err) => { powers.log.error({ event: "pm-save", error: String(err) }); btn.textContent = "save failed — copy instead"; btn.disabled = false; }); }