protocol-droid 0.3.0

A Model Context Protocol toolkit that speaks every published spec revision, with one canonical IR behind many per-revision codecs.
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
//! Completing the authorization-code exchange: trading the code for a token.
//!
//! [`begin_authorization`](super::client::begin_authorization) sends the user
//! somewhere, and [`authorize_callback`](super::callback::authorize_callback)
//! validates what comes back. Neither of those produces a token, and this is
//! the step that does.
//!
//! # The request is assembled, never supplied
//!
//! Every parameter of the token request is read off the
//! [`AuthorizedCode`] — the value the callback step produced — rather than
//! passed in by the caller. That is not tidiness. RFC 6749 §4.1.3 requires the
//! `redirect_uri` to be **identical** to the one in the authorization request,
//! and a caller that supplies it separately can supply a different one. The
//! same goes for the `code_verifier`: presenting anything other than the value
//! the `code_challenge` was derived from is a failed PKCE check, and the
//! failure is the server refusing a code the client genuinely owns.
//!
//! # The code is consumed, so it cannot be spent twice
//!
//! [`redeem`] takes the [`AuthorizedCode`] by value, and `AuthorizedCode` is
//! not [`Clone`], so one value cannot be redeemed twice. A retry after a
//! dropped connection — the case where the first attempt may already have been
//! honoured — therefore cannot silently spend the same code again.
//!
//! That is the whole of the guarantee, and it is narrower than "a code is used
//! once". A caller that runs [`authorize_callback`] twice against the same
//! [`PendingAuthorization`](super::client::PendingAuthorization) holds two
//! values and may try both; RFC 6749 §4.1.2 requires the authorization server
//! to deny the second. The type closes the accident, the server closes the
//! choice.

use std::fmt;

use crate::application::oauth::callback::AuthorizedCode;
use crate::domain::oauth::issued_token::{IssuedToken, TokenResponseError};
use crate::domain::oauth::pkce::CodeVerifier;
use crate::domain::oauth::registration::{
    ClientId, ClientSecret, RegisteredClient, TokenAuthMethod,
};
use crate::domain::oauth::resource::ResourceParameters;

/// One token request, assembled from an [`AuthorizedCode`] and a registration.
///
/// Borrowed rather than owned because every member already exists in one of
/// those two values and copying it would create a second copy of a credential
/// to keep alive for no reason.
///
/// # Examples
///
/// An implementor reads what it needs and encodes it however its transport
/// requires; the `oauth` adapter reads all of it:
///
/// ```
/// # use protocol_droid::application::oauth::redeem::{TokenEndpoint, TokenEndpointError, TokenRequest};
/// # use protocol_droid::domain::oauth::issued_token::{IssuedToken, TokenResponseError};
/// struct Transport;
///
/// impl TokenEndpoint for Transport {
///     async fn redeem(
///         &self,
///         request: TokenRequest<'_>,
///     ) -> Result<IssuedToken, TokenEndpointError> {
///         let body = format!(
///             "grant_type=authorization_code&code={}&redirect_uri={}",
///             request.code(),
///             request.redirect_uri(),
///         );
///         println!("{body}");
///         # Ok::<IssuedToken, TokenResponseError>(
///             #     IssuedToken::parse(br#"{"access_token":"at","token_type":"Bearer"}"#)?
///             # )
///         .map_err(TokenEndpointError::Refused)
///     }
/// }
/// ```
#[derive(Debug)]
pub struct TokenRequest<'a> {
    code: &'a str,
    redirect_uri: &'a str,
    verifier: &'a CodeVerifier,
    client_id: &'a ClientId,
    client_secret: Option<&'a ClientSecret>,
    resource: &'a ResourceParameters,
    auth_method: TokenAuthMethod,
    scope: Option<&'a str>,
}

