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
1 change: 1 addition & 0 deletions dev/relay-broker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ export function relayBrokerPlugin({
...((await getAuthority(relay)).channelCreation ? [9007] : []),
],
workflowReads: true,
attachmentUploads: true,
sidebarPreferences: true,
readState: true,
agentLibrary: true,
Expand Down
297 changes: 297 additions & 0 deletions src/features/relay/attachments.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import { assert, afterEach, expect, it, vi } from "vitest";
import {
attachmentMessage,
brokerUpload,
UPLOAD_MAX_BYTES,
validateUploadResult,
} from "./attachments";
import { foldMessages, parseAttachments } from "./fold";

const origin = "https://relay.test";
const hash = "a".repeat(64);
const descriptor = {
url: `${origin}/media/${hash}.pdf`,
type: "application/pdf",
size: 3,
sha256: hash,
};
const file = new File([new Uint8Array([0, 128, 255])], "report.pdf", {
type: "application/pdf",
});
afterEach(() => vi.unstubAllGlobals());

it("uploads exact bytes through the selected broker and returns frozen named metadata", async () => {
const fetcher = vi.fn(async (_url, options) => {
expect(new Uint8Array(await options.body.arrayBuffer())).toEqual(
new Uint8Array([0, 128, 255]),
);
return Response.json(descriptor);
});
vi.stubGlobal("fetch", fetcher);
const result = await brokerUpload("/api/relay/selected", origin)(
file,
new AbortController().signal,
);
expect(fetcher).toHaveBeenCalledWith(
"/api/relay/selected/upload",
expect.objectContaining({
method: "POST",
credentials: "same-origin",
body: file,
}),
);
expect(result).toEqual({ ...descriptor, name: "report.pdf" });
expect(Object.isFrozen(result)).toBe(true);
});

it.each([
{ url: `https://other.test/media/${hash}.pdf` },
{ url: `${origin}/media/${hash}.pdf?token=x` },
{ url: `${origin}/media/${hash}.pdf#fragment` },
{ url: `https://user@relay.test/media/${hash}.pdf` },
{ url: `${origin}/media/${"b".repeat(64)}.pdf` },
{ url: `${origin}/other/${hash}.pdf` },
{ type: "text/html; extra" },
{ size: 4 },
{ sha256: "invalid" },
])("rejects an invalid descriptor independently: %j", (patch) => {
expect(() =>
validateUploadResult({ ...descriptor, ...patch }, origin, 3, "file"),
).toThrow(/invalid upload/);
});

it.each([0, UPLOAD_MAX_BYTES + 1])(
"rejects size %s before network work",
async (size) => {
const fetcher = vi.fn();
vi.stubGlobal("fetch", fetcher);
await expect(
brokerUpload("/api/relay", origin)(
new File([new Uint8Array(size)], "file"),
new AbortController().signal,
),
).rejects.toMatchObject({ code: "size" });
expect(fetcher).not.toHaveBeenCalled();
},
);

it.each(["metadata", "denied", "capacity", "rejected", "size", "failed"])(
"preserves broker failure %s",
async (code) => {
vi.stubGlobal("fetch", async () =>
Response.json({ code }, { status: 400 }),
);
await expect(
brokerUpload("/api/relay", origin)(file, new AbortController().signal),
).rejects.toMatchObject({ code });
},
);

it("rejects oversized/malformed bodies and cancels response consumption", async () => {
const cancel = vi.fn();
vi.stubGlobal(
"fetch",
async () =>
new Response(
new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(8193));
},
cancel,
}),
),
);
await expect(
brokerUpload("/api/relay", origin)(file, new AbortController().signal),
).rejects.toMatchObject({ code: "invalid" });
expect(cancel).toHaveBeenCalledTimes(1);
vi.stubGlobal("fetch", async () => new Response("not json"));
await expect(
brokerUpload("/api/relay", origin)(file, new AbortController().signal),
).rejects.toMatchObject({ code: "invalid" });
});

it("passes cancellation to fetch and rejects late completion", async () => {
const controller = new AbortController();
vi.stubGlobal(
"fetch",
async (_url: RequestInfo | URL, init?: RequestInit) => {
assert.exists(init?.signal);
controller.abort();
expect(init.signal.aborted).toBe(true);
return Response.json(descriptor);
},
);
await expect(
brokerUpload("/api/relay", origin)(file, controller.signal),
).rejects.toMatchObject({ name: "AbortError" });
const fetcher = vi.fn();
vi.stubGlobal("fetch", fetcher);
await expect(
brokerUpload("/api/relay", origin)(file, controller.signal),
).rejects.toMatchObject({ name: "AbortError" });
expect(fetcher).not.toHaveBeenCalled();
});

