Skip to content

How it works

@fkn/lib is a thin client that mounts a hidden fkn.app iframe, the broker frame, and forwards your calls through it. This page covers how a realm reaches the broker, how calls keep working across a shell update, how the overlay is drawn over your page, what a busy token records, and what the library stores and posts on your origin.

None of this page is API, but it explains the behaviors the guides describe. It is worth reading if you embed the library somewhere unusual or are debugging something strange.

There are two halves. The library runs in your realm, one JavaScript execution context such as a window or a worker, and holds little logic of its own. The broker is the connection your realm holds into FKN. It lives in the fkn.app document inside the broker frame, with the data plane, a shared worker, behind it.

Together they hold the account, the keys, the relay session and every prompt. Every brokered call ends in the broker, or beyond it at the relay or the proxy. The relay holds the real socket at the far end of net and dgram. The proxy is what cloud.fetch sends a request through.

opfs and the in-memory half of fs never reach the broker. Neither do the root fetch and attachFrame while the extension is exposed. See backends for where each call runs.

A realm reaches its broker in one of two ways. Everything below depends on that choice:

Parent broker document MessagePort, on its ack Worker realm relayWorker Your realm extension exposed Content script window pair, once committed name, message, stack, cause hidden fkn.app/api iframe Broker document name, message, stack, cause Data plane Response status, or a socket error event Relay or proxy

A realm whose parent answers is handed a MessagePort and talks over it in both directions. A realm with no answering parent mounts the /api frame and talks over the window pair, its own window and the frame’s, once the frame commits.

The channel is built on osra, a message-passing library. On either transport the library makes one expose call on it with the key fkn-api. apiPromise then resolves with the facade over that connection, described under epochs and the facade.

Past the facade sit the broker document, the data plane behind it, and the relay or the proxy at the far end. A worker realm joins only through relayWorker. The content script is the extension’s sideways path. It never touches the frame.

The two osra hops carry an error’s name, message, stack and cause and nothing else. The far end answers with a Response status or on a socket’s error event. See version compatibility.

Importing the root entry, or any entry other than opfs, opfs/promises, react, messages, contract, wire, attach-policy and desktop, starts the broker connection when the module is evaluated, once per realm. In a window with no answering parent it mounts the frame. Otherwise it uses the parent’s port, or the worker’s own self.

Those eight entries reach no broker code and mount nothing. See entry points.

The root and extension entries also register the missing-extension handler that opens the install card. setMissingExtensionHandler(null) removes it. See when the extension is missing.

The frame’s URL is https://fkn.app/api, or https://fkn.app/api?coi=1 on a cross-origin isolated page. Only the ?coi=1 variant carries the embedder policy headers. The library never falls back to the plain URL on an isolated page, since that frame would hang. The frame the library creates there carries allow="cross-origin-isolated", so the isolation is passed down to it.

Once the parent question below has come back empty, the library looks for iframe[src="https://fkn.app/api"], an exact src attribute match that includes ?coi=1 when the page is isolated, and adopts it. Otherwise it creates one and appends it to document.body, which has to exist by then. Either way the element is then styled as the overlay projector, so a frame you pre-create keeps no layout of its own:

app.ts
// before the lib evaluates, so it adopts this frame instead of creating one
const
const frame: HTMLIFrameElement
frame
=
var document: Document

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

MDN Reference

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

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

MDN Reference

createElement
('iframe')
const frame: HTMLIFrameElement
frame
.
HTMLIFrameElement.src: string

The HTMLIFrameElement.src A string that reflects the src HTML attribute, containing the address of the content to be embedded.

MDN Reference

src
=
var crossOriginIsolated: boolean
crossOriginIsolated
? 'https://fkn.app/api?coi=1' : 'https://fkn.app/api'
if (
var crossOriginIsolated: boolean
crossOriginIsolated
)
const frame: HTMLIFrameElement
frame
.
Element.setAttribute(qualifiedName: string, value: string): void

The setAttribute() method of the Element interface sets the value of an attribute on the specified element.

MDN Reference

setAttribute
('allow', 'cross-origin-isolated')
var document: Document

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

MDN Reference

document
.
Document.body: HTMLElement

The Document.body property represents the null if no such element exists.

MDN Reference

body
.
ParentNode.append(...nodes: (Node | string)[]): void

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

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

MDN Reference

append
(
const frame: HTMLIFrameElement
frame
)
const {
const cloud: typeof cloud_d_exports
cloud
} = await import('@fkn/lib')
await
const cloud: typeof cloud_d_exports
cloud
.
cloud_d_exports.quota(): Promise<QuotaStatus>
export cloud_d_exports.quota
quota
()
var document: Document

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

MDN Reference

document
.
ParentNode.querySelectorAll<Element>(selectors: string): NodeListOf<Element> (+4 overloads)

Returns all element descendants of node that match selectors.

