@fanvue/builder-sdk 0.3.0 → 0.4.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 +77 -1
- package/dist/bridge/index.d.ts +2 -0
- package/dist/bridge/index.js +3 -0
- package/dist/bridge-CGVtI3hr.js +336 -0
- package/dist/bridge-CGVtI3hr.js.map +1 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core-BqdYUMrJ.js +1653 -0
- package/dist/core-BqdYUMrJ.js.map +1 -0
- package/dist/index-BRDYLBlc.d.ts +223 -0
- package/dist/index-BRDYLBlc.d.ts.map +1 -0
- package/dist/{index-CweVyIKX.d.ts → index-C4ewLil3.d.ts} +2 -2
- package/dist/{index-CweVyIKX.d.ts.map → index-C4ewLil3.d.ts.map} +1 -1
- package/dist/index-ClbZoV_Z.d.ts +420 -0
- package/dist/index-ClbZoV_Z.d.ts.map +1 -0
- package/dist/{index-pS9wR5yg.d.ts → index-Dr-mZ0qP.d.ts} +500 -901
- package/dist/index-Dr-mZ0qP.d.ts.map +1 -0
- package/dist/nextjs/embedded-app/index.d.ts +2 -2
- package/dist/nextjs/embedded-app/index.js +3 -2
- package/dist/nextjs/embedded-app/index.js.map +1 -1
- package/dist/nextjs/off-platform/index.d.ts +2 -2
- package/dist/nextjs/off-platform/index.js +3 -2
- package/dist/nextjs/off-platform/index.js.map +1 -1
- package/dist/{nextjs-CESI_EiU.js → nextjs-B5Tqgt_n.js} +2 -2
- package/dist/{nextjs-CESI_EiU.js.map → nextjs-B5Tqgt_n.js.map} +1 -1
- package/dist/react/index.d.ts +37 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +61 -2
- package/dist/react/index.js.map +1 -1
- package/dist/{core-CvVOMyqr.js → schemas-DbyHF7Xi.js} +261 -1662
- package/dist/schemas-DbyHF7Xi.js.map +1 -0
- package/package.json +20 -16
- package/dist/core-CvVOMyqr.js.map +0 -1
- package/dist/index-pS9wR5yg.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ this SDK covers both:
|
|
|
6
6
|
|
|
7
7
|
| Building an... | Import | What you get | Docs |
|
|
8
8
|
|---|---|---|---|
|
|
9
|
-
| **Embedded App** (runs inside Fanvue, in an iframe) | `@fanvue/builder-sdk/nextjs/embedded-app` + `@fanvue/builder-sdk/react` | Session-token exchange handler, `useEmbeddedAuth` hook, Bearer sessions with auto refresh | [Overview](https://api.fanvue.com/docs/app-store/embedded-apps/overview) · [Integration guide](https://api.fanvue.com/docs/app-store/embedded-apps/integration-guide) |
|
|
9
|
+
| **Embedded App** (runs inside Fanvue, in an iframe) | `@fanvue/builder-sdk/nextjs/embedded-app` + `@fanvue/builder-sdk/react` | Session-token exchange handler, `useEmbeddedAuth` hook, Bearer sessions with auto refresh, analytics through the host page | [Overview](https://api.fanvue.com/docs/app-store/embedded-apps/overview) · [Integration guide](https://api.fanvue.com/docs/app-store/embedded-apps/integration-guide) |
|
|
10
10
|
| **Off-Platform App** ("Login with Fanvue" on your own domain) | `@fanvue/builder-sdk/nextjs/off-platform` | Full-page redirect flow, httpOnly cookie sessions, auto token refresh | [Auth overview](https://api.fanvue.com/docs/authentication/overview) · [Implementation guide](https://api.fanvue.com/docs/authentication/implementation-guide) |
|
|
11
11
|
|
|
12
12
|
Building something else (Node, Deno, a custom server)? The core `@fanvue/builder-sdk`
|
|
@@ -138,6 +138,70 @@ export const { POST } = createSessionExchangeHandler(createConfig(), {
|
|
|
138
138
|
See the [embedded apps integration guide](https://api.fanvue.com/docs/app-store/embedded-apps/integration-guide)
|
|
139
139
|
for the full walkthrough, including app registration in the Builder.
|
|
140
140
|
|
|
141
|
+
### Analytics (embedded apps)
|
|
142
|
+
|
|
143
|
+
Embedded apps can fire product analytics through the Fanvue page hosting them,
|
|
144
|
+
so events land in Fanvue's analytics stitched into the viewer's session --
|
|
145
|
+
without your app ever handling identity data.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
"use client";
|
|
149
|
+
import { useFanvueAnalytics } from "@fanvue/builder-sdk/react";
|
|
150
|
+
|
|
151
|
+
export function CreateCourseButton() {
|
|
152
|
+
const { track } = useFanvueAnalytics();
|
|
153
|
+
return <button onClick={() => track("course_created", { chapters: 4 })}>Create course</button>;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
That is the whole integration -- no provider, no configuration. `track` is
|
|
158
|
+
fire-and-forget: it never throws, and outside Fanvue (local dev, previews, a
|
|
159
|
+
standalone deployment) it silently does nothing, so you can call it
|
|
160
|
+
unconditionally.
|
|
161
|
+
|
|
162
|
+
**Rules your events must follow:**
|
|
163
|
+
|
|
164
|
+
- Event names match `/^[a-z0-9_]{1,64}$/`. Fanvue emits them prefixed with
|
|
165
|
+
`embedded_app_`, so `course_created` arrives as `embedded_app_course_created`,
|
|
166
|
+
attributed to your app automatically.
|
|
167
|
+
- Properties are a flat record of at most 20 keys (max 64 characters each) with
|
|
168
|
+
`string` (max 256 characters), `number` or `boolean` values. No nesting.
|
|
169
|
+
- Identity is Fanvue's to set: `user_id`, `device_id`, `revenue` and any
|
|
170
|
+
`$`-prefixed key are rejected.
|
|
171
|
+
|
|
172
|
+
Analytics is granted per app by Fanvue, so `track` may legitimately be a no-op
|
|
173
|
+
for your app. `isEnabled` tells you which:
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
const { track, isEnabled } = useFanvueAnalytics();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Outside React**, connect to the bridge directly:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { connectFanvueBridge } from "@fanvue/builder-sdk/bridge";
|
|
183
|
+
|
|
184
|
+
const result = await connectFanvueBridge();
|
|
185
|
+
if (result.isOk() && result.value.has("analytics")) {
|
|
186
|
+
await result.value.analytics.track("course_created", { chapters: 4 });
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`connectFanvueBridge` handshakes with the host page: your app announces itself,
|
|
191
|
+
Fanvue verifies your registered embed origin and replies with the capabilities
|
|
192
|
+
it granted plus a private `MessagePort` carrying all further traffic. It fails
|
|
193
|
+
with `NOT_EMBEDDED` when the app is not in an iframe and `TIMEOUT` when no
|
|
194
|
+
`hello` arrives within the timeout (default 3s) -- neither is exceptional, and
|
|
195
|
+
apps are expected to keep working standalone.
|
|
196
|
+
|
|
197
|
+
> [!NOTE]
|
|
198
|
+
> Host support for the bridge is being rolled out per environment. Until it is
|
|
199
|
+
> enabled for the environment your app is running in, the handshake times out
|
|
200
|
+
> and every `track` call no-ops -- exactly as it does outside Fanvue. So a
|
|
201
|
+
> timeout does not necessarily mean your app is misconfigured. If events are not
|
|
202
|
+
> arriving from a live embedded surface, confirm the bridge is enabled there
|
|
203
|
+
> before you go looking for a bug in your integration.
|
|
204
|
+
|
|
141
205
|
### Off-Platform App ("Login with Fanvue")
|
|
142
206
|
|
|
143
207
|
The fastest path: create a config file and three route handlers.
|
|
@@ -400,6 +464,18 @@ Error types: `OAuthError` (token exchange/refresh), `EmbeddedAuthError` (delegat
|
|
|
400
464
|
| `AuthProvider` | Context provider. Manages JWT storage in `sessionStorage`. |
|
|
401
465
|
| `useAuth()` | Returns `{ jwt, isAuthenticated, setJwt, clearJwt, authFetch }` |
|
|
402
466
|
| `useEmbeddedAuth(opts?)` | Returns `{ status, error, theme }`. Exchanges the embedded session token on mount; `theme` is the creator's colour scheme (`'light' \| 'dark' \| null`). |
|
|
467
|
+
| `useFanvueAnalytics()` | Returns `{ track, isEnabled }`. Fires events through the host page; no-ops outside Fanvue. |
|
|
468
|
+
|
|
469
|
+
### Bridge (`@fanvue/builder-sdk/bridge`)
|
|
470
|
+
|
|
471
|
+
The host-to-app capability protocol. Most apps only need `useFanvueAnalytics`.
|
|
472
|
+
|
|
473
|
+
| Export | Description |
|
|
474
|
+
|---|---|
|
|
475
|
+
| `connectFanvueBridge(opts?)` | Handshakes with the host page. Returns `Result<FanvueBridge, BridgeConnectError>` (`NOT_EMBEDDED` or `TIMEOUT`) |
|
|
476
|
+
| `FanvueBridge` | `{ capabilities, has(capability), analytics.track(eventName, properties?) }` |
|
|
477
|
+
| `analyticsTrackPayloadSchema` | Zod schema for the `analytics.track` payload -- the same schema the host validates against |
|
|
478
|
+
| Protocol constants and schemas | `BRIDGE_VERSION`, `BRIDGE_READY_TYPE`, `BRIDGE_HELLO_TYPE`, `bridgeRequestSchema`, `bridgeResponseSchema` |
|
|
403
479
|
|
|
404
480
|
## Examples
|
|
405
481
|
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { C as bridgeCapabilitySchema, D as bridgeRequestSchema, E as bridgeReadyMessageSchema, O as bridgeResponseSchema, S as analyticsTrackPayloadSchema, T as bridgeHelloMessageSchema, _ as BridgeReadyMessage, a as FanvueBridge, b as analyticsEventNameSchema, c as ANALYTICS_TRACK_METHOD, d as BRIDGE_HELLO_TYPE, f as BRIDGE_READY_TYPE, g as BridgeHelloMessage, h as BridgeErrorCode, i as BridgeRequestError, l as AnalyticsProperties, m as BridgeCapability, n as ConnectFanvueBridgeOptions, o as FanvueBridgeAnalytics, p as BRIDGE_VERSION, r as connectFanvueBridge, s as ANALYTICS_CAPABILITY, t as BridgeConnectError, u as AnalyticsTrackPayload, v as BridgeRequest, w as bridgeErrorCodeSchema, x as analyticsPropertiesSchema, y as BridgeResponse } from "../index-BRDYLBlc.js";
|
|
2
|
+
export { ANALYTICS_CAPABILITY, ANALYTICS_TRACK_METHOD, AnalyticsProperties, AnalyticsTrackPayload, BRIDGE_HELLO_TYPE, BRIDGE_READY_TYPE, BRIDGE_VERSION, BridgeCapability, BridgeConnectError, BridgeErrorCode, BridgeHelloMessage, BridgeReadyMessage, BridgeRequest, BridgeRequestError, BridgeResponse, ConnectFanvueBridgeOptions, FanvueBridge, FanvueBridgeAnalytics, analyticsEventNameSchema, analyticsPropertiesSchema, analyticsTrackPayloadSchema, bridgeCapabilitySchema, bridgeErrorCodeSchema, bridgeHelloMessageSchema, bridgeReadyMessageSchema, bridgeRequestSchema, bridgeResponseSchema, connectFanvueBridge };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import "../schemas-DbyHF7Xi.js";
|
|
2
|
+
import { a as BRIDGE_READY_TYPE, c as analyticsPropertiesSchema, d as bridgeErrorCodeSchema, f as bridgeHelloMessageSchema, h as bridgeResponseSchema, i as BRIDGE_HELLO_TYPE, l as analyticsTrackPayloadSchema, m as bridgeRequestSchema, n as ANALYTICS_CAPABILITY, o as BRIDGE_VERSION, p as bridgeReadyMessageSchema, r as ANALYTICS_TRACK_METHOD, s as analyticsEventNameSchema, t as connectFanvueBridge, u as bridgeCapabilitySchema } from "../bridge-CGVtI3hr.js";
|
|
3
|
+
export { ANALYTICS_CAPABILITY, ANALYTICS_TRACK_METHOD, BRIDGE_HELLO_TYPE, BRIDGE_READY_TYPE, BRIDGE_VERSION, analyticsEventNameSchema, analyticsPropertiesSchema, analyticsTrackPayloadSchema, bridgeCapabilitySchema, bridgeErrorCodeSchema, bridgeHelloMessageSchema, bridgeReadyMessageSchema, bridgeRequestSchema, bridgeResponseSchema, connectFanvueBridge };
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { a as number, c as string, d as prettifyError, f as require_index_cjs, i as literal, l as union, n as array, o as object, r as boolean, s as record, t as _enum, u as unknown } from "./schemas-DbyHF7Xi.js";
|
|
2
|
+
//#region src/bridge/protocol.ts
|
|
3
|
+
/**
|
|
4
|
+
* The bridge protocol version spoken by this SDK.
|
|
5
|
+
*
|
|
6
|
+
* Both sides send it on every message. Changes within a version must be
|
|
7
|
+
* additive; anything breaking increments the version, and the host answers the
|
|
8
|
+
* handshake with the highest version both sides speak.
|
|
9
|
+
*/
|
|
10
|
+
const BRIDGE_VERSION = 1;
|
|
11
|
+
/**
|
|
12
|
+
* Message type the embedded app posts to the host to open a handshake.
|
|
13
|
+
*
|
|
14
|
+
* An app may re-announce (it re-posts `ready` until a `hello` lands, in case
|
|
15
|
+
* the host attached its listener late).
|
|
16
|
+
*/
|
|
17
|
+
const BRIDGE_READY_TYPE = "fanvue:bridge:ready";
|
|
18
|
+
/**
|
|
19
|
+
* Message type the host posts back, carrying the granted capabilities and the port.
|
|
20
|
+
*
|
|
21
|
+
* The host answers each verified `ready` with a fresh `hello` and port,
|
|
22
|
+
* closing the prior port; the app adopts the newest `hello`.
|
|
23
|
+
*/
|
|
24
|
+
const BRIDGE_HELLO_TYPE = "fanvue:bridge:hello";
|
|
25
|
+
/** Capability namespace for analytics (`analytics.track`). */
|
|
26
|
+
const ANALYTICS_CAPABILITY = "analytics";
|
|
27
|
+
/** Method name for tracking an analytics event. */
|
|
28
|
+
const ANALYTICS_TRACK_METHOD = "track";
|
|
29
|
+
/** The host capabilities an app can be granted. Grants are resolved server-side. */
|
|
30
|
+
const bridgeCapabilitySchema = _enum([ANALYTICS_CAPABILITY]);
|
|
31
|
+
/**
|
|
32
|
+
* Error codes the host can return for a request.
|
|
33
|
+
*
|
|
34
|
+
* - `capability_denied` — the app was not granted the requested capability.
|
|
35
|
+
* - `invalid_payload` — the payload failed the host's schema for the method.
|
|
36
|
+
* - `rate_limited` — the app exceeded the host's budget for the method.
|
|
37
|
+
* - `internal` — the host handler failed unexpectedly.
|
|
38
|
+
*/
|
|
39
|
+
const bridgeErrorCodeSchema = _enum([
|
|
40
|
+
"capability_denied",
|
|
41
|
+
"invalid_payload",
|
|
42
|
+
"rate_limited",
|
|
43
|
+
"internal"
|
|
44
|
+
]);
|
|
45
|
+
/** Schema for the `ready` message an app posts to `window.parent`. */
|
|
46
|
+
const bridgeReadyMessageSchema = object({
|
|
47
|
+
type: literal(BRIDGE_READY_TYPE),
|
|
48
|
+
v: literal(1)
|
|
49
|
+
});
|
|
50
|
+
/** Schema for the `hello` message the host posts back with the transferred port. */
|
|
51
|
+
const bridgeHelloMessageSchema = object({
|
|
52
|
+
type: literal(BRIDGE_HELLO_TYPE),
|
|
53
|
+
v: literal(1),
|
|
54
|
+
capabilities: array(bridgeCapabilitySchema)
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Schema for a request sent app → host over the port.
|
|
58
|
+
*
|
|
59
|
+
* Capability and method names are bounded so a misbehaving sender cannot make
|
|
60
|
+
* either side hold or log arbitrarily large strings.
|
|
61
|
+
*/
|
|
62
|
+
const bridgeRequestSchema = object({
|
|
63
|
+
v: literal(1),
|
|
64
|
+
id: string().min(1).max(64),
|
|
65
|
+
kind: literal("request"),
|
|
66
|
+
capability: string().min(1).max(64),
|
|
67
|
+
method: string().min(1).max(64),
|
|
68
|
+
payload: unknown()
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* Schema for a response sent host → app over the port.
|
|
72
|
+
*
|
|
73
|
+
* The `id` and `error.message` are bounded like the request fields, so a
|
|
74
|
+
* misbehaving host cannot push oversized strings into the app.
|
|
75
|
+
*/
|
|
76
|
+
const bridgeResponseSchema = object({
|
|
77
|
+
v: literal(1),
|
|
78
|
+
id: string().max(64),
|
|
79
|
+
kind: literal("response"),
|
|
80
|
+
ok: boolean(),
|
|
81
|
+
result: unknown().optional(),
|
|
82
|
+
error: object({
|
|
83
|
+
code: bridgeErrorCodeSchema,
|
|
84
|
+
message: string().max(1024)
|
|
85
|
+
}).optional()
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* Event names an app may emit.
|
|
89
|
+
*
|
|
90
|
+
* The host prefixes every name with `embedded_app_` before it reaches
|
|
91
|
+
* Amplitude, so an app can never emit a core platform event. Names are
|
|
92
|
+
* snake_case to match the platform's tracking-plan convention.
|
|
93
|
+
*/
|
|
94
|
+
const analyticsEventNameSchema = string().regex(/^[a-z0-9_]{1,64}$/);
|
|
95
|
+
/**
|
|
96
|
+
* Property keys reserved by the host.
|
|
97
|
+
*
|
|
98
|
+
* Identity and revenue are host-owned: the host page's Amplitude client is
|
|
99
|
+
* already identified as the viewer, and an app must not be able to overwrite
|
|
100
|
+
* that or fabricate revenue.
|
|
101
|
+
*/
|
|
102
|
+
const ANALYTICS_RESERVED_PROPERTY_KEYS = [
|
|
103
|
+
"user_id",
|
|
104
|
+
"device_id",
|
|
105
|
+
"revenue"
|
|
106
|
+
];
|
|
107
|
+
/** Maximum number of properties on a single event. */
|
|
108
|
+
const ANALYTICS_MAX_PROPERTY_KEYS = 20;
|
|
109
|
+
/** Whether a property key is one an app is allowed to set. */
|
|
110
|
+
function isAllowedAnalyticsPropertyKey(key) {
|
|
111
|
+
return !ANALYTICS_RESERVED_PROPERTY_KEYS.includes(key) && !key.startsWith("$");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Properties attached to an analytics event: a flat record of short scalars.
|
|
115
|
+
*
|
|
116
|
+
* Nesting is rejected so the payload stays cheap to validate and to read in
|
|
117
|
+
* Amplitude, and `$`-prefixed keys are rejected because Amplitude reserves them.
|
|
118
|
+
* Numbers must be finite — `Infinity` and `NaN` do not survive serialisation.
|
|
119
|
+
*/
|
|
120
|
+
const analyticsPropertiesSchema = record(string().min(1).max(64), union([
|
|
121
|
+
string().max(256),
|
|
122
|
+
number().finite(),
|
|
123
|
+
boolean()
|
|
124
|
+
])).refine((properties) => Object.keys(properties).length <= ANALYTICS_MAX_PROPERTY_KEYS, { message: `An event may carry at most ${ANALYTICS_MAX_PROPERTY_KEYS} properties` }).refine((properties) => Object.keys(properties).every(isAllowedAnalyticsPropertyKey), { message: `Properties may not use a reserved key (${ANALYTICS_RESERVED_PROPERTY_KEYS.join(", ")}) or a key starting with "$"` });
|
|
125
|
+
/** Schema for the payload of `analytics.track`. */
|
|
126
|
+
const analyticsTrackPayloadSchema = object({
|
|
127
|
+
eventName: analyticsEventNameSchema,
|
|
128
|
+
properties: analyticsPropertiesSchema.optional()
|
|
129
|
+
});
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/bridge/bridge.ts
|
|
132
|
+
var import_index_cjs = require_index_cjs();
|
|
133
|
+
/**
|
|
134
|
+
* Builds the {@link FanvueBridge} served over an established port.
|
|
135
|
+
*
|
|
136
|
+
* Requests are correlated to responses by `id`; anything arriving on the port
|
|
137
|
+
* that is not a well-formed response for a request still in flight is ignored,
|
|
138
|
+
* so a misbehaving host cannot resolve a request twice or crash the app.
|
|
139
|
+
*
|
|
140
|
+
* Apps get this from {@link connectFanvueBridge} rather than calling it directly.
|
|
141
|
+
*
|
|
142
|
+
* @param ctx - The port, granted capabilities, and per-request timeout.
|
|
143
|
+
* @returns The bridge exposed to the app.
|
|
144
|
+
*/
|
|
145
|
+
function createFanvueBridge(ctx) {
|
|
146
|
+
const { capabilities, requestTimeoutMs } = ctx;
|
|
147
|
+
const pending = /* @__PURE__ */ new Map();
|
|
148
|
+
let requestSequence = 0;
|
|
149
|
+
let port = ctx.port;
|
|
150
|
+
function onPortMessage(event) {
|
|
151
|
+
const parsed = bridgeResponseSchema.safeParse(event.data);
|
|
152
|
+
if (!parsed.success) return;
|
|
153
|
+
const inFlight = pending.get(parsed.data.id);
|
|
154
|
+
if (!inFlight) return;
|
|
155
|
+
pending.delete(parsed.data.id);
|
|
156
|
+
clearTimeout(inFlight.timer);
|
|
157
|
+
inFlight.resolve(parsed.data);
|
|
158
|
+
}
|
|
159
|
+
function attach(next) {
|
|
160
|
+
next.addEventListener("message", onPortMessage);
|
|
161
|
+
next.start();
|
|
162
|
+
}
|
|
163
|
+
attach(port);
|
|
164
|
+
function adoptPort(next) {
|
|
165
|
+
port.close();
|
|
166
|
+
port = next;
|
|
167
|
+
attach(next);
|
|
168
|
+
}
|
|
169
|
+
function has(capability) {
|
|
170
|
+
return capabilities.includes(capability);
|
|
171
|
+
}
|
|
172
|
+
async function request(capability, method, payload) {
|
|
173
|
+
if (!has(capability)) return (0, import_index_cjs.err)({
|
|
174
|
+
code: "capability_denied",
|
|
175
|
+
message: `The host did not grant the "${capability}" capability to this app.`
|
|
176
|
+
});
|
|
177
|
+
requestSequence += 1;
|
|
178
|
+
const id = `${capability}-${requestSequence}`;
|
|
179
|
+
const message = {
|
|
180
|
+
v: 1,
|
|
181
|
+
id,
|
|
182
|
+
kind: "request",
|
|
183
|
+
capability,
|
|
184
|
+
method,
|
|
185
|
+
payload
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
port.postMessage(message);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return (0, import_index_cjs.err)({
|
|
191
|
+
code: "PORT_CLOSED",
|
|
192
|
+
message: `Could not send the request to the host: ${error instanceof Error ? error.message : String(error)}`
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const settled = await new Promise((resolve) => {
|
|
196
|
+
const timer = setTimeout(() => {
|
|
197
|
+
pending.delete(id);
|
|
198
|
+
resolve(null);
|
|
199
|
+
}, requestTimeoutMs);
|
|
200
|
+
pending.set(id, {
|
|
201
|
+
resolve,
|
|
202
|
+
timer
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
if (settled === null) return (0, import_index_cjs.err)({
|
|
206
|
+
code: "TIMEOUT",
|
|
207
|
+
message: `The host did not answer "${capability}.${method}" within ${requestTimeoutMs}ms.`
|
|
208
|
+
});
|
|
209
|
+
if (!settled.ok) return (0, import_index_cjs.err)({
|
|
210
|
+
code: settled.error?.code ?? "internal",
|
|
211
|
+
message: settled.error?.message ?? "The host rejected the request without a reason."
|
|
212
|
+
});
|
|
213
|
+
return (0, import_index_cjs.ok)(settled.result);
|
|
214
|
+
}
|
|
215
|
+
async function track(eventName, properties) {
|
|
216
|
+
const payload = analyticsTrackPayloadSchema.safeParse(properties === void 0 ? { eventName } : {
|
|
217
|
+
eventName,
|
|
218
|
+
properties
|
|
219
|
+
});
|
|
220
|
+
if (!payload.success) return (0, import_index_cjs.err)({
|
|
221
|
+
code: "invalid_payload",
|
|
222
|
+
message: prettifyError(payload.error)
|
|
223
|
+
});
|
|
224
|
+
return (await request(ANALYTICS_CAPABILITY, ANALYTICS_TRACK_METHOD, payload.data)).map(() => void 0);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
capabilities,
|
|
228
|
+
has,
|
|
229
|
+
analytics: { track },
|
|
230
|
+
adoptPort
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/bridge/connect.ts
|
|
235
|
+
/** How long to wait for the host's `hello` before giving up. */
|
|
236
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 3e3;
|
|
237
|
+
/** How long to wait for a response to a single request once connected. */
|
|
238
|
+
const REQUEST_TIMEOUT_MS = 3e3;
|
|
239
|
+
/**
|
|
240
|
+
* How often to re-post `ready` until the handshake settles.
|
|
241
|
+
*
|
|
242
|
+
* The host attaches its listener asynchronously (its capability grants come
|
|
243
|
+
* from a query), so a single `ready` posted right after the iframe loads can
|
|
244
|
+
* arrive before anyone is listening. Re-announcing until a `hello` lands (or
|
|
245
|
+
* the timeout fires) closes that race.
|
|
246
|
+
*/
|
|
247
|
+
const READY_REANNOUNCE_INTERVAL_MS = 500;
|
|
248
|
+
/**
|
|
249
|
+
* Opens the capability bridge to the Fanvue page hosting this app.
|
|
250
|
+
*
|
|
251
|
+
* Posts `fanvue:bridge:ready` to `window.parent` and waits for the host's
|
|
252
|
+
* `fanvue:bridge:hello`, which names the granted capabilities and transfers the
|
|
253
|
+
* `MessagePort` all later traffic flows over. The host verifies the frame's
|
|
254
|
+
* origin before replying, so a page framed by anything other than its
|
|
255
|
+
* registered Fanvue surface simply times out.
|
|
256
|
+
*
|
|
257
|
+
* `ready` is re-posted every 500ms until a `hello` lands (or the timeout
|
|
258
|
+
* fires), because the host may attach its listener after the app's first
|
|
259
|
+
* announce. The host answers each verified `ready` with a fresh `hello` and
|
|
260
|
+
* port, closing the prior port — so the bridge keeps listening for the
|
|
261
|
+
* lifetime of the connection and adopts the newest `hello` by swapping onto
|
|
262
|
+
* its port. Requests in flight on a superseded port fail by their timeouts.
|
|
263
|
+
*
|
|
264
|
+
* Calling this again is safe and is how an app reconnects after the iframe
|
|
265
|
+
* reloads: the host closes the previous port and re-handshakes.
|
|
266
|
+
*
|
|
267
|
+
* Failure is not exceptional — apps are expected to keep working standalone
|
|
268
|
+
* (local dev, previews) where the connection never succeeds.
|
|
269
|
+
*
|
|
270
|
+
* @param options - Optional overrides (e.g. a shorter `timeoutMs`).
|
|
271
|
+
* @returns A `Result` with the connected {@link FanvueBridge}, or a
|
|
272
|
+
* {@link BridgeConnectError} when there is no host to talk to.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* const result = await connectFanvueBridge();
|
|
276
|
+
* if (result.isOk() && result.value.has('analytics')) {
|
|
277
|
+
* await result.value.analytics.track('course_created', { chapters: 4 });
|
|
278
|
+
* }
|
|
279
|
+
*/
|
|
280
|
+
function connectFanvueBridge(options) {
|
|
281
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
282
|
+
if (typeof window === "undefined" || window.parent === window) return Promise.resolve((0, import_index_cjs.err)({
|
|
283
|
+
code: "NOT_EMBEDDED",
|
|
284
|
+
message: "The app is not running inside a Fanvue iframe, so there is no host to connect to."
|
|
285
|
+
}));
|
|
286
|
+
const host = window.parent;
|
|
287
|
+
return new Promise((resolve) => {
|
|
288
|
+
let settled = false;
|
|
289
|
+
let bridge = null;
|
|
290
|
+
const timer = setTimeout(() => {
|
|
291
|
+
finish((0, import_index_cjs.err)({
|
|
292
|
+
code: "TIMEOUT",
|
|
293
|
+
message: `The Fanvue host did not complete the handshake within ${timeoutMs}ms.`
|
|
294
|
+
}));
|
|
295
|
+
}, timeoutMs);
|
|
296
|
+
const reannounce = setInterval(postReady, READY_REANNOUNCE_INTERVAL_MS);
|
|
297
|
+
function finish(result) {
|
|
298
|
+
if (settled) return;
|
|
299
|
+
settled = true;
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
clearInterval(reannounce);
|
|
302
|
+
if (result.isErr()) window.removeEventListener("message", onMessage);
|
|
303
|
+
resolve(result);
|
|
304
|
+
}
|
|
305
|
+
function onMessage(event) {
|
|
306
|
+
if (event.source !== host) return;
|
|
307
|
+
const hello = bridgeHelloMessageSchema.safeParse(event.data);
|
|
308
|
+
if (!hello.success) return;
|
|
309
|
+
const port = event.ports[0];
|
|
310
|
+
if (!port) return;
|
|
311
|
+
if (bridge) {
|
|
312
|
+
bridge.adoptPort(port);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
bridge = createFanvueBridge({
|
|
316
|
+
port,
|
|
317
|
+
capabilities: hello.data.capabilities,
|
|
318
|
+
requestTimeoutMs: REQUEST_TIMEOUT_MS
|
|
319
|
+
});
|
|
320
|
+
finish((0, import_index_cjs.ok)(bridge));
|
|
321
|
+
}
|
|
322
|
+
window.addEventListener("message", onMessage);
|
|
323
|
+
function postReady() {
|
|
324
|
+
const ready = {
|
|
325
|
+
type: BRIDGE_READY_TYPE,
|
|
326
|
+
v: 1
|
|
327
|
+
};
|
|
328
|
+
host.postMessage(ready, "*");
|
|
329
|
+
}
|
|
330
|
+
postReady();
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
//#endregion
|
|
334
|
+
export { BRIDGE_READY_TYPE as a, analyticsPropertiesSchema as c, bridgeErrorCodeSchema as d, bridgeHelloMessageSchema as f, bridgeResponseSchema as h, BRIDGE_HELLO_TYPE as i, analyticsTrackPayloadSchema as l, bridgeRequestSchema as m, ANALYTICS_CAPABILITY as n, BRIDGE_VERSION as o, bridgeReadyMessageSchema as p, ANALYTICS_TRACK_METHOD as r, analyticsEventNameSchema as s, connectFanvueBridge as t, bridgeCapabilitySchema as u };
|
|
335
|
+
|
|
336
|
+
//# sourceMappingURL=bridge-CGVtI3hr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-CGVtI3hr.js","names":["z.enum","z.object","z.literal","z.array","z.string","z.unknown","z.boolean","z\n .record","z.union","z.number","z.prettifyError"],"sources":["../src/bridge/protocol.ts","../src/bridge/bridge.ts","../src/bridge/connect.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * The bridge protocol version spoken by this SDK.\n *\n * Both sides send it on every message. Changes within a version must be\n * additive; anything breaking increments the version, and the host answers the\n * handshake with the highest version both sides speak.\n */\nexport const BRIDGE_VERSION = 1;\n\n/**\n * Message type the embedded app posts to the host to open a handshake.\n *\n * An app may re-announce (it re-posts `ready` until a `hello` lands, in case\n * the host attached its listener late).\n */\nexport const BRIDGE_READY_TYPE = 'fanvue:bridge:ready';\n\n/**\n * Message type the host posts back, carrying the granted capabilities and the port.\n *\n * The host answers each verified `ready` with a fresh `hello` and port,\n * closing the prior port; the app adopts the newest `hello`.\n */\nexport const BRIDGE_HELLO_TYPE = 'fanvue:bridge:hello';\n\n/** Capability namespace for analytics (`analytics.track`). */\nexport const ANALYTICS_CAPABILITY = 'analytics';\n\n/** Method name for tracking an analytics event. */\nexport const ANALYTICS_TRACK_METHOD = 'track';\n\n/** The host capabilities an app can be granted. Grants are resolved server-side. */\nexport const bridgeCapabilitySchema = z.enum([ANALYTICS_CAPABILITY]);\n\n/** A host capability an app can be granted. */\nexport type BridgeCapability = z.infer<typeof bridgeCapabilitySchema>;\n\n/**\n * Error codes the host can return for a request.\n *\n * - `capability_denied` — the app was not granted the requested capability.\n * - `invalid_payload` — the payload failed the host's schema for the method.\n * - `rate_limited` — the app exceeded the host's budget for the method.\n * - `internal` — the host handler failed unexpectedly.\n */\nexport const bridgeErrorCodeSchema = z.enum([\n 'capability_denied',\n 'invalid_payload',\n 'rate_limited',\n 'internal',\n]);\n\n/** An error code the host can return for a request. */\nexport type BridgeErrorCode = z.infer<typeof bridgeErrorCodeSchema>;\n\n/** Schema for the `ready` message an app posts to `window.parent`. */\nexport const bridgeReadyMessageSchema = z.object({\n type: z.literal(BRIDGE_READY_TYPE),\n v: z.literal(BRIDGE_VERSION),\n});\n\n/** The `ready` message an app posts to `window.parent` to open a handshake. */\nexport type BridgeReadyMessage = z.infer<typeof bridgeReadyMessageSchema>;\n\n/** Schema for the `hello` message the host posts back with the transferred port. */\nexport const bridgeHelloMessageSchema = z.object({\n type: z.literal(BRIDGE_HELLO_TYPE),\n v: z.literal(BRIDGE_VERSION),\n capabilities: z.array(bridgeCapabilitySchema),\n});\n\n/** The `hello` message the host posts back, alongside the transferred port. */\nexport type BridgeHelloMessage = z.infer<typeof bridgeHelloMessageSchema>;\n\n/**\n * Schema for a request sent app → host over the port.\n *\n * Capability and method names are bounded so a misbehaving sender cannot make\n * either side hold or log arbitrarily large strings.\n */\nexport const bridgeRequestSchema = z.object({\n v: z.literal(BRIDGE_VERSION),\n id: z.string().min(1).max(64),\n kind: z.literal('request'),\n capability: z.string().min(1).max(64),\n method: z.string().min(1).max(64),\n payload: z.unknown(),\n});\n\n/** A request sent app → host over the port, correlated to a response by `id`. */\nexport type BridgeRequest = z.infer<typeof bridgeRequestSchema>;\n\n/**\n * Schema for a response sent host → app over the port.\n *\n * The `id` and `error.message` are bounded like the request fields, so a\n * misbehaving host cannot push oversized strings into the app.\n */\nexport const bridgeResponseSchema = z.object({\n v: z.literal(BRIDGE_VERSION),\n id: z.string().max(64),\n kind: z.literal('response'),\n ok: z.boolean(),\n result: z.unknown().optional(),\n error: z.object({ code: bridgeErrorCodeSchema, message: z.string().max(1024) }).optional(),\n});\n\n/** A response sent host → app over the port, correlated to a request by `id`. */\nexport type BridgeResponse = z.infer<typeof bridgeResponseSchema>;\n\n/**\n * Event names an app may emit.\n *\n * The host prefixes every name with `embedded_app_` before it reaches\n * Amplitude, so an app can never emit a core platform event. Names are\n * snake_case to match the platform's tracking-plan convention.\n */\nexport const analyticsEventNameSchema = z.string().regex(/^[a-z0-9_]{1,64}$/);\n\n/**\n * Property keys reserved by the host.\n *\n * Identity and revenue are host-owned: the host page's Amplitude client is\n * already identified as the viewer, and an app must not be able to overwrite\n * that or fabricate revenue.\n */\nconst ANALYTICS_RESERVED_PROPERTY_KEYS: readonly string[] = ['user_id', 'device_id', 'revenue'];\n\n/** Maximum number of properties on a single event. */\nconst ANALYTICS_MAX_PROPERTY_KEYS = 20;\n\n/** Whether a property key is one an app is allowed to set. */\nfunction isAllowedAnalyticsPropertyKey(key: string): boolean {\n return !ANALYTICS_RESERVED_PROPERTY_KEYS.includes(key) && !key.startsWith('$');\n}\n\n/**\n * Properties attached to an analytics event: a flat record of short scalars.\n *\n * Nesting is rejected so the payload stays cheap to validate and to read in\n * Amplitude, and `$`-prefixed keys are rejected because Amplitude reserves them.\n * Numbers must be finite — `Infinity` and `NaN` do not survive serialisation.\n */\nexport const analyticsPropertiesSchema = z\n .record(\n z.string().min(1).max(64),\n z.union([z.string().max(256), z.number().finite(), z.boolean()]),\n )\n .refine((properties) => Object.keys(properties).length <= ANALYTICS_MAX_PROPERTY_KEYS, {\n message: `An event may carry at most ${ANALYTICS_MAX_PROPERTY_KEYS} properties`,\n })\n .refine((properties) => Object.keys(properties).every(isAllowedAnalyticsPropertyKey), {\n message: `Properties may not use a reserved key (${ANALYTICS_RESERVED_PROPERTY_KEYS.join(', ')}) or a key starting with \"$\"`,\n });\n\n/** Schema for the payload of `analytics.track`. */\nexport const analyticsTrackPayloadSchema = z.object({\n eventName: analyticsEventNameSchema,\n properties: analyticsPropertiesSchema.optional(),\n});\n\n/** The payload of `analytics.track`. */\nexport type AnalyticsTrackPayload = z.infer<typeof analyticsTrackPayloadSchema>;\n\n/** Properties attached to an analytics event. */\nexport type AnalyticsProperties = z.infer<typeof analyticsPropertiesSchema>;\n","import { err, ok } from 'neverthrow';\nimport { z } from 'zod';\n\nimport {\n ANALYTICS_CAPABILITY,\n ANALYTICS_TRACK_METHOD,\n BRIDGE_VERSION,\n analyticsTrackPayloadSchema,\n bridgeResponseSchema,\n type AnalyticsProperties,\n type BridgeCapability,\n type BridgeErrorCode,\n type BridgeRequest,\n type BridgeResponse,\n} from './protocol.js';\n\nimport type { Result } from 'neverthrow';\n\n/**\n * Why a bridge request failed.\n *\n * Beyond the codes the host can return, the SDK adds `TIMEOUT` (the host never\n * answered) and `PORT_CLOSED` (the message could not be put on the port —\n * in practice the payload was not structured-cloneable). Note that\n * `postMessage` on a closed port silently drops rather than throwing, so a\n * torn-down host surfaces as `TIMEOUT`, not `PORT_CLOSED`.\n */\nexport interface BridgeRequestError {\n code: BridgeErrorCode | 'TIMEOUT' | 'PORT_CLOSED';\n message: string;\n}\n\n/** The analytics capability, as exposed on a connected bridge. */\nexport interface FanvueBridgeAnalytics {\n track: (\n eventName: string,\n properties?: AnalyticsProperties,\n ) => Promise<Result<void, BridgeRequestError>>;\n}\n\n/**\n * A live connection to the Fanvue host page.\n *\n * @property capabilities - The capabilities the host granted this app.\n * @property has - Whether a given capability was granted. Checking is\n * optional: calling a capability without checking is safe and returns\n * `capability_denied`.\n * @property analytics - Fires events through the host page's Amplitude client.\n */\nexport interface FanvueBridge {\n capabilities: readonly BridgeCapability[];\n has: (capability: BridgeCapability) => boolean;\n analytics: FanvueBridgeAnalytics;\n}\n\n/**\n * A {@link FanvueBridge} plus the internals `connectFanvueBridge` drives.\n *\n * Not exported from the public entry: apps only ever see {@link FanvueBridge}.\n *\n * @property adoptPort - Swaps the underlying port for the one carried by a\n * newer `hello`, closing the old port. Requests in flight on the old port\n * fail by their existing timeouts.\n */\nexport interface InternalFanvueBridge extends FanvueBridge {\n adoptPort: (port: MessagePort) => void;\n}\n\n/**\n * Everything {@link createFanvueBridge} needs to serve requests.\n *\n * @property port - The `MessagePort` transferred by the host in its `hello`.\n * @property capabilities - The capabilities named in the host's `hello`.\n * @property requestTimeoutMs - How long to wait for a response before failing.\n */\nexport interface FanvueBridgeContext {\n port: MessagePort;\n capabilities: readonly BridgeCapability[];\n requestTimeoutMs: number;\n}\n\ninterface PendingRequest {\n resolve: (response: BridgeResponse) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Builds the {@link FanvueBridge} served over an established port.\n *\n * Requests are correlated to responses by `id`; anything arriving on the port\n * that is not a well-formed response for a request still in flight is ignored,\n * so a misbehaving host cannot resolve a request twice or crash the app.\n *\n * Apps get this from {@link connectFanvueBridge} rather than calling it directly.\n *\n * @param ctx - The port, granted capabilities, and per-request timeout.\n * @returns The bridge exposed to the app.\n */\nexport function createFanvueBridge(ctx: FanvueBridgeContext): InternalFanvueBridge {\n const { capabilities, requestTimeoutMs } = ctx;\n const pending = new Map<string, PendingRequest>();\n let requestSequence = 0;\n let port = ctx.port;\n\n function onPortMessage(event: MessageEvent): void {\n const parsed = bridgeResponseSchema.safeParse(event.data);\n if (!parsed.success) return;\n\n const inFlight = pending.get(parsed.data.id);\n if (!inFlight) return;\n\n pending.delete(parsed.data.id);\n clearTimeout(inFlight.timer);\n inFlight.resolve(parsed.data);\n }\n\n function attach(next: MessagePort): void {\n next.addEventListener('message', onPortMessage);\n next.start();\n }\n attach(port);\n\n function adoptPort(next: MessagePort): void {\n // The host closes its end of the superseded port when it re-answers a\n // ready; close this end too and route all later requests over the new\n // port. Requests still in flight on the old port fail by their timeouts.\n port.close();\n port = next;\n attach(next);\n }\n\n function has(capability: BridgeCapability): boolean {\n return capabilities.includes(capability);\n }\n\n async function request(\n capability: BridgeCapability,\n method: string,\n payload: unknown,\n ): Promise<Result<unknown, BridgeRequestError>> {\n if (!has(capability)) {\n return err({\n code: 'capability_denied',\n message: `The host did not grant the \"${capability}\" capability to this app.`,\n });\n }\n\n requestSequence += 1;\n const id = `${capability}-${requestSequence}`;\n const message: BridgeRequest = {\n v: BRIDGE_VERSION,\n id,\n kind: 'request',\n capability,\n method,\n payload,\n };\n\n try {\n port.postMessage(message);\n } catch (error) {\n return err({\n code: 'PORT_CLOSED',\n message: `Could not send the request to the host: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n\n // Registered after the send: port delivery is always queued as a task, so\n // no response can arrive before this synchronous block finishes.\n const settled = await new Promise<BridgeResponse | null>((resolve) => {\n const timer = setTimeout(() => {\n pending.delete(id);\n resolve(null);\n }, requestTimeoutMs);\n pending.set(id, { resolve, timer });\n });\n if (settled === null) {\n return err({\n code: 'TIMEOUT',\n message: `The host did not answer \"${capability}.${method}\" within ${requestTimeoutMs}ms.`,\n });\n }\n\n if (!settled.ok) {\n return err({\n code: settled.error?.code ?? 'internal',\n message: settled.error?.message ?? 'The host rejected the request without a reason.',\n });\n }\n\n return ok(settled.result);\n }\n\n async function track(\n eventName: string,\n properties?: AnalyticsProperties,\n ): Promise<Result<void, BridgeRequestError>> {\n // Validate before sending so a malformed call fails immediately with a\n // readable message; the host validates again as the authoritative check.\n const payload = analyticsTrackPayloadSchema.safeParse(\n properties === undefined ? { eventName } : { eventName, properties },\n );\n if (!payload.success) {\n return err({ code: 'invalid_payload', message: z.prettifyError(payload.error) });\n }\n\n const result = await request(ANALYTICS_CAPABILITY, ANALYTICS_TRACK_METHOD, payload.data);\n return result.map((): void => undefined);\n }\n\n return {\n capabilities,\n has,\n analytics: { track },\n adoptPort,\n };\n}\n","import { err, ok } from 'neverthrow';\n\nimport { createFanvueBridge, type FanvueBridge, type InternalFanvueBridge } from './bridge.js';\nimport {\n BRIDGE_READY_TYPE,\n BRIDGE_VERSION,\n bridgeHelloMessageSchema,\n type BridgeReadyMessage,\n} from './protocol.js';\n\nimport type { Result } from 'neverthrow';\n\n/** How long to wait for the host's `hello` before giving up. */\nconst DEFAULT_CONNECT_TIMEOUT_MS = 3000;\n\n/** How long to wait for a response to a single request once connected. */\nconst REQUEST_TIMEOUT_MS = 3000;\n\n/**\n * How often to re-post `ready` until the handshake settles.\n *\n * The host attaches its listener asynchronously (its capability grants come\n * from a query), so a single `ready` posted right after the iframe loads can\n * arrive before anyone is listening. Re-announcing until a `hello` lands (or\n * the timeout fires) closes that race.\n */\nconst READY_REANNOUNCE_INTERVAL_MS = 500;\n\n/**\n * Why connecting to the host failed.\n *\n * - `NOT_EMBEDDED` — the app is not running in an iframe, so there is no host.\n * - `TIMEOUT` — no `hello` arrived. The page is framed by something other than\n * Fanvue, or Fanvue refused the handshake (unregistered embed origin).\n */\nexport interface BridgeConnectError {\n code: 'NOT_EMBEDDED' | 'TIMEOUT';\n message: string;\n}\n\n/**\n * Options for {@link connectFanvueBridge}.\n *\n * @property timeoutMs - How long to wait for the host's `hello`. Defaults to 3000.\n */\nexport interface ConnectFanvueBridgeOptions {\n timeoutMs?: number;\n}\n\n/**\n * Opens the capability bridge to the Fanvue page hosting this app.\n *\n * Posts `fanvue:bridge:ready` to `window.parent` and waits for the host's\n * `fanvue:bridge:hello`, which names the granted capabilities and transfers the\n * `MessagePort` all later traffic flows over. The host verifies the frame's\n * origin before replying, so a page framed by anything other than its\n * registered Fanvue surface simply times out.\n *\n * `ready` is re-posted every 500ms until a `hello` lands (or the timeout\n * fires), because the host may attach its listener after the app's first\n * announce. The host answers each verified `ready` with a fresh `hello` and\n * port, closing the prior port — so the bridge keeps listening for the\n * lifetime of the connection and adopts the newest `hello` by swapping onto\n * its port. Requests in flight on a superseded port fail by their timeouts.\n *\n * Calling this again is safe and is how an app reconnects after the iframe\n * reloads: the host closes the previous port and re-handshakes.\n *\n * Failure is not exceptional — apps are expected to keep working standalone\n * (local dev, previews) where the connection never succeeds.\n *\n * @param options - Optional overrides (e.g. a shorter `timeoutMs`).\n * @returns A `Result` with the connected {@link FanvueBridge}, or a\n * {@link BridgeConnectError} when there is no host to talk to.\n *\n * @example\n * const result = await connectFanvueBridge();\n * if (result.isOk() && result.value.has('analytics')) {\n * await result.value.analytics.track('course_created', { chapters: 4 });\n * }\n */\nexport function connectFanvueBridge(\n options?: ConnectFanvueBridgeOptions,\n): Promise<Result<FanvueBridge, BridgeConnectError>> {\n const timeoutMs = options?.timeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;\n\n if (typeof window === 'undefined' || window.parent === window) {\n return Promise.resolve(\n err({\n code: 'NOT_EMBEDDED',\n message:\n 'The app is not running inside a Fanvue iframe, so there is no host to connect to.',\n }),\n );\n }\n const host = window.parent;\n\n return new Promise((resolve) => {\n let settled = false;\n let bridge: InternalFanvueBridge | null = null;\n\n const timer = setTimeout(() => {\n finish(\n err({\n code: 'TIMEOUT',\n message: `The Fanvue host did not complete the handshake within ${timeoutMs}ms.`,\n }),\n );\n }, timeoutMs);\n\n // Re-announce until the handshake settles, in case the host attached its\n // listener after the first `ready`.\n const reannounce = setInterval(postReady, READY_REANNOUNCE_INTERVAL_MS);\n\n function finish(result: Result<FanvueBridge, BridgeConnectError>): void {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n clearInterval(reannounce);\n // Only a failed handshake stops listening. After a `hello` the listener\n // lives for the lifetime of the bridge: the host may answer more than\n // one `ready` (it closes the prior port each time), and the bridge must\n // adopt the newest port to stay on the one the host is serving.\n if (result.isErr()) window.removeEventListener('message', onMessage);\n resolve(result);\n }\n\n function onMessage(event: MessageEvent): void {\n // The host is the only window that can hold this frame's port; anything\n // else on the window channel belongs to another protocol.\n if (event.source !== host) return;\n\n const hello = bridgeHelloMessageSchema.safeParse(event.data);\n if (!hello.success) return;\n\n const port = event.ports[0];\n if (!port) return;\n\n if (bridge) {\n bridge.adoptPort(port);\n return;\n }\n\n bridge = createFanvueBridge({\n port,\n capabilities: hello.data.capabilities,\n requestTimeoutMs: REQUEST_TIMEOUT_MS,\n });\n finish(ok(bridge));\n }\n\n window.addEventListener('message', onMessage);\n\n function postReady(): void {\n const ready: BridgeReadyMessage = { type: BRIDGE_READY_TYPE, v: BRIDGE_VERSION };\n // Targets \"*\" because the app cannot know the host's origin before the\n // handshake, and `ready` carries nothing but the protocol version.\n host.postMessage(ready, '*');\n }\n postReady();\n });\n}\n"],"mappings":"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"}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as DEFAULT_ISSUER_URL, C as OAuthConfig, D as TokenResponse, E as SessionVerifyError, F as HEADER_UPDATED_SESSION, M as DEFAULT_SCOPES, N as assertFanvueDomain, O as API_VERSION, P as BEARER_PREFIX, S as JsonParseError, T as SessionPayload, _ as refreshAccessToken, a as SessionPayloadSchema, b as EmbeddedAuthError, c as FanvueTheme, d as getThemeFromUrl, f as requestAuthorizationCodeOnBehalf, g as exchangeCodeForToken, h as createAuthorizationUrl, i as FanvueUserSchema, j as DEFAULT_PLATFORM_URL, k as DEFAULT_API_BASE_URL, l as exchangeSessionToken, m as verifySessionJwt, n as createFanvueClient, o as TokenResponseSchema, p as createSessionJwt, r as AuthorizeOnBehalfResponseSchema, s as safeJsonParse, t as FanvueClient, u as getSessionTokenFromUrl, v as ApiError, w as OAuthError, x as FanvueUser, y as EmbeddedAuthConfig } from "../index-
|
|
1
|
+
import { A as DEFAULT_ISSUER_URL, C as OAuthConfig, D as TokenResponse, E as SessionVerifyError, F as HEADER_UPDATED_SESSION, M as DEFAULT_SCOPES, N as assertFanvueDomain, O as API_VERSION, P as BEARER_PREFIX, S as JsonParseError, T as SessionPayload, _ as refreshAccessToken, a as SessionPayloadSchema, b as EmbeddedAuthError, c as FanvueTheme, d as getThemeFromUrl, f as requestAuthorizationCodeOnBehalf, g as exchangeCodeForToken, h as createAuthorizationUrl, i as FanvueUserSchema, j as DEFAULT_PLATFORM_URL, k as DEFAULT_API_BASE_URL, l as exchangeSessionToken, m as verifySessionJwt, n as createFanvueClient, o as TokenResponseSchema, p as createSessionJwt, r as AuthorizeOnBehalfResponseSchema, s as safeJsonParse, t as FanvueClient, u as getSessionTokenFromUrl, v as ApiError, w as OAuthError, x as FanvueUser, y as EmbeddedAuthConfig } from "../index-ClbZoV_Z.js";
|
|
2
2
|
export { API_VERSION, ApiError, AuthorizeOnBehalfResponseSchema, BEARER_PREFIX, DEFAULT_API_BASE_URL, DEFAULT_ISSUER_URL, DEFAULT_PLATFORM_URL, DEFAULT_SCOPES, EmbeddedAuthConfig, EmbeddedAuthError, FanvueClient, FanvueTheme, FanvueUser, FanvueUserSchema, HEADER_UPDATED_SESSION, JsonParseError, OAuthConfig, OAuthError, SessionPayload, SessionPayloadSchema, SessionVerifyError, TokenResponse, TokenResponseSchema, assertFanvueDomain, createAuthorizationUrl, createFanvueClient, createSessionJwt, exchangeCodeForToken, exchangeSessionToken, getSessionTokenFromUrl, getThemeFromUrl, refreshAccessToken, requestAuthorizationCodeOnBehalf, safeJsonParse, verifySessionJwt };
|
package/dist/core/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "../schemas-DbyHF7Xi.js";
|
|
2
|
+
import { C as HEADER_UPDATED_SESSION, S as BEARER_PREFIX, _ as DEFAULT_API_BASE_URL, a as requestAuthorizationCodeOnBehalf, b as DEFAULT_SCOPES, c as createAuthorizationUrl, d as AuthorizeOnBehalfResponseSchema, f as FanvueUserSchema, g as API_VERSION, h as safeJsonParse, i as getThemeFromUrl, l as exchangeCodeForToken, m as TokenResponseSchema, n as exchangeSessionToken, o as createSessionJwt, p as SessionPayloadSchema, r as getSessionTokenFromUrl, s as verifySessionJwt, t as createFanvueClient, u as refreshAccessToken, v as DEFAULT_ISSUER_URL, x as assertFanvueDomain, y as DEFAULT_PLATFORM_URL } from "../core-BqdYUMrJ.js";
|
|
2
3
|
export { API_VERSION, AuthorizeOnBehalfResponseSchema, BEARER_PREFIX, DEFAULT_API_BASE_URL, DEFAULT_ISSUER_URL, DEFAULT_PLATFORM_URL, DEFAULT_SCOPES, FanvueUserSchema, HEADER_UPDATED_SESSION, SessionPayloadSchema, TokenResponseSchema, assertFanvueDomain, createAuthorizationUrl, createFanvueClient, createSessionJwt, exchangeCodeForToken, exchangeSessionToken, getSessionTokenFromUrl, getThemeFromUrl, refreshAccessToken, requestAuthorizationCodeOnBehalf, safeJsonParse, verifySessionJwt };
|