Skip to content

Permissions and consent

The FKN extension checks each capability per app and scope. Severity-0 capabilities are granted automatically. Sensitive capabilities show a consent prompt unless an existing exact or area grant covers the request. Stored grants stay on the user’s device and can be revoked from the extension dashboard.

The 30-day on-device activity log records permission decisions. It also records the first automatic or area-covered use of each capability and exact scope during a visit. Repeated calls covered by the same decision are not logged as separate entries.

As an app developer you don’t implement any of this. You call the API; the extension prompts. Your job is to ask well.

The first time a sensitive action needs an ungranted capability, the extension shows a consent sheet describing the capability, requesting app, and exact scope. The user chooses allow once, allow for this session, always allow, or deny. A denial rejects the call with an error; handle it as a normal outcome.

Pass a reason to tell the user why you are asking:

await
const frame: Frame
frame
.
locator: (selector: string) => Locator
locator
('.controls')
.
getByText: (text: string) => Locator
getByText
('Play')
.
click: (options?: (OperationTimeoutOptions & {
position?: {
x?: number;
y?: number;
};
}) | undefined) => Promise<void>
click
({
reason?: string | undefined
reason
: 'Start playback when you press play in this app' })

ensure(operation, options?) resolves the permission an operation needs without running it. Use it to request a sensitive grant at a natural moment, such as player mount, instead of interrupting a later interaction. Severity-0 operations resolve automatically.

await
const frame: Frame
frame
.
locator: (selector: string) => Locator
locator
('.controls').
ensure: (operation: "click" | "fill" | "hover" | "textContent" | "isVisible" | "count" | "exists" | "getAttribute" | "videoElement", options?: {
reason?: string;
timeout?: number;
subtree?: boolean;
} & Record<string, unknown>) => Promise<void>
ensure
('click')

ensure(operation, { subtree: true }) asks for the capability on an element and everything inside it. One prompt covers all descendant locators; the prompt marks the request with a “Whole area” badge so the user knows it is broad. On the frame root it becomes the whole-page wildcard.

// One prompt: "click, everything inside .controls"
await
const frame: Frame
frame
.
locator: (selector: string) => Locator
locator
('.controls').
ensure: (operation: "click" | "fill" | "hover" | "textContent" | "isVisible" | "count" | "exists" | "getAttribute" | "videoElement", options?: {
reason?: string;
timeout?: number;
subtree?: boolean;
} & Record<string, unknown>) => Promise<void>
ensure
('click', {
subtree?: boolean | undefined
subtree
: true })
// Covered: no further prompts; the first covered use of each scope is recorded.
await
const frame: Frame
frame
.
locator: (selector: string) => Locator
locator
('.controls').
locator: (selector: string) => Locator
locator
('#play').
click: (options?: (OperationTimeoutOptions & {
position?: {
x?: number;
y?: number;
};
}) | undefined) => Promise<void>
click
()
await
const frame: Frame
frame
.
locator: (selector: string) => Locator
locator
('.controls').
locator: (selector: string) => Locator
locator
('#mute').
click: (options?: (OperationTimeoutOptions & {
position?: {
x?: number;
y?: number;
};
}) | undefined) => Promise<void>
click
()

The first covered use of each exact capability and scope during a visit appears in the activity log, attributed to the area grant. An exact grant or denial on a deeper element always takes precedence over a covering one.

permissions.request() asks for several capabilities up front in a single consent sheet. Anything already decided resolves without showing a sheet.

import {
const permissions: {
request: (requests: PermissionRequest[]) => Promise<PermissionGrant[]>;
}
permissions
} from '@fkn/lib'
const
const grants: PermissionGrant[]
grants
= await
const permissions: {
request: (requests: PermissionRequest[]) => Promise<PermissionGrant[]>;
}
permissions
.
request: (requests: PermissionRequest[]) => Promise<PermissionGrant[]>
request
([
{
key: "act.click" | "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "embed.iframe" | "embed.open" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"
key
: 'act.click',
scope?: string | undefined
scope
: '.controls *',
reason?: string | undefined
reason
: 'Control the player' },
{
key: "act.click" | "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "embed.iframe" | "embed.open" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"
key
: 'read.text',
scope?: string | undefined
scope
: '.title',
reason?: string | undefined
reason
: 'Show the current title' },
])
for (const
const grant: PermissionGrant
grant
of
const grants: PermissionGrant[]
grants
) {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const grant: PermissionGrant
grant
.
key: "act.click" | "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "embed.iframe" | "embed.open" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"
key
,
const grant: PermissionGrant
grant
.
scope: string
scope
,
const grant: PermissionGrant
grant
.
allow: boolean
allow
)
}

permissions.request() accepts these current keys. The scope is usually a locator selector for frame operations or a target host for network operations.

KeysUsed forPrompt behavior
read.text, read.infoRead text or an attributePrompts when not granted
read.visible, read.check, read.countCheck visibility, existence, or countAutomatic
act.click, act.type, act.hoverClick, fill, or hoverPrompts when not granted
media.video, media.appearControl video or add filtered CSSAutomatic
media.appearUAdd CSS without filteringPrompts when not granted
embed.iframe, embed.openAttach or navigate a frameAutomatic
network.fetchCookieless cross-origin fetchAutomatic
network.fetchCredentialed, network.fetchLocalFetch with browser cookies or reach a local-network targetPrompts when not granted
network.readCookieRead one named cookiePrompts when not granted
network.modifyRequestHeadersApply a tab-scoped request-header rulePrompts when not granted

Capabilities carry a severity from 0 to 4. Severity 0 is granted automatically and recorded once per capability and scope during a visit. Higher severities prompt unless a stored, session, once, or area grant already decides the request. The catalog above is the practical source for which operations prompt.

  • Ask at a moment the user understands, and use reason so the prompt reads as part of your flow.
  • Prefer one area grant over a dozen per-control prompts for dense UI like players.
  • Expect denials. A rejected promise is an answer, not an error state to retry in a loop.