You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

mentoss

Package Overview
Dependencies
Maintainers
1
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mentoss - npm Package Compare versions

Comparing version
0.9.2
to
0.10.0
+14
-0
dist/custom-request.js

@@ -56,2 +56,16 @@ /**

/*
* Default setting for `redirect` is "follow" in the fetch API.
* Not all runtimes follow the spec, so we need to ensure
* that the `redirect` property is set correctly.
*/
const expectedRedirect = init?.redirect ?? "follow";
if (expectedRedirect !== this.redirect) {
Object.defineProperty(this, "redirect", {
configurable: true,
enumerable: true,
value: expectedRedirect,
writable: false,
});
}
/*
* Not all runtimes properly support the `credentials` property.

@@ -58,0 +72,0 @@ * Bun's fetch implementation sets credentials to "include" by default

+226
-72

@@ -5,2 +5,3 @@ /**

*/
/* globals Headers */
//-----------------------------------------------------------------------------

@@ -10,4 +11,5 @@ // Imports

import { stringifyRequest } from "./util.js";
import { isCorsSimpleRequest, CorsPreflightData, assertCorsResponse, processCorsResponse, validateCorsRequest, CORS_REQUEST_METHOD, CORS_REQUEST_HEADERS, CORS_ORIGIN, createCorsPreflightError, getUnsafeHeaders, } from "./cors.js";
import { isCorsSimpleRequest, CorsPreflightData, assertCorsResponse, processCorsResponse, validateCorsRequest, CORS_REQUEST_METHOD, CORS_REQUEST_HEADERS, CORS_ORIGIN, createCorsPreflightError, getUnsafeHeaders, createCorsError, } from "./cors.js";
import { createCustomRequest } from "./custom-request.js";
import { isRedirectStatus, isBodylessMethod, isBodyPreservingRedirectStatus, isRequestBodyHeader, } from "./http.js";
//-----------------------------------------------------------------------------

@@ -106,2 +108,46 @@ // Type Definitions

}
/**
* Creates an opaque filtered redirect response.
* @param {typeof Response} ResponseConstructor The Response constructor to use
* @param {string} url The URL of the response
* @returns {Response} An opaque redirect response
*/
function createOpaqueRedirectResponse(ResponseConstructor, url) {
const response = new ResponseConstructor(null, {
status: 200, // Node.js doesn't accept 0 status, so use 200 then override it
statusText: "",
headers: {},
});
// Define non-configurable properties to match opaque redirect response behavior
Object.defineProperties(response, {
type: { value: "opaqueredirect", configurable: false },
url: { value: url, configurable: false },
ok: { value: false, configurable: false },
redirected: { value: false, configurable: false },
body: { value: null, configurable: false },
bodyUsed: { value: false, configurable: false },
status: { value: 0, configurable: false },
});
return response;
}
/**
* Checks if a redirect needs to adjust the request method and headers.
* @param {Request} request The original request
* @param {number} status The redirect status code
* @returns {boolean} True if the redirect needs to adjust the method
*/
function redirectNeedsAdjustment(request, status) {
// For 303 redirects, change method to GET if it's not already GET or HEAD
return ((status === 303 && !isBodylessMethod(request.method)) ||
// For 301/302 redirects, change method to GET if the original method was POST
((status === 301 || status === 302) && request.method === "POST"));
}
/**
* Checks if a response is tainted (violates CORS)
* @param {Response} response The response to check
* @returns {boolean} True if the response is tainted
*/
function isTaintedResponse(response) {
return response.type.startsWith("opaque");
}
//-----------------------------------------------------------------------------

@@ -148,4 +194,77 @@ // Exports

