@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 CHANGED
@@ -1,3 +1,120 @@
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
+ ```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.