mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-29 09:23:16 -06:00
Implements the #109 MVP from docs proposal: a floating overlay PUSHED to a device or group in real time, rendered above the playlist without disturbing it. Scope is the MVP only — video/RTSP, MQTT, offline-queue, and the priority/stacking system are deferred to follow-up PRs as the proposal specifies. Protocol (/device socket, player-agnostic): - device:pip-show { pip_id, type:image|web, uri, position, width, height, duration, title?, title_color?, background_color?, opacity?, border_radius?, close_button? } - device:pip-clear { pip_id? } The player fetches uri itself (same trust model as remote_url content; server never proxies). type:web is full-trust by design, hence the 'full' token scope. Server (server/routes/pip.js, new; mounted in config/api-surface.js PUBLIC_ROUTERS): - POST /api/pip and POST /api/pip/clear + DELETE /api/pip, all requireScope('full'). - Resolves device_id to a device OR a group, expands a group to members, and emits per-device — reusing the group command route's room-size online check and {device_id, name, status: sent|offline} result shape. Generates pip_id. - Validates type/position allowlists, uri http(s), numeric bounds on width/height/duration/opacity/border_radius, colors via the existing VALID_COLOR (#RRGGBB; transparency is the separate opacity field). - Workspace-isolated: every target query is scoped to req.workspaceId, so a token bound to workspace A can't address workspace B (404). Offline devices are reported, never queued (PiP is ephemeral). Player overlay layer (Tizen; tizen/js/pip-overlay.js, new): - A #pip sibling ABOVE #stage that PlaylistPlayer/ZoneRenderer never touch. - applyOrientation now applies the SAME transform to #pip as #stage, so corner positions track the visible CONTENT in all four orientations. - image -> <img>, web -> <iframe> (muted by default: empty allow= denies autoplay), sized/positioned/styled per payload, optional title bar. - Single overlay slot, last-show-wins; duration timer (0 = until cleared); pip-clear (id-aware) or timer tears down; teardown wrapped so a malformed payload can't wedge the layer. Reports show/clear over device:log (tag 'pip'). Dashboard: a minimal "Send overlay" / "Clear overlay" tester on the device-detail controls (device/group via the open device, type, uri, position, duration), calling POST /api/pip through the api helper. Tests (server suite green, 161/161): - api.test.js: PiP tier — authz (read/write 403, full passes), workspace isolation (wsA token -> wsB device 404), payload validation, device + group targeting, clear; plus the PUBLIC_ROUTERS snapshot-firewall updated for /api/pip. - pip-overlay.test.js: loads the real player.js + pip-overlay.js in a vm with a DOM shim; proves the overlay shows, auto-dismisses on the duration timer, and never changes the playlist signature / touches #stage; web->iframe, last-show-wins, id-aware clear, malformed-payload safety. Not in this PR (intentional): - Android player overlay — fast-follow. Protocol + server are player-agnostic; the Android layer (an overlay View above the player, orientation-matched to MainActivity's rootView rotation) is the same shape and lands next. - OpenAPI docs for POST /api/pip — the contract test's scope heuristic only treats 'command' paths as full-scope, so documenting a full-scope non-command route there needs that heuristic extended first; deferred with the docs item (proposal §8.6). - video/rtsp types, MQTT, offline queue-on-reconnect, priority/stacking, arbitrary (x,y)/selector positioning (proposal §6). Refs #109 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
3.3 KiB
JavaScript
62 lines
3.3 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 },
|
|
{ path: '/api/pip', mod: './routes/pip' },
|
|
];
|
|
|
|
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 };
|