Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
ab67bdd
test(core): require admitted node authority for pointer click
seonghobae Aug 31, 2026
b7fb56c
test(core): refine pointer node authority regression
seonghobae Aug 31, 2026
e024402
fix(core): retain admitted node wire authority
seonghobae Aug 31, 2026
fcab874
fix(core): bind pointer clicks to current node authority
seonghobae Aug 31, 2026
21fb5a9
fix(core): export pointer click authority boundary
seonghobae Aug 31, 2026
c878cc8
style(core): apply canonical pointer authority formatting
seonghobae Aug 31, 2026
286aeae
style(core): format pointer click authority boundary
seonghobae Aug 31, 2026
57e845a
test(core): preserve fixture error typing
seonghobae Aug 31, 2026
1527fc0
fix(core): close raw pointer command authority bypass
seonghobae Aug 31, 2026
0b40cdf
test(core): prove raw pointer constructor stays private
seonghobae Aug 31, 2026
e663fbe
test(network): construct pointer clicks through node authority
seonghobae Aug 31, 2026
701c8e7
style(network): apply canonical pointer-click test formatting
seonghobae Aug 31, 2026
f7f7a36
test(network): bind pointer-click failure paths to node authority
seonghobae Aug 31, 2026
57d2dfc
test(core): reject exact-tuple fabricated pointer authority
seonghobae Aug 31, 2026
16e7f4c
test(network): bind navigation postcondition clicks to node authority
seonghobae Aug 31, 2026
8b3cec3
test(network): bind pointer-click response path to node authority
seonghobae Aug 31, 2026
c4f7bf1
test(core): exercise pointer command through admitted authority
seonghobae Aug 31, 2026
b5372b7
fix(core): make admitted node authority registry-scoped
seonghobae Aug 31, 2026
42e1afa
style(core): apply canonical pointer authority formatting
seonghobae Aug 31, 2026
ad36988
fix(core): preserve descriptive node admission compatibility
seonghobae Aug 31, 2026
b0b835f
test(core): compare locate-node failures without handle equality
seonghobae Aug 31, 2026
d92a5d6
test(core): preserve failure assertions for opaque admitted handles
seonghobae Aug 31, 2026
40b5a01
style(core): apply canonical rustfmt diagnostics
seonghobae Aug 31, 2026
fb0d073
test(core): cover pointer authority failure contracts
seonghobae Aug 31, 2026
8bf0f92
fix(core): derive click epoch from origin authority
seonghobae Aug 31, 2026
228594b
test(core): distinguish missing-origin and stale-node clicks
seonghobae Aug 31, 2026
e644bc9
style(core): apply canonical rustfmt diagnostics
seonghobae Aug 31, 2026
f2cf08f
test(core): cover pointer serializer context rejection branches
seonghobae Aug 31, 2026
b3b3d87
test(core): drop unreachable serializer coverage shim
seonghobae Aug 31, 2026
ac11a01
refactor(core): remove duplicate pointer context validation
seonghobae Aug 31, 2026
2a02eb6
style(core): restore canonical source newline
seonghobae Aug 31, 2026
15baa48
test(core): require typed-input proof for pointer clicks
seonghobae Aug 31, 2026
dd0d211
style(core): apply canonical typed-input regression formatting
seonghobae Aug 31, 2026
6a078b0
test(network): require typed-input proof before pointer send
seonghobae Aug 31, 2026
4a089e7
test(core): move typed-input proof to transport boundary
seonghobae Aug 31, 2026
d6cd42a
test(network): reject non-typed-input pointer send proofs
seonghobae Aug 31, 2026
271af62
fix(network): bind pointer send to typed-input proof
seonghobae Aug 31, 2026
4fbd135
test(network): carry typed-input proof through click responses
seonghobae Aug 31, 2026
c733289
style(network): apply canonical pointer-send regression formatting
seonghobae Aug 31, 2026
914b27c
test(network): prove typed input before navigation postcondition
seonghobae Aug 31, 2026
c51aa1b
test(network): remove stale navigation diagnostic imports
seonghobae Aug 31, 2026
371a9a6
docs(core): record admitted pointer-click authority
seonghobae Aug 31, 2026
6f5ebbd
test(network): require click authority at send boundary
seonghobae Aug 31, 2026
cd7ccdb
style(network): apply rustfmt to click authority regression
seonghobae Aug 31, 2026
9ca3519
fix(network): revalidate pointer authority at send boundary
seonghobae Aug 31, 2026
cc47abc
test(network): carry current node authority into click send
seonghobae Aug 31, 2026
255be62
test(network): preserve click send failure ordering with live authority
seonghobae Aug 31, 2026
0577ea7
test(network): retain node authority through click response lifecycle
seonghobae Aug 31, 2026
80c5c09
test(network): carry click authority through navigation postcondition
seonghobae Aug 31, 2026
4ff9e98
refactor(network): bundle immediate click send inputs
seonghobae Aug 31, 2026
8bc1404
refactor(network): export click send request boundary
seonghobae Aug 31, 2026
f96ac57
style(network): document explicit click send authority boundary
seonghobae Aug 31, 2026
5161652
fix(network): remove stale click request export
seonghobae Aug 31, 2026
18c156b
style(network): factor stale click test server type
seonghobae Aug 31, 2026
1e530cd
style(network): apply canonical rustfmt to pointer authority test
seonghobae Aug 31, 2026
ffa70ee
merge: converge pointer authority on subscription admission repair
seonghobae Aug 31, 2026
3f9cdc1
test(network): expose pointer preflight correlation leakage
seonghobae Sep 6, 2026
7535d8a
fix(network): preserve pointer authority during parent integration
seonghobae Sep 6, 2026
35555d0
docs: record pointer authority integration boundaries
seonghobae Sep 6, 2026
98621ad
test(network): reproduce foreign session before parent adoption
seonghobae Sep 6, 2026
7147893
fix(network): adopt parent session transport provenance
seonghobae Sep 6, 2026
6dfc75b
test: reject pointer dispatch on foreign BiDi session
seonghobae Sep 6, 2026
9d61f9f
test: format pointer session provenance regression
seonghobae Sep 6, 2026
6c61563
fix: bind pointer dispatch to verified BiDi session
seonghobae Sep 6, 2026
6eb49d5
test: align pointer transport fixture session authority
seonghobae Sep 6, 2026
dcd2fdc
test: align pointer failure fixture session authority
seonghobae Sep 6, 2026
fd2da46
test: require typed pointer session-provenance denial
seonghobae Sep 6, 2026
ddce724
test: apply canonical pointer transport formatting
seonghobae Sep 6, 2026
e7fb152
test(network): replay replacement pointer replies with admitted node …
seonghobae Sep 6, 2026
d847b53
fix(network): adopt sealed pointer replies without losing outbound au…
seonghobae Sep 6, 2026
e94a237
docs: record combined pointer authority and reply safeguards
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ All notable changes to OriginWeave are documented in this file. The format follo

