Your app sends each request through the user’s own browser session when the FKN extension is on the page, and through the FKN cloud when it is not. The install offer appears inside your own interface rather than as the card the library opens on its own. This page covers the read that tells the two cases apart, the header probe, the credentialed fetch, the redirect that answers status 0, the install offer, and the fallback.
The extension is the FKN browser extension. It announces itself by marking the page’s <html> element, and isExtensionExposed() reads that marker at the moment you call. The content script lands a tick after the document starts, so the read answers false both when there is no extension and when nobody has answered yet. Your app keeps the third state itself, undefined until the wait settles:
app.ts
import {
constisExtensionExposed: () =>boolean
Unchanged on purpose, and still exported: a page half built before versioning calls exactly this,
and an extension that announces an ABI must keep answering it the same way. Backwards
compatibility here runs in BOTH directions, which is the only property that matters when neither
end updates on demand.
setMissingExtensionHandler(null) // before the first extension call, so absence rejects instead of opening the install card
let
let exposed:boolean|undefined
exposed:boolean|undefined// undefined until the wait below settles
functionisExtensionExposed():boolean
Unchanged on purpose, and still exported: a page half built before versioning calls exactly this,
and an extension that announces an ABI must keep answering it the same way. Backwards
compatibility here runs in BOTH directions, which is the only property that matters when neither
end updates on demand.
isExtensionExposed() // false, the same answer for absent and for not answered yet
extension.fetch builds a new Request(input, init) first, and the platform drops the forbidden request headers there, cookie among them. A cookie you set on the init is gone from the request the extension sees. @fkn/lib reads the names in FORGEABLE_HEADERS, the forbidden headers it can put back, off your init.headers separately and restores them on the first hop. Whether cookie is in that set is what to probe, rather than which version you installed:
credentials: 'include' attaches the cookies the browser holds for the URL, httpOnly ones included, and asks the user first through the consent sheet, the prompt the extension shows before an action above severity 0. Pass a reason the user will read there. A refusal arrives as a plain Error named PermissionDeniedError, and only its name and message survive the hop out of the extension, so test the name:
A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials.
credentials: 'include',
reason?: string |undefined
reason: 'Load the catalog from your own example.org account',
})
constresponse:Response
response.
Response.status: number
The status read-only property of the Response interface contains the HTTP status codes of the response.
With redirect at its default, 'follow', the extension follows the redirect and hands you the final response. Ask for redirect: 'manual' and a redirect answers with status 0: the opaque redirect the extension’s service worker saw, revived on your side as Response.error(). It is not a failure. It says the upstream redirected and this call did not follow. The browser refuses to rebuild such a response, since new Response(body, { status: 0 }) throws a RangeError, so branch on the status before you wrap or forward anything:
A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect.
redirect: 'manual' })
constresponse:Response
response.
Response.status: number
The status read-only property of the Response interface contains the HTTP status codes of the response.
status===0// true when the upstream answered a redirect this call did not follow
When redirected is true, re-issue the call with redirect: 'follow' or take the cloud path. cloud.fetch never follows a redirect, so a 301 or 302 arrives there as an ordinary Response with a location header, see cloud.fetch().
Importing @fkn/lib registers a missing-extension handler. By default an extension.* call that finds no extension runs it, and it opens the install card the broker draws. The broker is the connection your page holds into FKN. Step 1 removed that handler, so promptInstall(reason) is now the only thing that opens the card, from your own button, with your reason and a store link on it:
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
promptInstall('Load the catalog from your own account, with no cloud in between')
constinstalled:boolean
installed// true when the extension showed up while the card was open, false when it was dismissed
})
promptInstall resolves true at once when the extension is already there, and false without asking in a worker. It draws the card through the broker frame, the hidden fkn.app iframe the library mounts, and waits for the broker with no deadline. Show the offer only while exposed is false, and let the statuschange event flip your interface when the install lands. when the extension is missing owns the default handler.
The root fetch reads the marker at the moment of the call. With the extension on the page it takes the extension with credentials forced to 'omit', and otherwise the cloud, through the proxy, the FKN server that makes the request on your behalf. Neither carries a session of the user’s, so a refusal at the sheet and a dismissed card end on the same line:
app.ts
const
constloadCatalog: () =>Promise<Response|null>
loadCatalog=async () => {
if (
constexposed:boolean|undefined
exposed===
var undefined
undefined) returnnull// nobody has answered yet, so draw nothing and ask again later
const
constown:Response|null
own=
constexposed:boolean
exposed?await
constloadAsUser: () =>Promise<Response|null>
loadAsUser() :null// the session path, null when the user declined
json() // the same catalog, from the public side of example.org
The branch comes from the three states of step 1 and loadAsUser from step 3. The root fetch closes the path. Only response.url tells you which backend answered, a backend being where a call runs. Pin cloud.fetch instead when the request must leave from the cloud whatever the page has, see how the root fetch decides.
With the extension on the page, loadCatalog() resolves a Response whose url is the final URL and whose body is the catalog of the signed-in account. With it absent, the same call resolves a Response whose url is '', through the cloud. When neither happens, find the point of failure here:
exposed stays undefined, or isExtensionExposed() answers false with the extension installed. The wait has not settled, and it takes up to 1,000 ms. Nobody has answered yet, which is not absence, so draw nothing and read exposed after step 1.
The call throws fetch: refusing to forge request header(s): cookie. The extension enforces the set with a copy of its own, and the two copies can disagree when one is older. Leave the cookie off that call.
loadAsUser() resolves null every time. The user declined, or a stored denial covers https://example.org. The user lifts a stored denial from the extension’s own popup, and the fallback is the answer, see a refusal neither guard matches.