dalila 1.7.6 → 1.8.0

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.
package/README.md CHANGED
@@ -81,6 +81,10 @@ bind(document.getElementById('app')!, ctx);
81
81
  - [Query](./docs/core/query.md) — Cached queries
82
82
  - [Mutations](./docs/core/mutation.md) — Write operations
83
83
 
84
+ ### HTTP
85
+
86
+ - [HTTP Client](./docs/http.md) — Native fetch-based client with XSRF protection and interceptors
87
+
84
88
  ### Forms
85
89
 
86
90
  - [Forms](./docs/forms.md) — DOM-first form management with validation, field arrays, and accessibility
@@ -97,6 +101,7 @@ bind(document.getElementById('app')!, ctx);
97
101
  dalila → signal, computed, effect, batch, ...
98
102
  dalila/runtime → bind() for HTML templates
99
103
  dalila/context → createContext, provide, inject
104
+ dalila/http → createHttpClient with XSRF protection
100
105
  ```
101
106
 
102
107
  ### Signals
@@ -166,6 +171,28 @@ theme.set('light'); // Saved automatically
166
171
  // On reload: theme starts as 'light'
167
172
  ```
168
173
 
174
+ ### HTTP Client
175
+
176
+ ```ts
177
+ import { createHttpClient } from 'dalila/http';
178
+
179
+ const http = createHttpClient({
180
+ baseURL: 'https://api.example.com',
181
+ xsrf: true, // XSRF protection
182
+ onError: (error) => {
183
+ if (error.status === 401) window.location.href = '/login';
184
+ throw error;
185
+ }
186
+ });
187
+
188
+ // GET request
189
+ const response = await http.get('/users');
190
+ console.log(response.data);
191
+
192
+ // POST with auto JSON serialization
193
+ await http.post('/users', { name: 'John', email: 'john@example.com' });
194
+ ```
195
+
169
196
  ### File-Based Routing
170
197
 
