@kodelyth/zalouser 2026.5.42 → 2026.6.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 (71) hide show
  1. package/klaw.plugin.json +286 -3
  2. package/package.json +19 -6
  3. package/api.ts +0 -9
  4. package/channel-plugin-api.ts +0 -3
  5. package/contract-api.ts +0 -2
  6. package/doctor-contract-api.ts +0 -1
  7. package/index.ts +0 -34
  8. package/runtime-api.ts +0 -62
  9. package/secret-contract-api.ts +0 -4
  10. package/setup-entry.ts +0 -9
  11. package/setup-plugin-api.ts +0 -2
  12. package/src/accounts.runtime.ts +0 -1
  13. package/src/accounts.test-mocks.ts +0 -14
  14. package/src/accounts.test.ts +0 -298
  15. package/src/accounts.ts +0 -136
  16. package/src/channel-api.ts +0 -16
  17. package/src/channel.adapters.ts +0 -432
  18. package/src/channel.directory.test.ts +0 -59
  19. package/src/channel.runtime.ts +0 -12
  20. package/src/channel.sendpayload.test.ts +0 -311
  21. package/src/channel.setup.test.ts +0 -30
  22. package/src/channel.setup.ts +0 -12
  23. package/src/channel.test.ts +0 -424
  24. package/src/channel.ts +0 -221
  25. package/src/config-schema.ts +0 -33
  26. package/src/directory.ts +0 -54
  27. package/src/doctor-contract.ts +0 -156
  28. package/src/doctor.test.ts +0 -87
  29. package/src/doctor.ts +0 -37
  30. package/src/group-policy.test.ts +0 -61
  31. package/src/group-policy.ts +0 -83
  32. package/src/message-sid.test.ts +0 -66
  33. package/src/message-sid.ts +0 -80
  34. package/src/monitor.account-scope.test.ts +0 -122
  35. package/src/monitor.group-gating.test.ts +0 -967
  36. package/src/monitor.send-mocks.ts +0 -20
  37. package/src/monitor.ts +0 -1057
  38. package/src/probe.test.ts +0 -60
  39. package/src/probe.ts +0 -35
  40. package/src/qr-temp-file.ts +0 -19
  41. package/src/reaction.test.ts +0 -19
  42. package/src/reaction.ts +0 -32
  43. package/src/runtime.ts +0 -9
  44. package/src/security-audit.test.ts +0 -83
  45. package/src/security-audit.ts +0 -71
  46. package/src/send-receipt.ts +0 -31
  47. package/src/send.test.ts +0 -424
  48. package/src/send.ts +0 -280
  49. package/src/session-route.ts +0 -121
  50. package/src/setup-core.ts +0 -36
  51. package/src/setup-surface.test.ts +0 -367
  52. package/src/setup-surface.ts +0 -481
  53. package/src/setup-test-helpers.ts +0 -42
  54. package/src/shared.ts +0 -92
  55. package/src/status-issues.test.ts +0 -31
  56. package/src/status-issues.ts +0 -55
  57. package/src/test-helpers.ts +0 -26
  58. package/src/text-styles.test.ts +0 -203
  59. package/src/text-styles.ts +0 -540
  60. package/src/tool.test.ts +0 -212
  61. package/src/tool.ts +0 -200
  62. package/src/types.ts +0 -127
  63. package/src/zalo-js.credentials.test.ts +0 -465
  64. package/src/zalo-js.test-mocks.ts +0 -89
  65. package/src/zalo-js.ts +0 -1889
  66. package/src/zca-client.test.ts +0 -27
  67. package/src/zca-client.ts +0 -259
  68. package/src/zca-constants.ts +0 -55
  69. package/src/zca-js-exports.d.ts +0 -22
  70. package/test-api.ts +0 -21
  71. package/tsconfig.json +0 -16
