dsh-mcp-connector 0.2.25 → 0.2.26
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/CHANGELOG.md +14 -1
- package/docs/PLUGIN-UPDATE.md +4 -3
- package/docs/USER-GUIDE.md +1 -1
- package/lib/client.js +104 -2
- package/lib/index.js +114 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.26] - 2026-08-27
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- 升级启动时自动发现并归并历史版本为同 issuer 卡片保存的多份 OAuth Grant;归并前先验证过期 Grant 是否仍可刷新,避免用较新但已失效的凭据覆盖可恢复授权。
|
|
12
|
+
- 多个 DSH 进程并发刷新同一组轮换式 Refresh Token 时,收到 `invalid_grant` 后会短暂重读本机持久化 Grant;发现其他进程已保存新 Token 时自动采用并重试,不再误报“需重新授权”。
|
|
13
|
+
- 页面不再盲信 Update Provider 的 `succeeded`;独立校验更新前、预期与实际版本,检测降级、解析版本偏差或无效结果时拒绝重启,并在 Provider 保留恢复点时自动回滚。
|
|
14
|
+
|
|
15
|
+
### Verification
|
|
16
|
+
|
|
17
|
+
- 129 项自动测试、lint、npm 发布包白名单与敏感内容扫描通过;新增历史 Grant 安全归并、失效候选回退、跨进程 Token 轮换恢复和 Provider 版本完整性回归测试。
|
|
18
|
+
|
|
7
19
|
## [0.2.25] - 2026-08-27
|
|
8
20
|
|
|
9
21
|
### Added
|
|
@@ -414,7 +426,8 @@
|
|
|
414
426
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
415
427
|
- iframe 消息校验同源和消息来源。
|
|
416
428
|
|
|
417
|
-
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.
|
|
429
|
+
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.26...HEAD
|
|
430
|
+
[0.2.26]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.25...v0.2.26
|
|
418
431
|
[0.2.25]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.24...v0.2.25
|
|
419
432
|
[0.2.24]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.23...v0.2.24
|
|
420
433
|
[0.2.23]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.22...v0.2.23
|
package/docs/PLUGIN-UPDATE.md
CHANGED
|
@@ -21,10 +21,11 @@ DSH Market 适配器从 `GET /dsh-market/api/v1/capabilities` 开始,要求 `s
|
|
|
21
21
|
1. 插件自己的服务端缓存检查 npm `latest` 与 GitHub Release,只在 npm 已有可安装新版本时提示升级。
|
|
22
22
|
2. 选中的 Provider 再检查本 profile 中 `dsh-mcp-connector` 的实际安装版本和目标版本。
|
|
23
23
|
3. 用户点击“一键更新”后,Provider 立即返回 `operationId`;页面通过适配器轮询任务状态,展示排队、解析、下载和安装进度。
|
|
24
|
-
4.
|
|
25
|
-
5. Provider
|
|
24
|
+
4. Provider 报告成功后,页面仍会独立校验 `beforeVersion`、点击时的预期版本与 `installedVersion`。降级、目标不一致或无效结果会被拒绝;Provider 保留恢复点时自动回滚,不向用户展示重启。
|
|
25
|
+
5. 完整性校验通过后,按 Provider 返回的激活结论展示“刷新生效”或重启提示。Web 且 Provider 明确允许进程重启时才展示“立即重启”;Desktop 等由外壳管理生命周期的宿主只提示用户重启,不自行杀进程。
|
|
26
|
+
6. Provider 保留恢复点时展示“回滚”。回滚仍由 Provider 执行,MCP连接器不直接修改 profile、lockfile 或 `node_modules`。
|
|
26
27
|
|
|
27
|
-
失败结果使用稳定代码区分正在运行的 Agent
|
|
28
|
+
失败结果使用稳定代码区分正在运行的 Agent、其他安装任务占用、镜像同步、版本未变化、超时和权限拒绝,以及 `DOWNGRADE_DETECTED`、`RESOLVED_VERSION_MISMATCH`、`INVALID_UPDATE_RESULT` 三类完整性故障。可重试失败保留重试操作;Registry 刚发布但镜像仍未同步时,用户可在看到明确提示后选择强制重试。
|
|
28
29
|
|
|
29
30
|
## 安全边界
|
|
30
31
|
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -75,7 +75,7 @@ stdio 市场卡片也可能显示一个或多个凭据字段,例如 API Token
|
|
|
75
75
|
|
|
76
76
|
OAuth 一键连接要求服务商支持标准 OAuth 2.1/PKCE 和公开元数据发现。动态客户端注册既支持无需客户端密钥的 `none`,也支持服务商签发密钥的 `client_secret_post` 与 `client_secret_basic`。客户端密钥仅与 OAuth Grant 一同保存在 DSH 本机,用于换取、刷新和撤销 Token;插件不会要求用户把 OAuth Token 或客户端密钥复制到聊天中。
|
|
77
77
|
|
|
78
|
-
当市场描述声明 `grantSharing: "issuer"` 时,同一账号下相同 issuer、scope 和客户端鉴权方式的卡片共享一组 Grant。首次授权仍只启用用户点击的卡片;之后连接同组卡片会直接复用现有授权,不再重复打开 OAuth
|
|
78
|
+
当市场描述声明 `grantSharing: "issuer"` 时,同一账号下相同 issuer、scope 和客户端鉴权方式的卡片共享一组 Grant。首次授权仍只启用用户点击的卡片;之后连接同组卡片会直接复用现有授权,不再重复打开 OAuth 页面。升级时会验证并自动归并旧版本留下的同 issuer 多份 Grant;多个 DSH 进程并发轮换 Refresh Token 时也会重读较新的本机持久化凭据后自动恢复。网络、OAuth 元数据发现或服务端 5xx 等暂时故障只进入“自动重试中”,确认本机也没有其他进程保存的新 Token,且明确收到 Refresh Token/客户端失效错误时,才进入“需重新授权”。
|
|
79
79
|
|
|
80
80
|
## 5. 查看详情、Prompt 与工具
|
|
81
81
|
|
package/lib/client.js
CHANGED
|
@@ -363,6 +363,9 @@ window.__ModuleLoader__.load({
|
|
|
363
363
|
OPERATION_BUSY: "插件市场正在处理其他任务",
|
|
364
364
|
RELEASE_TOO_FRESH: "新版本仍在镜像同步",
|
|
365
365
|
VERSION_UNCHANGED: "下载后版本未变化",
|
|
366
|
+
DOWNGRADE_DETECTED: "已阻止版本降级",
|
|
367
|
+
RESOLVED_VERSION_MISMATCH: "下载版本与预期不一致",
|
|
368
|
+
INVALID_UPDATE_RESULT: "更新服务未返回有效版本",
|
|
366
369
|
UPDATE_TIMEOUT: "更新超时",
|
|
367
370
|
UPDATE_FORBIDDEN: "更新请求被拒绝",
|
|
368
371
|
UPDATE_REJECTED: "无法执行本次更新"
|
|
@@ -370,6 +373,71 @@ window.__ModuleLoader__.load({
|
|
|
370
373
|
return labels[failure?.code] ?? failure?.message ?? "更新失败";
|
|
371
374
|
}
|
|
372
375
|
|
|
376
|
+
function parseClientVersion(value) {
|
|
377
|
+
if (typeof value !== "string") return null;
|
|
378
|
+
const normalized = value.trim().replace(/^v/i, "");
|
|
379
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(normalized)) return null;
|
|
380
|
+
const [withoutBuild] = normalized.split("+");
|
|
381
|
+
const [core, prerelease] = withoutBuild.split("-", 2);
|
|
382
|
+
return { normalized, core: core.split(".").map(Number), prerelease: prerelease?.split(".") ?? [] };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function compareClientVersions(left, right) {
|
|
386
|
+
const a = parseClientVersion(left);
|
|
387
|
+
const b = parseClientVersion(right);
|
|
388
|
+
if (a === null || b === null) return null;
|
|
389
|
+
for (let index = 0; index < 3; index += 1) {
|
|
390
|
+
if (a.core[index] !== b.core[index]) return a.core[index] - b.core[index];
|
|
391
|
+
}
|
|
392
|
+
if (a.prerelease.length === 0 && b.prerelease.length > 0) return 1;
|
|
393
|
+
if (a.prerelease.length > 0 && b.prerelease.length === 0) return -1;
|
|
394
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
395
|
+
for (let index = 0; index < length; index += 1) {
|
|
396
|
+
const aPart = a.prerelease[index];
|
|
397
|
+
const bPart = b.prerelease[index];
|
|
398
|
+
if (aPart === void 0) return -1;
|
|
399
|
+
if (bPart === void 0) return 1;
|
|
400
|
+
if (aPart === bPart) continue;
|
|
401
|
+
const aNumeric = /^\d+$/.test(aPart);
|
|
402
|
+
const bNumeric = /^\d+$/.test(bPart);
|
|
403
|
+
if (aNumeric && bNumeric) return Number(aPart) - Number(bPart);
|
|
404
|
+
if (aNumeric) return -1;
|
|
405
|
+
if (bNumeric) return 1;
|
|
406
|
+
return aPart.localeCompare(bPart);
|
|
407
|
+
}
|
|
408
|
+
return 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Independently verify a provider's terminal success before asking the user to restart. */
|
|
412
|
+
function completedUpdateIntegrityFailure(operation, expectedVersion) {
|
|
413
|
+
if (operation?.state !== "succeeded") return null;
|
|
414
|
+
const installed = parseClientVersion(operation.installedVersion);
|
|
415
|
+
if (installed === null) {
|
|
416
|
+
return {
|
|
417
|
+
code: "INVALID_UPDATE_RESULT",
|
|
418
|
+
message: "更新服务报告成功,但未返回有效的已安装版本",
|
|
419
|
+
retryable: false
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const before = parseClientVersion(operation.beforeVersion);
|
|
423
|
+
if (before !== null && compareClientVersions(installed.normalized, before.normalized) < 0) {
|
|
424
|
+
return {
|
|
425
|
+
code: "DOWNGRADE_DETECTED",
|
|
426
|
+
message: `更新服务将插件从 v${before.normalized} 降级到 v${installed.normalized}`,
|
|
427
|
+
retryable: false
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const expected = parseClientVersion(expectedVersion);
|
|
431
|
+
if (expected !== null && compareClientVersions(installed.normalized, expected.normalized) !== 0) {
|
|
432
|
+
return {
|
|
433
|
+
code: "RESOLVED_VERSION_MISMATCH",
|
|
434
|
+
message: `预期安装 v${expected.normalized},更新服务实际安装了 v${installed.normalized}`,
|
|
435
|
+
retryable: true
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
373
441
|
/**
|
|
374
442
|
* DSH 暂未公开“打开指定设置分区”服务:只依赖稳定的 sidebar.settings
|
|
375
443
|
* slot 和可访问文本打开插件市场,不依赖构建生成的 CSS 类名。
|
|
@@ -451,6 +519,7 @@ window.__ModuleLoader__.load({
|
|
|
451
519
|
const [updateProviderState, setUpdateProviderState] = (0, react.useState)("checking");
|
|
452
520
|
const [updateProviderSelection, setUpdateProviderSelection] = (0, react.useState)(null);
|
|
453
521
|
const [providerUpdateCheck, setProviderUpdateCheck] = (0, react.useState)(null);
|
|
522
|
+
const [expectedUpdateVersion, setExpectedUpdateVersion] = (0, react.useState)(null);
|
|
454
523
|
const [updateOperation, setUpdateOperation] = (0, react.useState)(null);
|
|
455
524
|
const [updateError, setUpdateError] = (0, react.useState)(null);
|
|
456
525
|
const [updateStarting, setUpdateStarting] = (0, react.useState)(false);
|
|
@@ -564,6 +633,34 @@ window.__ModuleLoader__.load({
|
|
|
564
633
|
provider.operation(capabilities, operationId).then((operation) => {
|
|
565
634
|
if (disposed) return;
|
|
566
635
|
setUpdateError(null);
|
|
636
|
+
const integrityFailure = completedUpdateIntegrityFailure(operation, expectedUpdateVersion);
|
|
637
|
+
if (integrityFailure !== null) {
|
|
638
|
+
const failedOperation = { ...operation, state: "failed", failure: integrityFailure };
|
|
639
|
+
setUpdateOperation(failedOperation);
|
|
640
|
+
const canRollback = capabilities.features?.rollback === true
|
|
641
|
+
&& operation.outcome?.rollback?.available === true
|
|
642
|
+
&& capabilities.endpoints.rollback !== null;
|
|
643
|
+
if (!canRollback) return;
|
|
644
|
+
setRollbackRunning(true);
|
|
645
|
+
provider.rollback(capabilities, operationId).then((rolledBackOperation) => {
|
|
646
|
+
if (disposed) return;
|
|
647
|
+
setRollbackRunning(false);
|
|
648
|
+
setUpdateOperation({ ...rolledBackOperation, integrityFailure });
|
|
649
|
+
}, (error) => {
|
|
650
|
+
console.error("[mcp-connector] automatic rollback failed:", error);
|
|
651
|
+
if (disposed) return;
|
|
652
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
653
|
+
setRollbackRunning(false);
|
|
654
|
+
setUpdateOperation({
|
|
655
|
+
...failedOperation,
|
|
656
|
+
outcome: {
|
|
657
|
+
...failedOperation.outcome,
|
|
658
|
+
rollback: { available: true, state: "failed", detail }
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
567
664
|
setUpdateOperation(operation);
|
|
568
665
|
if (["queued", "running"].includes(operation.state)) {
|
|
569
666
|
timer = window.setTimeout(poll, UPDATE_PROVIDER_OPERATION_POLL_MS);
|
|
@@ -584,7 +681,7 @@ window.__ModuleLoader__.load({
|
|
|
584
681
|
disposed = true;
|
|
585
682
|
window.clearTimeout(timer);
|
|
586
683
|
};
|
|
587
|
-
}, [open, updateOperation?.operationId, updateProviderSelection?.provider?.id]);
|
|
684
|
+
}, [open, updateOperation?.operationId, updateProviderSelection?.provider?.id, expectedUpdateVersion]);
|
|
588
685
|
(0, react.useEffect)(() => {
|
|
589
686
|
if (!open) return void 0;
|
|
590
687
|
closeRef.current?.focus?.();
|
|
@@ -600,6 +697,7 @@ window.__ModuleLoader__.load({
|
|
|
600
697
|
if (updateRunning || provider === void 0 || capabilities === void 0) return;
|
|
601
698
|
setUpdateError(null);
|
|
602
699
|
setUpdateStarting(true);
|
|
700
|
+
setExpectedUpdateVersion(providerUpdateCheck?.latestVersion ?? versionStatus?.latestVersion ?? null);
|
|
603
701
|
provider.start(capabilities, PLUGIN_PACKAGE_NAME, force).then((operation) => {
|
|
604
702
|
setUpdateStarting(false);
|
|
605
703
|
setUpdateOperation(operation);
|
|
@@ -687,6 +785,10 @@ window.__ModuleLoader__.load({
|
|
|
687
785
|
onClick: () => startUpdate(force),
|
|
688
786
|
children: force ? "强制重试" : "重试更新"
|
|
689
787
|
}),
|
|
788
|
+
rollbackRunning && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
789
|
+
className: "mcpConnectorUpdateStatus",
|
|
790
|
+
children: "检测到异常版本,正在自动回滚…"
|
|
791
|
+
}),
|
|
690
792
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
691
793
|
type: "button",
|
|
692
794
|
className: "mcpConnectorUpdateButton mcpConnectorUpdateSecondary",
|
|
@@ -707,7 +809,7 @@ window.__ModuleLoader__.load({
|
|
|
707
809
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
708
810
|
className: "mcpConnectorUpdateStatus",
|
|
709
811
|
children: rolledBack
|
|
710
|
-
? "已回滚"
|
|
812
|
+
? updateOperation.integrityFailure === void 0 ? "已回滚" : "已阻止异常版本并回滚"
|
|
711
813
|
: `已安装 v${updateOperation.installedVersion ?? versionStatus?.latestVersion}`
|
|
712
814
|
}),
|
|
713
815
|
rollbackFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
package/lib/index.js
CHANGED
|
@@ -380,12 +380,41 @@ export async function apply(ctx, config) {
|
|
|
380
380
|
}
|
|
381
381
|
|
|
382
382
|
async function refreshGrantWithRecovery(grantKey, phase) {
|
|
383
|
+
const observed = state.grants.get(grantKey)?.grant;
|
|
383
384
|
try {
|
|
384
385
|
return await refreshGrant(grantKey);
|
|
385
386
|
} catch (error) {
|
|
387
|
+
let finalError = error;
|
|
388
|
+
const initialFailure = classifyRefreshFailure(error);
|
|
389
|
+
// Refresh tokens are commonly rotated. If another DSH process refreshed
|
|
390
|
+
// the same persisted grant first, this process still holds the old token
|
|
391
|
+
// and receives invalid_grant. Re-read storage before declaring the user
|
|
392
|
+
// logged out; a newer token is authoritative and can be retried once.
|
|
393
|
+
if (config.persistSecrets && observed
|
|
394
|
+
&& ['invalid_grant', 'invalid_token'].includes(initialFailure.code)) {
|
|
395
|
+
for (const delayMs of [0, 100, 250]) {
|
|
396
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
397
|
+
const persisted = await grantStore.get(grantKey).catch(() => undefined);
|
|
398
|
+
const entry = state.grants.get(grantKey);
|
|
399
|
+
if (!entry || !persisted
|
|
400
|
+
|| persisted.updatedAt <= observed.updatedAt
|
|
401
|
+
|| persisted.refreshToken === observed.refreshToken) continue;
|
|
402
|
+
entry.grant = persisted;
|
|
403
|
+
entry.needsReauth = false;
|
|
404
|
+
entry.refreshFailureKind = null;
|
|
405
|
+
entry.lastRefreshError = null;
|
|
406
|
+
logger.info(`OAuth grant ${grantKey} adopted a newer persisted token after a concurrent rotation`);
|
|
407
|
+
try {
|
|
408
|
+
return await refreshGrant(grantKey);
|
|
409
|
+
} catch (retryError) {
|
|
410
|
+
finalError = retryError;
|
|
411
|
+
}
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
386
415
|
// 多个调用方可能等待同一个 refreshPromise;同一次失败只能记一次并调度一个重试。
|
|
387
|
-
if (!
|
|
388
|
-
throw
|
|
416
|
+
if (!finalError.refreshFailure) finalError.refreshFailure = recordRefreshFailure(grantKey, finalError, phase);
|
|
417
|
+
throw finalError;
|
|
389
418
|
}
|
|
390
419
|
}
|
|
391
420
|
|
|
@@ -858,6 +887,86 @@ export async function apply(ctx, config) {
|
|
|
858
887
|
}
|
|
859
888
|
}
|
|
860
889
|
|
|
890
|
+
/**
|
|
891
|
+
* v0.2.22 及更早版本会让四张企查查卡片分别保存动态客户端和 Grant。
|
|
892
|
+
* 新版升级时把同 issuer、同资源域且授权范围完整的历史记录归并为一组,
|
|
893
|
+
* 这样启动只刷新一个 refresh token,之后任意一张卡片重新授权也会修复整组。
|
|
894
|
+
*/
|
|
895
|
+
async function consolidatePersistedSharedGrants() {
|
|
896
|
+
const groups = new Map();
|
|
897
|
+
for (const record of state.connections.values()) {
|
|
898
|
+
if (record.auth?.mode !== 'oauth' || !record.auth.grantKey) continue;
|
|
899
|
+
const connector = state.merged.find((item) => item.id === record.connectorId);
|
|
900
|
+
if (connector?.auth?.grantSharing !== 'issuer') continue;
|
|
901
|
+
const groupKey = oauthSharingKey(connector);
|
|
902
|
+
const group = groups.get(groupKey) ?? { records: [], grantKeys: new Set() };
|
|
903
|
+
group.records.push(record);
|
|
904
|
+
group.grantKeys.add(record.auth.grantKey);
|
|
905
|
+
groups.set(groupKey, group);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
let consolidated = 0;
|
|
909
|
+
for (const group of groups.values()) {
|
|
910
|
+
if (group.grantKeys.size <= 1) continue;
|
|
911
|
+
const requiredResources = new Set(group.records.map((record) => record.url).filter(Boolean));
|
|
912
|
+
const candidates = [...group.grantKeys]
|
|
913
|
+
.map((key) => [key, state.grants.get(key)])
|
|
914
|
+
.filter(([, entry]) => entry
|
|
915
|
+
&& [...requiredResources].every((resource) => entry.grant.authorizedResources.includes(resource)))
|
|
916
|
+
.sort((left, right) => (
|
|
917
|
+
(right[1].grant.updatedAt ?? 0) - (left[1].grant.updatedAt ?? 0)
|
|
918
|
+
|| (right[1].grant.accessTokenExpiresAt ?? 0) - (left[1].grant.accessTokenExpiresAt ?? 0)
|
|
919
|
+
));
|
|
920
|
+
if (candidates.length === 0) continue;
|
|
921
|
+
|
|
922
|
+
// Do not retire three recoverable historical grants in favour of a newer
|
|
923
|
+
// but already-revoked refresh token. Validate expired candidates in
|
|
924
|
+
// recency order and only consolidate after one grant is demonstrably
|
|
925
|
+
// usable. If none is usable, preserve every grant so one subsequent
|
|
926
|
+
// authorization can replace the group without destroying recovery data.
|
|
927
|
+
let winnerCandidate;
|
|
928
|
+
for (const candidate of candidates) {
|
|
929
|
+
const [candidateKey, candidateEntry] = candidate;
|
|
930
|
+
if (candidateEntry.needsReauth) continue;
|
|
931
|
+
if (candidateEntry.grant.accessTokenExpiresAt <= Date.now()) {
|
|
932
|
+
try {
|
|
933
|
+
await refreshGrantWithRecovery(candidateKey, 'startup-consolidation');
|
|
934
|
+
} catch {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
winnerCandidate = candidate;
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
if (winnerCandidate === undefined) {
|
|
942
|
+
logger.warn(`preserved ${group.grantKeys.size} historical issuer-shared OAuth grants because none could be refreshed`);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const [winnerKey, winnerEntry] = winnerCandidate;
|
|
947
|
+
const connectorIds = [...new Set([
|
|
948
|
+
...(winnerEntry.grant.connectorIds ?? []),
|
|
949
|
+
...group.records.map((record) => record.connectorId),
|
|
950
|
+
])];
|
|
951
|
+
const winner = { ...winnerEntry.grant, connectorIds, updatedAt: Date.now() };
|
|
952
|
+
winnerEntry.grant = winner;
|
|
953
|
+
if (config.persistSecrets) await grantStore.put(winner);
|
|
954
|
+
|
|
955
|
+
for (const record of group.records) {
|
|
956
|
+
if (record.auth.grantKey === winnerKey) continue;
|
|
957
|
+
const next = { ...record, auth: { ...record.auth, grantKey: winnerKey }, updatedAt: Date.now() };
|
|
958
|
+
const persisted = config.persistSecrets ? await connectionStore.put(next) : next;
|
|
959
|
+
state.connections.set(record.key, persisted);
|
|
960
|
+
}
|
|
961
|
+
for (const losingKey of group.grantKeys) {
|
|
962
|
+
if (losingKey !== winnerKey) await retireGrantIfUnused(losingKey);
|
|
963
|
+
}
|
|
964
|
+
consolidated += group.grantKeys.size - 1;
|
|
965
|
+
logger.info(`consolidated ${group.grantKeys.size} issuer-shared OAuth grants into ${winnerKey}`);
|
|
966
|
+
}
|
|
967
|
+
return consolidated;
|
|
968
|
+
}
|
|
969
|
+
|
|
861
970
|
/* ───────────────────────── api 门面 ───────────────────────── */
|
|
862
971
|
|
|
863
972
|
const api = {
|
|
@@ -1612,6 +1721,9 @@ export async function apply(ctx, config) {
|
|
|
1612
1721
|
}
|
|
1613
1722
|
}
|
|
1614
1723
|
|
|
1724
|
+
const consolidatedGrantCount = await consolidatePersistedSharedGrants();
|
|
1725
|
+
if (consolidatedGrantCount > 0) logger.info(`consolidated ${consolidatedGrantCount} historical OAuth grant(s) during startup`);
|
|
1726
|
+
|
|
1615
1727
|
const prunedGrantCount = await pruneOrphanGrants();
|
|
1616
1728
|
if (prunedGrantCount > 0) logger.info(`removed ${prunedGrantCount} unreferenced OAuth grant(s) from local storage`);
|
|
1617
1729
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
4
4
|
"description": "General-purpose MCP connector, connection manager, plugin extension, and integration marketplace for DeepSeek Harness, initiated and maintained by Qichacha/QCC.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|