From 4b94444626df8ab07dba930025122e0492e32dec Mon Sep 17 00:00:00 2001 From: Greg Soucy Date: Sun, 24 May 2026 15:27:34 -0400 Subject: [PATCH] Remove private commercial backend routes from public repo --- api/admin/_auth.js | 25 -- api/admin/claim-action.js | 140 ----------- api/admin/claim.js | 64 ----- api/admin/claims.js | 74 ------ api/admin/create-checkout-session.js | 204 ---------------- api/admin/publish-agent-cards.js | 174 -------------- api/claim/commandlayer-namespace.js | 122 +--------- api/stripe/webhook.js | 66 ----- public/admin/claims.html | 82 ------- tests/api-admin-claims.test.js | 153 ------------ tests/api-admin-publish-agent-cards.test.js | 99 -------- .../api-claim-commandlayer-namespace.test.js | 227 ------------------ tests/api-payments.test.js | 70 ------ 13 files changed, 12 insertions(+), 1488 deletions(-) delete mode 100644 api/admin/_auth.js delete mode 100644 api/admin/claim-action.js delete mode 100644 api/admin/claim.js delete mode 100644 api/admin/claims.js delete mode 100644 api/admin/create-checkout-session.js delete mode 100644 api/admin/publish-agent-cards.js delete mode 100644 api/stripe/webhook.js delete mode 100644 public/admin/claims.html delete mode 100644 tests/api-admin-claims.test.js delete mode 100644 tests/api-admin-publish-agent-cards.test.js delete mode 100644 tests/api-claim-commandlayer-namespace.test.js delete mode 100644 tests/api-payments.test.js diff --git a/api/admin/_auth.js b/api/admin/_auth.js deleted file mode 100644 index d279848..0000000 --- a/api/admin/_auth.js +++ /dev/null @@ -1,25 +0,0 @@ -'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-action.js b/api/admin/claim-action.js deleted file mode 100644 index dc94edc..0000000 --- a/api/admin/claim-action.js +++ /dev/null @@ -1,140 +0,0 @@ -'use strict'; - -const db = require('../../lib/db'); -const { requireAdminAuth } = require('./_auth'); - -const CLAIM_STATUSES = { - CREATED: 'created', - APPROVED: 'approved', - REJECTED: 'rejected', - CARDS_PUBLISHED: 'cards_published', - PAYMENT_PENDING: 'payment_pending', - PAID: 'paid', - ERC8004_REGISTERED: 'erc8004_registered', - ENS_RECORDS_GENERATED: 'ens_records_generated', - ENS_PROVISIONED: 'ens_provisioned', - LIVE_TEST_PASSED: 'live_test_passed', - LIVE: 'live', - FAILED: 'failed' -}; - -const SUPPORTED_ACTIONS = new Set(['approve', 'reject', 'mark_failed', 'add_note']); - -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 !== 'POST') { - res.setHeader('Allow', 'POST'); - return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' }); - } - if (!requireAdminAuth(req, res)) return; - - const body = req.body || {}; - const claimId = typeof body.claimId === 'string' ? body.claimId.trim() : ''; - const action = typeof body.action === 'string' ? body.action.trim() : ''; - const actor = typeof body.actor === 'string' && body.actor.trim() ? body.actor.trim() : 'admin'; - const reason = typeof body.reason === 'string' ? body.reason.trim() : ''; - const notes = typeof body.notes === 'string' ? body.notes.trim() : ''; - const override = body.override === true; - - if (!claimId) return res.status(400).json({ ok: false, status: 'INVALID_CLAIM_ID' }); - if (!SUPPORTED_ACTIONS.has(action)) return res.status(400).json({ ok: false, status: 'INVALID_ACTION' }); - if ((action === 'reject' || action === 'mark_failed') && !reason) { - return res.status(400).json({ ok: false, status: 'REASON_REQUIRED' }); - } - if (action === 'add_note' && !notes) { - return res.status(400).json({ ok: false, status: 'NOTES_REQUIRED' }); - } - - let fromStatus = null; - - 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' }); - const claim = claimRows[0]; - fromStatus = claim.status; - - if (action === 'approve') { - const allow = fromStatus === CLAIM_STATUSES.CREATED || (fromStatus === CLAIM_STATUSES.REJECTED && override); - if (!allow) { - return res.status(409).json({ ok: false, status: 'INVALID_STATUS_TRANSITION', error: `Cannot approve claim from ${fromStatus} status.`, fromStatus, action }); - } - await db.query( - `update claim_requests - set status = $2, - approved_at = now(), - reviewed_at = now(), - reviewed_by = $3, - admin_notes = case when $4 <> '' then $4 else admin_notes end - where claim_id = $1`, - [claimId, CLAIM_STATUSES.APPROVED, actor, notes] - ); - await insertEventAndTransition({ claimId, fromStatus, toStatus: CLAIM_STATUSES.APPROVED, action, actor, reason, notes, eventType: 'claim.approved', metadata: { override } }); - return res.status(200).json({ ok: true, status: 'CLAIM_ACTION_APPLIED', claimId, action, claimStatus: CLAIM_STATUSES.APPROVED }); - } - - if (action === 'reject') { - if (fromStatus !== CLAIM_STATUSES.CREATED) { - return res.status(409).json({ ok: false, status: 'INVALID_STATUS_TRANSITION', error: `Cannot reject claim from ${fromStatus} status.`, fromStatus, action }); - } - await db.query( - `update claim_requests - set status = $2, - rejected_at = now(), - reviewed_at = now(), - reviewed_by = $3, - rejection_reason = $4 - where claim_id = $1`, - [claimId, CLAIM_STATUSES.REJECTED, actor, reason] - ); - await insertEventAndTransition({ claimId, fromStatus, toStatus: CLAIM_STATUSES.REJECTED, action, actor, reason, notes, eventType: 'claim.rejected' }); - return res.status(200).json({ ok: true, status: 'CLAIM_ACTION_APPLIED', claimId, action, claimStatus: CLAIM_STATUSES.REJECTED }); - } - - if (action === 'mark_failed') { - if (fromStatus === CLAIM_STATUSES.LIVE) { - return res.status(409).json({ ok: false, status: 'INVALID_STATUS_TRANSITION', error: `Cannot mark_failed claim from ${fromStatus} status.`, fromStatus, action }); - } - await db.query( - `update claim_requests - set status = $2, - last_error = $3, - last_error_at = now() - where claim_id = $1`, - [claimId, CLAIM_STATUSES.FAILED, reason] - ); - await insertEventAndTransition({ claimId, fromStatus, toStatus: CLAIM_STATUSES.FAILED, action, actor, reason, notes, eventType: 'claim.failed', metadata: { previousStatus: fromStatus, actor } }); - return res.status(200).json({ ok: true, status: 'CLAIM_ACTION_APPLIED', claimId, action, claimStatus: CLAIM_STATUSES.FAILED }); - } - - const mergedNotes = [claim.admin_notes, notes].filter(Boolean).join('\n').trim(); - await db.query('update claim_requests set admin_notes = $2 where claim_id = $1', [claimId, mergedNotes || notes]); - await db.query( - `insert into claim_events (claim_id, event_type, actor, message, event_json) - values ($1, 'claim.note_added', $2, $3, $4::jsonb)`, - [claimId, actor, notes, JSON.stringify({ action, notes, actor })] - ); - return res.status(200).json({ ok: true, status: 'CLAIM_ACTION_APPLIED', claimId, action, claimStatus: fromStatus }); - } catch (error) { - const debug = { message: error.message, code: error.code }; - console.error('ADMIN_CLAIM_ACTION_FAILED', { ...debug, action, claimId, currentStatus: typeof fromStatus === 'string' ? fromStatus : null }); - const payload = { ok: false, status: 'ADMIN_CLAIM_ACTION_FAILED', error: 'Failed to apply claim action.' }; - if (process.env.NODE_ENV !== 'production') payload.debug = debug; - return res.status(500).json(payload); - } -}; - -async function insertEventAndTransition({ claimId, fromStatus, toStatus, action, actor, reason, notes, eventType, metadata }) { - const message = eventType === 'claim.failed' ? reason : notes || reason || null; - await db.query( - `insert into claim_events (claim_id, event_type, actor, message, event_json) - values ($1, $2, $3, $4, $5::jsonb)`, - [claimId, eventType, actor, message, JSON.stringify({ action, reason, notes, ...(metadata || {}) })] - ); - await db.query( - `insert into claim_status_transitions (claim_id, from_status, to_status, action, actor, reason, metadata_json) - values ($1, $2, $3, $4, $5, $6, $7::jsonb)`, - [claimId, fromStatus, toStatus, action, actor, reason || null, JSON.stringify({ notes, ...(metadata || {}) })] - ); -} diff --git a/api/admin/claim.js b/api/admin/claim.js deleted file mode 100644 index 2f682a3..0000000 --- a/api/admin/claim.js +++ /dev/null @@ -1,64 +0,0 @@ -'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 claimRows = db.normalizeRows( - await db.query(`select cr.*, (select cp.metadata_json->>'checkoutUrl' from claim_payments cp where cp.claim_id = cr.claim_id order by cp.created_at desc limit 1) as stripe_checkout_url from claim_requests cr where cr.claim_id = $1 limit 1`, [claimId]) - ); - if (!claimRows.length) { - return res.status(404).json({ ok: false, status: 'CLAIM_NOT_FOUND' }); - } - - const agentRows = db.normalizeRows( - await db.query('select * from claim_agents where claim_id = $1 order by capability asc', [claimId]) - ); - const eventRows = db.normalizeRows( - await db.query('select * from claim_events where claim_id = $1 order by created_at asc', [claimId]) - ); - 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, - cards: cardRows - }); - } catch (error) { - console.error('ADMIN_CLAIM_QUERY_FAILED', { message: error.message, code: error.code }); - const payload = { ok: false, status: 'ADMIN_CLAIM_QUERY_FAILED', error: 'Failed to load claim.' }; - if (process.env.NODE_ENV !== 'production') { - payload.debug = { - message: error.message, - code: error.code - }; - } - return res.status(500).json(payload); - } -}; diff --git a/api/admin/claims.js b/api/admin/claims.js deleted file mode 100644 index b4a2b46..0000000 --- a/api/admin/claims.js +++ /dev/null @@ -1,74 +0,0 @@ -'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 - cr.claim_id, - cr.tenant, - cr.authenticated_address, - cr.activation_mode, - cr.pack_id, - cr.status, - cr.created_at, - count(ca.id)::int as agent_count - from claim_requests cr - left join claim_agents ca on ca.claim_id = cr.claim_id - group by - cr.claim_id, - cr.tenant, - cr.authenticated_address, - cr.activation_mode, - cr.pack_id, - cr.status, - cr.created_at - order by cr.created_at desc - limit $1`, - [limit] - ); - - const rows = db.normalizeRows(result); - const claims = rows.map((row) => ({ - claimId: row.claim_id, - tenant: row.tenant, - authenticatedAddress: row.authenticated_address, - activationMode: row.activation_mode, - packId: row.pack_id, - status: row.status, - agentCount: Number(row.agent_count || 0), - createdAt: row.created_at - })); - - return res.status(200).json({ ok: true, claims }); - } catch (error) { - console.error('ADMIN_CLAIMS_QUERY_FAILED', { message: error.message, code: error.code }); - const payload = { ok: false, status: 'ADMIN_CLAIMS_QUERY_FAILED', error: 'Failed to load claims.' }; - if (process.env.NODE_ENV !== 'production') { - payload.debug = { - message: error.message, - code: error.code - }; - } - return res.status(500).json(payload); - } -}; diff --git a/api/admin/create-checkout-session.js b/api/admin/create-checkout-session.js deleted file mode 100644 index f4276b8..0000000 --- a/api/admin/create-checkout-session.js +++ /dev/null @@ -1,204 +0,0 @@ -'use strict'; - -const createStripeClient = require('../../lib/stripe-client'); -const db = require('../../lib/db'); -const { requireAdminAuth } = require('./_auth'); - -function asServiceUnavailable(res, status, error) { - return res.status(503).json({ ok: false, status, error }); -} - -function asConflict(res, status, error) { - return res.status(409).json({ ok: false, status, error }); -} - -function getSanitizedSiteUrl() { - const rawSiteUrl = process.env.COMMANDLAYER_SITE_URL; - const siteUrl = typeof rawSiteUrl === 'string' && rawSiteUrl.trim() - ? rawSiteUrl.trim() - : 'https://www.commandlayer.org'; - - if (siteUrl.includes(',')) { - const error = new Error('COMMANDLAYER_SITE_URL must be a valid https://www.commandlayer.org URL.'); - error.code = 'SITE_URL_INVALID'; - throw error; - } - - let parsed; - try { - parsed = new URL(siteUrl); - } catch (_error) { - const error = new Error('COMMANDLAYER_SITE_URL must be a valid https://www.commandlayer.org URL.'); - error.code = 'SITE_URL_INVALID'; - throw error; - } - - if (parsed.protocol !== 'https:' || !['commandlayer.org', 'www.commandlayer.org'].includes(parsed.hostname)) { - const error = new Error('COMMANDLAYER_SITE_URL must be a valid https://www.commandlayer.org URL.'); - error.code = 'SITE_URL_INVALID'; - throw error; - } - - return `${parsed.origin}${parsed.pathname}`.replace(/\/$/, ''); -} - -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 !== 'POST') { - res.setHeader('Allow', 'POST'); - return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' }); - } - if (!requireAdminAuth(req, res)) return; - - const body = req.body || {}; - const claimId = typeof body.claimId === 'string' ? body.claimId.trim() : ''; - const forceNew = body.forceNew === true; - if (!claimId) return res.status(400).json({ ok: false, status: 'INVALID_CLAIM_ID' }); - - let stripe; - try { - stripe = createStripeClient(process.env.STRIPE_SECRET_KEY); - } catch (error) { - if (error?.code === 'STRIPE_NOT_CONFIGURED') { - return asServiceUnavailable(res, 'STRIPE_NOT_CONFIGURED', 'Stripe secret key is not configured.'); - } - if (error?.code === 'STRIPE_SECRET_KEY_INVALID') { - return asServiceUnavailable(res, 'STRIPE_SECRET_KEY_INVALID', error.message); - } - console.error('ADMIN_CREATE_CHECKOUT_STRIPE_INIT_FAILED', { message: error?.message, code: error?.code, claimId }); - return asServiceUnavailable(res, 'STRIPE_NOT_CONFIGURED', 'Stripe secret key is not configured.'); - } - - try { - const claims = db.normalizeRows(await db.query('select * from claim_requests where claim_id = $1 limit 1', [claimId])); - if (!claims.length) return res.status(404).json({ ok: false, status: 'CLAIM_NOT_FOUND', error: 'Claim not found.' }); - - const claim = claims[0]; - if (claim.status === 'paid' || claim.payment_status === 'paid') { - return asConflict(res, 'PAYMENT_ALREADY_COMPLETED', 'Payment is already completed for this claim.'); - } - - if (!['cards_published', 'payment_pending'].includes(claim.status)) { - return asConflict(res, 'CLAIM_NOT_READY_FOR_PAYMENT', 'Claim must be cards_published before creating checkout.'); - } - - if (claim.status === 'payment_pending' && claim.stripe_checkout_session_id && !forceNew) { - return res.status(200).json({ - ok: true, - status: 'CHECKOUT_SESSION_CREATED', - claimId, - checkoutUrl: claim.stripe_checkout_url || null, - sessionId: claim.stripe_checkout_session_id - }); - } - - let siteUrl; - try { - siteUrl = getSanitizedSiteUrl(); - } catch (error) { - if (error?.code === 'SITE_URL_INVALID') { - return asServiceUnavailable(res, 'SITE_URL_INVALID', 'COMMANDLAYER_SITE_URL must be a valid https://www.commandlayer.org URL.'); - } - throw error; - } - - const successUrl = `${siteUrl}/claim/status.html?claimId=${encodeURIComponent(claimId)}&payment=success`; - const cancelUrl = `${siteUrl}/claim/status.html?claimId=${encodeURIComponent(claimId)}&payment=cancelled`; - - let session; - try { - session = await stripe.checkout.sessions.create({ - mode: 'payment', - success_url: successUrl, - cancel_url: cancelUrl, - line_items: [{ - quantity: 1, - price_data: { - currency: 'usd', - unit_amount: 2000, - product_data: { - name: 'CommandLayer Founding Activation', - description: '10 Trust Verification agent namespaces' - } - } - }], - metadata: { - claimId, - tenant: claim.tenant || '', - packId: claim.pack_id || '', - product: 'founding_activation' - } - }); - } catch (error) { - console.error('ADMIN_CREATE_CHECKOUT_SESSION_FAILED', { message: error?.message, code: error?.code, claimId }); - const payload = { - ok: false, - status: 'CHECKOUT_SESSION_CREATE_FAILED', - error: 'Unable to create Stripe checkout session.' - }; - if (process.env.NODE_ENV !== 'production') { - payload.debug = { message: error?.message || 'Unknown Stripe error', code: error?.code || null }; - } - return res.status(502).json(payload); - } - - await db.query( - `insert into claim_payments (claim_id, provider, stripe_checkout_session_id, amount_cents, currency, status, metadata_json) - values ($1, 'stripe', $2, $3, 'usd', 'pending', $4::jsonb) - on conflict (claim_id, provider) - do update set stripe_checkout_session_id = excluded.stripe_checkout_session_id, - amount_cents = excluded.amount_cents, - currency = excluded.currency, - status = excluded.status, - metadata_json = excluded.metadata_json, - updated_at = now()`, - [claimId, session.id, 2000, JSON.stringify({ checkoutUrl: session.url || null })] - ); - - const fromStatus = claim.status; - await db.query( - `update claim_requests - set status = 'payment_pending', - payment_status = 'pending', - payment_amount_cents = $2, - payment_currency = 'usd', - stripe_checkout_session_id = $3 - where claim_id = $1`, - [claimId, 2000, session.id] - ); - - const eventType = forceNew && fromStatus === 'payment_pending' - ? 'payment.checkout_regenerated' - : 'payment.checkout_created'; - const eventMessage = forceNew && fromStatus === 'payment_pending' - ? 'Stripe checkout regenerated.' - : 'Stripe checkout created.'; - - await db.query( - `insert into claim_events (claim_id, event_type, actor, message, event_json) - values ($1, $2, 'system', $3, $4::jsonb)`, - [claimId, eventType, eventMessage, JSON.stringify({ sessionId: session.id, checkoutUrl: session.url || null })] - ); - - if (fromStatus === 'cards_published') { - await db.query( - `insert into claim_status_transitions (claim_id, from_status, to_status, action, actor, metadata_json) - values ($1, 'cards_published', 'payment_pending', 'create_checkout', 'system', $2::jsonb)`, - [claimId, JSON.stringify({ sessionId: session.id })] - ); - } - - return res.status(200).json({ - ok: true, - status: forceNew && fromStatus === 'payment_pending' ? 'CHECKOUT_SESSION_REGENERATED' : 'CHECKOUT_SESSION_CREATED', - claimId, - checkoutUrl: session.url || null, - sessionId: session.id - }); - } catch (error) { - console.error('ADMIN_CREATE_CHECKOUT_SESSION_UNEXPECTED', { message: error?.message, code: error?.code, claimId }); - return res.status(500).json({ ok: false, status: 'ADMIN_CREATE_CHECKOUT_SESSION_FAILED', error: 'Failed to create checkout session.' }); - } -}; diff --git a/api/admin/publish-agent-cards.js b/api/admin/publish-agent-cards.js deleted file mode 100644 index af69505..0000000 --- a/api/admin/publish-agent-cards.js +++ /dev/null @@ -1,174 +0,0 @@ -'use strict'; - -const db = require('../../lib/db'); -const { requireAdminAuth } = require('./_auth'); - -const BASE_URL = 'https://www.commandlayer.org'; -const CARD_VERSION = '1.1.0'; - -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 !== 'POST') { - res.setHeader('Allow', 'POST'); - return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' }); - } - if (!requireAdminAuth(req, res)) return; - - 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' }); - - 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: 'AGENTS_NOT_FOUND' }); - - const existing = db.normalizeRows(await db.query('select * from agent_cards where claim_id = $1 order by ens asc', [claimId])); - 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(); - const capability = String(agent.capability || '').trim(); - const cardPath = `/agent-cards/agents/v${CARD_VERSION}/trust/${ens}.json`; - const cardUrl = `${BASE_URL}${cardPath}`; - const cardJson = buildCardJson(agent, ens, capability); - - await db.query( - `insert into agent_cards (claim_id, ens, card_url, card_json, version, status) - values ($1, $2, $3, $4::jsonb, $5, 'published') - on conflict (card_url) - do update set - claim_id = excluded.claim_id, - ens = excluded.ens, - card_json = excluded.card_json, - version = excluded.version, - status = 'published', - updated_at = now()`, - [claimId, ens, cardUrl, JSON.stringify(cardJson), CARD_VERSION] - ); - - await db.query( - `update claim_agents - set card_url = $3, - card_status = 'published', - card_published_at = coalesce(card_published_at, now()) - where claim_id = $1 and id = $2`, - [claimId, agent.id, cardUrl] - ); - cards.push({ ens, cardUrl }); - } - - 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]) - ); - 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 })] - ); - } - - 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, error: message }; - if (process.env.NODE_ENV !== 'production') payload.debug = { message: error.message, code: error.code }; - 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', - name: ens, - description: `CommandLayer Trust Verification agent for ${capability}.`, - image: 'https://www.commandlayer.org/favicon.ico', - services: [ - { type: 'ens', endpoint: ens }, - { type: 'commandlayer_runtime', endpoint: 'https://runtime.commandlayer.org' }, - { type: 'commandlayer_verifier', endpoint: 'https://runtime.commandlayer.org/verify' } - ], - commandlayer: { - version: CARD_VERSION, - tenant: agent.tenant, - capability, - canonicalParent: agent.canonical_parent, - skill: agent.skill, - skillFamily: agent.skill_family, - kid: agent.kid, - publicKey: agent.public_key, - runtime: 'https://runtime.commandlayer.org', - verifier: 'https://runtime.commandlayer.org/verify' - }, - registrations: [] - }; -} diff --git a/api/claim/commandlayer-namespace.js b/api/claim/commandlayer-namespace.js index d5b8fef..4cffe1d 100644 --- a/api/claim/commandlayer-namespace.js +++ b/api/claim/commandlayer-namespace.js @@ -3,21 +3,6 @@ const crypto = require('node:crypto'); const db = require('../../lib/db'); -const TRUST_VERIFICATION_MAP = { - sign: 'signagent.eth', - attest: 'attestagent.eth', - authorize: 'authorizeagent.eth', - approve: 'approveagent.eth', - reject: 'rejectagent.eth', - permit: 'permitagent.eth', - grant: 'grantagent.eth', - authenticate: 'authenticateagent.eth', - endorse: 'endorseagent.eth', - verify: 'verifyagent.eth' -}; - -const RUNTIME_URL = 'https://runtime.commandlayer.org'; -const VERIFIER_URL = 'https://runtime.commandlayer.org/verify'; const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/; const TENANT_RE = /^[a-z0-9-]{3,32}$/; @@ -25,123 +10,40 @@ function invalid(res, error, reason) { return res.status(400).json({ ok: false, status: 'CLAIM_REQUEST_INVALID', error, reason }); } -function storageUnavailable(res) { - return res.status(503).json({ - ok: false, - status: 'STORAGE_UNAVAILABLE', - error: 'DATABASE_URL is not configured' - }); -} - 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 !== 'POST') { res.setHeader('Allow', 'POST'); - return res.status(405).json({ ok: false, status: 'CLAIM_REQUEST_INVALID', error: 'method_not_allowed', reason: 'Method not allowed. Use POST.' }); + return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' }); } - const body = req.body; - if (!body || typeof body !== 'object' || Array.isArray(body)) { - return invalid(res, 'invalid_body', 'Missing or invalid JSON body.'); - } - - const { authenticatedAddress, tenant, activationMode, packId, capabilities, agents, publicKey, kid, verifier, runtime, schemaVersion } = body; + // TODO: add request rate limiting middleware for public intake. + const { authenticatedAddress, tenant, activationMode, packId } = req.body || {}; if (!authenticatedAddress || !ADDRESS_RE.test(authenticatedAddress)) return invalid(res, 'invalid_authenticated_address', 'authenticatedAddress must be a valid 0x Ethereum address.'); if (activationMode !== 'cl') return invalid(res, 'invalid_activation_mode', 'activationMode must be "cl".'); - if (typeof tenant !== 'string' || !tenant.trim()) return invalid(res, 'invalid_tenant', 'tenant is required.'); - if (tenant.includes('.eth')) return invalid(res, 'invalid_tenant', 'tenant must not include ".eth".'); - if (!TENANT_RE.test(tenant) || tenant.startsWith('-') || tenant.endsWith('-')) { - return invalid(res, 'invalid_tenant', 'tenant must be 3-32 chars of lowercase letters, numbers, hyphen, and cannot start or end with hyphen.'); - } - - if (packId !== 'trust') { - return invalid(res, 'unsupported_pack', 'Only Trust Verification activation requests are supported in this first backend flow.'); - } - if (!Array.isArray(capabilities) || capabilities.length === 0 || capabilities.length > 10) return invalid(res, 'invalid_capabilities', 'capabilities must contain between 1 and 10 items.'); - if (typeof publicKey !== 'string' || !publicKey.startsWith('ed25519:')) return invalid(res, 'invalid_public_key', 'publicKey must start with "ed25519:".'); - if (typeof kid !== 'string' || !kid.trim()) return invalid(res, 'invalid_kid', 'kid is required.'); - if (!Array.isArray(agents) || agents.length === 0) return invalid(res, 'invalid_agents', 'agents is required.'); - if (runtime !== RUNTIME_URL) return invalid(res, 'invalid_runtime', `runtime must be ${RUNTIME_URL}.`); - if (verifier !== VERIFIER_URL) return invalid(res, 'invalid_verifier', `verifier must be ${VERIFIER_URL}.`); - - for (const capability of capabilities) { - const canonicalParent = TRUST_VERIFICATION_MAP[capability]; - if (!canonicalParent) return invalid(res, 'invalid_capability', `Unsupported capability "${capability}" for Trust Verification pack.`); - } - - const capabilitySet = new Set(capabilities); - for (const agent of agents) { - if (!agent || typeof agent !== 'object') return invalid(res, 'invalid_agents', 'Each agent must be an object.'); - const { ens, capability, canonicalParent, skill, skillFamily } = agent; - if (!TRUST_VERIFICATION_MAP[capability]) return invalid(res, 'invalid_agent_capability', `Unsupported agent capability "${capability}".`); - if (!capabilitySet.has(capability)) return invalid(res, 'invalid_agent_capability', `Agent capability "${capability}" must be present in capabilities.`); - if (TRUST_VERIFICATION_MAP[capability] !== canonicalParent) return invalid(res, 'invalid_agent_mapping', `Capability "${capability}" must map to canonical parent "${TRUST_VERIFICATION_MAP[capability]}".`); - if (!Object.values(TRUST_VERIFICATION_MAP).includes(canonicalParent)) return invalid(res, 'invalid_canonical_parent', `Unsupported canonical parent "${canonicalParent}".`); - if (ens !== `${tenant}.${canonicalParent}`) return invalid(res, 'invalid_agent_ens', `Agent ENS must equal "${tenant}.${canonicalParent}".`); - if (skill !== `trust-verification.${capability}`) return invalid(res, 'invalid_skill', `skill must equal "trust-verification.${capability}".`); - if (skillFamily !== 'trust-verification') return invalid(res, 'invalid_skill_family', 'skillFamily must equal "trust-verification".'); - } - - if (!process.env.DATABASE_URL) { - return storageUnavailable(res); - } + if (typeof tenant !== 'string' || !TENANT_RE.test(tenant) || tenant.startsWith('-') || tenant.endsWith('-') || tenant.includes('.eth')) return invalid(res, 'invalid_tenant', 'tenant must be 3-32 lowercase alphanumeric/hyphen chars.'); + if (packId !== 'trust') return invalid(res, 'unsupported_pack', 'Only trust pack intake is currently accepted.'); + if (!process.env.DATABASE_URL) return res.status(503).json({ ok: false, status: 'STORAGE_UNAVAILABLE' }); const claimId = `clm_${crypto.randomUUID().replace(/-/g, '')}`; try { - await db.query('BEGIN'); await db.query( `insert into claim_requests (claim_id, authenticated_address, tenant, activation_mode, pack_id, public_key, kid, runtime, verifier, schema_version, request_json) - values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb)`, - [claimId, authenticatedAddress, tenant, activationMode, packId, publicKey, kid, runtime, verifier, schemaVersion || '1.1.0', JSON.stringify(body)] + values ($1,$2,$3,$4,$5,'','', '', '', '1.1.0', $6::jsonb)`, + [claimId, authenticatedAddress, tenant, activationMode, packId, JSON.stringify({ authenticatedAddress, tenant, activationMode, packId })] ); - - for (const agent of agents) { - await db.query( - `insert into claim_agents - (claim_id, ens, capability, canonical_parent, skill, skill_family) - values ($1,$2,$3,$4,$5,$6)`, - [claimId, agent.ens, agent.capability, agent.canonicalParent, agent.skill || '', agent.skillFamily || ''] - ); - } - await db.query( `insert into claim_events (claim_id, event_type, message, metadata_json) values ($1,$2,$3,$4::jsonb)`, - [ - claimId, - 'claim.created', - 'CommandLayer namespace claim request created.', - JSON.stringify({ agentCount: agents.length, packId, activationMode }) - ] + [claimId, 'claim.created', 'Public claim request received.', JSON.stringify({ publicIntake: true })] ); - await db.query('COMMIT'); - } catch (error) { - await db.query('ROLLBACK'); - if (error && error.code === 'DATABASE_URL_MISSING') { - return storageUnavailable(res); - } - return res.status(500).json({ ok: false, status: 'CLAIM_REQUEST_PERSISTENCE_ERROR', error: 'Failed to persist claim request.' }); + } catch (_error) { + return res.status(500).json({ ok: false, status: 'CLAIM_REQUEST_PERSISTENCE_ERROR' }); } - return res.status(200).json({ - ok: true, - status: 'CLAIM_REQUEST_CREATED', - claimId, - activationMode: 'cl', - tenant, - authenticatedAddress, - agents, - lifecycle: { - claim: 'created', - operatorReview: 'not_started', - ensProvisioning: 'not_started', - agentCards: 'not_started', - erc8004: 'not_started', - liveReceiptTest: 'not_started' - } - }); + return res.status(202).json({ ok: true, status: 'CLAIM_REQUEST_RECEIVED', claimId, message: 'Claim request received.' }); }; diff --git a/api/stripe/webhook.js b/api/stripe/webhook.js deleted file mode 100644 index 5a414b7..0000000 --- a/api/stripe/webhook.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict'; - -const Stripe = require('../../lib/stripe-client'); -const db = require('../../lib/db'); - -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 !== 'POST') { - res.setHeader('Allow', 'POST'); - return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' }); - } - - if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_WEBHOOK_SECRET) { - return res.status(503).json({ ok: false, status: 'STRIPE_NOT_CONFIGURED' }); - } - - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - const sig = req.headers['stripe-signature'] || req.headers['Stripe-Signature']; - const rawBody = req.rawBody || req.body; - - let event; - try { - event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET); - } catch (error) { - return res.status(400).json({ ok: false, status: 'WEBHOOK_SIGNATURE_INVALID' }); - } - - if (event.type !== 'checkout.session.completed') { - return res.status(200).json({ ok: true, status: 'WEBHOOK_EVENT_UNHANDLED' }); - } - - const session = event.data && event.data.object ? event.data.object : {}; - const claimId = session.metadata && session.metadata.claimId; - if (!claimId) return res.status(404).json({ ok: false, status: 'CLAIM_NOT_FOUND' }); - - const claims = db.normalizeRows(await db.query('select * from claim_requests where claim_id = $1 limit 1', [claimId])); - if (!claims.length) return res.status(404).json({ ok: false, status: 'CLAIM_NOT_FOUND' }); - if (claims[0].status === 'paid' || claims[0].payment_status === 'paid') return res.status(200).json({ ok: true }); - - await db.query( - `update claim_payments - set status = 'paid', stripe_payment_intent_id = $2, updated_at = now() - where stripe_checkout_session_id = $1 or claim_id = $3`, - [session.id || null, session.payment_intent || null, claimId] - ); - await db.query( - `update claim_requests - set status = 'paid', payment_status = 'paid', stripe_payment_intent_id = $2, paid_at = now() - where claim_id = $1`, - [claimId, session.payment_intent || null] - ); - await db.query( - `insert into claim_events (claim_id, event_type, actor, message, event_json) - values ($1, 'payment.completed', 'system', 'Stripe payment completed.', $2::jsonb)`, - [claimId, JSON.stringify({ sessionId: session.id || null, paymentIntentId: session.payment_intent || null })] - ); - await db.query( - `insert into claim_status_transitions (claim_id, from_status, to_status, action, actor, metadata_json) - values ($1, 'payment_pending', 'paid', 'payment_webhook', 'system', $2::jsonb)`, - [claimId, JSON.stringify({ sessionId: session.id || null })] - ); - - return res.status(200).json({ ok: true }); -}; diff --git a/public/admin/claims.html b/public/admin/claims.html deleted file mode 100644 index 5721298..0000000 --- a/public/admin/claims.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - CommandLayer Claims Admin - - - -
-

