mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
373 lines
13 KiB
TypeScript
373 lines
13 KiB
TypeScript
// 验证 Mind 两个固定授权端点的请求边界和严格失败关闭
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
createMindAuthorizationReader,
|
|
type MindAuthorizationClientConfig,
|
|
} from "../src/mind-authorization.ts";
|
|
import { createOneTalkCutoverPolicy } from "../src/cutover-policy.ts";
|
|
import { ONETALK_PROTOCOL_VERSION } from "@trade-message-center/onetalk-contract";
|
|
|
|
const scope = {
|
|
mindUserId: "mind-user-1",
|
|
workspaceId: "workspace-1",
|
|
channelAccountId: "account-1",
|
|
};
|
|
|
|
const response = (status: number, body: unknown): Response =>
|
|
({
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
json: async () => body,
|
|
}) as Response;
|
|
|
|
test("sends only binding fields to Mind binding authorization", async () => {
|
|
let request: RequestInit | undefined;
|
|
const reader = createMindAuthorizationReader({
|
|
baseUrl: "https://mind.example.com",
|
|
timeoutMs: 100,
|
|
fetch: async (_input, init) => {
|
|
request = init;
|
|
return response(200, {
|
|
binding: "binding-1",
|
|
authorizationVersion: "version-1",
|
|
permissions: ["read", "send"],
|
|
mindScope: scope,
|
|
});
|
|
},
|
|
});
|
|
|
|
const decision = await reader.authorize({
|
|
connectionType: "plugin",
|
|
operation: "connect",
|
|
scope: { channelAccountId: "account-1", deviceId: "device-1" },
|
|
binding: "binding-1",
|
|
});
|
|
assert.equal(decision.allowed, true);
|
|
assert.deepEqual(JSON.parse(String(request?.body)), {
|
|
channelAccountId: "account-1",
|
|
binding: "binding-1",
|
|
});
|
|
assert.equal((request?.headers as Record<string, string>).cookie, undefined);
|
|
});
|
|
|
|
test("does not expose raw Mind authorization payload hooks", () => {
|
|
const requestConfig = {
|
|
baseUrl: "https://mind.example.com",
|
|
timeoutMs: 100,
|
|
// @ts-expect-error Raw request payload callbacks are not a safe auth boundary.
|
|
onRequestPayload: () => {},
|
|
} satisfies MindAuthorizationClientConfig;
|
|
const responseConfig = {
|
|
baseUrl: "https://mind.example.com",
|
|
timeoutMs: 100,
|
|
// @ts-expect-error Raw response body callbacks are not a safe auth boundary.
|
|
onResponseResult: () => {},
|
|
} satisfies MindAuthorizationClientConfig;
|
|
void requestConfig;
|
|
void responseConfig;
|
|
});
|
|
|
|
test("emits safe diagnostics for Mind authorization responses and transport failures", async () => {
|
|
const events: unknown[] = [];
|
|
const rejectedReader = createMindAuthorizationReader({
|
|
baseUrl: "https://mind.example.com",
|
|
timeoutMs: 100,
|
|
onDiagnostic: (event) => events.push(event),
|
|
fetch: async () => response(403, { code: "binding_revoked" }),
|
|
});
|
|
|
|
await rejectedReader.authorize({
|
|
connectionType: "plugin",
|
|
operation: "connect",
|
|
scope: { channelAccountId: "account-1", deviceId: "device-1" },
|
|
binding: "binding-1",
|
|
});
|
|
|
|
const unavailableReader = createMindAuthorizationReader({
|
|
baseUrl: "https://mind.example.com",
|
|
timeoutMs: 100,
|
|
onDiagnostic: (event) => events.push(event),
|
|
fetch: async () => {
|
|
throw new Error("transport details must not be logged");
|
|
},
|
|
});
|
|
await unavailableReader.authorize({
|
|
connectionType: "mind_page",
|
|
operation: "read",
|
|
scope: { channelAccountId: "account-1" },
|
|
cookie: "secret-session",
|
|
});
|
|
|
|
assert.deepEqual(events, [
|
|
{
|
|
event: "mind_authorization",
|
|
endpoint: "binding",
|
|
operation: "connect",
|
|
outcome: "request_started",
|
|
},
|
|
{
|
|
event: "mind_authorization",
|
|
endpoint: "binding",
|
|
operation: "connect",
|
|
outcome: "rejected",
|
|
status: 403,
|
|
code: "binding_revoked",
|
|
},
|
|
{
|
|
event: "mind_authorization",
|
|
endpoint: "session",
|
|
operation: "read",
|
|
outcome: "request_started",
|
|
},
|
|
{
|
|
event: "mind_authorization",
|
|
endpoint: "session",
|
|
operation: "read",
|
|
outcome: "request_failed",
|
|
code: "authorization_unavailable",
|
|
},
|
|
]);
|
|
assert.equal(JSON.stringify(events).includes("secret-session"), false);
|
|
});
|
|
|
|
test("forwards the original Cookie only to session authorization", async () => {
|
|
let url = "";
|
|
let request: RequestInit | undefined;
|
|
const reader = createMindAuthorizationReader({
|
|
baseUrl: "https://mind.example.com",
|
|
timeoutMs: 100,
|
|
fetch: async (input, init) => {
|
|
url = String(input);
|
|
request = init;
|
|
return response(200, {
|
|
binding: "binding-1",
|
|
authorizationVersion: "version-1",
|
|
permissions: ["read"],
|
|
mindScope: scope,
|
|
});
|
|
},
|
|
});
|
|
const decision = await reader.authorize({
|
|
connectionType: "mind_page",
|
|
operation: "read",
|
|
scope: { channelAccountId: "account-1" },
|
|
cookie: "mind_session=opaque",
|
|
});
|
|
assert.equal(decision.allowed, true);
|
|
assert.match(url, /authorize-session$/);
|
|
assert.equal((request?.headers as Record<string, string>).cookie, "mind_session=opaque");
|
|
assert.deepEqual(JSON.parse(String(request?.body)), { channelAccountId: "account-1" });
|
|
});
|
|
|
|
test("maps malformed, unknown and transport failures to unavailable", async () => {
|
|
for (const result of [
|
|
response(200, { binding: "binding-1" }),
|
|
response(403, { code: "unknown_code" }),
|
|
null,
|
|
]) {
|
|
const reader = createMindAuthorizationReader({
|
|
baseUrl: "https://mind.example.com",
|
|
timeoutMs: 100,
|
|
fetch: async () => {
|
|
if (result === null) throw new Error("network");
|
|
return result;
|
|
},
|
|
});
|
|
const decision = await reader.authorize({
|
|
connectionType: "plugin",
|
|
operation: "connect",
|
|
scope: { channelAccountId: "account-1", deviceId: "device-1" },
|
|
binding: "binding-1",
|
|
});
|
|
assert.deepEqual(decision, { allowed: false, code: "authorization_unavailable" });
|
|
}
|
|
});
|
|
|
|
test("enforces exact Mind status-code pairs and transport failure boundaries", async () => {
|
|
const validBody = {
|
|
binding: "binding-1",
|
|
authorizationVersion: "version-1",
|
|
permissions: ["read"],
|
|
mindScope: scope,
|
|
};
|
|
const cases = [
|
|
{
|
|
input: {
|
|
connectionType: "plugin" as const,
|
|
operation: "connect" as const,
|
|
scope: { channelAccountId: "account-1", deviceId: "device-1" },
|
|
binding: "binding-1",
|
|
},
|
|
status: 200,
|
|
body: validBody,
|
|
expected: { allowed: true as const, ...validBody },
|
|
},
|
|
{
|
|
input: {
|
|
connectionType: "plugin" as const,
|
|
operation: "connect" as const,
|
|
scope: { channelAccountId: "account-1", deviceId: "device-1" },
|
|
binding: "binding-1",
|
|
},
|
|
status: 403,
|
|
body: { code: "binding_revoked" },
|
|
expected: { allowed: false as const, code: "binding_revoked" as const },
|
|
},
|
|
{
|
|
input: {
|
|
connectionType: "plugin" as const,
|
|
operation: "connect" as const,
|
|
scope: { channelAccountId: "account-1", deviceId: "device-1" },
|
|
binding: "binding-1",
|
|
},
|
|
status: 401,
|
|
body: { code: "binding_revoked" },
|
|
expected: { allowed: false as const, code: "authorization_unavailable" as const },
|
|
},
|
|
{
|
|
input: {
|
|
connectionType: "mind_page" as const,
|
|
operation: "read" as const,
|
|
scope: { channelAccountId: "account-1" },
|
|
cookie: "mind_session=opaque",
|
|
},
|
|
status: 401,
|
|
body: { code: "auth_required" },
|
|
expected: { allowed: false as const, code: "auth_required" as const },
|
|
},
|
|
{
|
|
input: {
|
|
connectionType: "mind_page" as const,
|
|
operation: "read" as const,
|
|
scope: { channelAccountId: "account-1" },
|
|
cookie: "mind_session=opaque",
|
|
},
|
|
status: 403,
|
|
body: { code: "scope_mismatch" },
|
|
expected: { allowed: false as const, code: "scope_mismatch" as const },
|
|
},
|
|
{
|
|
input: {
|
|
connectionType: "mind_page" as const,
|
|
operation: "read" as const,
|
|
scope: { channelAccountId: "account-1" },
|
|
cookie: "mind_session=opaque",
|
|
},
|
|
status: 403,
|
|
body: { code: "binding_revoked" },
|
|
expected: { allowed: false as const, code: "authorization_unavailable" as const },
|
|
},
|
|
{
|
|
input: {
|
|
connectionType: "plugin" as const,
|
|
operation: "connect" as const,
|
|
scope: { channelAccountId: "account-1", deviceId: "device-1" },
|
|
binding: "binding-1",
|
|
},
|
|
status: 500,
|
|
body: { code: "binding_revoked" },
|
|
expected: { allowed: false as const, code: "authorization_unavailable" as const },
|
|
},
|
|
{
|
|
input: {
|
|
connectionType: "plugin" as const,
|
|
operation: "connect" as const,
|
|
scope: { channelAccountId: "account-1", deviceId: "device-1" },
|
|
binding: "binding-1",
|
|
},
|
|
status: 200,
|
|
body: { code: "auth_required" },
|
|
expected: { allowed: false as const, code: "authorization_unavailable" as const },
|
|
},
|
|
] as const;
|
|
|
|
for (const testCase of cases) {
|
|
let request: RequestInit | undefined;
|
|
const reader = createMindAuthorizationReader({
|
|
baseUrl: "https://mind.example.com",
|
|
timeoutMs: 100,
|
|
fetch: async (_input, init) => {
|
|
request = init;
|
|
return response(testCase.status, testCase.body);
|
|
},
|
|
});
|
|
const decision = await reader.authorize(testCase.input);
|
|
assert.deepEqual(decision, testCase.expected);
|
|
assert.equal(request?.method, "POST");
|
|
assert.equal(request?.redirect, "error");
|
|
assert.equal(
|
|
(request?.headers as Record<string, string>)?.["content-type"],
|
|
"application/json",
|
|
);
|
|
}
|
|
|
|
const nonJsonReader = createMindAuthorizationReader({
|
|
baseUrl: "https://mind.example.com",
|
|
timeoutMs: 100,
|
|
fetch: async () =>
|
|
({
|
|
status: 200,
|
|
json: async () => {
|
|
throw new SyntaxError("not json");
|
|
},
|
|
}) as unknown as Response,
|
|
});
|
|
assert.deepEqual(
|
|
await nonJsonReader.authorize({
|
|
connectionType: "plugin",
|
|
operation: "connect",
|
|
scope: { channelAccountId: "account-1", deviceId: "device-1" },
|
|
binding: "binding-1",
|
|
}),
|
|
{ allowed: false, code: "authorization_unavailable" },
|
|
);
|
|
|
|
const timeoutReader = createMindAuthorizationReader({
|
|
baseUrl: "https://mind.example.com",
|
|
timeoutMs: 1,
|
|
fetch: async (_input, init) =>
|
|
new Promise((_resolve, reject) => {
|
|
const timer = setTimeout(() => reject(new Error("timeout")), 100);
|
|
if (init?.signal?.aborted) {
|
|
clearTimeout(timer);
|
|
reject(new Error("aborted"));
|
|
return;
|
|
}
|
|
init?.signal?.addEventListener(
|
|
"abort",
|
|
() => {
|
|
clearTimeout(timer);
|
|
reject(new Error("aborted"));
|
|
},
|
|
{ once: true },
|
|
);
|
|
}),
|
|
});
|
|
assert.deepEqual(
|
|
await timeoutReader.authorize({
|
|
connectionType: "plugin",
|
|
operation: "connect",
|
|
scope: { channelAccountId: "account-1", deviceId: "device-1" },
|
|
binding: "binding-1",
|
|
}),
|
|
{ allowed: false, code: "authorization_unavailable" },
|
|
);
|
|
});
|
|
|
|
test("cutover pause is fail-closed and Bright v6 can resume", () => {
|
|
const policy = createOneTalkCutoverPolicy();
|
|
assert.equal(policy.canAdmit("bright-v6", "plugin", ONETALK_PROTOCOL_VERSION), true);
|
|
policy.pause();
|
|
assert.equal(policy.canAdmit("bright-v6", "plugin", ONETALK_PROTOCOL_VERSION), false);
|
|
assert.equal(policy.resume(), true);
|
|
policy.pause();
|
|
assert.equal(policy.resume(), true);
|
|
assert.equal(policy.snapshot().paused, false);
|
|
assert.equal(policy.canAdmit("bright-v6", "plugin", ONETALK_PROTOCOL_VERSION), true);
|
|
assert.equal(policy.canAdmit("bright-v6", "plugin", 5), false);
|
|
assert.equal(policy.canAdmit("legacy", "plugin", 3), false);
|
|
});
|