@sdxc/http 0.0.0-pre.1

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.
@@ -0,0 +1,312 @@
1
+ /**
2
+ * MIME type constants for HTTP `Content-Type` header values.
3
+ *
4
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
5
+ * @copyright Sergio Xalambrí 2026
6
+ */
7
+ /**
8
+ * Used for simple text responses without any formatting.
9
+ * @example
10
+ * headers.set("Content-Type", Text);
11
+ * @example
12
+ * return new Response("Hello", { headers: { "Content-Type": Text } });
13
+ */
14
+ export const Text = "text/plain";
15
+ /**
16
+ * Used for web pages and HTML document responses.
17
+ * @example
18
+ * headers.set("Content-Type", HTML);
19
+ * @example
20
+ * return new Response("<h1>Hello</h1>", { headers: { "Content-Type": HTML } });
21
+ */
22
+ export const HTML = "text/html";
23
+ /**
24
+ * Used for stylesheet responses.
25
+ * @example
26
+ * headers.set("Content-Type", CSS);
27
+ * @example
28
+ * return new Response("body { color: red; }", { headers: { "Content-Type": CSS } });
29
+ */
30
+ export const CSS = "text/css";
31
+ /**
32
+ * Used for JavaScript file responses.
33
+ * @example
34
+ * headers.set("Content-Type", JavaScript);
35
+ * @example
36
+ * return new Response("console.log('hi')", { headers: { "Content-Type": JavaScript } });
37
+ */
38
+ export const JavaScript = "text/javascript";
39
+ /**
40
+ * Used for comma-separated values data exports.
41
+ * @example
42
+ * headers.set("Content-Type", CSV);
43
+ * @example
44
+ * return new Response("name,age\nJohn,30", { headers: { "Content-Type": CSV } });
45
+ */
46
+ export const CSV = "text/csv";
47
+ /**
48
+ * Used for XML documents intended for human readability.
49
+ * @example
50
+ * headers.set("Content-Type", XML);
51
+ * @example
52
+ * return new Response("<root><item/></root>", { headers: { "Content-Type": XML } });
53
+ */
54
+ export const XML = "text/xml";
55
+ /**
56
+ * Used for Markdown-formatted text responses.
57
+ * @example
58
+ * headers.set("Content-Type", Markdown);
59
+ * @example
60
+ * return new Response("# Hello", { headers: { "Content-Type": Markdown } });
61
+ */
62
+ export const Markdown = "text/markdown";
63
+ /**
64
+ * Used for API responses and requests with structured data.
65
+ * Named `Json` (not `JSON`) to avoid shadowing the global `JSON` object.
66
+ * @example
67
+ * headers.set("Content-Type", Json);
68
+ * @example
69
+ * if (contentType === Json) { parseJson(body); }
70
+ */
71
+ export const Json = "application/json";
72
+ /**
73
+ * Used for newline-delimited JSON streaming responses.
74
+ * @example
75
+ * headers.set("Content-Type", JSONLines);
76
+ * @example
77
+ * return new Response('{"a":1}\n{"b":2}', { headers: { "Content-Type": JSONLines } });
78
+ */
79
+ export const JSONLines = "application/jsonl";
80
+ /**
81
+ * PDF content type.
82
+ * Used for PDF document responses.
83
+ * @example
84
+ * headers.set("Content-Type", PDF);
85
+ * @example
86
+ * return new Response(pdfBuffer, { headers: { "Content-Type": PDF } });
87
+ */
88
+ export const PDF = "application/pdf";
89
+ /**
90
+ * ZIP archive content type.
91
+ * Used for compressed archive file responses.
92
+ * @example
93
+ * headers.set("Content-Type", ZIP);
94
+ * @example
95
+ * return new Response(zipBuffer, { headers: { "Content-Type": ZIP } });
96
+ */
97
+ export const ZIP = "application/zip";
98
+ /**
99
+ * GZip compressed content type.
100
+ * Used for gzip-compressed file responses.
101
+ * @example
102
+ * headers.set("Content-Type", GZip);
103
+ * @example
104
+ * return new Response(gzipBuffer, { headers: { "Content-Type": GZip } });
105
+ */
106
+ export const GZip = "application/gzip";
107
+ /**
108
+ * Multipart form data content type.
109
+ * Used for form submissions with file uploads.
110
+ * @example
111
+ * if (contentType.startsWith(FormData)) { parseFormData(body); }
112
+ * @example
113
+ * headers.set("Content-Type", FormData);
114
+ */
115
+ export const FormData = "multipart/form-data";
116
+ /**
117
+ * URL-encoded form content type.
118
+ * Used for simple form submissions without files.
119
+ * @example
120
+ * headers.set("Content-Type", FormURLEncoded);
121
+ * @example
122
+ * if (contentType === FormURLEncoded) { parseURLEncoded(body); }
123
+ */
124
+ export const FormURLEncoded = "application/x-www-form-urlencoded";
125
+ /**
126
+ * Binary octet stream content type.
127
+ * Used for arbitrary binary data when type is unknown.
128
+ * @example
129
+ * headers.set("Content-Type", OctetStream);
130
+ * @example
131
+ * return new Response(binaryData, { headers: { "Content-Type": OctetStream } });
132
+ */
133
+ export const OctetStream = "application/octet-stream";
134
+ /**
135
+ * Used for XML data in API responses and machine processing.
136
+ * @example
137
+ * headers.set("Content-Type", ApplicationXML);
138
+ * @example
139
+ * return new Response(xmlData, { headers: { "Content-Type": ApplicationXML } });
140
+ */
141
+ export const ApplicationXML = "application/xml";
142
+ /**
143
+ * PNG image content type.
144
+ * Used for lossless raster image responses.
145
+ * @example
146
+ * headers.set("Content-Type", PNG);
147
+ * @example
148
+ * return new Response(pngBuffer, { headers: { "Content-Type": PNG } });
149
+ */
150
+ export const PNG = "image/png";
151
+ /**
152
+ * JPEG image content type.
153
+ * Used for lossy compressed photo responses.
154
+ * @example
155
+ * headers.set("Content-Type", JPEG);
156
+ * @example
157
+ * return new Response(jpegBuffer, { headers: { "Content-Type": JPEG } });
158
+ */
159
+ export const JPEG = "image/jpeg";
160
+ /**
161
+ * GIF image content type.
162
+ * Used for animated or simple graphics responses.
163
+ * @example
164
+ * headers.set("Content-Type", GIF);
165
+ * @example
166
+ * return new Response(gifBuffer, { headers: { "Content-Type": GIF } });
167
+ */
168
+ export const GIF = "image/gif";
169
+ /**
170
+ * WebP image content type.
171
+ * Used for modern lossy/lossless image responses.
172
+ * @example
173
+ * headers.set("Content-Type", WebP);
174
+ * @example
175
+ * return new Response(webpBuffer, { headers: { "Content-Type": WebP } });
176
+ */
177
+ export const WebP = "image/webp";
178
+ /**
179
+ * SVG image content type.
180
+ * Used for vector graphics responses.
181
+ * @example
182
+ * headers.set("Content-Type", SVG);
183
+ * @example
184
+ * return new Response(svgString, { headers: { "Content-Type": SVG } });
185
+ */
186
+ export const SVG = "image/svg+xml";
187
+ /**
188
+ * ICO image content type.
189
+ * Used for favicon and icon file responses.
190
+ * @example
191
+ * headers.set("Content-Type", ICO);
192
+ * @example
193
+ * return new Response(icoBuffer, { headers: { "Content-Type": ICO } });
194
+ */
195
+ export const ICO = "image/x-icon";
196
+ /**
197
+ * AVIF image content type.
198
+ * Used for highly compressed modern image responses.
199
+ * @example
200
+ * headers.set("Content-Type", AVIF);
201
+ * @example
202
+ * return new Response(avifBuffer, { headers: { "Content-Type": AVIF } });
203
+ */
204
+ export const AVIF = "image/avif";
205
+ /**
206
+ * MP3 audio content type.
207
+ * Used for MPEG audio file responses.
208
+ * @example
209
+ * headers.set("Content-Type", MP3);
210
+ * @example
211
+ * return new Response(mp3Buffer, { headers: { "Content-Type": MP3 } });
212
+ */
213
+ export const MP3 = "audio/mpeg";
214
+ /**
215
+ * WAV audio content type.
216
+ * Used for uncompressed audio file responses.
217
+ * @example
218
+ * headers.set("Content-Type", WAV);
219
+ * @example
220
+ * return new Response(wavBuffer, { headers: { "Content-Type": WAV } });
221
+ */
222
+ export const WAV = "audio/wav";
223
+ /**
224
+ * OGG audio content type.
225
+ * Used for Ogg Vorbis audio file responses.
226
+ * @example
227
+ * headers.set("Content-Type", OGG);
228
+ * @example
229
+ * return new Response(oggBuffer, { headers: { "Content-Type": OGG } });
230
+ */
231
+ export const OGG = "audio/ogg";
232
+ /**
233
+ * WebM audio content type.
234
+ * Used for WebM audio-only file responses.
235
+ * @example
236
+ * headers.set("Content-Type", WebMAudio);
237
+ * @example
238
+ * return new Response(webmAudioBuffer, { headers: { "Content-Type": WebMAudio } });
239
+ */
240
+ export const WebMAudio = "audio/webm";
241
+ /**
242
+ * MP4 video content type.
243
+ * Used for MPEG-4 video file responses.
244
+ * @example
245
+ * headers.set("Content-Type", MP4);
246
+ * @example
247
+ * return new Response(mp4Buffer, { headers: { "Content-Type": MP4 } });
248
+ */
249
+ export const MP4 = "video/mp4";
250
+ /**
251
+ * WebM video content type.
252
+ * Used for WebM video file responses.
253
+ * @example
254
+ * headers.set("Content-Type", WebMVideo);
255
+ * @example
256
+ * return new Response(webmVideoBuffer, { headers: { "Content-Type": WebMVideo } });
257
+ */
258
+ export const WebMVideo = "video/webm";
259
+ /**
260
+ * WOFF font content type.
261
+ * Used for Web Open Font Format file responses.
262
+ * @example
263
+ * headers.set("Content-Type", WOFF);
264
+ * @example
265
+ * return new Response(woffBuffer, { headers: { "Content-Type": WOFF } });
266
+ */
267
+ export const WOFF = "font/woff";
268
+ /**
269
+ * WOFF2 font content type.
270
+ * Used for Web Open Font Format 2.0 file responses.
271
+ * @example
272
+ * headers.set("Content-Type", WOFF2);
273
+ * @example
274
+ * return new Response(woff2Buffer, { headers: { "Content-Type": WOFF2 } });
275
+ */
276
+ export const WOFF2 = "font/woff2";
277
+ /**
278
+ * TrueType font content type.
279
+ * Used for TTF font file responses.
280
+ * @example
281
+ * headers.set("Content-Type", TTF);
282
+ * @example
283
+ * return new Response(ttfBuffer, { headers: { "Content-Type": TTF } });
284
+ */
285
+ export const TTF = "font/ttf";
286
+ /**
287
+ * OpenType font content type.
288
+ * Used for OTF font file responses.
289
+ * @example
290
+ * headers.set("Content-Type", OTF);
291
+ * @example
292
+ * return new Response(otfBuffer, { headers: { "Content-Type": OTF } });
293
+ */
294
+ export const OTF = "font/otf";
295
+ /**
296
+ * Server-Sent Events content type.
297
+ * Used for streaming event responses to clients.
298
+ * @example
299
+ * headers.set("Content-Type", EventStream);
300
+ * @example
301
+ * return new Response(stream, { headers: { "Content-Type": EventStream } });
302
+ */
303
+ export const EventStream = "text/event-stream";
304
+ /**
305
+ * Newline-delimited JSON content type.
306
+ * Used for streaming JSON objects one per line.
307
+ * @example
308
+ * headers.set("Content-Type", NDJson);
309
+ * @example
310
+ * return new Response(ndjsonStream, { headers: { "Content-Type": NDJson } });
311
+ */
312
+ export const NDJson = "application/x-ndjson";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Router middleware that answers `HEAD` requests with the `GET` representation's
3
+ * status and headers and no body, as RFC 9110 §9.3.2 requires. The fetch router
4
+ * matches a route's method by strict equality, so without this a `HEAD` probe
5
+ * falls through to the default handler and a monitor reads a healthy page as a
6
+ * 404.
7
+ *
8
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
9
+ * @copyright Sergio Xalambrí 2026
10
+ */
11
+ import type { Middleware } from "remix/router";
12
+ /**
13
+ * Creates a middleware that dispatches `HEAD` requests as `GET`, strips the
14
+ * response body, and keeps `HEAD` on the same chain as `GET` so auth, rate
15
+ * limiting, and other guards behave identically; install it first in the chain.
16
+ *
17
+ * @returns The middleware to place at the head of the router's global chain.
18
+ */
19
+ export declare function headRequests(): Middleware;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Router middleware that answers `HEAD` requests with the `GET` representation's
3
+ * status and headers and no body, as RFC 9110 §9.3.2 requires. The fetch router
4
+ * matches a route's method by strict equality, so without this a `HEAD` probe
5
+ * falls through to the default handler and a monitor reads a healthy page as a
6
+ * 404.
7
+ *
8
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
9
+ * @copyright Sergio Xalambrí 2026
10
+ */
11
+ /**
12
+ * Creates a middleware that dispatches `HEAD` requests as `GET`, strips the
13
+ * response body, and keeps `HEAD` on the same chain as `GET` so auth, rate
14
+ * limiting, and other guards behave identically; install it first in the chain.
15
+ *
16
+ * @returns The middleware to place at the head of the router's global chain.
17
+ */
18
+ export function headRequests() {
19
+ return async function headRequestsMiddleware(context, next) {
20
+ if (context.method !== "HEAD")
21
+ return next();
22
+ context.method = "GET";
23
+ let response = await next();
24
+ return new Response(null, {
25
+ status: response.status,
26
+ statusText: response.statusText,
27
+ headers: response.headers,
28
+ });
29
+ };
30
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Accept-header content negotiation: parse quality-ordered client
3
+ * preferences and pick the response representation that matches.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ /**
9
+ * Holds an Accept header's types ordered by quality so callers can query
10
+ * preference without re-parsing on every check.
11
+ */
12
+ export declare class AcceptList {
13
+ private types;
14
+ constructor(header: string);
15
+ /**
16
+ * Matches shorthand types ("json", "html", "xml", "text", "markdown") as
17
+ * well as full MIME types, and treats a wildcard Accept header as
18
+ * matching any type.
19
+ * @param type - The content type or shorthand to check
20
+ * @returns True if the type is accepted
21
+ * @example
22
+ * if (accepts(request).includes("json")) { ... }
23
+ * @example
24
+ * if (accepts(request).includes("text/html")) { ... }
25
+ */
26
+ includes(type: string): boolean;
27
+ /**
28
+ * Accepted types sorted by preference, highest quality first.
29
+ * @returns Array of MIME types sorted by preference
30
+ * @example
31
+ * let types = accepts(request).all(); // ["text/html", "application/json", ...]
32
+ */
33
+ all(): string[];
34
+ /**
35
+ * Walks the client's accepted types in preference order and returns the
36
+ * first one present in `types`; a wildcard Accept header matches the
37
+ * first of `types` rather than every candidate.
38
+ * @param types - Available content types to choose from
39
+ * @returns The preferred type or null
40
+ * @example
41
+ * let preferred = accepts(request).preferred("application/json", "text/html");
42
+ */
43
+ preferred(...types: string[]): string | null;
44
+ /**
45
+ * Converts a MIME type to its shorthand form.
46
+ * @param mimeType - The full MIME type
47
+ * @returns The shorthand type or null if not recognized
48
+ */
49
+ toShortType(mimeType: string): string | null;
50
+ private parseAcceptHeader;
51
+ private resolveType;
52
+ }
53
+ /**
54
+ * Reads the Accept header, defaulting to accepting every type when the
55
+ * client sends none.
56
+ * @param request - The incoming request
57
+ * @returns An AcceptList instance for querying accepted types
58
+ * @example
59
+ * if (accepts(request).includes("json")) return json(data);
60
+ * @example
61
+ * let types = accepts(request).all();
62
+ */
63
+ export declare function accepts(request: Request): AcceptList;
64
+ /**
65
+ * Calls the handler for the client's preferred type, trying candidates in
66
+ * quality order; answers 406 Not Acceptable when nothing matches and no
67
+ * `default` handler is given.
68
+ * @param request - The incoming request
69
+ * @param handlers - Object mapping content types to response handlers
70
+ * @returns The response from the matching handler
71
+ * @example
72
+ * return respond(request, {
73
+ * json: () => json({ data }),
74
+ * html: () => html(renderPage(data)),
75
+ * });
76
+ * @example
77
+ * return respond(request, {
78
+ * json: () => json({ data }),
79
+ * default: () => json({ data }), // fallback
80
+ * });
81
+ */
82
+ export declare function respond(request: Request, handlers: respond.Handlers): Response;
83
+ export declare namespace respond {
84
+ type Handlers = {
85
+ json?: () => Response;
86
+ html?: () => Response;
87
+ xml?: () => Response;
88
+ text?: () => Response;
89
+ markdown?: () => Response;
90
+ css?: () => Response;
91
+ javascript?: () => Response;
92
+ csv?: () => Response;
93
+ pdf?: () => Response;
94
+ default?: () => Response;
95
+ [key: string]: (() => Response) | undefined;
96
+ };
97
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Accept-header content negotiation: parse quality-ordered client
3
+ * preferences and pick the response representation that matches.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ const shortToMime = new Map([
9
+ ["json", ["application/json"]],
10
+ ["html", ["text/html"]],
11
+ ["xml", ["application/xml", "text/xml"]],
12
+ ["text", ["text/plain"]],
13
+ ["markdown", ["text/markdown"]],
14
+ ["css", ["text/css"]],
15
+ ["javascript", ["text/javascript", "application/javascript"]],
16
+ ["csv", ["text/csv"]],
17
+ ["pdf", ["application/pdf"]],
18
+ ]);
19
+ const mimeToShort = new Map([
20
+ ["application/json", "json"],
21
+ ["text/html", "html"],
22
+ ["application/xml", "xml"],
23
+ ["text/xml", "xml"],
24
+ ["text/plain", "text"],
25
+ ["text/markdown", "markdown"],
26
+ ["text/css", "css"],
27
+ ["text/javascript", "javascript"],
28
+ ["application/javascript", "javascript"],
29
+ ["text/csv", "csv"],
30
+ ["application/pdf", "pdf"],
31
+ ]);
32
+ /**
33
+ * Holds an Accept header's types ordered by quality so callers can query
34
+ * preference without re-parsing on every check.
35
+ */
36
+ export class AcceptList {
37
+ types;
38
+ constructor(header) {
39
+ this.types = this.parseAcceptHeader(header);
40
+ }
41
+ /**
42
+ * Matches shorthand types ("json", "html", "xml", "text", "markdown") as
43
+ * well as full MIME types, and treats a wildcard Accept header as
44
+ * matching any type.
45
+ * @param type - The content type or shorthand to check
46
+ * @returns True if the type is accepted
47
+ * @example
48
+ * if (accepts(request).includes("json")) { ... }
49
+ * @example
50
+ * if (accepts(request).includes("text/html")) { ... }
51
+ */
52
+ includes(type) {
53
+ let mimeTypes = this.resolveType(type);
54
+ return this.types.some((t) => mimeTypes.includes(t) || t === "*/*");
55
+ }
56
+ /**
57
+ * Accepted types sorted by preference, highest quality first.
58
+ * @returns Array of MIME types sorted by preference
59
+ * @example
60
+ * let types = accepts(request).all(); // ["text/html", "application/json", ...]
61
+ */
62
+ all() {
63
+ return this.types;
64
+ }
65
+ /**
66
+ * Walks the client's accepted types in preference order and returns the
67
+ * first one present in `types`; a wildcard Accept header matches the
68
+ * first of `types` rather than every candidate.
69
+ * @param types - Available content types to choose from
70
+ * @returns The preferred type or null
71
+ * @example
72
+ * let preferred = accepts(request).preferred("application/json", "text/html");
73
+ */
74
+ preferred(...types) {
75
+ for (let accepted of this.types) {
76
+ if (types.includes(accepted))
77
+ return accepted;
78
+ if (accepted === "*/*")
79
+ return types[0] ?? null;
80
+ }
81
+ return null;
82
+ }
83
+ /**
84
+ * Converts a MIME type to its shorthand form.
85
+ * @param mimeType - The full MIME type
86
+ * @returns The shorthand type or null if not recognized
87
+ */
88
+ toShortType(mimeType) {
89
+ return mimeToShort.get(mimeType) ?? null;
90
+ }
91
+ parseAcceptHeader(header) {
92
+ return header
93
+ .split(",")
94
+ .map((part) => {
95
+ let [type = "", ...params] = part.trim().split(";");
96
+ let q = 1;
97
+ for (let param of params) {
98
+ let [key, value = ""] = param.trim().split("=");
99
+ if (key === "q")
100
+ q = Number.parseFloat(value) || 0;
101
+ }
102
+ return { type: type.trim(), q };
103
+ })
104
+ .sort((a, b) => b.q - a.q)
105
+ .map(({ type }) => type);
106
+ }
107
+ resolveType(type) {
108
+ return shortToMime.get(type) ?? [type];
109
+ }
110
+ }
111
+ /**
112
+ * Reads the Accept header, defaulting to accepting every type when the
113
+ * client sends none.
114
+ * @param request - The incoming request
115
+ * @returns An AcceptList instance for querying accepted types
116
+ * @example
117
+ * if (accepts(request).includes("json")) return json(data);
118
+ * @example
119
+ * let types = accepts(request).all();
120
+ */
121
+ export function accepts(request) {
122
+ let header = request.headers.get("Accept") ?? "*/*";
123
+ return new AcceptList(header);
124
+ }
125
+ /**
126
+ * Calls the handler for the client's preferred type, trying candidates in
127
+ * quality order; answers 406 Not Acceptable when nothing matches and no
128
+ * `default` handler is given.
129
+ * @param request - The incoming request
130
+ * @param handlers - Object mapping content types to response handlers
131
+ * @returns The response from the matching handler
132
+ * @example
133
+ * return respond(request, {
134
+ * json: () => json({ data }),
135
+ * html: () => html(renderPage(data)),
136
+ * });
137
+ * @example
138
+ * return respond(request, {
139
+ * json: () => json({ data }),
140
+ * default: () => json({ data }), // fallback
141
+ * });
142
+ */
143
+ export function respond(request, handlers) {
144
+ let accept = accepts(request);
145
+ for (let type of accept.all()) {
146
+ let shortType = accept.toShortType(type);
147
+ if (shortType && handlers[shortType])
148
+ return handlers[shortType]();
149
+ }
150
+ if (handlers.default)
151
+ return handlers.default();
152
+ return new Response("Not Acceptable", {
153
+ status: 406,
154
+ statusText: "Not Acceptable",
155
+ });
156
+ }