Skip to content
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
437492b
test(network): require committed navigation origin binding
seonghobae Aug 31, 2026
fcf6206
test(network): apply canonical origin-binding test formatting
seonghobae Aug 31, 2026
f9b787e
feat(network): bind committed navigation origin
seonghobae Aug 31, 2026
eaffe68
test(network): cover origin binding registry failures
seonghobae Aug 31, 2026
6fed822
feat(network): export committed navigation origin binding
seonghobae Aug 31, 2026
43a1ee5
fix(network): apply canonical Rust formatting
seonghobae Aug 31, 2026
a088ed1
fix(network): satisfy strict origin binding lint contract
seonghobae Aug 31, 2026
fc739e7
fix(network): apply exact Rust formatting
seonghobae Aug 31, 2026
0869f53
test(network): cover stale navigation origin advance
seonghobae Aug 31, 2026
b9d584d
test(network): remove synthetic coverage fallbacks
seonghobae Aug 31, 2026
f10ba9e
style(network): apply canonical rustfmt diagnostics
seonghobae Aug 31, 2026
da84955
merge: converge navigation origin on teardown authority repair
seonghobae Aug 31, 2026
c59b31c
merge(network): restack committed navigation origin binding
seonghobae Sep 1, 2026
953669f
fix(network): repair exact-head correlation contract checks
seonghobae Sep 2, 2026
9769733
fix(network): restore malformed JSON regression literal
seonghobae Sep 2, 2026
f9d2918
style(network): apply hosted rustfmt diagnostics
seonghobae Sep 2, 2026
127e025
fix(network): remove impossible success-id coverage branch
seonghobae Sep 2, 2026
ccfa13b
chore(network): restack navigation origin binding
seonghobae Sep 4, 2026
84195ec
style(network): apply pinned rustfmt
seonghobae Sep 4, 2026
7cecf40
merge(parent): include rustfmt repair
seonghobae Sep 4, 2026
c1f877b
merge(parent): refresh navigation origin base
seonghobae Sep 4, 2026
95f7548
merge(parent): refresh navigation origin base
seonghobae Sep 4, 2026
7388e6a
merge: restack origin binding on document advance
seonghobae Sep 5, 2026
323ac9e
merge: restack origin binding on current document
seonghobae Sep 5, 2026
934eb7d
merge: adopt current document parent for origin binding
seonghobae Sep 5, 2026
572fc42
chore(stack): adopt repaired document-advance topology
seonghobae Sep 6, 2026
c03e0dc
test: reject pointer reply from replacement connection
seonghobae Sep 6, 2026
80cfa18
merge: adopt connection-bound pointer replies into origin binding
seonghobae Sep 6, 2026
ba100fb
docs: record origin binding receipt parent adoption
seonghobae Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions crates/originweave-network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
//! correlated protocol response, admits a bounded navigation-committed
//! post-condition observation for one exact registered context and URL, rotates
//! the matched context's document epoch only from an exact caller-captured
//! pre-action epoch, sends narrowly typed `session.status` and `session.end`
//! commands, admits typed correlated status and end responses, observes bounded
//! peer Close or clean-EOF transport cessation, and keeps protocol/transport
//! evidence separate from explicit operational teardown observations without
//! exposing generic JSON bodies or granting browser, TLS, policy, secret,
//! process, profile, or Agent authority.
//! pre-action epoch, derives and binds the committed HTTP(S) URL's canonical
//! origin to that newly advanced document, sends narrowly typed `session.status`
//! and `session.end` commands, admits typed correlated status and end responses,
//! observes bounded peer Close or clean-EOF transport cessation, and keeps
//! protocol/transport evidence separate from explicit operational teardown
//! observations without exposing generic JSON bodies or granting browser, TLS,
//! policy, secret, process, profile, or Agent authority.

