Files

600 lines
21 KiB
JavaScript

// 验证 OneTalk WebSocket observer 只输出 normalized 消息与安全诊断
import assert from "node:assert/strict";
import test from "node:test";
import { installOneTalkMessageObserver } from "../src/onetalk/main-page/message-observer/entry.ts";
import { parseHistoryMessages } from "../src/onetalk/main-page/message-observer/history.ts";
import { createParsedMessageBatch } from "../src/onetalk/main-page/message-observer/model.ts";
import { createSendObservationCorrelator } from "../src/onetalk/main-page/message-observer/send-observation.ts";
const SELF_ACCOUNT_ID = "286995452";
const SELF_PARTICIPANT = "2500002169502@icbu";
const CONTACT_PARTICIPANT = "2208314000798@icbu";
class FakeWebSocket extends EventTarget {
static OPEN = 1;
constructor(url) {
super();
this.url = String(url);
}
receive(data) {
this.dispatchEvent(new MessageEvent("message", { data }));
}
}
const testPageWindow = () => {
const logs = [];
return {
logs,
pageWindow: {
WebSocket: FakeWebSocket,
currentUserAccountId: SELF_ACCOUNT_ID,
__conversationListFullData__: [
{ owner: { accountId: SELF_ACCOUNT_ID, aliId: "2500002169502" } },
],
console: { log: (...args) => logs.push(args) },
},
};
};
const rawTextMessage = (messageId = "message-1", text = "hello") => ({
type: 1,
messageId,
cid: "2208314000798-2500002169502#11011@icbu",
createAt: 1_787_649_815_828,
content: { contentType: 1, text: { content: text, extension: { ignored: true } } },
sender: { uid: CONTACT_PARTICIPANT },
unreadCount: 0,
});
const liveFrame = (message, pair = [CONTACT_PARTICIPANT, SELF_PARTICIPANT]) =>
JSON.stringify({
code: 200,
body: [
{
singleChatUserConversation: {
lastMessage: { message, readStatus: 2, msgStatus: 1 },
singleChatConversation: {
pairFirst: pair[0],
pairSecond: pair[1],
},
},
},
],
});
const historyFrame = (message) =>
JSON.stringify({
code: 200,
body: { userMessageModels: [{ message, readStatus: 2, msgStatus: 1 }] },
});
const flatHistoryItem = (message, unread = 2, status = 1) => {
const { cid, createAt, content, sender, unreadCount: _unreadCount, ...flatMessage } = message;
return {
...flatMessage,
conversationCode: cid,
sendTime: createAt,
msgType: 101,
type: 1,
subType: 1,
messageType: "rec",
status,
unread,
sender: { targetId: sender.uid.split("@")[0] },
content: "SDK display text is not canonical history content",
originalData: { text: content.text.content },
};
};
const flatHistoryMediaItem = (messageId, msgType, subType, originalData) => ({
type: 1,
messageId,
conversationCode: "2208314000798-2500002169502#11011@icbu",
sendTime: 1_787_649_815_828,
msgType,
subType,
messageType: "rec",
viewType: 0,
status: 1,
unread: 2,
sender: { targetId: "2208314000798" },
content: "SDK display media is not canonical history content",
originalData,
});
const observe = (frame) => {
const { logs, pageWindow } = testPageWindow();
const batches = [];
installOneTalkMessageObserver(pageWindow, (batch) => batches.push(batch));
new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com/").receive(frame);
return { batches, logs };
};
test("normalizes text before the sink and does not log raw frames or bodies", () => {
const { batches, logs } = observe(liveFrame(rawTextMessage("message-1", "sensitive body")));
assert.deepEqual(batches, [
{
messages: [
{
messageType: "new",
upstreamType: 1,
messageId: "message-1",
conversationId: "2208314000798-2500002169502#11011@icbu",
senderId: CONTACT_PARTICIPANT,
direction: "received",
sentAtMs: 1_787_649_815_828,
content: { version: 1, kind: "text", text: "sensitive body" },
participantIds: [CONTACT_PARTICIPANT, SELF_PARTICIPANT],
readStatus: 2,
messageStatus: 1,
unreadCount: 0,
},
],
diagnostics: { unsupportedSkippedCount: 0, invalidObservationCount: 0, anomalies: [] },
},
]);
assert.deepEqual(logs, []);
assert.equal(JSON.stringify(batches).includes("extension"), false);
});
test("uses the shared raw type gate for every known platform subtype and direction", () => {
const { pageWindow } = testPageWindow();
for (const [subType, sender] of [
[9999, CONTACT_PARTICIPANT],
[9999, SELF_PARTICIPANT],
[15, CONTACT_PARTICIPANT],
[15, SELF_PARTICIPANT],
[10, CONTACT_PARTICIPANT],
[10, SELF_PARTICIPANT],
[18, CONTACT_PARTICIPANT],
[18, SELF_PARTICIPANT],
]) {
const platformMessage = {
...rawTextMessage(`platform-${subType}-${sender}`),
type: 2,
subType,
sender: { uid: sender },
};
const live = observe(liveFrame(platformMessage));
assert.deepEqual(live.batches, [
{
messages: [],
diagnostics: {
unsupportedSkippedCount: 1,
invalidObservationCount: 0,
anomalies: [],
},
},
]);
const flatPlatformMessage = {
...flatHistoryItem(platformMessage),
type: 2,
subType,
sender: { targetId: sender.split("@", 1)[0] },
};
const history = createParsedMessageBatch(
parseHistoryMessages([flatPlatformMessage], pageWindow),
);
assert.deepEqual(history, {
messages: [],
diagnostics: {
unsupportedSkippedCount: 1,
invalidObservationCount: 0,
anomalies: [],
},
});
}
});
test("rejects explicit unknown live message types while preserving type-one buyer and AI replies", () => {
for (const type of [null, 3]) {
const unknownMessage = { ...rawTextMessage(`unknown-${String(type)}`), type };
if (type === undefined) delete unknownMessage.type;
const { batches } = observe(liveFrame(unknownMessage));
assert.deepEqual(batches, [
{
messages: [],
diagnostics: {
unsupportedSkippedCount: 0,
invalidObservationCount: 1,
anomalies: [],
},
},
]);
}
const buyer = observe(liveFrame(rawTextMessage("buyer-business", "buyer inquiry"))).batches[0];
const aiReply = observe(
liveFrame({
...rawTextMessage("ai-reply", "AI Auto Reception reply"),
sender: { uid: SELF_PARTICIPANT },
}),
).batches[0];
assert.equal(buyer.messages[0].upstreamType, 1);
assert.equal(buyer.messages[0].direction, "received");
assert.equal(aiReply.messages[0].upstreamType, 1);
assert.equal(aiReply.messages[0].direction, "sent");
});
test("production live messages omit message.type and still reach send confirmation", async () => {
const { pageWindow } = testPageWindow();
const correlator = createSendObservationCorrelator(1000);
const batches = [];
installOneTalkMessageObserver(pageWindow, (batch) => {
batches.push(batch);
correlator.observe(batch.messages);
});
const socket = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com/");
const message = {
...rawTextMessage("4292879038391.PNM", "live regression"),
createAt: Date.now(),
sender: { uid: SELF_PARTICIPANT },
};
delete message.type;
let sends = 0;
const result = correlator.execute(message.cid, "live regression", undefined, () => {
sends += 1;
return { messageId: message.messageId };
});
await new Promise((resolve) => setImmediate(resolve));
const frame = JSON.parse(liveFrame(message));
frame.body[0].type = 1; // Conversation discriminator, not message classification.
socket.receive(JSON.stringify(frame));
assert.equal(batches[0].messages[0].messageId, "4292879038391");
assert.equal(batches[0].messages[0].direction, "sent");
assert.equal((await result).status, "confirmed_sent");
assert.equal(sends, 1);
const inbound = {
...message,
messageId: "4292879038392.PNM",
sender: { uid: CONTACT_PARTICIPANT },
};
socket.receive(liveFrame(inbound));
assert.equal(batches[1].messages[0].direction, "received");
const history = flatHistoryItem(message);
delete history.type;
const invalidHistory = createParsedMessageBatch(parseHistoryMessages([history], pageWindow));
assert.equal(invalidHistory.messages.length, 0);
assert.equal(invalidHistory.diagnostics.invalidObservationCount, 1);
});
test("does not log OneTalk heartbeat responses", () => {
const { batches, logs } = observe(
JSON.stringify({
headers: { mid: "heartbeat-1", "server-timestamp": "1700000000000" },
code: 200,
}),
);
assert.deepEqual(batches, []);
assert.deepEqual(logs, []);
});
test("does not emit raw WebSocket history responses that the SDK adapter owns", () => {
const { batches } = observe(historyFrame(rawTextMessage("same-message")));
assert.deepEqual(batches, []);
});
test("canonicalizes OneTalk decimal .PNM aliases across live and flat history", () => {
const canonicalId = "4012345670429";
const live = observe(liveFrame(rawTextMessage(`${canonicalId}.PNM`))).batches[0].messages[0];
const { pageWindow } = testPageWindow();
const history = createParsedMessageBatch(
parseHistoryMessages(
[flatHistoryItem(rawTextMessage(Number(canonicalId), "hello"))],
pageWindow,
),
).messages[0];
assert.equal(live.messageId, canonicalId);
assert.equal(history.messageId, canonicalId);
assert.equal(
observe(liveFrame(rawTextMessage("message-1.PNM"))).batches[0].messages[0].messageId,
"message-1.PNM",
);
assert.equal(
observe(liveFrame(rawTextMessage(`${canonicalId}.PNM.extra`))).batches[0].messages[0]
.messageId,
`${canonicalId}.PNM.extra`,
);
});
test("adapts flat SDK history items through the existing history decoder", () => {
const { pageWindow } = testPageWindow();
const orderDraft = {
...flatHistoryItem(rawTextMessage("order-draft", "Order draft notification")),
localizedUiText: "订单草稿通知",
dataOriginal: "not-json",
};
const batch = createParsedMessageBatch(
parseHistoryMessages(
[flatHistoryItem(rawTextMessage("buyer-text", "buyer canonical body")), orderDraft],
pageWindow,
),
);
assert.deepEqual(
batch.messages.map(({ messageId, messageType, sentAtMs, content }) => ({
messageId,
messageType,
sentAtMs,
content,
})),
[
{
messageId: "buyer-text",
messageType: "history",
sentAtMs: 1_787_649_815_828,
content: { version: 1, kind: "text", text: "buyer canonical body" },
},
{
messageId: "order-draft",
messageType: "history",
sentAtMs: 1_787_649_815_828,
content: { version: 1, kind: "text", text: "Order draft notification" },
},
],
);
assert.deepEqual(batch.diagnostics, {
unsupportedSkippedCount: 0,
invalidObservationCount: 0,
anomalies: [],
});
assert.equal(JSON.stringify(batch).includes("订单草稿通知"), false);
assert.equal(JSON.stringify(batch).includes("not-json"), false);
});
test("adapts exact SDK flat history images and attachments through the sole history entry", () => {
const { pageWindow } = testPageWindow();
const redirect = (action, id) =>
`https://clouddisk.alibaba.com/file/redirectFileUrl.htm?appkey=onetalk&fileAction=${action}&id=${id}&parentId=parent-1&scene=im&secOperateAliId=operation-1`;
const batch = createParsedMessageBatch(
parseHistoryMessages(
[
flatHistoryMediaItem("history-image", 102, 60, {
fileId: "history-image",
suffix: "JPG",
size: 263_333,
isOriginal: 1,
md5: "f28f1f8f4b760d5e2a89c3f0f83f3f68",
url: redirect("imagePreview", "history-image"),
}),
flatHistoryMediaItem("history-file", 10010, 61, {
cardType: 12,
params: {
extensionType: "PDF",
id: "history-file",
parentId: "parent-1",
md5: "a614ee55b22a6545c2bc7342898c6a6f",
name: "quotation.pdf",
size: "8192",
url: redirect("officePreview", "history-file"),
thumbnailUrl:
"https://clouddisk.alibaba.com/file/videoThumb.htm?appkey=onetalk&id=history-file&parentId=parent-1&scene=im&secOperateAliId=operation-1",
downloadUrl: "",
},
}),
],
pageWindow,
),
);
assert.deepEqual(
batch.messages.map(({ messageId, messageType, upstreamType, content }) => ({
messageId,
messageType,
upstreamType,
contentKind: content.kind,
})),
[
{
messageId: "history-image",
messageType: "history",
upstreamType: 1,
contentKind: "image",
},
{
messageId: "history-file",
messageType: "history",
upstreamType: 1,
contentKind: "file",
},
],
);
assert.deepEqual(batch.diagnostics, {
unsupportedSkippedCount: 0,
invalidObservationCount: 0,
anomalies: [],
});
});
test("adapts only approved structured card projections through the sole history entry", () => {
const { pageWindow } = testPageWindow();
const orderSummary = Buffer.from(
JSON.stringify({
orderAmount: 12.5,
orderAmountCurrency: "USD",
paymentAmount: 10,
paymentAmountCurrency: "USD",
statusMessageKey: "order.pending_payment",
actionList: [
{ name: "pay", messageKey: "order.pay", properties: { payStep: "deposit" } },
],
}),
"utf8",
).toString("base64");
const batch = createParsedMessageBatch(
parseHistoryMessages(
[
{
...flatHistoryMediaItem("history-business-card", 10010, 57, {
cardType: 1,
params: { sign: "secret-sign" },
}),
contact: {
name: "Buyer Name",
companyName: "Buyer Company",
complianceCountryCode: "CN",
fullPortrait: "https://cdn.example.test/avatar/buyer.jpg",
accountIdEncrypt: "secret-account",
},
},
flatHistoryMediaItem("history-inquiry", 10010, 50, { cardType: 6 }),
flatHistoryMediaItem("history-order", 10010, 59, {
cardType: 9,
params: {
orderId: "order-1",
bizCode: 42,
contractId: "contract-1",
id: "id-1",
tenant: "tenant-1",
sign: "secret-sign",
params: orderSummary,
},
}),
],
pageWindow,
),
);
assert.deepEqual(
batch.messages.map((message) => [message.messageId, message.content.kind]),
[
["history-business-card", "business_card"],
["history-inquiry", "inquiry"],
["history-order", "order"],
],
);
assert.deepEqual(batch.messages[0]?.content, { version: 1, kind: "business_card" });
assert.equal(JSON.stringify(batch).includes("secret-sign"), false);
assert.equal(JSON.stringify(batch).includes("secret-account"), false);
assert.equal(JSON.stringify(batch).includes(orderSummary), false);
assert.equal(JSON.stringify(batch).includes("Buyer Name"), false);
assert.equal(JSON.stringify(batch).includes("buyer.jpg"), false);
});
test("isolates invalid flat history items without dropping valid siblings", () => {
const { pageWindow } = testPageWindow();
const valid = flatHistoryItem(rawTextMessage("valid-flat"));
const missingConversation = flatHistoryItem(rawTextMessage("missing-conversation"));
delete missingConversation.conversationCode;
const batch = createParsedMessageBatch(
parseHistoryMessages([valid, missingConversation, null], pageWindow),
);
assert.deepEqual(
batch.messages.map((message) => message.messageId),
["valid-flat"],
);
assert.deepEqual(batch.diagnostics, {
unsupportedSkippedCount: 0,
invalidObservationCount: 2,
anomalies: [],
});
});
test("canonicalizes a numeric flat messageId before the bridge contract", () => {
const { pageWindow } = testPageWindow();
const batch = createParsedMessageBatch(
parseHistoryMessages([flatHistoryItem(rawTextMessage(123456, "numeric flat"))], pageWindow),
);
assert.equal(batch.messages[0].messageId, "123456");
assert.equal(batch.messages[0].messageType, "history");
});
test("preserves participant order while resolving sent and received direction", () => {
for (const pair of [
[SELF_PARTICIPANT, CONTACT_PARTICIPANT],
[CONTACT_PARTICIPANT, SELF_PARTICIPANT],
]) {
const sent = observe(
liveFrame({ ...rawTextMessage("sent"), sender: { uid: SELF_PARTICIPANT } }, pair),
).batches[0].messages[0];
const received = observe(
liveFrame(
{ ...rawTextMessage("received"), sender: { uid: CONTACT_PARTICIPANT } },
pair,
),
).batches[0].messages[0];
assert.deepEqual(sent.participantIds, pair);
assert.equal(sent.direction, "sent");
assert.deepEqual(received.participantIds, pair);
assert.equal(received.direction, "received");
}
});
test("reports media failures through a deduplicated safe diagnostic without raw leakage", () => {
const raw = rawTextMessage("bad-media", "unused");
raw.content = { contentType: 101, custom: { type: 7, data: "not-base64-secret" } };
const { batches, logs } = observe(liveFrame(raw));
assert.deepEqual(batches[0], {
messages: [],
diagnostics: {
unsupportedSkippedCount: 0,
invalidObservationCount: 0,
anomalies: [{ code: "media_invalid_base64", mediaKind: "image", count: 1 }],
},
});
assert.equal(JSON.stringify(batches).includes("not-base64-secret"), false);
assert.equal(JSON.stringify(logs).includes("not-base64-secret"), false);
});
test("emits an invalid_observation diagnostic without forwarding the invalid message", () => {
const invalid = {
...rawTextMessage("invalid-message-id", "discarded body"),
sender: { uid: "outside@icbu" },
};
const { batches, logs } = observe(liveFrame(invalid));
assert.deepEqual(batches, [
{
messages: [],
diagnostics: {
unsupportedSkippedCount: 0,
invalidObservationCount: 1,
anomalies: [],
},
},
]);
assert.deepEqual(logs, [
["[Trade Message Center][OneTalk WebSocket]", { event: "invalid_observation" }],
]);
assert.equal(JSON.stringify(batches).includes("invalid-message-id"), false);
assert.equal(JSON.stringify(logs).includes("discarded body"), false);
});
test("does not treat MessagePack sync push as ordinary live media support", () => {
const { batches, logs } = observe(
JSON.stringify({
lwp: "/s/sync",
body: { syncPushPackage: { data: [{ data: "encoded" }] } },
}),
);
assert.deepEqual(batches, []);
assert.deepEqual(logs, []);
});
test("ignores other WebSocket hosts and installs the tap only once", () => {
const { logs, pageWindow } = testPageWindow();
installOneTalkMessageObserver(pageWindow);
const installed = pageWindow.WebSocket;
installOneTalkMessageObserver(pageWindow);
new pageWindow.WebSocket("wss://example.com/").receive(liveFrame(rawTextMessage()));
assert.equal(pageWindow.WebSocket, installed);
assert.deepEqual(logs, []);
});