@fluid-app/droplet-bridge 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -2
- package/dist/index.cjs +1248 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +243 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +243 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1214 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -3
package/README.md
CHANGED
|
@@ -1,3 +1,120 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fluid-app/droplet-bridge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Canonical browser client for embedded droplets.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createFluidBridge } from "@fluid-app/droplet-bridge";
|
|
7
|
+
|
|
8
|
+
const bridge = createFluidBridge({ clientId, hostOrigin });
|
|
9
|
+
const context = await bridge.getContext();
|
|
10
|
+
const token = await bridge.getSessionToken();
|
|
11
|
+
await fetch("/api/embedded-action", { method: "POST" });
|
|
12
|
+
// Use disableFetchPatch: true to opt out of ambient fetch.
|
|
13
|
+
await bridge.fetch("/api/embedded-action", { method: "POST" });
|
|
14
|
+
// Component teardown, or explicit shutdown:
|
|
15
|
+
bridge.destroy();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`hostOrigin` is the exact HTTP(S) host-shell origin supplied by the shell. The client
|
|
19
|
+
accepts only that origin and `window.parent`. It installs its listener before sending
|
|
20
|
+
UUID-v7 `bridge.ready`, retries at most eight times with bounded backoff until context
|
|
21
|
+
arrives, and rejects an incomplete handshake after ten seconds by default. Protocol or
|
|
22
|
+
SDK mismatches fail closed with typed errors. Only one client may own a document at a
|
|
23
|
+
time; destroy it before creating another. A real navigation's `pagehide`
|
|
24
|
+
(`persisted: false`) and `unload` destroy it automatically. A `pagehide` into the
|
|
25
|
+
back/forward cache (`persisted: true`) leaves the bridge intact — the document is
|
|
26
|
+
frozen, not gone — and `pageshow` on restore resumes an unresolved handshake; an
|
|
27
|
+
already-resolved one keeps working unchanged.
|
|
28
|
+
|
|
29
|
+
`getContext()` resolves the immutable, unsigned Rails context delivered at handshake;
|
|
30
|
+
it never fetches context on demand. `getSessionToken()` requests a fresh identity-only
|
|
31
|
+
token over `session_token.refresh_request` every time, coalescing concurrent callers
|
|
32
|
+
onto one in-flight request. The SDK schedules its own refresh from the token's
|
|
33
|
+
lifetime relative to receipt, never from comparing `exp` against this client's clock —
|
|
34
|
+
an admin's local clock can run well ahead of the host's, and a short-lived token must
|
|
35
|
+
not be rejected, or refreshed every second, because of it. Decoding is for delivery
|
|
36
|
+
and scheduling only: the droplet backend verifies signatures, identity and expiry per
|
|
37
|
+
the
|
|
38
|
+
[verification contract](../../../apps/rails/docs/droplet-session-token-verification-contract.md).
|
|
39
|
+
The SDK uses in-memory delivery binding against app/store/actor/entry/sid and ignores
|
|
40
|
+
replies for unknown or previous-document request IDs.
|
|
41
|
+
|
|
42
|
+
Explicit and ambient fetch authenticate only exact origins in the server-delivered
|
|
43
|
+
`fetch.allowedOrigins`. There is no iframe `appOrigins` option. Fluid API origins are
|
|
44
|
+
excluded first, even if accidentally listed. Ambient fetch also passes a request
|
|
45
|
+
through untouched — never waiting on the handshake — whenever the handshake has
|
|
46
|
+
failed, or is still pending and the request targets an origin other than the current
|
|
47
|
+
document's own. Explicit disallowed requests reject; ambient disallowed requests pass
|
|
48
|
+
through with their original input/options — including a reference to the ambient
|
|
49
|
+
wrapper captured before `destroy()`, for every cross-origin call made through it
|
|
50
|
+
afterward; a same-origin call through that same reference keeps failing closed.
|
|
51
|
+
Allowed requests preserve Request bodies,
|
|
52
|
+
headers and credentials, replace Authorization with the fresh session token, and send
|
|
53
|
+
the viewer's active language as `Accept-Language` from `store.locale`. Missing locale
|
|
54
|
+
fails closed. Relative request URLs resolve against the document's current location,
|
|
55
|
+
read at call time, so an in-document route change is honored. Explicit bridge fetch
|
|
56
|
+
refuses redirects with `redirect: "error"` — browsers strip the Authorization header
|
|
57
|
+
across a cross-origin redirect, so forwarding it would be silently insecure — and
|
|
58
|
+
converts HTTP 401/403 into a typed error. Ambient fetch keeps the caller's own redirect
|
|
59
|
+
mode and returns 401/403 as an ordinary Response, since same-origin app code expects to
|
|
60
|
+
read its own response status. Network failures return typed errors without response
|
|
61
|
+
bodies or raw exception text on both paths. `destroy()` restores the prior fetch,
|
|
62
|
+
cancels timers and rejects dangling requests.
|
|
63
|
+
|
|
64
|
+
## First request and later documents
|
|
65
|
+
|
|
66
|
+
The URL `session_token` is **first-request server bootstrap only**. The SDK strips it
|
|
67
|
+
immediately with `history.replaceState` and never uses it as a browser credential.
|
|
68
|
+
The droplet response must set `Referrer-Policy: no-referrer` before subresources load;
|
|
69
|
+
the host iframe’s referrer policy does not protect those subresources. Do not log full
|
|
70
|
+
bootstrap URLs or persist bridge tokens in localStorage, sessionStorage, cookies or
|
|
71
|
+
window.name.
|
|
72
|
+
|
|
73
|
+
A full-document navigation inside the iframe keeps the droplet’s current route. Its
|
|
74
|
+
server must treat any URL token as absent after bootstrap, use its own short HttpOnly
|
|
75
|
+
app session established from verified first-load identity, or render a bridge-first
|
|
76
|
+
recovery page. That recovery page initializes this SDK and sends a fresh bridge token
|
|
77
|
+
to its own backend for verification. A stale URL token must not hard-fail recovery.
|
|
78
|
+
Every new document initializes the SDK again and obtains fresh context/tokens over
|
|
79
|
+
postMessage. The host must **not rewrite the frozen iframe src** to inject another
|
|
80
|
+
bootstrap token, because it would reset the droplet’s route. An app session cookie
|
|
81
|
+
stores the app’s own bounded session, never the Fluid bridge token.
|
|
82
|
+
|
|
83
|
+
## Canonical host
|
|
84
|
+
|
|
85
|
+
`createCanonicalBridgeHost` uses the shared versioned protocol. The authenticated shell
|
|
86
|
+
supplies the registered app origin and fixed mount: client, store, actor, installation,
|
|
87
|
+
entry and sid. Create sid before the first URL mint and reuse it for the same mount.
|
|
88
|
+
Store/actor/installation/entry/resource changes destroy the host and iframe and create
|
|
89
|
+
a new mount; document navigation within the same mount preserves src and sid.
|
|
90
|
+
|
|
91
|
+
Every valid `bridge.ready` gets current permission-gated server context and a fresh
|
|
92
|
+
mint, including after the first document. A retry of the same in-flight readiness ID
|
|
93
|
+
coalesces; a new document ID proceeds even while older work is pending. Readiness and
|
|
94
|
+
refresh each have their own bounded pending pool, so a storm of refreshes can never
|
|
95
|
+
cost a new document's `bridge.ready` a `host_unavailable`. The client ignores old
|
|
96
|
+
correlations, binds refreshes to its delivered sid, and coalesces concurrent
|
|
97
|
+
`getSessionToken()` callers onto a single in-flight refresh request. Both host
|
|
98
|
+
adapters must honor AbortSignal and perform uncached authorized requests. Refresh
|
|
99
|
+
rechecks context and SDK compatibility before minting. Refresh also refuses to mint,
|
|
100
|
+
leaving the document's existing context and token in place, if an origin ever
|
|
101
|
+
delivered on this mount has since been removed from the live authorized context —
|
|
102
|
+
the removal is compared against the union of every origin set delivered by any
|
|
103
|
+
`bridge.ready` on this mount, never against a value a prior refresh already
|
|
104
|
+
advanced, so an earlier removal cannot hide behind a later addition. That baseline
|
|
105
|
+
is monotonic for the whole mount, not reset by a later `bridge.ready`: the host
|
|
106
|
+
cannot reliably tell a new document's readiness from a duplicate or retried one
|
|
107
|
+
from the same document (an SDK retry race, or a bfcache `pageshow` resend), so a
|
|
108
|
+
duplicate ready is never allowed to erase a prior removal. An origin removed
|
|
109
|
+
mid-session therefore stops refreshing tokens for the rest of that mount's life,
|
|
110
|
+
even for a document that attaches after the removal; reload the admin page to
|
|
111
|
+
remount the iframe and pick up the new context. Adding an origin mid-session
|
|
112
|
+
refreshes normally. Neither adapter trusts iframe identity or origin declarations.
|
|
113
|
+
`createFluidBridgeHost` remains
|
|
114
|
+
the legacy pull host used by the portal until its separately specified member tier and
|
|
115
|
+
cutover land.
|
|
116
|
+
|
|
117
|
+
Session tokens are identity assertions, never Fluid API credentials. Backend API
|
|
118
|
+
access uses the existing OAuth client_credentials flow with a store resource and
|
|
119
|
+
installation-scoped permission enforcement. Navigation and shell UI actions are
|
|
120
|
+
outside this client’s token/context scope.
|