Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
91e8b44
Pre-resolve secrets within runtime using secret resolvers
VShingala Jan 16, 2026
c4f12d0
Added secretResolver interface for secret resolution
VShingala Feb 3, 2026
be2e575
Update dependencies
appurva21 Feb 4, 2026
5d42846
Add changelog
appurva21 Feb 4, 2026
825fa9a
7.52.0-beta.1
appurva21 Feb 4, 2026
adbdcf8
Use proper schema for secrets and renamed `type` to `source`
VShingala Feb 13, 2026
342b8e4
Merge branch 'develop' of github.com:postmanlabs/postman-runtime into…
VShingala Feb 13, 2026
45f8eb3
7.52.0-beta.2
appurva21 Feb 15, 2026
af4420a
update postman-collection version to temp branch
VShingala Feb 20, 2026
62aed74
added secret support in script via safe flag and changed the secret r…
VShingala Feb 20, 2026
4c12ace
add options.secretResolver documentation
VShingala Feb 20, 2026
61b024d
added integrtion tests for safe secret access within test scripts
VShingala Feb 20, 2026
2cb6adf
revert back the original package.json version
VShingala Feb 20, 2026
5265a09
version bump
appurva21 Feb 21, 2026
2ff37bd
7.52.0-beta.3
appurva21 Feb 21, 2026
719f75d
chore: update dependencies
appurva21 Feb 23, 2026
09d98c9
7.52.0-beta.4
appurva21 Feb 23, 2026
7f03817
Update dependencies
appurva21 Feb 25, 2026
971af12
rename "safe" to "allowedInScript" and only provide url as second arg…
VShingala Feb 26, 2026
513f9f0
added support for resolving secrets based on pre-request script mutat…
VShingala Feb 27, 2026
42ed77e
fixed test failure due to mutations change in pm.environment.set apis
VShingala Feb 27, 2026
e014f24
chore: reverted package version bump
appurva21 Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
unreleased:
new features:
- Add support for secret variable resolution

