Skip to content

feat: add lazy registration support#328

Open
lukpod1 wants to merge 1 commit intoanomalyco:masterfrom
lukpod1:feat/lazy-registration
Open

feat: add lazy registration support#328
lukpod1 wants to merge 1 commit intoanomalyco:masterfrom
lukpod1:feat/lazy-registration

Conversation

@lukpod1
Copy link
Copy Markdown

@lukpod1 lukpod1 commented Mar 31, 2026

Summary

Issue#327

Adds a persistence.registration option to the issuer that lets providers defer user persistence until the authorization code is successfully exchanged for tokens, instead of committing during the success() callback.

Default behavior unchanged — The default is "immediate", which preserves the current behavior exactly. No existing provider or issuer configuration is affected.

Problem being solved

In the current flow, the user record is written to storage before token exchange completes. Any failure between those two steps (application bug, network error, crash) leaves an "orphaned record" that blocks future registration attempts until the TTL expires.

Concrete case that motivated this PR: a bug in my application code during the auth flow caused the exchange step to fail. Because the user had already been persisted, fixing the bug and retrying hit email already taken — with no recovery path other than waiting for TTL expiry.

How it works

New issuer option

issuer({
  persistence: { registration: "lazy" }, // default: "immediate"
  providers: { password: PasswordProvider({ ... }) },
})

New provider hook

Providers can implement an optional finalize() hook called at token exchange time. The provider signals intent by passing a commit payload through success():

// inside provider success callback
return ctx.success(c, { email }, { commit: { kind: "password-register", email, password } })

// finalize() — called at /token exchange when lazy mode is active
// Must be idempotent: retries with the same code will call finalize() again
async finalize(input) {
  if (input.data?.kind !== "password-register") return
  const email = input.data.email?.toString()?.toLowerCase()
  const password = input.data.password
  if (!email || !password) return

  // Idempotent guard — if a previous retry already committed, skip
  const existing = await Storage.get(input.storage, ["email", email, "password"])
  if (existing) return

  await Storage.set(input.storage, ["email", email, "password"], password)
}

Error handling

If finalize() throws, the issuer returns server_error without consuming the authorization code. The client can retry the exchange with the same code.

Files changed

File Change What
src/issuer.ts modified Add persistence.registration to IssuerInput; call finalize() at token exchange; store provider name and commit in code state
src/provider/provider.ts modified Add optional finalize() to Provider interface; add commit? to ProviderOptions.success(); export ProviderFinalizeInput
src/provider/password.ts modified Implement finalize() in PasswordProvider; defer Storage.set from success() to finalize()
test/issuer.test.ts modified Tests for immediate mode (regression), lazy mode happy path, finalize() failure, and retry with same code
www/src/content/docs/docs/concepts/lazy-registration.mdx new Concept page with sequence diagrams for both modes
www/src/plugins/mermaid.ts new Remark plugin for rendering Mermaid diagrams in Starlight
www/src/content/docs/docs/provider/password.mdx modified Document lazy registration usage for PasswordProvider

Introduce a `persistence.registration` option on the issuer that allows
deferring provider-side persistence (e.g. saving a new password) until
the authorization code is exchanged for tokens, instead of committing
during the provider success callback.

Core changes:
- Add `persistence.registration: "immediate" | "lazy"` to IssuerInput
- Add `commit` option to provider `success()` callback for passing
  arbitrary data to be persisted later
- Add `finalize()` hook to Provider interface, called at token exchange
  time when lazy mode is active
- Implement `finalize()` in PasswordProvider to defer password storage
  writes until code exchange
- Store provider name and commit payload in authorization code state
- Add comprehensive tests for both immediate and lazy registration flows

Documentation:
- Add lazy-registration.mdx concept page with two Mermaid sequence
  diagrams illustrating the authorize and token exchange phases
- Add custom remark plugin (www/src/plugins/mermaid.ts) for rendering
  Mermaid diagrams via CDN in Starlight
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