@fluid-app/droplet-bridge
v0.1.11
Published
Browser SDK for Fluid embedded app session tokens and context.
Readme
@fluid-app/droplet-bridge
Canonical browser client for embedded droplets.
Developer guide: docs/droplet-bridge-developer-guide.md.
import { createFluidBridge } from "@fluid-app/droplet-bridge";
const bridge = createFluidBridge({ clientId, hostOrigin });
const context = await bridge.getContext();
const token = await bridge.getSessionToken();
await fetch("/api/embedded-action", { method: "POST" });
// Use disableFetchPatch: true to opt out of ambient fetch.
await bridge.fetch("/api/embedded-action", { method: "POST" });
// Component teardown, or explicit shutdown:
bridge.destroy();hostOrigin is the exact HTTP(S) host-shell origin supplied by the shell. The client
accepts only that origin and window.parent. It installs its listener before sending
UUID-v7 bridge.ready, retries at most eight times with bounded backoff until context
arrives, and rejects an incomplete handshake after ten seconds by default. Protocol or
SDK mismatches fail closed with typed errors. Only one client may own a document at a
time; destroy it before creating another. A real navigation's pagehide
(persisted: false) and unload destroy it automatically. A pagehide into the
back/forward cache (persisted: true) leaves the bridge intact — the document is
frozen, not gone — and pageshow on restore resumes an unresolved handshake; an
already-resolved one keeps working unchanged.
getContext() resolves the immutable, unsigned Rails context delivered at handshake;
it never fetches context on demand. getSessionToken() requests a fresh identity-only
token over session_token.refresh_request every time, coalescing concurrent callers
onto one in-flight request. The SDK schedules its own refresh from the token's
lifetime relative to receipt, never from comparing exp against this client's clock —
an admin's local clock can run well ahead of the host's, and a short-lived token must
not be rejected, or refreshed every second, because of it. Decoding is for delivery
and scheduling only: the droplet backend verifies signatures, identity and expiry per
the
verification contract.
The SDK uses in-memory delivery binding against app/store/actor/entry/sid and ignores
replies for unknown or previous-document request IDs.
Explicit and ambient fetch authenticate only exact origins in the server-delivered
fetch.allowedOrigins. There is no iframe appOrigins option. Fluid API origins are
excluded first, even if accidentally listed. Ambient fetch also passes a request
through untouched — never waiting on the handshake — whenever the handshake has
failed, or is still pending and the request targets an origin other than the current
document's own. Explicit disallowed requests reject; ambient disallowed requests pass
through with their original input/options — including a reference to the ambient
wrapper captured before destroy(), for every cross-origin call made through it
afterward; a same-origin call through that same reference keeps failing closed.
Allowed requests preserve Request bodies,
headers and credentials, replace Authorization with the fresh session token, and send
the viewer's active language as Accept-Language from store.locale. Missing locale
fails closed. Relative request URLs resolve against the document's current location,
read at call time, so an in-document route change is honored. Explicit bridge fetch
refuses redirects with redirect: "error" — browsers strip the Authorization header
across a cross-origin redirect, so forwarding it would be silently insecure — and
converts HTTP 401/403 into a typed error. Ambient fetch keeps the caller's own redirect
mode and returns 401/403 as an ordinary Response, since same-origin app code expects to
read its own response status. Network failures return typed errors without response
bodies or raw exception text on both paths. destroy() restores the prior fetch,
cancels timers and rejects dangling requests.
First request and later documents
The URL session_token is first-request server bootstrap only. The SDK strips it
immediately with history.replaceState and never uses it as a browser credential.
The droplet response must set Referrer-Policy: no-referrer before subresources load;
the host iframe’s referrer policy does not protect those subresources. Do not log full
bootstrap URLs or persist bridge tokens in localStorage, sessionStorage, cookies or
window.name.
A full-document navigation inside the iframe keeps the droplet’s current route. Its server must treat any URL token as absent after bootstrap, use its own short HttpOnly app session established from verified first-load identity, or render a bridge-first recovery page. That recovery page initializes this SDK and sends a fresh bridge token to its own backend for verification. A stale URL token must not hard-fail recovery. Every new document initializes the SDK again and obtains fresh context/tokens over postMessage. The host must not rewrite the frozen iframe src to inject another bootstrap token, because it would reset the droplet’s route. An app session cookie stores the app’s own bounded session, never the Fluid bridge token.
Canonical host
createCanonicalBridgeHost uses the shared versioned protocol. The authenticated shell
supplies the registered app origin and fixed mount: client, store, actor, installation,
entry and sid. Create sid before the first URL mint and reuse it for the same mount.
Store/actor/installation/entry/resource changes destroy the host and iframe and create
a new mount; document navigation within the same mount preserves src and sid.
Every valid bridge.ready gets current permission-gated server context and a fresh
mint, including after the first document. A retry of the same in-flight readiness ID
coalesces; a new document ID proceeds even while older work is pending. Readiness and
refresh each have their own bounded pending pool, so a storm of refreshes can never
cost a new document's bridge.ready a host_unavailable. The client ignores old
correlations, binds refreshes to its delivered sid, and coalesces concurrent
getSessionToken() callers onto a single in-flight refresh request. Both host
adapters must honor AbortSignal and perform uncached authorized requests. Refresh
rechecks context and SDK compatibility before minting. Refresh also refuses to mint,
leaving the document's existing context and token in place, if an origin ever
delivered on this mount has since been removed from the live authorized context —
the removal is compared against the union of every origin set delivered by any
bridge.ready on this mount, never against a value a prior refresh already
advanced, so an earlier removal cannot hide behind a later addition. That baseline
is monotonic for the whole mount, not reset by a later bridge.ready: the host
cannot reliably tell a new document's readiness from a duplicate or retried one
from the same document (an SDK retry race, or a bfcache pageshow resend), so a
duplicate ready is never allowed to erase a prior removal. An origin removed
mid-session therefore stops refreshing tokens for the rest of that mount's life,
even for a document that attaches after the removal; reload the admin page to
remount the iframe and pick up the new context. Adding an origin mid-session
refreshes normally. Neither adapter trusts iframe identity or origin declarations.
createFluidBridgeHost remains
the legacy pull host used by the portal until its separately specified member tier and
cutover land.
Session tokens are identity assertions, never Fluid API credentials. Backend API access uses the existing OAuth client_credentials flow with a store resource and installation-scoped permission enforcement. Navigation and shell UI actions are outside this client’s token/context scope.
