@openclaw/feishu 2026.3.13 → 2026.5.1-beta.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.
- package/api.ts +31 -0
- package/channel-entry.ts +20 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +16 -0
- package/index.ts +70 -53
- package/openclaw.plugin.json +1653 -4
- package/package.json +32 -7
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/security-contract-api.ts +1 -0
- package/session-key-api.ts +1 -0
- package/setup-api.ts +3 -0
- package/setup-entry.test.ts +14 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +95 -7
- package/src/accounts.ts +199 -117
- package/src/app-registration.ts +331 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +25 -0
- package/src/async.test.ts +35 -0
- package/src/async.ts +43 -1
- package/src/audio-preflight.runtime.ts +9 -0
- package/src/bitable.test.ts +131 -0
- package/src/bitable.ts +59 -22
- package/src/bot-content.ts +474 -0
- package/src/bot-group-name.test.ts +108 -0
- package/src/bot-runtime-api.ts +12 -0
- package/src/bot-sender-name.ts +125 -0
- package/src/bot.broadcast.test.ts +463 -0
- package/src/bot.card-action.test.ts +519 -5
- package/src/bot.checkBotMentioned.test.ts +92 -20
- package/src/bot.helpers.test.ts +118 -0
- package/src/bot.stripBotMention.test.ts +13 -21
- package/src/bot.test.ts +1334 -401
- package/src/bot.ts +778 -775
- package/src/card-action.ts +408 -40
- package/src/card-interaction.test.ts +129 -0
- package/src/card-interaction.ts +159 -0
- package/src/card-test-helpers.ts +47 -0
- package/src/card-ux-approval.ts +65 -0
- package/src/card-ux-launcher.test.ts +99 -0
- package/src/card-ux-launcher.ts +121 -0
- package/src/card-ux-shared.ts +33 -0
- package/src/channel-runtime-api.ts +16 -0
- package/src/channel.runtime.ts +47 -0
- package/src/channel.test.ts +914 -3
- package/src/channel.ts +1252 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +84 -28
- package/src/chat.ts +68 -10
- package/src/client.test.ts +212 -103
- package/src/client.ts +115 -21
- package/src/comment-dispatcher-runtime-api.ts +6 -0
- package/src/comment-dispatcher.test.ts +169 -0
- package/src/comment-dispatcher.ts +107 -0
- package/src/comment-handler-runtime-api.ts +3 -0
- package/src/comment-handler.test.ts +486 -0
- package/src/comment-handler.ts +309 -0
- package/src/comment-reaction.test.ts +166 -0
- package/src/comment-reaction.ts +259 -0
- package/src/comment-shared.test.ts +182 -0
- package/src/comment-shared.ts +365 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +63 -1
- package/src/config-schema.ts +31 -4
- package/src/conversation-id.test.ts +18 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-runtime-api.ts +1 -0
- package/src/dedup.ts +32 -94
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +119 -20
- package/src/directory.ts +61 -91
- package/src/doc-schema.ts +1 -1
- package/src/docx-batch-insert.test.ts +39 -38
- package/src/docx-batch-insert.ts +55 -19
- package/src/docx-color-text.ts +9 -4
- package/src/docx-table-ops.test.ts +53 -0
- package/src/docx-table-ops.ts +52 -34
- package/src/docx-types.ts +38 -0
- package/src/docx.account-selection.test.ts +12 -3
- package/src/docx.test.ts +314 -74
- package/src/docx.ts +278 -122
- package/src/drive-schema.ts +47 -1
- package/src/drive.test.ts +1219 -0
- package/src/drive.ts +614 -13
- package/src/dynamic-agent.ts +10 -4
- package/src/event-types.ts +45 -0
- package/src/external-keys.ts +1 -1
- package/src/lifecycle.test-support.ts +220 -0
- package/src/media.test.ts +375 -26
- package/src/media.ts +434 -88
- package/src/mention-target.types.ts +5 -0
- package/src/mention.ts +32 -51
- package/src/message-action-contract.ts +13 -0
- package/src/monitor-state-runtime-api.ts +7 -0
- package/src/monitor-transport-runtime-api.ts +7 -0
- package/src/monitor.account.ts +218 -312
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
- package/src/monitor.bot-identity.ts +86 -0
- package/src/monitor.bot-menu-handler.ts +165 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
- package/src/monitor.bot-menu.test.ts +178 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
- package/src/monitor.cleanup.test.ts +376 -0
- package/src/monitor.comment-notice-handler.ts +105 -0
- package/src/monitor.comment.test.ts +937 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.lifecycle.test.ts +4 -0
- package/src/monitor.message-handler.ts +339 -0
- package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
- package/src/monitor.reaction.test.ts +108 -48
- package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
- package/src/monitor.startup.test.ts +11 -9
- package/src/monitor.startup.ts +26 -16
- package/src/monitor.state.ts +20 -5
- package/src/monitor.synthetic-error.ts +18 -0
- package/src/monitor.test-mocks.ts +2 -2
- package/src/monitor.transport.ts +220 -60
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +65 -7
- package/src/monitor.webhook-security.test.ts +122 -0
- package/src/monitor.webhook.test-helpers.ts +44 -26
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +616 -37
- package/src/outbound.ts +623 -81
- package/src/perm-schema.ts +1 -1
- package/src/perm.ts +1 -7
- package/src/pins.ts +108 -0
- package/src/policy.test.ts +297 -117
- package/src/policy.ts +142 -29
- package/src/post.ts +7 -6
- package/src/probe.test.ts +14 -9
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +4 -34
- package/src/reasoning-preview.test.ts +59 -0
- package/src/reasoning-preview.ts +20 -0
- package/src/reply-dispatcher-runtime-api.ts +7 -0
- package/src/reply-dispatcher.test.ts +660 -29
- package/src/reply-dispatcher.ts +407 -154
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +145 -0
- package/src/secret-input.ts +1 -13
- package/src/security-audit-shared.ts +69 -0
- package/src/security-audit.test.ts +61 -0
- package/src/security-audit.ts +1 -0
- package/src/send-result.ts +1 -1
- package/src/send-target.test.ts +9 -3
- package/src/send-target.ts +10 -4
- package/src/send.reply-fallback.test.ts +77 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +399 -86
- package/src/sequential-key.test.ts +72 -0
- package/src/sequential-key.ts +28 -0
- package/src/sequential-queue.test.ts +92 -0
- package/src/sequential-queue.ts +16 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +48 -0
- package/src/setup-core.ts +51 -0
- package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
- package/src/setup-surface.ts +581 -0
- package/src/streaming-card.test.ts +138 -2
- package/src/streaming-card.ts +134 -18
- package/src/subagent-hooks.test.ts +603 -0
- package/src/subagent-hooks.ts +397 -0
- package/src/targets.ts +3 -13
- package/src/test-support/lifecycle-test-support.ts +479 -0
- package/src/thread-bindings.test.ts +143 -0
- package/src/thread-bindings.ts +330 -0
- package/src/tool-account-routing.test.ts +66 -8
- package/src/tool-account.test.ts +44 -0
- package/src/tool-account.ts +40 -17
- package/src/tool-factory-test-harness.ts +11 -8
- package/src/tool-result.ts +3 -1
- package/src/tools-config.ts +1 -1
- package/src/types.ts +16 -15
- package/src/typing.ts +10 -6
- package/src/wiki-schema.ts +1 -1
- package/src/wiki.ts +1 -7
- package/subagent-hooks-api.ts +31 -0
- package/tsconfig.json +16 -0
- package/src/feishu-command-handler.ts +0 -59
- package/src/onboarding.status.test.ts +0 -25
- package/src/onboarding.ts +0 -489
- package/src/send-message.ts +0 -71
- package/src/targets.test.ts +0 -70
package/src/perm-schema.ts
CHANGED
package/src/perm.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
-
import type { OpenClawPluginApi } from "
|
|
2
|
+
import type { OpenClawPluginApi } from "../runtime-api.js";
|
|
3
3
|
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
4
4
|
import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js";
|
|
5
5
|
import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
|
|
@@ -114,19 +114,16 @@ async function removeMember(
|
|
|
114
114
|
|
|
115
115
|
export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
|
116
116
|
if (!api.config) {
|
|
117
|
-
api.logger.debug?.("feishu_perm: No config available, skipping perm tools");
|
|
118
117
|
return;
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
const accounts = listEnabledFeishuAccounts(api.config);
|
|
122
121
|
if (accounts.length === 0) {
|
|
123
|
-
api.logger.debug?.("feishu_perm: No Feishu accounts configured, skipping perm tools");
|
|
124
122
|
return;
|
|
125
123
|
}
|
|
126
124
|
|
|
127
125
|
const toolsCfg = resolveAnyEnabledFeishuToolsConfig(accounts);
|
|
128
126
|
if (!toolsCfg.perm) {
|
|
129
|
-
api.logger.debug?.("feishu_perm: perm tool disabled in config (default: false)");
|
|
130
127
|
return;
|
|
131
128
|
}
|
|
132
129
|
|
|
@@ -160,7 +157,6 @@ export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
|
|
160
157
|
await removeMember(client, p.token, p.type, p.member_type, p.member_id),
|
|
161
158
|
);
|
|
162
159
|
default:
|
|
163
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
|
|
164
160
|
return unknownToolActionResult((p as { action?: unknown }).action);
|
|
165
161
|
}
|
|
166
162
|
} catch (err) {
|
|
@@ -171,6 +167,4 @@ export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
|
|
171
167
|
},
|
|
172
168
|
{ name: "feishu_perm" },
|
|
173
169
|
);
|
|
174
|
-
|
|
175
|
-
api.logger.info?.(`feishu_perm: Registered feishu_perm tool`);
|
|
176
170
|
}
|
package/src/pins.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "../runtime-api.js";
|
|
2
|
+
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
|
3
|
+
import { createFeishuClient } from "./client.js";
|
|
4
|
+
|
|
5
|
+
type FeishuPin = {
|
|
6
|
+
messageId: string;
|
|
7
|
+
chatId?: string;
|
|
8
|
+
operatorId?: string;
|
|
9
|
+
operatorIdType?: string;
|
|
10
|
+
createTime?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function assertFeishuPinApiSuccess(response: { code?: number; msg?: string }, action: string) {
|
|
14
|
+
if (response.code !== 0) {
|
|
15
|
+
throw new Error(`Feishu ${action} failed: ${response.msg || `code ${response.code}`}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizePin(pin: {
|
|
20
|
+
message_id: string;
|
|
21
|
+
chat_id?: string;
|
|
22
|
+
operator_id?: string;
|
|
23
|
+
operator_id_type?: string;
|
|
24
|
+
create_time?: string;
|
|
25
|
+
}): FeishuPin {
|
|
26
|
+
return {
|
|
27
|
+
messageId: pin.message_id,
|
|
28
|
+
chatId: pin.chat_id,
|
|
29
|
+
operatorId: pin.operator_id,
|
|
30
|
+
operatorIdType: pin.operator_id_type,
|
|
31
|
+
createTime: pin.create_time,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function createPinFeishu(params: {
|
|
36
|
+
cfg: ClawdbotConfig;
|
|
37
|
+
messageId: string;
|
|
38
|
+
accountId?: string;
|
|
39
|
+
}): Promise<FeishuPin | null> {
|
|
40
|
+
const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
41
|
+
if (!account.configured) {
|
|
42
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const client = createFeishuClient(account);
|
|
46
|
+
const response = await client.im.pin.create({
|
|
47
|
+
data: {
|
|
48
|
+
message_id: params.messageId,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
assertFeishuPinApiSuccess(response, "pin create");
|
|
52
|
+
return response.data?.pin ? normalizePin(response.data.pin) : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function removePinFeishu(params: {
|
|
56
|
+
cfg: ClawdbotConfig;
|
|
57
|
+
messageId: string;
|
|
58
|
+
accountId?: string;
|
|
59
|
+
}): Promise<void> {
|
|
60
|
+
const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
61
|
+
if (!account.configured) {
|
|
62
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const client = createFeishuClient(account);
|
|
66
|
+
const response = await client.im.pin.delete({
|
|
67
|
+
path: {
|
|
68
|
+
message_id: params.messageId,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
assertFeishuPinApiSuccess(response, "pin delete");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function listPinsFeishu(params: {
|
|
75
|
+
cfg: ClawdbotConfig;
|
|
76
|
+
chatId: string;
|
|
77
|
+
startTime?: string;
|
|
78
|
+
endTime?: string;
|
|
79
|
+
pageSize?: number;
|
|
80
|
+
pageToken?: string;
|
|
81
|
+
accountId?: string;
|
|
82
|
+
}): Promise<{ chatId: string; pins: FeishuPin[]; hasMore: boolean; pageToken?: string }> {
|
|
83
|
+
const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
84
|
+
if (!account.configured) {
|
|
85
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const client = createFeishuClient(account);
|
|
89
|
+
const response = await client.im.pin.list({
|
|
90
|
+
params: {
|
|
91
|
+
chat_id: params.chatId,
|
|
92
|
+
...(params.startTime ? { start_time: params.startTime } : {}),
|
|
93
|
+
...(params.endTime ? { end_time: params.endTime } : {}),
|
|
94
|
+
...(typeof params.pageSize === "number"
|
|
95
|
+
? { page_size: Math.max(1, Math.min(100, Math.floor(params.pageSize))) }
|
|
96
|
+
: {}),
|
|
97
|
+
...(params.pageToken ? { page_token: params.pageToken } : {}),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
assertFeishuPinApiSuccess(response, "pin list");
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
chatId: params.chatId,
|
|
104
|
+
pins: (response.data?.items ?? []).map(normalizePin),
|
|
105
|
+
hasMore: response.data?.has_more === true,
|
|
106
|
+
pageToken: response.data?.page_token,
|
|
107
|
+
};
|
|
108
|
+
}
|
package/src/policy.test.ts
CHANGED
|
@@ -1,154 +1,334 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { FeishuConfigSchema } from "./config-schema.js";
|
|
2
4
|
import {
|
|
5
|
+
hasExplicitFeishuGroupConfig,
|
|
3
6
|
isFeishuGroupAllowed,
|
|
4
7
|
resolveFeishuAllowlistMatch,
|
|
5
8
|
resolveFeishuGroupConfig,
|
|
9
|
+
resolveFeishuReplyPolicy,
|
|
6
10
|
} from "./policy.js";
|
|
7
11
|
import type { FeishuConfig } from "./types.js";
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
},
|
|
17
|
-
} as unknown as FeishuConfig;
|
|
18
|
-
|
|
19
|
-
const resolved = resolveFeishuGroupConfig({
|
|
20
|
-
cfg,
|
|
21
|
-
groupId: "oc-missing",
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
expect(resolved).toEqual({ requireMention: false });
|
|
25
|
-
});
|
|
13
|
+
function createCfg(feishu: Record<string, unknown>): OpenClawConfig {
|
|
14
|
+
return {
|
|
15
|
+
channels: {
|
|
16
|
+
feishu,
|
|
17
|
+
},
|
|
18
|
+
} as OpenClawConfig;
|
|
19
|
+
}
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"*": { requireMention: false },
|
|
31
|
-
"oc-explicit": { requireMention: true },
|
|
32
|
-
},
|
|
33
|
-
} as unknown as FeishuConfig;
|
|
21
|
+
function createFeishuConfig(overrides: Partial<FeishuConfig>): FeishuConfig {
|
|
22
|
+
return FeishuConfigSchema.parse(overrides);
|
|
23
|
+
}
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
describe("resolveFeishuReplyPolicy", () => {
|
|
26
|
+
it("defaults open groups to no mention when unset", () => {
|
|
27
|
+
expect(
|
|
28
|
+
resolveFeishuReplyPolicy({
|
|
29
|
+
isDirectMessage: false,
|
|
30
|
+
cfg: createCfg({ groupPolicy: "open" }),
|
|
31
|
+
groupPolicy: "open",
|
|
32
|
+
groupId: "oc_1",
|
|
33
|
+
}),
|
|
34
|
+
).toEqual({ requireMention: false });
|
|
35
|
+
});
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
it("keeps explicit top-level mention gating in open groups", () => {
|
|
38
|
+
expect(
|
|
39
|
+
resolveFeishuReplyPolicy({
|
|
40
|
+
isDirectMessage: false,
|
|
41
|
+
cfg: createCfg({ groupPolicy: "open", requireMention: true }),
|
|
42
|
+
groupPolicy: "open",
|
|
43
|
+
groupId: "oc_1",
|
|
44
|
+
}),
|
|
45
|
+
).toEqual({ requireMention: true });
|
|
46
|
+
});
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
it("keeps explicit account mention gating in open groups", () => {
|
|
49
|
+
expect(
|
|
50
|
+
resolveFeishuReplyPolicy({
|
|
51
|
+
isDirectMessage: false,
|
|
52
|
+
cfg: createCfg({
|
|
53
|
+
groupPolicy: "allowlist",
|
|
54
|
+
requireMention: false,
|
|
55
|
+
accounts: {
|
|
56
|
+
work: {
|
|
57
|
+
groupPolicy: "open",
|
|
58
|
+
requireMention: true,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
accountId: "work",
|
|
63
|
+
groupPolicy: "open",
|
|
64
|
+
groupId: "oc_1",
|
|
65
|
+
}),
|
|
66
|
+
).toEqual({ requireMention: true });
|
|
67
|
+
});
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
it("keeps explicit per-group mention gating in open groups", () => {
|
|
70
|
+
expect(
|
|
71
|
+
resolveFeishuReplyPolicy({
|
|
72
|
+
isDirectMessage: false,
|
|
73
|
+
cfg: createCfg({
|
|
74
|
+
groupPolicy: "open",
|
|
75
|
+
groups: { oc_1: { requireMention: true } },
|
|
76
|
+
}),
|
|
77
|
+
groupPolicy: "open",
|
|
78
|
+
groupId: "oc_1",
|
|
79
|
+
}),
|
|
80
|
+
).toEqual({ requireMention: true });
|
|
81
|
+
});
|
|
55
82
|
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
it("defaults allowlist groups to require mentions", () => {
|
|
84
|
+
expect(
|
|
85
|
+
resolveFeishuReplyPolicy({
|
|
86
|
+
isDirectMessage: false,
|
|
87
|
+
cfg: createCfg({ groupPolicy: "allowlist" }),
|
|
88
|
+
groupPolicy: "allowlist",
|
|
89
|
+
groupId: "oc_1",
|
|
90
|
+
}),
|
|
91
|
+
).toEqual({ requireMention: true });
|
|
58
92
|
});
|
|
93
|
+
});
|
|
59
94
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
).toEqual({ allowed: true, matchKey: "*", matchSource: "wildcard" });
|
|
95
|
+
describe("resolveFeishuGroupConfig", () => {
|
|
96
|
+
it("falls back to wildcard group config when direct match is missing", () => {
|
|
97
|
+
const cfg = createFeishuConfig({
|
|
98
|
+
groups: {
|
|
99
|
+
"*": { requireMention: false },
|
|
100
|
+
"oc-explicit": { requireMention: true },
|
|
101
|
+
},
|
|
68
102
|
});
|
|
69
103
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
allowFrom: ["feishu:user:OU_ALLOWED"],
|
|
74
|
-
senderId: "ou_allowed",
|
|
75
|
-
}),
|
|
76
|
-
).toEqual({ allowed: true, matchKey: "ou_allowed", matchSource: "id" });
|
|
104
|
+
const resolved = resolveFeishuGroupConfig({
|
|
105
|
+
cfg,
|
|
106
|
+
groupId: "oc-missing",
|
|
77
107
|
});
|
|
78
108
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
resolveFeishuAllowlistMatch({
|
|
82
|
-
allowFrom: ["on_user_123"],
|
|
83
|
-
senderId: "ou_other",
|
|
84
|
-
senderIds: ["on_user_123"],
|
|
85
|
-
}),
|
|
86
|
-
).toEqual({ allowed: true, matchKey: "on_user_123", matchSource: "id" });
|
|
87
|
-
});
|
|
109
|
+
expect(resolved).toEqual({ requireMention: false });
|
|
110
|
+
});
|
|
88
111
|
|
|
89
|
-
|
|
90
|
-
|
|
112
|
+
it("prefers exact group config over wildcard", () => {
|
|
113
|
+
const cfg = createFeishuConfig({
|
|
114
|
+
groups: {
|
|
115
|
+
"*": { requireMention: false },
|
|
116
|
+
"oc-explicit": { requireMention: true },
|
|
117
|
+
},
|
|
118
|
+
});
|
|
91
119
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
senderId: "ou_attacker_real_open_id",
|
|
96
|
-
senderIds: ["on_attacker_user_id"],
|
|
97
|
-
senderName: victimOpenId,
|
|
98
|
-
}),
|
|
99
|
-
).toEqual({ allowed: false });
|
|
120
|
+
const resolved = resolveFeishuGroupConfig({
|
|
121
|
+
cfg,
|
|
122
|
+
groupId: "oc-explicit",
|
|
100
123
|
});
|
|
124
|
+
|
|
125
|
+
expect(resolved).toEqual({ requireMention: true });
|
|
101
126
|
});
|
|
102
127
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
senderId: "oc_group_123",
|
|
110
|
-
}),
|
|
111
|
-
).toBe(true);
|
|
128
|
+
it("keeps case-insensitive matching for explicit group ids", () => {
|
|
129
|
+
const cfg = createFeishuConfig({
|
|
130
|
+
groups: {
|
|
131
|
+
"*": { requireMention: false },
|
|
132
|
+
OC_UPPER: { requireMention: true },
|
|
133
|
+
},
|
|
112
134
|
});
|
|
113
135
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
groupPolicy: "open",
|
|
118
|
-
allowFrom: [],
|
|
119
|
-
senderId: "oc_group_999",
|
|
120
|
-
}),
|
|
121
|
-
).toBe(true);
|
|
136
|
+
const resolved = resolveFeishuGroupConfig({
|
|
137
|
+
cfg,
|
|
138
|
+
groupId: "oc_upper",
|
|
122
139
|
});
|
|
123
140
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
expect(resolved).toEqual({ requireMention: true });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("hasExplicitFeishuGroupConfig", () => {
|
|
146
|
+
it("matches direct and case-insensitive group ids", () => {
|
|
147
|
+
const cfg = createFeishuConfig({
|
|
148
|
+
groups: {
|
|
149
|
+
OC_UPPER: { requireMention: true },
|
|
150
|
+
},
|
|
132
151
|
});
|
|
133
152
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
expect(hasExplicitFeishuGroupConfig({ cfg, groupId: "OC_UPPER" })).toBe(true);
|
|
154
|
+
expect(hasExplicitFeishuGroupConfig({ cfg, groupId: "oc_upper" })).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("does not treat wildcard group defaults as explicit admission", () => {
|
|
158
|
+
const cfg = createFeishuConfig({
|
|
159
|
+
groups: {
|
|
160
|
+
"*": { requireMention: false },
|
|
161
|
+
},
|
|
142
162
|
});
|
|
143
163
|
|
|
144
|
-
|
|
164
|
+
expect(hasExplicitFeishuGroupConfig({ cfg, groupId: "oc_any" })).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("resolveFeishuAllowlistMatch", () => {
|
|
169
|
+
it("allows wildcard", () => {
|
|
170
|
+
expect(
|
|
171
|
+
resolveFeishuAllowlistMatch({
|
|
172
|
+
allowFrom: ["*"],
|
|
173
|
+
senderId: "ou-attacker",
|
|
174
|
+
}),
|
|
175
|
+
).toEqual({ allowed: true, matchKey: "*", matchSource: "wildcard" });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("allows provider-prefixed wildcard entries", () => {
|
|
179
|
+
expect(
|
|
180
|
+
resolveFeishuAllowlistMatch({
|
|
181
|
+
allowFrom: ["feishu:*", "lark:*"],
|
|
182
|
+
senderId: "ou_anyone",
|
|
183
|
+
}),
|
|
184
|
+
).toEqual({ allowed: true, matchKey: "*", matchSource: "wildcard" });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("treats typed wildcard aliases as bare wildcards", () => {
|
|
188
|
+
for (const wildcard of [
|
|
189
|
+
"chat:*",
|
|
190
|
+
"group:*",
|
|
191
|
+
"channel:*",
|
|
192
|
+
"user:*",
|
|
193
|
+
"dm:*",
|
|
194
|
+
"open_id:*",
|
|
195
|
+
"feishu:user:*",
|
|
196
|
+
]) {
|
|
145
197
|
expect(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
senderId: "oc_group_999",
|
|
198
|
+
resolveFeishuAllowlistMatch({
|
|
199
|
+
allowFrom: [wildcard],
|
|
200
|
+
senderId: "ou_anyone",
|
|
150
201
|
}),
|
|
151
|
-
).
|
|
152
|
-
}
|
|
202
|
+
).toEqual({ allowed: true, matchKey: "*", matchSource: "wildcard" });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("matches normalized ID entries", () => {
|
|
207
|
+
expect(
|
|
208
|
+
resolveFeishuAllowlistMatch({
|
|
209
|
+
allowFrom: ["feishu:user:ou_ALLOWED"],
|
|
210
|
+
senderId: "ou_ALLOWED",
|
|
211
|
+
}),
|
|
212
|
+
).toEqual({ allowed: true, matchKey: "user:ou_ALLOWED", matchSource: "id" });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("accepts repeated provider prefixes for legacy allowlist entries", () => {
|
|
216
|
+
expect(
|
|
217
|
+
resolveFeishuAllowlistMatch({
|
|
218
|
+
allowFrom: ["feishu:feishu:user:ou_ALLOWED"],
|
|
219
|
+
senderId: "ou_ALLOWED",
|
|
220
|
+
}),
|
|
221
|
+
).toEqual({ allowed: true, matchKey: "user:ou_ALLOWED", matchSource: "id" });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("does not fold opaque IDs to lowercase", () => {
|
|
225
|
+
expect(
|
|
226
|
+
resolveFeishuAllowlistMatch({
|
|
227
|
+
allowFrom: ["user:OU_ALLOWED"],
|
|
228
|
+
senderId: "ou_ALLOWED",
|
|
229
|
+
}),
|
|
230
|
+
).toEqual({ allowed: false });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("keeps user and chat allowlist namespaces distinct", () => {
|
|
234
|
+
expect(
|
|
235
|
+
resolveFeishuAllowlistMatch({
|
|
236
|
+
allowFrom: ["user:oc_group_123"],
|
|
237
|
+
senderId: "oc_group_123",
|
|
238
|
+
}),
|
|
239
|
+
).toEqual({ allowed: false });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("supports user_id as an additional immutable sender candidate", () => {
|
|
243
|
+
expect(
|
|
244
|
+
resolveFeishuAllowlistMatch({
|
|
245
|
+
allowFrom: ["on_user_123"],
|
|
246
|
+
senderId: "ou_other",
|
|
247
|
+
senderIds: ["on_user_123"],
|
|
248
|
+
}),
|
|
249
|
+
).toEqual({ allowed: true, matchKey: "user:on_user_123", matchSource: "id" });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("auto-detects bare open_id entries as user allowlist matches", () => {
|
|
253
|
+
expect(
|
|
254
|
+
resolveFeishuAllowlistMatch({
|
|
255
|
+
allowFrom: ["ou_BARE"],
|
|
256
|
+
senderId: "ou_BARE",
|
|
257
|
+
}),
|
|
258
|
+
).toEqual({ allowed: true, matchKey: "user:ou_BARE", matchSource: "id" });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("auto-detects bare chat_id entries as chat allowlist matches", () => {
|
|
262
|
+
expect(
|
|
263
|
+
resolveFeishuAllowlistMatch({
|
|
264
|
+
allowFrom: ["oc_group_123"],
|
|
265
|
+
senderId: "oc_group_123",
|
|
266
|
+
}),
|
|
267
|
+
).toEqual({ allowed: true, matchKey: "chat:oc_group_123", matchSource: "id" });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("does not authorize based on display-name collision", () => {
|
|
271
|
+
const victimOpenId = "ou_4f4ec5aa111122223333444455556666";
|
|
272
|
+
|
|
273
|
+
expect(
|
|
274
|
+
resolveFeishuAllowlistMatch({
|
|
275
|
+
allowFrom: [victimOpenId],
|
|
276
|
+
senderId: "ou_attacker_real_open_id",
|
|
277
|
+
senderIds: ["on_attacker_user_id"],
|
|
278
|
+
senderName: victimOpenId,
|
|
279
|
+
}),
|
|
280
|
+
).toEqual({ allowed: false });
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("isFeishuGroupAllowed", () => {
|
|
285
|
+
it("matches group IDs with chat: prefix", () => {
|
|
286
|
+
expect(
|
|
287
|
+
isFeishuGroupAllowed({
|
|
288
|
+
groupPolicy: "allowlist",
|
|
289
|
+
allowFrom: ["chat:oc_group_123"],
|
|
290
|
+
senderId: "oc_group_123",
|
|
291
|
+
}),
|
|
292
|
+
).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("allows group when groupPolicy is 'open'", () => {
|
|
296
|
+
expect(
|
|
297
|
+
isFeishuGroupAllowed({
|
|
298
|
+
groupPolicy: "open",
|
|
299
|
+
allowFrom: [],
|
|
300
|
+
senderId: "oc_group_999",
|
|
301
|
+
}),
|
|
302
|
+
).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("treats 'allowall' as equivalent to 'open'", () => {
|
|
306
|
+
expect(
|
|
307
|
+
isFeishuGroupAllowed({
|
|
308
|
+
groupPolicy: "allowall",
|
|
309
|
+
allowFrom: [],
|
|
310
|
+
senderId: "oc_group_999",
|
|
311
|
+
}),
|
|
312
|
+
).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("rejects group when groupPolicy is 'disabled'", () => {
|
|
316
|
+
expect(
|
|
317
|
+
isFeishuGroupAllowed({
|
|
318
|
+
groupPolicy: "disabled",
|
|
319
|
+
allowFrom: ["oc_group_999"],
|
|
320
|
+
senderId: "oc_group_999",
|
|
321
|
+
}),
|
|
322
|
+
).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("rejects group when groupPolicy is 'allowlist' and allowFrom is empty", () => {
|
|
326
|
+
expect(
|
|
327
|
+
isFeishuGroupAllowed({
|
|
328
|
+
groupPolicy: "allowlist",
|
|
329
|
+
allowFrom: [],
|
|
330
|
+
senderId: "oc_group_999",
|
|
331
|
+
}),
|
|
332
|
+
).toBe(false);
|
|
153
333
|
});
|
|
154
334
|
});
|