Skip to content
102 changes: 102 additions & 0 deletions crates/originweave-core/src/browser_protocol_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,44 @@ impl<'a> BrowserContextOriginDispatchTarget<'a> {
}
}

/// Exact browser context, canonical origin, and observed document epoch for one protocol dispatch.
///
/// This target is intended for actions whose authority was derived from a prior structured browser
/// observation. Construction grants no authority. The dispatch boundary must revalidate the
/// session/context/origin and prove that the registry is still at `expected_epoch` immediately
/// before protocol metadata validation and callback execution.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BrowserContextOriginEpochDispatchTarget<'a> {
context_origin: BrowserContextOriginDispatchTarget<'a>,
expected_epoch: DocumentEpoch,
}

impl<'a> BrowserContextOriginEpochDispatchTarget<'a> {
/// Bind one immediate-use context/origin target to the document epoch that was observed.
#[must_use]
pub const fn new(
context_origin: BrowserContextOriginDispatchTarget<'a>,
expected_epoch: DocumentEpoch,
) -> Self {
Self {
context_origin,
expected_epoch,
}
}

/// Return the exact browser context and canonical origin requested for dispatch.
#[must_use]
pub const fn context_origin(self) -> BrowserContextOriginDispatchTarget<'a> {
self.context_origin
}

/// Return the exact document epoch whose observation authorized the requested action.
#[must_use]
pub const fn expected_epoch(self) -> DocumentEpoch {
self.expected_epoch
}
}

impl BrowserProtocolAdapterDescriptor {
/// Validate current browser-protocol metadata and immediately invoke one dispatch callback.
///
Expand Down Expand Up @@ -223,13 +261,70 @@ impl BrowserProtocolAdapterDescriptor {
)
.map_err(BrowserContextProtocolDispatchError::ProtocolValidation)
}

/// Revalidate exact browser session/context/origin/document authority before protocol I/O.
///
/// This stronger action boundary first proves the exact current session/context/origin through
/// the authority registry, then compares the registry's current document epoch with the epoch
/// that produced the caller's observation. A same-origin navigation therefore fails closed
/// before protocol validation or callback execution even when the canonical origin is rebound.
/// Exact protocol generation, family, adapter version, protocol/browser revisions and required
/// capability are validated only after the document remains current.
///
/// The caller remains responsible for deriving the origin and observed epoch from the trusted
/// adapter/observation that produced the action, sampling runtime protocol metadata from the
/// adapter about to perform I/O, and preventing intervening mutation across the larger
/// transaction. This method does not authenticate Chromium, authorize destination/network
/// authority or policy approval, validate semantic node state, perform I/O, or prove success.
pub fn dispatch_if_context_origin_epoch_current<R, F>(
&self,
authority_registry: &BrowserAuthorityRegistry,
target: BrowserContextOriginEpochDispatchTarget<'_>,
required_originweave_protocol_version: OriginWeaveProtocolVersion,
runtime_metadata: BrowserProtocolRuntimeMetadata<'_>,
required_capability: BrowserProtocolCapability,
dispatch: F,
) -> Result<R, BrowserContextProtocolDispatchError>
where
F: FnOnce(ValidatedBrowserProtocolUse, DocumentEpoch) -> R,
{
let context_origin = target.context_origin();
let context = context_origin.context();
let current_epoch = authority_registry
.require_context_origin(
context.browser_session(),
context.browsing_context(),
context_origin.expected_origin(),
)
.map_err(BrowserContextProtocolDispatchError::BrowserAuthority)?;
if current_epoch != target.expected_epoch() {
return Err(BrowserContextProtocolDispatchError::DocumentEpochMismatch {
expected: target.expected_epoch(),
current: current_epoch,
});
}
self.dispatch_if_runtime_matches(
required_originweave_protocol_version,
runtime_metadata,
required_capability,
|validated| dispatch(validated, current_epoch),
)
.map_err(BrowserContextProtocolDispatchError::ProtocolValidation)
}
}

