Skip to content

feat(enrollment): track and invite passkey enrollment - #351

Merged
Bccorb merged 2 commits into
mainfrom
feat/enrollment-tracking
Oct 6, 2026
Merged

Bccorb merged 2 commits into
mainfrom
feat/enrollment-tracking

Conversation

@Bccorb

@Bccorb Bccorb commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Closes #338 (epic #334). Contract change. It adds two admin routes, a system config key, nextStep on code and magic link sign-ins, an auth event type and a delivery kind. The ripple PRs are listed below.

Changes

GET /admin/enrollment (admin read)

  • Lists active (non-revoked) users with their WebAuthn credential count and status: none, one, two_or_more. TOTP does not count.
  • Filters: organizationId, status, imported (users from POST /admin/users/import) and email search. LIKE wildcards in the search are matched literally.
  • summary counts every match whatever the status filter, which also feeds the coverage report in No authentication coverage report for assessment and insurance responses #178.

POST /admin/enrollment/invites (admin write)

  • Emails each target a notice to sign in and add a passkey. The link is the tenant's sign-in page (signInUrl on a configured origin, default <frontend_url>/login) and carries no credential.
  • Targets are userIds (up to 200) or an organizationId. For an organization, members at or below status are invited 200 at a time, anyone invited in the last 24 hours is skipped, and remaining says what is left.
  • Per-user results come back with skip reasons: not_found, already_enrolled, recently_invited, delivery_failed.
  • With x-seamless-auth-delivery-mode: external, each result carries the delivery for the caller to send instead. This does not require the service token, because the invite holds no secret.
  • Answers 409 when no sign-in method other than passkey is enabled, since invited users would have no way in.
  • Each invite is logged as admin_enrollment_invite_sent against the acting admin.
  • users.enrollment_invited_at records the last invite. This needs a migration.

prompt_passkey_enrollment (default off, env PROMPT_PASSKEY_ENROLLMENT)

With it on, these sign-ins carry nextStep: 'enroll_passkey' for a user with no passkey: email and phone code sign-ins, the registration email code, and the magic link poll. That is what sends an invited user into enrollment, whichever way they sign in.

Dependencies

@seamless-auth/types 0.26.0 and @seamless-auth/messaging 0.2.0 (plus aws and twilio). That also clears the last AGPL-3.0-only lockfile entries left after #335.

Ripple

Verification

  • The enrollment and invite SQL ran against a real Postgres 16 with seeded users, credentials, an organization and external ids. It checked counts and revoked exclusion, organization and imported scoping, literal LIKE matching, pagination, organization batching with the 24-hour skip, per-user skip reasons, and enrollment_invited_at updates. The migration ran up and down cleanly.
  • A security review covered SQL injection, authorization, relay or phishing through signInUrl, and the external delivery exemption. No findings.
  • npm run typecheck, lint, format:check, test:run (1585), build, check:changeset-bump and npm audit --omit=dev --audit-level=high (0) all pass. Branch coverage is above the 95% pre-commit gate.

Bccorb added 2 commits October 5, 2026 23:19
Adds GET /admin/enrollment for enrollment progress per organization and
POST /admin/enrollment/invites to email users a notice to sign in and
add a passkey. A new prompt_passkey_enrollment setting makes code and
magic link sign-ins return nextStep: 'enroll_passkey' for a user with no
passkey.

Part of #338.
@Bccorb
Bccorb merged commit bc7f8ca into main Oct 6, 2026
5 checks passed
@Bccorb
Bccorb deleted the feat/enrollment-tracking branch October 6, 2026 13:30
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.

feat: track and invite passkey enrollment for imported users

1 participant