UNPKG

@mjackson/form-data-parser

Version:

A request.formData() wrapper with streaming file upload handling

1,627 lines (1,625 loc) 62.6 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/form-data-parser.ts var form_data_parser_exports = {}; __export(form_data_parser_exports, { FileUpload: () => FileUpload, FormDataParseError: () => FormDataParseError, MaxFileSizeExceededError: () => MaxFileSizeExceededError, MaxFilesExceededError: () => MaxFilesExceededError, MaxHeaderSizeExceededError: () => MaxHeaderSizeExceededError, MultipartParseError: () => MultipartParseError, parseFormData: () => parseFormData }); module.exports = __toCommonJS(form_data_parser_exports); // ../multipart-parser/dist/multipart-parser.js function parseParams(input, delimiter = ";") { let parser = delimiter === ";" ? /(?:^|;)\s*([^=;\s]+)(\s*=\s*(?:"((?:[^"\\]|\\.)*)"|((?:[^;]|\\\;)+))?)?/g : /(?:^|,)\s*([^=,\s]+)(\s*=\s*(?:"((?:[^"\\]|\\.)*)"|((?:[^,]|\\\,)+))?)?/g; let params = []; let match; while ((match = parser.exec(input)) !== null) { let key = match[1].trim(); let value; if (match[2]) { value = (match[3] || match[4] || "").replace(/\\(.)/g, "$1").trim(); } params.push([key, value]); } return params; } function quote(value) { if (value.includes('"') || value.includes(";") || value.includes(" ")) { return `"${value.replace(/"/g, '\\"')}"`; } return value; } function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); } function isIterable(value) { return value != null && typeof value[Symbol.iterator] === "function"; } function isValidDate(date) { return date instanceof Date && !isNaN(date.getTime()); } function quoteEtag(tag) { return tag === "*" ? tag : /^(W\/)?".*"$/.test(tag) ? tag : `"${tag}"`; } var Accept = class { #map; constructor(init) { this.#map = /* @__PURE__ */ new Map(); if (init) { if (typeof init === "string") { for (let piece of init.split(/\s*,\s*/)) { let params = parseParams(piece); if (params.length < 1) continue; let mediaType = params[0][0]; let weight = 1; for (let i = 1; i < params.length; i++) { let [key, value] = params[i]; if (key === "q") { weight = Number(value); break; } } this.#map.set(mediaType.toLowerCase(), weight); } } else if (isIterable(init)) { for (let mediaType of init) { if (Array.isArray(mediaType)) { this.#map.set(mediaType[0].toLowerCase(), mediaType[1]); } else { this.#map.set(mediaType.toLowerCase(), 1); } } } else { for (let mediaType of Object.getOwnPropertyNames(init)) { this.#map.set(mediaType.toLowerCase(), init[mediaType]); } } this.#sort(); } } #sort() { this.#map = new Map([...this.#map].sort((a, b) => b[1] - a[1])); } /** * An array of all media types in the header. */ get mediaTypes() { return Array.from(this.#map.keys()); } /** * An array of all weights (q values) in the header. */ get weights() { return Array.from(this.#map.values()); } /** * The number of media types in the `Accept` header. */ get size() { return this.#map.size; } /** * Returns `true` if the header matches the given media type (i.e. it is "acceptable"). * @param mediaType The media type to check. * @returns `true` if the media type is acceptable, `false` otherwise. */ accepts(mediaType) { return this.getWeight(mediaType) > 0; } /** * Gets the weight of a given media type. Also supports wildcards, so e.g. `text/*` will match `text/html`. * @param mediaType The media type to get the weight of. * @returns The weight of the media type. */ getWeight(mediaType) { let [type, subtype] = mediaType.toLowerCase().split("/"); for (let [key, value] of this) { let [t, s] = key.split("/"); if ((t === type || t === "*" || type === "*") && (s === subtype || s === "*" || subtype === "*")) { return value; } } return 0; } /** * Returns the most preferred media type from the given list of media types. * @param mediaTypes The list of media types to choose from. * @returns The most preferred media type or `null` if none match. */ getPreferred(mediaTypes) { let sorted = mediaTypes.map((mediaType) => [mediaType, this.getWeight(mediaType)]).sort((a, b) => b[1] - a[1]); let first = sorted[0]; return first !== void 0 && first[1] > 0 ? first[0] : null; } /** * Returns the weight of a media type. If it is not in the header verbatim, this returns `null`. * @param mediaType The media type to get the weight of. * @returns The weight of the media type, or `null` if it is not in the header. */ get(mediaType) { return this.#map.get(mediaType.toLowerCase()) ?? null; } /** * Sets a media type with the given weight. * @param mediaType The media type to set. * @param weight The weight of the media type. Defaults to 1. */ set(mediaType, weight = 1) { this.#map.set(mediaType.toLowerCase(), weight); this.#sort(); } /** * Removes the given media type from the header. * @param mediaType The media type to remove. */ delete(mediaType) { this.#map.delete(mediaType.toLowerCase()); } /** * Checks if a media type is in the header. * @param mediaType The media type to check. * @returns `true` if the media type is in the header (verbatim), `false` otherwise. */ has(mediaType) { return this.#map.has(mediaType.toLowerCase()); } /** * Removes all media types from the header. */ clear() { this.#map.clear(); } entries() { return this.#map.entries(); } [Symbol.iterator]() { return this.entries(); } forEach(callback, thisArg) { for (let [mediaType, weight] of this) { callback.call(thisArg, mediaType, weight, this); } } toString() { let pairs = []; for (let [mediaType, weight] of this.#map) { pairs.push(`${mediaType}${weight === 1 ? "" : `;q=${weight}`}`); } return pairs.join(","); } }; var AcceptEncoding = class { #map; constructor(init) { this.#map = /* @__PURE__ */ new Map(); if (init) { if (typeof init === "string") { for (let piece of init.split(/\s*,\s*/)) { let params = parseParams(piece); if (params.length < 1) continue; let encoding = params[0][0]; let weight = 1; for (let i = 1; i < params.length; i++) { let [key, value] = params[i]; if (key === "q") { weight = Number(value); break; } } this.#map.set(encoding.toLowerCase(), weight); } } else if (isIterable(init)) { for (let value of init) { if (Array.isArray(value)) { this.#map.set(value[0].toLowerCase(), value[1]); } else { this.#map.set(value.toLowerCase(), 1); } } } else { for (let encoding of Object.getOwnPropertyNames(init)) { this.#map.set(encoding.toLowerCase(), init[encoding]); } } this.#sort(); } } #sort() { this.#map = new Map([...this.#map].sort((a, b) => b[1] - a[1])); } /** * An array of all encodings in the header. */ get encodings() { return Array.from(this.#map.keys()); } /** * An array of all weights (q values) in the header. */ get weights() { return Array.from(this.#map.values()); } /** * The number of encodings in the header. */ get size() { return this.#map.size; } /** * Returns `true` if the header matches the given encoding (i.e. it is "acceptable"). * @param encoding The encoding to check. * @returns `true` if the encoding is acceptable, `false` otherwise. */ accepts(encoding) { return encoding.toLowerCase() === "identity" || this.getWeight(encoding) > 0; } /** * Gets the weight an encoding. Performs wildcard matching so `*` matches all encodings. * @param encoding The encoding to get. * @returns The weight of the encoding, or `0` if it is not in the header. */ getWeight(encoding) { let lower = encoding.toLowerCase(); for (let [enc, weight] of this) { if (enc === lower || enc === "*" || lower === "*") { return weight; } } return 0; } /** * Returns the most preferred encoding from the given list of encodings. * @param encodings The encodings to choose from. * @returns The most preferred encoding or `null` if none match. */ getPreferred(encodings) { let sorted = encodings.map((encoding) => [encoding, this.getWeight(encoding)]).sort((a, b) => b[1] - a[1]); let first = sorted[0]; return first !== void 0 && first[1] > 0 ? first[0] : null; } /** * Gets the weight of an encoding. If it is not in the header verbatim, this returns `null`. * @param encoding The encoding to get. * @returns The weight of the encoding, or `null` if it is not in the header. */ get(encoding) { return this.#map.get(encoding.toLowerCase()) ?? null; } /** * Sets an encoding with the given weight. * @param encoding The encoding to set. * @param weight The weight of the encoding. Defaults to 1. */ set(encoding, weight = 1) { this.#map.set(encoding.toLowerCase(), weight); this.#sort(); } /** * Removes the given encoding from the header. * @param encoding The encoding to remove. */ delete(encoding) { this.#map.delete(encoding.toLowerCase()); } /** * Checks if the header contains a given encoding. * @param encoding The encoding to check. * @returns `true` if the encoding is in the header, `false` otherwise. */ has(encoding) { return this.#map.has(encoding.toLowerCase()); } /** * Removes all encodings from the header. */ clear() { this.#map.clear(); } entries() { return this.#map.entries(); } [Symbol.iterator]() { return this.entries(); } forEach(callback, thisArg) { for (let [encoding, weight] of this) { callback.call(thisArg, encoding, weight, this); } } toString() { let pairs = []; for (let [encoding, weight] of this.#map) { pairs.push(`${encoding}${weight === 1 ? "" : `;q=${weight}`}`); } return pairs.join(","); } }; var AcceptLanguage = class { #map; constructor(init) { this.#map = /* @__PURE__ */ new Map(); if (init) { if (typeof init === "string") { for (let piece of init.split(/\s*,\s*/)) { let params = parseParams(piece); if (params.length < 1) continue; let language = params[0][0]; let weight = 1; for (let i = 1; i < params.length; i++) { let [key, value] = params[i]; if (key === "q") { weight = Number(value); break; } } this.#map.set(language.toLowerCase(), weight); } } else if (isIterable(init)) { for (let value of init) { if (Array.isArray(value)) { this.#map.set(value[0].toLowerCase(), value[1]); } else { this.#map.set(value.toLowerCase(), 1); } } } else { for (let language of Object.getOwnPropertyNames(init)) { this.#map.set(language.toLowerCase(), init[language]); } } this.#sort(); } } #sort() { this.#map = new Map([...this.#map].sort((a, b) => b[1] - a[1])); } /** * An array of all languages in the header. */ get languages() { return Array.from(this.#map.keys()); } /** * An array of all weights (q values) in the header. */ get weights() { return Array.from(this.#map.values()); } /** * The number of languages in the header. */ get size() { return this.#map.size; } /** * Returns `true` if the header matches the given language (i.e. it is "acceptable"). * @param language The locale identifier of the language to check. * @returns `true` if the language is acceptable, `false` otherwise. */ accepts(language) { return this.getWeight(language) > 0; } /** * Gets the weight of a language with the given locale identifier. Performs wildcard and subtype * matching, so `en` matches `en-US` and `en-GB`, and `*` matches all languages. * @param language The locale identifier of the language to get. * @returns The weight of the language, or `0` if it is not in the header. */ getWeight(language) { let [base, subtype] = language.toLowerCase().split("-"); for (let [key, value] of this) { let [b, s] = key.split("-"); if ((b === base || b === "*" || base === "*") && (s === subtype || s === void 0 || subtype === void 0)) { return value; } } return 0; } /** * Returns the most preferred language from the given list of languages. * @param languages The locale identifiers of the languages to choose from. * @returns The most preferred language or `null` if none match. */ getPreferred(languages) { let sorted = languages.map((language) => [language, this.getWeight(language)]).sort((a, b) => b[1] - a[1]); let first = sorted[0]; return first !== void 0 && first[1] > 0 ? first[0] : null; } /** * Gets the weight of a language with the given locale identifier. If it is not in the header * verbatim, this returns `null`. * @param language The locale identifier of the language to get. * @returns The weight of the language, or `null` if it is not in the header. */ get(language) { return this.#map.get(language.toLowerCase()) ?? null; } /** * Sets a language with the given weight. * @param language The locale identifier of the language to set. * @param weight The weight of the language. Defaults to 1. */ set(language, weight = 1) { this.#map.set(language.toLowerCase(), weight); this.#sort(); } /** * Removes a language with the given locale identifier. * @param language The locale identifier of the language to remove. */ delete(language) { this.#map.delete(language.toLowerCase()); } /** * Checks if the header contains a language with the given locale identifier. * @param language The locale identifier of the language to check. * @returns `true` if the language is in the header, `false` otherwise. */ has(language) { return this.#map.has(language.toLowerCase()); } /** * Removes all languages from the header. */ clear() { this.#map.clear(); } entries() { return this.#map.entries(); } [Symbol.iterator]() { return this.entries(); } forEach(callback, thisArg) { for (let [language, weight] of this) { callback.call(thisArg, language, weight, this); } } toString() { let pairs = []; for (let [language, weight] of this.#map) { pairs.push(`${language}${weight === 1 ? "" : `;q=${weight}`}`); } return pairs.join(","); } }; var CacheControl = class { maxAge; maxStale; minFresh; sMaxage; noCache; noStore; noTransform; onlyIfCached; mustRevalidate; proxyRevalidate; mustUnderstand; private; public; immutable; staleWhileRevalidate; staleIfError; constructor(init) { if (init) { if (typeof init === "string") { let params = parseParams(init, ","); if (params.length > 0) { for (let [name, value] of params) { switch (name) { case "max-age": this.maxAge = Number(value); break; case "max-stale": this.maxStale = Number(value); break; case "min-fresh": this.minFresh = Number(value); break; case "s-maxage": this.sMaxage = Number(value); break; case "no-cache": this.noCache = true; break; case "no-store": this.noStore = true; break; case "no-transform": this.noTransform = true; break; case "only-if-cached": this.onlyIfCached = true; break; case "must-revalidate": this.mustRevalidate = true; break; case "proxy-revalidate": this.proxyRevalidate = true; break; case "must-understand": this.mustUnderstand = true; break; case "private": this.private = true; break; case "public": this.public = true; break; case "immutable": this.immutable = true; break; case "stale-while-revalidate": this.staleWhileRevalidate = Number(value); break; case "stale-if-error": this.staleIfError = Number(value); break; } } } } else { this.maxAge = init.maxAge; this.maxStale = init.maxStale; this.minFresh = init.minFresh; this.sMaxage = init.sMaxage; this.noCache = init.noCache; this.noStore = init.noStore; this.noTransform = init.noTransform; this.onlyIfCached = init.onlyIfCached; this.mustRevalidate = init.mustRevalidate; this.proxyRevalidate = init.proxyRevalidate; this.mustUnderstand = init.mustUnderstand; this.private = init.private; this.public = init.public; this.immutable = init.immutable; this.staleWhileRevalidate = init.staleWhileRevalidate; this.staleIfError = init.staleIfError; } } } toString() { let parts = []; if (this.public) { parts.push("public"); } if (this.private) { parts.push("private"); } if (typeof this.maxAge === "number") { parts.push(`max-age=${this.maxAge}`); } if (typeof this.sMaxage === "number") { parts.push(`s-maxage=${this.sMaxage}`); } if (this.noCache) { parts.push("no-cache"); } if (this.noStore) { parts.push("no-store"); } if (this.noTransform) { parts.push("no-transform"); } if (this.onlyIfCached) { parts.push("only-if-cached"); } if (this.mustRevalidate) { parts.push("must-revalidate"); } if (this.proxyRevalidate) { parts.push("proxy-revalidate"); } if (this.mustUnderstand) { parts.push("must-understand"); } if (this.immutable) { parts.push("immutable"); } if (typeof this.staleWhileRevalidate === "number") { parts.push(`stale-while-revalidate=${this.staleWhileRevalidate}`); } if (typeof this.staleIfError === "number") { parts.push(`stale-if-error=${this.staleIfError}`); } if (typeof this.maxStale === "number") { parts.push(`max-stale=${this.maxStale}`); } if (typeof this.minFresh === "number") { parts.push(`min-fresh=${this.minFresh}`); } return parts.join(", "); } }; var ContentDisposition = class { filename; filenameSplat; name; type; constructor(init) { if (init) { if (typeof init === "string") { let params = parseParams(init); if (params.length > 0) { this.type = params[0][0]; for (let [name, value] of params.slice(1)) { if (name === "filename") { this.filename = value; } else if (name === "filename*") { this.filenameSplat = value; } else if (name === "name") { this.name = value; } } } } else { this.filename = init.filename; this.filenameSplat = init.filenameSplat; this.name = init.name; this.type = init.type; } } } /** * The preferred filename for the content, using the `filename*` parameter if present, falling back to the `filename` parameter. * * From [RFC 6266](https://tools.ietf.org/html/rfc6266): * * Many user agent implementations predating this specification do not understand the "filename*" parameter. * Therefore, when both "filename" and "filename*" are present in a single header field value, recipients SHOULD * pick "filename*" and ignore "filename". This way, senders can avoid special-casing specific user agents by * sending both the more expressive "filename*" parameter, and the "filename" parameter as fallback for legacy recipients. */ get preferredFilename() { let filenameSplat = this.filenameSplat; if (filenameSplat) { let decodedFilename = decodeFilenameSplat(filenameSplat); if (decodedFilename) return decodedFilename; } return this.filename; } toString() { if (!this.type) { return ""; } let parts = [this.type]; if (this.name) { parts.push(`name=${quote(this.name)}`); } if (this.filename) { parts.push(`filename=${quote(this.filename)}`); } if (this.filenameSplat) { parts.push(`filename*=${quote(this.filenameSplat)}`); } return parts.join("; "); } }; function decodeFilenameSplat(value) { let match = value.match(/^([\w-]+)'([^']*)'(.+)$/); if (!match) return null; let [, charset, , encodedFilename] = match; let decodedFilename = percentDecode(encodedFilename); try { let decoder2 = new TextDecoder(charset); let bytes = new Uint8Array(decodedFilename.split("").map((char) => char.charCodeAt(0))); return decoder2.decode(bytes); } catch (error) { console.warn(`Failed to decode filename from charset ${charset}:`, error); return decodedFilename; } } function percentDecode(value) { return value.replace(/\+/g, " ").replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => { return String.fromCharCode(parseInt(hex, 16)); }); } var ContentType = class { boundary; charset; mediaType; constructor(init) { if (init) { if (typeof init === "string") { let params = parseParams(init); if (params.length > 0) { this.mediaType = params[0][0]; for (let [name, value] of params.slice(1)) { if (name === "boundary") { this.boundary = value; } else if (name === "charset") { this.charset = value; } } } } else { this.boundary = init.boundary; this.charset = init.charset; this.mediaType = init.mediaType; } } } toString() { if (!this.mediaType) { return ""; } let parts = [this.mediaType]; if (this.charset) { parts.push(`charset=${quote(this.charset)}`); } if (this.boundary) { parts.push(`boundary=${quote(this.boundary)}`); } return parts.join("; "); } }; var Cookie = class { #map; constructor(init) { this.#map = /* @__PURE__ */ new Map(); if (init) { if (typeof init === "string") { let params = parseParams(init); for (let [name, value] of params) { this.#map.set(name, value ?? ""); } } else if (isIterable(init)) { for (let [name, value] of init) { this.#map.set(name, value); } } else { for (let name of Object.getOwnPropertyNames(init)) { this.#map.set(name, init[name]); } } } } /** * An array of the names of the cookies in the header. */ get names() { return Array.from(this.#map.keys()); } /** * An array of the values of the cookies in the header. */ get values() { return Array.from(this.#map.values()); } /** * The number of cookies in the header. */ get size() { return this.#map.size; } /** * Gets the value of a cookie with the given name from the header. * @param name The name of the cookie. * @returns The value of the cookie, or `null` if the cookie does not exist. */ get(name) { return this.#map.get(name) ?? null; } /** * Sets a cookie with the given name and value in the header. * @param name The name of the cookie. * @param value The value of the cookie. */ set(name, value) { this.#map.set(name, value); } /** * Removes a cookie with the given name from the header. * @param name The name of the cookie. */ delete(name) { this.#map.delete(name); } /** * True if a cookie with the given name exists in the header. * @param name The name of the cookie. * @returns True if a cookie with the given name exists in the header. */ has(name) { return this.#map.has(name); } /** * Removes all cookies from the header. */ clear() { this.#map.clear(); } entries() { return this.#map.entries(); } [Symbol.iterator]() { return this.entries(); } forEach(callback, thisArg) { for (let [name, value] of this) { callback.call(thisArg, name, value, this); } } toString() { let pairs = []; for (let [name, value] of this.#map) { pairs.push(`${name}=${quote(value)}`); } return pairs.join("; "); } }; var IfNoneMatch = class { tags = []; constructor(init) { if (init) { if (typeof init === "string") { this.tags.push(...init.split(/\s*,\s*/).map(quoteEtag)); } else if (Array.isArray(init)) { this.tags.push(...init.map(quoteEtag)); } else { this.tags.push(...init.tags.map(quoteEtag)); } } } /** * Checks if the header contains the given entity tag. * * Note: This method checks only for exact matches and does not consider wildcards. * * @param tag The entity tag to check for. * @returns `true` if the tag is present in the header, `false` otherwise. */ has(tag) { return this.tags.includes(quoteEtag(tag)); } /** * Checks if this header matches the given entity tag. * * @param tag The entity tag to check for. * @returns `true` if the tag is present in the header (or the header contains a wildcard), `false` otherwise. */ matches(tag) { return this.has(tag) || this.tags.includes("*"); } toString() { return this.tags.join(", "); } }; var SetCookie = class { domain; expires; httpOnly; maxAge; name; path; sameSite; secure; value; constructor(init) { if (init) { if (typeof init === "string") { let params = parseParams(init); if (params.length > 0) { this.name = params[0][0]; this.value = params[0][1]; for (let [key, value] of params.slice(1)) { switch (key.toLowerCase()) { case "domain": this.domain = value; break; case "expires": { if (typeof value === "string") { let date = new Date(value); if (isValidDate(date)) { this.expires = date; } } break; } case "httponly": this.httpOnly = true; break; case "max-age": { if (typeof value === "string") { let v = parseInt(value, 10); if (!isNaN(v)) this.maxAge = v; } break; } case "path": this.path = value; break; case "samesite": if (typeof value === "string" && /strict|lax|none/i.test(value)) { this.sameSite = capitalize(value); } break; case "secure": this.secure = true; break; } } } } else { this.domain = init.domain; this.expires = init.expires; this.httpOnly = init.httpOnly; this.maxAge = init.maxAge; this.name = init.name; this.path = init.path; this.sameSite = init.sameSite; this.secure = init.secure; this.value = init.value; } } } toString() { if (!this.name) { return ""; } let parts = [`${this.name}=${quote(this.value || "")}`]; if (this.domain) { parts.push(`Domain=${this.domain}`); } if (this.path) { parts.push(`Path=${this.path}`); } if (this.expires) { parts.push(`Expires=${this.expires.toUTCString()}`); } if (this.maxAge) { parts.push(`Max-Age=${this.maxAge}`); } if (this.secure) { parts.push("Secure"); } if (this.httpOnly) { parts.push("HttpOnly"); } if (this.sameSite) { parts.push(`SameSite=${this.sameSite}`); } return parts.join("; "); } }; var HeaderWordCasingExceptions = { ct: "CT", etag: "ETag", te: "TE", www: "WWW", x: "X", xss: "XSS" }; function canonicalHeaderName(name) { return name.toLowerCase().split("-").map((word) => HeaderWordCasingExceptions[word] || word.charAt(0).toUpperCase() + word.slice(1)).join("-"); } var CRLF = "\r\n"; var AcceptKey = "accept"; var AcceptEncodingKey = "accept-encoding"; var AcceptLanguageKey = "accept-language"; var AcceptRangesKey = "accept-ranges"; var AgeKey = "age"; var CacheControlKey = "cache-control"; var ConnectionKey = "connection"; var ContentDispositionKey = "content-disposition"; var ContentEncodingKey = "content-encoding"; var ContentLanguageKey = "content-language"; var ContentLengthKey = "content-length"; var ContentTypeKey = "content-type"; var CookieKey = "cookie"; var DateKey = "date"; var ETagKey = "etag"; var ExpiresKey = "expires"; var HostKey = "host"; var IfModifiedSinceKey = "if-modified-since"; var IfNoneMatchKey = "if-none-match"; var IfUnmodifiedSinceKey = "if-unmodified-since"; var LastModifiedKey = "last-modified"; var LocationKey = "location"; var RefererKey = "referer"; var SetCookieKey = "set-cookie"; var SuperHeaders = class _SuperHeaders extends Headers { #map; #setCookies = []; constructor(init) { super(); this.#map = /* @__PURE__ */ new Map(); if (init) { if (typeof init === "string") { let lines = init.split(CRLF); for (let line of lines) { let match = line.match(/^([^:]+):(.*)/); if (match) { this.append(match[1].trim(), match[2].trim()); } } } else if (isIterable(init)) { for (let [name, value] of init) { this.append(name, value); } } else if (typeof init === "object") { for (let name of Object.getOwnPropertyNames(init)) { let value = init[name]; let descriptor = Object.getOwnPropertyDescriptor(_SuperHeaders.prototype, name); if (descriptor?.set) { descriptor.set.call(this, value); } else { this.set(name, value.toString()); } } } } } /** * Appends a new header value to the existing set of values for a header, * or adds the header if it does not already exist. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/append) */ append(name, value) { let key = name.toLowerCase(); if (key === SetCookieKey) { this.#setCookies.push(value); } else { let existingValue = this.#map.get(key); this.#map.set(key, existingValue ? `${existingValue}, ${value}` : value); } } /** * Removes a header. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/delete) */ delete(name) { let key = name.toLowerCase(); if (key === SetCookieKey) { this.#setCookies = []; } else { this.#map.delete(key); } } /** * Returns a string of all the values for a header, or `null` if the header does not exist. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/get) */ get(name) { let key = name.toLowerCase(); if (key === SetCookieKey) { return this.getSetCookie().join(", "); } else { let value = this.#map.get(key); if (typeof value === "string") { return value; } else if (value != null) { let str = value.toString(); return str === "" ? null : str; } else { return null; } } } /** * Returns an array of all values associated with the `Set-Cookie` header. This is * useful when building headers for a HTTP response since multiple `Set-Cookie` headers * must be sent on separate lines. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/getSetCookie) */ getSetCookie() { return this.#setCookies.map((v) => typeof v === "string" ? v : v.toString()); } /** * Returns `true` if the header is present in the list of headers. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/has) */ has(name) { let key = name.toLowerCase(); return key === SetCookieKey ? this.#setCookies.length > 0 : this.get(key) != null; } /** * Sets a new value for the given header. If the header already exists, the new value * will replace the existing value. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/set) */ set(name, value) { let key = name.toLowerCase(); if (key === SetCookieKey) { this.#setCookies = [value]; } else { this.#map.set(key, value); } } /** * Returns an iterator of all header keys (lowercase). * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/keys) */ *keys() { for (let [key] of this) yield key; } /** * Returns an iterator of all header values. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/values) */ *values() { for (let [, value] of this) yield value; } /** * Returns an iterator of all header key/value pairs. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries) */ *entries() { for (let [key] of this.#map) { let str = this.get(key); if (str) yield [key, str]; } for (let value of this.getSetCookie()) { yield [SetCookieKey, value]; } } [Symbol.iterator]() { return this.entries(); } /** * Invokes the `callback` for each header key/value pair. * * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/forEach) */ forEach(callback, thisArg) { for (let [key, value] of this) { callback.call(thisArg, value, key, this); } } /** * Returns a string representation of the headers suitable for use in a HTTP message. */ toString() { let lines = []; for (let [key, value] of this) { lines.push(`${canonicalHeaderName(key)}: ${value}`); } return lines.join(CRLF); } // Header-specific getters and setters /** * The `Accept` header is used by clients to indicate the media types that are acceptable * in the response. * * [MDN `Accept` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2) */ get accept() { return this.#getHeaderValue(AcceptKey, Accept); } set accept(value) { this.#setHeaderValue(AcceptKey, Accept, value); } /** * The `Accept-Encoding` header contains information about the content encodings that the client * is willing to accept in the response. * * [MDN `Accept-Encoding` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4) */ get acceptEncoding() { return this.#getHeaderValue(AcceptEncodingKey, AcceptEncoding); } set acceptEncoding(value) { this.#setHeaderValue(AcceptEncodingKey, AcceptEncoding, value); } /** * The `Accept-Language` header contains information about preferred natural language for the * response. * * [MDN `Accept-Language` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) */ get acceptLanguage() { return this.#getHeaderValue(AcceptLanguageKey, AcceptLanguage); } set acceptLanguage(value) { this.#setHeaderValue(AcceptLanguageKey, AcceptLanguage, value); } /** * The `Accept-Ranges` header indicates the server's acceptance of range requests. * * [MDN `Accept-Ranges` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7233#section-2.3) */ get acceptRanges() { return this.#getStringValue(AcceptRangesKey); } set acceptRanges(value) { this.#setStringValue(AcceptRangesKey, value); } /** * The `Age` header contains the time in seconds an object was in a proxy cache. * * [MDN `Age` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Age) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7234#section-5.1) */ get age() { return this.#getNumberValue(AgeKey); } set age(value) { this.#setNumberValue(AgeKey, value); } /** * The `Cache-Control` header contains directives for caching mechanisms in both requests and responses. * * [MDN `Cache-Control` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2) */ get cacheControl() { return this.#getHeaderValue(CacheControlKey, CacheControl); } set cacheControl(value) { this.#setHeaderValue(CacheControlKey, CacheControl, value); } /** * The `Connection` header controls whether the network connection stays open after the current * transaction finishes. * * [MDN `Connection` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7230#section-6.1) */ get connection() { return this.#getStringValue(ConnectionKey); } set connection(value) { this.#setStringValue(ConnectionKey, value); } /** * The `Content-Disposition` header is a response-type header that describes how the payload is displayed. * * [MDN `Content-Disposition` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) * * [RFC 6266](https://datatracker.ietf.org/doc/html/rfc6266) */ get contentDisposition() { return this.#getHeaderValue(ContentDispositionKey, ContentDisposition); } set contentDisposition(value) { this.#setHeaderValue(ContentDispositionKey, ContentDisposition, value); } /** * The `Content-Encoding` header specifies the encoding of the resource. * * Note: If multiple encodings have been used, this value may be a comma-separated list. However, most often this * header will only contain a single value. * * [MDN `Content-Encoding` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) * * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.content-encoding) */ get contentEncoding() { return this.#getStringValue(ContentEncodingKey); } set contentEncoding(value) { this.#setStringValue(ContentEncodingKey, Array.isArray(value) ? value.join(", ") : value); } /** * The `Content-Language` header describes the natural language(s) of the intended audience for the response content. * * Note: If the response content is intended for multiple audiences, this value may be a comma-separated list. However, * most often this header will only contain a single value. * * [MDN `Content-Language` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language) * * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.content-language) */ get contentLanguage() { return this.#getStringValue(ContentLanguageKey); } set contentLanguage(value) { this.#setStringValue(ContentLanguageKey, Array.isArray(value) ? value.join(", ") : value); } /** * The `Content-Length` header indicates the size of the entity-body in bytes. * * [MDN `Content-Length` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2) */ get contentLength() { return this.#getNumberValue(ContentLengthKey); } set contentLength(value) { this.#setNumberValue(ContentLengthKey, value); } /** * The `Content-Type` header indicates the media type of the resource. * * [MDN `Content-Type` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.5) */ get contentType() { return this.#getHeaderValue(ContentTypeKey, ContentType); } set contentType(value) { this.#setHeaderValue(ContentTypeKey, ContentType, value); } /** * The `Cookie` request header contains stored HTTP cookies previously sent by the server with * the `Set-Cookie` header. * * [MDN `Cookie` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc6265#section-5.4) */ get cookie() { return this.#getHeaderValue(CookieKey, Cookie); } set cookie(value) { this.#setHeaderValue(CookieKey, Cookie, value); } /** * The `Date` header contains the date and time at which the message was sent. * * [MDN `Date` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2) */ get date() { return this.#getDateValue(DateKey); } set date(value) { this.#setDateValue(DateKey, value); } /** * The `ETag` header provides a unique identifier for the current version of the resource. * * [MDN `ETag` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3) */ get etag() { return this.#getStringValue(ETagKey); } set etag(value) { this.#setStringValue(ETagKey, typeof value === "string" ? quoteEtag(value) : value); } /** * The `Expires` header contains the date/time after which the response is considered stale. * * [MDN `Expires` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7234#section-5.3) */ get expires() { return this.#getDateValue(ExpiresKey); } set expires(value) { this.#setDateValue(ExpiresKey, value); } /** * The `Host` header specifies the domain name of the server and (optionally) the TCP port number. * * [MDN `Host` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7230#section-5.4) */ get host() { return this.#getStringValue(HostKey); } set host(value) { this.#setStringValue(HostKey, value); } /** * The `If-Modified-Since` header makes a request conditional on the last modification date of the * requested resource. * * [MDN `If-Modified-Since` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.3) */ get ifModifiedSince() { return this.#getDateValue(IfModifiedSinceKey); } set ifModifiedSince(value) { this.#setDateValue(IfModifiedSinceKey, value); } /** * The `If-None-Match` header makes a request conditional on the absence of a matching ETag. * * [MDN `If-None-Match` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.2) */ get ifNoneMatch() { return this.#getHeaderValue(IfNoneMatchKey, IfNoneMatch); } set ifNoneMatch(value) { this.#setHeaderValue(IfNoneMatchKey, IfNoneMatch, value); } /** * The `If-Unmodified-Since` header makes a request conditional on the last modification date of the * requested resource. * * [MDN `If-Unmodified-Since` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Unmodified-Since) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.4) */ get ifUnmodifiedSince() { return this.#getDateValue(IfUnmodifiedSinceKey); } set ifUnmodifiedSince(value) { this.#setDateValue(IfUnmodifiedSinceKey, value); } /** * The `Last-Modified` header contains the date and time at which the resource was last modified. * * [MDN `Last-Modified` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-2.2) */ get lastModified() { return this.#getDateValue(LastModifiedKey); } set lastModified(value) { this.#setDateValue(LastModifiedKey, value); } /** * The `Location` header indicates the URL to redirect to. * * [MDN `Location` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2) */ get location() { return this.#getStringValue(LocationKey); } set location(value) { this.#setStringValue(LocationKey, value); } /** * The `Referer` header contains the address of the previous web page from which a link to the * currently requested page was followed. * * [MDN `Referer` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.5.2) */ get referer() { return this.#getStringValue(RefererKey); } set referer(value) { this.#setStringValue(RefererKey, value); } /** * The `Set-Cookie` header is used to send cookies from the server to the user agent. * * [MDN `Set-Cookie` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc6265#section-4.1) */ get setCookie() { let setCookies = this.#setCookies; for (let i = 0; i < setCookies.length; ++i) { if (typeof setCookies[i] === "string") { setCookies[i] = new SetCookie(setCookies[i]); } } return setCookies; } set setCookie(value) { if (value != null) { this.#setCookies = (Array.isArray(value) ? value : [value]).map( (v) => typeof v === "string" ? v : new SetCookie(v) ); } else { this.#setCookies = []; } } // Helpers #getHeaderValue(key, ctor) { let value = this.#map.get(key); if (value !== void 0) { if (typeof value === "string") { let obj2 = new ctor(value); this.#map.set(key, obj2); return obj2; } else { return value; } } let obj = new ctor(); this.#map.set(key, obj); return obj; } #setHeaderValue(key, ctor, value) { if (value != null) { this.#map.set(key, typeof value === "string" ? value : new ctor(value)); } else { this.#map.delete(key); } } #getDateValue(key) { let value = this.#map.get(key); return value === void 0 ? null : new Date(value); } #setDateValue(key, value) { if (value != null) { this.#map.set( key, typeof value === "string" ? value : (typeof value === "number" ? new Date(value) : value).toUTCString() ); } else { this.#map.delete(key); } } #getNumberValue(key) { let value = this.#map.get(key); return value === void 0 ? null : parseInt(value, 10); } #setNumberValue(key, value) { if (value != null) { this.#map.set(key, typeof value === "string" ? value : value.toString()); } else { this.#map.delete(key); } } #getStringValue(key) { let value = this.#map.get(key); return value === void 0 ? null : value; } #setStringValue(key, value) { if (value != null) { this.#map.set(key, value); } else { this.#map.delete(key); } } }; async function* readStream(stream) { let reader = stream.getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; yield value; } } finally { reader.relea