Permissions and devices
editSome web APIs depend on the device or on the user: a browser asks the user for a permission, shows a share sheet, reads the GPS, or displays a notification. A browsing context has no user nor device, so these APIs call the callbacks of BrowsingContextOptions. Each callback has a default that works without a host, so the pages run as in a browser whose user declines everything. Set a callback to simulate the device in a test, or to connect the API to your application.
| Option | API | Without the option |
|---|---|---|
| Permissions, RequestPermission | navigator.permissions.query() and the permission prompts | Every permission is prompt, and the prompts are denied |
| Clipboard, ClipboardPermission | navigator.clipboard, copy, cut and paste | An in-memory clipboard, and every page may use it |
| Share | navigator.share() | There is no share target: the promise is rejected with an AbortError |
| Vibrate | navigator.vibrate() | Nothing vibrates |
| Geolocation | navigator.geolocation | The position is unavailable |
| ShowNotification, CloseNotification | new Notification() | The notifications are displayed nowhere |
| Badging | navigator.setAppBadge(), navigator.clearAppBadge() | The badge is shown nowhere |
| BatteryStatus | navigator.getBattery() | A fully charged battery that is plugged in |
| WakeLock | navigator.wakeLock.request() | Nothing is kept on, but the page gets its locks once the permission is granted |
| KeyboardLock | navigator.keyboard.lock(), navigator.keyboard.unlock() | No key is captured |
| NetworkInformation | navigator.connection | An unknown connection as fast as a 4G network |
| ClientHints | navigator.deviceMemory, navigator.connection.saveData and the Save-Data header | 8 GiB of memory, and no data saving |
| GlobalPrivacyControl | navigator.globalPrivacyControl and the Sec-GPC header | The user expresses no preference |
| ScreenOrientation | screen.orientation | A desktop screen, which cannot be locked |
| VirtualKeyboard | navigator.virtualKeyboard, the keyboard-inset-* environment variables | There is no on-screen keyboard |
| ViewportSegments | window.viewport.segments, the *-viewport-segments media features | The viewport has a single segment |
| ComputePressure | PressureObserver | The cpu source is supported, but reports no state |
| IdleDetection | IdleDetector | The user is active and the screen is unlocked |
| DevicePosture | navigator.devicePosture, the device-posture media feature | The posture is continuous |
| KeyboardMap | navigator.keyboard.getLayoutMap() | A US QWERTY keyboard |
| MediaSession | navigator.mediaSession | The media session is shown nowhere |
| Sensors | Accelerometer, Gyroscope, AmbientLightSensor and the other sensors, the deviceorientation and devicemotion events | The device has no sensor: the sensors fail to start, and the events report null values |
To remove an API completely, so that the pages see it as unsupported, disable it with Features: for instance new WebPlatformFeatures { GeolocationEnabled = false } removes navigator.geolocation. See WebPlatformFeatures for the list of features.
Permissions
A PermissionStore holds the permissions the user gave to the origins: navigator.permissions.query() reads it, and so do the APIs that need a permission (geolocation, notifications, screen-wake-lock). Grant or deny a permission in advance with SetState, for every origin or for one top-level origin.
When a page asks for a permission whose state is prompt, RequestPermission plays the role of the permission prompt: it gets the name of the permission and returns PermissionState.Granted or PermissionState.Denied. The answer is stored in the permission store, so the user is asked once per origin. Without the callback, the request is denied and nothing is stored, as when the user dismisses the prompt.
The insecure contexts (http: pages) and the frames that their permissions policy does not allow get denied, whatever the store says.
Share
navigator.share() calls Share with the data the page shares, once the library has checked it: the page needs a user activation (a click), and the URL is resolved against the URL of the document. Return true when the data is shared, and false when the user cancels:
var shared = new List<string>();
var options = new BrowsingContextOptions
{
// Plays the role of the share sheet of the operating system: true when the data is shared, false when the
// user cancels (the promise of navigator.share() is rejected with an AbortError)
Share = (callbackContext, request) =>
{
shared.Add($"{request.Title}|{request.Text}|{request.Url}|{request.Files.Count}");
return ValueTask.FromResult(true);
},
};The callback can be asynchronous: the promise of the page settles when the returned task completes.
Vibration
navigator.vibrate() calls Vibrate with the pattern of the page: the durations alternate between the vibrations and the pauses. An empty pattern stops the vibration, which also happens when the page becomes hidden. A page that the user has never activated cannot vibrate:
var patterns = new List<string>();
var options = new BrowsingContextOptions
{
// The durations alternate between the vibrations and the pauses; an empty pattern cancels the vibration
Vibrate = (callbackContext, pattern) => patterns.Add(string.Join(',', pattern.Select(duration => duration.TotalMilliseconds))),
};Geolocation
navigator.geolocation reads the position of a GeolocationProvider. Set its Position to simulate a location, and grant the geolocation permission:
var geolocation = new GeolocationProvider
{
// The position of the device; null when it is unavailable
Position = new GeolocationReading(latitude: 48.8584, longitude: 2.2945, accuracy: 10) { Altitude = 35 },
};
var permissions = new PermissionStore();
permissions.SetState("geolocation", PermissionState.Granted);
var options = new BrowsingContextOptions
{
Geolocation = geolocation,
Permissions = permissions,
};Changing Position updates the pages that watch the position (watchPosition()); set it to null to make the position unavailable. To read the position from another source, such as the operating system, derive from GeolocationProvider, override GetPositionAsync, and call OnPositionChanged when the device moves. The library handles the timeout and maximumAge options of the pages: the cancellation token passed to GetPositionAsync is canceled when the timeout elapses.
Notifications
Once the notifications permission is granted, ShowNotification displays the notifications the page creates. Keep the Notification to report what the user does: Click when the user activates it, and Dismiss when the user closes it. CloseNotification removes a notification that the page closed, or that a new notification with the same tag replaced:
var notifications = new List<Notification>();
var permissions = new PermissionStore();
permissions.SetState("notifications", PermissionState.Granted);
var options = new BrowsingContextOptions
{
Permissions = permissions,
// Display the notification; call notification.Click() or notification.Dismiss() when the user interacts with it
ShowNotification = (callbackContext, notification) => notifications.Add(notification),
CloseNotification = (callbackContext, notification) => notifications.Remove(notification),
};Without ShowNotification, the notifications are displayed nowhere, but the page sees them as shown. The frames that are not same origin with the page cannot show notifications, as in the browsers. The persistent notifications of the service workers (showNotification()) are not supported.
Badging
navigator.setAppBadge() and navigator.clearAppBadge() call SetBadge with the badge to show on the icon of the application: a number, a flag (setAppBadge() without argument), or nothing (clearAppBadge(), or the number 0). The pages and their workers can set it, but not the frames that are not same origin with the page:
var badges = new List<string>();
var options = new BrowsingContextOptions
{
Badging = new BadgingOptions
{
// Shows the badge on the icon of the application: a number, a flag (setAppBadge() without argument), or nothing
SetBadge = (callbackContext, badge) => badges.Add(badge.Kind switch
{
AppBadgeKind.Count => badge.Count.ToString(CultureInfo.InvariantCulture),
AppBadgeKind.Flag => "flag",
_ => "nothing",
}),
},
};Set RequiresNotificationsPermission to require the notifications permission, as Safari does.
Battery
navigator.getBattery() reads the state of a BatteryStatusProvider. Set its Status to simulate a battery:
var battery = new BatteryStatusProvider
{
// The battery of the device: setting the status updates the BatteryManager objects, which fire their change events
Status = new BatteryStatus { Charging = false, Level = 0.5, ChargingTime = null, DischargingTime = TimeSpan.FromHours(2) },
};
var options = new BrowsingContextOptions
{
BatteryStatus = new BatteryStatusOptions { Provider = battery },
};Changing Status updates the BatteryManager of the pages, which fire their chargingchange, chargingtimechange, dischargingtimechange and levelchange events. A hidden page gets the events when it becomes visible. The pages see the level rounded to two decimals, as in the browsers. To read the battery of the operating system, derive from BatteryStatusProvider, override GetStatus, and call OnStatusChanged when it changes. The frames that their permissions policy does not allow (battery) get a NotAllowedError.
Screen wake lock
navigator.wakeLock.request() keeps the screen on while the page is visible. Once the screen-wake-lock permission is granted, Acquire is called when the page gets its first lock, and Release when its last lock is released:
var screenOn = false;
var permissions = new PermissionStore();
permissions.SetState("screen-wake-lock", PermissionState.Granted);
var options = new BrowsingContextOptions
{
Permissions = permissions,
WakeLock = new WakeLockOptions
{
// Called when a document gets its first lock, and when its last lock is released
Acquire = (callbackContext, type) => screenOn = true,
Release = (callbackContext, type) => screenOn = false,
},
};The locks are released, and their release event is fired, when the page releases them, becomes hidden, or is unloaded. A hidden page cannot get a lock. Chromium grants the permission without asking the user: to do the same, grant it in the permission store or answer granted in RequestPermission.
Keyboard lock
navigator.keyboard.lock() asks to capture the keys that the system or the browser handle, such as Alt+Tab or Escape, while the page is fullscreen. Lock gets the code values of the keys (an empty list captures all the keys), and Unlock is called by navigator.keyboard.unlock() and when the page is unloaded:
IReadOnlyList<string>? reservedKeys = null;
var options = new BrowsingContextOptions
{
KeyboardLock = new KeyboardLockOptions
{
// The keys to capture while the document is fullscreen (empty: all the keys)
Lock = (callbackContext, keyCodes) => reservedKeys = keyCodes,
Unlock = callbackContext => reservedKeys = null,
},
};Only the page of a tab can lock the keyboard, not its frames. As in Chromium, a page whose keyboard is locked is not kept in the back/forward cache.
Network information
navigator.connection describes the network of the device: its type (wifi, cellular...), its effective type (4g, 3g, 2g or slow-2g), its bandwidth (downlink, in Mbit/s) and its round-trip time (rtt, in ms). Its values come from a NetworkInformationProvider. Set its Status to simulate a network:
var network = new NetworkInformationProvider
{
// The connection of the device: setting the status updates the NetworkInformation objects, which fire their change event
Status = new NetworkConnectionStatus
{
Type = ConnectionType.Wifi,
EffectiveType = EffectiveConnectionType.Cellular3G,
Downlink = 1.5,
DownlinkMax = 54,
RoundTripTime = TimeSpan.FromMilliseconds(320),
},
};
var options = new BrowsingContextOptions
{
NetworkInformation = new NetworkInformationOptions { Provider = network },
};Setting the status fires the change event in the pages and the workers that see a different value. As in Chromium, the pages see rounded values: the bandwidth is a multiple of 50 kbit/s and at most 10 Mbit/s, and the round-trip time is a multiple of 50 ms and at most 3 seconds. To read the network of the machine, derive from the provider, override GetStatus, and call OnStatusChanged when the network changes.
The provider also gives the values of the RTT, Downlink and ECT client hints that ClientHintsOptions does not set. Without provider, the connection has an unknown type, the performance of a 4G network (10 Mbit/s, 50 ms), and the network values of ClientHintsOptions when you set them.
navigator.connection.saveData is true when the user wants to reduce the data usage: SaveData also sends the Save-Data: on header with the requests to the secure origins.
Device memory
navigator.deviceMemory is the memory of the device in GiB, in the secure pages and workers. It reports DeviceMemory, rounded down to a power of two between 0.25 and 8 to make the users harder to fingerprint. The library does not read the memory of the machine: without value, the pages see 8, but the Sec-CH-Device-Memory client hint is not sent.
Global Privacy Control
navigator.globalPrivacyControl tells the pages that the user does not want their data sold or shared, and the requests then have the Sec-GPC: 1 header. Set Enabled for a fixed preference, or IsEnabled for a setting the user can change, which is read at each request:
var enabled = true;
var options = new BrowsingContextOptions
{
// The preference of the user, read at each request (the Sec-GPC header) and each read of navigator.globalPrivacyControl
GlobalPrivacyControl = new GlobalPrivacyControlOptions { IsEnabled = _ => enabled },
};Screen orientation
screen.orientation reads the screen of a ScreenOrientationProvider. By default, it simulates a screen that rotates to the orientation of each lock:
// The screen of a phone, held in portrait: lock() rotates it to the orientation of the lock
var screen = new ScreenOrientationProvider(OrientationType.PortraitPrimary);
var options = new BrowsingContextOptions
{
ScreenOrientation = new ScreenOrientationOptions { Provider = screen },
};As in the browsers, a page can lock the orientation only while it is fullscreen (NotAllowedError otherwise), and not from a frame sandboxed without allow-orientation-lock or from a hidden page (SecurityError). The lock completes after the screen has rotated, after the change event of the documents, from the page down to its frames. A lock request aborts the pending request of the other documents of the tab (AbortError), and the screen is unlocked when the page exits fullscreen or is unloaded. Set DeviceOrientation to simulate the user rotating the device. To lock the screen of the operating system, derive from ScreenOrientationProvider, override TryLock, and call OnOrientationChanged when the orientation changes. The size of the viewport does not change with the orientation.
Virtual keyboard
navigator.virtualKeyboard.show() and hide() call Show and Hide, when the page has a user activation and its focused element has virtualkeyboardpolicy="manual". The host reports the rectangle of its keyboard with the BoundingRect of a VirtualKeyboardProvider:
var keyboard = new VirtualKeyboardProvider();
var options = new BrowsingContextOptions
{
VirtualKeyboard = new VirtualKeyboardOptions
{
// The system shows its keyboard, then reports its rectangle (relative to the viewport)
Show = callbackContext => keyboard.BoundingRect = new VirtualKeyboardRect(0, 400, 800, 200),
Hide = callbackContext => keyboard.BoundingRect = default,
Provider = keyboard,
},
};Setting BoundingRect updates navigator.virtualKeyboard.boundingRect of the page, which fires its geometrychange event. While the page sets overlaysContent to true, the keyboard-inset-* environment variables give the rectangle of the keyboard. Only the page of a tab gets the geometry of the keyboard, not its frames. The library does not resize the viewport when the keyboard is shown.
Viewport segments
The display features of a foldable device, such as its fold or the hinge between two screens, split the viewport in segments. Set the DisplayFeatures of a DisplayFeaturesProvider to simulate them:
// A foldable device: a vertical hinge of 20 pixels splits the viewport in two segments, side by side
var displayFeatures = new DisplayFeaturesProvider();
var options = new BrowsingContextOptions
{
ViewportSegments = new ViewportSegmentsOptions { Provider = displayFeatures },
};
displayFeatures.DisplayFeatures = [new DisplayFeature(DisplayFeatureOrientation.Vertical, Offset: 390, MaskLength: 20)];The page then sees two segments in window.viewport.segments, the horizontal-viewport-segments and vertical-viewport-segments media features match the number of columns and rows of segments, and the viewport-segment-* environment variables give their edges (env(viewport-segment-width 1 0) is the width of the second column). As in Chromium, the frames have a single segment, and their viewport.segments is null.
Compute pressure
A PressureObserver reads the samples of a PressureSourceProvider. Call SetSample to report the pressure state of a source:
var pressure = new PressureSourceProvider();
pressure.SetSample(PressureSource.Cpu, new PressureSample(PressureState.Nominal));
var options = new BrowsingContextOptions
{
ComputePressure = new ComputePressureOptions { Provider = pressure },
};An observer is called when the state changes, or at its sampleInterval when it sets one, with the latest sample. It only gets the samples while the tab has the focus and the focused frame is same origin with its page (or the page that owns its worker). SetSupported makes a source unsupported: observe() is then rejected with a NotSupportedError. To read the pressure of the operating system, derive from PressureSourceProvider, override GetSample, and call OnSampleChanged when it changes. The frames that their permissions policy does not allow (compute-pressure) get a NotAllowedError. The rate obfuscation of the specification is not applied.
Idle detection
An IdleDetector reads the state of an IdleStateProvider. Its Status gives the time of the last interaction of the user, measured by the clock of the browsing context, and whether the screen is locked:
var clock = new FakeTimeProvider(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
var idleState = new IdleStateProvider
{
// The user interacted with the device now: they are idle once the threshold of a detector elapsed
Status = new IdleStatus { LastInteractionTime = clock.GetUtcNow() },
};
var permissions = new PermissionStore();
permissions.SetState("idle-detection", PermissionState.Granted);
var options = new BrowsingContextOptions
{
Permissions = permissions,
IdleDetection = new IdleDetectionOptions { Provider = idleState },
TimeProvider = clock,
};The user becomes idle for a detector once its threshold (one minute at least) elapsed since the last interaction. Changing Status updates the started detectors, which fire their change event. start() is rejected with a NotAllowedError when the idle-detection permission is denied, or when the permissions policy does not allow idle-detection. IdleDetector.requestPermission() needs a click of the user, and asks with RequestPermission.
Device posture
navigator.devicePosture and the device-posture media feature read the posture of a DevicePostureProvider:
var posture = new DevicePostureProvider();
var options = new BrowsingContextOptions
{
DevicePosture = new DevicePostureOptions { Provider = posture },
};
// Fires the change event of navigator.devicePosture, and updates the device-posture media feature
posture.Posture = DevicePostureType.Folded;Changing Posture fires the change event of navigator.devicePosture, and the change event of the media query lists such as matchMedia('(device-posture: folded)'). A hidden page gets the event when it becomes visible. All the frames of a tab have the posture of the tab.
Keyboard layout
navigator.keyboard.getLayoutMap() tells the page which character each key of the keyboard produces, for instance to display the shortcuts of a game. It reads the layout of a KeyboardLayoutProvider: the key value of each code value. Setting its Layout fires the layoutchange event of navigator.keyboard:
var keyboard = new KeyboardLayoutProvider
{
// The key produced by each key of the keyboard, by code: setting the layout fires the layoutchange event
Layout = new Dictionary<string, string>(KeyboardLayoutProvider.UsQwerty) { ["KeyQ"] = "a", ["KeyA"] = "q", ["Digit1"] = "&" },
};
var options = new BrowsingContextOptions
{
KeyboardMap = new KeyboardMapOptions { Provider = keyboard },
};Without provider, the keyboard is a US QWERTY keyboard (UsQwerty). Only the writing system keys (the letters, the digits and the punctuation) are given to the pages. The frames that their permissions policy does not allow (keyboard-map) get a SecurityError.
Media session
A page that plays media describes it with navigator.mediaSession: the title, the artist, the album and the artwork of the media, whether it plays, its position, and the actions that the media controls of the system can trigger (play, pause, next track...). SessionChanged gets the MediaSession of the page each time it changes:
string? nowPlaying = null;
MediaSession? session = null;
var options = new BrowsingContextOptions
{
MediaSession = new MediaSessionOptions
{
// Called when the page changes its metadata, its playback state, its position or its action handlers
SessionChanged = (callbackContext, mediaSession) =>
{
session = mediaSession;
nowPlaying = mediaSession.Metadata is { } metadata ? $"{metadata.Title} - {metadata.Artist} ({mediaSession.PlaybackState})" : null;
},
},
};Call HandleAction when the user presses a media control: the handler that the page registered with setActionHandler() runs from a task, and the method returns false when the page has no handler for the action. The URLs of the artwork are absolute; the library does not fetch the images.
Sensors
The sensor objects of the pages (Accelerometer, LinearAccelerationSensor, GravitySensor, Gyroscope, Magnetometer, UncalibratedMagnetometer, AmbientLightSensor, ProximitySensor, AbsoluteOrientationSensor and RelativeOrientationSensor) read the sensors of a DeviceSensorProvider. Add a sensor with AddSensor, then push its readings with Update:
var sensors = new DeviceSensorProvider();
// The accelerometer of the device: Update() makes a new reading available to the sensor objects of the documents
var accelerometer = sensors.AddSensor(DeviceSensorType.Accelerometer);
var permissions = new PermissionStore();
permissions.SetState("accelerometer", PermissionState.Granted);
var options = new BrowsingContextOptions
{
Sensors = new DeviceSensorsOptions { Provider = sensors },
Permissions = permissions,
};The readings are created with the factory method of the shape of the sensor: FromXyz for the motion sensors and the magnetometer, FromIlluminance, FromProximity, FromUncalibratedMagnetometer and FromEulerAngles for the orientation sensors. A provider can be shared by several browsing contexts, and Update can be called from any thread.
start() asks for the permissions of the sensor (accelerometer, gyroscope, magnetometer or ambient-light-sensor) and fires the error event with a NotAllowedError when one is denied, or with a NotReadableError when the device has no working sensor of the type. Chromium grants these permissions without asking the user: to do the same, grant them in the permission store. The constructors throw a SecurityError in the frames that their permissions policy does not allow.
The pages get the readings as in a browser:
- The
readingevent is fired at most at the frequency the page asked for (5 Hz by default), clamped to the bounds of the sensor (1 Hz to 60 Hz by default). RequestedSamplingFrequency is the highest frequency the started sensors ask for. - The values are rounded to limit the fingerprinting: the accelerations to 0.1 m/s², the angular velocities to 0.1 deg/s (as Chromium), and the illuminance to 50 lux, whose changes of less than 25 lux are discarded.
- A hidden page, or a frame that is not same origin with the focused frame, gets no reading: the latest one is delivered when it becomes visible or focused.
Device orientation and motion
The deviceorientation, deviceorientationabsolute and devicemotion events read the same provider: the relative orientation sensor (or the absolute one when the device has no relative orientation sensor), the absolute orientation sensor, and the linear acceleration sensor, the accelerometer and the gyroscope:
var sensors = new DeviceSensorProvider();
// The deviceorientation event reads the relative orientation sensor, as Euler angles in degrees
var orientation = sensors.AddSensor(DeviceSensorType.RelativeOrientation);As in Chromium, the first event is fired when the page adds a listener, then the orientation events are fired when the orientation changes and the motion events every 16 ms while the sensors get new readings. The angles are rounded to 0.1 degree, the accelerations to 0.1 m/s² and the rotation rates to 0.1 deg/s. Without a working sensor, or without the accelerometer and gyroscope permissions, the first event is fired with null values. A hidden page gets no event. DeviceOrientationEvent.requestPermission() and DeviceMotionEvent.requestPermission() resolve with the state of the permissions, and need a user activation when a permission would prompt the user.