feat(cdp): honor Browser.grantPermissions / setPermission / resetPermissions

These three Browser methods were noops, so a CDP client could not change
what navigator.permissions.query() reports - it always returned "prompt".

Store permission state on the active Page (keyed by permission name) and
have navigator.permissions.query() read it back. grantPermissions sets each
listed permission to "granted", setPermission sets a single permission to an
explicit state, and resetPermissions clears them (query falls back to
"prompt"). State is scoped to the Page and resets on navigation.

Adds a CDP round-trip test exercising all three methods.
This commit is contained in:
Rohit
2026-06-12 21:31:50 +05:30
committed by Karl Seguin
parent 56795e3c0b
commit 697ae06852
3 changed files with 98 additions and 8 deletions

View File

@@ -70,6 +70,12 @@ frame_arena: Allocator,
// lifetime.
origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
// Permission state set via CDP Browser.grantPermissions / setPermission /
// resetPermissions, keyed by permission name (e.g. "geolocation"). Read back
// by navigator.permissions.query(). Keys and values are duped into
// frame_arena, so they live for (and reset with) the Page.
permissions: std.StringHashMapUnmanaged([]const u8) = .empty,
// Identity tracking for the main world. All main-world contexts in this Page
// share this, ensuring object identity works across same-origin frames.
identity: js.Identity = .{},
@@ -187,6 +193,10 @@ pub fn deinit(self: *Page) void {
self.origins = .empty;
}
// Keys and values are duped into frame_arena, released just below; drop
// the map so we don't keep a dangling reference.
self.permissions = .empty;
session.arena_pool.release(self.frame_arena);
}
@@ -198,6 +208,16 @@ pub fn releaseArena(self: *Page, allocator: Allocator) void {
return self.session.releaseArena(allocator);
}
// Set (or overwrite) the stored state for a permission. Name and state are
// duped into frame_arena. Used by CDP Browser.grantPermissions / setPermission.
pub fn setPermission(self: *Page, name: []const u8, state: []const u8) !void {
const gop = try self.permissions.getOrPut(self.frame_arena, name);
if (!gop.found_existing) {
gop.key_ptr.* = try self.frame_arena.dupe(u8, name);
}
gop.value_ptr.* = try self.frame_arena.dupe(u8, state);
}
pub fn getOrCreateOrigin(self: *Page, key_: ?[]const u8) !*js.Origin {
const session = self.session;
const key = key_ orelse {

View File

@@ -37,15 +37,19 @@ _pad: bool = false,
const QueryDescriptor = struct {
name: []const u8,
};
// We always report 'prompt' (the default safe value — neither granted nor denied).
// Report the state set via CDP Browser.grantPermissions / setPermission, or
// 'prompt' (the default safe value — neither granted nor denied) when unset.
pub fn query(_: *const Permissions, qd: QueryDescriptor, exec: *const Execution) !js.Promise {
const arena = try exec.getArena(.tiny, "PermissionStatus");
errdefer exec.releaseArena(arena);
const state = exec.page.permissions.get(qd.name) orelse "prompt";
const status = try arena.create(PermissionStatus);
status.* = .{
._arena = arena,
._state = "prompt",
._state = try arena.dupe(u8, state),
._name = try arena.dupe(u8, qd.name),
};
return exec.js.local.?.resolvePromise(status);

View File

@@ -96,19 +96,50 @@ fn setWindowBounds(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
// Grant the listed permissions so navigator.permissions.query() reports them
// as "granted". State is stored on the active Page and resets on navigation.
fn grantPermissions(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
const params = (try cmd.params(struct {
permissions: []const []const u8,
origin: ?[]const u8 = null,
browserContextId: ?[]const u8 = null,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
for (params.permissions) |name| {
try page.setPermission(name, "granted");
}
return cmd.sendResult(null, .{ .include_session_id = false });
}
// TODO: noop method
// Set a single permission to an explicit state ("granted", "denied" or
// "prompt"), reflected by navigator.permissions.query().
fn setPermission(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
const params = (try cmd.params(struct {
permission: struct { name: []const u8 },
setting: []const u8,
origin: ?[]const u8 = null,
browserContextId: ?[]const u8 = null,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
try page.setPermission(params.permission.name, params.setting);
return cmd.sendResult(null, .{ .include_session_id = false });
}
// TODO: noop method
// Clear all granted permissions; navigator.permissions.query() falls back to
// the default "prompt".
fn resetPermissions(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
if (bc.session.currentPage()) |page| {
page.permissions.clearRetainingCapacity();
}
return cmd.sendResult(null, .{ .include_session_id = false });
}
const testing = @import("../testing.zig");
@@ -146,3 +177,38 @@ test "cdp.browser: getWindowForTarget" {
.bounds = .{ .windowState = "normal" },
}, .{ .id = 33, .index = 0, .session_id = null });
}
test "cdp.browser: grant/set/reset permissions reach navigator.permissions" {
var ctx = try testing.context();
defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{ .id = "BID-PERM", .url = "cdp/dom1.html" });
const page = bc.session.currentPage() orelse unreachable;
// grantPermissions: each listed permission becomes "granted".
try ctx.processMessage(.{
.id = 40,
.method = "Browser.grantPermissions",
.params = .{ .permissions = &[_][]const u8{ "geolocation", "notifications" } },
});
try ctx.expectSentResult(null, .{ .id = 40, .session_id = null });
try testing.expectEqualSlices(u8, "granted", page.permissions.get("geolocation").?);
try testing.expectEqualSlices(u8, "granted", page.permissions.get("notifications").?);
// setPermission: override a single permission to an explicit state.
try ctx.processMessage(.{
.id = 41,
.method = "Browser.setPermission",
.params = .{ .permission = .{ .name = "geolocation" }, .setting = "denied" },
});
try ctx.expectSentResult(null, .{ .id = 41, .session_id = null });
try testing.expectEqualSlices(u8, "denied", page.permissions.get("geolocation").?);
// resetPermissions: clears everything; query falls back to "prompt".
try ctx.processMessage(.{
.id = 42,
.method = "Browser.resetPermissions",
});
try ctx.expectSentResult(null, .{ .id = 42, .session_id = null });
try testing.expectEqual(@as(usize, 0), page.permissions.count());
}