Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion dev/relay-broker-api.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,24 @@ test.each([undefined, "22222222-2222-4222-8222-222222222222"])(
?.delivery,
).toBe("accepted"),
);
const temporaryCreationId = owner.outbox.send({
kind: 9007,
content: "",
tags: [
["h", "33333333-3333-4333-8333-333333333333"],
["name", "Standup"],
["visibility", "open"],
["channel_type", "stream"],
["ttl", "604800"],
],
});
await vi.waitFor(() =>
expect(
owner.local
.snapshot()
.find((row) => row.event.id === temporaryCreationId)?.delivery,
).toBe("accepted"),
);
const invitationId = owner.outbox.send({
kind: 9000,
content: "",
Expand Down Expand Up @@ -1036,7 +1054,7 @@ test.each([undefined, "22222222-2222-4222-8222-222222222222"])(
).toBe("accepted"),
);
expect(h.publications.map((event) => event.kind)).toEqual([
9007, 9000, 9,
9007, 9007, 9000, 9,
]);
const denied = await h.post("sign", {
kind: 9050,
Expand All @@ -1052,3 +1070,29 @@ test.each([undefined, "22222222-2222-4222-8222-222222222222"])(
}
},
);

test.each(["sign", "publish"])(
"%s rejects truncated channel creation tags as a client error",
async (route) => {
const h = await harness(success, { channelCreation: true });
try {
const required = [
["h", "11111111-1111-4111-8111-111111111111"],
["name", "Work"],
["visibility", "open"],
["channel_type", "stream"],
];
for (let length = 0; length < required.length; length++) {
const response = await h.post(route, {
kind: 9007,
created_at: 1700000000,
content: "",
tags: required.slice(0, length),
});
expect(response.status).toBe(400);
}
} finally {
await h.close();
}
},
);
6 changes: 3 additions & 3 deletions dev/relay-broker.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { validSessionCommand } from "./session-commands.mjs";
import { validChannelCommand } from "./session-commands.mjs";
Comment thread
klopez4212 marked this conversation as resolved.
Comment thread
klopez4212 marked this conversation as resolved.
Comment thread
klopez4212 marked this conversation as resolved.
Comment thread
klopez4212 marked this conversation as resolved.
import { SocketRequestError } from "../src/features/relay/socket-requests.ts";
import {
validateWorkflowEvent,
Expand Down Expand Up @@ -1171,11 +1171,11 @@ export function relayBrokerPlugin({
const authority = await getAuthority(relay);
if (
!enrollment &&
!(authority.channelCreation && validSessionCommand(filters))
!(authority.channelCreation && validChannelCommand(filters))
)
return json(res, 400, {
error:
"Agent enrollment or session operation unavailable or invalid",
"Agent enrollment or channel operation unavailable or invalid",
sent: false,
});
} else if (![7, 9].includes(filters?.kind)) {
Expand Down
39 changes: 31 additions & 8 deletions dev/session-commands.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { sessionMetadata } from "../src/features/sessions/metadata.ts";
// Host signing allowlist. No arbitrary kinds, roles or metadata edits.
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
export function validSessionCommand(event) {
export function validChannelCommand(event) {
if (
!event ||
!Number.isSafeInteger(event.created_at) ||
Expand All @@ -25,18 +25,41 @@ export function validSessionCommand(event) {
const [h, p] = tags;
if (h?.length !== 2 || h[0] !== "h" || !uuid.test(h[1])) return false;
if (event.kind === 9007) {
const expected = ["h", "name", "visibility", "channel_type", "about"];
const expected = ["h", "name", "visibility", "channel_type"];
const optional = tags.slice(expected.length);
const optionalNames = optional.map(([name]) => name);
const validOptionalOrder = [[], ["about"], ["ttl"], ["about", "ttl"]].some(
(names) =>
names.length === optionalNames.length &&
names.every((name, index) => name === optionalNames[index]),
);
const about = optional.find(([name]) => name === "about");
const ttl = optional.find(([name]) => name === "ttl");
const aboutValue = about?.[1];
const ttlValue = ttl?.[1];
return (
event.content === "" &&
tags.length === expected.length &&
tags.every(
(tag, index) => tag.length === 2 && tag[0] === expected[index],
) &&
tags.length >= expected.length &&
validOptionalOrder &&
tags
.slice(0, expected.length)
.every(
(tag, index) => tag.length === 2 && tag[0] === expected[index],
) &&
!!tags[1][1].trim() &&
Comment thread
klopez4212 marked this conversation as resolved.
[...tags[1][1]].length <= 120 &&
tags[2][1] === "private" &&
["open", "private"].includes(tags[2][1]) &&
Comment thread
klopez4212 marked this conversation as resolved.
tags[3][1] === "stream" &&
sessionMetadata(tags[4][1]) !== undefined
(!about ||
(about.length === 2 &&
[...aboutValue].length <= 1000 &&
(sessionMetadata(aboutValue) !== undefined ||
!aboutValue.includes("Buzz session (")))) &&
(!ttl ||
(ttl.length === 2 &&
/^(?:[1-9]\d*)$/.test(ttlValue) &&
Number(ttlValue) <= 2_147_483_647 &&
sessionMetadata(aboutValue) === undefined))
);
}
if (event.kind === 9000)
Expand Down
130 changes: 113 additions & 17 deletions dev/session-commands.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, it } from "vitest";
import { validSessionCommand } from "./session-commands.mjs";
import { validChannelCommand } from "./session-commands.mjs";
const id = "11111111-1111-4111-8111-111111111111";
const event = (body, tags = [["h", id]]) => ({
kind: 9050,
Expand All @@ -8,11 +8,11 @@ const event = (body, tags = [["h", id]]) => ({
tags,
});
it("rejects custom session commands", () => {
expect(validSessionCommand(event({ action: "create", title: "Work" }))).toBe(
expect(validChannelCommand(event({ action: "create", title: "Work" }))).toBe(
false,
);
expect(
validSessionCommand(
validChannelCommand(
event({ action: "move", parent_id: id, confirm_history: true }),
),
).toBe(false);
Expand All @@ -27,17 +27,17 @@ it("allows only ordinary invitations, without extra roles", () => {
["p", "a".repeat(64)],
],
};
expect(validSessionCommand(invite)).toBe(true);
expect(validChannelCommand(invite)).toBe(true);
expect(
validSessionCommand({
validChannelCommand({
...invite,
tags: [...invite.tags, ["role", "admin"]],
}),
).toBe(false);
expect(validSessionCommand({ ...invite, kind: 9001 })).toBe(false);
expect(validChannelCommand({ ...invite, kind: 9001 })).toBe(false);
});

it("allows only private stream creation marked for Sessions", () => {
it("preserves strict Sessions metadata while allowing ordinary stream visibility", () => {
const create = {
kind: 9007,
created_at: 1,
Expand All @@ -50,31 +50,29 @@ it("allows only private stream creation marked for Sessions", () => {
["about", "Buzz session (buzz.sessions/v1)"],
],
};
expect(validSessionCommand(create)).toBe(true);
expect(validChannelCommand(create)).toBe(true);
const child = (description) => ({
...create,
tags: create.tags.map((tag) =>
tag[0] === "about" ? ["about", description] : tag,
),
});
expect(
validSessionCommand(child(`Buzz session (buzz.sessions/v1)\nparent:${id}`)),
validChannelCommand(child(`Buzz session (buzz.sessions/v1)\nparent:${id}`)),
).toBe(true);
for (const parent of ["invalid", `${id}\nrole:owner`, `${id}extra`]) {
expect(
validSessionCommand(
validChannelCommand(
child(`Buzz session (buzz.sessions/v1)\nparent:${parent}`),
),
).toBe(false);
}
for (const [index, value] of [
[2, "open"],
[3, "dm"],
[4, "arbitrary"],
[1, " "],
]) {
expect(
validSessionCommand({
validChannelCommand({
...create,
tags: create.tags.map((tag, i) =>
i === index ? [tag[0], value] : tag,
Expand All @@ -83,25 +81,123 @@ it("allows only private stream creation marked for Sessions", () => {
).toBe(false);
}
expect(
validSessionCommand({
validChannelCommand({
...create,
tags: create.tags.map((tag, i) =>
i === 4 ? [tag[0], "Buzz session (forged)"] : tag,
),
}),
).toBe(false);
expect(
validChannelCommand({
...create,
tags: [...create.tags, ["p", "a".repeat(64)]],
}),
).toBe(false);
expect(validSessionCommand({ ...create, content: "extra" })).toBe(false);
expect(validChannelCommand({ ...create, content: "extra" })).toBe(false);
});

it("accepts one trailing outbox identifier for invitations and rejects ambiguous envelopes", () => {
const invite = (tags) => ({ kind: 9000, created_at: 1, content: "", tags });
const h = ["h", id],
p = ["p", "a".repeat(64)],
client = ["client-id", id];
expect(validSessionCommand(invite([h, p, client]))).toBe(true);
expect(validChannelCommand(invite([h, p, client]))).toBe(true);
for (const tags of [
[h, p, client, client],
[client, h, p],
[h, p, ["client-id", "invalid"]],
[h, p, [...client, "extra"]],
])
expect(validSessionCommand(invite(tags))).toBe(false);
expect(validChannelCommand(invite(tags))).toBe(false);
});

it("allows bounded ordinary stream creation without a Sessions marker", () => {
const create = {
kind: 9007,
created_at: 1,
content: "",
tags: [
["h", id],
["name", "Release notes"],
["visibility", "open"],
["channel_type", "stream"],
["about", "Updates for the team"],
],
};
expect(validChannelCommand(create)).toBe(true);
expect(
validChannelCommand({ ...create, tags: create.tags.slice(0, 4) }),
).toBe(true);
expect(
validChannelCommand({
...create,
tags: create.tags.map((tag) =>
tag[0] === "about" ? ["about", "x".repeat(1001)] : tag,
),
}),
).toBe(false);
expect(
validChannelCommand({
...create,
tags: create.tags.map((tag) =>
tag[0] === "about" ? ["about", "Buzz session (forged)"] : tag,
),
}),
).toBe(false);
expect(
validChannelCommand({
...create,
tags: [...create.tags, ["ttl", "604800"]],
}),
).toBe(true);
expect(
validChannelCommand({
...create,
tags: [...create.tags.slice(0, 4), ["ttl", "604800"]],
}),
).toBe(true);
for (const ttl of ["0", "-1", "forever", "2147483648"]) {
expect(
validChannelCommand({
...create,
tags: [...create.tags, ["ttl", ttl]],
}),
).toBe(false);
}
});

it("rejects every truncated mandatory creation-tag prefix", () => {
const create = {
kind: 9007,
created_at: 1,
content: "",
tags: [
["h", id],
["name", "Release notes"],
["visibility", "open"],
["channel_type", "stream"],
],
};
for (let length = 0; length < create.tags.length; length++)
expect(
validChannelCommand({ ...create, tags: create.tags.slice(0, length) }),
).toBe(false);
});

it("does not add temporary cleanup to Sessions metadata", () => {
const create = {
kind: 9007,
created_at: 1,
content: "",
tags: [
["h", id],
["name", "Work"],
["visibility", "private"],
["channel_type", "stream"],
["about", "Buzz session (buzz.sessions/v1)"],
["ttl", "604800"],
],
};
expect(validChannelCommand(create)).toBe(false);
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/bundled/channels/ChannelSidebarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import type { ChannelSummary } from "../../features/relay/contracts";
import type { RelaySession } from "../../features/relay/session";
import {
ChatCircleIcon,
HashIcon,
UsersIcon,
} from "../../shared/design-system/icons/index";
import { channelIcon } from "../../features/channels/channel-icon";
import { ChannelActivityPopover } from "./ChannelActivityPopover";
import { ChannelSidebarRow } from "./ChannelSidebarRow";
import { UnreadBadge } from "./UnreadBadge";
Expand Down Expand Up @@ -49,7 +49,7 @@ export const ChannelSidebarItem = memo(function ChannelSidebarItem({
? (channel.participants?.length ?? 0) > 1
? UsersIcon
: ChatCircleIcon
: HashIcon;
: channelIcon(channel);
return (
<ChannelSidebarRow
channel={channel}
Expand Down
5 changes: 4 additions & 1 deletion src/bundled/channels/ChannelSidebarRow.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
opacity: 0;
pointer-events: none;
flex-shrink: 0;
margin-right: var(--space-chip-inset);
}
.row:is(:hover, :has(:focus-visible)) .more,
.row .more:has([data-popup-open]) {
Expand Down Expand Up @@ -93,3 +92,7 @@
color: var(--text-standard);
font-weight: var(--type-weight-medium);
}

.sessions.sessions [data-buzz-ui][data-variant="row"] {
border-radius: var(--radius-pill);
}
1 change: 1 addition & 0 deletions src/bundled/channels/ChannelSidebarRow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ it("opens a compact action menu independently of selecting its channel", async (
const trigger = screen.getByRole("button", {
name: "More options for Engineering",
});
expect(trigger).toHaveAttribute("data-icon-shape", "round");
await user.click(trigger);
await user.click(
await screen.findByRole("menuitem", { name: "New session" }),
Expand Down
Loading
Loading