### Changed

- Keep current-node and browser-session click checks when rejecting replies from replacement connections; the original request remains recoverable without consuming unrelated work.
- Recheck that a click still targets the admitted node in the current document before sending it. Invalid deadlines send nothing and reserve no pending request; uncertain writes remain pending instead of being treated as safe to retry.
- Reject replacement-connection click replies while retaining increasing request numbers, original subscription ownership and same-connection shutdown checks.

- Reject a navigation subscription aimed at a different browser session before sending it, without creating or replacing browser state.
Expand Down Expand Up @@ -79,6 +81,7 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Fail-closed WebDriver BiDi `script.NodeRemoteValue` admission that requires the exact remote type `node` and a non-empty `sharedId` within the same UTF-8 identifier budget as browser session and context identifiers, rejecting control and whitespace so an untrusted `locateNodes` item cannot be retained as a later typed-input handle without a usable shared node identity.
- Same-call `locateNodes` result admission that revalidates the exact current session, browsing context, canonical origin, and document epoch, rejects an over-budget or non-node result, and translates each admitted `sharedId` through the authority registry into an `ObservedNodeHandle` without performing browser I/O.
- Same-call QueryNodes admission that transfers a non-cloneable SemanticObservation protocol-use proof by ownership into `bind_current_nodes` before an untrusted `locateNodes` result can become current `ObservedNodeHandle` values, so Navigation-only or TypedInput-only proofs cannot mint observation handles.
- Registry-issued admitted node handles and authority-bound WebDriver BiDi pointer-click construction that revalidate the exact session, context, canonical origin, document epoch, registry provenance, and retained `sharedId` before serializing `input.performActions`; caller-constructed node tuples or arbitrary wire identifiers cannot become typed-input authority, and the command itself grants no policy or Agent authority.
- Fail-closed rejection of reviewed Unicode format and bidirectional-override characters in accessibility roles, accessible names, BiDi `sharedId` values, and registry external identifiers, while ordinary spaces in accessible names remain valid.
- Credential-safe browser-protocol validation evidence that copies only the already validated protocol family, OriginWeave generation, adapter version, pinned protocol/browser revisions, and exact capability into cloneable audit metadata without recreating the non-cloneable validation prerequisite or granting browser/Agent authority.
- Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors.
Expand Down
119 changes: 107 additions & 12 deletions crates/originweave-core/src/browser_authority_registry.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use std::ops::Deref;
use std::sync::Arc;

