@fanvue/builder-sdk 0.3.0 → 0.5.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 +361 -1
- package/dist/bridge/index.d.ts +2 -0
- package/dist/bridge/index.js +3 -0
- package/dist/bridge-QnF7P4Co.js +336 -0
- package/dist/bridge-QnF7P4Co.js.map +1 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +3 -2
- package/dist/core-BhKiA55a.js +2962 -0
- package/dist/core-BhKiA55a.js.map +1 -0
- package/dist/index-C3CXrRiw.d.ts +1412 -0
- package/dist/index-C3CXrRiw.d.ts.map +1 -0
- package/dist/index-CEFYZtlb.d.ts +223 -0
- package/dist/index-CEFYZtlb.d.ts.map +1 -0
- package/dist/{index-pS9wR5yg.d.ts → index-cf3ZMnLJ.d.ts} +524 -901
- package/dist/index-cf3ZMnLJ.d.ts.map +1 -0
- package/dist/index-v_6Z4Q0u.d.ts +84 -0
- package/dist/index-v_6Z4Q0u.d.ts.map +1 -0
- package/dist/{core-CvVOMyqr.js → index.cjs-teGsk6HB.js} +1506 -2826
- package/dist/index.cjs-teGsk6HB.js.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 +3 -3
- package/dist/nextjs/off-platform/index.d.ts.map +1 -1
- package/dist/nextjs/off-platform/index.js +4 -3
- package/dist/nextjs/off-platform/index.js.map +1 -1
- package/dist/nextjs-CvVhtMdb.js +144 -0
- package/dist/nextjs-CvVhtMdb.js.map +1 -0
- 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/package.json +20 -16
- package/dist/core-CvVOMyqr.js.map +0 -1
- package/dist/index-CweVyIKX.d.ts +0 -48
- package/dist/index-CweVyIKX.d.ts.map +0 -1
- package/dist/index-pS9wR5yg.d.ts.map +0 -1
- package/dist/nextjs-CESI_EiU.js +0 -80
- package/dist/nextjs-CESI_EiU.js.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.
|
|
@@ -284,6 +348,139 @@ const session = await verifySessionJwt("your-session-secret", jwt);
|
|
|
284
348
|
|
|
285
349
|
</details>
|
|
286
350
|
|
|
351
|
+
## Observability and Machine Auth
|
|
352
|
+
|
|
353
|
+
Privacy-first logging, Sentry scrubbing, machine (cron/queue) authentication, and a
|
|
354
|
+
configuration readiness probe. All exported from the root entrypoint and dependency-free.
|
|
355
|
+
|
|
356
|
+
### Safe structured logging
|
|
357
|
+
|
|
358
|
+
`createSafeLogFields` returns an allowlisting filter: unknown keys are dropped, only
|
|
359
|
+
`string | number | boolean | null` survives (an `Error` or a response body is never
|
|
360
|
+
serialised), UUID-bearing strings become `[redacted-uuid]` — **any** UUID version,
|
|
361
|
+
including v7 — and email-bearing strings become `[redacted-email]`.
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
import { createLogEvent, createSafeLogFields, errorName } from "@fanvue/builder-sdk";
|
|
365
|
+
|
|
366
|
+
const safeLogFields = createSafeLogFields(["wheelId"]); // base keys + your own
|
|
367
|
+
const logEvent = createLogEvent({ prefix: "[spinwheel]", safeLogFields });
|
|
368
|
+
|
|
369
|
+
logEvent("error", "exchange.failed", {
|
|
370
|
+
wheelId: "wheel_1",
|
|
371
|
+
httpStatus: 503,
|
|
372
|
+
errorName: errorName(caught), // class name only, never err.message
|
|
373
|
+
accessToken: "dropped", // not allowlisted
|
|
374
|
+
});
|
|
375
|
+
// [spinwheel] { event: 'exchange.failed', wheelId: 'wheel_1', httpStatus: 503, errorName: 'TypeError' }
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Base allowlist (`BASE_ALLOWED_LOG_KEYS`): `cause`, `code`, `creatorId`, `errorName`,
|
|
379
|
+
`httpStatus`, `reason`, `retryCount`, `status`.
|
|
380
|
+
|
|
381
|
+
### Sentry scrubbing
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
// sentry.server.config.ts
|
|
385
|
+
import { createSentryScrubber } from "@fanvue/builder-sdk";
|
|
386
|
+
|
|
387
|
+
Sentry.init({
|
|
388
|
+
dsn,
|
|
389
|
+
enabled: Boolean(dsn),
|
|
390
|
+
beforeSend: createSentryScrubber(/(prizedetail|fanhmac|seed)/i), // merged with the base pattern
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Recursive, and applies four rules to every node: values under a sensitive key become
|
|
395
|
+
`[redacted]`, UUIDs inside strings become `[redacted-uuid]` (any version, and also when the UUID
|
|
396
|
+
sits behind a `_` or another word character), a string that parses as a URL keeps its origin
|
|
397
|
+
and path but loses its query string, fragment and `user:pass@` userinfo
|
|
398
|
+
(`https://cdn/x?[redacted]`), and email addresses inside strings become `[redacted-email]`.
|
|
399
|
+
Base sensitive keys (`BASE_SENSITIVE_KEY_PATTERN`), matched as case-insensitive substrings:
|
|
400
|
+
`authorization|cookie|token|secret|password|signedurl|api[-_]?key|credential|email|phone|ip[-_]?address`.
|
|
401
|
+
|
|
402
|
+
Total by construction, because a `beforeSend` that throws loses the event and raises inside the
|
|
403
|
+
caller: a repeat visit yields `[circular]`, a node deeper than 32 levels yields `[max-depth]`,
|
|
404
|
+
and a property whose getter throws yields `[unreadable]`. All three fail closed — an untraversed
|
|
405
|
+
value cannot leak.
|
|
406
|
+
|
|
407
|
+
### Machine auth (cron and queue routes)
|
|
408
|
+
|
|
409
|
+
`requireMachineAuth` returns `authorized | unauthorized | not_configured`. The third arm exists
|
|
410
|
+
so a deployment with no usable credential answers 503, never 401 — a broken deploy must not look
|
|
411
|
+
like ordinary auth noise. Bearer secrets must be at least 32 characters (`MINIMUM_BEARER_SECRET_LENGTH`);
|
|
412
|
+
a shorter one is treated as absent rather than compared. Comparison is constant-time over the
|
|
413
|
+
UTF-8 bytes, with a byte-length pre-check. The `Authorization` scheme is required but matched
|
|
414
|
+
case-insensitively (`bearer` and `Bearer` both work, a bare secret does not).
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
// app/api/cron/reconcile/route.ts
|
|
418
|
+
import { boundedBatchSize, requireMachineAuth } from "@fanvue/builder-sdk";
|
|
419
|
+
|
|
420
|
+
export async function POST(request: Request): Promise<Response> {
|
|
421
|
+
const auth = await requireMachineAuth(request, {
|
|
422
|
+
bearerSecret: process.env.CRON_SECRET ?? null,
|
|
423
|
+
signedRequestVerifier: null,
|
|
424
|
+
rawBody: null,
|
|
425
|
+
});
|
|
426
|
+
if (auth.status === "not_configured") return new Response(null, { status: 503 });
|
|
427
|
+
if (auth.status === "unauthorized") return new Response(null, { status: 401 });
|
|
428
|
+
|
|
429
|
+
const limit = boundedBatchSize(new URL(request.url).searchParams.get("limit"), 20, 100);
|
|
430
|
+
// ...
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
A second strategy plugs in via `SignedRequestVerifier`. `@upstash/qstash` is deliberately **not**
|
|
435
|
+
a dependency of this package — the app owns it:
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { Receiver } from "@upstash/qstash";
|
|
439
|
+
import type { SignedRequestVerifier } from "@fanvue/builder-sdk";
|
|
440
|
+
|
|
441
|
+
const qstashVerifier: SignedRequestVerifier = {
|
|
442
|
+
verify: async (request, rawBody) => {
|
|
443
|
+
const signature = request.headers.get("upstash-signature");
|
|
444
|
+
const currentSigningKey = process.env.QSTASH_CURRENT_SIGNING_KEY;
|
|
445
|
+
const nextSigningKey = process.env.QSTASH_NEXT_SIGNING_KEY;
|
|
446
|
+
if (!signature || !currentSigningKey || !nextSigningKey) return false; // deny by default
|
|
447
|
+
return new Receiver({ currentSigningKey, nextSigningKey }).verify({
|
|
448
|
+
signature,
|
|
449
|
+
body: new TextDecoder().decode(rawBody),
|
|
450
|
+
url: request.url,
|
|
451
|
+
upstashRegion: request.headers.get("upstash-region") ?? undefined,
|
|
452
|
+
});
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// Pass the exact received bytes — a reparsed body invalidates the signature.
|
|
457
|
+
const rawBody = new Uint8Array(await request.arrayBuffer());
|
|
458
|
+
const auth = await requireMachineAuth(request, {
|
|
459
|
+
bearerSecret: process.env.CRON_SECRET ?? null,
|
|
460
|
+
signedRequestVerifier: qstashVerifier,
|
|
461
|
+
rawBody,
|
|
462
|
+
});
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Configuration readiness
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
import { configurationReadiness } from "@fanvue/builder-sdk";
|
|
469
|
+
|
|
470
|
+
export function GET(): Response {
|
|
471
|
+
const checks = configurationReadiness(); // env is a parameter; defaults to process.env
|
|
472
|
+
const ready = checks.every((check) => check.ready);
|
|
473
|
+
return Response.json({ ready, checks }, { status: ready ? 200 : 503 });
|
|
474
|
+
}
|
|
475
|
+
// [{ name: 'app_url', ready: false, detail: 'set FANVUE_APP_BASE_URL to an https:// origin' },
|
|
476
|
+
// { name: 'fanvue_oauth', ready: true, detail: null }]
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
`app_url` reads `FANVUE_APP_BASE_URL` (falling back to `APP_BASE_URL`) and requires an https
|
|
480
|
+
origin, because Fanvue refuses to embed anything else. `fanvue_oauth` requires `FANVUE_APP_UUID`,
|
|
481
|
+
`FANVUE_CLIENT_ID`, `FANVUE_CLIENT_SECRET` and `FANVUE_OAUTH_REDIRECT_URI`; details name the
|
|
482
|
+
missing variables and never echo a value.
|
|
483
|
+
|
|
287
484
|
## Environment Variables
|
|
288
485
|
|
|
289
486
|
Add these to your `.env.local` (Next.js) or equivalent:
|
|
@@ -317,6 +514,128 @@ const config = createConfig({
|
|
|
317
514
|
});
|
|
318
515
|
```
|
|
319
516
|
|
|
517
|
+
## Platform Contracts
|
|
518
|
+
|
|
519
|
+
Wire shapes, enums and environment validation shared by every Fanvue app, exported from the root entrypoint (`@fanvue/builder-sdk`). Everything here is runtime-agnostic — safe in a route handler, a worker, or the browser.
|
|
520
|
+
|
|
521
|
+
### `FANVUE_*` environment
|
|
522
|
+
|
|
523
|
+
```env
|
|
524
|
+
FANVUE_APP_UUID= # your app's UUID on Fanvue
|
|
525
|
+
FANVUE_CLIENT_ID=
|
|
526
|
+
FANVUE_CLIENT_SECRET=
|
|
527
|
+
FANVUE_OAUTH_REDIRECT_URI=
|
|
528
|
+
FANVUE_API_BASE_URL=https://api.fanvue.com # optional, shown with defaults
|
|
529
|
+
FANVUE_AUTH_BASE_URL=https://auth.fanvue.com
|
|
530
|
+
FANVUE_WEB_ORIGIN=https://www.fanvue.com
|
|
531
|
+
FANVUE_API_VERSION=2025-06-26
|
|
532
|
+
FANVUE_EXPERIENCE_URL_TEMPLATE= # optional share-link override
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
import { fanvueEnv, isFanvueConfigured, flagEnabled } from "@fanvue/builder-sdk";
|
|
537
|
+
|
|
538
|
+
// Parsed on first call and cached; a build never needs the secrets present.
|
|
539
|
+
const apiBaseUrl = fanvueEnv().FANVUE_API_BASE_URL;
|
|
540
|
+
|
|
541
|
+
// The four credentials default to "" so builds pass — check before an OAuth call.
|
|
542
|
+
if (!isFanvueConfigured()) {
|
|
543
|
+
return Response.json(
|
|
544
|
+
{ error: { code: "fanvue_not_configured", message: "Fanvue is not configured." } },
|
|
545
|
+
{ status: 503 },
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Case- and whitespace-insensitive flag reader; warns once on an unreadable value.
|
|
550
|
+
const devMode = flagEnabled("MY_APP_DEV_MODE");
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Call `resetFanvueEnvCache()` between tests that mutate `process.env`.
|
|
554
|
+
|
|
555
|
+
### Experience postMessage protocol
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
import {
|
|
559
|
+
PUBLISH_REQUEST_MESSAGE,
|
|
560
|
+
isPublishResultMessage,
|
|
561
|
+
isFanvueOrigin,
|
|
562
|
+
} from "@fanvue/builder-sdk";
|
|
563
|
+
|
|
564
|
+
window.parent.postMessage({ type: PUBLISH_REQUEST_MESSAGE, token }, platformOrigin);
|
|
565
|
+
|
|
566
|
+
window.addEventListener("message", (event) => {
|
|
567
|
+
// Validate the origin as well as the shape: Fanvue replies with targetOrigin "*".
|
|
568
|
+
if (!isFanvueOrigin(event.origin) || !isPublishResultMessage(event.data)) return;
|
|
569
|
+
if (event.data.status === "published") onPublished(event.data.experienceId);
|
|
570
|
+
});
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Access modes and denial reasons
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
import { accessModeFromDenialReason } from "@fanvue/builder-sdk";
|
|
577
|
+
|
|
578
|
+
// The exchange fails closed: a denied fan gets a reason but no access mode.
|
|
579
|
+
const accessMode = accessModeFromDenialReason(reason); // 'SUBSCRIPTION' | 'PAID' | 'HIDDEN' | null
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Error bodies
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
import {
|
|
586
|
+
AppErrorEnvelopeSchema,
|
|
587
|
+
NON_SESSION_401_CODES,
|
|
588
|
+
parseFanvueErrorBody,
|
|
589
|
+
} from "@fanvue/builder-sdk";
|
|
590
|
+
|
|
591
|
+
// Normalises all four Fanvue API error-body shapes, preserving `reason`.
|
|
592
|
+
// `reason` is what separates an entitlement refusal from an app-binding mismatch.
|
|
593
|
+
const { message, reason, code } = parseFanvueErrorBody(await fanvueResponse.json());
|
|
594
|
+
|
|
595
|
+
// Client side, against your own app's `{ error: { code, message } }` envelope:
|
|
596
|
+
// a 401 with one of these codes means the Fanvue grant is gone, not the app
|
|
597
|
+
// session, so prompt a reconnect instead of signing the creator out.
|
|
598
|
+
const parsed = AppErrorEnvelopeSchema.safeParse(await appResponse.json());
|
|
599
|
+
const appCode = parsed.success ? parsed.data.error.code : null;
|
|
600
|
+
if (appResponse.status === 401 && (appCode === null || !NON_SESSION_401_CODES.has(appCode))) {
|
|
601
|
+
onSessionExpired();
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Pagination
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
import { cursorPageSchema, offsetPageSchema, clampPageSize } from "@fanvue/builder-sdk";
|
|
609
|
+
|
|
610
|
+
const SubscribersPage = offsetPageSchema(SubscriberSchema); // { data, pagination }
|
|
611
|
+
const PaymentsPage = cursorPageSchema(PaymentSchema); // { data, nextCursor }
|
|
612
|
+
const size = clampPageSize(requested); // the API 400s above 50
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Contracts API reference
|
|
616
|
+
|
|
617
|
+
| Export | Description |
|
|
618
|
+
|---|---|
|
|
619
|
+
| `FanvueEnvSchema`, `parseFanvueEnv(source)` | Zod schema and pure parser for the `FANVUE_*` contract |
|
|
620
|
+
| `fanvueEnv()`, `resetFanvueEnvCache()` | Lazily parsed, cached `process.env` view, and the test reset |
|
|
621
|
+
| `isFanvueConfigured(env?)` | Whether the four Fanvue credentials are all set |
|
|
622
|
+
| `flagEnabled(name, source?)` | Boolean env-flag reader; trims, lowercases, warns once per flag |
|
|
623
|
+
| `PUBLISH_REQUEST_MESSAGE`, `PUBLISH_RESULT_MESSAGE`, `UNPUBLISH_REQUEST_MESSAGE`, `UNPUBLISH_RESULT_MESSAGE`, `EXPERIENCE_MESSAGE_TYPES` | `fanvue:experience:*` message types |
|
|
624
|
+
| `PublishRequestMessageSchema`, `PublishResultMessageSchema`, `UnpublishRequestMessageSchema`, `UnpublishResultMessageSchema`, `ExperienceMessageSchema` | Zod schemas for the four messages and their discriminated union |
|
|
625
|
+
| `isPublishResultMessage(v)`, `isUnpublishResultMessage(v)` | `safeParse`-backed type guards |
|
|
626
|
+
| `isFanvueOrigin(origin)` | `https` + `fanvue.com` / `*.fanvue.com` origin check |
|
|
627
|
+
| `FANVUE_ACCESS_MODES`, `FanvueAccessModeSchema` | `FREE \| SUBSCRIPTION \| PAID \| HIDDEN` |
|
|
628
|
+
| `EXPERIENCE_DENIAL_REASONS`, `EXPERIENCE_ENTITLED_REASONS` | Every reason the platform reports, documented |
|
|
629
|
+
| `accessModeFromDenialReason(reason)` | Recovers the access mode a denial implies, or `null` |
|
|
630
|
+
| `MAX_PAGE_SIZE`, `DEFAULT_PAGE_SIZE`, `clampPageSize(n)` | Offset page-size limits (50 / 15) and client-side clamping |
|
|
631
|
+
| `OffsetPaginationSchema`, `HybridPaginationSchema` | `{ page, size, hasMore }` and the `v0` subscribers variant |
|
|
632
|
+
| `offsetPageSchema(item)`, `cursorPageSchema(item)` | List-response schema builders |
|
|
633
|
+
| `parseFanvueErrorBody(body)` | Normalises any Fanvue error body to `{ message, reason, code }` |
|
|
634
|
+
| `FanvueErrorBodySchema` (+ the four per-shape schemas) | Zod schemas for the platform's error bodies |
|
|
635
|
+
| `AppErrorEnvelopeSchema` | The app's own `{ error: { code, message } }` envelope |
|
|
636
|
+
| `FANVUE_APP_ERROR_CODES`, `NON_SESSION_401_CODES` | Canonical app-facing codes and the two grant-loss 401s |
|
|
637
|
+
| `OAuthErrorBodySchema` | OAuth error body, normalised to `{ error, errorDescription }` |
|
|
638
|
+
|
|
320
639
|
## Error Handling
|
|
321
640
|
|
|
322
641
|
All async operations return `Result<T, E>` types from [`neverthrow`](https://github.com/supermacro/neverthrow) instead of throwing exceptions. This gives you type-safe, explicit error handling:
|
|
@@ -338,6 +657,16 @@ result
|
|
|
338
657
|
|
|
339
658
|
Error types: `OAuthError` (token exchange/refresh), `EmbeddedAuthError` (delegated authorize-on-behalf), `ApiError` (API requests), `SessionVerifyError` (JWT verification).
|
|
340
659
|
|
|
660
|
+
`TOKEN_EXCHANGE_FAILED` and `TOKEN_REFRESH_FAILED` carry the authorization server's own body as `oauthError: { error, errorDescription } | null` — the only way to tell a retired refresh token from bad client credentials, since both are `400`:
|
|
661
|
+
|
|
662
|
+
```ts
|
|
663
|
+
if (error.code === "TOKEN_REFRESH_FAILED" && error.oauthError?.error === "invalid_grant") {
|
|
664
|
+
// The stored grant is dead: prompt the creator to reconnect.
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
Treat it as a diagnostic, never as end-user copy.
|
|
669
|
+
|
|
341
670
|
## API Reference
|
|
342
671
|
|
|
343
672
|
### Core (`@fanvue/builder-sdk`)
|
|
@@ -356,6 +685,17 @@ Error types: `OAuthError` (token exchange/refresh), `EmbeddedAuthError` (delegat
|
|
|
356
685
|
| `createFanvueClient(accessToken, apiBaseUrl?)` | Create an authenticated API client. |
|
|
357
686
|
| `API_VERSION` | The API version header value (currently `2025-06-26`). |
|
|
358
687
|
| `HEADER_UPDATED_SESSION` | Response header (`X-Updated-Session`) carrying a refreshed session JWT. |
|
|
688
|
+
| `createSafeLogFields(extraAllowedKeys?)` | Build an allowlisting log-field filter. Drops unknown keys and non-scalars; redacts UUID-shaped values. |
|
|
689
|
+
| `createLogEvent(options?)` | Build a structured `logEvent(level, event, fields)` over an allowlist. |
|
|
690
|
+
| `logEvent(level, event, fields?)` | Ready-made structured logger over the base allowlist, prefixed `[fanvue]`. |
|
|
691
|
+
| `errorName(error)` | The `Error` class name (or `typeof`) of a caught value — never its message. |
|
|
692
|
+
| `createSentryScrubber(extraSensitiveKeyPattern?)` | Build a recursive `beforeSend` scrubber for Sentry. |
|
|
693
|
+
| `requireMachineAuth(request, options)` | Authenticate a cron/queue request. Returns `authorized \| unauthorized \| not_configured`. |
|
|
694
|
+
| `boundedBatchSize(requested, defaultSize, max)` | Clamp a caller-supplied batch size to a safe positive bound. |
|
|
695
|
+
| `configurationReadiness(env?)` | Named `app_url` / `fanvue_oauth` configuration checks for a readiness probe. |
|
|
696
|
+
| `BASE_ALLOWED_LOG_KEYS` | The generic log keys every app shares. |
|
|
697
|
+
| `BASE_SENSITIVE_KEY_PATTERN` | Key-name fragments whose values never reach an error tracker. |
|
|
698
|
+
| `MINIMUM_BEARER_SECRET_LENGTH` | Shortest machine-auth bearer secret the SDK will compare (32). |
|
|
359
699
|
|
|
360
700
|
#### Types
|
|
361
701
|
|
|
@@ -371,6 +711,13 @@ Error types: `OAuthError` (token exchange/refresh), `EmbeddedAuthError` (delegat
|
|
|
371
711
|
| `EmbeddedAuthError` | Error from the delegated authorize-on-behalf flow |
|
|
372
712
|
| `ApiError` | Error from API requests |
|
|
373
713
|
| `SessionVerifyError` | Error from session JWT verification |
|
|
714
|
+
| `SafeLogFields` / `SafeLogValue` | The allowlisting filter, and the scalar types that survive it |
|
|
715
|
+
| `LogEvent` / `LogEventOptions` / `LogLevel` / `LogSink` | Structured-logging shapes |
|
|
716
|
+
| `SentryScrubber` | A `beforeSend`-compatible scrubber: `<T>(event: T) => T` |
|
|
717
|
+
| `MachineAuthResult` | `{ status: 'authorized' \| 'unauthorized' \| 'not_configured' }` |
|
|
718
|
+
| `MachineAuthOptions` | Bearer secret, signed-request verifier, and raw body |
|
|
719
|
+
| `SignedRequestVerifier` | Pluggable signed-request strategy (e.g. QStash), implemented by the app |
|
|
720
|
+
| `ReadinessCheck` / `ReadinessEnv` | `{ name, ready, detail }` and the environment shape checks read |
|
|
374
721
|
|
|
375
722
|
### Next.js Embedded App (`@fanvue/builder-sdk/nextjs/embedded-app`)
|
|
376
723
|
|
|
@@ -387,6 +734,7 @@ Error types: `OAuthError` (token exchange/refresh), `EmbeddedAuthError` (delegat
|
|
|
387
734
|
| Export | Description |
|
|
388
735
|
|---|---|
|
|
389
736
|
| `createConfig(opts?)` | Build a `ResolvedConfig` from env vars and/or explicit options |
|
|
737
|
+
| `createConfigSafe(opts?)` | Non-throwing variant: `{ ok: true, config }` or `{ ok: false, missing }` |
|
|
390
738
|
| `createLoginHandler(opts)` | `{ GET }` -- redirects to the OAuth provider |
|
|
391
739
|
| `createCallbackHandler(opts)` | `{ GET, POST }` -- completes the code exchange, sets the session cookie |
|
|
392
740
|
| `createLogoutHandler(opts)` | `{ POST }` -- clears the session cookie |
|
|
@@ -400,6 +748,18 @@ Error types: `OAuthError` (token exchange/refresh), `EmbeddedAuthError` (delegat
|
|
|
400
748
|
| `AuthProvider` | Context provider. Manages JWT storage in `sessionStorage`. |
|
|
401
749
|
| `useAuth()` | Returns `{ jwt, isAuthenticated, setJwt, clearJwt, authFetch }` |
|
|
402
750
|
| `useEmbeddedAuth(opts?)` | Returns `{ status, error, theme }`. Exchanges the embedded session token on mount; `theme` is the creator's colour scheme (`'light' \| 'dark' \| null`). |
|
|
751
|
+
| `useFanvueAnalytics()` | Returns `{ track, isEnabled }`. Fires events through the host page; no-ops outside Fanvue. |
|
|
752
|
+
|
|
753
|
+
### Bridge (`@fanvue/builder-sdk/bridge`)
|
|
754
|
+
|
|
755
|
+
The host-to-app capability protocol. Most apps only need `useFanvueAnalytics`.
|
|
756
|
+
|
|
757
|
+
| Export | Description |
|
|
758
|
+
|---|---|
|
|
759
|
+
| `connectFanvueBridge(opts?)` | Handshakes with the host page. Returns `Result<FanvueBridge, BridgeConnectError>` (`NOT_EMBEDDED` or `TIMEOUT`) |
|
|
760
|
+
| `FanvueBridge` | `{ capabilities, has(capability), analytics.track(eventName, properties?) }` |
|
|
761
|
+
| `analyticsTrackPayloadSchema` | Zod schema for the `analytics.track` payload -- the same schema the host validates against |
|
|
762
|
+
| Protocol constants and schemas | `BRIDGE_VERSION`, `BRIDGE_READY_TYPE`, `BRIDGE_HELLO_TYPE`, `bridgeRequestSchema`, `bridgeResponseSchema` |
|
|
403
763
|
|
|
404
764
|
## Examples
|
|
405
765
|
|
|
@@ -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-CEFYZtlb.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 "../index.cjs-teGsk6HB.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-QnF7P4Co.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 };
|