impl<'a> TokenRequest<'a> {
    /// The authorization code to redeem.
    #[must_use]
    pub const fn code(&self) -> &'a str {
        self.code
    }

    /// The redirect URI, exactly as the authorization request carried it.
    #[must_use]
    pub const fn redirect_uri(&self) -> &'a str {
        self.redirect_uri
    }

    /// The PKCE verifier the `code_challenge` was derived from.
    #[must_use]
    pub const fn code_verifier(&self) -> &'a CodeVerifier {
        self.verifier
    }

    /// The client this exchange is being conducted as.
    #[must_use]
    pub const fn client_id(&self) -> &'a ClientId {
        self.client_id
    }

    /// The secret, for a confidential client only.
    ///
    /// [`None`] for a public client — one registered through a client
    /// identity metadata document, which has nowhere to keep a secret that is
    /// still secret. An adapter must **omit** the member in that case rather
    /// than sending it empty: an empty `client_secret` is a value, and RFC 6749
    /// §2.3.1 treats the presence of the member as the statement that the
    /// client is confidential.
    #[must_use]
    pub const fn client_secret(&self) -> Option<&'a ClientSecret> {
        self.client_secret
    }

    /// How the client authenticates at the token endpoint.
    #[must_use]
    pub const fn auth_method(&self) -> TokenAuthMethod {
        self.auth_method
    }

    /// The `scope` to send with the token request, if any.
    ///
    /// The authorization page lets a client that wants a refresh token add
    /// `offline_access` "to the `scope` parameter of the authorization and
    /// token requests", so the token request carries the scope the
    /// authorization request asked for.
    #[must_use]
    pub const fn scope(&self) -> Option<&'a str> {
        self.scope
    }

    /// The RFC 8707 resource indicators this grant is bound to.
    ///
    /// Repeated as one `resource` member per indicator, which is the encoding
    /// §2 of that RFC defines.
    #[must_use]
    pub const fn resource(&self) -> &'a ResourceParameters {
        self.resource
    }
}

/// Asks an authorization server to trade a code for a token.
///
/// One method, and one because redemption is a single act. An adapter does the
/// HTTP; this crate decides what to ask for, and the domain decides what came
/// back.
///
/// Not dyn compatible, and that is deliberate. Redemption happens once per
/// client rather than once per request, so the call site takes `impl
/// TokenEndpoint` and the monomorphised future costs nothing that a box would
/// not.
pub trait TokenEndpoint {
    /// Trade the authorization code for a token.
    ///
    /// # Errors
    ///
    /// [`TokenEndpointError::Refused`] when the server answered RFC 6749 §5.2,
    /// or with a body that was not a usable token. [`TokenEndpointError::Unavailable`]
    /// when the server could not be asked or its answer could not be read.
    fn redeem(
        &self,
        request: TokenRequest<'_>,
    ) -> impl Future<Output = Result<IssuedToken, TokenEndpointError>> + Send;

    /// Trade a refresh token for a new access token (RFC 6749 §6).
    ///
    /// The default refuses, so an endpoint written before this method existed
    /// keeps compiling and a caller finds out at runtime, as an
    /// [`TokenEndpointError::Unavailable`], that it cannot refresh.
    ///
    /// # Errors
    ///
    /// As [`redeem`](Self::redeem). `invalid_grant` here means the refresh
    /// token is spent or revoked, and the caller falls back to a full
    /// authorization.
    fn refresh(
        &self,
        request: RefreshRequest<'_>,
    ) -> impl Future<Output = Result<IssuedToken, TokenEndpointError>> + Send {
        let _ = request;
        core::future::ready(Err(TokenEndpointError::Unavailable(
            "this token endpoint does not support refresh tokens".to_owned(),
        )))
    }
}

/// One `grant_type=refresh_token` request.
///
/// Borrowed, like [`TokenRequest`], so a refresh token is not copied around.
#[derive(Debug)]
pub struct RefreshRequest<'a> {
    refresh_token: &'a str,
    client_id: &'a ClientId,
    client_secret: Option<&'a ClientSecret>,
    resource: &'a ResourceParameters,
    auth_method: TokenAuthMethod,
    scope: Option<&'a str>,
}

impl<'a> RefreshRequest<'a> {
    /// A refresh of `refresh_token` as `client`, for the resource the grant
    /// was bound to.
    #[must_use]
    pub const fn new(
        refresh_token: &'a str,
        client: &'a RegisteredClient,
        resource: &'a ResourceParameters,
        scope: Option<&'a str>,
    ) -> Self {
        Self {
            refresh_token,
            client_id: client.client_id(),
            client_secret: client.secret(),
            resource,
            auth_method: client.auth_method(),
            scope,
        }
    }

