feat: add standalone login renewal
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s

This commit is contained in:
2026-08-18 08:04:37 +00:00
parent ac7be79288
commit b3fa2315ba
11 changed files with 118 additions and 19 deletions
+34
View File
@@ -182,6 +182,39 @@ test("stdio bridge discards a connected client after its access token expires",
assert.equal(result.content[1].uri, "https://approval.example.test/oauth/device?user_code=NEXT-1234");
});
test("expired device link is replaced by a fresh login challenge without reinstalling", async () => {
const root = mkdtempSync(join(tmpdir(), "ding-bridge-expired-link-"));
const statePath = join(root, "session.json");
writeFileSync(statePath, JSON.stringify({
client: { client_id: "bridge-client" },
device: {
device_code: "expired-device", user_code: "OLD-1234",
verification_uri_complete: "https://approval.example.test/oauth/device?user_code=OLD-1234",
expiresAt: Date.now() - 1, interval: 5,
},
}));
const paths: string[] = [];
const bridge = new RemoteBridge("https://approval.example.test/mcp", statePath, async (input) => {
const path = new URL(String(input)).pathname;
paths.push(path);
if (path === "/oauth/device/authorize") return Response.json({
device_code: "fresh-device", user_code: "NEW-5678",
verification_uri_complete: "https://approval.example.test/oauth/device?user_code=NEW-5678",
expires_in: 600, interval: 5,
});
throw new Error(`unexpected ${path}`);
});
const result = await bridge.loginChallenge();
assert.deepEqual(paths, ["/oauth/device/authorize"]);
assert.equal(result.authenticated, false);
assert.ok("authChallenge" in result);
assert.equal(result.authChallenge.directUrl, "https://approval.example.test/oauth/device?user_code=NEW-5678");
assert.match(result.message, /无需重新安装/);
assert.doesNotMatch(readFileSync(statePath, "utf8"), /expired-device|OLD-1234/);
});
test("MCP approval_login returns both portable text and a standard resource link", async () => {
const root = mkdtempSync(join(tmpdir(), "ding-bridge-mcp-login-"));
const bridge = new RemoteBridge("https://approval.example.test/mcp", join(root, "session.json"), async (input) => {
@@ -210,6 +243,7 @@ test("MCP approval_login returns both portable text and a standard resource link
assert.equal((response.structuredContent as any).result.authChallenge.directUrl, link?.uri);
const tools = await client.listTools();
const instructions = client.getInstructions() ?? "";
assert.match(instructions, /登录链接过期.*approval_login.*绝不.*重新安装/);
assert.match(instructions, /审批引用.*调用容量.*内部分批.*不得向用户解释/);
assert.doesNotMatch(instructions, /最多.*20|selectionId|approvalId|targetId/);
const history = tools.tools.find((tool) => tool.name === "approval_list_history")!;
+36 -1
View File
@@ -98,7 +98,9 @@ test("generic installer preserves identity across safe updates, rollback, downgr
["https://approval.example.test/downloads/manifest.sig", signature],
...Object.entries(manifest.artifacts).map(([name, artifact]) => [artifact.url, bodies[name as keyof typeof bodies]] as [string, Buffer]),
]);
let downloadCalls = 0;
const fetchFn = async (input: string | URL | Request) => {
downloadCalls += 1;
const url = String(input);
const body = responses.get(url);
return body ? new Response(new Uint8Array(body), { status: 200 }) : new Response("missing", { status: 404 });
@@ -122,6 +124,11 @@ test("generic installer preserves identity across safe updates, rollback, downgr
assert.match(installOutput.nextSteps.join(" "), /立即处理 postInstall.*不得等待用户首次查询审批/s);
assert.equal(installOutput.postInstall.status, "authorization_required");
assert.equal(installOutput.postInstall.authChallenge.directUrl, "https://approval.example.test/oauth/device?user_code=INSTALL-1234");
assert.deepEqual(installOutput.loginCommand, {
command: join(appRoot, "bin", process.platform === "win32" ? "dingtalk-approval.cmd" : "dingtalk-approval"),
args: ["login", "--json"],
});
assert.deepEqual(installOutput.postInstall.loginCommand, installOutput.loginCommand);
assert.deepEqual(installOutput.postInstall.confirmTool, { name: "approval_login", arguments: {} });
assert.deepEqual(installOutput.postInstall.verifyTool, {
name: "approval_list_todos", arguments: { source: "all", query: "", limit: 1 },
@@ -137,6 +144,27 @@ test("generic installer preserves identity across safe updates, rollback, downgr
assert.doesNotMatch(readFileSync(paths.bridgeCommand, "utf8"), /--url/);
const stableBridgePath = paths.bridgeCommand;
const initialRuntime = state.runtimeDir;
const callsBeforeStandaloneLogin = downloadCalls;
output.length = 0;
assert.equal(await runClientInstaller(["login", "--app-root", appRoot, "--json"], commandContext), 0);
const loginOutput = JSON.parse(output.pop()!);
assert.equal(loginOutput.operation, "login");
assert.equal(loginOutput.status, "authorization_required");
assert.equal(loginOutput.authChallenge.directUrl, "https://approval.example.test/oauth/device?user_code=INSTALL-1234");
assert.deepEqual(loginOutput.loginCommand, installOutput.loginCommand);
assert.equal(downloadCalls, callsBeforeStandaloneLogin, "standalone login must not download or reinstall artifacts");
output.length = 0;
assert.equal(await runClientInstaller(["login", "--app-root", appRoot, "--json"], {
...commandContext, env: { ...env, FAKE_BRIDGE_AUTHENTICATED: "1" },
}), 0);
const authenticatedLogin = JSON.parse(output.pop()!);
assert.equal(authenticatedLogin.status, "authenticated");
assert.equal(authenticatedLogin.authenticated, true);
assert.equal(authenticatedLogin.authChallenge, undefined);
assert.equal(downloadCalls, callsBeforeStandaloneLogin, "authenticated login check must stay local except for the Bridge flow");
mkdirSync(dirname(paths.sessionFile), { recursive: true });
const sessionBody = Buffer.from(`${JSON.stringify({
authenticated: true,
@@ -273,6 +301,9 @@ test("generic installer preserves identity across safe updates, rollback, downgr
});
assert.equal(removed, 0);
assert.equal(existsSync(appRoot), false);
output.length = 0;
assert.equal(await runClientInstaller(["login", "--app-root", appRoot, "--json"], commandContext), 1);
assert.match(JSON.parse(output.pop()!).error, /尚未安装/);
});
function manifestFixture(
@@ -311,7 +342,11 @@ function digest(value: Buffer): string { return createHash("sha256").update(valu
function createFakeNpm(bin: string): void {
const fakeBridge = `#!/usr/bin/env node
if (process.env.FAKE_BRIDGE_FAIL === "1") process.exit(2);
if (process.argv.includes("--post-install-login")) {
if (process.argv.includes("--login") || process.argv.includes("--post-install-login")) {
if (process.env.FAKE_BRIDGE_AUTHENTICATED === "1") {
console.log(JSON.stringify({ authenticated: true, message: "已登录,可以读取审批。" }));
process.exit(0);
}
console.log(JSON.stringify({
authenticated: false,
message: "需要完成钉钉授权登录。",
+2
View File
@@ -15,6 +15,7 @@ test("customer distribution is a generic single-login gateway bridge", () => {
assert.doesNotMatch(installer, /--host|WorkBuddy|Codex|Claude/);
assert.match(installer, /不会通过其他消息通道自动推送链接.*完整登录 URL.*当前回复/s);
assert.match(installer, /读取返回的 `postInstall`.*不得等到用户第一次查询审批/s);
assert.match(installer, /登录链接过期.*绝不能重新安装.*approval_login.*loginCommand/s);
assert.match(installer, /onboarding\.firstRun=true.*简短示例.*不要自动执行/s);
assert.doesNotMatch(installer, /通过当前 Messenger/);
assert.doesNotMatch(installer, /DWS_CLI_PATH|dingtalk-workspace-cli|APP_MODE=local/);
@@ -27,6 +28,7 @@ test("customer distribution is a generic single-login gateway bridge", () => {
assert.match(skill, /does not send or push.*current user-facing reply/s);
assert.match(skill, /handle the installer's `postInstall` result immediately.*waiting for the user's first approval request/s);
assert.match(skill, /onboarding\.firstRun=true.*Do not execute an example automatically/s);
assert.match(skill, /expired login link.*approval_login.*never reinstall/s);
const builder = readFileSync("scripts/build-release.mjs", "utf8");
assert.match(builder, /schemaVersion: 3,\s+version,/s);