feat(credentials): bind native CredentialsContainer entry points

This commit is contained in:
ldm0
2026-10-12 20:48:29 +08:00
committed by Donough Liu
parent 985c527660
commit 1356f248ff
11 changed files with 949 additions and 2 deletions
@@ -2,6 +2,7 @@ use anyhow::{Context, Result, anyhow};
use crate::context_bootstrap::{
crypto::finalize_crypto_realm_bindings, events::finalize_pointer_event_realm_bindings,
navigator_runtime::finalize_navigator_credentials_realm_bindings,
notification_runtime::finalize_notification_realm_bindings,
performance_runtime::finalize_performance_observer_realm_bindings,
web_audio_runtime::finalize_base_audio_context_realm_bindings,
@@ -13,6 +14,7 @@ enum RealmDependentFinalizer {
DocumentRealmBindings,
NodeMixinUnscopables,
GlobalEventHandlersSecureContextSurface,
NavigatorCredentialsSecureContextSurface,
CryptoSecureContextSurface,
BaseAudioContextSecureContextSurface,
XmlHttpRequestEventTargetState,
@@ -40,6 +42,10 @@ const REALM_DEPENDENT_FINALIZER_ALLOWLIST: &[(&str, RealmDependentFinalizer)] =
),
// These entries install private realm state, context-conditional surface,
// or constructor data whose JavaScript identity must be realm-local.
(
"Navigator",
RealmDependentFinalizer::NavigatorCredentialsSecureContextSurface,
),
(
"BaseAudioContext",
RealmDependentFinalizer::BaseAudioContextSecureContextSurface,
@@ -113,6 +119,9 @@ pub(super) fn finalize_materialized_interface<'s>(
RealmDependentFinalizer::BaseAudioContextSecureContextSurface => {
finalize_base_audio_context_realm_bindings(scope, prototype)
}
RealmDependentFinalizer::NavigatorCredentialsSecureContextSurface => {
finalize_navigator_credentials_realm_bindings(scope, prototype)
}
RealmDependentFinalizer::GlobalEventHandlersSecureContextSurface => {
crate::context_bootstrap::window_events::finalize_secure_global_event_handler_realm_bindings(
scope, prototype,
@@ -171,6 +180,7 @@ mod tests {
"Element",
"DocumentType",
"CharacterData",
"Navigator",
"BaseAudioContext",
"Crypto",
"XMLHttpRequestEventTarget",
@@ -21,7 +21,7 @@ pub(in crate::context_bootstrap) use self::navigator::{
SERVICE_WORKER_OWNER_TOKEN_SLOT, STORAGE_BUCKET_MANAGER_CHILD_HANDLE_SLOT,
STORAGE_BUCKET_MANAGER_POPUP_ID_SLOT, STORAGE_MANAGER_CHILD_HANDLE_SLOT,
STORAGE_MANAGER_POPUP_ID_SLOT, build_storage_manager_worker_template,
current_protocol_user_gesture_activation,
current_protocol_user_gesture_activation, finalize_navigator_credentials_realm_bindings,
install_storage_manager_constructor_template_bindings, navigator_identity_profile,
navigator_receiver_branded, service_worker_owner_token_value, set_navigator_identity_profile,
};
@@ -7,9 +7,11 @@
use moli_webapi_declare::WebApiFunctionTemplate;
mod base64url;
mod container;
mod json_options;
mod schema;
mod value;
pub(super) use container::build as build_container;
use crate::{native_bridge::throw_dom_exception, util::v8str, web_api_interfaces};
@@ -90,6 +92,7 @@ pub(super) fn install<'s>(
// Static members are installed on the constructor template by the derive.
let prototype = template.prototype_template(scope);
match interface_name {
"CredentialsContainer" => container::install(scope, prototype),
"Credential" => {
CredentialDeclaration::initialize_template(scope, template);
CredentialDeclaration::initialize_prototype_template(scope, prototype);
@@ -0,0 +1,179 @@
//! Native credential-manager entry points without a credential-store backend.
//!
//! WebIDL conversion and fully-active/pre-aborted checks still apply when no
//! credential can be produced. The generated Promise adapter handles receiver
//! and conversion errors in the function realm. Do not manufacture credentials
//! or claim to persist them.
mod options;
use anyhow::Result;
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
use crate::{
native_bridge::{OwnerDispatchScope, throw_dom_exception},
util::context_host_ptr_from_context_slot,
web_api_interfaces, webidl,
};
use options::{CreateArgs, GetArgs, StoreArgs};
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::CredentialsContainer)]
struct ContainerObject<'s> {
#[webapi(prototype)]
prototype: v8::Local<'s, v8::Object>,
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::CredentialsContainer, enumerable, receiver)]
struct ContainerPrototype {
#[webapi(method, returns_promise, length = 0, callback = get)]
get: (),
#[webapi(method, returns_promise, length = 1, callback = store)]
store: (),
#[webapi(method, returns_promise, length = 0, callback = create)]
create: (),
#[webapi(method, returns_promise, length = 0, callback = prevent_silent_access)]
prevent_silent_access: (),
}
pub(super) fn install<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
prototype: v8::Local<'s, v8::ObjectTemplate>,
) {
ContainerPrototype::initialize_prototype_template(scope, prototype);
}
pub(in crate::context_bootstrap::navigator_runtime) fn build<'s>(
scope: &mut v8::PinScope<'s, '_>,
) -> Result<v8::Local<'s, v8::Object>> {
let prototype = crate::context_bootstrap::ensure_intrinsic_interface_prototype(
scope,
"CredentialsContainer",
)?;
ContainerObject::new(prototype)
.bind(scope)
.map_err(Into::into)
}
fn check_fully_active(scope: &mut v8::PinScope<'_, '_>) -> bool {
// Current settings come from the function realm, never from an author
// property such as window.document or a retained WindowProxy association.
let context = scope.get_current_context();
let fully_active = context_host_ptr_from_context_slot(context).is_some_and(|host_ptr| {
let host = unsafe { &*host_ptr };
let Some(identity) = host.window_execution_context_identity_for_access_check(context)
else {
return false;
};
if !host.window_execution_context_identity_is_current(identity) {
return false;
}
let document = match identity.dispatch_scope() {
OwnerDispatchScope::Top => Some(host.document_handle()),
OwnerDispatchScope::Child(frame) => host.child_browsing_context_document_handle(frame),
OwnerDispatchScope::LightweightPopup(id) => host.lightweight_popup_document_handle(id),
};
document.is_some_and(|document| host.document_is_fully_active(document))
});
if !fully_active {
throw_dom_exception(
scope,
"InvalidStateError",
11,
"The document is not fully active.",
);
}
fully_active
}
fn check_aborted<'s>(
scope: &mut v8::PinScope<'s, '_>,
signal: Option<options::Signal<'s>>,
) -> bool {
if let Some(signal) = signal
&& signal.0.is_aborted(scope)
{
let reason = signal
.0
.reason(scope)
.unwrap_or_else(|| v8::undefined(scope).into());
scope.throw_exception(reason);
return true;
}
false
}
fn backend_unavailable(scope: &mut v8::PinScope<'_, '_>) {
throw_dom_exception(
scope,
"NotSupportedError",
9,
"No supported credential backend is available.",
);
}
fn get<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'s>,
) {
let Some(parsed) = webidl::parse_args::<GetArgs>(scope, &args) else {
return;
};
if !check_fully_active(scope) || check_aborted(scope, parsed.options.0.signal) {
return;
}
backend_unavailable(scope);
}
fn create<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'s>,
) {
let Some(parsed) = webidl::parse_args::<CreateArgs>(scope, &args) else {
return;
};
if !check_fully_active(scope) {
return;
}
let options = parsed.options.0;
// Unlike get(), create() checks multiple credential types before abort.
let types = usize::from(options.password.is_some())
+ usize::from(options.federated.is_some())
+ usize::from(options.public_key.is_some());
if types > 1 {
backend_unavailable(scope);
return;
}
if !check_aborted(scope, options.signal) {
backend_unavailable(scope);
}
}
fn store<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'s>,
) {
if webidl::parse_args::<StoreArgs>(scope, &args).is_some() && check_fully_active(scope) {
backend_unavailable(scope);
}
}
fn prevent_silent_access<'s>(
scope: &mut v8::PinScope<'s, '_>,
_args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s>,
) {
if !check_fully_active(scope) {
return;
}
// The default prevent-silent-access flag is true. With no credential store
// or automatic sign-in path, this cannot enable silent access or persist a
// credential; its idempotent operation can complete normally.
let value = v8::undefined(scope).into();
crate::context_bootstrap::stream_adapter::set_resolved_promise(scope, &mut rv, value);
}
@@ -0,0 +1,308 @@
//! Convert credential dictionaries before entering the backend algorithm.
//!
//! All dictionaries preserve WebIDL's inherited-then-lexical member order.
//! Underscored fields are converted even though the unavailable backend cannot
//! consume them. BufferSource validation uses native identity and never copies
//! authenticator input or reads an author-visible `.buffer` property.
use crate::{abort_signal_route::ResolvedAbortSignal, web_api_interfaces, webidl};
use super::super::{
schema::{Entity, Parameters, RelyingParty, Selection},
value::{Dictionary, Text},
};
#[derive(Clone, Copy, webidl::WebIdlEnum)]
#[webidl(name = "CredentialMediationRequirement")]
enum Mediation {
Silent,
Optional,
Conditional,
Required,
}
pub(super) struct Signal<'s>(pub(super) ResolvedAbortSignal<'s>);
impl<'s> webidl::WebIdlConverter<'s> for Signal<'s> {
type Options = ();
fn convert(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
context: webidl::Context,
_options: &Self::Options,
) -> Result<Self, webidl::WebIdlError> {
let signal = v8::Local::<v8::Object>::try_from(value)
.ok()
.filter(|object| web_api_interfaces::AbortSignal::is_instance(scope, *object))
.and_then(|object| moli_webapi_declare::web_api_object_target(scope, object))
.and_then(|object| ResolvedAbortSignal::resolve(scope, object));
signal
.map(Self)
.ok_or_else(|| webidl::WebIdlError::cannot_convert(context, "AbortSignal"))
}
}
// This is validation for WebAuthn's BufferSource without AllowShared or
// AllowResizable. The existing byte-copy converter also accepts shared input,
// so it cannot be used for these dictionaries.
struct Buffer;
impl<'s> webidl::WebIdlConverter<'s> for Buffer {
type Options = ();
fn convert(
_scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
context: webidl::Context,
_options: &Self::Options,
) -> Result<Self, webidl::WebIdlError> {
let binary = value.is_array_buffer() || value.is_array_buffer_view();
if binary && !crate::blob::buffer_source_has_shared_or_resizable_backing_store(value) {
Ok(Self)
} else {
Err(webidl::WebIdlError::cannot_convert(context, "BufferSource"))
}
}
}
pub(super) struct PasswordInit;
impl<'s> webidl::WebIdlConverter<'s> for PasswordInit {
type Options = ();
fn convert(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
context: webidl::Context,
_options: &Self::Options,
) -> Result<Self, webidl::WebIdlError> {
if let Ok(object) = v8::Local::<v8::Object>::try_from(value)
&& web_api_interfaces::HTMLFormElement::is_instance(scope, object)
{
return Ok(Self);
}
webidl::convert::<Dictionary<PasswordData>>(scope, value, context)?;
Ok(Self)
}
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "CredentialsContainer.get")]
pub(super) struct GetArgs<'s> {
#[webidl(converter = "raw", default = Dictionary::empty(scope)?)]
pub(super) options: Dictionary<Request<'s>>,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "CredentialsContainer.create")]
pub(super) struct CreateArgs<'s> {
#[webidl(converter = "raw", default = Dictionary::empty(scope)?)]
pub(super) options: Dictionary<Creation<'s>>,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "CredentialsContainer.store")]
pub(super) struct StoreArgs<'s> {
#[webidl(required, interface = web_api_interfaces::Credential)]
_credential: v8::Local<'s, v8::Object>,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "CredentialRequestOptions")]
pub(super) struct Request<'s> {
#[webidl(converter = "raw")]
_federated: Option<Dictionary<FederatedRequest>>,
#[webidl(converter = "enum", default = Mediation::Optional)]
_mediation: Mediation,
#[webidl(default = false)]
_password: bool,
#[webidl(converter = "raw")]
_public_key: Option<Dictionary<PublicKeyRequest>>,
#[webidl(converter = "raw")]
pub(super) signal: Option<Signal<'s>>,
#[webidl(converter = "raw")]
_ui_mode: Option<Text>,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "CredentialCreationOptions")]
pub(super) struct Creation<'s> {
#[webidl(converter = "raw")]
pub(super) federated: Option<Dictionary<FederatedInit>>,
#[webidl(converter = "enum", default = Mediation::Optional)]
_mediation: Mediation,
#[webidl(converter = "raw")]
pub(super) password: Option<PasswordInit>,
#[webidl(converter = "raw")]
pub(super) public_key: Option<Dictionary<PublicKeyCreation>>,
#[webidl(converter = "raw")]
pub(super) signal: Option<Signal<'s>>,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "FederatedCredentialRequestOptions")]
struct FederatedRequest {
#[webidl(converter = "raw")]
_protocols: Option<webidl::Sequence<Text>>,
#[webidl(converter = "raw")]
_providers: Option<webidl::Sequence<webidl::UsvString>>,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "CredentialData")]
struct CredentialData {
#[webidl(required, converter = "usv_string")]
_id: String,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "FederatedCredentialInit")]
pub(super) struct FederatedInit {
#[webidl(inherit)]
_base: CredentialData,
#[webidl(name = "iconURL", converter = "usv_string")]
_icon_url: Option<String>,
#[webidl(converter = "usv_string")]
_name: Option<String>,
#[webidl(required, converter = "usv_string")]
_origin: String,
#[webidl(converter = "raw")]
_protocol: Option<Text>,
#[webidl(required, converter = "usv_string")]
_provider: String,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "PasswordCredentialData")]
struct PasswordData {
#[webidl(inherit)]
_base: CredentialData,
#[webidl(name = "iconURL", converter = "usv_string")]
_icon_url: Option<String>,
#[webidl(converter = "usv_string")]
_name: Option<String>,
#[webidl(required, converter = "usv_string")]
_origin: String,
#[webidl(required, converter = "usv_string")]
_password: String,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "PublicKeyCredentialCreationOptions")]
pub(super) struct PublicKeyCreation {
#[webidl(converter = "raw", default = Text::from("none"))]
_attestation: Text,
#[webidl(converter = "raw", default = webidl::Sequence(Vec::new()))]
_attestation_formats: webidl::Sequence<Text>,
#[webidl(converter = "raw")]
_authenticator_selection: Option<Dictionary<Selection>>,
#[webidl(required, converter = "raw")]
_challenge: Buffer,
#[webidl(converter = "raw", default = webidl::Sequence(Vec::new()))]
_exclude_credentials: webidl::Sequence<Dictionary<Descriptor>>,
#[webidl(converter = "raw")]
_extensions: Option<Dictionary<Extensions>>,
#[webidl(converter = "raw", default = webidl::Sequence(Vec::new()))]
_hints: webidl::Sequence<Text>,
#[webidl(required, converter = "raw")]
_pub_key_cred_params: webidl::Sequence<Dictionary<Parameters>>,
#[webidl(required, converter = "raw")]
_rp: Dictionary<RelyingParty>,
_timeout: Option<u32>,
#[webidl(required, converter = "raw")]
_user: Dictionary<User>,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "PublicKeyCredentialRequestOptions")]
struct PublicKeyRequest {
#[webidl(converter = "raw", default = webidl::Sequence(Vec::new()))]
_allow_credentials: webidl::Sequence<Dictionary<Descriptor>>,
#[webidl(required, converter = "raw")]
_challenge: Buffer,
#[webidl(converter = "raw")]
_extensions: Option<Dictionary<Extensions>>,
#[webidl(converter = "raw", default = webidl::Sequence(Vec::new()))]
_hints: webidl::Sequence<Text>,
#[webidl(converter = "raw")]
_rp_id: Option<Text>,
_timeout: Option<u32>,
#[webidl(converter = "raw", default = Text::from("preferred"))]
_user_verification: Text,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "PublicKeyCredentialUserEntity")]
struct User {
#[webidl(inherit)]
_base: Entity,
#[webidl(required, converter = "raw")]
_display_name: Text,
#[webidl(required, converter = "raw")]
_id: Buffer,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "PublicKeyCredentialDescriptor")]
struct Descriptor {
#[webidl(required, converter = "raw")]
_id: Buffer,
#[webidl(converter = "raw")]
_transports: Option<webidl::Sequence<Text>>,
#[webidl(required, name = "type", converter = "raw")]
_credential_type: Text,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "AuthenticationExtensionsClientInputs")]
struct Extensions {
#[webidl(converter = "raw")]
_appid: Option<Text>,
#[webidl(converter = "raw")]
_appid_exclude: Option<Text>,
#[webidl(converter = "raw")]
_cred_blob: Option<Buffer>,
_cred_props: Option<bool>,
#[webidl(converter = "usv_string")]
_credential_protection_policy: Option<String>,
#[webidl(default = false)]
_enforce_credential_protection_policy: bool,
_get_cred_blob: Option<bool>,
_hmac_create_secret: Option<bool>,
#[webidl(converter = "raw")]
_large_blob: Option<Dictionary<LargeBlob>>,
_min_pin_length: Option<bool>,
#[webidl(converter = "raw")]
_prf: Option<Dictionary<Prf>>,
#[webidl(name = "remoteClientDataJSON", converter = "raw")]
_remote_client_data_json: Option<Text>,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "AuthenticationExtensionsLargeBlobInputs")]
struct LargeBlob {
_read: Option<bool>,
#[webidl(converter = "raw")]
_support: Option<Text>,
#[webidl(converter = "raw")]
_write: Option<Buffer>,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "AuthenticationExtensionsPRFInputs")]
struct Prf {
#[webidl(converter = "raw")]
_eval: Option<Dictionary<PrfValues>>,
#[webidl(converter = "raw")]
_eval_by_credential: Option<webidl::Record<Text, Dictionary<PrfValues>>>,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "AuthenticationExtensionsPRFValues")]
struct PrfValues {
#[webidl(required, converter = "raw")]
_first: Buffer,
#[webidl(converter = "raw")]
_second: Option<Buffer>,
}
@@ -74,6 +74,7 @@ const NAVIGATOR_RUNTIME_DATA_KEYS: &[&str] = &[
"storageBuckets",
"geolocation",
"mediaCapabilities",
"credentials",
];
const WORKER_NAVIGATOR_INSTALLED_SLOT: &str = "__moliWorkerNavigatorInstalled";
const WORKER_NAVIGATOR_MATERIALIZING_SLOT: &str = "__moliWorkerNavigatorMaterializing";
@@ -197,6 +198,13 @@ struct NavigatorRuntimeDataPrototypeDeclaration {
cookie_enabled: (),
}
#[derive(Default, WebApiObject)]
#[webapi(fragment, prototype = "Navigator")]
struct NavigatorCredentialsPrototypeDeclaration {
#[webapi(accessor_property, enumerable, receiver = web_api_interfaces::Navigator::is_instance, getter = navigator_runtime_data_getter_callback, data = callback_data_index_value(scope, 33))]
credentials: (),
}
#[derive(Default, WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::Navigator)]
struct NavigatorPrototypeMethodsDeclaration {
@@ -591,6 +599,8 @@ struct WindowNavigatorBackingDeclaration<'scope, 'profile> {
#[webapi(data_property, enumerable)]
media_capabilities: v8::Local<'scope, v8::Value>,
#[webapi(data_property, enumerable)]
credentials: v8::Local<'scope, v8::Value>,
}
#[derive(WebApiObject)]
@@ -927,6 +937,19 @@ pub(in crate::context_bootstrap) fn install_navigator_template_bindings<'s>(
}
}
pub(in crate::context_bootstrap) fn finalize_navigator_credentials_realm_bindings<'s>(
scope: &mut v8::PinScope<'s, '_>,
prototype: v8::Local<'s, v8::Object>,
) -> Result<()> {
let global = scope.get_current_context().global(scope);
if super::super::runtime_state::window_realm_secure_context_available(scope, global) {
NavigatorCredentialsPrototypeDeclaration::default()
.initialize(scope, prototype)
.map_err(|error| anyhow!("failed to initialize Navigator credentials: {error}"))?;
}
Ok(())
}
fn filter_navigator_secure_context_exposure<'s>(
scope: &mut v8::PinScope<'s, '_>,
prototype: v8::Local<'s, v8::Object>,
@@ -934,6 +957,7 @@ fn filter_navigator_secure_context_exposure<'s>(
) -> Result<()> {
if !secure_context {
delete_object_property(scope, prototype, "clipboard")?;
delete_object_property(scope, prototype, "credentials")?;
delete_object_property(scope, prototype, "mediaDevices")?;
delete_object_property(scope, prototype, "storage")?;
delete_object_property(scope, prototype, "storageBuckets")?;
@@ -1086,6 +1110,7 @@ pub(super) fn build_lazy_navigator_subobject_in_current_realm<'s>(
build_service_worker_container(scope, owner_child, owner_popup)?.into()
}
NavigatorSubobject::Clipboard => build_clipboard_object(scope)?.into(),
NavigatorSubobject::Credentials => super::credentials::build_container(scope)?.into(),
NavigatorSubobject::UserActivation => build_user_activation(scope)?.into(),
NavigatorSubobject::StorageBuckets => {
build_storage_bucket_manager(scope, owner_child, owner_popup)?.into()
@@ -1275,6 +1300,7 @@ fn build_window_navigator_backing_for_owner<'s>(
storage_buckets: v8::undefined(scope).into(),
geolocation: v8::undefined(scope).into(),
media_capabilities: v8::undefined(scope).into(),
credentials: v8::undefined(scope).into(),
}
.bind(scope)
.map_err(|error| anyhow!("failed to bind Navigator backing object: {error}"))
@@ -1685,6 +1711,7 @@ fn navigator_runtime_data_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
navigator: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
let navigator = moli_webapi_declare::web_api_object_target(scope, navigator)?;
get_private_value(scope, navigator, NAVIGATOR_RUNTIME_DATA_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
}
@@ -22,11 +22,12 @@ pub(super) enum NavigatorSubobject {
StorageBuckets,
Geolocation,
MediaCapabilities,
Credentials,
}
impl NavigatorSubobject {
#[cfg(test)]
pub(super) const ALL: [Self; 16] = [
pub(super) const ALL: [Self; 17] = [
Self::Languages,
Self::MimeTypes,
Self::Plugins,
@@ -43,6 +44,7 @@ impl NavigatorSubobject {
Self::StorageBuckets,
Self::Geolocation,
Self::MediaCapabilities,
Self::Credentials,
];
pub(super) fn from_key(key: &str) -> Option<Self> {
@@ -63,6 +65,7 @@ impl NavigatorSubobject {
"storageBuckets" => Some(Self::StorageBuckets),
"geolocation" => Some(Self::Geolocation),
"mediaCapabilities" => Some(Self::MediaCapabilities),
"credentials" => Some(Self::Credentials),
_ => None,
}
}
@@ -85,6 +88,7 @@ impl NavigatorSubobject {
Self::StorageBuckets => "storageBuckets",
Self::Geolocation => "geolocation",
Self::MediaCapabilities => "mediaCapabilities",
Self::Credentials => "credentials",
}
}
}
@@ -1,5 +1,6 @@
use super::workers::WorkerExecutionState;
use super::*;
use crate::dom::native::Node;
impl JsContextHost {
pub(crate) fn document_has_browsing_context(&self, document: DomHandle) -> bool {
@@ -14,6 +15,35 @@ impl JsContextHost {
.is_some()
}
pub(crate) fn document_is_fully_active(&self, document: DomHandle) -> bool {
if self.page_context_resources_closed {
return false;
}
// A retained Window association is insufficient: the Document and each
// ancestor must still be current in a live browsing context.
let mut document = document;
let mut visited = std::collections::HashSet::new();
loop {
if document == self.document_handle() {
return true;
}
if let Some(popup_id) = self.lightweight_popup_id_for_document_handle(document) {
return self.lightweight_popup_is_open(popup_id);
}
let Some(frame) = self.child_browsing_context_host_for_document_handle(document) else {
return false;
};
if !visited.insert(frame) || !self.dom_host().is_connected(frame) {
return false;
}
let Some(parent_document) = self.dom_host().node(frame).and_then(Node::owner_document)
else {
return false;
};
document = parent_document;
}
}
pub(crate) fn document_activity(&self) -> moli_page_types::DocumentActivity {
self.document_activity
}
@@ -0,0 +1,262 @@
(async () => {
const facts = {complete: false, checks: [], traces: []};
globalThis.__uiEventResults = facts;
const assert = (ok, message) => { if (!ok) throw Error(message); };
const same = (actual, expected) => assert(JSON.stringify(actual) === JSON.stringify(expected), JSON.stringify({actual, expected}));
const foreign = document.getElementById('child').contentWindow;
for (const [realm, w] of [['main', globalThis], ['iframe', foreign]]) {
const run = async (name, fn) => {
try { await fn(); facts.checks.push({realm, name, passed: true}); }
catch (error) { facts.checks.push({realm, name, passed: false, error: error.name, message: error.message}); }
};
if (!w.navigator.credentials) {
facts.checks.push({realm, name: 'navigator.credentials producer', passed: false, message: 'Missing producer'});
continue;
}
const container = w.navigator.credentials;
const prototype = w.CredentialsContainer.prototype;
const PromiseCtor = w.Promise, TypeErrorCtor = w.TypeError, DOMExceptionCtor = w.DOMException;
const methods = Object.fromEntries(['get', 'create', 'store', 'preventSilentAccess'].map(name => [name, prototype[name]]));
const invoke = (name, receiver, args = []) => {
let result;
try { result = methods[name].apply(receiver, args); }
catch (error) { throw Error('Synchronous exception: ' + error); }
assert(result instanceof PromiseCtor, 'Promise must belong to function realm');
return result;
};
const rejection = async (name, receiver, args, Constructor, exceptionName) => {
let error, rejected = false;
try { await invoke(name, receiver, args); } catch (caught) { rejected = true; error = caught; }
assert(rejected, 'Expected a rejected Promise');
if (Constructor) assert(error instanceof Constructor, 'Wrong exception constructor/realm: ' + String(error));
if (exceptionName) {
assert(error.name === exceptionName, 'Wrong exception name: ' + String(error));
if (exceptionName === 'NotSupportedError') assert(error.code === 9, 'NotSupportedError code');
if (exceptionName === 'InvalidStateError') assert(error.code === 11, 'InvalidStateError code');
}
return error;
};
const controller = reason => { const value = new w.AbortController(); value.abort(reason); return value; };
const validCreation = () => ({rp: {name: 'Site'}, user: {name: 'user', displayName: 'User', id: new Uint8Array([1])}, challenge: new Uint8Array([2]), pubKeyCredParams: [{type: 'public-key', alg: -7}]});
const validRequest = () => ({challenge: new Uint8Array([3])});
const getters = (values, trace, prefix = '') => Object.defineProperties({}, Object.fromEntries(Object.entries(values).map(([key, value]) => [key, {enumerable: true, get() { trace.push(prefix + key); return value; }}])));
await run('secure SameObject producer and descriptor', async () => {
assert(w.isSecureContext, 'Fixture must run in a secure context');
const descriptor = Object.getOwnPropertyDescriptor(w.Navigator.prototype, 'credentials');
assert(descriptor.enumerable && descriptor.configurable && descriptor.set === undefined, 'Readonly enumerable accessor');
assert(container === w.navigator.credentials && descriptor.get.call(w.navigator) === container, 'SameObject');
assert(Object.getPrototypeOf(container) === prototype && container instanceof w.CredentialsContainer, 'Relevant-realm prototype');
assert(Object.prototype.toString.call(container) === '[object CredentialsContainer]', 'Interface tag');
let error; try { new w.CredentialsContainer(); } catch (caught) { error = caught; }
assert(error instanceof TypeErrorCtor, 'Illegal constructor');
const other = w === globalThis ? foreign : globalThis;
assert(descriptor.get.call(other.navigator) === other.navigator.credentials, 'Borrowed Navigator getter uses receiver realm/cache');
assert(Object.getPrototypeOf(descriptor.get.call(other.navigator)) === other.CredentialsContainer.prototype, 'Borrowed getter materializes in receiver realm');
});
await run('Navigator getter uses native brand without author traps', async () => {
const getter = Object.getOwnPropertyDescriptor(w.Navigator.prototype, 'credentials').get;
let reads = 0;
const trap = () => { reads++; throw Error('Getter trap'); };
const revoked = Proxy.revocable(w.navigator, {}); revoked.revoke();
for (const fake of [{}, Object.create(w.navigator), Object.create(w.Navigator.prototype), new Proxy(w.navigator, {get: trap, getPrototypeOf: trap}), revoked.proxy, new EventTarget()]) {
let error; try { getter.call(fake); } catch (caught) { error = caught; }
assert(error instanceof TypeErrorCtor, 'Navigator getter accepted a forged receiver');
}
assert(reads === 0, 'Receiver check ran author traps');
});
const trap = () => { throw Error('Unexpected author trap'); };
const revoked = Proxy.revocable(container, {}); revoked.revoke();
const badReceivers = [null, undefined, {}, Object.create(container), Object.create(prototype), new Proxy(container, {get: trap, getPrototypeOf: trap}), revoked.proxy, new EventTarget()];
for (const [name, length] of [['get', 0], ['create', 0], ['store', 1], ['preventSilentAccess', 0]]) {
await run(name + '/native descriptor and arity', async () => {
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
assert(descriptor.enumerable && descriptor.configurable && descriptor.writable, 'Writable enumerable method');
assert(methods[name].name === name && methods[name].length === length, 'Name and arity');
assert(Function.prototype.toString.call(methods[name]).includes('[native code]'), 'Native function');
});
for (const [index, receiver] of badReceivers.entries()) {
await run(name + '/brand before argument conversion/' + index, async () => {
let reads = 0;
const argument = new Proxy({}, {get() { reads++; throw Error('Argument conversion ran'); }});
await rejection(name, receiver, [argument], TypeErrorCtor);
assert(reads === 0, 'Invalid receiver must fail before any argument conversion');
});
}
}
for (const name of ['get', 'create']) {
for (const [label, args] of [['omitted', []], ['undefined', [undefined]], ['null', [null]], ['empty', [{}]], ['unknown', [{get unknown() { throw Error('Unknown member read'); }}]]]) {
await run(name + '/empty dictionary/' + label, () => rejection(name, container, args, DOMExceptionCtor, 'NotSupportedError'));
}
for (const [index, value] of [true, 1, 'string', Symbol('dictionary'), 1n].entries()) {
await run(name + '/primitive dictionary/' + index, () => rejection(name, container, [value], TypeErrorCtor));
}
for (const mediation of ['silent', 'optional', 'conditional', 'required']) {
await run(name + '/valid mediation/' + mediation, async () => {
const marker = {}, signal = controller(marker).signal;
assert(await rejection(name, container, [{mediation, signal}]) === marker, 'Pre-abort reason identity');
});
}
for (const [index, mediation] of ['', 'OPTIONAL', null, Symbol('mediation')].entries()) {
await run(name + '/invalid mediation/' + index, () => rejection(name, container, [{mediation, signal: controller({}).signal}], TypeErrorCtor));
}
for (const [index, reason] of ['reason', {}, [], new Error('reason'), null, 42, 1n, Symbol('reason')].entries()) {
await run(name + '/pre-abort exact reason/' + index, async () => {
const signal = controller(reason).signal;
assert(await rejection(name, container, [{signal}]) === reason, 'Abort reason was replaced');
});
}
await run(name + '/default abort reason and foreign signal', async () => {
const other = w === globalThis ? foreign : globalThis;
const abort = new other.AbortController(); abort.abort();
assert(await rejection(name, container, [{signal: abort.signal}]) === abort.signal.reason, 'Cross-realm native signal/default reason');
});
const signal = new w.AbortController().signal;
const revokedSignal = Proxy.revocable(signal, {}); revokedSignal.revoke();
for (const [index, invalid] of [null, {}, Object.create(signal), Object.create(w.AbortSignal.prototype), new Proxy(signal, {get: trap, getPrototypeOf: trap}), revokedSignal.proxy, new EventTarget()].entries()) {
await run(name + '/AbortSignal native brand/' + index, () => rejection(name, container, [{signal: invalid}], TypeErrorCtor));
}
await run(name + '/abort state ignores public getters', async () => {
const marker = {}, signal = controller(marker).signal;
Object.defineProperties(signal, {aborted: {get: trap}, reason: {get: trap}});
assert(await rejection(name, container, [{signal}]) === marker, 'Internal abort state/reason');
});
await run(name + '/conversion exception before pre-abort', async () => {
const marker = {}, signal = controller({}).signal;
assert(await rejection(name, container, [{get mediation() { throw marker; }, signal}]) === marker, 'Conversion exception was replaced');
});
await run(name + '/all dictionary getters precede native algorithm', async () => {
const trace = [], marker = {}, signal = controller(marker).signal;
const values = name === 'get' ? {federated: undefined, mediation: 'optional', password: false, publicKey: undefined, signal, uiMode: undefined} : {federated: undefined, mediation: 'optional', password: undefined, publicKey: undefined, signal};
const options = getters(values, trace);
assert(await rejection(name, container, [options]) === marker, 'Abort must follow conversion');
same(trace, Object.keys(values)); facts.traces.push({realm, name, trace});
});
await run(name + '/missing defaults avoid Object.prototype getters', async () => {
let reads = 0;
const get = () => { reads++; throw Error('Inherited default read'); };
for (const key of ['federated', 'mediation', 'password', 'publicKey', 'signal']) Object.defineProperty(w.Object.prototype, key, {configurable: true, get});
let error;
try { error = await rejection(name, container, [], DOMExceptionCtor, 'NotSupportedError'); }
finally { for (const key of ['federated', 'mediation', 'password', 'publicKey', 'signal']) delete w.Object.prototype[key]; }
assert(reads === 0 && error.name === 'NotSupportedError', 'Empty default is not an ordinary JS object');
});
for (const [index, binary] of [null, [], {}, 'bytes', new Proxy(new Uint8Array([1]), {})].entries()) {
await run(name + '/native BufferSource/' + index, async () => {
const publicKey = name === 'create' ? validCreation() : validRequest();
publicKey.challenge = binary;
await rejection(name, container, [{publicKey, signal: controller({}).signal}], TypeErrorCtor);
});
}
for (const [index, binary] of [new ArrayBuffer(2), new Uint8Array([1, 2]), new DataView(new ArrayBuffer(3))].entries()) {
await run(name + '/valid BufferSource before abort/' + index, async () => {
const marker = {}, publicKey = name === 'create' ? validCreation() : validRequest();
publicKey.challenge = binary;
assert(await rejection(name, container, [{publicKey, signal: controller(marker).signal}]) === marker, 'Native ArrayBuffer/view conversion');
});
}
for (const [index, binary] of [new ArrayBuffer(2, {maxByteLength: 4}), new Uint8Array(new ArrayBuffer(2, {maxByteLength: 4})), new DataView(new ArrayBuffer(2, {maxByteLength: 4}))].entries()) {
await run(name + '/reject resizable BufferSource/' + index, async () => {
const publicKey = name === 'create' ? validCreation() : validRequest(); publicKey.challenge = binary;
await rejection(name, container, [{publicKey, signal: controller({}).signal}], TypeErrorCtor);
});
}
if (typeof w.SharedArrayBuffer === 'function') {
for (const [index, binary] of [new w.SharedArrayBuffer(2), new Uint8Array(new w.SharedArrayBuffer(2)), new DataView(new w.SharedArrayBuffer(2))].entries()) {
await run(name + '/reject shared BufferSource/' + index, async () => {
const publicKey = name === 'create' ? validCreation() : validRequest(); publicKey.challenge = binary;
await rejection(name, container, [{publicKey, signal: controller({}).signal}], TypeErrorCtor);
});
}
}
for (const extension of [{credBlob: {}}, {largeBlob: {write: {}}}, {prf: {eval: {first: {}}}}, {prf: {evalByCredential: {AA: {first: {}}}}}]) {
await run(name + '/nested extension buffers/' + JSON.stringify(extension), async () => {
const publicKey = name === 'create' ? validCreation() : validRequest(); publicKey.extensions = extension;
await rejection(name, container, [{publicKey, signal: controller({}).signal}], TypeErrorCtor);
});
}
await run(name + '/credential descriptor required members', async () => {
const publicKey = name === 'create' ? validCreation() : validRequest();
publicKey[name === 'create' ? 'excludeCredentials' : 'allowCredentials'] = [{type: 'public-key'}];
await rejection(name, container, [{publicKey, signal: controller({}).signal}], TypeErrorCtor);
});
await run(name + '/PRF record key getter exception identity', async () => {
const marker = {}, publicKey = name === 'create' ? validCreation() : validRequest();
publicKey.extensions = {prf: {evalByCredential: {get AA() { throw marker; }}}};
assert(await rejection(name, container, [{publicKey, signal: controller({}).signal}]) === marker, 'Record getter exception identity');
});
}
await run('create/inherited dictionaries convert before derived members', async () => {
const marker = {}, trace = [], publicKey = validCreation();
publicKey.rp = getters({name: 'Site', id: location.hostname}, trace, 'rp.');
publicKey.user = getters({name: 'user', displayName: 'User', id: new Uint8Array([1])}, trace, 'user.');
assert(await rejection('create', container, [{publicKey, signal: controller(marker).signal}]) === marker, 'Valid nested conversion');
same(trace, ['rp.name', 'rp.id', 'user.name', 'user.displayName', 'user.id']);
});
for (const kind of ['password', 'federated']) {
await run('create/' + kind + '/inherited dictionary member order', async () => {
const marker = {}, trace = [];
const values = kind === 'password' ? {id: 'user', iconURL: '', name: 'User', origin: location.origin, password: 'secret'} : {id: 'user', iconURL: '', name: 'User', origin: location.origin, protocol: undefined, provider: 'https://idp.example'};
const options = {[kind]: getters(values, trace), signal: controller(marker).signal};
assert(await rejection('create', container, [options]) === marker, 'Abort after credential conversion');
same(trace, Object.keys(values));
});
await run('create/' + kind + '/required origin', async () => {
const data = kind === 'password' ? {id: 'user', password: 'secret'} : {id: 'user', provider: 'https://idp.example'};
await rejection('create', container, [{[kind]: data, signal: controller({}).signal}], TypeErrorCtor);
});
}
await run('create/native form union precedes dictionary getters', async () => {
const marker = {}, form = document.createElement('form');
Object.defineProperty(form, 'password', {get: trap});
assert(await rejection('create', container, [{password: form, signal: controller(marker).signal}]) === marker, 'Native form union branch');
});
await run('create/multiple credential types before pre-abort', async () => {
const password = {id: 'user', origin: location.origin, password: 'secret'}, publicKey = validCreation();
await rejection('create', container, [{password, publicKey, signal: controller({}).signal}], DOMExceptionCtor, 'NotSupportedError');
});
for (const [index, args] of [[], [undefined], [null], [{}], [Object.create(w.Credential.prototype)]].entries()) {
await run('store/required native Credential/' + index, () => rejection('store', container, args, TypeErrorCtor));
}
await run('preventSilentAccess/idempotent undefined without argument conversion', async () => {
assert(await invoke('preventSilentAccess', container, [new Proxy({}, {get: trap})]) === undefined, 'No extra argument conversion');
assert(await invoke('preventSilentAccess', container) === undefined, 'Idempotent result');
});
await run('native brand remains after prototype replacement', async () => {
const saved = Object.getPrototypeOf(container);
Object.setPrototypeOf(container, null);
try { assert(await invoke('preventSilentAccess', container) === undefined, 'Native brand survives prototype change'); }
finally { Object.setPrototypeOf(container, saved); }
});
await run('native Promise and errors ignore replaced public constructors', async () => {
const saved = [w.Promise, w.TypeError, w.DOMException, w.CredentialsContainer];
try {
w.Promise = w.TypeError = w.DOMException = w.CredentialsContainer = function() { throw Error('Public constructor called'); };
await rejection('store', container, [{}], TypeErrorCtor);
await rejection('create', container, [], DOMExceptionCtor, 'NotSupportedError');
assert(await invoke('preventSilentAccess', container) === undefined, 'Native Promise resolver');
} finally { [w.Promise, w.TypeError, w.DOMException, w.CredentialsContainer] = saved; }
});
}
for (const ancestor of [false, true]) {
const name = ancestor ? 'inactive ancestor' : 'removed iframe';
try {
const outer = document.createElement('iframe'); document.body.appendChild(outer);
let frame = outer;
if (ancestor) { frame = outer.contentDocument.createElement('iframe'); outer.contentDocument.body.appendChild(frame); }
const w = frame.contentWindow, container = w.navigator.credentials, PromiseCtor = w.Promise, DOMExceptionCtor = w.DOMException, TypeErrorCtor = w.TypeError;
const marker = {}, abort = new w.AbortController(); abort.abort(marker);
const get = container.get, create = container.create, prevent = container.preventSilentAccess, store = container.store;
outer.remove();
for (const [method, args] of [[get, [{signal: abort.signal}]], [create, [{signal: abort.signal}]], [prevent, []]]) {
let error; const promise = method.apply(container, args); assert(promise instanceof PromiseCtor, 'Inactive function Promise realm');
try { await promise; } catch (caught) { error = caught; }
assert(error instanceof DOMExceptionCtor && error.name === 'InvalidStateError', 'Fully active check precedes pre-abort');
}
let error; try { await store.call(container, {}); } catch (caught) { error = caught; }
assert(error instanceof TypeErrorCtor, 'WebIDL conversion precedes native activity check');
facts.checks.push({realm: 'lifecycle', name, passed: true});
} catch (error) { facts.checks.push({realm: 'lifecycle', name, passed: false, error: error.name, message: error.message}); }
}
facts.passed = facts.checks.filter(row => row.passed).length; facts.total = facts.checks.length; facts.complete = true;
return true;
})()
@@ -0,0 +1,123 @@
use super::*;
#[test]
fn credentials_container_preserves_native_brands_conversion_order_abort_and_inactive_realms() {
let mut vm = new_storage_page_task_executor_test_vm("https://credentials-container.test/");
vm.eval("document.body.innerHTML = '<iframe id=child></iframe>'")
.unwrap();
vm.eval(include_str!("credential_container.js")).unwrap();
assert_eq!(vm.eval("__uiEventResults.complete").unwrap(), "true");
assert_eq!(
vm.eval("JSON.stringify(__uiEventResults.checks.filter(row => !row.passed))")
.unwrap(),
"[]"
);
}
#[test]
fn credentials_container_exposes_navigator_property_in_secure_contexts() {
for (url, exposed) in [
("https://credentials-container.test/", true),
("http://localhost/", true),
("http://credentials-container.test/", false),
] {
let mut vm = new_storage_page_task_executor_test_vm(url);
assert_eq!(vm.eval("isSecureContext").unwrap(), exposed.to_string());
vm.eval(&format!(
"Object.defineProperty(globalThis, 'isSecureContext', {{value: {}}})",
!exposed
))
.unwrap();
assert_eq!(
vm.eval("Object.hasOwn(Navigator.prototype, 'credentials')")
.unwrap(),
exposed.to_string(),
"{url}"
);
assert_eq!(
vm.eval("'credentials' in navigator").unwrap(),
exposed.to_string(),
"{url} after Navigator materialization"
);
}
}
#[test]
fn credentials_container_accepts_registered_native_proxies_and_native_credential_arguments() {
let mut vm = new_storage_page_task_executor_test_vm("https://credentials-container.test/");
vm.eval("globalThis.nativeSignal = AbortSignal.abort({})")
.unwrap();
let context_ptr = &vm.page_default_runtime.context as *const _;
vm.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| {
let global = scope.get_current_context().global(scope);
let fixtures = v8::Object::new(scope);
for name in ["CredentialsContainer", "Credential", "PublicKeyCredential"] {
let object = v8::Object::new(scope);
// Identity-only fixtures, not produced credentials or saved secrets.
moli_webapi_declare::initialize_web_api_object(scope, object, name).unwrap();
let handler = crate::util::new_null_prototype_object(scope);
let proxy = v8::Proxy::new(scope, object, handler).unwrap();
moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap();
let key = crate::util::v8str(scope, name);
assert_eq!(
fixtures.create_data_property(scope, key.into(), proxy.into()),
Some(true)
);
}
let key = crate::util::v8str(scope, "navigator");
let navigator = global.get(scope, key.into()).unwrap();
let navigator = v8::Local::<v8::Object>::try_from(navigator).unwrap();
let handler = crate::util::new_null_prototype_object(scope);
let proxy = v8::Proxy::new(scope, navigator, handler).unwrap();
moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap();
let key = crate::util::v8str(scope, "Navigator");
assert_eq!(
fixtures.create_data_property(scope, key.into(), proxy.into()),
Some(true)
);
let key = crate::util::v8str(scope, "nativeSignal");
let signal = global.get(scope, key.into()).unwrap();
let signal = v8::Local::<v8::Object>::try_from(signal).unwrap();
let handler = crate::util::new_null_prototype_object(scope);
let proxy = v8::Proxy::new(scope, signal, handler).unwrap();
moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap();
let key = crate::util::v8str(scope, "AbortSignal");
assert_eq!(
fixtures.create_data_property(scope, key.into(), proxy.into()),
Some(true)
);
let key = crate::util::v8str(scope, "nativeFixtures");
assert_eq!(
global.create_data_property(scope, key.into(), fixtures.into()),
Some(true)
);
Ok(())
})
.unwrap();
vm.eval(r#"(async () => {
const prototype = CredentialsContainer.prototype, container = nativeFixtures.CredentialsContainer;
const getter = Object.getOwnPropertyDescriptor(Navigator.prototype, 'credentials').get;
if (getter.call(nativeFixtures.Navigator) !== navigator.credentials) throw Error('Native Navigator proxy rejected');
if (await prototype.preventSilentAccess.call(container) !== undefined) throw Error('Native proxy receiver rejected');
for (const method of ['get', 'create']) {
let reason; try { await prototype[method].call(container, {signal: nativeFixtures.AbortSignal}); } catch (caught) { reason = caught; }
if (reason !== nativeSignal.reason) throw Error('Native AbortSignal proxy rejected');
}
for (const credential of [nativeFixtures.Credential, nativeFixtures.PublicKeyCredential]) {
let error; try { await prototype.store.call(container, credential); } catch (caught) { error = caught; }
if (!(error instanceof DOMException) || error.name !== 'NotSupportedError') throw Error('Native Credential brand rejected');
let conversions = 0;
const revoked = Proxy.revocable(credential, {}); revoked.revoke();
for (const fake of [Object.create(credential), new Proxy(credential, {get() { conversions++; throw Error('Trap'); }}), revoked.proxy]) {
let error; try { await prototype.store.call(container, fake); } catch (caught) { error = caught; }
if (!(error instanceof TypeError)) throw Error('Author receiver accepted');
}
if (conversions) throw Error('Native interface conversion ran author trap');
}
globalThis.nativeCredentialTestDone = true;
})().catch(error => globalThis.nativeCredentialTestDone = String(error))"#).unwrap();
assert_eq!(
vm.eval("globalThis.nativeCredentialTestDone").unwrap(),
"true"
);
}
@@ -1,3 +1,4 @@
mod credential_container;
mod credential_json_options;
mod writable_stream_native_receivers;
use super::post_parse::dynamic_script_execute_is_runnable_before_dom_content_loaded;