Skip to content

@fkn/lib/extension

Thrown when the extension is current but does not serve the operation being asked for. message replaces the default advice where updating the extension cannot help.

  • Error
new ExtensionOperationUnsupportedError(
operation,
abi,
message?): ExtensionOperationUnsupportedError;

string

number

string

ExtensionOperationUnsupportedError

Error.constructor
readonly abi: number;
readonly name: "ExtensionOperationUnsupportedError" = "ExtensionOperationUnsupportedError";
Error.name
readonly operation: string;

Thrown instead of the generic “not installed” message when the extension is installed but too old. Carries both numbers so an app can say which and link its listing. Match it by name, which survives a structured-clone hop.

  • Error
new ExtensionOutdatedError(abi, required): ExtensionOutdatedError;

number

number

ExtensionOutdatedError

Error.constructor
readonly abi: number;
readonly name: "ExtensionOutdatedError" = "ExtensionOutdatedError";
Error.name
readonly required: number;

window.open returned null, so nothing was opened: the call ran without user activation, the popup blocker refused it, or the calling frame is sandboxed without allow-popups.

  • Error
new FrameWindowBlockedError(): FrameWindowBlockedError;

FrameWindowBlockedError

Error.constructor
readonly name: "FrameWindowBlockedError" = "FrameWindowBlockedError";
Error.name

The window refused to attach, by reason, after loading nothing.

  • Error
new FrameWindowRefusedError(reason): FrameWindowRefusedError;

reason is what the window said; anything this library does not know becomes 'unknown'.

unknown

FrameWindowRefusedError

Error.constructor
readonly name: "FrameWindowRefusedError" = "FrameWindowRefusedError";
Error.name
readonly reason: FrameWindowRefusal;

A call that ran out of time: its deadline passed before it settled. One class for every call that has a deadline, as Playwright’s TimeoutError, named 'TimeoutError' so a check by name works as well as instanceof. It is minted where the call was made, never in the realm that ran it, so it is an instance of this class in the caller’s realm.

message is what the call reported at its deadline, unchanged from the error it replaces. cause is the last retryable error an attempt met before the deadline, so a locator call that never found its element says why; it is absent when no attempt failed (one never settled, or the call is not retried). Not terminal: nothing retries a call past its own deadline.

  • Error
new TimeoutError(message, options?): TimeoutError;

string

unknown

TimeoutError

Error.constructor
readonly name: "TimeoutError" = "TimeoutError";
Error.name
type AddScriptTagOptions = object;

frame.addScriptTag’s options, Playwright’s content and FKN’s sourceUrl.

content: string;

The script’s source text.

optional sourceUrl?: string;

Names the script in stack traces through //# sourceURL. Absent, its frames name the document.


type AttachCookies = "persistent" | "ephemeral" | "native";

