@actagent/feishu 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 (207) hide show
  1. package/README.md +11 -0
  2. package/actagent.plugin.json +224 -0
  3. package/api.ts +33 -0
  4. package/channel-entry.ts +21 -0
  5. package/channel-plugin-api.ts +2 -0
  6. package/contract-api.ts +17 -0
  7. package/index.ts +83 -0
  8. package/legacy-state-migrations-api.ts +2 -0
  9. package/npm-shrinkwrap.json +539 -0
  10. package/package.json +64 -0
  11. package/runtime-api.ts +58 -0
  12. package/runtime-setter-api.ts +3 -0
  13. package/secret-contract-api.ts +6 -0
  14. package/security-contract-api.ts +2 -0
  15. package/session-key-api.ts +2 -0
  16. package/setup-api.ts +4 -0
  17. package/setup-entry.test.ts +33 -0
  18. package/setup-entry.ts +25 -0
  19. package/skills/feishu-doc/SKILL.md +211 -0
  20. package/skills/feishu-doc/references/block-types.md +103 -0
  21. package/skills/feishu-drive/SKILL.md +97 -0
  22. package/skills/feishu-perm/SKILL.md +119 -0
  23. package/skills/feishu-wiki/SKILL.md +113 -0
  24. package/src/accounts.test.ts +481 -0
  25. package/src/accounts.ts +380 -0
  26. package/src/agent-config.ts +22 -0
  27. package/src/app-registration.test.ts +62 -0
  28. package/src/app-registration.ts +355 -0
  29. package/src/approval-auth.test.ts +25 -0
  30. package/src/approval-auth.ts +26 -0
  31. package/src/async.test.ts +68 -0
  32. package/src/async.ts +109 -0
  33. package/src/audio-preflight.runtime.ts +10 -0
  34. package/src/bitable.test.ts +174 -0
  35. package/src/bitable.ts +781 -0
  36. package/src/bot-content.ts +488 -0
  37. package/src/bot-group-name.test.ts +148 -0
  38. package/src/bot-runtime-api.ts +13 -0
  39. package/src/bot-sender-name.test.ts +68 -0
  40. package/src/bot-sender-name.ts +137 -0
  41. package/src/bot.broadcast.test.ts +643 -0
  42. package/src/bot.card-action.test.ts +647 -0
  43. package/src/bot.checkBotMentioned.test.ts +266 -0
  44. package/src/bot.helpers.test.ts +136 -0
  45. package/src/bot.stripBotMention.test.ts +127 -0
  46. package/src/bot.test.ts +3817 -0
  47. package/src/bot.ts +1788 -0
  48. package/src/card-action.ts +515 -0
  49. package/src/card-interaction.test.ts +132 -0
  50. package/src/card-interaction.ts +160 -0
  51. package/src/card-test-helpers.ts +55 -0
  52. package/src/card-ux-approval.ts +66 -0
  53. package/src/card-ux-launcher.test.ts +126 -0
  54. package/src/card-ux-launcher.ts +136 -0
  55. package/src/card-ux-shared.ts +34 -0
  56. package/src/channel-runtime-api.ts +17 -0
  57. package/src/channel.runtime.ts +48 -0
  58. package/src/channel.test.ts +1337 -0
  59. package/src/channel.ts +1401 -0
  60. package/src/chat-schema.ts +30 -0
  61. package/src/chat.test.ts +295 -0
  62. package/src/chat.ts +198 -0
  63. package/src/client-timeout.ts +44 -0
  64. package/src/client.test.ts +463 -0
  65. package/src/client.ts +263 -0
  66. package/src/comment-dispatcher-runtime-api.ts +7 -0
  67. package/src/comment-dispatcher.test.ts +186 -0
  68. package/src/comment-dispatcher.ts +108 -0
  69. package/src/comment-handler-runtime-api.ts +4 -0
  70. package/src/comment-handler.test.ts +588 -0
  71. package/src/comment-handler.ts +304 -0
  72. package/src/comment-reaction.test.ts +139 -0
  73. package/src/comment-reaction.ts +260 -0
  74. package/src/comment-shared.test.ts +184 -0
  75. package/src/comment-shared.ts +405 -0
  76. package/src/comment-target.ts +45 -0
  77. package/src/config-schema.test.ts +327 -0
  78. package/src/config-schema.ts +338 -0
  79. package/src/conversation-id.test.ts +19 -0
  80. package/src/conversation-id.ts +199 -0
  81. package/src/dedup-migrations.test.ts +90 -0
  82. package/src/dedup-migrations.ts +103 -0
  83. package/src/dedup.test.ts +95 -0
  84. package/src/dedup.ts +304 -0
  85. package/src/dedupe-key.ts +68 -0
  86. package/src/directory.static.ts +62 -0
  87. package/src/directory.test.ts +142 -0
  88. package/src/directory.ts +125 -0
  89. package/src/doc-schema.ts +183 -0
  90. package/src/doctor.test.ts +382 -0
  91. package/src/doctor.ts +876 -0
  92. package/src/docx-batch-insert.test.ts +117 -0
  93. package/src/docx-batch-insert.ts +223 -0
  94. package/src/docx-color-text.ts +154 -0
  95. package/src/docx-table-ops.test.ts +54 -0
  96. package/src/docx-table-ops.ts +316 -0
  97. package/src/docx-types.ts +39 -0
  98. package/src/docx.account-selection.test.ts +96 -0
  99. package/src/docx.test.ts +706 -0
  100. package/src/docx.ts +1598 -0
  101. package/src/drive-schema.ts +93 -0
  102. package/src/drive.test.ts +1240 -0
  103. package/src/drive.ts +830 -0
  104. package/src/dynamic-agent.test.ts +156 -0
  105. package/src/dynamic-agent.ts +144 -0
  106. package/src/event-types.ts +46 -0
  107. package/src/external-keys.test.ts +21 -0
  108. package/src/external-keys.ts +20 -0
  109. package/src/lifecycle.test-support.ts +223 -0
  110. package/src/media.test.ts +956 -0
  111. package/src/media.ts +1106 -0
  112. package/src/mention-target.types.ts +6 -0
  113. package/src/mention.ts +115 -0
  114. package/src/message-action-contract.ts +14 -0
  115. package/src/monitor-state-runtime-api.ts +8 -0
  116. package/src/monitor-transport-runtime-api.ts +11 -0
  117. package/src/monitor.account.ts +501 -0
  118. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +215 -0
  119. package/src/monitor.bot-identity.ts +87 -0
  120. package/src/monitor.bot-menu-handler.ts +164 -0
  121. package/src/monitor.bot-menu.lifecycle.test-support.ts +221 -0
  122. package/src/monitor.bot-menu.test.ts +200 -0
  123. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +265 -0
  124. package/src/monitor.card-action.lifecycle.test-support.ts +418 -0
  125. package/src/monitor.cleanup.test.ts +384 -0
  126. package/src/monitor.comment-notice-handler.ts +106 -0
  127. package/src/monitor.comment.test.ts +968 -0
  128. package/src/monitor.comment.ts +1386 -0
  129. package/src/monitor.lifecycle.test.ts +5 -0
  130. package/src/monitor.message-handler.ts +346 -0
  131. package/src/monitor.reaction.test.ts +770 -0
  132. package/src/monitor.startup.test.ts +232 -0
  133. package/src/monitor.startup.ts +76 -0
  134. package/src/monitor.state.defaults.test.ts +47 -0
  135. package/src/monitor.state.ts +171 -0
  136. package/src/monitor.synthetic-error.ts +19 -0
  137. package/src/monitor.test-mocks.ts +47 -0
  138. package/src/monitor.transport.ts +451 -0
  139. package/src/monitor.ts +104 -0
  140. package/src/monitor.webhook-e2e.test.ts +284 -0
  141. package/src/monitor.webhook-security.test.ts +394 -0
  142. package/src/monitor.webhook.test-helpers.ts +138 -0
  143. package/src/outbound-runtime-api.ts +2 -0
  144. package/src/outbound.test.ts +1255 -0
  145. package/src/outbound.ts +742 -0
  146. package/src/perm-schema.ts +53 -0
  147. package/src/perm.ts +171 -0
  148. package/src/pins.ts +109 -0
  149. package/src/policy.test.ts +224 -0
  150. package/src/policy.ts +322 -0
  151. package/src/post.test.ts +106 -0
  152. package/src/post.ts +276 -0
  153. package/src/presentation-card.ts +204 -0
  154. package/src/probe.test.ts +310 -0
  155. package/src/probe.ts +181 -0
  156. package/src/processing-claims.ts +60 -0
  157. package/src/qr-terminal.ts +2 -0
  158. package/src/reactions.ts +124 -0
  159. package/src/reasoning-preview.test.ts +114 -0
  160. package/src/reasoning-preview.ts +29 -0
  161. package/src/reply-dispatcher-runtime-api.ts +8 -0
  162. package/src/reply-dispatcher.test.ts +2009 -0
  163. package/src/reply-dispatcher.ts +865 -0
  164. package/src/runtime.ts +10 -0
  165. package/src/secret-contract.ts +146 -0
  166. package/src/secret-input.ts +2 -0
  167. package/src/security-audit-shared.ts +70 -0
  168. package/src/security-audit.test.ts +60 -0
  169. package/src/security-audit.ts +2 -0
  170. package/src/send-result.ts +81 -0
  171. package/src/send-target.test.ts +87 -0
  172. package/src/send-target.ts +36 -0
  173. package/src/send.reply-fallback.test.ts +418 -0
  174. package/src/send.test.ts +661 -0
  175. package/src/send.ts +860 -0
  176. package/src/sequential-key.test.ts +73 -0
  177. package/src/sequential-key.ts +29 -0
  178. package/src/sequential-queue.test.ts +184 -0
  179. package/src/sequential-queue.ts +90 -0
  180. package/src/session-conversation.ts +42 -0
  181. package/src/session-route.ts +49 -0
  182. package/src/setup-core.ts +52 -0
  183. package/src/setup-surface.test.ts +485 -0
  184. package/src/setup-surface.ts +620 -0
  185. package/src/streaming-card.test.ts +549 -0
  186. package/src/streaming-card.ts +611 -0
  187. package/src/subagent-hooks.test.ts +632 -0
  188. package/src/subagent-hooks.ts +414 -0
  189. package/src/targets.ts +98 -0
  190. package/src/test-support/lifecycle-test-support.ts +459 -0
  191. package/src/thread-bindings.test.ts +181 -0
  192. package/src/thread-bindings.ts +332 -0
  193. package/src/tool-account-routing.test.ts +419 -0
  194. package/src/tool-account.test.ts +45 -0
  195. package/src/tool-account.ts +98 -0
  196. package/src/tool-factory-test-harness.ts +83 -0
  197. package/src/tool-result.test.ts +33 -0
  198. package/src/tool-result.ts +17 -0
  199. package/src/tools-config.test.ts +52 -0
  200. package/src/tools-config.ts +29 -0
  201. package/src/types.ts +111 -0
  202. package/src/typing.test.ts +145 -0
  203. package/src/typing.ts +215 -0
  204. package/src/wiki-schema.ts +70 -0
  205. package/src/wiki.ts +271 -0
  206. package/subagent-hooks-api.ts +22 -0
  207. package/tsconfig.json +16 -0
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: feishu-wiki
3
+ description: |
4
+ Feishu knowledge base navigation. Activate when user mentions knowledge base, wiki, or wiki links.
5
+ ---
6
+
7
+ # Feishu Wiki Tool
8
+
9
+ Single tool `feishu_wiki` for knowledge base operations.
10
+
11
+ Wiki `space_id` values are opaque strings. Always keep them quoted in tool calls, even when they contain only digits; passing a long numeric-looking ID as a number can corrupt the suffix due to JavaScript number precision limits.
12
+
13
+ ## Token Extraction
14
+
15
+ From URL `https://xxx.feishu.cn/wiki/ABC123def` → `token` = `ABC123def`
16
+
17
+ ## Actions
18
+
19
+ ### List Knowledge Spaces
20
+
21
+ ```json
22
+ { "action": "spaces" }
23
+ ```
24
+
25
+ Returns all accessible wiki spaces.
26
+
27
+ ### List Nodes
28
+
29
+ ```json
30
+ { "action": "nodes", "space_id": "7xxx" }
31
+ ```
32
+
33
+ With parent:
34
+
35
+ ```json
36
+ { "action": "nodes", "space_id": "7xxx", "parent_node_token": "wikcnXXX" }
37
+ ```
38
+
39
+ ### Get Node Details
40
+
41
+ ```json
42
+ { "action": "get", "token": "ABC123def" }
43
+ ```
44
+
45
+ Returns: `node_token`, `obj_token`, `obj_type`, etc. Use `obj_token` with `feishu_doc` to read/write the document.
46
+
47
+ ### Create Node
48
+
49
+ ```json
50
+ { "action": "create", "space_id": "7xxx", "title": "New Page" }
51
+ ```
52
+
53
+ With type and parent:
54
+
55
+ ```json
56
+ {
57
+ "action": "create",
58
+ "space_id": "7xxx",
59
+ "title": "Sheet",
60
+ "obj_type": "sheet",
61
+ "parent_node_token": "wikcnXXX"
62
+ }
63
+ ```
64
+
65
+ `obj_type`: `docx` (default), `sheet`, `bitable`, `mindnote`, `file`, `doc`, `slides`
66
+
67
+ ### Move Node
68
+
69
+ ```json
70
+ { "action": "move", "space_id": "7xxx", "node_token": "wikcnXXX" }
71
+ ```
72
+
73
+ To different location:
74
+
75
+ ```json
76
+ {
77
+ "action": "move",
78
+ "space_id": "7xxx",
79
+ "node_token": "wikcnXXX",
80
+ "target_space_id": "7yyy",
81
+ "target_parent_token": "wikcnYYY"
82
+ }
83
+ ```
84
+
85
+ ### Rename Node
86
+
87
+ ```json
88
+ { "action": "rename", "space_id": "7xxx", "node_token": "wikcnXXX", "title": "New Title" }
89
+ ```
90
+
91
+ ## Wiki-Doc Workflow
92
+
93
+ To edit a wiki page:
94
+
95
+ 1. Get node: `{ "action": "get", "token": "wiki_token" }` → returns `obj_token`
96
+ 2. Read doc: `feishu_doc { "action": "read", "doc_token": "obj_token" }`
97
+ 3. Write doc: `feishu_doc { "action": "write", "doc_token": "obj_token", "content": "..." }`
98
+
99
+ ## Configuration
100
+
101
+ ```yaml
102
+ channels:
103
+ feishu:
104
+ tools:
105
+ wiki: true # default: true
106
+ doc: true # required - wiki content uses feishu_doc
107
+ ```
108
+
109
+ **Dependency:** This tool requires `feishu_doc` to be enabled. Wiki pages are documents - use `feishu_wiki` to navigate, then `feishu_doc` to read/edit content.
110
+
111
+ ## Permissions
112
+
113
+ Required: `wiki:wiki` or `wiki:wiki:readonly`
@@ -0,0 +1,481 @@
1
+ // Feishu tests cover accounts plugin behavior.
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ FeishuSecretRefUnavailableError,
5
+ inspectFeishuCredentials,
6
+ listFeishuAccountIds,
7
+ resolveDefaultFeishuAccountId,
8
+ resolveDefaultFeishuAccountSelection,
9
+ resolveFeishuAccount,
10
+ resolveFeishuCredentials,
11
+ resolveFeishuRuntimeAccount,
12
+ } from "./accounts.js";
13
+ import type { FeishuConfig } from "./types.js";
14
+
15
+ function makeDefaultAndRouterAccounts() {
16
+ return {
17
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
18
+ "router-d": { appId: "cli_router", appSecret: "secret_router" }, // pragma: allowlist secret
19
+ };
20
+ }
21
+
22
+ function expectExplicitDefaultAccountSelection(
23
+ account: ReturnType<typeof resolveFeishuAccount>,
24
+ appId: string,
25
+ ) {
26
+ expect(account.accountId).toBe("router-d");
27
+ expect(account.selectionSource).toBe("explicit-default");
28
+ expect(account.configured).toBe(true);
29
+ expect(account.appId).toBe(appId);
30
+ }
31
+
32
+ function withEnvVar(key: string, value: string | undefined, run: () => void) {
33
+ const prev = process.env[key];
34
+ if (value === undefined) {
35
+ delete process.env[key];
36
+ } else {
37
+ process.env[key] = value;
38
+ }
39
+ try {
40
+ run();
41
+ } finally {
42
+ if (prev === undefined) {
43
+ delete process.env[key];
44
+ } else {
45
+ process.env[key] = prev;
46
+ }
47
+ }
48
+ }
49
+
50
+ function asConfig(config: Partial<FeishuConfig>): FeishuConfig {
51
+ return config as unknown as FeishuConfig;
52
+ }
53
+
54
+ function expectUnresolvedEnvSecretRefError(key: string) {
55
+ expect(() =>
56
+ resolveFeishuCredentials(
57
+ asConfig({
58
+ appId: "cli_123",
59
+ appSecret: { source: "env", provider: "default", id: key } as never,
60
+ }),
61
+ ),
62
+ ).toThrow(/unresolved SecretRef/i);
63
+ }
64
+
65
+ describe("resolveDefaultFeishuAccountId", () => {
66
+ it("preserves top-level default account when named accounts are configured", () => {
67
+ const cfg = {
68
+ channels: {
69
+ feishu: {
70
+ appId: "cli_default",
71
+ appSecret: "secret_default",
72
+ accounts: {
73
+ work: { enabled: false },
74
+ },
75
+ },
76
+ },
77
+ };
78
+
79
+ expect(listFeishuAccountIds(cfg as never)).toEqual(["default", "work"]);
80
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("default");
81
+ });
82
+
83
+ it("prefers channels.feishu.defaultAccount when configured", () => {
84
+ const cfg = {
85
+ channels: {
86
+ feishu: {
87
+ defaultAccount: "router-d",
88
+ accounts: makeDefaultAndRouterAccounts(),
89
+ },
90
+ },
91
+ };
92
+
93
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
94
+ });
95
+
96
+ it("normalizes configured defaultAccount before lookup", () => {
97
+ const cfg = {
98
+ channels: {
99
+ feishu: {
100
+ defaultAccount: "Router D",
101
+ accounts: {
102
+ "router-d": { appId: "cli_router", appSecret: "secret_router" }, // pragma: allowlist secret
103
+ },
104
+ },
105
+ },
106
+ };
107
+
108
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
109
+ });
110
+
111
+ it("keeps configured defaultAccount even when not present in accounts map", () => {
112
+ const cfg = {
113
+ channels: {
114
+ feishu: {
115
+ defaultAccount: "router-d",
116
+ accounts: {
117
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
118
+ zeta: { appId: "cli_zeta", appSecret: "secret_zeta" }, // pragma: allowlist secret
119
+ },
120
+ },
121
+ },
122
+ };
123
+
124
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
125
+ });
126
+
127
+ it("falls back to literal default account id when present", () => {
128
+ const cfg = {
129
+ channels: {
130
+ feishu: {
131
+ accounts: {
132
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
133
+ zeta: { appId: "cli_zeta", appSecret: "secret_zeta" }, // pragma: allowlist secret
134
+ },
135
+ },
136
+ },
137
+ };
138
+
139
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("default");
140
+ });
141
+
142
+ it("reports selection source for configured defaults and mapped defaults", () => {
143
+ const explicitDefaultCfg = {
144
+ channels: {
145
+ feishu: {
146
+ defaultAccount: "router-d",
147
+ accounts: {},
148
+ },
149
+ },
150
+ };
151
+ expect(resolveDefaultFeishuAccountSelection(explicitDefaultCfg as never)).toEqual({
152
+ accountId: "router-d",
153
+ source: "explicit-default",
154
+ });
155
+
156
+ const mappedDefaultCfg = {
157
+ channels: {
158
+ feishu: {
159
+ accounts: {
160
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
161
+ },
162
+ },
163
+ },
164
+ };
165
+ expect(resolveDefaultFeishuAccountSelection(mappedDefaultCfg as never)).toEqual({
166
+ accountId: "default",
167
+ source: "mapped-default",
168
+ });
169
+ });
170
+ });
171
+
172
+ describe("resolveFeishuCredentials", () => {
173
+ it("throws unresolved SecretRef errors by default for unsupported secret sources", () => {
174
+ expect(() =>
175
+ resolveFeishuCredentials(
176
+ asConfig({
177
+ appId: "cli_123",
178
+ appSecret: { source: "file", provider: "default", id: "path/to/secret" } as never,
179
+ }),
180
+ ),
181
+ ).toThrow(/unresolved SecretRef/i);
182
+ });
183
+
184
+ it("returns null (without throwing) when unresolved SecretRef is allowed", () => {
185
+ const creds = resolveFeishuCredentials(
186
+ asConfig({
187
+ appId: "cli_123",
188
+ appSecret: { source: "file", provider: "default", id: "path/to/secret" } as never,
189
+ }),
190
+ { allowUnresolvedSecretRef: true },
191
+ );
192
+
193
+ expect(creds).toBeNull();
194
+ });
195
+
196
+ it("supports explicit inspect mode for unresolved SecretRefs", () => {
197
+ const creds = resolveFeishuCredentials(
198
+ asConfig({
199
+ appId: "cli_123",
200
+ appSecret: { source: "file", provider: "default", id: "path/to/secret" } as never,
201
+ }),
202
+ { mode: "inspect" },
203
+ );
204
+
205
+ expect(creds).toBeNull();
206
+ });
207
+
208
+ it("throws unresolved SecretRef error when env SecretRef points to missing env var", () => {
209
+ const key = "FEISHU_APP_SECRET_MISSING_TEST";
210
+ withEnvVar(key, undefined, () => {
211
+ expectUnresolvedEnvSecretRefError(key);
212
+ });
213
+ });
214
+
215
+ it("resolves env SecretRef objects when unresolved refs are allowed", () => {
216
+ const key = "FEISHU_APP_SECRET_TEST";
217
+ const prev = process.env[key];
218
+ process.env[key] = " secret_from_env ";
219
+
220
+ try {
221
+ const creds = resolveFeishuCredentials(
222
+ asConfig({
223
+ appId: "cli_123",
224
+ appSecret: { source: "env", provider: "default", id: key } as never,
225
+ }),
226
+ { allowUnresolvedSecretRef: true },
227
+ );
228
+
229
+ expect(creds).toEqual({
230
+ appId: "cli_123",
231
+ appSecret: "secret_from_env", // pragma: allowlist secret
232
+ encryptKey: undefined,
233
+ verificationToken: undefined,
234
+ domain: "feishu",
235
+ });
236
+ } finally {
237
+ if (prev === undefined) {
238
+ delete process.env[key];
239
+ } else {
240
+ process.env[key] = prev;
241
+ }
242
+ }
243
+ });
244
+
245
+ it("resolves env SecretRef with custom provider alias when unresolved refs are allowed", () => {
246
+ const key = "FEISHU_APP_SECRET_CUSTOM_PROVIDER_TEST";
247
+ const prev = process.env[key];
248
+ process.env[key] = " secret_from_env_alias ";
249
+
250
+ try {
251
+ const creds = resolveFeishuCredentials(
252
+ asConfig({
253
+ appId: "cli_123",
254
+ appSecret: { source: "env", provider: "corp-env", id: key } as never,
255
+ }),
256
+ { allowUnresolvedSecretRef: true },
257
+ );
258
+
259
+ expect(creds?.appSecret).toBe("secret_from_env_alias");
260
+ } finally {
261
+ if (prev === undefined) {
262
+ delete process.env[key];
263
+ } else {
264
+ process.env[key] = prev;
265
+ }
266
+ }
267
+ });
268
+
269
+ it("preserves unresolved SecretRef diagnostics for env refs in default mode", () => {
270
+ const key = "FEISHU_APP_SECRET_POLICY_TEST";
271
+ withEnvVar(key, "secret_from_env", () => {
272
+ expectUnresolvedEnvSecretRefError(key);
273
+ });
274
+ });
275
+
276
+ it("trims and returns credentials when values are valid strings", () => {
277
+ const creds = resolveFeishuCredentials(
278
+ asConfig({
279
+ appId: " cli_123 ",
280
+ appSecret: " secret_456 ",
281
+ encryptKey: " enc ",
282
+ verificationToken: " vt ",
283
+ }),
284
+ );
285
+
286
+ expect(creds).toEqual({
287
+ appId: "cli_123",
288
+ appSecret: "secret_456", // pragma: allowlist secret
289
+ encryptKey: "enc",
290
+ verificationToken: "vt",
291
+ domain: "feishu",
292
+ });
293
+ });
294
+
295
+ it("does not resolve encryptKey SecretRefs outside webhook mode", () => {
296
+ const creds = resolveFeishuCredentials(
297
+ asConfig({
298
+ connectionMode: "websocket",
299
+ appId: "cli_123",
300
+ appSecret: "secret_456",
301
+ encryptKey: { source: "file", provider: "default", id: "path/to/secret" } as never,
302
+ }),
303
+ );
304
+
305
+ expect(creds).toEqual({
306
+ appId: "cli_123",
307
+ appSecret: "secret_456", // pragma: allowlist secret
308
+ encryptKey: undefined,
309
+ verificationToken: undefined,
310
+ domain: "feishu",
311
+ });
312
+ });
313
+
314
+ it("keeps required credentials when optional event SecretRefs are unresolved in inspect mode", () => {
315
+ const creds = inspectFeishuCredentials(
316
+ asConfig({
317
+ appId: "cli_123",
318
+ appSecret: "secret_456",
319
+ verificationToken: { source: "file", provider: "default", id: "path/to/token" } as never,
320
+ }),
321
+ );
322
+
323
+ expect(creds).toEqual({
324
+ appId: "cli_123",
325
+ appSecret: "secret_456", // pragma: allowlist secret
326
+ encryptKey: undefined,
327
+ verificationToken: undefined,
328
+ domain: "feishu",
329
+ });
330
+ });
331
+ });
332
+
333
+ describe("resolveFeishuAccount", () => {
334
+ it("uses top-level credentials with configured default account id even without account map entry", () => {
335
+ const cfg = {
336
+ channels: {
337
+ feishu: {
338
+ defaultAccount: "router-d",
339
+ appId: "top_level_app",
340
+ appSecret: "top_level_secret", // pragma: allowlist secret
341
+ accounts: {
342
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
343
+ },
344
+ },
345
+ },
346
+ };
347
+
348
+ const account = resolveFeishuAccount({ cfg: cfg as never, accountId: undefined });
349
+ expectExplicitDefaultAccountSelection(account, "top_level_app");
350
+ });
351
+
352
+ it("uses configured default account when accountId is omitted", () => {
353
+ const cfg = {
354
+ channels: {
355
+ feishu: {
356
+ defaultAccount: "router-d",
357
+ accounts: {
358
+ default: { enabled: true },
359
+ "router-d": { appId: "cli_router", appSecret: "secret_router", enabled: true }, // pragma: allowlist secret
360
+ },
361
+ },
362
+ },
363
+ };
364
+
365
+ const account = resolveFeishuAccount({ cfg: cfg as never, accountId: undefined });
366
+ expectExplicitDefaultAccountSelection(account, "cli_router");
367
+ });
368
+
369
+ it("keeps explicit accountId selection", () => {
370
+ const cfg = {
371
+ channels: {
372
+ feishu: {
373
+ defaultAccount: "router-d",
374
+ accounts: makeDefaultAndRouterAccounts(),
375
+ },
376
+ },
377
+ };
378
+
379
+ const account = resolveFeishuAccount({ cfg: cfg as never, accountId: "default" });
380
+ expect(account.accountId).toBe("default");
381
+ expect(account.selectionSource).toBe("explicit");
382
+ expect(account.appId).toBe("cli_default");
383
+ });
384
+
385
+ it("treats unresolved SecretRef as not configured in account resolution", () => {
386
+ const account = resolveFeishuAccount({
387
+ cfg: {
388
+ channels: {
389
+ feishu: {
390
+ accounts: {
391
+ main: {
392
+ appId: "cli_123",
393
+ appSecret: { source: "file", provider: "default", id: "path/to/secret" },
394
+ } as never,
395
+ },
396
+ },
397
+ },
398
+ } as never,
399
+ accountId: "main",
400
+ });
401
+ expect(account.configured).toBe(false);
402
+ expect(account.appSecret).toBeUndefined();
403
+ });
404
+
405
+ it("keeps account configured when optional event SecretRefs are unresolved in inspect mode", () => {
406
+ const account = resolveFeishuAccount({
407
+ cfg: {
408
+ channels: {
409
+ feishu: {
410
+ accounts: {
411
+ main: {
412
+ appId: "cli_123",
413
+ appSecret: "secret_456",
414
+ verificationToken: {
415
+ source: "file",
416
+ provider: "default",
417
+ id: "path/to/token",
418
+ },
419
+ } as never,
420
+ },
421
+ },
422
+ },
423
+ } as never,
424
+ accountId: "main",
425
+ });
426
+
427
+ expect(account.configured).toBe(true);
428
+ expect(account.appSecret).toBe("secret_456");
429
+ expect(account.verificationToken).toBeUndefined();
430
+ });
431
+
432
+ it("throws typed SecretRef errors in runtime account resolution", () => {
433
+ let caught: unknown;
434
+ try {
435
+ resolveFeishuRuntimeAccount({
436
+ cfg: {
437
+ channels: {
438
+ feishu: {
439
+ accounts: {
440
+ main: {
441
+ appId: "cli_123",
442
+ appSecret: { source: "file", provider: "default", id: "path/to/secret" },
443
+ } as never,
444
+ },
445
+ },
446
+ },
447
+ } as never,
448
+ accountId: "main",
449
+ });
450
+ } catch (error) {
451
+ caught = error;
452
+ }
453
+
454
+ expect(caught).toBeInstanceOf(FeishuSecretRefUnavailableError);
455
+ expect((caught as Error).message).toMatch(/channels\.feishu\.appSecret: unresolved SecretRef/i);
456
+ });
457
+
458
+ it("ignores non-string account names", () => {
459
+ const account = resolveFeishuAccount({
460
+ cfg: {
461
+ channels: {
462
+ feishu: {
463
+ accounts: {
464
+ main: {
465
+ name: { bad: true },
466
+ appId: "cli_123",
467
+ appSecret: "secret_456", // pragma: allowlist secret
468
+ } as never,
469
+ },
470
+ },
471
+ },
472
+ } as never,
473
+ accountId: "main",
474
+ });
475
+
476
+ expect(account.accountId).toBe("main");
477
+ expect(account.appId).toBe("cli_123");
478
+ expect(account.appSecret).toBe("secret_456");
479
+ expect(account.name).toBeUndefined();
480
+ });
481
+ });