171
198
  ```txt
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Fetch Adapter
3
+ *
4
+ * Native fetch-based HTTP adapter with timeout and abort support.
5
+ * Uses only browser/Node native APIs (fetch, AbortController, URL).
6
+ */
7
+ import { type RequestConfig, type HttpResponse } from './types.js';
8
+ /**
9
+ * Execute an HTTP request using native fetch API.
10
+ *
11
+ * Features:
12
+ * - Timeout support via AbortController
13
+ * - Manual cancellation via config.signal
14
+ * - Automatic JSON serialization
15
+ * - URL params handling
16
+ * - Response type parsing
17
+ *
18
+ * @param config - Request configuration
19
+ * @returns Promise that resolves to HttpResponse
20
+ * @throws HttpError on any failure
21
+ */
22
+ export declare function fetchAdapter<T = any>(config: RequestConfig): Promise<HttpResponse<T>>;
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Fetch Adapter
3
+ *
4
+ * Native fetch-based HTTP adapter with timeout and abort support.
5
+ * Uses only browser/Node native APIs (fetch, AbortController, URL).
6
+ */
7
+ import { HttpError } from './types.js';
8
+ /**
9
+ * Execute an HTTP request using native fetch API.
10
+ *
11
+ * Features:
12
+ * - Timeout support via AbortController
13
+ * - Manual cancellation via config.signal
14
+ * - Automatic JSON serialization
15
+ * - URL params handling
16
+ * - Response type parsing
17
+ *
18
+ * @param config - Request configuration
19
+ * @returns Promise that resolves to HttpResponse
20
+ * @throws HttpError on any failure
21
+ */
22
+ export async function fetchAdapter(config) {
23
+ const { url = '', method = 'GET', headers = {}, data, params, timeout, signal: userSignal, responseType = 'json', baseURL = '', } = config;
24
+ // Build full URL
25
+ const fullUrl = buildUrl(baseURL, url, params);
26
+ // Setup abort handling (timeout + manual signal)
27
+ const controller = new AbortController();
28
+ const { signal, cleanup } = setupAbort(controller, timeout, userSignal);
29
+ // Build request headers
30
+ const requestHeaders = buildHeaders(headers, data);
31
+ // Build request body
32
+ const body = buildBody(data);
33
+ let response;
34
+ try {
35
+ // Execute fetch
36
+ response = await fetch(fullUrl, {
37
+ method,
38
+ headers: requestHeaders,
39
+ body,
40
+ signal,
41
+ });
42
+ cleanup();
43
+ // Handle non-2xx responses
44
+ if (!response.ok) {
45
+ const errorData = await parseResponseSafe(response, responseType);
46
+ throw new HttpError(`HTTP Error ${response.status}: ${response.statusText}`, 'http', config, {
47
+ status: response.status,
48
+ data: errorData,
49
+ response,
50
+ });
51
+ }
52
+ // Check if response has a body
53
+ const hasBody = shouldParseResponseBody(response, method);
54
+ // Parse response (or return null for empty responses)
55
+ const responseData = hasBody
56
+ ? await parseResponse(response, responseType)
57
+ : null;
58
+ return {
59
+ data: responseData,
60
+ status: response.status,
61
+ statusText: response.statusText,
62
+ headers: response.headers,
63
+ config,
64
+ };
65
+ }
66
+ catch (error) {
67
+ cleanup();
68
+ // Already an HttpError (from non-2xx response)
69
+ if (error instanceof HttpError) {
70
+ throw error;
71
+ }
72
+ // AbortError (timeout or manual cancel)
73
+ if (error instanceof Error && error.name === 'AbortError') {
74
+ const isTimeout = controller.signal.reason === 'timeout';
75
+ throw new HttpError(isTimeout ? `Request timeout after ${timeout}ms` : 'Request aborted', isTimeout ? 'timeout' : 'abort', config);
76
+ }
77
+ // Parse error (invalid JSON, malformed response body, etc)
78
+ if (error instanceof Error && error.message.startsWith('Failed to parse response')) {
79
+ throw new HttpError(error.message, 'parse', config, {
80
+ status: response?.status,
81
+ response: response,
82
+ });
83
+ }
84
+ // Network error (DNS, connection refused, etc)
85
+ if (error instanceof TypeError) {
86
+ throw new HttpError(`Network error: ${error.message}`, 'network', config);
87
+ }
88
+ // Unknown error (treat as network error for backwards compatibility)
89
+ throw new HttpError(error instanceof Error ? error.message : String(error), 'network', config);
90
+ }
91
+ }
92
+ /**
93
+ * Check if a URL is absolute (starts with http://, https://, or //).
94
+ */
95
+ function isAbsoluteUrl(url) {
96
+ return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url);
97
+ }
98
+ /**
99
+ * Build full URL with baseURL and query params.
100
+ */
101
+ function buildUrl(baseURL, url, params) {
102
+ // Only prepend baseURL if url is relative
103
+ let fullUrl;
104
+ if (baseURL && !isAbsoluteUrl(url)) {
105
+ fullUrl = `${baseURL}${url}`;
106
+ }
107
+ else {
108
+ fullUrl = url;
109
+ }
110
+ if (params && Object.keys(params).length > 0) {
111
+ // Build query string
112
+ const searchParams = new URLSearchParams();
113
+ Object.entries(params).forEach(([key, value]) => {
114
+ searchParams.append(key, String(value));
115
+ });
116
+ const queryString = searchParams.toString();
117
+ // Append query string to original URL
118
+ // Preserve URL format (absolute http://, root-relative /, or path-relative)
119
+ if (fullUrl.includes('?')) {
120
+ // URL already has query params, append with &
121
+ fullUrl = `${fullUrl}&${queryString}`;
122
+ }
123
+ else {
124
+ // Add query params with ?
125
+ fullUrl = `${fullUrl}?${queryString}`;
126
+ }
127
+ }
128
+ return fullUrl;
129
+ }
130
+ /**
131
+ * Build request headers with automatic Content-Type for JSON.
132
+ */
133
+ function buildHeaders(headers, data) {
134
+ const result = { ...headers };
135
+ // Auto-add Content-Type for JSON data (but not for FormData/Blob/ArrayBuffer)
136
+ // Browser automatically sets correct Content-Type for FormData/Blob
137
+ if (data &&
138
+ typeof data === 'object' &&
139
+ !(data instanceof FormData) &&
140
+ !(data instanceof Blob) &&
141
+ !(data instanceof ArrayBuffer) &&
142
+ !result['Content-Type'] &&
143
+ !result['content-type']) {
144
+ result['Content-Type'] = 'application/json';
145
+ }
146
+ return result;
147
+ }
148
+ /**
149
+ * Build request body (auto-stringify JSON objects).
150
+ */
151
+ function buildBody(data) {
152
+ // Only skip for null/undefined (not other falsy values like 0, false, "")
153
+ if (data === null || data === undefined) {
154
+ return undefined;
155
+ }
156
+ // Already serialized (string, FormData, Blob, etc)
157
+ if (typeof data === 'string' || data instanceof FormData || data instanceof Blob || data instanceof ArrayBuffer) {
158
+ return data;
159
+ }
160
+ // Serialize objects to JSON
161
+ if (typeof data === 'object') {
162
+ return JSON.stringify(data);
163
+ }
164
+ // Serialize primitives (numbers, booleans, etc) to string
165
+ return String(data);
166
+ }
167
+ /**
168
+ * Setup abort handling (timeout + manual signal).
169
+ *
170
+ * Returns:
171
+ * - signal: AbortSignal to pass to fetch
172
+ * - cleanup: Function to clear timeout
173
+ */
174
+ function setupAbort(controller, timeout, userSignal) {
175
+ let timeoutId;
176
+ // Link user signal (manual cancellation)
177
+ if (userSignal) {
178
+ if (userSignal.aborted) {
179
+ controller.abort(userSignal.reason);
180
+ }
181
+ else {
182
+ userSignal.addEventListener('abort', () => {
183
+ controller.abort(userSignal.reason);
184
+ }, { once: true });
185
+ }
186
+ }
187
+ // Setup timeout
188
+ if (timeout && timeout > 0) {
189
+ timeoutId = setTimeout(() => {
190
+ controller.abort('timeout');
191
+ }, timeout);
192
+ }
193
+ const cleanup = () => {
194
+ if (timeoutId !== undefined) {
195
+ clearTimeout(timeoutId);
196
+ }
197
+ };
198
+ return { signal: controller.signal, cleanup };
199
+ }
200
+ /**
201
+ * Check if response should be parsed (has a body).
202
+ *
203
+ * Responses without body:
204
+ * - 204 No Content
205
+ * - 205 Reset Content
206
+ * - 304 Not Modified
207
+ * - HEAD requests
208
+ * - Content-Length: 0
209
+ */
210
+ function shouldParseResponseBody(response, method) {
211
+ // Status codes that never have a body
212
+ if (response.status === 204 || response.status === 205 || response.status === 304) {
213
+ return false;
214
+ }
215
+ // HEAD requests never have a body
216
+ if (method.toUpperCase() === 'HEAD') {
217
+ return false;
218
+ }
219
+ // Check Content-Length header
220
+ const contentLength = response.headers.get('Content-Length');
221
+ if (contentLength === '0') {
222
+ return false;
223
+ }
224
+ return true;
225
+ }
226
+ /**
227
+ * Parse response based on responseType.
228
+ */
229
+ async function parseResponse(response, responseType) {
230
+ try {
231
+ switch (responseType) {
232
+ case 'json':
233
+ return await response.json();
234
+ case 'text':
235
+ return await response.text();
236
+ case 'blob':
237
+ return await response.blob();
238
+ case 'arraybuffer':
239
+ return await response.arrayBuffer();
240
+ default:
241
+ return await response.json();
242
+ }
243
+ }
244
+ catch (error) {
245
+ throw new Error(`Failed to parse response as ${responseType}: ${error instanceof Error ? error.message : String(error)}`);
246
+ }
247
+ }
248
+ /**
249
+ * Parse response safely for error handling (never throws).
250
+ */
251
+ async function parseResponseSafe(response, responseType) {
252
+ try {
253
+ return await parseResponse(response, responseType);
254
+ }
255
+ catch {
256
+ // If parsing fails, try text as fallback
257
+ try {
258
+ return await response.text();
259
+ }
260
+ catch {
261
+ return null;
262
+ }
263
+ }
264
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * HTTP Client
3
+ *
4
+ * Main HTTP client factory for Dalila framework.
5
+ * Provides a simple, Axios-inspired API with native fetch under the hood.
6
+ */
7
+ import { type HttpClient, type HttpClientConfig } from './types.js';
8
+ /**
9
+ * Create an HTTP client instance.
10
+ *
11
+ * Features:
12
+ * - Global config (baseURL, headers, timeout)
13
+ * - Interceptors (onRequest, onResponse, onError)
14
+ * - Convenient methods (get, post, put, patch, delete)
15
+ * - Full TypeScript support
16
+ *
17
+ * Example:
18
+ * ```ts
19
+ * const http = createHttpClient({
20
+ * baseURL: 'https://api.example.com',
21
+ * headers: { 'Authorization': 'Bearer token' },
22
+ * timeout: 5000,
23
+ * onRequest: (config) => {
24
+ * console.log('Sending request:', config.url);
25
+ * return config;
26
+ * },
27
+ * onResponse: (response) => {
28
+ * console.log('Received response:', response.status);
29
+ * return response;
30
+ * },
31
+ * onError: (error) => {
32
+ * if (error.status === 401) {
33
+ * // Redirect to login
34
+ * window.location.href = '/login';
35
+ * }
36
+ * throw error;
37
+ * }
38
+ * });
39
+ *
40
+ * // Usage
41
+ * const response = await http.get('/users');
42
+ * await http.post('/login', { email, password });
43
+ * ```
44
+ *
45
+ * @param config - Global client configuration
46
+ * @returns HTTP client instance
47
+ */
48
+ export declare function createHttpClient(config?: HttpClientConfig): HttpClient;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * HTTP Client
3
+ *
4
+ * Main HTTP client factory for Dalila framework.
5
+ * Provides a simple, Axios-inspired API with native fetch under the hood.
6
+ */
7
+ import { fetchAdapter } from './adapter.js';
8
+ import { getXsrfToken, requiresXsrfToken } from './xsrf.js';
9
+ /**
10
+ * Create an HTTP client instance.
11
+ *
12
+ * Features:
13
+ * - Global config (baseURL, headers, timeout)
14
+ * - Interceptors (onRequest, onResponse, onError)
15
+ * - Convenient methods (get, post, put, patch, delete)
16
+ * - Full TypeScript support
17
+ *
18
+ * Example:
19
+ * ```ts
20
+ * const http = createHttpClient({
21
+ * baseURL: 'https://api.example.com',
22
+ * headers: { 'Authorization': 'Bearer token' },
23
+ * timeout: 5000,
24
+ * onRequest: (config) => {
25
+ * console.log('Sending request:', config.url);
26
+ * return config;
27
+ * },
28
+ * onResponse: (response) => {
29
+ * console.log('Received response:', response.status);
30
+ * return response;
31
+ * },
32
+ * onError: (error) => {
33
+ * if (error.status === 401) {
34
+ * // Redirect to login
35
+ * window.location.href = '/login';
36
+ * }
37
+ * throw error;
38
+ * }
39
+ * });
40
+ *
41
+ * // Usage
42
+ * const response = await http.get('/users');
43
+ * await http.post('/login', { email, password });
44
+ * ```
45
+ *
46
+ * @param config - Global client configuration
47
+ * @returns HTTP client instance
48
+ */
49
+ export function createHttpClient(config = {}) {
50
+ const { baseURL = '', headers: defaultHeaders = {}, timeout: defaultTimeout, responseType: defaultResponseType = 'json', xsrf: xsrfConfig, onRequest, onResponse, onError, } = config;
51
+ // Parse XSRF config
52
+ const xsrf = xsrfConfig === true
53
+ ? { cookieName: 'XSRF-TOKEN', headerName: 'X-XSRF-TOKEN', safeMethods: ['GET', 'HEAD', 'OPTIONS'] }
54
+ : xsrfConfig === false || !xsrfConfig
55
+ ? null
56
+ : { cookieName: 'XSRF-TOKEN', headerName: 'X-XSRF-TOKEN', safeMethods: ['GET', 'HEAD', 'OPTIONS'], ...xsrfConfig };
57
+ /**
58
+ * Core request method.
59
+ * Merges global config with per-request config and executes interceptors.
60
+ */
61
+ async function request(requestConfig) {
62
+ // Merge global config with request config
63
+ // Spread requestConfig first, then override with merged values
64
+ let mergedConfig = {
65
+ ...requestConfig,
66
+ baseURL: requestConfig.baseURL ?? baseURL,
67
+ timeout: requestConfig.timeout ?? defaultTimeout,
68
+ responseType: requestConfig.responseType ?? defaultResponseType,
69
+ headers: mergeHeaders(defaultHeaders, requestConfig.headers),
70
+ };
71
+ // XSRF: Add token to header if configured
72
+ if (xsrf) {
73
+ const method = (mergedConfig.method || 'GET');
74
+ const safeMethods = xsrf.safeMethods || ['GET', 'HEAD', 'OPTIONS'];
75
+ if (requiresXsrfToken(method, safeMethods)) {
76
+ const cookieName = xsrf.cookieName || 'XSRF-TOKEN';
77
+ const token = getXsrfToken(cookieName);
78
+ if (token) {
79
+ const headerName = xsrf.headerName || 'X-XSRF-TOKEN';
80
+ mergedConfig.headers = {
81
+ ...mergedConfig.headers,
82
+ [headerName]: token,
83
+ };
84
+ }
85
+ }
86
+ }
87
+ try {
88
+ // Run request interceptor
89
+ if (onRequest) {
90
+ mergedConfig = await onRequest(mergedConfig);
91
+ }
92
+ // Execute request
93
+ let response = await fetchAdapter(mergedConfig);
94
+ // Run response interceptor
95
+ if (onResponse) {
96
+ response = await onResponse(response);
97
+ }
98
+ return response;
99
+ }
100
+ catch (error) {
101
+ // Run error interceptor
102
+ if (onError) {
103
+ await onError(error);
104
+ }
105
+ // Rethrow error
106
+ throw error;
107
+ }
108
+ }
109
+ /**
110
+ * GET request.
111
+ */
112
+ function get(url, requestConfig) {
113
+ return request({
114
+ ...requestConfig,
115
+ url,
116
+ method: 'GET',
117
+ });
118
+ }
119
+ /**
120
+ * POST request.
121
+ */
122
+ function post(url, data, requestConfig) {
123
+ return request({
124
+ ...requestConfig,
125
+ url,
126
+ method: 'POST',
127
+ data,
128
+ });
129
+ }
130
+ /**
131
+ * PUT request.
132
+ */
133
+ function put(url, data, requestConfig) {
134
+ return request({
135
+ ...requestConfig,
136
+ url,
137
+ method: 'PUT',
138
+ data,
139
+ });
140
+ }
141
+ /**
142
+ * PATCH request.
143
+ */
144
+ function patch(url, data, requestConfig) {
145
+ return request({
146
+ ...requestConfig,
147
+ url,
148
+ method: 'PATCH',
149
+ data,
150
+ });
151
+ }
152
+ /**
153
+ * DELETE request.
154
+ */
155
+ function deleteFn(url, requestConfig) {
156
+ return request({
157
+ ...requestConfig,
158
+ url,
159
+ method: 'DELETE',
160
+ });
161
+ }
162
+ return {
163
+ request,
164
+ get,
165
+ post,
166
+ put,
167
+ patch,
168
+ delete: deleteFn,
169
+ };
170
+ }
171
+ /**
172
+ * Merge headers (per-request headers override global headers).
173
+ */
174
+ function mergeHeaders(globalHeaders, requestHeaders) {
175
+ return {
176
+ ...globalHeaders,
177
+ ...requestHeaders,
178
+ };
179
+ }
@@ -0,0 +1,4 @@
1
+ export { createHttpClient } from './client.js';
2
+ export { fetchAdapter } from './adapter.js';
3
+ export type { HttpClient, HttpClientConfig, HttpMethod, HttpResponse, HttpErrorType, RequestConfig, RequestInterceptor, ResponseInterceptor, ErrorInterceptor, Interceptors, XsrfConfig, } from './types.js';
4
+ export { HttpError } from './types.js';
@@ -0,0 +1,3 @@
1
+ export { createHttpClient } from './client.js';
2
+ export { fetchAdapter } from './adapter.js';
3
+ export { HttpError } from './types.js';
@@ -0,0 +1,149 @@
1
+ /**
2
+ * HTTP Client Types
3
+ *
4
+ * Type definitions for the Dalila HTTP client.
5
+ * Designed for simplicity and SPA-first workflows.
6
+ */
7
+ /**
8
+ * HTTP methods supported by the client.
9
+ */
10
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
11
+ /**
12
+ * Request configuration.
13
+ */
14
+ export interface RequestConfig {
15
+ /** Request URL (relative to baseURL if configured). */
16
+ url?: string;
17
+ /** HTTP method (defaults to GET). */
18
+ method?: HttpMethod;
19
+ /** Request headers. */
20
+ headers?: Record<string, string>;
21
+ /** Request body (auto-serialized to JSON if object). */
22
+ data?: any;
23
+ /** URL query parameters. */
24
+ params?: Record<string, string | number | boolean>;
25
+ /** Request timeout in milliseconds. */
26
+ timeout?: number;
27
+ /** AbortSignal for manual cancellation. */
28
+ signal?: AbortSignal;
29
+ /** Response type (defaults to 'json'). */
30
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
31
+ /** Base URL for this request (overrides global baseURL). */
32
+ baseURL?: string;
33
+ }
34
+ /**
35
+ * HTTP response.
36
+ */
37
+ export interface HttpResponse<T = any> {
38
+ /** Response data (parsed). */
39
+ data: T;
40
+ /** HTTP status code. */
41
+ status: number;
42
+ /** HTTP status text. */
43
+ statusText: string;
44
+ /** Response headers. */
45
+ headers: Headers;
46
+ /** Original request config. */
47
+ config: RequestConfig;
48
+ }
49
+ /**
50
+ * Error types for predictable error handling.
51
+ */
52
+ export type HttpErrorType = 'network' | 'timeout' | 'abort' | 'http' | 'parse';
53
+ /**
54
+ * HTTP error with structured information.
55
+ */
56
+ export declare class HttpError extends Error {
57
+ /** Error type (network, timeout, http, etc). */
58
+ type: HttpErrorType;
59
+ /** HTTP status code (if available). */
60
+ status?: number;
61
+ /** Response data (if available). */
62
+ data?: any;
63
+ /** Original request config. */
64
+ config: RequestConfig;
65
+ /** Native Response object (if available). */
66
+ response?: Response;
67
+ constructor(message: string, type: HttpErrorType, config: RequestConfig, options?: {
68
+ status?: number;
69
+ data?: any;
70
+ response?: Response;
71
+ });
72
+ }
73
+ /**
74
+ * Request interceptor.
75
+ * Called before each request is sent.
76
+ * Can modify config or throw to abort the request.
77
+ */
78
+ export type RequestInterceptor = (config: RequestConfig) => RequestConfig | Promise<RequestConfig>;
79
+ /**
80
+ * Response interceptor.
81
+ * Called after each successful response.
82
+ * Can transform the response or throw to convert success to error.
83
+ */
84
+ export type ResponseInterceptor = <T = any>(response: HttpResponse<T>) => HttpResponse<T> | Promise<HttpResponse<T>>;
85
+ /**
86
+ * Error interceptor.
87
+ * Called when a request fails.
88
+ * Can recover from errors or rethrow.
89
+ */
90
+ export type ErrorInterceptor = (error: HttpError) => never | Promise<never>;
91
+ /**
92
+ * Interceptor hooks.
93
+ */
94
+ export interface Interceptors {
95
+ /** Called before each request. */
96
+ onRequest?: RequestInterceptor;
97
+ /** Called after each successful response. */
98
+ onResponse?: ResponseInterceptor;
99
+ /** Called when a request fails. */
100
+ onError?: ErrorInterceptor;
101
+ }
102
+ /**
103
+ * XSRF (CSRF) protection configuration.
104
+ */
105
+ export interface XsrfConfig {
106
+ /** Name of the cookie where the token is stored (default: 'XSRF-TOKEN'). */
107
+ cookieName?: string;
108
+ /** Name of the header to send the token in (default: 'X-XSRF-TOKEN'). */
109
+ headerName?: string;
110
+ /** HTTP methods that don't require XSRF protection (default: ['GET', 'HEAD', 'OPTIONS']). */
111
+ safeMethods?: HttpMethod[];
112
+ }
113
+ /**
114
+ * HTTP client configuration.
115
+ */
116
+ export interface HttpClientConfig extends Interceptors {
117
+ /** Base URL for all requests. */
118
+ baseURL?: string;
119
+ /** Default headers for all requests. */
120
+ headers?: Record<string, string>;
121
+ /** Default timeout in milliseconds. */
122
+ timeout?: number;
123
+ /** Default response type. */
124
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
125
+ /**
126
+ * XSRF (CSRF) protection configuration.
127
+ * - `true`: Enable with defaults (cookieName: 'XSRF-TOKEN', headerName: 'X-XSRF-TOKEN')
128
+ * - `false`: Disable XSRF protection
129
+ * - `XsrfConfig`: Custom configuration
130
+ */
131
+ xsrf?: boolean | XsrfConfig;
132
+ }
133
+ /**
134
+ * HTTP client instance.
135
+ */
136
+ export interface HttpClient {
137
+ /** Make a request with full config. */
138
+ request<T = any>(config: RequestConfig): Promise<HttpResponse<T>>;
139
+ /** GET request. */
140
+ get<T = any>(url: string, config?: Omit<RequestConfig, 'url' | 'method'>): Promise<HttpResponse<T>>;
141
+ /** POST request. */
142
+ post<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<HttpResponse<T>>;
143
+ /** PUT request. */
144
+ put<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<HttpResponse<T>>;
145
+ /** PATCH request. */
146
+ patch<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<HttpResponse<T>>;
147
+ /** DELETE request. */
148
+ delete<T = any>(url: string, config?: Omit<RequestConfig, 'url' | 'method'>): Promise<HttpResponse<T>>;
149
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * HTTP Client Types
3
+ *
4
+ * Type definitions for the Dalila HTTP client.
5
+ * Designed for simplicity and SPA-first workflows.
6
+ */
7
+ /**
8
+ * HTTP error with structured information.
9
+ */
10
+ export class HttpError extends Error {
11
+ constructor(message, type, config, options) {
12
+ super(message);
13
+ this.name = 'HttpError';
14
+ this.type = type;
15
+ this.config = config;
16
+ this.status = options?.status;
17
+ this.data = options?.data;
18
+ this.response = options?.response;
19
+ }
20
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * XSRF (CSRF) Protection Utilities
3
+ *
4
+ * Helpers for reading XSRF tokens from cookies/meta tags and
5
+ * determining which HTTP methods require protection.
6
+ */
7
+ import type { HttpMethod } from './types.js';
8
+ /**
9
+ * Extract value from a cookie by name.
10
+ */
11
+ export declare function getCookie(name: string): string | null;
12
+ /**
13
+ * Extract XSRF token from meta tag.
14
+ */
15
+ export declare function getMetaTag(name: string): string | null;
16
+ /**
17
+ * Get XSRF token (tries cookie first, then meta tag fallback).
18
+ */
19
+ export declare function getXsrfToken(cookieName: string): string | null;
20
+ /**
21
+ * Check if HTTP method requires XSRF token.
22
+ * Safe methods (GET, HEAD, OPTIONS) don't need tokens.
23
+ */
24
+ export declare function requiresXsrfToken(method: HttpMethod, safeMethods: HttpMethod[]): boolean;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * XSRF (CSRF) Protection Utilities
3
+ *
4
+ * Helpers for reading XSRF tokens from cookies/meta tags and
5
+ * determining which HTTP methods require protection.
6
+ */
7
+ /**
8
+ * Extract value from a cookie by name.
9
+ */
10
+ export function getCookie(name) {
11
+ if (typeof document === 'undefined')
12
+ return null;
13
+ const value = `; ${document.cookie}`;
14
+ const parts = value.split(`; ${name}=`);
15
+ if (parts.length === 2) {
16
+ return parts.pop()?.split(';').shift() || null;
17
+ }
18
+ return null;
19
+ }
20
+ /**
21
+ * Extract XSRF token from meta tag.
22
+ */
23
+ export function getMetaTag(name) {
24
+ if (typeof document === 'undefined')
25
+ return null;
26
+ const element = document.querySelector(`meta[name="${name}"]`);
27
+ return element?.getAttribute('content') || null;
28
+ }
29
+ /**
30
+ * Get XSRF token (tries cookie first, then meta tag fallback).
31
+ */
32
+ export function getXsrfToken(cookieName) {
33
+ const fromCookie = getCookie(cookieName);
34
+ if (fromCookie)
35
+ return fromCookie;
36
+ // Fallback to common meta tag names
37
+ return getMetaTag('csrf-token') || getMetaTag('xsrf-token');
38
+ }
39
+ /**
40
+ * Check if HTTP method requires XSRF token.
41
+ * Safe methods (GET, HEAD, OPTIONS) don't need tokens.
42
+ */
43
+ export function requiresXsrfToken(method, safeMethods) {
44
+ return !safeMethods.includes(method);
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dalila",
3
- "version": "1.7.6",
3
+ "version": "1.8.0",
4
4
  "description": "DOM-first reactive framework based on signals",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -38,6 +38,10 @@
38
38
  "types": "./dist/form/index.d.ts",
39
39
  "default": "./dist/form/index.js"
40
40
  },
41
+ "./http": {
42
+ "types": "./dist/http/index.d.ts",
43
+ "default": "./dist/http/index.js"
44
+ },
41
45
  "./components/ui": {
42
46
  "types": "./dist/components/ui/index.d.ts",
43
47
  "default": "./dist/components/ui/index.js"