@spfn/core 0.1.0-alpha.85 → 0.1.0-alpha.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,24 @@ interface CallOptions<TContract extends RouteContract> {
36
36
  body?: InferContract<TContract>['body'];
37
37
  headers?: Record<string, string>;
38
38
  baseUrl?: string;
39
+ /**
40
+ * Additional fetch options (extends RequestInit)
41
+ *
42
+ * Can be used for environment-specific options like Next.js cache/next
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * // Next.js time-based revalidation
47
+ * { fetchOptions: { next: { revalidate: 60 } } }
48
+ *
49
+ * // Next.js disable cache
50
+ * { fetchOptions: { cache: 'no-store' } }
51
+ *
52
+ * // Next.js on-demand revalidation
53
+ * { fetchOptions: { next: { tags: ['products'] } } }
54
+ * ```
55
+ */
56
+ fetchOptions?: Record<string, any>;
39
57
  }
40
58
  /**
41
59
  * API Client Error
@@ -74,51 +92,6 @@ declare class ContractClient {
74
92
  private static getHttpMethod;
75
93
  private static isFormData;
76
94
  }
77
- /**
78
- * Create a new contract-based API client
79
- */
80
- declare function createClient(config?: ClientConfig): ContractClient;
81
- /**
82
- * Configure the global client instance
83
- *
84
- * Call this in your app initialization to set default configuration
85
- * for all auto-generated API calls.
86
- *
87
- * @example
88
- * ```ts
89
- * // In app initialization (layout.tsx, _app.tsx, etc)
90
- * import { configureClient } from '@spfn/core/client';
91
- *
92
- * configureClient({
93
- * baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',
94
- * timeout: 60000,
95
- * headers: {
96
- * 'X-App-Version': '1.0.0'
97
- * }
98
- * });
99
- *
100
- * // Add interceptors
101
- * import { client } from '@spfn/core/client';
102
- * client.use(async (url, init) => {
103
- * // Add auth header
104
- * return {
105
- * ...init,
106
- * headers: {
107
- * ...init.headers,
108
- * Authorization: `Bearer ${getToken()}`
109
- * }
110
- * };
111
- * });
112
- * ```
113
- */
114
- declare function configureClient(config: ClientConfig): void;
115
- /**
116
- * Global client singleton with Proxy
117
- *
118
- * This client can be configured using configureClient() before use.
119
- * Used by auto-generated API client code.
120
- */
121
- declare const client: ContractClient;
122
95
  /**
123
96
  * Type guard for timeout errors
124
97
  *
@@ -209,4 +182,177 @@ declare function getServerErrorType(error: ApiClientError): string | undefined;
209
182
  */
210
183
  declare function getServerErrorDetails<T = any>(error: ApiClientError): T | undefined;
211
184
 