CommandLayer Claims Admin

Internal operator dashboard for claim review and activation pipeline.

-
-

Claims

Select a claim to review.

-
- diff --git a/tests/api-admin-claims.test.js b/tests/api-admin-claims.test.js deleted file mode 100644 index 58aa155..0000000 --- a/tests/api-admin-claims.test.js +++ /dev/null @@ -1,153 +0,0 @@ -'use strict'; - -const test = require('node:test'); -const assert = require('node:assert/strict'); - -function makeRes() { return { statusCode: 200, headers: {}, body: null, setHeader(n,v){this.headers[n.toLowerCase()]=v;}, status(c){this.statusCode=c;return this;}, json(p){this.body=p;return this;} }; } -function normalizeRows(result) { if (Array.isArray(result)) return result; if (result && Array.isArray(result.rows)) return result.rows; return []; } - -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, normalizeRows, 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 claim detail includes transitions and cards', 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 [{ claim_id: 'clm_1', tenant: 'commandlayer', request_json: {} }]; - if (q.includes('from claim_agents')) return [{ ens: 'x.signagent.eth' }]; - if (q.includes('from claim_events')) return [{ event_type: 'claim.created' }]; - if (q.includes('from claim_status_transitions')) return [{ to_status: 'approved' }]; - return [{ ens: 'x.signagent.eth', card_url: 'https://www.commandlayer.org/c.json' }]; - }); - const res = makeRes(); await handler({ method: 'GET', headers: { authorization: 'Bearer secret' }, query: { claimId: 'clm_1' } }, res); - assert.equal(res.statusCode, 200); assert.deepEqual(res.body.transitions, [{ to_status: 'approved' }]); assert.equal(Array.isArray(res.body.cards), true); -}); - -test('claim action auth and config checks', async () => { - delete process.env.ADMIN_API_KEY; - let handler = load('../api/admin/claim-action', async () => []); - let res = makeRes(); await handler({ method: 'POST', headers: {}, body: {} }, res); - assert.equal(res.statusCode, 503); assert.equal(res.body.status, 'ADMIN_NOT_CONFIGURED'); - - process.env.ADMIN_API_KEY = 'secret'; - handler = load('../api/admin/claim-action', async () => []); - res = makeRes(); await handler({ method: 'POST', headers: {}, body: {} }, res); - assert.equal(res.statusCode, 401); assert.equal(res.body.status, 'UNAUTHORIZED'); -}); - -test('approve created claim succeeds and writes event/transition', async () => { - process.env.ADMIN_API_KEY = 'secret'; - const calls = []; - const handler = load('../api/admin/claim-action', async (text, params) => { calls.push({ text: String(text), params }); if (String(text).includes('from claim_requests')) return [{ claim_id: 'clm_1', status: 'created', admin_notes: '' }]; return { rows: [] }; }); - const res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1', action: 'approve', actor: 'admin', notes: 'ok' } }, res); - assert.equal(res.statusCode, 200); assert.equal(res.body.claimStatus, 'approved'); - assert.ok(calls.some((c) => c.text.includes('update claim_requests'))); - assert.ok(calls.some((c) => c.text.includes('insert into claim_events'))); - assert.ok(calls.some((c) => c.text.includes('insert into claim_status_transitions'))); -}); - -test('reject created claim with reason succeeds; reject without reason fails', async () => { - process.env.ADMIN_API_KEY = 'secret'; - let handler = load('../api/admin/claim-action', async (text) => String(text).includes('from claim_requests') ? [{ claim_id: 'clm_1', status: 'created' }] : []); - let res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1', action: 'reject', actor: 'admin', reason: 'bad docs' } }, res); - assert.equal(res.statusCode, 200); assert.equal(res.body.claimStatus, 'rejected'); - - handler = load('../api/admin/claim-action', async () => []); - res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1', action: 'reject', actor: 'admin' } }, res); - assert.equal(res.statusCode, 400); assert.equal(res.body.status, 'REASON_REQUIRED'); -}); - -test('approving an already approved claim returns explicit transition error', async () => { - process.env.ADMIN_API_KEY = 'secret'; - const handler = load('../api/admin/claim-action', async (text) => String(text).includes('from claim_requests') ? [{ claim_id: 'clm_1', status: 'approved' }] : []); - const res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1', action: 'approve', actor: 'admin' } }, res); - assert.equal(res.statusCode, 409); - assert.equal(res.body.status, 'INVALID_STATUS_TRANSITION'); - assert.equal(res.body.error, 'Cannot approve claim from approved status.'); - assert.equal(res.body.fromStatus, 'approved'); - assert.equal(res.body.action, 'approve'); -}); - -test('mark_failed without reason returns REASON_REQUIRED', async () => { - process.env.ADMIN_API_KEY = 'secret'; - const handler = load('../api/admin/claim-action', async () => []); - const res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1', action: 'mark_failed', actor: 'admin' } }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.status, 'REASON_REQUIRED'); -}); - -test('mark_failed from approved succeeds, inserts event and transition', async () => { - process.env.ADMIN_API_KEY = 'secret'; - const calls = []; - const handler = load('../api/admin/claim-action', async (text, params) => { - calls.push({ text: String(text), params }); - if (String(text).includes('from claim_requests')) return [{ claim_id: 'clm_1', status: 'approved', admin_notes: 'n1' }]; - return []; - }); - const res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1', action: 'mark_failed', actor: 'admin', reason: 'ops failed' } }, res); - assert.equal(res.statusCode, 200); - assert.equal(res.body.claimStatus, 'failed'); - assert.ok(calls.some((c) => c.text.includes('update claim_requests') && c.params[1] === 'failed' && c.params[2] === 'ops failed')); - assert.ok(calls.some((c) => c.text.includes('insert into claim_events') && c.params[1] === 'claim.failed' && c.params[3] === 'ops failed')); - assert.ok(calls.some((c) => c.text.includes('insert into claim_status_transitions') && c.params[1] === 'approved' && c.params[2] === 'failed')); -}); - -test('add_note from approved succeeds, inserts note event, does not insert transition', async () => { - process.env.ADMIN_API_KEY = 'secret'; - const calls = []; - const handler = load('../api/admin/claim-action', async (text, params) => { - calls.push({ text: String(text), params }); - if (String(text).includes('from claim_requests')) return [{ claim_id: 'clm_1', status: 'approved', admin_notes: 'n1' }]; - return []; - }); - const res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1', action: 'add_note', actor: 'admin', notes: 'n2' } }, res); - assert.equal(res.statusCode, 200); - assert.equal(res.body.claimStatus, 'approved'); - assert.ok(calls.some((c) => c.text.includes('update claim_requests set admin_notes'))); - assert.ok(calls.some((c) => c.text.includes("insert into claim_events (claim_id, event_type, actor, message") && c.params[2] === 'n2')); - assert.equal(calls.some((c) => c.text.includes('insert into claim_status_transitions')), false); -}); - -test('add_note without notes returns NOTES_REQUIRED', async () => { - process.env.ADMIN_API_KEY = 'secret'; - const handler = load('../api/admin/claim-action', async () => []); - const res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1', action: 'add_note', actor: 'admin' } }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.status, 'NOTES_REQUIRED'); -}); - - - -test('frontend admin dashboard includes status-aware actions and raw-json toggle', async () => { - const html = require('node:fs').readFileSync(require('node:path').join(__dirname, '../public/admin/claims.html'), 'utf8'); - assert.ok(html.includes('Repair / Publish cards')); - assert.ok(html.includes('Show raw JSON')); - assert.ok(html.includes('claim-action')); -}); diff --git a/tests/api-admin-publish-agent-cards.test.js b/tests/api-admin-publish-agent-cards.test.js deleted file mode 100644 index ac7bef0..0000000 --- a/tests/api-admin-publish-agent-cards.test.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -const test = require('node:test'); -const assert = require('node:assert/strict'); - -function makeRes() { return { statusCode: 200, headers: {}, body: null, setHeader(n,v){this.headers[n.toLowerCase()]=v;}, status(c){this.statusCode=c;return this;}, json(p){this.body=p;return this;} }; } -function normalizeRows(result) { if (Array.isArray(result)) return result; if (result && Array.isArray(result.rows)) return result.rows; return []; } - -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, normalizeRows, getDatabaseUrl: () => process.env.DATABASE_URL } }; - return require(modulePath); -} - -test('non-approved claim cannot publish cards', async () => { - process.env.ADMIN_API_KEY = 'secret'; - const handler = load('../api/admin/publish-agent-cards', async (text) => String(text).includes('from claim_requests') ? [{ claim_id: 'clm_1', status: 'created' }] : []); - const res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1' } }, res); - assert.equal(res.statusCode, 409); - assert.equal(res.body.status, 'CLAIM_NOT_APPROVED'); -}); - -test('approved claim publishes cards with status update last', async () => { - process.env.ADMIN_API_KEY = 'secret'; - const calls = []; - const handler = load('../api/admin/publish-agent-cards', async (text) => { - const q = String(text); calls.push(q); - if (q.includes('from claim_requests')) return [{ claim_id: 'clm_1', status: 'approved' }]; - if (q.includes('from claim_agents')) return [{ id: 'a1', claim_id: 'clm_1', ens: 'a.signagent.eth', capability: 'trust-verification', tenant: 'commandlayer' }]; - if (q.includes('from agent_cards')) return []; - return []; - }); - const res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1' } }, res); - assert.equal(res.statusCode, 200); - const idxStatus = calls.findIndex((q) => q.includes("update claim_requests set status = 'cards_published'")); - const idxEvent = calls.findIndex((q) => q.includes('insert into claim_events')); - const idxTrans = calls.findIndex((q) => q.includes('insert into claim_status_transitions')); - assert.ok(idxStatus > idxEvent); - assert.ok(idxStatus > idxTrans); -}); - -test('cards_published with existing complete cards returns existing', async () => { - process.env.ADMIN_API_KEY = 'secret'; - const handler = load('../api/admin/publish-agent-cards', async (text) => { - const q = String(text); - if (q.includes('from claim_requests')) return [{ claim_id: 'clm_1', status: 'cards_published' }]; - if (q.includes('from claim_agents')) return [{ id: 'a1', ens: 'a.signagent.eth', card_url: 'u', card_status: 'published' }]; - if (q.includes('from agent_cards')) return [{ ens: 'a.signagent.eth', card_url: 'u' }]; - return []; - }); - const res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1' } }, res); - assert.equal(res.statusCode, 200); - assert.equal(res.body.status, 'CARDS_ALREADY_PUBLISHED'); -}); - -test('cards_published with missing cards repairs successfully', async () => { - process.env.ADMIN_API_KEY = 'secret'; - const handler = load('../api/admin/publish-agent-cards', async (text) => { - const q = String(text); - if (q.includes('from claim_requests')) return [{ claim_id: 'clm_1', status: 'cards_published' }]; - if (q.includes('from claim_agents')) return [{ id: 'a1', ens: 'a.signagent.eth', capability: 'trust-verification', tenant: 'commandlayer' }]; - if (q.includes('from agent_cards')) return []; - if (q.includes('select id from claim_events')) return []; - if (q.includes('select id from claim_status_transitions')) return []; - return []; - }); - const res = makeRes(); - await handler({ method: 'POST', headers: { authorization: 'Bearer secret' }, body: { claimId: 'clm_1' } }, res); - assert.equal(res.statusCode, 200); - assert.equal(res.body.status, 'AGENT_CARDS_REPAIRED'); -}); - -test('public card route supports ens and pretty path lookup with .json stripping', async () => { - const queries = []; - const handlerOk = load('../api/agent-cards/card', async (text, params) => { - queries.push(params); - return [{ card_json: { ok: true } }]; - }); - - const ensRes = makeRes(); - await handlerOk({ method: 'GET', query: { ens: 'tenant.approveagent.eth' } }, ensRes); - assert.equal(ensRes.statusCode, 200); - assert.equal(queries[0][0], 'tenant.approveagent.eth'); - - const pathRes = makeRes(); - await handlerOk({ method: 'GET', query: { path: '/agent-cards/agents/v1.1.0/trust/tenant.approveagent.eth.json' } }, pathRes); - assert.equal(pathRes.statusCode, 200); - assert.equal(queries[1][0], 'tenant.approveagent.eth'); - - const missRes = makeRes(); - const handlerMissing = load('../api/agent-cards/card', async () => []); - await handlerMissing({ method: 'GET', query: { path: '/agent-cards/agents/v1.1.0/trust/missing.approveagent.eth.json' } }, missRes); - assert.equal(missRes.statusCode, 404); -}); diff --git a/tests/api-claim-commandlayer-namespace.test.js b/tests/api-claim-commandlayer-namespace.test.js deleted file mode 100644 index 3bf8901..0000000 --- a/tests/api-claim-commandlayer-namespace.test.js +++ /dev/null @@ -1,227 +0,0 @@ -'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 validBody() { - return { - authenticatedAddress: '0x0000000000000000000000000000000000000001', - tenant: 'acme', - activationMode: 'cl', - packId: 'trust', - capabilities: ['sign', 'attest', 'authorize', 'approve', 'reject', 'permit', 'grant', 'authenticate', 'endorse', 'verify'], - agents: [ - { ens: 'acme.signagent.eth', capability: 'sign', canonicalParent: 'signagent.eth', skill: 'trust-verification.sign', skillFamily: 'trust-verification' } - ], - publicKey: 'ed25519:abc123', - kid: 'kid123', - verifier: 'https://runtime.commandlayer.org/verify', - runtime: 'https://runtime.commandlayer.org', - schemaVersion: '1.1.0' - }; -} - -function loadHandlerWithMockQuery(mockQuery) { - const handlerPath = require.resolve('../api/claim/commandlayer-namespace'); - 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('../api/claim/commandlayer-namespace'); -} - -test('rejects non-POST', async () => { - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const res = makeRes(); - await handler({ method: 'GET', body: validBody() }, res); - assert.equal(res.statusCode, 405); -}); - -test('missing DATABASE_URL returns STORAGE_UNAVAILABLE for valid payload', async () => { - const original = process.env.DATABASE_URL; - delete process.env.DATABASE_URL; - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const res = makeRes(); - await handler({ method: 'POST', body: validBody() }, res); - assert.equal(res.statusCode, 503); - assert.equal(res.body.status, 'STORAGE_UNAVAILABLE'); - process.env.DATABASE_URL = original; -}); - -test('invalid tenant fails before DB', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - let dbCalled = false; - const handler = loadHandlerWithMockQuery(async () => { dbCalled = true; return { rows: [] }; }); - const body = validBody(); - body.tenant = 'Acme'; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'invalid_tenant'); - assert.equal(dbCalled, false); -}); - -test('unsupported pack fails before DB', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - let dbCalled = false; - const handler = loadHandlerWithMockQuery(async () => { dbCalled = true; return { rows: [] }; }); - const body = validBody(); - body.packId = 'commerce'; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'unsupported_pack'); - assert.equal(dbCalled, false); -}); - -test('valid payload with mocked DB returns CLAIM_REQUEST_CREATED', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const calls = []; - const handler = loadHandlerWithMockQuery(async (text, params) => { - calls.push({ text, params }); - return { rows: [] }; - }); - - const res = makeRes(); - await handler({ method: 'POST', body: validBody() }, res); - assert.equal(res.statusCode, 200); - assert.equal(res.body.ok, true); - assert.equal(res.body.status, 'CLAIM_REQUEST_CREATED'); - assert.match(res.body.claimId, /^clm_[a-f0-9]{32}$/); - assert.equal(Array.isArray(res.body.agents), true); - assert.equal(calls.length >= 5, true); -}); - - -test('user-owned ENS activationMode rejected by commandlayer namespace endpoint', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const body = validBody(); - body.activationMode = 'own'; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'invalid_activation_mode'); -}); -test('single activationMode rejected by commandlayer namespace endpoint', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const body = validBody(); - body.activationMode = 'single'; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'invalid_activation_mode'); -}); - -test('agent.ens with wrong parent rejected', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const body = validBody(); - body.agents[0].ens = 'acme.approve.someone.eth'; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'invalid_agent_ens'); -}); - -test('unsupported canonical parent rejected', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const body = validBody(); - body.agents[0].canonicalParent = 'searchagent.eth'; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'invalid_agent_mapping'); -}); - -test('tenant containing .eth rejected', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const body = validBody(); - body.tenant = 'acme.eth'; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'invalid_tenant'); -}); -test('tenant starting with hyphen rejected', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const body = validBody(); - body.tenant = '-acme'; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'invalid_tenant'); -}); -test('tenant ending with hyphen rejected', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const body = validBody(); - body.tenant = 'acme-'; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'invalid_tenant'); -}); -test('mismatched capability/canonical parent rejected', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const body = validBody(); - body.agents[0].capability = 'verify'; - body.agents[0].canonicalParent = 'signagent.eth'; - body.agents[0].skill = 'trust-verification.verify'; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'invalid_agent_mapping'); -}); -test('skill mismatch rejected', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const body = validBody(); - body.agents[0].skill = 'trust-verification.attest'; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'invalid_skill'); -}); -test('too many namespaces rejected', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const handler = loadHandlerWithMockQuery(async () => ({ rows: [] })); - const body = validBody(); - body.capabilities = ['a','b','c','d','e','f','g','h','i','j','k']; - const res = makeRes(); - await handler({ method: 'POST', body }, res); - assert.equal(res.statusCode, 400); - assert.equal(res.body.error, 'invalid_capabilities'); -}); -test('claim.created event insertion is attempted', async () => { - process.env.DATABASE_URL = 'postgres://example.com/db'; - const calls = []; - const handler = loadHandlerWithMockQuery(async (text, params) => { - calls.push({ text, params }); - return { rows: [] }; - }); - - const res = makeRes(); - await handler({ method: 'POST', body: validBody() }, res); - - const eventInsert = calls.find((entry) => String(entry.text).includes('insert into claim_events')); - assert.ok(eventInsert); - assert.equal(eventInsert.params[1], 'claim.created'); - assert.equal(eventInsert.params[2], 'CommandLayer namespace claim request created.'); -}); diff --git a/tests/api-payments.test.js b/tests/api-payments.test.js deleted file mode 100644 index 59b2dbb..0000000 --- a/tests/api-payments.test.js +++ /dev/null @@ -1,70 +0,0 @@ -'use strict'; -const test = require('node:test'); -const assert = require('node:assert/strict'); - -function makeRes(){return{statusCode:200,headers:{},body:null,setHeader(n,v){this.headers[n.toLowerCase()]=v;},status(c){this.statusCode=c;return this;},json(p){this.body=p;return this;}}} -function normalizeRows(r){if(Array.isArray(r))return r;if(r&&Array.isArray(r.rows))return r.rows;return []} - -function loadCheckout(mockQuery, stripeMock){ - const p = require.resolve('../api/admin/create-checkout-session'); - const dbp = require.resolve('../lib/db'); - delete require.cache[p]; delete require.cache[dbp]; - require.cache[dbp]={exports:{query:mockQuery,normalizeRows,getDatabaseUrl:()=>process.env.DATABASE_URL}}; - const sp=require.resolve('../lib/stripe-client'); delete require.cache[sp]; require.cache[sp]={exports:stripeMock}; - return require('../api/admin/create-checkout-session'); -} -function loadWebhook(mockQuery, stripeMock){ - const p = require.resolve('../api/stripe/webhook'); - const dbp = require.resolve('../lib/db'); - delete require.cache[p]; delete require.cache[dbp]; - require.cache[dbp]={exports:{query:mockQuery,normalizeRows,getDatabaseUrl:()=>process.env.DATABASE_URL}}; - const sp=require.resolve('../lib/stripe-client'); delete require.cache[sp]; require.cache[sp]={exports:stripeMock}; - return require('../api/stripe/webhook'); -} - -const StripeMock = function(){return{checkout:{sessions:{create:async()=>({id:'cs_1',url:'https://checkout.stripe.test/cs_1'})}},webhooks:{constructEvent:()=>({})}}}; - -function createStripeRecorder(record){ - return function(){ - return { - checkout: { - sessions: { - create: async(payload)=>{ - record(payload); - return {id:'cs_1',url:'https://checkout.stripe.test/cs_1'}; - } - } - } - }; - }; -} - -test('missing STRIPE_SECRET_KEY returns STRIPE_NOT_CONFIGURED', async()=>{delete process.env.STRIPE_SECRET_KEY; process.env.ADMIN_API_KEY='k'; const h=loadCheckout(()=>[],()=>{const e=new Error('missing');e.code='STRIPE_NOT_CONFIGURED';throw e;}); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1'}},r); assert.equal(r.statusCode,503); assert.equal(r.body.status,'STRIPE_NOT_CONFIGURED');}); -test('pk_ STRIPE_SECRET_KEY returns STRIPE_SECRET_KEY_INVALID', async()=>{process.env.ADMIN_API_KEY='k'; const h=loadCheckout(()=>[],()=>{const e=new Error('bad');e.code='STRIPE_SECRET_KEY_INVALID';throw e;}); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1'}},r); assert.equal(r.statusCode,503); assert.equal(r.body.status,'STRIPE_SECRET_KEY_INVALID');}); -test('unauthorized admin checkout rejected', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; const h=loadCheckout(async()=>[],StripeMock); const r=makeRes(); await h({method:'POST',headers:{},body:{claimId:'clm_1'}},r); assert.equal(r.statusCode,401);}); -test('claim not cards_published rejected', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; const h=loadCheckout(async(q)=>String(q).includes('from claim_requests')?[{claim_id:'clm_1',status:'approved'}]:[],StripeMock); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1'}},r); assert.equal(r.body.status,'CLAIM_NOT_READY_FOR_PAYMENT');}); -test('stripe session creation failure does not update claim status', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; const calls=[]; const h=loadCheckout(async(q)=>{calls.push(String(q)); if(String(q).includes('from claim_requests')) return [{claim_id:'clm_1',status:'cards_published',tenant:'commandlayer',pack_id:'founding'}]; return [];},function(){return{checkout:{sessions:{create:async()=>{const e=new Error('boom');e.code='api_error';throw e;}}}}}); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1'}},r); assert.equal(r.body.status,'CHECKOUT_SESSION_CREATE_FAILED'); assert.equal(calls.some(c=>c.includes("set status = 'payment_pending'")),false);}); -test('cards_published claim creates checkout and returns checkoutUrl', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; const calls=[]; const h=loadCheckout(async(q,p)=>{calls.push(String(q)); if(String(q).includes('from claim_requests')) return [{claim_id:'clm_1',status:'cards_published',tenant:'commandlayer',pack_id:'founding'}]; return [];},StripeMock); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1'}},r); assert.equal(r.body.status,'CHECKOUT_SESSION_CREATED'); assert.equal(r.body.checkoutUrl,'https://checkout.stripe.test/cs_1'); assert.ok(calls.some(c=>c.includes("set status = 'payment_pending'"))); assert.ok(calls.some(c=>c.includes('insert into claim_events'))); assert.ok(calls.some(c=>c.includes('cards_published')&&c.includes('payment_pending')));}); - - -test('missing COMMANDLAYER_SITE_URL defaults to https://www.commandlayer.org', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; delete process.env.COMMANDLAYER_SITE_URL; let payload=null; const h=loadCheckout(async(q)=>String(q).includes('from claim_requests')?[{claim_id:'clm_1',status:'cards_published',tenant:'commandlayer',pack_id:'founding'}]:[],createStripeRecorder((p)=>{payload=p;})); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1'}},r); assert.equal(r.statusCode,200); assert.equal(payload.success_url,'https://www.commandlayer.org/claim/status.html?claimId=clm_1&payment=success'); assert.equal(payload.cancel_url,'https://www.commandlayer.org/claim/status.html?claimId=clm_1&payment=cancelled');}); - -test('valid https://www.commandlayer.org site URL works and trims trailing slash', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; process.env.COMMANDLAYER_SITE_URL=' https://www.commandlayer.org/ '; let payload=null; const h=loadCheckout(async(q)=>String(q).includes('from claim_requests')?[{claim_id:'clm_1',status:'cards_published',tenant:'commandlayer',pack_id:'founding'}]:[],createStripeRecorder((p)=>{payload=p;})); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1'}},r); assert.equal(r.statusCode,200); assert.equal(payload.success_url,'https://www.commandlayer.org/claim/status.html?claimId=clm_1&payment=success'); assert.equal(payload.cancel_url,'https://www.commandlayer.org/claim/status.html?claimId=clm_1&payment=cancelled');}); - -test('COMMANDLAYER_SITE_URL with comma is rejected', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; process.env.COMMANDLAYER_SITE_URL='www.commandlayer.org,https://www.commandlayer.org'; const h=loadCheckout(async(q)=>String(q).includes('from claim_requests')?[{claim_id:'clm_1',status:'cards_published',tenant:'commandlayer',pack_id:'founding'}]:[],StripeMock); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1'}},r); assert.equal(r.statusCode,503); assert.equal(r.body.status,'SITE_URL_INVALID');}); - -test('http COMMANDLAYER_SITE_URL is rejected', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; process.env.COMMANDLAYER_SITE_URL='http://www.commandlayer.org'; const h=loadCheckout(async(q)=>String(q).includes('from claim_requests')?[{claim_id:'clm_1',status:'cards_published',tenant:'commandlayer',pack_id:'founding'}]:[],StripeMock); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1'}},r); assert.equal(r.statusCode,503); assert.equal(r.body.status,'SITE_URL_INVALID');}); - -test('unrelated COMMANDLAYER_SITE_URL domain is rejected', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; process.env.COMMANDLAYER_SITE_URL='https://example.com'; const h=loadCheckout(async(q)=>String(q).includes('from claim_requests')?[{claim_id:'clm_1',status:'cards_published',tenant:'commandlayer',pack_id:'founding'}]:[],StripeMock); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1'}},r); assert.equal(r.statusCode,503); assert.equal(r.body.status,'SITE_URL_INVALID');}); -test('webhook invalid signature rejected', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.STRIPE_WEBHOOK_SECRET='wh'; const Bad=function(){return{webhooks:{constructEvent:()=>{throw new Error('bad')}}}}; const h=loadWebhook(async()=>[],Bad); const r=makeRes(); await h({method:'POST',headers:{'stripe-signature':'x'},body:'{}'},r); assert.equal(r.body.status,'WEBHOOK_SIGNATURE_INVALID');}); - -test('checkout.session.completed marks claim paid + idempotent', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.STRIPE_WEBHOOK_SECRET='wh'; let paid=false; const calls=[]; const Stripe=function(){return{webhooks:{constructEvent:()=>({type:'checkout.session.completed',data:{object:{id:'cs_1',payment_intent:'pi_1',metadata:{claimId:'clm_1'}}}})}}}; const h=loadWebhook(async(q)=>{calls.push(String(q)); if(String(q).includes('from claim_requests')) return [paid?{claim_id:'clm_1',status:'paid',payment_status:'paid'}:{claim_id:'clm_1',status:'payment_pending'}]; if(String(q).includes("set status = 'paid'")) paid=true; return [];},Stripe); let r=makeRes(); await h({method:'POST',headers:{'stripe-signature':'x'},body:'{}'},r); assert.equal(r.body.ok,true); assert.ok(calls.some(c=>c.includes('payment.completed'))); - r=makeRes(); await h({method:'POST',headers:{'stripe-signature':'x'},body:'{}'},r); assert.equal(r.body.ok,true); -}); - - -test('payment_pending without forceNew returns existing checkout', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; process.env.COMMANDLAYER_SITE_URL='https://www.commandlayer.org'; let sessionCreateCalled=false; const h=loadCheckout(async(q)=>String(q).includes('from claim_requests')?[{claim_id:'clm_1',status:'payment_pending',stripe_checkout_session_id:'cs_existing',stripe_checkout_url:'https://checkout.stripe.test/existing'}]:[],function(){return{checkout:{sessions:{create:async()=>{sessionCreateCalled=true;return {id:'cs_new',url:'https://checkout.stripe.test/cs_new'};}}}};}); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1'}},r); assert.equal(r.statusCode,200); assert.equal(r.body.status,'CHECKOUT_SESSION_CREATED'); assert.equal(r.body.sessionId,'cs_existing'); assert.equal(sessionCreateCalled,false);}); - -test('payment_pending with forceNew creates a new Stripe session + regenerated event + no duplicate transition', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; process.env.COMMANDLAYER_SITE_URL='https://www.commandlayer.org'; const calls=[]; const h=loadCheckout(async(q)=>{calls.push(String(q)); if(String(q).includes('from claim_requests')) return [{claim_id:'clm_1',status:'payment_pending',tenant:'commandlayer',pack_id:'founding',stripe_checkout_session_id:'cs_old'}]; return [];},StripeMock); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1',forceNew:true}},r); assert.equal(r.statusCode,200); assert.equal(r.body.status,'CHECKOUT_SESSION_REGENERATED'); assert.ok(calls.some(c=>c.includes('insert into claim_events'))); assert.equal(calls.some(c=>c.includes('cards_published')&&c.includes('payment_pending')),false);}); - -test('paid claim cannot regenerate checkout', async()=>{process.env.STRIPE_SECRET_KEY='sk';process.env.ADMIN_API_KEY='k'; process.env.COMMANDLAYER_SITE_URL='https://www.commandlayer.org'; const h=loadCheckout(async(q)=>String(q).includes('from claim_requests')?[{claim_id:'clm_1',status:'paid',payment_status:'paid'}]:[],StripeMock); const r=makeRes(); await h({method:'POST',headers:{authorization:'Bearer k'},body:{claimId:'clm_1',forceNew:true}},r); assert.equal(r.statusCode,409); assert.equal(r.body.status,'PAYMENT_ALREADY_COMPLETED');});