push to remote first for entire testing
This commit is contained in:
parent
d322fbe2d4
commit
cf78cee0db
|
|
@ -5,15 +5,13 @@ import apiClient from './api/client';
|
||||||
import BusinessReviewModal from './components/BusinessReviewModal';
|
import BusinessReviewModal from './components/BusinessReviewModal';
|
||||||
import Sidebar from './components/Sidebar';
|
import Sidebar from './components/Sidebar';
|
||||||
import Businesses from './pages/Businesses';
|
import Businesses from './pages/Businesses';
|
||||||
import Providers from './pages/Providers';
|
|
||||||
import GlobalSms from './pages/GlobalSms';
|
import GlobalSms from './pages/GlobalSms';
|
||||||
import Analytics from './pages/Analytics';
|
import Analytics from './pages/Analytics';
|
||||||
import Events from './pages/Events';
|
import Events from './pages/Events';
|
||||||
import Templates from './pages/Templates';
|
import Templates from './pages/Templates';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
function SubLayout({ children }) {
|
function SubLayout({ children }) {
|
||||||
const { activeBusiness, activeBusinessId, hasGlobalSms } = useBusiness();
|
const { activeBusiness, activeBusinessId } = useBusiness();
|
||||||
const [reviewBusiness, setReviewBusiness] = useState(null);
|
const [reviewBusiness, setReviewBusiness] = useState(null);
|
||||||
const [reviewLoading, setReviewLoading] = useState(false);
|
const [reviewLoading, setReviewLoading] = useState(false);
|
||||||
const [reviewError, setReviewError] = useState('');
|
const [reviewError, setReviewError] = useState('');
|
||||||
|
|
@ -48,17 +46,7 @@ function SubLayout({ children }) {
|
||||||
reviewError={reviewError}
|
reviewError={reviewError}
|
||||||
/>
|
/>
|
||||||
<main className="flex-1 ml-60 flex flex-col">
|
<main className="flex-1 ml-60 flex flex-col">
|
||||||
<header className="h-16 border-b border-border-main bg-white flex items-center justify-end px-8 z-10 shrink-0">
|
<header className="h-16 border-b border-border-main bg-white shrink-0" />
|
||||||
{hasGlobalSms && (
|
|
||||||
<Link
|
|
||||||
to={`/${activeBusinessId}/settings`}
|
|
||||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-border-soft bg-gray-50 text-gray-500 transition-colors hover:border-gray-300 hover:bg-white hover:text-primary-blue"
|
|
||||||
title="Settings"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</header>
|
|
||||||
<div className="flex-1 p-5 overflow-auto">
|
<div className="flex-1 p-5 overflow-auto">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -106,9 +94,6 @@ export default function App() {
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Businesses />} />
|
<Route path="/" element={<Businesses />} />
|
||||||
<Route path="/:businessId/settings" element={
|
|
||||||
<BusinessGuard><SubLayout><Providers /></SubLayout></BusinessGuard>
|
|
||||||
} />
|
|
||||||
<Route path="/:businessId/global-sms" element={
|
<Route path="/:businessId/global-sms" element={
|
||||||
<BusinessGuard isGlobalSmsRoute><SubLayout><GlobalSms /></SubLayout></BusinessGuard>
|
<BusinessGuard isGlobalSmsRoute><SubLayout><GlobalSms /></SubLayout></BusinessGuard>
|
||||||
} />
|
} />
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ export default function Sidebar({ onOpenReview, reviewLoading = false, reviewErr
|
||||||
activeBusiness,
|
activeBusiness,
|
||||||
activeBusinessId,
|
activeBusinessId,
|
||||||
clearBusiness,
|
clearBusiness,
|
||||||
hasGlobalSms,
|
|
||||||
isSetupComplete,
|
isSetupComplete,
|
||||||
hasSelectedTemplates,
|
hasSelectedTemplates,
|
||||||
} = useBusiness();
|
} = useBusiness();
|
||||||
|
|
@ -88,13 +87,6 @@ export default function Sidebar({ onOpenReview, reviewLoading = false, reviewErr
|
||||||
const isEventsRoute = location.pathname === eventsPath;
|
const isEventsRoute = location.pathname === eventsPath;
|
||||||
const isTemplatesRoute = location.pathname === templatesPath;
|
const isTemplatesRoute = location.pathname === templatesPath;
|
||||||
|
|
||||||
const omniSubsteps = [
|
|
||||||
{ id: 'profile', label: 'Add / Select Profile', done: hasGlobalSms, active: isGlobalSmsRoute && !hasGlobalSms },
|
|
||||||
{ id: 'validate', label: 'Validate cURL', done: hasGlobalSms, active: false },
|
|
||||||
{ id: 'fields', label: 'Complete Fields', done: isSetupComplete, active: isGlobalSmsRoute && hasGlobalSms && !isSetupComplete },
|
|
||||||
{ id: 'ready', label: 'Ready', done: isSetupComplete, active: isGlobalSmsRoute && isSetupComplete },
|
|
||||||
];
|
|
||||||
|
|
||||||
const stepItems = [
|
const stepItems = [
|
||||||
{
|
{
|
||||||
id: 'globalSms',
|
id: 'globalSms',
|
||||||
|
|
@ -103,8 +95,7 @@ export default function Sidebar({ onOpenReview, reviewLoading = false, reviewErr
|
||||||
enabled: true,
|
enabled: true,
|
||||||
done: isSetupComplete && !isGlobalSmsRoute,
|
done: isSetupComplete && !isGlobalSmsRoute,
|
||||||
active: isGlobalSmsRoute,
|
active: isGlobalSmsRoute,
|
||||||
expanded: isGlobalSmsRoute,
|
expanded: false,
|
||||||
substeps: omniSubsteps,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'events',
|
id: 'events',
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,7 +3,13 @@ const router = express.Router();
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const { buildBrandContextPlan, collectBrandContextPages } = require('../services/firecrawl');
|
const { buildBrandContextPlan, collectBrandContextPages } = require('../services/firecrawl');
|
||||||
const { parseBrandContext, generateTemplates, processCurl, validateEditedTemplate, validateCurlFields } = require('../services/openai2');
|
const { parseBrandContext, generateTemplates, processCurl, validateEditedTemplate, validateCurlFields } = require('../services/openai2');
|
||||||
const { executeTemplatedCurl, parseCurlCommand } = require('../services/curlExecutor');
|
const {
|
||||||
|
buildPatchedCurlTemplateFromRequest,
|
||||||
|
buildRequestBlueprintFromCurl,
|
||||||
|
executeTemplatedCurl,
|
||||||
|
normalizeHeaderEntries,
|
||||||
|
parseCurlCommand,
|
||||||
|
} = require('../services/curlExecutor');
|
||||||
const { buildCrawlSummary } = require('../services/crawlSummary');
|
const { buildCrawlSummary } = require('../services/crawlSummary');
|
||||||
const {
|
const {
|
||||||
uploadJSON,
|
uploadJSON,
|
||||||
|
|
@ -127,6 +133,7 @@ async function findBusinessByBrandName(merchantId, brandName) {
|
||||||
const PROVIDER_FIELDS = ['providerName', 'senderId', 'dltEntityId'];
|
const PROVIDER_FIELDS = ['providerName', 'senderId', 'dltEntityId'];
|
||||||
const BASE_PROFILE_INPUT_KEYS = ['providerName', 'senderId', 'dltEntityId'];
|
const BASE_PROFILE_INPUT_KEYS = ['providerName', 'senderId', 'dltEntityId'];
|
||||||
const MASKED_SECRET = '••••••••';
|
const MASKED_SECRET = '••••••••';
|
||||||
|
const SENSITIVE_HEADER_KEYS = new Set(['authorization', 'proxy-authorization', 'x-api-key', 'api-key', 'x-auth-key']);
|
||||||
const RUNTIME_TOKEN_MAP = {
|
const RUNTIME_TOKEN_MAP = {
|
||||||
toNumber: '__SMS_TO_NUMBER__',
|
toNumber: '__SMS_TO_NUMBER__',
|
||||||
content: '__SMS_CONTENT__',
|
content: '__SMS_CONTENT__',
|
||||||
|
|
@ -203,6 +210,135 @@ function firstNonEmptyResolvedText(...values) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compactPlaceholderText(value) {
|
||||||
|
return normalizeText(String(value || ''))
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const GENERIC_SECRET_PLACEHOLDER_COMPACTS = new Set([
|
||||||
|
'password',
|
||||||
|
'passwd',
|
||||||
|
'passcode',
|
||||||
|
'secret',
|
||||||
|
'token',
|
||||||
|
'authtoken',
|
||||||
|
'accesstoken',
|
||||||
|
'bearertoken',
|
||||||
|
'apikey',
|
||||||
|
'authkey',
|
||||||
|
'clientsecret',
|
||||||
|
'credential',
|
||||||
|
'credentials',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const SECRET_PLACEHOLDER_PREFIXES = [
|
||||||
|
'your',
|
||||||
|
'enter',
|
||||||
|
'insert',
|
||||||
|
'provide',
|
||||||
|
'replace',
|
||||||
|
'replacewith',
|
||||||
|
'set',
|
||||||
|
'use',
|
||||||
|
'sample',
|
||||||
|
'dummy',
|
||||||
|
'test',
|
||||||
|
'example',
|
||||||
|
];
|
||||||
|
|
||||||
|
const SECRET_PLACEHOLDER_SUFFIXES = ['here', 'value', 'placeholder'];
|
||||||
|
|
||||||
|
function buildSecretPlaceholderCandidates(input = {}) {
|
||||||
|
const candidates = new Set(GENERIC_SECRET_PLACEHOLDER_COMPACTS);
|
||||||
|
const normalizedKey = compactPlaceholderText(input.key);
|
||||||
|
const normalizedLabel = compactPlaceholderText(input.label);
|
||||||
|
|
||||||
|
if (normalizedKey) candidates.add(normalizedKey);
|
||||||
|
if (normalizedLabel) candidates.add(normalizedLabel);
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesSecretPlaceholderCandidate(compactValue, input = {}) {
|
||||||
|
if (!compactValue) return false;
|
||||||
|
|
||||||
|
const candidates = buildSecretPlaceholderCandidates(input);
|
||||||
|
if (candidates.has(compactValue)) return true;
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate) continue;
|
||||||
|
|
||||||
|
if (compactValue === `${candidate}here`
|
||||||
|
|| compactValue === `${candidate}value`
|
||||||
|
|| compactValue === `${candidate}placeholder`) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const prefix of SECRET_PLACEHOLDER_PREFIXES) {
|
||||||
|
if (compactValue === `${prefix}${candidate}`) return true;
|
||||||
|
|
||||||
|
for (const suffix of SECRET_PLACEHOLDER_SUFFIXES) {
|
||||||
|
if (compactValue === `${prefix}${candidate}${suffix}`) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeWrappedPlaceholderInner(value = '') {
|
||||||
|
const normalized = normalizeText(value);
|
||||||
|
if (!normalized) return false;
|
||||||
|
|
||||||
|
return /[_\-\s]/.test(normalized)
|
||||||
|
|| /^[A-Z0-9_:-]+$/.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesWrappedSecretPlaceholderCandidate(value = '', input = {}) {
|
||||||
|
const compactValue = compactPlaceholderText(value);
|
||||||
|
if (!compactValue) return false;
|
||||||
|
if (matchesSecretPlaceholderCandidate(compactValue, input)) return true;
|
||||||
|
if (!looksLikeWrappedPlaceholderInner(value)) return false;
|
||||||
|
|
||||||
|
const candidates = buildSecretPlaceholderCandidates(input);
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate) continue;
|
||||||
|
if (compactValue.startsWith(candidate) || compactValue.endsWith(candidate)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlaceholderLikeSecretValue(value, input = {}) {
|
||||||
|
const normalized = normalizeResolvedScalarText(value);
|
||||||
|
if (!normalized) return false;
|
||||||
|
|
||||||
|
const compactValue = compactPlaceholderText(normalized);
|
||||||
|
if (matchesSecretPlaceholderCandidate(compactValue, input)) return true;
|
||||||
|
|
||||||
|
const wrappedMatch = normalized.match(/^(?:<\s*([^>]+)\s*>|\{\{\s*([^}]+)\s*\}\}|\[\s*([^\]]+)\s*\])$/);
|
||||||
|
if (!wrappedMatch) return false;
|
||||||
|
|
||||||
|
const innerValue = wrappedMatch[1] || wrappedMatch[2] || wrappedMatch[3] || '';
|
||||||
|
return matchesWrappedSecretPlaceholderCandidate(innerValue, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeResolvedInputValue(value, input = {}) {
|
||||||
|
const normalized = normalizeResolvedScalarText(value);
|
||||||
|
if (!normalized) return '';
|
||||||
|
if (input?.secret === true && isPlaceholderLikeSecretValue(normalized, input)) return '';
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstNonEmptyInputValue(input = {}, ...values) {
|
||||||
|
for (const value of values) {
|
||||||
|
const normalized = normalizeResolvedInputValue(value, input);
|
||||||
|
if (normalized) return normalized;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSenderId(value) {
|
function normalizeSenderId(value) {
|
||||||
return normalizeText(value).toUpperCase();
|
return normalizeText(value).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
@ -366,7 +502,11 @@ function normalizeRequiredInput(input = {}) {
|
||||||
? 'runtime'
|
? 'runtime'
|
||||||
: requestedSource,
|
: requestedSource,
|
||||||
token: normalizeText(input.token),
|
token: normalizeText(input.token),
|
||||||
currentValue: normalizeResolvedScalarText(input.currentValue || input.value),
|
currentValue: normalizeResolvedInputValue(input.currentValue || input.value, {
|
||||||
|
key,
|
||||||
|
label: normalizeText(input.label) || humanizeInputKey(key),
|
||||||
|
secret: input.secret === true,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -419,7 +559,11 @@ function getStoredProfileValue(profile = {}, key) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedKey === 'authKey') {
|
if (normalizedKey === 'authKey') {
|
||||||
return firstNonEmptyResolvedText(profile.profileInputValues?.authKey, profile.provider?.authKey);
|
return firstNonEmptyInputValue(
|
||||||
|
{ key: 'authKey', label: 'Auth Key', secret: true },
|
||||||
|
profile.profileInputValues?.authKey,
|
||||||
|
profile.provider?.authKey
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizeResolvedScalarText(profile.profileInputValues?.[normalizedKey]);
|
return normalizeResolvedScalarText(profile.profileInputValues?.[normalizedKey]);
|
||||||
|
|
@ -483,7 +627,13 @@ function getProfileInputDefinitions(profile = {}) {
|
||||||
label: normalized.label || current?.label || humanizeInputKey(normalized.key),
|
label: normalized.label || current?.label || humanizeInputKey(normalized.key),
|
||||||
required: normalized.required !== false || current?.required === true,
|
required: normalized.required !== false || current?.required === true,
|
||||||
secret: normalized.secret === true || current?.secret === true,
|
secret: normalized.secret === true || current?.secret === true,
|
||||||
currentValue: firstNonEmptyResolvedText(
|
currentValue: firstNonEmptyInputValue({
|
||||||
|
...current,
|
||||||
|
...normalized,
|
||||||
|
key: normalized.key,
|
||||||
|
label: normalized.label || current?.label || humanizeInputKey(normalized.key),
|
||||||
|
secret: normalized.secret === true || current?.secret === true,
|
||||||
|
},
|
||||||
getStoredProfileValue(profile, normalized.key),
|
getStoredProfileValue(profile, normalized.key),
|
||||||
normalized.currentValue,
|
normalized.currentValue,
|
||||||
current?.currentValue
|
current?.currentValue
|
||||||
|
|
@ -496,7 +646,7 @@ function getProfileInputDefinitions(profile = {}) {
|
||||||
|
|
||||||
function serializeProfileInput(profile, input, options = {}) {
|
function serializeProfileInput(profile, input, options = {}) {
|
||||||
const revealSecrets = options.revealSecrets === true;
|
const revealSecrets = options.revealSecrets === true;
|
||||||
const value = firstNonEmptyResolvedText(getStoredProfileValue(profile, input.key), input.currentValue);
|
const value = firstNonEmptyInputValue(input, getStoredProfileValue(profile, input.key), input.currentValue);
|
||||||
const hasValue = Boolean(value);
|
const hasValue = Boolean(value);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -515,7 +665,7 @@ function serializeProfileInput(profile, input, options = {}) {
|
||||||
function getMissingProfileInputs(profile = {}) {
|
function getMissingProfileInputs(profile = {}) {
|
||||||
return getProfileInputDefinitions(profile)
|
return getProfileInputDefinitions(profile)
|
||||||
.filter((input) => input.required !== false)
|
.filter((input) => input.required !== false)
|
||||||
.filter((input) => !firstNonEmptyResolvedText(getStoredProfileValue(profile, input.key), input.currentValue))
|
.filter((input) => !firstNonEmptyInputValue(input, getStoredProfileValue(profile, input.key), input.currentValue))
|
||||||
.map((input) => serializeProfileInput(profile, input));
|
.map((input) => serializeProfileInput(profile, input));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -538,7 +688,7 @@ function buildDisplayCurl(profile = {}, options = {}) {
|
||||||
inputs.forEach((input) => {
|
inputs.forEach((input) => {
|
||||||
const token = normalizeText(input.token);
|
const token = normalizeText(input.token);
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
const value = firstNonEmptyResolvedText(getStoredProfileValue(profile, input.key), input.currentValue);
|
const value = firstNonEmptyInputValue(input, getStoredProfileValue(profile, input.key), input.currentValue);
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
output = output.split(token).join(input.secret && !revealSecrets ? MASKED_SECRET : value);
|
output = output.split(token).join(input.secret && !revealSecrets ? MASKED_SECRET : value);
|
||||||
});
|
});
|
||||||
|
|
@ -546,6 +696,249 @@ function buildDisplayCurl(profile = {}, options = {}) {
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyProfileInputsToRequestText(value = '', profile = {}, options = {}) {
|
||||||
|
let output = String(value ?? '');
|
||||||
|
const revealSecrets = options.revealSecrets === true;
|
||||||
|
|
||||||
|
getProfileInputDefinitions(profile).forEach((input) => {
|
||||||
|
const token = normalizeText(input.token);
|
||||||
|
const currentValue = firstNonEmptyInputValue(input, getStoredProfileValue(profile, input.key), input.currentValue);
|
||||||
|
if (!currentValue) {
|
||||||
|
if (token) {
|
||||||
|
output = output.split(token).join('');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacement = input.secret && !revealSecrets ? MASKED_SECRET : currentValue;
|
||||||
|
if (token) {
|
||||||
|
output = output.split(token).join(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.secret && !revealSecrets) {
|
||||||
|
output = output.split(currentValue).join(MASKED_SECRET);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyProfileTokensToRequestText(value = '', profile = {}) {
|
||||||
|
let output = normalizeScalarText(value);
|
||||||
|
|
||||||
|
getProfileInputDefinitions(profile).forEach((input) => {
|
||||||
|
const token = normalizeText(input.token);
|
||||||
|
const currentValue = firstNonEmptyInputValue(input, getStoredProfileValue(profile, input.key), input.currentValue);
|
||||||
|
if (!token || !currentValue || token === currentValue) return;
|
||||||
|
output = output.split(currentValue).join(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSensitiveRequestText(value = '', profile = {}) {
|
||||||
|
const source = String(value ?? '');
|
||||||
|
if (!source) return false;
|
||||||
|
|
||||||
|
return getProfileInputDefinitions(profile)
|
||||||
|
.filter((input) => input.secret === true)
|
||||||
|
.some((input) => {
|
||||||
|
const token = normalizeText(input.token);
|
||||||
|
const currentValue = firstNonEmptyInputValue(input, getStoredProfileValue(profile, input.key), input.currentValue);
|
||||||
|
return (token && source.includes(token)) || (currentValue && source.includes(currentValue));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSensitiveHeaderEntry(entry = {}, profile = {}) {
|
||||||
|
const normalizedKey = normalizeText(entry.key).toLowerCase();
|
||||||
|
return SENSITIVE_HEADER_KEYS.has(normalizedKey)
|
||||||
|
|| isSensitiveRequestText(entry.key, profile)
|
||||||
|
|| isSensitiveRequestText(entry.value, profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasStoredSecretValueForHeaderEntry(entry = {}, profile = {}) {
|
||||||
|
const keyText = String(entry.key ?? '');
|
||||||
|
const valueText = String(entry.value ?? '');
|
||||||
|
|
||||||
|
return getProfileInputDefinitions(profile)
|
||||||
|
.filter((input) => input.secret === true)
|
||||||
|
.some((input) => {
|
||||||
|
const token = normalizeText(input.token);
|
||||||
|
const currentValue = firstNonEmptyInputValue(input, getStoredProfileValue(profile, input.key), input.currentValue);
|
||||||
|
if (!currentValue) return false;
|
||||||
|
|
||||||
|
return (token && (keyText.includes(token) || valueText.includes(token)))
|
||||||
|
|| keyText.includes(currentValue)
|
||||||
|
|| valueText.includes(currentValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskSensitiveHeaderPreviewValue(entry = {}, resolvedValue = '', maskedCandidate = '') {
|
||||||
|
const normalizedKey = normalizeText(entry.key).toLowerCase();
|
||||||
|
const baseValue = String(maskedCandidate || resolvedValue || '');
|
||||||
|
if (!baseValue) return '';
|
||||||
|
|
||||||
|
if (normalizedKey === 'authorization' || normalizedKey === 'proxy-authorization') {
|
||||||
|
const schemeMatch = String(resolvedValue || baseValue).match(/^(\S+\s+)/);
|
||||||
|
return schemeMatch ? `${schemeMatch[1]}${MASKED_SECRET}` : MASKED_SECRET;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SENSITIVE_HEADER_KEYS.has(normalizedKey)) {
|
||||||
|
return MASKED_SECRET;
|
||||||
|
}
|
||||||
|
|
||||||
|
return maskedCandidate && maskedCandidate !== resolvedValue ? maskedCandidate : MASKED_SECRET;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeRequestPreview(profile = {}, options = {}) {
|
||||||
|
const hydratedProfile = hydrateProfile(profile);
|
||||||
|
const rawCurlTemplate = getStoredCurlTemplate(hydratedProfile);
|
||||||
|
if (!rawCurlTemplate) return null;
|
||||||
|
|
||||||
|
let requestBlueprint = null;
|
||||||
|
try {
|
||||||
|
requestBlueprint = buildRequestBlueprintFromCurl(rawCurlTemplate);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const revealSecrets = options.revealSecrets === true;
|
||||||
|
const resolvedUrl = applyProfileInputsToRequestText(requestBlueprint.url, hydratedProfile, { revealSecrets: true });
|
||||||
|
const maskedUrl = applyProfileInputsToRequestText(requestBlueprint.url, hydratedProfile, { revealSecrets: false });
|
||||||
|
|
||||||
|
return {
|
||||||
|
method: normalizeText(requestBlueprint.method).toUpperCase() || 'POST',
|
||||||
|
url: revealSecrets ? resolvedUrl : maskedUrl,
|
||||||
|
maskedUrl,
|
||||||
|
urlMasked: !revealSecrets && maskedUrl !== resolvedUrl,
|
||||||
|
headers: normalizeHeaderEntries(requestBlueprint.headerEntries).map((entry, index) => {
|
||||||
|
const resolvedKey = applyProfileInputsToRequestText(entry.key, hydratedProfile, { revealSecrets: true });
|
||||||
|
const resolvedValue = applyProfileInputsToRequestText(entry.value, hydratedProfile, { revealSecrets: true });
|
||||||
|
const maskedCandidate = applyProfileInputsToRequestText(entry.value, hydratedProfile, { revealSecrets: false });
|
||||||
|
const secret = isSensitiveHeaderEntry(entry, hydratedProfile);
|
||||||
|
const maskedValue = secret && hasStoredSecretValueForHeaderEntry(entry, hydratedProfile)
|
||||||
|
? maskSensitiveHeaderPreviewValue({ ...entry, key: resolvedKey }, resolvedValue, maskedCandidate)
|
||||||
|
: maskedCandidate;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: normalizeText(entry.id) || `header-${index}`,
|
||||||
|
key: resolvedKey,
|
||||||
|
value: revealSecrets ? resolvedValue : maskedValue,
|
||||||
|
maskedValue,
|
||||||
|
masked: maskedValue !== resolvedValue,
|
||||||
|
secret,
|
||||||
|
enabled: entry.enabled !== false,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRequestPatch(payload = {}) {
|
||||||
|
const request = payload?.request;
|
||||||
|
if (!request || typeof request !== 'object' || Array.isArray(request)) return null;
|
||||||
|
|
||||||
|
const patch = {};
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(request, 'url')) {
|
||||||
|
patch.url = normalizeScalarText(request.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(request.headers)) {
|
||||||
|
patch.headers = request.headers.reduce((items, entry, index) => {
|
||||||
|
const id = normalizeText(entry?.id) || `header-${index}`;
|
||||||
|
const key = normalizeScalarText(entry?.key);
|
||||||
|
const value = normalizeScalarText(entry?.value);
|
||||||
|
const enabled = entry?.enabled !== false;
|
||||||
|
|
||||||
|
if (!key && !value) return items;
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
id,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(patch).length > 0 ? patch : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRequestPatch(requestPatch = {}) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(requestPatch, 'url')) {
|
||||||
|
if (!isAbsoluteHttpUrl(requestPatch.url)) {
|
||||||
|
throw createHttpError(400, 'Request URL must be a valid absolute http(s) URL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(requestPatch.headers)) {
|
||||||
|
requestPatch.headers.forEach((entry) => {
|
||||||
|
if (!normalizeText(entry.key)) {
|
||||||
|
throw createHttpError(400, 'Request headers must include a key');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyRequestPatch(profile, requestPatch = {}) {
|
||||||
|
const currentRawCurl = getStoredCurlTemplate(profile);
|
||||||
|
if (!currentRawCurl) {
|
||||||
|
throw createHttpError(400, 'Profile does not have an editable request');
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentRequest = null;
|
||||||
|
try {
|
||||||
|
currentRequest = buildRequestBlueprintFromCurl(currentRawCurl);
|
||||||
|
} catch {
|
||||||
|
throw createHttpError(400, 'Profile does not have an editable request');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingHeaderEntries = normalizeHeaderEntries(currentRequest.headerEntries);
|
||||||
|
const existingHeadersById = new Map(existingHeaderEntries.map((entry) => [normalizeText(entry.id), entry]));
|
||||||
|
const nextUrl = Object.prototype.hasOwnProperty.call(requestPatch, 'url')
|
||||||
|
? normalizeScalarText(requestPatch.url)
|
||||||
|
: normalizeText(currentRequest.url);
|
||||||
|
const safeUrl = nextUrl.includes(MASKED_SECRET) && isSensitiveRequestText(currentRequest.url, profile)
|
||||||
|
? normalizeText(currentRequest.url)
|
||||||
|
: nextUrl;
|
||||||
|
|
||||||
|
const nextHeaderEntries = (Array.isArray(requestPatch.headers) ? requestPatch.headers : existingHeaderEntries).reduce((items, entry, index) => {
|
||||||
|
const fallbackEntry = existingHeaderEntries[index] || {};
|
||||||
|
const id = normalizeText(entry?.id) || normalizeText(fallbackEntry.id) || `header-${index}`;
|
||||||
|
const existingEntry = existingHeadersById.get(id) || fallbackEntry;
|
||||||
|
const key = normalizeScalarText(entry?.key);
|
||||||
|
const rawValue = normalizeScalarText(entry?.value);
|
||||||
|
const value = rawValue.includes(MASKED_SECRET) && isSensitiveHeaderEntry(existingEntry, profile)
|
||||||
|
? normalizeScalarText(existingEntry.value)
|
||||||
|
: rawValue;
|
||||||
|
|
||||||
|
if (!key && !value) return items;
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
id,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
enabled: entry?.enabled !== false,
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tokenizedUrl = applyProfileTokensToRequestText(safeUrl, profile);
|
||||||
|
const tokenizedHeaderEntries = nextHeaderEntries.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
key: applyProfileTokensToRequestText(entry.key, profile),
|
||||||
|
value: applyProfileTokensToRequestText(entry.value, profile),
|
||||||
|
}));
|
||||||
|
const patchedCurlTemplate = buildPatchedCurlTemplateFromRequest(currentRawCurl, {
|
||||||
|
url: tokenizedUrl,
|
||||||
|
headers: tokenizedHeaderEntries,
|
||||||
|
});
|
||||||
|
const validation = await validateCurlAndExtractProvider(patchedCurlTemplate);
|
||||||
|
|
||||||
|
return buildStoredProfileFromValidation(profile, validation);
|
||||||
|
}
|
||||||
|
|
||||||
function serializeCurlAnalysis(profile = {}) {
|
function serializeCurlAnalysis(profile = {}) {
|
||||||
const curlAnalysis = normalizeCurlAnalysis(profile.curlAnalysis, profile.provider);
|
const curlAnalysis = normalizeCurlAnalysis(profile.curlAnalysis, profile.provider);
|
||||||
return {
|
return {
|
||||||
|
|
@ -593,6 +986,7 @@ function serializeProfile(profile = {}) {
|
||||||
maskedCurl: buildDisplayCurl(hydratedProfile),
|
maskedCurl: buildDisplayCurl(hydratedProfile),
|
||||||
curlAnalysis: serializeCurlAnalysis(hydratedProfile),
|
curlAnalysis: serializeCurlAnalysis(hydratedProfile),
|
||||||
profileInputs: getProfileInputDefinitions(hydratedProfile).map((input) => serializeProfileInput(hydratedProfile, input)),
|
profileInputs: getProfileInputDefinitions(hydratedProfile).map((input) => serializeProfileInput(hydratedProfile, input)),
|
||||||
|
requestPreview: serializeRequestPreview(hydratedProfile),
|
||||||
executionReadiness: getExecutionReadiness(hydratedProfile),
|
executionReadiness: getExecutionReadiness(hydratedProfile),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -603,6 +997,7 @@ function getProfileRevealPayload(profile = {}) {
|
||||||
rawCurl: buildDisplayCurl(hydratedProfile, { revealSecrets: true }),
|
rawCurl: buildDisplayCurl(hydratedProfile, { revealSecrets: true }),
|
||||||
profileInputs: getProfileInputDefinitions(hydratedProfile)
|
profileInputs: getProfileInputDefinitions(hydratedProfile)
|
||||||
.map((input) => serializeProfileInput(hydratedProfile, input, { revealSecrets: true })),
|
.map((input) => serializeProfileInput(hydratedProfile, input, { revealSecrets: true })),
|
||||||
|
requestPreview: serializeRequestPreview(hydratedProfile, { revealSecrets: true }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -627,7 +1022,8 @@ function sanitizeStoredCurlAnalysis(profile = {}) {
|
||||||
|
|
||||||
function persistableProfile(profile = {}) {
|
function persistableProfile(profile = {}) {
|
||||||
const hydratedProfile = syncAutomaticProfileName(hydrateProfile(profile));
|
const hydratedProfile = syncAutomaticProfileName(hydrateProfile(profile));
|
||||||
const normalizedAuthKey = firstNonEmptyResolvedText(
|
const normalizedAuthKey = firstNonEmptyInputValue(
|
||||||
|
{ key: 'authKey', label: 'Auth Key', secret: true },
|
||||||
hydratedProfile.profileInputValues?.authKey,
|
hydratedProfile.profileInputValues?.authKey,
|
||||||
hydratedProfile.provider?.authKey,
|
hydratedProfile.provider?.authKey,
|
||||||
);
|
);
|
||||||
|
|
@ -1813,7 +2209,7 @@ function buildExecutionTokenValues(boundProfile = {}, executionSnapshot = {}, ru
|
||||||
|
|
||||||
getProfileInputDefinitions(boundProfile).forEach((input) => {
|
getProfileInputDefinitions(boundProfile).forEach((input) => {
|
||||||
const token = normalizeText(input.token);
|
const token = normalizeText(input.token);
|
||||||
const value = firstNonEmptyResolvedText(getStoredProfileValue(boundProfile, input.key), input.currentValue);
|
const value = firstNonEmptyInputValue(input, getStoredProfileValue(boundProfile, input.key), input.currentValue);
|
||||||
if (token && value) {
|
if (token && value) {
|
||||||
tokenValues[token] = value;
|
tokenValues[token] = value;
|
||||||
}
|
}
|
||||||
|
|
@ -1829,7 +2225,7 @@ function buildExecutionTokenValues(boundProfile = {}, executionSnapshot = {}, ru
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = firstNonEmptyResolvedText(getStoredProfileValue(boundProfile, input.key), input.currentValue);
|
const value = firstNonEmptyInputValue(input, getStoredProfileValue(boundProfile, input.key), input.currentValue);
|
||||||
if (value) tokenValues[token] = value;
|
if (value) tokenValues[token] = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2737,7 +3133,7 @@ function buildStoredProfileFromValidation(baseProfile = {}, validation = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
getProfileInputDefinitions(profile).forEach((input) => {
|
getProfileInputDefinitions(profile).forEach((input) => {
|
||||||
const currentValue = firstNonEmptyResolvedText(getStoredProfileValue(profile, input.key), input.currentValue);
|
const currentValue = firstNonEmptyInputValue(input, getStoredProfileValue(profile, input.key), input.currentValue);
|
||||||
if (currentValue) {
|
if (currentValue) {
|
||||||
setStoredProfileValue(profile, input.key, currentValue);
|
setStoredProfileValue(profile, input.key, currentValue);
|
||||||
}
|
}
|
||||||
|
|
@ -3312,6 +3708,7 @@ router.patch('/:businessId/global-sms/profiles/:profileId', async (req, res) =>
|
||||||
const { businessId, profileId } = req.params;
|
const { businessId, profileId } = req.params;
|
||||||
const { name, rawCurl } = req.body;
|
const { name, rawCurl } = req.body;
|
||||||
const profilePatch = collectProfileInputPatch(req.body);
|
const profilePatch = collectProfileInputPatch(req.body);
|
||||||
|
const requestPatch = collectRequestPatch(req.body);
|
||||||
|
|
||||||
if (name !== undefined && !normalizeText(name)) {
|
if (name !== undefined && !normalizeText(name)) {
|
||||||
return res.status(400).json({ error: 'name is required' });
|
return res.status(400).json({ error: 'name is required' });
|
||||||
|
|
@ -3323,16 +3720,24 @@ router.patch('/:businessId/global-sms/profiles/:profileId', async (req, res) =>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
validateProfileInputPatch(profilePatch);
|
validateProfileInputPatch(profilePatch);
|
||||||
|
if (requestPatch) {
|
||||||
|
validateRequestPatch(requestPatch);
|
||||||
|
}
|
||||||
|
|
||||||
const bizRoot = businessRoot(getCompanyId(req), businessId);
|
const bizRoot = businessRoot(getCompanyId(req), businessId);
|
||||||
const { profileData } = await getProfileState(bizRoot);
|
const { profileData } = await getProfileState(bizRoot);
|
||||||
const profile = profileData.profiles.find(p => p.id === profileId);
|
const profileIndex = profileData.profiles.findIndex((item) => item.id === profileId);
|
||||||
if (!profile) return res.status(404).json({ error: 'Profile not found' });
|
if (profileIndex < 0) return res.status(404).json({ error: 'Profile not found' });
|
||||||
|
let profile = profileData.profiles[profileIndex];
|
||||||
|
|
||||||
if (name !== undefined) {
|
if (name !== undefined) {
|
||||||
profile.name = normalizeText(name);
|
profile.name = normalizeText(name);
|
||||||
profile.isAutoNamed = false;
|
profile.isAutoNamed = false;
|
||||||
}
|
}
|
||||||
|
if (requestPatch) {
|
||||||
|
profile = await applyRequestPatch(profile, requestPatch);
|
||||||
|
profileData.profiles[profileIndex] = profile;
|
||||||
|
}
|
||||||
applyProfileInputPatch(profile, profilePatch);
|
applyProfileInputPatch(profile, profilePatch);
|
||||||
|
|
||||||
await saveGlobalSmsProfiles(bizRoot, profileData);
|
await saveGlobalSmsProfiles(bizRoot, profileData);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ const STATUS_MARKER = '__CODEX_HTTP_STATUS__:';
|
||||||
const DEFAULT_TIMEOUT_MS = 30000;
|
const DEFAULT_TIMEOUT_MS = 30000;
|
||||||
const MAX_CAPTURE_LENGTH = 1024 * 1024;
|
const MAX_CAPTURE_LENGTH = 1024 * 1024;
|
||||||
const DATA_FLAGS = new Set(['--data', '--data-raw', '--data-binary', '-d']);
|
const DATA_FLAGS = new Set(['--data', '--data-raw', '--data-binary', '-d']);
|
||||||
|
const HEADER_FLAGS = new Set(['--header', '-H']);
|
||||||
|
const METHOD_FLAGS = new Set(['--request', '-X']);
|
||||||
|
const IGNORED_HEADER_KEYS = new Set(['content-length']);
|
||||||
|
const HEADER_ID_PREFIX = 'header';
|
||||||
const STRIP_VALUE_FLAGS = new Set(['--write-out', '-w', '--output', '-o', '--dump-header', '-D']);
|
const STRIP_VALUE_FLAGS = new Set(['--write-out', '-w', '--output', '-o', '--dump-header', '-D']);
|
||||||
const STRIP_BOOLEAN_FLAGS = new Set([
|
const STRIP_BOOLEAN_FLAGS = new Set([
|
||||||
'--silent',
|
'--silent',
|
||||||
|
|
@ -30,6 +34,10 @@ function createExecutionError(message, extra = {}) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
return typeof value === 'string' ? value.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
function skipShellIndentation(input, index) {
|
function skipShellIndentation(input, index) {
|
||||||
let cursor = index;
|
let cursor = index;
|
||||||
|
|
||||||
|
|
@ -203,6 +211,256 @@ function parseCurlCommand(command) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectHeaders(args = []) {
|
||||||
|
const headers = {};
|
||||||
|
|
||||||
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
|
const argument = args[index];
|
||||||
|
let rawHeader = '';
|
||||||
|
|
||||||
|
if (HEADER_FLAGS.has(argument) && index + 1 < args.length) {
|
||||||
|
rawHeader = String(args[index + 1] || '');
|
||||||
|
index += 1;
|
||||||
|
} else if (argument.startsWith('--header=')) {
|
||||||
|
rawHeader = argument.slice('--header='.length);
|
||||||
|
} else if (argument.startsWith('-H=')) {
|
||||||
|
rawHeader = argument.slice(3);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const separatorIndex = rawHeader.indexOf(':');
|
||||||
|
if (separatorIndex < 0) continue;
|
||||||
|
|
||||||
|
const key = normalizeText(rawHeader.slice(0, separatorIndex));
|
||||||
|
const value = normalizeText(rawHeader.slice(separatorIndex + 1));
|
||||||
|
if (!key) continue;
|
||||||
|
headers[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeHeaders(headers = {}) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(headers || {}).filter(([key]) => {
|
||||||
|
const normalizedKey = normalizeText(key).toLowerCase();
|
||||||
|
return normalizedKey && !IGNORED_HEADER_KEYS.has(normalizedKey);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataArguments(args = []) {
|
||||||
|
const dataArgs = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
|
const argument = args[index];
|
||||||
|
|
||||||
|
if (DATA_FLAGS.has(argument) && index + 1 < args.length) {
|
||||||
|
dataArgs.push(String(args[index + 1] || ''));
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flag = Array.from(DATA_FLAGS).find((entry) => argument.startsWith(`${entry}=`));
|
||||||
|
if (flag) {
|
||||||
|
dataArgs.push(argument.slice(flag.length + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMethod(args = [], dataArgs = []) {
|
||||||
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
|
const argument = args[index];
|
||||||
|
|
||||||
|
if (METHOD_FLAGS.has(argument) && index + 1 < args.length) {
|
||||||
|
return normalizeText(args[index + 1]).toUpperCase() || 'POST';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argument.startsWith('--request=')) {
|
||||||
|
return normalizeText(argument.slice('--request='.length)).toUpperCase() || 'POST';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argument.startsWith('-X=')) {
|
||||||
|
return normalizeText(argument.slice(3)).toUpperCase() || 'POST';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataArgs.length > 0 ? 'POST' : 'GET';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractUrl(args = []) {
|
||||||
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
|
const argument = String(args[index] || '');
|
||||||
|
|
||||||
|
if (argument === '--url' && index + 1 < args.length) {
|
||||||
|
return normalizeText(args[index + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argument.startsWith('--url=')) {
|
||||||
|
return normalizeText(argument.slice('--url='.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^https?:\/\//i.test(argument)) {
|
||||||
|
return normalizeText(argument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHeaderEntries(headers = {}) {
|
||||||
|
return Object.entries(sanitizeHeaders(headers)).map(([key, value], index) => ({
|
||||||
|
id: `${HEADER_ID_PREFIX}-${index}`,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
enabled: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeaderEntries(headerEntries = [], fallbackHeaders = {}) {
|
||||||
|
const normalizedEntries = [];
|
||||||
|
const seenIds = new Set();
|
||||||
|
|
||||||
|
(Array.isArray(headerEntries) ? headerEntries : []).forEach((entry, index) => {
|
||||||
|
const key = normalizeText(entry?.key);
|
||||||
|
const value = normalizeText(entry?.value);
|
||||||
|
const enabled = entry?.enabled !== false;
|
||||||
|
const fallbackId = `${HEADER_ID_PREFIX}-${index}`;
|
||||||
|
const rawId = normalizeText(entry?.id) || fallbackId;
|
||||||
|
const id = seenIds.has(rawId) ? `${rawId}-${index}` : rawId;
|
||||||
|
|
||||||
|
if (!key && !value) return;
|
||||||
|
|
||||||
|
seenIds.add(id);
|
||||||
|
normalizedEntries.push({
|
||||||
|
id,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (normalizedEntries.length > 0) {
|
||||||
|
return normalizedEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(fallbackHeaders)) {
|
||||||
|
return normalizeHeaderEntries(fallbackHeaders, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildHeaderEntries(fallbackHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRequestBlueprintFromCurl(rawCurlTemplate = '') {
|
||||||
|
const parsed = parseCurlCommand(String(rawCurlTemplate || ''));
|
||||||
|
const url = extractUrl(parsed.args);
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw createExecutionError('Stored cURL template must include an absolute http(s) URL.', {
|
||||||
|
code: 'INVALID_CURL_TEMPLATE',
|
||||||
|
status: 422,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataArgs = getDataArguments(parsed.args);
|
||||||
|
const method = extractMethod(parsed.args, dataArgs);
|
||||||
|
const headers = sanitizeHeaders(collectHeaders(parsed.args));
|
||||||
|
|
||||||
|
return {
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
headerEntries: buildHeaderEntries(headers),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteCurlValue(value = '') {
|
||||||
|
return `'${String(value ?? '').replace(/'/g, `'\\''`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeCurlTemplateFromArgs(args = []) {
|
||||||
|
return ['curl', ...(Array.isArray(args) ? args : []).map((argument) => quoteCurlValue(argument))].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPatchedCurlTemplateFromRequest(rawCurlTemplate = '', requestPatch = {}) {
|
||||||
|
const parsed = parseCurlCommand(String(rawCurlTemplate || ''));
|
||||||
|
const currentRequest = buildRequestBlueprintFromCurl(rawCurlTemplate);
|
||||||
|
const nextUrl = normalizeText(requestPatch?.url) || currentRequest.url;
|
||||||
|
const nextHeaders = normalizeHeaderEntries(
|
||||||
|
requestPatch?.headers,
|
||||||
|
currentRequest.headerEntries,
|
||||||
|
);
|
||||||
|
const nextHeaderArgs = nextHeaders
|
||||||
|
.filter((entry) => entry.enabled !== false)
|
||||||
|
.filter((entry) => normalizeText(entry.key))
|
||||||
|
.flatMap((entry) => ['--header', `${entry.key}: ${entry.value}`]);
|
||||||
|
|
||||||
|
const patchedArgs = [];
|
||||||
|
let insertedUrl = false;
|
||||||
|
let insertedHeaders = false;
|
||||||
|
|
||||||
|
for (let index = 0; index < parsed.args.length; index += 1) {
|
||||||
|
const argument = parsed.args[index];
|
||||||
|
|
||||||
|
if (HEADER_FLAGS.has(argument) && index + 1 < parsed.args.length) {
|
||||||
|
if (!insertedHeaders) {
|
||||||
|
patchedArgs.push(...nextHeaderArgs);
|
||||||
|
insertedHeaders = true;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argument.startsWith('--header=') || argument.startsWith('-H=')) {
|
||||||
|
if (!insertedHeaders) {
|
||||||
|
patchedArgs.push(...nextHeaderArgs);
|
||||||
|
insertedHeaders = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argument === '--url' && index + 1 < parsed.args.length) {
|
||||||
|
if (!insertedUrl) {
|
||||||
|
patchedArgs.push('--url', nextUrl);
|
||||||
|
insertedUrl = true;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argument.startsWith('--url=')) {
|
||||||
|
if (!insertedUrl) {
|
||||||
|
patchedArgs.push('--url', nextUrl);
|
||||||
|
insertedUrl = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^https?:\/\//i.test(String(argument || ''))) {
|
||||||
|
if (!insertedUrl) {
|
||||||
|
patchedArgs.push(nextUrl);
|
||||||
|
insertedUrl = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
patchedArgs.push(argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!insertedUrl) {
|
||||||
|
patchedArgs.push('--url', nextUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!insertedHeaders && nextHeaderArgs.length > 0) {
|
||||||
|
patchedArgs.push(...nextHeaderArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return serializeCurlTemplateFromArgs(patchedArgs);
|
||||||
|
}
|
||||||
|
|
||||||
function replaceTokensInString(value, tokenValues = {}) {
|
function replaceTokensInString(value, tokenValues = {}) {
|
||||||
let output = String(value || '');
|
let output = String(value || '');
|
||||||
const entries = Object.entries(tokenValues).sort((left, right) => right[0].length - left[0].length);
|
const entries = Object.entries(tokenValues).sort((left, right) => right[0].length - left[0].length);
|
||||||
|
|
@ -555,6 +813,9 @@ async function executeTemplatedCurl(curlTemplate, tokenValues = {}, options = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
buildPatchedCurlTemplateFromRequest,
|
||||||
|
buildRequestBlueprintFromCurl,
|
||||||
executeTemplatedCurl,
|
executeTemplatedCurl,
|
||||||
|
normalizeHeaderEntries,
|
||||||
parseCurlCommand,
|
parseCurlCommand,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user