skyauth 0.3.4

High-assurance, formally verified OAuth 2.1 and RFC 9449 DPoP authentication engine for the AT Protocol (Bluesky)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
//! Strongly-typed error definitions for `skyauth`.
//!
//! This module provides the central [`AtprotoOAuthError`] hierarchy along with
//! specialized error types for cryptography, DPoP (RFC 9449), PKCE (RFC 7636),
//! and session/token validation.

use thiserror::Error;

/// Root error type encompassing all failure modes across the `skyauth` library.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum AtprotoOAuthError {
    /// Low-level cryptographic primitive failure.
    #[error("Cryptographic error: {0}")]
    Crypto(#[from] CryptoError),

    /// RFC 9449 Demonstrating Proof-of-Possession (DPoP) failure.
    #[error("DPoP error: {0}")]
    DPoP(#[from] DPoPError),

    /// RFC 7636 Proof Key for Code Exchange (PKCE) failure.
    #[error("PKCE error: {0}")]
    Pkce(#[from] PkceError),

    /// Token, session, or authentication scheme validation failure.
    #[error("Token error: {0}")]
    Token(#[from] TokenError),

    /// RFC 9126 Pushed Authorization Requests (PAR) failure.
    #[error("PAR error: {0}")]
    Par(#[from] ParError),

    /// Server-Side Request Forgery (SSRF) security filter violation.
    #[error("SSRF error: {0}")]
    Ssrf(#[from] SsrfError),

    /// Decentralized identity and handle resolution error.
    #[error("Identity error: {0}")]
    Identity(#[from] IdentityError),

    /// OAuth 2.0 / RFC 8414 / RFC 9728 discovery error.
    #[error("Discovery error: {0}")]
    Discovery(#[from] DiscoveryError),

    /// State storage and persistence error.
    #[error("Store error: {0}")]
    Store(#[from] StoreError),

    /// Framework integration and extractor error.
    #[error("Integration error: {0}")]
    Integration(#[from] IntegrationError),
}

/// Errors originating from cryptographic operations and primitive transformations.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum CryptoError {
    /// Failure during ECDSA P-256 signature generation.
    #[error("ECDSA P-256 signature generation error: {0}")]
    EcdsaSign(String),

    /// Failure during ECDSA P-256 signature verification.
    #[error("ECDSA P-256 signature verification failed: {0}")]
    EcdsaVerify(String),

    /// The provided key data or PEM/DER encoding is invalid.
    #[error("Invalid cryptographic key: {0}")]
    InvalidKey(String),

    /// The elliptic curve point coordinates are invalid or not on curve NIST P-256.
    #[error("Invalid elliptic curve point: {0}")]
    InvalidPoint(String),

    /// Base64 decoding failed due to malformed characters or padding.
    #[error("Base64 decode error: {0}")]
    Base64Decode(String),

    /// JSON serialization or deserialization failed.
    #[error("JSON serialization/deserialization error: {0}")]
    Json(String),

    /// The cryptographic random number generator failed.
    #[error("Random number generator error: {0}")]
    Rng(String),

    /// HMAC key initialization or digest computation failed.
    #[error("HMAC error: {0}")]
    Hmac(String),

    /// PEM certificate/key decoding error.
    #[error("PEM decoding error: {0}")]
    Pem(String),

    /// Authenticated encryption (sealing) failed.
    #[error("Authenticated encryption failed: {0}")]
    Seal(String),

    /// Authenticated decryption (opening) failed, typically due to a tag mismatch or wrong key.
    #[error("Authenticated decryption failed: {0}")]
    Open(String),

    /// A sealed envelope was structurally invalid (e.g. truncated).
    #[error("Invalid sealed envelope: {0}")]
    InvalidEnvelope(String),

    /// Decrypted plaintext was not valid UTF-8.
    #[error("Decrypted payload is not valid UTF-8: {0}")]
    Utf8(String),
}

/// Errors arising from RFC 9449 DPoP proof generation, serialization, or verification.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum DPoPError {
    /// Malformed compact JWT structure (must contain exactly three period-separated parts).
    #[error("Malformed JWT structure: {0}")]
    MalformedJwt(String),

    /// Invalid JOSE header `typ` parameter (RFC 9449 § 4.2 requires `dpop+jwt`).
    #[error("Invalid JOSE header typ parameter: expected 'dpop+jwt', got '{0}'")]
    InvalidHeaderTyp(String),

    /// Unsupported JOSE header `alg` algorithm (must be `ES256`).
    #[error("Unsupported JOSE algorithm: expected 'ES256', got '{0}'")]
    UnsupportedAlgorithm(String),

    /// The JOSE header is missing the mandatory public JWK parameter.
    #[error("Missing JWK in JOSE header")]
    MissingJwk,

    /// The JWK in the JOSE header is invalid or contains unexpected parameters.
    #[error("Invalid JWK parameter: {0}")]
    InvalidJwk(String),

    /// Security violation: The JWK contains private key coordinates (RFC 9449 § 4.3 item 7).
    #[error("Security violation: JWK must not contain private key parameters")]
    PrivateKeyInJwk,

    /// A required claim was absent from the DPoP payload.
    #[error("Missing required DPoP claim: {0}")]
    MissingClaim(&'static str),

    /// The `jti` claim exceeds the maximum admissible length
    /// ([`crate::dpop::MAX_JTI_LENGTH`]). Unbounded `jti` values would become
    /// replay-cache keys, enabling memory-amplification attacks on the verifier
    /// (independent review finding; bounded fail-closed).
    #[error("DPoP jti claim exceeds maximum length of {max} bytes (got {actual})")]
    JtiTooLong {
        /// The maximum permitted `jti` length in bytes.
        max: usize,
        /// The actual `jti` length in bytes.
        actual: usize,
    },

    /// The HTTP method in the `htm` claim does not match the actual HTTP request method.
    #[error("HTTP method mismatch: expected '{expected}', got '{actual}'")]
    MethodMismatch {
        /// The expected HTTP method.
        expected: String,
        /// The actual HTTP method found in the claim.
        actual: String,
    },

    /// The HTTP target URI in the `htu` claim does not match the actual target URI.
    #[error("HTTP URI mismatch: expected '{expected}', got '{actual}'")]
    UriMismatch {
        /// The expected normalized URI.
        expected: String,
        /// The actual normalized URI found in the claim.
        actual: String,
    },

    /// The target URI cannot be parsed or normalized according to RFC 3986 / RFC 9449.
    #[error("Invalid URI: {0}")]
    InvalidUri(String),

    /// The DPoP proof has expired according to its `exp` claim.
    #[error("DPoP proof expired: exp {exp} < now {now}")]
    ExpiredProof {
        /// Expiration timestamp in seconds since epoch.
        exp: u64,
        /// Current timestamp in seconds since epoch.
        now: u64,
    },

    /// The DPoP proof `iat` claim is too far in the future (exceeds clock skew allowance).
    #[error("DPoP proof creation time in future: iat {iat} > now {now} (+{leeway}s leeway)")]
    FutureProof {
        /// Creation timestamp in seconds since epoch.
        iat: u64,
        /// Current timestamp in seconds since epoch.
        now: u64,
        /// Allowed clock skew leeway in seconds.
        leeway: u64,
    },

    /// The proof age exceeds the maximum allowed age limit.
    #[error("DPoP proof too old: iat {iat} older than maximum age {max_age_secs}s at now {now}")]
    ProofTooOld {
        /// Creation timestamp in seconds since epoch.
        iat: u64,
        /// Current timestamp in seconds since epoch.
        now: u64,
        /// Maximum permitted proof age in seconds.
        max_age_secs: u64,
    },

    /// The `nonce` claim does not match the server-issued challenge nonce.
    #[error("DPoP nonce mismatch: expected '{expected}', got '{actual}'")]
    NonceMismatch {
        /// The expected nonce issued by the server.
        expected: String,
        /// The actual nonce provided in the proof.
        actual: String,
    },

    /// The server requires a DPoP nonce, but none was supplied in the proof.
    #[error("Missing server-required DPoP nonce")]
    MissingNonce,

    /// A DPoP-authenticated response was accepted without the `DPoP-Nonce`
    /// header that the ATProto OAuth profile mandates on every such response
    /// (review H2). The client refuses to continue with a server that
    /// violates the nonce contract, since nonce-less responses defeat replay
    /// protection for subsequent requests.
    #[error("DPoP-authenticated response missing mandatory DPoP-Nonce header (ATProto profile violation)")]
    ResponseMissingDpopNonce,

    /// The access token hash (`ath`) claim does not match the SHA-256 hash of the presented access token.
    #[error("Access token hash (ath) mismatch: expected '{expected}', got '{actual}'")]
    AthMismatch {
        /// The expected access token hash.
        expected: String,
        /// The actual access token hash in the proof.
        actual: String,
    },

    /// An access token was presented against a protected resource without an `ath` claim in the DPoP proof.
    #[error("Missing access token hash (ath) claim for protected resource access")]
    MissingAth,

    /// The cryptographic ECDSA P-256 signature on the DPoP proof is invalid.
    #[error("Cryptographic signature verification failed")]
    SignatureVerificationFailed,

    /// A duplicate `jti` token identifier was presented, indicating a potential replay attack.
    #[error("DPoP proof replay detected: jti '{jti}' already consumed")]
    ReplayDetected {
        /// The duplicated JWT unique identifier.
        jti: String,
    },

    /// Automatic nonce retry loop exceeded the maximum allowed attempts (1 retry).
    #[error("DPoP nonce retry limit exceeded")]
    NonceRetryLimitExceeded,

    /// The DPoP replay cache has reached capacity with live (unexpired) proofs.
    ///
    /// This is a server-side resource-exhaustion condition, not a defective client
    /// proof; callers should map it to an HTTP 503-class response, not 401.
    #[error(
        "DPoP replay cache capacity saturated with active proofs (server-side resource exhaustion)"
    )]
    ReplayCacheSaturated,

    /// The DPoP server-nonce cache has reached capacity with live (unexpired) nonces.
    ///
    /// Like [`DPoPError::ReplayCacheSaturated`], this is a server-side resource-
    /// exhaustion condition; callers should map it to an HTTP 503-class response.
    #[error(
        "DPoP nonce cache capacity saturated with active nonces (server-side resource exhaustion)"
    )]
    NonceCacheSaturated,

    /// JSON or byte serialization failed.
    #[error("Serialization error: {0}")]
    Serialization(String),

    /// System clock error or excessive clock skew.
    #[error("Clock skew error: {0}")]
    ClockSkew(String),

    /// Underlying cryptographic primitive error.
    #[error("Crypto error: {0}")]
    Crypto(#[from] CryptoError),
}

/// Errors originating from RFC 7636 PKCE code verifier or challenge generation/verification.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum PkceError {
    /// The code verifier length is out of the RFC 7636 range (43 to 128 characters).
    #[error("Invalid code_verifier length: {len} (must be between {min} and {max} characters)")]
    InvalidVerifierLength {
        /// Provided verifier character length.
        len: usize,
        /// Minimum permitted length (43).
        min: usize,
        /// Maximum permitted length (128).
        max: usize,
    },

    /// The code verifier contains an illegal character outside `[A-Za-z0-9-._~]`.
    #[error("Invalid character '{char}' at position {position} in code_verifier")]
    InvalidVerifierCharacter {
        /// The forbidden character encountered.
        char: char,
        /// Zero-based byte index where the character was found.
        position: usize,
    },

    /// The code challenge length is invalid (RFC 7636 S256 requires 43 characters).
    #[error("Invalid code_challenge length: {len} (must be 43 characters)")]
    InvalidChallengeLength {
        /// Provided challenge character length.
        len: usize,
    },

    /// An unsupported transformation method was requested (only `S256` is permitted).
    #[error("Unsupported code_challenge_method: expected 'S256', got '{0}'")]
    UnsupportedMethod(String),

    /// The code verifier does not match the expected code challenge.
    #[error("PKCE code challenge verification failed")]
    ChallengeMismatch,

    /// Underlying cryptographic primitive error.
    #[error("Crypto error: {0}")]
    Crypto(#[from] CryptoError),
}

/// Errors related to access tokens, refresh tokens, and session credentials.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum TokenError {
    /// The HTTP Authorization header is missing.
    #[error("Missing Authorization header")]
    MissingHeader,

    /// The HTTP Authorization scheme is invalid (must be `DPoP` or `Bearer`).
    #[error("Invalid authentication scheme: expected '{expected}', got '{actual}'")]
    InvalidScheme {
        /// The expected scheme.
        expected: String,
        /// The actual scheme provided.
        actual: String,
    },

    /// The token has expired.
    #[error("Token expired: exp {exp} < now {now}")]
    Expired {
        /// Expiration timestamp in seconds since epoch.
        exp: u64,
        /// Current timestamp in seconds since epoch.
        now: u64,
    },

    /// The token is not yet valid (`nbf` claim in future).
    #[error("Token not yet valid: nbf {nbf} > now {now}")]
    NotYetValid {
        /// Not-before timestamp in seconds since epoch.
        nbf: u64,
        /// Current timestamp in seconds since epoch.
        now: u64,
    },

    /// The token audience does not match the expected client ID or resource server.
    #[error("Audience mismatch: expected '{expected}', got '{actual}'")]
    AudienceMismatch {
        /// Expected audience string.
        expected: String,
        /// Actual audience found in token.
        actual: String,
    },

    /// The token issuer does not match the expected authorization server.
    #[error("Issuer mismatch: expected '{expected}', got '{actual}'")]
    IssuerMismatch {
        /// Expected issuer string.
        expected: String,
        /// Actual issuer found in token.
        actual: String,
    },

    /// The cryptographic signature on the token is invalid.
    #[error("Invalid token signature")]
    InvalidSignature,

    /// The access token `cnf.jkt` binding does not match the presented DPoP key thumbprint.
    #[error(
        "DPoP key thumbprint mismatch: token cnf.jkt '{expected_jkt}' does not match proof jkt '{actual_jkt}'"
    )]
    CnfThumbprintMismatch {
        /// Expected JWK thumbprint declared in token `cnf.jkt`.
        expected_jkt: String,
        /// Actual JWK thumbprint computed from the DPoP proof public key.
        actual_jkt: String,
    },

    /// The access token is missing a required audience (`aud`) claim.
    #[error("Missing required audience (aud) claim in access token")]
    MissingAudience,

    /// The access token is missing a required issuer (`iss`) claim.
    #[error("Missing required issuer (iss) claim in access token")]
    MissingIssuer,

    /// The token is missing the required Decentralized Identifier (`did`) subject.
    #[error("Missing or invalid subject/issuer DID")]
    MissingDid,

    /// The token format or claims payload is malformed.
    #[error("Malformed token: {0}")]
    MalformedToken(String),

    /// The XRPC NSID fails ATProto NSID grammar validation.
    #[error(
        "Invalid NSID '{0}': must be a reverse-DNS, dot-separated identifier of at least three segments (total <=317 chars, each segment <=63 chars, ASCII alphanumerics and internal hyphens only, no leading/trailing hyphens, first segment starting with a letter, final name segment letters and digits only with no leading digit)"
    )]
    InvalidNsid(String),

    /// The configured authorization state TTL is not a whole number of seconds.
    #[error(
        "Invalid state TTL {0:?}: must be a whole number of seconds (sub-second TTLs cannot be represented in StoredStateEntry)"
    )]
    InvalidStateTtl(std::time::Duration),

    /// Invalid token_type (must be case-insensitively "DPoP").
    #[error("Invalid token_type: expected 'DPoP', got '{0}'")]
    InvalidTokenType(String),

    /// A required field was missing from the token response.
    #[error("Missing required token response field: {0}")]
    MissingField(&'static str),

    /// The subject DID does not match the expected DID.
    #[error("Subject DID mismatch: expected '{expected}', got '{actual}'")]
    SubMismatch {
        /// Expected DID subject.
        expected: String,
        /// Actual DID subject in token response.
        actual: String,
    },

    /// The token response is missing the mandatory `atproto` scope.
    #[error("Token scope '{0}' is missing mandatory 'atproto' scope")]
    MissingAtprotoScope(String),

    /// A refresh response granted a scope exceeding the original grant
    /// (RFC 6749 § 6: a refresh grant MUST NOT exceed the original). The
    /// client refuses to silently accumulate privileges; persistence of the
    /// returned scope is rejected with this variant.
    #[error("Refresh attempted scope expansion from '{granted}' to '{requested}'")]
    ScopeExpansion {
        /// The scope originally granted to the session.
        granted: String,
        /// The scope requested in the refresh response.
        requested: String,
    },

    /// The token endpoint rejected the request.
    #[error("Token request failed with HTTP status {status}: {error} ({description:?})")]
    RequestFailed {
        /// HTTP status code.
        status: u16,
        /// Error code returned by server (e.g. `invalid_grant`).
        error: String,
        /// Optional descriptive explanation.
        description: Option<String>,
    },

    /// The session does not have a refresh token to perform rotation.
    #[error("Session is missing a refresh token for rotation")]
    MissingRefreshToken,

    /// The callback state parameter is invalid or missing.
    #[error("Invalid or missing OAuth state parameter: {0}")]
    InvalidState(String),

    /// The OAuth state entry has expired.
    #[error("OAuth state entry has expired")]
    StateExpired,

    /// The callback query is missing the mandatory RFC 9207 `iss` issuer parameter.
    #[error("Callback query is missing mandatory RFC 9207 'iss' issuer parameter")]
    MissingCallbackIssuer,

    /// The token response is missing the mandatory `scope` field.
    #[error("Token response is missing mandatory 'scope' field")]
    MissingScope,

    /// HTTP error during token exchange or refresh.
    #[error("HTTP error during token operation: {0}")]
    Http(String),

    /// JSON error during token operation.
    #[error("JSON error during token operation: {0}")]
    Json(String),

    /// DPoP error during token operation.
    #[error("DPoP error during token operation: {0}")]
    DPoP(#[from] DPoPError),

    /// SSRF error during token operation.
    #[error("SSRF error during token operation: {0}")]
    Ssrf(#[from] SsrfError),

    /// Underlying cryptographic primitive error.
    #[error("Crypto error: {0}")]
    Crypto(#[from] CryptoError),
}

/// Errors originating from RFC 9126 Pushed Authorization Requests (PAR).
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParError {
    /// The authorization server rejected the PAR request with an HTTP error.
    #[error("PAR request failed with HTTP status {status}: {error} ({description:?})")]
    RequestFailed {
        /// HTTP status code.
        status: u16,
        /// Error code returned by server (e.g. `invalid_request`).
        error: String,
        /// Optional descriptive error explanation.
        description: Option<String>,
    },

    /// The PAR response is missing a required parameter.
    #[error("Missing required PAR response field: {0}")]
    MissingField(&'static str),

    /// The returned `request_uri` is invalid or malformed.
    #[error("Invalid request_uri returned from PAR: '{0}'")]
    InvalidRequestUri(String),

    /// The PAR endpoint URL is invalid or malformed.
    #[error("Invalid PAR endpoint URL: {0}")]
    InvalidEndpoint(String),

    /// Low-level HTTP transport or connection error.
    #[error("HTTP error during PAR: {0}")]
    Http(String),

    /// JSON serialization or deserialization error.
    #[error("JSON error during PAR: {0}")]
    Json(String),

    /// RFC 9449 DPoP proof error during PAR.
    #[error("DPoP error during PAR: {0}")]
    DPoP(#[from] DPoPError),

    /// SSRF security filter violation during PAR.
    #[error("SSRF violation during PAR: {0}")]
    Ssrf(#[from] SsrfError),
}

/// Errors arising from Server-Side Request Forgery (SSRF) and IP boundary filtering.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum SsrfError {
    /// An outbound request targeted a forbidden/restricted IP address.
    #[error("SSRF violation: blocked attempt to connect to restricted IP {0}")]
    BlockedIp(String),

    /// An outbound request targeted a forbidden cloud metadata or internal hostname.
    #[error("SSRF violation: blocked attempt to connect to restricted host '{0}'")]
    BlockedHost(String),

    /// An insecure URL scheme was encountered (HTTPS is required in production).
    #[error("SSRF violation: insecure URL scheme in '{0}' (HTTPS required)")]
    InsecureScheme(String),

    /// DNS resolution failed for the target hostname.
    #[error("DNS resolution failed: {0}")]
    DnsResolutionFailed(String),

    /// The provided URL is malformed or invalid.
    #[error("Invalid URL: {0}")]
    InvalidUrl(String),

    /// Redirect chain exceeded the maximum allowed depth limit.
    #[error("Too many redirects: exceeded maximum permitted redirect limit")]
    TooManyRedirects,

    /// Response body exceeded the maximum permitted size limit.
    #[error(
        "Response body too large: max {max_bytes} bytes allowed, received {actual_bytes} bytes"
    )]
    ResponseTooLarge {
        /// Maximum permitted byte size.
        max_bytes: usize,
        /// Actual received byte size.
        actual_bytes: usize,
    },

    /// HTTP response returned an unsuccessful status code.
    #[error("HTTP status {0}: {1}")]
    HttpStatus(u16, String),

    /// Low-level HTTP transport or connection error.
    #[error("HTTP transport error: {0}")]
    Http(String),

    /// I/O or network socket error.
    #[error("I/O error: {0}")]
    Io(String),

    /// JSON serialization or deserialization error.
    #[error("JSON error: {0}")]
    Json(String),
}

/// Errors originating from decentralized identity (DID) and handle resolution.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum IdentityError {
    /// Handle does not conform to ATProto syntax requirements.
    #[error("Invalid handle syntax: {0}")]
    InvalidHandleSyntax(String),

    /// Handle uses a disallowed or restricted top-level domain (TLD).
    #[error("Disallowed handle TLD: {0}")]
    DisallowedHandleTld(String),

    /// Handle resolution failed via all configured mechanisms.
    #[error("Handle resolution failed for '{0}'")]
    HandleResolutionFailed(String),

    /// DNS TXT resolution returned multiple conflicting DID records.
    #[error("Ambiguous handle resolution: multiple conflicting DIDs found for '{0}'")]
    AmbiguousHandleResolution(String),

    /// Resolved DID document `alsoKnownAs` does not match the claimed handle.
    #[error(
        "Bidirectional verification failed: handle '{0}' does not match DID document alsoKnownAs"
    )]
    HandleDidMismatch(String),

    /// The provided DID string is malformed or has an invalid syntax.
    #[error("Invalid DID syntax: {0}")]
    InvalidDidSyntax(String),

    /// The DID method is unsupported (only `did:plc` and `did:web` are supported).
    #[error("Unsupported DID method: {0}")]
    UnsupportedDidMethod(String),

    /// The queried DID was not found in the directory or host.
    #[error("DID not found: {0}")]
    DidNotFound(String),

    /// The returned DID document JSON is malformed or missing mandatory fields.
    #[error("Malformed DID document: {0}")]
    MalformedDidDocument(String),

    /// The DID document `id` field does not match the queried DID.
    #[error("DID document ID mismatch: expected '{expected}', found '{actual}'")]
    DidDocumentIdMismatch {
        /// The expected DID identifier.
        expected: String,
        /// The actual DID identifier in the document.
        actual: String,
    },

    /// The DID document is missing the mandatory `#atproto_pds` service endpoint.
    #[error("DID document missing '#atproto_pds' service endpoint for DID '{0}'")]
    MissingPdsEndpoint(String),

    /// The `#atproto_pds` service endpoint URL is invalid or malformed.
    #[error("Invalid PDS endpoint URL: {0}")]
    InvalidPdsEndpoint(String),

    /// SSRF security violation during identity resolution.
    #[error("SSRF violation during identity resolution: {0}")]
    Ssrf(#[from] SsrfError),

    /// HTTP error during identity resolution.
    #[error("HTTP error during identity resolution: {0}")]
    Http(String),

    /// JSON error during identity resolution.
    #[error("JSON error during identity resolution: {0}")]
    Json(String),

    /// DNS resolution error during handle resolution.
    #[error("DNS error during handle resolution: {0}")]
    Dns(String),
}

/// Errors originating from RFC 8414 and RFC 9728 OAuth discovery.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum DiscoveryError {
    /// Protected Resource Metadata discovery failed (RFC 9728).
    #[error("Protected resource metadata discovery failed: {0}")]
    ProtectedResourceDiscoveryFailed(String),

    /// Protected Resource Metadata does not list any authorization servers.
    #[error("No authorization servers listed in protected resource metadata for PDS '{0}'")]
    MissingAuthorizationServers(String),

    /// Protected Resource Metadata listed multiple authorization servers (ATProto requires exactly one).
    #[error(
        "Protected resource metadata declared {0} authorization servers; ATProto requires exactly one"
    )]
    MultipleAuthorizationServers(usize),

    /// Authorization server URL in Protected Resource Metadata is not a valid origin.
    #[error("Authorization server URL '{0}' is not a valid origin")]
    InvalidAuthorizationServerUrl(String),

    /// Protected Resource Metadata `resource` does not match the queried PDS origin.
    #[error(
        "Protected resource metadata resource '{actual}' does not match expected PDS origin '{expected}'"
    )]
    ResourceMismatch {
        /// Expected PDS endpoint/origin.
        expected: String,
        /// Actual resource declared in metadata.
        actual: String,
    },

    /// Authorization Server Metadata discovery failed (RFC 8414).
    #[error("Authorization server metadata discovery failed: {0}")]
    AuthServerDiscoveryFailed(String),

    /// Authorization Server Metadata `issuer` does not match the expected origin.
    #[error("Authorization server issuer mismatch: expected '{expected}', found '{actual}'")]
    IssuerMismatch {
        /// The expected authorization server origin.
        expected: String,
        /// The actual issuer string declared in the metadata.
        actual: String,
    },

    /// Authorization server is missing required `ES256` DPoP signing algorithm support.
    #[error("Authorization server '{0}' is missing required ES256 DPoP algorithm support")]
    MissingDpopAlgorithm(String),

    /// Authorization server is missing required `S256` PKCE code challenge method support.
    #[error("Authorization server '{0}' is missing required S256 PKCE method support")]
    MissingPkceMethod(String),

    /// Authorization server metadata is missing the required PAR endpoint.
    #[error(
        "Authorization server '{0}' is missing required pushed_authorization_request_endpoint"
    )]
    MissingParEndpoint(String),

    /// Authorization server metadata does not mandate pushed authorization requests (`require_pushed_authorization_requests` must be true).
    #[error(
        "Authorization server '{0}' does not mandate pushed authorization requests (require_pushed_authorization_requests must be true)"
    )]
    ParNotRequired(String),

    /// Authorization server explicitly disabled RFC 9126 request_uri registration (`require_request_uri_registration` must not be false).
    #[error(
        "Authorization server '{0}' explicitly disabled require_request_uri_registration; the ATProto OAuth profile mandates it"
    )]
    MissingRequestUriRegistration(String),

    /// Authorization server metadata is missing the required 'code' response type.
    #[error("Authorization server '{0}' is missing required 'code' response type support")]
    MissingResponseType(String),

    /// Authorization server metadata is missing a required grant type (`authorization_code` or `refresh_token`).
    #[error("Authorization server '{auth_server}' is missing required '{missing}' grant type")]
    MissingGrantType {
        /// Authorization server URL.
        auth_server: String,
        /// Missing grant type name.
        missing: String,
    },

    /// Authorization server metadata is missing required token endpoint authentication methods (`none` AND `private_key_jwt`).
    #[error(
        "Authorization server '{0}' must advertise both required token endpoint authentication methods ('none' and 'private_key_jwt')"
    )]
    MissingTokenAuthMethod(String),

    /// Authorization server is missing required `ES256` token endpoint authentication signing algorithm support.
    #[error(
        "Authorization server '{0}' is missing required ES256 in token_endpoint_auth_signing_alg_values_supported"
    )]
    MissingTokenAuthSigningAlg(String),

    /// Authorization server advertised forbidden 'none' in token_endpoint_auth_signing_alg_values_supported.
    #[error(
        "Authorization server '{0}' advertised forbidden 'none' in token_endpoint_auth_signing_alg_values_supported"
    )]
    InvalidTokenAuthSigningAlg(String),

    /// Authorization server metadata is missing required `atproto` scope in `scopes_supported`.
    #[error("Authorization server '{0}' is missing required 'atproto' scope in scopes_supported")]
    MissingAtprotoScope(String),

    /// Authorization server metadata does not support RFC 9207 `iss` response parameter (`authorization_response_iss_parameter_supported` must be true).
    #[error(
        "Authorization server '{0}' does not support RFC 9207 authorization_response_iss_parameter_supported"
    )]
    MissingIssParameterSupport(String),

    /// Authorization server metadata does not support client metadata document resolution (`client_id_metadata_document_supported` must be true).
    #[error("Authorization server '{0}' does not support client_id_metadata_document_supported")]
    MissingClientMetadataSupport(String),

    /// Authorization server metadata is missing the required token endpoint.
    #[error("Authorization server '{0}' is missing required token_endpoint")]
    MissingTokenEndpoint(String),

    /// Authorization server metadata is missing the required authorization endpoint.
    #[error("Authorization server '{0}' is missing required authorization_endpoint")]
    MissingAuthorizationEndpoint(String),

    /// An endpoint URL in the metadata is invalid.
    #[error("Invalid endpoint URL in discovery metadata: {0}")]
    InvalidEndpointUrl(String),

    /// Identity resolution error during discovery.
    #[error("Identity resolution error during discovery: {0}")]
    Identity(#[from] IdentityError),

    /// SSRF security violation during discovery.
    #[error("SSRF violation during discovery: {0}")]
    Ssrf(#[from] SsrfError),

    /// HTTP error during discovery.
    #[error("HTTP error during discovery: {0}")]
    Http(String),

    /// JSON error during discovery.
    #[error("JSON error during discovery: {0}")]
    Json(String),
}

/// Errors originating from OAuth state and session storage backends.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum StoreError {
    /// The requested authorization state token has expired.
    #[error("State token has expired: '{0}'")]
    StateExpired(String),

    /// The requested authorization state token was not found (or already consumed).
    #[error("State token not found or already consumed: '{0}'")]
    StateNotFound(String),

    /// An error occurred in the underlying storage backend.
    #[error("Storage backend error: {0}")]
    Backend(String),

    /// The state store is at its admission capacity (per-shard cap); the
    /// caller should retry after pruning or use a distributed backend.
    /// Fail-closed by design (review H5/M2: unbounded pre-auth state enables
    /// memory-exhaustion floods).
    #[error("State store at admission capacity ({0} entries per shard)")]
    CapacityExceeded(usize),

    /// State serialization or deserialization failed.
    #[error("Serialization error: {0}")]
    Serialization(String),

    /// A lock acquisition or concurrency invariant violation occurred.
    #[error("Lock acquisition or concurrency error: {0}")]
    Lock(String),
}

