Files
AgentHub/server/lib/cc-target.js
T
nntrivi2001 8546748759 chore: maintenance — dependency majors, rename step 4, member Claude config, OpenAPI coverage
Dependencies
- Security: body-parser and multer DoS advisories fixed (npm audit: 0).
- Server: Express 5 (query parser kept "extended", req.body defaults to {},
  SPA catch-all /{*splat}, send-file helper restores Express 4's dot-folder
  rule for sendFile/download, alerts ack route without inline regex),
  better-sqlite3 13, js-yaml 5 (server/lib/yaml.js keeps js-yaml 4's default
  schema), uuid dropped for crypto.randomUUID, node-pty beta.18, multer 2.5.
- Client: React 19, xterm 6 (WebGL renderer replaces the removed canvas one,
  DOM fallback on context loss; Alt+arrow word moves re-mapped), lucide-react 1,
  TypeScript 7, i18next/react-i18next/codemirror minors.

Workspace rename step 4
- `agenthub lanes` and `--lane` exit 1 with "renamed: use …"; client api.ts,
  the CLI and the MCP server call /api/workspaces; /api/lanes stays mounted.

Server mode (S3): members manage their own Claude Code config
- server/lib/cc-target.js resolves whose config a /cc-config request touches
  and gates the router by allowlist; /profiles* and /plugins/update-all stay
  admin-only. Member writes go through write-as, CLI calls through sudo as the
  member (never -y), reads through openSafe; cc_config_changed reaches only
  its owner. Not verified live on an S3 install yet.

Other
- Chat: an HTML file is an artifact only when written whole and not a project
  page (index.html, or under src/, public/, templates/, …).
- OpenAPI covers every /api route; openapi-coverage.test.js guards it.
- read-pool test keeps its event loop alive (its worker is unref'd).
- Docs: plan statuses, checklists, CLI.md, LANES.md, SERVER-MODE.md, API.md.
2026-10-26 14:22:41 +07:00

136 lines
4.6 KiB
JavaScript

/**
* @file cc-target.js
* @description Whose Claude Code configuration a `/api/cc-config` request reads
* and writes — the ONE resolver every handler of `routes/cc-config.js` uses,
* and the allowlist of the endpoints a non-admin may reach there at all.
*
* - Local mode, or an admin: the host's Claude home (`getClaudeHome()`), with
* every handler behaving exactly as it did before members had a config page.
* - Server mode, a non-admin with an enabled, provisioned Linux account (S3):
* their OWN `<home>/.claude` and `<home>/.claude.json`. Their writes run as
* them (cc-mutate's `ioFor`, cc-plugin-cli's sudo), never as the host.
* - An analyst with no such account, reading: the host's, read-only — what an
* analyst read before. `?view=host` gives an analyst WITH an account that
* same read-only view.
* - Anyone else (a developer with no account, any non-admin write without
* one): 403 ENOACCOUNT — never "the host's then".
*
* Nothing here takes a home or a path from the request.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const path = require("node:path");
const { getClaudeHome } = require("./claude-home");
/**
* What a non-admin may call under `/api/cc-config` ("METHOD /path", lower
* case, HEAD counted as GET): reads of their own config and the writes that
* run as them. Everything else — `/profiles*` (the host's login switching),
* `/plugins/update-all` (the host's Update all, AgentHub's own plugin
* included), and any endpoint added later — stays an admin's until it is
* listed here.
*/
const MEMBER_ROUTES = new Set([
"GET /target",
"GET /overview",
"GET /skills",
"GET /agents",
"GET /commands",
"GET /output-styles",
"GET /plugins",
"GET /plugins/available",
"GET /mcp",
"GET /hooks",
"GET /settings",
"GET /memory",
"GET /marketplaces",
"GET /keybindings",
"GET /statusline",
"GET /hook-scripts",
"GET /file",
"GET /backups",
"PUT /keybindings",
"PUT /file",
"DELETE /file",
"PATCH /settings",
"POST /project-memory",
"POST /plugins/install",
"POST /plugins/action",
"POST /marketplaces/add",
"POST /marketplaces/action",
"POST /mcp/add",
"POST /mcp/remove",
"POST /reload",
]);
/** The host-wide reads an analyst had before (staff read everything): still read-only. */
const ANALYST_READS = new Set(["GET /profiles", "GET /plugins/update-all"]);
/** `"METHOD /path"` as the allowlists spell it (decoded, lower case, no trailing slash). */
function routeKey(req) {
const p = require("./auth").apiPath(req).toLowerCase().replace(/\/+$/, "") || "/";
const method = req.method === "HEAD" ? "GET" : String(req.method || "").toUpperCase();
return `${method} ${p}`;
}
/** Whether `req.user` may reach this cc-config endpoint at all (local mode and admins: yes). */
function mayReach(req) {
const u = req.user;
if (!u || u.role === "admin") return true;
const key = routeKey(req);
return MEMBER_ROUTES.has(key) || (u.role === "analyst" && ANALYST_READS.has(key));
}
/** Express middleware: the router's own gate (403 EADMIN), the single rule for `/cc-config`. */
function ccGate(req, res, next) {
if (mayReach(req)) return next();
res.status(403).json({ error: { code: "EADMIN", message: "only an admin may open this" } });
}
const isRead = (req) => req.method === "GET" || req.method === "HEAD";
function host(readOnly = false) {
return {
acct: null,
own: false,
readOnly,
home: getClaudeHome(),
claudeJson: require("./cc-discovery").getClaudeJsonPath(),
};
}
/**
* The target of `req`: `{acct, own, readOnly, home, claudeJson}` — `acct` and
* `own` set for a member's own config.
* @throws ENOACCOUNT (status 403) for a non-admin with no account to act on
*/
function resolveCcTarget(req) {
const u = req.user;
if (!u || u.role === "admin") return host();
if (u.role === "analyst" && isRead(req) && req.query?.view === "host") return host(true);
let acct = null;
try {
acct = require("./run-as").accountForUser(u.username);
} catch {
acct = null; // a disabled / unprovisioned account is no account: refuse below
}
if (acct) {
return {
acct,
own: true,
readOnly: false,
home: path.join(acct.home, ".claude"),
claudeJson: path.join(acct.home, ".claude.json"),
};
}
if (u.role === "analyst" && isRead(req)) return host(true);
throw Object.assign(
new Error(
"your Claude Code config lives in your own account, and this server has none for you yet"
),
{ code: "ENOACCOUNT", status: 403 }
);
}
module.exports = { resolveCcTarget, ccGate, mayReach, routeKey, MEMBER_ROUTES, ANALYST_READS };