@@ -1,481 +0,0 @@
1
- import {
2
- addWildcardAllowFrom,
3
- DEFAULT_ACCOUNT_ID,
4
- formatCliCommand,
5
- formatDocsLink,
6
- formatResolvedUnresolvedNote,
7
- mergeAllowFromEntries,
8
- normalizeAccountId,
9
- patchScopedAccountConfig,
10
- createSetupTranslator,
11
- type ChannelSetupDmPolicy,
12
- type ChannelSetupWizard,
13
- type DmPolicy,
14
- type KlawConfig,
15
- } from "klaw/plugin-sdk/setup";
16
- import {
17
- checkZcaAuthenticated,
18
- listZalouserAccountIds,
19
- resolveDefaultZalouserAccountId,
20
- resolveZalouserAccountSync,
21
- } from "./accounts.js";
22
- import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
23
- import {
24
- logoutZaloProfile,
25
- resolveZaloAllowFromEntries,
26
- resolveZaloGroupsByEntries,
27
- startZaloQrLogin,
28
- waitForZaloQrLogin,
29
- } from "./zalo-js.js";
30
-
31
- const t = createSetupTranslator();
32
-
33
- const channel = "zalouser" as const;
34
- const ZALOUSER_ALLOW_FROM_PLACEHOLDER = t("wizard.zalouser.allowFromPlaceholder");
35
- const ZALOUSER_GROUPS_PLACEHOLDER = t("wizard.zalouser.groupsPlaceholder");
36
- const ZALOUSER_DM_ACCESS_TITLE = t("wizard.zalouser.dmAccessTitle");
37
- const ZALOUSER_ALLOWLIST_TITLE = t("wizard.zalouser.allowlistTitle");
38
- const ZALOUSER_GROUPS_TITLE = t("wizard.zalouser.groupsTitle");
39
-
40
- function parseZalouserEntries(raw: string): string[] {
41
- return raw
42
- .split(/[\n,;]+/g)
43
- .map((entry) => entry.trim())
44
- .filter(Boolean);
45
- }
46
-
47
- function setZalouserAccountScopedConfig(
48
- cfg: KlawConfig,
49
- accountId: string,
50
- defaultPatch: Record<string, unknown>,
51
- accountPatch: Record<string, unknown> = defaultPatch,
52
- ): KlawConfig {
53
- return patchScopedAccountConfig({
54
- cfg,
55
- channelKey: channel,
56
- accountId,
57
- patch: defaultPatch,
58
- accountPatch,
59
- });
60
- }
61
-
62
- function setZalouserDmPolicy(cfg: KlawConfig, accountId: string, policy: DmPolicy): KlawConfig {
63
- const resolvedAccountId = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID;
64
- const resolved = resolveZalouserAccountSync({ cfg, accountId: resolvedAccountId });
65
- return setZalouserAccountScopedConfig(
66
- cfg,
67
- resolvedAccountId,
68
- {
69
- dmPolicy: policy,
70
- ...(policy === "open" ? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) } : {}),
71
- },
72
- {
73
- dmPolicy: policy,
74
- ...(policy === "open" ? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) } : {}),
75
- },
76
- );
77
- }
78
-
79
- function setZalouserGroupPolicy(
80
- cfg: KlawConfig,
81
- accountId: string,
82
- groupPolicy: "open" | "allowlist" | "disabled",
83
- ): KlawConfig {
84
- return setZalouserAccountScopedConfig(cfg, accountId, {
85
- groupPolicy,
86
- });
87
- }
88
-
89
- function setZalouserGroupAllowlist(
90
- cfg: KlawConfig,
91
- accountId: string,
92
- groupKeys: string[],
93
- ): KlawConfig {
94
- const groups = Object.fromEntries(
95
- groupKeys.map((key) => [key, { enabled: true, requireMention: true }]),
96
- );
97
- return setZalouserAccountScopedConfig(cfg, accountId, {
98
- groups,
99
- });
100
- }
101
-
102
- function ensureZalouserPluginEnabled(cfg: KlawConfig): KlawConfig {
103
- const next: KlawConfig = {
104
- ...cfg,
105
- plugins: {
106
- ...cfg.plugins,
107
- entries: {
108
- ...cfg.plugins?.entries,
109
- zalouser: {
110
- ...cfg.plugins?.entries?.zalouser,
111
- enabled: true,
112
- },
113
- },
114
- },
115
- };
116
- const allow = next.plugins?.allow;
117
- if (!Array.isArray(allow) || allow.includes(channel)) {
118
- return next;
119
- }
120
- return {
121
- ...next,
122
- plugins: {
123
- ...next.plugins,
124
- allow: [...allow, channel],
125
- },
126
- };
127
- }
128
-
129
- async function noteZalouserHelp(
130
- prompter: Parameters<NonNullable<ChannelSetupWizard["prepare"]>>[0]["prompter"],
131
- ): Promise<void> {
132
- await prompter.note(
133
- [
134
- t("wizard.zalouser.helpQrLogin"),
135
- "",
136
- t("wizard.zalouser.helpZcaJs"),
137
- "",
138
- `Docs: ${formatDocsLink("/channels/zalouser", "zalouser")}`,
139
- ].join("\n"),
140
- t("wizard.zalouser.setupTitle"),
141
- );
142
- }
143
-
144
- async function promptZalouserAllowFrom(params: {
145
- cfg: KlawConfig;
146
- prompter: Parameters<NonNullable<ChannelSetupDmPolicy["promptAllowFrom"]>>[0]["prompter"];
147
- accountId: string;
148
- }): Promise<KlawConfig> {
149
- const { cfg, prompter, accountId } = params;
150
- const resolved = resolveZalouserAccountSync({ cfg, accountId });
151
- const existingAllowFrom = resolved.config.allowFrom ?? [];
152
-
153
- while (true) {
154
- const entry = await prompter.text({
155
- message: t("wizard.zalouser.allowFromPrompt"),
156
- placeholder: ZALOUSER_ALLOW_FROM_PLACEHOLDER,
157
- initialValue: existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : undefined,
158
- });
159
- const parts = parseZalouserEntries(entry);
160
- if (parts.length === 0) {
161
- await prompter.note(
162
- [
163
- t("wizard.zalouser.noDmAllowlist"),
164
- t("wizard.zalouser.directChatsBlocked"),
165
- t("wizard.zalouser.peersLookupTip", {
166
- command: formatCliCommand("klaw directory peers list --channel zalouser"),
167
- }),
168
- ].join("\n"),
169
- ZALOUSER_ALLOWLIST_TITLE,
170
- );
171
- return setZalouserAccountScopedConfig(cfg, accountId, {
172
- dmPolicy: "allowlist",
173
- allowFrom: [],
174
- });
175
- }
176
- const resolvedEntries = await resolveZaloAllowFromEntries({
177
- profile: resolved.profile,
178
- entries: parts,
179
- });
180
-
181
- const unresolved = resolvedEntries.filter((item) => !item.resolved).map((item) => item.input);
182
- if (unresolved.length > 0) {
183
- await prompter.note(
184
- t("wizard.zalouser.couldNotResolve", { entries: unresolved.join(", ") }),
185
- ZALOUSER_ALLOWLIST_TITLE,
186
- );
187
- continue;
188
- }
189
-
190
- const resolvedIds = resolvedEntries
191
- .filter((item) => item.resolved && item.id)
192
- .map((item) => item.id as string);
193
- const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds);
194
-
195
- const notes = resolvedEntries
196
- .filter((item) => item.note)
197
- .map((item) => `${item.input} -> ${item.id} (${item.note})`);
198
- if (notes.length > 0) {
199
- await prompter.note(notes.join("\n"), ZALOUSER_ALLOWLIST_TITLE);
200
- }
201
-
202
- return setZalouserAccountScopedConfig(cfg, accountId, {
203
- dmPolicy: "allowlist",
204
- allowFrom: unique,
205
- });
206
- }
207
- }
208
-
209
- const zalouserDmPolicy: ChannelSetupDmPolicy = {
210
- label: "Zalo Personal",
211
- channel,
212
- policyKey: "channels.zalouser.dmPolicy",
213
- allowFromKey: "channels.zalouser.allowFrom",
214
- resolveConfigKeys: (cfg, accountId) =>
215
- (accountId ?? resolveDefaultZalouserAccountId(cfg)) !== DEFAULT_ACCOUNT_ID
216
- ? {
217
- policyKey: `channels.zalouser.accounts.${accountId ?? resolveDefaultZalouserAccountId(cfg)}.dmPolicy`,
218
- allowFromKey: `channels.zalouser.accounts.${accountId ?? resolveDefaultZalouserAccountId(cfg)}.allowFrom`,
219
- }
220
- : {
221
- policyKey: "channels.zalouser.dmPolicy",
222
- allowFromKey: "channels.zalouser.allowFrom",
223
- },
224
- getCurrent: (cfg, accountId) =>
225
- resolveZalouserAccountSync({
226
- cfg,
227
- accountId: accountId ?? resolveDefaultZalouserAccountId(cfg),
228
- }).config.dmPolicy ?? "pairing",
229
- setPolicy: (cfg, policy, accountId) =>
230
- setZalouserDmPolicy(cfg, accountId ?? resolveDefaultZalouserAccountId(cfg), policy),
231
- promptAllowFrom: async ({ cfg, prompter, accountId }) => {
232
- const id =
233
- accountId && normalizeAccountId(accountId)
234
- ? (normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID)
235
- : resolveDefaultZalouserAccountId(cfg);
236
- return await promptZalouserAllowFrom({
237
- cfg: cfg,
238
- prompter,
239
- accountId: id,
240
- });
241
- },
242
- };
243
-
244
- async function promptZalouserQuickstartDmPolicy(params: {
245
- cfg: KlawConfig;
246
- prompter: Parameters<NonNullable<ChannelSetupWizard["prepare"]>>[0]["prompter"];
247
- accountId: string;
248
- }): Promise<KlawConfig> {
249
- const { cfg, prompter, accountId } = params;
250
- const resolved = resolveZalouserAccountSync({ cfg, accountId });
251
- const existingPolicy = resolved.config.dmPolicy ?? "pairing";
252
- const existingAllowFrom = resolved.config.allowFrom ?? [];
253
- const existingLabel = existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : "unset";
254
-
255
- await prompter.note(
256
- [
257
- t("wizard.zalouser.dmHelpSeparate"),
258
- t("wizard.zalouser.dmHelpPairing"),
259
- t("wizard.zalouser.dmHelpAllowlist"),
260
- t("wizard.zalouser.dmHelpOpen"),
261
- t("wizard.zalouser.dmHelpDisabled"),
262
- "",
263
- `Current: dmPolicy=${existingPolicy}, allowFrom=${existingLabel}`,
264
- t("wizard.zalouser.dmHelpAllowlistEmpty"),
265
- ].join("\n"),
266
- ZALOUSER_DM_ACCESS_TITLE,
267
- );
268
-
269
- const policy = (await prompter.select({
270
- message: t("wizard.zalouser.dmPolicyPrompt"),
271
- options: [
272
- { value: "pairing", label: t("wizard.channels.dmPolicyPairing") },
273
- { value: "allowlist", label: t("wizard.channels.dmPolicyAllowlistOption") },
274
- { value: "open", label: t("wizard.channels.dmPolicyOpenOption") },
275
- { value: "disabled", label: t("wizard.channels.dmPolicyDisabledOption") },
276
- ],
277
- initialValue: existingPolicy,
278
- })) as DmPolicy;
279
-
280
- if (policy === "allowlist") {
281
- return await promptZalouserAllowFrom({
282
- cfg,
283
- prompter,
284
- accountId,
285
- });
286
- }
287
- return setZalouserDmPolicy(cfg, accountId, policy);
288
- }
289
-
290
- export { zalouserSetupAdapter } from "./setup-core.js";
291
-
292
- export const zalouserSetupWizard: ChannelSetupWizard = {
293
- channel,
294
- status: {
295
- configuredLabel: t("wizard.channels.statusLoggedIn"),
296
- unconfiguredLabel: t("wizard.channels.statusNeedsQrLogin"),
297
- configuredHint: t("wizard.channels.statusRecommendedLoggedIn"),
298
- unconfiguredHint: t("wizard.channels.statusRecommendedQrLogin"),
299
- configuredScore: 1,
300
- unconfiguredScore: 15,
301
- resolveConfigured: async ({ cfg, accountId }) => {
302
- const ids = accountId ? [accountId] : listZalouserAccountIds(cfg);
303
- for (const resolvedAccountId of ids) {
304
- const account = resolveZalouserAccountSync({ cfg, accountId: resolvedAccountId });
305
- if (await checkZcaAuthenticated(account.profile)) {
306
- return true;
307
- }
308
- }
309
- return false;
310
- },
311
- resolveStatusLines: async ({ cfg, accountId, configured }) => {
312
- void cfg;
313
- const label =
314
- accountId && accountId !== DEFAULT_ACCOUNT_ID
315
- ? `Zalo Personal (${accountId})`
316
- : "Zalo Personal";
317
- return [`${label}: ${configured ? "logged in" : "needs QR login"}`];
318
- },
319
- },
320
- prepare: async ({ cfg, accountId, prompter, options }) => {
321
- let next = cfg;
322
- const account = resolveZalouserAccountSync({ cfg: next, accountId });
323
- const alreadyAuthenticated = await checkZcaAuthenticated(account.profile);
324
-
325
- if (!alreadyAuthenticated) {
326
- await noteZalouserHelp(prompter);
327
- const wantsLogin = await prompter.confirm({
328
- message: t("wizard.zalouser.loginQrPrompt"),
329
- initialValue: true,
330
- });
331
-
332
- if (wantsLogin) {
333
- const start = await startZaloQrLogin({ profile: account.profile, timeoutMs: 35_000 });
334
- if (start.qrDataUrl) {
335
- const qrPath = await writeQrDataUrlToTempFile(start.qrDataUrl, account.profile);
336
- await prompter.note(
337
- [
338
- start.message,
339
- qrPath
340
- ? t("wizard.zalouser.qrImageSaved", { path: qrPath })
341
- : t("wizard.zalouser.qrImageWriteFailed"),
342
- t("wizard.zalouser.scanApproveContinue"),
343
- ].join("\n"),
344
- t("wizard.zalouser.qrLoginTitle"),
345
- );
346
- const scanned = await prompter.confirm({
347
- message: t("wizard.zalouser.qrScannedPrompt"),
348
- initialValue: true,
349
- });
350
- if (scanned) {
351
- const waited = await waitForZaloQrLogin({
352
- profile: account.profile,
353
- timeoutMs: 120_000,
354
- });
355
- await prompter.note(
356
- waited.message,
357
- waited.connected ? t("common.done") : t("wizard.zalouser.loginPendingTitle"),
358
- );
359
- }
360
- } else {
361
- await prompter.note(start.message, t("wizard.zalouser.loginPendingTitle"));
362
- }
363
- }
364
- } else {
365
- const keepSession = await prompter.confirm({
366
- message: t("wizard.zalouser.keepSessionPrompt"),
367
- initialValue: true,
368
- });
369
- if (!keepSession) {
370
- await logoutZaloProfile(account.profile);
371
- const start = await startZaloQrLogin({
372
- profile: account.profile,
373
- force: true,
374
- timeoutMs: 35_000,
375
- });
376
- if (start.qrDataUrl) {
377
- const qrPath = await writeQrDataUrlToTempFile(start.qrDataUrl, account.profile);
378
- await prompter.note(
379
- [
380
- start.message,
381
- qrPath ? t("wizard.zalouser.qrImageSaved", { path: qrPath }) : undefined,
382
- ]
383
- .filter(Boolean)
384
- .join("\n"),
385
- t("wizard.zalouser.qrLoginTitle"),
386
- );
387
- const waited = await waitForZaloQrLogin({ profile: account.profile, timeoutMs: 120_000 });
388
- await prompter.note(
389
- waited.message,
390
- waited.connected ? t("common.done") : t("wizard.zalouser.loginPendingTitle"),
391
- );
392
- }
393
- }
394
- }
395
-
396
- next = setZalouserAccountScopedConfig(
397
- next,
398
- accountId,
399
- { profile: account.profile !== "default" ? account.profile : undefined },
400
- { profile: account.profile, enabled: true },
401
- );
402
-
403
- if (options?.quickstartDefaults) {
404
- next = await promptZalouserQuickstartDmPolicy({
405
- cfg: next,
406
- prompter,
407
- accountId,
408
- });
409
- }
410
-
411
- return { cfg: next };
412
- },
413
- credentials: [],
414
- groupAccess: {
415
- label: "Zalo groups",
416
- placeholder: ZALOUSER_GROUPS_PLACEHOLDER,
417
- currentPolicy: ({ cfg, accountId }) =>
418
- resolveZalouserAccountSync({ cfg, accountId }).config.groupPolicy ?? "allowlist",
419
- currentEntries: ({ cfg, accountId }) =>
420
- Object.keys(resolveZalouserAccountSync({ cfg, accountId }).config.groups ?? {}),
421
- updatePrompt: ({ cfg, accountId }) =>
422
- Boolean(resolveZalouserAccountSync({ cfg, accountId }).config.groups),
423
- setPolicy: ({ cfg, accountId, policy }) => setZalouserGroupPolicy(cfg, accountId, policy),
424
- resolveAllowlist: async ({ cfg, accountId, entries, prompter }) => {
425
- if (entries.length === 0) {
426
- await prompter.note(
427
- [
428
- t("wizard.zalouser.noGroupAllowlist"),
429
- t("wizard.zalouser.groupChatsBlocked"),
430
- t("wizard.zalouser.groupsLookupTip", {
431
- command: formatCliCommand("klaw directory groups list --channel zalouser"),
432
- }),
433
- t("wizard.zalouser.groupMentionRequirement"),
434
- ].join("\n"),
435
- ZALOUSER_GROUPS_TITLE,
436
- );
437
- return [];
438
- }
439
- const updatedAccount = resolveZalouserAccountSync({ cfg: cfg, accountId });
440
- try {
441
- const resolved = await resolveZaloGroupsByEntries({
442
- profile: updatedAccount.profile,
443
- entries,
444
- });
445
- const resolvedIds = resolved
446
- .filter((entry) => entry.resolved && entry.id)
447
- .map((entry) => entry.id as string);
448
- const unresolved = resolved.filter((entry) => !entry.resolved).map((entry) => entry.input);
449
- const keys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
450
- const resolution = formatResolvedUnresolvedNote({
451
- resolved: resolvedIds,
452
- unresolved,
453
- });
454
- if (resolution) {
455
- await prompter.note(resolution, ZALOUSER_GROUPS_TITLE);
456
- }
457
- return keys;
458
- } catch (err) {
459
- await prompter.note(
460
- t("wizard.zalouser.groupLookupFailed", { error: String(err) }),
461
- ZALOUSER_GROUPS_TITLE,
462
- );
463
- return entries.map((entry) => entry.trim()).filter(Boolean);
464
- }
465
- },
466
- applyAllowlist: ({ cfg, accountId, resolved }) =>
467
- setZalouserGroupAllowlist(cfg, accountId, resolved as string[]),
468
- },
469
- finalize: async ({ cfg, accountId, forceAllowFrom, options, prompter }) => {
470
- let next = cfg;
471
- if (forceAllowFrom && !options?.quickstartDefaults) {
472
- next = await promptZalouserAllowFrom({
473
- cfg: next,
474
- prompter,
475
- accountId,
476
- });
477
- }
478
- return { cfg: ensureZalouserPluginEnabled(next) };
479
- },
480
- dmPolicy: zalouserDmPolicy,
481
- };
@@ -1,42 +0,0 @@
1
- import { createScopedDmSecurityResolver } from "klaw/plugin-sdk/channel-config-helpers";
2
- import type { KlawConfig } from "../runtime-api.js";
3
- import {
4
- listZalouserAccountIds,
5
- resolveDefaultZalouserAccountId,
6
- resolveZalouserAccountSync,
7
- } from "./accounts.js";
8
- import { zalouserSetupAdapter } from "./setup-core.js";
9
- import { zalouserSetupWizard } from "./setup-surface.js";
10
-
11
- export const zalouserSetupPlugin = {
12
- id: "zalouser",
13
- meta: {
14
- id: "zalouser",
15
- label: "ZaloUser",
16
- selectionLabel: "ZaloUser",
17
- docsPath: "/channels/zalouser",
18
- blurb: "Unofficial Zalo personal account connector.",
19
- },
20
- capabilities: {
21
- chatTypes: ["direct", "group"] as Array<"direct" | "group">,
22
- },
23
- config: {
24
- listAccountIds: (cfg: unknown) => listZalouserAccountIds(cfg as never),
25
- defaultAccountId: (cfg: unknown) => resolveDefaultZalouserAccountId(cfg as never),
26
- resolveAccount: (cfg: KlawConfig, accountId?: string | null) =>
27
- resolveZalouserAccountSync({ cfg, accountId }),
28
- },
29
- security: {
30
- resolveDmPolicy: createScopedDmSecurityResolver({
31
- channelKey: "zalouser",
32
- resolvePolicy: (account: ReturnType<typeof resolveZalouserAccountSync>) =>
33
- account.config.dmPolicy,
34
- resolveAllowFrom: (account: ReturnType<typeof resolveZalouserAccountSync>) =>
35
- account.config.allowFrom,
36
- policyPathSuffix: "dmPolicy",
37
- normalizeEntry: (raw: string) => raw.trim().replace(/^(zalouser|zlu):/i, ""),
38
- }),
39
- },
40
- setup: zalouserSetupAdapter,
41
- setupWizard: zalouserSetupWizard,
42
- } as const;
package/src/shared.ts DELETED
@@ -1,92 +0,0 @@
1
- import { describeAccountSnapshot } from "klaw/plugin-sdk/account-helpers";
2
- import {
3
- adaptScopedAccountAccessor,
4
- createScopedChannelConfigAdapter,
5
- } from "klaw/plugin-sdk/channel-config-helpers";
6
- import {
7
- listZalouserAccountIds,
8
- resolveDefaultZalouserAccountId,
9
- resolveZalouserAccountSync,
10
- checkZcaAuthenticated,
11
- type ResolvedZalouserAccount,
12
- } from "./accounts.js";
13
- import type { ChannelPlugin } from "./channel-api.js";
14
- import { buildChannelConfigSchema, formatAllowFromLowercase } from "./channel-api.js";
15
- import { ZalouserConfigSchema } from "./config-schema.js";
16
- import { zalouserDoctor } from "./doctor.js";
17
-
18
- const zalouserMeta: ChannelPlugin<ResolvedZalouserAccount>["meta"] = {
19
- id: "zalouser",
20
- label: "Zalo Personal",
21
- selectionLabel: "Zalo (Personal Account)",
22
- docsPath: "/channels/zalouser",
23
- docsLabel: "zalouser",
24
- blurb: "Zalo personal account via QR code login.",
25
- aliases: ["zlu"],
26
- order: 85,
27
- quickstartAllowFrom: false,
28
- };
29
-
30
- const zalouserConfigAdapter = createScopedChannelConfigAdapter<ResolvedZalouserAccount>({
31
- sectionKey: "zalouser",
32
- listAccountIds: listZalouserAccountIds,
33
- resolveAccount: adaptScopedAccountAccessor(resolveZalouserAccountSync),
34
- defaultAccountId: resolveDefaultZalouserAccountId,
35
- clearBaseFields: [
36
- "profile",
37
- "name",
38
- "dmPolicy",
39
- "allowFrom",
40
- "historyLimit",
41
- "groupAllowFrom",
42
- "groupPolicy",
43
- "groups",
44
- "messagePrefix",
45
- ],
46
- resolveAllowFrom: (account) => account.config.allowFrom,
47
- formatAllowFrom: (allowFrom) =>
48
- formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }),
49
- });
50
-
51
- export function createZalouserPluginBase(params: {
52
- setupWizard: NonNullable<ChannelPlugin<ResolvedZalouserAccount>["setupWizard"]>;
53
- setup: NonNullable<ChannelPlugin<ResolvedZalouserAccount>["setup"]>;
54
- }): Pick<
55
- ChannelPlugin<ResolvedZalouserAccount>,
56
- | "id"
57
- | "meta"
58
- | "setupWizard"
59
- | "capabilities"
60
- | "doctor"
61
- | "reload"
62
- | "configSchema"
63
- | "config"
64
- | "setup"
65
- > {
66
- return {
67
- id: "zalouser",
68
- meta: zalouserMeta,
69
- setupWizard: params.setupWizard,
70
- capabilities: {
71
- chatTypes: ["direct", "group"],
72
- media: true,
73
- reactions: true,
74
- threads: false,
75
- polls: false,
76
- nativeCommands: false,
77
- blockStreaming: true,
78
- },
79
- doctor: zalouserDoctor,
80
- reload: { configPrefixes: ["channels.zalouser"] },
81
- configSchema: buildChannelConfigSchema(ZalouserConfigSchema),
82
- config: {
83
- ...zalouserConfigAdapter,
84
- isConfigured: async (account) => await checkZcaAuthenticated(account.profile),
85
- describeAccount: (account) =>
86
- describeAccountSnapshot({
87
- account,
88
- }),
89
- },
90
- setup: params.setup,
91
- };
92
- }
@@ -1,31 +0,0 @@
1
- import { expectOpenDmPolicyConfigIssue } from "klaw/plugin-sdk/channel-test-helpers";
2
- import { describe, expect, it } from "vitest";
3
- import { collectZalouserStatusIssues } from "./status-issues.js";
4
-
5
- describe("collectZalouserStatusIssues", () => {
6
- it("flags missing auth when configured is false", () => {
7
- const issues = collectZalouserStatusIssues([
8
- {
9
- accountId: "default",
10
- enabled: true,
11
- configured: false,
12
- lastError: "not authenticated",
13
- },
14
- ]);
15
- expect(issues).toHaveLength(1);
16
- expect(issues[0]?.kind).toBe("auth");
17
- expect(issues[0]?.message).toMatch(/Not authenticated/i);
18
- });
19
-
20
- it("warns when dmPolicy is open", () => {
21
- expectOpenDmPolicyConfigIssue({
22
- collectIssues: collectZalouserStatusIssues,
23
- account: {
24
- accountId: "default",
25
- enabled: true,
26
- configured: true,
27
- dmPolicy: "open",
28
- },
29
- });
30
- });
31
- });
@@ -1,55 +0,0 @@
1
- import type { ChannelAccountSnapshot, ChannelStatusIssue } from "klaw/plugin-sdk/channel-contract";
2
- import {
3
- coerceStatusIssueAccountId,
4
- readStatusIssueFields,
5
- } from "klaw/plugin-sdk/extension-shared";
6
-
7
- const ZALOUSER_STATUS_FIELDS = [
8
- "accountId",
9
- "enabled",
10
- "configured",
11
- "dmPolicy",
12
- "lastError",
13
- ] as const;
14
-
15
- export function collectZalouserStatusIssues(
16
- accounts: ChannelAccountSnapshot[],
17
- ): ChannelStatusIssue[] {
18
- const issues: ChannelStatusIssue[] = [];
19
- for (const entry of accounts) {
20
- const account = readStatusIssueFields(entry, ZALOUSER_STATUS_FIELDS);
21
- if (!account) {
22
- continue;
23
- }
24
- const accountId = coerceStatusIssueAccountId(account.accountId) ?? "default";
25
- const enabled = account.enabled !== false;
26
- if (!enabled) {
27
- continue;
28
- }
29
-
30
- const configured = account.configured === true;
31
-
32
- if (!configured) {
33
- issues.push({
34
- channel: "zalouser",
35
- accountId,
36
- kind: "auth",
37
- message: "Not authenticated (no saved Zalo session).",
38
- fix: "Run: klaw channels login --channel zalouser",
39
- });
40
- continue;
41
- }
42
-
43
- if (account.dmPolicy === "open") {
44
- issues.push({
45
- channel: "zalouser",
46
- accountId,
47
- kind: "config",
48
- message:
49
- 'Zalo Personal dmPolicy is "open", allowing any user to message the bot without pairing.',
50
- fix: 'Set channels.zalouser.dmPolicy to "pairing" or "allowlist" to restrict access.',
51
- });
52
- }
53
- }
54
- return issues;
55
- }