///|
/// Maximum length in bytes for stored credential fields. RFC 7617 / 7616
/// don't impose a fixed limit, but capping at 1 KB each prevents a
/// malicious or accidental write from filling the per-session profile.
const CREDENTIAL_FIELD_LIMIT : Int = 1024
///|
/// Validate a candidate credential string (username or password).
/// Reject empty, oversized, or values containing control characters /
/// ANSI escape sequences. Same rules as `validate_header_value` minus
/// the tab allowance — credentials shouldn't contain control whitespace.
fn validate_credential_field(
value : String,
field_name : String,
) -> Result[Unit, String] {
if value.length() == 0 {
return Err(field_name + " must not be empty")
}
if value.length() > CREDENTIAL_FIELD_LIMIT {
return Err(field_name + " exceeds 1KB 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(field_name + " must not contain ANSI escape sequences")
}
if code < 0x20 {
return Err(field_name + " must not contain control characters")
}
}
Ok(())
}
///|
/// Handle `crater.setOriginCredentials`. Persists a username/password
/// pair on the per-session profile under a normalized origin key.
/// Mirror of `handle_crater_set_origin_authorization`; credentials are
/// only consumed by the runtime fetch shim's 401 Digest auto-retry path,
/// never sent unsolicited.
fn BidiProtocol::handle_crater_set_origin_credentials(
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 username = match map.get("username") {
Some(String(v)) => v
_ => {
self.send_error(
request_id, "invalid argument", "username must be a string",
)
return
}
}
let password = match map.get("password") {
Some(String(v)) => v
_ => {
self.send_error(
request_id, "invalid argument", "password 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_credential_field(username, "username") {
Ok(_) => ()
Err(reason) => {
self.send_error(request_id, "invalid argument", reason)
return
}
}
match validate_credential_field(password, "password") {
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_credentials(normalized, username, password)
self.push_credentials_snapshot(ctx)
self.send_success(request_id, Some(make_object({})))
}
///|
/// Handle `crater.clearOriginCredentials`. Removes any stored
/// credentials for the normalized origin from the per-session profile.
fn BidiProtocol::handle_crater_clear_origin_credentials(
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_credentials(normalized)
self.push_credentials_snapshot(ctx)
self.send_success(request_id, Some(make_object({})))
}
///|
/// Handle `crater.listOriginCredentials`. Returns the set of registered
/// origins without exposing any username or password.
fn BidiProtocol::handle_crater_list_origin_credentials(
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_credential_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_credentials as a JSON object
/// suitable for pushing into globalThis.__bidiContextCredentials[ctxId].
/// Credential values only cross the JS bridge — the WebDriver-facing
/// surface (list / events) never serializes them.
fn BidiProtocol::serialize_credentials_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_credential_origins() {
match auth_state.credentials_for_origin(origin) {
Some(creds) =>
entries[origin] = make_object({
"username": Json::string(creds.username()),
"password": Json::string(creds.password()),
})
None => ()
}
}
make_object(entries).stringify()
}
None => "{}"
}
}
///|
/// Push the per-context credentials snapshot to the JS runtime so the
/// fetch shim's Digest auto-retry can resolve username/password by origin.
fn BidiProtocol::push_credentials_snapshot(
self : BidiProtocol,
ctx_id : String,
) -> Unit {
let snapshot = self.serialize_credentials_snapshot_for_runtime(ctx_id)
set_runtime_context_credentials(ctx_id, snapshot)
}