@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/README.md +290 -0
- package/index.ts +18 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +42 -0
- package/src/api.ts +1160 -0
- package/src/auth.ts +133 -0
- package/src/channel.ts +883 -0
- package/src/context.ts +292 -0
- package/src/dedupe.ts +85 -0
- package/src/dispatch.ts +185 -0
- package/src/history.ts +130 -0
- package/src/inbound.ts +83 -0
- package/src/message.ts +386 -0
- package/src/runtime.ts +14 -0
- package/src/types.ts +330 -0
- package/src/webhook.ts +549 -0
- package/src/websocket.ts +372 -0
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
|
+
}
|