@openclaw/zalouser 2026.3.13 → 2026.5.1-beta.2

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 (67) hide show
  1. package/README.md +4 -3
  2. package/api.ts +9 -0
  3. package/channel-plugin-api.ts +3 -0
  4. package/contract-api.ts +2 -0
  5. package/doctor-contract-api.ts +1 -0
  6. package/index.ts +29 -24
  7. package/openclaw.plugin.json +288 -1
  8. package/package.json +38 -11
  9. package/runtime-api.ts +67 -0
  10. package/secret-contract-api.ts +4 -0
  11. package/setup-entry.ts +9 -0
  12. package/setup-plugin-api.ts +2 -0
  13. package/src/accounts.runtime.ts +1 -0
  14. package/src/accounts.test-mocks.ts +7 -3
  15. package/src/accounts.test.ts +53 -1
  16. package/src/accounts.ts +38 -24
  17. package/src/channel-api.ts +20 -0
  18. package/src/channel.adapters.ts +390 -0
  19. package/src/channel.directory.test.ts +47 -40
  20. package/src/channel.runtime.ts +12 -0
  21. package/src/channel.sendpayload.test.ts +41 -23
  22. package/src/channel.setup.test.ts +33 -0
  23. package/src/channel.setup.ts +12 -0
  24. package/src/channel.test.ts +231 -20
  25. package/src/channel.ts +176 -685
  26. package/src/config-schema.ts +5 -5
  27. package/src/directory.ts +54 -0
  28. package/src/doctor-contract.ts +156 -0
  29. package/src/doctor.test.ts +77 -0
  30. package/src/doctor.ts +37 -0
  31. package/src/group-policy.test.ts +4 -4
  32. package/src/group-policy.ts +4 -2
  33. package/src/monitor.account-scope.test.ts +2 -1
  34. package/src/monitor.group-gating.test.ts +162 -8
  35. package/src/monitor.ts +233 -173
  36. package/src/probe.ts +3 -2
  37. package/src/qr-temp-file.ts +1 -1
  38. package/src/reaction.ts +5 -2
  39. package/src/runtime.ts +6 -3
  40. package/src/security-audit.test.ts +80 -0
  41. package/src/security-audit.ts +71 -0
  42. package/src/send.test.ts +2 -2
  43. package/src/send.ts +3 -3
  44. package/src/session-route.ts +121 -0
  45. package/src/setup-core.ts +33 -0
  46. package/src/setup-surface.test.ts +363 -0
  47. package/src/setup-surface.ts +470 -0
  48. package/src/setup-test-helpers.ts +42 -0
  49. package/src/shared.ts +92 -0
  50. package/src/status-issues.test.ts +1 -13
  51. package/src/status-issues.ts +8 -2
  52. package/src/test-helpers.ts +1 -1
  53. package/src/text-styles.test.ts +1 -1
  54. package/src/text-styles.ts +5 -2
  55. package/src/tool.test.ts +66 -3
  56. package/src/tool.ts +76 -14
  57. package/src/types.ts +3 -3
  58. package/src/zalo-js.credentials.test.ts +465 -0
  59. package/src/zalo-js.test-mocks.ts +89 -0
  60. package/src/zalo-js.ts +491 -274
  61. package/src/zca-client.test.ts +24 -0
  62. package/src/zca-client.ts +24 -58
  63. package/src/zca-constants.ts +55 -0
  64. package/test-api.ts +21 -0
  65. package/tsconfig.json +16 -0
  66. package/CHANGELOG.md +0 -107
  67. package/src/onboarding.ts +0 -340
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ describe("zca-client runtime loading", () => {
4
+ it("does not import zca-js until a session is created", async () => {
5
+ vi.clearAllMocks();
6
+ const runtimeFactory = vi.fn(() => ({
7
+ Zalo: class MockZalo {
8
+ constructor(public readonly options?: { logging?: boolean; selfListen?: boolean }) {}
9
+ },
10
+ }));
11
+
12
+ vi.doMock("zca-js", runtimeFactory);
13
+
14
+ const zcaClient = await import("./zca-client.js");
15
+ expect(runtimeFactory).not.toHaveBeenCalled();
16
+
17
+ const client = await zcaClient.createZalo({ logging: false, selfListen: true });
18
+
19
+ expect(runtimeFactory).toHaveBeenCalledTimes(1);
20
+ expect(client).toMatchObject({
21
+ options: { logging: false, selfListen: true },
22
+ });
23
+ });
24
+ });
package/src/zca-client.ts CHANGED
@@ -1,65 +1,25 @@
1
1
  import {
2
- LoginQRCallbackEventType as LoginQRCallbackEventTypeRuntime,
3
- Reactions as ReactionsRuntime,
4
- ThreadType as ThreadTypeRuntime,
5
- Zalo as ZaloRuntime,
6
- } from "zca-js";
2
+ LoginQRCallbackEventType,
3
+ Reactions,
4
+ TextStyle,
5
+ ThreadType,
6
+ type Style,
7
+ } from "./zca-constants.js";
7
8
 
