@openclaw/zalouser 2026.3.13 → 2026.5.1-beta.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/README.md +4 -3
  2. package/api.ts +9 -0
  3. package/channel-plugin-api.ts +3 -0
  4. package/contract-api.ts +2 -0
  5. package/doctor-contract-api.ts +1 -0
  6. package/index.ts +29 -24
  7. package/openclaw.plugin.json +288 -1
  8. package/package.json +38 -11
  9. package/runtime-api.ts +67 -0
  10. package/secret-contract-api.ts +4 -0
  11. package/setup-entry.ts +9 -0
  12. package/setup-plugin-api.ts +2 -0
  13. package/src/accounts.runtime.ts +1 -0
  14. package/src/accounts.test-mocks.ts +7 -3
  15. package/src/accounts.test.ts +53 -1
  16. package/src/accounts.ts +38 -24
  17. package/src/channel-api.ts +20 -0
  18. package/src/channel.adapters.ts +390 -0
  19. package/src/channel.directory.test.ts +47 -40
  20. package/src/channel.runtime.ts +12 -0
  21. package/src/channel.sendpayload.test.ts +41 -23
  22. package/src/channel.setup.test.ts +33 -0
  23. package/src/channel.setup.ts +12 -0
  24. package/src/channel.test.ts +231 -20
  25. package/src/channel.ts +176 -685
  26. package/src/config-schema.ts +5 -5
  27. package/src/directory.ts +54 -0
  28. package/src/doctor-contract.ts +156 -0
  29. package/src/doctor.test.ts +77 -0
  30. package/src/doctor.ts +37 -0
  31. package/src/group-policy.test.ts +4 -4
  32. package/src/group-policy.ts +4 -2
  33. package/src/monitor.account-scope.test.ts +2 -1
  34. package/src/monitor.group-gating.test.ts +162 -8
  35. package/src/monitor.ts +233 -173
  36. package/src/probe.ts +3 -2
  37. package/src/qr-temp-file.ts +1 -1
  38. package/src/reaction.ts +5 -2
  39. package/src/runtime.ts +6 -3
  40. package/src/security-audit.test.ts +80 -0
  41. package/src/security-audit.ts +71 -0
  42. package/src/send.test.ts +2 -2
  43. package/src/send.ts +3 -3
  44. package/src/session-route.ts +121 -0
  45. package/src/setup-core.ts +33 -0
  46. package/src/setup-surface.test.ts +363 -0
  47. package/src/setup-surface.ts +470 -0
  48. package/src/setup-test-helpers.ts +42 -0
  49. package/src/shared.ts +92 -0
  50. package/src/status-issues.test.ts +1 -13
  51. package/src/status-issues.ts +8 -2
  52. package/src/test-helpers.ts +1 -1
  53. package/src/text-styles.test.ts +1 -1
  54. package/src/text-styles.ts +5 -2
  55. package/src/tool.test.ts +66 -3
  56. package/src/tool.ts +76 -14
  57. package/src/types.ts +3 -3
  58. package/src/zalo-js.credentials.test.ts +465 -0
  59. package/src/zalo-js.test-mocks.ts +89 -0
  60. package/src/zalo-js.ts +491 -274
  61. package/src/zca-client.test.ts +24 -0
  62. package/src/zca-client.ts +24 -58
  63. package/src/zca-constants.ts +55 -0
  64. package/test-api.ts +21 -0
  65. package/tsconfig.json +16 -0
  66. package/CHANGELOG.md +0 -107
  67. package/src/onboarding.ts +0 -340
package/README.md CHANGED
@@ -6,7 +6,7 @@ OpenClaw extension for Zalo Personal Account messaging via native `zca-js` integ
6
6
 
7
7
  ## Features
8
8
 
9
- - Channel plugin integration with onboarding + QR login
9
+ - Channel plugin integration with setup wizard + QR login
10
10
  - In-process listener/sender via `zca-js` (no external CLI)
11
11
  - Multi-account support
12
12
  - Agent tool integration (`zalouser`)
@@ -30,8 +30,9 @@ openclaw plugins install @openclaw/zalouser
30
30
  ### Option B: local source checkout
31
31
 
