feat: support secure XFT submission attachments
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
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:
@@ -5,7 +5,8 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
||||
import type { AppConfig } from "../src/config.js";
|
||||
import type { ApprovalService } from "../src/approval-service.js";
|
||||
import type { XftApprovalProvider } from "../src/xft.js";
|
||||
import { approvalBatchSummary, APPROVAL_MCP_INSTRUCTIONS, compactResult, createApprovalMcpServer, isLoopbackAddress, mcpAllowedHosts, oauthHtmlContentSecurityPolicy } from "../src/server.js";
|
||||
import { SignedReferenceCodec } from "../src/signed-reference.js";
|
||||
import { approvalBatchSummary, APPROVAL_MCP_INSTRUCTIONS, assertAttachmentConfirmation, compactResult, createApprovalMcpServer, isLoopbackAddress, mcpAllowedHosts, oauthHtmlContentSecurityPolicy } from "../src/server.js";
|
||||
|
||||
function config(): AppConfig {
|
||||
return {
|
||||
@@ -96,6 +97,17 @@ test("MCP initialization teaches the workflow while tools/list stays compact", a
|
||||
assert.match(descriptions.approval_prepare_submission ?? "", /values.*code.*不能直接填label.*预览.*业务名称/s);
|
||||
const prepareSchema = tools.tools.find((tool) => tool.name === "approval_prepare_submission")?.inputSchema as any;
|
||||
assert.equal(prepareSchema.properties.allowDuplicateInvoices.default, false);
|
||||
for (const toolName of ["approval_prepare_submission", "approval_submit"]) {
|
||||
const submission = tools.tools.find((tool) => tool.name === toolName)!;
|
||||
const attachmentItem = (submission.inputSchema as any).properties.attachments.items;
|
||||
assert.ok(attachmentItem.properties.fileName, `${toolName} must accept validated attachment metadata`);
|
||||
assert.ok(attachmentItem.properties.sha256);
|
||||
assert.equal(attachmentItem.properties.localPath, undefined, "the gateway must never accept local paths");
|
||||
}
|
||||
const prepareAttachmentItem = prepareSchema.properties.attachments.items;
|
||||
assert.equal(prepareAttachmentItem.properties.attachmentRef, undefined, "prepare must not accept pre-uploaded references");
|
||||
const submitSchema = tools.tools.find((tool) => tool.name === "approval_submit")?.inputSchema as any;
|
||||
assert.ok(submitSchema.properties.attachments.items.properties.attachmentRef);
|
||||
assert.match(descriptions.approval_list_submission_templates ?? "", /出差申请.*外出.*差旅费用报销.*业务招待费用报销/);
|
||||
assert.match(descriptions.approval_submit ?? "", /禁止自动重试/);
|
||||
assert.match(descriptions.approval_revoke_submission ?? "", /禁止自动重试.*薪福通.*实验能力/);
|
||||
@@ -139,6 +151,29 @@ test("OAuth consent CSP permits only the DingTalk login redirect after same-orig
|
||||
assert.doesNotMatch(policy, /form-action[^;]*\*/);
|
||||
});
|
||||
|
||||
test("attachment uploads are bound to the signed-in user and the exact confirmed preview", () => {
|
||||
const signer = new SignedReferenceCodec("fixed-reference-secret-for-tests");
|
||||
const attachment = {
|
||||
fieldName: "发票附件", fileName: "invoice.pdf", size: 551,
|
||||
sha256: "a".repeat(64),
|
||||
};
|
||||
const confirmationId = signer.sign({
|
||||
kind: "submission_confirmation",
|
||||
source: "xft",
|
||||
userId: "user-1",
|
||||
attachmentManifest: [attachment],
|
||||
exp: Date.now() + 60_000,
|
||||
});
|
||||
|
||||
assert.doesNotThrow(() => assertAttachmentConfirmation(signer, { userId: "user-1" }, confirmationId, attachment));
|
||||
assert.throws(() => assertAttachmentConfirmation(
|
||||
signer, { userId: "user-1" }, confirmationId, { ...attachment, fileName: "unconfirmed.pdf" },
|
||||
), /不在用户刚刚确认的提单预览中/);
|
||||
assert.throws(() => assertAttachmentConfirmation(
|
||||
signer, { userId: "another-user" }, confirmationId, attachment,
|
||||
), /不属于当前用户/);
|
||||
});
|
||||
|
||||
test("remote gateway lists DingTalk OA and XFT through one authenticated MCP session", async () => {
|
||||
const dingTalk = {
|
||||
async listMyTodos() {
|
||||
|
||||
Reference in New Issue
Block a user