@infuro/cms-core 1.0.31 → 1.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/admin.cjs +929 -345
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.js +947 -362
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +839 -59
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +833 -53
- package/dist/api.js.map +1 -1
- package/dist/auth.cjs +159 -14
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +37 -22
- package/dist/auth.d.ts +37 -22
- package/dist/auth.js +153 -14
- package/dist/auth.js.map +1 -1
- package/dist/cli.cjs +3 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/{index-Bda8D1Rm.d.ts → index-DWd5Gjc4.d.ts} +2 -2
- package/dist/{index-BcMRGNjS.d.cts → index-rZTnpK7y.d.cts} +2 -2
- package/dist/index.cjs +1141 -254
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -30
- package/dist/index.d.ts +41 -30
- package/dist/index.js +1135 -254
- package/dist/index.js.map +1 -1
- package/dist/migrations/1778660000003-AddQrTokenToOrders.ts +32 -0
- package/dist/migrations/1779100000000-ChatConversationLeadEmailSent.ts +16 -0
- package/dist/migrations/1779200000000-LlmAgentScope.ts +72 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -2237,6 +2237,58 @@ function escapeHtml7(s) {
|
|
|
2237
2237
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2238
2238
|
}
|
|
2239
2239
|
|
|
2240
|
+
// src/plugins/email/templates/chatLead.ts
|
|
2241
|
+
function render10(ctx) {
|
|
2242
|
+
const {
|
|
2243
|
+
contactName,
|
|
2244
|
+
contactEmail,
|
|
2245
|
+
contactPhone,
|
|
2246
|
+
conversationId,
|
|
2247
|
+
latestMessage,
|
|
2248
|
+
intentCode,
|
|
2249
|
+
intentReason,
|
|
2250
|
+
transcript,
|
|
2251
|
+
companyDetails
|
|
2252
|
+
} = ctx;
|
|
2253
|
+
const intentDisplay = intentCode?.trim() ? `${intentCode.trim()}${intentReason?.trim() ? ` \u2014 ${intentReason.trim()}` : ""}` : intentReason;
|
|
2254
|
+
const subject = `Chat lead${intentCode?.trim() ? ` [${intentCode.trim()}]` : ""}: ${contactName || contactEmail || "Visitor"}`;
|
|
2255
|
+
const transcriptBlock = transcript?.trim() ? `<div style="margin-top:16px;padding:12px;background:#f9fafb;border-radius:6px;border:1px solid #e5e7eb;">
|
|
2256
|
+
<p style="margin:0 0 8px 0;font-size:13px;font-weight:600;color:#374151;">Recent conversation</p>
|
|
2257
|
+
<pre style="margin:0;font-size:12px;white-space:pre-wrap;font-family:inherit;color:#4b5563;">${escapeHtml8(transcript.trim())}</pre>
|
|
2258
|
+
</div>` : "";
|
|
2259
|
+
const bodyHtml = `<p style="margin:0 0 6px 0;font-size:18px;font-weight:600;color:#111;">New chat lead</p>
|
|
2260
|
+
<p style="margin:0 0 12px 0;font-size:14px;color:#374151;">A visitor showed interest based on your intent rules.</p>
|
|
2261
|
+
<table style="width:100%;border-collapse:collapse;font-size:14px;">
|
|
2262
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Name</td><td>${escapeHtml8(contactName || "\u2014")}</td></tr>
|
|
2263
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Email</td><td><a href="mailto:${escapeHtml8(contactEmail)}">${escapeHtml8(contactEmail)}</a></td></tr>
|
|
2264
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Phone</td><td>${escapeHtml8(contactPhone?.trim() || "\u2014")}</td></tr>
|
|
2265
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Conversation</td><td>#${conversationId}</td></tr>
|
|
2266
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Intent</td><td>${escapeHtml8(intentReason)}</td></tr>
|
|
2267
|
+
</table>
|
|
2268
|
+
<p style="margin:16px 0 6px 0;font-size:13px;font-weight:600;color:#374151;">Latest message</p>
|
|
2269
|
+
<p style="margin:0;font-size:14px;white-space:pre-wrap;">${escapeHtml8(latestMessage)}</p>
|
|
2270
|
+
${transcriptBlock}`;
|
|
2271
|
+
const text = [
|
|
2272
|
+
"New chat lead",
|
|
2273
|
+
`Name: ${contactName || "\u2014"}`,
|
|
2274
|
+
`Email: ${contactEmail}`,
|
|
2275
|
+
`Phone: ${contactPhone?.trim() || "\u2014"}`,
|
|
2276
|
+
`Conversation: #${conversationId}`,
|
|
2277
|
+
`Intent: ${intentDisplay}`,
|
|
2278
|
+
"",
|
|
2279
|
+
"Latest message:",
|
|
2280
|
+
latestMessage,
|
|
2281
|
+
transcript?.trim() ? `
|
|
2282
|
+
Recent conversation:
|
|
2283
|
+
${transcript.trim()}` : ""
|
|
2284
|
+
].join("\n");
|
|
2285
|
+
const html = renderLayout({ bodyHtml, companyDetails });
|
|
2286
|
+
return { subject, html, text };
|
|
2287
|
+
}
|
|
2288
|
+
function escapeHtml8(s) {
|
|
2289
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2240
2292
|
// src/plugins/email/templates/index.ts
|
|
2241
2293
|
var templateRenderMap = {
|
|
2242
2294
|
signup: render,
|
|
@@ -2247,7 +2299,8 @@ var templateRenderMap = {
|
|
|
2247
2299
|
shippingUpdate: render6,
|
|
2248
2300
|
invite: render7,
|
|
2249
2301
|
formSubmission: render8,
|
|
2250
|
-
otp: render9
|
|
2302
|
+
otp: render9,
|
|
2303
|
+
chatLead: render10
|
|
2251
2304
|
};
|
|
2252
2305
|
function getTemplateRenderer(name) {
|
|
2253
2306
|
return templateRenderMap[name];
|
|
@@ -2264,11 +2317,11 @@ function renderEmail(templateName, ctx, options) {
|
|
|
2264
2317
|
return { subject: custom.subject, html, text: custom.text };
|
|
2265
2318
|
}
|
|
2266
2319
|
}
|
|
2267
|
-
const
|
|
2268
|
-
if (!
|
|
2320
|
+
const render11 = getTemplateRenderer(templateName);
|
|
2321
|
+
if (!render11) {
|
|
2269
2322
|
throw new Error(`Unknown email template: ${templateName}`);
|
|
2270
2323
|
}
|
|
2271
|
-
return
|
|
2324
|
+
return render11(ctx);
|
|
2272
2325
|
}
|
|
2273
2326
|
|
|
2274
2327
|
// src/plugins/email/email-service.ts
|
|
@@ -2340,6 +2393,10 @@ var EmailService = class {
|
|
|
2340
2393
|
renderTemplate(templateName, ctx) {
|
|
2341
2394
|
return renderEmail(templateName, ctx, this.templateOptions);
|
|
2342
2395
|
}
|
|
2396
|
+
/** Default notification recipient from plugin config (SMTP_TO / plugin `to`). */
|
|
2397
|
+
getDefaultTo() {
|
|
2398
|
+
return this.config.to;
|
|
2399
|
+
}
|
|
2343
2400
|
};
|
|
2344
2401
|
var emailTemplates = {
|
|
2345
2402
|
formSubmission: (data) => ({
|
|
@@ -2370,6 +2427,434 @@ This link expires in 1 hour.`
|
|
|
2370
2427
|
|
|
2371
2428
|
// src/plugins/email/index.ts
|
|
2372
2429
|
init_email_queue();
|
|
2430
|
+
|
|
2431
|
+
// src/plugins/email/chat-lead-email.ts
|
|
2432
|
+
init_email_queue();
|
|
2433
|
+
|
|
2434
|
+
// src/lib/email-recipients.ts
|
|
2435
|
+
function parseEmailRecipientsFromConfig(raw) {
|
|
2436
|
+
if (raw == null || raw === "") return [];
|
|
2437
|
+
const trimmed = raw.trim();
|
|
2438
|
+
if (trimmed.startsWith("[")) {
|
|
2439
|
+
try {
|
|
2440
|
+
const parsed = JSON.parse(trimmed);
|
|
2441
|
+
if (Array.isArray(parsed)) {
|
|
2442
|
+
return parsed.map((e) => String(e).trim()).filter(Boolean);
|
|
2443
|
+
}
|
|
2444
|
+
} catch {
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
return trimmed.split(/[,;]+/).map((s) => s.trim()).filter(Boolean);
|
|
2448
|
+
}
|
|
2449
|
+
function serializeEmailRecipients(emails) {
|
|
2450
|
+
return JSON.stringify(emails);
|
|
2451
|
+
}
|
|
2452
|
+
function joinRecipientsForSend(emails) {
|
|
2453
|
+
if (!emails.length) return null;
|
|
2454
|
+
return emails.join(", ");
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/plugins/llm/chat-email-intent.ts
|
|
2458
|
+
var NONE_INTENT = "NONE";
|
|
2459
|
+
var CHAT_EMAIL_LOG = "[chat-email-tool]";
|
|
2460
|
+
function logChatEmail(step, data) {
|
|
2461
|
+
if (data && Object.keys(data).length > 0) {
|
|
2462
|
+
console.info(CHAT_EMAIL_LOG, step, data);
|
|
2463
|
+
} else {
|
|
2464
|
+
console.info(CHAT_EMAIL_LOG, step);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
function normalizeIntentKey(raw) {
|
|
2468
|
+
return raw.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2469
|
+
}
|
|
2470
|
+
function parseIntentsJson(raw) {
|
|
2471
|
+
if (!raw?.trim()) return null;
|
|
2472
|
+
try {
|
|
2473
|
+
const parsed = JSON.parse(raw);
|
|
2474
|
+
if (!Array.isArray(parsed)) return null;
|
|
2475
|
+
const out = [];
|
|
2476
|
+
for (const row of parsed) {
|
|
2477
|
+
if (!row || typeof row !== "object") continue;
|
|
2478
|
+
const o = row;
|
|
2479
|
+
const intent = normalizeIntentKey(String(o.intent ?? o.id ?? ""));
|
|
2480
|
+
const description = String(o.description ?? "").trim();
|
|
2481
|
+
const emailTo = String(o.emailTo ?? o.email ?? "").trim();
|
|
2482
|
+
if (!intent || !description || !emailTo) continue;
|
|
2483
|
+
out.push({ intent, description, emailTo });
|
|
2484
|
+
}
|
|
2485
|
+
return out.length > 0 ? out : null;
|
|
2486
|
+
} catch {
|
|
2487
|
+
return null;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
function dedupeIntents(intents) {
|
|
2491
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2492
|
+
const out = [];
|
|
2493
|
+
for (const row of intents) {
|
|
2494
|
+
const key = normalizeIntentKey(row.intent);
|
|
2495
|
+
if (!key || seen.has(key)) continue;
|
|
2496
|
+
seen.add(key);
|
|
2497
|
+
out.push({
|
|
2498
|
+
intent: key,
|
|
2499
|
+
description: row.description.trim(),
|
|
2500
|
+
emailTo: row.emailTo.trim()
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
return out;
|
|
2504
|
+
}
|
|
2505
|
+
function parseChatEmailToolSettings(map) {
|
|
2506
|
+
const fromJson = parseIntentsJson(map.emailIntents);
|
|
2507
|
+
const intents = dedupeIntents(fromJson ?? []);
|
|
2508
|
+
const legacyPositive = map.emailIntentPrompt?.trim() ?? "";
|
|
2509
|
+
const legacyNegative = map.emailNegativeIntentPrompt?.trim() ?? "";
|
|
2510
|
+
let classifierInstructions = map.emailClassifierInstructions?.trim() ?? "";
|
|
2511
|
+
if (!classifierInstructions && (legacyPositive || legacyNegative)) {
|
|
2512
|
+
const parts = [];
|
|
2513
|
+
if (legacyPositive) parts.push(`Legacy positive signals:
|
|
2514
|
+
${legacyPositive}`);
|
|
2515
|
+
if (legacyNegative) parts.push(`Do NOT assign an intent when:
|
|
2516
|
+
${legacyNegative}`);
|
|
2517
|
+
classifierInstructions = parts.join("\n\n");
|
|
2518
|
+
}
|
|
2519
|
+
return {
|
|
2520
|
+
enabled: map.emailToolEnabled === "true",
|
|
2521
|
+
intents,
|
|
2522
|
+
classifierInstructions,
|
|
2523
|
+
toolPrompt: map.emailToolPrompt ?? ""
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
var EMAIL_TOOL_VALIDATION_KEY = "emailTool";
|
|
2527
|
+
function parseEmailToolObject(raw) {
|
|
2528
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
2529
|
+
const o = raw;
|
|
2530
|
+
const intentsRaw = o.intents;
|
|
2531
|
+
let intents;
|
|
2532
|
+
if (Array.isArray(intentsRaw)) {
|
|
2533
|
+
const parsed = parseIntentsJson(JSON.stringify(intentsRaw));
|
|
2534
|
+
if (parsed?.length) intents = parsed;
|
|
2535
|
+
}
|
|
2536
|
+
return {
|
|
2537
|
+
enabled: o.enabled === true,
|
|
2538
|
+
classifierInstructions: typeof o.classifierInstructions === "string" ? o.classifierInstructions.trim() : void 0,
|
|
2539
|
+
toolPrompt: typeof o.toolPrompt === "string" ? o.toolPrompt.trim() : void 0,
|
|
2540
|
+
intents
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
function parseEmailToolFromAgentValidationRules(validationRulesText) {
|
|
2544
|
+
const raw = validationRulesText?.trim();
|
|
2545
|
+
if (!raw) return null;
|
|
2546
|
+
try {
|
|
2547
|
+
const parsed = JSON.parse(raw);
|
|
2548
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
2549
|
+
const emailTool = parsed[EMAIL_TOOL_VALIDATION_KEY];
|
|
2550
|
+
return parseEmailToolObject(emailTool);
|
|
2551
|
+
} catch {
|
|
2552
|
+
return null;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
function resolveChatEmailToolSettings(configMap, sources) {
|
|
2556
|
+
const fromConfig = parseChatEmailToolSettings(configMap);
|
|
2557
|
+
const resolved = typeof sources === "string" || sources == null ? { chatbotValidationRules: typeof sources === "string" ? sources : null } : sources;
|
|
2558
|
+
const fromChatbot = parseEmailToolFromAgentValidationRules(resolved.chatbotValidationRules);
|
|
2559
|
+
const fromNotify = resolved.notifyAgent ? parseEmailToolFromAgentValidationRules(resolved.notifyAgent.validationRules) : null;
|
|
2560
|
+
const notifySystem = resolved.notifyAgent?.systemInstruction?.trim() ?? "";
|
|
2561
|
+
const intents = fromNotify?.intents?.length ? fromNotify.intents : fromChatbot?.intents?.length ? fromChatbot.intents : fromConfig.intents;
|
|
2562
|
+
const classifierParts = [
|
|
2563
|
+
notifySystem,
|
|
2564
|
+
fromNotify?.classifierInstructions?.trim(),
|
|
2565
|
+
fromChatbot?.classifierInstructions?.trim(),
|
|
2566
|
+
fromConfig.classifierInstructions.trim()
|
|
2567
|
+
].filter(Boolean);
|
|
2568
|
+
const toolPrompt = fromNotify?.toolPrompt?.trim() || fromChatbot?.toolPrompt?.trim() || fromConfig.toolPrompt?.trim() || "";
|
|
2569
|
+
return {
|
|
2570
|
+
enabled: fromConfig.enabled,
|
|
2571
|
+
intents,
|
|
2572
|
+
classifierInstructions: classifierParts.join("\n\n"),
|
|
2573
|
+
toolPrompt
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
function intentByKey(intents, key) {
|
|
2577
|
+
if (!key) return null;
|
|
2578
|
+
const norm3 = normalizeIntentKey(key);
|
|
2579
|
+
return intents.find((i) => i.intent === norm3) ?? null;
|
|
2580
|
+
}
|
|
2581
|
+
function buildIntentTableForPrompt(intents) {
|
|
2582
|
+
return intents.map((i) => `- ${i.intent}: ${i.description} \u2192 notify ${i.emailTo}`).join("\n");
|
|
2583
|
+
}
|
|
2584
|
+
function buildIntentClassifierSystem(config, agentClassifierInstructions) {
|
|
2585
|
+
const table = buildIntentTableForPrompt(config.intents);
|
|
2586
|
+
const extra = [agentClassifierInstructions?.trim(), config.classifierInstructions.trim()].filter(Boolean).join("\n\n");
|
|
2587
|
+
const intentKeys = config.intents.map((i) => i.intent).join(", ");
|
|
2588
|
+
return `You classify a chat visitor into exactly one lead intent for email routing.
|
|
2589
|
+
|
|
2590
|
+
Available intents (pick the best match):
|
|
2591
|
+
${table}
|
|
2592
|
+
|
|
2593
|
+
If the visitor is not ready for a lead email (general FAQ, greetings only, spam, off-topic, no clear business need), use intent "${NONE_INTENT}".
|
|
2594
|
+
|
|
2595
|
+
Allowed intent values: ${intentKeys}, or ${NONE_INTENT}.
|
|
2596
|
+
|
|
2597
|
+
${extra ? `Additional instructions:
|
|
2598
|
+
${extra}
|
|
2599
|
+
` : ""}
|
|
2600
|
+
Reply with ONLY valid JSON, no markdown:
|
|
2601
|
+
{"intent":"INTENT_KEY","reason":"short explanation"}
|
|
2602
|
+
Use "${NONE_INTENT}" when no intent applies.`;
|
|
2603
|
+
}
|
|
2604
|
+
function buildChatEmailAgentContext(settings) {
|
|
2605
|
+
if (!settings.enabled) return "";
|
|
2606
|
+
const classifier = settings.classifierInstructions.trim();
|
|
2607
|
+
const extra = settings.toolPrompt?.trim() ?? "";
|
|
2608
|
+
const parts = [];
|
|
2609
|
+
if (settings.intents.length > 0) {
|
|
2610
|
+
parts.push(
|
|
2611
|
+
"## Lead email routing",
|
|
2612
|
+
"When a visitor message clearly matches an intent below, the system sends one team notification email (per conversation). Continue the conversation normally in all cases."
|
|
2613
|
+
);
|
|
2614
|
+
if (classifier) parts.push(`### Routing rules
|
|
2615
|
+
${classifier}`);
|
|
2616
|
+
parts.push(`### Intent catalog
|
|
2617
|
+
${buildIntentTableForPrompt(settings.intents)}`);
|
|
2618
|
+
} else if (classifier) {
|
|
2619
|
+
parts.push(`## Lead email routing
|
|
2620
|
+
${classifier}`);
|
|
2621
|
+
}
|
|
2622
|
+
if (extra) parts.push(`### Assistant behavior
|
|
2623
|
+
${extra}`);
|
|
2624
|
+
return parts.join("\n\n");
|
|
2625
|
+
}
|
|
2626
|
+
function buildChatbotSystemPromptWithEmailTool(baseSystemInstruction, guardrailsForPrompt, emailSettings) {
|
|
2627
|
+
let prompt = (baseSystemInstruction ?? "").trim();
|
|
2628
|
+
const guard = guardrailsForPrompt?.trim();
|
|
2629
|
+
if (guard) prompt = [prompt, guard].filter(Boolean).join("\n\n");
|
|
2630
|
+
if (emailSettings.enabled) {
|
|
2631
|
+
const emailCtx = buildChatEmailAgentContext(emailSettings);
|
|
2632
|
+
if (emailCtx) prompt = [prompt, emailCtx].filter(Boolean).join("\n\n");
|
|
2633
|
+
}
|
|
2634
|
+
return prompt;
|
|
2635
|
+
}
|
|
2636
|
+
function formatTranscript(history, latestMessage, maxChars = 4e3) {
|
|
2637
|
+
const lines = [];
|
|
2638
|
+
for (const m of history) {
|
|
2639
|
+
const role = m.role === "assistant" ? "Assistant" : m.role === "user" ? "Visitor" : "System";
|
|
2640
|
+
lines.push(`${role}: ${m.content}`);
|
|
2641
|
+
}
|
|
2642
|
+
const last = history[history.length - 1];
|
|
2643
|
+
if (!last || last.role !== "user" || last.content !== latestMessage) {
|
|
2644
|
+
lines.push(`Visitor: ${latestMessage}`);
|
|
2645
|
+
}
|
|
2646
|
+
let text = lines.join("\n");
|
|
2647
|
+
if (text.length > maxChars) text = text.slice(-maxChars);
|
|
2648
|
+
return text;
|
|
2649
|
+
}
|
|
2650
|
+
function parseIntentJson(raw, intents) {
|
|
2651
|
+
const trimmed = raw.trim();
|
|
2652
|
+
const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
2653
|
+
const candidate = (fence?.[1] ?? trimmed).trim();
|
|
2654
|
+
const tryParse = (s) => {
|
|
2655
|
+
const o = JSON.parse(s);
|
|
2656
|
+
let intentRaw = null;
|
|
2657
|
+
if (typeof o.intent === "string") intentRaw = o.intent;
|
|
2658
|
+
else if (o.interested === false) intentRaw = NONE_INTENT;
|
|
2659
|
+
else if (o.interested === true && intents[0]) intentRaw = intents[0].intent;
|
|
2660
|
+
if (intentRaw == null) return null;
|
|
2661
|
+
const reason = typeof o.reason === "string" && o.reason.trim() ? o.reason.trim() : "Classified from conversation";
|
|
2662
|
+
return { intent: normalizeIntentKey(intentRaw), reason };
|
|
2663
|
+
};
|
|
2664
|
+
try {
|
|
2665
|
+
return tryParse(candidate);
|
|
2666
|
+
} catch {
|
|
2667
|
+
const m = candidate.match(/\{[\s\S]*\}/);
|
|
2668
|
+
if (!m) return null;
|
|
2669
|
+
try {
|
|
2670
|
+
return tryParse(m[0]);
|
|
2671
|
+
} catch {
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
async function detectChatLeadIntent(llm, params) {
|
|
2677
|
+
if (!params.settings.intents.length) {
|
|
2678
|
+
logChatEmail("classify skipped", { reason: "no_intents_configured" });
|
|
2679
|
+
return {
|
|
2680
|
+
intent: null,
|
|
2681
|
+
intentLabel: "NONE",
|
|
2682
|
+
reason: "No lead intents configured",
|
|
2683
|
+
emailTo: null
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
logChatEmail("classify start", {
|
|
2687
|
+
intentCount: params.settings.intents.length,
|
|
2688
|
+
intentKeys: params.settings.intents.map((i) => i.intent),
|
|
2689
|
+
model: params.model?.trim() || "(gateway default)",
|
|
2690
|
+
messageChars: params.message.length,
|
|
2691
|
+
historyTurns: params.history.length,
|
|
2692
|
+
hasClassifierInstructions: Boolean(
|
|
2693
|
+
params.agentClassifierInstructions?.trim() || params.settings.classifierInstructions.trim()
|
|
2694
|
+
)
|
|
2695
|
+
});
|
|
2696
|
+
const transcript = formatTranscript(params.history, params.message);
|
|
2697
|
+
const contactLines = [
|
|
2698
|
+
params.contact.name?.trim() ? `Name: ${params.contact.name.trim()}` : null,
|
|
2699
|
+
params.contact.email?.trim() ? `Email: ${params.contact.email.trim()}` : null,
|
|
2700
|
+
params.contact.phone?.trim() ? `Phone: ${params.contact.phone.trim()}` : null
|
|
2701
|
+
].filter(Boolean).join("\n");
|
|
2702
|
+
const userPrompt = `Customer:
|
|
2703
|
+
${contactLines || "(unknown)"}
|
|
2704
|
+
|
|
2705
|
+
Conversation:
|
|
2706
|
+
${transcript}
|
|
2707
|
+
|
|
2708
|
+
Pick the single best intent for the visitor's latest message.`;
|
|
2709
|
+
const res = await llm.chatAgent({
|
|
2710
|
+
systemPrompt: buildIntentClassifierSystem(params.settings, params.agentClassifierInstructions),
|
|
2711
|
+
userPrompt,
|
|
2712
|
+
temperature: 0.1,
|
|
2713
|
+
max_tokens: 256,
|
|
2714
|
+
...params.model?.trim() ? { model: params.model.trim() } : {}
|
|
2715
|
+
});
|
|
2716
|
+
const rawContent = res.content ?? "";
|
|
2717
|
+
logChatEmail("classify llm response", {
|
|
2718
|
+
responseChars: rawContent.length,
|
|
2719
|
+
responsePreview: rawContent.slice(0, 280) + (rawContent.length > 280 ? "\u2026" : "")
|
|
2720
|
+
});
|
|
2721
|
+
const parsed = parseIntentJson(rawContent, params.settings.intents);
|
|
2722
|
+
if (!parsed) {
|
|
2723
|
+
logChatEmail("classify result", {
|
|
2724
|
+
matched: false,
|
|
2725
|
+
outcome: "parse_failed",
|
|
2726
|
+
emailWillSend: false
|
|
2727
|
+
});
|
|
2728
|
+
return {
|
|
2729
|
+
intent: null,
|
|
2730
|
+
intentLabel: "Unclassified",
|
|
2731
|
+
reason: "Could not parse intent classifier response",
|
|
2732
|
+
emailTo: null
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
if (parsed.intent === NONE_INTENT) {
|
|
2736
|
+
logChatEmail("classify result", {
|
|
2737
|
+
matched: false,
|
|
2738
|
+
outcome: NONE_INTENT,
|
|
2739
|
+
reason: parsed.reason,
|
|
2740
|
+
emailWillSend: false
|
|
2741
|
+
});
|
|
2742
|
+
return {
|
|
2743
|
+
intent: null,
|
|
2744
|
+
intentLabel: NONE_INTENT,
|
|
2745
|
+
reason: parsed.reason,
|
|
2746
|
+
emailTo: null
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
const matched = intentByKey(params.settings.intents, parsed.intent);
|
|
2750
|
+
if (!matched) {
|
|
2751
|
+
logChatEmail("classify result", {
|
|
2752
|
+
matched: false,
|
|
2753
|
+
outcome: "unknown_intent",
|
|
2754
|
+
parsedIntent: parsed.intent,
|
|
2755
|
+
reason: parsed.reason,
|
|
2756
|
+
emailWillSend: false
|
|
2757
|
+
});
|
|
2758
|
+
return {
|
|
2759
|
+
intent: null,
|
|
2760
|
+
intentLabel: parsed.intent,
|
|
2761
|
+
reason: `Unknown intent "${parsed.intent}": ${parsed.reason}`,
|
|
2762
|
+
emailTo: null
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
logChatEmail("classify result", {
|
|
2766
|
+
matched: true,
|
|
2767
|
+
outcome: "intent_found",
|
|
2768
|
+
intent: matched.intent,
|
|
2769
|
+
emailTo: matched.emailTo,
|
|
2770
|
+
reason: parsed.reason,
|
|
2771
|
+
emailWillSend: true
|
|
2772
|
+
});
|
|
2773
|
+
return {
|
|
2774
|
+
intent: matched.intent,
|
|
2775
|
+
intentLabel: `${matched.intent} \u2014 ${matched.description}`,
|
|
2776
|
+
reason: parsed.reason,
|
|
2777
|
+
emailTo: matched.emailTo
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
function buildTranscriptForLeadEmail(history, latestMessage) {
|
|
2781
|
+
return formatTranscript(history, latestMessage);
|
|
2782
|
+
}
|
|
2783
|
+
function parseIntentRecipientEmails(emailTo) {
|
|
2784
|
+
return emailTo.split(/[,;]+/).map((s) => s.trim()).filter((s) => s.length > 0 && s.includes("@"));
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// src/plugins/email/chat-lead-email.ts
|
|
2788
|
+
function resolveLeadRecipients(emailPlugin2, emailSettings) {
|
|
2789
|
+
const fromCrm = parseEmailRecipientsFromConfig(
|
|
2790
|
+
emailSettings.crmEmails ?? emailSettings.crmEmail ?? ""
|
|
2791
|
+
);
|
|
2792
|
+
if (fromCrm.length > 0) return fromCrm;
|
|
2793
|
+
const fallback = emailPlugin2.getDefaultTo?.() ?? "";
|
|
2794
|
+
if (fallback.trim()) return [fallback.trim()];
|
|
2795
|
+
return [];
|
|
2796
|
+
}
|
|
2797
|
+
async function sendChatLeadEmail(cms, emailSettings, brandingSettings, input) {
|
|
2798
|
+
console.info(CHAT_EMAIL_LOG, "send start", {
|
|
2799
|
+
conversationId: input.conversationId,
|
|
2800
|
+
intentCode: input.intentCode ?? null,
|
|
2801
|
+
contactEmail: input.contactEmail,
|
|
2802
|
+
contactName: input.contactName,
|
|
2803
|
+
explicitRecipients: input.recipients?.length ?? 0
|
|
2804
|
+
});
|
|
2805
|
+
const email = cms.getPlugin("email");
|
|
2806
|
+
if (!email?.send || !email.renderTemplate) {
|
|
2807
|
+
console.warn(CHAT_EMAIL_LOG, "send failed", {
|
|
2808
|
+
conversationId: input.conversationId,
|
|
2809
|
+
reason: "email_plugin_disabled"
|
|
2810
|
+
});
|
|
2811
|
+
return { sent: false, recipients: [], error: "Email plugin is not enabled" };
|
|
2812
|
+
}
|
|
2813
|
+
const recipients = input.recipients?.filter((r) => r.trim().includes("@")).map((r) => r.trim()) ?? resolveLeadRecipients(email, emailSettings);
|
|
2814
|
+
if (recipients.length === 0) {
|
|
2815
|
+
console.warn(CHAT_EMAIL_LOG, "send failed", {
|
|
2816
|
+
conversationId: input.conversationId,
|
|
2817
|
+
reason: "no_recipients"
|
|
2818
|
+
});
|
|
2819
|
+
return {
|
|
2820
|
+
sent: false,
|
|
2821
|
+
recipients: [],
|
|
2822
|
+
error: "No lead email recipients (configure intent Email To, CRM emails, or SMTP default To)"
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
console.info(CHAT_EMAIL_LOG, "send recipients resolved", {
|
|
2826
|
+
conversationId: input.conversationId,
|
|
2827
|
+
recipients,
|
|
2828
|
+
source: input.recipients?.length ? "intent_email_to" : "crm_or_smtp_fallback"
|
|
2829
|
+
});
|
|
2830
|
+
const companyDetails = mergeEmailLayoutCompanyDetails(brandingSettings, emailSettings);
|
|
2831
|
+
const ctx = {
|
|
2832
|
+
...input,
|
|
2833
|
+
companyDetails: input.companyDetails ?? companyDetails
|
|
2834
|
+
};
|
|
2835
|
+
let anySent = false;
|
|
2836
|
+
for (const to of recipients) {
|
|
2837
|
+
await queueEmail(cms, {
|
|
2838
|
+
to,
|
|
2839
|
+
templateName: "chatLead",
|
|
2840
|
+
ctx
|
|
2841
|
+
});
|
|
2842
|
+
console.info(CHAT_EMAIL_LOG, "send queued", {
|
|
2843
|
+
conversationId: input.conversationId,
|
|
2844
|
+
to,
|
|
2845
|
+
template: "chatLead"
|
|
2846
|
+
});
|
|
2847
|
+
anySent = true;
|
|
2848
|
+
}
|
|
2849
|
+
console.info(CHAT_EMAIL_LOG, "send complete", {
|
|
2850
|
+
conversationId: input.conversationId,
|
|
2851
|
+
sent: anySent,
|
|
2852
|
+
recipientCount: recipients.length
|
|
2853
|
+
});
|
|
2854
|
+
return { sent: anySent, recipients };
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
// src/plugins/email/index.ts
|
|
2373
2858
|
function emailPlugin(config) {
|
|
2374
2859
|
return {
|
|
2375
2860
|
name: "email",
|
|
@@ -3513,6 +3998,56 @@ ${context}`;
|
|
|
3513
3998
|
}
|
|
3514
3999
|
};
|
|
3515
4000
|
|
|
4001
|
+
// src/plugins/llm/llm-agent-scope.ts
|
|
4002
|
+
var LLM_AGENT_SCOPE_CHATBOT = "chatbot";
|
|
4003
|
+
var LLM_AGENT_SCOPE_BLOG_CREATION = "blog_creation";
|
|
4004
|
+
var LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST = "social_media_post";
|
|
4005
|
+
var LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT = "email_intent_chatbot";
|
|
4006
|
+
var LLM_AGENT_SCOPE_BLOG_METADATA = "blog_metadata";
|
|
4007
|
+
var LLM_AGENT_SCOPES = [
|
|
4008
|
+
LLM_AGENT_SCOPE_CHATBOT,
|
|
4009
|
+
LLM_AGENT_SCOPE_BLOG_CREATION,
|
|
4010
|
+
LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST,
|
|
4011
|
+
LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT,
|
|
4012
|
+
LLM_AGENT_SCOPE_BLOG_METADATA
|
|
4013
|
+
];
|
|
4014
|
+
var LLM_AGENT_DEFAULT_SLUG_BY_SCOPE = {
|
|
4015
|
+
[LLM_AGENT_SCOPE_CHATBOT]: "site-chat-assistant",
|
|
4016
|
+
[LLM_AGENT_SCOPE_BLOG_CREATION]: "blog-generator",
|
|
4017
|
+
[LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]: "blog-generator-social",
|
|
4018
|
+
[LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT]: "email-intent-chatbot",
|
|
4019
|
+
[LLM_AGENT_SCOPE_BLOG_METADATA]: "blog-generator-metadata"
|
|
4020
|
+
};
|
|
4021
|
+
var LLM_AGENT_DEFAULT_NAME_BY_SCOPE = {
|
|
4022
|
+
[LLM_AGENT_SCOPE_CHATBOT]: "Site Chat Assistant",
|
|
4023
|
+
[LLM_AGENT_SCOPE_BLOG_CREATION]: "Blog Generator",
|
|
4024
|
+
[LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]: "Blog Generator (Social)",
|
|
4025
|
+
[LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT]: "Chat Lead Intent Classifier",
|
|
4026
|
+
[LLM_AGENT_SCOPE_BLOG_METADATA]: "Blog Generator (Metadata)"
|
|
4027
|
+
};
|
|
4028
|
+
var SITE_CHAT_ASSISTANT_SLUG = LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
|
|
4029
|
+
var SITE_CHAT_ASSISTANT_NAME = LLM_AGENT_DEFAULT_NAME_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
|
|
4030
|
+
function isLlmAgentScope(value) {
|
|
4031
|
+
if (!value?.trim()) return false;
|
|
4032
|
+
return LLM_AGENT_SCOPES.includes(value.trim());
|
|
4033
|
+
}
|
|
4034
|
+
var BLOG_LLM_AGENT_SLUGS = [
|
|
4035
|
+
LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_BLOG_CREATION],
|
|
4036
|
+
LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_BLOG_METADATA],
|
|
4037
|
+
LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]
|
|
4038
|
+
];
|
|
4039
|
+
|
|
4040
|
+
// src/plugins/llm/find-llm-agent-by-scope.ts
|
|
4041
|
+
async function findLlmAgentByScope(dataSource, entityMap, scope, options = {}) {
|
|
4042
|
+
const { enabledOnly = true } = options;
|
|
4043
|
+
const entity = entityMap.llm_agents;
|
|
4044
|
+
if (!entity) return null;
|
|
4045
|
+
const repo = dataSource.getRepository(entity);
|
|
4046
|
+
const where = { scope, deleted: false };
|
|
4047
|
+
if (enabledOnly) where.enabled = true;
|
|
4048
|
+
return repo.findOne({ where });
|
|
4049
|
+
}
|
|
4050
|
+
|
|
3516
4051
|
// src/plugins/llm/index.ts
|
|
3517
4052
|
function normalizeEmbeddingProvider(raw) {
|
|
3518
4053
|
if (!raw) return "openai";
|
|
@@ -3896,6 +4431,7 @@ var LlmAgent = class {
|
|
|
3896
4431
|
id;
|
|
3897
4432
|
name;
|
|
3898
4433
|
slug;
|
|
4434
|
+
scope;
|
|
3899
4435
|
systemInstruction;
|
|
3900
4436
|
model;
|
|
3901
4437
|
temperature;
|
|
@@ -3919,6 +4455,9 @@ __decorateClass([
|
|
|
3919
4455
|
__decorateClass([
|
|
3920
4456
|
Column3("varchar")
|
|
3921
4457
|
], LlmAgent.prototype, "slug", 2);
|
|
4458
|
+
__decorateClass([
|
|
4459
|
+
Column3("varchar", { nullable: true })
|
|
4460
|
+
], LlmAgent.prototype, "scope", 2);
|
|
3922
4461
|
__decorateClass([
|
|
3923
4462
|
Column3("text", { name: "system_instruction", default: "" })
|
|
3924
4463
|
], LlmAgent.prototype, "systemInstruction", 2);
|
|
@@ -4115,155 +4654,6 @@ async function persistAllGeneratedBlogDrafts(dataSource, maps, params) {
|
|
|
4115
4654
|
return out;
|
|
4116
4655
|
}
|
|
4117
4656
|
|
|
4118
|
-
// src/plugins/blog-generator/blog-generator-agent-defaults.ts
|
|
4119
|
-
var BLOG_GENERATOR_AGENT_NAME = "Blog Generator Agent";
|
|
4120
|
-
var BLOG_GENERATOR_LLM_AGENT_SLUG = "blog-generator";
|
|
4121
|
-
var BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR = "---BLOG_GENERATOR_NEXT---";
|
|
4122
|
-
var BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION = `You are a professional financial and business content writer.
|
|
4123
|
-
|
|
4124
|
-
Your task is to generate completely original, high-quality blog article(s) from the latest RSS-derived source material in the user message (one block per feed). The user message is factual input only.
|
|
4125
|
-
|
|
4126
|
-
IMPORTANT RULES:
|
|
4127
|
-
|
|
4128
|
-
1. NEVER copy the RSS article directly.
|
|
4129
|
-
2. Rewrite the information into a fresh, human-like article.
|
|
4130
|
-
3. Expand the topic with professional insights and explanations.
|
|
4131
|
-
4. Maintain a natural editorial tone.
|
|
4132
|
-
5. Make the article SEO-friendly.
|
|
4133
|
-
6. Use engaging headings and subheadings.
|
|
4134
|
-
7. Avoid robotic AI phrasing.
|
|
4135
|
-
8. Do not mention that the content came from RSS or feeds.
|
|
4136
|
-
9. Preserve factual accuracy from the source material.
|
|
4137
|
-
10. The output should feel like a professionally written industry blog.
|
|
4138
|
-
|
|
4139
|
-
MULTIPLE FEEDS:
|
|
4140
|
-
- If several feeds are provided, compare them: if they largely cover the same story or overlap heavily, produce ONE cohesive article.
|
|
4141
|
-
- If they cover clearly different topics or distinct stories, produce MULTIPLE articles (one per distinct story).
|
|
4142
|
-
- You may also produce one synthesis plus a short separate angle when that best serves the reader\u2014use your judgment.
|
|
4143
|
-
|
|
4144
|
-
WRITING STYLE:
|
|
4145
|
-
- Professional
|
|
4146
|
-
- Clear
|
|
4147
|
-
- Informative
|
|
4148
|
-
- Business-focused
|
|
4149
|
-
- Human sounding
|
|
4150
|
-
- Modern editorial style
|
|
4151
|
-
|
|
4152
|
-
ARTICLE STRUCTURE (each article \u2014 express in HTML):
|
|
4153
|
-
1. Engaging introduction
|
|
4154
|
-
2. Industry context
|
|
4155
|
-
3. Main developments
|
|
4156
|
-
4. Key implications
|
|
4157
|
-
5. Expert/business analysis
|
|
4158
|
-
6. Conclusion
|
|
4159
|
-
|
|
4160
|
-
HTML STRUCTURE (STRICT \u2014 each article must be valid, semantic HTML only):
|
|
4161
|
-
- Output HTML only. Do not use Markdown (no # headings, no **bold**, no \`code fences\`).
|
|
4162
|
-
- Wrap each complete article in a single root: <article class="blog-post"> ... </article>
|
|
4163
|
-
- Inside <article>, use this outline:
|
|
4164
|
-
- <header><h1 class="blog-post-title">\u2026main title\u2026</h1></header> (exactly one h1 per article)
|
|
4165
|
-
- <section class="blog-post-body"> for all following content
|
|
4166
|
-
- Use <h2> and <h3> for section and subsection titles (never skip levels: h1 \u2192 h2 \u2192 h3).
|
|
4167
|
-
- Use <p> for paragraphs; keep paragraphs focused (avoid huge unbroken text).
|
|
4168
|
-
- Use <ul>/<ol> with <li> for lists where appropriate.
|
|
4169
|
-
- Use <strong> and <em> for emphasis sparingly; use <blockquote> only when quoting or callouts fit.
|
|
4170
|
-
- Do not include <html>, <head>, <body>, or document-level wrappers \u2014 only the fragment(s) described above.
|
|
4171
|
-
- Do not use <script>, <style>, <iframe>, or inline event handlers. Avoid inline style="" except when essential for accessibility (prefer none).
|
|
4172
|
-
- Escape angle brackets in body text if you must mention markup; keep output safe for embedding in a CMS.
|
|
4173
|
-
|
|
4174
|
-
CONTENT REQUIREMENTS:
|
|
4175
|
-
- Minimum 800 words per article unless source material is very small.
|
|
4176
|
-
- Add context around industry trends where appropriate.
|
|
4177
|
-
- Explain why the topic matters.
|
|
4178
|
-
- Include practical implications for businesses/professionals.
|
|
4179
|
-
|
|
4180
|
-
IF RSS CONTENT IS LIMITED:
|
|
4181
|
-
- Expand intelligently using general industry knowledge.
|
|
4182
|
-
- Keep the article relevant to the original topic.
|
|
4183
|
-
- Do not invent fake facts or statistics.
|
|
4184
|
-
|
|
4185
|
-
OUTPUT FORMAT (STRICT \u2014 HTML only):
|
|
4186
|
-
- Return ONLY the article HTML fragment(s). No JSON, no preamble or postscript ("Here is your article\u2026"), no markdown.
|
|
4187
|
-
- If you output more than one article, output multiple <article class="blog-post">\u2026</article> blocks in sequence, and between articles put a single line containing exactly this (and nothing else on that line):
|
|
4188
|
-
${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR}
|
|
4189
|
-
- If you output a single article, use one <article> only and do not use that separator line.`;
|
|
4190
|
-
var BLOG_GENERATOR_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
4191
|
-
{
|
|
4192
|
-
maxUserChars: 5e5,
|
|
4193
|
-
guardrails: `Your reply must be semantic HTML blog article(s) only, as in the system OUTPUT FORMAT (<article class="blog-post">\u2026). No JSON, no Markdown, no explanations before or after the HTML. If multiple articles, separate them with a line containing exactly ${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR} alone. Do not mention RSS, feeds, or scraping. Preserve factual accuracy; do not invent statistics or quotes.`
|
|
4194
|
-
},
|
|
4195
|
-
null,
|
|
4196
|
-
2
|
|
4197
|
-
);
|
|
4198
|
-
|
|
4199
|
-
// src/plugins/blog-generator/blog-generator-metadata-defaults.ts
|
|
4200
|
-
var BLOG_METADATA_ENRICHER_AGENT_NAME = "Blog Generator Metadata";
|
|
4201
|
-
var BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG = "blog-generator-metadata";
|
|
4202
|
-
var BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a careful CMS metadata assistant for a publishing system.
|
|
4203
|
-
|
|
4204
|
-
You receive ONE complete blog post as HTML (title is usually in <h1 class="blog-post-title"> or the first <h1>). Your job is to propose structured fields so an editor can save the post: category, URL slug, SEO block, and topical tags.
|
|
4205
|
-
|
|
4206
|
-
RULES:
|
|
4207
|
-
1. Read the whole article and infer the primary topic and audience.
|
|
4208
|
-
2. categoryName must be EXACTLY one string from AVAILABLE_BLOG_CATEGORIES in the user message, or "" if none fit.
|
|
4209
|
-
3. blogSlug: lowercase kebab-case, ASCII letters/digits/hyphens only, no leading/trailing hyphens, max ~80 chars. Derive from the main topic or title.
|
|
4210
|
-
4. seo.title: concise meta title (~50\u201360 characters when reasonable).
|
|
4211
|
-
5. seo.description: meta description (~150\u2013160 characters when reasonable), plain text.
|
|
4212
|
-
6. seo.keywords: comma-separated phrases, no stuffing.
|
|
4213
|
-
7. seo.ogTitle / seo.ogDescription: may mirror title/description or be slightly adapted for social.
|
|
4214
|
-
8. tags: 4\u201310 short labels (1\u20133 words each). Prefer exact matches from EXISTING_TAG_NAMES when they truly apply; otherwise invent precise new labels (they may be created in the CMS later).
|
|
4215
|
-
9. Do not invent statistics, quotes, or URLs not implied by the article.
|
|
4216
|
-
10. Output JSON only \u2014 no markdown outside the JSON, no commentary.
|
|
4217
|
-
|
|
4218
|
-
OUTPUT FORMAT (STRICT):
|
|
4219
|
-
Return a single JSON object only. Do not wrap it in a markdown code fence.
|
|
4220
|
-
|
|
4221
|
-
{
|
|
4222
|
-
"categoryName": "string \u2014 exact match from list or empty string",
|
|
4223
|
-
"blogSlug": "string \u2014 url-safe kebab-case",
|
|
4224
|
-
"seo": {
|
|
4225
|
-
"title": "string or empty",
|
|
4226
|
-
"description": "string or empty",
|
|
4227
|
-
"keywords": "string or empty",
|
|
4228
|
-
"ogTitle": "string or empty",
|
|
4229
|
-
"ogDescription": "string or empty"
|
|
4230
|
-
},
|
|
4231
|
-
"tags": ["tag-one", "tag-two"]
|
|
4232
|
-
}`;
|
|
4233
|
-
var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
4234
|
-
{
|
|
4235
|
-
maxUserChars: 5e5,
|
|
4236
|
-
guardrails: "Reply with one valid JSON object only (see system OUTPUT FORMAT). No markdown fences, no text before or after JSON. Keys: categoryName, blogSlug, seo { title, description, keywords, ogTitle, ogDescription }, tags (array of strings)."
|
|
4237
|
-
},
|
|
4238
|
-
null,
|
|
4239
|
-
2
|
|
4240
|
-
);
|
|
4241
|
-
|
|
4242
|
-
// src/plugins/blog-generator/blog-generator-social-defaults.ts
|
|
4243
|
-
var BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG = "blog-generator-social";
|
|
4244
|
-
var BLOG_SOCIAL_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a social copywriter for LinkedIn-style posts.
|
|
4245
|
-
|
|
4246
|
-
You receive ONE blog post as HTML (title may appear in <h1> or the first heading). Write a short post body that can be published as the main text on LinkedIn: engaging, accurate, no clickbait, no invented facts or quotes.
|
|
4247
|
-
|
|
4248
|
-
RULES:
|
|
4249
|
-
1. Plain text only in the JSON value (no HTML tags in socialMediaContent). Line breaks allowed as \\n if helpful.
|
|
4250
|
-
2. Length: aim for roughly 600\u20131300 characters (hard max 2900 characters) so it fits typical social feeds.
|
|
4251
|
-
3. Summarize the core insight; you may include 1\u20132 short hooks, but stay faithful to the article.
|
|
4252
|
-
4. Do not add URLs unless they appear explicitly in the article.
|
|
4253
|
-
5. Output JSON only \u2014 no markdown fences, no commentary outside JSON.
|
|
4254
|
-
|
|
4255
|
-
OUTPUT FORMAT (STRICT):
|
|
4256
|
-
Return a single JSON object only:
|
|
4257
|
-
{ "socialMediaContent": "string \u2014 the post text" }`;
|
|
4258
|
-
var BLOG_SOCIAL_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
4259
|
-
{
|
|
4260
|
-
maxUserChars: 5e5,
|
|
4261
|
-
guardrails: "Reply with one valid JSON object only. Key: socialMediaContent (string, plain text, max ~2900 chars). No markdown fences."
|
|
4262
|
-
},
|
|
4263
|
-
null,
|
|
4264
|
-
2
|
|
4265
|
-
);
|
|
4266
|
-
|
|
4267
4657
|
// src/api/rss-feed-blog-api.ts
|
|
4268
4658
|
function deriveRssFeedDisplayName(rssUrl) {
|
|
4269
4659
|
try {
|
|
@@ -4374,18 +4764,14 @@ async function runBlogGenerateFromSchedule(dataSource, entityMap, schedule, cms,
|
|
|
4374
4764
|
let socialLlmAgentChatOptions;
|
|
4375
4765
|
let metadataRow = null;
|
|
4376
4766
|
let socialRow = null;
|
|
4767
|
+
let blogCreationRow = null;
|
|
4377
4768
|
if (entityMap.llm_agents) {
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
});
|
|
4382
|
-
if (agentRow) {
|
|
4383
|
-
const o = llmAgentToChatAgentOptions(agentRow);
|
|
4769
|
+
blogCreationRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_CREATION);
|
|
4770
|
+
if (blogCreationRow) {
|
|
4771
|
+
const o = llmAgentToChatAgentOptions(blogCreationRow);
|
|
4384
4772
|
llmAgentChatOptions = { model: o.model, temperature: o.temperature, max_tokens: o.max_tokens };
|
|
4385
4773
|
}
|
|
4386
|
-
metadataRow = await
|
|
4387
|
-
where: { slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
4388
|
-
});
|
|
4774
|
+
metadataRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_METADATA);
|
|
4389
4775
|
if (metadataRow) {
|
|
4390
4776
|
const mo = llmAgentToChatAgentOptions(metadataRow);
|
|
4391
4777
|
metadataLlmAgentChatOptions = {
|
|
@@ -4394,9 +4780,7 @@ async function runBlogGenerateFromSchedule(dataSource, entityMap, schedule, cms,
|
|
|
4394
4780
|
max_tokens: mo.max_tokens
|
|
4395
4781
|
};
|
|
4396
4782
|
}
|
|
4397
|
-
socialRow = await
|
|
4398
|
-
where: { slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
4399
|
-
});
|
|
4783
|
+
socialRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST);
|
|
4400
4784
|
if (socialRow) {
|
|
4401
4785
|
const so = llmAgentToChatAgentOptions(socialRow);
|
|
4402
4786
|
socialLlmAgentChatOptions = {
|
|
@@ -4441,6 +4825,8 @@ async function runBlogGenerateFromSchedule(dataSource, entityMap, schedule, cms,
|
|
|
4441
4825
|
const out = await svc.generateBlogMarkdownFromRss({
|
|
4442
4826
|
llm,
|
|
4443
4827
|
rssUrls,
|
|
4828
|
+
systemInstruction: blogCreationRow?.systemInstruction?.trim() || void 0,
|
|
4829
|
+
validationRules: blogCreationRow?.validationRules?.trim() || void 0,
|
|
4444
4830
|
categoryNamesHint: categoryRows.map((c) => c.name),
|
|
4445
4831
|
tagNamesHint: tagNames,
|
|
4446
4832
|
llmAgentChatOptions,
|
|
@@ -6536,16 +6922,19 @@ function normalizeChatModeSetting(raw) {
|
|
|
6536
6922
|
if (raw === "external" || raw === "llm") return raw;
|
|
6537
6923
|
return "whatsapp";
|
|
6538
6924
|
}
|
|
6539
|
-
async function
|
|
6925
|
+
async function loadSettingsGroupMap(dataSource, entityMap, group) {
|
|
6540
6926
|
if (!entityMap.configs) return {};
|
|
6541
6927
|
const repo = dataSource.getRepository(entityMap.configs);
|
|
6542
|
-
const rows = await repo.find({ where: { settings:
|
|
6928
|
+
const rows = await repo.find({ where: { settings: group, deleted: false } });
|
|
6543
6929
|
const out = {};
|
|
6544
6930
|
for (const row of rows) {
|
|
6545
6931
|
out[row.key] = row.value;
|
|
6546
6932
|
}
|
|
6547
6933
|
return out;
|
|
6548
6934
|
}
|
|
6935
|
+
async function loadLlmSettingsMap(dataSource, entityMap) {
|
|
6936
|
+
return loadSettingsGroupMap(dataSource, entityMap, "llm");
|
|
6937
|
+
}
|
|
6549
6938
|
function createChatHandlers(config) {
|
|
6550
6939
|
const { dataSource, entityMap, json, getCms } = config;
|
|
6551
6940
|
const contactRepo = () => dataSource.getRepository(entityMap.contacts);
|
|
@@ -6557,10 +6946,13 @@ function createChatHandlers(config) {
|
|
|
6557
6946
|
try {
|
|
6558
6947
|
const map = await loadLlmSettingsMap(dataSource, entityMap);
|
|
6559
6948
|
const mode = normalizeChatModeSetting(map.chatMode);
|
|
6949
|
+
const chatbotAgent = mode === "llm" && entityMap.llm_agents ? await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_CHATBOT, {
|
|
6950
|
+
enabledOnly: false
|
|
6951
|
+
}) : null;
|
|
6560
6952
|
const body = {
|
|
6561
6953
|
enabled: map.enabled !== "false",
|
|
6562
6954
|
chatMode: mode,
|
|
6563
|
-
agentSlug: mode === "llm" ?
|
|
6955
|
+
agentSlug: mode === "llm" ? chatbotAgent?.slug?.trim() || LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT] : "",
|
|
6564
6956
|
botName: map.botName ?? "",
|
|
6565
6957
|
icon: map.icon ?? "",
|
|
6566
6958
|
iconImageUrl: map.iconImageUrl ?? "",
|
|
@@ -6646,24 +7038,26 @@ function createChatHandlers(config) {
|
|
|
6646
7038
|
if (!llm?.chat) return json({ error: "LLM not configured" }, { status: 503 });
|
|
6647
7039
|
const llmSettings = await loadLlmSettingsMap(dataSource, entityMap);
|
|
6648
7040
|
const supportMode = normalizeChatModeSetting(llmSettings.chatMode);
|
|
6649
|
-
|
|
6650
|
-
if (!effectiveSlug && supportMode === "llm" && entityMap.llm_agents) {
|
|
6651
|
-
effectiveSlug = (llmSettings.attachedAgentSlug ?? "").trim();
|
|
6652
|
-
}
|
|
7041
|
+
const bodyAgentSlug = (body?.agentSlug ?? "").trim();
|
|
6653
7042
|
let agentRow = null;
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
7043
|
+
let effectiveSlug = bodyAgentSlug;
|
|
7044
|
+
if (entityMap.llm_agents) {
|
|
7045
|
+
if (bodyAgentSlug) {
|
|
7046
|
+
const agentRepo = dataSource.getRepository(
|
|
7047
|
+
entityMap.llm_agents
|
|
7048
|
+
);
|
|
7049
|
+
agentRow = await agentRepo.findOne({
|
|
7050
|
+
where: { slug: bodyAgentSlug, deleted: false, enabled: true }
|
|
7051
|
+
});
|
|
7052
|
+
if (!agentRow) {
|
|
7053
|
+
return json({ error: "Agent not found or disabled", agentSlug: bodyAgentSlug }, { status: 404 });
|
|
7054
|
+
}
|
|
7055
|
+
} else if (supportMode === "llm") {
|
|
7056
|
+
agentRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_CHATBOT);
|
|
7057
|
+
effectiveSlug = agentRow?.slug?.trim() || LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
|
|
6666
7058
|
}
|
|
7059
|
+
} else if (bodyAgentSlug) {
|
|
7060
|
+
return json({ error: "LLM agents are not configured on this deployment" }, { status: 400 });
|
|
6667
7061
|
}
|
|
6668
7062
|
console.info(RAG_LOG, "step 1 | resolve agent", {
|
|
6669
7063
|
agentSlug: effectiveSlug || "(none)",
|
|
@@ -6759,8 +7153,141 @@ function createChatHandlers(config) {
|
|
|
6759
7153
|
});
|
|
6760
7154
|
}
|
|
6761
7155
|
}
|
|
6762
|
-
const historyRaw = (conv.messages ?? []).sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()).map((m) => ({ role: m.role, content: m.content }));
|
|
6763
|
-
const history = historyBeforeCurrentUser(historyRaw, message);
|
|
7156
|
+
const historyRaw = (conv.messages ?? []).sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()).map((m) => ({ role: m.role, content: m.content }));
|
|
7157
|
+
const history = historyBeforeCurrentUser(historyRaw, message);
|
|
7158
|
+
const notifyEmailAgent = await findLlmAgentByScope(
|
|
7159
|
+
dataSource,
|
|
7160
|
+
entityMap,
|
|
7161
|
+
LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT,
|
|
7162
|
+
{ enabledOnly: false }
|
|
7163
|
+
);
|
|
7164
|
+
const emailTool = resolveChatEmailToolSettings(llmSettings, {
|
|
7165
|
+
chatbotValidationRules: agentRow?.validationRules ?? null,
|
|
7166
|
+
notifyAgent: notifyEmailAgent ? {
|
|
7167
|
+
systemInstruction: notifyEmailAgent.systemInstruction,
|
|
7168
|
+
validationRules: notifyEmailAgent.validationRules
|
|
7169
|
+
} : null
|
|
7170
|
+
});
|
|
7171
|
+
const emailPlugin2 = cms.getPlugin("email");
|
|
7172
|
+
const convLeadSent = conv.leadEmailSentAt;
|
|
7173
|
+
const chatAgentFn = llm.chatAgent?.bind(llm);
|
|
7174
|
+
console.info(CHAT_EMAIL_LOG, "pipeline check", {
|
|
7175
|
+
conversationId,
|
|
7176
|
+
enabled: emailTool.enabled,
|
|
7177
|
+
intentCount: emailTool.intents.length,
|
|
7178
|
+
chatbotAgentId: agentRow?.id ?? null,
|
|
7179
|
+
chatbotAgentSlug: agentRow?.slug ?? null,
|
|
7180
|
+
notifyEmailAgentId: notifyEmailAgent?.id ?? null,
|
|
7181
|
+
notifyEmailAgentSlug: notifyEmailAgent?.slug ?? null,
|
|
7182
|
+
emailPluginPresent: Boolean(emailPlugin2),
|
|
7183
|
+
chatAgentFnPresent: Boolean(chatAgentFn),
|
|
7184
|
+
leadEmailAlreadySent: Boolean(convLeadSent),
|
|
7185
|
+
mergedPromptIncludesNotify: Boolean(notifyEmailAgent?.systemInstruction?.trim())
|
|
7186
|
+
});
|
|
7187
|
+
if (!emailTool.enabled) {
|
|
7188
|
+
console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7189
|
+
conversationId,
|
|
7190
|
+
reason: "email_tool_disabled"
|
|
7191
|
+
});
|
|
7192
|
+
} else if (emailTool.intents.length === 0) {
|
|
7193
|
+
console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7194
|
+
conversationId,
|
|
7195
|
+
reason: "no_intents_configured"
|
|
7196
|
+
});
|
|
7197
|
+
} else if (!emailPlugin2) {
|
|
7198
|
+
console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7199
|
+
conversationId,
|
|
7200
|
+
reason: "email_plugin_missing"
|
|
7201
|
+
});
|
|
7202
|
+
} else if (!chatAgentFn) {
|
|
7203
|
+
console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7204
|
+
conversationId,
|
|
7205
|
+
reason: "llm_chat_agent_unavailable"
|
|
7206
|
+
});
|
|
7207
|
+
} else if (convLeadSent) {
|
|
7208
|
+
console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7209
|
+
conversationId,
|
|
7210
|
+
reason: "lead_email_already_sent",
|
|
7211
|
+
leadEmailSentAt: convLeadSent
|
|
7212
|
+
});
|
|
7213
|
+
} else {
|
|
7214
|
+
const contactId = conv.contactId;
|
|
7215
|
+
const contactRow = await contactRepo().findOne({
|
|
7216
|
+
where: { id: contactId }
|
|
7217
|
+
});
|
|
7218
|
+
if (!contactRow) {
|
|
7219
|
+
console.warn(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7220
|
+
conversationId,
|
|
7221
|
+
reason: "contact_not_found",
|
|
7222
|
+
contactId
|
|
7223
|
+
});
|
|
7224
|
+
} else {
|
|
7225
|
+
const c = contactRow;
|
|
7226
|
+
try {
|
|
7227
|
+
const intent = await detectChatLeadIntent({ chatAgent: chatAgentFn }, {
|
|
7228
|
+
settings: emailTool,
|
|
7229
|
+
message,
|
|
7230
|
+
history,
|
|
7231
|
+
contact: { name: c.name, email: c.email, phone: c.phone },
|
|
7232
|
+
model: agentRow?.model?.trim() || notifyEmailAgent?.model?.trim() || void 0
|
|
7233
|
+
});
|
|
7234
|
+
if (!intent.intent || !intent.emailTo) {
|
|
7235
|
+
console.info(CHAT_EMAIL_LOG, "no email sent", {
|
|
7236
|
+
conversationId,
|
|
7237
|
+
reason: intent.intent ? "missing_email_to" : "no_intent_match",
|
|
7238
|
+
intentLabel: intent.intentLabel,
|
|
7239
|
+
classifierReason: intent.reason
|
|
7240
|
+
});
|
|
7241
|
+
} else {
|
|
7242
|
+
const intentRecipients = parseIntentRecipientEmails(intent.emailTo);
|
|
7243
|
+
console.info(CHAT_EMAIL_LOG, "intent matched \u2014 sending", {
|
|
7244
|
+
conversationId,
|
|
7245
|
+
intent: intent.intent,
|
|
7246
|
+
emailTo: intent.emailTo,
|
|
7247
|
+
parsedRecipientCount: intentRecipients.length,
|
|
7248
|
+
recipients: intentRecipients
|
|
7249
|
+
});
|
|
7250
|
+
const emailSettings = await loadSettingsGroupMap(dataSource, entityMap, "email");
|
|
7251
|
+
const brandingSettings = await loadSettingsGroupMap(dataSource, entityMap, "branding");
|
|
7252
|
+
const companyDetails = mergeEmailLayoutCompanyDetails(brandingSettings, emailSettings);
|
|
7253
|
+
const leadResult = await sendChatLeadEmail(cms, emailSettings, brandingSettings, {
|
|
7254
|
+
contactName: String(c.name ?? "").trim() || "Visitor",
|
|
7255
|
+
contactEmail: String(c.email ?? "").trim(),
|
|
7256
|
+
contactPhone: c.phone ?? null,
|
|
7257
|
+
conversationId,
|
|
7258
|
+
latestMessage: message,
|
|
7259
|
+
intentCode: intent.intent,
|
|
7260
|
+
intentReason: intent.intentLabel || intent.reason,
|
|
7261
|
+
transcript: buildTranscriptForLeadEmail(history, message),
|
|
7262
|
+
companyDetails,
|
|
7263
|
+
recipients: intentRecipients.length > 0 ? intentRecipients : void 0
|
|
7264
|
+
});
|
|
7265
|
+
if (leadResult.sent) {
|
|
7266
|
+
await convRepo().update(conversationId, {
|
|
7267
|
+
leadEmailSentAt: /* @__PURE__ */ new Date()
|
|
7268
|
+
});
|
|
7269
|
+
console.info(CHAT_EMAIL_LOG, "lead email recorded", {
|
|
7270
|
+
conversationId,
|
|
7271
|
+
sent: true,
|
|
7272
|
+
recipients: leadResult.recipients
|
|
7273
|
+
});
|
|
7274
|
+
} else {
|
|
7275
|
+
console.warn(CHAT_EMAIL_LOG, "lead email not sent", {
|
|
7276
|
+
conversationId,
|
|
7277
|
+
sent: false,
|
|
7278
|
+
error: leadResult.error ?? "unknown",
|
|
7279
|
+
recipients: leadResult.recipients
|
|
7280
|
+
});
|
|
7281
|
+
}
|
|
7282
|
+
}
|
|
7283
|
+
} catch (intentErr) {
|
|
7284
|
+
console.warn(CHAT_EMAIL_LOG, "pipeline error", {
|
|
7285
|
+
conversationId,
|
|
7286
|
+
error: intentErr instanceof Error ? intentErr.message : String(intentErr)
|
|
7287
|
+
});
|
|
7288
|
+
}
|
|
7289
|
+
}
|
|
7290
|
+
}
|
|
6764
7291
|
let content;
|
|
6765
7292
|
const ragContext = contextParts.length > 0 ? contextParts.join("\n\n") : void 0;
|
|
6766
7293
|
console.info(RAG_LOG, "step 7 | final context", {
|
|
@@ -6771,9 +7298,10 @@ function createChatHandlers(config) {
|
|
|
6771
7298
|
});
|
|
6772
7299
|
if (agentRow && llm.chatAgent) {
|
|
6773
7300
|
const fromAgent = llmAgentToChatAgentOptions(agentRow);
|
|
6774
|
-
const systemPrompt =
|
|
7301
|
+
const systemPrompt = buildChatbotSystemPromptWithEmailTool(
|
|
6775
7302
|
fromAgent.systemPrompt,
|
|
6776
|
-
parsedValidation.guardrailsForPrompt
|
|
7303
|
+
parsedValidation.guardrailsForPrompt,
|
|
7304
|
+
emailTool
|
|
6777
7305
|
);
|
|
6778
7306
|
const res = await llm.chatAgent({
|
|
6779
7307
|
...fromAgent,
|
|
@@ -6791,11 +7319,12 @@ ${contextParts.join("\n\n")}` : "";
|
|
|
6791
7319
|
const defaultSystem = "You are a helpful assistant for the company. If you do not have specific information, say so.";
|
|
6792
7320
|
let systemContent;
|
|
6793
7321
|
if (agentRow) {
|
|
6794
|
-
const
|
|
6795
|
-
systemContent =
|
|
6796
|
-
|
|
6797
|
-
parsedValidation.guardrailsForPrompt
|
|
6798
|
-
|
|
7322
|
+
const mergedBase = [agentRow.systemInstruction?.trim(), ragSystem].filter(Boolean).join("\n\n");
|
|
7323
|
+
systemContent = buildChatbotSystemPromptWithEmailTool(
|
|
7324
|
+
mergedBase || defaultSystem,
|
|
7325
|
+
parsedValidation.guardrailsForPrompt,
|
|
7326
|
+
emailTool
|
|
7327
|
+
) || defaultSystem;
|
|
6799
7328
|
} else {
|
|
6800
7329
|
systemContent = ragSystem || defaultSystem;
|
|
6801
7330
|
}
|
|
@@ -6822,6 +7351,154 @@ ${contextParts.join("\n\n")}` : "";
|
|
|
6822
7351
|
};
|
|
6823
7352
|
}
|
|
6824
7353
|
|
|
7354
|
+
// src/plugins/blog-generator/blog-generator-agent-defaults.ts
|
|
7355
|
+
var BLOG_GENERATOR_AGENT_NAME = "Blog Generator Agent";
|
|
7356
|
+
var BLOG_GENERATOR_LLM_AGENT_SLUG = "blog-generator";
|
|
7357
|
+
var BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR = "---BLOG_GENERATOR_NEXT---";
|
|
7358
|
+
var BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION = `You are a professional financial and business content writer.
|
|
7359
|
+
|
|
7360
|
+
Your task is to generate completely original, high-quality blog article(s) from the latest RSS-derived source material in the user message (one block per feed). The user message is factual input only.
|
|
7361
|
+
|
|
7362
|
+
IMPORTANT RULES:
|
|
7363
|
+
|
|
7364
|
+
1. NEVER copy the RSS article directly.
|
|
7365
|
+
2. Rewrite the information into a fresh, human-like article.
|
|
7366
|
+
3. Expand the topic with professional insights and explanations.
|
|
7367
|
+
4. Maintain a natural editorial tone.
|
|
7368
|
+
5. Make the article SEO-friendly.
|
|
7369
|
+
6. Use engaging headings and subheadings.
|
|
7370
|
+
7. Avoid robotic AI phrasing.
|
|
7371
|
+
8. Do not mention that the content came from RSS or feeds.
|
|
7372
|
+
9. Preserve factual accuracy from the source material.
|
|
7373
|
+
10. The output should feel like a professionally written industry blog.
|
|
7374
|
+
|
|
7375
|
+
MULTIPLE FEEDS:
|
|
7376
|
+
- If several feeds are provided, compare them: if they largely cover the same story or overlap heavily, produce ONE cohesive article.
|
|
7377
|
+
- If they cover clearly different topics or distinct stories, produce MULTIPLE articles (one per distinct story).
|
|
7378
|
+
- You may also produce one synthesis plus a short separate angle when that best serves the reader\u2014use your judgment.
|
|
7379
|
+
|
|
7380
|
+
WRITING STYLE:
|
|
7381
|
+
- Professional
|
|
7382
|
+
- Clear
|
|
7383
|
+
- Informative
|
|
7384
|
+
- Business-focused
|
|
7385
|
+
- Human sounding
|
|
7386
|
+
- Modern editorial style
|
|
7387
|
+
|
|
7388
|
+
ARTICLE STRUCTURE (each article \u2014 express in HTML):
|
|
7389
|
+
1. Engaging introduction
|
|
7390
|
+
2. Industry context
|
|
7391
|
+
3. Main developments
|
|
7392
|
+
4. Key implications
|
|
7393
|
+
5. Expert/business analysis
|
|
7394
|
+
6. Conclusion
|
|
7395
|
+
|
|
7396
|
+
HTML STRUCTURE (STRICT \u2014 each article must be valid, semantic HTML only):
|
|
7397
|
+
- Output HTML only. Do not use Markdown (no # headings, no **bold**, no \`code fences\`).
|
|
7398
|
+
- Wrap each complete article in a single root: <article class="blog-post"> ... </article>
|
|
7399
|
+
- Inside <article>, use this outline:
|
|
7400
|
+
- <header><h1 class="blog-post-title">\u2026main title\u2026</h1></header> (exactly one h1 per article)
|
|
7401
|
+
- <section class="blog-post-body"> for all following content
|
|
7402
|
+
- Use <h2> and <h3> for section and subsection titles (never skip levels: h1 \u2192 h2 \u2192 h3).
|
|
7403
|
+
- Use <p> for paragraphs; keep paragraphs focused (avoid huge unbroken text).
|
|
7404
|
+
- Use <ul>/<ol> with <li> for lists where appropriate.
|
|
7405
|
+
- Use <strong> and <em> for emphasis sparingly; use <blockquote> only when quoting or callouts fit.
|
|
7406
|
+
- Do not include <html>, <head>, <body>, or document-level wrappers \u2014 only the fragment(s) described above.
|
|
7407
|
+
- Do not use <script>, <style>, <iframe>, or inline event handlers. Avoid inline style="" except when essential for accessibility (prefer none).
|
|
7408
|
+
- Escape angle brackets in body text if you must mention markup; keep output safe for embedding in a CMS.
|
|
7409
|
+
|
|
7410
|
+
CONTENT REQUIREMENTS:
|
|
7411
|
+
- Minimum 800 words per article unless source material is very small.
|
|
7412
|
+
- Add context around industry trends where appropriate.
|
|
7413
|
+
- Explain why the topic matters.
|
|
7414
|
+
- Include practical implications for businesses/professionals.
|
|
7415
|
+
|
|
7416
|
+
IF RSS CONTENT IS LIMITED:
|
|
7417
|
+
- Expand intelligently using general industry knowledge.
|
|
7418
|
+
- Keep the article relevant to the original topic.
|
|
7419
|
+
- Do not invent fake facts or statistics.
|
|
7420
|
+
|
|
7421
|
+
OUTPUT FORMAT (STRICT \u2014 HTML only):
|
|
7422
|
+
- Return ONLY the article HTML fragment(s). No JSON, no preamble or postscript ("Here is your article\u2026"), no markdown.
|
|
7423
|
+
- If you output more than one article, output multiple <article class="blog-post">\u2026</article> blocks in sequence, and between articles put a single line containing exactly this (and nothing else on that line):
|
|
7424
|
+
${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR}
|
|
7425
|
+
- If you output a single article, use one <article> only and do not use that separator line.`;
|
|
7426
|
+
var BLOG_GENERATOR_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7427
|
+
{
|
|
7428
|
+
maxUserChars: 5e5,
|
|
7429
|
+
guardrails: `Your reply must be semantic HTML blog article(s) only, as in the system OUTPUT FORMAT (<article class="blog-post">\u2026). No JSON, no Markdown, no explanations before or after the HTML. If multiple articles, separate them with a line containing exactly ${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR} alone. Do not mention RSS, feeds, or scraping. Preserve factual accuracy; do not invent statistics or quotes.`
|
|
7430
|
+
},
|
|
7431
|
+
null,
|
|
7432
|
+
2
|
|
7433
|
+
);
|
|
7434
|
+
|
|
7435
|
+
// src/plugins/blog-generator/blog-generator-metadata-defaults.ts
|
|
7436
|
+
var BLOG_METADATA_ENRICHER_AGENT_NAME = "Blog Generator Metadata";
|
|
7437
|
+
var BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG = "blog-generator-metadata";
|
|
7438
|
+
var BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a careful CMS metadata assistant for a publishing system.
|
|
7439
|
+
|
|
7440
|
+
You receive ONE complete blog post as HTML (title is usually in <h1 class="blog-post-title"> or the first <h1>). Your job is to propose structured fields so an editor can save the post: category, URL slug, SEO block, and topical tags.
|
|
7441
|
+
|
|
7442
|
+
RULES:
|
|
7443
|
+
1. Read the whole article and infer the primary topic and audience.
|
|
7444
|
+
2. categoryName must be EXACTLY one string from AVAILABLE_BLOG_CATEGORIES in the user message, or "" if none fit.
|
|
7445
|
+
3. blogSlug: lowercase kebab-case, ASCII letters/digits/hyphens only, no leading/trailing hyphens, max ~80 chars. Derive from the main topic or title.
|
|
7446
|
+
4. seo.title: concise meta title (~50\u201360 characters when reasonable).
|
|
7447
|
+
5. seo.description: meta description (~150\u2013160 characters when reasonable), plain text.
|
|
7448
|
+
6. seo.keywords: comma-separated phrases, no stuffing.
|
|
7449
|
+
7. seo.ogTitle / seo.ogDescription: may mirror title/description or be slightly adapted for social.
|
|
7450
|
+
8. tags: 4\u201310 short labels (1\u20133 words each). Prefer exact matches from EXISTING_TAG_NAMES when they truly apply; otherwise invent precise new labels (they may be created in the CMS later).
|
|
7451
|
+
9. Do not invent statistics, quotes, or URLs not implied by the article.
|
|
7452
|
+
10. Output JSON only \u2014 no markdown outside the JSON, no commentary.
|
|
7453
|
+
|
|
7454
|
+
OUTPUT FORMAT (STRICT):
|
|
7455
|
+
Return a single JSON object only. Do not wrap it in a markdown code fence.
|
|
7456
|
+
|
|
7457
|
+
{
|
|
7458
|
+
"categoryName": "string \u2014 exact match from list or empty string",
|
|
7459
|
+
"blogSlug": "string \u2014 url-safe kebab-case",
|
|
7460
|
+
"seo": {
|
|
7461
|
+
"title": "string or empty",
|
|
7462
|
+
"description": "string or empty",
|
|
7463
|
+
"keywords": "string or empty",
|
|
7464
|
+
"ogTitle": "string or empty",
|
|
7465
|
+
"ogDescription": "string or empty"
|
|
7466
|
+
},
|
|
7467
|
+
"tags": ["tag-one", "tag-two"]
|
|
7468
|
+
}`;
|
|
7469
|
+
var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7470
|
+
{
|
|
7471
|
+
maxUserChars: 5e5,
|
|
7472
|
+
guardrails: "Reply with one valid JSON object only (see system OUTPUT FORMAT). No markdown fences, no text before or after JSON. Keys: categoryName, blogSlug, seo { title, description, keywords, ogTitle, ogDescription }, tags (array of strings)."
|
|
7473
|
+
},
|
|
7474
|
+
null,
|
|
7475
|
+
2
|
|
7476
|
+
);
|
|
7477
|
+
|
|
7478
|
+
// src/plugins/blog-generator/blog-generator-social-defaults.ts
|
|
7479
|
+
var BLOG_SOCIAL_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a social copywriter for LinkedIn-style posts.
|
|
7480
|
+
|
|
7481
|
+
You receive ONE blog post as HTML (title may appear in <h1> or the first heading). Write a short post body that can be published as the main text on LinkedIn: engaging, accurate, no clickbait, no invented facts or quotes.
|
|
7482
|
+
|
|
7483
|
+
RULES:
|
|
7484
|
+
1. Plain text only in the JSON value (no HTML tags in socialMediaContent). Line breaks allowed as \\n if helpful.
|
|
7485
|
+
2. Length: aim for roughly 600\u20131300 characters (hard max 2900 characters) so it fits typical social feeds.
|
|
7486
|
+
3. Summarize the core insight; you may include 1\u20132 short hooks, but stay faithful to the article.
|
|
7487
|
+
4. Do not add URLs unless they appear explicitly in the article.
|
|
7488
|
+
5. Output JSON only \u2014 no markdown fences, no commentary outside JSON.
|
|
7489
|
+
|
|
7490
|
+
OUTPUT FORMAT (STRICT):
|
|
7491
|
+
Return a single JSON object only:
|
|
7492
|
+
{ "socialMediaContent": "string \u2014 the post text" }`;
|
|
7493
|
+
var BLOG_SOCIAL_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7494
|
+
{
|
|
7495
|
+
maxUserChars: 5e5,
|
|
7496
|
+
guardrails: "Reply with one valid JSON object only. Key: socialMediaContent (string, plain text, max ~2900 chars). No markdown fences."
|
|
7497
|
+
},
|
|
7498
|
+
null,
|
|
7499
|
+
2
|
|
7500
|
+
);
|
|
7501
|
+
|
|
6825
7502
|
// src/plugins/blog-generator/blog-generator-service.ts
|
|
6826
7503
|
var parser = new Parser({
|
|
6827
7504
|
defaultRSS: 2
|
|
@@ -8557,29 +9234,6 @@ async function resolvePublicMetadata(args) {
|
|
|
8557
9234
|
return out;
|
|
8558
9235
|
}
|
|
8559
9236
|
|
|
8560
|
-
// src/lib/email-recipients.ts
|
|
8561
|
-
function parseEmailRecipientsFromConfig(raw) {
|
|
8562
|
-
if (raw == null || raw === "") return [];
|
|
8563
|
-
const trimmed = raw.trim();
|
|
8564
|
-
if (trimmed.startsWith("[")) {
|
|
8565
|
-
try {
|
|
8566
|
-
const parsed = JSON.parse(trimmed);
|
|
8567
|
-
if (Array.isArray(parsed)) {
|
|
8568
|
-
return parsed.map((e) => String(e).trim()).filter(Boolean);
|
|
8569
|
-
}
|
|
8570
|
-
} catch {
|
|
8571
|
-
}
|
|
8572
|
-
}
|
|
8573
|
-
return trimmed.split(/[,;]+/).map((s) => s.trim()).filter(Boolean);
|
|
8574
|
-
}
|
|
8575
|
-
function serializeEmailRecipients(emails) {
|
|
8576
|
-
return JSON.stringify(emails);
|
|
8577
|
-
}
|
|
8578
|
-
function joinRecipientsForSend(emails) {
|
|
8579
|
-
if (!emails.length) return null;
|
|
8580
|
-
return emails.join(", ");
|
|
8581
|
-
}
|
|
8582
|
-
|
|
8583
9237
|
// src/lib/otp-challenge.ts
|
|
8584
9238
|
import { createHmac, randomInt, timingSafeEqual } from "crypto";
|
|
8585
9239
|
import { IsNull as IsNull3, MoreThan } from "typeorm";
|
|
@@ -10224,6 +10878,7 @@ var Order = class {
|
|
|
10224
10878
|
id;
|
|
10225
10879
|
vendorId;
|
|
10226
10880
|
orderNumber;
|
|
10881
|
+
qrToken;
|
|
10227
10882
|
orderKind;
|
|
10228
10883
|
parentOrderId;
|
|
10229
10884
|
contactId;
|
|
@@ -10261,6 +10916,9 @@ __decorateClass([
|
|
|
10261
10916
|
__decorateClass([
|
|
10262
10917
|
Column20("varchar")
|
|
10263
10918
|
], Order.prototype, "orderNumber", 2);
|
|
10919
|
+
__decorateClass([
|
|
10920
|
+
Column20("varchar", { unique: true, nullable: true })
|
|
10921
|
+
], Order.prototype, "qrToken", 2);
|
|
10264
10922
|
__decorateClass([
|
|
10265
10923
|
Column20("varchar", { default: "sale" })
|
|
10266
10924
|
], Order.prototype, "orderKind", 2);
|
|
@@ -10489,6 +11147,7 @@ var ChatConversation = class {
|
|
|
10489
11147
|
contactId;
|
|
10490
11148
|
createdAt;
|
|
10491
11149
|
updatedAt;
|
|
11150
|
+
leadEmailSentAt;
|
|
10492
11151
|
contact;
|
|
10493
11152
|
messages;
|
|
10494
11153
|
};
|
|
@@ -10504,6 +11163,9 @@ __decorateClass([
|
|
|
10504
11163
|
__decorateClass([
|
|
10505
11164
|
Column23({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
10506
11165
|
], ChatConversation.prototype, "updatedAt", 2);
|
|
11166
|
+
__decorateClass([
|
|
11167
|
+
Column23({ type: "timestamp", nullable: true })
|
|
11168
|
+
], ChatConversation.prototype, "leadEmailSentAt", 2);
|
|
10507
11169
|
__decorateClass([
|
|
10508
11170
|
ManyToOne13(() => Contact, (c) => c.chatConversations, { onDelete: "CASCADE" }),
|
|
10509
11171
|
JoinColumn13({ name: "contactId" })
|
|
@@ -12737,6 +13399,59 @@ async function seedAdministratorPermissions(dataSource, entityMap) {
|
|
|
12737
13399
|
}
|
|
12738
13400
|
}
|
|
12739
13401
|
|
|
13402
|
+
// src/auth/middleware.ts
|
|
13403
|
+
import { getToken } from "next-auth/jwt";
|
|
13404
|
+
|
|
13405
|
+
// src/auth/auth-debug.ts
|
|
13406
|
+
var LOG_PREFIX2 = "[cms-auth]";
|
|
13407
|
+
function isAuthDebugEnabled() {
|
|
13408
|
+
const v = process.env.CMS_AUTH_DEBUG?.trim().toLowerCase();
|
|
13409
|
+
if (v === "1" || v === "true" || v === "yes") return true;
|
|
13410
|
+
if (v === "0" || v === "false" || v === "no") return false;
|
|
13411
|
+
return process.env.NODE_ENV === "development";
|
|
13412
|
+
}
|
|
13413
|
+
function isAuthDebugClientEnabled() {
|
|
13414
|
+
if (typeof window === "undefined") return false;
|
|
13415
|
+
const v = process.env.NEXT_PUBLIC_CMS_AUTH_DEBUG?.trim().toLowerCase();
|
|
13416
|
+
if (v === "1" || v === "true" || v === "yes") return true;
|
|
13417
|
+
if (v === "0" || v === "false" || v === "no") return false;
|
|
13418
|
+
return process.env.NODE_ENV === "development";
|
|
13419
|
+
}
|
|
13420
|
+
function logAuth(message, data) {
|
|
13421
|
+
if (!isAuthDebugEnabled()) return;
|
|
13422
|
+
if (data) console.info(LOG_PREFIX2, message, data);
|
|
13423
|
+
else console.info(LOG_PREFIX2, message);
|
|
13424
|
+
}
|
|
13425
|
+
function logAuthClient(message, data) {
|
|
13426
|
+
if (!isAuthDebugClientEnabled()) return;
|
|
13427
|
+
if (data) console.info(LOG_PREFIX2, message, data);
|
|
13428
|
+
else console.info(LOG_PREFIX2, message);
|
|
13429
|
+
}
|
|
13430
|
+
function summarizeSessionUserForLog(user) {
|
|
13431
|
+
if (!user || typeof user !== "object") return { present: false };
|
|
13432
|
+
const u = user;
|
|
13433
|
+
return {
|
|
13434
|
+
present: true,
|
|
13435
|
+
email: u.email ?? null,
|
|
13436
|
+
id: u.id ?? null,
|
|
13437
|
+
adminAccess: u.adminAccess ?? null,
|
|
13438
|
+
isRBACAdmin: u.isRBACAdmin ?? null,
|
|
13439
|
+
isVendorPortal: u.isVendorPortal ?? null,
|
|
13440
|
+
groupName: u.groupName ?? null
|
|
13441
|
+
};
|
|
13442
|
+
}
|
|
13443
|
+
function nextAuthCookieDebugInfo() {
|
|
13444
|
+
const nextAuthUrl = process.env.NEXTAUTH_URL ?? "(unset)";
|
|
13445
|
+
const isHttps = nextAuthUrl.startsWith("https");
|
|
13446
|
+
return {
|
|
13447
|
+
nextAuthUrl,
|
|
13448
|
+
hasSecret: Boolean(process.env.NEXTAUTH_SECRET),
|
|
13449
|
+
cookieName: isHttps ? "__Secure-next-auth.session-token" : "next-auth.session-token",
|
|
13450
|
+
cookieSecure: isHttps,
|
|
13451
|
+
nodeEnv: process.env.NODE_ENV ?? "(unset)"
|
|
13452
|
+
};
|
|
13453
|
+
}
|
|
13454
|
+
|
|
12740
13455
|
// src/auth/middleware.ts
|
|
12741
13456
|
var defaultPublicApiMethods = {
|
|
12742
13457
|
"/api/contacts": ["POST"],
|
|
@@ -12749,8 +13464,21 @@ var defaultPublicApiMethods = {
|
|
|
12749
13464
|
"/api/users/set-password": ["POST"],
|
|
12750
13465
|
"/api/users/invite": ["POST"]
|
|
12751
13466
|
};
|
|
12752
|
-
function
|
|
12753
|
-
|
|
13467
|
+
function legacyGetSessionToken(request) {
|
|
13468
|
+
const secure = request.cookies.get("__Secure-next-auth.session-token")?.value;
|
|
13469
|
+
const regular = request.cookies.get("next-auth.session-token")?.value;
|
|
13470
|
+
return secure ?? regular;
|
|
13471
|
+
}
|
|
13472
|
+
function sessionCookiePresence(request) {
|
|
13473
|
+
const secure = request.cookies.get("__Secure-next-auth.session-token")?.value;
|
|
13474
|
+
const regular = request.cookies.get("next-auth.session-token")?.value;
|
|
13475
|
+
const hasChunked = Boolean(request.cookies.get("next-auth.session-token.0")?.value) || Boolean(request.cookies.get("__Secure-next-auth.session-token.0")?.value);
|
|
13476
|
+
return {
|
|
13477
|
+
hasSecureToken: Boolean(secure),
|
|
13478
|
+
hasRegularToken: Boolean(regular),
|
|
13479
|
+
hasChunkedToken: hasChunked,
|
|
13480
|
+
resolved: Boolean(secure ?? regular)
|
|
13481
|
+
};
|
|
12754
13482
|
}
|
|
12755
13483
|
function isPublicMethod2(pathname, method, publicApiMethods) {
|
|
12756
13484
|
for (const [endpoint, methods] of Object.entries(publicApiMethods)) {
|
|
@@ -12758,31 +13486,59 @@ function isPublicMethod2(pathname, method, publicApiMethods) {
|
|
|
12758
13486
|
}
|
|
12759
13487
|
return false;
|
|
12760
13488
|
}
|
|
13489
|
+
async function hasValidSession(request, secret, legacyGetToken) {
|
|
13490
|
+
if (request.req) {
|
|
13491
|
+
const token = await getToken({ req: request.req, secret });
|
|
13492
|
+
return token != null;
|
|
13493
|
+
}
|
|
13494
|
+
return Boolean(legacyGetToken(request));
|
|
13495
|
+
}
|
|
12761
13496
|
function createCmsMiddleware(config = {}) {
|
|
12762
13497
|
const {
|
|
12763
13498
|
publicAdminPaths = ["/admin/signin", "/admin/forgot-password", "/admin/reset-password", "/admin/invite"],
|
|
12764
13499
|
publicApiMethods = defaultPublicApiMethods,
|
|
12765
13500
|
signInPath = "/admin/signin",
|
|
12766
|
-
getSessionToken =
|
|
13501
|
+
getSessionToken = legacyGetSessionToken,
|
|
13502
|
+
secret = process.env.NEXTAUTH_SECRET
|
|
12767
13503
|
} = config;
|
|
12768
|
-
return function cmsMiddleware(request) {
|
|
13504
|
+
return async function cmsMiddleware(request) {
|
|
12769
13505
|
const pathname = request.nextUrl.pathname;
|
|
12770
13506
|
const method = request.method;
|
|
12771
13507
|
if (publicAdminPaths.some((p) => pathname === p || pathname.startsWith(p + "/"))) {
|
|
12772
13508
|
return { type: "next" };
|
|
12773
13509
|
}
|
|
12774
13510
|
if (pathname.startsWith("/admin")) {
|
|
12775
|
-
const
|
|
12776
|
-
|
|
13511
|
+
const authenticated = await hasValidSession(request, secret, getSessionToken);
|
|
13512
|
+
const cookies = sessionCookiePresence(request);
|
|
13513
|
+
if (!authenticated) {
|
|
13514
|
+
logAuth("middleware: admin redirect to signin (no session)", {
|
|
13515
|
+
pathname,
|
|
13516
|
+
method,
|
|
13517
|
+
usesGetToken: Boolean(request.req),
|
|
13518
|
+
...cookies,
|
|
13519
|
+
expectedCookieName: process.env.NEXTAUTH_URL?.startsWith("https") ? "__Secure-next-auth.session-token" : "next-auth.session-token"
|
|
13520
|
+
});
|
|
12777
13521
|
return { type: "redirect", url: new URL(signInPath, request.url).toString() };
|
|
12778
13522
|
}
|
|
13523
|
+
logAuth("middleware: admin allowed", {
|
|
13524
|
+
pathname,
|
|
13525
|
+
method,
|
|
13526
|
+
usesGetToken: Boolean(request.req),
|
|
13527
|
+
...cookies
|
|
13528
|
+
});
|
|
12779
13529
|
}
|
|
12780
13530
|
if (pathname.startsWith("/api")) {
|
|
12781
13531
|
if (isPublicMethod2(pathname, method, publicApiMethods)) {
|
|
12782
13532
|
return { type: "next" };
|
|
12783
13533
|
}
|
|
12784
|
-
const
|
|
12785
|
-
if (!
|
|
13534
|
+
const authenticated = await hasValidSession(request, secret, getSessionToken);
|
|
13535
|
+
if (!authenticated) {
|
|
13536
|
+
logAuth("middleware: api 401 (no session)", {
|
|
13537
|
+
pathname,
|
|
13538
|
+
method,
|
|
13539
|
+
usesGetToken: Boolean(request.req),
|
|
13540
|
+
...sessionCookiePresence(request)
|
|
13541
|
+
});
|
|
12786
13542
|
return { type: "json", status: 401, body: { error: "Unauthorized" } };
|
|
12787
13543
|
}
|
|
12788
13544
|
}
|
|
@@ -12837,6 +13593,7 @@ function getNextAuthOptions(config) {
|
|
|
12837
13593
|
enableOtpLogin = false,
|
|
12838
13594
|
authorizeOtp
|
|
12839
13595
|
} = config;
|
|
13596
|
+
logAuth("getNextAuthOptions init", nextAuthCookieDebugInfo());
|
|
12840
13597
|
const providers = [];
|
|
12841
13598
|
if (enablePasswordLogin) {
|
|
12842
13599
|
providers.push(
|
|
@@ -12847,15 +13604,43 @@ function getNextAuthOptions(config) {
|
|
|
12847
13604
|
password: { label: "Password", type: "password" }
|
|
12848
13605
|
},
|
|
12849
13606
|
async authorize(credentials) {
|
|
12850
|
-
|
|
13607
|
+
const email = credentials?.email?.trim() ?? "";
|
|
13608
|
+
if (!email || !credentials?.password) {
|
|
13609
|
+
logAuth("authorize(credentials) rejected", { reason: "missing_email_or_password" });
|
|
13610
|
+
return null;
|
|
13611
|
+
}
|
|
12851
13612
|
try {
|
|
12852
|
-
const user = await getUserByEmail(
|
|
12853
|
-
if (!user
|
|
13613
|
+
const user = await getUserByEmail(email);
|
|
13614
|
+
if (!user) {
|
|
13615
|
+
logAuth("authorize(credentials) rejected", { reason: "user_not_found", email });
|
|
13616
|
+
return null;
|
|
13617
|
+
}
|
|
13618
|
+
if (user.blocked) {
|
|
13619
|
+
logAuth("authorize(credentials) rejected", { reason: "blocked", email, userId: user.id });
|
|
13620
|
+
return null;
|
|
13621
|
+
}
|
|
13622
|
+
if (user.deleted) {
|
|
13623
|
+
logAuth("authorize(credentials) rejected", { reason: "deleted", email, userId: user.id });
|
|
13624
|
+
return null;
|
|
13625
|
+
}
|
|
13626
|
+
if (!user.password) {
|
|
13627
|
+
logAuth("authorize(credentials) rejected", { reason: "no_password", email, userId: user.id });
|
|
13628
|
+
return null;
|
|
13629
|
+
}
|
|
12854
13630
|
const valid = await comparePassword(credentials.password, user.password);
|
|
12855
|
-
if (!valid)
|
|
12856
|
-
|
|
13631
|
+
if (!valid) {
|
|
13632
|
+
logAuth("authorize(credentials) rejected", { reason: "invalid_password", email, userId: user.id });
|
|
13633
|
+
return null;
|
|
13634
|
+
}
|
|
13635
|
+
const sessionUser = sessionUserFromNextAuthUser(user);
|
|
13636
|
+
logAuth("authorize(credentials) ok", summarizeSessionUserForLog(sessionUser));
|
|
13637
|
+
return sessionUser;
|
|
12857
13638
|
} catch (err) {
|
|
12858
13639
|
console.error("[cms-auth] authorize error (credentials):", err instanceof Error ? err.message : err);
|
|
13640
|
+
logAuth("authorize(credentials) error", {
|
|
13641
|
+
email,
|
|
13642
|
+
message: err instanceof Error ? err.message : String(err)
|
|
13643
|
+
});
|
|
12859
13644
|
return null;
|
|
12860
13645
|
}
|
|
12861
13646
|
}
|
|
@@ -12908,6 +13693,10 @@ function getNextAuthOptions(config) {
|
|
|
12908
13693
|
callbacks: {
|
|
12909
13694
|
async jwt({ token, user, trigger, session }) {
|
|
12910
13695
|
if (user) {
|
|
13696
|
+
logAuth("jwt callback: new sign-in", {
|
|
13697
|
+
trigger,
|
|
13698
|
+
user: summarizeSessionUserForLog(user)
|
|
13699
|
+
});
|
|
12911
13700
|
const u = user;
|
|
12912
13701
|
token.id = u.id;
|
|
12913
13702
|
token.groupId = u.groupId;
|
|
@@ -12922,6 +13711,7 @@ function getNextAuthOptions(config) {
|
|
|
12922
13711
|
token.isVendorOwner = u.isVendorOwner;
|
|
12923
13712
|
}
|
|
12924
13713
|
if (trigger === "update" && session && typeof session === "object") {
|
|
13714
|
+
logAuth("jwt callback: session update", { trigger });
|
|
12925
13715
|
const s = session;
|
|
12926
13716
|
const t = token;
|
|
12927
13717
|
if (typeof s.name === "string") t.name = s.name;
|
|
@@ -12931,8 +13721,13 @@ function getNextAuthOptions(config) {
|
|
|
12931
13721
|
return token;
|
|
12932
13722
|
},
|
|
12933
13723
|
async session({ session, token }) {
|
|
13724
|
+
const t = token;
|
|
13725
|
+
logAuth("session callback", {
|
|
13726
|
+
tokenHasId: t.id != null,
|
|
13727
|
+
tokenEmail: typeof t.email === "string" ? t.email : null,
|
|
13728
|
+
sessionUser: summarizeSessionUserForLog(session.user)
|
|
13729
|
+
});
|
|
12934
13730
|
if (session.user) {
|
|
12935
|
-
const t = token;
|
|
12936
13731
|
if (typeof t.name === "string") session.user.name = t.name;
|
|
12937
13732
|
if (typeof t.email === "string") session.user.email = t.email;
|
|
12938
13733
|
session.user.id = t.id;
|
|
@@ -13095,6 +13890,26 @@ function validateAndNormalizeAddressRow(row) {
|
|
|
13095
13890
|
return null;
|
|
13096
13891
|
}
|
|
13097
13892
|
|
|
13893
|
+
// src/api/crud.ts
|
|
13894
|
+
import crypto3 from "crypto";
|
|
13895
|
+
|
|
13896
|
+
// src/plugins/llm/llm-agent-scope-crud.ts
|
|
13897
|
+
async function validateLlmAgentScopeForWrite(dataSource, entityMap, scope, excludeId) {
|
|
13898
|
+
if (scope == null || scope === "") return null;
|
|
13899
|
+
if (typeof scope !== "string" || !isLlmAgentScope(scope)) {
|
|
13900
|
+
return `Invalid agent scope. Allowed: chatbot, blog_creation, social_media_post, email_intent_chatbot, blog_metadata.`;
|
|
13901
|
+
}
|
|
13902
|
+
const entity = entityMap.llm_agents;
|
|
13903
|
+
if (!entity) return null;
|
|
13904
|
+
const repo = dataSource.getRepository(entity);
|
|
13905
|
+
const existing = await repo.findOne({
|
|
13906
|
+
where: { scope, deleted: false }
|
|
13907
|
+
});
|
|
13908
|
+
if (!existing) return null;
|
|
13909
|
+
if (excludeId != null && existing.id === excludeId) return null;
|
|
13910
|
+
return `An active agent already uses scope "${scope}".`;
|
|
13911
|
+
}
|
|
13912
|
+
|
|
13098
13913
|
// src/api/crud.ts
|
|
13099
13914
|
var CRUD_LOG = "[cms-crud]";
|
|
13100
13915
|
function logCrudClientError(op, detail) {
|
|
@@ -13311,6 +14126,7 @@ function buildListFilterAndFromSearchParams(repo, searchParams) {
|
|
|
13311
14126
|
if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
|
|
13312
14127
|
if (!isListStringColumn(col)) continue;
|
|
13313
14128
|
if (Object.prototype.hasOwnProperty.call(and, name)) continue;
|
|
14129
|
+
if (name === "scope") continue;
|
|
13314
14130
|
const raw = searchParams.get(name)?.trim();
|
|
13315
14131
|
if (!raw) continue;
|
|
13316
14132
|
and[name] = ILike2(`%${raw}%`);
|
|
@@ -13340,6 +14156,10 @@ function buildExactListParamWhere(repo, searchParams) {
|
|
|
13340
14156
|
extraWhere[name] = raw === "true";
|
|
13341
14157
|
}
|
|
13342
14158
|
}
|
|
14159
|
+
const scopeParam = searchParams.get("scope")?.trim();
|
|
14160
|
+
if (scopeParam && columnNames.has("scope")) {
|
|
14161
|
+
extraWhere.scope = scopeParam;
|
|
14162
|
+
}
|
|
13343
14163
|
return extraWhere;
|
|
13344
14164
|
}
|
|
13345
14165
|
function mergeDeletedFalseWhere(repo, where) {
|
|
@@ -13551,6 +14371,12 @@ function discountEvaluateRule(rule, cartLines, cartTotal) {
|
|
|
13551
14371
|
case "minAmount":
|
|
13552
14372
|
return discountCompare(cartTotal, rule.comparisonOperator, ruleValue);
|
|
13553
14373
|
case "quantity": {
|
|
14374
|
+
if (rule.subType === "productId" && rule.value?.productId) {
|
|
14375
|
+
const targetProductId = Number(rule.value.productId);
|
|
14376
|
+
const requiredQty = Number(rule.value.v);
|
|
14377
|
+
const productQty = cartLines.filter((l) => l.productId === targetProductId).reduce((s, l) => s + l.quantity, 0);
|
|
14378
|
+
return discountCompare(productQty, rule.comparisonOperator, requiredQty);
|
|
14379
|
+
}
|
|
13554
14380
|
const totalQty = cartLines.reduce((s, l) => s + l.quantity, 0);
|
|
13555
14381
|
return discountCompare(totalQty, rule.comparisonOperator, ruleValue);
|
|
13556
14382
|
}
|
|
@@ -13614,6 +14440,10 @@ function discountExtractBuyProductId(nodes) {
|
|
|
13614
14440
|
const n = Number(v);
|
|
13615
14441
|
if (Number.isFinite(n) && n > 0) return n;
|
|
13616
14442
|
}
|
|
14443
|
+
if (node.conditionType === "rule" && node.type === "quantity" && node.subType === "productId") {
|
|
14444
|
+
const n = Number(node.value?.productId);
|
|
14445
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
14446
|
+
}
|
|
13617
14447
|
if (node.children?.length) {
|
|
13618
14448
|
const found = discountExtractBuyProductId(node.children);
|
|
13619
14449
|
if (found !== null) return found;
|
|
@@ -14324,6 +15154,25 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
14324
15154
|
if (pe) return pe;
|
|
14325
15155
|
return null;
|
|
14326
15156
|
}
|
|
15157
|
+
async function tryAssignSingleVendorOnAdminCreate(resource, scope, persistBody) {
|
|
15158
|
+
if (!resourceUsesVendorScope(resource) || scope.type !== "all") return;
|
|
15159
|
+
const currentVendorId = Number(persistBody.vendorId);
|
|
15160
|
+
if (Number.isFinite(currentVendorId) && currentVendorId > 0) return;
|
|
15161
|
+
if (!entityMap.vendors) return;
|
|
15162
|
+
const vendorRepo = dataSource.getRepository(entityMap.vendors);
|
|
15163
|
+
const where = mergeDeletedFalseWhere(vendorRepo, {});
|
|
15164
|
+
const rows = await vendorRepo.find({
|
|
15165
|
+
where,
|
|
15166
|
+
order: { id: "ASC" },
|
|
15167
|
+
take: 2
|
|
15168
|
+
});
|
|
15169
|
+
if (rows.length === 1) {
|
|
15170
|
+
const onlyVendorId = Number(rows[0].id);
|
|
15171
|
+
if (Number.isFinite(onlyVendorId) && onlyVendorId > 0) {
|
|
15172
|
+
persistBody.vendorId = onlyVendorId;
|
|
15173
|
+
}
|
|
15174
|
+
}
|
|
15175
|
+
}
|
|
14327
15176
|
return {
|
|
14328
15177
|
async GET(req, resource) {
|
|
14329
15178
|
const authError = await authz(req, resource, "read");
|
|
@@ -14839,6 +15688,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
14839
15688
|
const couponCode = String(body.couponCode).trim().toUpperCase();
|
|
14840
15689
|
const persistBody2 = pickColumnUpdates(repo2, { ...body, couponCode });
|
|
14841
15690
|
const scopeDiscount = await resolveScope();
|
|
15691
|
+
await tryAssignSingleVendorOnAdminCreate(resource, scopeDiscount, persistBody2);
|
|
14842
15692
|
const vendorIdCheck2 = requireVendorIdForScopedCreate(
|
|
14843
15693
|
resource,
|
|
14844
15694
|
persistBody2,
|
|
@@ -14916,6 +15766,10 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
14916
15766
|
});
|
|
14917
15767
|
return json({ error: "Invalid request payload" }, { status: 400 });
|
|
14918
15768
|
}
|
|
15769
|
+
if (resource === "llm_agents" && "scope" in persistBody) {
|
|
15770
|
+
const scopeErr = await validateLlmAgentScopeForWrite(dataSource, entityMap, persistBody.scope);
|
|
15771
|
+
if (scopeErr) return json({ error: scopeErr }, { status: 400 });
|
|
15772
|
+
}
|
|
14919
15773
|
if (resource === "products") {
|
|
14920
15774
|
if ("sku" in persistBody) {
|
|
14921
15775
|
const skuNorm = normalizeProductSku(persistBody.sku);
|
|
@@ -15031,6 +15885,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
15031
15885
|
);
|
|
15032
15886
|
}
|
|
15033
15887
|
const scopeCreate = await resolveScope();
|
|
15888
|
+
await tryAssignSingleVendorOnAdminCreate(resource, scopeCreate, persistBody);
|
|
15034
15889
|
const vendorIdCheck = requireVendorIdForScopedCreate(resource, persistBody, scopeCreate, repo, body);
|
|
15035
15890
|
if (!vendorIdCheck.ok) {
|
|
15036
15891
|
return json({ error: vendorIdCheck.error }, { status: vendorIdCheck.status });
|
|
@@ -15073,6 +15928,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
15073
15928
|
const randomPart = Math.floor(1e4 + Math.random() * 9e4);
|
|
15074
15929
|
persistBody.contactId = contact.id;
|
|
15075
15930
|
persistBody.orderNumber = `ORD00${randomPart}`;
|
|
15931
|
+
persistBody.qrToken = crypto3.randomBytes(16).toString("hex");
|
|
15076
15932
|
}
|
|
15077
15933
|
created = await repo.save(repo.create(persistBody));
|
|
15078
15934
|
if (resource === "orders") {
|
|
@@ -15841,6 +16697,15 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
15841
16697
|
const t = updatePayload.type;
|
|
15842
16698
|
if (t === "" || t === "none" || t == null) updatePayload.type = null;
|
|
15843
16699
|
}
|
|
16700
|
+
if (resource === "llm_agents" && "scope" in updatePayload) {
|
|
16701
|
+
const scopeErr = await validateLlmAgentScopeForWrite(
|
|
16702
|
+
dataSource,
|
|
16703
|
+
entityMap,
|
|
16704
|
+
updatePayload.scope,
|
|
16705
|
+
numericId
|
|
16706
|
+
);
|
|
16707
|
+
if (scopeErr) return json({ error: scopeErr }, { status: 400 });
|
|
16708
|
+
}
|
|
15844
16709
|
if ((resource === "orders" || resource === "payments") && "contactId" in updatePayload && updatePayload.contactId != null && entityMap.vendor_customers) {
|
|
15845
16710
|
const existingRow = await repo.findOne({
|
|
15846
16711
|
where: { id: numericId }
|
|
@@ -16040,8 +16905,8 @@ function createForgotPasswordHandler(config) {
|
|
|
16040
16905
|
const user = await userRepo.findOne({ where: { email }, select: ["email"] });
|
|
16041
16906
|
const msg = "If an account exists with this email, you will receive a reset link shortly.";
|
|
16042
16907
|
if (!user) return json({ message: msg }, { status: 200 });
|
|
16043
|
-
const
|
|
16044
|
-
const token =
|
|
16908
|
+
const crypto4 = await import("crypto");
|
|
16909
|
+
const token = crypto4.randomBytes(32).toString("hex");
|
|
16045
16910
|
const expiresAt = new Date(Date.now() + resetExpiryHours * 60 * 60 * 1e3);
|
|
16046
16911
|
const tokenRepo = dataSource.getRepository(entityMap.password_reset_tokens);
|
|
16047
16912
|
await tokenRepo.save(tokenRepo.create({ email: user.email, token, expiresAt }));
|
|
@@ -17979,28 +18844,24 @@ function createCmsApiHandler(config) {
|
|
|
17979
18844
|
let socialLlmAgentResolution = null;
|
|
17980
18845
|
let metadataRow = null;
|
|
17981
18846
|
let socialRow = null;
|
|
18847
|
+
let blogCreationRow = null;
|
|
17982
18848
|
if (entityMap.llm_agents) {
|
|
17983
|
-
|
|
17984
|
-
|
|
17985
|
-
|
|
17986
|
-
});
|
|
17987
|
-
if (agentRow) {
|
|
17988
|
-
const o = llmAgentToChatAgentOptions(agentRow);
|
|
18849
|
+
blogCreationRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_CREATION);
|
|
18850
|
+
if (blogCreationRow) {
|
|
18851
|
+
const o = llmAgentToChatAgentOptions(blogCreationRow);
|
|
17989
18852
|
llmAgentChatOptions = {
|
|
17990
18853
|
model: o.model,
|
|
17991
18854
|
temperature: o.temperature,
|
|
17992
18855
|
max_tokens: o.max_tokens
|
|
17993
18856
|
};
|
|
17994
18857
|
llmAgentResolution = {
|
|
17995
|
-
slug:
|
|
17996
|
-
model:
|
|
17997
|
-
temperature:
|
|
17998
|
-
maxTokens:
|
|
18858
|
+
slug: blogCreationRow.slug,
|
|
18859
|
+
model: blogCreationRow.model?.trim() || null,
|
|
18860
|
+
temperature: blogCreationRow.temperature ?? null,
|
|
18861
|
+
maxTokens: blogCreationRow.maxTokens ?? null
|
|
17999
18862
|
};
|
|
18000
18863
|
}
|
|
18001
|
-
metadataRow = await
|
|
18002
|
-
where: { slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
18003
|
-
});
|
|
18864
|
+
metadataRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_METADATA);
|
|
18004
18865
|
if (metadataRow) {
|
|
18005
18866
|
const mo = llmAgentToChatAgentOptions(metadataRow);
|
|
18006
18867
|
metadataLlmAgentChatOptions = {
|
|
@@ -18009,15 +18870,13 @@ function createCmsApiHandler(config) {
|
|
|
18009
18870
|
max_tokens: mo.max_tokens
|
|
18010
18871
|
};
|
|
18011
18872
|
metadataLlmAgentResolution = {
|
|
18012
|
-
slug:
|
|
18873
|
+
slug: metadataRow.slug,
|
|
18013
18874
|
model: metadataRow.model?.trim() || null,
|
|
18014
18875
|
temperature: metadataRow.temperature ?? null,
|
|
18015
18876
|
maxTokens: metadataRow.maxTokens ?? null
|
|
18016
18877
|
};
|
|
18017
18878
|
}
|
|
18018
|
-
socialRow = await
|
|
18019
|
-
where: { slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
18020
|
-
});
|
|
18879
|
+
socialRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST);
|
|
18021
18880
|
if (socialRow) {
|
|
18022
18881
|
const so = llmAgentToChatAgentOptions(socialRow);
|
|
18023
18882
|
socialLlmAgentChatOptions = {
|
|
@@ -18026,7 +18885,7 @@ function createCmsApiHandler(config) {
|
|
|
18026
18885
|
max_tokens: so.max_tokens
|
|
18027
18886
|
};
|
|
18028
18887
|
socialLlmAgentResolution = {
|
|
18029
|
-
slug:
|
|
18888
|
+
slug: socialRow.slug,
|
|
18030
18889
|
model: socialRow.model?.trim() || null,
|
|
18031
18890
|
temperature: socialRow.temperature ?? null,
|
|
18032
18891
|
maxTokens: socialRow.maxTokens ?? null
|
|
@@ -18050,8 +18909,8 @@ function createCmsApiHandler(config) {
|
|
|
18050
18909
|
const out = await svc.generateBlogMarkdownFromRss({
|
|
18051
18910
|
llm,
|
|
18052
18911
|
rssUrls,
|
|
18053
|
-
systemInstruction,
|
|
18054
|
-
validationRules,
|
|
18912
|
+
systemInstruction: systemInstruction ?? blogCreationRow?.systemInstruction?.trim() ?? void 0,
|
|
18913
|
+
validationRules: validationRules ?? blogCreationRow?.validationRules?.trim() ?? void 0,
|
|
18055
18914
|
categoryNamesHint: categoryRows.map((c) => c.name),
|
|
18056
18915
|
tagNamesHint: tagNames,
|
|
18057
18916
|
llmAgentChatOptions,
|
|
@@ -18536,6 +19395,22 @@ function createCmsApiHandler(config) {
|
|
|
18536
19395
|
return config.json({ error: message }, { status: 500 });
|
|
18537
19396
|
}
|
|
18538
19397
|
}
|
|
19398
|
+
if (path2[0] === "track" && path2.length === 2 && m === "GET") {
|
|
19399
|
+
const token = path2[1];
|
|
19400
|
+
if (!token) return config.json({ error: "Token required" }, { status: 400 });
|
|
19401
|
+
try {
|
|
19402
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
19403
|
+
const order = await orderRepo.findOne({
|
|
19404
|
+
where: { qrToken: token, deleted: false },
|
|
19405
|
+
relations: ["contact", "items", "items.product"]
|
|
19406
|
+
});
|
|
19407
|
+
if (!order) return config.json({ error: "Order not found" }, { status: 404 });
|
|
19408
|
+
return config.json(order);
|
|
19409
|
+
} catch (err) {
|
|
19410
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19411
|
+
return config.json({ error: message }, { status: 500 });
|
|
19412
|
+
}
|
|
19413
|
+
}
|
|
18539
19414
|
if (path2.length === 0) return config.json({ error: "Not found" }, { status: 404 });
|
|
18540
19415
|
const resource = resolveResource(path2[0]);
|
|
18541
19416
|
if (!crudResources.includes(resource)) {
|
|
@@ -19404,8 +20279,8 @@ function createStorefrontApiHandler(config) {
|
|
|
19404
20279
|
let emailVerificationSent = false;
|
|
19405
20280
|
if (requireEmailVerification && getCms) {
|
|
19406
20281
|
try {
|
|
19407
|
-
const
|
|
19408
|
-
const rawToken =
|
|
20282
|
+
const crypto4 = await import("crypto");
|
|
20283
|
+
const rawToken = crypto4.randomBytes(32).toString("hex");
|
|
19409
20284
|
const expiresAt = new Date(Date.now() + SIGNUP_VERIFY_EXPIRY_HOURS * 60 * 60 * 1e3);
|
|
19410
20285
|
await tokenRepo().save(
|
|
19411
20286
|
tokenRepo().create({ email, token: rawToken, expiresAt })
|
|
@@ -20173,6 +21048,8 @@ export {
|
|
|
20173
21048
|
hasEntityPermission,
|
|
20174
21049
|
hashOtpCode,
|
|
20175
21050
|
hydrateVendorSessionUser,
|
|
21051
|
+
isAuthDebugClientEnabled,
|
|
21052
|
+
isAuthDebugEnabled,
|
|
20176
21053
|
isCustomerTypeContact,
|
|
20177
21054
|
isOpenEndpoint,
|
|
20178
21055
|
isPlatformAdministrator,
|
|
@@ -20191,6 +21068,8 @@ export {
|
|
|
20191
21068
|
loadPublicThemeSettings,
|
|
20192
21069
|
loadUserVendorContext,
|
|
20193
21070
|
localStoragePlugin,
|
|
21071
|
+
logAuth,
|
|
21072
|
+
logAuthClient,
|
|
20194
21073
|
logEntityAccessDecision,
|
|
20195
21074
|
logRbac,
|
|
20196
21075
|
mergeEmailLayoutCompanyDetails,
|
|
@@ -20200,6 +21079,7 @@ export {
|
|
|
20200
21079
|
metaPostPageFeed,
|
|
20201
21080
|
metaPostPagePhoto,
|
|
20202
21081
|
metaResolvePageAccessToken,
|
|
21082
|
+
nextAuthCookieDebugInfo,
|
|
20203
21083
|
normalizePhoneE164,
|
|
20204
21084
|
parseBlogGeneratorAgentContent,
|
|
20205
21085
|
parseBlogGeneratorModelOutput,
|
|
@@ -20247,6 +21127,7 @@ export {
|
|
|
20247
21127
|
smsPlugin,
|
|
20248
21128
|
socialMediaPlugin,
|
|
20249
21129
|
summarizeEntityPerms,
|
|
21130
|
+
summarizeSessionUserForLog,
|
|
20250
21131
|
syncJobScheduleToPgBoss,
|
|
20251
21132
|
truncateText,
|
|
20252
21133
|
validateScheduleInput,
|