    /// The refresh token being redeemed.
    #[must_use]
    pub const fn refresh_token(&self) -> &'a str {
        self.refresh_token
    }

    /// The client the refresh is made as.
    #[must_use]
    pub const fn client_id(&self) -> &'a ClientId {
        self.client_id
    }

    /// The secret, for a confidential client only; omitted otherwise.
    #[must_use]
    pub const fn client_secret(&self) -> Option<&'a ClientSecret> {
        self.client_secret
    }

    /// How the client authenticates at the token endpoint.
    #[must_use]
    pub const fn auth_method(&self) -> TokenAuthMethod {
        self.auth_method
    }

    /// The RFC 8707 resource indicators the grant is bound to.
    #[must_use]
    pub const fn resource(&self) -> &'a ResourceParameters {
        self.resource
    }

    /// The scope to ask for, if narrower than the original grant.
    #[must_use]
    pub const fn scope(&self) -> Option<&'a str> {
        self.scope
    }
}

/// Why a redemption did not produce a token.
///
/// `#[error]` text never carries the code, the verifier or the secret. This is
/// the first place any of those three could reach a message somebody pastes
/// into a bug report.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TokenEndpointError {
    /// The server refused, or answered with something unusable.
    #[error(transparent)]
    Refused(#[from] TokenResponseError),

    /// The server could not be asked, or its answer could not be read.
    #[error("the token endpoint could not be reached: {0}")]
    Unavailable(String),
}

/// Why [`redeem`] did not produce a token.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RedeemError {
    /// The registration presented does not match the client that started the
    /// exchange.
    ///
    /// [`redeem`] compares rather than trusting, because the client id is a
    /// parameter the adapter is about to send: a caller that reached for the
    /// wrong registration would otherwise produce a token request naming one
    /// client with a `code_verifier` generated for another, and the server's
    /// answer would be an `invalid_grant` with nothing to explain it.
    #[error("the registration is for {presented}, but this code was issued to {expected}")]
    ClientMismatch {
        /// The client the code was issued to.
        expected: ClientId,
        /// The client the caller presented.
        presented: ClientId,
    },

    /// The endpoint refused, or could not be reached.
    #[error(transparent)]
    Endpoint(#[from] TokenEndpointError),
}

/// Trade a validated authorization code for a token.
///
/// The code is consumed. See the module docs for why that is the single-use
/// rule rather than a convention.
///
/// # Errors
///
/// [`RedeemError::ClientMismatch`] when the registration is not the client the
/// code was issued to. [`RedeemError::Endpoint`] when the endpoint refused or
/// could not be reached; that carries RFC 6749 §5.2's own error code when the
/// server named one.
///
/// # Examples
///
/// ```
/// # use protocol_droid::application::oauth::redeem::{RedeemError, redeem};
/// # use protocol_droid::domain::oauth::registration::{ClientId, RegisteredClient};
/// # async fn run(
/// #     code: protocol_droid::application::oauth::callback::AuthorizedCode,
/// #     client: RegisteredClient,
/// #     endpoint: &(impl protocol_droid::application::oauth::redeem::TokenEndpoint + Sync),
/// # ) -> Result<(), RedeemError> {
/// let issued = redeem(code, &client, endpoint).await?;
/// println!("scope granted: {:?}", issued.scope());
/// # Ok(())
/// # }
/// ```
pub async fn redeem(
    code: AuthorizedCode,
    client: &RegisteredClient,
    endpoint: &(impl TokenEndpoint + Sync),
) -> Result<IssuedToken, RedeemError> {
    redeem_with_scope(code, client, endpoint, None).await
}

/// [`redeem`], sending `scope` with the token request.
///
/// # Errors
///
/// As [`redeem`].
pub async fn redeem_with_scope(
    code: AuthorizedCode,
    client: &RegisteredClient,
    endpoint: &(impl TokenEndpoint + Sync),
    scope: Option<&str>,
) -> Result<IssuedToken, RedeemError> {
    if code.client_id() != client.client_id() {
        return Err(RedeemError::ClientMismatch {
            expected: code.client_id().clone(),
            presented: client.client_id().clone(),
        });
    }

    let request = TokenRequest {
        code: code.code(),
        // Read back off the code rather than taken from the registration: the
        // registration records where the client is willing to be redirected
        // *now*, and §4.1.3 asks for where this request was actually sent.
        redirect_uri: code.redirect_uri(),
        verifier: code.code_verifier(),
        client_id: code.client_id(),
        client_secret: client.secret(),
        resource: code.resource().token_request(),
        auth_method: client.auth_method(),
        scope,
    };

    endpoint
        .redeem(request)
        .await
        .map_err(RedeemError::Endpoint)
}

