From ca59ee9807a03e4bdf2c5a2ddccf6b4327cfc69f Mon Sep 17 00:00:00 2001 From: Greg Soucy Date: Fri, 22 May 2026 20:46:18 -0400 Subject: [PATCH] Add internal admin claims APIs and review page --- api/admin/_auth.js | 25 +++++++ api/admin/claim.js | 50 +++++++++++++ api/admin/claims.js | 56 +++++++++++++++ public/admin/claims.html | 128 +++++++++++++++++++++++++++++++++ tests/api-admin-claims.test.js | 75 +++++++++++++++++++ 5 files changed, 334 insertions(+) create mode 100644 api/admin/_auth.js create mode 100644 api/admin/claim.js create mode 100644 api/admin/claims.js create mode 100644 public/admin/claims.html create mode 100644 tests/api-admin-claims.test.js diff --git a/api/admin/_auth.js b/api/admin/_auth.js new file mode 100644 index 0000000..d279848 --- /dev/null +++ b/api/admin/_auth.js @@ -0,0 +1,25 @@ +'use strict'; + +function requireAdminAuth(req, res) { + const configured = process.env.ADMIN_API_KEY; + if (!configured) { + res.status(503).json({ ok: false, status: 'ADMIN_NOT_CONFIGURED' }); + return false; + } + + const authorization = req.headers && (req.headers.authorization || req.headers.Authorization); + const token = typeof authorization === 'string' && authorization.startsWith('Bearer ') + ? authorization.slice('Bearer '.length) + : ''; + + if (!token || token !== configured) { + res.status(401).json({ ok: false, status: 'UNAUTHORIZED' }); + return false; + } + + return true; +} + +module.exports = { + requireAdminAuth +}; diff --git a/api/admin/claim.js b/api/admin/claim.js new file mode 100644 index 0000000..54caef0 --- /dev/null +++ b/api/admin/claim.js @@ -0,0 +1,50 @@ +'use strict'; + +const db = require('../../lib/db'); +const { requireAdminAuth } = require('./_auth'); + +module.exports = async function handler(req, res) { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.setHeader('Cache-Control', 'no-store'); + + if (req.method !== 'GET') { + res.setHeader('Allow', 'GET'); + return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' }); + } + + if (!requireAdminAuth(req, res)) { + return; + } + + const claimId = req.query && req.query.claimId; + if (!claimId || typeof claimId !== 'string') { + return res.status(400).json({ ok: false, status: 'INVALID_CLAIM_ID' }); + } + + try { + const claimResult = await db.query('select * from claim_requests where claim_id = $1 limit 1', [claimId]); + if (!claimResult.rows.length) { + return res.status(404).json({ ok: false, status: 'CLAIM_NOT_FOUND' }); + } + + const agentsResult = await db.query( + `select ens, capability, canonical_parent, skill, skill_family, created_at + from claim_agents where claim_id = $1 order by created_at asc`, + [claimId] + ); + const eventsResult = await db.query( + `select event_type, message, metadata_json, created_at + from claim_events where claim_id = $1 order by created_at asc`, + [claimId] + ); + + return res.status(200).json({ + ok: true, + claim: claimResult.rows[0], + agents: agentsResult.rows, + events: eventsResult.rows + }); + } catch (error) { + return res.status(500).json({ ok: false, status: 'ADMIN_CLAIM_QUERY_FAILED' }); + } +}; diff --git a/api/admin/claims.js b/api/admin/claims.js new file mode 100644 index 0000000..e6cccce --- /dev/null +++ b/api/admin/claims.js @@ -0,0 +1,56 @@ +'use strict'; + +const db = require('../../lib/db'); +const { requireAdminAuth } = require('./_auth'); + +module.exports = async function handler(req, res) { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.setHeader('Cache-Control', 'no-store'); + + if (req.method !== 'GET') { + res.setHeader('Allow', 'GET'); + return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' }); + } + + if (!requireAdminAuth(req, res)) { + return; + } + + const requestedLimit = Number.parseInt(req.query && req.query.limit, 10); + const limit = Number.isFinite(requestedLimit) && requestedLimit > 0 + ? Math.min(requestedLimit, 200) + : 50; + + try { + const result = await db.query( + `select claim_id, tenant, authenticated_address, activation_mode, pack_id, status, created_at + from claim_requests + order by created_at desc + limit $1`, + [limit] + ); + + const claims = []; + for (const row of result.rows) { + const countResult = await db.query( + 'select count(*)::int as agent_count from claim_agents where claim_id = $1', + [row.claim_id] + ); + const agentCount = countResult.rows && countResult.rows[0] ? Number(countResult.rows[0].agent_count || 0) : 0; + claims.push({ + claimId: row.claim_id, + tenant: row.tenant, + authenticatedAddress: row.authenticated_address, + activationMode: row.activation_mode, + packId: row.pack_id, + status: row.status, + agentCount, + createdAt: row.created_at + }); + } + + return res.status(200).json({ ok: true, claims }); + } catch (error) { + return res.status(500).json({ ok: false, status: 'ADMIN_CLAIMS_QUERY_FAILED' }); + } +}; diff --git a/public/admin/claims.html b/public/admin/claims.html new file mode 100644 index 0000000..652f73d --- /dev/null +++ b/public/admin/claims.html @@ -0,0 +1,128 @@ + + + + + + CommandLayer Claims Admin + + + + +
+

