teqo-backup/src/utilities/campaignBiometricsPrompt.ts
Francisco Ignácio Fontoura Solla 763f64080a Deliver B40: biometric login for /campanha.
WebAuthn platform passkeys: opt-in enrollment after a password login, a
password-less sign-in that mints the same 14-day campaign-token, and
revocation from the profile. The session is minted through Payload's public
pieces (checkLoginPermission, addSessionToUser, getFieldsToSign, jwtSign)
inside a transaction with an advisory lock, so a passkey login neither
bypasses a lockout nor evicts the account's other sessions. The rpID is
derived from NEXT_PUBLIC_SITE_URL and resolves to null when the request host
disagrees, so previews never offer a ceremony the browser would refuse.
2026-07-28 10:59:23 -03:00

65 lines
2.4 KiB
TypeScript

/**
* Browser-side memory for the biometric-enrollment prompt (B40). Sibling of
* `recentVisits.ts` / `campaignGeolocation.ts`: no Payload, no Next, just
* `localStorage` behind failure-tolerant reads.
*
* Two flags, and the split is the whole point: the server knows HOW MANY
* passkeys an account has, never WHICH device it is being read on. So "already
* enrolled here" and "stop offering" can only be answered locally, and they are
* kept in `localStorage` rather than `sessionStorage` — a decision about this
* phone should outlive the tab (the PWA install toast, which is a suggestion
* rather than an answer, deliberately uses `sessionStorage` instead).
*/
const CAMPAIGN_BIOMETRICS_ENROLLED_KEY = 'teqo:campaign:biometrics-enrolled'
/** Exported because the e2e spec pre-sets it instead of hardcoding the string. */
export const CAMPAIGN_BIOMETRICS_PROMPT_DISMISSED_KEY = 'teqo:campaign:biometrics-prompt-dismissed'
const read = (key: string): boolean => {
if (typeof window === 'undefined') return false
try {
return window.localStorage.getItem(key) === '1'
} catch {
// localStorage throws in private mode / with storage disabled.
return false
}
}
const write = (key: string): void => {
if (typeof window === 'undefined') return
try {
window.localStorage.setItem(key, '1')
} catch {
// Ignore quota / private-mode failures: the prompt reappearing is a far
// smaller problem than a thrown error inside a toast callback.
}
}
const clear = (key: string): void => {
if (typeof window === 'undefined') return
try {
window.localStorage.removeItem(key)
} catch {
// Same tolerance as above.
}
}
export const wasCampaignBiometricsEnrolledHere = (): boolean =>
read(CAMPAIGN_BIOMETRICS_ENROLLED_KEY)
export const markCampaignBiometricsEnrolledHere = (): void =>
write(CAMPAIGN_BIOMETRICS_ENROLLED_KEY)
/**
* Called when the account has no passkeys at all: whatever this browser
* believed about "enrolled here" is stale, and a person who revoked every
* device should be offered enrollment again.
*/
export const forgetCampaignBiometricsEnrollment = (): void =>
clear(CAMPAIGN_BIOMETRICS_ENROLLED_KEY)
export const wasCampaignBiometricsPromptDismissed = (): boolean =>
read(CAMPAIGN_BIOMETRICS_PROMPT_DISMISSED_KEY)
export const dismissCampaignBiometricsPrompt = (): void =>
write(CAMPAIGN_BIOMETRICS_PROMPT_DISMISSED_KEY)