@zhin.js/adapter-lark 1.1.26 → 1.1.28

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 (39) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/LICENSE +21 -0
  3. package/PERMITS.md +11 -11
  4. package/README.md +7 -3
  5. package/adapters/lark/index.js +3 -0
  6. package/adapters/lark/index.ts +3 -0
  7. package/agents/lark/conversation.js +15 -0
  8. package/agents/lark/conversation.ts +17 -0
  9. package/agents/lark/messages.js +4 -0
  10. package/agents/lark/messages.ts +4 -0
  11. package/agents/lark/skills/lark/tools/upload_file/index.js +3 -1
  12. package/agents/lark/skills/lark/tools/upload_file/index.ts +3 -1
  13. package/agents/lark/skills/lark-group-admin/tools/add_members/index.js +5 -6
  14. package/agents/lark/skills/lark-group-admin/tools/add_members/index.ts +6 -7
  15. package/agents/lark/skills/lark-group-admin/tools/dissolve_chat/index.js +6 -6
  16. package/agents/lark/skills/lark-group-admin/tools/dissolve_chat/index.ts +7 -7
  17. package/agents/lark/skills/lark-group-admin/tools/remove_managers/index.js +5 -6
  18. package/agents/lark/skills/lark-group-admin/tools/remove_managers/index.ts +6 -7
  19. package/agents/lark/skills/lark-group-admin/tools/set_managers/index.js +5 -6
  20. package/agents/lark/skills/lark-group-admin/tools/set_managers/index.ts +6 -7
  21. package/agents/lark/skills/lark-group-admin/tools/update_chat/index.js +5 -6
  22. package/agents/lark/skills/lark-group-admin/tools/update_chat/index.ts +6 -7
  23. package/lib/client.js +49 -16
  24. package/lib/index.d.ts +2 -2
  25. package/lib/index.js +2 -2
  26. package/lib/platform-permit.d.ts +4 -11
  27. package/lib/platform-permit.js +7 -33
  28. package/lib/protocol.d.ts +10 -5
  29. package/lib/protocol.js +17 -20
  30. package/lib/webhook.d.ts +2 -3
  31. package/lib/webhook.js +35 -28
  32. package/package.json +25 -26
  33. package/plugin.js +0 -6
  34. package/schema.json +0 -56
  35. package/src/client.ts +44 -19
  36. package/src/index.ts +2 -7
  37. package/src/platform-permit.ts +5 -45
  38. package/src/protocol.ts +21 -24
  39. package/src/webhook.ts +38 -33
@@ -1,22 +1,21 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { messages as t } from "../../../../messages.js";
3
+ import { currentChat } from "../../../../conversation.js";
3
4
  import { defineAgentTool } from '@zhin.js/tool';
4
5
  import { z } from 'zod';
