screentinker/server/lib/agency-targets.js
ScreenTinker 289d54f4fa feat(api): zone-grant confinement for agency tokens - FK-anchored (#73)
Placement-as-grant, replacing the inferred auto-place idea. api_token_target_zones is an
ADDITIVE second table (does NOT touch the proven api_token_targets), structurally anchored:
a composite FK to api_token_targets(token_id, playlist_id) makes a zone grant orphan-
impossible and cascade away when the playlist grant is revoked - "narrow" is structural, not
conventional. zone_id FK -> layout_zones cascades on zone/layout delete.

Confinement (lib/agency-targets.resolveGrantedZone, called in the item-add): grants exist ->
the item MUST land in a granted zone (a body zone_id picks among grants, never escapes them);
none -> whole-playlist/full-screen as before. The item-add stamps the granted zone_id.

Bite-tested (6, all proven incl. neutralize->red on the confinement): granted YES; non-
granted/cross-playlist/ambiguous blocked; orphan-grant rejected by the FK; cascade on
playlist-grant revoke, on playlist delete, on zone/layout delete; and foreign_keys=ON
asserted (a cascade that no-ops because FKs are off is the trap). 153 suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:57:27 -05:00

43 lines
2.2 KiB
JavaScript

'use strict';
// #73: the single query behind GET /api/agency/playlists. Returns ONLY this token's
// designated playlists, in its bound workspace. The WHERE clause IS the confinement and is
// the thing to bite-test:
// t.token_id = ? -> this token's targets, never another token's
// (JOIN api_token_targets) -> only allowlisted playlists, never one outside the allowlist
// p.workspace_id = ? -> only the bound workspace, never cross-workspace
// db is passed in (not module-required) so the confinement is unit-testable in isolation.
function listDesignatedPlaylists(db, tokenId, workspaceId) {
return db.prepare(`
SELECT p.id, p.name, p.status
FROM api_token_targets t
JOIN playlists p ON p.id = t.playlist_id
WHERE t.token_id = ? AND p.workspace_id = ?
ORDER BY p.name
`).all(tokenId, workspaceId);
}
// #73: resolve which zone an agency item-add lands in, enforcing the zone grants. The grant
// is the boundary; a body-supplied zone can pick WITHIN it but never escape it.
// - No zone grants for (token, playlist) -> whole-playlist/full-screen (zone_id NULL); a
// body zone_id is ignored (placement isn't agency-driven when nothing's granted).
// - Zone grants exist -> the item MUST land in a GRANTED zone:
// requested zone that IS granted -> use it (agency picks among its grants);
// requested zone NOT granted -> { ok:false, reason:'forbidden' } (403);
// no request, exactly one grant -> auto-place into it;
// no request, multiple grants -> { ok:false, reason:'ambiguous' } (must pick).
function resolveGrantedZone(db, tokenId, playlistId, requestedZoneId) {
const grants = db.prepare('SELECT zone_id FROM api_token_target_zones WHERE token_id = ? AND playlist_id = ?')
.all(tokenId, playlistId).map(r => r.zone_id);
if (!grants.length) return { ok: true, zoneId: null };
if (requestedZoneId) {
return grants.includes(requestedZoneId)
? { ok: true, zoneId: requestedZoneId }
: { ok: false, reason: 'forbidden' };
}
if (grants.length === 1) return { ok: true, zoneId: grants[0] };
return { ok: false, reason: 'ambiguous' };
}
module.exports = { listDesignatedPlaylists, resolveGrantedZone };