Skip to content

feat(passkey): let a signed-in user add a passkey - #147

Merged
Bccorb merged 2 commits into
mainfrom
feat/enroll-a-passkey-while-signed-in
Sep 8, 2026
Merged

feat(passkey): let a signed-in user add a passkey#147
Bccorb merged 2 commits into
mainfrom
feat/enroll-a-passkey-while-signed-in

Conversation

@Bccorb

@Bccorb Bccorb commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Coordinated with fells-code/seamless-auth-api#287 and
fells-code/seamless-auth-server#155. Neither can merge alone.

registerPasskey lived only on the client, so the bundled UI promised something the package
did not implement: "You can continue without one and add a passkey later from a device that
does" (PassKeyRegistration.tsx:110).
credentials could be listed and deleted but never added. It is now on useAuth() and on
the framework-agnostic session actions, and the context version refreshes the session
afterwards so a settings screen renders the new passkey without a reload.

Enrollment requires a signed-in session as of the matching API and adapter releases. The
bundled flow already satisfies it, because verifying the email OTP issues a session before
the passkey screen is reached, so no view moves and no request path changes. An
application that called registerPasskey() before verifying an address has to move that
call after it.

A 401 at enrollment now says the session expired rather than "Error registering passkey."
It is the session rather than anything about the authenticator, and the generic wording
invited a retry with the same expired one. isUnauthenticated(error) is exported for
callers rendering their own screens.

Also corrects a README example that still passed a token to registerPasskey, a field
removed when the wire contract moved to @seamless-auth/types, and documents that
enrollment comes after the step that establishes a session.

Type of Change

  • Feature
  • Fix
  • Refactor
  • Documentation
  • Test

Release Impact

  • Changeset added for adopter-facing package change
  • No package release expected

minor. Releases in lockstep with the API and the adapter: there is no safe order between
those two, and enrollment answers 401 until both land.

Checklist

  • Tests pass
  • No breaking changes
  • Changeset summary is clear for SDK adopters
  • Docs updated (if needed)
  • Security implications considered

On breaking changes: nothing in this package's own API breaks, but enrollment now
requires a session because of the coordinated API change, so the box stays unchecked
honestly.

On security: this is the client half of closing an account-takeover path. /login and
/registration/register both mint a pre-auth token for an existing account from an email
address alone, and enrollment used to accept it.

Verification

npm run typecheck, npm run lint, npm run format:check and npm run build all clean.
Tests: 32 suites, 327 passed (was 324).

New coverage: the session store adds an enrolled passkey to credentials and leaves state
alone when enrollment fails; the view renders the re-authenticate message on a 401 and not
the generic one. Reverting either behaviour fails four tests.

Testing locally

Link this and the adapter into a test app rather than pulling published versions, since all
three move together. The two paths worth exercising are enrolling during signup, which is
what #278 broke, and adding a second passkey from a signed-in screen, which was not possible
before at all.

registerPasskey lived only on the client, so the bundled UI promised something the
package did not implement: "You can continue without one and add a passkey later from a
device that does". credentials could be listed and deleted but never added. It is now on
useAuth() and on the framework-agnostic session actions, and the context version
refreshes the session afterwards so a settings screen renders the new passkey without a
reload.

Enrollment requires a signed-in session as of the matching seamless-auth-api and adapter
releases. The bundled flow already satisfies it: verifying the email OTP issues a session
before the passkey screen is reached, so no view moves. An application that called
registerPasskey() before verifying an address has to move that call after it.

A 401 at enrollment now says the session expired rather than "Error registering passkey".
It is the session rather than anything about the authenticator, and the generic wording
invited a retry with the same expired one. isUnauthenticated(error) is exported for
callers rendering their own screens.

Also corrects a README example that still passed a token to registerPasskey, a field
removed when the wire contract moved to @seamless-auth/types, and documents that
enrollment comes after the step that establishes a session.
The auth API and the server adapter have no safe release order between them, so
enrollment answers 401 until both land. An adopter upgrading one package at a time needs
to read that here.
@Bccorb
Bccorb merged commit 18927ff into main Sep 8, 2026
3 checks passed
@Bccorb
Bccorb deleted the feat/enroll-a-passkey-while-signed-in branch September 8, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant