Permissions and consent
The FKN browser extension asks the user before your app reads or acts on an embedded site. Every read and every action maps to one permission key, an entry such as read.text or act.click. Anything above severity 0 goes on the consent sheet first, and the extension remembers the answer. This page covers the sheet, the reason you attach to a call, area grants, asking for several keys at once, the keys themselves, the activity log, the card the cloud backend draws instead, and when to ask.
Only the extension backend shows a sheet, see backends, so pin it and catch the refusal:
try { await const frame: extension.Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('#play').click: (options?: PositionOptions | undefined) => Promise<void>
click({ reason?: string | undefined
reason: 'Start playback from this app' })} catch (var error: unknown
error) { const { const name: string
name, const message: string
message } = var error: unknown
error as interface Error
Error const name: string
name // 'PermissionDeniedError', when the user refused const message: string
message // 'Permission denied: act.click (#play)'}The key names the action, act.click. The scope names where. A locator chain is the Locator you built by chaining selectors, and the scope is that chain rendered as text, #play here, see the chain.
The extension builds the error by assigning the name PermissionDeniedError to a plain Error rather than as a class, so test error.name. Neither isLocatorDenied nor isTerminalError matches it, and its permissionKey and scope fields do not survive the hop out of the extension, see a refusal neither guard matches.
The sheet
Section titled “The sheet”When a call needs a permission the user has not granted, the extension draws the consent sheet over the page. The sheet is headed <app host> is asking to… and shows one row per request: the permission’s title and description, the scope, and your reason. The user allows or denies each row and chooses how long the extension remembers the answer:
- Once holds for the rest of this page load and is never stored
- Session is stored for the rest of the browser session
- Always is stored until the user revokes it
Every row starts at Allow and Session. The extension remembers a denial the same way it remembers a grant. A plain Deny is therefore stored for the session, and every retry rejects with no sheet until the user revokes it. Only Once stops at the page load, and a dismissal does not even do that.
A dismissal holds for nothing: the extension logs it as a denial remembered once, and the next call asks again. Closing the sheet counts as a dismissal, and so does covering it for three checks in a row.
With more than one row the sheet offers Remember for all and Deny all, and it groups six rows or more by category. A row raised by a locator call carries a Hold to highlight button that outlines the element on the page, and a row from permissions.request() has none. A subtree scope is marked Whole area, an unsafe key Sensitive, and the apply button stays disabled, reading Reading…, for the first 500 ms.
A grant is what a later call finds instead of a sheet:
const const title: Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
title = const frame: extension.Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('#title')await const title: Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
title.textContent: (_options?: LocatorOptions | undefined) => Promise<string>
textContent({ reason?: string | undefined
reason: 'Show the title in this app' }) // the sheet, one timeawait const title: Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
title.textContent: (_options?: LocatorOptions | undefined) => Promise<string>
textContent() // no sheet, answered by the grant the first call storedThe second read is answered by the grant the first one stored. The extension consults the stored grants first, on every call, so a revoke from its toolbar popup or dashboard takes effect on the next call.
Giving a reason
Section titled “Giving a reason”reason is a sentence for the user, shown on the row under the label App’s reason. Write it as the outcome they get rather than the operation you run:
await const frame: extension.Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('#search').fill: (value: string, options?: LocatorOptions | undefined) => Promise<void>
fill('big buck bunny', { reason?: string | undefined
reason: 'Search the catalog for you' }) // a sheet for act.type, then the fillThe sheet shows that sentence under the row for act.type. Every locator action takes a reason, and so do permissions.request(), frame.fetch(), extension.fetch() and extension.setRequestHeaderRule(), see fetch(). extension.cookies.get() is the only call that can put a row on the sheet without one: its row shows the permission’s own words and the origin, and no App’s reason line. attachFrame() and goto() take no reason either, because their severity-0 row is never shown.
Asking before acting
Section titled “Asking before acting”ensure(operation, options?) on a chain runs the consent check for operation and touches no element. With subtree: true the scope widens from the chain to everything under it, and that wider grant is an area grant. The demo on locators and actions does this for a player’s controls:
try { // one sheet, marked "Whole area": act.click on '.controls *' await const frame: extension.Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('.controls').ensure: (operation: "click" | "fill" | "hover" | "textContent" | "getAttribute" | "isVisible" | "count" | "exists" | "videoElement", options?: EnsureOptions) => Promise<void>
ensure('click', { subtree: boolean
subtree: true, reason: string
reason: 'Control the player from this app' })
// covered by the area grant: no sheet, one 'covered' activity row per scope per page load await const frame: extension.Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('.controls').locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('#play').click: (options?: PositionOptions | undefined) => Promise<void>
click() await const frame: extension.Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('.controls').locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('#mute').click: (options?: PositionOptions | undefined) => Promise<void>
click()
await const frame: extension.Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('#outside').click: (options?: PositionOptions | undefined) => Promise<void>
click() // outside the area, so the sheet again} catch (var error: unknown
error) { const { const name: string
name, const message: string
message } = var error: unknown
error as interface Error
Error if (const name: string
name !== 'PermissionDeniedError') throw var error: unknown
error const message: string
message // 'Permission denied: act.click (.controls *)', when the area was refused}One sheet, marked Whole area, covers the two clicks inside .controls, and the click outside the box asks again. The extension stores a grant under its exact scope. A call is covered when its own scope matches, or when a wider scope that contains it does: the chain’s own subtree scope, then the subtree scope of each shorter chain, longest first, and finally *, the whole page.
Several at once
Section titled “Several at once”permissions.request(requests) puts several keys on one sheet and answers them together, before any frame call. The guided demo on frames asks for a read, a fill and a click, then runs all three under that one answer:
import { const permissions: { request: (requests: PermissionRequest[]) => Promise<extension.PermissionGrant[]>;}
permissions } from '@fkn/lib'import type { type PermissionRequest = { key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"; scope?: string; reason?: string;}
PermissionRequest } from '@fkn/lib'
// '*' covers every later chain in the frameconst const requests: extension.PermissionRequest[]
requests: type PermissionRequest = { key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"; scope?: string; reason?: string;}
PermissionRequest[] = [ { key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"
key: 'read.text', scope?: string | undefined
scope: '*', reason?: string | undefined
reason: 'Read the page heading' }, { key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"
key: 'act.type', scope?: string | undefined
scope: '*', reason?: string | undefined
reason: 'Type the search query for you' }, { key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"
key: 'act.click', scope?: string | undefined
scope: '*', reason?: string | undefined
reason: 'Press Search' },]const const grants: extension.PermissionGrant[]
grants = await const permissions: { request: (requests: PermissionRequest[]) => Promise<extension.PermissionGrant[]>;}
permissions.request: (requests: PermissionRequest[]) => Promise<extension.PermissionGrant[]>
request(const requests: extension.PermissionRequest[]
requests) // one sheet with three rowsconst grants: extension.PermissionGrant[]
grants.Array<PermissionGrant>.map<boolean>(callbackfn: (value: extension.PermissionGrant, index: number, array: extension.PermissionGrant[]) => boolean, thisArg?: any): boolean[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
map(grant: extension.PermissionGrant
grant => grant: extension.PermissionGrant
grant.allow: boolean
allow) // [true, true, false] if the click was refused
if (const grants: extension.PermissionGrant[]
grants.Array<PermissionGrant>.every(predicate: (value: extension.PermissionGrant, index: number, array: extension.PermissionGrant[]) => unknown, thisArg?: any): boolean (+1 overload)
Determines whether all the members of an array satisfy the specified test.
every(grant: extension.PermissionGrant
grant => grant: extension.PermissionGrant
grant.allow: boolean
allow)) { await const frame: extension.Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('#search').fill: (value: string, options?: LocatorOptions | undefined) => Promise<void>
fill('big buck bunny') // covered, no further sheet await const frame: extension.Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('button[type="submit"]').click: (options?: PositionOptions | undefined) => Promise<void>
click()}The answer comes back in request order as { key, scope, allow }, and a refused row is allow: false rather than a rejection. A pair already decided at that exact key and scope is answered from the store without a sheet, and only that pair. The call sends no covering scopes, so an earlier * grant does not answer a narrower request. Reuse the same scope string when you ask again.
Without the extension the call runs the missing-extension handler after the exposure wait and then rejects with The FKN WebExtension is not installed, enabled or not exposed on this page., see when the extension is missing.
The scope has to be one a later call will render to, or the grant covers nothing:
- the locator keys: the rendered chain,
.controls #play, a subtree scope,.controls *, or* frame.fetchReadandframe.fetchWrite: the chain, orthis frameat the rootnetwork.fetch,network.fetchCredentialed,network.fetchLocalandnetwork.readCookie: the target origin,https://example.orgnetwork.modifyRequestHeaders: the rule’s deduplicateddomains, joined by,
Permission keys
Section titled “Permission keys”Twenty keys exist, and the table says what raises each:
| Key | Severity | Raised by | Title on the sheet |
|---|---|---|---|
read.text | 1 | textContent() | Read text from the page |
read.info | 1 | getAttribute() | Read info from the page |
read.visible | 0 | isVisible() | See what’s visible on the page |
read.check | 0 | exists() | Check if something is on the page |
read.count | 0 | count() | Count items on the page |
act.click | 3 | click() | Click on the page |
act.type | 1 | fill() | Type into a text field |
act.hover | 1 | hover() | Hover over the page |
media.video | 0 | videoElement() | Control video playback |
media.appear | 0 | addStyleTag() | Change page appearance |
media.appearU | 2 | addStyleTag({ noSanitize: true }) | Change page appearance (unsafe) |
embed.iframe | 0 | attachFrame() | Load another website inside this app |
embed.open | 0 | goto() | Open a website inside this app |
frame.fetchRead | 3 | frame.fetch() with GET, HEAD, OPTIONS | Read your account data on this site |
frame.fetchWrite | 3 | frame.fetch() with another method | Change your account data on this site |
network.fetch | 0 | extension.fetch() without credentials | Fetch data from other sites |
network.fetchCredentialed | 3 | extension.fetch() with credentials: 'include' | Fetch from other sites, signed in as you |
network.fetchLocal | 3 | extension.fetch() to a local network address | Access devices on your local network |
network.readCookie | 3 | extension.cookies.get() | Read a site’s cookie |
network.modifyRequestHeaders | 2 | extension.setRequestHeaderRule() | Rewrite request headers to other sites |
Severity runs 0 None, 1 Low, 2 Medium, 3 High, with a 4 declared that no key uses, see severity. The extension grants severity 0 silently and never stores it. It logs one auto row per origin, key and scope per document in the activity log, the on-device record of what an app did. The root fetch() raises network.fetchLocal for a local address, then network.fetchCredentialed or network.fetch, whenever the extension is exposed, see how the root fetch decides.
The seven keys flagged unsafe are marked Sensitive on the sheet: media.appearU, frame.fetchRead, frame.fetchWrite and every network.* key except network.fetch.
@fkn/lib exports seven descriptors, one per key raised outside a locator chain: attachFramePermission, gotoPermission, fetchPermission, fetchCredentialedPermission, fetchLocalPermission, readCookiePermission and modifyRequestHeadersPermission. There is no exported descriptor for the thirteen locator keys, so do not look for a clickPermission. Each descriptor carries the title and description the sheet shows, so your app can say the same words before it asks:
import { const fetchCredentialedPermission: { readonly scope: "network.fetchCredentialed"; readonly category: "network"; readonly severity: 3; readonly unsafe: true; readonly title: "Fetch from other sites, signed in as you"; readonly description: "Lets the app fetch other sites using your logged-in session cookies, so it can read data only you should see.";}
fetchCredentialedPermission, const permissions: { request: (requests: PermissionRequest[]) => Promise<PermissionGrant[]>;}
permissions } from '@fkn/lib'
var document: Document
window.document returns a reference to the document contained in the window.
document.ParentNode.querySelector<Element>(selectors: string): Element | null (+4 overloads)
Returns the first element that is a descendant of node that matches selectors.
querySelector('#notice')!.Element.textContent: string | null
textContent = const fetchCredentialedPermission: { readonly scope: "network.fetchCredentialed"; readonly category: "network"; readonly severity: 3; readonly unsafe: true; readonly title: "Fetch from other sites, signed in as you"; readonly description: "Lets the app fetch other sites using your logged-in session cookies, so it can read data only you should see.";}
fetchCredentialedPermission.description: "Lets the app fetch other sites using your logged-in session cookies, so it can read data only you should see."
description // the sheet's own words, before the sheet
const [const grant: PermissionGrant
grant] = await const permissions: { request: (requests: PermissionRequest[]) => Promise<PermissionGrant[]>;}
permissions.request: (requests: PermissionRequest[]) => Promise<PermissionGrant[]>
request([{ key: "network.fetchCredentialed" | "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"
key: const fetchCredentialedPermission: { readonly scope: "network.fetchCredentialed"; readonly category: "network"; readonly severity: 3; readonly unsafe: true; readonly title: "Fetch from other sites, signed in as you"; readonly description: "Lets the app fetch other sites using your logged-in session cookies, so it can read data only you should see.";}
fetchCredentialedPermission.scope: "network.fetchCredentialed"
scope, scope?: string | undefined
scope: 'https://example.org', reason?: string | undefined
reason: 'Load your watch history' }])const grant: PermissionGrant
grant.allow: boolean
allow // true once the user allowed itThe notice carries the description the sheet is about to show, and the request uses the descriptor’s scope as its key. The exact shape of permissions and its two types lives under @fkn/lib/extension.
The activity log
Section titled “The activity log”The extension writes a row to its toolbar popup and dashboard when the sheet is answered or dismissed, when it grants a severity-0 call silently and when a wider stored grant covers a call. A row carries the origin, the key, the scope, the outcome (allowed, denied, auto or covered) and the answer’s remember mode, once, session or always. A call answered by an exact stored grant writes no row, and the auto and covered rows are written once per origin, key and scope per document. frame.fetch() adds one receipt row per call with the method, the path and the status, whatever answered the permission:
const const result: FrameFetchResult
result = await const frame: extension.Frame
frame.fetch: (url: string, init?: FrameFetchOptions | undefined) => Promise<FrameFetchResult>
fetch('https://example.org/api/catalog.json', { reason?: string | undefined
reason: 'Load your catalog' }) // a sheet for frame.fetchRead, then one receipt rowconst result: FrameFetchResult
result.status: number
status // whatever example.org answered, which the receipt recordsThe receipt lands before the result reaches you. A call whose receipt cannot be written has its result withheld, with frame.fetch: the call completed but its audit receipt could not be recorded; result withheld. A failing call writes one receipt per identical call per 60,000 ms rather than one per retry.
The extension deletes rows older than 30 days on every write, and a read returns the newest 500. Nothing in @fkn/lib reads the log. It is the user’s record of what your app was allowed to do, so point them at the extension’s popup when they ask what was allowed.
Consent on the cloud backend
Section titled “Consent on the cloud backend”The cloud backend, the render proxy, has no extension. Nothing there draws a sheet, stores an answer or writes a log. Every locator action runs there without a prompt, see two backends. The one gated operation is frame.fetch(), because it spends the frame’s session, and the broker, the connection your app holds into FKN, draws a card for it.
Before the card, @fkn/lib checks the attachment rules, and each one refuses as a terminal LocatorDeniedError, see fetching as the frame. Past the card the request is the render proxy’s to serve. The refusals it can answer with have their rows on every error. What passes the rules reaches the card:
try { // the broker's card const const result: FrameFetchResult
result = await const frame: Frame
frame.fetch: (url: string, init?: FrameFetchOptions | undefined) => Promise<FrameFetchResult>
fetch('https://example.org/api/catalog.json') const result: FrameFetchResult
result.status: number
status // reached only when the render proxy served the call} catch (var error: unknown
error) { const { const message: string
message } = var error: unknown
error as interface Error
Error if (function isLocatorDenied(error: unknown): boolean
isLocatorDenied(var error: unknown
error)) const message: string
message // 'frame.fetch: the user did not grant this', refused at the card else if (function isTerminalError(error: unknown): boolean
isTerminalError(var error: unknown
error)) const message: string
message // the render proxy's own refusal else throw var error: unknown
error}The card names your app’s host, the declared hosts and the permission’s title and description. The list is cut to 16 hosts, and it is the target host when the attachment declared none. The card offers Allow for this session or Not now, and your reason is never shown.
The card offers one duration, the life of the tab: the broker keeps a grant in its own sessionStorage, per app, scope and host. A denial is not stored. Not now and closing the card both start a 10,000 ms cooldown, during which the call fails closed with frame.fetch: the user did not grant this and no card is drawn.
Asking well
Section titled “Asking well”Ask when the user understands why. We recommend running ensure() or permissions.request() from the click that turns a feature on, with a reason that names what they get. A sheet on page load reads as noise and gets denied:
const enableControls: HTMLButtonElement
enableControls.HTMLButtonElement.addEventListener<"click">(type: "click", listener: (this: HTMLButtonElement, ev: PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
addEventListener('click', async () => { try { await const frame: extension.Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('.controls').ensure: (operation: "click" | "fill" | "hover" | "textContent" | "getAttribute" | "isVisible" | "count" | "exists" | "videoElement", options?: EnsureOptions) => Promise<void>
ensure('click', { subtree: boolean
subtree: true, reason: string
reason: 'Control the player from this app' }) // the sheet, from the user's own click } catch (function (local var) error: unknown
error) { if ((function (local var) error: unknown
error as interface Error
Error).Error.name: string
name !== 'PermissionDeniedError') throw function (local var) error: unknown
error const enableControls: HTMLButtonElement
enableControls.Element.textContent: string | null
textContent = 'Player controls stay off' // the answer stands until the user changes it }})The sheet appears from the user’s own click, and a refusal changes the button rather than asking again. Denials are normal, and a refusal is not retried. A plain Deny is stored for the session, so catch it, tell the user what will not happen, and move on.
Do not pre-ask for severity 0. exists(), count(), isVisible(), videoElement(), addStyleTag() without noSanitize, attachFrame(), goto() and extension.fetch() without credentials to a public address never raise a sheet. The exception is a chain whose CSS probes the value attribute, which a severity-0 action refuses with count: selectors matching the value attribute need a consent-gated operation, see reading versus acting.
Do not cover the sheet. A prompt your own UI hides is dismissed as a denial remembered once, so the call fails and the next one asks again.
Errors you might see
Section titled “Errors you might see”The four you will meet most:
| Message | What happened |
|---|---|
Permission denied: <key> (<scope>) | The user refused, dismissed the sheet, or a stored deny covers the scope. Not retried. |
The FKN WebExtension is not installed, enabled or not exposed on this page. | No extension answered, and the install card produced none. |
permission rpc: the background answered with an unknown shape | The background’s reply had a shape the page side could not read. Retry, then reload the extension. |
| the text of the background’s own failure | The message of whatever rejected under the background. Read the text. |
Every other message has its row on every error. The rules for matching one are on handling errors.