@fanvue/builder-sdk, unless a sample says otherwise.
Encrypt secrets at rest
createFanvueCrypto({ currentKey, previousKeys }) returns AES-256-GCM encryption over a key registry. currentKey is kid:base64key, for example v2:<32 bytes base64>. Bare key material without a prefix takes the key id v1 (DEFAULT_KEY_ID). previousKeys is either null or a comma-separated list of retired kid:base64key entries, which can decrypt but never encrypt.
withPurpose returns a SecretCipher with only encrypt and decrypt, which is the type the token store and webhook receiver take. parseKeyRegistry(config) validates the configuration without encrypting anything.
Every failure is a CryptoError with one of the codes CRYPTO_KEY_INVALID, CRYPTO_KEY_UNKNOWN, CRYPTO_MALFORMED_CIPHERTEXT, CRYPTO_DECRYPT_FAILED or CRYPTO_ENCRYPT_FAILED. Error messages never contain key material or plaintext.
lib/crypto.ts
Store tokens
The token store keeps one encryptedTokenSet per subject, where the subject is your own stable id for the creator. You implement TokenStorageAdapter over your database, and the SDK owns the refresh logic.
createTokenStoreContext({ adapter, cipher, oauth }) binds the adapter, a SecretCipher and your OAuthConfig, and wires the SDK’s own refresh call.
Nothing in the store throws. A
null from getAccessToken means one of these, and in every case you should show the creator a reconnect state:
- the subject never connected
- the subject has no refresh token
- the record can’t be decrypted after a key rotation that dropped the old key
- the refresh was refused
throw inside onTokens: storeTokenSet never throws, so without it a creator would receive a session whose tokens were never stored.
lib/token-store.ts
app/api/fanvue/session/route.ts
workers/daily-digest.ts
Issue your own app sessions
createAppSessions(config) signs and verifies the bearer JWTs your creator surface and fan surface send to your routes. These are the SDK’s own session tokens, not Fanvue tokens. They carry identity claims and no access token, which is why they can live longer than a Fanvue access token.
CreatorSessionClaimsSchema carries typ: 'creator', subjectId, fanvueUserUuid, displayName and handle. FanSessionClaimsSchema carries typ: 'fan', fanvueFanUuid, experienceUuid, entitled: true and preview. Extend either with .extend({ ... }) to add app claims. Never make it .strict(), because a verified payload also carries iss, aud, sub, jti, iat and exp.
lib/sessions.ts
signCreatorSession, signFanSession, getCreatorSession(authorizationHeader) and getFanSession(authorizationHeader). A creator token presented to the fan verifier does not verify, because the audiences differ. Every verification failure returns null.
The @fanvue/builder-sdk/nextjs/embedded-app entrypoint builds on these sessions:
createCreatorSessionHandlerandcreateFanSessionHandlerare the bootstrap routes that exchange Fanvue’s tokens for app sessions.requireCreatorSession,requireFanSessionandrequireCreatorAccessTokenguard your routes. Each returns{ ok: true, session }, or{ ok: false, response }with the response ready to return.requireCreatorAccessTokenalso fetches a live Fanvue token through the token store, and answers401 fanvue_reconnect_requiredwhen none exists.
app/api/courses/route.ts
isSessionCurrent is an optional revocation check against your own database. A check that throws counts as revoked.
Protect internal endpoints
requireMachineAuth(request, options) authenticates a cron tick or queue drain against your own endpoints. It isn’t a way to authenticate to Fanvue. It tries two strategies in order: a shared bearer secret compared in constant time, then an app-supplied SignedRequestVerifier.
app/api/cron/refresh/route.ts
boundedBatchSize(requested, defaultSize, max) clamps an operator-supplied ?limit= to a positive integer no greater than max. Pass a SignedRequestVerifier with the exact request bytes in rawBody when a queue provider signs its deliveries.
Scrub logs and error reports
createSafeLogFields(extraAllowedKeys?) returns a filter that:
- drops every field not on an allowlist
- drops values that are not
string | number | boolean | null - replaces any string containing a UUID or an email address with
[redacted-uuid]or[redacted-email]
BASE_ALLOWED_LOG_KEYS holds cause, code, creatorId, errorName, httpStatus, reason, retryCount and status.
lib/log.ts
errorName(error) returns only the class name, never error.message, which routinely carries upstream bodies and signed URLs. logEvent is a ready-made logger over the base allowlist with the prefix [fanvue].
createSentryScrubber(extraSensitiveKeyPattern?) returns a beforeSend function for an error-reporting client, with no dependency on one. It redacts values under keys matching BASE_SENSITIVE_KEY_PATTERN, which covers authorization, cookie, token, secret, password, signedurl, api_key, credential, email, phone and ip_address. It also strips UUIDs, emails and URL query strings from every string, and stops its walk at depth 32.
configurationReadiness(env?) returns two named checks for a readiness route. The detail names missing variables, never their values.
Secrets checklist
SESSION_SECRETof at least 32 random characters.createConfigaccepts shorter values, butcreateAppSessionsandrequireMachineAuthdon’t.- Encryption keys of exactly 32 bytes, base64 encoded, with the key id prefix.
- Client secret, encryption keys and
CRON_SECRETon the server only. The browser receives the session JWT and nothing else. - Webhook signing secrets encrypted with their own purpose, as in Receive webhooks with the SDK.