AUTH4D-332 email claims on guest upgrades and refresh

No new routes or wire shapes. Two token paths now follow the AUTH4D-265 (repository file docs/api-spec/auth4d-265.md) rule: email and email_verified: true appear only with the email scope and a provider email identity whose address auth4.dev verified. sub never changes.

Guest upgrades

POST /t/{tenantId}/guest/upgrade and the native intent’s POST /t/{tenantId}/guest/upgrade-intent/token (and their verified email step completions, auth4d-302.md (repository file docs/api-spec/auth4d-302.md)) no longer copy the linked identity’s address into the replacement session. A guest upgraded through Discord, Google, Apple, Microsoft, Epic or an SSO connection gets no email claim from that provider, even when the provider marked it verified.

  • The replacement ID token, and userinfo with its access token, carry idp: the method used for the upgrade (discord, google, sso, steam, email and so on).
  • Guest sessions are granted openid, profile and offline_access only, never email, so an upgraded session carries no address claim. The address is stored on the account; the player’s next sign-in with the email scope gets it.

Refresh

POST /oauth/token with grant_type=refresh_token reads the user’s verified email identity again instead of re-emitting the address stored with the session at sign-in, the way roles and metadata are re-read (auth4d-154.md (repository file docs/api-spec/auth4d-154.md)).

  • An address changed through the add-email intent (auth4d-149.md (repository file docs/api-spec/auth4d-149.md)) or its change path shows up in the next refreshed ID token.
  • A removed address, or one moved to another user, is left out of the next refreshed ID token.
  • The refreshed ID token and userinfo with the refreshed access token give the same email, email_verified and idp. idp stays the method of the original sign-in.
  • A failed lookup answers 400 invalid_grant before rotation, so the presented refresh token stays usable.

Tests

tests/integration/auth4d-332.email-claims.test.ts: browser upgrades through Discord, Google and SSO and a native Discord upgrade (with a refresh) emit idp and no upstream address; a refresh after the address is changed, removed, moved to another user or marked unverified carries the current address or none, and agrees with userinfo.

Source: docs/api-spec/auth4d-332.md