use crate::browser_registry::BrowserAuthorityRegistry as RawBrowserAuthorityRegistry;
Expand All @@ -6,17 +8,38 @@ use crate::{
Origin,
};

/// A registry-issued node handle that carries opaque provenance in addition to descriptive node state.
///
/// The contained [`ObservedNodeHandle`] remains readable through [`Deref`], but only
/// [`BrowserAuthorityRegistry`] can construct this wrapper. Typed actions therefore can require
/// proof that a node came from the same live registry instance instead of trusting a publicly
/// reproducible session/context/origin/epoch/node tuple.
#[derive(Debug)]
pub struct AdmittedNodeHandle {
observed: ObservedNodeHandle,
registry_instance: Arc<()>,
}

impl Deref for AdmittedNodeHandle {
type Target = ObservedNodeHandle;

fn deref(&self) -> &Self::Target {
&self.observed
}
}

/// Public browser-authority registry with raw node minting kept inside the crate.
///
/// Browser-session, browsing-context, document-epoch, and canonical-origin lifecycle operations are
/// public because trusted adapters need them to maintain current authority. Converting untrusted
/// browser-protocol node identifiers into [`ObservedNodeHandle`] values is deliberately
/// browser-protocol node identifiers into descriptive [`ObservedNodeHandle`] values is deliberately
/// crate-private: external callers must use a reviewed semantic-observation admission boundary such
/// as [`crate::WebDriverBiDiAccessibilityQuery::bind_current_nodes`], which consumes the required
/// protocol-use proof, validates the complete batch, and revalidates the exact current document
/// before atomically minting handles.
/// as [`crate::WebDriverBiDiAccessibilityQuery::bind_current_nodes`]. Action-capable paths use the
/// stricter registry-issued [`AdmittedNodeHandle`] wrapper so a copied descriptive tuple cannot
/// become typed-input authority.
pub struct BrowserAuthorityRegistry {
inner: RawBrowserAuthorityRegistry,
admitted_node_external_identifiers: BTreeMap<(u64, u64, u64, u64), String>,
registry_identity: Arc<()>,
}