MDN Reference

querySelectorAll
('iframe[title="FKN"]').
NodeList.length: number

The NodeList.length property returns the number of items in a NodeList.

MDN Reference

length
// 1, the lib restyled yours instead of creating a second

The count keys on title="FKN", which the library sets on whichever element it ends up with. The library takes an adopted frame as you built it, so set allow="cross-origin-isolated" yourself on an isolated page.

A freshly appended iframe still holds its initial about:blank document. The browser drops any message posted to that document for the future origin. So the library waits for the frame to commit before handing out the transport: it reads contentWindow.location.href every 16 ms and listens for load.

An empty href or about:blank means the frame has not committed yet. A cross-origin throw means it has. After 10 seconds the library hands the transport out anyway.

A detached frame counts as committed. The library reads contentWindow again after the wait, so a frame removed in the meantime leaves the realm with no transport. A call made through apiPromise then waits with no deadline. relayWorker rejects with FKN @fkn/lib: relayWorker found no FKN transport in this realm.

A realm whose parent is already an FKN broker document mounts no frame of its own. For example, a package tenant, the realm a package runs in under the fkn.app shell, has a broker document as its parent. Before mounting, every framed realm asks its parent for a port, in four steps:

  1. It creates a MessageChannel.
  2. It posts { type: 'fkn-api-port' } to window.parent with target origin https://fkn.app and port2 transferred.
  3. It waits 2 seconds for { type: 'fkn-api-port-ack' } on port1.
  4. On the ack that port is the transport in both directions, and only a broker document answers.

If the wait times out, postMessage throws, or the parent cannot be reached, the realm closes the port and mounts its own frame. A top-level page skips the question entirely.

On the port branch no iframe exists in that realm at all. That is why relayWorker bridges whatever transport the realm ended up with, port or window pair, into a worker. On the window pair it forwards with target origins: the broker’s toward the frame and * toward the worker. On a port it passes none, since a MessagePort carries no origin to compare:

app.ts
import {
const relayWorker: (worker: Worker, options?: {
unregisterSignal?: AbortSignal;
originA?: string;
originB?: string;
}) => Promise<void>
relayWorker
} from '@fkn/lib'
const
const engine: Worker
engine
= new
var Worker: new (scriptURL: string | URL, options?: WorkerOptions) => Worker

The Worker interface of the Web Workers API represents a background task that can be created via script, which can send messages back to its creator.

MDN Reference

Worker
(new
var URL: new (url: string | URL, base?: string | URL) => URL

The URL interface is used to parse, construct, normalize, and encode URL.

MDN Reference

URL
('./engine.ts', import.

The type of import.meta.

If you need to declare that a given property exists on import.meta, this type may be augmented via interface merging.

meta
.
ImportMeta.url: string
url
), {
WorkerOptions.type?: WorkerType | undefined
type
: 'module' })
const
const stop: AbortController
stop
= new
var AbortController: new () => AbortController

The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired.

MDN Reference

AbortController
()
await
function relayWorker(worker: Worker, options?: {
unregisterSignal?: AbortSignal;
originA?: string;
originB?: string;
}): Promise<void>
relayWorker
(
const engine: Worker
engine
, {
unregisterSignal?: AbortSignal | undefined
unregisterSignal
:
const stop: AbortController
stop
.
AbortController.signal: AbortSignal

The signal read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.

MDN Reference

signal
}) // bridges this realm's transport into the worker
// later, when the engine is torn down, so the worker relay and its busy token go with it
const stop: AbortController
stop
.
AbortController.abort(reason?: any): void

The abort() method of the AbortController interface aborts an asynchronous operation before it has completed.

MDN Reference

abort
()
const engine: Worker
engine
.
Worker.terminate(): void

The terminate() method of the Worker interface immediately terminates the Worker.

MDN Reference

terminate
()

The signal travels with the worker. The worker relay holds the busy token relayed worker until the signal aborts, so tearing both down together keeps shell.busyReasons() accurate. What a relayed worker can do is on workers.

A parent that is not fkn.app never receives the question. That is the 2 seconds a package under packages.mount pays before its first broker call. See mounting into your own iframe.

The channel is one osra connection per realm, on the key fkn-api. With a transport in hand the library calls expose<Resolvers>({}, { key: 'fkn-api', transport }) once. It exposes nothing and iterates the result as a connection queue.

Resolvers is the broker’s whole surface as a type, from @fkn/lib/contract. @fkn/lib/api is where the connection itself is reachable:

app.ts
import {
const hasTransport: boolean
hasTransport
,
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>;
... 11 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 16 more ...;
hideInstallPrompt: () => Promise<...>;
}>

The broker api. Settles when the first connection exists, exactly as before, but 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.