7.51.1:
date: 2026-01-27
chores:
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ runner.run(collection, {
// Local variables (a "VariableScope" from the SDK)
localVariables: new sdk.VariableScope(),

// Function to resolve secret variables (variables with secret: true) before request execution.
// Receives { secrets, url }; url is request URL without query params. Callback(err, result).
// On fatal error: callback(err) — execution stops. On success: callback(null, result) where result
// is Array<{ resolvedValue?, error?, allowedInScript? }>; result[i] maps to secrets[i].
// allowedInScript: true exposes value to scripts; false/undefined masks it from pm.environment/pm.variables.
secretResolver: function ({ secrets, url }, callback) {
callback(null, secrets.map(function (s) {
return { resolvedValue: 'resolved-string', error: undefined, allowedInScript: true };
}));
},

// Execute a folder/request using id/name or path
entrypoint: {
// execute a folder/request using id or name
Expand Down
49 changes: 47 additions & 2 deletions lib/runner/extensions/event.command.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,46 @@ var _ = require('lodash'),

getCookieDomain, // fn
postProcessContext, // fn
sanitizeFiles; // fn
sanitizeFiles, // fn
maskForbiddenSecrets; // fn

/**
* Clones variable scopes and masks variables in forbiddenSecretKeys (Set of "scopeName:variableKey").
* Used to hide secrets not allowed in scripts while keeping them available for request substitution.
*
* @param {Object} context - Context with environment, globals, collectionVariables
* @param {Set} forbiddenSecretKeys - Set of "scopeName:variableKey" for secrets with allowedInScript=false
* @returns {Object} - Context with cloned, masked scopes
*/
maskForbiddenSecrets = function (context, forbiddenSecretKeys) {
if (!forbiddenSecretKeys || forbiddenSecretKeys.size === 0) {
return context;
}

var scopeNames = ['environment', 'globals', 'collectionVariables'],
maskedContext = _.assign({}, context);

scopeNames.forEach(function (scopeName) {
var scope = context[scopeName],
maskedScope;

if (!scope || !sdk.VariableScope.isVariableScope(scope)) {
return;
}

maskedScope = new sdk.VariableScope(scope.toJSON ? scope.toJSON() : scope);

maskedScope.values.each(function (variable) {
if (variable && forbiddenSecretKeys.has(scopeName + ':' + variable.key)) {
variable.set(undefined);
}
});

maskedContext[scopeName] = maskedScope;
});

return maskedContext;
};

postProcessContext = function (execution, failures) { // function determines whether the event needs to abort
var error;
Expand Down Expand Up @@ -514,6 +553,12 @@ module.exports = {
const currentEventItem = event.parent && event.parent(),

executeScript = ({ resolvedPackages }, done) => {
var contextToUse = _.pick(payload.context, SAFE_CONTEXT_VARIABLES);

if (payload.context._forbiddenSecretKeys) {
contextToUse = maskForbiddenSecrets(contextToUse, payload.context._forbiddenSecretKeys);
}

// finally execute the script
this.host.execute(event, {
id: executionId,
Expand All @@ -522,7 +567,7 @@ module.exports = {
// @todo: Expose this as a property in Collection SDK's Script
timeout: payload.scriptTimeout,
cursor: scriptCursor,
context: _.pick(payload.context, SAFE_CONTEXT_VARIABLES),
context: contextToUse,
resolvedPackages: resolvedPackages,

disabledAPIs: !_.get(this, 'options.script.requestResolver') ?
Expand Down
394 changes: 256 additions & 138 deletions lib/runner/extensions/item.command.js

Large diffs are not rendered by default.

14 changes: 13 additions & 1 deletion lib/runner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ _.assign(Runner.prototype, {
* @param {String} [options.entrypoint.lookupStrategy=idOrName] strategy to lookup the entrypoint [idOrName, path]
* @param {Array<String>} [options.entrypoint.path] path to lookup
* @param {Object} [options.run] Run-specific options, such as options related to the host
* @param {Function} [options.secretResolver] - Function({ secrets, url }, callback) that resolves secrets.
* Receives: secrets (array of { scopeName, scope, variable, context }), url (request URL without query).
* Callback is (err, result).
* On fatal error: callback(err) — request execution stops.
* On success: callback(null, result) where result is Array<{ resolvedValue?: string,
* error?: Error, allowedInScript?: boolean }>;
* result[i] corresponds to secrets[i].
* resolvedValue: resolved string (undefined if failed/skipped).
* error: Error when resolution failed for particular secret.
* allowedInScript: if true, value is exposed to scripts via pm.environment/pm.variables;
* if false/undefined, masked from scripts. Runtime applies values.
*
* @param {Function} callback -
*/
Expand Down Expand Up @@ -147,7 +158,8 @@ _.assign(Runner.prototype, {
collectionVariables: collection.variables,
localVariables: options.localVariables,
certificates: options.certificates,
proxies: options.proxies
proxies: options.proxies,
secretResolver: options.secretResolver
}, runOptions)));
});
}
Expand Down
130 changes: 130 additions & 0 deletions lib/runner/resolve-secrets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
var _ = require('lodash'),
{ resolveUrlString } = require('./util');

/**
* Extract all variables with secret === true from a variable scope.
*
* @param {VariableScope|Object} scope - scope
* @returns {Array} - variables
*/
function extractSecretVariables (scope) {
if (!scope) {
return [];
}

var values = _.get(scope, 'values'),
secretVariables = [];

if (!values || typeof values.each !== 'function') {
return [];
}

values.each(function (variable) {
if (variable && variable.secret === true) {
secretVariables.push(variable);
}
});

return secretVariables;
}

/**
* Build context object for resolver function
*
* @param {Object} payload - Request payload
* @param {String} variableKey - The key of the variable being resolved
* @returns {Object} - Context object
*/
function buildResolverContext (payload, variableKey) {
return {
item: payload.item,
variableKey: variableKey
};
}

/**
* Scans variable scopes for secrets and invokes the secretResolver with them.
* Consumer returns array of { resolvedValue, error, allowedInScript }; runtime applies values.
*
* @param {Object} payload - Request payload
* @param {Object} payload.item - The request item
* @param {Object} payload.environment - Environment scope
* @param {Object} payload.globals - Globals scope
* @param {Object} payload.collectionVariables - Collection variables scope
* @param {Object} payload.vaultSecrets - Vault secrets scope
* @param {Object} payload._variables - Local variables scope
* @param {Object} payload.data - Iteration data
* @param {Function} secretResolver - Function({ secrets, urlString }, callback) that resolves secrets.
* Callback is (err, result) where result is Array<{ resolvedValue?: string, error?: Error,
* allowedInScript?: boolean }>.
* @param {Function} callback - callback(err, secretResolutionErrors, forbiddenSecretKeys)
*/
function resolveSecrets (payload, secretResolver, callback) {
if (!secretResolver || typeof secretResolver !== 'function') {
return callback();
}

var scopesToScan = [
{ name: 'environment', scope: payload.environment },
{ name: 'globals', scope: payload.globals },
{ name: 'collectionVariables', scope: payload.collectionVariables }
],
secrets = [],
url = resolveUrlString(payload.item, payload),
urlString = typeof url.toString === 'function' ? url.toString() : String(url);

scopesToScan.forEach(function (scopeInfo) {
var secretVars = extractSecretVariables(scopeInfo.scope);

secretVars.forEach(function (variable) {
secrets.push({
scopeName: scopeInfo.name,
scope: scopeInfo.scope,
variable: variable,
context: buildResolverContext(payload, variable.key)
});
});
});

if (secrets.length === 0) {
return callback();
}

secretResolver({ secrets, urlString }, function (err, result) {
if (err) {
return callback(err);
}

if (result && Array.isArray(result)) {
result.forEach(function (entry, i) {
var hasResolvedValue = entry && entry.resolvedValue !== undefined &&
typeof entry.resolvedValue === 'string';

if (i < secrets.length && hasResolvedValue) {
secrets[i].variable.set(entry.resolvedValue);
}
});
}

var secretResolutionErrors = (result && Array.isArray(result)) ?
result.filter(function (entry) { return entry && entry.error; })
.map(function (entry) { return entry.error; }) :
[],
forbiddenSecretKeys = new Set();

if (result && Array.isArray(result)) {
result.forEach(function (entry, i) {
if (i < secrets.length && entry && entry.allowedInScript === false) {
forbiddenSecretKeys.add(secrets[i].scopeName + ':' + secrets[i].variable.key);
}
});
}

callback(null, secretResolutionErrors, forbiddenSecretKeys);
});
}

module.exports = {
resolveSecrets,
extractSecretVariables
};
Loading
Loading