@kodelyth/zalo 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 (67) hide show
  1. package/klaw.plugin.json +509 -2
  2. package/package.json +19 -6
  3. package/api.ts +0 -8
  4. package/channel-plugin-api.ts +0 -1
  5. package/contract-api.ts +0 -5
  6. package/index.test.ts +0 -15
  7. package/index.ts +0 -20
  8. package/runtime-api.test.ts +0 -10
  9. package/runtime-api.ts +0 -71
  10. package/secret-contract-api.ts +0 -5
  11. package/setup-api.ts +0 -34
  12. package/setup-entry.ts +0 -13
  13. package/src/accounts.test.ts +0 -95
  14. package/src/accounts.ts +0 -65
  15. package/src/actions.runtime.ts +0 -5
  16. package/src/actions.test.ts +0 -32
  17. package/src/actions.ts +0 -62
  18. package/src/api.test.ts +0 -166
  19. package/src/api.ts +0 -265
  20. package/src/approval-auth.test.ts +0 -17
  21. package/src/approval-auth.ts +0 -25
  22. package/src/channel.directory.test.ts +0 -56
  23. package/src/channel.runtime.ts +0 -89
  24. package/src/channel.startup.test.ts +0 -121
  25. package/src/channel.ts +0 -309
  26. package/src/config-schema.test.ts +0 -30
  27. package/src/config-schema.ts +0 -29
  28. package/src/group-access.ts +0 -23
  29. package/src/monitor-durable.test.ts +0 -49
  30. package/src/monitor-durable.ts +0 -38
  31. package/src/monitor.group-policy.test.ts +0 -213
  32. package/src/monitor.image.polling.test.ts +0 -113
  33. package/src/monitor.lifecycle.test.ts +0 -194
  34. package/src/monitor.pairing.lifecycle.test.ts +0 -139
  35. package/src/monitor.polling.media-reply.test.ts +0 -433
  36. package/src/monitor.reply-once.lifecycle.test.ts +0 -178
  37. package/src/monitor.ts +0 -1009
  38. package/src/monitor.types.ts +0 -4
  39. package/src/monitor.webhook.test.ts +0 -808
  40. package/src/monitor.webhook.ts +0 -278
  41. package/src/outbound-media.test.ts +0 -186
  42. package/src/outbound-media.ts +0 -236
  43. package/src/outbound-payload.contract.test.ts +0 -143
  44. package/src/probe.ts +0 -45
  45. package/src/proxy.ts +0 -18
  46. package/src/runtime-api.ts +0 -71
  47. package/src/runtime-support.ts +0 -82
  48. package/src/runtime.ts +0 -9
  49. package/src/secret-contract.ts +0 -109
  50. package/src/secret-input.ts +0 -5
  51. package/src/send.test.ts +0 -150
  52. package/src/send.ts +0 -207
  53. package/src/session-route.ts +0 -32
  54. package/src/setup-allow-from.ts +0 -97
  55. package/src/setup-core.ts +0 -152
  56. package/src/setup-status.test.ts +0 -33
  57. package/src/setup-surface.test.ts +0 -193
  58. package/src/setup-surface.ts +0 -294
  59. package/src/status-issues.test.ts +0 -17
  60. package/src/status-issues.ts +0 -34
  61. package/src/test-support/lifecycle-test-support.ts +0 -456
  62. package/src/test-support/monitor-mocks-test-support.ts +0 -209
  63. package/src/token.test.ts +0 -92
  64. package/src/token.ts +0 -79
  65. package/src/types.ts +0 -50
  66. package/test-api.ts +0 -1
  67. package/tsconfig.json +0 -16