*/
fetch;
fetch = (input, init) => this.#fetch(input, init);
/**
* @type {typeof fetch}
*/
async #fetch(input, init) {
// first check to see if the request has been aborted
const signal = init?.signal;
signal?.throwIfAborted();
// TODO: For some reason this causes Mocha tests to fail with "multiple done"
// signal?.addEventListener("abort", () => {
// throw new Error("Fetch was aborted.");
// });
// adjust any relative URLs
const fixedInput = typeof input === "string" && this.#baseUrl
? new URL(input, this.#baseUrl).toString()
: input;
const request = new this.#Request(fixedInput, init);
let useCors = false;
let useCorsCredentials = false;
let preflightData;
let isSimpleRequest = false;
// if there's a base URL then we need to check for CORS
if (this.#baseUrl) {
const requestUrl = new URL(request.url);
if (isSameOrigin(requestUrl, this.#baseUrl)) {
// if we aren't explicitly blocking credentials then add them
if (request.credentials !== "omit") {
this.#attachCredentialsToRequest(request);
}
}
else {
// check for same-origin mode
if (request.mode === "same-origin") {
throw new TypeError(`Failed to fetch. Request mode is "same-origin" but the URL's origin is not same as the request origin ${this.#baseUrl.origin}`);
}
useCors = true;
isSimpleRequest = isCorsSimpleRequest(request);
const includeCredentials = request.credentials === "include";
validateCorsRequest(request, this.#baseUrl.origin);
if (isSimpleRequest) {
if (includeCredentials) {
useCorsCredentials = true;
this.#attachCredentialsToRequest(request);
}
}
else {
preflightData = await this.#preflightFetch(request);
preflightData.validate(request, this.#baseUrl.origin);
if (includeCredentials) {
if (!preflightData.allowCredentials) {
throw createCorsPreflightError(request.url, this.#baseUrl.origin, "No 'Access-Control-Allow-Credentials' header is present on the requested resource.");
}
useCorsCredentials = true;
this.#attachCredentialsToRequest(request);
}
}
// add the origin header to the request
request.headers.append("origin", this.#baseUrl.origin);
// if the preflight response is successful, then we can make the actual request
}
}
signal?.throwIfAborted();
const response = await this.#internalFetch(request, init?.body);
if (useCors && this.#baseUrl) {
// handle no-cors mode for any cross-origin request
if (isSimpleRequest && request.mode === "no-cors") {
return createOpaqueResponse(this.#Response);
}
processCorsResponse(response, this.#baseUrl.origin, useCorsCredentials);
}
signal?.throwIfAborted();
// Process redirects
return this.#processRedirect(response, request, [], init?.body);
}
/**
* Map to store original fetch functions for objects

@@ -178,72 +297,2 @@ * @type {WeakMap<object, Map<string, Function>>}

}
// create the function here to bind to `this`
this.fetch = async (input, init) => {
// first check to see if the request has been aborted
const signal = init?.signal;
signal?.throwIfAborted();
// TODO: For some reason this causes Mocha tests to fail with "multiple done"
// signal?.addEventListener("abort", () => {
// throw new Error("Fetch was aborted.");
// });
// adjust any relative URLs
const fixedInput = typeof input === "string" && this.#baseUrl
? new URL(input, this.#baseUrl).toString()
: input;
const request = new this.#Request(fixedInput, init);
let useCors = false;
let useCorsCredentials = false;
let preflightData;
let isSimpleRequest = false;
// if there's a base URL then we need to check for CORS
if (this.#baseUrl) {
const requestUrl = new URL(request.url);
if (isSameOrigin(requestUrl, this.#baseUrl)) {
// if we aren't explicitly blocking credentials then add them
if (request.credentials !== "omit") {
this.#attachCredentialsToRequest(request);
}
}
else {
// check for same-origin mode
if (request.mode === "same-origin") {
throw new TypeError(`Failed to fetch. Request mode is "same-origin" but the URL's origin is not same as the request origin ${this.#baseUrl.origin}`);
}
useCors = true;
isSimpleRequest = isCorsSimpleRequest(request);
const includeCredentials = request.credentials === "include";
validateCorsRequest(request, this.#baseUrl.origin);
if (isSimpleRequest) {
if (includeCredentials) {
useCorsCredentials = true;
this.#attachCredentialsToRequest(request);
}
}
else {
preflightData = await this.#preflightFetch(request);
preflightData.validate(request, this.#baseUrl.origin);
if (includeCredentials) {
if (!preflightData.allowCredentials) {
throw createCorsPreflightError(request.url, this.#baseUrl.origin, "No 'Access-Control-Allow-Credentials' header is present on the requested resource.");
}
useCorsCredentials = true;
this.#attachCredentialsToRequest(request);
}
}
// add the origin header to the request
request.headers.append("origin", this.#baseUrl.origin);
// if the preflight response is successful, then we can make the actual request
}
}
signal?.throwIfAborted();
const response = await this.#internalFetch(request, init?.body);
if (useCors && this.#baseUrl) {
// handle no-cors mode for any cross-origin request
if (isSimpleRequest && request.mode === "no-cors") {
return createOpaqueResponse(this.#Response);
}
processCorsResponse(response, this.#baseUrl.origin, useCorsCredentials);
}
signal?.throwIfAborted();
return response;
};
}

@@ -363,2 +412,107 @@ /**

}
/**
* Processes a redirect response according to the fetch spec
* @param {Response} response The response to check for a redirect
* @param {Request} request The original request
* @param {URL[]} urlList The list of URLs already visited in this redirect chain
* @param {any} requestBody The body of the original request
* @returns {Promise<Response>} The final response after any redirects
* @see https://fetch.spec.whatwg.org/#http-redirect-fetch
*/
async #processRedirect(response, request, urlList = [], requestBody = null) {
// Add current URL to list
urlList.push(new URL(request.url));
const isRedirect = isRedirectStatus(response.status);
// Process response based on redirect status and mode
if (!isRedirect) {
// Not a redirect - set redirected flag if we've had previous redirects
if (urlList.length > 1) {
Object.defineProperty(response, "redirected", { value: true });
}
return response;
}
// Handle based on redirect mode
switch (request.redirect) {
case "manual":
// Return an opaque redirect response
return createOpaqueRedirectResponse(this.#Response, request.url);
case "error":
// Just throw an error
throw new TypeError(`Redirect at ${request.url} was blocked due to redirect mode being 'error'`);
case "follow":
default:
// Continue with redirect handling
break;
}
// Get and validate the redirect location
const location = response.headers.get("Location");
if (!location) {
throw new TypeError(`Redirect at ${request.url} has no Location header`);
}
// Construct the new URL
let redirectUrl;
try {
redirectUrl = new URL(location, request.url);
}
catch {
throw new TypeError(`Invalid redirect URL: ${location}`);
}
// Check for redirect loops
if (urlList.some(url => url.href === redirectUrl.href)) {
throw new TypeError(`Redirect loop detected for ${redirectUrl.href}`);
}
// Check redirect limit
if (urlList.length >= 20) {
throw new TypeError("Too many redirects (maximum is 20)");
}
let method = request.method;
const headers = new Headers(request.headers);
// If this is a redirect that changes the method, adjust accordingly
if (redirectNeedsAdjustment(request, response.status)) {
method = "GET";
for (const header of headers.keys()) {
// Remove headers that should not be sent with GET requests
if (isRequestBodyHeader(header)) {
headers.delete(header);
}
}
}
// Create a new request for the redirect
const init = {
method,
headers,
mode: request.mode,
credentials: request.credentials,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
signal: request.signal,
keepalive: request.keepalive,
body: null,
};
// Determine if we should preserve the body (307/308 redirects)
const preserveBodyStatus = isBodyPreservingRedirectStatus(response.status);
if (preserveBodyStatus &&
requestBody !== null &&
!isBodylessMethod(method)) {
init.body = requestBody;
}
// Check if this is a cross-origin redirect
const currentOrigin = new URL(request.url).origin;
const isCrossOrigin = this.#baseUrl && redirectUrl.origin !== currentOrigin;
if (isCrossOrigin) {
// For non-same-origin redirect, remove authorization header
init.headers.delete("authorization");
// For cross-origin redirect with credentials, check for CORS issues
if (request.credentials === "include" &&
!isTaintedResponse(response)) {
throw createCorsError(redirectUrl.href, this.#baseUrl?.origin || "", "Cross-origin redirect with credentials is not allowed");
}
}
// Make the new request
const redirectRequest = new this.#Request(redirectUrl.href, init);
const redirectResponse = await this.#internalFetch(redirectRequest, init.body);
// Process further redirects recursively
return this.#processRedirect(redirectResponse, redirectRequest, urlList, init.body);
}
// #region: Testing Helpers

@@ -365,0 +519,0 @@ /**

/**
* Checks if a status code represents a redirect
* @param {number} status The HTTP status code
* @returns {boolean} True if the status code is a redirect status
*/
export function isRedirectStatus(status: number): boolean;
/**
* Checks if a status code is a redirect that changes the method to GET
* @param {number} status The HTTP status code
* @returns {boolean} True if the status code is a method-changing redirect status
*/
export function isMethodChangingRedirectStatus(status: number): boolean;
/**
* Checks if a status code is a body-preserving redirect
* @param {number} status The HTTP status code
* @returns {boolean} True if the status code is a body-preserving redirect status
*/
export function isBodyPreservingRedirectStatus(status: number): boolean;
/**
* Checks if a method is considered a safe method (GET or HEAD)
* @param {string} method The HTTP method
* @returns {boolean} True if the method is a safe method
*/
export function isBodylessMethod(method: string): boolean;
/**
* Checks if a header is a request body header
* @param {string} header The HTTP header name
* @returns {boolean} True if the header is a request body header
*/
export function isRequestBodyHeader(header: string): boolean;
/**
* @fileoverview HTTP status codes and text

@@ -3,0 +33,0 @@ * @author Nicholas C. Zakas

@@ -83,1 +83,52 @@ /**

];
const redirectStatuses = new Set([301, 302, 303, 307, 308]);
const methodChangingRedirectStatuses = new Set([301, 302, 303]);
const bodyPreservingRedirectStatuses = new Set([307, 308]);
// methods that don't need request bodies
const bodylessMethods = new Set(["GET", "HEAD"]);
const requestBodyHeaders = new Set([
"content-encoding",
"content-language",
"content-location",
"content-type",
]);
/**
* Checks if a status code represents a redirect
* @param {number} status The HTTP status code
* @returns {boolean} True if the status code is a redirect status
*/
export function isRedirectStatus(status) {
return redirectStatuses.has(status);
}
/**
* Checks if a status code is a redirect that changes the method to GET
* @param {number} status The HTTP status code
* @returns {boolean} True if the status code is a method-changing redirect status
*/
export function isMethodChangingRedirectStatus(status) {
return methodChangingRedirectStatuses.has(status);
}
/**
* Checks if a status code is a body-preserving redirect
* @param {number} status The HTTP status code
* @returns {boolean} True if the status code is a body-preserving redirect status
*/
export function isBodyPreservingRedirectStatus(status) {
return bodyPreservingRedirectStatuses.has(status);
}
/**
* Checks if a method is considered a safe method (GET or HEAD)
* @param {string} method The HTTP method
* @returns {boolean} True if the method is a safe method
*/
export function isBodylessMethod(method) {
return bodylessMethods.has(method);
}
/**
* Checks if a header is a request body header
* @param {string} header The HTTP header name
* @returns {boolean} True if the header is a request body header
*/
export function isRequestBodyHeader(header) {
return requestBodyHeaders.has(header);
}
{
"name": "mentoss",
"version": "0.9.2",
"version": "0.10.0",
"description": "A utility to mock fetch requests and responses.",

@@ -5,0 +5,0 @@ "type": "module",