129 lines
5.5 KiB
JavaScript
129 lines
5.5 KiB
JavaScript
"use strict";
|
|
|
|
const assert = require("assert");
|
|
const { WebSocket } = require("ws");
|
|
const { parseClientMessage, sanitizeChat, sanitizeEvent, MAX_MESSAGE_BYTES } = require("../backend/protocol");
|
|
const { OverlayFeed, MAX_QUEUE, matchesSelectedSource } = require("../backend/feed");
|
|
const { isReadableTextChannel } = require("../backend/sources");
|
|
|
|
function run() {
|
|
verifyProtocol();
|
|
verifyCapabilityGate();
|
|
verifyFilteringAndNoBacklog();
|
|
verifyDiscordReadability();
|
|
verifyBoundedQueueAndDeduplication();
|
|
verifySanitization();
|
|
console.log("Lumi native overlay verification passed: protocol limits, server-side filtering, no backlog, reconnect-safe deduplication, bounded queues, and payload sanitization.");
|
|
}
|
|
|
|
function verifyDiscordReadability() {
|
|
const discord = { user: { id: "bot" } };
|
|
const channel = {
|
|
guild: { id: "guild" },
|
|
isTextBased: () => true,
|
|
permissionsFor: () => ({ has: (flag) => flag === 1024n })
|
|
};
|
|
assert.equal(isReadableTextChannel(channel, discord), true);
|
|
assert.equal(isReadableTextChannel({ ...channel, permissionsFor: () => ({ has: () => false }) }, discord), false);
|
|
assert.equal(isReadableTextChannel({ ...channel, isTextBased: () => false }, discord), false);
|
|
}
|
|
|
|
function verifyCapabilityGate() {
|
|
const feed = Object.create(OverlayFeed.prototype);
|
|
let response = "";
|
|
const socket = {
|
|
write(value) { response += value; },
|
|
destroy() {}
|
|
};
|
|
feed.devices = { authenticate: (_header, capability) => {
|
|
assert.equal(capability, "overlay.read.v1");
|
|
return { allowed: false, reason: "missing_credentials" };
|
|
} };
|
|
feed.upgrade({ headers: {}, socket: {} }, socket, Buffer.alloc(0));
|
|
assert.match(response, /^HTTP\/1.1 401/);
|
|
response = "";
|
|
feed.devices = { authenticate: () => ({ allowed: false, reason: "capability_revoked" }) };
|
|
feed.upgrade({ headers: { authorization: "redacted" }, socket: {} }, socket, Buffer.alloc(0));
|
|
assert.match(response, /^HTTP\/1.1 403/);
|
|
}
|
|
|
|
function verifyProtocol() {
|
|
assert.deepEqual(parseClientMessage(JSON.stringify({
|
|
type: "subscribe",
|
|
sources: ["twitch:lumi", "twitch:lumi", "discord:guild:channel"],
|
|
events: ["twitch.follow"]
|
|
})), {
|
|
type: "subscribe",
|
|
sources: ["twitch:lumi", "discord:guild:channel"],
|
|
events: ["twitch.follow"]
|
|
});
|
|
assert.throws(() => parseClientMessage(JSON.stringify({ type: "history" })), /Unsupported/);
|
|
assert.throws(() => parseClientMessage(Buffer.alloc(MAX_MESSAGE_BYTES + 1)), /too large/);
|
|
}
|
|
|
|
function verifyFilteringAndNoBacklog() {
|
|
const feed = Object.create(OverlayFeed.prototype);
|
|
feed.sessions = new Set();
|
|
feed.sequence = 0;
|
|
feed.publishedSeen = new Map();
|
|
feed.publishChat({ id: "before", platform: "twitch", channel: { name: "lumi" } });
|
|
const sent = [];
|
|
const session = sessionFor(sent, ["twitch:lumi"]);
|
|
feed.sessions.add(session);
|
|
assert.equal(sent.length, 0, "opening/subscribing must not replay a pre-connection message");
|
|
feed.publishChat({
|
|
id: "selected", platform: "twitch", text: "hello", timestamp: 1,
|
|
channel: { id: "1", name: "lumi", key: "lumi" },
|
|
author: { id: "viewer", name: "Viewer", badges: [] }, emotes: []
|
|
});
|
|
feed.publishChat({
|
|
id: "selected", platform: "twitch", text: "duplicate", timestamp: 3,
|
|
channel: { id: "1", name: "lumi", key: "lumi" },
|
|
author: { id: "viewer", name: "Viewer", badges: [] }, emotes: []
|
|
});
|
|
feed.publishChat({
|
|
id: "other", platform: "twitch", text: "hidden", timestamp: 2,
|
|
channel: { id: "2", name: "other", key: "other" },
|
|
author: { id: "viewer", name: "Viewer", badges: [] }, emotes: []
|
|
});
|
|
assert.equal(sent.length, 1, "provider duplicates must remain suppressed across feed sessions/reconnects");
|
|
assert.equal(JSON.parse(sent[0]).message.id, "selected");
|
|
assert.equal(matchesSelectedSource(new Set(["discord:guild:channel"]), "discord:guild:*"), true);
|
|
}
|
|
|
|
function verifyBoundedQueueAndDeduplication() {
|
|
const feed = Object.create(OverlayFeed.prototype);
|
|
feed.sequence = 0;
|
|
const session = sessionFor([], ["twitch:lumi"], WebSocket.CLOSED);
|
|
for (let index = 0; index < MAX_QUEUE + 40; index += 1)
|
|
feed.enqueue(session, "chat", { index }, `message:${index}`);
|
|
assert.equal(session.queue.length, MAX_QUEUE);
|
|
assert.equal(session.queue[0].index, 40, "overflow must deterministically drop the oldest queued item");
|
|
feed.enqueue(session, "chat", { index: 999 }, `message:${MAX_QUEUE + 39}`);
|
|
assert.equal(session.queue.length, MAX_QUEUE, "a duplicate event must not enter the queue twice");
|
|
}
|
|
|
|
function verifySanitization() {
|
|
const chat = sanitizeChat({
|
|
id: "id", platform: "twitch", text: "<script>plain text</script>",
|
|
channel: {}, author: { id: "user", name: "Viewer", avatar: "http://insecure.test/avatar.png" },
|
|
emotes: [{ start: 0, end: 1, image: "https://cdn.test/emote.png" }]
|
|
});
|
|
assert.equal(chat.text, "<script>plain text</script>", "content remains plain data and is not interpreted server-side");
|
|
assert.equal(chat.author.avatar, null);
|
|
assert.equal(chat.emotes[0].image, "https://cdn.test/emote.png");
|
|
const event = sanitizeEvent({ id: "event", type: "twitch.follow", payload: { user_name: "Viewer", access_token: "secret" } });
|
|
assert.equal(event.payload.access_token, undefined);
|
|
assert.equal(event.payload.user_name, "Viewer");
|
|
}
|
|
|
|
function sessionFor(sent, sources, state = WebSocket.OPEN) {
|
|
return {
|
|
socket: { readyState: state, send(value) { sent.push(value); } },
|
|
device: {}, sources: new Set(sources), events: new Set(), subscribed: true,
|
|
queue: [], draining: false, seen: new Map(), lastPong: Date.now()
|
|
};
|
|
}
|
|
|
|
run();
|