it("builds escaped message links and metadata understood by the existing receiver", () => {
const uploaded = { ...descriptor, name: "[report](x)\n!.pdf" };
const result = attachmentMessage(" hello ", [uploaded], origin);
expect(result.content).toBe(
`hello\n\n[\\[report\\]\\(x\\) \\!.pdf](<${descriptor.url}>)`,
);
expect(result.tags).toEqual([
[
"imeta",
`url ${descriptor.url}`,
"m application/pdf",
"size 3",
`x ${hash}`,
"filename [report](x) !.pdf",
],
]);
const received = parseAttachments(
{ id: "", pubkey: "", kind: 9, created_at: 1, ...result },
[],
);
expect(received).toMatchObject([
{
kind: "file",
name: "[report](x) !.pdf",
size: 3,
mime: "application/pdf",
},
]);
expect(() =>
attachmentMessage("", [uploaded], "https://different.test"),
).toThrow();
});

it.each(["report &copy;.pdf", "report &#65;.pdf", "report &#x41;.pdf"])(
"preserves the literal filename %s through message projection",
(name) => {
const message = attachmentMessage(
"hello",
[{ ...descriptor, name }],
origin,
);
const [received] = foldMessages("c", "d".repeat(64), [
{
id: "b".repeat(64),
pubkey: "c".repeat(64),
created_at: 1,
kind: 9,
content: message.content,
tags: [["h", "c"], ...message.tags],
},
]);
expect(received?.content).toBe("hello");
expect(received?.attachments).toEqual([
{
url: descriptor.url,
kind: "file",
mime: descriptor.type,
size: 3,
name,
},
]);
},
);

it("keeps ordinary text unchanged and marks prepared image/video links as media", () => {
expect(attachmentMessage(" hello ", [])).toEqual({
content: "hello",
tags: [],
});
for (const type of ["image/png", "video/mp4"]) {
const result = attachmentMessage(
"",
[{ ...descriptor, name: "media", type }],
origin,
);
expect(result.content).toBe(`![media](<${descriptor.url}>)`);
}
});

it.each([
[401, "denied"],
[403, "denied"],
[413, "size"],
[429, "capacity"],
])(
"preserves HTTP %s without requiring an error body",
async (status, code) => {
vi.stubGlobal(
"fetch",
async () => new Response(null, { status: Number(status) }),
);
await expect(
brokerUpload("/api/relay", origin)(file, new AbortController().signal),
).rejects.toMatchObject({ code });
},
);

it("bounds a stalled request and rejects its late result after the upload deadline", async () => {
vi.useFakeTimers();
const deadline = new AbortController();
const timeout = vi
.spyOn(AbortSignal, "timeout")
.mockReturnValue(deadline.signal);
let release!: (response: Response) => void;
let signal!: AbortSignal;
vi.stubGlobal("fetch", (_url: RequestInfo | URL, init?: RequestInit) => {
assert.exists(init?.signal);
signal = init.signal;
return new Promise((resolve) => {
release = resolve;
});
});
try {
const pending = brokerUpload("/api/relay", origin)(
file,
new AbortController().signal,
);
const rejected = expect(pending).rejects.toMatchObject({
name: "TimeoutError",
});
expect(timeout).toHaveBeenCalledWith(120_000);
deadline.abort(new DOMException("Upload deadline", "TimeoutError"));
expect(signal.aborted).toBe(true);
release(Response.json(descriptor));
await rejected;
} finally {
timeout.mockRestore();
vi.useRealTimers();
}
});

it("keeps filename metadata within the relay basename and UTF-8 byte rules", () => {
for (const name of [
"folder/name\\other\u0085.pdf",
"📎".repeat(100),
" /\\\u0085 ",
]) {
const result = attachmentMessage("", [{ ...descriptor, name }], origin);
const field = result.tags[0]?.find((item) => item.startsWith("filename "));
assert.exists(field);
const value = field.slice("filename ".length);
expect(new TextEncoder().encode(value).length).toBeLessThanOrEqual(255);
expect(value.length).toBeGreaterThan(0);
expect(value).not.toContain("/");
expect(value).not.toContain("\\");
expect(value).not.toContain("\u0085");
}
});

it("removes deceptive bidi controls from both metadata and the sent Markdown label", () => {
const controls =
"\u200e\u200f\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069";
const result = attachmentMessage(
"",
[{ ...descriptor, name: `report${controls}.pdf` }],
origin,
);
for (const char of controls) {
expect(result.content).not.toContain(char);
expect(result.tags.flat().join(" ")).not.toContain(char);
}
});
Loading
Loading