@kodelyth/zalo 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 (97) hide show
  1. package/README.md +50 -0
  2. package/api.ts +8 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/contract-api.ts +5 -0
  5. package/dist/actions.runtime-C61oPfyd.js +5 -0
  6. package/dist/api.js +5 -0
  7. package/dist/channel-D8ylaEdN.js +367 -0
  8. package/dist/channel-plugin-api.js +2 -0
  9. package/dist/channel.runtime-sf-rx5n-.js +105 -0
  10. package/dist/contract-api.js +3 -0
  11. package/dist/group-access-DTQVR6Nd.js +15 -0
  12. package/dist/index.js +22 -0
  13. package/dist/monitor-CQ1bjGih.js +825 -0
  14. package/dist/monitor.webhook-CDxUxa9l.js +175 -0
  15. package/dist/runtime-api-CxXTp1Q2.js +23 -0
  16. package/dist/runtime-api.js +2 -0
  17. package/dist/secret-contract-CRFukr2n.js +87 -0
  18. package/dist/secret-contract-api.js +2 -0
  19. package/dist/send-CGAqdfSA.js +270 -0
  20. package/dist/setup-api.js +30 -0
  21. package/dist/setup-core-Dr75wK6l.js +287 -0
  22. package/dist/setup-entry.js +15 -0
  23. package/dist/setup-surface-C8zxrnzG.js +216 -0
  24. package/dist/test-api.js +2 -0
  25. package/index.test.ts +15 -0
  26. package/index.ts +20 -0
  27. package/klaw.plugin.json +2 -509
  28. package/package.json +4 -4
  29. package/runtime-api.test.ts +10 -0
  30. package/runtime-api.ts +71 -0
  31. package/secret-contract-api.ts +5 -0
  32. package/setup-api.ts +34 -0
  33. package/setup-entry.ts +13 -0
  34. package/src/accounts.test.ts +95 -0
  35. package/src/accounts.ts +65 -0
  36. package/src/actions.runtime.ts +5 -0
  37. package/src/actions.test.ts +32 -0
  38. package/src/actions.ts +62 -0
  39. package/src/api.test.ts +166 -0
  40. package/src/api.ts +265 -0
  41. package/src/approval-auth.test.ts +17 -0
  42. package/src/approval-auth.ts +25 -0
  43. package/src/channel.directory.test.ts +56 -0
  44. package/src/channel.runtime.ts +89 -0
  45. package/src/channel.startup.test.ts +121 -0
  46. package/src/channel.ts +309 -0
  47. package/src/config-schema.test.ts +30 -0
  48. package/src/config-schema.ts +29 -0
  49. package/src/group-access.ts +23 -0
  50. package/src/monitor-durable.test.ts +49 -0
  51. package/src/monitor-durable.ts +38 -0
  52. package/src/monitor.group-policy.test.ts +213 -0
  53. package/src/monitor.image.polling.test.ts +113 -0
  54. package/src/monitor.lifecycle.test.ts +194 -0
  55. package/src/monitor.pairing.lifecycle.test.ts +139 -0
  56. package/src/monitor.polling.media-reply.test.ts +433 -0
  57. package/src/monitor.reply-once.lifecycle.test.ts +178 -0
  58. package/src/monitor.ts +1009 -0
  59. package/src/monitor.types.ts +4 -0
  60. package/src/monitor.webhook.test.ts +808 -0
  61. package/src/monitor.webhook.ts +278 -0
  62. package/src/outbound-media.test.ts +186 -0
  63. package/src/outbound-media.ts +236 -0
  64. package/src/outbound-payload.contract.test.ts +143 -0
  65. package/src/probe.ts +45 -0
  66. package/src/proxy.ts +18 -0
  67. package/src/runtime-api.ts +71 -0
  68. package/src/runtime-support.ts +82 -0
  69. package/src/runtime.ts +9 -0
  70. package/src/secret-contract.ts +109 -0
  71. package/src/secret-input.ts +5 -0
  72. package/src/send.test.ts +150 -0
  73. package/src/send.ts +207 -0
  74. package/src/session-route.ts +32 -0
  75. package/src/setup-allow-from.ts +97 -0
  76. package/src/setup-core.ts +152 -0
  77. package/src/setup-status.test.ts +33 -0
  78. package/src/setup-surface.test.ts +193 -0
  79. package/src/setup-surface.ts +294 -0
  80. package/src/status-issues.test.ts +17 -0
  81. package/src/status-issues.ts +34 -0
  82. package/src/test-support/lifecycle-test-support.ts +456 -0
  83. package/src/test-support/monitor-mocks-test-support.ts +209 -0
  84. package/src/token.test.ts +92 -0
  85. package/src/token.ts +79 -0
  86. package/src/types.ts +50 -0
  87. package/test-api.ts +1 -0
  88. package/tsconfig.json +16 -0
  89. package/api.js +0 -7
  90. package/channel-plugin-api.js +0 -7
  91. package/contract-api.js +0 -7
  92. package/index.js +0 -7
  93. package/runtime-api.js +0 -7
  94. package/secret-contract-api.js +0 -7
  95. package/setup-api.js +0 -7
  96. package/setup-entry.js +0 -7
  97. package/test-api.js +0 -7
