diff --git a/lib/cookie.ts b/lib/cookie.ts index 50e90209..6f0abb67 100644 --- a/lib/cookie.ts +++ b/lib/cookie.ts @@ -39,6 +39,7 @@ import * as validators from './validators' import { version } from './version' import { permuteDomain } from './permuteDomain' import { getCustomInspectSymbol } from './utilHelper' +import { ErrorCallback, safeToString } from './utils' // From RFC6265 S4.1.1 // note that it excludes \x3B ";" @@ -357,7 +358,7 @@ function parseDate(str: string | undefined | null): Date | undefined { } function formatDate(date: Date) { - validators.validate(validators.isDate(date), date) + validators.validate(validators.isDate(date), safeToString(date)) return date.toUTCString() } @@ -699,7 +700,7 @@ function parse( * @returns boolean */ function isSecurePrefixConditionMet(cookie: Cookie) { - validators.validate(validators.isObject(cookie), cookie) + validators.validate(validators.isObject(cookie), safeToString(cookie)) const startsWithSecurePrefix = typeof cookie.key === 'string' && cookie.key.startsWith('__Secure-') return !startsWithSecurePrefix || cookie.secure @@ -786,8 +787,8 @@ function fromJSON(str: string | SerializedCookie | null | undefined) { */ function cookieCompare(a: Cookie, b: Cookie) { - validators.validate(validators.isObject(a), a) - validators.validate(validators.isObject(b), b) + validators.validate(validators.isObject(a), safeToString(a)) + validators.validate(validators.isObject(b), safeToString(b)) let cmp = 0 // descending for length: b CMP a @@ -977,8 +978,11 @@ export class Cookie { ) { return false } - // @ts-ignore - if (this.maxAge != null && this.maxAge <= 0) { + if ( + this.maxAge != null && + this.maxAge !== 'Infinity' && + (this.maxAge === '-Infinity' || this.maxAge <= 0) + ) { return false // "Max-Age=" non-zero-digit *DIGIT } if (this.path != null && !PATH_VALUE.test(this.path)) { @@ -1343,7 +1347,11 @@ export class CookieJar { const promiseCallback = createPromiseCallback(arguments) const cb = promiseCallback.callback - validators.validate(validators.isNonEmptyString(url), callback, options) + validators.validate( + validators.isNonEmptyString(url), + callback, + safeToString(options), + ) let err if (validators.isFunction(url)) { @@ -1629,7 +1637,7 @@ export class CookieJar { if (typeof options === 'function' || options === undefined) { options = defaultGetCookieOptions } - validators.validate(validators.isObject(options), cb, options) + validators.validate(validators.isObject(options), cb, safeToString(options)) validators.validate(validators.isFunction(cb), cb) const host = canonicalDomain(context.hostname) @@ -2247,4 +2255,3 @@ export type Callback = ( error: Error | undefined, result: T | undefined, ) => void -export type ErrorCallback = (error: Error) => void diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 00000000..a66e5a37 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,19 @@ +/** Signature for a callback function that expects an error to be passed. */ +export type ErrorCallback = (error: Error, result?: never) => void + +/** Unbound `Object.prototype.toString`. */ +const unboundToString = Object.prototype.toString + +/** Wrapped `Object.prototype.toString`, so that you don't need to remember to use `.call()`. */ +export const objectToString = (obj: unknown) => unboundToString.call(obj) + +/** Safely converts any value to string, using the value's own `toString` when available. */ +export const safeToString = (val: unknown) => { + // Ideally, we'd just use String() for everything, but it breaks if `toString` is missing (mostly + // values with no prototype), so we have to use Object#toString as a fallback. + if (val === undefined || val === null || typeof val.toString === 'function') { + return String(val) + } else { + return objectToString(val) + } +} diff --git a/lib/validators.ts b/lib/validators.ts index a99ed49b..35db4755 100644 --- a/lib/validators.ts +++ b/lib/validators.ts @@ -25,58 +25,79 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ************************************************************************************ */ -'use strict' -const toString = Object.prototype.toString +import { ErrorCallback, objectToString, safeToString } from './utils' /* Validation functions copied from check-types package - https://www.npmjs.com/package/check-types */ -export function isFunction(data: unknown): boolean { + +/** Determines whether the argument is a function. */ +export function isFunction(data: unknown): data is Function { return typeof data === 'function' } +/** Determines whether the argument is a non-empty string. */ export function isNonEmptyString(data: unknown): boolean { return isString(data) && data !== '' } +/** Determines whether the argument is a *valid* Date. */ export function isDate(data: unknown): boolean { - if (data instanceof Date) { - return isInteger(data.getTime()) - } - return false + return isInstanceStrict(data, Date) && isInteger(data.getTime()) } +/** Determines whether the argument is the empty string. */ export function isEmptyString(data: unknown): boolean { return data === '' || (data instanceof String && data.toString() === '') } +/** Determines whether the argument is a string. */ export function isString(data: unknown): boolean { return typeof data === 'string' || data instanceof String } +/** Determines whether the string representation of the argument is "[object Object]". */ export function isObject(data: unknown): boolean { - return toString.call(data) === '[object Object]' + return objectToString(data) === '[object Object]' } +/** Determines whether the first argument is an instance of the second. */ +export function isInstanceStrict( + data: unknown, + Constructor: T, +): data is T['prototype'] { + try { + return data instanceof Constructor + } catch { + return false + } +} + +/** Determines whether the argument is an integer. */ export function isInteger(data: unknown): boolean { return typeof data === 'number' && data % 1 === 0 } -/* End validation functions */ -export function validate(bool: boolean, cb?: unknown, options?: unknown): void { - if (!isFunction(cb)) { - options = cb - cb = null - } - if (!isObject(options)) options = { Error: 'Failed Check' } - if (!bool) { - if (typeof cb === 'function') { - // @ts-ignore - cb(new ParameterError(options)) - } else { - // @ts-ignore - throw new ParameterError(options) - } - } +/* -- End validation functions -- */ + +/** + * When the first argument is false, an error is created with the given message. If a callback is + * provided, the error is passed to the callback, otherwise the error is thrown. + */ +export function validate( + bool: boolean, + cbOrMessage?: ErrorCallback | string, + message?: string, +): void { + if (bool) return // Validation passes + const cb = isFunction(cbOrMessage) ? cbOrMessage : null + let options = isFunction(cbOrMessage) ? message : cbOrMessage + // The default message prior to v5 was '[object Object]' due to a bug, and the message is kept + // for backwards compatibility. + if (!isObject(options)) options = '[object Object]' + + const err = new ParameterError(safeToString(options)) + if (cb) cb(err) + else throw err } export class ParameterError extends Error {} diff --git a/package-lock.json b/package-lock.json index fc3f5781..167bf701 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@types/jest": "^29", + "@types/node": "^16.18.23", "@types/psl": "^1", "@types/punycode": "^2", "@types/url-parse": "^1.4.8", @@ -1660,9 +1661,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.15.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", - "integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==", + "version": "16.18.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.23.tgz", + "integrity": "sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==", "dev": true }, "node_modules/@types/prettier": { @@ -7896,9 +7897,9 @@ "dev": true }, "@types/node": { - "version": "18.15.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", - "integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==", + "version": "16.18.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.23.tgz", + "integrity": "sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==", "dev": true }, "@types/prettier": { diff --git a/package.json b/package.json index 8d3ffa67..85d0e571 100644 --- a/package.json +++ b/package.json @@ -100,13 +100,14 @@ }, "devDependencies": { "@types/jest": "^29", + "@types/node": "^16.18.23", "@types/psl": "^1", "@types/punycode": "^2", "@types/url-parse": "^1.4.8", + "@typescript-eslint/eslint-plugin": "^5.57.0", + "@typescript-eslint/parser": "^5.57.0", "async": "2.6.4", "eslint": "^8.36.0", - "@typescript-eslint/parser": "^5.57.0", - "@typescript-eslint/eslint-plugin": "^5.57.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", "genversion": "^3.1.1",