/// Rendered only so a `TokenRequest` in a log line names its fields without
/// their values.
impl fmt::Display for TokenRequest<'_> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "TokenRequest {{ grant_type: authorization_code, code: …, redirect_uri: {}, \
             code_verifier: …, client_id: {}, client_secret: {}, resource: [{}] }}",
            self.redirect_uri,
            self.client_id,
            self.client_secret.is_some(),
            self.resource.iter().collect::<Vec<_>>().join(", "),
        )
    }
}
#[cfg(test)]
mod tests {
    use std::assert_matches;
    use std::future::Future;
    use std::sync::Mutex;
    use std::task::{Context, Poll, Waker};

    use super::{RedeemError, TokenEndpoint, TokenEndpointError, TokenRequest, redeem};
    use crate::application::oauth::callback::{
        AuthorizationCallback, AuthorizedCode, authorize_callback,
    };
    use crate::application::oauth::client::{
        AuthorizationSecrets, PendingAuthorization, SecretError, State, begin_authorization,
    };
    use crate::domain::ProtocolVersion;
    use crate::domain::mcp::authorization::{AuthorizationModel, authorization_model};
    use crate::domain::mcp::resource_metadata::ResourceUri;
    use crate::domain::oauth::issued_token::{IssuedToken, TokenResponseError};
    use crate::domain::oauth::pkce::CodeVerifier;
    use crate::domain::oauth::registration::{ClientId, ClientSecret, RegisteredClient};
    use crate::domain::oauth::resource::ResourceParameters;

    const AS: &str = "https://auth.example.com";
    const CLIENT_ID: &str = "https://client.example.com/metadata.json";
    const REDIRECT: &str = "https://client.example.com/callback";
    const RESOURCE: &str = "https://api.example.com/app";
    const VERIFIER: &str = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
    const STATE: &str = "a-state-value";
    const GRANTED: &[u8] = br#"{"access_token":"at-1","token_type":"Bearer","expires_in":3600}"#;

    /// Every error in play implements `std::error::Error`, so a test can `?`
    /// through the lot and print whatever the engine refused with, rather than
    /// unwrapping the first call that happens to be fallible.
    type Outcome = Result<(), Box<dyn std::error::Error>>;

    /// Drives a future that is expected never to pend.
    ///
    /// `#[tokio::test]` is not available under `oauth`: the tokio macros come in
    /// with `http`, and pulling a runtime in to test fakes that return
    /// immediately would be a heavier dependency than the thing under test.
    /// `Waker::noop` is correct for a future that resolves on its first poll,
    /// and returning `None` on `Pending` is what makes that assumption
    /// checkable rather than merely stated.
    fn now<T>(future: impl Future<Output = T>) -> Option<T> {
        let mut future = std::pin::pin!(future);
        match future
            .as_mut()
            .poll(&mut Context::from_waker(Waker::noop()))
        {
            Poll::Ready(output) => Some(output),
            Poll::Pending => None,
        }
    }

    struct Fixed;

    impl AuthorizationSecrets for Fixed {
        fn new_state(&self) -> Result<State, SecretError> {
            Ok(State::new(STATE))
        }
        fn new_code_verifier(&self) -> Result<CodeVerifier, SecretError> {
            CodeVerifier::new(VERIFIER).map_err(|_| SecretError::Rejected)
        }
    }

    const fn model() -> AuthorizationModel {
        authorization_model(ProtocolVersion::V2026_07_28)
    }

    /// A client registered through a metadata document, so public: no secret.
    fn public_client() -> Result<RegisteredClient, Box<dyn std::error::Error>> {
        Ok(RegisteredClient::from_metadata_document(
            AS,
            ClientId::metadata_url(CLIENT_ID)?,
        ))
    }

    fn confidential_client() -> Result<RegisteredClient, Box<dyn std::error::Error>> {
        Ok(RegisteredClient::dynamic(
            AS,
            ClientId::opaque("client-1"),
            Some(ClientSecret::issued("s3cret")),
        )?)
    }

