@zhin.js/adapter-lark 1.1.25 → 1.1.27

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 (42) hide show
  1. package/CHANGELOG.md +146 -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/console-spec.d.ts +1 -1
  25. package/lib/console-spec.js +71 -7
  26. package/lib/index.d.ts +2 -2
  27. package/lib/index.js +2 -2
  28. package/lib/platform-permit.d.ts +4 -11
  29. package/lib/platform-permit.js +7 -33
  30. package/lib/protocol.d.ts +10 -5
  31. package/lib/protocol.js +17 -20
  32. package/lib/webhook.d.ts +2 -3
  33. package/lib/webhook.js +35 -28
  34. package/package.json +25 -26
  35. package/plugin.js +0 -6
  36. package/schema.json +0 -56
  37. package/src/client.ts +44 -19
  38. package/src/console-spec.ts +71 -7
  39. package/src/index.ts +2 -7
  40. package/src/platform-permit.ts +5 -45
  41. package/src/protocol.ts +21 -24
  42. package/src/webhook.ts +38 -33
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-lark",
3
- "version": "1.1.25",
3
+ "version": "1.1.27",
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.31",
39
- "@zhin.js/agent-feature": "1.1.16",
40
- "@zhin.js/console-contract": "1.1.5",
41
- "@zhin.js/core": "1.1.59",
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.2.0",
43
+ "@zhin.js/feature-kit": "1.1.16",
44
+ "@zhin.js/host-http": "1.2.0",
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.31",
53
- "@zhin.js/command": "^1.1.17",
54
- "@zhin.js/core": "^1.1.59",
55
- "@zhin.js/host-http": "^1.1.17",
56
- "@zhin.js/permission": "^1.1.15",
57
- "zhin.js": "^1.1.24"
53
+ "@zhin.js/adapter": "^1.1.33",
54
+ "@zhin.js/command": "^1.2.0",
55
+ "@zhin.js/core": "^1.2.0",
56
+ "@zhin.js/host-http": "^1.2.0",
57
+ "@zhin.js/permission": "^1.2.0",
58
+ "zhin.js": "^1.1.26"
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.24"
74
+ "ws": "^8.21.1",
75
+ "zhin.js": "1.1.26"
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
  });
package/schema.json CHANGED
@@ -14,34 +14,6 @@
14
14
  "type": "boolean",
15
15
  "default": true
16
16
  },
