You can have a link inspector running on your own dev server in six steps: paste a URL, fetch it through the FKN cloud even though the site sends no CORS headers, show the title it found, keep the last results across reloads, then render the same page in a frame and read its heading. This page covers the install, the fetch and how to check it, the file that survives a reload, the attached frame, and a table of inputs that proves your handler tells a refusal from a rejection.
Nothing on this path needs an account, an install or a consent prompt. A backend is where a call runs, and every call below runs on the cloud backend, FKN’s own infrastructure, which the library reaches through the broker frame, a hidden fkn.app iframe it mounts on your page. How the root exports choose a backend is on backends.
Each step opens with a badge naming where its code runs, [Build] for the bundler configuration and [Page] for app.ts, the app’s own file, as defined on recipes. The page holds a text input, a list and an iframe, which the blocks refer to as input, list and iframe. The markup around them is yours.
In a Vite project, add the library and the plugin:
Terminal window
npminstall@fkn/lib
npminstall--save-dev@fkn/vite-plugin
@fkn/lib is the whole library in one package. The root import reaches buffer, events and stream, three Node modules a browser bundle has to supply, and @fkn/vite-plugin supplies them. It is one entry in your Vite plugins:
Type helper to make it easier to use vite.config.ts
accepts a direct
UserConfig
object, or a function that returns it.
The function receives a
ConfigEnv
object.
defineConfig({
UserConfig.plugins?: PluginOption[] |undefined
Array of vite plugins to use.
plugins: [
functionfkn(options?: {
fs?:boolean;
net?:boolean;
dgram?:boolean;
http?:boolean;
}):Plugin<any>[]
fkn()], // buffer, events and stream are supplied, so the root import of @fkn/lib resolves
})
That entry is the whole build configuration. What else the plugin wires is on @fkn/vite-plugin, and what a bundler other than Vite has to provide is on install.
Load app.ts from your page as a module script. The library appends the broker frame to document.body when it evaluates, so the body has to exist by then, see install.
cloud.fetch takes the same arguments as the platform’s fetch and returns the same Response. The request leaves from the proxy, the FKN server that makes the request on your behalf, so the page’s cross-origin rules do not apply to it and the site needs no CORS header. Fetch the pasted URL and read the title out of the HTML:
url) // through the proxy, so the page's cross-origin rules do not apply
constresponse:Response
response.
Response.ok: boolean
The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.
title// 'Example Domain', the title of example.org, parsed out of the body
response.ok and response.status are the site’s own. response.url is '' because the cloud rebuilds the Response on your side, and that empty string is how you can tell the proxy answered. Wire inspect to the input’s change event and put each result.title in the list.
The root fetch from @fkn/lib makes the same call and picks the extension, the FKN browser extension, when it is on the page. The quickstart pins cloud.fetch so the check in the next step holds on every page. How the root call decides is on fetch().
Paste https://example.org/ and press Enter. Two things show the request went through the cloud: Example Domain appears in the list, and the Network panel of your browser’s developer tools shows no request to example.org. The request that carried it went to the proxy, from the broker frame, and names the target in its fkn-proxy-hostname request header.
A refusal from the proxy is not a rejection. The Response resolves with the proxy’s own status and a JSON body naming the reason, so read response.ok before the body:
fetch('http://192.168.1.10/') // a private address, which the proxy refuses
constresponse:Response
response.
Response.ok: boolean
The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.
The refusal arrived as an ordinary Response. A handler that shows body.error in the list when response.ok is false covers every answer the proxy gives on its own behalf.
When the title does not appear, the failure is in one of five places. Test them in this order:
response.ok is false. A 403 with egress refused (non-public target) means the host resolved to a non-public address or did not resolve at all. A 502 with upstream fetch failed: <e> means the proxy reached the address and the site refused or stalled. A 429 with rate limit exceeded means this caller is past its fair-use limit for now.
The call rejected with FKN cloud.fetch: no proxy is available. The broker, the connection your page holds into FKN, could not read its list of proxies. Retry after 30,000 ms, when it reads the list again.
The call never settles. cloud.fetch waits for the broker with no deadline, so a broker frame that never connects leaves it pending. Look for the hidden fkn.app iframe in document.body. The library appends it when it evaluates, and the body has to exist by then, which a module script guarantees. When the frame is there and the page is served with cross-origin isolation headers, see install.
response.ok is true and the title is empty. The URL answered something other than an HTML document with a title, so there was nothing to parse.
fs is the default file system of @fkn/lib, a path-based subset of Node’s fs that keeps every file on this device and, when an account is connected, a copy in the account. The account is the FKN identity a person carries between sites, and nothing here connects one, so the list stays on this device. The three file systems are on storage.
flush() // the write to this device runs now rather than on the timer
}
mount() fills the in-memory layer, the copy of every file that the synchronous calls read. The promise forms await it on their own, so calling it first only moves that wait to page load. A write goes to memory and reaches this device’s storage 250 ms later, or when flush() runs, and a reload after either still lists it.
Read it back when the page loads, and treat a missing file as an empty list:
app.ts
const
constload: () =>Promise<Result[]>
load=async ():
interfacePromise<T>
Represents the completion of an asynchronous operation
Promise<
typeResult= {
url:string;
title:string;
status:number;
}
Result[]> => {
try {
return
varJSON: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.
readFile('library/links.json', 'utf8'))) // the results from the last visit
} catch (
function (localvar) error: unknown
error) {
if ((
function (localvar) error: unknown
erroras
namespaceNodeJS
NodeJS.
interfaceNodeJS.ErrnoException
ErrnoException).
NodeJS.ErrnoException.code?: string |undefined
code==='ENOENT') return [] // nothing saved yet, so the list starts empty
throw
function (localvar) error: unknown
error
}
}
const
constresults:Result[]
results=await
constload: () =>Promise<Result[]>
load() // [] on the first visit, and the saved list after a reload
The read is wrapped in String(...) because the return type is Buffer | string whatever encoding you pass. A missing file rejects with an Error whose code is ENOENT, the one code that means nothing is there, so the first visit starts empty rather than failing. Every other code is rethrown, see error codes. The account’s own file system, cloud.fs, reports absence as a StorageNotFoundError that isNotFound() recognises, described on the same page.
attachFrame() puts another site inside your iframe and hands back a Frame you navigate and drive, an attached frame. With nothing installed, the root call takes the render proxy, the cloud frame backend, 150 ms after the document has loaded without the extension. The iframe has to be in the document already, with no sandbox attribute or one that includes allow-scripts and allow-same-origin:
goto('https://example.org/') // resolves once example.org has loaded inside your iframe
The site renders inside your own iframe, under an identity bar the cloud draws naming your app. goto() resolves on the frame’s load event, and every wait on the way to a working frame is bounded, see timeouts.
The demo below is this step on a real site. It attaches a blank iframe, navigates it to the Wikipedia search page and restyles it, then reads the heading, fills the search box and presses the search button, on the render proxy when no extension is on the page:
message// 'fetch refuses FKN platform domains (fkn.app)', thrown before anything left the page
}
Nothing left the page, so there is no Response to read. A handler that shows the title for the first row, body.error for the second and third, and the thrown message for the fourth tells the three shapes apart rather than merely not crashing.
With the extension on the page, a request can leave from the user’s own browser and carry their session, with the cloud as the fallback when it is not there, see use the extension or the cloud per request.
connect() asks the person to use their FKN account on this site, which gives fs a second copy of every file in the account and cloud.quota() a metered volume to report, see account and quota.
@fkn/lib/net and @fkn/lib/dgram are Node’s net and dgram in the browser, with the bytes carried through the relay, the server that holds the real socket at the far end, see TCP and UDP sockets.
A worker holds no broker of its own, so the page relays it with await relayWorker(worker, { unregisterSignal }) and the engine inside opens sockets exactly as a page would, see run sockets in a worker.
A package is an npm module FKN loads on a sandbox origin of its own, and your app installs one, connects to it and draws it inside its own layout, see install a package and show its UI.
A production build supplies the same three shims, and a page served with cross-origin isolation headers needs two more lines in the config, see install.