@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.js CHANGED
@@ -2237,6 +2237,58 @@ function escapeHtml7(s) {
2237
2237
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 render10 = getTemplateRenderer(templateName);
2268
- if (!render10) {
2320
+ const render11 = getTemplateRenderer(templateName);
2321
+ if (!render11) {
2269
2322
  throw new Error(`Unknown email template: ${templateName}`);
2270
2323
  }
2271
- return render10(ctx);
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
- const agentRepo = dataSource.getRepository(entityMap.llm_agents);
4379
- const agentRow = await agentRepo.findOne({
4380
- where: { slug: BLOG_GENERATOR_LLM_AGENT_SLUG, deleted: false, enabled: true }
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 agentRepo.findOne({
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 agentRepo.findOne({
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 loadLlmSettingsMap(dataSource, entityMap) {
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: "llm", deleted: false } });
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" ? (map.attachedAgentSlug ?? "").trim() : "",
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
- let effectiveSlug = (body?.agentSlug ?? "").trim();
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
- if (effectiveSlug) {
6655
- if (!entityMap.llm_agents) {
6656
- return json({ error: "LLM agents are not configured on this deployment" }, { status: 400 });
6657
- }
6658
- const agentRepo = dataSource.getRepository(
6659
- entityMap.llm_agents
6660
- );
6661
- agentRow = await agentRepo.findOne({
6662
- where: { slug: effectiveSlug, deleted: false, enabled: true }
6663
- });
6664
- if (!agentRow && (body?.agentSlug ?? "").trim()) {
6665
- return json({ error: "Agent not found or disabled", agentSlug: effectiveSlug }, { status: 404 });
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 = mergeGuardrailsIntoSystemPrompt(
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 base = agentRow.systemInstruction?.trim() || "";
6795
- systemContent = mergeGuardrailsIntoSystemPrompt(
6796
- [base, ragSystem].filter(Boolean).join("\n\n") || defaultSystem,
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 defaultGetSessionToken(request) {
12753
- return request.cookies.get("__Secure-next-auth.session-token")?.value ?? request.cookies.get("next-auth.session-token")?.value;
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 = defaultGetSessionToken
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 token = getSessionToken(request);
12776
- if (!token) {
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 token = getSessionToken(request);
12785
- if (!token) {
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
- if (!credentials?.email || !credentials?.password) return null;
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(credentials.email);
12853
- if (!user || user.blocked || user.deleted || !user.password) return null;
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) return null;
12856
- return sessionUserFromNextAuthUser(user);
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 crypto3 = await import("crypto");
16044
- const token = crypto3.randomBytes(32).toString("hex");
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
- const agentRepo = dataSource.getRepository(entityMap.llm_agents);
17984
- const agentRow = await agentRepo.findOne({
17985
- where: { slug: BLOG_GENERATOR_LLM_AGENT_SLUG, deleted: false, enabled: true }
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: BLOG_GENERATOR_LLM_AGENT_SLUG,
17996
- model: agentRow.model?.trim() || null,
17997
- temperature: agentRow.temperature ?? null,
17998
- maxTokens: agentRow.maxTokens ?? null
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 agentRepo.findOne({
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: BLOG_METADATA_ENRICHER_LLM_AGENT_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 agentRepo.findOne({
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: BLOG_SOCIAL_ENRICHER_LLM_AGENT_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 crypto3 = await import("crypto");
19408
- const rawToken = crypto3.randomBytes(32).toString("hex");
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,