@zhin.js/adapter-slack 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 (48) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/LICENSE +21 -0
  3. package/PERMITS.md +10 -17
  4. package/README.md +8 -4
  5. package/agents/slack/conversation.js +15 -0
  6. package/agents/slack/conversation.ts +17 -0
  7. package/agents/slack/messages.js +4 -0
  8. package/agents/slack/messages.ts +4 -0
  9. package/agents/slack/skills/slack-channels/tools/archive_channel/index.js +6 -7
  10. package/agents/slack/skills/slack-channels/tools/archive_channel/index.ts +7 -10
  11. package/agents/slack/skills/slack-channels/tools/invite_to_channel/index.js +5 -5
  12. package/agents/slack/skills/slack-channels/tools/invite_to_channel/index.ts +5 -6
  13. package/agents/slack/skills/slack-channels/tools/set_purpose/index.js +5 -5
  14. package/agents/slack/skills/slack-channels/tools/set_purpose/index.ts +5 -6
  15. package/agents/slack/skills/slack-channels/tools/set_topic/index.js +5 -5
  16. package/agents/slack/skills/slack-channels/tools/set_topic/index.ts +5 -6
  17. package/agents/slack/skills/slack-channels/tools/unarchive/index.js +2 -3
  18. package/agents/slack/skills/slack-channels/tools/unarchive/index.ts +2 -3
  19. package/agents/slack/skills/slack-messages/tools/add_reaction/index.js +5 -4
  20. package/agents/slack/skills/slack-messages/tools/add_reaction/index.ts +5 -5
  21. package/agents/slack/skills/slack-messages/tools/edit_message/index.js +6 -5
  22. package/agents/slack/skills/slack-messages/tools/edit_message/index.ts +6 -6
  23. package/agents/slack/skills/slack-messages/tools/pin_message/index.js +5 -5
  24. package/agents/slack/skills/slack-messages/tools/pin_message/index.ts +5 -6
  25. package/agents/slack/skills/slack-messages/tools/remove_reaction/index.js +5 -4
  26. package/agents/slack/skills/slack-messages/tools/remove_reaction/index.ts +5 -5
  27. package/agents/slack/skills/slack-messages/tools/unpin_message/index.js +5 -5
  28. package/agents/slack/skills/slack-messages/tools/unpin_message/index.ts +5 -6
  29. package/lib/console-spec.js +119 -11
  30. package/lib/index.d.ts +2 -2
  31. package/lib/index.js +2 -2
  32. package/lib/platform-permit.d.ts +4 -12
  33. package/lib/platform-permit.js +7 -41
  34. package/lib/protocol.d.ts +1 -5
  35. package/lib/protocol.js +1 -19
  36. package/lib/slack-message-ref.d.ts +0 -1
  37. package/lib/slack-message-ref.js +0 -3
  38. package/lib/webhook.d.ts +1 -2
  39. package/lib/webhook.js +12 -29
  40. package/package.json +24 -26
  41. package/plugin.js +0 -6
  42. package/schema.json +0 -56
  43. package/src/console-spec.ts +119 -11
  44. package/src/index.ts +1 -7
  45. package/src/platform-permit.ts +5 -54
  46. package/src/protocol.ts +2 -23
  47. package/src/slack-message-ref.ts +0 -4
  48. package/src/webhook.ts +12 -32
package/lib/webhook.js CHANGED
@@ -1,23 +1,20 @@
1
+ import { readTextBody } from '@zhin.js/host-http-contract';
1
2
  import { getLogger } from '@zhin.js/logger';
2
- import { headerValue, readTextBody, verifySlackSignature, } from './protocol.js';
3
+ import { headerValue, verifySlackSignature, } from './protocol.js';
3
4
  const logger = getLogger('slack');
4
5
  export function registerSlackWebhookRoutes(http, handler) {
5
6
  const path = handler.config.webhookPath;
6
7
  return [
7
- http.route('POST', path, async (request, response) => {
8
- await handleSlackWebhookRequest(request, response, handler);
9
- }, { summary: 'Slack Events API / Interactivity', tags: ['slack'] }),
8
+ http.route('POST', path, (request) => handleSlackWebhookRequest(request, handler), { summary: 'Slack Events API / Interactivity', tags: ['slack'] }),
10
9
  ];
11
10
  }
