mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-17 03:32:32 -06:00
The capability/target-restricted token model for the agency portal (#73 option B), proven before any endpoint sits on it: - 'agency' scope value is OFF the read/write/full ladder, so the existing tokenScopeGate rejects it on every public router by construction (auto-confinement, no new code). - api_token_targets join table: which playlists an agency token may act on. - agencyGate: THE single seam - agency-scope-only + (playlist in this token's allowlist AND in the bound workspace), one query enforcing target + cross-workspace isolation. - AGENCY_ROUTERS category in config/api-surface.js (mounted with agencyGate, not tokenScopeGate) - declared; router/mount land with the endpoints. Both bite-tested: spine (agency 403s on tokenScopeGate; read/write still pass) and the gate (non-designated/cross-workspace/non-agency/JWT -> 403; neutralizing the target check goes red). NARROW - not the general capability-scope system. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
61 lines
3.2 KiB
JavaScript
61 lines
3.2 KiB
JavaScript
'use strict';
|
|
|
|
// SINGLE SOURCE OF TRUTH for the API router partition.
|
|
//
|
|
// server.js mounts from these two lists; test/api.test.js (the partition firewall
|
|
// test) asserts against the SAME lists. Because both read this one file, the mount
|
|
// list and the test cannot drift: add a router to PUBLIC_ROUTERS and it gets the
|
|
// token front door AND the firewall test covers it; the day a JWT-only router stops
|
|
// returning 401 to a `Bearer st_` token (e.g. someone gives it the token door), CI
|
|
// fails. This is the firewall-rule-as-code.
|
|
//
|
|
// PUBLIC_ROUTERS - token-reachable. Mounted with the bearerAuth front door +
|
|
// resolveTenancy + tokenScopeGate. A scoped API token AND a JWT
|
|
// session both reach these.
|
|
// JWT_ONLY_ROUTERS - requireAuth only (no token front door). A `Bearer st_` token
|
|
// fails jwt.verify -> 401, so these are unreachable by any token
|
|
// (secure by exclusion). Privileged surfaces live here.
|
|
//
|
|
// Per-entry flags:
|
|
// renderBypass: also exposes a public GET /:id/render (device render) that skips auth.
|
|
// tenancy: JWT-only router also runs resolveTenancy (acts on the caller's active
|
|
// workspace). Routers without it target a workspace by URL/body param
|
|
// and are gated per-handler (e.g. canAdminWorkspace).
|
|
|
|
const PUBLIC_ROUTERS = [
|
|
{ path: '/api/devices', mod: './routes/devices' },
|
|
{ path: '/api/content', mod: './routes/content' },
|
|
{ path: '/api/folders', mod: './routes/folders' },
|
|
{ path: '/api/assignments', mod: './routes/assignments' },
|
|
{ path: '/api/layouts', mod: './routes/layouts' },
|
|
{ path: '/api/widgets', mod: './routes/widgets', renderBypass: true },
|
|
{ path: '/api/schedules', mod: './routes/schedules' },
|
|
{ path: '/api/walls', mod: './routes/video-walls' },
|
|
{ path: '/api/reports', mod: './routes/reports' },
|
|
{ path: '/api/groups', mod: './routes/device-groups' },
|
|
{ path: '/api/playlists', mod: './routes/playlists' },
|
|
{ path: '/api/activity', mod: './routes/activity' },
|
|
{ path: '/api/kiosk', mod: './routes/kiosk', renderBypass: true },
|
|
];
|
|
|
|
const JWT_ONLY_ROUTERS = [
|
|
{ path: '/api/ai', mod: './routes/ai', tenancy: true },
|
|
{ path: '/api/provision', mod: './routes/provisioning', tenancy: true },
|
|
{ path: '/api/teams', mod: './routes/teams', tenancy: true },
|
|
{ path: '/api/white-label', mod: './routes/white-label', tenancy: true },
|
|
{ path: '/api/workspaces', mod: './routes/workspaces' },
|
|
{ path: '/api/admin', mod: './routes/admin' },
|
|
{ path: '/api/tokens', mod: './routes/tokens', tenancy: true },
|
|
];
|
|
|
|
// #73: AGENCY_ROUTERS - capability-restricted ('agency' scope) surface. Mounted with
|
|
// bearerAuth + resolveTenancy + agencyGate (NOT tokenScopeGate). An 'agency' token is
|
|
// OFF the read/write/full ladder, so tokenScopeGate rejects it on every PUBLIC_ROUTER -
|
|
// it can reach ONLY this router, and only its allowlisted playlists in its bound
|
|
// workspace (agencyGate enforces both). read/write/full tokens and JWTs are rejected here.
|
|
const AGENCY_ROUTERS = [
|
|
{ path: '/api/agency', mod: './routes/agency' },
|
|
];
|
|
|
|
module.exports = { PUBLIC_ROUTERS, JWT_ONLY_ROUTERS, AGENCY_ROUTERS };
|