#![forbid(unsafe_code)]
#![deny(missing_docs)]
Expand All @@ -28,6 +29,7 @@ mod webdriver_bidi_connection;
mod webdriver_bidi_json_envelope;
mod webdriver_bidi_navigation_committed_postcondition;
mod webdriver_bidi_navigation_document_advance;
mod webdriver_bidi_navigation_document_origin;
mod webdriver_bidi_pointer_click_response;
mod webdriver_bidi_pointer_click_transport;
mod webdriver_bidi_session_end_command;
Expand Down Expand Up @@ -72,6 +74,11 @@ pub use webdriver_bidi_navigation_document_advance::{
WebDriverBiDiNavigationCommittedDocumentAdvanceError,
advance_webdriver_bidi_navigation_document_epoch,
};
pub use webdriver_bidi_navigation_document_origin::{
WebDriverBiDiNavigationCommittedDocumentOrigin,
WebDriverBiDiNavigationCommittedDocumentOriginError,
advance_and_bind_webdriver_bidi_navigation_document_origin,
};
pub use webdriver_bidi_pointer_click_response::{
WebDriverBiDiPointerClickResponseError, WebDriverBiDiPointerClickResult,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@ impl WebDriverBiDiCommandCorrelation {
)
}
WebDriverBiDiJsonEnvelopeKind::Success => {
let Some(command_id) = envelope.command_id() else {
return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding);
};
let command_id = envelope
.command_id()
.expect("validated WebDriver BiDi success envelopes always carry a command id");
self.complete(
command_id,
expected_kind,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ use std::{
use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint};

use crate::{
WebDriverBiDiCommandCorrelation, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError,
WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiNavigationCommittedObservation,
WebDriverBiDiNavigationCommittedObservationError, WebDriverBiDiSessionStatusResponseError,
WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan,
WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan,
WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly,
WebDriverBiDiWebSocketTextMessage,
WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope,
WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind,
WebDriverBiDiNavigationCommittedObservation, WebDriverBiDiNavigationCommittedObservationError,
WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult,
WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey,
WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler,
WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage,
};

const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef";
Expand Down Expand Up @@ -155,7 +155,7 @@ fn public_json_envelope_unit_build_covers_fail_closed_json_edges() -> Result<(),
fn public_session_status_empty_result_fails_closed_from_unit_build() -> Result<(), Box<dyn Error>> {
let text = read_text_over_loopback(EMPTY_STATUS_RESULT)?;
let mut correlation = WebDriverBiDiCommandCorrelation::new();
correlation.register_command(7)?;
correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?;

let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation);
assert!(matches!(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
use std::{error::Error, fmt};

use originweave_core::{
BrowserAuthorityRegistry, BrowserRegistryError, BrowserSessionId, BrowsingContextId,
DocumentEpoch, Origin,
};

use crate::{
WebDriverBiDiNavigationCommittedDocumentAdvanceError,
WebDriverBiDiNavigationCommittedObservation, advance_webdriver_bidi_navigation_document_epoch,
};

/// Immutable evidence that one accepted navigation rotated its document and bound its observed origin.
///
/// The value records only the registry-local session/context/document transition and the canonical
/// origin derived from the exact serialized URL carried by the accepted WebDriver BiDi navigation
/// observation. It does not authenticate the browser adapter, prove which action caused the
/// navigation, authorize the destination, or grant browser, policy, node, credential, process, or
/// reusable Agent authority.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebDriverBiDiNavigationCommittedDocumentOrigin {
browser_session: BrowserSessionId,
browsing_context: BrowsingContextId,
previous_epoch: DocumentEpoch,
current_epoch: DocumentEpoch,
origin: Origin,
}

impl WebDriverBiDiNavigationCommittedDocumentOrigin {
/// Return the exact OriginWeave browser session whose context advanced.
#[must_use]
pub const fn browser_session(&self) -> BrowserSessionId {
self.browser_session
}

/// Return the exact OriginWeave browsing context whose document advanced.
#[must_use]
pub const fn browsing_context(&self) -> BrowsingContextId {
self.browsing_context
}

/// Return the caller-captured document epoch that was current before the navigation advance.
#[must_use]
pub const fn previous_epoch(&self) -> DocumentEpoch {
self.previous_epoch
}

/// Return the new document epoch to which the observed canonical origin was bound.
#[must_use]
pub const fn current_epoch(&self) -> DocumentEpoch {
self.current_epoch
}

/// Borrow the canonical OriginWeave origin derived from the accepted serialized navigation URL.
#[must_use]
pub const fn origin(&self) -> &Origin {
&self.origin
}
}

/// Fail-closed failures while rotating document authority and binding the observed navigation origin.
#[derive(Debug)]
pub enum WebDriverBiDiNavigationCommittedDocumentOriginError {
/// The accepted serialized navigation URL could not enter canonical OriginWeave origin authority.
InvalidObservedOrigin,
/// The accepted navigation could not rotate the exact caller-captured document epoch.
DocumentAdvance {
/// Underlying typed document-advance failure.
source: WebDriverBiDiNavigationCommittedDocumentAdvanceError,
},
/// The canonical observed origin could not bind to the newly advanced registered document.
RegistryState {
/// Underlying browser-registry authority failure.
source: BrowserRegistryError,
},
}

impl fmt::Display for WebDriverBiDiNavigationCommittedDocumentOriginError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidObservedOrigin => formatter.write_str(
"WebDriver BiDi committed navigation URL cannot enter canonical origin authority",
),
Self::DocumentAdvance { .. } => formatter.write_str(
"WebDriver BiDi committed navigation cannot rotate registered document authority",
),
Self::RegistryState { .. } => formatter.write_str(
"WebDriver BiDi committed navigation origin cannot bind registered document authority",
),
}
}
}

