feat: harden install and resumable OAuth login
This commit is contained in:
@@ -3,7 +3,7 @@ import { existsSync, mkdtempSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { clientConfig, runCli } from "../src/cli.js";
|
||||
import { clientConfig, runCli, setupAuthorizationEvent } from "../src/cli.js";
|
||||
import { VERSION } from "../src/version.js";
|
||||
|
||||
test("package, manifest, and runtime versions agree with commit-pinned installation", () => {
|
||||
@@ -19,33 +19,115 @@ test("package, manifest, and runtime versions agree with commit-pinned installat
|
||||
assert.equal(manifest.distribution.releaseRef, undefined);
|
||||
assert.equal(manifest.installation.temporarySourceArchiveBuild, true);
|
||||
assert.equal(manifest.installation.fallbackToRelease, false);
|
||||
assert.equal(manifest.installation.supportsInPlaceUpdate, true);
|
||||
assert.equal(manifest.installation.sameCommitBuildSkip, true);
|
||||
assert.deepEqual(manifest.installation.installReceipt, {
|
||||
path: "<install-prefix>/install-receipt.json",
|
||||
schemaVersion: 1,
|
||||
posixFileMode: "0600",
|
||||
containsSecrets: false,
|
||||
});
|
||||
assert.deepEqual(manifest.installation.operationDetection, [
|
||||
"fresh-install",
|
||||
"in-place-update",
|
||||
"repair",
|
||||
"already-current",
|
||||
]);
|
||||
assert.equal(manifest.entrypoints.guidedCommand, "client-config");
|
||||
assert.equal(manifest.entrypoints.mcpLoginTool, "nocobase_login");
|
||||
assert.equal(manifest.entrypoints.mcpLoginStatusTool, "nocobase_login_status");
|
||||
assert.equal(manifest.authentication.processRestartResumable, true);
|
||||
assert.equal(
|
||||
manifest.authentication.pendingAuthorizationStorage,
|
||||
"short-lived per-user private local file",
|
||||
);
|
||||
assert.equal(manifest.authentication.allowDeviceCodeExposure, false);
|
||||
assert.deepEqual(manifest.authentication.userVerificationDelivery, {
|
||||
fields: ["login_url", "verification_uri_complete", "user_code"],
|
||||
intendedRecipient: "requesting-user",
|
||||
channel: "current-private-conversation",
|
||||
shareableWithIntendedRecipient: true,
|
||||
publicSharingAllowed: false,
|
||||
containsBearerCredentials: false,
|
||||
containsDeviceCode: false,
|
||||
remoteDesktopRequired: false,
|
||||
});
|
||||
assert.deepEqual(packageJson.dependencies, undefined);
|
||||
assert.equal(manifest.runtime.runtimeNpmDependencies, 0);
|
||||
});
|
||||
|
||||
test("installation protocol resolves main once and never falls back to an old Release", () => {
|
||||
test("installation and update protocol resolves main once and never falls back to an old Release", () => {
|
||||
const guide = readFileSync(new URL("../INSTALL.md", import.meta.url), "utf8");
|
||||
const readme = readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
||||
const installUrl = "https://git.justconn.top/gitea-admin/nocobase-mcp/raw/branch/main/INSTALL.md";
|
||||
assert.match(guide, /branches\/main/);
|
||||
assert.match(guide, /40 位十六进制完整 SHA/);
|
||||
assert.match(guide, /resolve-latest-commit-then-pin/);
|
||||
assert.match(guide, /installation\.supportsInPlaceUpdate=true/);
|
||||
assert.match(guide, /installation\.sameCommitBuildSkip=true/);
|
||||
assert.match(guide, /archive\/<commit-sha>\.zip/);
|
||||
assert.match(guide, /archive\/<commit-sha>\.tar\.gz/);
|
||||
assert.match(guide, /Windows 使用 `.zip`/);
|
||||
assert.match(guide, /不得无条件调用.*TarFile\.extractall/);
|
||||
assert.match(guide, /inspect\.signature/);
|
||||
assert.match(guide, /extractfile\(\)/);
|
||||
assert.match(guide, /不得直接调用不带过滤器的 `extractall\(\)`/);
|
||||
assert.match(guide, /不需要重新解析 `main`、重新下载归档或中断用户/);
|
||||
assert.match(guide, /不得.*退回旧 Release/);
|
||||
assert.match(guide, /client-config --json/);
|
||||
assert.match(guide, /client-config \[--config <existing-config-path>\] \[--enable-writes\] --json/);
|
||||
assert.match(guide, /nocobase_login\(\{\}\)/);
|
||||
assert.match(guide, /stdio 进程.*退出或重启/);
|
||||
assert.match(guide, /下一次 `nocobase_login_status`.*恢复同一个链接/);
|
||||
assert.match(guide, /判断首次安装、更新或已有最新版/);
|
||||
assert.match(guide, /install-receipt\.json/);
|
||||
assert.match(guide, /保留.*CRM.*配置.*个人凭据.*只读\/写入模式/);
|
||||
assert.match(guide, /先调用 `crm_health`/);
|
||||
assert.match(guide, /不得仅因更新而要求用户重新登录/);
|
||||
assert.match(guide, /不得以“链接包含一次性凭据”为由拒绝发送/);
|
||||
assert.match(guide, /不得要求用户操作服务器远程桌面/);
|
||||
assert.ok(guide.includes(installUrl));
|
||||
assert.ok(readme.includes(installUrl));
|
||||
assert.match(readme, /帮我安装或更新 NocoBase CRM MCP/);
|
||||
assert.match(readme, /相同 Commit 不会重复构建/);
|
||||
assert.match(readme, /只有认证确实缺失或失效时才发起登录/);
|
||||
assert.match(readme, /无界面或远程环境也要发到当前私聊/);
|
||||
assert.match(readme, /不要要求远程桌面/);
|
||||
assert.match(readme, /nocobase_login/);
|
||||
const promptStart = readme.indexOf("请按以下 INSTALL.md");
|
||||
const promptEnd = readme.indexOf(installUrl, promptStart) + installUrl.length;
|
||||
assert.ok(promptStart >= 0 && promptEnd > promptStart);
|
||||
assert.ok([...readme.slice(promptStart, promptEnd)].length < 350);
|
||||
assert.doesNotMatch(guide, /下载本文件所在的同一固定 Release/);
|
||||
assert.doesNotMatch(readme, /<main 分支的 INSTALL\.md 链接>/);
|
||||
});
|
||||
|
||||
test("headless setup emits user-deliverable OAuth verification metadata", () => {
|
||||
const event = setupAuthorizationEvent({
|
||||
opened: false,
|
||||
url: "https://justconn.top/idpOAuth/device?user_code=TEST-CODE",
|
||||
userCode: "TEST-CODE",
|
||||
expiresIn: 600,
|
||||
});
|
||||
assert.equal(event.event, "authorization_required");
|
||||
assert.equal(event.authorization_type, "oauth_device_user_verification");
|
||||
assert.equal(event.login_url, "https://justconn.top/idpOAuth/device?user_code=TEST-CODE");
|
||||
assert.equal(event.verification_uri_complete, event.login_url);
|
||||
assert.equal(event.user_code, "TEST-CODE");
|
||||
assert.equal(event.intended_recipient, "requesting_user");
|
||||
assert.equal(event.delivery_channel, "current_private_conversation");
|
||||
assert.equal(event.shareable_with_requesting_user, true);
|
||||
assert.equal(event.private_conversation_required, true);
|
||||
assert.equal(event.public_sharing_allowed, false);
|
||||
assert.equal(event.contains_bearer_credentials, false);
|
||||
assert.equal(event.contains_device_code, false);
|
||||
assert.equal(event.remote_desktop_required, false);
|
||||
assert.match(String(event.next_action), /current private conversation/);
|
||||
assert.match(String(event.next_action), /remote desktop is not required/);
|
||||
assert.equal(Object.hasOwn(event, "access_token"), false);
|
||||
assert.equal(Object.hasOwn(event, "refresh_token"), false);
|
||||
assert.equal(Object.hasOwn(event, "device_code"), false);
|
||||
});
|
||||
|
||||
test("generated MCP config contains no Key, token, or environment secret", () => {
|
||||
const config = clientConfig("/private/config.json", false);
|
||||
const serialized = JSON.stringify(config);
|
||||
|
||||
Reference in New Issue
Block a user