17
- "master": {
18
- "type": [
19
- "string",
20
- "number"
21
- ],
22
- "description": "框架 master(Lark/Feishu open_id or user_id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖",
23
- "x-i18n": {
24
- "en": {
25
- "description": "Framework masters (Lark/Feishu open_id or user_id; AI/tool permissions and endpoint management). endpoints[i].master can override this value."
26
- }
27
- }
28
- },
29
- "trusted": {
30
- "type": "array",
31
- "items": {
32
- "type": [
33
- "string",
34
- "number"
35
- ],
36
- "description": "Trusted Lark/Feishu open_id or user_id"
37
- },
38
- "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加",
39
- "x-i18n": {
40
- "en": {
41
- "description": "Framework trusted users, below master authority. endpoints[i].trusted can add users."
42
- }
43
- }
44
- },
45
17
  "endpoints": {
46
18
  "type": "array",
47
19
  "minItems": 1,
@@ -50,34 +22,6 @@
50
22
  "type": "object",
51
23
  "additionalProperties": false,
52
24
  "properties": {
53
- "master": {
54
- "type": [
55
- "string",
56
- "number"
57
- ],
58
- "description": "本 endpoint 的框架 master(Lark/Feishu open_id or user_id);覆盖顶层 master",
59
- "x-i18n": {
60
- "en": {
61
- "description": "Framework masters for this endpoint (Lark/Feishu open_id or user_id), overriding the top-level master list."
62
- }
63
- }
64
- },
65
- "trusted": {
66
- "type": "array",
67
- "items": {
68
- "type": [
69
- "string",
70
- "number"
71
- ],
72
- "description": "Trusted Lark/Feishu open_id or user_id"
73
- },
74
- "description": "本 endpoint 的 trusted 列表",
75
- "x-i18n": {
76
- "en": {
77
- "description": "Trusted users for this endpoint."
78
- }
79
- }
80
- },
81
25
  "appId": {
82
26
  "type": "string",
83
27
  "description": "Lark app ID"
package/src/client.ts CHANGED
@@ -2,7 +2,7 @@ import { Client as SdkClient, LoggerLevel, DefaultCache } from '@larksuiteoapi/n
2
2
  import { createLarkSdkHttpInstance } from './sdk-http.js';
3
3
  import { createLarkWebApiTransport } from './web-api-proxy.js';
4
4
  import type { LarkCardAction } from './cards.js';
5
- import { EndpointDeliveryError, type ConversationRef } from '@zhin.js/im-contract';
5
+ import { EndpointDeliveryError, type ConversationRef, type PlatformStanding, type SelfStandingRequest } from '@zhin.js/im-contract';
6
6
  /**
7
7
  * LarkEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
8
8
  */
@@ -43,7 +43,6 @@ import {
43
43
  type LarkMessage,
44
44
  type ResolvedLarkConfig,
45
45
  } from './protocol.js';
46
- import { normalizeLarkSenderForPermit } from './platform-permit.js';
47
46
  import { registerLarkWebhookRoutes } from './webhook.js';
48
47
  import { createLarkLongConnection, type LarkLongConnectionFactory } from './long-connection.js';
49
48
  import { openLarkReplyStream, supportsLarkReplyStream } from './reply-stream.js';
@@ -93,6 +92,7 @@ export interface LarkClientApi {
93
92
  /** Feishu/Lark OpenAPI client surface carried by every Endpoint event. */
94
93
 
95
94
  const LARK_CHAT_MANAGER_CACHE_TTL_MS = 60_000;
95
+ const LARK_CHAT_MANAGER_FAILURE_TTL_MS = 10_000;
96
96
 
97
97
  export class LarkClient extends SdkClient {
98
98
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
@@ -125,6 +125,23 @@ export class LarkClient extends SdkClient {
125
125
  `/im/v1/chats/${encodeURIComponent(groupId)}/members`,
126
126
  { params: { member_id_type: 'open_id' }, signal },
127
127
  ),
128
+ /**
129
+ * ADR 0056 §1:机器人自身在群里的地位(chat.get:群主为本应用 → owner;在 bot_manager_id_list →
130
+ * admin;有 owner_id 且都不是 → member)。查询失败或缺 owner_id 为未知。
131
+ */
132
+ inspectSelfStanding: async ({ conversation, signal }: SelfStandingRequest): Promise<PlatformStanding | undefined> => {
133
+ if (conversation.kind !== 'group') return undefined;
134
+ const appId = this.#options.config.appId;
135
+ const data = await createEndpointOperation({ signal }).run(() => this.#request(`/im/v1/chats/${encodeURIComponent(conversation.id)}`, { method: 'GET' }));
136
+ const info = data?.data as { owner_id?: unknown; owner_id_type?: unknown; bot_manager_id_list?: unknown } | undefined;
137
+ if (!info || !appId) return undefined;
138
+ if (info.owner_id_type === 'app_id' && info.owner_id === appId) return { standing: 'owner' };
139
+ const managers = Array.isArray(info.bot_manager_id_list) ? info.bot_manager_id_list.map(String) : [];
140
+ if (managers.includes(appId)) return { standing: 'admin' };
141
+ // chat.get may omit owner_id (e.g. when this app owns the group): without it the Bot is not
142
+ // provably a plain member, so the standing stays unknown.
143
+ return typeof info.owner_id === 'string' && info.owner_id ? { standing: 'member' } : undefined;
144
+ },
128
145
  });
129
146
  protected readonly endpointControl: EndpointControl = createRecallEndpointControl((id, signal) => this.recallMessage(id, { signal }));
130
147
  protected get endpointStreaming(): import('zhin.js/adapter').EndpointStreaming | undefined {
@@ -141,9 +158,11 @@ export class LarkClient extends SdkClient {
141
158
  readonly #lifecycle: EndpointLifecycle;
142
159
  readonly #cardSources = new Map<string, { conversation: ConversationRef; time: number }>();
143
160
  readonly #seen = new InboundDeduper({ ttlMs: 300_000, maxEntries: 10_000 });
144
- readonly #chatManagers = new TtlLookupCache<string, { owner?: string; admins: ReadonlySet<string> }>({
161
+ readonly #chatManagers = new TtlLookupCache<string, { owner?: string; admins: ReadonlySet<string> } | undefined>({
145
162
  ttlMs: LARK_CHAT_MANAGER_CACHE_TTL_MS,
146
163
  });
164
+ /** chat.get failures: the standing stays unknown for a short while instead of retrying every message. */
165
+ readonly #chatManagerFailures = new TtlLookupCache<string, true>({ ttlMs: LARK_CHAT_MANAGER_FAILURE_TTL_MS });
147
166
  #startupGeneration = 0;
148
167
  readonly #pendingInbound: { message: LarkMessage; event: unknown }[] = [];
149
168
 
@@ -370,11 +389,10 @@ export class LarkClient extends SdkClient {
370
389
  async #emitInbound(msg: LarkMessage, conversation: ConversationRef, nativeEvent: unknown): Promise<void> {
371
390
  const chatType = resolveChatType(msg.chat_id, msg.chat_type);
372
391
  const senderId = resolveSender(msg);
373
- const permit = senderId && chatType === 'group' && msg.chat_id
374
- ? normalizeLarkSenderForPermit(await this.#resolveChatRole(msg.chat_id, senderId))
392
+ // ADR 0056 §1:群主 / 群管理员 / 成员 → sender.platform.standing(飞书群无原生角色与权限位);未知不上报。
393
+ const role = senderId && chatType === 'group' && msg.chat_id
394
+ ? await this.#resolveChatRole(msg.chat_id, senderId)
375
395
  : undefined;
376
- // 规范角色 owner / admin 进 roles;细粒度能力放 senderPermissions
377
- const role = permit?.role === 'owner' || permit?.role === 'admin' ? permit.role : undefined;
378
396
  const mentioned = isLarkBotMentioned(msg);
379
397
  const replyToId = msg.parent_id || msg.root_id;
380
398
  await this.publishEvent('message.receive', {
@@ -382,36 +400,43 @@ export class LarkClient extends SdkClient {
382
400
  conversation,
383
401
  message: { conversation, id: generateMessageId(msg) },
384
402
  content: formatInboundContent(msg),
385
- ...(senderId ? { sender: { id: senderId, ...(role ? { roles: [role] } : {}) } } : {}),
403
+ ...(senderId ? { sender: { id: senderId, ...(role ? { platform: { standing: role } } : {}) } } : {}),
386
404
  endpointId: this.#options.config.id,
387
405
  ...(mentioned ? { mentioned: true } : {}),
388
406
  ...(replyToId ? { replyTo: { id: replyToId } } : {}),
389
407
  metadata: Object.freeze({
390
408
  messageType: msg.message_type,
391
409
  chatType,
392
- ...(role ? { senderRole: role } : {}),
393
- ...(role && permit?.permissions?.length ? { senderPermissions: [...permit.permissions] } : {}),
394
410
  }),
395
411
  });
396
412
  }
397
413
 
398
- /** 群主 / 群管理员来自 chat.get(owner_id + 管理员列表),按群缓存;失败时保守地视为普通成员。 */
399
- async #resolveChatRole(chatId: string, senderId: string): Promise<{ isOwner: boolean; isAdmin: boolean }> {
414
+ /**
415
+ * 群主 / 群管理员来自 chat.get(owner_id + 管理员列表),按群缓存 60 秒。chat.get 失败时地位未知
416
+ * (负缓存 10 秒,不报确定的 member);未返回 owner_id(如群主是机器人应用)时只能确认管理员,
417
+ * 其余发送者地位未知。
418
+ */
419
+ async #resolveChatRole(chatId: string, senderId: string): Promise<'owner' | 'admin' | 'member' | undefined> {
420
+ if (this.#chatManagerFailures.get(chatId)) return undefined;
400
421
  const entry = await this.#chatManagers.getOrLoad(chatId, async () => {
401
422
  const info = await this.getChatInfo(chatId) as {
402
423
  owner_id?: unknown;
403
424
  user_manager_id_list?: unknown;
404
425
  } | null;
405
- const admins = Array.isArray(info?.user_manager_id_list) ? info.user_manager_id_list.map(String) : [];
426
+ if (!info) {
427
+ this.#chatManagerFailures.set(chatId, true);
428
+ return undefined;
429
+ }
430
+ const admins = Array.isArray(info.user_manager_id_list) ? info.user_manager_id_list.map(String) : [];
406
431
  return {
407
- ...(typeof info?.owner_id === 'string' && info.owner_id ? { owner: info.owner_id } : {}),
432
+ ...(typeof info.owner_id === 'string' && info.owner_id ? { owner: info.owner_id } : {}),
408
433
  admins: new Set(admins) as ReadonlySet<string>,
409
434
  };
410
- });
411
- return {
412
- isOwner: entry.owner !== undefined && entry.owner === senderId,
413
- isAdmin: entry.admins.has(senderId),
414
- };
435
+ }, { cacheWhen: entry => entry !== undefined });
436
+ if (!entry) return undefined;
437
+ if (entry.owner !== undefined && entry.owner === senderId) return 'owner';
438
+ if (entry.admins.has(senderId)) return 'admin';
439
+ return entry.owner !== undefined ? 'member' : undefined;
415
440
  }
416
441
 
417
442
  /** Only correlate callbacks to a card actually sent by this endpoint. */
@@ -5,14 +5,78 @@ import type { AdapterConsoleField } from 'zhin.js/adapter';
5
5
 
6
6
  export const larkConsoleSpec = {
7
7
  adapterKey: 'lark',
8
- adapterDisplayName: 'Lark',
8
+ adapterDisplayName: '飞书 / Lark',
9
9
  fields: [
10
- { key: 'appId', required: true, env: true, description: '飞书 App ID', descriptionI18n: { en: 'Lark app ID' } },
11
- { key: 'appSecret', required: true, env: true, description: '飞书 App Secret', descriptionI18n: { en: 'Lark app secret' } },
12
- { key: 'encryptKey', env: true, description: '事件加密 key', descriptionI18n: { en: 'Event encryption key' } },
13
- { key: 'verificationToken', env: true, description: '事件验证 token', descriptionI18n: { en: 'Event verification token' } },
14
- { key: 'streaming', type: 'boolean', description: '生成时逐步显示回复', descriptionI18n: { en: 'Show replies progressively while generating' } },
15
- { key: 'streamingIntervalMs', type: 'number', description: '回复更新间隔(毫秒)', descriptionI18n: { en: 'Reply update interval in milliseconds' } },
10
+ {
11
+ key: 'appId',
12
+ required: true,
13
+ env: true,
14
+ label: '飞书 App ID',
15
+ labelI18n: { en: 'Lark App ID' },
16
+ group: '企业应用凭证',
17
+ groupI18n: { en: 'Enterprise App Credentials' },
18
+ placeholder: 'cli_a1b2c3d4e5f6g7h8',
19
+ description: '飞书开放平台企业自建应用的 App ID',
20
+ descriptionI18n: { en: 'Lark app ID' },
21
+ },
22
+ {
23
+ key: 'appSecret',
24
+ required: true,
25
+ env: true,
26
+ label: '飞书 App Secret',
27
+ labelI18n: { en: 'Lark App Secret' },
28
+ group: '企业应用凭证',
29
+ groupI18n: { en: 'Enterprise App Credentials' },
30
+ placeholder: '开放平台生成的 App Secret',
31
+ placeholderI18n: { en: 'App Secret from Lark Developer Console' },
32
+ description: '飞书开放平台企业自建应用的 App Secret',
33
+ descriptionI18n: { en: 'Lark app secret' },
34
+ },
35
+ {
36
+ key: 'encryptKey',
37
+ env: true,
38
+ label: 'Encrypt Key (事件加密密钥)',
39
+ labelI18n: { en: 'Event Encrypt Key' },
40
+ group: '事件回调与加解密',
41
+ groupI18n: { en: 'Event Callback & Encryption' },
42
+ placeholder: '留空则不启用消息加密',
43
+ placeholderI18n: { en: 'Optional event encryption key' },
44
+ description: '事件订阅加密密钥(Encrypt Key)',
45
+ descriptionI18n: { en: 'Event encryption key' },
46
+ },
47
+ {
48
+ key: 'verificationToken',
49
+ env: true,
50
+ label: 'Verification Token (验证令牌)',
51
+ labelI18n: { en: 'Verification Token' },
52
+ group: '事件回调与加解密',
53
+ groupI18n: { en: 'Event Callback & Encryption' },
54
+ placeholder: '事件订阅 Verification Token',
55
+ placeholderI18n: { en: 'Optional event verification token' },
56
+ description: '事件订阅验证令牌(Verification Token)',
57
+ descriptionI18n: { en: 'Event verification token' },
58
+ },
59
+ {
60
+ key: 'streaming',
61
+ type: 'boolean',
62
+ label: '卡片流式回复',
63
+ labelI18n: { en: 'Streaming Reply' },
64
+ group: '流式回复',
65
+ groupI18n: { en: 'Streaming Reply' },
66
+ description: '生成时逐步显示回复',
67
+ descriptionI18n: { en: 'Show replies progressively while generating' },
68
+ },
69
+ {
70
+ key: 'streamingIntervalMs',
71
+ type: 'number',
72
+ label: '流式刷新间隔 (ms)',
73
+ labelI18n: { en: 'Streaming Interval (ms)' },
74
+ group: '流式回复',
75
+ groupI18n: { en: 'Streaming Reply' },
76
+ placeholder: '500',
77
+ description: '回复更新间隔(毫秒)',
78
+ descriptionI18n: { en: 'Reply update interval in milliseconds' },
79
+ },
16
80
  ] as readonly AdapterConsoleField[],
17
81
  docsUrl: 'https://zhin.js.org/adapters/lark',
18
82
  readPermission: 'lark:read',
package/src/index.ts CHANGED
@@ -5,10 +5,10 @@ export {
5
5
  headerValue,
6
6
  larkInboundConversation,
7
7
  normalizeWebhookPath,
8
- readTextBody,
9
8
  resolveChatType,
10
9
  resolveLarkConfig,
11
10
  resolveSender,
11
+ decryptLarkEvent,
12
12
  verifySignature,
13
13
  type AccessToken,
14
14
  type LarkEndpointConfig,
@@ -22,12 +22,7 @@ export {
22
22
 
23
23
  export { type LarkClientEventMap } from './client.js';
24
24
 
25
- export {
26
- checkLarkPlatformPermit,
27
- larkGroupPermitResolver,
28
- normalizeLarkSenderForPermit,
29
- platformPermit,
30
- } from './platform-permit.js';
25
+ export { platformPermit } from './platform-permit.js';
31
26
 
32
27
  export {
33
28
  LarkEndpoint,
@@ -1,50 +1,10 @@
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
-
6
- const ADAPTER = 'lark';
7
7
 
8
8
  export function platformPermit(perm: string): string {
9
- return `platform(${ADAPTER},${perm})`;
10
- }
11
-
12
- const FACTORY_PERM_MAP: Record<string, string> = {
13
- scene_admin: 'chat_admin',
14
- scene_owner: 'chat_owner',
15
- };
16
-
17
- export function larkGroupPermitResolver(logicalPerm: string): string {
18
- return platformPermit(FACTORY_PERM_MAP[logicalPerm] ?? logicalPerm);
19
- }
20
-
21
- export function normalizeLarkSenderForPermit(input: {
22
- isOwner?: boolean;
23
- isAdmin?: boolean;
24
- }): { role?: string; permissions?: string[] } {
25
- if (input.isOwner) {
26
- return { role: 'owner', permissions: ['chat_owner', 'chat_admin', 'manage_managers'] };
27
- }
28
- if (input.isAdmin) {
29
- return { role: 'admin', permissions: ['chat_admin'] };
30
- }
31
- return { role: 'member', permissions: [] };
9
+ return `platform-permission:${perm}`;
32
10
  }
33
-
34
- export function checkLarkPlatformPermit(perm: string, subject: PermissionSubject): boolean {
35
- const role = subject.sender?.role?.[0];
36
- const permissions = subject.sender?.permissions ?? [];
37
- const has = (t: string) => permissions.includes(t);
38
-
39
- switch (perm) {
40
- case 'chat_owner':
41
- return role === 'owner' || has('chat_owner');
42
- case 'chat_admin':
43
- return role === 'owner' || role === 'admin' || has('chat_admin') || has('chat_owner');
44
- case 'manage_managers':
45
- return role === 'owner' || has('chat_owner');
46
- default:
47
- return false;
48
- }
49
- }
50
-
package/src/protocol.ts CHANGED
@@ -6,8 +6,7 @@ import { validateLarkWebApiProxy } from './web-api-proxy.js';
6
6
  * Canonicalization is owned by gateway/core before endpoint.send.
7
7
  */
8
8
 
9
- import { createHash, timingSafeEqual } from 'node:crypto';
10
- import type { IncomingMessage } from 'node:http';
9
+ import { createDecipheriv, createHash, timingSafeEqual } from 'node:crypto';
11
10
  import { EndpointDeliveryError, isMediaRef, type ConversationRef } from '@zhin.js/im-contract';
12
11
  import { formatCompact, getLogger } from '@zhin.js/logger';
13
12
 
@@ -88,6 +87,8 @@ export interface LarkEventBody {
88
87
  readonly ts?: string;
89
88
  readonly type?: string;
90
89
  readonly challenge?: string;
90
+ /** Present instead of every other field when the app has an Encrypt Key. */
91
+ readonly encrypt?: string;
91
92
  readonly event?: {
92
93
  readonly sender?: LarkMessage['sender'];
93
94
  readonly message?: LarkMessage;
@@ -273,6 +274,22 @@ export function verifySignature(
273
274
  }
274
275
  }
275
276
 
277
+ /**
278
+ * Decrypt an `{"encrypt": "..."}` webhook payload (Lark/Feishu open-platform
279
+ * spec): key = SHA-256(Encrypt Key), base64 payload = 16-byte IV followed by
280
+ * AES-256-CBC ciphertext with PKCS#7 padding. Throws when the payload cannot
281
+ * be decrypted with this key.
282
+ */
283
+ export function decryptLarkEvent(encryptKey: string, encrypted: string): string {
284
+ const payload = Buffer.from(encrypted, 'base64');
285
+ if (payload.length <= 16 || (payload.length - 16) % 16 !== 0) {
286
+ throw new Error('Malformed encrypted Lark event');
287
+ }
288
+ const key = createHash('sha256').update(encryptKey).digest();
289
+ const decipher = createDecipheriv('aes-256-cbc', key, payload.subarray(0, 16));
290
+ return Buffer.concat([decipher.update(payload.subarray(16)), decipher.final()]).toString('utf8');
291
+ }
292
+
276
293
  /** timing-safe 比较 webhook verification token(长度不同快速失败)。 */
277
294
  export function verifyToken(token: string, expected: string): boolean {
278
295
  try {
@@ -439,29 +456,9 @@ export function formatOutboundBody(payload: unknown, locale?: Locale): LarkSendB
439
456
  }
440
457
 
441
458
  export function headerValue(
442
- headers: IncomingMessage['headers'],
459
+ headers: Headers,
443
460
  name: string,
444
461
  ): string {
445
- const value = headers[name] ?? headers[name.toLowerCase()];
446
- if (Array.isArray(value)) return value[0] ?? '';
447
- return value ?? '';
462
+ return headers.get(name) ?? '';
448
463
  }
449
464
 
450
- export async function readTextBody(
451
- request: IncomingMessage,
452
- options: { readonly limit?: number } = {},
453
- ): Promise<string> {
454
- const limit = options.limit ?? 1_048_576;
455
- const chunks: Buffer[] = [];
456
- let size = 0;
457
- for await (const chunk of request) {
458
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
459
- size += buffer.length;
460
- if (size > limit) {
461
- request.destroy();
462
- throw new Error(`Request body exceeds ${limit} bytes`);
463
- }
464
- chunks.push(buffer);
465
- }
466
- return Buffer.concat(chunks).toString('utf8');
467
- }