/// Failure to compose current browser context ownership with protocol validation before dispatch.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowserContextProtocolDispatchError {
/// The supplied browser session/context pair is not current in the authority registry.
BrowserAuthority(BrowserRegistryError),
/// The observed document epoch no longer matches the registry's current document.
DocumentEpochMismatch {
/// The document epoch that produced the action's observation.
expected: DocumentEpoch,
/// The document epoch currently active in the registry.
current: DocumentEpoch,
},
/// The current browser-protocol metadata or required capability failed validation.
ProtocolValidation(BrowserProtocolUseValidationError),
}
Expand All @@ -243,6 +338,12 @@ impl fmt::Display for BrowserContextProtocolDispatchError {
"browser context authority denied protocol dispatch: {error}"
)
}
Self::DocumentEpochMismatch { expected, current } => write!(
formatter,
"browser document epoch {} no longer matches observed epoch {}",
current.value(),
expected.value()
),
Self::ProtocolValidation(error) => {
write!(
formatter,
Expand All @@ -257,6 +358,7 @@ impl std::error::Error for BrowserContextProtocolDispatchError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::BrowserAuthority(error) => Some(error),
Self::DocumentEpochMismatch { .. } => None,
Self::ProtocolValidation(error) => Some(error),
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ pub use browser_protocol::{
};
pub use browser_protocol_dispatch::{
BrowserContextDispatchTarget, BrowserContextOriginDispatchTarget,
BrowserContextProtocolDispatchError, BrowserProtocolRuntimeMetadata,
BrowserContextOriginEpochDispatchTarget, BrowserContextProtocolDispatchError,
BrowserProtocolRuntimeMetadata,
};
pub use browser_registry::{
BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
use std::{cell::Cell, error::Error, io};

use originweave_core::{
BrowserAuthorityRegistry, BrowserContextDispatchTarget, BrowserContextOriginDispatchTarget,
BrowserContextOriginEpochDispatchTarget, BrowserContextProtocolDispatchError,
BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolKind,
BrowserProtocolRuntimeMetadata, DocumentEpoch, Origin, OriginWeaveProtocolVersion,
ValidatedBrowserProtocolUse,
};

const ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion =
OriginWeaveProtocolVersion::new(0, 1);
const ADAPTER_VERSION: &str = "originweave-bidi-v1";
const PROTOCOL_REVISION: &str = "webdriver-bidi-wd-2026-06-01";
const BROWSER_REVISION: &str = "chromium-r1639810";

type DispatchOutcome = Result<(u64, BrowserProtocolCapability), &'static str>;
type DispatchFn = fn(ValidatedBrowserProtocolUse, DocumentEpoch) -> DispatchOutcome;

thread_local! {
static DISPATCH_CALLED: Cell<bool> = const { Cell::new(false) };
}

fn descriptor() -> Result<BrowserProtocolAdapterDescriptor, Box<dyn Error>> {
Ok(BrowserProtocolAdapterDescriptor::new(
BrowserProtocolKind::WebDriverBiDi,
ORIGINWEAVE_PROTOCOL_VERSION,
ADAPTER_VERSION,
PROTOCOL_REVISION,
BROWSER_REVISION,
&[BrowserProtocolCapability::TypedInput],
)?)
}

fn runtime_metadata() -> BrowserProtocolRuntimeMetadata<'static> {
BrowserProtocolRuntimeMetadata::new(
BrowserProtocolKind::WebDriverBiDi,
ADAPTER_VERSION,
PROTOCOL_REVISION,
BROWSER_REVISION,
)
}

fn origin(value: &str) -> Result<Origin, Box<dyn Error>> {
Origin::parse(value).map_err(|_| {
Box::new(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid controlled origin fixture",
)) as Box<dyn Error>
})
}

fn reset_dispatch_marker() {
DISPATCH_CALLED.with(|called| called.set(false));
}

fn dispatch_was_called() -> bool {
DISPATCH_CALLED.with(Cell::get)
}

fn successful_dispatch(
validated: ValidatedBrowserProtocolUse,
current_epoch: DocumentEpoch,
) -> DispatchOutcome {
DISPATCH_CALLED.with(|called| called.set(true));
Ok((current_epoch.value(), validated.capability()))
}

#[test]
fn exact_context_origin_epoch_and_protocol_metadata_gate_one_dispatch_call()
-> Result<(), Box<dyn Error>> {
let descriptor = descriptor()?;
let mut registry = BrowserAuthorityRegistry::new();
let session = registry.register_session("webdriver-session")?;
let context = registry.register_context(session, "top-level-context")?;
let expected_origin = origin("https://app.example")?;
let expected_epoch = registry.bind_context_origin(session, context, &expected_origin)?;
let context_origin = BrowserContextOriginDispatchTarget::new(
BrowserContextDispatchTarget::new(session, context),
&expected_origin,
);
let target = BrowserContextOriginEpochDispatchTarget::new(context_origin, expected_epoch);

assert_eq!(target.context_origin(), context_origin);
assert_eq!(target.expected_epoch(), expected_epoch);
reset_dispatch_marker();

let result = descriptor.dispatch_if_context_origin_epoch_current(
&registry,
target,
ORIGINWEAVE_PROTOCOL_VERSION,
runtime_metadata(),
BrowserProtocolCapability::TypedInput,
successful_dispatch as DispatchFn,
)?;

assert!(dispatch_was_called());
assert_eq!(result, Ok((1, BrowserProtocolCapability::TypedInput)));
Ok(())
}

#[test]
fn same_origin_new_document_epoch_fails_before_protocol_dispatch() -> Result<(), Box<dyn Error>> {
let descriptor = descriptor()?;
let mut registry = BrowserAuthorityRegistry::new();
let session = registry.register_session("webdriver-session")?;
let context = registry.register_context(session, "top-level-context")?;
let expected_origin = origin("https://app.example")?;
let observed_epoch = registry.bind_context_origin(session, context, &expected_origin)?;
let context_origin = BrowserContextOriginDispatchTarget::new(
BrowserContextDispatchTarget::new(session, context),
&expected_origin,
);
let target = BrowserContextOriginEpochDispatchTarget::new(context_origin, observed_epoch);

let current_epoch = registry.advance_document(context)?;
registry.bind_context_origin(session, context, &expected_origin)?;
reset_dispatch_marker();

let result = descriptor.dispatch_if_context_origin_epoch_current(
&registry,
target,
ORIGINWEAVE_PROTOCOL_VERSION,
runtime_metadata(),
BrowserProtocolCapability::TypedInput,
successful_dispatch as DispatchFn,
);
let error = match result {
Err(error) => error,
Ok(_) => {
return Err(Box::new(io::Error::other(
"stale document epoch unexpectedly dispatched",
)));
}
};

assert_eq!(
error,
BrowserContextProtocolDispatchError::DocumentEpochMismatch {
expected: observed_epoch,
current: current_epoch,
}
);
assert_eq!(
error.to_string(),
"browser document epoch 2 no longer matches observed epoch 1"
);
assert!(error.source().is_none());
assert!(!dispatch_was_called());
Ok(())
}

#[test]
fn epoch_dispatch_preserves_authority_and_protocol_failures_before_callback()
-> Result<(), Box<dyn Error>> {
let descriptor = descriptor()?;
let mut registry = BrowserAuthorityRegistry::new();
let session = registry.register_session("webdriver-session")?;
let context = registry.register_context(session, "top-level-context")?;
let current_origin = origin("https://app.example")?;
let other_origin = origin("https://other.example")?;
let expected_epoch = registry.bind_context_origin(session, context, &current_origin)?;

let wrong_origin_target = BrowserContextOriginEpochDispatchTarget::new(
BrowserContextOriginDispatchTarget::new(
BrowserContextDispatchTarget::new(session, context),
&other_origin,
),
expected_epoch,
);
reset_dispatch_marker();
let authority_result = descriptor.dispatch_if_context_origin_epoch_current(
&registry,
wrong_origin_target,
ORIGINWEAVE_PROTOCOL_VERSION,
runtime_metadata(),
BrowserProtocolCapability::TypedInput,
successful_dispatch as DispatchFn,
);
assert!(matches!(
authority_result,
Err(BrowserContextProtocolDispatchError::BrowserAuthority(_))
));
assert!(!dispatch_was_called());

let current_target = BrowserContextOriginEpochDispatchTarget::new(
BrowserContextOriginDispatchTarget::new(
BrowserContextDispatchTarget::new(session, context),
&current_origin,
),
expected_epoch,
);
let drifted_runtime = BrowserProtocolRuntimeMetadata::new(
BrowserProtocolKind::WebDriverBiDi,
"originweave-bidi-v2",
PROTOCOL_REVISION,
BROWSER_REVISION,
);
reset_dispatch_marker();
let protocol_result = descriptor.dispatch_if_context_origin_epoch_current(
&registry,
current_target,
ORIGINWEAVE_PROTOCOL_VERSION,
drifted_runtime,
BrowserProtocolCapability::TypedInput,
successful_dispatch as DispatchFn,
);
assert!(matches!(
protocol_result,
Err(BrowserContextProtocolDispatchError::ProtocolValidation(_))
));
assert!(!dispatch_was_called());
Ok(())
}
Loading