apiPromise
} from '@fkn/lib/api'
const hasTransport: boolean
hasTransport
// true in every window and every worker, connected or not
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>;
... 11 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 16 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>;
... 11 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 16 more ...;
hideInstallPrompt: () => Promise<...>;
}>

The broker api. Settles when the first connection exists, exactly as before, but 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.

apiPromise
// settles once a connection exists, and never rejects
const
const quota: {
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}
quota
= 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>;
... 11 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 16 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>;
... 11 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
() // Quota, straight from the contract

The raw resolver answers the contract’s Quota. cloud.quota() reshapes it, renaming remaining to remainingBytes.

The library passes no origin option, so on the window pair every message goes out with target origin * and is filtered by key alone. A granted port carries no origin to compare at all, so the worker relay forwards origins only for the window pair. Identity is established on the broker side from the browser-set origin of your messages. Nothing you pass changes who the broker thinks you are.

In a worker the transport is { receive: self, emit: self }. It stays inert until the page bridges it with relayWorker. apiPromise never rejects, in a worker or anywhere else, so a call made with no broker waits silently. A waiting call has not failed: it runs once a connection exists.

apiWithin, from the same entry, is the bounded form. It gives up after 8 seconds, and after 1 second for every later call once one deadline has been missed. See connecting.

Osra binds a remote to one connection. The shell, the FKN surface that can update and reload the page, replaces the broker document under that connection on every update. Calls on the old remote would then hang, since the dying document sends no close.

So the library consumes the connection queue instead. A broker epoch is one generation of the broker. Every peer that osra yields becomes a new one, a reloaded broker included. apiPromise resolves with a stable deep Proxy whose every call routes to the newest remote.

The facade answers property reads against the current remote, so a member the connected broker lacks reads as undefined. That is how the library detects features, and what lets a pinned library keep working against a broker of another age:

app.ts
import {
const onApiEpoch: (listener: EpochListener<...>) => () => void

Runs for the current broker connection on subscribe (if one exists) and for every later one. This is the seam every registration that crosses the connection must ride, because callbacks and subscriptions die with the connection they were sent on.

onApiEpoch
,
const currentApiEpoch: () => {
remote: {
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>;
availability: () => Promise<ConnectAvailability>;
list: () => Promise<{
path: string;
size: number;
contentType: string | null;
updatedAt: string;
encryption: string | null;
}[]>;
quota: () => Promise<{
usedBytes: number;
limitBytes: number;
remaining: number;
objects: number;
maxObjects: number;
}>;
readFile: (path: string) => Promise<Uint8Array<ArrayBufferLike>>;
readFileSealed: (path: string) => Promise<{
bytes: Uint8Array;
sealedAt: number | null;
}>;
writeFile: (path: string, data: WriteData, contentType: string | null) => Promise<void>;
remove: (path: string) => Promise<void>;
encryption: () => Promise<{
unlocked: boolean;
enrolled: boolean;
keyEpoch: number | null;
}>;
unlock: () => Promise<boolean>;
promptAdopt: (request: AdoptRequest) => Promise<boolean>;
promptConflict: (request: ConflictRequest) => Promise<ConflictChoice>;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<void>;
};
webvpn: {
tcpSocket: (options: TcpSocketOptions) => Promise<{
localAddress: string;
localFamily: IpFamily;
localPort: number;
remoteAddress: string;
remoteFamily: IpFamily;
remotePort: number;
dataReadableStream: ReadableStream<Uint8Array>;
dataWritableStream: WritableStream<Uint8Array>;
end: () => Promise<void>;
destroy: () => Promise<void>;
destroySoon: () => Promise<void>;
resetAndDestroy: () => Promise<void>;
setOption: (option: TcpSocketOption) => Promise<void>;
}>;
tcpSocketListener: (options: TcpSocketListenerOptions) => Promise<{
localAddress: string;
localFamily: IpFamily;
localPort: number;
close: () => Promise<void>;
}>;
udpSocket: (options: UdpSocketOptions) => Promise<{
socketId: number;
dataPort: MessagePort | undefined;
dataPortAcks: true | undefined;
closed: Promise<{
reason: string;
}>;
localAddress: string;
localFamily: IpFamily;
localPort: number;
dataReadableStream: ReadableStream<UdpDatagram>;
connect: (options: {
remoteAddress: string;
remotePort: number;
}) => Promise<{
local: boolean;
address: string;
family: IpFamily;
port: number;
}>;
disconnect: () => Promise<void>;
send: (options: {
message: ArrayBuffer;
address?: string;
port?: number;
}) => Promise<void>;
close: () => Promise<void>;
setOption: (option: UdpSocketOption) => Promise<void>;
}>;
};
};
overlay: {
setHost: (push: (state: OverlayState) => unknown, options?: {
exactClips?: boolean;
} | undefined) => Promise<void>;
};
installPrompt: {
show: (reason?: string | undefined ...
currentApiEpoch
} from '@fkn/lib/api'
const
const stop: () => void
stop
=
function onApiEpoch(listener: EpochListener<...>): () => void

Runs for the current broker connection on subscribe (if one exists) and for every later one. This is the seam every registration that crosses the connection must ride, because callbacks and subscriptions die with the connection they were sent on.

onApiEpoch
((
remote: {
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>;
availability: () => Promise<ConnectAvailability>;
list: () => Promise<{
path: string;
size: number;
contentType: string | null;
updatedAt: string;
encryption: string | null;
}[]>;
quota: () => Promise<{
usedBytes: number;
limitBytes: number;
remaining: number;
objects: number;
maxObjects: number;
}>;
readFile: (path: string) => Promise<Uint8Array<ArrayBufferLike>>;
readFileSealed: (path: string) => Promise<{
bytes: Uint8Array;
sealedAt: number | null;
}>;
writeFile: (path: string, data: WriteData, contentType: string | null) => Promise<void>;
remove: (path: string) => Promise<void>;
encryption: () => Promise<{
unlocked: boolean;
enrolled: boolean;
keyEpoch: number | null;
}>;
unlock: () => Promise<boolean>;
promptAdopt: (request: AdoptRequest) => Promise<boolean>;
promptConflict: (request: ConflictRequest) => Promise<ConflictChoice>;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<void>;
};
webvpn: {
tcpSocket: (options: TcpSocketOptions) => Promise<{
localAddress: string;
localFamily: IpFamily;
localPort: number;
remoteAddress: string;
remoteFamily: IpFamily;
remotePort: number;
dataReadableStream: ReadableStream<Uint8Array>;
dataWritableStream: WritableStream<Uint8Array>;
end: () => Promise<void>;
destroy: () => Promise<void>;
destroySoon: () => Promise<void>;
resetAndDestroy: () => Promise<void>;
setOption: (option: TcpSocketOption) => Promise<void>;
}>;
tcpSocketListener: (options: TcpSocketListenerOptions) => Promise<{
localAddress: string;
localFamily: IpFamily;
localPort: number;
close: () => Promise<void>;
}>;
udpSocket: (options: UdpSocketOptions) => Promise<{
socketId: number;
dataPort: MessagePort | undefined;
dataPortAcks: true | undefined;
closed: Promise<{
reason: string;
}>;
localAddress: string;
localFamily: IpFamily;
localPort: number;
dataReadableStream: ReadableStream<UdpDatagram>;
connect: (options: {
remoteAddress: string;
remotePort: number;
}) => Promise<{
local: boolean;
address: string;
family: IpFamily;
port: number;
}>;
disconnect: () => Promise<void>;
send: (options: {
message: ArrayBuffer;
address?: string;
port?: number;
}) => Promise<void>;
close: () => Promise<void>;
setOption: (option: UdpSocketOption) => Promise<void>;
}>;
};
};
... 16 more ...;
hideInstallPrompt: () => Promise<void>;
}
remote
,
epoch: number
epoch
) => {
epoch: number
epoch
// 1 for the first connection, 2 once a shell update reloaded the frame
typeof
remote: {
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>;
availability: () => Promise<ConnectAvailability>;
list: () => Promise<{
path: string;
size: number;
contentType: string | null;
updatedAt: string;
encryption: string | null;
}[]>;
quota: () => Promise<{
usedBytes: number;
limitBytes: number;
remaining: number;
objects: number;
maxObjects: number;
}>;
readFile: (path: string) => Promise<Uint8Array<ArrayBufferLike>>;
readFileSealed: (path: string) => Promise<{
bytes: Uint8Array;
sealedAt: number | null;
}>;
writeFile: (path: string, data: WriteData, contentType: string | null) => Promise<void>;
remove: (path: string) => Promise<void>;
encryption: () => Promise<{
unlocked: boolean;
enrolled: boolean;
keyEpoch: number | null;
}>;
unlock: () => Promise<boolean>;
promptAdopt: (request: AdoptRequest) => Promise<boolean>;
promptConflict: (request: ConflictRequest) => Promise<ConflictChoice>;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<void>;
};
webvpn: {
tcpSocket: (options: TcpSocketOptions) => Promise<{
localAddress: string;
localFamily: IpFamily;
localPort: number;
remoteAddress: string;
remoteFamily: IpFamily;
remotePort: number;
dataReadableStream: ReadableStream<Uint8Array>;
dataWritableStream: WritableStream<Uint8Array>;
end: () => Promise<void>;
destroy: () => Promise<void>;
destroySoon: () => Promise<void>;
resetAndDestroy: () => Promise<void>;
setOption: (option: TcpSocketOption) => Promise<void>;
}>;
tcpSocketListener: (options: TcpSocketListenerOptions) => Promise<{
localAddress: string;
localFamily: IpFamily;
localPort: number;
close: () => Promise<void>;
}>;
udpSocket: (options: UdpSocketOptions) => Promise<{
socketId: number;
dataPort: MessagePort | undefined;
dataPortAcks: true | undefined;
closed: Promise<{
reason: string;
}>;
localAddress: string;
localFamily: IpFamily;
localPort: number;
dataReadableStream: ReadableStream<UdpDatagram>;
connect: (options: {
remoteAddress: string;
remotePort: number;
}) => Promise<{
local: boolean;
address: string;
family: IpFamily;
port: number;
}>;
disconnect: () => Promise<void>;
send: (options: {
message: ArrayBuffer;
address?: string;
port?: number;
}) => Promise<void>;
close: () => Promise<void>;
setOption: (option: UdpSocketOption) => Promise<void>;
}>;
};
};
... 16 more ...;
hideInstallPrompt: () => Promise<void>;
}
remote
.
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>;
availability: () => Promise<ConnectAvailability>;
list: () => Promise<{
path: string;
size: number;
contentType: string | null;
updatedAt: string;
encryption: string | null;
}[]>;
quota: () => Promise<{
usedBytes: number;
limitBytes: number;
remaining: number;
objects: number;
maxObjects: number;
}>;
readFile: (path: string) => Promise<Uint8Array<ArrayBufferLike>>;
readFileSealed: (path: string) => Promise<{
bytes: Uint8Array;
sealedAt: number | null;
}>;
writeFile: (path: string, data: WriteData, contentType: string | null) => Promise<void>;
remove: (path: string) => Promise<void>;
encryption: () => Promise<{
unlocked: boolean;
enrolled: boolean;
keyEpoch: number | null;
}>;
unlock: () => Promise<boolean>;
promptAdopt: (request: AdoptRequest) => Promise<boolean>;
promptConflict: (request: ConflictRequest) => Promise<ConflictChoice>;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<void>;
};
webvpn: {
tcpSocket: (options: TcpSocketOptions) => Promise<{
localAddress: string;
localFamily: IpFamily;
localPort: number;
remoteAddress: string;
remoteFamily: IpFamily;
remotePort: number;
dataReadableStream: ReadableStream<Uint8Array>;
dataWritableStream: WritableStream<Uint8Array>;
end: () => Promise<void>;
destroy: () => Promise<void>;
destroySoon: () => Promise<void>;
resetAndDestroy: () => Promise<void>;
setOption: (option: TcpSocketOption) => Promise<void>;
}>;
tcpSocketListener: (options: TcpSocketListenerOptions) => Promise<{
localAddress: string;
localFamily: IpFamily;
localPort: number;
close: () => Promise<void>;
}>;
udpSocket: (options: UdpSocketOptions) => Promise<{
socketId: number;
dataPort: MessagePort | undefined;
dataPortAcks: true | undefined;
closed: Promise<{
reason: string;
}>;
localAddress: string;
localFamily: IpFamily;
localPort: number;
dataReadableStream: ReadableStream<UdpDatagram>;
connect: (options: {
remoteAddress: string;
remotePort: number;
}) => Promise<{
local: boolean;
address: string;
family: IpFamily;
port: number;
}>;
disconnect: () => Promise<void>;
send: (options: {
message: ArrayBuffer;
address?: string;
port?: number;
}) => Promise<void>;
close: () => Promise<void>;
setOption: (option: UdpSocketOption) => Promise<void>;
}>;
};
}
cloud
.
fs: {
available: () => Promise<boolean>;
availability: () => Promise<ConnectAvailability>;
list: () => Promise<{
path: string;
size: number;
contentType: string | null;
updatedAt: string;
encryption: string | null;
}[]>;
quota: () => Promise<{
usedBytes: number;
limitBytes: number;
remaining: number;
objects: number;
maxObjects: number;
}>;
readFile: (path: string) => Promise<Uint8Array<ArrayBufferLike>>;
... 7 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<void>;
}
fs
.
readFileSealed: (path: string) => Promise<{
bytes: Uint8Array;
sealedAt: number | null;
}>
readFileSealed
=== 'function' // false against a broker that predates sealed reads
})
function currentApiEpoch(): {
remote: {
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>;
availability: () => Promise<ConnectAvailability>;
list: () => Promise<{
path: string;
size: number;
contentType: string | null;
updatedAt: string;
encryption: string | null;
}[]>;
quota: () => Promise<{
usedBytes: number;
limitBytes: number;
remaining: number;
objects: number;
maxObjects: number;
}>;
readFile: (path: string) => Promise<Uint8Array<ArrayBufferLike>>;
readFileSealed: (path: string) => Promise<{
bytes: Uint8Array;
sealedAt: number | null;
}>;
writeFile: (path: string, data: WriteData, contentType: string | null) => Promise<void>;
remove: (path: string) => Promise<void>;
encryption: () => Promise<{
unlocked: boolean;
enrolled: boolean;
keyEpoch: number | null;
}>;
unlock: () => Promise<boolean>;
promptAdopt: (request: AdoptRequest) => Promise<boolean>;
promptConflict: (request: ConflictRequest) => Promise<ConflictChoice>;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<void>;
};
webvpn: {
tcpSocket: (options: TcpSocketOptions) => Promise<{
localAddress: string;
localFamily: IpFamily;
localPort: number;
remoteAddress: string;
remoteFamily: IpFamily;
remotePort: number;
dataReadableStream: ReadableStream<Uint8Array>;
dataWritableStream: WritableStream<Uint8Array>;
end: () => Promise<void>;
destroy: () => Promise<void>;
destroySoon: () => Promise<void>;
resetAndDestroy: () => Promise<void>;
setOption: (option: TcpSocketOption) => Promise<void>;
}>;
tcpSocketListener: (options: TcpSocketListenerOptions) => Promise<{
localAddress: string;
localFamily: IpFamily;
localPort: number;
close: () => Promise<void>;
}>;
udpSocket: (options: UdpSocketOptions) => Promise<{
socketId: number;
dataPort: MessagePort | undefined;
dataPortAcks: true | undefined;
closed: Promise<{
reason: string;
}>;
localAddress: string;
localFamily: IpFamily;
localPort: number;
dataReadableStream: ReadableStream<UdpDatagram>;
connect: (options: {
remoteAddress: string;
remotePort: number;
}) => Promise<{
local: boolean;
address: string;
family: IpFamily;
port: number;
}>;
disconnect: () => Promise<void>;
send: (options: {
message: ArrayBuffer;
address?: string;
port?: number;
}) => Promise<void>;
close: () => Promise<void>;
setOption: (option: UdpSocketOption) => Promise<void>;
}>;
};
};
overlay: {
setHost: (push: (state: OverlayState) => unknown, options?: {
exactClips?: boolean;
} | undefined) => Promise<void>;
};
installPrompt: {
show: (reason?: string | undefined ...
currentApiEpoch
()?.
epoch: number | undefined
epoch
// undefined before the first connection, 1 once it exists

The listener stays registered until you call stop. It runs for the current broker epoch when you subscribe, and again for every later one.

A few behaviors of the facade are worth knowing:

The library re-sends every registration that crosses the connection on each broker epoch, because a callback dies with the connection that carried it. Those registrations are the overlay host, account.onChange, shell.onUpdate and the taken signal. Only the facade survives a swap.

The broker frame is also the overlay: one <iframe> carries the channel and draws the platform’s UI, the header bar and the cards, over your page. At mount the library styles it position: fixed, full viewport, clip-path: inset(100%), title="FKN", with a z-index of 2147483647, so at rest every pointer event passes through.

Everything but z-index, background and color-scheme carries !important, so a host iframe reset cannot un-clip or move it. A host !important on z-index wins. The frame then draws behind whatever stacks above it.

The broker pushes the rectangles it is drawing, as { modal, rects, hidden, view, inset }, whenever they change. The library ignores the pushed view, clamps the rects into the iframe’s own box, at most 32 of them, keeps at most 8 hidden entries, and writes a clip that exposes exactly those islands:

  • none for a modal
  • inset() for one rect, its radius carried in the round clause
  • a polygon() ring for two to five unrounded rects
  • path() beyond that, and for two or more rects as soon as one carries a rounded corner

The inset strip is the header’s reservation: a positive inset.top, at most 120 px, becomes margin-top and --fkn-inset-top on <html>. Both belong to the library and are removed when the strip retracts. An inline root margin-top of your own is the one thing it cannot restore. Your half of the contract is one line:

app.ts
const
const toolbar: HTMLElement
toolbar
=
var document: Document

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

MDN Reference

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

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

MDN Reference

querySelector
<
interface HTMLElement

The HTMLElement interface represents any HTML element.

MDN Reference

HTMLElement
>('#toolbar')!
const toolbar: HTMLElement
toolbar
.
ElementCSSInlineStyle.style: CSSStyleDeclaration
style
.
CSSStyleDeclaration.position: string
position
= 'fixed'
const toolbar: HTMLElement
toolbar
.
ElementCSSInlineStyle.style: CSSStyleDeclaration
style
.
CSSStyleDeclaration.top: string
top
= 'var(--fkn-inset-top, 0px)' // follows the strip while it is reserved, 0 otherwise

The 0px fallback carries the rest, since the variable is removed rather than set to zero.

When the broker measures a surface as not visible, the library reports it once per surface kind per page load with console.error, naming the cause: not painted, clipped away, or rendered but not seen. The report is advice: nothing is enforced on your page. The clip belongs to the library and the box is yours. Three habits keep the two in agreement:

  • leave the properties named in that message alone on iframe[title="FKN"]
  • keep the frame out from under an ancestor with transform or filter, which position: fixed then resolves against, or with opacity below 1, visibility: hidden or display: none, which hides it
  • never hold the frame’s contentWindow across a shell update, since the document inside it is replaced

With two or more islands on screen, a capturing wheel listener scrolls the nearest scrollable ancestor under the pointer itself and cancels the event. It exists because Chrome picks the scroll target from the clip’s bounding box.

A wheel handler of your own inside that box therefore sees a cancelled event while a card is open. The pointer has to be over one of your elements with something scrollable under it. Otherwise the event is left alone.

A busy token is the library’s record of one thing a broker swap would cut off: an open socket, a streaming response, or a relayed worker. Inside the library, busyAcquire(kind) bumps a per-kind count and returns an idempotent release. registerBusyProbe(kind, probe) registers a live predicate. A probe that throws reads as busy.

None of it is exported. shell.busyReasons() is the only reader, per realm, so an app can tell before it reloads, while nothing in the library acts on it. Who holds which token is on busy tokens. Here is the shape of one holder, the streamed response:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
,
(alias) namespace shell
import shell
shell
} from '@fkn/lib'
const
const response: Response
response
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.fetch(input: ProxyFetchInput, init?: ProxyFetchInit): Promise<Response>
export cloud_d_exports.fetch
fetch
('https://example.org/api/catalog.json')
(alias) namespace shell
import shell
shell
.
shell_d_exports.busyReasons(): string[]
export shell_d_exports.busyReasons

What a shell reload would sever right now, as a list of human-readable reasons, empty when nothing is bound to the broker connection.

This is the evidence for deciding whether to call applyUpdate yet: open sockets and listening servers, a streaming proxy response, a mounted package, a live frame attachment, unflushed write-behind. An empty list is not a promise that a reload is free, only that this realm holds nothing the lib knows about, so an app with state of its own should weigh that too.

It is per REALM, and that distinction has already caused a wrong reading once. A worker that imports @fkn/lib/net keeps its own tally, which this function cannot see from the window. An app whose transfers live in a worker should ask the worker, not the page.

busyReasons
() // ['streamed response (1)'] until the body is read or cancelled
await
const response: Response
response
.
Body.arrayBuffer(): Promise<ArrayBuffer>
arrayBuffer
()
(alias) namespace shell
import shell
shell
.
shell_d_exports.busyReasons(): string[]
export shell_d_exports.busyReasons

What a shell reload would sever right now, as a list of human-readable reasons, empty when nothing is bound to the broker connection.

This is the evidence for deciding whether to call applyUpdate yet: open sockets and listening servers, a streaming proxy response, a mounted package, a live frame attachment, unflushed write-behind. An empty list is not a promise that a reload is free, only that this realm holds nothing the lib knows about, so an app with state of its own should weigh that too.

It is per REALM, and that distinction has already caused a wrong reading once. A worker that imports @fkn/lib/net keeps its own tally, which this function cannot see from the window. An app whose transfers live in a worker should ask the worker, not the page.

busyReasons
() // []

The token is taken on the response head and released by the body, since the bytes keep streaming for as long as you read them. A body you never read keeps the token until the Response is garbage collected, so cancel bodies you will not read. See streaming responses and deadlines.

Everything the platform remembers, the connect token, the keys and the install records, lives inside the broker on the fkn.app origin. The library never sees it. Two rows below name the account copy, the replicated copy of a file in the account. On your own origin the library writes exactly this:

StoreKeyWhat it holds
localStoragefkn:fs:pendingqueued uploads to the account copy, path to base stamp
localStoragefkn:fs:pending-deletequeued deletes on the account copy, same shape
localStoragefkn:fs:adopt-declinedpaths whose adoption the user declined
OPFSthe origin’s root directoryfile bytes, with Blob.type as the content type
memorythe in-memory layer, per realmthe whole working set of fs and opfs

Only the fs and opfs surfaces write any of this, whether you reach them through those entries, their promises variants or the root. Every other entry persists nothing.

The library opens no IndexedDB, sessionStorage, Cache API, BroadcastChannel or cookie. Every localStorage access is wrapped in a try, so a realm without it keeps nothing queued:

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'
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', '[]') // lands in memory, OPFS follows on the next flush
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
.
pending: () => string[]
pending
().
Array<string>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
+
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
.
pendingDeletes: () => string[]
pendingDeletes
().
Array<string>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
// 0, the write is still in memory and nothing is queued yet

The count reads both keys on every call, so you need no cache of your own. In a worker, where localStorage is absent, it stays 0.

Nothing reaches the queue before the flush, 250 ms after the write. The queue’s rules are on listings and pending work.

None of this is API. It rides on postMessage underneath. The shapes are here so you can recognise them in a trace, not send them:

MessageDirectionWhat it does
{ type: 'fkn-api-port' } with a MessagePorta framed library realm, to window.parentasks a parent broker for a channel, answered on the port by { type: 'fkn-api-port-ack' }
osra frames on key fkn-apithe app, to its transportthe announce, every call and every reply
osra frames on key fkn-attach-framethe app, to the /attach-frame page in the iframethe cloud frame handshake
{ type: 'webvpn-udp-port-ack', consumedBytes }a dgram socket, to the broker over its data portflow control, every 512 KiB, when the broker announced dataPortAcks

The fkn-packages-* family and the osra key fkn-packages-connection it hands over are on messages between the two sides.

The library is published to npm, while the broker is a deploy of fkn.app, so the two are rarely the same age. A library pinned in your bundle keeps working against a newer broker because the broker keeps the flat resolvers of @fkn/lib 0.3.x beside the namespaced ones. New overlay fields ride on the pushed state rather than on new methods, for the same reason.

A newer library works against an older broker because of the facade: every member that arrived later is probed with typeof or optional chaining. So shell.updateReady() answers false and readFileSealed falls back to readFile instead of throwing.

Errors follow the same rule. Osra’s error boxer ships name, message, stack and cause only, so the sentences in @fkn/lib/messages are the contract that survives the broker hop and the data plane hop behind it.

A class the broker threw does not exist as the same constructor in your realm. A custom code is gone by the time you see the error. See handling errors.

The extension has its own version seam, an ABI number it announces on <html>:

app.ts
import {
const readExtensionHandshake: () => ExtensionHandshake

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

readExtensionHandshake
,
const supportsOperation: (handshake: ExtensionHandshake, operation: string) => boolean

Whether a named operation is callable.

null operations means the extension could not tell us, and the answer is YES: it predates the ops list and supports the original surface. The ABI floor is the mechanism for refusing an extension that is genuinely too old; this one only refines a list that was actually announced.

supportsOperation
,
const EXTENSION_ABI: 1

What the extension half announces about itself. Increment when the callable surface CHANGES in a way a page could notice: an operation added, removed, renamed, or given different semantics.

This is not the package version and not the manifest version. Those move for reasons that have nothing to do with the protocol (a dependency bump, a store resubmission), and tying the contract to them would make every release look like a protocol change.

EXTENSION_ABI
,
const REQUIRED_EXTENSION_ABI: 0

The oldest extension the page half will talk to. Raise it ONLY when the page half starts depending on something older extensions cannot do, never merely because EXTENSION_ABI moved: an added operation does not break a page that does not call it.

Deliberately 0 for this release. Every extension currently installed predates versioning and announces no ABI at all, so a floor of 1 would refuse all of them the moment this page half deployed, which is precisely the failure this file exists to prevent.

REQUIRED_EXTENSION_ABI
} from '@fkn/lib'
const EXTENSION_ABI: 1

What the extension half announces about itself. Increment when the callable surface CHANGES in a way a page could notice: an operation added, removed, renamed, or given different semantics.

This is not the package version and not the manifest version. Those move for reasons that have nothing to do with the protocol (a dependency bump, a store resubmission), and tying the contract to them would make every release look like a protocol change.

EXTENSION_ABI
// 1, what the current extension announces
const REQUIRED_EXTENSION_ABI: 0

The oldest extension the page half will talk to. Raise it ONLY when the page half starts depending on something older extensions cannot do, never merely because EXTENSION_ABI moved: an added operation does not break a page that does not call it.

Deliberately 0 for this release. Every extension currently installed predates versioning and announces no ABI at all, so a floor of 1 would refuse all of them the moment this page half deployed, which is precisely the failure this file exists to prevent.

REQUIRED_EXTENSION_ABI
// 0, the floor this page accepts, so no installed extension reads as outdated today
const
const handshake: ExtensionHandshake
handshake
=
function readExtensionHandshake(): ExtensionHandshake

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

readExtensionHandshake
()
const handshake: ExtensionHandshake
handshake
.
status: "absent" | "ok" | "outdated"
status
// 'absent', 'ok' or 'outdated'
function supportsOperation(handshake: ExtensionHandshake, operation: string): boolean

Whether a named operation is callable.

null operations means the extension could not tell us, and the answer is YES: it predates the ops list and supports the original surface. The ABI floor is the mechanism for refusing an extension that is genuinely too old; this one only refines a list that was actually announced.

supportsOperation
(
const handshake: ExtensionHandshake
handshake
, 'videoElement') // false unless the status is 'ok'

A handshake with no operations list reads as supporting every operation, since an extension from before versioning serves the whole original surface and cannot say so. That is why the floor is 0 today.

That is the whole mechanism: one frame, one channel, one facade, and a message contract on each side of it. What it cannot hide is in limitations. Every timeout and cap above is in limits and timeouts.