Expand All @@ -34,6 +57,7 @@ impl BrowserAuthorityRegistry {
pub fn new() -> Self {
Self {
inner: RawBrowserAuthorityRegistry::new(),
admitted_node_external_identifiers: BTreeMap::new(),
registry_identity: Arc::new(()),
}
}
Expand All @@ -46,6 +70,7 @@ impl BrowserAuthorityRegistry {
pub fn with_identifier_limit(maximum_identifier: u64) -> Self {
Self {
inner: RawBrowserAuthorityRegistry::with_identifier_limit(maximum_identifier),
admitted_node_external_identifiers: BTreeMap::new(),
registry_identity: Arc::new(()),
}
}
Expand Down Expand Up @@ -110,15 +135,25 @@ impl BrowserAuthorityRegistry {
&mut self,
browsing_context: BrowsingContextId,
) -> Result<(), BrowserRegistryError> {
self.inner.remove_context(browsing_context)
self.inner.remove_context(browsing_context)?;
let browsing_context_value = browsing_context.value();
self.admitted_node_external_identifiers.retain(
|(_session, context, _epoch, _node), _external| *context != browsing_context_value,
);
Ok(())
}

/// Retire one browser session and every registered context and node binding beneath it.
pub fn remove_session(
&mut self,
browser_session: BrowserSessionId,
) -> Result<(), BrowserRegistryError> {
self.inner.remove_session(browser_session)
self.inner.remove_session(browser_session)?;
let browser_session_value = browser_session.value();
self.admitted_node_external_identifiers.retain(
|(session, _context, _epoch, _node), _external| *session != browser_session_value,
);
Ok(())
}

/// Return the currently active document epoch for a known browsing context.
Expand Down Expand Up @@ -180,29 +215,80 @@ impl BrowserAuthorityRegistry {
&mut self,
browsing_context: BrowsingContextId,
) -> Result<DocumentEpoch, BrowserRegistryError> {
self.inner.advance_document(browsing_context)
let next_epoch = self.inner.advance_document(browsing_context)?;
let browsing_context_value = browsing_context.value();
self.admitted_node_external_identifiers.retain(
|(_session, context, _epoch, _node), _external| *context != browsing_context_value,
);
Ok(next_epoch)
}

/// Bind one admitted batch of adapter-local node identifiers to current browser authority.
/// Bind one admitted batch of adapter-local identifiers as descriptive current-node evidence.
///
/// This operation is intentionally crate-private. The raw registry commits the batch only when
/// every identifier can be bound; a later failure rolls back node identifiers and any origin
/// binding created by the batch before the error is returned. Production callers outside this
/// crate therefore cannot bypass semantic admission or observe partial authority from a failed
/// `locateNodes` result.
/// binding created by the batch before the error is returned. The exact external identifier is
/// retained for later action admission, but the returned [`ObservedNodeHandle`] values remain
/// descriptive and publicly reproducible rather than typed-input authority.
pub(crate) fn bind_nodes(
&mut self,
browser_session: BrowserSessionId,
browsing_context: BrowsingContextId,
origin: &Origin,
external_identifiers: &[&str],
) -> Result<Vec<ObservedNodeHandle>, BrowserRegistryError> {
self.inner.bind_nodes(
let handles = self.inner.bind_nodes(
browser_session,
browsing_context,
origin,
external_identifiers,
)?;
for (handle, external_identifier) in handles.iter().zip(external_identifiers) {
self.admitted_node_external_identifiers.insert(
node_authority_key(handle),
(*external_identifier).to_owned(),
);
}
Ok(handles)
}

/// Bind one admitted batch and attach opaque registry-instance provenance for typed actions.
pub(crate) fn bind_admitted_nodes(
&mut self,
browser_session: BrowserSessionId,
browsing_context: BrowsingContextId,
origin: &Origin,
external_identifiers: &[&str],
) -> Result<Vec<AdmittedNodeHandle>, BrowserRegistryError> {
self.bind_nodes(
browser_session,
browsing_context,
origin,
external_identifiers,
)
.map(|handles| {
handles
.into_iter()
.map(|observed| AdmittedNodeHandle {
observed,
registry_instance: Arc::clone(&self.registry_identity),
})
.collect()
})
}

/// Return whether this registry issued the handle under the exact supplied wire identifier.
pub(crate) fn node_external_identifier_matches(
&self,
handle: &AdmittedNodeHandle,
external_identifier: &str,
) -> bool {
if !Arc::ptr_eq(&self.registry_identity, &handle.registry_instance) {
return false;
}
self.admitted_node_external_identifiers
.get(&node_authority_key(&handle.observed))
.is_some_and(|admitted| admitted == external_identifier)
}
}

Expand All @@ -211,3 +297,12 @@ impl Default for BrowserAuthorityRegistry {
Self::new()
}
}

fn node_authority_key(handle: &ObservedNodeHandle) -> (u64, u64, u64, u64) {
(
handle.browser_session().value(),
handle.browsing_context().value(),
handle.document_epoch().value(),
handle.node_id(),
)
}
19 changes: 18 additions & 1 deletion crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@
//! let _handle = registry.bind_node(session, context, &origin, "backend-node-17")?;
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! Raw pointer-command serialization is likewise not a public escape hatch. External callers must
//! bind the exact registry-issued admitted node and current browser authority through the reviewed
//! current-node constructor instead of selecting an arbitrary WebDriver BiDi `sharedId` or
//! recreating authority from a descriptive [`ObservedNodeHandle`] tuple:
//!
//! ```compile_fail
//! use originweave_core::{WebDriverBiDiPointerClickCommand, WebDriverBiDiRemoteNodeReference};
//!
//! if let Ok(node) = WebDriverBiDiRemoteNodeReference::new("node", Some("caller-selected-node")) {
//! let _command = WebDriverBiDiPointerClickCommand::new(1, "context-a", &node);
//! }
//! ```

#![forbid(unsafe_code)]
#![deny(missing_docs)]
Expand All @@ -34,14 +47,17 @@ mod browser_registry_external_context;
mod contracts;
mod webdriver_bidi_command;
mod webdriver_bidi_error_code;
mod webdriver_bidi_pointer_click_authority;
mod webdriver_bidi_response_document;
mod webdriver_bidi_response_document_correlation;
mod webdriver_bidi_response_envelope;
mod webdriver_bidi_result;
mod webdriver_bidi_websocket_connect_target;
mod webdriver_bidi_websocket_endpoint;

pub use browser_authority_registry::{BrowserAuthorityRegistry, BrowserRegistryIdentity};
pub use browser_authority_registry::{
AdmittedNodeHandle, BrowserAuthorityRegistry, BrowserRegistryIdentity,
};
pub use browser_protocol::{
BrowserProtocolAdapterDescriptor, BrowserProtocolCapability,
BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind,
Expand Down Expand Up @@ -79,6 +95,7 @@ pub use webdriver_bidi_command::{
WebDriverBiDiPointerClickCommandError,
};
pub use webdriver_bidi_error_code::WebDriverBiDiErrorCode;
pub use webdriver_bidi_pointer_click_authority::WebDriverBiDiPointerClickAuthorityError;
pub use webdriver_bidi_response_document::{
BoundedWebDriverBiDiResponseDocument, MAX_WEBDRIVER_BIDI_RESPONSE_DOCUMENT_BYTES,
WebDriverBiDiResponseDocumentAdmissionError,
Expand Down
17 changes: 6 additions & 11 deletions crates/originweave-core/src/webdriver_bidi_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ impl Error for WebDriverBiDiPointerClickCommandError {}
/// Deterministic command for one primary-button click on an admitted remote node.
///
/// The fixed mouse action sequence moves to the element origin, presses button zero, and releases
/// button zero. Construction accepts an already admitted remote node reference and does not grant
/// browser-session, context, origin, document-epoch, policy, approval, or Agent authority. A trusted
/// adapter must bind this inert command to current authority before transport.
/// button zero. Public construction is authority-bound through
/// [`Self::new_for_current_node`]; raw wire serialization remains crate-private so external callers
/// cannot choose an arbitrary WebDriver BiDi node identifier while bypassing current node authority.
#[derive(Debug, PartialEq, Eq)]
pub struct WebDriverBiDiPointerClickCommand {
command_id: u64,
Expand All @@ -49,21 +49,16 @@ pub struct WebDriverBiDiPointerClickCommand {
}

impl WebDriverBiDiPointerClickCommand {
/// Validate and serialize one bounded `input.performActions` pointer click command.
pub fn new(
/// Serialize one bounded `input.performActions` pointer click command for an already
/// authority-validated browsing context.
pub(crate) fn new(
command_id: u64,
browsing_context: &str,
node: &crate::WebDriverBiDiRemoteNodeReference,
) -> Result<Self, WebDriverBiDiPointerClickCommandError> {
if command_id > MAX_WEBDRIVER_BIDI_COMMAND_ID {
return Err(WebDriverBiDiPointerClickCommandError::InvalidCommandId);
}
if browsing_context.is_empty()
|| browsing_context.len() > MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES
|| contains_disallowed_protocol_text(browsing_context, false)
{
return Err(WebDriverBiDiPointerClickCommandError::InvalidBrowsingContext);
}

let mut json = String::from("{\"id\":");
json.push_str(&command_id.to_string());
Expand Down
Loading
Loading