212
- export { ApiClientError, type CallOptions, type ClientConfig, ContractClient, type RequestInterceptor, client, configureClient, createClient, getServerErrorDetails, getServerErrorType, isHttpError, isNetworkError, isServerError, isTimeoutError };
185
+ /**
186
+ * Universal API Client
187
+ *
188
+ * Automatically routes requests based on execution environment:
189
+ * - Server Environment: Direct call to SPFN API (internal network)
190
+ * - Browser Environment: Proxies through Next.js API Route (cookie forwarding)
191
+ */
192
+
193
+ /**
194
+ * Universal Client Configuration
195
+ */
196
+ interface UniversalClientConfig {
197
+ /**
198
+ * SPFN API server URL (for server-side direct calls)
199
+ *
200
+ * @default process.env.SERVER_API_URL || process.env.SPFN_API_URL || 'http://localhost:8790'
201
+ */
202
+ apiUrl?: string;
203
+ /**
204
+ * Next.js API route base path (for client-side proxy calls)
205
+ *
206
+ * @default '/api/actions'
207
+ * @example '/api/proxy'
208
+ */
209
+ proxyBasePath?: string;
210
+ /**
211
+ * Additional headers to include in all requests
212
+ */
213
+ headers?: Record<string, string>;
214
+ /**
215
+ * Request timeout in milliseconds
216
+ *
217
+ * @default 30000
218
+ */
219
+ timeout?: number;
220
+ /**
221
+ * Custom fetch implementation
222
+ */
223
+ fetch?: typeof fetch;
224
+ }
225
+ /**
226
+ * Universal API Client
227
+ *
228
+ * Automatically detects execution environment and routes requests accordingly:
229
+ *
230
+ * **Server Environment** (Next.js Server Components, API Routes):
231
+ * - Direct HTTP call to SPFN API server
232
+ * - Uses internal network (e.g., http://localhost:8790)
233
+ * - No proxy overhead
234
+ *
235
+ * **Browser Environment** (Next.js Client Components):
236
+ * - Routes through Next.js API Route proxy (e.g., /api/proxy/*)
237
+ * - Enables HttpOnly cookie forwarding
238
+ * - Maintains CORS security
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * // Server Component - direct call
243
+ * import { createUniversalClient } from '@spfn/core/client';
244
+ * const client = createUniversalClient();
245
+ * const result = await client.call(loginContract, { body: {...} });
246
+ *
247
+ * // Client Component - proxied call (automatic)
248
+ * 'use client';
249
+ * const client = createUniversalClient();
250
+ * const result = await client.call(loginContract, { body: {...} }); // Goes through /api/proxy
251
+ * ```
252
+ */
253
+ declare class UniversalClient {
254
+ private readonly directClient;
255
+ private readonly proxyBasePath;
256
+ private readonly isServer;
257
+ private readonly fetchImpl;
258
+ constructor(config?: UniversalClientConfig);
259
+ /**
260
+ * Make a type-safe API call using a contract
261
+ *
262
+ * Automatically routes based on environment:
263
+ * - Server: Direct SPFN API call
264
+ * - Browser: Next.js API Route proxy
265
+ *
266
+ * @param contract - Route contract with absolute path
267
+ * @param options - Call options (params, query, body, headers)
268
+ */
269
+ call<TContract extends RouteContract>(contract: TContract, options?: CallOptions<TContract>): Promise<InferContract<TContract>['response']>;
270
+ /**
271
+ * Call via Next.js API Route proxy (client-side)
272
+ *
273
+ * Routes request through /api/proxy/[...path] to enable:
274
+ * - HttpOnly cookie forwarding
275
+ * - CORS security
276
+ * - Server-side session management
277
+ *
278
+ * @private
279
+ */
280
+ private callViaProxy;
281
+ /**
282
+ * Build URL path with parameter substitution
283
+ */
284
+ private buildUrlPath;
285
+ /**
286
+ * Build query string from query parameters
287
+ */
288
+ private buildQueryString;
289
+ /**
290
+ * Get HTTP method from contract or infer from options
291
+ */
292
+ private getHttpMethod;
293
+ /**
294
+ * Check if body is FormData
295
+ */
296
+ private isFormData;
297
+ /**
298
+ * Check if currently running in server environment
299
+ */
300
+ isServerEnv(): boolean;
301
+ /**
302
+ * Create a new client with merged configuration
303
+ */
304
+ withConfig(config: Partial<UniversalClientConfig>): UniversalClient;
305
+ }
306
+ /**
307
+ * Create a new universal API client
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * // Default configuration
312
+ * const client = createUniversalClient();
313
+ *
314
+ * // Custom configuration
315
+ * const client = createUniversalClient({
316
+ * apiUrl: 'http://localhost:4000',
317
+ * proxyBasePath: '/api/spfn',
318
+ * headers: { 'X-App-Version': '1.0.0' },
319
+ * });
320
+ * ```
321
+ */
322
+ declare function createUniversalClient(config?: UniversalClientConfig): UniversalClient;
323
+ /**
324
+ * Configure the global universal client instance
325
+ *
326
+ * Call this in your app initialization to set default configuration
327
+ * for all auto-generated API calls.
328
+ *
329
+ * @example
330
+ * ```typescript
331
+ * // In app initialization (layout.tsx, _app.tsx, etc)
332
+ * import { configureUniversalClient } from '@spfn/core/client';
333
+ *
334
+ * configureUniversalClient({
335
+ * apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8790',
336
+ * proxyBasePath: '/api/proxy',
337
+ * headers: {
338
+ * 'X-App-Version': '1.0.0'
339
+ * }
340
+ * });
341
+ * ```
342
+ */
343
+ declare function configureUniversalClient(config: UniversalClientConfig): void;
344
+ /**
345
+ * Get the global universal client instance
346
+ *
347
+ * Creates a default instance if not configured
348
+ */
349
+ declare function getUniversalClient(): UniversalClient;
350
+ /**
351
+ * Global universal client singleton with Proxy
352
+ *
353
+ * This client can be configured using configureUniversalClient() before use.
354
+ * Used by auto-generated API client code.
355
+ */
356
+ declare const universalClient: UniversalClient;
357
+
358
+ export { ApiClientError, type CallOptions, type UniversalClientConfig as ClientConfig, ContractClient, type RequestInterceptor, UniversalClient, type UniversalClientConfig, universalClient as client, configureUniversalClient as configureClient, configureUniversalClient, createUniversalClient as createClient, createUniversalClient, getServerErrorDetails, getServerErrorType, getUniversalClient, isHttpError, isNetworkError, isServerError, isTimeoutError, universalClient };
@@ -53,7 +53,9 @@ var ContractClient = class _ContractClient {
53
53
  }
54
54
  let init = {
55
55
  method,
56
- headers
56
+ headers,
57
+ ...options?.fetchOptions
58
+ // Spread environment-specific options (e.g., Next.js cache/next)
57
59
  };
58
60
  if (options?.body !== void 0) {
59
61
  init.body = isFormData ? options.body : JSON.stringify(options.body);
@@ -145,14 +147,8 @@ var ContractClient = class _ContractClient {
145
147
  return body instanceof FormData;
146
148
  }
147
149
  };
148
- function createClient(config) {
149
- return new ContractClient(config);
150
- }
151
150
  var _clientInstance = new ContractClient();