Which cookie jar an attachment runs on, and with it which backend serves it. One attachment, one jar: no value unions two jars, so which cookie a request carries always has one answer. HttpOnly values never reach app code on any value.

  • 'persistent', the default: the cloud render proxy’s jar of the app’s top-level site (https://fkn.app for every fkn.app app), which every app of that site shares, the same jar its inline frames and its windows use, kept across visits on this device, signed in to FKN or not. It is written sealed under a key the render proxy generates for that site’s jar and keeps beside it on the device, so no cookie of it is on disk in the clear, while a copy of the device’s files holds what opens it. Where an earlier build kept that jar in the clear, Chrome can keep the old text in its files for a time after the seal. Always the cloud backend, whatever is installed, so frame.backend() answers 'cloud'. Its requests leave through WebVPN, which keeps the cookies their answers set.
  • 'ephemeral': a fresh jar of the attachment’s own, with site storage under a namespace of its own, both gone with it, seeded from storageState when one is given. Always the cloud backend: the browser keeps a partition’s cookies after its frame goes, so no extension jar ends with the attachment. Its requests leave through WebVPN.
  • 'native': the person’s own browser cookies for domains and every goto’s host, copied into the browser’s partition for the app’s top-level site on the attach and on every goto. Extension backend only, so frame.backend() answers 'extension'. Without the extension the top-level attachFrame shows the install prompt, as every extension-only call does; cloud.attachFrame refuses it.

Refused by name before anything is attached, opened, waited for or dispatched, in this order:

  • any syncCookies key, true and false alike: TypeError attachFrame: syncCookies was replaced by cookies. true is cookies: 'persistent', or 'native' for the person's own browser cookies on the extension; false is cookies: 'ephemeral'
  • an unknown value: TypeError attachFrame: cookies must be 'persistent', 'ephemeral' or 'native', not "<value>"
  • a storageState beside anything but 'ephemeral', the default included: TypeError attachFrame: storageState seeds a fresh jar, so it needs cookies: 'ephemeral'
  • a malformed storageState: TypeError attachFrame: storageState.<path> <what is wrong>, for the first field of another shape in the order the state lists them (StorageState)
  • a cookie whose expires is above 0 and below 100000000000: TypeError attachFrame: storageState.cookies[<i>].expires is milliseconds since the epoch, and <value> reads as a date before 1974; a Playwright storageState gives seconds, so multiply it by 1000
  • then the shape of the attachment: iframe and window at once, and lockdown or blank with a window, each a TypeError
  • lockdown with anything but 'native': TypeError attachFrame: lockdown runs on the extension, which serves only cookies: 'native'
  • cloud.attachFrame with 'native': TypeError cloud.attachFrame: cookies: 'native' needs the FKN browser extension; the cloud render proxy has no browser cookies
  • extension.attachFrame with 'persistent' or 'ephemeral', so also with no cookies at all: ExtensionOperationUnsupportedError (operation 'cookies') attachFrame: cookies: '<value>' runs on the cloud render proxy; call cloud.attachFrame or the top-level attachFrame
  • a window on 'native', through the top-level attachFrame or extension.attachFrame, when the extension does not announce attachWindow (one before ABI 5): ExtensionOperationUnsupportedError (operation 'attachWindow') with the advice to update it, and with no extension on the page at all attachFrame: a window on cookies: 'native' needs the FKN WebExtension, which is not on this page. Neither waits or shows the install prompt, so the click’s activation is left for the app’s fallback.

type AttachFrameOptions = object;
optional blank?: BlankPage;

Starts the frame on an empty page presented at blank.url, with nothing requested from that site for it. For running the app’s own code (an attestation VM, a session client) in the origin it needs, without loading or running the site’s page.

The document is exactly <!doctype html><html><head><meta charset="utf-8"></head><body></body></html>, served 200 with content-type: text/html; charset=utf-8 and cache-control: no-store: no content security policy, no Set-Cookie. Its location, origin, document.URL, document.domain and baseURI read blank.url, and document.referrer reads '' on the first load (after a reload it reads the page’s own url, as on any proxied page). url() returns it. evaluate, postMessage and the message and document events work as on any page. Requests the page’s code makes later go out as that page’s would, through WebVPN, on the attachment’s jar: with cookies: 'persistent', the default, the cloud jar of the app’s top-level site, which every app of that site shares (HttpOnly values are never readable in the page); with 'ephemeral' a jar of the attachment’s own, and site storage under a namespace of its own, both gone with it.

With cookies: 'ephemeral' nothing of the user’s is reachable, so calls the Evaluation grant covers ask no card while the frame shows the empty page: until the attachment’s first goto, or until code in the page moves it to another url, even on the same host, after which they ask as on any page. With 'persistent' the Evaluation card names the url’s host, as for any page.

The empty page answers its url until the first goto: a reload of the page, or a replacement of the render proxy’s own document, brings it back and fires document, so an installer must be idempotent. After the first goto the url loads from the site like any other.

Always the cloud render proxy, with or without the extension: only it can present an origin without loading it. frame.backend() then answers 'cloud', and the jar is the cloud’s, never the browser’s.

Refused, in this order, before anything is attached, and after the refusals of cookies (AttachCookies), so through cloud.attachFrame lockdown and 'native' meet those first:

  • TypeError attachFrame: blank must be an object { url } (null, a string)
  • TypeError attachFrame: blank takes { url }, not "<key>" (an unknown key)
  • TypeError attachFrame: blank.url must be an absolute http or https url, not "<value>"
  • TypeError attachFrame: blank needs an iframe with no src; this one has "<src>"
  • TypeError attachFrame: blank does not combine with lockdown
  • TypeError attachFrame: blank runs on the cloud render proxy, which has no browser cookies; pass cookies: 'persistent' or 'ephemeral' (with cookies: 'native')
  • Error attachFrame: refusing to target the extension's own pages or an FKN platform origin, the scope check every attach url meets
  • Error attachFrame: a blank page is served by the cloud render proxy, which this build does not configure

After the handshake, with the iframe put back on about:blank and nothing requested from the site: the terminal LocatorUnsupportedError cloud.attachFrame: this FKN page predates blank pages; reload the app to load the current one. The handshake and ready limits are every cloud attach’s, 20000 ms and 65000 ms.

Also refused, before anything opens or is dispatched: with window, the TypeError attachFrame: blank does not apply to a window, since a window opened with no url is already called blank there; and through extension.attachFrame with cookies: 'native', ExtensionOperationUnsupportedError (operation 'blank') attachFrame: a blank page is served by the cloud render proxy only; call cloud.attachFrame.

optional cookies?: AttachCookies;

Which jar, and with it which backend: 'persistent' when absent. See AttachCookies.

optional domains?: string[];
iframe: HTMLIFrameElement;
optional lockdown?: boolean;

Replace the embedded site’s CSP with a lockdown policy: the frame cannot fetch, connect, or load any resource. Extension backend only, so it takes cookies: 'native'; beside any other value it is a TypeError (see AttachCookies). It needs an extension that announces an ABI: an older one may not serve it, so the attach is refused with ExtensionOperationUnsupportedError (operation 'lockdown') before anything is attached.

optional permissions?: CategoryRequest[];

Whole categories to ask for on one sheet or card right after the attach, before it resolves. A refusal does not reject the attach: it surfaces on the first refused operation.

optional storageState?: StorageState;

Seeds an 'ephemeral' jar before the attachment’s first document; with any other value a TypeError. See StorageState.


type AttachWindowOptions = object;

attachFrame’s options when the attachment is a new window rather than an iframe.

Call attachFrame directly in the click or key handler: the window opens before the first await, with the activation of that event, and the browser’s popup rules apply to it as to any page’s own window.open. iframe and lockdown do not apply to a window, and passing either is a TypeError.

optional cookies?: AttachCookies;

The window’s jar, with an iframe’s values (AttachCookies). 'persistent', the default: the jar of the app that opened it, the same as that app’s inline frames; a window whose app is on another site shares cookies only with those frames, and its site storage stays its own. 'ephemeral': a jar of the window’s own that ends with it, seeded from storageState. Both run on the cloud backend whatever is installed. 'native': a real browser window on the extension, on the person’s own browser session for every site it shows, as a window they opened themselves would be (WindowFrame says what differs there).

optional domains?: string[];
optional permissions?: CategoryRequest[];

Whole categories asked for once the window has connected: the card is drawn in the window on the cloud backend, and the sheet on the app’s page on the extension, as every extension sheet is.

optional storageState?: StorageState;

As for an iframe: only with 'ephemeral'. The window opens at once, with the click’s activation, and goes to window.url once the seed is in place.

window: FrameWindowOptions;

type BackgroundStoppedError = Error & object;

What a call into the extension rejects with when Chrome stopped the extension’s worker before it finished answering: before the call resolved, or, for a fetch, while its response body was still arriving. The call may or may not have run, so it is never resent; a retry by the app reaches the restarted worker.

name: typeof BACKGROUND_STOPPED;

type BlankPage = object;

Where attachFrame’s blank option presents its empty page.

url: string;

Absolute http or https url the empty page is presented at. The fragment is ignored for matching.


type ClearCookiesOptions = object;

frame.clearCookies’ options, Playwright’s browserContext.clearCookies names with its string semantics: each matches exactly. A cookie is removed when it matches every option given; an option left out, or undefined, matches every cookie. Playwright also takes a RegExp for each, which FKN refuses for now (Frame.clearCookies says why).

optional domain?: string;

Only cookies with this domain, in the form Playwright reports it: .youtube.com for a cookie its subdomains also receive, www.youtube.com for a host-only one, so 'youtube.com' matches neither of those. It must be on a site this attachment reaches.

optional name?: string;

Only cookies with this name.

optional path?: string;

Only cookies with this path, exactly as the cookie carries it.


type CookieDetails = object;
name: string;
url: string;

type Executor = object;
execute: (parts, operation, args, context?) => Promise<unknown>;

Runs operation on what parts resolve to, in whichever realm the chain lands in.

context is an opaque per-call value for the host: frame-locator never reads it, and carries it unchanged through every pivot and across the window bridge, so each executor the call passes through hands it to its own assertRealm (see ExecutorOptions). It crosses a window by structured clone, so it must be cloneable. A chain built with createFrameLocator never sends one; only a caller holding the executor itself can.

SelectorPart[]

string

unknown[]

unknown

Promise<unknown>

highlight: (parts, on) => Promise<void>;

SelectorPart[]

boolean

Promise<void>

optional stale?: Promise<void>;

type ExtensionHandshake =
| {
status: "absent";
}
| {
abi: number;
operations: readonly string[] | null;
status: "ok";
}
| {
abi: number;
operations: readonly string[] | null;
required: number;
status: "outdated";
};

operations is null when the extension announced no list, which is not the same as an empty one: a pre-versioning extension supports the whole original surface and cannot say so, so null reads as “assume the original set”, never “supports nothing”.


type FetchInit = RequestInit & object;
optional reason?: string;

type Frame = Omit<FrameLocator, "owner"> & object;

An attached frame. What it promises across navigations is the same on both backends.

A Frame follows its FRAME, not a document. The page in it may move itself (a link, a form, a redirect, a script), and every call runs against the document the frame holds when the call arrives. A call made while the next document commits waits for it, within the call’s own timeout.

It reads a document only while that document’s host is inside the attachment: the attach url’s host, each goto target’s host, and domains, each matched exactly. A document on any other host is refused with a LocatorDeniedError (“frame: this frame no longer holds the document the app attached it to”), which names nothing about where the frame went. The Frame stays attached, and answers again once the frame is back inside, by the page’s own move or by a goto.

What belongs to a document ends with it. A style addStyleTag added is gone after a navigation. A videoElement handle from a document that left rejects every call that answers (play, the picture-in-picture calls), and its fire-and-forget ones (pause, load, the setters) do nothing. On the cloud backend an evaluate whose document leaves before it settles rejects, and is not run again.

Not followed: a navigation the page aims out of its frame (a link or a form targeting the top window or a new one, a popup). A document with nothing of the page’s own to run it in, such as an error page or a file that is not HTML, cannot be read, and a call on it waits to its deadline.

addScriptTag(options): Promise<void>;

Runs content as a classic inline script of the attached document, as one of that page’s own scripts would run: its top-level declarations stay on the page’s global for later scripts and evaluate calls, document.currentScript is the script while it runs, and the page’s own error listeners see its top-level error. Mirage rewrites it as it rewrites the page’s scripts, and the page’s eval refusal and Trusted Types requirement do not refuse it, as for evaluate. Playwright’s name and its content option; its url, path and type are not served, and FKN’s sourceUrl names the script rather than loading one.

Resolves undefined once the script ran; what it installed lives as long as the document, and the document event says when that is gone. Same grant, limit and timing as evaluate: the Evaluation grant (one card covers both), 30000 ms counted after any consent card, the attached frame only, and after a goto the document that goto brought, waited for as evaluate waits. It runs once: it never rides the locator retry loop, and a call whose document left before it answered rejects and is not run again. Cloud only for now: the extension refuses it by name.

Refused:

  • TypeError, before anything is sent, in this order: frame.addScriptTag: options must be an object { content }; for Playwright’s url, path and type, frame.addScriptTag: "<key>" is not served; it runs content as a classic inline script; frame.addScriptTag: unknown option "<key>"; frame.addScriptTag: content must be a string; frame.addScriptTag: sourceUrl must be a non-empty string with no line break.
  • an Error carrying the page’s own name and message: the script’s top-level throw, after it ran.
  • LocatorDeniedError: no Evaluation grant, the frame holds no page, or its document is outside the attachment. Against an FKN page that predates addScriptTag the call arrives with no proof of a grant and the render proxy refuses it: frame.addScriptTag: the call arrived without an Evaluation grant, so it was not run.
  • cloud, LocatorUnsupportedError frame.addScriptTag: this render proxy predates addScriptTag; reload the app.
  • cloud, LocatorError: the document of the goto before it had not come by that goto’s deadline, or the app started another goto first, so the script was not run.
  • extension, ExtensionOperationUnsupportedError (operation 'addScriptTag') frame.addScriptTag: an extension frame does not run scripts through mirage yet; attach with cloud.attachFrame, before anything is dispatched.
  • TimeoutError frame.addScriptTag: no answer within 30000ms; the script may or may not have run.
  • the terminal detach error once the attachment ended.

AddScriptTagOptions

Promise<void>

backend(): "cloud" | "extension" | undefined;

Which backend serves this attachment, fixed for its life. The same call can draw a card on one backend and not the other, and spends a different identity on each, so an app must be able to tell. Undefined only for a Frame an app built itself with createFrame from a backend that names none.

"cloud" | "extension" | undefined

clearCookies(options?): Promise<void>;

Removes cookies from this attachment’s cookie jar, as Playwright’s browserContext.clearCookies removes them from a context. With no options it removes every cookie of the sites this attachment reaches; with options, only the ones that match every option given (ClearCookiesOptions). Each option is a string for now: a RegExp is refused.

For signing out of a site the user signed in to inside an FKN frame: that session lives in FKN’s jar, never in the app, so the app cannot remove it any other way.

Which jar: with cookies: 'persistent', the default, the cloud jar of the app’s top-level site, which every app of that site shares (every fkn.app app shares one), so a removal there signs every one of those apps out of that site. With 'ephemeral' the attachment’s own jar.

Which cookies: only those of a site this attachment reaches, a site being a host’s registered domain under the Public Suffix List, private section included: the attach url’s host (the empty page’s, for blank), each goto target’s host from the goto’s send, and domains. www.youtube.com reaches every *.youtube.com cookie and no google.com one, the rule browsers follow for Clear-Site-Data: "cookies". A host under the same name is still another site when a suffix lies between: amazonaws.com reaches no mybucket.s3.amazonaws.com cookie, since s3.amazonaws.com is a suffix. A host that is itself a public suffix (com, co.uk, github.io) is no site, and reaches only the cookies set for exactly that host. A Partitioned cookie matches in every partition.

Once it resolves: no request a document of this attachment starts carries a removed cookie, its documents’ document.cookie lists none, and the removal is committed to the jar, so an attachment created afterwards never sees one. Another live attachment on the same jar (another tab, another app’s frame) drops them when it hears the commit, normally within milliseconds; a request it started before then may still carry one. A request already in flight keeps what it was sent with, and a later response may set cookies again, as in any browser.

It resolves undefined and never says what or how much it removed, so it cannot tell an app whether the user had a session on a site. It takes the same steps and the same store write whether or not a cookie matched, so neither how long it takes nor which refusal it meets says so either. That is why a RegExp is refused before anything is sent: it would be tested in the render proxy against cookies the app cannot read, and a pattern slow on some of them would make the call’s duration, or a TimeoutError, say whether the jar holds one. Calling it again is harmless. It does not tell the site: the session stays valid there until it lapses, and FKN no longer holds it.

No consent card on either jar, and a frame holding no page is served. Cloud only. It runs once, never inside the locator retry loop, and waits at most 30000 ms.

Refused:

  • TypeError, before anything is sent, in this order: frame.clearCookies: options must be an object; frame.clearCookies: unknown option "<key>"; frame.clearCookies: <key> is a RegExp, and clearCookies takes strings for name, domain and path for now; frame.clearCookies: <key> must be a string; frame.clearCookies: <key> must not be empty; leave it out to match every <key>.
  • LocatorDeniedError frame.clearCookies: this attachment reaches no site yet; attach a url, goto one, or declare it in domains.
  • LocatorDeniedError frame.clearCookies: <domain> is not on a site this attachment reaches; goto it or declare it in domains, for a string domain.
  • cloud, LocatorUnsupportedError frame.clearCookies: this FKN page predates clearCookies; reload the app to load the current one, with nothing sent.
  • cloud, LocatorUnsupportedError frame.clearCookies: this render proxy predates clearCookies; reload the app.
  • cloud, Error frame.clearCookies: the cookies are gone from this attachment, but its jar did not take the removal, so other attachments may still send them; call it again.
  • cloud, TimeoutError frame.clearCookies: the render proxy did not answer within 30000ms; the cookies may or may not be gone, and calling it again is safe.
  • extension, ExtensionOperationUnsupportedError (operation 'clearCookies') frame.clearCookies: an extension frame runs on the browser's own cookies (cookies: 'native'), which FKN does not clear; attach with cookies: 'persistent' to clear the app's jar, before anything is dispatched.
  • the terminal detach error once the attachment ended.

ClearCookiesOptions

Promise<void>

evaluate<R, A>(pageFunction, arg?): Promise<Awaited<R>>;

Runs pageFunction inside the attached site’s OWN page realm, as a script of that page would: it sees the site’s window, its globals and its DOM. The function is sent as source (Function.prototype.toString), so it captures NOTHING from the caller’s scope; everything it needs must come through arg. A plain source string is run as an expression, the way Playwright does, and is not called. A promise, returned by the function or produced by the expression, is awaited.

arg is structured-cloned at the call, in the app’s realm, on both backends, and the result comes back by structured clone, so an ArrayBuffer or a typed array of bytes survives the round trip. A value structured clone refuses in arg (a function, a DOM node, an untransferred port) rejects with the platform’s DataCloneError before anything is sent; a port goes to the page with postMessage. A thrown error or a rejected promise comes back as an Error carrying the page’s own name and message. A result that cannot be cloned (a function, a DOM node) is a LocatorInvalidError, never a silent undefined and never a live handle into the page.

Every limit is refused by name, never a call left pending:

  • time, both backends: the code has 30 seconds to settle, counted after any consent card, and then the call rejects with TimeoutError. Only the wait ends there; code that never settles keeps running in the page.
  • size, extension only, a LocatorInvalidError: the source with its arg, and the result, each carry at most 32 MiB (binary data by its bytes, text by its UTF-8 size), the most its message relay carries.
  • Blob, extension only, a LocatorInvalidError: the relay cannot carry one, so pass or return await blob.arrayBuffer(). The cloud backend has neither limit, and returns a Blob as a Blob.

Needs the Evaluation grant on both backends; an app without it is refused by name. It runs on the attached frame only: a nested FrameLocator does not offer it, and a call there anyway is refused.

Called after goto resolved, whichever waitUntil, it runs once on the document that goto brought, never on the one it replaced. On the cloud backend the call waits for that document when it has not committed yet, and the wait counts toward the 30 seconds; if it has not come by the goto’s own deadline (the goto failed, or loaded something that is not a page), or the app started another goto first, the call rejects with a LocatorError saying the code was not run.

On the extension it also needs an extension new enough to run code (ABI 3, older ones are refused with ExtensionOperationUnsupportedError), and it runs where the page’s content security policy allows it: a frame whose host the app declared has its CSP replaced, so code runs; an undeclared frame, and every attached window, keeps its CSP, and a page that forbids eval refuses with a named error. On the cloud backend the code runs in the proxied document’s realm, which mirage virtualizes: the page’s own eval refusal and Trusted Types requirement do not refuse the source evaluate compiles, and every string that code compiles itself (eval, Function, a string timer) is held to them, as the page’s own are.

R = unknown

A = undefined

string | ((arg) => R | Promise<R>)

A

Promise<Awaited<R>>

goto(url, options?): Promise<void>;

Navigates the frame to url, which must pass the rules an iframe src does, and adds its host to the attachment. Resolves at options.waitUntil, and rejects with TimeoutError past options.timeout (GotoOptions).

string

GotoOptions

Promise<void>

off<K>(type, listener): void;

Removes a listener on added; one that is not on the Frame is ignored. An unknown type is a TypeError.

K extends keyof FrameEventMap

K

FrameEventListener<K>

void

on<K>(
type,
listener,
options?): void;

message: what the page posted to the app, with its origin and ports, and this Frame as source. The page reaches the app with parent.postMessage(x, appOrigin), top.postMessage(x, appOrigin), or event.source.postMessage(x, event.origin) on a message the app sent it; in an extension window, with opener.postMessage(x, appOrigin). A document outside the attachment is dropped, never masked. On the cloud backend a post to '*' does not reach the app: there the page is its own parent, so it cannot be told apart from the page messaging itself, and it stays with the page. The extension delivers it, as a real parent would receive it.

document: a new document arrived in the frame. What evaluate installed in the one before is gone, with every port it held, and nothing is reinstalled for the app: this is the signal to install again. It can fire twice for one document (a return from the back/forward cache), so an install should be idempotent. Cloud: fires for a document inside the attachment, with its origin, and not for the document the frame already held when the first listener was added. Extension: fires on every load of an iframe and every new document of a window, with origin ''.

Listeners stay on the Frame across navigations and end with the attachment, or when signal aborts, FKN’s addition to Playwright’s on. A listener added twice is called once, and one whose signal already aborted is not added. A listener that throws is reported and does not stop the others. An unknown type, or a listener that is not a function, is a TypeError. On the cloud backend, against an FKN page that predates messaging, nothing arrives and the console says so once.

K extends keyof FrameEventMap

K

FrameEventListener<K>

AbortSignal

void

postMessage(
message,
targetOrigin,
transfer?): Promise<void>;

Delivers message to the page in the frame as a real message event, the way iframe.contentWindow.postMessage would. The page sees a trusted event: data is the message, ports are the MessagePorts transfer moved, source is the page’s own parent, and origin is this app’s origin. So a page script that evaluate installed can take event.ports[0] and run any port protocol (osra included) over it, and no FKN code sits on that port afterwards.

targetOrigin is the page’s origin as the site knows it (https://anilist.co), parsed as the platform parses it, so a trailing slash or a path is fine. The message is delivered only if the frame’s document is on that origin, and otherwise dropped silently, as Window.postMessage does. '*', the default, means whatever document the frame holds; the extension refuses it for now.

It resolves once the message was handed to the browser for the document the frame holds, in the order the calls were made. That says nothing about whether the page listened, and a message dropped for its origin resolves the same way. Sent after goto resolved, it goes to the document that goto brought, as evaluate runs there.

Refused, and never sent again. On the cloud backend a refusal that depends on the grant or on the frame’s document comes after transfer was detached, and loses what it moved with the message; every other refusal comes before anything is detached.

  • TypeError, before anything is sent or detached: a targetOrigin that is not '*' or an origin ('/' included), or a transfer that is not an array. A message that cannot be cloned is the platform’s own DataCloneError.
  • LocatorDeniedError naming nothing: targetOrigin names a host outside the attachment, or the frame’s document is outside it.
  • LocatorDeniedError “frame: navigate the frame before postMessage”: the frame holds no page.
  • cloud, LocatorDeniedError: the app has no Evaluation grant, which is asked for on first use as evaluate asks. Messaging sits under Evaluation because the message arrives as the page’s own traffic and a port handed over is a live channel into the page.
  • cloud, LocatorUnsupportedError: an FKN page or render proxy that predates messaging.
  • cloud, LocatorError: the document changed during the call, so the message may or may not have reached it and is not sent to the next one; or the document of the goto before it had not come by that goto’s deadline (the goto failed, or loaded something that is not a page) or before the app started another goto, so nothing was sent.
  • extension, LocatorUnsupportedError: '*', which needs an extension release that reports the frame’s origin. Pass the origin.
  • extension, LocatorDeniedError: a window whose page cut its link to the app’s page (WindowFrame).
  • the terminal detach error once the attachment ended.

unknown

string

Transferable[]

Promise<void>

postMessage(message, options?): Promise<void>;

unknown

FramePostMessageOptions

Promise<void>

requestPermissions(requests): Promise<CategoryAnswer[]>;

Asks for whole categories on this attachment’s hosts, before any call. Resolves per request, in the order they were asked; a refusal is allow: false, never a rejection. An ask naming a category or key the registry cannot place on a row is a TypeError, thrown before it is sent. Not Playwright’s page.request, which is an HTTP client; the app asks here and the user grants.

CategoryRequest[]

Promise<CategoryAnswer[]>

url(): string;

The url last given to attachFrame or goto, never the frame’s live location: a page that moves itself does not change it.

string


type FrameLocator = FrameLocatorChain<LocatorModules, LocatorRewrites>;

A frame inside an attached frame, entered with frameLocator(selector). Its document is read as the attached frame’s is. owner() is Playwright’s: a Locator on the iframe it entered, whose contentFrame() comes back here. getByTestId searches from its document root. evaluate and addScriptTag run on the attached frame only, so neither is offered here.


type FrameWindowOptions = object;

Where an attachment’s window opens and how big it asks to be.

optional height?: number;

Inner height in CSS pixels, 700 by default. The browser may clamp it.

optional url?: string;

Where the window opens: an http or https address, which must also pass the rules an iframe src does. Omitted, the window opens blank and the app navigates it with goto, which is how an app keeps the click’s activation when the url needs an await first.

optional width?: number;

Inner width in CSS pixels, 500 by default. The browser may clamp it.


type FrameWindowRefusal = "jar-unreachable" | "bad-url" | "opaque-opener" | "unknown";

Why a window refused to attach. 'jar-unreachable': the cookie jar of the app that opened it could not be reached. 'bad-url': the window would not open that address. 'opaque-opener': this page’s origin is opaque (a sandboxed frame without allow-same-origin, or a file: page). A window can never address such a page, so none is opened. 'unknown': a reason this library does not know, sent by a newer window.


type Gate = (request, tools) => void | Promise<void>;

OperationRequest

GateTools

void | Promise<void>


type GateTools = object;
highlight: (on) => Promise<void>;

boolean

Promise<void>


type GotoOptions = object;

frame.goto’s options. Refused with a TypeError before anything is sent: a waitUntil other than 'load' or 'commit' (Playwright’s 'domcontentloaded' and 'networkidle' are not served, and 'documentstart' is now 'commit'), a timeout of 0 or below or not finite, and timeoutMs.

optional domains?: string[];
optional timeout?: number;

How long the goto may take, in milliseconds: 30000 when absent. Past it the goto rejects with TimeoutError. A positive number: 0 is a TypeError, since every call ends by its deadline and so does not take Playwright’s “0 disables the timeout”.

optional waitUntil?: "commit" | "load";

When goto resolves: 'load', the default, once the document it brings fired load; 'commit', Playwright’s, once that document holds the frame, possibly before the page’s own scripts ran and whether or not it ever fires load. Either way a call made after it runs on the document the goto brought, never on the one it replaced, and the goto rejects with TimeoutError when that document has not come by its deadline.

A goto that changes only the fragment of the url the page is at keeps its document. On the cloud backend it resolves, either way, once the page reports the new fragment. Two pages are the exception there, where such a goto brings a new document like any other: one reached through a redirect, and one that followed a link onto another origin. On the extension (measured on Chromium) one awaited to 'load' resolves, since the frame fires load for it, and one awaited to 'commit' runs out at its timeout, since no new document starts.


type HeaderOperation = object;
header: string;
operation: "set" | "remove";
optional value?: string;

type OperationRequest = object;
args: unknown[];
operation: string;
parts: SelectorPart[];
phase: "execute" | "ensure";

type PermissionGrant =
| CategoryGrant
| {
allow: boolean;
key: PermissionScope;
scope: string;
site: string;
};

key and scope are set only for a legacy answer, category and hosts only for a category one. A legacy answer also carries site, the host its scope named, empty where the scope names none (a locator selector), which is the case that is always refused.


type PermissionRequest = CategoryRequest & object | LegacyPermissionRequest;

hosts is what makes an item a category ask; without it the item is the legacy per-key one.


type RemoteVideoElement = EventTarget & object;
autoplay: boolean;
readonly buffered: TimeRanges;
readonly currentSrc: string;
currentTime: number;
disableRemotePlayback: boolean;
readonly duration: number;
readonly ended: boolean;
readonly error: MediaError | null;
readonly HAVE_ENOUGH_DATA: 4;
readonly HAVE_FUTURE_DATA: 3;
loop: boolean;
muted: boolean;
readonly paused: boolean;
playbackRate: number;
poster: string;
preload: string;
readonly readyState: number;
readonly seekable: TimeRanges;
readonly seeking: boolean;
src: string;
volume: number;
exitPictureInPicture(): Promise<void>;

Promise<void>

load(): void;

void

pause(): void;

void

play(): Promise<void>;

Promise<void>

requestPictureInPicture(): Promise<void>;

Promise<void>


type RequestHeaderRule = object;
domains: string[];
optional reason?: string;
requestHeaders: HeaderOperation[];

type SelectorPart = object;
args: unknown[];
kind: ChainKind;
name: string;

type SiteCookie = object;
name: string;
value: string;

type StorageState = object;

A jar’s starting state, in the shape Playwright’s browser.newContext({ storageState }) takes, except that a cookie’s expires is milliseconds here where Playwright’s is seconds. Seeded into an 'ephemeral' attachment’s fresh jar and its storage namespace before the attachment’s first document is requested, so that request already carries its cookies and the page’s first script already reads its localStorage. With blank, whose empty page requests nothing and runs no code, it is seeded right after that page is in place, before the attach resolves.

Accepted only with cookies: 'ephemeral': with 'persistent' an app-written cookie would land in the jar every app of the top-level site shares, and with 'native' in the person’s own session. The seed travels on FKN’s own channels, never in a url the app page could read. A seeded HttpOnly cookie is sent to its site and never readable in the page, as any HttpOnly cookie, and it never comes back to app code.

Refused by name, before anything is attached (AttachCookies has the order): a field of another shape; a name or value with ; or a control character, or a name with =; a cookie a browser does not store (SameSite=None or __Secure- without secure, __Host- without secure, a host-only domain and path ’/’); an origin that is not an absolute http or https origin; and an expires that reads as seconds. Against an FKN page or render proxy that predates it, after the iframe is put back on about:blank (or the window closed) and before anything is requested from the site: the terminal LocatorUnsupportedError attachFrame: this FKN page predates storageState; reload the app to load the current one.

optional cookies?: StorageStateCookie[];
optional origins?: object[];

Each origin’s localStorage items, read by every page of that origin in the attachment.

localStorage: object[];
origin: string;

type StorageStateCookie = object;

One cookie of a StorageState.

domain: string;

.youtube.com for a cookie its subdomains also receive, www.youtube.com for a host-only one, as Playwright writes it.

optional expires?: number;

Milliseconds since the epoch, where Playwright’s is seconds; -1 or absent for a session cookie.

optional httpOnly?: boolean;
name: string;

Non-empty, with no =, ; or control character.

optional path?: string;

/ when absent.

optional sameSite?: "Strict" | "Lax" | "None";

'Lax' when absent, as a browser defaults it.

optional secure?: boolean;
value: string;

With no ; or control character.


type WindowFrame = Frame & object;

The Frame of an attachment that lives in its own window.

On the extension (cookies: 'native') the window is a real browser popup the app’s page opens with window.open, whose page is the site itself: there is no FKN page in it. Where that differs:

  • The Frame follows the window’s page wherever it goes, as it follows an iframe’s, and its reads answer only inside the attachment. A page that goes to an FKN host ends the attachment (closed).
  • postMessage and the message event need the window to keep its link to the app’s page. A page served with Cross-Origin-Opener-Policy (same-origin, same-origin-allow-popups) cuts it as it loads, and postMessage is then the LocatorDeniedError frame.postMessage: this window's page no longer keeps a link to the app's page, so a message cannot reach it. Everything else still answers: the extension knows the window by its tab, never by that link. The page reaches the app with opener.postMessage(x, appOrigin) while the link holds.
  • Its consent sheets are drawn on the app’s page, not in the window.
  • evaluate keeps the window page’s content security policy (an attached iframe on a declared host has it replaced), so a page that forbids eval refuses with a named error.
  • Calls after closed reject with the terminal LocatorUnsupportedError extension.attachFrame: the attached window closed; attach a fresh window.
  • The extension never throws FrameWindowRefusedError, which is the cloud’s.
closed: Promise<void>;

Resolves once the attachment has ended, and never rejects. Calls made after it resolved reject with the terminal detach error. On the cloud backend: the window was closed by anyone, reloaded, or left the page FKN attached; it stopped answering (a crash, a frozen page); it lost the app’s cookie jar mid-session; or the app page went away. On the extension: the window was closed by anyone, close() ran, its page went to an FKN host (a goto redirected there rejects with the detach error), or the app page went away; the last two leave the window open for the person. Any other reload or navigation in the window does not end it, since the Frame follows the window’s page.

close(): Promise<void>;

Ends the attachment and closes the window. On the cloud backend it first waits, at most 2 seconds, for the window’s cookie changes to be committed to its jar, so a goto on the app’s inline frame right after sees the session. On the extension the window ran on the person’s own browser cookies, which are already written, so an inline 'native' frame sees a sign-in on its next goto. Idempotent, and never rejects.

Promise<void>

const assertAttachableFrameUrl: (raw) => void;

string

void


const assertFetchableUrl: (raw) => void;

string

void


const attachFrame: AttachFrameFunction;

Attaches through the extension, on cookies: 'native' only: the other values, the default among them, are the cloud render proxy’s, refused here with ExtensionOperationUnsupportedError (operation 'cookies') before any exposure wait (AttachCookies). A domains entry, of the attach or of a goto, or a goto target’s host, that the frame’s header rule would carry onto an FKN platform host (one of them, one under one, or a parent such as app) is refused with an Error before anything is armed.


const BACKGROUND_STOPPED: "BackgroundStoppedError" = "BackgroundStoppedError";

const cookies: object;
get: (details) => Promise<SiteCookie | null>;

Reads one named cookie of another site behind a Network ask on its host, null when there is none. A url on an FKN platform host (fkn.app, fkn.dev, sdbx.app and their subdomains) is refused with an Error before anything is asked, and again by the extension.

CookieDetails

Promise<SiteCookie | null>


const events: TypedEventTarget;

const EXTENSION_ABI: 5 = 5;

What the extension half announces about itself. Increment when the callable surface changes in a way a page could notice: an operation added, removed, renamed, or given different semantics. Not the package or manifest version, which move for reasons unrelated to the protocol.


const fetch: (input, init?) => Promise<Response>;

Fetches through the extension. A credentialed fetch (credentials: 'include') and one to the local network are asked for on the target’s host first. A url on the extension’s own pages or an FKN platform host is refused with an Error before anything is asked. A redirect onto an FKN platform host is not followed: the hop is blocked before it is sent, on both engines and in every spelling, and should one ever slip the block the final answer is still withheld with an Error. A fetch that follows redirects is refused before it is sent when the extension could not set the block up.

RequestInfo | URL

FetchInit

Promise<Response>


const FORGEABLE_HEADERS: string[];

const isBackgroundStopped: (error) => boolean;

Whether error is a BackgroundStoppedError, matched on name since it may arrive as a revived Error or a plain record.

unknown

boolean


const isExtensionExposed: () => boolean;

Unchanged on purpose: a page half built before versioning calls exactly this, so an extension that announces an ABI keeps answering it the same way. An outdated extension also answers true.

boolean


const isLocalNetworkUrl: (raw) => boolean;

string

boolean


const isLocatorDenied: (error) => boolean;

unknown

boolean


const isLocatorInvalid: (error) => boolean;

unknown

boolean


const isLocatorUnsupported: (error) => boolean;

unknown

boolean


const isTerminalError: (error) => boolean;

unknown

boolean


const LOCATOR_DENIED: "LocatorDeniedError" = "LocatorDeniedError";

const LOCATOR_ERROR: "LocatorError" = "LocatorError";

const LOCATOR_INVALID: "LocatorInvalidError" = "LocatorInvalidError";

const LOCATOR_UNSUPPORTED: "LocatorUnsupportedError" = "LocatorUnsupportedError";

const permissions: object;
request: (requests) => Promise<PermissionGrant[]>;

Asks for permissions on one sheet (extension) or card (cloud), in the order given, and answers one grant per request.

A category ask needs hosts, the sites the grant applies to, and is answered allow: true only where every one of them was granted. It needs an extension that serves categories and throws ExtensionOutdatedError below that; a legacy { key, scope } item is forwarded to any version.

An ask naming a category or key the registry cannot place on a row is a TypeError, thrown before anything is sent: its answer would have to name a category the caller never asked for. A category ask naming an FKN platform host is an Error, also before anything is sent; the extension answers allow: false for any item, legacy ones included, that names one.

PermissionRequest[]

Promise<PermissionGrant[]>


const readExtensionHandshake: () => ExtensionHandshake;

The three-way answer: absent, present but too old, or usable.

ExtensionHandshake


const removeRequestHeaderRule: (ruleId) => Promise<void>;

number

Promise<void>


const REQUIRED_EXTENSION_ABI: 0 = 0;

The oldest extension the page half will talk to. Raise it only when the page half starts depending on something older extensions cannot do, never merely because EXTENSION_ABI moved. Deliberately 0: extensions that predate versioning announce no ABI, and a floor of 1 would refuse all of them.


const setMissingExtensionHandler: (handler) => void;

MissingExtensionHandler | null

void


const setRequestHeaderRule: (rule) => Promise<{
ruleId: number;
}>;

Tab-scoped: the rule is dropped when the tab goes away. A domain the rule would carry onto an FKN platform host (one of them, one under one, or a parent such as app, since a rule also matches every host under each domain) is refused with an Error before anything is asked, and again by the extension. So is a header other than Origin, Referer and Cookie (FORGEABLE_HEADERS).

RequestHeaderRule

Promise<{ ruleId: number; }>


const supportsOperation: (handshake, operation) => boolean;

Whether a named operation is callable. A null list answers true (the extension predates the list and supports the original surface); refusing a too-old extension is the ABI floor’s job.

ExtensionHandshake

string

boolean


const waitForExtensionExposure: (timeout?) => Promise<void>;

number

Promise<void>

function available(): boolean;

boolean


function promptInstall(reason?): Promise<boolean>;

string

Promise<boolean>

Renames and re-exports events