5
- import { platformPermission } from '@zhin.js/permission';
6
6
  export default defineAgentTool({
7
- description: '更新飞书群聊信息(群名、描述)',
7
+ description: '更新当前飞书群聊信息(群名、描述)',
8
8
  inputSchema: z.object({
9
- chat_id: z.string().describe('群聊 ID'),
10
9
  name: z.string().optional().describe('新群名(可选)'),
11
10
  description: z.string().optional().describe('新描述(可选)'),
12
11
  }),
13
12
  adapter: 'lark',
14
13
  scopes: ["group"],
15
14
  tags: ['lark'],
16
- permissions: [platformPermission('lark', 'chat_admin')],
17
- async execute({ chat_id, name, description }, context) {
15
+ permissions: ['platform:admin', 'bot:admin'],
16
+ async execute({ name, description }, context) {
18
17
  const endpoint = context.$client;
19
- const success = await endpoint.updateChatInfo(chat_id, { name, description });
18
+ const success = await endpoint.updateChatInfo(currentChat(context), { name, description });
20
19
  return { success, message: success ? t(context.locale, 'chatInfoUpdated') : t(context.locale, 'updateFailed') };
21
20
  },
22
21
  });
@@ -1,22 +1,21 @@
1
1
  import { messages as t } from "../../../../messages.js";
2
+ import { currentChat } from "../../../../conversation.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
- import { platformPermission } from '@zhin.js/permission';
5
5
 
6
- export default defineAgentTool<{ chat_id: string; name?: string; description?: string }>({
7
- description: '更新飞书群聊信息(群名、描述)',
6
+ export default defineAgentTool<{ name?: string; description?: string }>({
7
+ description: '更新当前飞书群聊信息(群名、描述)',
8
8
  inputSchema: z.object({
9
- chat_id: z.string().describe('群聊 ID'),
10
9
  name: z.string().optional().describe('新群名(可选)'),
11
10
  description: z.string().optional().describe('新描述(可选)'),
12
11
  }),
13
12
  adapter: 'lark',
14
13
  scopes: ["group"],
15
14
  tags: ['lark'],
16
- permissions: [platformPermission('lark', 'chat_admin')],
17
- async execute({ chat_id, name, description }: { chat_id: string; name?: string; description?: string }, context) {
15
+ permissions: ['platform:admin', 'bot:admin'],
16
+ async execute({ name, description }, context) {
18
17
  const endpoint = context.$client;
19
- const success = await endpoint.updateChatInfo(chat_id, { name, description });
18
+ const success = await endpoint.updateChatInfo(currentChat(context), { name, description });
20
19
  return { success, message: success ? t(context.locale, 'chatInfoUpdated') : t(context.locale, 'updateFailed') };
21
20
  },
22
21
  });
package/lib/client.js CHANGED
@@ -11,7 +11,6 @@ import { createEndpointLifecycle, createEndpointOperation, createRecallEndpointC
11
11
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
12
12
  import { buildImageUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
13
13
  import { formatInboundContent, formatOutboundBody, generateMessageId, isLarkBotMentioned, larkInboundConversation, resolveChatType, resolveSender, } from './protocol.js';
14
- import { normalizeLarkSenderForPermit } from './platform-permit.js';
15
14
  import { registerLarkWebhookRoutes } from './webhook.js';
16
15
  import { createLarkLongConnection } from './long-connection.js';
17
16
  import { openLarkReplyStream, supportsLarkReplyStream } from './reply-stream.js';
@@ -19,6 +18,7 @@ import { openLarkReplyStream, supportsLarkReplyStream } from './reply-stream.js'
19
18
  const OUTBOUND_TIMEOUT_MS = 30_000;
20
19
  /** Feishu/Lark OpenAPI client surface carried by every Endpoint event. */
21
20
  const LARK_CHAT_MANAGER_CACHE_TTL_MS = 60_000;
21
+ const LARK_CHAT_MANAGER_FAILURE_TTL_MS = 10_000;
22
22
  export class LarkClient extends SdkClient {
23
23
  #logger;
24
24
  #options;
@@ -47,6 +47,27 @@ export class LarkClient extends SdkClient {
47
47
  return groups;
48
48
  },
49
49
  listGroupMembers: ({ groupId, signal }) => this.#listAllPages(`/im/v1/chats/${encodeURIComponent(groupId)}/members`, { params: { member_id_type: 'open_id' }, signal }),
50
+ /**
51
+ * ADR 0056 §1:机器人自身在群里的地位(chat.get:群主为本应用 → owner;在 bot_manager_id_list →
52
+ * admin;有 owner_id 且都不是 → member)。查询失败或缺 owner_id 为未知。
53
+ */
54
+ inspectSelfStanding: async ({ conversation, signal }) => {
55
+ if (conversation.kind !== 'group')
56
+ return undefined;
57
+ const appId = this.#options.config.appId;
58
+ const data = await createEndpointOperation({ signal }).run(() => this.#request(`/im/v1/chats/${encodeURIComponent(conversation.id)}`, { method: 'GET' }));
59
+ const info = data?.data;
60
+ if (!info || !appId)
61
+ return undefined;
62
+ if (info.owner_id_type === 'app_id' && info.owner_id === appId)
63
+ return { standing: 'owner' };
64
+ const managers = Array.isArray(info.bot_manager_id_list) ? info.bot_manager_id_list.map(String) : [];
65
+ if (managers.includes(appId))
66
+ return { standing: 'admin' };
67
+ // chat.get may omit owner_id (e.g. when this app owns the group): without it the Bot is not
68
+ // provably a plain member, so the standing stays unknown.
69
+ return typeof info.owner_id === 'string' && info.owner_id ? { standing: 'member' } : undefined;
70
+ },
50
71
  });
51
72
  endpointControl = createRecallEndpointControl((id, signal) => this.recallMessage(id, { signal }));
52
73
  get endpointStreaming() {
@@ -65,6 +86,8 @@ export class LarkClient extends SdkClient {
65
86
  #chatManagers = new TtlLookupCache({
66
87
  ttlMs: LARK_CHAT_MANAGER_CACHE_TTL_MS,
67
88
  });
89
+ /** chat.get failures: the standing stays unknown for a short while instead of retrying every message. */
90
+ #chatManagerFailures = new TtlLookupCache({ ttlMs: LARK_CHAT_MANAGER_FAILURE_TTL_MS });
68
91
  #startupGeneration = 0;
69
92
  #pendingInbound = [];
70
93
  #bindSignal;
@@ -313,11 +336,10 @@ export class LarkClient extends SdkClient {
313
336
  async #emitInbound(msg, conversation, nativeEvent) {
314
337
  const chatType = resolveChatType(msg.chat_id, msg.chat_type);
315
338
  const senderId = resolveSender(msg);
316
- const permit = senderId && chatType === 'group' && msg.chat_id
317
- ? normalizeLarkSenderForPermit(await this.#resolveChatRole(msg.chat_id, senderId))
339
+ // ADR 0056 §1:群主 / 群管理员 / 成员 → sender.platform.standing(飞书群无原生角色与权限位);未知不上报。
340
+ const role = senderId && chatType === 'group' && msg.chat_id
341
+ ? await this.#resolveChatRole(msg.chat_id, senderId)
318
342
  : undefined;
319
- // 规范角色 owner / admin 进 roles;细粒度能力放 senderPermissions
320
- const role = permit?.role === 'owner' || permit?.role === 'admin' ? permit.role : undefined;
321
343
  const mentioned = isLarkBotMentioned(msg);
322
344
  const replyToId = msg.parent_id || msg.root_id;
323
345
  await this.publishEvent('message.receive', {
@@ -325,32 +347,43 @@ export class LarkClient extends SdkClient {
325
347
  conversation,
326
348
  message: { conversation, id: generateMessageId(msg) },
327
349
  content: formatInboundContent(msg),
328
- ...(senderId ? { sender: { id: senderId, ...(role ? { roles: [role] } : {}) } } : {}),
350
+ ...(senderId ? { sender: { id: senderId, ...(role ? { platform: { standing: role } } : {}) } } : {}),
329
351
  endpointId: this.#options.config.id,
330
352
  ...(mentioned ? { mentioned: true } : {}),
331
353
  ...(replyToId ? { replyTo: { id: replyToId } } : {}),
332
354
  metadata: Object.freeze({
333
355
  messageType: msg.message_type,
334
356
  chatType,
335
- ...(role ? { senderRole: role } : {}),
336
- ...(role && permit?.permissions?.length ? { senderPermissions: [...permit.permissions] } : {}),
337
357
  }),
338
358
  });
339
359
  }
340
- /** 群主 / 群管理员来自 chat.get(owner_id + 管理员列表),按群缓存;失败时保守地视为普通成员。 */
360
+ /**
361
+ * 群主 / 群管理员来自 chat.get(owner_id + 管理员列表),按群缓存 60 秒。chat.get 失败时地位未知
362
+ * (负缓存 10 秒,不报确定的 member);未返回 owner_id(如群主是机器人应用)时只能确认管理员,
363
+ * 其余发送者地位未知。
364
+ */
341
365
  async #resolveChatRole(chatId, senderId) {
366
+ if (this.#chatManagerFailures.get(chatId))
367
+ return undefined;
342
368
  const entry = await this.#chatManagers.getOrLoad(chatId, async () => {
343
369
  const info = await this.getChatInfo(chatId);
344
- const admins = Array.isArray(info?.user_manager_id_list) ? info.user_manager_id_list.map(String) : [];
370
+ if (!info) {
371
+ this.#chatManagerFailures.set(chatId, true);
372
+ return undefined;
373
+ }
374
+ const admins = Array.isArray(info.user_manager_id_list) ? info.user_manager_id_list.map(String) : [];
345
375
  return {
346
- ...(typeof info?.owner_id === 'string' && info.owner_id ? { owner: info.owner_id } : {}),
376
+ ...(typeof info.owner_id === 'string' && info.owner_id ? { owner: info.owner_id } : {}),
347
377
  admins: new Set(admins),
348
378
  };
349
- });
350
- return {
351
- isOwner: entry.owner !== undefined && entry.owner === senderId,
352
- isAdmin: entry.admins.has(senderId),
353
- };
379
+ }, { cacheWhen: entry => entry !== undefined });
380
+ if (!entry)
381
+ return undefined;
382
+ if (entry.owner !== undefined && entry.owner === senderId)
383
+ return 'owner';
384
+ if (entry.admins.has(senderId))
385
+ return 'admin';
386
+ return entry.owner !== undefined ? 'member' : undefined;
354
387
  }
355
388
  /** Only correlate callbacks to a card actually sent by this endpoint. */
356
389
  admitCardAction(event) {
package/lib/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { formatInboundContent, formatOutboundBody, generateMessageId, headerValue, larkInboundConversation, normalizeWebhookPath, readTextBody, resolveChatType, resolveLarkConfig, resolveSender, verifySignature, type AccessToken, type LarkEndpointConfig, type LarkApiResponse, type LarkEventBody, type LarkMessage, type LarkSendBody, type LarkWireSegment, type ResolvedLarkConfig, } from './protocol.js';
1
+ export { formatInboundContent, formatOutboundBody, generateMessageId, headerValue, larkInboundConversation, normalizeWebhookPath, resolveChatType, resolveLarkConfig, resolveSender, decryptLarkEvent, verifySignature, type AccessToken, type LarkEndpointConfig, type LarkApiResponse, type LarkEventBody, type LarkMessage, type LarkSendBody, type LarkWireSegment, type ResolvedLarkConfig, } from './protocol.js';
2
2
  export { type LarkClientEventMap } from './client.js';
3
- export { checkLarkPlatformPermit, larkGroupPermitResolver, normalizeLarkSenderForPermit, platformPermit, } from './platform-permit.js';
3
+ export { platformPermit } from './platform-permit.js';
4
4
  export { LarkEndpoint, type LarkEndpointOptions, type LarkFetch, } from './endpoint.js';
5
5
  export { buildImageUploadForm, readOutboundImageMedia, resolveMediaBinary, type MediaBinary, } from './media-upload.js';
6
6
  export { registerLarkWebhookRoutes, handleLarkWebhookRequest, type LarkWebhookHandler, } from './webhook.js';
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export { formatInboundContent, formatOutboundBody, generateMessageId, headerValue, larkInboundConversation, normalizeWebhookPath, readTextBody, resolveChatType, resolveLarkConfig, resolveSender, verifySignature, } from './protocol.js';
2
- export { checkLarkPlatformPermit, larkGroupPermitResolver, normalizeLarkSenderForPermit, platformPermit, } from './platform-permit.js';
1
+ export { formatInboundContent, formatOutboundBody, generateMessageId, headerValue, larkInboundConversation, normalizeWebhookPath, resolveChatType, resolveLarkConfig, resolveSender, decryptLarkEvent, verifySignature, } from './protocol.js';
2
+ export { platformPermit } from './platform-permit.js';
3
3
  export { LarkEndpoint, } from './endpoint.js';
4
4
  export { buildImageUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
5
5
  export { registerLarkWebhookRoutes, handleLarkWebhookRequest, } from './webhook.js';
@@ -1,14 +1,7 @@
1
1
  /**
2
- * 飞书 Lark platform permit
2
+ * 飞书 Lark platform permit helper(ADR 0056 §5)。
3
+ *
4
+ * 平台身份由适配器上报到 `sender.platform`,permit 由内置评估器(`@zhin.js/permission`)判断;
5
+ * 适配器不再导出自有的 permit 解析器或检查函数。
3
6
  */
4
- import type { PermissionSubject } from '@zhin.js/permission';
5
7
  export declare function platformPermit(perm: string): string;
6
- export declare function larkGroupPermitResolver(logicalPerm: string): string;
7
- export declare function normalizeLarkSenderForPermit(input: {
8
- isOwner?: boolean;
9
- isAdmin?: boolean;
10
- }): {
11
- role?: string;
12
- permissions?: string[];
13
- };
14
- export declare function checkLarkPlatformPermit(perm: string, subject: PermissionSubject): boolean;
@@ -1,35 +1,9 @@
1
- const ADAPTER = 'lark';
1
+ /**
2
+ * 飞书 Lark platform permit helper(ADR 0056 §5)。
3
+ *
4
+ * 平台身份由适配器上报到 `sender.platform`,permit 由内置评估器(`@zhin.js/permission`)判断;
5
+ * 适配器不再导出自有的 permit 解析器或检查函数。
6
+ */
2
7
  export function platformPermit(perm) {
3
- return `platform(${ADAPTER},${perm})`;
4
- }
5
- const FACTORY_PERM_MAP = {
6
- scene_admin: 'chat_admin',
7
- scene_owner: 'chat_owner',
8
- };
9
- export function larkGroupPermitResolver(logicalPerm) {
10
- return platformPermit(FACTORY_PERM_MAP[logicalPerm] ?? logicalPerm);
11
- }
12
- export function normalizeLarkSenderForPermit(input) {
13
- if (input.isOwner) {
14
- return { role: 'owner', permissions: ['chat_owner', 'chat_admin', 'manage_managers'] };
15
- }
16
- if (input.isAdmin) {
17
- return { role: 'admin', permissions: ['chat_admin'] };
18
- }
19
- return { role: 'member', permissions: [] };
20
- }
21
- export function checkLarkPlatformPermit(perm, subject) {
22
- const role = subject.sender?.role?.[0];
23
- const permissions = subject.sender?.permissions ?? [];
24
- const has = (t) => permissions.includes(t);
25
- switch (perm) {
26
- case 'chat_owner':
27
- return role === 'owner' || has('chat_owner');
28
- case 'chat_admin':
29
- return role === 'owner' || role === 'admin' || has('chat_admin') || has('chat_owner');
30
- case 'manage_managers':
31
- return role === 'owner' || has('chat_owner');
32
- default:
33
- return false;
34
- }
8
+ return `platform-permission:${perm}`;
35
9
  }
package/lib/protocol.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
2
2
  import type { Locale } from 'zhin.js/i18n';
3
- import type { IncomingMessage } from 'node:http';
4
3
  import { type ConversationRef } from '@zhin.js/im-contract';
5
4
  /** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
6
5
  export interface LarkEndpointConfig extends EndpointProfileConfiguration {
@@ -72,6 +71,8 @@ export interface LarkEventBody {
72
71
  readonly ts?: string;
73
72
  readonly type?: string;
74
73
  readonly challenge?: string;
74
+ /** Present instead of every other field when the app has an Encrypt Key. */
75
+ readonly encrypt?: string;
75
76
  readonly event?: {
76
77
  readonly sender?: LarkMessage['sender'];
77
78
  readonly message?: LarkMessage;
@@ -118,6 +119,13 @@ export declare function formatInboundContent(msg: LarkMessage): string;
118
119
  /** 验签时间戳时效窗口(防重放)。 */
119
120
  export declare const LARK_SIGNATURE_MAX_AGE_MS: number;
120
121
  export declare function verifySignature(encryptKey: string, timestamp: string, nonce: string, body: string, signature: string, nowMs?: number): boolean;
122
+ /**
123
+ * Decrypt an `{"encrypt": "..."}` webhook payload (Lark/Feishu open-platform
124
+ * spec): key = SHA-256(Encrypt Key), base64 payload = 16-byte IV followed by
125
+ * AES-256-CBC ciphertext with PKCS#7 padding. Throws when the payload cannot
126
+ * be decrypted with this key.
127
+ */
128
+ export declare function decryptLarkEvent(encryptKey: string, encrypted: string): string;
121
129
  /** timing-safe 比较 webhook verification token(长度不同快速失败)。 */
122
130
  export declare function verifyToken(token: string, expected: string): boolean;
123
131
  /**
@@ -125,7 +133,4 @@ export declare function verifyToken(token: string, expected: string): boolean;
125
133
  * Segment canonicalization is intentionally not done here.
126
134
  */
127
135
  export declare function formatOutboundBody(payload: unknown, locale?: Locale): LarkSendBody;
128
- export declare function headerValue(headers: IncomingMessage['headers'], name: string): string;
129
- export declare function readTextBody(request: IncomingMessage, options?: {
130
- readonly limit?: number;
131
- }): Promise<string>;
136
+ export declare function headerValue(headers: Headers, name: string): string;
package/lib/protocol.js CHANGED
@@ -3,7 +3,7 @@ import { validateLarkWebApiProxy } from './web-api-proxy.js';
3
3
  * Lark/Feishu protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
4
4
  * Canonicalization is owned by gateway/core before endpoint.send.
5
5
  */
6
- import { createHash, timingSafeEqual } from 'node:crypto';
6
+ import { createDecipheriv, createHash, timingSafeEqual } from 'node:crypto';
7
7
  import { EndpointDeliveryError, isMediaRef } from '@zhin.js/im-contract';
8
8
  import { formatCompact, getLogger } from '@zhin.js/logger';
9
9
  import { formatLarkCard } from './cards.js';
@@ -150,6 +150,21 @@ export function verifySignature(encryptKey, timestamp, nonce, body, signature, n
150
150
  return false;
151
151
  }
152
152
  }
153
+ /**
154
+ * Decrypt an `{"encrypt": "..."}` webhook payload (Lark/Feishu open-platform
155
+ * spec): key = SHA-256(Encrypt Key), base64 payload = 16-byte IV followed by
156
+ * AES-256-CBC ciphertext with PKCS#7 padding. Throws when the payload cannot
157
+ * be decrypted with this key.
158
+ */
159
+ export function decryptLarkEvent(encryptKey, encrypted) {
160
+ const payload = Buffer.from(encrypted, 'base64');
161
+ if (payload.length <= 16 || (payload.length - 16) % 16 !== 0) {
162
+ throw new Error('Malformed encrypted Lark event');
163
+ }
164
+ const key = createHash('sha256').update(encryptKey).digest();
165
+ const decipher = createDecipheriv('aes-256-cbc', key, payload.subarray(0, 16));
166
+ return Buffer.concat([decipher.update(payload.subarray(16)), decipher.final()]).toString('utf8');
167
+ }
153
168
  /** timing-safe 比较 webhook verification token(长度不同快速失败)。 */
154
169
  export function verifyToken(token, expected) {
155
170
  try {
@@ -323,23 +338,5 @@ export function formatOutboundBody(payload, locale) {
323
338
  };
324
339
  }
325
340
  export function headerValue(headers, name) {
326
- const value = headers[name] ?? headers[name.toLowerCase()];
327
- if (Array.isArray(value))
328
- return value[0] ?? '';
329
- return value ?? '';
330
- }
331
- export async function readTextBody(request, options = {}) {
332
- const limit = options.limit ?? 1_048_576;
333
- const chunks = [];
334
- let size = 0;
335
- for await (const chunk of request) {
336
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
337
- size += buffer.length;
338
- if (size > limit) {
339
- request.destroy();
340
- throw new Error(`Request body exceeds ${limit} bytes`);
341
- }
342
- chunks.push(buffer);
343
- }
344
- return Buffer.concat(chunks).toString('utf8');
341
+ return headers.get(name) ?? '';
345
342
  }
package/lib/webhook.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Lark/Feishu webhook HTTP: verification → signature → challenge/event → admit.
2
+ * Lark/Feishu webhook HTTP: signature → decrypt → verification token → challenge/event → admit.
3
3
  */
4
4
  import type { LarkCardAction } from './cards.js';
5
- import type { IncomingMessage, ServerResponse } from 'node:http';
6
5
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
7
6
  import { type LarkMessage, type ResolvedLarkConfig } from './protocol.js';
8
7
  export interface LarkWebhookHandler {
@@ -13,4 +12,4 @@ export interface LarkWebhookHandler {
13
12
  admitCardAction?(event: LarkCardAction): void;
14
13
  }
15
14
  export declare function registerLarkWebhookRoutes(http: HttpHost, handler: LarkWebhookHandler): HttpRouteRegistration[];
16
- export declare function handleLarkWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: LarkWebhookHandler): Promise<void>;
15
+ export declare function handleLarkWebhookRequest(request: Request, handler: LarkWebhookHandler): Promise<Response>;
package/lib/webhook.js CHANGED
@@ -1,15 +1,14 @@
1
+ import { readTextBody } from '@zhin.js/host-http-contract';
1
2
  import { formatCompact, getLogger } from '@zhin.js/logger';
2
- import { headerValue, readTextBody, verifySignature, verifyToken, } from './protocol.js';
3
+ import { decryptLarkEvent, headerValue, verifySignature, verifyToken, } from './protocol.js';
3
4
  const logger = getLogger('lark');
4
5
  export function registerLarkWebhookRoutes(http, handler) {
5
6
  const path = handler.config.webhookPath;
6
7
  return [
7
- http.route('POST', path, async (request, response) => {
8
- await handleLarkWebhookRequest(request, response, handler);
9
- }, { summary: 'Lark/Feishu event webhook', tags: ['lark'] }),
8
+ http.route('POST', path, (request) => handleLarkWebhookRequest(request, handler), { summary: 'Lark/Feishu event webhook', tags: ['lark'] }),
10
9
  ];
11
10
  }
12
- export async function handleLarkWebhookRequest(request, response, handler) {
11
+ export async function handleLarkWebhookRequest(request, handler) {
13
12
  try {
14
13
  const rawBody = await readTextBody(request);
15
14
  let event;
@@ -17,20 +16,9 @@ export async function handleLarkWebhookRequest(request, response, handler) {
17
16
  event = JSON.parse(rawBody);
18
17
  }
19
18
  catch {
20
- response.writeHead(200, { 'Content-Type': 'application/json' });
21
- response.end(JSON.stringify({ code: 0, msg: 'success' }));
22
- return;
23
- }
24
- if (handler.config.verificationToken) {
25
- const bodyToken = event.header?.token ?? event.token;
26
- const token = typeof bodyToken === 'string' ? bodyToken : headerValue(request.headers, 'x-lark-request-token');
27
- if (!verifyToken(token, handler.config.verificationToken)) {
28
- logger.warn(formatCompact({ op: 'webhook', ok: false, error: 'invalid verification token' }));
29
- response.writeHead(403, { 'Content-Type': 'text/plain' });
30
- response.end('Forbidden');
31
- return;
32
- }
19
+ return new Response(JSON.stringify({ code: 0, msg: 'success' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
33
20
  }
21
+ // Signature first: it covers the raw (possibly encrypted) body.
34
22
  if (handler.config.encryptKey) {
35
23
  const timestamp = headerValue(request.headers, 'x-lark-request-timestamp');
36
24
  const nonce = headerValue(request.headers, 'x-lark-request-nonce');
@@ -40,9 +28,32 @@ export async function handleLarkWebhookRequest(request, response, handler) {
40
28
  || !signature
41
29
  || !verifySignature(handler.config.encryptKey, timestamp, nonce, rawBody, signature)) {
42
30
  logger.warn(formatCompact({ op: 'webhook', ok: false, error: 'invalid signature' }));
43
- response.writeHead(403, { 'Content-Type': 'text/plain' });
44
- response.end('Forbidden');
45
- return;
31
+ return new Response('Forbidden', { status: 403, headers: { 'Content-Type': 'text/plain' } });
32
+ }
33
+ }
34
+ if (typeof event?.encrypt === 'string') {
35
+ if (!handler.config.encryptKey) {
36
+ logger.warn(formatCompact({ op: 'webhook', ok: false, error: 'encrypted event received but encryptKey is not configured' }));
37
+ return new Response(JSON.stringify({ code: -1, msg: 'Encrypt key is not configured' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
38
+ }
39
+ try {
40
+ event = JSON.parse(decryptLarkEvent(handler.config.encryptKey, event.encrypt));
41
+ }
42
+ catch {
43
+ logger.warn(formatCompact({ op: 'webhook', ok: false, error: 'event decryption failed' }));
44
+ return new Response(JSON.stringify({ code: -1, msg: 'Decryption failed' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
45
+ }
46
+ }
47
+ if (!event || typeof event !== 'object') {
48
+ return new Response(JSON.stringify({ code: 0, msg: 'success' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
49
+ }
50
+ // Token and challenge live inside the decrypted event.
51
+ if (handler.config.verificationToken) {
52
+ const bodyToken = event.header?.token ?? event.token;
53
+ const token = typeof bodyToken === 'string' ? bodyToken : headerValue(request.headers, 'x-lark-request-token');
54
+ if (!verifyToken(token, handler.config.verificationToken)) {
55
+ logger.warn(formatCompact({ op: 'webhook', ok: false, error: 'invalid verification token' }));
56
+ return new Response('Forbidden', { status: 403, headers: { 'Content-Type': 'text/plain' } });
46
57
  }
47
58
  }
48
59
  if (handler.isOpen) {
@@ -52,9 +63,7 @@ export async function handleLarkWebhookRequest(request, response, handler) {
52
63
  handler.admitPlatform(eventName, event);
53
64
  }
54
65
  if (event.type === 'url_verification') {
55
- response.writeHead(200, { 'Content-Type': 'application/json' });
56
- response.end(JSON.stringify({ challenge: event.challenge }));
57
- return;
66
+ return new Response(JSON.stringify({ challenge: event.challenge }), { status: 200, headers: { 'Content-Type': 'application/json' } });
58
67
  }
59
68
  const headerEventType = event.header?.event_type;
60
69
  if (headerEventType === 'card.action.trigger' && handler.isOpen)
@@ -66,12 +75,10 @@ export async function handleLarkWebhookRequest(request, response, handler) {
66
75
  };
67
76
  handler.admit(message, event);
68
77
  }
69
- response.writeHead(200, { 'Content-Type': 'application/json' });
70
- response.end(JSON.stringify({ code: 0, msg: 'success' }));
78
+ return new Response(JSON.stringify({ code: 0, msg: 'success' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
71
79
  }
72
80
  catch (error) {
73
81
  logger.error('Webhook error:', error);
74
- response.writeHead(500, { 'Content-Type': 'application/json' });
75
- response.end(JSON.stringify({ code: -1, msg: 'Internal Server Error' }));
82
+ return new Response(JSON.stringify({ code: -1, msg: 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
76
83
  }
77
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-lark",
3
- "version": "1.1.26",
3
+ "version": "1.1.28",
4
4
  "description": "Zhin.js Lark/Feishu adapter (HTTP webhook and WebSocket long connection)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -35,26 +35,27 @@
35
35
  "dependencies": {
36
36
  "@larksuiteoapi/node-sdk": "^1.67.0",
37
37
  "undici": "6.29.1",
38
- "@zhin.js/adapter": "1.1.32",
39
- "@zhin.js/agent-feature": "1.1.16",
40
- "@zhin.js/console-contract": "1.1.5",
41
- "@zhin.js/core": "1.1.60",
42
- "@zhin.js/feature-kit": "1.1.15",
43
- "@zhin.js/host-http": "1.1.17",
44
- "@zhin.js/im-contract": "1.1.13",
45
- "@zhin.js/logger": "1.1.2",
46
- "@zhin.js/page": "1.1.17",
47
- "@zhin.js/skill": "1.1.16",
48
- "@zhin.js/tool": "1.1.18"
38
+ "zod": "^4.6.7",
39
+ "@zhin.js/adapter": "1.1.33",
40
+ "@zhin.js/agent-feature": "1.1.17",
41
+ "@zhin.js/console-contract": "1.1.6",
42
+ "@zhin.js/core": "1.6.0",
43
+ "@zhin.js/feature-kit": "1.1.16",
44
+ "@zhin.js/host-http": "1.2.1",
45
+ "@zhin.js/host-http-contract": "1.2.0",
46
+ "@zhin.js/im-contract": "1.1.14",
47
+ "@zhin.js/logger": "1.1.3",
48
+ "@zhin.js/page": "1.1.18",
49
+ "@zhin.js/skill": "1.1.17",
50
+ "@zhin.js/tool": "1.1.19"
49
51
  },
50
52
  "peerDependencies": {
51
- "zod": "^4.0.0",
52
- "@zhin.js/adapter": "^1.1.32",
53
- "@zhin.js/command": "^1.1.17",
54
- "@zhin.js/core": "^1.1.60",
55
- "@zhin.js/host-http": "^1.1.17",
56
- "@zhin.js/permission": "^1.1.15",
57
- "zhin.js": "^1.1.25"
53
+ "@zhin.js/adapter": "^1.1.33",
54
+ "@zhin.js/command": "^1.2.1",
55
+ "@zhin.js/core": "^1.6.0",
56
+ "@zhin.js/host-http": "^1.2.1",
57
+ "@zhin.js/permission": "^1.2.0",
58
+ "zhin.js": "^1.1.27"
58
59
  },
59
60
  "peerDependenciesMeta": {
60
61
  "@zhin.js/command": {
@@ -62,17 +63,16 @@
62
63
  },
63
64
  "zhin.js": {
64
65
  "optional": true
65
- },
66
- "zod": {
67
- "optional": true
68
66
  }
69
67
  },
70
68
  "devDependencies": {
71
69
  "@types/node": "^26.1.2",
70
+ "@types/ws": "^8.18.1",
71
+ "ajv": "8.18.0",
72
72
  "typescript": "^6.0.3",
73
73
  "vitest": "^4.1.10",
74
- "zod": "^4.4.3",
75
- "zhin.js": "1.1.25"
74
+ "ws": "^8.21.1",
75
+ "zhin.js": "1.1.27"
76
76
  },
77
77
  "files": [
78
78
  "adapters",
@@ -82,7 +82,6 @@
82
82
  "schema.json",
83
83
  "src",
84
84
  "lib",
85
- "tools",
86
85
  "agents",
87
86
  "README.md",
88
87
  "CHANGELOG.md",
@@ -132,6 +131,6 @@
132
131
  "scripts": {
133
132
  "build": "tsc",
134
133
  "clean": "rimraf lib",
135
- "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/lark/tests"
134
+ "test": "vitest run --root ../../.. plugins/adapters/lark/tests"
136
135
  }
137
136
  }
package/plugin.js CHANGED
@@ -1,7 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { definePlugin } from 'zhin.js';
3
- import { permissionHostToken } from '@zhin.js/permission';
4
- import { checkLarkPlatformPermit } from "./lib/platform-permit.js";
5
3
  import { createLarkConsoleRpc } from "./lib/console.js";
6
4
  export default definePlugin({
7
5
  name: 'lark',
@@ -10,12 +8,8 @@ export default definePlugin({
10
8
  displayName: 'Lark/Feishu (飞书) Adapter',
11
9
  },
12
10
  setup(context) {
13
- const dispose = context.resources.has(permissionHostToken)
14
- ? context.resources.use(permissionHostToken).registerPlatform('lark', checkLarkPlatformPermit)
15
- : undefined;
16
11
  return {
17
12
  consoleRpc: createLarkConsoleRpc(context.resources, () => context.config.get()),
18
- ...(dispose ? { dispose } : {}),
19
13
  };
20
14
  },
21
15
  });