Compose-ready workspace: packages/zappier (rate card, portal, Stripe), packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier (CLI app), vendor/zapier-platform, and research/zapier vendor corpus. Gate 0 structure checks pass. Product code and research are not yet wired.
331 lines
9.2 KiB
JavaScript
331 lines
9.2 KiB
JavaScript
const querystring = require('querystring');
|
|
|
|
const _ = require('lodash');
|
|
|
|
const isPrimitive = (x) => {
|
|
const tipe = typeof x;
|
|
return (
|
|
tipe === 'string' || tipe === 'number' || tipe === 'boolean' || tipe == null
|
|
);
|
|
};
|
|
|
|
const renderTemplate = (templateString, context) => {
|
|
// Security: Ensure templateString is a string and not user-controlled
|
|
if (!isPrimitive(templateString)) {
|
|
throw new Error('Template string must be a primitive');
|
|
}
|
|
|
|
const re = /{{([^}]+)}}/g;
|
|
|
|
// _.template doesn't allow us to set defaults, so we need to make sure all the
|
|
// variables in templateString are defined (as empty strings) in context
|
|
const defaults = _.clone(context);
|
|
let match = re.exec(templateString);
|
|
while (match) {
|
|
const key = match[1].trim();
|
|
defaults[key] = '';
|
|
match = re.exec(templateString);
|
|
}
|
|
const finalContext = _.defaults(_.clone(context), defaults);
|
|
|
|
const options = {
|
|
interpolate: re,
|
|
// Security: Disable code evaluation to prevent injection
|
|
evaluate: false,
|
|
escape: false,
|
|
};
|
|
|
|
// Security: Use safe template compilation
|
|
try {
|
|
return _.template(templateString, options)(finalContext);
|
|
} catch (error) {
|
|
// Log template errors but don't expose internal details
|
|
console.error('Template rendering error:', error.message);
|
|
return templateString; // Return original string on error
|
|
}
|
|
};
|
|
|
|
const getLowerHeaders = (headers) =>
|
|
Object.entries(headers).reduce((result, [k, v]) => {
|
|
result[k.toLowerCase()] = v;
|
|
return result;
|
|
}, {});
|
|
|
|
const renderAuthMapping = (authMapping, authData) => {
|
|
if (_.isEmpty(authMapping)) {
|
|
return authData;
|
|
}
|
|
return Object.entries(authMapping).reduce((result, [k, v]) => {
|
|
result[k] = renderTemplate(v, authData);
|
|
return result;
|
|
}, {});
|
|
};
|
|
|
|
const applyAuthMappingInHeaders = (authMapping, req, authData) => {
|
|
const rendered = renderAuthMapping(authMapping, authData);
|
|
const lowerHeaders = getLowerHeaders(req.headers);
|
|
Object.entries(rendered).forEach(([k, v]) => {
|
|
const lowerKey = k.toLowerCase();
|
|
if (!lowerHeaders[lowerKey]) {
|
|
req.headers[k] = v;
|
|
}
|
|
});
|
|
};
|
|
|
|
const applyAuthMappingInQuerystring = (authMapping, req, authData) => {
|
|
const rendered = renderAuthMapping(authMapping, authData);
|
|
Object.entries(rendered).forEach(([k, v]) => {
|
|
req.params[k] = req.params[k] || v;
|
|
});
|
|
};
|
|
|
|
const createBeforeRequest = (app) => {
|
|
const authType = _.get(app, 'authentication.type');
|
|
const authMapping = _.get(app, 'legacy.authentication.mapping');
|
|
const placement = _.get(app, 'legacy.authentication.placement') || 'header';
|
|
|
|
const sessionAuthInHeader = (req, z, bundle) => {
|
|
if (!_.isEmpty(bundle.authData)) {
|
|
applyAuthMappingInHeaders(authMapping, req, bundle.authData);
|
|
}
|
|
return req;
|
|
};
|
|
|
|
const sessionAuthInQuerystring = (req, z, bundle) => {
|
|
if (!_.isEmpty(bundle.authData)) {
|
|
applyAuthMappingInQuerystring(authMapping, req, bundle.authData);
|
|
}
|
|
return req;
|
|
};
|
|
|
|
const sessionAuthInBoth = (req, z, bundle) => {
|
|
if (!_.isEmpty(bundle.authData)) {
|
|
applyAuthMappingInHeaders(authMapping, req, bundle.authData);
|
|
applyAuthMappingInQuerystring(authMapping, req, bundle.authData);
|
|
}
|
|
return req;
|
|
};
|
|
|
|
const getGrantType = (req) => {
|
|
const grantType = _.get(req, 'params.grant_type');
|
|
if (grantType) {
|
|
return grantType;
|
|
}
|
|
|
|
const contentType = req.headers['Content-Type'] || '';
|
|
if (contentType.includes('application/json')) {
|
|
try {
|
|
return JSON.parse(req.body).grant_type;
|
|
} catch (err) {
|
|
return null;
|
|
}
|
|
}
|
|
return querystring.parse(req.body).grant_type;
|
|
};
|
|
|
|
const oauth2InHeader = (req, z, bundle) => {
|
|
if (bundle.authData.access_token && getGrantType(req) !== 'refresh_token') {
|
|
req.headers.Authorization =
|
|
req.headers.Authorization || `Bearer ${bundle.authData.access_token}`;
|
|
}
|
|
return req;
|
|
};
|
|
|
|
const oauth2InQuerystring = (req, z, bundle) => {
|
|
if (bundle.authData.access_token && getGrantType(req) !== 'refresh_token') {
|
|
req.params.access_token =
|
|
req.params.access_token || bundle.authData.access_token;
|
|
}
|
|
return req;
|
|
};
|
|
|
|
const oauth2InBoth = (req, z, bundle) => {
|
|
if (bundle.authData.access_token && getGrantType(req) !== 'refresh_token') {
|
|
req.headers.Authorization =
|
|
req.headers.Authorization || `Bearer ${bundle.authData.access_token}`;
|
|
req.params.access_token =
|
|
req.params.access_token || bundle.authData.access_token;
|
|
}
|
|
return req;
|
|
};
|
|
|
|
const apiKeyInHeader = (req, z, bundle) => {
|
|
if (!_.isEmpty(bundle.authData)) {
|
|
applyAuthMappingInHeaders(authMapping, req, bundle.authData);
|
|
}
|
|
return req;
|
|
};
|
|
|
|
const apiKeyInQuerystring = (req, z, bundle) => {
|
|
if (!_.isEmpty(bundle.authData)) {
|
|
applyAuthMappingInQuerystring(authMapping, req, bundle.authData);
|
|
}
|
|
return req;
|
|
};
|
|
|
|
const basicDigestAuth = (req, z, bundle) => {
|
|
if (!bundle._legacyBasicDigestAuthMiddlewareApplied) {
|
|
const username = renderTemplate(
|
|
authMapping.username || '',
|
|
bundle.authData,
|
|
);
|
|
const password = renderTemplate(
|
|
authMapping.password || '',
|
|
bundle.authData,
|
|
);
|
|
bundle.authData.username = username;
|
|
bundle.authData.password = password;
|
|
bundle._legacyBasicDigestAuthMiddlewareApplied = true;
|
|
}
|
|
return req;
|
|
};
|
|
|
|
const oauth1 = (req, z, bundle) => {
|
|
if (
|
|
bundle.authData &&
|
|
bundle.authData.oauth_token &&
|
|
bundle.authData.oauth_token_secret
|
|
) {
|
|
req.auth = req.auth || {};
|
|
|
|
let templateContext;
|
|
|
|
if (!req.auth.oauth_consumer_key) {
|
|
templateContext = Object.assign({}, bundle.authData, bundle.inputData);
|
|
req.auth.oauth_consumer_key = renderTemplate(
|
|
process.env.CLIENT_ID,
|
|
templateContext,
|
|
);
|
|
}
|
|
if (!req.auth.oauth_consumer_secret) {
|
|
if (!templateContext) {
|
|
templateContext = Object.assign(
|
|
{},
|
|
bundle.authData,
|
|
bundle.inputData,
|
|
);
|
|
}
|
|
req.auth.oauth_consumer_secret = renderTemplate(
|
|
process.env.CLIENT_SECRET,
|
|
templateContext,
|
|
);
|
|
}
|
|
|
|
req.auth.oauth_token =
|
|
req.auth.oauth_token || bundle.authData.oauth_token;
|
|
req.auth.oauth_token_secret =
|
|
req.auth.oauth_token_secret || bundle.authData.oauth_token_secret;
|
|
}
|
|
return req;
|
|
};
|
|
|
|
let authBefore;
|
|
|
|
if (authType === 'session') {
|
|
authBefore = {
|
|
header: sessionAuthInHeader,
|
|
querystring: sessionAuthInQuerystring,
|
|
both: sessionAuthInBoth,
|
|
}[placement];
|
|
} else if (authType === 'oauth2') {
|
|
authBefore = {
|
|
header: oauth2InHeader,
|
|
querystring: oauth2InQuerystring,
|
|
both: oauth2InBoth,
|
|
}[placement];
|
|
} else if (authType === 'custom') {
|
|
authBefore = {
|
|
header: apiKeyInHeader,
|
|
querystring: apiKeyInQuerystring,
|
|
}[placement];
|
|
} else if (authType === 'basic' || authType === 'digest') {
|
|
authBefore = basicDigestAuth;
|
|
} else if (authType === 'oauth1') {
|
|
authBefore = oauth1;
|
|
}
|
|
|
|
if (!authBefore) {
|
|
authBefore = (req) => req;
|
|
}
|
|
|
|
const pruneEmptyBodyForGET = (req, z, bundle) => {
|
|
if (req.allowGetBody && req.method === 'GET') {
|
|
const contentType = req.headers['Content-Type'] || '';
|
|
try {
|
|
const parsedBody = contentType.includes('application/json')
|
|
? JSON.parse(req.body)
|
|
: req.body;
|
|
if (_.isEmpty(parsedBody)) {
|
|
delete req.body;
|
|
}
|
|
} catch (err) {
|
|
// Ignore
|
|
}
|
|
}
|
|
return req;
|
|
};
|
|
|
|
return (req, z, bundle) => {
|
|
req = authBefore(req, z, bundle);
|
|
return pruneEmptyBodyForGET(req, z, bundle);
|
|
};
|
|
};
|
|
|
|
const proxyHeaders = (headers) => {
|
|
const proxy = {
|
|
get: (target, prop) => {
|
|
const original = Reflect.get(target, prop);
|
|
if (typeof original === 'function' || typeof original === 'symbol') {
|
|
// defaults to defined functions on the Headers class; the symbol type
|
|
// is used for accessing an internal map
|
|
return original;
|
|
}
|
|
try {
|
|
// try to retrieve the header via the get() function
|
|
return target.get(prop);
|
|
} catch {
|
|
// otherwise, default to original target[prop] value
|
|
return original;
|
|
}
|
|
},
|
|
};
|
|
return new Proxy(headers, proxy);
|
|
};
|
|
|
|
const createAfterResponse = (app) => {
|
|
const authType = _.get(app, 'authentication.type');
|
|
const autoRefresh = _.get(app, 'authentication.oauth2Config.autoRefresh');
|
|
|
|
const throwForStaleAuth = (response, z) => {
|
|
if (response.status === 401) {
|
|
throw new z.errors.RefreshAuthError('Authentication needs refreshing');
|
|
}
|
|
return response;
|
|
};
|
|
|
|
const makeHeaderCaseInsensitive = (response, z) => {
|
|
response.headers = proxyHeaders(response.headers);
|
|
return response;
|
|
};
|
|
|
|
let afterResponse;
|
|
|
|
if (authType === 'session' || (authType === 'oauth2' && autoRefresh)) {
|
|
afterResponse = throwForStaleAuth;
|
|
}
|
|
|
|
if (!afterResponse) {
|
|
afterResponse = (response) => response;
|
|
}
|
|
|
|
return (response, z, bundle) => {
|
|
response = afterResponse(response, z, bundle);
|
|
return makeHeaderCaseInsensitive(response);
|
|
};
|
|
};
|
|
|
|
module.exports = {
|
|
createBeforeRequest,
|
|
createAfterResponse,
|
|
renderTemplate,
|
|
};
|