CommandLayer Claims Admin

+
+
+ + + + + +
+
+ +
+

Claims

+ + + + + +
Claim IDTenantWalletPackStatusAgentsCreated
+
+ +
+

Claim detail

+
Select a claim row to view details.
+
+
+ + + diff --git a/tests/api-admin-claims.test.js b/tests/api-admin-claims.test.js new file mode 100644 index 0000000..6735bde --- /dev/null +++ b/tests/api-admin-claims.test.js @@ -0,0 +1,75 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); + +function makeRes() { + return { + statusCode: 200, + headers: {}, + body: null, + setHeader(name, value) { this.headers[name.toLowerCase()] = value; }, + status(code) { this.statusCode = code; return this; }, + json(payload) { this.body = payload; return this; } + }; +} + +function load(modulePath, mockQuery) { + const handlerPath = require.resolve(modulePath); + const dbPath = require.resolve('../lib/db'); + delete require.cache[handlerPath]; + delete require.cache[dbPath]; + require.cache[dbPath] = { exports: { query: mockQuery, getDatabaseUrl: () => process.env.DATABASE_URL } }; + return require(modulePath); +} + +test('admin claims returns ADMIN_NOT_CONFIGURED when key missing', async () => { + delete process.env.ADMIN_API_KEY; + const handler = load('../api/admin/claims', async () => ({ rows: [] })); + const res = makeRes(); + await handler({ method: 'GET', headers: {}, query: {} }, res); + assert.equal(res.statusCode, 503); + assert.equal(res.body.status, 'ADMIN_NOT_CONFIGURED'); +}); + +test('admin claims returns UNAUTHORIZED when auth missing', async () => { + process.env.ADMIN_API_KEY = 'secret'; + const handler = load('../api/admin/claims', async () => ({ rows: [] })); + const res = makeRes(); + await handler({ method: 'GET', headers: {}, query: {} }, res); + assert.equal(res.statusCode, 401); + assert.equal(res.body.status, 'UNAUTHORIZED'); +}); + +test('admin claims returns list when authorized', async () => { + process.env.ADMIN_API_KEY = 'secret'; + const calls = []; + const handler = load('../api/admin/claims', async (text, params) => { + calls.push(String(text)); + if (String(text).includes('from claim_requests')) { + return { rows: [{ claim_id: 'clm_1', tenant: 'commandlayer', authenticated_address: '0x1', activation_mode: 'cl', pack_id: 'trust', status: 'created', created_at: '2026-05-23T00:00:00.000Z' }] }; + } + return { rows: [{ agent_count: 2 }] }; + }); + const res = makeRes(); + await handler({ method: 'GET', headers: { authorization: 'Bearer secret' }, query: {} }, res); + assert.equal(res.statusCode, 200); + assert.equal(res.body.ok, true); + assert.equal(res.body.claims[0].agentCount, 2); +}); + +test('admin claim detail returns agents and events when authorized', async () => { + process.env.ADMIN_API_KEY = 'secret'; + const handler = load('../api/admin/claim', async (text) => { + const q = String(text); + if (q.includes('from claim_requests')) return { rows: [{ claim_id: 'clm_1', tenant: 'commandlayer', request_json: {} }] }; + if (q.includes('from claim_agents')) return { rows: [{ ens: 'x.signagent.eth' }] }; + return { rows: [{ event_type: 'claim.created' }] }; + }); + const res = makeRes(); + await handler({ method: 'GET', headers: { authorization: 'Bearer secret' }, query: { claimId: 'clm_1' } }, res); + assert.equal(res.statusCode, 200); + assert.equal(res.body.ok, true); + assert.equal(Array.isArray(res.body.agents), true); + assert.equal(Array.isArray(res.body.events), true); +});