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