Skip to content
Open
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
10 changes: 5 additions & 5 deletions __tests__/api/explorer/receipts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"
import { createX402Quote, listX402ExplorerReceipts, settleX402 } from "@/lib/protocols/x402"

describe("x402 explorer receipts", () => {
it("records accepted settlements for explorer queries", () => {
it("records accepted settlements for explorer queries", async () => {
const quote = createX402Quote({
serviceId: "data-api",
chain: "stellar",
Expand All @@ -11,14 +11,14 @@ describe("x402 explorer receipts", () => {
unitPriceUsd: 0.05,
})

const result = settleX402({
const result = await settleX402({
paymentRef: quote.paymentRef,
chain: quote.chain,
txHash: `0x${"a".repeat(64)}`,
paidBy: quote.payer,
})

const explorer = listX402ExplorerReceipts({ q: "Nexus-7", chain: "stellar" })
const explorer = await listX402ExplorerReceipts({ q: "Nexus-7", chain: "stellar" })

expect(result.ok).toBe(true)
expect(explorer.total).toBeGreaterThanOrEqual(1)
Expand All @@ -31,8 +31,8 @@ describe("x402 explorer receipts", () => {
expect(explorer.stats.totalPayments).toBeGreaterThanOrEqual(1)
})

it("paginates receipt responses", () => {
const explorer = listX402ExplorerReceipts({ page: 1, pageSize: 1 })
it("paginates receipt responses", async () => {
const explorer = await listX402ExplorerReceipts({ page: 1, pageSize: 1 })

expect(explorer.page).toBe(1)
expect(explorer.pageSize).toBe(1)
Expand Down
303 changes: 303 additions & 0 deletions __tests__/api/protocol/x402-passport-gate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'
import { POST as postSettle } from '@/app/api/protocol/x402/settle/route'
import { createX402Quote } from '@/lib/protocols/x402'
import { savePassport, type AgentPassport } from '@/lib/passport/passport'

const testAgentId = 'passport-gate-test-agent'
const collectionKey = `open-stellar:passport-collection:testnet:${testAgentId}`

// Mock localStorage for Node environment
const localStorageMock = (() => {
let store: Record<string, string> = {}
return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => { store[key] = value },
removeItem: (key: string) => { delete store[key] },
clear: () => { store = {} },
}
})()

function createTestQuote(overrides: Record<string, unknown> = {}) {
return createX402Quote({
serviceId: 'passport-gate-service',
chain: 'stellar',
payer: testAgentId,
units: 1,
unitPriceUsd: 0.1,
ttlSeconds: 300,
...overrides,
})
}

function createTestPassport(spendCap: string): AgentPassport {
return {
id: `passport-${Date.now()}`,
agentId: testAgentId,
spendCap,
registryRoot: '0x' + 'a'.repeat(64),
nullifierHash: '0x' + 'b'.repeat(64),
issuedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
status: 'ACTIVE',
network: 'testnet',
}
}

beforeEach(() => {
// Setup localStorage mock
vi.stubGlobal('localStorage', localStorageMock)
// Clear localStorage before each test
localStorageMock.removeItem(collectionKey)
})

afterEach(() => {
// Clean up after each test
localStorageMock.removeItem(collectionKey)
vi.unstubAllGlobals()
})

describe('POST /api/protocol/x402/settle with passport gate', () => {
it('rejects settlement when agentId is provided but passport does not exist', async () => {
const quote = createTestQuote()
const mockTxHash = `0x${'a'.repeat(64)}`

const req = new Request('http://localhost/api/protocol/x402/settle', {
method: 'POST',
body: JSON.stringify({
paymentRef: quote.paymentRef,
chain: 'stellar',
txHash: mockTxHash,
agentId: testAgentId,
}),
headers: { 'Content-Type': 'application/json' },
})

const res = await postSettle(req)
const data = await res.json()

expect(res.status).toBe(402)
expect(data.ok).toBe(false)
expect(data.error).toMatch(/passport/i)
expect(data.gate).toBeDefined()
expect(data.gate.authorized).toBe(false)
})

it('rejects settlement when payment amount exceeds passport spend cap', async () => {
// Create a passport with low spend cap
const passport = createTestPassport('1000000') // 0.1 XLM
savePassport(passport)

// Create a quote that requires more than the cap
const quote = createTestQuote({ unitPriceUsd: 1.0 }) // Higher price
const mockTxHash = `0x${'b'.repeat(64)}`

const req = new Request('http://localhost/api/protocol/x402/settle', {
method: 'POST',
body: JSON.stringify({
paymentRef: quote.paymentRef,
chain: 'stellar',
txHash: mockTxHash,
agentId: testAgentId,
}),
headers: { 'Content-Type': 'application/json' },
})

const res = await postSettle(req)
const data = await res.json()

expect(res.status).toBe(402)
expect(data.ok).toBe(false)
expect(data.error).toMatch(/passport/i)
expect(data.gate).toBeDefined()
expect(data.gate.authorized).toBe(false)
expect(data.gate.reason).toMatch(/exceeds/i)
})

it('approves settlement when payment amount is within passport spend cap', async () => {
// Create a passport with sufficient spend cap
const passport = createTestPassport('100000000') // 10 XLM
savePassport(passport)

const quote = createTestQuote({ unitPriceUsd: 0.1 })
const mockTxHash = `0x${'c'.repeat(64)}`

const req = new Request('http://localhost/api/protocol/x402/settle', {
method: 'POST',
body: JSON.stringify({
paymentRef: quote.paymentRef,
chain: 'stellar',
txHash: mockTxHash,
agentId: testAgentId,
}),
headers: { 'Content-Type': 'application/json' },
})

const res = await postSettle(req)
const data = await res.json()

expect(res.status).toBe(200)
expect(data.ok).toBe(true)
expect(data.receipt).toBeDefined()
expect(data.receipt.accepted).toBe(true)
})

it('allows settlement without passport gate when agentId is not provided', async () => {
const quote = createTestQuote({ payer: 'anonymous' })
const mockTxHash = `0x${'d'.repeat(64)}`

const req = new Request('http://localhost/api/protocol/x402/settle', {
method: 'POST',
body: JSON.stringify({
paymentRef: quote.paymentRef,
chain: 'stellar',
txHash: mockTxHash,
paidBy: 'anonymous',
}),
headers: { 'Content-Type': 'application/json' },
})

const res = await postSettle(req)
const data = await res.json()

expect(res.status).toBe(200)
expect(data.ok).toBe(true)
expect(data.receipt).toBeDefined()
expect(data.receipt.accepted).toBe(true)
})

it('rejects settlement with expired passport', async () => {
// Create an expired passport
const passport: AgentPassport = {
...createTestPassport('100000000'),
expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // Expired yesterday
}
savePassport(passport)

const quote = createTestQuote()
const mockTxHash = `0x${'e'.repeat(64)}`

const req = new Request('http://localhost/api/protocol/x402/settle', {
method: 'POST',
body: JSON.stringify({
paymentRef: quote.paymentRef,
chain: 'stellar',
txHash: mockTxHash,
agentId: testAgentId,
}),
headers: { 'Content-Type': 'application/json' },
})

const res = await postSettle(req)
const data = await res.json()

expect(res.status).toBe(402)
expect(data.ok).toBe(false)
expect(data.error).toMatch(/passport/i)
})

it('rejects settlement with revoked passport', async () => {
// Create a revoked passport
const passport: AgentPassport = {
...createTestPassport('100000000'),
status: 'REVOKED',
}
savePassport(passport)

const quote = createTestQuote()
const mockTxHash = `0x${'f'.repeat(64)}`

const req = new Request('http://localhost/api/protocol/x402/settle', {
method: 'POST',
body: JSON.stringify({
paymentRef: quote.paymentRef,
chain: 'stellar',
txHash: mockTxHash,
agentId: testAgentId,
}),
headers: { 'Content-Type': 'application/json' },
})

const res = await postSettle(req)
const data = await res.json()

expect(res.status).toBe(402)
expect(data.ok).toBe(false)
expect(data.error).toMatch(/passport/i)
})

it('includes spend cap in response when passport gate is triggered', async () => {
const passport = createTestPassport('5000000') // 0.5 XLM
savePassport(passport)

const quote = createTestQuote({ unitPriceUsd: 1.0 })
const mockTxHash = `0x${'1'.repeat(64)}`

const req = new Request('http://localhost/api/protocol/x402/settle', {
method: 'POST',
body: JSON.stringify({
paymentRef: quote.paymentRef,
chain: 'stellar',
txHash: mockTxHash,
agentId: testAgentId,
}),
headers: { 'Content-Type': 'application/json' },
})

const res = await postSettle(req)
const data = await res.json()

expect(data.gate).toBeDefined()
expect(data.gate.cap).toBe('5000000')
})

it('handles quoteId instead of paymentRef with passport gate', async () => {
const passport = createTestPassport('100000000')
savePassport(passport)

const quote = createTestQuote()
const mockTxHash = `0x${'2'.repeat(64)}`

const req = new Request('http://localhost/api/protocol/x402/settle', {
method: 'POST',
body: JSON.stringify({
quoteId: quote.quoteId,
chain: 'stellar',
txHash: mockTxHash,
agentId: testAgentId,
}),
headers: { 'Content-Type': 'application/json' },
})

const res = await postSettle(req)
const data = await res.json()

expect(res.status).toBe(200)
expect(data.ok).toBe(true)
expect(data.receipt).toBeDefined()
})

it('rejects settlement when quote is not found for agentId settlement', async () => {
const passport = createTestPassport('100000000')
savePassport(passport)

const mockTxHash = `0x${'3'.repeat(64)}`

const req = new Request('http://localhost/api/protocol/x402/settle', {
method: 'POST',
body: JSON.stringify({
paymentRef: 'nonexistent:payment:ref',
chain: 'stellar',
txHash: mockTxHash,
agentId: testAgentId,
}),
headers: { 'Content-Type': 'application/json' },
})

const res = await postSettle(req)
const data = await res.json()

expect(res.status).toBe(400)
expect(data.ok).toBe(false)
expect(data.error).toMatch(/quote not found/i)
})
})
2 changes: 1 addition & 1 deletion __tests__/api/protocol/x402-receipts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("GET /api/protocol/x402/receipts", () => {
units: 1,
unitPriceUsd: 0.01,
})
const settlement = settleX402({
const settlement = await settleX402({
paymentRef: quote.paymentRef,
chain: quote.chain,
txHash: `0x${"b".repeat(64)}`,
Expand Down
2 changes: 1 addition & 1 deletion __tests__/api/skills/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ describe('Skills API', () => {
})

// Settle payment
const settlement = settleX402({
const settlement = await settleX402({
paymentRef: quote.paymentRef,
chain: 'stellar',
txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
Expand Down
16 changes: 5 additions & 11 deletions app/api/admin/agents/route.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
import { NextResponse } from "next/server"
import { cloudConfigToAgent, listCloudAgentConfigs, provisionCloudAgent } from "@/lib/agent-runtime/cloud-agents"
import { isAuthorized } from "@/lib/auth"
import { withApiKeyAuth } from "@/lib/auth/api-key-middleware"

export const dynamic = "force-dynamic"

export async function GET(req: Request) {
if (!isAuthorized(req)) {
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 })
}
export const GET = withApiKeyAuth(async (req: Request) => {
const configs = listCloudAgentConfigs()
return NextResponse.json({ ok: true, agents: configs.map((config, index) => cloudConfigToAgent(config, index)), configs }, { headers: { "Cache-Control": "no-store" } })
}
}, { keyType: 'admin' })

export async function POST(req: Request) {
if (!isAuthorized(req)) {
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 })
}
export const POST = withApiKeyAuth(async (req: Request) => {
try {
const body = await req.json().catch(() => ({}))
const config = provisionCloudAgent({ name: body.name, model: body.model, district: body.district, queueMode: body.queueMode }, req)
return NextResponse.json({ ok: true, config, agent: cloudConfigToAgent(config, listCloudAgentConfigs().length - 1) }, { status: 201, headers: { "Cache-Control": "no-store" } })
} catch (error) {
return NextResponse.json({ ok: false, error: error instanceof Error ? error.message : "Failed provisioning cloud agent" }, { status: 400 })
}
}
}, { keyType: 'admin' })
Loading
Loading