32
32
  ```bash
33
- openclaw plugins install ./extensions/zalouser
34
- cd ./extensions/zalouser && pnpm install
33
+ PLUGIN_SRC=./path/to/local/zalouser-plugin
34
+ openclaw plugins install "$PLUGIN_SRC"
35
+ cd "$PLUGIN_SRC" && pnpm install
35
36
  ```
36
37
 
37
38
  Restart the Gateway after install.
package/api.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { zalouserPlugin } from "./src/channel.js";
2
+ export { zalouserSetupPlugin } from "./src/channel.setup.js";
3
+ export { createZalouserTool } from "./src/tool.js";
4
+ export { createZalouserSetupWizardProxy, zalouserSetupAdapter } from "./src/setup-core.js";
5
+ export { zalouserSetupWizard } from "./src/setup-surface.js";
6
+ export {
7
+ collectZalouserSecurityAuditFindings,
8
+ isZalouserMutableGroupEntry,
9
+ } from "./src/security-audit.js";
@@ -0,0 +1,3 @@
1
+ // Keep bundled channel entry imports narrow so bootstrap/discovery paths do
2
+ // not drag setup-only or tool runtime surfaces into lightweight plugin loads.
3
+ export { zalouserPlugin } from "./src/channel.js";
@@ -0,0 +1,2 @@
1
+ export { collectZalouserSecurityAuditFindings } from "./src/security-audit.js";
2
+ export { legacyConfigRules, normalizeCompatibilityConfig } from "./src/doctor-contract.js";
@@ -0,0 +1 @@
1
+ export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
package/index.ts CHANGED
@@ -1,29 +1,34 @@
1
- import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/zalouser";
2
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/zalouser";
3
- import { zalouserDock, zalouserPlugin } from "./src/channel.js";
4
- import { setZalouserRuntime } from "./src/runtime.js";
5
- import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";
1
+ import {
2
+ type AnyAgentTool,
3
+ defineBundledChannelEntry,
4
+ loadBundledEntryExportSync,
5
+ } from "openclaw/plugin-sdk/channel-entry-contract";
6
6
 
