@fluid-app/droplet-bridge 0.0.0-stage → 0.1.1

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