/** * A name for a passkey the customer did not name themselves (#38). * * The management screen (#40) lists credentials and offers to revoke them, so * two entries that read identically are a screen where the customer cannot tell * which device they are removing. A default that says something is the * difference between "Passkey, Passkey, Passkey" and a list worth showing. * * Derived from the authenticator's transports, which is the only thing the * ceremony learns about the device. It is a hint rather than a fact — the * browser reports what the authenticator claims — so these are deliberately * vague. "This device" is honest about a platform authenticator in a way that * guessing "MacBook" would not be. */ /** The fallback when the authenticator reports nothing usable. */ export const GENERIC_CREDENTIAL_NAME = 'Passkey'; export function defaultCredentialName(transports: readonly string[] | null | undefined): string { if (!transports || transports.length === 0) return GENERIC_CREDENTIAL_NAME; // Checked in this order because an authenticator can report several. A phone // used as a cross-device passkey reports `hybrid` and often `internal` too, // and "Phone or tablet" is the more useful of the two readings — `internal` // alone means the authenticator built into the machine being used. if (transports.includes('hybrid')) return 'Phone or tablet'; if (transports.includes('internal')) return 'This device'; if (transports.some((t) => t === 'usb' || t === 'nfc' || t === 'ble')) return 'Security key'; return GENERIC_CREDENTIAL_NAME; } /** * The customer's own name for a passkey, or null when they gave none. * * Trimmed, because a name of spaces is a name nobody can read in a list, and * bounded because this is rendered — a customer is naming their laptop, not * writing prose, and an unbounded string in a table cell is a layout problem * rather than an expressive one. */ export const MAX_CREDENTIAL_NAME_LENGTH = 64; export function readCredentialName(value: unknown): string | null { if (typeof value !== 'string') return null; const trimmed = value.trim(); if (trimmed === '') return null; return trimmed.slice(0, MAX_CREDENTIAL_NAME_LENGTH); }