feat: adding cookie apis (#5117)

This commit is contained in:
Pooja
2025-07-30 19:35:54 +05:30
committed by GitHub
parent 60a0a32743
commit 31027cb2e0
34 changed files with 2125 additions and 427 deletions

138
package-lock.json generated
View File

@@ -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"
},

View File

@@ -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)',
]
};

View File

@@ -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"
}

View File

@@ -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})(:?\/\/|:)/;

View File

@@ -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;

View File

@@ -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'
};

View File

@@ -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",

View File

@@ -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<Array<{ domain: string; cookies: Cookie[]; cookieString: string }>> => {
return new Promise((resolve, reject) => {
const domainCookieMap: Record<string, Cookie[]> = {};
(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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<Cookie | null>((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<Cookie[]>((resolve, reject) => {
cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
if (err) return reject(err);
resolve(cookies);
});
});
},
setCookie: function (
url: string,
nameOrCookieObj: string | Record<string, any>,
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<void>((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<void>((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<void>((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<void>((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<void>((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;

View File

@@ -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';

View File

@@ -1,5 +1,9 @@
export {
encodeUrl,
parseQueryParams,
buildQueryString
buildQueryString,
} from './url';
export {
isPotentiallyTrustworthyOrigin
} from './url/validation';

View File

@@ -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);
});
});
});
});

View File

@@ -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 };

View File

@@ -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');
});
});
});

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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);
});

View File

@@ -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);
});
});

View File

@@ -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"
},

View File

@@ -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', {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)
};
};
`);
};

View File

@@ -1,7 +1,5 @@
export { addDigestInterceptor, getOAuth2Token } from './auth';
export * as utils from './utils';
export * as network from './network';
export * as scripting from './scripting';

View File

@@ -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
};

View File

@@ -1 +0,0 @@
export * from './cookie-utils';

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,8 @@
meta {
name: cookies
seq: 17
}
auth {
mode: inherit
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}