@gauts/ft 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GVALFER
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,403 @@
1
+ # @gauts/ft
2
+
3
+ A small, typed HTTP client built on the native Fetch API.
4
+
5
+ - ✅ Ready-to-use default client
6
+ - ✅ Configured instances with `ft.create()`
7
+ - ✅ Typed response shortcuts
8
+ - ✅ JSON bodies and search parameters
9
+ - ✅ Timeout and opt-in retries
10
+ - ✅ Typed HTTP, network, and timeout errors
11
+ - ✅ Upload and download progress with native streams
12
+ - ✅ Request, response, retry, error, and status callbacks
13
+ - ✅ Automatic browser/server base URL selection
14
+ - ✅ Opt-in server header forwarding through a safe allowlist
15
+ - ✅ Native `RequestInit` options
16
+ - ✅ Zero runtime dependencies
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @gauts/ft
22
+ ```
23
+
24
+ Node.js 22 or a modern browser with the native Fetch API is required.
25
+
26
+ ## Quick start
27
+
28
+ Import the ready-to-use client:
29
+
30
+ ```ts
31
+ import ft from "@gauts/ft";
32
+
33
+ type Account = {
34
+ email: string;
35
+ id: string;
36
+ };
37
+
38
+ const account = await ft.get("https://api.example.com/accounts/123").json<Account>();
39
+ ```
40
+
41
+ Or create a configured instance:
42
+
43
+ ```ts
44
+ import ft from "@gauts/ft";
45
+
46
+ export const api = ft.create({
47
+ baseUrl: "https://api.example.com",
48
+ headers: {
49
+ accept: "application/json",
50
+ },
51
+ prefix: "v1",
52
+ });
53
+
54
+ const account = await api.get("accounts/123").json<Account>();
55
+ ```
56
+
57
+ The resulting URL is `https://api.example.com/v1/accounts/123`.
58
+
59
+ For applications with separate browser and server paths:
60
+
61
+ ```ts
62
+ const api = ft.create({
63
+ baseUrl: {
64
+ client: "/proxy",
65
+ server: "http://api:4000",
66
+ },
67
+ });
68
+ ```
69
+
70
+ The browser uses `client`; Node.js and other runtimes without `window` use `server`.
71
+
72
+ ## Methods
73
+
74
+ All methods accept an optional URL and request options:
75
+
76
+ ```ts
77
+ ft.get(url, options);
78
+ ft.post(url, options);
79
+ ft.put(url, options);
80
+ ft.patch(url, options);
81
+ ft.delete(url, options);
82
+ ft.head(url, options);
83
+ ```
84
+
85
+ Calling a method starts one request operation and returns a `FetchTask`. The task can be awaited as a native `Response` or consumed through a body shortcut.
86
+
87
+ ```ts
88
+ const response = await api.get("accounts/123");
89
+ const sameResponse = await api.get("accounts/123").response();
90
+ ```
91
+
92
+ ## Request bodies
93
+
94
+ Use `json` to serialize a JSON body and set `content-type: application/json` when it is not already configured:
95
+
96
+ ```ts
97
+ const account = await api
98
+ .post("accounts", {
99
+ json: {
100
+ email: "user@example.com",
101
+ name: "Example User",
102
+ },
103
+ })
104
+ .json<Account>();
105
+ ```
106
+
107
+ Use `body` for any native `BodyInit` value:
108
+
109
+ ```ts
110
+ const form = new FormData();
111
+ form.set("avatar", file);
112
+
113
+ const account = await api
114
+ .post("accounts/avatar", {
115
+ body: form,
116
+ })
117
+ .json<Account>();
118
+ ```
119
+
120
+ `json` and `body` are mutually exclusive.
121
+
122
+ ## Search parameters
123
+
124
+ Search parameters can be configured on the instance and overridden per request:
125
+
126
+ ```ts
127
+ const api = ft.create({
128
+ baseUrl: "https://api.example.com",
129
+ searchParams: {
130
+ locale: "en",
131
+ },
132
+ });
133
+
134
+ const accounts = await api
135
+ .get("accounts", {
136
+ searchParams: {
137
+ page: 2,
138
+ role: ["OWNER", "ADMIN"],
139
+ },
140
+ })
141
+ .json<Account[]>();
142
+ ```
143
+
144
+ Supported values are strings, numbers, booleans, `null`, `undefined`, and arrays of those values. `null` and `undefined` are omitted. Request parameters replace instance parameters with the same name.
145
+
146
+ ## Response shortcuts
147
+
148
+ ```ts
149
+ api.get("data").json<MyType>();
150
+ api.get("data").text();
151
+ api.get("data").blob();
152
+ api.get("data").arrayBuffer();
153
+ api.get("data").bytes();
154
+ api.get("data").formData();
155
+ api.get("data").response();
156
+ ```
157
+
158
+ `json<T>()` defaults to `unknown`. The generic type provides compile-time typing only; it does not validate the response at runtime. Empty or invalid JSON rejects with the native parsing error.
159
+
160
+ ## Configuration
161
+
162
+ ### Instance options
163
+
164
+ | Property | Type | Default | Description |
165
+ | ----------------- | -------------------------------- | ------- | ----------------------------------------------------- |
166
+ | `baseUrl` | `string \| URL \| RuntimeBaseUrl` | — | Static URL or automatic client/server URLs. |
167
+ | `prefix` | `string` | — | Path inserted between `baseUrl` and the request path. |
168
+ | `searchParams` | `SearchParams` | — | Parameters included in every request. |
169
+ | `headers` | `HeadersInit` | — | Headers included in every request. |
170
+ | `forwardHeaders` | `boolean \| { extra: string[] }` | `false` | Forwards allowlisted incoming headers on the server. |
171
+ | `getHeaders` | `() => HeadersInit \| Promise<HeadersInit>` | — | Provides the current incoming server headers. |
172
+ | `timeout` | `number \| false` | `false` | Request timeout in milliseconds. |
173
+ | `retry` | `number \| RetryConfig \| false` | `false` | Enables retries. A number is the retry limit. |
174
+ | `throwHttpErrors` | `boolean` | `true` | Throws `HTTPError` for non-2xx responses. |
175
+ | `beforeRequest` | `BeforeRequest` | — | Runs before every attempt. |
176
+ | `afterResponse` | `AfterResponse` | — | Runs after every received response. |
177
+ | `onRetry` | `OnRetry` | — | Runs before a retry delay. |
178
+ | `onError` | `OnError` | — | Observes or replaces the final error. |
179
+ | `onStatus` | `StatusHandlers` | — | Runs an action for the final response status. |
180
+
181
+ All other native `RequestInit` properties, such as `cache`, `credentials`, `mode`, and `redirect`, are supported.
182
+
183
+ ### Request options
184
+
185
+ Request options support the same reliability, lifecycle, and native options, plus:
186
+
187
+ | Property | Type | Description |
188
+ | -------------------- | -------------------- | ----------------------------------------------------------------- |
189
+ | `json` | `unknown` | Serializes a JSON request body. |
190
+ | `body` | `BodyInit \| null` | Sends a native request body. |
191
+ | `searchParams` | `SearchParams` | Adds or replaces search parameters. |
192
+ | `signal` | `AbortSignal` | Cancels the request without being replaced by the timeout signal. |
193
+ | `onUploadProgress` | `(progress) => void` | Reports native upload stream progress. |
194
+ | `onDownloadProgress` | `(progress) => void` | Reports native download stream progress. |
195
+
196
+ Request-specific lifecycle callbacks replace the matching instance callback. They are not silently chained.
197
+
198
+ ## Runtime URLs and header forwarding
199
+
200
+ Runtime selection and header filtering are framework-independent. Only the function that obtains the current incoming request headers belongs to the application:
201
+
202
+ ```ts
203
+ const api = ft.create({
204
+ baseUrl: {
205
+ client: "/proxy",
206
+ server: process.env.API_URL!,
207
+ },
208
+
209
+ getHeaders: async () => {
210
+ const { headers } = await import("next/headers");
211
+ return headers();
212
+ },
213
+
214
+ forwardHeaders: true,
215
+ });
216
+ ```
217
+
218
+ `forwardHeaders: true` enables the built-in allowlist:
219
+
220
+ ```text
221
+ accept-language
222
+ cf-connecting-ip
223
+ origin
224
+ referer
225
+ sec-ch-ua
226
+ sec-ch-ua-mobile
227
+ sec-ch-ua-platform
228
+ sec-fetch-dest
229
+ sec-fetch-mode
230
+ sec-fetch-site
231
+ sec-fetch-user
232
+ true-client-ip
233
+ user-agent
234
+ x-forwarded-for
235
+ x-forwarded-host
236
+ x-forwarded-port
237
+ x-forwarded-proto
238
+ x-real-ip
239
+ ```
240
+
241
+ Add application-specific headers without replacing the defaults:
242
+
243
+ ```ts
244
+ const api = ft.create({
245
+ baseUrl: {
246
+ client: "/proxy",
247
+ server: process.env.API_URL!,
248
+ },
249
+ getHeaders,
250
+ forwardHeaders: {
251
+ extra: ["x-tenant-id"],
252
+ },
253
+ });
254
+ ```
255
+
256
+ The property is disabled when omitted or set to `false`. On the server, enabling it without `getHeaders` throws a configuration error. In the browser, `getHeaders` is not called because the browser controls its own outgoing request headers.
257
+
258
+ `cookie` and `authorization` are intentionally excluded from the default allowlist. Add them explicitly only when the destination is trusted:
259
+
260
+ ```ts
261
+ forwardHeaders: {
262
+ extra: ["cookie", "authorization"],
263
+ }
264
+ ```
265
+
266
+ Forwarded headers have the lowest priority. Instance headers, headers from an input `Request`, and request-specific headers override them in that order. `getHeaders` is called once per operation, not once per retry.
267
+
268
+ The application and its reverse proxy remain responsible for ensuring IP and forwarding headers are trustworthy before they reach the fetcher.
269
+
270
+ ## Retries
271
+
272
+ Retries are disabled by default. Enable them with a number:
273
+
274
+ ```ts
275
+ const api = ft.create({
276
+ retry: 2,
277
+ });
278
+ ```
279
+
280
+ Or configure them explicitly:
281
+
282
+ ```ts
283
+ const api = ft.create({
284
+ retry: {
285
+ baseDelay: 300,
286
+ jitter: true,
287
+ limit: 2,
288
+ maxDelay: 30_000,
289
+ methods: ["GET", "HEAD"],
290
+ statusCodes: [408, 429, 500, 502, 503, 504],
291
+ },
292
+ });
293
+ ```
294
+
295
+ Only `GET` and `HEAD` are retried by default. Add mutation methods explicitly only when the endpoint is idempotent. Request streams are never replayed or buffered silently. `Retry-After` is respected and capped by `maxDelay`.
296
+
297
+ ## Timeout and cancellation
298
+
299
+ ```ts
300
+ const account = await api
301
+ .get("accounts/123", {
302
+ timeout: 10_000,
303
+ })
304
+ .json<Account>();
305
+ ```
306
+
307
+ The timeout covers all attempts and retry delays until the final response headers are received. A timeout throws `TimeoutError`. A user-provided `AbortSignal` remains independent and preserves its own abort reason.
308
+
309
+ ## Lifecycle
310
+
311
+ ```ts
312
+ const api = ft.create({
313
+ beforeRequest: ({ attempt, request }) => {
314
+ request.headers.set("x-attempt", String(attempt));
315
+ },
316
+
317
+ afterResponse: ({ response }) => {
318
+ console.log(response.status);
319
+ },
320
+
321
+ onRetry: ({ attempt, delay, error }) => {
322
+ console.log({ attempt, delay, error });
323
+ },
324
+
325
+ onError: ({ error }) => {
326
+ return new Error("API request failed", { cause: error });
327
+ },
328
+ });
329
+ ```
330
+
331
+ The order is:
332
+
333
+ ```text
334
+ beforeRequest
335
+ -> fetch
336
+ -> afterResponse
337
+ -> onRetry (when another attempt will run)
338
+ -> onStatus (final response only)
339
+ -> onError (final fetcher error only)
340
+ ```
341
+
342
+ `afterResponse` may return a replacement `Response`. `onError` may return a replacement `Error`.
343
+
344
+ ## Status actions
345
+
346
+ `onStatus` runs after retries and before an `HTTPError` is created:
347
+
348
+ ```ts
349
+ const api = ft.create({
350
+ onStatus: {
351
+ 401: ({ response }) => {
352
+ console.log("Unauthorized", response.url);
353
+ },
354
+ 503: () => {
355
+ throw new Error("Maintenance mode");
356
+ },
357
+ },
358
+ });
359
+ ```
360
+
361
+ Errors thrown by a status action propagate unchanged and do not pass through `onError`. This allows the application to use its own routing or control-flow mechanism.
362
+
363
+ ## Progress
364
+
365
+ ```ts
366
+ await api
367
+ .post("upload", {
368
+ body: file,
369
+ onUploadProgress: ({ percent, transferred, total }) => {
370
+ console.log({ percent, transferred, total });
371
+ },
372
+ })
373
+ .json();
374
+
375
+ await api
376
+ .get("download", {
377
+ onDownloadProgress: ({ percent, transferred, total }) => {
378
+ console.log({ percent, transferred, total });
379
+ },
380
+ })
381
+ .blob();
382
+ ```
383
+
384
+ `total` and `percent` are `null` when the runtime or server does not provide a known size. Upload progress depends on native request stream support. The package does not switch to XMLHttpRequest or another transport.
385
+
386
+ ## Errors
387
+
388
+ ```ts
389
+ import { FetchError, HTTPError, NetworkError, TimeoutError } from "@gauts/ft";
390
+ ```
391
+
392
+ - `HTTPError` exposes `request` and `response`.
393
+ - `NetworkError` exposes `request` and the native error through `cause`.
394
+ - `TimeoutError` exposes `request` and `timeout`.
395
+ - `FetchError` is the shared base class.
396
+
397
+ ## Current scope
398
+
399
+ This version contains only the framework-independent native Fetch client. It can select browser/server URLs and filter incoming headers, but it never imports a framework or discovers a framework request context by itself. The consuming application provides that context through `getHeaders`. Authentication, session management, and application caching remain outside the package.
400
+
401
+ ## License
402
+
403
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,20 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please do not disclose security vulnerabilities through a public issue.
6
+
7
+ Send a private report to the repository owner with:
8
+
9
+ - the affected version;
10
+ - a clear reproduction;
11
+ - the expected and observed behavior;
12
+ - the potential impact.
13
+
14
+ Reports will be reviewed before a public fix or advisory is published.
15
+
16
+ ## Package scope
17
+
18
+ `@gauts/ft` is an HTTP client. It does not provide authentication, authorization, runtime response validation, secret storage, CSRF protection, or application-level caching. Applications remain responsible for validating remote data and applying their own security policy.
19
+
20
+ Server header forwarding is disabled by default. `cookie` and `authorization` are not part of the built-in allowlist and must be added explicitly. Applications must only forward credentials and proxy-derived IP headers to trusted destinations.
@@ -0,0 +1,3 @@
1
+ import type { Fetcher, FetcherConfig } from "./types.js";
2
+ export declare const createFetcher: (config?: FetcherConfig) => Fetcher;
3
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACR,OAAO,EACP,aAAa,EAOhB,MAAM,YAAY,CAAC;AAucpB,eAAO,MAAM,aAAa,GAAI,SAAQ,aAAkB,KAAG,OAOzD,CAAC"}