@spfn/core
Version:
SPFN Framework Core - File-based routing, transactions, repository pattern
558 lines (548 loc) • 14.9 kB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
/**
* SPFN Next.js Proxy Interceptor Types
*/
/**
* Request Interceptor Context
*
* Available before calling SPFN API
*/
interface RequestInterceptorContext {
/**
* Request path (e.g., '/_auth/login')
*/
path: string;
/**
* HTTP method (e.g., 'POST')
*/
method: string;
/**
* Request headers (mutable)
*/
headers: Record<string, string>;
/**
* Request body (mutable)
*/
body?: any;
/**
* Query parameters from original request
*/
query: Record<string, string | string[]>;
/**
* Cookies from Next.js request
*/
cookies: Map<string, string>;
/**
* Original Next.js request
*/
request: NextRequest;
/**
* Metadata for sharing data between interceptors
*/
metadata: Record<string, any>;
}
/**
* Response Interceptor Context
*
* Available after SPFN API responds
*/
interface ResponseInterceptorContext {
/**
* Request path
*/
path: string;
/**
* HTTP method
*/
method: string;
/**
* Original request data (immutable)
*/
request: {
headers: Record<string, string>;
body?: any;
};
/**
* Response data (mutable)
*/
response: {
status: number;
statusText: string;
headers: Headers;
body: any;
};
/**
* Cookies to set in response
*
* @example
* ```typescript
* ctx.setCookies.push({
* name: 'session',
* value: 'xxx',
* options: { httpOnly: true, maxAge: 3600 }
* });
* ```
*/
setCookies: Array<{
name: string;
value: string;
options?: {
httpOnly?: boolean;
secure?: boolean;
sameSite?: 'strict' | 'lax' | 'none';
maxAge?: number;
path?: string;
domain?: string;
};
}>;
/**
* Metadata shared from request interceptors
*/
metadata: Record<string, any>;
}
/**
* Request Interceptor Function
*
* @param context - Request context (mutable)
* @param next - Call to continue to next interceptor
*
* @example
* ```typescript
* const interceptor: RequestInterceptor = async (ctx, next) => {
* // Modify headers
* ctx.headers['Authorization'] = 'Bearer token';
*
* // Store data for response interceptor
* ctx.metadata.userId = '123';
*
* // Continue to next interceptor
* await next();
* };
* ```
*/
type RequestInterceptor = (context: RequestInterceptorContext, next: () => Promise<void>) => Promise<void>;
/**
* Response Interceptor Function
*
* @param context - Response context (mutable)
* @param next - Call to continue to next interceptor
*
* @example
* ```typescript
* const interceptor: ResponseInterceptor = async (ctx, next) => {
* // Modify response body
* ctx.response.body = { ...ctx.response.body, extra: 'data' };
*
* // Set cookie
* ctx.setCookies.push({
* name: 'session',
* value: 'xxx',
* options: { httpOnly: true }
* });
*
* // Continue to next interceptor
* await next();
* };
* ```
*/
type ResponseInterceptor = (context: ResponseInterceptorContext, next: () => Promise<void>) => Promise<void>;
/**
* Interceptor Rule
*
* Defines when and how to intercept requests/responses
*/
interface InterceptorRule {
/**
* Path pattern to match
*
* - String with wildcards: '/_auth/*', '/users/:id'
* - RegExp: /^\/_auth\/.+$/
* - '*' matches all paths
*
* @example
* ```typescript
* pathPattern: '/_auth/*' // matches /_auth/login, /_auth/register
* pathPattern: '/users/:id' // matches /users/123, /users/456
* pathPattern: /^\/_auth\/.+$/ // regex match
* pathPattern: '*' // matches all paths
* ```
*/
pathPattern: string | RegExp;
/**
* HTTP method(s) to match (optional)
*
* - Single method: 'POST'
* - Multiple methods: ['POST', 'PUT']
* - Omit to match all methods
*
* @default undefined (matches all methods)
*/
method?: string | string[];
/**
* Request interceptor
*
* Called before SPFN API request
*/
request?: RequestInterceptor;
/**
* Response interceptor
*
* Called after SPFN API response
*/
response?: ResponseInterceptor;
}
/**
* Proxy Configuration
*/
interface ProxyConfig {
/**
* SPFN API base URL
*
* @default process.env.SERVER_API_URL || process.env.SPFN_API_URL || 'http://localhost:8790'
*/
apiUrl?: string;
/**
* Additional custom interceptors
*
* These are executed after auto-discovered interceptors
*
* Executed in order: first registered -> last registered
*/
interceptors?: InterceptorRule[];
/**
* Enable automatic interceptor discovery from registry
*
* When enabled, all interceptors registered via registerInterceptors()
* are automatically applied to the proxy.
*
* @default true
*/
autoDiscoverInterceptors?: boolean;
/**
* Disable interceptors from specific packages
*
* Use this to exclude auto-discovered interceptors from certain packages
* when you want to provide custom implementations.
*
* @example ['auth', 'storage']
*/
disableAutoInterceptors?: string[];
/**
* Enable debug logging
*
* @default false
*/
debug?: boolean;
}
/**
* SPFN Next.js API Route Proxy with Interceptor Pattern
*
* Automatically proxies requests to SPFN API server with:
* - Cookie forwarding
* - Request/Response interceptors
* - Flexible header manipulation
*
* Usage:
* ```typescript
* // Basic usage (no interceptors)
* // app/api/actions/[...path]/route.ts
* export { GET, POST, PUT, DELETE, PATCH } from '@spfn/core/nextjs';
*
* // With interceptors
* import { createProxy } from '@spfn/core/nextjs';
*
* export const { GET, POST } = createProxy({
* interceptors: [
* {
* pathPattern: '/_auth/*',
* request: async (ctx, next) => {
* ctx.headers['Authorization'] = 'Bearer token';
* await next();
* }
* }
* ]
* });
* ```
*/
/**
* Create proxy with custom configuration and interceptors
*
* @param config - Proxy configuration with interceptors
* @returns HTTP method handlers for Next.js API routes
*
* @example
* ```typescript
* // app/api/actions/[...path]/route.ts
* import { createProxy } from '@spfn/core/nextjs';
*
* export const { GET, POST, PUT, DELETE, PATCH } = createProxy({
* apiUrl: 'http://localhost:8790',
* debug: true,
* interceptors: [
* {
* pathPattern: '/_auth/*',
* method: 'POST',
* request: async (ctx, next) => {
* const session = await getSession();
* if (session) {
* ctx.headers['Authorization'] = `Bearer ${session.token}`;
* }
* await next();
* },
* response: async (ctx, next) => {
* if (ctx.response.status === 200) {
* ctx.setCookies.push({
* name: 'session',
* value: ctx.response.body.token,
* options: { httpOnly: true, maxAge: 3600 }
* });
* }
* await next();
* }
* }
* ]
* });
* ```
*/
declare function createProxy(config?: ProxyConfig): {
GET: (request: NextRequest, context: {
params: Promise<{
path: string[];
}> | {
path: string[];
};
}) => Promise<NextResponse<unknown>>;
POST: (request: NextRequest, context: {
params: Promise<{
path: string[];
}> | {
path: string[];
};
}) => Promise<NextResponse<unknown>>;
PUT: (request: NextRequest, context: {
params: Promise<{
path: string[];
}> | {
path: string[];
};
}) => Promise<NextResponse<unknown>>;
PATCH: (request: NextRequest, context: {
params: Promise<{
path: string[];
}> | {
path: string[];
};
}) => Promise<NextResponse<unknown>>;
DELETE: (request: NextRequest, context: {
params: Promise<{
path: string[];
}> | {
path: string[];
};
}) => Promise<NextResponse<unknown>>;
};
declare const GET: (request: NextRequest, context: {
params: Promise<{
path: string[];
}> | {
path: string[];
};
}) => Promise<NextResponse<unknown>>;
declare const POST: (request: NextRequest, context: {
params: Promise<{
path: string[];
}> | {
path: string[];
};
}) => Promise<NextResponse<unknown>>;
declare const PUT: (request: NextRequest, context: {
params: Promise<{
path: string[];
}> | {
path: string[];
};
}) => Promise<NextResponse<unknown>>;
declare const PATCH: (request: NextRequest, context: {
params: Promise<{
path: string[];
}> | {
path: string[];
};
}) => Promise<NextResponse<unknown>>;
declare const DELETE: (request: NextRequest, context: {
params: Promise<{
path: string[];
}> | {
path: string[];
};
}) => Promise<NextResponse<unknown>>;
/**
* Global Interceptor Registry
*
* Allows packages to automatically register their interceptors
* for Next.js proxy without manual configuration.
*/
/**
* Global interceptor registry
*
* Packages register their interceptors on import,
* and proxy automatically discovers and applies them.
*/
declare class InterceptorRegistry {
private interceptors;
/**
* Register interceptors for a package
*
* @param packageName - Unique package identifier (e.g., 'auth', 'storage')
* @param interceptors - Array of interceptor rules
*
* @example
* ```typescript
* registerInterceptors('auth', [
* {
* pathPattern: '/_auth/*',
* request: async (ctx, next) => { ... }
* }
* ]);
* ```
*/
register(packageName: string, interceptors: InterceptorRule[]): void;
/**
* Get all registered interceptors
*
* @param exclude - Package names to exclude
* @returns Flat array of all interceptor rules
*/
getAll(exclude?: string[]): InterceptorRule[];
/**
* Get interceptors for specific package
*
* @param packageName - Package identifier
* @returns Interceptor rules or undefined
*/
get(packageName: string): InterceptorRule[] | undefined;
/**
* Get list of registered package names
*/
getPackageNames(): string[];
/**
* Check if package has registered interceptors
*/
has(packageName: string): boolean;
/**
* Unregister interceptors for a package
*
* @param packageName - Package identifier
*/
unregister(packageName: string): void;
/**
* Clear all registered interceptors
*
* Useful for testing
*/
clear(): void;
/**
* Get total count of registered interceptors
*/
count(): number;
}
/**
* Global singleton registry instance
*/
declare const interceptorRegistry: InterceptorRegistry;
/**
* Register interceptors for a package
*
* This should be called during package initialization (on import).
* The interceptors will be automatically applied by the Next.js proxy.
*
* @param packageName - Unique package identifier (e.g., 'auth', 'storage')
* @param interceptors - Array of interceptor rules
*
* @example
* ```typescript
* // packages/auth/src/adapters/nextjs/interceptors/index.ts
* import { registerInterceptors } from '@spfn/core/client/nextjs';
*
* const authInterceptors = [
* {
* pathPattern: '/_auth/*',
* request: async (ctx, next) => {
* // Add JWT token
* ctx.headers['Authorization'] = 'Bearer token';
* await next();
* }
* }
* ];
*
* // Auto-register on import
* registerInterceptors('auth', authInterceptors);
* ```
*/
declare function registerInterceptors(packageName: string, interceptors: InterceptorRule[]): void;
/**
* SPFN Next.js Proxy Interceptor Execution Engine
*/
/**
* Check if path matches pattern
*
* Supports:
* - Wildcards: '/_auth/*' matches '/_auth/login'
* - Path params: '/users/:id' matches '/users/123'
* - RegExp: /^\/_auth\/.+$/ matches '/_auth/login'
* - Exact match: '/_auth/login' matches '/_auth/login'
* - All: '*' matches any path
*
* @param path - Request path to test
* @param pattern - Pattern to match against
* @returns True if path matches pattern
*/
declare function matchPath(path: string, pattern: string | RegExp): boolean;
/**
* Check if method matches pattern
*
* @param method - Request method (e.g., 'POST')
* @param pattern - Method pattern (e.g., 'POST' or ['POST', 'PUT'])
* @returns True if method matches pattern
*/
declare function matchMethod(method: string, pattern?: string | string[]): boolean;
/**
* Filter interceptors that match the request
*
* @param rules - All interceptor rules
* @param path - Request path
* @param method - Request method
* @returns Matched interceptors
*/
declare function filterMatchingInterceptors(rules: InterceptorRule[], path: string, method: string): InterceptorRule[];
/**
* Execute request interceptors in chain
*
* Interceptors are executed in order:
* 1. First registered interceptor
* 2. Second registered interceptor
* 3. ... and so on
*
* Each interceptor must call next() to continue the chain.
* If next() is not called, the chain stops and remaining interceptors are skipped.
*
* @param context - Request interceptor context
* @param interceptors - Interceptors to execute
*/
declare function executeRequestInterceptors(context: RequestInterceptorContext, interceptors: RequestInterceptor[]): Promise<void>;
/**
* Execute response interceptors in chain
*
* Interceptors are executed in order:
* 1. First registered interceptor
* 2. Second registered interceptor
* 3. ... and so on
*
* Each interceptor must call next() to continue the chain.
* If next() is not called, the chain stops and remaining interceptors are skipped.
*
* @param context - Response interceptor context
* @param interceptors - Interceptors to execute
*/
declare function executeResponseInterceptors(context: ResponseInterceptorContext, interceptors: ResponseInterceptor[]): Promise<void>;
export { DELETE, GET, type InterceptorRule, PATCH, POST, PUT, type ProxyConfig, type RequestInterceptor, type RequestInterceptorContext, type ResponseInterceptor, type ResponseInterceptorContext, createProxy, executeRequestInterceptors, executeResponseInterceptors, filterMatchingInterceptors, interceptorRegistry, matchMethod, matchPath, registerInterceptors };