@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.
Files changed (39) hide show
  1. package/README.md +361 -1
  2. package/dist/bridge/index.d.ts +2 -0
  3. package/dist/bridge/index.js +3 -0
  4. package/dist/bridge-QnF7P4Co.js +336 -0
  5. package/dist/bridge-QnF7P4Co.js.map +1 -0
  6. package/dist/core/index.d.ts +2 -2
  7. package/dist/core/index.js +3 -2
  8. package/dist/core-BhKiA55a.js +2962 -0
  9. package/dist/core-BhKiA55a.js.map +1 -0
  10. package/dist/index-C3CXrRiw.d.ts +1412 -0
  11. package/dist/index-C3CXrRiw.d.ts.map +1 -0
  12. package/dist/index-CEFYZtlb.d.ts +223 -0
  13. package/dist/index-CEFYZtlb.d.ts.map +1 -0
  14. package/dist/{index-pS9wR5yg.d.ts → index-cf3ZMnLJ.d.ts} +524 -901
  15. package/dist/index-cf3ZMnLJ.d.ts.map +1 -0
  16. package/dist/index-v_6Z4Q0u.d.ts +84 -0
  17. package/dist/index-v_6Z4Q0u.d.ts.map +1 -0
  18. package/dist/{core-CvVOMyqr.js → index.cjs-teGsk6HB.js} +1506 -2826
  19. package/dist/index.cjs-teGsk6HB.js.map +1 -0
  20. package/dist/nextjs/embedded-app/index.d.ts +2 -2
  21. package/dist/nextjs/embedded-app/index.js +3 -2
  22. package/dist/nextjs/embedded-app/index.js.map +1 -1
  23. package/dist/nextjs/off-platform/index.d.ts +3 -3
  24. package/dist/nextjs/off-platform/index.d.ts.map +1 -1
  25. package/dist/nextjs/off-platform/index.js +4 -3
  26. package/dist/nextjs/off-platform/index.js.map +1 -1
  27. package/dist/nextjs-CvVhtMdb.js +144 -0
  28. package/dist/nextjs-CvVhtMdb.js.map +1 -0
  29. package/dist/react/index.d.ts +37 -2
  30. package/dist/react/index.d.ts.map +1 -1
  31. package/dist/react/index.js +61 -2
  32. package/dist/react/index.js.map +1 -1
  33. package/package.json +20 -16
  34. package/dist/core-CvVOMyqr.js.map +0 -1
  35. package/dist/index-CweVyIKX.d.ts +0 -48
  36. package/dist/index-CweVyIKX.d.ts.map +0 -1
  37. package/dist/index-pS9wR5yg.d.ts.map +0 -1
  38. package/dist/nextjs-CESI_EiU.js +0 -80
  39. 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 };