UNPKG

@ayonli/jsext

Version:

A JavaScript extension package for building strong and modern applications.

640 lines (639 loc) 18.7 kB
/** * 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";