Skip to content

TypeScript

Most of the types you will reach for are the types of values you already hold: Frame is what attachFrame resolves to, Locator is what locator() returns, and QuotaStatus is what cloud.quota() resolves to. This page covers which entry point exports each name, how to spell the shapes @fkn/lib declares but does not export, what the broker’s own vocabulary in @fkn/lib/contract is for (the broker is the connection your app holds into FKN), and how to tell an error you can test with instanceof from one you match by message.

The three you will meet first, with the entry each comes from:

app.ts
import {
const attachFrame: AttachFrameFunction

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
,
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
import type {
type Frame = 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;
}

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

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

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

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

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

Frame
,
type Locator = {
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 ...

Elements in an attached frame, found again on every call. Playwright's members where FKN has Playwright's meaning, plus exists, videoElement and the reason option. last() is the last match; contentFrame() enters the iframe a trailing locator(selector) matches, as frameLocator(selector) would, and is a TypeError on a locator that ends in anything else.

Locator
} from '@fkn/lib'
import type {
type QuotaStatus = {
premium: boolean;
overQuota: boolean;
throttled: boolean;
usedBytes: number;
limitBytes: number;
remainingBytes: number;
bytesPerSecond: number;
bitsPerSecond: number;
}
QuotaStatus
} from '@fkn/lib/cloud'
const
const frame: Frame
frame
:
type Frame = 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;
}

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

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

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

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

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

Frame
= await
function attachFrame(options: AttachFrameOptions): Promise<Frame> (+1 overload)

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.

MDN Reference

document
.
ParentNode.querySelector<"iframe">(selectors: "iframe"): HTMLIFrameElement | null (+4 overloads)

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

MDN Reference

