push to remote first for entire testing

This commit is contained in:
Ritul Jadhav 2026-04-30 12:22:56 +05:30
parent d322fbe2d4
commit cf78cee0db
5 changed files with 2063 additions and 366 deletions

View File

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

View File

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

View File

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

View File

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