impl Error for WebDriverBiDiNavigationCommittedDocumentOriginError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::InvalidObservedOrigin => None,
Self::DocumentAdvance { source } => Some(source),
Self::RegistryState { source } => Some(source),
}
}
}

fn origin_from_serialized_navigation_url(serialized_url: &str) -> Option<Origin> {
let (scheme, remainder) = serialized_url.split_once("://")?;
let authority_end = remainder.find(['/', '?', '#']).unwrap_or(remainder.len());
let authority = &remainder[..authority_end];
Origin::parse(&format!("{scheme}://{authority}")).ok()
}

fn bind_advanced_document_origin(
registry: &mut BrowserAuthorityRegistry,
browser_session: BrowserSessionId,
browsing_context: BrowsingContextId,
origin: &Origin,
) -> Result<DocumentEpoch, WebDriverBiDiNavigationCommittedDocumentOriginError> {
registry
.bind_context_origin(browser_session, browsing_context, origin)
.map_err(
|source| WebDriverBiDiNavigationCommittedDocumentOriginError::RegistryState { source },
)
}

/// Consume one accepted navigation, rotate the exact expected document, and bind its canonical origin.
///
/// Origin derivation is completed before any registry mutation. Only serialized HTTP(S) URLs whose
/// authority can enter [`Origin`] are accepted; credential-bearing, opaque, malformed, insecure
/// remote HTTP, and otherwise unsupported authorities therefore fail before document rotation.
/// The accepted observation is then consumed by the existing exact-epoch document-advance boundary,
/// which clears stale origin and node authority. Finally, the derived canonical origin is bound to
/// that newly advanced document through the canonical browser registry lifecycle.
///
/// The caller must still treat the returned value as immediate-use registry evidence rather than as
/// proof of action causality, browser authenticity, destination authorization, or reusable authority.
pub fn advance_and_bind_webdriver_bidi_navigation_document_origin(
observation: WebDriverBiDiNavigationCommittedObservation,
registry: &mut BrowserAuthorityRegistry,
expected_previous_epoch: DocumentEpoch,
) -> Result<
WebDriverBiDiNavigationCommittedDocumentOrigin,
WebDriverBiDiNavigationCommittedDocumentOriginError,
> {
let origin = origin_from_serialized_navigation_url(observation.url())
.ok_or(WebDriverBiDiNavigationCommittedDocumentOriginError::InvalidObservedOrigin)?;
let advance = advance_webdriver_bidi_navigation_document_epoch(
observation,
registry,
expected_previous_epoch,
)
.map_err(|source| {
WebDriverBiDiNavigationCommittedDocumentOriginError::DocumentAdvance { source }
})?;
bind_advanced_document_origin(
registry,
advance.browser_session(),
advance.browsing_context(),
&origin,
)
.map(
|current_epoch| WebDriverBiDiNavigationCommittedDocumentOrigin {
browser_session: advance.browser_session(),
browsing_context: advance.browsing_context(),
previous_epoch: advance.previous_epoch(),
current_epoch,
origin,
},
)
}

