@kodelyth/zalo 2026.5.42 → 2026.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/klaw.plugin.json +509 -2
  2. package/package.json +17 -4
  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,113 +0,0 @@
1
- import { createRuntimeEnv } from "klaw/plugin-sdk/plugin-test-runtime";
2
- import { afterAll, beforeEach, describe, expect, it } from "vitest";
3
- import {
4
- createImageLifecycleCore,
5
- createImageUpdate,
6
- createLifecycleMonitorSetup,
7
- expectImageLifecycleDelivery,
8
- settleAsyncWork,
9
- } from "./test-support/lifecycle-test-support.js";
10
- import {
11
- getUpdatesMock,
12
- getZaloRuntimeMock,
13
- loadCachedLifecycleMonitorModule,
14
- resetLifecycleTestState,
15
- sendMessageMock,
16
- } from "./test-support/monitor-mocks-test-support.js";
17
-
18
- describe("Zalo polling image handling", () => {
19
- const {
20
- core,
21
- finalizeInboundContextMock,
22
- recordInboundSessionMock,
23
- readRemoteMediaBufferMock,
24
- saveRemoteMediaMock,
25
- saveMediaBufferMock,
26
- } = createImageLifecycleCore();
27
-
28
- beforeEach(async () => {
29
- await resetLifecycleTestState();
30
- getZaloRuntimeMock.mockReturnValue(core);
31
- });
32
-
33
- afterAll(async () => {
34
- await resetLifecycleTestState();
35
- });
36
-
37
- it("downloads inbound image media from photo_url and preserves display_name", async () => {
38
- getUpdatesMock
39
- .mockResolvedValueOnce({
40
- ok: true,
41
- result: createImageUpdate({ date: 1774084566880 }),
42
- })
43
- .mockImplementation(() => new Promise(() => {}));
44
-
45
- const { monitorZaloProvider } = await loadCachedLifecycleMonitorModule("zalo-image-polling");
46
- const abort = new AbortController();
47
- const runtime = createRuntimeEnv();
48
- const { account, config } = createLifecycleMonitorSetup({
49
- accountId: "default",
50
- dmPolicy: "open",
51
- });
52
- const run = monitorZaloProvider({
53
- token: "zalo-token", // pragma: allowlist secret
54
- account,
55
- config,
56
- runtime,
57
- abortSignal: abort.signal,
58
- });
59
-
60
- await settleAsyncWork();
61
- expect(saveRemoteMediaMock).toHaveBeenCalledTimes(1);
62
- expect(readRemoteMediaBufferMock).not.toHaveBeenCalled();
63
- expectImageLifecycleDelivery({
64
- readRemoteMediaBufferMock,
65
- saveRemoteMediaMock,
66
- saveMediaBufferMock,
67
- finalizeInboundContextMock,
68
- recordInboundSessionMock,
69
- });
70
-
71
- abort.abort();
72
- await run;
73
- });
74
-
75
- it("rejects unauthorized DM images before downloading media", async () => {
76
- getUpdatesMock
77
- .mockResolvedValueOnce({
78
- ok: true,
79
- result: createImageUpdate({
80
- messageId: "msg-unauthorized-1",
81
- userId: "user-unauthorized-1",
82
- chatId: "chat-unauthorized-1",
83
- }),
84
- })
85
- .mockImplementation(() => new Promise(() => {}));
86
-
87
- const { monitorZaloProvider } = await loadCachedLifecycleMonitorModule("zalo-image-polling");
88
- const abort = new AbortController();
89
- const runtime = createRuntimeEnv();
90
- const { account, config } = createLifecycleMonitorSetup({
91
- accountId: "default",
92
- dmPolicy: "pairing",
93
- allowFrom: ["allowed-user"],
94
- });
95
- const run = monitorZaloProvider({
96
- token: "zalo-token", // pragma: allowlist secret
97
- account,
98
- config,
99
- runtime,
100
- abortSignal: abort.signal,
101
- });
102
-
103
- await settleAsyncWork();
104
- expect(sendMessageMock).toHaveBeenCalledTimes(1);
105
- expect(readRemoteMediaBufferMock).not.toHaveBeenCalled();
106
- expect(saveMediaBufferMock).not.toHaveBeenCalled();
107
- expect(finalizeInboundContextMock).not.toHaveBeenCalled();
108
- expect(recordInboundSessionMock).not.toHaveBeenCalled();
109
-
110
- abort.abort();
111
- await run;
112
- });
113
- });
@@ -1,194 +0,0 @@
1
- import {
2
- createEmptyPluginRegistry,
3
- createRuntimeEnv,
4
- setActivePluginRegistry,
5
- } from "klaw/plugin-sdk/plugin-test-runtime";
6
- import { afterEach, describe, expect, it, vi } from "vitest";
7
- import type { KlawConfig } from "../runtime-api.js";
8
- import type { ResolvedZaloAccount } from "./accounts.js";
9
-
10
- const getWebhookInfoMock = vi.fn(async () => ({ ok: true, result: { url: "" } }));
11
- const deleteWebhookMock = vi.fn(async () => ({ ok: true, result: { url: "" } }));
12
- const getUpdatesMock = vi.fn(() => new Promise(() => {}));
13
- const setWebhookMock = vi.fn(async () => ({ ok: true, result: { url: "" } }));
14
-
15
- vi.mock("./api.js", async () => {
16
- const actual = await vi.importActual<typeof import("./api.js")>("./api.js");
17
- return {
18
- ...actual,
19
- deleteWebhook: deleteWebhookMock,
20
- getWebhookInfo: getWebhookInfoMock,
21
- getUpdates: getUpdatesMock,
22
- setWebhook: setWebhookMock,
23
- };
24
- });
25
-
26
- vi.mock("./runtime.js", () => ({
27
- getZaloRuntime: () => ({
28
- logging: {
29
- shouldLogVerbose: () => false,
30
- },
31
- }),
32
- }));
33
-
34
- const TEST_ACCOUNT = {
35
- accountId: "default",
36
- config: {},
37
- } as unknown as ResolvedZaloAccount;
38
-
39
- const TEST_CONFIG = {} as KlawConfig;
40
-
41
- async function settleLifecycleWork(): Promise<void> {
42
- for (let i = 0; i < 6; i += 1) {
43
- await Promise.resolve();
44
- await new Promise((resolve) => setImmediate(resolve));
45
- }
46
- }
47
-
48
- async function startLifecycleMonitor(
49
- options: {
50
- useWebhook?: boolean;
51
- webhookSecret?: string;
52
- webhookUrl?: string;
53
- } = {},
54
- ) {
55
- const { monitorZaloProvider } = await import("./monitor.js");
56
- const abort = new AbortController();
57
- const runtime = createRuntimeEnv();
58
- const run = monitorZaloProvider({
59
- token: "test-token",
60
- account: TEST_ACCOUNT,
61
- config: TEST_CONFIG,
62
- runtime,
63
- abortSignal: abort.signal,
64
- ...options,
65
- });
66
- return { abort, runtime, run };
67
- }
68
-
69
- describe("monitorZaloProvider lifecycle", () => {
70
- afterEach(() => {
71
- vi.clearAllMocks();
72
- setActivePluginRegistry(createEmptyPluginRegistry());
73
- });
74
-
75
- it("stays alive in polling mode until abort", async () => {
76
- let settled = false;
77
- const { abort, runtime, run } = await startLifecycleMonitor();
78
- const monitoredRun = run.then(() => {
79
- settled = true;
80
- });
81
-
82
- await settleLifecycleWork();
83
- expect(getUpdatesMock).toHaveBeenCalledTimes(1);
84
-
85
- expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
86
- expect(deleteWebhookMock).not.toHaveBeenCalled();
87
- expect(getUpdatesMock).toHaveBeenCalledTimes(1);
88
- expect(settled).toBe(false);
89
-
90
- abort.abort();
91
- await monitoredRun;
92
-
93
- expect(settled).toBe(true);
94
- expect(runtime.log).toHaveBeenCalledWith("[default] Zalo provider stopped mode=polling");
95
- });
96
-
97
- it("deletes an existing webhook before polling", async () => {
98
- getWebhookInfoMock.mockResolvedValueOnce({
99
- ok: true,
100
- result: { url: "https://example.com/hooks/zalo" },
101
- });
102
-
103
- const { abort, runtime, run } = await startLifecycleMonitor();
104
-
105
- await settleLifecycleWork();
106
- expect(getUpdatesMock).toHaveBeenCalledTimes(1);
107
-
108
- expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
109
- expect(deleteWebhookMock).toHaveBeenCalledTimes(1);
110
- expect(runtime.log).toHaveBeenCalledWith(
111
- "[default] Zalo polling mode ready (webhook disabled)",
112
- );
113
-
114
- abort.abort();
115
- await run;
116
- });
117
-
118
- it("continues polling when webhook inspection returns 404", async () => {
119
- const { ZaloApiError } = await import("./api.js");
120
- getWebhookInfoMock.mockRejectedValueOnce(new ZaloApiError("Not Found", 404, "Not Found"));
121
-
122
- const { abort, runtime, run } = await startLifecycleMonitor();
123
-
124
- await settleLifecycleWork();
125
- expect(getUpdatesMock).toHaveBeenCalledTimes(1);
126
-
127
- expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
128
- expect(deleteWebhookMock).not.toHaveBeenCalled();
129
- expect(runtime.log).toHaveBeenCalledWith(
130
- "[default] Zalo polling mode webhook inspection unavailable; continuing without webhook cleanup",
131
- );
132
- expect(runtime.error).not.toHaveBeenCalled();
133
-
134
- abort.abort();
135
- await run;
136
- });
137
-
138
- it("waits for webhook deletion before finishing webhook shutdown", async () => {
139
- const registry = createEmptyPluginRegistry();
140
- setActivePluginRegistry(registry);
141
-
142
- let resolveSetWebhookCalled: (() => void) | undefined;
143
- const setWebhookCalled = new Promise<void>((resolve) => {
144
- resolveSetWebhookCalled = resolve;
145
- });
146
- setWebhookMock.mockImplementationOnce(async () => {
147
- resolveSetWebhookCalled?.();
148
- return { ok: true, result: { url: "" } };
149
- });
150
-
151
- let resolveDeleteWebhookCalled: (() => void) | undefined;
152
- const deleteWebhookCalled = new Promise<void>((resolve) => {
153
- resolveDeleteWebhookCalled = resolve;
154
- });
155
- let resolveDeleteWebhook: (() => void) | undefined;
156
- deleteWebhookMock.mockImplementationOnce(
157
- () =>
158
- new Promise((resolve) => {
159
- resolveDeleteWebhookCalled?.();
160
- resolveDeleteWebhook = () => resolve({ ok: true, result: { url: "" } });
161
- }),
162
- );
163
-
164
- let settled = false;
165
- const { abort, runtime, run } = await startLifecycleMonitor({
166
- useWebhook: true,
167
- webhookUrl: "https://example.com/hooks/zalo",
168
- webhookSecret: "supersecret", // pragma: allowlist secret
169
- });
170
- const monitoredRun = run.then(() => {
171
- settled = true;
172
- });
173
-
174
- await setWebhookCalled;
175
- await settleLifecycleWork();
176
- expect(setWebhookMock).toHaveBeenCalledTimes(1);
177
- expect(registry.httpRoutes).toHaveLength(2);
178
-
179
- abort.abort();
180
-
181
- await deleteWebhookCalled;
182
- expect(deleteWebhookMock).toHaveBeenCalledTimes(1);
183
- expect(deleteWebhookMock).toHaveBeenCalledWith("test-token", undefined, 5000);
184
- expect(settled).toBe(false);
185
- expect(registry.httpRoutes).toHaveLength(2);
186
-
187
- resolveDeleteWebhook?.();
188
- await monitoredRun;
189
-
190
- expect(settled).toBe(true);
191
- expect(registry.httpRoutes).toHaveLength(0);
192
- expect(runtime.log).toHaveBeenCalledWith("[default] Zalo provider stopped mode=webhook");
193
- });
194
- });
@@ -1,139 +0,0 @@
1
- import { withServer } from "klaw/plugin-sdk/test-env";
2
- import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
3
- import {
4
- createLifecycleMonitorSetup,
5
- createTextUpdate,
6
- postWebhookReplay,
7
- settleAsyncWork,
8
- } from "./test-support/lifecycle-test-support.js";
9
- import {
10
- resetLifecycleTestState,
11
- sendMessageMock,
12
- setLifecycleRuntimeCore,
13
- startWebhookLifecycleMonitor,
14
- } from "./test-support/monitor-mocks-test-support.js";
15
-
16
- describe("Zalo pairing lifecycle", () => {
17
- const readAllowFromStoreMock = vi.fn(async () => [] as string[]);
18
- const upsertPairingRequestMock = vi.fn(async () => ({ code: "PAIRCODE", created: true }));
19
-
20
- beforeEach(async () => {
21
- await resetLifecycleTestState();
22
- setLifecycleRuntimeCore({
23
- pairing: {
24
- readAllowFromStore: readAllowFromStoreMock,
25
- upsertPairingRequest: upsertPairingRequestMock,
26
- },
27
- commands: {
28
- shouldComputeCommandAuthorized: vi.fn(() => false),
29
- resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false),
30
- },
31
- });
32
- });
33
-
34
- afterAll(async () => {
35
- await resetLifecycleTestState();
36
- });
37
-
38
- function createPairingMonitorSetup() {
39
- return createLifecycleMonitorSetup({
40
- accountId: "acct-zalo-pairing",
41
- dmPolicy: "pairing",
42
- allowFrom: [],
43
- });
44
- }
45
-
46
- it("emits one pairing reply across duplicate webhook replay and scopes reads and writes to accountId", async () => {
47
- const monitor = await startWebhookLifecycleMonitor({
48
- ...createPairingMonitorSetup(),
49
- cacheKey: "zalo-pairing-lifecycle",
50
- });
51
-
52
- try {
53
- await withServer(
54
- (req, res) => monitor.route.handler(req, res),
55
- async (baseUrl) => {
56
- const { first, replay } = await postWebhookReplay({
57
- baseUrl,
58
- path: "/hooks/zalo",
59
- secret: "supersecret",
60
- payload: createTextUpdate({
61
- messageId: `zalo-pairing-${Date.now()}`,
62
- userId: "user-unauthorized",
63
- userName: "Unauthorized User",
64
- chatId: "dm-pairing-1",
65
- }),
66
- });
67
-
68
- expect(first.status).toBe(200);
69
- expect(replay.status).toBe(200);
70
- await settleAsyncWork();
71
- },
72
- );
73
-
74
- expect(readAllowFromStoreMock).toHaveBeenCalledTimes(1);
75
- expect(readAllowFromStoreMock).toHaveBeenCalledWith({
76
- channel: "zalo",
77
- accountId: "acct-zalo-pairing",
78
- });
79
- expect(upsertPairingRequestMock).toHaveBeenCalledTimes(1);
80
- expect(upsertPairingRequestMock).toHaveBeenCalledWith({
81
- channel: "zalo",
82
- accountId: "acct-zalo-pairing",
83
- id: "user-unauthorized",
84
- meta: { name: "Unauthorized User" },
85
- });
86
- expect(sendMessageMock).toHaveBeenCalledTimes(1);
87
- const [sendToken, sendPayload, sendOptions] = sendMessageMock.mock.calls[0] as [
88
- string,
89
- { chat_id?: string; text?: string },
90
- unknown,
91
- ];
92
- expect(sendToken).toBe("zalo-token");
93
- expect(sendPayload.chat_id).toBe("dm-pairing-1");
94
- expect(sendPayload.text).toContain("PAIRCODE");
95
- expect(sendOptions).toBeUndefined();
96
- } finally {
97
- await monitor.stop();
98
- }
99
- });
100
-
101
- it("does not emit a second pairing reply when replay arrives after the first send fails", async () => {
102
- sendMessageMock.mockRejectedValueOnce(new Error("pairing send failed"));
103
-
104
- const monitor = await startWebhookLifecycleMonitor({
105
- ...createPairingMonitorSetup(),
106
- cacheKey: "zalo-pairing-lifecycle",
107
- });
108
-
109
- try {
110
- await withServer(
111
- (req, res) => monitor.route.handler(req, res),
112
- async (baseUrl) => {
113
- const { first, replay } = await postWebhookReplay({
114
- baseUrl,
115
- path: "/hooks/zalo",
116
- secret: "supersecret",
117
- payload: createTextUpdate({
118
- messageId: `zalo-pairing-retry-${Date.now()}`,
119
- userId: "user-unauthorized",
120
- userName: "Unauthorized User",
121
- chatId: "dm-pairing-1",
122
- }),
123
- settleBeforeReplay: true,
124
- });
125
-
126
- expect(first.status).toBe(200);
127
- expect(replay.status).toBe(200);
128
- await settleAsyncWork();
129
- },
130
- );
131
-
132
- expect(upsertPairingRequestMock).toHaveBeenCalledTimes(1);
133
- expect(sendMessageMock).toHaveBeenCalledTimes(1);
134
- expect(monitor.runtime.error).not.toHaveBeenCalled();
135
- } finally {
136
- await monitor.stop();
137
- }
138
- });
139
- });