AUTH4D-260 Verified email step settings
The two settings of the verified email step (AUTH4D-258 (repository file docs/api-spec/auth4d-258.md)) can be read and changed
through the management API and are part of the tenancy policy. They are stored by migration
0037_verified_email_step.sql. This ticket only exposes them: hosted login starts asking for the
step in later tickets of the epic.
| Setting | Where | Values | Default |
|---|---|---|---|
requireVerifiedEmail |
Application (browser or game) | always, when_missing, off |
when_missing |
linkVerifiedEmail |
Project settings | never, after_code |
never |
Every application and project that existed before migration 0037 reads as the default. A database that predates 0037 also reads as the defaults; changing a value needs the migration.
Application: requireVerifiedEmail
GET /management/tenants/{tenantId}/clients/{clientId} returns it in client:
{
"client": {
"clientId": "cli_…",
"clientType": "browser",
"requireVerifiedEmail": "when_missing",
"…": "…"
},
"…": "…"
}
PATCH (or PUT) on the same path accepts it alone or with other fields, and the response’s
client carries the stored value. A PATCH that changes other fields keeps the stored value.
{ "requireVerifiedEmail": "always" }
POST /management/tenants/{tenantId}/clients accepts it for browser and game (native)
applications; without it a new application gets when_missing. Server clients never sign anyone
in, so they have no requireVerifiedEmail: creating one with the field answers
400 email_step_unsupported, and their GET omits it. The application list
(GET …/clients) does not include the field; read a single application instead.
Project: linkVerifiedEmail
GET /management/tenants/{tenantId} returns it in settings, and PATCH on the same path accepts
it alone or with other project settings:
{ "linkVerifiedEmail": "after_code" }
{
"tenant": { "tenantId": "ten_…", "…": "…" },
"role": "owner",
"settings": { "signupEnabled": true, "linkVerifiedEmail": "after_code", "…": "…" }
}
Errors
Errors use the management error shape; read the code from error.code.
| Status | error.code |
When |
|---|---|---|
400 |
validation_failed |
A value outside the table above, including null; details[0].field names it |
400 |
email_step_unsupported |
requireVerifiedEmail when creating a server client |
403 |
forbidden |
The project is unknown or belongs to someone else; both answer the same |
403 |
insufficient_role |
A viewer tries to change a setting |
400 |
validation_failed |
requireVerifiedEmail in a PATCH on a server client (AUTH4D-305) |
404 |
not_found |
The application is not in this project, including another project’s application |
Changing either setting needs the client:write (application) or tenant:write (project) token
scope and an owner or admin role, as for the other sign-in settings.
Tenancy policy
validateApplicationPolicy in packages/tenancy returns a ResolvedApplicationPolicy: the
application’s client.requireVerifiedEmail and the project’s linkVerifiedEmail, both with their
defaults filled in. The repository’s resolveAuthenticationApplication reads both from D1. For the
hosted-login gate, loadEmailStepPolicy(db, tenantId, clientId) returns
{ requireVerifiedEmail, linkVerifiedEmail }. Every read filters on the tenant id, so a client id
from another tenant reads as the default, never as that tenant’s value.
Audit events
A change writes the existing settings-changed event in the same D1 batch, through the shared
auditEventStatement helper. Audit metadata keys may not contain email, so the settings appear
as require_verified and link_verified. A PATCH that sets the current value writes no event.
| Change | event |
metadata |
|---|---|---|
requireVerifiedEmail (PATCH) |
management.client.updated |
{"client_type": "browser", "require_verified": "always", "previous_require_verified": "when_missing"} |
requireVerifiedEmail (create) |
management.client.created |
the usual fields plus "require_verified", only when the field was supplied |
linkVerifiedEmail |
management.tenant.updated |
{"status": "active", "link_verified": "after_code", "previous_link_verified": "never"} |
actor_id is the developer, and target_type/target_id name the application or the project.
Tests
tests/integration/auth4d-260.email-step-settings.test.ts:
cd tests/integration
npx vitest run --config vitest.config.ts --testTimeout=30000 auth4d-260
Source: docs/api-spec/auth4d-260.md