retire the single-user claim from the docs it outlived
CLAUDE.md asserted "single-user is a hard invariant, not a stage" while users held six rows and role_capabilities held grants. Every doc that repeated it is corrected here, in prose and in the code comments that carried the same claim. The accurate statement is narrower: one owner who bypasses every check, other accounts holding only what their role is granted, and a set of capabilities — terminal, chat, files, tasks, items, desktop, browser — that are structurally ungrantable because they execute as the owner's OS user. TODO.md gains a Multi-user section for what the read turned up: no way to create a second account, dashboards.id colliding across users, authorize.ts untested, pty/vault/opencode taking no identity, Radicale still owner_only. claude-sidecar-isolation.md's open question is answered rather than left open — the per-email spawn model is dead weight, because chat is an execution capability and no second account can ever reach it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -104,8 +104,9 @@ if (PUBLIC_ORIGIN) ORIGIN_RULES[PUBLIC_ORIGIN] = {};
|
||||
// because an app has to reach /api/auth to sign in before it ever calls its own feature.
|
||||
//
|
||||
// What still holds with these on: every protected route requires a valid token (userMiddleware), and the
|
||||
// account backstop below still confines a non-owner account to /api/auth + /api/music whatever Origin it
|
||||
// claims — that one is deliberately NOT disabled, since it is account-based, not origin-based.
|
||||
// capability backstop below still confines a non-owner account to what its ROLE has been granted,
|
||||
// whatever Origin it claims — that one is deliberately NOT disabled, since it is account-based, not
|
||||
// origin-based.
|
||||
//
|
||||
// What is lost: defence in depth, not the lock. Origin was never authentication here — `officer://<hex>`
|
||||
// is chosen by the client, forgeable outside a browser, and extractable from a shipped app binary.
|
||||
@@ -172,12 +173,12 @@ function resolveOrigin(headerOrigin: string | undefined, referer: string | undef
|
||||
// Global gate applied to every request (mounted in hono.ts). Reads the Origin header directly (not
|
||||
// ctx 'origin') so it covers the whole /api tree — the public /api/auth and the protected /api/music
|
||||
// alike — regardless of which routers mount originMiddleware. Two layers:
|
||||
// 1. Account backstop (origin-INDEPENDENT): a valid NON-owner token may reach only /api/auth +
|
||||
// /api/music, no matter the origin. This is the airtight rule — it holds even if a client omits
|
||||
// or forges the Origin header — and is what confines the music accounts to the music app.
|
||||
// 2. Per-origin rules (ORIGIN_RULES): path scoping (music app) and super-admin-only origins (web +
|
||||
// mobile). Redundant with the backstop for the account dimension, but keeps owner-only origins
|
||||
// fully off-limits to non-owners (incl. /api/music) and blocks unknown-path access there.
|
||||
// 1. Capability backstop (origin-INDEPENDENT): a valid NON-owner token may reach only what its ROLE
|
||||
// has been granted, no matter the origin. This is the airtight rule — it holds even if a client
|
||||
// omits or forges the Origin header. It replaced a hardcoded "/api/auth + /api/music" list on
|
||||
// 2026-08-07; that list was why a Member could not reach /api/gitea and no UI could change it.
|
||||
// 2. Per-origin rules (ORIGIN_RULES): path scoping for the single-feature apps. Redundant with the
|
||||
// backstop for the account dimension, but blocks unknown-path access from an app origin.
|
||||
// A missing/invalid token passes both layers (signin needs it; userMiddleware rejects bad tokens on
|
||||
// protected routes). Only a VALID non-owner token is constrained.
|
||||
export const originScopeMiddleware: MiddlewareHandler = async function (ctx, next) {
|
||||
|
||||
Reference in New Issue
Block a user