@@ -0,0 +1,178 @@
1
+ import { withServer } from "klaw/plugin-sdk/test-env";
2
+ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type { PluginRuntime } from "../runtime-api.js";
4
+ import {
5
+ createLifecycleMonitorSetup,
6
+ createTextUpdate,
7
+ postWebhookReplay,
8
+ settleAsyncWork,
9
+ } from "./test-support/lifecycle-test-support.js";
10
+ import {
11
+ resetLifecycleTestState,
12
+ sendMessageMock,
13
+ setLifecycleRuntimeCore,
14
+ startWebhookLifecycleMonitor,
15
+ } from "./test-support/monitor-mocks-test-support.js";
16
+
17
+ describe("Zalo reply-once lifecycle", () => {
18
+ const finalizeInboundContextMock = vi.fn((ctx: Record<string, unknown>) => ctx);
19
+ const recordInboundSessionMock = vi.fn(
20
+ async (_input: { sessionKey?: string; ctx?: Record<string, unknown> }) => undefined,
21
+ );
22
+ const resolveAgentRouteMock = vi.fn(() => ({
23
+ agentId: "main",
24
+ channel: "zalo",
25
+ accountId: "acct-zalo-lifecycle",
26
+ sessionKey: "agent:main:zalo:direct:dm-chat-1",
27
+ mainSessionKey: "agent:main:main",
28
+ matchedBy: "default",
29
+ }));
30
+ const dispatchReplyWithBufferedBlockDispatcherMock = vi.fn();
31
+
32
+ beforeEach(async () => {
33
+ await resetLifecycleTestState();
34
+ setLifecycleRuntimeCore({
35
+ routing: {
36
+ resolveAgentRoute:
37
+ resolveAgentRouteMock as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
38
+ },
39
+ reply: {
40
+ finalizeInboundContext:
41
+ finalizeInboundContextMock as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
42
+ dispatchReplyWithBufferedBlockDispatcher:
43
+ dispatchReplyWithBufferedBlockDispatcherMock as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
44
+ },
45
+ session: {
46
+ recordInboundSession:
47
+ recordInboundSessionMock as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
48
+ },
49
+ });
50
+ });
51
+
52
+ afterAll(async () => {
53
+ await resetLifecycleTestState();
54
+ });
55
+
56
+ function createReplyOnceMonitorSetup() {
57
+ return createLifecycleMonitorSetup({
58
+ accountId: "acct-zalo-lifecycle",
59
+ dmPolicy: "open",
60
+ });
61
+ }
62
+
63
+ function requireRecordInboundSessionArgs() {
64
+ const [call] = recordInboundSessionMock.mock.calls;
65
+ if (!call) {
66
+ throw new Error("expected inbound session record call");
67
+ }
68
+ const [recordArgs] = call;
69
+ return recordArgs;
70
+ }
71
+
72
+ it("routes one accepted webhook event to one visible reply across duplicate replay", async () => {
73
+ dispatchReplyWithBufferedBlockDispatcherMock.mockImplementation(
74
+ async ({ dispatcherOptions }) => {
75
+ await dispatcherOptions.deliver({ text: "zalo reply once" });
76
+ },
77
+ );
78
+
79
+ const monitor = await startWebhookLifecycleMonitor({
80
+ ...createReplyOnceMonitorSetup(),
81
+ cacheKey: "zalo-reply-once-lifecycle",
82
+ });
83
+
84
+ try {
85
+ await withServer(
86
+ (req, res) => monitor.route.handler(req, res),
87
+ async (baseUrl) => {
88
+ const { first, replay } = await postWebhookReplay({
89
+ baseUrl,
90
+ path: "/hooks/zalo",
91
+ secret: "supersecret",
92
+ payload: createTextUpdate({
93
+ messageId: `zalo-replay-${Date.now()}`,
94
+ userId: "user-1",
95
+ userName: "User One",
96
+ chatId: "dm-chat-1",
97
+ }),
98
+ });
99
+
100
+ expect(first.status).toBe(200);
101
+ expect(replay.status).toBe(200);
102
+ await settleAsyncWork();
103
+ },
104
+ );
105
+
106
+ expect(recordInboundSessionMock).toHaveBeenCalledTimes(1);
107
+ const recordArgs = requireRecordInboundSessionArgs();
108
+ expect(recordArgs?.sessionKey).toBe("agent:main:zalo:direct:dm-chat-1");
109
+ expect(recordArgs?.ctx?.AccountId).toBe("acct-zalo-lifecycle");
110
+ expect(recordArgs?.ctx?.SessionKey).toBe("agent:main:zalo:direct:dm-chat-1");
111
+ expect(recordArgs?.ctx?.From).toBe("zalo:user-1");
112
+ expect(recordArgs?.ctx?.To).toBe("zalo:dm-chat-1");
113
+ expect(recordArgs?.ctx?.MessageSid).toContain("zalo-replay-");
114
+ expect(sendMessageMock).toHaveBeenCalledTimes(1);
115
+ const [sendToken, sendPayload, sendOptions] = sendMessageMock.mock.calls[0] as [
116
+ string,
117
+ { chat_id?: string; text?: string },
118
+ unknown,
119
+ ];
120
+ expect(sendToken).toBe("zalo-token");
121
+ expect(sendPayload.chat_id).toBe("dm-chat-1");
122
+ expect(sendPayload.text).toBe("zalo reply once");
123
+ expect(sendOptions).toBeUndefined();
124
+ } finally {
125
+ await monitor.stop();
126
+ }
127
+ });
128
+
129
+ it("does not emit a second visible reply when replay arrives after a post-send failure", async () => {
130
+ let dispatchAttempts = 0;
131
+ dispatchReplyWithBufferedBlockDispatcherMock.mockImplementation(
132
+ async ({ dispatcherOptions }) => {
133
+ dispatchAttempts += 1;
134
+ await dispatcherOptions.deliver({ text: "zalo reply after failure" });
135
+ if (dispatchAttempts === 1) {
136
+ throw new Error("post-send failure");
137
+ }
138
+ },
139
+ );
140
+
141
+ const monitor = await startWebhookLifecycleMonitor({
142
+ ...createReplyOnceMonitorSetup(),
143
+ cacheKey: "zalo-reply-once-lifecycle",
144
+ });
145
+
146
+ try {
147
+ await withServer(
148
+ (req, res) => monitor.route.handler(req, res),
149
+ async (baseUrl) => {
150
+ const { first, replay } = await postWebhookReplay({
151
+ baseUrl,
152
+ path: "/hooks/zalo",
153
+ secret: "supersecret",
154
+ payload: createTextUpdate({
155
+ messageId: `zalo-retry-${Date.now()}`,
156
+ userId: "user-1",
157
+ userName: "User One",
158
+ chatId: "dm-chat-1",
159
+ }),
160
+ settleBeforeReplay: true,
161
+ });
162
+
163
+ expect(first.status).toBe(200);
164
+ expect(replay.status).toBe(200);
165
+ await settleAsyncWork();
166
+ },
167
+ );
168
+
169
+ expect(dispatchReplyWithBufferedBlockDispatcherMock).toHaveBeenCalledTimes(1);
170
+ expect(sendMessageMock).toHaveBeenCalledTimes(1);
171
+ expect(monitor.runtime.error).toHaveBeenCalledWith(
172
+ "[acct-zalo-lifecycle] Zalo webhook failed: Error: post-send failure",
173
+ );
174
+ } finally {
175
+ await monitor.stop();
176
+ }
177
+ });
178
+ });