From d166cc36504aeeb99edd0ebff16e02818fcb67ee Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:52:04 +0000 Subject: [PATCH 1/4] docs: add dynamic authentication guide for JWT signing in SDKs Co-Authored-By: Chris McDonnell --- .../sdks/guides/dynamic-authentication.mdx | 251 ++++++++++++++++++ fern/products/sdks/sdks.yml | 3 + 2 files changed, 254 insertions(+) create mode 100644 fern/products/sdks/guides/dynamic-authentication.mdx diff --git a/fern/products/sdks/guides/dynamic-authentication.mdx b/fern/products/sdks/guides/dynamic-authentication.mdx new file mode 100644 index 000000000..f35d15727 --- /dev/null +++ b/fern/products/sdks/guides/dynamic-authentication.mdx @@ -0,0 +1,251 @@ +--- +title: Dynamic authentication +description: Implement dynamic authentication patterns like short-lived JWT signing in your SDKs. +--- + +Your API may require dynamic authentication where credentials need to be generated or refreshed for each request, such as signing short-lived JWTs or rotating tokens. Fern-generated SDKs support this pattern through request-level header injection. + +## How it works + +Fern-generated SDKs accept a `headers` parameter in the `RequestOptions` for every API call. This allows you to inject dynamically computed headers (like signed JWTs) on a per-request basis without needing to override each method. + +## TypeScript example: Short-lived JWT signing + +Here's how to implement a client wrapper that signs a JWT valid for 15 seconds before each request: + + + +### Create a wrapper client + +Create a custom client class that extends the generated Fern client and adds JWT signing logic: + +```typescript title="src/wrapper/MyClient.ts" +import { MyClient as FernClient } from "../Client"; +import * as jwt from "jsonwebtoken"; + +export class MyClient extends FernClient { + private privateKey: string; + + constructor(options: { privateKey: string; environment: string }) { + super({ + environment: options.environment, + }); + this.privateKey = options.privateKey; + } + + /** + * Generate a short-lived JWT token valid for 15 seconds + */ + private generateJWT(): string { + const now = Math.floor(Date.now() / 1000); + const payload = { + iat: now, + exp: now + 15, // Expires in 15 seconds + }; + + return jwt.sign(payload, this.privateKey, { algorithm: "RS256" }); + } + + /** + * Helper method to inject JWT into request options + */ + private withJWT(requestOptions?: any): any { + const token = this.generateJWT(); + return { + ...requestOptions, + headers: { + ...requestOptions?.headers, + Authorization: `Bearer ${token}`, + }, + }; + } + + // Override methods to automatically inject JWT + async getUser(userId: string, requestOptions?: any) { + return super.getUser(userId, this.withJWT(requestOptions)); + } + + async createUser(request: any, requestOptions?: any) { + return super.createUser(request, this.withJWT(requestOptions)); + } + + // Add overrides for all other methods... +} +``` + +### Export the wrapper client + +Update your `index.ts` to export the wrapper instead of the generated client: + +```typescript title="src/index.ts" +export { MyClient } from "./wrapper/MyClient"; +export * from "./api"; // Export types +``` + +### Add to `.fernignore` + +Protect your custom code from being overwritten: + +```diff title=".fernignore" ++ src/wrapper ++ src/index.ts +``` + +### Use the client + +Your users can now use the client with automatic JWT signing: + +```typescript +import { MyClient } from "my-sdk"; + +const client = new MyClient({ + privateKey: process.env.PRIVATE_KEY, + environment: "https://api.example.com", +}); + +// JWT is automatically signed and injected for each request +const user = await client.getUser("user-123"); +const newUser = await client.createUser({ name: "Alice" }); +``` + + + +## Alternative: Proxy pattern without method overrides + +If you want to avoid overriding each method individually, you can use a Proxy to intercept all method calls: + +```typescript title="src/wrapper/MyClient.ts" +import { MyClient as FernClient } from "../Client"; +import * as jwt from "jsonwebtoken"; + +export class MyClient { + private client: FernClient; + private privateKey: string; + + constructor(options: { privateKey: string; environment: string }) { + this.client = new FernClient({ + environment: options.environment, + }); + this.privateKey = options.privateKey; + + // Return a proxy that intercepts all method calls + return new Proxy(this, { + get(target, prop) { + // If accessing the client property itself, return it + if (prop === "client" || prop === "privateKey") { + return target[prop as keyof typeof target]; + } + + // Get the property from the underlying client + const value = (target.client as any)[prop]; + + // If it's a function, wrap it to inject JWT + if (typeof value === "function") { + return function (...args: any[]) { + // The last argument is typically requestOptions + const lastArg = args[args.length - 1]; + const isRequestOptions = + lastArg && typeof lastArg === "object" && !Array.isArray(lastArg); + + if (isRequestOptions) { + // Inject JWT into existing request options + args[args.length - 1] = target.withJWT(lastArg); + } else { + // Add JWT as new request options + args.push(target.withJWT()); + } + + return value.apply(target.client, args); + }; + } + + return value; + }, + }); + } + + private generateJWT(): string { + const now = Math.floor(Date.now() / 1000); + const payload = { + iat: now, + exp: now + 15, + }; + return jwt.sign(payload, this.privateKey, { algorithm: "RS256" }); + } + + private withJWT(requestOptions?: any): any { + const token = this.generateJWT(); + return { + ...requestOptions, + headers: { + ...requestOptions?.headers, + Authorization: `Bearer ${token}`, + }, + }; + } +} +``` + +This approach automatically wraps all methods without needing to override each one individually. + +## Python example: Short-lived JWT signing + +For Python SDKs, you can use a similar pattern: + +```python title="src/wrapper/client.py" +from .client import Client as FernClient +import jwt +import time +from typing import Optional, Dict, Any + +class Client(FernClient): + def __init__(self, *, private_key: str, environment: str): + super().__init__(environment=environment) + self._private_key = private_key + + def _generate_jwt(self) -> str: + """Generate a short-lived JWT token valid for 15 seconds""" + now = int(time.time()) + payload = { + "iat": now, + "exp": now + 15, + } + return jwt.encode(payload, self._private_key, algorithm="RS256") + + def _with_jwt(self, request_options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Inject JWT into request options""" + token = self._generate_jwt() + options = request_options or {} + headers = options.get("headers", {}) + headers["Authorization"] = f"Bearer {token}" + options["headers"] = headers + return options + + def get_user(self, user_id: str, *, request_options: Optional[Dict[str, Any]] = None): + return super().get_user(user_id, request_options=self._with_jwt(request_options)) + + def create_user(self, request: Any, *, request_options: Optional[Dict[str, Any]] = None): + return super().create_user(request, request_options=self._with_jwt(request_options)) +``` + +## Other authentication patterns + +This same pattern works for other dynamic authentication scenarios: + +- **OAuth token refresh**: Automatically refresh expired access tokens before each request +- **HMAC signing**: Sign requests with HMAC signatures based on request content +- **Rotating API keys**: Switch between multiple API keys based on rate limits +- **Time-based tokens**: Generate tokens that include timestamps or nonces + +## Best practices + +- **Cache when possible**: If your tokens are valid for longer periods, cache them to avoid regenerating on every request +- **Handle errors gracefully**: Implement retry logic for authentication failures +- **Secure key storage**: Never hardcode private keys; use environment variables or secure key management systems +- **Test thoroughly**: Ensure your wrapper handles all edge cases, including concurrent requests + +## See also + +- [Adding custom code](/sdks/capabilities/custom-code) - Learn more about extending generated SDKs +- [TypeScript custom code](/sdks/generators/typescript/custom-code) - TypeScript-specific customization guide +- [Python custom code](/sdks/generators/python/custom-code) - Python-specific customization guide diff --git a/fern/products/sdks/sdks.yml b/fern/products/sdks/sdks.yml index ac3c9b610..5a04b3f86 100644 --- a/fern/products/sdks/sdks.yml +++ b/fern/products/sdks/sdks.yml @@ -223,6 +223,9 @@ navigation: - page: Configure global headers path: ./guides/configure-global-headers.mdx slug: global-headers + - page: Dynamic authentication + path: ./guides/dynamic-authentication.mdx + slug: dynamic-authentication - page: Configure auto-pagination path: ./guides/configure-auto-pagination.mdx slug: auto-pagination From 4bf04e2535b374a6b807ea66d753d5f8e85bd7c6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:14:28 +0000 Subject: [PATCH 2/4] docs: update dynamic authentication guide with custom fetcher approach - Add custom fetcher middleware as recommended approach per Swimburger feedback - Replace Proxy pattern with simpler method override alternative - Add comprehensive gotchas and security considerations - Update all examples to use plant-themed content (PlantStoreClient, plants.get, etc.) - Fix Vale linting issues: remove time-relative 'now', define HMAC acronym - Add allowCustomFetcher configuration example - Include token memoization pattern with grace period - Document import paths, header merging, and thread safety considerations Co-Authored-By: Chris McDonnell --- .../sdks/guides/dynamic-authentication.mdx | 250 ++++++++++-------- 1 file changed, 139 insertions(+), 111 deletions(-) diff --git a/fern/products/sdks/guides/dynamic-authentication.mdx b/fern/products/sdks/guides/dynamic-authentication.mdx index f35d15727..c7b069a0d 100644 --- a/fern/products/sdks/guides/dynamic-authentication.mdx +++ b/fern/products/sdks/guides/dynamic-authentication.mdx @@ -3,73 +3,92 @@ title: Dynamic authentication description: Implement dynamic authentication patterns like short-lived JWT signing in your SDKs. --- -Your API may require dynamic authentication where credentials need to be generated or refreshed for each request, such as signing short-lived JWTs or rotating tokens. Fern-generated SDKs support this pattern through request-level header injection. +Your API may require dynamic authentication where credentials need to be generated or refreshed for each request, such as signing short-lived JWTs or rotating tokens. Fern-generated SDKs support this pattern through custom fetcher middleware. -## How it works +## Recommended: Custom fetcher middleware -Fern-generated SDKs accept a `headers` parameter in the `RequestOptions` for every API call. This allows you to inject dynamically computed headers (like signed JWTs) on a per-request basis without needing to override each method. +The best way to implement dynamic authentication in TypeScript SDKs is to use a custom fetcher. This acts as middleware for all requests, allowing you to inject authentication logic in a single place without overriding individual methods. -## TypeScript example: Short-lived JWT signing +### How it works -Here's how to implement a client wrapper that signs a JWT valid for 15 seconds before each request: +When you enable `allowCustomFetcher` in your generator configuration, the generated SDK accepts a `fetcher` parameter in the client options. This fetcher wraps all HTTP requests, giving you a single injection point for authentication logic. + +### TypeScript example: Short-lived JWT signing + +Here's how to implement JWT signing with token memoization: -### Create a wrapper client +### Enable custom fetcher in generator configuration + +Add `allowCustomFetcher: true` to your `generators.yml`: + +```yaml title="generators.yml" +default-group: local +groups: + local: + generators: + - name: fernapi/fern-typescript-node-sdk + version: 0.x.x + output: + location: local-file-system + path: ../generated/typescript + config: + allowCustomFetcher: true +``` + +### Create a custom fetcher with JWT signing -Create a custom client class that extends the generated Fern client and adds JWT signing logic: +Create a fetcher function that wraps the default fetcher and injects JWT authentication: -```typescript title="src/wrapper/MyClient.ts" -import { MyClient as FernClient } from "../Client"; +```typescript title="src/wrapper/jwtFetcher.ts" import * as jwt from "jsonwebtoken"; +import { fetcher as defaultFetcher, type FetchFunction } from "../core/fetcher"; -export class MyClient extends FernClient { - private privateKey: string; +export function createJwtFetcher(privateKey: string): FetchFunction { + // Cache token to avoid regenerating on every request + let cachedToken: { value: string; expiresAt: number } | undefined; - constructor(options: { privateKey: string; environment: string }) { - super({ - environment: options.environment, - }); - this.privateKey = options.privateKey; - } - - /** - * Generate a short-lived JWT token valid for 15 seconds - */ - private generateJWT(): string { + return async (args) => { const now = Math.floor(Date.now() / 1000); - const payload = { - iat: now, - exp: now + 15, // Expires in 15 seconds - }; - return jwt.sign(payload, this.privateKey, { algorithm: "RS256" }); - } - - /** - * Helper method to inject JWT into request options - */ - private withJWT(requestOptions?: any): any { - const token = this.generateJWT(); - return { - ...requestOptions, - headers: { - ...requestOptions?.headers, - Authorization: `Bearer ${token}`, - }, + // Regenerate token if expired or about to expire (within 2 seconds) + if (!cachedToken || cachedToken.expiresAt - 2 <= now) { + const payload = { + iat: now, + exp: now + 15, // Token valid for 15 seconds + }; + const token = jwt.sign(payload, privateKey, { algorithm: "RS256" }); + cachedToken = { value: token, expiresAt: payload.exp }; + } + + // Inject JWT into request headers + const headers = { + ...(args.headers ?? {}), + Authorization: `Bearer ${cachedToken.value}`, }; - } - // Override methods to automatically inject JWT - async getUser(userId: string, requestOptions?: any) { - return super.getUser(userId, this.withJWT(requestOptions)); - } + // Call the default fetcher with modified headers + return defaultFetcher({ ...args, headers }); + }; +} +``` - async createUser(request: any, requestOptions?: any) { - return super.createUser(request, this.withJWT(requestOptions)); - } +### Create a wrapper client - // Add overrides for all other methods... +Extend the generated client to use the custom fetcher: + +```typescript title="src/wrapper/PlantStoreClient.ts" +import { PlantStoreClient as FernClient } from "../Client"; +import { createJwtFetcher } from "./jwtFetcher"; + +export class PlantStoreClient extends FernClient { + constructor(options: { privateKey: string; environment: string }) { + super({ + environment: options.environment, + fetcher: createJwtFetcher(options.privateKey), + }); + } } ``` @@ -78,7 +97,7 @@ export class MyClient extends FernClient { Update your `index.ts` to export the wrapper instead of the generated client: ```typescript title="src/index.ts" -export { MyClient } from "./wrapper/MyClient"; +export { PlantStoreClient } from "./wrapper/PlantStoreClient"; export * from "./api"; // Export types ``` @@ -93,75 +112,42 @@ Protect your custom code from being overwritten: ### Use the client -Your users can now use the client with automatic JWT signing: +Users can use the client with automatic JWT signing on all requests: ```typescript -import { MyClient } from "my-sdk"; +import { PlantStoreClient } from "plant-store-sdk"; -const client = new MyClient({ +const client = new PlantStoreClient({ privateKey: process.env.PRIVATE_KEY, - environment: "https://api.example.com", + environment: "https://api.plantstore.com", }); // JWT is automatically signed and injected for each request -const user = await client.getUser("user-123"); -const newUser = await client.createUser({ name: "Alice" }); +const plant = await client.plants.get("monstera-123"); +const newPlant = await client.plants.create({ + name: "Fiddle Leaf Fig", + species: "Ficus lyrata" +}); ``` -## Alternative: Proxy pattern without method overrides +## Alternative: Method overrides -If you want to avoid overriding each method individually, you can use a Proxy to intercept all method calls: +If you cannot enable `allowCustomFetcher` or prefer a simpler approach, you can override individual methods: -```typescript title="src/wrapper/MyClient.ts" -import { MyClient as FernClient } from "../Client"; +```typescript title="src/wrapper/PlantStoreClient.ts" +import { PlantStoreClient as FernClient } from "../Client"; import * as jwt from "jsonwebtoken"; -export class MyClient { - private client: FernClient; +export class PlantStoreClient extends FernClient { private privateKey: string; constructor(options: { privateKey: string; environment: string }) { - this.client = new FernClient({ + super({ environment: options.environment, }); this.privateKey = options.privateKey; - - // Return a proxy that intercepts all method calls - return new Proxy(this, { - get(target, prop) { - // If accessing the client property itself, return it - if (prop === "client" || prop === "privateKey") { - return target[prop as keyof typeof target]; - } - - // Get the property from the underlying client - const value = (target.client as any)[prop]; - - // If it's a function, wrap it to inject JWT - if (typeof value === "function") { - return function (...args: any[]) { - // The last argument is typically requestOptions - const lastArg = args[args.length - 1]; - const isRequestOptions = - lastArg && typeof lastArg === "object" && !Array.isArray(lastArg); - - if (isRequestOptions) { - // Inject JWT into existing request options - args[args.length - 1] = target.withJWT(lastArg); - } else { - // Add JWT as new request options - args.push(target.withJWT()); - } - - return value.apply(target.client, args); - }; - } - - return value; - }, - }); } private generateJWT(): string { @@ -183,22 +169,33 @@ export class MyClient { }, }; } + + // Override each method to inject JWT + async getPlant(plantId: string, requestOptions?: any) { + return super.plants.get(plantId, this.withJWT(requestOptions)); + } + + async createPlant(request: any, requestOptions?: any) { + return super.plants.create(request, this.withJWT(requestOptions)); + } + + // Override additional methods as needed... } ``` -This approach automatically wraps all methods without needing to override each one individually. +This approach requires overriding each method individually, which can be tedious for large APIs. The custom fetcher approach is recommended for better maintainability. ## Python example: Short-lived JWT signing -For Python SDKs, you can use a similar pattern: +For Python SDKs, you can use a similar method override pattern: ```python title="src/wrapper/client.py" -from .client import Client as FernClient +from .client import PlantStoreClient as FernClient import jwt import time from typing import Optional, Dict, Any -class Client(FernClient): +class PlantStoreClient(FernClient): def __init__(self, *, private_key: str, environment: str): super().__init__(environment=environment) self._private_key = private_key @@ -221,11 +218,11 @@ class Client(FernClient): options["headers"] = headers return options - def get_user(self, user_id: str, *, request_options: Optional[Dict[str, Any]] = None): - return super().get_user(user_id, request_options=self._with_jwt(request_options)) + def get_plant(self, plant_id: str, *, request_options: Optional[Dict[str, Any]] = None): + return super().plants.get(plant_id, request_options=self._with_jwt(request_options)) - def create_user(self, request: Any, *, request_options: Optional[Dict[str, Any]] = None): - return super().create_user(request, request_options=self._with_jwt(request_options)) + def create_plant(self, request: Any, *, request_options: Optional[Dict[str, Any]] = None): + return super().plants.create(request, request_options=self._with_jwt(request_options)) ``` ## Other authentication patterns @@ -233,16 +230,47 @@ class Client(FernClient): This same pattern works for other dynamic authentication scenarios: - **OAuth token refresh**: Automatically refresh expired access tokens before each request -- **HMAC signing**: Sign requests with HMAC signatures based on request content +- **HMAC (Hash-based Message Authentication Code) signing**: Sign requests with HMAC signatures based on request content - **Rotating API keys**: Switch between multiple API keys based on rate limits - **Time-based tokens**: Generate tokens that include timestamps or nonces -## Best practices +## Important considerations -- **Cache when possible**: If your tokens are valid for longer periods, cache them to avoid regenerating on every request -- **Handle errors gracefully**: Implement retry logic for authentication failures +### Custom fetcher requirements + +- **Generator configuration**: The `allowCustomFetcher` option must be enabled in your `generators.yml` for the `fetcher` parameter to be available in `BaseClientOptions` +- **Import path**: Import the default fetcher from `../core/fetcher` (or the appropriate path in your generated SDK) to wrap it with your custom logic +- **Preserve all arguments**: When wrapping the default fetcher, ensure you pass through all arguments to maintain compatibility with the SDK's internal behavior + +### Security considerations + +- **Server-side only**: Never expose private keys in browser environments. JWT signing with private keys should only be done in server-side code (Node.js, Deno, Bun) - **Secure key storage**: Never hardcode private keys; use environment variables or secure key management systems -- **Test thoroughly**: Ensure your wrapper handles all edge cases, including concurrent requests +- **Avoid double authentication**: If your API already uses bearer token authentication in the Fern definition, be careful not to override the existing `Authorization` header. Consider using a different header name or conditionally setting the header + +### Performance and concurrency + +- **Token memoization**: Cache tokens to avoid regenerating them on every request. The example above caches tokens and refreshes them 2 seconds before expiration +- **Thread safety**: The memoization pattern shown is safe for concurrent requests in JavaScript's single-threaded event loop, but be mindful of race conditions in other environments +- **Grace period**: Refresh tokens slightly before they expire (e.g., 2 seconds early) to avoid edge cases where a token expires during request processing + +### Header merging + +- **Preserve existing headers**: When injecting authentication headers, always spread existing headers to avoid overwriting headers set by the SDK or user +- **Header precedence**: Headers are merged in order: SDK defaults → client options → request options → custom fetcher. Your custom fetcher runs last and can override previous headers + +### Time synchronization + +- **Clock drift**: Be aware of potential clock drift between your client and server. Consider adding tolerance to your token expiration checks +- **Timestamp precision**: Use Unix timestamps (seconds since epoch) for `iat` and `exp` claims to match JWT standards + +## Best practices + +- **Use custom fetcher for TypeScript**: The custom fetcher approach is the most maintainable solution for TypeScript SDKs when `allowCustomFetcher` is available +- **Cache tokens appropriately**: Balance between security (shorter token lifetime) and performance (less frequent regeneration) +- **Handle errors gracefully**: Implement retry logic for authentication failures and token refresh errors +- **Test thoroughly**: Ensure your wrapper handles all edge cases, including concurrent requests, token expiration, and network failures +- **Monitor token usage**: Log token generation and refresh events to help debug authentication issues in production ## See also From fd3ec2031ee657d51d68aff0738608b992343ffa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:27:49 +0000 Subject: [PATCH 3/4] docs: update wrapper client to accept all FernClient options - Use ConstructorParameters to infer exact options type from generated client - Accept all client options (headers, timeout, maxRetries, etc.) except fetcher - Extract privateKey and pass remaining options to parent constructor - Add explanatory note about ConstructorParameters pattern - Update both custom fetcher and method override examples for consistency Addresses Swimburger's feedback about dynamically passing all options. Co-Authored-By: Chris McDonnell --- .../sdks/guides/dynamic-authentication.mdx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/fern/products/sdks/guides/dynamic-authentication.mdx b/fern/products/sdks/guides/dynamic-authentication.mdx index c7b069a0d..49c7946d4 100644 --- a/fern/products/sdks/guides/dynamic-authentication.mdx +++ b/fern/products/sdks/guides/dynamic-authentication.mdx @@ -82,16 +82,27 @@ Extend the generated client to use the custom fetcher: import { PlantStoreClient as FernClient } from "../Client"; import { createJwtFetcher } from "./jwtFetcher"; +// Infer the exact options type from the generated client's constructor +type FernClientOptions = ConstructorParameters[0]; +// Accept all options except 'fetcher', and add our 'privateKey' +type Options = Omit & { privateKey: string }; + export class PlantStoreClient extends FernClient { - constructor(options: { privateKey: string; environment: string }) { + constructor(options: Options) { + // Extract privateKey and pass all other options to the parent + const { privateKey, ...clientOptions } = options; super({ - environment: options.environment, - fetcher: createJwtFetcher(options.privateKey), + ...clientOptions, + fetcher: createJwtFetcher(privateKey), }); } } ``` + +This pattern uses `ConstructorParameters` to infer the exact options type from the generated client, ensuring compatibility with all client options (headers, timeoutInSeconds, maxRetries, etc.) without hardcoding them. This keeps the wrapper future-proof as the generator adds new options. + + ### Export the wrapper client Update your `index.ts` to export the wrapper instead of the generated client: @@ -140,14 +151,16 @@ If you cannot enable `allowCustomFetcher` or prefer a simpler approach, you can import { PlantStoreClient as FernClient } from "../Client"; import * as jwt from "jsonwebtoken"; +type FernClientOptions = ConstructorParameters[0]; +type Options = Omit & { privateKey: string }; + export class PlantStoreClient extends FernClient { private privateKey: string; - constructor(options: { privateKey: string; environment: string }) { - super({ - environment: options.environment, - }); - this.privateKey = options.privateKey; + constructor(options: Options) { + const { privateKey, ...clientOptions } = options; + super(clientOptions); + this.privateKey = privateKey; } private generateJWT(): string { From e2b730aced4845da63240cd74b9f250e060ea4ff Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:51:06 +0000 Subject: [PATCH 4/4] docs: reorganize dynamic authentication by language - Remove 'Alternative: Method overrides' section from TypeScript docs - Create TypeScript-specific page with custom fetcher approach only - Create Python-specific page with method override approach - Convert main guide to lightweight overview hub with language cards - Add navigation entries for both language-specific pages - Keep language-agnostic considerations in overview Addresses Swimburger's feedback to organize content by language. Co-Authored-By: Chris McDonnell --- .../sdks/guides/dynamic-authentication.mdx | 292 ++---------------- .../python/dynamic-authentication.mdx | 148 +++++++++ .../typescript/dynamic-authentication.mdx | 195 ++++++++++++ fern/products/sdks/sdks.yml | 6 + 4 files changed, 377 insertions(+), 264 deletions(-) create mode 100644 fern/products/sdks/overview/python/dynamic-authentication.mdx create mode 100644 fern/products/sdks/overview/typescript/dynamic-authentication.mdx diff --git a/fern/products/sdks/guides/dynamic-authentication.mdx b/fern/products/sdks/guides/dynamic-authentication.mdx index 49c7946d4..2662911d5 100644 --- a/fern/products/sdks/guides/dynamic-authentication.mdx +++ b/fern/products/sdks/guides/dynamic-authentication.mdx @@ -3,290 +3,54 @@ title: Dynamic authentication description: Implement dynamic authentication patterns like short-lived JWT signing in your SDKs. --- -Your API may require dynamic authentication where credentials need to be generated or refreshed for each request, such as signing short-lived JWTs or rotating tokens. Fern-generated SDKs support this pattern through custom fetcher middleware. +Your API may require dynamic authentication where credentials need to be generated or refreshed for each request, such as signing short-lived JWTs or rotating tokens. Fern-generated SDKs support this pattern through language-specific approaches. -## Recommended: Custom fetcher middleware +## Language-specific guides -The best way to implement dynamic authentication in TypeScript SDKs is to use a custom fetcher. This acts as middleware for all requests, allowing you to inject authentication logic in a single place without overriding individual methods. +Each language has its own recommended approach for implementing dynamic authentication: -### How it works + + + Use custom fetcher middleware to inject authentication logic in a single place for all requests. Supports JWT signing, OAuth token refresh, and more. + + + Use method overrides to inject authentication logic for each API call. Supports JWT signing, OAuth token refresh, and more. + + -When you enable `allowCustomFetcher` in your generator configuration, the generated SDK accepts a `fetcher` parameter in the client options. This fetcher wraps all HTTP requests, giving you a single injection point for authentication logic. +## Common use cases -### TypeScript example: Short-lived JWT signing - -Here's how to implement JWT signing with token memoization: - - - -### Enable custom fetcher in generator configuration - -Add `allowCustomFetcher: true` to your `generators.yml`: - -```yaml title="generators.yml" -default-group: local -groups: - local: - generators: - - name: fernapi/fern-typescript-node-sdk - version: 0.x.x - output: - location: local-file-system - path: ../generated/typescript - config: - allowCustomFetcher: true -``` - -### Create a custom fetcher with JWT signing - -Create a fetcher function that wraps the default fetcher and injects JWT authentication: - -```typescript title="src/wrapper/jwtFetcher.ts" -import * as jwt from "jsonwebtoken"; -import { fetcher as defaultFetcher, type FetchFunction } from "../core/fetcher"; - -export function createJwtFetcher(privateKey: string): FetchFunction { - // Cache token to avoid regenerating on every request - let cachedToken: { value: string; expiresAt: number } | undefined; - - return async (args) => { - const now = Math.floor(Date.now() / 1000); - - // Regenerate token if expired or about to expire (within 2 seconds) - if (!cachedToken || cachedToken.expiresAt - 2 <= now) { - const payload = { - iat: now, - exp: now + 15, // Token valid for 15 seconds - }; - const token = jwt.sign(payload, privateKey, { algorithm: "RS256" }); - cachedToken = { value: token, expiresAt: payload.exp }; - } - - // Inject JWT into request headers - const headers = { - ...(args.headers ?? {}), - Authorization: `Bearer ${cachedToken.value}`, - }; - - // Call the default fetcher with modified headers - return defaultFetcher({ ...args, headers }); - }; -} -``` - -### Create a wrapper client - -Extend the generated client to use the custom fetcher: - -```typescript title="src/wrapper/PlantStoreClient.ts" -import { PlantStoreClient as FernClient } from "../Client"; -import { createJwtFetcher } from "./jwtFetcher"; - -// Infer the exact options type from the generated client's constructor -type FernClientOptions = ConstructorParameters[0]; -// Accept all options except 'fetcher', and add our 'privateKey' -type Options = Omit & { privateKey: string }; - -export class PlantStoreClient extends FernClient { - constructor(options: Options) { - // Extract privateKey and pass all other options to the parent - const { privateKey, ...clientOptions } = options; - super({ - ...clientOptions, - fetcher: createJwtFetcher(privateKey), - }); - } -} -``` - - -This pattern uses `ConstructorParameters` to infer the exact options type from the generated client, ensuring compatibility with all client options (headers, timeoutInSeconds, maxRetries, etc.) without hardcoding them. This keeps the wrapper future-proof as the generator adds new options. - - -### Export the wrapper client - -Update your `index.ts` to export the wrapper instead of the generated client: - -```typescript title="src/index.ts" -export { PlantStoreClient } from "./wrapper/PlantStoreClient"; -export * from "./api"; // Export types -``` - -### Add to `.fernignore` - -Protect your custom code from being overwritten: - -```diff title=".fernignore" -+ src/wrapper -+ src/index.ts -``` - -### Use the client - -Users can use the client with automatic JWT signing on all requests: - -```typescript -import { PlantStoreClient } from "plant-store-sdk"; - -const client = new PlantStoreClient({ - privateKey: process.env.PRIVATE_KEY, - environment: "https://api.plantstore.com", -}); - -// JWT is automatically signed and injected for each request -const plant = await client.plants.get("monstera-123"); -const newPlant = await client.plants.create({ - name: "Fiddle Leaf Fig", - species: "Ficus lyrata" -}); -``` - - - -## Alternative: Method overrides - -If you cannot enable `allowCustomFetcher` or prefer a simpler approach, you can override individual methods: - -```typescript title="src/wrapper/PlantStoreClient.ts" -import { PlantStoreClient as FernClient } from "../Client"; -import * as jwt from "jsonwebtoken"; - -type FernClientOptions = ConstructorParameters[0]; -type Options = Omit & { privateKey: string }; - -export class PlantStoreClient extends FernClient { - private privateKey: string; - - constructor(options: Options) { - const { privateKey, ...clientOptions } = options; - super(clientOptions); - this.privateKey = privateKey; - } - - private generateJWT(): string { - const now = Math.floor(Date.now() / 1000); - const payload = { - iat: now, - exp: now + 15, - }; - return jwt.sign(payload, this.privateKey, { algorithm: "RS256" }); - } - - private withJWT(requestOptions?: any): any { - const token = this.generateJWT(); - return { - ...requestOptions, - headers: { - ...requestOptions?.headers, - Authorization: `Bearer ${token}`, - }, - }; - } - - // Override each method to inject JWT - async getPlant(plantId: string, requestOptions?: any) { - return super.plants.get(plantId, this.withJWT(requestOptions)); - } - - async createPlant(request: any, requestOptions?: any) { - return super.plants.create(request, this.withJWT(requestOptions)); - } - - // Override additional methods as needed... -} -``` - -This approach requires overriding each method individually, which can be tedious for large APIs. The custom fetcher approach is recommended for better maintainability. - -## Python example: Short-lived JWT signing - -For Python SDKs, you can use a similar method override pattern: - -```python title="src/wrapper/client.py" -from .client import PlantStoreClient as FernClient -import jwt -import time -from typing import Optional, Dict, Any - -class PlantStoreClient(FernClient): - def __init__(self, *, private_key: str, environment: str): - super().__init__(environment=environment) - self._private_key = private_key - - def _generate_jwt(self) -> str: - """Generate a short-lived JWT token valid for 15 seconds""" - now = int(time.time()) - payload = { - "iat": now, - "exp": now + 15, - } - return jwt.encode(payload, self._private_key, algorithm="RS256") - - def _with_jwt(self, request_options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - """Inject JWT into request options""" - token = self._generate_jwt() - options = request_options or {} - headers = options.get("headers", {}) - headers["Authorization"] = f"Bearer {token}" - options["headers"] = headers - return options - - def get_plant(self, plant_id: str, *, request_options: Optional[Dict[str, Any]] = None): - return super().plants.get(plant_id, request_options=self._with_jwt(request_options)) - - def create_plant(self, request: Any, *, request_options: Optional[Dict[str, Any]] = None): - return super().plants.create(request, request_options=self._with_jwt(request_options)) -``` - -## Other authentication patterns - -This same pattern works for other dynamic authentication scenarios: +Dynamic authentication is useful for several scenarios: +- **Short-lived JWT signing**: Generate and sign JWTs that expire after a short period (e.g., 15 seconds) for enhanced security - **OAuth token refresh**: Automatically refresh expired access tokens before each request - **HMAC (Hash-based Message Authentication Code) signing**: Sign requests with HMAC signatures based on request content -- **Rotating API keys**: Switch between multiple API keys based on rate limits -- **Time-based tokens**: Generate tokens that include timestamps or nonces +- **Rotating API keys**: Switch between multiple API keys based on rate limits or other criteria +- **Time-based tokens**: Generate tokens that include timestamps or nonces for replay protection ## Important considerations -### Custom fetcher requirements - -- **Generator configuration**: The `allowCustomFetcher` option must be enabled in your `generators.yml` for the `fetcher` parameter to be available in `BaseClientOptions` -- **Import path**: Import the default fetcher from `../core/fetcher` (or the appropriate path in your generated SDK) to wrap it with your custom logic -- **Preserve all arguments**: When wrapping the default fetcher, ensure you pass through all arguments to maintain compatibility with the SDK's internal behavior - -### Security considerations +When implementing dynamic authentication, keep these language-agnostic considerations in mind: -- **Server-side only**: Never expose private keys in browser environments. JWT signing with private keys should only be done in server-side code (Node.js, Deno, Bun) -- **Secure key storage**: Never hardcode private keys; use environment variables or secure key management systems -- **Avoid double authentication**: If your API already uses bearer token authentication in the Fern definition, be careful not to override the existing `Authorization` header. Consider using a different header name or conditionally setting the header +### Security -### Performance and concurrency +- **Secure key storage**: Never hardcode private keys or secrets; use environment variables or secure key management systems +- **Server-side only**: For JWT signing with private keys, ensure this is only done in server-side code, never in browser environments +- **Avoid double authentication**: If your API already uses bearer token authentication in the Fern definition, be careful not to override existing authentication headers -- **Token memoization**: Cache tokens to avoid regenerating them on every request. The example above caches tokens and refreshes them 2 seconds before expiration -- **Thread safety**: The memoization pattern shown is safe for concurrent requests in JavaScript's single-threaded event loop, but be mindful of race conditions in other environments -- **Grace period**: Refresh tokens slightly before they expire (e.g., 2 seconds early) to avoid edge cases where a token expires during request processing +### Performance -### Header merging - -- **Preserve existing headers**: When injecting authentication headers, always spread existing headers to avoid overwriting headers set by the SDK or user -- **Header precedence**: Headers are merged in order: SDK defaults → client options → request options → custom fetcher. Your custom fetcher runs last and can override previous headers +- **Token caching**: Cache tokens to avoid regenerating them on every request, balancing security (shorter token lifetime) with performance (less frequent regeneration) +- **Grace period**: Refresh tokens slightly before they expire to avoid edge cases where a token expires during request processing +- **Concurrency**: Be mindful of race conditions when caching tokens in multi-threaded environments ### Time synchronization -- **Clock drift**: Be aware of potential clock drift between your client and server. Consider adding tolerance to your token expiration checks -- **Timestamp precision**: Use Unix timestamps (seconds since epoch) for `iat` and `exp` claims to match JWT standards - -## Best practices - -- **Use custom fetcher for TypeScript**: The custom fetcher approach is the most maintainable solution for TypeScript SDKs when `allowCustomFetcher` is available -- **Cache tokens appropriately**: Balance between security (shorter token lifetime) and performance (less frequent regeneration) -- **Handle errors gracefully**: Implement retry logic for authentication failures and token refresh errors -- **Test thoroughly**: Ensure your wrapper handles all edge cases, including concurrent requests, token expiration, and network failures -- **Monitor token usage**: Log token generation and refresh events to help debug authentication issues in production +- **Clock drift**: Be aware of potential clock drift between your client and server; consider adding tolerance to token expiration checks +- **Timestamp precision**: Use Unix timestamps (seconds since epoch) for JWT `iat` and `exp` claims to match standards ## See also -- [Adding custom code](/sdks/capabilities/custom-code) - Learn more about extending generated SDKs +- [Adding custom code](/sdks/custom-code) - Learn more about extending generated SDKs - [TypeScript custom code](/sdks/generators/typescript/custom-code) - TypeScript-specific customization guide - [Python custom code](/sdks/generators/python/custom-code) - Python-specific customization guide diff --git a/fern/products/sdks/overview/python/dynamic-authentication.mdx b/fern/products/sdks/overview/python/dynamic-authentication.mdx new file mode 100644 index 000000000..43ce67c78 --- /dev/null +++ b/fern/products/sdks/overview/python/dynamic-authentication.mdx @@ -0,0 +1,148 @@ +--- +title: Dynamic authentication +description: Implement dynamic authentication patterns like short-lived JWT signing in Python SDKs using method overrides. +--- + +Your API may require dynamic authentication where credentials need to be generated or refreshed for each request, such as signing short-lived JWTs or rotating tokens. Python SDKs support this pattern through method overrides. + +## Method override pattern + +For Python SDKs, you can implement dynamic authentication by extending the generated client and overriding methods to inject authentication logic. + +### Example: Short-lived JWT signing + +Here's how to implement JWT signing for Python SDKs: + + + +### Create a wrapper client + +Create a custom client class that extends the generated Fern client and adds JWT signing logic: + +```python title="src/wrapper/client.py" +from .client import PlantStoreClient as FernClient +import jwt +import time +from typing import Optional, Dict, Any + +class PlantStoreClient(FernClient): + def __init__(self, *, private_key: str, environment: str): + super().__init__(environment=environment) + self._private_key = private_key + + def _generate_jwt(self) -> str: + """Generate a short-lived JWT token valid for 15 seconds""" + now = int(time.time()) + payload = { + "iat": now, + "exp": now + 15, + } + return jwt.encode(payload, self._private_key, algorithm="RS256") + + def _with_jwt(self, request_options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Inject JWT into request options""" + token = self._generate_jwt() + options = request_options or {} + headers = options.get("headers", {}) + headers["Authorization"] = f"Bearer {token}" + options["headers"] = headers + return options + + def get_plant(self, plant_id: str, *, request_options: Optional[Dict[str, Any]] = None): + return super().plants.get(plant_id, request_options=self._with_jwt(request_options)) + + def create_plant(self, request: Any, *, request_options: Optional[Dict[str, Any]] = None): + return super().plants.create(request, request_options=self._with_jwt(request_options)) + + # Override additional methods as needed... +``` + +### Export the wrapper client + +Update your `__init__.py` to export the wrapper instead of the generated client: + +```python title="src/__init__.py" +from .wrapper.client import PlantStoreClient + +__all__ = ["PlantStoreClient"] +``` + +### Add to `.fernignore` + +Protect your custom code from being overwritten: + +```diff title=".fernignore" ++ src/wrapper ++ src/__init__.py +``` + +### Use the client + +Users can use the client with automatic JWT signing: + +```python +from plant_store_sdk import PlantStoreClient +import os + +client = PlantStoreClient( + private_key=os.environ["PRIVATE_KEY"], + environment="https://api.plantstore.com" +) + +# JWT is automatically signed and injected for each request +plant = client.get_plant("monstera-123") +new_plant = client.create_plant({ + "name": "Fiddle Leaf Fig", + "species": "Ficus lyrata" +}) +``` + + + + +This approach requires overriding each method individually. You'll need to override all methods that make API calls to ensure JWT authentication is applied consistently across your SDK. + + +## Other authentication patterns + +This same pattern works for other dynamic authentication scenarios: + +- **OAuth token refresh**: Automatically refresh expired access tokens before each request +- **HMAC (Hash-based Message Authentication Code) signing**: Sign requests with HMAC signatures based on request content +- **Rotating API keys**: Switch between multiple API keys based on rate limits +- **Time-based tokens**: Generate tokens that include timestamps or nonces + +## Important considerations + +### Security considerations + +- **Secure key storage**: Never hardcode private keys; use environment variables or secure key management systems +- **Avoid double authentication**: If your API already uses bearer token authentication in the Fern definition, be careful not to override the existing `Authorization` header. Consider using a different header name or conditionally setting the header + +### Performance + +- **Token memoization**: Consider caching tokens to avoid regenerating them on every request. You can add a simple cache that refreshes tokens before they expire +- **Grace period**: Refresh tokens slightly before they expire (e.g., 2 seconds early) to avoid edge cases where a token expires during request processing + +### Header merging + +- **Preserve existing headers**: When injecting authentication headers, always merge with existing headers to avoid overwriting headers set by the SDK or user +- **Request options**: Python SDKs accept `request_options` as a keyword argument that can include custom headers + +### Time synchronization + +- **Clock drift**: Be aware of potential clock drift between your client and server. Consider adding tolerance to your token expiration checks +- **Timestamp precision**: Use Unix timestamps (seconds since epoch) for `iat` and `exp` claims to match JWT standards + +## Best practices + +- **Override all methods**: Ensure you override all API methods to maintain consistent authentication across your SDK +- **Cache tokens appropriately**: Balance between security (shorter token lifetime) and performance (less frequent regeneration) +- **Handle errors gracefully**: Implement retry logic for authentication failures and token refresh errors +- **Test thoroughly**: Ensure your wrapper handles all edge cases, including concurrent requests, token expiration, and network failures +- **Monitor token usage**: Log token generation and refresh events to help debug authentication issues in production + +## See also + +- [Adding custom code](/sdks/generators/python/custom-code) - Python-specific customization guide +- [Python configuration](/sdks/generators/python/configuration) - Full list of configuration options diff --git a/fern/products/sdks/overview/typescript/dynamic-authentication.mdx b/fern/products/sdks/overview/typescript/dynamic-authentication.mdx new file mode 100644 index 000000000..16eb130fb --- /dev/null +++ b/fern/products/sdks/overview/typescript/dynamic-authentication.mdx @@ -0,0 +1,195 @@ +--- +title: Dynamic authentication +description: Implement dynamic authentication patterns like short-lived JWT signing in TypeScript SDKs using custom fetcher middleware. +--- + +Your API may require dynamic authentication where credentials need to be generated or refreshed for each request, such as signing short-lived JWTs or rotating tokens. TypeScript SDKs support this pattern through custom fetcher middleware. + +## Custom fetcher middleware + +The recommended way to implement dynamic authentication in TypeScript SDKs is to use a custom fetcher. This acts as middleware for all requests, allowing you to inject authentication logic in a single place without overriding individual methods. + +### How it works + +When you enable `allowCustomFetcher` in your generator configuration, the generated SDK accepts a `fetcher` parameter in the client options. This fetcher wraps all HTTP requests, giving you a single injection point for authentication logic. + +### Example: Short-lived JWT signing + +Here's how to implement JWT signing with token memoization: + + + +### Enable custom fetcher in generator configuration + +Add `allowCustomFetcher: true` to your `generators.yml`: + +```yaml title="generators.yml" +default-group: local +groups: + local: + generators: + - name: fernapi/fern-typescript-node-sdk + version: 0.x.x + output: + location: local-file-system + path: ../generated/typescript + config: + allowCustomFetcher: true +``` + +### Create a custom fetcher with JWT signing + +Create a fetcher function that wraps the default fetcher and injects JWT authentication: + +```typescript title="src/wrapper/jwtFetcher.ts" +import * as jwt from "jsonwebtoken"; +import { fetcher as defaultFetcher, type FetchFunction } from "../core/fetcher"; + +export function createJwtFetcher(privateKey: string): FetchFunction { + // Cache token to avoid regenerating on every request + let cachedToken: { value: string; expiresAt: number } | undefined; + + return async (args) => { + const now = Math.floor(Date.now() / 1000); + + // Regenerate token if expired or about to expire (within 2 seconds) + if (!cachedToken || cachedToken.expiresAt - 2 <= now) { + const payload = { + iat: now, + exp: now + 15, // Token valid for 15 seconds + }; + const token = jwt.sign(payload, privateKey, { algorithm: "RS256" }); + cachedToken = { value: token, expiresAt: payload.exp }; + } + + // Inject JWT into request headers + const headers = { + ...(args.headers ?? {}), + Authorization: `Bearer ${cachedToken.value}`, + }; + + // Call the default fetcher with modified headers + return defaultFetcher({ ...args, headers }); + }; +} +``` + +### Create a wrapper client + +Extend the generated client to use the custom fetcher: + +```typescript title="src/wrapper/PlantStoreClient.ts" +import { PlantStoreClient as FernClient } from "../Client"; +import { createJwtFetcher } from "./jwtFetcher"; + +// Infer the exact options type from the generated client's constructor +type FernClientOptions = ConstructorParameters[0]; +// Accept all options except 'fetcher', and add our 'privateKey' +type Options = Omit & { privateKey: string }; + +export class PlantStoreClient extends FernClient { + constructor(options: Options) { + // Extract privateKey and pass all other options to the parent + const { privateKey, ...clientOptions } = options; + super({ + ...clientOptions, + fetcher: createJwtFetcher(privateKey), + }); + } +} +``` + + +This pattern uses `ConstructorParameters` to infer the exact options type from the generated client, ensuring compatibility with all client options (headers, timeoutInSeconds, maxRetries, etc.) without hardcoding them. This keeps the wrapper future-proof as the generator adds new options. + + +### Export the wrapper client + +Update your `index.ts` to export the wrapper instead of the generated client: + +```typescript title="src/index.ts" +export { PlantStoreClient } from "./wrapper/PlantStoreClient"; +export * from "./api"; // Export types +``` + +### Add to `.fernignore` + +Protect your custom code from being overwritten: + +```diff title=".fernignore" ++ src/wrapper ++ src/index.ts +``` + +### Use the client + +Users can use the client with automatic JWT signing on all requests: + +```typescript +import { PlantStoreClient } from "plant-store-sdk"; + +const client = new PlantStoreClient({ + privateKey: process.env.PRIVATE_KEY, + environment: "https://api.plantstore.com", +}); + +// JWT is automatically signed and injected for each request +const plant = await client.plants.get("monstera-123"); +const newPlant = await client.plants.create({ + name: "Fiddle Leaf Fig", + species: "Ficus lyrata" +}); +``` + + + +## Other authentication patterns + +This same pattern works for other dynamic authentication scenarios: + +- **OAuth token refresh**: Automatically refresh expired access tokens before each request +- **HMAC (Hash-based Message Authentication Code) signing**: Sign requests with HMAC signatures based on request content +- **Rotating API keys**: Switch between multiple API keys based on rate limits +- **Time-based tokens**: Generate tokens that include timestamps or nonces + +## Important considerations + +### Custom fetcher requirements + +- **Generator configuration**: The `allowCustomFetcher` option must be enabled in your `generators.yml` for the `fetcher` parameter to be available in `BaseClientOptions` +- **Import path**: Import the default fetcher from `../core/fetcher` (or the appropriate path in your generated SDK) to wrap it with your custom logic +- **Preserve all arguments**: When wrapping the default fetcher, ensure you pass through all arguments to maintain compatibility with the SDK's internal behavior + +### Security considerations + +- **Server-side only**: Never expose private keys in browser environments. JWT signing with private keys should only be done in server-side code (Node.js, Deno, Bun) +- **Secure key storage**: Never hardcode private keys; use environment variables or secure key management systems +- **Avoid double authentication**: If your API already uses bearer token authentication in the Fern definition, be careful not to override the existing `Authorization` header. Consider using a different header name or conditionally setting the header + +### Performance and concurrency + +- **Token memoization**: Cache tokens to avoid regenerating them on every request. The example above caches tokens and refreshes them 2 seconds before expiration +- **Thread safety**: The memoization pattern shown is safe for concurrent requests in JavaScript's single-threaded event loop, but be mindful of race conditions in other environments +- **Grace period**: Refresh tokens slightly before they expire (e.g., 2 seconds early) to avoid edge cases where a token expires during request processing + +### Header merging + +- **Preserve existing headers**: When injecting authentication headers, always spread existing headers to avoid overwriting headers set by the SDK or user +- **Header precedence**: Headers are merged in order: SDK defaults → client options → request options → custom fetcher. Your custom fetcher runs last and can override previous headers + +### Time synchronization + +- **Clock drift**: Be aware of potential clock drift between your client and server. Consider adding tolerance to your token expiration checks +- **Timestamp precision**: Use Unix timestamps (seconds since epoch) for `iat` and `exp` claims to match JWT standards + +## Best practices + +- **Cache tokens appropriately**: Balance between security (shorter token lifetime) and performance (less frequent regeneration) +- **Handle errors gracefully**: Implement retry logic for authentication failures and token refresh errors +- **Test thoroughly**: Ensure your wrapper handles all edge cases, including concurrent requests, token expiration, and network failures +- **Monitor token usage**: Log token generation and refresh events to help debug authentication issues in production + +## See also + +- [Adding custom code](/sdks/generators/typescript/custom-code) - TypeScript-specific customization guide +- [TypeScript configuration](/sdks/generators/typescript/configuration) - Full list of configuration options diff --git a/fern/products/sdks/sdks.yml b/fern/products/sdks/sdks.yml index 5a04b3f86..fea04a3b9 100644 --- a/fern/products/sdks/sdks.yml +++ b/fern/products/sdks/sdks.yml @@ -41,6 +41,9 @@ navigation: - page: Adding custom code path: ./overview/typescript/custom-code.mdx slug: custom-code + - page: Dynamic authentication + path: ./overview/typescript/dynamic-authentication.mdx + slug: dynamic-authentication - page: Enabling the serde layer path: ./overview/typescript/serde-layer.mdx slug: serde-layer @@ -66,6 +69,9 @@ navigation: - page: Adding custom code path: ./overview/python/custom-code.mdx slug: custom-code + - page: Dynamic authentication + path: ./overview/python/dynamic-authentication.mdx + slug: dynamic-authentication - changelog: ./overview/python/changelog slug: changelog - link: Customer showcase