@@ -1,193 +0,0 @@
1
- import { adaptScopedAccountAccessor } from "klaw/plugin-sdk/channel-config-helpers";
2
- import {
3
- createPluginSetupWizardConfigure,
4
- createTestWizardPrompter,
5
- runSetupWizardConfigure,
6
- } from "klaw/plugin-sdk/plugin-test-runtime";
7
- import type { WizardPrompter } from "klaw/plugin-sdk/plugin-test-runtime";
8
- import { describe, expect, it, vi } from "vitest";
9
- import type { KlawConfig } from "../runtime-api.js";
10
- import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js";
11
- import { zaloDmPolicy } from "./setup-core.js";
12
- import { zaloSetupAdapter, zaloSetupWizard } from "./setup-surface.js";
13
-
14
- const zaloSetupPlugin = {
15
- id: "zalo",
16
- meta: {
17
- id: "zalo",
18
- label: "Zalo",
19
- selectionLabel: "Zalo (Bot API)",
20
- docsPath: "/channels/zalo",
21
- blurb: "Vietnam-focused messaging platform with Bot API.",
22
- },
23
- capabilities: {
24
- chatTypes: ["direct", "group"] as Array<"direct" | "group">,
25
- },
26
- config: {
27
- listAccountIds: (cfg: unknown) => listZaloAccountIds(cfg as never),
28
- defaultAccountId: (cfg: unknown) => resolveDefaultZaloAccountId(cfg as never),
29
- resolveAccount: adaptScopedAccountAccessor(resolveZaloAccount),
30
- },
31
- setup: zaloSetupAdapter,
32
- setupWizard: zaloSetupWizard,
33
- } as const;
34
-
35
- const zaloConfigure = createPluginSetupWizardConfigure(zaloSetupPlugin);
36
-
37
- describe("zalo setup wizard", () => {
38
- it("configures a polling token flow", async () => {
39
- const prompter = createTestWizardPrompter({
40
- select: vi.fn(async () => "plaintext") as WizardPrompter["select"],
41
- text: vi.fn(async ({ message }: { message: string }) => {
42
- if (message === "Enter Zalo bot token") {
43
- return "12345689:abc-xyz";
44
- }
45
- throw new Error(`Unexpected prompt: ${message}`);
46
- }) as WizardPrompter["text"],
47
- confirm: vi.fn(async ({ message }: { message: string }) => {
48
- if (message === "Use webhook mode for Zalo?") {
49
- return false;
50
- }
51
- return false;
52
- }),
53
- });
54
-
55
- const result = await runSetupWizardConfigure({
56
- configure: zaloConfigure,
57
- cfg: {} as KlawConfig,
58
- prompter,
59
- options: { secretInputMode: "plaintext" as const },
60
- });
61
-
62
- expect(result.accountId).toBe("default");
63
- const zaloConfig = result.cfg.channels?.zalo;
64
- if (!zaloConfig) {
65
- throw new Error("expected Zalo config");
66
- }
67
- expect(zaloConfig.enabled).toBe(true);
68
- expect(zaloConfig.botToken).toBe("12345689:abc-xyz");
69
- expect(zaloConfig.webhookUrl).toBeUndefined();
70
- });
71
-
72
- it("reads the named-account DM policy instead of the channel root", () => {
73
- expect(
74
- zaloDmPolicy.getCurrent(
75
- {
76
- channels: {
77
- zalo: {
78
- dmPolicy: "disabled",
79
- accounts: {
80
- work: {
81
- botToken: "12345689:abc-xyz",
82
- dmPolicy: "allowlist",
83
- },
84
- },
85
- },
86
- },
87
- } as KlawConfig,
88
- "work",
89
- ),
90
- ).toBe("allowlist");
91
- });
92
-
93
- it("reports account-scoped config keys for named accounts", () => {
94
- expect(zaloDmPolicy.resolveConfigKeys?.({} as KlawConfig, "work")).toEqual({
95
- policyKey: "channels.zalo.accounts.work.dmPolicy",
96
- allowFromKey: "channels.zalo.accounts.work.allowFrom",
97
- });
98
- });
99
-
100
- it("uses configured defaultAccount for omitted DM policy account context", () => {
101
- const cfg = {
102
- channels: {
103
- zalo: {
104
- defaultAccount: "work",
105
- dmPolicy: "disabled",
106
- allowFrom: ["123456789"],
107
- accounts: {
108
- work: {
109
- botToken: "12345689:abc-xyz",
110
- dmPolicy: "allowlist",
111
- },
112
- },
113
- },
114
- },
115
- } as KlawConfig;
116
-
117
- expect(zaloDmPolicy.getCurrent(cfg)).toBe("allowlist");
118
- expect(zaloDmPolicy.resolveConfigKeys?.(cfg)).toEqual({
119
- policyKey: "channels.zalo.accounts.work.dmPolicy",
120
- allowFromKey: "channels.zalo.accounts.work.allowFrom",
121
- });
122
-
123
- const next = zaloDmPolicy.setPolicy(cfg, "open");
124
- const zaloConfig = next.channels?.zalo;
125
- if (!zaloConfig) {
126
- throw new Error("expected Zalo config");
127
- }
128
- expect(zaloConfig.dmPolicy).toBe("disabled");
129
- const workAccount = next.channels?.zalo?.accounts?.work as
130
- | { dmPolicy?: string; allowFrom?: Array<string | number> }
131
- | undefined;
132
- if (!workAccount) {
133
- throw new Error("expected Zalo work account");
134
- }
135
- expect(workAccount.dmPolicy).toBe("open");
136
- });
137
-
138
- it('writes open policy state to the named account and preserves inherited allowFrom with "*"', () => {
139
- const next = zaloDmPolicy.setPolicy(
140
- {
141
- channels: {
142
- zalo: {
143
- allowFrom: ["123456789"],
144
- accounts: {
145
- work: {
146
- botToken: "12345689:abc-xyz",
147
- },
148
- },
149
- },
150
- },
151
- } as KlawConfig,
152
- "open",
153
- "work",
154
- );
155
-
156
- const zaloConfig = next.channels?.zalo;
157
- if (!zaloConfig) {
158
- throw new Error("expected Zalo config");
159
- }
160
- expect(zaloConfig.dmPolicy).toBeUndefined();
161
- const workAccount = next.channels?.zalo?.accounts?.work as
162
- | { dmPolicy?: string; allowFrom?: Array<string | number> }
163
- | undefined;
164
- if (!workAccount) {
165
- throw new Error("expected Zalo work account");
166
- }
167
- expect(workAccount.dmPolicy).toBe("open");
168
- expect(workAccount.allowFrom).toEqual(["123456789", "*"]);
169
- });
170
-
171
- it("uses configured defaultAccount for omitted setup configured state", async () => {
172
- const configured = await zaloSetupWizard.status.resolveConfigured({
173
- cfg: {
174
- channels: {
175
- zalo: {
176
- defaultAccount: "work",
177
- botToken: "root-token",
178
- accounts: {
179
- alerts: {
180
- botToken: "alerts-token",
181
- },
182
- work: {
183
- botToken: "",
184
- },
185
- },
186
- },
187
- },
188
- } as KlawConfig,
189
- });
190
-
191
- expect(configured).toBe(false);
192
- });
193
- });
@@ -1,294 +0,0 @@
1
- import {
2
- buildSingleChannelSecretPromptState,
3
- createStandardChannelSetupStatus,
4
- DEFAULT_ACCOUNT_ID,
5
- hasConfiguredSecretInput,
6
- promptSingleChannelSecretInput,
7
- runSingleChannelSecretStep,
8
- type ChannelSetupWizard,
9
- type KlawConfig,
10
- type SecretInput,
11
- createSetupTranslator,
12
- } from "klaw/plugin-sdk/setup";
13
- import { resolveZaloAccount } from "./accounts.js";
14
- import { noteZaloTokenHelp, promptZaloAllowFrom } from "./setup-allow-from.js";
15
- import { zaloDmPolicy } from "./setup-core.js";
16
-
17
- const t = createSetupTranslator();
18
-
19
- const channel = "zalo" as const;
20
-
21
- type UpdateMode = "polling" | "webhook";
22
-
23
- function setZaloUpdateMode(
24
- cfg: KlawConfig,
25
- accountId: string,
26
- mode: UpdateMode,
27
- webhookUrl?: string,
28
- webhookSecret?: SecretInput,
29
- webhookPath?: string,
30
- ): KlawConfig {
31
- const isDefault = accountId === DEFAULT_ACCOUNT_ID;
32
- if (mode === "polling") {
33
- if (isDefault) {
34
- const {
35
- webhookUrl: _url,
36
- webhookSecret: _secret,
37
- webhookPath: _path,
38
- ...rest
39
- } = cfg.channels?.zalo ?? {};
40
- return {
41
- ...cfg,
42
- channels: {
43
- ...cfg.channels,
44
- zalo: rest,
45
- },
46
- } as KlawConfig;
47
- }
48
- const accounts = { ...cfg.channels?.zalo?.accounts } as Record<string, Record<string, unknown>>;
49
- const existing = accounts[accountId] ?? {};
50
- const { webhookUrl: _url, webhookSecret: _secret, webhookPath: _path, ...rest } = existing;
51
- accounts[accountId] = rest;
52
- return {
53
- ...cfg,
54
- channels: {
55
- ...cfg.channels,
56
- zalo: {
57
- ...cfg.channels?.zalo,
58
- accounts,
59
- },
60
- },
61
- } as KlawConfig;
62
- }
63
-
64
- if (isDefault) {
65
- return {
66
- ...cfg,
67
- channels: {
68
- ...cfg.channels,
69
- zalo: {
70
- ...cfg.channels?.zalo,
71
- webhookUrl,
72
- webhookSecret,
73
- webhookPath,
74
- },
75
- },
76
- } as KlawConfig;
77
- }
78
-
79
- const accounts = { ...cfg.channels?.zalo?.accounts } as Record<string, Record<string, unknown>>;
80
- accounts[accountId] = {
81
- ...accounts[accountId],
82
- webhookUrl,
83
- webhookSecret,
84
- webhookPath,
85
- };
86
- return {
87
- ...cfg,
88
- channels: {
89
- ...cfg.channels,
90
- zalo: {
91
- ...cfg.channels?.zalo,
92
- accounts,
93
- },
94
- },
95
- } as KlawConfig;
96
- }
97
-
98
- export { zaloSetupAdapter } from "./setup-core.js";
99
-
100
- export const zaloSetupWizard: ChannelSetupWizard = {
101
- channel,
102
- status: createStandardChannelSetupStatus({
103
- channelLabel: "Zalo",
104
- configuredLabel: t("wizard.channels.statusConfigured"),
105
- unconfiguredLabel: t("wizard.channels.statusNeedsToken"),
106
- configuredHint: t("wizard.channels.statusRecommendedConfigured"),
107
- unconfiguredHint: t("wizard.channels.statusRecommendedNewcomerFriendly"),
108
- configuredScore: 1,
109
- unconfiguredScore: 10,
110
- includeStatusLine: true,
111
- resolveConfigured: ({ cfg, accountId }) => {
112
- const account = resolveZaloAccount({
113
- cfg,
114
- accountId,
115
- allowUnresolvedSecretRef: true,
116
- });
117
- return (
118
- Boolean(account.token) ||
119
- hasConfiguredSecretInput(account.config.botToken) ||
120
- Boolean(account.config.tokenFile?.trim())
121
- );
122
- },
123
- }),
124
- credentials: [],
125
- finalize: async ({ cfg, accountId, forceAllowFrom, options, prompter }) => {
126
- let next = cfg;
127
- const resolvedAccount = resolveZaloAccount({
128
- cfg: next,
129
- accountId,
130
- allowUnresolvedSecretRef: true,
131
- });
132
- const accountConfigured = Boolean(resolvedAccount.token);
133
- const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
134
- const hasConfigToken = Boolean(
135
- hasConfiguredSecretInput(resolvedAccount.config.botToken) || resolvedAccount.config.tokenFile,
136
- );
137
- const tokenStep = await runSingleChannelSecretStep({
138
- cfg: next,
139
- prompter,
140
- providerHint: "zalo",
141
- credentialLabel: t("wizard.zalo.botToken"),
142
- secretInputMode: options?.secretInputMode,
143
- accountConfigured,
144
- hasConfigToken,
145
- allowEnv,
146
- envValue: process.env.ZALO_BOT_TOKEN,
147
- envPrompt: t("wizard.zalo.tokenEnvPrompt"),
148
- keepPrompt: t("wizard.zalo.tokenKeep"),
149
- inputPrompt: t("wizard.zalo.tokenInput"),
150
- preferredEnvVar: "ZALO_BOT_TOKEN",
151
- onMissingConfigured: async () => await noteZaloTokenHelp(prompter),
152
- applyUseEnv: async (currentCfg) =>
153
- accountId === DEFAULT_ACCOUNT_ID
154
- ? ({
155
- ...currentCfg,
156
- channels: {
157
- ...currentCfg.channels,
158
- zalo: {
159
- ...currentCfg.channels?.zalo,
160
- enabled: true,
161
- },
162
- },
163
- } as KlawConfig)
164
- : currentCfg,
165
- applySet: async (currentCfg, value) =>
166
- accountId === DEFAULT_ACCOUNT_ID
167
- ? ({
168
- ...currentCfg,
169
- channels: {
170
- ...currentCfg.channels,
171
- zalo: {
172
- ...currentCfg.channels?.zalo,
173
- enabled: true,
174
- botToken: value,
175
- },
176
- },
177
- } as KlawConfig)
178
- : ({
179
- ...currentCfg,
180
- channels: {
181
- ...currentCfg.channels,
182
- zalo: {
183
- ...currentCfg.channels?.zalo,
184
- enabled: true,
185
- accounts: {
186
- ...currentCfg.channels?.zalo?.accounts,
187
- [accountId]: {
188
- ...(currentCfg.channels?.zalo?.accounts?.[accountId] as
189
- | Record<string, unknown>
190
- | undefined),
191
- enabled: true,
192
- botToken: value,
193
- },
194
- },
195
- },
196
- },
197
- } as KlawConfig),
198
- });
199
- next = tokenStep.cfg;
200
-
201
- const wantsWebhook = await prompter.confirm({
202
- message: t("wizard.zalo.webhookModePrompt"),
203
- initialValue: Boolean(resolvedAccount.config.webhookUrl),
204
- });
205
- if (wantsWebhook) {
206
- const webhookUrl = (
207
- await prompter.text({
208
- message: t("wizard.zalo.webhookUrlPrompt"),
209
- initialValue: resolvedAccount.config.webhookUrl,
210
- validate: (value) =>
211
- value?.trim()?.startsWith("https://") ? undefined : "HTTPS URL required",
212
- })
213
- ).trim();
214
- const defaultPath = (() => {
215
- try {
216
- return new URL(webhookUrl).pathname || "/zalo-webhook";
217
- } catch {
218
- return "/zalo-webhook";
219
- }
220
- })();
221
-
222
- let webhookSecretResult = await promptSingleChannelSecretInput({
223
- cfg: next,
224
- prompter,
225
- providerHint: "zalo-webhook",
226
- credentialLabel: t("wizard.zalo.webhookSecret"),
227
- secretInputMode: options?.secretInputMode,
228
- ...buildSingleChannelSecretPromptState({
229
- accountConfigured: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
230
- hasConfigToken: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
231
- allowEnv: false,
232
- }),
233
- envPrompt: "",
234
- keepPrompt: t("wizard.zalo.webhookSecretKeep"),
235
- inputPrompt: t("wizard.zalo.webhookSecretInput"),
236
- preferredEnvVar: "ZALO_WEBHOOK_SECRET",
237
- });
238
- while (
239
- webhookSecretResult.action === "set" &&
240
- typeof webhookSecretResult.value === "string" &&
241
- (webhookSecretResult.value.length < 8 || webhookSecretResult.value.length > 256)
242
- ) {
243
- await prompter.note(t("wizard.zalo.webhookSecretLength"), t("wizard.zalo.webhookTitle"));
244
- webhookSecretResult = await promptSingleChannelSecretInput({
245
- cfg: next,
246
- prompter,
247
- providerHint: "zalo-webhook",
248
- credentialLabel: t("wizard.zalo.webhookSecret"),
249
- secretInputMode: options?.secretInputMode,
250
- ...buildSingleChannelSecretPromptState({
251
- accountConfigured: false,
252
- hasConfigToken: false,
253
- allowEnv: false,
254
- }),
255
- envPrompt: "",
256
- keepPrompt: t("wizard.zalo.webhookSecretKeep"),
257
- inputPrompt: t("wizard.zalo.webhookSecretInput"),
258
- preferredEnvVar: "ZALO_WEBHOOK_SECRET",
259
- });
260
- }
261
- const webhookSecret =
262
- webhookSecretResult.action === "set"
263
- ? webhookSecretResult.value
264
- : resolvedAccount.config.webhookSecret;
265
- const webhookPath = (
266
- await prompter.text({
267
- message: t("wizard.zalo.webhookPathPrompt"),
268
- initialValue: resolvedAccount.config.webhookPath ?? defaultPath,
269
- })
270
- ).trim();
271
- next = setZaloUpdateMode(
272
- next,
273
- accountId,
274
- "webhook",
275
- webhookUrl,
276
- webhookSecret,
277
- webhookPath || undefined,
278
- );
279
- } else {
280
- next = setZaloUpdateMode(next, accountId, "polling");
281
- }
282
-
283
- if (forceAllowFrom) {
284
- next = await promptZaloAllowFrom({
285
- cfg: next,
286
- prompter,
287
- accountId,
288
- });
289
- }
290
-
291
- return { cfg: next };
292
- },
293
- dmPolicy: zaloDmPolicy,
294
- };
@@ -1,17 +0,0 @@
1
- import { expectOpenDmPolicyConfigIssue } from "klaw/plugin-sdk/channel-test-helpers";
2
- import { describe, it } from "vitest";
3
- import { collectZaloStatusIssues } from "./status-issues.js";
4
-
5
- describe("collectZaloStatusIssues", () => {
6
- it("warns when dmPolicy is open", () => {
7
- expectOpenDmPolicyConfigIssue({
8
- collectIssues: collectZaloStatusIssues,
9
- account: {
10
- accountId: "default",
11
- enabled: true,
12
- configured: true,
13
- dmPolicy: "open",
14
- },
15
- });
16
- });
17
- });
@@ -1,34 +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 ZALO_STATUS_FIELDS = ["accountId", "enabled", "configured", "dmPolicy"] as const;
8
-
9
- export function collectZaloStatusIssues(accounts: ChannelAccountSnapshot[]): ChannelStatusIssue[] {
10
- const issues: ChannelStatusIssue[] = [];
11
- for (const entry of accounts) {
12
- const account = readStatusIssueFields(entry, ZALO_STATUS_FIELDS);
13
- if (!account) {
14
- continue;
15
- }
16
- const accountId = coerceStatusIssueAccountId(account.accountId) ?? "default";
17
- const enabled = account.enabled !== false;
18
- const configured = account.configured === true;
19
- if (!enabled || !configured) {
20
- continue;
21
- }
22
-
23
- if (account.dmPolicy === "open") {
24
- issues.push({
25
- channel: "zalo",
26
- accountId,
27
- kind: "config",
28
- message: 'Zalo dmPolicy is "open", allowing any user to message the bot without pairing.',
29
- fix: 'Set channels.zalo.dmPolicy to "pairing" or "allowlist" to restrict access.',
30
- });
31
- }
32
- }
33
- return issues;
34
- }