@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
640 lines (639 loc) • 18.7 kB
TypeScript
/**
* Utility functions for handling HTTP related tasks, such as parsing headers.
* @module
* @experimental
*/
export * from "./user-agent.ts";
/**
* Represents the HTTP request `Accept`, `Accept-Encoding` and `Accept-Language`
* headers.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language
*/
export interface Accept {
/**
* The MIME type of the `Accept` header, the encoding of the
* `Accept-Encoding` header, or the language of the `Accept-Language` header.
*/
type: string;
/**
* q-factor value, which represents the relative quality factor of the media
* type, encoding or language.
*/
weight: number;
}
/**
* Parses the `Accept`, `Accept-Encoding` and `Accept-Language` headers.
*
* NOTE: This function automatically sorts the results by the q-factor value in
* descending order.
*
* @example
* ```ts
* import { parseAccepts } from "@ayonli/jsext/http";
*
* const accepts = parseAccepts("text/html,application/xhtml+xml;q=0.9");
* console.log(accepts);
* // [
* // { value: "text/html", weight: 1 },
* // { value: "application/xhtml+xml", weight: 0.9 }
* // ]
*
* const acceptEncodings = parseAccepts("gzip, deflate, br;q=0.8");
* console.log(acceptEncodings);
* // [
* // { value: "gzip", weight: 1 },
* // { value: "deflate", weight: 1 },
* // { value: "br", weight: 0.8 }
* // ]
*
* const acceptLanguages = parseAccepts("en-US,en;q=0.9");
* console.log(acceptLanguages);
* // [
* // { value: "en-US", weight: 1 },
* // { value: "en", weight: 0.9 }
* // ]
* ```
*/
export declare function parseAccepts(str: string): Accept[];
/**
* Represents the HTTP request or response `Content-Type` header.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
*/
export interface ContentType {
/**
* The MIME type of the resource.
*/
type: string;
/**
* The character encoding of the resource.
*/
charset?: string;
/**
* The boundary string used in `multipart/*` types.
*/
boundary?: string;
}
/**
* Parses the `Content-Type` header.
*
* @example
* ```ts
* import { parseContentType } from "@ayonli/jsext/http";
*
* const type = parseContentType("text/html; charset=utf-8");
* console.log(type);
* // { type: "text/html", charset: "utf-8" }
*
* const type2 = parseContentType("multipart/form-data; boundary=----WebKitFormBoundaryzjK4sVZ2QeZvz5zB");
* console.log(type2);
* // { type: "multipart/form-data", boundary: "----WebKitFormBoundaryzjK4sVZ2QeZvz5zB" }
* ```
*/
export declare function parseContentType(str: string): ContentType;
/**
* Represents an HTTP Cookie.
*
* @sse https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie
* @see https://developer.mozilla.org/en-US/docs/Web/API/CookieStore/get
*/
export interface Cookie {
/**
* The name of the cookie.
*/
name: string;
/**
* The value of the cookie.
*/
value: string;
/**
* Defines the host to which the cookie will be sent.
*/
domain?: string;
/**
* Indicates the path that must exist in the requested URL for the browser
* to send the Cookie header.
*/
path?: string;
/**
* The expiration time of the cookie in milliseconds since the Unix epoch.
* If the value is equal to or less than the current time, the cookie will
* be expired immediately.
*/
expires?: number;
/**
* The number of seconds until the cookie expires. A zero or negative number
* will expire the cookie immediately. If both `expires` and `maxAge` are
* present, `maxAge` has precedence.
*/
maxAge?: number;
/**
* Controls whether or not a cookie is sent with cross-site requests,
* providing some protection against cross-site request forgery attacks
* (CSRF).
*
* - `strict`: The cookie will only be sent in a first-party context and not
* be sent with requests initiated by third party websites.
* - `lax`: The cookie is not sent on normal cross-site sub-requests (for
* example to load images or frames into a third party site), but is sent
* when a user is navigating within the origin site (i.e. when following a
* link). When `sameSite` is not specified, this is the default behavior.
* - `none`: The cookie will be sent in all contexts.
*/
sameSite?: "strict" | "lax" | "none";
/**
* Forbids JavaScript from accessing the cookie, for example, through the
* `document.cookie` property.
*/
httpOnly?: boolean;
/**
* Whether the cookie is to be used in secure contexts only, that is over
* HTTPS.
*/
secure?: boolean;
/**
* Indicates that the cookie should be stored using partitioned storage.
* @see https://developer.mozilla.org/en-US/docs/Web/Privacy/Privacy_sandbox/Partitioned_cookies
*/
partitioned?: boolean;
}
/**
* Parses the `Set-Cookie` header.
*
* @example
* ```ts
* import { parseCookie } from "@ayonli/jsext/http";
*
* const cookie = parseCookie("foo=bar; Domain=example.com; Path=/; Expires=Wed, 09 Jun 2021 10:18:14 GMT; HttpOnly; Secure; SameSite=Strict");
* console.log(cookie);
* // {
* // name: "foo",
* // value: "bar",
* // domain: "example.com",
* // path: "/",
* // expires: 1623233894000,
* // httpOnly: true,
* // secure: true,
* // sameSite: "strict"
* // }
* ```
*/
export declare function parseCookie(str: string): Cookie;
/**
* Converts a {@link Cookie} object to a string.
*
* @example
* ```ts
* import { stringifyCookie } from "@ayonli/jsext/http";
*
* const cookie = stringifyCookie({
* name: "foo",
* value: "bar",
* domain: "example.com",
* path: "/",
* expires: new Date("2021-06-09T10:18:14Z"),
* httpOnly: true,
* secure: true,
* sameSite: "Strict"
* });
* console.log(cookie);
* // foo=bar; Domain=example.com; Path=/; Expires=Wed, 09 Jun 2021 10:18:14 GMT; HttpOnly; Secure; SameSite=Strict
*/
export declare function stringifyCookie(cookie: Cookie): string;
/**
* Parses the `Cookie` header or the `document.cookie` property.
*/
export declare function parseCookies(str: string): Cookie[];
/**
* Converts a list of cookies to a string that can be used in the `Cookie`
* header.
*/
export declare function stringifyCookies(cookies: Cookie[]): string;
/**
* Gets the cookies from the `Cookie` header of the request or the `Set-Cookie`
* header of the response.
*
* @example
* ```ts
* import { getCookies } from "@ayonli/jsext/http";
*
* export default {
* fetch(req: Request) {
* const cookies = getCookies(req);
* console.log(cookies);
*
* return new Response("Hello, World!");
* }
* }
* ```
*/
export declare function getCookies(obj: Request | Response): Cookie[];
/**
* Gets the cookie by the given `name` from the `Cookie` header of the request
* or the `Set-Cookie` header of the response.
*
* @example
* ```ts
* import { getCookie } from "@ayonli/jsext/http";
*
* export default {
* fetch(req: Request) {
* const cookie = getCookie(req, "foo");
* console.log(cookie);
*
* return new Response("Hello, World!");
* }
* }
* ```
*/
export declare function getCookie(obj: Request | Response, name: string): Cookie | null;
/**
* Sets a cookie in the `Set-Cookie` header of the response.
*
* NOTE: This function can be used with both {@link Response} and {@link Headers}
* objects. However, when using with a `Headers` instance, make sure to set the
* cookie before the headers instance is used by the response object.
*
* @example
* ```ts
* import { setCookie } from "@ayonli/jsext/http";
*
* export default {
* fetch(req: Request) {
* const res = new Response("Hello, World!");
* setCookie(res, { name: "hello", value: "world" });
*
* return res;
* }
* }
* ```
*/
export declare function setCookie(res: Response | Headers, cookie: Cookie): void;
/**
* Sets the `Content-Disposition` header with the given filename when the
* response is intended to be downloaded.
*
* This function encodes the filename with {@link encodeURIComponent} and sets
* both the `filename` and the `filename*` parameters in the header for maximum
* compatibility.
*
* NOTE: This function can be used with both {@link Response} and {@link Headers}
* objects. However, when using with a `Headers` instance, make sure to set the
* filename before the headers instance is used by the response object.
*
* @example
* ```ts
* import { setFilename } from "@ayonli/jsext/http";
*
* export default {
* fetch(req: Request) {
* const res = new Response("Hello, World!");
* setFilename(res, "hello.txt");
*
* return res;
* }
* }
* ```
*/
export declare function setFilename(res: Response | Headers, filename: string): void;
/**
* Represents the HTTP request `Range` header.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
*/
export interface Range {
/**
* The unit in which ranges are specified, usually `bytes`.
*/
unit: string;
/**
* The ranges of units requested.
*/
ranges: {
start: number;
end?: number;
}[];
/**
* The number of units at the end of the resource requested.
*/
suffix?: number;
}
/**
* Parses the `Range` header.
*
* @example
* ```ts
* import { parseRange } from "@ayonli/jsext/http";
*
* const range = parseRange("bytes=0-499");
* console.log(range);
* // { unit: "bytes", ranges: [{ start: 0, end: 499 }] }
*
* const range2 = parseRange("bytes=0-499,1000-1499");
* console.log(range2);
* // { unit: "bytes", ranges: [{ start: 0, end: 499 }, { start: 1000, end: 1499 }] }
*
* const range3 = parseRange("bytes=2000-");
* console.log(range3);
* // { unit: "bytes", ranges: [{ start: 2000 }] }
*
* const range4 = parseRange("bytes=-500");
* console.log(range4);
* // { unit: "bytes", ranges: [], suffix: 500 }
* ```
*/
export declare function parseRange(str: string): Range;
/**
* Checks if the value from the `If-Match` header matches the given ETag.
*
* NOTE: Weak tags cannot be matched and will return `false`.
*
* @example
* ```ts
* import { etag, ifMatch } from "@ayonli/jsext/http";
*
* const _etag = await etag("Hello, World!");
* const match = ifMatch("d-3/1gIbsr1bCvZ2KQgJ7DpTGR3YH", _etag);
* console.log(match); // true
* ```
*/
export declare function ifMatch(value: string | null, etag: string): boolean;
/**
* Checks if the value from the `If-None-Match` header matches the given ETag.
*
* @example
* ```ts
* import { etag, ifNoneMatch } from "@ayonli/jsext/http";
*
* const _etag = await etag("Hello, World!");
* const match = ifNoneMatch("d-3/1gIbsr1bCvZ2KQgJ7DpTGR3YH", _etag);
* console.log(match); // false
* ```
*/
export declare function ifNoneMatch(value: string | null, etag: string): boolean;
/**
* Represents the HTTP request `Authorization` header with the `Basic` scheme.
*/
export interface BasicAuthorization {
username: string;
password: string;
}
/**
* Parses the `Authorization` header with the `Basic` scheme.
*
* @example
* ```ts
* import { parseBasicAuth } from "@ayonli/jsext/http";
*
* const auth = parseBasicAuth("Basic cm9vdDpwYSQkdzByZA==");
* console.log(auth);
* // { username: "root", password: "pa$$w0rd" }
* ```
*/
export declare function parseBasicAuth(str: string): BasicAuthorization;
/**
* Performs basic authentication verification for the request. When passed, this
* function returns nothing (`undefined`), otherwise it returns a `Response`
* with status `401 Unauthorized`, which should be responded to the client.
*
* @example
* ```ts
* import { verifyBasicAuth, type BasicAuthorization } from "@ayonli/jsext/http";
*
* const users = new Map([
* ["root", "pa$$w0rd"]
* ]);
*
* async function verify(auth: BasicAuthorization) {
* const password = users.get(auth.username);
* return !!password && password === auth.password;
* }
*
* export default {
* async fetch(req) {
* const res = await verifyBasicAuth(req, verify);
*
* if (res) {
* return res;
* }
*
* // continue with the request
* },
* };
* ```
*/
export declare function verifyBasicAuth(req: Request, verify: (auth: BasicAuthorization) => boolean | Promise<boolean>): Promise<void | Response>;
export declare const HTTP_METHODS: string[];
export declare const HTTP_STATUS: {
200: string;
201: string;
202: string;
204: string;
206: string;
301: string;
302: string;
304: string;
400: string;
401: string;
403: string;
404: string;
405: string;
406: string;
408: string;
409: string;
410: string;
413: string;
414: string;
415: string;
416: string;
417: string;
426: string;
500: string;
501: string;
502: string;
503: string;
504: string;
505: string;
};
/**
* Parses the text message as an HTTP request.
*
* **NOTE:** This function only supports HTTP/1.1 protocol.
*
* @example
* ```ts
* // GET example
* import { parseRequest } from "@ayonli/jsext/http";
*
* const message = "GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\n";
* const req = parseRequest(message);
*
* console.log(req.method); // "GET"
* console.log(req.url); // "http://example.com/foo"
* console.log(req.headers.get("Host")); // "example.com"
* ```
*
* @example
* ```ts
* // POST example
* import { parseRequest } from "@ayonli/jsext/http";
*
* const message = "POST /foo HTTP/1.1\r\n"
* + "Host: example.com\r\n"
* + "Content-Type: application/x-www-form-urlencoded\r\n"
* + "Content-Length: 19\r\n"
* + "\r\n"
* + "foo=hello&bar=world";
* const req = parseRequest(message);
*
* console.log(req.method); // "POST"
* console.log(req.url); // "http://example.com/foo"
* console.log(req.headers.get("Host")); // "example.com"
*
* const form = new URLSearchParams(await req.text());
*
* console.log(form.get("foo")); // "hello"
* console.log(form.get("bar")); // "world"
* ```
*/
export declare function parseRequest(message: string): Request;
/**
* Parses the text message as an HTTP response.
*
* @example
* ```ts
* import { parseResponse } from "@ayonli/jsext/http";
*
* const message = "HTTP/1.1 200 OK\r\n"
* + "Content-Type: text/plain\r\n"
* + "Content-Length: 12\r\n"
* + "\r\n"
* + "Hello, World!";
*
* const res = parseResponse(message);
*
* console.log(res.status); // 200
* console.log(res.statusText); // "OK"
* console.log(res.headers.get("Content-Type")); // "text/plain"
*
* const text = await res.text();
* console.log(text); // "Hello, World!"
* ```
*/
export declare function parseResponse(message: string): Response;
/**
* Converts the request object to text format.
*
* @example
* ```ts
* // GET example
* import { stringifyRequest } from "@ayonli/jsext/http";
*
* const req = new Request("http://example.com/foo");
* const message = await stringifyRequest(req);
*
* console.log(message);
* // "GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\n"
* ```
*
* @example
* ```ts
* // POST example
* import { stringifyRequest } from "@ayonli/jsext/http";
*
* const req = new Request("http://example.com/foo", {
* method: "POST",
* headers: {
* "Content-Type": "application/x-www-form-urlencoded",
* },
* body: "foo=hello&bar=world",
* });
* const message = await stringifyRequest(req);
*
* console.log(message);
* // "POST /foo HTTP/1.1\r\n" +
* // "Host: example.com\r\n" +
* // "Content-Type: application/x-www-form-urlencoded\r\n" +
* // "Content-Length: 19\r\n" +
* // "\r\n" +
* // "foo=hello&bar=world"
* ```
*/
export declare function stringifyRequest(req: Request): Promise<string>;
/**
* Converts the response object to text format.
*
* @example
* ```ts
* import { stringifyResponse } from "@ayonli/jsext/http";
*
* const res = new Response("Hello, World!", {
* headers: {
* "Content-Type": "text/plain",
* },
* });
* const message = await stringifyResponse(res);
*
* console.log(message);
* // "HTTP/1.1 200 OK\r\n" +
* // "Content-Type: text/plain\r\n" +
* // "Content-Length: 12\r\n" +
* // "\r\n" +
* // "Hello, World!"
* ```
*/
export declare function stringifyResponse(res: Response): Promise<string>;
/**
* Gets the suggested response type for the request.
*
* This function checks the `Accept` or the `Content-Type` header of the request,
* or the request method, or other factors to determine the most suitable
* response type for the client.
*
* For example, when requesting an article which is stored in markdown, the
* server can respond an HTML page for the browser, a plain text for the
* terminal, or a JSON object for the API client.
*
* This function returns the following response types:
*
* - `text`: plain text content (default)
* - `html`: an HTML page
* - `xml`: an XML document
* - `json`: a JSON object
* - `stream`: text stream or binary stream, depending on the use case
* - `none`: no content should be sent, such as for a `HEAD` request
*
* @example
* ```ts
* import { suggestResponseType } from "@ayonli/jsext/http";
*
* export default {
* async fetch(req: Request) {
* const type = suggestResponseType(req);
*
* if (type === "text") {
* return new Response("Hello, World!");
* } else if (type === "html") {
* return new Response("<h1>Hello, World!</h1>", {
* headers: { "Content-Type": "text/html" },
* });
* } else if (type === "xml") {
* return new Response("<xml><message>Hello, World!</message></xml>", {
* headers: { "Content-Type": "application/xml" },
* });
* } else if (type === "json") {
* return new Response(JSON.stringify({ message: "Hello, World!" }), {
* headers: { "Content-Type": "application/json" },
* });
* } else {
* return new Response(null, { status: 204 });
* }
* }
* }
* ```
*/
export declare function suggestResponseType(req: Request): "text" | "html" | "xml" | "json" | "stream" | "none";