    /// A validated code for `client`, made through the real path so the values
    /// in it are the ones a live flow would carry.
    fn authorized(client: &RegisteredClient) -> Result<AuthorizedCode, Box<dyn std::error::Error>> {
        let resource = ResourceUri::new(RESOURCE)?;
        let pending: PendingAuthorization = begin_authorization(
            &model(),
            client,
            REDIRECT,
            ResourceParameters::single(&resource),
            &Fixed,
        )?;
        let callback = AuthorizationCallback::granted("the-code", STATE);
        Ok(authorize_callback(&model(), &pending, &callback)?)
    }

    /// What the fake saw, copied out so it can outlive the borrow the
    /// `TokenRequest` held.
    #[derive(Debug)]
    struct Recorded {
        code: String,
        redirect_uri: String,
        verifier: String,
        client_id: String,
        client_secret: Option<String>,
        resources: Vec<String>,
    }

    enum Outcome_ {
        Granted,
        Refused(TokenResponseError),
        Unreachable,
    }

    struct Recording {
        recorded: Mutex<Option<Recorded>>,
        outcome: Outcome_,
    }

    impl Recording {
        fn granting() -> Self {
            Self {
                recorded: Mutex::new(None),
                outcome: Outcome_::Granted,
            }
        }

        fn refusing(error: TokenResponseError) -> Self {
            Self {
                recorded: Mutex::new(None),
                outcome: Outcome_::Refused(error),
            }
        }

        fn unreachable() -> Self {
            Self {
                recorded: Mutex::new(None),
                outcome: Outcome_::Unreachable,
            }
        }

        fn seen(&self) -> Option<Recorded> {
            self.recorded.lock().ok().and_then(|mut guard| guard.take())
        }
    }

    impl TokenEndpoint for Recording {
        // Not `async`: this fake resolves on its first poll, so an `async fn`
        // here would be an async block with no await point in it — which is a
        // `ready` wearing a hat.
        fn redeem(
            &self,
            request: TokenRequest<'_>,
        ) -> impl Future<Output = Result<IssuedToken, TokenEndpointError>> + Send {
            let recorded = Recorded {
                code: request.code().to_owned(),
                redirect_uri: request.redirect_uri().to_owned(),
                verifier: request.code_verifier().as_str().to_owned(),
                client_id: request.client_id().as_str().to_owned(),
                client_secret: request
                    .client_secret()
                    .map(|secret| secret.expose().to_owned()),
                resources: request.resource().iter().map(str::to_owned).collect(),
            };
            if let Ok(mut guard) = self.recorded.lock() {
                *guard = Some(recorded);
            }

            std::future::ready(match &self.outcome {
                Outcome_::Granted => {
                    IssuedToken::parse(GRANTED).map_err(TokenEndpointError::Refused)
                }
                Outcome_::Refused(error) => Err(TokenEndpointError::Refused(error.clone())),
                Outcome_::Unreachable => Err(TokenEndpointError::Unavailable(
                    "no route to host".to_owned(),
                )),
            })
        }
    }

    /// A redeemed grant is the thing every downstream caller wants, so the
    /// happy path gets a test that says so rather than one that only checks it
    /// did not error.
    #[test]
    fn a_valid_code_becomes_a_token() -> Outcome {
        let client = public_client()?;
        let endpoint = Recording::granting();

        let issued =
            now(redeem(authorized(&client)?, &client, &endpoint)).ok_or("the future pended")??;

        assert_eq!(issued.access_token().as_str(), "at-1");
        Ok(())
    }

    /// §4.1.3 asks for a `redirect_uri` *identical* to the authorization
    /// request's, and the verifier the challenge was derived from. Both come
    /// off the code rather than off the caller for exactly that reason.
    #[test]
    fn the_request_is_assembled_from_the_code_not_the_caller() -> Outcome {
        let client = public_client()?;
        let endpoint = Recording::granting();

        now(redeem(authorized(&client)?, &client, &endpoint)).ok_or("the future pended")??;

        let seen = endpoint.seen().ok_or("the endpoint was never asked")?;
        assert_eq!(seen.code, "the-code");
        assert_eq!(seen.redirect_uri, REDIRECT);
        assert_eq!(seen.verifier, VERIFIER);
        assert_eq!(seen.client_id, CLIENT_ID);
        Ok(())
    }

