@larksuiteoapi/node-sdk 1.59.0 → 1.61.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 +62 -0
- package/README.zh.md +62 -0
- package/es/index.js +166 -5
- package/lib/index.js +166 -4
- package/package.json +1 -1
- package/types/index.d.ts +30 -1
package/README.md
CHANGED
|
@@ -627,6 +627,68 @@ server.listen(3000);
|
|
|
627
627
|
| logger | - | Logger | No | - |
|
|
628
628
|
| cache | Cache | Cache | No | - |
|
|
629
629
|
|
|
630
|
+
### App Registration
|
|
631
|
+
The SDK provides a `registerApp` method for one-click app creation based on the OAuth 2.0 Device Authorization Grant (RFC 8628) protocol. It returns a verification URL that users can open in Feishu/Lark to authorize and automatically register an app, obtaining credentials without manually creating one on the developer console.
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
import * as lark from '@larksuiteoapi/node-sdk';
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const result = await lark.registerApp({
|
|
638
|
+
onQRCodeReady(info) {
|
|
639
|
+
console.log(`Scan the QR code: ${info.url}`);
|
|
640
|
+
console.log(`Link expires in ${info.expireIn} seconds`);
|
|
641
|
+
},
|
|
642
|
+
onStatusChange(info) {
|
|
643
|
+
// Handle status changes: 'polling' | 'slow_down' | 'domain_switched'
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
console.log('App ID:', result.client_id);
|
|
648
|
+
console.log('App Secret:', result.client_secret);
|
|
649
|
+
|
|
650
|
+
// Use the credentials to initialize a Client
|
|
651
|
+
const client = new lark.Client({
|
|
652
|
+
appId: result.client_id,
|
|
653
|
+
appSecret: result.client_secret,
|
|
654
|
+
});
|
|
655
|
+
} catch (e) {
|
|
656
|
+
// e.code: 'access_denied' | 'expired_token' | 'abort' | ...
|
|
657
|
+
// e.description: error description
|
|
658
|
+
console.error(e.code, e.description);
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
#### `registerApp` parameters
|
|
663
|
+
|
|
664
|
+
| Parameter | Description | Type | Required | Default |
|
|
665
|
+
| ---- | ---- | ---- | ---- | ---- |
|
|
666
|
+
| domain | Custom accounts domain (host only) | string | No | `accounts.feishu.cn` |
|
|
667
|
+
| larkDomain | Custom Lark accounts domain (host only), used when tenant brand is detected as Lark | string | No | `accounts.larksuite.com` |
|
|
668
|
+
| source | Source identifier, appended to the QR code URL `from` parameter as `node-sdk/{source}` | string | No | - |
|
|
669
|
+
| signal | `AbortSignal` to cancel the polling | AbortSignal | No | - |
|
|
670
|
+
| onQRCodeReady | Callback when the verification URL is ready. Receives `{ url, expireIn }`. You can render the URL as a QR code for users to scan, or display it as a link | function | Yes | - |
|
|
671
|
+
| onStatusChange | Callback on polling status changes. Receives `{ status, interval? }`. Status values: `polling`, `slow_down`, `domain_switched` | function | No | - |
|
|
672
|
+
|
|
673
|
+
#### Return value
|
|
674
|
+
|
|
675
|
+
| Field | Type | Description |
|
|
676
|
+
| ---- | ---- | ---- |
|
|
677
|
+
| client_id | string | App ID |
|
|
678
|
+
| client_secret | string | App Secret |
|
|
679
|
+
| user_info | object (optional) | Scanning user info |
|
|
680
|
+
| user_info.open_id | string (optional) | User's open_id |
|
|
681
|
+
| user_info.tenant_brand | string (optional) | `"feishu"` or `"lark"` |
|
|
682
|
+
|
|
683
|
+
#### Error handling
|
|
684
|
+
The thrown error object contains `code` and `description` fields:
|
|
685
|
+
|
|
686
|
+
| code | Description |
|
|
687
|
+
| ---- | ---- |
|
|
688
|
+
| `access_denied` | User denied the authorization |
|
|
689
|
+
| `expired_token` | QR code expired or polling timed out |
|
|
690
|
+
| `abort` | Cancelled via AbortSignal |
|
|
691
|
+
|
|
630
692
|
### Tool method
|
|
631
693
|
#### AESCipher
|
|
632
694
|
Decrypt. If [Encrypted Push](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/encrypt-key-encryption-configuration-case) is configured, the open platform will push encrypted data, At this time, the data needs to be decrypted, and this method can be called for convenient decryption. (In general, the decryption logic is built into the SDK, and no manual processing is required).
|
package/README.zh.md
CHANGED
|
@@ -627,6 +627,68 @@ server.listen(3000);
|
|
|
627
627
|
| logger | - | Logger | 否 | - |
|
|
628
628
|
| cache | 缓存器 | Cache | 否 | - |
|
|
629
629
|
|
|
630
|
+
### 一键创建应用
|
|
631
|
+
SDK提供了`registerApp`方法,基于 OAuth 2.0 Device Authorization Grant(RFC 8628)协议实现一键创建应用。该方法会返回一个验证链接,用户在飞书/Lark中打开该链接完成授权后,即可自动注册应用并获取凭据,无需手动到开发者后台创建。
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
import * as lark from '@larksuiteoapi/node-sdk';
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const result = await lark.registerApp({
|
|
638
|
+
onQRCodeReady(info) {
|
|
639
|
+
console.log(`请扫码: ${info.url}`);
|
|
640
|
+
console.log(`链接将在 ${info.expireIn} 秒后过期`);
|
|
641
|
+
},
|
|
642
|
+
onStatusChange(info) {
|
|
643
|
+
// 处理状态变化:'polling' | 'slow_down' | 'domain_switched'
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
console.log('App ID:', result.client_id);
|
|
648
|
+
console.log('App Secret:', result.client_secret);
|
|
649
|
+
|
|
650
|
+
// 用获取到的凭据初始化 Client
|
|
651
|
+
const client = new lark.Client({
|
|
652
|
+
appId: result.client_id,
|
|
653
|
+
appSecret: result.client_secret,
|
|
654
|
+
});
|
|
655
|
+
} catch (e) {
|
|
656
|
+
// e.code: 'access_denied' | 'expired_token' | 'abort' | ...
|
|
657
|
+
// e.description: 错误描述
|
|
658
|
+
console.error(e.code, e.description);
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
#### `registerApp`参数
|
|
663
|
+
|
|
664
|
+
| 参数 | 描述 | 类型 | 必须 | 默认 |
|
|
665
|
+
| ---- | ---- | ---- | ---- | ---- |
|
|
666
|
+
| domain | 自定义认证域名(仅 host 部分) | string | 否 | `accounts.feishu.cn` |
|
|
667
|
+
| larkDomain | 自定义 Lark 认证域名(仅 host 部分),检测到 Lark 租户时自动切换 | string | 否 | `accounts.larksuite.com` |
|
|
668
|
+
| source | 来源标识,拼入二维码 URL 的 `from` 参数,格式为 `node-sdk/{source}` | string | 否 | - |
|
|
669
|
+
| signal | 用于取消轮询的 `AbortSignal` | AbortSignal | 否 | - |
|
|
670
|
+
| onQRCodeReady | 验证链接就绪时的回调,参数为 `{ url, expireIn }`。可将 URL 渲染为二维码供用户扫码,或直接作为链接展示 | function | 是 | - |
|
|
671
|
+
| onStatusChange | 轮询状态变化时的回调,参数为 `{ status, interval? }`。status 取值:`polling`、`slow_down`、`domain_switched` | function | 否 | - |
|
|
672
|
+
|
|
673
|
+
#### 返回值
|
|
674
|
+
|
|
675
|
+
| 字段 | 类型 | 描述 |
|
|
676
|
+
| ---- | ---- | ---- |
|
|
677
|
+
| client_id | string | App ID |
|
|
678
|
+
| client_secret | string | App Secret |
|
|
679
|
+
| user_info | object(可选) | 扫码用户信息 |
|
|
680
|
+
| user_info.open_id | string(可选) | 扫码用户的 open_id |
|
|
681
|
+
| user_info.tenant_brand | string(可选) | `"feishu"` 或 `"lark"` |
|
|
682
|
+
|
|
683
|
+
#### 错误处理
|
|
684
|
+
抛出的错误对象包含 `code` 和 `description` 字段:
|
|
685
|
+
|
|
686
|
+
| code | 描述 |
|
|
687
|
+
| ---- | ---- |
|
|
688
|
+
| `access_denied` | 用户拒绝了授权 |
|
|
689
|
+
| `expired_token` | 二维码过期或轮询超时 |
|
|
690
|
+
| `abort` | 通过 AbortSignal 取消 |
|
|
691
|
+
|
|
630
692
|
### 工具方法
|
|
631
693
|
#### AESCipher
|
|
632
694
|
解密。如果配置了[加密推送](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/encrypt-key-encryption-configuration-case),开放平台会推送加密的数据,这时候需要对数据进行解密处理,调用此方法可以便捷的进行解密。(一般情况下,SDK中内置了解密逻辑,不需要手动进行处理)。
|
package/es/index.js
CHANGED
|
@@ -85285,6 +85285,7 @@ var HttpStatusCode;
|
|
|
85285
85285
|
class WSClient {
|
|
85286
85286
|
constructor(params) {
|
|
85287
85287
|
this.wsConfig = new WSConfig();
|
|
85288
|
+
this.reconnectGeneration = 0;
|
|
85288
85289
|
this.isConnecting = false;
|
|
85289
85290
|
this.reconnectInfo = {
|
|
85290
85291
|
lastConnectTime: 0,
|
|
@@ -85376,11 +85377,13 @@ class WSClient {
|
|
|
85376
85377
|
}
|
|
85377
85378
|
reConnect(isStart = false) {
|
|
85378
85379
|
return __awaiter(this, void 0, void 0, function* () {
|
|
85379
|
-
if (this.isConnecting) {
|
|
85380
|
+
if (this.isConnecting && !isStart) {
|
|
85380
85381
|
this.logger.debug('[ws]', 'repeat connection');
|
|
85381
85382
|
return;
|
|
85382
85383
|
}
|
|
85383
85384
|
this.isConnecting = true;
|
|
85385
|
+
// Invalidate any in-flight reconnect loops from previous sessions
|
|
85386
|
+
const currentGeneration = ++this.reconnectGeneration;
|
|
85384
85387
|
const tryConnect = () => {
|
|
85385
85388
|
this.reconnectInfo.lastConnectTime = Date.now();
|
|
85386
85389
|
return this.pullConnectConfig()
|
|
@@ -85396,14 +85399,14 @@ class WSClient {
|
|
|
85396
85399
|
if (this.pingInterval) {
|
|
85397
85400
|
clearTimeout(this.pingInterval);
|
|
85398
85401
|
}
|
|
85402
|
+
if (this.reconnectInterval) {
|
|
85403
|
+
clearTimeout(this.reconnectInterval);
|
|
85404
|
+
}
|
|
85399
85405
|
const wsInstance = this.wsConfig.getWSInstance();
|
|
85400
85406
|
if (isStart) {
|
|
85401
85407
|
if (wsInstance) {
|
|
85402
85408
|
wsInstance === null || wsInstance === void 0 ? void 0 : wsInstance.terminate();
|
|
85403
85409
|
}
|
|
85404
|
-
if (this.reconnectInterval) {
|
|
85405
|
-
clearTimeout(this.reconnectInterval);
|
|
85406
|
-
}
|
|
85407
85410
|
let isSuccess = false;
|
|
85408
85411
|
try {
|
|
85409
85412
|
isSuccess = yield tryConnect();
|
|
@@ -85431,8 +85434,16 @@ class WSClient {
|
|
|
85431
85434
|
this.reconnectInterval = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
|
|
85432
85435
|
(function loopReConnect(count) {
|
|
85433
85436
|
return __awaiter(this, void 0, void 0, function* () {
|
|
85437
|
+
// Stale loop — a newer reConnect session has started
|
|
85438
|
+
if (currentGeneration !== this.reconnectGeneration) {
|
|
85439
|
+
return;
|
|
85440
|
+
}
|
|
85434
85441
|
count++;
|
|
85435
85442
|
const isSuccess = yield tryConnect();
|
|
85443
|
+
// Re-check after async operation in case a new session started
|
|
85444
|
+
if (currentGeneration !== this.reconnectGeneration) {
|
|
85445
|
+
return;
|
|
85446
|
+
}
|
|
85436
85447
|
// if reconnectCount < 0, the reconnect time is infinite
|
|
85437
85448
|
if (isSuccess) {
|
|
85438
85449
|
this.logger.debug('[ws]', 'reconnect success');
|
|
@@ -85578,6 +85589,8 @@ class WSClient {
|
|
|
85578
85589
|
*/
|
|
85579
85590
|
close(params = {}) {
|
|
85580
85591
|
const { force = false } = params;
|
|
85592
|
+
// Invalidate any in-flight reconnect loops
|
|
85593
|
+
this.reconnectGeneration++;
|
|
85581
85594
|
if (this.pingInterval) {
|
|
85582
85595
|
clearTimeout(this.pingInterval);
|
|
85583
85596
|
this.pingInterval = undefined;
|
|
@@ -85882,4 +85895,152 @@ class Aily {
|
|
|
85882
85895
|
}
|
|
85883
85896
|
}
|
|
85884
85897
|
|
|
85885
|
-
|
|
85898
|
+
function createError(code, description) {
|
|
85899
|
+
return { code, description };
|
|
85900
|
+
}
|
|
85901
|
+
|
|
85902
|
+
const SDK_NAME = 'node-sdk';
|
|
85903
|
+
const DEFAULT_FEISHU_DOMAIN = 'accounts.feishu.cn';
|
|
85904
|
+
const DEFAULT_LARK_DOMAIN = 'accounts.larksuite.com';
|
|
85905
|
+
const ENDPOINT = '/oauth/v1/app/registration';
|
|
85906
|
+
function requestRegistration(baseUrl, params) {
|
|
85907
|
+
var _a;
|
|
85908
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85909
|
+
try {
|
|
85910
|
+
const resp = yield defaultHttpInstance.post(`${baseUrl}${ENDPOINT}`, new URLSearchParams(params).toString(), {
|
|
85911
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
85912
|
+
});
|
|
85913
|
+
return resp;
|
|
85914
|
+
}
|
|
85915
|
+
catch (e) {
|
|
85916
|
+
// RFC 8628: authorization_pending, slow_down etc. are returned with HTTP 400
|
|
85917
|
+
if (e instanceof AxiosError && ((_a = e.response) === null || _a === void 0 ? void 0 : _a.data)) {
|
|
85918
|
+
return e.response.data;
|
|
85919
|
+
}
|
|
85920
|
+
throw e;
|
|
85921
|
+
}
|
|
85922
|
+
});
|
|
85923
|
+
}
|
|
85924
|
+
function begin(baseUrl) {
|
|
85925
|
+
return requestRegistration(baseUrl, {
|
|
85926
|
+
action: 'begin',
|
|
85927
|
+
archetype: 'PersonalAgent',
|
|
85928
|
+
auth_method: 'client_secret',
|
|
85929
|
+
request_user_info: 'open_id',
|
|
85930
|
+
});
|
|
85931
|
+
}
|
|
85932
|
+
function startPolling(ctx) {
|
|
85933
|
+
return new Promise((resolve, reject) => {
|
|
85934
|
+
var _a, _b;
|
|
85935
|
+
let { baseUrl, interval } = ctx;
|
|
85936
|
+
let domainSwitched = false;
|
|
85937
|
+
let pollTimer = null;
|
|
85938
|
+
let expireTimer = null;
|
|
85939
|
+
const cleanup = () => {
|
|
85940
|
+
var _a;
|
|
85941
|
+
if (pollTimer !== null) {
|
|
85942
|
+
clearTimeout(pollTimer);
|
|
85943
|
+
pollTimer = null;
|
|
85944
|
+
}
|
|
85945
|
+
if (expireTimer !== null) {
|
|
85946
|
+
clearTimeout(expireTimer);
|
|
85947
|
+
expireTimer = null;
|
|
85948
|
+
}
|
|
85949
|
+
(_a = ctx.signal) === null || _a === void 0 ? void 0 : _a.removeEventListener('abort', onAbort);
|
|
85950
|
+
};
|
|
85951
|
+
const onAbort = () => {
|
|
85952
|
+
cleanup();
|
|
85953
|
+
reject(createError('abort', 'Registration was aborted'));
|
|
85954
|
+
};
|
|
85955
|
+
if ((_a = ctx.signal) === null || _a === void 0 ? void 0 : _a.aborted) {
|
|
85956
|
+
return reject(createError('abort', 'Registration was aborted'));
|
|
85957
|
+
}
|
|
85958
|
+
(_b = ctx.signal) === null || _b === void 0 ? void 0 : _b.addEventListener('abort', onAbort, { once: true });
|
|
85959
|
+
expireTimer = setTimeout(() => {
|
|
85960
|
+
cleanup();
|
|
85961
|
+
reject(createError('expired_token', 'Polling timed out'));
|
|
85962
|
+
}, ctx.expireIn);
|
|
85963
|
+
const poll = () => __awaiter(this, void 0, void 0, function* () {
|
|
85964
|
+
var _c, _d, _e, _f, _g, _h;
|
|
85965
|
+
try {
|
|
85966
|
+
const pollRes = yield requestRegistration(baseUrl, {
|
|
85967
|
+
action: 'poll',
|
|
85968
|
+
device_code: ctx.deviceCode,
|
|
85969
|
+
});
|
|
85970
|
+
// Lark domain switch (once only)
|
|
85971
|
+
if (((_c = pollRes.user_info) === null || _c === void 0 ? void 0 : _c.tenant_brand) === 'lark' && !domainSwitched) {
|
|
85972
|
+
baseUrl = ctx.larkBaseUrl;
|
|
85973
|
+
domainSwitched = true;
|
|
85974
|
+
(_d = ctx.onStatusChange) === null || _d === void 0 ? void 0 : _d.call(ctx, { status: 'domain_switched' });
|
|
85975
|
+
poll();
|
|
85976
|
+
return;
|
|
85977
|
+
}
|
|
85978
|
+
// Success
|
|
85979
|
+
if (pollRes.client_id && pollRes.client_secret) {
|
|
85980
|
+
cleanup();
|
|
85981
|
+
resolve({
|
|
85982
|
+
client_id: pollRes.client_id,
|
|
85983
|
+
client_secret: pollRes.client_secret,
|
|
85984
|
+
user_info: pollRes.user_info,
|
|
85985
|
+
});
|
|
85986
|
+
return;
|
|
85987
|
+
}
|
|
85988
|
+
// Handle errors
|
|
85989
|
+
switch (pollRes.error) {
|
|
85990
|
+
case 'authorization_pending':
|
|
85991
|
+
(_e = ctx.onStatusChange) === null || _e === void 0 ? void 0 : _e.call(ctx, { status: 'polling' });
|
|
85992
|
+
break;
|
|
85993
|
+
case 'slow_down':
|
|
85994
|
+
interval += 5000;
|
|
85995
|
+
(_f = ctx.onStatusChange) === null || _f === void 0 ? void 0 : _f.call(ctx, { status: 'slow_down', interval: interval / 1000 });
|
|
85996
|
+
break;
|
|
85997
|
+
case 'access_denied':
|
|
85998
|
+
case 'expired_token':
|
|
85999
|
+
cleanup();
|
|
86000
|
+
reject(createError(pollRes.error, (_g = pollRes.error_description) !== null && _g !== void 0 ? _g : 'Unknown error'));
|
|
86001
|
+
return;
|
|
86002
|
+
default:
|
|
86003
|
+
if (pollRes.error) {
|
|
86004
|
+
cleanup();
|
|
86005
|
+
reject(createError(pollRes.error, (_h = pollRes.error_description) !== null && _h !== void 0 ? _h : 'Unknown error'));
|
|
86006
|
+
return;
|
|
86007
|
+
}
|
|
86008
|
+
break;
|
|
86009
|
+
}
|
|
86010
|
+
pollTimer = setTimeout(poll, interval);
|
|
86011
|
+
}
|
|
86012
|
+
catch (e) {
|
|
86013
|
+
cleanup();
|
|
86014
|
+
reject(e);
|
|
86015
|
+
}
|
|
86016
|
+
});
|
|
86017
|
+
poll();
|
|
86018
|
+
});
|
|
86019
|
+
}
|
|
86020
|
+
function registerApp(options) {
|
|
86021
|
+
var _a, _b, _c, _d;
|
|
86022
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
86023
|
+
const { domain, source, signal, onQRCodeReady, onStatusChange } = options;
|
|
86024
|
+
const baseUrl = `https://${domain !== null && domain !== void 0 ? domain : DEFAULT_FEISHU_DOMAIN}`;
|
|
86025
|
+
const beginRes = yield begin(baseUrl);
|
|
86026
|
+
const qrCodeUrl = new URL(beginRes.verification_uri_complete);
|
|
86027
|
+
qrCodeUrl.searchParams.set('from', 'sdk');
|
|
86028
|
+
qrCodeUrl.searchParams.set('source', source ? `${SDK_NAME}/${source}` : SDK_NAME);
|
|
86029
|
+
qrCodeUrl.searchParams.set('tp', 'sdk');
|
|
86030
|
+
onQRCodeReady({
|
|
86031
|
+
url: qrCodeUrl.toString(),
|
|
86032
|
+
expireIn: (_a = beginRes.expire_in) !== null && _a !== void 0 ? _a : 600,
|
|
86033
|
+
});
|
|
86034
|
+
return startPolling({
|
|
86035
|
+
baseUrl,
|
|
86036
|
+
deviceCode: beginRes.device_code,
|
|
86037
|
+
interval: ((_b = beginRes.interval) !== null && _b !== void 0 ? _b : 5) * 1000,
|
|
86038
|
+
expireIn: ((_c = beginRes.expire_in) !== null && _c !== void 0 ? _c : 600) * 1000,
|
|
86039
|
+
larkBaseUrl: `https://${(_d = options.larkDomain) !== null && _d !== void 0 ? _d : DEFAULT_LARK_DOMAIN}`,
|
|
86040
|
+
signal,
|
|
86041
|
+
onStatusChange,
|
|
86042
|
+
});
|
|
86043
|
+
});
|
|
86044
|
+
}
|
|
86045
|
+
|
|
86046
|
+
export { AESCipher, Aily, AppType, CAppTicket, CTenantAccessToken, CardActionHandler, Client, Domain, EventDispatcher, LoggerLevel, WSClient, adaptDefault, adaptExpress, adaptKoa, adaptKoaRouter, defaultHttpInstance, generateChallenge, messageCard, registerApp, withAll, withHelpDeskCredential, withTenantKey, withTenantToken, withUserAccessToken };
|
package/lib/index.js
CHANGED
|
@@ -85300,6 +85300,7 @@ var HttpStatusCode;
|
|
|
85300
85300
|
class WSClient {
|
|
85301
85301
|
constructor(params) {
|
|
85302
85302
|
this.wsConfig = new WSConfig();
|
|
85303
|
+
this.reconnectGeneration = 0;
|
|
85303
85304
|
this.isConnecting = false;
|
|
85304
85305
|
this.reconnectInfo = {
|
|
85305
85306
|
lastConnectTime: 0,
|
|
@@ -85391,11 +85392,13 @@ class WSClient {
|
|
|
85391
85392
|
}
|
|
85392
85393
|
reConnect(isStart = false) {
|
|
85393
85394
|
return __awaiter(this, void 0, void 0, function* () {
|
|
85394
|
-
if (this.isConnecting) {
|
|
85395
|
+
if (this.isConnecting && !isStart) {
|
|
85395
85396
|
this.logger.debug('[ws]', 'repeat connection');
|
|
85396
85397
|
return;
|
|
85397
85398
|
}
|
|
85398
85399
|
this.isConnecting = true;
|
|
85400
|
+
// Invalidate any in-flight reconnect loops from previous sessions
|
|
85401
|
+
const currentGeneration = ++this.reconnectGeneration;
|
|
85399
85402
|
const tryConnect = () => {
|
|
85400
85403
|
this.reconnectInfo.lastConnectTime = Date.now();
|
|
85401
85404
|
return this.pullConnectConfig()
|
|
@@ -85411,14 +85414,14 @@ class WSClient {
|
|
|
85411
85414
|
if (this.pingInterval) {
|
|
85412
85415
|
clearTimeout(this.pingInterval);
|
|
85413
85416
|
}
|
|
85417
|
+
if (this.reconnectInterval) {
|
|
85418
|
+
clearTimeout(this.reconnectInterval);
|
|
85419
|
+
}
|
|
85414
85420
|
const wsInstance = this.wsConfig.getWSInstance();
|
|
85415
85421
|
if (isStart) {
|
|
85416
85422
|
if (wsInstance) {
|
|
85417
85423
|
wsInstance === null || wsInstance === void 0 ? void 0 : wsInstance.terminate();
|
|
85418
85424
|
}
|
|
85419
|
-
if (this.reconnectInterval) {
|
|
85420
|
-
clearTimeout(this.reconnectInterval);
|
|
85421
|
-
}
|
|
85422
85425
|
let isSuccess = false;
|
|
85423
85426
|
try {
|
|
85424
85427
|
isSuccess = yield tryConnect();
|
|
@@ -85446,8 +85449,16 @@ class WSClient {
|
|
|
85446
85449
|
this.reconnectInterval = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
|
|
85447
85450
|
(function loopReConnect(count) {
|
|
85448
85451
|
return __awaiter(this, void 0, void 0, function* () {
|
|
85452
|
+
// Stale loop — a newer reConnect session has started
|
|
85453
|
+
if (currentGeneration !== this.reconnectGeneration) {
|
|
85454
|
+
return;
|
|
85455
|
+
}
|
|
85449
85456
|
count++;
|
|
85450
85457
|
const isSuccess = yield tryConnect();
|
|
85458
|
+
// Re-check after async operation in case a new session started
|
|
85459
|
+
if (currentGeneration !== this.reconnectGeneration) {
|
|
85460
|
+
return;
|
|
85461
|
+
}
|
|
85451
85462
|
// if reconnectCount < 0, the reconnect time is infinite
|
|
85452
85463
|
if (isSuccess) {
|
|
85453
85464
|
this.logger.debug('[ws]', 'reconnect success');
|
|
@@ -85593,6 +85604,8 @@ class WSClient {
|
|
|
85593
85604
|
*/
|
|
85594
85605
|
close(params = {}) {
|
|
85595
85606
|
const { force = false } = params;
|
|
85607
|
+
// Invalidate any in-flight reconnect loops
|
|
85608
|
+
this.reconnectGeneration++;
|
|
85596
85609
|
if (this.pingInterval) {
|
|
85597
85610
|
clearTimeout(this.pingInterval);
|
|
85598
85611
|
this.pingInterval = undefined;
|
|
@@ -85897,6 +85910,154 @@ class Aily {
|
|
|
85897
85910
|
}
|
|
85898
85911
|
}
|
|
85899
85912
|
|
|
85913
|
+
function createError(code, description) {
|
|
85914
|
+
return { code, description };
|
|
85915
|
+
}
|
|
85916
|
+
|
|
85917
|
+
const SDK_NAME = 'node-sdk';
|
|
85918
|
+
const DEFAULT_FEISHU_DOMAIN = 'accounts.feishu.cn';
|
|
85919
|
+
const DEFAULT_LARK_DOMAIN = 'accounts.larksuite.com';
|
|
85920
|
+
const ENDPOINT = '/oauth/v1/app/registration';
|
|
85921
|
+
function requestRegistration(baseUrl, params) {
|
|
85922
|
+
var _a;
|
|
85923
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85924
|
+
try {
|
|
85925
|
+
const resp = yield defaultHttpInstance.post(`${baseUrl}${ENDPOINT}`, new URLSearchParams(params).toString(), {
|
|
85926
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
85927
|
+
});
|
|
85928
|
+
return resp;
|
|
85929
|
+
}
|
|
85930
|
+
catch (e) {
|
|
85931
|
+
// RFC 8628: authorization_pending, slow_down etc. are returned with HTTP 400
|
|
85932
|
+
if (e instanceof axios.AxiosError && ((_a = e.response) === null || _a === void 0 ? void 0 : _a.data)) {
|
|
85933
|
+
return e.response.data;
|
|
85934
|
+
}
|
|
85935
|
+
throw e;
|
|
85936
|
+
}
|
|
85937
|
+
});
|
|
85938
|
+
}
|
|
85939
|
+
function begin(baseUrl) {
|
|
85940
|
+
return requestRegistration(baseUrl, {
|
|
85941
|
+
action: 'begin',
|
|
85942
|
+
archetype: 'PersonalAgent',
|
|
85943
|
+
auth_method: 'client_secret',
|
|
85944
|
+
request_user_info: 'open_id',
|
|
85945
|
+
});
|
|
85946
|
+
}
|
|
85947
|
+
function startPolling(ctx) {
|
|
85948
|
+
return new Promise((resolve, reject) => {
|
|
85949
|
+
var _a, _b;
|
|
85950
|
+
let { baseUrl, interval } = ctx;
|
|
85951
|
+
let domainSwitched = false;
|
|
85952
|
+
let pollTimer = null;
|
|
85953
|
+
let expireTimer = null;
|
|
85954
|
+
const cleanup = () => {
|
|
85955
|
+
var _a;
|
|
85956
|
+
if (pollTimer !== null) {
|
|
85957
|
+
clearTimeout(pollTimer);
|
|
85958
|
+
pollTimer = null;
|
|
85959
|
+
}
|
|
85960
|
+
if (expireTimer !== null) {
|
|
85961
|
+
clearTimeout(expireTimer);
|
|
85962
|
+
expireTimer = null;
|
|
85963
|
+
}
|
|
85964
|
+
(_a = ctx.signal) === null || _a === void 0 ? void 0 : _a.removeEventListener('abort', onAbort);
|
|
85965
|
+
};
|
|
85966
|
+
const onAbort = () => {
|
|
85967
|
+
cleanup();
|
|
85968
|
+
reject(createError('abort', 'Registration was aborted'));
|
|
85969
|
+
};
|
|
85970
|
+
if ((_a = ctx.signal) === null || _a === void 0 ? void 0 : _a.aborted) {
|
|
85971
|
+
return reject(createError('abort', 'Registration was aborted'));
|
|
85972
|
+
}
|
|
85973
|
+
(_b = ctx.signal) === null || _b === void 0 ? void 0 : _b.addEventListener('abort', onAbort, { once: true });
|
|
85974
|
+
expireTimer = setTimeout(() => {
|
|
85975
|
+
cleanup();
|
|
85976
|
+
reject(createError('expired_token', 'Polling timed out'));
|
|
85977
|
+
}, ctx.expireIn);
|
|
85978
|
+
const poll = () => __awaiter(this, void 0, void 0, function* () {
|
|
85979
|
+
var _c, _d, _e, _f, _g, _h;
|
|
85980
|
+
try {
|
|
85981
|
+
const pollRes = yield requestRegistration(baseUrl, {
|
|
85982
|
+
action: 'poll',
|
|
85983
|
+
device_code: ctx.deviceCode,
|
|
85984
|
+
});
|
|
85985
|
+
// Lark domain switch (once only)
|
|
85986
|
+
if (((_c = pollRes.user_info) === null || _c === void 0 ? void 0 : _c.tenant_brand) === 'lark' && !domainSwitched) {
|
|
85987
|
+
baseUrl = ctx.larkBaseUrl;
|
|
85988
|
+
domainSwitched = true;
|
|
85989
|
+
(_d = ctx.onStatusChange) === null || _d === void 0 ? void 0 : _d.call(ctx, { status: 'domain_switched' });
|
|
85990
|
+
poll();
|
|
85991
|
+
return;
|
|
85992
|
+
}
|
|
85993
|
+
// Success
|
|
85994
|
+
if (pollRes.client_id && pollRes.client_secret) {
|
|
85995
|
+
cleanup();
|
|
85996
|
+
resolve({
|
|
85997
|
+
client_id: pollRes.client_id,
|
|
85998
|
+
client_secret: pollRes.client_secret,
|
|
85999
|
+
user_info: pollRes.user_info,
|
|
86000
|
+
});
|
|
86001
|
+
return;
|
|
86002
|
+
}
|
|
86003
|
+
// Handle errors
|
|
86004
|
+
switch (pollRes.error) {
|
|
86005
|
+
case 'authorization_pending':
|
|
86006
|
+
(_e = ctx.onStatusChange) === null || _e === void 0 ? void 0 : _e.call(ctx, { status: 'polling' });
|
|
86007
|
+
break;
|
|
86008
|
+
case 'slow_down':
|
|
86009
|
+
interval += 5000;
|
|
86010
|
+
(_f = ctx.onStatusChange) === null || _f === void 0 ? void 0 : _f.call(ctx, { status: 'slow_down', interval: interval / 1000 });
|
|
86011
|
+
break;
|
|
86012
|
+
case 'access_denied':
|
|
86013
|
+
case 'expired_token':
|
|
86014
|
+
cleanup();
|
|
86015
|
+
reject(createError(pollRes.error, (_g = pollRes.error_description) !== null && _g !== void 0 ? _g : 'Unknown error'));
|
|
86016
|
+
return;
|
|
86017
|
+
default:
|
|
86018
|
+
if (pollRes.error) {
|
|
86019
|
+
cleanup();
|
|
86020
|
+
reject(createError(pollRes.error, (_h = pollRes.error_description) !== null && _h !== void 0 ? _h : 'Unknown error'));
|
|
86021
|
+
return;
|
|
86022
|
+
}
|
|
86023
|
+
break;
|
|
86024
|
+
}
|
|
86025
|
+
pollTimer = setTimeout(poll, interval);
|
|
86026
|
+
}
|
|
86027
|
+
catch (e) {
|
|
86028
|
+
cleanup();
|
|
86029
|
+
reject(e);
|
|
86030
|
+
}
|
|
86031
|
+
});
|
|
86032
|
+
poll();
|
|
86033
|
+
});
|
|
86034
|
+
}
|
|
86035
|
+
function registerApp(options) {
|
|
86036
|
+
var _a, _b, _c, _d;
|
|
86037
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
86038
|
+
const { domain, source, signal, onQRCodeReady, onStatusChange } = options;
|
|
86039
|
+
const baseUrl = `https://${domain !== null && domain !== void 0 ? domain : DEFAULT_FEISHU_DOMAIN}`;
|
|
86040
|
+
const beginRes = yield begin(baseUrl);
|
|
86041
|
+
const qrCodeUrl = new URL(beginRes.verification_uri_complete);
|
|
86042
|
+
qrCodeUrl.searchParams.set('from', 'sdk');
|
|
86043
|
+
qrCodeUrl.searchParams.set('source', source ? `${SDK_NAME}/${source}` : SDK_NAME);
|
|
86044
|
+
qrCodeUrl.searchParams.set('tp', 'sdk');
|
|
86045
|
+
onQRCodeReady({
|
|
86046
|
+
url: qrCodeUrl.toString(),
|
|
86047
|
+
expireIn: (_a = beginRes.expire_in) !== null && _a !== void 0 ? _a : 600,
|
|
86048
|
+
});
|
|
86049
|
+
return startPolling({
|
|
86050
|
+
baseUrl,
|
|
86051
|
+
deviceCode: beginRes.device_code,
|
|
86052
|
+
interval: ((_b = beginRes.interval) !== null && _b !== void 0 ? _b : 5) * 1000,
|
|
86053
|
+
expireIn: ((_c = beginRes.expire_in) !== null && _c !== void 0 ? _c : 600) * 1000,
|
|
86054
|
+
larkBaseUrl: `https://${(_d = options.larkDomain) !== null && _d !== void 0 ? _d : DEFAULT_LARK_DOMAIN}`,
|
|
86055
|
+
signal,
|
|
86056
|
+
onStatusChange,
|
|
86057
|
+
});
|
|
86058
|
+
});
|
|
86059
|
+
}
|
|
86060
|
+
|
|
85900
86061
|
exports.AESCipher = AESCipher;
|
|
85901
86062
|
exports.Aily = Aily;
|
|
85902
86063
|
exports.CAppTicket = CAppTicket;
|
|
@@ -85912,6 +86073,7 @@ exports.adaptKoaRouter = adaptKoaRouter;
|
|
|
85912
86073
|
exports.defaultHttpInstance = defaultHttpInstance;
|
|
85913
86074
|
exports.generateChallenge = generateChallenge;
|
|
85914
86075
|
exports.messageCard = messageCard;
|
|
86076
|
+
exports.registerApp = registerApp;
|
|
85915
86077
|
exports.withAll = withAll;
|
|
85916
86078
|
exports.withHelpDeskCredential = withHelpDeskCredential;
|
|
85917
86079
|
exports.withTenantKey = withTenantKey;
|
package/package.json
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -293056,6 +293056,7 @@ declare class WSClient {
|
|
|
293056
293056
|
private eventDispatcher?;
|
|
293057
293057
|
private pingInterval?;
|
|
293058
293058
|
private reconnectInterval?;
|
|
293059
|
+
private reconnectGeneration;
|
|
293059
293060
|
private isConnecting;
|
|
293060
293061
|
private reconnectInfo;
|
|
293061
293062
|
private agent?;
|
|
@@ -293195,4 +293196,32 @@ declare class Aily {
|
|
|
293195
293196
|
};
|
|
293196
293197
|
}
|
|
293197
293198
|
|
|
293198
|
-
|
|
293199
|
+
interface QRCodeInfo {
|
|
293200
|
+
url: string;
|
|
293201
|
+
expireIn: number;
|
|
293202
|
+
}
|
|
293203
|
+
interface StatusChangeInfo {
|
|
293204
|
+
status: 'polling' | 'slow_down' | 'domain_switched';
|
|
293205
|
+
interval?: number;
|
|
293206
|
+
}
|
|
293207
|
+
interface RegisterAppOptions {
|
|
293208
|
+
domain?: string;
|
|
293209
|
+
larkDomain?: string;
|
|
293210
|
+
source?: string;
|
|
293211
|
+
signal?: AbortSignal;
|
|
293212
|
+
onQRCodeReady: (info: QRCodeInfo) => void;
|
|
293213
|
+
onStatusChange?: (info: StatusChangeInfo) => void;
|
|
293214
|
+
}
|
|
293215
|
+
interface UserInfo {
|
|
293216
|
+
open_id?: string;
|
|
293217
|
+
tenant_brand?: 'feishu' | 'lark';
|
|
293218
|
+
}
|
|
293219
|
+
interface RegisterAppResult {
|
|
293220
|
+
client_id: string;
|
|
293221
|
+
client_secret: string;
|
|
293222
|
+
user_info?: UserInfo;
|
|
293223
|
+
}
|
|
293224
|
+
|
|
293225
|
+
declare function registerApp(options: RegisterAppOptions): Promise<RegisterAppResult>;
|
|
293226
|
+
|
|
293227
|
+
export { AESCipher, Aily, AppType, CAppTicket, CTenantAccessToken, Cache, CardActionHandler, Client, Domain, EventDispatcher, IHandles as EventHandles, HttpInstance, HttpRequestOptions, InteractiveCard, InteractiveCardActionEvent, InteractiveCardActionItem, InteractiveCardButtonActionItem, InteractiveCardDatePickerActionItem, InteractiveCardDivElement, InteractiveCardDividerElement, InteractiveCardElement, InteractiveCardField, InteractiveCardImageElement, InteractiveCardImageItem, InteractiveCardLarkMdItem, InteractiveCardMarkdownElement, InteractiveCardNoteElement, InteractiveCardOverflowActionItem, InteractiveCardPlainTextItem, InteractiveCardSelectMenuActionItem, InteractiveCardTextItem, InteractiveCardTitle, InteractiveCardUrlItem, InterfaceCardActionElement, LoggerLevel, WSClient, adaptDefault, adaptExpress, adaptKoa, adaptKoaRouter, defaultHttpInstance, generateChallenge, messageCard, registerApp, withAll, withHelpDeskCredential, withTenantKey, withTenantToken, withUserAccessToken };
|