bolt-templates-sms-extensio.../server/services/openai2.js
2026-04-09 15:30:14 +05:30

1543 lines
59 KiB
JavaScript

require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') });
const OpenAI = require('openai');
const { parseCurlCommand } = require('./curlExecutor');
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
const BRAND_LLM_MODEL = 'openai/gpt-4o';
const TEMPLATE_LLM_MODEL = 'openai/gpt-4o';
const CURL_LLM_MODEL = 'openai/gpt-4o-mini';
const EDIT_CHECK_LLM_MODEL = 'openai/gpt-4o-mini';
const DLT_VARIABLE_SPECS = [
{
token: '{#numeric#}',
label: '#numeric',
purpose: 'Digits-only dynamic values such as OTPs, amounts, or numeric IDs.',
validation: 'Only digits are allowed.',
},
{
token: '{#url#}',
label: '#url',
purpose: 'Web links.',
validation: 'Must resolve to a valid registered HTTP(S) URL.',
},
{
token: '{#urlott#}',
label: '#urlott',
purpose: 'OTT or app-download links.',
validation: 'Must resolve to a valid registered OTT or APK URL.',
},
{
token: '{#cbn#}',
label: '#cbn',
purpose: 'Callback phone numbers.',
validation: 'Must resolve to a valid registered callback number.',
},
{
token: '{#email#}',
label: '#email',
purpose: 'Email addresses.',
validation: 'Must resolve to a syntactically valid email address.',
},
{
token: '{#alphanumeric#}',
label: '#alphanumeric',
purpose: 'Mixed letter-and-number values such as order IDs or booking references.',
validation: 'Letters and numbers only; avoid spaces and special characters.',
},
];
const LEGACY_DLT_VAR_TOKEN = '{#var#}';
const SUPPORTED_DLT_TOKENS = [LEGACY_DLT_VAR_TOKEN, ...DLT_VARIABLE_SPECS.map((spec) => spec.token)];
const SUPPORTED_DLT_TOKEN_SET = new Set(SUPPORTED_DLT_TOKENS);
const DLT_PLACEHOLDER_REGEX = /\{#(?:var|numeric|url|urlott|cbn|email|alphanumeric)#\}/g;
const DLT_PLACEHOLDER_LIKE_REGEX = /\{#[^{}]*#\}/g;
const TEMPLATE_HARD_RULE_LINES = [
'Keep the SMS within 160 characters.',
`Use only approved placeholders: ${SUPPORTED_DLT_TOKENS.join(', ')}.`,
'Do not use malformed or incomplete placeholder text.',
'Do not include exact blocked brand or tagline phrases when they are provided.',
];
const TEMPLATE_GUIDANCE_RULE_LINES = [
'Keep the message transactional and relevant to the selected event.',
'Start with clear order or event context when possible.',
`Use ${LEGACY_DLT_VAR_TOKEN} whenever the safest placeholder type is uncertain.`,
`Prefer typed placeholders (${DLT_VARIABLE_SPECS.map((spec) => spec.token).join(', ')}) only when the text makes that type obvious.`,
'Avoid raw URLs unless the template genuinely requires a link.',
'Avoid extra signoffs or provider suffixes at the end of the message.',
];
const TEMPLATE_HARD_RULES_TEXT = TEMPLATE_HARD_RULE_LINES.map((rule, index) => `${index + 1}) ${rule}`).join(' ');
const TEMPLATE_GUIDANCE_RULES_TEXT = TEMPLATE_GUIDANCE_RULE_LINES.map((rule, index) => `${index + 1}) ${rule}`).join(' ');
const TEMPLATE_VALIDATION_ISSUE_CODES = new Set([
'EMPTY_TEMPLATE',
'LENGTH_EXCEEDED',
'UNSUPPORTED_DLT_TOKEN',
'MALFORMED_DLT_TOKEN',
'BLOCKED_LITERAL_PHRASE',
]);
const BRAND_CONTEXT_TONE_OPTIONS = ['friendly', 'professional', 'formal', 'casual', 'energetic'];
const EVENT_DESCRIPTIONS = {
placed: 'The customer has successfully placed an order',
confirmed: 'The order has been confirmed by the seller/warehouse',
dp_assigned: 'A delivery partner has been assigned to deliver the order',
pack: 'The order has been packed and is ready for dispatch',
cancelled: 'The order has been cancelled',
delivery_done: 'The order has been successfully delivered to the customer',
};
let cachedClient = null;
const RESERVED_RUNTIME_TOKENS = {
toNumber: '__SMS_TO_NUMBER__',
content: '__SMS_CONTENT__',
templateId: '__SMS_TEMPLATE_ID__',
senderId: '__SMS_SENDER_ID__',
dltEntityId: '__SMS_DLT_ENTITY_ID__',
};
const REQUIRED_INPUT_SOURCES = new Set(['embedded', 'profile', 'runtime']);
const CURL_DATA_FLAGS = new Set(['--data', '--data-raw', '--data-binary', '-d']);
const CURL_UNSAFE_CHECKS = [
{ pattern: /`/, reason: 'Backticks are not supported in provider cURLs.' },
{ pattern: /\$\(/, reason: 'Command substitution is not supported in provider cURLs.' },
{ pattern: /&&|\|\|/, reason: 'Command chaining is not supported in provider cURLs.' },
{ pattern: /<\(|>\(/, reason: 'Process substitution is not supported in provider cURLs.' },
{ pattern: /(?:^|\s);\s*(?:curl\b|[A-Za-z0-9_./-]+)/, reason: 'Multiple shell commands are not supported in provider cURLs.' },
{ pattern: /(?:^|\s)(?:--config|-K)\b/, reason: 'Loading external curl config files is not supported.' },
{ pattern: /(?:--data(?:-raw|-binary)?|-d)\s+@/, reason: 'Reading request bodies from local files is not supported.' },
];
const INPUT_KEY_ALIASES = {
provider: 'providerName',
providerLabel: 'providerName',
smsProvider: 'providerName',
dltSenderId: 'senderId',
sender: 'senderId',
senderCode: 'senderId',
peId: 'dltEntityId',
entityId: 'dltEntityId',
dltPeId: 'dltEntityId',
dltEntity: 'dltEntityId',
apiKey: 'authKey',
apiAuthKey: 'authKey',
accessToken: 'authToken',
bearerToken: 'authToken',
destination: 'toNumber',
destinationNumber: 'toNumber',
mobile: 'toNumber',
phone: 'toNumber',
phoneNumber: 'toNumber',
to: 'toNumber',
message: 'content',
body: 'content',
smsBody: 'content',
smsContent: 'content',
text: 'content',
dltTemplate: 'templateId',
dltTemplateId: 'templateId',
template: 'templateId',
};
function normalizeText(value) {
return typeof value === 'string' ? value.trim() : '';
}
function toCamelCase(value) {
const normalized = normalizeText(String(value || ''))
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/[^A-Za-z0-9]+/g, ' ')
.trim();
if (!normalized) return '';
const parts = normalized.split(/\s+/).filter(Boolean);
return parts
.map((part, index) => {
const lower = part.toLowerCase();
if (index === 0) return lower;
return lower.charAt(0).toUpperCase() + lower.slice(1);
})
.join('');
}
function toUpperSnakeCase(value) {
return normalizeText(String(value || ''))
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
.replace(/[^A-Za-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
.toUpperCase();
}
function humanizeKey(value) {
const camel = toCamelCase(value);
if (!camel) return '';
return camel
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/^./, (char) => char.toUpperCase());
}
function canonicalizeInputKey(value) {
const camel = toCamelCase(value);
return INPUT_KEY_ALIASES[camel] || camel;
}
function getProfileToken(key) {
return `__PROFILE_${toUpperSnakeCase(key)}__`;
}
function getRuntimeToken(key) {
return RESERVED_RUNTIME_TOKENS[canonicalizeInputKey(key)] || '';
}
function getCurlUrl(rawCurl) {
const match = String(rawCurl || '').match(/https?:\/\/[^\s'"\\]+/i);
return match ? match[0] : '';
}
function detectUnsafeCurl(rawCurl) {
const normalized = String(rawCurl || '');
if (!normalizeText(normalized).toLowerCase().startsWith('curl')) {
return 'The provided input must begin with "curl".';
}
if (!getCurlUrl(normalized)) {
return 'The provided cURL must include an absolute http(s) URL.';
}
const matchedRule = CURL_UNSAFE_CHECKS.find(({ pattern }) => pattern.test(normalized));
return matchedRule ? matchedRule.reason : '';
}
function describeDltVariableTypes() {
return DLT_VARIABLE_SPECS
.map((spec) => `- ${spec.token}: ${spec.purpose} ${spec.validation}`)
.join('\n');
}
function getUnsupportedDltTokens(text) {
return (String(text).match(DLT_PLACEHOLDER_LIKE_REGEX) || [])
.filter((token) => !SUPPORTED_DLT_TOKEN_SET.has(token));
}
function hasMalformedDltFragments(text) {
const stripped = String(text).replace(DLT_PLACEHOLDER_LIKE_REGEX, '');
return stripped.includes('{#') || stripped.includes('#}');
}
function validateTemplateStructure(text) {
const normalized = normalizeText(text);
if (!normalized) return 'Template is empty.';
if (normalized.length > 160) return 'Template exceeds 160 characters.';
const unsupportedTokens = getUnsupportedDltTokens(normalized);
if (unsupportedTokens.length > 0) {
return `Template uses unsupported placeholders: ${unsupportedTokens.join(', ')}.`;
}
if (hasMalformedDltFragments(normalized)) {
return 'Template contains malformed placeholder text.';
}
return '';
}
function escapeRegex(value) {
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function buildPhraseRegex(phrase) {
const normalized = normalizeText(phrase).replace(/\s+/g, ' ');
if (!normalized) return null;
const parts = normalized.split(' ').filter(Boolean).map(escapeRegex);
if (parts.length === 0) return null;
return new RegExp(`(^|[^a-z0-9])${parts.join('\\s+')}([^a-z0-9]|$)`, 'i');
}
function getBlockedBrandPhrases(options = {}) {
const phrases = [
options?.brandName,
...(Array.isArray(options?.brandTaglines) ? options.brandTaglines : []),
]
.map((value) => normalizeText(value))
.filter(Boolean);
return [...new Set(phrases)];
}
function findBlockedBrandPhrase(text, options = {}) {
const normalizedText = normalizeText(text);
if (!normalizedText) return '';
return getBlockedBrandPhrases(options).find((phrase) => {
const matcher = buildPhraseRegex(phrase);
return matcher ? matcher.test(normalizedText) : false;
}) || '';
}
function describeEvent(eventSlug, eventLabel) {
const normalizedSlug = normalizeText(eventSlug).toLowerCase();
if (EVENT_DESCRIPTIONS[normalizedSlug]) {
return EVENT_DESCRIPTIONS[normalizedSlug];
}
const normalizedLabel = normalizeText(eventLabel) || normalizedSlug.replace(/_/g, ' ').trim();
if (!normalizedLabel) {
return 'This is a transactional order lifecycle update.';
}
const lower = `${normalizedSlug} ${normalizedLabel.toLowerCase()}`;
if (lower.includes('refund')) {
return `This is a refund-related transactional order update for "${normalizedLabel}".`;
}
if (lower.includes('return')) {
return `This is a return-related transactional order update for "${normalizedLabel}".`;
}
if (lower.includes('payment')) {
return `This is a payment-related transactional order update for "${normalizedLabel}".`;
}
if (lower.includes('cancel')) {
return `This is a cancellation-related transactional order update for "${normalizedLabel}".`;
}
if (lower.includes('delivery') || lower.includes('transit') || lower.includes('pickup') || lower.includes('dp')) {
return `This is a logistics or delivery status update for "${normalizedLabel}".`;
}
return `This is a transactional order lifecycle update for "${normalizedLabel}".`;
}
function summarizeRejectionReasons(reasons = [], limit = 6) {
const seen = new Set();
const summarized = [];
reasons.forEach((reason) => {
const normalized = normalizeText(reason);
if (!normalized) return;
const dedupeKey = normalized.toLowerCase();
if (seen.has(dedupeKey)) return;
seen.add(dedupeKey);
summarized.push(normalized);
});
return summarized.slice(-limit);
}
function buildTemplateValidationContext(options = {}) {
return {
eventSlug: normalizeText(options?.eventSlug),
eventLabel: normalizeText(options?.eventLabel),
eventDescription: describeEvent(options?.eventSlug, options?.eventLabel),
registeredSenderId: normalizeText(options?.senderId).toUpperCase(),
blockedBrandPhrases: getBlockedBrandPhrases({
brandName: options?.brandName,
brandTaglines: options?.brandTaglines,
}),
};
}
function createValidationIssue(code, message, evidence = '') {
const normalizedCode = normalizeText(code).toUpperCase();
if (!TEMPLATE_VALIDATION_ISSUE_CODES.has(normalizedCode)) return null;
return {
code: normalizedCode,
message: sanitizeString(message),
evidence: sanitizeString(evidence),
};
}
function appendValidationIssue(issues, issue) {
if (!Array.isArray(issues) || !issue?.code || !issue?.message) return;
const dedupeKey = `${issue.code}:${issue.evidence || issue.message}`.toLowerCase();
if (issues.some((entry) => `${entry.code}:${entry.evidence || entry.message}`.toLowerCase() === dedupeKey)) return;
issues.push(issue);
}
function getLiteralTemplateIssues(templateText, validationContext = {}) {
const normalizedTemplate = sanitizeString(templateText);
const issues = [];
if (!normalizedTemplate) {
appendValidationIssue(issues, createValidationIssue(
'EMPTY_TEMPLATE',
'Template is empty.',
));
return issues;
}
if (normalizedTemplate.length > 160) {
appendValidationIssue(issues, createValidationIssue(
'LENGTH_EXCEEDED',
`Template exceeds 160 characters (${normalizedTemplate.length}/160).`,
));
}
const unsupportedTokens = getUnsupportedDltTokens(normalizedTemplate);
if (unsupportedTokens.length > 0) {
appendValidationIssue(issues, createValidationIssue(
'UNSUPPORTED_DLT_TOKEN',
`Unsupported placeholder token${unsupportedTokens.length === 1 ? '' : 's'}: ${unsupportedTokens.join(', ')}.`,
unsupportedTokens.join(', '),
));
}
if (hasMalformedDltFragments(normalizedTemplate)) {
appendValidationIssue(issues, createValidationIssue(
'MALFORMED_DLT_TOKEN',
'Template contains malformed placeholder text.',
));
}
const blockedBrandPhrase = findBlockedBrandPhrase(normalizedTemplate, {
brandName: validationContext?.blockedBrandPhrases?.[0],
brandTaglines: validationContext?.blockedBrandPhrases?.slice(1),
});
if (blockedBrandPhrase) {
appendValidationIssue(issues, createValidationIssue(
'BLOCKED_LITERAL_PHRASE',
`Remove blocked literal phrase "${blockedBrandPhrase}" from the message body.`,
blockedBrandPhrase,
));
}
return issues;
}
function normalizeIssueCode(value) {
const normalized = toUpperSnakeCase(value);
return TEMPLATE_VALIDATION_ISSUE_CODES.has(normalized) ? normalized : '';
}
function normalizeValidationIssues(rawIssues, templateText, validationContext = {}) {
const normalizedTemplate = sanitizeString(templateText);
const issues = [];
const sourceIssues = Array.isArray(rawIssues)
? rawIssues
: (rawIssues && typeof rawIssues === 'object' ? [rawIssues] : []);
const literalIssues = getLiteralTemplateIssues(normalizedTemplate, validationContext);
const literalBlockedPhraseIssue = literalIssues.find((issue) => issue.code === 'BLOCKED_LITERAL_PHRASE') || null;
const unsupportedIssue = literalIssues.find((issue) => issue.code === 'UNSUPPORTED_DLT_TOKEN') || null;
sourceIssues.forEach((rawIssue) => {
const code = normalizeIssueCode(rawIssue?.code || rawIssue?.type || rawIssue?.category);
if (!code) return;
let evidence = sanitizeString(rawIssue?.evidence || rawIssue?.text || rawIssue?.phrase);
let message = sanitizeString(rawIssue?.message || rawIssue?.reason || rawIssue?.why);
if (code === 'BLOCKED_LITERAL_PHRASE') {
if (!literalBlockedPhraseIssue) return;
evidence = literalBlockedPhraseIssue.evidence;
message = message || literalBlockedPhraseIssue.message;
} else if (code === 'LENGTH_EXCEEDED' && normalizedTemplate.length <= 160) {
return;
} else if (code === 'EMPTY_TEMPLATE' && normalizedTemplate) {
return;
} else if (code === 'UNSUPPORTED_DLT_TOKEN') {
if (!unsupportedIssue) return;
evidence = unsupportedIssue.evidence;
message = message || unsupportedIssue.message;
} else if (code === 'MALFORMED_DLT_TOKEN' && !hasMalformedDltFragments(normalizedTemplate)) {
return;
}
appendValidationIssue(issues, createValidationIssue(code, message, evidence));
});
literalIssues.forEach((issue) => appendValidationIssue(issues, issue));
return issues;
}
function summarizeValidationIssues(issues = [], limit = 3) {
return summarizeRejectionReasons(
(Array.isArray(issues) ? issues : [])
.map((issue) => sanitizeString(issue?.message))
.filter(Boolean),
limit,
);
}
function requestId(prefix) {
return `${prefix}_${Date.now()}`;
}
function extractMessageText(content) {
if (typeof content === 'string') return content.trim();
if (Array.isArray(content)) {
return content
.map((entry) => {
if (typeof entry === 'string') return entry;
if (entry && typeof entry.text === 'string') return entry.text;
return '';
})
.join('')
.trim();
}
return '';
}
function tryParseJson(text) {
const trimmed = normalizeText(text);
if (!trimmed) return null;
try {
return JSON.parse(trimmed);
} catch {
// fall through
}
const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
if (fencedMatch?.[1]) {
try {
return JSON.parse(fencedMatch[1].trim());
} catch {
// fall through
}
}
const firstBrace = trimmed.indexOf('{');
const lastBrace = trimmed.lastIndexOf('}');
if (firstBrace >= 0 && lastBrace > firstBrace) {
try {
return JSON.parse(trimmed.slice(firstBrace, lastBrace + 1));
} catch {
// fall through
}
}
return null;
}
function isAbsoluteHttpUrl(value) {
if (!normalizeText(value)) return false;
try {
const parsed = new URL(value);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch {
return false;
}
}
function getLlmClient() {
if (cachedClient) return cachedClient;
const apiKey = normalizeText(process.env.OPENROUTER_API_KEY);
if (!apiKey) {
throw new Error('OPENROUTER_API_KEY is not configured');
}
const referer = normalizeText(process.env.EXTENSION_BASE_URL);
const appName = 'SMS Extension';
const defaultHeaders = {};
if (referer) defaultHeaders['HTTP-Referer'] = referer;
if (appName) defaultHeaders['X-Title'] = appName;
cachedClient = new OpenAI({
apiKey,
baseURL: OPENROUTER_BASE_URL,
defaultHeaders,
});
return cachedClient;
}
async function requestStructuredJson({ model, taskName, systemPrompt, userPrompt, temperature = 0.2 }) {
try {
const client = getLlmClient();
const completion = await client.chat.completions.create({
model,
temperature,
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
});
const text = extractMessageText(completion?.choices?.[0]?.message?.content);
const parsed = tryParseJson(text);
if (!parsed || typeof parsed !== 'object') {
throw new Error(`${taskName} returned unreadable JSON`);
}
return parsed;
} catch (error) {
const details = error.response?.data ? ` | response: ${JSON.stringify(error.response.data)}` : '';
throw new Error(`${taskName} failed: ${error.message}${details}`);
}
}
function sanitizeStringArray(value, options = {}) {
const { maxItems = Infinity, allowUrlsOnly = false } = options;
if (!Array.isArray(value)) return [];
const seen = new Set();
const items = [];
value.forEach((entry) => {
if (items.length >= maxItems) return;
const normalized = normalizeText(String(entry || ''));
if (!normalized) return;
if (allowUrlsOnly && !isAbsoluteHttpUrl(normalized)) return;
if (seen.has(normalized)) return;
seen.add(normalized);
items.push(normalized);
});
return items;
}
function sanitizeVariableMap(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
return Object.entries(value).reduce((accumulator, [key, rawValue]) => {
const normalizedKey = normalizeText(String(key || ''));
const normalizedValue = canonicalizeInputKey(rawValue);
if (!normalizedKey || !normalizedValue) return accumulator;
accumulator[normalizedKey] = normalizedValue;
return accumulator;
}, {});
}
function sanitizeString(value) {
return normalizeText(String(value || ''));
}
function sanitizeProvider(provider = {}) {
return {
providerName: sanitizeString(provider.providerName),
senderId: sanitizeString(provider.senderId).toUpperCase(),
dltEntityId: sanitizeString(provider.dltEntityId),
authKey: sanitizeString(provider.authKey),
};
}
function buildNamedFieldAlternation(fieldNames = []) {
return fieldNames
.map((name) => normalizeText(name))
.filter(Boolean)
.map((name) => name.split(/[_-]/).filter(Boolean).map(escapeRegex).join('[_-]?'))
.filter(Boolean)
.join('|');
}
function curlContainsNamedField(rawCurl = '', fieldNames = []) {
const alternation = buildNamedFieldAlternation(fieldNames);
if (!alternation) return false;
const normalizedCurl = String(rawCurl || '');
const patterns = [
new RegExp(`["'](?:${alternation})["']\\s*:`, 'i'),
new RegExp(`(?:\\?|&)(?:${alternation})=`, 'i'),
new RegExp(`(?:--header|-H)\\s+['"][^'"]*(?:${alternation})\\s*:`, 'i'),
];
return patterns.some((pattern) => pattern.test(normalizedCurl));
}
function hasExplicitAuthKeySlot(rawCurl = '') {
return curlContainsNamedField(rawCurl, [
'authKey',
'apiKey',
'apiAuthKey',
'x-api-key',
'x-auth-key',
]);
}
function hasPasswordSlot(rawCurl = '') {
return curlContainsNamedField(rawCurl, [
'password',
'passwd',
'passcode',
]);
}
function getCurlDataArguments(rawCurl = '') {
try {
const parsed = parseCurlCommand(rawCurl);
const dataArgs = [];
for (let index = 0; index < parsed.args.length; index += 1) {
const argument = parsed.args[index];
if (CURL_DATA_FLAGS.has(argument) && index + 1 < parsed.args.length) {
dataArgs.push(String(parsed.args[index + 1] || ''));
index += 1;
continue;
}
const dataFlagWithValue = Array.from(CURL_DATA_FLAGS).find((flag) => argument.startsWith(`${flag}=`));
if (dataFlagWithValue) {
dataArgs.push(argument.slice(dataFlagWithValue.length + 1));
}
}
return dataArgs;
} catch {
return [];
}
}
function findValueInJsonPayload(payload, matcher) {
if (!payload || typeof payload !== 'object') return '';
if (Array.isArray(payload)) {
for (const entry of payload) {
const value = findValueInJsonPayload(entry, matcher);
if (value) return value;
}
return '';
}
for (const [key, value] of Object.entries(payload)) {
if (matcher(key) && value !== null && value !== undefined) {
return sanitizeString(value);
}
if (value && typeof value === 'object') {
const nestedValue = findValueInJsonPayload(value, matcher);
if (nestedValue) return nestedValue;
}
}
return '';
}
function normalizeExtractedSenderId(value) {
return sanitizeString(value)
.replace(/^['"]+|['"]+$/g, '')
.toUpperCase();
}
function extractDeterministicSenderId(rawCurl = '') {
const matchesSenderIdKey = (key) => {
const normalizedKey = normalizeText(String(key || ''));
return normalizedKey === 'sender_id'
|| normalizedKey === 'sender-id'
|| normalizedKey === 'senderId'
|| normalizedKey === 'dlt_sender_id'
|| normalizedKey === 'dlt-sender-id'
|| normalizedKey === 'dltSenderId';
};
for (const argument of getCurlDataArguments(rawCurl)) {
const trimmed = sanitizeString(argument);
if (!trimmed) continue;
if (
(trimmed.startsWith('{') && trimmed.endsWith('}'))
|| (trimmed.startsWith('[') && trimmed.endsWith(']'))
) {
try {
const parsed = JSON.parse(trimmed);
const jsonValue = findValueInJsonPayload(parsed, matchesSenderIdKey);
if (jsonValue) return normalizeExtractedSenderId(jsonValue);
} catch {
// Fall back to regex-based extraction below for partially dynamic payloads.
}
}
const directMatch = trimmed.match(/["'](?:sender[_-]?id|dlt[_-]?sender[_-]?id)["']\s*:\s*["']([^"']+)["']/i);
if (directMatch?.[1]) {
return normalizeExtractedSenderId(directMatch[1]);
}
}
const rawMatch = String(rawCurl || '').match(/["'](?:sender[_-]?id|dlt[_-]?sender[_-]?id)["']\s*:\s*["']([^"']+)["']/i);
return rawMatch?.[1] ? normalizeExtractedSenderId(rawMatch[1]) : '';
}
function sanitizeRequiredInput(value) {
if (!value || typeof value !== 'object') return null;
const key = canonicalizeInputKey(value.key || value.name || value.field || value.slot);
if (!key) return null;
const requestedSource = REQUIRED_INPUT_SOURCES.has(normalizeText(value.source).toLowerCase())
? normalizeText(value.source).toLowerCase()
: 'profile';
const source = ['toNumber', 'content', 'templateId'].includes(key)
? 'runtime'
: requestedSource;
const runtimeToken = getRuntimeToken(key);
const token = source === 'runtime'
? runtimeToken
: sanitizeString(value.token) || getProfileToken(key);
return {
key,
label: sanitizeString(value.label) || humanizeKey(key),
required: value.required !== false,
secret: value.secret === true,
source,
token,
currentValue: sanitizeString(value.currentValue || value.value),
};
}
function sanitizeRequiredInputs(value) {
if (!Array.isArray(value)) return [];
const seenKeys = new Set();
const inputs = [];
value.forEach((entry) => {
const sanitized = sanitizeRequiredInput(entry);
if (!sanitized) return;
const dedupeKey = `${sanitized.source}:${sanitized.key}`;
if (seenKeys.has(dedupeKey)) return;
seenKeys.add(dedupeKey);
inputs.push(sanitized);
});
return inputs;
}
function sanitizeSlotMap(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
return Object.entries(value).reduce((accumulator, [rawKey, rawValue]) => {
const key = canonicalizeInputKey(rawKey);
const mappedValue = canonicalizeInputKey(rawValue);
if (!key || !mappedValue) return accumulator;
accumulator[key] = mappedValue;
return accumulator;
}, {});
}
function sanitizeWarnings(value) {
return sanitizeStringArray(value, { maxItems: 10 });
}
function reconcileCredentialInputs(requiredInputs, provider, rawCurl = '') {
const inputs = sanitizeRequiredInputs(requiredInputs);
const normalizedProvider = sanitizeProvider(provider);
const usesPasswordField = hasPasswordSlot(rawCurl);
const usesAuthKeyField = hasExplicitAuthKeySlot(rawCurl);
if (!usesPasswordField || usesAuthKeyField) {
return {
provider: normalizedProvider,
requiredInputs: inputs,
};
}
const existingPasswordInput = inputs.find((input) => input.key === 'password') || null;
const existingAuthKeyInput = inputs.find((input) => input.key === 'authKey') || null;
const inferredPasswordValue = sanitizeString(
existingPasswordInput?.currentValue
|| existingAuthKeyInput?.currentValue
|| normalizedProvider.authKey
);
const nextInputs = inputs
.filter((input) => input.key !== 'authKey')
.map((input) => {
if (input.key !== 'password') return input;
return {
...input,
label: input.label || 'Password',
required: true,
secret: true,
token: sanitizeString(input.token) || getProfileToken('password'),
currentValue: sanitizeString(input.currentValue) || inferredPasswordValue,
};
});
if (!nextInputs.some((input) => input.key === 'password')) {
nextInputs.push({
key: 'password',
label: 'Password',
required: true,
secret: true,
source: 'profile',
token: getProfileToken('password'),
currentValue: inferredPasswordValue,
});
}
return {
provider: {
...normalizedProvider,
authKey: '',
},
requiredInputs: sanitizeRequiredInputs(nextInputs),
};
}
function injectMissingProviderInputs(requiredInputs, provider, rawCurl = '') {
const inputs = Array.isArray(requiredInputs) ? [...requiredInputs] : [];
const hasAuthKeyInput = inputs.some((input) => input?.key === 'authKey');
const providerAuthKey = sanitizeString(provider?.authKey);
if (!hasExplicitAuthKeySlot(rawCurl)) {
return sanitizeRequiredInputs(inputs);
}
if (!hasAuthKeyInput && providerAuthKey) {
inputs.push({
key: 'authKey',
label: 'Auth Key',
required: true,
secret: true,
source: 'embedded',
token: getProfileToken('authKey'),
currentValue: providerAuthKey,
});
}
return sanitizeRequiredInputs(inputs);
}
function applyRequiredInputTokensToCurlTemplate(rawCurlTemplate, requiredInputs = []) {
let output = sanitizeString(rawCurlTemplate);
if (!output) return '';
requiredInputs.forEach((input) => {
const currentValue = sanitizeString(input?.currentValue);
const token = sanitizeString(input?.token);
if (!currentValue || !token || !output.includes(currentValue)) return;
output = output.replace(new RegExp(escapeRegex(currentValue), 'g'), token);
});
return output;
}
function serializeExecutionRequiredInputs(requiredInputs = []) {
return sanitizeRequiredInputs(requiredInputs).map((input) => ({
key: input.key,
label: input.label,
required: input.required !== false,
secret: input.secret === true,
source: input.source,
token: input.token,
}));
}
function collectExecutionRuntimeKeys(requiredInputs = [], slotMap = {}, processedCurlTemplate = '') {
const runtimeKeys = new Set();
const normalizedSlotMap = sanitizeSlotMap(slotMap);
const normalizedCurlTemplate = sanitizeString(processedCurlTemplate);
serializeExecutionRequiredInputs(requiredInputs)
.filter((input) => input.source === 'runtime')
.forEach((input) => runtimeKeys.add(input.key));
Object.entries(normalizedSlotMap).forEach(([key, value]) => {
if (getRuntimeToken(key)) runtimeKeys.add(key);
if (getRuntimeToken(value)) runtimeKeys.add(value);
});
Object.entries(RESERVED_RUNTIME_TOKENS).forEach(([key, token]) => {
if (normalizedCurlTemplate.includes(token)) {
runtimeKeys.add(canonicalizeInputKey(key));
}
});
return Array.from(runtimeKeys);
}
function buildExecutionMeta({ approvedTemplate, eventSlug, requiredInputs = [], slotMap = {}, processedCurlTemplate = '' }) {
const placeholderTokens = sanitizeString(approvedTemplate).match(DLT_PLACEHOLDER_REGEX) || [];
const serializedRequiredInputs = serializeExecutionRequiredInputs(requiredInputs);
const runtimeInputKeys = collectExecutionRuntimeKeys(serializedRequiredInputs, slotMap, processedCurlTemplate);
const profileInputKeys = serializedRequiredInputs
.filter((input) => input.source !== 'runtime')
.map((input) => input.key);
return {
eventSlug: sanitizeString(eventSlug),
renderStrategy: 'deterministic_sample_payload',
placeholderCount: placeholderTokens.length,
placeholderTokens,
runtimeInputKeys,
profileInputKeys,
hasContentToken: sanitizeString(processedCurlTemplate).includes(RESERVED_RUNTIME_TOKENS.content),
hasToNumberToken: sanitizeString(processedCurlTemplate).includes(RESERVED_RUNTIME_TOKENS.toNumber),
hasTemplateIdToken: sanitizeString(processedCurlTemplate).includes(RESERVED_RUNTIME_TOKENS.templateId),
};
}
async function parseBrandContext(scrapedData = {}) {
const representativePages = Array.isArray(scrapedData.representativePages)
? scrapedData.representativePages.slice(0, 20)
: [];
const representativeTextBlocks = Array.isArray(scrapedData.representativeTextBlocks)
? scrapedData.representativeTextBlocks.slice(0, 20)
: [];
const productPages = Array.isArray(scrapedData.productPages)
? scrapedData.productPages.slice(0, 5)
: [];
const contentDigest = representativeTextBlocks
.map((block) => {
const title = String(block?.title || '').trim();
const pageType = String(block?.pageType || '').trim();
const text = String(block?.text || '').trim();
return [title, pageType, text].filter(Boolean).join(' | ');
})
.filter(Boolean)
.join('\n\n')
.slice(0, 14000);
const result = await requestStructuredJson({
model: BRAND_LLM_MODEL,
taskName: 'Brand context extraction',
temperature: 0.2,
systemPrompt: 'You are a brand analyst for ecommerce storefronts. Infer brand identity from crawl evidence and return only valid JSON that matches the requested schema exactly.',
userPrompt: [
'Analyze the storefront evidence below and infer brand context.',
'',
'Return only valid JSON with exactly these keys:',
'{',
' "brandName": "string",',
` "tone": "one of ${BRAND_CONTEXT_TONE_OPTIONS.join(', ')}",`,
' "taglines": ["up to 3 strings"],',
' "colors": ["hex colors only"],',
' "logoUrl": "absolute http(s) URL for the best company logo or empty string",',
' "relevantImageUrls": ["3-5 absolute http(s) image URLs only"],',
' "aboutSummary": "2-4 concise customer-facing sentences"',
'}',
'',
'Constraints:',
'- No markdown.',
'- No explanatory prose.',
'- Do not copy the About page verbatim.',
'- `logoUrl` must be a real brand logo or wordmark when one is available. Prefer storefront/header logos from branding_json.logos.',
'- If no credible logo is present, return an empty string for `logoUrl`.',
'- Exclude icons, tracking pixels, and data URLs from images.',
'',
`start_url: ${String(scrapedData.startUrl || '')}`,
`domain: ${String(scrapedData.domain || '')}`,
`site_stats_json: ${JSON.stringify(scrapedData.siteStats || {})}`,
`homepage_json: ${JSON.stringify(scrapedData.homepage || {})}`,
`about_page_json: ${JSON.stringify(scrapedData.aboutPage || {})}`,
`product_pages_json: ${JSON.stringify(productPages)}`,
`contact_page_json: ${JSON.stringify(scrapedData.contactPage || {})}`,
`representative_pages_json: ${JSON.stringify(representativePages)}`,
`representative_text_blocks_json: ${JSON.stringify(representativeTextBlocks)}`,
`navigation_json: ${JSON.stringify(scrapedData.navigation || [])}`,
`policy_pages_json: ${JSON.stringify(scrapedData.policyPages || [])}`,
`links_json: ${JSON.stringify(scrapedData.links || [])}`,
`top_images_json: ${JSON.stringify(scrapedData.topImages || [])}`,
`screenshots_json: ${JSON.stringify(scrapedData.screenshots || [])}`,
`branding_json: ${JSON.stringify(scrapedData.branding || {})}`,
`crawl_summary_json: ${JSON.stringify(scrapedData || {})}`,
`content_digest: ${contentDigest}`,
].join('\n'),
});
const normalizedTone = normalizeText(String(result.tone || '')).toLowerCase();
const normalizedLogoUrl = normalizeText(String(result.logoUrl || ''));
return {
brandName: normalizeText(String(result.brandName || '')) || 'Unknown Brand',
tone: BRAND_CONTEXT_TONE_OPTIONS.includes(normalizedTone) ? normalizedTone : 'professional',
taglines: sanitizeStringArray(result.taglines, { maxItems: 3 }),
colors: sanitizeStringArray(result.colors),
logoUrl: isAbsoluteHttpUrl(normalizedLogoUrl) ? normalizedLogoUrl : '',
relevantImageUrls: sanitizeStringArray(result.relevantImageUrls, { maxItems: 5, allowUrlsOnly: true }),
aboutSummary: normalizeText(String(result.aboutSummary || '')),
};
}
async function getTemplateApprovalOutcome(templateText, options = {}) {
const validation = await validateEditedTemplate(templateText, options);
return {
approved: validation.approved,
reason: validation.why || 'Template did not pass compliance validation.',
issues: Array.isArray(validation.issues) ? validation.issues : [],
source: 'llm',
};
}
async function repairRejectedTemplate(rejectedTemplate, rejectionReason, options = {}) {
const validationContext = buildTemplateValidationContext(options);
try {
const result = await requestStructuredJson({
model: TEMPLATE_LLM_MODEL,
taskName: 'Rejected template repair',
temperature: 0.15,
systemPrompt: [
'You repair one Indian transactional SMS template so it becomes compliant and clear.',
'Keep the same event meaning.',
'Fix only what is necessary.',
'Return only valid JSON.',
].join(' '),
userPrompt: [
'Rewrite this rejected SMS template so it passes compliance validation.',
'',
`Rejected template:\n${sanitizeString(rejectedTemplate)}`,
'',
`Why it was rejected: ${sanitizeString(rejectionReason) || 'It did not pass compliance validation.'}`,
'',
validationContext.eventSlug ? `Event slug: ${validationContext.eventSlug}` : '',
validationContext.eventLabel ? `Event label: ${validationContext.eventLabel}` : '',
`Event description: ${validationContext.eventDescription}`,
`Registered sender ID: ${validationContext.registeredSenderId || 'Not provided. Do not append any sender signature.'}`,
'',
`Hard rules:\n${TEMPLATE_HARD_RULES_TEXT}`,
'',
`Guidance:\n${TEMPLATE_GUIDANCE_RULES_TEXT}`,
'',
'Approved placeholder types:',
describeDltVariableTypes(),
`- ${LEGACY_DLT_VAR_TOKEN}: Use this when the exact typed placeholder is uncertain or the value is free-form text.`,
'',
'Repair rules:',
'- keep the same event meaning',
'- stay under 160 characters',
'- start with clear order or event context',
'- describe only the provided lifecycle stage; do not drift into a later or different order status',
'- do not mention the brand name, tagline, sender ID, or any brand-style signoff in the body',
'- do not use raw URLs unless the event truly requires a link',
'- if the correct placeholder type is uncertain, prefer {#var#}',
'- do not add explanations or notes',
validationContext.blockedBrandPhrases.length > 0
? `- specifically do not include these literal phrases: ${validationContext.blockedBrandPhrases.join(', ')}`
: '',
'',
'Return only valid JSON with exactly this shape:',
'{ "template": "rewritten compliant SMS" }',
].filter(Boolean).join('\n'),
});
return sanitizeString(result.template);
} catch {
return '';
}
}
async function generateTemplates(brandContext = {}, eventSlug, eventLabel, options = {}) {
const validationContext = buildTemplateValidationContext({
...options,
eventSlug,
eventLabel,
brandName: brandContext?.brandName,
brandTaglines: brandContext?.taglines,
});
const approvedTemplates = [];
const seenTemplates = new Set();
const rejectionReasons = [];
const validationOptions = {
senderId: validationContext.registeredSenderId,
eventSlug,
eventLabel,
brandName: brandContext?.brandName,
brandTaglines: brandContext?.taglines,
};
const attemptTemplateCounts = [8, 10, 12];
let repairAttempts = 0;
const maxRepairAttempts = 6;
for (let attempt = 0; attempt < attemptTemplateCounts.length && approvedTemplates.length < 3; attempt += 1) {
const templateCount = attemptTemplateCounts[attempt];
const recentRejectionReasons = summarizeRejectionReasons(rejectionReasons);
const result = await requestStructuredJson({
model: TEMPLATE_LLM_MODEL,
taskName: 'SMS template generation',
temperature: 0.35,
systemPrompt: [
'You are an expert in Indian transactional SMS templates.',
'Follow the rules exactly, self-check every candidate before returning it, and return only valid JSON.',
'Never include a brand name, tagline, sender signature, or signoff in the message body.',
'If the correct typed placeholder is uncertain, use {#var#}.',
].join(' '),
userPrompt: [
`Generate exactly ${templateCount} distinct transactional SMS templates.`,
'',
`Brand: ${String(brandContext.brandName || '')}`,
`Tone: ${String(brandContext.tone || '')}`,
`Taglines: ${JSON.stringify(Array.isArray(brandContext.taglines) ? brandContext.taglines : [])}`,
`Event slug: ${String(eventSlug || '')}`,
`Event label: ${String(eventLabel || '')}`,
`Event description: ${validationContext.eventDescription}`,
`Registered sender ID: ${validationContext.registeredSenderId || 'Not provided. Do not append any brand or sender signature.'}`,
'',
`Hard rules:\n${TEMPLATE_HARD_RULES_TEXT}`,
'',
`Guidance:\n${TEMPLATE_GUIDANCE_RULES_TEXT}`,
'',
'Approved placeholder types:',
describeDltVariableTypes(),
`- ${LEGACY_DLT_VAR_TOKEN}: Generic fallback for free-form values such as customer names, product names, or addresses when a stricter typed token does not fit or the exact type is uncertain.`,
'',
'Each template must:',
'- be under 160 characters',
'- use only the approved placeholder tokens',
'- do not include blocked literal brand/tagline phrases',
'- follow the guidance when possible without breaking readability',
'- self-check that each draft already satisfies every rule before returning it',
validationContext.blockedBrandPhrases.length > 0
? `- specifically do not include these literal phrases: ${validationContext.blockedBrandPhrases.join(', ')}`
: '',
'',
recentRejectionReasons.length > 0
? `Avoid these issues seen in rejected drafts: ${recentRejectionReasons.join(' | ')}`
: '',
'',
'Return only valid JSON with exactly this shape:',
`{ "templates": ["template 1", "template 2", "... up to ${templateCount} templates"] }`,
].filter(Boolean).join('\n'),
});
const candidateTemplates = sanitizeStringArray(result.templates, { maxItems: templateCount });
for (const candidate of candidateTemplates) {
if (approvedTemplates.length >= 3) break;
const normalizedCandidate = sanitizeString(candidate);
if (!normalizedCandidate || seenTemplates.has(normalizedCandidate)) continue;
seenTemplates.add(normalizedCandidate);
const validation = await getTemplateApprovalOutcome(normalizedCandidate, validationOptions);
if (validation.approved) {
approvedTemplates.push(normalizedCandidate);
continue;
}
const issueSummary = summarizeValidationIssues(validation.issues, 2);
if (issueSummary.length > 0) {
rejectionReasons.push(...issueSummary);
} else if (validation.reason) {
rejectionReasons.push(validation.reason);
}
if (repairAttempts >= maxRepairAttempts) continue;
const repairedCandidate = await repairRejectedTemplate(
normalizedCandidate,
issueSummary.join(' | ') || validation.reason,
validationOptions,
);
const normalizedRepairedCandidate = sanitizeString(repairedCandidate);
if (!normalizedRepairedCandidate || seenTemplates.has(normalizedRepairedCandidate)) continue;
repairAttempts += 1;
seenTemplates.add(normalizedRepairedCandidate);
const repairedValidation = await getTemplateApprovalOutcome(normalizedRepairedCandidate, validationOptions);
if (repairedValidation.approved) {
approvedTemplates.push(normalizedRepairedCandidate);
continue;
}
const repairedIssueSummary = summarizeValidationIssues(repairedValidation.issues, 2);
if (repairedIssueSummary.length > 0) {
rejectionReasons.push(...repairedIssueSummary);
} else if (repairedValidation.reason) {
rejectionReasons.push(repairedValidation.reason);
}
}
}
if (approvedTemplates.length < 3) {
const recentRejectionReasons = summarizeRejectionReasons(rejectionReasons, 3);
const reasonSuffix = recentRejectionReasons.length > 0
? ` Last blockers: ${recentRejectionReasons.join(' | ')}`
: ' Please try again.';
throw new Error(`Could not generate 3 compliant templates.${reasonSuffix}`);
}
return approvedTemplates.slice(0, 3);
}
async function processCurl(rawCurl, approvedTemplate, eventSlug, options = {}) {
const normalizedApprovedTemplate = sanitizeString(approvedTemplate);
const normalizedCurlTemplate = applyRequiredInputTokensToCurlTemplate(
sanitizeString(options.normalizedCurlTemplate) || sanitizeString(rawCurl),
options.requiredInputs,
) || sanitizeString(options.normalizedCurlTemplate) || sanitizeString(rawCurl);
const requiredInputs = serializeExecutionRequiredInputs(options.requiredInputs);
const slotMap = sanitizeSlotMap(options.slotMap);
const executionMeta = buildExecutionMeta({
approvedTemplate: normalizedApprovedTemplate,
eventSlug,
requiredInputs,
slotMap,
processedCurlTemplate: normalizedCurlTemplate,
});
if (!normalizedApprovedTemplate) {
return {
processedCurl: normalizedCurlTemplate,
processedCurlTemplate: normalizedCurlTemplate,
variableMap: {},
requiredInputs,
slotMap,
executionMeta,
};
}
const placeholderMatches = normalizedApprovedTemplate.match(DLT_PLACEHOLDER_REGEX) || [];
if (placeholderMatches.length === 0) {
return {
processedCurl: normalizedCurlTemplate,
processedCurlTemplate: normalizedCurlTemplate,
variableMap: {},
requiredInputs,
slotMap,
executionMeta,
};
}
const placeholderKeys = placeholderMatches.map((token, index) => `${token}[${index}]`);
const result = await requestStructuredJson({
model: CURL_LLM_MODEL,
taskName: 'Template execution mapping',
temperature: 0.1,
systemPrompt: [
'You infer semantic shipment field mappings for DLT SMS placeholders.',
'Return only valid JSON.',
'Do not modify the cURL template or invent provider fields.',
'Mapped values must be concise camelCase shipment field names such as customerName, orderId, trackingUrl, otp, refundAmount, or callbackNumber.',
].join(' '),
userPrompt: [
'Map each DLT placeholder in the approved SMS template to the shipment field name that should populate it.',
'',
`Approved SMS template:\n${normalizedApprovedTemplate}`,
'',
`Event slug: ${sanitizeString(eventSlug)}`,
'',
`Normalized provider cURL template:\n${normalizedCurlTemplate}`,
'',
`Provider slot map:\n${JSON.stringify(slotMap, null, 2)}`,
'',
`Execution required inputs:\n${JSON.stringify(requiredInputs, null, 2)}`,
'',
'Placeholder appearance order:',
...placeholderKeys.map((key) => `- ${key}`),
'',
'Rules:',
`- supported DLT token types are ${SUPPORTED_DLT_TOKENS.join(', ')}`,
'- build variableMap using the exact positional placeholder keys shown above',
'- map to shipment semantics, not provider parameter names',
'- do not include explanations or extra keys',
'',
'Return only valid JSON with exactly this shape:',
'{',
' "variableMap": { "{#var#}[0]": "customerName", "{#numeric#}[1]": "otp" }',
'}',
].join('\n'),
});
return {
processedCurl: normalizedCurlTemplate,
processedCurlTemplate: normalizedCurlTemplate,
variableMap: sanitizeVariableMap(result.variableMap),
requiredInputs,
slotMap,
executionMeta,
};
}
async function validateEditedTemplate(editedTemplate, options = {}) {
const normalizedTemplate = sanitizeString(editedTemplate);
const validationContext = buildTemplateValidationContext(options);
const literalIssues = getLiteralTemplateIssues(normalizedTemplate, validationContext);
const result = await requestStructuredJson({
model: EDIT_CHECK_LLM_MODEL,
taskName: 'Edited template validation',
temperature: 0,
systemPrompt: [
'You validate one Indian transactional SMS template using a fixed rule set.',
'Return only valid JSON.',
'Reject only when a hard rule is actually broken.',
'Guidance items must never cause rejection.',
'Do not invent violations that are not visible in the template text.',
'If you use BLOCKED_LITERAL_PHRASE, the evidence must be an exact blocked phrase from the template.',
'Prefer the smallest set of hard-rule issues necessary to explain the rejection.',
].join(' '),
userPrompt: [
'Review this edited SMS template and decide whether it should be approved.',
'',
`Template:\n${normalizedTemplate}`,
'',
validationContext.eventSlug ? `Event slug: ${validationContext.eventSlug}` : '',
validationContext.eventLabel ? `Event label: ${validationContext.eventLabel}` : '',
`Event description: ${validationContext.eventDescription}`,
`Registered sender ID: ${validationContext.registeredSenderId || 'Not provided. Reject appended brand or sender signatures.'}`,
validationContext.blockedBrandPhrases.length > 0
? `Blocked literal phrases: ${validationContext.blockedBrandPhrases.join(', ')}`
: 'Blocked literal phrases: none provided',
'',
`Hard rules:\n${TEMPLATE_HARD_RULES_TEXT}`,
'',
`Guidance (do not reject on guidance alone):\n${TEMPLATE_GUIDANCE_RULES_TEXT}`,
'',
'Approved placeholder types:',
describeDltVariableTypes(),
`- ${LEGACY_DLT_VAR_TOKEN}: Generic fallback for free-form values such as names, product names, or addresses when a stricter typed token does not fit or the exact type is uncertain.`,
'',
'Issue codes you may use:',
'- EMPTY_TEMPLATE: the template is empty',
'- LENGTH_EXCEEDED: the template is longer than 160 characters',
'- UNSUPPORTED_DLT_TOKEN: a placeholder token is not one of the approved DLT tokens',
'- MALFORMED_DLT_TOKEN: placeholder text is incomplete or malformed',
'- BLOCKED_LITERAL_PHRASE: an exact blocked literal phrase appears in the template',
'',
'Return approved=true only when there are no issues.',
'Return approved=false when any issue exists.',
'Each issue must contain a concise user-facing message.',
'Use evidence only when you can quote the exact text from the template.',
'',
'Return only valid JSON with exactly this shape:',
'{',
' "approved": true,',
' "issues": [',
' {',
' "code": "BLOCKED_LITERAL_PHRASE",',
' "message": "Remove blocked literal phrase \\"Rare Beauty\\" from the message body.",',
' "evidence": "Rare Beauty"',
' }',
' ]',
'}',
].join('\n'),
});
const llmIssues = normalizeValidationIssues(result.issues, normalizedTemplate, validationContext);
const issues = [];
literalIssues.forEach((issue) => appendValidationIssue(issues, issue));
llmIssues.forEach((issue) => appendValidationIssue(issues, issue));
return {
approved: issues.length === 0,
issues,
why: issues[0]?.message || '',
workflowResult: result,
};
}
async function validateCurlFields(rawCurl) {
const normalizedCurl = sanitizeString(rawCurl);
const deterministicFailure = detectUnsafeCurl(normalizedCurl);
if (deterministicFailure) {
return {
isValidCurl: false,
reason: deterministicFailure,
provider: sanitizeProvider(),
requiredInputs: [],
slotMap: {},
authMode: '',
warnings: [],
normalizedCurlTemplate: '',
};
}
const result = await requestStructuredJson({
model: CURL_LLM_MODEL,
taskName: 'Provider cURL validation',
temperature: 0,
systemPrompt: [
'You analyze SMS provider curl commands and return only valid JSON.',
'Do not invent missing provider details.',
'Preserve the provider request structure in normalizedCurlTemplate.',
'Profile-level values that should be stored separately must use __PROFILE_<KEY>__ tokens.',
'Do not rename a request field literally named "password" to authKey.',
'Only use provider.authKey when the request explicitly contains an auth-key or api-key field/header.',
'Do not duplicate one credential as both password and authKey.',
'Runtime SMS values must use only these reserved tokens when applicable:',
`- toNumber => ${RESERVED_RUNTIME_TOKENS.toNumber}`,
`- content => ${RESERVED_RUNTIME_TOKENS.content}`,
`- templateId => ${RESERVED_RUNTIME_TOKENS.templateId}`,
`- senderId => ${RESERVED_RUNTIME_TOKENS.senderId}`,
`- dltEntityId => ${RESERVED_RUNTIME_TOKENS.dltEntityId}`,
].join('\n'),
userPrompt: [
'Analyze this provider cURL for SMS sending setup.',
'',
`cURL:\n${normalizedCurl}`,
'',
'Classify fields as follows:',
'- source "embedded" when the value already appears literally in the cURL and can be extracted now',
'- source "profile" when the value must be filled and stored on the profile',
'- source "runtime" when the value changes per send or per whitelisting flow',
'- if the request field is literally named "password", keep it as key "password"',
'- only use key "authKey" when the request explicitly has an auth-key or api-key field/header',
'- never return the same secret twice as both "password" and "authKey"',
'',
'Provider summary fields should be extracted when available:',
'- providerName',
'- senderId',
'- dltEntityId',
'- authKey',
'',
'Return only valid JSON with exactly this shape:',
'{',
' "isValidCurl": true,',
' "reason": "",',
' "provider": {',
' "providerName": "string",',
' "senderId": "string",',
' "dltEntityId": "string",',
' "authKey": "string"',
' },',
' "authMode": "string",',
' "requiredInputs": [',
' {',
' "key": "string",',
' "label": "string",',
' "required": true,',
' "secret": false,',
' "source": "embedded|profile|runtime",',
' "token": "string",',
' "currentValue": "string"',
' }',
' ],',
' "slotMap": {',
' "toNumber": "string",',
' "content": "string",',
' "templateId": "string"',
' },',
' "warnings": ["string"],',
' "normalizedCurlTemplate": "string"',
'}',
].join('\n'),
});
const rawSenderId = extractDeterministicSenderId(normalizedCurl);
const reconciled = reconcileCredentialInputs(result.requiredInputs, result.provider, normalizedCurl);
const provider = sanitizeProvider({
...reconciled.provider,
...(rawSenderId ? { senderId: rawSenderId } : {}),
});
const requiredInputs = injectMissingProviderInputs(reconciled.requiredInputs, provider, normalizedCurl);
const normalizedCurlTemplate = applyRequiredInputTokensToCurlTemplate(
sanitizeString(result.normalizedCurlTemplate) || normalizedCurl,
requiredInputs,
) || normalizedCurl;
const isValidCurl = result.isValidCurl !== false;
return {
isValidCurl,
reason: sanitizeString(result.reason),
provider,
requiredInputs,
slotMap: sanitizeSlotMap(result.slotMap),
authMode: sanitizeString(result.authMode),
warnings: sanitizeWarnings(result.warnings),
normalizedCurlTemplate,
url: getCurlUrl(normalizedCurl),
};
}
module.exports = {
parseBrandContext,
generateTemplates,
processCurl,
validateEditedTemplate,
validateCurlFields,
};