Serve every media file privately at /media/<id>

There is no public media any more. `/media/public/<id>` served a media
row with no session at all, so anyone with the id could read a file.
The public/private split is gone: the path is `/media/<id>` (and
`/media/<id>/thumbnail`), every request is signed in first, and
`grabUserMedia` decides whether the user may see it: their own upload,
a bot's or workspace's picture or file they reach, or a file on a task
they reach. Anything else is a 404 before a path is built.

The `media.is_private` column and the two per-user directories went
with it: uploads live in `media/<user_id>/` and there is nothing to
pick between. The files browser's media volume is now the viewer's
own folder whatever their role.

Bots also say which of the two things they mean. A `[file:ID]` ref is a
saved file and its chip opens the bytes; a folder is a new
`[folder:volume:owner:path]` ref (`src/utils/folder-ref.ts`) whose chip
opens the files browser there and downloads nothing. The old `[path:...]`
ref and the extension sniffing that guessed file from folder are gone,
so `import_file` names the folder holding a file it copied rather than
the file itself.

The prompts say what the Assistant got wrong: list before importing or
saving, one copy per artifact, never split a file into parts unless
asked, never claim the store is empty without a fresh list, and never
invent a size cap. A re-import now says the path already existed
instead of silently overwriting and reporting a fresh copy.
This commit is contained in:
2026-10-22 10:10:53 +01:00
parent ffd32c8b27
commit aebd6b8ccc
37 changed files with 466 additions and 399 deletions
@@ -220,7 +220,6 @@ async function insertMediaRow({
media_type: media_type as any,
media_write_path: write_path,
mime_type: mime,
is_private: 1,
},
],
});
@@ -43,18 +43,6 @@ export default async function grabMedia(
values.push(...query.media_paradigms, query.media_paradigms.length);
}
if (query?.is_media_private) {
sql += ` AND m.is_private = 1\n`;
if (!user?.id) {
throw new Error(`Private media requires authenticated user`);
} else {
sql += ` AND m.user_id = ?\n`;
values.push(user.id);
}
} else {
sql += ` AND COALESCE(m.is_private, 0) <> 1\n`;
}
if (query?.id) {
sql += ` AND m.id = ?\n`;
values.push(query.id);
@@ -0,0 +1,56 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_MEDIA, BunSQLiteTables } from "@/db/types/db";
import grabSharedPictureMedia from "@/src/functions/backend/db/access/grab-shared-picture-media";
import grabBotFileMedia from "@/src/functions/backend/db/bot-files/grab-bot-file-media";
import grabTaskMediaFile from "@/src/functions/backend/db/tasks/grab-task-media-file";
type Params = {
media_id: number | string;
user_id: number | string;
};
/**
* One media row, only if this user may load its bytes. Every media URL is
* `/media/<id>` and every one of them is private, so this is the single
* gate in front of the file on disk:
*
* - the user's own uploads (`media.user_id`),
* - a bot's or workspace's picture, to whoever reaches that bot or
* workspace (`grabSharedPictureMedia`),
* - a bot's or workspace's file, to whoever reaches the folder it lives in
* (`grabBotFileMedia`),
* - a file attached to a task, to whoever reaches the task
* (`grabTaskMediaFile`).
*
* Null when it is not theirs to see, so a request never turns into a path
* before this has passed.
*/
export default async function grabUserMedia({
media_id,
user_id,
}: Params): Promise<BUN_SQLITE_COBBLE_MEDIA | undefined> {
if (!media_id || !user_id) return undefined;
const own = await BunSQLite.select<
BUN_SQLITE_COBBLE_MEDIA,
(typeof BunSQLiteTables)[number]
>({
table: "media",
query: {
query: {
user_id: {
value: user_id,
},
},
},
targetId: media_id,
});
return (
own.singleRes ||
(await grabSharedPictureMedia({ media_id, user_id })) ||
(await grabBotFileMedia({ media_id, user_id })) ||
(await grabTaskMediaFile({ media_id, user_id })) ||
undefined
);
}
@@ -62,7 +62,6 @@ export default async function setProfileImage({ user_id, target, id, data_url }:
media_mime_type: ext,
media_paradigms: [paradigm],
media: { title: `${record.name || target} picture` },
is_private: true,
user: { id: Number(user_id) },
});
@@ -34,7 +34,6 @@ type Params = {
media?: BUN_SQLITE_COBBLE_MEDIA;
media_name?: string;
media_mime_type?: string;
is_private?: boolean;
image_width?: number;
image_height?: number;
thumbnail_width?: number;
@@ -53,7 +52,6 @@ export default async function uploadAndRecordMedia({
media,
media_mime_type,
media_name,
is_private,
image_width,
image_height,
thumbnail_width,
@@ -64,17 +62,10 @@ export default async function uploadAndRecordMedia({
let target_file_name = `${media_name || now}`;
let target_file_thumbnail_name = `${media_name || now}-thumb`;
const {
USER_MEDIA_PRIVATE_RELATIVE_DIR,
USER_MEDIA_PUBLIC_RELATIVE_DIR,
DATA_DIR,
} = grabDirNames({
user,
});
const MEDIA_RELATIVE_DIR = is_private
? USER_MEDIA_PRIVATE_RELATIVE_DIR
: USER_MEDIA_PUBLIC_RELATIVE_DIR;
const { USER_MEDIA_RELATIVE_DIR: MEDIA_RELATIVE_DIR, DATA_DIR } =
grabDirNames({
user,
});
if (!MEDIA_RELATIVE_DIR) {
throw new Error(`No target media directory found!`);
@@ -236,7 +227,6 @@ export default async function uploadAndRecordMedia({
* lets NULLs repeat.
*/
is_primary: is_primary ? 1 : (null as any),
is_private: is_private ? 1 : 0,
media_write_path: media_write_path,
media_thumbnail_write_path:
media_type === "image" ? media_thumbnail_write_path : undefined,
@@ -6,6 +6,14 @@ type Params = {
user?: BUN_SQLITE_COBBLE_USERS_JOIN;
};
/**
* Where a media file is served: `/media/<id>`, always, with
* `/media/<id>/thumbnail` for an image's smaller copy.
*
* There is no public media and no `public`/`private` segment to pick: every
* media URL is private, so the request is checked against the signed-in
* user when it arrives (`grabUserMedia`).
*/
export default function grabMediaURL({ media: passed_media, user }: Params) {
const media = passed_media || {};
@@ -13,19 +21,11 @@ export default function grabMediaURL({ media: passed_media, user }: Params) {
media.id = user.profile_media_id;
}
let media_url = `/media`;
if (media.is_private) {
media_url += `/private`;
} else {
media_url += `/public`;
}
if (!media.id) {
return {};
}
media_url += `/${media.id}`;
const media_url = `/media/${media.id}`;
const media_thumbnail_url =
media.media_type == "image" || user?.id
@@ -25,8 +25,8 @@ type Params = {
*
* Who reaches what: a bot, workspace or computer through the same SQL the
* crew uses (`grabUserBot`, `grabUserWorkspace`, `grabUserComputer`, so a
* member sees only their own grants); their own media folder (super
* admins and admins anyone's); an MCP server's install folder for crew
* member sees only their own grants); their own media folder, and only
* their own whatever their role; an MCP server's install folder for crew
* managers; the database for the super admin alone.
*/
export default async function resolveStorageSpec({ user_id, spec }: Params) {
@@ -75,7 +75,12 @@ async function storageRoot({
: null;
case "media":
if (role == "member" && Number(spec.owner_id) != Number(user_id)) return null;
/**
* A person's uploads are theirs alone, whatever their role:
* the folder is the media library behind `/media/<id>`, not a
* crew-shared one.
*/
if (Number(spec.owner_id) != Number(user_id)) return null;
return (await hasUser({ user_id: spec.owner_id }))
? path.join(DATA_DIR, "media", String(spec.owner_id))
@@ -121,13 +126,8 @@ async function storageLabel({
(await grabUserComputer({ computer_id: spec.owner_id, user_id }))?.name ||
`Computer ${spec.owner_id}`
);
case "media": {
if (role == "member" || Number(spec.owner_id) == Number(user_id)) return "My media";
const person = await grabPersonName({ user_id: spec.owner_id });
return person ? `${person}'s media` : `Media of user ${spec.owner_id}`;
}
case "media":
return "My media";
case "mcp":
return (await grabMcpServerName({ server_id: spec.owner_id })) || `MCP server ${spec.owner_id}`;
case "db":
@@ -154,22 +154,6 @@ async function hasUser({ user_id }: { user_id: number | string }) {
return Boolean(res.payload?.[0]?.id);
}
async function grabPersonName({ user_id }: { user_id: number | string }) {
const res = await BunSQLite.sql<{
first_name?: string | null;
last_name?: string | null;
username?: string | null;
}>({
sql: `SELECT first_name, last_name, username FROM users WHERE id = ? LIMIT 1`,
values: [Number(user_id)],
});
const person = res.payload?.[0];
if (!person) return "";
return [person.first_name, person.last_name].filter(Boolean).join(" ") || person.username || "";
}
async function hasMcpServer({ server_id }: { server_id: number | string }) {
const res = await BunSQLite.sql<{ id: number }>({
sql: `SELECT id FROM mcp_servers WHERE id = ? LIMIT 1`,
@@ -1,12 +1,8 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_MEDIA, BunSQLiteTables } from "@/db/types/db";
import grabDirNames from "@/src/utils/grab-dir-names";
import { existsSync } from "node:fs";
import path from "node:path";
import userAuth from "../../backend/auth/user-auth";
import grabSharedPictureMedia from "@/src/functions/backend/db/access/grab-shared-picture-media";
import grabBotFileMedia from "@/src/functions/backend/db/bot-files/grab-bot-file-media";
import grabTaskMediaFile from "@/src/functions/backend/db/tasks/grab-task-media-file";
import grabUserMedia from "../../backend/db/media/grab-user-media";
const { DATA_DIR } = grabDirNames();
@@ -14,70 +10,37 @@ type Params = {
req: Request;
};
/**
* Serves a media file's bytes: `/media/<id>`, and `/media/<id>/thumbnail`
* for an image's smaller copy.
*
* There is no public media. Every URL is private, so the request has to be
* signed in and the user has to reach the file (`grabUserMedia` decides
* that: their own upload, a bot's or workspace's file or picture they can
* chat with, a file on one of their tasks). Anything else is answered with
* a 404, never with the file.
*/
export default async function handleMediaServer({
req,
}: Params): Promise<Response> {
const url = new URL(req.url);
const [root, privacy, media_id, thumbnail] = url.pathname
const [_root, media_id, thumbnail] = url.pathname
.split("/")
.filter((p) => p.match(/./));
if (!privacy) {
throw new Error(`No media privacy params passed`);
if (!media_id) {
throw new Error(`No media id passed`);
}
let media: BUN_SQLITE_COBBLE_MEDIA | undefined = undefined;
const { user } = await userAuth({ req });
if (privacy == "private") {
const { user, user_types } = await userAuth({ req });
if (!user?.logged_in_status) {
throw new Error(`Not logged in`);
}
const media_res = await BunSQLite.select<
BUN_SQLITE_COBBLE_MEDIA,
(typeof BunSQLiteTables)[number]
>({
table: "media",
query: {
query: {
user_id: {
value: user.id,
},
},
},
targetId: media_id,
});
media =
media_res.singleRes ||
(media_id ? await grabSharedPictureMedia({ media_id, user_id: user.id! }) : null) ||
(media_id ? await grabBotFileMedia({ media_id, user_id: user.id! }) : null) ||
(media_id ? await grabTaskMediaFile({ media_id, user_id: user.id! }) : null) ||
undefined;
} else {
const media_res = await BunSQLite.select<
BUN_SQLITE_COBBLE_MEDIA,
(typeof BunSQLiteTables)[number]
>({
table: "media",
targetId: media_id,
});
media = media_res.singleRes || undefined;
/**
* A private file is only served through `/media/private`, to its
* owner (or, for a bot's or workspace's picture, to whoever sees
* it).
*/
if (media?.is_private) {
media = undefined;
}
if (!user?.logged_in_status) {
throw new Error(`Not logged in`);
}
const media = await grabUserMedia({ media_id, user_id: user.id! });
if (!media?.id) {
throw new Error(`Media record not found!`);
}
@@ -98,12 +61,14 @@ export default async function handleMediaServer({
const file = Bun.file(full_media_location);
const etag = `"${file.size}-${file.lastModified}"`;
const cache_control =
privacy == "private" ? "private, no-cache" : "public, no-cache";
const headers = new Headers({
ETag: etag,
"Cache-Control": cache_control,
/**
* Private: the response depends on who is signed in, so no shared
* cache may hold it.
*/
"Cache-Control": "private, no-cache",
});
if (req.headers.get("if-none-match") === etag) {