You can let a user sign in to a site in a window of its own and bring that session back to the copy of the site your app shows inline. This page takes an inline player that shows signed out to the same player signed in, through one click, one window and one reload.
The end state is the inline frame showing the signed-in page, with the window closed by your app. What the recipe costs:
Needs nothing installed, and a page not served with Cross-Origin-Opener-Policy: same-origin
Proven by no shipped integration yet. The production check behind @fkn/lib 0.9.35 drove the window half from a cross-site page: the attach, a locator read, close() and closed
Every step carries the [Page] badge, defined on recipes, because a window opens only from a window realm, a JavaScript execution context with a window.
A window runs on the cloud backend only, and a sign-in in it reaches the frames that share its cookie jar, which are your app’s inline frames on cookies: 'persistent'. That is the default, on the root call and on cloud.attachFrame() alike, and pinning the cloud says so at the call site:
app.ts
const
constiframe:HTMLIFrameElement
iframe=
var document:Document
window.document returns a reference to the document contained in the window.
In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.
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,
domains?: string[] |undefined
domains: ['example.org'] }) // the app's cloud cookie jar
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).
exists() // true, the jar holds no example.org session yet
cookies stays at its default, 'persistent', which is the jar a window opened by this app uses too. With 'ephemeral' the frame gets a jar of its own and no window sign-in reaches it, and with 'native' it runs on the extension and the person’s own browser session, see the window’s cookie jar.
cloud.attachFrame({ window }) has to be the first call in the click handler. The window opens before the call’s first await, on the click’s activation, and anything awaited before it can cost the window:
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 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({
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', 'accounts.example.org'], // every host the sign-in passes through
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 sign in again'// nothing opened
}
})
A popup blocker, a missing user gesture and a frame sandboxed without allow-popups all reject with FrameWindowBlockedError and open nothing. A page served with Cross-Origin-Opener-Policy: same-origin loses the window as it opens, and the attach rejects with cloud.attachFrame: the window closed before it connected, so serve it with same-origin-allow-popups.
A read at severity 0 never asks, so exists() can poll the window for something only a signed-in page shows. Race it against closed, which resolves whoever ends the window, the user included:
app.ts
// true once the signed-in marker shows, false when the window ends first
The Frame of an attachment that lives in its own window.
On the extension (cookies: 'native') the window is a real browser popup the app's page opens with
window.open, whose page is the site itself: there is no FKN page in it. Where that differs:
The Frame follows the window's page wherever it goes, as it follows an iframe's, and its reads
answer only inside the attachment. A page that goes to an FKN host ends the attachment (closed).
postMessage and the message event need the window to keep its link to the app's page. A page
served with Cross-Origin-Opener-Policy (same-origin, same-origin-allow-popups) cuts it as it
loads, and postMessage is then the LocatorDeniedErrorframe.postMessage: this window's page no longer keeps a link to the app's page, so a message cannot reach it. Everything else still
answers: the extension knows the window by its tab, never by that link. The page reaches the app
with opener.postMessage(x, appOrigin) while the link holds.
Its consent sheets are drawn on the app's page, not in the window.
evaluate keeps the window page's content security policy (an attached iframe on a declared host
has it replaced), so a page that forbids eval refuses with a named error.
Calls after closed reject with the terminal LocatorUnsupportedErrorextension.attachFrame: the attached window closed; attach a fresh window.
The extension never throws FrameWindowRefusedError, which is the cloud's.
new <unknown>(executor: (resolve: (value:unknown) =>void, reject: (reason?:any) =>void) =>void) =>Promise<unknown>
Creates a new Promise.
@param ― executor A callback used to initialize the promise. This callback is passed two arguments:
a resolve callback used to resolve the promise with a value or the result of another promise,
and a reject callback used to reject the promise with a provided reason or error.
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.
The Frame of an attachment that lives in its own window.
On the extension (cookies: 'native') the window is a real browser popup the app's page opens with
window.open, whose page is the site itself: there is no FKN page in it. Where that differs:
The Frame follows the window's page wherever it goes, as it follows an iframe's, and its reads
answer only inside the attachment. A page that goes to an FKN host ends the attachment (closed).
postMessage and the message event need the window to keep its link to the app's page. A page
served with Cross-Origin-Opener-Policy (same-origin, same-origin-allow-popups) cuts it as it
loads, and postMessage is then the LocatorDeniedErrorframe.postMessage: this window's page no longer keeps a link to the app's page, so a message cannot reach it. Everything else still
answers: the extension knows the window by its tab, never by that link. The page reaches the app
with opener.postMessage(x, appOrigin) while the link holds.
Its consent sheets are drawn on the app's page, not in the window.
evaluate keeps the window page's content security policy (an attached iframe on a declared host
has it replaced), so a page that forbids eval refuses with a named error.
Calls after closed reject with the terminal LocatorUnsupportedErrorextension.attachFrame: the attached window closed; attach a fresh window.
The extension never throws FrameWindowRefusedError, which is the cloud's.
Ends the attachment and closes the window. On the cloud backend it first waits, at most 2
seconds, for the window's cookie changes to be committed to its jar, so a goto on the app's
inline frame right after sees the session. On the extension the window ran on the person's own
browser cookies, which are already written, so an inline 'native' frame sees a sign-in on its
next goto. Idempotent, and never rejects.
close() // commits the window's cookies to the app's jar, then closes it
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, now with the session
}
close() waits at most 2,000 ms for the window’s cookie changes to reach the jar before it closes the window, so the goto() right after it sees the session. It is idempotent and never rejects, and closed resolves once it has run.
For an app outside the fkn.app site, cookies are all that come back. A site that keeps its session in localStorage or IndexedDB signs the user in to the window alone, because the window’s site storage is its own and is cleared when it closes.
The inline player shows the signed-in page, and the window is gone. When it does not, take these in order:
FrameWindowBlockedError means something ran an await before the attach, or the page’s popups are blocked. Move the call to the top of the handler, then ask the user to allow popups.
A window that signed in beside a player that still shows signed out means the player is not on the window’s jar: it was attached with cookies: 'native' or 'ephemeral', or the site keeps its session in storage rather than in cookies.
When the sign-in address needs an await, open the window with window: {} so the click’s activation goes to the window, then goto() the address once it is known:
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 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({
window: FrameWindowOptions
window: {},
domains?: string[] |undefined
domains: ['example.org', 'accounts.example.org'] }) // blank, on the click's activation
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).
The session the window brought back lives in your app’s cloud jar, never in your app, so ending it is clearCookies() on the player, followed by the same page again:
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.
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({
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' }) // the example.org cookies its subdomains also receive
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
})
The jar is shared by every app of your top-level site, so this signs each of them out of example.org too. It removes only the cookies of the sites the player reaches, and never tells the site, so the session stays valid there until it lapses, see clearing cookies.