feat(wake-lock): expose secure native interfaces with explicit platform denial

This commit is contained in:
ldm0
2026-10-11 18:06:45 +08:00
parent 1f47061c6c
commit ea1354d461
12 changed files with 370 additions and 34 deletions
@@ -16,6 +16,7 @@ enum RealmDependentFinalizer {
GlobalEventHandlersSecureContextSurface,
CryptoSecureContextSurface,
BaseAudioContextSecureContextSurface,
NavigatorSecureContextSurface,
XmlHttpRequestEventTargetState,
NotificationPermission,
PointerEventSecureContextSurface,
@@ -46,6 +47,10 @@ const REALM_DEPENDENT_FINALIZER_ALLOWLIST: &[(&str, RealmDependentFinalizer)] =
"BaseAudioContext",
RealmDependentFinalizer::BaseAudioContextSecureContextSurface,
),
(
"Navigator",
RealmDependentFinalizer::NavigatorSecureContextSurface,
),
(
"Crypto",
RealmDependentFinalizer::CryptoSecureContextSurface,
@@ -123,7 +128,12 @@ pub(super) fn finalize_materialized_interface<'s>(
RealmDependentFinalizer::GlobalEventHandlersSecureContextSurface => {
crate::context_bootstrap::window_events::finalize_secure_global_event_handler_realm_bindings(
scope, prototype,
)
)?;
}
RealmDependentFinalizer::NavigatorSecureContextSurface => {
crate::context_bootstrap::navigator_runtime::finalize_navigator_realm_bindings(
scope, prototype,
)?;
}
RealmDependentFinalizer::CryptoSecureContextSurface => {
finalize_crypto_realm_bindings(scope, prototype)
@@ -193,6 +203,7 @@ mod tests {
"DocumentType",
"CharacterData",
"BaseAudioContext",
"Navigator",
"Crypto",
"XMLHttpRequestEventTarget",
"Notification",
@@ -201,8 +201,8 @@ const SECURE_CONTEXT_ONLY_INTERFACE_NAMES: &[&str] = &[
"MediaDeviceInfo",
"InputDeviceInfo",
"MediaDevices",
"MediaDeviceInfo",
"InputDeviceInfo",
"WakeLock",
"WakeLockSentinel",
"SubtleCrypto",
"CryptoKey",
"PaymentResponse",
@@ -10,12 +10,14 @@ mod navigator_subobjects;
mod screen;
mod user_activation;
mod visual_viewport;
mod wake_lock;
mod window_state;
pub(crate) use geolocation::notify_geolocation_override_changed;
pub(in crate::context_bootstrap) use self::clipboard::clipboard_item_constructor_callback;
pub(crate) use self::navigator::build_lightweight_popup_window_navigator_object;
pub(super) use self::navigator::finalize_navigator_realm_bindings;
pub(super) use self::navigator::install_navigator_template_bindings;
pub(crate) use self::navigator::install_worker_navigator_runtime_state;
pub(in crate::context_bootstrap) use self::navigator::{
@@ -75,6 +75,7 @@ const NAVIGATOR_RUNTIME_DATA_KEYS: &[&str] = &[
"storageBuckets",
"geolocation",
"mediaCapabilities",
"wakeLock",
];
const WORKER_NAVIGATOR_INSTALLED_SLOT: &str = "__moliWorkerNavigatorInstalled";
const WORKER_NAVIGATOR_MATERIALIZING_SLOT: &str = "__moliWorkerNavigatorMaterializing";
@@ -194,6 +195,8 @@ struct NavigatorRuntimeDataPrototypeDeclaration {
geolocation: (),
#[webapi(accessor_property, getter = navigator_runtime_data_getter_callback, data = callback_data_index_value(scope, 32))]
media_capabilities: (),
#[webapi(accessor_property, getter = navigator_runtime_data_getter_callback, data = callback_data_index_value(scope, 33))]
wake_lock: (),
#[webapi(accessor_property, getter = navigator_cookie_enabled_getter_callback)]
cookie_enabled: (),
}
@@ -585,6 +588,9 @@ struct WindowNavigatorBackingDeclaration<'scope, 'profile> {
#[webapi(data_property, enumerable)]
media_capabilities: v8::Local<'scope, v8::Value>,
#[webapi(data_property, enumerable)]
wake_lock: v8::Local<'scope, v8::Value>,
}
#[derive(WebApiObject)]
@@ -877,6 +883,7 @@ pub(in crate::context_bootstrap) fn install_navigator_template_bindings<'s>(
super::gamepad::install(scope, template, interface_name);
install_media_capabilities_template_bindings(scope, template, interface_name);
super::credentials::install(scope, template, interface_name);
super::wake_lock::install(scope, template, interface_name);
super::user_activation::install_user_activation_template_bindings(
scope,
template,
@@ -918,14 +925,17 @@ pub(in crate::context_bootstrap) fn install_navigator_template_bindings<'s>(
}
}
fn filter_navigator_secure_context_exposure<'s>(
pub(in crate::context_bootstrap) fn finalize_navigator_realm_bindings<'s>(
scope: &mut v8::PinScope<'s, '_>,
prototype: v8::Local<'s, v8::Object>,
secure_context: bool,
) -> Result<()> {
let window = scope.get_current_context().global(scope);
let secure_context =
super::super::runtime_state::window_realm_secure_context_available(scope, window);
if !secure_context {
delete_object_property(scope, prototype, "clipboard")?;
delete_object_property(scope, prototype, "mediaDevices")?;
delete_object_property(scope, prototype, "wakeLock")?;
delete_object_property(scope, prototype, "storage")?;
delete_object_property(scope, prototype, "storageBuckets")?;
delete_object_property(scope, prototype, "serviceWorker")?;
@@ -1088,6 +1098,7 @@ pub(super) fn build_lazy_navigator_subobject_in_current_realm<'s>(
|| navigator_geolocation_secure_context_available(scope, owner_child, owner_popup);
build_media_capabilities_object(scope, secure_context, worker)?.into()
}
NavigatorSubobject::WakeLock => super::wake_lock::build(scope)?.into(),
};
Ok(value)
}
@@ -1261,6 +1272,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(),
wake_lock: v8::undefined(scope).into(),
}
.bind(scope)
.map_err(|error| anyhow!("failed to bind Navigator backing object: {error}"))
@@ -1289,15 +1301,19 @@ pub(super) fn build_window_navigator_object_for_owner<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner_child: Option<DomHandle>,
identity: Option<&BrowserIdentityProfile>,
storage_apis_available: bool,
) -> Result<v8::Local<'s, v8::Object>> {
let backing = build_window_navigator_backing_for_owner(scope, owner_child, None, identity)?;
bind_window_navigator(scope, backing)
}
fn bind_window_navigator<'s>(
scope: &mut v8::PinScope<'s, '_>,
backing: v8::Local<'s, v8::Object>,
) -> Result<v8::Local<'s, v8::Object>> {
let _ = ensure_intrinsic_interface_prototype(scope, "Navigator")?;
let navigator = NavigatorObjectDeclaration::new(backing)
.bind(scope)
.map_err(|error| anyhow!("failed to bind Navigator object: {error}"))?;
if let Some(prototype) = global_constructor_prototype(scope, "Navigator") {
filter_navigator_secure_context_exposure(scope, prototype, storage_apis_available)?;
}
Ok(navigator)
}
@@ -1353,9 +1369,7 @@ pub(crate) fn build_lightweight_popup_window_navigator_object<'s>(
owner_popup: u64,
) -> Result<v8::Local<'s, v8::Object>> {
let backing = build_window_navigator_backing_for_owner(scope, None, Some(owner_popup), None)?;
NavigatorObjectDeclaration::new(backing)
.bind(scope)
.map_err(|error| anyhow!("failed to bind lightweight popup Navigator object: {error}"))
bind_window_navigator(scope, backing)
}
fn build_worker_navigator_backing<'s>(
@@ -22,11 +22,12 @@ pub(super) enum NavigatorSubobject {
StorageBuckets,
Geolocation,
MediaCapabilities,
WakeLock,
}
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::WakeLock,
];
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),
"wakeLock" => Some(Self::WakeLock),
_ => None,
}
}
@@ -85,6 +88,7 @@ impl NavigatorSubobject {
Self::StorageBuckets => "storageBuckets",
Self::Geolocation => "geolocation",
Self::MediaCapabilities => "mediaCapabilities",
Self::WakeLock => "wakeLock",
}
}
}
@@ -0,0 +1,108 @@
//! Secure Window wake-lock bindings for a UA without a platform wake-lock backend.
//!
//! The specification permits implementation-specific denial. Convert arguments
//! before rejecting valid requests; never manufacture an acquired sentinel.
use anyhow::Result;
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
use crate::{native_bridge::throw_dom_exception, web_api_interfaces, webidl};
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::WakeLock)]
struct WakeLockObjectDeclaration<'s> {
#[webapi(prototype)]
prototype: v8::Local<'s, v8::Object>,
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::WakeLock, enumerable, receiver)]
struct WakeLockPrototypeDeclaration {
#[webapi(method, returns_promise, length = 0, callback = request)]
request: (),
}
// request() cannot return a sentinel until platform acquisition is implemented.
// Keep the native interface surface and receiver validation without inventing
// acquired/released state or dispatching a synthetic release event.
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::WakeLockSentinel, enumerable, receiver)]
struct WakeLockSentinelPrototypeDeclaration {
#[webapi(accessor_property, getter = sentinel_backend_unavailable)]
released: (),
#[webapi(accessor_property = "type", getter = sentinel_backend_unavailable)]
lock_type: (),
#[webapi(method, returns_promise, length = 0, callback = sentinel_backend_unavailable)]
release: (),
#[webapi(accessor_property, getter = sentinel_backend_unavailable, setter = sentinel_backend_unavailable)]
onrelease: (),
}
#[derive(Clone, Copy, webidl::WebIdlEnum)]
#[webidl(name = "WakeLockType")]
enum WakeLockType {
Screen,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "WakeLock.request")]
struct RequestArgs {
#[webidl(converter = "enum", default = WakeLockType::Screen)]
lock_type: WakeLockType,
}
pub(super) fn install<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
interface_name: &str,
) {
let prototype = template.prototype_template(scope);
match interface_name {
"WakeLock" => {
WakeLockPrototypeDeclaration::initialize_prototype_template(scope, prototype);
}
"WakeLockSentinel" => {
WakeLockSentinelPrototypeDeclaration::initialize_prototype_template(scope, prototype);
}
_ => {}
}
}
pub(super) fn build<'s>(scope: &mut v8::PinScope<'s, '_>) -> Result<v8::Local<'s, v8::Object>> {
let prototype =
crate::context_bootstrap::ensure_intrinsic_interface_prototype(scope, "WakeLock")?;
WakeLockObjectDeclaration::new(prototype)
.bind(scope)
.map_err(Into::into)
}
fn request<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'s>,
) {
let Some(parsed) = webidl::parse_args::<RequestArgs>(scope, &args) else {
return;
};
match parsed.lock_type {
WakeLockType::Screen => throw_dom_exception(
scope,
"NotAllowedError",
0,
"Screen wake locks are unavailable on this platform.",
),
}
}
fn sentinel_backend_unavailable<'s>(
scope: &mut v8::PinScope<'s, '_>,
_args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'s>,
) {
throw_dom_exception(
scope,
"NotSupportedError",
9,
"Platform wake-lock sentinels are not implemented.",
);
}
@@ -12,8 +12,6 @@ use crate::{
use anyhow::{Result, anyhow};
use moli_browser_profile::BrowserIdentityProfile;
const WINDOW_NAVIGATOR_STORAGE_APIS_AVAILABLE_SLOT: &str =
"__moliWindowNavigatorStorageApisAvailable";
const WINDOW_NAVIGATOR_USER_AGENT_SLOT: &str = "__moliWindowNavigatorUserAgent";
const WINDOW_NAVIGATOR_ACCEPT_LANGUAGE_SLOT: &str = "__moliWindowNavigatorAcceptLanguage";
@@ -32,18 +30,7 @@ pub(in crate::context_bootstrap) fn build_window_navigator_for_receiver<'s>(
let identity = navigator_identity_profile(scope, receiver).or_else(|| {
user_agent.map(|user_agent| BrowserIdentityProfile::new(user_agent, accept_language))
});
let storage_apis_available = get_private_value(
scope,
receiver,
WINDOW_NAVIGATOR_STORAGE_APIS_AVAILABLE_SLOT,
)
.is_none_or(|value| value.boolean_value(scope));
build_window_navigator_object_for_owner(
scope,
owner_child,
identity.as_ref(),
storage_apis_available,
)
build_window_navigator_object_for_owner(scope, owner_child, identity.as_ref())
}
pub(in crate::context_bootstrap) fn install_navigator_runtime_state<'s>(
@@ -52,13 +39,6 @@ pub(in crate::context_bootstrap) fn install_navigator_runtime_state<'s>(
storage_apis_available: bool,
) -> Result<()> {
super::user_activation::initialize_window_user_activation(scope, global)?;
let storage_apis_available_value = v8::Boolean::new(scope, storage_apis_available);
set_private_value(
scope,
global,
WINDOW_NAVIGATOR_STORAGE_APIS_AVAILABLE_SLOT,
storage_apis_available_value.into(),
);
if storage_apis_available {
GlobalCachesAccessorDeclaration::new(true)
.initialize(scope, global)
@@ -856,6 +856,14 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
interface: web_api_interfaces::MediaDevices::DESCRIPTOR,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
interface: web_api_interfaces::WakeLock::DESCRIPTOR,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
interface: web_api_interfaces::WakeLockSentinel::DESCRIPTOR,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
interface: web_api_interfaces::Clipboard::DESCRIPTOR,
kind: ConstructorKind::Illegal,
@@ -2320,6 +2320,7 @@ mod time_ranges;
mod url_components;
mod video_codecs_shell;
mod web_lock_interface_exposure_interfaces;
mod wake_lock_interfaces;
mod webgl_interfaces;
mod webidl_collections;
mod webidl_fetch;
@@ -0,0 +1,176 @@
(async () => {
const rows = [], platformRequests = [];
const assert = (condition, message) => { if (!condition) throw Error(message); };
const check = async (name, callback) => {
try { await callback(); rows.push({name, pass: true}); }
catch (error) { rows.push({name, pass: false, message: String(error)}); }
};
const thrown = callback => {
try { callback(); } catch (error) { return error; }
throw Error('expected synchronous exception');
};
const rejected = async (w, callback) => {
const promise = callback();
assert(promise instanceof w.Promise, 'Promise belongs to the callee realm');
try { await promise; } catch (error) { return error; }
throw Error('expected rejected Promise');
};
const popup = open();
const realms = [['main', window], ['child', document.getElementById('child').contentWindow], ['popup', popup]];
const mainGetter = isSecureContext && Object.getOwnPropertyDescriptor(Navigator.prototype, 'wakeLock')?.get;
try {
for (const [label, w] of realms) {
if (!w.isSecureContext) {
await check(label + '/navigator/insecure prototype before materialization', () => {
assert(!Object.hasOwn(w.Navigator.prototype, 'wakeLock'), 'secure prototype member hidden before Navigator getter');
});
for (const name of ['WakeLock', 'WakeLockSentinel']) {
await check(label + '/' + name + '/insecure', () => assert(!(name in w), 'secure interface hidden'));
}
await check(label + '/navigator/insecure', () => assert(!('wakeLock' in w.navigator), 'secure Navigator member hidden'));
await check(label + '/navigator/author properties after finalization', () => {
Object.defineProperty(w.Navigator.prototype, 'wakeLock', {configurable: true, value: 42});
try { assert(w.navigator.wakeLock === 42, 'author property remains observable'); }
finally { delete w.Navigator.prototype.wakeLock; }
});
continue;
}
for (const name of ['WakeLock', 'WakeLockSentinel']) {
await check(label + '/' + name + '/interface', () => {
const C = w[name], parent = name === 'WakeLock' ? w.Object : w.EventTarget;
const d = Object.getOwnPropertyDescriptor(w, name);
assert(typeof C === 'function' && C.name === name && C.length === 0, 'interface constructor metadata');
assert(d.writable && d.configurable && !d.enumerable, 'global descriptor');
assert(Object.getPrototypeOf(C.prototype) === parent.prototype, 'prototype inheritance');
assert(Object.getPrototypeOf(C) === (name === 'WakeLock' ? w.Function.prototype : parent), 'constructor inheritance');
assert(C.prototype.constructor === C, 'prototype constructor');
const tag = Object.getOwnPropertyDescriptor(C.prototype, Symbol.toStringTag);
assert(tag.value === name && !tag.writable && !tag.enumerable && tag.configurable, 'interface tag');
assert(thrown(() => C()) instanceof w.TypeError && thrown(() => new C()) instanceof w.TypeError, 'illegal constructor realm');
});
}
let lock;
await check(label + '/navigator/SameObject intrinsic and relevant realm', () => {
const C = w.WakeLock, original = Object.getOwnPropertyDescriptor(w, 'WakeLock');
const inherited = Object.getOwnPropertyDescriptor(w.Object.prototype, 'wakeLock');
let constructorReads = 0, inheritedReads = 0;
Object.defineProperty(w, 'WakeLock', {configurable: true, get() { constructorReads++; throw Error('author constructor'); }});
Object.defineProperty(w.Object.prototype, 'wakeLock', {configurable: true, get() { inheritedReads++; throw Error('inherited cache hook'); }});
try { lock = mainGetter.call(w.navigator); }
finally {
if (original) Object.defineProperty(w, 'WakeLock', original); else delete w.WakeLock;
if (inherited) Object.defineProperty(w.Object.prototype, 'wakeLock', inherited); else delete w.Object.prototype.wakeLock;
}
assert(constructorReads === 0 && inheritedReads === 0, 'native materialization ignores public hooks');
assert(lock === w.navigator.wakeLock && Object.getPrototypeOf(lock) === C.prototype, 'SameObject in Navigator relevant realm');
assert(lock instanceof C && Object.prototype.toString.call(lock) === '[object WakeLock]', 'native WakeLock identity');
assert(!Object.hasOwn(w.navigator, 'wakeLock') && !Object.hasOwn(lock, 'request'), 'shared prototype surface');
const d = Object.getOwnPropertyDescriptor(w.Navigator.prototype, 'wakeLock');
assert(d.enumerable && d.configurable && d.set === undefined && d.get.length === 0 && d.get.name === 'get wakeLock', 'readonly Navigator accessor');
assert(Reflect.set(w.navigator, 'wakeLock', {}) === false && w.navigator.wakeLock === lock, 'readonly SameObject');
});
await check(label + '/navigator/receiver brands', () => {
const get = Object.getOwnPropertyDescriptor(w.Navigator.prototype, 'wakeLock').get;
let traps = 0; const trap = () => { traps++; throw Error('receiver hook'); };
const revoked = Proxy.revocable(w.navigator, {}); revoked.revoke();
for (const value of [null, {}, w.Navigator.prototype, Object.create(w.navigator), new Proxy(w.navigator, {get: trap, getPrototypeOf: trap}), revoked.proxy]) {
assert(thrown(() => get.call(value)) instanceof w.TypeError, 'callee realm rejects unbranded Navigator');
}
assert(traps === 0, 'receiver validation does not run author hooks');
});
await check(label + '/request/descriptor', () => {
const d = Object.getOwnPropertyDescriptor(w.WakeLock.prototype, 'request');
assert(d.value.length === 0 && d.value.name === 'request' && d.writable && d.enumerable && d.configurable, 'request operation metadata');
});
await check(label + '/request/brand before conversion', async () => {
const request = w.WakeLock.prototype.request;
let conversions = 0, traps = 0;
const argument = {toString() { conversions++; throw Error('argument conversion'); }};
const trap = () => { traps++; throw Error('receiver hook'); };
const revoked = Proxy.revocable(lock, {}); revoked.revoke();
for (const receiver of [null, {}, w.WakeLock.prototype, Object.create(lock), new Proxy(lock, {get: trap, getPrototypeOf: trap}), revoked.proxy]) {
assert(await rejected(w, () => request.call(receiver, argument)) instanceof w.TypeError, 'reject forged and author proxy receivers');
}
assert(conversions === 0 && traps === 0, 'native receiver checks precede argument conversion');
});
await check(label + '/request/enum rejection', async () => {
for (const value of [null, '', 'system', 'SCREEN', 0, true, Symbol(), {}]) {
assert(await rejected(w, () => lock.request(value)) instanceof w.TypeError, 'invalid WakeLockType rejects Promise');
}
});
await check(label + '/request/conversion exceptions and order', async () => {
const sentinel = {}, order = [];
const argument = {
get [Symbol.toPrimitive]() { order.push('get primitive'); return () => { order.push('call primitive'); throw sentinel; }; },
toString() { throw Error('unexpected string fallback'); }
};
assert(await rejected(w, () => lock.request(argument)) === sentinel, 'preserve author conversion exception identity');
assert(order.join() === 'get primitive,call primitive', 'enum string conversion runs exactly once');
});
await check(label + '/request/valid default and platform outcome', async () => {
const request = w.WakeLock.prototype.request;
const promises = [request.call(lock), request.call(lock, undefined), request.call(lock, 'screen')];
for (let i = 0; i < promises.length; i++) {
const promise = promises[i];
assert(promise instanceof w.Promise && promises.filter(value => value === promise).length === 1, 'fresh Promise per request');
try {
const sentinel = await promise;
assert(sentinel instanceof w.WakeLockSentinel && sentinel instanceof w.EventTarget && sentinel.type === 'screen', 'native sentinel if platform permits request');
platformRequests.push({realm: label, call: i, result: 'acquired'});
await sentinel.release();
} catch (error) {
assert(error instanceof w.DOMException && error.name === 'NotAllowedError' && error.code === 0, 'platform denial uses NotAllowedError');
platformRequests.push({realm: label, call: i, result: 'denied', name: error.name, code: error.code});
}
}
});
await check(label + '/request/native identity independent of public prototype', async () => {
const request = w.WakeLock.prototype.request, prototype = Object.getPrototypeOf(lock);
Object.setPrototypeOf(lock, null);
try {
assert(await rejected(w, () => request.call(lock, 'invalid')) instanceof w.TypeError, 'native brand retained after prototype removal');
} finally { Object.setPrototypeOf(lock, prototype); }
});
const entries = [['released', false], ['type', false], ['onrelease', true]];
for (const [name, writable] of entries) {
await check(label + '/sentinel/' + name, () => {
const d = Object.getOwnPropertyDescriptor(w.WakeLockSentinel.prototype, name);
assert(d.enumerable && d.configurable && d.get.name === 'get ' + name && d.get.length === 0, 'shared sentinel getter metadata');
assert(writable ? d.set.name === 'set ' + name && d.set.length === 1 : d.set === undefined, 'sentinel attribute writability');
let conversions = 0, traps = 0;
const value = {toString() { conversions++; throw Error('unexpected conversion'); }};
const trap = () => { traps++; throw Error('receiver hook'); };
const fake = Object.create(w.WakeLockSentinel.prototype), revoked = Proxy.revocable(fake, {}); revoked.revoke();
for (const receiver of [null, {}, w.WakeLockSentinel.prototype, fake, new Proxy(fake, {get: trap, getPrototypeOf: trap}), revoked.proxy, lock]) {
assert(thrown(() => d.get.call(receiver)) instanceof w.TypeError, 'sentinel getter rejects unbranded receiver');
if (writable) assert(thrown(() => d.set.call(receiver, value)) instanceof w.TypeError, 'sentinel setter checks receiver first');
}
assert(conversions === 0 && traps === 0, 'sentinel validation ignores author hooks');
});
}
await check(label + '/sentinel/release', async () => {
const d = Object.getOwnPropertyDescriptor(w.WakeLockSentinel.prototype, 'release');
assert(d.value.name === 'release' && d.value.length === 0 && d.enumerable && d.configurable && d.writable, 'release operation metadata');
const fake = Object.create(w.WakeLockSentinel.prototype), revoked = Proxy.revocable(fake, {}); revoked.revoke();
for (const receiver of [null, {}, fake, new Proxy(fake, {}), revoked.proxy, lock]) {
assert(await rejected(w, () => d.value.call(receiver)) instanceof w.TypeError, 'release rejects Promise for illegal receiver');
}
});
await check(label + '/request/cross-realm callee Promise and errors', async () => {
const other = w === window ? document.getElementById('child').contentWindow : window;
const error = await rejected(w, () => w.WakeLock.prototype.request.call(other.navigator.wakeLock, 'invalid'));
assert(error instanceof w.TypeError && !(error instanceof other.TypeError), 'TypeError belongs to binding realm');
});
await check(label + '/request/immutable Promise intrinsic', async () => {
const original = w.Promise;
w.Promise = function() { throw Error('author Promise constructor'); };
let promise;
try { promise = w.WakeLock.prototype.request.call(lock, 'invalid'); }
finally { w.Promise = original; }
assert(promise instanceof original && await rejected(w, () => promise) instanceof w.TypeError, 'Promise wrapper ignores public constructor');
});
}
} finally { popup.close(); }
globalThis.__nodeReplacementResults = {rows, platformRequests, failures: rows.filter(row => !row.pass), passed: rows.filter(row => row.pass).length, total: rows.length};
return rows.every(row => row.pass);
})()
@@ -0,0 +1,30 @@
use super::*;
#[test]
fn wake_lock_interfaces_validate_receivers_and_secure_realm_exposure() {
for url in ["https://wake-lock.test/", "http://wake-lock.test/"] {
let mut vm = new_storage_page_task_executor_test_vm(url);
vm.eval("document.body.innerHTML = '<iframe id=child></iframe>'")
.unwrap();
vm.eval(&format!(
"({}).then(value => globalThis.__wakeLockDone = value, error => globalThis.__wakeLockDone = String(error));",
include_str!("wake_lock_interfaces.js")
))
.unwrap();
assert_eq!(
vm.eval_after_selected_page_tasks("JSON.stringify(__nodeReplacementResults.failures)")
.unwrap(),
"[]",
"{url}"
);
assert_eq!(vm.eval("__wakeLockDone").unwrap(), "true", "{url}");
if url.starts_with("https:") {
assert_eq!(
vm.eval("__nodeReplacementResults.platformRequests.length === 9 && __nodeReplacementResults.platformRequests.every(row => row.result === 'denied' && row.name === 'NotAllowedError' && row.code === 0)")
.unwrap(),
"true",
"No platform backend must not produce an acquired sentinel"
);
}
}
}
@@ -342,6 +342,8 @@ interfaces! {
MediaStreamTrack: EventTarget;
CanvasCaptureMediaStreamTrack: MediaStreamTrack;
MediaDevices: EventTarget;
WakeLock;
WakeLockSentinel: EventTarget;
MediaError;
MediaKeySession: EventTarget;
MediaKeyStatusMap;