12
- export async function handleSlackWebhookRequest(request, response, handler) {
11
+ export async function handleSlackWebhookRequest(request, handler) {
13
12
  try {
14
13
  const rawBody = await readTextBody(request);
15
14
  const timestamp = headerValue(request.headers, 'x-slack-request-timestamp');
16
15
  const signature = headerValue(request.headers, 'x-slack-signature');
17
16
  if (!verifySlackSignature(handler.config.signingSecret, rawBody, timestamp, signature)) {
18
- response.writeHead(401, { 'Content-Type': 'text/plain' });
19
- response.end('Invalid signature');
20
- return;
17
+ return new Response('Invalid signature', { status: 401, headers: { 'Content-Type': 'text/plain' } });
21
18
  }
22
19
  const contentType = headerValue(request.headers, 'content-type');
23
20
  if (contentType.includes('application/x-www-form-urlencoded')) {
@@ -25,17 +22,13 @@ export async function handleSlackWebhookRequest(request, response, handler) {
25
22
  const payloadStr = params.get('payload');
26
23
  if (payloadStr) {
27
24
  const payload = JSON.parse(payloadStr);
28
- response.writeHead(200);
29
- response.end('');
30
25
  handler.admitInteraction(payload);
31
- return;
26
+ return new Response('', { status: 200 });
32
27
  }
33
28
  const body = Object.fromEntries(params);
34
29
  if (body.command) {
35
- response.writeHead(200);
36
- response.end('');
37
30
  handler.admitSlashCommand(body);
38
- return;
31
+ return new Response('', { status: 200 });
39
32
  }
40
33
  }
41
34
  let envelope;
@@ -43,30 +36,20 @@ export async function handleSlackWebhookRequest(request, response, handler) {
43
36
  envelope = JSON.parse(rawBody);
44
37
  }
45
38
  catch {
46
- response.writeHead(200);
47
- response.end('');
48
- return;
39
+ return new Response('', { status: 200 });
49
40
  }
50
41
  if (envelope.type === 'url_verification') {
51
42
  const challenge = envelope.challenge;
52
- response.writeHead(200, { 'Content-Type': 'application/json' });
53
- response.end(JSON.stringify({ challenge }));
54
- return;
43
+ return new Response(JSON.stringify({ challenge }), { status: 200, headers: { 'Content-Type': 'application/json' } });
55
44
  }
56
45
  if (envelope.type === 'event_callback' && envelope.event) {
57
- response.writeHead(200);
58
- response.end('');
59
46
  handler.handleEnvelope(envelope);
60
- return;
47
+ return new Response('', { status: 200 });
61
48
  }
62
- response.writeHead(200);
63
- response.end('');
49
+ return new Response('', { status: 200 });
64
50
  }
65
51
  catch (error) {
66
52
  logger.error('Slack webhook error:', error);
67
- if (!response.headersSent) {
68
- response.writeHead(200);
69
- response.end('');
70
- }
53
+ return new Response('', { status: 200 });
71
54
  }
72
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-slack",
3
- "version": "1.1.25",
3
+ "version": "1.1.27",
4
4
  "description": "Zhin.js Slack adapter for Plugin Runtime (Socket Mode / HTTP Events)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -33,26 +33,27 @@
33
33
  "dependencies": {
34
34
  "@slack/socket-mode": "^2.0.7",
35
35
  "@slack/web-api": "^7.19.0",
36
- "@zhin.js/adapter": "1.1.31",
37
- "@zhin.js/agent-feature": "1.1.16",
38
- "@zhin.js/console-contract": "1.1.5",
39
- "@zhin.js/core": "1.1.59",
40
- "@zhin.js/feature-kit": "1.1.15",
41
- "@zhin.js/host-http": "1.1.17",
42
- "@zhin.js/im-contract": "1.1.13",
43
- "@zhin.js/logger": "1.1.2",
44
- "@zhin.js/page": "1.1.17",
45
- "@zhin.js/skill": "1.1.16",
46
- "@zhin.js/tool": "1.1.18"
36
+ "zod": "^4.6.7",
37
+ "@zhin.js/adapter": "1.1.33",
38
+ "@zhin.js/agent-feature": "1.1.17",
39
+ "@zhin.js/console-contract": "1.1.6",
40
+ "@zhin.js/core": "1.2.0",
41
+ "@zhin.js/feature-kit": "1.1.16",
42
+ "@zhin.js/host-http": "1.2.0",
43
+ "@zhin.js/host-http-contract": "1.2.0",
44
+ "@zhin.js/im-contract": "1.1.14",
45
+ "@zhin.js/logger": "1.1.3",
46
+ "@zhin.js/page": "1.1.18",
47
+ "@zhin.js/skill": "1.1.17",
48
+ "@zhin.js/tool": "1.1.19"
47
49
  },
48
50
  "peerDependencies": {
49
- "zod": "^4.0.0",
50
- "@zhin.js/adapter": "^1.1.31",
51
- "@zhin.js/command": "^1.1.17",
52
- "@zhin.js/core": "^1.1.59",
53
- "@zhin.js/host-http": "^1.1.17",
54
- "@zhin.js/permission": "^1.1.15",
55
- "zhin.js": "^1.1.24"
51
+ "@zhin.js/adapter": "^1.1.33",
52
+ "@zhin.js/command": "^1.2.0",
53
+ "@zhin.js/core": "^1.2.0",
54
+ "@zhin.js/host-http": "^1.2.0",
55
+ "@zhin.js/permission": "^1.2.0",
56
+ "zhin.js": "^1.1.26"
56
57
  },
57
58
  "peerDependenciesMeta": {
58
59
  "@zhin.js/command": {
@@ -60,17 +61,15 @@
60
61
  },
61
62
  "zhin.js": {
62
63
  "optional": true
63
- },
64
- "zod": {
65
- "optional": true
66
64
  }
67
65
  },
68
66
  "devDependencies": {
69
67
  "@types/node": "^26.1.2",
68
+ "@types/ws": "^8.18.1",
70
69
  "typescript": "^6.0.3",
71
70
  "vitest": "^4.1.10",
72
- "zod": "^4.4.3",
73
- "zhin.js": "1.1.24"
71
+ "ws": "^8.21.1",
72
+ "zhin.js": "1.1.26"
74
73
  },
75
74
  "files": [
76
75
  "adapters",
@@ -80,7 +79,6 @@
80
79
  "schema.json",
81
80
  "src",
82
81
  "lib",
83
- "tools",
84
82
  "agents",
85
83
  "README.md",
86
84
  "CHANGELOG.md",
@@ -130,6 +128,6 @@
130
128
  "scripts": {
131
129
  "build": "tsc",
132
130
  "clean": "rimraf lib",
133
- "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/slack/tests"
131
+ "test": "vitest run --root ../../.. plugins/adapters/slack/tests"
134
132
  }
135
133
  }
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 { checkSlackPlatformPermit } from "./lib/platform-permit.js";
5
3
  import { createSlackConsoleRpc } from "./lib/console.js";
6
4
  export default definePlugin({
7
5
  name: 'slack',
@@ -10,12 +8,8 @@ export default definePlugin({
10
8
  displayName: 'Slack Adapter',
11
9
  },
12
10
  setup(context) {
13
- const dispose = context.resources.has(permissionHostToken)
14
- ? context.resources.use(permissionHostToken).registerPlatform('slack', checkSlackPlatformPermit)
15
- : undefined;
16
11
  return {
17
12
  consoleRpc: createSlackConsoleRpc(() => context.config.get()),
18
- ...(dispose ? { dispose } : {}),
19
13
  };
20
14
  },
21
15
  });
package/schema.json CHANGED
@@ -17,34 +17,6 @@
17
17
  "default": 15000,
18
18
  "description": "Socket Mode client ping timeout (ms)"
19
19
  },
20
- "master": {
21
- "type": [
22
- "string",
23
- "number"
24
- ],
25
- "description": "框架 master(Slack user id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖",
26
- "x-i18n": {
27
- "en": {
28
- "description": "Framework masters (Slack user id; AI/tool permissions and endpoint management). endpoints[i].master can override this value."
29
- }
30
- }
31
- },
32
- "trusted": {
33
- "type": "array",
34
- "items": {
35
- "type": [
36
- "string",
37
- "number"
38
- ],
39
- "description": "Trusted Slack user id"
40
- },
41
- "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加",
42
- "x-i18n": {
43
- "en": {
44
- "description": "Framework trusted users, below master authority. endpoints[i].trusted can add users."
45
- }
46
- }
47
- },
48
20
  "endpoints": {
49
21
  "type": "array",
50
22
  "minItems": 1,
@@ -53,34 +25,6 @@
53
25
  "type": "object",
54
26
  "additionalProperties": false,
55
27
  "properties": {
56
- "master": {
57
- "type": [
58
- "string",
59
- "number"
60
- ],
61
- "description": "本 endpoint 的框架 master(Slack user id);覆盖顶层 master",
62
- "x-i18n": {
63
- "en": {
64
- "description": "Framework masters for this endpoint (Slack user id), overriding the top-level master list."
65
- }
66
- }
67
- },
68
- "trusted": {
69
- "type": "array",
70
- "items": {
71
- "type": [
72
- "string",
73
- "number"
74
- ],
75
- "description": "Trusted Slack user id"
76
- },
77
- "description": "本 endpoint 的 trusted 列表",
78
- "x-i18n": {
79
- "en": {
80
- "description": "Trusted users for this endpoint."
81
- }
82
- }
83
- },
84
28
  "token": {
85
29
  "type": "string",
86
30
  "description": "Bot User OAuth Token (xoxb-...)"
@@ -7,17 +7,125 @@ export const slackConsoleSpec = {
7
7
  adapterKey: 'slack',
8
8
  adapterDisplayName: 'Slack',
9
9
  fields: [
10
- { key: 'token', required: true, env: true, description: 'Bot User OAuth Token(xoxb-...)' },
11
- { key: 'signingSecret', env: true, description: '签名密钥(HTTP Events,socketMode: false 必填)', descriptionI18n: { en: 'Signing secret (required for HTTP Events)' } },
12
- { key: 'appToken', env: true, description: 'App-Level Token(xapp-...,Socket Mode)', descriptionI18n: { en: 'App-level token (xapp-..., Socket Mode)' } },
13
- { key: 'socketMode', type: 'boolean', description: 'Socket Mode; false selects HTTP Events' },
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' } },
16
- { key: 'webhookPath', description: 'HTTP Events route' },
17
- { key: 'clientPingTimeout', type: 'number', description: 'SDK client ping timeout in milliseconds' },
18
- { key: 'webApiProxy.port', type: 'number', description: 'Configured Web API loopback proxy port' },
19
- { key: 'streamProxy.port', type: 'number', description: 'Configured Socket Mode loopback proxy port' },
20
- { key: 'streamProxy.serverName', description: 'Expected Socket Mode TLS hostname' },
10
+ {
11
+ key: 'token',
12
+ required: true,
13
+ env: true,
14
+ label: 'Bot User OAuth Token',
15
+ labelI18n: { en: 'Bot User OAuth Token' },
16
+ group: '基础认证',
17
+ groupI18n: { en: 'Basic Auth' },
18
+ placeholder: 'xoxb-...',
19
+ description: 'Bot User OAuth Token(xoxb-...)',
20
+ descriptionI18n: { en: 'Bot User OAuth Token (xoxb-...)' },
21
+ },
22
+ {
23
+ key: 'appToken',
24
+ env: true,
25
+ label: 'App-Level Token (Socket Mode)',
26
+ labelI18n: { en: 'App-Level Token' },
27
+ group: '基础认证',
28
+ groupI18n: { en: 'Basic Auth' },
29
+ placeholder: 'xapp-...',
30
+ description: 'App-Level Token(xapp-...,Socket Mode)',
31
+ descriptionI18n: { en: 'App-level token (xapp-..., Socket Mode)' },
32
+ },
33
+ {
34
+ key: 'signingSecret',
35
+ env: true,
36
+ label: 'Signing Secret (签名密钥)',
37
+ labelI18n: { en: 'Signing Secret' },
38
+ group: '基础认证',
39
+ groupI18n: { en: 'Basic Auth' },
40
+ placeholder: '32 位十六进制签名密钥',
41
+ placeholderI18n: { en: 'Slack app signing secret' },
42
+ description: '签名密钥(HTTP Events,socketMode: false 必填)',
43
+ descriptionI18n: { en: 'Signing secret (required for HTTP Events)' },
44
+ },
45
+ {
46
+ key: 'socketMode',
47
+ type: 'boolean',
48
+ label: '启用 Socket Mode',
49
+ labelI18n: { en: 'Enable Socket Mode' },
50
+ group: '连接与回调',
51
+ groupI18n: { en: 'Connection & Webhook' },
52
+ description: '开启后使用 WebSocket 长连接;关闭则使用 HTTP Events 回调',
53
+ descriptionI18n: { en: 'Socket Mode; false selects HTTP Events' },
54
+ },
55
+ {
56
+ key: 'webhookPath',
57
+ label: 'HTTP Events 本地路由',
58
+ labelI18n: { en: 'HTTP Events Local Path' },
59
+ group: '连接与回调',
60
+ groupI18n: { en: 'Connection & Webhook' },
61
+ placeholder: '/slack/events',
62
+ description: 'HTTP Events 模式下监听的本地路由',
63
+ descriptionI18n: { en: 'HTTP Events route' },
64
+ },
65
+ {
66
+ key: 'clientPingTimeout',
67
+ type: 'number',
68
+ label: '心跳超时时间 (ms)',
69
+ labelI18n: { en: 'Client Ping Timeout (ms)' },
70
+ group: '连接与回调',
71
+ groupI18n: { en: 'Connection & Webhook' },
72
+ placeholder: '5000',
73
+ description: 'Socket Mode 客户端心跳超时毫秒数',
74
+ descriptionI18n: { en: 'SDK client ping timeout in milliseconds' },
75
+ },
76
+ {
77
+ key: 'streaming',
78
+ type: 'boolean',
79
+ label: '流式回复',
80
+ labelI18n: { en: 'Streaming Reply' },
81
+ group: '流式体验与代理',
82
+ groupI18n: { en: 'Streaming & Proxy' },
83
+ description: '生成时逐步显示回复',
84
+ descriptionI18n: { en: 'Show replies progressively while generating' },
85
+ },
86
+ {
87
+ key: 'streamingIntervalMs',
88
+ type: 'number',
89
+ label: '流式刷新间隔 (ms)',
90
+ labelI18n: { en: 'Streaming Interval (ms)' },
91
+ group: '流式体验与代理',
92
+ groupI18n: { en: 'Streaming & Proxy' },
93
+ placeholder: '500',
94
+ description: '回复更新间隔(毫秒)',
95
+ descriptionI18n: { en: 'Reply update interval in milliseconds' },
96
+ },
97
+ {
98
+ key: 'webApiProxy.port',
99
+ type: 'number',
100
+ label: 'Web API 代理端口',
101
+ labelI18n: { en: 'Web API Proxy Port' },
102
+ group: '流式体验与代理',
103
+ groupI18n: { en: 'Streaming & Proxy' },
104
+ placeholder: '例如 7890',
105
+ description: '本地回环 Web API 代理端口(可选)',
106
+ descriptionI18n: { en: 'Configured Web API loopback proxy port' },
107
+ },
108
+ {
109
+ key: 'streamProxy.port',
110
+ type: 'number',
111
+ label: 'Socket Mode 代理端口',
112
+ labelI18n: { en: 'Socket Mode Proxy Port' },
113
+ group: '流式体验与代理',
114
+ groupI18n: { en: 'Streaming & Proxy' },
115
+ placeholder: '例如 7890',
116
+ description: '本地回环 Socket Mode 代理端口(可选)',
117
+ descriptionI18n: { en: 'Configured Socket Mode loopback proxy port' },
118
+ },
119
+ {
120
+ key: 'streamProxy.serverName',
121
+ label: 'Socket Mode TLS 主机名',
122
+ labelI18n: { en: 'Socket Mode TLS Server Name' },
123
+ group: '流式体验与代理',
124
+ groupI18n: { en: 'Streaming & Proxy' },
125
+ placeholder: 'wss-primary.slack.com',
126
+ description: '代理校验的 Socket Mode TLS 主机名(可选)',
127
+ descriptionI18n: { en: 'Expected Socket Mode TLS hostname' },
128
+ },
21
129
  ] as readonly AdapterConsoleField[],
22
130
  docsUrl: 'https://zhin.js.org/adapters/slack',
23
131
  readPermission: 'slack:read',
package/src/index.ts CHANGED
@@ -6,7 +6,6 @@ export {
6
6
  headerValue,
7
7
  keyboardToBlockKitBlocks,
8
8
  normalizeWebhookPath,
9
- readTextBody,
10
9
  resolveSlackChannelType,
11
10
  resolveSlackConfig,
12
11
  slackInboundConversation,
@@ -41,12 +40,7 @@ export {
41
40
  type SlackUserInfo,
42
41
  } from './client.js';
43
42
 
44
- export {
45
- checkSlackPlatformPermit,
46
- normalizeSlackSenderForPermit,
47
- platformPermit,
48
- slackGroupPermitResolver,
49
- } from './platform-permit.js';
43
+ export { platformPermit } from './platform-permit.js';
50
44
 
51
45
  export { normalizeSlackReactionName } from './slack-reaction.js';
52
46
  export { markdownToMrkdwn, mrkdwnToPlainFallback, splitMrkdwnText } from './markdown-to-mrkdwn.js';
@@ -1,59 +1,10 @@
1
1
  /**
2
- * Slack platform permit — workspace + channel 权限
2
+ * Slack 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 = 'slack';
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: 'channel_manager',
14
- scene_owner: 'workspace_owner',
15
- };
16
-
17
- export function slackGroupPermitResolver(logicalPerm: string): string {
18
- return platformPermit(FACTORY_PERM_MAP[logicalPerm] ?? logicalPerm);
19
- }
20
-
21
- export function normalizeSlackSenderForPermit(input: {
22
- isWorkspaceOwner?: boolean;
23
- isWorkspaceAdmin?: boolean;
24
- isChannelManager?: boolean;
25
- }): { role?: string; permissions?: string[] } {
26
- const permissions: string[] = [];
27
- if (input.isWorkspaceOwner) {
28
- permissions.push('workspace_owner', 'workspace_admin', 'channel_manager');
29
- return { role: 'owner', permissions };
30
- }
31
- if (input.isWorkspaceAdmin) {
32
- permissions.push('workspace_admin', 'channel_manager');
33
- return { role: 'admin', permissions };
34
- }
35
- if (input.isChannelManager) {
36
- permissions.push('channel_manager');
37
- return { role: 'channel_admin', permissions };
38
- }
39
- return { role: 'member', permissions };
9
+ return `platform-permission:${perm}`;
40
10
  }
41
-
42
- export function checkSlackPlatformPermit(perm: string, subject: PermissionSubject): boolean {
43
- const role = subject.sender?.role?.[0];
44
- const permissions = subject.sender?.permissions ?? [];
45
- const has = (t: string) => permissions.includes(t);
46
-
47
- switch (perm) {
48
- case 'workspace_owner':
49
- return role === 'owner' || has('workspace_owner');
50
- case 'workspace_admin':
51
- return role === 'owner' || role === 'admin' || has('workspace_admin') || has('workspace_owner');
52
- case 'channel_manager':
53
- return role === 'owner' || role === 'admin' || role === 'channel_admin'
54
- || has('channel_manager') || has('workspace_admin') || has('workspace_owner');
55
- default:
56
- return false;
57
- }
58
- }
59
-
package/src/protocol.ts CHANGED
@@ -4,7 +4,6 @@ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
4
4
  * Canonicalization is owned by gateway/core before endpoint.send.
5
5
  */
6
6
  import { createHmac, timingSafeEqual } from 'node:crypto';
7
- import type { IncomingMessage } from 'node:http';
8
7
  import { isMediaRef, type MediaRef, type ConversationKind, type ConversationRef } from '@zhin.js/im-contract';
9
8
  import { formatCompact, getLogger } from '@zhin.js/logger';
10
9
  import { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
@@ -417,30 +416,10 @@ export function verifySlackSignature(
417
416
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
418
417
  }
419
418
 
420
- export async function readTextBody(
421
- request: IncomingMessage,
422
- options: { readonly limit?: number } = {},
423
- ): Promise<string> {
424
- const limit = options.limit ?? 1_048_576;
425
- const chunks: Buffer[] = [];
426
- let size = 0;
427
- for await (const chunk of request) {
428
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
429
- size += buffer.length;
430
- if (size > limit) {
431
- request.destroy();
432
- throw new Error(`Request body exceeds ${limit} bytes`);
433
- }
434
- chunks.push(buffer);
435
- }
436
- return Buffer.concat(chunks).toString('utf8');
437
- }
438
419
 
439
420
  export function headerValue(
440
- headers: IncomingMessage['headers'],
421
+ headers: Headers,
441
422
  name: string,
442
423
  ): string {
443
- const raw = headers[name.toLowerCase()];
444
- if (Array.isArray(raw)) return raw[0] ?? '';
445
- return raw ?? '';
424
+ return headers.get(name) ?? '';
446
425
  }
@@ -12,7 +12,3 @@ export function parseSlackMessageRef(ref: string): { channel: string; ts: string
12
12
  if (!channel || !ts) return null;
13
13
  return { channel, ts };
14
14
  }
15
-
16
- export function slackMessageTs(ref: string): string {
17
- return parseSlackMessageRef(ref)?.ts ?? ref;
18
- }
package/src/webhook.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * Slack webhook HTTP: signature → parse → admit.
3
3
  */
4
- import type { IncomingMessage, ServerResponse } from 'node:http';
5
4
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
5
+ import { readTextBody } from '@zhin.js/host-http-contract';
6
6
  import { formatCompact, getLogger } from '@zhin.js/logger';
7
7
  import {
8
8
  headerValue,
9
- readTextBody,
10
9
  verifySlackSignature,
11
10
  type ResolvedSlackConfig,
12
11
  type SlackInteractionPayload,
@@ -29,26 +28,21 @@ export function registerSlackWebhookRoutes(
29
28
  ): HttpRouteRegistration[] {
30
29
  const path = handler.config.webhookPath;
31
30
  return [
32
- http.route('POST', path, async (request, response) => {
33
- await handleSlackWebhookRequest(request, response, handler);
34
- }, { summary: 'Slack Events API / Interactivity', tags: ['slack'] }),
31
+ http.route('POST', path, (request) => handleSlackWebhookRequest(request, handler), { summary: 'Slack Events API / Interactivity', tags: ['slack'] }),
35
32
  ];
36
33
  }
37
34
 
38
35
  export async function handleSlackWebhookRequest(
39
- request: IncomingMessage,
40
- response: ServerResponse,
36
+ request: Request,
41
37
  handler: SlackWebhookHandler,
42
- ): Promise<void> {
38
+ ): Promise<Response> {
43
39
  try {
44
40
  const rawBody = await readTextBody(request);
45
41
  const timestamp = headerValue(request.headers, 'x-slack-request-timestamp');
46
42
  const signature = headerValue(request.headers, 'x-slack-signature');
47
43
 
48
44
  if (!verifySlackSignature(handler.config.signingSecret, rawBody, timestamp, signature)) {
49
- response.writeHead(401, { 'Content-Type': 'text/plain' });
50
- response.end('Invalid signature');
51
- return;
45
+ return new Response('Invalid signature', { status: 401, headers: { 'Content-Type': 'text/plain' } });
52
46
  }
53
47
 
54
48
  const contentType = headerValue(request.headers, 'content-type');
@@ -57,17 +51,13 @@ export async function handleSlackWebhookRequest(
57
51
  const payloadStr = params.get('payload');
58
52
  if (payloadStr) {
59
53
  const payload = JSON.parse(payloadStr) as SlackInteractionPayload;
60
- response.writeHead(200);
61
- response.end('');
62
54
  handler.admitInteraction(payload);
63
- return;
55
+ return new Response('', { status: 200 });
64
56
  }
65
57
  const body = Object.fromEntries(params) as unknown as SlackSlashCommand;
66
58
  if (body.command) {
67
- response.writeHead(200);
68
- response.end('');
69
59
  handler.admitSlashCommand(body);
70
- return;
60
+ return new Response('', { status: 200 });
71
61
  }
72
62
  }
73
63
 
@@ -75,32 +65,22 @@ export async function handleSlackWebhookRequest(
75
65
  try {
76
66
  envelope = JSON.parse(rawBody) as Record<string, unknown>;
77
67
  } catch {
78
- response.writeHead(200);
79
- response.end('');
80
- return;
68
+ return new Response('', { status: 200 });
81
69
  }
82
70
 
83
71
  if (envelope.type === 'url_verification') {
84
72
  const challenge = (envelope as unknown as SlackUrlVerification).challenge;
85
- response.writeHead(200, { 'Content-Type': 'application/json' });
86
- response.end(JSON.stringify({ challenge }));
87
- return;
73
+ return new Response(JSON.stringify({ challenge }), { status: 200, headers: { 'Content-Type': 'application/json' } });
88
74
  }
89
75
 
90
76
  if (envelope.type === 'event_callback' && envelope.event) {
91
- response.writeHead(200);
92
- response.end('');
93
77
  handler.handleEnvelope(envelope);
94
- return;
78
+ return new Response('', { status: 200 });
95
79
  }
96
80
 
97
- response.writeHead(200);
98
- response.end('');
81
+ return new Response('', { status: 200 });
99
82
  } catch (error) {
100
83
  logger.error('Slack webhook error:', error);
101
- if (!response.headersSent) {
102
- response.writeHead(200);
103
- response.end('');
104
- }
84
+ return new Response('', { status: 200 });
105
85
  }
106
86
  }