cc-lark 0.1.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/.github/workflows/ci.yml +47 -0
- package/.github/workflows/release.yml +47 -0
- package/.github/workflows/sync-upstream.yml +127 -0
- package/.prettierrc.json +7 -0
- package/README.md +214 -0
- package/dist/core/api-error.d.ts +193 -0
- package/dist/core/api-error.d.ts.map +1 -0
- package/dist/core/api-error.js +263 -0
- package/dist/core/api-error.js.map +1 -0
- package/dist/core/auth-errors.d.ts +13 -0
- package/dist/core/auth-errors.d.ts.map +1 -0
- package/dist/core/auth-errors.js +14 -0
- package/dist/core/auth-errors.js.map +1 -0
- package/dist/core/config.d.ts +60 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +115 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/device-flow.d.ts +80 -0
- package/dist/core/device-flow.d.ts.map +1 -0
- package/dist/core/device-flow.js +231 -0
- package/dist/core/device-flow.js.map +1 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +16 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/lark-client.d.ts +136 -0
- package/dist/core/lark-client.d.ts.map +1 -0
- package/dist/core/lark-client.js +315 -0
- package/dist/core/lark-client.js.map +1 -0
- package/dist/core/token-store.d.ts +67 -0
- package/dist/core/token-store.d.ts.map +1 -0
- package/dist/core/token-store.js +215 -0
- package/dist/core/token-store.js.map +1 -0
- package/dist/core/types.d.ts +286 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +11 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/uat-client.d.ts +64 -0
- package/dist/core/uat-client.d.ts.map +1 -0
- package/dist/core/uat-client.js +227 -0
- package/dist/core/uat-client.js.map +1 -0
- package/dist/core/version.d.ts +26 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +50 -0
- package/dist/core/version.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/bitable/app.d.ts +20 -0
- package/dist/tools/bitable/app.d.ts.map +1 -0
- package/dist/tools/bitable/app.js +301 -0
- package/dist/tools/bitable/app.js.map +1 -0
- package/dist/tools/bitable/field.d.ts +19 -0
- package/dist/tools/bitable/field.d.ts.map +1 -0
- package/dist/tools/bitable/field.js +315 -0
- package/dist/tools/bitable/field.js.map +1 -0
- package/dist/tools/bitable/index.d.ts +21 -0
- package/dist/tools/bitable/index.d.ts.map +1 -0
- package/dist/tools/bitable/index.js +39 -0
- package/dist/tools/bitable/index.js.map +1 -0
- package/dist/tools/bitable/record.d.ts +22 -0
- package/dist/tools/bitable/record.d.ts.map +1 -0
- package/dist/tools/bitable/record.js +434 -0
- package/dist/tools/bitable/record.js.map +1 -0
- package/dist/tools/bitable/table.d.ts +21 -0
- package/dist/tools/bitable/table.d.ts.map +1 -0
- package/dist/tools/bitable/table.js +361 -0
- package/dist/tools/bitable/table.js.map +1 -0
- package/dist/tools/calendar/calendar.d.ts +18 -0
- package/dist/tools/calendar/calendar.d.ts.map +1 -0
- package/dist/tools/calendar/calendar.js +192 -0
- package/dist/tools/calendar/calendar.js.map +1 -0
- package/dist/tools/calendar/event.d.ts +20 -0
- package/dist/tools/calendar/event.d.ts.map +1 -0
- package/dist/tools/calendar/event.js +465 -0
- package/dist/tools/calendar/event.js.map +1 -0
- package/dist/tools/calendar/index.d.ts +19 -0
- package/dist/tools/calendar/index.d.ts.map +1 -0
- package/dist/tools/calendar/index.js +37 -0
- package/dist/tools/calendar/index.js.map +1 -0
- package/dist/tools/chat/chat.d.ts +11 -0
- package/dist/tools/chat/chat.d.ts.map +1 -0
- package/dist/tools/chat/chat.js +106 -0
- package/dist/tools/chat/chat.js.map +1 -0
- package/dist/tools/chat/index.d.ts +11 -0
- package/dist/tools/chat/index.d.ts.map +1 -0
- package/dist/tools/chat/index.js +20 -0
- package/dist/tools/chat/index.js.map +1 -0
- package/dist/tools/chat/members.d.ts +9 -0
- package/dist/tools/chat/members.d.ts.map +1 -0
- package/dist/tools/chat/members.js +80 -0
- package/dist/tools/chat/members.js.map +1 -0
- package/dist/tools/common/get-user.d.ts +11 -0
- package/dist/tools/common/get-user.d.ts.map +1 -0
- package/dist/tools/common/get-user.js +112 -0
- package/dist/tools/common/get-user.js.map +1 -0
- package/dist/tools/common/index.d.ts +11 -0
- package/dist/tools/common/index.d.ts.map +1 -0
- package/dist/tools/common/index.js +20 -0
- package/dist/tools/common/index.js.map +1 -0
- package/dist/tools/common/search-user.d.ts +9 -0
- package/dist/tools/common/search-user.d.ts.map +1 -0
- package/dist/tools/common/search-user.js +88 -0
- package/dist/tools/common/search-user.js.map +1 -0
- package/dist/tools/doc/create.d.ts +17 -0
- package/dist/tools/doc/create.d.ts.map +1 -0
- package/dist/tools/doc/create.js +159 -0
- package/dist/tools/doc/create.js.map +1 -0
- package/dist/tools/doc/fetch.d.ts +17 -0
- package/dist/tools/doc/fetch.d.ts.map +1 -0
- package/dist/tools/doc/fetch.js +123 -0
- package/dist/tools/doc/fetch.js.map +1 -0
- package/dist/tools/doc/index.d.ts +21 -0
- package/dist/tools/doc/index.d.ts.map +1 -0
- package/dist/tools/doc/index.js +33 -0
- package/dist/tools/doc/index.js.map +1 -0
- package/dist/tools/doc/shared.d.ts +69 -0
- package/dist/tools/doc/shared.d.ts.map +1 -0
- package/dist/tools/doc/shared.js +172 -0
- package/dist/tools/doc/shared.js.map +1 -0
- package/dist/tools/doc/update.d.ts +25 -0
- package/dist/tools/doc/update.d.ts.map +1 -0
- package/dist/tools/doc/update.js +208 -0
- package/dist/tools/doc/update.js.map +1 -0
- package/dist/tools/drive/file.d.ts +13 -0
- package/dist/tools/drive/file.d.ts.map +1 -0
- package/dist/tools/drive/file.js +212 -0
- package/dist/tools/drive/file.js.map +1 -0
- package/dist/tools/drive/index.d.ts +12 -0
- package/dist/tools/drive/index.d.ts.map +1 -0
- package/dist/tools/drive/index.js +25 -0
- package/dist/tools/drive/index.js.map +1 -0
- package/dist/tools/im/format-messages.d.ts +99 -0
- package/dist/tools/im/format-messages.d.ts.map +1 -0
- package/dist/tools/im/format-messages.js +277 -0
- package/dist/tools/im/format-messages.js.map +1 -0
- package/dist/tools/im/helpers.d.ts +53 -0
- package/dist/tools/im/helpers.d.ts.map +1 -0
- package/dist/tools/im/helpers.js +85 -0
- package/dist/tools/im/helpers.js.map +1 -0
- package/dist/tools/im/index.d.ts +25 -0
- package/dist/tools/im/index.d.ts.map +1 -0
- package/dist/tools/im/index.js +44 -0
- package/dist/tools/im/index.js.map +1 -0
- package/dist/tools/im/message-read.d.ts +19 -0
- package/dist/tools/im/message-read.d.ts.map +1 -0
- package/dist/tools/im/message-read.js +526 -0
- package/dist/tools/im/message-read.js.map +1 -0
- package/dist/tools/im/message.d.ts +22 -0
- package/dist/tools/im/message.d.ts.map +1 -0
- package/dist/tools/im/message.js +233 -0
- package/dist/tools/im/message.js.map +1 -0
- package/dist/tools/im/resource.d.ts +19 -0
- package/dist/tools/im/resource.d.ts.map +1 -0
- package/dist/tools/im/resource.js +185 -0
- package/dist/tools/im/resource.js.map +1 -0
- package/dist/tools/im/time-utils.d.ts +70 -0
- package/dist/tools/im/time-utils.d.ts.map +1 -0
- package/dist/tools/im/time-utils.js +277 -0
- package/dist/tools/im/time-utils.js.map +1 -0
- package/dist/tools/index.d.ts +85 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +135 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/oauth.d.ts +15 -0
- package/dist/tools/oauth.d.ts.map +1 -0
- package/dist/tools/oauth.js +379 -0
- package/dist/tools/oauth.js.map +1 -0
- package/dist/tools/search/doc-search.d.ts +9 -0
- package/dist/tools/search/doc-search.d.ts.map +1 -0
- package/dist/tools/search/doc-search.js +219 -0
- package/dist/tools/search/doc-search.js.map +1 -0
- package/dist/tools/search/index.d.ts +11 -0
- package/dist/tools/search/index.d.ts.map +1 -0
- package/dist/tools/search/index.js +18 -0
- package/dist/tools/search/index.js.map +1 -0
- package/dist/tools/sheets/index.d.ts +11 -0
- package/dist/tools/sheets/index.d.ts.map +1 -0
- package/dist/tools/sheets/index.js +18 -0
- package/dist/tools/sheets/index.js.map +1 -0
- package/dist/tools/sheets/sheet.d.ts +11 -0
- package/dist/tools/sheets/sheet.d.ts.map +1 -0
- package/dist/tools/sheets/sheet.js +332 -0
- package/dist/tools/sheets/sheet.js.map +1 -0
- package/dist/tools/task/index.d.ts +12 -0
- package/dist/tools/task/index.d.ts.map +1 -0
- package/dist/tools/task/index.js +30 -0
- package/dist/tools/task/index.js.map +1 -0
- package/dist/tools/task/task.d.ts +13 -0
- package/dist/tools/task/task.d.ts.map +1 -0
- package/dist/tools/task/task.js +225 -0
- package/dist/tools/task/task.js.map +1 -0
- package/dist/tools/task/tasklist.d.ts +13 -0
- package/dist/tools/task/tasklist.d.ts.map +1 -0
- package/dist/tools/task/tasklist.js +206 -0
- package/dist/tools/task/tasklist.js.map +1 -0
- package/dist/tools/wiki/index.d.ts +11 -0
- package/dist/tools/wiki/index.d.ts.map +1 -0
- package/dist/tools/wiki/index.js +20 -0
- package/dist/tools/wiki/index.js.map +1 -0
- package/dist/tools/wiki/node.d.ts +11 -0
- package/dist/tools/wiki/node.d.ts.map +1 -0
- package/dist/tools/wiki/node.js +112 -0
- package/dist/tools/wiki/node.js.map +1 -0
- package/dist/tools/wiki/space.d.ts +11 -0
- package/dist/tools/wiki/space.d.ts.map +1 -0
- package/dist/tools/wiki/space.js +125 -0
- package/dist/tools/wiki/space.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +101 -0
- package/dist/utils/logger.js.map +1 -0
- package/eslint.config.js +13 -0
- package/package.json +54 -0
- package/skills/feishu-bitable/SKILL.md +248 -0
- package/skills/feishu-bitable/references/examples.md +813 -0
- package/skills/feishu-bitable/references/field-properties.md +763 -0
- package/skills/feishu-bitable/references/record-values.md +911 -0
- package/skills/feishu-calendar/SKILL.md +244 -0
- package/skills/feishu-channel-rules/SKILL.md +18 -0
- package/skills/feishu-channel-rules/references/markdown-syntax.md +138 -0
- package/skills/feishu-create-doc/SKILL.md +719 -0
- package/skills/feishu-fetch-doc/SKILL.md +93 -0
- package/skills/feishu-im-read/SKILL.md +163 -0
- package/skills/feishu-task/SKILL.md +293 -0
- package/skills/feishu-troubleshoot/SKILL.md +70 -0
- package/skills/feishu-update-doc/SKILL.md +285 -0
- package/src/core/api-error.ts +342 -0
- package/src/core/auth-errors.ts +27 -0
- package/src/core/config.ts +134 -0
- package/src/core/device-flow.ts +314 -0
- package/src/core/index.ts +16 -0
- package/src/core/lark-client.ts +391 -0
- package/src/core/token-store.ts +249 -0
- package/src/core/types.ts +302 -0
- package/src/core/uat-client.ts +298 -0
- package/src/core/version.ts +53 -0
- package/src/index.ts +138 -0
- package/src/tools/bitable/app.ts +390 -0
- package/src/tools/bitable/field.ts +406 -0
- package/src/tools/bitable/index.ts +43 -0
- package/src/tools/bitable/record.ts +559 -0
- package/src/tools/bitable/table.ts +472 -0
- package/src/tools/calendar/calendar.ts +254 -0
- package/src/tools/calendar/event.ts +606 -0
- package/src/tools/calendar/index.ts +41 -0
- package/src/tools/chat/chat.ts +127 -0
- package/src/tools/chat/index.ts +24 -0
- package/src/tools/chat/members.ts +93 -0
- package/src/tools/common/get-user.ts +127 -0
- package/src/tools/common/index.ts +24 -0
- package/src/tools/common/search-user.ts +99 -0
- package/src/tools/doc/create.ts +184 -0
- package/src/tools/doc/fetch.ts +149 -0
- package/src/tools/doc/index.ts +38 -0
- package/src/tools/doc/shared.ts +228 -0
- package/src/tools/doc/update.ts +240 -0
- package/src/tools/drive/file.ts +265 -0
- package/src/tools/drive/index.ts +29 -0
- package/src/tools/im/format-messages.ts +391 -0
- package/src/tools/im/helpers.ts +109 -0
- package/src/tools/im/index.ts +49 -0
- package/src/tools/im/message-read.ts +676 -0
- package/src/tools/im/message.ts +303 -0
- package/src/tools/im/resource.ts +225 -0
- package/src/tools/im/time-utils.ts +347 -0
- package/src/tools/index.ts +205 -0
- package/src/tools/oauth.ts +460 -0
- package/src/tools/search/doc-search.ts +250 -0
- package/src/tools/search/index.ts +22 -0
- package/src/tools/sheets/index.ts +22 -0
- package/src/tools/sheets/sheet.ts +382 -0
- package/src/tools/task/index.ts +34 -0
- package/src/tools/task/task.ts +265 -0
- package/src/tools/task/tasklist.ts +262 -0
- package/src/tools/wiki/index.ts +24 -0
- package/src/tools/wiki/node.ts +131 -0
- package/src/tools/wiki/space.ts +152 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +132 -0
- package/tests/core/config.test.ts +238 -0
- package/tests/core/device-flow.test.ts +490 -0
- package/tests/core/lark-client.test.ts +378 -0
- package/tests/core/token-store.test.ts +438 -0
- package/tests/index.test.ts +360 -0
- package/tests/tools/doc/create.test.ts +224 -0
- package/tests/tools/doc/fetch.test.ts +182 -0
- package/tests/tools/doc/shared.test.ts +183 -0
- package/tests/tools/doc/update.test.ts +330 -0
- package/tests/tools/im/format-messages.test.ts +184 -0
- package/tests/tools/im/time-utils.test.ts +178 -0
- package/tests/utils/logger.test.ts +140 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Configuration management for the cc-lark MCP Server.
|
|
6
|
+
*
|
|
7
|
+
* Loads configuration from environment variables and provides validation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FeishuConfig, ConfigValidationResult, LarkBrand } from './types.js';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Environment variable names
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export const ENV_VARS = {
|
|
17
|
+
APP_ID: 'FEISHU_APP_ID',
|
|
18
|
+
APP_SECRET: 'FEISHU_APP_SECRET',
|
|
19
|
+
USER_ACCESS_TOKEN: 'FEISHU_USER_ACCESS_TOKEN',
|
|
20
|
+
BRAND: 'FEISHU_BRAND',
|
|
21
|
+
ENCRYPT_KEY: 'FEISHU_ENCRYPT_KEY',
|
|
22
|
+
VERIFICATION_TOKEN: 'FEISHU_VERIFICATION_TOKEN',
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Configuration loader
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load Feishu configuration from environment variables.
|
|
31
|
+
*
|
|
32
|
+
* Required:
|
|
33
|
+
* - FEISHU_APP_ID: Feishu App ID
|
|
34
|
+
* - FEISHU_APP_SECRET: Feishu App Secret
|
|
35
|
+
*
|
|
36
|
+
* Optional:
|
|
37
|
+
* - FEISHU_USER_ACCESS_TOKEN: User access token for user-authorized operations
|
|
38
|
+
* - FEISHU_BRAND: Platform brand ('feishu' | 'lark' | custom URL)
|
|
39
|
+
* - FEISHU_ENCRYPT_KEY: Encryption key for webhook events
|
|
40
|
+
* - FEISHU_VERIFICATION_TOKEN: Verification token for webhooks
|
|
41
|
+
*/
|
|
42
|
+
export function loadConfig(): FeishuConfig {
|
|
43
|
+
const appId = process.env[ENV_VARS.APP_ID];
|
|
44
|
+
const appSecret = process.env[ENV_VARS.APP_SECRET];
|
|
45
|
+
const userAccessToken = process.env[ENV_VARS.USER_ACCESS_TOKEN];
|
|
46
|
+
const brand = process.env[ENV_VARS.BRAND] as LarkBrand | undefined;
|
|
47
|
+
const encryptKey = process.env[ENV_VARS.ENCRYPT_KEY];
|
|
48
|
+
const verificationToken = process.env[ENV_VARS.VERIFICATION_TOKEN];
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
appId: appId ?? '',
|
|
52
|
+
appSecret: appSecret ?? '',
|
|
53
|
+
userAccessToken,
|
|
54
|
+
brand: brand ?? 'feishu',
|
|
55
|
+
encryptKey,
|
|
56
|
+
verificationToken,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate the Feishu configuration.
|
|
62
|
+
*
|
|
63
|
+
* @param config - Configuration to validate
|
|
64
|
+
* @returns Validation result with errors if any
|
|
65
|
+
*/
|
|
66
|
+
export function validateConfig(config: FeishuConfig): ConfigValidationResult {
|
|
67
|
+
const errors: string[] = [];
|
|
68
|
+
|
|
69
|
+
if (!config.appId) {
|
|
70
|
+
errors.push(`Missing required environment variable: ${ENV_VARS.APP_ID}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!config.appSecret) {
|
|
74
|
+
errors.push(`Missing required environment variable: ${ENV_VARS.APP_SECRET}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (config.brand) {
|
|
78
|
+
const validBrands: LarkBrand[] = ['feishu', 'lark'];
|
|
79
|
+
if (!validBrands.includes(config.brand) && !config.brand.startsWith('https://')) {
|
|
80
|
+
errors.push(
|
|
81
|
+
`Invalid FEISHU_BRAND value: "${config.brand}". Must be "feishu", "lark", or a custom HTTPS URL`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (errors.length > 0) {
|
|
87
|
+
return { valid: false, errors };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { valid: true, errors: [], config };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Load and validate configuration in one step.
|
|
95
|
+
*
|
|
96
|
+
* @throws Error if configuration is invalid
|
|
97
|
+
* @returns Validated Feishu configuration
|
|
98
|
+
*/
|
|
99
|
+
export function loadAndValidateConfig(): FeishuConfig {
|
|
100
|
+
const config = loadConfig();
|
|
101
|
+
const result = validateConfig(config);
|
|
102
|
+
|
|
103
|
+
if (!result.valid) {
|
|
104
|
+
throw new Error(`Invalid configuration:\n${result.errors.map(e => ` - ${e}`).join('\n')}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return config;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if configuration has user access token.
|
|
112
|
+
*
|
|
113
|
+
* @param config - Configuration to check
|
|
114
|
+
* @returns True if user access token is configured
|
|
115
|
+
*/
|
|
116
|
+
export function hasUserAccessToken(config: FeishuConfig): boolean {
|
|
117
|
+
return typeof config.userAccessToken === 'string' && config.userAccessToken.length > 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the base URL for the Lark API based on brand.
|
|
122
|
+
*
|
|
123
|
+
* @param brand - Platform brand
|
|
124
|
+
* @returns Base URL for API calls
|
|
125
|
+
*/
|
|
126
|
+
export function getApiBaseUrl(brand: LarkBrand = 'feishu'): string {
|
|
127
|
+
if (brand === 'lark') {
|
|
128
|
+
return 'https://open.larksuite.com/open-apis';
|
|
129
|
+
}
|
|
130
|
+
if (brand.startsWith('https://')) {
|
|
131
|
+
return brand;
|
|
132
|
+
}
|
|
133
|
+
return 'https://open.feishu.cn/open-apis';
|
|
134
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* OAuth 2.0 Device Authorization Grant (RFC 8628) for Lark/Feishu.
|
|
6
|
+
*
|
|
7
|
+
* Two-step flow:
|
|
8
|
+
* 1. `requestDeviceAuthorization` – obtains device_code + user_code.
|
|
9
|
+
* 2. `pollDeviceToken` – polls the token endpoint until the user authorizes,
|
|
10
|
+
* rejects, or the code expires.
|
|
11
|
+
*
|
|
12
|
+
* All HTTP calls use the built-in `fetch` (Node 18+). The Lark SDK is not
|
|
13
|
+
* used here because these OAuth endpoints are outside the SDK's scope.
|
|
14
|
+
*
|
|
15
|
+
* Adapted from openclaw-lark for MCP Server architecture.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { LarkBrand } from './types.js';
|
|
19
|
+
import { logger } from '../utils/logger.js';
|
|
20
|
+
import { getUserAgent } from './version.js';
|
|
21
|
+
|
|
22
|
+
const log = logger('device-flow');
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Types
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export interface DeviceAuthResponse {
|
|
29
|
+
deviceCode: string;
|
|
30
|
+
userCode: string;
|
|
31
|
+
verificationUri: string;
|
|
32
|
+
verificationUriComplete: string;
|
|
33
|
+
expiresIn: number; // seconds
|
|
34
|
+
interval: number; // recommended polling interval (seconds)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DeviceFlowTokenData {
|
|
38
|
+
accessToken: string;
|
|
39
|
+
refreshToken: string;
|
|
40
|
+
expiresIn: number; // seconds
|
|
41
|
+
refreshExpiresIn: number; // seconds
|
|
42
|
+
scope: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type DeviceFlowResult =
|
|
46
|
+
| { ok: true; token: DeviceFlowTokenData }
|
|
47
|
+
| { ok: false; error: DeviceFlowError; message: string };
|
|
48
|
+
|
|
49
|
+
export type DeviceFlowError = 'authorization_pending' | 'slow_down' | 'access_denied' | 'expired_token';
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// HTTP helper
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Header-aware fetch for Lark API calls.
|
|
57
|
+
*
|
|
58
|
+
* Drop-in replacement for `fetch()` that automatically injects
|
|
59
|
+
* the User-Agent header.
|
|
60
|
+
*/
|
|
61
|
+
function larkFetch(url: string | URL | Request, init?: RequestInit): Promise<Response> {
|
|
62
|
+
const headers = {
|
|
63
|
+
...init?.headers,
|
|
64
|
+
'User-Agent': getUserAgent(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return fetch(url, { ...init, headers });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Endpoint resolution
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the two OAuth endpoint URLs based on the configured brand.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveOAuthEndpoints(brand: LarkBrand): {
|
|
78
|
+
deviceAuthorization: string;
|
|
79
|
+
token: string;
|
|
80
|
+
} {
|
|
81
|
+
if (!brand || brand === 'feishu') {
|
|
82
|
+
return {
|
|
83
|
+
deviceAuthorization: 'https://accounts.feishu.cn/oauth/v1/device_authorization',
|
|
84
|
+
token: 'https://open.feishu.cn/open-apis/authen/v2/oauth/token',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (brand === 'lark') {
|
|
88
|
+
return {
|
|
89
|
+
deviceAuthorization: 'https://accounts.larksuite.com/oauth/v1/device_authorization',
|
|
90
|
+
token: 'https://open.larksuite.com/open-apis/authen/v2/oauth/token',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Custom domain – derive paths by convention.
|
|
94
|
+
// Smart derivation: open.X → accounts.X for the device authorization endpoint.
|
|
95
|
+
const base = brand.replace(/\/+$/, '');
|
|
96
|
+
let accountsBase = base;
|
|
97
|
+
try {
|
|
98
|
+
const parsed = new URL(base);
|
|
99
|
+
if (parsed.hostname.startsWith('open.')) {
|
|
100
|
+
accountsBase = `${parsed.protocol}//${parsed.hostname.replace(/^open\./, 'accounts.')}`;
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
/* fallback to base */
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
deviceAuthorization: `${accountsBase}/oauth/v1/device_authorization`,
|
|
108
|
+
token: `${base}/open-apis/authen/v2/oauth/token`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Step 1 – Device Authorization Request
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Request a device authorization code from the Feishu OAuth server.
|
|
118
|
+
*
|
|
119
|
+
* Uses Confidential Client authentication (HTTP Basic with appId:appSecret).
|
|
120
|
+
* The `offline_access` scope is automatically appended so that the token
|
|
121
|
+
* response includes a refresh_token.
|
|
122
|
+
*/
|
|
123
|
+
export async function requestDeviceAuthorization(params: {
|
|
124
|
+
appId: string;
|
|
125
|
+
appSecret: string;
|
|
126
|
+
brand: LarkBrand;
|
|
127
|
+
scope?: string;
|
|
128
|
+
}): Promise<DeviceAuthResponse> {
|
|
129
|
+
const { appId, appSecret, brand } = params;
|
|
130
|
+
const endpoints = resolveOAuthEndpoints(brand);
|
|
131
|
+
|
|
132
|
+
// Ensure offline_access is always requested.
|
|
133
|
+
let scope = params.scope ?? '';
|
|
134
|
+
if (!scope.includes('offline_access')) {
|
|
135
|
+
scope = scope ? `${scope} offline_access` : 'offline_access';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const basicAuth = Buffer.from(`${appId}:${appSecret}`).toString('base64');
|
|
139
|
+
|
|
140
|
+
const body = new URLSearchParams();
|
|
141
|
+
body.set('client_id', appId);
|
|
142
|
+
body.set('scope', scope);
|
|
143
|
+
|
|
144
|
+
log.info(
|
|
145
|
+
`requesting device authorization (scope="${scope}") url=${endpoints.deviceAuthorization} token_url=${endpoints.token}`
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const resp = await larkFetch(endpoints.deviceAuthorization, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: {
|
|
151
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
152
|
+
Authorization: `Basic ${basicAuth}`,
|
|
153
|
+
},
|
|
154
|
+
body: body.toString(),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const text = await resp.text();
|
|
158
|
+
log.info(`response status=${resp.status} body=${text.slice(0, 500)}`);
|
|
159
|
+
|
|
160
|
+
let data: Record<string, unknown>;
|
|
161
|
+
try {
|
|
162
|
+
data = JSON.parse(text) as Record<string, unknown>;
|
|
163
|
+
} catch {
|
|
164
|
+
throw new Error(`Device authorization failed: HTTP ${resp.status} – ${text.slice(0, 200)}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!resp.ok || data.error) {
|
|
168
|
+
const msg = (data.error_description as string) ?? (data.error as string) ?? 'Unknown error';
|
|
169
|
+
throw new Error(`Device authorization failed: ${msg}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const expiresIn = (data.expires_in as number) ?? 240;
|
|
173
|
+
const interval = (data.interval as number) ?? 5;
|
|
174
|
+
log.info(
|
|
175
|
+
`device_code obtained, expires_in=${expiresIn}s (${Math.round(expiresIn / 60)}min), interval=${interval}s`
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
deviceCode: data.device_code as string,
|
|
180
|
+
userCode: data.user_code as string,
|
|
181
|
+
verificationUri: data.verification_uri as string,
|
|
182
|
+
verificationUriComplete: (data.verification_uri_complete as string) ?? (data.verification_uri as string),
|
|
183
|
+
expiresIn,
|
|
184
|
+
interval,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Step 2 – Poll Token Endpoint
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
193
|
+
return new Promise<void>((resolve, reject) => {
|
|
194
|
+
const timer = setTimeout(resolve, ms);
|
|
195
|
+
signal?.addEventListener(
|
|
196
|
+
'abort',
|
|
197
|
+
() => {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
200
|
+
},
|
|
201
|
+
{ once: true }
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Poll the token endpoint until the user authorizes, rejects, or the code
|
|
208
|
+
* expires.
|
|
209
|
+
*
|
|
210
|
+
* Handles `authorization_pending` (keep polling), `slow_down` (back off by
|
|
211
|
+
* +5 s), `access_denied` and `expired_token` (terminal errors).
|
|
212
|
+
*
|
|
213
|
+
* Pass an `AbortSignal` to cancel polling from the outside.
|
|
214
|
+
*/
|
|
215
|
+
export async function pollDeviceToken(params: {
|
|
216
|
+
appId: string;
|
|
217
|
+
appSecret: string;
|
|
218
|
+
brand: LarkBrand;
|
|
219
|
+
deviceCode: string;
|
|
220
|
+
interval: number;
|
|
221
|
+
expiresIn: number;
|
|
222
|
+
signal?: AbortSignal;
|
|
223
|
+
}): Promise<DeviceFlowResult> {
|
|
224
|
+
const MAX_POLL_INTERVAL = 60; // slow_down max interval 60 seconds
|
|
225
|
+
const MAX_POLL_ATTEMPTS = 200; // Safety limit (well beyond device code expiry)
|
|
226
|
+
|
|
227
|
+
const { appId, appSecret, brand, deviceCode, expiresIn, signal } = params;
|
|
228
|
+
let interval = params.interval;
|
|
229
|
+
const endpoints = resolveOAuthEndpoints(brand);
|
|
230
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
231
|
+
let attempts = 0;
|
|
232
|
+
|
|
233
|
+
while (Date.now() < deadline && attempts < MAX_POLL_ATTEMPTS) {
|
|
234
|
+
attempts++;
|
|
235
|
+
if (signal?.aborted) {
|
|
236
|
+
return { ok: false, error: 'expired_token', message: 'Polling was cancelled' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await sleep(interval * 1000, signal);
|
|
240
|
+
|
|
241
|
+
let data: Record<string, unknown>;
|
|
242
|
+
try {
|
|
243
|
+
const resp = await larkFetch(endpoints.token, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
246
|
+
body: new URLSearchParams({
|
|
247
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
248
|
+
device_code: deviceCode,
|
|
249
|
+
client_id: appId,
|
|
250
|
+
client_secret: appSecret,
|
|
251
|
+
}).toString(),
|
|
252
|
+
});
|
|
253
|
+
data = (await resp.json()) as Record<string, unknown>;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
log.warn(`poll network error: ${err}`);
|
|
256
|
+
interval = Math.min(interval + 1, MAX_POLL_INTERVAL);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const error = data.error as string | undefined;
|
|
261
|
+
|
|
262
|
+
if (!error && data.access_token) {
|
|
263
|
+
log.info('token obtained successfully');
|
|
264
|
+
const refreshToken = (data.refresh_token as string) ?? '';
|
|
265
|
+
const expiresIn = (data.expires_in as number) ?? 7200;
|
|
266
|
+
let refreshExpiresIn = (data.refresh_token_expires_in as number) ?? 604800;
|
|
267
|
+
if (!refreshToken) {
|
|
268
|
+
log.warn('no refresh_token in response, token will not be refreshable');
|
|
269
|
+
refreshExpiresIn = expiresIn;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
ok: true,
|
|
273
|
+
token: {
|
|
274
|
+
accessToken: data.access_token as string,
|
|
275
|
+
refreshToken,
|
|
276
|
+
expiresIn,
|
|
277
|
+
refreshExpiresIn,
|
|
278
|
+
scope: (data.scope as string) ?? '',
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (error === 'authorization_pending') {
|
|
284
|
+
log.debug('authorization_pending, retrying...');
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (error === 'slow_down') {
|
|
289
|
+
interval = Math.min(interval + 5, MAX_POLL_INTERVAL);
|
|
290
|
+
log.info(`slow_down, interval increased to ${interval}s`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (error === 'access_denied') {
|
|
295
|
+
log.info('user denied authorization');
|
|
296
|
+
return { ok: false, error: 'access_denied', message: 'User denied authorization' };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (error === 'expired_token' || error === 'invalid_grant') {
|
|
300
|
+
log.info(`device code expired/invalid (error=${error})`);
|
|
301
|
+
return { ok: false, error: 'expired_token', message: 'Authorization code expired, please retry' };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Unknown error – treat as terminal.
|
|
305
|
+
const desc = (data.error_description as string) ?? error ?? 'Unknown error';
|
|
306
|
+
log.warn(`unexpected error: error=${error}, desc=${desc}`);
|
|
307
|
+
return { ok: false, error: 'expired_token', message: desc };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (attempts >= MAX_POLL_ATTEMPTS) {
|
|
311
|
+
log.warn(`max poll attempts (${MAX_POLL_ATTEMPTS}) reached`);
|
|
312
|
+
}
|
|
313
|
+
return { ok: false, error: 'expired_token', message: 'Authorization timed out, please retry' };
|
|
314
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Core module exports for cc-lark MCP Server.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export * from './config.js';
|
|
10
|
+
export * from './api-error.js';
|
|
11
|
+
export * from './lark-client.js';
|
|
12
|
+
export * from './version.js';
|
|
13
|
+
export * from './auth-errors.js';
|
|
14
|
+
export * from './device-flow.js';
|
|
15
|
+
export * from './token-store.js';
|
|
16
|
+
export * from './uat-client.js';
|