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
4 changes: 3 additions & 1 deletion internal/api/src/routes/files.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ export default function mountFiles(server: APIServer) {
// Note: this happens with createServer from node:http, not with Bun.serve.
return c.body(file.stream, 200, {
"Content-Type": file.type,
// Inline to prevent the browser from downloading the file.
"Content-Disposition": `inline; filename="${file.name}"`,
"X-Content-Type-Options": "nosniff",
"Content-Security-Policy":
"default-src 'none'; sandbox; frame-ancestors 'none'",
});
});
}
26 changes: 25 additions & 1 deletion internal/api/src/routes/files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { serve } from "../test";

test("POST+GET /api/files", async () => {
const { helpers } = await serve();
const { client, user } = await helpers.createUser();
const { client } = await helpers.createUser();
const file = new File(["Hello, world!"], "test.txt");
const resp = await client.files.upload(file);
expect(resp.id).toBeString();
Expand All @@ -12,3 +12,27 @@ test("POST+GET /api/files", async () => {
const fileResp = await client.files.get(resp.id);
expect(await fileResp.text()).toBe("Hello, world!");
});

test("GET /api/files serves uploaded files inline with restrictive headers", async () => {
const { helpers } = await serve();
const { client } = await helpers.createUser();
const file = new File(["<h1>content</h1>"], "content.html", {
type: "text/html",
});

const uploaded = await client.files.upload(file);
const response = await fetch(uploaded.url);

expect(response.status).toBe(200);
expect(await response.text()).toBe("<h1>content</h1>");
expect(response.headers.get("content-type")).toStartWith("text/html");
expect(response.headers.get("content-disposition")).toBe(
'inline; filename="content.html"'
);
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
const csp = response.headers.get("content-security-policy");
expect(csp).toContain("default-src 'none'");
expect(csp).toContain("sandbox");
expect(csp).toContain("frame-ancestors 'none'");
expect(response.headers.get("x-frame-options")).toBeNull();
});
8 changes: 6 additions & 2 deletions internal/database/src/postgres-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ self.onmessage = async (e) => {
},
async onMessage(data, { isAuthenticated }) {
if (!isAuthenticated) return;
if (data[0] === FrontendMessageCode.Terminate) return;
maybeClaimOnEnqueue(socket, data);
return new Promise<Uint8Array>((resolve, reject) => {
taskQueue.push({ socket, data, resolve, reject });
Expand All @@ -519,9 +520,12 @@ self.onmessage = async (e) => {
const cleanupSocket = () => {
releaseOwnerIf(socket);
for (let i = taskQueue.length - 1; i >= 0; i--) {
if (taskQueue[i]!.socket === socket) {
const task = taskQueue[i];
if (task?.socket === socket) {
try {
taskQueue[i]!.reject(new Error("Socket closed"));
// Closed sockets cannot receive pending responses, so complete
// queued work without producing an error.
task.resolve(new Uint8Array(0));
} catch {}
taskQueue.splice(i, 1);
}
Expand Down
12 changes: 7 additions & 5 deletions internal/site/components/chat-message-input.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { render, waitFor } from "@testing-library/react";
// Load the core package first so Bun initializes Lexical's shared exports before @lexical/react.
import "lexical";
import { afterAll, beforeAll, expect, test } from "bun:test";
import { render, waitFor } from "@testing-library/react";
import { Window } from "happy-dom";
import {
ChatMessageInput,
Expand All @@ -14,13 +16,13 @@ beforeAll(() => {
});

afterAll(async () => {
// @ts-ignore
// @ts-expect-error
delete globalThis.window;
// @ts-ignore
// @ts-expect-error
delete globalThis.document;
// @ts-ignore
// @ts-expect-error
delete globalThis.MutationObserver;
// @ts-ignore
// @ts-expect-error
delete globalThis.getComputedStyle;
});

Expand Down
14 changes: 8 additions & 6 deletions internal/site/components/chat-multimodal-input.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { UIAttachment } from "@/hooks/use-attachments";
import { fireEvent, render, waitFor } from "@testing-library/react";
// Load the core package first so Bun initializes Lexical's shared exports before @lexical/react.
import "lexical";
import { afterAll, beforeAll, expect, mock, test } from "bun:test";
import { fireEvent, render, waitFor } from "@testing-library/react";
import { Window } from "happy-dom";
import type { UIAttachment } from "@/hooks/use-attachments";
import type { ChatMessageInputRef } from "./chat-message-input";
import {
ChatMultimodalInput,
Expand All @@ -17,13 +19,13 @@ beforeAll(() => {
});

afterAll(async () => {
// @ts-ignore
// @ts-expect-error
delete globalThis.window;
// @ts-ignore
// @ts-expect-error
delete globalThis.document;
// @ts-ignore
// @ts-expect-error
delete globalThis.MutationObserver;
// @ts-ignore
// @ts-expect-error
delete globalThis.getComputedStyle;
});

Expand Down
Loading