@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
@@ -1,5 +1,5 @@
1
- import { C as OAuthConfig, D as TokenResponse, T as SessionPayload, t as FanvueClient, u as getSessionTokenFromUrl, x as FanvueUser, y as EmbeddedAuthConfig } from "../../index-pS9wR5yg.js";
2
- import { n as ResolvedConfig, t as FanvueAuthOptions } from "../../index-CweVyIKX.js";
1
+ import { H as EmbeddedAuthConfig, J as SessionPayload, K as OAuthConfig, N as getSessionTokenFromUrl, W as FanvueUser, X as TokenResponse, w as FanvueClient } from "../../index-C3CXrRiw.js";
2
+ import { n as FanvueAuthOptions, r as ResolvedConfig } from "../../index-v_6Z4Q0u.js";
3
3
  import { NextResponse } from "next/server";
4
4
 
5
5
  //#region src/nextjs/bearer-session.d.ts
@@ -1,5 +1,6 @@
1
- import { g as string, h as object, n as exchangeSessionToken, o as createSessionJwt, r as getSessionTokenFromUrl, s as verifySessionJwt, t as createFanvueClient, u as refreshAccessToken, w as BEARER_PREFIX } from "../../core-CvVOMyqr.js";
2
- import { n as fetchUserProfile, o as LOG_PREFIX, t as createConfig$1 } from "../../nextjs-CESI_EiU.js";
1
+ import { f as string, l as object } from "../../index.cjs-teGsk6HB.js";
2
+ import { _ as createSessionJwt, bt as BEARER_PREFIX, f as createFanvueClient, m as getSessionTokenFromUrl, p as exchangeSessionToken, v as verifySessionJwt, x as refreshAccessToken } from "../../core-BhKiA55a.js";
3
+ import { r as fetchUserProfile, s as LOG_PREFIX, t as createConfig$1 } from "../../nextjs-CvVhtMdb.js";
3
4
  import { NextResponse } from "next/server";
4
5
  import { headers } from "next/headers";
