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.
Files changed (298) hide show
  1. package/.github/workflows/ci.yml +47 -0
  2. package/.github/workflows/release.yml +47 -0
  3. package/.github/workflows/sync-upstream.yml +127 -0
  4. package/.prettierrc.json +7 -0
  5. package/README.md +214 -0
  6. package/dist/core/api-error.d.ts +193 -0
  7. package/dist/core/api-error.d.ts.map +1 -0
  8. package/dist/core/api-error.js +263 -0
  9. package/dist/core/api-error.js.map +1 -0
  10. package/dist/core/auth-errors.d.ts +13 -0
  11. package/dist/core/auth-errors.d.ts.map +1 -0
  12. package/dist/core/auth-errors.js +14 -0
  13. package/dist/core/auth-errors.js.map +1 -0
  14. package/dist/core/config.d.ts +60 -0
  15. package/dist/core/config.d.ts.map +1 -0
  16. package/dist/core/config.js +115 -0
  17. package/dist/core/config.js.map +1 -0
  18. package/dist/core/device-flow.d.ts +80 -0
  19. package/dist/core/device-flow.d.ts.map +1 -0
  20. package/dist/core/device-flow.js +231 -0
  21. package/dist/core/device-flow.js.map +1 -0
  22. package/dist/core/index.d.ts +16 -0
  23. package/dist/core/index.d.ts.map +1 -0
  24. package/dist/core/index.js +16 -0
  25. package/dist/core/index.js.map +1 -0
  26. package/dist/core/lark-client.d.ts +136 -0
  27. package/dist/core/lark-client.d.ts.map +1 -0
  28. package/dist/core/lark-client.js +315 -0
  29. package/dist/core/lark-client.js.map +1 -0
  30. package/dist/core/token-store.d.ts +67 -0
  31. package/dist/core/token-store.d.ts.map +1 -0
  32. package/dist/core/token-store.js +215 -0
  33. package/dist/core/token-store.js.map +1 -0
  34. package/dist/core/types.d.ts +286 -0
  35. package/dist/core/types.d.ts.map +1 -0
  36. package/dist/core/types.js +11 -0
  37. package/dist/core/types.js.map +1 -0
  38. package/dist/core/uat-client.d.ts +64 -0
  39. package/dist/core/uat-client.d.ts.map +1 -0
  40. package/dist/core/uat-client.js +227 -0
  41. package/dist/core/uat-client.js.map +1 -0
  42. package/dist/core/version.d.ts +26 -0
  43. package/dist/core/version.d.ts.map +1 -0
  44. package/dist/core/version.js +50 -0
  45. package/dist/core/version.js.map +1 -0
  46. package/dist/index.d.ts +12 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +116 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/tools/bitable/app.d.ts +20 -0
  51. package/dist/tools/bitable/app.d.ts.map +1 -0
  52. package/dist/tools/bitable/app.js +301 -0
  53. package/dist/tools/bitable/app.js.map +1 -0
  54. package/dist/tools/bitable/field.d.ts +19 -0
  55. package/dist/tools/bitable/field.d.ts.map +1 -0
  56. package/dist/tools/bitable/field.js +315 -0
  57. package/dist/tools/bitable/field.js.map +1 -0
  58. package/dist/tools/bitable/index.d.ts +21 -0
  59. package/dist/tools/bitable/index.d.ts.map +1 -0
  60. package/dist/tools/bitable/index.js +39 -0
  61. package/dist/tools/bitable/index.js.map +1 -0
  62. package/dist/tools/bitable/record.d.ts +22 -0
  63. package/dist/tools/bitable/record.d.ts.map +1 -0
  64. package/dist/tools/bitable/record.js +434 -0
  65. package/dist/tools/bitable/record.js.map +1 -0
  66. package/dist/tools/bitable/table.d.ts +21 -0
  67. package/dist/tools/bitable/table.d.ts.map +1 -0
  68. package/dist/tools/bitable/table.js +361 -0
  69. package/dist/tools/bitable/table.js.map +1 -0
  70. package/dist/tools/calendar/calendar.d.ts +18 -0
  71. package/dist/tools/calendar/calendar.d.ts.map +1 -0
  72. package/dist/tools/calendar/calendar.js +192 -0
  73. package/dist/tools/calendar/calendar.js.map +1 -0
  74. package/dist/tools/calendar/event.d.ts +20 -0
  75. package/dist/tools/calendar/event.d.ts.map +1 -0
  76. package/dist/tools/calendar/event.js +465 -0
  77. package/dist/tools/calendar/event.js.map +1 -0
  78. package/dist/tools/calendar/index.d.ts +19 -0
  79. package/dist/tools/calendar/index.d.ts.map +1 -0
  80. package/dist/tools/calendar/index.js +37 -0
  81. package/dist/tools/calendar/index.js.map +1 -0
  82. package/dist/tools/chat/chat.d.ts +11 -0
  83. package/dist/tools/chat/chat.d.ts.map +1 -0
  84. package/dist/tools/chat/chat.js +106 -0
  85. package/dist/tools/chat/chat.js.map +1 -0
  86. package/dist/tools/chat/index.d.ts +11 -0
  87. package/dist/tools/chat/index.d.ts.map +1 -0
  88. package/dist/tools/chat/index.js +20 -0
  89. package/dist/tools/chat/index.js.map +1 -0
  90. package/dist/tools/chat/members.d.ts +9 -0
  91. package/dist/tools/chat/members.d.ts.map +1 -0
  92. package/dist/tools/chat/members.js +80 -0
  93. package/dist/tools/chat/members.js.map +1 -0
  94. package/dist/tools/common/get-user.d.ts +11 -0
  95. package/dist/tools/common/get-user.d.ts.map +1 -0
  96. package/dist/tools/common/get-user.js +112 -0
  97. package/dist/tools/common/get-user.js.map +1 -0
  98. package/dist/tools/common/index.d.ts +11 -0
  99. package/dist/tools/common/index.d.ts.map +1 -0
  100. package/dist/tools/common/index.js +20 -0
  101. package/dist/tools/common/index.js.map +1 -0
  102. package/dist/tools/common/search-user.d.ts +9 -0
  103. package/dist/tools/common/search-user.d.ts.map +1 -0
  104. package/dist/tools/common/search-user.js +88 -0
  105. package/dist/tools/common/search-user.js.map +1 -0
  106. package/dist/tools/doc/create.d.ts +17 -0
  107. package/dist/tools/doc/create.d.ts.map +1 -0
  108. package/dist/tools/doc/create.js +159 -0
  109. package/dist/tools/doc/create.js.map +1 -0
  110. package/dist/tools/doc/fetch.d.ts +17 -0
  111. package/dist/tools/doc/fetch.d.ts.map +1 -0
  112. package/dist/tools/doc/fetch.js +123 -0
  113. package/dist/tools/doc/fetch.js.map +1 -0
  114. package/dist/tools/doc/index.d.ts +21 -0
  115. package/dist/tools/doc/index.d.ts.map +1 -0
  116. package/dist/tools/doc/index.js +33 -0
  117. package/dist/tools/doc/index.js.map +1 -0
  118. package/dist/tools/doc/shared.d.ts +69 -0
  119. package/dist/tools/doc/shared.d.ts.map +1 -0
  120. package/dist/tools/doc/shared.js +172 -0
  121. package/dist/tools/doc/shared.js.map +1 -0
  122. package/dist/tools/doc/update.d.ts +25 -0
  123. package/dist/tools/doc/update.d.ts.map +1 -0
  124. package/dist/tools/doc/update.js +208 -0
  125. package/dist/tools/doc/update.js.map +1 -0
  126. package/dist/tools/drive/file.d.ts +13 -0
  127. package/dist/tools/drive/file.d.ts.map +1 -0
  128. package/dist/tools/drive/file.js +212 -0
  129. package/dist/tools/drive/file.js.map +1 -0
  130. package/dist/tools/drive/index.d.ts +12 -0
  131. package/dist/tools/drive/index.d.ts.map +1 -0
  132. package/dist/tools/drive/index.js +25 -0
  133. package/dist/tools/drive/index.js.map +1 -0
  134. package/dist/tools/im/format-messages.d.ts +99 -0
  135. package/dist/tools/im/format-messages.d.ts.map +1 -0
  136. package/dist/tools/im/format-messages.js +277 -0
  137. package/dist/tools/im/format-messages.js.map +1 -0
  138. package/dist/tools/im/helpers.d.ts +53 -0
  139. package/dist/tools/im/helpers.d.ts.map +1 -0
  140. package/dist/tools/im/helpers.js +85 -0
  141. package/dist/tools/im/helpers.js.map +1 -0
  142. package/dist/tools/im/index.d.ts +25 -0
  143. package/dist/tools/im/index.d.ts.map +1 -0
  144. package/dist/tools/im/index.js +44 -0
  145. package/dist/tools/im/index.js.map +1 -0
  146. package/dist/tools/im/message-read.d.ts +19 -0
  147. package/dist/tools/im/message-read.d.ts.map +1 -0
  148. package/dist/tools/im/message-read.js +526 -0
  149. package/dist/tools/im/message-read.js.map +1 -0
  150. package/dist/tools/im/message.d.ts +22 -0
  151. package/dist/tools/im/message.d.ts.map +1 -0
  152. package/dist/tools/im/message.js +233 -0
  153. package/dist/tools/im/message.js.map +1 -0
  154. package/dist/tools/im/resource.d.ts +19 -0
  155. package/dist/tools/im/resource.d.ts.map +1 -0
  156. package/dist/tools/im/resource.js +185 -0
  157. package/dist/tools/im/resource.js.map +1 -0
  158. package/dist/tools/im/time-utils.d.ts +70 -0
  159. package/dist/tools/im/time-utils.d.ts.map +1 -0
  160. package/dist/tools/im/time-utils.js +277 -0
  161. package/dist/tools/im/time-utils.js.map +1 -0
  162. package/dist/tools/index.d.ts +85 -0
  163. package/dist/tools/index.d.ts.map +1 -0
  164. package/dist/tools/index.js +135 -0
  165. package/dist/tools/index.js.map +1 -0
  166. package/dist/tools/oauth.d.ts +15 -0
  167. package/dist/tools/oauth.d.ts.map +1 -0
  168. package/dist/tools/oauth.js +379 -0
  169. package/dist/tools/oauth.js.map +1 -0
  170. package/dist/tools/search/doc-search.d.ts +9 -0
  171. package/dist/tools/search/doc-search.d.ts.map +1 -0
  172. package/dist/tools/search/doc-search.js +219 -0
  173. package/dist/tools/search/doc-search.js.map +1 -0
  174. package/dist/tools/search/index.d.ts +11 -0
  175. package/dist/tools/search/index.d.ts.map +1 -0
  176. package/dist/tools/search/index.js +18 -0
  177. package/dist/tools/search/index.js.map +1 -0
  178. package/dist/tools/sheets/index.d.ts +11 -0
  179. package/dist/tools/sheets/index.d.ts.map +1 -0
  180. package/dist/tools/sheets/index.js +18 -0
  181. package/dist/tools/sheets/index.js.map +1 -0
  182. package/dist/tools/sheets/sheet.d.ts +11 -0
  183. package/dist/tools/sheets/sheet.d.ts.map +1 -0
  184. package/dist/tools/sheets/sheet.js +332 -0
  185. package/dist/tools/sheets/sheet.js.map +1 -0
  186. package/dist/tools/task/index.d.ts +12 -0
  187. package/dist/tools/task/index.d.ts.map +1 -0
  188. package/dist/tools/task/index.js +30 -0
  189. package/dist/tools/task/index.js.map +1 -0
  190. package/dist/tools/task/task.d.ts +13 -0
  191. package/dist/tools/task/task.d.ts.map +1 -0
  192. package/dist/tools/task/task.js +225 -0
  193. package/dist/tools/task/task.js.map +1 -0
  194. package/dist/tools/task/tasklist.d.ts +13 -0
  195. package/dist/tools/task/tasklist.d.ts.map +1 -0
  196. package/dist/tools/task/tasklist.js +206 -0
  197. package/dist/tools/task/tasklist.js.map +1 -0
  198. package/dist/tools/wiki/index.d.ts +11 -0
  199. package/dist/tools/wiki/index.d.ts.map +1 -0
  200. package/dist/tools/wiki/index.js +20 -0
  201. package/dist/tools/wiki/index.js.map +1 -0
  202. package/dist/tools/wiki/node.d.ts +11 -0
  203. package/dist/tools/wiki/node.d.ts.map +1 -0
  204. package/dist/tools/wiki/node.js +112 -0
  205. package/dist/tools/wiki/node.js.map +1 -0
  206. package/dist/tools/wiki/space.d.ts +11 -0
  207. package/dist/tools/wiki/space.d.ts.map +1 -0
  208. package/dist/tools/wiki/space.js +125 -0
  209. package/dist/tools/wiki/space.js.map +1 -0
  210. package/dist/utils/index.d.ts +8 -0
  211. package/dist/utils/index.d.ts.map +1 -0
  212. package/dist/utils/index.js +8 -0
  213. package/dist/utils/index.js.map +1 -0
  214. package/dist/utils/logger.d.ts +36 -0
  215. package/dist/utils/logger.d.ts.map +1 -0
  216. package/dist/utils/logger.js +101 -0
  217. package/dist/utils/logger.js.map +1 -0
  218. package/eslint.config.js +13 -0
  219. package/package.json +54 -0
  220. package/skills/feishu-bitable/SKILL.md +248 -0
  221. package/skills/feishu-bitable/references/examples.md +813 -0
  222. package/skills/feishu-bitable/references/field-properties.md +763 -0
  223. package/skills/feishu-bitable/references/record-values.md +911 -0
  224. package/skills/feishu-calendar/SKILL.md +244 -0
  225. package/skills/feishu-channel-rules/SKILL.md +18 -0
  226. package/skills/feishu-channel-rules/references/markdown-syntax.md +138 -0
  227. package/skills/feishu-create-doc/SKILL.md +719 -0
  228. package/skills/feishu-fetch-doc/SKILL.md +93 -0
  229. package/skills/feishu-im-read/SKILL.md +163 -0
  230. package/skills/feishu-task/SKILL.md +293 -0
  231. package/skills/feishu-troubleshoot/SKILL.md +70 -0
  232. package/skills/feishu-update-doc/SKILL.md +285 -0
  233. package/src/core/api-error.ts +342 -0
  234. package/src/core/auth-errors.ts +27 -0
  235. package/src/core/config.ts +134 -0
  236. package/src/core/device-flow.ts +314 -0
  237. package/src/core/index.ts +16 -0
  238. package/src/core/lark-client.ts +391 -0
  239. package/src/core/token-store.ts +249 -0
  240. package/src/core/types.ts +302 -0
  241. package/src/core/uat-client.ts +298 -0
  242. package/src/core/version.ts +53 -0
  243. package/src/index.ts +138 -0
  244. package/src/tools/bitable/app.ts +390 -0
  245. package/src/tools/bitable/field.ts +406 -0
  246. package/src/tools/bitable/index.ts +43 -0
  247. package/src/tools/bitable/record.ts +559 -0
  248. package/src/tools/bitable/table.ts +472 -0
  249. package/src/tools/calendar/calendar.ts +254 -0
  250. package/src/tools/calendar/event.ts +606 -0
  251. package/src/tools/calendar/index.ts +41 -0
  252. package/src/tools/chat/chat.ts +127 -0
  253. package/src/tools/chat/index.ts +24 -0
  254. package/src/tools/chat/members.ts +93 -0
  255. package/src/tools/common/get-user.ts +127 -0
  256. package/src/tools/common/index.ts +24 -0
  257. package/src/tools/common/search-user.ts +99 -0
  258. package/src/tools/doc/create.ts +184 -0
  259. package/src/tools/doc/fetch.ts +149 -0
  260. package/src/tools/doc/index.ts +38 -0
  261. package/src/tools/doc/shared.ts +228 -0
  262. package/src/tools/doc/update.ts +240 -0
  263. package/src/tools/drive/file.ts +265 -0
  264. package/src/tools/drive/index.ts +29 -0
  265. package/src/tools/im/format-messages.ts +391 -0
  266. package/src/tools/im/helpers.ts +109 -0
  267. package/src/tools/im/index.ts +49 -0
  268. package/src/tools/im/message-read.ts +676 -0
  269. package/src/tools/im/message.ts +303 -0
  270. package/src/tools/im/resource.ts +225 -0
  271. package/src/tools/im/time-utils.ts +347 -0
  272. package/src/tools/index.ts +205 -0
  273. package/src/tools/oauth.ts +460 -0
  274. package/src/tools/search/doc-search.ts +250 -0
  275. package/src/tools/search/index.ts +22 -0
  276. package/src/tools/sheets/index.ts +22 -0
  277. package/src/tools/sheets/sheet.ts +382 -0
  278. package/src/tools/task/index.ts +34 -0
  279. package/src/tools/task/task.ts +265 -0
  280. package/src/tools/task/tasklist.ts +262 -0
  281. package/src/tools/wiki/index.ts +24 -0
  282. package/src/tools/wiki/node.ts +131 -0
  283. package/src/tools/wiki/space.ts +152 -0
  284. package/src/utils/index.ts +8 -0
  285. package/src/utils/logger.ts +132 -0
  286. package/tests/core/config.test.ts +238 -0
  287. package/tests/core/device-flow.test.ts +490 -0
  288. package/tests/core/lark-client.test.ts +378 -0
  289. package/tests/core/token-store.test.ts +438 -0
  290. package/tests/index.test.ts +360 -0
  291. package/tests/tools/doc/create.test.ts +224 -0
  292. package/tests/tools/doc/fetch.test.ts +182 -0
  293. package/tests/tools/doc/shared.test.ts +183 -0
  294. package/tests/tools/doc/update.test.ts +330 -0
  295. package/tests/tools/im/format-messages.test.ts +184 -0
  296. package/tests/tools/im/time-utils.test.ts +178 -0
  297. package/tests/utils/logger.test.ts +140 -0
  298. 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';