AUTH4D-9 identity and user lifecycle behavior
This ticket adds identity-service primitives and no HTTP routes. A user subject is stable within one tenant. Email and Discord sign-ins resolve against the tenant-scoped provider subject; Discord uses the provider-issued Discord user ID. Provider subjects are unique per tenant, so the same Discord ID may identify separate users in separate tenants.
Email matching trims surrounding whitespace, applies Unicode NFC normalization, and lowercases the address. Plus tags and provider-specific aliases are preserved. A verified email identity is only created after the server consumes a valid email verification challenge. Discord profile email is metadata and never determines the Discord account key. Equal email values from email and Discord providers do not merge accounts.
Guest upgrades attach the verified credential to the existing guest user and preserve its user_id.
An identity already attached to another user in that tenant is rejected with the stable
identity_in_use conflict code. Disabled users are tombstoned (disabled_at) immediately and stop
resolving at sign-in. A tenant-scoped deletion job removes the D1 user and cascaded projections only
after the caller completes authoritative Durable Object session and refresh-family cleanup.
User lookup, identity lookup, and user search always require a tenant ID. Identity proofs use tenant-scoped atomic challenge keys and are consumed once; a mismatched or expired proof is rejected.
Source: docs/api-spec/auth4d-9.md