@tobeyoureyes/feishu 1.0.0

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/src/auth.ts ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Feishu authentication and token management
3
+ */
4
+
5
+ import type { FeishuTokenResponse, FeishuTokenCache, ResolvedFeishuAccount } from "./types.js";
6
+
7
+ const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000; // Refresh 5 minutes before expiry
8
+
9
+ /** Get API base URL for an account */
10
+ function getApiBase(account: ResolvedFeishuAccount): string {
11
+ return account.apiBase || "https://open.feishu.cn/open-apis";
12
+ }
13
+
14
+ // Token cache per account
15
+ const tokenCache = new Map<string, FeishuTokenCache>();
16
+
17
+ /**
18
+ * Get tenant_access_token for a Feishu account
19
+ * Automatically handles caching and refresh
20
+ */
21
+ export async function getTenantAccessToken(account: ResolvedFeishuAccount): Promise<string> {
22
+ const cacheKey = account.accountId;
23
+ const cached = tokenCache.get(cacheKey);
24
+
25
+ // Return cached token if still valid
26
+ if (cached && Date.now() < cached.expiresAt - TOKEN_REFRESH_BUFFER_MS) {
27
+ return cached.token;
28
+ }
29
+
30
+ // Fetch new token
31
+ const token = await fetchTenantAccessToken(account);
32
+ return token;
33
+ }
34
+
35
+ /**
36
+ * Fetch a new tenant_access_token from Feishu API
37
+ */
38
+ async function fetchTenantAccessToken(account: ResolvedFeishuAccount): Promise<string> {
39
+ const { appId, appSecret } = account;
40
+
41
+ if (!appId || !appSecret) {
42
+ throw new Error("Feishu appId and appSecret are required");
43
+ }
44
+
45
+ const apiBase = getApiBase(account);
46
+ const url = `${apiBase}/auth/v3/tenant_access_token/internal`;
47
+
48
+ const response = await fetch(url, {
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": "application/json; charset=utf-8",
52
+ },
53
+ body: JSON.stringify({
54
+ app_id: appId,
55
+ app_secret: appSecret,
56
+ }),
57
+ });
58
+
59
+ if (!response.ok) {
60
+ throw new Error(`Failed to get Feishu access token: ${response.status} ${response.statusText}`);
61
+ }
62
+
63
+ const data = (await response.json()) as FeishuTokenResponse;
64
+
65
+ if (data.code !== 0) {
66
+ throw new Error(`Feishu API error: ${data.code} - ${data.msg}`);
67
+ }
68
+
69
+ if (!data.tenant_access_token || !data.expire) {
70
+ throw new Error("Invalid token response from Feishu API");
71
+ }
72
+
73
+ // Cache the token
74
+ const expiresAt = Date.now() + data.expire * 1000;
75
+ tokenCache.set(account.accountId, {
76
+ token: data.tenant_access_token,
77
+ expiresAt,
78
+ });
79
+
80
+ return data.tenant_access_token;
81
+ }
82
+
83
+ /**
84
+ * Invalidate cached token for an account
85
+ */
86
+ export function invalidateToken(accountId: string): void {
87
+ tokenCache.delete(accountId);
88
+ }
89
+
90
+ /**
91
+ * Clear all cached tokens
92
+ */
93
+ export function clearAllTokens(): void {
94
+ tokenCache.clear();
95
+ }
96
+
97
+ /**
98
+ * Check if an account has valid credentials configured
99
+ */
100
+ export function hasValidCredentials(account: ResolvedFeishuAccount): boolean {
101
+ return Boolean(account.appId && account.appSecret);
102
+ }
103
+
104
+ /**
105
+ * Verify the signature for webhook events (if encryptKey is configured)
106
+ */
107
+ export function verifyWebhookSignature(
108
+ _timestamp: string,
109
+ _nonce: string,
110
+ _encryptKey: string,
111
+ _body: string,
112
+ _signature: string,
113
+ ): boolean {
114
+ // Feishu uses SHA256 for signature verification
115
+ // signature = sha256(timestamp + nonce + encryptKey + body)
116
+ // TODO: Implement proper HMAC-SHA256 verification
117
+ // For now, return true to allow development without encryption
118
+ return true;
119
+ }
120
+
121
+ /**
122
+ * Decrypt encrypted event body (if encryptKey is configured)
123
+ */
124
+ export async function decryptEventBody(
125
+ encryptedBody: string,
126
+ _encryptKey: string,
127
+ ): Promise<string> {
128
+ // Feishu uses AES-256-CBC encryption
129
+ // The encrypted body is base64 encoded
130
+ // TODO: Implement proper AES decryption
131
+ // For now, return the body as-is
132
+ return encryptedBody;
133
+ }