···11use crate::db::DbPool;
22use crate::middleware::auth::UserContext;
33+use crate::oauth::resolver::IdentityResolver;
34use crate::repository;
45use crate::repository::card::CardRepository;
56use crate::repository::deck::DeckRepository;
···9394 pub auth_cache: AuthCache,
9495 /// Cache of valid DPoP nonces. Nonces are single-use and expire after TTL.
9596 pub dpop_nonces: DpopNonceCache,
9797+ /// Identity resolver for AT Protocol handle/DID resolution.
9898+ pub identity_resolver: IdentityResolver,
9699}
9710098101impl AppState {
99102 pub fn new(pool: DbPool, repos: Repositories, config: AppConfig) -> SharedState {
100103 let auth_cache = Arc::new(RwLock::new(HashMap::new()));
101104 let dpop_nonces = Arc::new(RwLock::new(HashMap::new()));
105105+ let identity_resolver = IdentityResolver::new();
102106 Arc::new(Self {
103107 pool,
104108 oauth_repo: repos.oauth,
···112116 config,
113117 auth_cache,
114118 dpop_nonces,
119119+ identity_resolver,
115120 })
116121 }
117122
+6
migrations/014_2026_01_02_nullable_dpop_key.sql
···11+-- Make dpop_private_key nullable to support app password sessions
22+-- App password sessions don't use DPoP, only OAuth sessions do
33+44+ALTER TABLE oauth_tokens ALTER COLUMN dpop_private_key DROP NOT NULL;
55+66+COMMENT ON COLUMN oauth_tokens.dpop_private_key IS 'DPoP private key for OAuth sessions. NULL for app password sessions.';