mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-17 11:42:40 -06:00
The agency capability behind the proven off-ladder/agencyGate primitive:
- agencyGate is now SCOPE-only at the mount; the per-target check is router.param
('playlistId') in routes/agency.js - it fires WITH the param before the handler, so no
:playlistId route can skip it (drift-proof). A mount-level target check was silently
bypassed (Express populates req.params only at route match); the integration bite-suite
caught it - this is the fix.
- routes/agency.js: POST /content (shared ingest) + POST /playlists/:id/items (date-bounded
#74/#75 item; lands as draft so the admin's re-publish is the approval gate).
- tokens.js: issue scope='agency' tokens bound to a non-empty in-workspace playlist
allowlist (atomic); PUT /:id/targets re-designates (JWT-only -> can't self-widen).
- server.js: AGENCY_ROUTERS mounted bearerAuth + resolveTenancy + agencyGate.
Full bite-suite (test/agency.test.js) GREEN and re-proven to bite on the SHIPPING path:
neutralizing the router.param check makes non-designated->403 go red. Four assertions at
three seams: target (router.param), off-ladder (tokenScopeGate), can't-widen (tokens
JWT-only), issuance cross-workspace (create validation). 139 suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
33 lines
1.5 KiB
JavaScript
33 lines
1.5 KiB
JavaScript
'use strict';
|
|
|
|
// #73 mount seam: agencyGate does SCOPE/off-ladder confinement ONLY (only an agency token
|
|
// reaches the agency router). The per-target check moved to router.param('playlistId') in
|
|
// routes/agency.js, because Express doesn't populate req.params at mount-level middleware -
|
|
// so the target restriction is proven on the REAL runtime path by test/agency.test.js
|
|
// (the integration bite-suite), not here.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const Database = require('better-sqlite3');
|
|
|
|
// agencyGate needs no db now, but requiring the module loads db/database - inject a stub.
|
|
require.cache[require.resolve('../db/database')] = {
|
|
id: require.resolve('../db/database'), loaded: true, exports: { db: new Database(':memory:') },
|
|
};
|
|
const { agencyGate } = require('../middleware/apiToken');
|
|
|
|
function gate(over = {}) {
|
|
const req = { viaToken: true, tokenScope: 'agency', ...over };
|
|
let status = 200, nexted = false;
|
|
const res = { status(s) { status = s; return this; }, json() { return this; } };
|
|
agencyGate(req, res, () => { nexted = true; });
|
|
return { status, nexted };
|
|
}
|
|
|
|
test('#73 agencyGate (mount seam): only agency tokens pass; non-agency + JWT rejected', () => {
|
|
assert.equal(gate().nexted, true, 'agency token passes the scope seam');
|
|
assert.equal(gate({ tokenScope: 'write' }).status, 403, 'read/write/full token -> 403');
|
|
assert.equal(gate({ tokenScope: 'full' }).status, 403, 'full token -> 403');
|
|
assert.equal(gate({ viaToken: false }).status, 403, 'JWT (not a token) -> 403');
|
|
});
|