@rubytech/taskmaster 1.11.2 → 1.12.1

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.
@@ -6,7 +6,7 @@
6
6
  <title>Taskmaster Control</title>
7
7
  <meta name="color-scheme" content="dark light" />
8
8
  <link rel="icon" type="image/png" href="./favicon.png" />
9
- <script type="module" crossorigin src="./assets/index-s8s_YKvR.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-4h8fLLNN.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-CpaEIgQy.css">
11
11
  </head>
12
12
  <body>
@@ -5,7 +5,7 @@ import { resolveAgentWorkspaceRoot } from "../agents/agent-scope.js";
5
5
  import { buildAgentSummaries } from "../commands/agents.config.js";
6
6
  import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.js";
7
7
  import { resolveAgentBoundAccountId } from "../routing/bindings.js";
8
- import { detectOtpChannels } from "./public-chat/deliver-otp.js";
8
+ import { detectOtpChannels, normalizeVerifyMethods } from "./public-chat/deliver-otp.js";
9
9
  import { resolvePublicAgentId } from "./public-chat/session.js";
10
10
  import { findBrandLogo } from "./server-methods/brand.js";
11
11
  import { buildControlUiAvatarUrl, CONTROL_UI_AVATAR_PREFIX, normalizeControlUiBasePath, resolveAssistantAvatarUrl, } from "./control-ui-shared.js";
