Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion api/admin/claim.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ module.exports = async function handler(req, res) {
const transitionRows = db.normalizeRows(
await db.query('select * from claim_status_transitions where claim_id = $1 order by created_at asc', [claimId])
);
const cardRows = db.normalizeRows(
await db.query('select * from agent_cards where claim_id = $1 order by ens asc', [claimId])
);

return res.status(200).json({
ok: true,
claim: claimRows[0],
agents: agentRows,
events: eventRows,
transitions: transitionRows
transitions: transitionRows,
cards: cardRows
});
} catch (error) {
console.error('ADMIN_CLAIM_QUERY_FAILED', { message: error.message, code: error.code });
Expand Down
94 changes: 76 additions & 18 deletions api/admin/publish-agent-cards.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,29 @@ module.exports = async function handler(req, res) {
const claimId = typeof req.body?.claimId === 'string' ? req.body.claimId.trim() : '';
if (!claimId) return res.status(400).json({ ok: false, status: 'INVALID_CLAIM_ID' });

let transactionOpen = false;
try {
const claimRows = db.normalizeRows(await db.query('select * from claim_requests where claim_id = $1 limit 1', [claimId]));
if (!claimRows.length) return res.status(404).json({ ok: false, status: 'CLAIM_NOT_FOUND' });
if (claimRows[0].status !== 'approved' && claimRows[0].status !== 'cards_published') {
return res.status(409).json({ ok: false, status: 'INVALID_STATUS', error: 'Claim must be approved before publishing cards.' });

const claim = claimRows[0];
if (claim.status !== 'approved' && claim.status !== 'cards_published') {
return res.status(409).json({ ok: false, status: 'CLAIM_NOT_APPROVED', error: 'Claim must be approved before publishing cards.' });
}

const agents = db.normalizeRows(await db.query('select * from claim_agents where claim_id = $1 order by capability asc', [claimId]));
if (!agents.length) return res.status(409).json({ ok: false, status: 'NO_AGENTS_FOR_CLAIM' });
if (!agents.length) return res.status(409).json({ ok: false, status: 'AGENTS_NOT_FOUND' });

const existing = db.normalizeRows(await db.query('select * from agent_cards where claim_id = $1 order by ens asc', [claimId]));
if (existing.length) {
return res.status(200).json({ ok: true, claimId, status: 'CARDS_ALREADY_PUBLISHED', cards: existing.map((r) => ({ ens: r.ens, cardUrl: r.card_url })) });
const existingByEns = new Map(existing.map((row) => [String(row.ens || '').trim(), row]));

if (claim.status === 'cards_published' && isComplete(agents, existingByEns)) {
return res.status(200).json({ ok: true, claimId, status: 'CARDS_ALREADY_PUBLISHED', cards: formatCards(existing) });
}

await db.query('begin');
transactionOpen = true;

const cards = [];
for (const agent of agents) {
const ens = String(agent.ens || '').trim();
Expand All @@ -60,34 +68,84 @@ module.exports = async function handler(req, res) {
`update claim_agents
set card_url = $3,
card_status = 'published',
card_published_at = now()
card_published_at = coalesce(card_published_at, now())
where claim_id = $1 and id = $2`,
[claimId, agent.id, cardUrl]
);
cards.push({ ens, cardUrl });
}

await db.query("update claim_requests set status = 'cards_published' where claim_id = $1", [claimId]);
await db.query(
`insert into claim_events (claim_id, event_type, actor, message, event_json)
values ($1, 'agent_cards.published', 'admin', 'Agent cards published', $2::jsonb)`,
[claimId, JSON.stringify({ count: cards.length, cards })]
const alreadyPublishedEvent = db.normalizeRows(
await db.query("select id from claim_events where claim_id = $1 and event_type = 'agent_cards.published' limit 1", [claimId])
);
await db.query(
`insert into claim_status_transitions (claim_id, from_status, to_status, action, actor, reason, metadata_json)
values ($1, 'approved', 'cards_published', 'publish_agent_cards', 'admin', null, $2::jsonb)`,
[claimId, JSON.stringify({ cardCount: cards.length })]
if (!alreadyPublishedEvent.length) {
await db.query(
`insert into claim_events (claim_id, event_type, actor, message, event_json)
values ($1, 'agent_cards.published', 'admin', 'Agent cards published', $2::jsonb)`,
[claimId, JSON.stringify({ count: cards.length, cards })]
);
}

const publishedTransition = db.normalizeRows(
await db.query(
`select id from claim_status_transitions
where claim_id = $1 and from_status = 'approved' and to_status = 'cards_published' and action = 'publish_agent_cards'
limit 1`,
[claimId]
)
);
if (!publishedTransition.length) {
await db.query(
`insert into claim_status_transitions (claim_id, from_status, to_status, action, actor, reason, metadata_json)
values ($1, 'approved', 'cards_published', 'publish_agent_cards', 'admin', null, $2::jsonb)`,
[claimId, JSON.stringify({ cardCount: cards.length })]
);
}

return res.status(200).json({ ok: true, claimId, status: 'CARDS_PUBLISHED', cards });
if (claim.status === 'approved') {
await db.query("update claim_requests set status = 'cards_published' where claim_id = $1", [claimId]);
}

await db.query('commit');
transactionOpen = false;

return res.status(200).json({
ok: true,
claimId,
status: claim.status === 'approved' ? 'CARDS_PUBLISHED' : 'AGENT_CARDS_REPAIRED',
cards
});
} catch (error) {
if (transactionOpen) {
try { await db.query('rollback'); } catch (_) {}
}

const statusCode = error && error.statusCode ? error.statusCode : 500;
const status = error && error.status ? error.status : (statusCode === 500 ? 'ADMIN_PUBLISH_AGENT_CARDS_FAILED' : 'AGENT_CARD_PUBLISH_FAILED');
const message = error && error.error ? error.error : 'Failed to publish agent cards.';

console.error('ADMIN_PUBLISH_AGENT_CARDS_FAILED', { message: error.message, code: error.code, claimId });
const payload = { ok: false, status: 'ADMIN_PUBLISH_AGENT_CARDS_FAILED', error: 'Failed to publish agent cards.' };

const payload = { ok: false, status, error: message };
if (process.env.NODE_ENV !== 'production') payload.debug = { message: error.message, code: error.code };
return res.status(500).json(payload);
return res.status(statusCode).json(payload);
}
};

function isComplete(agents, existingByEns) {
for (const agent of agents) {
const ens = String(agent.ens || '').trim();
const row = existingByEns.get(ens);
if (!row) return false;
if (!agent.card_url || !agent.card_status) return false;
}
return true;
}

function formatCards(rows) {
return rows.map((row) => ({ ens: row.ens, cardUrl: row.card_url }));
}

function buildCardJson(agent, ens, capability) {
return {
type: 'erc8004/registration/v1',
Expand Down
Loading
Loading