@fanvue/builder-sdk 0.3.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 ADDED
@@ -0,0 +1,427 @@
1
+ # @fanvue/builder-sdk
2
+
3
+ Authentication for Fanvue apps. Every Fanvue app is either **embedded** or
4
+ **off-platform** ([which should I build?](https://api.fanvue.com/docs/app-store/app-types)) —
5
+ this SDK covers both:
6
+
7
+ | Building an... | Import | What you get | Docs |
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) |
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
+
12
+ Building something else (Node, Deno, a custom server)? The core `@fanvue/builder-sdk`
13
+ entrypoint exposes the low-level OAuth primitives both flows are built on.
14
+
15
+ - Drop-in route handlers for Next.js -- a couple of files and you're done
16
+ - Handles token exchange, refresh, and session management so you don't have to
17
+ - Fully typed with TypeScript
18
+
19
+ > [!IMPORTANT]
20
+ > **Renamed from `@fanvue/auth`.** This package was previously published as
21
+ > [`@fanvue/auth`](https://www.npmjs.com/package/@fanvue/auth) (≤ 0.2.3). The
22
+ > API is unchanged — to migrate, swap the dependency and update import paths:
23
+ >
24
+ > ```diff
25
+ > - "@fanvue/auth": "^0.2.3"
26
+ > + "@fanvue/builder-sdk": "^0.3.0"
27
+ > ```
28
+ >
29
+ > ```diff
30
+ > - import { useEmbeddedAuth } from "@fanvue/auth/react";
31
+ > + import { useEmbeddedAuth } from "@fanvue/builder-sdk/react";
32
+ > ```
33
+ >
34
+ > `@fanvue/auth` is deprecated on npm and will receive no further releases.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ npm install @fanvue/builder-sdk
40
+ # or
41
+ pnpm add @fanvue/builder-sdk
42
+ # or
43
+ yarn add @fanvue/builder-sdk
44
+ ```
45
+
46
+ Peer dependencies (`next`, `react`) are optional -- only install what your project uses.
47
+
48
+ ## Quick Start
49
+
50
+ ### Embedded App (your app inside Fanvue)
51
+
52
+ When Fanvue opens your app in an iframe, it appends a short-lived session token
53
+ to your embed URL (`?token=...`). Exchange it server-side for real OAuth tokens
54
+ -- the SDK handles the whole delegated flow (PKCE, state, code exchange).
55
+
56
+ **1. Add the session-exchange route**
57
+
58
+ ```ts
59
+ // app/api/fanvue/session/route.ts
60
+ import { createConfig, createSessionExchangeHandler } from "@fanvue/builder-sdk/nextjs/embedded-app";
61
+
62
+ export const { POST } = createSessionExchangeHandler(createConfig());
63
+ ```
64
+
65
+ **2. Authenticate in your embedded page**
66
+
67
+ ```tsx
68
+ // app/embedded/page.tsx
69
+ "use client";
70
+ import { AuthProvider, useAuth, useEmbeddedAuth } from "@fanvue/builder-sdk/react";
71
+
72
+ function Embedded() {
73
+ const { status, error, theme } = useEmbeddedAuth();
74
+ const { authFetch } = useAuth();
75
+
76
+ // `theme` is the creator's active colour scheme ("light" | "dark"), read
77
+ // from the iframe URL -- use it to match Fanvue. It's `null` when the app is
78
+ // opened outside Fanvue, so fall back to a sensible default.
79
+ return (
80
+ <div data-theme={theme ?? "light"}>
81
+ {status === "exchanging" && <p>Connecting to Fanvue…</p>}
82
+ {status === "error" && <p>Auth failed: {error}</p>}
83
+ <button onClick={() => authFetch("/api/me")}>Load my profile</button>
84
+ </div>
85
+ );
86
+ }
87
+
88
+ export default function Page() {
89
+ return (
90
+ <AuthProvider>
91
+ <Embedded />
92
+ </AuthProvider>
93
+ );
94
+ }
95
+ ```
96
+
97
+ **3. Call the Fanvue API from your own routes**
98
+
99
+ ```ts
100
+ // app/api/me/route.ts
101
+ import { NextResponse } from "next/server";
102
+ import { HEADER_UPDATED_SESSION } from "@fanvue/builder-sdk";
103
+ import { createConfig, getAuthenticatedClient } from "@fanvue/builder-sdk/nextjs/embedded-app";
104
+
105
+ const config = createConfig();
106
+
107
+ export async function GET() {
108
+ const auth = await getAuthenticatedClient({ sessionSecret: config.sessionSecret, config });
109
+ if (!auth) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
110
+
111
+ const user = await auth.client.getCurrentUser();
112
+ const res = NextResponse.json(user.isOk() ? user.value : { error: "api_failed" });
113
+ // When the access token was refreshed, hand the new session JWT back to the
114
+ // client -- `authFetch` stores it automatically.
115
+ if (auth.refreshedJwt) res.headers.set(HEADER_UPDATED_SESSION, auth.refreshedJwt);
116
+ return res;
117
+ }
118
+ ```
119
+
120
+ **Hosting requirements for embedded apps:**
121
+
122
+ - Serve over HTTPS with a browser-trusted certificate.
123
+ - Send `Content-Security-Policy: frame-ancestors https://www.fanvue.com` so
124
+ Fanvue can frame your page.
125
+ - The session token lives ~60 seconds -- exchange it promptly (the
126
+ `useEmbeddedAuth` hook does this on mount).
127
+ - To act on the creator's behalf **after** the iframe closes, persist the
128
+ refresh token from the `onTokens` hook:
129
+
130
+ ```ts
131
+ export const { POST } = createSessionExchangeHandler(createConfig(), {
132
+ onTokens: async ({ tokens, user }) => {
133
+ await db.saveRefreshToken(user.uuid, tokens.refresh_token);
134
+ },
135
+ });
136
+ ```
137
+
138
+ See the [embedded apps integration guide](https://api.fanvue.com/docs/app-store/embedded-apps/integration-guide)
139
+ for the full walkthrough, including app registration in the Builder.
140
+
141
+ ### Off-Platform App ("Login with Fanvue")
142
+
143
+ The fastest path: create a config file and three route handlers.
144
+
145
+ **1. Configure**
146
+
147
+ ```ts
148
+ // lib/auth.ts
149
+ import { createConfig } from "@fanvue/builder-sdk/nextjs/off-platform";
150
+
151
+ export const authConfig = {
152
+ ...createConfig(), // reads OAUTH_* and SESSION_* env vars
153
+ afterLoginPath: "/dashboard",
154
+ afterLogoutPath: "/",
155
+ };
156
+ ```
157
+
158
+ **2. Add route handlers**
159
+
160
+ ```ts
161
+ // app/api/oauth/login/route.ts
162
+ import { createLoginHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
163
+ import { authConfig } from "@/lib/auth";
164
+
165
+ export const { GET } = createLoginHandler(authConfig);
166
+ ```
167
+
168
+ ```ts
169
+ // app/api/oauth/callback/route.ts
170
+ import { createCallbackHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
171
+ import { authConfig } from "@/lib/auth";
172
+
173
+ export const { GET, POST } = createCallbackHandler(authConfig);
174
+ ```
175
+
176
+ ```ts
177
+ // app/api/oauth/logout/route.ts
178
+ import { createLogoutHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
179
+ import { authConfig } from "@/lib/auth";
180
+
181
+ export const { POST } = createLogoutHandler(authConfig);
182
+ ```
183
+
184
+ **3. Use the session**
185
+
186
+ ```ts
187
+ import { getSession, getAuthenticatedClient } from "@fanvue/builder-sdk/nextjs/off-platform";
188
+ import { authConfig } from "@/lib/auth";
189
+
190
+ // Read the session in any server component or route handler
191
+ const session = await getSession(authConfig.sessionSecret, authConfig.sessionCookieName);
192
+
193
+ // Or get an API client that automatically refreshes expired tokens
194
+ const client = await getAuthenticatedClient({
195
+ sessionSecret: authConfig.sessionSecret,
196
+ sessionCookieName: authConfig.sessionCookieName,
197
+ config: authConfig,
198
+ });
199
+
200
+ if (client) {
201
+ const user = await client.getCurrentUser();
202
+ if (user.isOk()) console.log(user.value);
203
+ }
204
+ ```
205
+
206
+ See the [authentication docs](https://api.fanvue.com/docs/authentication/overview)
207
+ for scopes, rate limits, and the underlying OAuth flow.
208
+
209
+ ### Core (advanced)
210
+
211
+ Use the core entrypoint when you need full control -- CLI tools, custom servers, or non-Next.js frameworks.
212
+
213
+ ```ts
214
+ import {
215
+ createAuthorizationUrl,
216
+ exchangeCodeForToken,
217
+ refreshAccessToken,
218
+ createSessionJwt,
219
+ verifySessionJwt,
220
+ createFanvueClient,
221
+ } from "@fanvue/builder-sdk";
222
+
223
+ const config = {
224
+ clientId: "your-client-id",
225
+ clientSecret: "your-client-secret",
226
+ redirectUri: "http://localhost:3000/callback",
227
+ issuerUrl: null,
228
+ apiBaseUrl: null,
229
+ scopes: null,
230
+ responseMode: null,
231
+ prompt: null,
232
+ };
233
+
234
+ // 1. Build the authorization URL (PKCE is handled automatically)
235
+ const { url, codeVerifier, state } = await createAuthorizationUrl(config);
236
+ // Redirect the user to `url`...
237
+
238
+ // 2. Exchange the authorization code for tokens
239
+ const tokenResult = await exchangeCodeForToken(config, {
240
+ code: "code-from-callback",
241
+ codeVerifier,
242
+ redirectUri: null,
243
+ });
244
+ if (tokenResult.isErr()) throw new Error(tokenResult.error.message);
245
+ const tokens = tokenResult.value;
246
+
247
+ // 3. Make authenticated API calls
248
+ const client = createFanvueClient(tokens.access_token, null);
249
+ const userResult = await client.getCurrentUser();
250
+ if (userResult.isOk()) console.log(userResult.value);
251
+
252
+ // 4. Refresh an expired access token
253
+ const refreshResult = await refreshAccessToken(config, tokens.refresh_token);
254
+ ```
255
+
256
+ For the embedded flow, the equivalent core primitive is `exchangeSessionToken(config, sessionToken)`,
257
+ which runs PKCE generation, the delegated authorize-on-behalf request, and the
258
+ code exchange in one call.
259
+
260
+ <details>
261
+ <summary>Session JWT helpers (for managing your own sessions)</summary>
262
+
263
+ ```ts
264
+ import { createSessionJwt, verifySessionJwt } from "@fanvue/builder-sdk";
265
+
266
+ // Create a signed session JWT (HS256, default 30-day expiry)
267
+ const jwt = await createSessionJwt("your-session-secret", {
268
+ accessToken: tokens.access_token,
269
+ refreshToken: tokens.refresh_token,
270
+ expiresAt: Date.now() + tokens.expires_in * 1000,
271
+ tokenType: tokens.token_type,
272
+ scope: tokens.scope,
273
+ idToken: tokens.id_token,
274
+ userUuid: "...",
275
+ handle: "...",
276
+ displayName: "...",
277
+ isCreator: false,
278
+ avatarUrl: null,
279
+ });
280
+
281
+ // Verify a session JWT
282
+ const session = await verifySessionJwt("your-session-secret", jwt);
283
+ ```
284
+
285
+ </details>
286
+
287
+ ## Environment Variables
288
+
289
+ Add these to your `.env.local` (Next.js) or equivalent:
290
+
291
+ ```env
292
+ # Required
293
+ OAUTH_CLIENT_ID=your-client-id
294
+ OAUTH_CLIENT_SECRET=your-client-secret
295
+ OAUTH_REDIRECT_URI=http://localhost:3000/api/oauth/callback
296
+ SESSION_SECRET=at-least-32-characters-long-random-string
297
+
298
+ # Optional (shown with defaults)
299
+ OAUTH_ISSUER_BASE_URL=https://auth.fanvue.com
300
+ API_BASE_URL=https://api.fanvue.com
301
+ FANVUE_PLATFORM_URL=https://www.fanvue.com # embedded apps only
302
+ OAUTH_SCOPES="openid offline_access offline"
303
+ SESSION_COOKIE_NAME=fanvue_session
304
+ # OAUTH_RESPONSE_MODE= # query, form_post, etc.
305
+ # OAUTH_PROMPT= # login, consent, etc.
306
+ ```
307
+
308
+ For the Fanvue dev environment use `https://auth.dev.fanvue.com`,
309
+ `https://api.dev.fanvue.com`, and `https://dev.fanvue.com` respectively.
310
+
311
+ The `createConfig()` helper reads these automatically. You can also override any value programmatically:
312
+
313
+ ```ts
314
+ const config = createConfig({
315
+ clientId: "explicit-id", // overrides OAUTH_CLIENT_ID
316
+ sessionCookieName: "my_session", // overrides SESSION_COOKIE_NAME
317
+ });
318
+ ```
319
+
320
+ ## Error Handling
321
+
322
+ 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:
323
+
324
+ ```ts
325
+ const result = await client.getCurrentUser();
326
+
327
+ if (result.isOk()) {
328
+ console.log(result.value); // FanvueUser
329
+ } else {
330
+ console.error(result.error); // ApiError
331
+ }
332
+
333
+ // Or use neverthrow's functional API
334
+ result
335
+ .map((user) => console.log(user.displayName))
336
+ .mapErr((err) => console.error(err.message));
337
+ ```
338
+
339
+ Error types: `OAuthError` (token exchange/refresh), `EmbeddedAuthError` (delegated authorize-on-behalf), `ApiError` (API requests), `SessionVerifyError` (JWT verification).
340
+
341
+ ## API Reference
342
+
343
+ ### Core (`@fanvue/builder-sdk`)
344
+
345
+ | Export | Description |
346
+ |---|---|
347
+ | `createAuthorizationUrl(config, opts?)` | Build a PKCE authorization URL. Returns `{ url, codeVerifier, state }`. |
348
+ | `exchangeCodeForToken(config, opts)` | Exchange an authorization code for tokens. Returns `Result<TokenResponse, OAuthError>`. |
349
+ | `refreshAccessToken(config, refreshToken)` | Refresh an expired access token. Returns `Result<TokenResponse, OAuthError>`. |
350
+ | `exchangeSessionToken(config, sessionToken)` | Exchange an embedded session token for tokens (full delegated flow). Returns `Result<TokenResponse, EmbeddedAuthError \| OAuthError>`. |
351
+ | `requestAuthorizationCodeOnBehalf(config, sessionToken, opts)` | Low-level: request a delegated authorization code from the platform. |
352
+ | `getSessionTokenFromUrl(url)` | Extract the `?token=` session token from a URL. |
353
+ | `getThemeFromUrl(url)` | Extract the `?theme=` colour scheme from a URL. Returns `'light' \| 'dark' \| null`. |
354
+ | `createSessionJwt(secret, payload, expiresIn?)` | Create a signed HS256 session JWT. Default expiry: 30 days. |
355
+ | `verifySessionJwt(secret, token)` | Verify and decode a session JWT. Returns `Result<SessionPayload, SessionVerifyError>`. |
356
+ | `createFanvueClient(accessToken, apiBaseUrl?)` | Create an authenticated API client. |
357
+ | `API_VERSION` | The API version header value (currently `2025-06-26`). |
358
+ | `HEADER_UPDATED_SESSION` | Response header (`X-Updated-Session`) carrying a refreshed session JWT. |
359
+
360
+ #### Types
361
+
362
+ | Type | Description |
363
+ |---|---|
364
+ | `OAuthConfig` | Client ID, secret, redirect URI, and optional overrides |
365
+ | `EmbeddedAuthConfig` | `OAuthConfig` plus the Fanvue platform base URL |
366
+ | `FanvueTheme` | The creator's active colour scheme: `'light' \| 'dark'` |
367
+ | `TokenResponse` | Access token, refresh token, expiry, scopes |
368
+ | `SessionPayload` | JWT claims: tokens, user info, expiry timestamp |
369
+ | `FanvueUser` | User profile (uuid, email, handle, displayName, isCreator, avatarUrl, etc.) |
370
+ | `OAuthError` | Error from token exchange or refresh |
371
+ | `EmbeddedAuthError` | Error from the delegated authorize-on-behalf flow |
372
+ | `ApiError` | Error from API requests |
373
+ | `SessionVerifyError` | Error from session JWT verification |
374
+
375
+ ### Next.js Embedded App (`@fanvue/builder-sdk/nextjs/embedded-app`)
376
+
377
+ | Export | Description |
378
+ |---|---|
379
+ | `createConfig(opts?)` | Build an `EmbeddedAppConfig` from env vars and/or explicit options (adds `platformUrl`) |
380
+ | `createSessionExchangeHandler(config, hooks?)` | `{ POST }` -- exchanges the embedded session token for tokens, responds with a session JWT |
381
+ | `getSession(secret)` | Read and verify the session from the `Authorization: Bearer` header |
382
+ | `getAuthenticatedClient(opts)` | Get a `FanvueClient` from the Bearer session, with automatic token refresh |
383
+ | `getSessionTokenFromUrl(url)` | Extract the `?token=` session token from a URL |
384
+
385
+ ### Next.js Off-Platform App (`@fanvue/builder-sdk/nextjs/off-platform`)
386
+
387
+ | Export | Description |
388
+ |---|---|
389
+ | `createConfig(opts?)` | Build a `ResolvedConfig` from env vars and/or explicit options |
390
+ | `createLoginHandler(opts)` | `{ GET }` -- redirects to the OAuth provider |
391
+ | `createCallbackHandler(opts)` | `{ GET, POST }` -- completes the code exchange, sets the session cookie |
392
+ | `createLogoutHandler(opts)` | `{ POST }` -- clears the session cookie |
393
+ | `getSession(secret, cookieName?)` | Read and verify the session from cookies |
394
+ | `getAuthenticatedClient(opts)` | Get a `FanvueClient` with automatic token refresh |
395
+
396
+ ### React (`@fanvue/builder-sdk/react`)
397
+
398
+ | Export | Description |
399
+ |---|---|
400
+ | `AuthProvider` | Context provider. Manages JWT storage in `sessionStorage`. |
401
+ | `useAuth()` | Returns `{ jwt, isAuthenticated, setJwt, clearJwt, authFetch }` |
402
+ | `useEmbeddedAuth(opts?)` | Returns `{ status, error, theme }`. Exchanges the embedded session token on mount; `theme` is the creator's colour scheme (`'light' \| 'dark' \| null`). |
403
+
404
+ ## Examples
405
+
406
+ Complete working examples:
407
+
408
+ | Example | App type | Auth flow |
409
+ |---|---|---|
410
+ | [`examples/nextjs-embedded-app`](examples/nextjs-embedded-app) | Embedded | Session-token exchange (delegated authorize-on-behalf) |
411
+ | [`examples/nextjs-off-platform-app`](examples/nextjs-off-platform-app) | Off-platform | "Login with Fanvue" full-page redirect |
412
+
413
+ ## Contributing
414
+
415
+ ```bash
416
+ pnpm install # Install dependencies
417
+ pnpm build # Build the library
418
+ pnpm dev # Watch mode
419
+ pnpm test # Run tests
420
+ pnpm typecheck # Type-check
421
+ pnpm lint # Lint
422
+ pnpm check # Run all checks (format + lint + typecheck + test)
423
+ ```
424
+
425
+ ## License
426
+
427
+ MIT
@@ -0,0 +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-pS9wR5yg.js";
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 };
@@ -0,0 +1,2 @@
1
+ import { C as assertFanvueDomain, S as DEFAULT_SCOPES, T as HEADER_UPDATED_SESSION, _ as safeJsonParse, a as requestAuthorizationCodeOnBehalf, b as DEFAULT_ISSUER_URL, c as createAuthorizationUrl, d as AuthorizeOnBehalfResponseSchema, f as FanvueUserSchema, 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 API_VERSION, w as BEARER_PREFIX, x as DEFAULT_PLATFORM_URL, y as DEFAULT_API_BASE_URL } from "../core-CvVOMyqr.js";
2
+ 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 };