Skip to content

Sign a user in through a window

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:

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
const iframe: HTMLIFrameElement
iframe
=
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
Document.createElement<"iframe">(tagName: "iframe", options?: ElementCreationOptions): HTMLIFrameElement (+2 overloads)

In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.

MDN Reference

createElement
('iframe')
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<Element>(selectors: string): Element | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
('#player-slot')!.
ParentNode.append(...nodes: (Node | string)[]): void

Inserts nodes after the last child of node, while replacing strings in nodes with equivalent Text nodes.

Throws a "HierarchyRequestError" DOMException if the constraints of the node tree are violated.

MDN Reference

append
(
const iframe: HTMLIFrameElement
iframe
) // in the document, in its final place, before the attach
const
const player: Frame
player
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.attachFrame(options: AttachFrameOptions): Promise<Frame> (+1 overload)
export cloud_d_exports.attachFrame

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
await
const player: Frame
player
.
function goto(url: string, options?: GotoOptions): Promise<void>

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

goto
('https://example.org/watch')
await
const player: Frame
player
.
locator: (selector: string) => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
locator
('.sign-in-prompt').
exists: (_options?: LocatorOptions | undefined) => Promise<boolean>
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:

app.ts
const signIn: HTMLButtonElement
signIn
.
HTMLButtonElement.addEventListener<"click">(type: "click", listener: (this: HTMLButtonElement, ev: PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('click', async () => {
try {
const
const login: WindowFrame
login
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.attachFrame(options: AttachWindowOptions): Promise<WindowFrame> (+1 overload)
export cloud_d_exports.attachFrame

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
})
await
const finishSignIn: (login: WindowFrame) => Promise<void>
finishSignIn
(
const login: WindowFrame
login
) // the next two steps
} catch (
function (local var) error: unknown
error
) {
if (!(
function (local var) error: unknown
error
instanceof
class FrameWindowBlockedError

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.

FrameWindowBlockedError
)) throw
function (local var) error: unknown
error
const signIn: HTMLButtonElement
signIn
.
Element.textContent: string | null
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
const
const waitForSignIn: (login: WindowFrame) => Promise<boolean>
waitForSignIn
= (
login: WindowFrame
login
:
type WindowFrame = Omit<FrameLocator, "owner"> & {
goto(url: string, options?: GotoOptions): Promise<void>;
url(): string;
backend(): "cloud" | "extension" | undefined;
requestPermissions(requests: CategoryRequest[]): Promise<CategoryGrant[]>;
evaluate<R = unknown, A = undefined>(pageFunction: ((arg: A) => R | Promise<R>) | string, arg?: A): Promise<Awaited<R>>;
addScriptTag(options: AddScriptTagOptions): Promise<void>;
clearCookies(options?: ClearCookiesOptions): Promise<void>;
postMessage(message: unknown, targetOrigin: string, transfer?: Transferable[]): Promise<void>;
postMessage(message: unknown, options?: FramePostMessageOptions): Promise<void>;
on<K extends keyof FrameEventMap>(type: K, listener: FrameEventListener<K>, options?: {
signal?: AbortSignal;
}): void;
off<K extends keyof FrameEventMap>(type: K, listener: FrameEventListener<K>): void;
} & {
...;
}

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

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

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

WindowFrame
):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<boolean> => {
const
const poll: () => Promise<boolean>
poll
= async ():
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<boolean> => {
try {
while (!(await
login: WindowFrame
login
.
locator: (selector: string) => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
locator
('#account-menu').
exists: (_options?: LocatorOptions | undefined) => Promise<boolean>
exists
())) { // severity 0, never a card
await new
var Promise: PromiseConstructor
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.

Promise
(
resolve: (value: unknown) => void
resolve
=>
function setTimeout<[value: unknown]>(callback: (value: unknown) => void, delay?: number, value?: unknown): NodeJS.Timeout (+1 overload)
setTimeout
(
resolve: (value: unknown) => void
resolve
, 1_000))
}
return true
} catch (
function (local var) error: unknown
error
) {
if (
function isLocatorUnsupported(error: unknown): boolean
isLocatorUnsupported
(
function (local var) error: unknown
error
)) return false // the window ended under the call
throw
function (local var) error: unknown
error
}
}
return
var Promise: PromiseConstructor

Represents the completion of an asynchronous operation

Promise
.
PromiseConstructor.race<[Promise<boolean>, Promise<boolean>]>(values: [Promise<boolean>, Promise<boolean>]): Promise<boolean> (+1 overload)

Creates a Promise that is resolved or rejected when any of the provided Promises are resolved or rejected.

@param ― values An array of Promises.

@returns ― A new Promise.

race
([
const poll: () => Promise<boolean>
poll
(),
login: WindowFrame
login
.
closed: Promise<void>

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

closed
.
Promise<void>.then<boolean, never>(onfulfilled?: ((value: void) => boolean | PromiseLike<boolean>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<boolean>

Attaches callbacks for the resolution and/or rejection of the Promise.

@param ― onfulfilled The callback to execute when the Promise is resolved.

@param ― onrejected The callback to execute when the Promise is rejected.

@returns ― A Promise for the completion of which ever callback is executed.

then
(() => false)])
}

Every host the sign-in passes through belongs in domains. A read while the window is on a host outside them, the attach target and your goto() targets is refused with the terminal frame: this frame no longer holds the document the app attached it to, which the catch passes on rather than reading as a closed window.

Once the marker shows, close the window yourself and load the inline page again:

app.ts
const
const finishSignIn: (login: WindowFrame) => Promise<void>
finishSignIn
= async (
login: WindowFrame
login
:
type WindowFrame = Omit<FrameLocator, "owner"> & {
goto(url: string, options?: GotoOptions): Promise<void>;
url(): string;
backend(): "cloud" | "extension" | undefined;
requestPermissions(requests: CategoryRequest[]): Promise<CategoryGrant[]>;
evaluate<R = unknown, A = undefined>(pageFunction: ((arg: A) => R | Promise<R>) | string, arg?: A): Promise<Awaited<R>>;
addScriptTag(options: AddScriptTagOptions): Promise<void>;
clearCookies(options?: ClearCookiesOptions): Promise<void>;
postMessage(message: unknown, targetOrigin: string, transfer?: Transferable[]): Promise<void>;
postMessage(message: unknown, options?: FramePostMessageOptions): Promise<void>;
on<K extends keyof FrameEventMap>(type: K, listener: FrameEventListener<K>, options?: {
signal?: AbortSignal;
}): void;
off<K extends keyof FrameEventMap>(type: K, listener: FrameEventListener<K>): void;
} & {
...;
}

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

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

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

WindowFrame
) => {
if (!(await
const waitForSignIn: (login: WindowFrame) => Promise<boolean>
waitForSignIn
(
login: WindowFrame
login
))) return // the user closed the window, and the player stays signed out
await
login: WindowFrame
login
.
function close(): Promise<void>

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

close
() // commits the window's cookies to the app's jar, then closes it
await
const player: Frame
player
.
function goto(url: string, options?: GotoOptions): Promise<void>

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

goto
(
const player: Frame
player
.
function url(): string

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

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:

  1. 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.
  2. cloud.attachFrame: the window closed before it connected on every click is the page’s Cross-Origin-Opener-Policy. Serve it with same-origin-allow-popups.
  3. frame: this frame no longer holds the document the app attached it to from the poll means the sign-in went through a host missing from domains. Add it.
  4. An ExtensionOperationUnsupportedError with operation 'attachWindow', attachFrame: a window on cookies: 'native' needs the FKN WebExtension or The FKN WebExtension does not support "attachWindow", means the window was asked for with cookies: 'native' where no extension serves one. This recipe’s player is on the cloud jar, so leave cookies at its default for the window and the player alike. A window on 'native' signs in to the person’s own browser session instead, see a window on the extension.
  5. 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:

app.ts
const signIn: HTMLButtonElement
signIn
.
HTMLButtonElement.addEventListener<"click">(type: "click", listener: (this: HTMLButtonElement, ev: PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('click', async () => {
const
const login: WindowFrame
login
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.attachFrame(options: AttachWindowOptions): Promise<WindowFrame> (+1 overload)
export cloud_d_exports.attachFrame

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
await
const login: WindowFrame
login
.
function goto(url: string, options?: GotoOptions): Promise<void>

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

goto
(await
const loginUrl: () => Promise<string>
loginUrl
()) // the address that needed an await
})

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:

app.ts
const signOut: HTMLButtonElement
signOut
.
HTMLButtonElement.addEventListener<"click">(type: "click", listener: (this: HTMLButtonElement, ev: PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('click', async () => {
await
const player: Frame
player
.
function clearCookies(options?: ClearCookiesOptions): Promise<void>

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

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

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

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

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

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

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

Refused:

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

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
await
const player: Frame
player
.
function goto(url: string, options?: GotoOptions): Promise<void>

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

goto
(
const player: Frame
player
.
function url(): string

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

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.