@yuuko1410/feishu-bitable 0.0.4 → 0.0.5
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 +100 -0
- package/lib/auth.d.ts +20 -0
- package/lib/index.cjs +178 -0
- package/lib/index.d.ts +3 -1
- package/lib/index.js +181 -3
- package/lib/types.d.ts +41 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- 支持小文件直接上传
|
|
18
18
|
- 支持大文件自动切换分片上传
|
|
19
19
|
- 支持文件下载
|
|
20
|
+
- 支持 H5 OAuth 登录(生成登录链接、code 换 token、获取用户信息)
|
|
20
21
|
- 支持字段值归一化
|
|
21
22
|
- 支持批量请求自动分片
|
|
22
23
|
- 支持并发控制
|
|
@@ -33,11 +34,13 @@ packages/feishu-bitable/
|
|
|
33
34
|
├── tsconfig.build.json
|
|
34
35
|
├── src/
|
|
35
36
|
│ ├── client.ts
|
|
37
|
+
│ ├── auth.ts
|
|
36
38
|
│ ├── errors.ts
|
|
37
39
|
│ ├── index.ts
|
|
38
40
|
│ ├── types.ts
|
|
39
41
|
│ └── utils.ts
|
|
40
42
|
├── test/
|
|
43
|
+
│ ├── auth.test.ts
|
|
41
44
|
│ └── bitable.test.ts
|
|
42
45
|
└── lib/ # bun run build 后生成
|
|
43
46
|
```
|
|
@@ -81,6 +84,7 @@ npm install @yuuko1410/feishu-bitable
|
|
|
81
84
|
主导出:
|
|
82
85
|
|
|
83
86
|
- `Bitable`
|
|
87
|
+
- `FeishuOAuthClient`
|
|
84
88
|
- `FeishuBitableError`
|
|
85
89
|
- `AppType`
|
|
86
90
|
- `Domain`
|
|
@@ -89,6 +93,11 @@ npm install @yuuko1410/feishu-bitable
|
|
|
89
93
|
类型导出:
|
|
90
94
|
|
|
91
95
|
- `BitableConstructorOptions`
|
|
96
|
+
- `FeishuOAuthConstructorOptions`
|
|
97
|
+
- `BuildOAuthLoginUrlOptions`
|
|
98
|
+
- `OAuthTokenInfo`
|
|
99
|
+
- `OAuthUserInfo`
|
|
100
|
+
- `OAuthCallbackResult`
|
|
92
101
|
- `FetchAllRecordsOptions`
|
|
93
102
|
- `BatchOperationOptions`
|
|
94
103
|
- `UpdateRecordsOptions`
|
|
@@ -141,6 +150,7 @@ const bitable = Bitable.fromEnv();
|
|
|
141
150
|
- `FEISHU_APP_ID`
|
|
142
151
|
- `FEISHU_APP_SECRET`
|
|
143
152
|
- `FEISHU_APP_TOKEN`
|
|
153
|
+
- `FEISHU_OAUTH_REDIRECT_URI`(OAuth 登录回调地址)
|
|
144
154
|
|
|
145
155
|
## 构造参数说明
|
|
146
156
|
|
|
@@ -156,6 +166,15 @@ const bitable = Bitable.fromEnv();
|
|
|
156
166
|
- `defaultConcurrency`:默认并发数,默认 `1`
|
|
157
167
|
- `sdkClient`:可注入已创建的官方 SDK client,便于测试或复用
|
|
158
168
|
|
|
169
|
+
`FeishuOAuthConstructorOptions` 支持:
|
|
170
|
+
|
|
171
|
+
- `appId`:飞书应用 `app_id`
|
|
172
|
+
- `appSecret`:飞书应用 `app_secret`
|
|
173
|
+
- `redirectUri`:OAuth 回调地址
|
|
174
|
+
- `domain`:飞书 SDK `Domain`
|
|
175
|
+
- `sdkClient`:可注入官方 SDK client
|
|
176
|
+
- `logger`:可注入日志器;传 `null` 可禁用日志
|
|
177
|
+
|
|
159
178
|
## 公开 API
|
|
160
179
|
|
|
161
180
|
### 1. `fetchAllRecords`
|
|
@@ -352,6 +371,84 @@ const buffer = await bitable.downloadFile("file_token");
|
|
|
352
371
|
const buffer = await bitable.downLoadFile("file_token");
|
|
353
372
|
```
|
|
354
373
|
|
|
374
|
+
### 9. `FeishuOAuthClient.buildLoginUrl`
|
|
375
|
+
|
|
376
|
+
生成 H5 飞书登录链接。
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
import { FeishuOAuthClient } from "@yuuko1410/feishu-bitable";
|
|
380
|
+
|
|
381
|
+
const oauth = FeishuOAuthClient.fromEnv();
|
|
382
|
+
const loginUrl = oauth.buildLoginUrl({
|
|
383
|
+
state: "csrf_state_123",
|
|
384
|
+
scope: ["contact:user.base:readonly"],
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 10. `FeishuOAuthClient.exchangeCodeForUserToken`
|
|
389
|
+
|
|
390
|
+
后端使用回调 `code` 换取 `user_access_token`。
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
const token = await oauth.exchangeCodeForUserToken(code);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 11. `FeishuOAuthClient.getUserInfo`
|
|
397
|
+
|
|
398
|
+
使用 `user_access_token` 拉取飞书用户信息。
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
const user = await oauth.getUserInfo(token.access_token);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### 12. `FeishuOAuthClient.handleCallback`
|
|
405
|
+
|
|
406
|
+
最简回调处理:一键完成 `code -> token -> user`。
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
const result = await oauth.handleCallback(code);
|
|
410
|
+
// result.token
|
|
411
|
+
// result.user
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## H5 登录最简接入
|
|
415
|
+
|
|
416
|
+
前端按钮跳转:
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
window.location.href = "/api/auth/feishu/login";
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
后端示例(Express/SvelteKit 思路一致):
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
import { FeishuOAuthClient } from "@yuuko1410/feishu-bitable";
|
|
426
|
+
|
|
427
|
+
const oauth = FeishuOAuthClient.fromEnv();
|
|
428
|
+
|
|
429
|
+
export async function loginHandler() {
|
|
430
|
+
const state = crypto.randomUUID();
|
|
431
|
+
const loginUrl = oauth.buildLoginUrl({
|
|
432
|
+
state,
|
|
433
|
+
scope: ["contact:user.base:readonly"],
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
return Response.redirect(loginUrl, 302);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export async function callbackHandler(code: string) {
|
|
440
|
+
const { token, user } = await oauth.handleCallback(code);
|
|
441
|
+
|
|
442
|
+
// 这里创建你自己的业务登录态(session/JWT)
|
|
443
|
+
// 不建议把 user_access_token 暴露给前端
|
|
444
|
+
return {
|
|
445
|
+
uid: user.open_id ?? user.user_id,
|
|
446
|
+
name: user.name,
|
|
447
|
+
tokenType: token.token_type,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
355
452
|
## 自动行为说明
|
|
356
453
|
|
|
357
454
|
### 自动分页
|
|
@@ -449,6 +546,9 @@ await bitable.deleteList("table_id", ["recyyy"]);
|
|
|
449
546
|
- 数组式批量更新到官方 payload 的转换
|
|
450
547
|
- 官方批量更新 payload 透传
|
|
451
548
|
- 文件下载
|
|
549
|
+
- OAuth 登录链接生成
|
|
550
|
+
- OAuth code 换 token
|
|
551
|
+
- OAuth 回调获取用户信息
|
|
452
552
|
- 凭证缺失报错
|
|
453
553
|
|
|
454
554
|
## 发布
|
package/lib/auth.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import type { BitableLogger, BuildOAuthLoginUrlOptions, FeishuOAuthConstructorOptions, OAuthCallbackResult, OAuthTokenInfo, OAuthUserInfo } from "./types";
|
|
3
|
+
type OAuthSdkClient = Pick<lark.Client, "authen">;
|
|
4
|
+
export declare class FeishuOAuthClient {
|
|
5
|
+
readonly appId: string;
|
|
6
|
+
readonly appSecret: string;
|
|
7
|
+
readonly redirectUri?: string;
|
|
8
|
+
readonly domain: lark.Domain | string;
|
|
9
|
+
readonly logger: BitableLogger | null;
|
|
10
|
+
readonly client: OAuthSdkClient;
|
|
11
|
+
constructor(options?: FeishuOAuthConstructorOptions);
|
|
12
|
+
static fromEnv(env?: NodeJS.ProcessEnv): FeishuOAuthClient;
|
|
13
|
+
buildLoginUrl(options: BuildOAuthLoginUrlOptions): string;
|
|
14
|
+
exchangeCodeForUserToken(code: string): Promise<OAuthTokenInfo>;
|
|
15
|
+
refreshUserAccessToken(refreshToken: string): Promise<OAuthTokenInfo>;
|
|
16
|
+
getUserInfo(userAccessToken: string): Promise<OAuthUserInfo>;
|
|
17
|
+
handleCallback(code: string): Promise<OAuthCallbackResult>;
|
|
18
|
+
private logInfo;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
package/lib/index.cjs
CHANGED
|
@@ -65,6 +65,7 @@ var exports_src = {};
|
|
|
65
65
|
__export(exports_src, {
|
|
66
66
|
default: () => src_default,
|
|
67
67
|
LoggerLevel: () => import_node_sdk.LoggerLevel,
|
|
68
|
+
FeishuOAuthClient: () => FeishuOAuthClient,
|
|
68
69
|
FeishuBitableError: () => FeishuBitableError,
|
|
69
70
|
Domain: () => import_node_sdk.Domain,
|
|
70
71
|
Bitable: () => Bitable,
|
|
@@ -675,6 +676,183 @@ class Bitable {
|
|
|
675
676
|
}
|
|
676
677
|
}
|
|
677
678
|
|
|
679
|
+
// src/auth.ts
|
|
680
|
+
var lark2 = __toESM(require("@larksuiteoapi/node-sdk"));
|
|
681
|
+
var DEFAULT_LOGGER2 = {
|
|
682
|
+
info(message, meta) {
|
|
683
|
+
if (meta) {
|
|
684
|
+
console.info(`[feishu-bitable] ${message}`, meta);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
console.info(`[feishu-bitable] ${message}`);
|
|
688
|
+
},
|
|
689
|
+
warn(message, meta) {
|
|
690
|
+
if (meta) {
|
|
691
|
+
console.warn(`[feishu-bitable] ${message}`, meta);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
console.warn(`[feishu-bitable] ${message}`);
|
|
695
|
+
},
|
|
696
|
+
error(message, meta) {
|
|
697
|
+
if (meta) {
|
|
698
|
+
console.error(`[feishu-bitable] ${message}`, meta);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
console.error(`[feishu-bitable] ${message}`);
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
class FeishuOAuthClient {
|
|
706
|
+
appId;
|
|
707
|
+
appSecret;
|
|
708
|
+
redirectUri;
|
|
709
|
+
domain;
|
|
710
|
+
logger;
|
|
711
|
+
client;
|
|
712
|
+
constructor(options = {}) {
|
|
713
|
+
const appId = options.appId ?? process.env.FEISHU_APP_ID;
|
|
714
|
+
const appSecret = options.appSecret ?? process.env.FEISHU_APP_SECRET;
|
|
715
|
+
if (!appId || !appSecret) {
|
|
716
|
+
throw new FeishuBitableError("appId and appSecret are required. Pass them in constructor or provide FEISHU_APP_ID and FEISHU_APP_SECRET.");
|
|
717
|
+
}
|
|
718
|
+
this.appId = appId;
|
|
719
|
+
this.appSecret = appSecret;
|
|
720
|
+
this.redirectUri = options.redirectUri;
|
|
721
|
+
this.domain = options.domain ?? lark2.Domain.Feishu;
|
|
722
|
+
this.logger = options.logger === null ? null : options.logger ?? DEFAULT_LOGGER2;
|
|
723
|
+
this.client = options.sdkClient ?? new lark2.Client({
|
|
724
|
+
appId: this.appId,
|
|
725
|
+
appSecret: this.appSecret,
|
|
726
|
+
appType: lark2.AppType.SelfBuild,
|
|
727
|
+
domain: this.domain
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
static fromEnv(env = process.env) {
|
|
731
|
+
return new FeishuOAuthClient({
|
|
732
|
+
appId: env.FEISHU_APP_ID,
|
|
733
|
+
appSecret: env.FEISHU_APP_SECRET,
|
|
734
|
+
redirectUri: env.FEISHU_OAUTH_REDIRECT_URI
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
buildLoginUrl(options) {
|
|
738
|
+
const redirectUri = options.redirectUri ?? this.redirectUri;
|
|
739
|
+
if (!redirectUri) {
|
|
740
|
+
throw new FeishuBitableError("redirectUri is required. Pass it into constructor or buildLoginUrl options.");
|
|
741
|
+
}
|
|
742
|
+
const scope = Array.isArray(options.scope) ? options.scope.join(" ") : options.scope;
|
|
743
|
+
const url = new URL("/open-apis/authen/v1/authorize", resolveOpenDomain(this.domain));
|
|
744
|
+
url.searchParams.set("app_id", this.appId);
|
|
745
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
746
|
+
url.searchParams.set("state", options.state);
|
|
747
|
+
if (scope?.trim()) {
|
|
748
|
+
url.searchParams.set("scope", scope.trim());
|
|
749
|
+
}
|
|
750
|
+
this.logInfo("oauth buildLoginUrl", {
|
|
751
|
+
redirectUri,
|
|
752
|
+
hasScope: Boolean(scope),
|
|
753
|
+
state: options.state
|
|
754
|
+
});
|
|
755
|
+
return url.toString();
|
|
756
|
+
}
|
|
757
|
+
async exchangeCodeForUserToken(code) {
|
|
758
|
+
if (!code.trim()) {
|
|
759
|
+
throw new FeishuBitableError("code is required to exchange user token.");
|
|
760
|
+
}
|
|
761
|
+
this.logInfo("oauth exchangeCodeForUserToken started");
|
|
762
|
+
const response = assertFeishuResponse(await this.client.authen.v1.oidcAccessToken.create({
|
|
763
|
+
data: {
|
|
764
|
+
grant_type: "authorization_code",
|
|
765
|
+
code
|
|
766
|
+
}
|
|
767
|
+
}), "oauth exchange code");
|
|
768
|
+
const data = response.data;
|
|
769
|
+
if (!data?.access_token || !data.token_type) {
|
|
770
|
+
throw new FeishuBitableError("oauth exchange code failed: missing token fields.", {
|
|
771
|
+
details: response
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
const token = {
|
|
775
|
+
access_token: data.access_token,
|
|
776
|
+
refresh_token: data.refresh_token,
|
|
777
|
+
token_type: data.token_type,
|
|
778
|
+
expires_in: data.expires_in,
|
|
779
|
+
refresh_expires_in: data.refresh_expires_in,
|
|
780
|
+
scope: data.scope
|
|
781
|
+
};
|
|
782
|
+
this.logInfo("oauth exchangeCodeForUserToken succeeded", {
|
|
783
|
+
hasRefreshToken: Boolean(token.refresh_token),
|
|
784
|
+
expiresIn: token.expires_in
|
|
785
|
+
});
|
|
786
|
+
return token;
|
|
787
|
+
}
|
|
788
|
+
async refreshUserAccessToken(refreshToken) {
|
|
789
|
+
if (!refreshToken.trim()) {
|
|
790
|
+
throw new FeishuBitableError("refreshToken is required to refresh user token.");
|
|
791
|
+
}
|
|
792
|
+
this.logInfo("oauth refreshUserAccessToken started");
|
|
793
|
+
const response = assertFeishuResponse(await this.client.authen.v1.oidcRefreshAccessToken.create({
|
|
794
|
+
data: {
|
|
795
|
+
grant_type: "refresh_token",
|
|
796
|
+
refresh_token: refreshToken
|
|
797
|
+
}
|
|
798
|
+
}), "oauth refresh token");
|
|
799
|
+
const data = response.data;
|
|
800
|
+
if (!data?.access_token || !data.token_type) {
|
|
801
|
+
throw new FeishuBitableError("oauth refresh token failed: missing token fields.", {
|
|
802
|
+
details: response
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
const token = {
|
|
806
|
+
access_token: data.access_token,
|
|
807
|
+
refresh_token: data.refresh_token,
|
|
808
|
+
token_type: data.token_type,
|
|
809
|
+
expires_in: data.expires_in,
|
|
810
|
+
refresh_expires_in: data.refresh_expires_in,
|
|
811
|
+
scope: data.scope
|
|
812
|
+
};
|
|
813
|
+
this.logInfo("oauth refreshUserAccessToken succeeded", {
|
|
814
|
+
hasRefreshToken: Boolean(token.refresh_token),
|
|
815
|
+
expiresIn: token.expires_in
|
|
816
|
+
});
|
|
817
|
+
return token;
|
|
818
|
+
}
|
|
819
|
+
async getUserInfo(userAccessToken) {
|
|
820
|
+
if (!userAccessToken.trim()) {
|
|
821
|
+
throw new FeishuBitableError("userAccessToken is required to get user info.");
|
|
822
|
+
}
|
|
823
|
+
this.logInfo("oauth getUserInfo started");
|
|
824
|
+
const response = assertFeishuResponse(await this.client.authen.v1.userInfo.get({}, lark2.withUserAccessToken(userAccessToken)), "oauth get user info");
|
|
825
|
+
this.logInfo("oauth getUserInfo succeeded", {
|
|
826
|
+
openId: response.data?.open_id,
|
|
827
|
+
userId: response.data?.user_id
|
|
828
|
+
});
|
|
829
|
+
return response.data ?? {};
|
|
830
|
+
}
|
|
831
|
+
async handleCallback(code) {
|
|
832
|
+
const token = await this.exchangeCodeForUserToken(code);
|
|
833
|
+
const user = await this.getUserInfo(token.access_token);
|
|
834
|
+
return { token, user };
|
|
835
|
+
}
|
|
836
|
+
logInfo(message, meta) {
|
|
837
|
+
this.logger?.info(message, meta);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function resolveOpenDomain(domain) {
|
|
841
|
+
if (domain === lark2.Domain.Lark) {
|
|
842
|
+
return "https://open.larksuite.com";
|
|
843
|
+
}
|
|
844
|
+
if (domain === lark2.Domain.Feishu) {
|
|
845
|
+
return "https://open.feishu.cn";
|
|
846
|
+
}
|
|
847
|
+
if (typeof domain === "string") {
|
|
848
|
+
if (domain.includes("larksuite")) {
|
|
849
|
+
return "https://open.larksuite.com";
|
|
850
|
+
}
|
|
851
|
+
return "https://open.feishu.cn";
|
|
852
|
+
}
|
|
853
|
+
return "https://open.feishu.cn";
|
|
854
|
+
}
|
|
855
|
+
|
|
678
856
|
// src/index.ts
|
|
679
857
|
var import_node_sdk = require("@larksuiteoapi/node-sdk");
|
|
680
858
|
var src_default = Bitable;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Bitable } from "./client";
|
|
2
|
+
import { FeishuOAuthClient } from "./auth";
|
|
2
3
|
export { Bitable };
|
|
4
|
+
export { FeishuOAuthClient };
|
|
3
5
|
export { FeishuBitableError } from "./errors";
|
|
4
6
|
export { AppType, Domain, LoggerLevel } from "@larksuiteoapi/node-sdk";
|
|
5
|
-
export type { BatchOperationOptions, BitableBatchUpdatePayload, BitableBatchUpdateResponse, BitableConstructorOptions, BitableFieldValue, BitableFilterCondition, BitableFilterGroup, BitableInsertRecord, BitableLocationValue, BitableLogger, BitableMemberValue, BitableRecord, BitableRecordFields, BitableSort, BitableTextValue, BitableUpdateRecord, FetchAllRecordsOptions, MediaParentType, UpdateRecordsOptions, UploadFileOptions, UploadableFile, } from "./types";
|
|
7
|
+
export type { BatchOperationOptions, BitableBatchUpdatePayload, BitableBatchUpdateResponse, BitableConstructorOptions, BitableFieldValue, BitableFilterCondition, BitableFilterGroup, BitableInsertRecord, BitableLocationValue, BitableLogger, BitableMemberValue, BitableRecord, BitableRecordFields, BitableSort, BitableTextValue, BitableUpdateRecord, BuildOAuthLoginUrlOptions, FetchAllRecordsOptions, FeishuOAuthConstructorOptions, MediaParentType, OAuthCallbackResult, OAuthTokenInfo, OAuthUserInfo, UpdateRecordsOptions, UploadFileOptions, UploadableFile, } from "./types";
|
|
6
8
|
export default Bitable;
|
package/lib/index.js
CHANGED
|
@@ -601,14 +601,192 @@ class Bitable {
|
|
|
601
601
|
}
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
+
// src/auth.ts
|
|
605
|
+
import * as lark2 from "@larksuiteoapi/node-sdk";
|
|
606
|
+
var DEFAULT_LOGGER2 = {
|
|
607
|
+
info(message, meta) {
|
|
608
|
+
if (meta) {
|
|
609
|
+
console.info(`[feishu-bitable] ${message}`, meta);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
console.info(`[feishu-bitable] ${message}`);
|
|
613
|
+
},
|
|
614
|
+
warn(message, meta) {
|
|
615
|
+
if (meta) {
|
|
616
|
+
console.warn(`[feishu-bitable] ${message}`, meta);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
console.warn(`[feishu-bitable] ${message}`);
|
|
620
|
+
},
|
|
621
|
+
error(message, meta) {
|
|
622
|
+
if (meta) {
|
|
623
|
+
console.error(`[feishu-bitable] ${message}`, meta);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
console.error(`[feishu-bitable] ${message}`);
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
class FeishuOAuthClient {
|
|
631
|
+
appId;
|
|
632
|
+
appSecret;
|
|
633
|
+
redirectUri;
|
|
634
|
+
domain;
|
|
635
|
+
logger;
|
|
636
|
+
client;
|
|
637
|
+
constructor(options = {}) {
|
|
638
|
+
const appId = options.appId ?? process.env.FEISHU_APP_ID;
|
|
639
|
+
const appSecret = options.appSecret ?? process.env.FEISHU_APP_SECRET;
|
|
640
|
+
if (!appId || !appSecret) {
|
|
641
|
+
throw new FeishuBitableError("appId and appSecret are required. Pass them in constructor or provide FEISHU_APP_ID and FEISHU_APP_SECRET.");
|
|
642
|
+
}
|
|
643
|
+
this.appId = appId;
|
|
644
|
+
this.appSecret = appSecret;
|
|
645
|
+
this.redirectUri = options.redirectUri;
|
|
646
|
+
this.domain = options.domain ?? lark2.Domain.Feishu;
|
|
647
|
+
this.logger = options.logger === null ? null : options.logger ?? DEFAULT_LOGGER2;
|
|
648
|
+
this.client = options.sdkClient ?? new lark2.Client({
|
|
649
|
+
appId: this.appId,
|
|
650
|
+
appSecret: this.appSecret,
|
|
651
|
+
appType: lark2.AppType.SelfBuild,
|
|
652
|
+
domain: this.domain
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
static fromEnv(env = process.env) {
|
|
656
|
+
return new FeishuOAuthClient({
|
|
657
|
+
appId: env.FEISHU_APP_ID,
|
|
658
|
+
appSecret: env.FEISHU_APP_SECRET,
|
|
659
|
+
redirectUri: env.FEISHU_OAUTH_REDIRECT_URI
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
buildLoginUrl(options) {
|
|
663
|
+
const redirectUri = options.redirectUri ?? this.redirectUri;
|
|
664
|
+
if (!redirectUri) {
|
|
665
|
+
throw new FeishuBitableError("redirectUri is required. Pass it into constructor or buildLoginUrl options.");
|
|
666
|
+
}
|
|
667
|
+
const scope = Array.isArray(options.scope) ? options.scope.join(" ") : options.scope;
|
|
668
|
+
const url = new URL("/open-apis/authen/v1/authorize", resolveOpenDomain(this.domain));
|
|
669
|
+
url.searchParams.set("app_id", this.appId);
|
|
670
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
671
|
+
url.searchParams.set("state", options.state);
|
|
672
|
+
if (scope?.trim()) {
|
|
673
|
+
url.searchParams.set("scope", scope.trim());
|
|
674
|
+
}
|
|
675
|
+
this.logInfo("oauth buildLoginUrl", {
|
|
676
|
+
redirectUri,
|
|
677
|
+
hasScope: Boolean(scope),
|
|
678
|
+
state: options.state
|
|
679
|
+
});
|
|
680
|
+
return url.toString();
|
|
681
|
+
}
|
|
682
|
+
async exchangeCodeForUserToken(code) {
|
|
683
|
+
if (!code.trim()) {
|
|
684
|
+
throw new FeishuBitableError("code is required to exchange user token.");
|
|
685
|
+
}
|
|
686
|
+
this.logInfo("oauth exchangeCodeForUserToken started");
|
|
687
|
+
const response = assertFeishuResponse(await this.client.authen.v1.oidcAccessToken.create({
|
|
688
|
+
data: {
|
|
689
|
+
grant_type: "authorization_code",
|
|
690
|
+
code
|
|
691
|
+
}
|
|
692
|
+
}), "oauth exchange code");
|
|
693
|
+
const data = response.data;
|
|
694
|
+
if (!data?.access_token || !data.token_type) {
|
|
695
|
+
throw new FeishuBitableError("oauth exchange code failed: missing token fields.", {
|
|
696
|
+
details: response
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
const token = {
|
|
700
|
+
access_token: data.access_token,
|
|
701
|
+
refresh_token: data.refresh_token,
|
|
702
|
+
token_type: data.token_type,
|
|
703
|
+
expires_in: data.expires_in,
|
|
704
|
+
refresh_expires_in: data.refresh_expires_in,
|
|
705
|
+
scope: data.scope
|
|
706
|
+
};
|
|
707
|
+
this.logInfo("oauth exchangeCodeForUserToken succeeded", {
|
|
708
|
+
hasRefreshToken: Boolean(token.refresh_token),
|
|
709
|
+
expiresIn: token.expires_in
|
|
710
|
+
});
|
|
711
|
+
return token;
|
|
712
|
+
}
|
|
713
|
+
async refreshUserAccessToken(refreshToken) {
|
|
714
|
+
if (!refreshToken.trim()) {
|
|
715
|
+
throw new FeishuBitableError("refreshToken is required to refresh user token.");
|
|
716
|
+
}
|
|
717
|
+
this.logInfo("oauth refreshUserAccessToken started");
|
|
718
|
+
const response = assertFeishuResponse(await this.client.authen.v1.oidcRefreshAccessToken.create({
|
|
719
|
+
data: {
|
|
720
|
+
grant_type: "refresh_token",
|
|
721
|
+
refresh_token: refreshToken
|
|
722
|
+
}
|
|
723
|
+
}), "oauth refresh token");
|
|
724
|
+
const data = response.data;
|
|
725
|
+
if (!data?.access_token || !data.token_type) {
|
|
726
|
+
throw new FeishuBitableError("oauth refresh token failed: missing token fields.", {
|
|
727
|
+
details: response
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
const token = {
|
|
731
|
+
access_token: data.access_token,
|
|
732
|
+
refresh_token: data.refresh_token,
|
|
733
|
+
token_type: data.token_type,
|
|
734
|
+
expires_in: data.expires_in,
|
|
735
|
+
refresh_expires_in: data.refresh_expires_in,
|
|
736
|
+
scope: data.scope
|
|
737
|
+
};
|
|
738
|
+
this.logInfo("oauth refreshUserAccessToken succeeded", {
|
|
739
|
+
hasRefreshToken: Boolean(token.refresh_token),
|
|
740
|
+
expiresIn: token.expires_in
|
|
741
|
+
});
|
|
742
|
+
return token;
|
|
743
|
+
}
|
|
744
|
+
async getUserInfo(userAccessToken) {
|
|
745
|
+
if (!userAccessToken.trim()) {
|
|
746
|
+
throw new FeishuBitableError("userAccessToken is required to get user info.");
|
|
747
|
+
}
|
|
748
|
+
this.logInfo("oauth getUserInfo started");
|
|
749
|
+
const response = assertFeishuResponse(await this.client.authen.v1.userInfo.get({}, lark2.withUserAccessToken(userAccessToken)), "oauth get user info");
|
|
750
|
+
this.logInfo("oauth getUserInfo succeeded", {
|
|
751
|
+
openId: response.data?.open_id,
|
|
752
|
+
userId: response.data?.user_id
|
|
753
|
+
});
|
|
754
|
+
return response.data ?? {};
|
|
755
|
+
}
|
|
756
|
+
async handleCallback(code) {
|
|
757
|
+
const token = await this.exchangeCodeForUserToken(code);
|
|
758
|
+
const user = await this.getUserInfo(token.access_token);
|
|
759
|
+
return { token, user };
|
|
760
|
+
}
|
|
761
|
+
logInfo(message, meta) {
|
|
762
|
+
this.logger?.info(message, meta);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function resolveOpenDomain(domain) {
|
|
766
|
+
if (domain === lark2.Domain.Lark) {
|
|
767
|
+
return "https://open.larksuite.com";
|
|
768
|
+
}
|
|
769
|
+
if (domain === lark2.Domain.Feishu) {
|
|
770
|
+
return "https://open.feishu.cn";
|
|
771
|
+
}
|
|
772
|
+
if (typeof domain === "string") {
|
|
773
|
+
if (domain.includes("larksuite")) {
|
|
774
|
+
return "https://open.larksuite.com";
|
|
775
|
+
}
|
|
776
|
+
return "https://open.feishu.cn";
|
|
777
|
+
}
|
|
778
|
+
return "https://open.feishu.cn";
|
|
779
|
+
}
|
|
780
|
+
|
|
604
781
|
// src/index.ts
|
|
605
|
-
import { AppType as
|
|
782
|
+
import { AppType as AppType3, Domain as Domain3, LoggerLevel } from "@larksuiteoapi/node-sdk";
|
|
606
783
|
var src_default = Bitable;
|
|
607
784
|
export {
|
|
608
785
|
src_default as default,
|
|
609
786
|
LoggerLevel,
|
|
787
|
+
FeishuOAuthClient,
|
|
610
788
|
FeishuBitableError,
|
|
611
|
-
|
|
789
|
+
Domain3 as Domain,
|
|
612
790
|
Bitable,
|
|
613
|
-
|
|
791
|
+
AppType3 as AppType
|
|
614
792
|
};
|
package/lib/types.d.ts
CHANGED
|
@@ -83,6 +83,47 @@ export interface BitableConstructorOptions {
|
|
|
83
83
|
sdkClient?: lark.Client;
|
|
84
84
|
logger?: BitableLogger | null;
|
|
85
85
|
}
|
|
86
|
+
export interface FeishuOAuthConstructorOptions {
|
|
87
|
+
appId?: string;
|
|
88
|
+
appSecret?: string;
|
|
89
|
+
redirectUri?: string;
|
|
90
|
+
domain?: lark.Domain | string;
|
|
91
|
+
sdkClient?: lark.Client;
|
|
92
|
+
logger?: BitableLogger | null;
|
|
93
|
+
}
|
|
94
|
+
export interface BuildOAuthLoginUrlOptions {
|
|
95
|
+
state: string;
|
|
96
|
+
redirectUri?: string;
|
|
97
|
+
scope?: string | string[];
|
|
98
|
+
}
|
|
99
|
+
export interface OAuthTokenInfo {
|
|
100
|
+
access_token: string;
|
|
101
|
+
refresh_token?: string;
|
|
102
|
+
token_type: string;
|
|
103
|
+
expires_in?: number;
|
|
104
|
+
refresh_expires_in?: number;
|
|
105
|
+
scope?: string;
|
|
106
|
+
}
|
|
107
|
+
export interface OAuthUserInfo {
|
|
108
|
+
name?: string;
|
|
109
|
+
en_name?: string;
|
|
110
|
+
avatar_url?: string;
|
|
111
|
+
avatar_thumb?: string;
|
|
112
|
+
avatar_middle?: string;
|
|
113
|
+
avatar_big?: string;
|
|
114
|
+
open_id?: string;
|
|
115
|
+
union_id?: string;
|
|
116
|
+
email?: string;
|
|
117
|
+
enterprise_email?: string;
|
|
118
|
+
user_id?: string;
|
|
119
|
+
mobile?: string;
|
|
120
|
+
tenant_key?: string;
|
|
121
|
+
employee_no?: string;
|
|
122
|
+
}
|
|
123
|
+
export interface OAuthCallbackResult {
|
|
124
|
+
token: OAuthTokenInfo;
|
|
125
|
+
user: OAuthUserInfo;
|
|
126
|
+
}
|
|
86
127
|
export interface FetchAllRecordsOptions {
|
|
87
128
|
viewId?: string;
|
|
88
129
|
fieldNames?: string[];
|