···2323 public let pdsEndpoint: String
2424}
25252626+/// Client authentication method declared in the client metadata's
2727+/// `token_endpoint_auth_method`. Determines whether OAuth flows may fall back
2828+/// to unproxied requests when an auth proxy is temporarily unreachable.
2929+public enum ATProtoClientAuthMethod: Sendable {
3030+ /// Public client. Direct requests to the authorization server are valid;
3131+ /// the auth proxy (if configured) is an optional signing helper.
3232+ case none
3333+ /// Confidential client using `private_key_jwt`. The auth server requires a
3434+ /// `client_assertion` JWT on every token request, which only the auth proxy
3535+ /// can produce — direct fallback would be rejected and risks consuming the
3636+ /// single-use refresh token, so it is disabled.
3737+ case privateKeyJWT
3838+}
3939+2640/// Configuration for AT Protocol OAuth
2741public struct ATProtoOAuthConfig: Sendable {
2842 public let clientMetadataURL: String
2943 public let redirectURI: String
3044 public let scopes: [String]
3145 public let authProxyBaseURL: String?
4646+ public let clientAuthMethod: ATProtoClientAuthMethod
32473348 public init(
3449 clientMetadataURL: String,
3550 redirectURI: String,
3651 scopes: [String] = ["atproto", "transition:generic"],
3737- authProxyBaseURL: String? = nil
5252+ authProxyBaseURL: String? = nil,
5353+ clientAuthMethod: ATProtoClientAuthMethod = .none
3854 ) {
3955 self.clientMetadataURL = clientMetadataURL
4056 self.redirectURI = redirectURI
4157 self.scopes = scopes
4258 self.authProxyBaseURL = authProxyBaseURL
5959+ self.clientAuthMethod = clientAuthMethod
4360 }
4461}
4562···470487 refreshedLogin = try await refreshProvider(login, appCredentials, proxyResponseProvider)
471488 usedAuthProxy = true
472489 } catch {
473473- guard shouldRetryWithoutAuthProxy(after: error) else {
490490+ // A confidential client cannot authenticate directly — the auth
491491+ // server rejects refreshes without a `client_assertion`, and a
492492+ // rejected refresh may still consume the single-use token.
493493+ // Surface the proxy error so the caller can retry later.
494494+ guard config.clientAuthMethod != .privateKeyJWT,
495495+ shouldRetryWithoutAuthProxy(after: error) else {
474496 throw error
475497 }
476498···601623 usedAuthProxy: prefersAuthProxy
602624 )
603625 } catch {
604604- if let fallbackAuthenticator, shouldRetryWithoutAuthProxy(after: error) {
626626+ if let fallbackAuthenticator,
627627+ config.clientAuthMethod != .privateKeyJWT,
628628+ shouldRetryWithoutAuthProxy(after: error) {
605629 return AuthenticationAttemptResult(
606630 login: try await fallbackAuthenticator.authenticate(),
607631 usedAuthProxy: false
···612636 }
613637 }
614638615615- if let fallbackAuthenticator, shouldRetryWithoutAuthProxy(after: error) {
639639+ if let fallbackAuthenticator,
640640+ config.clientAuthMethod != .privateKeyJWT,
641641+ shouldRetryWithoutAuthProxy(after: error) {
616642 return AuthenticationAttemptResult(
617643 login: try await fallbackAuthenticator.authenticate(),
618644 usedAuthProxy: false