@kodelyth/zalouser 2026.5.39 → 2026.5.42

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.
Files changed (106) hide show
  1. package/README.md +120 -0
  2. package/api.ts +9 -0
  3. package/channel-plugin-api.ts +3 -0
  4. package/contract-api.ts +2 -0
  5. package/dist/accounts-DOefD_if.js +66 -0
  6. package/dist/accounts.runtime-KT101uuu.js +2 -0
  7. package/dist/api-DSWT4Dh_.js +133 -0
  8. package/dist/api.js +7 -0
  9. package/dist/channel-pby_3Sur.js +602 -0
  10. package/dist/channel-plugin-api.js +2 -0
  11. package/dist/channel.runtime-0aJ2O7Y8.js +25 -0
  12. package/dist/channel.setup-CqyWwqcQ.js +9 -0
  13. package/dist/contract-api.js +3 -0
  14. package/dist/doctor-contract-B9EvrW0j.js +128 -0
  15. package/dist/doctor-contract-api.js +2 -0
  16. package/dist/index.js +27 -0
  17. package/dist/monitor-CVtrUqyW.js +708 -0
  18. package/dist/runtime-api.js +19 -0
  19. package/dist/secret-contract-api.js +5 -0
  20. package/dist/security-audit-D_rftvs-.js +34 -0
  21. package/dist/send-uRjUB8mG.js +542 -0
  22. package/dist/session-route-CalHiv1d.js +92 -0
  23. package/dist/setup-entry.js +11 -0
  24. package/dist/setup-plugin-api.js +2 -0
  25. package/dist/setup-surface-Cfj4GQlB.js +360 -0
  26. package/dist/shared-DjK0e2FC.js +160 -0
  27. package/dist/test-api.js +5 -0
  28. package/dist/zalo-js-B80cRyDF.js +1285 -0
  29. package/doctor-contract-api.ts +1 -0
  30. package/index.ts +34 -0
  31. package/klaw.plugin.json +3 -286
  32. package/package.json +4 -4
  33. package/runtime-api.ts +62 -0
  34. package/secret-contract-api.ts +4 -0
  35. package/setup-entry.ts +9 -0
  36. package/setup-plugin-api.ts +2 -0
  37. package/src/accounts.runtime.ts +1 -0
  38. package/src/accounts.test-mocks.ts +14 -0
  39. package/src/accounts.test.ts +298 -0
  40. package/src/accounts.ts +136 -0
  41. package/src/channel-api.ts +16 -0
  42. package/src/channel.adapters.ts +432 -0
  43. package/src/channel.directory.test.ts +59 -0
  44. package/src/channel.runtime.ts +12 -0
  45. package/src/channel.sendpayload.test.ts +311 -0
  46. package/src/channel.setup.test.ts +30 -0
  47. package/src/channel.setup.ts +12 -0
  48. package/src/channel.test.ts +424 -0
  49. package/src/channel.ts +221 -0
  50. package/src/config-schema.ts +33 -0
  51. package/src/directory.ts +54 -0
  52. package/src/doctor-contract.ts +156 -0
  53. package/src/doctor.test.ts +87 -0
  54. package/src/doctor.ts +37 -0
  55. package/src/group-policy.test.ts +61 -0
  56. package/src/group-policy.ts +83 -0
  57. package/src/message-sid.test.ts +66 -0
  58. package/src/message-sid.ts +80 -0
  59. package/src/monitor.account-scope.test.ts +122 -0
  60. package/src/monitor.group-gating.test.ts +967 -0
  61. package/src/monitor.send-mocks.ts +20 -0
  62. package/src/monitor.ts +1057 -0
  63. package/src/probe.test.ts +60 -0
  64. package/src/probe.ts +35 -0
  65. package/src/qr-temp-file.ts +19 -0
  66. package/src/reaction.test.ts +19 -0
  67. package/src/reaction.ts +32 -0
  68. package/src/runtime.ts +9 -0
  69. package/src/security-audit.test.ts +83 -0
  70. package/src/security-audit.ts +71 -0
  71. package/src/send-receipt.ts +31 -0
  72. package/src/send.test.ts +424 -0
  73. package/src/send.ts +280 -0
  74. package/src/session-route.ts +121 -0
  75. package/src/setup-core.ts +36 -0
  76. package/src/setup-surface.test.ts +367 -0
  77. package/src/setup-surface.ts +481 -0
  78. package/src/setup-test-helpers.ts +42 -0
  79. package/src/shared.ts +92 -0
  80. package/src/status-issues.test.ts +31 -0
  81. package/src/status-issues.ts +55 -0
  82. package/src/test-helpers.ts +26 -0
  83. package/src/text-styles.test.ts +203 -0
  84. package/src/text-styles.ts +540 -0
  85. package/src/tool.test.ts +212 -0
  86. package/src/tool.ts +200 -0
  87. package/src/types.ts +127 -0
  88. package/src/zalo-js.credentials.test.ts +465 -0
  89. package/src/zalo-js.test-mocks.ts +89 -0
  90. package/src/zalo-js.ts +1889 -0
  91. package/src/zca-client.test.ts +27 -0
  92. package/src/zca-client.ts +259 -0
  93. package/src/zca-constants.ts +55 -0
  94. package/src/zca-js-exports.d.ts +22 -0
  95. package/test-api.ts +21 -0
  96. package/tsconfig.json +16 -0
  97. package/api.js +0 -7
  98. package/channel-plugin-api.js +0 -7
  99. package/contract-api.js +0 -7
  100. package/doctor-contract-api.js +0 -7
  101. package/index.js +0 -7
  102. package/runtime-api.js +0 -7
  103. package/secret-contract-api.js +0 -7
  104. package/setup-entry.js +0 -7
  105. package/setup-plugin-api.js +0 -7
  106. package/test-api.js +0 -7