    /// RFC 8707 §2. Dropping the indicator here would let the server mint a
    /// token for an audience the client never asked for.
    #[test]
    fn the_resource_binding_is_carried_to_the_token_request() -> Outcome {
        let client = public_client()?;
        let endpoint = Recording::granting();

        now(redeem(authorized(&client)?, &client, &endpoint)).ok_or("the future pended")??;

        let seen = endpoint.seen().ok_or("the endpoint was never asked")?;
        assert_eq!(seen.resources, vec![RESOURCE.to_owned()]);
        Ok(())
    }

    /// A public client has no secret, and §2.3.1 reads the member's *presence*
    /// as a claim of confidentiality — so the adapter must be able to omit it.
    #[test]
    fn a_public_client_sends_no_secret() -> Outcome {
        let client = public_client()?;
        let endpoint = Recording::granting();

        now(redeem(authorized(&client)?, &client, &endpoint)).ok_or("the future pended")??;

        let seen = endpoint.seen().ok_or("the endpoint was never asked")?;
        assert_eq!(seen.client_secret, None);
        Ok(())
    }

    #[test]
    fn a_confidential_client_sends_its_secret() -> Outcome {
        let client = confidential_client()?;
        let endpoint = Recording::granting();

        now(redeem(authorized(&client)?, &client, &endpoint)).ok_or("the future pended")??;

        let seen = endpoint.seen().ok_or("the endpoint was never asked")?;
        assert_eq!(seen.client_secret.as_deref(), Some("s3cret"));
        Ok(())
    }

    /// The check that stops a caller reaching for the wrong registration. The
    /// alternative is a token request naming one client with a verifier
    /// generated for another, and the server's `invalid_grant` explains
    /// nothing about which of the two was wrong.
    #[test]
    fn a_registration_for_another_client_is_refused_before_the_request() -> Outcome {
        let issued_for = confidential_client()?;
        let endpoint = Recording::granting();

        let outcome = now(redeem(
            authorized(&issued_for)?,
            &public_client()?,
            &endpoint,
        ));

        assert_matches!(outcome, Some(Err(RedeemError::ClientMismatch { .. })));
        assert!(
            endpoint.seen().is_none(),
            "a mismatched code was sent anyway"
        );
        Ok(())
    }

    /// §5.2's code reaches the caller, because `invalid_grant` and
    /// `invalid_client` call for different responses and a bare message does not
    /// distinguish them.
    #[test]
    fn the_servers_own_refusal_reaches_the_caller() -> Outcome {
        let client = public_client()?;
        let endpoint = Recording::refusing(TokenResponseError::AuthorizationServer {
            error: "invalid_grant".to_owned(),
            description: None,
        });

        let outcome = now(redeem(authorized(&client)?, &client, &endpoint));

        assert_matches!(
            outcome,
            Some(Err(RedeemError::Endpoint(TokenEndpointError::Refused(
                TokenResponseError::AuthorizationServer { error, ..
            })))) if error == "invalid_grant"
        );
        Ok(())
    }

    /// An endpoint that could not be reached is not the same as an endpoint
    /// that refused: the first is worth retrying later, the second is not.
    #[test]
    fn an_unreachable_endpoint_is_not_reported_as_a_refusal() -> Outcome {
        let client = public_client()?;
        let endpoint = Recording::unreachable();

        let outcome = now(redeem(authorized(&client)?, &client, &endpoint));

        assert_matches!(
            outcome,
            Some(Err(RedeemError::Endpoint(TokenEndpointError::Unavailable(
                _
            ))))
        );
        Ok(())
    }

    /// No error on any path carries the code, the verifier or the secret.
    /// These are the three values a pasted bug report would leak.
    #[test]
    fn no_error_text_carries_a_credential() -> Outcome {
        let client = confidential_client()?;
        let endpoint = Recording::refusing(TokenResponseError::AuthorizationServer {
            error: "invalid_grant".to_owned(),
            description: None,
        });

        let outcome = now(redeem(authorized(&client)?, &client, &endpoint));

        let rendered = match outcome {
            Some(Err(error)) => error.to_string(),
            _ => String::from("no error was produced"),
        };
        for secret in ["the-code", VERIFIER, "s3cret"] {
            assert!(!rendered.contains(secret), "{rendered} leaked {secret}");
        }
        Ok(())
    }
}