152
- function configureClient(config) {
153
- _clientInstance = new ContractClient(config);
154
- }
155
- var client = new Proxy({}, {
151
+ new Proxy({}, {
156
152
  get(_target, prop) {
157
153
  return _clientInstance[prop];
158
154
  }
@@ -180,6 +176,182 @@ function getServerErrorDetails(error) {
180
176
  return response?.error?.details;
181
177
  }
182
178
 
183
- export { ApiClientError, ContractClient, client, configureClient, createClient, getServerErrorDetails, getServerErrorType, isHttpError, isNetworkError, isServerError, isTimeoutError };
179
+ // src/client/universal-client.ts
180
+ function isServerEnvironment() {
181
+ return typeof window === "undefined";
182
+ }
183
+ var UniversalClient = class _UniversalClient {
184
+ directClient;
185
+ proxyBasePath;
186
+ isServer;
187
+ fetchImpl;
188
+ constructor(config = {}) {
189
+ this.isServer = isServerEnvironment();
190
+ this.directClient = new ContractClient({
191
+ baseUrl: config.apiUrl,
192
+ headers: config.headers,
193
+ timeout: config.timeout,
194
+ fetch: config.fetch
195
+ });
196
+ this.proxyBasePath = config.proxyBasePath || "/api/actions";
197
+ this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
198
+ }
199
+ /**
200
+ * Make a type-safe API call using a contract
201
+ *
202
+ * Automatically routes based on environment:
203
+ * - Server: Direct SPFN API call
204
+ * - Browser: Next.js API Route proxy
205
+ *
206
+ * @param contract - Route contract with absolute path
207
+ * @param options - Call options (params, query, body, headers)
208
+ */
209
+ async call(contract, options) {
210
+ if (this.isServer) {
211
+ return this.directClient.call(contract, options);
212
+ } else {
213
+ return this.callViaProxy(contract, options);
214
+ }
215
+ }
216
+ /**
217
+ * Call via Next.js API Route proxy (client-side)
218
+ *
219
+ * Routes request through /api/proxy/[...path] to enable:
220
+ * - HttpOnly cookie forwarding
221
+ * - CORS security
222
+ * - Server-side session management
223
+ *
224
+ * @private
225
+ */
226
+ async callViaProxy(contract, options) {
227
+ const path = this.buildUrlPath(
228
+ contract.path,
229
+ options?.params
230
+ );
231
+ const queryString = this.buildQueryString(
232
+ options?.query
233
+ );
234
+ const proxyUrl = `${this.proxyBasePath}${path}${queryString}`;
235
+ const method = this.getHttpMethod(contract, options);
236
+ const headers = {
237
+ ...options?.headers
238
+ };
239
+ const isFormData = this.isFormData(options?.body);
240
+ if (options?.body !== void 0 && !isFormData && !headers["Content-Type"]) {
241
+ headers["Content-Type"] = "application/json";
242
+ }
243
+ const init = {
244
+ method,
245
+ headers,
246
+ credentials: "include",
247
+ // Important: Include cookies for session
248
+ ...options?.fetchOptions
249
+ // Spread environment-specific options (e.g., Next.js cache/next)
250
+ };
251
+ if (options?.body !== void 0) {
252
+ init.body = isFormData ? options.body : JSON.stringify(options.body);
253
+ }
254
+ const response = await this.fetchImpl(proxyUrl, init);
255
+ if (!response.ok) {
256
+ const errorBody = await response.json().catch(() => null);
257
+ throw new ApiClientError(
258
+ `${method} ${path} failed: ${response.status} ${response.statusText}`,
259
+ response.status,
260
+ proxyUrl,
261
+ errorBody,
262
+ "http"
263
+ );
264
+ }
265
+ const data = await response.json();
266
+ return data;
267
+ }
268
+ /**
269
+ * Build URL path with parameter substitution
270
+ */
271
+ buildUrlPath(path, params) {
272
+ if (!params) return path;
273
+ let url = path;
274
+ for (const [key, value] of Object.entries(params)) {
275
+ url = url.replace(`:${key}`, String(value));
276
+ }
277
+ return url;
278
+ }
279
+ /**
280
+ * Build query string from query parameters
281
+ */
282
+ buildQueryString(query) {
283
+ if (!query || Object.keys(query).length === 0) return "";
284
+ const params = new URLSearchParams();
285
+ for (const [key, value] of Object.entries(query)) {
286
+ if (Array.isArray(value)) {
287
+ value.forEach((v) => params.append(key, String(v)));
288
+ } else if (value !== void 0 && value !== null) {
289
+ params.append(key, String(value));
290
+ }
291
+ }
292
+ const queryString = params.toString();
293
+ return queryString ? `?${queryString}` : "";
294
+ }
295
+ /**
296
+ * Get HTTP method from contract or infer from options
297
+ */
298
+ getHttpMethod(contract, options) {
299
+ if ("method" in contract && typeof contract.method === "string") {
300
+ return contract.method.toUpperCase();
301
+ }
302
+ if (options?.body !== void 0) {
303
+ return "POST";
304
+ }
305
+ return "GET";
306
+ }
307
+ /**
308
+ * Check if body is FormData
309
+ */
310
+ isFormData(body) {
311
+ return typeof FormData !== "undefined" && body instanceof FormData;
312
+ }
313
+ /**
314
+ * Check if currently running in server environment
315
+ */
316
+ isServerEnv() {
317
+ return this.isServer;
318
+ }
319
+ /**
320
+ * Create a new client with merged configuration
321
+ */
322
+ withConfig(config) {
323
+ return new _UniversalClient({
324
+ apiUrl: config.apiUrl || this.directClient["config"].baseUrl,
325
+ proxyBasePath: config.proxyBasePath || this.proxyBasePath,
326
+ headers: {
327
+ ...this.directClient["config"].headers,
328
+ ...config.headers
329
+ },
330
+ timeout: config.timeout || this.directClient["config"].timeout,
331
+ fetch: config.fetch || this.fetchImpl
332
+ });
333
+ }
334
+ };
335
+ function createUniversalClient(config) {
336
+ return new UniversalClient(config);
337
+ }
338
+ var _universalClientInstance = null;
339
+ function configureUniversalClient(config) {
340
+ _universalClientInstance = new UniversalClient(config);
341
+ }
342
+ function getUniversalClient() {
343
+ if (!_universalClientInstance) {
344
+ _universalClientInstance = new UniversalClient();
345
+ }
346
+ return _universalClientInstance;
347
+ }
348
+ var universalClient = new Proxy({}, {
349
+ get(_target, prop) {
350
+ const instance = getUniversalClient();
351
+ return instance[prop];
352
+ }
353
+ });
354
+
355
+ export { ApiClientError, ContractClient, UniversalClient, universalClient as client, configureUniversalClient as configureClient, configureUniversalClient, createUniversalClient as createClient, createUniversalClient, getServerErrorDetails, getServerErrorType, getUniversalClient, isHttpError, isNetworkError, isServerError, isTimeoutError, universalClient };
184
356
  //# sourceMappingURL=index.js.map
185
357
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/contract-client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Contract-Based API Client\n *\n * Type-safe HTTP client that works with RouteContract for full end-to-end type safety\n */\nimport type { RouteContract, InferContract } from '../route';\n\nexport type RequestInterceptor = (\n url: string,\n init: RequestInit\n) => Promise<RequestInit> | RequestInit;\n\nexport interface ClientConfig\n{\n /**\n * API base URL (e.g., http://localhost:4000)\n */\n baseUrl?: string;\n\n /**\n * Default headers to include in all requests\n */\n headers?: Record<string, string>;\n\n /**\n * Request timeout in milliseconds\n */\n timeout?: number;\n\n /**\n * Custom fetch implementation\n */\n fetch?: typeof fetch;\n}\n\nexport interface CallOptions<TContract extends RouteContract>\n{\n params?: InferContract<TContract>['params'];\n query?: InferContract<TContract>['query'];\n body?: InferContract<TContract>['body'];\n headers?: Record<string, string>;\n baseUrl?: string;\n}\n\n/**\n * API Client Error\n */\nexport class ApiClientError extends Error\n{\n constructor(\n message: string,\n public readonly status: number,\n public readonly url: string,\n public readonly response?: unknown,\n public readonly errorType?: 'timeout' | 'network' | 'http'\n )\n {\n super(message);\n this.name = 'ApiClientError';\n }\n}\n\n/**\n * Contract-based API Client\n */\nexport class ContractClient\n{\n private readonly config: Required<ClientConfig>;\n private readonly interceptors: RequestInterceptor[] = [];\n\n constructor(config: ClientConfig = {})\n {\n this.config = {\n baseUrl: config.baseUrl || process.env.SERVER_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',\n headers: config.headers || {},\n timeout: config.timeout || 30000,\n fetch: config.fetch || globalThis.fetch.bind(globalThis),\n };\n }\n\n /**\n * Add request interceptor\n */\n use(interceptor: RequestInterceptor): void\n {\n this.interceptors.push(interceptor);\n }\n\n /**\n * Make a type-safe API call using a contract\n *\n * @param contract - Route contract with absolute path\n * @param options - Call options (params, query, body, headers)\n */\n async call<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n ): Promise<InferContract<TContract>['response']>\n {\n const baseUrl = options?.baseUrl || this.config.baseUrl;\n\n // Use contract.path directly (contracts use absolute paths)\n const urlPath = ContractClient.buildUrl(\n contract.path,\n options?.params as Record<string, string | number> | undefined\n );\n\n const queryString = ContractClient.buildQuery(\n options?.query as Record<string, string | string[] | number | boolean> | undefined\n );\n\n const url = `${baseUrl}${urlPath}${queryString}`;\n\n const method = ContractClient.getHttpMethod(contract, options);\n\n const headers: Record<string, string> = {\n ...this.config.headers,\n ...options?.headers,\n };\n\n const isFormData = ContractClient.isFormData(options?.body);\n\n if (options?.body !== undefined && !isFormData && !headers['Content-Type'])\n {\n headers['Content-Type'] = 'application/json';\n }\n\n let init: RequestInit = {\n method,\n headers,\n };\n\n if (options?.body !== undefined)\n {\n init.body = isFormData ? (options.body as FormData) : JSON.stringify(options.body);\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n init.signal = controller.signal;\n\n for (const interceptor of this.interceptors)\n {\n init = await interceptor(url, init);\n }\n\n const response = await this.config.fetch(url, init).catch((error) =>\n {\n clearTimeout(timeoutId);\n\n if (error instanceof Error && error.name === 'AbortError')\n {\n throw new ApiClientError(\n `Request timed out after ${this.config.timeout}ms`,\n 0,\n url,\n undefined,\n 'timeout'\n );\n }\n\n if (error instanceof Error)\n {\n throw new ApiClientError(\n `Network error: ${error.message}`,\n 0,\n url,\n undefined,\n 'network'\n );\n }\n\n throw error;\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok)\n {\n const errorBody = await response.json().catch(() => null);\n throw new ApiClientError(\n `${method} ${urlPath} failed: ${response.status} ${response.statusText}`,\n response.status,\n url,\n errorBody,\n 'http'\n );\n }\n\n const data = await response.json();\n return data as InferContract<TContract>['response'];\n }\n\n /**\n * Create a new client with merged configuration\n */\n withConfig(config: Partial<ClientConfig>): ContractClient\n {\n return new ContractClient({\n baseUrl: config.baseUrl || this.config.baseUrl,\n headers: { ...this.config.headers, ...config.headers },\n timeout: config.timeout || this.config.timeout,\n fetch: config.fetch || this.config.fetch,\n });\n }\n\n private static buildUrl(path: string, params?: Record<string, string | number>): string\n {\n if (!params) return path;\n\n let url = path;\n for (const [key, value] of Object.entries(params))\n {\n url = url.replace(`:${key}`, String(value));\n }\n\n return url;\n }\n\n private static buildQuery(query?: Record<string, string | string[] | number | boolean>): string\n {\n if (!query || Object.keys(query).length === 0) return '';\n\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query))\n {\n if (Array.isArray(value))\n {\n value.forEach((v) => params.append(key, String(v)));\n }\n else if (value !== undefined && value !== null)\n {\n params.append(key, String(value));\n }\n }\n\n const queryString = params.toString();\n return queryString ? `?${queryString}` : '';\n }\n\n private static getHttpMethod<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n ): string\n {\n if ('method' in contract && typeof contract.method === 'string')\n {\n return contract.method.toUpperCase();\n }\n\n if (options?.body !== undefined)\n {\n return 'POST';\n }\n\n return 'GET';\n }\n\n private static isFormData(body: unknown): body is FormData\n {\n return body instanceof FormData;\n }\n}\n\n/**\n * Create a new contract-based API client\n */\nexport function createClient(config?: ClientConfig): ContractClient\n{\n return new ContractClient(config);\n}\n\n/**\n * Global client singleton instance\n */\nlet _clientInstance: ContractClient = new ContractClient();\n\n/**\n * Configure the global client instance\n *\n * Call this in your app initialization to set default configuration\n * for all auto-generated API calls.\n *\n * @example\n * ```ts\n * // In app initialization (layout.tsx, _app.tsx, etc)\n * import { configureClient } from '@spfn/core/client';\n *\n * configureClient({\n * baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',\n * timeout: 60000,\n * headers: {\n * 'X-App-Version': '1.0.0'\n * }\n * });\n *\n * // Add interceptors\n * import { client } from '@spfn/core/client';\n * client.use(async (url, init) => {\n * // Add auth header\n * return {\n * ...init,\n * headers: {\n * ...init.headers,\n * Authorization: `Bearer ${getToken()}`\n * }\n * };\n * });\n * ```\n */\nexport function configureClient(config: ClientConfig): void\n{\n _clientInstance = new ContractClient(config);\n}\n\n/**\n * Global client singleton with Proxy\n *\n * This client can be configured using configureClient() before use.\n * Used by auto-generated API client code.\n */\nexport const client = new Proxy({} as ContractClient, {\n get(_target, prop)\n {\n return _clientInstance[prop as keyof ContractClient];\n }\n});\n\n/**\n * Type guard for timeout errors\n *\n * @example\n * ```ts\n * try {\n * await api.users.getById({ params: { id: '123' } });\n * } catch (error) {\n * if (isTimeoutError(error)) {\n * console.error('Request timed out, retrying...');\n * // Implement retry logic\n * }\n * }\n * ```\n */\nexport function isTimeoutError(error: unknown): error is ApiClientError\n{\n return error instanceof ApiClientError && error.errorType === 'timeout';\n}\n\n/**\n * Type guard for network errors\n *\n * @example\n * ```ts\n * try {\n * await api.users.list();\n * } catch (error) {\n * if (isNetworkError(error)) {\n * showOfflineMessage();\n * }\n * }\n * ```\n */\nexport function isNetworkError(error: unknown): error is ApiClientError\n{\n return error instanceof ApiClientError && error.errorType === 'network';\n}\n\n/**\n * Type guard for HTTP errors (4xx, 5xx)\n *\n * @example\n * ```ts\n * try {\n * await api.users.create({ body: userData });\n * } catch (error) {\n * if (isHttpError(error)) {\n * if (error.status === 401) {\n * redirectToLogin();\n * } else if (error.status === 404) {\n * showNotFoundMessage();\n * }\n * }\n * }\n * ```\n */\nexport function isHttpError(error: unknown): error is ApiClientError\n{\n return error instanceof ApiClientError && error.errorType === 'http';\n}\n\n/**\n * Check if error is a specific server error type\n *\n * @example\n * ```ts\n * try {\n * await api.workflows.getById({ params: { uuid: 'xxx' } });\n * } catch (error) {\n * if (isServerError(error, 'NotFoundError')) {\n * showNotFoundMessage();\n * } else if (isServerError(error, 'ValidationError')) {\n * showValidationErrors(getServerErrorDetails(error));\n * }\n * }\n * ```\n */\nexport function isServerError(error: unknown, errorType: string): error is ApiClientError\n{\n if (!isHttpError(error)) return false;\n const response = error.response as any;\n return response?.error?.type === errorType;\n}\n\n/**\n * Get server error type from ApiClientError\n *\n * @example\n * ```ts\n * const errorType = getServerErrorType(error);\n * // 'NotFoundError', 'ValidationError', 'PaymentFailedError', etc.\n * ```\n */\nexport function getServerErrorType(error: ApiClientError): string | undefined\n{\n const response = error.response as any;\n return response?.error?.type;\n}\n\n/**\n * Get server error details from ApiClientError\n *\n * @example\n * ```ts\n * if (isServerError(error, 'PaymentFailedError')) {\n * const details = getServerErrorDetails(error);\n * console.log('Payment ID:', details.paymentId);\n * }\n * ```\n */\nexport function getServerErrorDetails<T = any>(error: ApiClientError): T | undefined\n{\n const response = error.response as any;\n return response?.error?.details;\n}\n"]}
1
+ {"version":3,"sources":["../../src/client/contract-client.ts","../../src/client/universal-client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Contract-Based API Client\n *\n * Type-safe HTTP client that works with RouteContract for full end-to-end type safety\n */\nimport type { RouteContract, InferContract } from '../route';\n\nexport type RequestInterceptor = (\n url: string,\n init: RequestInit\n) => Promise<RequestInit> | RequestInit;\n\nexport interface ClientConfig\n{\n /**\n * API base URL (e.g., http://localhost:4000)\n */\n baseUrl?: string;\n\n /**\n * Default headers to include in all requests\n */\n headers?: Record<string, string>;\n\n /**\n * Request timeout in milliseconds\n */\n timeout?: number;\n\n /**\n * Custom fetch implementation\n */\n fetch?: typeof fetch;\n}\n\nexport interface CallOptions<TContract extends RouteContract>\n{\n params?: InferContract<TContract>['params'];\n query?: InferContract<TContract>['query'];\n body?: InferContract<TContract>['body'];\n headers?: Record<string, string>;\n baseUrl?: string;\n\n /**\n * Additional fetch options (extends RequestInit)\n *\n * Can be used for environment-specific options like Next.js cache/next\n *\n * @example\n * ```ts\n * // Next.js time-based revalidation\n * { fetchOptions: { next: { revalidate: 60 } } }\n *\n * // Next.js disable cache\n * { fetchOptions: { cache: 'no-store' } }\n *\n * // Next.js on-demand revalidation\n * { fetchOptions: { next: { tags: ['products'] } } }\n * ```\n */\n fetchOptions?: Record<string, any>;\n}\n\n/**\n * API Client Error\n */\nexport class ApiClientError extends Error\n{\n constructor(\n message: string,\n public readonly status: number,\n public readonly url: string,\n public readonly response?: unknown,\n public readonly errorType?: 'timeout' | 'network' | 'http'\n )\n {\n super(message);\n this.name = 'ApiClientError';\n }\n}\n\n/**\n * Contract-based API Client\n */\nexport class ContractClient\n{\n private readonly config: Required<ClientConfig>;\n private readonly interceptors: RequestInterceptor[] = [];\n\n constructor(config: ClientConfig = {})\n {\n this.config = {\n baseUrl: config.baseUrl || process.env.SERVER_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',\n headers: config.headers || {},\n timeout: config.timeout || 30000,\n fetch: config.fetch || globalThis.fetch.bind(globalThis),\n };\n }\n\n /**\n * Add request interceptor\n */\n use(interceptor: RequestInterceptor): void\n {\n this.interceptors.push(interceptor);\n }\n\n /**\n * Make a type-safe API call using a contract\n *\n * @param contract - Route contract with absolute path\n * @param options - Call options (params, query, body, headers)\n */\n async call<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n ): Promise<InferContract<TContract>['response']>\n {\n const baseUrl = options?.baseUrl || this.config.baseUrl;\n\n // Use contract.path directly (contracts use absolute paths)\n const urlPath = ContractClient.buildUrl(\n contract.path,\n options?.params as Record<string, string | number> | undefined\n );\n\n const queryString = ContractClient.buildQuery(\n options?.query as Record<string, string | string[] | number | boolean> | undefined\n );\n\n const url = `${baseUrl}${urlPath}${queryString}`;\n\n const method = ContractClient.getHttpMethod(contract, options);\n\n const headers: Record<string, string> = {\n ...this.config.headers,\n ...options?.headers,\n };\n\n const isFormData = ContractClient.isFormData(options?.body);\n\n if (options?.body !== undefined && !isFormData && !headers['Content-Type'])\n {\n headers['Content-Type'] = 'application/json';\n }\n\n let init: RequestInit = {\n method,\n headers,\n ...options?.fetchOptions, // Spread environment-specific options (e.g., Next.js cache/next)\n };\n\n if (options?.body !== undefined)\n {\n init.body = isFormData ? (options.body as FormData) : JSON.stringify(options.body);\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n init.signal = controller.signal;\n\n for (const interceptor of this.interceptors)\n {\n init = await interceptor(url, init);\n }\n\n const response = await this.config.fetch(url, init).catch((error) =>\n {\n clearTimeout(timeoutId);\n\n if (error instanceof Error && error.name === 'AbortError')\n {\n throw new ApiClientError(\n `Request timed out after ${this.config.timeout}ms`,\n 0,\n url,\n undefined,\n 'timeout'\n );\n }\n\n if (error instanceof Error)\n {\n throw new ApiClientError(\n `Network error: ${error.message}`,\n 0,\n url,\n undefined,\n 'network'\n );\n }\n\n throw error;\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok)\n {\n const errorBody = await response.json().catch(() => null);\n throw new ApiClientError(\n `${method} ${urlPath} failed: ${response.status} ${response.statusText}`,\n response.status,\n url,\n errorBody,\n 'http'\n );\n }\n\n const data = await response.json();\n return data as InferContract<TContract>['response'];\n }\n\n /**\n * Create a new client with merged configuration\n */\n withConfig(config: Partial<ClientConfig>): ContractClient\n {\n return new ContractClient({\n baseUrl: config.baseUrl || this.config.baseUrl,\n headers: { ...this.config.headers, ...config.headers },\n timeout: config.timeout || this.config.timeout,\n fetch: config.fetch || this.config.fetch,\n });\n }\n\n private static buildUrl(path: string, params?: Record<string, string | number>): string\n {\n if (!params) return path;\n\n let url = path;\n for (const [key, value] of Object.entries(params))\n {\n url = url.replace(`:${key}`, String(value));\n }\n\n return url;\n }\n\n private static buildQuery(query?: Record<string, string | string[] | number | boolean>): string\n {\n if (!query || Object.keys(query).length === 0) return '';\n\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query))\n {\n if (Array.isArray(value))\n {\n value.forEach((v) => params.append(key, String(v)));\n }\n else if (value !== undefined && value !== null)\n {\n params.append(key, String(value));\n }\n }\n\n const queryString = params.toString();\n return queryString ? `?${queryString}` : '';\n }\n\n private static getHttpMethod<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n ): string\n {\n if ('method' in contract && typeof contract.method === 'string')\n {\n return contract.method.toUpperCase();\n }\n\n if (options?.body !== undefined)\n {\n return 'POST';\n }\n\n return 'GET';\n }\n\n private static isFormData(body: unknown): body is FormData\n {\n return body instanceof FormData;\n }\n}\n\n/**\n * Create a new contract-based API client\n */\nexport function createClient(config?: ClientConfig): ContractClient\n{\n return new ContractClient(config);\n}\n\n/**\n * Global client singleton instance\n */\nlet _clientInstance: ContractClient = new ContractClient();\n\n/**\n * Configure the global client instance\n *\n * Call this in your app initialization to set default configuration\n * for all auto-generated API calls.\n *\n * @example\n * ```ts\n * // In app initialization (layout.tsx, _app.tsx, etc)\n * import { configureClient } from '@spfn/core/client';\n *\n * configureClient({\n * baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',\n * timeout: 60000,\n * headers: {\n * 'X-App-Version': '1.0.0'\n * }\n * });\n *\n * // Add interceptors\n * import { client } from '@spfn/core/client';\n * client.use(async (url, init) => {\n * // Add auth header\n * return {\n * ...init,\n * headers: {\n * ...init.headers,\n * Authorization: `Bearer ${getToken()}`\n * }\n * };\n * });\n * ```\n */\nexport function configureClient(config: ClientConfig): void\n{\n _clientInstance = new ContractClient(config);\n}\n\n/**\n * Global client singleton with Proxy\n *\n * This client can be configured using configureClient() before use.\n * Used by auto-generated API client code.\n */\nexport const client = new Proxy({} as ContractClient, {\n get(_target, prop)\n {\n return _clientInstance[prop as keyof ContractClient];\n }\n});\n\n/**\n * Type guard for timeout errors\n *\n * @example\n * ```ts\n * try {\n * await api.users.getById({ params: { id: '123' } });\n * } catch (error) {\n * if (isTimeoutError(error)) {\n * console.error('Request timed out, retrying...');\n * // Implement retry logic\n * }\n * }\n * ```\n */\nexport function isTimeoutError(error: unknown): error is ApiClientError\n{\n return error instanceof ApiClientError && error.errorType === 'timeout';\n}\n\n/**\n * Type guard for network errors\n *\n * @example\n * ```ts\n * try {\n * await api.users.list();\n * } catch (error) {\n * if (isNetworkError(error)) {\n * showOfflineMessage();\n * }\n * }\n * ```\n */\nexport function isNetworkError(error: unknown): error is ApiClientError\n{\n return error instanceof ApiClientError && error.errorType === 'network';\n}\n\n/**\n * Type guard for HTTP errors (4xx, 5xx)\n *\n * @example\n * ```ts\n * try {\n * await api.users.create({ body: userData });\n * } catch (error) {\n * if (isHttpError(error)) {\n * if (error.status === 401) {\n * redirectToLogin();\n * } else if (error.status === 404) {\n * showNotFoundMessage();\n * }\n * }\n * }\n * ```\n */\nexport function isHttpError(error: unknown): error is ApiClientError\n{\n return error instanceof ApiClientError && error.errorType === 'http';\n}\n\n/**\n * Check if error is a specific server error type\n *\n * @example\n * ```ts\n * try {\n * await api.workflows.getById({ params: { uuid: 'xxx' } });\n * } catch (error) {\n * if (isServerError(error, 'NotFoundError')) {\n * showNotFoundMessage();\n * } else if (isServerError(error, 'ValidationError')) {\n * showValidationErrors(getServerErrorDetails(error));\n * }\n * }\n * ```\n */\nexport function isServerError(error: unknown, errorType: string): error is ApiClientError\n{\n if (!isHttpError(error)) return false;\n const response = error.response as any;\n return response?.error?.type === errorType;\n}\n\n/**\n * Get server error type from ApiClientError\n *\n * @example\n * ```ts\n * const errorType = getServerErrorType(error);\n * // 'NotFoundError', 'ValidationError', 'PaymentFailedError', etc.\n * ```\n */\nexport function getServerErrorType(error: ApiClientError): string | undefined\n{\n const response = error.response as any;\n return response?.error?.type;\n}\n\n/**\n * Get server error details from ApiClientError\n *\n * @example\n * ```ts\n * if (isServerError(error, 'PaymentFailedError')) {\n * const details = getServerErrorDetails(error);\n * console.log('Payment ID:', details.paymentId);\n * }\n * ```\n */\nexport function getServerErrorDetails<T = any>(error: ApiClientError): T | undefined\n{\n const response = error.response as any;\n return response?.error?.details;\n}\n","/**\n * Universal API Client\n *\n * Automatically routes requests based on execution environment:\n * - Server Environment: Direct call to SPFN API (internal network)\n * - Browser Environment: Proxies through Next.js API Route (cookie forwarding)\n */\n\nimport type { RouteContract, InferContract } from '../route';\nimport { ContractClient, type CallOptions, ApiClientError } from './contract-client';\n\n// Type declaration for window (available in browser)\ndeclare const window: unknown | undefined;\n\n/**\n * Detect if code is running in server environment\n *\n * Uses typeof window check for reliable browser detection\n */\nfunction isServerEnvironment(): boolean\n{\n return typeof window === 'undefined';\n}\n\n/**\n * Request interceptor function\n *\n * Called before each request to modify headers dynamically\n * Useful for adding authentication tokens, session data, etc.\n *\n * @param headers - Current request headers (mutable)\n * @param contract - Route contract being called\n * @returns Modified headers or void (modify in place)\n */\nexport type RequestInterceptor = (\n headers: Record<string, string>,\n contract: RouteContract\n) => Promise<void> | void;\n\n/**\n * Universal Client Configuration\n */\nexport interface UniversalClientConfig\n{\n /**\n * SPFN API server URL (for server-side direct calls)\n *\n * @default process.env.SERVER_API_URL || process.env.SPFN_API_URL || 'http://localhost:8790'\n */\n apiUrl?: string;\n\n /**\n * Next.js API route base path (for client-side proxy calls)\n *\n * @default '/api/actions'\n * @example '/api/proxy'\n */\n proxyBasePath?: string;\n\n /**\n * Additional headers to include in all requests\n */\n headers?: Record<string, string>;\n\n /**\n * Request timeout in milliseconds\n *\n * @default 30000\n */\n timeout?: number;\n\n /**\n * Custom fetch implementation\n */\n fetch?: typeof fetch;\n}\n\n/**\n * Universal API Client\n *\n * Automatically detects execution environment and routes requests accordingly:\n *\n * **Server Environment** (Next.js Server Components, API Routes):\n * - Direct HTTP call to SPFN API server\n * - Uses internal network (e.g., http://localhost:8790)\n * - No proxy overhead\n *\n * **Browser Environment** (Next.js Client Components):\n * - Routes through Next.js API Route proxy (e.g., /api/proxy/*)\n * - Enables HttpOnly cookie forwarding\n * - Maintains CORS security\n *\n * @example\n * ```typescript\n * // Server Component - direct call\n * import { createUniversalClient } from '@spfn/core/client';\n * const client = createUniversalClient();\n * const result = await client.call(loginContract, { body: {...} });\n *\n * // Client Component - proxied call (automatic)\n * 'use client';\n * const client = createUniversalClient();\n * const result = await client.call(loginContract, { body: {...} }); // Goes through /api/proxy\n * ```\n */\nexport class UniversalClient\n{\n private readonly directClient: ContractClient;\n private readonly proxyBasePath: string;\n private readonly isServer: boolean;\n private readonly fetchImpl: typeof fetch;\n\n constructor(config: UniversalClientConfig = {})\n {\n // Detect environment once during construction\n this.isServer = isServerEnvironment();\n\n // Direct client for server-side calls\n this.directClient = new ContractClient({\n baseUrl: config.apiUrl,\n headers: config.headers,\n timeout: config.timeout,\n fetch: config.fetch,\n });\n\n // Proxy configuration for client-side calls\n this.proxyBasePath = config.proxyBasePath || '/api/actions';\n\n // Fetch implementation\n this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);\n }\n\n /**\n * Make a type-safe API call using a contract\n *\n * Automatically routes based on environment:\n * - Server: Direct SPFN API call\n * - Browser: Next.js API Route proxy\n *\n * @param contract - Route contract with absolute path\n * @param options - Call options (params, query, body, headers)\n */\n async call<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n ): Promise<InferContract<TContract>['response']>\n {\n if (this.isServer)\n {\n // Server environment: Direct call to SPFN API\n return this.directClient.call(contract, options);\n }\n else\n {\n // Browser environment: Proxy through Next.js API Route\n return this.callViaProxy(contract, options);\n }\n }\n\n /**\n * Call via Next.js API Route proxy (client-side)\n *\n * Routes request through /api/proxy/[...path] to enable:\n * - HttpOnly cookie forwarding\n * - CORS security\n * - Server-side session management\n *\n * @private\n */\n private async callViaProxy<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n ): Promise<InferContract<TContract>['response']>\n {\n // Build proxy URL: /api/proxy + contract.path\n // Example: /_auth/login -> /api/proxy/_auth/login\n const path = this.buildUrlPath(\n contract.path,\n options?.params as Record<string, string | number> | undefined\n );\n const queryString = this.buildQueryString(\n options?.query as Record<string, string | string[] | number | boolean> | undefined\n );\n const proxyUrl = `${this.proxyBasePath}${path}${queryString}`;\n\n const method = this.getHttpMethod(contract, options);\n\n const headers: Record<string, string> = {\n ...options?.headers,\n };\n\n const isFormData = this.isFormData(options?.body);\n\n // Set Content-Type for JSON bodies\n if (options?.body !== undefined && !isFormData && !headers['Content-Type'])\n {\n headers['Content-Type'] = 'application/json';\n }\n\n const init: RequestInit = {\n method,\n headers,\n credentials: 'include', // Important: Include cookies for session\n ...options?.fetchOptions, // Spread environment-specific options (e.g., Next.js cache/next)\n };\n\n // Add body for POST/PUT/PATCH\n if (options?.body !== undefined)\n {\n init.body = isFormData\n ? (options.body as FormData)\n : JSON.stringify(options.body);\n }\n\n const response = await this.fetchImpl(proxyUrl, init);\n\n if (!response.ok)\n {\n const errorBody = await response.json().catch(() => null);\n throw new ApiClientError(\n `${method} ${path} failed: ${response.status} ${response.statusText}`,\n response.status,\n proxyUrl,\n errorBody,\n 'http'\n );\n }\n\n const data = await response.json();\n return data as InferContract<TContract>['response'];\n }\n\n /**\n * Build URL path with parameter substitution\n */\n private buildUrlPath(\n path: string,\n params?: Record<string, string | number>\n ): string\n {\n if (!params) return path;\n\n let url = path;\n for (const [key, value] of Object.entries(params))\n {\n url = url.replace(`:${key}`, String(value));\n }\n\n return url;\n }\n\n /**\n * Build query string from query parameters\n */\n private buildQueryString(\n query?: Record<string, string | string[] | number | boolean>\n ): string\n {\n if (!query || Object.keys(query).length === 0) return '';\n\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query))\n {\n if (Array.isArray(value))\n {\n value.forEach((v) => params.append(key, String(v)));\n }\n else if (value !== undefined && value !== null)\n {\n params.append(key, String(value));\n }\n }\n\n const queryString = params.toString();\n return queryString ? `?${queryString}` : '';\n }\n\n /**\n * Get HTTP method from contract or infer from options\n */\n private getHttpMethod<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n ): string\n {\n if ('method' in contract && typeof contract.method === 'string')\n {\n return contract.method.toUpperCase();\n }\n\n if (options?.body !== undefined)\n {\n return 'POST';\n }\n\n return 'GET';\n }\n\n /**\n * Check if body is FormData\n */\n private isFormData(body: unknown): body is FormData\n {\n return typeof FormData !== 'undefined' && body instanceof FormData;\n }\n\n /**\n * Check if currently running in server environment\n */\n isServerEnv(): boolean\n {\n return this.isServer;\n }\n\n /**\n * Create a new client with merged configuration\n */\n withConfig(config: Partial<UniversalClientConfig>): UniversalClient\n {\n return new UniversalClient({\n apiUrl: config.apiUrl || this.directClient['config'].baseUrl,\n proxyBasePath: config.proxyBasePath || this.proxyBasePath,\n headers: {\n ...this.directClient['config'].headers,\n ...config.headers,\n },\n timeout: config.timeout || this.directClient['config'].timeout,\n fetch: config.fetch || this.fetchImpl,\n });\n }\n}\n\n/**\n * Create a new universal API client\n *\n * @example\n * ```typescript\n * // Default configuration\n * const client = createUniversalClient();\n *\n * // Custom configuration\n * const client = createUniversalClient({\n * apiUrl: 'http://localhost:4000',\n * proxyBasePath: '/api/spfn',\n * headers: { 'X-App-Version': '1.0.0' },\n * });\n * ```\n */\nexport function createUniversalClient(config?: UniversalClientConfig): UniversalClient\n{\n return new UniversalClient(config);\n}\n\n/**\n * Global universal client singleton instance\n */\nlet _universalClientInstance: UniversalClient | null = null;\n\n/**\n * Configure the global universal client instance\n *\n * Call this in your app initialization to set default configuration\n * for all auto-generated API calls.\n *\n * @example\n * ```typescript\n * // In app initialization (layout.tsx, _app.tsx, etc)\n * import { configureUniversalClient } from '@spfn/core/client';\n *\n * configureUniversalClient({\n * apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8790',\n * proxyBasePath: '/api/proxy',\n * headers: {\n * 'X-App-Version': '1.0.0'\n * }\n * });\n * ```\n */\nexport function configureUniversalClient(config: UniversalClientConfig): void\n{\n _universalClientInstance = new UniversalClient(config);\n}\n\n/**\n * Get the global universal client instance\n *\n * Creates a default instance if not configured\n */\nexport function getUniversalClient(): UniversalClient\n{\n if (!_universalClientInstance)\n {\n _universalClientInstance = new UniversalClient();\n }\n\n return _universalClientInstance;\n}\n\n/**\n * Global universal client singleton with Proxy\n *\n * This client can be configured using configureUniversalClient() before use.\n * Used by auto-generated API client code.\n */\nexport const universalClient = new Proxy({} as UniversalClient, {\n get(_target, prop)\n {\n const instance = getUniversalClient();\n return instance[prop as keyof UniversalClient];\n }\n});"]}