screentinker/server/test/agency-scope.test.js
ScreenTinker c8a24d2243 feat(api): agency-token security primitive - off-ladder scope + agencyGate (#73)
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>
2026-06-13 21:30:38 -05:00

33 lines
1.6 KiB
JavaScript

'use strict';
// #73 SPINE: an 'agency' scope is OFF the read/write/full ladder, so the EXISTING
// tokenScopeGate rejects it on every router by construction (auto-confinement). This is
// the foundation the whole model rests on - prove it before building anything on top.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const Database = require('better-sqlite3');
// tokenScopeGate is pure (no db), but requiring the module loads db/database - inject one.
require.cache[require.resolve('../db/database')] = {
id: require.resolve('../db/database'), loaded: true, exports: { db: new Database(':memory:') },
};
const { tokenScopeGate } = require('../middleware/apiToken');
function run(scope, method) {
const req = { viaToken: true, tokenScope: scope, method };
let status = 200, nexted = false;
const res = { status(s) { status = s; return this; }, json() { return this; } };
tokenScopeGate(req, res, () => { nexted = true; });
return { status, nexted };
}
test('#73 spine: agency scope auto-fails tokenScopeGate everywhere (off-ladder)', () => {
assert.equal(run('agency', 'GET').status, 403, 'agency cannot read on a normal router');
assert.equal(run('agency', 'POST').status, 403, 'agency cannot write on a normal router');
assert.equal(run('agency', 'GET').nexted, false, 'agency never reaches the handler');
// Contrast: normal scopes still pass - the gate isn't just rejecting everything.
assert.equal(run('write', 'POST').nexted, true, 'write still passes write');
assert.equal(run('read', 'GET').nexted, true, 'read still passes read');
});