attachFrame() puts another website inside your page, or in a window your page opens, and hands back a Frame you navigate and drive. This page covers the two backends a frame runs on and the cookies option that picks between them, every option, blank pages, seeding a fresh jar, goto() and url(), opening a window, running code in the page, clearing cookies, frame.fetch() and its two refusals, what is refused with which message, and the timeouts on each path.
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({
iframe: HTMLIFrameElement
iframe:
var document:Document
window.document returns a reference to the document contained in the window.
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).
textContent() // 'Catalog', after one Site data row on the cloud card
constframe:Frame
frame.
functionurl():string
The url last given to attachFrame or goto, never the frame's live location: a page that moves itself does not change it.
url() // 'https://example.org/catalog'
The call resolves once a backend has taken the iframe. A backend is where a call runs. On the cloud, the fkn.app page has answered the handshake and the render proxy, the cloud frame backend, has reported ready. On the extension, the content script has registered the iframe, installed the header rule for any domains and recorded the embed.iframe consent.
Both backends mark the frame for the user: the cloud draws an identity bar naming your app, and the extension pushes an identity pill into the framed document.
A blank iframe holds no document a locator can reach until the first goto() commits one, unless it was attached on a blank page. On the extension a blank frame gets no content script, so a locator call before that navigation waits out its 30,000 ms deadline.
The demo below attaches a blank iframe with domains: ['en.wikipedia.org', 'wikipedia.org'], navigates it to the Wikipedia search page, and restyles that page with addStyleTag(). With the extension exposed it attaches with cookies: 'native', on the extension and the person’s own Wikipedia cookies. Otherwise it attaches with cookies: 'ephemeral', on the cloud render proxy and a jar of its own, which frame.fetch() needs there.
The button first asks for two categories, Site data and Interaction, on one prompt through frame.requestPermissions(), the call that asks for several at once. The consent sheet is what the extension shows a user before an action above severity 0, and the cloud backend draws the broker’s card in its place, so both paths ask. A refusal stops the run, and what follows is one read, one fill and one click, all covered by the answer.
On an extension below ABI 2, an install still on 0.1.3, the store build before 0.1.53, the demo falls back to the pre-category ask and puts read.text, act.type and act.click on one sheet instead. That build also fails a gated locator call with Unknown locator kind, so the run needs 0.1.53 or newer to finish, and the stores serve 0.1.54.
The cloud backend loads the site through the render proxy, needs nothing installed and runs on one of FKN’s own cookie jars. The extension backend drives the iframe in place through the FKN extension’s content script, so the person’s own browser session can travel with it.
cookies picks the jar an attachment runs on, and the jar decides the backend. One attachment has one jar, so which cookie a request carries always has one answer:
the render proxy’s jar of your app’s top-level site, https://fkn.app for every fkn.app app, which every app of that site shares and keeps across visits on this device. Your inline frames and your windows use it too. It is kept sealed on this device, signed in to FKN or not, see the persistent jar on this device.
'ephemeral'
the cloud, whatever is installed
a fresh jar of the attachment’s own, with the site’s storage under a namespace of its own, both gone with it, and seeded from storageState when you pass one.
'native'
the extension
the person’s own browser cookies for domains and every goto() host, copied into the browser’s partition for your app’s top-level site on the attach and on every goto().
HttpOnly values never reach your code on any of the three. The root attachFrame() never waits for the extension on 'persistent' or 'ephemeral'. On 'native' it waits for the extension to expose itself while the page loads, 150 ms once the document is complete and 10,000 ms at most, and shows the install card when it does not. frame.backend() says which backend serves an attachment, fixed for its life:
app.ts
const
constiframe:HTMLIFrameElement
iframe=
var document:Document
window.document returns a reference to the document contained in the window.
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({
iframe: HTMLIFrameElement
iframe,
domains?: string[] |undefined
domains: ['example.org'],
cookies?: AttachCookies |undefined
Which jar, and with it which backend: 'persistent' when absent. See AttachCookies.
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.
isExtensionExposed() ?'native':'persistent', // read once the page has loaded, since the marker lands a tick after it starts
})
if (
constframe:Frame
frame.
functionbackend():"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.
backend() ==='extension') {
// the person's own example.org cookies came in with the attach
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).
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).
goto('https://example.org/catalog') // your app's cloud jar, none of the person's browser cookies
textContent() // 'Catalog', after one Site data row on the cloud card
}
The same call can draw a card on one backend and a sheet on the other, and spends a different identity on each, so the Frame names its backend and your app can tell. attachFrame() needs a window realm on either backend, so drive frames from the page and not from workers.
cookies replaced syncCookies in @fkn/lib 0.9.42. A syncCookies key, true or false, is refused with attachFrame: syncCookies was replaced by cookies before anything is attached. true is 'persistent', or 'native' where it meant the person’s own cookies on the extension, and false is 'ephemeral'. Since @fkn/lib 0.9.43 storageState, Playwright’s name for a context’s starting cookies and localStorage, seeds an 'ephemeral' jar, see seeding a fresh jar.
The 'persistent' jar is kept in the render proxy’s storage on this device, one jar per top-level site, and it is written sealed (AES-GCM) for everyone, signed in to FKN or not. The key is the jar’s own: the render proxy generates it the first time that site’s jar loads on this device, and the browser keeps it, non-extractable, beside the jar. Every page that opens the jar opens its key with it, so the jar acts as a clear one would: no card, no wait, no signed-out state, and a reset of the FKN account’s key leaves it alone. Your code changes nothing for it: the same attach, the same goto(), the same cookies in requests, and clearCookies() as before.
No cookie of the jar is in the clear in its records on disk, HttpOnly ones included, so a search of the browser’s files, or a backup index of them, does not find cookie text there.
What the seal does not keep, stated plainly:
The key sits beside the jar. The browser writes even a non-extractable key to its files, so a copy of the device’s files holds what opens the jar.
Older clear text can stay for a time on Chrome. Where an earlier build kept the jar in the clear, Chrome can keep that text in its files for a while after the seal. Firefox removes it at once.
Nothing else is sealed: the proxied site’s localStorage, IndexedDB and caches are never sealed, and while a frame runs, its jar is in memory to build each request.
Each jar’s first load since 2026-10-06 seals what earlier builds left: a jar kept in the clear is taken in and sealed, sign-ins included. A jar sealed between 2026-10-05 and 2026-10-06 under a key from the FKN account’s own cannot be opened without that key, so it was dropped on that first load, and the person signs in to those sites once more.
The hosts the frame will hold. Extension: framing headers lifted and cookies copied for them. Cloud: the hosts frame.fetch() may reach, and sites frame.clearCookies() reaches.
Categories of access to ask for on one prompt, as the attach’s last step, see several at once.
blank
none
{ url }: start on an empty page presented at that url, with nothing requested from the site, see starting on a blank page.
storageState
none
With cookies: 'ephemeral' only: cookies and localStorage put in the attachment’s own jar before its first page is requested, see seeding a fresh jar.
domains on the extension installs one session rule for those hosts, in this tab only. The rule removes X-Frame-Options, Content-Security-Policy and Content-Security-Policy-Report-Only from the framed document’s responses and its subresources. With lockdown it still removes X-Frame-Options, sets Content-Security-Policy to default-src 'none' instead of removing it, and leaves Content-Security-Policy-Report-Only alone. A domain the rule would carry onto an FKN platform host (one of them, one under one, or a parent such as app) is refused with attachFrame: refusing to target FKN platform domains before anything is armed.
Without domains a site that forbids framing stays blank until a goto(), which adds its target host to the rule. On the cloud the list is normalised to bare lowercase hostnames of at most 253 characters, and anything else is dropped rather than repaired.
cookies: 'native' copies a cookie whose domain equals a declared host, or is a subdomain of one that itself contains a dot. The copy runs only for the attach domains and each goto() target, so an attach without domains copies nothing and the frame starts logged out.
An attachment that should hold nothing of anyone’s takes 'ephemeral':
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({
iframe: HTMLIFrameElement
iframe:
var document:Document
window.document returns a reference to the document contained in the window.
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).
goto('https://example.org/catalog') // resolves on the frame's load event
textContent() // 'Catalog', after one Site data row, from a jar no other attachment shares
That frame runs on the cloud whatever is installed, on a jar that ends with the attachment, and the two hosts are what a later frame.fetch() may reach.
permissions asks before the attach resolves, on the document’s host and the declared domains, and a refusal does not reject the attach: it surfaces on the first refused operation. It needs a host to name, so a blank frame with no domains throws frame.requestPermissions: navigate the frame or declare domains first. An extension below ABI 2 skips the ask with a console warning rather than failing the attach:
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({
iframe: HTMLIFrameElement
iframe:
var document:Document
window.document returns a reference to the document contained in the window.
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.
blank: { url } starts the frame on an empty page presented at url, with nothing requested from that site for it. It is for running your app’s own code, an attestation VM or a session client, in the origin that code needs, without loading or running the site’s page:
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({
iframe: HTMLIFrameElement
iframe:
var document:Document
window.document returns a reference to the document contained in the window.
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.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 LocatorUnsupportedErrorcloud.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.
blank: {
url: string
Absolute http or https url the empty page is presented at. The fragment is ignored for matching.
url: 'https://www.example.org/' }, // nothing is requested from www.example.org for this page
cookies?: AttachCookies |undefined
Which jar, and with it which backend: 'persistent' when absent. See AttachCookies.
cookies: 'ephemeral', // a jar of its own, so evaluate and addScriptTag ask no card here
})
constengine:Frame
engine.
functionbackend():"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.
backend() // 'cloud', whatever is installed
constengine:Frame
engine.
functionurl():string
The url last given to attachFrame or goto, never the frame's live location: a page that moves itself does not change it.
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, LocatorUnsupportedErrorframe.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.
TimeoutErrorframe.addScriptTag: no answer within 30000ms; the script may or may not have run.
the terminal detach error once the attachment ended.
addScriptTag({
content: string
The script's source text.
content: 'globalThis.engineReady = true',
sourceUrl?: string |undefined
Names the script in stack traces through //# sourceURL. Absent, its frames name the document.
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.
evaluate(() => [
var location:Location
The Window.location read-only property returns a Location object with information about the current location of the document.
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 and no Set-Cookie. Its location, origin, document.URL, document.domain and baseURI read blank.url, and document.referrer reads '' on the first load. evaluate(), addScriptTag(), postMessage() and the message and document events work as on any page.
Requests the page’s code makes go out as that page’s would, through WebVPN, on the attachment’s jar. With 'persistent' that is your app’s cloud jar, which every app of your top-level site shares. With 'ephemeral' it is a jar of the attachment’s own, gone with it. Coming with the next FKN extension store release, and not served yet: on 'persistent' an installed extension sends the page’s own fetch and XHR requests to the attachment’s hosts from the browser, on the same jar and with none of the browser’s own cookies.
With 'ephemeral' nothing of the user’s is reachable, so the calls the Evaluation grant covers ask no card while the frame shows the empty page: until the first goto(), or until code in the page moves it to another url, even on the same host. After that 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 whatever you install must be safe to install twice. After the first goto() the url loads from the site like any other.
A blank page is always served by the cloud render proxy, since only it can present an origin without loading it. It is refused before anything is attached, in this order:
storageState starts an 'ephemeral' attachment with cookies and localStorage already in place, as Playwright’s browser.newContext({ storageState }) starts a context. The seed goes into the attachment’s own jar and storage namespace before its first page is requested, so that request already carries the cookies and the page’s first script already reads its localStorage:
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({
iframe: HTMLIFrameElement
iframe:
var document:Document
window.document returns a reference to the document contained in the window.
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).
goto('https://example.org/account') // the first request already carries session=b2f1c9
textContent() // the name that session belongs to, after one Site data row on the cloud card
The shape is Playwright’s, with cookies and origins under the same names, and an origins entry the same { origin, localStorage: [{ name, value }] }. It differs in five places:
Playwright’s storageState
FKN’s
a cookie’s expires
seconds since the epoch, -1 for a session cookie
milliseconds since the epoch, -1 or absent for a session cookie
the object only, and a key outside the shape is refused by name, an indexedDB entry that Playwright’s storageState({ indexedDB: true }) writes among them
a partitioned (CHIPS) cookie
carries partitionKey, and from Chromium _crHasCrossSiteAncestor beside it
takes no partition: both keys are refused by name, and a seeded cookie is never partitioned
A cookie’s domain is written as Playwright writes it, .example.org for a cookie its subdomains also receive and www.example.org for a host-only one, and an origin as URL.origin writes it, with no trailing slash. A state Playwright saved needs up to three changes, all made by the sample below:
every expires times 1000
on a partitioned cookie from Chromium, partitionKey and _crHasCrossSiteAncestor left out, so it is seeded as an ordinary cookie of its domain; leave the whole cookie out instead if it should reach its site only under the top-level site its partitionKey names
on a cookie Firefox saved, sameSite: 'None' without secure made 'Lax': Firefox saves 'None' for a cookie that set no SameSite, which Chromium reads as 'Lax', and FKN refuses 'None' without secure as a browser would
app.ts
const
conststorageState:StorageState
storageState:
typeStorageState= {
cookies?:StorageStateCookie[];
origins?: {
origin:string;
localStorage: {
name:string;
value:string;
}[];
}[];
}
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 LocatorUnsupportedErrorattachFrame: this FKN page predates storageState; reload the app to load the current one.
Calls a defined callback function on each element of an array, and returns an array that contains the results.
@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map(({
name: string
name,
value: string
value,
domain: string
domain,
path: string
path,
expires: number
expires,
httpOnly: boolean
httpOnly,
secure: boolean
secure,
sameSite: "Strict"|"Lax"|"None"
sameSite }) => ({
name: string
name,
value: string
value,
domain: string
domain,
path: string
path,
httpOnly: boolean
httpOnly,
secure: boolean
secure,
expires: number
expires:
expires: number
expires===-1?-1:
expires: number
expires*1000, // seconds to milliseconds
sameSite: "Strict"|"Lax"|"None"
sameSite:
sameSite: "Strict"|"Lax"|"None"
sameSite==='None'&&!
secure: boolean
secure?'Lax':
sameSite: "Strict"|"Lax"|"None"
sameSite, // Firefox's 'None' for a cookie that set no SameSite
})),
origins?: {
origin: string;
localStorage: {
name: string;
value: string;
}[];
}[] |undefined
Each origin's localStorage items, read by every page of that origin in the attachment.
Calls a defined callback function on each element of an array, and returns an array that contains the results.
@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map(({
origin: string
origin,
localStorage: {
name: string;
value: string;
}[]
localStorage }) => ({
origin: string
origin,
localStorage: {
name: string;
value: string;
}[]
localStorage })), // an indexedDB entry stays behind
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({
iframe: HTMLIFrameElement
iframe:
var document:Document
window.document returns a reference to the document contained in the window.
Seeds an 'ephemeral' jar before the attachment's first document; with any other value a TypeError. See StorageState.
storageState })
The seed reaches the attachment’s own jar and storage namespace and nothing else: never the jar your 'persistent' attachments share, never the person’s browser cookies, and it is gone with the attachment. It travels on FKN’s own channels, never in a url your page could read, and the library seeds the copy it checked, so a change to the object afterwards changes nothing. A seeded HttpOnly cookie is sent to its site and is never readable in the page, as any HttpOnly cookie, and nothing about the jar comes back to your code.
When the iframe has a src, or a window a url, the attach starts on no page, seeds, and loads that url as its first goto(). It then resolves once that page has loaded, and a failure on the way rejects the attach with the iframe put back on about:blank, or the window closed. A window still opens at once, on the click’s activation.
With blank the seed is in place before the attach resolves. Each origin’s localStorage items are written before that origin’s first page in the attachment runs a script, whichever goto() reaches it.
storageState is refused before anything is attached, in this order, after the cookies refusals above:
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).
goto('https://example.org/catalog') // resolves on the frame's load event
constframe:Frame
frame.
functionurl():string
The url last given to attachFrame or goto, never the frame's live location: a page that moves itself does not change it.
url() // 'https://example.org/catalog', the url you asked for
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).
goto('https://example.org/player', {
waitUntil?:"commit"|"load"|undefined
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.
waitUntil: 'commit',
timeout?: number |undefined
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".
timeout: 10_000 }) // once the new document holds the frame
waitUntil takes Playwright’s two values that both backends serve. 'load', the default, resolves once the document the goto brought fired load. 'commit' resolves 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.
Any other value is a TypeError before anything moves, frame.goto: waitUntil must be 'load' or 'commit': 'documentstart' is now 'commit', and 'domcontentloaded' and 'networkidle' are not served.
timeout is the goto’s deadline, 30,000 ms when absent, and a positive number: 0 is refused, since every call here ends by its deadline. Past it the goto rejects with a TimeoutError, exported from the root and minted in your realm, so instanceof works:
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).
goto('https://example.org/slow', {
timeout?: number |undefined
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".
timeout: 5_000 })
} catch (
var error:unknown
error) {
if (!(
var error:unknown
errorinstanceof
classTimeoutError
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.
On the extension every goto() is itself a consent, embed.open, at severity 0: no sheet, one auto row in the activity log, the on-device record of what an app did, scoped to the URL you asked for. A relative url resolves against your page, not the frame, so pass absolute URLs. options.domains extends the extension’s header rule and cookie copy for that navigation, and the cloud ignores it.
url() is the URL the app last asked for, on both backends: the attach target, or the target of your last goto(). A navigation the framed document performs on its own is invisible to it, and a frame that leaves the hosts of the attachment refuses reads with frame: this frame no longer holds the document the app attached it to.
The extension one waits up to 1,000 ms for an ok handshake (or 150 ms after load). An extension announcing an ABI below the floor rejects with ExtensionOutdatedError before any card is shown. With no handshake it opens the install card drawn by the broker, the connection your app holds into FKN, and stays pending while the card is open. Pass null to setMissingExtensionHandler() to draw that state yourself, and the call rejects quietly instead:
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.
attachFrame({
iframe: HTMLIFrameElement
iframe:
var document:Document
window.document returns a reference to the document contained in the window.
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).
reason: 'Show who is signed in' }) // the signed-in name, after one Site data row on the sheet
} catch (
var error:unknown
error) {
if (
var error:unknown
errorinstanceof
classExtensionOutdatedError
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.
Attaches the render proxy to an iframe the app mounted, or opens it in a window of its own with
{ window }. A window opens before the first await, so call this directly in a click handler.
Serves cookies: 'persistent', the default, and 'ephemeral'; 'native', the browser's own
cookies, is a TypeError before anything is attached or opened (AttachCookies).
attachFrame({
iframe: HTMLIFrameElement
iframe:
var document:Document
window.document returns a reference to the document contained in the window.
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).
textContent() // 'Catalog', after one Site data row on the cloud card, and no activity log there
It rewrites the iframe’s src to https://fkn.app/attach-frame?url=..., connects to that page within 20,000 ms, waits up to 65,000 ms for the render proxy to report ready, and on either failure restores src, allow and referrerPolicy before rethrowing. A live attachment holds one of the busy tokens, the reasons a realm reports itself busy, under the name attached frame until the iframe leaves the document or the page hides.
attachFrame({ window }) opens the attachment in a child window instead of an iframe of yours, and resolves with a WindowFrame. It is built for sign-in pages, and your app reads the outcome through the same locators it uses inline. The iframe path is unchanged.
Call attachFrame directly inside the click or key handler, with no await before it. The window opens before the call’s first await, on that event’s activation, and the browser’s popup rules apply to it as to any window.open:
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.
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({
window: FrameWindowOptions
window: {
url?: string |undefined
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.
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.
disabled=false }) // resolves whoever ends the window
} catch (
function (localvar) error: unknown
error) {
if (!(
function (localvar) error: unknown
errorinstanceof
classFrameWindowBlockedError
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.
textContent='Allow pop-ups for this page, then click again'
}
})
A call outside a user gesture, one the popup blocker refuses, and one from a frame sandboxed without allow-popups all reject with FrameWindowBlockedError, a class minted in your realm. When the address needs an await first, open the window blank with window: {} and goto() it once the address is known. A page served with Cross-Origin-Opener-Policy: same-origin cannot keep its window, and the attach rejects with cloud.attachFrame: the window closed before it connected, so serve it with same-origin-allow-popups instead.
The window takes these options, and iframe, lockdown or blank beside them is a TypeError:
Option
Default
What it does
window.url
blank
Where the window opens, an http or https address held to the same rules as an iframe src. Omitted, goto() navigates it later.
window.width, window.height
500, 700
The inner size asked for, in CSS pixels. The browser may clamp it.
'persistent' is the cookie jar of the app that opened the window. 'ephemeral' gives the window a jar of its own that ends with it. Both run on the cloud. 'native' is a real browser window on the extension, on the person’s own browser session, from its first store release after 0.1.54, see a window on the extension.
storageState
none
With cookies: 'ephemeral' only: cookies and localStorage put in the window’s jar before window.url loads, see seeding a fresh jar.
Categories to ask for once the window has connected: on a card drawn in the window on the cloud, on the extension’s sheet over your page on 'native'.
On the cloud, a WindowFrame is the whole Frame: locators, goto(), url(), requestPermissions(), clearCookies(), and fetch() under the cloud’s rules. It adds two members. close() commits the window’s cookie changes to its jar, waiting at most 2,000 ms, then closes the window, and it is idempotent and never rejects.
On 'persistent', the default, a window uses the cookie jar of the app that opened it, the same jar as that app’s inline frames on 'persistent', and never another app’s. A sign-in in the window therefore reaches your inline frames: close() the window, goto() the inline frame again, and it loads signed in. cookies: 'ephemeral' gives the window a jar of its own instead, which ends with the window and can start from a storageState.
Cookies are all that carry over for an app outside the fkn.app site. The proxied site’s localStorage and IndexedDB in a window are kept per window and cleared when it closes, so a site that keeps its session in storage rather than in cookies does not carry it back to the inline frame.
From the extension’s first store release after 0.1.54, cookies: 'native' opens a real browser window, a popup of the person’s own browser like one they opened themselves, on their own browser session for every site it shows. There is no FKN page in it: your page opens it with window.open on the click, the extension adopts that window by the tab the browser reports for it, and the WindowFrame follows whatever page the window holds. A sign-in there lands in the person’s own browser cookies, so an iframe attached on 'native' sees it on its next goto().
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.
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({
window: FrameWindowOptions
window: {
url?: string |undefined
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.
url: 'https://example.org/login' },
domains?: string[] |undefined
domains: ['example.org'],
cookies?: AttachCookies |undefined
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).
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.
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.
closed// the person closed it, close() ran, it reached an FKN host, or this page went away
} catch (
function (localvar) error: unknown
error) {
if (!(
function (localvar) error: unknown
errorinstanceof
classExtensionOperationUnsupportedError
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.
It follows the page. A reload or a navigation in the window, the person’s or the site’s, does not end the attachment, and reads answer only while the window is on a host the attach, domains or a goto() named, as for an iframe. closed resolves when anyone closes the window, close() runs, its page goes to an FKN host, or your page goes away; the last two leave the window open for the person. Every call after it rejects with extension.attachFrame: the attached window closed; attach a fresh window.
close() waits for nothing. The window ran on the person’s own cookies, which the browser has already written.
Messages need the window’s link to your page. The page reaches your app with opener.postMessage(x, appOrigin), and frame.postMessage() reaches it, while that link holds. A page served with a Cross-Origin-Opener-Policy cuts it as it loads, and postMessage is then refused with frame.postMessage: this window's page no longer keeps a link to the app's page. Every other call still answers, since the extension knows the window by its tab.
Consent is asked on your page. The sheet is drawn over your app’s page, as every extension sheet is, never in the window.
evaluate keeps the page’s content security policy, so a page that forbids eval refuses with a named error.
It serves what an extension iframe serves.addScriptTag() and clearCookies() are refused by name, as on an iframe, storageState needs 'ephemeral', and the cloud’s FrameWindowRefusedError never comes from it. FrameWindowBlockedError still means the browser did not open the window.
While the window is attached, the extension blocks the attached site’s requests from that tab to FKN hosts, as it does for an iframe, see what is refused. A window adopted in the first seconds after the extension was installed, before its worker ever ran, can be missed: the attach then rejects with extension.attachFrame: the window this page opened was not found and closes the window, and the next attempt is adopted.
evaluate(pageFunction, arg?) runs a function inside the 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, so it captures nothing from your scope and everything it needs comes through arg, which is structured-cloned at the call. The result comes back by structured clone:
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.
evaluate((
selector: string
selector) =>
var document:Document
window.document returns a reference to the document contained in the window.
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, LocatorUnsupportedErrorframe.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.
TimeoutErrorframe.addScriptTag: no answer within 30000ms; the script may or may not have run.
the terminal detach error once the attachment ended.
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.
evaluate(() => (
moduleglobalThis
globalThisasany).
any
greet('FKN')) // 'hello FKN', the script's declaration stayed on the page's global
addScriptTag({ content, sourceUrl? }) is Playwright’s name and its content option: it runs content as a classic inline script of the attached document, so its top-level declarations stay on the page’s global for later scripts and evaluate() calls, and the page’s own error listeners see its top-level error. sourceUrl names the script in stack traces. Playwright’s url, path and type are refused by name, is not served; it runs content as a classic inline script. It resolves undefined once the script ran, runs once and never inside the locator retry loop, and a top-level throw comes back as an Error with the page’s own name and message.
Both need the Evaluation grant (one card covers both), run on the attached frame only, never on a nested frameLocator(), and have 30 seconds to settle, counted after any consent card, before they reject with a TimeoutError, see frame.evaluate: the code did not settle within. Called after goto() resolved, either runs on the document that goto brought.
On the cloud 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 what evaluate() and addScriptTag() compile, and every string that code compiles itself is held to them, as the page’s own are. addScriptTag() is cloud only for now. The extension refuses it with ExtensionOperationUnsupportedError, operation 'addScriptTag', and runs evaluate() in the page’s main world, where an undeclared frame, and every attached window, keeps its content security policy.
postMessage(message, targetOrigin) delivers a real message event to the page, as iframe.contentWindow.postMessage would, and on(type, listener, { signal }) listens to the page. on and off are Playwright’s names, and { signal } is FKN’s addition:
app.ts
const
constlistening:AbortController
listening=new
var AbortController:new () =>AbortController
The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired.
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.
on('message', (
event: FrameMessageEvent
event) => {
event: FrameMessageEvent
event.
origin: string
The page's origin as the site knows it (https://anilist.co), never FKN's own.
origin// 'https://example.org', the page's origin as the site knows it
event: FrameMessageEvent
event.
data: unknown
data// what the page posted with parent.postMessage(data, appOrigin)
}, {
signal?: AbortSignal |undefined
signal:
constlistening:AbortController
listening.
AbortController.signal: AbortSignal
The signal read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.
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.
on('document', () => {
// a new document arrived: what evaluate installed in the last one is gone, so install it again
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.
postMessage({
type: string
type: 'hello' }, 'https://example.org') // dropped silently unless the frame's document is on that origin
constlistening:AbortController
listening.
AbortController.abort(reason?: any): void
The abort() method of the AbortController interface aborts an asynchronous operation before it has completed.
clearCookies(options?) removes cookies from the attachment’s jar, as Playwright’s browserContext.clearCookies removes them from a context. It is how an app signs a user out of a site they signed in to inside an FKN frame: that session lives in FKN’s jar, never in your app, so your app cannot remove it any other way:
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>.
LocatorDeniedErrorframe.clearCookies: this attachment reaches no site yet; attach a url, goto one, or declare it in domains.
LocatorDeniedErrorframe.clearCookies: <domain> is not on a site this attachment reaches; goto it or declare it in domains, for a string domain.
cloud, LocatorUnsupportedErrorframe.clearCookies: this FKN page predates clearCookies; reload the app to load the current one, with nothing sent.
cloud, LocatorUnsupportedErrorframe.clearCookies: this render proxy predates clearCookies; reload the app.
cloud, Errorframe.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, TimeoutErrorframe.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.
clearCookies({
name?: string |undefined
Only cookies with this name.
name: 'session',
domain?: string |undefined
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.
domain: '.example.org' }) // one cookie, as Playwright reports its domain
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>.
LocatorDeniedErrorframe.clearCookies: this attachment reaches no site yet; attach a url, goto one, or declare it in domains.
LocatorDeniedErrorframe.clearCookies: <domain> is not on a site this attachment reaches; goto it or declare it in domains, for a string domain.
cloud, LocatorUnsupportedErrorframe.clearCookies: this FKN page predates clearCookies; reload the app to load the current one, with nothing sent.
cloud, LocatorUnsupportedErrorframe.clearCookies: this render proxy predates clearCookies; reload the app.
cloud, Errorframe.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, TimeoutErrorframe.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.
clearCookies() // every cookie of every site this attachment reaches
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).
goto(
constplayer:Frame
player.
functionurl():string
The url last given to attachFrame or goto, never the frame's live location: a page that moves itself does not change it.
url()) // the same page again, signed out
With no options it removes every cookie of the sites the attachment reaches. With options it removes only the ones that match every option given: name, domain in the form Playwright reports it (.example.org for a cookie its subdomains also receive, www.example.org for a host-only one), and path, each matched exactly. Each is a string for now: a RegExp is refused with is a RegExp, and clearCookies takes strings for name, domain and path for now, since a pattern tested against cookies your app cannot read would let its running time say which of them exist. An empty string is refused where Playwright would read it as no filter.
Which jar: on 'persistent', the default, your app’s cloud jar, which every app of your top-level site shares (every fkn.app app shares one), so a removal there signs every one of those apps out of that site. On 'ephemeral', the attachment’s own jar. The seal on this device changes none of this: the jar opens with its own key, so a removal is never refused for want of one.
Which cookies: only those of a site the attachment reaches, a site being a host’s registered domain under the Public Suffix List: the attach url’s host (the empty page’s, for blank), each goto() target’s host, and domains. www.example.org reaches every *.example.org cookie and no example.net one, the rule browsers follow for Clear-Site-Data: "cookies". A host that is itself a public suffix, such as github.io, reaches only the cookies set for exactly that host. A domain off those sites is refused with is not on a site this attachment reaches, and an attachment with no site at all with frame.clearCookies: this attachment reaches no site yet.
Once it resolves, no request a document of the attachment starts carries a removed cookie, and the removal is committed to the jar, so an attachment created afterwards never sees one. Another live attachment on the same jar drops them when it hears the commit, normally within milliseconds. It resolves undefined and never says what or how much it removed, so it cannot tell your app whether the user had a session on a site, and 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.
It asks no consent card, runs once, never inside the locator retry loop, and waits at most 30,000 ms before it rejects with a TimeoutError. It is cloud only: an extension frame runs on the person’s own browser cookies, which FKN does not clear, and is refused with ExtensionOperationUnsupportedError, operation 'clearCookies'.
frame.fetch(url, init?) issues a request from inside the framed document, with that document’s cookies, Origin and Referer, and resolves with the whole response. It lives on the Frame and after frameLocator(), never on an element locator. Two refusals can meet it, one per backend, and they do not share a shape:
app.ts
// the default, 'persistent', runs on the cloud's shared jar, where frame.fetch is refused
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({
iframe: HTMLIFrameElement
iframe:
var document:Document
window.document returns a reference to the document contained in the window.
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).
goto('https://example.org/')
try {
// GET, HEAD and OPTIONS ask for frame.fetchRead, every other method for frame.fetchWrite
status// 200 when the catalog answered, and on the extension one receipt row in the activity log
varJSON:JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON.
JSON.parse(text: string, reviver?: (this:any, key:string, value:any) => any): any
Converts a JavaScript Object Notation (JSON) string into an object.
@param ― text A valid JSON string.
@param ― reviver A function that transforms the results. This function is called for each member of the object.
If a member contains nested objects, the nested objects are transformed before the parent object is.
@throws ― {SyntaxError} If text is not valid JSON.
parse(new
var TextDecoder:new (label?:string, options?:TextDecoderOptions) =>TextDecoder
The TextDecoder interface represents a decoder for a specific text encoding, such as UTF-8, ISO-8859-2, KOI8-R, GBK, etc.
Neither exported guard matches the first branch. A refusal on the extension’s sheet arrives under the name PermissionDeniedError with Permission denied: network on <host> (frame.fetchRead this frame), so test error.name first, as a refusal neither guard matches shows. The second is the cloud’s, a terminal LocatorDeniedError the library raises itself. This attach meets it there at once, because the default cookies is the shared cookie jar.
The result is { status, statusText, ok, url, redirected, type, headers, body }, with headers as [name, value] pairs and body an ArrayBuffer. Its type is not exported by name (TypeScript). method defaults to GET, redirect to 'follow' and credentials to 'include'. There is no signal, and the body arrives whole.
From the first extension store release after 0.1.54, a fetch that fails once it may have been sent, on a network error, a cross-origin read the browser refused, or a redirect hop onto an FKN host the extension blocked, ends the call with the terminal frame.fetch: <error>; not retried, since the request may already have been sent. It is not sent again every 50 ms to the deadline, so a POST goes out at most once.
On the extension the two keys, frame.fetchRead and frame.fetchWrite, sit at severity 3 and both belong to Network, so one row covers reading and writing, see permission keys. The reason you pass is shown on that row, with the chip load or send naming which one is needed now. Every call writes one receipt row to the activity log, and a failing call is written once per identical call per 60,000 ms window, so the retries do not multiply rows.
On the cloud the call needs all four of these, and each miss is a terminal LocatorDeniedError the library raises before anything crosses:
FrameWindowBlockedError: the call to open a window ran without the click’s activation, or popups are blocked.
The platform rule runs on the src at attach and on every goto() target, page-side and again inside the extension’s content script and the fkn.app page that hosts the render proxy. The full list is on every error.
From the first extension store release after 0.1.54, the attached site cannot reach a platform host either. Before the extension hands the frame to your app, and before each goto() navigates it, it installs browser rules in your app’s tab, so a request the site sends to an FKN host fails as a network error: its own fetch, a WebSocket, code evaluate() ran there, a frame inside it, or a redirect hop of a frame.fetch. Requests from the shared and service workers that site starts are blocked the same way, with the exceptions on limitations. Your app’s own requests are untouched, so an app served from an FKN domain attaches and fetches as before, and a browser that refuses the rules ends the call with could not install the rule that keeps an attached site off FKN platform domains.
A window on the extension gets the same rules on its own tab, armed before the attach resolves and kept across every page the window shows until it closes; a window whose rules the browser refuses is closed, with so the window was closed.
The install card that extension.attachFrame() opens stays up until the user dismisses it, and the call waits with it unless setMissingExtensionHandler(null) removed it. The broker’s card for frame.fetch() on the cloud backend waits the same way, and a dismissal there starts a 10,000 ms cooldown during which the call fails closed. The consent sheet is raised before the timer starts, so it never counts against a deadline. A locator action’s own deadline is under options, and the other knobs are in limits and timeouts.
The demo below runs on the extension backend only. It waits for the extension, shows an install hint until it is exposed, then attaches a same-origin mock player with cookies: 'native' and no domains, so no header rule and no cookie copy. Six buttons drive it, and every rejection is caught and shown, since a denial or a timeout is a normal outcome:
a click on play, .controls then #play
a read of the title
a fill of the search box
a frame.requestPermissions() for Interaction, one row covering the whole page
two clicks inside .controls, covered by that row
a click outside the box, covered by the same row, since the grant names the site
Revoke a grant from the extension’s popup or dashboard to see the sheet come back. On an extension below ABI 2 the fourth button falls back to the pre-category ask, and the click outside the box then prompts again. The same six buttons are walked from the locator side in locators and actions.