feat: v2.8.0 — Artifacts uploads and plugin links, console tab combos, account avatars
- Artifacts hub: upload a file from the computer; make a skill by uploading a Markdown file, files, a folder or a .zip/.skill, or by writing it in the dialog; add plugins by picking from the list a marketplace link answers (POST /api/artifacts/plugin/inspect); Settings → AI tools → Artifacts Hub. - Console tabs are combos of up to four split panes (ConsoleTabs, PaneGrid). - Accounts: an own avatar (PUT/DELETE /api/me/avatar, raster only, ≤ 1 MB, <data dir>/avatars/), shown with name and role at the sidebar's foot and on the workspace owner groups (GET /api/users/:id/avatar, owner_avatar_at). - Users page: per-user storage (GET /api/users/storage, measured in a worker) and account-pool spend/tokens per account (server/lib/pool-usage.js). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+50
-1
@@ -22,13 +22,18 @@
|
||||
* `POST /sign-out-others` ends every login but this one. These writes take the
|
||||
* account from the login COOKIE only — a pane token (an agent acting as its
|
||||
* owner) may not change the owner's login. Each change is audited.
|
||||
* `/avatar` (server mode) is the account's own picture (server/lib/avatars.js):
|
||||
* GET serves it (`nosniff`, `sandbox` CSP), PUT takes the raw image bytes
|
||||
* (PNG/JPEG/GIF/WebP, at most 1 MB) and DELETE removes it — both writes are
|
||||
* account writes like the profile's. The sidebar shows it beside the name.
|
||||
*
|
||||
* S3 (one Linux user per account): an account's dark/light choice also becomes
|
||||
* the theme in their own Claude Code settings, written as them.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const { Router } = require("express");
|
||||
const express = require("express");
|
||||
const { Router } = express;
|
||||
const { db } = require("../db");
|
||||
const auth = require("../lib/auth");
|
||||
const { sameOriginGuard } = require("../lib/same-origin");
|
||||
@@ -162,6 +167,8 @@ const ACCOUNT_STATUS = {
|
||||
EBADFIELD: 400,
|
||||
EBADEMAIL: 400,
|
||||
EBADPASSWORD: 400,
|
||||
EBADIMAGE: 400,
|
||||
ETOOBIG: 413,
|
||||
EREAUTH: 403,
|
||||
ETOOMANY: 429,
|
||||
};
|
||||
@@ -237,6 +244,48 @@ router.post(
|
||||
})
|
||||
);
|
||||
|
||||
const avatars = require("../lib/avatars");
|
||||
|
||||
router.get("/avatar", (req, res) => {
|
||||
if (!auth.isServerMode()) return res.status(404).json({ error: { code: "ENOAVATAR" } });
|
||||
const user = req.user || auth.userFromRequest(req);
|
||||
if (!user) return unauth(res);
|
||||
const file = avatars.avatarFile(user.id);
|
||||
if (!file) return res.status(404).json({ error: { code: "ENOAVATAR", message: "no avatar" } });
|
||||
res.set("Content-Type", file.type);
|
||||
res.set("X-Content-Type-Options", "nosniff");
|
||||
res.set("Content-Security-Policy", "sandbox; default-src 'none'");
|
||||
// The client versions the URL with `avatarAt`, so a new picture is a new URL.
|
||||
res.set("Cache-Control", "private, max-age=86400");
|
||||
res.sendFile(file.path);
|
||||
});
|
||||
|
||||
router.put(
|
||||
"/avatar",
|
||||
sameOriginGuard,
|
||||
express.raw({ type: () => true, limit: avatars.MAX_AVATAR_BYTES }),
|
||||
accountWrite(async (req, user) => {
|
||||
avatars.setAvatar(user.id, req.body);
|
||||
return { out: accountView(user.id), action: "own avatar set" };
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/avatar",
|
||||
sameOriginGuard,
|
||||
accountWrite(async (_req, user) => {
|
||||
avatars.removeAvatar(user.id);
|
||||
return { out: accountView(user.id), action: "own avatar removed" };
|
||||
})
|
||||
);
|
||||
|
||||
// An avatar past its limit reaches here as express.raw's 413.
|
||||
router.use((e, _req, res, next) => {
|
||||
if (e && e.type === "entity.too.large")
|
||||
return res.status(413).json({ error: { code: "ETOOBIG", message: "the image is too large" } });
|
||||
return next(e);
|
||||
});
|
||||
|
||||
/** S3: an account's dark/light mode becomes their Claude Code theme (best effort). */
|
||||
function followClaudeTheme(req, mode) {
|
||||
if (mode !== "dark" && mode !== "light") return;
|
||||
|
||||
Reference in New Issue
Block a user