diff --git a/package-lock.json b/package-lock.json index f3c68d89c..0ee290ea7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11521,6 +11521,33 @@ "node": ">=6" } }, + "node_modules/clone-regexp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", + "integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==", + "dev": true, + "dependencies": { + "is-regexp": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-regexp/node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -11953,6 +11980,18 @@ "node": ">= 0.6" } }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -15557,6 +15596,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz", + "integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -16902,6 +16953,14 @@ "node": ">= 12" } }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -17155,6 +17214,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "dependencies": { + "ip-regex": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -26277,6 +26347,23 @@ "dev": true, "license": "MIT" }, + "node_modules/super-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz", + "integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==", + "dev": true, + "dependencies": { + "clone-regexp": "^3.0.0", + "function-timeout": "^0.1.0", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -26809,6 +26896,21 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "dev": true, + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/timers-browserify": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", @@ -26906,6 +27008,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", @@ -26921,6 +27024,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -29943,7 +30047,6 @@ "lodash": "^4.17.21", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", - "tough-cookie": "^4.1.3", "xmlbuilder": "^15.1.1", "yargs": "^17.6.2" }, @@ -31007,6 +31110,9 @@ "name": "@usebruno/common", "version": "0.1.0", "license": "MIT", + "dependencies": { + "is-ip": "^3.1.0" + }, "devDependencies": { "@babel/preset-env": "^7.26.9", "@babel/preset-typescript": "^7.27.0", @@ -31017,6 +31123,7 @@ "@rollup/plugin-typescript": "^12.1.2", "@types/jest": "^29.5.14", "babel-jest": "^29.7.0", + "is-ip": "^5.0.1", "moment": "^2.29.4", "rollup": "3.29.5", "rollup-plugin-dts": "^5.0.0", @@ -31537,6 +31644,34 @@ "node": ">=4" } }, + "packages/bruno-common/node_modules/ip-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/bruno-common/node_modules/is-ip": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.1.tgz", + "integrity": "sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==", + "dev": true, + "dependencies": { + "ip-regex": "^5.0.0", + "super-regex": "^0.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/bruno-common/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -31723,7 +31858,6 @@ "nanoid": "3.3.8", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", - "tough-cookie": "^4.1.3", "uuid": "^9.0.0", "yup": "^0.32.11" }, diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.js b/packages/bruno-app/src/utils/codemirror/autocomplete.js index 7b65f4234..adfa74939 100644 --- a/packages/bruno-app/src/utils/codemirror/autocomplete.js +++ b/packages/bruno-app/src/utils/codemirror/autocomplete.js @@ -78,7 +78,17 @@ const STATIC_API_HINTS = { 'bru.runner.setNextRequest(requestName)', 'bru.runner.skipRequest()', 'bru.runner.stopExecution()', - 'bru.interpolate(str)' + 'bru.interpolate(str)', + 'bru.cookies', + 'bru.cookies.jar()', + 'bru.cookies.jar().getCookie(url, name, callback)', + 'bru.cookies.jar().getCookies(url, callback)', + 'bru.cookies.jar().setCookie(url, name, value, callback)', + 'bru.cookies.jar().setCookie(url, cookieObject, callback)', + 'bru.cookies.jar().setCookies(url, cookiesArray, callback)', + 'bru.cookies.jar().clear(callback)', + 'bru.cookies.jar().deleteCookies(url, callback)', + 'bru.cookies.jar().deleteCookie(url, name, callback)', ] }; diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index ca55861f1..c298273b4 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -48,12 +48,12 @@ "dependencies": { "@aws-sdk/credential-providers": "3.750.0", "@usebruno/common": "0.1.0", - "@usebruno/js": "0.12.0", - "@usebruno/lang": "0.12.0", - "@usebruno/vm2": "^3.9.13", - "@usebruno/requests": "^0.1.0", "@usebruno/converters": "^0.1.0", "@usebruno/filestore": "^0.1.0", + "@usebruno/js": "0.12.0", + "@usebruno/lang": "0.12.0", + "@usebruno/requests": "^0.1.0", + "@usebruno/vm2": "^3.9.13", "aws4-axios": "^3.3.0", "axios": "^1.8.3", "axios-ntlm": "^1.4.2", @@ -69,7 +69,6 @@ "lodash": "^4.17.21", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", - "tough-cookie": "^4.1.3", "xmlbuilder": "^15.1.1", "yargs": "^17.6.2" } diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index ed1e554e0..a682d4814 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -20,7 +20,7 @@ const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-he const { shouldUseProxy, PatchedHttpsProxyAgent, getSystemProxyEnvVariables } = require('../utils/proxy-util'); const path = require('path'); const { parseDataFromResponse } = require('../utils/common'); -const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies'); +const { getCookieStringForUrl, saveCookies } = require('../utils/cookies'); const { createFormData } = require('../utils/form-data'); const { getOAuth2Token } = require('./oauth2'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; diff --git a/packages/bruno-cli/src/utils/cookies.js b/packages/bruno-cli/src/utils/cookies.js index 01a82316b..f4aaef547 100644 --- a/packages/bruno-cli/src/utils/cookies.js +++ b/packages/bruno-cli/src/utils/cookies.js @@ -1,103 +1 @@ -const { Cookie, CookieJar } = require('tough-cookie'); -const each = require('lodash/each'); -const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils; - -const cookieJar = new CookieJar(); - -const addCookieToJar = (setCookieHeader, requestUrl) => { - const cookie = Cookie.parse(setCookieHeader, { loose: true }); - cookieJar.setCookieSync(cookie, requestUrl, { - ignoreError: true // silently ignore things like parse errors and invalid domains - }); -}; - -const getCookiesForUrl = (url) => { - return cookieJar.getCookiesSync(url, { - secure: isPotentiallyTrustworthyOrigin(url) - }); -}; - -const getCookieStringForUrl = (url) => { - const cookies = getCookiesForUrl(url); - - if (!Array.isArray(cookies) || !cookies.length) { - return ''; - } - - const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now()); - - return validCookies.map((cookie) => cookie.cookieString()).join('; '); -}; - -const getDomainsWithCookies = () => { - return new Promise((resolve, reject) => { - const domainCookieMap = {}; - - cookieJar.store.getAllCookies((err, cookies) => { - if (err) { - return reject(err); - } - - cookies.forEach((cookie) => { - if (!domainCookieMap[cookie.domain]) { - domainCookieMap[cookie.domain] = [cookie]; - } else { - domainCookieMap[cookie.domain].push(cookie); - } - }); - - const domains = Object.keys(domainCookieMap); - const domainsWithCookies = []; - - each(domains, (domain) => { - const cookies = domainCookieMap[domain]; - const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now()); - - if (validCookies.length) { - domainsWithCookies.push({ - domain, - cookies: validCookies, - cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ') - }); - } - }); - - resolve(domainsWithCookies); - }); - }); -}; - -const deleteCookiesForDomain = (domain) => { - return new Promise((resolve, reject) => { - cookieJar.store.removeCookies(domain, null, (err) => { - if (err) { - return reject(err); - } - - return resolve(); - }); - }); -}; - -const saveCookies = (url, headers) => { - let setCookieHeaders = []; - if (headers['set-cookie']) { - setCookieHeaders = Array.isArray(headers['set-cookie']) - ? headers['set-cookie'] - : [headers['set-cookie']]; - for (let setCookieHeader of setCookieHeaders) { - if (typeof setCookieHeader === 'string' && setCookieHeader.length) { - addCookieToJar(setCookieHeader, url); - } - } - } -} - -module.exports = { - addCookieToJar, - getCookiesForUrl, - getCookieStringForUrl, - getDomainsWithCookies, - deleteCookiesForDomain, - saveCookies -}; +module.exports = require('@usebruno/common').cookies; diff --git a/packages/bruno-common/jest.config.js b/packages/bruno-common/jest.config.js index cd4a5f5ae..546abd629 100644 --- a/packages/bruno-common/jest.config.js +++ b/packages/bruno-common/jest.config.js @@ -3,7 +3,7 @@ module.exports = { '^.+\\.(ts|js)$': 'babel-jest', }, transformIgnorePatterns: [ - '/node_modules/(?!(lodash-es)/)', + '/node_modules/(?!(lodash-es|is-ip|ip-regex|super-regex|function-timeout|time-span|convert-hrtime|clone-regexp|is-regexp)/)' ], testEnvironment: 'node' }; diff --git a/packages/bruno-common/package.json b/packages/bruno-common/package.json index 66ded2519..c989069df 100644 --- a/packages/bruno-common/package.json +++ b/packages/bruno-common/package.json @@ -46,6 +46,7 @@ "@rollup/plugin-typescript": "^12.1.2", "@types/jest": "^29.5.14", "babel-jest": "^29.7.0", + "is-ip": "^5.0.1", "moment": "^2.29.4", "rollup": "3.29.5", "rollup-plugin-dts": "^5.0.0", diff --git a/packages/bruno-common/src/cookies/index.ts b/packages/bruno-common/src/cookies/index.ts new file mode 100644 index 000000000..2bfbf7a77 --- /dev/null +++ b/packages/bruno-common/src/cookies/index.ts @@ -0,0 +1,500 @@ +import { Cookie, CookieJar } from 'tough-cookie'; +import each from 'lodash/each'; +import moment from 'moment'; +import { isPotentiallyTrustworthyOrigin } from '../utils'; + +const cookieJar = new CookieJar(); + +const addCookieToJar = (setCookieHeader: string, requestUrl: string): void => { + const cookie = Cookie.parse(setCookieHeader, { loose: true }); + if (!cookie) return; + cookieJar.setCookieSync(cookie, requestUrl, { + ignoreError: true + }); +}; + +const getCookiesForUrl = (url: string) => { + return cookieJar.getCookiesSync(url, { + secure: isPotentiallyTrustworthyOrigin(url) + }); +}; + +const getCookieStringForUrl = (url: string): string => { + const cookies = getCookiesForUrl(url); + if (!Array.isArray(cookies) || !cookies.length) return ''; + + const validCookies = cookies.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now()); + return validCookies.map((cookie) => cookie.cookieString()).join('; '); +}; + +const getDomainsWithCookies = (): Promise> => { + return new Promise((resolve, reject) => { + const domainCookieMap: Record = {}; + + (cookieJar as any).store.getAllCookies((err: Error, cookies: Cookie[]) => { + if (err) return reject(err); + + cookies.forEach((cookie) => { + // Handle null domain by skipping the cookie + if (!cookie.domain) return; + + if (!domainCookieMap[cookie.domain]) { + domainCookieMap[cookie.domain] = [cookie]; + } else { + domainCookieMap[cookie.domain].push(cookie); + } + }); + + const domains = Object.keys(domainCookieMap); + const domainsWithCookies: Array<{ domain: string; cookies: Cookie[]; cookieString: string }> = []; + + each(domains, (domain) => { + const cookiesForDomain = domainCookieMap[domain]; + const validCookies = cookiesForDomain.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now()); + + if (validCookies.length) { + domainsWithCookies.push({ + domain, + cookies: validCookies, + cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ') + }); + } + }); + + resolve(domainsWithCookies); + }); + }); +}; + +const deleteCookie = (domain: string, path: string, cookieKey: string): Promise => { + return new Promise((resolve, reject) => { + (cookieJar as any).store.removeCookie(domain, path, cookieKey, (err: Error) => { + if (err) return reject(err); + resolve(); + }); + }); +}; + +const deleteCookiesForDomain = (domain: string): Promise => { + return new Promise((resolve, reject) => { + (cookieJar as any).store.removeCookies(domain, null, (err: Error) => { + if (err) return reject(err); + resolve(); + }); + }); +}; + +const updateCookieObj = (cookieObj: any, oldCookie: Cookie) => { + return { + ...cookieObj, + path: oldCookie.path, + key: oldCookie.key, + domain: oldCookie.domain, + expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity, + creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(), + lastAccessed: + oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid() + ? new Date(oldCookie.lastAccessed) + : new Date() + } as any; +}; + +const createCookieObj = (cookieObj: any) => { + return { + ...cookieObj, + path: cookieObj.path, + expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity, + creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(), + lastAccessed: + cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid() + ? new Date(cookieObj.lastAccessed) + : new Date() + } as any; +}; + +const addCookieForDomain = (domain: string, cookieObj: any): Promise => { + return new Promise((resolve, reject) => { + try { + const cookie = new Cookie(createCookieObj(cookieObj)); + (cookieJar as any).store.putCookie(cookie, (err: Error) => { + if (err) return reject(err); + resolve(); + }); + } catch (err) { + reject(err); + } + }); +}; + +const modifyCookieForDomain = (domain: string, oldCookieObj: any, cookieObj: any): Promise => { + return new Promise((resolve, reject) => { + try { + const oldCookie = new Cookie(createCookieObj(oldCookieObj)); + const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie)); + (cookieJar as any).store.updateCookie(oldCookie, newCookie, (removeErr: Error) => { + if (removeErr) return reject(removeErr); + resolve(); + }); + } catch (err) { + reject(err); + } + }); +}; + +const parseCookieString = (cookieStr: string): any | null => { + try { + const cookie = Cookie.parse(cookieStr); + if (!cookie) return null; + return { + ...cookie, + expires: cookie.expires === 'Infinity' || (cookie.expires as any) === Infinity ? null : cookie.expires + }; + } catch (err) { + throw err; + } +}; + +const createCookieString = (cookieObj: any): string => { + const cookie = new Cookie(createCookieObj(cookieObj)); + let cookieString = cookie.toString(); // tough-cookie omits domain + + // Manually append domain if cookie is hostOnly but we still want Domain flag + if (cookieObj.hostOnly && !cookieString.includes('Domain=')) { + cookieString += `; Domain=${cookieObj.domain}`; + } + return cookieString; +} + +const saveCookies = (url: string, headers: any) => { + if (headers['set-cookie']) { + let setCookieHeaders = Array.isArray(headers['set-cookie']) + ? headers['set-cookie'] + : [headers['set-cookie']]; + for (let setCookieHeader of setCookieHeaders) { + if (typeof setCookieHeader === 'string' && setCookieHeader.length) { + addCookieToJar(setCookieHeader, url); + } + } + } +}; + +const cookieJarWrapper = () => { + return { + + // Get the full cookie object for the given URL & name. + getCookie: function ( + url: string, + cookieName: string, + callback?: (err: Error | null | undefined, cookie?: Cookie | null) => void + ) { + if (!url || !cookieName) { + const error = new Error('URL and cookie name are required'); + if (callback) return callback(error); + return Promise.reject(error); + } + + if (callback) { + // Callback mode + return cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return callback(err); + const cookie = cookies.find((c) => c.key === cookieName); + callback(null, cookie || null); + }); + } + + // Promise mode + return new Promise((resolve, reject) => { + cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return reject(err); + const cookie = cookies.find((c) => c.key === cookieName); + resolve(cookie || null); + }); + }); + }, + + // Get all cookies that would be sent to the given URL. + getCookies: function (url: string, callback?: (err: Error | null | undefined, cookies?: Cookie[]) => void) { + if (!url) { + const error = new Error('URL is required'); + if (callback) return callback(error); + return Promise.reject(error); + } + + if (callback) { + // Callback mode + return cookieJar.getCookies(url, callback); + } + + // Promise mode + return new Promise((resolve, reject) => { + cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return reject(err); + resolve(cookies); + }); + }); + }, + + setCookie: function ( + url: string, + nameOrCookieObj: string | Record, + valueOrCallback?: string | ((err?: Error | undefined) => void), + maybeCallback?: (err?: Error | undefined) => void + ) { + // Determine the callback + let callback: ((err?: Error | undefined) => void) | undefined; + if (typeof maybeCallback === 'function') { + callback = maybeCallback; + } else if (typeof valueOrCallback === 'function') { + callback = valueOrCallback as (err?: Error | undefined) => void; + } + + const executeSetCookie = () => { + if (!url) throw new Error('URL is required'); + + // CASE 1: name/value pair provided + if (typeof nameOrCookieObj === 'string') { + const cookieName = nameOrCookieObj; + const cookieValue = typeof valueOrCallback === 'string' ? valueOrCallback : ''; + + if (!cookieName) throw new Error('Cookie name is required'); + + const cookie = new Cookie({ + key: cookieName, + value: cookieValue, + domain: new URL(url).hostname, + }); + + cookieJar.setCookieSync(cookie, url, { ignoreError: true }); + return; + } + + // CASE 2: cookie object provided + if (typeof nameOrCookieObj === 'object' && nameOrCookieObj !== null) { + const obj = { ...(nameOrCookieObj as any) } as any; + + if (!obj.key && obj.name) obj.key = obj.name; + if (!obj.key) throw new Error('cookieObject.key (name) is required'); + + const base = { + domain: new URL(url).hostname, + ...obj, + } as any; + + const processedCookie = createCookieObj(base); + const cookie = new Cookie(processedCookie); + cookieJar.setCookieSync(cookie, url, { ignoreError: true }); + return; + } + + // If we reach here, arguments were invalid + throw new Error('Invalid arguments passed to setCookie'); + }; + + if (callback) { + // Callback mode + try { + executeSetCookie(); + callback(undefined); + } catch (err) { + callback(err as Error); + } + return; + } + + // Promise mode + return new Promise((resolve, reject) => { + try { + executeSetCookie(); + resolve(); + } catch (err) { + reject(err); + } + }); + }, + + + setCookies: function ( + url: string, + cookiesArray: any[], + callback?: (err?: Error | undefined) => void + ) { + const executeSetCookies = () => { + if (!url) throw new Error('URL is required'); + if (!Array.isArray(cookiesArray)) { + throw new Error('setCookies expects an array of cookie objects'); + } + + for (const cookieObject of cookiesArray) { + const obj = { ...(cookieObject as any) } as any; + + if (!obj.key && obj.name) obj.key = obj.name; + if (!obj.key) throw new Error('cookieObject.key (name) is required'); + + const base = { + domain: new URL(url).hostname, + ...obj + } as any; + + const processedCookie = createCookieObj(base); + const cookie = new Cookie(processedCookie); + cookieJar.setCookieSync(cookie, url, { ignoreError: true }); + } + }; + + if (callback) { + // Callback mode + try { + executeSetCookies(); + callback(undefined); + } catch (err) { + callback(err as Error); + } + return; + } + + // Promise mode + return new Promise((resolve, reject) => { + try { + executeSetCookies(); + resolve(); + } catch (err) { + reject(err); + } + }); + }, + + + clear: function (callback?: (err?: Error | undefined) => void) { + if (callback) { + // Callback mode + return (cookieJar as any).store.removeAllCookies(callback); + } + + // Promise mode + return new Promise((resolve, reject) => { + (cookieJar as any).store.removeAllCookies((err?: Error) => { + if (err) reject(err); + else resolve(); + }); + }); + }, + + deleteCookies: function (url: string, callback?: (err?: Error | undefined) => void) { + if (!url) { + const error = new Error('URL is required'); + if (callback) return callback(error); + return Promise.reject(error); + } + + if (callback) { + // Callback mode + return cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return callback(err); + if (!cookies || !cookies.length) return callback(undefined); + + let pending = cookies.length; + const done = (removeErr?: Error) => { + if (removeErr) return callback(removeErr); + if (--pending === 0) { + callback(undefined); + } + }; + + cookies.forEach((cookie) => { + (cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done); + }); + }); + } + + // Promise mode + return new Promise((resolve, reject) => { + cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return reject(err); + if (!cookies || !cookies.length) return resolve(); + + let pending = cookies.length; + const done = (removeErr?: Error) => { + if (removeErr) return reject(removeErr); + if (--pending === 0) { + resolve(); + } + }; + + cookies.forEach((cookie) => { + (cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done); + }); + }); + }); + }, + + deleteCookie: function (url: string, cookieName: string, callback?: (err?: Error | undefined) => void) { + if (!url || !cookieName) { + const error = new Error('URL and cookie name are required'); + if (callback) return callback(error); + return Promise.reject(error); + } + + const executeDelete = (callback: (err?: Error) => void) => { + cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return callback(err); + + // Filter cookies matching key + const matchingCookies = (cookies || []).filter((c) => c.key === cookieName); + if (!matchingCookies.length) return callback(undefined); + + const urlPath = new URL(url).pathname || '/'; + + // Prioritise a cookie whose path exactly matches the URL path + let cookieToDelete = matchingCookies.find((c) => c.path === urlPath); + + // If not found, fall back to the first matching cookie (most specific path first) + if (!cookieToDelete) { + // tough-cookie sorts cookies by path length desc, preserve that order + cookieToDelete = matchingCookies[0]; + } + + (cookieJar as any).store.removeCookie( + cookieToDelete.domain, + cookieToDelete.path, + cookieToDelete.key, + callback + ); + }); + }; + + if (callback) { + // Callback mode + return executeDelete(callback); + } + + // Promise mode + return new Promise((resolve, reject) => { + executeDelete((err?: Error) => { + if (err) reject(err); + else resolve(); + }); + }); + } + } as const; +}; + + +const cookiesModule = { + cookieJar, + addCookieToJar, + getCookiesForUrl, + getCookieStringForUrl, + getDomainsWithCookies, + deleteCookie, + deleteCookiesForDomain, + addCookieForDomain, + modifyCookieForDomain, + parseCookieString, + createCookieString, + updateCookieObj, + createCookieObj, + jar: cookieJarWrapper, + saveCookies +}; + +export default cookiesModule; \ No newline at end of file diff --git a/packages/bruno-common/src/index.ts b/packages/bruno-common/src/index.ts index e72c1d847..15e55346e 100644 --- a/packages/bruno-common/src/index.ts +++ b/packages/bruno-common/src/index.ts @@ -1,5 +1,6 @@ export { mockDataFunctions } from './utils/faker-functions'; export { default as interpolate } from './interpolate'; export { default as isRequestTagsIncluded } from './tags'; +export { default as cookies } from './cookies'; export * as utils from './utils'; \ No newline at end of file diff --git a/packages/bruno-common/src/utils/index.ts b/packages/bruno-common/src/utils/index.ts index 70c92ea7c..4f79ff185 100644 --- a/packages/bruno-common/src/utils/index.ts +++ b/packages/bruno-common/src/utils/index.ts @@ -1,5 +1,9 @@ export { encodeUrl, parseQueryParams, - buildQueryString + buildQueryString, } from './url'; + +export { + isPotentiallyTrustworthyOrigin +} from './url/validation'; \ No newline at end of file diff --git a/packages/bruno-requests/src/utils/cookie-utils.spec.js b/packages/bruno-common/src/utils/url/validation.spec.ts similarity index 98% rename from packages/bruno-requests/src/utils/cookie-utils.spec.js rename to packages/bruno-common/src/utils/url/validation.spec.ts index 90e8e74c3..1732898f5 100644 --- a/packages/bruno-requests/src/utils/cookie-utils.spec.js +++ b/packages/bruno-common/src/utils/url/validation.spec.ts @@ -1,4 +1,4 @@ -const { isPotentiallyTrustworthyOrigin } = require('./cookie-utils'); +import { isPotentiallyTrustworthyOrigin } from './validation'; describe('isPotentiallyTrustworthyOrigin', () => { describe('secure schemes', () => { @@ -130,4 +130,4 @@ describe('isPotentiallyTrustworthyOrigin', () => { expect(isPotentiallyTrustworthyOrigin('wss://localhost')).toBe(true); }); }); -}); +}); \ No newline at end of file diff --git a/packages/bruno-common/src/utils/url/validation.ts b/packages/bruno-common/src/utils/url/validation.ts new file mode 100644 index 000000000..1a94bcfdc --- /dev/null +++ b/packages/bruno-common/src/utils/url/validation.ts @@ -0,0 +1,67 @@ +import { isIPv4, isIPv6, isIP } from 'is-ip'; + +const hostNoBrackets = (host: string): string => { + if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) { + return host.substring(1, host.length - 1); + } + return host; +}; + +const isLoopbackV4 = (address: string): boolean => { + const octets = address.split('.'); + if (octets.length !== 4 || parseInt(octets[0], 10) !== 127) { + return false; + } + return octets.every((octet) => { + const n = parseInt(octet, 10); + return !Number.isNaN(n) && n >= 0 && n <= 255; + }); +}; + +const isLoopbackV6 = (address: string): boolean => address === '::1'; + +const isIpLoopback = (address: string): boolean => { + if (isIPv4(address)) { + return isLoopbackV4(address); + } + if (isIPv6(address)) { + return isLoopbackV6(address); + } + return false; +}; + +const isNormalizedLocalhostTLD = (host: string): boolean => host.toLowerCase().endsWith('.localhost'); + +const isLocalHostname = (host: string): boolean => { + return host.toLowerCase() === 'localhost' || isNormalizedLocalhostTLD(host); +}; + +/** + * Mirrors Chrome / Secure Contexts spec for "potentially trustworthy origins". + */ +const isPotentiallyTrustworthyOrigin = (urlString: string): boolean => { + let url: URL; + try { + url = new URL(urlString); + } catch { + return false; // invalid URL or opaque origin + } + + const scheme = url.protocol.replace(':', '').toLowerCase(); + const hostname = hostNoBrackets(url.hostname).replace(/\.+$/, ''); + + // Secure schemes + if (scheme === 'https' || scheme === 'wss' || scheme === 'file') { + return true; + } + + // IP literals + if (isIP(hostname)) { + return isIpLoopback(hostname); + } + + // localhost / *.localhost + return isLocalHostname(hostname); +}; + +export { isPotentiallyTrustworthyOrigin }; \ No newline at end of file diff --git a/packages/bruno-common/tests/cookies/cookie-jar-wrapper.spec.js b/packages/bruno-common/tests/cookies/cookie-jar-wrapper.spec.js new file mode 100644 index 000000000..c95dbaabe --- /dev/null +++ b/packages/bruno-common/tests/cookies/cookie-jar-wrapper.spec.js @@ -0,0 +1,228 @@ +const cookiesModule = require('../../src/cookies/index.ts').default; + +describe('Bruno Cookie Jar Wrapper - API Examples', () => { + let jar; + const testUrl = 'https://api.example.com'; + + beforeEach(() => { + jar = cookiesModule.jar(); + // Clear all cookies before each test + jar.clear(); + }); + + describe('Basic Cookie Operations', () => { + test('setCookie and getCookie - name/value pair', async () => { + const cookieName = 'authToken'; + const cookieValue = 'jwt123'; + + // Set a cookie + await jar.setCookie(testUrl, cookieName, cookieValue); + + // Get the cookie back + const cookie = await jar.getCookie(testUrl, cookieName); + expect(cookie.key).toBe(cookieName); + expect(cookie.value).toBe(cookieValue); + expect(cookie.domain).toBe('api.example.com'); + }); + + test('setCookie with cookie object', async () => { + const cookieObj = { + key: 'sessionId', + value: 'abc123', + path: '/api', + httpOnly: true, + secure: true + }; + + await jar.setCookie(testUrl, cookieObj); + + const cookie = await jar.getCookie(testUrl + '/api', 'sessionId'); + expect(cookie.key).toBe('sessionId'); + expect(cookie.value).toBe('abc123'); + expect(cookie.path).toBe('/api'); + expect(cookie.httpOnly).toBe(true); + expect(cookie.secure).toBe(true); + }); + + test('getCookie returns null for non-existent cookie', async () => { + const cookie = await jar.getCookie(testUrl, 'nonexistent'); + expect(cookie).toBeNull(); + }); + }); + + describe('Multiple Cookie Operations', () => { + test('setCookies with array of cookie objects', async () => { + const cookies = [ + { key: 'cookie1', value: 'value1' }, + { key: 'cookie2', value: 'value2' }, + { key: 'cookie3', value: 'value3', httpOnly: true } + ]; + + await jar.setCookies(testUrl, cookies); + + // Verify all cookies were set + const retrievedCookies = await jar.getCookies(testUrl); + expect(retrievedCookies).toHaveLength(3); + + const cookieNames = retrievedCookies.map(c => c.key); + expect(cookieNames).toContain('cookie1'); + expect(cookieNames).toContain('cookie2'); + expect(cookieNames).toContain('cookie3'); + }); + + test('getCookies returns all cookies for URL', async () => { + // Set multiple cookies + await jar.setCookie(testUrl, 'auth', 'token123'); + await jar.setCookie(testUrl, 'session', 'sess456'); + await jar.setCookie(testUrl, 'prefs', 'theme=dark'); + + const cookies = await jar.getCookies(testUrl); + expect(cookies).toHaveLength(3); + + const cookieMap = cookies.reduce((map, cookie) => { + map[cookie.key] = cookie.value; + return map; + }, {}); + + expect(cookieMap.auth).toBe('token123'); + expect(cookieMap.session).toBe('sess456'); + expect(cookieMap.prefs).toBe('theme=dark'); + }); + }); + + describe('Cookie Deletion', () => { + test('deleteCookie removes specific cookie', async () => { + // Set two cookies + await jar.setCookie(testUrl, 'keep', 'keepValue'); + await jar.setCookie(testUrl, 'remove', 'removeValue'); + + // Delete one cookie + await jar.deleteCookie(testUrl, 'remove'); + + // Verify only one cookie remains + const cookies = await jar.getCookies(testUrl); + expect(cookies).toHaveLength(1); + expect(cookies[0].key).toBe('keep'); + expect(cookies[0].value).toBe('keepValue'); + }); + + test('deleteCookies removes all cookies for URL', async () => { + // Set multiple cookies + await jar.setCookie(testUrl, 'cookie1', 'value1'); + await jar.setCookie(testUrl, 'cookie2', 'value2'); + + // Delete all cookies for the URL + await jar.deleteCookies(testUrl); + + // Verify no cookies remain + const cookies = await jar.getCookies(testUrl); + expect(cookies).toHaveLength(0); + }); + + test('clear removes all cookies from jar', async () => { + // Set cookies for multiple URLs + await jar.setCookie('https://site1.com', 'cookie1', 'value1'); + await jar.setCookie('https://site2.com', 'cookie2', 'value2'); + + // Clear entire jar + await jar.clear(); + + // Verify no cookies remain for any URL + const cookies1 = await jar.getCookies('https://site1.com'); + const cookies2 = await jar.getCookies('https://site2.com'); + + expect(cookies1).toHaveLength(0); + expect(cookies2).toHaveLength(0); + }); + }); + + describe('Error Handling', () => { + test('setCookie handles missing URL', async () => { + await expect(jar.setCookie('', 'name', 'value')).rejects.toThrow('URL is required'); + }); + + test('getCookie handles missing URL', async () => { + await expect(jar.getCookie('', 'name')).rejects.toThrow('URL and cookie name are required'); + }); + + test('setCookies handles invalid input', async () => { + await expect(jar.setCookies(testUrl, 'not-an-array')).rejects.toThrow('expects an array'); + }); + + test('setCookie handles missing cookie name in object', async () => { + await expect(jar.setCookie(testUrl, { value: 'test' })).rejects.toThrow('key (name) is required'); + }); + }); + + describe('Real-world Usage Examples', () => { + test('Authentication workflow example', async () => { + const apiUrl = 'https://api.example.com'; + const authToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; + + // Simulate login - set auth cookie + await jar.setCookie(apiUrl, 'authToken', authToken); + + // Later in the session - retrieve auth token + const cookie = await jar.getCookie(apiUrl, 'authToken'); + expect(cookie.value).toBe(authToken); + + // Simulate logout - remove auth cookie + await jar.deleteCookie(apiUrl, 'authToken'); + + // Verify cookie is gone + const deletedCookie = await jar.getCookie(apiUrl, 'authToken'); + expect(deletedCookie).toBeNull(); + }); + + test('Session management with multiple cookies', async () => { + const sessionUrl = 'https://app.example.com'; + + // Set session cookies + const sessionCookies = [ + { key: 'sessionId', value: 'sess_123', httpOnly: true }, + { key: 'csrfToken', value: 'csrf_456' }, + { key: 'userPrefs', value: JSON.stringify({ theme: 'dark', lang: 'en' }) } + ]; + + await jar.setCookies(sessionUrl, sessionCookies); + + // Retrieve all session cookies + const cookies = await jar.getCookies(sessionUrl); + expect(cookies).toHaveLength(3); + + // Find specific cookies + const sessionCookie = cookies.find(c => c.key === 'sessionId'); + const csrfCookie = cookies.find(c => c.key === 'csrfToken'); + const prefsCookie = cookies.find(c => c.key === 'userPrefs'); + + expect(sessionCookie.value).toBe('sess_123'); + expect(sessionCookie.httpOnly).toBe(true); + expect(csrfCookie.value).toBe('csrf_456'); + + const prefs = JSON.parse(prefsCookie.value); + expect(prefs.theme).toBe('dark'); + expect(prefs.lang).toBe('en'); + }); + + test('Cookie path handling', async () => { + const baseUrl = 'https://example.com'; + + // Set cookies with different paths + await jar.setCookie(baseUrl, { key: 'global', value: 'global_val', path: '/' }); + await jar.setCookie(baseUrl, { key: 'api', value: 'api_val', path: '/api' }); + await jar.setCookie(baseUrl, { key: 'admin', value: 'admin_val', path: '/admin' }); + + const rootCookies = await jar.getCookies(baseUrl + '/'); + const globalCookie = rootCookies.find(c => c.key === 'global'); + expect(globalCookie).toBeTruthy(); + expect(globalCookie.value).toBe('global_val'); + + const apiCookies = await jar.getCookies(baseUrl + '/api/users'); + expect(apiCookies.length).toBeGreaterThanOrEqual(2); + + const apiCookieNames = apiCookies.map(c => c.key); + expect(apiCookieNames).toContain('global'); + expect(apiCookieNames).toContain('api'); + }); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/src/postman/postman-translations.js b/packages/bruno-converters/src/postman/postman-translations.js index 9dc48a5cd..43c034db9 100644 --- a/packages/bruno-converters/src/postman/postman-translations.js +++ b/packages/bruno-converters/src/postman/postman-translations.js @@ -48,6 +48,13 @@ const replacements = { 'pm\\.execution\\.skipRequest': 'bru.runner.skipRequest', 'pm\\.execution\\.setNextRequest\\(null\\)': 'bru.runner.stopExecution()', 'pm\\.execution\\.setNextRequest\\(\'null\'\\)': 'bru.runner.stopExecution()', + // Cookie jar translations + 'pm\\.cookies\\.jar\\(\\)': 'bru.cookies.jar()', + 'pm\\.cookies\\.jar\\(\\)\\.get\\(': 'bru.cookies.jar().getCookie(', + 'pm\\.cookies\\.jar\\(\\)\\.set\\(': 'bru.cookies.jar().setCookie(', + 'pm\\.cookies\\.jar\\(\\)\\.unset\\(': 'bru.cookies.jar().deleteCookie(', + 'pm\\.cookies\\.jar\\(\\)\\.clear\\(': 'bru.cookies.jar().deleteCookies(', + 'pm\\.cookies\\.jar\\(\\)\\.getAll\\(': 'bru.cookies.jar().getCookies(', }; const extendedReplacements = Object.keys(replacements).reduce((acc, key) => { diff --git a/packages/bruno-converters/src/utils/jscode-shift-translator.js b/packages/bruno-converters/src/utils/jscode-shift-translator.js index 6b0cea683..405a8d540 100644 --- a/packages/bruno-converters/src/utils/jscode-shift-translator.js +++ b/packages/bruno-converters/src/utils/jscode-shift-translator.js @@ -14,6 +14,11 @@ function getMemberExpressionString(node) { return node.name; } + if (node.type === 'CallExpression') { + const calleeStr = getMemberExpressionString(node.callee); + return `${calleeStr}()`; + } + // Handle member expressions if (node.type === 'MemberExpression') { const objectStr = getMemberExpressionString(node.object); @@ -89,6 +94,13 @@ const simpleTranslations = { 'pm.response.size().body': 'res.getSize().body', 'pm.response.size().header': 'res.getSize().header', 'pm.response.size().total': 'res.getSize().total', + 'pm.cookies.jar': 'bru.cookies.jar', + + 'pm.cookies.jar().get': 'bru.cookies.jar().getCookie', + 'pm.cookies.jar().getAll': 'bru.cookies.jar().getCookies', + 'pm.cookies.jar().set': 'bru.cookies.jar().setCookie', + 'pm.cookies.jar().unset': 'bru.cookies.jar().deleteCookie', + 'pm.cookies.jar().clear': 'bru.cookies.jar().deleteCookies', // Execution control 'pm.execution.skipRequest': 'bru.runner.skipRequest', @@ -332,6 +344,9 @@ function translateCode(code) { // Preprocess the code to resolve all aliases preprocessAliases(ast); + // Handle cookie jar variable assignments and method renaming + processCookieJarVariables(ast); + // Process all transformations in a single pass processTransformations(ast, transformedNodes); @@ -610,6 +625,59 @@ function removeResolvedDeclarations(ast, symbolTable) { return changesMade; } +/** + * Process cookie jar variable assignments and rename methods on those variables + * @param {Object} ast - jscodeshift AST + */ +function processCookieJarVariables(ast) { + // Map of Postman cookie jar method names to Bruno equivalents + const cookieMethodMapping = { + 'get': 'getCookie', + 'getAll': 'getCookies', + 'set': 'setCookie', + 'unset': 'deleteCookie', + 'clear': 'deleteCookies' + }; + + // Track variables that are assigned to cookie jar instances + const cookieJarVariables = new Set(); + + // First pass: Find all variables assigned to cookie jar instances + ast.find(j.VariableDeclarator).forEach(path => { + if (path.value.init && path.value.init.type === 'CallExpression') { + const initCall = path.value.init; + + // Check if this is a cookie jar assignment + if (initCall.callee.type === 'MemberExpression') { + const calleeStr = getMemberExpressionString(initCall.callee); + + if (calleeStr === 'pm.cookies.jar' || calleeStr === 'bru.cookies.jar') { + if (path.value.id.type === 'Identifier') { + cookieJarVariables.add(path.value.id.name); + } + } + } + } + }); + + // Second pass: Rename method calls on cookie jar variables + ast.find(j.CallExpression).forEach(path => { + if (path.value.callee.type === 'MemberExpression' && + path.value.callee.object.type === 'Identifier' && + path.value.callee.property.type === 'Identifier') { + + const varName = path.value.callee.object.name; + const methodName = path.value.callee.property.name; + + // If this is a method call on a cookie jar variable + if (cookieJarVariables.has(varName) && cookieMethodMapping[methodName]) { + const newMethodName = cookieMethodMapping[methodName]; + path.value.callee.property.name = newMethodName; + } + } + }); +} + /** * Handle Postman's tests["..."] = ... syntax * @param {Object} ast - jscodeshift AST diff --git a/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js b/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js index fed9f2931..ba3f29d37 100644 --- a/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js +++ b/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js @@ -16,8 +16,8 @@ describe('postmanTranslations - comment handling', () => { }); test('should comment non-translated pm commands', () => { - const inputScript = "pm.test('random test', () => pm.cookies.get('cookieName'));"; - const expectedOutput = "// test('random test', () => pm.cookies.get('cookieName'));"; + const inputScript = "pm.test('random test', () => pm.globals.clear());"; + const expectedOutput = "// test('random test', () => pm.globals.clear());"; expect(postmanTranslation(inputScript)).toBe(expectedOutput); }); diff --git a/packages/bruno-converters/tests/postman/postman-translations/postman-cookie-conversions.spec.js b/packages/bruno-converters/tests/postman/postman-translations/postman-cookie-conversions.spec.js new file mode 100644 index 000000000..e4fe0ee32 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/postman-cookie-conversions.spec.js @@ -0,0 +1,319 @@ +const { default: postmanTranslation } = require("../../../src/postman/postman-translations"); + +describe('postmanTranslations - cookie API conversions', () => { + test('should convert pm.cookies.jar().get to bru.cookies.jar().getCookie', () => { + const inputScript = `pm.cookies.jar().get('https://example.com', 'sessionId', (err, cookie) => { + console.log(cookie); + });`; + + const expectedOutput = `bru.cookies.jar().getCookie('https://example.com', 'sessionId', (err, cookie) => { + console.log(cookie); + });`; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert pm.cookies.jar().getAll to bru.cookies.jar().getCookies', () => { + const inputScript = `pm.cookies.jar().getAll('https://example.com', (err, cookies) => { + console.log(cookies); + });`; + + const expectedOutput = `bru.cookies.jar().getCookies('https://example.com', (err, cookies) => { + console.log(cookies); + });`; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert pm.cookies.jar().set to bru.cookies.jar().setCookie', () => { + const inputScript = `pm.cookies.jar().set('https://example.com', 'sessionId', 'abc123', (err) => { + if (err) console.error(err); + });`; + + const expectedOutput = `bru.cookies.jar().setCookie('https://example.com', 'sessionId', 'abc123', (err) => { + if (err) console.error(err); + });`; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert pm.cookies.jar().unset to bru.cookies.jar().deleteCookie', () => { + const inputScript = `pm.cookies.jar().unset('https://example.com', 'sessionId', (err) => { + if (err) console.error(err); + });`; + + const expectedOutput = `bru.cookies.jar().deleteCookie('https://example.com', 'sessionId', (err) => { + if (err) console.error(err); + });`; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert pm.cookies.jar().clear to bru.cookies.jar().deleteCookies (behavior difference)', () => { + const inputScript = `pm.cookies.jar().clear('https://example.com', (err) => { + if (err) console.error(err); + });`; + + const expectedOutput = `bru.cookies.jar().deleteCookies('https://example.com', (err) => { + if (err) console.error(err); + });`; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should handle multiple cookie operations in one script', () => { + const inputScript = ` + pm.cookies.jar().set('https://api.example.com', 'auth', 'token123'); + const cookie = pm.cookies.jar().get('https://api.example.com', 'auth'); + pm.cookies.jar().getAll('https://api.example.com', (err, cookies) => { + console.log('All cookies:', cookies); + }); + pm.cookies.jar().unset('https://api.example.com', 'temp'); + pm.cookies.jar().clear('https://api.example.com'); + `; + + const expectedOutput = ` + bru.cookies.jar().setCookie('https://api.example.com', 'auth', 'token123'); + const cookie = bru.cookies.jar().getCookie('https://api.example.com', 'auth'); + bru.cookies.jar().getCookies('https://api.example.com', (err, cookies) => { + console.log('All cookies:', cookies); + }); + bru.cookies.jar().deleteCookie('https://api.example.com', 'temp'); + bru.cookies.jar().deleteCookies('https://api.example.com'); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert variable assignment and method calls on cookie jar variables', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.set('https://example.com', 'user', 'john'); + const userCookie = jar.get('https://example.com', 'user'); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.setCookie('https://example.com', 'user', 'john'); + const userCookie = jar.getCookie('https://example.com', 'user'); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert jar.get to jar.getCookie with callback', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.get('https://api.example.com', 'authToken', (error, cookie) => { + if (error) { + console.error('Error getting cookie:', error); + } else { + console.log('Retrieved cookie:', cookie); + } + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.getCookie('https://api.example.com', 'authToken', (error, cookie) => { + if (error) { + console.error('Error getting cookie:', error); + } else { + console.log('Retrieved cookie:', cookie); + } + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert jar.getAll to jar.getCookies with callback', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.getAll('https://api.example.com', (error, cookies) => { + if (error) { + console.error('Error getting cookies:', error); + } else { + console.log('All cookies:', cookies); + } + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.getCookies('https://api.example.com', (error, cookies) => { + if (error) { + console.error('Error getting cookies:', error); + } else { + console.log('All cookies:', cookies); + } + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert jar.set to jar.setCookie with cookie object', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.set('https://api.example.com', { + key: 'sessionId', + value: 'abc123', + path: '/api', + httpOnly: true, + secure: true + }, (error) => { + if (error) console.error(error); + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.setCookie('https://api.example.com', { + key: 'sessionId', + value: 'abc123', + path: '/api', + httpOnly: true, + secure: true + }, (error) => { + if (error) console.error(error); + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert jar.unset to jar.deleteCookie', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.unset('https://api.example.com', 'tempCookie', (error) => { + if (error) { + console.error('Failed to delete cookie:', error); + } else { + console.log('Cookie deleted successfully'); + } + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.deleteCookie('https://api.example.com', 'tempCookie', (error) => { + if (error) { + console.error('Failed to delete cookie:', error); + } else { + console.log('Cookie deleted successfully'); + } + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert jar.clear to jar.deleteCookies', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.clear('https://api.example.com', (error) => { + if (error) { + console.error('Failed to clear cookies:', error); + } else { + console.log('All cookies cleared for domain'); + } + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.deleteCookies('https://api.example.com', (error) => { + if (error) { + console.error('Failed to clear cookies:', error); + } else { + console.log('All cookies cleared for domain'); + } + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should handle complex cookie workflow with jar variable', () => { + const inputScript = ` + const cookieJar = pm.cookies.jar(); + + // Set multiple cookies + cookieJar.set('https://example.com', 'auth', 'token123'); + cookieJar.set('https://example.com', { + key: 'preferences', + value: JSON.stringify({theme: 'dark'}), + path: '/' + }); + + // Get specific cookie + cookieJar.get('https://example.com', 'auth', (err, authCookie) => { + console.log('Auth cookie:', authCookie); + }); + + // Get all cookies + cookieJar.getAll('https://example.com', (err, allCookies) => { + console.log('Total cookies:', allCookies.length); + }); + + // Clean up + cookieJar.unset('https://example.com', 'temp'); + cookieJar.clear('https://example.com'); + `; + + const expectedOutput = ` + const cookieJar = bru.cookies.jar(); + + // Set multiple cookies + cookieJar.setCookie('https://example.com', 'auth', 'token123'); + cookieJar.setCookie('https://example.com', { + key: 'preferences', + value: JSON.stringify({theme: 'dark'}), + path: '/' + }); + + // Get specific cookie + cookieJar.getCookie('https://example.com', 'auth', (err, authCookie) => { + console.log('Auth cookie:', authCookie); + }); + + // Get all cookies + cookieJar.getCookies('https://example.com', (err, allCookies) => { + console.log('Total cookies:', allCookies.length); + }); + + // Clean up + cookieJar.deleteCookie('https://example.com', 'temp'); + cookieJar.deleteCookies('https://example.com'); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should handle mixed jar variable and direct calls', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.get('https://api.com', 'session'); + + pm.cookies.jar().set('https://other.com', 'temp', 'value'); + + jar.getAll('https://api.com', (err, cookies) => { + console.log(cookies); + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.getCookie('https://api.com', 'session'); + + bru.cookies.jar().setCookie('https://other.com', 'temp', 'value'); + + jar.getCookies('https://api.com', (err, cookies) => { + console.log(cookies); + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); +}); \ No newline at end of file diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index b398cc9bf..cdfd6bcb7 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -32,13 +32,13 @@ "@aws-sdk/credential-providers": "3.750.0", "@usebruno/common": "0.1.0", "@usebruno/converters": "^0.1.0", + "@usebruno/filestore": "^0.1.0", "@usebruno/js": "0.12.0", "@usebruno/lang": "0.12.0", "@usebruno/node-machine-id": "^2.0.0", + "@usebruno/requests": "^0.1.0", "@usebruno/schema": "0.7.0", "@usebruno/vm2": "^3.9.13", - "@usebruno/requests": "^0.1.0", - "@usebruno/filestore": "^0.1.0", "about-window": "^1.15.2", "aws4-axios": "^3.3.0", "axios": "^1.8.3", @@ -65,7 +65,6 @@ "nanoid": "3.3.8", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", - "tough-cookie": "^4.1.3", "uuid": "^9.0.0", "yup": "^0.32.11" }, diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 8e39e0d5c..6e12126b3 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -472,6 +472,9 @@ const registerNetworkIpc = (mainWindow) => { }); collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables; + + const domainsWithCookies = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies))); } // interpolate variables inside request @@ -584,6 +587,9 @@ const registerNetworkIpc = (mainWindow) => { }); collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables; + + const domainsWithCookiesPost = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPost))); } return scriptResult; }; @@ -891,6 +897,9 @@ const registerNetworkIpc = (mainWindow) => { scriptType: 'test', error: testError }); + + const domainsWithCookiesTest = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest))); } return { @@ -1094,6 +1103,9 @@ const registerNetworkIpc = (mainWindow) => { error: preRequestError }); + const domainsWithCookiesPreRequest = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPreRequest))); + if (preRequestError) { throw preRequestError; } @@ -1282,6 +1294,9 @@ const registerNetworkIpc = (mainWindow) => { error: postResponseError }); + const domainsWithCookiesPostResponse = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPostResponse))); + if (postResponseScriptResult?.nextRequestName !== undefined) { nextRequestName = postResponseScriptResult.nextRequestName; } @@ -1386,6 +1401,9 @@ const registerNetworkIpc = (mainWindow) => { scriptType: 'test', error: testError }); + + const domainsWithCookiesTest = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest))); } } catch (error) { mainWindow.webContents.send('main:run-folder-event', { diff --git a/packages/bruno-electron/src/utils/cookies.js b/packages/bruno-electron/src/utils/cookies.js index 7f3751eaf..f4aaef547 100644 --- a/packages/bruno-electron/src/utils/cookies.js +++ b/packages/bruno-electron/src/utils/cookies.js @@ -1,197 +1 @@ -const { Cookie, CookieJar } = require('tough-cookie'); -const each = require('lodash/each'); -const moment = require('moment'); -const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils; - -const cookieJar = new CookieJar(); - -const addCookieToJar = (setCookieHeader, requestUrl) => { - const cookie = Cookie.parse(setCookieHeader, { loose: true }); - cookieJar.setCookieSync(cookie, requestUrl, { - ignoreError: true // silently ignore things like parse errors and invalid domains - }); -}; - -const getCookiesForUrl = (url) => { - return cookieJar.getCookiesSync(url, { - secure: isPotentiallyTrustworthyOrigin(url) - }); -}; - -const getCookieStringForUrl = (url) => { - const cookies = getCookiesForUrl(url); - - if (!Array.isArray(cookies) || !cookies.length) { - return ''; - } - - const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now()); - - return validCookies.map((cookie) => cookie.cookieString()).join('; '); -}; - -const getDomainsWithCookies = () => { - return new Promise((resolve, reject) => { - const domainCookieMap = {}; - - cookieJar.store.getAllCookies((err, cookies) => { - if (err) { - return reject(err); - } - - cookies.forEach((cookie) => { - if (!domainCookieMap[cookie.domain]) { - domainCookieMap[cookie.domain] = [cookie]; - } else { - domainCookieMap[cookie.domain].push(cookie); - } - }); - - const domains = Object.keys(domainCookieMap); - const domainsWithCookies = []; - - each(domains, (domain) => { - const cookies = domainCookieMap[domain]; - const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now()); - - if (validCookies.length) { - domainsWithCookies.push({ - domain, - cookies: validCookies, - cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ') - }); - } - }); - - resolve(domainsWithCookies); - }); - }); -}; - -const deleteCookie = (domain, path, cookieKey) => { - return new Promise((resolve, reject) => { - cookieJar.store.removeCookie(domain, path, cookieKey, (err) => { - if (err) { - return reject(err); - } - return resolve(); - }); - }); -}; - -const deleteCookiesForDomain = (domain) => { - return new Promise((resolve, reject) => { - cookieJar.store.removeCookies(domain, null, (err) => { - if (err) { - return reject(err); - } - return resolve(); - }); - }); -}; - -const updateCookieObj = (cookieObj, oldCookie) => { - return { - ...cookieObj, - // Preserve immutable properties from old cookie - path: oldCookie.path, - key: oldCookie.key, - domain: oldCookie.domain, - // Handle other mutable properties - expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity, - creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(), - lastAccessed: - oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid() - ? new Date(oldCookie.lastAccessed) - : new Date() - }; -}; - -const createCookieObj = (cookieObj) => { - return { - ...cookieObj, - path: cookieObj.path || '/', - expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity, - creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(), - lastAccessed: - cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid() - ? new Date(cookieObj.lastAccessed) - : new Date() - }; -}; - -const addCookieForDomain = (domain, cookieObj) => { - return new Promise((resolve, reject) => { - try { - const cookie = new Cookie(createCookieObj(cookieObj)); - cookieJar.store.putCookie(cookie, (err) => { - if (err) { - return reject(err); - } - return resolve(); - }); - } catch (err) { - reject(err); - } - }); -}; - -const modifyCookieForDomain = (domain, oldCookieObj, cookieObj) => { - return new Promise((resolve, reject) => { - try { - const oldCookie = new Cookie(createCookieObj(oldCookieObj)); - const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie)); - cookieJar.store.updateCookie(oldCookie, newCookie, (removeErr) => { - if (removeErr) { - return reject(removeErr); - } - return resolve(); - }); - } catch (err) { - reject(err); - } - }); -}; - -const parseCookieString = (cookieStr) => { - try { - const cookie = Cookie.parse(cookieStr); - if (!cookie) return null; - - return { - ...cookie, - expires: cookie.expires === Infinity ? null : cookie.expires - }; - } catch (err) { - throw new Error(err); - } -}; - -const createCookieString = (cookieObj) => { - const cookie = new Cookie(createCookieObj(cookieObj)); - - // cookie.toString() omits the domain - let cookieString = cookie.toString(); - - // Manually append domain and hostOnly if they exist - if (cookieObj.hostOnly && !cookieString.includes('Domain=')) { - cookieString += `; Domain=${cookieObj.domain}`; - } - - return cookieString; -}; - -module.exports = { - addCookieToJar, - getCookiesForUrl, - getCookieStringForUrl, - getDomainsWithCookies, - deleteCookie, - deleteCookiesForDomain, - addCookieForDomain, - modifyCookieForDomain, - parseCookieString, - createCookieString, - updateCookieObj, - createCookieObj -}; +module.exports = require('@usebruno/common').cookies; diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js index 5dad6935e..c00ac9b55 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -1,6 +1,7 @@ const { cloneDeep } = require('lodash'); const { interpolate: _interpolate } = require('@usebruno/common'); const { sendRequest } = require('@usebruno/requests').scripting; +const { jar: createCookieJar } = require('@usebruno/common').cookies; const variableNameRegex = /^[\w-.]*$/; @@ -17,6 +18,50 @@ class Bru { this.collectionPath = collectionPath; this.collectionName = collectionName; this.sendRequest = sendRequest; + + this.cookies = { + jar: () => { + const cookieJar = createCookieJar(); + + return { + getCookie: (url, cookieName, callback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.getCookie(interpolatedUrl, cookieName, callback); + }, + + getCookies: (url, callback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.getCookies(interpolatedUrl, callback); + }, + + setCookie: (url, nameOrCookieObj, valueOrCallback, maybeCallback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.setCookie(interpolatedUrl, nameOrCookieObj, valueOrCallback, maybeCallback); + }, + + setCookies: (url, cookiesArray, callback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.setCookies(interpolatedUrl, cookiesArray, callback); + }, + + // Clear entire cookie jar + clear: (callback) => { + return cookieJar.clear(callback); + }, + + // Delete cookies for a specific URL/domain + deleteCookies: (url, callback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.deleteCookies(interpolatedUrl, callback); + }, + + deleteCookie: (url, cookieName, callback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.deleteCookie(interpolatedUrl, cookieName, callback); + } + }; + } + }; this.runner = { skipRequest: () => { this.skipRequest = true; diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index 5be5e26d0..d99aec94b 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -258,6 +258,136 @@ const addBruShimToContext = (vm, bru) => { }); sleep.consume((handle) => vm.setProp(bruObject, 'sleep', handle)); + let bruCookiesObject = vm.newObject(); + + const _jarFn = vm.newFunction('_jar', () => { + const nativeJar = bru.cookies.jar(); + const jarObj = vm.newObject(); + + const _getCookieFn = vm.newFunction('_getCookie', (url, cookieName) => { + const promise = vm.newPromise(); + nativeJar.getCookie(vm.dump(url), vm.dump(cookieName), (err, cookie) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(marshallToVm(cleanCircularJson(cookie), vm)); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _getCookieFn.consume((handle) => vm.setProp(jarObj, '_getCookie', handle)); + + const _getCookiesFn = vm.newFunction('_getCookies', (url) => { + const promise = vm.newPromise(); + nativeJar.getCookies(vm.dump(url), (err, cookies) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(marshallToVm(cleanCircularJson(cookies), vm)); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _getCookiesFn.consume((handle) => vm.setProp(jarObj, '_getCookies', handle)); + + const _setCookieFn = vm.newFunction('_setCookie', (url, nameOrCookieObj, value) => { + const promise = vm.newPromise(); + const dumpedUrl = vm.dump(url); + const dumpedNameOrObj = vm.dump(nameOrCookieObj); + + // Check if the second argument is an object (cookie object case) + if (typeof dumpedNameOrObj === 'object' && dumpedNameOrObj !== null) { + // Cookie object case: setCookie(url, cookieObject, callback) + nativeJar.setCookie(dumpedUrl, dumpedNameOrObj, (err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + } else { + // Name/value case: setCookie(url, name, value, callback) + const dumpedValue = value ? vm.dump(value) : ''; + nativeJar.setCookie(dumpedUrl, dumpedNameOrObj, dumpedValue, (err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + } + + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _setCookieFn.consume((handle) => vm.setProp(jarObj, '_setCookie', handle)); + + const _setCookiesFn = vm.newFunction('_setCookies', (url, cookiesArray) => { + const promise = vm.newPromise(); + + nativeJar.setCookies(vm.dump(url), vm.dump(cookiesArray), (err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _setCookiesFn.consume((handle) => vm.setProp(jarObj, '_setCookies', handle)); + + const _clearFn = vm.newFunction('_clear', () => { + const promise = vm.newPromise(); + nativeJar.clear((err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _clearFn.consume((handle) => vm.setProp(jarObj, '_clear', handle)); + + const _deleteCookiesFn = vm.newFunction('_deleteCookies', (url) => { + const promise = vm.newPromise(); + nativeJar.deleteCookies(vm.dump(url), (err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _deleteCookiesFn.consume((handle) => vm.setProp(jarObj, '_deleteCookies', handle)); + + const _deleteCookieFn = vm.newFunction('_deleteCookie', (url, cookieName) => { + const promise = vm.newPromise(); + nativeJar.deleteCookie(vm.dump(url), vm.dump(cookieName), (err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _deleteCookieFn.consume((handle) => vm.setProp(jarObj, '_deleteCookie', handle)); + + return jarObj; + }); + _jarFn.consume((handle) => vm.setProp(bruCookiesObject, '_jar', handle)); + + vm.setProp(bruObject, 'cookies', bruCookiesObject); + bruCookiesObject.dispose(); + vm.setProp(bruObject, 'runner', bruRunnerObject); vm.setProp(vm.global, 'bru', bruObject); bruObject.dispose(); @@ -282,7 +412,41 @@ const addBruShimToContext = (vm, bru) => { return Promise.reject(err); } } - } + }; + + globalThis.bru.cookies.jar = () => { + const _jar = globalThis.bru.cookies._jar(); + + const callWithCallback = async (promiseFn, callback) => { + if (!callback) return await promiseFn(); + try { + const result = await promiseFn(); + try { await callback(null, result); } catch(cbErr) { return Promise.reject(cbErr); } + } catch(err) { + try { await callback(err, null); } catch(cbErr) { return Promise.reject(cbErr); } + } + }; + + return { + getCookie: (url, name, cb) => callWithCallback(() => _jar._getCookie(url, name), cb), + getCookies: (url, cb) => callWithCallback(() => _jar._getCookies(url), cb), + setCookie: (url, nameOrCookieObj, valueOrCallback, maybeCallback) => { + if (typeof nameOrCookieObj === 'object' && nameOrCookieObj !== null) { + const callback = typeof valueOrCallback === 'function' ? valueOrCallback : undefined; + return callWithCallback(() => _jar._setCookie(url, nameOrCookieObj), callback); + } else { + const value = typeof valueOrCallback === 'string' ? valueOrCallback : ''; + const callback = typeof maybeCallback === 'function' ? maybeCallback : + (typeof valueOrCallback === 'function' ? valueOrCallback : undefined); + return callWithCallback(() => _jar._setCookie(url, nameOrCookieObj, value), callback); + } + }, + setCookies: (url, cookiesArray, cb) => callWithCallback(() => _jar._setCookies(url, cookiesArray), cb), + clear: (cb) => callWithCallback(() => _jar._clear(), cb), + deleteCookies: (url, cb) => callWithCallback(() => _jar._deleteCookies(url), cb), + deleteCookie: (url, name, cb) => callWithCallback(() => _jar._deleteCookie(url, name), cb) + }; + }; `); }; diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts index 3bdef99a0..b56f22347 100644 --- a/packages/bruno-requests/src/index.ts +++ b/packages/bruno-requests/src/index.ts @@ -1,7 +1,5 @@ export { addDigestInterceptor, getOAuth2Token } from './auth'; -export * as utils from './utils'; - export * as network from './network'; export * as scripting from './scripting'; \ No newline at end of file diff --git a/packages/bruno-requests/src/utils/cookie-utils.js b/packages/bruno-requests/src/utils/cookie-utils.js deleted file mode 100644 index 6a1a5ac57..000000000 --- a/packages/bruno-requests/src/utils/cookie-utils.js +++ /dev/null @@ -1,105 +0,0 @@ -const { URL } = require('node:url'); -const net = require('node:net'); - -const isLoopbackV4 = (address) => { - // 127.0.0.0/8: first octet = 127 - const octets = address.split('.'); - return ( - octets.length === 4 - ) && parseInt(octets[0], 10) === 127; -} - -const isLoopbackV6 = (address) => { - // new URL(...) follows the WHATWG URL Standard - // which compresses IPv6 addresses, therefore the IPv6 - // loopback address will always be compressed to '[::1]': - // https://url.spec.whatwg.org/#concept-ipv6-serializer - return (address === '::1'); -} - -const isIpLoopback = (address) => { - if (net.isIPv4(address)) { - return isLoopbackV4(address); - } - - if (net.isIPv6(address)) { - return isLoopbackV6(address); - } - - return false; -} - -const isNormalizedLocalhostTLD = (host) => { - return host.toLowerCase().endsWith('.localhost'); -} - -const isLocalHostname = (host) => { - return host.toLowerCase() === 'localhost' || - isNormalizedLocalhostTLD(host); -} - -/** - * Removes leading and trailing square brackets if present. - * Adapted from https://github.com/chromium/chromium/blob/main/url/gurl.cc#L440-L448 - * - * @param {string} host - * @returns {string} - */ -const hostNoBrackets = (host) => { - if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) { - return host.substring(1, host.length - 1); - } - return host; -} - -/** - * Determines if a URL string represents a potentially trustworthy origin. - * - * A URL is considered potentially trustworthy if it: - * - Uses HTTPS, WSS or file schemes - * - Points to a loopback address (IPv4 127.0.0.0/8 or IPv6 ::1) - * - Uses localhost or *.localhost hostnames - * - * @param {string} urlString - The URL to check - * @returns {boolean} - * @see {@link https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin W3C Spec} - */ -const isPotentiallyTrustworthyOrigin = (urlString) => { - let url; - - // try ... catch doubles as an opaque origin check - try { - url = new URL(urlString); - } catch (e) { - if (e instanceof TypeError && e.code === 'ERR_INVALID_URL') { - return false; - } else throw e; - } - - const scheme = url.protocol.replace(':', '').toLowerCase(); - const hostname = hostNoBrackets( - url.hostname - ).replace(/\.+$/, ''); - - if ( - scheme === 'https' || - scheme === 'wss' || - scheme === 'file' // https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin - ) { - return true; - } - - // If it's already an IP literal, check if it's a loopback address - if (net.isIP(hostname)) { - return isIpLoopback(hostname); - } - - // RFC 6761 states that localhost names will always resolve - // to the respective IP loopback address: - // https://datatracker.ietf.org/doc/html/rfc6761#section-6.3 - return isLocalHostname(hostname); -} - -module.exports = { - isPotentiallyTrustworthyOrigin -}; \ No newline at end of file diff --git a/packages/bruno-requests/src/utils/index.ts b/packages/bruno-requests/src/utils/index.ts deleted file mode 100644 index dd94dd186..000000000 --- a/packages/bruno-requests/src/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './cookie-utils'; diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/clear.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/clear.bru new file mode 100644 index 000000000..2f0000b3d --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/clear.bru @@ -0,0 +1,67 @@ +meta { + name: clear + type: http + seq: 6 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar() + + await jar.setCookies('https://testbench-sanity.usebruno.com', [ + { + key: 'test_cookie_1', + value: 'value1', + path: '/', + secure: true + }, + { + key: 'test_cookie_2', + value: 'value2', + path: '/', + secure: true + } + ]); + + console.log("Test cookies set up for clear test"); +} + +script:post-response { + const jar = bru.cookies.jar() + + const cookiesBeforeClear = await jar.getCookies('https://testbench-sanity.usebruno.com'); + console.log(`Found ${cookiesBeforeClear.length} cookies before clearing`); + + test("cookies should exist before clearing", function() { + expect(cookiesBeforeClear).to.be.an('array'); + expect(cookiesBeforeClear.length).to.be.greaterThan(0); + }); + + await jar.clear(); + console.log("Cookie jar cleared"); +} + +tests { + const jar = bru.cookies.jar() + + test("should have no cookies after clearing", async function() { + const cookiesAfterClear = await jar.getCookies('https://testbench-sanity.usebruno.com'); + expect(cookiesAfterClear).to.be.an('array'); + expect(cookiesAfterClear.length).to.equal(0); + }); + + jar.clear(function(error) { + test("should successfully clear with callback", function() { + expect(error).to.be.null; + }); + }); +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookie.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookie.bru new file mode 100644 index 000000000..d1d1da1c2 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookie.bru @@ -0,0 +1,75 @@ +meta { + name: deleteCookie + type: http + seq: 5 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar() + + await jar.setCookies('https://testbench-sanity.usebruno.com', [ + { + key: 'cookie_to_delete', + value: 'will_be_deleted', + path: '/', + secure: true + }, + { + key: 'cookie_to_keep', + value: 'should_remain', + path: '/', + secure: true + } + ]); + + console.log("Test cookies set up"); +} + +script:post-response { + const jar = bru.cookies.jar() + + const cookiesBefore = await jar.getCookies('https://testbench-sanity.usebruno.com'); + console.log(`Found ${cookiesBefore.length} cookies before deletion`); + + const targetCookie = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete'); + test("cookie should exist before deletion", function() { + expect(targetCookie).to.not.be.null; + expect(targetCookie.key).to.equal('cookie_to_delete'); + }); + + await jar.deleteCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete'); + console.log("Cookie deleted"); +} + +tests { + const jar = bru.cookies.jar() + + test("should have deleted the target cookie", async function() { + const deletedCookie = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete'); + expect(deletedCookie).to.be.null; + }); + + test("should keep other cookies intact", async function() { + const cookieToKeep = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_keep'); + expect(cookieToKeep).to.not.be.null; + expect(cookieToKeep.key).to.equal('cookie_to_keep'); + }); + + jar.deleteCookie("https://testbench-sanity.usebruno.com", "cookie_to_keep", function(error) { + test("should successfully delete with callback", function() { + expect(error).to.be.null; + }); + }); + + jar.clear() +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookies.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookies.bru new file mode 100644 index 000000000..03e604e8c --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookies.bru @@ -0,0 +1,106 @@ +meta { + name: deleteCookies + type: http + seq: 7 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar() + + // Set up test cookies before the request + try { + await jar.setCookies('https://testbench-sanity.usebruno.com', [ + { + key: 'test_cookie_1', + value: 'value1', + path: '/', + httpOnly: false, + secure: true + }, + { + key: 'test_cookie_2', + value: 'value2', + path: '/', + httpOnly: true, + secure: true + }, + { + key: 'test_cookie_3', + value: 'value3', + path: '/api', + httpOnly: false, + secure: true + } + ]); + + console.log("Test cookies set up successfully in pre-request script"); + + // Verify cookies were set + const cookies = await jar.getCookies('https://testbench-sanity.usebruno.com'); + console.log(`${cookies.length} cookies set for domain`); + + } catch (error) { + console.error("Failed to set up test cookies:", error); + throw new Error(`Pre-request cookie setup failed: ${error.message || error}`); + } +} + +script:post-response { + const jar = bru.cookies.jar() + + // Verify cookies exist before deletion + try { + const cookiesBeforeDeletion = await jar.getCookies('https://testbench-sanity.usebruno.com'); + + test("cookies should exist before clearing", function() { + expect(cookiesBeforeDeletion).to.be.an('array'); + expect(cookiesBeforeDeletion.length).to.be.greaterThan(0); + }); + + + if (cookiesBeforeDeletion.length === 0) { + throw new Error("No cookies found to delete - setup may have failed"); + } + + // Delete all cookies for the domain + await jar.deleteCookies('https://testbench-sanity.usebruno.com'); + console.log("deleteCookies operation completed in post-response"); + + // Verify deletion worked + const cookiesAfterDeletion = await jar.getCookies('https://testbench-sanity.usebruno.com'); + console.log(`Found ${cookiesAfterDeletion.length} cookies after deletion`); + + } catch (error) { + console.error("Delete cookies error in post-response:", error); + throw new Error(`Failed to delete cookies in post-response: ${error.message || error}`); + } +} + +tests { + const jar = bru.cookies.jar() + + jar.getCookies("https://testbench-sanity.usebruno.com", function(error, remainingCookies) { + if(error) { + console.error("Error checking remaining cookies:", error) + throw new Error(`Failed to get remaining cookies: ${error.message || error}`) + } + + test("should have no cookies remaining after deletion", function() { + expect(remainingCookies).to.be.an('array'); + expect(remainingCookies.length).to.equal(0); + console.log("✓ Confirmed: no cookies remain for domain after deleteCookies"); + }); + }); + + jar.clear() +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/folder.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/folder.bru new file mode 100644 index 000000000..1ae98288d --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/folder.bru @@ -0,0 +1,8 @@ +meta { + name: cookies + seq: 17 +} + +auth { + mode: inherit +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookie.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookie.bru new file mode 100644 index 000000000..729592345 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookie.bru @@ -0,0 +1,38 @@ +meta { + name: getCookie + type: http + seq: 1 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +tests { + const jar = bru.cookies.jar() + + jar.getCookie("https://testbench-sanity.usebruno.com", "__cf_bm", function(error, data) { + if(error) { + console.error("Cookie retrieval error:", error) + throw new Error(`Failed to get cookie: ${error.message || error}`) + } + + test("should successfully retrieve cookie data", function() { + expect(data).to.have.property('key'); + expect(data).to.have.property('value'); + expect(data.key).to.equal("__cf_bm"); + expect(data.value).to.be.a('string'); + expect(data.value).to.not.be.empty; + expect(data.domain).to.include('usebruno.com'); + console.log("Retrieved cookie:", data); + }); + }) + + jar.clear() +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookies.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookies.bru new file mode 100644 index 000000000..7c09371c7 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookies.bru @@ -0,0 +1,52 @@ +meta { + name: getCookies + type: http + seq: 3 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +tests { + const jar = bru.cookies.jar() + + jar.getCookies("https://testbench-sanity.usebruno.com", function(error, data) { + if(error) { + console.error("Cookies retrieval error:", error) + throw new Error(`Failed to get cookies: ${error.message || error}`) + } + + test("should successfully retrieve cookies array", function() { + expect(error).to.be.null; + expect(data).to.not.be.null; + expect(data).to.be.an('array'); + console.log("Retrieved cookies count:", data.length); + }); + + test("should have valid cookie structure in array", function() { + data.forEach((cookie, index) => { + expect(cookie).to.have.property('key'); + expect(cookie).to.have.property('value'); + expect(cookie.key).to.be.a('string'); + expect(cookie.value).to.be.a('string'); + expect(cookie.domain).to.include('usebruno.com'); + console.log(`Cookie ${index + 1}:`, cookie); + }); + }); + + test("should contain expected cookie properties", function() { + const cookieKeys = data.map(cookie => cookie.key); + expect(cookieKeys).to.be.an('array'); + console.log("Found cookie keys:", cookieKeys); + }); + }) + + jar.clear() +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookie.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookie.bru new file mode 100644 index 000000000..5449a248a --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookie.bru @@ -0,0 +1,69 @@ +meta { + name: setCookie + type: http + seq: 2 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar() + + // Set cookie before the request + try { + await jar.setCookie("https://testbench-sanity.usebruno.com", { + key: "auth", + value: "1234", + path: "/path" + }); + + console.log("Cookie set successfully in pre-request script"); + + } catch (error) { + console.error("Cookie setting error in pre-request:", error); + throw new Error(`Pre-request setCookie failed: ${error.message || error}`); + } +} + +tests { + const jar = bru.cookies.jar() + + test("should have set cookie successfully", function() { + console.log("Verifying cookie set in pre-request script"); + }); + + // Test: Verify the cookie was set by retrieving it + const cookieData = await jar.getCookie("https://testbench-sanity.usebruno.com/path", "auth"); + + test("should retrieve the set cookie with correct properties", function() { + expect(cookieData.key).to.equal("auth"); + expect(cookieData.value).to.equal("1234"); + expect(cookieData.path).to.equal("/path"); + expect(cookieData.domain).to.include('usebruno.com'); + console.log("Retrieved and verified cookie:", cookieData); + }); + + // Test: Additional verification - check all cookies for the domain + const allCookies = await jar.getCookies("https://testbench-sanity.usebruno.com/path"); + + test("should find the cookie in domain cookie list", function() { + expect(allCookies).to.be.an('array'); + expect(allCookies.length).to.be.at.least(1); + + const authCookie = allCookies.find(c => c.key === 'auth'); + expect(authCookie).to.not.be.undefined; + expect(authCookie.value).to.equal("1234"); + + console.log("All cookies for domain:", allCookies.map(c => ({ key: c.key, value: c.value, path: c.path }))); + }); + + jar.clear() +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookieHeader.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookieHeader.bru new file mode 100644 index 000000000..c388917ba --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookieHeader.bru @@ -0,0 +1,40 @@ +meta { + name: setCookie header inclusion + type: http + seq: 6 +} + +post { + url: {{echo-host}} + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar(); + + // Set a cookie that should be sent with the upcoming request + await jar.setCookie('https://echo.usebruno.com', { + key: 'auth', + value: 'token123', + path: '/', + secure: false + }); +} + +tests { + const cookieHeader = res.getHeader('cookie'); + + test('should attach auth cookie in request headers', function () { + expect(cookieHeader).to.be.a('string'); + expect(cookieHeader).to.include('auth=token123'); + }); + + // Clean up the jar so other tests are not affected + const jar = bru.cookies.jar(); + await jar.clear(); +} + +settings { + encodeUrl: false +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookies.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookies.bru new file mode 100644 index 000000000..87cefde76 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookies.bru @@ -0,0 +1,85 @@ +meta { + name: setCookies + type: http + seq: 4 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar() + + // Set multiple cookies before the request + try { + await jar.setCookies('https://example.com', [ + { + key: 'auth', + value: 'abc123', + path: '/path', + httpOnly: true, + secure: true, + expires: new Date(Date.now() + 24 * 60 * 60 * 1000) + }, + { + key: 'session', + value: 'xyz789', + path: '/foo', + httpOnly: true, + secure: true, + } + ]); + + console.log("Multiple cookies set successfully in pre-request script"); + + } catch (error) { + console.error("setCookies operation failed in pre-request:", error); + throw new Error(`Pre-request setCookies failed: ${error.message || error}`); + } +} + +tests { + const jar = bru.cookies.jar() + + test("should have set multiple cookies successfully", function() { + console.log("Verifying cookies set in pre-request script"); + }); + + // Test: Verify first cookie was set correctly + const authCookie = await jar.getCookie('https://example.com/path', 'auth'); + + test("should retrieve first cookie with correct properties", function() { + expect(authCookie.key).to.equal("auth"); + expect(authCookie.value).to.equal("abc123"); + expect(authCookie.path).to.equal("/path"); + expect(authCookie.httpOnly).to.be.true; + expect(authCookie.secure).to.be.true; + expect(authCookie.domain).to.include('example.com'); + console.log("Auth cookie verified:", authCookie); + }); + + // Test: Verify second cookie was set correctly + const sessionCookie = await jar.getCookie('https://example.com/foo', 'session'); + + test("should retrieve second cookie with correct properties", function() { + expect(sessionCookie).to.not.be.null; + if (sessionCookie) { + expect(sessionCookie.key).to.equal("session"); + expect(sessionCookie.value).to.equal("xyz789"); + expect(sessionCookie.path).to.equal("/foo"); + expect(sessionCookie.httpOnly).to.be.true; + expect(sessionCookie.secure).to.be.true; + expect(sessionCookie.domain).to.include('example.com'); + console.log("Session cookie verified:", sessionCookie); + } + }); + + jar.clear() +} + +settings { + encodeUrl: true +}