@kodelyth/zalouser 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 (106) hide show
  1. package/README.md +120 -0
  2. package/api.ts +9 -0
  3. package/channel-plugin-api.ts +3 -0
  4. package/contract-api.ts +2 -0
  5. package/dist/accounts-DOefD_if.js +66 -0
  6. package/dist/accounts.runtime-KT101uuu.js +2 -0
  7. package/dist/api-DSWT4Dh_.js +133 -0
  8. package/dist/api.js +7 -0
  9. package/dist/channel-pby_3Sur.js +602 -0
  10. package/dist/channel-plugin-api.js +2 -0
  11. package/dist/channel.runtime-0aJ2O7Y8.js +25 -0
  12. package/dist/channel.setup-CqyWwqcQ.js +9 -0
  13. package/dist/contract-api.js +3 -0
  14. package/dist/doctor-contract-B9EvrW0j.js +128 -0
  15. package/dist/doctor-contract-api.js +2 -0
  16. package/dist/index.js +27 -0
  17. package/dist/monitor-CVtrUqyW.js +708 -0
  18. package/dist/runtime-api.js +19 -0
  19. package/dist/secret-contract-api.js +5 -0
  20. package/dist/security-audit-D_rftvs-.js +34 -0
  21. package/dist/send-uRjUB8mG.js +542 -0
  22. package/dist/session-route-CalHiv1d.js +92 -0
  23. package/dist/setup-entry.js +11 -0
  24. package/dist/setup-plugin-api.js +2 -0
  25. package/dist/setup-surface-Cfj4GQlB.js +360 -0
  26. package/dist/shared-DjK0e2FC.js +160 -0
  27. package/dist/test-api.js +5 -0
  28. package/dist/zalo-js-B80cRyDF.js +1285 -0
  29. package/doctor-contract-api.ts +1 -0
  30. package/index.ts +34 -0
  31. package/klaw.plugin.json +3 -286
  32. package/package.json +4 -4
  33. package/runtime-api.ts +62 -0
  34. package/secret-contract-api.ts +4 -0
  35. package/setup-entry.ts +9 -0
  36. package/setup-plugin-api.ts +2 -0
  37. package/src/accounts.runtime.ts +1 -0
  38. package/src/accounts.test-mocks.ts +14 -0
  39. package/src/accounts.test.ts +298 -0
  40. package/src/accounts.ts +136 -0
  41. package/src/channel-api.ts +16 -0
  42. package/src/channel.adapters.ts +432 -0
  43. package/src/channel.directory.test.ts +59 -0
  44. package/src/channel.runtime.ts +12 -0
  45. package/src/channel.sendpayload.test.ts +311 -0
  46. package/src/channel.setup.test.ts +30 -0
  47. package/src/channel.setup.ts +12 -0
  48. package/src/channel.test.ts +424 -0
  49. package/src/channel.ts +221 -0
  50. package/src/config-schema.ts +33 -0
  51. package/src/directory.ts +54 -0
  52. package/src/doctor-contract.ts +156 -0
  53. package/src/doctor.test.ts +87 -0
  54. package/src/doctor.ts +37 -0
  55. package/src/group-policy.test.ts +61 -0
  56. package/src/group-policy.ts +83 -0
  57. package/src/message-sid.test.ts +66 -0
  58. package/src/message-sid.ts +80 -0
  59. package/src/monitor.account-scope.test.ts +122 -0
  60. package/src/monitor.group-gating.test.ts +967 -0
  61. package/src/monitor.send-mocks.ts +20 -0
  62. package/src/monitor.ts +1057 -0
  63. package/src/probe.test.ts +60 -0
  64. package/src/probe.ts +35 -0
  65. package/src/qr-temp-file.ts +19 -0
  66. package/src/reaction.test.ts +19 -0
  67. package/src/reaction.ts +32 -0
  68. package/src/runtime.ts +9 -0
  69. package/src/security-audit.test.ts +83 -0
  70. package/src/security-audit.ts +71 -0
  71. package/src/send-receipt.ts +31 -0
  72. package/src/send.test.ts +424 -0
  73. package/src/send.ts +280 -0
  74. package/src/session-route.ts +121 -0
  75. package/src/setup-core.ts +36 -0
  76. package/src/setup-surface.test.ts +367 -0
  77. package/src/setup-surface.ts +481 -0
  78. package/src/setup-test-helpers.ts +42 -0
  79. package/src/shared.ts +92 -0
  80. package/src/status-issues.test.ts +31 -0
  81. package/src/status-issues.ts +55 -0
  82. package/src/test-helpers.ts +26 -0
  83. package/src/text-styles.test.ts +203 -0
  84. package/src/text-styles.ts +540 -0
  85. package/src/tool.test.ts +212 -0
  86. package/src/tool.ts +200 -0
  87. package/src/types.ts +127 -0
  88. package/src/zalo-js.credentials.test.ts +465 -0
  89. package/src/zalo-js.test-mocks.ts +89 -0
  90. package/src/zalo-js.ts +1889 -0
  91. package/src/zca-client.test.ts +27 -0
  92. package/src/zca-client.ts +259 -0
  93. package/src/zca-constants.ts +55 -0
  94. package/src/zca-js-exports.d.ts +22 -0
  95. package/test-api.ts +21 -0
  96. package/tsconfig.json +16 -0
  97. package/api.js +0 -7
  98. package/channel-plugin-api.js +0 -7
  99. package/contract-api.js +0 -7
  100. package/doctor-contract-api.js +0 -7
  101. package/index.js +0 -7
  102. package/runtime-api.js +0 -7
  103. package/secret-contract-api.js +0 -7
  104. package/setup-entry.js +0 -7
  105. package/setup-plugin-api.js +0 -7
  106. package/test-api.js +0 -7
