@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/dist/index.cjs CHANGED
@@ -1296,6 +1296,8 @@ __export(src_exports, {
1296
1296
  hasEntityPermission: () => hasEntityPermission,
1297
1297
  hashOtpCode: () => hashOtpCode,
1298
1298
  hydrateVendorSessionUser: () => hydrateVendorSessionUser,
1299
+ isAuthDebugClientEnabled: () => isAuthDebugClientEnabled,
1300
+ isAuthDebugEnabled: () => isAuthDebugEnabled,
1299
1301
  isCustomerTypeContact: () => isCustomerTypeContact,
1300
1302
  isOpenEndpoint: () => isOpenEndpoint,
1301
1303
  isPlatformAdministrator: () => isPlatformAdministrator,
@@ -1314,6 +1316,8 @@ __export(src_exports, {
1314
1316
  loadPublicThemeSettings: () => loadPublicThemeSettings,
1315
1317
  loadUserVendorContext: () => loadUserVendorContext,
1316
1318
  localStoragePlugin: () => localStoragePlugin,
1319
+ logAuth: () => logAuth,
1320
+ logAuthClient: () => logAuthClient,
1317
1321
  logEntityAccessDecision: () => logEntityAccessDecision,
1318
1322
  logRbac: () => logRbac,
1319
1323
  mergeEmailLayoutCompanyDetails: () => mergeEmailLayoutCompanyDetails,
@@ -1323,6 +1327,7 @@ __export(src_exports, {
1323
1327
  metaPostPageFeed: () => metaPostPageFeed,
1324
1328
  metaPostPagePhoto: () => metaPostPagePhoto,
1325
1329
  metaResolvePageAccessToken: () => metaResolvePageAccessToken,
1330
+ nextAuthCookieDebugInfo: () => nextAuthCookieDebugInfo,
1326
1331
  normalizePhoneE164: () => normalizePhoneE164,
1327
1332
  parseBlogGeneratorAgentContent: () => parseBlogGeneratorAgentContent,
1328
1333
  parseBlogGeneratorModelOutput: () => parseBlogGeneratorModelOutput,
@@ -1370,6 +1375,7 @@ __export(src_exports, {
1370
1375
  smsPlugin: () => smsPlugin,
1371
1376
  socialMediaPlugin: () => socialMediaPlugin,
1372
1377
  summarizeEntityPerms: () => summarizeEntityPerms,
1378
+ summarizeSessionUserForLog: () => summarizeSessionUserForLog,
1373
1379
  syncJobScheduleToPgBoss: () => syncJobScheduleToPgBoss,
1374
1380
  truncateText: () => truncateText,
1375
1381
  validateScheduleInput: () => validateScheduleInput,
@@ -2497,6 +2503,58 @@ function escapeHtml7(s) {
2497
2503
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2498
2504
  }
2499
2505
 
2506
+ // src/plugins/email/templates/chatLead.ts
2507
+ function render10(ctx) {
2508
+ const {
2509
+ contactName,
2510
+ contactEmail,
2511
+ contactPhone,
2512
+ conversationId,
2513
+ latestMessage,
2514
+ intentCode,
2515
+ intentReason,
2516
+ transcript,
2517
+ companyDetails
2518
+ } = ctx;
2519
+ const intentDisplay = intentCode?.trim() ? `${intentCode.trim()}${intentReason?.trim() ? ` \u2014 ${intentReason.trim()}` : ""}` : intentReason;
2520
+ const subject = `Chat lead${intentCode?.trim() ? ` [${intentCode.trim()}]` : ""}: ${contactName || contactEmail || "Visitor"}`;
2521
+ const transcriptBlock = transcript?.trim() ? `<div style="margin-top:16px;padding:12px;background:#f9fafb;border-radius:6px;border:1px solid #e5e7eb;">
2522
+ <p style="margin:0 0 8px 0;font-size:13px;font-weight:600;color:#374151;">Recent conversation</p>
2523
+ <pre style="margin:0;font-size:12px;white-space:pre-wrap;font-family:inherit;color:#4b5563;">${escapeHtml8(transcript.trim())}</pre>
2524
+ </div>` : "";
2525
+ const bodyHtml = `<p style="margin:0 0 6px 0;font-size:18px;font-weight:600;color:#111;">New chat lead</p>
2526
+ <p style="margin:0 0 12px 0;font-size:14px;color:#374151;">A visitor showed interest based on your intent rules.</p>
2527
+ <table style="width:100%;border-collapse:collapse;font-size:14px;">
2528
+ <tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Name</td><td>${escapeHtml8(contactName || "\u2014")}</td></tr>
2529
+ <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>
2530
+ <tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Phone</td><td>${escapeHtml8(contactPhone?.trim() || "\u2014")}</td></tr>
2531
+ <tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Conversation</td><td>#${conversationId}</td></tr>
2532
+ <tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Intent</td><td>${escapeHtml8(intentReason)}</td></tr>
2533
+ </table>
2534
+ <p style="margin:16px 0 6px 0;font-size:13px;font-weight:600;color:#374151;">Latest message</p>
2535
+ <p style="margin:0;font-size:14px;white-space:pre-wrap;">${escapeHtml8(latestMessage)}</p>
2536
+ ${transcriptBlock}`;
2537
+ const text = [
2538
+ "New chat lead",
2539
+ `Name: ${contactName || "\u2014"}`,
2540
+ `Email: ${contactEmail}`,
2541
+ `Phone: ${contactPhone?.trim() || "\u2014"}`,
2542
+ `Conversation: #${conversationId}`,
2543
+ `Intent: ${intentDisplay}`,
2544
+ "",
2545
+ "Latest message:",
2546
+ latestMessage,
2547
+ transcript?.trim() ? `
2548
+ Recent conversation:
2549
+ ${transcript.trim()}` : ""
2550
+ ].join("\n");
2551
+ const html = renderLayout({ bodyHtml, companyDetails });
2552
+ return { subject, html, text };
2553
+ }
2554
+ function escapeHtml8(s) {
2555
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2556
+ }
2557
+
2500
2558
  // src/plugins/email/templates/index.ts
2501
2559
  var templateRenderMap = {
2502
2560
  signup: render,
@@ -2507,7 +2565,8 @@ var templateRenderMap = {
2507
2565
  shippingUpdate: render6,
2508
2566
  invite: render7,
2509
2567
  formSubmission: render8,
2510
- otp: render9
2568
+ otp: render9,
2569
+ chatLead: render10
2511
2570
  };
2512
2571
  function getTemplateRenderer(name) {
2513
2572
  return templateRenderMap[name];
@@ -2524,11 +2583,11 @@ function renderEmail(templateName, ctx, options) {
2524
2583
  return { subject: custom.subject, html, text: custom.text };
2525
2584
  }
2526
2585
  }
2527
- const render10 = getTemplateRenderer(templateName);
2528
- if (!render10) {
2586
+ const render11 = getTemplateRenderer(templateName);
2587
+ if (!render11) {
2529
2588
  throw new Error(`Unknown email template: ${templateName}`);
2530
2589
  }
2531
- return render10(ctx);
2590
+ return render11(ctx);
2532
2591
  }
2533
2592
 
2534
2593
  // src/plugins/email/email-service.ts
@@ -2600,6 +2659,10 @@ var EmailService = class {
2600
2659
  renderTemplate(templateName, ctx) {
2601
2660
  return renderEmail(templateName, ctx, this.templateOptions);
2602
2661
  }
2662
+ /** Default notification recipient from plugin config (SMTP_TO / plugin `to`). */
2663
+ getDefaultTo() {
2664
+ return this.config.to;
2665
+ }
2603
2666
  };
2604
2667
  var emailTemplates = {
2605
2668
  formSubmission: (data) => ({
@@ -2630,6 +2693,434 @@ This link expires in 1 hour.`
2630
2693
 
2631
2694
  // src/plugins/email/index.ts
2632
2695
  init_email_queue();
2696
+
2697
+ // src/plugins/email/chat-lead-email.ts
2698
+ init_email_queue();
2699
+
2700
+ // src/lib/email-recipients.ts
2701
+ function parseEmailRecipientsFromConfig(raw) {
2702
+ if (raw == null || raw === "") return [];
2703
+ const trimmed = raw.trim();
2704
+ if (trimmed.startsWith("[")) {
2705
+ try {
2706
+ const parsed = JSON.parse(trimmed);
2707
+ if (Array.isArray(parsed)) {
2708
+ return parsed.map((e) => String(e).trim()).filter(Boolean);
2709
+ }
2710
+ } catch {
2711
+ }
2712
+ }
2713
+ return trimmed.split(/[,;]+/).map((s) => s.trim()).filter(Boolean);
2714
+ }
2715
+ function serializeEmailRecipients(emails) {
2716
+ return JSON.stringify(emails);
2717
+ }
2718
+ function joinRecipientsForSend(emails) {
2719
+ if (!emails.length) return null;
2720
+ return emails.join(", ");
2721
+ }
2722
+
2723
+ // src/plugins/llm/chat-email-intent.ts
2724
+ var NONE_INTENT = "NONE";
2725
+ var CHAT_EMAIL_LOG = "[chat-email-tool]";
2726
+ function logChatEmail(step, data) {
2727
+ if (data && Object.keys(data).length > 0) {
2728
+ console.info(CHAT_EMAIL_LOG, step, data);
2729
+ } else {
2730
+ console.info(CHAT_EMAIL_LOG, step);
2731
+ }
2732
+ }
2733
+ function normalizeIntentKey(raw) {
2734
+ return raw.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
2735
+ }
2736
+ function parseIntentsJson(raw) {
2737
+ if (!raw?.trim()) return null;
2738
+ try {
2739
+ const parsed = JSON.parse(raw);
2740
+ if (!Array.isArray(parsed)) return null;
2741
+ const out = [];
2742
+ for (const row of parsed) {
2743
+ if (!row || typeof row !== "object") continue;
2744
+ const o = row;
2745
+ const intent = normalizeIntentKey(String(o.intent ?? o.id ?? ""));
2746
+ const description = String(o.description ?? "").trim();
2747
+ const emailTo = String(o.emailTo ?? o.email ?? "").trim();
2748
+ if (!intent || !description || !emailTo) continue;
2749
+ out.push({ intent, description, emailTo });
2750
+ }
2751
+ return out.length > 0 ? out : null;
2752
+ } catch {
2753
+ return null;
2754
+ }
2755
+ }
2756
+ function dedupeIntents(intents) {
2757
+ const seen = /* @__PURE__ */ new Set();
2758
+ const out = [];
2759
+ for (const row of intents) {
2760
+ const key = normalizeIntentKey(row.intent);
2761
+ if (!key || seen.has(key)) continue;
2762
+ seen.add(key);
2763
+ out.push({
2764
+ intent: key,
2765
+ description: row.description.trim(),
2766
+ emailTo: row.emailTo.trim()
2767
+ });
2768
+ }
2769
+ return out;
2770
+ }
2771
+ function parseChatEmailToolSettings(map) {
2772
+ const fromJson = parseIntentsJson(map.emailIntents);
2773
+ const intents = dedupeIntents(fromJson ?? []);
2774
+ const legacyPositive = map.emailIntentPrompt?.trim() ?? "";
2775
+ const legacyNegative = map.emailNegativeIntentPrompt?.trim() ?? "";
2776
+ let classifierInstructions = map.emailClassifierInstructions?.trim() ?? "";
2777
+ if (!classifierInstructions && (legacyPositive || legacyNegative)) {
2778
+ const parts = [];
2779
+ if (legacyPositive) parts.push(`Legacy positive signals:
2780
+ ${legacyPositive}`);
2781
+ if (legacyNegative) parts.push(`Do NOT assign an intent when:
2782
+ ${legacyNegative}`);
2783
+ classifierInstructions = parts.join("\n\n");
2784
+ }
2785
+ return {
2786
+ enabled: map.emailToolEnabled === "true",
2787
+ intents,
2788
+ classifierInstructions,
2789
+ toolPrompt: map.emailToolPrompt ?? ""
2790
+ };
2791
+ }
2792
+ var EMAIL_TOOL_VALIDATION_KEY = "emailTool";
2793
+ function parseEmailToolObject(raw) {
2794
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
2795
+ const o = raw;
2796
+ const intentsRaw = o.intents;
2797
+ let intents;
2798
+ if (Array.isArray(intentsRaw)) {
2799
+ const parsed = parseIntentsJson(JSON.stringify(intentsRaw));
2800
+ if (parsed?.length) intents = parsed;
2801
+ }
2802
+ return {
2803
+ enabled: o.enabled === true,
2804
+ classifierInstructions: typeof o.classifierInstructions === "string" ? o.classifierInstructions.trim() : void 0,
2805
+ toolPrompt: typeof o.toolPrompt === "string" ? o.toolPrompt.trim() : void 0,
2806
+ intents
2807
+ };
2808
+ }
2809
+ function parseEmailToolFromAgentValidationRules(validationRulesText) {
2810
+ const raw = validationRulesText?.trim();
2811
+ if (!raw) return null;
2812
+ try {
2813
+ const parsed = JSON.parse(raw);
2814
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2815
+ const emailTool = parsed[EMAIL_TOOL_VALIDATION_KEY];
2816
+ return parseEmailToolObject(emailTool);
2817
+ } catch {
2818
+ return null;
2819
+ }
2820
+ }
2821
+ function resolveChatEmailToolSettings(configMap, sources) {
2822
+ const fromConfig = parseChatEmailToolSettings(configMap);
2823
+ const resolved = typeof sources === "string" || sources == null ? { chatbotValidationRules: typeof sources === "string" ? sources : null } : sources;
2824
+ const fromChatbot = parseEmailToolFromAgentValidationRules(resolved.chatbotValidationRules);
2825
+ const fromNotify = resolved.notifyAgent ? parseEmailToolFromAgentValidationRules(resolved.notifyAgent.validationRules) : null;
2826
+ const notifySystem = resolved.notifyAgent?.systemInstruction?.trim() ?? "";
2827
+ const intents = fromNotify?.intents?.length ? fromNotify.intents : fromChatbot?.intents?.length ? fromChatbot.intents : fromConfig.intents;
2828
+ const classifierParts = [
2829
+ notifySystem,
2830
+ fromNotify?.classifierInstructions?.trim(),
2831
+ fromChatbot?.classifierInstructions?.trim(),
2832
+ fromConfig.classifierInstructions.trim()
2833
+ ].filter(Boolean);
2834
+ const toolPrompt = fromNotify?.toolPrompt?.trim() || fromChatbot?.toolPrompt?.trim() || fromConfig.toolPrompt?.trim() || "";
2835
+ return {
2836
+ enabled: fromConfig.enabled,
2837
+ intents,
2838
+ classifierInstructions: classifierParts.join("\n\n"),
2839
+ toolPrompt
2840
+ };
2841
+ }
2842
+ function intentByKey(intents, key) {
2843
+ if (!key) return null;
2844
+ const norm3 = normalizeIntentKey(key);
2845
+ return intents.find((i) => i.intent === norm3) ?? null;
2846
+ }
2847
+ function buildIntentTableForPrompt(intents) {
2848
+ return intents.map((i) => `- ${i.intent}: ${i.description} \u2192 notify ${i.emailTo}`).join("\n");
2849
+ }
2850
+ function buildIntentClassifierSystem(config, agentClassifierInstructions) {
2851
+ const table = buildIntentTableForPrompt(config.intents);
2852
+ const extra = [agentClassifierInstructions?.trim(), config.classifierInstructions.trim()].filter(Boolean).join("\n\n");
2853
+ const intentKeys = config.intents.map((i) => i.intent).join(", ");
2854
+ return `You classify a chat visitor into exactly one lead intent for email routing.
2855
+
2856
+ Available intents (pick the best match):
2857
+ ${table}
2858
+
2859
+ 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}".
2860
+
2861
+ Allowed intent values: ${intentKeys}, or ${NONE_INTENT}.
2862
+
2863
+ ${extra ? `Additional instructions:
2864
+ ${extra}
2865
+ ` : ""}
2866
+ Reply with ONLY valid JSON, no markdown:
2867
+ {"intent":"INTENT_KEY","reason":"short explanation"}
2868
+ Use "${NONE_INTENT}" when no intent applies.`;
2869
+ }
2870
+ function buildChatEmailAgentContext(settings) {
2871
+ if (!settings.enabled) return "";
2872
+ const classifier = settings.classifierInstructions.trim();
2873
+ const extra = settings.toolPrompt?.trim() ?? "";
2874
+ const parts = [];
2875
+ if (settings.intents.length > 0) {
2876
+ parts.push(
2877
+ "## Lead email routing",
2878
+ "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."
2879
+ );
2880
+ if (classifier) parts.push(`### Routing rules
2881
+ ${classifier}`);
2882
+ parts.push(`### Intent catalog
2883
+ ${buildIntentTableForPrompt(settings.intents)}`);
2884
+ } else if (classifier) {
2885
+ parts.push(`## Lead email routing
2886
+ ${classifier}`);
2887
+ }
2888
+ if (extra) parts.push(`### Assistant behavior
2889
+ ${extra}`);
2890
+ return parts.join("\n\n");
2891
+ }
2892
+ function buildChatbotSystemPromptWithEmailTool(baseSystemInstruction, guardrailsForPrompt, emailSettings) {
2893
+ let prompt = (baseSystemInstruction ?? "").trim();
2894
+ const guard = guardrailsForPrompt?.trim();
2895
+ if (guard) prompt = [prompt, guard].filter(Boolean).join("\n\n");
2896
+ if (emailSettings.enabled) {
2897
+ const emailCtx = buildChatEmailAgentContext(emailSettings);
2898
+ if (emailCtx) prompt = [prompt, emailCtx].filter(Boolean).join("\n\n");
2899
+ }
2900
+ return prompt;
2901
+ }
2902
+ function formatTranscript(history, latestMessage, maxChars = 4e3) {
2903
+ const lines = [];
2904
+ for (const m of history) {
2905
+ const role = m.role === "assistant" ? "Assistant" : m.role === "user" ? "Visitor" : "System";
2906
+ lines.push(`${role}: ${m.content}`);
2907
+ }
2908
+ const last = history[history.length - 1];
2909
+ if (!last || last.role !== "user" || last.content !== latestMessage) {
2910
+ lines.push(`Visitor: ${latestMessage}`);
2911
+ }
2912
+ let text = lines.join("\n");
2913
+ if (text.length > maxChars) text = text.slice(-maxChars);
2914
+ return text;
2915
+ }
2916
+ function parseIntentJson(raw, intents) {
2917
+ const trimmed = raw.trim();
2918
+ const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
2919
+ const candidate = (fence?.[1] ?? trimmed).trim();
2920
+ const tryParse = (s) => {
2921
+ const o = JSON.parse(s);
2922
+ let intentRaw = null;
2923
+ if (typeof o.intent === "string") intentRaw = o.intent;
2924
+ else if (o.interested === false) intentRaw = NONE_INTENT;
2925
+ else if (o.interested === true && intents[0]) intentRaw = intents[0].intent;
2926
+ if (intentRaw == null) return null;
2927
+ const reason = typeof o.reason === "string" && o.reason.trim() ? o.reason.trim() : "Classified from conversation";
2928
+ return { intent: normalizeIntentKey(intentRaw), reason };
2929
+ };
2930
+ try {
2931
+ return tryParse(candidate);
2932
+ } catch {
2933
+ const m = candidate.match(/\{[\s\S]*\}/);
2934
+ if (!m) return null;
2935
+ try {
2936
+ return tryParse(m[0]);
2937
+ } catch {
2938
+ return null;
2939
+ }
2940
+ }
2941
+ }
2942
+ async function detectChatLeadIntent(llm, params) {
2943
+ if (!params.settings.intents.length) {
2944
+ logChatEmail("classify skipped", { reason: "no_intents_configured" });
2945
+ return {
2946
+ intent: null,
2947
+ intentLabel: "NONE",
2948
+ reason: "No lead intents configured",
2949
+ emailTo: null
2950
+ };
2951
+ }
2952
+ logChatEmail("classify start", {
2953
+ intentCount: params.settings.intents.length,
2954
+ intentKeys: params.settings.intents.map((i) => i.intent),
2955
+ model: params.model?.trim() || "(gateway default)",
2956
+ messageChars: params.message.length,
2957
+ historyTurns: params.history.length,
2958
+ hasClassifierInstructions: Boolean(
2959
+ params.agentClassifierInstructions?.trim() || params.settings.classifierInstructions.trim()
2960
+ )
2961
+ });
2962
+ const transcript = formatTranscript(params.history, params.message);
2963
+ const contactLines = [
2964
+ params.contact.name?.trim() ? `Name: ${params.contact.name.trim()}` : null,
2965
+ params.contact.email?.trim() ? `Email: ${params.contact.email.trim()}` : null,
2966
+ params.contact.phone?.trim() ? `Phone: ${params.contact.phone.trim()}` : null
2967
+ ].filter(Boolean).join("\n");
2968
+ const userPrompt = `Customer:
2969
+ ${contactLines || "(unknown)"}
2970
+
2971
+ Conversation:
2972
+ ${transcript}
2973
+
2974
+ Pick the single best intent for the visitor's latest message.`;
2975
+ const res = await llm.chatAgent({
2976
+ systemPrompt: buildIntentClassifierSystem(params.settings, params.agentClassifierInstructions),
2977
+ userPrompt,
2978
+ temperature: 0.1,
2979
+ max_tokens: 256,
2980
+ ...params.model?.trim() ? { model: params.model.trim() } : {}
2981
+ });
2982
+ const rawContent = res.content ?? "";
2983
+ logChatEmail("classify llm response", {
2984
+ responseChars: rawContent.length,
2985
+ responsePreview: rawContent.slice(0, 280) + (rawContent.length > 280 ? "\u2026" : "")
2986
+ });
2987
+ const parsed = parseIntentJson(rawContent, params.settings.intents);
2988
+ if (!parsed) {
2989
+ logChatEmail("classify result", {
2990
+ matched: false,
2991
+ outcome: "parse_failed",
2992
+ emailWillSend: false
2993
+ });
2994
+ return {
2995
+ intent: null,
2996
+ intentLabel: "Unclassified",
2997
+ reason: "Could not parse intent classifier response",
2998
+ emailTo: null
2999
+ };
3000
+ }
3001
+ if (parsed.intent === NONE_INTENT) {
3002
+ logChatEmail("classify result", {
3003
+ matched: false,
3004
+ outcome: NONE_INTENT,
3005
+ reason: parsed.reason,
3006
+ emailWillSend: false
3007
+ });
3008
+ return {
3009
+ intent: null,
3010
+ intentLabel: NONE_INTENT,
3011
+ reason: parsed.reason,
3012
+ emailTo: null
3013
+ };
3014
+ }
3015
+ const matched = intentByKey(params.settings.intents, parsed.intent);
3016
+ if (!matched) {
3017
+ logChatEmail("classify result", {
3018
+ matched: false,
3019
+ outcome: "unknown_intent",
3020
+ parsedIntent: parsed.intent,
3021
+ reason: parsed.reason,
3022
+ emailWillSend: false
3023
+ });
3024
+ return {
3025
+ intent: null,
3026
+ intentLabel: parsed.intent,
3027
+ reason: `Unknown intent "${parsed.intent}": ${parsed.reason}`,
3028
+ emailTo: null
3029
+ };
3030
+ }
3031
+ logChatEmail("classify result", {
3032
+ matched: true,
3033
+ outcome: "intent_found",
3034
+ intent: matched.intent,
3035
+ emailTo: matched.emailTo,
3036
+ reason: parsed.reason,
3037
+ emailWillSend: true
3038
+ });
3039
+ return {
3040
+ intent: matched.intent,
3041
+ intentLabel: `${matched.intent} \u2014 ${matched.description}`,
3042
+ reason: parsed.reason,
3043
+ emailTo: matched.emailTo
3044
+ };
3045
+ }
3046
+ function buildTranscriptForLeadEmail(history, latestMessage) {
3047
+ return formatTranscript(history, latestMessage);
3048
+ }
3049
+ function parseIntentRecipientEmails(emailTo) {
3050
+ return emailTo.split(/[,;]+/).map((s) => s.trim()).filter((s) => s.length > 0 && s.includes("@"));
3051
+ }
3052
+
3053
+ // src/plugins/email/chat-lead-email.ts
3054
+ function resolveLeadRecipients(emailPlugin2, emailSettings) {
3055
+ const fromCrm = parseEmailRecipientsFromConfig(
3056
+ emailSettings.crmEmails ?? emailSettings.crmEmail ?? ""
3057
+ );
3058
+ if (fromCrm.length > 0) return fromCrm;
3059
+ const fallback = emailPlugin2.getDefaultTo?.() ?? "";
3060
+ if (fallback.trim()) return [fallback.trim()];
3061
+ return [];
3062
+ }
3063
+ async function sendChatLeadEmail(cms, emailSettings, brandingSettings, input) {
3064
+ console.info(CHAT_EMAIL_LOG, "send start", {
3065
+ conversationId: input.conversationId,
3066
+ intentCode: input.intentCode ?? null,
3067
+ contactEmail: input.contactEmail,
3068
+ contactName: input.contactName,
3069
+ explicitRecipients: input.recipients?.length ?? 0
3070
+ });
3071
+ const email = cms.getPlugin("email");
3072
+ if (!email?.send || !email.renderTemplate) {
3073
+ console.warn(CHAT_EMAIL_LOG, "send failed", {
3074
+ conversationId: input.conversationId,
3075
+ reason: "email_plugin_disabled"
3076
+ });
3077
+ return { sent: false, recipients: [], error: "Email plugin is not enabled" };
3078
+ }
3079
+ const recipients = input.recipients?.filter((r) => r.trim().includes("@")).map((r) => r.trim()) ?? resolveLeadRecipients(email, emailSettings);
3080
+ if (recipients.length === 0) {
3081
+ console.warn(CHAT_EMAIL_LOG, "send failed", {
3082
+ conversationId: input.conversationId,
3083
+ reason: "no_recipients"
3084
+ });
3085
+ return {
3086
+ sent: false,
3087
+ recipients: [],
3088
+ error: "No lead email recipients (configure intent Email To, CRM emails, or SMTP default To)"
3089
+ };
3090
+ }
3091
+ console.info(CHAT_EMAIL_LOG, "send recipients resolved", {
3092
+ conversationId: input.conversationId,
3093
+ recipients,
3094
+ source: input.recipients?.length ? "intent_email_to" : "crm_or_smtp_fallback"
3095
+ });
3096
+ const companyDetails = mergeEmailLayoutCompanyDetails(brandingSettings, emailSettings);
3097
+ const ctx = {
3098
+ ...input,
3099
+ companyDetails: input.companyDetails ?? companyDetails
3100
+ };
3101
+ let anySent = false;
3102
+ for (const to of recipients) {
3103
+ await queueEmail(cms, {
3104
+ to,
3105
+ templateName: "chatLead",
3106
+ ctx
3107
+ });
3108
+ console.info(CHAT_EMAIL_LOG, "send queued", {
3109
+ conversationId: input.conversationId,
3110
+ to,
3111
+ template: "chatLead"
3112
+ });
3113
+ anySent = true;
3114
+ }
3115
+ console.info(CHAT_EMAIL_LOG, "send complete", {
3116
+ conversationId: input.conversationId,
3117
+ sent: anySent,
3118
+ recipientCount: recipients.length
3119
+ });
3120
+ return { sent: anySent, recipients };
3121
+ }
3122
+
3123
+ // src/plugins/email/index.ts
2633
3124
  function emailPlugin(config) {
2634
3125
  return {
2635
3126
  name: "email",
@@ -3773,6 +4264,56 @@ ${context}`;
3773
4264
  }
3774
4265
  };
3775
4266
 
4267
+ // src/plugins/llm/llm-agent-scope.ts
4268
+ var LLM_AGENT_SCOPE_CHATBOT = "chatbot";
4269
+ var LLM_AGENT_SCOPE_BLOG_CREATION = "blog_creation";
4270
+ var LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST = "social_media_post";
4271
+ var LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT = "email_intent_chatbot";
4272
+ var LLM_AGENT_SCOPE_BLOG_METADATA = "blog_metadata";
4273
+ var LLM_AGENT_SCOPES = [
4274
+ LLM_AGENT_SCOPE_CHATBOT,
4275
+ LLM_AGENT_SCOPE_BLOG_CREATION,
4276
+ LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST,
4277
+ LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT,
4278
+ LLM_AGENT_SCOPE_BLOG_METADATA
4279
+ ];
4280
+ var LLM_AGENT_DEFAULT_SLUG_BY_SCOPE = {
4281
+ [LLM_AGENT_SCOPE_CHATBOT]: "site-chat-assistant",
4282
+ [LLM_AGENT_SCOPE_BLOG_CREATION]: "blog-generator",
4283
+ [LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]: "blog-generator-social",
4284
+ [LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT]: "email-intent-chatbot",
4285
+ [LLM_AGENT_SCOPE_BLOG_METADATA]: "blog-generator-metadata"
4286
+ };
4287
+ var LLM_AGENT_DEFAULT_NAME_BY_SCOPE = {
4288
+ [LLM_AGENT_SCOPE_CHATBOT]: "Site Chat Assistant",
4289
+ [LLM_AGENT_SCOPE_BLOG_CREATION]: "Blog Generator",
4290
+ [LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]: "Blog Generator (Social)",
4291
+ [LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT]: "Chat Lead Intent Classifier",
4292
+ [LLM_AGENT_SCOPE_BLOG_METADATA]: "Blog Generator (Metadata)"
4293
+ };
4294
+ var SITE_CHAT_ASSISTANT_SLUG = LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
4295
+ var SITE_CHAT_ASSISTANT_NAME = LLM_AGENT_DEFAULT_NAME_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
4296
+ function isLlmAgentScope(value) {
4297
+ if (!value?.trim()) return false;
4298
+ return LLM_AGENT_SCOPES.includes(value.trim());
4299
+ }
4300
+ var BLOG_LLM_AGENT_SLUGS = [
4301
+ LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_BLOG_CREATION],
4302
+ LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_BLOG_METADATA],
4303
+ LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]
4304
+ ];
4305
+
4306
+ // src/plugins/llm/find-llm-agent-by-scope.ts
4307
+ async function findLlmAgentByScope(dataSource, entityMap, scope, options = {}) {
4308
+ const { enabledOnly = true } = options;
4309
+ const entity = entityMap.llm_agents;
4310
+ if (!entity) return null;
4311
+ const repo = dataSource.getRepository(entity);
4312
+ const where = { scope, deleted: false };
4313
+ if (enabledOnly) where.enabled = true;
4314
+ return repo.findOne({ where });
4315
+ }
4316
+
3776
4317
  // src/plugins/llm/index.ts
3777
4318
  function normalizeEmbeddingProvider(raw) {
3778
4319
  if (!raw) return "openai";
@@ -4142,6 +4683,7 @@ var LlmAgent = class {
4142
4683
  id;
4143
4684
  name;
4144
4685
  slug;
4686
+ scope;
4145
4687
  systemInstruction;
4146
4688
  model;
4147
4689
  temperature;
@@ -4165,6 +4707,9 @@ __decorateClass([
4165
4707
  __decorateClass([
4166
4708
  (0, import_typeorm3.Column)("varchar")
4167
4709
  ], LlmAgent.prototype, "slug", 2);
4710
+ __decorateClass([
4711
+ (0, import_typeorm3.Column)("varchar", { nullable: true })
4712
+ ], LlmAgent.prototype, "scope", 2);
4168
4713
  __decorateClass([
4169
4714
  (0, import_typeorm3.Column)("text", { name: "system_instruction", default: "" })
4170
4715
  ], LlmAgent.prototype, "systemInstruction", 2);
@@ -4361,155 +4906,6 @@ async function persistAllGeneratedBlogDrafts(dataSource, maps, params) {
4361
4906
  return out;
4362
4907
  }
4363
4908
 
4364
- // src/plugins/blog-generator/blog-generator-agent-defaults.ts
4365
- var BLOG_GENERATOR_AGENT_NAME = "Blog Generator Agent";
4366
- var BLOG_GENERATOR_LLM_AGENT_SLUG = "blog-generator";
4367
- var BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR = "---BLOG_GENERATOR_NEXT---";
4368
- var BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION = `You are a professional financial and business content writer.
4369
-
4370
- 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.
4371
-
4372
- IMPORTANT RULES:
4373
-
4374
- 1. NEVER copy the RSS article directly.
4375
- 2. Rewrite the information into a fresh, human-like article.
4376
- 3. Expand the topic with professional insights and explanations.
4377
- 4. Maintain a natural editorial tone.
4378
- 5. Make the article SEO-friendly.
4379
- 6. Use engaging headings and subheadings.
4380
- 7. Avoid robotic AI phrasing.
4381
- 8. Do not mention that the content came from RSS or feeds.
4382
- 9. Preserve factual accuracy from the source material.
4383
- 10. The output should feel like a professionally written industry blog.
4384
-
4385
- MULTIPLE FEEDS:
4386
- - If several feeds are provided, compare them: if they largely cover the same story or overlap heavily, produce ONE cohesive article.
4387
- - If they cover clearly different topics or distinct stories, produce MULTIPLE articles (one per distinct story).
4388
- - You may also produce one synthesis plus a short separate angle when that best serves the reader\u2014use your judgment.
4389
-
4390
- WRITING STYLE:
4391
- - Professional
4392
- - Clear
4393
- - Informative
4394
- - Business-focused
4395
- - Human sounding
4396
- - Modern editorial style
4397
-
4398
- ARTICLE STRUCTURE (each article \u2014 express in HTML):
4399
- 1. Engaging introduction
4400
- 2. Industry context
4401
- 3. Main developments
4402
- 4. Key implications
4403
- 5. Expert/business analysis
4404
- 6. Conclusion
4405
-
4406
- HTML STRUCTURE (STRICT \u2014 each article must be valid, semantic HTML only):
4407
- - Output HTML only. Do not use Markdown (no # headings, no **bold**, no \`code fences\`).
4408
- - Wrap each complete article in a single root: <article class="blog-post"> ... </article>
4409
- - Inside <article>, use this outline:
4410
- - <header><h1 class="blog-post-title">\u2026main title\u2026</h1></header> (exactly one h1 per article)
4411
- - <section class="blog-post-body"> for all following content
4412
- - Use <h2> and <h3> for section and subsection titles (never skip levels: h1 \u2192 h2 \u2192 h3).
4413
- - Use <p> for paragraphs; keep paragraphs focused (avoid huge unbroken text).
4414
- - Use <ul>/<ol> with <li> for lists where appropriate.
4415
- - Use <strong> and <em> for emphasis sparingly; use <blockquote> only when quoting or callouts fit.
4416
- - Do not include <html>, <head>, <body>, or document-level wrappers \u2014 only the fragment(s) described above.
4417
- - Do not use <script>, <style>, <iframe>, or inline event handlers. Avoid inline style="" except when essential for accessibility (prefer none).
4418
- - Escape angle brackets in body text if you must mention markup; keep output safe for embedding in a CMS.
4419
-
4420
- CONTENT REQUIREMENTS:
4421
- - Minimum 800 words per article unless source material is very small.
4422
- - Add context around industry trends where appropriate.
4423
- - Explain why the topic matters.
4424
- - Include practical implications for businesses/professionals.
4425
-
4426
- IF RSS CONTENT IS LIMITED:
4427
- - Expand intelligently using general industry knowledge.
4428
- - Keep the article relevant to the original topic.
4429
- - Do not invent fake facts or statistics.
4430
-
4431
- OUTPUT FORMAT (STRICT \u2014 HTML only):
4432
- - Return ONLY the article HTML fragment(s). No JSON, no preamble or postscript ("Here is your article\u2026"), no markdown.
4433
- - 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):
4434
- ${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR}
4435
- - If you output a single article, use one <article> only and do not use that separator line.`;
4436
- var BLOG_GENERATOR_DEFAULT_VALIDATION_RULES = JSON.stringify(
4437
- {
4438
- maxUserChars: 5e5,
4439
- 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.`
4440
- },
4441
- null,
4442
- 2
4443
- );
4444
-
4445
- // src/plugins/blog-generator/blog-generator-metadata-defaults.ts
4446
- var BLOG_METADATA_ENRICHER_AGENT_NAME = "Blog Generator Metadata";
4447
- var BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG = "blog-generator-metadata";
4448
- var BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a careful CMS metadata assistant for a publishing system.
4449
-
4450
- 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.
4451
-
4452
- RULES:
4453
- 1. Read the whole article and infer the primary topic and audience.
4454
- 2. categoryName must be EXACTLY one string from AVAILABLE_BLOG_CATEGORIES in the user message, or "" if none fit.
4455
- 3. blogSlug: lowercase kebab-case, ASCII letters/digits/hyphens only, no leading/trailing hyphens, max ~80 chars. Derive from the main topic or title.
4456
- 4. seo.title: concise meta title (~50\u201360 characters when reasonable).
4457
- 5. seo.description: meta description (~150\u2013160 characters when reasonable), plain text.
4458
- 6. seo.keywords: comma-separated phrases, no stuffing.
4459
- 7. seo.ogTitle / seo.ogDescription: may mirror title/description or be slightly adapted for social.
4460
- 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).
4461
- 9. Do not invent statistics, quotes, or URLs not implied by the article.
4462
- 10. Output JSON only \u2014 no markdown outside the JSON, no commentary.
4463
-
4464
- OUTPUT FORMAT (STRICT):
4465
- Return a single JSON object only. Do not wrap it in a markdown code fence.
4466
-
4467
- {
4468
- "categoryName": "string \u2014 exact match from list or empty string",
4469
- "blogSlug": "string \u2014 url-safe kebab-case",
4470
- "seo": {
4471
- "title": "string or empty",
4472
- "description": "string or empty",
4473
- "keywords": "string or empty",
4474
- "ogTitle": "string or empty",
4475
- "ogDescription": "string or empty"
4476
- },
4477
- "tags": ["tag-one", "tag-two"]
4478
- }`;
4479
- var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
4480
- {
4481
- maxUserChars: 5e5,
4482
- 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)."
4483
- },
4484
- null,
4485
- 2
4486
- );
4487
-
4488
- // src/plugins/blog-generator/blog-generator-social-defaults.ts
4489
- var BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG = "blog-generator-social";
4490
- var BLOG_SOCIAL_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a social copywriter for LinkedIn-style posts.
4491
-
4492
- 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.
4493
-
4494
- RULES:
4495
- 1. Plain text only in the JSON value (no HTML tags in socialMediaContent). Line breaks allowed as \\n if helpful.
4496
- 2. Length: aim for roughly 600\u20131300 characters (hard max 2900 characters) so it fits typical social feeds.
4497
- 3. Summarize the core insight; you may include 1\u20132 short hooks, but stay faithful to the article.
4498
- 4. Do not add URLs unless they appear explicitly in the article.
4499
- 5. Output JSON only \u2014 no markdown fences, no commentary outside JSON.
4500
-
4501
- OUTPUT FORMAT (STRICT):
4502
- Return a single JSON object only:
4503
- { "socialMediaContent": "string \u2014 the post text" }`;
4504
- var BLOG_SOCIAL_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
4505
- {
4506
- maxUserChars: 5e5,
4507
- guardrails: "Reply with one valid JSON object only. Key: socialMediaContent (string, plain text, max ~2900 chars). No markdown fences."
4508
- },
4509
- null,
4510
- 2
4511
- );
4512
-
4513
4909
  // src/api/rss-feed-blog-api.ts
4514
4910
  function deriveRssFeedDisplayName(rssUrl) {
4515
4911
  try {
@@ -4620,18 +5016,14 @@ async function runBlogGenerateFromSchedule(dataSource, entityMap, schedule, cms,
4620
5016
  let socialLlmAgentChatOptions;
4621
5017
  let metadataRow = null;
4622
5018
  let socialRow = null;
5019
+ let blogCreationRow = null;
4623
5020
  if (entityMap.llm_agents) {
4624
- const agentRepo = dataSource.getRepository(entityMap.llm_agents);
4625
- const agentRow = await agentRepo.findOne({
4626
- where: { slug: BLOG_GENERATOR_LLM_AGENT_SLUG, deleted: false, enabled: true }
4627
- });
4628
- if (agentRow) {
4629
- const o = llmAgentToChatAgentOptions(agentRow);
5021
+ blogCreationRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_CREATION);
5022
+ if (blogCreationRow) {
5023
+ const o = llmAgentToChatAgentOptions(blogCreationRow);
4630
5024
  llmAgentChatOptions = { model: o.model, temperature: o.temperature, max_tokens: o.max_tokens };
4631
5025
  }
4632
- metadataRow = await agentRepo.findOne({
4633
- where: { slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
4634
- });
5026
+ metadataRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_METADATA);
4635
5027
  if (metadataRow) {
4636
5028
  const mo = llmAgentToChatAgentOptions(metadataRow);
4637
5029
  metadataLlmAgentChatOptions = {
@@ -4640,9 +5032,7 @@ async function runBlogGenerateFromSchedule(dataSource, entityMap, schedule, cms,
4640
5032
  max_tokens: mo.max_tokens
4641
5033
  };
4642
5034
  }
4643
- socialRow = await agentRepo.findOne({
4644
- where: { slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
4645
- });
5035
+ socialRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST);
4646
5036
  if (socialRow) {
4647
5037
  const so = llmAgentToChatAgentOptions(socialRow);
4648
5038
  socialLlmAgentChatOptions = {
@@ -4687,6 +5077,8 @@ async function runBlogGenerateFromSchedule(dataSource, entityMap, schedule, cms,
4687
5077
  const out = await svc.generateBlogMarkdownFromRss({
4688
5078
  llm,
4689
5079
  rssUrls,
5080
+ systemInstruction: blogCreationRow?.systemInstruction?.trim() || void 0,
5081
+ validationRules: blogCreationRow?.validationRules?.trim() || void 0,
4690
5082
  categoryNamesHint: categoryRows.map((c) => c.name),
4691
5083
  tagNamesHint: tagNames,
4692
5084
  llmAgentChatOptions,
@@ -6782,16 +7174,19 @@ function normalizeChatModeSetting(raw) {
6782
7174
  if (raw === "external" || raw === "llm") return raw;
6783
7175
  return "whatsapp";
6784
7176
  }
6785
- async function loadLlmSettingsMap(dataSource, entityMap) {
7177
+ async function loadSettingsGroupMap(dataSource, entityMap, group) {
6786
7178
  if (!entityMap.configs) return {};
6787
7179
  const repo = dataSource.getRepository(entityMap.configs);
6788
- const rows = await repo.find({ where: { settings: "llm", deleted: false } });
7180
+ const rows = await repo.find({ where: { settings: group, deleted: false } });
6789
7181
  const out = {};
6790
7182
  for (const row of rows) {
6791
7183
  out[row.key] = row.value;
6792
7184
  }
6793
7185
  return out;
6794
7186
  }
7187
+ async function loadLlmSettingsMap(dataSource, entityMap) {
7188
+ return loadSettingsGroupMap(dataSource, entityMap, "llm");
7189
+ }
6795
7190
  function createChatHandlers(config) {
6796
7191
  const { dataSource, entityMap, json, getCms } = config;
6797
7192
  const contactRepo = () => dataSource.getRepository(entityMap.contacts);
@@ -6803,10 +7198,13 @@ function createChatHandlers(config) {
6803
7198
  try {
6804
7199
  const map = await loadLlmSettingsMap(dataSource, entityMap);
6805
7200
  const mode = normalizeChatModeSetting(map.chatMode);
7201
+ const chatbotAgent = mode === "llm" && entityMap.llm_agents ? await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_CHATBOT, {
7202
+ enabledOnly: false
7203
+ }) : null;
6806
7204
  const body = {
6807
7205
  enabled: map.enabled !== "false",
6808
7206
  chatMode: mode,
6809
- agentSlug: mode === "llm" ? (map.attachedAgentSlug ?? "").trim() : "",
7207
+ agentSlug: mode === "llm" ? chatbotAgent?.slug?.trim() || LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT] : "",
6810
7208
  botName: map.botName ?? "",
6811
7209
  icon: map.icon ?? "",
6812
7210
  iconImageUrl: map.iconImageUrl ?? "",
@@ -6892,24 +7290,26 @@ function createChatHandlers(config) {
6892
7290
  if (!llm?.chat) return json({ error: "LLM not configured" }, { status: 503 });
6893
7291
  const llmSettings = await loadLlmSettingsMap(dataSource, entityMap);
6894
7292
  const supportMode = normalizeChatModeSetting(llmSettings.chatMode);
6895
- let effectiveSlug = (body?.agentSlug ?? "").trim();
6896
- if (!effectiveSlug && supportMode === "llm" && entityMap.llm_agents) {
6897
- effectiveSlug = (llmSettings.attachedAgentSlug ?? "").trim();
6898
- }
7293
+ const bodyAgentSlug = (body?.agentSlug ?? "").trim();
6899
7294
  let agentRow = null;
6900
- if (effectiveSlug) {
6901
- if (!entityMap.llm_agents) {
6902
- return json({ error: "LLM agents are not configured on this deployment" }, { status: 400 });
6903
- }
6904
- const agentRepo = dataSource.getRepository(
6905
- entityMap.llm_agents
6906
- );
6907
- agentRow = await agentRepo.findOne({
6908
- where: { slug: effectiveSlug, deleted: false, enabled: true }
6909
- });
6910
- if (!agentRow && (body?.agentSlug ?? "").trim()) {
6911
- return json({ error: "Agent not found or disabled", agentSlug: effectiveSlug }, { status: 404 });
7295
+ let effectiveSlug = bodyAgentSlug;
7296
+ if (entityMap.llm_agents) {
7297
+ if (bodyAgentSlug) {
7298
+ const agentRepo = dataSource.getRepository(
7299
+ entityMap.llm_agents
7300
+ );
7301
+ agentRow = await agentRepo.findOne({
7302
+ where: { slug: bodyAgentSlug, deleted: false, enabled: true }
7303
+ });
7304
+ if (!agentRow) {
7305
+ return json({ error: "Agent not found or disabled", agentSlug: bodyAgentSlug }, { status: 404 });
7306
+ }
7307
+ } else if (supportMode === "llm") {
7308
+ agentRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_CHATBOT);
7309
+ effectiveSlug = agentRow?.slug?.trim() || LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
6912
7310
  }
7311
+ } else if (bodyAgentSlug) {
7312
+ return json({ error: "LLM agents are not configured on this deployment" }, { status: 400 });
6913
7313
  }
6914
7314
  console.info(RAG_LOG, "step 1 | resolve agent", {
6915
7315
  agentSlug: effectiveSlug || "(none)",
@@ -7005,8 +7405,141 @@ function createChatHandlers(config) {
7005
7405
  });
7006
7406
  }
7007
7407
  }
7008
- 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 }));
7009
- const history = historyBeforeCurrentUser(historyRaw, message);
7408
+ 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 }));
7409
+ const history = historyBeforeCurrentUser(historyRaw, message);
7410
+ const notifyEmailAgent = await findLlmAgentByScope(
7411
+ dataSource,
7412
+ entityMap,
7413
+ LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT,
7414
+ { enabledOnly: false }
7415
+ );
7416
+ const emailTool = resolveChatEmailToolSettings(llmSettings, {
7417
+ chatbotValidationRules: agentRow?.validationRules ?? null,
7418
+ notifyAgent: notifyEmailAgent ? {
7419
+ systemInstruction: notifyEmailAgent.systemInstruction,
7420
+ validationRules: notifyEmailAgent.validationRules
7421
+ } : null
7422
+ });
7423
+ const emailPlugin2 = cms.getPlugin("email");
7424
+ const convLeadSent = conv.leadEmailSentAt;
7425
+ const chatAgentFn = llm.chatAgent?.bind(llm);
7426
+ console.info(CHAT_EMAIL_LOG, "pipeline check", {
7427
+ conversationId,
7428
+ enabled: emailTool.enabled,
7429
+ intentCount: emailTool.intents.length,
7430
+ chatbotAgentId: agentRow?.id ?? null,
7431
+ chatbotAgentSlug: agentRow?.slug ?? null,
7432
+ notifyEmailAgentId: notifyEmailAgent?.id ?? null,
7433
+ notifyEmailAgentSlug: notifyEmailAgent?.slug ?? null,
7434
+ emailPluginPresent: Boolean(emailPlugin2),
7435
+ chatAgentFnPresent: Boolean(chatAgentFn),
7436
+ leadEmailAlreadySent: Boolean(convLeadSent),
7437
+ mergedPromptIncludesNotify: Boolean(notifyEmailAgent?.systemInstruction?.trim())
7438
+ });
7439
+ if (!emailTool.enabled) {
7440
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
7441
+ conversationId,
7442
+ reason: "email_tool_disabled"
7443
+ });
7444
+ } else if (emailTool.intents.length === 0) {
7445
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
7446
+ conversationId,
7447
+ reason: "no_intents_configured"
7448
+ });
7449
+ } else if (!emailPlugin2) {
7450
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
7451
+ conversationId,
7452
+ reason: "email_plugin_missing"
7453
+ });
7454
+ } else if (!chatAgentFn) {
7455
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
7456
+ conversationId,
7457
+ reason: "llm_chat_agent_unavailable"
7458
+ });
7459
+ } else if (convLeadSent) {
7460
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
7461
+ conversationId,
7462
+ reason: "lead_email_already_sent",
7463
+ leadEmailSentAt: convLeadSent
7464
+ });
7465
+ } else {
7466
+ const contactId = conv.contactId;
7467
+ const contactRow = await contactRepo().findOne({
7468
+ where: { id: contactId }
7469
+ });
7470
+ if (!contactRow) {
7471
+ console.warn(CHAT_EMAIL_LOG, "pipeline skipped", {
7472
+ conversationId,
7473
+ reason: "contact_not_found",
7474
+ contactId
7475
+ });
7476
+ } else {
7477
+ const c = contactRow;
7478
+ try {
7479
+ const intent = await detectChatLeadIntent({ chatAgent: chatAgentFn }, {
7480
+ settings: emailTool,
7481
+ message,
7482
+ history,
7483
+ contact: { name: c.name, email: c.email, phone: c.phone },
7484
+ model: agentRow?.model?.trim() || notifyEmailAgent?.model?.trim() || void 0
7485
+ });
7486
+ if (!intent.intent || !intent.emailTo) {
7487
+ console.info(CHAT_EMAIL_LOG, "no email sent", {
7488
+ conversationId,
7489
+ reason: intent.intent ? "missing_email_to" : "no_intent_match",
7490
+ intentLabel: intent.intentLabel,
7491
+ classifierReason: intent.reason
7492
+ });
7493
+ } else {
7494
+ const intentRecipients = parseIntentRecipientEmails(intent.emailTo);
7495
+ console.info(CHAT_EMAIL_LOG, "intent matched \u2014 sending", {
7496
+ conversationId,
7497
+ intent: intent.intent,
7498
+ emailTo: intent.emailTo,
7499
+ parsedRecipientCount: intentRecipients.length,
7500
+ recipients: intentRecipients
7501
+ });
7502
+ const emailSettings = await loadSettingsGroupMap(dataSource, entityMap, "email");
7503
+ const brandingSettings = await loadSettingsGroupMap(dataSource, entityMap, "branding");
7504
+ const companyDetails = mergeEmailLayoutCompanyDetails(brandingSettings, emailSettings);
7505
+ const leadResult = await sendChatLeadEmail(cms, emailSettings, brandingSettings, {
7506
+ contactName: String(c.name ?? "").trim() || "Visitor",
7507
+ contactEmail: String(c.email ?? "").trim(),
7508
+ contactPhone: c.phone ?? null,
7509
+ conversationId,
7510
+ latestMessage: message,
7511
+ intentCode: intent.intent,
7512
+ intentReason: intent.intentLabel || intent.reason,
7513
+ transcript: buildTranscriptForLeadEmail(history, message),
7514
+ companyDetails,
7515
+ recipients: intentRecipients.length > 0 ? intentRecipients : void 0
7516
+ });
7517
+ if (leadResult.sent) {
7518
+ await convRepo().update(conversationId, {
7519
+ leadEmailSentAt: /* @__PURE__ */ new Date()
7520
+ });
7521
+ console.info(CHAT_EMAIL_LOG, "lead email recorded", {
7522
+ conversationId,
7523
+ sent: true,
7524
+ recipients: leadResult.recipients
7525
+ });
7526
+ } else {
7527
+ console.warn(CHAT_EMAIL_LOG, "lead email not sent", {
7528
+ conversationId,
7529
+ sent: false,
7530
+ error: leadResult.error ?? "unknown",
7531
+ recipients: leadResult.recipients
7532
+ });
7533
+ }
7534
+ }
7535
+ } catch (intentErr) {
7536
+ console.warn(CHAT_EMAIL_LOG, "pipeline error", {
7537
+ conversationId,
7538
+ error: intentErr instanceof Error ? intentErr.message : String(intentErr)
7539
+ });
7540
+ }
7541
+ }
7542
+ }
7010
7543
  let content;
7011
7544
  const ragContext = contextParts.length > 0 ? contextParts.join("\n\n") : void 0;
7012
7545
  console.info(RAG_LOG, "step 7 | final context", {
@@ -7017,9 +7550,10 @@ function createChatHandlers(config) {
7017
7550
  });
7018
7551
  if (agentRow && llm.chatAgent) {
7019
7552
  const fromAgent = llmAgentToChatAgentOptions(agentRow);
7020
- const systemPrompt = mergeGuardrailsIntoSystemPrompt(
7553
+ const systemPrompt = buildChatbotSystemPromptWithEmailTool(
7021
7554
  fromAgent.systemPrompt,
7022
- parsedValidation.guardrailsForPrompt
7555
+ parsedValidation.guardrailsForPrompt,
7556
+ emailTool
7023
7557
  );
7024
7558
  const res = await llm.chatAgent({
7025
7559
  ...fromAgent,
@@ -7037,11 +7571,12 @@ ${contextParts.join("\n\n")}` : "";
7037
7571
  const defaultSystem = "You are a helpful assistant for the company. If you do not have specific information, say so.";
7038
7572
  let systemContent;
7039
7573
  if (agentRow) {
7040
- const base = agentRow.systemInstruction?.trim() || "";
7041
- systemContent = mergeGuardrailsIntoSystemPrompt(
7042
- [base, ragSystem].filter(Boolean).join("\n\n") || defaultSystem,
7043
- parsedValidation.guardrailsForPrompt
7044
- );
7574
+ const mergedBase = [agentRow.systemInstruction?.trim(), ragSystem].filter(Boolean).join("\n\n");
7575
+ systemContent = buildChatbotSystemPromptWithEmailTool(
7576
+ mergedBase || defaultSystem,
7577
+ parsedValidation.guardrailsForPrompt,
7578
+ emailTool
7579
+ ) || defaultSystem;
7045
7580
  } else {
7046
7581
  systemContent = ragSystem || defaultSystem;
7047
7582
  }
@@ -7068,6 +7603,154 @@ ${contextParts.join("\n\n")}` : "";
7068
7603
  };
7069
7604
  }
7070
7605
 
7606
+ // src/plugins/blog-generator/blog-generator-agent-defaults.ts
7607
+ var BLOG_GENERATOR_AGENT_NAME = "Blog Generator Agent";
7608
+ var BLOG_GENERATOR_LLM_AGENT_SLUG = "blog-generator";
7609
+ var BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR = "---BLOG_GENERATOR_NEXT---";
7610
+ var BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION = `You are a professional financial and business content writer.
7611
+
7612
+ 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.
7613
+
7614
+ IMPORTANT RULES:
7615
+
7616
+ 1. NEVER copy the RSS article directly.
7617
+ 2. Rewrite the information into a fresh, human-like article.
7618
+ 3. Expand the topic with professional insights and explanations.
7619
+ 4. Maintain a natural editorial tone.
7620
+ 5. Make the article SEO-friendly.
7621
+ 6. Use engaging headings and subheadings.
7622
+ 7. Avoid robotic AI phrasing.
7623
+ 8. Do not mention that the content came from RSS or feeds.
7624
+ 9. Preserve factual accuracy from the source material.
7625
+ 10. The output should feel like a professionally written industry blog.
7626
+
7627
+ MULTIPLE FEEDS:
7628
+ - If several feeds are provided, compare them: if they largely cover the same story or overlap heavily, produce ONE cohesive article.
7629
+ - If they cover clearly different topics or distinct stories, produce MULTIPLE articles (one per distinct story).
7630
+ - You may also produce one synthesis plus a short separate angle when that best serves the reader\u2014use your judgment.
7631
+
7632
+ WRITING STYLE:
7633
+ - Professional
7634
+ - Clear
7635
+ - Informative
7636
+ - Business-focused
7637
+ - Human sounding
7638
+ - Modern editorial style
7639
+
7640
+ ARTICLE STRUCTURE (each article \u2014 express in HTML):
7641
+ 1. Engaging introduction
7642
+ 2. Industry context
7643
+ 3. Main developments
7644
+ 4. Key implications
7645
+ 5. Expert/business analysis
7646
+ 6. Conclusion
7647
+
7648
+ HTML STRUCTURE (STRICT \u2014 each article must be valid, semantic HTML only):
7649
+ - Output HTML only. Do not use Markdown (no # headings, no **bold**, no \`code fences\`).
7650
+ - Wrap each complete article in a single root: <article class="blog-post"> ... </article>
7651
+ - Inside <article>, use this outline:
7652
+ - <header><h1 class="blog-post-title">\u2026main title\u2026</h1></header> (exactly one h1 per article)
7653
+ - <section class="blog-post-body"> for all following content
7654
+ - Use <h2> and <h3> for section and subsection titles (never skip levels: h1 \u2192 h2 \u2192 h3).
7655
+ - Use <p> for paragraphs; keep paragraphs focused (avoid huge unbroken text).
7656
+ - Use <ul>/<ol> with <li> for lists where appropriate.
7657
+ - Use <strong> and <em> for emphasis sparingly; use <blockquote> only when quoting or callouts fit.
7658
+ - Do not include <html>, <head>, <body>, or document-level wrappers \u2014 only the fragment(s) described above.
7659
+ - Do not use <script>, <style>, <iframe>, or inline event handlers. Avoid inline style="" except when essential for accessibility (prefer none).
7660
+ - Escape angle brackets in body text if you must mention markup; keep output safe for embedding in a CMS.
7661
+
7662
+ CONTENT REQUIREMENTS:
7663
+ - Minimum 800 words per article unless source material is very small.
7664
+ - Add context around industry trends where appropriate.
7665
+ - Explain why the topic matters.
7666
+ - Include practical implications for businesses/professionals.
7667
+
7668
+ IF RSS CONTENT IS LIMITED:
7669
+ - Expand intelligently using general industry knowledge.
7670
+ - Keep the article relevant to the original topic.
7671
+ - Do not invent fake facts or statistics.
7672
+
7673
+ OUTPUT FORMAT (STRICT \u2014 HTML only):
7674
+ - Return ONLY the article HTML fragment(s). No JSON, no preamble or postscript ("Here is your article\u2026"), no markdown.
7675
+ - 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):
7676
+ ${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR}
7677
+ - If you output a single article, use one <article> only and do not use that separator line.`;
7678
+ var BLOG_GENERATOR_DEFAULT_VALIDATION_RULES = JSON.stringify(
7679
+ {
7680
+ maxUserChars: 5e5,
7681
+ 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.`
7682
+ },
7683
+ null,
7684
+ 2
7685
+ );
7686
+
7687
+ // src/plugins/blog-generator/blog-generator-metadata-defaults.ts
7688
+ var BLOG_METADATA_ENRICHER_AGENT_NAME = "Blog Generator Metadata";
7689
+ var BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG = "blog-generator-metadata";
7690
+ var BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a careful CMS metadata assistant for a publishing system.
7691
+
7692
+ 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.
7693
+
7694
+ RULES:
7695
+ 1. Read the whole article and infer the primary topic and audience.
7696
+ 2. categoryName must be EXACTLY one string from AVAILABLE_BLOG_CATEGORIES in the user message, or "" if none fit.
7697
+ 3. blogSlug: lowercase kebab-case, ASCII letters/digits/hyphens only, no leading/trailing hyphens, max ~80 chars. Derive from the main topic or title.
7698
+ 4. seo.title: concise meta title (~50\u201360 characters when reasonable).
7699
+ 5. seo.description: meta description (~150\u2013160 characters when reasonable), plain text.
7700
+ 6. seo.keywords: comma-separated phrases, no stuffing.
7701
+ 7. seo.ogTitle / seo.ogDescription: may mirror title/description or be slightly adapted for social.
7702
+ 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).
7703
+ 9. Do not invent statistics, quotes, or URLs not implied by the article.
7704
+ 10. Output JSON only \u2014 no markdown outside the JSON, no commentary.
7705
+
7706
+ OUTPUT FORMAT (STRICT):
7707
+ Return a single JSON object only. Do not wrap it in a markdown code fence.
7708
+
7709
+ {
7710
+ "categoryName": "string \u2014 exact match from list or empty string",
7711
+ "blogSlug": "string \u2014 url-safe kebab-case",
7712
+ "seo": {
7713
+ "title": "string or empty",
7714
+ "description": "string or empty",
7715
+ "keywords": "string or empty",
7716
+ "ogTitle": "string or empty",
7717
+ "ogDescription": "string or empty"
7718
+ },
7719
+ "tags": ["tag-one", "tag-two"]
7720
+ }`;
7721
+ var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
7722
+ {
7723
+ maxUserChars: 5e5,
7724
+ 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)."
7725
+ },
7726
+ null,
7727
+ 2
7728
+ );
7729
+
7730
+ // src/plugins/blog-generator/blog-generator-social-defaults.ts
7731
+ var BLOG_SOCIAL_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a social copywriter for LinkedIn-style posts.
7732
+
7733
+ 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.
7734
+
7735
+ RULES:
7736
+ 1. Plain text only in the JSON value (no HTML tags in socialMediaContent). Line breaks allowed as \\n if helpful.
7737
+ 2. Length: aim for roughly 600\u20131300 characters (hard max 2900 characters) so it fits typical social feeds.
7738
+ 3. Summarize the core insight; you may include 1\u20132 short hooks, but stay faithful to the article.
7739
+ 4. Do not add URLs unless they appear explicitly in the article.
7740
+ 5. Output JSON only \u2014 no markdown fences, no commentary outside JSON.
7741
+
7742
+ OUTPUT FORMAT (STRICT):
7743
+ Return a single JSON object only:
7744
+ { "socialMediaContent": "string \u2014 the post text" }`;
7745
+ var BLOG_SOCIAL_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
7746
+ {
7747
+ maxUserChars: 5e5,
7748
+ guardrails: "Reply with one valid JSON object only. Key: socialMediaContent (string, plain text, max ~2900 chars). No markdown fences."
7749
+ },
7750
+ null,
7751
+ 2
7752
+ );
7753
+
7071
7754
  // src/plugins/blog-generator/blog-generator-service.ts
7072
7755
  var parser = new import_rss_parser.default({
7073
7756
  defaultRSS: 2
@@ -8803,29 +9486,6 @@ async function resolvePublicMetadata(args) {
8803
9486
  return out;
8804
9487
  }
8805
9488
 
8806
- // src/lib/email-recipients.ts
8807
- function parseEmailRecipientsFromConfig(raw) {
8808
- if (raw == null || raw === "") return [];
8809
- const trimmed = raw.trim();
8810
- if (trimmed.startsWith("[")) {
8811
- try {
8812
- const parsed = JSON.parse(trimmed);
8813
- if (Array.isArray(parsed)) {
8814
- return parsed.map((e) => String(e).trim()).filter(Boolean);
8815
- }
8816
- } catch {
8817
- }
8818
- }
8819
- return trimmed.split(/[,;]+/).map((s) => s.trim()).filter(Boolean);
8820
- }
8821
- function serializeEmailRecipients(emails) {
8822
- return JSON.stringify(emails);
8823
- }
8824
- function joinRecipientsForSend(emails) {
8825
- if (!emails.length) return null;
8826
- return emails.join(", ");
8827
- }
8828
-
8829
9489
  // src/lib/otp-challenge.ts
8830
9490
  var import_crypto3 = require("crypto");
8831
9491
  var import_typeorm8 = require("typeorm");
@@ -10461,6 +11121,7 @@ var Order = class {
10461
11121
  id;
10462
11122
  vendorId;
10463
11123
  orderNumber;
11124
+ qrToken;
10464
11125
  orderKind;
10465
11126
  parentOrderId;
10466
11127
  contactId;
@@ -10498,6 +11159,9 @@ __decorateClass([
10498
11159
  __decorateClass([
10499
11160
  (0, import_typeorm26.Column)("varchar")
10500
11161
  ], Order.prototype, "orderNumber", 2);
11162
+ __decorateClass([
11163
+ (0, import_typeorm26.Column)("varchar", { unique: true, nullable: true })
11164
+ ], Order.prototype, "qrToken", 2);
10501
11165
  __decorateClass([
10502
11166
  (0, import_typeorm26.Column)("varchar", { default: "sale" })
10503
11167
  ], Order.prototype, "orderKind", 2);
@@ -10726,6 +11390,7 @@ var ChatConversation = class {
10726
11390
  contactId;
10727
11391
  createdAt;
10728
11392
  updatedAt;
11393
+ leadEmailSentAt;
10729
11394
  contact;
10730
11395
  messages;
10731
11396
  };
@@ -10741,6 +11406,9 @@ __decorateClass([
10741
11406
  __decorateClass([
10742
11407
  (0, import_typeorm29.Column)({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
10743
11408
  ], ChatConversation.prototype, "updatedAt", 2);
11409
+ __decorateClass([
11410
+ (0, import_typeorm29.Column)({ type: "timestamp", nullable: true })
11411
+ ], ChatConversation.prototype, "leadEmailSentAt", 2);
10744
11412
  __decorateClass([
10745
11413
  (0, import_typeorm29.ManyToOne)(() => Contact, (c) => c.chatConversations, { onDelete: "CASCADE" }),
10746
11414
  (0, import_typeorm29.JoinColumn)({ name: "contactId" })
@@ -12951,6 +13619,59 @@ async function seedAdministratorPermissions(dataSource, entityMap) {
12951
13619
  }
12952
13620
  }
12953
13621
 
13622
+ // src/auth/middleware.ts
13623
+ var import_jwt = require("next-auth/jwt");
13624
+
13625
+ // src/auth/auth-debug.ts
13626
+ var LOG_PREFIX2 = "[cms-auth]";
13627
+ function isAuthDebugEnabled() {
13628
+ const v = process.env.CMS_AUTH_DEBUG?.trim().toLowerCase();
13629
+ if (v === "1" || v === "true" || v === "yes") return true;
13630
+ if (v === "0" || v === "false" || v === "no") return false;
13631
+ return process.env.NODE_ENV === "development";
13632
+ }
13633
+ function isAuthDebugClientEnabled() {
13634
+ if (typeof window === "undefined") return false;
13635
+ const v = process.env.NEXT_PUBLIC_CMS_AUTH_DEBUG?.trim().toLowerCase();
13636
+ if (v === "1" || v === "true" || v === "yes") return true;
13637
+ if (v === "0" || v === "false" || v === "no") return false;
13638
+ return process.env.NODE_ENV === "development";
13639
+ }
13640
+ function logAuth(message, data) {
13641
+ if (!isAuthDebugEnabled()) return;
13642
+ if (data) console.info(LOG_PREFIX2, message, data);
13643
+ else console.info(LOG_PREFIX2, message);
13644
+ }
13645
+ function logAuthClient(message, data) {
13646
+ if (!isAuthDebugClientEnabled()) return;
13647
+ if (data) console.info(LOG_PREFIX2, message, data);
13648
+ else console.info(LOG_PREFIX2, message);
13649
+ }
13650
+ function summarizeSessionUserForLog(user) {
13651
+ if (!user || typeof user !== "object") return { present: false };
13652
+ const u = user;
13653
+ return {
13654
+ present: true,
13655
+ email: u.email ?? null,
13656
+ id: u.id ?? null,
13657
+ adminAccess: u.adminAccess ?? null,
13658
+ isRBACAdmin: u.isRBACAdmin ?? null,
13659
+ isVendorPortal: u.isVendorPortal ?? null,
13660
+ groupName: u.groupName ?? null
13661
+ };
13662
+ }
13663
+ function nextAuthCookieDebugInfo() {
13664
+ const nextAuthUrl = process.env.NEXTAUTH_URL ?? "(unset)";
13665
+ const isHttps = nextAuthUrl.startsWith("https");
13666
+ return {
13667
+ nextAuthUrl,
13668
+ hasSecret: Boolean(process.env.NEXTAUTH_SECRET),
13669
+ cookieName: isHttps ? "__Secure-next-auth.session-token" : "next-auth.session-token",
13670
+ cookieSecure: isHttps,
13671
+ nodeEnv: process.env.NODE_ENV ?? "(unset)"
13672
+ };
13673
+ }
13674
+
12954
13675
  // src/auth/middleware.ts
12955
13676
  var defaultPublicApiMethods = {
12956
13677
  "/api/contacts": ["POST"],
@@ -12963,8 +13684,21 @@ var defaultPublicApiMethods = {
12963
13684
  "/api/users/set-password": ["POST"],
12964
13685
  "/api/users/invite": ["POST"]
12965
13686
  };
12966
- function defaultGetSessionToken(request) {
12967
- return request.cookies.get("__Secure-next-auth.session-token")?.value ?? request.cookies.get("next-auth.session-token")?.value;
13687
+ function legacyGetSessionToken(request) {
13688
+ const secure = request.cookies.get("__Secure-next-auth.session-token")?.value;
13689
+ const regular = request.cookies.get("next-auth.session-token")?.value;
13690
+ return secure ?? regular;
13691
+ }
13692
+ function sessionCookiePresence(request) {
13693
+ const secure = request.cookies.get("__Secure-next-auth.session-token")?.value;
13694
+ const regular = request.cookies.get("next-auth.session-token")?.value;
13695
+ const hasChunked = Boolean(request.cookies.get("next-auth.session-token.0")?.value) || Boolean(request.cookies.get("__Secure-next-auth.session-token.0")?.value);
13696
+ return {
13697
+ hasSecureToken: Boolean(secure),
13698
+ hasRegularToken: Boolean(regular),
13699
+ hasChunkedToken: hasChunked,
13700
+ resolved: Boolean(secure ?? regular)
13701
+ };
12968
13702
  }
12969
13703
  function isPublicMethod2(pathname, method, publicApiMethods) {
12970
13704
  for (const [endpoint, methods] of Object.entries(publicApiMethods)) {
@@ -12972,31 +13706,59 @@ function isPublicMethod2(pathname, method, publicApiMethods) {
12972
13706
  }
12973
13707
  return false;
12974
13708
  }
13709
+ async function hasValidSession(request, secret, legacyGetToken) {
13710
+ if (request.req) {
13711
+ const token = await (0, import_jwt.getToken)({ req: request.req, secret });
13712
+ return token != null;
13713
+ }
13714
+ return Boolean(legacyGetToken(request));
13715
+ }
12975
13716
  function createCmsMiddleware(config = {}) {
12976
13717
  const {
12977
13718
  publicAdminPaths = ["/admin/signin", "/admin/forgot-password", "/admin/reset-password", "/admin/invite"],
12978
13719
  publicApiMethods = defaultPublicApiMethods,
12979
13720
  signInPath = "/admin/signin",
12980
- getSessionToken = defaultGetSessionToken
13721
+ getSessionToken = legacyGetSessionToken,
13722
+ secret = process.env.NEXTAUTH_SECRET
12981
13723
  } = config;
12982
- return function cmsMiddleware(request) {
13724
+ return async function cmsMiddleware(request) {
12983
13725
  const pathname = request.nextUrl.pathname;
12984
13726
  const method = request.method;
12985
13727
  if (publicAdminPaths.some((p) => pathname === p || pathname.startsWith(p + "/"))) {
12986
13728
  return { type: "next" };
12987
13729
  }
12988
13730
  if (pathname.startsWith("/admin")) {
12989
- const token = getSessionToken(request);
12990
- if (!token) {
13731
+ const authenticated = await hasValidSession(request, secret, getSessionToken);
13732
+ const cookies = sessionCookiePresence(request);
13733
+ if (!authenticated) {
13734
+ logAuth("middleware: admin redirect to signin (no session)", {
13735
+ pathname,
13736
+ method,
13737
+ usesGetToken: Boolean(request.req),
13738
+ ...cookies,
13739
+ expectedCookieName: process.env.NEXTAUTH_URL?.startsWith("https") ? "__Secure-next-auth.session-token" : "next-auth.session-token"
13740
+ });
12991
13741
  return { type: "redirect", url: new URL(signInPath, request.url).toString() };
12992
13742
  }
13743
+ logAuth("middleware: admin allowed", {
13744
+ pathname,
13745
+ method,
13746
+ usesGetToken: Boolean(request.req),
13747
+ ...cookies
13748
+ });
12993
13749
  }
12994
13750
  if (pathname.startsWith("/api")) {
12995
13751
  if (isPublicMethod2(pathname, method, publicApiMethods)) {
12996
13752
  return { type: "next" };
12997
13753
  }
12998
- const token = getSessionToken(request);
12999
- if (!token) {
13754
+ const authenticated = await hasValidSession(request, secret, getSessionToken);
13755
+ if (!authenticated) {
13756
+ logAuth("middleware: api 401 (no session)", {
13757
+ pathname,
13758
+ method,
13759
+ usesGetToken: Boolean(request.req),
13760
+ ...sessionCookiePresence(request)
13761
+ });
13000
13762
  return { type: "json", status: 401, body: { error: "Unauthorized" } };
13001
13763
  }
13002
13764
  }
@@ -13051,6 +13813,7 @@ function getNextAuthOptions(config) {
13051
13813
  enableOtpLogin = false,
13052
13814
  authorizeOtp
13053
13815
  } = config;
13816
+ logAuth("getNextAuthOptions init", nextAuthCookieDebugInfo());
13054
13817
  const providers = [];
13055
13818
  if (enablePasswordLogin) {
13056
13819
  providers.push(
@@ -13061,15 +13824,43 @@ function getNextAuthOptions(config) {
13061
13824
  password: { label: "Password", type: "password" }
13062
13825
  },
13063
13826
  async authorize(credentials) {
13064
- if (!credentials?.email || !credentials?.password) return null;
13827
+ const email = credentials?.email?.trim() ?? "";
13828
+ if (!email || !credentials?.password) {
13829
+ logAuth("authorize(credentials) rejected", { reason: "missing_email_or_password" });
13830
+ return null;
13831
+ }
13065
13832
  try {
13066
- const user = await getUserByEmail(credentials.email);
13067
- if (!user || user.blocked || user.deleted || !user.password) return null;
13833
+ const user = await getUserByEmail(email);
13834
+ if (!user) {
13835
+ logAuth("authorize(credentials) rejected", { reason: "user_not_found", email });
13836
+ return null;
13837
+ }
13838
+ if (user.blocked) {
13839
+ logAuth("authorize(credentials) rejected", { reason: "blocked", email, userId: user.id });
13840
+ return null;
13841
+ }
13842
+ if (user.deleted) {
13843
+ logAuth("authorize(credentials) rejected", { reason: "deleted", email, userId: user.id });
13844
+ return null;
13845
+ }
13846
+ if (!user.password) {
13847
+ logAuth("authorize(credentials) rejected", { reason: "no_password", email, userId: user.id });
13848
+ return null;
13849
+ }
13068
13850
  const valid = await comparePassword(credentials.password, user.password);
13069
- if (!valid) return null;
13070
- return sessionUserFromNextAuthUser(user);
13851
+ if (!valid) {
13852
+ logAuth("authorize(credentials) rejected", { reason: "invalid_password", email, userId: user.id });
13853
+ return null;
13854
+ }
13855
+ const sessionUser = sessionUserFromNextAuthUser(user);
13856
+ logAuth("authorize(credentials) ok", summarizeSessionUserForLog(sessionUser));
13857
+ return sessionUser;
13071
13858
  } catch (err) {
13072
13859
  console.error("[cms-auth] authorize error (credentials):", err instanceof Error ? err.message : err);
13860
+ logAuth("authorize(credentials) error", {
13861
+ email,
13862
+ message: err instanceof Error ? err.message : String(err)
13863
+ });
13073
13864
  return null;
13074
13865
  }
13075
13866
  }
@@ -13122,6 +13913,10 @@ function getNextAuthOptions(config) {
13122
13913
  callbacks: {
13123
13914
  async jwt({ token, user, trigger, session }) {
13124
13915
  if (user) {
13916
+ logAuth("jwt callback: new sign-in", {
13917
+ trigger,
13918
+ user: summarizeSessionUserForLog(user)
13919
+ });
13125
13920
  const u = user;
13126
13921
  token.id = u.id;
13127
13922
  token.groupId = u.groupId;
@@ -13136,6 +13931,7 @@ function getNextAuthOptions(config) {
13136
13931
  token.isVendorOwner = u.isVendorOwner;
13137
13932
  }
13138
13933
  if (trigger === "update" && session && typeof session === "object") {
13934
+ logAuth("jwt callback: session update", { trigger });
13139
13935
  const s = session;
13140
13936
  const t = token;
13141
13937
  if (typeof s.name === "string") t.name = s.name;
@@ -13145,8 +13941,13 @@ function getNextAuthOptions(config) {
13145
13941
  return token;
13146
13942
  },
13147
13943
  async session({ session, token }) {
13944
+ const t = token;
13945
+ logAuth("session callback", {
13946
+ tokenHasId: t.id != null,
13947
+ tokenEmail: typeof t.email === "string" ? t.email : null,
13948
+ sessionUser: summarizeSessionUserForLog(session.user)
13949
+ });
13148
13950
  if (session.user) {
13149
- const t = token;
13150
13951
  if (typeof t.name === "string") session.user.name = t.name;
13151
13952
  if (typeof t.email === "string") session.user.email = t.email;
13152
13953
  session.user.id = t.id;
@@ -13309,6 +14110,26 @@ function validateAndNormalizeAddressRow(row) {
13309
14110
  return null;
13310
14111
  }
13311
14112
 
14113
+ // src/api/crud.ts
14114
+ var import_crypto4 = __toESM(require("crypto"), 1);
14115
+
14116
+ // src/plugins/llm/llm-agent-scope-crud.ts
14117
+ async function validateLlmAgentScopeForWrite(dataSource, entityMap, scope, excludeId) {
14118
+ if (scope == null || scope === "") return null;
14119
+ if (typeof scope !== "string" || !isLlmAgentScope(scope)) {
14120
+ return `Invalid agent scope. Allowed: chatbot, blog_creation, social_media_post, email_intent_chatbot, blog_metadata.`;
14121
+ }
14122
+ const entity = entityMap.llm_agents;
14123
+ if (!entity) return null;
14124
+ const repo = dataSource.getRepository(entity);
14125
+ const existing = await repo.findOne({
14126
+ where: { scope, deleted: false }
14127
+ });
14128
+ if (!existing) return null;
14129
+ if (excludeId != null && existing.id === excludeId) return null;
14130
+ return `An active agent already uses scope "${scope}".`;
14131
+ }
14132
+
13312
14133
  // src/api/crud.ts
13313
14134
  var CRUD_LOG = "[cms-crud]";
13314
14135
  function logCrudClientError(op, detail) {
@@ -13525,6 +14346,7 @@ function buildListFilterAndFromSearchParams(repo, searchParams) {
13525
14346
  if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
13526
14347
  if (!isListStringColumn(col)) continue;
13527
14348
  if (Object.prototype.hasOwnProperty.call(and, name)) continue;
14349
+ if (name === "scope") continue;
13528
14350
  const raw = searchParams.get(name)?.trim();
13529
14351
  if (!raw) continue;
13530
14352
  and[name] = (0, import_typeorm61.ILike)(`%${raw}%`);
@@ -13554,6 +14376,10 @@ function buildExactListParamWhere(repo, searchParams) {
13554
14376
  extraWhere[name] = raw === "true";
13555
14377
  }
13556
14378
  }
14379
+ const scopeParam = searchParams.get("scope")?.trim();
14380
+ if (scopeParam && columnNames.has("scope")) {
14381
+ extraWhere.scope = scopeParam;
14382
+ }
13557
14383
  return extraWhere;
13558
14384
  }
13559
14385
  function mergeDeletedFalseWhere(repo, where) {
@@ -13765,6 +14591,12 @@ function discountEvaluateRule(rule, cartLines, cartTotal) {
13765
14591
  case "minAmount":
13766
14592
  return discountCompare(cartTotal, rule.comparisonOperator, ruleValue);
13767
14593
  case "quantity": {
14594
+ if (rule.subType === "productId" && rule.value?.productId) {
14595
+ const targetProductId = Number(rule.value.productId);
14596
+ const requiredQty = Number(rule.value.v);
14597
+ const productQty = cartLines.filter((l) => l.productId === targetProductId).reduce((s, l) => s + l.quantity, 0);
14598
+ return discountCompare(productQty, rule.comparisonOperator, requiredQty);
14599
+ }
13768
14600
  const totalQty = cartLines.reduce((s, l) => s + l.quantity, 0);
13769
14601
  return discountCompare(totalQty, rule.comparisonOperator, ruleValue);
13770
14602
  }
@@ -13828,6 +14660,10 @@ function discountExtractBuyProductId(nodes) {
13828
14660
  const n = Number(v);
13829
14661
  if (Number.isFinite(n) && n > 0) return n;
13830
14662
  }
14663
+ if (node.conditionType === "rule" && node.type === "quantity" && node.subType === "productId") {
14664
+ const n = Number(node.value?.productId);
14665
+ if (Number.isFinite(n) && n > 0) return n;
14666
+ }
13831
14667
  if (node.children?.length) {
13832
14668
  const found = discountExtractBuyProductId(node.children);
13833
14669
  if (found !== null) return found;
@@ -14538,6 +15374,25 @@ function createCrudHandler(dataSource, entityMap, options) {
14538
15374
  if (pe) return pe;
14539
15375
  return null;
14540
15376
  }
15377
+ async function tryAssignSingleVendorOnAdminCreate(resource, scope, persistBody) {
15378
+ if (!resourceUsesVendorScope(resource) || scope.type !== "all") return;
15379
+ const currentVendorId = Number(persistBody.vendorId);
15380
+ if (Number.isFinite(currentVendorId) && currentVendorId > 0) return;
15381
+ if (!entityMap.vendors) return;
15382
+ const vendorRepo = dataSource.getRepository(entityMap.vendors);
15383
+ const where = mergeDeletedFalseWhere(vendorRepo, {});
15384
+ const rows = await vendorRepo.find({
15385
+ where,
15386
+ order: { id: "ASC" },
15387
+ take: 2
15388
+ });
15389
+ if (rows.length === 1) {
15390
+ const onlyVendorId = Number(rows[0].id);
15391
+ if (Number.isFinite(onlyVendorId) && onlyVendorId > 0) {
15392
+ persistBody.vendorId = onlyVendorId;
15393
+ }
15394
+ }
15395
+ }
14541
15396
  return {
14542
15397
  async GET(req, resource) {
14543
15398
  const authError = await authz(req, resource, "read");
@@ -15053,6 +15908,7 @@ function createCrudHandler(dataSource, entityMap, options) {
15053
15908
  const couponCode = String(body.couponCode).trim().toUpperCase();
15054
15909
  const persistBody2 = pickColumnUpdates(repo2, { ...body, couponCode });
15055
15910
  const scopeDiscount = await resolveScope();
15911
+ await tryAssignSingleVendorOnAdminCreate(resource, scopeDiscount, persistBody2);
15056
15912
  const vendorIdCheck2 = requireVendorIdForScopedCreate(
15057
15913
  resource,
15058
15914
  persistBody2,
@@ -15130,6 +15986,10 @@ function createCrudHandler(dataSource, entityMap, options) {
15130
15986
  });
15131
15987
  return json({ error: "Invalid request payload" }, { status: 400 });
15132
15988
  }
15989
+ if (resource === "llm_agents" && "scope" in persistBody) {
15990
+ const scopeErr = await validateLlmAgentScopeForWrite(dataSource, entityMap, persistBody.scope);
15991
+ if (scopeErr) return json({ error: scopeErr }, { status: 400 });
15992
+ }
15133
15993
  if (resource === "products") {
15134
15994
  if ("sku" in persistBody) {
15135
15995
  const skuNorm = normalizeProductSku(persistBody.sku);
@@ -15245,6 +16105,7 @@ function createCrudHandler(dataSource, entityMap, options) {
15245
16105
  );
15246
16106
  }
15247
16107
  const scopeCreate = await resolveScope();
16108
+ await tryAssignSingleVendorOnAdminCreate(resource, scopeCreate, persistBody);
15248
16109
  const vendorIdCheck = requireVendorIdForScopedCreate(resource, persistBody, scopeCreate, repo, body);
15249
16110
  if (!vendorIdCheck.ok) {
15250
16111
  return json({ error: vendorIdCheck.error }, { status: vendorIdCheck.status });
@@ -15287,6 +16148,7 @@ function createCrudHandler(dataSource, entityMap, options) {
15287
16148
  const randomPart = Math.floor(1e4 + Math.random() * 9e4);
15288
16149
  persistBody.contactId = contact.id;
15289
16150
  persistBody.orderNumber = `ORD00${randomPart}`;
16151
+ persistBody.qrToken = import_crypto4.default.randomBytes(16).toString("hex");
15290
16152
  }
15291
16153
  created = await repo.save(repo.create(persistBody));
15292
16154
  if (resource === "orders") {
@@ -16055,6 +16917,15 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
16055
16917
  const t = updatePayload.type;
16056
16918
  if (t === "" || t === "none" || t == null) updatePayload.type = null;
16057
16919
  }
16920
+ if (resource === "llm_agents" && "scope" in updatePayload) {
16921
+ const scopeErr = await validateLlmAgentScopeForWrite(
16922
+ dataSource,
16923
+ entityMap,
16924
+ updatePayload.scope,
16925
+ numericId
16926
+ );
16927
+ if (scopeErr) return json({ error: scopeErr }, { status: 400 });
16928
+ }
16058
16929
  if ((resource === "orders" || resource === "payments") && "contactId" in updatePayload && updatePayload.contactId != null && entityMap.vendor_customers) {
16059
16930
  const existingRow = await repo.findOne({
16060
16931
  where: { id: numericId }
@@ -16254,8 +17125,8 @@ function createForgotPasswordHandler(config) {
16254
17125
  const user = await userRepo.findOne({ where: { email }, select: ["email"] });
16255
17126
  const msg = "If an account exists with this email, you will receive a reset link shortly.";
16256
17127
  if (!user) return json({ message: msg }, { status: 200 });
16257
- const crypto3 = await import("crypto");
16258
- const token = crypto3.randomBytes(32).toString("hex");
17128
+ const crypto4 = await import("crypto");
17129
+ const token = crypto4.randomBytes(32).toString("hex");
16259
17130
  const expiresAt = new Date(Date.now() + resetExpiryHours * 60 * 60 * 1e3);
16260
17131
  const tokenRepo = dataSource.getRepository(entityMap.password_reset_tokens);
16261
17132
  await tokenRepo.save(tokenRepo.create({ email: user.email, token, expiresAt }));
@@ -18193,28 +19064,24 @@ function createCmsApiHandler(config) {
18193
19064
  let socialLlmAgentResolution = null;
18194
19065
  let metadataRow = null;
18195
19066
  let socialRow = null;
19067
+ let blogCreationRow = null;
18196
19068
  if (entityMap.llm_agents) {
18197
- const agentRepo = dataSource.getRepository(entityMap.llm_agents);
18198
- const agentRow = await agentRepo.findOne({
18199
- where: { slug: BLOG_GENERATOR_LLM_AGENT_SLUG, deleted: false, enabled: true }
18200
- });
18201
- if (agentRow) {
18202
- const o = llmAgentToChatAgentOptions(agentRow);
19069
+ blogCreationRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_CREATION);
19070
+ if (blogCreationRow) {
19071
+ const o = llmAgentToChatAgentOptions(blogCreationRow);
18203
19072
  llmAgentChatOptions = {
18204
19073
  model: o.model,
18205
19074
  temperature: o.temperature,
18206
19075
  max_tokens: o.max_tokens
18207
19076
  };
18208
19077
  llmAgentResolution = {
18209
- slug: BLOG_GENERATOR_LLM_AGENT_SLUG,
18210
- model: agentRow.model?.trim() || null,
18211
- temperature: agentRow.temperature ?? null,
18212
- maxTokens: agentRow.maxTokens ?? null
19078
+ slug: blogCreationRow.slug,
19079
+ model: blogCreationRow.model?.trim() || null,
19080
+ temperature: blogCreationRow.temperature ?? null,
19081
+ maxTokens: blogCreationRow.maxTokens ?? null
18213
19082
  };
18214
19083
  }
18215
- metadataRow = await agentRepo.findOne({
18216
- where: { slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
18217
- });
19084
+ metadataRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_METADATA);
18218
19085
  if (metadataRow) {
18219
19086
  const mo = llmAgentToChatAgentOptions(metadataRow);
18220
19087
  metadataLlmAgentChatOptions = {
@@ -18223,15 +19090,13 @@ function createCmsApiHandler(config) {
18223
19090
  max_tokens: mo.max_tokens
18224
19091
  };
18225
19092
  metadataLlmAgentResolution = {
18226
- slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG,
19093
+ slug: metadataRow.slug,
18227
19094
  model: metadataRow.model?.trim() || null,
18228
19095
  temperature: metadataRow.temperature ?? null,
18229
19096
  maxTokens: metadataRow.maxTokens ?? null
18230
19097
  };
18231
19098
  }
18232
- socialRow = await agentRepo.findOne({
18233
- where: { slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
18234
- });
19099
+ socialRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST);
18235
19100
  if (socialRow) {
18236
19101
  const so = llmAgentToChatAgentOptions(socialRow);
18237
19102
  socialLlmAgentChatOptions = {
@@ -18240,7 +19105,7 @@ function createCmsApiHandler(config) {
18240
19105
  max_tokens: so.max_tokens
18241
19106
  };
18242
19107
  socialLlmAgentResolution = {
18243
- slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG,
19108
+ slug: socialRow.slug,
18244
19109
  model: socialRow.model?.trim() || null,
18245
19110
  temperature: socialRow.temperature ?? null,
18246
19111
  maxTokens: socialRow.maxTokens ?? null
@@ -18264,8 +19129,8 @@ function createCmsApiHandler(config) {
18264
19129
  const out = await svc.generateBlogMarkdownFromRss({
18265
19130
  llm,
18266
19131
  rssUrls,
18267
- systemInstruction,
18268
- validationRules,
19132
+ systemInstruction: systemInstruction ?? blogCreationRow?.systemInstruction?.trim() ?? void 0,
19133
+ validationRules: validationRules ?? blogCreationRow?.validationRules?.trim() ?? void 0,
18269
19134
  categoryNamesHint: categoryRows.map((c) => c.name),
18270
19135
  tagNamesHint: tagNames,
18271
19136
  llmAgentChatOptions,
@@ -18750,6 +19615,22 @@ function createCmsApiHandler(config) {
18750
19615
  return config.json({ error: message }, { status: 500 });
18751
19616
  }
18752
19617
  }
19618
+ if (path2[0] === "track" && path2.length === 2 && m === "GET") {
19619
+ const token = path2[1];
19620
+ if (!token) return config.json({ error: "Token required" }, { status: 400 });
19621
+ try {
19622
+ const orderRepo = dataSource.getRepository(entityMap.orders);
19623
+ const order = await orderRepo.findOne({
19624
+ where: { qrToken: token, deleted: false },
19625
+ relations: ["contact", "items", "items.product"]
19626
+ });
19627
+ if (!order) return config.json({ error: "Order not found" }, { status: 404 });
19628
+ return config.json(order);
19629
+ } catch (err) {
19630
+ const message = err instanceof Error ? err.message : String(err);
19631
+ return config.json({ error: message }, { status: 500 });
19632
+ }
19633
+ }
18753
19634
  if (path2.length === 0) return config.json({ error: "Not found" }, { status: 404 });
18754
19635
  const resource = resolveResource(path2[0]);
18755
19636
  if (!crudResources.includes(resource)) {
@@ -19618,8 +20499,8 @@ function createStorefrontApiHandler(config) {
19618
20499
  let emailVerificationSent = false;
19619
20500
  if (requireEmailVerification && getCms) {
19620
20501
  try {
19621
- const crypto3 = await import("crypto");
19622
- const rawToken = crypto3.randomBytes(32).toString("hex");
20502
+ const crypto4 = await import("crypto");
20503
+ const rawToken = crypto4.randomBytes(32).toString("hex");
19623
20504
  const expiresAt = new Date(Date.now() + SIGNUP_VERIFY_EXPIRY_HOURS * 60 * 60 * 1e3);
19624
20505
  await tokenRepo().save(
19625
20506
  tokenRepo().create({ email, token: rawToken, expiresAt })
@@ -20388,6 +21269,8 @@ console.log("\u{1F525} USING LOCAL CMS CORE(index.ts loaded) \u{1F525}");
20388
21269
  hasEntityPermission,
20389
21270
  hashOtpCode,
20390
21271
  hydrateVendorSessionUser,
21272
+ isAuthDebugClientEnabled,
21273
+ isAuthDebugEnabled,
20391
21274
  isCustomerTypeContact,
20392
21275
  isOpenEndpoint,
20393
21276
  isPlatformAdministrator,
@@ -20406,6 +21289,8 @@ console.log("\u{1F525} USING LOCAL CMS CORE(index.ts loaded) \u{1F525}");
20406
21289
  loadPublicThemeSettings,
20407
21290
  loadUserVendorContext,
20408
21291
  localStoragePlugin,
21292
+ logAuth,
21293
+ logAuthClient,
20409
21294
  logEntityAccessDecision,
20410
21295
  logRbac,
20411
21296
  mergeEmailLayoutCompanyDetails,
@@ -20415,6 +21300,7 @@ console.log("\u{1F525} USING LOCAL CMS CORE(index.ts loaded) \u{1F525}");
20415
21300
  metaPostPageFeed,
20416
21301
  metaPostPagePhoto,
20417
21302
  metaResolvePageAccessToken,
21303
+ nextAuthCookieDebugInfo,
20418
21304
  normalizePhoneE164,
20419
21305
  parseBlogGeneratorAgentContent,
20420
21306
  parseBlogGeneratorModelOutput,
@@ -20462,6 +21348,7 @@ console.log("\u{1F525} USING LOCAL CMS CORE(index.ts loaded) \u{1F525}");
20462
21348
  smsPlugin,
20463
21349
  socialMediaPlugin,
20464
21350
  summarizeEntityPerms,
21351
+ summarizeSessionUserForLog,
20465
21352
  syncJobScheduleToPgBoss,
20466
21353
  truncateText,
20467
21354
  validateScheduleInput,