querySelector
('iframe')! }) // the cloud backend, on the default cookies: 'persistent'
const
const rows: Locator
rows
:
type Locator = {
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 ...

Elements in an attached frame, found again on every call. Playwright's members where FKN has Playwright's meaning, plus exists, videoElement and the reason option. last() is the last match; contentFrame() enters the iframe a trailing locator(selector) matches, as frameLocator(selector) would, and is a TypeError on a locator that ends in anything else.

Locator
=
const frame: Frame
frame
.
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
('.result') // a chain, nothing touches the page yet
const
const quota: cloud.QuotaStatus
quota
:
type QuotaStatus = {
premium: boolean;
overQuota: boolean;
throttled: boolean;
usedBytes: number;
limitBytes: number;
remainingBytes: number;
bytesPerSecond: number;
bitsPerSecond: number;
}
QuotaStatus
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.quota(): Promise<QuotaStatus>
export cloud_d_exports.quota
quota
()
const
const total: number
total
= await
const rows: Locator
rows
.
count: (_options?: LocatorOptions | undefined) => Promise<number>
count
() // the first call that touches the page
const quota: cloud.QuotaStatus
quota
.
throttled: boolean

transfers are actually being rate-limited right now (overQuota and not premium)

throttled
// false while today's free volume is left

Each annotation names a type the call already infers, so the imports are the only new thing. The namespaces carry their types too, so cloud.QuotaStatus, fs.Stats and packages.PackagesError all resolve from a root import. A named export usually has an entry in the generated API reference, the pages built from the declarations @fkn/lib ships. A shape declared under no exported name never does, and this page shows how to derive each of those.

Frame, Locator and FrameLocator, what frameLocator() returns, come from the root and from @fkn/lib/extension, with AttachFrameOptions, GotoOptions, RemoteVideoElement and VideoElementState beside them. AttachCookies is the union cookies takes, BlankPage the shape of blank, StorageState and its StorageStateCookie the shape of storageState, and AddScriptTagOptions and ClearCookiesOptions what addScriptTag() and clearCookies() take. The window form adds AttachWindowOptions, FrameWindowOptions, WindowFrame and the FrameWindowRefusal union. The option and result shapes are declared but not exported by name, so derive them from the members that take or return them:

app.ts
import type {
type Frame = 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;
}

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

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

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

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

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

Frame
,
type Locator = {
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 ...

Elements in an attached frame, found again on every call. Playwright's members where FKN has Playwright's meaning, plus exists, videoElement and the reason option. last() is the last match; contentFrame() enters the iframe a trailing locator(selector) matches, as frameLocator(selector) would, and is a TypeError on a locator that ends in anything else.

Locator
} from '@fkn/lib'
type
type ReadOptions = OperationOptions & {
reason?: string;
}
ReadOptions
=
type NonNullable<T> = T & {}

Exclude null and undefined from T

NonNullable
<
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never

Obtain the parameters of a function type in a tuple

Parameters
<
type Locator = {
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 ...

Elements in an attached frame, found again on every call. Playwright's members where FKN has Playwright's meaning, plus exists, videoElement and the reason option. last() is the last match; contentFrame() enters the iframe a trailing locator(selector) matches, as frameLocator(selector) would, and is a TypeError on a locator that ends in anything else.

Locator
['textContent']>[0]> // { timeout?: number; reason?: string }
type
type ClickOptions = OperationOptions & {
reason?: string;
} & {
relativePosition?: {
x?: number;
y?: number;
};
}
ClickOptions
=
type NonNullable<T> = T & {}

Exclude null and undefined from T

NonNullable
<
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never

Obtain the parameters of a function type in a tuple

Parameters
<
type Locator = {
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 ...

Elements in an attached frame, found again on every call. Playwright's members where FKN has Playwright's meaning, plus exists, videoElement and the reason option. last() is the last match; contentFrame() enters the iframe a trailing locator(selector) matches, as frameLocator(selector) would, and is a TypeError on a locator that ends in anything else.

Locator
['click']>[0]> // the same plus relativePosition?: { x?: number; y?: number }
type
type FrameFetchResult = {
status: number;
statusText: string;
ok: boolean;
url: string;
redirected: boolean;
type: ResponseType;
headers: [string, string][];
body: ArrayBuffer;
}
FrameFetchResult
=
type Awaited<T> = T extends null | undefined ? T : T extends object & {
then(onfulfilled: infer F, ...args: infer _): any;
} ? F extends (value: infer V, ...args: infer _) => any ? Awaited<V> : never : T

Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to never. This emulates the behavior of await.

Awaited
<
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

Obtain the return type of a function type

ReturnType
<
type Frame = 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;
}

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

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

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

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

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

Frame
['fetch']>> // { status, statusText, ok, url, redirected, type, headers: [string, string][], body: ArrayBuffer }
const
const readAll: (list: Locator, options?: ReadOptions) => Promise<string[]>
readAll
= async (
list: Locator
list
:
type Locator = {
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 ...

Elements in an attached frame, found again on every call. Playwright's members where FKN has Playwright's meaning, plus exists, videoElement and the reason option. last() is the last match; contentFrame() enters the iframe a trailing locator(selector) matches, as frameLocator(selector) would, and is a TypeError on a locator that ends in anything else.

Locator
,
options: OperationOptions & {
reason?: string;
}
options
:
type ReadOptions = OperationOptions & {
reason?: string;
}
ReadOptions
= {}):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<string[]> => {
const
const total: number
total
= await
list: Locator
list
.
count: (_options?: LocatorOptions | undefined) => Promise<number>
count
()
const
const out: string[]
out
: string[] = []
for (let
let i: number
i
= 0;
let i: number
i
<
const total: number
total
;
let i: number
i
++)
const out: string[]
out
.
Array<string>.push(...items: string[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@param ― items New elements to add to the array.

push
(await
list: Locator
list
.
nth: (index: number) => 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 ...
nth
(
let i: number
i
).
textContent: (_options?: LocatorOptions | undefined) => Promise<string>
textContent
(
options: OperationOptions & {
reason?: string;
}
options
))
return
const out: string[]
out
}
const
const pressRight: (button: Locator, options?: ClickOptions) => Promise<void>
pressRight
= (
button: Locator
button
:
type Locator = {
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 ...

Elements in an attached frame, found again on every call. Playwright's members where FKN has Playwright's meaning, plus exists, videoElement and the reason option. last() is the last match; contentFrame() enters the iframe a trailing locator(selector) matches, as frameLocator(selector) would, and is a TypeError on a locator that ends in anything else.

Locator
,
options: OperationOptions & {
reason?: string;
} & {
relativePosition?: {
x?: number;
y?: number;
};
}
options
:
type ClickOptions = OperationOptions & {
reason?: string;
} & {
relativePosition?: {
x?: number;
y?: number;
};
}
ClickOptions
= {}) =>
button: Locator
button
.
click: (options?: PositionOptions | undefined) => Promise<void>
click
({ ...
options: OperationOptions & {
reason?: string;
} & {
relativePosition?: {
x?: number;
y?: number;
};
}
options
,
relativePosition?: {
x?: number;
y?: number;
} | undefined

Where the pointer lands, as a 0..1 fraction of the element's box from its top-left corner: x across, y down, each 0.5 when left out, so the centre when absent. FKN's name, since Playwright's position is in pixels, which no backend serves yet: passing position is a TypeError, before anything is sent.

relativePosition
: {
x?: number | undefined
x
: 0.9,
y?: number | undefined
y
: 0.5 } }) // fractions of the element's box, so this lands near its right edge
const
const text: (result: FrameFetchResult) => string
text
= (
result: FrameFetchResult
result
:
type FrameFetchResult = {
status: number;
statusText: string;
ok: boolean;
url: string;
redirected: boolean;
type: ResponseType;
headers: [string, string][];
body: ArrayBuffer;
}
FrameFetchResult
): string => 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.

MDN Reference

TextDecoder
().
TextDecoder.decode(input?: AllowSharedBufferSource, options?: TextDecodeOptions): string

The TextDecoder.decode() method returns a string containing text decoded from the buffer passed as a parameter.

MDN Reference

decode
(
result: FrameFetchResult
result
.
body: ArrayBuffer
body
) // the whole body arrived as one ArrayBuffer

Spelling a type is worth it when the type goes back into a signature, and each of the three does here. textContent() is typed Promise<string>, never string | null. An Element’s textContent is never null at runtime either, unlike a Node’s.

The root Frame has no click, getByRole or first, since those are element members and appear only after the first locator(). Those members, and Locator itself, which has no entry of its own in the generated API reference, are on locators and actions. What attachFrame takes is on frames.

PermissionRequest is a union of two shapes, both from the root and from @fkn/lib/extension. A category ask is { category, hosts, reason? }, or { key, hosts, reason? } where the key stands for its own category, and hosts is what makes an item one. The pre-category ask is { key, scope?, reason? }, which carries no hosts and is forwarded to any extension version. PermissionGrant mirrors the split: { category, key?, hosts, allow } for the first, { key, scope, site, allow } for the second. permissions runs on the extension backend: the call is carried out by the FKN browser extension, not by the cloud. Every permissions.request therefore waits for the extension to expose itself.

On a page without the extension, the missing-extension handler runs first (the install card by default), and then the call rejects with The FKN WebExtension is not installed, enabled or not exposed on this page.. setMissingExtensionHandler(null) removes that card. The rejection then lands after the waitForExtensionExposure wait instead of after a dismissal, so the call is bounded.

PermissionCategory is exported. The union of the twenty keys is not, and PermissionRequest['key'] no longer reaches it, since the category member carries no key, so extract the member that does:

app.ts
import {
const permissions: {
request: (requests: PermissionRequest[]) => Promise<PermissionGrant[]>;
}
permissions
,
const readExtensionHandshake: () => ExtensionHandshake

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

readExtensionHandshake
,
const setMissingExtensionHandler: (handler: MissingExtensionHandler | null) => void
setMissingExtensionHandler
} from '@fkn/lib'
import type {
type PermissionCategory = "interaction" | "storage" | "network" | "evaluation"

The four categories a grant is asked and stored under. Every descriptor names one, so a key without a category does not compile. Category ids carry no dot and registry keys always do, so the two can share the grant key column without colliding.

PermissionCategory
,
type PermissionGrant = CategoryAnswer | {
key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "frame.evaluate" | "frame.message" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders";
scope: string;
site: string;
allow: boolean;
}

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

PermissionGrant
,
type PermissionRequest = (CategoryRequest & {
hosts: string[];
}) | LegacyPermissionRequest

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

PermissionRequest
} from '@fkn/lib'
type
type PermissionKey = "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "frame.evaluate" | "frame.message" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"
PermissionKey
=
type Extract<T, U> = T extends U ? T : never

Extract from T those types that are assignable to U

Extract
<
type PermissionRequest = (CategoryRequest & {
hosts: string[];
}) | LegacyPermissionRequest

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

PermissionRequest
, {
key: unknown
key
: unknown }>['key'] // 'read.text' | 'read.info' | 'read.visible' | ... twenty keys
const
const categories: PermissionCategory[]
categories
:
type PermissionCategory = "interaction" | "storage" | "network" | "evaluation"

The four categories a grant is asked and stored under. Every descriptor names one, so a key without a category does not compile. Category ids carry no dot and registry keys always do, so the two can share the grant key column without colliding.

PermissionCategory
[] = ['storage', 'interaction']
const
const keys: ("read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "frame.evaluate" | "frame.message" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders")[]
keys
:
type PermissionKey = "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "frame.evaluate" | "frame.message" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"
PermissionKey
[] = ['read.text', 'act.click']
const
const handshake: ExtensionHandshake
handshake
=
function readExtensionHandshake(): ExtensionHandshake

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

readExtensionHandshake
() // `abi` is absent on a status of 'absent', so read it through the narrowing
const
const requests: PermissionRequest[]
requests
:
type PermissionRequest = (CategoryRequest & {
hosts: string[];
}) | LegacyPermissionRequest

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

PermissionRequest
[] =
const handshake: ExtensionHandshake
handshake
.
status: "absent" | "ok" | "outdated"
status
!== 'absent' &&
const handshake: {
status: "ok";
abi: number;
operations: readonly string[] | null;
} | {
status: "outdated";
abi: number;
required: number;
operations: readonly string[] | null;
}
handshake
.
abi: number
abi
>= 2
?
const categories: PermissionCategory[]
categories
.
Array<PermissionCategory>.map<{
category: PermissionCategory;
hosts: string[];
reason: string;
}>(callbackfn: (value: PermissionCategory, index: number, array: PermissionCategory[]) => {
category: PermissionCategory;
hosts: string[];
reason: string;
}, thisArg?: any): {
category: PermissionCategory;
hosts: string[];
reason: string;
}[]

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
(
category: PermissionCategory
category
=> ({
category: PermissionCategory
category
,
hosts: string[]
hosts
: ['example.org'],
reason: string
reason
: 'Read the results and press Search for you' }))
:
const keys: ("read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "frame.evaluate" | "frame.message" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders")[]
keys
.
Array<"read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | ... 10 more ... | "network.modifyRequestHeaders">.map<{
key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "frame.evaluate" | "frame.message" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders";
scope: string;
reason: string;
}>(callbackfn: (value: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | ... 15 more ... | "network.modifyRequestHeaders", index: number, array: ("read.text" | ... 20 more ... | "network.modifyRequestHeaders")[]) => {
key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "frame.evaluate" | "frame.message" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders";
scope: string;
reason: string;
}, thisArg?: any): {
key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "frame.evaluate" | "frame.message" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders";
scope: string;
reason: string;
}[]

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
(
key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "frame.evaluate" | "frame.message" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"
key
=> ({
key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "frame.evaluate" | "frame.message" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders"
key
,
scope: string
scope
: '*',
reason: string
reason
: 'Read the results and press Search for you' }))
function setMissingExtensionHandler(handler: MissingExtensionHandler | null): void
setMissingExtensionHandler
(null) // a page without the extension rejects after the wait instead of opening the install card
const
const grants: PermissionGrant[]
grants
:
type PermissionGrant = CategoryAnswer | {
key: "read.text" | "read.info" | "read.visible" | "read.check" | "read.count" | "act.click" | "act.type" | "act.hover" | "media.video" | "media.appear" | "media.appearU" | "frame.evaluate" | "frame.message" | "embed.iframe" | "embed.open" | "frame.fetchRead" | "frame.fetchWrite" | "network.fetch" | "network.fetchCredentialed" | "network.fetchLocal" | "network.readCookie" | "network.modifyRequestHeaders";
scope: string;
site: string;
allow: boolean;
}

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

PermissionGrant
[] = await
const permissions: {
request: (requests: PermissionRequest[]) => Promise<PermissionGrant[]>;
}
permissions
.
request: (requests: PermissionRequest[]) => Promise<PermissionGrant[]>

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

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

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

request
(
const requests: PermissionRequest[]
requests
) // one grant per request, in the order asked

A category ask needs an extension that serves categories and rejects with ExtensionOutdatedError below ABI 2, which is why the branch reads the handshake first. The pre-category form needs no check. The consent sheet is what the extension shows the user before it carries out a gated action. What each key gates, what the scope means and what the sheet shows are on permissions and consent.

An account is the FKN identity a person carries between sites. AccountInfo is what account.info() resolves to, or null when no account is connected. It comes from @fkn/lib/account and holds { name: string, image: string | null, premium: boolean, premiumUntil: number | null } and nothing else: no id and no email, by design.

QuotaStatus is what cloud.quota() resolves to, from @fkn/lib/cloud. It holds the three booleans premium, overQuota and throttled, and the five numbers usedBytes, limitBytes, remainingBytes, bytesPerSecond and bitsPerSecond, each defined on account and quota.

The example reads throttled and bitsPerSecond, the two fields a rate label needs:

app.ts
import {
(alias) namespace account
import account
account
,
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
import type {
type AccountInfo = {
name: string;
image: string | null;
premium: boolean;
premiumUntil: number | null;
}
AccountInfo
} from '@fkn/lib/account'
const
const label: (info: AccountInfo | null) => string
label
= (
info: account.AccountInfo | null
info
:
type AccountInfo = {
name: string;
image: string | null;
premium: boolean;
premiumUntil: number | null;
}
AccountInfo
| null): string =>
info: account.AccountInfo | null
info
? `${
info: account.AccountInfo
info
.
name: string

the account's display name (a username, not necessarily a real name)

name
}${
info: account.AccountInfo
info
.
premium: boolean

the account has an active paid subscription

premium
? ' (premium)' : ''}` : 'Not connected'
const
const who: string
who
=
const label: (info: AccountInfo | null) => string
label
(await
(alias) namespace account
import account
account
.
account_d_exports.info(): Promise<AccountInfo | null>
export account_d_exports.info
info
()) // 'Not connected' until the user connected this site
const {
const throttled: boolean

transfers are actually being rate-limited right now (overQuota and not premium)

throttled
,
const bitsPerSecond: number

current effective egress rate cap, bits per second (for display)

bitsPerSecond
} = await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.quota(): Promise<cloud.QuotaStatus>
export cloud_d_exports.quota
quota
() // false and the free rate while today's free volume is left
const
const rate: string
rate
=
const throttled: boolean

transfers are actually being rate-limited right now (overQuota and not premium)

throttled
? `${
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.round(x: number): number

Returns a supplied numeric expression rounded to the nearest integer.

@param ― x The value to be rounded to the nearest integer.

round
(
const bitsPerSecond: number

current effective egress rate cap, bits per second (for display)

bitsPerSecond
/ 1e6)} Mbit/s` : 'full rate' // 'full rate', since throttled is false

label takes AccountInfo | null, so the disconnected case is typed. The rest of the account surface is on account and quota.

The storage types split by entry: @fkn/lib/fs exports the shapes of the hybrid fs, @fkn/lib/cloud/fs exports the account’s, and Stats comes from all three file systems:

TypeFromWhat it holds
Statsfs, opfs, cloud/fs{ size, mtimeMs, mtime, atimeMs, ctimeMs, mode, isFile(), isDirectory(), isSymbolicLink() }, and isSymbolicLink() is always false
SealedReadfs, cloud/fs{ data: Uint8Array, sealedAt: string | null }
StorageQuotacloud/fs{ usedBytes, limitBytes, remaining, objects, maxObjects }
EncryptionStatuscloud/fs{ unlocked, enrolled, keyEpoch: number | null }
FileEntrycloud/fs{ path, size, contentType: string | null, updatedAt, encryption: string | null }, which no exported function produces
ConflictSidefs{ size, updatedAt: string | null }
FileConflictfs{ path, local, cloud, readLocal(), readCloud() }, what a resolver receives
ConflictResolutionfsBlob | ArrayBuffer | Uint8Array | string | null, what a resolver returns
ConflictResolverfs(conflict: FileConflict) => ConflictResolution | Promise<ConflictResolution>
AdoptSummaryfsuploaded, kept, unresolved and deleted as string[], resolved: { path, choice: 'local' | 'cloud' | 'merged' }[] and failed: { path, error }[], what adopt() resolves to

A read is typed Buffer | string whatever encoding you pass, because the declarations carry no overloads. Coerce the result before JSON.parse. The option types (ReadOptions, WriteOptions, WriteData, MakeOptions, Callback) are not exported by name from any storage entry, and the WriteData on @fkn/lib/contract is a different type (see the broker’s vocabulary). Derive them from the functions instead:

app.ts
import
const fs: {
available: () => Promise<boolean>;
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string;
writeFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
appendFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
existsSync: (path: import("node:fs").PathLike) => boolean;
statSync: (path: import("node:fs").PathLike) => Stats;
... 28 more ...;
onConflict: (next: ConflictResolver) => (() => void);
}
fs
from '@fkn/lib/fs'
type
type WriteOptions = BufferEncoding | {
encoding?: BufferEncoding | null;
mode?: number;
flag?: string;
contentType?: string;
} | null | undefined
WriteOptions
=
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never

Obtain the parameters of a function type in a tuple

Parameters
<typeof
const fs: {
available: () => Promise<boolean>;
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string;
writeFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
appendFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
existsSync: (path: import("node:fs").PathLike) => boolean;
statSync: (path: import("node:fs").PathLike) => Stats;
... 28 more ...;
onConflict: (next: ConflictResolver) => (() => void);
}
fs
.
promises: {
readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>;
writeFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>;
appendFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>;
stat: (path: import("node:fs").PathLike) => Promise<Stats>;
lstat: (path: import("node:fs").PathLike) => Promise<Stats>;
... 6 more ...;
access: (path: import("node:fs").PathLike) => Promise<void>;
}
promises
.
writeFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>
writeFile
>[2] // BufferEncoding | { encoding?, mode?, flag?, contentType? } | null | undefined
type
type WriteData = string | ArrayBuffer | Uint8Array<ArrayBufferLike> | DataView<ArrayBufferLike>
WriteData
=
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never

Obtain the parameters of a function type in a tuple

Parameters
<typeof
const fs: {
available: () => Promise<boolean>;
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string;
writeFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
appendFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
existsSync: (path: import("node:fs").PathLike) => boolean;
statSync: (path: import("node:fs").PathLike) => Stats;
... 28 more ...;
onConflict: (next: ConflictResolver) => (() => void);
}
fs
.
promises: {
readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>;
writeFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>;
appendFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>;
stat: (path: import("node:fs").PathLike) => Promise<Stats>;
lstat: (path: import("node:fs").PathLike) => Promise<Stats>;
... 6 more ...;
access: (path: import("node:fs").PathLike) => Promise<void>;
}
promises
.
writeFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>
writeFile
>[1] // string | Uint8Array | ArrayBuffer | DataView, and never a Blob
const
const options: WriteOptions
options
:
type WriteOptions = BufferEncoding | {
encoding?: BufferEncoding | null;
mode?: number;
flag?: string;
contentType?: string;
} | null | undefined
WriteOptions
= {
contentType?: string | undefined
contentType
: 'application/json' }
const
const data: WriteData
data
:
type WriteData = string | ArrayBuffer | Uint8Array<ArrayBufferLike> | DataView<ArrayBufferLike>
WriteData
=
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@param ― value A JavaScript value, usually an object or array, to be converted.

@param ― replacer A function that transforms the results.

@param ― space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws ― {TypeError} If a circular reference or a BigInt value is found.

stringify
({
items: never[]
items
: [] })
await
const fs: {
available: () => Promise<boolean>;
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string;
writeFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
appendFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
existsSync: (path: import("node:fs").PathLike) => boolean;
statSync: (path: import("node:fs").PathLike) => Stats;
... 28 more ...;
onConflict: (next: ConflictResolver) => (() => void);
}
fs
.
promises: {
readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>;
writeFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>;
appendFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>;
stat: (path: import("node:fs").PathLike) => Promise<Stats>;
lstat: (path: import("node:fs").PathLike) => Promise<Stats>;
... 6 more ...;
access: (path: import("node:fs").PathLike) => Promise<void>;
}
promises
.
writeFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>
writeFile
('library/catalog.json',
const data: string
data
,
const options: {
encoding?: BufferEncoding | null;
mode?: number;
flag?: string;
contentType?: string;
}
options
)
const
const catalog: any
catalog
=
var JSON: 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
(
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(
const fs: {
available: () => Promise<boolean>;
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string;
writeFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
appendFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
existsSync: (path: import("node:fs").PathLike) => boolean;
statSync: (path: import("node:fs").PathLike) => Stats;
... 28 more ...;
onConflict: (next: ConflictResolver) => (() => void);
}
fs
.
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string
readFileSync
('library/catalog.json', 'utf8'))) // Buffer | string in the type, a string at runtime

The read still needs String(). The string is what arrives, and the union is what the declaration says.

@fkn/lib/cloud/fs has no synchronous members at all, so a synchronous call there is a compile error rather than a runtime one:

app.ts
import * as
import cloudFs
cloudFs
from '@fkn/lib/cloud/fs'
type
type CloudMember = "STORAGE_ACCOUNT_CHANGED" | "STORAGE_ACCOUNT_PIN_UNSUPPORTED" | "STORAGE_NOT_FOUND" | "StorageAccountChangedError" | "StorageAccountPinUnsupportedError" | "StorageLockedError" | "StorageNotFoundError" | "availability" | "available" | "encryption" | "isAccountChanged" | "isNotFound" | "list" | "mkdir" | "promises" | "quota" | "readFile" | "readFileSealed" | "readdir" | "rename" | "rm" | "stat" | "unlink" | "unlock" | "writeFile"
CloudMember
= keyof typeof
import cloudFs
cloudFs
// 'available' | 'readFile' | 'writeFile' | 'quota' | 'encryption' | 'unlock' | ... eighteen names, none ending in Sync
import cloudFs
cloudFs
.readFileSync('library/catalog.json')
Error ts(2551) ― Property 'readFileSync' does not exist on type 'typeof import("/opt/buildhome/repo/node_modules/@fkn/lib/cloud/fs")'. Did you mean 'readFile'?

The key union is the whole surface, and mount, flush and appendFile are missing from it as well. The rest is on storage, sync and conflicts and encryption.

Socket and Server from @fkn/lib/net are classes that implement Node’s net.Socket and net.Server. The option and address types around them (SocketConnectOpts, ListenOptions, AddressInfo) are Node’s own, from @types/node. @fkn/lib/dgram and @fkn/lib/http are typed the same way, so code written against Node’s declarations compiles against these, with three exceptions.

net.connect and net.createConnection hand back a Socket before the relay, the server that holds the real socket at the far end, has answered. The connect method on that Socket accepts every Node overload. The two functions accept only the options form:

app.ts
import * as
import net
net
from '@fkn/lib/net'
const
const socket: net.Socket
socket
=
import net
net
.
function connect(options: SocketConnectOpts, connectionListener?: () => void): net.Socket
export connect
connect
({
TcpSocketConnectOpts.host?: string | undefined
host
: 'example.org',
TcpSocketConnectOpts.port: number
port
: 6881 }) // a Socket, handed back before the relay answers
import net
net
.
function connect(options: SocketConnectOpts, connectionListener?: () => void): net.Socket
export connect
connect
(6881, 'example.org')
Error ts(2345) ― Argument of type 'number' is not assignable to parameter of type 'SocketConnectOpts'.

The positional call fails at the call site rather than on the wire.

The dgram.Socket constructor requires its options, where Node’s does not, so build sockets with createSocket:

app.ts
import {
class Socket
Socket
,
const createSocket: (options: SocketType | SocketOptions, callback?: (msg: Buffer, rinfo: RemoteInfo) => void) => Socket
createSocket
} from '@fkn/lib/dgram'
const
const socket: Socket
socket
=
function createSocket(options: SocketType | SocketOptions, callback?: (msg: Buffer, rinfo: RemoteInfo) => void): Socket
createSocket
('udp4') // a Socket built as new Socket({ type: 'udp4' })
new
new Socket(options: SocketOptions & EventEmitterOptions & {
connection?: ReturnType<(options: UdpSocketOptions) => Promise<UdpSocketResult>>;
}): Socket
Socket
()
Error ts(2554) ― Expected 1 arguments, but got 0.

createSocket supplies that object, so code written against Node’s createSocket needs no change.

The third exception is a name rather than a narrowing. OutgoingMessage, the base of ClientRequest and ServerResponse, sits on the default export of @fkn/lib/http only. http.OutgoingMessage resolves, and an import of it by name does not compile:

app.ts
import
const http: {
METHODS: string[];
STATUS_CODES: Record<number, string>;
Agent: typeof Agent;
globalAgent: Agent;
Server: typeof ServerImpl;
ServerResponse: typeof ServerResponse;
IncomingMessage: typeof IncomingMessage;
ClientRequest: typeof ClientRequest;
OutgoingMessage: typeof OutgoingMessage;
createServer: typeof createServer;
request: typeof request;
get: typeof get;
}
http
from '@fkn/lib/http'
import { OutgoingMessage } from '@fkn/lib/http'
Error ts(2614) ― Module '"@fkn/lib/http"' has no exported member 'OutgoingMessage'. Did you mean to use 'import OutgoingMessage from "@fkn/lib/http"' instead?
type
type Out = OutgoingMessage
Out
=
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any

Obtain the return type of a constructor function type

InstanceType
<typeof
const http: {
METHODS: string[];
STATUS_CODES: Record<number, string>;
Agent: typeof Agent;
globalAgent: Agent;
Server: typeof ServerImpl;
ServerResponse: typeof ServerResponse;
IncomingMessage: typeof IncomingMessage;
ClientRequest: typeof ClientRequest;
OutgoingMessage: typeof OutgoingMessage;
createServer: typeof createServer;
request: typeof request;
get: typeof get;
}
http
.
type OutgoingMessage: typeof OutgoingMessage
OutgoingMessage
> // the base both ClientRequest and ServerResponse extend
const
const sent: (message: Out) => boolean
sent
= (
message: OutgoingMessage
message
:
type Out = OutgoingMessage
Out
): boolean =>
message: OutgoingMessage
message
.
OutgoingMessage.headersSent: boolean
headersSent

Out is the base class a signature over both a request and a response needs.

@fkn/lib/dns exports lookup, generic over its all option, and AddressLookupResult = { address: string; family: 0 | 4 | 6 }. @fkn/lib/wire exports the socket option codes as literal types and the two unions TcpSocketOption and UdpSocketOption. The rest is on TCP and UDP sockets and HTTP and DNS.

A package is an npm module FKN loads on a sandbox origin of its own. @fkn/lib/packages exports every shape the packages surface uses, so nothing there has to be derived:

TypeWhat it covers
PackageQuery, PackageResultwhat search and pick take, and one hit they answer
InstalledPackagewhat install resolves to, and one row of list
PickOptions, InstallOptions{ title?, multiple? } and { version?, noConfirm? }
ConnectOptions, AppConnectOptions{ protocol? }, and the app’s payload?, signal? and raw? on top
Connectthe overloaded call type behind packages.connect
PackageConnection, RawPackageConnection{ remote: Remote<T>, closed, port }, and the same without remote
MountOptions, MountedPackageAppConnectOptions plus your iframe, and the connection plus frame and unmount()
ShowOptions, PackageView{ element?, rect? }, and the { hide(), refresh() } that show resolves to
IncomingConnectionInfo, IncomingConnectionwho connected, and that plus the connection
ConnectPayloadthe first argument of onConnect
SurfaceRect, Radii, Placementthe geometry behind show
PackagesError, PackagesErrorCodeError & { code }, the six codes on packages

The type argument on connect, mount and attach is the payload the other side exposed, mapped through osra’s Remote<T>. Every function on it becomes async, and plain data stays itself:

package.ts
import {
const onConnect: <T = unknown>(createPayload: ConnectPayload, handler?: (connection: IncomingConnection<T>) => void) => {
unsubscribe: () => void;
}

Serve connections from apps that installed this package. The first argument is called once per incoming connection with the connection info and returns the payload exposed to that app (its remote). The latest registration receives new connections; existing connections are unaffected.

onConnect
} from '@fkn/lib/packages'
import type {
type IncomingConnection<T = unknown> = IncomingConnectionInfo & PackageConnection<T>
IncomingConnection
} from '@fkn/lib/packages'
type
type HostApi = {
appVersion: string;
}
HostApi
= {
appVersion: string
appVersion
: string }
const
const payload: {
search: (text: string) => Promise<string[]>;
}
payload
= {
search: (text: string) => Promise<string[]>
search
: async (
text: string
text
: string) => [`result for ${
text: string
text
}`] }
export type
type SourceApi = {
search: (text: string) => Promise<string[]>;
}
SourceApi
= typeof
const payload: {
search: (text: string) => Promise<string[]>;
}
payload
onConnect<HostApi>(createPayload: ConnectPayload, handler?: ((connection: IncomingConnection<HostApi>) => void) | undefined): {
unsubscribe: () => void;
}

Serve connections from apps that installed this package. The first argument is called once per incoming connection with the connection info and returns the payload exposed to that app (its remote). The latest registration receives new connections; existing connections are unaffected.

onConnect
<
type HostApi = {
appVersion: string;
}
HostApi
>(
() =>
const payload: {
search: (text: string) => Promise<string[]>;
}
payload
,
(
connection: IncomingConnection<HostApi>
connection
:
type IncomingConnection<T = unknown> = IncomingConnectionInfo & PackageConnection<T>
IncomingConnection
<
type HostApi = {
appVersion: string;
}
HostApi
>) => {
connection: IncomingConnection<HostApi>
connection
.
protocol: string | null

the contract tag the app passed to connect(), e.g. 'stub-source@1'

protocol
// 'example-source@1', the tag the app passed
connection: IncomingConnection<HostApi>
connection
.
remote: {
appVersion: string;
}

the package's exposed payload

remote
.
appVersion: string
appVersion
// '1.2.0', plain data on the app's payload, so no await
},
)
app.ts
import {
(alias) namespace packages
import packages
packages
} from '@fkn/lib'
import type {
type PackageConnection<T = unknown> = {
remote: Remote<T>;
closed: Promise<void>;
port: MessagePort;
}
PackageConnection
,
type PackagesError = Error & {
code: packages.PackagesErrorCode;
}
PackagesError
} from '@fkn/lib/packages'
import type {
type SourceApi = {
search: (text: string) => Promise<string[]>;
}
SourceApi
} from './package'
try {
const
const connection: packages.PackageConnection<{
search: (text: string) => Promise<string[]>;
}>
connection
:
type PackageConnection<T = unknown> = {
remote: Remote<T>;
closed: Promise<void>;
port: MessagePort;
}
PackageConnection
<
type SourceApi = {
search: (text: string) => Promise<string[]>;
}
SourceApi
> = await
(alias) namespace packages
import packages
packages
.
packages_d_exports.connect<{
search: (text: string) => Promise<string[]>;
}>(uri: string, options?: packages.AppConnectOptions & {
raw?: false;
}): Promise<packages.PackageConnection<{
search: (text: string) => Promise<string[]>;
}>> (+1 overload)
export packages_d_exports.connect

Connect to an installed package. Throws a PackagesError with code 'not-installed' when it is not.

connect
<
type SourceApi = {
search: (text: string) => Promise<string[]>;
}
SourceApi
>('npm:@example/subtitles-plugin', {
protocol?: string | undefined

opaque contract tag delivered to the package's onConnect, e.g. 'stub-source@1'

protocol
: 'example-source@1',
payload?: unknown

exposed to the package as ITS remote

payload
: {
appVersion: string
appVersion
: '1.2.0' }, // what the package sees as its remote
})
const
const found: string[]
found
= await
const connection: packages.PackageConnection<{
search: (text: string) => Promise<string[]>;
}>
connection
.
remote: {
search: (text: string) => Promise<string[]>;
}

the package's exposed payload

remote
.
search: (text: string) => Promise<string[]>
search
('naruto') // ['result for naruto'], answered by the package
} catch (
var error: unknown
error
) {
const {
const code: packages.PackagesErrorCode
code
} =
var error: unknown
error
as
type PackagesError = Error & {
code: packages.PackagesErrorCode;
}
PackagesError
// 'timeout' when the package never registered onConnect
}

Each side names the other’s payload as its type argument. The app imports what the package exposes, and the package names what the app passed as payload. The rest is on packages.

A room is a realtime channel several browsers open by name or join from an invite. @fkn/lib/rooms exports every shape the surface uses:

TypeWhat it covers
Roomwhat open, create and join resolve to: id, name, key, invite, self, owner, claimed, mailbox, and every method
RoomMember, RoomPermissions, RoomPermissionone member as this room sees them with its message cap, the four booleans, and their names
RoomDefaultsthe room-wide send, receive and maxMessageBytes defaults the opener sets and setDefault and limit change
RoomMessage, RoomEventone delivered message, and the eight events on delivers
RoomMailbox, RoomBacklogwhat a claimed room’s mailbox stores, as usage answers it, null while the room keeps no messages, and what one backlog page answered
RoomEndwhat closed settles with
OpenOptions, CreateOptions, JoinOptions{ signal?, members?, defaults?, key? } for the first two, and { signal? }
ClaimOptions{ description?, temporary?, mailbox? }: what the account reads beside the room in its fkn.app settings, when the claim ends by itself, and whether it keeps messages
RoomsError, RoomsErrorCodethe rejection every member of the entry throws, and its twelve codes
app.ts
const
const render: (event: RoomEvent) => string
render
= (
event: rooms.RoomEvent
event
:
type RoomEvent = {
type: "message";
message: rooms.RoomMessage;
replayed: boolean;
} | {
type: "edited";
message: rooms.RoomMessage;
} | {
type: "deleted";
from: number;
to: number;
} | {
type: "joined";
member: rooms.RoomMember;
} | {
type: "left";
id: string;
reason: "left" | "removed" | "blocked";
} | {
type: "permissions";
id: string;
permissions: rooms.RoomPermissions;
maxMessageBytes: number;
} | {
type: "defaults";
defaults: rooms.RoomDefaults;
} | {
type: "claim";
claimed: boolean;
owner: string;
mailbox: boolean;
}
RoomEvent
) =>
event: rooms.RoomEvent
event
.
type: "message" | "edited" | "deleted" | "joined" | "left" | "permissions" | "defaults" | "claim"
type
=== 'message' ?
event: {
type: "message";
message: rooms.RoomMessage;
replayed: boolean;
}
event
.
message: rooms.RoomMessage
message
.
text: string
text
:
event: {
type: "edited";
message: rooms.RoomMessage;
} | {
type: "deleted";
from: number;
to: number;
} | {
type: "joined";
member: rooms.RoomMember;
} | {
type: "left";
id: string;
reason: "left" | "removed" | "blocked";
} | {
type: "permissions";
id: string;
permissions: rooms.RoomPermissions;
maxMessageBytes: number;
} | {
type: "defaults";
defaults: rooms.RoomDefaults;
} | {
type: "claim";
claimed: boolean;
owner: string;
mailbox: boolean;
}
event
.
type: "edited" | "deleted" | "joined" | "left" | "permissions" | "defaults" | "claim"
type
try {
const
const room: rooms.Room
room
:
type Room = {
readonly id: string;
readonly name: string;
readonly key: string;
readonly invite: string;
readonly self: rooms.RoomMember;
readonly owner: string;
readonly claimed: boolean;
readonly mailbox: rooms.RoomMailbox | null;
defaults: () => rooms.RoomDefaults;
members: () => Promise<rooms.RoomMember[]>;
send: (text: string) => Promise<void>;
edit: (seq: number, text: string) => Promise<void>;
delete: (from: number, to?: number) => Promise<void>;
backlog: (after: number, limit?: number) => Promise<rooms.RoomBacklog>;
... 13 more ...;
readonly closed: Promise<rooms.RoomEnd>;
}

A joined room. The same object survives a broker replacement, so it is safe to hold for as long as the chat lasts.

Room
= await
import rooms
rooms
.
const join: (invite: string, options?: rooms.JoinOptions) => Promise<Room>

invite is room.invite: a key and a room id joined by a dot.

join
(
var location: Location

The Window.location read-only property returns a Location object with information about the current location of the document.

MDN Reference

location
.
Location.hash: string

The hash property of the Location interface is a string containing a '#' followed by the fragment identifier of the location URL.

MDN Reference

hash
.
String.slice(start?: number, end?: number): string

Returns a section of a string.

@param ― start The index to the beginning of the specified portion of stringObj.

@param ― end The index to the end of the specified portion of stringObj. The substring includes the characters up to, but not including, the character indicated by end. If this value is not specified, the substring continues to the end of stringObj.

slice
(1))
await
const room: rooms.Room
room
.
on: (listener: (event: RoomEvent) => void) => Promise<() => void>

Await the returned unsubscribe in cleanup, the account.onChange shape.

on
(
const render: (event: RoomEvent) => string
render
)
} catch (
var error: unknown
error
) {
const {
const code: rooms.RoomsErrorCode
code
} =
var error: unknown
error
as
type RoomsError = Error & {
code: rooms.RoomsErrorCode;
}

Thrown by every member of this namespace. Match on code, never on the message.

RoomsError
// 'bad-key' when the room exists under another key
}

RoomsError is a type, never a class, so you test error.code on a rejection rather than instanceof, exactly like PackagesError. The rest is on rooms.

An object is a file stored under a key your app holds and read by anyone given its url and that key. @fkn/lib/storage exports every shape the surface uses:

TypeWhat it covers
StoredObjectwhat put and list answer for one object: url, size, status and created, with put adding the key
StoredPageone page of list, and the cursor present while more remain
StoredBlobwhat get resolves to: size, stream, arrayBuffer and slice
StoragePutOptions, StorageProgress{ key?, size?, signal?, onProgress? }, and the { loaded, total } that onProgress receives
StorageError, StorageErrorCodethe rejection every member of the entry throws, and its eight codes
app.ts
try {
const
const blob: storage.StoredBlob
blob
:
type StoredBlob = {
size: number;
stream: () => ReadableStream<Uint8Array>;
arrayBuffer: () => Promise<ArrayBuffer>;
slice: (start?: number, end?: number) => StoredBlob;
}

The bytes get opened, shaped like a Blob: size, stream, arrayBuffer and slice. Every read is by range and each 1 MiB record is checked before it is handed over, so a slice near the end of a large object fetches only that end.

StoredBlob
= await
(alias) namespace storage
import storage
storage
.
storage_d_exports.get(url: string, key: string, options?: {
signal?: AbortSignal;
}): Promise<StoredBlob>
export storage_d_exports.get

Opens an object any app stored, with no account: the broker reads its size from the first ranged answer and fetches the sealed bytes by range. Refused invalid for a url that names no stored object or a key that is not 32 bytes base64url, not-found for an object that was never made, was deleted or is still uploading (one answer for all of them). A wrong key shows on the first read, as integrity. A broker replaced while the object is held is asked to open it again on the next read.

get
(
const url: string
url
,
const key: string
key
)
await
const blob: storage.StoredBlob
blob
.
arrayBuffer: () => Promise<ArrayBuffer>
arrayBuffer
()
} catch (
var error: unknown
error
) {
const {
const code: storage.StorageErrorCode
code
} =
var error: unknown
error
as
type StorageError = Error & {
code: storage.StorageErrorCode;
}

Thrown by every member of this namespace except on an aborted signal. Match on code, never on the message.

StorageError
// 'integrity' when the key is not the one this url was stored with
}

StorageError is a type, never a class, so you test error.code on a rejection rather than instanceof, as for RoomsError. The rest is on object storage.

Resolvers is the whole surface the broker exposes over osra: the namespaces cloud, overlay, installPrompt, frameConsent, connect, relay, packages, rooms, storage, account and shell, plus flat legacy members kept so an older published @fkn/lib keeps working. It lives on @fkn/lib/contract, an entry whose .js carries only a few constants, such as ROOM_TEMPORARY_MS and STORAGE_REFUSALS, and @fkn/lib/api re-exports it beside apiPromise. The alias Api = Remote<Resolvers> is declared inside @fkn/lib/api and not exported, so spell it through the promise:

app.ts
import {
const apiPromise: Promise<{
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 12 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 17 more ...;
hideInstallPrompt: () => Promise<...>;
}>

The broker api. Settles when the first connection exists and resolves with a stable facade that always routes to the NEWEST connection, so holding the resolved value across a broker replacement is safe. A call in flight at the moment of replacement rejects with a named error instead of hanging.

A window realm that relays its channel to a worker (relayWorker) moves its own calls to a channel of their own, never back to the relayed one. There it also settles once the realm relays, a call in flight on the relayed channel then rejects as on a replacement, and a call waits for the realm's own channel within the deadline below, rejecting with OwnChannelUnavailableError when the broker never answers there.

apiPromise
} from '@fkn/lib/api'
type
type Api = {
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 12 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 17 more ...;
hideInstallPrompt: () => Promise<...>;
}
Api
=
type Awaited<T> = T extends null | undefined ? T : T extends object & {
then(onfulfilled: infer F, ...args: infer _): any;
} ? F extends (value: infer V, ...args: infer _) => any ? Awaited<V> : never : T

Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to never. This emulates the behavior of await.

Awaited
<typeof
const apiPromise: Promise<{
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 12 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 17 more ...;
hideInstallPrompt: () => Promise<...>;
}>

The broker api. Settles when the first connection exists and resolves with a stable facade that always routes to the NEWEST connection, so holding the resolved value across a broker replacement is safe. A call in flight at the moment of replacement rejects with a named error instead of hanging.

A window realm that relays its channel to a worker (relayWorker) moves its own calls to a channel of their own, never back to the relayed one. There it also settles once the realm relays, a call in flight on the relayed channel then rejects as on a replacement, and a call waits for the realm's own channel within the deadline below, rejecting with OwnChannelUnavailableError when the broker never answers there.

apiPromise
> // Remote<Resolvers>, osra's mapping, with every resolver an async function
const
const api: {
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 12 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 17 more ...;
hideInstallPrompt: () => Promise<...>;
}
api
:
type Api = {
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 12 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 17 more ...;
hideInstallPrompt: () => Promise<...>;
}
Api
= await
const apiPromise: Promise<{
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 12 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 17 more ...;
hideInstallPrompt: () => Promise<...>;
}>

The broker api. Settles when the first connection exists and resolves with a stable facade that always routes to the NEWEST connection, so holding the resolved value across a broker replacement is safe. A call in flight at the moment of replacement rejects with a named error instead of hanging.

A window realm that relays its channel to a worker (relayWorker) moves its own calls to a channel of their own, never back to the relayed one. There it also settles once the realm relays, a call in flight on the relayed channel then rejects as on a replacement, and a call waits for the realm's own channel within the deadline below, rejecting with OwnChannelUnavailableError when the broker never answers there.

apiPromise
// every resolver on the newest broker, from a promise that never rejects
const {
const remaining: number
remaining
} = await
const api: {
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 12 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 17 more ...;
hideInstallPrompt: () => Promise<...>;
}
api
.
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 12 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
}
cloud
.
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>
quota
() // bytes of free volume left today, on the broker's Quota rather than QuotaStatus

The call comes back as the broker’s own shape: Quota has remaining where QuotaStatus has remainingBytes, and @fkn/lib owns the mapping between the two. The wrappers are what an app calls. @fkn/lib/api is for code that observes the connection itself (see entry points).

The rest of the entry is the vocabulary those resolvers exchange, one line each:

TypeWhat it holds
ConnectAvailability'connected' | 'disconnected' | 'unknown', the storage availability probe’s three answers
Account{ name, image, premium, premiumUntil: string | null }, behind AccountInfo, with the ISO string
Quota{ overQuota, remaining, usedBytes, limitBytes, premium, bytesPerSecond }, behind QuotaStatus
StorageEntrythe same shape @fkn/lib/cloud/fs exports as FileEntry, one listed object
StorageQuotathe same shape @fkn/lib/cloud/fs exports
EncryptionState{ unlocked, enrolled, keyEpoch: number | null }, behind EncryptionStatus
WriteDataArrayBuffer | Uint8Array | string, what a write carries to the broker
AdoptState, AdoptRequest{ files: number, bytes: number }, what the adopt card is told
ConflictSide, ConflictRequest{ size, updatedAt: string | null } and { path, local, cloud }, what the conflict card is told
ConflictChoice'local' | 'cloud' | null, the card’s answer
AddressLookupResult, DnsLookup{ address, family: 0 | 4 | 6 } and the generic lookup signature
FrameFetchScope, FrameConsentRequest'frame.fetchRead' | 'frame.fetchWrite' and { scope, hosts: string[] }, what the cloud frame consent card is told
OverlayState, OverlayRect, HiddenSurface, DisplayCause, DisplayLevel, SetOverlayHostwhat the broker pushes so the overlay frame clips itself to its cards, see how it works
TcpSocketOptions, TcpSocketResult, TcpSocketListenerOptions, TcpSocketListenerResult, UdpSocketOptions, UdpDatagram, UdpSocketResult, IpFamilywhat net and dgram exchange with the broker for one socket
ProxyFetch, ProxyFetchInput, ProxyFetchInitthe signature behind cloud.fetch, taking string | URL | Request and an optional RequestInit widened with render?: boolean
MountDescriptor, PackagesFailthe broker’s answer to packages.frame, which is what packages.mount calls, and the refusal @fkn/lib re-mints as a PackagesError

It also re-exports the package shapes and the two socket option unions from @fkn/lib/wire, which their own sections cover.

Read the @fkn/lib/fs name whenever your code calls fs, and the contract name only when it reads the broker directly.

An error that crosses the broker keeps only its name, message, stack and cause. instanceof therefore works for the classes @fkn/lib constructs in your realm, the JavaScript execution context your code runs in, and for nothing from the other side. See what crosses a realm.

An error is recognised by one of five things: a class, a predicate, a code, a name or a message prefix. Each family answers to just one:

ErrorRecognise it by
BrokerUnreachableError, from @fkn/lib/apiinstanceof, minted in your realm by apiWithin and on the error event of net.connect, Server.listen and dgram.bind
StorageLockedError, StorageNotFoundError, from @fkn/lib/cloud/fsinstanceof or isNotFound(), with code FKN_E2E_LOCKED or FKN_STORAGE_NOT_FOUND, re-minted on reads and writes only, see unlink() skips the re-mint
ExtensionOutdatedError, ExtensionOperationUnsupportedError, from the rootinstanceof, on The FKN WebExtension is installed but too old for this page, raised by a category ask against an extension below ABI 2, and on what the extension does not serve, named by operation: 'cookies' for an extension.attachFrame without cookies: 'native', 'attachWindow', 'blank', 'addScriptTag', 'clearCookies', 'lockdown' and 'evaluate'
TimeoutError, from the rootinstanceof or error.name === 'TimeoutError', minted in your realm for every call that ran out of time: a locator action, goto, evaluate, addScriptTag and clearCookies, with the last attempt’s error as cause on a locator action
FrameWindowBlockedError, FrameWindowRefusedError, from the rootinstanceof, minted in your realm by attachFrame({ window }), on attachFrame: the browser did not open the window and on a refusal whose reason is a FrameWindowRefusal, see every error
ENOENT and its family, and FKN_E2E_LOCKED, on fs and opfscode, on <CODE>: <text>, <syscall> '<path>' and on storage: <path> exists but could not be read, retry once its scope is available, see storage
PackagesErrorcode, one of PackagesErrorCode, on every packages.* call and the local attach
BackgroundStoppedErrorerror.name === BACKGROUND_STOPPED or isBackgroundStopped(), both from the root, on a call into the extension that Chrome’s stop of its worker cut off, raised from the next extension store release, the first after 0.1.54, see a call the extension never finished
PermissionDeniedErrorerror.name or the exported isPermissionDenied, on every gated extension call, the message starting Permission denied: <category> on <site>, matched by neither locator guard, see a refusal neither guard matches
LocatorDeniedError, LocatorUnsupportedError, LocatorInvalidErrorerror.name, or from the root isLocatorDenied, isLocatorUnsupported, isLocatorInvalid and isTerminalError, which matches all three, see the three terminal names
LocatorErrorerror.name or LOCATOR_ERROR from the root, with no guard, the one locator name an action retries rather than stops on, met as a TimeoutError’s cause
fkn:e2e-locked, fkn:e2e-stale-epoch…, fkn:e2e-integrity…message.startsWith(...) with the three E2E_* constants from @fkn/lib/messages, the first re-minted as StorageLockedError before you see it
storage: api unreachablecode === STORAGE_UNREACHABLE in the realm that set it, else the message prefix
FKN: the broker was replaced while this call was pending; retry itthe exact message, on a call left pending across a replacement, see connection and lifecycle
getaddrinfo ENOTFOUND <hostname>, tcp connect to <address>:<port> timed out after 12000msthe message on a socket’s error event, the relay’s own refusal text included, the Node code not surviving the hop

A class test is true only for an error constructed on your side. A name or prefix test is what remains for one that crossed:

app.ts
import {
const API_DEADLINE_MS: 8000

The deadline of apiWithin, for call sites that own a socket, a timer or a UI and must not park. apiPromise NEVER REJECTS: a broker frame that never bridges leaves it pending for the realm's life. In a worker the transport {receive: self, emit: self} is inert until the page bridges it, so an unbridged worker parks every socket call with no listening, error or rejection, which presents as a transport fault invisible from every counter.

Once one deadline is missed, later calls wait only the retry deadline (as account-storage.ts does): net.ts listens with bind('::').catch(() => bind('0.0.0.0')) and would otherwise pay it twice.

API_DEADLINE_MS
,
class BrokerUnreachableError
BrokerUnreachableError
} from '@fkn/lib/api'
const
const minted: BrokerUnreachableError
minted
= new
new BrokerUnreachableError(what: string, ms: number): BrokerUnreachableError
BrokerUnreachableError
('the quota readout',
const API_DEADLINE_MS: 8000

The deadline of apiWithin, for call sites that own a socket, a timer or a UI and must not park. apiPromise NEVER REJECTS: a broker frame that never bridges leaves it pending for the realm's life. In a worker the transport {receive: self, emit: self} is inert until the page bridges it, so an unbridged worker parks every socket call with no listening, error or rejection, which presents as a transport fault invisible from every counter.

Once one deadline is missed, later calls wait only the retry deadline (as account-storage.ts does): net.ts listens with bind('::').catch(() => bind('0.0.0.0')) and would otherwise pay it twice.

API_DEADLINE_MS
)
const minted: BrokerUnreachableError
minted
instanceof
class BrokerUnreachableError
BrokerUnreachableError
// true, constructed in this realm
const
const crossed: Error & {
name: string;
}
crossed
=
var Object: ObjectConstructor

Provides functionality common to all JavaScript objects.

Object
.
ObjectConstructor.assign<Error, {
name: string;
}>(target: Error, source: {
name: string;
}): Error & {
name: string;
} (+3 overloads)

Copy the values of all of the enumerable own properties from one or more source objects to a target object. Returns the target object.

@param ― target The target object to copy to.

@param ― source The source object from which to copy properties.

assign
(new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('Permission denied: interaction on example.org (act.click #play)'), {
name: string
name
: 'PermissionDeniedError' }) // the shape a hop hands you
const crossed: Error & {
name: string;
}
crossed
instanceof
var Error: ErrorConstructor
Error
// true, and nothing narrower is left to test
const crossed: Error & {
name: string;
}
crossed
.
name: string
name
=== 'PermissionDeniedError' // true, the name survives
const crossed: Error & {
name: string;
}
crossed
.
Error.message: string
message
.
String.startsWith(searchString: string, position?: number): boolean

Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.

startsWith
('Permission denied: ') // true, and so does the prefix

The class test holds only because the error was built on this side. The order to test a mixed handler in is on handling errors, and every message with its cause is on every error.

Every entry point ships one bundled .d.ts, so a type import needs nothing installed beyond @fkn/lib and its own dependencies. @types/node is one of those, a dependency rather than a peer, since the socket and file system surfaces are typed against Node’s own declarations. @fkn/lib/react imports its types from react, an optional peer at >=18, so that one entry needs React’s type declarations installed beside it.

Every twoslash example on this site is checked with TypeScript 5.9 under strict, with an ESNext target and module, bundler module resolution and the DOM library. Top-level await is used freely.

From here, entry points says which import path carries each name. The generated API reference carries the exact declaration of most named exports, Locator and VideoElementState among the ones it misses. The vocabulary above is under @fkn/lib/contract.