7
- const plugin = {
7
+ function createZalouserTool(context?: unknown): AnyAgentTool {
8
+ const createTool = loadBundledEntryExportSync<(context?: unknown) => AnyAgentTool>(
9
+ import.meta.url,
10
+ {
11
+ specifier: "./api.js",
12
+ exportName: "createZalouserTool",
13
+ },
14
+ );
15
+ return createTool(context);
16
+ }
17
+
18
+ export default defineBundledChannelEntry({
8
19
  id: "zalouser",
9
20
  name: "Zalo Personal",
10
21
  description: "Zalo personal account messaging via native zca-js integration",
11
- configSchema: emptyPluginConfigSchema(),
12
- register(api: OpenClawPluginApi) {
13
- setZalouserRuntime(api.runtime);
14
- api.registerChannel({ plugin: zalouserPlugin, dock: zalouserDock });
15
-
16
- api.registerTool({
17
- name: "zalouser",
18
- label: "Zalo Personal",
19
- description:
20
- "Send messages and access data via Zalo personal account. " +
21
- "Actions: send (text message), image (send image URL), link (send link), " +
22
- "friends (list/search friends), groups (list groups), me (profile info), status (auth check).",
23
- parameters: ZalouserToolSchema,
24
- execute: executeZalouserTool,
25
- } as AnyAgentTool);
22
+ importMetaUrl: import.meta.url,
23
+ plugin: {
24
+ specifier: "./channel-plugin-api.js",
25
+ exportName: "zalouserPlugin",
26
26
  },
27
- };
28
-
29
- export default plugin;
27
+ runtime: {
28
+ specifier: "./runtime-api.js",
29
+ exportName: "setZalouserRuntime",
30
+ },
31
+ registerFull(api) {
32
+ api.registerTool((ctx) => createZalouserTool(ctx), { name: "zalouser" });
33
+ },
34
+ });
@@ -1,9 +1,296 @@
1
1
  {
2
2
  "id": "zalouser",
3
- "channels": ["zalouser"],
3
+ "activation": {
4
+ "onStartup": false
5
+ },
6
+ "channels": [
7
+ "zalouser"
8
+ ],
9
+ "channelEnvVars": {
10
+ "zalouser": [
11
+ "ZALOUSER_PROFILE",
12
+ "ZCA_PROFILE"
13
+ ]
14
+ },
4
15
  "configSchema": {
5
16
  "type": "object",
6
17
  "additionalProperties": false,
7
18
  "properties": {}
19
+ },
20
+ "channelConfigs": {
21
+ "zalouser": {
22
+ "schema": {
23
+ "$schema": "http://json-schema.org/draft-07/schema#",
24
+ "type": "object",
25
+ "properties": {
26
+ "name": {
27
+ "type": "string"
28
+ },
29
+ "enabled": {
30
+ "type": "boolean"
31
+ },
32
+ "markdown": {
33
+ "type": "object",
34
+ "properties": {
35
+ "tables": {
36
+ "type": "string",
37
+ "enum": [
38
+ "off",
39
+ "bullets",
40
+ "code",
41
+ "block"
42
+ ]
43
+ }
44
+ },
45
+ "additionalProperties": false
46
+ },
47
+ "profile": {
48
+ "type": "string"
49
+ },
50
+ "dangerouslyAllowNameMatching": {
51
+ "type": "boolean"
52
+ },
53
+ "dmPolicy": {
54
+ "type": "string",
55
+ "enum": [
56
+ "pairing",
57
+ "allowlist",
58
+ "open",
59
+ "disabled"
60
+ ]
61
+ },
62
+ "allowFrom": {
63
+ "type": "array",
64
+ "items": {
65
+ "anyOf": [
66
+ {
67
+ "type": "string"
68
+ },
69
+ {
70
+ "type": "number"
71
+ }
72
+ ]
73
+ }
74
+ },
75
+ "historyLimit": {
76
+ "type": "integer",
77
+ "minimum": 0,
78
+ "maximum": 9007199254740991
79
+ },
80
+ "groupAllowFrom": {
81
+ "type": "array",
82
+ "items": {
83
+ "anyOf": [
84
+ {
85
+ "type": "string"
86
+ },
87
+ {
88
+ "type": "number"
89
+ }
90
+ ]
91
+ }
92
+ },
93
+ "groupPolicy": {
94
+ "default": "allowlist",
95
+ "type": "string",
96
+ "enum": [
97
+ "open",
98
+ "disabled",
99
+ "allowlist"
100
+ ]
101
+ },
102
+ "groups": {
103
+ "type": "object",
104
+ "properties": {},
105
+ "additionalProperties": {
106
+ "type": "object",
107
+ "properties": {
108
+ "enabled": {
109
+ "type": "boolean"
110
+ },
111
+ "requireMention": {
112
+ "type": "boolean"
113
+ },
114
+ "tools": {
115
+ "type": "object",
116
+ "properties": {
117
+ "allow": {
118
+ "type": "array",
119
+ "items": {
120
+ "type": "string"
121
+ }
122
+ },
123
+ "alsoAllow": {
124
+ "type": "array",
125
+ "items": {
126
+ "type": "string"
127
+ }
128
+ },
129
+ "deny": {
130
+ "type": "array",
131
+ "items": {
132
+ "type": "string"
133
+ }
134
+ }
135
+ },
136
+ "additionalProperties": false
137
+ }
138
+ },
139
+ "additionalProperties": false
140
+ }
141
+ },
142
+ "messagePrefix": {
143
+ "type": "string"
144
+ },
145
+ "responsePrefix": {
146
+ "type": "string"
147
+ },
148
+ "accounts": {
149
+ "type": "object",
150
+ "properties": {},
151
+ "additionalProperties": {
152
+ "type": "object",
153
+ "properties": {
154
+ "name": {
155
+ "type": "string"
156
+ },
157
+ "enabled": {
158
+ "type": "boolean"
159
+ },
160
+ "markdown": {
161
+ "type": "object",
162
+ "properties": {
163
+ "tables": {
164
+ "type": "string",
165
+ "enum": [
166
+ "off",
167
+ "bullets",
168
+ "code",
169
+ "block"
170
+ ]
171
+ }
172
+ },
173
+ "additionalProperties": false
174
+ },
175
+ "profile": {
176
+ "type": "string"
177
+ },
178
+ "dangerouslyAllowNameMatching": {
179
+ "type": "boolean"
180
+ },
181
+ "dmPolicy": {
182
+ "type": "string",
183
+ "enum": [
184
+ "pairing",
185
+ "allowlist",
186
+ "open",
187
+ "disabled"
188
+ ]
189
+ },
190
+ "allowFrom": {
191
+ "type": "array",
192
+ "items": {
193
+ "anyOf": [
194
+ {
195
+ "type": "string"
196
+ },
197
+ {
198
+ "type": "number"
199
+ }
200
+ ]
201
+ }
202
+ },
203
+ "historyLimit": {
204
+ "type": "integer",
205
+ "minimum": 0,
206
+ "maximum": 9007199254740991
207
+ },
208
+ "groupAllowFrom": {
209
+ "type": "array",
210
+ "items": {
211
+ "anyOf": [
212
+ {
213
+ "type": "string"
214
+ },
215
+ {
216
+ "type": "number"
217
+ }
218
+ ]
219
+ }
220
+ },
221
+ "groupPolicy": {
222
+ "default": "allowlist",
223
+ "type": "string",
224
+ "enum": [
225
+ "open",
226
+ "disabled",
227
+ "allowlist"
228
+ ]
229
+ },
230
+ "groups": {
231
+ "type": "object",
232
+ "properties": {},
233
+ "additionalProperties": {
234
+ "type": "object",
235
+ "properties": {
236
+ "enabled": {
237
+ "type": "boolean"
238
+ },
239
+ "requireMention": {
240
+ "type": "boolean"
241
+ },
242
+ "tools": {
243
+ "type": "object",
244
+ "properties": {
245
+ "allow": {
246
+ "type": "array",
247
+ "items": {
248
+ "type": "string"
249
+ }
250
+ },
251
+ "alsoAllow": {
252
+ "type": "array",
253
+ "items": {
254
+ "type": "string"
255
+ }
256
+ },
257
+ "deny": {
258
+ "type": "array",
259
+ "items": {
260
+ "type": "string"
261
+ }
262
+ }
263
+ },
264
+ "additionalProperties": false
265
+ }
266
+ },
267
+ "additionalProperties": false
268
+ }
269
+ },
270
+ "messagePrefix": {
271
+ "type": "string"
272
+ },
273
+ "responsePrefix": {
274
+ "type": "string"
275
+ }
276
+ },
277
+ "required": [
278
+ "groupPolicy"
279
+ ],
280
+ "additionalProperties": false
281
+ }
282
+ },
283
+ "defaultAccount": {
284
+ "type": "string"
285
+ }
286
+ },
287
+ "required": [
288
+ "groupPolicy"
289
+ ],
290
+ "additionalProperties": false
291
+ },
292
+ "label": "Zalo Personal",
293
+ "description": "Zalo personal account via QR code login."
294
+ }
8
295
  }
9
296
  }
package/package.json CHANGED
@@ -1,17 +1,33 @@
1
1
  {
2
2
  "name": "@openclaw/zalouser",
3
- "version": "2026.3.13",
3
+ "version": "2026.5.1-beta.2",
4
4
  "description": "OpenClaw Zalo Personal Account plugin via native zca-js integration",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/openclaw/openclaw"
8
+ },
5
9
  "type": "module",
6
10
  "dependencies": {
7
- "@sinclair/typebox": "0.34.48",
8
- "zca-js": "2.1.1",
9
- "zod": "^4.3.6"
11
+ "typebox": "1.1.37",
12
+ "zca-js": "2.1.2"
13
+ },
14
+ "devDependencies": {
15
+ "@openclaw/plugin-sdk": "workspace:*",
16
+ "openclaw": "workspace:*"
17
+ },
18
+ "peerDependencies": {
19
+ "openclaw": ">=2026.4.25"
20
+ },
21
+ "peerDependenciesMeta": {
22
+ "openclaw": {
23
+ "optional": true
24
+ }
10
25
  },
11
26
  "openclaw": {
12
27
  "extensions": [
13
28
  "./index.ts"
14
29
  ],
30
+ "setupEntry": "./setup-entry.ts",
15
31
  "channel": {
16
32
  "id": "zalouser",
17
33
  "label": "Zalo Personal",
@@ -23,17 +39,28 @@
23
39
  "zlu"
24
40
  ],
25
41
  "order": 85,
26
- "quickstartAllowFrom": true
42
+ "quickstartAllowFrom": false,
43
+ "doctorCapabilities": {
44
+ "dmAllowFromMode": "topOnly",
45
+ "groupModel": "hybrid",
46
+ "groupAllowFromFallbackToAllowFrom": false,
47
+ "warnOnEmptyGroupSenderAllowlist": false
48
+ }
27
49
  },
28
50
  "install": {
29
51
  "npmSpec": "@openclaw/zalouser",
30
- "localPath": "extensions/zalouser",
31
- "defaultChoice": "npm"
52
+ "defaultChoice": "npm",
53
+ "minHostVersion": ">=2026.4.10"
54
+ },
55
+ "compat": {
56
+ "pluginApi": ">=2026.4.25"
57
+ },
58
+ "build": {
59
+ "openclawVersion": "2026.5.1-beta.2"
32
60
  },
33
- "releaseChecks": {
34
- "rootDependencyMirrorAllowlist": [
35
- "zca-js"
36
- ]
61
+ "release": {
62
+ "publishToClawHub": true,
63
+ "publishToNpm": true
37
64
  }
38
65
  }
39
66
  }
