mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-19 09:45:07 +00:00
feat+test+docs: D50 — server.mjs management endpoints (Phase 3 dashboard wire-up) (#27)
* feat+test+docs: D50 — server.mjs management endpoints (Phase 3 dashboard wire-up)
Third Phase 3 D-day. Wires the D49 lib/audit-query.mjs aggregate query
layer into 4 owner_only_block HTTP endpoints per ADR 0008 §§ 7-8.
Ships a placeholder dashboard.html at repo root (D51 lands the full
multi-panel UI). All endpoints follow the Phase 2 / D45 auth + audit
+ touchLastUsed pattern.
4 NEW ENDPOINTS — all owner_only_block per ADR 0008 § 8:
GET /dashboard
Serves dashboard.html (text/html). D50 stub explains state +
lists backing endpoints. D51 replaces with full UI.
GET /v0/management/dashboard-data
Full aggregate per § 7.2:
{ generated_at, window_24h, cache_hit_24h, quota,
spend_trend_30d, top_fallback_chains_24h, cache_stats }
GET /v0/management/quota
Quota subset only (per-provider provider.quotaStatus + error
capture per § 9 graceful degradation).
GET /cache/stats
Live in-memory cacheStore.stats() with generated_at wrapper.
HELPER:
_runOwnerOnlyManagementEndpoint(req, res, method, path, inner)
Factors common auth + audit ctx + owner-block + res.on('finish')
wire. inner is async (req, res, olpIdentity, auditCtx) → void.
Eliminates 4× boilerplate.
OWNER_ONLY_BLOCK MODE (ADR 0008 § 8 D48-fold-in):
authenticate → if owner_tier !== 'owner' → 401 owner_required.
Distinct from owner_only_trim (Phase 2 /health pattern). Anonymous
identity (when allow_anonymous: true) REACHES the handler and is
401'd by the owner check (Suite 24c). Allow_anonymous: false + no
header → 401 auth_required at middleware (Suite 24d).
PROVIDER QUOTASTATUS ERROR CAPTURE:
Dashboard-data + quota endpoints catch per-provider throws and
surface { provider, error, available: null } so one bad provider
doesn't fail the whole panel (ADR 0008 § 9 graceful degradation).
DASHBOARD.HTML PLACEHOLDER (~50 lines at repo root):
Explains D50 state, lists backing endpoints with curl example.
Cached in memory at first /dashboard request via _loadDashboardHtml
with module-scope _dashboardHtmlCache; falls back to in-memory stub
if file missing (defensive for test imports from non-repo cwd).
AUDIT ON MANAGEMENT ENDPOINTS (ADR 0008 § 7.5):
Every management request appends audit row including 401 paths
(verified by Suite 24j). Touch wire skips anonymous + env-owner
identities (matches Phase 2 pattern).
TESTS — Suite 24, +11 (571 → 582):
24a-d: /dashboard owner_only_block matrix (owner 200 / guest 401
/ anonymous-with-allow_anonymous=true 401 / no-auth-with-
allow_anonymous=false 401)
24e: dashboard-data owner → 200 JSON with all § 7.2 fields
(asserts spend_trend_30d.length === 30)
24f: dashboard-data guest → 401 owner_required
24g: quota owner → 200 JSON with quota array
24h: cache/stats owner → 200 JSON shape
24h-401: cache/stats guest → 401
24i: successful dashboard-data appends audit row with status 200
+ key_id + correct path
24j: 401 (guest blocked) dashboard-data appends audit row with
error_code: 'owner_required' + owner_tier: 'guest'
DOCUMENTATION:
- AGENTS.md: dashboard.html new entry (D50 placeholder); lib/audit-
query.mjs marker note unchanged.
- CHANGELOG.md: D50 entry under Unreleased per release_kit overlay.
NOT IN D50 scope:
- Full dashboard UI (D51 — replaces dashboard.html with the real
4-panel layout + 30s poll JS)
- Daily audit rotation (D52)
- tried_providers schema fix (D53)
- Phase 3 close (D55; v0.3.0; maintainer-triggered)
Test count: 571 → 582 (+11). Verified locally via npm test.
AUTHORITY:
- ADR 0008 § 7 (endpoint definitions) + § 8 (owner_only_block
mode) + § 9 (graceful degradation) + § 7.5 (audit on management
endpoints).
- ADR 0007 § 7 (auth model reused).
- ADR 0002 § Provider contract (quotaStatus).
- ADR 0005 (cacheStore.stats source of truth).
- CLAUDE.md release_kit overlay phase_rolling_mode — under
Unreleased.
- Standing autopilot grant.
ALIGNMENT.md scope check: this PR adds 4 new entry-surface endpoints
under owner-only_block gating + a new lib/audit-query consumer surface.
Per Rule 5: management endpoints are owner-only operational surface,
not OpenAI-spec-compatible — they exist outside the /v1/chat/completions
+ /v1/models spec scope. No provider plugin / IR change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs: D50 fold-in — AGENTS.md dashboard.html duplicate (opus P3)
Fresh-context opus reviewer (PR #27) flagged a duplicate dashboard.html
entry: my D50 addition was added directly above a stale
"Planned (Phase 6) — not yet authored" line that should have been
removed. The file contradicted itself.
Fix: merge into single entry — keep the original line phrasing and
attach the D50 status update.
No code change, no test change.
Authority: PR #27 fresh-context opus reviewer P3 finding.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -11344,3 +11344,260 @@ describe('Suite 23 — D49 lib/audit-query.mjs (Phase 3 audit aggregate query la
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Suite 24: D50 management endpoints (Phase 3, ADR 0008 §§ 7-8) ─────────
|
||||
//
|
||||
// HTTP-level tests for the 4 owner_only_block endpoints:
|
||||
// /dashboard, /v0/management/dashboard-data, /v0/management/quota, /cache/stats
|
||||
// Each must 401 non-owner identities (including anonymous-when-allow_anonymous=true
|
||||
// per ADR 0008 § 8) + serve owner identities with proper Content-Type and shape.
|
||||
|
||||
describe('Suite 24 — D50 management endpoints (Phase 3, ADR 0008 §§ 7-8)', () => {
|
||||
const GLOBAL_OLP_HOME = process.env.OLP_HOME;
|
||||
|
||||
let _suite24SavedOAuth;
|
||||
function ensureSuite24FakeOAuth() {
|
||||
_suite24SavedOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
process.env.CLAUDE_CODE_OAUTH_TOKEN = 'suite24-fake-oauth-token';
|
||||
}
|
||||
function restoreSuite24OAuth() {
|
||||
if (_suite24SavedOAuth !== undefined) process.env.CLAUDE_CODE_OAUTH_TOKEN = _suite24SavedOAuth;
|
||||
else delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
}
|
||||
|
||||
function makeSuite24Server() {
|
||||
__setProvidersEnabled({ anthropic: true });
|
||||
__setSpawnImpl(makeMockSpawn(['suite24-response']));
|
||||
ensureSuite24FakeOAuth();
|
||||
const server = createOlpServer();
|
||||
return new Promise(resolve => {
|
||||
server.listen(0, '127.0.0.1', () => resolve({ server, port: server.address().port }));
|
||||
});
|
||||
}
|
||||
function teardownSuite24(server) {
|
||||
return new Promise(resolve => {
|
||||
__resetSpawnImpl();
|
||||
__setProvidersEnabled({});
|
||||
__clearCache();
|
||||
restoreSuite24OAuth();
|
||||
if (server) server.close(() => resolve());
|
||||
else resolve();
|
||||
});
|
||||
}
|
||||
|
||||
describe('24a-d — /dashboard owner_only_block', () => {
|
||||
let TMP, server, port;
|
||||
before(async () => {
|
||||
TMP = _mkdtempSyncForSetup(_pathJoinForSetup(_tmpdirForSetup(), 'olp-test-24ad-'));
|
||||
process.env.OLP_HOME = TMP;
|
||||
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
|
||||
({ server, port } = await makeSuite24Server());
|
||||
});
|
||||
after(async () => {
|
||||
await teardownSuite24(server);
|
||||
process.env.OLP_HOME = GLOBAL_OLP_HOME;
|
||||
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
|
||||
rmSync(TMP, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('24a: owner → 200 text/html with "OLP Dashboard"', async () => {
|
||||
const { plaintext_token } = createKey({ name: '24a-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP });
|
||||
const r = await fetch({
|
||||
port, method: 'GET', path: '/dashboard',
|
||||
headers: { Authorization: `Bearer ${plaintext_token}` },
|
||||
});
|
||||
assert.equal(r.status, 200);
|
||||
assert.ok(r.headers['content-type'].startsWith('text/html'));
|
||||
assert.match(r.body, /OLP Dashboard/i);
|
||||
});
|
||||
|
||||
it('24b: guest → 401 owner_required', async () => {
|
||||
const { plaintext_token } = createKey({ name: '24b-guest', owner_tier: 'guest', providers_enabled: '*', olpHome: TMP });
|
||||
const r = await fetch({
|
||||
port, method: 'GET', path: '/dashboard',
|
||||
headers: { Authorization: `Bearer ${plaintext_token}` },
|
||||
});
|
||||
assert.equal(r.status, 401);
|
||||
const err = JSON.parse(r.body);
|
||||
assert.equal(err.error.type, 'owner_required');
|
||||
});
|
||||
|
||||
it('24c: anonymous (allow_anonymous: true) → 401 owner_required (owner_only_block per ADR 0008 § 8)', async () => {
|
||||
// allow_anonymous: true → no header → anonymous identity → STILL 401
|
||||
// because management endpoints are owner_only_block (not trim).
|
||||
const r = await fetch({ port, method: 'GET', path: '/dashboard' });
|
||||
assert.equal(r.status, 401);
|
||||
const err = JSON.parse(r.body);
|
||||
assert.equal(err.error.type, 'owner_required');
|
||||
});
|
||||
|
||||
it('24d: allow_anonymous: false + no header → 401 auth_required (middleware path)', async () => {
|
||||
__setAuthConfig({ allow_anonymous: false, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
|
||||
try {
|
||||
const r = await fetch({ port, method: 'GET', path: '/dashboard' });
|
||||
assert.equal(r.status, 401);
|
||||
const err = JSON.parse(r.body);
|
||||
assert.equal(err.error.type, 'auth_required');
|
||||
} finally {
|
||||
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('24e-g — /v0/management/dashboard-data + /v0/management/quota', () => {
|
||||
let TMP, server, port;
|
||||
before(async () => {
|
||||
TMP = _mkdtempSyncForSetup(_pathJoinForSetup(_tmpdirForSetup(), 'olp-test-24eg-'));
|
||||
process.env.OLP_HOME = TMP;
|
||||
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
|
||||
({ server, port } = await makeSuite24Server());
|
||||
});
|
||||
after(async () => {
|
||||
await teardownSuite24(server);
|
||||
process.env.OLP_HOME = GLOBAL_OLP_HOME;
|
||||
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
|
||||
rmSync(TMP, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('24e: owner GET /v0/management/dashboard-data → 200 JSON with required ADR 0008 § 7.2 fields', async () => {
|
||||
const { plaintext_token } = createKey({ name: '24e-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP });
|
||||
const r = await fetch({
|
||||
port, method: 'GET', path: '/v0/management/dashboard-data',
|
||||
headers: { Authorization: `Bearer ${plaintext_token}` },
|
||||
});
|
||||
assert.equal(r.status, 200);
|
||||
const body = JSON.parse(r.body);
|
||||
assert.ok(typeof body.generated_at === 'string');
|
||||
assert.ok(typeof body.window_24h === 'object');
|
||||
assert.ok(typeof body.window_24h.request_count === 'number');
|
||||
assert.ok(typeof body.cache_hit_24h === 'object');
|
||||
assert.ok(Array.isArray(body.quota));
|
||||
assert.ok(Array.isArray(body.spend_trend_30d));
|
||||
assert.equal(body.spend_trend_30d.length, 30, 'spend_trend_30d must have 30 entries');
|
||||
assert.ok(Array.isArray(body.top_fallback_chains_24h));
|
||||
assert.ok(typeof body.cache_stats === 'object');
|
||||
});
|
||||
|
||||
it('24f: guest GET /v0/management/dashboard-data → 401 owner_required', async () => {
|
||||
const { plaintext_token } = createKey({ name: '24f-guest', owner_tier: 'guest', providers_enabled: '*', olpHome: TMP });
|
||||
const r = await fetch({
|
||||
port, method: 'GET', path: '/v0/management/dashboard-data',
|
||||
headers: { Authorization: `Bearer ${plaintext_token}` },
|
||||
});
|
||||
assert.equal(r.status, 401);
|
||||
assert.equal(JSON.parse(r.body).error.type, 'owner_required');
|
||||
});
|
||||
|
||||
it('24g: owner GET /v0/management/quota → 200 JSON with quota array', async () => {
|
||||
const { plaintext_token } = createKey({ name: '24g-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP });
|
||||
const r = await fetch({
|
||||
port, method: 'GET', path: '/v0/management/quota',
|
||||
headers: { Authorization: `Bearer ${plaintext_token}` },
|
||||
});
|
||||
assert.equal(r.status, 200);
|
||||
const body = JSON.parse(r.body);
|
||||
assert.ok(typeof body.generated_at === 'string');
|
||||
assert.ok(Array.isArray(body.quota));
|
||||
// Each quota entry has at least a provider key (and possibly more fields).
|
||||
for (const q of body.quota) {
|
||||
assert.ok(typeof q.provider === 'string');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('24h — /cache/stats', () => {
|
||||
let TMP, server, port;
|
||||
before(async () => {
|
||||
TMP = _mkdtempSyncForSetup(_pathJoinForSetup(_tmpdirForSetup(), 'olp-test-24h-'));
|
||||
process.env.OLP_HOME = TMP;
|
||||
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
|
||||
({ server, port } = await makeSuite24Server());
|
||||
});
|
||||
after(async () => {
|
||||
await teardownSuite24(server);
|
||||
process.env.OLP_HOME = GLOBAL_OLP_HOME;
|
||||
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
|
||||
rmSync(TMP, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('24h: owner GET /cache/stats → 200 JSON with hits/misses/size/inflightCount', async () => {
|
||||
const { plaintext_token } = createKey({ name: '24h-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP });
|
||||
const r = await fetch({
|
||||
port, method: 'GET', path: '/cache/stats',
|
||||
headers: { Authorization: `Bearer ${plaintext_token}` },
|
||||
});
|
||||
assert.equal(r.status, 200);
|
||||
const body = JSON.parse(r.body);
|
||||
assert.ok(typeof body.generated_at === 'string');
|
||||
assert.ok(typeof body.hits === 'number');
|
||||
assert.ok(typeof body.misses === 'number');
|
||||
assert.ok(typeof body.size === 'number');
|
||||
assert.ok(typeof body.inflightCount === 'number');
|
||||
});
|
||||
|
||||
it('24h-401: guest GET /cache/stats → 401', async () => {
|
||||
const { plaintext_token } = createKey({ name: '24h-guest', owner_tier: 'guest', providers_enabled: '*', olpHome: TMP });
|
||||
const r = await fetch({
|
||||
port, method: 'GET', path: '/cache/stats',
|
||||
headers: { Authorization: `Bearer ${plaintext_token}` },
|
||||
});
|
||||
assert.equal(r.status, 401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('24i-j — audit rows on management endpoints', () => {
|
||||
let TMP, server, port;
|
||||
before(async () => {
|
||||
TMP = _mkdtempSyncForSetup(_pathJoinForSetup(_tmpdirForSetup(), 'olp-test-24ij-'));
|
||||
process.env.OLP_HOME = TMP;
|
||||
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
|
||||
({ server, port } = await makeSuite24Server());
|
||||
});
|
||||
after(async () => {
|
||||
await teardownSuite24(server);
|
||||
process.env.OLP_HOME = GLOBAL_OLP_HOME;
|
||||
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
|
||||
rmSync(TMP, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('24i: successful /v0/management/dashboard-data appends audit row with path + status_code 200', async () => {
|
||||
const { id, plaintext_token } = createKey({ name: '24i-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP });
|
||||
const r = await fetch({
|
||||
port, method: 'GET', path: '/v0/management/dashboard-data',
|
||||
headers: { Authorization: `Bearer ${plaintext_token}` },
|
||||
});
|
||||
assert.equal(r.status, 200);
|
||||
await new Promise(resolve => setTimeout(resolve, 25));
|
||||
|
||||
const auditPath = _pathJoinForSetup(TMP, 'logs', 'audit.ndjson');
|
||||
assert.ok(fsExistsSync(auditPath));
|
||||
const lines = fsReadFileSync(auditPath, 'utf-8').trim().split('\n').filter(Boolean);
|
||||
const mgmtRow = lines.map(l => JSON.parse(l)).find(row =>
|
||||
row.path === '/v0/management/dashboard-data' && row.status_code === 200,
|
||||
);
|
||||
assert.ok(mgmtRow, 'management dashboard-data audit row must be present');
|
||||
assert.equal(mgmtRow.key_id, id);
|
||||
assert.equal(mgmtRow.owner_tier, 'owner');
|
||||
assert.equal(mgmtRow.method, 'GET');
|
||||
});
|
||||
|
||||
it('24j: 401 (guest blocked) /v0/management/dashboard-data appends audit row with error_code', async () => {
|
||||
const { id, plaintext_token } = createKey({ name: '24j-guest', owner_tier: 'guest', providers_enabled: '*', olpHome: TMP });
|
||||
const r = await fetch({
|
||||
port, method: 'GET', path: '/v0/management/dashboard-data',
|
||||
headers: { Authorization: `Bearer ${plaintext_token}` },
|
||||
});
|
||||
assert.equal(r.status, 401);
|
||||
await new Promise(resolve => setTimeout(resolve, 25));
|
||||
|
||||
const auditPath = _pathJoinForSetup(TMP, 'logs', 'audit.ndjson');
|
||||
const lines = fsReadFileSync(auditPath, 'utf-8').trim().split('\n').filter(Boolean);
|
||||
const blockedRow = lines.map(l => JSON.parse(l)).find(row =>
|
||||
row.path === '/v0/management/dashboard-data' && row.status_code === 401 && row.key_id === id,
|
||||
);
|
||||
assert.ok(blockedRow, 'management 401 audit row must be present');
|
||||
assert.equal(blockedRow.error_code, 'owner_required');
|
||||
assert.equal(blockedRow.owner_tier, 'guest');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user