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 Sidebar from './components/Sidebar';
|
||||
import Businesses from './pages/Businesses';
|
||||
import Providers from './pages/Providers';
|
||||
import GlobalSms from './pages/GlobalSms';
|
||||
import Analytics from './pages/Analytics';
|
||||
import Events from './pages/Events';
|
||||
import Templates from './pages/Templates';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function SubLayout({ children }) {
|
||||
const { activeBusiness, activeBusinessId, hasGlobalSms } = useBusiness();
|
||||
const { activeBusiness, activeBusinessId } = useBusiness();
|
||||
const [reviewBusiness, setReviewBusiness] = useState(null);
|
||||
const [reviewLoading, setReviewLoading] = useState(false);
|
||||
const [reviewError, setReviewError] = useState('');
|
||||
|
|
@ -48,17 +46,7 @@ function SubLayout({ children }) {
|
|||
reviewError={reviewError}
|
||||
/>
|
||||
<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">
|
||||
{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>
|
||||
<header className="h-16 border-b border-border-main bg-white shrink-0" />
|
||||
<div className="flex-1 p-5 overflow-auto">
|
||||
{children}
|
||||
</div>
|
||||
|
|
@ -106,9 +94,6 @@ export default function App() {
|
|||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Businesses />} />
|
||||
<Route path="/:businessId/settings" element={
|
||||
<BusinessGuard><SubLayout><Providers /></SubLayout></BusinessGuard>
|
||||
} />
|
||||
<Route path="/:businessId/global-sms" element={
|
||||
<BusinessGuard isGlobalSmsRoute><SubLayout><GlobalSms /></SubLayout></BusinessGuard>
|
||||
} />
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ export default function Sidebar({ onOpenReview, reviewLoading = false, reviewErr
|
|||
activeBusiness,
|
||||
activeBusinessId,
|
||||
clearBusiness,
|
||||
hasGlobalSms,
|
||||
isSetupComplete,
|
||||
hasSelectedTemplates,
|
||||
} = useBusiness();
|
||||
|
|
@ -88,13 +87,6 @@ export default function Sidebar({ onOpenReview, reviewLoading = false, reviewErr
|
|||
const isEventsRoute = location.pathname === eventsPath;
|
||||
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 = [
|
||||
{
|
||||
id: 'globalSms',
|
||||
|
|
@ -103,8 +95,7 @@ export default function Sidebar({ onOpenReview, reviewLoading = false, reviewErr
|
|||
enabled: true,
|
||||
done: isSetupComplete && !isGlobalSmsRoute,
|
||||
active: isGlobalSmsRoute,
|
||||
expanded: isGlobalSmsRoute,
|
||||
substeps: omniSubsteps,
|
||||
expanded: false,
|
||||
},
|
||||
{
|
||||
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 { buildBrandContextPlan, collectBrandContextPages } = require('../services/firecrawl');
|
||||
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 {
|
||||
uploadJSON,
|
||||
|
|
@ -127,6 +133,7 @@ async function findBusinessByBrandName(merchantId, brandName) {
|
|||
const PROVIDER_FIELDS = ['providerName', 'senderId', 'dltEntityId'];
|
||||
const BASE_PROFILE_INPUT_KEYS = ['providerName', 'senderId', 'dltEntityId'];
|
||||
const MASKED_SECRET = '••••••••';
|
||||
const SENSITIVE_HEADER_KEYS = new Set(['authorization', 'proxy-authorization', 'x-api-key', 'api-key', 'x-auth-key']);
|
||||
const RUNTIME_TOKEN_MAP = {
|
||||
toNumber: '__SMS_TO_NUMBER__',
|
||||
content: '__SMS_CONTENT__',
|
||||
|
|
@ -203,6 +210,135 @@ function firstNonEmptyResolvedText(...values) {
|
|||
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) {
|
||||
return normalizeText(value).toUpperCase();
|
||||
}
|
||||
|
|
@ -366,7 +502,11 @@ function normalizeRequiredInput(input = {}) {
|
|||
? 'runtime'
|
||||
: requestedSource,
|
||||
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') {
|
||||
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]);
|
||||
|
|
@ -483,7 +627,13 @@ function getProfileInputDefinitions(profile = {}) {
|
|||
label: normalized.label || current?.label || humanizeInputKey(normalized.key),
|
||||
required: normalized.required !== false || current?.required === 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),
|
||||
normalized.currentValue,
|
||||
current?.currentValue
|
||||
|
|
@ -496,7 +646,7 @@ function getProfileInputDefinitions(profile = {}) {
|
|||
|
||||
function serializeProfileInput(profile, input, options = {}) {
|
||||
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);
|
||||
|
||||
return {
|
||||
|
|
@ -515,7 +665,7 @@ function serializeProfileInput(profile, input, options = {}) {
|
|||
function getMissingProfileInputs(profile = {}) {
|
||||
return getProfileInputDefinitions(profile)
|
||||
.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));
|
||||
}
|
||||
|
||||
|
|
@ -538,7 +688,7 @@ function buildDisplayCurl(profile = {}, options = {}) {
|
|||
inputs.forEach((input) => {
|
||||
const token = normalizeText(input.token);
|
||||
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;
|
||||
output = output.split(token).join(input.secret && !revealSecrets ? MASKED_SECRET : value);
|
||||
});
|
||||
|
|
@ -546,6 +696,249 @@ function buildDisplayCurl(profile = {}, options = {}) {
|
|||
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 = {}) {
|
||||
const curlAnalysis = normalizeCurlAnalysis(profile.curlAnalysis, profile.provider);
|
||||
return {
|
||||
|
|
@ -593,6 +986,7 @@ function serializeProfile(profile = {}) {
|
|||
maskedCurl: buildDisplayCurl(hydratedProfile),
|
||||
curlAnalysis: serializeCurlAnalysis(hydratedProfile),
|
||||
profileInputs: getProfileInputDefinitions(hydratedProfile).map((input) => serializeProfileInput(hydratedProfile, input)),
|
||||
requestPreview: serializeRequestPreview(hydratedProfile),
|
||||
executionReadiness: getExecutionReadiness(hydratedProfile),
|
||||
};
|
||||
}
|
||||
|
|
@ -603,6 +997,7 @@ function getProfileRevealPayload(profile = {}) {
|
|||
rawCurl: buildDisplayCurl(hydratedProfile, { revealSecrets: true }),
|
||||
profileInputs: getProfileInputDefinitions(hydratedProfile)
|
||||
.map((input) => serializeProfileInput(hydratedProfile, input, { revealSecrets: true })),
|
||||
requestPreview: serializeRequestPreview(hydratedProfile, { revealSecrets: true }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -627,7 +1022,8 @@ function sanitizeStoredCurlAnalysis(profile = {}) {
|
|||
|
||||
function persistableProfile(profile = {}) {
|
||||
const hydratedProfile = syncAutomaticProfileName(hydrateProfile(profile));
|
||||
const normalizedAuthKey = firstNonEmptyResolvedText(
|
||||
const normalizedAuthKey = firstNonEmptyInputValue(
|
||||
{ key: 'authKey', label: 'Auth Key', secret: true },
|
||||
hydratedProfile.profileInputValues?.authKey,
|
||||
hydratedProfile.provider?.authKey,
|
||||
);
|
||||
|
|
@ -1813,7 +2209,7 @@ function buildExecutionTokenValues(boundProfile = {}, executionSnapshot = {}, ru
|
|||
|
||||
getProfileInputDefinitions(boundProfile).forEach((input) => {
|
||||
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) {
|
||||
tokenValues[token] = value;
|
||||
}
|
||||
|
|
@ -1829,7 +2225,7 @@ function buildExecutionTokenValues(boundProfile = {}, executionSnapshot = {}, ru
|
|||
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;
|
||||
});
|
||||
|
||||
|
|
@ -2737,7 +3133,7 @@ function buildStoredProfileFromValidation(baseProfile = {}, validation = {}) {
|
|||
});
|
||||
|
||||
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) {
|
||||
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 { name, rawCurl } = req.body;
|
||||
const profilePatch = collectProfileInputPatch(req.body);
|
||||
const requestPatch = collectRequestPatch(req.body);
|
||||
|
||||
if (name !== undefined && !normalizeText(name)) {
|
||||
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);
|
||||
if (requestPatch) {
|
||||
validateRequestPatch(requestPatch);
|
||||
}
|
||||
|
||||
const bizRoot = businessRoot(getCompanyId(req), businessId);
|
||||
const { profileData } = await getProfileState(bizRoot);
|
||||
const profile = profileData.profiles.find(p => p.id === profileId);
|
||||
if (!profile) return res.status(404).json({ error: 'Profile not found' });
|
||||
const profileIndex = profileData.profiles.findIndex((item) => item.id === profileId);
|
||||
if (profileIndex < 0) return res.status(404).json({ error: 'Profile not found' });
|
||||
let profile = profileData.profiles[profileIndex];
|
||||
|
||||
if (name !== undefined) {
|
||||
profile.name = normalizeText(name);
|
||||
profile.isAutoNamed = false;
|
||||
}
|
||||
if (requestPatch) {
|
||||
profile = await applyRequestPatch(profile, requestPatch);
|
||||
profileData.profiles[profileIndex] = profile;
|
||||
}
|
||||
applyProfileInputPatch(profile, profilePatch);
|
||||
|
||||
await saveGlobalSmsProfiles(bizRoot, profileData);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ const STATUS_MARKER = '__CODEX_HTTP_STATUS__:';
|
|||
const DEFAULT_TIMEOUT_MS = 30000;
|
||||
const MAX_CAPTURE_LENGTH = 1024 * 1024;
|
||||
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_BOOLEAN_FLAGS = new Set([
|
||||
'--silent',
|
||||
|
|
@ -30,6 +34,10 @@ function createExecutionError(message, extra = {}) {
|
|||
return error;
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function skipShellIndentation(input, 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 = {}) {
|
||||
let output = String(value || '');
|
||||
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 = {
|
||||
buildPatchedCurlTemplateFromRequest,
|
||||
buildRequestBlueprintFromCurl,
|
||||
executeTemplatedCurl,
|
||||
normalizeHeaderEntries,
|
||||
parseCurlCommand,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user