5
6
  //#region src/nextjs/bearer-session.ts
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["createSharedConfig","z.object","z.string"],"sources":["../../../src/nextjs/bearer-session.ts","../../../src/nextjs/embedded-app/index.ts"],"sourcesContent":["import { headers } from 'next/headers';\n\nimport {\n BEARER_PREFIX,\n verifySessionJwt,\n createSessionJwt,\n createFanvueClient,\n refreshAccessToken,\n type FanvueClient,\n type OAuthConfig,\n type SessionPayload,\n} from '../core/index.js';\n\nimport { LOG_PREFIX } from './constants.js';\n\nconst TOKEN_REFRESH_THRESHOLD_MS = 30_000;\n\n/**\n * Reads and verifies the session JWT from the `Authorization: Bearer` header.\n *\n * Used by embedded apps, which hold the session JWT client-side and send it\n * with each request, instead of using cookies.\n *\n * @param secret - The secret used to verify the JWT.\n * @returns The verified {@link SessionPayload}, or `null` if missing/invalid.\n */\nexport async function getSession(secret: string): Promise<SessionPayload | null> {\n const hdrs = await headers();\n const auth = hdrs.get('authorization');\n if (!auth?.startsWith(BEARER_PREFIX)) return null;\n const token = auth.slice(BEARER_PREFIX.length);\n const result = await verifySessionJwt(secret, token);\n if (result.isErr()) {\n console.warn(`${LOG_PREFIX} session verify failed:`, result.error);\n return null;\n }\n return result.value;\n}\n\n/**\n * Returns an authenticated {@link FanvueClient} for the current\n * `Authorization: Bearer` session.\n *\n * If the access token is close to expiring and a refresh token is available,\n * the token is refreshed transparently. Because the session lives client-side\n * (not in a cookie), the refreshed JWT is returned as `refreshedJwt` — set it\n * on the response as the `X-Updated-Session` header so the client can store\n * it (the React `authFetch` helper picks it up automatically).\n *\n * @param opts - Session secret and OAuth config.\n * @returns The client and (when a refresh happened) the new session JWT, or\n * `null` if no valid session exists or token refresh fails.\n */\nexport async function getAuthenticatedClient(opts: {\n sessionSecret: string;\n config: OAuthConfig;\n}): Promise<{ client: FanvueClient; refreshedJwt: string | null } | null> {\n let session = await getSession(opts.sessionSecret);\n if (!session) return null;\n\n let refreshedJwt: string | null = null;\n if (Date.now() >= session.expiresAt - TOKEN_REFRESH_THRESHOLD_MS && session.refreshToken) {\n const refreshResult = await refreshAccessToken(opts.config, session.refreshToken);\n\n if (refreshResult.isErr()) {\n console.warn(`${LOG_PREFIX} token refresh failed:`, refreshResult.error);\n return null;\n }\n\n const refreshed = refreshResult.value;\n // When the refresh response omits optional fields (parsed as null by the schema),\n // preserve the existing session values. Per RFC 6749 §6, a refresh response MAY\n // include a new refresh_token; when absent, the existing one remains valid.\n session = {\n ...session,\n accessToken: refreshed.access_token,\n refreshToken: refreshed.refresh_token ?? session.refreshToken,\n tokenType: refreshed.token_type ?? session.tokenType,\n scope: refreshed.scope ?? session.scope,\n idToken: refreshed.id_token ?? session.idToken,\n expiresAt: Date.now() + refreshed.expires_in * 1000,\n };\n refreshedJwt = await createSessionJwt(opts.sessionSecret, session);\n }\n\n return {\n client: createFanvueClient(session.accessToken, opts.config.apiBaseUrl),\n refreshedJwt,\n };\n}\n","import { NextResponse } from 'next/server';\nimport { z } from 'zod';\n\nimport {\n exchangeSessionToken,\n createSessionJwt,\n type EmbeddedAuthConfig,\n type FanvueUser,\n type SessionPayload,\n type TokenResponse,\n} from '../../core/index.js';\nimport { LOG_PREFIX } from '../constants.js';\nimport { createConfig as createSharedConfig } from '../index.js';\nimport { fetchUserProfile } from '../utils.js';\n\nimport type { FanvueAuthOptions, ResolvedConfig } from '../index.js';\n\nexport { getSession, getAuthenticatedClient } from '../bearer-session.js';\nexport { getSessionTokenFromUrl } from '../../core/index.js';\nexport { type FanvueAuthOptions } from '../index.js';\n\n/**\n * User-facing configuration options for an embedded Fanvue app.\n *\n * Extends {@link FanvueAuthOptions} with the Fanvue platform base URL. All\n * fields are optional — when omitted, values are read from environment\n * variables (`platformUrl` reads `FANVUE_PLATFORM_URL`).\n */\nexport type EmbeddedAppAuthOptions = FanvueAuthOptions & {\n platformUrl?: string;\n};\n\n/**\n * Fully-resolved configuration for the embedded-app handlers, produced by\n * {@link createConfig}.\n */\nexport type EmbeddedAppConfig = ResolvedConfig & EmbeddedAuthConfig;\n\n/**\n * Resolves a complete {@link EmbeddedAppConfig} from the supplied options,\n * falling back to environment variables for any values not provided.\n *\n * @param opts - Optional overrides for the OAuth, session, and platform configuration.\n * @returns A fully-resolved configuration object.\n * @throws If any required value (`clientId`, `clientSecret`, `redirectUri`,\n * `sessionSecret`) is missing from both `opts` and the environment.\n */\nexport function createConfig(opts?: EmbeddedAppAuthOptions): EmbeddedAppConfig {\n const shared = createSharedConfig(opts);\n const platformUrl = opts?.platformUrl ?? process.env['FANVUE_PLATFORM_URL'];\n return { ...shared, platformUrl: platformUrl ?? null };\n}\n\nconst ExchangeRequestSchema = z.object({\n token: z.string().min(1),\n});\n\n/**\n * Callback invoked after a successful session-token exchange, before the\n * response is sent. Use it to persist the refresh token per creator so your\n * app can act on their behalf later, without the iframe open.\n */\nexport type OnTokensCallback = (input: {\n tokens: TokenResponse;\n user: FanvueUser;\n}) => void | Promise<void>;\n\n/**\n * Creates a Next.js route handler that exchanges an embedded session token\n * for a Fanvue session.\n *\n * The embedded page reads the `?token=` query parameter that Fanvue appends\n * to the iframe URL and POSTs it here as JSON (`{ \"token\": \"...\" }`). The\n * handler runs the delegated authorize-on-behalf flow, fetches the user's\n * profile, and responds with a signed session JWT (`{ \"jwt\": \"...\" }`) that\n * the client sends back as an `Authorization: Bearer` header — the React\n * `useEmbeddedAuth` hook does all of this for you.\n *\n * Error responses mirror the platform's semantics:\n * - `400 { error: \"invalid_request\" }` — malformed body.\n * - `401 { error: \"invalid_session_token\" }` — the session token was\n * rejected (they expire after ~60 seconds; reopening the surface mints a\n * fresh one).\n * - `403 { error: \"consent_required\" }` — the creator has not approved (or\n * has revoked) consent for this app in the Fanvue UI.\n * - `502 { error: \"exchange_failed\" }` — any other failure.\n *\n * @param config - The embedded-app configuration.\n * @param hooks - Optional `onTokens` callback for persisting tokens (e.g.\n * the refresh token, keyed by `user.uuid`, for offline/background work).\n * @returns A route handler object with a `POST` method.\n */\nexport function createSessionExchangeHandler(\n config: EmbeddedAppConfig,\n hooks?: { onTokens?: OnTokensCallback },\n): {\n POST(request: Request): Promise<NextResponse>;\n} {\n return {\n async POST(request: Request): Promise<NextResponse> {\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n return NextResponse.json({ error: 'invalid_request' }, { status: 400 });\n }\n\n const parsed = ExchangeRequestSchema.safeParse(body);\n if (!parsed.success) {\n return NextResponse.json({ error: 'invalid_request' }, { status: 400 });\n }\n\n const tokenResult = await exchangeSessionToken(config, parsed.data.token);\n if (tokenResult.isErr()) {\n const error = tokenResult.error;\n if (error.code === 'SESSION_TOKEN_REJECTED') {\n return NextResponse.json({ error: 'invalid_session_token' }, { status: 401 });\n }\n if (error.code === 'CONSENT_REQUIRED') {\n return NextResponse.json({ error: 'consent_required' }, { status: 403 });\n }\n console.error(`${LOG_PREFIX} session token exchange failed:`, error);\n return NextResponse.json({ error: 'exchange_failed' }, { status: 502 });\n }\n\n const tokens = tokenResult.value;\n const user = await fetchUserProfile(tokens.access_token, config.apiBaseUrl);\n if (!user) {\n return NextResponse.json({ error: 'exchange_failed' }, { status: 502 });\n }\n\n if (hooks?.onTokens) {\n try {\n await hooks.onTokens({ tokens, user });\n } catch (error) {\n console.error(`${LOG_PREFIX} onTokens callback failed:`, error);\n return NextResponse.json({ error: 'exchange_failed' }, { status: 502 });\n }\n }\n\n const sessionPayload: SessionPayload = {\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n tokenType: tokens.token_type,\n scope: tokens.scope,\n idToken: tokens.id_token,\n expiresAt: Date.now() + tokens.expires_in * 1000,\n userUuid: user.uuid,\n handle: user.handle,\n displayName: user.displayName,\n isCreator: user.isCreator,\n avatarUrl: user.avatarUrl,\n };\n\n const jwt = await createSessionJwt(config.sessionSecret, sessionPayload);\n return NextResponse.json({ jwt });\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["createSharedConfig","z.object","z.string"],"sources":["../../../src/nextjs/bearer-session.ts","../../../src/nextjs/embedded-app/index.ts"],"sourcesContent":["import { headers } from 'next/headers';\n\nimport {\n BEARER_PREFIX,\n verifySessionJwt,\n createSessionJwt,\n createFanvueClient,\n refreshAccessToken,\n type FanvueClient,\n type OAuthConfig,\n type SessionPayload,\n} from '../core/index.js';\n\nimport { LOG_PREFIX } from './constants.js';\n\nconst TOKEN_REFRESH_THRESHOLD_MS = 30_000;\n\n/**\n * Reads and verifies the session JWT from the `Authorization: Bearer` header.\n *\n * Used by embedded apps, which hold the session JWT client-side and send it\n * with each request, instead of using cookies.\n *\n * @param secret - The secret used to verify the JWT.\n * @returns The verified {@link SessionPayload}, or `null` if missing/invalid.\n */\nexport async function getSession(secret: string): Promise<SessionPayload | null> {\n const hdrs = await headers();\n const auth = hdrs.get('authorization');\n if (!auth?.startsWith(BEARER_PREFIX)) return null;\n const token = auth.slice(BEARER_PREFIX.length);\n const result = await verifySessionJwt(secret, token);\n if (result.isErr()) {\n console.warn(`${LOG_PREFIX} session verify failed:`, result.error);\n return null;\n }\n return result.value;\n}\n\n/**\n * Returns an authenticated {@link FanvueClient} for the current\n * `Authorization: Bearer` session.\n *\n * If the access token is close to expiring and a refresh token is available,\n * the token is refreshed transparently. Because the session lives client-side\n * (not in a cookie), the refreshed JWT is returned as `refreshedJwt` — set it\n * on the response as the `X-Updated-Session` header so the client can store\n * it (the React `authFetch` helper picks it up automatically).\n *\n * @param opts - Session secret and OAuth config.\n * @returns The client and (when a refresh happened) the new session JWT, or\n * `null` if no valid session exists or token refresh fails.\n */\nexport async function getAuthenticatedClient(opts: {\n sessionSecret: string;\n config: OAuthConfig;\n}): Promise<{ client: FanvueClient; refreshedJwt: string | null } | null> {\n let session = await getSession(opts.sessionSecret);\n if (!session) return null;\n\n let refreshedJwt: string | null = null;\n if (Date.now() >= session.expiresAt - TOKEN_REFRESH_THRESHOLD_MS && session.refreshToken) {\n const refreshResult = await refreshAccessToken(opts.config, session.refreshToken);\n\n if (refreshResult.isErr()) {\n console.warn(`${LOG_PREFIX} token refresh failed:`, refreshResult.error);\n return null;\n }\n\n const refreshed = refreshResult.value;\n // When the refresh response omits optional fields (parsed as null by the schema),\n // preserve the existing session values. Per RFC 6749 §6, a refresh response MAY\n // include a new refresh_token; when absent, the existing one remains valid.\n session = {\n ...session,\n accessToken: refreshed.access_token,\n refreshToken: refreshed.refresh_token ?? session.refreshToken,\n tokenType: refreshed.token_type ?? session.tokenType,\n scope: refreshed.scope ?? session.scope,\n idToken: refreshed.id_token ?? session.idToken,\n expiresAt: Date.now() + refreshed.expires_in * 1000,\n };\n refreshedJwt = await createSessionJwt(opts.sessionSecret, session);\n }\n\n return {\n client: createFanvueClient(session.accessToken, opts.config.apiBaseUrl),\n refreshedJwt,\n };\n}\n","import { NextResponse } from 'next/server';\nimport { z } from 'zod';\n\nimport {\n exchangeSessionToken,\n createSessionJwt,\n type EmbeddedAuthConfig,\n type FanvueUser,\n type SessionPayload,\n type TokenResponse,\n} from '../../core/index.js';\nimport { LOG_PREFIX } from '../constants.js';\nimport { createConfig as createSharedConfig } from '../index.js';\nimport { fetchUserProfile } from '../utils.js';\n\nimport type { FanvueAuthOptions, ResolvedConfig } from '../index.js';\n\nexport { getSession, getAuthenticatedClient } from '../bearer-session.js';\nexport { getSessionTokenFromUrl } from '../../core/index.js';\nexport { type FanvueAuthOptions } from '../index.js';\n\n/**\n * User-facing configuration options for an embedded Fanvue app.\n *\n * Extends {@link FanvueAuthOptions} with the Fanvue platform base URL. All\n * fields are optional — when omitted, values are read from environment\n * variables (`platformUrl` reads `FANVUE_PLATFORM_URL`).\n */\nexport type EmbeddedAppAuthOptions = FanvueAuthOptions & {\n platformUrl?: string;\n};\n\n/**\n * Fully-resolved configuration for the embedded-app handlers, produced by\n * {@link createConfig}.\n */\nexport type EmbeddedAppConfig = ResolvedConfig & EmbeddedAuthConfig;\n\n/**\n * Resolves a complete {@link EmbeddedAppConfig} from the supplied options,\n * falling back to environment variables for any values not provided.\n *\n * @param opts - Optional overrides for the OAuth, session, and platform configuration.\n * @returns A fully-resolved configuration object.\n * @throws If any required value (`clientId`, `clientSecret`, `redirectUri`,\n * `sessionSecret`) is missing from both `opts` and the environment.\n */\nexport function createConfig(opts?: EmbeddedAppAuthOptions): EmbeddedAppConfig {\n const shared = createSharedConfig(opts);\n const platformUrl = opts?.platformUrl ?? process.env['FANVUE_PLATFORM_URL'];\n return { ...shared, platformUrl: platformUrl ?? null };\n}\n\nconst ExchangeRequestSchema = z.object({\n token: z.string().min(1),\n});\n\n/**\n * Callback invoked after a successful session-token exchange, before the\n * response is sent. Use it to persist the refresh token per creator so your\n * app can act on their behalf later, without the iframe open.\n */\nexport type OnTokensCallback = (input: {\n tokens: TokenResponse;\n user: FanvueUser;\n}) => void | Promise<void>;\n\n/**\n * Creates a Next.js route handler that exchanges an embedded session token\n * for a Fanvue session.\n *\n * The embedded page reads the `?token=` query parameter that Fanvue appends\n * to the iframe URL and POSTs it here as JSON (`{ \"token\": \"...\" }`). The\n * handler runs the delegated authorize-on-behalf flow, fetches the user's\n * profile, and responds with a signed session JWT (`{ \"jwt\": \"...\" }`) that\n * the client sends back as an `Authorization: Bearer` header — the React\n * `useEmbeddedAuth` hook does all of this for you.\n *\n * Error responses mirror the platform's semantics:\n * - `400 { error: \"invalid_request\" }` — malformed body.\n * - `401 { error: \"invalid_session_token\" }` — the session token was\n * rejected (they expire after ~60 seconds; reopening the surface mints a\n * fresh one).\n * - `403 { error: \"consent_required\" }` — the creator has not approved (or\n * has revoked) consent for this app in the Fanvue UI.\n * - `502 { error: \"exchange_failed\" }` — any other failure.\n *\n * @param config - The embedded-app configuration.\n * @param hooks - Optional `onTokens` callback for persisting tokens (e.g.\n * the refresh token, keyed by `user.uuid`, for offline/background work).\n * @returns A route handler object with a `POST` method.\n */\nexport function createSessionExchangeHandler(\n config: EmbeddedAppConfig,\n hooks?: { onTokens?: OnTokensCallback },\n): {\n POST(request: Request): Promise<NextResponse>;\n} {\n return {\n async POST(request: Request): Promise<NextResponse> {\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n return NextResponse.json({ error: 'invalid_request' }, { status: 400 });\n }\n\n const parsed = ExchangeRequestSchema.safeParse(body);\n if (!parsed.success) {\n return NextResponse.json({ error: 'invalid_request' }, { status: 400 });\n }\n\n const tokenResult = await exchangeSessionToken(config, parsed.data.token);\n if (tokenResult.isErr()) {\n const error = tokenResult.error;\n if (error.code === 'SESSION_TOKEN_REJECTED') {\n return NextResponse.json({ error: 'invalid_session_token' }, { status: 401 });\n }\n if (error.code === 'CONSENT_REQUIRED') {\n return NextResponse.json({ error: 'consent_required' }, { status: 403 });\n }\n console.error(`${LOG_PREFIX} session token exchange failed:`, error);\n return NextResponse.json({ error: 'exchange_failed' }, { status: 502 });\n }\n\n const tokens = tokenResult.value;\n const user = await fetchUserProfile(tokens.access_token, config.apiBaseUrl);\n if (!user) {\n return NextResponse.json({ error: 'exchange_failed' }, { status: 502 });\n }\n\n if (hooks?.onTokens) {\n try {\n await hooks.onTokens({ tokens, user });\n } catch (error) {\n console.error(`${LOG_PREFIX} onTokens callback failed:`, error);\n return NextResponse.json({ error: 'exchange_failed' }, { status: 502 });\n }\n }\n\n const sessionPayload: SessionPayload = {\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n tokenType: tokens.token_type,\n scope: tokens.scope,\n idToken: tokens.id_token,\n expiresAt: Date.now() + tokens.expires_in * 1000,\n userUuid: user.uuid,\n handle: user.handle,\n displayName: user.displayName,\n isCreator: user.isCreator,\n avatarUrl: user.avatarUrl,\n };\n\n const jwt = await createSessionJwt(config.sessionSecret, sessionPayload);\n return NextResponse.json({ jwt });\n },\n };\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { C as OAuthConfig, T as SessionPayload, t as FanvueClient } from "../../index-pS9wR5yg.js";
2
- import { n as ResolvedConfig, r as createConfig, t as FanvueAuthOptions } from "../../index-CweVyIKX.js";
1
+ import { J as SessionPayload, K as OAuthConfig, w as FanvueClient } from "../../index-C3CXrRiw.js";
2
+ import { a as createConfigSafe, i as createConfig, n as FanvueAuthOptions, r as ResolvedConfig, t as CreateConfigResult } from "../../index-v_6Z4Q0u.js";
3
3
  import { NextResponse } from "next/server";
4
4
 
5
5
  //#region src/nextjs/off-platform/session.d.ts
@@ -70,5 +70,5 @@ declare function createLogoutHandler(opts: OffPlatformOptions): {
70
70
  POST(request: Request): NextResponse;
71
71
  };
72
72
  //#endregion
73
- export { type FanvueAuthOptions, OffPlatformOptions, type ResolvedConfig, createCallbackHandler, createConfig, createLoginHandler, createLogoutHandler, getAuthenticatedClient, getSession };
73
+ export { type CreateConfigResult, type FanvueAuthOptions, OffPlatformOptions, type ResolvedConfig, createCallbackHandler, createConfig, createConfigSafe, createLoginHandler, createLogoutHandler, getAuthenticatedClient, getSession };
74
74
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/nextjs/off-platform/session.ts","../../../src/nextjs/off-platform/index.ts"],"mappings":";;;;;;;;;AA2BA;;;iBAAsB,UAAA,CACpB,MAAA,UACA,UAAA,mBACC,OAAA,CAAQ,cAAA;;;;;;;;;;;iBAuDW,sBAAA,CAAuB,IAAA;EAC3C,aAAA;EACA,iBAAA;EACA,MAAA,EAAQ,WAAA;AAAA,IACN,OAAA,CAAQ,YAAA;;;;AA9DZ;;;;KCLY,kBAAA,GAAqB,cAAA;EAC/B,cAAA;EACA,eAAA;AAAA;;;AD6DF;;;;iBCpDgB,kBAAA,CAAmB,IAAA,EAAM,kBAAA;EACvC,GAAA,CAAI,OAAA,EAAS,OAAA,GAAU,OAAA,CAAQ,YAAA;AAAA;;;;;;;;;;iBAcjB,qBAAA,CAAsB,IAAA,EAAM,kBAAA;EAC1C,GAAA,CAAI,OAAA,EAAS,OAAA,GAAU,OAAA,CAAQ,YAAA;EAC/B,IAAA,CAAK,OAAA,EAAS,OAAA,GAAU,OAAA,CAAQ,YAAA;AAAA;AA5BlC;;;;;;;AAAA,iBAoEgB,mBAAA,CAAoB,IAAA,EAAM,kBAAA;EACxC,IAAA,CAAK,OAAA,EAAS,OAAA,GAAU,YAAA;AAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/nextjs/off-platform/session.ts","../../../src/nextjs/off-platform/index.ts"],"mappings":";;;;;;;;;AA2BA;;;iBAAsB,UAAA,CACpB,MAAA,UACA,UAAA,mBACC,OAAA,CAAQ,cAAA;;;;;;;;;;;iBAuDW,sBAAA,CAAuB,IAAA;EAC3C,aAAA;EACA,iBAAA;EACA,MAAA,EAAQ,WAAA;AAAA,IACN,OAAA,CAAQ,YAAA;;;AA9DZ;;;;;AAAA,KCJY,kBAAA,GAAqB,cAAA;EAC/B,cAAA;EACA,eAAA;AAAA;;AD4DF;;;;;iBCnDgB,kBAAA,CAAmB,IAAA,EAAM,kBAAA;EACvC,GAAA,CAAI,OAAA,EAAS,OAAA,GAAU,OAAA,CAAQ,YAAA;AAAA;;;;;;;;;;iBAcjB,qBAAA,CAAsB,IAAA,EAAM,kBAAA;EAC1C,GAAA,CAAI,OAAA,EAAS,OAAA,GAAU,OAAA,CAAQ,YAAA;EAC/B,IAAA,CAAK,OAAA,EAAS,OAAA,GAAU,OAAA,CAAQ,YAAA;AAAA;;;;;;;;iBAwClB,mBAAA,CAAoB,IAAA,EAAM,kBAAA;EACxC,IAAA,CAAK,OAAA,EAAS,OAAA,GAAU,YAAA;AAAA"}
@@ -1,5 +1,6 @@
1
- import { c as createAuthorizationUrl, l as exchangeCodeForToken, o as createSessionJwt, s as verifySessionJwt, t as createFanvueClient, u as refreshAccessToken } from "../../core-CvVOMyqr.js";
2
- import { i as getFormString, n as fetchUserProfile, o as LOG_PREFIX, r as getBaseUrl, s as SESSION_COOKIE_MAX_AGE_SECONDS, t as createConfig } from "../../nextjs-CESI_EiU.js";
1
+ import "../../index.cjs-teGsk6HB.js";
2
+ import { _ as createSessionJwt, b as exchangeCodeForToken, f as createFanvueClient, v as verifySessionJwt, x as refreshAccessToken, y as createAuthorizationUrl } from "../../core-BhKiA55a.js";
3
+ import { a as getFormString, c as SESSION_COOKIE_MAX_AGE_SECONDS, i as getBaseUrl, n as createConfigSafe, r as fetchUserProfile, s as LOG_PREFIX, t as createConfig } from "../../nextjs-CvVhtMdb.js";
3
4
  import { NextResponse } from "next/server";
4
5
  import { cookies } from "next/headers";
5
6
  //#region src/nextjs/callback.ts
@@ -252,6 +253,6 @@ function createLogoutHandler(opts) {
252
253
  } };
253
254
  }
254
255
  //#endregion
255
- export { createCallbackHandler, createConfig, createLoginHandler, createLogoutHandler, getAuthenticatedClient, getSession };
256
+ export { createCallbackHandler, createConfig, createConfigSafe, createLoginHandler, createLogoutHandler, getAuthenticatedClient, getSession };
256
257
 
257
258
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["createLoginHandler","createSharedLoginHandler"],"sources":["../../../src/nextjs/callback.ts","../../../src/nextjs/off-platform/session.ts","../../../src/nextjs/off-platform/index.ts"],"sourcesContent":["import { cookies } from 'next/headers';\nimport { NextResponse } from 'next/server';\n\nimport {\n createAuthorizationUrl,\n exchangeCodeForToken,\n createSessionJwt,\n type SessionPayload,\n} from '../core/index.js';\n\nimport { LOG_PREFIX, COOKIE_SAME_SITE } from './constants.js';\nimport { fetchUserProfile, getBaseUrl } from './utils.js';\n\nimport type { ResolvedConfig } from './index.js';\n\nconst COOKIE_OAUTH_STATE = 'oauth_state';\nconst COOKIE_OAUTH_VERIFIER = 'oauth_verifier';\nconst OAUTH_COOKIE_MAX_AGE_SECONDS = 600;\nconst ERROR_STATE_MISMATCH = 'oauth_state_mismatch';\nconst ERROR_TOKEN_EXCHANGE_FAILED = 'oauth_token_exchange_failed';\nconst ERROR_USER_PROFILE_FAILED = 'oauth_user_profile_failed';\n\n/** Parameters extracted from the OAuth callback request. */\nexport type CallbackParams =\n | { kind: 'success'; code: string; state: string }\n | { kind: 'error'; error: string; errorDescription: string | null }\n | { kind: 'empty' };\n\n/** Strategy for completing the OAuth callback — how the JWT is delivered to the client. */\nexport interface CallbackStrategy {\n complete(request: Request, jwt: string, baseUrl: string): NextResponse;\n}\n\n/** Options required by the shared callback handler. */\nexport type CallbackHandlerOptions = ResolvedConfig;\n\n/**\n * Shared callback handler that validates the OAuth state, exchanges the\n * authorization code for tokens, fetches the user profile, creates a session\n * JWT, and delegates to the strategy for final response.\n */\nexport async function handleOAuthCallback(\n request: Request,\n opts: CallbackHandlerOptions,\n getParams: (request: Request) => Promise<CallbackParams>,\n strategy: CallbackStrategy,\n): Promise<NextResponse> {\n const baseUrl = getBaseUrl(opts, request.url);\n const params = await getParams(request);\n\n const cookieStore = await cookies();\n const storedState = cookieStore.get(COOKIE_OAUTH_STATE)?.value;\n const verifier = cookieStore.get(COOKIE_OAUTH_VERIFIER)?.value;\n\n cookieStore.delete(COOKIE_OAUTH_STATE);\n cookieStore.delete(COOKIE_OAUTH_VERIFIER);\n\n if (params.kind === 'error') {\n const search = new URLSearchParams();\n search.set('error', params.error);\n if (params.errorDescription) search.set('error_description', params.errorDescription);\n return NextResponse.redirect(new URL(`${baseUrl}/?${search.toString()}`, request.url));\n }\n\n if (params.kind === 'empty' || !storedState || !verifier || params.state !== storedState) {\n return NextResponse.redirect(new URL(`${baseUrl}/?error=${ERROR_STATE_MISMATCH}`, request.url));\n }\n\n const { code } = params;\n\n const tokenResult = await exchangeCodeForToken(opts, {\n code,\n codeVerifier: verifier,\n redirectUri: null,\n });\n\n if (tokenResult.isErr()) {\n console.error(`${LOG_PREFIX} token exchange failed:`, tokenResult.error);\n return NextResponse.redirect(\n new URL(`${baseUrl}/?error=${ERROR_TOKEN_EXCHANGE_FAILED}`, request.url),\n );\n }\n\n const token = tokenResult.value;\n const user = await fetchUserProfile(token.access_token, opts.apiBaseUrl);\n\n if (!user) {\n return NextResponse.redirect(\n new URL(`${baseUrl}/?error=${ERROR_USER_PROFILE_FAILED}`, request.url),\n );\n }\n\n const sessionPayload: SessionPayload = {\n accessToken: token.access_token,\n refreshToken: token.refresh_token,\n tokenType: token.token_type,\n scope: token.scope,\n idToken: token.id_token,\n expiresAt: Date.now() + token.expires_in * 1000,\n userUuid: user.uuid,\n handle: user.handle,\n displayName: user.displayName,\n isCreator: user.isCreator,\n avatarUrl: user.avatarUrl,\n };\n\n const jwt = await createSessionJwt(opts.sessionSecret, sessionPayload);\n\n return strategy.complete(request, jwt, baseUrl);\n}\n\n/**\n * Creates a Next.js route handler that initiates the OAuth login flow.\n * Used by the off-platform module.\n */\nexport function createLoginHandler(opts: CallbackHandlerOptions): {\n GET(request: Request): Promise<NextResponse>;\n} {\n return {\n async GET(request: Request): Promise<NextResponse> {\n const { url, codeVerifier, state } = await createAuthorizationUrl(opts);\n\n const cookieStore = await cookies();\n const reqUrl = new URL(request.url);\n const secure = reqUrl.protocol === 'https:';\n\n cookieStore.set(COOKIE_OAUTH_STATE, state, {\n httpOnly: true,\n path: '/',\n sameSite: COOKIE_SAME_SITE,\n secure,\n maxAge: OAUTH_COOKIE_MAX_AGE_SECONDS,\n });\n cookieStore.set(COOKIE_OAUTH_VERIFIER, codeVerifier, {\n httpOnly: true,\n path: '/',\n sameSite: COOKIE_SAME_SITE,\n secure,\n maxAge: OAUTH_COOKIE_MAX_AGE_SECONDS,\n });\n\n return NextResponse.redirect(url);\n },\n };\n}\n\n/** Extracts callback params from a GET request's query string. */\nexport function getParamsFromQuery(request: Request): Promise<CallbackParams> {\n const url = new URL(request.url);\n const error = url.searchParams.get('error');\n if (error) {\n return Promise.resolve({\n kind: 'error',\n error,\n errorDescription: url.searchParams.get('error_description'),\n });\n }\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n if (code && state) {\n return Promise.resolve({ kind: 'success', code, state });\n }\n return Promise.resolve({ kind: 'empty' });\n}\n\n/** Extracts callback params from a POST request's form body. */\nexport async function getParamsFromForm(\n request: Request,\n getFormString: (form: FormData, key: string) => string | null,\n): Promise<CallbackParams> {\n const form = await request.formData();\n const error = getFormString(form, 'error');\n if (error) {\n return {\n kind: 'error',\n error,\n errorDescription: getFormString(form, 'error_description'),\n };\n }\n const code = getFormString(form, 'code');\n const state = getFormString(form, 'state');\n if (code && state) {\n return { kind: 'success', code, state };\n }\n return { kind: 'empty' };\n}\n","import { cookies } from 'next/headers';\n\nimport {\n verifySessionJwt,\n createSessionJwt,\n createFanvueClient,\n refreshAccessToken,\n type FanvueClient,\n type OAuthConfig,\n type SessionPayload,\n} from '../../core/index.js';\nimport {\n DEFAULT_SESSION_COOKIE_NAME,\n SESSION_COOKIE_MAX_AGE_SECONDS,\n COOKIE_SAME_SITE,\n LOG_PREFIX,\n} from '../constants.js';\n\nconst TOKEN_REFRESH_THRESHOLD_MS = 30_000;\n\n/**\n * Reads and verifies the session JWT from the cookie store.\n *\n * @param secret - The secret used to verify the JWT.\n * @param cookieName - The cookie name to read. Defaults to {@link DEFAULT_SESSION_COOKIE_NAME}.\n * @returns The verified {@link SessionPayload}, or `null` if missing/invalid.\n */\nexport async function getSession(\n secret: string,\n cookieName: string | null = null,\n): Promise<SessionPayload | null> {\n const cookieStore = await cookies();\n const token = cookieStore.get(cookieName ?? DEFAULT_SESSION_COOKIE_NAME)?.value;\n if (!token) return null;\n const result = await verifySessionJwt(secret, token);\n if (result.isErr()) {\n console.warn(`${LOG_PREFIX} session verify failed:`, result.error);\n return null;\n }\n return result.value;\n}\n\n/**\n * Creates or updates the session cookie with a fresh JWT.\n *\n * @param secret - The secret used to sign the JWT.\n * @param cookieName - The cookie name to set.\n * @param payload - The session data to encode into the JWT.\n */\nexport async function setSession(\n secret: string,\n cookieName: string,\n payload: SessionPayload,\n): Promise<void> {\n const jwt = await createSessionJwt(secret, payload);\n const cookieStore = await cookies();\n cookieStore.set(cookieName, jwt, {\n httpOnly: true,\n secure: process.env['NODE_ENV'] === 'production',\n sameSite: COOKIE_SAME_SITE,\n path: '/',\n maxAge: SESSION_COOKIE_MAX_AGE_SECONDS,\n });\n}\n\n/**\n * Deletes the session cookie.\n *\n * @param cookieName - The cookie name to delete.\n */\nexport async function clearSession(cookieName: string): Promise<void> {\n const cookieStore = await cookies();\n cookieStore.delete(cookieName);\n}\n\n/**\n * Returns an authenticated {@link FanvueClient} for the current session.\n *\n * If the access token is close to expiring and a refresh token is available,\n * the token is refreshed transparently and the session cookie is updated.\n *\n * @param opts - Session secret, cookie name, and OAuth config.\n * @returns A {@link FanvueClient} instance, or `null` if no valid session exists\n * or token refresh fails.\n */\nexport async function getAuthenticatedClient(opts: {\n sessionSecret: string;\n sessionCookieName: string | null;\n config: OAuthConfig;\n}): Promise<FanvueClient | null> {\n const cookieName = opts.sessionCookieName ?? DEFAULT_SESSION_COOKIE_NAME;\n let session = await getSession(opts.sessionSecret, cookieName);\n if (!session) return null;\n\n if (Date.now() >= session.expiresAt - TOKEN_REFRESH_THRESHOLD_MS && session.refreshToken) {\n const refreshResult = await refreshAccessToken(opts.config, session.refreshToken);\n\n if (refreshResult.isErr()) {\n console.warn(`${LOG_PREFIX} token refresh failed:`, refreshResult.error);\n return null;\n }\n\n const refreshed = refreshResult.value;\n // When the refresh response omits optional fields (parsed as null by the schema),\n // preserve the existing session values. Per RFC 6749 §6, a refresh response MAY\n // include a new refresh_token; when absent, the existing one remains valid.\n session = {\n ...session,\n accessToken: refreshed.access_token,\n refreshToken: refreshed.refresh_token ?? session.refreshToken,\n tokenType: refreshed.token_type ?? session.tokenType,\n scope: refreshed.scope ?? session.scope,\n idToken: refreshed.id_token ?? session.idToken,\n expiresAt: Date.now() + refreshed.expires_in * 1000,\n };\n try {\n await setSession(opts.sessionSecret, cookieName, session);\n } catch (error) {\n console.warn(`${LOG_PREFIX} failed to persist refreshed session:`, error);\n }\n }\n\n return createFanvueClient(session.accessToken, opts.config.apiBaseUrl);\n}\n","import { NextResponse } from 'next/server';\n\nimport {\n handleOAuthCallback,\n createLoginHandler as createSharedLoginHandler,\n getParamsFromQuery,\n getParamsFromForm,\n} from '../callback.js';\nimport { COOKIE_SAME_SITE, SESSION_COOKIE_MAX_AGE_SECONDS } from '../constants.js';\nimport { type ResolvedConfig } from '../index.js';\nimport { getBaseUrl, getFormString } from '../utils.js';\n\nimport type { CallbackStrategy } from '../callback.js';\n\nexport { getSession, getAuthenticatedClient } from './session.js';\nexport { createConfig, type FanvueAuthOptions, type ResolvedConfig } from '../index.js';\n\n/**\n * Configuration for the off-platform OAuth handlers.\n *\n * Extends {@link ResolvedConfig} with optional routing overrides.\n */\nexport type OffPlatformOptions = ResolvedConfig & {\n afterLoginPath: string | null;\n afterLogoutPath: string | null;\n};\n\n/**\n * Creates a Next.js route handler that initiates the OAuth login flow.\n *\n * @param opts - The off-platform configuration.\n * @returns A route handler object with a `GET` method.\n */\nexport function createLoginHandler(opts: OffPlatformOptions): {\n GET(request: Request): Promise<NextResponse>;\n} {\n return createSharedLoginHandler(opts);\n}\n\n/**\n * Creates a Next.js route handler that processes the OAuth callback.\n *\n * Supports both `GET` (query-string params) and `POST` (form-encoded body)\n * response modes.\n *\n * @param opts - The off-platform configuration.\n * @returns A route handler object with `GET` and `POST` methods.\n */\nexport function createCallbackHandler(opts: OffPlatformOptions): {\n GET(request: Request): Promise<NextResponse>;\n POST(request: Request): Promise<NextResponse>;\n} {\n const strategy: CallbackStrategy = {\n complete(request: Request, jwt: string, baseUrl: string): NextResponse {\n const res = NextResponse.redirect(\n new URL(`${baseUrl}${opts.afterLoginPath ?? '/'}`, request.url),\n );\n res.cookies.set(opts.sessionCookieName, jwt, {\n httpOnly: true,\n secure: new URL(request.url).protocol === 'https:',\n sameSite: COOKIE_SAME_SITE,\n path: '/',\n maxAge: SESSION_COOKIE_MAX_AGE_SECONDS,\n });\n return res;\n },\n };\n\n return {\n async GET(request: Request): Promise<NextResponse> {\n return handleOAuthCallback(request, opts, getParamsFromQuery, strategy);\n },\n async POST(request: Request): Promise<NextResponse> {\n return handleOAuthCallback(\n request,\n opts,\n (req) => getParamsFromForm(req, getFormString),\n strategy,\n );\n },\n };\n}\n\n/**\n * Creates a Next.js route handler that logs the user out by deleting the\n * session cookie and redirecting.\n *\n * @param opts - The off-platform configuration.\n * @returns A route handler object with a `POST` method.\n */\nexport function createLogoutHandler(opts: OffPlatformOptions): {\n POST(request: Request): NextResponse;\n} {\n return {\n POST(request: Request): NextResponse {\n const baseUrl = getBaseUrl(opts, request.url);\n const res = NextResponse.redirect(\n new URL(`${baseUrl}${opts.afterLogoutPath ?? '/'}`, request.url),\n );\n res.cookies.delete(opts.sessionCookieName);\n return res;\n },\n };\n}\n"],"mappings":";;;;;AAeA,MAAM,qBAAqB;AAC3B,MAAM,wBAAwB;AAC9B,MAAM,+BAA+B;AACrC,MAAM,uBAAuB;AAC7B,MAAM,8BAA8B;AACpC,MAAM,4BAA4B;;;;;;AAqBlC,eAAsB,oBACpB,SACA,MACA,WACA,UACuB;CACvB,MAAM,UAAU,WAAW,MAAM,QAAQ,IAAI;CAC7C,MAAM,SAAS,MAAM,UAAU,QAAQ;CAEvC,MAAM,cAAc,MAAM,SAAS;CACnC,MAAM,cAAc,YAAY,IAAI,mBAAmB,EAAE;CACzD,MAAM,WAAW,YAAY,IAAI,sBAAsB,EAAE;AAEzD,aAAY,OAAO,mBAAmB;AACtC,aAAY,OAAO,sBAAsB;AAEzC,KAAI,OAAO,SAAS,SAAS;EAC3B,MAAM,SAAS,IAAI,iBAAiB;AACpC,SAAO,IAAI,SAAS,OAAO,MAAM;AACjC,MAAI,OAAO,iBAAkB,QAAO,IAAI,qBAAqB,OAAO,iBAAiB;AACrF,SAAO,aAAa,SAAS,IAAI,IAAI,GAAG,QAAQ,IAAI,OAAO,UAAU,IAAI,QAAQ,IAAI,CAAC;;AAGxF,KAAI,OAAO,SAAS,WAAW,CAAC,eAAe,CAAC,YAAY,OAAO,UAAU,YAC3E,QAAO,aAAa,SAAS,IAAI,IAAI,GAAG,QAAQ,UAAU,wBAAwB,QAAQ,IAAI,CAAC;CAGjG,MAAM,EAAE,SAAS;CAEjB,MAAM,cAAc,MAAM,qBAAqB,MAAM;EACnD;EACA,cAAc;EACd,aAAa;EACd,CAAC;AAEF,KAAI,YAAY,OAAO,EAAE;AACvB,UAAQ,MAAM,GAAG,WAAW,0BAA0B,YAAY,MAAM;AACxE,SAAO,aAAa,SAClB,IAAI,IAAI,GAAG,QAAQ,UAAU,+BAA+B,QAAQ,IAAI,CACzE;;CAGH,MAAM,QAAQ,YAAY;CAC1B,MAAM,OAAO,MAAM,iBAAiB,MAAM,cAAc,KAAK,WAAW;AAExE,KAAI,CAAC,KACH,QAAO,aAAa,SAClB,IAAI,IAAI,GAAG,QAAQ,UAAU,6BAA6B,QAAQ,IAAI,CACvE;CAGH,MAAM,iBAAiC;EACrC,aAAa,MAAM;EACnB,cAAc,MAAM;EACpB,WAAW,MAAM;EACjB,OAAO,MAAM;EACb,SAAS,MAAM;EACf,WAAW,KAAK,KAAK,GAAG,MAAM,aAAa;EAC3C,UAAU,KAAK;EACf,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,WAAW,KAAK;EAChB,WAAW,KAAK;EACjB;CAED,MAAM,MAAM,MAAM,iBAAiB,KAAK,eAAe,eAAe;AAEtE,QAAO,SAAS,SAAS,SAAS,KAAK,QAAQ;;;;;;AAOjD,SAAgBA,qBAAmB,MAEjC;AACA,QAAO,EACL,MAAM,IAAI,SAAyC;EACjD,MAAM,EAAE,KAAK,cAAc,UAAU,MAAM,uBAAuB,KAAK;EAEvE,MAAM,cAAc,MAAM,SAAS;EAEnC,MAAM,SADS,IAAI,IAAI,QAAQ,IAAI,CACb,aAAa;AAEnC,cAAY,IAAI,oBAAoB,OAAO;GACzC,UAAU;GACV,MAAM;GACN,UAAA;GACA;GACA,QAAQ;GACT,CAAC;AACF,cAAY,IAAI,uBAAuB,cAAc;GACnD,UAAU;GACV,MAAM;GACN,UAAA;GACA;GACA,QAAQ;GACT,CAAC;AAEF,SAAO,aAAa,SAAS,IAAI;IAEpC;;;AAIH,SAAgB,mBAAmB,SAA2C;CAC5E,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI;CAChC,MAAM,QAAQ,IAAI,aAAa,IAAI,QAAQ;AAC3C,KAAI,MACF,QAAO,QAAQ,QAAQ;EACrB,MAAM;EACN;EACA,kBAAkB,IAAI,aAAa,IAAI,oBAAoB;EAC5D,CAAC;CAEJ,MAAM,OAAO,IAAI,aAAa,IAAI,OAAO;CACzC,MAAM,QAAQ,IAAI,aAAa,IAAI,QAAQ;AAC3C,KAAI,QAAQ,MACV,QAAO,QAAQ,QAAQ;EAAE,MAAM;EAAW;EAAM;EAAO,CAAC;AAE1D,QAAO,QAAQ,QAAQ,EAAE,MAAM,SAAS,CAAC;;;AAI3C,eAAsB,kBACpB,SACA,eACyB;CACzB,MAAM,OAAO,MAAM,QAAQ,UAAU;CACrC,MAAM,QAAQ,cAAc,MAAM,QAAQ;AAC1C,KAAI,MACF,QAAO;EACL,MAAM;EACN;EACA,kBAAkB,cAAc,MAAM,oBAAoB;EAC3D;CAEH,MAAM,OAAO,cAAc,MAAM,OAAO;CACxC,MAAM,QAAQ,cAAc,MAAM,QAAQ;AAC1C,KAAI,QAAQ,MACV,QAAO;EAAE,MAAM;EAAW;EAAM;EAAO;AAEzC,QAAO,EAAE,MAAM,SAAS;;;;ACtK1B,MAAM,6BAA6B;;;;;;;;AASnC,eAAsB,WACpB,QACA,aAA4B,MACI;CAEhC,MAAM,SADc,MAAM,SAAS,EACT,IAAI,cAAA,iBAA0C,EAAE;AAC1E,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,SAAS,MAAM,iBAAiB,QAAQ,MAAM;AACpD,KAAI,OAAO,OAAO,EAAE;AAClB,UAAQ,KAAK,GAAG,WAAW,0BAA0B,OAAO,MAAM;AAClE,SAAO;;AAET,QAAO,OAAO;;;;;;;;;AAUhB,eAAsB,WACpB,QACA,YACA,SACe;CACf,MAAM,MAAM,MAAM,iBAAiB,QAAQ,QAAQ;AAEnD,EADoB,MAAM,SAAS,EACvB,IAAI,YAAY,KAAK;EAC/B,UAAU;EACV,QAAQ,QAAQ,IAAI,gBAAgB;EACpC,UAAA;EACA,MAAM;EACN,QAAQ;EACT,CAAC;;;;;;;;;;;;AAuBJ,eAAsB,uBAAuB,MAIZ;CAC/B,MAAM,aAAa,KAAK,qBAAA;CACxB,IAAI,UAAU,MAAM,WAAW,KAAK,eAAe,WAAW;AAC9D,KAAI,CAAC,QAAS,QAAO;AAErB,KAAI,KAAK,KAAK,IAAI,QAAQ,YAAY,8BAA8B,QAAQ,cAAc;EACxF,MAAM,gBAAgB,MAAM,mBAAmB,KAAK,QAAQ,QAAQ,aAAa;AAEjF,MAAI,cAAc,OAAO,EAAE;AACzB,WAAQ,KAAK,GAAG,WAAW,yBAAyB,cAAc,MAAM;AACxE,UAAO;;EAGT,MAAM,YAAY,cAAc;AAIhC,YAAU;GACR,GAAG;GACH,aAAa,UAAU;GACvB,cAAc,UAAU,iBAAiB,QAAQ;GACjD,WAAW,UAAU,cAAc,QAAQ;GAC3C,OAAO,UAAU,SAAS,QAAQ;GAClC,SAAS,UAAU,YAAY,QAAQ;GACvC,WAAW,KAAK,KAAK,GAAG,UAAU,aAAa;GAChD;AACD,MAAI;AACF,SAAM,WAAW,KAAK,eAAe,YAAY,QAAQ;WAClD,OAAO;AACd,WAAQ,KAAK,GAAG,WAAW,wCAAwC,MAAM;;;AAI7E,QAAO,mBAAmB,QAAQ,aAAa,KAAK,OAAO,WAAW;;;;;;;;;;ACzFxE,SAAgB,mBAAmB,MAEjC;AACA,QAAOC,qBAAyB,KAAK;;;;;;;;;;;AAYvC,SAAgB,sBAAsB,MAGpC;CACA,MAAM,WAA6B,EACjC,SAAS,SAAkB,KAAa,SAA+B;EACrE,MAAM,MAAM,aAAa,SACvB,IAAI,IAAI,GAAG,UAAU,KAAK,kBAAkB,OAAO,QAAQ,IAAI,CAChE;AACD,MAAI,QAAQ,IAAI,KAAK,mBAAmB,KAAK;GAC3C,UAAU;GACV,QAAQ,IAAI,IAAI,QAAQ,IAAI,CAAC,aAAa;GAC1C,UAAA;GACA,MAAM;GACN,QAAQ;GACT,CAAC;AACF,SAAO;IAEV;AAED,QAAO;EACL,MAAM,IAAI,SAAyC;AACjD,UAAO,oBAAoB,SAAS,MAAM,oBAAoB,SAAS;;EAEzE,MAAM,KAAK,SAAyC;AAClD,UAAO,oBACL,SACA,OACC,QAAQ,kBAAkB,KAAK,cAAc,EAC9C,SACD;;EAEJ;;;;;;;;;AAUH,SAAgB,oBAAoB,MAElC;AACA,QAAO,EACL,KAAK,SAAgC;EACnC,MAAM,UAAU,WAAW,MAAM,QAAQ,IAAI;EAC7C,MAAM,MAAM,aAAa,SACvB,IAAI,IAAI,GAAG,UAAU,KAAK,mBAAmB,OAAO,QAAQ,IAAI,CACjE;AACD,MAAI,QAAQ,OAAO,KAAK,kBAAkB;AAC1C,SAAO;IAEV"}
1
+ {"version":3,"file":"index.js","names":["createLoginHandler","createSharedLoginHandler"],"sources":["../../../src/nextjs/callback.ts","../../../src/nextjs/off-platform/session.ts","../../../src/nextjs/off-platform/index.ts"],"sourcesContent":["import { cookies } from 'next/headers';\nimport { NextResponse } from 'next/server';\n\nimport {\n createAuthorizationUrl,\n exchangeCodeForToken,\n createSessionJwt,\n type SessionPayload,\n} from '../core/index.js';\n\nimport { LOG_PREFIX, COOKIE_SAME_SITE } from './constants.js';\nimport { fetchUserProfile, getBaseUrl } from './utils.js';\n\nimport type { ResolvedConfig } from './index.js';\n\nconst COOKIE_OAUTH_STATE = 'oauth_state';\nconst COOKIE_OAUTH_VERIFIER = 'oauth_verifier';\nconst OAUTH_COOKIE_MAX_AGE_SECONDS = 600;\nconst ERROR_STATE_MISMATCH = 'oauth_state_mismatch';\nconst ERROR_TOKEN_EXCHANGE_FAILED = 'oauth_token_exchange_failed';\nconst ERROR_USER_PROFILE_FAILED = 'oauth_user_profile_failed';\n\n/** Parameters extracted from the OAuth callback request. */\nexport type CallbackParams =\n | { kind: 'success'; code: string; state: string }\n | { kind: 'error'; error: string; errorDescription: string | null }\n | { kind: 'empty' };\n\n/** Strategy for completing the OAuth callback — how the JWT is delivered to the client. */\nexport interface CallbackStrategy {\n complete(request: Request, jwt: string, baseUrl: string): NextResponse;\n}\n\n/** Options required by the shared callback handler. */\nexport type CallbackHandlerOptions = ResolvedConfig;\n\n/**\n * Shared callback handler that validates the OAuth state, exchanges the\n * authorization code for tokens, fetches the user profile, creates a session\n * JWT, and delegates to the strategy for final response.\n */\nexport async function handleOAuthCallback(\n request: Request,\n opts: CallbackHandlerOptions,\n getParams: (request: Request) => Promise<CallbackParams>,\n strategy: CallbackStrategy,\n): Promise<NextResponse> {\n const baseUrl = getBaseUrl(opts, request.url);\n const params = await getParams(request);\n\n const cookieStore = await cookies();\n const storedState = cookieStore.get(COOKIE_OAUTH_STATE)?.value;\n const verifier = cookieStore.get(COOKIE_OAUTH_VERIFIER)?.value;\n\n cookieStore.delete(COOKIE_OAUTH_STATE);\n cookieStore.delete(COOKIE_OAUTH_VERIFIER);\n\n if (params.kind === 'error') {\n const search = new URLSearchParams();\n search.set('error', params.error);\n if (params.errorDescription) search.set('error_description', params.errorDescription);\n return NextResponse.redirect(new URL(`${baseUrl}/?${search.toString()}`, request.url));\n }\n\n if (params.kind === 'empty' || !storedState || !verifier || params.state !== storedState) {\n return NextResponse.redirect(new URL(`${baseUrl}/?error=${ERROR_STATE_MISMATCH}`, request.url));\n }\n\n const { code } = params;\n\n const tokenResult = await exchangeCodeForToken(opts, {\n code,\n codeVerifier: verifier,\n redirectUri: null,\n });\n\n if (tokenResult.isErr()) {\n console.error(`${LOG_PREFIX} token exchange failed:`, tokenResult.error);\n return NextResponse.redirect(\n new URL(`${baseUrl}/?error=${ERROR_TOKEN_EXCHANGE_FAILED}`, request.url),\n );\n }\n\n const token = tokenResult.value;\n const user = await fetchUserProfile(token.access_token, opts.apiBaseUrl);\n\n if (!user) {\n return NextResponse.redirect(\n new URL(`${baseUrl}/?error=${ERROR_USER_PROFILE_FAILED}`, request.url),\n );\n }\n\n const sessionPayload: SessionPayload = {\n accessToken: token.access_token,\n refreshToken: token.refresh_token,\n tokenType: token.token_type,\n scope: token.scope,\n idToken: token.id_token,\n expiresAt: Date.now() + token.expires_in * 1000,\n userUuid: user.uuid,\n handle: user.handle,\n displayName: user.displayName,\n isCreator: user.isCreator,\n avatarUrl: user.avatarUrl,\n };\n\n const jwt = await createSessionJwt(opts.sessionSecret, sessionPayload);\n\n return strategy.complete(request, jwt, baseUrl);\n}\n\n/**\n * Creates a Next.js route handler that initiates the OAuth login flow.\n * Used by the off-platform module.\n */\nexport function createLoginHandler(opts: CallbackHandlerOptions): {\n GET(request: Request): Promise<NextResponse>;\n} {\n return {\n async GET(request: Request): Promise<NextResponse> {\n const { url, codeVerifier, state } = await createAuthorizationUrl(opts);\n\n const cookieStore = await cookies();\n const reqUrl = new URL(request.url);\n const secure = reqUrl.protocol === 'https:';\n\n cookieStore.set(COOKIE_OAUTH_STATE, state, {\n httpOnly: true,\n path: '/',\n sameSite: COOKIE_SAME_SITE,\n secure,\n maxAge: OAUTH_COOKIE_MAX_AGE_SECONDS,\n });\n cookieStore.set(COOKIE_OAUTH_VERIFIER, codeVerifier, {\n httpOnly: true,\n path: '/',\n sameSite: COOKIE_SAME_SITE,\n secure,\n maxAge: OAUTH_COOKIE_MAX_AGE_SECONDS,\n });\n\n return NextResponse.redirect(url);\n },\n };\n}\n\n/** Extracts callback params from a GET request's query string. */\nexport function getParamsFromQuery(request: Request): Promise<CallbackParams> {\n const url = new URL(request.url);\n const error = url.searchParams.get('error');\n if (error) {\n return Promise.resolve({\n kind: 'error',\n error,\n errorDescription: url.searchParams.get('error_description'),\n });\n }\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n if (code && state) {\n return Promise.resolve({ kind: 'success', code, state });\n }\n return Promise.resolve({ kind: 'empty' });\n}\n\n/** Extracts callback params from a POST request's form body. */\nexport async function getParamsFromForm(\n request: Request,\n getFormString: (form: FormData, key: string) => string | null,\n): Promise<CallbackParams> {\n const form = await request.formData();\n const error = getFormString(form, 'error');\n if (error) {\n return {\n kind: 'error',\n error,\n errorDescription: getFormString(form, 'error_description'),\n };\n }\n const code = getFormString(form, 'code');\n const state = getFormString(form, 'state');\n if (code && state) {\n return { kind: 'success', code, state };\n }\n return { kind: 'empty' };\n}\n","import { cookies } from 'next/headers';\n\nimport {\n verifySessionJwt,\n createSessionJwt,\n createFanvueClient,\n refreshAccessToken,\n type FanvueClient,\n type OAuthConfig,\n type SessionPayload,\n} from '../../core/index.js';\nimport {\n DEFAULT_SESSION_COOKIE_NAME,\n SESSION_COOKIE_MAX_AGE_SECONDS,\n COOKIE_SAME_SITE,\n LOG_PREFIX,\n} from '../constants.js';\n\nconst TOKEN_REFRESH_THRESHOLD_MS = 30_000;\n\n/**\n * Reads and verifies the session JWT from the cookie store.\n *\n * @param secret - The secret used to verify the JWT.\n * @param cookieName - The cookie name to read. Defaults to {@link DEFAULT_SESSION_COOKIE_NAME}.\n * @returns The verified {@link SessionPayload}, or `null` if missing/invalid.\n */\nexport async function getSession(\n secret: string,\n cookieName: string | null = null,\n): Promise<SessionPayload | null> {\n const cookieStore = await cookies();\n const token = cookieStore.get(cookieName ?? DEFAULT_SESSION_COOKIE_NAME)?.value;\n if (!token) return null;\n const result = await verifySessionJwt(secret, token);\n if (result.isErr()) {\n console.warn(`${LOG_PREFIX} session verify failed:`, result.error);\n return null;\n }\n return result.value;\n}\n\n/**\n * Creates or updates the session cookie with a fresh JWT.\n *\n * @param secret - The secret used to sign the JWT.\n * @param cookieName - The cookie name to set.\n * @param payload - The session data to encode into the JWT.\n */\nexport async function setSession(\n secret: string,\n cookieName: string,\n payload: SessionPayload,\n): Promise<void> {\n const jwt = await createSessionJwt(secret, payload);\n const cookieStore = await cookies();\n cookieStore.set(cookieName, jwt, {\n httpOnly: true,\n secure: process.env['NODE_ENV'] === 'production',\n sameSite: COOKIE_SAME_SITE,\n path: '/',\n maxAge: SESSION_COOKIE_MAX_AGE_SECONDS,\n });\n}\n\n/**\n * Deletes the session cookie.\n *\n * @param cookieName - The cookie name to delete.\n */\nexport async function clearSession(cookieName: string): Promise<void> {\n const cookieStore = await cookies();\n cookieStore.delete(cookieName);\n}\n\n/**\n * Returns an authenticated {@link FanvueClient} for the current session.\n *\n * If the access token is close to expiring and a refresh token is available,\n * the token is refreshed transparently and the session cookie is updated.\n *\n * @param opts - Session secret, cookie name, and OAuth config.\n * @returns A {@link FanvueClient} instance, or `null` if no valid session exists\n * or token refresh fails.\n */\nexport async function getAuthenticatedClient(opts: {\n sessionSecret: string;\n sessionCookieName: string | null;\n config: OAuthConfig;\n}): Promise<FanvueClient | null> {\n const cookieName = opts.sessionCookieName ?? DEFAULT_SESSION_COOKIE_NAME;\n let session = await getSession(opts.sessionSecret, cookieName);\n if (!session) return null;\n\n if (Date.now() >= session.expiresAt - TOKEN_REFRESH_THRESHOLD_MS && session.refreshToken) {\n const refreshResult = await refreshAccessToken(opts.config, session.refreshToken);\n\n if (refreshResult.isErr()) {\n console.warn(`${LOG_PREFIX} token refresh failed:`, refreshResult.error);\n return null;\n }\n\n const refreshed = refreshResult.value;\n // When the refresh response omits optional fields (parsed as null by the schema),\n // preserve the existing session values. Per RFC 6749 §6, a refresh response MAY\n // include a new refresh_token; when absent, the existing one remains valid.\n session = {\n ...session,\n accessToken: refreshed.access_token,\n refreshToken: refreshed.refresh_token ?? session.refreshToken,\n tokenType: refreshed.token_type ?? session.tokenType,\n scope: refreshed.scope ?? session.scope,\n idToken: refreshed.id_token ?? session.idToken,\n expiresAt: Date.now() + refreshed.expires_in * 1000,\n };\n try {\n await setSession(opts.sessionSecret, cookieName, session);\n } catch (error) {\n console.warn(`${LOG_PREFIX} failed to persist refreshed session:`, error);\n }\n }\n\n return createFanvueClient(session.accessToken, opts.config.apiBaseUrl);\n}\n","import { NextResponse } from 'next/server';\n\nimport {\n handleOAuthCallback,\n createLoginHandler as createSharedLoginHandler,\n getParamsFromQuery,\n getParamsFromForm,\n} from '../callback.js';\nimport { COOKIE_SAME_SITE, SESSION_COOKIE_MAX_AGE_SECONDS } from '../constants.js';\nimport { type ResolvedConfig } from '../index.js';\nimport { getBaseUrl, getFormString } from '../utils.js';\n\nimport type { CallbackStrategy } from '../callback.js';\n\nexport { getSession, getAuthenticatedClient } from './session.js';\nexport { createConfig, type FanvueAuthOptions, type ResolvedConfig } from '../index.js';\nexport { createConfigSafe, type CreateConfigResult } from '../index.js';\n\n/**\n * Configuration for the off-platform OAuth handlers.\n *\n * Extends {@link ResolvedConfig} with optional routing overrides.\n */\nexport type OffPlatformOptions = ResolvedConfig & {\n afterLoginPath: string | null;\n afterLogoutPath: string | null;\n};\n\n/**\n * Creates a Next.js route handler that initiates the OAuth login flow.\n *\n * @param opts - The off-platform configuration.\n * @returns A route handler object with a `GET` method.\n */\nexport function createLoginHandler(opts: OffPlatformOptions): {\n GET(request: Request): Promise<NextResponse>;\n} {\n return createSharedLoginHandler(opts);\n}\n\n/**\n * Creates a Next.js route handler that processes the OAuth callback.\n *\n * Supports both `GET` (query-string params) and `POST` (form-encoded body)\n * response modes.\n *\n * @param opts - The off-platform configuration.\n * @returns A route handler object with `GET` and `POST` methods.\n */\nexport function createCallbackHandler(opts: OffPlatformOptions): {\n GET(request: Request): Promise<NextResponse>;\n POST(request: Request): Promise<NextResponse>;\n} {\n const strategy: CallbackStrategy = {\n complete(request: Request, jwt: string, baseUrl: string): NextResponse {\n const res = NextResponse.redirect(\n new URL(`${baseUrl}${opts.afterLoginPath ?? '/'}`, request.url),\n );\n res.cookies.set(opts.sessionCookieName, jwt, {\n httpOnly: true,\n secure: new URL(request.url).protocol === 'https:',\n sameSite: COOKIE_SAME_SITE,\n path: '/',\n maxAge: SESSION_COOKIE_MAX_AGE_SECONDS,\n });\n return res;\n },\n };\n\n return {\n async GET(request: Request): Promise<NextResponse> {\n return handleOAuthCallback(request, opts, getParamsFromQuery, strategy);\n },\n async POST(request: Request): Promise<NextResponse> {\n return handleOAuthCallback(\n request,\n opts,\n (req) => getParamsFromForm(req, getFormString),\n strategy,\n );\n },\n };\n}\n\n/**\n * Creates a Next.js route handler that logs the user out by deleting the\n * session cookie and redirecting.\n *\n * @param opts - The off-platform configuration.\n * @returns A route handler object with a `POST` method.\n */\nexport function createLogoutHandler(opts: OffPlatformOptions): {\n POST(request: Request): NextResponse;\n} {\n return {\n POST(request: Request): NextResponse {\n const baseUrl = getBaseUrl(opts, request.url);\n const res = NextResponse.redirect(\n new URL(`${baseUrl}${opts.afterLogoutPath ?? '/'}`, request.url),\n );\n res.cookies.delete(opts.sessionCookieName);\n return res;\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,144 @@
1
+ import { f as createFanvueClient, mt as assertFanvueDomain } from "./core-BhKiA55a.js";
2
+ //#region src/nextjs/constants.ts
3
+ /** Default name for the session cookie. */
4
+ const DEFAULT_SESSION_COOKIE_NAME = "fanvue_session";
5
+ /** Session cookie max-age in seconds (30 days). */
6
+ const SESSION_COOKIE_MAX_AGE_SECONDS = 3600 * 24 * 30;
7
+ /** Log prefix for all library log messages. */
8
+ const LOG_PREFIX = "[fanvue/auth]";
9
+ //#endregion
10
+ //#region src/nextjs/utils.ts
11
+ /**
12
+ * Fetches the authenticated user's profile from the Fanvue API.
13
+ *
14
+ * @returns The user profile on success, or `null` on failure.
15
+ */
16
+ async function fetchUserProfile(accessToken, apiBaseUrl) {
17
+ const result = await createFanvueClient(accessToken, apiBaseUrl).getCurrentUser();
18
+ if (result.isErr()) {
19
+ console.warn(`${LOG_PREFIX} user profile fetch failed:`, result.error);
20
+ return null;
21
+ }
22
+ return result.value;
23
+ }
24
+ /**
25
+ * Extracts a string value from `FormData`, returning `null` when the value
26
+ * is missing or is not a string (e.g. a `File`).
27
+ */
28
+ function getFormString(form, key) {
29
+ const value = form.get(key);
30
+ return typeof value === "string" ? value : null;
31
+ }
32
+ /**
33
+ * Derives the application base URL from the options or falls back to the
34
+ * request's origin.
35
+ */
36
+ function getBaseUrl(opts, requestUrl) {
37
+ return opts.baseUrl ?? new URL(requestUrl).origin;
38
+ }
39
+ //#endregion
40
+ //#region src/nextjs/index.ts
41
+ /**
42
+ * Resolves configuration from options with environment-variable fallbacks,
43
+ * reporting what is missing instead of throwing.
44
+ *
45
+ * Shared by {@link createConfig} and {@link createConfigSafe} so the two can
46
+ * never drift. The check order — required values first, then the API base-URL
47
+ * domain — is part of `createConfig`'s existing contract.
48
+ */
49
+ function resolveConfig(opts) {
50
+ const clientId = opts?.clientId ?? process.env["OAUTH_CLIENT_ID"];
51
+ const clientSecret = opts?.clientSecret ?? process.env["OAUTH_CLIENT_SECRET"];
52
+ const redirectUri = opts?.redirectUri ?? process.env["OAUTH_REDIRECT_URI"];
53
+ const sessionSecret = opts?.sessionSecret ?? process.env["SESSION_SECRET"];
54
+ const missing = [];
55
+ if (!clientId) missing.push("OAUTH_CLIENT_ID");
56
+ if (!clientSecret) missing.push("OAUTH_CLIENT_SECRET");
57
+ if (!redirectUri) missing.push("OAUTH_REDIRECT_URI");
58
+ if (!sessionSecret) missing.push("SESSION_SECRET");
59
+ if (!clientId || !clientSecret || !redirectUri || !sessionSecret) return {
60
+ ok: false,
61
+ missing,
62
+ invalidApiBaseUrl: null
63
+ };
64
+ const apiBaseUrl = opts?.apiBaseUrl ?? process.env["API_BASE_URL"];
65
+ if (apiBaseUrl != null) try {
66
+ assertFanvueDomain(apiBaseUrl);
67
+ } catch {
68
+ return {
69
+ ok: false,
70
+ missing: ["API_BASE_URL"],
71
+ invalidApiBaseUrl: apiBaseUrl
72
+ };
73
+ }
74
+ const issuerUrl = opts?.issuerUrl ?? process.env["OAUTH_ISSUER_BASE_URL"];
75
+ const scopes = opts?.scopes ?? process.env["OAUTH_SCOPES"];
76
+ const responseMode = opts?.responseMode ?? process.env["OAUTH_RESPONSE_MODE"];
77
+ const prompt = opts?.prompt ?? process.env["OAUTH_PROMPT"];
78
+ const baseUrl = opts?.baseUrl ?? process.env["BASE_URL"];
79
+ return {
80
+ ok: true,
81
+ config: {
82
+ clientId,
83
+ clientSecret,
84
+ redirectUri,
85
+ issuerUrl: issuerUrl ?? null,
86
+ apiBaseUrl: apiBaseUrl ?? null,
87
+ scopes: scopes ?? null,
88
+ responseMode: responseMode ?? null,
89
+ prompt: prompt ?? null,
90
+ baseUrl: baseUrl ?? null,
91
+ sessionSecret,
92
+ sessionCookieName: opts?.sessionCookieName ?? process.env["SESSION_COOKIE_NAME"] ?? "fanvue_session"
93
+ }
94
+ };
95
+ }
96
+ /**
97
+ * Resolves a complete {@link ResolvedConfig} from the supplied options,
98
+ * falling back to environment variables for any values not provided.
99
+ *
100
+ * @param opts - Optional overrides for the OAuth and session configuration.
101
+ * @returns A fully-resolved configuration object.
102
+ * @throws If any required value (`clientId`, `clientSecret`, `redirectUri`,
103
+ * `sessionSecret`) is missing from both `opts` and the environment.
104
+ */
105
+ function createConfig(opts) {
106
+ const result = resolveConfig(opts);
107
+ if (!result.ok) {
108
+ if (result.invalidApiBaseUrl !== null) assertFanvueDomain(result.invalidApiBaseUrl);
109
+ throw new Error("Missing required OAuth configuration. Provide via options or env vars: OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, SESSION_SECRET");
110
+ }
111
+ return result.config;
112
+ }
113
+ /**
114
+ * Non-throwing variant of {@link createConfig}.
115
+ *
116
+ * A route that calls the throwing form on an under-configured deployment turns a
117
+ * missing environment variable into an unhandled 500 with a stack trace, which
118
+ * is why every app grew its own `isFanvueConfigured()` pre-check. This returns
119
+ * the same configuration as a discriminated result instead, so a handler can
120
+ * answer with a deliberate "not configured" response.
121
+ *
122
+ * @param opts - Optional overrides for the OAuth and session configuration.
123
+ * @returns `{ ok: true, config }`, or `{ ok: false, missing }` naming the
124
+ * environment variables that are unset (or, for `API_BASE_URL`, set to a
125
+ * non-Fanvue domain).
126
+ * @example
127
+ * const result = createConfigSafe();
128
+ * if (!result.ok) {
129
+ * console.error('Fanvue OAuth not configured:', result.missing.join(', '));
130
+ * return new Response(null, { status: 503 });
131
+ * }
132
+ * const handler = createLoginHandler({ ...result.config, afterLoginPath: null, afterLogoutPath: null });
133
+ */
134
+ function createConfigSafe(opts) {
135
+ const result = resolveConfig(opts);
136
+ return result.ok ? result : {
137
+ ok: false,
138
+ missing: result.missing
139
+ };
140
+ }
141
+ //#endregion
142
+ export { getFormString as a, SESSION_COOKIE_MAX_AGE_SECONDS as c, getBaseUrl as i, createConfigSafe as n, DEFAULT_SESSION_COOKIE_NAME as o, fetchUserProfile as r, LOG_PREFIX as s, createConfig as t };
143
+
144
+ //# sourceMappingURL=nextjs-CvVhtMdb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nextjs-CvVhtMdb.js","names":[],"sources":["../src/nextjs/constants.ts","../src/nextjs/utils.ts","../src/nextjs/index.ts"],"sourcesContent":["/** Default name for the session cookie. */\nexport const DEFAULT_SESSION_COOKIE_NAME = 'fanvue_session' as const;\n\n/** Session cookie max-age in seconds (30 days). */\nexport const SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;\n\n/** SameSite attribute for cookies. */\nexport const COOKIE_SAME_SITE = 'lax' as const;\n\n/** Log prefix for all library log messages. */\nexport const LOG_PREFIX = '[fanvue/auth]' as const;\n","import { createFanvueClient } from '../core/index.js';\n\nimport { LOG_PREFIX } from './constants.js';\n\nimport type { FanvueUser } from '../core/index.js';\n\n/**\n * Fetches the authenticated user's profile from the Fanvue API.\n *\n * @returns The user profile on success, or `null` on failure.\n */\nexport async function fetchUserProfile(\n accessToken: string,\n apiBaseUrl: string | null,\n): Promise<FanvueUser | null> {\n const client = createFanvueClient(accessToken, apiBaseUrl);\n const result = await client.getCurrentUser();\n if (result.isErr()) {\n console.warn(`${LOG_PREFIX} user profile fetch failed:`, result.error);\n return null;\n }\n return result.value;\n}\n\n/**\n * Extracts a string value from `FormData`, returning `null` when the value\n * is missing or is not a string (e.g. a `File`).\n */\nexport function getFormString(form: FormData, key: string): string | null {\n const value = form.get(key);\n return typeof value === 'string' ? value : null;\n}\n\n/**\n * Derives the application base URL from the options or falls back to the\n * request's origin.\n */\nexport function getBaseUrl(opts: { baseUrl: string | null }, requestUrl: string): string {\n return opts.baseUrl ?? new URL(requestUrl).origin;\n}\n","import { assertFanvueDomain } from '../core/index.js';\n\nimport { DEFAULT_SESSION_COOKIE_NAME } from './constants.js';\n\nimport type { OAuthConfig } from '../core/index.js';\n\n/**\n * User-facing configuration options for Fanvue OAuth.\n *\n * All fields are optional — when omitted, values are read from the\n * corresponding environment variables at runtime. This type uses optional\n * properties (`?`) rather than nullable fields because every value has an\n * env-var fallback, so `undefined` genuinely means \"use the default.\"\n */\nexport type FanvueAuthOptions = {\n clientId?: string;\n clientSecret?: string;\n redirectUri?: string;\n issuerUrl?: string;\n apiBaseUrl?: string;\n scopes?: string;\n sessionSecret?: string;\n sessionCookieName?: string;\n responseMode?: string;\n prompt?: string;\n baseUrl?: string;\n};\n\n/**\n * Fully-resolved configuration produced by {@link createConfig}.\n *\n * Extends the core {@link OAuthConfig} with session-specific settings that\n * are guaranteed to be present.\n */\nexport type ResolvedConfig = OAuthConfig & {\n sessionSecret: string;\n sessionCookieName: string;\n baseUrl: string | null;\n};\n\n/**\n * The outcome of a non-throwing configuration resolution.\n *\n * `missing` names the environment variables (never the option keys) that would\n * have to be set for resolution to succeed, so the message can be shown to\n * whoever deploys the app.\n */\nexport type CreateConfigResult =\n | { ok: true; config: ResolvedConfig }\n | { ok: false; missing: string[] };\n\n/**\n * Internal resolution outcome.\n *\n * The failure arm names the offending API base URL separately so\n * {@link createConfig} can raise the exact domain error it always has, while\n * {@link createConfigSafe} reports the same condition as a missing variable.\n */\ntype ResolveOutcome =\n | { ok: true; config: ResolvedConfig }\n | { ok: false; missing: string[]; invalidApiBaseUrl: string | null };\n\n/**\n * Resolves configuration from options with environment-variable fallbacks,\n * reporting what is missing instead of throwing.\n *\n * Shared by {@link createConfig} and {@link createConfigSafe} so the two can\n * never drift. The check order — required values first, then the API base-URL\n * domain — is part of `createConfig`'s existing contract.\n */\nfunction resolveConfig(opts?: FanvueAuthOptions): ResolveOutcome {\n const clientId = opts?.clientId ?? process.env['OAUTH_CLIENT_ID'];\n const clientSecret = opts?.clientSecret ?? process.env['OAUTH_CLIENT_SECRET'];\n const redirectUri = opts?.redirectUri ?? process.env['OAUTH_REDIRECT_URI'];\n const sessionSecret = opts?.sessionSecret ?? process.env['SESSION_SECRET'];\n\n const missing: string[] = [];\n if (!clientId) missing.push('OAUTH_CLIENT_ID');\n if (!clientSecret) missing.push('OAUTH_CLIENT_SECRET');\n if (!redirectUri) missing.push('OAUTH_REDIRECT_URI');\n if (!sessionSecret) missing.push('SESSION_SECRET');\n\n // The truthiness checks are repeated rather than tested through\n // `missing.length` so the compiler narrows the four required values to\n // `string` without an assertion.\n if (!clientId || !clientSecret || !redirectUri || !sessionSecret) {\n return { ok: false, missing, invalidApiBaseUrl: null };\n }\n\n const apiBaseUrl = opts?.apiBaseUrl ?? process.env['API_BASE_URL'];\n if (apiBaseUrl != null) {\n // An off-domain API base URL is unusable rather than merely absent, but it\n // is the same class of deployment mistake and belongs in the same report.\n try {\n assertFanvueDomain(apiBaseUrl);\n } catch {\n return { ok: false, missing: ['API_BASE_URL'], invalidApiBaseUrl: apiBaseUrl };\n }\n }\n\n const issuerUrl = opts?.issuerUrl ?? process.env['OAUTH_ISSUER_BASE_URL'];\n const scopes = opts?.scopes ?? process.env['OAUTH_SCOPES'];\n const responseMode = opts?.responseMode ?? process.env['OAUTH_RESPONSE_MODE'];\n const prompt = opts?.prompt ?? process.env['OAUTH_PROMPT'];\n const baseUrl = opts?.baseUrl ?? process.env['BASE_URL'];\n\n return {\n ok: true,\n config: {\n clientId,\n clientSecret,\n redirectUri,\n issuerUrl: issuerUrl ?? null,\n apiBaseUrl: apiBaseUrl ?? null,\n scopes: scopes ?? null,\n responseMode: responseMode ?? null,\n prompt: prompt ?? null,\n baseUrl: baseUrl ?? null,\n sessionSecret,\n sessionCookieName:\n opts?.sessionCookieName ??\n process.env['SESSION_COOKIE_NAME'] ??\n DEFAULT_SESSION_COOKIE_NAME,\n },\n };\n}\n\n/**\n * Resolves a complete {@link ResolvedConfig} from the supplied options,\n * falling back to environment variables for any values not provided.\n *\n * @param opts - Optional overrides for the OAuth and session configuration.\n * @returns A fully-resolved configuration object.\n * @throws If any required value (`clientId`, `clientSecret`, `redirectUri`,\n * `sessionSecret`) is missing from both `opts` and the environment.\n */\nexport function createConfig(opts?: FanvueAuthOptions): ResolvedConfig {\n const result = resolveConfig(opts);\n if (!result.ok) {\n if (result.invalidApiBaseUrl !== null) {\n // Re-raises the original domain error, message unchanged.\n assertFanvueDomain(result.invalidApiBaseUrl);\n }\n throw new Error(\n 'Missing required OAuth configuration. Provide via options or env vars: OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, SESSION_SECRET',\n );\n }\n return result.config;\n}\n\n/**\n * Non-throwing variant of {@link createConfig}.\n *\n * A route that calls the throwing form on an under-configured deployment turns a\n * missing environment variable into an unhandled 500 with a stack trace, which\n * is why every app grew its own `isFanvueConfigured()` pre-check. This returns\n * the same configuration as a discriminated result instead, so a handler can\n * answer with a deliberate \"not configured\" response.\n *\n * @param opts - Optional overrides for the OAuth and session configuration.\n * @returns `{ ok: true, config }`, or `{ ok: false, missing }` naming the\n * environment variables that are unset (or, for `API_BASE_URL`, set to a\n * non-Fanvue domain).\n * @example\n * const result = createConfigSafe();\n * if (!result.ok) {\n * console.error('Fanvue OAuth not configured:', result.missing.join(', '));\n * return new Response(null, { status: 503 });\n * }\n * const handler = createLoginHandler({ ...result.config, afterLoginPath: null, afterLogoutPath: null });\n */\nexport function createConfigSafe(opts?: FanvueAuthOptions): CreateConfigResult {\n const result = resolveConfig(opts);\n return result.ok ? result : { ok: false, missing: result.missing };\n}\n"],"mappings":";;;AACA,MAAa,8BAA8B;;AAG3C,MAAa,iCAAiC,OAAU,KAAK;;AAM7D,MAAa,aAAa;;;;;;;;ACC1B,eAAsB,iBACpB,aACA,YAC4B;CAE5B,MAAM,SAAS,MADA,mBAAmB,aAAa,WAAW,CAC9B,gBAAgB;AAC5C,KAAI,OAAO,OAAO,EAAE;AAClB,UAAQ,KAAK,GAAG,WAAW,8BAA8B,OAAO,MAAM;AACtE,SAAO;;AAET,QAAO,OAAO;;;;;;AAOhB,SAAgB,cAAc,MAAgB,KAA4B;CACxE,MAAM,QAAQ,KAAK,IAAI,IAAI;AAC3B,QAAO,OAAO,UAAU,WAAW,QAAQ;;;;;;AAO7C,SAAgB,WAAW,MAAkC,YAA4B;AACvF,QAAO,KAAK,WAAW,IAAI,IAAI,WAAW,CAAC;;;;;;;;;;;;ACgC7C,SAAS,cAAc,MAA0C;CAC/D,MAAM,WAAW,MAAM,YAAY,QAAQ,IAAI;CAC/C,MAAM,eAAe,MAAM,gBAAgB,QAAQ,IAAI;CACvD,MAAM,cAAc,MAAM,eAAe,QAAQ,IAAI;CACrD,MAAM,gBAAgB,MAAM,iBAAiB,QAAQ,IAAI;CAEzD,MAAM,UAAoB,EAAE;AAC5B,KAAI,CAAC,SAAU,SAAQ,KAAK,kBAAkB;AAC9C,KAAI,CAAC,aAAc,SAAQ,KAAK,sBAAsB;AACtD,KAAI,CAAC,YAAa,SAAQ,KAAK,qBAAqB;AACpD,KAAI,CAAC,cAAe,SAAQ,KAAK,iBAAiB;AAKlD,KAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC,eAAe,CAAC,cACjD,QAAO;EAAE,IAAI;EAAO;EAAS,mBAAmB;EAAM;CAGxD,MAAM,aAAa,MAAM,cAAc,QAAQ,IAAI;AACnD,KAAI,cAAc,KAGhB,KAAI;AACF,qBAAmB,WAAW;SACxB;AACN,SAAO;GAAE,IAAI;GAAO,SAAS,CAAC,eAAe;GAAE,mBAAmB;GAAY;;CAIlF,MAAM,YAAY,MAAM,aAAa,QAAQ,IAAI;CACjD,MAAM,SAAS,MAAM,UAAU,QAAQ,IAAI;CAC3C,MAAM,eAAe,MAAM,gBAAgB,QAAQ,IAAI;CACvD,MAAM,SAAS,MAAM,UAAU,QAAQ,IAAI;CAC3C,MAAM,UAAU,MAAM,WAAW,QAAQ,IAAI;AAE7C,QAAO;EACL,IAAI;EACJ,QAAQ;GACN;GACA;GACA;GACA,WAAW,aAAa;GACxB,YAAY,cAAc;GAC1B,QAAQ,UAAU;GAClB,cAAc,gBAAgB;GAC9B,QAAQ,UAAU;GAClB,SAAS,WAAW;GACpB;GACA,mBACE,MAAM,qBACN,QAAQ,IAAI,0BAAA;GAEf;EACF;;;;;;;;;;;AAYH,SAAgB,aAAa,MAA0C;CACrE,MAAM,SAAS,cAAc,KAAK;AAClC,KAAI,CAAC,OAAO,IAAI;AACd,MAAI,OAAO,sBAAsB,KAE/B,oBAAmB,OAAO,kBAAkB;AAE9C,QAAM,IAAI,MACR,kJACD;;AAEH,QAAO,OAAO;;;;;;;;;;;;;;;;;;;;;;;AAwBhB,SAAgB,iBAAiB,MAA8C;CAC7E,MAAM,SAAS,cAAc,KAAK;AAClC,QAAO,OAAO,KAAK,SAAS;EAAE,IAAI;EAAO,SAAS,OAAO;EAAS"}
@@ -1,4 +1,5 @@
1
- import { c as FanvueTheme } from "../index-pS9wR5yg.js";
1
+ import { l as AnalyticsProperties } from "../index-CEFYZtlb.js";
2
+ import { j as FanvueTheme } from "../index-C3CXrRiw.js";
2
3
  import { ReactNode } from "react";
3
4
 
4
5
  //#region src/react/auth-context.d.ts
@@ -92,5 +93,39 @@ interface UseEmbeddedAuthResult {
92
93
  */
93
94
  declare function useEmbeddedAuth(opts?: UseEmbeddedAuthOptions): UseEmbeddedAuthResult;
94
95
  //#endregion
95
- export { type AuthContextValue, AuthProvider, type EmbeddedAuthStatus, type FanvueTheme, type UseEmbeddedAuthOptions, type UseEmbeddedAuthResult, useAuth, useEmbeddedAuth };
96
+ //#region src/react/use-fanvue-analytics.d.ts
97
+ /**
98
+ * The value returned by {@link useFanvueAnalytics}.
99
+ *
100
+ * @property track - Fires an event through the host page's Amplitude client.
101
+ * Fire-and-forget: it never throws and no-ops when the app is not embedded in
102
+ * Fanvue or was not granted analytics.
103
+ * @property isEnabled - Whether events are actually reaching Fanvue. `false`
104
+ * until the handshake completes, and permanently `false` outside Fanvue.
105
+ */
106
+ interface UseFanvueAnalyticsResult {
107
+ track: (eventName: string, properties?: AnalyticsProperties) => void;
108
+ isEnabled: boolean;
109
+ }
110
+ /**
111
+ * Fires analytics events through the Fanvue page hosting this app.
112
+ *
113
+ * Events land in Fanvue's Amplitude project namespaced as `embedded_app_<eventName>`,
114
+ * attributed to the app and stitched into the viewer's session — no identity
115
+ * data crosses the frame boundary. Outside Fanvue (local dev, previews, a
116
+ * standalone deployment) `track` silently does nothing, so app code can call it
117
+ * unconditionally.
118
+ *
119
+ * @returns A stable {@link UseFanvueAnalyticsResult.track} and whether events
120
+ * are currently reaching Fanvue.
121
+ *
122
+ * @example
123
+ * function CreateCourseButton() {
124
+ * const { track } = useFanvueAnalytics();
125
+ * return <button onClick={() => track('course_created', { chapters: 4 })}>Create</button>;
126
+ * }
127
+ */
128
+ declare function useFanvueAnalytics(): UseFanvueAnalyticsResult;
129
+ //#endregion
130
+ export { type AuthContextValue, AuthProvider, type EmbeddedAuthStatus, type FanvueTheme, type UseEmbeddedAuthOptions, type UseEmbeddedAuthResult, type UseFanvueAnalyticsResult, useAuth, useEmbeddedAuth, useFanvueAnalytics };
96
131
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/react/auth-context.tsx","../../src/react/use-embedded-auth.ts"],"mappings":";;;;;;;;;;UAuDiB,gBAAA;EACf,GAAA;EACA,eAAA;EACA,MAAA,GAAS,GAAA;EACT,QAAA;EACA,SAAA,GAAY,KAAA,EAAO,WAAA,GAAc,GAAA,EAAK,IAAA,GAAO,WAAA,KAAgB,OAAA,CAAQ,QAAA;AAAA;;;;;;;iBAWvD,YAAA,CAAa,KAAA;EAAS,QAAA,EAAU,SAAA;AAAA,IAAc,SAAA;;;;;;;;iBA6D9C,OAAA,CAAA,GAAW,gBAAA;;;;;;AA7E3B;;;;;;KCpCY,kBAAA;;;;;;;UAQK,sBAAA;EACf,YAAA;AAAA;;;;;;;;;;AD2CF;UC9BiB,qBAAA;EACf,MAAA,EAAQ,kBAAA;EACR,KAAA;EACA,KAAA,EAAO,WAAA;AAAA;;;;;ADwFT;;;;;;;;ACjHA;;;;;AAQA;;;;;iBA0CgB,eAAA,CAAgB,IAAA,GAAO,sBAAA,GAAyB,qBAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/react/auth-context.tsx","../../src/react/use-embedded-auth.ts","../../src/react/use-fanvue-analytics.ts"],"mappings":";;;;;;;;;;;UAuDiB,gBAAA;EACf,GAAA;EACA,eAAA;EACA,MAAA,GAAS,GAAA;EACT,QAAA;EACA,SAAA,GAAY,KAAA,EAAO,WAAA,GAAc,GAAA,EAAK,IAAA,GAAO,WAAA,KAAgB,OAAA,CAAQ,QAAA;AAAA;;;;;;;iBAWvD,YAAA,CAAa,KAAA;EAAS,QAAA,EAAU,SAAA;AAAA,IAAc,SAAA;;;;;;;;iBA6D9C,OAAA,CAAA,GAAW,gBAAA;;;;;;;AA7E3B;;;;;KCpCY,kBAAA;;;;;;;UAQK,sBAAA;EACf,YAAA;AAAA;;;;;;;;;;;UAae,qBAAA;EACf,MAAA,EAAQ,kBAAA;EACR,KAAA;EACA,KAAA,EAAO,WAAA;AAAA;;;;;;ADwFT;;;;;;;;ACjHA;;;;;AAQA;;;;iBA0CgB,eAAA,CAAgB,IAAA,GAAO,sBAAA,GAAyB,qBAAA;;;;;;;ADdhE;;;;;UEhCiB,wBAAA;EACf,KAAA,GAAQ,SAAA,UAAmB,UAAA,GAAa,mBAAA;EACxC,SAAA;AAAA;;;;;;;;;;;;;;;;;AF8CF;;iBEHgB,kBAAA,CAAA,GAAsB,wBAAA"}
@@ -1,5 +1,7 @@
1
1
  "use client";
2
- import { T as HEADER_UPDATED_SESSION, i as getThemeFromUrl, r as getSessionTokenFromUrl, w as BEARER_PREFIX } from "../core-CvVOMyqr.js";
2
+ import "../index.cjs-teGsk6HB.js";
3
+ import { bt as BEARER_PREFIX, h as getThemeFromUrl, m as getSessionTokenFromUrl, xt as HEADER_UPDATED_SESSION } from "../core-BhKiA55a.js";
4
+ import { t as connectFanvueBridge } from "../bridge-QnF7P4Co.js";
3
5
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
6
  import { jsx } from "react/jsx-runtime";
5
7
  //#region src/react/auth-context.tsx
@@ -187,6 +189,63 @@ function useEmbeddedAuth(opts) {
187
189
  };
188
190
  }
189
191
  //#endregion
190
- export { AuthProvider, useAuth, useEmbeddedAuth };
192
+ //#region src/react/use-fanvue-analytics.ts
193
+ /**
194
+ * The connection is shared by every caller: the handshake costs a round trip
195
+ * and the host issues one port per frame, so a page with several components
196
+ * tracking events must not open several bridges.
197
+ */
198
+ let sharedBridge = null;
199
+ /** Connects on first use, then returns the same connection to every caller. */
200
+ function getSharedBridge() {
201
+ sharedBridge ??= connectFanvueBridge().then((result) => {
202
+ if (result.isErr() && result.error.code === "TIMEOUT") sharedBridge = null;
203
+ return result;
204
+ });
205
+ return sharedBridge;
206
+ }
207
+ /**
208
+ * Fires analytics events through the Fanvue page hosting this app.
209
+ *
210
+ * Events land in Fanvue's Amplitude project namespaced as `embedded_app_<eventName>`,
211
+ * attributed to the app and stitched into the viewer's session — no identity
212
+ * data crosses the frame boundary. Outside Fanvue (local dev, previews, a
213
+ * standalone deployment) `track` silently does nothing, so app code can call it
214
+ * unconditionally.
215
+ *
216
+ * @returns A stable {@link UseFanvueAnalyticsResult.track} and whether events
217
+ * are currently reaching Fanvue.
218
+ *
219
+ * @example
220
+ * function CreateCourseButton() {
221
+ * const { track } = useFanvueAnalytics();
222
+ * return <button onClick={() => track('course_created', { chapters: 4 })}>Create</button>;
223
+ * }
224
+ */
225
+ function useFanvueAnalytics() {
226
+ const [isEnabled, setIsEnabled] = useState(false);
227
+ useEffect(() => {
228
+ let active = true;
229
+ getSharedBridge().then((result) => {
230
+ if (!active) return;
231
+ setIsEnabled(result.isOk() && result.value.has("analytics"));
232
+ });
233
+ return () => {
234
+ active = false;
235
+ };
236
+ }, []);
237
+ return {
238
+ track: useCallback((eventName, properties) => {
239
+ getSharedBridge().then(async (result) => {
240
+ if (result.isErr()) return;
241
+ const tracked = await result.value.analytics.track(eventName, properties);
242
+ if (tracked.isErr() && tracked.error.code === "invalid_payload" && process.env["NODE_ENV"] !== "production") console.warn(`[fanvue/analytics] dropped "${eventName}": ${tracked.error.message}`);
243
+ });
244
+ }, []),
245
+ isEnabled
246
+ };
247
+ }
248
+ //#endregion
249
+ export { AuthProvider, useAuth, useEmbeddedAuth, useFanvueAnalytics };
191
250
 
192
251
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/react/auth-context.tsx","../../src/react/use-embedded-auth.ts"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from 'react';\n\nimport { BEARER_PREFIX, HEADER_UPDATED_SESSION } from '../core/index.js';\n\nconst STORAGE_KEY = 'fanvue:jwt';\n\n/**\n * Retrieve the JWT from session storage, or `null` if unavailable.\n *\n * Exported for sibling hooks that need a synchronous storage check inside\n * effects (which can run before this provider's mount effect has loaded the\n * stored JWT into state). Not part of the public package API.\n */\nexport function getStoredJwt(): string | null {\n try {\n return sessionStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\n/** Persist the JWT into session storage. */\nfunction setStoredJwt(jwt: string): void {\n try {\n sessionStorage.setItem(STORAGE_KEY, jwt);\n } catch {\n // sessionStorage may be unavailable (e.g. sandboxed iframe)\n }\n}\n\n/** Remove the JWT from session storage. */\nfunction removeStoredJwt(): void {\n try {\n sessionStorage.removeItem(STORAGE_KEY);\n } catch {\n // sessionStorage may be unavailable\n }\n}\n\n/**\n * The value exposed by {@link AuthProvider} via React context.\n *\n * Includes the current JWT, authentication status, and helpers for\n * managing credentials and making authenticated requests.\n */\nexport interface AuthContextValue {\n jwt: string | null;\n isAuthenticated: boolean;\n setJwt: (jwt: string) => void;\n clearJwt: () => void;\n authFetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;\n}\n\nconst AuthContext = createContext<AuthContextValue | null>(null);\n\n/**\n * Provides authentication state to the React component tree.\n *\n * Wrap your application (or the authenticated section) with this provider\n * so that descendant components can access auth helpers via {@link useAuth}.\n */\nexport function AuthProvider(props: { children: ReactNode }): ReactNode {\n const { children } = props;\n // Start at null so the first client render matches the server-rendered HTML\n // (the server can't see sessionStorage); load any stored JWT after mount.\n // Reading storage in the useState initializer causes hydration mismatches\n // (React error #418) whenever a JWT is already stored.\n const [jwt, setJwtState] = useState<string | null>(null);\n\n useEffect(() => {\n const stored = getStoredJwt();\n if (stored !== null) {\n setJwtState(stored);\n }\n }, []);\n\n const setJwt = useCallback((token: string): void => {\n setStoredJwt(token);\n setJwtState(token);\n }, []);\n\n const clearJwt = useCallback((): void => {\n removeStoredJwt();\n setJwtState(null);\n }, []);\n\n const authFetch = useCallback(\n async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const currentJwt = getStoredJwt();\n const headers = new Headers(init?.headers);\n if (currentJwt) {\n headers.set('Authorization', `${BEARER_PREFIX}${currentJwt}`);\n }\n\n const response = await fetch(input, { ...init, headers });\n\n const updatedToken = response.headers.get(HEADER_UPDATED_SESSION);\n if (updatedToken) {\n setStoredJwt(updatedToken);\n setJwtState(updatedToken);\n }\n\n return response;\n },\n [],\n );\n\n const value = useMemo<AuthContextValue>(\n () => ({ jwt, isAuthenticated: jwt != null, setJwt, clearJwt, authFetch }),\n [jwt, setJwt, clearJwt, authFetch],\n );\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n/**\n * Access the current authentication context.\n *\n * Must be called from a component that is a descendant of {@link AuthProvider}.\n *\n * @throws {Error} If called outside of an `AuthProvider`.\n */\nexport function useAuth(): AuthContextValue {\n const ctx = useContext(AuthContext);\n if (!ctx) {\n throw new Error('useAuth must be used within an <AuthProvider>');\n }\n return ctx;\n}\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\n\nimport { getSessionTokenFromUrl, getThemeFromUrl, type FanvueTheme } from '../core/index.js';\n\nimport { useAuth, getStoredJwt } from './auth-context.js';\n\nconst DEFAULT_EXCHANGE_PATH = '/api/fanvue/session';\n\n/**\n * The state of the embedded session exchange.\n *\n * - `idle` — no session token in the URL (the page was not opened from a\n * Fanvue embedded surface) and no existing session.\n * - `exchanging` — the session token is being exchanged with the backend.\n * - `authenticated` — a session JWT is available; `authFetch` will attach it.\n * - `error` — the exchange failed; see `error` for the cause.\n */\nexport type EmbeddedAuthStatus = 'idle' | 'exchanging' | 'authenticated' | 'error';\n\n/**\n * Options for {@link useEmbeddedAuth}.\n *\n * @property exchangePath - The path of the session-exchange route created by\n * `createSessionExchangeHandler`. Defaults to `/api/fanvue/session`.\n */\nexport interface UseEmbeddedAuthOptions {\n exchangePath?: string;\n}\n\n/**\n * The value returned by {@link useEmbeddedAuth}.\n *\n * @property status - The current state of the embedded session exchange.\n * @property error - The error code when `status` is `error` (e.g.\n * `consent_required`, `invalid_session_token`), otherwise `null`.\n * @property theme - The creator's active colour scheme (`'light'` or `'dark'`)\n * read from the iframe URL, or `null` when absent (e.g. the app was opened\n * outside Fanvue). Use it to theme-match Fanvue.\n */\nexport interface UseEmbeddedAuthResult {\n status: EmbeddedAuthStatus;\n error: string | null;\n theme: FanvueTheme | null;\n}\n\n/**\n * Completes embedded authentication when the app is opened inside Fanvue.\n *\n * On mount, reads the short-lived session token that Fanvue appends to the\n * iframe URL (`?token=`), exchanges it with your backend session route\n * (created by `createSessionExchangeHandler`), and stores the returned\n * session JWT in the auth context — after which `authFetch` automatically\n * authenticates your API calls.\n *\n * Must be used inside an `AuthProvider`.\n *\n * @param opts - Optional overrides (e.g. a custom `exchangePath`).\n * @returns The exchange {@link EmbeddedAuthStatus} and error code, if any.\n *\n * @example\n * function EmbeddedHome() {\n * const { status, error, theme } = useEmbeddedAuth();\n * if (status === 'exchanging') return <p>Connecting…</p>;\n * if (status === 'error') return <p>Auth failed: {error}</p>;\n * return <Dashboard theme={theme ?? 'light'} />;\n * }\n */\nexport function useEmbeddedAuth(opts?: UseEmbeddedAuthOptions): UseEmbeddedAuthResult {\n const { isAuthenticated, setJwt } = useAuth();\n const [status, setStatus] = useState<EmbeddedAuthStatus>(\n isAuthenticated ? 'authenticated' : 'idle',\n );\n const [error, setError] = useState<string | null>(null);\n // Start at null so the first client render matches the server-rendered HTML\n // (the server can't read window.location); read the theme after mount.\n const [theme, setTheme] = useState<FanvueTheme | null>(null);\n const startedRef = useRef(false);\n const exchangePath = opts?.exchangePath ?? DEFAULT_EXCHANGE_PATH;\n\n useEffect(() => {\n // Guard against React Strict Mode double-invocation: the session token is\n // single-use, so the exchange must only run once.\n if (startedRef.current) return;\n startedRef.current = true;\n\n // The theme is read from the URL independently of the (single-use) token\n // exchange, so capture it before any of the auth-status early returns.\n setTheme(getThemeFromUrl(window.location.href));\n\n // Check storage directly as well as context state: this effect runs\n // before the provider's mount effect has loaded any stored JWT, and the\n // session token in the URL is single-use — a re-exchange would fail.\n if (isAuthenticated || getStoredJwt() !== null) {\n setStatus('authenticated');\n return;\n }\n\n const token = getSessionTokenFromUrl(window.location.href);\n if (!token) {\n setStatus('idle');\n return;\n }\n\n setStatus('exchanging');\n\n void (async (): Promise<void> => {\n let response: Response;\n try {\n response = await fetch(exchangePath, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token }),\n });\n } catch {\n setError('network_error');\n setStatus('error');\n return;\n }\n\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = null;\n }\n\n if (!response.ok) {\n const errorCode =\n body !== null && typeof body === 'object' && 'error' in body\n ? String((body as { error: unknown }).error)\n : 'exchange_failed';\n setError(errorCode);\n setStatus('error');\n return;\n }\n\n const jwt =\n body !== null && typeof body === 'object' && 'jwt' in body\n ? (body as { jwt: unknown }).jwt\n : null;\n if (typeof jwt !== 'string' || jwt.length === 0) {\n setError('exchange_failed');\n setStatus('error');\n return;\n }\n\n setJwt(jwt);\n setStatus('authenticated');\n })();\n }, [exchangePath, isAuthenticated, setJwt]);\n\n return { status, error, theme };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/react/auth-context.tsx","../../src/react/use-embedded-auth.ts","../../src/react/use-fanvue-analytics.ts"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from 'react';\n\nimport { BEARER_PREFIX, HEADER_UPDATED_SESSION } from '../core/index.js';\n\nconst STORAGE_KEY = 'fanvue:jwt';\n\n/**\n * Retrieve the JWT from session storage, or `null` if unavailable.\n *\n * Exported for sibling hooks that need a synchronous storage check inside\n * effects (which can run before this provider's mount effect has loaded the\n * stored JWT into state). Not part of the public package API.\n */\nexport function getStoredJwt(): string | null {\n try {\n return sessionStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\n/** Persist the JWT into session storage. */\nfunction setStoredJwt(jwt: string): void {\n try {\n sessionStorage.setItem(STORAGE_KEY, jwt);\n } catch {\n // sessionStorage may be unavailable (e.g. sandboxed iframe)\n }\n}\n\n/** Remove the JWT from session storage. */\nfunction removeStoredJwt(): void {\n try {\n sessionStorage.removeItem(STORAGE_KEY);\n } catch {\n // sessionStorage may be unavailable\n }\n}\n\n/**\n * The value exposed by {@link AuthProvider} via React context.\n *\n * Includes the current JWT, authentication status, and helpers for\n * managing credentials and making authenticated requests.\n */\nexport interface AuthContextValue {\n jwt: string | null;\n isAuthenticated: boolean;\n setJwt: (jwt: string) => void;\n clearJwt: () => void;\n authFetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;\n}\n\nconst AuthContext = createContext<AuthContextValue | null>(null);\n\n/**\n * Provides authentication state to the React component tree.\n *\n * Wrap your application (or the authenticated section) with this provider\n * so that descendant components can access auth helpers via {@link useAuth}.\n */\nexport function AuthProvider(props: { children: ReactNode }): ReactNode {\n const { children } = props;\n // Start at null so the first client render matches the server-rendered HTML\n // (the server can't see sessionStorage); load any stored JWT after mount.\n // Reading storage in the useState initializer causes hydration mismatches\n // (React error #418) whenever a JWT is already stored.\n const [jwt, setJwtState] = useState<string | null>(null);\n\n useEffect(() => {\n const stored = getStoredJwt();\n if (stored !== null) {\n setJwtState(stored);\n }\n }, []);\n\n const setJwt = useCallback((token: string): void => {\n setStoredJwt(token);\n setJwtState(token);\n }, []);\n\n const clearJwt = useCallback((): void => {\n removeStoredJwt();\n setJwtState(null);\n }, []);\n\n const authFetch = useCallback(\n async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const currentJwt = getStoredJwt();\n const headers = new Headers(init?.headers);\n if (currentJwt) {\n headers.set('Authorization', `${BEARER_PREFIX}${currentJwt}`);\n }\n\n const response = await fetch(input, { ...init, headers });\n\n const updatedToken = response.headers.get(HEADER_UPDATED_SESSION);\n if (updatedToken) {\n setStoredJwt(updatedToken);\n setJwtState(updatedToken);\n }\n\n return response;\n },\n [],\n );\n\n const value = useMemo<AuthContextValue>(\n () => ({ jwt, isAuthenticated: jwt != null, setJwt, clearJwt, authFetch }),\n [jwt, setJwt, clearJwt, authFetch],\n );\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n/**\n * Access the current authentication context.\n *\n * Must be called from a component that is a descendant of {@link AuthProvider}.\n *\n * @throws {Error} If called outside of an `AuthProvider`.\n */\nexport function useAuth(): AuthContextValue {\n const ctx = useContext(AuthContext);\n if (!ctx) {\n throw new Error('useAuth must be used within an <AuthProvider>');\n }\n return ctx;\n}\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\n\nimport { getSessionTokenFromUrl, getThemeFromUrl, type FanvueTheme } from '../core/index.js';\n\nimport { useAuth, getStoredJwt } from './auth-context.js';\n\nconst DEFAULT_EXCHANGE_PATH = '/api/fanvue/session';\n\n/**\n * The state of the embedded session exchange.\n *\n * - `idle` — no session token in the URL (the page was not opened from a\n * Fanvue embedded surface) and no existing session.\n * - `exchanging` — the session token is being exchanged with the backend.\n * - `authenticated` — a session JWT is available; `authFetch` will attach it.\n * - `error` — the exchange failed; see `error` for the cause.\n */\nexport type EmbeddedAuthStatus = 'idle' | 'exchanging' | 'authenticated' | 'error';\n\n/**\n * Options for {@link useEmbeddedAuth}.\n *\n * @property exchangePath - The path of the session-exchange route created by\n * `createSessionExchangeHandler`. Defaults to `/api/fanvue/session`.\n */\nexport interface UseEmbeddedAuthOptions {\n exchangePath?: string;\n}\n\n/**\n * The value returned by {@link useEmbeddedAuth}.\n *\n * @property status - The current state of the embedded session exchange.\n * @property error - The error code when `status` is `error` (e.g.\n * `consent_required`, `invalid_session_token`), otherwise `null`.\n * @property theme - The creator's active colour scheme (`'light'` or `'dark'`)\n * read from the iframe URL, or `null` when absent (e.g. the app was opened\n * outside Fanvue). Use it to theme-match Fanvue.\n */\nexport interface UseEmbeddedAuthResult {\n status: EmbeddedAuthStatus;\n error: string | null;\n theme: FanvueTheme | null;\n}\n\n/**\n * Completes embedded authentication when the app is opened inside Fanvue.\n *\n * On mount, reads the short-lived session token that Fanvue appends to the\n * iframe URL (`?token=`), exchanges it with your backend session route\n * (created by `createSessionExchangeHandler`), and stores the returned\n * session JWT in the auth context — after which `authFetch` automatically\n * authenticates your API calls.\n *\n * Must be used inside an `AuthProvider`.\n *\n * @param opts - Optional overrides (e.g. a custom `exchangePath`).\n * @returns The exchange {@link EmbeddedAuthStatus} and error code, if any.\n *\n * @example\n * function EmbeddedHome() {\n * const { status, error, theme } = useEmbeddedAuth();\n * if (status === 'exchanging') return <p>Connecting…</p>;\n * if (status === 'error') return <p>Auth failed: {error}</p>;\n * return <Dashboard theme={theme ?? 'light'} />;\n * }\n */\nexport function useEmbeddedAuth(opts?: UseEmbeddedAuthOptions): UseEmbeddedAuthResult {\n const { isAuthenticated, setJwt } = useAuth();\n const [status, setStatus] = useState<EmbeddedAuthStatus>(\n isAuthenticated ? 'authenticated' : 'idle',\n );\n const [error, setError] = useState<string | null>(null);\n // Start at null so the first client render matches the server-rendered HTML\n // (the server can't read window.location); read the theme after mount.\n const [theme, setTheme] = useState<FanvueTheme | null>(null);\n const startedRef = useRef(false);\n const exchangePath = opts?.exchangePath ?? DEFAULT_EXCHANGE_PATH;\n\n useEffect(() => {\n // Guard against React Strict Mode double-invocation: the session token is\n // single-use, so the exchange must only run once.\n if (startedRef.current) return;\n startedRef.current = true;\n\n // The theme is read from the URL independently of the (single-use) token\n // exchange, so capture it before any of the auth-status early returns.\n setTheme(getThemeFromUrl(window.location.href));\n\n // Check storage directly as well as context state: this effect runs\n // before the provider's mount effect has loaded any stored JWT, and the\n // session token in the URL is single-use — a re-exchange would fail.\n if (isAuthenticated || getStoredJwt() !== null) {\n setStatus('authenticated');\n return;\n }\n\n const token = getSessionTokenFromUrl(window.location.href);\n if (!token) {\n setStatus('idle');\n return;\n }\n\n setStatus('exchanging');\n\n void (async (): Promise<void> => {\n let response: Response;\n try {\n response = await fetch(exchangePath, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token }),\n });\n } catch {\n setError('network_error');\n setStatus('error');\n return;\n }\n\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = null;\n }\n\n if (!response.ok) {\n const errorCode =\n body !== null && typeof body === 'object' && 'error' in body\n ? String((body as { error: unknown }).error)\n : 'exchange_failed';\n setError(errorCode);\n setStatus('error');\n return;\n }\n\n const jwt =\n body !== null && typeof body === 'object' && 'jwt' in body\n ? (body as { jwt: unknown }).jwt\n : null;\n if (typeof jwt !== 'string' || jwt.length === 0) {\n setError('exchange_failed');\n setStatus('error');\n return;\n }\n\n setJwt(jwt);\n setStatus('authenticated');\n })();\n }, [exchangePath, isAuthenticated, setJwt]);\n\n return { status, error, theme };\n}\n","'use client';\n\nimport { useCallback, useEffect, useState } from 'react';\n\nimport {\n ANALYTICS_CAPABILITY,\n connectFanvueBridge,\n type AnalyticsProperties,\n type BridgeConnectError,\n type FanvueBridge,\n} from '../bridge/index.js';\n\nimport type { Result } from 'neverthrow';\n\n/**\n * The value returned by {@link useFanvueAnalytics}.\n *\n * @property track - Fires an event through the host page's Amplitude client.\n * Fire-and-forget: it never throws and no-ops when the app is not embedded in\n * Fanvue or was not granted analytics.\n * @property isEnabled - Whether events are actually reaching Fanvue. `false`\n * until the handshake completes, and permanently `false` outside Fanvue.\n */\nexport interface UseFanvueAnalyticsResult {\n track: (eventName: string, properties?: AnalyticsProperties) => void;\n isEnabled: boolean;\n}\n\n/**\n * The connection is shared by every caller: the handshake costs a round trip\n * and the host issues one port per frame, so a page with several components\n * tracking events must not open several bridges.\n */\nlet sharedBridge: Promise<Result<FanvueBridge, BridgeConnectError>> | null = null;\n\n/** Connects on first use, then returns the same connection to every caller. */\nfunction getSharedBridge(): Promise<Result<FanvueBridge, BridgeConnectError>> {\n sharedBridge ??= connectFanvueBridge().then((result) => {\n // A timeout is not final — the host may simply not have been ready yet —\n // so forget it and let a later track or mount handshake again. A\n // connected bridge and NOT_EMBEDDED stay cached: neither can change\n // without a reload.\n if (result.isErr() && result.error.code === 'TIMEOUT') {\n sharedBridge = null;\n }\n return result;\n });\n return sharedBridge;\n}\n\n/**\n * Fires analytics events through the Fanvue page hosting this app.\n *\n * Events land in Fanvue's Amplitude project namespaced as `embedded_app_<eventName>`,\n * attributed to the app and stitched into the viewer's session — no identity\n * data crosses the frame boundary. Outside Fanvue (local dev, previews, a\n * standalone deployment) `track` silently does nothing, so app code can call it\n * unconditionally.\n *\n * @returns A stable {@link UseFanvueAnalyticsResult.track} and whether events\n * are currently reaching Fanvue.\n *\n * @example\n * function CreateCourseButton() {\n * const { track } = useFanvueAnalytics();\n * return <button onClick={() => track('course_created', { chapters: 4 })}>Create</button>;\n * }\n */\nexport function useFanvueAnalytics(): UseFanvueAnalyticsResult {\n const [isEnabled, setIsEnabled] = useState(false);\n\n useEffect(() => {\n let active = true;\n void getSharedBridge().then((result) => {\n if (!active) return;\n setIsEnabled(result.isOk() && result.value.has(ANALYTICS_CAPABILITY));\n });\n return (): void => {\n active = false;\n };\n }, []);\n\n const track = useCallback((eventName: string, properties?: AnalyticsProperties): void => {\n void getSharedBridge().then(async (result) => {\n if (result.isErr()) return;\n const tracked = await result.value.analytics.track(eventName, properties);\n // Surface a typo'd event name or property in development rather than\n // silently swallowing the event; production stays fire-and-forget.\n if (\n tracked.isErr() &&\n tracked.error.code === 'invalid_payload' &&\n process.env['NODE_ENV'] !== 'production'\n ) {\n console.warn(`[fanvue/analytics] dropped \"${eventName}\": ${tracked.error.message}`);\n }\n });\n }, []);\n\n return { track, isEnabled };\n}\n"],"mappings":"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"}