@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:
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
constframe:HTMLIFrameElement
frame=
var document:Document
window.document returns a reference to the document contained in the window.
In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.
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:
It creates a MessageChannel.
It posts { type: 'fkn-api-port' } to window.parent with target origin https://fkn.app and port2 transferred.
It waits 2 seconds for { type: 'fkn-api-port-ack' } on port1.
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:
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.
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.
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:
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.
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
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:
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.
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.
calling an absent resolver unguarded throws a TypeError, which is swallowed inside an epoch listener
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
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
consttoolbar:HTMLElement
toolbar=
var document:Document
window.document returns a reference to the document contained in the window.
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:
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
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:
Store
Key
What it holds
localStorage
fkn:fs:pending
queued uploads to the account copy, path to base stamp
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:
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>:
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.
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.
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.
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
constREQUIRED_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
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.
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.