#[cfg(test)]
mod tests {
use super::*;

fn browser_sessions(value: u64) -> Vec<BrowserSessionId> {
BrowserSessionId::new(value).into_iter().collect()
}

fn browsing_contexts(value: u64) -> Vec<BrowsingContextId> {
BrowsingContextId::new(value).into_iter().collect()
}

#[test]
fn serialized_navigation_origin_is_canonical_and_rejects_non_authority_urls() {
let canonical = origin_from_serialized_navigation_url(
"HTTPS://EXAMPLE.TEST:443/path?query=value#fragment",
);
assert_eq!(
canonical.as_ref().map(Origin::as_str),
Some("https://example.test")
);
assert!(origin_from_serialized_navigation_url("https://user@example.test/path").is_none());
assert!(origin_from_serialized_navigation_url("data:text/plain,originweave").is_none());
assert!(origin_from_serialized_navigation_url("http://example.test/path").is_none());
assert_eq!(
origin_from_serialized_navigation_url("http://127.0.0.1:8080/path")
.as_ref()
.map(Origin::as_str),
Some("http://127.0.0.1:8080")
);
}

#[test]
fn binding_helper_preserves_registry_failure_source() {
let mut registry = BrowserAuthorityRegistry::new();
let sessions = browser_sessions(1);
let contexts = browsing_contexts(1);
assert_eq!(sessions.len(), 1);
assert_eq!(contexts.len(), 1);

let origins = origin_from_serialized_navigation_url("https://example.test")
.into_iter()
.collect::<Vec<_>>();
assert_eq!(origins.len(), 1);
assert_eq!(origins[0].as_str(), "https://example.test");

let errors =
bind_advanced_document_origin(&mut registry, sessions[0], contexts[0], &origins[0])
.err()
.into_iter()
.collect::<Vec<_>>();
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].to_string(),
"WebDriver BiDi committed navigation origin cannot bind registered document authority"
);
assert_eq!(
errors[0]
.source()
.and_then(|source| source.downcast_ref::<BrowserRegistryError>()),
Some(&BrowserRegistryError::UnknownBrowserSession)
);
}

#[test]
fn public_diagnostics_preserve_only_causal_sources() {
let invalid = WebDriverBiDiNavigationCommittedDocumentOriginError::InvalidObservedOrigin;
assert_eq!(
invalid.to_string(),
"WebDriver BiDi committed navigation URL cannot enter canonical origin authority"
);
assert!(invalid.source().is_none());

let advance = WebDriverBiDiNavigationCommittedDocumentOriginError::DocumentAdvance {
source: WebDriverBiDiNavigationCommittedDocumentAdvanceError::UnexpectedDocumentEpoch,
};
assert_eq!(
advance.to_string(),
"WebDriver BiDi committed navigation cannot rotate registered document authority"
);
assert!(advance.source().is_some());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ use std::{error::Error, fmt};

use crate::{
WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError,
WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome,
WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError,
WebDriverBiDiWebSocketTextMessage,
WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope,
WebDriverBiDiJsonEnvelopeError, WebDriverBiDiWebSocketTextMessage,
};

/// Typed protocol acknowledgment for one correlated WebDriver BiDi `input.performActions`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use originweave_core::WebDriverBiDiPointerClickCommand;

use crate::{
WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError,
WebDriverBiDiCommandKind, WebDriverBiDiWebSocketEstablished,
WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketMaskKey,
WebDriverBiDiCommandKind, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError,
WebDriverBiDiWebSocketMaskKey,
};

/// Fail-closed errors while transporting one already validated pointer-click command.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ impl WebDriverBiDiSessionStatusResult {
retain_validated_error_code(envelope.error_code()).and_then(|error_code| {
let completed = correlation
.correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionStatus)
.map_err(|source| {
WebDriverBiDiSessionStatusResponseError::Correlation { source }
})?;
.map_err(
|source| WebDriverBiDiSessionStatusResponseError::Correlation {
source,
},
)?;
Err(
WebDriverBiDiSessionStatusResponseError::RemoteProtocolError {
command_id: completed.command_id(),
Expand Down
Loading
Loading