@fkn/lib is one package with 30 exports: 29 import paths that carry code, plus its manifest. This page lists every entry, what it gives you, what importing it starts, and the guide that explains it.
The subpaths let you carry one piece of the library at a time. Every one of the 29 ships an ESM .js, a CJS .cjs and one bundled .d.ts. What each one pulls in, and whether importing it opens the broker (the connection your app holds into FKN), is under what each entry reaches.
A subpath and the matching root namespace are one implementation. The choice between them is about what the bundle carries and what the import starts, never about what a call does:
The narrow entry gives you the root’s cloud.fetch and nothing else. The root’s bare fetch is a different function: it picks a backend on every call. A backend is where a call runs: the FKN cloud, the FKN browser extension or the desktop. See backends.
Several entries name the account and the account copy. The account is the FKN identity a person carries between sites, and the account copy is the copy of each file kept in the account. Here is every export:
search, pick, install, list, uninstall, show, hide, connect, mount, attach, onConnect, isVisible, onVisibilityChange, their types, and the PackagesError type you match on error.code
The package exports its manifest for tooling that reads the version. Nothing else in it is reachable by path. The exact signature of every name above is in the generated API reference.
The root is the whole library under one import, and importing it is not free. When the module evaluates in a window, it mounts the broker frame or adopts one the page already holds. The broker frame is the hidden fkn.app iframe that carries the broker.
The one exception is a nested realm whose parent broker hands it a port. A realm is one JavaScript execution context, such as a window or a worker. The exact conditions are on how it works.
Importing also registers the handler that opens the install card when an extension call finds no extension:
What a shell reload would sever right now, as human-readable reasons, empty when nothing is bound
to the broker connection: open sockets and listening servers, a streaming proxy response, a mounted
package, a live frame attachment, unflushed write-behind. Empty is not a promise that a reload is
free, only that this realm holds nothing the lib knows about.
It is per REALM: a worker that imports @fkn/lib/net keeps its own tally, which the window cannot
see. An app whose transfers live in a worker should ask the worker, not the page.
busyReasons() // [] while nothing in this realm holds a busy token
The root’s fetch, attachFrame and extension are the library’s own, not the ones @fkn/lib/extension exports under the same names. Of that entry, only events and available are missing from the root by name, and both are still reachable as extension.events and extension.available.
shell exists only in the root: the package publishes no @fkn/lib/shell subpath. Its busyReasons() lists the busy tokens held in this realm, each one a reason the realm reports itself busy, such as an open socket or a relayed worker. A relayed worker keeps its own tally, which the page never sees.
readdir('library') // ['catalog.json'] once the app wrote it, always a broker round trip
@fkn/lib/fs keeps its bytes in this device’s OPFS and in the account copy, behind one in-memory layer. The root’s fs namespace reads and writes that same layer. See storage.
@fkn/lib/opfs keeps them in this device’s OPFS alone and never reaches the broker frame.
@fkn/lib/cloud/fs keeps them in the account alone and has no in-memory layer, so it is async only. The *Sync members, the default export, and mount, flush, remount, pull, appendFile and exists are all absent from it.
Every read above is typed Buffer | string whatever encoding you pass. See TypeScript.
The /promises paths export the promise members by name. On the two device-side entries the same object is also the default export. @fkn/lib/cloud/fs/promises has no default export.
@fkn/lib/cloud is the cloud backend pinned. Its fetch never consults the extension, and its attachFrame always uses the render proxy, the cloud backend for frames, even when the extension is installed. The four Node subpaths under it re-export @fkn/lib/net, @fkn/lib/dgram, @fkn/lib/http and @fkn/lib/dns, so there is exactly one implementation behind every spelling:
The namespace and the subpath answer the same call. @fkn/lib/cloud/fetch is the narrow one: fetch alone, with no stream shim behind it, for when you cannot carry @fkn/lib/cloud.
cloud.fetch sends every request through the proxy. What the proxy answers is on fetch().
Four entries give you Node’s net, dgram, http and dns shapes. The sockets run over the relay, which holds the real socket at the far end, and the lookup goes through the broker. They work on the page and inside a worker the page relayed:
Not every entry needs the same shims. The published files import buffer, events and stream as bare specifiers only where a shape uses them, and the four shapes above are where events and stream are used:
Every entry that reaches osra mounts or adopts the broker frame when it evaluates in a window, the way the root does. The others never do. So @fkn/lib/cloud/fetch is narrow in what it carries, not in what it starts.
osra, ip-address and react are ordinary npm packages a bundler resolves on its own. The shims a browser bundle has to supply are buffer, events and stream. @fkn/vite-plugin aliases the Node names to these paths and supplies the shims, with no alias for dns.
@fkn/lib/extension is everything the FKN browser extension exposes to a page, plus available, events and promptInstall. @fkn/lib/desktop is a typed placeholder for a backend that does not exist yet:
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
Typed placeholder for the planned desktop backend. Always false in this release.
available() // false
available() answers on both namespaces, and only desktop answers false everywhere. extension.available() reads whether the content script has marked this page at the moment you call it, so it is false in a worker and before that mark lands. See backends.
@fkn/lib/account and @fkn/lib/packages hold the same functions as the root namespaces. A package, an npm module FKN loads on a sandbox origin of its own, imports onConnect from the second at boot. @fkn/lib/react is the only entry that imports react, an optional peer at >=18:
Install a package for this app behind an FKN-rendered confirm, or with { noConfirm: true } for a notice instead of a prompt. Resolves null when the user declines.
Install a package for this app behind an FKN-rendered confirm, or with { noConfirm: true } for a notice instead of a prompt. Resolves null when the user declines.
install('npm:@example/subtitles-plugin') // the install record, or null when the person pressed Not now
@fkn/lib/rooms holds available, open, create and join, and the Room they resolve carries everything else. It is also the rooms namespace on the root:
app.ts
import {
constavailable: () =>Promise<boolean>
Whether this realm can join a room: false in Node, false in a worker nothing bridged, false against a shell older than named rooms. Answers rather than rejecting.
Whether this realm can join a room: false in Node, false in a worker nothing bridged, false against a shell older than named rooms. Answers rather than rejecting.
available()) { // false in Node, and in a worker nobody relayed
key and id as one string, the thing to put in a link
invite// the key and the id, joined by a dot, for a URL fragment
}
The key is minted in the browser and the platform never holds it, and a member’s id is derived for each room from its key. See rooms and run a chat room.
@fkn/lib/storage holds available, put, get, list and delete. It is also the storage namespace on the root, which is where delete reads most naturally, since a named import has to rename it:
app.ts
const {
consturl:string
https://cdn.fkn.app/<uuid> in production: anyone holding it can download the sealed bytes, and nothing more
Seals the data in the broker and uploads it, then answers its url and the key that opens it: the
app's own key unchanged, or the one the broker minted. Resolves once the object is ready. Needs
a signed-in account, and counts the sealed size (28 bytes more per 1 MiB) against the account's
storage from the moment it starts until the object is deleted or the upload is aborted. The account
the call belongs to is the one this page is on when it is made, as for every storage call.
data is a Blob (a File included) or a ReadableStream<Uint8Array> with its size. An upload
survives the network dropping a part, not a reload: a reload starts the file again, and the upload
left behind stops counting within the hour.
Refused invalid for a stream without its size or one that delivers another, a file past the
service's part ceiling, or a key that is not 32 bytes base64url; denied with no account; quota;
too-many past the account's stored objects or this hour's uploads; account-changed.
file) // the url anyone may fetch, and the key that opens it
await
functionremove(url:string):Promise<void>
Deletes an object by its url: every later get of it answers not-found, and an upload in progress
is aborted and stops counting. Only the app that stored it (the same app once it is verified)
deletes it, denied otherwise. An object already gone resolves.
remove(
consturl:string
https://cdn.fkn.app/<uuid> in production: anyone holding it can download the sealed bytes, and nothing more
url) // the only way the object ends
The broker in the fkn.app frame seals and opens every object, and FKN’s servers never hold the key. See object storage.
@fkn/lib/contract carries the broker’s types plus a few constants, such as ROOM_TEMPORARY_MS and STORAGE_REFUSALS. Resolvers is the surface the broker exposes, and the rest is the data vocabulary those resolvers exchange. @fkn/lib/messages is five string constants: three message prefixes you match with startsWith, two of which continue into a sentence, and two error codes:
The flat members at the bottom duplicate members of cloud and overlay. They predate the
namespaced form and are kept because a published consumer may still be calling them: they are
contract, not dead code.
The error code for "the api never answered". A caller that relaxes anything when the server is
unreachable must never relax it on an answered error: an answered 500 lands exactly when a key
rotation or a setting flip may be propagating. Across the SharedWorker hop the message prefix is
the wire contract, since osra drops the code; same-realm callers can test the code.
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.
The flat members at the bottom duplicate members of cloud and overlay. They predate the
namespaced form and are kept because a published consumer may still be calling them: they are
contract, not dead code.
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.
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(
constE2E_STALE_EPOCH_MESSAGE:"fkn:e2e-stale-epoch: this file is encrypted under a previous key you reset"
The error code for "the api never answered". A caller that relaxes anything when the server is
unreachable must never relax it on an answered error: an answered 500 lands exactly when a key
rotation or a setting flip may be propagating. Across the SharedWorker hop the message prefix is
the wire contract, since osra drops the code; same-realm callers can test the code.
@fkn/lib/wire is the numeric codes a socket option carries to the relay. @fkn/lib/attach-policy is the pure half of the cloud backend’s frame.fetch check. Both run locally, and neither touches the broker:
app.ts
import {
constTCP_OPTION_NODELAY:0
Socket option codes as they appear on the WebVPN wire, and the option shapes built from them.
Also the ack a UDP data-port consumer posts back to the broker that produces the port's batches.
Owned by the library so both sides share one source: the packet codecs in
src/api/webvpn/packets/ import from this file, and the library needs nothing from the app.
The VALUES are protocol: the u8 discriminant of a tagged union inside a TcpSetOption or
UdpSetOption client packet (metadata-stream tag 8). A relay built against these numbers deploys
independently of any browser holding this file, so renumbering one is a wire break. Booleans go
on the wire as u8, 0 for false and anything else true.
Bare hostnames only: lowercase, no scheme, no port, no path, deduped in first-seen order, plus a
bracketed IPv6 literal when ipv6 is set. Anything else is dropped rather than repaired, so a
malformed declaration narrows the app's reach instead of widening it. The sheet, the cloud card
and the middle page all run this one function.
Socket option codes as they appear on the WebVPN wire, and the option shapes built from them.
Also the ack a UDP data-port consumer posts back to the broker that produces the port's batches.
Owned by the library so both sides share one source: the packet codecs in
src/api/webvpn/packets/ import from this file, and the library needs nothing from the app.
The VALUES are protocol: the u8 discriminant of a tagged union inside a TcpSetOption or
UdpSetOption client packet (metadata-stream tag 8). A relay built against these numbers deploys
independently of any browser holding this file, so renumbering one is a wire break. Booleans go
on the wire as u8, 0 for false and anything else true.
Bare hostnames only: lowercase, no scheme, no port, no path, deduped in first-seen order, plus a
bracketed IPv6 literal when ipv6 is set. Anything else is dropped rather than repaired, so a
malformed declaration narrows the app's reach instead of widening it. The sheet, the cloud card
and the middle page all run this one function.
setNoDelay and setKeepAlive on a TCP socket, and setTTL and setBroadcast on a UDP one, send these codes for you. See socket options. The numbers are public because renumbering one would break compatibility with the relay.
normalizeDeclaredHosts drops a scheme, port or path rather than repairing it, so a malformed declaration narrows what your frame may reach.
frameFetchVerdict answers a refusal or a consent request from the facts of the attached frame, the Frame that attachFrame returns. The cloud backend runs it before every frame.fetch leaves the page. See fetching as the frame.
@fkn/lib/api is the broker connection itself, for an app that needs to observe it rather than use it through a wrapper. apiPromise settles once a broker connection exists and never rejects. It resolves the facade, the object that carries the broker’s resolvers and routes every call to the newest broker connection.
apiWithin is the bounded form that net and dgram use internally, and it resolves the same facade. It waits 8,000 ms the first time, and 1,000 ms for every later wait once any deadline was missed:
onApiEpoch and currentApiEpoch expose the broker epochs, each a broker generation. When a broker is replaced, the library re-sends on the new epoch every registration that died with the old one. See how it works and errors and lifecycle.
apiLastCall() answers a millisecond timestamp of the last call through the facade, and 0 before any. apiInFlight() answers the number of calls pending.
What supplying the shims above takes, with or without the plugin, is on install. Every type these paths export is on TypeScript.