@@ -0,0 +1,424 @@
1
+ import { createNonExitingRuntimeEnv } from "klaw/plugin-sdk/plugin-test-runtime";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import "./zalo-js.test-mocks.js";
4
+ import {
5
+ zalouserAuthAdapter,
6
+ zalouserGroupsAdapter,
7
+ zalouserMessageActions,
8
+ zalouserOutboundAdapter,
9
+ zalouserPairingTextAdapter,
10
+ zalouserResolverAdapter,
11
+ zalouserSecurityAdapter,
12
+ } from "./channel.adapters.js";
13
+ import { setZalouserRuntime } from "./runtime.js";
14
+ import { sendMessageZalouser, sendReactionZalouser } from "./send.js";
15
+ import {
16
+ listZaloFriendsMatchingMock,
17
+ startZaloQrLoginMock,
18
+ waitForZaloQrLoginMock,
19
+ } from "./zalo-js.test-mocks.js";
20
+
21
+ vi.mock("./qr-temp-file.js", () => ({
22
+ writeQrDataUrlToTempFile: vi.fn(async () => null),
23
+ }));
24
+
25
+ vi.mock("./send.js", async () => {
26
+ const actual = (await vi.importActual("./send.js")) as Record<string, unknown>;
27
+ return {
28
+ ...actual,
29
+ sendMessageZalouser: vi.fn(async () => ({ ok: true, messageId: "mid-1" })),
30
+ sendReactionZalouser: vi.fn(async () => ({ ok: true })),
31
+ };
32
+ });
33
+
34
+ const mockSendMessage = vi.mocked(sendMessageZalouser);
35
+ const mockSendReaction = vi.mocked(sendReactionZalouser);
36
+
37
+ function requireZalouserSendText() {
38
+ const sendText = zalouserOutboundAdapter.sendText;
39
+ if (!sendText) {
40
+ throw new Error("zalouser outbound.sendText unavailable");
41
+ }
42
+ return sendText;
43
+ }
44
+
45
+ function getResolveToolPolicy() {
46
+ const resolveToolPolicy = zalouserGroupsAdapter.resolveToolPolicy;
47
+ if (!resolveToolPolicy) {
48
+ throw new Error("resolveToolPolicy unavailable");
49
+ }
50
+ return resolveToolPolicy;
51
+ }
52
+
53
+ function requireZalouserResolveRequireMention() {
54
+ const resolveRequireMention = zalouserGroupsAdapter.resolveRequireMention;
55
+ if (!resolveRequireMention) {
56
+ throw new Error("resolveRequireMention unavailable");
57
+ }
58
+ return resolveRequireMention;
59
+ }
60
+
61
+ function requireZalouserPairingNormalizer() {
62
+ const normalizeAllowEntry = zalouserPairingTextAdapter.normalizeAllowEntry;
63
+ if (!normalizeAllowEntry) {
64
+ throw new Error("pairing.normalizeAllowEntry unavailable");
65
+ }
66
+ return normalizeAllowEntry;
67
+ }
68
+
69
+ function resolveGroupToolPolicy(
70
+ groups: Record<string, { tools: { allow?: string[]; deny?: string[] } }>,
71
+ groupId: string,
72
+ ) {
73
+ return getResolveToolPolicy()({
74
+ cfg: {
75
+ channels: {
76
+ zalouser: {
77
+ groups,
78
+ },
79
+ },
80
+ },
81
+ accountId: "default",
82
+ groupId,
83
+ groupChannel: groupId,
84
+ });
85
+ }
86
+
87
+ describe("zalouser outbound", () => {
88
+ beforeEach(() => {
89
+ mockSendMessage.mockClear();
90
+ setZalouserRuntime({
91
+ channel: {
92
+ text: {
93
+ resolveChunkMode: vi.fn(() => "newline"),
94
+ resolveTextChunkLimit: vi.fn(() => 10),
95
+ },
96
+ },
97
+ } as never);
98
+ });
99
+
100
+ it("passes markdown chunk settings through sendText", async () => {
101
+ const sendText = requireZalouserSendText();
102
+
103
+ const result = await sendText({
104
+ cfg: { channels: { zalouser: { enabled: true } } } as never,
105
+ to: "group:123456",
106
+ text: "hello world\nthis is a test",
107
+ accountId: "default",
108
+ } as never);
109
+
110
+ expect(mockSendMessage).toHaveBeenCalledWith("123456", "hello world\nthis is a test", {
111
+ profile: "default",
112
+ isGroup: true,
113
+ textMode: "markdown",
114
+ textChunkMode: "newline",
115
+ textChunkLimit: 10,
116
+ });
117
+ expect(result).toEqual({
118
+ channel: "zalouser",
119
+ messageId: "mid-1",
120
+ ok: true,
121
+ });
122
+ });
123
+
124
+ it("uses the selected account profile for direct outbound messages", async () => {
125
+ const sendText = requireZalouserSendText();
126
+
127
+ const result = await sendText({
128
+ cfg: {
129
+ channels: {
130
+ zalouser: {
131
+ accounts: {
132
+ work: {
133
+ profile: "work-profile",
134
+ },
135
+ },
136
+ },
137
+ },
138
+ } as never,
139
+ to: "user:987654",
140
+ text: "hello user",
141
+ accountId: "work",
142
+ } as never);
143
+
144
+ expect(mockSendMessage).toHaveBeenCalledWith("987654", "hello user", {
145
+ profile: "work-profile",
146
+ isGroup: false,
147
+ textMode: "markdown",
148
+ textChunkMode: "newline",
149
+ textChunkLimit: 10,
150
+ });
151
+ expect(result).toEqual({
152
+ channel: "zalouser",
153
+ messageId: "mid-1",
154
+ ok: true,
155
+ });
156
+ });
157
+
158
+ it("keeps the default account profile for unscoped outbound messages", async () => {
159
+ const sendText = requireZalouserSendText();
160
+
161
+ await sendText({
162
+ cfg: { channels: { zalouser: { enabled: true } } } as never,
163
+ to: "user:111222",
164
+ text: "hello default",
165
+ } as never);
166
+
167
+ expect(mockSendMessage).toHaveBeenCalledWith("111222", "hello default", {
168
+ profile: "default",
169
+ isGroup: false,
170
+ textMode: "markdown",
171
+ textChunkMode: "newline",
172
+ textChunkLimit: 10,
173
+ });
174
+ });
175
+ });
176
+
177
+ describe("zalouser outbound chunking", () => {
178
+ it("chunks outbound text without requiring Zalouser runtime initialization", () => {
179
+ const chunker = zalouserOutboundAdapter.chunker;
180
+ if (!chunker) {
181
+ throw new Error("zalouser outbound.chunker unavailable");
182
+ }
183
+
184
+ expect(chunker("alpha beta", 5)).toEqual(["alpha", "beta"]);
185
+ });
186
+ });
187
+
188
+ describe("zalouser channel policies", () => {
189
+ beforeEach(() => {
190
+ mockSendReaction.mockClear();
191
+ mockSendReaction.mockResolvedValue({ ok: true } as never);
192
+ });
193
+
194
+ it("normalizes dm allowlist entries after trimming channel prefixes", () => {
195
+ const resolveDmPolicy = zalouserSecurityAdapter.resolveDmPolicy;
196
+ if (!resolveDmPolicy) {
197
+ throw new Error("resolveDmPolicy unavailable");
198
+ }
199
+
200
+ const cfg = {
201
+ channels: {
202
+ zalouser: {
203
+ dmPolicy: "allowlist",
204
+ allowFrom: [" zlu:123456 "],
205
+ },
206
+ },
207
+ } as never;
208
+ const account = {
209
+ accountId: "default",
210
+ enabled: true,
211
+ authenticated: false,
212
+ profile: "default",
213
+ config: {
214
+ dmPolicy: "allowlist",
215
+ allowFrom: [" zlu:123456 "],
216
+ },
217
+ } as never;
218
+
219
+ const result = resolveDmPolicy({ cfg, account });
220
+ if (!result) {
221
+ throw new Error("zalouser resolveDmPolicy returned null");
222
+ }
223
+
224
+ expect(result.policy).toBe("allowlist");
225
+ expect(result.allowFrom).toEqual([" zlu:123456 "]);
226
+ expect(result.normalizeEntry?.(" zlu:123456 ")).toBe("123456");
227
+ });
228
+
229
+ it("normalizes pairing allowlist entries after trimming channel prefixes", () => {
230
+ const normalizeAllowEntry = requireZalouserPairingNormalizer();
231
+
232
+ expect(normalizeAllowEntry(" zlu:123456 ")).toBe("123456");
233
+ expect(normalizeAllowEntry(" zalouser:654321 ")).toBe("654321");
234
+ });
235
+
236
+ it("resolves requireMention from group config", () => {
237
+ const resolveRequireMention = requireZalouserResolveRequireMention();
238
+ const requireMention = resolveRequireMention({
239
+ cfg: {
240
+ channels: {
241
+ zalouser: {
242
+ groups: {
243
+ "123": { requireMention: false },
244
+ },
245
+ },
246
+ },
247
+ },
248
+ accountId: "default",
249
+ groupId: "123",
250
+ groupChannel: "123",
251
+ });
252
+ expect(requireMention).toBe(false);
253
+ });
254
+
255
+ it("resolves group tool policy by explicit group id", () => {
256
+ const policy = resolveGroupToolPolicy({ "123": { tools: { allow: ["search"] } } }, "123");
257
+ expect(policy).toEqual({ allow: ["search"] });
258
+ });
259
+
260
+ it("falls back to wildcard group policy", () => {
261
+ const policy = resolveGroupToolPolicy({ "*": { tools: { deny: ["system.run"] } } }, "missing");
262
+ expect(policy).toEqual({ deny: ["system.run"] });
263
+ });
264
+
265
+ it("handles react action", async () => {
266
+ const actions = zalouserMessageActions;
267
+ expect(
268
+ actions?.describeMessageTool?.({ cfg: { channels: { zalouser: { enabled: true } } } })
269
+ ?.actions,
270
+ ).toEqual(["react"]);
271
+ const result = await actions?.handleAction?.({
272
+ channel: "zalouser",
273
+ action: "react",
274
+ params: {
275
+ threadId: "123456",
276
+ messageId: "111",
277
+ cliMsgId: "222",
278
+ emoji: "👍",
279
+ },
280
+ cfg: {
281
+ channels: {
282
+ zalouser: {
283
+ enabled: true,
284
+ profile: "default",
285
+ },
286
+ },
287
+ },
288
+ });
289
+ expect(mockSendReaction).toHaveBeenCalledWith({
290
+ profile: "default",
291
+ threadId: "123456",
292
+ isGroup: false,
293
+ msgId: "111",
294
+ cliMsgId: "222",
295
+ emoji: "👍",
296
+ remove: false,
297
+ });
298
+ expect(result).toEqual({
299
+ content: [{ type: "text", text: "Reacted 👍 on 111" }],
300
+ details: {
301
+ messageId: "111",
302
+ cliMsgId: "222",
303
+ threadId: "123456",
304
+ },
305
+ });
306
+ });
307
+
308
+ it("honors the selected Zalouser account during discovery", () => {
309
+ const actions = zalouserMessageActions;
310
+ const cfg = {
311
+ channels: {
312
+ zalouser: {
313
+ enabled: true,
314
+ profile: "default",
315
+ accounts: {
316
+ default: {
317
+ enabled: false,
318
+ profile: "default",
319
+ },
320
+ work: {
321
+ enabled: true,
322
+ profile: "work",
323
+ },
324
+ },
325
+ },
326
+ },
327
+ };
328
+
329
+ expect(actions?.describeMessageTool?.({ cfg, accountId: "default" })).toBeNull();
330
+ expect(actions?.describeMessageTool?.({ cfg, accountId: "work" })?.actions).toEqual(["react"]);
331
+ });
332
+ });
333
+
334
+ describe("zalouser account resolution", () => {
335
+ beforeEach(() => {
336
+ listZaloFriendsMatchingMock.mockReset();
337
+ startZaloQrLoginMock.mockReset();
338
+ waitForZaloQrLoginMock.mockReset();
339
+ });
340
+
341
+ it("uses the configured default account for omitted target lookup", async () => {
342
+ const resolveTargets = zalouserResolverAdapter.resolveTargets;
343
+ if (!resolveTargets) {
344
+ throw new Error("zalouser resolver.resolveTargets unavailable");
345
+ }
346
+
347
+ listZaloFriendsMatchingMock.mockResolvedValue([
348
+ { userId: "42", displayName: "Work User" } as never,
349
+ ]);
350
+
351
+ const result = await resolveTargets({
352
+ cfg: {
353
+ channels: {
354
+ zalouser: {
355
+ defaultAccount: "work",
356
+ accounts: {
357
+ work: {
358
+ profile: "work-profile",
359
+ },
360
+ },
361
+ },
362
+ },
363
+ } as never,
364
+ inputs: ["Work User"],
365
+ kind: "user",
366
+ runtime: createNonExitingRuntimeEnv(),
367
+ });
368
+
369
+ expect(listZaloFriendsMatchingMock).toHaveBeenCalledWith("work-profile", "Work User");
370
+ expect(result).toEqual([
371
+ {
372
+ input: "Work User",
373
+ resolved: true,
374
+ id: "42",
375
+ name: "Work User",
376
+ note: undefined,
377
+ },
378
+ ]);
379
+ });
380
+
381
+ it("uses the configured default account for omitted qr login", async () => {
382
+ const login = zalouserAuthAdapter.login;
383
+ if (!login) {
384
+ throw new Error("zalouser auth.login unavailable");
385
+ }
386
+
387
+ startZaloQrLoginMock.mockResolvedValue({
388
+ message: "qr ready",
389
+ qrDataUrl: "data:image/png;base64,abc",
390
+ } as never);
391
+ waitForZaloQrLoginMock.mockResolvedValue({
392
+ connected: true,
393
+ userId: "u-1",
394
+ displayName: "Work User",
395
+ } as never);
396
+
397
+ const runtime = createNonExitingRuntimeEnv();
398
+
399
+ await login({
400
+ cfg: {
401
+ channels: {
402
+ zalouser: {
403
+ defaultAccount: "work",
404
+ accounts: {
405
+ work: {
406
+ profile: "work-profile",
407
+ },
408
+ },
409
+ },
410
+ },
411
+ } as never,
412
+ runtime,
413
+ });
414
+
415
+ expect(startZaloQrLoginMock).toHaveBeenCalledWith({
416
+ profile: "work-profile",
417
+ timeoutMs: 35_000,
418
+ });
419
+ expect(waitForZaloQrLoginMock).toHaveBeenCalledWith({
420
+ profile: "work-profile",
421
+ timeoutMs: 180_000,
422
+ });
423
+ });
424
+ });
package/src/channel.ts ADDED
@@ -0,0 +1,221 @@
1
+ import { createChatChannelPlugin } from "klaw/plugin-sdk/channel-core";
2
+ import { createAccountStatusSink } from "klaw/plugin-sdk/channel-lifecycle";
3
+ import { buildPassiveProbedChannelStatusSummary } from "klaw/plugin-sdk/extension-shared";
4
+ import { createLazyRuntimeModule } from "klaw/plugin-sdk/lazy-runtime";
5
+ import {
6
+ createAsyncComputedAccountStatusAdapter,
7
+ createDefaultChannelRuntimeState,
8
+ } from "klaw/plugin-sdk/status-helpers";
9
+ import {
10
+ checkZcaAuthenticated,
11
+ resolveZalouserAccountSync,
12
+ type ResolvedZalouserAccount,
13
+ } from "./accounts.js";
14
+ import type { ChannelDirectoryEntry, ChannelPlugin } from "./channel-api.js";
15
+ import { DEFAULT_ACCOUNT_ID } from "./channel-api.js";
16
+ import {
17
+ zalouserAuthAdapter,
18
+ zalouserGroupsAdapter,
19
+ zalouserMessageAdapter,
20
+ zalouserMessageActions,
21
+ zalouserMessagingAdapter,
22
+ zalouserOutboundAdapter,
23
+ zalouserPairingTextAdapter,
24
+ resolveZalouserQrProfile,
25
+ zalouserResolverAdapter,
26
+ zalouserSecurityAdapter,
27
+ zalouserThreadingAdapter,
28
+ } from "./channel.adapters.js";
29
+ import { listZalouserDirectoryGroupMembers } from "./directory.js";
30
+ import type { ZalouserProbeResult } from "./probe.js";
31
+ import { createZalouserSetupWizardProxy, zalouserSetupAdapter } from "./setup-core.js";
32
+ import { createZalouserPluginBase } from "./shared.js";
33
+ import { collectZalouserStatusIssues } from "./status-issues.js";
34
+
35
+ const loadZalouserChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
36
+ const zalouserSetupWizardProxy = createZalouserSetupWizardProxy(
37
+ async () => (await import("./setup-surface.js")).zalouserSetupWizard,
38
+ );
39
+
40
+ function mapUser(params: {
41
+ id: string;
42
+ name?: string | null;
43
+ avatarUrl?: string | null;
44
+ raw?: unknown;
45
+ }): ChannelDirectoryEntry {
46
+ return {
47
+ kind: "user",
48
+ id: params.id,
49
+ name: params.name ?? undefined,
50
+ avatarUrl: params.avatarUrl ?? undefined,
51
+ raw: params.raw,
52
+ };
53
+ }
54
+
55
+ function mapGroup(params: {
56
+ id: string;
57
+ name?: string | null;
58
+ raw?: unknown;
59
+ }): ChannelDirectoryEntry {
60
+ return {
61
+ kind: "group",
62
+ id: params.id,
63
+ name: params.name ?? undefined,
64
+ raw: params.raw,
65
+ };
66
+ }
67
+
68
+ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount, ZalouserProbeResult> =
69
+ createChatChannelPlugin({
70
+ base: {
71
+ ...createZalouserPluginBase({
72
+ setupWizard: zalouserSetupWizardProxy,
73
+ setup: zalouserSetupAdapter,
74
+ }),
75
+ groups: zalouserGroupsAdapter,
76
+ actions: zalouserMessageActions,
77
+ messaging: zalouserMessagingAdapter,
78
+ directory: {
79
+ self: async ({ cfg, accountId }) => {
80
+ const { getZaloUserInfo } = await loadZalouserChannelRuntime();
81
+ const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
82
+ const parsed = await getZaloUserInfo(account.profile);
83
+ if (!parsed?.userId) {
84
+ return null;
85
+ }
86
+ return mapUser({
87
+ id: parsed.userId,
88
+ name: parsed.displayName ?? null,
89
+ avatarUrl: parsed.avatar ?? null,
90
+ raw: parsed,
91
+ });
92
+ },
93
+ listPeers: async ({ cfg, accountId, query, limit }) => {
94
+ const { listZaloFriendsMatching } = await loadZalouserChannelRuntime();
95
+ const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
96
+ const friends = await listZaloFriendsMatching(account.profile, query);
97
+ const rows = friends.map((friend) =>
98
+ mapUser({
99
+ id: friend.userId,
100
+ name: friend.displayName ?? null,
101
+ avatarUrl: friend.avatar ?? null,
102
+ raw: friend,
103
+ }),
104
+ );
105
+ return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
106
+ },
107
+ listGroups: async ({ cfg, accountId, query, limit }) => {
108
+ const { listZaloGroupsMatching } = await loadZalouserChannelRuntime();
109
+ const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
110
+ const groups = await listZaloGroupsMatching(account.profile, query);
111
+ const rows = groups.map((group) =>
112
+ mapGroup({
113
+ id: `group:${group.groupId}`,
114
+ name: group.name ?? null,
115
+ raw: group,
116
+ }),
117
+ );
118
+ return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
119
+ },
120
+ listGroupMembers: async ({ cfg, accountId, groupId, limit }) => {
121
+ const { listZaloGroupMembers } = await loadZalouserChannelRuntime();
122
+ return await listZalouserDirectoryGroupMembers(
123
+ {
124
+ cfg,
125
+ accountId: accountId ?? undefined,
126
+ groupId,
127
+ limit: limit ?? undefined,
128
+ },
129
+ { listZaloGroupMembers },
130
+ );
131
+ },
132
+ },
133
+ resolver: zalouserResolverAdapter,
134
+ auth: zalouserAuthAdapter,
135
+ message: zalouserMessageAdapter,
136
+ status: createAsyncComputedAccountStatusAdapter<ResolvedZalouserAccount, ZalouserProbeResult>(
137
+ {
138
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
139
+ collectStatusIssues: collectZalouserStatusIssues,
140
+ buildChannelSummary: ({ snapshot }) => buildPassiveProbedChannelStatusSummary(snapshot),
141
+ probeAccount: async ({ account, timeoutMs }) =>
142
+ (await loadZalouserChannelRuntime()).probeZalouser(account.profile, timeoutMs),
143
+ resolveAccountSnapshot: async ({ account, runtime }) => {
144
+ const configured = await checkZcaAuthenticated(account.profile);
145
+ const configError = "not authenticated";
146
+ return {
147
+ accountId: account.accountId,
148
+ name: account.name,
149
+ enabled: account.enabled,
150
+ configured,
151
+ extra: {
152
+ dmPolicy: account.config.dmPolicy ?? "pairing",
153
+ lastError: configured
154
+ ? (runtime?.lastError ?? null)
155
+ : (runtime?.lastError ?? configError),
156
+ },
157
+ };
158
+ },
159
+ },
160
+ ),
161
+ gateway: {
162
+ startAccount: async (ctx) => {
163
+ const { getZaloUserInfo } = await loadZalouserChannelRuntime();
164
+ const account = ctx.account;
165
+ let userLabel = "";
166
+ try {
167
+ const userInfo = await getZaloUserInfo(account.profile);
168
+ if (userInfo?.displayName) {
169
+ userLabel = ` (${userInfo.displayName})`;
170
+ }
171
+ ctx.setStatus({
172
+ accountId: account.accountId,
173
+ profile: userInfo,
174
+ });
175
+ } catch {
176
+ // ignore probe errors
177
+ }
178
+ const statusSink = createAccountStatusSink({
179
+ accountId: ctx.accountId,
180
+ setStatus: ctx.setStatus,
181
+ });
182
+ ctx.log?.info(`[${account.accountId}] starting zalouser provider${userLabel}`);
183
+ const { monitorZalouserProvider } = await import("./monitor.js");
184
+ return monitorZalouserProvider({
185
+ account,
186
+ config: ctx.cfg,
187
+ runtime: ctx.runtime,
188
+ abortSignal: ctx.abortSignal,
189
+ statusSink,
190
+ });
191
+ },
192
+ loginWithQrStart: async (params) => {
193
+ const { startZaloQrLogin } = await loadZalouserChannelRuntime();
194
+ const profile = resolveZalouserQrProfile(params.accountId);
195
+ return await startZaloQrLogin({
196
+ profile,
197
+ force: params.force,
198
+ timeoutMs: params.timeoutMs,
199
+ });
200
+ },
201
+ loginWithQrWait: async (params) => {
202
+ const { waitForZaloQrLogin } = await loadZalouserChannelRuntime();
203
+ const profile = resolveZalouserQrProfile(params.accountId);
204
+ return await waitForZaloQrLogin({
205
+ profile,
206
+ timeoutMs: params.timeoutMs,
207
+ });
208
+ },
209
+ logoutAccount: async (ctx) =>
210
+ await (
211
+ await loadZalouserChannelRuntime()
212
+ ).logoutZaloProfile(ctx.account.profile || resolveZalouserQrProfile(ctx.accountId)),
213
+ },
214
+ },
215
+ security: zalouserSecurityAdapter,
216
+ threading: zalouserThreadingAdapter,
217
+ pairing: {
218
+ text: zalouserPairingTextAdapter,
219
+ },
220
+ outbound: zalouserOutboundAdapter,
221
+ });
@@ -0,0 +1,33 @@
1
+ import {
2
+ AllowFromListSchema,
3
+ buildCatchallMultiAccountChannelSchema,
4
+ DmPolicySchema,
5
+ GroupPolicySchema,
6
+ MarkdownConfigSchema,
7
+ ToolPolicySchema,
8
+ } from "klaw/plugin-sdk/channel-config-schema";
9
+ import { z } from "zod";
10
+
11
+ const groupConfigSchema = z.object({
12
+ enabled: z.boolean().optional(),
13
+ requireMention: z.boolean().optional(),
14
+ tools: ToolPolicySchema,
15
+ });
16
+
17
+ const zalouserAccountSchema = z.object({
18
+ name: z.string().optional(),
19
+ enabled: z.boolean().optional(),
20
+ markdown: MarkdownConfigSchema,
21
+ profile: z.string().optional(),
22
+ dangerouslyAllowNameMatching: z.boolean().optional(),
23
+ dmPolicy: DmPolicySchema.optional(),
24
+ allowFrom: AllowFromListSchema,
25
+ historyLimit: z.number().int().min(0).optional(),
26
+ groupAllowFrom: AllowFromListSchema,
27
+ groupPolicy: GroupPolicySchema.optional().default("allowlist"),
28
+ groups: z.object({}).catchall(groupConfigSchema).optional(),
29
+ messagePrefix: z.string().optional(),
30
+ responsePrefix: z.string().optional(),
31
+ });
32
+
33
+ export const ZalouserConfigSchema = buildCatchallMultiAccountChannelSchema(zalouserAccountSchema);