/// Errors originating from web framework integrations and extractors.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum IntegrationError {
    /// The callback request query is missing the mandatory `code` parameter.
    #[error("Missing OAuth code parameter in callback query")]
    MissingCode,

    /// The callback request query is missing the mandatory `state` parameter.
    #[error("Missing OAuth state parameter in callback query")]
    MissingState,

    /// An OAuth error code and description returned by the authorization server.
    #[error("OAuth authorization server error: {error} ({description})")]
    OAuthError {
        /// Standard OAuth error code (e.g. `access_denied`, `invalid_request`).
        error: String,
        /// Optional human-readable error description.
        description: String,
    },

    /// The request is missing the required `Authorization` header.
    #[error("Missing or malformed Authorization header")]
    MissingAuthHeader,

    /// The `Authorization` header scheme is invalid (expected `DPoP`).
    #[error("Invalid Authorization header scheme: expected 'DPoP', got '{0}'")]
    InvalidAuthScheme(String),

    /// The request is missing the required `DPoP` proof header.
    #[error("Missing DPoP proof header")]
    MissingDPoPProofHeader,

    /// The requested authenticated session was not found or has expired.
    #[error("Session not found or expired")]
    SessionNotFound,

    /// Inbound request authentication failed.
    #[error("Authentication failed: {0}")]
    AuthFailed(String),

    /// Internal framework integration or response generation failure.
    #[error("Internal framework error: {0}")]
    Internal(String),

    /// Underlying store error.
    #[error("Store error: {0}")]
    Store(#[from] StoreError),

    /// Underlying DPoP validation error.
    #[error("DPoP error: {0}")]
    DPoP(#[from] DPoPError),

    /// Underlying token error.
    #[error("Token error: {0}")]
    Token(#[from] TokenError),
}