package/runtime-api.ts ADDED
@@ -0,0 +1,67 @@
1
+ export {
2
+ collectZalouserSecurityAuditFindings,
3
+ createZalouserSetupWizardProxy,
4
+ createZalouserTool,
5
+ isZalouserMutableGroupEntry,
6
+ zalouserPlugin,
7
+ zalouserSetupAdapter,
8
+ zalouserSetupPlugin,
9
+ zalouserSetupWizard,
10
+ } from "./api.js";
11
+ export { setZalouserRuntime } from "./src/runtime.js";
12
+ export type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
13
+ export type {
14
+ BaseProbeResult,
15
+ ChannelAccountSnapshot,
16
+ ChannelDirectoryEntry,
17
+ ChannelGroupContext,
18
+ ChannelMessageActionAdapter,
19
+ ChannelStatusIssue,
20
+ } from "openclaw/plugin-sdk/channel-contract";
21
+ export type {
22
+ OpenClawConfig,
23
+ GroupToolPolicyConfig,
24
+ MarkdownTableMode,
25
+ } from "openclaw/plugin-sdk/config-types";
26
+ export type {
27
+ PluginRuntime,
28
+ AnyAgentTool,
29
+ ChannelPlugin,
30
+ OpenClawPluginToolContext,
31
+ } from "openclaw/plugin-sdk/core";
32
+ export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
33
+ export {
34
+ DEFAULT_ACCOUNT_ID,
35
+ buildChannelConfigSchema,
36
+ normalizeAccountId,
37
+ } from "openclaw/plugin-sdk/core";
38
+ export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
39
+ export { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
40
+ export {
41
+ resolveDefaultGroupPolicy,
42
+ resolveOpenProviderRuntimeGroupPolicy,
43
+ warnMissingProviderGroupPolicyFallbackOnce,
44
+ } from "openclaw/plugin-sdk/runtime-group-policy";
45
+ export {
46
+ mergeAllowlist,
47
+ summarizeMapping,
48
+ formatAllowFromLowercase,
49
+ } from "openclaw/plugin-sdk/allow-from";
50
+ export { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
51
+ export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
52
+ export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
53
+ export { buildBaseAccountStatusSnapshot } from "openclaw/plugin-sdk/status-helpers";
54
+ export { resolveSenderCommandAuthorization } from "openclaw/plugin-sdk/command-auth";
55
+ export {
56
+ evaluateGroupRouteAccessForPolicy,
57
+ resolveSenderScopedGroupPolicy,
58
+ } from "openclaw/plugin-sdk/group-access";
59
+ export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
60
+ export {
61
+ deliverTextOrMediaReply,
62
+ isNumericTargetId,
63
+ resolveSendableOutboundReplyParts,
64
+ sendPayloadWithChunkedTextAndMedia,
65
+ type OutboundReplyPayload,
66
+ } from "openclaw/plugin-sdk/reply-payload";
67
+ export { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
@@ -0,0 +1,4 @@
1
+ // Zalo User does not expose secret-contract surfaces.
2
+ export const secretTargetRegistryEntries: readonly [] = [];
3
+
4
+ export function collectRuntimeConfigAssignments(): void {}
package/setup-entry.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract";
2
+
3
+ export default defineBundledChannelSetupEntry({
4
+ importMetaUrl: import.meta.url,
5
+ plugin: {
6
+ specifier: "./setup-plugin-api.js",
7
+ exportName: "zalouserSetupPlugin",
8
+ },
9
+ });
@@ -0,0 +1,2 @@
1
+ // Keep setup-entry imports narrow so setup loads do not pull tool surfaces.
2
+ export { zalouserSetupPlugin } from "./src/channel.setup.js";
@@ -0,0 +1 @@
1
+ export { checkZaloAuthenticated, getZaloUserInfo } from "./zalo-js.js";
@@ -1,10 +1,14 @@
1
1
  import { vi } from "vitest";
2
2
  import { createDefaultResolvedZalouserAccount } from "./test-helpers.js";
3
3
 
4
- vi.mock("./accounts.js", async (importOriginal) => {
5
- const actual = (await importOriginal()) as Record<string, unknown>;
4
+ vi.mock("./accounts.js", () => {
6
5
  return {
7
- ...actual,
6
+ listZalouserAccountIds: () => ["default"],
7
+ resolveDefaultZalouserAccountId: () => "default",
8
8
  resolveZalouserAccountSync: () => createDefaultResolvedZalouserAccount(),
9
+ resolveZalouserAccount: async () => createDefaultResolvedZalouserAccount(),
10
+ listEnabledZalouserAccounts: async () => [createDefaultResolvedZalouserAccount()],
11
+ getZcaUserInfo: async () => null,
12
+ checkZcaAuthenticated: async () => false,
9
13
  };
10
14
  });
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
2
- import type { OpenClawConfig } from "openclaw/plugin-sdk/zalouser";
3
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type { OpenClawConfig } from "../runtime-api.js";
4
4
  import {
5
5
  getZcaUserInfo,
6
6
  listEnabledZalouserAccounts,
@@ -124,6 +124,58 @@ describe("zalouser account resolution", () => {
124
124
  expect(resolved.config.allowFrom).toEqual(["123"]);
125
125
  });
126
126
 
127
+ it("uses configured defaultAccount when accountId is omitted", () => {
128
+ const cfg = asConfig({
129
+ channels: {
130
+ zalouser: {
131
+ defaultAccount: "work",
132
+ accounts: {
133
+ work: {
134
+ name: "Work",
135
+ profile: "work-profile",
136
+ },
137
+ },
138
+ },
139
+ },
140
+ });
141
+
142
+ const resolved = resolveZalouserAccountSync({ cfg });
143
+ expect(resolved.accountId).toBe("work");
144
+ expect(resolved.name).toBe("Work");
145
+ expect(resolved.profile).toBe("work-profile");
146
+ });
147
+
148
+ it("resolves account config when account key casing differs from normalized id", () => {
149
+ const cfg = asConfig({
150
+ channels: {
151
+ zalouser: {
152
+ accounts: {
153
+ Work: {
154
+ name: "Work",
155
+ },
156
+ },
157
+ },
158
+ },
159
+ });
160
+
161
+ const resolved = resolveZalouserAccountSync({ cfg, accountId: "work" });
162
+ expect(resolved.accountId).toBe("work");
163
+ expect(resolved.name).toBe("Work");
164
+ });
165
+
166
+ it("defaults group policy to allowlist when unset", () => {
167
+ const cfg = asConfig({
168
+ channels: {
169
+ zalouser: {
170
+ enabled: true,
171
+ },
172
+ },
173
+ });
174
+
175
+ const resolved = resolveZalouserAccountSync({ cfg, accountId: "default" });
176
+ expect(resolved.config.groupPolicy).toBe("allowlist");
177
+ });
178
+
127
179
  it("resolves profile precedence correctly", () => {
128
180
  const cfg = asConfig({
129
181
  channels: {