Summary
@spfn/auth binds a login to a device key (per-device keypair, device-code approval from a signed-in device, listKeys / revokeKey / revokeAllKeys). What the account owner cannot do today is learn that a device was added. A stolen password used to register a new device is silent: nothing in the package fires when a key is registered, authLoginEvent carries provider but not the device, and there is no way to hand the owner a "sign every device out" action they can take from an email without first signing in on a device they may no longer trust.
Outcome
- A
device registered event (authDeviceRegisteredEvent, auth.device.registered) emitted after a key is registered on any channel — password login on a new device, OAuth callback, device-code approval, passkey login — with { userId, keyId, deviceName?, platform?, algorithm, fingerprintPrefix, ip?, userAgent?, createdAtMillis, channel }. Nothing is emitted for key rotation of an existing device. The app subscribes and sends the notice; the package sends no mail.
- A signed sign-out-everywhere link.
createRevokeAllLink(userId, { ttl }) returns a URL under /_auth/keys/revoke-all/<token>; opening it revokes every key of that user (including any device-code approval in flight, as revokeAllKeys already does), needs no session, is single-use, expires, and answers a plain confirmation page or a JSON result for an app-rendered page. The token is bound to the user's current key generation so it is dead once used or once revokeAllKeys ran by another path.
listKeys grows ip? and userAgent? (captured at registration, never updated) so the device list an app renders can show where a device came from.
Narrow review dispatch contract
- Required behavior: the event fires exactly once per new key on every registration channel; the link revokes all keys without a session, once, within its TTL; the list carries the two new optional fields.
- Touched surfaces: key registration paths (
register, login, oauth/finalize, device/*/approve, passkeys/login/verify), keys routes, Events docs.
- Supported inputs: an existing user with ≥1 key; a token that is valid, expired, reused, or from a user whose keys were already revoked.
- Trust boundary: the link token is a bearer capability — HMAC with the auth secret, ≥128-bit random id, constant-time compare, stored hash only; a reused or unknown token answers the same 404 as an expired one.
- Explicit exclusions: sending email, a UI, MFA (separate issue), notifying on key rotation, geo-IP.
- Stop condition: tests for each channel firing the event; tests for valid/expired/reused/revoked-elsewhere tokens; README section.
Summary
@spfn/authbinds a login to a device key (per-device keypair, device-code approval from a signed-in device,listKeys/revokeKey/revokeAllKeys). What the account owner cannot do today is learn that a device was added. A stolen password used to register a new device is silent: nothing in the package fires when a key is registered,authLoginEventcarriesproviderbut not the device, and there is no way to hand the owner a "sign every device out" action they can take from an email without first signing in on a device they may no longer trust.Outcome
device registeredevent (authDeviceRegisteredEvent,auth.device.registered) emitted after a key is registered on any channel — password login on a new device, OAuth callback, device-code approval, passkey login — with{ userId, keyId, deviceName?, platform?, algorithm, fingerprintPrefix, ip?, userAgent?, createdAtMillis, channel }. Nothing is emitted for key rotation of an existing device. The app subscribes and sends the notice; the package sends no mail.createRevokeAllLink(userId, { ttl })returns a URL under/_auth/keys/revoke-all/<token>; opening it revokes every key of that user (including any device-code approval in flight, asrevokeAllKeysalready does), needs no session, is single-use, expires, and answers a plain confirmation page or a JSON result for an app-rendered page. The token is bound to the user's current key generation so it is dead once used or oncerevokeAllKeysran by another path.listKeysgrowsip?anduserAgent?(captured at registration, never updated) so the device list an app renders can show where a device came from.Narrow review dispatch contract
register,login,oauth/finalize,device/*/approve,passkeys/login/verify),keysroutes,Eventsdocs.