UNPKG

@nhost/nhost-js

Version:

Nhost JavaScript SDK

1 lines 23.9 kB
{"version":3,"file":"fetch-B_MCWy66.cjs","names":[],"sources":["../src/fetch/fetch.ts","../src/fetch/middlewareAttachAccessToken.ts","../src/fetch/middlewareSessionRefresh.ts","../src/fetch/middlewareUpdateSessionFromResponse.ts","../src/fetch/middlewareWithAdminSession.ts","../src/fetch/middlewareWithHeaders.ts","../src/fetch/middlewareWithRole.ts"],"sourcesContent":["/**\n * Type definition for a fetch-like function.\n * Takes the same parameters as fetch and returns the same type.\n * This allows middleware to intercept and modify requests and responses.\n */\nexport type FetchFunction = (\n url: string,\n options?: RequestInit,\n) => Promise<Response>;\n\n/**\n * Type definition for a chain function (middleware).\n * Takes a fetch-like function and returns another fetch-like function.\n *\n * Chain functions can be used to implement:\n * - Authentication token handling\n * - Error handling and retry logic\n * - Request and response transformations\n * - Logging and metrics\n */\nexport type ChainFunction = (next: FetchFunction) => FetchFunction;\n\n/**\n * Creates an enhanced fetch function using a chain of middleware functions.\n *\n * The fetch chain executes in the order of the array, with each middleware\n * wrapping the next one in the chain. This allows each middleware to\n * intercept both the request (before calling next) and the response\n * (after calling next).\n *\n * @example\n * ```typescript\n * // Simple logging middleware\n * const loggingMiddleware: ChainFunction = (next) => {\n * return async (url, options) => {\n * console.log(`Request to ${url}`);\n * const response = await next(url, options);\n * console.log(`Response from ${url}: ${response.status}`);\n * return response;\n * };\n * };\n *\n * const enhancedFetch = createEnhancedFetch([loggingMiddleware]);\n * const response = await enhancedFetch('https://api.example.com/data');\n * ```\n *\n * @param chainFunctions - Array of chain functions to apply in order\n * @returns Enhanced fetch function with all middleware applied\n */\nexport function createEnhancedFetch(\n chainFunctions: ChainFunction[] = [],\n): FetchFunction {\n // Build the chain starting with vanilla fetch, but apply functions in reverse\n // to achieve the desired execution order\n return chainFunctions.reduceRight(\n (nextInChain, chainFunction) => chainFunction(nextInChain),\n fetch as FetchFunction,\n );\n}\n\n/**\n * Interface representing a structured API response.\n *\n * This interface provides a consistent structure for responses across the SDK,\n * offering access to the parsed response body along with status and headers.\n *\n * @template T - The type of the response body\n */\nexport interface FetchResponse<T> {\n /** The parsed response body */\n body: T;\n /** HTTP status code of the response */\n status: number;\n /** Response headers */\n headers: Headers;\n}\n\nfunction extractMessage(body: unknown): string {\n if (body && typeof body === 'string') {\n return body;\n }\n\n if (body && typeof body === 'object') {\n const typedBody = body as Record<string, unknown>;\n\n if ('message' in typedBody && typeof typedBody['message'] === 'string') {\n return typedBody['message'];\n }\n\n if ('error' in typedBody && typeof typedBody['error'] === 'string') {\n return typedBody['error'];\n }\n\n if (\n 'error' in typedBody &&\n typedBody['error'] &&\n typeof typedBody['error'] === 'object'\n ) {\n const error = typedBody['error'] as Record<string, unknown>;\n if ('message' in error && typeof error['message'] === 'string') {\n return error['message'];\n }\n }\n\n if ('errors' in typedBody && Array.isArray(typedBody['errors'])) {\n const messages = (typedBody['errors'] as unknown[])\n .filter(\n (error): error is { message: string } =>\n typeof error === 'object' &&\n error !== null &&\n 'message' in error &&\n typeof (error as { message: unknown })['message'] === 'string',\n )\n .map((error) => error['message']);\n\n if (messages.length > 0) {\n return messages.join(', ');\n }\n }\n }\n\n return 'An unexpected error occurred';\n}\n\n/**\n * Error class for representing fetch operation failures.\n *\n * This class extends the standard Error to include additional\n * information about failed requests, including the response body,\n * status code, and headers. The error message is automatically\n * extracted from common error response formats.\n *\n * @template T - The type of the response body\n */\nexport class FetchError<T = unknown> extends Error {\n /** The original response body */\n body: T;\n /** HTTP status code of the failed response */\n status: number;\n /** Response headers */\n headers: Headers;\n\n /**\n * Creates a new FetchError instance\n *\n * @param body - The response body from the failed request\n * @param status - The HTTP status code\n * @param headers - The response headers\n */\n constructor(body: T, status: number, headers: Headers) {\n super(extractMessage(body));\n this.body = body;\n this.status = status;\n this.headers = headers;\n }\n}\n","/**\n * Authorization token attachment middleware for the Nhost SDK.\n *\n * This module provides middleware functionality to automatically attach\n * authorization tokens to outgoing API requests, ensuring the client\n * is properly authenticated.\n */\n\nimport type { Session } from '../auth';\nimport type { SessionStorage } from '../session/storage';\nimport type { ChainFunction, FetchFunction } from './fetch';\n\n/**\n * Creates a fetch middleware that adds the Authorization header with the current access token.\n *\n * This middleware:\n * 1. Gets the current session from storage\n * 2. Adds the authorization header with the access token to outgoing requests\n *\n * This middleware should be used after the refresh middleware in the chain to\n * ensure the most recent token is used.\n *\n * @param storage - Storage implementation for retrieving session data\n * @returns A middleware function that adds Authorization headers\n */\nexport const attachAccessTokenMiddleware =\n (storage: SessionStorage): ChainFunction =>\n (next: FetchFunction): FetchFunction =>\n async (url: string, options: RequestInit = {}): Promise<Response> => {\n const headers = new Headers(options.headers || {});\n\n // Skip if Authorization header is already set\n if (headers.has('Authorization')) {\n return next(url, options);\n }\n\n // Get current session from storage\n const session = storage.get();\n\n if (session?.accessToken) {\n // Add authorization header\n const newOptions = {\n ...options,\n headers: addAuthorizationHeader(headers, session),\n };\n\n // Continue with the fetch chain\n return next(url, newOptions);\n }\n\n // No session or no access token, continue without authorization\n return next(url, options);\n };\n\n/**\n * Adds the Authorization header with the access token to the request headers\n *\n * @param headers - Original request headers\n * @param session - Current session containing the access token\n * @returns Modified headers with Authorization header\n */\nfunction addAuthorizationHeader(headers: Headers, session: Session): Headers {\n if (session.accessToken) {\n headers.set('Authorization', `Bearer ${session.accessToken}`);\n }\n return headers;\n}\n","/**\n * Auth token refresh middleware for the Nhost SDK.\n *\n * This module provides middleware functionality to automatically refresh\n * authentication tokens before they expire, ensuring seamless API access\n * without requiring manual token refresh by the application.\n */\n\nimport type { Client } from '../auth';\nimport { refreshSession } from '../session/refreshSession';\nimport type { SessionStorage } from '../session/storage';\nimport type { ChainFunction, FetchFunction } from './fetch';\n\n/**\n * Creates a fetch middleware that automatically refreshes authentication tokens.\n *\n * This middleware:\n * 1. Checks if the current token is about to expire\n * 2. If so, uses the refresh token to obtain a new access token\n *\n * The middleware handles token refresh transparently, so the application\n * doesn't need to manually refresh tokens.\n *\n * @param auth - Auth API client for token refresh operations\n * @param storage - Storage implementation for persisting session data\n * @param options - Configuration options for token refresh behavior\n * @param options.marginSeconds - Number of seconds before token expiration to trigger a refresh, default is 60 seconds\n * @returns A middleware function that can be used in the fetch chain\n */\nexport const sessionRefreshMiddleware = (\n auth: Client,\n storage: SessionStorage,\n options?: {\n marginSeconds?: number;\n },\n): ChainFunction => {\n const { marginSeconds = 60 } = options || {};\n\n // Create and return the chain function\n return (next: FetchFunction): FetchFunction =>\n async (url: string, options: RequestInit = {}): Promise<Response> => {\n // Skip token handling for certain requests\n if (shouldSkipTokenHandling(url, options)) {\n return next(url, options);\n }\n\n try {\n await refreshSession(auth, storage, marginSeconds);\n } catch {\n // do nothing, we still want to call the next function\n }\n return next(url, options);\n };\n};\n\n/**\n * Determines if token handling should be skipped for this request\n *\n * @param url - Request URL\n * @param options - Request options\n * @returns True if token handling should be skipped, false otherwise\n */\nfunction shouldSkipTokenHandling(url: string, options: RequestInit): boolean {\n const headers = new Headers(options.headers || {});\n\n // If Authorization header is explicitly set, skip token handling\n if (headers.has('Authorization')) {\n return true;\n }\n\n // If calling the token endpoint, skip to avoid infinite loops\n if (url.endsWith('/v1/token')) {\n return true;\n }\n\n return false;\n}\n","/**\n * Session response middleware for the Nhost SDK.\n *\n * This module provides middleware functionality to automatically extract\n * and persist session information from authentication responses, ensuring\n * that new sessions are properly stored after sign-in operations.\n */\n\nimport type { Session, SessionPayload } from '../auth';\nimport type { SessionStorage } from '../session/storage';\nimport type { ChainFunction } from './fetch';\n\n/**\n * Creates a fetch middleware that automatically extracts and stores session data from API responses.\n *\n * This middleware:\n * 1. Monitors responses from authentication-related endpoints\n * 2. Extracts session information when present\n * 3. Stores the session in the provided storage implementation\n * 4. Handles session removal on sign-out\n *\n * This ensures that session data is always up-to-date in storage after operations\n * that create or invalidate sessions.\n *\n * @param storage - Storage implementation for persisting session data\n * @returns A middleware function that can be used in the fetch chain\n */\nexport const updateSessionFromResponseMiddleware = (\n storage: SessionStorage,\n): ChainFunction => {\n /**\n * Helper function to extract session data from various response formats\n *\n * @param body - Response data to extract session from\n * @returns Session object if found, null otherwise\n */\n const sessionExtractor = (\n body: Session | SessionPayload | string,\n ): Session | null => {\n if (typeof body === 'string') {\n return null;\n }\n\n if ('session' in body) {\n // SessionPayload\n return body.session || null;\n }\n\n if ('accessToken' in body && 'refreshToken' in body && 'user' in body) {\n // Session\n return body;\n }\n\n return null;\n };\n\n return (next: (url: string, options?: RequestInit) => Promise<Response>) =>\n async (url: string, options?: RequestInit) => {\n // Call the next middleware in the chain\n const response = await next(url, options);\n\n try {\n // Check if this is a logout request\n if (url.endsWith('/signout')) {\n // Remove session on sign-out\n storage.remove();\n return response;\n }\n\n // A successful password change revokes every refresh token for the\n // user on the server (including the caller's), so the locally stored\n // session is no longer valid. Clear it so the user is treated as\n // signed out on the client too.\n if (url.endsWith('/user/password') && response.ok) {\n storage.remove();\n return response;\n }\n\n // Check if this is an auth-related endpoint that might return session data\n if (\n url.endsWith('/token') ||\n url.includes('/token/exchange') ||\n url.includes('/signin/') ||\n url.includes('/signup/')\n ) {\n // Clone the response to avoid consuming it\n const clonedResponse = response.clone();\n\n // Parse the JSON data\n const body = (await clonedResponse.json().catch(() => null)) as\n | Session\n | SessionPayload;\n\n if (body) {\n // Extract session data from response using provided extractor\n const session = sessionExtractor(body);\n\n // If session data is found, store it\n if (session?.accessToken && session.refreshToken) {\n storage.set(session);\n }\n }\n }\n } catch (error) {\n console.warn('Error in session response middleware:', error);\n }\n\n // Return the original response\n return response;\n };\n};\n","/**\n * Admin session middleware for the Nhost SDK.\n *\n * This module provides middleware functionality to automatically attach\n * Hasura admin secret for admin permissions in requests.\n */\n\nimport type { ChainFunction, FetchFunction } from './fetch';\n\n/**\n * Configuration options for admin session middleware\n */\nexport interface AdminSessionOptions {\n /**\n * Hasura admin secret for elevated permissions (sets x-hasura-admin-secret header)\n */\n adminSecret: string;\n\n /**\n * Hasura role to use for the request (sets x-hasura-role header)\n */\n role?: string;\n\n /**\n * Additional Hasura session variables to attach to requests.\n * Keys will be automatically prefixed with 'x-hasura-' if not already present.\n *\n * @example\n * ```ts\n * {\n * 'user-id': '123',\n * 'org-id': '456'\n * }\n * // Results in headers:\n * // x-hasura-user-id: 123\n * // x-hasura-org-id: 456\n * ```\n */\n sessionVariables?: Record<string, string>;\n}\n\n/**\n * Creates a fetch middleware that attaches the Hasura admin secret and optional session variables to requests.\n *\n * This middleware:\n * 1. Sets the x-hasura-admin-secret header, which grants full admin access to Hasura\n * 2. Optionally sets the x-hasura-role header if a role is provided\n * 3. Optionally sets additional x-hasura-* headers for custom session variables\n *\n * **Security Warning**: Never use this middleware in client-side code or expose\n * the admin secret to end users. Admin secrets grant unrestricted access to your\n * entire database. This should only be used in trusted server-side environments.\n *\n * The middleware preserves request-specific headers when they conflict with the\n * admin session configuration.\n *\n * @param options - Admin session options including admin secret, role, and session variables\n * @returns A middleware function that can be used in the fetch chain\n *\n * @example\n * ```ts\n * // Create middleware with admin secret only\n * const adminMiddleware = withAdminSessionMiddleware({\n * adminSecret: process.env.NHOST_ADMIN_SECRET\n * });\n *\n * // Create middleware with admin secret and role\n * const adminUserMiddleware = withAdminSessionMiddleware({\n * adminSecret: process.env.NHOST_ADMIN_SECRET,\n * role: 'user'\n * });\n *\n * // Create middleware with admin secret, role, and custom session variables\n * const fullMiddleware = withAdminSessionMiddleware({\n * adminSecret: process.env.NHOST_ADMIN_SECRET,\n * role: 'user',\n * sessionVariables: {\n * 'user-id': '123',\n * 'org-id': '456'\n * }\n * });\n *\n * // Use with createCustomClient for an admin client\n * const adminClient = createCustomClient({\n * subdomain: 'myproject',\n * region: 'eu-central-1',\n * chainFunctions: [adminMiddleware]\n * });\n * ```\n */\nexport const withAdminSessionMiddleware =\n (options: AdminSessionOptions): ChainFunction =>\n (next: FetchFunction): FetchFunction =>\n async (url: string, requestOptions: RequestInit = {}): Promise<Response> => {\n const headers = new Headers(requestOptions.headers || {});\n\n // Set x-hasura-admin-secret if not already present\n if (!headers.has('x-hasura-admin-secret')) {\n headers.set('x-hasura-admin-secret', options.adminSecret);\n }\n\n // Set x-hasura-role if provided and not already present\n if (options.role && !headers.has('x-hasura-role')) {\n headers.set('x-hasura-role', options.role);\n }\n\n // Set custom session variables\n if (options.sessionVariables) {\n for (const [key, value] of Object.entries(options.sessionVariables)) {\n // Ensure the key has the x-hasura- prefix\n const headerKey = key.startsWith('x-hasura-') ? key : `x-hasura-${key}`;\n\n // Only set if not already present in the request\n if (!headers.has(headerKey)) {\n headers.set(headerKey, value);\n }\n }\n }\n\n return next(url, { ...requestOptions, headers });\n };\n","/**\n * Headers middleware for the Nhost SDK.\n *\n * This module provides middleware functionality to automatically attach\n * default headers to all outgoing requests, while allowing request-specific\n * headers to take precedence.\n */\n\nimport type { ChainFunction, FetchFunction } from './fetch';\n\n/**\n * Creates a fetch middleware that attaches default headers to requests.\n *\n * This middleware:\n * 1. Merges default headers with request-specific headers\n * 2. Preserves request-specific headers when they conflict with defaults\n *\n * The middleware ensures consistent headers across requests while allowing\n * individual requests to override defaults as needed.\n *\n * @param defaultHeaders - Default headers to attach to all requests\n * @returns A middleware function that can be used in the fetch chain\n */\nexport const withHeadersMiddleware =\n (defaultHeaders: HeadersInit): ChainFunction =>\n (next: FetchFunction): FetchFunction =>\n async (url: string, options: RequestInit = {}): Promise<Response> => {\n const headers = new Headers(options.headers || {});\n const defaults = new Headers(defaultHeaders);\n\n defaults.forEach((value, key) => {\n if (!headers.has(key)) {\n headers.set(key, value);\n }\n });\n\n return next(url, { ...options, headers });\n };\n","/**\n * Role middleware for the Nhost SDK.\n *\n * This module provides middleware functionality to automatically set\n * the Hasura role for all requests. This is useful when you want to\n * make requests as a specific role without using the admin secret.\n */\n\nimport type { ChainFunction, FetchFunction } from './fetch';\n\n/**\n * Creates a fetch middleware that sets the Hasura role header.\n *\n * This middleware sets the x-hasura-role header for all requests, allowing\n * you to specify which role's permissions should be used. This works with\n * authenticated sessions where the user has access to the specified role.\n *\n * Unlike `withAdminSessionMiddleware`, this does not bypass permission rules\n * but instead uses the permission rules defined for the specified role.\n *\n * The middleware preserves request-specific headers when they conflict with\n * the role configuration.\n *\n * @param role - The Hasura role to use for requests\n * @returns A middleware function that can be used in the fetch chain\n *\n * @example\n * ```ts\n * // Use with createClient to default all requests to a specific role\n * const nhost = createClient({\n * subdomain: 'myproject',\n * region: 'eu-central-1',\n * chainFunctions: [withRoleMiddleware('moderator')]\n * });\n *\n * // Use with createServerClient for server-side requests\n * const serverNhost = createServerClient({\n * subdomain: 'myproject',\n * region: 'eu-central-1',\n * storage: myServerStorage,\n * chainFunctions: [withRoleMiddleware('moderator')]\n * });\n * ```\n */\nexport const withRoleMiddleware =\n (role: string): ChainFunction =>\n (next: FetchFunction): FetchFunction =>\n async (url: string, requestOptions: RequestInit = {}): Promise<Response> => {\n const headers = new Headers(requestOptions.headers || {});\n\n // Set x-hasura-role if not already present\n if (!headers.has('x-hasura-role')) {\n headers.set('x-hasura-role', role);\n }\n\n return next(url, { ...requestOptions, headers });\n };\n"],"mappings":"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"}