@rubytech/taskmaster 1.12.0 → 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.
- package/dist/build-info.json +3 -3
- package/dist/config/zod-schema.js +18 -0
- package/dist/control-ui/assets/{index-DpMaqt-b.js → index-4h8fLLNN.js} +76 -74
- package/dist/control-ui/assets/index-4h8fLLNN.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/gateway/control-ui.js +24 -9
- package/dist/gateway/public-chat/deliver-email.js +5 -5
- package/dist/gateway/public-chat/deliver-otp.js +3 -3
- package/dist/gateway/public-chat/deliver-sms.js +5 -1
- package/dist/gateway/public-chat-api.js +26 -11
- package/dist/gateway/server-methods/public-chat.js +4 -2
- package/package.json +1 -1
- package/dist/control-ui/assets/index-DpMaqt-b.js.map +0 -1
|
@@ -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-
|
|
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>
|
|
@@ -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
|
|
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: normalizeVerifyMethods(config.publicChat?.verifyMethods ?? ["whatsapp", "sms"]), 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:"
|
|
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.
|
|
601
|
+
if(opts&&opts.color) cfg.btnColor=opts.color;
|
|
595
602
|
if(opts&&opts.bgColor) cfg.bgColor=opts.bgColor;
|
|
596
|
-
|
|
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.
|
|
604
|
-
"box-shadow:0 2px 8px rgba(0,0,0,.3);z-index:999999;
|
|
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
|
|
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
|
|
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,
|
|
@@ -55,12 +55,12 @@ export function detectOtpChannels(whatsappAccountId) {
|
|
|
55
55
|
* Phone identifiers try WhatsApp then SMS, but only channels that
|
|
56
56
|
* are enabled in `enabledMethods` are attempted.
|
|
57
57
|
*/
|
|
58
|
-
export async function deliverOtp(identifier, code, accountId, enabledMethods) {
|
|
58
|
+
export async function deliverOtp(identifier, code, accountId, enabledMethods, brandName) {
|
|
59
59
|
const message = `Your verification code is: ${code}`;
|
|
60
60
|
const methods = enabledMethods ?? ["whatsapp", "sms", "email"];
|
|
61
61
|
// Email identifiers — deliver via Brevo email API
|
|
62
62
|
if (isEmail(identifier)) {
|
|
63
|
-
const emailCreds = await resolveEmailCredentials();
|
|
63
|
+
const emailCreds = await resolveEmailCredentials(brandName);
|
|
64
64
|
if (!emailCreds) {
|
|
65
65
|
throw new Error("Email verification is not configured. Add a Brevo API key and verify a sender in the Brevo console.");
|
|
66
66
|
}
|
|
@@ -80,7 +80,7 @@ export async function deliverOtp(identifier, code, accountId, enabledMethods) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
if (trySms) {
|
|
83
|
-
const smsCreds = await resolveSmsCredentials();
|
|
83
|
+
const smsCreds = await resolveSmsCredentials(brandName);
|
|
84
84
|
if (smsCreds) {
|
|
85
85
|
await sendSms(identifier, message, smsCreds);
|
|
86
86
|
return { channel: "sms" };
|
|
@@ -72,11 +72,15 @@ export function hasSmsApiKey() {
|
|
|
72
72
|
* brand identity. The name is truncated to 11 characters (GSM limit).
|
|
73
73
|
* Returns null if no API key is configured.
|
|
74
74
|
*/
|
|
75
|
-
export async function resolveSmsCredentials() {
|
|
75
|
+
export async function resolveSmsCredentials(brandName) {
|
|
76
76
|
const cfg = loadConfig();
|
|
77
77
|
const apiKey = cfg.publicChat?.email?.apiKey;
|
|
78
78
|
if (!apiKey)
|
|
79
79
|
return null;
|
|
80
|
+
// Brand name override takes precedence — use it directly (truncated to GSM limit)
|
|
81
|
+
if (brandName) {
|
|
82
|
+
return { apiKey, sender: truncateSender(brandName) };
|
|
83
|
+
}
|
|
80
84
|
if (cachedSenderName &&
|
|
81
85
|
cachedSenderName.apiKey === apiKey &&
|
|
82
86
|
cachedSenderName.name.length <= 11) {
|
|
@@ -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
|
|
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
|
|
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
|
}
|
|
@@ -229,7 +238,7 @@ async function handleOtpRequest(req, res, accountId, cfg, maxBodyBytes) {
|
|
|
229
238
|
? payload.phone.trim()
|
|
230
239
|
: "";
|
|
231
240
|
const isEmailId = rawIdentifier.includes("@");
|
|
232
|
-
const verifyMethods = normalizeVerifyMethods(cfg
|
|
241
|
+
const verifyMethods = normalizeVerifyMethods(resolveAccountVerifyMethods(cfg, accountId));
|
|
233
242
|
let identifier;
|
|
234
243
|
if (isEmailId) {
|
|
235
244
|
if (!verifyMethods.includes("email")) {
|
|
@@ -270,8 +279,9 @@ async function handleOtpRequest(req, res, accountId, cfg, maxBodyBytes) {
|
|
|
270
279
|
const deliveryMethods = preferredChannel && verifyMethods.includes(preferredChannel)
|
|
271
280
|
? [preferredChannel]
|
|
272
281
|
: verifyMethods;
|
|
282
|
+
const brandName = accountId || undefined;
|
|
273
283
|
try {
|
|
274
|
-
const delivery = await deliverOtp(identifier, result.code, whatsappAccountId, deliveryMethods);
|
|
284
|
+
const delivery = await deliverOtp(identifier, result.code, whatsappAccountId, deliveryMethods, brandName);
|
|
275
285
|
sendJson(res, 200, { ok: true, channel: delivery.channel });
|
|
276
286
|
}
|
|
277
287
|
catch (err) {
|
|
@@ -287,7 +297,7 @@ async function handleOtpVerify(req, res, accountId, cfg, maxBodyBytes) {
|
|
|
287
297
|
sendMethodNotAllowed(res);
|
|
288
298
|
return;
|
|
289
299
|
}
|
|
290
|
-
if (!allowsVerified(cfg)) {
|
|
300
|
+
if (!allowsVerified(cfg, accountId)) {
|
|
291
301
|
sendForbidden(res, "OTP verification is disabled for this account");
|
|
292
302
|
return;
|
|
293
303
|
}
|
|
@@ -755,14 +765,19 @@ async function handleCapabilities(req, res, accountId, cfg) {
|
|
|
755
765
|
sendMethodNotAllowed(res, "GET");
|
|
756
766
|
return;
|
|
757
767
|
}
|
|
758
|
-
const authMode = cfg
|
|
759
|
-
const verifyMethods = normalizeVerifyMethods(cfg
|
|
768
|
+
const authMode = resolveAccountAuthMode(cfg, accountId);
|
|
769
|
+
const verifyMethods = normalizeVerifyMethods(resolveAccountVerifyMethods(cfg, accountId));
|
|
760
770
|
const agentId = resolvePublicAgentId(cfg, accountId);
|
|
761
771
|
const whatsappAccountId = resolveAgentBoundAccountId(cfg, agentId, "whatsapp") ?? undefined;
|
|
762
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;
|
|
763
776
|
sendJson(res, 200, {
|
|
764
777
|
auth: authMode,
|
|
765
778
|
verify_methods: verifyMethods,
|
|
779
|
+
accent_color: accentColor,
|
|
780
|
+
background_color: backgroundColor,
|
|
766
781
|
otp: {
|
|
767
782
|
available: channels.length > 0,
|
|
768
783
|
channels,
|
|
@@ -47,7 +47,8 @@ export const publicChatHandlers = {
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
const isEmailId = rawIdentifier.includes("@");
|
|
50
|
-
const verifyMethods = normalizeVerifyMethods(cfg.publicChat?.
|
|
50
|
+
const verifyMethods = normalizeVerifyMethods(cfg.publicChat?.accountSettings?.[accountId ?? ""]?.verifyMethods ??
|
|
51
|
+
cfg.publicChat?.verifyMethods ?? ["whatsapp", "sms"]);
|
|
51
52
|
let identifier;
|
|
52
53
|
if (isEmailId) {
|
|
53
54
|
if (!verifyMethods.includes("email")) {
|
|
@@ -87,8 +88,9 @@ export const publicChatHandlers = {
|
|
|
87
88
|
const deliveryMethods = preferredChannel && verifyMethods.includes(preferredChannel)
|
|
88
89
|
? [preferredChannel]
|
|
89
90
|
: verifyMethods;
|
|
91
|
+
const brandName = accountId || undefined;
|
|
90
92
|
try {
|
|
91
|
-
const delivery = await deliverOtp(identifier, result.code, whatsappAccountId, deliveryMethods);
|
|
93
|
+
const delivery = await deliverOtp(identifier, result.code, whatsappAccountId, deliveryMethods, brandName);
|
|
92
94
|
respond(true, { ok: true, channel: delivery.channel });
|
|
93
95
|
}
|
|
94
96
|
catch (err) {
|