import { Middleware } from 'openapi-fetch'; import * as zustand_middleware from 'zustand/middleware'; import * as zustand from 'zustand'; import { NextResponse } from 'next/server'; /** * @file types.ts * @description Shared type definitions for the API Visual Debugger module. * These are the public-facing types that would be exported in an NPM package. */ type ApiRequestStatus = 'pending' | 'success' | 'error'; type ApiEnvironment = 'SSR' | 'Client'; interface StackFrame { fn: string; file: string; line: string; column: string; } interface ApiRequest { /** Unique identifier for this request, used to correlate onRequest → onResponse. */ id: string; url: string; method: string; status: ApiRequestStatus; /** HTTP status code received from the server, null while pending. */ statusCode: number | null; /** Unix timestamp (ms) when the request was initiated. */ startedAt: number; /** Round-trip duration in milliseconds, null while pending. */ duration: number | null; environment: ApiEnvironment; /** Raw Error.stack string captured at the call site. */ stackTrace: string; /** * Parsed and filtered StackFrame objects derived from stackTrace. * Populated lazily on the client (parsed by the overlay on demand) — * may be `undefined` for entries just added by the interceptor. */ stackFrames?: StackFrame[]; /** * The extracted trigger function name from the call stack. * Populated lazily on the client — may be `undefined` for entries just * added by the interceptor. */ trigger?: string; } interface DebuggerConfig { /** * Explicitly enable or disable the debugger. When omitted, the resolved * default falls back to `process.env.NODE_ENV === 'development'`. */ enabled?: boolean; /** * Position of the floating badge and panel on screen. * @default 'bottom-right' */ position: 'bottom-left' | 'bottom-right'; /** * Optional list of URL filters. Each entry can be: * - A **string**: treated as a substring match against the full request URL. * - A **RegExp**: tested against the full request URL. * * If the array is empty or undefined, **all** requests are logged. * * @example ['vrid', 'checkout', /^\/v1\/products/] */ urlFilters: Array; /** * Maximum number of requests to retain in the store. * Oldest requests are discarded when the cap is reached. * @default 100 */ maxRequests: number; /** * When `true`, emits a collapsed console group with a native async stack * trace for every intercepted request. Leverages Chrome's async stack * stitching so you can trace the call back to its originating component. * @default false */ logToConsole: boolean; /** * When `true`, clears all recorded requests (both Client-side Zustand state * and the SSR server-side cache) on initial client mount — i.e. on every * full page reload the debugger starts with a clean slate. * * When `false`, the client `requests` array is persisted to `sessionStorage` * so it survives soft navigations and reloads within the same tab session. * (SSR requests are not persisted; they always come from the live server * cache polled every second.) * * @default true */ clearOnReload: boolean; } /** * @file interceptor.ts * @description Factory function that creates an openapi-fetch-compatible middleware * for the API Visual Debugger. * * Usage: * import { createDebugInterceptor } from '@/lib/api-debugger'; * client.use(createDebugInterceptor({ position: 'bottom-right', urlFilters: ['vrid'] })); * * CORS Safety: * We intentionally avoid writing the tracking ID to HTTP headers. * Instead we mutate the in-memory Request object: (request as any)._debugId = id * This prevents the browser from sending a CORS preflight for a custom header. * * SSR / Client routing: * - typeof window === 'undefined' → server environment → writes to serverStore * - typeof window !== 'undefined' → browser environment → writes to Zustand store * * The import of `serverStore` at the top of this file is intentionally static. * Next.js compiles separate server and client bundles; because this file is used * server-side (via Server Components / server actions) the import is safe and * will NOT be included in the browser bundle. */ declare function createDebugInterceptor(config?: Partial): Middleware; type ApiDebuggerState = { /** Live Client-side request log, newest first. Capped at config.maxRequests. */ requests: ApiRequest[]; /** * SSR requests polled from /api/dev/api-debugger. * Replaced wholesale on each successful poll tick. */ ssrRequests: ApiRequest[]; /** Active configuration. Seeded by createDebugInterceptor() on mount. */ config: DebuggerConfig; }; type ApiDebuggerActions = { /** * Adds a new Client-side request entry to the log. * Respects the maxRequests cap from config. */ addRequest: (request: ApiRequest) => void; /** * Patches an existing Client-side request by ID (used in onResponse to update * status, statusCode, and duration). */ updateRequest: (id: string, patch: Partial>) => void; /** * Applies a batch of adds and updates in a single `set()` call. * * This is the hot-path entry point used by the interceptor's * microtask-coalesced flush. It replaces N discrete `addRequest` / * `updateRequest` calls (each of which would trigger its own subscriber * notification and `persist` serialization) with a single write, which * is what keeps the store cheap under bursts of concurrent openapi-fetch * calls. */ applyBatch: (adds: ApiRequest[], updates: Array<{ id: string; patch: Partial>; }>) => void; /** * Replaces the SSR request list with a fresh snapshot from the server. * Called by the polling effect in . */ setSsrRequests: (requests: ApiRequest[]) => void; /** * Clears all recorded Client-side requests. * The overlay is responsible for also sending DELETE /api/dev/api-debugger * to clear the server-side cache and reset ssrRequests. */ clearRequests: () => void; /** * Merges a partial config patch into the active configuration. * Called by createDebugInterceptor() to seed initial config from call-site options. */ setConfig: (patch: Partial) => void; }; /** * The store is always wrapped in the `persist` middleware, but rehydration is * gated on `config.clearOnReload`: * - `clearOnReload: true` (default) — persisted state is dropped on every * fresh page load, so the debugger starts empty. * - `clearOnReload: false` — persisted `requests` are restored from * sessionStorage so they survive reloads within the same tab. * * The active `clearOnReload` value is read from a globalThis handshake set * by `createDebugInterceptor` before the store is first hydrated. This avoids * a circular dependency (interceptor → store → interceptor) at module load. */ declare const useApiDebuggerStore: zustand.UseBoundStore, "setState" | "persist"> & { setState(partial: (ApiDebuggerState & ApiDebuggerActions) | Partial | ((state: ApiDebuggerState & ApiDebuggerActions) => (ApiDebuggerState & ApiDebuggerActions) | Partial), replace?: false | undefined): unknown; setState(state: (ApiDebuggerState & ApiDebuggerActions) | ((state: ApiDebuggerState & ApiDebuggerActions) => ApiDebuggerState & ApiDebuggerActions), replace: true): unknown; persist: { setOptions: (options: Partial>) => void; clearStorage: () => void; rehydrate: () => Promise | void; hasHydrated: () => boolean; onHydrate: (fn: (state: ApiDebuggerState & ApiDebuggerActions) => void) => () => void; onFinishHydration: (fn: (state: ApiDebuggerState & ApiDebuggerActions) => void) => () => void; getOptions: () => Partial>; }; }>; /** * Factory that returns the { GET, DELETE } handlers for the SSR debug bridge. * * @param options.enabled - When provided, explicitly toggles the route on/off. * When omitted, falls back to * `process.env.NODE_ENV === 'development'`. */ declare function createApiDebuggerRouteHandler(options?: { enabled?: boolean; }): { GET: () => NextResponse<{ error: string; }> | NextResponse; DELETE: () => NextResponse; }; export { type ApiEnvironment, type ApiRequest, type ApiRequestStatus, type DebuggerConfig, type StackFrame, createApiDebuggerRouteHandler, createDebugInterceptor, useApiDebuggerStore };