@@ -527,7 +527,9 @@ export function handlePublicChatHttpRequest(req, res, opts) {
527
527
  const avatarValue = resolvedAvatar && /^(https?:\/\/|data:image\/|\/)/i.test(resolvedAvatar)
528
528
  ? resolvedAvatar
529
529
  : undefined;
530
- const authMode = config.publicChat.auth ?? "anonymous";
530
+ const authMode = (config.publicChat?.accountSettings?.[accountId]?.auth ??
531
+ config.publicChat?.auth ??
532
+ "anonymous");
531
533
  const cookieTtlDays = config.publicChat.cookieTtlDays ?? 30;
532
534
  const brandName = config.ui?.brand?.name;
533
535
  const brandIconUrl = resolveBrandIconUrl(config.ui?.brand?.icon, root);
@@ -570,7 +572,7 @@ export function handlePublicChatHttpRequest(req, res, opts) {
570
572
  // Inject public-chat globals before </head>
571
573
  const publicScript = `<script>` +
572
574
  `window.__TASKMASTER_PUBLIC_CHAT__=true;` +
573
- `window.__TASKMASTER_PUBLIC_CHAT_CONFIG__=${JSON.stringify({ accountId, auth: authMode, cookieTtlDays, otpChannels, verifyMethods: config.publicChat?.verifyMethods ?? ["phone"], greeting: config.publicChat?.greetings?.[accountId] || undefined })};` +
575
+ `window.__TASKMASTER_PUBLIC_CHAT_CONFIG__=${JSON.stringify({ accountId, auth: authMode, cookieTtlDays, otpChannels, verifyMethods: normalizeVerifyMethods(config.publicChat?.accountSettings?.[accountId]?.verifyMethods ?? config.publicChat?.verifyMethods ?? ["whatsapp", "sms"]), greeting: config.publicChat?.greetings?.[accountId] || undefined })};` +
574
576
  `</script>`;
575
577
  const headClose = baseInjected.indexOf("</head>");
576
578
  const withPublic = headClose !== -1
@@ -584,24 +586,37 @@ export function handlePublicChatHttpRequest(req, res, opts) {
584
586
  /** Widget script content — self-contained JS for embedding. */
585
587
  const WIDGET_SCRIPT = `(function(){
586
588
  "use strict";
587
- var cfg={server:"",accountId:"",color:"#1a1a2e",bgColor:"#1a1a2e"};
589
+ var cfg={server:"",accountId:"",color:"",bgColor:"#1a1a2e",btnColor:""};
588
590
  var isOpen=false;
589
591
  var btn,overlay,iframe;
592
+ var SVG_NS="http://www.w3.org/2000/svg";
593
+ function svgEl(tag,attrs){var e=document.createElementNS(SVG_NS,tag);for(var k in attrs)e.setAttribute(k,attrs[k]);return e;}
594
+ function makeChatIcon(){var s=svgEl("svg",{width:"22",height:"22",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"});s.appendChild(svgEl("path",{d:"M7.9 20A9 9 0 1 0 4 16.1L2 22Z"}));return s;}
595
+ function makeCloseIcon(){var s=svgEl("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5","stroke-linecap":"round","stroke-linejoin":"round"});s.appendChild(svgEl("line",{x1:"18",y1:"6",x2:"6",y2:"18"}));s.appendChild(svgEl("line",{x1:"6",y1:"6",x2:"18",y2:"18"}));return s;}
596
+ function setIcon(el,fn){while(el.firstChild)el.removeChild(el.firstChild);el.appendChild(fn());}
590
597
 
591
598
  function init(opts){
592
599
  if(opts&&opts.server) cfg.server=opts.server.replace(/\\/$/,"");
593
600
  if(opts&&opts.accountId) cfg.accountId=opts.accountId;
594
- if(opts&&opts.color) cfg.color=opts.color;
601
+ if(opts&&opts.color) cfg.btnColor=opts.color;
595
602
  if(opts&&opts.bgColor) cfg.bgColor=opts.bgColor;
596
- build();
603
+ if(!cfg.btnColor){
604
+ fetch(cfg.server+"/public/api/v1/"+encodeURIComponent(cfg.accountId)+"/capabilities")
605
+ .then(function(r){return r.json()})
606
+ .then(function(d){if(d.background_color) cfg.btnColor=d.background_color; else if(d.accent_color) cfg.btnColor=d.accent_color; build();})
607
+ .catch(function(){build();});
608
+ } else {
609
+ build();
610
+ }
597
611
  }
598
612
 
599
613
  function build(){
614
+ if(!cfg.btnColor) cfg.btnColor="#1a1a2e";
600
615
  var css=document.createElement("style");
601
616
  css.textContent=[
602
617
  ".tm-widget-btn{position:fixed;bottom:20px;right:20px;width:48px;height:48px;",
603
- "border-radius:50%;background:"+cfg.color+";color:#fff;border:none;cursor:pointer;",
604
- "box-shadow:0 2px 8px rgba(0,0,0,.3);z-index:999999;font-size:22px;",
618
+ "border-radius:50%;background:"+cfg.btnColor+";color:#fff;border:none;cursor:pointer;",
619
+ "box-shadow:0 2px 8px rgba(0,0,0,.3);z-index:999999;",
605
620
  "display:flex;align-items:center;justify-content:center;transition:transform .2s}",
606
621
  ".tm-widget-btn:hover{transform:scale(1.08)}",
607
622
  ".tm-widget-overlay{position:fixed;bottom:78px;right:20px;width:400px;height:600px;",
@@ -618,7 +633,7 @@ const WIDGET_SCRIPT = `(function(){
618
633
 
619
634
  btn=document.createElement("button");
620
635
  btn.className="tm-widget-btn";
621
- btn.textContent="\\uD83D\\uDCAC";
636
+ setIcon(btn,makeChatIcon);
622
637
  btn.setAttribute("aria-label","Chat");
623
638
  btn.onclick=toggle;
624
639
  document.body.appendChild(btn);
@@ -635,7 +650,7 @@ const WIDGET_SCRIPT = `(function(){
635
650
  function toggle(){
636
651
  isOpen=!isOpen;
637
652
  overlay.classList.toggle("open",isOpen);
638
- btn.textContent=isOpen?"\\u2715":"\\uD83D\\uDCAC";
653
+ setIcon(btn,isOpen?makeCloseIcon:makeChatIcon);
639
654
  }
640
655
 
641
656
  window.Taskmaster={init:init};
@@ -45,24 +45,24 @@ async function fetchVerifiedSender(apiKey) {
45
45
  * in order: explicit config value → cached sender → live Brevo API lookup.
46
46
  * Returns null if no API key is configured.
47
47
  */
48
- export async function resolveEmailCredentials() {
48
+ export async function resolveEmailCredentials(fromName) {
49
49
  const cfg = loadConfig();
50
50
  const email = cfg.publicChat?.email;
51
51
  if (!email?.apiKey)
52
52
  return null;
53
53
  // Explicit from address in config — use it directly
54
54
  if (email.from)
55
- return { apiKey: email.apiKey, from: email.from };
55
+ return { apiKey: email.apiKey, from: email.from, fromName };
56
56
  // Cached sender for this API key
57
57
  if (cachedSender && cachedSender.apiKey === email.apiKey) {
58
- return { apiKey: email.apiKey, from: cachedSender.from };
58
+ return { apiKey: email.apiKey, from: cachedSender.from, fromName };
59
59
  }
60
60
  // Auto-detect from Brevo senders API
61
61
  const sender = await fetchVerifiedSender(email.apiKey);
62
62
  if (!sender)
63
63
  return null;
64
64
  cachedSender = { apiKey: email.apiKey, from: sender };
65
- return { apiKey: email.apiKey, from: sender };
65
+ return { apiKey: email.apiKey, from: sender, fromName };
66
66
  }
67
67
  /**
68
68
  * Send an email via the Brevo transactional email API.
@@ -76,7 +76,7 @@ export async function sendEmail(to, subject, body, creds) {
76
76
  Accept: "application/json",
77
77
  },
78
78
  body: JSON.stringify({
79
- sender: { email: creds.from },
79
+ sender: creds.fromName ? { email: creds.from, name: creds.fromName } : { email: creds.from },
80
80
  to: [{ email: to }],
81
81
  subject,
82
82
  textContent: body,
@@ -1,6 +1,9 @@
1
1
  /**
2
- * Deliver OTP verification codes via WhatsApp (primary), SMS (fallback), or email (Brevo).
2
+ * Deliver OTP verification codes via WhatsApp, SMS (Brevo), or email (Brevo).
3
3
  * Both SMS and email use the same Brevo API key — SMS requires prepaid credits.
4
+ *
5
+ * The admin controls which channels are enabled via `verifyMethods` config:
6
+ * "whatsapp", "sms", "email". The delivery logic only tries enabled channels.
4
7
  */
5
8
  import { getActiveWebListener } from "../../web/active-listener.js";
6
9
  import { sendMessageWhatsApp } from "../../web/outbound.js";
@@ -10,6 +13,27 @@ import { hasEmailApiKey, resolveEmailCredentials, sendEmail } from "./deliver-em
10
13
  function isEmail(identifier) {
11
14
  return identifier.includes("@");
12
15
  }
16
+ /**
17
+ * Normalize verify methods — expand deprecated "phone" into ["whatsapp", "sms"].
18
+ * Returns a deduplicated array of canonical method names.
19
+ */
20
+ export function normalizeVerifyMethods(methods) {
21
+ const result = new Set();
22
+ for (const m of methods) {
23
+ if (m === "phone") {
24
+ result.add("whatsapp");
25
+ result.add("sms");
26
+ }
27
+ else {
28
+ result.add(m);
29
+ }
30
+ }
31
+ return [...result];
32
+ }
33
+ /** Check whether any phone-based verify method is enabled. */
34
+ export function hasPhoneMethod(methods) {
35
+ return methods.includes("whatsapp") || methods.includes("sms");
36
+ }
13
37
  /**
14
38
  * Detect which OTP delivery channels are currently available.
15
39
  * Does not attempt delivery — used by the capabilities endpoint.
@@ -28,38 +52,48 @@ export function detectOtpChannels(whatsappAccountId) {
28
52
  * Deliver a verification code to the given identifier.
29
53
  *
30
54
  * Email identifiers (containing @) are sent via Brevo email API.
31
- * Phone identifiers use the existing WhatsApp -> SMS fallback chain.
32
- * Both SMS and email share the same Brevo API key.
55
+ * Phone identifiers try WhatsApp then SMS, but only channels that
56
+ * are enabled in `enabledMethods` are attempted.
33
57
  */
34
- export async function deliverOtp(identifier, code, accountId) {
58
+ export async function deliverOtp(identifier, code, accountId, enabledMethods, brandName) {
35
59
  const message = `Your verification code is: ${code}`;
60
+ const methods = enabledMethods ?? ["whatsapp", "sms", "email"];
36
61
  // Email identifiers — deliver via Brevo email API
37
62
  if (isEmail(identifier)) {
38
- const emailCreds = await resolveEmailCredentials();
63
+ const emailCreds = await resolveEmailCredentials(brandName);
39
64
  if (!emailCreds) {
40
65
  throw new Error("Email verification is not configured. Add a Brevo API key and verify a sender in the Brevo console.");
41
66
  }
42
67
  await sendEmail(identifier, "Your verification code", message, emailCreds);
43
68
  return { channel: "email" };
44
69
  }
45
- // Phone identifiers — WhatsApp first, SMS fallback
46
- const hasWhatsApp = !!getActiveWebListener(accountId);
47
- const smsCreds = await resolveSmsCredentials();
48
- if (hasWhatsApp) {
70
+ // Phone identifiers — try enabled channels in order: WhatsApp, then SMS
71
+ const tryWhatsApp = methods.includes("whatsapp") && !!getActiveWebListener(accountId);
72
+ const trySms = methods.includes("sms");
73
+ if (tryWhatsApp) {
49
74
  try {
50
75
  await sendMessageWhatsApp(identifier, message, { verbose: false, accountId });
51
76
  return { channel: "whatsapp" };
52
77
  }
53
78
  catch {
54
- // WhatsApp failed — fall through to SMS
79
+ // WhatsApp failed — fall through to SMS if enabled
55
80
  }
56
81
  }
57
- if (smsCreds) {
58
- await sendSms(identifier, message, smsCreds);
59
- return { channel: "sms" };
82
+ if (trySms) {
83
+ const smsCreds = await resolveSmsCredentials(brandName);
84
+ if (smsCreds) {
85
+ await sendSms(identifier, message, smsCreds);
86
+ return { channel: "sms" };
87
+ }
60
88
  }
61
- if (hasWhatsApp) {
62
- throw new Error("Failed to send verification code via WhatsApp and SMS is not configured.");
89
+ // Build a specific error message based on what was tried
90
+ const tried = [];
91
+ if (methods.includes("whatsapp"))
92
+ tried.push("WhatsApp");
93
+ if (methods.includes("sms"))
94
+ tried.push("SMS");
95
+ if (tried.length === 0) {
96
+ throw new Error("No phone verification channel is enabled. Enable WhatsApp or SMS in Public Chat settings.");
63
97
  }
64
- throw new Error("No verification channel available. Connect WhatsApp or add a Brevo API key with SMS credits.");
98
+ throw new Error(`Failed to send verification code via ${tried.join(" and ")}. Check that the channel is connected and configured.`);
65
99
  }
@@ -1,18 +1,37 @@
1
1
  /**
2
2
  * Lightweight Brevo SMS sender — single HTTP POST, no SDK dependency.
3
3
  *
4
- * Uses the same Brevo API key as email delivery. The sender name is
5
- * auto-detected from the verified email sender's display name.
4
+ * Uses the same Brevo API key as email delivery. The SMS sender name comes
5
+ * from the same Brevo verified sender that email uses matched by email
6
+ * address, so both channels present consistent branding.
6
7
  * SMS requires prepaid credits in the Brevo account.
7
8
  */
8
9
  import { loadConfig } from "../../config/config.js";
9
- /** Cached sender name avoids hitting the API on every SMS. */
10
+ import { resolveEmailCredentials } from "./deliver-email.js";
11
+ /** Cached sender name (already truncated to ≤11 chars). */
10
12
  let cachedSenderName = null;
11
13
  /**
12
- * Query Brevo for the first active sender's display name.
13
- * Returns the sender name or "Verify" as a safe fallback.
14
+ * Truncate a sender name to fit the GSM alphanumeric sender limit (11 chars).
15
+ * Tries to break at a word boundary to avoid ugly truncations.
14
16
  */
15
- async function fetchSenderName(apiKey) {
17
+ function truncateSender(name) {
18
+ if (name.length <= 11)
19
+ return name;
20
+ // Try the first word — often the business name
21
+ const firstWord = name.split(/\s/)[0];
22
+ if (firstWord.length <= 11)
23
+ return firstWord;
24
+ // Last resort: hard truncate
25
+ return name.slice(0, 11);
26
+ }
27
+ /**
28
+ * Resolve the SMS sender name from Brevo's verified senders.
29
+ *
30
+ * If an email address is provided (from config), finds the matching sender
31
+ * and uses its display name — this ensures SMS and email use the same identity.
32
+ * Falls back to the first active sender, then "Verify" as a safe default.
33
+ */
34
+ async function fetchSenderName(apiKey, matchEmail) {
16
35
  try {
17
36
  const res = await fetch("https://api.brevo.com/v3/senders", {
18
37
  headers: { "api-key": apiKey, Accept: "application/json" },
@@ -20,8 +39,17 @@ async function fetchSenderName(apiKey) {
20
39
  if (!res.ok)
21
40
  return "Verify";
22
41
  const json = (await res.json());
23
- const active = json.senders?.find((s) => s.active && s.name);
24
- return active?.name ?? json.senders?.[0]?.name ?? "Verify";
42
+ const senders = json.senders ?? [];
43
+ // Match the sender whose email matches the one configured for email delivery
44
+ if (matchEmail) {
45
+ const match = senders.find((s) => s.email?.toLowerCase() === matchEmail.toLowerCase() && s.name);
46
+ if (match?.name)
47
+ return truncateSender(match.name);
48
+ }
49
+ // Fallback: first active sender with a name
50
+ const active = senders.find((s) => s.active && s.name);
51
+ const raw = active?.name ?? senders[0]?.name ?? "Verify";
52
+ return truncateSender(raw);
25
53
  }
26
54
  catch {
27
55
  return "Verify";
@@ -38,19 +66,30 @@ export function hasSmsApiKey() {
38
66
  /**
39
67
  * Resolve SMS credentials from config.
40
68
  *
41
- * Uses the same Brevo API key as email. The sender name is resolved
42
- * from the cached value or fetched from the Brevo senders API.
69
+ * Uses the same Brevo API key as email. The sender name is resolved by asking
70
+ * the email delivery module which sender it uses, then finding that sender's
71
+ * display name in Brevo. This ensures both email and SMS present the same
72
+ * brand identity. The name is truncated to 11 characters (GSM limit).
43
73
  * Returns null if no API key is configured.
44
74
  */
45
- export async function resolveSmsCredentials() {
75
+ export async function resolveSmsCredentials(brandName) {
46
76
  const cfg = loadConfig();
47
77
  const apiKey = cfg.publicChat?.email?.apiKey;
48
78
  if (!apiKey)
49
79
  return null;
50
- if (cachedSenderName && cachedSenderName.apiKey === apiKey) {
80
+ // Brand name override takes precedence — use it directly (truncated to GSM limit)
81
+ if (brandName) {
82
+ return { apiKey, sender: truncateSender(brandName) };
83
+ }
84
+ if (cachedSenderName &&
85
+ cachedSenderName.apiKey === apiKey &&
86
+ cachedSenderName.name.length <= 11) {
51
87
  return { apiKey, sender: cachedSenderName.name };
52
88
  }
53
- const name = await fetchSenderName(apiKey);
89
+ // Resolve the email address that email delivery uses — ensures SMS matches
90
+ // the same Brevo sender regardless of whether `from` is set in config.
91
+ const emailCreds = await resolveEmailCredentials();
92
+ const name = await fetchSenderName(apiKey, emailCreds?.from);
54
93
  cachedSenderName = { apiKey, name };
55
94
  return { apiKey, sender: name };
56
95
  }
@@ -74,7 +113,7 @@ export async function sendSms(to, body, creds) {
74
113
  });
75
114
  if (!res.ok) {
76
115
  const text = await res.text().catch(() => "");
77
- throw new Error(`Brevo SMS failed (${res.status}): ${text}`);
116
+ throw new Error(`Brevo SMS failed (${res.status}): ${text} [sender=${JSON.stringify(creds.sender)}, recipient=${to}]`);
78
117
  }
79
118
  const json = (await res.json());
80
119
  return { id: String(json.messageId ?? "unknown") };
@@ -36,7 +36,7 @@ import { createInternalHookEvent, triggerInternalHook } from "../hooks/internal-
36
36
  import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
37
37
  import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
38
38
  import { requestOtp, verifyOtp } from "./public-chat/otp.js";
39
- import { deliverOtp, detectOtpChannels } from "./public-chat/deliver-otp.js";
39
+ import { deliverOtp, detectOtpChannels, hasPhoneMethod, normalizeVerifyMethods, } from "./public-chat/deliver-otp.js";
40
40
  import { buildPublicSessionKey, resolvePublicAgentId } from "./public-chat/session.js";
41
41
  import { loadSessionEntry, readSessionMessages } from "./session-utils.js";
42
42
  import { extractFileAttachments, sanitizeMediaForChat, stripEnvelopeFromMessages, } from "./chat-sanitize.js";
@@ -98,14 +98,23 @@ function isValidEmail(email) {
98
98
  function normalizePhone(raw) {
99
99
  return raw.replace(/[\s\-()]/g, "");
100
100
  }
101
+ /** Resolve the auth mode for a specific account, falling back to the global setting. */
102
+ function resolveAccountAuthMode(cfg, accountId) {
103
+ return cfg.publicChat?.accountSettings?.[accountId]?.auth ?? cfg.publicChat?.auth ?? "anonymous";
104
+ }
105
+ /** Resolve the verify methods for a specific account, falling back to the global setting. */
106
+ function resolveAccountVerifyMethods(cfg, accountId) {
107
+ return (cfg.publicChat?.accountSettings?.[accountId]?.verifyMethods ??
108
+ cfg.publicChat?.verifyMethods ?? ["whatsapp", "sms"]);
109
+ }
101
110
  /** Check whether the auth mode allows anonymous sessions. */
102
- function allowsAnonymous(cfg) {
103
- const mode = cfg.publicChat?.auth ?? "anonymous";
111
+ function allowsAnonymous(cfg, accountId) {
112
+ const mode = resolveAccountAuthMode(cfg, accountId);
104
113
  return mode === "anonymous" || mode === "choice";
105
114
  }
106
115
  /** Check whether the auth mode allows OTP verification. */
107
- function allowsVerified(cfg) {
108
- const mode = cfg.publicChat?.auth ?? "anonymous";
116
+ function allowsVerified(cfg, accountId) {
117
+ const mode = resolveAccountAuthMode(cfg, accountId);
109
118
  return mode === "verified" || mode === "choice";
110
119
  }
111
120
  function writeSse(res, data) {
@@ -184,7 +193,7 @@ async function handleSession(req, res, accountId, cfg, maxBodyBytes) {
184
193
  sendMethodNotAllowed(res);
185
194
  return;
186
195
  }
187
- if (!allowsAnonymous(cfg)) {
196
+ if (!allowsAnonymous(cfg, accountId)) {
188
197
  sendForbidden(res, "anonymous sessions are disabled — use OTP verification");
189
198
  return;
190
199
  }
@@ -214,7 +223,7 @@ async function handleOtpRequest(req, res, accountId, cfg, maxBodyBytes) {
214
223
  sendMethodNotAllowed(res);
215
224
  return;
216
225
  }
217
- if (!allowsVerified(cfg)) {
226
+ if (!allowsVerified(cfg, accountId)) {
218
227
  sendForbidden(res, "OTP verification is disabled for this account");
219
228
  return;
220
229
  }
@@ -228,10 +237,10 @@ async function handleOtpRequest(req, res, accountId, cfg, maxBodyBytes) {
228
237
  : typeof payload.phone === "string"
229
238
  ? payload.phone.trim()
230
239
  : "";
231
- const isEmail = rawIdentifier.includes("@");
232
- const verifyMethods = cfg.publicChat?.verifyMethods ?? ["phone"];
240
+ const isEmailId = rawIdentifier.includes("@");
241
+ const verifyMethods = normalizeVerifyMethods(resolveAccountVerifyMethods(cfg, accountId));
233
242
  let identifier;
234
- if (isEmail) {
243
+ if (isEmailId) {
235
244
  if (!verifyMethods.includes("email")) {
236
245
  sendForbidden(res, "email verification is not enabled for this account");
237
246
  return;
@@ -243,7 +252,7 @@ async function handleOtpRequest(req, res, accountId, cfg, maxBodyBytes) {
243
252
  }
244
253
  }
245
254
  else {
246
- if (!verifyMethods.includes("phone")) {
255
+ if (!hasPhoneMethod(verifyMethods)) {
247
256
  sendForbidden(res, "phone verification is not enabled for this account");
248
257
  return;
249
258
  }
@@ -265,8 +274,14 @@ async function handleOtpRequest(req, res, accountId, cfg, maxBodyBytes) {
265
274
  // OTP code is sent from the correct number (not the first active account).
266
275
  const agentId = resolvePublicAgentId(cfg, accountId);
267
276
  const whatsappAccountId = resolveAgentBoundAccountId(cfg, agentId, "whatsapp") ?? undefined;
277
+ // Narrow delivery to visitor's preferred channel if specified and admin-enabled
278
+ const preferredChannel = typeof payload.channel === "string" ? payload.channel : undefined;
279
+ const deliveryMethods = preferredChannel && verifyMethods.includes(preferredChannel)
280
+ ? [preferredChannel]
281
+ : verifyMethods;
282
+ const brandName = accountId || undefined;
268
283
  try {
269
- const delivery = await deliverOtp(identifier, result.code, whatsappAccountId);
284
+ const delivery = await deliverOtp(identifier, result.code, whatsappAccountId, deliveryMethods, brandName);
270
285
  sendJson(res, 200, { ok: true, channel: delivery.channel });
271
286
  }
272
287
  catch (err) {
@@ -282,7 +297,7 @@ async function handleOtpVerify(req, res, accountId, cfg, maxBodyBytes) {
282
297
  sendMethodNotAllowed(res);
283
298
  return;
284
299
  }
285
- if (!allowsVerified(cfg)) {
300
+ if (!allowsVerified(cfg, accountId)) {
286
301
  sendForbidden(res, "OTP verification is disabled for this account");
287
302
  return;
288
303
  }
@@ -750,14 +765,19 @@ async function handleCapabilities(req, res, accountId, cfg) {
750
765
  sendMethodNotAllowed(res, "GET");
751
766
  return;
752
767
  }
753
- const authMode = cfg.publicChat?.auth ?? "anonymous";
754
- const verifyMethods = cfg.publicChat?.verifyMethods ?? ["phone"];
768
+ const authMode = resolveAccountAuthMode(cfg, accountId);
769
+ const verifyMethods = normalizeVerifyMethods(resolveAccountVerifyMethods(cfg, accountId));
755
770
  const agentId = resolvePublicAgentId(cfg, accountId);
756
771
  const whatsappAccountId = resolveAgentBoundAccountId(cfg, agentId, "whatsapp") ?? undefined;
757
772
  const channels = detectOtpChannels(whatsappAccountId);
773
+ const wsBrand = cfg.workspaces?.[accountId]?.brand;
774
+ const accentColor = wsBrand?.accentColor ?? cfg.ui?.seamColor ?? null;
775
+ const backgroundColor = wsBrand?.backgroundColor ?? null;
758
776
  sendJson(res, 200, {
759
777
  auth: authMode,
760
778
  verify_methods: verifyMethods,
779
+ accent_color: accentColor,
780
+ background_color: backgroundColor,
761
781
  otp: {
762
782
  available: channels.length > 0,
763
783
  channels,
@@ -6,7 +6,7 @@ import { resolveAgentBoundAccountId } from "../../routing/bindings.js";
6
6
  import { generateGreeting } from "../../suggestions/greeting.js";
7
7
  import { ErrorCodes, errorShape } from "../protocol/index.js";
8
8
  import { requestOtp, verifyOtp } from "../public-chat/otp.js";
9
- import { deliverOtp } from "../public-chat/deliver-otp.js";
9
+ import { deliverOtp, hasPhoneMethod, normalizeVerifyMethods } from "../public-chat/deliver-otp.js";
10
10
  import { buildPublicSessionKey, resolvePublicAgentId } from "../public-chat/session.js";
11
11
  /** Strip spaces, dashes, and parentheses from a phone number. */
12
12
  function normalizePhone(raw) {
@@ -46,10 +46,11 @@ export const publicChatHandlers = {
46
46
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "public chat disabled"));
47
47
  return;
48
48
  }
49
- const isEmail = rawIdentifier.includes("@");
50
- const verifyMethods = cfg.publicChat?.verifyMethods ?? ["phone"];
49
+ const isEmailId = rawIdentifier.includes("@");
50
+ const verifyMethods = normalizeVerifyMethods(cfg.publicChat?.accountSettings?.[accountId ?? ""]?.verifyMethods ??
51
+ cfg.publicChat?.verifyMethods ?? ["whatsapp", "sms"]);
51
52
  let identifier;
52
- if (isEmail) {
53
+ if (isEmailId) {
53
54
  if (!verifyMethods.includes("email")) {
54
55
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "email verification is not enabled"));
55
56
  return;
@@ -61,7 +62,7 @@ export const publicChatHandlers = {
61
62
  }
62
63
  }
63
64
  else {
64
- if (!verifyMethods.includes("phone")) {
65
+ if (!hasPhoneMethod(verifyMethods)) {
65
66
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "phone verification is not enabled"));
66
67
  return;
67
68
  }
@@ -82,8 +83,14 @@ export const publicChatHandlers = {
82
83
  const whatsappAccountId = agentId
83
84
  ? (resolveAgentBoundAccountId(cfg, agentId, "whatsapp") ?? undefined)
84
85
  : undefined;
86
+ // Narrow delivery to visitor's preferred channel if specified and admin-enabled
87
+ const preferredChannel = typeof params.channel === "string" ? params.channel : undefined;
88
+ const deliveryMethods = preferredChannel && verifyMethods.includes(preferredChannel)
89
+ ? [preferredChannel]
90
+ : verifyMethods;
91
+ const brandName = accountId || undefined;
85
92
  try {
86
- const delivery = await deliverOtp(identifier, result.code, whatsappAccountId);
93
+ const delivery = await deliverOtp(identifier, result.code, whatsappAccountId, deliveryMethods, brandName);
87
94
  respond(true, { ok: true, channel: delivery.channel });
88
95
  }
89
96
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.11.2",
3
+ "version": "1.12.1",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -2,7 +2,9 @@
2
2
 
3
3
  Walk the user through purchasing SMS credits in Brevo. SMS verification uses the same Brevo API key as email — no additional configuration in Taskmaster. The user just needs credits in their Brevo account.
4
4
 
5
- **Important:** SMS credits are prepaid and never expire. Pricing varies by country (see below). The SMS sender name is auto-detected from the user's verified email sender in Brevo — no manual configuration needed.
5
+ **Important:** SMS credits are prepaid and never expire. The cost depends on the destination country and number of messages.
6
+
7
+ **SMS sender name:** The name that appears on the SMS (e.g. "Taskmaster") is taken from the same Brevo verified sender used for email — matched by email address. If you have multiple senders in Brevo, the one whose email matches the configured sender is used. The name is truncated to 11 characters (GSM limit) — if longer, the first word is used (e.g. "Rubytech LLC" becomes "Rubytech"). To control the SMS sender name, edit the sender's display name in Brevo under **Senders, Domains & Dedicated IPs → Senders**.
6
8
 
7
9
  ---
8
10
 
@@ -21,38 +23,43 @@ Walk the user through purchasing SMS credits in Brevo. SMS verification uses the
21
23
 
22
24
  ## Step 2: Purchase SMS credits
23
25
 
24
- > "1. Go to **brevo.com** and log in
25
- > 2. In the **top-right toolbar**, click the **gear icon** (⚙️) to open **Settings**
26
- > 3. In the left sidebar, look for **Your plan** or **Billing** under your account settings
27
- > 4. Find the **SMS credits** section and click **Buy credits**
28
- > 5. Choose how many credits you need:
29
- > - **100 credits** is a good starting amount for verification codes
30
- > - Each SMS uses 1 credit
31
- > - Credits never expire
32
- > 6. Complete the purchase
26
+ **Navigation: Dashboard home lightning icon (top-right toolbar) → "Campaign usage and plan" popup → SMS section → "Get more credits".**
27
+
28
+ > "1. Go to **app.brevo.com** and log in you should see the main dashboard ('Hello [name]')
29
+ > 2. In the **top-right toolbar**, click the **lightning bolt icon** (⚡) — it's to the left of the help (?) and gear (⚙️) icons
30
+ > 3. A **'Campaign usage and plan'** popup appears showing your plan, email credits, and SMS credits
31
+ > 4. Under the **SMS** section (shows '0 credits left' if you haven't bought any), click **'Get more credits'**
32
+ > 5. On the **'Add messages credits'** page:
33
+ > - **Type of message** — select **SMS**
34
+ > - **Number of messages** — enter **100** (a good starting amount for verification codes)
35
+ > - **Message destination** — select your country (e.g. **United Kingdom**)
36
+ > - The **Purchase summary** on the right shows the total cost
37
+ > 6. Click **'Proceed to checkout'** and complete the purchase
33
38
  >
34
39
  > Let me know when that's done."
35
40
 
41
+ If the user can't find the lightning icon:
42
+
43
+ > "In the top-right toolbar of the Brevo dashboard, there are several small icons. The lightning bolt (⚡) is the first one on the left, before the help (?), gear (⚙️), bell (🔔), and chart icons. Click that — it opens a popup showing your plan and credits."
44
+
36
45
  Wait for the user to confirm.
37
46
 
38
47
  ## Step 3: Confirm
39
48
 
40
- > "SMS verification is now enabled. When a visitor verifies their phone number and WhatsApp is unavailable, the verification code will be sent by text message instead. No restart or configuration change needed — it works automatically with your existing Brevo API key."
49
+ > "SMS credits are ready. To enable SMS verification, go to the **Setup** page → **Public Chat** settings → check the **SMS** checkbox under verification methods. You can enable SMS alongside WhatsApp, or on its own. When a visitor chooses SMS on the public chat page, the code is sent by text message. No restart needed — it uses your existing Brevo API key."
41
50
 
42
51
  ---
43
52
 
44
53
  ## Pricing reference
45
54
 
46
- Approximate SMS credit costs (varies by country):
55
+ Pricing depends on the destination country and volume. The exact cost is shown on the checkout page when you select a country and message count. As a rough guide:
47
56
 
48
- | Region | Cost per 100 credits |
49
- |--------|---------------------|
57
+ | Destination | 100 messages |
58
+ |-------------|-------------|
59
+ | UK | ~£2.82 |
50
60
  | US / Canada | ~$1.09 |
51
- | UK | ~$3.45 |
52
- | Australia | ~$5.50 |
53
- | EU (varies) | ~$3.00–$7.00 |
54
61
 
55
- Exact pricing is shown during purchase in the Brevo dashboard.
62
+ Credits are called "message credits" and can be used for SMS. On the free plan, WhatsApp messages via Brevo require a Professional plan upgrade — but we don't use Brevo for WhatsApp, so this doesn't apply.
56
63
 
57
64
  ---
58
65
 
@@ -61,8 +68,9 @@ Exact pricing is shown during purchase in the Brevo dashboard.
61
68
  | Question | Answer |
62
69
  |----------|--------|
63
70
  | "Do credits expire?" | No — prepaid SMS credits never expire. |
64
- | "How many credits per SMS?" | One credit per message. Each verification code is one SMS. |
71
+ | "How many credits per SMS?" | Depends on the destination country. The purchase page shows the exact conversion (e.g. 316.5 credits = 100 UK messages). |
65
72
  | "What phone number do SMS come from?" | Brevo assigns a sender automatically based on the destination country. You don't need to buy or manage a phone number. |
66
- | "What name appears on the SMS?" | The sender name from your verified email sender in Brevo (e.g. your business name). Taskmaster detects this automatically. |
73
+ | "What name appears on the SMS?" | The display name from the same Brevo verified sender used for email (matched by email address). Must be 11 characters or fewer — if longer, the first word is used. Edit sender names under **Senders, Domains & Dedicated IPs → Senders** in Brevo. |
67
74
  | "Do I need to change anything in Taskmaster?" | No. If your Brevo API key is already configured, SMS works automatically when you have credits. |
68
75
  | "Can I use SMS without email?" | The Brevo API key is configured through the email setup, but once configured it serves both email and SMS. You need at least one verified sender. |
76
+ | "Where do I check my remaining credits?" | Click the lightning bolt icon (⚡) in the top-right toolbar. The popup shows your SMS credits remaining. |