8
- export const ThreadType = ThreadTypeRuntime as {
9
- User: 0;
10
- Group: 1;
9
+ type ZcaJsRuntime = {
10
+ Zalo: unknown;
11
11
  };
12
+ let zcaJsRuntimePromise: Promise<ZcaJsRuntime> | null = null;
12
13
 
13
- export const LoginQRCallbackEventType = LoginQRCallbackEventTypeRuntime as {
14
- QRCodeGenerated: 0;
15
- QRCodeExpired: 1;
16
- QRCodeScanned: 2;
17
- QRCodeDeclined: 3;
18
- GotLoginInfo: 4;
19
- };
20
-
21
- export const Reactions = ReactionsRuntime as Record<string, string> & {
22
- HEART: string;
23
- LIKE: string;
24
- HAHA: string;
25
- WOW: string;
26
- CRY: string;
27
- ANGRY: string;
28
- NONE: string;
29
- };
30
-
31
- // Mirror zca-js sendMessage style constants locally because the package root
32
- // typing surface does not consistently expose TextStyle/Style to tsgo.
33
- export const TextStyle = {
34
- Bold: "b",
35
- Italic: "i",
36
- Underline: "u",
37
- StrikeThrough: "s",
38
- Red: "c_db342e",
39
- Orange: "c_f27806",
40
- Yellow: "c_f7b503",
41
- Green: "c_15a85f",
42
- Small: "f_13",
43
- Big: "f_18",
44
- UnorderedList: "lst_1",
45
- OrderedList: "lst_2",
46
- Indent: "ind_$",
47
- } as const;
48
-
49
- type TextStyleValue = (typeof TextStyle)[keyof typeof TextStyle];
14
+ async function loadZcaJsRuntime(): Promise<ZcaJsRuntime> {
15
+ // Keep zca-js behind a runtime boundary so bundled metadata/contracts can load
16
+ // without resolving its optional WebSocket dependency tree.
17
+ zcaJsRuntimePromise ??= import("zca-js").then((mod) => mod as unknown as ZcaJsRuntime);
18
+ return await zcaJsRuntimePromise;
19
+ }
50
20
 
51
- export type Style =
52
- | {
53
- start: number;
54
- len: number;
55
- st: Exclude<TextStyleValue, typeof TextStyle.Indent>;
56
- }
57
- | {
58
- start: number;
59
- len: number;
60
- st: typeof TextStyle.Indent;
61
- indentSize?: number;
62
- };
21
+ export { LoginQRCallbackEventType, Reactions, TextStyle, ThreadType };
22
+ export type { Style };
63
23
 
64
24
  export type Credentials = {
65
25
  imei: string;
@@ -290,4 +250,10 @@ type ZaloCtor = new (options?: { logging?: boolean; selfListen?: boolean }) => {
290
250
  ): Promise<API>;
291
251
  };
292
252
 
293
- export const Zalo = ZaloRuntime as unknown as ZaloCtor;
253
+ export async function createZalo(
254
+ options?: ConstructorParameters<ZaloCtor>[0],
255
+ ): Promise<InstanceType<ZaloCtor>> {
256
+ const zcaJs = await loadZcaJsRuntime();
257
+ const Zalo = zcaJs.Zalo as ZaloCtor;
258
+ return new Zalo(options);
259
+ }
@@ -0,0 +1,55 @@
1
+ export const ThreadType = {
2
+ User: 0,
3
+ Group: 1,
4
+ } as const;
5
+
6
+ export const LoginQRCallbackEventType = {
7
+ QRCodeGenerated: 0,
8
+ QRCodeExpired: 1,
9
+ QRCodeScanned: 2,
10
+ QRCodeDeclined: 3,
11
+ GotLoginInfo: 4,
12
+ } as const;
13
+
14
+ export const Reactions = {
15
+ HEART: "/-heart",
16
+ LIKE: "/-strong",
17
+ HAHA: ":>",
18
+ WOW: ":o",
19
+ CRY: ":-((",
20
+ ANGRY: ":-h",
21
+ NONE: "",
22
+ } as const;
23
+
24
+ // Mirror zca-js sendMessage style constants locally because the package root
25
+ // typing surface does not consistently expose TextStyle/Style to tsgo.
26
+ export const TextStyle = {
27
+ Bold: "b",
28
+ Italic: "i",
29
+ Underline: "u",
30
+ StrikeThrough: "s",
31
+ Red: "c_db342e",
32
+ Orange: "c_f27806",
33
+ Yellow: "c_f7b503",
34
+ Green: "c_15a85f",
35
+ Small: "f_13",
36
+ Big: "f_18",
37
+ UnorderedList: "lst_1",
38
+ OrderedList: "lst_2",
39
+ Indent: "ind_$",
40
+ } as const;
41
+
42
+ type TextStyleValue = (typeof TextStyle)[keyof typeof TextStyle];
43
+
44
+ export type Style =
45
+ | {
46
+ start: number;
47
+ len: number;
48
+ st: Exclude<TextStyleValue, typeof TextStyle.Indent>;
49
+ }
50
+ | {
51
+ start: number;
52
+ len: number;
53
+ st: typeof TextStyle.Indent;
54
+ indentSize?: number;
55
+ };
package/test-api.ts ADDED
@@ -0,0 +1,21 @@
1
+ export { sendMessageZalouser } from "./src/send.js";
2
+ export { parseZalouserOutboundTarget } from "./src/session-route.js";
3
+ export {
4
+ checkZcaAuthenticated,
5
+ getZcaUserInfo,
6
+ listZalouserAccountIds,
7
+ resolveDefaultZalouserAccountId,
8
+ resolveZalouserAccountSync,
9
+ } from "./src/accounts.js";
10
+ export {
11
+ checkZaloAuthenticated,
12
+ getZaloUserInfo,
13
+ listZaloFriendsMatching,
14
+ listZaloGroupMembers,
15
+ listZaloGroupsMatching,
16
+ logoutZaloProfile,
17
+ resolveZaloAllowFromEntries,
18
+ resolveZaloGroupsByEntries,
19
+ startZaloQrLogin,
20
+ waitForZaloQrLogin,
21
+ } from "./src/zalo-js.js";
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../tsconfig.package-boundary.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "."
5
+ },
6
+ "include": ["./*.ts", "./src/**/*.ts"],
7
+ "exclude": [
8
+ "./**/*.test.ts",
9
+ "./dist/**",
10
+ "./node_modules/**",
11
+ "./src/test-support/**",
12
+ "./src/**/*test-helpers.ts",
13
+ "./src/**/*test-harness.ts",
14
+ "./src/**/*test-support.ts"
15
+ ]
16
+ }
package/CHANGELOG.md DELETED
@@ -1,107 +0,0 @@
1
- # Changelog
2
-
3
- ## 2026.3.13
4
-
5
- ### Changes
6
-
7
- - Version alignment with core OpenClaw release numbers.
8
-
9
- ## 2026.3.12
10
-
11
- ### Changes
12
-
13
- - Version alignment with core OpenClaw release numbers.
14
-
15
- ## 2026.3.11
16
-
17
- ### Changes
18
-
19
- - Version alignment with core OpenClaw release numbers.
20
-
21
- ## 2026.3.10
22
-
23
- ### Changes
24
-
25
- - Version alignment with core OpenClaw release numbers.
26
-
27
- ## 2026.3.9
28
-
29
- ### Changes
30
-
31
- - Version alignment with core OpenClaw release numbers.
32
-
33
- ## 2026.3.8-beta.1
34
-
35
- ### Changes
36
-
37
- - Version alignment with core OpenClaw release numbers.
38
-
39
- ## 2026.3.8
40
-
41
- ### Changes
42
-
43
- - Version alignment with core OpenClaw release numbers.
44
-
45
- ## 2026.3.7
46
-
47
- ### Changes
48
-
49
- - Version alignment with core OpenClaw release numbers.
50
-
51
- ## 2026.3.3
52
-
53
- ### Changes
54
-
55
- - Version alignment with core OpenClaw release numbers.
56
-
57
- ## 2026.3.2
58
-
59
- ### Changes
60
-
61
- - Rebuilt the plugin to use native `zca-js` integration inside OpenClaw (no external `zca` CLI runtime dependency).
62
-
63
- ### Breaking
64
-
65
- - **BREAKING:** Removed the old external CLI-based backend (`zca`/`openzca`/`zca-cli`) from runtime flow. Existing setups that depended on external CLI binaries should re-login with `openclaw channels login --channel zalouser` after upgrading.
66
-
67
- ## 2026.3.1
68
-
69
- ### Changes
70
-
71
- - Version alignment with core OpenClaw release numbers.
72
-
73
- ## 2026.2.26
74
-
75
- ### Changes
76
-
77
- - Version alignment with core OpenClaw release numbers.
78
-
79
- ## 2026.2.25
80
-
81
- ### Changes
82
-
83
- - Version alignment with core OpenClaw release numbers.
84
-
85
- ## 2026.2.24
86
-
87
- ### Changes
88
-
89
- - Version alignment with core OpenClaw release numbers.
90
-
91
- ## 2026.2.22
92
-
93
- ### Changes
94
-
95
- - Version alignment with core OpenClaw release numbers.
96
-
97
- ## 2026.1.17-1
98
-
99
- - Initial version with full channel plugin support
100
- - QR code login via zca-cli
101
- - Multi-account support
102
- - Agent tool for sending messages
103
- - Group and DM policy support
104
- - ChannelDock for lightweight shared metadata
105
- - Zod-based config schema validation
106
- - Setup adapter for programmatic configuration
107
- - Dedicated probe and status issues modules
package/src/onboarding.ts DELETED
@@ -1,340 +0,0 @@
1
- import type {
2
- ChannelOnboardingAdapter,
3
- ChannelOnboardingDmPolicy,
4
- OpenClawConfig,
5
- WizardPrompter,
6
- } from "openclaw/plugin-sdk/zalouser";
7
- import {
8
- DEFAULT_ACCOUNT_ID,
9
- formatResolvedUnresolvedNote,
10
- mergeAllowFromEntries,
11
- normalizeAccountId,
12
- patchScopedAccountConfig,
13
- promptChannelAccessConfig,
14
- resolveAccountIdForConfigure,
15
- setTopLevelChannelDmPolicyWithAllowFrom,
16
- } from "openclaw/plugin-sdk/zalouser";
17
- import {
18
- listZalouserAccountIds,
19
- resolveDefaultZalouserAccountId,
20
- resolveZalouserAccountSync,
21
- checkZcaAuthenticated,
22
- } from "./accounts.js";
23
- import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
24
- import {
25
- logoutZaloProfile,
26
- resolveZaloAllowFromEntries,
27
- resolveZaloGroupsByEntries,
28
- startZaloQrLogin,
29
- waitForZaloQrLogin,
30
- } from "./zalo-js.js";
31
-
32
- const channel = "zalouser" as const;
33
-
34
- function setZalouserAccountScopedConfig(
35
- cfg: OpenClawConfig,
36
- accountId: string,
37
- defaultPatch: Record<string, unknown>,
38
- accountPatch: Record<string, unknown> = defaultPatch,
39
- ): OpenClawConfig {
40
- return patchScopedAccountConfig({
41
- cfg,
42
- channelKey: channel,
43
- accountId,
44
- patch: defaultPatch,
45
- accountPatch,
46
- }) as OpenClawConfig;
47
- }
48
-
49
- function setZalouserDmPolicy(
50
- cfg: OpenClawConfig,
51
- dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
52
- ): OpenClawConfig {
53
- return setTopLevelChannelDmPolicyWithAllowFrom({
54
- cfg,
55
- channel: "zalouser",
56
- dmPolicy,
57
- }) as OpenClawConfig;
58
- }
59
-
60
- async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
61
- await prompter.note(
62
- [
63
- "Zalo Personal Account login via QR code.",
64
- "",
65
- "This plugin uses zca-js directly (no external CLI dependency).",
66
- "",
67
- "Docs: https://docs.openclaw.ai/channels/zalouser",
68
- ].join("\n"),
69
- "Zalo Personal Setup",
70
- );
71
- }
72
-
73
- async function promptZalouserAllowFrom(params: {
74
- cfg: OpenClawConfig;
75
- prompter: WizardPrompter;
76
- accountId: string;
77
- }): Promise<OpenClawConfig> {
78
- const { cfg, prompter, accountId } = params;
79
- const resolved = resolveZalouserAccountSync({ cfg, accountId });
80
- const existingAllowFrom = resolved.config.allowFrom ?? [];
81
- const parseInput = (raw: string) =>
82
- raw
83
- .split(/[\n,;]+/g)
84
- .map((entry) => entry.trim())
85
- .filter(Boolean);
86
-
87
- while (true) {
88
- const entry = await prompter.text({
89
- message: "Zalouser allowFrom (name or user id)",
90
- placeholder: "Alice, 123456789",
91
- initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
92
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
93
- });
94
- const parts = parseInput(String(entry));
95
- const resolvedEntries = await resolveZaloAllowFromEntries({
96
- profile: resolved.profile,
97
- entries: parts,
98
- });
99
-
100
- const unresolved = resolvedEntries.filter((item) => !item.resolved).map((item) => item.input);
101
- if (unresolved.length > 0) {
102
- await prompter.note(
103
- `Could not resolve: ${unresolved.join(", ")}. Use numeric user ids or exact friend names.`,
104
- "Zalo Personal allowlist",
105
- );
106
- continue;
107
- }
108
-
109
- const resolvedIds = resolvedEntries
110
- .filter((item) => item.resolved && item.id)
111
- .map((item) => item.id as string);
112
- const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds);
113
-
114
- const notes = resolvedEntries
115
- .filter((item) => item.note)
116
- .map((item) => `${item.input} -> ${item.id} (${item.note})`);
117
- if (notes.length > 0) {
118
- await prompter.note(notes.join("\n"), "Zalo Personal allowlist");
119
- }
120
-
121
- return setZalouserAccountScopedConfig(cfg, accountId, {
122
- dmPolicy: "allowlist",
123
- allowFrom: unique,
124
- });
125
- }
126
- }
127
-
128
- function setZalouserGroupPolicy(
129
- cfg: OpenClawConfig,
130
- accountId: string,
131
- groupPolicy: "open" | "allowlist" | "disabled",
132
- ): OpenClawConfig {
133
- return setZalouserAccountScopedConfig(cfg, accountId, {
134
- groupPolicy,
135
- });
136
- }
137
-
138
- function setZalouserGroupAllowlist(
139
- cfg: OpenClawConfig,
140
- accountId: string,
141
- groupKeys: string[],
142
- ): OpenClawConfig {
143
- const groups = Object.fromEntries(groupKeys.map((key) => [key, { allow: true }]));
144
- return setZalouserAccountScopedConfig(cfg, accountId, {
145
- groups,
146
- });
147
- }
148
-
149
- const dmPolicy: ChannelOnboardingDmPolicy = {
150
- label: "Zalo Personal",
151
- channel,
152
- policyKey: "channels.zalouser.dmPolicy",
153
- allowFromKey: "channels.zalouser.allowFrom",
154
- getCurrent: (cfg) => (cfg.channels?.zalouser?.dmPolicy ?? "pairing") as "pairing",
155
- setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg, policy),
156
- promptAllowFrom: async ({ cfg, prompter, accountId }) => {
157
- const id =
158
- accountId && normalizeAccountId(accountId)
159
- ? (normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID)
160
- : resolveDefaultZalouserAccountId(cfg);
161
- return promptZalouserAllowFrom({
162
- cfg,
163
- prompter,
164
- accountId: id,
165
- });
166
- },
167
- };
168
-
169
- export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
170
- channel,
171
- dmPolicy,
172
- getStatus: async ({ cfg }) => {
173
- const ids = listZalouserAccountIds(cfg);
174
- let configured = false;
175
- for (const accountId of ids) {
176
- const account = resolveZalouserAccountSync({ cfg, accountId });
177
- const isAuth = await checkZcaAuthenticated(account.profile);
178
- if (isAuth) {
179
- configured = true;
180
- break;
181
- }
182
- }
183
- return {
184
- channel,
185
- configured,
186
- statusLines: [`Zalo Personal: ${configured ? "logged in" : "needs QR login"}`],
187
- selectionHint: configured ? "recommended · logged in" : "recommended · QR login",
188
- quickstartScore: configured ? 1 : 15,
189
- };
190
- },
191
- configure: async ({
192
- cfg,
193
- prompter,
194
- accountOverrides,
195
- shouldPromptAccountIds,
196
- forceAllowFrom,
197
- }) => {
198
- const defaultAccountId = resolveDefaultZalouserAccountId(cfg);
199
- const accountId = await resolveAccountIdForConfigure({
200
- cfg,
201
- prompter,
202
- label: "Zalo Personal",
203
- accountOverride: accountOverrides.zalouser,
204
- shouldPromptAccountIds,
205
- listAccountIds: listZalouserAccountIds,
206
- defaultAccountId,
207
- });
208
-
209
- let next = cfg;
210
- const account = resolveZalouserAccountSync({ cfg: next, accountId });
211
- const alreadyAuthenticated = await checkZcaAuthenticated(account.profile);
212
-
213
- if (!alreadyAuthenticated) {
214
- await noteZalouserHelp(prompter);
215
-
216
- const wantsLogin = await prompter.confirm({
217
- message: "Login via QR code now?",
218
- initialValue: true,
219
- });
220
-
221
- if (wantsLogin) {
222
- const start = await startZaloQrLogin({ profile: account.profile, timeoutMs: 35_000 });
223
- if (start.qrDataUrl) {
224
- const qrPath = await writeQrDataUrlToTempFile(start.qrDataUrl, account.profile);
225
- await prompter.note(
226
- [
227
- start.message,
228
- qrPath
229
- ? `QR image saved to: ${qrPath}`
230
- : "Could not write QR image file; use gateway web login UI instead.",
231
- "Scan + approve on phone, then continue.",
232
- ].join("\n"),
233
- "QR Login",
234
- );
235
- const scanned = await prompter.confirm({
236
- message: "Did you scan and approve the QR on your phone?",
237
- initialValue: true,
238
- });
239
- if (scanned) {
240
- const waited = await waitForZaloQrLogin({
241
- profile: account.profile,
242
- timeoutMs: 120_000,
243
- });
244
- await prompter.note(waited.message, waited.connected ? "Success" : "Login pending");
245
- }
246
- } else {
247
- await prompter.note(start.message, "Login pending");
248
- }
249
- }
250
- } else {
251
- const keepSession = await prompter.confirm({
252
- message: "Zalo Personal already logged in. Keep session?",
253
- initialValue: true,
254
- });
255
- if (!keepSession) {
256
- await logoutZaloProfile(account.profile);
257
- const start = await startZaloQrLogin({
258
- profile: account.profile,
259
- force: true,
260
- timeoutMs: 35_000,
261
- });
262
- if (start.qrDataUrl) {
263
- const qrPath = await writeQrDataUrlToTempFile(start.qrDataUrl, account.profile);
264
- await prompter.note(
265
- [start.message, qrPath ? `QR image saved to: ${qrPath}` : undefined]
266
- .filter(Boolean)
267
- .join("\n"),
268
- "QR Login",
269
- );
270
- const waited = await waitForZaloQrLogin({ profile: account.profile, timeoutMs: 120_000 });
271
- await prompter.note(waited.message, waited.connected ? "Success" : "Login pending");
272
- }
273
- }
274
- }
275
-
276
- next = setZalouserAccountScopedConfig(
277
- next,
278
- accountId,
279
- { profile: account.profile !== "default" ? account.profile : undefined },
280
- { profile: account.profile, enabled: true },
281
- );
282
-
283
- if (forceAllowFrom) {
284
- next = await promptZalouserAllowFrom({
285
- cfg: next,
286
- prompter,
287
- accountId,
288
- });
289
- }
290
-
291
- const updatedAccount = resolveZalouserAccountSync({ cfg: next, accountId });
292
- const accessConfig = await promptChannelAccessConfig({
293
- prompter,
294
- label: "Zalo groups",
295
- currentPolicy: updatedAccount.config.groupPolicy ?? "allowlist",
296
- currentEntries: Object.keys(updatedAccount.config.groups ?? {}),
297
- placeholder: "Family, Work, 123456789",
298
- updatePrompt: Boolean(updatedAccount.config.groups),
299
- });
300
-
301
- if (accessConfig) {
302
- if (accessConfig.policy !== "allowlist") {
303
- next = setZalouserGroupPolicy(next, accountId, accessConfig.policy);
304
- } else {
305
- let keys = accessConfig.entries;
306
- if (accessConfig.entries.length > 0) {
307
- try {
308
- const resolved = await resolveZaloGroupsByEntries({
309
- profile: updatedAccount.profile,
310
- entries: accessConfig.entries,
311
- });
312
- const resolvedIds = resolved
313
- .filter((entry) => entry.resolved && entry.id)
314
- .map((entry) => entry.id as string);
315
- const unresolved = resolved
316
- .filter((entry) => !entry.resolved)
317
- .map((entry) => entry.input);
318
- keys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
319
- const resolution = formatResolvedUnresolvedNote({
320
- resolved: resolvedIds,
321
- unresolved,
322
- });
323
- if (resolution) {
324
- await prompter.note(resolution, "Zalo groups");
325
- }
326
- } catch (err) {
327
- await prompter.note(
328
- `Group lookup failed; keeping entries as typed. ${String(err)}`,
329
- "Zalo groups",
330
- );
331
- }
332
- }
333
- next = setZalouserGroupPolicy(next, accountId, "allowlist");
334
- next = setZalouserGroupAllowlist(next, accountId, keys);
335
- }
336
- }
337
-
338
- return { cfg: next, accountId };
339
- },
340
- };