@@ -0,0 +1,311 @@
1
+ import {
2
+ installChannelOutboundPayloadContractSuite,
3
+ primeChannelOutboundSendMock,
4
+ type OutboundPayloadHarnessParams,
5
+ } from "klaw/plugin-sdk/channel-contract-testing";
6
+ import {
7
+ createMessageReceiptFromOutboundResults,
8
+ verifyChannelMessageAdapterCapabilityProofs,
9
+ } from "klaw/plugin-sdk/channel-message";
10
+ import { beforeEach, describe, expect, it, vi } from "vitest";
11
+ import "./accounts.test-mocks.js";
12
+ import "./zalo-js.test-mocks.js";
13
+ import type { ReplyPayload } from "../runtime-api.js";
14
+ import { zalouserPlugin } from "./channel.js";
15
+ import { setZalouserRuntime } from "./runtime.js";
16
+ import * as sendModule from "./send.js";
17
+
18
+ vi.mock("./send.js", () => ({
19
+ sendMessageZalouser: vi.fn().mockResolvedValue({ ok: true, messageId: "zlu-1" } as never),
20
+ sendReactionZalouser: vi.fn().mockResolvedValue({ ok: true } as never),
21
+ }));
22
+
23
+ function baseCtx(payload: ReplyPayload) {
24
+ return {
25
+ cfg: {},
26
+ to: "user:987654321",
27
+ text: "",
28
+ payload,
29
+ };
30
+ }
31
+
32
+ type ZalouserOutbound = NonNullable<typeof zalouserPlugin.outbound>;
33
+ type ZalouserSendPayload = NonNullable<ZalouserOutbound["sendPayload"]>;
34
+ type ZalouserMessageAdapter = NonNullable<typeof zalouserPlugin.message>;
35
+ type ZalouserMessageSender = NonNullable<ZalouserMessageAdapter["send"]>;
36
+
37
+ function requireZalouserSendPayload(): ZalouserSendPayload {
38
+ const sendPayload = zalouserPlugin.outbound?.sendPayload;
39
+ if (!sendPayload) {
40
+ throw new Error("Expected Zalouser outbound sendPayload");
41
+ }
42
+ return sendPayload;
43
+ }
44
+
45
+ function requireZalouserMessageAdapter(): ZalouserMessageAdapter {
46
+ const adapter = zalouserPlugin.message;
47
+ if (!adapter) {
48
+ throw new Error("Expected Zalouser message adapter");
49
+ }
50
+ return adapter;
51
+ }
52
+
53
+ function requireZalouserTextSender(
54
+ adapter: ZalouserMessageAdapter,
55
+ ): NonNullable<ZalouserMessageSender["text"]> {
56
+ const text = adapter.send?.text;
57
+ if (!text) {
58
+ throw new Error("Expected Zalouser message adapter text sender");
59
+ }
60
+ return text;
61
+ }
62
+
63
+ function requireZalouserMediaSender(
64
+ adapter: ZalouserMessageAdapter,
65
+ ): NonNullable<ZalouserMessageSender["media"]> {
66
+ const media = adapter.send?.media;
67
+ if (!media) {
68
+ throw new Error("Expected Zalouser message adapter media sender");
69
+ }
70
+ return media;
71
+ }
72
+
73
+ function requireRecord(value: unknown, label: string): Record<string, unknown> {
74
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
75
+ throw new Error(`expected ${label} to be a record`);
76
+ }
77
+ return value as Record<string, unknown>;
78
+ }
79
+
80
+ function requireSendOptions(
81
+ mockedSend: ReturnType<typeof vi.mocked<(typeof import("./send.js"))["sendMessageZalouser"]>>,
82
+ ): Record<string, unknown> {
83
+ return requireRecord(requireSendCall(mockedSend)[2], "Zalouser send options");
84
+ }
85
+
86
+ function requireSendCall(
87
+ mockedSend: ReturnType<typeof vi.mocked<(typeof import("./send.js"))["sendMessageZalouser"]>>,
88
+ ): unknown[] {
89
+ const [call] = mockedSend.mock.calls as unknown[][];
90
+ if (!call) {
91
+ throw new Error("expected Zalouser send call");
92
+ }
93
+ return call;
94
+ }
95
+
96
+ describe("zalouserPlugin outbound sendPayload", () => {
97
+ let mockedSend: ReturnType<typeof vi.mocked<(typeof import("./send.js"))["sendMessageZalouser"]>>;
98
+
99
+ beforeEach(() => {
100
+ setZalouserRuntime({
101
+ channel: {
102
+ text: {
103
+ resolveChunkMode: vi.fn(() => "length"),
104
+ resolveTextChunkLimit: vi.fn(() => 1200),
105
+ },
106
+ },
107
+ } as never);
108
+ mockedSend = vi.mocked(sendModule.sendMessageZalouser);
109
+ primeChannelOutboundSendMock(mockedSend, { ok: true, messageId: "zlu-1" });
110
+ });
111
+
112
+ it("group target delegates with isGroup=true and stripped threadId", async () => {
113
+ mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-g1" } as never);
114
+ const sendPayload = requireZalouserSendPayload();
115
+
116
+ const result = await sendPayload({
117
+ ...baseCtx({ text: "hello group" }),
118
+ to: "group:1471383327500481391",
119
+ });
120
+
121
+ expect(mockedSend).toHaveBeenCalledOnce();
122
+ const sendCall = requireSendCall(mockedSend);
123
+ expect(sendCall[0]).toBe("1471383327500481391");
124
+ expect(sendCall[1]).toBe("hello group");
125
+ const options = requireSendOptions(mockedSend);
126
+ expect(options.isGroup).toBe(true);
127
+ expect(options.textMode).toBe("markdown");
128
+ expect(result.channel).toBe("zalouser");
129
+ expect(result.messageId).toBe("zlu-g1");
130
+ });
131
+
132
+ it("treats bare numeric targets as direct chats for backward compatibility", async () => {
133
+ mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-d1" } as never);
134
+ const sendPayload = requireZalouserSendPayload();
135
+
136
+ const result = await sendPayload({
137
+ ...baseCtx({ text: "hello" }),
138
+ to: "987654321",
139
+ });
140
+
141
+ expect(mockedSend).toHaveBeenCalledOnce();
142
+ const sendCall = requireSendCall(mockedSend);
143
+ expect(sendCall[0]).toBe("987654321");
144
+ expect(sendCall[1]).toBe("hello");
145
+ const options = requireSendOptions(mockedSend);
146
+ expect(options.isGroup).toBe(false);
147
+ expect(options.textMode).toBe("markdown");
148
+ expect(result.channel).toBe("zalouser");
149
+ expect(result.messageId).toBe("zlu-d1");
150
+ });
151
+
152
+ it("preserves provider-native group ids when sending to raw g- targets", async () => {
153
+ mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-g-native" } as never);
154
+ const sendPayload = requireZalouserSendPayload();
155
+
156
+ const result = await sendPayload({
157
+ ...baseCtx({ text: "hello native group" }),
158
+ to: "g-1471383327500481391",
159
+ });
160
+
161
+ expect(mockedSend).toHaveBeenCalledOnce();
162
+ const sendCall = requireSendCall(mockedSend);
163
+ expect(sendCall[0]).toBe("g-1471383327500481391");
164
+ expect(sendCall[1]).toBe("hello native group");
165
+ const options = requireSendOptions(mockedSend);
166
+ expect(options.isGroup).toBe(true);
167
+ expect(options.textMode).toBe("markdown");
168
+ expect(result.channel).toBe("zalouser");
169
+ expect(result.messageId).toBe("zlu-g-native");
170
+ });
171
+
172
+ it("passes long markdown through once so formatting happens before chunking", async () => {
173
+ const text = `**${"a".repeat(2501)}**`;
174
+ mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-code" } as never);
175
+ const sendPayload = requireZalouserSendPayload();
176
+
177
+ const result = await sendPayload({
178
+ ...baseCtx({ text }),
179
+ to: "987654321",
180
+ });
181
+
182
+ expect(mockedSend).toHaveBeenCalledTimes(1);
183
+ const sendCall = requireSendCall(mockedSend);
184
+ expect(sendCall[0]).toBe("987654321");
185
+ expect(sendCall[1]).toBe(text);
186
+ const options = requireSendOptions(mockedSend);
187
+ expect(options.isGroup).toBe(false);
188
+ expect(options.textMode).toBe("markdown");
189
+ expect(options.textChunkMode).toBe("length");
190
+ expect(options.textChunkLimit).toBe(1200);
191
+ expect(result.channel).toBe("zalouser");
192
+ expect(result.messageId).toBe("zlu-code");
193
+ });
194
+
195
+ it("declares message adapter durable text and media with receipt proofs", async () => {
196
+ mockedSend.mockImplementation(async (_threadId, _text, opts: { mediaUrl?: string } = {}) =>
197
+ opts.mediaUrl
198
+ ? {
199
+ ok: true,
200
+ messageId: "zlu-media-1",
201
+ receipt: createMessageReceiptFromOutboundResults({
202
+ results: [{ channel: "zalouser", messageId: "zlu-media-1" }],
203
+ kind: "media",
204
+ }),
205
+ }
206
+ : {
207
+ ok: true,
208
+ messageId: "zlu-text-1",
209
+ receipt: createMessageReceiptFromOutboundResults({
210
+ results: [{ channel: "zalouser", messageId: "zlu-text-1" }],
211
+ kind: "text",
212
+ }),
213
+ },
214
+ );
215
+ const adapter = requireZalouserMessageAdapter();
216
+ const sendText = requireZalouserTextSender(adapter);
217
+ const sendMedia = requireZalouserMediaSender(adapter);
218
+
219
+ const proofs = await verifyChannelMessageAdapterCapabilityProofs({
220
+ adapterName: "zalouser",
221
+ adapter,
222
+ proofs: {
223
+ text: async () => {
224
+ const result = await sendText({
225
+ cfg: {},
226
+ to: "user:987654321",
227
+ text: "hello",
228
+ });
229
+ expect(result.receipt.platformMessageIds).toEqual(["zlu-text-1"]);
230
+ },
231
+ media: async () => {
232
+ const result = await sendMedia({
233
+ cfg: {},
234
+ to: "user:987654321",
235
+ text: "image",
236
+ mediaUrl: "https://example.com/image.png",
237
+ });
238
+ expect(result.receipt.platformMessageIds).toEqual(["zlu-media-1"]);
239
+ },
240
+ messageSendingHooks: () => {
241
+ expect(adapter.durableFinal?.capabilities?.messageSendingHooks).toBe(true);
242
+ },
243
+ },
244
+ });
245
+ const proofStatusByCapability = new Map(
246
+ proofs.map((proof) => [proof.capability, proof.status] as const),
247
+ );
248
+ expect(proofStatusByCapability.get("text")).toBe("verified");
249
+ expect(proofStatusByCapability.get("media")).toBe("verified");
250
+ expect(proofStatusByCapability.get("messageSendingHooks")).toBe("verified");
251
+ });
252
+ });
253
+
254
+ describe("zalouserPlugin outbound payload contract", () => {
255
+ function createZalouserHarness(params: OutboundPayloadHarnessParams) {
256
+ const mockedSend = vi.mocked(sendModule.sendMessageZalouser);
257
+ setZalouserRuntime({
258
+ channel: {
259
+ text: {
260
+ resolveChunkMode: vi.fn(() => "length"),
261
+ resolveTextChunkLimit: vi.fn(() => 1200),
262
+ },
263
+ },
264
+ } as never);
265
+ primeChannelOutboundSendMock(mockedSend, { ok: true, messageId: "zlu-1" }, params.sendResults);
266
+ const ctx = {
267
+ cfg: {},
268
+ to: "user:987654321",
269
+ text: "",
270
+ payload: params.payload,
271
+ };
272
+ const sendPayload = requireZalouserSendPayload();
273
+ return {
274
+ run: async () => await sendPayload(ctx),
275
+ sendMock: mockedSend,
276
+ to: "987654321",
277
+ };
278
+ }
279
+
280
+ installChannelOutboundPayloadContractSuite({
281
+ channel: "zalouser",
282
+ chunking: { mode: "passthrough", longTextLength: 3000 },
283
+ createHarness: createZalouserHarness,
284
+ });
285
+ });
286
+
287
+ describe("zalouserPlugin messaging target normalization", () => {
288
+ it("normalizes user/group aliases to canonical targets", () => {
289
+ const normalize = zalouserPlugin.messaging?.normalizeTarget;
290
+ if (!normalize) {
291
+ throw new Error("normalizeTarget unavailable");
292
+ }
293
+ expect(normalize("zlu:g:30003")).toBe("group:30003");
294
+ expect(normalize("zalouser:u:20002")).toBe("user:20002");
295
+ expect(normalize("zlu:g-30003")).toBe("group:g-30003");
296
+ expect(normalize("zalouser:u-20002")).toBe("user:u-20002");
297
+ expect(normalize("20002")).toBe("20002");
298
+ });
299
+
300
+ it("treats canonical and provider-native user/group targets as ids", () => {
301
+ const looksLikeId = zalouserPlugin.messaging?.targetResolver?.looksLikeId;
302
+ if (!looksLikeId) {
303
+ throw new Error("looksLikeId unavailable");
304
+ }
305
+ expect(looksLikeId("user:20002")).toBe(true);
306
+ expect(looksLikeId("group:30003")).toBe(true);
307
+ expect(looksLikeId("g-30003")).toBe(true);
308
+ expect(looksLikeId("u-20002")).toBe(true);
309
+ expect(looksLikeId("Alice Nguyen")).toBe(false);
310
+ });
311
+ });
@@ -0,0 +1,30 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { createPluginSetupWizardStatus } from "klaw/plugin-sdk/plugin-test-runtime";
5
+ import { withEnvAsync } from "klaw/plugin-sdk/test-env";
6
+ import { describe, expect, it } from "vitest";
7
+ import "./zalo-js.test-mocks.js";
8
+ import { zalouserSetupPlugin } from "./setup-test-helpers.js";
9
+
10
+ const zalouserSetupGetStatus = createPluginSetupWizardStatus(zalouserSetupPlugin);
11
+
12
+ describe("zalouser setup plugin", () => {
13
+ it("builds setup status without an initialized runtime", async () => {
14
+ const stateDir = await mkdtemp(path.join(os.tmpdir(), "klaw-zalouser-setup-"));
15
+
16
+ try {
17
+ await withEnvAsync({ KLAW_STATE_DIR: stateDir }, async () => {
18
+ const status = await zalouserSetupGetStatus({
19
+ cfg: {},
20
+ accountOverrides: {},
21
+ });
22
+ expect(status.channel).toBe("zalouser");
23
+ expect(status.configured).toBe(false);
24
+ expect(status.statusLines).toEqual(["Zalo Personal: needs QR login"]);
25
+ });
26
+ } finally {
27
+ await rm(stateDir, { recursive: true, force: true });
28
+ }
29
+ });
30
+ });
@@ -0,0 +1,12 @@
1
+ import type { ResolvedZalouserAccount } from "./accounts.js";
2
+ import type { ChannelPlugin } from "./channel-api.js";
3
+ import { zalouserSetupAdapter } from "./setup-core.js";
4
+ import { zalouserSetupWizard } from "./setup-surface.js";
5
+ import { createZalouserPluginBase } from "./shared.js";
6
+
7
+ export const zalouserSetupPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
8
+ ...createZalouserPluginBase({
9
+ setupWizard: zalouserSetupWizard,
10
+ setup: zalouserSetupAdapter,
11
+ }),
12
+ };