///|
/// Maximum length in bytes for a stored Authorization header value.
/// Real-world JWTs are typically under 4 KB; 8 KB leaves headroom
/// without making it cheap to wedge unbounded state.
const AUTH_HEADER_VALUE_LIMIT : Int = 8192
///|
/// Normalize an origin per the spec: only http/https, no path/query/
/// fragment, lowercased scheme and host, default port stripped.
fn normalize_origin(input : String) -> Result[String, String] {
let trimmed = input.trim().to_owned()
let scheme_end = match trimmed.find("://") {
Some(idx) => idx
None => return Err("origin must be a valid URL (scheme://host[:port])")
}
let scheme = trimmed.unsafe_substring(start=0, end=scheme_end).to_lower()
if scheme != "http" && scheme != "https" {
return Err("origin must use http or https scheme")
}
let host_start = scheme_end + 3
let rest = trimmed.unsafe_substring(start=host_start, end=trimmed.length())
// Reject path / query / fragment
if rest.contains("/") || rest.contains("?") || rest.contains("#") {
return Err("origin must not contain path / query / fragment")
}
if rest.length() == 0 {
return Err("origin must include a host")
}
let lowered_host = rest.to_lower()
// Strip default port
let normalized_host_port = if scheme == "http" &&
lowered_host.has_suffix(":80") {
lowered_host.unsafe_substring(start=0, end=lowered_host.length() - 3)
} else if scheme == "https" && lowered_host.has_suffix(":443") {
lowered_host.unsafe_substring(start=0, end=lowered_host.length() - 4)
} else {
lowered_host
}
Ok(scheme + "://" + normalized_host_port)
}
///|
/// Validate a candidate header value. Reject empty, oversized, or
/// values containing control characters / ANSI escape sequences. Tab
/// (0x09) is allowed because it is sometimes legitimately used inside
/// scheme parameters.
fn validate_header_value(value : String) -> Result[Unit, String] {
if value.length() == 0 {
return Err("headerValue must not be empty")
}
if value.length() > AUTH_HEADER_VALUE_LIMIT {
return Err("headerValue exceeds 8KB limit")
}
let chars = value.to_array()
for i = 0; i < chars.length(); i = i + 1 {
let code = chars[i].to_int()
if code == 0x1b {
return Err("headerValue must not contain ANSI escape sequences")
}
if code < 0x20 && code != 0x09 {
return Err("headerValue must not contain control characters")
}
}
Ok(())
}
///|
/// Resolve the target context: explicit `context` param wins; otherwise
/// fall back to the active context. Returns None and sends an error
/// when the context argument is malformed or unknown.
fn BidiProtocol::resolve_authorization_context(
self : BidiProtocol,
map : Map[String, Json],
request_id : Int,
) -> String? {
match map.get("context") {
Some(String(ctx)) =>
if !self.manager.has_session(ctx) {
self.send_error(request_id, "no such frame", "Unknown context: " + ctx)
None
} else {
Some(ctx)
}
Some(_) => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
None
}
None =>
match self.default_context_id {
Some(ctx) => Some(ctx)
None => {
self.send_error(
request_id, "no such frame", "No active browsing context",
)
None
}
}
}
}
///|
/// Handle `crater.setOriginAuthorization`. Persists a header value on
/// the per-session profile under a normalized origin key.
fn BidiProtocol::handle_crater_set_origin_authorization(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(m)) => m
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let raw_origin = match map.get("origin") {
Some(String(o)) => o
_ => {
self.send_error(request_id, "invalid argument", "origin must be a string")
return
}
}
let header_value = match map.get("headerValue") {
Some(String(v)) => v
_ => {
self.send_error(
request_id, "invalid argument", "headerValue must be a string",
)
return
}
}
let normalized = match normalize_origin(raw_origin) {
Ok(o) => o
Err(reason) => {
self.send_error(request_id, "invalid argument", reason)
return
}
}
match validate_header_value(header_value) {
Ok(_) => ()
Err(reason) => {
self.send_error(request_id, "invalid argument", reason)
return
}
}
let ctx_id = self.resolve_authorization_context(map, request_id)
guard ctx_id is Some(ctx) else { return }
let profile = match self.profile_for_session(ctx) {
Some(p) => p
None => {
self.send_error(request_id, "no such frame", "Unknown context: " + ctx)
return
}
}
profile.auth_state().set_origin_header(normalized, header_value)
self.push_authorization_snapshot(ctx)
self.send_success(request_id, Some(make_object({})))
}
///|
/// Handle `crater.clearOriginAuthorization`. Removes any stored header
/// for the normalized origin from the per-session profile.
fn BidiProtocol::handle_crater_clear_origin_authorization(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(m)) => m
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let raw_origin = match map.get("origin") {
Some(String(o)) => o
_ => {
self.send_error(request_id, "invalid argument", "origin must be a string")
return
}
}
let normalized = match normalize_origin(raw_origin) {
Ok(o) => o
Err(reason) => {
self.send_error(request_id, "invalid argument", reason)
return
}
}
let ctx_id = self.resolve_authorization_context(map, request_id)
guard ctx_id is Some(ctx) else { return }
let profile = match self.profile_for_session(ctx) {
Some(p) => p
None => {
self.send_error(request_id, "no such frame", "Unknown context: " + ctx)
return
}
}
profile.auth_state().clear_origin_header(normalized)
self.push_authorization_snapshot(ctx)
self.send_success(request_id, Some(make_object({})))
}
///|
/// Handle `crater.listOriginAuthorizations`. Returns the set of
/// registered origins without exposing any header values.
fn BidiProtocol::handle_crater_list_origin_authorizations(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map : Map[String, Json] = match params {
Some(Object(m)) => m
_ => Map([], capacity=0)
}
let ctx_id = self.resolve_authorization_context(map, request_id)
guard ctx_id is Some(ctx) else { return }
let profile = match self.profile_for_session(ctx) {
Some(p) => p
None => {
self.send_error(request_id, "no such frame", "Unknown context: " + ctx)
return
}
}
let origins_json : Array[Json] = []
let auth_state = profile.auth_state()
for origin in auth_state.list_origins() {
origins_json.push(make_object({ "origin": Json::string(origin) }))
}
self.send_success(
request_id,
Some(make_object({ "origins": Json::array(origins_json) })),
)
}
///|
/// Serialize the partition's origin_headers as a JSON object suitable
/// for pushing into globalThis.__bidiContextAuth[ctxId]. Header values
/// only cross the JS bridge — the WebDriver-facing surface (list /
/// events) never serializes them.
fn BidiProtocol::serialize_auth_snapshot_for_runtime(
self : BidiProtocol,
ctx_id : String,
) -> String {
match self.profile_for_session(ctx_id) {
Some(profile) => {
let auth_state = profile.auth_state()
let entries : Map[String, Json] = Map([], capacity=0)
for origin in auth_state.list_origins() {
match auth_state.header_for_origin(origin) {
Some(value) => entries[origin] = Json::string(value)
None => ()
}
}
make_object(entries).stringify()
}
None => "{}"
}
}
///|
/// Push the per-context Authorization snapshot to the JS runtime so
/// the fetch shim can attach the header on outbound requests.
fn BidiProtocol::push_authorization_snapshot(
self : BidiProtocol,
ctx_id : String,
) -> Unit {
let snapshot = self.serialize_auth_snapshot_for_runtime(ctx_id)
set_runtime_context_authorization(ctx_id, snapshot)
}
///|
/// Dispatch crater.* extension commands. Currently scoped to the
/// origin-Authorization injection surface (set / clear / list); other
/// crater.* extensions live as separate dispatch branches today and
/// can be folded in if a second command lands here.
fn BidiProtocol::dispatch_crater(
self : BidiProtocol,
request : BidiRequest,
action : String,
) -> Result[Unit, String] {
match action {
"setOriginAuthorization" => {
self.handle_crater_set_origin_authorization(request.id, request.params)
Ok(())
}
"clearOriginAuthorization" => {
self.handle_crater_clear_origin_authorization(request.id, request.params)
Ok(())
}
"listOriginAuthorizations" => {
self.handle_crater_list_origin_authorizations(request.id, request.params)
Ok(())
}
"setOriginCredentials" => {
self.handle_crater_set_origin_credentials(request.id, request.params)
Ok(())
}
"clearOriginCredentials" => {
self.handle_crater_clear_origin_credentials(request.id, request.params)
Ok(())
}
"listOriginCredentials" => {
self.handle_crater_list_origin_credentials(request.id, request.params)
Ok(())
}
_ => {
self.send_error(
request.id,
"unknown command",
"Unknown method: " + request.method_name,
)
Ok(())
}
}
}