Skip to main content

termivar_scanner/web_runtime/
supplied_session_runtime.rs

1//! Bounded supplied-session orchestration under the assessment's shared authority.
2
3mod oauth_authorization;
4mod oauth_code;
5pub(crate) use oauth_authorization::OAuthAuthorizationDispatchPermit;
6pub use oauth_authorization::{
7    SuppliedSessionOAuthAuthorizationAudit, SuppliedSessionOAuthAuthorizationOutcome,
8};
9mod oauth_refresh;
10use oauth_code::{CommittedOAuthCodeReceipt, OAuthCodeAcquisitionContext};
11pub use oauth_code::{
12    SuppliedSessionOAuthCodeAudit, SuppliedSessionOAuthCodeOutcome,
13    SuppliedSessionOAuthInitiatedTokenAudit,
14};
15pub(crate) use oauth_refresh::OAuthRefreshDispatchPermit;
16use oauth_refresh::{CommittedOAuthRefreshReceipt, OAuthRefreshContext};
17pub use oauth_refresh::{SuppliedSessionOAuthRefreshAudit, SuppliedSessionOAuthRefreshOutcome};
18
19/// V6 records one explicitly scheduled, separately committed OAuth renewal.
20pub const SUPPLIED_SESSION_OAUTH_REFRESH_AUDIT_SCHEMA: &str = "security.supplied-session-audit/v6";
21/// V7 client-generated authorization and a separately committed token exchange.
22pub const SUPPLIED_SESSION_OAUTH_AUTHORIZATION_AUDIT_SCHEMA: &str =
23    "security.supplied-session-audit/v7";
24
25use std::time::Duration;
26
27use serde::{
28    de::{DeserializeSeed, MapAccess, SeqAccess, Visitor},
29    Deserializer, Serialize,
30};
31use sha2::{Digest, Sha256};
32use termivar_core::{
33    ConfidenceScore, EntityId, Evidence, EvidenceId, EvidenceKind, EvidenceSource, EvidenceValue,
34    KnowledgePredicate,
35};
36
37#[cfg(feature = "websocket-review")]
38use super::websocket_runtime::{
39    execute_supplied_session_websocket_review, websocket_review_not_run,
40    WebAssessmentWebSocketReviewAudit, WebSocketSuppliedSessionContextAudit,
41    WebSocketSuppliedSessionHealthAudit, WebSocketSuppliedSessionQualification,
42};
43use super::{authority::SharedWebRuntimeAuthority, RuntimeExecution};
44#[cfg(feature = "websocket-review")]
45use crate::supplied_session_review::SuppliedSessionAuthorization;
46use crate::{
47    http_evidence::{
48        extract_supplied_session_login_csrf, CollectedHttpResponse, HttpRequestBroker,
49        OAuthCodeRequestBroker, OAuthCodeRequestDescriptor, OAuthRefreshRequestDescriptor,
50        SuppliedSessionCookieUpdateClassification, SuppliedSessionCookieUpdateObservation,
51        SuppliedSessionLoginCsrfToken, SuppliedSessionLoginFormError, SuppliedSessionRequestError,
52    },
53    supplied_session_review::{
54        SuppliedSessionCookieSummary, SuppliedSessionCookieUpdatePolicy, SuppliedSessionCredential,
55        SuppliedSessionCredentialAcquisition, SuppliedSessionCredentialMechanism,
56        SuppliedSessionFormBody, SuppliedSessionPolicy, SuppliedSessionPolicyVersion,
57        SuppliedSessionRequestDescriptor, SuppliedSessionRequestPurpose,
58        SuppliedSessionRuntimeInput, MAX_SUPPLIED_SESSION_CHECKPOINTS,
59        MAX_SUPPLIED_SESSION_RESOURCES,
60    },
61    DecisionActionOrigin, DecisionExecutionLimits, DecisionExecutionStage, KnowledgeBase,
62};
63#[cfg(feature = "websocket-review")]
64use crate::{web_runtime::WebSocketReviewFailureKind, websocket_review::WebSocketReviewPolicy};
65
66/// Sealed runtime observer for consumers of health-qualified supplied-session
67/// resource responses. Implementations may reduce a body while it is alive,
68/// but can publish only after receiving the exact committed S01 receipt.
69pub(super) trait SuppliedSessionCommittedResourceObserver: Send + Sync {
70    fn begin(
71        &self,
72        policy: &SuppliedSessionPolicy,
73        resources: &[(SuppliedSessionRequestDescriptor, String)],
74    ) -> Result<(), ()>;
75
76    fn stage_response(
77        &self,
78        sequence: u8,
79        policy: &SuppliedSessionPolicy,
80        descriptor: &SuppliedSessionRequestDescriptor,
81        response: &CollectedHttpResponse,
82    ) -> Result<(), ()>;
83
84    fn commit_response(
85        &self,
86        sequence: u8,
87        policy: &SuppliedSessionPolicy,
88        descriptor: &SuppliedSessionRequestDescriptor,
89        receipt: &CommittedSuppliedSessionResourceReceipt,
90        knowledge: &KnowledgeBase,
91    ) -> Result<(), ()>;
92
93    fn discard_response(&self, sequence: u8);
94
95    fn fail(&self);
96}
97
98/// Exact, value-safe proof that S01 committed one resource only after its
99/// immediately following healthy checkpoint. Fields are intentionally sealed.
100pub(super) struct CommittedSuppliedSessionResourceReceipt {
101    #[cfg(feature = "wordpress-review")]
102    sequence: u8,
103    #[cfg(feature = "wordpress-review")]
104    resource_reference: String,
105    #[cfg(feature = "wordpress-review")]
106    policy_reference: String,
107    #[cfg(feature = "wordpress-review")]
108    application_reference: String,
109    #[cfg(feature = "wordpress-review")]
110    principal_reference: String,
111    #[cfg(feature = "wordpress-review")]
112    credential_mechanism: SuppliedSessionCredentialMechanism,
113    #[cfg(feature = "wordpress-review")]
114    epoch: u8,
115    evidence_reference: String,
116    #[cfg(feature = "wordpress-review")]
117    expected_evidence: Evidence,
118}
119
120#[cfg(feature = "wordpress-review")]
121impl CommittedSuppliedSessionResourceReceipt {
122    pub(super) const fn sequence(&self) -> u8 {
123        self.sequence
124    }
125
126    pub(super) fn resource_reference(&self) -> &str {
127        &self.resource_reference
128    }
129
130    pub(super) fn evidence_reference(&self) -> &str {
131        &self.evidence_reference
132    }
133
134    pub(super) fn is_valid_for(
135        &self,
136        sequence: u8,
137        policy: &SuppliedSessionPolicy,
138        descriptor: &SuppliedSessionRequestDescriptor,
139        knowledge: &KnowledgeBase,
140    ) -> bool {
141        EvidenceId::parse(self.evidence_reference.clone())
142            .ok()
143            .and_then(|id| {
144                knowledge.inspect_evidence(&id, |stored| stored == &self.expected_evidence)
145            })
146            == Some(true)
147            && self.sequence == sequence
148            && self.resource_reference == descriptor.target_reference()
149            && self.policy_reference == policy.policy_reference()
150            && self.application_reference == policy.application_reference()
151            && self.principal_reference == policy.principal_reference()
152            && self.credential_mechanism == policy.credential_mechanism()
153            && self.epoch == 1
154            && descriptor.validate()
155            && descriptor.purpose() == SuppliedSessionRequestPurpose::Resource
156            && self.expected_evidence.kind() == &EvidenceKind::Authentication
157            && self.expected_evidence.predicate().namespace() == RESOURCE_COMMIT_EVIDENCE_NAMESPACE
158            && self.expected_evidence.predicate().name() == RESOURCE_COMMIT_EVIDENCE_PREDICATE
159    }
160}
161
162/// Exact, value-safe startup/terminal health proof used only to narrow the
163/// supplied-session WebSocket authority. The proof is sealed to this module
164/// and revalidates its committed parent evidence before minting transport.
165#[cfg(feature = "websocket-review")]
166struct CommittedSuppliedSessionHealthReceipt {
167    sequence: u8,
168    phase: SuppliedSessionHealthCheckpointPhase,
169    after_subject_count: u8,
170    policy_reference: String,
171    application_reference: String,
172    principal_reference: String,
173    credential_mechanism: SuppliedSessionCredentialMechanism,
174    epoch: u8,
175    evidence_reference: String,
176    expected_evidence: Evidence,
177}
178
179#[cfg(feature = "websocket-review")]
180impl CommittedSuppliedSessionHealthReceipt {
181    fn from_checkpoint(
182        policy: &SuppliedSessionPolicy,
183        checkpoint: &WebAssessmentSuppliedSessionCheckpointAudit,
184        expected_phase: SuppliedSessionHealthCheckpointPhase,
185        expected_after_subject_count: u8,
186        knowledge: &KnowledgeBase,
187    ) -> Option<Self> {
188        if checkpoint.phase() != expected_phase
189            || checkpoint.after_subject_count() != expected_after_subject_count
190            || checkpoint.outcome() != SuppliedSessionHealthOutcome::Healthy
191            || checkpoint.body_state() != SuppliedSessionBodyState::Complete
192            || checkpoint.predicate() != SuppliedSessionPredicateOutcome::Matched
193        {
194            return None;
195        }
196        let evidence_reference = checkpoint.evidence_reference()?.to_owned();
197        let evidence_id = EvidenceId::parse(evidence_reference.clone()).ok()?;
198        let expected_evidence = knowledge.inspect_evidence(&evidence_id, Clone::clone)?;
199        Some(Self {
200            sequence: checkpoint.sequence(),
201            phase: checkpoint.phase(),
202            after_subject_count: checkpoint.after_subject_count(),
203            policy_reference: policy.policy_reference().to_owned(),
204            application_reference: policy.application_reference().to_owned(),
205            principal_reference: policy.principal_reference().to_owned(),
206            credential_mechanism: policy.credential_mechanism(),
207            epoch: 1,
208            evidence_reference,
209            expected_evidence,
210        })
211    }
212
213    fn is_valid_for(
214        &self,
215        policy: &SuppliedSessionPolicy,
216        expected_phase: SuppliedSessionHealthCheckpointPhase,
217        expected_after_subject_count: u8,
218        knowledge: &KnowledgeBase,
219    ) -> bool {
220        EvidenceId::parse(self.evidence_reference.clone())
221            .ok()
222            .and_then(|id| {
223                knowledge.inspect_evidence(&id, |stored| stored == &self.expected_evidence)
224            })
225            == Some(true)
226            && self.sequence
227                == match expected_phase {
228                    SuppliedSessionHealthCheckpointPhase::Startup => 0,
229                    SuppliedSessionHealthCheckpointPhase::Terminal => 1,
230                    SuppliedSessionHealthCheckpointPhase::SubjectBoundary
231                    | SuppliedSessionHealthCheckpointPhase::Renewal => return false,
232                }
233            && self.phase == expected_phase
234            && self.after_subject_count == expected_after_subject_count
235            && self.policy_reference == policy.policy_reference()
236            && self.application_reference == policy.application_reference()
237            && self.principal_reference == policy.principal_reference()
238            && self.credential_mechanism == SuppliedSessionCredentialMechanism::AuthorizationHeader
239            && policy.version() == SuppliedSessionPolicyVersion::V1
240            && policy.credential_mechanism()
241                == SuppliedSessionCredentialMechanism::AuthorizationHeader
242            && policy.resource_count() == 1
243            && self.epoch == 1
244            && self.expected_evidence.kind() == &EvidenceKind::Authentication
245            && self.expected_evidence.predicate().namespace() == RESOURCE_COMMIT_EVIDENCE_NAMESPACE
246            && self.expected_evidence.predicate().name() == HEALTH_COMMIT_EVIDENCE_PREDICATE
247    }
248}
249
250/// Move-only, module-sealed authorization for one supplied-session WebSocket
251/// handshake. It borrows the guarded header and never stores or hashes it.
252#[cfg(feature = "websocket-review")]
253pub(super) struct SuppliedSessionWebSocketPermit<'a> {
254    policy: &'a SuppliedSessionPolicy,
255    authorization: &'a SuppliedSessionAuthorization,
256    websocket_policy_reference: &'a str,
257    websocket_endpoint_reference: &'a str,
258    session_deadline: Option<tokio::time::Instant>,
259    startup: CommittedSuppliedSessionHealthReceipt,
260}
261
262#[cfg(feature = "websocket-review")]
263impl<'a> SuppliedSessionWebSocketPermit<'a> {
264    fn new(
265        policy: &'a SuppliedSessionPolicy,
266        authorization: &'a SuppliedSessionAuthorization,
267        websocket: &'a WebSocketReviewPolicy,
268        session_deadline: Option<tokio::time::Instant>,
269        startup: CommittedSuppliedSessionHealthReceipt,
270    ) -> Self {
271        Self {
272            policy,
273            authorization,
274            websocket_policy_reference: websocket.policy_reference(),
275            websocket_endpoint_reference: websocket.endpoint_reference(),
276            session_deadline,
277            startup,
278        }
279    }
280
281    pub(super) fn is_valid_for(
282        &self,
283        selected_application: &url::Url,
284        websocket: &WebSocketReviewPolicy,
285        knowledge: &KnowledgeBase,
286        parent_deadline: Option<tokio::time::Instant>,
287    ) -> bool {
288        self.policy.application_url() == selected_application
289            && self.policy.version() == SuppliedSessionPolicyVersion::V1
290            && self.policy.credential_mechanism()
291                == SuppliedSessionCredentialMechanism::AuthorizationHeader
292            && self.policy.resource_count() == 1
293            && self.websocket_policy_reference == websocket.policy_reference()
294            && self.websocket_endpoint_reference == websocket.endpoint_reference()
295            && self.session_deadline.is_some()
296            && parent_deadline.is_none_or(|parent| {
297                self.session_deadline
298                    .is_some_and(|session| session <= parent)
299            })
300            && self.startup.is_valid_for(
301                self.policy,
302                SuppliedSessionHealthCheckpointPhase::Startup,
303                0,
304                knowledge,
305            )
306    }
307
308    pub(super) const fn authorization(&self) -> &SuppliedSessionAuthorization {
309        self.authorization
310    }
311
312    pub(super) const fn session_deadline(&self) -> Option<tokio::time::Instant> {
313        self.session_deadline
314    }
315}
316
317/// Stable saved-audit schema for the supplied-session child.
318pub const SUPPLIED_SESSION_AUDIT_SCHEMA: &str = "security.supplied-session-audit/v1";
319/// Strict saved-audit schema for supplied-cookie sessions.
320pub const SUPPLIED_SESSION_COOKIE_AUDIT_SCHEMA: &str = "security.supplied-session-audit/v2";
321/// Strict saved-audit schema for one bounded form-login transition.
322pub const SUPPLIED_SESSION_FORM_LOGIN_AUDIT_SCHEMA: &str = "security.supplied-session-audit/v3";
323/// Scheduled supplied-cookie renewal audit, preserving the historical audits.
324pub const SUPPLIED_SESSION_RENEWAL_AUDIT_SCHEMA: &str = "security.supplied-session-audit/v4";
325/// Imported-code acquisition plus the existing independently qualified health loop.
326pub const SUPPLIED_SESSION_OAUTH_CODE_AUDIT_SCHEMA: &str = "security.supplied-session-audit/v5";
327/// Stable capability identity. This audit-only V1 emits no assessment item.
328pub const SUPPLIED_SESSION_CAPABILITY_ID: &str = "session.supplied-context-assessment@1";
329
330const HEALTH_JSON_MAX_DEPTH: usize = 16;
331const HEALTH_JSON_MAX_NODES: usize = 512;
332const HEALTH_JSON_MAX_KEY_BYTES: usize = 256;
333const RESOURCE_COMMIT_EVIDENCE_DOMAIN: &[u8] = b"security.supplied-session-resource-evidence.v1\0";
334const HEALTH_COMMIT_EVIDENCE_DOMAIN: &[u8] = b"security.supplied-session-health-evidence.v1\0";
335const RESOURCE_COMMIT_EVIDENCE_PREFIX: &str = "supplied-session-resource-evidence-sha256";
336const HEALTH_COMMIT_EVIDENCE_PREFIX: &str = "supplied-session-checkpoint-evidence-sha256";
337const RESOURCE_COMMIT_EVIDENCE_NAMESPACE: &str = "session.supplied-context-assessment";
338const RESOURCE_COMMIT_EVIDENCE_PREDICATE: &str = "protected-resource-response-committed";
339const RESOURCE_HEALTH_UNQUALIFIED_EVIDENCE_PREDICATE: &str =
340    "protected-resource-response-health-unqualified";
341const HEALTH_COMMIT_EVIDENCE_PREDICATE: &str = "health-checkpoint-observation-committed";
342const RESOURCE_COMMIT_EVIDENCE_COMPONENT: &str = "supplied-session.runtime";
343const RESOURCE_COMMIT_EVIDENCE_METHOD_V1: &str = "descriptor-bound-complete-authorization-get";
344const HEALTH_COMMIT_EVIDENCE_METHOD_V1: &str = "descriptor-bound-health-authorization-get";
345const RESOURCE_COMMIT_EVIDENCE_METHOD_V2: &str = "descriptor-bound-complete-supplied-cookie-get";
346const HEALTH_COMMIT_EVIDENCE_METHOD_V2: &str = "descriptor-bound-health-supplied-cookie-get";
347const RESOURCE_COMMIT_EVIDENCE_METHOD_V3: &str = "descriptor-bound-complete-form-login-cookie-get";
348const HEALTH_COMMIT_EVIDENCE_METHOD_V3: &str = "descriptor-bound-health-form-login-cookie-get";
349const SUPPLIED_SESSION_NON_BROWSER_COOKIE_SEMANTICS: &str =
350    "attributes_preserved_not_browser_csrf_emulation";
351
352/// Assurance attached to the principal label used for this run.
353#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
354#[serde(rename_all = "snake_case")]
355#[non_exhaustive]
356pub enum SuppliedSessionPrincipalAssurance {
357    /// The principal alias was declared by the operator, not authenticated as identity truth.
358    OperatorDeclared,
359}
360
361/// The closed health predicate implemented by V1.
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
363#[serde(rename_all = "snake_case")]
364#[non_exhaustive]
365pub enum SuppliedSessionHealthOracleKind {
366    /// A configured top-level JSON member must be the boolean `true`.
367    JsonBooleanTrue,
368}
369
370/// Value-free description of the configured health oracle.
371#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
372pub struct SuppliedSessionHealthOracleAudit {
373    kind: SuppliedSessionHealthOracleKind,
374    field_reference: String,
375}
376
377impl SuppliedSessionHealthOracleAudit {
378    pub const fn kind(&self) -> SuppliedSessionHealthOracleKind {
379        self.kind
380    }
381
382    pub fn field_reference(&self) -> &str {
383        &self.field_reference
384    }
385}
386
387/// Overall bounded child outcome.
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
389#[serde(rename_all = "snake_case")]
390#[non_exhaustive]
391pub enum SuppliedSessionAuditOutcome {
392    Complete,
393    StartupUnhealthy,
394    SessionLost,
395    ResourceUnavailable,
396    RuntimeLimit,
397    Cancelled,
398    /// A selected cookie was reissued; V2 stopped without applying it.
399    CredentialUpdateRequired,
400    /// A response cookie update could not be classified within V2's bounds.
401    CredentialUpdateUnusable,
402    /// The explicit login page did not yield one admissible form/token.
403    LoginFormUnavailable,
404    /// The single stateful login submission could not complete.
405    LoginSubmitUnavailable,
406    /// The login response did not yield the exact declared cookie set.
407    LoginCookieUnavailable,
408    /// The one selected renewal did not yield a usable replacement context.
409    RenewalUnavailable,
410    /// The one external code exchange did not yield a committed usable credential.
411    #[serde(rename = "oauth_code_exchange_unavailable")]
412    OAuthCodeExchangeUnavailable,
413    /// The one declared OAuth refresh did not commit a usable next context.
414    OAuthRefreshUnavailable,
415}
416
417/// Value-free result of selecting the one policy-bound login form.
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
419#[serde(rename_all = "snake_case")]
420#[non_exhaustive]
421pub enum SuppliedSessionLoginFormOutcome {
422    Matched,
423    IneligibleResponse,
424    Missing,
425    Ambiguous,
426    Invalid,
427    TransportFailed,
428    RuntimeLimit,
429    Cancelled,
430    NotEvaluated,
431}
432
433/// Value-free result of the single login POST.
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
435#[serde(rename_all = "snake_case")]
436#[non_exhaustive]
437pub enum SuppliedSessionLoginSubmitOutcome {
438    CookieAcquired,
439    CookieUnavailable,
440    HttpError,
441    RedirectRefused,
442    Incomplete,
443    TransportFailed,
444    RuntimeLimit,
445    Cancelled,
446    NotDispatched,
447}
448
449/// Redaction-safe V3 acquisition record. Epoch one is established only after
450/// the existing startup health checkpoint matches and commits.
451#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
452pub struct SuppliedSessionLoginAudit {
453    login_reference: String,
454    max_attempts: u8,
455    attempt_count: u8,
456    page_dispatched: bool,
457    page_status: Option<u16>,
458    page_body_state: SuppliedSessionBodyState,
459    form_outcome: SuppliedSessionLoginFormOutcome,
460    #[serde(skip_serializing_if = "Option::is_none")]
461    form_error_code: Option<&'static str>,
462    submit_dispatched: bool,
463    submit_status: Option<u16>,
464    submit_body_state: SuppliedSessionBodyState,
465    submit_outcome: SuppliedSessionLoginSubmitOutcome,
466    acquired_cookie_count: u8,
467    response_bytes: u64,
468    initial_epoch: u8,
469    final_epoch: u8,
470    pre_session_cookie_applied: bool,
471}
472
473impl SuppliedSessionLoginAudit {
474    pub fn login_reference(&self) -> &str {
475        &self.login_reference
476    }
477    pub const fn max_attempts(&self) -> u8 {
478        self.max_attempts
479    }
480    pub const fn attempt_count(&self) -> u8 {
481        self.attempt_count
482    }
483    pub const fn page_dispatched(&self) -> bool {
484        self.page_dispatched
485    }
486    pub const fn page_status(&self) -> Option<u16> {
487        self.page_status
488    }
489    pub const fn page_body_state(&self) -> SuppliedSessionBodyState {
490        self.page_body_state
491    }
492    pub const fn form_outcome(&self) -> SuppliedSessionLoginFormOutcome {
493        self.form_outcome
494    }
495    pub const fn form_error_code(&self) -> Option<&'static str> {
496        self.form_error_code
497    }
498    pub const fn submit_dispatched(&self) -> bool {
499        self.submit_dispatched
500    }
501    pub const fn submit_status(&self) -> Option<u16> {
502        self.submit_status
503    }
504    pub const fn submit_body_state(&self) -> SuppliedSessionBodyState {
505        self.submit_body_state
506    }
507    pub const fn submit_outcome(&self) -> SuppliedSessionLoginSubmitOutcome {
508        self.submit_outcome
509    }
510    pub const fn acquired_cookie_count(&self) -> u8 {
511        self.acquired_cookie_count
512    }
513    pub const fn response_bytes(&self) -> u64 {
514        self.response_bytes
515    }
516    pub const fn initial_epoch(&self) -> u8 {
517        self.initial_epoch
518    }
519    pub const fn final_epoch(&self) -> u8 {
520        self.final_epoch
521    }
522    pub const fn pre_session_cookie_applied(&self) -> bool {
523        self.pre_session_cookie_applied
524    }
525}
526
527/// Authenticated resource coverage established by committed resource responses and checkpoints.
528#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
529#[serde(rename_all = "snake_case")]
530#[non_exhaustive]
531pub enum SuppliedSessionCoverage {
532    Complete,
533    Partial,
534    None,
535}
536
537/// Position of one health check in the fixed schedule.
538#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
539#[serde(rename_all = "snake_case")]
540#[non_exhaustive]
541pub enum SuppliedSessionHealthCheckpointPhase {
542    Startup,
543    SubjectBoundary,
544    Terminal,
545    /// Immediate health check after applying the selected replacement cookies.
546    Renewal,
547}
548
549/// Health interpretation at one actually dispatched checkpoint.
550#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
551#[serde(rename_all = "snake_case")]
552#[non_exhaustive]
553pub enum SuppliedSessionHealthOutcome {
554    Healthy,
555    Unhealthy,
556    Indeterminate,
557}
558
559/// Completeness of a health-check response body.
560#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
561#[serde(rename_all = "snake_case")]
562#[non_exhaustive]
563pub enum SuppliedSessionBodyState {
564    Complete,
565    Incomplete,
566    Unavailable,
567}
568
569/// Whether the closed health predicate was evaluated successfully.
570#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
571#[serde(rename_all = "snake_case")]
572#[non_exhaustive]
573pub enum SuppliedSessionPredicateOutcome {
574    Matched,
575    NotMatched,
576    NotEvaluated,
577}
578
579/// Outcome of one selected protected-resource request.
580#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
581#[serde(rename_all = "snake_case")]
582#[non_exhaustive]
583pub enum SuppliedSessionResourceOutcome {
584    Committed,
585    /// A complete response was observed, but the following health checkpoint
586    /// did not qualify it as authenticated coverage.
587    HealthUnqualified,
588    HttpError,
589    RedirectRefused,
590    Incomplete,
591    TransportFailed,
592    NotDispatched,
593}
594
595/// One value-free, actually dispatched health checkpoint.
596#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
597pub struct WebAssessmentSuppliedSessionCheckpointAudit {
598    sequence: u8,
599    evidence_reference: Option<String>,
600    phase: SuppliedSessionHealthCheckpointPhase,
601    after_subject_count: u8,
602    outcome: SuppliedSessionHealthOutcome,
603    status: Option<u16>,
604    body_state: SuppliedSessionBodyState,
605    predicate: SuppliedSessionPredicateOutcome,
606    response_bytes: u64,
607    #[serde(skip_serializing_if = "Option::is_none")]
608    epoch: Option<u8>,
609    #[serde(skip)]
610    cookie_update: Option<SuppliedSessionResponseCookieUpdate>,
611}
612
613impl WebAssessmentSuppliedSessionCheckpointAudit {
614    pub fn epoch(&self) -> u8 {
615        self.epoch.unwrap_or(1)
616    }
617    pub const fn sequence(&self) -> u8 {
618        self.sequence
619    }
620    pub fn evidence_reference(&self) -> Option<&str> {
621        self.evidence_reference.as_deref()
622    }
623    pub const fn phase(&self) -> SuppliedSessionHealthCheckpointPhase {
624        self.phase
625    }
626    pub const fn after_subject_count(&self) -> u8 {
627        self.after_subject_count
628    }
629    pub const fn outcome(&self) -> SuppliedSessionHealthOutcome {
630        self.outcome
631    }
632    pub const fn status(&self) -> Option<u16> {
633        self.status
634    }
635    pub const fn body_state(&self) -> SuppliedSessionBodyState {
636        self.body_state
637    }
638    pub const fn predicate(&self) -> SuppliedSessionPredicateOutcome {
639        self.predicate
640    }
641    pub const fn response_bytes(&self) -> u64 {
642        self.response_bytes
643    }
644}
645
646/// One selected protected resource and its value-free execution result.
647#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
648pub struct WebAssessmentSuppliedSessionResourceAudit {
649    sequence: u8,
650    resource_reference: String,
651    evidence_reference: Option<String>,
652    outcome: SuppliedSessionResourceOutcome,
653    status: Option<u16>,
654    response_bytes: u64,
655    epoch: u8,
656    #[serde(skip)]
657    cookie_update: Option<SuppliedSessionResponseCookieUpdate>,
658}
659
660impl WebAssessmentSuppliedSessionResourceAudit {
661    pub const fn sequence(&self) -> u8 {
662        self.sequence
663    }
664    pub fn resource_reference(&self) -> &str {
665        &self.resource_reference
666    }
667    pub fn evidence_reference(&self) -> Option<&str> {
668        self.evidence_reference.as_deref()
669    }
670    pub const fn outcome(&self) -> SuppliedSessionResourceOutcome {
671        self.outcome
672    }
673    pub const fn status(&self) -> Option<u16> {
674        self.status
675    }
676    pub const fn response_bytes(&self) -> u64 {
677        self.response_bytes
678    }
679    pub const fn epoch(&self) -> u8 {
680        self.epoch
681    }
682}
683
684#[derive(Debug, Clone, Copy, PartialEq, Eq)]
685enum SuppliedSessionResponseCookieUpdateKind {
686    None,
687    UnselectedOnly,
688    Selected,
689    Unusable,
690}
691
692#[derive(Debug, Clone, Copy, PartialEq, Eq)]
693struct SuppliedSessionResponseCookieUpdate {
694    kind: SuppliedSessionResponseCookieUpdateKind,
695    selected_count: u8,
696    unselected_count: u8,
697}
698
699/// Redaction-safe static cookie-policy aggregate for audit V2.
700#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
701pub struct SuppliedSessionCookiePolicyAudit {
702    declared_count: u8,
703    host_only_count: u8,
704    domain_count: u8,
705    secure_count: u8,
706    http_only_count: u8,
707    session_count: u8,
708    persistent_count: u8,
709    same_site_missing_count: u8,
710    same_site_strict_count: u8,
711    same_site_lax_count: u8,
712    same_site_none_count: u8,
713    update_policy: SuppliedSessionCookieUpdatePolicy,
714}
715
716impl SuppliedSessionCookiePolicyAudit {
717    pub const fn declared_count(&self) -> u8 {
718        self.declared_count
719    }
720    pub const fn host_only_count(&self) -> u8 {
721        self.host_only_count
722    }
723    pub const fn domain_count(&self) -> u8 {
724        self.domain_count
725    }
726    pub const fn secure_count(&self) -> u8 {
727        self.secure_count
728    }
729    pub const fn http_only_count(&self) -> u8 {
730        self.http_only_count
731    }
732    pub const fn session_count(&self) -> u8 {
733        self.session_count
734    }
735    pub const fn persistent_count(&self) -> u8 {
736        self.persistent_count
737    }
738    pub const fn same_site_missing_count(&self) -> u8 {
739        self.same_site_missing_count
740    }
741    pub const fn same_site_strict_count(&self) -> u8 {
742        self.same_site_strict_count
743    }
744    pub const fn same_site_lax_count(&self) -> u8 {
745        self.same_site_lax_count
746    }
747    pub const fn same_site_none_count(&self) -> u8 {
748        self.same_site_none_count
749    }
750    pub const fn update_policy(&self) -> SuppliedSessionCookieUpdatePolicy {
751        self.update_policy
752    }
753    pub const fn browser_semantics(&self) -> &'static str {
754        SUPPLIED_SESSION_NON_BROWSER_COOKIE_SEMANTICS
755    }
756}
757
758/// Redaction-safe dynamic cookie lifecycle aggregate for audit V2.
759#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
760pub struct SuppliedSessionCookieLifecycleAudit {
761    initial_epoch: u8,
762    final_epoch: u8,
763    selected_update_response_count: u8,
764    unselected_update_response_count: u8,
765    update_classification_failure_count: u8,
766    updates_applied: u8,
767}
768
769impl SuppliedSessionCookieLifecycleAudit {
770    pub const fn initial_epoch(&self) -> u8 {
771        self.initial_epoch
772    }
773    pub const fn final_epoch(&self) -> u8 {
774        self.final_epoch
775    }
776    pub const fn selected_update_response_count(&self) -> u8 {
777        self.selected_update_response_count
778    }
779    pub const fn unselected_update_response_count(&self) -> u8 {
780        self.unselected_update_response_count
781    }
782    pub const fn update_classification_failure_count(&self) -> u8 {
783        self.update_classification_failure_count
784    }
785    pub const fn updates_applied(&self) -> u8 {
786        self.updates_applied
787    }
788}
789
790/// Redaction-safe audit for one explicitly selected supplied-session child.
791#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
792pub struct WebAssessmentSuppliedSessionAudit {
793    schema: &'static str,
794    capability_id: &'static str,
795    policy_reference: String,
796    application_reference: String,
797    principal_reference: String,
798    principal_alias: String,
799    principal_assurance: SuppliedSessionPrincipalAssurance,
800    credential_mechanism: SuppliedSessionCredentialMechanism,
801    #[serde(skip_serializing_if = "legacy_credential_acquisition")]
802    credential_acquisition: SuppliedSessionCredentialAcquisition,
803    health_oracle: SuppliedSessionHealthOracleAudit,
804    outcome: SuppliedSessionAuditOutcome,
805    coverage: SuppliedSessionCoverage,
806    checkpoints: Vec<WebAssessmentSuppliedSessionCheckpointAudit>,
807    resources: Vec<WebAssessmentSuppliedSessionResourceAudit>,
808    selected_resource_count: u8,
809    dispatched_resource_count: u8,
810    committed_resource_count: u8,
811    dispatched_request_count: u8,
812    response_bytes: u64,
813    response_byte_limit: u64,
814    response_byte_limit_exceeded: bool,
815    refresh_performed: bool,
816    anonymous_fallback_performed: bool,
817    continuous_authentication_established: bool,
818    exploit_execution_performed: bool,
819    impact_validation_performed: bool,
820    #[serde(skip_serializing_if = "Option::is_none")]
821    cookie_policy: Option<SuppliedSessionCookiePolicyAudit>,
822    #[serde(skip_serializing_if = "Option::is_none")]
823    cookie_lifecycle: Option<SuppliedSessionCookieLifecycleAudit>,
824    #[serde(skip_serializing_if = "Option::is_none")]
825    login: Option<SuppliedSessionLoginAudit>,
826    #[serde(skip_serializing_if = "Option::is_none")]
827    renewal: Option<SuppliedSessionRenewalAudit>,
828    // V5-only state must not enlarge every legacy report's inline payload.
829    #[serde(skip_serializing_if = "Option::is_none")]
830    oauth_code: Option<Box<SuppliedSessionOAuthCodeAudit>>,
831    #[serde(skip_serializing_if = "Option::is_none")]
832    oauth_refresh: Option<Box<SuppliedSessionOAuthRefreshAudit>>,
833    #[serde(skip_serializing_if = "Option::is_none")]
834    oauth_authorization: Option<Box<SuppliedSessionOAuthAuthorizationAudit>>,
835    #[serde(skip_serializing_if = "Option::is_none")]
836    oauth_token_exchange: Option<Box<SuppliedSessionOAuthInitiatedTokenAudit>>,
837}
838
839/// Value-free record of the one explicit credential transition and its health gate.
840#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
841pub struct SuppliedSessionRenewalAudit {
842    renewal_reference: String,
843    after_resource_count: u8,
844    attempt_count: u8,
845    status: Option<u16>,
846    body_state: SuppliedSessionBodyState,
847    outcome: SuppliedSessionLoginSubmitOutcome,
848    response_bytes: u64,
849    applied_cookie_count: u8,
850    initial_epoch: u8,
851    final_epoch: u8,
852    #[serde(skip_serializing_if = "Option::is_none")]
853    health: Option<WebAssessmentSuppliedSessionCheckpointAudit>,
854    #[serde(skip)]
855    cookie_update: Option<SuppliedSessionResponseCookieUpdate>,
856}
857
858impl SuppliedSessionRenewalAudit {
859    pub fn renewal_reference(&self) -> &str {
860        &self.renewal_reference
861    }
862    pub const fn after_resource_count(&self) -> u8 {
863        self.after_resource_count
864    }
865    pub const fn attempt_count(&self) -> u8 {
866        self.attempt_count
867    }
868    pub const fn status(&self) -> Option<u16> {
869        self.status
870    }
871    pub const fn body_state(&self) -> SuppliedSessionBodyState {
872        self.body_state
873    }
874    pub const fn outcome(&self) -> SuppliedSessionLoginSubmitOutcome {
875        self.outcome
876    }
877    pub const fn response_bytes(&self) -> u64 {
878        self.response_bytes
879    }
880    pub const fn applied_cookie_count(&self) -> u8 {
881        self.applied_cookie_count
882    }
883    pub const fn initial_epoch(&self) -> u8 {
884        self.initial_epoch
885    }
886    pub const fn final_epoch(&self) -> u8 {
887        self.final_epoch
888    }
889    pub fn health(&self) -> Option<&WebAssessmentSuppliedSessionCheckpointAudit> {
890        self.health.as_ref()
891    }
892}
893
894const fn legacy_credential_acquisition(value: &SuppliedSessionCredentialAcquisition) -> bool {
895    matches!(
896        value,
897        SuppliedSessionCredentialAcquisition::SuppliedAuthorization
898            | SuppliedSessionCredentialAcquisition::SuppliedCookieJar
899    )
900}
901
902impl WebAssessmentSuppliedSessionAudit {
903    pub fn oauth_authorization(&self) -> Option<&SuppliedSessionOAuthAuthorizationAudit> {
904        self.oauth_authorization.as_deref()
905    }
906    pub fn oauth_token_exchange(&self) -> Option<&SuppliedSessionOAuthInitiatedTokenAudit> {
907        self.oauth_token_exchange.as_deref()
908    }
909    pub fn oauth_refresh(&self) -> Option<&SuppliedSessionOAuthRefreshAudit> {
910        self.oauth_refresh.as_deref()
911    }
912    pub fn oauth_code(&self) -> Option<&SuppliedSessionOAuthCodeAudit> {
913        self.oauth_code.as_deref()
914    }
915    pub fn renewal(&self) -> Option<&SuppliedSessionRenewalAudit> {
916        self.renewal.as_ref()
917    }
918    pub const fn schema(&self) -> &'static str {
919        self.schema
920    }
921    pub const fn capability_id(&self) -> &'static str {
922        self.capability_id
923    }
924    pub fn policy_reference(&self) -> &str {
925        &self.policy_reference
926    }
927    pub fn application_reference(&self) -> &str {
928        &self.application_reference
929    }
930    pub fn principal_reference(&self) -> &str {
931        &self.principal_reference
932    }
933    /// Explicitly non-secret operator assertion, not authenticated identity truth.
934    pub fn principal_alias(&self) -> &str {
935        &self.principal_alias
936    }
937    pub const fn principal_assurance(&self) -> SuppliedSessionPrincipalAssurance {
938        self.principal_assurance
939    }
940    pub const fn credential_mechanism(&self) -> SuppliedSessionCredentialMechanism {
941        self.credential_mechanism
942    }
943    pub const fn credential_acquisition(&self) -> SuppliedSessionCredentialAcquisition {
944        self.credential_acquisition
945    }
946    pub const fn health_oracle(&self) -> &SuppliedSessionHealthOracleAudit {
947        &self.health_oracle
948    }
949    pub const fn outcome(&self) -> SuppliedSessionAuditOutcome {
950        self.outcome
951    }
952    pub const fn coverage(&self) -> SuppliedSessionCoverage {
953        self.coverage
954    }
955    pub fn checkpoints(&self) -> &[WebAssessmentSuppliedSessionCheckpointAudit] {
956        &self.checkpoints
957    }
958    pub fn resources(&self) -> &[WebAssessmentSuppliedSessionResourceAudit] {
959        &self.resources
960    }
961    pub const fn selected_resource_count(&self) -> u8 {
962        self.selected_resource_count
963    }
964    pub const fn dispatched_resource_count(&self) -> u8 {
965        self.dispatched_resource_count
966    }
967    pub const fn committed_resource_count(&self) -> u8 {
968        self.committed_resource_count
969    }
970    pub const fn dispatched_request_count(&self) -> u8 {
971        self.dispatched_request_count
972    }
973    pub const fn response_bytes(&self) -> u64 {
974        self.response_bytes
975    }
976    /// Configured child byte budget. Actual transport accounting may exceed it
977    /// by the broker's final delivered chunk and is never clamped in the audit.
978    pub const fn response_byte_limit(&self) -> u64 {
979        self.response_byte_limit
980    }
981    pub const fn response_byte_limit_exceeded(&self) -> bool {
982        self.response_byte_limit_exceeded
983    }
984    pub const fn refresh_performed(&self) -> bool {
985        self.refresh_performed
986    }
987    pub const fn anonymous_fallback_performed(&self) -> bool {
988        self.anonymous_fallback_performed
989    }
990    pub const fn continuous_authentication_established(&self) -> bool {
991        self.continuous_authentication_established
992    }
993    pub const fn exploit_execution_performed(&self) -> bool {
994        self.exploit_execution_performed
995    }
996    pub const fn impact_validation_performed(&self) -> bool {
997        self.impact_validation_performed
998    }
999    pub const fn cookie_policy(&self) -> Option<&SuppliedSessionCookiePolicyAudit> {
1000        self.cookie_policy.as_ref()
1001    }
1002    pub const fn cookie_lifecycle(&self) -> Option<&SuppliedSessionCookieLifecycleAudit> {
1003        self.cookie_lifecycle.as_ref()
1004    }
1005    pub const fn login(&self) -> Option<&SuppliedSessionLoginAudit> {
1006        self.login.as_ref()
1007    }
1008}
1009
1010pub(super) struct SuppliedSessionRuntimeConfig {
1011    policy: SuppliedSessionPolicy,
1012    input: SuppliedSessionRuntimeInput,
1013    requests: HttpRequestBroker,
1014    login_page: Option<SuppliedSessionRequestDescriptor>,
1015    login_submit: Option<SuppliedSessionRequestDescriptor>,
1016    // These children are allocated only when V5 is selected. The broker and
1017    // descriptor retain the same owner and shared assessment allowance.
1018    oauth_code_requests: Option<Box<OAuthCodeRequestBroker>>,
1019    oauth_code_descriptor: Option<Box<OAuthCodeRequestDescriptor>>,
1020    oauth_refresh_descriptor: Option<Box<OAuthRefreshRequestDescriptor>>,
1021    health: SuppliedSessionRequestDescriptor,
1022    resources: Vec<(SuppliedSessionRequestDescriptor, String)>,
1023}
1024
1025impl SuppliedSessionRuntimeConfig {
1026    pub(super) fn new<C>(
1027        policy: SuppliedSessionPolicy,
1028        credential: C,
1029        authority: &SharedWebRuntimeAuthority,
1030    ) -> Result<Self, ()>
1031    where
1032        C: Into<SuppliedSessionRuntimeInput>,
1033    {
1034        let input = credential.into();
1035        if !input.matches_policy(&policy) {
1036            return Err(());
1037        }
1038        let requests = authority
1039            .requests()
1040            .isolated_supplied_session()
1041            .map_err(|_| ())?;
1042        let health = SuppliedSessionRequestDescriptor::health(&policy);
1043        let (login_page, login_submit) = if policy.version() == SuppliedSessionPolicyVersion::V3 {
1044            (
1045                Some(SuppliedSessionRequestDescriptor::login_page(&policy).ok_or(())?),
1046                Some(SuppliedSessionRequestDescriptor::login_submit(&policy).ok_or(())?),
1047            )
1048        } else {
1049            (None, None)
1050        };
1051        let resources = policy
1052            .execution_resources()
1053            .map(|(target, reference)| {
1054                SuppliedSessionRequestDescriptor::resource(&policy, target, reference)
1055                    .map(|descriptor| (descriptor, reference.to_owned()))
1056            })
1057            .collect::<Option<Vec<_>>>()
1058            .ok_or(())?;
1059        let (oauth_code_requests, oauth_code_descriptor) = if matches!(
1060            policy.version(),
1061            SuppliedSessionPolicyVersion::V5
1062                | SuppliedSessionPolicyVersion::V6
1063                | SuppliedSessionPolicyVersion::V7
1064        ) {
1065            (
1066                Some(Box::new(authority.mint_oauth_code_broker(&policy)?)),
1067                Some(Box::new(
1068                    OAuthCodeRequestDescriptor::from_policy(&policy).ok_or(())?,
1069                )),
1070            )
1071        } else {
1072            (None, None)
1073        };
1074        let oauth_refresh_descriptor = if policy.version() == SuppliedSessionPolicyVersion::V6 {
1075            Some(Box::new(
1076                OAuthRefreshRequestDescriptor::from_policy(&policy).ok_or(())?,
1077            ))
1078        } else {
1079            None
1080        };
1081        Ok(Self {
1082            policy,
1083            input,
1084            requests,
1085            login_page,
1086            login_submit,
1087            oauth_code_requests,
1088            oauth_code_descriptor,
1089            oauth_refresh_descriptor,
1090            health,
1091            resources,
1092        })
1093    }
1094
1095    pub(super) async fn execute(
1096        self,
1097        authority: &SharedWebRuntimeAuthority,
1098        resource_observer: Option<&dyn SuppliedSessionCommittedResourceObserver>,
1099    ) -> WebAssessmentSuppliedSessionAudit {
1100        let Self {
1101            policy,
1102            input,
1103            requests,
1104            login_page,
1105            login_submit,
1106            oauth_code_requests,
1107            oauth_code_descriptor,
1108            oauth_refresh_descriptor,
1109            health,
1110            resources,
1111        } = self;
1112        let selected_resource_count = u8::try_from(resources.len()).unwrap_or(u8::MAX);
1113        let timing = authority.start();
1114        let local_deadline = tokio::time::Instant::now()
1115            .checked_add(Duration::from_millis(policy.max_wall_time_ms()));
1116        let mut session_deadline = earliest_deadline(timing.deadline(), local_deadline);
1117        let mut deadline = session_deadline;
1118        let mut state = ExecutionState::new(&policy, selected_resource_count);
1119        if resource_observer.is_some_and(|observer| observer.begin(&policy, &resources).is_err()) {
1120            resource_observer.expect("observer remains present").fail();
1121        }
1122
1123        let mut oauth_receipt = None;
1124        let mut oauth_refresh_token = None;
1125        let mut oauth_refresh_receipt = None;
1126        let mut credential = match input {
1127            SuppliedSessionRuntimeInput::Ready(credential) => credential,
1128            SuppliedSessionRuntimeInput::OAuthAuthorization(output) => {
1129                let mint = match authority.mint_oauth_authorization_callback(&policy) {
1130                    Ok(mint) => mint,
1131                    Err(()) => {
1132                        state.outcome = SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable;
1133                        state.add_not_dispatched(&resources);
1134                        return state.finish(policy);
1135                    },
1136                };
1137                let (authorization_audit, permit) = boxed_dispatch_execution(|| {
1138                    oauth_authorization::receive(authority, mint, &policy, *output)
1139                })
1140                .await;
1141                let authorization_outcome = authorization_audit.outcome();
1142                state.oauth_authorization = Some(Box::new(authorization_audit));
1143                let permit = match permit {
1144                    Ok(permit) => permit,
1145                    Err(()) => {
1146                        state.outcome = match authorization_outcome {
1147                            SuppliedSessionOAuthAuthorizationOutcome::Cancelled => {
1148                                SuppliedSessionAuditOutcome::Cancelled
1149                            },
1150                            SuppliedSessionOAuthAuthorizationOutcome::RuntimeLimit => {
1151                                SuppliedSessionAuditOutcome::RuntimeLimit
1152                            },
1153                            _ => SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable,
1154                        };
1155                        state.add_not_dispatched(&resources);
1156                        return state.finish(policy);
1157                    },
1158                };
1159                // Only the V7 pre-session wait is separate. The existing parent
1160                // clock is never reset or extended; its smaller deadline wins.
1161                session_deadline = earliest_deadline(timing.deadline(), Some(permit.deadline()));
1162                deadline = session_deadline;
1163                let (Some(idp_requests), Some(descriptor)) =
1164                    (oauth_code_requests.as_ref(), oauth_code_descriptor.as_ref())
1165                else {
1166                    state.outcome = SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable;
1167                    state.add_not_dispatched(&resources);
1168                    return state.finish(policy);
1169                };
1170                let (audit, acquired) = boxed_dispatch_execution(|| {
1171                    oauth_code::acquire_authorized(
1172                        OAuthCodeAcquisitionContext {
1173                            authority,
1174                            requests: idp_requests,
1175                            descriptor,
1176                            policy: &policy,
1177                            deadline,
1178                            budget: state.dispatch_budget(),
1179                        },
1180                        permit,
1181                    )
1182                })
1183                .await;
1184                state.response_bytes = state.response_bytes.saturating_add(audit.response_bytes());
1185                state.oauth_code = Some(Box::new(audit));
1186                match acquired {
1187                    Ok(acquired) => {
1188                        deadline = earliest_deadline(
1189                            session_deadline,
1190                            tokio::time::Instant::now().checked_add(acquired.declared_lifetime),
1191                        );
1192                        oauth_receipt = Some(Box::new(acquired.receipt));
1193                        acquired.credential
1194                    },
1195                    Err(outcome) => {
1196                        state.outcome = outcome;
1197                        state.add_not_dispatched(&resources);
1198                        return state.finish(policy);
1199                    },
1200                }
1201            },
1202            SuppliedSessionRuntimeInput::FormLogin(form_credential) => {
1203                let (Some(login_page), Some(login_submit)) =
1204                    (login_page.as_ref(), login_submit.as_ref())
1205                else {
1206                    state.outcome = SuppliedSessionAuditOutcome::LoginFormUnavailable;
1207                    state.add_not_dispatched(&resources);
1208                    return state.finish(policy);
1209                };
1210                state.begin_login(&policy, login_page);
1211                let acquisition_context = LoginDispatchContext {
1212                    authority,
1213                    requests: &requests,
1214                    policy: &policy,
1215                    deadline,
1216                };
1217                let login_page_result =
1218                    dispatch_login_page(&acquisition_context, login_page, state.dispatch_budget())
1219                        .await;
1220                let csrf = match state.record_login_page(login_page_result, &policy) {
1221                    Ok(csrf) => csrf,
1222                    Err(outcome) => {
1223                        state.outcome = outcome;
1224                        state.add_not_dispatched(&resources);
1225                        return state.finish(policy);
1226                    },
1227                };
1228                let body = match form_credential.compose_form_body(&policy, csrf.as_str()) {
1229                    Ok(body) => body,
1230                    Err(_) => {
1231                        state.mark_login_form_body_invalid();
1232                        state.outcome = SuppliedSessionAuditOutcome::LoginFormUnavailable;
1233                        state.add_not_dispatched(&resources);
1234                        return state.finish(policy);
1235                    },
1236                };
1237                let login_submit_result = dispatch_login_submit(
1238                    &acquisition_context,
1239                    login_submit,
1240                    &body,
1241                    state.dispatch_budget(),
1242                )
1243                .await;
1244                match state.record_login_submit(login_submit_result, &policy) {
1245                    Ok(credential) => credential,
1246                    Err(outcome) => {
1247                        state.outcome = outcome;
1248                        state.add_not_dispatched(&resources);
1249                        return state.finish(policy);
1250                    },
1251                }
1252            },
1253            SuppliedSessionRuntimeInput::OAuthCode(grant) => {
1254                let (Some(idp_requests), Some(descriptor)) =
1255                    (oauth_code_requests.as_ref(), oauth_code_descriptor.as_ref())
1256                else {
1257                    state.outcome = SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable;
1258                    state.add_not_dispatched(&resources);
1259                    return state.finish(policy);
1260                };
1261                // Keep this new child's construction frame out of legacy
1262                // session polls; it is still driven by this one parent.
1263                let (audit, acquired) = boxed_dispatch_execution(|| {
1264                    oauth_code::acquire(
1265                        OAuthCodeAcquisitionContext {
1266                            authority,
1267                            requests: idp_requests,
1268                            descriptor,
1269                            policy: &policy,
1270                            deadline,
1271                            budget: state.dispatch_budget(),
1272                        },
1273                        &grant,
1274                    )
1275                })
1276                .await;
1277                state.response_bytes = state.response_bytes.saturating_add(audit.response_bytes());
1278                state.oauth_code = Some(Box::new(audit));
1279                match acquired {
1280                    Ok(acquired) => {
1281                        // Provider-declared lifetime narrows use; reaching it
1282                        // is not an observed server-side expiration decision.
1283                        deadline = earliest_deadline(
1284                            session_deadline,
1285                            tokio::time::Instant::now().checked_add(acquired.declared_lifetime),
1286                        );
1287                        oauth_receipt = Some(Box::new(acquired.receipt));
1288                        oauth_refresh_token = acquired.refresh_token.map(Box::new);
1289                        acquired.credential
1290                    },
1291                    Err(outcome) => {
1292                        state.outcome = outcome;
1293                        state.add_not_dispatched(&resources);
1294                        return state.finish(policy);
1295                    },
1296                }
1297            },
1298        };
1299        let dispatch_context = DispatchContext {
1300            authority,
1301            requests: &requests,
1302            credential: &credential,
1303            credential_acquisition: oauth_receipt.as_deref(),
1304            credential_renewal: oauth_refresh_receipt.as_deref(),
1305            credential_epoch: state.epoch,
1306            policy: &policy,
1307            deadline,
1308        };
1309        let resource_commit_context = ResourceCommitContext {
1310            policy: &policy,
1311            knowledge: authority.knowledge(),
1312            observer: resource_observer,
1313        };
1314
1315        let startup = dispatch(
1316            &dispatch_context,
1317            &health,
1318            state.dispatch_budget(),
1319            DecisionActionOrigin::Bootstrap,
1320            SuppliedSessionRequestPurpose::Health,
1321        )
1322        .await;
1323        match state.record_health(
1324            startup,
1325            &health,
1326            SuppliedSessionHealthCheckpointPhase::Startup,
1327            0,
1328            &policy,
1329            authority.knowledge(),
1330        ) {
1331            Continue::Yes => state.mark_login_authenticated(),
1332            Continue::No(outcome) => {
1333                state.outcome = outcome;
1334                state.add_not_dispatched(&resources);
1335                return state.finish(policy);
1336            },
1337        }
1338
1339        for (index, (descriptor, reference)) in resources.iter().enumerate() {
1340            let dispatch_context = DispatchContext {
1341                authority,
1342                requests: &requests,
1343                credential: &credential,
1344                credential_acquisition: oauth_receipt.as_deref(),
1345                credential_renewal: oauth_refresh_receipt.as_deref(),
1346                credential_epoch: state.epoch,
1347                policy: &policy,
1348                deadline,
1349            };
1350            let sequence = u8::try_from(index).unwrap_or(u8::MAX);
1351            let resource = dispatch(
1352                &dispatch_context,
1353                descriptor,
1354                state.dispatch_budget(),
1355                DecisionActionOrigin::Planned,
1356                SuppliedSessionRequestPurpose::Resource,
1357            )
1358            .await;
1359            let staged = match state.record_resource(
1360                sequence,
1361                descriptor,
1362                reference,
1363                resource,
1364                resource_commit_context,
1365            ) {
1366                ResourceContinuation::AwaitingHealth(staged) => staged,
1367                ResourceContinuation::No(outcome) => {
1368                    state.outcome = outcome;
1369                    state.add_not_dispatched(&resources[index + 1..]);
1370                    return state.finish(policy);
1371                },
1372            };
1373
1374            let phase = if index + 1 == resources.len() {
1375                SuppliedSessionHealthCheckpointPhase::Terminal
1376            } else {
1377                SuppliedSessionHealthCheckpointPhase::SubjectBoundary
1378            };
1379            let health_result = dispatch(
1380                &dispatch_context,
1381                &health,
1382                state.dispatch_budget_with_pending_resource(staged),
1383                DecisionActionOrigin::Planned,
1384                SuppliedSessionRequestPurpose::Health,
1385            )
1386            .await;
1387            let after_subject_count = u8::try_from(index + 1).unwrap_or(u8::MAX);
1388            match state.record_health(
1389                health_result,
1390                &health,
1391                phase,
1392                after_subject_count,
1393                &policy,
1394                authority.knowledge(),
1395            ) {
1396                Continue::Yes => {
1397                    if let Continue::No(outcome) = state.record_health_qualified_resource(
1398                        sequence,
1399                        descriptor,
1400                        reference,
1401                        staged,
1402                        resource_commit_context,
1403                    ) {
1404                        state.outcome = outcome;
1405                        state.add_not_dispatched(&resources[index + 1..]);
1406                        return state.finish(policy);
1407                    }
1408                },
1409                Continue::No(outcome) => {
1410                    state.record_health_unqualified_resource(
1411                        sequence,
1412                        descriptor,
1413                        reference,
1414                        staged,
1415                        &policy,
1416                        authority.knowledge(),
1417                    );
1418                    if let Some(observer) = resource_observer {
1419                        observer.discard_response(sequence);
1420                    }
1421                    state.outcome = outcome;
1422                    state.add_not_dispatched(&resources[index + 1..]);
1423                    return state.finish(policy);
1424                },
1425            }
1426            if policy.renewal_after_resource_count() == Some(after_subject_count) {
1427                let renewal = SuppliedSessionRequestDescriptor::renewal(&policy)
1428                    .expect("V4 policy owns the validated renewal descriptor");
1429                let result = dispatch(
1430                    &dispatch_context,
1431                    &renewal,
1432                    state.dispatch_budget(),
1433                    DecisionActionOrigin::Planned,
1434                    SuppliedSessionRequestPurpose::Renewal,
1435                )
1436                .await;
1437                let replacement = match state.record_renewal(result, &renewal, &policy) {
1438                    Ok(replacement) => replacement,
1439                    Err(outcome) => {
1440                        state.outcome = outcome;
1441                        state.add_not_dispatched(&resources[index + 1..]);
1442                        return state.finish(policy);
1443                    },
1444                };
1445                credential = replacement;
1446                let dispatch_context = DispatchContext {
1447                    authority,
1448                    requests: &requests,
1449                    credential: &credential,
1450                    credential_acquisition: oauth_receipt.as_deref(),
1451                    credential_renewal: oauth_refresh_receipt.as_deref(),
1452                    credential_epoch: state.epoch,
1453                    policy: &policy,
1454                    deadline,
1455                };
1456                let result = dispatch(
1457                    &dispatch_context,
1458                    &health,
1459                    state.dispatch_budget(),
1460                    DecisionActionOrigin::Planned,
1461                    SuppliedSessionRequestPurpose::Health,
1462                )
1463                .await;
1464                let previous_checkpoint_count = state.checkpoints.len();
1465                let continued = state.record_health(
1466                    result,
1467                    &health,
1468                    SuppliedSessionHealthCheckpointPhase::Renewal,
1469                    after_subject_count,
1470                    &policy,
1471                    authority.knowledge(),
1472                );
1473                let renewal_health = (state.checkpoints.len() > previous_checkpoint_count)
1474                    .then(|| state.checkpoints.pop())
1475                    .flatten();
1476                state
1477                    .renewal
1478                    .as_mut()
1479                    .expect("renewal is initialized")
1480                    .health = renewal_health;
1481                if let Continue::No(outcome) = continued {
1482                    state.outcome = outcome;
1483                    state.add_not_dispatched(&resources[index + 1..]);
1484                    return state.finish(policy);
1485                }
1486            }
1487            if policy
1488                .oauth_refresh_policy()
1489                .is_some_and(|refresh| refresh.after_resource_count() == after_subject_count)
1490            {
1491                let (
1492                    Some(idp_requests),
1493                    Some(refresh_descriptor),
1494                    Some(code),
1495                    Some(old_refresh),
1496                    Some(resource),
1497                    Some(boundary_health),
1498                ) = (
1499                    oauth_code_requests.as_deref(),
1500                    oauth_refresh_descriptor.as_deref(),
1501                    oauth_receipt.as_deref(),
1502                    oauth_refresh_token.take(),
1503                    state.resources.last(),
1504                    state.checkpoints.last(),
1505                )
1506                else {
1507                    if let Some(audit) = state.oauth_refresh.as_mut() {
1508                        audit.context_unavailable();
1509                    }
1510                    state.outcome = SuppliedSessionAuditOutcome::OAuthRefreshUnavailable;
1511                    state.add_not_dispatched(&resources[index + 1..]);
1512                    return state.finish(policy);
1513                };
1514                let refresh_context = OAuthRefreshContext {
1515                    authority,
1516                    requests: idp_requests,
1517                    descriptor: refresh_descriptor,
1518                    policy: &policy,
1519                    code,
1520                    resource,
1521                    health: boundary_health,
1522                    // Renewal may replace the credential lifetime, never this
1523                    // immutable assessment/policy wall-time bound.
1524                    deadline: session_deadline,
1525                    budget: state.dispatch_budget(),
1526                };
1527                let (audit, acquired) = boxed_dispatch_execution(|| {
1528                    oauth_refresh::execute_refresh(refresh_context, &old_refresh)
1529                })
1530                .await;
1531                // This phase has been spent even on ambiguous failure. Never
1532                // retain the old refresh token for fallback or another attempt.
1533                drop(old_refresh);
1534                state.response_bytes = state.response_bytes.saturating_add(audit.response_bytes());
1535                state.oauth_refresh = Some(Box::new(audit));
1536                match acquired {
1537                    Ok(acquired) => {
1538                        credential = acquired.credential;
1539                        oauth_refresh_receipt = Some(Box::new(acquired.receipt));
1540                        state.epoch = 2;
1541                        deadline = earliest_deadline(
1542                            session_deadline,
1543                            tokio::time::Instant::now().checked_add(acquired.declared_lifetime),
1544                        );
1545                    },
1546                    Err(outcome) => {
1547                        state.outcome = outcome;
1548                        state.add_not_dispatched(&resources[index + 1..]);
1549                        return state.finish(policy);
1550                    },
1551                }
1552                let renewed_context = DispatchContext {
1553                    authority,
1554                    requests: &requests,
1555                    credential: &credential,
1556                    credential_acquisition: oauth_receipt.as_deref(),
1557                    credential_renewal: oauth_refresh_receipt.as_deref(),
1558                    credential_epoch: state.epoch,
1559                    policy: &policy,
1560                    deadline,
1561                };
1562                let result = dispatch(
1563                    &renewed_context,
1564                    &health,
1565                    state.dispatch_budget(),
1566                    DecisionActionOrigin::Planned,
1567                    SuppliedSessionRequestPurpose::Health,
1568                )
1569                .await;
1570                let previous_count = state.checkpoints.len();
1571                let continued = state.record_health(
1572                    result,
1573                    &health,
1574                    SuppliedSessionHealthCheckpointPhase::Renewal,
1575                    after_subject_count,
1576                    &policy,
1577                    authority.knowledge(),
1578                );
1579                if state.checkpoints.len() > previous_count {
1580                    if let Some(health) = state.checkpoints.pop() {
1581                        state
1582                            .oauth_refresh
1583                            .as_mut()
1584                            .expect("selected refresh audit")
1585                            .set_health(health);
1586                    }
1587                }
1588                if let Continue::No(outcome) = continued {
1589                    state.outcome = outcome;
1590                    state.add_not_dispatched(&resources[index + 1..]);
1591                    return state.finish(policy);
1592                }
1593            }
1594        }
1595        state.outcome = SuppliedSessionAuditOutcome::Complete;
1596        state.finish(policy)
1597    }
1598
1599    /// Executes the first supplied-Authorization WebSocket integration as one
1600    /// fixed health window: startup health, one staged protected resource, one
1601    /// bounded WebSocket exchange, then terminal health. No cookie or login
1602    /// policy can enter this path and the credential is borrowed only at the
1603    /// handshake boundary.
1604    #[cfg(feature = "websocket-review")]
1605    pub(super) async fn execute_with_websocket(
1606        self,
1607        authority: &SharedWebRuntimeAuthority,
1608        websocket: &WebSocketReviewPolicy,
1609    ) -> (
1610        WebAssessmentSuppliedSessionAudit,
1611        WebAssessmentWebSocketReviewAudit,
1612    ) {
1613        let Self {
1614            policy,
1615            input,
1616            requests,
1617            login_page,
1618            login_submit,
1619            oauth_code_requests: _,
1620            oauth_code_descriptor: _,
1621            oauth_refresh_descriptor: _,
1622            health,
1623            resources,
1624        } = self;
1625        let selected_resource_count = u8::try_from(resources.len()).unwrap_or(u8::MAX);
1626        let timing = authority.start();
1627        let local_deadline = tokio::time::Instant::now()
1628            .checked_add(Duration::from_millis(policy.max_wall_time_ms()));
1629        let deadline = earliest_deadline(timing.deadline(), local_deadline);
1630        let mut state = ExecutionState::new(&policy, selected_resource_count);
1631
1632        if policy.version() != SuppliedSessionPolicyVersion::V1
1633            || policy.credential_mechanism()
1634                != SuppliedSessionCredentialMechanism::AuthorizationHeader
1635            || policy.resource_count() != 1
1636            || login_page.is_some()
1637            || login_submit.is_some()
1638            || resources.len() != 1
1639        {
1640            state.outcome = SuppliedSessionAuditOutcome::ResourceUnavailable;
1641            state.add_not_dispatched(&resources);
1642            let websocket_audit = supplied_session_websocket_not_run(
1643                websocket,
1644                &policy,
1645                &state,
1646                WebSocketReviewFailureKind::ParentAuthorityUnavailable,
1647            );
1648            return (state.finish(policy), websocket_audit);
1649        }
1650        let credential = match input {
1651            SuppliedSessionRuntimeInput::Ready(SuppliedSessionCredential::Authorization(value)) => {
1652                SuppliedSessionCredential::Authorization(value)
1653            },
1654            SuppliedSessionRuntimeInput::Ready(SuppliedSessionCredential::SuppliedCookies(_))
1655            | SuppliedSessionRuntimeInput::FormLogin(_)
1656            | SuppliedSessionRuntimeInput::OAuthCode(_)
1657            | SuppliedSessionRuntimeInput::OAuthAuthorization(_) => {
1658                state.outcome = SuppliedSessionAuditOutcome::ResourceUnavailable;
1659                state.add_not_dispatched(&resources);
1660                let websocket_audit = supplied_session_websocket_not_run(
1661                    websocket,
1662                    &policy,
1663                    &state,
1664                    WebSocketReviewFailureKind::ParentAuthorityUnavailable,
1665                );
1666                return (state.finish(policy), websocket_audit);
1667            },
1668        };
1669        let dispatch_context = DispatchContext {
1670            authority,
1671            requests: &requests,
1672            credential: &credential,
1673            credential_acquisition: None,
1674            credential_renewal: None,
1675            credential_epoch: 1,
1676            policy: &policy,
1677            deadline,
1678        };
1679        let resource_commit_context = ResourceCommitContext {
1680            policy: &policy,
1681            knowledge: authority.knowledge(),
1682            observer: None,
1683        };
1684
1685        let startup = dispatch(
1686            &dispatch_context,
1687            &health,
1688            state.dispatch_budget(),
1689            DecisionActionOrigin::Bootstrap,
1690            SuppliedSessionRequestPurpose::Health,
1691        )
1692        .await;
1693        match state.record_health(
1694            startup,
1695            &health,
1696            SuppliedSessionHealthCheckpointPhase::Startup,
1697            0,
1698            &policy,
1699            authority.knowledge(),
1700        ) {
1701            Continue::Yes => state.mark_login_authenticated(),
1702            Continue::No(outcome) => {
1703                state.outcome = outcome;
1704                state.add_not_dispatched(&resources);
1705                let websocket_audit = supplied_session_websocket_not_run(
1706                    websocket,
1707                    &policy,
1708                    &state,
1709                    WebSocketReviewFailureKind::ParentAuthorityUnavailable,
1710                );
1711                return (state.finish(policy), websocket_audit);
1712            },
1713        }
1714        let Some(startup_receipt) = state.checkpoints.last().and_then(|checkpoint| {
1715            CommittedSuppliedSessionHealthReceipt::from_checkpoint(
1716                &policy,
1717                checkpoint,
1718                SuppliedSessionHealthCheckpointPhase::Startup,
1719                0,
1720                authority.knowledge(),
1721            )
1722        }) else {
1723            state.outcome = SuppliedSessionAuditOutcome::ResourceUnavailable;
1724            state.add_not_dispatched(&resources);
1725            let websocket_audit = supplied_session_websocket_not_run(
1726                websocket,
1727                &policy,
1728                &state,
1729                WebSocketReviewFailureKind::ParentAuthorityUnavailable,
1730            );
1731            return (state.finish(policy), websocket_audit);
1732        };
1733
1734        let (descriptor, reference) = &resources[0];
1735        let resource = dispatch(
1736            &dispatch_context,
1737            descriptor,
1738            state.dispatch_budget(),
1739            DecisionActionOrigin::Planned,
1740            SuppliedSessionRequestPurpose::Resource,
1741        )
1742        .await;
1743        let staged = match state.record_resource(
1744            0,
1745            descriptor,
1746            reference,
1747            resource,
1748            resource_commit_context,
1749        ) {
1750            ResourceContinuation::AwaitingHealth(staged) => staged,
1751            ResourceContinuation::No(outcome) => {
1752                state.outcome = outcome;
1753                let websocket_audit = supplied_session_websocket_not_run(
1754                    websocket,
1755                    &policy,
1756                    &state,
1757                    WebSocketReviewFailureKind::ParentAuthorityUnavailable,
1758                );
1759                return (state.finish(policy), websocket_audit);
1760            },
1761        };
1762
1763        let authorization = credential
1764            .authorization()
1765            .expect("combined V1 credential was matched above");
1766        let permit = SuppliedSessionWebSocketPermit::new(
1767            &policy,
1768            authorization,
1769            websocket,
1770            deadline,
1771            startup_receipt,
1772        );
1773        let mut websocket_audit =
1774            execute_supplied_session_websocket_review(authority, websocket, &permit).await;
1775
1776        let terminal = dispatch(
1777            &dispatch_context,
1778            &health,
1779            state.dispatch_budget_with_pending_resource(staged),
1780            DecisionActionOrigin::Planned,
1781            SuppliedSessionRequestPurpose::Health,
1782        )
1783        .await;
1784        let health_qualification = match state.record_health(
1785            terminal,
1786            &health,
1787            SuppliedSessionHealthCheckpointPhase::Terminal,
1788            1,
1789            &policy,
1790            authority.knowledge(),
1791        ) {
1792            Continue::Yes => {
1793                let terminal_receipt = state.checkpoints.last().and_then(|checkpoint| {
1794                    CommittedSuppliedSessionHealthReceipt::from_checkpoint(
1795                        &policy,
1796                        checkpoint,
1797                        SuppliedSessionHealthCheckpointPhase::Terminal,
1798                        1,
1799                        authority.knowledge(),
1800                    )
1801                });
1802                if terminal_receipt.is_none() {
1803                    state.record_health_unqualified_resource(
1804                        0,
1805                        descriptor,
1806                        reference,
1807                        staged,
1808                        &policy,
1809                        authority.knowledge(),
1810                    );
1811                    state.outcome = SuppliedSessionAuditOutcome::ResourceUnavailable;
1812                    WebSocketSuppliedSessionQualification::ContextUnqualified
1813                } else if let Continue::No(outcome) = state.record_health_qualified_resource(
1814                    0,
1815                    descriptor,
1816                    reference,
1817                    staged,
1818                    resource_commit_context,
1819                ) {
1820                    state.outcome = outcome;
1821                    WebSocketSuppliedSessionQualification::ContextQualified
1822                } else {
1823                    state.outcome = SuppliedSessionAuditOutcome::Complete;
1824                    WebSocketSuppliedSessionQualification::ContextQualified
1825                }
1826            },
1827            Continue::No(outcome) => {
1828                state.record_health_unqualified_resource(
1829                    0,
1830                    descriptor,
1831                    reference,
1832                    staged,
1833                    &policy,
1834                    authority.knowledge(),
1835                );
1836                state.outcome = outcome;
1837                WebSocketSuppliedSessionQualification::ContextUnqualified
1838            },
1839        };
1840        let qualification = if websocket_audit.request_admitted_count() == 0 {
1841            WebSocketSuppliedSessionQualification::NotEstablished
1842        } else {
1843            health_qualification
1844        };
1845        websocket_audit.bind_supplied_session_context(supplied_session_websocket_context(
1846            &policy,
1847            &state,
1848            qualification,
1849        ));
1850        debug_assert!(websocket_audit.is_consistent());
1851        (state.finish(policy), websocket_audit)
1852    }
1853}
1854
1855#[cfg(feature = "websocket-review")]
1856fn supplied_session_websocket_not_run(
1857    websocket: &WebSocketReviewPolicy,
1858    policy: &SuppliedSessionPolicy,
1859    state: &ExecutionState,
1860    failure: WebSocketReviewFailureKind,
1861) -> WebAssessmentWebSocketReviewAudit {
1862    let mut audit = websocket_review_not_run(websocket, failure);
1863    audit.bind_supplied_session_context(supplied_session_websocket_context(
1864        policy,
1865        state,
1866        WebSocketSuppliedSessionQualification::NotEstablished,
1867    ));
1868    debug_assert!(audit.is_consistent());
1869    audit
1870}
1871
1872#[cfg(feature = "websocket-review")]
1873fn supplied_session_websocket_context(
1874    policy: &SuppliedSessionPolicy,
1875    state: &ExecutionState,
1876    qualification: WebSocketSuppliedSessionQualification,
1877) -> WebSocketSuppliedSessionContextAudit {
1878    let health = |phase| {
1879        state
1880            .checkpoints
1881            .iter()
1882            .find(|checkpoint| checkpoint.phase() == phase)
1883            .map(|checkpoint| {
1884                WebSocketSuppliedSessionHealthAudit::new(
1885                    health_outcome_token(checkpoint.outcome()),
1886                    checkpoint.evidence_reference(),
1887                )
1888            })
1889    };
1890    WebSocketSuppliedSessionContextAudit::new(
1891        policy.policy_reference(),
1892        policy.application_reference(),
1893        policy.principal_reference(),
1894        policy.principal_alias(),
1895        health(SuppliedSessionHealthCheckpointPhase::Startup),
1896        health(SuppliedSessionHealthCheckpointPhase::Terminal),
1897        qualification,
1898    )
1899}
1900
1901struct ExecutionState {
1902    checkpoints: Vec<WebAssessmentSuppliedSessionCheckpointAudit>,
1903    resources: Vec<WebAssessmentSuppliedSessionResourceAudit>,
1904    login: Option<SuppliedSessionLoginAudit>,
1905    outcome: SuppliedSessionAuditOutcome,
1906    selected_resource_count: u8,
1907    response_bytes: u64,
1908    epoch: u8,
1909    renewal: Option<SuppliedSessionRenewalAudit>,
1910    oauth_code: Option<Box<SuppliedSessionOAuthCodeAudit>>,
1911    oauth_refresh: Option<Box<SuppliedSessionOAuthRefreshAudit>>,
1912    oauth_authorization: Option<Box<SuppliedSessionOAuthAuthorizationAudit>>,
1913}
1914
1915#[derive(Clone, Copy)]
1916struct ResourceCommitContext<'a> {
1917    policy: &'a SuppliedSessionPolicy,
1918    knowledge: &'a KnowledgeBase,
1919    observer: Option<&'a dyn SuppliedSessionCommittedResourceObserver>,
1920}
1921
1922impl ExecutionState {
1923    fn new(policy: &SuppliedSessionPolicy, selected_resource_count: u8) -> Self {
1924        debug_assert!(policy.resource_count() <= MAX_SUPPLIED_SESSION_RESOURCES);
1925        Self {
1926            checkpoints: Vec::with_capacity(MAX_SUPPLIED_SESSION_CHECKPOINTS),
1927            resources: Vec::with_capacity(MAX_SUPPLIED_SESSION_RESOURCES),
1928            login: None,
1929            outcome: SuppliedSessionAuditOutcome::RuntimeLimit,
1930            selected_resource_count,
1931            response_bytes: 0,
1932            epoch: 1,
1933            oauth_code: SuppliedSessionOAuthCodeAudit::selected(policy).map(Box::new),
1934            oauth_refresh: SuppliedSessionOAuthRefreshAudit::selected(policy).map(Box::new),
1935            oauth_authorization: SuppliedSessionOAuthAuthorizationAudit::selected(policy)
1936                .map(Box::new),
1937            renewal: policy
1938                .renewal_after_resource_count()
1939                .map(|after_resource_count| SuppliedSessionRenewalAudit {
1940                    renewal_reference: SuppliedSessionRequestDescriptor::renewal(policy)
1941                        .expect("validated V4 descriptor")
1942                        .target_reference()
1943                        .to_owned(),
1944                    after_resource_count,
1945                    attempt_count: 0,
1946                    status: None,
1947                    body_state: SuppliedSessionBodyState::Unavailable,
1948                    outcome: SuppliedSessionLoginSubmitOutcome::NotDispatched,
1949                    response_bytes: 0,
1950                    applied_cookie_count: 0,
1951                    initial_epoch: 1,
1952                    final_epoch: 1,
1953                    health: None,
1954                    cookie_update: None,
1955                }),
1956        }
1957    }
1958
1959    fn begin_login(
1960        &mut self,
1961        policy: &SuppliedSessionPolicy,
1962        descriptor: &SuppliedSessionRequestDescriptor,
1963    ) {
1964        debug_assert_eq!(policy.version(), SuppliedSessionPolicyVersion::V3);
1965        debug_assert_eq!(
1966            descriptor.purpose(),
1967            SuppliedSessionRequestPurpose::LoginPage
1968        );
1969        self.login = Some(SuppliedSessionLoginAudit {
1970            login_reference: descriptor.target_reference().to_owned(),
1971            max_attempts: policy.max_login_attempts().unwrap_or(0),
1972            attempt_count: 0,
1973            page_dispatched: false,
1974            page_status: None,
1975            page_body_state: SuppliedSessionBodyState::Unavailable,
1976            form_outcome: SuppliedSessionLoginFormOutcome::NotEvaluated,
1977            form_error_code: None,
1978            submit_dispatched: false,
1979            submit_status: None,
1980            submit_body_state: SuppliedSessionBodyState::Unavailable,
1981            submit_outcome: SuppliedSessionLoginSubmitOutcome::NotDispatched,
1982            acquired_cookie_count: 0,
1983            response_bytes: 0,
1984            initial_epoch: 0,
1985            final_epoch: 0,
1986            pre_session_cookie_applied: false,
1987        });
1988    }
1989
1990    fn record_login_page(
1991        &mut self,
1992        dispatch: DispatchResult,
1993        policy: &SuppliedSessionPolicy,
1994    ) -> Result<SuppliedSessionLoginCsrfToken, SuppliedSessionAuditOutcome> {
1995        let login = self
1996            .login
1997            .as_mut()
1998            .expect("V3 login state is initialized before page dispatch");
1999        match dispatch {
2000            DispatchResult::Response {
2001                response,
2002                bytes,
2003                exact_target,
2004                cookie_update: _,
2005            } => {
2006                login.page_dispatched = true;
2007                login.page_status = Some(response.status());
2008                login.page_body_state = if response.body_complete() {
2009                    SuppliedSessionBodyState::Complete
2010                } else {
2011                    SuppliedSessionBodyState::Incomplete
2012                };
2013                login.response_bytes = login.response_bytes.saturating_add(bytes);
2014                self.response_bytes = self.response_bytes.saturating_add(bytes);
2015                if !exact_target || response.supplied_session_has_set_cookie_fields() {
2016                    login.form_outcome = SuppliedSessionLoginFormOutcome::IneligibleResponse;
2017                    login.form_error_code = Some(if exact_target {
2018                        "pre_session_cookie_unsupported"
2019                    } else {
2020                        "inexact_final_target"
2021                    });
2022                    return Err(SuppliedSessionAuditOutcome::LoginFormUnavailable);
2023                }
2024                match extract_supplied_session_login_csrf(&response, policy) {
2025                    Ok(token) => {
2026                        login.form_outcome = SuppliedSessionLoginFormOutcome::Matched;
2027                        Ok(token)
2028                    },
2029                    Err(error) => {
2030                        login.form_outcome = login_form_outcome(error);
2031                        login.form_error_code = Some(error.as_str());
2032                        Err(SuppliedSessionAuditOutcome::LoginFormUnavailable)
2033                    },
2034                }
2035            },
2036            DispatchResult::Transport { bytes, dispatched } => {
2037                login.page_dispatched = dispatched;
2038                login.form_outcome = SuppliedSessionLoginFormOutcome::TransportFailed;
2039                login.form_error_code = Some("transport_failed");
2040                login.response_bytes = login.response_bytes.saturating_add(bytes);
2041                self.response_bytes = self.response_bytes.saturating_add(bytes);
2042                Err(SuppliedSessionAuditOutcome::LoginFormUnavailable)
2043            },
2044            DispatchResult::RuntimeLimit { bytes, dispatched } => {
2045                login.page_dispatched = dispatched;
2046                login.form_outcome = SuppliedSessionLoginFormOutcome::RuntimeLimit;
2047                login.form_error_code = Some("runtime_limit");
2048                login.response_bytes = login.response_bytes.saturating_add(bytes);
2049                self.response_bytes = self.response_bytes.saturating_add(bytes);
2050                Err(SuppliedSessionAuditOutcome::RuntimeLimit)
2051            },
2052            DispatchResult::Cancelled { bytes, dispatched } => {
2053                login.page_dispatched = dispatched;
2054                login.form_outcome = SuppliedSessionLoginFormOutcome::Cancelled;
2055                login.form_error_code = Some("cancelled");
2056                login.response_bytes = login.response_bytes.saturating_add(bytes);
2057                self.response_bytes = self.response_bytes.saturating_add(bytes);
2058                Err(SuppliedSessionAuditOutcome::Cancelled)
2059            },
2060            DispatchResult::InvalidDescriptor => {
2061                login.form_outcome = SuppliedSessionLoginFormOutcome::Invalid;
2062                login.form_error_code = Some("invalid_descriptor");
2063                Err(SuppliedSessionAuditOutcome::LoginFormUnavailable)
2064            },
2065        }
2066    }
2067
2068    fn mark_login_form_body_invalid(&mut self) {
2069        if let Some(login) = self.login.as_mut() {
2070            login.form_outcome = SuppliedSessionLoginFormOutcome::Invalid;
2071            login.form_error_code = Some("invalid_form_body");
2072        }
2073    }
2074
2075    fn record_login_submit(
2076        &mut self,
2077        dispatch: DispatchResult,
2078        policy: &SuppliedSessionPolicy,
2079    ) -> Result<SuppliedSessionCredential, SuppliedSessionAuditOutcome> {
2080        let login = self
2081            .login
2082            .as_mut()
2083            .expect("V3 login state is initialized before submit dispatch");
2084        match dispatch {
2085            DispatchResult::Response {
2086                response,
2087                bytes,
2088                exact_target,
2089                cookie_update: _,
2090            } => {
2091                login.submit_dispatched = true;
2092                login.attempt_count = 1;
2093                login.submit_status = Some(response.status());
2094                login.submit_body_state = if response.body_complete() {
2095                    SuppliedSessionBodyState::Complete
2096                } else {
2097                    SuppliedSessionBodyState::Incomplete
2098                };
2099                login.response_bytes = login.response_bytes.saturating_add(bytes);
2100                self.response_bytes = self.response_bytes.saturating_add(bytes);
2101                if !exact_target || (300..400).contains(&response.status()) {
2102                    login.submit_outcome = SuppliedSessionLoginSubmitOutcome::RedirectRefused;
2103                    return Err(SuppliedSessionAuditOutcome::LoginSubmitUnavailable);
2104                }
2105                if !response.body_complete() {
2106                    login.submit_outcome = SuppliedSessionLoginSubmitOutcome::Incomplete;
2107                    return Err(SuppliedSessionAuditOutcome::LoginSubmitUnavailable);
2108                }
2109                if response.status() != 200 {
2110                    login.submit_outcome = SuppliedSessionLoginSubmitOutcome::HttpError;
2111                    return Err(SuppliedSessionAuditOutcome::LoginSubmitUnavailable);
2112                }
2113                login.submit_outcome = SuppliedSessionLoginSubmitOutcome::CookieUnavailable;
2114                let now = unix_time_seconds()
2115                    .ok_or(SuppliedSessionAuditOutcome::LoginCookieUnavailable)?;
2116                let cookies = response
2117                    .supplied_session_form_login_cookies(policy, now)
2118                    .map_err(|_| SuppliedSessionAuditOutcome::LoginCookieUnavailable)?;
2119                login.acquired_cookie_count =
2120                    u8::try_from(cookies.cookie_count()).unwrap_or(u8::MAX);
2121                login.submit_outcome = SuppliedSessionLoginSubmitOutcome::CookieAcquired;
2122                Ok(SuppliedSessionCredential::SuppliedCookies(cookies))
2123            },
2124            DispatchResult::Transport { bytes, dispatched } => {
2125                login.submit_dispatched = dispatched;
2126                login.attempt_count = u8::from(dispatched);
2127                login.submit_outcome = SuppliedSessionLoginSubmitOutcome::TransportFailed;
2128                login.response_bytes = login.response_bytes.saturating_add(bytes);
2129                self.response_bytes = self.response_bytes.saturating_add(bytes);
2130                Err(SuppliedSessionAuditOutcome::LoginSubmitUnavailable)
2131            },
2132            DispatchResult::RuntimeLimit { bytes, dispatched } => {
2133                login.submit_dispatched = dispatched;
2134                login.attempt_count = u8::from(dispatched);
2135                login.submit_outcome = SuppliedSessionLoginSubmitOutcome::RuntimeLimit;
2136                login.response_bytes = login.response_bytes.saturating_add(bytes);
2137                self.response_bytes = self.response_bytes.saturating_add(bytes);
2138                Err(SuppliedSessionAuditOutcome::RuntimeLimit)
2139            },
2140            DispatchResult::Cancelled { bytes, dispatched } => {
2141                login.submit_dispatched = dispatched;
2142                login.attempt_count = u8::from(dispatched);
2143                login.submit_outcome = SuppliedSessionLoginSubmitOutcome::Cancelled;
2144                login.response_bytes = login.response_bytes.saturating_add(bytes);
2145                self.response_bytes = self.response_bytes.saturating_add(bytes);
2146                Err(SuppliedSessionAuditOutcome::Cancelled)
2147            },
2148            DispatchResult::InvalidDescriptor => {
2149                login.submit_outcome = SuppliedSessionLoginSubmitOutcome::NotDispatched;
2150                Err(SuppliedSessionAuditOutcome::LoginSubmitUnavailable)
2151            },
2152        }
2153    }
2154
2155    fn mark_login_authenticated(&mut self) {
2156        if let Some(login) = self.login.as_mut() {
2157            login.final_epoch = 1;
2158        }
2159    }
2160
2161    fn record_renewal(
2162        &mut self,
2163        dispatch: DispatchResult,
2164        descriptor: &SuppliedSessionRequestDescriptor,
2165        policy: &SuppliedSessionPolicy,
2166    ) -> Result<SuppliedSessionCredential, SuppliedSessionAuditOutcome> {
2167        let renewal = self
2168            .renewal
2169            .as_mut()
2170            .ok_or(SuppliedSessionAuditOutcome::RenewalUnavailable)?;
2171        if renewal.attempt_count != 0 || !descriptor.validate_against(policy) {
2172            return Err(SuppliedSessionAuditOutcome::RenewalUnavailable);
2173        }
2174        let (bytes, dispatched, outcome) = match dispatch {
2175            DispatchResult::Response {
2176                response,
2177                bytes,
2178                exact_target,
2179                cookie_update,
2180            } => {
2181                renewal.attempt_count = 1;
2182                renewal.response_bytes = bytes;
2183                renewal.status = Some(response.status());
2184                renewal.body_state = if response.body_complete() {
2185                    SuppliedSessionBodyState::Complete
2186                } else {
2187                    SuppliedSessionBodyState::Incomplete
2188                };
2189                renewal.cookie_update = cookie_update;
2190                renewal.outcome = if (300..400).contains(&response.status()) {
2191                    SuppliedSessionLoginSubmitOutcome::RedirectRefused
2192                } else if !response.body_complete() {
2193                    SuppliedSessionLoginSubmitOutcome::Incomplete
2194                } else if response.status() != 200 {
2195                    SuppliedSessionLoginSubmitOutcome::HttpError
2196                } else {
2197                    SuppliedSessionLoginSubmitOutcome::CookieUnavailable
2198                };
2199                self.response_bytes = self.response_bytes.saturating_add(bytes);
2200                if !exact_target
2201                    || !cookie_update.is_some_and(|update| {
2202                        update.kind == SuppliedSessionResponseCookieUpdateKind::Selected
2203                    })
2204                {
2205                    return Err(SuppliedSessionAuditOutcome::RenewalUnavailable);
2206                }
2207                let now =
2208                    unix_time_seconds().ok_or(SuppliedSessionAuditOutcome::RenewalUnavailable)?;
2209                let cookies = response
2210                    .supplied_session_renewal_cookies(policy, now)
2211                    .map_err(|_| SuppliedSessionAuditOutcome::RenewalUnavailable)?;
2212                renewal.applied_cookie_count =
2213                    u8::try_from(cookies.cookie_count()).unwrap_or(u8::MAX);
2214                renewal.outcome = SuppliedSessionLoginSubmitOutcome::CookieAcquired;
2215                renewal.final_epoch = 2;
2216                self.epoch = 2;
2217                return Ok(SuppliedSessionCredential::SuppliedCookies(cookies));
2218            },
2219            DispatchResult::Transport { bytes, dispatched } => (
2220                bytes,
2221                dispatched,
2222                SuppliedSessionAuditOutcome::RenewalUnavailable,
2223            ),
2224            DispatchResult::RuntimeLimit { bytes, dispatched } => {
2225                (bytes, dispatched, SuppliedSessionAuditOutcome::RuntimeLimit)
2226            },
2227            DispatchResult::Cancelled { bytes, dispatched } => {
2228                (bytes, dispatched, SuppliedSessionAuditOutcome::Cancelled)
2229            },
2230            DispatchResult::InvalidDescriptor => {
2231                (0, false, SuppliedSessionAuditOutcome::RenewalUnavailable)
2232            },
2233        };
2234        renewal.attempt_count = u8::from(dispatched);
2235        renewal.outcome = match outcome {
2236            SuppliedSessionAuditOutcome::RuntimeLimit => {
2237                SuppliedSessionLoginSubmitOutcome::RuntimeLimit
2238            },
2239            SuppliedSessionAuditOutcome::Cancelled => SuppliedSessionLoginSubmitOutcome::Cancelled,
2240            _ if dispatched => SuppliedSessionLoginSubmitOutcome::TransportFailed,
2241            _ => SuppliedSessionLoginSubmitOutcome::NotDispatched,
2242        };
2243        renewal.response_bytes = bytes;
2244        self.response_bytes = self.response_bytes.saturating_add(bytes);
2245        Err(outcome)
2246    }
2247
2248    fn dispatched_request_count(&self) -> u8 {
2249        let dispatched_resources = self
2250            .resources
2251            .iter()
2252            .filter(|resource| resource.outcome != SuppliedSessionResourceOutcome::NotDispatched)
2253            .count();
2254        let login_requests = self.login.as_ref().map_or(0, |login| {
2255            u8::from(login.page_dispatched).saturating_add(u8::from(login.submit_dispatched))
2256        });
2257        u8::try_from(self.checkpoints.len())
2258            .unwrap_or(u8::MAX)
2259            .saturating_add(u8::try_from(dispatched_resources).unwrap_or(u8::MAX))
2260            .saturating_add(login_requests)
2261            .saturating_add(
2262                self.oauth_code
2263                    .as_ref()
2264                    .map_or(0, |audit| audit.request_admitted_count()),
2265            )
2266            .saturating_add(self.renewal.as_ref().map_or(0, |renewal| {
2267                renewal
2268                    .attempt_count
2269                    .saturating_add(u8::from(renewal.health.is_some()))
2270            }))
2271            .saturating_add(self.oauth_refresh.as_ref().map_or(0, |refresh| {
2272                refresh
2273                    .request_admitted_count()
2274                    .saturating_add(u8::from(refresh.health().is_some()))
2275            }))
2276    }
2277
2278    fn dispatch_budget(&self) -> DispatchBudget {
2279        DispatchBudget {
2280            accounted_response_bytes: self.response_bytes,
2281            dispatched_requests: self.dispatched_request_count(),
2282        }
2283    }
2284
2285    fn dispatch_budget_with_pending_resource(
2286        &self,
2287        staged: StagedSuppliedSessionResource,
2288    ) -> DispatchBudget {
2289        DispatchBudget {
2290            accounted_response_bytes: self.response_bytes.saturating_add(staged.response_bytes),
2291            dispatched_requests: self.dispatched_request_count().saturating_add(1),
2292        }
2293    }
2294
2295    fn record_health(
2296        &mut self,
2297        dispatch: DispatchResult,
2298        descriptor: &SuppliedSessionRequestDescriptor,
2299        phase: SuppliedSessionHealthCheckpointPhase,
2300        after_subject_count: u8,
2301        policy: &SuppliedSessionPolicy,
2302        knowledge: &KnowledgeBase,
2303    ) -> Continue {
2304        let is_startup = phase == SuppliedSessionHealthCheckpointPhase::Startup;
2305        let sequence = u8::try_from(self.checkpoints.len()).unwrap_or(u8::MAX);
2306        let (record, stop) = match dispatch {
2307            DispatchResult::Response {
2308                response,
2309                bytes,
2310                exact_target,
2311                cookie_update,
2312            } => {
2313                let mut record = classify_health(
2314                    *response,
2315                    bytes,
2316                    exact_target,
2317                    cookie_update,
2318                    HealthCheckpointPosition {
2319                        sequence,
2320                        phase,
2321                        after_subject_count,
2322                    },
2323                    policy,
2324                );
2325                record.epoch = matches!(
2326                    policy.version(),
2327                    SuppliedSessionPolicyVersion::V4 | SuppliedSessionPolicyVersion::V6
2328                )
2329                .then_some(self.epoch);
2330                bind_health_evidence(
2331                    &mut record,
2332                    policy,
2333                    descriptor,
2334                    Some(exact_target),
2335                    knowledge,
2336                );
2337                let stop = cookie_update_stop_outcome(record.cookie_update)
2338                    .or_else(|| health_stop_outcome(record.outcome, is_startup));
2339                (Some(record), stop)
2340            },
2341            DispatchResult::Transport { bytes, dispatched } => {
2342                let record = dispatched.then(|| {
2343                    committed_health_unavailable(
2344                        policy,
2345                        descriptor,
2346                        sequence,
2347                        phase,
2348                        after_subject_count,
2349                        bytes,
2350                        knowledge,
2351                    )
2352                });
2353                (
2354                    record,
2355                    Some(SuppliedSessionAuditOutcome::ResourceUnavailable),
2356                )
2357            },
2358            DispatchResult::RuntimeLimit { bytes, dispatched } => {
2359                let record = dispatched.then(|| {
2360                    committed_health_unavailable(
2361                        policy,
2362                        descriptor,
2363                        sequence,
2364                        phase,
2365                        after_subject_count,
2366                        bytes,
2367                        knowledge,
2368                    )
2369                });
2370                (record, Some(SuppliedSessionAuditOutcome::RuntimeLimit))
2371            },
2372            DispatchResult::Cancelled { bytes, dispatched } => {
2373                let record = dispatched.then(|| {
2374                    committed_health_unavailable(
2375                        policy,
2376                        descriptor,
2377                        sequence,
2378                        phase,
2379                        after_subject_count,
2380                        bytes,
2381                        knowledge,
2382                    )
2383                });
2384                (record, Some(SuppliedSessionAuditOutcome::Cancelled))
2385            },
2386            DispatchResult::InvalidDescriptor => {
2387                (None, Some(SuppliedSessionAuditOutcome::ResourceUnavailable))
2388            },
2389        };
2390        if let Some(mut record) = record {
2391            if matches!(
2392                policy.version(),
2393                SuppliedSessionPolicyVersion::V4 | SuppliedSessionPolicyVersion::V6
2394            ) && record.epoch != Some(self.epoch)
2395            {
2396                record.epoch = Some(self.epoch);
2397                bind_health_evidence(&mut record, policy, descriptor, None, knowledge);
2398            }
2399            self.response_bytes = self.response_bytes.saturating_add(record.response_bytes);
2400            self.checkpoints.push(record);
2401        }
2402        stop.map_or(Continue::Yes, Continue::No)
2403    }
2404
2405    fn record_resource(
2406        &mut self,
2407        sequence: u8,
2408        descriptor: &SuppliedSessionRequestDescriptor,
2409        reference: &str,
2410        dispatch: DispatchResult,
2411        context: ResourceCommitContext<'_>,
2412    ) -> ResourceContinuation {
2413        let (mut record, stop) = match dispatch {
2414            DispatchResult::Response {
2415                response,
2416                bytes,
2417                exact_target,
2418                cookie_update,
2419            } => {
2420                let mut staged =
2421                    stage_resource_response(&response, bytes, exact_target, cookie_update);
2422                staged.epoch = self.epoch;
2423                if let Some(outcome) = cookie_update_stop_outcome(staged.cookie_update) {
2424                    let record = commit_health_unqualified_resource(
2425                        sequence,
2426                        descriptor,
2427                        reference,
2428                        context.policy,
2429                        context.knowledge,
2430                        staged,
2431                        None,
2432                    );
2433                    self.response_bytes = self.response_bytes.saturating_add(record.response_bytes);
2434                    self.resources.push(record);
2435                    return ResourceContinuation::No(outcome);
2436                }
2437                if staged_resource_can_await_health(descriptor, reference, staged) {
2438                    if let Some(observer) = context.observer {
2439                        if observer
2440                            .stage_response(sequence, context.policy, descriptor, &response)
2441                            .is_err()
2442                        {
2443                            observer.fail();
2444                        }
2445                    }
2446                    return ResourceContinuation::AwaitingHealth(staged);
2447                }
2448                let record = commit_staged_resource(
2449                    sequence,
2450                    descriptor,
2451                    reference,
2452                    context.policy,
2453                    context.knowledge,
2454                    staged,
2455                    None,
2456                );
2457                let outcome = record.outcome;
2458                let stop = (outcome != SuppliedSessionResourceOutcome::Committed)
2459                    .then_some(SuppliedSessionAuditOutcome::ResourceUnavailable);
2460                (record, stop)
2461            },
2462            DispatchResult::Transport { bytes, dispatched } => {
2463                if !dispatched {
2464                    self.resources
2465                        .push(resource_not_dispatched(sequence, reference));
2466                    return ResourceContinuation::No(
2467                        SuppliedSessionAuditOutcome::ResourceUnavailable,
2468                    );
2469                }
2470                (
2471                    resource_transport_failure(sequence, reference, bytes),
2472                    Some(SuppliedSessionAuditOutcome::ResourceUnavailable),
2473                )
2474            },
2475            DispatchResult::RuntimeLimit { bytes, dispatched } => {
2476                if !dispatched {
2477                    self.resources
2478                        .push(resource_not_dispatched(sequence, reference));
2479                    return ResourceContinuation::No(SuppliedSessionAuditOutcome::RuntimeLimit);
2480                }
2481                (
2482                    resource_transport_failure(sequence, reference, bytes),
2483                    Some(SuppliedSessionAuditOutcome::RuntimeLimit),
2484                )
2485            },
2486            DispatchResult::Cancelled { bytes, dispatched } => {
2487                if !dispatched {
2488                    self.resources
2489                        .push(resource_not_dispatched(sequence, reference));
2490                    return ResourceContinuation::No(SuppliedSessionAuditOutcome::Cancelled);
2491                }
2492                (
2493                    resource_transport_failure(sequence, reference, bytes),
2494                    Some(SuppliedSessionAuditOutcome::Cancelled),
2495                )
2496            },
2497            DispatchResult::InvalidDescriptor => {
2498                self.resources
2499                    .push(resource_not_dispatched(sequence, reference));
2500                return ResourceContinuation::No(SuppliedSessionAuditOutcome::ResourceUnavailable);
2501            },
2502        };
2503        if context.policy.version() == SuppliedSessionPolicyVersion::V6 {
2504            record.epoch = self.epoch;
2505        }
2506        self.response_bytes = self.response_bytes.saturating_add(record.response_bytes);
2507        self.resources.push(record);
2508        ResourceContinuation::No(stop.unwrap_or(SuppliedSessionAuditOutcome::ResourceUnavailable))
2509    }
2510
2511    fn record_health_qualified_resource(
2512        &mut self,
2513        sequence: u8,
2514        descriptor: &SuppliedSessionRequestDescriptor,
2515        reference: &str,
2516        staged: StagedSuppliedSessionResource,
2517        context: ResourceCommitContext<'_>,
2518    ) -> Continue {
2519        let qualifying_checkpoint = self.checkpoints.last().and_then(|checkpoint| {
2520            (checkpoint.outcome == SuppliedSessionHealthOutcome::Healthy
2521                && checkpoint.after_subject_count == sequence.saturating_add(1))
2522            .then_some(checkpoint.evidence_reference.as_deref())
2523            .flatten()
2524        });
2525        let (record, receipt) = commit_staged_resource_with_receipt(
2526            sequence,
2527            descriptor,
2528            reference,
2529            context.policy,
2530            context.knowledge,
2531            staged,
2532            qualifying_checkpoint,
2533        );
2534        let committed = record.outcome == SuppliedSessionResourceOutcome::Committed;
2535        if let (Some(observer), Some(receipt)) = (context.observer, receipt.as_ref()) {
2536            if observer
2537                .commit_response(
2538                    sequence,
2539                    context.policy,
2540                    descriptor,
2541                    receipt,
2542                    context.knowledge,
2543                )
2544                .is_err()
2545            {
2546                observer.fail();
2547            }
2548        } else if let Some(observer) = context.observer {
2549            observer.discard_response(sequence);
2550        }
2551        self.response_bytes = self.response_bytes.saturating_add(record.response_bytes);
2552        self.resources.push(record);
2553        if committed {
2554            Continue::Yes
2555        } else {
2556            Continue::No(SuppliedSessionAuditOutcome::ResourceUnavailable)
2557        }
2558    }
2559
2560    fn record_health_unqualified_resource(
2561        &mut self,
2562        sequence: u8,
2563        descriptor: &SuppliedSessionRequestDescriptor,
2564        reference: &str,
2565        staged: StagedSuppliedSessionResource,
2566        policy: &SuppliedSessionPolicy,
2567        knowledge: &KnowledgeBase,
2568    ) {
2569        let checkpoint_reference = self.checkpoints.last().and_then(|checkpoint| {
2570            (checkpoint.after_subject_count == sequence.saturating_add(1))
2571                .then_some(checkpoint.evidence_reference.as_deref())
2572                .flatten()
2573        });
2574        let record = commit_health_unqualified_resource(
2575            sequence,
2576            descriptor,
2577            reference,
2578            policy,
2579            knowledge,
2580            staged,
2581            checkpoint_reference,
2582        );
2583        self.response_bytes = self.response_bytes.saturating_add(record.response_bytes);
2584        self.resources.push(record);
2585    }
2586
2587    fn add_not_dispatched(&mut self, resources: &[(SuppliedSessionRequestDescriptor, String)]) {
2588        for (_, reference) in resources {
2589            let sequence = u8::try_from(self.resources.len()).unwrap_or(u8::MAX);
2590            self.resources
2591                .push(resource_not_dispatched(sequence, reference));
2592        }
2593    }
2594
2595    fn finish(mut self, policy: SuppliedSessionPolicy) -> WebAssessmentSuppliedSessionAudit {
2596        if let Some(boundary) = policy.renewal_after_resource_count() {
2597            for resource in &mut self.resources {
2598                resource.epoch = if self.epoch == 2 && resource.sequence >= boundary {
2599                    2
2600                } else {
2601                    1
2602                };
2603            }
2604        }
2605        if policy.version() == SuppliedSessionPolicyVersion::V6 {
2606            // Actual observed rows already carry their acquisition epoch. Do
2607            // not rewrite them to hide a bad transition. Only undispatched
2608            // declarations inherit the current planned context.
2609            for resource in &mut self.resources {
2610                if resource.outcome == SuppliedSessionResourceOutcome::NotDispatched {
2611                    resource.epoch = if self.epoch == 2 && resource.sequence >= 1 {
2612                        2
2613                    } else {
2614                        1
2615                    };
2616                }
2617            }
2618        }
2619        let response_byte_limit = policy.max_total_response_bytes();
2620        let cookie_policy = policy
2621            .cookie_summary()
2622            .map(|summary| cookie_policy_audit(summary, &policy));
2623        let cookie_lifecycle = cookie_policy.is_some().then(|| {
2624            let mut lifecycle = cookie_lifecycle_audit(
2625                &self.checkpoints,
2626                &self.resources,
2627                self.login.as_ref(),
2628                self.renewal.as_ref(),
2629            );
2630            if let Some(renewal) = self.renewal.as_ref() {
2631                lifecycle.final_epoch = renewal.final_epoch;
2632                lifecycle.updates_applied = u8::from(renewal.applied_cookie_count > 0);
2633            }
2634            lifecycle
2635        });
2636        let dispatched_resource_count = u8::try_from(
2637            self.resources
2638                .iter()
2639                .filter(|resource| {
2640                    resource.outcome != SuppliedSessionResourceOutcome::NotDispatched
2641                })
2642                .count(),
2643        )
2644        .unwrap_or(u8::MAX);
2645        let committed_resource_count = u8::try_from(
2646            self.resources
2647                .iter()
2648                .filter(|resource| resource.outcome == SuppliedSessionResourceOutcome::Committed)
2649                .count(),
2650        )
2651        .unwrap_or(u8::MAX);
2652        let coverage = if self.outcome == SuppliedSessionAuditOutcome::Complete
2653            && committed_resource_count == self.selected_resource_count
2654        {
2655            SuppliedSessionCoverage::Complete
2656        } else if committed_resource_count == 0 {
2657            SuppliedSessionCoverage::None
2658        } else {
2659            SuppliedSessionCoverage::Partial
2660        };
2661        let dispatched_request_count = self.dispatched_request_count();
2662        debug_assert_eq!(
2663            self.resources.len(),
2664            usize::from(self.selected_resource_count)
2665        );
2666        debug_assert!(self
2667            .checkpoints
2668            .iter()
2669            .enumerate()
2670            .all(
2671                |(index, checkpoint)| checkpoint.sequence == u8::try_from(index).unwrap_or(u8::MAX)
2672            ));
2673        debug_assert!(self.checkpoints.iter().all(|checkpoint| {
2674            checkpoint.outcome == SuppliedSessionHealthOutcome::Indeterminate
2675                || checkpoint.evidence_reference.is_some()
2676        }));
2677        debug_assert!(self
2678            .resources
2679            .iter()
2680            .enumerate()
2681            .all(|(index, resource)| resource.sequence == u8::try_from(index).unwrap_or(u8::MAX)));
2682        debug_assert!(self.resources.iter().all(|resource| {
2683            matches!(
2684                resource.outcome,
2685                SuppliedSessionResourceOutcome::Committed
2686                    | SuppliedSessionResourceOutcome::HealthUnqualified
2687            ) == resource.evidence_reference.is_some()
2688        }));
2689        debug_assert_eq!(
2690            self.response_bytes,
2691            self.login
2692                .as_ref()
2693                .map_or(0, SuppliedSessionLoginAudit::response_bytes)
2694                .saturating_add(
2695                    self.oauth_code
2696                        .as_ref()
2697                        .map_or(0, |audit| audit.response_bytes())
2698                )
2699                .saturating_add(
2700                    self.checkpoints
2701                        .iter()
2702                        .map(WebAssessmentSuppliedSessionCheckpointAudit::response_bytes)
2703                        .sum::<u64>()
2704                )
2705                .saturating_add(
2706                    self.resources
2707                        .iter()
2708                        .map(WebAssessmentSuppliedSessionResourceAudit::response_bytes)
2709                        .sum::<u64>()
2710                )
2711                .saturating_add(self.renewal.as_ref().map_or(0, |renewal| {
2712                    renewal.response_bytes.saturating_add(
2713                        renewal
2714                            .health
2715                            .as_ref()
2716                            .map_or(0, |health| health.response_bytes),
2717                    )
2718                }))
2719                .saturating_add(self.oauth_refresh.as_ref().map_or(0, |refresh| {
2720                    refresh.response_bytes().saturating_add(
2721                        refresh.health().map_or(0, |health| health.response_bytes()),
2722                    )
2723                }))
2724        );
2725        let oauth_token_exchange = if policy.version() == SuppliedSessionPolicyVersion::V7 {
2726            self.oauth_code.take().map(|audit| {
2727                Box::new(SuppliedSessionOAuthInitiatedTokenAudit::from_exchange(
2728                    *audit,
2729                ))
2730            })
2731        } else {
2732            None
2733        };
2734        WebAssessmentSuppliedSessionAudit {
2735            schema: match policy.version() {
2736                SuppliedSessionPolicyVersion::V1 => SUPPLIED_SESSION_AUDIT_SCHEMA,
2737                SuppliedSessionPolicyVersion::V2 => SUPPLIED_SESSION_COOKIE_AUDIT_SCHEMA,
2738                SuppliedSessionPolicyVersion::V3 => SUPPLIED_SESSION_FORM_LOGIN_AUDIT_SCHEMA,
2739                SuppliedSessionPolicyVersion::V4 => SUPPLIED_SESSION_RENEWAL_AUDIT_SCHEMA,
2740                SuppliedSessionPolicyVersion::V5 => SUPPLIED_SESSION_OAUTH_CODE_AUDIT_SCHEMA,
2741                SuppliedSessionPolicyVersion::V6 => SUPPLIED_SESSION_OAUTH_REFRESH_AUDIT_SCHEMA,
2742                SuppliedSessionPolicyVersion::V7 => {
2743                    SUPPLIED_SESSION_OAUTH_AUTHORIZATION_AUDIT_SCHEMA
2744                },
2745            },
2746            capability_id: SUPPLIED_SESSION_CAPABILITY_ID,
2747            policy_reference: policy.policy_reference().to_owned(),
2748            application_reference: policy.application_reference().to_owned(),
2749            principal_reference: policy.principal_reference().to_owned(),
2750            principal_alias: policy.principal_alias().to_owned(),
2751            principal_assurance: SuppliedSessionPrincipalAssurance::OperatorDeclared,
2752            credential_mechanism: policy.credential_mechanism(),
2753            credential_acquisition: policy.credential_acquisition(),
2754            health_oracle: SuppliedSessionHealthOracleAudit {
2755                kind: SuppliedSessionHealthOracleKind::JsonBooleanTrue,
2756                field_reference: policy.health_field_reference().to_owned(),
2757            },
2758            outcome: self.outcome,
2759            coverage,
2760            checkpoints: self.checkpoints,
2761            resources: self.resources,
2762            selected_resource_count: self.selected_resource_count,
2763            dispatched_resource_count,
2764            committed_resource_count,
2765            dispatched_request_count,
2766            response_bytes: self.response_bytes,
2767            response_byte_limit,
2768            response_byte_limit_exceeded: self.response_bytes > response_byte_limit,
2769            refresh_performed: self
2770                .renewal
2771                .as_ref()
2772                .is_some_and(|renewal| renewal.applied_cookie_count > 0)
2773                || self
2774                    .oauth_refresh
2775                    .as_ref()
2776                    .is_some_and(|refresh| refresh.credential_commit_count() == 1),
2777            anonymous_fallback_performed: false,
2778            continuous_authentication_established: false,
2779            exploit_execution_performed: false,
2780            impact_validation_performed: false,
2781            cookie_policy,
2782            cookie_lifecycle,
2783            login: self.login,
2784            renewal: self.renewal,
2785            oauth_code: self.oauth_code,
2786            oauth_refresh: self.oauth_refresh,
2787            oauth_authorization: self.oauth_authorization,
2788            oauth_token_exchange,
2789        }
2790    }
2791}
2792
2793fn cookie_policy_audit(
2794    summary: SuppliedSessionCookieSummary,
2795    policy: &SuppliedSessionPolicy,
2796) -> SuppliedSessionCookiePolicyAudit {
2797    SuppliedSessionCookiePolicyAudit {
2798        declared_count: u8::try_from(summary.cookie_count()).unwrap_or(u8::MAX),
2799        host_only_count: u8::try_from(summary.host_only_count()).unwrap_or(u8::MAX),
2800        domain_count: u8::try_from(summary.domain_count()).unwrap_or(u8::MAX),
2801        secure_count: u8::try_from(summary.secure_count()).unwrap_or(u8::MAX),
2802        http_only_count: u8::try_from(summary.http_only_count()).unwrap_or(u8::MAX),
2803        session_count: u8::try_from(summary.session_count()).unwrap_or(u8::MAX),
2804        persistent_count: u8::try_from(summary.persistent_count()).unwrap_or(u8::MAX),
2805        same_site_missing_count: u8::try_from(summary.same_site_missing_count()).unwrap_or(u8::MAX),
2806        same_site_strict_count: u8::try_from(summary.same_site_strict_count()).unwrap_or(u8::MAX),
2807        same_site_lax_count: u8::try_from(summary.same_site_lax_count()).unwrap_or(u8::MAX),
2808        same_site_none_count: u8::try_from(summary.same_site_none_count()).unwrap_or(u8::MAX),
2809        update_policy: policy
2810            .cookie_update_policy()
2811            .expect("cookie policy summary requires an update policy"),
2812    }
2813}
2814
2815fn cookie_lifecycle_audit(
2816    checkpoints: &[WebAssessmentSuppliedSessionCheckpointAudit],
2817    resources: &[WebAssessmentSuppliedSessionResourceAudit],
2818    login: Option<&SuppliedSessionLoginAudit>,
2819    renewal: Option<&SuppliedSessionRenewalAudit>,
2820) -> SuppliedSessionCookieLifecycleAudit {
2821    let updates = checkpoints
2822        .iter()
2823        .filter_map(|record| record.cookie_update)
2824        .chain(resources.iter().filter_map(|record| record.cookie_update))
2825        .chain(renewal.and_then(|renewal| renewal.cookie_update))
2826        .chain(
2827            renewal
2828                .and_then(|renewal| renewal.health.as_ref())
2829                .and_then(|health| health.cookie_update),
2830        );
2831    let mut selected_update_response_count = 0_u8;
2832    let mut unselected_update_response_count = 0_u8;
2833    let mut update_classification_failure_count = 0_u8;
2834    for update in updates {
2835        if update.selected_count > 0 {
2836            selected_update_response_count = selected_update_response_count.saturating_add(1);
2837        }
2838        if update.unselected_count > 0 {
2839            unselected_update_response_count = unselected_update_response_count.saturating_add(1);
2840        }
2841        if update.kind == SuppliedSessionResponseCookieUpdateKind::Unusable {
2842            update_classification_failure_count =
2843                update_classification_failure_count.saturating_add(1);
2844        }
2845    }
2846    SuppliedSessionCookieLifecycleAudit {
2847        initial_epoch: login.map_or(1, SuppliedSessionLoginAudit::initial_epoch),
2848        final_epoch: login.map_or(1, SuppliedSessionLoginAudit::final_epoch),
2849        selected_update_response_count,
2850        unselected_update_response_count,
2851        update_classification_failure_count,
2852        updates_applied: 0,
2853    }
2854}
2855
2856fn health_stop_outcome(
2857    outcome: SuppliedSessionHealthOutcome,
2858    is_startup: bool,
2859) -> Option<SuppliedSessionAuditOutcome> {
2860    match outcome {
2861        SuppliedSessionHealthOutcome::Healthy => None,
2862        SuppliedSessionHealthOutcome::Unhealthy if is_startup => {
2863            Some(SuppliedSessionAuditOutcome::StartupUnhealthy)
2864        },
2865        SuppliedSessionHealthOutcome::Unhealthy => Some(SuppliedSessionAuditOutcome::SessionLost),
2866        SuppliedSessionHealthOutcome::Indeterminate => {
2867            Some(SuppliedSessionAuditOutcome::ResourceUnavailable)
2868        },
2869    }
2870}
2871
2872enum Continue {
2873    Yes,
2874    No(SuppliedSessionAuditOutcome),
2875}
2876
2877enum ResourceContinuation {
2878    AwaitingHealth(StagedSuppliedSessionResource),
2879    No(SuppliedSessionAuditOutcome),
2880}
2881
2882enum DispatchResult {
2883    Response {
2884        response: Box<CollectedHttpResponse>,
2885        bytes: u64,
2886        exact_target: bool,
2887        cookie_update: Option<SuppliedSessionResponseCookieUpdate>,
2888    },
2889    Transport {
2890        bytes: u64,
2891        dispatched: bool,
2892    },
2893    RuntimeLimit {
2894        bytes: u64,
2895        dispatched: bool,
2896    },
2897    Cancelled {
2898        bytes: u64,
2899        dispatched: bool,
2900    },
2901    InvalidDescriptor,
2902}
2903
2904struct DispatchContext<'a> {
2905    authority: &'a SharedWebRuntimeAuthority,
2906    requests: &'a HttpRequestBroker,
2907    credential: &'a SuppliedSessionCredential,
2908    credential_acquisition: Option<&'a CommittedOAuthCodeReceipt>,
2909    credential_renewal: Option<&'a CommittedOAuthRefreshReceipt>,
2910    credential_epoch: u8,
2911    policy: &'a SuppliedSessionPolicy,
2912    deadline: Option<tokio::time::Instant>,
2913}
2914
2915struct LoginDispatchContext<'a> {
2916    authority: &'a SharedWebRuntimeAuthority,
2917    requests: &'a HttpRequestBroker,
2918    policy: &'a SuppliedSessionPolicy,
2919    deadline: Option<tokio::time::Instant>,
2920}
2921
2922#[derive(Clone, Copy)]
2923struct DispatchBudget {
2924    accounted_response_bytes: u64,
2925    dispatched_requests: u8,
2926}
2927
2928#[derive(Clone, Copy)]
2929struct HealthCheckpointPosition {
2930    sequence: u8,
2931    phase: SuppliedSessionHealthCheckpointPhase,
2932    after_subject_count: u8,
2933}
2934
2935async fn dispatch_login_page(
2936    context: &LoginDispatchContext<'_>,
2937    descriptor: &SuppliedSessionRequestDescriptor,
2938    budget: DispatchBudget,
2939) -> DispatchResult {
2940    if descriptor.purpose() != SuppliedSessionRequestPurpose::LoginPage {
2941        return DispatchResult::InvalidDescriptor;
2942    }
2943    let Some(limits) = login_dispatch_limits(context, budget) else {
2944        return if context.authority.cancellation().is_cancelled() {
2945            DispatchResult::Cancelled {
2946                bytes: 0,
2947                dispatched: false,
2948            }
2949        } else {
2950            DispatchResult::RuntimeLimit {
2951                bytes: 0,
2952                dispatched: false,
2953            }
2954        };
2955    };
2956    let before = context.authority.request_accounting().snapshot();
2957    let execution = context
2958        .requests
2959        .collect_supplied_session_login_page_for_runtime(
2960            DecisionExecutionStage::Passive,
2961            Some(DecisionActionOrigin::Bootstrap),
2962            limits,
2963            descriptor,
2964            context.policy,
2965        );
2966    let result = super::await_execution(
2967        context.authority.cancellation(),
2968        context.deadline,
2969        execution,
2970    )
2971    .await;
2972    login_dispatch_result(context, descriptor, before, result)
2973}
2974
2975async fn dispatch_login_submit(
2976    context: &LoginDispatchContext<'_>,
2977    descriptor: &SuppliedSessionRequestDescriptor,
2978    body: &SuppliedSessionFormBody,
2979    budget: DispatchBudget,
2980) -> DispatchResult {
2981    if descriptor.purpose() != SuppliedSessionRequestPurpose::LoginSubmit {
2982        return DispatchResult::InvalidDescriptor;
2983    }
2984    let Some(limits) = login_dispatch_limits(context, budget) else {
2985        return if context.authority.cancellation().is_cancelled() {
2986            DispatchResult::Cancelled {
2987                bytes: 0,
2988                dispatched: false,
2989            }
2990        } else {
2991            DispatchResult::RuntimeLimit {
2992                bytes: 0,
2993                dispatched: false,
2994            }
2995        };
2996    };
2997    let before = context.authority.request_accounting().snapshot();
2998    let execution = context
2999        .requests
3000        .collect_supplied_session_login_submit_for_runtime(
3001            DecisionExecutionStage::Active,
3002            None,
3003            limits,
3004            descriptor,
3005            context.policy,
3006            body,
3007        );
3008    let result = super::await_execution(
3009        context.authority.cancellation(),
3010        context.deadline,
3011        execution,
3012    )
3013    .await;
3014    login_dispatch_result(context, descriptor, before, result)
3015}
3016
3017fn login_dispatch_limits(
3018    context: &LoginDispatchContext<'_>,
3019    budget: DispatchBudget,
3020) -> Option<DecisionExecutionLimits> {
3021    if context.authority.cancellation().is_cancelled()
3022        || context
3023            .deadline
3024            .is_some_and(|deadline| tokio::time::Instant::now() >= deadline)
3025        || budget.accounted_response_bytes >= context.policy.max_total_response_bytes()
3026        || budget.dispatched_requests >= context.policy.max_session_requests()
3027    {
3028        return None;
3029    }
3030    let remaining = context
3031        .policy
3032        .max_total_response_bytes()
3033        .saturating_sub(budget.accounted_response_bytes);
3034    let response_limit =
3035        remaining.min(u64::try_from(context.policy.max_response_body_bytes()).unwrap_or(u64::MAX));
3036    (response_limit > 0)
3037        .then(|| DecisionExecutionLimits::new().with_max_response_body_bytes(response_limit))
3038}
3039
3040fn login_dispatch_result(
3041    context: &LoginDispatchContext<'_>,
3042    descriptor: &SuppliedSessionRequestDescriptor,
3043    before: crate::runtime_budget::RequestAccountingSnapshot,
3044    result: RuntimeExecution<Result<CollectedHttpResponse, SuppliedSessionRequestError>>,
3045) -> DispatchResult {
3046    let after = context.authority.request_accounting().snapshot();
3047    let dispatched = after.total_requests() > before.total_requests();
3048    let bytes = after
3049        .response_bytes()
3050        .saturating_sub(before.response_bytes());
3051    match result {
3052        RuntimeExecution::Completed(Ok(response)) => DispatchResult::Response {
3053            exact_target: response.final_url() == descriptor.target(),
3054            response: Box::new(response),
3055            bytes,
3056            cookie_update: None,
3057        },
3058        RuntimeExecution::Completed(Err(SuppliedSessionRequestError::RuntimeLimit)) => {
3059            DispatchResult::RuntimeLimit { bytes, dispatched }
3060        },
3061        RuntimeExecution::Completed(Err(SuppliedSessionRequestError::Http)) => {
3062            DispatchResult::Transport { bytes, dispatched }
3063        },
3064        RuntimeExecution::Completed(Err(SuppliedSessionRequestError::InvalidDescriptor)) => {
3065            DispatchResult::InvalidDescriptor
3066        },
3067        RuntimeExecution::Cancelled => DispatchResult::Cancelled { bytes, dispatched },
3068        RuntimeExecution::WallTimeExceeded => DispatchResult::RuntimeLimit { bytes, dispatched },
3069    }
3070}
3071
3072// Construct the selected broker future outside dispatch's poll frame. The
3073// one-shot factory runs immediately and retains no independent execution.
3074#[inline(never)]
3075fn boxed_dispatch_execution<F: std::future::Future>(
3076    make: impl FnOnce() -> F,
3077) -> std::pin::Pin<Box<F>> {
3078    Box::pin(make())
3079}
3080
3081async fn dispatch(
3082    context: &DispatchContext<'_>,
3083    descriptor: &SuppliedSessionRequestDescriptor,
3084    budget: DispatchBudget,
3085    origin: DecisionActionOrigin,
3086    expected_purpose: SuppliedSessionRequestPurpose,
3087) -> DispatchResult {
3088    if descriptor.purpose() != expected_purpose {
3089        return DispatchResult::InvalidDescriptor;
3090    }
3091    let acquisition_is_valid = if context.policy.version() == SuppliedSessionPolicyVersion::V6 {
3092        context.credential_acquisition.is_some_and(|code| {
3093            code.is_valid_for(context.policy, context.authority.knowledge())
3094                && match context.credential_epoch {
3095                    1 => context.credential_renewal.is_none(),
3096                    2 => context.credential_renewal.is_some_and(|renewed| {
3097                        renewed.is_valid_for(context.policy, code, context.authority.knowledge())
3098                    }),
3099                    _ => false,
3100                }
3101        })
3102    } else if matches!(
3103        context.policy.version(),
3104        SuppliedSessionPolicyVersion::V5 | SuppliedSessionPolicyVersion::V7
3105    ) {
3106        context.credential_acquisition.is_some_and(|receipt| {
3107            context.credential_epoch == 1
3108                && context.credential_renewal.is_none()
3109                && receipt.is_valid_for(context.policy, context.authority.knowledge())
3110        })
3111    } else {
3112        context.credential_acquisition.is_none() && context.credential_renewal.is_none()
3113    };
3114    if !acquisition_is_valid {
3115        return DispatchResult::InvalidDescriptor;
3116    }
3117    if context.authority.cancellation().is_cancelled() {
3118        return DispatchResult::Cancelled {
3119            bytes: 0,
3120            dispatched: false,
3121        };
3122    }
3123    if context
3124        .deadline
3125        .is_some_and(|deadline| tokio::time::Instant::now() >= deadline)
3126        || budget.accounted_response_bytes >= context.policy.max_total_response_bytes()
3127        || budget.dispatched_requests >= context.policy.max_session_requests()
3128    {
3129        return DispatchResult::RuntimeLimit {
3130            bytes: 0,
3131            dispatched: false,
3132        };
3133    }
3134    let remaining = context
3135        .policy
3136        .max_total_response_bytes()
3137        .saturating_sub(budget.accounted_response_bytes);
3138    let response_limit =
3139        remaining.min(u64::try_from(context.policy.max_response_body_bytes()).unwrap_or(u64::MAX));
3140    if response_limit == 0 {
3141        return DispatchResult::RuntimeLimit {
3142            bytes: 0,
3143            dispatched: false,
3144        };
3145    }
3146    let limits = DecisionExecutionLimits::new().with_max_response_body_bytes(response_limit);
3147    let before = context.authority.request_accounting().snapshot();
3148    let execution = boxed_dispatch_execution(|| async {
3149        if expected_purpose == SuppliedSessionRequestPurpose::Renewal {
3150            context
3151                .requests
3152                .collect_supplied_session_renewal_for_runtime(
3153                    limits,
3154                    descriptor,
3155                    context.policy,
3156                    context.credential,
3157                )
3158                .await
3159        } else {
3160            context
3161                .requests
3162                .collect_supplied_session_get_for_runtime(
3163                    DecisionExecutionStage::Passive,
3164                    Some(origin),
3165                    limits,
3166                    descriptor,
3167                    context.policy,
3168                    context.credential,
3169                )
3170                .await
3171        }
3172    });
3173    let result = super::await_execution(
3174        context.authority.cancellation(),
3175        context.deadline,
3176        execution,
3177    )
3178    .await;
3179    let after = context.authority.request_accounting().snapshot();
3180    let dispatched = after.total_requests() > before.total_requests();
3181    let bytes = after
3182        .response_bytes()
3183        .saturating_sub(before.response_bytes());
3184    match result {
3185        RuntimeExecution::Completed(Ok(response)) => {
3186            let exact_target = response.final_url() == descriptor.target();
3187            let cookie_update = response_cookie_update(context.policy, &response, exact_target);
3188            DispatchResult::Response {
3189                response: Box::new(response),
3190                bytes,
3191                exact_target,
3192                cookie_update,
3193            }
3194        },
3195        RuntimeExecution::Completed(Err(SuppliedSessionRequestError::RuntimeLimit)) => {
3196            DispatchResult::RuntimeLimit { bytes, dispatched }
3197        },
3198        RuntimeExecution::Completed(Err(SuppliedSessionRequestError::Http)) => {
3199            DispatchResult::Transport { bytes, dispatched }
3200        },
3201        RuntimeExecution::Completed(Err(SuppliedSessionRequestError::InvalidDescriptor)) => {
3202            DispatchResult::InvalidDescriptor
3203        },
3204        RuntimeExecution::Cancelled => DispatchResult::Cancelled { bytes, dispatched },
3205        RuntimeExecution::WallTimeExceeded => DispatchResult::RuntimeLimit { bytes, dispatched },
3206    }
3207}
3208
3209fn cookie_update_stop_outcome(
3210    update: Option<SuppliedSessionResponseCookieUpdate>,
3211) -> Option<SuppliedSessionAuditOutcome> {
3212    match update.map(|value| value.kind) {
3213        Some(SuppliedSessionResponseCookieUpdateKind::Selected) => {
3214            Some(SuppliedSessionAuditOutcome::CredentialUpdateRequired)
3215        },
3216        Some(SuppliedSessionResponseCookieUpdateKind::Unusable) => {
3217            Some(SuppliedSessionAuditOutcome::CredentialUpdateUnusable)
3218        },
3219        None
3220        | Some(SuppliedSessionResponseCookieUpdateKind::None)
3221        | Some(SuppliedSessionResponseCookieUpdateKind::UnselectedOnly) => None,
3222    }
3223}
3224
3225fn response_cookie_update(
3226    policy: &SuppliedSessionPolicy,
3227    response: &CollectedHttpResponse,
3228    exact_target: bool,
3229) -> Option<SuppliedSessionResponseCookieUpdate> {
3230    if !matches!(
3231        policy.version(),
3232        SuppliedSessionPolicyVersion::V2
3233            | SuppliedSessionPolicyVersion::V3
3234            | SuppliedSessionPolicyVersion::V4
3235    ) {
3236        return None;
3237    }
3238    if !exact_target {
3239        return Some(SuppliedSessionResponseCookieUpdate {
3240            kind: SuppliedSessionResponseCookieUpdateKind::Unusable,
3241            selected_count: 0,
3242            unselected_count: 0,
3243        });
3244    }
3245    Some(response.supplied_session_cookie_updates(policy).into())
3246}
3247
3248fn login_form_outcome(error: SuppliedSessionLoginFormError) -> SuppliedSessionLoginFormOutcome {
3249    match error {
3250        SuppliedSessionLoginFormError::IneligibleResponse => {
3251            SuppliedSessionLoginFormOutcome::IneligibleResponse
3252        },
3253        SuppliedSessionLoginFormError::MissingForm | SuppliedSessionLoginFormError::MissingCsrf => {
3254            SuppliedSessionLoginFormOutcome::Missing
3255        },
3256        SuppliedSessionLoginFormError::AmbiguousForm
3257        | SuppliedSessionLoginFormError::AmbiguousCsrf => {
3258            SuppliedSessionLoginFormOutcome::Ambiguous
3259        },
3260        SuppliedSessionLoginFormError::InvalidFormContract
3261        | SuppliedSessionLoginFormError::InvalidCsrf => SuppliedSessionLoginFormOutcome::Invalid,
3262    }
3263}
3264
3265fn unix_time_seconds() -> Option<i64> {
3266    std::time::SystemTime::now()
3267        .duration_since(std::time::UNIX_EPOCH)
3268        .ok()
3269        .and_then(|duration| i64::try_from(duration.as_secs()).ok())
3270}
3271
3272impl From<SuppliedSessionCookieUpdateObservation> for SuppliedSessionResponseCookieUpdate {
3273    fn from(observation: SuppliedSessionCookieUpdateObservation) -> Self {
3274        let kind = match observation.classification() {
3275            SuppliedSessionCookieUpdateClassification::None => {
3276                SuppliedSessionResponseCookieUpdateKind::None
3277            },
3278            SuppliedSessionCookieUpdateClassification::UnselectedOnly => {
3279                SuppliedSessionResponseCookieUpdateKind::UnselectedOnly
3280            },
3281            SuppliedSessionCookieUpdateClassification::Selected => {
3282                SuppliedSessionResponseCookieUpdateKind::Selected
3283            },
3284            SuppliedSessionCookieUpdateClassification::Unusable => {
3285                SuppliedSessionResponseCookieUpdateKind::Unusable
3286            },
3287        };
3288        Self {
3289            kind,
3290            selected_count: observation.selected_count(),
3291            unselected_count: observation.unselected_count(),
3292        }
3293    }
3294}
3295
3296fn earliest_deadline(
3297    parent: Option<tokio::time::Instant>,
3298    local: Option<tokio::time::Instant>,
3299) -> Option<tokio::time::Instant> {
3300    match (parent, local) {
3301        (Some(parent), Some(local)) => Some(parent.min(local)),
3302        (Some(parent), None) => Some(parent),
3303        (None, Some(local)) => Some(local),
3304        (None, None) => None,
3305    }
3306}
3307
3308fn classify_health(
3309    response: CollectedHttpResponse,
3310    bytes: u64,
3311    exact_target: bool,
3312    cookie_update: Option<SuppliedSessionResponseCookieUpdate>,
3313    position: HealthCheckpointPosition,
3314    policy: &SuppliedSessionPolicy,
3315) -> WebAssessmentSuppliedSessionCheckpointAudit {
3316    let status = response.status();
3317    let complete = response.body_complete();
3318    let body_state = if complete {
3319        SuppliedSessionBodyState::Complete
3320    } else {
3321        SuppliedSessionBodyState::Incomplete
3322    };
3323    let predicate =
3324        if exact_target && complete && status == 200 && response.has_json_compatible_media_type() {
3325            if strict_top_level_boolean(response.body(), policy.health_json_field()) == Some(true) {
3326                SuppliedSessionPredicateOutcome::Matched
3327            } else {
3328                SuppliedSessionPredicateOutcome::NotMatched
3329            }
3330        } else {
3331            SuppliedSessionPredicateOutcome::NotEvaluated
3332        };
3333    let outcome = if predicate == SuppliedSessionPredicateOutcome::Matched {
3334        SuppliedSessionHealthOutcome::Healthy
3335    } else if complete {
3336        SuppliedSessionHealthOutcome::Unhealthy
3337    } else {
3338        SuppliedSessionHealthOutcome::Indeterminate
3339    };
3340    WebAssessmentSuppliedSessionCheckpointAudit {
3341        sequence: position.sequence,
3342        evidence_reference: None,
3343        phase: position.phase,
3344        after_subject_count: position.after_subject_count,
3345        outcome,
3346        status: Some(status),
3347        body_state,
3348        predicate,
3349        response_bytes: bytes,
3350        epoch: None,
3351        cookie_update,
3352    }
3353}
3354
3355fn health_unavailable(
3356    sequence: u8,
3357    phase: SuppliedSessionHealthCheckpointPhase,
3358    after_subject_count: u8,
3359    bytes: u64,
3360) -> WebAssessmentSuppliedSessionCheckpointAudit {
3361    WebAssessmentSuppliedSessionCheckpointAudit {
3362        sequence,
3363        evidence_reference: None,
3364        phase,
3365        after_subject_count,
3366        outcome: SuppliedSessionHealthOutcome::Indeterminate,
3367        status: None,
3368        body_state: SuppliedSessionBodyState::Unavailable,
3369        predicate: SuppliedSessionPredicateOutcome::NotEvaluated,
3370        response_bytes: bytes,
3371        epoch: None,
3372        cookie_update: None,
3373    }
3374}
3375
3376fn committed_health_unavailable(
3377    policy: &SuppliedSessionPolicy,
3378    descriptor: &SuppliedSessionRequestDescriptor,
3379    sequence: u8,
3380    phase: SuppliedSessionHealthCheckpointPhase,
3381    after_subject_count: u8,
3382    response_bytes: u64,
3383    knowledge: &KnowledgeBase,
3384) -> WebAssessmentSuppliedSessionCheckpointAudit {
3385    let mut record = health_unavailable(sequence, phase, after_subject_count, response_bytes);
3386    bind_health_evidence(&mut record, policy, descriptor, None, knowledge);
3387    record
3388}
3389
3390fn bind_health_evidence(
3391    checkpoint: &mut WebAssessmentSuppliedSessionCheckpointAudit,
3392    policy: &SuppliedSessionPolicy,
3393    descriptor: &SuppliedSessionRequestDescriptor,
3394    exact_response_target: Option<bool>,
3395    knowledge: &KnowledgeBase,
3396) {
3397    checkpoint.evidence_reference = commit_health_evidence(
3398        policy,
3399        descriptor,
3400        checkpoint,
3401        exact_response_target,
3402        knowledge,
3403    );
3404    if checkpoint.evidence_reference.is_none() {
3405        checkpoint.outcome = SuppliedSessionHealthOutcome::Indeterminate;
3406        checkpoint.predicate = SuppliedSessionPredicateOutcome::NotEvaluated;
3407    }
3408}
3409
3410/// Commits a redaction-safe checkpoint receipt to the parent knowledge store.
3411/// A complete health predicate cannot qualify resource coverage unless this
3412/// transaction succeeds and the immutable record can be read back exactly.
3413fn commit_health_evidence(
3414    policy: &SuppliedSessionPolicy,
3415    descriptor: &SuppliedSessionRequestDescriptor,
3416    checkpoint: &WebAssessmentSuppliedSessionCheckpointAudit,
3417    exact_response_target: Option<bool>,
3418    knowledge: &KnowledgeBase,
3419) -> Option<String> {
3420    if !descriptor.validate()
3421        || descriptor.purpose() != SuppliedSessionRequestPurpose::Health
3422        || exact_response_target == Some(false)
3423    {
3424        return None;
3425    }
3426    let status = checkpoint
3427        .status
3428        .map_or_else(|| "unavailable".to_owned(), |status| status.to_string());
3429    let target_state = if exact_response_target == Some(true) {
3430        "exact"
3431    } else {
3432        "unavailable"
3433    };
3434    let mut binding = format!(
3435        "policy={};application={};health_field={};purpose=health;sequence={};phase={};after_subject_count={};target={target_state};status={status};body_state={};predicate={};outcome={};response_bytes={}",
3436        policy.policy_reference(),
3437        policy.application_reference(),
3438        policy.health_field_reference(),
3439        checkpoint.sequence,
3440        checkpoint_phase_token(checkpoint.phase),
3441        checkpoint.after_subject_count,
3442        body_state_token(checkpoint.body_state),
3443        predicate_token(checkpoint.predicate),
3444        health_outcome_token(checkpoint.outcome),
3445        checkpoint.response_bytes,
3446    );
3447    if matches!(
3448        policy.version(),
3449        SuppliedSessionPolicyVersion::V4 | SuppliedSessionPolicyVersion::V6
3450    ) {
3451        use std::fmt::Write as _;
3452        let _ = write!(binding, ";epoch={}", checkpoint.epoch());
3453    }
3454    append_cookie_update_binding(policy, &mut binding, checkpoint.cookie_update);
3455    commit_value_safe_evidence(
3456        policy,
3457        policy.health_field_reference(),
3458        HEALTH_COMMIT_EVIDENCE_DOMAIN,
3459        HEALTH_COMMIT_EVIDENCE_PREFIX,
3460        HEALTH_COMMIT_EVIDENCE_PREDICATE,
3461        health_commit_evidence_method(policy),
3462        binding,
3463        knowledge,
3464    )
3465}
3466
3467fn checkpoint_phase_token(phase: SuppliedSessionHealthCheckpointPhase) -> &'static str {
3468    match phase {
3469        SuppliedSessionHealthCheckpointPhase::Startup => "startup",
3470        SuppliedSessionHealthCheckpointPhase::SubjectBoundary => "subject_boundary",
3471        SuppliedSessionHealthCheckpointPhase::Terminal => "terminal",
3472        SuppliedSessionHealthCheckpointPhase::Renewal => "renewal",
3473    }
3474}
3475
3476fn health_outcome_token(outcome: SuppliedSessionHealthOutcome) -> &'static str {
3477    match outcome {
3478        SuppliedSessionHealthOutcome::Healthy => "healthy",
3479        SuppliedSessionHealthOutcome::Unhealthy => "unhealthy",
3480        SuppliedSessionHealthOutcome::Indeterminate => "indeterminate",
3481    }
3482}
3483
3484fn body_state_token(state: SuppliedSessionBodyState) -> &'static str {
3485    match state {
3486        SuppliedSessionBodyState::Complete => "complete",
3487        SuppliedSessionBodyState::Incomplete => "incomplete",
3488        SuppliedSessionBodyState::Unavailable => "unavailable",
3489    }
3490}
3491
3492fn predicate_token(predicate: SuppliedSessionPredicateOutcome) -> &'static str {
3493    match predicate {
3494        SuppliedSessionPredicateOutcome::Matched => "matched",
3495        SuppliedSessionPredicateOutcome::NotMatched => "not_matched",
3496        SuppliedSessionPredicateOutcome::NotEvaluated => "not_evaluated",
3497    }
3498}
3499
3500/// Body-free staging record awaiting the following health checkpoint.
3501///
3502/// The collected response is reduced immediately, so neither its body nor
3503/// headers can enter the shared knowledge base or saved audit. A complete 200
3504/// remains staged until its immediately following committed health checkpoint
3505/// qualifies it. Only then may it become authenticated `Committed` coverage.
3506#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3507struct StagedSuppliedSessionResource {
3508    status: u16,
3509    response_bytes: u64,
3510    exact_target: bool,
3511    body_complete: bool,
3512    cookie_update: Option<SuppliedSessionResponseCookieUpdate>,
3513    epoch: u8,
3514}
3515
3516fn stage_resource_response(
3517    response: &CollectedHttpResponse,
3518    response_bytes: u64,
3519    exact_target: bool,
3520    cookie_update: Option<SuppliedSessionResponseCookieUpdate>,
3521) -> StagedSuppliedSessionResource {
3522    StagedSuppliedSessionResource {
3523        status: response.status(),
3524        response_bytes,
3525        exact_target,
3526        body_complete: response.body_complete(),
3527        cookie_update,
3528        epoch: 1,
3529    }
3530}
3531
3532fn staged_resource_can_await_health(
3533    descriptor: &SuppliedSessionRequestDescriptor,
3534    expected_reference: &str,
3535    staged: StagedSuppliedSessionResource,
3536) -> bool {
3537    descriptor.validate()
3538        && descriptor.purpose() == SuppliedSessionRequestPurpose::Resource
3539        && descriptor.target_reference() == expected_reference
3540        && staged.exact_target
3541        && staged.body_complete
3542        && staged.status == 200
3543}
3544
3545fn commit_staged_resource(
3546    sequence: u8,
3547    descriptor: &SuppliedSessionRequestDescriptor,
3548    expected_reference: &str,
3549    policy: &SuppliedSessionPolicy,
3550    knowledge: &KnowledgeBase,
3551    staged: StagedSuppliedSessionResource,
3552    qualifying_checkpoint_reference: Option<&str>,
3553) -> WebAssessmentSuppliedSessionResourceAudit {
3554    commit_staged_resource_with_receipt(
3555        sequence,
3556        descriptor,
3557        expected_reference,
3558        policy,
3559        knowledge,
3560        staged,
3561        qualifying_checkpoint_reference,
3562    )
3563    .0
3564}
3565
3566fn commit_staged_resource_with_receipt(
3567    sequence: u8,
3568    descriptor: &SuppliedSessionRequestDescriptor,
3569    expected_reference: &str,
3570    policy: &SuppliedSessionPolicy,
3571    knowledge: &KnowledgeBase,
3572    staged: StagedSuppliedSessionResource,
3573    qualifying_checkpoint_reference: Option<&str>,
3574) -> (
3575    WebAssessmentSuppliedSessionResourceAudit,
3576    Option<CommittedSuppliedSessionResourceReceipt>,
3577) {
3578    let descriptor_is_exact = descriptor.validate()
3579        && descriptor.purpose() == SuppliedSessionRequestPurpose::Resource
3580        && descriptor.target_reference() == expected_reference;
3581    let (outcome, receipt) = if (300..400).contains(&staged.status) {
3582        (SuppliedSessionResourceOutcome::RedirectRefused, None)
3583    } else if !descriptor_is_exact || !staged.exact_target || !staged.body_complete {
3584        (SuppliedSessionResourceOutcome::Incomplete, None)
3585    } else if staged.status == 200 {
3586        match qualifying_checkpoint_reference.and_then(|checkpoint_reference| {
3587            commit_resource_evidence(
3588                ResourceEvidenceCommitInput {
3589                    #[cfg(feature = "wordpress-review")]
3590                    sequence,
3591                    descriptor,
3592                    resource_reference: expected_reference,
3593                    response_bytes: staged.response_bytes,
3594                    cookie_update: staged.cookie_update,
3595                    epoch: staged.epoch,
3596                    qualifying_checkpoint_reference: checkpoint_reference,
3597                },
3598                policy,
3599                knowledge,
3600            )
3601        }) {
3602            Some(receipt) => (SuppliedSessionResourceOutcome::Committed, Some(receipt)),
3603            None => (SuppliedSessionResourceOutcome::Incomplete, None),
3604        }
3605    } else {
3606        (SuppliedSessionResourceOutcome::HttpError, None)
3607    };
3608    let evidence_reference = receipt
3609        .as_ref()
3610        .map(|receipt| receipt.evidence_reference.clone());
3611    (
3612        WebAssessmentSuppliedSessionResourceAudit {
3613            sequence,
3614            resource_reference: expected_reference.to_owned(),
3615            evidence_reference,
3616            outcome,
3617            status: Some(staged.status),
3618            response_bytes: staged.response_bytes,
3619            epoch: staged.epoch,
3620            cookie_update: staged.cookie_update,
3621        },
3622        receipt,
3623    )
3624}
3625
3626fn commit_health_unqualified_resource(
3627    sequence: u8,
3628    descriptor: &SuppliedSessionRequestDescriptor,
3629    expected_reference: &str,
3630    policy: &SuppliedSessionPolicy,
3631    knowledge: &KnowledgeBase,
3632    staged: StagedSuppliedSessionResource,
3633    checkpoint_reference: Option<&str>,
3634) -> WebAssessmentSuppliedSessionResourceAudit {
3635    let evidence_reference =
3636        staged_resource_can_await_health(descriptor, expected_reference, staged)
3637            .then(|| {
3638                commit_health_unqualified_resource_evidence(
3639                    policy,
3640                    descriptor,
3641                    expected_reference,
3642                    staged.response_bytes,
3643                    staged.cookie_update,
3644                    checkpoint_reference,
3645                    knowledge,
3646                )
3647            })
3648            .flatten();
3649    WebAssessmentSuppliedSessionResourceAudit {
3650        sequence,
3651        resource_reference: expected_reference.to_owned(),
3652        outcome: if evidence_reference.is_some() {
3653            SuppliedSessionResourceOutcome::HealthUnqualified
3654        } else {
3655            SuppliedSessionResourceOutcome::Incomplete
3656        },
3657        evidence_reference,
3658        status: Some(staged.status),
3659        response_bytes: staged.response_bytes,
3660        epoch: staged.epoch,
3661        cookie_update: staged.cookie_update,
3662    }
3663}
3664
3665/// Commits one value-safe resource receipt to the assessment's parent knowledge
3666/// transaction. The retained record includes only typed references, purpose,
3667/// status, byte count, and outcome; it never includes the URL, response body,
3668/// headers, authorization value, or a digest of the authorization value.
3669struct ResourceEvidenceCommitInput<'a> {
3670    #[cfg(feature = "wordpress-review")]
3671    sequence: u8,
3672    descriptor: &'a SuppliedSessionRequestDescriptor,
3673    resource_reference: &'a str,
3674    response_bytes: u64,
3675    cookie_update: Option<SuppliedSessionResponseCookieUpdate>,
3676    qualifying_checkpoint_reference: &'a str,
3677    epoch: u8,
3678}
3679
3680fn commit_resource_evidence(
3681    input: ResourceEvidenceCommitInput<'_>,
3682    policy: &SuppliedSessionPolicy,
3683    knowledge: &KnowledgeBase,
3684) -> Option<CommittedSuppliedSessionResourceReceipt> {
3685    if !input.descriptor.validate()
3686        || input.descriptor.purpose() != SuppliedSessionRequestPurpose::Resource
3687        || input.descriptor.target_reference() != input.resource_reference
3688    {
3689        return None;
3690    }
3691    require_committed_health_evidence(input.qualifying_checkpoint_reference, knowledge)?;
3692    if matches!(
3693        policy.version(),
3694        SuppliedSessionPolicyVersion::V4 | SuppliedSessionPolicyVersion::V6
3695    ) {
3696        let id = EvidenceId::parse(input.qualifying_checkpoint_reference).ok()?;
3697        let expected_binding = format!(
3698            "policy={};application={};",
3699            policy.policy_reference(),
3700            policy.application_reference()
3701        );
3702        let epoch_binding = format!(";epoch={};", input.epoch);
3703        let v6_epoch_field = format!("epoch={}", input.epoch);
3704        if !knowledge.inspect_evidence(&id, |evidence| {
3705            matches!(evidence.value(), EvidenceValue::Text(binding)
3706                if binding.starts_with(&expected_binding)
3707                    && if policy.version() == SuppliedSessionPolicyVersion::V6 {
3708                        binding.split(';').any(|field| field == v6_epoch_field)
3709                    } else { binding.contains(&epoch_binding) }
3710                    && binding.contains(";outcome=healthy;"))
3711        })? {
3712            return None;
3713        }
3714    }
3715    let mut binding = format!(
3716        "policy={};application={};resource={};purpose=resource;status=200;response_bytes={};qualifying_checkpoint={};outcome=committed",
3717        policy.policy_reference(),
3718        policy.application_reference(),
3719        input.resource_reference,
3720        input.response_bytes,
3721        input.qualifying_checkpoint_reference,
3722    );
3723    if matches!(
3724        policy.version(),
3725        SuppliedSessionPolicyVersion::V4 | SuppliedSessionPolicyVersion::V6
3726    ) {
3727        use std::fmt::Write as _;
3728        let _ = write!(binding, ";epoch={}", input.epoch);
3729    }
3730    append_cookie_update_binding(policy, &mut binding, input.cookie_update);
3731    let evidence_reference = commit_value_safe_evidence(
3732        policy,
3733        input.resource_reference,
3734        RESOURCE_COMMIT_EVIDENCE_DOMAIN,
3735        RESOURCE_COMMIT_EVIDENCE_PREFIX,
3736        RESOURCE_COMMIT_EVIDENCE_PREDICATE,
3737        resource_commit_evidence_method(policy),
3738        binding,
3739        knowledge,
3740    )?;
3741    #[cfg(feature = "wordpress-review")]
3742    let expected_evidence = {
3743        let evidence_id = EvidenceId::parse(evidence_reference.clone()).ok()?;
3744        knowledge.inspect_evidence(&evidence_id, Clone::clone)?
3745    };
3746    Some(CommittedSuppliedSessionResourceReceipt {
3747        #[cfg(feature = "wordpress-review")]
3748        sequence: input.sequence,
3749        #[cfg(feature = "wordpress-review")]
3750        resource_reference: input.resource_reference.to_owned(),
3751        #[cfg(feature = "wordpress-review")]
3752        policy_reference: policy.policy_reference().to_owned(),
3753        #[cfg(feature = "wordpress-review")]
3754        application_reference: policy.application_reference().to_owned(),
3755        #[cfg(feature = "wordpress-review")]
3756        principal_reference: policy.principal_reference().to_owned(),
3757        #[cfg(feature = "wordpress-review")]
3758        credential_mechanism: policy.credential_mechanism(),
3759        #[cfg(feature = "wordpress-review")]
3760        epoch: input.epoch,
3761        evidence_reference,
3762        #[cfg(feature = "wordpress-review")]
3763        expected_evidence,
3764    })
3765}
3766
3767fn commit_health_unqualified_resource_evidence(
3768    policy: &SuppliedSessionPolicy,
3769    descriptor: &SuppliedSessionRequestDescriptor,
3770    resource_reference: &str,
3771    response_bytes: u64,
3772    cookie_update: Option<SuppliedSessionResponseCookieUpdate>,
3773    checkpoint_reference: Option<&str>,
3774    knowledge: &KnowledgeBase,
3775) -> Option<String> {
3776    if !descriptor.validate()
3777        || descriptor.purpose() != SuppliedSessionRequestPurpose::Resource
3778        || descriptor.target_reference() != resource_reference
3779    {
3780        return None;
3781    }
3782    if let Some(reference) = checkpoint_reference {
3783        require_committed_health_evidence(reference, knowledge)?;
3784    }
3785    let checkpoint = checkpoint_reference.unwrap_or("unavailable");
3786    let mut binding = format!(
3787        "policy={};application={};resource={resource_reference};purpose=resource;status=200;response_bytes={response_bytes};qualifying_checkpoint={checkpoint};outcome=health_unqualified",
3788        policy.policy_reference(),
3789        policy.application_reference(),
3790    );
3791    append_cookie_update_binding(policy, &mut binding, cookie_update);
3792    commit_value_safe_evidence(
3793        policy,
3794        resource_reference,
3795        RESOURCE_COMMIT_EVIDENCE_DOMAIN,
3796        RESOURCE_COMMIT_EVIDENCE_PREFIX,
3797        RESOURCE_HEALTH_UNQUALIFIED_EVIDENCE_PREDICATE,
3798        resource_commit_evidence_method(policy),
3799        binding,
3800        knowledge,
3801    )
3802}
3803
3804fn health_commit_evidence_method(policy: &SuppliedSessionPolicy) -> &'static str {
3805    match policy.version() {
3806        SuppliedSessionPolicyVersion::V1 => HEALTH_COMMIT_EVIDENCE_METHOD_V1,
3807        SuppliedSessionPolicyVersion::V2 => HEALTH_COMMIT_EVIDENCE_METHOD_V2,
3808        SuppliedSessionPolicyVersion::V3 => HEALTH_COMMIT_EVIDENCE_METHOD_V3,
3809        SuppliedSessionPolicyVersion::V4 => "supplied-session-health/v4",
3810        SuppliedSessionPolicyVersion::V5 => "supplied-session-health/v5",
3811        SuppliedSessionPolicyVersion::V6 => "supplied-session-health/v6",
3812        SuppliedSessionPolicyVersion::V7 => "supplied-session-health/v7",
3813    }
3814}
3815
3816fn resource_commit_evidence_method(policy: &SuppliedSessionPolicy) -> &'static str {
3817    match policy.version() {
3818        SuppliedSessionPolicyVersion::V1 => RESOURCE_COMMIT_EVIDENCE_METHOD_V1,
3819        SuppliedSessionPolicyVersion::V2 => RESOURCE_COMMIT_EVIDENCE_METHOD_V2,
3820        SuppliedSessionPolicyVersion::V3 => RESOURCE_COMMIT_EVIDENCE_METHOD_V3,
3821        SuppliedSessionPolicyVersion::V4 => "supplied-session-resource/v4",
3822        SuppliedSessionPolicyVersion::V5 => "supplied-session-resource/v5",
3823        SuppliedSessionPolicyVersion::V6 => "supplied-session-resource/v6",
3824        SuppliedSessionPolicyVersion::V7 => "supplied-session-resource/v7",
3825    }
3826}
3827
3828fn append_cookie_update_binding(
3829    policy: &SuppliedSessionPolicy,
3830    binding: &mut String,
3831    update: Option<SuppliedSessionResponseCookieUpdate>,
3832) {
3833    if !matches!(
3834        policy.version(),
3835        SuppliedSessionPolicyVersion::V2
3836            | SuppliedSessionPolicyVersion::V3
3837            | SuppliedSessionPolicyVersion::V4
3838    ) {
3839        return;
3840    }
3841    let update = update.unwrap_or(SuppliedSessionResponseCookieUpdate {
3842        kind: SuppliedSessionResponseCookieUpdateKind::None,
3843        selected_count: 0,
3844        unselected_count: 0,
3845    });
3846    use std::fmt::Write as _;
3847    let _ = write!(
3848        binding,
3849        ";cookie_update={};selected_update_count={};unselected_update_count={}",
3850        cookie_update_token(update.kind),
3851        update.selected_count,
3852        update.unselected_count,
3853    );
3854}
3855
3856fn cookie_update_token(kind: SuppliedSessionResponseCookieUpdateKind) -> &'static str {
3857    match kind {
3858        SuppliedSessionResponseCookieUpdateKind::None => "none",
3859        SuppliedSessionResponseCookieUpdateKind::UnselectedOnly => "unselected_only",
3860        SuppliedSessionResponseCookieUpdateKind::Selected => "selected",
3861        SuppliedSessionResponseCookieUpdateKind::Unusable => "unusable",
3862    }
3863}
3864
3865fn require_committed_health_evidence(reference: &str, knowledge: &KnowledgeBase) -> Option<()> {
3866    let evidence_id = EvidenceId::parse(reference).ok()?;
3867    knowledge
3868        .inspect_evidence(&evidence_id, |evidence| {
3869            evidence.kind() == &EvidenceKind::Authentication
3870                && evidence.predicate().namespace() == RESOURCE_COMMIT_EVIDENCE_NAMESPACE
3871                && evidence.predicate().name() == HEALTH_COMMIT_EVIDENCE_PREDICATE
3872        })?
3873        .then_some(())
3874}
3875
3876#[allow(clippy::too_many_arguments)]
3877fn commit_value_safe_evidence(
3878    policy: &SuppliedSessionPolicy,
3879    correlation_reference: &str,
3880    digest_domain: &[u8],
3881    evidence_prefix: &str,
3882    predicate_name: &str,
3883    method: &str,
3884    binding: String,
3885    knowledge: &KnowledgeBase,
3886) -> Option<String> {
3887    let mut digest = Sha256::new();
3888    digest.update(digest_domain);
3889    digest.update(binding.as_bytes());
3890    let evidence_reference = format!("{evidence_prefix}:{:x}", digest.finalize());
3891    let evidence_id = EvidenceId::parse(evidence_reference.clone()).ok()?;
3892    let subject = EntityId::new(policy.application_reference().to_owned()).ok()?;
3893    let predicate =
3894        KnowledgePredicate::new(RESOURCE_COMMIT_EVIDENCE_NAMESPACE, predicate_name).ok()?;
3895    let source = EvidenceSource::new(RESOURCE_COMMIT_EVIDENCE_COMPONENT, method)
3896        .and_then(|source| source.with_correlation_id(correlation_reference.to_owned()))
3897        .ok()?;
3898    let evidence = Evidence::with_id(
3899        evidence_id.clone(),
3900        subject,
3901        EvidenceKind::Authentication,
3902        predicate,
3903        EvidenceValue::Text(binding),
3904        source,
3905        ConfidenceScore::MAX,
3906    );
3907    knowledge.insert_evidence(evidence.clone()).ok()?;
3908    (knowledge.inspect_evidence(&evidence_id, |stored| stored == &evidence) == Some(true))
3909        .then_some(evidence_reference)
3910}
3911
3912fn resource_transport_failure(
3913    sequence: u8,
3914    reference: &str,
3915    bytes: u64,
3916) -> WebAssessmentSuppliedSessionResourceAudit {
3917    WebAssessmentSuppliedSessionResourceAudit {
3918        sequence,
3919        resource_reference: reference.to_owned(),
3920        evidence_reference: None,
3921        outcome: SuppliedSessionResourceOutcome::TransportFailed,
3922        status: None,
3923        response_bytes: bytes,
3924        epoch: 1,
3925        cookie_update: None,
3926    }
3927}
3928
3929fn resource_not_dispatched(
3930    sequence: u8,
3931    reference: &str,
3932) -> WebAssessmentSuppliedSessionResourceAudit {
3933    WebAssessmentSuppliedSessionResourceAudit {
3934        sequence,
3935        resource_reference: reference.to_owned(),
3936        evidence_reference: None,
3937        outcome: SuppliedSessionResourceOutcome::NotDispatched,
3938        status: None,
3939        response_bytes: 0,
3940        epoch: 1,
3941        cookie_update: None,
3942    }
3943}
3944
3945#[derive(Clone, Copy)]
3946enum StrictHealthValue {
3947    Bool(bool),
3948    Other,
3949}
3950
3951struct StrictHealthSeed<'a> {
3952    depth: usize,
3953    nodes: &'a mut usize,
3954}
3955
3956impl<'de> DeserializeSeed<'de> for StrictHealthSeed<'_> {
3957    type Value = StrictHealthValue;
3958    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
3959    where
3960        D: Deserializer<'de>,
3961    {
3962        if self.depth > HEALTH_JSON_MAX_DEPTH || *self.nodes >= HEALTH_JSON_MAX_NODES {
3963            return Err(serde::de::Error::custom("bounded health JSON limit"));
3964        }
3965        *self.nodes += 1;
3966        deserializer.deserialize_any(StrictHealthVisitor {
3967            depth: self.depth,
3968            nodes: self.nodes,
3969        })
3970    }
3971}
3972
3973struct StrictHealthVisitor<'a> {
3974    depth: usize,
3975    nodes: &'a mut usize,
3976}
3977
3978impl<'de> Visitor<'de> for StrictHealthVisitor<'_> {
3979    type Value = StrictHealthValue;
3980    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3981        formatter.write_str("bounded JSON")
3982    }
3983    fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
3984        Ok(StrictHealthValue::Bool(value))
3985    }
3986    fn visit_i64<E>(self, _: i64) -> Result<Self::Value, E> {
3987        Ok(StrictHealthValue::Other)
3988    }
3989    fn visit_u64<E>(self, _: u64) -> Result<Self::Value, E> {
3990        Ok(StrictHealthValue::Other)
3991    }
3992    fn visit_f64<E>(self, _: f64) -> Result<Self::Value, E> {
3993        Ok(StrictHealthValue::Other)
3994    }
3995    fn visit_none<E>(self) -> Result<Self::Value, E> {
3996        Ok(StrictHealthValue::Other)
3997    }
3998    fn visit_unit<E>(self) -> Result<Self::Value, E> {
3999        Ok(StrictHealthValue::Other)
4000    }
4001    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
4002    where
4003        E: serde::de::Error,
4004    {
4005        if value.len() > HEALTH_JSON_MAX_KEY_BYTES {
4006            return Err(E::custom("bounded health JSON string"));
4007        }
4008        Ok(StrictHealthValue::Other)
4009    }
4010    fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
4011    where
4012        A: SeqAccess<'de>,
4013    {
4014        while sequence
4015            .next_element_seed(StrictHealthSeed {
4016                depth: self.depth + 1,
4017                nodes: self.nodes,
4018            })?
4019            .is_some()
4020        {}
4021        Ok(StrictHealthValue::Other)
4022    }
4023    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
4024    where
4025        A: MapAccess<'de>,
4026    {
4027        let mut seen = std::collections::BTreeSet::new();
4028        while let Some(key) = map.next_key::<String>()? {
4029            if key.len() > HEALTH_JSON_MAX_KEY_BYTES || !seen.insert(key) {
4030                return Err(serde::de::Error::custom(
4031                    "duplicate or oversized health JSON key",
4032                ));
4033            }
4034            let _ = map.next_value_seed(StrictHealthSeed {
4035                depth: self.depth + 1,
4036                nodes: self.nodes,
4037            })?;
4038        }
4039        Ok(StrictHealthValue::Other)
4040    }
4041}
4042
4043fn strict_top_level_boolean(bytes: &[u8], field: &str) -> Option<bool> {
4044    struct RootSeed<'a> {
4045        field: &'a str,
4046        nodes: &'a mut usize,
4047    }
4048    impl<'de> DeserializeSeed<'de> for RootSeed<'_> {
4049        type Value = Option<bool>;
4050        fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
4051        where
4052            D: Deserializer<'de>,
4053        {
4054            struct RootVisitor<'a> {
4055                field: &'a str,
4056                nodes: &'a mut usize,
4057            }
4058            impl<'de> Visitor<'de> for RootVisitor<'_> {
4059                type Value = Option<bool>;
4060                fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4061                    formatter.write_str("a bounded JSON object")
4062                }
4063                fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
4064                where
4065                    A: MapAccess<'de>,
4066                {
4067                    let mut seen = std::collections::BTreeSet::new();
4068                    let mut selected = None;
4069                    while let Some(key) = map.next_key::<String>()? {
4070                        if key.len() > HEALTH_JSON_MAX_KEY_BYTES || !seen.insert(key.clone()) {
4071                            return Err(serde::de::Error::custom(
4072                                "duplicate or oversized health JSON key",
4073                            ));
4074                        }
4075                        let value = map.next_value_seed(StrictHealthSeed {
4076                            depth: 1,
4077                            nodes: self.nodes,
4078                        })?;
4079                        if key == self.field {
4080                            selected = match value {
4081                                StrictHealthValue::Bool(value) => Some(value),
4082                                StrictHealthValue::Other => None,
4083                            };
4084                        }
4085                    }
4086                    Ok(selected)
4087                }
4088            }
4089            deserializer.deserialize_map(RootVisitor {
4090                field: self.field,
4091                nodes: self.nodes,
4092            })
4093        }
4094    }
4095    let mut nodes = 0;
4096    let mut decoder = serde_json::Deserializer::from_slice(bytes);
4097    let parsed = RootSeed {
4098        field,
4099        nodes: &mut nodes,
4100    }
4101    .deserialize(&mut decoder)
4102    .ok()?;
4103    decoder.end().ok()?;
4104    parsed
4105}
4106
4107#[cfg(test)]
4108mod tests {
4109    #[test]
4110    fn oauth_optional_runtime_storage_is_indirect_for_legacy_contexts() {
4111        let _: fn(&SuppliedSessionRuntimeConfig) -> &Option<Box<OAuthCodeRequestBroker>> =
4112            |config| &config.oauth_code_requests;
4113        let _: fn(&SuppliedSessionRuntimeConfig) -> &Option<Box<OAuthCodeRequestDescriptor>> =
4114            |config| &config.oauth_code_descriptor;
4115        let _: fn(
4116            &WebAssessmentSuppliedSessionAudit,
4117        ) -> &Option<Box<SuppliedSessionOAuthCodeAudit>> = |audit| &audit.oauth_code;
4118        let legacy = policy(&url::Url::parse("http://127.0.0.1:1/app/").unwrap());
4119        let state = ExecutionState::new(&legacy, 1);
4120        assert!(state.oauth_code.is_none());
4121        assert_eq!(
4122            std::mem::size_of_val(&state.oauth_code),
4123            std::mem::size_of::<usize>()
4124        );
4125    }
4126    use super::*;
4127    use crate::supplied_session_review::{
4128        SuppliedSessionAuthorization, SuppliedSessionCookies, SuppliedSessionFormCredential,
4129    };
4130    use crate::web_runtime::{
4131        WebAssessmentLimits, WebAssessmentRuntimeBuilder, WebAssessmentRuntimeError,
4132    };
4133    #[cfg(feature = "wordpress-review")]
4134    use crate::wordpress_review::WordPressReviewInputs;
4135    #[cfg(feature = "websocket-review")]
4136    use futures::{SinkExt, StreamExt};
4137    #[cfg(feature = "websocket-review")]
4138    use serde_json::json;
4139    #[cfg(feature = "websocket-review")]
4140    use std::sync::atomic::{AtomicBool, Ordering};
4141    use std::sync::Arc;
4142    use tokio::{
4143        io::{AsyncReadExt, AsyncWriteExt},
4144        net::TcpListener,
4145        sync::Mutex,
4146    };
4147
4148    fn policy(application: &url::Url) -> SuppliedSessionPolicy {
4149        policy_with_wall_time(application, 5_000)
4150    }
4151
4152    fn policy_with_wall_time(
4153        application: &url::Url,
4154        max_wall_time_ms: u64,
4155    ) -> SuppliedSessionPolicy {
4156        let source = format!(
4157            r#"schema = "security.supplied-session-policy/v1"
4158principal_alias = "fixture-principal"
4159credential_mechanism = "authorization_header"
4160health_path = "/app/session-health"
4161health_json_field = "authenticated"
4162resources = ["/app/private"]
4163max_session_requests = 3
4164max_total_response_bytes = 65536
4165max_response_body_bytes = 16384
4166max_wall_time_ms = {max_wall_time_ms}
4167"#
4168        );
4169        SuppliedSessionPolicy::parse_toml(application, source.as_bytes()).unwrap()
4170    }
4171
4172    #[cfg(feature = "websocket-review")]
4173    fn websocket_policy(
4174        application: &url::Url,
4175        endpoint: &url::Url,
4176        max_wall_time_ms: u64,
4177    ) -> WebSocketReviewPolicy {
4178        let expected = b"expected";
4179        let source = serde_json::to_vec(&json!({
4180            "schema": "security.websocket-review-policy/v1",
4181            "target_authorized": true,
4182            "messages_read_only_acknowledged": true,
4183            "message_content_is_non_secret": true,
4184            "endpoint": endpoint.as_str(),
4185            "origin_mode": "application_origin",
4186            "compression": false,
4187            "reconnect": false,
4188            "messages": [{
4189                "id": "combined-session-message",
4190                "text": "request",
4191                "expected_response": {
4192                    "sha256": format!("{:x}", Sha256::digest(expected)),
4193                    "length": expected.len()
4194                }
4195            }],
4196            "limits": {
4197                "max_inbound_message_bytes": 4096,
4198                "max_outbound_message_bytes": 4096,
4199                "max_messages": 1,
4200                "max_control_frames": 4,
4201                "max_wall_time_ms": max_wall_time_ms
4202            }
4203        }))
4204        .unwrap();
4205        WebSocketReviewPolicy::parse_json(application, &source).unwrap()
4206    }
4207
4208    #[cfg(feature = "websocket-review")]
4209    struct ExactAuthorizationCallback(&'static str);
4210
4211    #[cfg(feature = "websocket-review")]
4212    impl tokio_tungstenite::tungstenite::handshake::server::Callback for ExactAuthorizationCallback {
4213        fn on_request(
4214            self,
4215            request: &tokio_tungstenite::tungstenite::handshake::server::Request,
4216            response: tokio_tungstenite::tungstenite::handshake::server::Response,
4217        ) -> Result<
4218            tokio_tungstenite::tungstenite::handshake::server::Response,
4219            tokio_tungstenite::tungstenite::handshake::server::ErrorResponse,
4220        > {
4221            assert!(
4222                request
4223                    .headers()
4224                    .get("authorization")
4225                    .and_then(|value| value.to_str().ok())
4226                    == Some(self.0),
4227                "fixture WebSocket authorization mismatch"
4228            );
4229            Ok(response)
4230        }
4231    }
4232
4233    #[cfg(feature = "websocket-review")]
4234    struct ObserveAuthorizationCallback {
4235        expected: &'static [u8],
4236        observed: Arc<AtomicBool>,
4237    }
4238
4239    #[cfg(feature = "websocket-review")]
4240    impl tokio_tungstenite::tungstenite::handshake::server::Callback for ObserveAuthorizationCallback {
4241        fn on_request(
4242            self,
4243            request: &tokio_tungstenite::tungstenite::handshake::server::Request,
4244            response: tokio_tungstenite::tungstenite::handshake::server::Response,
4245        ) -> Result<
4246            tokio_tungstenite::tungstenite::handshake::server::Response,
4247            tokio_tungstenite::tungstenite::handshake::server::ErrorResponse,
4248        > {
4249            self.observed.store(
4250                request
4251                    .headers()
4252                    .get("authorization")
4253                    .is_some_and(|value| value.as_bytes() == self.expected),
4254                Ordering::SeqCst,
4255            );
4256            Ok(response)
4257        }
4258    }
4259
4260    fn cookie_policy(application: &url::Url) -> SuppliedSessionPolicy {
4261        let bytes = format!(
4262            r#"schema = "security.supplied-session-policy/v2"
4263principal_alias = "cookie-fixture-principal"
4264credential_mechanism = "cookie_jar"
4265cookie_update_policy = "stop_on_selected_cookie"
4266health_path = "/app/session-health"
4267health_json_field = "authenticated"
4268resources = ["/app/private"]
4269max_session_requests = 3
4270max_total_response_bytes = 65536
4271max_response_body_bytes = 16384
4272max_wall_time_ms = 5000
4273
4274[[cookies]]
4275id = "session-secret"
4276name = "session"
4277domain = "{}"
4278host_only = true
4279path = "/app/"
4280secure = false
4281http_only = true
4282same_site = "lax"
4283"#,
4284            application.host_str().unwrap()
4285        );
4286        SuppliedSessionPolicy::parse_toml(application, bytes.as_bytes()).unwrap()
4287    }
4288
4289    fn renewal_policy(application: &url::Url) -> SuppliedSessionPolicy {
4290        let source = format!(
4291            r#"schema = "security.supplied-session-policy/v4"
4292principal_alias = "renewal-fixture"
4293credential_mechanism = "cookie_jar"
4294cookie_update_policy = "stop_on_selected_cookie"
4295renewal_path = "/app/renew"
4296renewal_method = "post"
4297renewal_after_resource_count = 1
4298max_renewal_attempts = 1
4299health_path = "/app/health"
4300health_json_field = "authenticated"
4301resources = ["/app/before", "/app/after"]
4302max_session_requests = 7
4303max_total_response_bytes = 65536
4304max_response_body_bytes = 16384
4305max_wall_time_ms = 5000
4306[[cookies]]
4307id = "session"
4308name = "session"
4309domain = "{}"
4310host_only = true
4311path = "/app/"
4312secure = {}
4313http_only = true
4314same_site = "lax"
4315"#,
4316            application.host_str().unwrap(),
4317            application.scheme() == "https"
4318        );
4319        SuppliedSessionPolicy::parse_toml(application, source.as_bytes()).unwrap()
4320    }
4321
4322    #[test]
4323    fn renewal_resource_commit_requires_the_committed_matching_epoch_not_a_reported_number() {
4324        let policy = renewal_policy(&url::Url::parse("https://example.test/app/").unwrap());
4325        let health = SuppliedSessionRequestDescriptor::health(&policy);
4326        let (target, reference) = policy.execution_resources().nth(1).unwrap();
4327        let descriptor =
4328            SuppliedSessionRequestDescriptor::resource(&policy, target, reference).unwrap();
4329        let knowledge = KnowledgeBase::new();
4330        let mut checkpoint = WebAssessmentSuppliedSessionCheckpointAudit {
4331            sequence: 2,
4332            evidence_reference: None,
4333            phase: SuppliedSessionHealthCheckpointPhase::Terminal,
4334            after_subject_count: 2,
4335            outcome: SuppliedSessionHealthOutcome::Healthy,
4336            status: Some(200),
4337            body_state: SuppliedSessionBodyState::Complete,
4338            predicate: SuppliedSessionPredicateOutcome::Matched,
4339            response_bytes: 22,
4340            epoch: Some(1),
4341            cookie_update: None,
4342        };
4343        bind_health_evidence(&mut checkpoint, &policy, &health, Some(true), &knowledge);
4344        let staged = StagedSuppliedSessionResource {
4345            status: 200,
4346            response_bytes: 17,
4347            exact_target: true,
4348            body_complete: true,
4349            cookie_update: None,
4350            epoch: 2,
4351        };
4352        let (rejected, receipt) = commit_staged_resource_with_receipt(
4353            1,
4354            &descriptor,
4355            reference,
4356            &policy,
4357            &knowledge,
4358            staged,
4359            checkpoint.evidence_reference(),
4360        );
4361        assert_eq!(
4362            rejected.outcome(),
4363            SuppliedSessionResourceOutcome::Incomplete
4364        );
4365        assert!(
4366            receipt.is_none(),
4367            "old-epoch committed health authorized renewed resource"
4368        );
4369        checkpoint.epoch = Some(2);
4370        bind_health_evidence(&mut checkpoint, &policy, &health, Some(true), &knowledge);
4371        let (accepted, receipt) = commit_staged_resource_with_receipt(
4372            1,
4373            &descriptor,
4374            reference,
4375            &policy,
4376            &knowledge,
4377            staged,
4378            checkpoint.evidence_reference(),
4379        );
4380        assert_eq!(
4381            accepted.outcome(),
4382            SuppliedSessionResourceOutcome::Committed
4383        );
4384        assert_eq!(accepted.epoch(), 2);
4385        assert!(receipt.is_some());
4386    }
4387
4388    #[tokio::test]
4389    async fn normal_runtime_renewal_preserves_epochs_classifier_parent_budget_and_cancellation() {
4390        for case in 0_u8..4 {
4391            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4392            let address = listener.local_addr().unwrap();
4393            let trace = Arc::new(Mutex::new(Vec::new()));
4394            let server_trace = Arc::clone(&trace);
4395            let replacement = if case == 1 {
4396                format!(
4397                    "Set-Cookie: session={}; Path=/app/; HttpOnly; SameSite=Lax\r\n",
4398                    "COOKIE-OVERFLOW-CANARY".repeat(185)
4399                )
4400            } else {
4401                "Set-Cookie: session=NEW-RENEWAL-CANARY; Path=/app/; HttpOnly; SameSite=Lax\r\n"
4402                    .to_owned()
4403            };
4404            let server = tokio::spawn(async move {
4405                let mut renewed = false;
4406                loop {
4407                    let Ok((mut stream, _)) = listener.accept().await else {
4408                        break;
4409                    };
4410                    let mut request = Vec::new();
4411                    while request.len() <= 16 * 1024 {
4412                        let mut chunk = [0_u8; 1024];
4413                        let read = stream.read(&mut chunk).await.unwrap_or(0);
4414                        if read == 0 {
4415                            break;
4416                        }
4417                        request.extend_from_slice(&chunk[..read]);
4418                        if request.windows(4).any(|window| window == b"\r\n\r\n") {
4419                            break;
4420                        }
4421                    }
4422                    let text = String::from_utf8_lossy(&request);
4423                    let mut line = text.lines().next().unwrap_or("").split_whitespace();
4424                    let method = line.next().unwrap_or("");
4425                    let path = line.next().unwrap_or("");
4426                    let old = text.lines().any(|line| {
4427                        line.eq_ignore_ascii_case("cookie: session=OLD-RENEWAL-CANARY")
4428                    });
4429                    let new = text.lines().any(|line| {
4430                        line.eq_ignore_ascii_case("cookie: session=NEW-RENEWAL-CANARY")
4431                    });
4432                    server_trace
4433                        .lock()
4434                        .await
4435                        .push((method.to_owned(), path.to_owned(), old, new));
4436                    let active = if renewed { new } else { old };
4437                    let (status, media, body, headers) = match (method, path) {
4438                        ("GET", "/app/health") => (
4439                            200,
4440                            "application/json",
4441                            if active {
4442                                r#"{"authenticated":true}"#
4443                            } else {
4444                                r#"{"authenticated":false}"#
4445                            },
4446                            "",
4447                        ),
4448                        ("GET", "/app/before") if old && !renewed => {
4449                            (200, "text/html", "PRIVATE-RENEWAL-BODY", "")
4450                        },
4451                        ("POST", "/app/renew") if old && !renewed => {
4452                            renewed = true;
4453                            (200, "application/json", "{}", replacement.as_str())
4454                        },
4455                        ("GET", "/app/after") if new && renewed => {
4456                            (200, "text/html", "PRIVATE-RENEWAL-BODY", "")
4457                        },
4458                        (_, "/app/before" | "/app/after" | "/app/renew") => {
4459                            (403, "text/plain", "denied", "")
4460                        },
4461                        _ => (200, "text/html", "<title>public</title>", ""),
4462                    };
4463                    let response = format!("HTTP/1.1 {status} OK\r\nContent-Type: {media}\r\n{headers}Content-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len());
4464                    let _ = stream.write_all(response.as_bytes()).await;
4465                    let _ = stream.shutdown().await;
4466                }
4467            });
4468            let application = url::Url::parse(&format!("http://{address}/app/")).unwrap();
4469            let policy = renewal_policy(&application);
4470            let credential = SuppliedSessionCookies::parse_tsv(
4471                &policy,
4472                b"session\tOLD-RENEWAL-CANARY".to_vec(),
4473                0,
4474            )
4475            .unwrap();
4476            let limits = if case == 2 {
4477                WebAssessmentLimits::default()
4478                    .with_max_total_requests(4)
4479                    .unwrap()
4480            } else {
4481                WebAssessmentLimits::default()
4482            };
4483            let cancellation = tokio_util::sync::CancellationToken::new();
4484            if case == 3 {
4485                cancellation.cancel();
4486            }
4487            let mut runtime = WebAssessmentRuntimeBuilder::new(application)
4488                .limits(limits)
4489                .cancellation_token(cancellation)
4490                .with_supplied_session_review(policy, credential)
4491                .build()
4492                .unwrap();
4493            let report = runtime.analyze().await.unwrap();
4494            let audit = report.supplied_session_audit().unwrap();
4495            let (expected_outcome, expected_requests, expected_committed, expected_epoch) =
4496                match case {
4497                    0 => (SuppliedSessionAuditOutcome::Complete, 7, 2, 2),
4498                    1 => (SuppliedSessionAuditOutcome::RenewalUnavailable, 4, 1, 1),
4499                    2 => (SuppliedSessionAuditOutcome::RuntimeLimit, 4, 1, 2),
4500                    _ => (SuppliedSessionAuditOutcome::Cancelled, 0, 0, 1),
4501                };
4502            assert_eq!(audit.outcome(), expected_outcome, "case {case}");
4503            assert_eq!(audit.dispatched_request_count(), expected_requests);
4504            assert_eq!(audit.committed_resource_count(), expected_committed);
4505            assert_eq!(audit.resources()[0].epoch(), 1);
4506            assert_eq!(audit.resources()[1].epoch(), expected_epoch);
4507            assert_eq!(
4508                audit.renewal().unwrap().attempt_count(),
4509                u8::from(case != 3)
4510            );
4511            if case == 0 {
4512                assert_eq!(audit.renewal().unwrap().health().unwrap().epoch(), 2);
4513            } else {
4514                assert!(audit.renewal().unwrap().health().is_none());
4515            }
4516            if case == 2 {
4517                assert_eq!(report.usage().total_requests(), 4);
4518            }
4519            let saved = serde_json::to_string(audit).unwrap();
4520            for private in [
4521                "OLD-RENEWAL-CANARY",
4522                "NEW-RENEWAL-CANARY",
4523                "PRIVATE-RENEWAL-BODY",
4524                "COOKIE-OVERFLOW-CANARY",
4525                "/app/renew",
4526            ] {
4527                assert!(!saved.contains(private));
4528            }
4529            let trace = trace.lock().await;
4530            let actual: Vec<_> = trace
4531                .iter()
4532                .filter(|(_, _, old, new)| *old || *new)
4533                .cloned()
4534                .collect();
4535            let expected = [
4536                ("GET".into(), "/app/health".into(), true, false),
4537                ("GET".into(), "/app/before".into(), true, false),
4538                ("GET".into(), "/app/health".into(), true, false),
4539                ("POST".into(), "/app/renew".into(), true, false),
4540                ("GET".into(), "/app/health".into(), false, true),
4541                ("GET".into(), "/app/after".into(), false, true),
4542                ("GET".into(), "/app/health".into(), false, true),
4543            ];
4544            assert_eq!(
4545                actual,
4546                expected[..expected_requests as usize],
4547                "case {case}"
4548            );
4549            server.abort();
4550        }
4551    }
4552
4553    fn cookie_credential(policy: &SuppliedSessionPolicy) -> SuppliedSessionCookies {
4554        SuppliedSessionCookies::parse_tsv(
4555            policy,
4556            b"session-secret\tCOOKIE-RUNTIME-CANARY\n".to_vec(),
4557            1,
4558        )
4559        .unwrap()
4560    }
4561
4562    fn form_login_policy(application: &url::Url) -> SuppliedSessionPolicy {
4563        let bytes = format!(
4564            r#"schema = "security.supplied-session-policy/v3"
4565principal_alias = "login-fixture-principal"
4566credential_mechanism = "cookie_jar"
4567credential_acquisition = "bounded_form_login"
4568cookie_update_policy = "stop_on_selected_cookie"
4569login_path = "/app/login"
4570login_method = "post"
4571login_encoding = "application/x-www-form-urlencoded"
4572username_field = "user"
4573password_field = "pass"
4574csrf_field = "csrf"
4575max_login_attempts = 1
4576health_path = "/app/session-health"
4577health_json_field = "authenticated"
4578resources = ["/app/private"]
4579max_session_requests = 5
4580max_total_response_bytes = 65536
4581max_response_body_bytes = 16384
4582max_wall_time_ms = 5000
4583
4584[[cookies]]
4585id = "generated-session"
4586name = "session"
4587domain = "{}"
4588host_only = true
4589path = "/app/"
4590secure = false
4591http_only = true
4592same_site = "lax"
4593"#,
4594            application.host_str().unwrap()
4595        );
4596        SuppliedSessionPolicy::parse_toml(application, bytes.as_bytes()).unwrap()
4597    }
4598
4599    fn committed_health_qualification(
4600        policy: &SuppliedSessionPolicy,
4601        knowledge: &KnowledgeBase,
4602    ) -> String {
4603        let descriptor = SuppliedSessionRequestDescriptor::health(policy);
4604        let mut checkpoint = WebAssessmentSuppliedSessionCheckpointAudit {
4605            sequence: 1,
4606            evidence_reference: None,
4607            phase: SuppliedSessionHealthCheckpointPhase::Terminal,
4608            after_subject_count: 1,
4609            outcome: SuppliedSessionHealthOutcome::Healthy,
4610            status: Some(200),
4611            body_state: SuppliedSessionBodyState::Complete,
4612            predicate: SuppliedSessionPredicateOutcome::Matched,
4613            response_bytes: 22,
4614            epoch: None,
4615            cookie_update: None,
4616        };
4617        bind_health_evidence(&mut checkpoint, policy, &descriptor, Some(true), knowledge);
4618        checkpoint
4619            .evidence_reference
4620            .expect("fixture health receipt must commit")
4621    }
4622
4623    #[test]
4624    fn strict_health_json_rejects_duplicates_and_non_boolean_values() {
4625        assert_eq!(
4626            strict_top_level_boolean(br#"{"authenticated":true}"#, "authenticated"),
4627            Some(true)
4628        );
4629        assert_eq!(
4630            strict_top_level_boolean(br#"{"authenticated":false}"#, "authenticated"),
4631            Some(false)
4632        );
4633        assert_eq!(
4634            strict_top_level_boolean(br#"{"authenticated":"true"}"#, "authenticated"),
4635            None
4636        );
4637        assert_eq!(
4638            strict_top_level_boolean(
4639                br#"{"authenticated":true,"authenticated":false}"#,
4640                "authenticated"
4641            ),
4642            None
4643        );
4644        assert_eq!(
4645            strict_top_level_boolean(br#"[true]"#, "authenticated"),
4646            None
4647        );
4648    }
4649
4650    #[test]
4651    fn only_a_complete_negative_post_startup_checkpoint_claims_session_loss() {
4652        assert_eq!(
4653            health_stop_outcome(SuppliedSessionHealthOutcome::Healthy, true),
4654            None
4655        );
4656        assert_eq!(
4657            health_stop_outcome(SuppliedSessionHealthOutcome::Unhealthy, true),
4658            Some(SuppliedSessionAuditOutcome::StartupUnhealthy)
4659        );
4660        assert_eq!(
4661            health_stop_outcome(SuppliedSessionHealthOutcome::Indeterminate, true),
4662            Some(SuppliedSessionAuditOutcome::ResourceUnavailable)
4663        );
4664        assert_eq!(
4665            health_stop_outcome(SuppliedSessionHealthOutcome::Unhealthy, false),
4666            Some(SuppliedSessionAuditOutcome::SessionLost)
4667        );
4668        assert_eq!(
4669            health_stop_outcome(SuppliedSessionHealthOutcome::Indeterminate, false),
4670            Some(SuppliedSessionAuditOutcome::ResourceUnavailable)
4671        );
4672    }
4673
4674    #[test]
4675    fn health_truth_requires_a_parent_knowledge_commit() {
4676        let application = url::Url::parse("https://example.test/app/").unwrap();
4677        let policy = policy(&application);
4678        let descriptor = SuppliedSessionRequestDescriptor::health(&policy);
4679        let healthy = || WebAssessmentSuppliedSessionCheckpointAudit {
4680            sequence: 0,
4681            evidence_reference: None,
4682            phase: SuppliedSessionHealthCheckpointPhase::Startup,
4683            after_subject_count: 0,
4684            outcome: SuppliedSessionHealthOutcome::Healthy,
4685            status: Some(200),
4686            body_state: SuppliedSessionBodyState::Complete,
4687            predicate: SuppliedSessionPredicateOutcome::Matched,
4688            response_bytes: 22,
4689            epoch: None,
4690            cookie_update: None,
4691        };
4692
4693        let committed_knowledge = KnowledgeBase::new();
4694        let mut committed = healthy();
4695        bind_health_evidence(
4696            &mut committed,
4697            &policy,
4698            &descriptor,
4699            Some(true),
4700            &committed_knowledge,
4701        );
4702        assert_eq!(committed.outcome(), SuppliedSessionHealthOutcome::Healthy);
4703        let evidence_reference = committed
4704            .evidence_reference()
4705            .expect("healthy checkpoint must have a committed receipt")
4706            .to_owned();
4707
4708        let conflicting = Evidence::with_id(
4709            EvidenceId::parse(evidence_reference).unwrap(),
4710            EntityId::new("supplied-session-conflict-fixture").unwrap(),
4711            EvidenceKind::Authentication,
4712            KnowledgePredicate::new("fixture", "conflict").unwrap(),
4713            EvidenceValue::Boolean(false),
4714            EvidenceSource::new("fixture", "conflict").unwrap(),
4715            ConfidenceScore::MAX,
4716        );
4717        let conflicting_knowledge = KnowledgeBase::new();
4718        conflicting_knowledge.insert_evidence(conflicting).unwrap();
4719        let mut rejected = healthy();
4720        bind_health_evidence(
4721            &mut rejected,
4722            &policy,
4723            &descriptor,
4724            Some(true),
4725            &conflicting_knowledge,
4726        );
4727        assert_eq!(rejected.evidence_reference(), None);
4728        assert_eq!(
4729            rejected.outcome(),
4730            SuppliedSessionHealthOutcome::Indeterminate
4731        );
4732        assert_eq!(
4733            rejected.predicate(),
4734            SuppliedSessionPredicateOutcome::NotEvaluated
4735        );
4736
4737        let mut wrong_target = healthy();
4738        bind_health_evidence(
4739            &mut wrong_target,
4740            &policy,
4741            &descriptor,
4742            Some(false),
4743            &KnowledgeBase::new(),
4744        );
4745        assert_eq!(wrong_target.evidence_reference(), None);
4746        assert_eq!(
4747            wrong_target.outcome(),
4748            SuppliedSessionHealthOutcome::Indeterminate
4749        );
4750    }
4751
4752    #[cfg(feature = "websocket-review")]
4753    #[test]
4754    fn startup_health_permit_rejects_mutated_binding_and_widened_deadline() {
4755        let application = url::Url::parse("http://127.0.0.1:8123/app/").unwrap();
4756        let endpoint = url::Url::parse("ws://127.0.0.1:8123/app/socket").unwrap();
4757        let policy = policy(&application);
4758        let websocket = websocket_policy(&application, &endpoint, 1_000);
4759        let descriptor = SuppliedSessionRequestDescriptor::health(&policy);
4760        let knowledge = KnowledgeBase::new();
4761        let mut checkpoint = WebAssessmentSuppliedSessionCheckpointAudit {
4762            sequence: 0,
4763            evidence_reference: None,
4764            phase: SuppliedSessionHealthCheckpointPhase::Startup,
4765            after_subject_count: 0,
4766            outcome: SuppliedSessionHealthOutcome::Healthy,
4767            status: Some(200),
4768            body_state: SuppliedSessionBodyState::Complete,
4769            predicate: SuppliedSessionPredicateOutcome::Matched,
4770            response_bytes: 22,
4771            epoch: None,
4772            cookie_update: None,
4773        };
4774        bind_health_evidence(
4775            &mut checkpoint,
4776            &policy,
4777            &descriptor,
4778            Some(true),
4779            &knowledge,
4780        );
4781        let startup = CommittedSuppliedSessionHealthReceipt::from_checkpoint(
4782            &policy,
4783            &checkpoint,
4784            SuppliedSessionHealthCheckpointPhase::Startup,
4785            0,
4786            &knowledge,
4787        )
4788        .expect("fixture must commit one exact startup receipt");
4789        let authorization =
4790            SuppliedSessionAuthorization::new("Bearer STARTUP-PERMIT-CANARY").unwrap();
4791        let parent_deadline = tokio::time::Instant::now()
4792            .checked_add(Duration::from_secs(10))
4793            .unwrap();
4794        let session_deadline = tokio::time::Instant::now()
4795            .checked_add(Duration::from_secs(5))
4796            .unwrap();
4797        let mut permit = SuppliedSessionWebSocketPermit::new(
4798            &policy,
4799            &authorization,
4800            &websocket,
4801            Some(session_deadline),
4802            startup,
4803        );
4804        assert!(permit.is_valid_for(&application, &websocket, &knowledge, Some(parent_deadline)));
4805
4806        permit.startup.epoch = 2;
4807        assert!(!permit.is_valid_for(&application, &websocket, &knowledge, Some(parent_deadline)));
4808        permit.startup.epoch = 1;
4809        permit.startup.after_subject_count = 1;
4810        assert!(!permit.is_valid_for(&application, &websocket, &knowledge, Some(parent_deadline)));
4811        permit.startup.after_subject_count = 0;
4812        permit.websocket_endpoint_reference = "websocket-endpoint-sha256:wrong";
4813        assert!(!permit.is_valid_for(&application, &websocket, &knowledge, Some(parent_deadline)));
4814        permit.websocket_endpoint_reference = websocket.endpoint_reference();
4815        permit.session_deadline = None;
4816        assert!(!permit.is_valid_for(&application, &websocket, &knowledge, Some(parent_deadline)));
4817        permit.session_deadline = parent_deadline.checked_add(Duration::from_millis(1));
4818        assert!(!permit.is_valid_for(&application, &websocket, &knowledge, Some(parent_deadline)));
4819    }
4820
4821    #[test]
4822    fn resource_commit_revalidates_descriptor_and_complete_response_receipt() {
4823        let application = url::Url::parse("https://example.test/app/").unwrap();
4824        let policy = policy(&application);
4825        let (target, reference) = policy.execution_resources().next().unwrap();
4826        let descriptor = SuppliedSessionRequestDescriptor::resource(&policy, target, reference)
4827            .expect("fixture resource descriptor");
4828        let complete = StagedSuppliedSessionResource {
4829            status: 200,
4830            response_bytes: 17,
4831            exact_target: true,
4832            body_complete: true,
4833            cookie_update: None,
4834            epoch: 1,
4835        };
4836        let knowledge = KnowledgeBase::new();
4837        assert_eq!(
4838            commit_staged_resource(
4839                0,
4840                &descriptor,
4841                reference,
4842                &policy,
4843                &knowledge,
4844                complete,
4845                None,
4846            )
4847            .outcome(),
4848            SuppliedSessionResourceOutcome::Incomplete,
4849            "a complete 200 is not authenticated coverage without its following health receipt"
4850        );
4851        let qualifier = committed_health_qualification(&policy, &knowledge);
4852        let committed = commit_staged_resource(
4853            0,
4854            &descriptor,
4855            reference,
4856            &policy,
4857            &knowledge,
4858            complete,
4859            Some(&qualifier),
4860        );
4861        assert_eq!(
4862            committed.outcome(),
4863            SuppliedSessionResourceOutcome::Committed
4864        );
4865        let evidence_reference = committed
4866            .evidence_reference()
4867            .expect("knowledge commit must leave an opaque receipt")
4868            .to_owned();
4869        assert_eq!(knowledge.stats().evidence, 2);
4870
4871        let conflicting = Evidence::with_id(
4872            EvidenceId::parse(evidence_reference).unwrap(),
4873            EntityId::new("supplied-session-conflict-fixture").unwrap(),
4874            EvidenceKind::Authentication,
4875            KnowledgePredicate::new("fixture", "conflict").unwrap(),
4876            EvidenceValue::Boolean(false),
4877            EvidenceSource::new("fixture", "conflict").unwrap(),
4878            ConfidenceScore::MAX,
4879        );
4880        let conflicting_knowledge = KnowledgeBase::new();
4881        let conflicting_qualifier = committed_health_qualification(&policy, &conflicting_knowledge);
4882        conflicting_knowledge.insert_evidence(conflicting).unwrap();
4883        let rejected = commit_staged_resource(
4884            0,
4885            &descriptor,
4886            reference,
4887            &policy,
4888            &conflicting_knowledge,
4889            complete,
4890            Some(&conflicting_qualifier),
4891        );
4892        assert_eq!(
4893            rejected.outcome(),
4894            SuppliedSessionResourceOutcome::Incomplete
4895        );
4896        assert_eq!(rejected.evidence_reference(), None);
4897
4898        for mutation in [
4899            StagedSuppliedSessionResource {
4900                exact_target: false,
4901                ..complete
4902            },
4903            StagedSuppliedSessionResource {
4904                body_complete: false,
4905                ..complete
4906            },
4907        ] {
4908            assert_eq!(
4909                commit_staged_resource(
4910                    0,
4911                    &descriptor,
4912                    reference,
4913                    &policy,
4914                    &KnowledgeBase::new(),
4915                    mutation,
4916                    None,
4917                )
4918                .outcome(),
4919                SuppliedSessionResourceOutcome::Incomplete
4920            );
4921        }
4922        assert_eq!(
4923            commit_staged_resource(
4924                0,
4925                &descriptor,
4926                "supplied-session-resource-sha256:wrong",
4927                &policy,
4928                &KnowledgeBase::new(),
4929                complete,
4930                None,
4931            )
4932            .outcome(),
4933            SuppliedSessionResourceOutcome::Incomplete
4934        );
4935        assert_eq!(
4936            commit_staged_resource(
4937                0,
4938                &descriptor,
4939                reference,
4940                &policy,
4941                &KnowledgeBase::new(),
4942                StagedSuppliedSessionResource {
4943                    status: 302,
4944                    ..complete
4945                },
4946                None,
4947            )
4948            .outcome(),
4949            SuppliedSessionResourceOutcome::RedirectRefused
4950        );
4951        assert_eq!(
4952            commit_staged_resource(
4953                0,
4954                &descriptor,
4955                reference,
4956                &policy,
4957                &KnowledgeBase::new(),
4958                StagedSuppliedSessionResource {
4959                    status: 403,
4960                    ..complete
4961                },
4962                None,
4963            )
4964            .outcome(),
4965            SuppliedSessionResourceOutcome::HttpError
4966        );
4967    }
4968
4969    #[cfg(feature = "wordpress-review")]
4970    #[test]
4971    fn committed_resource_receipt_revalidates_the_full_session_binding() {
4972        let application = url::Url::parse("https://example.test/app/").unwrap();
4973        let policy = policy(&application);
4974        let (target, reference) = policy.execution_resources().next().unwrap();
4975        let descriptor = SuppliedSessionRequestDescriptor::resource(&policy, target, reference)
4976            .expect("fixture resource descriptor");
4977        let staged = StagedSuppliedSessionResource {
4978            status: 200,
4979            response_bytes: 17,
4980            exact_target: true,
4981            body_complete: true,
4982            cookie_update: None,
4983            epoch: 1,
4984        };
4985        let knowledge = KnowledgeBase::new();
4986        let qualifier = committed_health_qualification(&policy, &knowledge);
4987        let (audit, receipt) = commit_staged_resource_with_receipt(
4988            0,
4989            &descriptor,
4990            reference,
4991            &policy,
4992            &knowledge,
4993            staged,
4994            Some(&qualifier),
4995        );
4996        assert_eq!(audit.outcome(), SuppliedSessionResourceOutcome::Committed);
4997        let mut receipt = receipt.expect("complete qualified response must mint a sealed receipt");
4998        assert!(receipt.is_valid_for(0, &policy, &descriptor, &knowledge));
4999
5000        receipt.epoch = 2;
5001        assert!(!receipt.is_valid_for(0, &policy, &descriptor, &knowledge));
5002        receipt.epoch = 1;
5003        receipt.principal_reference = "supplied-session-principal-9999".to_owned();
5004        assert!(!receipt.is_valid_for(0, &policy, &descriptor, &knowledge));
5005        receipt.principal_reference = policy.principal_reference().to_owned();
5006        receipt.credential_mechanism = SuppliedSessionCredentialMechanism::CookieJar;
5007        assert!(!receipt.is_valid_for(0, &policy, &descriptor, &knowledge));
5008        receipt.credential_mechanism = SuppliedSessionCredentialMechanism::AuthorizationHeader;
5009        receipt.application_reference =
5010            "supplied-session-application-sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
5011                .to_owned();
5012        assert!(!receipt.is_valid_for(0, &policy, &descriptor, &knowledge));
5013        receipt.application_reference = policy.application_reference().to_owned();
5014        receipt.policy_reference =
5015            "supplied-session-policy-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
5016                .to_owned();
5017        assert!(!receipt.is_valid_for(0, &policy, &descriptor, &knowledge));
5018        receipt.policy_reference = policy.policy_reference().to_owned();
5019        receipt.resource_reference =
5020            "supplied-session-resource-sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
5021                .to_owned();
5022        assert!(!receipt.is_valid_for(0, &policy, &descriptor, &knowledge));
5023        receipt.resource_reference = descriptor.target_reference().to_owned();
5024        assert!(!receipt.is_valid_for(1, &policy, &descriptor, &knowledge));
5025        assert!(receipt.is_valid_for(0, &policy, &descriptor, &knowledge));
5026    }
5027
5028    #[test]
5029    fn builder_rejects_cleartext_non_loopback_and_application_mismatch() {
5030        let insecure = url::Url::parse("http://example.test/app/").unwrap();
5031        let error = WebAssessmentRuntimeBuilder::new(insecure.clone())
5032            .with_supplied_session_review(
5033                policy(&insecure),
5034                SuppliedSessionAuthorization::new("Bearer REDACTED-CANARY").unwrap(),
5035            )
5036            .build()
5037            .err()
5038            .expect("cleartext non-loopback session must be rejected");
5039        assert!(matches!(
5040            error,
5041            WebAssessmentRuntimeError::InsecureSuppliedSessionTransport
5042        ));
5043
5044        let selected = url::Url::parse("https://example.test/app/").unwrap();
5045        let other = url::Url::parse("https://example.test/other/").unwrap();
5046        let other_policy = SuppliedSessionPolicy::parse_toml(
5047            &other,
5048            br#"schema = "security.supplied-session-policy/v1"
5049principal_alias = "fixture-principal"
5050credential_mechanism = "authorization_header"
5051health_path = "/other/session-health"
5052health_json_field = "authenticated"
5053resources = ["/other/private"]
5054max_session_requests = 3
5055max_total_response_bytes = 65536
5056max_response_body_bytes = 16384
5057max_wall_time_ms = 5000
5058"#,
5059        )
5060        .unwrap();
5061        let error = WebAssessmentRuntimeBuilder::new(selected)
5062            .with_supplied_session_review(
5063                other_policy,
5064                SuppliedSessionAuthorization::new("Bearer REDACTED-CANARY").unwrap(),
5065            )
5066            .build()
5067            .err()
5068            .expect("policy bound to another application must be rejected");
5069        assert!(matches!(
5070            error,
5071            WebAssessmentRuntimeError::SuppliedSessionApplicationMismatch
5072        ));
5073    }
5074
5075    #[cfg(feature = "wordpress-review")]
5076    #[test]
5077    fn wordpress_supplied_session_selection_requires_every_runtime_input() {
5078        let application = url::Url::parse("https://example.test/app/").unwrap();
5079        let authorization =
5080            || SuppliedSessionAuthorization::new("Bearer REDACTED-WORDPRESS-CANARY").unwrap();
5081
5082        let missing_all = WebAssessmentRuntimeBuilder::new(application.clone())
5083            .with_wordpress_supplied_session()
5084            .build()
5085            .err()
5086            .expect("integration without either child must fail preflight");
5087        assert!(matches!(
5088            missing_all,
5089            WebAssessmentRuntimeError::WordPressSuppliedSessionRequiresInputs
5090        ));
5091
5092        let missing_session = WebAssessmentRuntimeBuilder::new(application.clone())
5093            .with_wordpress_review(WordPressReviewInputs::new(None, None))
5094            .with_wordpress_discovery()
5095            .with_wordpress_supplied_session()
5096            .build()
5097            .err()
5098            .expect("WordPress discovery without a supplied session must fail preflight");
5099        assert!(matches!(
5100            missing_session,
5101            WebAssessmentRuntimeError::WordPressSuppliedSessionRequiresInputs
5102        ));
5103
5104        let missing_discovery = WebAssessmentRuntimeBuilder::new(application.clone())
5105            .with_wordpress_review(WordPressReviewInputs::new(None, None))
5106            .with_supplied_session_review(policy(&application), authorization())
5107            .with_wordpress_supplied_session()
5108            .build()
5109            .err()
5110            .expect("integration without metadata discovery must fail preflight");
5111        assert!(matches!(
5112            missing_discovery,
5113            WebAssessmentRuntimeError::WordPressSuppliedSessionRequiresInputs
5114        ));
5115
5116        let form_application = url::Url::parse("http://127.0.0.1:8123/app/").unwrap();
5117        let form_policy = form_login_policy(&form_application);
5118        let form_credential = SuppliedSessionFormCredential::parse_tsv(
5119            &form_policy,
5120            b"username\tUSER-RUNTIME-CANARY\npassword\tPASSWORD-RUNTIME-CANARY\n".to_vec(),
5121        )
5122        .unwrap();
5123        WebAssessmentRuntimeBuilder::new(form_application)
5124            .with_wordpress_review(WordPressReviewInputs::new(None, None))
5125            .with_wordpress_discovery()
5126            .with_supplied_session_review(form_policy, form_credential)
5127            .with_wordpress_supplied_session()
5128            .build()
5129            .expect("V3 is a bounded fixed-epoch WordPress session consumer");
5130
5131        let renewal = renewal_policy(&application);
5132        let credential =
5133            SuppliedSessionCookies::parse_tsv(&renewal, b"session\tRENEWAL-WP-CANARY".to_vec(), 0)
5134                .unwrap();
5135        let unsupported_renewal = WebAssessmentRuntimeBuilder::new(application.clone())
5136            .with_wordpress_review(WordPressReviewInputs::new(None, None))
5137            .with_wordpress_discovery()
5138            .with_supplied_session_review(renewal, credential)
5139            .with_wordpress_supplied_session()
5140            .build()
5141            .err()
5142            .expect("V4 renewal must not reach the fixed-epoch WordPress consumer");
5143        assert!(matches!(
5144            unsupported_renewal,
5145            WebAssessmentRuntimeError::WordPressSuppliedSessionUnsupportedPolicy
5146        ));
5147
5148        WebAssessmentRuntimeBuilder::new(application.clone())
5149            .with_wordpress_review(WordPressReviewInputs::new(None, None))
5150            .with_wordpress_discovery()
5151            .with_supplied_session_review(policy(&application), authorization())
5152            .with_wordpress_supplied_session()
5153            .build()
5154            .expect("complete integration selection must build");
5155    }
5156
5157    #[tokio::test]
5158    async fn normal_runtime_runs_session_before_anonymous_bfs_and_keeps_bodies_private() {
5159        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5160        let address = listener.local_addr().unwrap();
5161        let observed = Arc::new(Mutex::new(Vec::<(String, bool, bool)>::new()));
5162        let server_observed = observed.clone();
5163        let server = tokio::spawn(async move {
5164            loop {
5165                let Ok((mut stream, _)) = listener.accept().await else {
5166                    break;
5167                };
5168                let mut request = Vec::new();
5169                loop {
5170                    let mut chunk = [0_u8; 1024];
5171                    let read = stream.read(&mut chunk).await.unwrap_or(0);
5172                    if read == 0 {
5173                        break;
5174                    }
5175                    request.extend_from_slice(&chunk[..read]);
5176                    if request.windows(4).any(|window| window == b"\r\n\r\n")
5177                        || request.len() > 16 * 1024
5178                    {
5179                        break;
5180                    }
5181                }
5182                let text = String::from_utf8_lossy(&request);
5183                let path = text
5184                    .lines()
5185                    .next()
5186                    .and_then(|line| line.split_ascii_whitespace().nth(1))
5187                    .unwrap_or("")
5188                    .to_owned();
5189                let has_authorization = text.lines().any(|line| {
5190                    line.eq_ignore_ascii_case("authorization: Bearer SECRET-RUNTIME-CANARY")
5191                });
5192                let has_cookie = text
5193                    .lines()
5194                    .any(|line| line.to_ascii_lowercase().starts_with("cookie:"));
5195                server_observed
5196                    .lock()
5197                    .await
5198                    .push((path.clone(), has_authorization, has_cookie));
5199                let (media_type, body) = match path.as_str() {
5200                    "/app/session-health" => ("application/json", r#"{"authenticated":true}"#),
5201                    "/app/private" => ("text/html", "PRIVATE-BODY-MUST-NOT-BE-RETAINED"),
5202                    _ => ("text/html", "<!doctype html><title>public root</title>"),
5203                };
5204                let response = format!(
5205                    "HTTP/1.1 200 OK\r\nContent-Type: {media_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
5206                    body.len()
5207                );
5208                let _ = stream.write_all(response.as_bytes()).await;
5209                let _ = stream.shutdown().await;
5210            }
5211        });
5212
5213        let application = url::Url::parse(&format!("http://{address}/app/")).unwrap();
5214        let mut runtime = WebAssessmentRuntimeBuilder::new(application.clone())
5215            .with_supplied_session_review(
5216                policy(&application),
5217                SuppliedSessionAuthorization::new("Bearer SECRET-RUNTIME-CANARY").unwrap(),
5218            )
5219            .build()
5220            .unwrap();
5221        let report = runtime.analyze().await.unwrap();
5222        let audit = report
5223            .supplied_session_audit()
5224            .expect("selected child must retain an audit");
5225        assert_eq!(audit.capability_id(), SUPPLIED_SESSION_CAPABILITY_ID);
5226        assert_eq!(audit.outcome(), SuppliedSessionAuditOutcome::Complete);
5227        assert_eq!(audit.coverage(), SuppliedSessionCoverage::Complete);
5228        assert_eq!(audit.selected_resource_count(), 1);
5229        assert_eq!(audit.dispatched_resource_count(), 1);
5230        assert_eq!(audit.committed_resource_count(), 1);
5231        assert_eq!(audit.dispatched_request_count(), 3);
5232        assert_eq!(audit.checkpoints().len(), 2);
5233        assert_eq!(audit.checkpoints()[0].sequence(), 0);
5234        assert!(audit.checkpoints()[0]
5235            .evidence_reference()
5236            .is_some_and(|reference| reference.starts_with(HEALTH_COMMIT_EVIDENCE_PREFIX)));
5237        assert_eq!(audit.checkpoints()[0].after_subject_count(), 0);
5238        assert_eq!(
5239            audit.checkpoints()[0].phase(),
5240            SuppliedSessionHealthCheckpointPhase::Startup
5241        );
5242        assert_eq!(audit.checkpoints()[1].sequence(), 1);
5243        assert!(audit.checkpoints()[1]
5244            .evidence_reference()
5245            .is_some_and(|reference| reference.starts_with(HEALTH_COMMIT_EVIDENCE_PREFIX)));
5246        assert_eq!(audit.checkpoints()[1].after_subject_count(), 1);
5247        assert_eq!(
5248            audit.checkpoints()[1].phase(),
5249            SuppliedSessionHealthCheckpointPhase::Terminal
5250        );
5251        assert_eq!(audit.resources()[0].sequence(), 0);
5252        assert_eq!(
5253            audit.resources()[0].outcome(),
5254            SuppliedSessionResourceOutcome::Committed
5255        );
5256        assert_eq!(audit.resources()[0].epoch(), 1);
5257        let evidence_reference = audit.resources()[0]
5258            .evidence_reference()
5259            .expect("committed resource must retain its knowledge receipt");
5260        assert!(evidence_reference.starts_with(RESOURCE_COMMIT_EVIDENCE_PREFIX));
5261        let evidence_id = EvidenceId::parse(evidence_reference).unwrap();
5262        assert_eq!(
5263            runtime
5264                .knowledge()
5265                .inspect_evidence(&evidence_id, |evidence| {
5266                    evidence.kind() == &EvidenceKind::Authentication
5267                        && evidence.predicate().namespace() == RESOURCE_COMMIT_EVIDENCE_NAMESPACE
5268                        && evidence.predicate().name() == RESOURCE_COMMIT_EVIDENCE_PREDICATE
5269                }),
5270            Some(true)
5271        );
5272        for checkpoint in audit.checkpoints() {
5273            let evidence_id = EvidenceId::parse(checkpoint.evidence_reference().unwrap()).unwrap();
5274            assert_eq!(
5275                runtime
5276                    .knowledge()
5277                    .inspect_evidence(&evidence_id, |evidence| {
5278                        evidence.kind() == &EvidenceKind::Authentication
5279                            && evidence.predicate().namespace()
5280                                == RESOURCE_COMMIT_EVIDENCE_NAMESPACE
5281                            && evidence.predicate().name() == HEALTH_COMMIT_EVIDENCE_PREDICATE
5282                    }),
5283                Some(true)
5284            );
5285        }
5286        assert_eq!(
5287            audit.response_bytes(),
5288            audit
5289                .checkpoints()
5290                .iter()
5291                .map(WebAssessmentSuppliedSessionCheckpointAudit::response_bytes)
5292                .chain(
5293                    audit
5294                        .resources()
5295                        .iter()
5296                        .map(WebAssessmentSuppliedSessionResourceAudit::response_bytes),
5297                )
5298                .sum::<u64>()
5299        );
5300        let saved = serde_json::to_string(audit).unwrap();
5301        let direct = serde_json::to_value(audit).unwrap();
5302        assert_eq!(
5303            direct.get("schema").and_then(serde_json::Value::as_str),
5304            Some(SUPPLIED_SESSION_AUDIT_SCHEMA)
5305        );
5306        assert!(!direct
5307            .as_object()
5308            .unwrap()
5309            .contains_key("credential_acquisition"));
5310        assert_eq!(audit.principal_alias(), "fixture-principal");
5311        assert!(saved.contains(r#""principal_alias":"fixture-principal""#));
5312        for forbidden in [
5313            "SECRET-RUNTIME-CANARY",
5314            "PRIVATE-BODY-MUST-NOT-BE-RETAINED",
5315            "/app/private",
5316        ] {
5317            assert!(!saved.contains(forbidden));
5318        }
5319
5320        let observed = observed.lock().await.clone();
5321        assert!(
5322            observed.len() >= 4,
5323            "session plus ordinary root must execute"
5324        );
5325        assert_eq!(observed[0], ("/app/session-health".to_owned(), true, false));
5326        assert_eq!(observed[1], ("/app/private".to_owned(), true, false));
5327        assert_eq!(observed[2], ("/app/session-health".to_owned(), true, false));
5328        assert_eq!(observed[3], ("/app/".to_owned(), false, false));
5329        server.abort();
5330    }
5331
5332    #[cfg(feature = "websocket-review")]
5333    #[tokio::test]
5334    async fn shared_request_refusal_retains_health_projection_without_claiming_ws_context() {
5335        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5336        let address = listener.local_addr().unwrap();
5337        let observed = Arc::new(Mutex::new(Vec::<String>::new()));
5338        let server_observed = Arc::clone(&observed);
5339        let server = tokio::spawn(async move {
5340            loop {
5341                let Ok((mut stream, _)) = listener.accept().await else {
5342                    break;
5343                };
5344                let mut request = Vec::new();
5345                loop {
5346                    let mut chunk = [0_u8; 1024];
5347                    let read = stream.read(&mut chunk).await.unwrap_or(0);
5348                    if read == 0 {
5349                        break;
5350                    }
5351                    request.extend_from_slice(&chunk[..read]);
5352                    if request.windows(4).any(|window| window == b"\r\n\r\n")
5353                        || request.len() > 16 * 1024
5354                    {
5355                        break;
5356                    }
5357                }
5358                let text = String::from_utf8_lossy(&request);
5359                let path = text
5360                    .lines()
5361                    .next()
5362                    .and_then(|line| line.split_ascii_whitespace().nth(1))
5363                    .unwrap_or("")
5364                    .to_owned();
5365                server_observed.lock().await.push(path.clone());
5366                let (media_type, body) = match path.as_str() {
5367                    "/app/session-health" => ("application/json", r#"{"authenticated":true}"#),
5368                    "/app/private" => ("text/html", "PRIVATE-ZERO-ADMISSION-CANARY"),
5369                    _ => ("text/html", "unexpected"),
5370                };
5371                let response = format!(
5372                    "HTTP/1.1 200 OK\r\nContent-Type: {media_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
5373                    body.len()
5374                );
5375                let _ = stream.write_all(response.as_bytes()).await;
5376                let _ = stream.shutdown().await;
5377            }
5378        });
5379
5380        let application = url::Url::parse(&format!("http://{address}/app/")).unwrap();
5381        let endpoint = url::Url::parse(&format!("ws://{address}/app/socket")).unwrap();
5382        let limits = WebAssessmentLimits::default()
5383            .with_max_total_requests(2)
5384            .unwrap();
5385        let mut runtime = WebAssessmentRuntimeBuilder::new(application.clone())
5386            .limits(limits)
5387            .with_supplied_session_review(
5388                policy(&application),
5389                SuppliedSessionAuthorization::new("Bearer ZERO-ADMISSION-SECRET-CANARY").unwrap(),
5390            )
5391            .with_websocket_review(websocket_policy(&application, &endpoint, 2_000))
5392            .with_websocket_supplied_session()
5393            .build()
5394            .unwrap();
5395        let report = runtime.analyze().await.unwrap();
5396        assert!(report
5397            .completion()
5398            .reasons()
5399            .contains(&crate::web_runtime::WebAssessmentIncompleteReason::TotalRequestLimit));
5400
5401        let session = report.supplied_session_audit().unwrap();
5402        assert_eq!(session.checkpoints().len(), 1);
5403        assert_eq!(
5404            session.checkpoints()[0].outcome(),
5405            SuppliedSessionHealthOutcome::Healthy
5406        );
5407        let websocket = report.websocket_review_audit().unwrap();
5408        assert_eq!(websocket.request_attempt_count(), 1);
5409        assert_eq!(websocket.request_admitted_count(), 0);
5410        assert_eq!(websocket.connection_attempt_count(), 0);
5411        assert_eq!(websocket.handshake_completed_count(), 0);
5412        let context = websocket.supplied_session_context().unwrap();
5413        assert_eq!(
5414            context.qualification(),
5415            WebSocketSuppliedSessionQualification::NotEstablished
5416        );
5417        assert_eq!(
5418            context
5419                .startup_health()
5420                .map(WebSocketSuppliedSessionHealthAudit::outcome),
5421            Some("healthy")
5422        );
5423        assert!(context.terminal_health().is_none());
5424        assert!(websocket.is_consistent());
5425
5426        let diagnostic = format!("{websocket:?}");
5427        for forbidden in [
5428            "ZERO-ADMISSION-SECRET-CANARY",
5429            "PRIVATE-ZERO-ADMISSION-CANARY",
5430            "/app/private",
5431            "/app/socket",
5432        ] {
5433            assert!(!diagnostic.contains(forbidden));
5434        }
5435        assert_eq!(
5436            observed.lock().await.clone(),
5437            vec!["/app/session-health".to_owned(), "/app/private".to_owned()]
5438        );
5439        server.abort();
5440    }
5441
5442    #[cfg(feature = "websocket-review")]
5443    #[tokio::test]
5444    async fn completed_combined_ws_prefix_survives_later_ordinary_runtime_failure() {
5445        const SECRET: &str = "Bearer FAILURE-RECEIPT-SECRET-CANARY";
5446        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5447        let address = listener.local_addr().unwrap();
5448        let server = tokio::spawn(async move {
5449            loop {
5450                let Ok((mut stream, _)) = listener.accept().await else {
5451                    break;
5452                };
5453                tokio::spawn(async move {
5454                    let mut preview = [0_u8; 4096];
5455                    let Ok(Ok(preview_len)) =
5456                        tokio::time::timeout(Duration::from_secs(1), stream.peek(&mut preview))
5457                            .await
5458                    else {
5459                        return;
5460                    };
5461                    let preview = String::from_utf8_lossy(&preview[..preview_len]);
5462                    if preview.starts_with("GET /app/socket HTTP/1.1\r\n") {
5463                        let accepted = tokio_tungstenite::accept_hdr_async(
5464                            stream,
5465                            ExactAuthorizationCallback(SECRET),
5466                        )
5467                        .await;
5468                        if let Ok(mut socket) = accepted {
5469                            if socket.next().await.is_some() {
5470                                let _ = socket
5471                                    .send(tokio_tungstenite::tungstenite::Message::Text(
5472                                        "expected".into(),
5473                                    ))
5474                                    .await;
5475                            }
5476                            let _ = socket.close(None).await;
5477                        }
5478                        return;
5479                    }
5480
5481                    let mut request = Vec::new();
5482                    loop {
5483                        let mut chunk = [0_u8; 1024];
5484                        let read = stream.read(&mut chunk).await.unwrap_or(0);
5485                        if read == 0 {
5486                            break;
5487                        }
5488                        request.extend_from_slice(&chunk[..read]);
5489                        if request.windows(4).any(|window| window == b"\r\n\r\n")
5490                            || request.len() > 16 * 1024
5491                        {
5492                            break;
5493                        }
5494                    }
5495                    let text = String::from_utf8_lossy(&request);
5496                    let path = text
5497                        .lines()
5498                        .next()
5499                        .and_then(|line| line.split_ascii_whitespace().nth(1))
5500                        .unwrap_or("");
5501                    if path == "/app/" {
5502                        let _ = stream.shutdown().await;
5503                        return;
5504                    }
5505                    let (media_type, body) = match path {
5506                        "/app/session-health" => ("application/json", r#"{"authenticated":true}"#),
5507                        "/app/private" => ("text/html", "PRIVATE-FAILURE-BODY-CANARY"),
5508                        _ => ("text/html", "unexpected"),
5509                    };
5510                    let response = format!(
5511                        "HTTP/1.1 200 OK\r\nContent-Type: {media_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
5512                        body.len()
5513                    );
5514                    let _ = stream.write_all(response.as_bytes()).await;
5515                    let _ = stream.shutdown().await;
5516                });
5517            }
5518        });
5519
5520        let application = url::Url::parse(&format!("http://{address}/app/")).unwrap();
5521        let endpoint = url::Url::parse(&format!("ws://{address}/app/socket")).unwrap();
5522        let mut runtime = WebAssessmentRuntimeBuilder::new(application.clone())
5523            .with_supplied_session_review(
5524                policy(&application),
5525                SuppliedSessionAuthorization::new(SECRET).unwrap(),
5526            )
5527            .with_websocket_review(websocket_policy(&application, &endpoint, 2_000))
5528            .with_websocket_supplied_session()
5529            .build()
5530            .unwrap();
5531        let error = runtime
5532            .analyze()
5533            .await
5534            .expect_err("ordinary root failure must preserve the completed combined prefix");
5535        let receipt = error.failure_receipt().expect("started failure receipt");
5536
5537        let session = receipt.supplied_session_audit().unwrap();
5538        assert_eq!(session.outcome(), SuppliedSessionAuditOutcome::Complete);
5539        assert_eq!(session.checkpoints().len(), 2);
5540        let websocket = receipt.websocket_review_audit().unwrap();
5541        assert_eq!(
5542            websocket.schema(),
5543            crate::web_runtime::WEBSOCKET_SUPPLIED_SESSION_AUDIT_SCHEMA
5544        );
5545        assert_eq!(websocket.request_attempt_count(), 1);
5546        assert_eq!(websocket.request_admitted_count(), 1);
5547        assert_eq!(websocket.handshake_completed_count(), 1);
5548        assert_eq!(websocket.outbound_message_count(), 1);
5549        assert_eq!(websocket.inbound_message_count(), 1);
5550        assert_eq!(websocket.matched_response_count(), 1);
5551        assert_eq!(
5552            websocket
5553                .supplied_session_context()
5554                .unwrap()
5555                .qualification(),
5556            WebSocketSuppliedSessionQualification::ContextQualified
5557        );
5558        assert!(websocket.is_consistent());
5559
5560        let diagnostic = format!("{error:?}{websocket:?}");
5561        for forbidden in [
5562            "FAILURE-RECEIPT-SECRET-CANARY",
5563            "PRIVATE-FAILURE-BODY-CANARY",
5564            "/app/private",
5565            "/app/socket",
5566        ] {
5567            assert!(!diagnostic.contains(forbidden));
5568        }
5569        server.abort();
5570    }
5571
5572    #[cfg(feature = "websocket-review")]
5573    #[tokio::test]
5574    async fn supplied_session_deadline_bounds_websocket_and_leaves_context_unqualified() {
5575        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5576        let address = listener.local_addr().unwrap();
5577        let http_observed = Arc::new(Mutex::new(Vec::<(String, bool)>::new()));
5578        let websocket_authorized = Arc::new(AtomicBool::new(false));
5579        let server_http_observed = http_observed.clone();
5580        let server_websocket_authorized = websocket_authorized.clone();
5581        let server = tokio::spawn(async move {
5582            loop {
5583                let Ok((mut stream, _)) = listener.accept().await else {
5584                    break;
5585                };
5586                let observed = server_http_observed.clone();
5587                let websocket_authorized = server_websocket_authorized.clone();
5588                tokio::spawn(async move {
5589                    let mut preview = [0_u8; 4096];
5590                    let Ok(Ok(preview_len)) =
5591                        tokio::time::timeout(Duration::from_secs(1), stream.peek(&mut preview))
5592                            .await
5593                    else {
5594                        return;
5595                    };
5596                    let preview = String::from_utf8_lossy(&preview[..preview_len]);
5597                    if preview.starts_with("GET /app/socket HTTP/1.1\r\n") {
5598                        let accepted = tokio_tungstenite::accept_hdr_async(
5599                            stream,
5600                            ObserveAuthorizationCallback {
5601                                expected: b"Bearer SESSION-DEADLINE-CANARY",
5602                                observed: websocket_authorized,
5603                            },
5604                        )
5605                        .await;
5606                        if let Ok(mut socket) = accepted {
5607                            let _ = socket.next().await;
5608                            tokio::time::sleep(Duration::from_secs(2)).await;
5609                        }
5610                        return;
5611                    }
5612
5613                    let mut request = Vec::new();
5614                    loop {
5615                        let mut chunk = [0_u8; 1024];
5616                        let read = stream.read(&mut chunk).await.unwrap_or(0);
5617                        if read == 0 {
5618                            break;
5619                        }
5620                        request.extend_from_slice(&chunk[..read]);
5621                        if request.windows(4).any(|window| window == b"\r\n\r\n")
5622                            || request.len() > 16 * 1024
5623                        {
5624                            break;
5625                        }
5626                    }
5627                    let text = String::from_utf8_lossy(&request);
5628                    let path = text
5629                        .lines()
5630                        .next()
5631                        .and_then(|line| line.split_ascii_whitespace().nth(1))
5632                        .unwrap_or("")
5633                        .to_owned();
5634                    let authorized = text.lines().any(|line| {
5635                        line.eq_ignore_ascii_case("authorization: Bearer SESSION-DEADLINE-CANARY")
5636                    });
5637                    observed.lock().await.push((path.clone(), authorized));
5638                    let (media_type, body) = match path.as_str() {
5639                        "/app/session-health" => ("application/json", r#"{"authenticated":true}"#),
5640                        "/app/private" => ("text/html", "PRIVATE-DEADLINE-BODY"),
5641                        _ => ("text/html", "<!doctype html><title>public root</title>"),
5642                    };
5643                    let response = format!(
5644                        "HTTP/1.1 200 OK\r\nContent-Type: {media_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
5645                        body.len()
5646                    );
5647                    let _ = stream.write_all(response.as_bytes()).await;
5648                    let _ = stream.shutdown().await;
5649                });
5650            }
5651        });
5652
5653        let application = url::Url::parse(&format!("http://{address}/app/")).unwrap();
5654        let endpoint = url::Url::parse(&format!("ws://{address}/app/socket")).unwrap();
5655        let mut runtime = WebAssessmentRuntimeBuilder::new(application.clone())
5656            .with_supplied_session_review(
5657                policy_with_wall_time(&application, 250),
5658                SuppliedSessionAuthorization::new("Bearer SESSION-DEADLINE-CANARY").unwrap(),
5659            )
5660            .with_websocket_review(websocket_policy(&application, &endpoint, 2_000))
5661            .with_websocket_supplied_session()
5662            .build()
5663            .unwrap();
5664        let report = runtime.analyze().await.unwrap();
5665        let session = report.supplied_session_audit().unwrap();
5666        assert_eq!(session.outcome(), SuppliedSessionAuditOutcome::RuntimeLimit);
5667        assert_eq!(session.checkpoints().len(), 1);
5668        assert_eq!(session.committed_resource_count(), 0);
5669        assert_eq!(
5670            session.resources()[0].outcome(),
5671            SuppliedSessionResourceOutcome::HealthUnqualified
5672        );
5673
5674        let websocket = report.websocket_review_audit().unwrap();
5675        assert_eq!(
5676            websocket.schema(),
5677            crate::web_runtime::WEBSOCKET_SUPPLIED_SESSION_AUDIT_SCHEMA
5678        );
5679        assert_eq!(
5680            websocket.terminal(),
5681            crate::web_runtime::WebSocketReviewTerminal::DeadlineReached
5682        );
5683        assert_eq!(websocket.request_attempt_count(), 1);
5684        assert_eq!(websocket.request_admitted_count(), 1);
5685        assert_eq!(websocket.handshake_completed_count(), 1);
5686        let context = websocket.supplied_session_context().unwrap();
5687        assert_eq!(
5688            context.qualification(),
5689            WebSocketSuppliedSessionQualification::ContextUnqualified
5690        );
5691        assert!(context.startup_health().is_some_and(|health| {
5692            health.outcome() == "healthy" && health.evidence_reference().is_some()
5693        }));
5694        assert_eq!(context.terminal_health(), None);
5695        assert!(websocket.is_consistent());
5696        assert!(websocket_authorized.load(Ordering::SeqCst));
5697
5698        let observed = http_observed.lock().await.clone();
5699        assert_eq!(
5700            observed
5701                .iter()
5702                .filter(|(path, authorized)| { path == "/app/session-health" && *authorized })
5703                .count(),
5704            1,
5705            "the elapsed child deadline must prevent a terminal-health dispatch"
5706        );
5707        assert!(observed
5708            .iter()
5709            .any(|(path, authorized)| path == "/app/private" && *authorized));
5710        assert!(observed
5711            .iter()
5712            .any(|(path, authorized)| path == "/app/" && !authorized));
5713        server.abort();
5714    }
5715
5716    #[cfg(feature = "wordpress-review")]
5717    #[tokio::test]
5718    async fn wordpress_integration_commits_health_qualified_html_before_anonymous_metadata() {
5719        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5720        let address = listener.local_addr().unwrap();
5721        let observed = Arc::new(Mutex::new(Vec::<(String, bool)>::new()));
5722        let server_observed = observed.clone();
5723        let server = tokio::spawn(async move {
5724            loop {
5725                let Ok((mut stream, _)) = listener.accept().await else {
5726                    break;
5727                };
5728                let mut request = Vec::new();
5729                loop {
5730                    let mut chunk = [0_u8; 1024];
5731                    let read = stream.read(&mut chunk).await.unwrap_or(0);
5732                    if read == 0 {
5733                        break;
5734                    }
5735                    request.extend_from_slice(&chunk[..read]);
5736                    if request.windows(4).any(|window| window == b"\r\n\r\n")
5737                        || request.len() > 16 * 1024
5738                    {
5739                        break;
5740                    }
5741                }
5742                let request = String::from_utf8_lossy(&request);
5743                let request_line = request.lines().next().unwrap_or_default();
5744                let method = request_line
5745                    .split_ascii_whitespace()
5746                    .next()
5747                    .unwrap_or_default();
5748                let path = request_line
5749                    .split_ascii_whitespace()
5750                    .nth(1)
5751                    .unwrap_or_default()
5752                    .to_owned();
5753                let authorized = request.lines().any(|line| {
5754                    line.eq_ignore_ascii_case("authorization: Bearer WORDPRESS-SESSION-CANARY")
5755                });
5756                server_observed
5757                    .lock()
5758                    .await
5759                    .push((path.clone(), authorized));
5760                let (media_type, body) = match path.as_str() {
5761                    "/app/" => (
5762                        "text/html",
5763                        r#"<meta name="generator" content="WordPress 6.9.4">
5764                           <link rel="https://api.w.org/" href="/app/wp-json/">
5765                           <link rel="stylesheet" href="/app/wp-content/themes/session-theme/style.css">"#,
5766                    ),
5767                    "/app/session-health" => ("application/json", r#"{"authenticated":true}"#),
5768                    "/app/private" => (
5769                        "text/html",
5770                        r#"<script src="/app/wp-content/plugins/private-component/assets/site.js"></script>
5771                           <link rel="stylesheet" href="/app/wp-content/plugins/private-component/assets/site.css">"#,
5772                    ),
5773                    "/app/wp-json/" => ("application/json", r#"{"namespaces":["wp/v2"]}"#),
5774                    "/app/wp-content/themes/session-theme/style.css" => (
5775                        "text/css",
5776                        "/*\nTheme Name: Session Theme\nVersion: 1.5\n*/\n",
5777                    ),
5778                    "/app/wp-content/plugins/private-component/readme.txt" => (
5779                        "text/plain",
5780                        "=== Private Component ===\nStable tag: 9.9.9\n",
5781                    ),
5782                    _ => ("text/plain", "not found"),
5783                };
5784                let status = if body == "not found" {
5785                    "404 Not Found"
5786                } else {
5787                    "200 OK"
5788                };
5789                let response = format!(
5790                    "HTTP/1.1 {status}\r\nContent-Type: {media_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
5791                    body.len()
5792                );
5793                let _ = stream.write_all(response.as_bytes()).await;
5794                let _ = stream.shutdown().await;
5795                if method.is_empty() {
5796                    break;
5797                }
5798            }
5799        });
5800
5801        let application = url::Url::parse(&format!("http://{address}/app/")).unwrap();
5802        let mut runtime = WebAssessmentRuntimeBuilder::new(application.clone())
5803            .with_wordpress_review(WordPressReviewInputs::new(None, None))
5804            .with_wordpress_discovery()
5805            .with_supplied_session_review(
5806                policy(&application),
5807                SuppliedSessionAuthorization::new("Bearer WORDPRESS-SESSION-CANARY").unwrap(),
5808            )
5809            .with_wordpress_supplied_session()
5810            .build()
5811            .unwrap();
5812        let report = runtime.analyze().await.unwrap();
5813        let session = report.supplied_session_audit().unwrap();
5814        assert_eq!(session.outcome(), SuppliedSessionAuditOutcome::Complete);
5815        assert_eq!(session.committed_resource_count(), 1);
5816        let wordpress = report.wordpress_review_audit().unwrap();
5817        let discovery = wordpress.discovery().unwrap();
5818        let pages = discovery.supplied_session_pages().unwrap();
5819        assert_eq!(pages.selected_count(), 1);
5820        assert_eq!(pages.committed_count(), 1);
5821        assert_eq!(pages.accepted_count(), 1);
5822        assert_eq!(pages.rejected_count(), 0);
5823        assert_eq!(pages.not_established_count(), 0);
5824        assert_eq!(pages.not_evaluated_count(), 0);
5825        assert_eq!(pages.pages().len(), 1);
5826        let page = &pages.pages()[0];
5827        assert_eq!(page.association().as_str(), "accepted");
5828        assert_eq!(page.outcome().as_str(), "accepted");
5829        assert!(page.resource_evidence_reference().is_some());
5830        assert_eq!(page.evidence_ids().len(), 1);
5831
5832        let private_source = discovery
5833            .sources()
5834            .iter()
5835            .find(|source| source.component_slug() == Some("private-component"))
5836            .expect("health-qualified private page must nominate one plugin readme");
5837        assert!(private_source.source_page_references().is_empty());
5838        assert_eq!(
5839            private_source.source_supplied_session_page_references(),
5840            [page.context_page_reference()]
5841        );
5842        assert!(discovery
5843            .sources()
5844            .iter()
5845            .filter(|source| source.component_slug() != Some("private-component"))
5846            .all(|source| source.source_supplied_session_page_references().is_empty()));
5847
5848        let observed = observed.lock().await.clone();
5849        let root_index = observed
5850            .iter()
5851            .position(|(path, authorized)| path == "/app/" && !authorized)
5852            .unwrap();
5853        let startup_index = observed
5854            .iter()
5855            .position(|(path, authorized)| path == "/app/session-health" && *authorized)
5856            .unwrap();
5857        let metadata_index = observed
5858            .iter()
5859            .position(|(path, authorized)| {
5860                path == "/app/wp-content/plugins/private-component/readme.txt" && !authorized
5861            })
5862            .unwrap();
5863        assert!(root_index < startup_index && startup_index < metadata_index);
5864        assert_eq!(
5865            observed
5866                .iter()
5867                .filter(|(path, _)| path == "/app/private")
5868                .count(),
5869            1
5870        );
5871        assert!(observed
5872            .iter()
5873            .all(|(path, authorized)| { !path.starts_with("/app/wp-content/") || !authorized }));
5874        server.abort();
5875    }
5876
5877    #[tokio::test]
5878    async fn bounded_form_login_uses_one_stateful_attempt_then_existing_health_oracle() {
5879        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5880        let address = listener.local_addr().unwrap();
5881        let observed = Arc::new(Mutex::new(
5882            Vec::<(String, String, bool, bool, bool, String)>::new(),
5883        ));
5884        let server_observed = observed.clone();
5885        let server = tokio::spawn(async move {
5886            loop {
5887                let Ok((mut stream, _)) = listener.accept().await else {
5888                    break;
5889                };
5890                let mut request = Vec::new();
5891                let header_end = loop {
5892                    if let Some(offset) =
5893                        request.windows(4).position(|window| window == b"\r\n\r\n")
5894                    {
5895                        break offset + 4;
5896                    }
5897                    let mut chunk = [0_u8; 1024];
5898                    let read = stream.read(&mut chunk).await.unwrap_or(0);
5899                    if read == 0 || request.len() > 16 * 1024 {
5900                        break request.len();
5901                    }
5902                    request.extend_from_slice(&chunk[..read]);
5903                };
5904                let header_text = String::from_utf8_lossy(&request[..header_end]).into_owned();
5905                let content_length = header_text
5906                    .lines()
5907                    .find_map(|line| {
5908                        line.strip_prefix("content-length: ")
5909                            .or_else(|| line.strip_prefix("Content-Length: "))
5910                    })
5911                    .and_then(|value| value.trim().parse::<usize>().ok())
5912                    .unwrap_or(0);
5913                while request.len() < header_end.saturating_add(content_length) {
5914                    let mut chunk = [0_u8; 1024];
5915                    let read = stream.read(&mut chunk).await.unwrap_or(0);
5916                    if read == 0 || request.len() > 24 * 1024 {
5917                        break;
5918                    }
5919                    request.extend_from_slice(&chunk[..read]);
5920                }
5921                let request_line = header_text.lines().next().unwrap_or("");
5922                let mut parts = request_line.split_ascii_whitespace();
5923                let method = parts.next().unwrap_or("").to_owned();
5924                let path = parts.next().unwrap_or("").to_owned();
5925                let has_cookie = header_text.lines().any(|line| {
5926                    line.eq_ignore_ascii_case("cookie: session=SESSION-RUNTIME-CANARY")
5927                });
5928                let has_authorization = header_text
5929                    .lines()
5930                    .any(|line| line.to_ascii_lowercase().starts_with("authorization:"));
5931                let is_form_encoded = header_text.lines().any(|line| {
5932                    line.eq_ignore_ascii_case("content-type: application/x-www-form-urlencoded")
5933                });
5934                let body = String::from_utf8_lossy(&request[header_end..]).into_owned();
5935                server_observed.lock().await.push((
5936                    method.clone(),
5937                    path.clone(),
5938                    has_cookie,
5939                    has_authorization,
5940                    is_form_encoded,
5941                    body.clone(),
5942                ));
5943
5944                let (status, media_type, extra_headers, response_body) =
5945                    match (method.as_str(), path.as_str()) {
5946                        ("GET", "/app/login") => (
5947                            "200 OK",
5948                            "text/html",
5949                            "",
5950                            concat!(
5951                            "<form action=\"/app/login\" method=\"post\">",
5952                            "<input name=\"user\">",
5953                            "<input type=\"password\" name=\"pass\">",
5954                            "<input type=\"hidden\" name=\"csrf\" value=\"CSRF-RUNTIME-CANARY\">",
5955                            "</form>"
5956                        ),
5957                        ),
5958                        ("POST", "/app/login") => (
5959                            "200 OK",
5960                            "application/json",
5961                            concat!(
5962                                "Set-Cookie: session=SESSION-RUNTIME-CANARY; ",
5963                                "Path=/app/; HttpOnly; SameSite=Lax\r\n"
5964                            ),
5965                            r#"{"login":"received"}"#,
5966                        ),
5967                        ("GET", "/app/session-health") if has_cookie => (
5968                            "200 OK",
5969                            "application/json",
5970                            "",
5971                            r#"{"authenticated":true}"#,
5972                        ),
5973                        ("GET", "/app/session-health") => (
5974                            "200 OK",
5975                            "application/json",
5976                            "",
5977                            r#"{"authenticated":false}"#,
5978                        ),
5979                        ("GET", "/app/private") if has_cookie => (
5980                            "200 OK",
5981                            "text/html",
5982                            "",
5983                            "PRIVATE-FORM-LOGIN-BODY-MUST-NOT-BE-RETAINED",
5984                        ),
5985                        _ => ("200 OK", "text/html", "", "<title>public root</title>"),
5986                    };
5987                let response = format!(
5988                    "HTTP/1.1 {status}\r\nContent-Type: {media_type}\r\n{extra_headers}Content-Length: {}\r\nConnection: close\r\n\r\n{response_body}",
5989                    response_body.len()
5990                );
5991                let _ = stream.write_all(response.as_bytes()).await;
5992                let _ = stream.shutdown().await;
5993            }
5994        });
5995
5996        let application = url::Url::parse(&format!("http://{address}/app/")).unwrap();
5997        let policy = form_login_policy(&application);
5998        let credential = SuppliedSessionFormCredential::parse_tsv(
5999            &policy,
6000            b"username\tUSER-RUNTIME-CANARY\npassword\tPASSWORD-RUNTIME-CANARY\n".to_vec(),
6001        )
6002        .unwrap();
6003        let mut runtime = WebAssessmentRuntimeBuilder::new(application)
6004            .with_supplied_session_review(policy, credential)
6005            .build()
6006            .unwrap();
6007        let report = runtime.analyze().await.unwrap();
6008        let audit = report.supplied_session_audit().unwrap();
6009        assert_eq!(audit.schema(), SUPPLIED_SESSION_FORM_LOGIN_AUDIT_SCHEMA);
6010        assert_eq!(
6011            audit.credential_acquisition(),
6012            SuppliedSessionCredentialAcquisition::BoundedFormLogin
6013        );
6014        assert_eq!(audit.outcome(), SuppliedSessionAuditOutcome::Complete);
6015        assert_eq!(audit.coverage(), SuppliedSessionCoverage::Complete);
6016        assert_eq!(audit.dispatched_request_count(), 5);
6017        assert_eq!(audit.committed_resource_count(), 1);
6018        let login = audit.login().unwrap();
6019        assert_eq!(login.max_attempts(), 1);
6020        assert_eq!(login.attempt_count(), 1);
6021        assert!(login.page_dispatched());
6022        assert_eq!(
6023            login.form_outcome(),
6024            SuppliedSessionLoginFormOutcome::Matched
6025        );
6026        assert_eq!(login.form_error_code(), None);
6027        assert!(login.submit_dispatched());
6028        assert_eq!(
6029            login.submit_outcome(),
6030            SuppliedSessionLoginSubmitOutcome::CookieAcquired
6031        );
6032        assert_eq!(login.acquired_cookie_count(), 1);
6033        assert_eq!(login.initial_epoch(), 0);
6034        assert_eq!(login.final_epoch(), 1);
6035        assert!(!login.pre_session_cookie_applied());
6036        let lifecycle = audit.cookie_lifecycle().unwrap();
6037        assert_eq!(lifecycle.initial_epoch(), 0);
6038        assert_eq!(lifecycle.final_epoch(), 1);
6039        assert_eq!(lifecycle.updates_applied(), 0);
6040        assert_eq!(report.usage().active_verifications(), 1);
6041        assert_eq!(
6042            audit.response_bytes(),
6043            login
6044                .response_bytes()
6045                .saturating_add(
6046                    audit
6047                        .checkpoints()
6048                        .iter()
6049                        .map(WebAssessmentSuppliedSessionCheckpointAudit::response_bytes)
6050                        .sum::<u64>()
6051                )
6052                .saturating_add(
6053                    audit
6054                        .resources()
6055                        .iter()
6056                        .map(WebAssessmentSuppliedSessionResourceAudit::response_bytes)
6057                        .sum::<u64>()
6058                )
6059        );
6060
6061        let observed = observed.lock().await.clone();
6062        assert!(
6063            observed.len() >= 6,
6064            "session child plus public root must run"
6065        );
6066        assert_eq!(observed[0].0, "GET");
6067        assert_eq!(observed[0].1, "/app/login");
6068        assert!(!observed[0].2 && !observed[0].3 && !observed[0].4);
6069        assert_eq!(observed[1].0, "POST");
6070        assert_eq!(observed[1].1, "/app/login");
6071        assert!(!observed[1].2 && !observed[1].3 && observed[1].4);
6072        assert_eq!(
6073            observed[1].5,
6074            "csrf=CSRF-RUNTIME-CANARY&user=USER-RUNTIME-CANARY&pass=PASSWORD-RUNTIME-CANARY"
6075        );
6076        assert_eq!(observed[2].1, "/app/session-health");
6077        assert!(observed[2].2 && !observed[2].3);
6078        assert_eq!(observed[3].1, "/app/private");
6079        assert!(observed[3].2 && !observed[3].3);
6080        assert_eq!(observed[4].1, "/app/session-health");
6081        assert!(observed[4].2 && !observed[4].3);
6082        assert_eq!(observed[5].1, "/app/");
6083        assert!(!observed[5].2 && !observed[5].3);
6084        assert_eq!(
6085            observed
6086                .iter()
6087                .filter(|request| request.0 == "POST" && request.1 == "/app/login")
6088                .count(),
6089            1
6090        );
6091
6092        let saved = serde_json::to_string(audit).unwrap();
6093        let direct = serde_json::to_value(audit).unwrap();
6094        assert_eq!(
6095            direct
6096                .get("credential_acquisition")
6097                .and_then(serde_json::Value::as_str),
6098            Some("bounded_form_login")
6099        );
6100        for forbidden in [
6101            "USER-RUNTIME-CANARY",
6102            "PASSWORD-RUNTIME-CANARY",
6103            "CSRF-RUNTIME-CANARY",
6104            "SESSION-RUNTIME-CANARY",
6105            "PRIVATE-FORM-LOGIN-BODY-MUST-NOT-BE-RETAINED",
6106            "/app/login",
6107            "/app/private",
6108        ] {
6109            assert!(!saved.contains(forbidden));
6110        }
6111        server.abort();
6112    }
6113
6114    async fn run_cookie_update_fixture(
6115        set_cookie: &'static str,
6116        update_path: &'static str,
6117    ) -> (WebAssessmentSuppliedSessionAudit, Vec<(String, bool, bool)>) {
6118        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6119        let address = listener.local_addr().unwrap();
6120        let observed = Arc::new(Mutex::new(Vec::<(String, bool, bool)>::new()));
6121        let server_observed = observed.clone();
6122        let server = tokio::spawn(async move {
6123            loop {
6124                let Ok((mut stream, _)) = listener.accept().await else {
6125                    break;
6126                };
6127                let mut request = Vec::new();
6128                loop {
6129                    let mut chunk = [0_u8; 1024];
6130                    let read = stream.read(&mut chunk).await.unwrap_or(0);
6131                    if read == 0 {
6132                        break;
6133                    }
6134                    request.extend_from_slice(&chunk[..read]);
6135                    if request.windows(4).any(|window| window == b"\r\n\r\n")
6136                        || request.len() > 16 * 1024
6137                    {
6138                        break;
6139                    }
6140                }
6141                let text = String::from_utf8_lossy(&request);
6142                let path = text
6143                    .lines()
6144                    .next()
6145                    .and_then(|line| line.split_ascii_whitespace().nth(1))
6146                    .unwrap_or("")
6147                    .to_owned();
6148                let exact_cookie = text
6149                    .lines()
6150                    .any(|line| line.eq_ignore_ascii_case("cookie: session=COOKIE-RUNTIME-CANARY"));
6151                let has_authorization = text
6152                    .lines()
6153                    .any(|line| line.to_ascii_lowercase().starts_with("authorization:"));
6154                server_observed
6155                    .lock()
6156                    .await
6157                    .push((path.clone(), exact_cookie, has_authorization));
6158                let (media_type, body) = match path.as_str() {
6159                    "/app/session-health" => ("application/json", r#"{"authenticated":true}"#),
6160                    "/app/private" => ("text/html", "COOKIE-PRIVATE-BODY-MUST-NOT-BE-RETAINED"),
6161                    _ => ("text/html", "<title>public root</title>"),
6162                };
6163                let update = if path == update_path { set_cookie } else { "" };
6164                let response = format!(
6165                    "HTTP/1.1 200 OK\r\nContent-Type: {media_type}\r\n{update}Content-Length: {}\r\nConnection: close\r\n\r\n{body}",
6166                    body.len()
6167                );
6168                let _ = stream.write_all(response.as_bytes()).await;
6169                let _ = stream.shutdown().await;
6170            }
6171        });
6172
6173        let application = url::Url::parse(&format!("http://{address}/app/")).unwrap();
6174        let policy = cookie_policy(&application);
6175        let cookies = cookie_credential(&policy);
6176        let mut runtime = WebAssessmentRuntimeBuilder::new(application)
6177            .with_supplied_session_review(policy, cookies)
6178            .build()
6179            .unwrap();
6180        let report = runtime.analyze().await.unwrap();
6181        let audit = report.supplied_session_audit().unwrap().clone();
6182        let observed = observed.lock().await.clone();
6183        server.abort();
6184        (audit, observed)
6185    }
6186
6187    #[tokio::test]
6188    async fn selected_cookie_update_stops_before_following_health_and_is_value_free() {
6189        let (audit, observed) = run_cookie_update_fixture(
6190            "Set-Cookie: session=ROTATED-SECRET; Path=/app/\r\n",
6191            "/app/private",
6192        )
6193        .await;
6194        assert_eq!(audit.schema(), SUPPLIED_SESSION_COOKIE_AUDIT_SCHEMA);
6195        assert_eq!(
6196            audit.outcome(),
6197            SuppliedSessionAuditOutcome::CredentialUpdateRequired
6198        );
6199        assert_eq!(audit.coverage(), SuppliedSessionCoverage::None);
6200        assert_eq!(audit.dispatched_request_count(), 2);
6201        assert_eq!(audit.checkpoints().len(), 1);
6202        assert_eq!(audit.resources().len(), 1);
6203        assert_eq!(
6204            audit.resources()[0].outcome(),
6205            SuppliedSessionResourceOutcome::HealthUnqualified
6206        );
6207        let lifecycle = audit.cookie_lifecycle().unwrap();
6208        assert_eq!(lifecycle.selected_update_response_count(), 1);
6209        assert_eq!(lifecycle.unselected_update_response_count(), 0);
6210        assert_eq!(lifecycle.update_classification_failure_count(), 0);
6211        assert_eq!(lifecycle.initial_epoch(), 1);
6212        assert_eq!(lifecycle.final_epoch(), 1);
6213        assert_eq!(lifecycle.updates_applied(), 0);
6214        assert_eq!(observed[0], ("/app/session-health".to_owned(), true, false));
6215        assert_eq!(observed[1], ("/app/private".to_owned(), true, false));
6216        assert!(observed
6217            .iter()
6218            .skip(2)
6219            .all(|(_, has_cookie, _)| !has_cookie));
6220        let saved = serde_json::to_string(&audit).unwrap();
6221        let direct = serde_json::to_value(&audit).unwrap();
6222        assert_eq!(
6223            direct.get("schema").and_then(serde_json::Value::as_str),
6224            Some(SUPPLIED_SESSION_COOKIE_AUDIT_SCHEMA)
6225        );
6226        assert!(!direct
6227            .as_object()
6228            .unwrap()
6229            .contains_key("credential_acquisition"));
6230        for forbidden in [
6231            "COOKIE-RUNTIME-CANARY",
6232            "ROTATED-SECRET",
6233            "COOKIE-PRIVATE-BODY-MUST-NOT-BE-RETAINED",
6234        ] {
6235            assert!(!saved.contains(forbidden));
6236        }
6237    }
6238
6239    #[tokio::test]
6240    async fn unselected_cookie_update_does_not_mutate_or_stop_the_session() {
6241        let (audit, observed) =
6242            run_cookie_update_fixture("Set-Cookie: analytics=OPAQUE; Path=/\r\n", "/app/private")
6243                .await;
6244        assert_eq!(audit.outcome(), SuppliedSessionAuditOutcome::Complete);
6245        assert_eq!(audit.coverage(), SuppliedSessionCoverage::Complete);
6246        assert_eq!(audit.dispatched_request_count(), 3);
6247        assert_eq!(audit.checkpoints().len(), 2);
6248        assert_eq!(audit.committed_resource_count(), 1);
6249        let lifecycle = audit.cookie_lifecycle().unwrap();
6250        assert_eq!(lifecycle.selected_update_response_count(), 0);
6251        assert_eq!(lifecycle.unselected_update_response_count(), 1);
6252        assert_eq!(lifecycle.update_classification_failure_count(), 0);
6253        assert_eq!(lifecycle.updates_applied(), 0);
6254        assert_eq!(
6255            &observed[..3],
6256            &[
6257                ("/app/session-health".to_owned(), true, false),
6258                ("/app/private".to_owned(), true, false),
6259                ("/app/session-health".to_owned(), true, false),
6260            ]
6261        );
6262    }
6263
6264    #[tokio::test]
6265    async fn selected_startup_cookie_update_stops_before_any_resource() {
6266        let (audit, observed) = run_cookie_update_fixture(
6267            "Set-Cookie: session=ROTATED-AT-STARTUP; Path=/app/\r\n",
6268            "/app/session-health",
6269        )
6270        .await;
6271        assert_eq!(
6272            audit.outcome(),
6273            SuppliedSessionAuditOutcome::CredentialUpdateRequired
6274        );
6275        assert_eq!(audit.coverage(), SuppliedSessionCoverage::None);
6276        assert_eq!(audit.dispatched_request_count(), 1);
6277        assert_eq!(audit.dispatched_resource_count(), 0);
6278        assert_eq!(audit.committed_resource_count(), 0);
6279        assert_eq!(audit.checkpoints().len(), 1);
6280        assert!(audit
6281            .resources()
6282            .iter()
6283            .all(|resource| resource.outcome() == SuppliedSessionResourceOutcome::NotDispatched));
6284        let lifecycle = audit.cookie_lifecycle().unwrap();
6285        assert_eq!(lifecycle.selected_update_response_count(), 1);
6286        assert_eq!(lifecycle.update_classification_failure_count(), 0);
6287        assert_eq!(lifecycle.updates_applied(), 0);
6288        assert_eq!(observed[0], ("/app/session-health".to_owned(), true, false));
6289        assert!(observed
6290            .iter()
6291            .skip(1)
6292            .all(|(_, has_cookie, has_authorization)| !has_cookie && !has_authorization));
6293        assert!(observed.iter().any(|(path, _, _)| path == "/app/"));
6294        assert!(!serde_json::to_string(&audit)
6295            .unwrap()
6296            .contains("ROTATED-AT-STARTUP"));
6297    }
6298
6299    #[tokio::test]
6300    async fn unusable_cookie_update_stops_without_guessing_selected_names() {
6301        let (audit, observed) = run_cookie_update_fixture(
6302            "Set-Cookie: bad name=UNUSABLE-COOKIE-CANARY; Path=/app/\r\n",
6303            "/app/private",
6304        )
6305        .await;
6306        assert_eq!(
6307            audit.outcome(),
6308            SuppliedSessionAuditOutcome::CredentialUpdateUnusable
6309        );
6310        assert_eq!(audit.coverage(), SuppliedSessionCoverage::None);
6311        assert_eq!(audit.dispatched_request_count(), 2);
6312        assert_eq!(audit.dispatched_resource_count(), 1);
6313        assert_eq!(audit.committed_resource_count(), 0);
6314        assert_eq!(
6315            audit.resources()[0].outcome(),
6316            SuppliedSessionResourceOutcome::HealthUnqualified
6317        );
6318        let lifecycle = audit.cookie_lifecycle().unwrap();
6319        assert_eq!(lifecycle.selected_update_response_count(), 0);
6320        assert_eq!(lifecycle.unselected_update_response_count(), 0);
6321        assert_eq!(lifecycle.update_classification_failure_count(), 1);
6322        assert_eq!(lifecycle.updates_applied(), 0);
6323        assert_eq!(
6324            &observed[..2],
6325            &[
6326                ("/app/session-health".to_owned(), true, false),
6327                ("/app/private".to_owned(), true, false),
6328            ]
6329        );
6330        assert!(!serde_json::to_string(&audit)
6331            .unwrap()
6332            .contains("UNUSABLE-COOKIE-CANARY"));
6333    }
6334
6335    #[tokio::test]
6336    async fn unhealthy_post_resource_checkpoint_prevents_authenticated_coverage() {
6337        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6338        let address = listener.local_addr().unwrap();
6339        let observed = Arc::new(Mutex::new(Vec::<(String, bool)>::new()));
6340        let health_count = Arc::new(Mutex::new(0_u8));
6341        let server_observed = observed.clone();
6342        let server_health_count = health_count.clone();
6343        let server = tokio::spawn(async move {
6344            loop {
6345                let Ok((mut stream, _)) = listener.accept().await else {
6346                    break;
6347                };
6348                let mut request = Vec::new();
6349                loop {
6350                    let mut chunk = [0_u8; 1024];
6351                    let read = stream.read(&mut chunk).await.unwrap_or(0);
6352                    if read == 0 {
6353                        break;
6354                    }
6355                    request.extend_from_slice(&chunk[..read]);
6356                    if request.windows(4).any(|window| window == b"\r\n\r\n")
6357                        || request.len() > 16 * 1024
6358                    {
6359                        break;
6360                    }
6361                }
6362                let text = String::from_utf8_lossy(&request);
6363                let path = text
6364                    .lines()
6365                    .next()
6366                    .and_then(|line| line.split_ascii_whitespace().nth(1))
6367                    .unwrap_or("")
6368                    .to_owned();
6369                let authorized = text.lines().any(|line| {
6370                    line.eq_ignore_ascii_case("authorization: Bearer HEALTH-LOSS-CANARY")
6371                });
6372                server_observed
6373                    .lock()
6374                    .await
6375                    .push((path.clone(), authorized));
6376                let (media_type, body) = match path.as_str() {
6377                    "/app/session-health" => {
6378                        let mut count = server_health_count.lock().await;
6379                        *count = count.saturating_add(1);
6380                        if *count == 1 {
6381                            ("application/json", r#"{"authenticated":true}"#)
6382                        } else {
6383                            ("application/json", r#"{"authenticated":false}"#)
6384                        }
6385                    },
6386                    "/app/private" => (
6387                        "text/html",
6388                        "<form>LOGIN-PAGE-BODY-MUST-NOT-BE-RETAINED</form>",
6389                    ),
6390                    _ => ("text/html", "<!doctype html><title>public root</title>"),
6391                };
6392                let response = format!(
6393                    "HTTP/1.1 200 OK\r\nContent-Type: {media_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
6394                    body.len()
6395                );
6396                let _ = stream.write_all(response.as_bytes()).await;
6397                let _ = stream.shutdown().await;
6398            }
6399        });
6400
6401        let application = url::Url::parse(&format!("http://{address}/app/")).unwrap();
6402        let mut runtime = WebAssessmentRuntimeBuilder::new(application.clone())
6403            .with_supplied_session_review(
6404                policy(&application),
6405                SuppliedSessionAuthorization::new("Bearer HEALTH-LOSS-CANARY").unwrap(),
6406            )
6407            .build()
6408            .unwrap();
6409        let report = runtime.analyze().await.unwrap();
6410        let audit = report.supplied_session_audit().unwrap();
6411        assert_eq!(audit.outcome(), SuppliedSessionAuditOutcome::SessionLost);
6412        assert_eq!(audit.coverage(), SuppliedSessionCoverage::None);
6413        assert_eq!(audit.selected_resource_count(), 1);
6414        assert_eq!(audit.dispatched_resource_count(), 1);
6415        assert_eq!(audit.committed_resource_count(), 0);
6416        assert_eq!(audit.dispatched_request_count(), 3);
6417        assert_eq!(audit.checkpoints().len(), 2);
6418        assert_eq!(
6419            audit.checkpoints()[0].outcome(),
6420            SuppliedSessionHealthOutcome::Healthy
6421        );
6422        assert_eq!(
6423            audit.checkpoints()[1].outcome(),
6424            SuppliedSessionHealthOutcome::Unhealthy
6425        );
6426        assert_eq!(audit.resources().len(), 1);
6427        assert_eq!(
6428            audit.resources()[0].outcome(),
6429            SuppliedSessionResourceOutcome::HealthUnqualified
6430        );
6431        assert_eq!(audit.resources()[0].status(), Some(200));
6432        let evidence_reference = audit.resources()[0]
6433            .evidence_reference()
6434            .expect("observed but health-unqualified response keeps a value-safe receipt");
6435        let evidence_id = EvidenceId::parse(evidence_reference).unwrap();
6436        assert_eq!(
6437            runtime
6438                .knowledge()
6439                .inspect_evidence(&evidence_id, |evidence| {
6440                    evidence.kind() == &EvidenceKind::Authentication
6441                        && evidence.predicate().namespace() == RESOURCE_COMMIT_EVIDENCE_NAMESPACE
6442                        && evidence.predicate().name()
6443                            == RESOURCE_HEALTH_UNQUALIFIED_EVIDENCE_PREDICATE
6444                }),
6445            Some(true)
6446        );
6447        let saved = serde_json::to_string(audit).unwrap();
6448        for forbidden in [
6449            "HEALTH-LOSS-CANARY",
6450            "LOGIN-PAGE-BODY-MUST-NOT-BE-RETAINED",
6451            "/app/private",
6452        ] {
6453            assert!(!saved.contains(forbidden));
6454        }
6455
6456        let observed = observed.lock().await.clone();
6457        assert_eq!(
6458            &observed[..3],
6459            &[
6460                ("/app/session-health".to_owned(), true),
6461                ("/app/private".to_owned(), true),
6462                ("/app/session-health".to_owned(), true),
6463            ]
6464        );
6465        server.abort();
6466    }
6467
6468    #[tokio::test]
6469    async fn hostile_resource_redirect_is_not_followed_or_committed() {
6470        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6471        let address = listener.local_addr().unwrap();
6472        let observed = Arc::new(Mutex::new(Vec::<String>::new()));
6473        let server_observed = observed.clone();
6474        let server = tokio::spawn(async move {
6475            loop {
6476                let Ok((mut stream, _)) = listener.accept().await else {
6477                    break;
6478                };
6479                let mut request = Vec::new();
6480                loop {
6481                    let mut chunk = [0_u8; 1024];
6482                    let read = stream.read(&mut chunk).await.unwrap_or(0);
6483                    if read == 0 {
6484                        break;
6485                    }
6486                    request.extend_from_slice(&chunk[..read]);
6487                    if request.windows(4).any(|window| window == b"\r\n\r\n")
6488                        || request.len() > 16 * 1024
6489                    {
6490                        break;
6491                    }
6492                }
6493                let text = String::from_utf8_lossy(&request);
6494                let path = text
6495                    .lines()
6496                    .next()
6497                    .and_then(|line| line.split_ascii_whitespace().nth(1))
6498                    .unwrap_or("")
6499                    .to_owned();
6500                server_observed.lock().await.push(path.clone());
6501                let response = match path.as_str() {
6502                    "/app/session-health" => concat!(
6503                        "HTTP/1.1 200 OK\r\n",
6504                        "Content-Type: application/json\r\n",
6505                        "Content-Length: 22\r\n",
6506                        "Connection: close\r\n\r\n",
6507                        "{\"authenticated\":true}"
6508                    )
6509                    .to_owned(),
6510                    "/app/private" => concat!(
6511                        "HTTP/1.1 302 Found\r\n",
6512                        "Location: /app/redirect-sink\r\n",
6513                        "Content-Length: 0\r\n",
6514                        "Connection: close\r\n\r\n"
6515                    )
6516                    .to_owned(),
6517                    "/app/redirect-sink" => concat!(
6518                        "HTTP/1.1 200 OK\r\n",
6519                        "Content-Type: text/html\r\n",
6520                        "Content-Length: 21\r\n",
6521                        "Connection: close\r\n\r\n",
6522                        "REDIRECT-WAS-FOLLOWED"
6523                    )
6524                    .to_owned(),
6525                    _ => concat!(
6526                        "HTTP/1.1 200 OK\r\n",
6527                        "Content-Type: text/html\r\n",
6528                        "Content-Length: 19\r\n",
6529                        "Connection: close\r\n\r\n",
6530                        "<title>root</title>"
6531                    )
6532                    .to_owned(),
6533                };
6534                let _ = stream.write_all(response.as_bytes()).await;
6535                let _ = stream.shutdown().await;
6536            }
6537        });
6538
6539        let application = url::Url::parse(&format!("http://{address}/app/")).unwrap();
6540        let mut runtime = WebAssessmentRuntimeBuilder::new(application.clone())
6541            .with_supplied_session_review(
6542                policy(&application),
6543                SuppliedSessionAuthorization::new("Bearer REDIRECT-TEST-CANARY").unwrap(),
6544            )
6545            .build()
6546            .unwrap();
6547        let report = runtime.analyze().await.unwrap();
6548        let audit = report.supplied_session_audit().unwrap();
6549        assert_eq!(
6550            audit.outcome(),
6551            SuppliedSessionAuditOutcome::ResourceUnavailable
6552        );
6553        assert_eq!(audit.coverage(), SuppliedSessionCoverage::None);
6554        assert_eq!(audit.dispatched_request_count(), 2);
6555        assert_eq!(audit.checkpoints().len(), 1);
6556        assert_eq!(audit.resources().len(), 1);
6557        assert_eq!(
6558            audit.resources()[0].outcome(),
6559            SuppliedSessionResourceOutcome::RedirectRefused
6560        );
6561        assert_eq!(audit.resources()[0].status(), Some(302));
6562        assert_eq!(audit.resources()[0].evidence_reference(), None);
6563
6564        let observed = observed.lock().await.clone();
6565        assert!(observed.iter().any(|path| path == "/app/private"));
6566        assert!(
6567            observed.iter().all(|path| path != "/app/redirect-sink"),
6568            "the session client must never follow even a same-origin Location"
6569        );
6570        server.abort();
6571    }
6572
6573    #[tokio::test]
6574    async fn pre_cancelled_parent_dispatches_nothing_and_commits_no_session_evidence() {
6575        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6576        let address = listener.local_addr().unwrap();
6577        let application = url::Url::parse(&format!("http://{address}/app/")).unwrap();
6578        let cancellation = tokio_util::sync::CancellationToken::new();
6579        cancellation.cancel();
6580        let mut runtime = WebAssessmentRuntimeBuilder::new(application.clone())
6581            .cancellation_token(cancellation)
6582            .with_supplied_session_review(
6583                policy(&application),
6584                SuppliedSessionAuthorization::new("Bearer CANCELLED-TEST-CANARY").unwrap(),
6585            )
6586            .build()
6587            .unwrap();
6588
6589        let report = runtime.analyze().await.unwrap();
6590        let audit = report.supplied_session_audit().unwrap();
6591        assert_eq!(audit.outcome(), SuppliedSessionAuditOutcome::Cancelled);
6592        assert_eq!(audit.coverage(), SuppliedSessionCoverage::None);
6593        assert_eq!(audit.dispatched_request_count(), 0);
6594        assert_eq!(audit.dispatched_resource_count(), 0);
6595        assert_eq!(audit.committed_resource_count(), 0);
6596        assert_eq!(audit.response_bytes(), 0);
6597        assert!(audit.checkpoints().is_empty());
6598        assert_eq!(audit.resources().len(), 1);
6599        assert_eq!(
6600            audit.resources()[0].outcome(),
6601            SuppliedSessionResourceOutcome::NotDispatched
6602        );
6603        assert_eq!(audit.resources()[0].evidence_reference(), None);
6604        assert_eq!(runtime.knowledge().stats().evidence, 0);
6605        assert_eq!(report.usage().total_requests(), 0);
6606        assert!(report.transport().is_empty());
6607
6608        let no_connection =
6609            tokio::time::timeout(Duration::from_millis(50), listener.accept()).await;
6610        assert!(
6611            no_connection.is_err(),
6612            "pre-cancelled authority must not open a socket"
6613        );
6614    }
6615}