@kodelyth/zalouser 2026.5.39 → 2026.6.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.
@@ -0,0 +1,19 @@
1
+ import { n as zalouserSetupWizard } from "./setup-surface-Cfj4GQlB.js";
2
+ import { n as createZalouserSetupWizardProxy, r as zalouserSetupAdapter } from "./shared-DjK0e2FC.js";
3
+ import { r as setZalouserRuntime, t as zalouserPlugin } from "./channel-pby_3Sur.js";
4
+ import { n as isZalouserMutableGroupEntry, t as collectZalouserSecurityAuditFindings } from "./security-audit-D_rftvs-.js";
5
+ import { t as zalouserSetupPlugin } from "./channel.setup-CqyWwqcQ.js";
6
+ import { t as createZalouserTool } from "./api-DSWT4Dh_.js";
7
+ import { buildBaseAccountStatusSnapshot } from "klaw/plugin-sdk/status-helpers";
8
+ import { formatAllowFromLowercase, mergeAllowlist, summarizeMapping } from "klaw/plugin-sdk/allow-from";
9
+ import { DEFAULT_ACCOUNT_ID, buildChannelConfigSchema, normalizeAccountId } from "klaw/plugin-sdk/core";
10
+ import { isDangerousNameMatchingEnabled } from "klaw/plugin-sdk/dangerous-name-runtime";
11
+ import { chunkTextForOutbound } from "klaw/plugin-sdk/text-chunking";
12
+ import { deliverTextOrMediaReply, isNumericTargetId, resolveSendableOutboundReplyParts, sendPayloadWithChunkedTextAndMedia } from "klaw/plugin-sdk/reply-payload";
13
+ import { createChannelMessageReplyPipeline } from "klaw/plugin-sdk/channel-message";
14
+ import { createChannelPairingController } from "klaw/plugin-sdk/channel-pairing";
15
+ import { resolvePreferredKlawTmpDir } from "klaw/plugin-sdk/temp-path";
16
+ import { loadOutboundMediaFromUrl } from "klaw/plugin-sdk/outbound-media";
17
+ import { resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "klaw/plugin-sdk/runtime-group-policy";
18
+ import { resolveInboundMentionDecision } from "klaw/plugin-sdk/channel-inbound";
19
+ export { DEFAULT_ACCOUNT_ID, buildBaseAccountStatusSnapshot, buildChannelConfigSchema, chunkTextForOutbound, collectZalouserSecurityAuditFindings, createChannelMessageReplyPipeline, createChannelPairingController, createZalouserSetupWizardProxy, createZalouserTool, deliverTextOrMediaReply, formatAllowFromLowercase, isDangerousNameMatchingEnabled, isNumericTargetId, isZalouserMutableGroupEntry, loadOutboundMediaFromUrl, mergeAllowlist, normalizeAccountId, resolveDefaultGroupPolicy, resolveInboundMentionDecision, resolveOpenProviderRuntimeGroupPolicy, resolvePreferredKlawTmpDir, resolveSendableOutboundReplyParts, sendPayloadWithChunkedTextAndMedia, setZalouserRuntime, summarizeMapping, warnMissingProviderGroupPolicyFallbackOnce, zalouserPlugin, zalouserSetupAdapter, zalouserSetupPlugin, zalouserSetupWizard };
@@ -0,0 +1,5 @@
1
+ //#region extensions/zalouser/secret-contract-api.ts
2
+ const secretTargetRegistryEntries = [];
3
+ function collectRuntimeConfigAssignments() {}
4
+ //#endregion
5
+ export { collectRuntimeConfigAssignments, secretTargetRegistryEntries };
@@ -0,0 +1,34 @@
1
+ import { isDangerousNameMatchingEnabled } from "klaw/plugin-sdk/dangerous-name-runtime";
2
+ //#region extensions/zalouser/src/security-audit.ts
3
+ function isZalouserMutableGroupEntry(raw) {
4
+ const text = raw.trim();
5
+ if (!text || text === "*") return false;
6
+ const normalized = text.replace(/^(zalouser|zlu):/i, "").replace(/^group:/i, "").trim();
7
+ if (!normalized) return false;
8
+ if (/^\d+$/.test(normalized)) return false;
9
+ return !/^g-\S+$/i.test(normalized);
10
+ }
11
+ function collectZalouserSecurityAuditFindings(params) {
12
+ const zalouserCfg = params.account.config ?? {};
13
+ const accountId = params.accountId?.trim() || params.account.accountId || "default";
14
+ const dangerousNameMatchingEnabled = isDangerousNameMatchingEnabled(zalouserCfg);
15
+ const zalouserPathPrefix = params.orderedAccountIds.length > 1 || params.hasExplicitAccountPath ? `channels.zalouser.accounts.${accountId}` : "channels.zalouser";
16
+ const mutableGroupEntries = /* @__PURE__ */ new Set();
17
+ const groups = zalouserCfg.groups;
18
+ if (groups && typeof groups === "object" && !Array.isArray(groups)) for (const key of Object.keys(groups)) {
19
+ if (!isZalouserMutableGroupEntry(key)) continue;
20
+ mutableGroupEntries.add(`${zalouserPathPrefix}.groups:${key}`);
21
+ }
22
+ if (mutableGroupEntries.size === 0) return [];
23
+ const examples = Array.from(mutableGroupEntries).slice(0, 5);
24
+ const more = mutableGroupEntries.size > examples.length ? ` (+${mutableGroupEntries.size - examples.length} more)` : "";
25
+ return [{
26
+ checkId: "channels.zalouser.groups.mutable_entries",
27
+ severity: dangerousNameMatchingEnabled ? "info" : "warn",
28
+ title: dangerousNameMatchingEnabled ? "Zalouser group routing uses break-glass name matching" : "Zalouser group routing contains mutable group entries",
29
+ detail: dangerousNameMatchingEnabled ? `Zalouser group-name routing is explicitly enabled via dangerouslyAllowNameMatching. This mutable-identity mode is operator-selected break-glass behavior and out-of-scope for vulnerability reports by itself. Found: ${examples.join(", ")}${more}.` : `Zalouser group auth is ID-only by default, so unresolved group-name or slug entries are ignored for auth and can drift from the intended trusted group. Found: ${examples.join(", ")}${more}.`,
30
+ remediation: dangerousNameMatchingEnabled ? "Prefer stable Zalo group IDs (for example group:<id> or provider-native g- ids), then disable dangerouslyAllowNameMatching." : "Prefer stable Zalo group IDs in channels.zalouser.groups, or explicitly opt in with dangerouslyAllowNameMatching=true if you accept mutable group-name matching."
31
+ }];
32
+ }
33
+ //#endregion
34
+ export { isZalouserMutableGroupEntry as n, collectZalouserSecurityAuditFindings as t };
@@ -0,0 +1,542 @@
1
+ import { S as TextStyle, _ as sendZaloTypingEvent, f as sendZaloDeliveredEvent, g as sendZaloTextMessage, h as sendZaloSeenEvent, m as sendZaloReaction, p as sendZaloLink, x as createZalouserSendReceipt } from "./zalo-js-B80cRyDF.js";
2
+ //#region extensions/zalouser/src/text-styles.ts
3
+ const ESCAPE_SENTINEL_START = "";
4
+ const ESCAPE_SENTINEL_END = "";
5
+ const TAG_STYLE_MAP = {
6
+ red: TextStyle.Red,
7
+ orange: TextStyle.Orange,
8
+ yellow: TextStyle.Yellow,
9
+ green: TextStyle.Green,
10
+ small: null,
11
+ big: TextStyle.Big,
12
+ underline: TextStyle.Underline
13
+ };
14
+ const INLINE_MARKERS = [
15
+ {
16
+ pattern: /`([^`\n]+)`/g,
17
+ extractText: (match) => match[0],
18
+ literal: true
19
+ },
20
+ {
21
+ pattern: /\\([*_~#\\{}>+\-`])/g,
22
+ extractText: (match) => match[1],
23
+ literal: true
24
+ },
25
+ {
26
+ pattern: new RegExp(`\\{(${Object.keys(TAG_STYLE_MAP).join("|")})\\}(.+?)\\{/\\1\\}`, "g"),
27
+ extractText: (match) => match[2],
28
+ resolveStyles: (match) => {
29
+ const style = TAG_STYLE_MAP[match[1]];
30
+ return style ? [style] : [];
31
+ }
32
+ },
33
+ {
34
+ pattern: /(?<!\*)\*\*\*(?=\S)([^\n]*?\S)(?<!\*)\*\*\*(?!\*)/g,
35
+ extractText: (match) => match[1],
36
+ resolveStyles: () => [TextStyle.Bold, TextStyle.Italic]
37
+ },
38
+ {
39
+ pattern: /(?<!\*)\*\*(?![\s*])([^\n]*?\S)(?<!\*)\*\*(?!\*)/g,
40
+ extractText: (match) => match[1],
41
+ resolveStyles: () => [TextStyle.Bold]
42
+ },
43
+ {
44
+ pattern: /(?<![\w_])__(?![\s_])([^\n]*?\S)(?<!_)__(?![\w_])/g,
45
+ extractText: (match) => match[1],
46
+ resolveStyles: () => [TextStyle.Bold]
47
+ },
48
+ {
49
+ pattern: /(?<!~)~~(?=\S)([^\n]*?\S)(?<!~)~~(?!~)/g,
50
+ extractText: (match) => match[1],
51
+ resolveStyles: () => [TextStyle.StrikeThrough]
52
+ },
53
+ {
54
+ pattern: /(?<!\*)\*(?![\s*])([^\n]*?\S)(?<!\*)\*(?!\*)/g,
55
+ extractText: (match) => match[1],
56
+ resolveStyles: () => [TextStyle.Italic]
57
+ },
58
+ {
59
+ pattern: /(?<![\w_])_(?![\s_])([^\n]*?\S)(?<!_)_(?![\w_])/g,
60
+ extractText: (match) => match[1],
61
+ resolveStyles: () => [TextStyle.Italic]
62
+ }
63
+ ];
64
+ function parseZalouserTextStyles(input) {
65
+ const allStyles = [];
66
+ const escapeMap = [];
67
+ const lines = input.replace(/\r\n?/g, "\n").split("\n");
68
+ const lineStyles = [];
69
+ const processedLines = [];
70
+ let activeFence = null;
71
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
72
+ const rawLine = lines[lineIndex];
73
+ const { text: unquotedLine, indent: baseIndent } = stripQuotePrefix(rawLine);
74
+ if (activeFence) {
75
+ const codeLine = activeFence.quoteIndent > 0 ? stripQuotePrefix(rawLine, activeFence.quoteIndent).text : rawLine;
76
+ if (isClosingFence(codeLine, activeFence)) {
77
+ activeFence = null;
78
+ continue;
79
+ }
80
+ processedLines.push(escapeLiteralText(normalizeCodeBlockLeadingWhitespace(stripCodeFenceIndent(codeLine, activeFence.indent)), escapeMap));
81
+ continue;
82
+ }
83
+ let line = unquotedLine;
84
+ const openingFence = resolveOpeningFence(rawLine);
85
+ if (openingFence) {
86
+ const fenceLine = openingFence.quoteIndent > 0 ? unquotedLine : rawLine;
87
+ if (!hasClosingFence(lines, lineIndex + 1, openingFence)) {
88
+ processedLines.push(escapeLiteralText(fenceLine, escapeMap));
89
+ activeFence = openingFence;
90
+ continue;
91
+ }
92
+ activeFence = openingFence;
93
+ continue;
94
+ }
95
+ const outputLineIndex = processedLines.length;
96
+ if (isIndentedCodeBlockLine(line)) {
97
+ if (baseIndent > 0) lineStyles.push({
98
+ lineIndex: outputLineIndex,
99
+ style: TextStyle.Indent,
100
+ indentSize: baseIndent
101
+ });
102
+ processedLines.push(escapeLiteralText(normalizeCodeBlockLeadingWhitespace(line), escapeMap));
103
+ continue;
104
+ }
105
+ const { text: markdownLine, size: markdownPadding } = stripOptionalMarkdownPadding(line);
106
+ const headingMatch = markdownLine.match(/^(#{1,4})\s(.*)$/);
107
+ if (headingMatch) {
108
+ const depth = headingMatch[1].length;
109
+ lineStyles.push({
110
+ lineIndex: outputLineIndex,
111
+ style: TextStyle.Bold
112
+ });
113
+ if (depth === 1) lineStyles.push({
114
+ lineIndex: outputLineIndex,
115
+ style: TextStyle.Big
116
+ });
117
+ if (baseIndent > 0) lineStyles.push({
118
+ lineIndex: outputLineIndex,
119
+ style: TextStyle.Indent,
120
+ indentSize: baseIndent
121
+ });
122
+ processedLines.push(headingMatch[2]);
123
+ continue;
124
+ }
125
+ const indentMatch = markdownLine.match(/^(\s+)(.*)$/);
126
+ let indentLevel = 0;
127
+ let content = markdownLine;
128
+ if (indentMatch) {
129
+ indentLevel = clampIndent(indentMatch[1].length);
130
+ content = indentMatch[2];
131
+ }
132
+ const totalIndent = Math.min(5, baseIndent + indentLevel);
133
+ if (/^[-*+]\s\[[ xX]\]\s/.test(content)) {
134
+ if (totalIndent > 0) lineStyles.push({
135
+ lineIndex: outputLineIndex,
136
+ style: TextStyle.Indent,
137
+ indentSize: totalIndent
138
+ });
139
+ processedLines.push(content);
140
+ continue;
141
+ }
142
+ const orderedListMatch = content.match(/^(\d+)\.\s(.*)$/);
143
+ if (orderedListMatch) {
144
+ if (totalIndent > 0) lineStyles.push({
145
+ lineIndex: outputLineIndex,
146
+ style: TextStyle.Indent,
147
+ indentSize: totalIndent
148
+ });
149
+ lineStyles.push({
150
+ lineIndex: outputLineIndex,
151
+ style: TextStyle.OrderedList
152
+ });
153
+ processedLines.push(orderedListMatch[2]);
154
+ continue;
155
+ }
156
+ const unorderedListMatch = content.match(/^[-*+]\s(.*)$/);
157
+ if (unorderedListMatch) {
158
+ if (totalIndent > 0) lineStyles.push({
159
+ lineIndex: outputLineIndex,
160
+ style: TextStyle.Indent,
161
+ indentSize: totalIndent
162
+ });
163
+ lineStyles.push({
164
+ lineIndex: outputLineIndex,
165
+ style: TextStyle.UnorderedList
166
+ });
167
+ processedLines.push(unorderedListMatch[1]);
168
+ continue;
169
+ }
170
+ if (markdownPadding > 0) {
171
+ if (baseIndent > 0) lineStyles.push({
172
+ lineIndex: outputLineIndex,
173
+ style: TextStyle.Indent,
174
+ indentSize: baseIndent
175
+ });
176
+ processedLines.push(line);
177
+ continue;
178
+ }
179
+ if (totalIndent > 0) {
180
+ lineStyles.push({
181
+ lineIndex: outputLineIndex,
182
+ style: TextStyle.Indent,
183
+ indentSize: totalIndent
184
+ });
185
+ processedLines.push(content);
186
+ continue;
187
+ }
188
+ processedLines.push(line);
189
+ }
190
+ const segments = parseInlineSegments(processedLines.join("\n"));
191
+ let plainText = "";
192
+ for (const segment of segments) {
193
+ const start = plainText.length;
194
+ plainText += segment.text;
195
+ for (const style of segment.styles) allStyles.push({
196
+ start,
197
+ len: segment.text.length,
198
+ st: style
199
+ });
200
+ }
201
+ if (escapeMap.length > 0) {
202
+ const escapeRegex = new RegExp(`${ESCAPE_SENTINEL_START}(\\d+)${ESCAPE_SENTINEL_END}`, "g");
203
+ const shifts = [];
204
+ let cumulativeDelta = 0;
205
+ for (const match of plainText.matchAll(escapeRegex)) {
206
+ const escapeIndex = Number.parseInt(match[1], 10);
207
+ cumulativeDelta += match[0].length - escapeMap[escapeIndex].length;
208
+ shifts.push({
209
+ pos: (match.index ?? 0) + match[0].length,
210
+ delta: cumulativeDelta
211
+ });
212
+ }
213
+ for (const style of allStyles) {
214
+ let startDelta = 0;
215
+ let endDelta = 0;
216
+ const end = style.start + style.len;
217
+ for (const shift of shifts) {
218
+ if (shift.pos <= style.start) startDelta = shift.delta;
219
+ if (shift.pos <= end) endDelta = shift.delta;
220
+ }
221
+ style.start -= startDelta;
222
+ style.len -= endDelta - startDelta;
223
+ }
224
+ plainText = plainText.replace(escapeRegex, (_match, index) => escapeMap[Number.parseInt(index, 10)]);
225
+ }
226
+ const finalLines = plainText.split("\n");
227
+ let offset = 0;
228
+ for (let lineIndex = 0; lineIndex < finalLines.length; lineIndex += 1) {
229
+ const lineLength = finalLines[lineIndex].length;
230
+ if (lineLength > 0) for (const lineStyle of lineStyles) {
231
+ if (lineStyle.lineIndex !== lineIndex) continue;
232
+ if (lineStyle.style === TextStyle.Indent) allStyles.push({
233
+ start: offset,
234
+ len: lineLength,
235
+ st: TextStyle.Indent,
236
+ indentSize: lineStyle.indentSize
237
+ });
238
+ else allStyles.push({
239
+ start: offset,
240
+ len: lineLength,
241
+ st: lineStyle.style
242
+ });
243
+ }
244
+ offset += lineLength + 1;
245
+ }
246
+ return {
247
+ text: plainText,
248
+ styles: allStyles
249
+ };
250
+ }
251
+ function clampIndent(spaceCount) {
252
+ return Math.min(5, Math.max(1, Math.floor(spaceCount / 2)));
253
+ }
254
+ function stripOptionalMarkdownPadding(line) {
255
+ const match = line.match(/^( {1,3})(?=\S)/);
256
+ if (!match) return {
257
+ text: line,
258
+ size: 0
259
+ };
260
+ return {
261
+ text: line.slice(match[1].length),
262
+ size: match[1].length
263
+ };
264
+ }
265
+ function hasClosingFence(lines, startIndex, fence) {
266
+ for (let index = startIndex; index < lines.length; index += 1) if (isClosingFence(fence.quoteIndent > 0 ? stripQuotePrefix(lines[index], fence.quoteIndent).text : lines[index], fence)) return true;
267
+ return false;
268
+ }
269
+ function resolveOpeningFence(line) {
270
+ const directFence = parseFenceMarker(line);
271
+ if (directFence) return {
272
+ ...directFence,
273
+ quoteIndent: 0
274
+ };
275
+ const quoted = stripQuotePrefix(line);
276
+ if (quoted.indent === 0) return null;
277
+ const quotedFence = parseFenceMarker(quoted.text);
278
+ if (!quotedFence) return null;
279
+ return {
280
+ ...quotedFence,
281
+ quoteIndent: quoted.indent
282
+ };
283
+ }
284
+ function stripQuotePrefix(line, maxDepth = Number.POSITIVE_INFINITY) {
285
+ let cursor = 0;
286
+ while (cursor < line.length && cursor < 3 && line[cursor] === " ") cursor += 1;
287
+ let removedDepth = 0;
288
+ let consumedCursor = cursor;
289
+ while (removedDepth < maxDepth && consumedCursor < line.length && line[consumedCursor] === ">") {
290
+ removedDepth += 1;
291
+ consumedCursor += 1;
292
+ if (line[consumedCursor] === " ") consumedCursor += 1;
293
+ }
294
+ if (removedDepth === 0) return {
295
+ text: line,
296
+ indent: 0
297
+ };
298
+ return {
299
+ text: line.slice(consumedCursor),
300
+ indent: Math.min(5, removedDepth)
301
+ };
302
+ }
303
+ function parseFenceMarker(line) {
304
+ const match = line.match(/^([ ]{0,3})(`{3,}|~{3,})(.*)$/);
305
+ if (!match) return null;
306
+ const marker = match[2];
307
+ const char = marker[0];
308
+ if (char !== "`" && char !== "~") return null;
309
+ return {
310
+ char,
311
+ length: marker.length,
312
+ indent: match[1].length
313
+ };
314
+ }
315
+ function isClosingFence(line, fence) {
316
+ const match = line.match(/^([ ]{0,3})(`{3,}|~{3,})[ \t]*$/);
317
+ if (!match) return false;
318
+ return match[2][0] === fence.char && match[2].length >= fence.length;
319
+ }
320
+ function escapeLiteralText(input, escapeMap) {
321
+ return input.replace(/[\\*_~{}`]/g, (ch) => {
322
+ const index = escapeMap.length;
323
+ escapeMap.push(ch);
324
+ return `\x01${index}\x02`;
325
+ });
326
+ }
327
+ function parseInlineSegments(text, inheritedStyles = []) {
328
+ const segments = [];
329
+ let cursor = 0;
330
+ while (cursor < text.length) {
331
+ const nextMatch = findNextInlineMatch(text, cursor);
332
+ if (!nextMatch) {
333
+ pushSegment(segments, text.slice(cursor), inheritedStyles);
334
+ break;
335
+ }
336
+ if (nextMatch.match.index > cursor) pushSegment(segments, text.slice(cursor, nextMatch.match.index), inheritedStyles);
337
+ const combinedStyles = [...inheritedStyles, ...nextMatch.styles];
338
+ if (nextMatch.marker.literal) pushSegment(segments, nextMatch.text, combinedStyles);
339
+ else segments.push(...parseInlineSegments(nextMatch.text, combinedStyles));
340
+ cursor = nextMatch.match.index + nextMatch.match[0].length;
341
+ }
342
+ return segments;
343
+ }
344
+ function findNextInlineMatch(text, startIndex) {
345
+ let bestMatch = null;
346
+ for (const [priority, marker] of INLINE_MARKERS.entries()) {
347
+ const regex = new RegExp(marker.pattern.source, marker.pattern.flags);
348
+ regex.lastIndex = startIndex;
349
+ const match = regex.exec(text);
350
+ if (!match) continue;
351
+ if (bestMatch && (match.index > bestMatch.match.index || match.index === bestMatch.match.index && priority > bestMatch.priority)) continue;
352
+ bestMatch = {
353
+ match,
354
+ marker,
355
+ text: marker.extractText(match),
356
+ styles: marker.resolveStyles?.(match) ?? [],
357
+ priority
358
+ };
359
+ }
360
+ return bestMatch;
361
+ }
362
+ function pushSegment(segments, text, styles) {
363
+ if (!text) return;
364
+ const lastSegment = segments.at(-1);
365
+ if (lastSegment && sameStyles(lastSegment.styles, styles)) {
366
+ lastSegment.text += text;
367
+ return;
368
+ }
369
+ segments.push({
370
+ text,
371
+ styles: [...styles]
372
+ });
373
+ }
374
+ function sameStyles(left, right) {
375
+ return left.length === right.length && left.every((style, index) => style === right[index]);
376
+ }
377
+ function normalizeCodeBlockLeadingWhitespace(line) {
378
+ return line.replace(/^[ \t]+/, (leadingWhitespace) => leadingWhitespace.replace(/\t/g, "\xA0\xA0\xA0\xA0").replace(/ /g, "\xA0"));
379
+ }
380
+ function isIndentedCodeBlockLine(line) {
381
+ return /^(?: {4,}|\t)/.test(line);
382
+ }
383
+ function stripCodeFenceIndent(line, indent) {
384
+ let consumed = 0;
385
+ let cursor = 0;
386
+ while (cursor < line.length && consumed < indent && line[cursor] === " ") {
387
+ cursor += 1;
388
+ consumed += 1;
389
+ }
390
+ return line.slice(cursor);
391
+ }
392
+ //#endregion
393
+ //#region extensions/zalouser/src/send.ts
394
+ const ZALO_TEXT_LIMIT = 2e3;
395
+ const DEFAULT_TEXT_CHUNK_MODE = "length";
396
+ async function sendMessageZalouser(threadId, text, options = {}) {
397
+ const prepared = options.textMode === "markdown" ? parseZalouserTextStyles(text) : {
398
+ text,
399
+ styles: options.textStyles
400
+ };
401
+ const textChunkLimit = options.textChunkLimit ?? ZALO_TEXT_LIMIT;
402
+ const chunks = splitStyledText(prepared.text, (prepared.styles?.length ?? 0) > 0 ? prepared.styles : void 0, textChunkLimit, options.textChunkMode);
403
+ let lastResult = null;
404
+ for (const [index, chunk] of chunks.entries()) {
405
+ const chunkOptions = index === 0 ? {
406
+ ...options,
407
+ textStyles: chunk.styles
408
+ } : {
409
+ ...options,
410
+ caption: void 0,
411
+ mediaLocalRoots: void 0,
412
+ mediaUrl: void 0,
413
+ textStyles: chunk.styles
414
+ };
415
+ const result = await sendZaloTextMessage(threadId, chunk.text, chunkOptions);
416
+ if (!result.ok) return result;
417
+ lastResult = result;
418
+ }
419
+ return lastResult ?? {
420
+ ok: false,
421
+ error: "No message content provided",
422
+ receipt: createZalouserSendReceipt({
423
+ threadId,
424
+ kind: "text"
425
+ })
426
+ };
427
+ }
428
+ async function sendImageZalouser(threadId, imageUrl, options = {}) {
429
+ return await sendMessageZalouser(threadId, options.caption ?? "", {
430
+ ...options,
431
+ caption: void 0,
432
+ mediaUrl: imageUrl
433
+ });
434
+ }
435
+ async function sendLinkZalouser(threadId, url, options = {}) {
436
+ return await sendZaloLink(threadId, url, options);
437
+ }
438
+ async function sendTypingZalouser(threadId, options = {}) {
439
+ await sendZaloTypingEvent(threadId, options);
440
+ }
441
+ async function sendReactionZalouser(params) {
442
+ const result = await sendZaloReaction({
443
+ profile: params.profile,
444
+ threadId: params.threadId,
445
+ isGroup: params.isGroup,
446
+ msgId: params.msgId,
447
+ cliMsgId: params.cliMsgId,
448
+ emoji: params.emoji,
449
+ remove: params.remove
450
+ });
451
+ return {
452
+ ok: result.ok,
453
+ error: result.error,
454
+ receipt: createZalouserSendReceipt({
455
+ threadId: params.threadId,
456
+ kind: "unknown"
457
+ })
458
+ };
459
+ }
460
+ async function sendDeliveredZalouser(params) {
461
+ await sendZaloDeliveredEvent(params);
462
+ }
463
+ async function sendSeenZalouser(params) {
464
+ await sendZaloSeenEvent(params);
465
+ }
466
+ function splitStyledText(text, styles, limit, mode) {
467
+ if (text.length === 0) return [{
468
+ text,
469
+ styles: void 0
470
+ }];
471
+ const chunks = [];
472
+ for (const range of splitTextRanges(text, limit, mode ?? DEFAULT_TEXT_CHUNK_MODE)) {
473
+ const { start, end } = range;
474
+ chunks.push({
475
+ text: text.slice(start, end),
476
+ styles: sliceTextStyles(styles, start, end)
477
+ });
478
+ }
479
+ return chunks;
480
+ }
481
+ function sliceTextStyles(styles, start, end) {
482
+ if (!styles || styles.length === 0) return;
483
+ const chunkStyles = styles.map((style) => {
484
+ const overlapStart = Math.max(style.start, start);
485
+ const overlapEnd = Math.min(style.start + style.len, end);
486
+ if (overlapEnd <= overlapStart) return null;
487
+ if (style.st === TextStyle.Indent) return {
488
+ start: overlapStart - start,
489
+ len: overlapEnd - overlapStart,
490
+ st: style.st,
491
+ indentSize: style.indentSize
492
+ };
493
+ return {
494
+ start: overlapStart - start,
495
+ len: overlapEnd - overlapStart,
496
+ st: style.st
497
+ };
498
+ }).filter((style) => style !== null);
499
+ return chunkStyles.length > 0 ? chunkStyles : void 0;
500
+ }
501
+ function splitTextRanges(text, limit, mode) {
502
+ if (mode === "newline") return splitTextRangesByPreferredBreaks(text, limit);
503
+ const ranges = [];
504
+ for (let start = 0; start < text.length; start += limit) ranges.push({
505
+ start,
506
+ end: Math.min(text.length, start + limit)
507
+ });
508
+ return ranges;
509
+ }
510
+ function splitTextRangesByPreferredBreaks(text, limit) {
511
+ const ranges = [];
512
+ let start = 0;
513
+ while (start < text.length) {
514
+ const maxEnd = Math.min(text.length, start + limit);
515
+ let end = maxEnd;
516
+ if (maxEnd < text.length) end = findParagraphBreak(text, start, maxEnd) ?? findLastBreak(text, "\n", start, maxEnd) ?? findLastWhitespaceBreak(text, start, maxEnd) ?? maxEnd;
517
+ if (end <= start) end = maxEnd;
518
+ ranges.push({
519
+ start,
520
+ end
521
+ });
522
+ start = end;
523
+ }
524
+ return ranges;
525
+ }
526
+ function findParagraphBreak(text, start, end) {
527
+ const matches = text.slice(start, end).matchAll(/\n[\t ]*\n+/g);
528
+ let lastMatch;
529
+ for (const match of matches) lastMatch = match;
530
+ if (!lastMatch || lastMatch.index === void 0) return;
531
+ return start + lastMatch.index + lastMatch[0].length;
532
+ }
533
+ function findLastBreak(text, marker, start, end) {
534
+ const index = text.lastIndexOf(marker, end - 1);
535
+ if (index < start) return;
536
+ return index + marker.length;
537
+ }
538
+ function findLastWhitespaceBreak(text, start, end) {
539
+ for (let index = end - 1; index > start; index -= 1) if (/\s/.test(text[index])) return index + 1;
540
+ }
541
+ //#endregion
542
+ export { sendReactionZalouser as a, sendMessageZalouser as i, sendImageZalouser as n, sendSeenZalouser as o, sendLinkZalouser as r, sendTypingZalouser as s, sendDeliveredZalouser as t };
@@ -0,0 +1,92 @@
1
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString } from "klaw/plugin-sdk/string-coerce-runtime";
2
+ import { buildChannelOutboundSessionRoute } from "klaw/plugin-sdk/core";
3
+ //#region extensions/zalouser/src/session-route.ts
4
+ function stripZalouserTargetPrefix(raw) {
5
+ return raw.trim().replace(/^(zalouser|zlu):/i, "").trim();
6
+ }
7
+ function normalizeZalouserTarget(raw) {
8
+ const trimmed = stripZalouserTargetPrefix(raw);
9
+ if (!trimmed) return;
10
+ const lower = normalizeLowercaseStringOrEmpty(trimmed);
11
+ if (lower.startsWith("group:")) {
12
+ const id = trimmed.slice(6).trim();
13
+ return id ? `group:${id}` : void 0;
14
+ }
15
+ if (lower.startsWith("g:")) {
16
+ const id = trimmed.slice(2).trim();
17
+ return id ? `group:${id}` : void 0;
18
+ }
19
+ if (lower.startsWith("user:")) {
20
+ const id = trimmed.slice(5).trim();
21
+ return id ? `user:${id}` : void 0;
22
+ }
23
+ if (lower.startsWith("dm:")) {
24
+ const id = trimmed.slice(3).trim();
25
+ return id ? `user:${id}` : void 0;
26
+ }
27
+ if (lower.startsWith("u:")) {
28
+ const id = trimmed.slice(2).trim();
29
+ return id ? `user:${id}` : void 0;
30
+ }
31
+ if (/^g-\S+$/i.test(trimmed)) return `group:${trimmed}`;
32
+ if (/^u-\S+$/i.test(trimmed)) return `user:${trimmed}`;
33
+ return trimmed;
34
+ }
35
+ function parseZalouserOutboundTarget(raw) {
36
+ const normalized = normalizeZalouserTarget(raw);
37
+ if (!normalized) throw new Error("Zalouser target is required");
38
+ const lowered = normalizeLowercaseStringOrEmpty(normalized);
39
+ if (lowered.startsWith("group:")) {
40
+ const threadId = normalized.slice(6).trim();
41
+ if (!threadId) throw new Error("Zalouser group target is missing group id");
42
+ return {
43
+ threadId,
44
+ isGroup: true
45
+ };
46
+ }
47
+ if (lowered.startsWith("user:")) {
48
+ const threadId = normalized.slice(5).trim();
49
+ if (!threadId) throw new Error("Zalouser user target is missing user id");
50
+ return {
51
+ threadId,
52
+ isGroup: false
53
+ };
54
+ }
55
+ return {
56
+ threadId: normalized,
57
+ isGroup: false
58
+ };
59
+ }
60
+ function parseZalouserDirectoryGroupId(raw) {
61
+ const normalized = normalizeZalouserTarget(raw);
62
+ if (!normalized) throw new Error("Zalouser group target is required");
63
+ const lowered = normalizeLowercaseStringOrEmpty(normalized);
64
+ if (lowered.startsWith("group:")) {
65
+ const groupId = normalized.slice(6).trim();
66
+ if (!groupId) throw new Error("Zalouser group target is missing group id");
67
+ return groupId;
68
+ }
69
+ if (lowered.startsWith("user:")) throw new Error("Zalouser group members lookup requires a group target (group:<id>)");
70
+ return normalized;
71
+ }
72
+ function resolveZalouserOutboundSessionRoute(params) {
73
+ const normalized = normalizeZalouserTarget(params.target);
74
+ if (!normalized) return null;
75
+ const isGroup = (normalizeOptionalLowercaseString(normalized) ?? "").startsWith("group:");
76
+ const peerId = normalized.replace(/^(group|user):/i, "").trim();
77
+ return buildChannelOutboundSessionRoute({
78
+ cfg: params.cfg,
79
+ agentId: params.agentId,
80
+ channel: "zalouser",
81
+ accountId: params.accountId,
82
+ peer: {
83
+ kind: isGroup ? "group" : "direct",
84
+ id: peerId
85
+ },
86
+ chatType: isGroup ? "group" : "direct",
87
+ from: isGroup ? `zalouser:group:${peerId}` : `zalouser:${peerId}`,
88
+ to: `zalouser:${peerId}`
89
+ });
90
+ }
91
+ //#endregion
92
+ export { resolveZalouserOutboundSessionRoute as i, parseZalouserDirectoryGroupId as n, parseZalouserOutboundTarget as r, normalizeZalouserTarget as t };